@rebasepro/server-postgres 0.17.3 → 0.18.1

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 (204) hide show
  1. package/LICENSE +0 -1
  2. package/README.md +31 -0
  3. package/dist/BranchService-ByLfM_Tt.js +360 -0
  4. package/dist/BranchService-ByLfM_Tt.js.map +1 -0
  5. package/dist/__vite-browser-external-BnuHet1e.js +46 -0
  6. package/dist/__vite-browser-external-BnuHet1e.js.map +1 -0
  7. package/dist/arg-Dni7MzLB.js +87 -0
  8. package/dist/arg-Dni7MzLB.js.map +1 -0
  9. package/dist/auth-users-columns-D2LBFrMH.js +116 -0
  10. package/dist/auth-users-columns-D2LBFrMH.js.map +1 -0
  11. package/dist/backup-argv.d.ts +19 -0
  12. package/dist/backup-cli-oM9WQVlV.js +408 -0
  13. package/dist/backup-cli-oM9WQVlV.js.map +1 -0
  14. package/dist/backup-service-Br3tqwsy.js +715 -0
  15. package/dist/backup-service-Br3tqwsy.js.map +1 -0
  16. package/dist/branch-argv.d.ts +28 -0
  17. package/dist/branch-prune.d.ts +77 -0
  18. package/dist/cli-collections-path.d.ts +31 -0
  19. package/dist/cli-errors-DxN4yzdH.js +657 -0
  20. package/dist/cli-errors-DxN4yzdH.js.map +1 -0
  21. package/dist/cli-errors.d.ts +123 -0
  22. package/dist/cli-flags.d.ts +101 -0
  23. package/dist/cli-helpers.d.ts +94 -10
  24. package/dist/cli-output-CNdMql-L.js +47 -0
  25. package/dist/cli-output-CNdMql-L.js.map +1 -0
  26. package/dist/cli.js +2020 -0
  27. package/dist/cli.js.map +1 -0
  28. package/dist/collection-index-BHgT_TKd.js +276 -0
  29. package/dist/collection-index-BHgT_TKd.js.map +1 -0
  30. package/dist/{collections-schema-version-BMeu3cgv.js → collections-schema-version-T6cGvrDi.js} +1 -2
  31. package/dist/{collections-schema-version-BMeu3cgv.js.map → collections-schema-version-T6cGvrDi.js.map} +1 -1
  32. package/dist/{connection-GOKU3Hu5.js → databasePoolManager-Bj5FbeAs.js} +112 -40
  33. package/dist/databasePoolManager-Bj5FbeAs.js.map +1 -0
  34. package/dist/databasePoolManager.d.ts +27 -0
  35. package/dist/doctor-D7HWmGzs.js +974 -0
  36. package/dist/doctor-D7HWmGzs.js.map +1 -0
  37. package/dist/{ensure-collection-policies-UCqgv_8c.js → ensure-collection-policies-2c2NV8zX.js} +7 -27
  38. package/dist/ensure-collection-policies-2c2NV8zX.js.map +1 -0
  39. package/dist/ensure-collection-tables-D4wJzM4y.js +820 -0
  40. package/dist/ensure-collection-tables-D4wJzM4y.js.map +1 -0
  41. package/dist/ensure-tables-CIojK-Rx.js +650 -0
  42. package/dist/ensure-tables-CIojK-Rx.js.map +1 -0
  43. package/dist/generate-drizzle-schema-CwEgVyjO.js +123 -0
  44. package/dist/generate-drizzle-schema-CwEgVyjO.js.map +1 -0
  45. package/dist/generate-drizzle-schema-logic-so2Z_lyC.js +528 -0
  46. package/dist/generate-drizzle-schema-logic-so2Z_lyC.js.map +1 -0
  47. package/dist/{ensure-collection-tables-DgVixhX3.js → generate-postgres-ddl-logic-CtnpIBVv.js} +126 -984
  48. package/dist/generate-postgres-ddl-logic-CtnpIBVv.js.map +1 -0
  49. package/dist/generated-schema-staleness-DQdfJl2A.js +157 -0
  50. package/dist/generated-schema-staleness-DQdfJl2A.js.map +1 -0
  51. package/dist/index.d.ts +12 -0
  52. package/dist/index.es.js +7423 -1133
  53. package/dist/index.es.js.map +1 -1
  54. package/dist/introspect-db-logic-C6LQdTxj.js +1378 -0
  55. package/dist/introspect-db-logic-C6LQdTxj.js.map +1 -0
  56. package/dist/policy-drift-kNJ6X2Uj.js +313 -0
  57. package/dist/policy-drift-kNJ6X2Uj.js.map +1 -0
  58. package/dist/{backup-service-DCk7KhhL.js → rls-bootstrap-sql-DLDnUjtx.js} +737 -2689
  59. package/dist/rls-bootstrap-sql-DLDnUjtx.js.map +1 -0
  60. package/dist/{rls-enforcement-DvAbL9YJ.js → rls-enforcement-DeYEjYLt.js} +65 -190
  61. package/dist/rls-enforcement-DeYEjYLt.js.map +1 -0
  62. package/dist/schema/bin/generate-drizzle-schema.d.ts +1 -0
  63. package/dist/schema/bin/generate-postgres-ddl.d.ts +1 -0
  64. package/dist/schema/column-type-drift.d.ts +79 -0
  65. package/dist/schema/doctor-cli.js +129 -0
  66. package/dist/schema/doctor-cli.js.map +1 -0
  67. package/dist/schema/doctor.d.ts +42 -0
  68. package/dist/schema/ensure-collection-tables.d.ts +90 -0
  69. package/dist/schema/generate-drizzle-schema.d.ts +1 -1
  70. package/dist/schema/generate-drizzle-schema.js +24 -0
  71. package/dist/schema/generate-drizzle-schema.js.map +1 -0
  72. package/dist/schema/generate-next-step.d.ts +30 -0
  73. package/dist/schema/generate-postgres-ddl-logic.d.ts +28 -0
  74. package/dist/schema/generate-postgres-ddl.d.ts +1 -1
  75. package/dist/schema/generate-postgres-ddl.js +138 -0
  76. package/dist/schema/generate-postgres-ddl.js.map +1 -0
  77. package/dist/schema/generated-schema-staleness.d.ts +37 -0
  78. package/dist/schema/introspect-db-queries.d.ts +34 -0
  79. package/dist/schema/introspect-db.js +866 -0
  80. package/dist/schema/introspect-db.js.map +1 -0
  81. package/dist/security/rls-enforcement.d.ts +63 -2
  82. package/dist/services/BranchService.d.ts +86 -1
  83. package/dist/services/collection-helpers.d.ts +1 -1
  84. package/dist/services/realtimeService.d.ts +30 -2
  85. package/dist/services/row-pipeline.d.ts +12 -1
  86. package/dist/source-Br7L7GOI.js +410 -0
  87. package/dist/source-Br7L7GOI.js.map +1 -0
  88. package/dist/utils/pg-error-utils.d.ts +3 -1
  89. package/dist/utils/pg-numeric-number-patch.d.ts +31 -0
  90. package/dist/vector-index-DkWxm2de.js +309 -0
  91. package/dist/vector-index-DkWxm2de.js.map +1 -0
  92. package/package.json +32 -27
  93. package/dist/backup-service-DCk7KhhL.js.map +0 -1
  94. package/dist/collection-index-BRUg10H5.js +0 -5690
  95. package/dist/collection-index-BRUg10H5.js.map +0 -1
  96. package/dist/connection-GOKU3Hu5.js.map +0 -1
  97. package/dist/ensure-collection-policies-UCqgv_8c.js.map +0 -1
  98. package/dist/ensure-collection-tables-DgVixhX3.js.map +0 -1
  99. package/dist/rls-bootstrap-sql-B5C9LoJ6.js +0 -244
  100. package/dist/rls-bootstrap-sql-B5C9LoJ6.js.map +0 -1
  101. package/dist/rls-enforcement-DvAbL9YJ.js.map +0 -1
  102. package/dist/src-DiB5RP2Z.js +0 -541
  103. package/dist/src-DiB5RP2Z.js.map +0 -1
  104. package/dist/websocket-BZ4H5wUz.js +0 -8198
  105. package/dist/websocket-BZ4H5wUz.js.map +0 -1
  106. package/src/PostgresAdapter.ts +0 -91
  107. package/src/PostgresBackendDriver.ts +0 -1857
  108. package/src/PostgresBootstrapper.ts +0 -1157
  109. package/src/auth/ensure-tables.ts +0 -896
  110. package/src/auth/schema-version.ts +0 -260
  111. package/src/auth/services.ts +0 -1610
  112. package/src/backup/backup-cli.ts +0 -520
  113. package/src/backup/backup-cron.ts +0 -212
  114. package/src/backup/backup-logic.ts +0 -93
  115. package/src/backup/backup-service.ts +0 -485
  116. package/src/backup/index.ts +0 -12
  117. package/src/backup/pg-tools.ts +0 -403
  118. package/src/backup/retention.ts +0 -86
  119. package/src/cli-errors.ts +0 -265
  120. package/src/cli-helpers.ts +0 -569
  121. package/src/cli-output.ts +0 -43
  122. package/src/cli.ts +0 -1339
  123. package/src/collections/PostgresCollectionRegistry.ts +0 -108
  124. package/src/collections/buildRegistry.ts +0 -68
  125. package/src/collections/validate-relations.ts +0 -402
  126. package/src/connection.ts +0 -333
  127. package/src/data-transformer.ts +0 -845
  128. package/src/databasePoolManager.ts +0 -95
  129. package/src/history/HistoryService.ts +0 -254
  130. package/src/history/ensure-history-table.ts +0 -53
  131. package/src/index.ts +0 -19
  132. package/src/interfaces.ts +0 -60
  133. package/src/module-dir.ts +0 -7
  134. package/src/schema/atlas-argv.ts +0 -94
  135. package/src/schema/auth-schema.ts +0 -298
  136. package/src/schema/auth-users-columns.ts +0 -131
  137. package/src/schema/carved-out-migration.ts +0 -404
  138. package/src/schema/classify-change.ts +0 -436
  139. package/src/schema/collection-index.ts +0 -427
  140. package/src/schema/collections-schema-version.ts +0 -103
  141. package/src/schema/destructive-sql.ts +0 -94
  142. package/src/schema/doctor-cli.ts +0 -66
  143. package/src/schema/doctor-policy-checks.ts +0 -105
  144. package/src/schema/doctor.ts +0 -837
  145. package/src/schema/drizzle-ddl.ts +0 -33
  146. package/src/schema/dynamic-tables.test.ts +0 -302
  147. package/src/schema/dynamic-tables.ts +0 -293
  148. package/src/schema/ensure-collection-policies.ts +0 -198
  149. package/src/schema/ensure-collection-tables.test.ts +0 -518
  150. package/src/schema/ensure-collection-tables.ts +0 -1267
  151. package/src/schema/generate-drizzle-schema-logic.ts +0 -957
  152. package/src/schema/generate-drizzle-schema.ts +0 -143
  153. package/src/schema/generate-postgres-ddl-logic.ts +0 -1339
  154. package/src/schema/generate-postgres-ddl.ts +0 -180
  155. package/src/schema/generate-schema-commit.ts +0 -267
  156. package/src/schema/generated-schema-staleness.ts +0 -284
  157. package/src/schema/introspect-db-constraints.ts +0 -385
  158. package/src/schema/introspect-db-inference.ts +0 -255
  159. package/src/schema/introspect-db-logic.ts +0 -1387
  160. package/src/schema/introspect-db-naming.ts +0 -15
  161. package/src/schema/introspect-db-project.ts +0 -78
  162. package/src/schema/introspect-db-queries.ts +0 -326
  163. package/src/schema/introspect-db-structure.ts +0 -670
  164. package/src/schema/introspect-db-types.ts +0 -56
  165. package/src/schema/introspect-db.ts +0 -257
  166. package/src/schema/introspect-runtime.test.ts +0 -260
  167. package/src/schema/introspect-runtime.ts +0 -327
  168. package/src/schema/non-sql-collections.test.ts +0 -131
  169. package/src/schema/rls-bootstrap-sql.ts +0 -288
  170. package/src/schema/search-column.ts +0 -643
  171. package/src/schema/test-schema.ts +0 -11
  172. package/src/schema/vector-index.ts +0 -491
  173. package/src/security/anonymous-grants.test.ts +0 -73
  174. package/src/security/policy-drift.test.ts +0 -435
  175. package/src/security/policy-drift.ts +0 -473
  176. package/src/security/rls-enforcement.ts +0 -505
  177. package/src/services/BranchService.ts +0 -326
  178. package/src/services/FetchService.ts +0 -2115
  179. package/src/services/PersistService.ts +0 -557
  180. package/src/services/RelationService.ts +0 -978
  181. package/src/services/RelationWriteService.ts +0 -653
  182. package/src/services/cdc/CdcListener.ts +0 -103
  183. package/src/services/cdc/junction-tables.ts +0 -91
  184. package/src/services/cdc/trigger-cdc.ts +0 -173
  185. package/src/services/channel-bus/ChannelBus.ts +0 -44
  186. package/src/services/channel-bus/PostgresChannelBus.ts +0 -299
  187. package/src/services/channel-bus/index.ts +0 -114
  188. package/src/services/channel-history.ts +0 -424
  189. package/src/services/channel-presence.ts +0 -185
  190. package/src/services/collection-helpers.ts +0 -420
  191. package/src/services/dataService.ts +0 -256
  192. package/src/services/index.ts +0 -14
  193. package/src/services/junction-writes.ts +0 -295
  194. package/src/services/nested-path.ts +0 -145
  195. package/src/services/pg-notify-listener.ts +0 -151
  196. package/src/services/realtimeService.ts +0 -2545
  197. package/src/services/row-pipeline.ts +0 -240
  198. package/src/services/write-denial.ts +0 -55
  199. package/src/types.ts +0 -4
  200. package/src/utils/connection-string.ts +0 -58
  201. package/src/utils/drizzle-conditions.ts +0 -2743
  202. package/src/utils/pg-array-null-patch.ts +0 -42
  203. package/src/utils/pg-error-utils.ts +0 -391
  204. package/src/websocket.ts +0 -855
