Zum Inhalt springen

Schema als Code

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

Rebase bietet auch einen visuellen Sammlungseditor im Studio-Modus. Wenn ein Nicht-Entwickler den visuellen Editor verwendet, um ein Feld hinzuzufügen:

  1. Das Studio modifiziert die Datenbank nicht direkt
  2. Stattdessen verwendet es ts-morph, um Ihre TypeScript-Quelldatei als AST zu parsen
  3. Es fügt die neue Eigenschaftsdefinition präzise in den properties-Block ein
  4. Alle vorhandenen Code, Callbacks und benutzerdefinierte Logik bleiben unberührt erhalten
  5. 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.

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
└─────────────┬─────────────┘
▼
PostgreSQL

db 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.

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.

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:

  1. 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.
  2. Schema-Einschränkungen (schemaFilter): Die DB-Synchronisierung ist ausschließlich auf das public-Schema beschränkt. Interne Datenbanktabellen, benutzerdefinierte Schemata und erweiterungsspezifische Tabellen bleiben unberührt.
  3. Rollen- und Erweiterungsschutz: Drizzle ist so konfiguriert, dass es keine Datenbankrollen (entities.roles: false) oder Hilfstabellen von Erweiterungen wie PostGIS verwaltet.
  4. Interaktive Bestätigung im Entwicklungsmodus: Beim Ausführen von rebase db push in der Entwicklung wird die CLI mit den Flags --strict und --verbose ausgeführt. Dies stellt sicher, dass Entwickler alle destruktiven SQL-Aktionen vor der Ausführung explizit überprüfen und genehmigen müssen.
  5. 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 INDEX fü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_ bei unique), 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.
  • Sammlungen — Vollständige Referenz zur Sammlungs-Konfiguration
  • Eigenschaften — Detaillierte Spaltentyp-Zuordnungen
  • Indizes — Die Indizes deklarieren, die Ihre Abfragen brauchen