@rebasepro/server-postgresql 0.6.1 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (75) hide show
  1. package/package.json +24 -18
  2. package/src/PostgresBackendDriver.ts +65 -44
  3. package/src/PostgresBootstrapper.ts +34 -2
  4. package/src/auth/ensure-tables.ts +56 -1
  5. package/src/auth/services.ts +94 -1
  6. package/src/cli-errors.ts +162 -0
  7. package/src/cli-helpers.ts +183 -0
  8. package/src/cli.ts +198 -251
  9. package/src/schema/auth-default-policies.ts +90 -0
  10. package/src/schema/auth-schema.ts +25 -2
  11. package/src/schema/doctor.ts +2 -4
  12. package/src/schema/generate-drizzle-schema-logic.ts +4 -3
  13. package/src/schema/generate-drizzle-schema.ts +3 -5
  14. package/src/schema/generate-postgres-ddl-logic.ts +524 -0
  15. package/src/schema/generate-postgres-ddl.ts +116 -0
  16. package/src/services/EntityPersistService.ts +10 -8
  17. package/src/services/entityService.ts +28 -3
  18. package/src/utils/pg-error-utils.ts +16 -0
  19. package/src/utils/table-classification.ts +16 -0
  20. package/src/websocket.ts +9 -0
  21. package/test/auth-default-policies.test.ts +89 -0
  22. package/test/cli-helpers-extended.test.ts +324 -0
  23. package/test/cli-helpers.test.ts +59 -0
  24. package/test/connection.test.ts +292 -0
  25. package/test/databasePoolManager.test.ts +289 -0
  26. package/test/doctor-extended.test.ts +443 -0
  27. package/test/e2e/db-e2e.test.ts +293 -0
  28. package/test/e2e/pg-setup.ts +79 -0
  29. package/test/entity-persist-composite-keys.test.ts +451 -0
  30. package/test/generate-postgres-ddl-edge-cases.test.ts +716 -0
  31. package/test/generate-postgres-ddl.test.ts +300 -0
  32. package/test/mfa-service.test.ts +544 -0
  33. package/test/pg-error-utils.test.ts +50 -1
  34. package/test/realtimeService-channels.test.ts +696 -0
  35. package/test/unmapped-tables-safety.test.ts +55 -342
  36. package/vitest.e2e.config.ts +10 -0
  37. package/build-errors.txt +0 -37
  38. package/dist/PostgresAdapter.d.ts +0 -6
  39. package/dist/PostgresBackendDriver.d.ts +0 -110
  40. package/dist/PostgresBootstrapper.d.ts +0 -46
  41. package/dist/auth/ensure-tables.d.ts +0 -10
  42. package/dist/auth/services.d.ts +0 -231
  43. package/dist/cli.d.ts +0 -1
  44. package/dist/collections/PostgresCollectionRegistry.d.ts +0 -47
  45. package/dist/connection.d.ts +0 -65
  46. package/dist/data-transformer.d.ts +0 -55
  47. package/dist/databasePoolManager.d.ts +0 -20
  48. package/dist/history/HistoryService.d.ts +0 -71
  49. package/dist/history/ensure-history-table.d.ts +0 -7
  50. package/dist/index.d.ts +0 -14
  51. package/dist/index.es.js +0 -10803
  52. package/dist/index.es.js.map +0 -1
  53. package/dist/interfaces.d.ts +0 -18
  54. package/dist/schema/auth-schema.d.ts +0 -2149
  55. package/dist/schema/doctor-cli.d.ts +0 -2
  56. package/dist/schema/doctor.d.ts +0 -52
  57. package/dist/schema/generate-drizzle-schema-logic.d.ts +0 -2
  58. package/dist/schema/generate-drizzle-schema.d.ts +0 -1
  59. package/dist/schema/introspect-db-inference.d.ts +0 -5
  60. package/dist/schema/introspect-db-logic.d.ts +0 -118
  61. package/dist/schema/introspect-db.d.ts +0 -1
  62. package/dist/schema/test-schema.d.ts +0 -24
  63. package/dist/services/BranchService.d.ts +0 -47
  64. package/dist/services/EntityFetchService.d.ts +0 -214
  65. package/dist/services/EntityPersistService.d.ts +0 -40
  66. package/dist/services/RelationService.d.ts +0 -98
  67. package/dist/services/entity-helpers.d.ts +0 -38
  68. package/dist/services/entityService.d.ts +0 -110
  69. package/dist/services/index.d.ts +0 -4
  70. package/dist/services/realtimeService.d.ts +0 -220
  71. package/dist/types.d.ts +0 -3
  72. package/dist/utils/drizzle-conditions.d.ts +0 -138
  73. package/dist/utils/pg-array-null-patch.d.ts +0 -16
  74. package/dist/utils/pg-error-utils.d.ts +0 -55
  75. package/dist/websocket.d.ts +0 -11
@@ -118,6 +118,18 @@ export function createAuthSchema(usersSchemaName = "rebase") {
118
118
  createdAt: timestamp("created_at").defaultNow().notNull()
119
119
  });