@@ -1,957 +0,0 @@
1
- import { CollectionConfig, NumberProperty, Property, ResolvedRelation, RelationProperty, SecurityOperation, SecurityRule, StringProperty, isPostgresCollectionConfig, DateProperty, ArrayProperty, MapProperty, ReferenceProperty, VectorProperty, BinaryProperty, isManyToMany, type ResolvedManyToMany, type ResolvedBelongsTo, type ResolvedForeignKeyOnTarget, hasForeignKeyOnTarget } from "@rebasepro/types";
2
- import { getPrimaryKeys } from "../services/collection-helpers";
3
- import { buildSearchColumnSpec } from "./search-column";
4
- import { getEnumVarName, getTableName, getTableVarName, resolveCollectionRelations, findRelation, fieldKeyForColumn, securityRuleToConditions, policyToPostgres, getEffectiveSecurityRules, resolveJunctionSpecs, getJunctionSecurityRules, getJunctionCollectionConfig, resolveStringColumnLength, relationalCollections, sortCollectionsBySlug } from "@rebasepro/common";
5
- import { toSnakeCase, getPolicyNamesForRule } from "@rebasepro/utils";
6
- import { logger } from "@rebasepro/server";
7
- // --- Helper Functions ---
8
-
9
- /**
10
- * Resolve the SQL column name for a property.
11
- * Uses the explicit `columnName` when set (e.g. from introspection),
12
- * falling back to `toSnakeCase(propName)` for manually-authored collections.
13
- */
14
- const JS_IDENTIFIER = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
15
-
16
- /**
17
- * A string literal for the generated schema file.
18
- *
19
- * Column names, table names and enum values are all written into this file as
20
- * literals, and none of them is constrained to be quote-free: a Postgres
21
- * identifier only has to be quoted, and `O'Brien` is an ordinary enum value.
22
- * Interpolating them raw ended the literal early — for enum values, inside
23
- * single quotes, where an apostrophe is not an edge case.
24
- */
25
- const quote = (value: string): string => JSON.stringify(value);
26
-
27
- /** An object key: verbatim when it is an identifier, quoted otherwise. */
28
- const propKey = (name: string): string => (JS_IDENTIFIER.test(name) ? name : quote(name));
29
-
30
- /**
31
- * A property access on a generated table variable.
32
- *
33
- * `users.full name` is not an expression; `users["full name"]` is, and Drizzle
34
- * treats the two identically.
35
- */
36
- const member = (object: string, key: string): string =>
37
- (JS_IDENTIFIER.test(key) ? `${object}.${key}` : `${object}[${quote(key)}]`);
38
-
39
- const resolveColumnName = (propName: string, prop?: Property | null): string => {
40
- if (prop && "columnName" in prop && typeof prop.columnName === "string") {
41
- return prop.columnName;
42
- }
43
- return toSnakeCase(propName);
44
- };
45
-
46
- const getPrimaryKeyProp = (collection: CollectionConfig): { name: string, type: "string" | "number", isUuid: boolean } => {
47
- if (collection.properties) {
48
- const idPropEntry = Object.entries(collection.properties).find(([_, prop]) => "isId" in (prop as unknown as object) && Boolean((prop as unknown as Record<string, unknown>).isId));
49
- if (idPropEntry) {
50
- const prop = idPropEntry[1] as unknown as Property;
51
- const isUuid = prop.type === "string" && "isId" in prop && (prop as unknown as StringProperty).isId === "uuid";
52
- return { name: idPropEntry[0],
53
- type: prop.type === "number" ? "number" : "string",
54
- isUuid };
55
- }
56
- }
57
- // Fallback
58
- const idProp = collection.properties?.["id"] as unknown as Property | undefined;
59
- if (idProp?.type === "number") {
60
- return { name: "id",
61
- type: "number",
62
- isUuid: false };
63
- }
64
- const isUuid = idProp?.type === "string" && "isId" in idProp && (idProp as unknown as StringProperty).isId === "uuid";
65
- return { name: "id",
66
- type: "string",
67
- isUuid: isUuid ?? false };
68
- };
69
-
70
- /**
71
- * Given a raw DB column name (e.g. "client_id"), the Drizzle property key that
72
- * maps to it.
73
- *
74
- * One line, because the rule is shared: the Drizzle object key is the wire
75
- * name, and {@link fieldKeyForColumn} is the one definition of what a column is
76
- * named on the wire. This used to be a private copy that fell back to the
77
- * column verbatim, which is how a derived foreign key ended up served as
78
- * `author_id` beside a hand-authored `displayName`.
79
- */
80
- const resolvePropertyKeyForColumn = (collection: CollectionConfig, column: string): string =>
81
- fieldKeyForColumn(collection, column);
82
-
83
- const isNumericId = (collection: CollectionConfig): boolean => {
84
- return getPrimaryKeyProp(collection).type === "number";
85
- };
86
-
87
- const getPrimaryKeyName = (collection: CollectionConfig): string => {
88
- return getPrimaryKeyProp(collection).name;
89
- };
90
-
91
- const isIdProperty = (propName: string, prop: Property, collection: CollectionConfig): boolean => {
92
- if ("isId" in prop && Boolean(prop.isId)) return true;
93
-
94
- // We only fallback to "id" if NO property is explicitly marked with `isId: true` or a generator string
95
- const hasExplicitId = Object.values(collection.properties ?? {}).some(p => "isId" in (p as unknown as object) && Boolean((p as unknown as Record<string, unknown>).isId));
96
- return !hasExplicitId && propName === "id";
97
- };
98
-
99
- /**
100
- * The Drizzle column declaration a property compiles to, or `null` when the
101
- * property puts no column on *this* table (an inverse relation, whose column
102
- * lives on the target). Exported so it can be checked against its DDL twin
103
- * `getSqlColumnType` directly — the two disagreeing is what left `geopoint`
104
- * with a database column and no Drizzle key.
105
- */
106
- export const getDrizzleColumn = (propName: string, prop: Property, collection: CollectionConfig, collections: CollectionConfig[]): string | null => {
107
-
108
- const colName = resolveColumnName(propName, prop);
109
- let columnDefinition: string;
110
-
111
- switch (prop.type) {
112
- case "string": {
113
- const stringProp = prop as unknown as StringProperty;
114
- if (stringProp.enum) {
115
- const enumName = getEnumVarName(getTableName(collection), propName);
116
- columnDefinition = `${enumName}(${quote(colName)})`;
117
- } else if ("isId" in stringProp && stringProp.isId === "uuid") {
118
- columnDefinition = `uuid(${quote(colName)})`;
119
- } else if (stringProp.columnType === "uuid") {
120
- columnDefinition = `uuid(${quote(colName)})`;
121
- } else if (stringProp.columnType === "char") {
122
- columnDefinition = `char(${quote(colName)}, { length: ${resolveStringColumnLength(stringProp)} })`;
123
- } else if (stringProp.columnType === "varchar") {
124
- // The length is not optional decoration: `varchar("col")` with
125
- // no length is an UNBOUNDED varchar in Postgres, which is what
126
- // this emitted while the DDL generator emitted VARCHAR(255) for
127
- // the very same property.
128
- columnDefinition = `varchar(${quote(colName)}, { length: ${resolveStringColumnLength(stringProp)} })`;
129
- } else {
130
- // `text` is the default, and the only length-unbounded choice.
131
- // Ask for `varchar` explicitly if you want the length constraint.
132
- columnDefinition = `text(${quote(colName)})`;
133
- }
134
- if (isIdProperty(propName, prop, collection)) {
135
- columnDefinition += ".primaryKey()";
136
- }
137
- if ("isId" in stringProp && stringProp.isId !== "manual" && stringProp.isId !== true) {
138
- if (stringProp.isId === "uuid") {
139
- columnDefinition += ".defaultRandom()";
140
- } else if (stringProp.isId === "cuid") {
141
- columnDefinition += ".default(sql`cuid()`)";
142
- } else if (typeof stringProp.isId === "string") {
143
- const sqlContent = stringProp.isId.startsWith("sql`") && stringProp.isId.endsWith("`")
144
- ? stringProp.isId.substring(4, stringProp.isId.length - 1)
145
- : stringProp.isId;
146
- columnDefinition += `.default(sql\`${sqlContent}\`)`;
147
- }
148
- }
149
- if (stringProp.validation?.unique) {
150
- columnDefinition += ".unique()";
151
- }
152
- break;
153
- }
154
- case "number": {
155
- const numProp = prop as unknown as NumberProperty;
156
- const isId = isIdProperty(propName, prop, collection);
157
-
158
- let baseType = (numProp.validation?.integer || isId) ? `integer(${quote(colName)})` : `numeric(${quote(colName)})`;
159
- if (numProp.columnType) {
160
- if (numProp.columnType === "double precision") baseType = `doublePrecision(${quote(colName)})`;
161
- // `bigint` and `bigserial` are the only pg-core builders that
162
- // *require* a config argument: without `mode`, drizzle cannot
163
- // know whether to hand back a `number` or a `bigint`, and the
164
- // emitted call does not typecheck.
165
- //
166
- // This is why `schema.generated.ts` drifted. Regenerating it
167
- // produced a file that would not compile, so the bigint lines
168
- // were hand-patched — and every regeneration after that looked
169
- // like a large, alarming diff nobody wanted to ship. The file
170
- // then sat stale for long enough that a security fix to two RLS
171
- // policies never reached production.
172
- //
173
- // `number` rather than `bigint`: these are counters and byte
174
- // totals that every caller already treats as numbers, and
175
- // switching the runtime type would be a breaking change to
176
- // every consumer of the generated schema.
177
- else if (numProp.columnType === "bigint" || numProp.columnType === "bigserial") {
178
- baseType = `${numProp.columnType}(${quote(colName)}, { mode: "number" })`;
179
- }
180
- else baseType = `${numProp.columnType}(${quote(colName)})`;
181
- }
182
-
183
- if ("isId" in numProp && numProp.isId === "increment") {
184
- columnDefinition = `${baseType}.generatedByDefaultAsIdentity()`;
185
- } else if ("isId" in numProp && typeof numProp.isId === "string" && numProp.isId !== "manual") {
186
- columnDefinition = baseType;
187
- const sqlContent = numProp.isId.startsWith("sql`") && numProp.isId.endsWith("`")
188
- ? numProp.isId.substring(4, numProp.isId.length - 1)
189
- : numProp.isId;
190
- columnDefinition += `.default(sql\`${sqlContent}\`)`;
191
- } else {
192
- columnDefinition = baseType;
193
- }
194
-
195
- if (isId) {
196
- columnDefinition += ".primaryKey()";
197
- }
198
- if (numProp.validation?.unique) {
199
- columnDefinition += ".unique()";
200
- }
201
- break;
202
- }
203
- case "boolean":
204
- columnDefinition = `boolean(${quote(colName)})`;
205
- break;
206
- case "date": {
207
- const dateProp = prop as DateProperty;
208
- if (dateProp.columnType === "date") {
209
- columnDefinition = `date(${quote(colName)}, { mode: 'string' })`;
210
- } else if (dateProp.columnType === "time") {
211
- columnDefinition = `time(${quote(colName)})`;
212
- } else {
213
- columnDefinition = `timestamp(${quote(colName)}, { withTimezone: true, mode: 'string' })`;
214
- }
215
- // autoValue: database-level default for initial value on INSERT
216
- if (dateProp.autoValue === "on_create" || dateProp.autoValue === "on_update") {
217
- columnDefinition += ".default(sql`now()`)";
218
- }
219
- break;
220
- }
221
- case "map": {
222
- const mapProp = prop as MapProperty;
223
- if (mapProp.columnType === "json") {
224
- columnDefinition = `json(${quote(colName)})`;
225
- } else {
226
- columnDefinition = `jsonb(${quote(colName)})`;
227
- }
228
- break;
229
- }
230
- case "geopoint": {
231
- // `{ latitude, longitude }`, which is what the OpenAPI schema, the
232
- // generated TS type and the admin's field binding all describe.
233
- // This arm did not exist: `geopoint` fell to `default: return null`
234
- // and the caller dropped the column, so the DDL generator created a
235
- // column the Drizzle table had no key for — and every write to it
236
- // was silently discarded with a 201.
237
- columnDefinition = `jsonb(${quote(colName)})`;
238
- break;
239
- }
240
- case "array": {
241
- const arrayProp = prop as ArrayProperty;
242
- let colType = arrayProp.columnType;
243
- if (!colType && arrayProp.of && !Array.isArray(arrayProp.of)) {
244
- const ofProp = arrayProp.of as Property;
245
- if (ofProp.type === "string") {
246
- colType = "text[]";
247
- } else if (ofProp.type === "number") {
248
- colType = ofProp.validation?.integer ? "integer[]" : "numeric[]";
249
- } else if (ofProp.type === "boolean") {
250
- colType = "boolean[]";
251
- }
252
- }
253
-
254
- if (colType === "json") {
255
- columnDefinition = `json(${quote(colName)})`;
256
- } else if (colType === "text[]") {
257
- columnDefinition = `text(${quote(colName)}).array()`;
258
- } else if (colType === "integer[]") {
259
- columnDefinition = `integer(${quote(colName)}).array()`;
260
- } else if (colType === "boolean[]") {
261
- columnDefinition = `boolean(${quote(colName)}).array()`;
262
- } else if (colType === "numeric[]") {
263
- columnDefinition = `numeric(${quote(colName)}).array()`;
264
- } else {
265
- columnDefinition = `jsonb(${quote(colName)})`;
266
- }
267
- break;
268
- }
269
- case "vector": {
270
- const vp = prop as VectorProperty;
271
- columnDefinition = `vector(${quote(colName)}, { dimensions: ${vp.dimensions} })`;
272
- break;
273
- }
274
- case "binary": {
275
- columnDefinition = `customType({ dataType() { return 'bytea'; } })(${quote(colName)})`;
276
- break;
277
- }
278
- case "relation": {
279
- const refProp = prop as RelationProperty;
280
- const resolvedRelations = resolveCollectionRelations(collection);
281
- const relation = findRelation(resolvedRelations, refProp.relation?.relationName ?? propName);
282
-
283
- // Only `belongsTo` puts a column on this table; every other kind
284
- // is a column on the target, a junction row, or a join chain.
285
- if (!relation || relation.kind !== "belongsTo") {
286
- return null;
287
- }
288
-
289
- // If a property of its own already declares this foreign key, that
290
- // property emits the column and this relation must not emit a second
291
- // key for it. Asked of the *field key*, not the column: a property
292
- // declared `authorId` with `columnName: "author_id"` did not answer
293
- // to `properties["author_id"]`, so both sides emitted and the table
294
- // carried two Drizzle keys pointing at one column.
295
- const fkFieldKey = fieldKeyForColumn(collection, relation.localKey);
296
- if (collection.properties[fkFieldKey] && propName !== fkFieldKey) {
297
- return null;
298
- }
299
-
300
- let targetCollection: CollectionConfig;
301
- try {
302
- targetCollection = relation.target();
303
- } catch {
304
- return null; // Cannot resolve target
305
- }
306
-
307
- const fkColumnName = relation.localKey;
308
- const targetTableVar = getTableVarName(getTableName(targetCollection));
309
- const pkProp = getPrimaryKeyProp(targetCollection);
310
- const targetIdField = pkProp.name;
311
- // `quote`, like every other column literal in this file: a column
312
- // name only has to be quotable in Postgres, and this one is derived
313
- // from a relation the author wrote.
314
- const baseColumn = pkProp.type === "number"
315
- ? `integer(${quote(fkColumnName)})`
316
- : (pkProp.isUuid ? `uuid(${quote(fkColumnName)})` : `text(${quote(fkColumnName)})`);
317
-
318
- const onUpdate = relation.onUpdate ? `onUpdate: "${relation.onUpdate}"` : "";
319
- const required = prop.validation?.required;
320
- const onDeleteVal = relation.onDelete ?? (required ? "cascade" : "set null");
321
- const onDelete = `onDelete: \"${onDeleteVal}\"`;
322
-
323
- const refOptionsParts = [onUpdate, onDelete].filter(Boolean);
324
- const refOptions = refOptionsParts.length > 0 ? `{ ${refOptionsParts.join(", ")} }` : "";
325
-
326
- let columnDef = `${baseColumn}.references(() => ${member(targetTableVar, targetIdField)}${refOptions ? `, ${refOptions}` : ""})`;
327
-
328
- if (required) {
329
- columnDef += ".notNull()";
330
- }
331
-
332
- // Key by the wire name, column by `localKey`. They are two different
333
- // names for one thing and the generated line is where they meet:
334
- // `authorId: integer("author_id")`.
335
- return ` ${propKey(fkFieldKey)}: ${columnDef}`;
336
- }
337
- case "reference": {
338
- const refProp = prop as ReferenceProperty;
339
- const targetCollection = collections.find(c => c.slug === refProp.path || getTableName(c) === refProp.path);
340
- if (!targetCollection) {
341
- columnDefinition = `text(${quote(colName)})`;
342
- break;
343
- }
344
-
345
- const pkProp = getPrimaryKeyProp(targetCollection);
346
- const targetTableVar = getTableVarName(getTableName(targetCollection));
347
- const targetIdField = pkProp.name;
348
- const baseColumn = pkProp.type === "number" ? `integer(${quote(colName)})` : (pkProp.isUuid ? `uuid(${quote(colName)})` : `text(${quote(colName)})`);
349
-
350
- const required = prop.validation?.required;
351
- const onDelete = required ? "cascade" : "set null";
352
- const refOptions = `{ onDelete: "${onDelete}" }`;
353
-
354
- columnDefinition = `${baseColumn}.references(() => ${member(targetTableVar, targetIdField)}, ${refOptions})`;
355
- if (required) {
356
- columnDefinition += ".notNull()";
357
- }
358
- // Skip the standard notNull() handling below because we did it here with references
359
- return ` ${propKey(propName)}: ${columnDefinition}`;
360
- }
361
- default:
362
- // Not `return null`. A `null` here means "this property puts no
363
- // column on this table", which is true of an inverse relation and
364
- // false of everything else — and the caller cannot tell the two
365
- // apart, so a type this switch simply forgot produced a table
366
- // missing a column while the DDL generator happily created one.
367
- // That is how `geopoint` stayed unpersistable: writes to it were
368
- // dropped before the SQL was built, forever, with a 201. A property
369
- // type nobody mapped is a generator bug, and it says so here rather
370
- // than at some caller's next INSERT.
371
- throw new Error(
372
- `No Postgres column mapping for property '${propName}' of type ` +
373
- `'${(prop as Property).type}' in collection '${collection.slug}'. ` +
374
- "Add a case to `getDrizzleColumn` (and to `getSqlColumnType`, which must agree)."
375
- );
376
- }
377
-
378
- if (prop.validation?.required) {
379
- columnDefinition += ".notNull()";
380
- }
381
-
382
- return ` ${propKey(propName)}: ${columnDefinition}`;
383
- };
384
-
385
- /**
386
- * Wraps a compiled SQL clause in a Drizzle `sql\`...\`` template literal.
387
- *
388
- * The clause is SQL being written into a TypeScript file, so it has to survive
389
- * being read back as a template literal. Three characters do not:
390
- *
391
- * - `` ` `` closes the template early, and the rest of the clause becomes code.
392
- * - `${` opens an interpolation — the file stops compiling, or worse, compiles
393
- * against whatever identifier happens to be in scope.
394
- * - `\` is an escape, and Drizzle's `sql` tag reads the *cooked* strings, not
395
- * `.raw`. So a policy written as `email ~ '^admin\.user@corp\.com$'` reaches
396
- * the database as `^admin.user@corp.com$`, where every `\.` now matches any
397
- * character. A `USING` clause is a security boundary and that one silently
398
- * widened it — the SQL file emitted by the DDL generator kept the backslashes
399
- * while this path dropped them, so the two disagreed about who could read the
400
- * table.
401
- *
402
- * Escaping here rather than in the compiler: the clause is correct SQL, and it
403
- * is only this destination that has an opinion about backslashes.
404
- */
405
- const wrapSql = (clause: string): string =>
406
- `sql\`${clause.replace(/\\/g, "\\\\").replace(/`/g, "\\`").replace(/\$\{/g, "\\${")}\``;
407
-
408
- /**
409
- * Generates a deterministic hash based on the rule configuration.
410
- */
411
-
412
- /**
413
- * Generates Drizzle pgPolicy() calls from a declarative SecurityRule definition.
414
- *
415
- * Supports the full spectrum:
416
- * - Convenience shortcuts: ownerField, access, roles
417
- * - Raw SQL: using, withCheck
418
- * - Mode: permissive (default) or restrictive
419
- * - operations[] array: generates one policy per operation
420
- * - Combinations: roles + ownerField, roles + raw SQL, etc.
421
- */
422
- type ResolveCollection = (slug: string) => CollectionConfig | undefined;
423
-
424
- const generatePolicyCode = (collection: CollectionConfig, rule: SecurityRule, index: number, resolveCollection: ResolveCollection): string => {
425
- const tableName = getTableName(collection);
426
- // Resolve operations: operations[] takes precedence over operation (singular)
427
- const ops: readonly SecurityOperation[] = rule.operations && rule.operations.length > 0
428
- ? rule.operations
429
- : [rule.operation ?? "all"];
430
-
431
- const policyNames = getPolicyNamesForRule(rule, tableName);
432
-
433
- // Generate one pgPolicy per operation
434
- return ops.map((op, opIdx) => {
435
- return generateSinglePolicyCode(collection, rule, op, policyNames[opIdx], resolveCollection);
436
- }).join("");
437
- };
438
-
439
- /**
440
- * Generates a single pgPolicy() call for one specific operation.
441
- */
442
- const generateSinglePolicyCode = (collection: CollectionConfig, rule: SecurityRule, operation: SecurityOperation, policyName: string, resolveCollection: ResolveCollection): string => {
443
- const mode = rule.mode ?? "permissive";
444
-
445
- // Determine which clauses this operation needs:
446
- // SELECT, DELETE → USING only
447
- // INSERT → WITH CHECK only
448
- // UPDATE, ALL → both USING and WITH CHECK
449
- const needsUsing = operation !== "insert";
450
- const needsWithCheck = operation !== "select" && operation !== "delete";
451
-
452
- // Desugar the rule (access / ownerField / roles / structured condition / raw
453
- // SQL) into the shared PolicyExpression model, then compile to SQL — the same
454
- // normalization the DDL generator and the client-side evaluator use.
455
- const { usingExpr, withCheckExpr } = securityRuleToConditions(rule);
456
-
457
- let usingClause = needsUsing && usingExpr ? wrapSql(policyToPostgres(usingExpr, collection, { resolveCollection })) : null;
458
- let withCheckClause = needsWithCheck && withCheckExpr ? wrapSql(policyToPostgres(withCheckExpr, collection, { resolveCollection })) : null;
459
-
460
- // Fallback: if we still have no clauses, deny all (safety net)
461
- if (!usingClause && needsUsing) {
462
- usingClause = "sql`false`";
463
- }
464
- if (!withCheckClause && needsWithCheck) {
465
- withCheckClause = "sql`false`";
466
- }
467
-
468
- // Build the policy options object
469
- const parts: string[] = [];
470
- parts.push(`as: "${mode}"`);
471
- parts.push(`for: "${operation}"`);
472
- const toRoles = rule.pgRoles ? [...rule.pgRoles].sort() : ["public"];
473
- parts.push(`to: [${toRoles.map(r => `"${r}"`).join(", ")}]`);
474
- if (usingClause) parts.push(`using: ${usingClause}`);
475
- if (withCheckClause) parts.push(`withCheck: ${withCheckClause}`);
476
-
477
- return ` pgPolicy(${quote(policyName)}, { ${parts.join(", ")} }),\n`;
478
- };
479
-
480
- /**
481
- * Computes a deterministic shared relation name for Drizzle.
482
- *
483
- * Drizzle requires both sides of a relation (owning + inverse) to use the
484
- * exact same `relationName` string so it can pair them. Each collection
485
- * definition may use a different local `relationName`, so we need a canonical
486
- * form that both sides can independently compute.
487
- *
488
- * Strategy: `{owningTable}_{foreignKey}`
489
- * - owning side → `{thisTable}_{localKey}` e.g. "jobs_company_id"
490
- * - inverse side → `{targetTable}_{foreignKeyOnTarget}` e.g. "jobs_company_id"
491
- *
492
- * For M2M with junction tables the owning relation name is already shared via
493
- * the junction table wiring, so we keep it as-is.
494
- *
495
- * Falls back to the local relation name when the counterpart can't be resolved.
496
- */
497
- const computeSharedRelationName = (
498
- rel: ResolvedRelation,
499
- sourceCollection: CollectionConfig,
500
- _collections: CollectionConfig[]
501
- ): string => {
502
- const fallback = rel.relationName ?? toSnakeCase(rel.target().slug);
503
-
504
- // Both sides of a link must derive the same name, so each resolves the
505
- // column to its Drizzle property key and builds the name from the table
506
- // that actually owns it.
507
- if (rel.kind === "belongsTo") {
508
- const normalisedKey = resolvePropertyKeyForColumn(sourceCollection, rel.localKey);
509
- return `${getTableName(sourceCollection)}_${normalisedKey}`;
510
- }
511
-
512
- if (rel.kind === "hasMany" || rel.kind === "hasOne") {
513
- // The owning table is the *target*; the column is foreignKeyOnTarget.
514
- try {
515
- const targetCollection = rel.target();
516
- const normalisedFK = resolvePropertyKeyForColumn(targetCollection, rel.foreignKeyOnTarget);
517
- return `${getTableName(targetCollection)}_${normalisedFK}`;
518
- } catch {
519
- return fallback;
520
- }
521
- }
522
-
523
- // manyToMany is named through its junction wiring; `via` is not emitted as
524
- // a Drizzle relation at all. Both keep the local name.
525
- return fallback;
526
- };
527
-
528
- // --- Main Schema Generation Logic ---
529
- export const generateSchema = async (allCollections: CollectionConfig[], stripPolicies = false): Promise<string> => {
530
- // A Firestore or MongoDB collection has no table to generate, and generating
531
- // one for it is not merely wasted output: `db push` would create it, and
532
- // `rebase doctor` would then report the store the collection actually reads
533
- // from as drift. Non-SQL collections leave the toolchain here, once, rather
534
- // than being filtered again in each stage below.
535
- //
536
- // Sorted here rather than in the writer: the output is order-dependent and
537
- // `rebase doctor` regenerates it in memory to diff against the file on
538
- // disk. With the sort in the writer only, a project whose file order
539
- // differed from its slug order was reported stale forever.
540
- const collections = sortCollectionsBySlug(relationalCollections(allCollections));
541
- let schemaContent = "// This file is auto-generated by the Rebase Drizzle generator. Do not edit manually.\n\n";
542
-
543
-
544
- const hasUuid = collections.some(c =>
545
- c.properties && Object.values(c.properties).some(
546
- (p: Property) => p.type === "string" && ((p as unknown as Record<string, unknown>).autoValue === "uuid" || (p as unknown as Record<string, unknown>).isId === "uuid")
547
- )
548
- );
549
-
550
- const hasVector = collections.some(c =>
551
- c.properties && Object.values(c.properties).some(
552
- (p: Property) => p.type === "vector"
553
- )
554
- );
555
-
556
- const hasBinary = collections.some(c =>
557
- c.properties && Object.values(c.properties).some(
558
- (p: Property) => p.type === "binary"
559
- )
560
- );
561
-
562
- // drizzle ships no `tsvector` builder, so an opted-in search column reaches
563
- // the schema the same way `bytea` does — through `customType`.
564
- const hasSearch = collections.some(c => buildSearchColumnSpec(c) !== undefined);
565
-
566
- // Always import pgPolicy and sql — RLS is enabled on every table (secure by default)
567
- const pgCoreImports = ["primaryKey", "pgTable", "integer", "varchar", "text", "char", "boolean", "timestamp", "date", "time", "jsonb", "json", "pgEnum", "numeric", "real", "doublePrecision", "bigint", "serial", "bigserial", "pgPolicy"];
568
- if (hasUuid) pgCoreImports.push("uuid");
569
- if (hasVector) pgCoreImports.push("vector");
570
- if (hasBinary || hasSearch) pgCoreImports.push("customType");
571
-
572
- const uniqueSchemas = Array.from(new Set(
573
- collections.map(c => isPostgresCollectionConfig(c) ? c.schema : undefined).filter(Boolean)
574
- ));
575
- if (uniqueSchemas.length > 0) {
576
- pgCoreImports.push("pgSchema");
577
- }
578
-
579
- schemaContent += `import { ${pgCoreImports.join(", ")} } from 'drizzle-orm/pg-core';\n`;
580
- schemaContent += "import { relations as drizzleRelations, sql } from 'drizzle-orm';\n\n";
581
-
582
- uniqueSchemas.forEach(schema => {
583
- schemaContent += `export const ${schema}Schema = pgSchema("${schema}");\n`;
584
- });
585
- if (uniqueSchemas.length > 0) {
586
- schemaContent += "\n";
587
- }
588
-
589
- const exportedTableVars: string[] = [];
590
- const exportedEnumVars: string[] = [];
591
- const exportedRelationVars: string[] = [];
592
-
593
- const allTablesToGenerate = new Map<string, {
594
- collection: CollectionConfig,
595
- isJunction?: boolean,
596
- relation?: ResolvedRelation,
597
- sourceCollection?: CollectionConfig
598
- }>();
599
-
600
- // 1. Generate Enums
601
- collections.forEach(collection => {
602
- const collectionPath = getTableName(collection);
603
- Object.entries(collection.properties ?? {}).forEach(([propName, prop]) => {
604
-
605
- if (("enum" in prop) && (prop.type === "string" || prop.type === "number") && prop.enum) {
606
- const enumVarName = getEnumVarName(collectionPath, propName);
607
- const enumDbName = `${collectionPath}_${resolveColumnName(propName, prop)}`;
608
- const values = Array.isArray(prop.enum)
609
- ? (prop.enum as (string | number | { id: string | number })[]).map((v: string | number | { id: string | number }) =>
610
- String(typeof v === "object" && v !== null && "id" in v ? v.id : v)
611
- )
612
- : Object.keys(prop.enum);
613
- if (values.length > 0) {
614
- schemaContent += `export const ${enumVarName} = pgEnum(${quote(enumDbName)}, [${values.map(v => quote(v)).join(", ")}]);\n`;
615
- if (!exportedEnumVars.includes(enumVarName)) exportedEnumVars.push(enumVarName);
616
- }
617
- }
618
- });
619
- });
620
- schemaContent += "\n";
621
-
622
- // Junction policy derivation needs every declaring side of each junction,
623
- // not just the first relation that reached it in the walk below.
624
- const junctionSpecs = resolveJunctionSpecs(collections);
625
-
626
- // 2. Identify all tables (collections and junction tables only)
627
- for (const collection of collections) {
628
- const tableName = getTableName(collection);
629
- if (tableName) {
630
- allTablesToGenerate.set(tableName, { collection });
631
- }
632
-
633
- const resolvedRelations = resolveCollectionRelations(collection);
634
- for (const relation of Object.values(resolvedRelations)) {
635
- if (isManyToMany(relation)) { // Standard M2M junction table
636
- const junctionTableName = relation.through.table;
637
- if (!allTablesToGenerate.has(junctionTableName)) {
638
- allTablesToGenerate.set(junctionTableName, {
639
- collection: {
640
- table: junctionTableName,
641
- properties: {}
642
- } as CollectionConfig,
643
- isJunction: true,
644
- relation: relation,
645
- sourceCollection: collection
646
- });
647
- }
648
- }
649
- // joinPath relations use existing user-controlled tables - no generation needed
650
- }
651
- }
652
-
653
- // 3. Generate pgTable definitions for all unique tables
654
- for (const [tableName, {
655
- collection,
656
- isJunction,
657
- relation,
658
- sourceCollection
659
- }] of allTablesToGenerate.entries()) {
660
- const tableVarName = getTableVarName(tableName);
661
- if (isJunction && relation && sourceCollection && isManyToMany(relation)) {
662
- const targetCollection = relation.target();
663
- // Junctions live in `public`, full stop — `resolveJunctionSpecs`
664
- // hardcodes that, so it is where `planJunctionTables` CREATEs them
665
- // and where the derived RLS policies are applied. Inheriting the
666
- // endpoint's schema here (as this used to) put the junction of any
667
- // m2m onto `users` in `rebase`, while its policies were still
668
- // created against `public.<junction>` — RLS enabled on one table,
669
- // rows written to another. The three generators have to agree, and
670
- // the other two already did.
671
- const tableCreator = "pgTable";
672
- const baseTableName = tableName.includes(".") ? tableName.split(".").pop()! : tableName;
673
- const {
674
- sourceColumn,
675
- targetColumn
676
- } = relation.through;
677
-
678
- const onDelete = relation.onDelete ?? "cascade";
679
- const refOptions = `{ onDelete: \"${onDelete}\" }`;
680
-
681
- // `text`, matching the string default: a junction column must have the
682
- // same type as the primary key it references.
683
- const sourceColType = isNumericId(sourceCollection) ? "integer" : (getPrimaryKeyProp(sourceCollection).isUuid ? "uuid" : "text");
684
- const targetColType = isNumericId(targetCollection) ? "integer" : (getPrimaryKeyProp(targetCollection).isUuid ? "uuid" : "text");
685
- const sourceId = getPrimaryKeyName(sourceCollection);
686
- const targetId = getPrimaryKeyName(targetCollection);
687
-
688
- schemaContent += `export const ${tableVarName} = ${tableCreator}(\"${baseTableName}\", {\n`;
689
- // The junction block was the one place these three helpers were not
690
- // applied, so a junction column containing a space or a hyphen —
691
- // both legal in Postgres — produced a file that does not parse.
692
- schemaContent += ` ${propKey(sourceColumn)}: ${sourceColType}(${quote(sourceColumn)}).notNull().references(() => ${member(getTableVarName(getTableName(sourceCollection)), sourceId)}, ${refOptions}),\n`;
693
- schemaContent += ` ${propKey(targetColumn)}: ${targetColType}(${quote(targetColumn)}).notNull().references(() => ${member(getTableVarName(getTableName(targetCollection)), targetId)}, ${refOptions}),\n`;
694
- schemaContent += "}, (table) => ([\n";
695
- schemaContent += ` primaryKey({ columns: [${member("table", sourceColumn)}, ${member("table", targetColumn)}] }),\n`;
696
-
697
- // Junctions are generated tables like any other: locked by default,
698
- // with derived policies (reads follow the endpoints, writes follow
699
- // the declaring side's update rules). RLS is enabled regardless of
700
- // policy stripping — a bare junction must default-deny, not fail open.
701
- const junctionSpec = junctionSpecs.get(baseTableName);
702
- if (!stripPolicies && junctionSpec) {
703
- const junctionCollection = getJunctionCollectionConfig(junctionSpec);
704
- const resolveCollection: ResolveCollection = (slug) => collections.find(c => c.slug === slug || getTableName(c) === slug);
705
- getJunctionSecurityRules(junctionSpec).forEach((rule: SecurityRule, idx: number) => {
706
- schemaContent += generatePolicyCode(junctionCollection, rule, idx, resolveCollection);
707
- });
708
- }
709
- schemaContent += "])).enableRLS();\n\n";
710
- } else if (!isJunction) {
711
- const schema = isPostgresCollectionConfig(collection) ? collection.schema : undefined;
712
- const tableCreator = schema ? `${schema}Schema.table` : "pgTable";
713
- const baseTableName = tableName.includes(".") ? tableName.split(".").pop()! : tableName;
714
- schemaContent += `export const ${tableVarName} = ${tableCreator}(\"${baseTableName}\", {\n`;
715
- const columns = new Set<string>();
716
- Object.entries(collection.properties ?? {}).forEach(([propName, prop]) => {
717
- const columnString = getDrizzleColumn(propName, prop as Property, collection, collections);
718
- if (columnString) columns.add(columnString);
719
-
720
- });
721
-
722
- // The opt-in search column. Declared with its real generation
723
- // expression rather than as a bare custom type: `schema.generated.ts`
724
- // is what a developer running drizzle-kit themselves diffs against,
725
- // and a column declared without its expression reads to drizzle-kit
726
- // as one it should alter.
727
- const searchSpec = buildSearchColumnSpec(collection);
728
- if (searchSpec) {
729
- columns.add(
730
- ` ${propKey(searchSpec.column)}: customType({ dataType() { return 'tsvector'; } })(${quote(searchSpec.column)})` +
731
- `.generatedAlwaysAs(sql\`${searchSpec.expression}\`)`
732
- );
733
- if (searchSpec.fuzzy) {
734
- columns.add(
735
- ` ${propKey(searchSpec.fuzzy.column)}: text(${quote(searchSpec.fuzzy.column)})` +
736
- `.generatedAlwaysAs(sql\`${searchSpec.fuzzy.expression}\`)`
737
- );
738
- }
739
- }
740
-
741
- // A collection that declares no primary key gets an implicit one:
742
- // `id TEXT PRIMARY KEY`. The DDL generator emits the same column,
743
- // and `derivePrimaryKeys` reads it back.
744
- const hasIdColumn = Array.from(columns).some(col => col.includes(".primaryKey()"));
745
- if (!hasIdColumn) {
746
- columns.add(" id: text(\"id\").primaryKey()");
747
- }
748
-
749
- schemaContent += `${Array.from(columns).join(",\n")}`;
750
-
751
- const securityRules = getEffectiveSecurityRules(collection);
752
- if (!stripPolicies && securityRules.length > 0) {
753
- schemaContent += "\n}, (table) => ([\n";
754
- const resolveCollection: ResolveCollection = (slug) => collections.find(c => c.slug === slug || getTableName(c) === slug);
755
- securityRules.forEach((rule: SecurityRule, idx: number) => {
756
- schemaContent += generatePolicyCode(collection, rule, idx, resolveCollection);
757
- });
758
- schemaContent += "])).enableRLS();\n\n";
759
- } else {
760
- // No explicit policies — RLS enabled with deny-all default (Postgres denies
761
- // everything when RLS is on and no permissive policies exist).
762
- schemaContent += "\n}).enableRLS();\n\n";
763
- }
764
- }
765
- if (!exportedTableVars.includes(tableVarName)) exportedTableVars.push(tableVarName);
766
- }
767
-
768
- // 4. Generate Drizzle Relations
769
- for (const [tableName, {
770
- collection,
771
- isJunction
772
- }] of allTablesToGenerate.entries()) {
773
- const tableVarName = getTableVarName(tableName);
774
- const tableRelations: string[] = [];
775
-
776
- if (isJunction) {
777
- const relationInfo = Array.from(allTablesToGenerate.values()).find(v => v.isJunction && getTableName(v.collection) === tableName);
778
- if (relationInfo && relationInfo.relation && relationInfo.sourceCollection && isManyToMany(relationInfo.relation)) {
779
- const {
780
- relation,
781
- sourceCollection
782
- } = relationInfo;
783
- const targetCollection = relation.target();
784
- const sourceTableVar = getTableVarName(getTableName(sourceCollection));
785
- const targetTableVar = getTableVarName(getTableName(targetCollection));
786
- const sourceId = getPrimaryKeyName(sourceCollection);
787
- const targetId = getPrimaryKeyName(targetCollection);
788
-
789
- if (!relation?.through)
790
- throw new Error("Internal, the relation should have a through property. Relations passed to this script should sanitized first with sanitizeRelation().");
791
-
792
- // The owning relation's name — used on the source side of the junction
793
- const owningRelationName = relation.relationName ?? toSnakeCase(getTableName(targetCollection));
794
-
795
- // Find the inverse relation name on the target collection (if any)
796
- // This is needed so the junction's target-side one() can pair with the
797
- // inverse many() on the target table.
798
- let inverseRelationName: string | null = null;
799
- try {
800
- const targetRelations = resolveCollectionRelations(targetCollection);
801
- for (const [, targetRel] of Object.entries(targetRelations)) {
802
- if (targetRel.kind !== "belongsTo" &&
803
- targetRel.cardinality === "many" &&
804
- targetRel.relationName === owningRelationName) {
805
- inverseRelationName = targetRel.relationName ?? null;
806
- break;
807
- }
808
- }
809
- } catch {
810
- // ignore — inverse side may not exist
811
- }
812
-
813
- // Source side one(): pairs with owning table's many(junctionTable, { relationName })
814
- tableRelations.push(` ${quote(relation.through.sourceColumn)}: one(${sourceTableVar}, {\n fields: [${member(tableVarName, relation.through.sourceColumn)}],\n references: [${member(sourceTableVar, sourceId)}],\n relationName: ${quote(owningRelationName)}\n })`);
815
-
816
- // Target side one(): pairs with inverse table's many(junctionTable, { relationName })
817
- // Always emit a relationName to avoid collisions with the source-side's owningRelationName.
818
- // When no inverse relation exists on the target collection, synthesize a unique name.
819
- const targetRelationName = inverseRelationName
820
- ? inverseRelationName
821
- : `${tableName}_${relation.through.targetColumn}`;
822
- tableRelations.push(` ${quote(relation.through.targetColumn)}: one(${targetTableVar}, {\n fields: [${member(tableVarName, relation.through.targetColumn)}],\n references: [${member(targetTableVar, targetId)}],\n relationName: ${quote(targetRelationName)}\n })`);
823
- }
824
- } else {
825
- const resolvedRelations = resolveCollectionRelations(collection);
826
- // Defensive safety net: track emitted `drizzleRelationName` values
827
- // to prevent duplicate one()/many() entries in the generated schema.
828
- // The root deduplication happens inside resolveCollectionRelations,
829
- // but this guards against any future regressions in that utility.
830
- const emittedRelationNames = new Set<string>();
831
- for (const [relationKey, rel] of Object.entries(resolvedRelations)) {
832
- try {
833
- const target = rel.target();
834
- const targetTableVar = getTableVarName(getTableName(target));
835
-
836
- // Compute a deterministic shared relationName for Drizzle.
837
- // Both sides of an owning/inverse pair MUST share the same
838
- // relationName, otherwise Drizzle cannot pair them.
839
- //
840
- // Strategy: use "{ownerTable}_{foreignKey}" which is
841
- // computable from either side:
842
- // - owning side: {thisTable}_{localKey}
843
- // - inverse side: {targetTable}_{foreignKeyOnTarget}
844
- const drizzleRelationName = computeSharedRelationName(rel, collection, collections);
845
-
846
- // Skip if we've already emitted a relation with this drizzleRelationName
847
- // for this table — prevents duplicate definitions when
848
- // resolveCollectionRelations returns alias entries for the same FK.
849
- const deduplicationKey = `${drizzleRelationName}::${rel.kind}`;
850
- if (emittedRelationNames.has(deduplicationKey)) continue;
851
- emittedRelationNames.add(deduplicationKey);
852
-
853
- switch (rel.kind) {
854
- case "belongsTo": {
855
- // `localKey` is a COLUMN name; the generated Drizzle
856
- // object is keyed by PROPERTY. They differ whenever
857
- // the property is camelCase — `user_id` is exposed
858
- // as `userId` — and emitting the column produces a
859
- // schema that does not compile. The three other
860
- // emission sites normalise; this one did not.
861
- const localFieldKey = resolvePropertyKeyForColumn(collection, rel.localKey);
862
- tableRelations.push(` ${quote(relationKey)}: one(${targetTableVar}, {\n fields: [${member(tableVarName, localFieldKey)}],\n references: [${member(targetTableVar, getPrimaryKeyName(target))}],\n relationName: ${quote(drizzleRelationName)}\n })`);
863
- break;
864
- }
865
-
866
- case "hasOne":
867
- // The foreign key lives on the TARGET table. Drizzle pairs
868
- // the two sides by `relationName` alone — giving
869
- // `fields`/`references` here is invalid and crashes
870
- // `normalizeRelation` with "Cannot read properties of
871
- // undefined (reading 'referencedTable')".
872
- // A `relationName` is authored, and a `"` in one
873
- // closed this string literal early — in a file that
874
- // is compiled and imported by the server.
875
- tableRelations.push(` ${quote(relationKey)}: one(${targetTableVar}, {\n relationName: ${quote(drizzleRelationName)}\n })`);
876
- break;
877
-
878
- case "hasMany":
879
- tableRelations.push(` ${quote(relationKey)}: many(${targetTableVar}, { relationName: ${quote(drizzleRelationName)} })`);
880
- break;
881
-
882
- case "manyToMany": {
883
- // Both sides point at the junction. This used to have a
884
- // second arm that searched the *target's* relations for an
885
- // owning many-to-many whose name matched, to borrow its
886
- // junction table — unnecessary now that each side names
887
- // its own.
888
- const junctionTableVar = getTableVarName(rel.through.table);
889
- tableRelations.push(` ${quote(relationKey)}: many(${junctionTableVar}, { relationName: ${quote(drizzleRelationName)} })`);
890
- break;
891
- }
892
-
893
- case "via":
894
- // A join chain is resolved at query time, not modelled
895
- // as a Drizzle relation.
896
- break;
897
- }
898
- } catch (e) {
899
- logger.warn(`Could not generate relation ${relationKey} for ${collection.name}`, { error: e });
900
- }
901
- }
902
-
903
- // Synthesize missing reciprocal relations
904
- for (const otherCollection of collections) {
905
- if (otherCollection.slug === collection.slug) continue;
906
-
907
- const otherRelations = resolveCollectionRelations(otherCollection);
908
- for (const [otherKey, otherRel] of Object.entries(otherRelations)) {
909
- if (hasForeignKeyOnTarget(otherRel)) {
910
- try {
911
- const otherTarget = otherRel.target();
912
- if (otherTarget.slug === collection.slug) {
913
- const drizzleRelationName = computeSharedRelationName(otherRel, otherCollection, collections);
914
- const deduplicationKey = `${drizzleRelationName}::belongsTo`;
915
-
916
- if (!emittedRelationNames.has(deduplicationKey)) {
917
- const otherTableVar = getTableVarName(getTableName(otherCollection));
918
- // Resolve foreignKeyOnTarget to the Drizzle property key
919
- // on THIS collection (the owning table). The raw FK column
920
- // name (e.g. "client_id") may differ from the property key
921
- // (e.g. "clientId") when `columnName` is set.
922
- const drizzleFieldKey = resolvePropertyKeyForColumn(collection, otherRel.foreignKeyOnTarget);
923
- // The column the far side points at: its
924
- // primary key, unless the link names another
925
- // one with `sourceKey`.
926
- const referencedKey = otherRel.sourceKey
927
- ? resolvePropertyKeyForColumn(otherCollection, otherRel.sourceKey)
928
- : getPrimaryKeyName(otherCollection);
929
- const synthKey = `_synth_${otherTableVar}_${drizzleFieldKey}`;
930
- tableRelations.push(` ${quote(synthKey)}: one(${otherTableVar}, {\n fields: [${member(tableVarName, drizzleFieldKey)}],\n references: [${member(otherTableVar, referencedKey)}],\n relationName: ${quote(drizzleRelationName)}\n })`);
931
- emittedRelationNames.add(deduplicationKey);
932
- }
933
- }
934
- } catch (e) {
935
- // ignore
936
- }
937
- }
938
- }
939
- }
940
- }
941
-
942
- if (tableRelations.length > 0) {
943
- const relVarName = `${tableVarName}Relations`;
944
- schemaContent += `export const ${relVarName} = drizzleRelations(${tableVarName}, ({ one, many }) => ({\n${tableRelations.join(",\n")}\n}));\n\n`;
945
- if (!exportedRelationVars.includes(relVarName)) exportedRelationVars.push(relVarName);
946
- }
947
- }
948
-
949
- // <<< ADDED: Final aggregated exports block
950
- const tablesExport = `export const tables = { ${exportedTableVars.join(", ")} };\n`;
951
- const enumsExport = `export const enums = { ${exportedEnumVars.join(", ")} };\n`;
952
- const relationsExport = `export const relations = { ${exportedRelationVars.join(", ")} };\n\n`;
953
- schemaContent += tablesExport + enumsExport + relationsExport;
954
-
955
- return schemaContent;
956
- };
957
-