Scripts e o Console
Visão geral
Seção intitulada “Visão geral”Desde 0.25 Na 0.24 não existe
openRebase nem rebase console: um script precisa construir um backend, ou
chamar a API REST de um servidor em execução.
Para olhar algumas linhas, corrigir uma, testar um filtro ou conferir o que uma política permite que um usuário veja, você não deveria precisar iniciar um servidor. O Rebase entrega os dados do projeto como um objeto, em dois lugares:
rebase console, um REPL do Node no seu terminal com o projeto aberto;openRebase, o mesmo objeto em um script ou em um teste seu.
Ambos abrem o banco de dados sem alterá-lo: nenhuma tabela, role, trigger ou
função é criada. O close() de um script encerra o que o openRebase abriu. No
banco de dados de desenvolvimento gerenciado, o rebase console inicia esse banco
quando ele não está em execução e o deixa em execução depois, como fazem os comandos
rebase db; o rebase db stop o encerra. O objeto tem o vocabulário que uma
função customizada recebe do servidor,
dataAsAdmin e sql, de modo que um trecho de código passa de uma função para
um script e para o console sem mudar.
rebase console
Seção intitulada “rebase console”Desde 0.25
Execute-o no seu projeto:
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.length3Dois nomes estão no escopo, e o await de nível superior funciona:
| Nome | O que é |
|---|---|
rebase |
O que o openRebase retorna: dataAsAdmin, sql, withAuth(user) e close() |
collections |
As coleções do projeto, carregadas da mesma forma que o rebase db push as carrega |
O console abre o projeto da mesma forma que o rebase db push: o mesmo .env,
o mesmo banco de dados e as mesmas coleções. Um projeto no banco de dados de
desenvolvimento gerenciado o tem iniciado antes. O histórico do que você digitou
fica em .rebase/console_history, que o .gitignore do scaffold já deixa de
fora.
Uma entrada encadeada por pipe roda uma linha de cada vez, cada uma depois que o
await da linha anterior foi resolvido, de modo que a transcrição acima funciona como
printf '…\n…\n' | rebase console. Tudo o que vem depois de .exit é ignorado, e os
valores só são coloridos em um terminal. Tudo o que o console registra em log vai para
o stderr.
Um trecho de código: --eval
Seção intitulada “Um trecho de código: --eval”Desde 0.25
rebase console -e "<code>" executa um único trecho de código em vez do REPL. O
valor de uma expressão, ou o return do corpo de uma função, é aguardado
(awaited) e impresso como JSON na saída padrão (stdout), de modo que a saída
pode ser encadeada em um 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"Tudo o que o console registra em log, seja qual for o LOG_LEVEL, vai para o stderr,
de modo que o stdout contém o resultado e nada mais. Um trecho que lança uma exceção
imprime o erro no stderr e termina com código 1.
Desde 0.25 Um banco de dados que
o console não consegue abrir é relatado uma única vez, e ele termina com código 1:
o banner “Cannot connect to PostgreSQL” que os comandos db imprimem, ou, no banco
de desenvolvimento gerenciado, o aviso de que ele não tem nenhuma conexão livre. Esse
banco aceita apenas algumas conexões por vez e um rebase dev em execução ocupa
algumas, então feche outro cliente ou pare o rebase dev e execute o console novamente. Na
0.24 o banner aparecia duas vezes, sob uma linha ❌ [ERROR] vazia, e um banco
gerenciado cheio imprimia read ECONNRESET com o seu stack trace como uma linha de JSON.
| Flag | O que faz |
|---|---|
--eval, -e |
Executa este código em vez do REPL, imprime seu resultado aguardado como JSON e termina com código 1 se ele lançar uma exceção |
--collections, -c |
Caminho para o diretório de coleções, se não for o do próprio projeto |
--database-url |
Abre este banco de dados em vez do banco do projeto |
--docker |
Usa o banco de dados do docker compose, iniciando-o se necessário |
openRebase
Seção intitulada “openRebase”Desde 0.25
O openRebase de @rebasepro/server-postgres é o que o console chama. Use-o em
um script, um seed, uma correção de dados ou um teste. Em um projeto gerado pelo
scaffold, coloque o script em backend/, por exemplo backend/scripts/publish-drafts.ts:
o workspace do backend é o que depende de @rebasepro/server-postgres, e um script na
pasta scripts/ da raiz não consegue importá-lo.
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();Execute-o a partir de backend/. O rebase db url imprime a URL do banco de dados que o
projeto usa, inclusive o banco de desenvolvimento gerenciado:
cd backendDATABASE_URL="$(pnpm exec rebase db url)" pnpm exec tsx scripts/publish-drafts.ts| Membro | O que faz |
|---|---|
dataAsAdmin |
Toda coleção, lida e escrita como a identidade de serviço do servidor ({ uid: "service", roles: ["admin"] }): admin, e ainda avaliada pelas suas políticas de RLS, exatamente como o rebase.dataAsAdmin em uma função |
sql(query, { params }) |
SQL puro na conexão do owner, com a mesma convenção de chamada do rebase.sql em uma função |
withAuth(user) |
Resolve para { data }: as coleções como aquele usuário as vê. Toda leitura e escrita roda como ele, filtrada pela segurança em nível de linha, como seria uma requisição HTTP desse usuário |
collections |
As coleções com que foi aberto |
close() |
Encerra o pool de conexões que o openRebase criou. É seguro chamá-lo duas vezes |
Suas opções:
| Opção | O que faz |
|---|---|
databaseUrl |
O banco de dados a abrir. O openRebase cria o pool e o close() o encerra |
connection |
Uma conexão Drizzle que você já possui (createPostgresDatabaseConnection(url).db), no lugar de databaseUrl. Ela continua sendo sua: o close() a deixa aberta |
collections |
As coleções do projeto. Um array vazio as lê do banco de dados, como faz um servidor sem arquivos de coleção, e as serve somente para leitura |
pool |
Configurações do pool, junto com databaseUrl |
dataSourceKey |
A fonte de dados de onde essas coleções são servidas. Padrão (default) |
O openRebase não lê nenhuma variável de ambiente. O DATABASE_URL acima é lido
pelo seu script, e configurações como REBASE_DB_POOL_MAX ou REALTIME_CDC não
mudam nada aqui.
Desde 0.25 A abertura não registra
nada em log no nível info, de modo que o stdout de um script contém o que o script
imprime, e tsx scripts/report.ts > report.json escreve apenas o seu JSON. Avisos e
erros vão para o stderr em desenvolvimento. A nota sobre como as requisições são
isoladas (RLS enforcement active) é registrada no nível debug; a inicialização de
um servidor ainda a imprime em info. Um banco de dados que ele não consegue alcançar
rejeita a abertura com um único erro,
Cannot connect to PostgreSQL at <host>:<port>: <reason>., e não registra nada em
log: o que imprimir fica a cargo do script. Na 0.24 essa nota era a primeira linha do stdout de
um script, e um banco de dados inalcançável registrava um banner antes do erro.
Seus acessores são tipados pelo Database que o rebase generate-sdk --register
registra, como o rebase de uma função, de modo que uma coleção, um campo ou um valor
de enum escrito errado em um script é um erro de tipo. O backend/tsconfig.json do
scaffold inclui scripts/, então o tsc --noEmit em backend/ o relata. Sem um registro, nomeie-o a cada
chamada com openRebase<Database>({ … }); OpenedRebase<Database> é o tipo a dar a um
helper que recebe um. Sem nenhum dos dois, as linhas não são tipadas, e é por isso que o
primeiro exemplo converte draft.id com String().
Conferindo o que um usuário pode ver
Seção intitulada “Conferindo o que um usuário pode ver”Desde 0.25
O withAuth é a forma mais rápida de testar uma regra de segurança contra dados reais. O
scaffold de blog, o padrão do rebase init, dá a toda coleção os
defaultSecurityRules de config/collections/index.ts: todo usuário autenticado
lê todos os posts, e só um admin escreve um. Isto confere as duas metades, 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();Execute-o da mesma forma que o primeiro script, e ele imprime true duas vezes:
cd backendDATABASE_URL="$(pnpm exec rebase db url)" pnpm exec tsx scripts/ada-check.tsUma regra que filtra linhas pelo dono é conferida da mesma forma: compare o campo de
dono de cada linha com o uid que você passou. No scaffold de blog nenhuma regra faz
isso, e o authorId de um post é o id de uma linha de autor, não de um usuário.
Um usuário é { uid, roles?, isAnonymous?, claims? }; claims é o que o
rebase.jwt() lê em uma política.
O withAuth roda como a role restrita que a inicialização de um servidor cria
(rebase_user). Contra um banco de dados em que nenhum servidor Rebase foi
iniciado, essa role não existe, e as requisições de withAuth falham com o erro
de role desconhecida do Postgres em vez de rodar sem filtro. Execute
rebase db push, ou inicie o servidor uma vez, antes.
Em um teste
Seção intitulada “Em um teste”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" }));});Salve-o como backend/scripts/posts.test.ts e execute-o a partir de backend/ com o
test runner do Node, apontando TEST_DATABASE_URL para um banco de dados em que um
servidor Rebase já tenha sido iniciado:
TEST_DATABASE_URL="$(pnpm exec rebase db url)" pnpm exec tsx --test scripts/posts.test.tsO mesmo funciona com Vitest ou Jest.
Coleções com histórico
Seção intitulada “Coleções com histórico”Desde 0.25
Uma escrita em uma coleção com history: true registra
a sua entrada da mesma forma que uma escrita do servidor. A entrada é escrita na
transação da própria linha, então uma escrita que sofre rollback não deixa
entrada alguma. Ela é registrada com a identidade com que a escrita rodou:
service para o dataAsAdmin, o uid do usuário sob o withAuth. As escritas
feitas pelo rebase console são registradas da mesma forma.
O openRebase não cria nada onde registrar. Se nenhum backend com histórico foi
iniciado no banco de dados, falta a tabela rebase.entity_history. Uma escrita em
uma coleção com histórico é então recusada com
HISTORY_UNAVAILABLE antes de
qualquer um dos seus callbacks rodar, e a mensagem diz o que está faltando. As
leituras funcionam normalmente. Inicie o backend uma vez com o histórico ativado
(o rebase dev faz isso), e as escritas passam.
As entradas são registradas, nunca podadas. A retenção é uma configuração do servidor que um script não enxerga, então o servidor as poda como de costume: quando ele escreve a entidade em seguida, e na sua varredura de hora em hora.
O que ele deixa de fora
Seção intitulada “O que ele deixa de fora”Desde 0.25
O openRebase abre a camada de dados e nada mais. Ele não inicia o tempo real,
então suas escritas não são anunciadas às consultas ao vivo. Storage, e-mail,
administração de autenticação e as suas funções não estão nele.
Quando um script precisa dessas coisas também, construa o backend sem servi-lo:
createRebaseBackend({ …, provision: false }) entrega a você o backend.rebase,
o cliente completo em processo, sem nada montado e sem nada iniciado. Veja
o backend sem um servidor.
Para testar um callback de coleção sem banco de dados algum, chame-o com
createCallContext.
Relacionado
Seção intitulada “Relacionado”- Integração com Servidor Customizado — o backend como peças públicas
- Referência da CLI — todos os comandos
rebase - Regras de Segurança — o que filtra o
withAuth