@rebasepro/server-postgresql 0.0.1-canary.4d4fb3e

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 (147) hide show
  1. package/LICENSE +6 -0
  2. package/README.md +106 -0
  3. package/build-errors.txt +37 -0
  4. package/dist/common/src/collections/CollectionRegistry.d.ts +48 -0
  5. package/dist/common/src/collections/index.d.ts +1 -0
  6. package/dist/common/src/data/buildRebaseData.d.ts +14 -0
  7. package/dist/common/src/index.d.ts +3 -0
  8. package/dist/common/src/util/builders.d.ts +57 -0
  9. package/dist/common/src/util/callbacks.d.ts +6 -0
  10. package/dist/common/src/util/collections.d.ts +11 -0
  11. package/dist/common/src/util/common.d.ts +2 -0
  12. package/dist/common/src/util/conditions.d.ts +26 -0
  13. package/dist/common/src/util/entities.d.ts +36 -0
  14. package/dist/common/src/util/enums.d.ts +3 -0
  15. package/dist/common/src/util/index.d.ts +16 -0
  16. package/dist/common/src/util/navigation_from_path.d.ts +34 -0
  17. package/dist/common/src/util/navigation_utils.d.ts +20 -0
  18. package/dist/common/src/util/parent_references_from_path.d.ts +6 -0
  19. package/dist/common/src/util/paths.d.ts +14 -0
  20. package/dist/common/src/util/permissions.d.ts +5 -0
  21. package/dist/common/src/util/references.d.ts +2 -0
  22. package/dist/common/src/util/relations.d.ts +12 -0
  23. package/dist/common/src/util/resolutions.d.ts +72 -0
  24. package/dist/common/src/util/storage.d.ts +24 -0
  25. package/dist/index.es.js +10635 -0
  26. package/dist/index.es.js.map +1 -0
  27. package/dist/index.umd.js +10643 -0
  28. package/dist/index.umd.js.map +1 -0
  29. package/dist/server-postgresql/src/PostgresBackendDriver.d.ts +112 -0
  30. package/dist/server-postgresql/src/PostgresBootstrapper.d.ts +40 -0
  31. package/dist/server-postgresql/src/auth/ensure-tables.d.ts +6 -0
  32. package/dist/server-postgresql/src/auth/services.d.ts +188 -0
  33. package/dist/server-postgresql/src/cli.d.ts +1 -0
  34. package/dist/server-postgresql/src/collections/PostgresCollectionRegistry.d.ts +43 -0
  35. package/dist/server-postgresql/src/connection.d.ts +7 -0
  36. package/dist/server-postgresql/src/data-transformer.d.ts +36 -0
  37. package/dist/server-postgresql/src/databasePoolManager.d.ts +20 -0
  38. package/dist/server-postgresql/src/history/HistoryService.d.ts +71 -0
  39. package/dist/server-postgresql/src/history/ensure-history-table.d.ts +7 -0
  40. package/dist/server-postgresql/src/index.d.ts +13 -0
  41. package/dist/server-postgresql/src/interfaces.d.ts +18 -0
  42. package/dist/server-postgresql/src/schema/auth-schema.d.ts +767 -0
  43. package/dist/server-postgresql/src/schema/generate-drizzle-schema-logic.d.ts +2 -0
  44. package/dist/server-postgresql/src/schema/generate-drizzle-schema.d.ts +1 -0
  45. package/dist/server-postgresql/src/services/BranchService.d.ts +47 -0
  46. package/dist/server-postgresql/src/services/EntityFetchService.d.ts +195 -0
  47. package/dist/server-postgresql/src/services/EntityPersistService.d.ts +41 -0
  48. package/dist/server-postgresql/src/services/RelationService.d.ts +92 -0
  49. package/dist/server-postgresql/src/services/entity-helpers.d.ts +24 -0
  50. package/dist/server-postgresql/src/services/entityService.d.ts +102 -0
  51. package/dist/server-postgresql/src/services/index.d.ts +4 -0
  52. package/dist/server-postgresql/src/services/realtimeService.d.ts +186 -0
  53. package/dist/server-postgresql/src/utils/drizzle-conditions.d.ts +116 -0
  54. package/dist/server-postgresql/src/websocket.d.ts +5 -0
  55. package/dist/types/src/controllers/analytics_controller.d.ts +7 -0
  56. package/dist/types/src/controllers/auth.d.ts +117 -0
  57. package/dist/types/src/controllers/client.d.ts +58 -0
  58. package/dist/types/src/controllers/collection_registry.d.ts +44 -0
  59. package/dist/types/src/controllers/customization_controller.d.ts +54 -0
  60. package/dist/types/src/controllers/data.d.ts +141 -0
  61. package/dist/types/src/controllers/data_driver.d.ts +168 -0
  62. package/dist/types/src/controllers/database_admin.d.ts +11 -0
  63. package/dist/types/src/controllers/dialogs_controller.d.ts +36 -0
  64. package/dist/types/src/controllers/effective_role.d.ts +4 -0
  65. package/dist/types/src/controllers/index.d.ts +17 -0
  66. package/dist/types/src/controllers/local_config_persistence.d.ts +20 -0
  67. package/dist/types/src/controllers/navigation.d.ts +213 -0
  68. package/dist/types/src/controllers/registry.d.ts +51 -0
  69. package/dist/types/src/controllers/side_dialogs_controller.d.ts +67 -0
  70. package/dist/types/src/controllers/side_entity_controller.d.ts +89 -0
  71. package/dist/types/src/controllers/snackbar.d.ts +24 -0
  72. package/dist/types/src/controllers/storage.d.ts +173 -0
  73. package/dist/types/src/index.d.ts +4 -0
  74. package/dist/types/src/rebase_context.d.ts +101 -0
  75. package/dist/types/src/types/backend.d.ts +533 -0
  76. package/dist/types/src/types/builders.d.ts +14 -0
  77. package/dist/types/src/types/chips.d.ts +5 -0
  78. package/dist/types/src/types/collections.d.ts +812 -0
  79. package/dist/types/src/types/data_source.d.ts +64 -0
  80. package/dist/types/src/types/entities.d.ts +145 -0
  81. package/dist/types/src/types/entity_actions.d.ts +98 -0
  82. package/dist/types/src/types/entity_callbacks.d.ts +173 -0
  83. package/dist/types/src/types/entity_link_builder.d.ts +7 -0
  84. package/dist/types/src/types/entity_overrides.d.ts +9 -0
  85. package/dist/types/src/types/entity_views.d.ts +61 -0
  86. package/dist/types/src/types/export_import.d.ts +21 -0
  87. package/dist/types/src/types/index.d.ts +22 -0
  88. package/dist/types/src/types/locales.d.ts +4 -0
  89. package/dist/types/src/types/modify_collections.d.ts +5 -0
  90. package/dist/types/src/types/plugins.d.ts +225 -0
  91. package/dist/types/src/types/properties.d.ts +1091 -0
  92. package/dist/types/src/types/property_config.d.ts +70 -0
  93. package/dist/types/src/types/relations.d.ts +336 -0
  94. package/dist/types/src/types/slots.d.ts +228 -0
  95. package/dist/types/src/types/translations.d.ts +826 -0
  96. package/dist/types/src/types/user_management_delegate.d.ts +120 -0
  97. package/dist/types/src/types/websockets.d.ts +78 -0
  98. package/dist/types/src/users/index.d.ts +2 -0
  99. package/dist/types/src/users/roles.d.ts +22 -0
  100. package/dist/types/src/users/user.d.ts +46 -0
  101. package/jest-all.log +3128 -0
  102. package/jest.log +49 -0
  103. package/package.json +93 -0
  104. package/src/PostgresBackendDriver.ts +1024 -0
  105. package/src/PostgresBootstrapper.ts +232 -0
  106. package/src/auth/ensure-tables.ts +309 -0
  107. package/src/auth/services.ts +740 -0
  108. package/src/cli.ts +347 -0
  109. package/src/collections/PostgresCollectionRegistry.ts +96 -0
  110. package/src/connection.ts +62 -0
  111. package/src/data-transformer.ts +569 -0
  112. package/src/databasePoolManager.ts +84 -0
  113. package/src/history/HistoryService.ts +257 -0
  114. package/src/history/ensure-history-table.ts +45 -0
  115. package/src/index.ts +13 -0
  116. package/src/interfaces.ts +60 -0
  117. package/src/schema/auth-schema.ts +146 -0
  118. package/src/schema/generate-drizzle-schema-logic.ts +618 -0
  119. package/src/schema/generate-drizzle-schema.ts +151 -0
  120. package/src/services/BranchService.ts +237 -0
  121. package/src/services/EntityFetchService.ts +1447 -0
  122. package/src/services/EntityPersistService.ts +351 -0
  123. package/src/services/RelationService.ts +1012 -0
  124. package/src/services/entity-helpers.ts +121 -0
  125. package/src/services/entityService.ts +209 -0
  126. package/src/services/index.ts +13 -0
  127. package/src/services/realtimeService.ts +1005 -0
  128. package/src/utils/drizzle-conditions.ts +999 -0
  129. package/src/websocket.ts +487 -0
  130. package/test/auth-services.test.ts +569 -0
  131. package/test/branchService.test.ts +357 -0
  132. package/test/drizzle-conditions.test.ts +895 -0
  133. package/test/entityService.errors.test.ts +352 -0
  134. package/test/entityService.relations.test.ts +912 -0
  135. package/test/entityService.subcollection-search.test.ts +516 -0
  136. package/test/entityService.test.ts +977 -0
  137. package/test/generate-drizzle-schema.test.ts +795 -0
  138. package/test/historyService.test.ts +126 -0
  139. package/test/postgresDataDriver.test.ts +556 -0
  140. package/test/realtimeService.test.ts +276 -0
  141. package/test/relations.test.ts +662 -0
  142. package/test_drizzle_mock.js +3 -0
  143. package/test_find_changed.mjs +30 -0
  144. package/test_output.txt +3145 -0
  145. package/tsconfig.json +49 -0
  146. package/tsconfig.prod.json +20 -0
  147. package/vite.config.ts +82 -0
