@rebasepro/server-postgresql 0.6.0 → 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 (82) hide show
  1. package/package.json +24 -18
  2. package/src/PostgresBackendDriver.ts +65 -44
  3. package/src/PostgresBootstrapper.ts +49 -20
  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/data-transformer.ts +9 -1
  10. package/src/schema/auth-default-policies.ts +90 -0
  11. package/src/schema/auth-schema.ts +25 -2
  12. package/src/schema/doctor.ts +2 -4
  13. package/src/schema/generate-drizzle-schema-logic.ts +4 -3
  14. package/src/schema/generate-drizzle-schema.ts +3 -5
  15. package/src/schema/generate-postgres-ddl-logic.ts +524 -0
  16. package/src/schema/generate-postgres-ddl.ts +116 -0
  17. package/src/services/EntityPersistService.ts +18 -16
  18. package/src/services/entityService.ts +28 -3
  19. package/src/utils/pg-array-null-patch.ts +42 -0
  20. package/src/utils/pg-error-utils.ts +16 -0
  21. package/src/utils/table-classification.ts +16 -0
  22. package/src/websocket.ts +9 -0
  23. package/test/array-null-safety.test.ts +335 -0
  24. package/test/auth-default-policies.test.ts +89 -0
  25. package/test/cli-helpers-extended.test.ts +324 -0
  26. package/test/cli-helpers.test.ts +59 -0
  27. package/test/connection.test.ts +292 -0
  28. package/test/data-transformer.test.ts +53 -2
  29. package/test/databasePoolManager.test.ts +289 -0
  30. package/test/doctor-extended.test.ts +443 -0
  31. package/test/e2e/db-e2e.test.ts +293 -0
  32. package/test/e2e/pg-setup.ts +79 -0
  33. package/test/entity-persist-composite-keys.test.ts +451 -0
  34. package/test/generate-postgres-ddl-edge-cases.test.ts +716 -0
  35. package/test/generate-postgres-ddl.test.ts +300 -0
  36. package/test/mfa-service.test.ts +544 -0
  37. package/test/pg-array-null-patch.test.ts +65 -0
  38. package/test/pg-error-utils.test.ts +50 -1
  39. package/test/realtimeService-channels.test.ts +696 -0
  40. package/test/unmapped-tables-safety.test.ts +55 -342
  41. package/vite.config.ts +8 -6
  42. package/vitest.e2e.config.ts +10 -0
  43. package/build-errors.txt +0 -37
  44. package/dist/PostgresAdapter.d.ts +0 -6
  45. package/dist/PostgresBackendDriver.d.ts +0 -110
  46. package/dist/PostgresBootstrapper.d.ts +0 -46
  47. package/dist/auth/ensure-tables.d.ts +0 -10
  48. package/dist/auth/services.d.ts +0 -231
  49. package/dist/cli.d.ts +0 -1
  50. package/dist/collections/PostgresCollectionRegistry.d.ts +0 -47
  51. package/dist/connection.d.ts +0 -65
  52. package/dist/data-transformer.d.ts +0 -55
  53. package/dist/databasePoolManager.d.ts +0 -20
  54. package/dist/history/HistoryService.d.ts +0 -71
  55. package/dist/history/ensure-history-table.d.ts +0 -7
  56. package/dist/index.d.ts +0 -14
  57. package/dist/index.es.js +0 -10764
  58. package/dist/index.es.js.map +0 -1
  59. package/dist/index.umd.js +0 -11055
  60. package/dist/index.umd.js.map +0 -1
  61. package/dist/interfaces.d.ts +0 -18
  62. package/dist/schema/auth-schema.d.ts +0 -2149
  63. package/dist/schema/doctor-cli.d.ts +0 -2
  64. package/dist/schema/doctor.d.ts +0 -52
  65. package/dist/schema/generate-drizzle-schema-logic.d.ts +0 -2
  66. package/dist/schema/generate-drizzle-schema.d.ts +0 -1
  67. package/dist/schema/introspect-db-inference.d.ts +0 -5
  68. package/dist/schema/introspect-db-logic.d.ts +0 -118
  69. package/dist/schema/introspect-db.d.ts +0 -1
  70. package/dist/schema/test-schema.d.ts +0 -24
  71. package/dist/services/BranchService.d.ts +0 -47
  72. package/dist/services/EntityFetchService.d.ts +0 -214
  73. package/dist/services/EntityPersistService.d.ts +0 -40
  74. package/dist/services/RelationService.d.ts +0 -98
  75. package/dist/services/entity-helpers.d.ts +0 -38
  76. package/dist/services/entityService.d.ts +0 -110
  77. package/dist/services/index.d.ts +0 -4
  78. package/dist/services/realtimeService.d.ts +0 -220
  79. package/dist/types.d.ts +0 -3
  80. package/dist/utils/drizzle-conditions.d.ts +0 -138
  81. package/dist/utils/pg-error-utils.d.ts +0 -55
  82. package/dist/websocket.d.ts +0 -11
