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
  • Client-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.

Your collections are read once and emitted twice — as a Drizzle schema the running server queries through, and as SQL that describes the database you meant to have. It is the SQL that the database is brought into line with, by Atlas, which diffs your desired schema against the live one and plans the change:

Collections (TypeScript)
┌─────────────────────┴─────────────────────┐
▼ ▼
rebase schema generate (the same command also
│ writes the SQL below)
▼ │
backend/src/schema.generated.ts ▼
the Drizzle schema the runtime drizzle/schema.sql ← Atlas's desired state
reads and writes rows through policies.sql ← RLS, applied separately
search.sql, vector.sql ← Atlas cannot manage these
┌─────────────┴─────────────┐
▼ ▼
rebase db push rebase db generate
Atlas plans the diff Atlas writes the diff
and applies it now to drizzle/migrations/
│ │
│ ▼
│ rebase db migrate
│ applies them in order
└─────────────┬─────────────┘
PostgreSQL

db push is the development loop; db generate + db migrate is the reviewable one, and the one to use in production. Both go through the same generated SQL, so they cannot disagree about what your collections mean. See Schema Generation for every flag.

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()
}, (table) => ([
pgPolicy("products_default_admin_read", { as: "permissive", for: "select", to: ["public"], using: sql`(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`(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`(rebase.uid() IS NULL) OR (string_to_array(rebase.roles(), ',') && ARRAY['admin'])`, withCheck: sql`(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`(rebase.uid() IS NULL) OR (string_to_array(rebase.roles(), ',') && ARRAY['admin'])` }),
])).enableRLS();
export const tables = { products };
export const enums = { };
export const relations = { };

Two things in there are worth reading twice. The id column you did not declare: every collection gets a text primary key unless a property claims isId. And the pgPolicy block: row level security is enabled on every table, and those baseline policies are what keep the trusted server context and the admin role able to read it at all — see Security Rules.

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,
"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