0.25 también cambia lo que hacen algunas llamadas existentes, sin romper la compatibilidad. Revisa
estas si tu app depende del comportamiento anterior:
Los wrappers de campos de plugins se componen. El fieldBuilder.wrap de cada plugin se renderiza,
cada uno alrededor del campo que dejaron los plugins anteriores, en lugar de solo el último.
Las anulaciones (overrides) de componentes se componen. El override con wrap: true de una colección recibe
el override del mismo componente de la app como OriginalComponent, no el integrado.
rebase.auth, rebase.admin, rebase.functions y rebase.cron en el
servidor acceden a las rutas del backend en el mismo proceso (in-process), por lo que el middleware que hayas añadido a
tu propia app Hono ya no se ejecuta sobre ellos.
El socket de tiempo real responde únicamente a su propia ruta (/ para una API en /api).
Un cliente que conecte con otra ruta en el mismo servidor recibe un 404, o tu propio
listener de upgrade.
El socket obtiene su token al igual que HTTP, por lo que client.setAuthTokenGetter()
ahora lo autentica, y tras cerrar sesión ya no recurre a la opción token.
El directorio de un bucket local con nombre se ubica junto a STORAGE_PATH, como
<STORAGE_PATH>__<key>, no junto a la ruta base del propio runtime. Si defines
STORAGE_PATH y un bucket local con nombre ya contiene archivos, el arranque nombra el
directorio antiguo, y STORAGE_PATH__<KEY> continúa sirviéndolo. Un archivo compose generado por
rebase init o rebase eject monta su volumen en /uploads. Ese volumen
contiene solo el bucket predeterminado, y el usuario de la imagen no puede escribir en él. Móntalo
en /storage con STORAGE_PATH: /storage/uploads, tal como hacen los nuevos archivos compose.
Un servicio al que llama una solicitud y que no es accesible se responde con
503 SERVICE_UNAVAILABLE, como un bucket de S3 caído o un host al que una función
hace fetch(). Antes era un 500 cuyo código era el error del socket
(ECONNREFUSED), o INTERNAL_ERROR.
rebase db push conserva las restricciones CHECK y EXCLUDE de la tabla de una
colección, mientras que el primer push tras rebase schema introspect las eliminaba.
La comprobación de destrucción de datos ahora cuenta también la eliminación de una, de modo que
rebase db migrate --refuse-destructive se detiene ante una migración que elimina una.
Elimina a mano una restricción que ya no quieras.
Estos nombres todavía funcionan, muestran una advertencia única en tiempo de ejecución y tienen un reemplazo:
Obsoleto
Usar
loadFunctionsFromDirectory, loadFunctionsWithDiagnostics
loadFunctions(dir, { onSkip })
loadCronJobsFromDirectory, loadCronJobsWithDiagnostics
loadCrons(dir, { onSkip }), luego declareCrons(crons) donde el par anterior los declaraba
client.ws.setAuthTokenGetter()
client.setAuthTokenGetter()
hook de plugin hooks.modifyCollectionAsync (nunca invocado)
hooks.modifyCollection, con los datos obtenidos antes de que <Rebase> renderice
lifecycle.onCollectionChange de plugin (nunca invocado)
context.data.collection(slug).listen(…) desde lifecycle.onMount
DatabaseAdapter.mountRoutes, BackendBootstrapper.mountRoutes (nunca invocados)
una factoría de rutas que tu app monte
:::
?count= en una lista REST ahora es el selector (switch) para el conteo detrás de meta.total:
?count=none lo omite y ?count=exact es el valor predeterminado. Cualquier otro valor se
rechaza con 400 INVALID_COUNT. Antes, ?count=5 filtraba las filas cuya
columna count era 5.
Qué cambiar: nada con el SDK, que ya envía un filtro en una
columna llamada count dentro de ?where=. Una llamada REST manual que filtre en
dicha columna debe mover el filtro allí:
GET /api/data/products?count=5
GET /api/data/products?where={"count":["==",5]}
Un SDK nuevo contra un servidor antiguo: iterate() y findAll() envían
?count=none en cada página, al igual que find({ count: "none" }). Un servidor
en 0.24 lo interpreta como un filtro en una columna llamada count. Donde la colección no
tenga dicha columna, responderá 400 UNKNOWN_FILTER_FIELD, y el SDK reintentará la lectura
sin él. Donde la colección tenga una columna de texto llamada count, el
servidor antiguo filtrará por las filas cuyo count sea 'none', sin error:
la página llegará incompleta o vacía y un recorrido finalizará antes de tiempo. Actualiza el servidor
antes que el SDK cuando una colección tenga dicha columna.
Consulta Omitir el conteo .
list_collections enviaba su listado dos veces: con formato legible en el bloque de texto, y
de nuevo como structuredContent. En un proyecto grande, ambos juntos superaban
el contexto del modelo. Ahora envía el listado una sola vez, en el bloque de texto, como
JSON compacto. Las demás herramientas conservan su structuredContent.
Qué cambiar: nada para un asistente, que lee el texto. Un programa
que leía el listado desde structuredContent debe parsear el bloque de texto en su lugar;
su contenido es el mismo. Consulta El endpoint remoto .
El punto de entrada del paquete reexportaba la tabla de traducción al español, es, lo que incluía
las cadenas en español en cada bundle compilado a partir de él. Las traducciones distintas del
inglés ahora se obtienen cuando un usuario selecciona su idioma, y es ya no se
exporta. en sigue exportándose.
Qué cambiar: nada, a menos que importaras es. Para cambiar las
cadenas del admin, pasa anulaciones (overrides) a <Rebase translations={…}>; para fijar el idioma,
pasa locale.
grep -rnE "import \{[^}]*\bes\b[^}]*\} from \" @rebasepro/app \" " --exclude-dir=node_modules .
El middleware de CORS solía responder a cada solicitud OPTIONS por sí mismo, con un
204 vacío. Ahora responde únicamente a las solicitudes previas (preflights) de CORS (un OPTIONS que incluya
Access-Control-Request-Method); cualquier otro OPTIONS llega a su ruta correspondiente. Una ruta
sin un manejador de OPTIONS responde con 404. OPTIONS /api/storage/tus ahora devuelve
las capacidades de TUS (Tus-Version, Tus-Extension, Tus-Max-Size), como
siempre indicaron las notas técnicas.
Quiénes se ven afectados: un cliente, proxy o comprobación de salud (health check) que sondeaba una ruta con
OPTIONS e interpretaba el 204 como “existe”. Las solicitudes preflight de los navegadores no
cambian.
Qué cambiar: sondear con el método real de la ruta (HEAD o GET).
Una tabla, tabla de unión (junction table) o tipo enum cuyos nombres coincidan tras la
truncación a 63 bytes de Postgres solían fallar en db push con “already exists”. Dos
propiedades que resolvían a la misma columna (myTitle y my_title), y un
target de relación escrito como string (target: "authors"), fallaban más tarde o
de forma confusa. Ahora cada uno se rechaza cuando se carga la configuración, y el mensaje
indica qué renombrar o cómo escribirlo.
Quiénes se ven afectados: un proyecto que declare dicho par. Antes tampoco podía completar
db push, por lo que esto solo adelanta el fallo.
Qué cambiar: renombra uno de los nombres en conflicto y escribe el objetivo (target) de la relación
como la colección, no como su slug:
relation: { kind: "belongsTo", target: "authors" },
relation: { kind: "belongsTo", target: () => authors },
El cliente de cron del SDK usaba por defecto /api/cron, el alias obsoleto. Ahora
llama a /api/admin/cron, donde residen todas las interfaces de administración.
Quiénes se ven afectados: un servidor (o una regla de proxy) que solo atienda la ruta antigua.
Las llamadas de cron del SDK responderán con 404 en él.
Qué cambiar: actualiza el servidor junto con el SDK, o mantén la ruta antigua
de forma explícita:
const client = createRebaseClient ({ baseUrl, cron: { cronPath: "/api/cron" } });
Al estar desconectado (offline), findAll() y iterate() devolvían la parte de la colección
que este dispositivo había almacenado en caché como si fuera la totalidad de ella, y count() contaba las
filas cacheadas. Una página que este dispositivo nunca obtuvo ahora falla con OFFLINE, y lo mismo sucede con un
count() sin un conteo en caché desde el servidor.
Quiénes se ven afectados: una app que muestre esas lecturas sin conexión.
Qué cambiar: captura el error donde la app mostraba resultados parciales y
muestra lo que tenga con una nota, o solicita una página que el dispositivo ya posea.
rows = await client.data.posts. findAll ();
if (e instanceof RebaseApiError && e.code === "OFFLINE" ) rows = cached;
ClientStorageSourceRegistry.getOrDefault(key) devolvía el origen predeterminado para
una clave que no conocía, por lo que una propiedad que nombrara un origen que el panel aún no había
cargado subía los archivos al bucket predeterminado. Ahora lanza un error.
Quiénes se ven afectados: código que llama a getOrDefault con una clave que no
registró. El propio admin ya no lo hace.
Qué cambiar: registra el origen primero, o utiliza get(key), que devuelve
undefined para una clave desconocida, en los casos donde se espere que pueda faltar un origen.
TextField declaraba maxRows, y MultiSelect declaraba onChange, name,
position, endAdornment, multiple y padding. Ninguna de ellas llegó a leerse jamás, por lo
que pasarlas no hacía nada. Se han eliminado de los tipos, y pasar una de ellas ahora es
un error de tipos.
Quiénes se ven afectados: código que pasaba alguna de ellas. Nada cambia en tiempo de ejecución.
Qué cambiar: elimina la prop. Para los cambios de un selector múltiple, usa
onValueChange.
La colección de usuarios (aquella con auth habilitado) ahora responde con un 400
INVALID_CONFLICT_TARGET ante un upsert, en todas las vías de entrada: ?on_conflict=, /bulk
con upsert: true, una operación upsert en _batch, los métodos del SDK upsert() y
createMany(…, { upsert: true }), y el socket en tiempo real. Un upsert solo
descubre durante la escritura si crea o actualiza, por lo que no aplicaba ninguno de los
dos conjuntos de reglas: sin hashing de contraseñas, normalización de emails, onCreateUser o
invitación al crear, y sin comprobación del último administrador al actualizar.
Quiénes se ven afectados: código que realiza upsert de usuarios, por ejemplo una sincronización que ejecutaba
client.data.users.upsert(user, { onConflict: ["email"] }).
Qué cambiar: busca al usuario y luego créalo o actualízalo.
const user = { email: "ada@example.com" , displayName: "Ada" };
const { data : [ found ] } = await client.data.users. find ({ where: { email: [ "==" , user.email] }, limit: 1 });
if ( ! found) await client.data.users. create (user);
else await client.data.users. update ( String (found.id), user);
Una lectura que especifica tanto page como offset ahora se rechaza. El servidor responde
400 PAGE_WITH_OFFSET, y el SDK rechaza el par antes de cualquier petición, con un
RebaseClientError que incluye el mismo código: a través de HTTP, en el mismo proceso
(rebase.data) y offline. Antes, page prevalecía y offset se descartaba
silenciosamente.
Quiénes se ven afectados: código que envía ambos, por ejemplo un find({ page, offset })
o una llamada REST con ?page=3&offset=40. Dicha lectura en la práctica ya solo leía
page.
Qué cambiar: envía solo uno de ellos. Para mantener lo que devolvía la lectura anteriormente,
elimina offset.
await client.data.products. find ({ page: 3 , offset: 40 , limit: 20 });
await client.data.products. find ({ page: 3 , limit: 20 });
En una tabla cuyo nombre tiene 42 bytes o más, las tres directivas predeterminadas de escritura del admin
(…_default_admin_write_insert, _update y _delete) ahora se nombran
<head>_<hash8>_<op>, con un máximo de 63 bytes. Postgres conserva 63 bytes de un nombre, por lo
que los tres nombres antiguos se truncaban al mismo nombre y solo existía una directiva, la de
DELETE: los administradores no podían insertar ni actualizar.
Quiénes se ven afectados: un proyecto con un nombre de tabla de 42 bytes o más, y
cualquier elemento que haga referencia a esas directivas por su nombre, como una migración o un script
que las elimine. Los nombres antiguos nunca llegaron a existir en la base de datos.
Qué cambiar: nada para la base de datos. El próximo inicio o rebase db push
eliminará la directiva truncada y creará una directiva por cada operación. Actualiza cualquier
script que mencione las directivas antiguas por su nombre.
La duración predeterminada del refresh token es ahora de 30 días en todos los puntos de entrada. Un
servidor iniciado con el runtime rebase-server (o rebase start) utilizaba 400
días cuando JWT_REFRESH_EXPIRES_IN no estaba definido; un initializeRebaseBackend
escrito a mano ya utilizaba 30.
La duración es deslizante: cada refresco emite un token válido por otros 30
días, por lo que un usuario que abre la app al menos una vez al mes se mantiene conectado. Un
usuario inactivo por más tiempo debe iniciar sesión nuevamente. Los tokens emitidos antes de la actualización conservan el
vencimiento con el que fueron emitidos hasta su siguiente refresco.
Quiénes se ven afectados: un despliegue en el runtime que nunca haya configurado
JWT_REFRESH_EXPIRES_IN, cuyos usuarios regresen con una frecuencia menor a una vez al mes.
Qué cambiar: nada, para adoptar el nuevo valor predeterminado. Para mantener el comportamiento
anterior, configúralo explícitamente:
JWT_REFRESH_EXPIRES_IN = 400d
o auth: { refreshExpiresIn: "400d" } en un backend desacoplado (ejected). Los navegadores limitan
la cookie de refresco a 400 días, por lo que un valor mayor no aporta nada en el modo de cookies.
POST /api/admin/users y una creación en la colección de usuarios a través de la API
de datos (POST /api/data/users, /bulk, un batch, create_document de MCP, un guardado
por socket) ahora rechazan una password que no cumpla la directiva con 400 WEAK_PASSWORD,
tal como ya hacían el registro, los restablecimientos y PUT /api/admin/users/:uid. No se escribe
nada y ningún hook de creación se ejecuta. En un /bulk, el mensaje comienza con
Row <n>: , como los demás errores de fila del bulk. Una creación sin contraseña no
cambia: se genera una contraseña o se envía una invitación.
Quiénes se ven afectados: código que crea usuarios con una contraseña elegida en otro lugar,
como una importación o una sincronización desde otro sistema.
Qué cambiar: para importar cuentas cuyas contraseñas se definieron en otro lugar, envía
skipPasswordPolicy: true en el cuerpo de cada creación (en cada fila de un /bulk), como
administrador o utilizando la clave de servicio:
await client.admin. createUser ({ email, password: legacyPassword, skipPasswordPolicy: true });
Desde cualquier otro emisor, el flag no omite nada. Nunca se almacena y nunca
llega a onCreateUser ni a onAdminCreateUser. La importación del panel de administración lo envía
automáticamente en las filas que contienen una password. Un AuthAdapter personalizado recibe
la decisión como el nuevo tercer argumento de prepareUserCreation
({ mayWaivePasswordPolicy }); un adaptador que lo ignore mantiene su comportamiento
actual. Para modificar la directiva en sí, pasa auth.hooks.validatePasswordStrength.
Una escritura que envíe "" para una propiedad string con validation.required ahora
responde 400 VALIDATION_CONSTRAINT, con una entrada en details.violations
{ field, code: "required" }: la misma respuesta que ante un valor ausente. Esto aplica
a creaciones y actualizaciones en todas las vías de acceso (REST, bulk, batch, upsert, el socket en
tiempo real, MCP), e incluso cuando la propiedad tiene un defaultValue, ya que un valor predeterminado
completa una clave ausente, no una vacía. El panel de administración ya escribe null
para un campo limpiado, por lo que no se ve afectado.
Quiénes se ven afectados: código que escribe "" en un campo de texto obligatorio, por
ejemplo un formulario o una importación que envía todas las columnas estén o no completas.
Qué cambiar: envía un valor, omite la clave al crear cuando deba aplicarse un defaultValue,
o elimina required de los campos que legítimamente puedan estar vacíos. Las colecciones
que declaran un beforeSave no se validan en el límite (el hook puede completar el valor),
como antes. El código del servidor que escribe en el mismo proceso (rebase.data) no se ve afectado.
Ambos eran los motores internos detrás de las llamadas del cliente.
new QueryBuilder(collection).where(…) → client.data.<collection>.where(…);
los helpers or, and, not y cond se siguen exportando.
createBackups(transport) → client.backups (mismos métodos list, download,
downloadUrl, stream). CreateBackupsOptions se ha eliminado junto con él;
BackupStream se sigue exportando.
client.ws no cambia.
const storageSource = useBackendStorageSource ({ apiUrl, getAuthToken });
// after — the client you already pass to the admin
const storageSource = client.storage; // default source
const media = client. createStorageSource ( "media" ); // a named source
El origen del cliente también envía public y context en las subidas (límites de propiedad comprobados en el servidor) y storageId para orígenes con nombre, lo cual el hook no hacía.
Elimina la clave de storage: { type: "s3" | "gcs", … }; TypeScript la reporta
como una propiedad desconocida. Nunca definió la duración de una URL recibida por un navegador.
Para cambiar cuánto tiempo funciona la URL de un archivo privado, define storageDownloadTokenTtl
(en segundos; STORAGE_DOWNLOAD_TOKEN_TTL en un despliegue en bundle). El código del servidor que
llama directamente a controller.getSignedUrl() obtiene una URL válida por una hora.
Un proyecto que declaraba solo buckets con nombre en config/resources.ts, como
bucket("media", { engine: "s3" }), tenía el primero promovido a predeterminado:
las subidas cuya propiedad no especifica ningún storageSource iban a él. Ahora el
predeterminado siempre lee su propia configuración, sea lo que sea lo que se declare:
STORAGE_TYPE, STORAGE_PATH (o ./uploads) y las S3_* / GCS_* sin sufijo.
Si esas subidas deben seguir yendo al bucket con nombre, indícalo:
export const media = bucket ( "media" , { engine: "s3" , default: true });
Sin la marca, en producción esas subidas responden
501 STORAGE_NOT_CONFIGURED hasta que el predeterminado se vincule (STORAGE_TYPE=s3 y
S3_BUCKET, o STORAGE_TYPE=gcs y GCS_BUCKET), y el arranque registra una advertencia
que nombra ambas soluciones. Los archivos que escribió el predeterminado promovido están en el bucket con nombre;
con la marca se sirven a través del predeterminado como antes. En desarrollo, los
archivos que ya están en ./uploads vuelven a servirse.
El arranque también rechaza dos orígenes que se resuelven en un mismo directorio (o uno dentro de
otro), o en un mismo bucket en un mismo endpoint. Dale al origen con nombre una ubicación
propia (STORAGE_PATH__MEDIA, S3_BUCKET__MEDIA), o márcalo como el predeterminado.