120
120
 
121
+ /**
122
+ * Magic link tokens for passwordless email login
123
+ */
124
+ const magicLinkTokens = tableCreator("magic_link_tokens", {
125
+ id: uuid("id").defaultRandom().primaryKey(),
126
+ userId: uuid("user_id").notNull().references(() => users.id, { onDelete: "cascade" }),
127
+ tokenHash: varchar("token_hash", { length: 255 }).notNull().unique(),
128
+ expiresAt: timestamp("expires_at").notNull(),
129
+ usedAt: timestamp("used_at"),
130
+ createdAt: timestamp("created_at").defaultNow().notNull()
131
+ });
132
+
121
133
  return {
122
134
  usersSchema,
123
135
  users,
@@ -127,7 +139,8 @@ export function createAuthSchema(usersSchemaName = "rebase") {
127
139
  userIdentities,
128
140
  mfaFactors,
129
141
  mfaChallenges,
130
- recoveryCodes
142
+ recoveryCodes,
143
+ magicLinkTokens
131
144
  };
132
145
  }
133
146
 
@@ -144,6 +157,7 @@ export const userIdentities = defaultAuthSchema.userIdentities;
144
157
  export const mfaFactors = defaultAuthSchema.mfaFactors;
145
158
  export const mfaChallenges = defaultAuthSchema.mfaChallenges;
146
159
  export const recoveryCodes = defaultAuthSchema.recoveryCodes;
160
+ export const magicLinkTokens = defaultAuthSchema.magicLinkTokens;
147
161
 
148
162
  // Relations