@@ -0,0 +1,618 @@
1
+ import { EntityCollection, NumberProperty, Property, Relation, RelationProperty, SecurityOperation, SecurityRule, StringProperty } from "@rebasepro/types";
2
+ import { getPrimaryKeys } from "../services/entity-helpers";
3
+ import { getEnumVarName, getTableName, getTableVarName, resolveCollectionRelations } from "@rebasepro/common";
4
+ import { toSnakeCase } from "@rebasepro/utils";
5
+
6
+ // --- Helper Functions ---
7
+
8
+ const getPrimaryKeyProp = (collection: EntityCollection): { name: string, type: "string" | "number" } => {
9
+ if (collection.properties) {
10
+ const idPropEntry = Object.entries(collection.properties).find(([_, prop]) => "isId" in (prop as object) && Boolean((prop as unknown as Record<string, unknown>).isId));
11
+ if (idPropEntry) {
12
+ return { name: idPropEntry[0], type: (idPropEntry[1] as Property).type === "number" ? "number" : "string" };
13
+ }
14
+ }
15
+ // Fallback
16
+ const idProp = collection.properties?.["id"] as Property | undefined;
17
+ if (idProp?.type === "number") {
18
+ return { name: "id", type: "number" };
19
+ }
20
+ return { name: "id", type: "string" };
21
+ };
22
+
23
+ const isNumericId = (collection: EntityCollection): boolean => {
24
+ return getPrimaryKeyProp(collection).type === "number";
25
+ };
26
+
27
+ const getPrimaryKeyName = (collection: EntityCollection): string => {
28
+ return getPrimaryKeyProp(collection).name;
29
+ };
30
+
31
+ const isIdProperty = (propName: string, prop: Property, collection: EntityCollection): boolean => {
32
+ if ("isId" in prop && Boolean(prop.isId)) return true;
33
+
34
+ // We only fallback to "id" if NO property is explicitly marked with `isId: true` or a generator string
35
+ const hasExplicitId = Object.values(collection.properties ?? {}).some(p => "isId" in (p as object) && Boolean((p as unknown as Record<string, unknown>).isId));
36
+ return !hasExplicitId && propName === "id";
37
+ };
38
+
39
+ const getDrizzleColumn = (propName: string, prop: Property, collection: EntityCollection): string | null => {
40
+ const colName = toSnakeCase(propName);
41
+ let columnDefinition: string;
42
+
43
+ switch (prop.type) {
44
+ case "string": {
45
+ const stringProp = prop as StringProperty;
46
+ if (stringProp.enum) {
47
+ const enumName = getEnumVarName(getTableName(collection), propName);
48
+ columnDefinition = `${enumName}("${colName}")`;
49
+ } else if ("isId" in stringProp && stringProp.isId === "uuid") {
50
+ columnDefinition = `uuid("${colName}")`;
51
+ } else if (stringProp.columnType === "text") {
52
+ columnDefinition = `text("${colName}")`;
53
+ } else if (stringProp.columnType === "char") {
54
+ columnDefinition = `char("${colName}")`;
55
+ } else {
56
+ columnDefinition = `varchar("${colName}")`;
57
+ }
58
+ if (isIdProperty(propName, prop, collection)) {
59
+ columnDefinition += `.primaryKey()`;
60
+ }
61
+ if ("isId" in stringProp && stringProp.isId !== "manual" && stringProp.isId !== true) {
62
+ if (stringProp.isId === "uuid") {
63
+ columnDefinition += `.defaultRandom()`;
64
+ } else if (stringProp.isId === "cuid") {
65
+ columnDefinition += `.default(sql\`cuid()\`)`;
66
+ } else if (typeof stringProp.isId === "string") {
67
+ const sqlContent = stringProp.isId.startsWith("sql`") && stringProp.isId.endsWith("`")
68
+ ? stringProp.isId.substring(4, stringProp.isId.length - 1)
69
+ : stringProp.isId;
70
+ columnDefinition += `.default(sql\`${sqlContent}\`)`;
71
+ }
72
+ }
73
+ if (stringProp.validation?.unique) {
74
+ columnDefinition += `.unique()`;
75
+ }
76
+ break;
77
+ }
78
+ case "number": {
79
+ const numProp = prop as NumberProperty;
80
+ const isId = isIdProperty(propName, prop, collection);
81
+
82
+ let baseType = (numProp.validation?.integer || isId) ? `integer("${colName}")` : `numeric("${colName}")`;
83
+ if (numProp.columnType) {
84
+ if (numProp.columnType === "double precision") baseType = `doublePrecision("${colName}")`;
85
+ else baseType = `${numProp.columnType}("${colName}")`;
86
+ }
87
+
88
+ if ("isId" in numProp && numProp.isId === "increment") {
89
+ columnDefinition = `${baseType}.generatedByDefaultAsIdentity()`;
90
+ } else if ("isId" in numProp && typeof numProp.isId === "string" && numProp.isId !== "manual") {
91
+ columnDefinition = baseType;
92
+ const sqlContent = numProp.isId.startsWith("sql`") && numProp.isId.endsWith("`")
93
+ ? numProp.isId.substring(4, numProp.isId.length - 1)
94
+ : numProp.isId;
95
+ columnDefinition += `.default(sql\`${sqlContent}\`)`;
96
+ } else {
97
+ columnDefinition = baseType;
98
+ }
99
+
100
+ if (isId) {
101
+ columnDefinition += `.primaryKey()`;
102
+ }
103
+ if (numProp.validation?.unique) {
104
+ columnDefinition += `.unique()`;
105
+ }
106
+ break;
107
+ }
108
+ case "boolean":
109
+ columnDefinition = `boolean("${colName}")`;
110
+ break;
111
+ case "date": {
112
+ const dateProp = prop as import("@rebasepro/types").DateProperty;
113
+ if (dateProp.columnType === "date") {
114
+ columnDefinition = `date("${colName}", { mode: 'string' })`;
115
+ } else if (dateProp.columnType === "time") {
116
+ columnDefinition = `time("${colName}")`;
117
+ } else {
118
+ columnDefinition = `timestamp("${colName}", { withTimezone: true, mode: 'string' })`;
119
+ }
120
+ break;
121
+ }
122
+ case "map":
123
+ case "array": {
124
+ const arrayOrMapProp = prop as import("@rebasepro/types").ArrayProperty | import("@rebasepro/types").MapProperty;
125
+ if (arrayOrMapProp.columnType === "json") {
126
+ columnDefinition = `json("${colName}")`;
127
+ } else {
128
+ columnDefinition = `jsonb("${colName}")`;
129
+ }
130
+ break;
131
+ }
132
+ case "relation": {
133
+ const refProp = prop as RelationProperty;
134
+ const resolvedRelations = resolveCollectionRelations(collection as import("@rebasepro/types").PostgresCollection<any, any>);
135
+ const relation = resolvedRelations[refProp.relationName ?? propName];
136
+
137
+ // Only owning one-to-one/many-to-one relations create a column here.
138
+ if (!relation || relation.direction !== "owning" || relation.cardinality !== "one") {
139
+ return null;
140
+ }
141
+
142
+ // The localKey property is the source of truth for the FK column name.
143
+ if (!relation.localKey) {
144
+ console.warn(`Could not generate column for owning relation '${relation.relationName}' on '${collection.name}': 'localKey' is not defined.`);
145
+ return null;
146
+ }
147
+
148
+ // If the localKey property is defined elsewhere in the properties, it will be handled there.
149
+ // This logic is for when the relation property itself defines the FK.
150
+ if (collection.properties[relation.localKey] && propName !== relation.localKey) {
151
+ return null;
152
+ }
153
+
154
+ let targetCollection: EntityCollection;
155
+ try {
156
+ targetCollection = relation.target();
157
+ } catch {
158
+ return null; // Cannot resolve target
159
+ }
160
+
161
+ const fkColumnName = toSnakeCase(relation.localKey);
162
+ const targetTableVar = getTableVarName(getTableName(targetCollection));
163
+ const targetIdField = getPrimaryKeyName(targetCollection);
164
+ const baseColumn = isNumericId(targetCollection) ? `integer(\"${fkColumnName}\")` : `varchar(\"${fkColumnName}\")`;
165
+
166
+ const onUpdate = relation.onUpdate ? `onUpdate: \"${relation.onUpdate}\"` : "";
167
+ const required = prop.validation?.required;
168
+ const onDeleteVal = relation.onDelete ?? (required ? "cascade" : "set null");
169
+ const onDelete = `onDelete: \"${onDeleteVal}\"`;
170
+
171
+ const refOptionsParts = [onUpdate, onDelete].filter(Boolean);
172
+ const refOptions = refOptionsParts.length > 0 ? `{ ${refOptionsParts.join(", ")} }` : "";
173
+
174
+ let columnDef = `${baseColumn}.references(() => ${targetTableVar}.${targetIdField}${refOptions ? `, ${refOptions}` : ""})`;
175
+
176
+ if (required) {
177
+ columnDef += ".notNull()";
178
+ }
179
+
180
+ return ` ${relation.localKey}: ${columnDef}`;
181
+ }
182
+ default:
183
+ return null;
184
+ }
185
+
186
+ if (prop.validation?.required) {
187
+ columnDefinition += ".notNull()";
188
+ }
189
+
190
+ return ` ${propName}: ${columnDefinition}`;
191
+ };
192
+
193
+ /**
194
+ * Resolves a raw SQL string, replacing `{column_name}` with `${table.column_name}`.
195
+ * The result is wrapped in a Drizzle sql`` template literal.
196
+ */
197
+ const resolveRawSql = (expression: string): string => {
198
+ // Replace {column_name} with ${table.column_name}
199
+ const resolved = expression.replace(/\{(\w+)\}/g, (_, col) => `\${table.${col}}`);
200
+ return `sql\`${resolved}\``;
201
+ };
202
+
203
+ /**
204
+ * Wraps a SQL clause with a role check using AND.
205
+ * Generates: `(<clause>) AND (string_to_array(auth.roles(), ',') && ARRAY['<role1>','<role2>'])`
206
+ */
207
+ const wrapWithRoleCheck = (clause: string, roles: string[]): string => {
208
+ const rolesArrayString = `ARRAY[${roles.map(r => `'${r}'`).join(',')}]`;
209
+ const roleCondition = `string_to_array(auth.roles(), ',') @> ${rolesArrayString}`;
210
+ return `sql\`(${unwrapSql(clause)}) AND (${roleCondition})\``;
211
+ };
212
+
213
+ /**
214
+ * Extracts the inner expression from a `sql\`...\`` wrapper.
215
+ */
216
+ const unwrapSql = (sqlExpr: string): string => {
217
+ const match = sqlExpr.match(/^sql`(.*)`$/s);
218
+ return match ? match[1] : sqlExpr;
219
+ };
220
+
221
+ /**
222
+ * Builds the USING clause for a policy based on shortcuts or raw SQL.
223
+ */
224
+ const buildUsingClause = (rule: SecurityRule): string | null => {
225
+ if (rule.using) {
226
+ return resolveRawSql(rule.using);
227
+ }
228
+ if (rule.access === "public") {
229
+ return `sql\`true\``;
230
+ }
231
+ if (rule.ownerField) {
232
+ return `sql\`\${table.${rule.ownerField}} = auth.uid()\``;
233
+ }
234
+ return null;
235
+ };
236
+
237
+ /**
238
+ * Builds the WITH CHECK clause for a policy based on shortcuts or raw SQL.
239
+ * Falls back to the USING clause if not explicitly provided.
240
+ */
241
+ const buildWithCheckClause = (rule: SecurityRule): string | null => {
242
+ if (rule.withCheck) {
243
+ return resolveRawSql(rule.withCheck);
244
+ }
245
+ // For insert/update/all, fall back to using clause if withCheck not specified
246
+ return buildUsingClause(rule);
247
+ };
248
+
249
+ /**
250
+ * Generates Drizzle pgPolicy() calls from a declarative SecurityRule definition.
251
+ *
252
+ * Supports the full spectrum:
253
+ * - Convenience shortcuts: ownerField, access, roles
254
+ * - Raw SQL: using, withCheck
255
+ * - Mode: permissive (default) or restrictive
256
+ * - operations[] array: generates one policy per operation
257
+ * - Combinations: roles + ownerField, roles + raw SQL, etc.
258
+ */
259
+ const generatePolicyCode = (tableName: string, rule: SecurityRule, index: number): string => {
260
+ // Resolve operations: operations[] takes precedence over operation (singular)
261
+ const ops: SecurityOperation[] = rule.operations && rule.operations.length > 0
262
+ ? rule.operations
263
+ : [rule.operation ?? "all"];
264
+
265
+ // Generate one pgPolicy per operation
266
+ return ops.map((op, opIdx) => {
267
+ const policyName = rule.name
268
+ ? (ops.length > 1 ? `${rule.name}_${op}` : rule.name)
269
+ : `${tableName}_${op}_policy_${index}${ops.length > 1 ? `_${opIdx}` : ""}`;
270
+
271
+ return generateSinglePolicyCode(tableName, rule, op, policyName);
272
+ }).join("");
273
+ };
274
+
275
+ /**
276
+ * Generates a single pgPolicy() call for one specific operation.
277
+ */
278
+ const generateSinglePolicyCode = (tableName: string, rule: SecurityRule, operation: SecurityOperation, policyName: string): string => {
279
+ const mode = rule.mode ?? "permissive";
280
+ const roles = rule.roles;
281
+
282
+ // Determine which clauses this operation needs:
283
+ // SELECT, DELETE → USING only
284
+ // INSERT → WITH CHECK only
285
+ // UPDATE, ALL → both USING and WITH CHECK
286
+ const needsUsing = operation !== "insert";
287
+ const needsWithCheck = operation !== "select" && operation !== "delete";
288
+
289
+ let usingClause = needsUsing ? buildUsingClause(rule) : null;
290
+ let withCheckClause = needsWithCheck ? buildWithCheckClause(rule) : null;
291
+
292
+ // If roles are specified, wrap existing clauses with role check,
293
+ // or generate a roles-only clause.
294
+ if (roles && roles.length > 0) {
295
+ if (usingClause) {
296
+ usingClause = wrapWithRoleCheck(usingClause, roles);
297
+ } else if (needsUsing) {
298
+ // Roles-only rule (e.g. { operation: "select", roles: ["admin"] })
299
+ const rolesArrayString = `ARRAY[${roles.map(r => `'${r}'`).join(',')}]`;
300
+ usingClause = `sql\`string_to_array(auth.roles(), ',') @> ${rolesArrayString}\``;
301
+ }
302
+ if (withCheckClause) {
303
+ withCheckClause = wrapWithRoleCheck(withCheckClause, roles);
304
+ } else if (needsWithCheck) {
305
+ const rolesArrayString = `ARRAY[${roles.map(r => `'${r}'`).join(',')}]`;
306
+ withCheckClause = `sql\`string_to_array(auth.roles(), ',') @> ${rolesArrayString}\``;
307
+ }
308
+ }
309
+
310
+ // Fallback: if we still have no clauses, deny all (safety net)
311
+ if (!usingClause && needsUsing) {
312
+ usingClause = `sql\`false\``;
313
+ }
314
+ if (!withCheckClause && needsWithCheck) {
315
+ withCheckClause = `sql\`false\``;
316
+ }
317
+
318
+ // Build the policy options object
319
+ const parts: string[] = [];
320
+ parts.push(`as: "${mode}"`);
321
+ parts.push(`for: "${operation}"`);
322
+ const toRoles = rule.pgRoles ?? ["public"];
323
+ parts.push(`to: [${toRoles.map(r => `"${r}"`).join(", ")}]`);
324
+ if (usingClause) parts.push(`using: ${usingClause}`);
325
+ if (withCheckClause) parts.push(`withCheck: ${withCheckClause}`);
326
+
327
+ return ` pgPolicy("${policyName}", { ${parts.join(", ")} }),\n`;
328
+ };
329
+
330
+ // --- Main Schema Generation Logic ---
331
+ export const generateSchema = async (collections: EntityCollection[]): Promise<string> => {
332
+ let schemaContent = "// This file is auto-generated by the Rebase Drizzle generator. Do not edit manually.\n\n";
333
+
334
+ const hasUuid = collections.some(c =>
335
+ c.properties && Object.values(c.properties).some(
336
+ (p: Property) => p.type === "string" && ((p as unknown as Record<string, unknown>).autoValue === "uuid" || (p as unknown as Record<string, unknown>).isId === "uuid")
337
+ )
338
+ );
339
+
340
+ const hasJson = collections.some(c =>
341
+ c.properties && Object.values(c.properties).some(
342
+ (p: Property) => (p.type === "map" || p.type === "array") && (p as unknown as Record<string, unknown>).columnType === "json"
343
+ )
344
+ );
345
+
346
+ // Always import pgPolicy and sql — RLS is enabled on every table (secure by default)
347
+ const pgCoreImports = ["primaryKey", "pgTable", "integer", "varchar", "text", "char", "boolean", "timestamp", "date", "time", "jsonb", "json", "pgEnum", "numeric", "real", "doublePrecision", "bigint", "serial", "bigserial", "pgPolicy"];
348
+ if (hasUuid) pgCoreImports.push("uuid");
349
+ schemaContent += `import { ${pgCoreImports.join(", ")} } from 'drizzle-orm/pg-core';\n`;
350
+ schemaContent += `import { relations as drizzleRelations, sql } from 'drizzle-orm';\n\n`;
351
+
352
+ const exportedTableVars: string[] = [];
353
+ const exportedEnumVars: string[] = [];
354
+ const exportedRelationVars: string[] = [];
355
+
356
+ const allTablesToGenerate = new Map<string, {
357
+ collection: EntityCollection,
358
+ isJunction?: boolean,
359
+ relation?: Relation,
360
+ sourceCollection?: EntityCollection
361
+ }>();
362
+
363
+ // 1. Generate Enums
364
+ collections.forEach(collection => {
365
+ const collectionPath = getTableName(collection);
366
+ Object.entries(collection.properties ?? {}).forEach(([propName, prop]) => {
367
+ if (("enum" in prop) && (prop.type === "string" || prop.type === "number") && prop.enum) {
368
+ const enumVarName = getEnumVarName(collectionPath, propName);
369
+ const enumDbName = `${collectionPath}_${toSnakeCase(propName)}`;
370
+ const values = Array.isArray(prop.enum)
371
+ ? prop.enum.map(v => String(v.id ?? v))
372
+ : Object.keys(prop.enum);
373
+ if (values.length > 0) {
374
+ schemaContent += `export const ${enumVarName} = pgEnum(\"${enumDbName}\", [${values.map(v => `'${v}'`).join(", ")}]);\n`;
375
+ if (!exportedEnumVars.includes(enumVarName)) exportedEnumVars.push(enumVarName);
376
+ }
377
+ }
378
+ });
379
+ });
380
+ schemaContent += "\n";
381
+
382
+ // 2. Identify all tables (collections and junction tables only)
383
+ for (const collection of collections) {
384
+ const tableName = getTableName(collection);
385
+ if (tableName) {
386
+ allTablesToGenerate.set(tableName, { collection });
387
+ }
388
+
389
+ const resolvedRelations = resolveCollectionRelations(collection as import("@rebasepro/types").PostgresCollection<any, any>);
390
+ for (const relation of Object.values(resolvedRelations)) {
391
+ if (relation.through) { // Standard M2M junction table
392
+ const junctionTableName = relation.through.table;
393
+ if (!allTablesToGenerate.has(junctionTableName)) {
394
+ allTablesToGenerate.set(junctionTableName, {
395
+ collection: {
396
+ table: junctionTableName,
397
+ properties: {}
398
+ } as EntityCollection,
399
+ isJunction: true,
400
+ relation: relation,
401
+ sourceCollection: collection
402
+ });
403
+ }
404
+ }
405
+ // joinPath relations use existing user-controlled tables - no generation needed
406
+ }
407
+ }
408
+
409
+ // 3. Generate pgTable definitions for all unique tables
410
+ for (const [tableName, {
411
+ collection,
412
+ isJunction,
413
+ relation,
414
+ sourceCollection
415
+ }] of allTablesToGenerate.entries()) {
416
+ const tableVarName = getTableVarName(tableName);
417
+ if (isJunction && relation && sourceCollection && relation.through) {
418
+ const targetCollection = relation.target();
419
+ const {
420
+ sourceColumn,
421
+ targetColumn
422
+ } = relation.through;
423
+
424
+ const onDelete = relation.onDelete ?? "cascade";
425
+ const refOptions = `{ onDelete: \"${onDelete}\" }`;
426
+
427
+ const sourceColType = isNumericId(sourceCollection) ? "integer" : "varchar";
428
+ const targetColType = isNumericId(targetCollection) ? "integer" : "varchar";
429
+ const sourceId = getPrimaryKeyName(sourceCollection);
430
+ const targetId = getPrimaryKeyName(targetCollection);
431
+
432
+ schemaContent += `export const ${tableVarName} = pgTable(\"${tableName}\", {\n`;
433
+ schemaContent += ` ${sourceColumn}: ${sourceColType}(\"${toSnakeCase(sourceColumn)}\").notNull().references(() => ${getTableVarName(getTableName(sourceCollection))}.${sourceId}, ${refOptions}),\n`;
434
+ schemaContent += ` ${targetColumn}: ${targetColType}(\"${toSnakeCase(targetColumn)}\").notNull().references(() => ${getTableVarName(getTableName(targetCollection))}.${targetId}, ${refOptions}),\n`;
435
+ schemaContent += `}, (table) => ({\n`;
436
+ schemaContent += ` pk: primaryKey({ columns: [table.${sourceColumn}, table.${targetColumn}] })\n`;
437
+ schemaContent += `}));\n\n`;
438
+ } else if (!isJunction) {
439
+ schemaContent += `export const ${tableVarName} = pgTable(\"${tableName}\", {\n`;
440
+ const columns = new Set<string>();
441
+ Object.entries(collection.properties ?? {}).forEach(([propName, prop]) => {
442
+ const columnString = getDrizzleColumn(propName, prop as Property, collection);
443
+ if (columnString) columns.add(columnString);
444
+ });
445
+
446
+ // Backwards compatibility: if no id/primary key column is found in properties, but `id` wasn't explicitly provided
447
+ // We should generate a basic id column if one was completely omitted.
448
+ const hasIdColumn = Array.from(columns).some(col => col.includes(".primaryKey()"));
449
+ if (!hasIdColumn) {
450
+ columns.add(` id: varchar(\"id\").primaryKey()`);
451
+ }
452
+
453
+ schemaContent += `${Array.from(columns).join(",\n")}`;
454
+
455
+ const securityRules = (collection as import("@rebasepro/types").PostgresCollection<Record<string, unknown>, import("@rebasepro/types").User>).securityRules;
456
+ if (securityRules && securityRules.length > 0) {
457
+ schemaContent += "\n}, (table) => ([\n";
458
+ securityRules.forEach((rule: SecurityRule, idx: number) => {
459
+ schemaContent += generatePolicyCode(tableName, rule, idx);
460
+ });
461
+ schemaContent += "])).enableRLS();\n\n";
462
+ } else {
463
+ // No explicit policies — RLS enabled with deny-all default (Postgres denies
464
+ // everything when RLS is on and no permissive policies exist).
465
+ schemaContent += "\n}).enableRLS();\n\n";
466
+ }
467
+ }
468
+ if (!exportedTableVars.includes(tableVarName)) exportedTableVars.push(tableVarName);
469
+ }
470
+
471
+ // 4. Generate Drizzle Relations
472
+ for (const [tableName, {
473
+ collection,
474
+ isJunction
475
+ }] of allTablesToGenerate.entries()) {
476
+ const tableVarName = getTableVarName(tableName);
477
+ const tableRelations: string[] = [];
478
+
479
+ if (isJunction) {
480
+ const relationInfo = Array.from(allTablesToGenerate.values()).find(v => v.isJunction && getTableName(v.collection) === tableName);
481
+ if (relationInfo && relationInfo.relation && relationInfo.sourceCollection && relationInfo.relation.through) {
482
+ const {
483
+ relation,
484
+ sourceCollection
485
+ } = relationInfo;
486
+ const targetCollection = relation.target();
487
+ const sourceTableVar = getTableVarName(getTableName(sourceCollection));
488
+ const targetTableVar = getTableVarName(getTableName(targetCollection));
489
+ const sourceId = getPrimaryKeyName(sourceCollection);
490
+ const targetId = getPrimaryKeyName(targetCollection);
491
+
492
+ if (!relation?.through)
493
+ throw new Error("Internal, the relation should have a through property. Relations passed to this script should sanitized first with sanitizeRelation().");
494
+
495
+ // The owning relation's name — used on the source side of the junction
496
+ const owningRelationName = relation.relationName ?? toSnakeCase(getTableName(targetCollection));
497
+
498
+ // Find the inverse relation name on the target collection (if any)
499
+ // This is needed so the junction's target-side one() can pair with the
500
+ // inverse many() on the target table.
501
+ let inverseRelationName: string | null = null;
502
+ try {
503
+ const targetRelations = resolveCollectionRelations(targetCollection as import("@rebasepro/types").PostgresCollection<any, any>);
504
+ for (const [, targetRel] of Object.entries(targetRelations)) {
505
+ if (targetRel.direction === "inverse" &&
506
+ targetRel.cardinality === "many" &&
507
+ targetRel.inverseRelationName === owningRelationName) {
508
+ inverseRelationName = targetRel.relationName ?? null;
509
+ break;
510
+ }
511
+ }
512
+ } catch {
513
+ // ignore — inverse side may not exist
514
+ }
515
+
516
+ // Source side one(): pairs with owning table's many(junctionTable, { relationName })
517
+ tableRelations.push(` ${relation.through.sourceColumn}: one(${sourceTableVar}, {\n fields: [${tableVarName}.${relation.through.sourceColumn}],\n references: [${sourceTableVar}.${sourceId}],\n relationName: \"${owningRelationName}\"\n })`);
518
+
519
+ // Target side one(): pairs with inverse table's many(junctionTable, { relationName })
520
+ const targetRelName = inverseRelationName ?? owningRelationName;
521
+ tableRelations.push(` ${relation.through.targetColumn}: one(${targetTableVar}, {\n fields: [${tableVarName}.${relation.through.targetColumn}],\n references: [${targetTableVar}.${targetId}],\n relationName: \"${targetRelName}\"\n })`);
522
+ }
523
+ } else {
524
+ const resolvedRelations = resolveCollectionRelations(collection as import("@rebasepro/types").PostgresCollection<any, any>);
525
+ for (const [relationKey, rel] of Object.entries(resolvedRelations)) {
526
+ try {
527
+ const target = rel.target();
528
+ const targetTableVar = getTableVarName(getTableName(target));
529
+ const relationName = rel.relationName ?? relationKey;
530
+
531
+ // Determine the correct relation name for Drizzle
532
+ // For inverse relations, we should use the current relation's name, not the inverse name
533
+ const drizzleRelationName = relationName;
534
+
535
+ if (rel.cardinality === "one") {
536
+ if (rel.direction === "owning" && rel.localKey) {
537
+ tableRelations.push(` ${relationKey}: one(${targetTableVar}, {\n fields: [${tableVarName}.${rel.localKey}],\n references: [${targetTableVar}.${getPrimaryKeyName(target)}],\n relationName: \"${drizzleRelationName}\"\n })`);
538
+ } else if (rel.direction === "inverse" && rel.foreignKeyOnTarget) {
539
+ const sourceIdField = getPrimaryKeyName(collection);
540
+ tableRelations.push(` ${relationKey}: one(${targetTableVar}, {\n fields: [${tableVarName}.${sourceIdField}],\n references: [${targetTableVar}.${rel.foreignKeyOnTarget}],\n relationName: \"${drizzleRelationName}\"\n })`);
541
+ } else if (rel.direction === "inverse" && !rel.foreignKeyOnTarget) {
542
+ // Handle inverse one-to-one relations where the FK is on the target table
543
+ // but foreignKeyOnTarget is not explicitly specified
544
+ // In this case, we need to find the corresponding owning relation on the target
545
+ try {
546
+ const targetCollection = rel.target();
547
+ const targetResolvedRelations = resolveCollectionRelations(targetCollection as import("@rebasepro/types").PostgresCollection<any, any>);
548
+
549
+ // Find the owning relation on the target that points back to this collection
550
+ const correspondingRelation = Object.values(targetResolvedRelations).find(targetRel =>
551
+ targetRel.direction === "owning" &&
552
+ targetRel.cardinality === "one" &&
553
+ targetRel.target().slug === collection.slug
554
+ );
555
+
556
+ if (correspondingRelation && correspondingRelation.localKey) {
557
+ const sourceIdField = getPrimaryKeyName(collection);
558
+ tableRelations.push(` ${relationKey}: one(${targetTableVar}, {\n fields: [${tableVarName}.${sourceIdField}],\n references: [${targetTableVar}.${correspondingRelation.localKey}],\n relationName: \"${drizzleRelationName}\"\n })`);
559
+ }
560
+ } catch (e) {
561
+ console.warn(`Could not resolve inverse one-to-one relation '${relationKey}':`, e);
562
+ }
563
+ }
564
+ } else if (rel.cardinality === "many") {
565
+ if (rel.direction === "inverse" && rel.foreignKeyOnTarget) {
566
+ // One-to-many inverse relation
567
+ tableRelations.push(` ${relationKey}: many(${targetTableVar}, { relationName: \"${drizzleRelationName}\" })`);
568
+ } else if (rel.through) {
569
+ // Many-to-many owning relation with explicit junction table
570
+ const junctionTableVar = getTableVarName(rel.through.table);
571
+ tableRelations.push(` ${relationKey}: many(${junctionTableVar}, { relationName: \"${drizzleRelationName}\" })`);
572
+ } else if (rel.direction === "inverse" && rel.inverseRelationName) {
573
+ // Many-to-many inverse relation - find the corresponding owning relation's junction table
574
+ try {
575
+ const targetCollection = rel.target();
576
+ const targetResolvedRelations = resolveCollectionRelations(targetCollection as import("@rebasepro/types").PostgresCollection<any, any>);
577
+
578
+ // Find the corresponding owning many-to-many relation on the target
579
+ const correspondingRelation = Object.values(targetResolvedRelations).find(targetRel =>
580
+ targetRel.direction === "owning" &&
581
+ targetRel.cardinality === "many" &&
582
+ targetRel.through &&
583
+ targetRel.relationName === rel.inverseRelationName
584
+ );
585
+
586
+ if (correspondingRelation && correspondingRelation.through) {
587
+ const junctionTableVar = getTableVarName(correspondingRelation.through.table);
588
+ tableRelations.push(` ${relationKey}: many(${junctionTableVar}, { relationName: \"${drizzleRelationName}\" })`);
589
+ } else {
590
+ console.warn(`Could not find corresponding owning many-to-many relation for inverse relation '${relationKey}' on '${collection.name}'`);
591
+ }
592
+ } catch (e) {
593
+ console.warn(`Could not resolve inverse many-to-many relation '${relationKey}':`, e);
594
+ }
595
+ }
596
+ // joinPath relations don't generate Drizzle relations - they use existing user tables
597
+ }
598
+ } catch (e) {
599
+ console.warn(`Could not generate relation ${relationKey} for ${collection.name}:`, e);
600
+ }
601
+ }
602
+ }
603
+
604
+ if (tableRelations.length > 0) {
605
+ const relVarName = `${tableVarName}Relations`;
606
+ schemaContent += `export const ${relVarName} = drizzleRelations(${tableVarName}, ({ one, many }) => ({\n${tableRelations.join(",\n")}\n}));\n\n`;
607
+ if (!exportedRelationVars.includes(relVarName)) exportedRelationVars.push(relVarName);
608
+ }
609
+ }
610
+
611
+ // <<< ADDED: Final aggregated exports block
612
+ const tablesExport = `export const tables = { ${exportedTableVars.join(", ")} };\n`;
613
+ const enumsExport = `export const enums = { ${exportedEnumVars.join(", ")} };\n`;
614
+ const relationsExport = `export const relations = { ${exportedRelationVars.join(", ")} };\n\n`;
615
+ schemaContent += tablesExport + enumsExport + relationsExport;
616
+
617
+ return schemaContent;
618
+ };