@rebasepro/server-postgres 0.17.3 → 0.18.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (149) hide show
  1. package/LICENSE +0 -1
  2. package/README.md +31 -0
  3. package/dist/backup-argv.d.ts +19 -0
  4. package/dist/branch-argv.d.ts +28 -0
  5. package/dist/branch-prune.d.ts +77 -0
  6. package/dist/cli-collections-path.d.ts +31 -0
  7. package/dist/cli-errors.d.ts +123 -0
  8. package/dist/cli-flags.d.ts +101 -0
  9. package/dist/cli-helpers.d.ts +94 -10
  10. package/dist/collection-index-C7LopMyX.js +247 -0
  11. package/dist/collection-index-C7LopMyX.js.map +1 -0
  12. package/dist/databasePoolManager.d.ts +27 -0
  13. package/dist/{ensure-collection-policies-UCqgv_8c.js → ensure-collection-policies-CagOzEPS.js} +6 -6
  14. package/dist/{ensure-collection-policies-UCqgv_8c.js.map → ensure-collection-policies-CagOzEPS.js.map} +1 -1
  15. package/dist/{ensure-collection-tables-DgVixhX3.js → ensure-collection-tables-DbAhrjnG.js} +614 -52
  16. package/dist/ensure-collection-tables-DbAhrjnG.js.map +1 -0
  17. package/dist/index.d.ts +12 -0
  18. package/dist/index.es.js +19307 -3059
  19. package/dist/index.es.js.map +1 -1
  20. package/dist/{rls-enforcement-DvAbL9YJ.js → rls-enforcement-ClJoTboT.js} +53 -189
  21. package/dist/rls-enforcement-ClJoTboT.js.map +1 -0
  22. package/dist/schema/column-type-drift.d.ts +79 -0
  23. package/dist/schema/doctor.d.ts +42 -0
  24. package/dist/schema/ensure-collection-tables.d.ts +90 -0
  25. package/dist/schema/generate-next-step.d.ts +30 -0
  26. package/dist/schema/generate-postgres-ddl-logic.d.ts +28 -0
  27. package/dist/schema/generated-schema-staleness.d.ts +37 -0
  28. package/dist/schema/introspect-db-queries.d.ts +34 -0
  29. package/dist/security/rls-enforcement.d.ts +63 -2
  30. package/dist/services/BranchService.d.ts +86 -1
  31. package/dist/services/collection-helpers.d.ts +1 -1
  32. package/dist/services/realtimeService.d.ts +30 -2
  33. package/dist/services/row-pipeline.d.ts +12 -1
  34. package/dist/utils/pg-error-utils.d.ts +3 -1
  35. package/dist/utils/pg-numeric-number-patch.d.ts +31 -0
  36. package/package.json +32 -27
  37. package/dist/backup-service-DCk7KhhL.js +0 -9044
  38. package/dist/backup-service-DCk7KhhL.js.map +0 -1
  39. package/dist/collection-index-BRUg10H5.js +0 -5690
  40. package/dist/collection-index-BRUg10H5.js.map +0 -1
  41. package/dist/connection-GOKU3Hu5.js +0 -277
  42. package/dist/connection-GOKU3Hu5.js.map +0 -1
  43. package/dist/ensure-collection-tables-DgVixhX3.js.map +0 -1
  44. package/dist/rls-bootstrap-sql-B5C9LoJ6.js +0 -244
  45. package/dist/rls-bootstrap-sql-B5C9LoJ6.js.map +0 -1
  46. package/dist/rls-enforcement-DvAbL9YJ.js.map +0 -1
  47. package/dist/src-DiB5RP2Z.js +0 -541
  48. package/dist/src-DiB5RP2Z.js.map +0 -1
  49. package/dist/websocket-BZ4H5wUz.js +0 -8198
  50. package/dist/websocket-BZ4H5wUz.js.map +0 -1
  51. package/src/PostgresAdapter.ts +0 -91
  52. package/src/PostgresBackendDriver.ts +0 -1857
  53. package/src/PostgresBootstrapper.ts +0 -1157
  54. package/src/auth/ensure-tables.ts +0 -896
  55. package/src/auth/schema-version.ts +0 -260
  56. package/src/auth/services.ts +0 -1610
  57. package/src/backup/backup-cli.ts +0 -520
  58. package/src/backup/backup-cron.ts +0 -212
  59. package/src/backup/backup-logic.ts +0 -93
  60. package/src/backup/backup-service.ts +0 -485
  61. package/src/backup/index.ts +0 -12
  62. package/src/backup/pg-tools.ts +0 -403
  63. package/src/backup/retention.ts +0 -86
  64. package/src/cli-errors.ts +0 -265
  65. package/src/cli-helpers.ts +0 -569
  66. package/src/cli-output.ts +0 -43
  67. package/src/cli.ts +0 -1339
  68. package/src/collections/PostgresCollectionRegistry.ts +0 -108
  69. package/src/collections/buildRegistry.ts +0 -68
  70. package/src/collections/validate-relations.ts +0 -402
  71. package/src/connection.ts +0 -333
  72. package/src/data-transformer.ts +0 -845
  73. package/src/databasePoolManager.ts +0 -95
  74. package/src/history/HistoryService.ts +0 -254
  75. package/src/history/ensure-history-table.ts +0 -53
  76. package/src/index.ts +0 -19
  77. package/src/interfaces.ts +0 -60
  78. package/src/module-dir.ts +0 -7
  79. package/src/schema/atlas-argv.ts +0 -94
  80. package/src/schema/auth-schema.ts +0 -298
  81. package/src/schema/auth-users-columns.ts +0 -131
  82. package/src/schema/carved-out-migration.ts +0 -404
  83. package/src/schema/classify-change.ts +0 -436
  84. package/src/schema/collection-index.ts +0 -427
  85. package/src/schema/collections-schema-version.ts +0 -103
  86. package/src/schema/destructive-sql.ts +0 -94
  87. package/src/schema/doctor-cli.ts +0 -66
  88. package/src/schema/doctor-policy-checks.ts +0 -105
  89. package/src/schema/doctor.ts +0 -837
  90. package/src/schema/drizzle-ddl.ts +0 -33
  91. package/src/schema/dynamic-tables.test.ts +0 -302
  92. package/src/schema/dynamic-tables.ts +0 -293
  93. package/src/schema/ensure-collection-policies.ts +0 -198
  94. package/src/schema/ensure-collection-tables.test.ts +0 -518
  95. package/src/schema/ensure-collection-tables.ts +0 -1267
  96. package/src/schema/generate-drizzle-schema-logic.ts +0 -957
  97. package/src/schema/generate-drizzle-schema.ts +0 -143
  98. package/src/schema/generate-postgres-ddl-logic.ts +0 -1339
  99. package/src/schema/generate-postgres-ddl.ts +0 -180
  100. package/src/schema/generate-schema-commit.ts +0 -267
  101. package/src/schema/generated-schema-staleness.ts +0 -284
  102. package/src/schema/introspect-db-constraints.ts +0 -385
  103. package/src/schema/introspect-db-inference.ts +0 -255
  104. package/src/schema/introspect-db-logic.ts +0 -1387
  105. package/src/schema/introspect-db-naming.ts +0 -15
  106. package/src/schema/introspect-db-project.ts +0 -78
  107. package/src/schema/introspect-db-queries.ts +0 -326
  108. package/src/schema/introspect-db-structure.ts +0 -670
  109. package/src/schema/introspect-db-types.ts +0 -56
  110. package/src/schema/introspect-db.ts +0 -257
  111. package/src/schema/introspect-runtime.test.ts +0 -260
  112. package/src/schema/introspect-runtime.ts +0 -327
  113. package/src/schema/non-sql-collections.test.ts +0 -131
  114. package/src/schema/rls-bootstrap-sql.ts +0 -288
  115. package/src/schema/search-column.ts +0 -643
  116. package/src/schema/test-schema.ts +0 -11
  117. package/src/schema/vector-index.ts +0 -491
  118. package/src/security/anonymous-grants.test.ts +0 -73
  119. package/src/security/policy-drift.test.ts +0 -435
  120. package/src/security/policy-drift.ts +0 -473
  121. package/src/security/rls-enforcement.ts +0 -505
  122. package/src/services/BranchService.ts +0 -326
  123. package/src/services/FetchService.ts +0 -2115
  124. package/src/services/PersistService.ts +0 -557
  125. package/src/services/RelationService.ts +0 -978
  126. package/src/services/RelationWriteService.ts +0 -653
  127. package/src/services/cdc/CdcListener.ts +0 -103
  128. package/src/services/cdc/junction-tables.ts +0 -91
  129. package/src/services/cdc/trigger-cdc.ts +0 -173
  130. package/src/services/channel-bus/ChannelBus.ts +0 -44
  131. package/src/services/channel-bus/PostgresChannelBus.ts +0 -299
  132. package/src/services/channel-bus/index.ts +0 -114
  133. package/src/services/channel-history.ts +0 -424
  134. package/src/services/channel-presence.ts +0 -185
  135. package/src/services/collection-helpers.ts +0 -420
  136. package/src/services/dataService.ts +0 -256
  137. package/src/services/index.ts +0 -14
  138. package/src/services/junction-writes.ts +0 -295
  139. package/src/services/nested-path.ts +0 -145
  140. package/src/services/pg-notify-listener.ts +0 -151
  141. package/src/services/realtimeService.ts +0 -2545
  142. package/src/services/row-pipeline.ts +0 -240
  143. package/src/services/write-denial.ts +0 -55
  144. package/src/types.ts +0 -4
  145. package/src/utils/connection-string.ts +0 -58
  146. package/src/utils/drizzle-conditions.ts +0 -2743
  147. package/src/utils/pg-array-null-patch.ts +0 -42
  148. package/src/utils/pg-error-utils.ts +0 -391
  149. package/src/websocket.ts +0 -855