149
163
  export const usersRelations = relations(users, ({ many }) => ({
@@ -151,7 +165,8 @@ export const usersRelations = relations(users, ({ many }) => ({
151
165
  passwordResetTokens: many(passwordResetTokens),
152
166
  userIdentities: many(userIdentities),
153
167
  mfaFactors: many(mfaFactors),
154
- recoveryCodes: many(recoveryCodes)
168
+ recoveryCodes: many(recoveryCodes),
169
+ magicLinkTokens: many(magicLinkTokens)
155
170
  }));
156
171
 
157
172
  export const refreshTokensRelations = relations(refreshTokens, ({ one }) => ({
@@ -197,6 +212,13 @@ export const recoveryCodesRelations = relations(recoveryCodes, ({ one }) => ({
197
212
  })
198
213
  }));
199
214
 
215
+ export const magicLinkTokensRelations = relations(magicLinkTokens, ({ one }) => ({
216
+ user: one(users, {
217
+ fields: [magicLinkTokens.userId],
218
+ references: [users.id]
219
+ })
220
+ }));
221
+
200
222
  // Type exports
201
223
  export type User = typeof users.$inferSelect;
202
224
  export type NewUser = typeof users.$inferInsert;
@@ -208,3 +230,4 @@ export type NewUserIdentity = typeof userIdentities.$inferInsert;
208
230
  export type MfaFactorRow = typeof mfaFactors.$inferSelect;
209
231
  export type MfaChallengeRow = typeof mfaChallenges.$inferSelect;
210
232
  export type RecoveryCodeRow = typeof recoveryCodes.$inferSelect;
233
+ export type MagicLinkToken = typeof magicLinkTokens.$inferSelect;
@@ -143,8 +143,7 @@ export async function loadCollections(collectionsPath: string): Promise<EntityCo
143
143
  const filePath = path.join(resolvedPath, file);
144
144
  try {
145
145
  const fileUrl = pathToFileURL(filePath).href;
146
- const dynamicImport = new Function("url", "return import(url)");
147
- const mod = await dynamicImport(fileUrl);
146
+ const mod = await import(fileUrl);
148
147
  if (mod?.default) {
149
148
  collections.push(mod.default);
150
149
  }
@@ -156,8 +155,7 @@ export async function loadCollections(collectionsPath: string): Promise<EntityCo
156
155
  }
157
156
  } else {
158
157
  const fileUrl = pathToFileURL(resolvedPath).href + `?t=${Date.now()}`;
159
- const dynamicImport = new Function("url", "return import(url)");
160
- const imported = await dynamicImport(fileUrl);
158
+ const imported = await import(fileUrl);
161
159
  const loaded = imported.backendCollections || imported.collections;
162
160
  if (Array.isArray(loaded)) {
163
161
  collections.push(...loaded);
@@ -4,6 +4,7 @@ import { getEnumVarName, getTableName, getTableVarName, resolveCollectionRelatio
4
4
  import { toSnakeCase } from "@rebasepro/utils";
5
5
  import { createHash } from "crypto";
6
6
  import { logger } from "@rebasepro/server-core";
7
+ import { getEffectiveSecurityRules } from "./auth-default-policies";
7
8
  // --- Helper Functions ---
8
9
 
9
10
  /**
@@ -98,7 +99,7 @@ const getDrizzleColumn = (propName: string, prop: Property, collection: EntityCo
98
99
  columnDefinition = `uuid("${colName}")`;
99
100
  } else if (stringProp.columnType === "uuid") {
100
101
  columnDefinition = `uuid("${colName}")`;
101
- } else if (stringProp.columnType === "text") {
102
+ } else if (stringProp.columnType === "text" || stringProp.ui?.markdown || stringProp.ui?.multiline) {
102
103
  columnDefinition = `text("${colName}")`;
103
104
  } else if (stringProp.columnType === "char") {
104
105
  columnDefinition = `char("${colName}")`;
@@ -707,8 +708,8 @@ export const generateSchema = async (collections: EntityCollection[], stripPolic
707
708
 
708
709
  schemaContent += `${Array.from(columns).join(",\n")}`;
709
710
 
710
- const securityRules = isPostgresCollection(collection) ? collection.securityRules : undefined;
711
- if (!stripPolicies && securityRules && securityRules.length > 0) {
711
+ const securityRules = getEffectiveSecurityRules(collection);
712
+ if (!stripPolicies && securityRules.length > 0) {
712
713
  schemaContent += "\n}, (table) => ([\n";
713
714
  securityRules.forEach((rule: SecurityRule, idx: number) => {
714
715
  schemaContent += generatePolicyCode(collection, rule, idx);
@@ -68,8 +68,7 @@ const runGeneration = async (collectionsFilePath?: string, outputPath?: string)
68
68
  const filePath = path.join(resolvedPath, file);
69
69
  try {
70
70
  const fileUrl = pathToFileURL(filePath).href;
71
- const dynamicImport = new Function("url", "return import(url)");
72
- const module = await dynamicImport(fileUrl);
71
+ const module = await import(fileUrl);
73
72
  if (module && module.default) {
74
73
  collections.push(module.default);
75
74
  }
@@ -81,8 +80,7 @@ const runGeneration = async (collectionsFilePath?: string, outputPath?: string)
81
80
  }
82
81
  } else {
83
82
  const fileUrl = pathToFileURL(resolvedPath).href + `?t=${Date.now()}`;
84
- const dynamicImport = new Function("url", "return import(url)");
85
- const imported = await dynamicImport(fileUrl);
83
+ const imported = await import(fileUrl);
86
84
  collections = imported.backendCollections || imported.collections;
87
85
  }
88
86
 
@@ -101,7 +99,7 @@ const runGeneration = async (collectionsFilePath?: string, outputPath?: string)
101
99
  const outputDir = path.dirname(outputPath);
102
100
  await fsPromises.mkdir(outputDir, { recursive: true });
103
101
  await fsPromises.writeFile(outputPath, schemaContent);
104
- logger.info("✅ Drizzle schema generated successfully at", { detail: outputPath });
102
+ logger.info(`✅ Drizzle schema generated successfully at ${outputPath}`);
105
103
  } else {
106
104
  logger.info("✅ Drizzle schema generated successfully.");
107
105
  logger.info(String(schemaContent));
@@ -0,0 +1,524 @@
1
+ import { EntityCollection, NumberProperty, Property, Relation, RelationProperty, SecurityOperation, SecurityRule, StringProperty, isPostgresCollection, DateProperty, ArrayProperty, MapProperty, ReferenceProperty, VectorProperty, BinaryProperty } from "@rebasepro/types";
2
+ import { getEnumVarName, getTableName, resolveCollectionRelations, findRelation } from "@rebasepro/common";
3
+ import { toSnakeCase } from "@rebasepro/utils";
4
+ import { createHash } from "crypto";
5
+ import { getEffectiveSecurityRules } from "./auth-default-policies";
6
+
7
+ // --- Helper Functions ---
8
+
9
+ const resolveColumnName = (propName: string, prop?: Property | null): string => {
10
+ if (prop && "columnName" in prop && typeof prop.columnName === "string") {
11
+ return prop.columnName;
12
+ }
13
+ return toSnakeCase(propName);
14
+ };
15
+
16
+ const getPrimaryKeyProp = (collection: EntityCollection): { name: string, type: "string" | "number", isUuid: boolean } => {
17
+ if (collection.properties) {
18
+ const idPropEntry = Object.entries(collection.properties).find(([_, prop]) => "isId" in (prop as unknown as object) && Boolean((prop as unknown as Record<string, unknown>).isId));
19
+ if (idPropEntry) {
20
+ const prop = idPropEntry[1] as unknown as Property;
21
+ const isUuid = prop.type === "string" && "isId" in prop && (prop as unknown as StringProperty).isId === "uuid";
22
+ return { name: idPropEntry[0], type: prop.type === "number" ? "number" : "string", isUuid };
23
+ }
24
+ }
25
+ const idProp = collection.properties?.["id"] as unknown as Property | undefined;
26
+ if (idProp?.type === "number") {
27
+ return { name: "id", type: "number", isUuid: false };
28
+ }
29
+ const isUuid = idProp?.type === "string" && "isId" in idProp && (idProp as unknown as StringProperty).isId === "uuid";
30
+ return { name: "id", type: "string", isUuid: isUuid ?? false };
31
+ };
32
+
33
+ const isNumericId = (collection: EntityCollection): boolean => {
34
+ return getPrimaryKeyProp(collection).type === "number";
35
+ };
36
+
37
+ const getPrimaryKeyName = (collection: EntityCollection): string => {
38
+ return getPrimaryKeyProp(collection).name;
39
+ };
40
+
41
+ const isIdProperty = (propName: string, prop: Property, collection: EntityCollection): boolean => {
42
+ if ("isId" in prop && Boolean(prop.isId)) return true;
43
+ const hasExplicitId = Object.values(collection.properties ?? {}).some(p => "isId" in (p as unknown as object) && Boolean((p as unknown as Record<string, unknown>).isId));
44
+ return !hasExplicitId && propName === "id";
45
+ };
46
+
47
+ const resolveRawSql = (expression: string): string => {
48
+ return expression.replace(/\{(\w+)\}/g, (_, col) => col);
49
+ };
50
+
51
+ const unwrapSql = (sqlExpr: string): string => {
52
+ return sqlExpr;
53
+ };
54
+
55
+ const buildUsingClause = (rule: SecurityRule, collection: EntityCollection): string | null => {
56
+ if (rule.using) {
57
+ return resolveRawSql(rule.using);
58
+ }
59
+ if (rule.access === "public") {
60
+ return "true";
61
+ }
62
+ if (rule.ownerField) {
63
+ const prop = collection.properties?.[rule.ownerField];
64
+ const colName = resolveColumnName(rule.ownerField, prop);
65
+ return `${colName} = auth.uid()`;
66
+ }
67
+ return null;
68
+ };
69
+
70
+ const buildWithCheckClause = (rule: SecurityRule, collection: EntityCollection): string | null => {
71
+ if (rule.withCheck) {
72
+ return resolveRawSql(rule.withCheck);
73
+ }
74
+ return buildUsingClause(rule, collection);
75
+ };
76
+
77
+ const getPolicyNameHash = (rule: SecurityRule): string => {
78
+ const data = JSON.stringify({
79
+ a: rule.access,
80
+ m: rule.mode,
81
+ op: rule.operation,
82
+ ops: rule.operations?.slice().sort(),
83
+ own: rule.ownerField,
84
+ rol: rule.roles?.slice().sort(),
85
+ pg: rule.pgRoles?.slice().sort(),
86
+ u: rule.using,
87
+ w: rule.withCheck
88
+ });
89
+ return createHash("sha1").update(data).digest("hex").substring(0, 7);
90
+ };
91
+
92
+ const generatePolicyDdl = (collection: EntityCollection, rule: SecurityRule): string => {
93
+ const tableName = getTableName(collection);
94
+ const ops: SecurityOperation[] = rule.operations && rule.operations.length > 0
95
+ ? rule.operations
96
+ : [rule.operation ?? "all"];
97
+
98
+ const ruleHash = getPolicyNameHash(rule);
99
+
100
+ return ops.map((op, opIdx) => {
101
+ const policyName = rule.name
102
+ ? (ops.length > 1 ? `${rule.name}_${op}` : rule.name)
103
+ : `${tableName}_${op}_${ruleHash}${ops.length > 1 ? `_${opIdx}` : ""}`;
104
+
105
+ return generateSinglePolicyDdl(collection, rule, op, policyName);
106
+ }).join("");
107
+ };
108
+
109
+ const generateSinglePolicyDdl = (collection: EntityCollection, rule: SecurityRule, operation: SecurityOperation, policyName: string): string => {
110
+ const schema = isPostgresCollection(collection) && collection.schema ? collection.schema : "public";
111
+ const tableName = getTableName(collection);
112
+ const mode = (rule.mode ?? "permissive").toUpperCase();
113
+ const operationUpper = operation.toUpperCase();
114
+ const roles = rule.roles ? [...rule.roles].sort() : undefined;
115
+ const pgRoles = rule.pgRoles ? [...rule.pgRoles].sort() : ["public"];
116
+
117
+ const needsUsing = operation !== "insert";
118
+ const needsWithCheck = operation !== "select" && operation !== "delete";
119
+
120
+ let usingClause = needsUsing ? buildUsingClause(rule, collection) : null;
121
+ let withCheckClause = needsWithCheck ? buildWithCheckClause(rule, collection) : null;
122
+
123
+ if (roles && roles.length > 0) {
124
+ const rolesArrayString = `ARRAY[${roles.map(r => `'${r}'`).join(",")}]`;
125
+ const roleCondition = `string_to_array(auth.roles(), ',') && ${rolesArrayString}`;
126
+ if (usingClause) {
127
+ usingClause = `(${usingClause}) AND (${roleCondition})`;
128
+ } else if (needsUsing) {
129
+ usingClause = roleCondition;
130
+ }
131
+ if (withCheckClause) {
132
+ withCheckClause = `(${withCheckClause}) AND (${roleCondition})`;
133
+ } else if (needsWithCheck) {
134
+ withCheckClause = roleCondition;
135
+ }
136
+ }
137
+
138
+ if (!usingClause && needsUsing) {
139
+ usingClause = "false";
140
+ }
141
+ if (!withCheckClause && needsWithCheck) {
142
+ withCheckClause = "false";
143
+ }
144
+
145
+ let ddl = `DROP POLICY IF EXISTS "${policyName}" ON "${schema}"."${tableName}";\n`;
146
+ ddl += `CREATE POLICY "${policyName}" ON "${schema}"."${tableName}" AS ${mode} FOR ${operationUpper} TO ${pgRoles.map(r => `"${r}"`).join(", ")}`;
147
+ if (usingClause) ddl += ` USING (${usingClause})`;
148
+ if (withCheckClause) ddl += ` WITH CHECK (${withCheckClause})`;
149
+ return `${ddl};\n`;
150
+ };
151
+
152
+ const getSqlColumnType = (propName: string, prop: Property, collection: EntityCollection, collections: EntityCollection[]): string => {
153
+ switch (prop.type) {
154
+ case "string": {
155
+ const stringProp = prop as StringProperty;
156
+ if (stringProp.enum) {
157
+ const tableName = getTableName(collection);
158
+ const colName = resolveColumnName(propName, prop);
159
+ const schema = isPostgresCollection(collection) && collection.schema ? collection.schema : "public";
160
+ return `"${schema}"."${tableName}_${colName}"`;
161
+ }
162
+ if (stringProp.isId === "uuid" || stringProp.columnType === "uuid") {
163
+ return "UUID";
164
+ }
165
+ if (stringProp.columnType === "text" || stringProp.ui?.markdown || stringProp.ui?.multiline) {
166
+ return "TEXT";
167
+ }
168
+ if (stringProp.columnType === "char") {
169
+ return "CHAR(255)";
170
+ }
171
+ return "VARCHAR(255)";
172
+ }
173
+ case "number": {
174
+ const numProp = prop as NumberProperty;
175
+ const isId = isIdProperty(propName, prop, collection);
176
+ if ("isId" in numProp && numProp.isId === "increment") {
177
+ return "INTEGER GENERATED BY DEFAULT AS IDENTITY";
178
+ }
179
+ if (numProp.columnType) {
180
+ if (numProp.columnType === "double precision") return "DOUBLE PRECISION";
181
+ return numProp.columnType.toUpperCase();
182
+ }
183
+ return (numProp.validation?.integer || isId) ? "INTEGER" : "NUMERIC";
184
+ }
185
+ case "boolean":
186
+ return "BOOLEAN";
187
+ case "date": {
188
+ const dateProp = prop as DateProperty;
189
+ if (dateProp.columnType === "date") return "DATE";
190
+ if (dateProp.columnType === "time") return "TIME";
191
+ return "TIMESTAMP WITH TIME ZONE";
192
+ }
193
+ case "map": {
194
+ const mapProp = prop as MapProperty;
195
+ return mapProp.columnType === "json" ? "JSON" : "JSONB";
196
+ }
197
+ case "array": {
198
+ const arrayProp = prop as ArrayProperty;
199
+ let colType = arrayProp.columnType;
200
+ if (!colType && arrayProp.of && !Array.isArray(arrayProp.of)) {
201
+ const ofProp = arrayProp.of as Property;
202
+ if (ofProp.type === "string") {
203
+ colType = "text[]";
204
+ } else if (ofProp.type === "number") {
205
+ colType = ofProp.validation?.integer ? "integer[]" : "numeric[]";
206
+ } else if (ofProp.type === "boolean") {
207
+ colType = "boolean[]";
208
+ }
209
+ }
210
+ if (colType === "json") return "JSON";
211
+ if (colType === "text[]") return "TEXT[]";
212
+ if (colType === "integer[]") return "INTEGER[]";
213
+ if (colType === "boolean[]") return "BOOLEAN[]";
214
+ if (colType === "numeric[]") return "NUMERIC[]";
215
+ return "JSONB";
216
+ }
217
+ case "vector": {
218
+ const vp = prop as VectorProperty;
219
+ return `VECTOR(${vp.dimensions})`;
220
+ }
221
+ case "binary": {
222
+ return "BYTEA";
223
+ }
224
+ case "relation": {
225
+ const refProp = prop as RelationProperty;
226
+ const resolvedRelations = resolveCollectionRelations(collection);
227
+ const relation = findRelation(resolvedRelations, refProp.relationName ?? propName);
228
+ if (!relation || relation.direction !== "owning" || relation.cardinality !== "one") {
229
+ throw new Error(`Relation ${propName} is not an owning one-to-one/many-to-one relation`);
230
+ }
231
+ let targetCollection: EntityCollection;
232
+ try {
233
+ targetCollection = relation.target();
234
+ } catch {
235
+ return "VARCHAR(255)";
236
+ }
237
+ const pkProp = getPrimaryKeyProp(targetCollection);
238
+ return pkProp.type === "number" ? "INTEGER" : (pkProp.isUuid ? "UUID" : "VARCHAR(255)");
239
+ }
240
+ case "reference": {
241
+ const refProp = prop as ReferenceProperty;
242
+ const targetCollection = collections.find(c => c.slug === refProp.path || getTableName(c) === refProp.path);
243
+ if (!targetCollection) return "VARCHAR(255)";
244
+ const pkProp = getPrimaryKeyProp(targetCollection);
245
+ return pkProp.type === "number" ? "INTEGER" : (pkProp.isUuid ? "UUID" : "VARCHAR(255)");
246
+ }
247
+ default:
248
+ return "VARCHAR(255)";
249
+ }
250
+ };
251
+
252
+ export const generatePostgresDdl = async (
253
+ collections: EntityCollection[],
254
+ options: { includePolicies?: boolean } = { includePolicies: true }
255
+ ): Promise<string> => {
256
+ let ddl = "-- This file is auto-generated by the Rebase DDL generator. Do not edit manually.\n\n";
257
+
258
+ // 1. Create custom schemas
259
+ const uniqueSchemas = Array.from(new Set([
260
+ "auth",
261
+ ...collections.map(c => isPostgresCollection(c) ? c.schema : undefined).filter(Boolean)
262
+ ]));
263
+ uniqueSchemas.forEach(schema => {
264
+ if (schema) ddl += `CREATE SCHEMA IF NOT EXISTS "${schema}";\n`;
265
+ });
266
+ if (uniqueSchemas.length > 0) ddl += "\n";
267
+
268
+ // 2. Generate Enums
269
+ collections.forEach(collection => {
270
+ const collectionTable = getTableName(collection);
271
+ const schema = isPostgresCollection(collection) && collection.schema ? collection.schema : "public";
272
+ Object.entries(collection.properties ?? {}).forEach(([propName, prop]) => {
273
+ if (("enum" in prop) && (prop.type === "string" || prop.type === "number") && prop.enum) {
274
+ const enumDbName = `${collectionTable}_${resolveColumnName(propName, prop)}`;
275
+ const values = Array.isArray(prop.enum)
276
+ ? prop.enum.map(v => String(v.id ?? v))
277
+ : Object.keys(prop.enum);
278
+ if (values.length > 0) {
279
+ ddl += `CREATE TYPE "${schema}"."${enumDbName}" AS ENUM (${values.map(v => `'${v}'`).join(", ")});\n`;
280
+ }
281
+ }
282
+ });
283
+ });
284
+ if (ddl.endsWith(";\n")) ddl += "\n";
285
+
286
+ const allTablesToGenerate = new Map<string, {
287
+ collection: EntityCollection,
288
+ isJunction?: boolean,
289
+ relation?: Relation,
290
+ sourceCollection?: EntityCollection
291
+ }>();
292
+
293
+ // Identify all tables
294
+ for (const collection of collections) {
295
+ const tableName = getTableName(collection);
296
+ if (tableName) {
297
+ allTablesToGenerate.set(tableName, { collection });
298
+ }
299
+
300
+ const resolvedRelations = resolveCollectionRelations(collection);
301
+ for (const relation of Object.values(resolvedRelations)) {
302
+ if (relation.through) {
303
+ const junctionTableName = relation.through.table;
304
+ if (!allTablesToGenerate.has(junctionTableName)) {
305
+ allTablesToGenerate.set(junctionTableName, {
306
+ collection: {
307
+ table: junctionTableName,
308
+ properties: {}
309
+ } as EntityCollection,
310
+ isJunction: true,
311
+ relation: relation,
312
+ sourceCollection: collection
313
+ });
314
+ }
315
+ }
316
+ }
317
+ }
318
+
319
+ // 3. Generate tables
320
+ const fkStatements: string[] = [];
321
+ for (const [tableName, {
322
+ collection,
323
+ isJunction,
324
+ relation,
325
+ sourceCollection
326
+ }] of allTablesToGenerate.entries()) {
327
+ const schema = isPostgresCollection(collection) && collection.schema ? collection.schema : "public";
328
+ const baseTableName = tableName.includes(".") ? tableName.split(".").pop()! : tableName;
329
+
330
+ if (isJunction && relation && sourceCollection && relation.through) {
331
+ const targetCollection = relation.target();
332
+ const sourceTable = getTableName(sourceCollection);
333
+ const targetTable = getTableName(targetCollection);
334
+ const sourceSchema = isPostgresCollection(sourceCollection) && sourceCollection.schema ? sourceCollection.schema : "public";
335
+ const targetSchema = isPostgresCollection(targetCollection) && targetCollection.schema ? targetCollection.schema : "public";
336
+ const { sourceColumn, targetColumn } = relation.through;
337
+
338
+ const sourceColType = isNumericId(sourceCollection) ? "INTEGER" : (getPrimaryKeyProp(sourceCollection).isUuid ? "UUID" : "VARCHAR(255)");
339
+ const targetColType = isNumericId(targetCollection) ? "INTEGER" : (getPrimaryKeyProp(targetCollection).isUuid ? "UUID" : "VARCHAR(255)");
340
+ const sourceId = getPrimaryKeyName(sourceCollection);
341
+ const targetId = getPrimaryKeyName(targetCollection);
342
+
343
+ const onDelete = relation.onDelete ?? "CASCADE";
344
+
345
+ ddl += `CREATE TABLE "${schema}"."${baseTableName}" (\n`;
346
+ ddl += ` "${sourceColumn}" ${sourceColType} NOT NULL,\n`;
347
+ ddl += ` "${targetColumn}" ${targetColType} NOT NULL,\n`;
348
+ ddl += ` PRIMARY KEY ("${sourceColumn}", "${targetColumn}")\n`;
349
+ ddl += `);\n\n`;
350
+
351
+ fkStatements.push(`ALTER TABLE "${schema}"."${baseTableName}" ADD CONSTRAINT "${baseTableName}_${sourceColumn}_fkey" FOREIGN KEY ("${sourceColumn}") REFERENCES "${sourceSchema}"."${sourceTable}" ("${sourceId}") ON DELETE ${onDelete.toUpperCase()};`);
352
+ fkStatements.push(`ALTER TABLE "${schema}"."${baseTableName}" ADD CONSTRAINT "${baseTableName}_${targetColumn}_fkey" FOREIGN KEY ("${targetColumn}") REFERENCES "${targetSchema}"."${targetTable}" ("${targetId}") ON DELETE ${onDelete.toUpperCase()};`);
353
+ } else if (!isJunction) {
354
+ ddl += `CREATE TABLE "${schema}"."${baseTableName}" (\n`;
355
+ const columns: string[] = [];
356
+
357
+ Object.entries(collection.properties ?? {}).forEach(([propName, prop]) => {
358
+ if (prop.type === "relation") {
359
+ const refProp = prop as RelationProperty;
360
+ const resolvedRelations = resolveCollectionRelations(collection);
361
+ const relInfo = findRelation(resolvedRelations, refProp.relationName ?? propName);
362
+
363
+ if (!relInfo || relInfo.direction !== "owning" || relInfo.cardinality !== "one" || !relInfo.localKey) {
364
+ return;
365
+ }
366
+
367
+ if (collection.properties[relInfo.localKey] && propName !== relInfo.localKey) {
368
+ return;
369
+ }
370
+
371
+ let targetCollection: EntityCollection;
372
+ try {
373
+ targetCollection = relInfo.target();
374
+ } catch {
375
+ return;
376
+ }
377
+
378
+ const targetTable = getTableName(targetCollection);
379
+ const targetSchema = isPostgresCollection(targetCollection) && targetCollection.schema ? targetCollection.schema : "public";
380
+ const targetId = getPrimaryKeyName(targetCollection);
381
+ const fkColType = getSqlColumnType(propName, prop, collection, collections);
382
+
383
+ const onUpdate = relInfo.onUpdate ? ` ON UPDATE ${relInfo.onUpdate.toUpperCase()}` : "";
384
+ const required = prop.validation?.required;
385
+ const onDeleteVal = relInfo.onDelete ?? (required ? "CASCADE" : "SET NULL");
386
+
387
+ let colDef = ` "${relInfo.localKey}" ${fkColType}`;
388
+ if (required) colDef += " NOT NULL";
389
+ columns.push(colDef);
390
+
391
+ fkStatements.push(`ALTER TABLE "${schema}"."${baseTableName}" ADD CONSTRAINT "${baseTableName}_${relInfo.localKey}_fkey" FOREIGN KEY ("${relInfo.localKey}") REFERENCES "${targetSchema}"."${targetTable}" ("${targetId}") ON DELETE ${onDeleteVal.toUpperCase()}${onUpdate};`);
392
+ } else if (prop.type === "reference") {
393
+ const refProp = prop as ReferenceProperty;
394
+ const targetCollection = collections.find(c => c.slug === refProp.path || getTableName(c) === refProp.path);
395
+ const colName = resolveColumnName(propName, prop);
396
+ const colType = getSqlColumnType(propName, prop, collection, collections);
397
+ const required = prop.validation?.required;
398
+
399
+ if (!targetCollection) {
400
+ let colDef = ` "${colName}" ${colType}`;
401
+ if (required) colDef += " NOT NULL";
402
+ columns.push(colDef);
403
+ } else {
404
+ const targetTable = getTableName(targetCollection);
405
+ const targetSchema = isPostgresCollection(targetCollection) && targetCollection.schema ? targetCollection.schema : "public";
406
+ const targetId = getPrimaryKeyName(targetCollection);
407
+ const onDelete = required ? "CASCADE" : "SET NULL";
408
+
409
+ let colDef = ` "${colName}" ${colType}`;
410
+ if (required) colDef += " NOT NULL";
411
+ columns.push(colDef);
412
+
413
+ fkStatements.push(`ALTER TABLE "${schema}"."${baseTableName}" ADD CONSTRAINT "${baseTableName}_${colName}_fkey" FOREIGN KEY ("${colName}") REFERENCES "${targetSchema}"."${targetTable}" ("${targetId}") ON DELETE ${onDelete.toUpperCase()};`);
414
+ }
415
+ } else {
416
+ const colName = resolveColumnName(propName, prop);
417
+ const colType = getSqlColumnType(propName, prop, collection, collections);
418
+ let colDef = ` "${colName}" ${colType}`;
419
+
420
+ if (isIdProperty(propName, prop, collection)) {
421
+ colDef += " PRIMARY KEY";
422
+ }
423
+
424
+ if ("isId" in prop && prop.isId !== "manual" && prop.isId !== true) {
425
+ if (prop.isId === "uuid") {
426
+ colDef += " DEFAULT gen_random_uuid()";
427
+ } else if (prop.isId === "cuid") {
428
+ colDef += " DEFAULT cuid()";
429
+ } else if (typeof prop.isId === "string") {
430
+ colDef += ` DEFAULT ${prop.isId}`;
431
+ }
432
+ }
433
+
434
+ if (!isIdProperty(propName, prop, collection) && prop.validation?.unique) {
435
+ colDef += " UNIQUE";
436
+ }
437
+
438
+ if (prop.type === "date") {
439
+ const dateProp = prop as DateProperty;
440
+ if (dateProp.autoValue === "on_create" || dateProp.autoValue === "on_update") {
441
+ colDef += " DEFAULT now()";
442
+ }
443
+ }
444
+
445
+ if (prop.validation?.required && !colDef.includes("PRIMARY KEY")) {
446
+ colDef += " NOT NULL";
447
+ }
448
+
449
+ columns.push(colDef);
450
+ }
451
+ });
452
+
453
+ // Backwards compatibility: add default id primary key if missing
454
+ const hasPk = columns.some(c => c.includes("PRIMARY KEY"));
455
+ if (!hasPk) {
456
+ columns.unshift(' "id" VARCHAR(255) PRIMARY KEY');
457
+ }
458
+
459
+ ddl += columns.join(",\n");
460
+ ddl += `\n);\n\n`;
461
+
462
+ if (options.includePolicies) {
463
+ // Enable RLS and add Policies
464
+ ddl += `ALTER TABLE "${schema}"."${baseTableName}" ENABLE ROW LEVEL SECURITY;\n`;
465
+ if (collection.auth) {
466
+ ddl += `ALTER TABLE "${schema}"."${baseTableName}" FORCE ROW LEVEL SECURITY;\n`;
467
+ }
468
+ ddl += `\n`;
469
+
470
+ const securityRules = getEffectiveSecurityRules(collection);
471
+ if (securityRules.length > 0) {
472
+ securityRules.forEach((rule: SecurityRule) => {
473
+ ddl += generatePolicyDdl(collection, rule);
474
+ });
475
+ ddl += "\n";
476
+ }
477
+ }
478
+ }
479
+ }
480
+
481
+ if (fkStatements.length > 0) {
482
+ ddl += "-- Foreign Key Constraints\n";
483
+ ddl += fkStatements.join("\n") + "\n\n";
484
+ }
485
+
486
+ return ddl;
487
+ };
488
+
489
+ export const generatePostgresPoliciesDdl = (collections: EntityCollection[]): string => {
490
+ let ddl = "-- This file contains RLS policies generated by Rebase. Applied separately from migrations.\n\n";
491
+
492
+ const allTablesToGenerate = new Map<string, {
493
+ collection: EntityCollection
494
+ }>();
495
+
496
+ for (const collection of collections) {
497
+ const tableName = getTableName(collection);
498
+ if (tableName) {
499
+ allTablesToGenerate.set(tableName, { collection });
500
+ }
501
+ }
502
+
503
+ for (const [tableName, { collection }] of allTablesToGenerate.entries()) {
504
+ const schema = isPostgresCollection(collection) && collection.schema ? collection.schema : "public";
505
+ const baseTableName = tableName.includes(".") ? tableName.split(".").pop()! : tableName;
506
+
507
+ ddl += `ALTER TABLE "${schema}"."${baseTableName}" ENABLE ROW LEVEL SECURITY;\n`;
508
+ if (collection.auth) {
509
+ ddl += `ALTER TABLE "${schema}"."${baseTableName}" FORCE ROW LEVEL SECURITY;\n`;
510
+ }
511
+ ddl += `\n`;
512
+
513
+ const securityRules = getEffectiveSecurityRules(collection);
514
+ if (securityRules.length > 0) {
515
+ securityRules.forEach((rule: SecurityRule) => {
516
+ ddl += generatePolicyDdl(collection, rule);
517
+ });
518
+ ddl += "\n";
519
+ }
520
+ }
521
+
522
+ return ddl;
523
+ };
524
+