Schema come Codice
L’Idea Centrale
Sezione intitolata “L’Idea Centrale”In Rebase, le tue definizioni di collezione TypeScript sono l’unica fonte di verità. Da un unico set di oggetti TypeScript, Rebase genera:
- Tabelle PostgreSQL tramite la generazione dello schema Drizzle ORM
- Interfaccia Utente CRUD — moduli, tabelle, validazione, tipi di campo
- Endpoint API REST con filtraggio, ordinamento e impaginazione
- SDK tipizzato — operazioni sui dati type-safe
- Politiche RLS — Sicurezza a Livello di Riga in Postgres
Ciò significa che il tuo schema è:
- Sotto controllo di versione — ogni modifica è un commit git
- Type-safe — TypeScript rileva gli errori in fase di compilazione
- Revisionabile — le modifiche allo schema passano attraverso le pull request
- Portatile — la stessa definizione funziona su frontend, backend e CLI
Modifica Visuale con Manipolazione AST
Sezione intitolata “Modifica Visuale con Manipolazione AST”Rebase fornisce anche un editor di collezioni visuale in modalità Studio. Quando un non-sviluppatore utilizza l’editor visuale per aggiungere un campo:
- Lo Studio non modifica direttamente il database
- Invece, utilizza ts-morph per analizzare il tuo file sorgente TypeScript come un AST
- Inserisce la nuova definizione di proprietà precisamente nel blocco
properties - Tutto il codice esistente, le callback e la logica personalizzata sono preservati intatti
- Il file viene salvato, attivando il ricaricamento a caldo
Questo approccio “UI come Generatore di Codice” significa che le modifiche visuali producono lo stesso TypeScript pulito che uno sviluppatore scriverebbe a mano.
Pipeline di Generazione dello Schema
Sezione intitolata “Pipeline di Generazione dello Schema”Le tue collezioni vengono lette una volta ed emesse due volte — come uno schema Drizzle attraverso cui il server in esecuzione esegue le sue query, e come SQL che descrive il database che volevi avere. È a questo SQL che il database viene allineato, da Atlas, che confronta lo schema desiderato con quello reale e pianifica la modifica:
Collezioni (TypeScript) │ ┌─────────────────────┴─────────────────────┐ ▼ ▼ rebase schema generate (db push e db generate scrivono │ i file SQL qui sotto a ogni esecuzione, ▼ in .rebase/sql/, che non va nel commit) backend/src/schema.generated.ts │ lo schema Drizzle attraverso cui ▼ il runtime legge e scrive le righe schema.sql ← stato desiderato di Atlas policies.sql ← RLS, si applica a parte search.sql, vector.sql, ← Atlas non può gestirli triggers.sql │ ┌─────────────┴─────────────┐ ▼ ▼ rebase db push rebase db generate Atlas pianifica il diff Atlas scrive il diff e lo applica subito in drizzle/migrations/ │ │ │ ▼ │ rebase db migrate │ le applica in ordine └─────────────┬─────────────┘ ▼ PostgreSQLdb push è il ciclo di sviluppo; db generate + db migrate è quello
revisionabile, e quello da usare in produzione. Entrambi passano per lo stesso
SQL generato, quindi non possono essere in disaccordo su cosa significhino le
tue collezioni, ed entrambi lo riscrivono da zero a partire dalle collezioni
prima di leggerlo, quindi nel tuo repository non c’è nessuna copia che possa
diventare obsoleta. Ciò che va nel commit sono le migrazioni. Consulta
Generazione dello Schema per tutti i flag.
Esempio
Sezione intitolata “Esempio”Data questa collezione:
import { defineCollection } from "@rebasepro/cms-types";const productsCollection = defineCollection({ slug: "products", name: "Products", table: "products", properties: { name: { type: "string", name: "Name", validation: { required: true } }, price: { type: "number", name: "Price", columnType: "numeric" }, active: { type: "boolean", name: "Active", defaultValue: true }, createdAt: { type: "date", name: "Created", autoValue: "on_create" } }});Rebase genera questo schema Drizzle:
// schema.generated.ts// This file is auto-generated by the Rebase Drizzle generator. Do not edit manually.
import { boolean, numeric, pgPolicy, pgTable, text, timestamp } from 'drizzle-orm/pg-core';import { relations as drizzleRelations, sql } from 'drizzle-orm';
export const products = pgTable("products", { name: text("name").notNull(), price: numeric("price"), active: boolean("active").default(sql`TRUE`), createdAt: timestamp("created_at", { withTimezone: true, mode: 'string' }).default(sql`now()`), id: text("id").primaryKey().default(sql`gen_random_uuid()::text`)}, (table) => ([ pgPolicy("products_default_admin_read", { as: "permissive", for: "select", to: ["public"], using: sql`(SELECT (rebase.uid() IS NULL) OR (string_to_array(rebase.roles(), ',') && ARRAY['admin']))` }), pgPolicy("products_default_admin_write_insert", { as: "permissive", for: "insert", to: ["public"], withCheck: sql`(SELECT (rebase.uid() IS NULL) OR (string_to_array(rebase.roles(), ',') && ARRAY['admin']))` }), pgPolicy("products_default_admin_write_update", { as: "permissive", for: "update", to: ["public"], using: sql`(SELECT (rebase.uid() IS NULL) OR (string_to_array(rebase.roles(), ',') && ARRAY['admin']))`, withCheck: sql`(SELECT (rebase.uid() IS NULL) OR (string_to_array(rebase.roles(), ',') && ARRAY['admin']))` }), pgPolicy("products_default_admin_write_delete", { as: "permissive", for: "delete", to: ["public"], using: sql`(SELECT (rebase.uid() IS NULL) OR (string_to_array(rebase.roles(), ',') && ARRAY['admin']))` }),])).enableRLS();
export const tables = { products };export const enums = { };export const relations = { };Tre cose qui meritano una seconda lettura. La colonna id che non hai
dichiarato: ogni collezione ottiene una primary key text a meno che una
proprietà non dichiari isId, e il database la riempie con un uuid
(gen_random_uuid()::text), così una riga creata dal pannello di
amministrazione, dalla REST API o dall’SDK non ha bisogno di una propria
chiave. Una chiave che invii tu viene usata come fornita. Sulla 0.23 la colonna
non ha un default, e una creazione che non invia una chiave fallisce. Il blocco
pgPolicy: la row level security è abilitata su ogni tabella, e quelle
policy di base sono ciò che permette al contesto server attendibile e al
ruolo admin di poterla leggere comunque — consulta
Regole di Sicurezza. E active, che porta
il defaultValue che hai scritto come default di colonna: un
defaultValue letterale viene compilato in un vero DEFAULT, quindi anche
una riga inserita dalla REST API, da uno script di seed o da psql lo
riceve, non solo quella digitata nel pannello di amministrazione. Un default
di tipo reference o vector non è un letterale di colonna e resta un
valore a livello applicativo.
Che produce questo SQL:
-- This file is auto-generated by the Rebase DDL generator. Do not edit manually.
CREATE SCHEMA IF NOT EXISTS "rebase";
CREATE TABLE "public"."products" ( "id" TEXT PRIMARY KEY DEFAULT gen_random_uuid()::text, "name" TEXT NOT NULL, "price" NUMERIC, "active" BOOLEAN DEFAULT TRUE, "created_at" TIMESTAMP WITH TIME ZONE DEFAULT now());Policies, search columns, vector indexes and updated_at triggers are written to
files of their own and applied by the CLI in their own right — Atlas manages
none of the four.
Sicurezza e Oggetti di Database Non Mappati
Sezione intitolata “Sicurezza e Oggetti di Database Non Mappati”Quando Rebase aggiorna lo schema del database, mappa le definizioni delle collezioni TypeScript sulle tabelle del database. Per garantire che gli oggetti del database non mappati (ad esempio tabelle, viste, enum) non vengano mai eliminati o modificati, Rebase implementa diversi livelli di sicurezza:
- Filtraggio Rigido delle Tabelle (
tablesFilter): La configurazione Drizzle generata limita dinamicamente la sincronizzazione alle sole tabelle esportate nello schema generato. Eventuali tabelle non riconosciute o tabelle di sistemi legacy esistenti nel database vengono ignorate dal motore di sincronizzazione. - Restrizioni sullo Schema (
schemaFilter): La sincronizzazione del database è limitata esclusivamente allo schemapublic. Le tabelle interne del database, gli schemi personalizzati e le tabelle specifiche delle estensioni non vengono toccate. - Protezione dei Ruoli e delle Estensioni: Drizzle è configurato per non gestire i ruoli del database (
entities.roles: false) o le tabelle di supporto di estensioni come PostGIS. - Conferma Interattiva in Modalità di Sviluppo: Quando si esegue
rebase db pushin sviluppo, la CLI viene eseguita con i flag--stricte--verbose, il che assicura che gli sviluppatori debbano esaminare e approvare esplicitamente qualsiasi azione SQL distruttiva prima che venga eseguita. - Proprietà degli Indici in Base al Nome: Gli indici sono l’unico oggetto che vive su una tabella mappata, quindi il filtro sulle tabelle non può proteggerli — e un push pianificava
DROP INDEXper qualsiasi indice assente dallo stato desiderato, cioè per ogni indice scritto a mano. Ora Rebase nomina gli indici che genera<table>_<columns>_ix_<7 hex>(_ux_quando sono unici), una forma che nessun altro generatore di nomi qui produce, ed esclude dal diff tutto il resto in base al nome. Un indice scritto a mano, o arrivato tramite introspezione, non viene mai toccato; una dichiarazione che elimini rimuove comunque il suo indice, ed è l’intento. Vedi Indici.
Prossimi Passi
Sezione intitolata “Prossimi Passi”- Collezioni — Riferimento completo alla configurazione delle collezioni
- Proprietà — Mappature dettagliate dei tipi di colonna
- Indici — Dichiarare gli indici di cui le tue query hanno bisogno