Dados tipados no servidor
O cliente do navegador é tipado passando o seu Database gerado para
createRebaseClient<Database>() (veja Geração do SDK tipado).
As superfícies de dados do lado do servidor não têm uma chamada assim para recebê-lo:
context.dataem um callback de coleção,rebase.dataAsAdminno singletonrebase,- o
rebaseque uma função customizada ou um cron job recebe.
Então você diz ao TypeScript uma vez, para o projeto inteiro, qual Database elas servem.
Até que você faça isso, elas permanecem sem tipos: qualquer coleção com qualquer nome, linhas como
Record<string, unknown>. Nada muda para um projeto que não registra nada.
Registrar o Database
Seção intitulada “Registrar o Database”Desde 0.25
rebase generate-sdk --registerAlém do SDK em generated/sdk/, isso grava dois arquivos em
config/generated/: uma cópia de database.types.ts e um register.ts que
declara o registro:
import type { Database } from "./database.types";
declare module "@rebasepro/types" { interface Register { database: Database; }}Eles ficam em config/ porque os arquivos das suas coleções são compilados duas vezes, pelo
backend (cujo tsconfig.json inclui ../config/) e pelo próprio tsc do pacote
config, e config/ é o único diretório que ambos leem. Um
registro ali que importasse ../generated/sdk/ traria para a compilação desse pacote um arquivo de
fora de config/, o que o TypeScript recusa. Assim, um único registro tipa suas funções, seus cron jobs e os callbacks nos arquivos
das suas coleções, nas duas compilações. É apenas uma declaração: o runtime faz
exatamente o que fazia antes.
A opção é lembrada em rebase.json, como "registerDatabase": true no app
de backend, de modo que todo rebase generate-sdk posterior, e o rebase dev a cada
alteração de coleção, mantém os dois arquivos atualizados. Isso vale em um clone novo e no
CI também: o .gitignore do scaffold ignora os diretórios generated/, e o
primeiro rebase generate-sdk ou rebase dev os grava de novo. Um projeto sem
rebase.json continua registrando enquanto config/generated/register.ts
estiver lá. Para voltar aos dados do servidor sem tipos, remova registerDatabase e
exclua config/generated/.
Você também pode escrever o registro você mesmo, em qualquer arquivo que o backend compile
(backend/src/rebase-env.d.ts, importando ../../generated/sdk/database.types).
Ele então alcança apenas a compilação do backend: no tsc do próprio pacote config,
context.data continua sendo o objeto sem tipos, então os callbacks precisam compilar nos dois casos.
O que ele tipa
Seção intitulada “O que ele tipa”// config/collections/orders.ts — a callbackcallbacks: { async beforeSave({ context, values }) { const { data } = await context.data.customers.find({ where: { email: ["==", "ada@example.com"] }, // `email` is a column of customers }); data[0]?.email; // completes from the customers Row
await context.data.customer.find(); // error: no collection `customer` return values; }}// backend/functions/stats.tsimport { defineFunction } from "@rebasepro/server/functions";
export default defineFunction((app, { rebase }) => { app.get("/", async (c) => { const { data } = await rebase.dataAsAdmin.orders.find({ where: { status: ["==", "paid"] }, // `status` and its values are checked }); return c.json({ paid: data.length }); });});// backend/crons/cleanup.tsimport { defineCron } from "@rebasepro/server";
export default defineCron({ name: "Cleanup", schedule: "0 3 * * *", async handler({ rebase }) { // `create` takes the collection's Insert type: required columns are required. await rebase.dataAsAdmin.auditLog.create({ action: "cleanup" }); },});Cada acessador é tipado como o do cliente do navegador: find, findById
e listen retornam o Row da coleção; where e orderBy são
indexados pelas suas colunas; include nomeia as suas relações; create recebe o seu
Insert e update o seu Update. Os acessadores são os nomes em camelCase do
arquivo gerado (order_items é orderItems).
O servidor resolve esses nomes pelas coleções registradas, então uma
coleção my-notes armazenada em uma tabela notes é context.data.myNotes.
collection(slug) com um slug que o Database declara é tipado da mesma forma:
context.data.collection("my-notes") é context.data.myNotes. Um slug que você
calcula em tempo de execução, ou um que o schema não declara, recebe o cliente
sem tipos.
Uma função, sem o registro
Seção intitulada “Uma função, sem o registro”defineFunction e defineCron também recebem o tipo como argumento, que prevalece
sobre o registro. Use-o para tipar um único arquivo, ou para experimentar dados tipados antes
de registrar:
import type { Database } from "../../generated/sdk/database.types";
export default defineFunction<Database>((app, { rebase }) => { // rebase.dataAsAdmin is typed from Database here});Helpers que recebem o objeto de dados
Seção intitulada “Helpers que recebem o objeto de dados”Um helper que recebe context.data ou rebase.dataAsAdmin nomeia o tipo
registrado, para que continue tipado e aceite ambos:
import type { RebaseSdkData, RegisteredDatabase } from "@rebasepro/types";
export async function paidOrders(data: RebaseSdkData<RegisteredDatabase>) { return data.orders.find({ where: { status: ["==", "paid"] } });}Um helper declarado com um RebaseSdkData simples mantém o tipo sem tipagem, e um
objeto de dados tipado não é atribuível a ele. Mude o parâmetro como acima quando
você registrar.