Schema als Code
Die Kernidee
Abschnitt betitelt „Die Kernidee“In Rebase sind Ihre TypeScript-Sammlungsdefinitionen die einzige Quelle der Wahrheit. Aus einem Satz von TypeScript-Objekten generiert Rebase:
- PostgreSQL-Tabellen über die Drizzle ORM-Schemaerzeugung
- CRUD-Benutzeroberfläche — Formulare, Tabellen, Validierung, Feldtypen
- REST-API-Endpunkte mit Filterung, Sortierung und Paginierung
- Typisiertes SDK — typsichere Datenoperationen
- RLS-Richtlinien — Zeilenebenen-Sicherheit in Postgres
Das bedeutet, Ihr Schema ist:
- Versionskontrolliert — jede Änderung ist ein Git-Commit
- Typsicher — TypeScript fängt Fehler zur Kompilierzeit ab
- Überprüfbar — Schemaänderungen durchlaufen Pull-Requests
- Portabel — dieselbe Definition funktioniert über Frontend, Backend und CLI hinweg
Visuelle Bearbeitung mit AST-Manipulation
Abschnitt betitelt „Visuelle Bearbeitung mit AST-Manipulation“Rebase bietet auch einen visuellen Sammlungseditor im Studio-Modus. Wenn ein Nicht-Entwickler den visuellen Editor verwendet, um ein Feld hinzuzufügen:
- Das Studio modifiziert die Datenbank nicht direkt
- Stattdessen verwendet es ts-morph, um Ihre TypeScript-Quelldatei als AST zu parsen
- Es fügt die neue Eigenschaftsdefinition präzise in den
properties-Block ein - Alle vorhandenen Code, Callbacks und benutzerdefinierte Logik bleiben unberührt erhalten
- Die Datei wird gespeichert, wodurch ein Hot-Reload ausgelöst wird
Dieser Ansatz „UI als Code-Generator“ bedeutet, dass visuelle Bearbeitungen dasselbe saubere TypeScript erzeugen, das ein Entwickler von Hand schreiben würde.
Schema-Generierungs-Pipeline
Abschnitt betitelt „Schema-Generierungs-Pipeline“Ihre Sammlungen werden einmal gelesen und zweimal ausgegeben — als Drizzle-Schema, über das der laufende Server seine Abfragen stellt, und als SQL, welche die Datenbank beschreibt, die Sie haben wollten. An diese SQL wird die Datenbank angeglichen, und zwar von Atlas, das Ihr gewünschtes Schema mit dem bestehenden vergleicht und die Änderung plant:
Sammlungen (TypeScript) │ ┌─────────────────────┴─────────────────────┐ ▼ ▼ rebase schema generate (db push und db generate schreiben │ die SQL darunter bei jedem Lauf nach ▼ .rebase/sql/, das nicht committet wird) backend/src/schema.generated.ts │ das Drizzle-Schema, über das die ▼ Runtime Zeilen liest und schreibt schema.sql ← Zielzustand für Atlas policies.sql ← RLS, separat angewendet search.sql, vector.sql, ← kann Atlas nicht verwalten triggers.sql │ ┌─────────────┴─────────────┐ ▼ ▼ rebase db push rebase db generate Atlas plant den Diff Atlas schreibt den Diff und wendet ihn sofort an nach drizzle/migrations/ │ │ │ ▼ │ rebase db migrate │ wendet sie der Reihe nach an └─────────────┬─────────────┘ ▼ PostgreSQLdb push ist die Entwicklungsschleife; db generate + db migrate ist die
überprüfbare und diejenige, die Sie in Produktion verwenden. Beide laufen über
dieselbe generierte SQL, sodass sie sich nicht darüber uneinig sein können, was
Ihre Sammlungen bedeuten, und beide schreiben sie vor dem Lesen neu aus den
Sammlungen, sodass es in Ihrem Repository keine Kopie gibt, die veralten könnte.
Was Sie committen, sind die Migrationen. Alle Flags finden Sie unter
Schema-Generierung.
Beispiel
Abschnitt betitelt „Beispiel“Gegeben diese Sammlung:
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 generiert dieses Drizzle-Schema:
// 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 = { };Drei Dinge darin lohnen es, zweimal gelesen zu werden. Die Spalte id, die Sie
nicht deklariert haben: Jede Collection erhält einen text-Primärschlüssel,
sofern keine Eigenschaft isId beansprucht, und befüllt die Datenbank sie mit
einer UUID (gen_random_uuid()::text), sodass eine über das Admin-Panel, die
REST-API oder das SDK erstellte Zeile keinen eigenen Schlüssel braucht. Ein
Schlüssel, den Sie selbst mitsenden, wird genau so verwendet. Auf 0.23 hat die Spalte keinen
Default, und ein Erstellen ohne mitgesendeten Schlüssel schlägt fehl. Der pgPolicy-Block:
Row Level Security ist auf jeder Tabelle aktiviert, und diese Basis-Policies
sind es, die dem vertrauenswürdigen Server-Kontext und der admin-Rolle
überhaupt das Lesen ermöglichen — siehe
Security Rules. Und active, das den von
Ihnen geschriebenen defaultValue als Spalten-Default trägt: Ein
literaler defaultValue wird zu einem echten DEFAULT kompiliert, sodass auch
eine von der REST-API, einem Seed-Skript oder psql eingefügte Zeile ihn
erhält, nicht nur eine, die ins Admin-Panel eingetippt wurde. Ein reference-
oder vector-Default ist kein Spalten-Literal und bleibt ein Wert auf
Anwendungsebene.
Das ergibt diese 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.
Sicherheit und nicht gemappte Datenbankobjekte
Abschnitt betitelt „Sicherheit und nicht gemappte Datenbankobjekte“Wenn Rebase das Datenbankschema aktualisiert, ordnet es TypeScript-Sammlungsdefinitionen Datenbanktabellen zu. Um sicherzustellen, dass nicht gemappte Datenbankobjekte (z. B. Tabellen, Sichten, Enums) niemals gelöscht oder geändert werden, implementiert Rebase mehrere Sicherheitsebenen:
- Strikte Tabellenfilterung (
tablesFilter): Die generierte Drizzle-Konfiguration beschränkt die Synchronisierung dynamisch auf die im generierten Schema exportierten Tabellen. Alle nicht erkannten Tabellen oder Tabellen aus Legacy-Systemen, die in der Datenbank vorhanden sind, werden von der Synchronisierungs-Engine ignoriert. - Schema-Einschränkungen (
schemaFilter): Die DB-Synchronisierung ist ausschließlich auf daspublic-Schema beschränkt. Interne Datenbanktabellen, benutzerdefinierte Schemata und erweiterungsspezifische Tabellen bleiben unberührt. - Rollen- und Erweiterungsschutz: Drizzle ist so konfiguriert, dass es keine Datenbankrollen (
entities.roles: false) oder Hilfstabellen von Erweiterungen wie PostGIS verwaltet. - Interaktive Bestätigung im Entwicklungsmodus: Beim Ausführen von
rebase db pushin der Entwicklung wird die CLI mit den Flags--strictund--verboseausgeführt. Dies stellt sicher, dass Entwickler alle destruktiven SQL-Aktionen vor der Ausführung explizit überprüfen und genehmigen müssen. - Index-Besitz über den Namen: Indizes sind das eine Objekt, das auf einer gemappten Tabelle liegt, also kann Tabellen-Filterung sie nicht schützen — und ein Push plante früher
DROP INDEXfür jeden Index, der im Zielzustand fehlte, also für jeden handgeschriebenen. Rebase benennt die von ihm erzeugten Indizes jetzt<table>_<columns>_ix_<7 hex>(_ux_beiunique), eine Form, die kein anderer Namensgeber hier erzeugt, und schließt alles Übrige namentlich vom Diff aus. Ein selbst geschriebener oder per Introspektion übernommener Index wird nie angefasst; eine gelöschte Deklaration entfernt ihren Index weiterhin, was so gewollt ist. Siehe Indizes.
Nächste Schritte
Abschnitt betitelt „Nächste Schritte“- Sammlungen — Vollständige Referenz zur Sammlungs-Konfiguration
- Eigenschaften — Detaillierte Spaltentyp-Zuordnungen
- Indizes — Die Indizes deklarieren, die Ihre Abfragen brauchen