Scripts y la consola
Visión general
Sección titulada «Visión general»Desde 0.25 En 0.24 no existe
openRebase ni rebase console: un script tiene que construir un backend, o
llamar a la API REST de un servidor en ejecución.
Para mirar unas cuantas filas, corregir una, probar un filtro o comprobar qué deja ver una política a un usuario, no deberías tener que arrancar un servidor. Rebase te da los datos del proyecto como un objeto, en dos lugares:
rebase console, un REPL de Node en tu terminal con el proyecto abierto;openRebase, el mismo objeto en un script o en una prueba tuyos.
Ambos abren la base de datos sin cambiarla: no se crea ninguna tabla, rol, trigger ni
función. El close() de un script termina lo que abrió openRebase. En la base de
datos de desarrollo gestionada, rebase console inicia esa base de datos cuando no
está en ejecución y la deja en marcha después, como hacen los comandos rebase db;
rebase db stop la detiene. El objeto tiene el
vocabulario que una función personalizada recibe del
servidor, dataAsAdmin y sql, de modo que un fragmento de código pasa sin cambios entre una función, un script
y la consola.
rebase console
Sección titulada «rebase console»Desde 0.25
Ejecútala en tu proyecto:
rebase console> const { data } = await rebase.dataAsAdmin.posts.find({ limit: 3 })> data.map(p => p.title)[ 'Draft', 'Second post', 'Hello' ]> const ada = await rebase.withAuth({ uid: "ada", roles: ["user"] })> (await ada.data.posts.find({ limit: 3 })).data.length3Hay dos nombres en el ámbito, y await de nivel superior funciona:
| Nombre | Qué es |
|---|---|
rebase |
Lo que devuelve openRebase: dataAsAdmin, sql, withAuth(user) y close() |
collections |
Las colecciones del proyecto, cargadas como las carga rebase db push |
La consola abre el proyecto como lo hace rebase db push: el mismo .env,
la misma base de datos y las mismas colecciones. Un proyecto en la base de datos
de desarrollo gestionada la tiene iniciada primero. El historial de lo que escribiste se
guarda en .rebase/console_history, que el .gitignore del scaffold ya
deja fuera.
La entrada canalizada se ejecuta línea a línea, cada una después de que el await de
la anterior se haya resuelto, así que la transcripción de arriba funciona como
printf '…\n…\n' | rebase console. Todo lo que viene después de .exit se ignora, y los
valores solo se colorean en una terminal. Todo lo que la consola registra va a stderr.
Un solo fragmento: --eval
Sección titulada «Un solo fragmento: --eval»Desde 0.25
rebase console -e "<code>" ejecuta un fragmento de código en lugar del REPL. El valor
de una expresión, o el return del cuerpo de una función, se espera con await y se imprime como
JSON en stdout, de modo que la salida se puede canalizar (pipe):
rebase console -e "rebase.dataAsAdmin.posts.find({ limit: 5 })"rebase console -e "rebase.sql('select count(*) from posts')" | jq .rebase console -e "const u = await rebase.withAuth({ uid: 'u1', roles: ['user'] }); return (await u.data.posts.find()).data"Todo lo que la consola registra, diga lo que diga LOG_LEVEL, va a stderr, de modo que
stdout contiene el resultado y nada más. Un fragmento que lanza un error imprime el error
en stderr y termina con código 1.
Desde 0.25 Una base de datos que la
consola no puede abrir se notifica una sola vez, y termina con código 1: el aviso
“Cannot connect to PostgreSQL” que imprimen los comandos db, o, en la base de
datos de desarrollo gestionada, que no le queda ninguna conexión libre. Esa base de
datos solo acepta unas pocas conexiones a la vez y un rebase dev en marcha ocupa
algunas, así que cierra otro cliente o detén rebase dev y vuelve a ejecutarla. En
la 0.24 el aviso salía dos veces, bajo una línea ❌ [ERROR] vacía, y una base de
datos gestionada llena imprimía read ECONNRESET con su stack como una sola línea
de JSON.
Opciones
Sección titulada «Opciones»| Flag | Qué hace |
|---|---|
--eval, -e |
Ejecuta este código en lugar del REPL, imprime su resultado esperado como JSON y termina con código 1 si lanza un error |
--collections, -c |
Ruta al directorio de colecciones, si no es el propio del proyecto |
--database-url |
Abre esta base de datos en lugar de la del proyecto |
--docker |
Usa la base de datos de docker compose, iniciándola si hace falta |
openRebase
Sección titulada «openRebase»Desde 0.25
openRebase de @rebasepro/server-postgres es lo que llama la consola. Úsalo
en un script, un seed, una corrección de datos o una prueba. En un proyecto scaffold,
pon el script bajo backend/, por ejemplo backend/scripts/publish-drafts.ts: el
workspace del backend es el que depende de @rebasepro/server-postgres, y un script
en la carpeta scripts/ de la raíz no puede importarlo.
import { openRebase } from "@rebasepro/server-postgres";import { collections } from "../../config/collections";
const rebase = await openRebase({ databaseUrl: process.env.DATABASE_URL!, collections });
const { data: drafts } = await rebase.dataAsAdmin.posts.find({ where: { status: ["==", "draft"] } });console.log(`${drafts.length} drafts`);for (const draft of drafts) { await rebase.dataAsAdmin.posts.update(String(draft.id), { status: "published" });}
await rebase.close();Ejecútalo desde backend/. rebase db url imprime la URL de la base de datos que usa
el proyecto, incluida la de desarrollo gestionada:
cd backendDATABASE_URL="$(pnpm exec rebase db url)" pnpm exec tsx scripts/publish-drafts.ts| Miembro | Qué hace |
|---|---|
dataAsAdmin |
Todas las colecciones, leídas y escritas con la identidad de servicio del servidor ({ uid: "service", roles: ["admin"] }): administrador, y aun así evaluado por tus políticas RLS, exactamente igual que rebase.dataAsAdmin en una función |
sql(query, { params }) |
SQL en bruto sobre la conexión del propietario, con la misma convención de llamada que rebase.sql en una función |
withAuth(user) |
Se resuelve a { data }: las colecciones tal como las ve ese usuario. Cada lectura y escritura se ejecuta como él, filtrada por la seguridad a nivel de fila, como lo sería una solicitud HTTP de ese usuario |
collections |
Las colecciones con las que se abrió |
close() |
Termina el pool de conexiones que creó openRebase. Se puede llamar dos veces sin problema |
Sus opciones:
| Opción | Qué hace |
|---|---|
databaseUrl |
La base de datos que se abre. openRebase crea el pool y close() lo termina |
connection |
Una conexión de Drizzle que ya tienes (createPostgresDatabaseConnection(url).db), en lugar de databaseUrl. Sigue siendo tuya: close() la deja abierta |
collections |
Las colecciones del proyecto. Un array vacío las lee de la base de datos en su lugar, como hace un servidor sin archivos de colección, y las sirve en modo de solo lectura |
pool |
Ajustes del pool, junto con databaseUrl |
dataSourceKey |
La fuente de datos desde la que se sirven estas colecciones. Por defecto (default) |
openRebase no lee ninguna variable de entorno. El DATABASE_URL de arriba lo lee
tu script, y ajustes como REBASE_DB_POOL_MAX o REALTIME_CDC no cambian
nada aquí.
Desde 0.25 Al abrirse no registra
nada en info, así que el stdout de un script contiene lo que imprime el script, y
tsx scripts/report.ts > report.json escribe solo su JSON. Los avisos y errores
van a stderr en desarrollo. La nota sobre cómo se aíslan las solicitudes (RLS enforcement active) se registra en debug; el arranque de un servidor la sigue
imprimiendo en info. Una base de datos a la que no puede llegar rechaza la
apertura con un solo error,
Cannot connect to PostgreSQL at <host>:<port>: <reason>., y no registra nada:
qué imprimir lo decide el script. En la 0.24 esa nota abría el stdout de un script,
y una base de datos inaccesible registraba un aviso antes del error.
Sus accesores se tipan con el Database que registra rebase generate-sdk --register,
como el rebase de una función, así que una colección, un campo o un valor de enum mal
escritos en un script son un error de tipos. El backend/tsconfig.json del scaffold
incluye scripts/, así que tsc --noEmit en backend/ lo detecta. Sin un registro, nómbralo en cada llamada
con openRebase<Database>({ … }); OpenedRebase<Database> es el tipo que hay que dar a
un helper que recibe uno. Sin ninguno de los dos, las filas no tienen tipo, por eso el
primer ejemplo convierte draft.id con String().
Comprobar qué puede ver un usuario
Sección titulada «Comprobar qué puede ver un usuario»Desde 0.25
withAuth es la forma más rápida de probar una regla de seguridad con datos reales. El
scaffold del blog, el predeterminado de rebase init, da a todas las colecciones las
defaultSecurityRules de config/collections/index.ts: todo usuario con sesión
iniciada lee todas las entradas, y solo un administrador escribe una. Esto comprueba
ambas mitades, como backend/scripts/ada-check.ts:
import { openRebase } from "@rebasepro/server-postgres";import { collections } from "../../config/collections";
const rebase = await openRebase({ databaseUrl: process.env.DATABASE_URL!, collections });const ada = await rebase.withAuth({ uid: "ada", roles: ["user"] });
// A signed-in user reads every post…const { data: seen } = await ada.data.posts.find();const { data: all } = await rebase.dataAsAdmin.posts.find();console.log(seen.length === all.length);
// …and writing one is refused: only an admin may.const refused = await ada.data.posts.create({ title: "Not an admin" }).then(() => false, () => true);console.log(refused);
await rebase.close();Ejecútalo como se ejecuta el primer script, e imprime true dos veces:
cd backendDATABASE_URL="$(pnpm exec rebase db url)" pnpm exec tsx scripts/ada-check.tsUna regla que filtra filas por propietario se comprueba de la misma forma: compara
el campo de propietario de cada fila con el uid que pasaste. En el scaffold del
blog ninguna regla lo hace, y authorId en una entrada es el id de una fila de
autor, no de un usuario.
Un usuario es { uid, roles?, isAnonymous?, claims? }; claims es lo que
lee rebase.jwt() en una política.
withAuth se ejecuta con el rol restringido que crea el arranque de un servidor (rebase_user).
Contra una base de datos en la que ningún servidor de Rebase ha arrancado, ese rol no existe, y
las solicitudes de withAuth fallan con el error de rol desconocido de Postgres en lugar de ejecutarse
sin filtrar. Ejecuta antes rebase db push, o arranca el servidor una vez.
En una prueba
Sección titulada «En una prueba»Desde 0.25
import { after, before, test } from "node:test";import assert from "node:assert/strict";import { openRebase, type OpenedRebase } from "@rebasepro/server-postgres";import { collections } from "../../config/collections";
let rebase: OpenedRebase;before(async () => { rebase = await openRebase({ databaseUrl: process.env.TEST_DATABASE_URL!, collections });});after(() => rebase.close());
test("a signed-in user reads every post and writes none", async () => { const ada = await rebase.withAuth({ uid: "ada", roles: ["user"] }); const { data: seen } = await ada.data.posts.find(); const { data: all } = await rebase.dataAsAdmin.posts.find(); assert.equal(seen.length, all.length); await assert.rejects(ada.data.posts.create({ title: "Not an admin" }));});Guárdala como backend/scripts/posts.test.ts y ejecútala desde backend/ con el
ejecutor de pruebas de Node, apuntando TEST_DATABASE_URL a una base de datos sobre
la que haya arrancado un servidor de Rebase:
TEST_DATABASE_URL="$(pnpm exec rebase db url)" pnpm exec tsx --test scripts/posts.test.tsLo mismo funciona con Vitest o Jest.
Colecciones con historial
Sección titulada «Colecciones con historial»Desde 0.25
Una escritura en una colección con history: true registra
su entrada igual que lo hace una escritura del servidor. La entrada se escribe en la
transacción de la propia fila, así que una escritura que se revierte no deja ninguna
entrada. Se registra con la identidad con la que se ejecutó la escritura: service
para dataAsAdmin, el uid del usuario con withAuth. Las escrituras desde
rebase console se registran del mismo modo.
openRebase no crea nada donde registrar. Si ningún backend con historial ha arrancado
sobre la base de datos, falta la tabla rebase.entity_history. Una escritura en una
colección con historial se rechaza entonces con
HISTORY_UNAVAILABLE antes de que se
ejecute cualquiera de sus callbacks, y el mensaje nombra lo que falta. Las lecturas
funcionan como siempre. Arranca el backend una vez con el historial activado
(rebase dev lo hace) y las escrituras pasan.
Las entradas se registran, nunca se purgan. La retención es un ajuste del servidor que un script no puede ver, así que el servidor las purga como de costumbre: cuando vuelve a escribir la entidad, y en su barrido horario.
Lo que deja fuera
Sección titulada «Lo que deja fuera»Desde 0.25
openRebase abre la capa de datos y nada más. No inicia realtime, así que
sus escrituras no se anuncian a las consultas en vivo.
Almacenamiento, correo electrónico, administración de autenticación y tus funciones no están en él.
Cuando un script también necesita esas piezas, construye el backend sin servirlo:
createRebaseBackend({ …, provision: false }) te da backend.rebase, el
cliente completo en el proceso, con nada montado y nada iniciado. Consulta
el backend sin un servidor.
Para probar un callback de colección sin ninguna base de datos, llámalo con
createCallContext.
Relacionado
Sección titulada «Relacionado»- Integración de servidor personalizado — el backend como piezas públicas
- Referencia de la CLI — cada comando
rebase - Reglas de seguridad — lo que filtra
withAuth