@@ -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
+
@@ -0,0 +1,116 @@
1
+ import { promises as fsPromises } from "fs";
2
+ import * as fs from "fs";
3
+ import path from "path";
4
+ import { pathToFileURL } from "url";
5
+ import chokidar from "chokidar";
6
+ import { generatePostgresDdl, generatePostgresPoliciesDdl } from "./generate-postgres-ddl-logic";
7
+ import { EntityCollection } from "@rebasepro/types";
8
+ import { logger } from "@rebasepro/server-core";
9
+
10
+
11
+ const runGeneration = async (collectionsFilePath?: string, outputPath?: string) => {
12
+ try {
13
+ if (!collectionsFilePath) {
14
+ logger.error("Error: No collections file path provided. Skipping schema generation.");
15
+ return;
16
+ }
17
+
18
+ const resolvedPath = path.resolve(collectionsFilePath);
19
+ let collections: EntityCollection[] = [];
20
+ const stats = fs.statSync(resolvedPath);
21
+
22
+ if (stats.isDirectory()) {
23
+ const files = fs.readdirSync(resolvedPath);
24
+ for (const file of files) {
25
+ if ((file.endsWith(".ts") || file.endsWith(".js")) &&
26
+ !file.includes(".test.") &&
27
+ !file.endsWith(".d.ts") &&
28
+ file !== "index.ts" && file !== "index.js") {
29
+
30
+ const filePath = path.join(resolvedPath, file);
31
+ try {
32
+ const fileUrl = pathToFileURL(filePath).href;
33
+ const module = await import(fileUrl);
34
+ if (module && module.default) {
35
+ collections.push(module.default);
36
+ }
37
+ } catch (err: unknown) {
38
+ const message = err instanceof Error ? err.message : String(err);
39
+ logger.error(`Error loading ${file}`, { detail: message });
40
+ }
41
+ }
42
+ }
43
+ } else {
44
+ const fileUrl = pathToFileURL(resolvedPath).href + `?t=${Date.now()}`;
45
+ const imported = await import(fileUrl);
46
+ collections = imported.backendCollections || imported.collections;
47
+ }
48
+
49
+ if (!collections || !Array.isArray(collections)) {
50
+ collections = [];
51
+ }
52
+
53
+ // Sort collections by slug alphabetically to ensure deterministic DDL generation
54
+ collections.sort((a, b) => a.slug.localeCompare(b.slug));
55
+
56
+ const ddlContent = await generatePostgresDdl(collections, { includePolicies: false });
57
+ const policiesContent = generatePostgresPoliciesDdl(collections);
58
+
59
+ if (outputPath) {
60
+ const outputDir = path.dirname(outputPath);
61
+ await fsPromises.mkdir(outputDir, { recursive: true });
62
+ await fsPromises.writeFile(outputPath, ddlContent);
63
+ logger.info(`✅ PostgreSQL DDL generated successfully at ${outputPath}`);
64
+
65
+ const policiesPath = path.join(outputDir, "policies.sql");
66
+ await fsPromises.writeFile(policiesPath, policiesContent);
67
+ logger.info(`✅ PostgreSQL Policies DDL generated successfully at ${policiesPath}`);
68
+ } else {
69
+ logger.info("✅ PostgreSQL DDL generated successfully.");
70
+ logger.info(String(ddlContent));
71
+ logger.info("\n✅ PostgreSQL Policies DDL generated successfully.");
72
+ logger.info(String(policiesContent));
73
+ }
74
+
75
+ } catch (error) {
76
+ logger.error("Error generating DDL schema", { error: error });
77
+ }
78
+ };
79
+
80
+ const main = () => {
81
+ const collectionsFilePathArg = process.argv.find(arg => arg.startsWith("--collections="));
82
+ const collectionsFilePath = collectionsFilePathArg ? collectionsFilePathArg.split("=")[1] : process.argv[2];
83
+
84
+ const outputPathArg = process.argv.find(arg => arg.startsWith("--output="));
85
+ const outputPath = outputPathArg ? outputPathArg.split("=")[1] : undefined;
86
+
87
+ const watch = process.argv.includes("--watch");
88
+
89
+ if (!collectionsFilePath) {
90
+ logger.info("Usage: ts-node generate-postgres-ddl.ts <path-to-collections-file> [--output <path-to-output-file>] [--watch]");
91
+ return;
92
+ }
93
+
94
+ const resolvedPath = path.resolve(process.cwd(), collectionsFilePath);
95
+ const resolvedOutputPath = outputPath ? path.resolve(process.cwd(), outputPath) : undefined;
96
+
97
+ if (watch) {
98
+ logger.info(`Watching for changes in ${resolvedPath}...`);
99
+ const watcher = chokidar.watch(resolvedPath, {
100
+ persistent: true,
101
+ ignoreInitial: false
102
+ });
103
+
104
+ watcher.on("all", (event, filePath) => {
105
+ logger.info(`[${event}] ${filePath}. Regenerating DDL schema...`);
106
+ runGeneration(resolvedPath, resolvedOutputPath);
107
+ });
108
+ } else {
109
+ runGeneration(resolvedPath, resolvedOutputPath);
110
+ }
111
+ };
112
+
113
+ // This check ensures the script only runs when executed directly
114
+ if (import.meta.url.endsWith(process.argv[1])) {
115
+ main();
116
+ }