@@ -1,33 +0,0 @@
1
- /**
2
- * The server's DDL bootstrapper, over a Drizzle handle.
3
- *
4
- * `createDdlBootstrapper` in `@rebasepro/server` wants a plain
5
- * `(sql: string) => Promise<rows>`; the driver's internal stores hold a Drizzle
6
- * database. This is the adapter between them, and it exists so the retry policy
7
- * has exactly one definition. A second copy of the SQLSTATE list living in the
8
- * driver is how the two drift apart, and the drift is invisible: both versions
9
- * work perfectly on every single-instance deployment.
10
- */
11
- import { sql } from "drizzle-orm";
12
- import type { NodePgDatabase } from "drizzle-orm/node-postgres";
13
- import { createDdlBootstrapper, type DdlBootstrapper } from "@rebasepro/server";
14
-
15
- /**
16
- * A {@link DdlBootstrapper} that runs its statements through `db.execute`.
17
- *
18
- * @param db the Drizzle handle the calling store already holds
19
- * @param scope log prefix identifying the caller, e.g. `"channel-presence"`
20
- */
21
- export function drizzleDdlBootstrapper(
22
- db: NodePgDatabase<Record<string, unknown>>,
23
- scope: string
24
- ): DdlBootstrapper {
25
- return createDdlBootstrapper(async (statement: string) => {
26
- // `sql.raw`, because everything reaching this path is DDL assembled from
27
- // identifiers that were validated before they got here — there is no
28
- // parameter to bind, and Drizzle's tagged template would treat the whole
29
- // statement as one.
30
- const result = await db.execute(sql.raw(statement));
31
- return (result as unknown as { rows?: Record<string, unknown>[] }).rows ?? [];
32
- }, scope);
33
- }
@@ -1,302 +0,0 @@
1
- import { describe, expect, it } from "@jest/globals";
2
- import { getTableName, sql } from "drizzle-orm";
3
- import { getTableConfig } from "drizzle-orm/pg-core";
4
- import { drizzle } from "drizzle-orm/node-postgres";
5
-
6
- import { buildDrizzleTablesFromSchema, buildDrizzleRelationsFromSchema } from "./dynamic-tables";
7
- import { buildTablesMap, TableColumn, PrimaryKeyRow, ForeignKeyRow } from "./introspect-db-logic";
8
-
9
- function column(overrides: Partial<TableColumn> & { table_name: string; column_name: string }): TableColumn {
10
- return {
11
- data_type: "text",
12
- udt_name: "text",
13
- is_nullable: "YES",
14
- column_default: null,
15
- atttypmod: null,
16
- ...overrides
17
- };
18
- }
19
-
20
- function tablesMapOf(columns: TableColumn[], pks: PrimaryKeyRow[], fks: ForeignKeyRow[] = []) {
21
- const tableNames = [...new Set(columns.map((c) => c.table_name))].map((table_name) => ({ table_name }));
22
- return buildTablesMap(tableNames, columns, pks, fks);
23
- }
24
-
25
- describe("buildDrizzleTablesFromSchema", () => {
26
- it("builds a table per introspected table, named after the pg table", () => {
27
- const tables = buildDrizzleTablesFromSchema(
28
- tablesMapOf(
29
- [column({ table_name: "posts", column_name: "id", data_type: "uuid", udt_name: "uuid", is_nullable: "NO" })],
30
- [{ table_name: "posts", column_name: "id" }]
31
- )
32
- );
33
-
34
- expect(Object.keys(tables)).toEqual(["posts"]);
35
- expect(getTableName(tables.posts)).toBe("posts");
36
- });
37
-
38
- it("maps pg types onto the matching drizzle column types", () => {
39
- const tables = buildDrizzleTablesFromSchema(
40
- tablesMapOf(
41
- [
42
- column({ table_name: "t", column_name: "id", data_type: "uuid", udt_name: "uuid", is_nullable: "NO" }),
43
- column({ table_name: "t", column_name: "count", data_type: "integer", udt_name: "int4" }),
44
- column({ table_name: "t", column_name: "big", data_type: "bigint", udt_name: "int8" }),
45
- column({ table_name: "t", column_name: "price", data_type: "numeric", udt_name: "numeric" }),
46
- column({ table_name: "t", column_name: "active", data_type: "boolean", udt_name: "bool" }),
47
- column({ table_name: "t", column_name: "meta", data_type: "jsonb", udt_name: "jsonb" }),
48
- column({ table_name: "t", column_name: "created_at", data_type: "timestamp with time zone", udt_name: "timestamptz" }),
49
- column({ table_name: "t", column_name: "born_on", data_type: "date", udt_name: "date" })
50
- ],
51
- [{ table_name: "t", column_name: "id" }]
52
- )
53
- );
54
-
55
- const byName = Object.fromEntries(
56
- getTableConfig(tables.t).columns.map((c) => [c.name, c.getSQLType()])
57
- );
58
-
59
- expect(byName.id).toBe("uuid");
60
- expect(byName.count).toBe("integer");
61
- expect(byName.big).toBe("bigint");
62
- expect(byName.price).toBe("numeric");
63
- expect(byName.active).toBe("boolean");
64
- expect(byName.meta).toBe("jsonb");
65
- expect(byName.created_at).toBe("timestamp with time zone");
66
- expect(byName.born_on).toBe("date");
67
- });
68
-
69
- it("carries NOT NULL and single-column primary keys onto the columns", () => {
70
- const tables = buildDrizzleTablesFromSchema(
71
- tablesMapOf(
72
- [
73
- column({ table_name: "t", column_name: "id", data_type: "uuid", udt_name: "uuid", is_nullable: "NO" }),
74
- column({ table_name: "t", column_name: "title", is_nullable: "NO" }),
75
- column({ table_name: "t", column_name: "subtitle", is_nullable: "YES" })
76
- ],
77
- [{ table_name: "t", column_name: "id" }]
78
- )
79
- );
80
-
81
- const columns = getTableConfig(tables.t).columns;
82
- const id = columns.find((c) => c.name === "id")!;
83
- const title = columns.find((c) => c.name === "title")!;
84
- const subtitle = columns.find((c) => c.name === "subtitle")!;
85
-
86
- expect(id.primary).toBe(true);
87
- expect(title.notNull).toBe(true);
88
- expect(title.primary).toBe(false);
89
- expect(subtitle.notNull).toBe(false);
90
- });
91
-
92
- it("declares composite primary keys as a table constraint", () => {
93
- const tables = buildDrizzleTablesFromSchema(
94
- tablesMapOf(
95
- [
96
- column({ table_name: "posts_tags", column_name: "post_id", data_type: "uuid", udt_name: "uuid", is_nullable: "NO" }),
97
- column({ table_name: "posts_tags", column_name: "tag_id", data_type: "uuid", udt_name: "uuid", is_nullable: "NO" })
98
- ],
99
- [
100
- { table_name: "posts_tags", column_name: "post_id" },
101
- { table_name: "posts_tags", column_name: "tag_id" }
102
- ]
103
- )
104
- );
105
-
106
- const config = getTableConfig(tables.posts_tags);
107
- expect(config.primaryKeys).toHaveLength(1);
108
- expect(config.primaryKeys[0].columns.map((c) => c.name).sort()).toEqual(["post_id", "tag_id"]);
109
- // No column should claim the primary key on its own.
110
- expect(config.columns.every((c) => !c.primary)).toBe(true);
111
- });
112
-
113
- it("binds tables to a non-public schema", () => {
114
- const tables = buildDrizzleTablesFromSchema(
115
- tablesMapOf(
116
- [column({ table_name: "users", column_name: "id", data_type: "uuid", udt_name: "uuid", is_nullable: "NO" })],
117
- [{ table_name: "users", column_name: "id" }]
118
- ),
119
- "rebase"
120
- );
121
-
122
- expect(getTableConfig(tables.users).schema).toBe("rebase");
123
- });
124
-
125
- it("falls back to text for unknown types rather than dropping the column", () => {
126
- const tables = buildDrizzleTablesFromSchema(
127
- tablesMapOf(
128
- [
129
- column({ table_name: "t", column_name: "id", data_type: "uuid", udt_name: "uuid", is_nullable: "NO" }),
130
- column({ table_name: "t", column_name: "location", data_type: "USER-DEFINED", udt_name: "geography" }),
131
- column({ table_name: "t", column_name: "weird", data_type: "USER-DEFINED", udt_name: "some_future_type" })
132
- ],
133
- [{ table_name: "t", column_name: "id" }]
134
- )
135
- );
136
-
137
- const byName = Object.fromEntries(getTableConfig(tables.t).columns.map((c) => [c.name, c.getSQLType()]));
138
- expect(byName.location).toBe("text");
139
- expect(byName.weird).toBe("text");
140
- });
141
-
142
- it("maps the types a plain type switch tends to miss", () => {
143
- const tables = buildDrizzleTablesFromSchema(
144
- tablesMapOf(
145
- [
146
- column({ table_name: "t", column_name: "id", data_type: "uuid", udt_name: "uuid", is_nullable: "NO" }),
147
- column({ table_name: "t", column_name: "blob", data_type: "bytea", udt_name: "bytea" }),
148
- column({ table_name: "t", column_name: "span", data_type: "interval", udt_name: "interval" }),
149
- column({ table_name: "t", column_name: "ip", data_type: "inet", udt_name: "inet" }),
150
- column({ table_name: "t", column_name: "mac", data_type: "macaddr", udt_name: "macaddr" }),
151
- column({ table_name: "t", column_name: "code", data_type: "character", udt_name: "bpchar", atttypmod: 14 }),
152
- column({ table_name: "t", column_name: "name", data_type: "character varying", udt_name: "varchar", atttypmod: 104 }),
153
- column({ table_name: "t", column_name: "clock", data_type: "time with time zone", udt_name: "timetz" }),
154
- column({ table_name: "t", column_name: "embedding", data_type: "USER-DEFINED", udt_name: "vector", atttypmod: 3 })
155
- ],
156
- [{ table_name: "t", column_name: "id" }]
157
- )
158
- );
159
-
160
- const byName = Object.fromEntries(getTableConfig(tables.t).columns.map((c) => [c.name, c.getSQLType()]));
161
-
162
- // bytea has no drizzle builder — a text fallback would corrupt binary.
163
- expect(byName.blob).toBe("bytea");
164
- expect(byName.span).toBe("interval");
165
- expect(byName.ip).toBe("inet");
166
- expect(byName.mac).toBe("macaddr");
167
- expect(byName.code).toBe("char(10)");
168
- expect(byName.name).toBe("varchar(100)");
169
- expect(byName.clock).toBe("time with time zone");
170
- expect(byName.embedding).toBe("vector(3)");
171
- });
172
-
173
- it("types arrays by their element rather than flattening them to text[]", () => {
174
- const tables = buildDrizzleTablesFromSchema(
175
- tablesMapOf(
176
- [
177
- column({ table_name: "t", column_name: "id", data_type: "uuid", udt_name: "uuid", is_nullable: "NO" }),
178
- column({ table_name: "t", column_name: "tags", data_type: "ARRAY", udt_name: "_text" }),
179
- column({ table_name: "t", column_name: "scores", data_type: "ARRAY", udt_name: "_int4" }),
180
- column({ table_name: "t", column_name: "ids", data_type: "ARRAY", udt_name: "_uuid" })
181
- ],
182
- [{ table_name: "t", column_name: "id" }]
183
- )
184
- );
185
-
186
- const byName = Object.fromEntries(getTableConfig(tables.t).columns.map((c) => [c.name, c.getSQLType()]));
187
-
188
- expect(byName.tags).toBe("text[]");
189
- expect(byName.scores).toBe("integer[]");
190
- expect(byName.ids).toBe("uuid[]");
191
- });
192
-
193
- it("falls back to text for a vector of undeclared width", () => {
194
- const tables = buildDrizzleTablesFromSchema(
195
- tablesMapOf(
196
- [
197
- column({ table_name: "t", column_name: "id", data_type: "uuid", udt_name: "uuid", is_nullable: "NO" }),
198
- column({ table_name: "t", column_name: "embedding", data_type: "USER-DEFINED", udt_name: "vector", atttypmod: -1 })
199
- ],
200
- [{ table_name: "t", column_name: "id" }]
201
- )
202
- );
203
-
204
- const byName = Object.fromEntries(getTableConfig(tables.t).columns.map((c) => [c.name, c.getSQLType()]));
205
- expect(byName.embedding).toBe("text");
206
- });
207
-
208
- it("produces tables drizzle can compile into SQL", () => {
209
- // The real proof: a table object is only useful if the query builder
210
- // accepts it. Compiling a select exercises the whole construction.
211
- const tables = buildDrizzleTablesFromSchema(
212
- tablesMapOf(
213
- [
214
- column({ table_name: "posts", column_name: "id", data_type: "uuid", udt_name: "uuid", is_nullable: "NO" }),
215
- column({ table_name: "posts", column_name: "title", is_nullable: "NO" })
216
- ],
217
- [{ table_name: "posts", column_name: "id" }]
218
- )
219
- );
220
-
221
- const db = drizzle({ schema: tables, client: {} as never });
222
- const query = db.select().from(tables.posts as never).toSQL();
223
-
224
- expect(query.sql).toContain('from "posts"');
225
- expect(query.sql).toContain('"id"');
226
- expect(query.sql).toContain('"title"');
227
- });
228
- });
229
-
230
- describe("buildDrizzleRelationsFromSchema", () => {
231
- const schema = () =>
232
- tablesMapOf(
233
- [
234
- column({ table_name: "authors", column_name: "id", data_type: "uuid", udt_name: "uuid", is_nullable: "NO" }),
235
- column({ table_name: "posts", column_name: "id", data_type: "uuid", udt_name: "uuid", is_nullable: "NO" }),
236
- column({ table_name: "posts", column_name: "author_id", data_type: "uuid", udt_name: "uuid" })
237
- ],
238
- [
239
- { table_name: "authors", column_name: "id" },
240
- { table_name: "posts", column_name: "id" }
241
- ],
242
- [{ table_name: "posts", column_name: "author_id", foreign_table_name: "authors", foreign_column_name: "id" }]
243
- );
244
-
245
- it("pairs each owning one() with the inverse many() drizzle requires", () => {
246
- // Drizzle's normalizeRelation throws on a named one() with no matching
247
- // many(), which silently drops the query to the N+1 fallback path.
248
- const map = schema();
249
- const tables = buildDrizzleTablesFromSchema(map);
250
- const rels = buildDrizzleRelationsFromSchema(map, tables);
251
-
252
- expect(Object.keys(rels).sort()).toEqual(["authorsRelations", "postsRelations"]);
253
-
254
- const db = drizzle({ schema: { ...tables, ...rels }, client: {} as never });
255
- const config = (db as unknown as { _: { schema: Record<string, { relations: Record<string, unknown> }> } })._.schema;
256
-
257
- expect(Object.keys(config.posts.relations)).toEqual(["author"]);
258
- expect(Object.keys(config.authors.relations)).toEqual(["posts"]);
259
- });
260
-
261
- it("compiles a relational query that joins the related table", () => {
262
- const map = schema();
263
- const tables = buildDrizzleTablesFromSchema(map);
264
- const rels = buildDrizzleRelationsFromSchema(map, tables);
265
- const db = drizzle({ schema: { ...tables, ...rels }, client: {} as never });
266
-
267
- const query = (db as never as { query: Record<string, { findMany(a: unknown): { toSQL(): { sql: string } } }> })
268
- .query.posts.findMany({ with: { author: true } }).toSQL();
269
-
270
- expect(query.sql).toContain('"authors"');
271
- expect(query.sql).toContain("author_id");
272
- });
273
-
274
- it("skips a relation whose key would shadow a real column", () => {
275
- const map = tablesMapOf(
276
- [
277
- column({ table_name: "authors", column_name: "id", data_type: "uuid", udt_name: "uuid", is_nullable: "NO" }),
278
- column({ table_name: "posts", column_name: "id", data_type: "uuid", udt_name: "uuid", is_nullable: "NO" }),
279
- // A literal "author" column alongside the author_id foreign key.
280
- column({ table_name: "posts", column_name: "author", data_type: "text", udt_name: "text" }),
281
- column({ table_name: "posts", column_name: "author_id", data_type: "uuid", udt_name: "uuid" })
282
- ],
283
- [
284
- { table_name: "authors", column_name: "id" },
285
- { table_name: "posts", column_name: "id" }
286
- ],
287
- [{ table_name: "posts", column_name: "author_id", foreign_table_name: "authors", foreign_column_name: "id" }]
288
- );
289
- const tables = buildDrizzleTablesFromSchema(map);
290
- const rels = buildDrizzleRelationsFromSchema(map, tables);
291
-
292
- expect(rels.postsRelations).toBeUndefined();
293
- });
294
-
295
- it("returns nothing for a schema with no foreign keys", () => {
296
- const map = tablesMapOf(
297
- [column({ table_name: "posts", column_name: "id", data_type: "uuid", udt_name: "uuid", is_nullable: "NO" })],
298
- [{ table_name: "posts", column_name: "id" }]
299
- );
300
- expect(buildDrizzleRelationsFromSchema(map, buildDrizzleTablesFromSchema(map))).toEqual({});
301
- });
302
- });
@@ -1,293 +0,0 @@
1
- /**
2
- * Build drizzle tables at runtime from an introspected schema.
3
- *
4
- * A project with declared collections gets its drizzle tables from a generated `schema.generated.ts` that
5
- * the developer commits. BaaS mode has no such file — it points at a database
6
- * and serves it — so the equivalent table objects are constructed here from
7
- * `information_schema` metadata.
8
- *
9
- * These are handed to drizzle as its schema, which keeps the relational query
10
- * path (`db.query.*`) working; without them FetchService would fall back to
11
- * plain selects and lose relation loading.
12
- */
13
- import {
14
- bigint,
15
- boolean,
16
- char,
17
- cidr,
18
- customType,
19
- date,
20
- doublePrecision,
21
- geometry,
22
- inet,
23
- integer,
24
- interval,
25
- json,
26
- jsonb,
27
- line,
28
- macaddr,
29
- macaddr8,
30
- numeric,
31
- pgSchema,
32
- pgTable,
33
- point,
34
- primaryKey,
35
- real,
36
- smallint,
37
- text,
38
- time,
39
- timestamp,
40
- uuid,
41
- varchar,
42
- vector,
43
- type PgColumnBuilderBase,
44
- type PgTable
45
- } from "drizzle-orm/pg-core";
46
-
47
- import { relations, type Relations } from "drizzle-orm";
48
-
49
- import type { TableColumn, TableMeta } from "./introspect-db-logic";
50
-
51
- /** drizzle ships no bytea builder; binary must round-trip as a Buffer. */
52
- const bytea = customType<{ data: Buffer; driverData: Buffer }>({
53
- dataType: () => "bytea"
54
- });
55
-
56
- /**
57
- * Postgres stores a column's type modifier (varchar length, vector dimensions)
58
- * in `atttypmod`. For length-carrying string types it is length + VARHDRSZ(4);
59
- * for pgvector it is the dimension count as-is. -1 means unspecified.
60
- */
61
- function varlenLength(col: TableColumn): number | undefined {
62
- return col.atttypmod && col.atttypmod > 4 ? col.atttypmod - 4 : undefined;
63
- }
64
-
65
- /**
66
- * Map a Postgres type to a drizzle column builder, keyed on `udt_name` — the
67
- * concrete underlying type, which is exact where `data_type` reports umbrella
68
- * values like "ARRAY" or "USER-DEFINED".
69
- *
70
- * Unknown types fall back to `text`: the driver still reads and writes them,
71
- * with the value passing through as a string, which beats dropping the column.
72
- */
73
- function scalarBuilder(udtName: string, name: string, col: TableColumn): PgColumnBuilderBase {
74
- switch (udtName) {
75
- case "uuid":
76
- return uuid(name);
77
- case "bool":
78
- return boolean(name);
79
- case "int2":
80
- return smallint(name);
81
- case "int4":
82
- return integer(name);
83
- case "int8":
84
- return bigint(name, { mode: "number" });
85
- case "float4":
86
- return real(name);
87
- case "float8":
88
- return doublePrecision(name);
89
- case "numeric":
90
- case "money":
91
- return numeric(name);
92
- case "json":
93
- return json(name);
94
- case "jsonb":
95
- return jsonb(name);
96
- case "date":
97
- return date(name);
98
- case "time":
99
- return time(name);
100
- case "timetz":
101
- return time(name, { withTimezone: true });
102
- case "timestamp":
103
- return timestamp(name);
104
- case "timestamptz":
105
- return timestamp(name, { withTimezone: true });
106
- case "interval":
107
- return interval(name);
108
- case "bytea":
109
- return bytea(name);
110
- case "inet":
111
- return inet(name);
112
- case "cidr":
113
- return cidr(name);
114
- case "macaddr":
115
- return macaddr(name);
116
- case "macaddr8":
117
- return macaddr8(name);
118
- case "point":
119
- return point(name);
120
- case "line":
121
- return line(name);
122
- case "geometry":
123
- return geometry(name);
124
- case "vector": {
125
- // drizzle requires the dimension count; without it fall back to text
126
- // rather than declaring a vector of unknown width.
127
- const dimensions = col.atttypmod && col.atttypmod > 0 ? col.atttypmod : undefined;
128
- return dimensions ? vector(name, { dimensions }) : text(name);
129
- }
130
- case "bpchar": {
131
- const length = varlenLength(col);
132
- return length ? char(name, { length }) : char(name);
133
- }
134
- case "varchar": {
135
- const length = varlenLength(col);
136
- return length ? varchar(name, { length }) : text(name);
137
- }
138
- default:
139
- // Includes text, enums (pg enums are strings on the wire), citext,
140
- // geography, and anything else this driver hasn't met yet.
141
- return text(name);
142
- }
143
- }
144
-
145
- function columnBuilderFor(col: TableColumn): PgColumnBuilderBase {
146
- // Array types are named after their element with a leading underscore
147
- // (_int4 = int4[]), so the element mapping is reused verbatim.
148
- if (col.udt_name.startsWith("_")) {
149
- const element = scalarBuilder(col.udt_name.slice(1), col.column_name, col);
150
- return (element as unknown as { array(): PgColumnBuilderBase }).array();
151
- }
152
- return scalarBuilder(col.udt_name, col.column_name, col);
153
- }
154
-
155
- /**
156
- * Build one drizzle table per introspected table, keyed by table name.
157
- */
158
- export function buildDrizzleTablesFromSchema(
159
- tablesMap: Map<string, TableMeta>,
160
- pgSchemaName = "public"
161
- ): Record<string, PgTable> {
162
- const schema = pgSchemaName === "public" ? null : pgSchema(pgSchemaName);
163
- // The column set is only known at runtime, so drizzle's generic table
164
- // signature can't be satisfied statically; call it through a loose type.
165
- const createTable = (schema ? schema.table.bind(schema) : pgTable) as unknown as (
166
- name: string,
167
- columns: Record<string, PgColumnBuilderBase>,
168
- extras?: (self: Record<string, unknown>) => unknown[]
169
- ) => PgTable;
170
-
171
- const tables: Record<string, PgTable> = {};
172
-
173
- for (const [tableName, meta] of tablesMap) {
174
- const columns: Record<string, PgColumnBuilderBase> = {};
175
-
176
- for (const col of meta.columns) {
177
- let builder = columnBuilderFor(col);
178
-
179
- if (col.is_nullable === "NO") {
180
- builder = (builder as unknown as { notNull(): PgColumnBuilderBase }).notNull();
181
- }
182
- // Single-column primary keys are marked inline; composite keys are
183
- // declared in the table extras below.
184
- if (meta.pks.length === 1 && meta.pks[0] === col.column_name) {
185
- builder = (builder as unknown as { primaryKey(): PgColumnBuilderBase }).primaryKey();
186
- }
187
-
188
- columns[col.column_name] = builder;
189
- }
190
-
191
- const isComposite = meta.pks.length > 1;
192
- tables[tableName] = createTable(
193
- tableName,
194
- columns,
195
- isComposite
196
- ? (t) => [primaryKey({ columns: meta.pks.map((pk) => t[pk]) as never })]
197
- : undefined
198
- );
199
- }
200
-
201
- return tables;
202
- }
203
-
204
- /**
205
- * Build drizzle `relations()` for the foreign keys, so the relational query
206
- * path can actually load them.
207
- *
208
- * FetchService asks drizzle for `with: { <key>: true }`, keyed by the relation
209
- * property on the collection. Tables alone don't satisfy that — without these,
210
- * `?include=author` silently returns the raw `author_id` and no author.
211
- *
212
- * The keys here must match `buildRelations` in introspect-runtime.ts, which is
213
- * what names the collection's relation properties.
214
- */
215
- export function buildDrizzleRelationsFromSchema(
216
- tablesMap: Map<string, TableMeta>,
217
- tables: Record<string, PgTable>
218
- ): Record<string, Relations> {
219
- /** Owning side, per table: the `one()` relations from its foreign keys. */
220
- const owning = new Map<string, { key: string; targetTable: string; fkColumn: string; targetColumn: string; relationName: string }[]>();
221
- /** Inverse side, per referenced table: the matching `many()` back-references. */
222
- const inverse = new Map<string, { key: string; sourceTable: string; relationName: string }[]>();
223
-
224
- for (const [tableName, meta] of tablesMap) {
225
- if (!tables[tableName]) continue;
226
- const columnNames = new Set(meta.columns.map((c) => c.column_name));
227
-
228
- for (const fk of meta.fks) {
229
- if (!tables[fk.foreign_table_name]) continue;
230
-
231
- // Mirrors buildRelations in introspect-runtime: author_id -> author,
232
- // falling back to the target table when the column name carries no hint.
233
- let key = fk.column_name.replace(/_id$/, "");
234
- if (meta.pks.includes(fk.column_name) && key === fk.column_name) {
235
- key = fk.foreign_table_name;
236
- }
237
- // A relation key must not shadow a real column.
238
- if (columnNames.has(key)) continue;
239
-
240
- // Pairs the two sides. Drizzle matches a named one() to the many()
241
- // carrying the same name, and disambiguates multiple foreign keys
242
- // into the same table.
243
- const relationName = `${tableName}_${fk.column_name}`;
244
-
245
- owning.set(tableName, [
246
- ...(owning.get(tableName) ?? []),
247
- { key, targetTable: fk.foreign_table_name, fkColumn: fk.column_name, targetColumn: fk.foreign_column_name, relationName }
248
- ]);
249
-
250
- const backKey = tableName;
251
- const targetColumns = new Set((tablesMap.get(fk.foreign_table_name)?.columns ?? []).map((c) => c.column_name));
252
- if (targetColumns.has(backKey)) continue;
253
-
254
- inverse.set(fk.foreign_table_name, [
255
- ...(inverse.get(fk.foreign_table_name) ?? []),
256
- { key: backKey, sourceTable: tableName, relationName }
257
- ]);
258
- }
259
- }
260
-
261
- const built: Record<string, Relations> = {};
262
-
263
- for (const tableName of tablesMap.keys()) {
264
- const table = tables[tableName];
265
- const ones = owning.get(tableName) ?? [];
266
- const manys = inverse.get(tableName) ?? [];
267
- if (!table || (ones.length === 0 && manys.length === 0)) continue;
268
-
269
- built[`${tableName}Relations`] = relations(table, ({ one, many }) => {
270
- const map: Record<string, unknown> = {};
271
-
272
- for (const rel of ones) {
273
- map[rel.key] = one(tables[rel.targetTable], {
274
- fields: [(table as unknown as Record<string, never>)[rel.fkColumn]],
275
- references: [(tables[rel.targetTable] as unknown as Record<string, never>)[rel.targetColumn]],
276
- relationName: rel.relationName
277
- });
278
- }
279
-
280
- // Drizzle needs the inverse of every named one(); without it,
281
- // normalizeRelation throws and the relational path is dead.
282
- for (const rel of manys) {
283
- // An inverse must not collide with an owning key on this table.
284
- if (map[rel.key]) continue;
285
- map[rel.key] = many(tables[rel.sourceTable], { relationName: rel.relationName });
286
- }
287
-
288
- return map as never;
289
- });
290
- }
291
-
292
- return built;
293
- }