@rebasepro/server-postgres 0.0.1-canary.4829d6e

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 (121) hide show
  1. package/LICENSE +22 -0
  2. package/README.md +86 -0
  3. package/dist/PostgresAdapter.d.ts +6 -0
  4. package/dist/PostgresBackendDriver.d.ts +150 -0
  5. package/dist/PostgresBootstrapper.d.ts +51 -0
  6. package/dist/auth/ensure-tables.d.ts +10 -0
  7. package/dist/auth/services.d.ts +250 -0
  8. package/dist/backup/backup-cli.d.ts +3 -0
  9. package/dist/backup/backup-cron.d.ts +53 -0
  10. package/dist/backup/backup-service.d.ts +85 -0
  11. package/dist/backup/index.d.ts +12 -0
  12. package/dist/backup/pg-tools.d.ts +110 -0
  13. package/dist/backup/retention.d.ts +35 -0
  14. package/dist/chunk-DSJWtz9O.js +40 -0
  15. package/dist/cli-errors.d.ts +42 -0
  16. package/dist/cli-helpers.d.ts +7 -0
  17. package/dist/cli.d.ts +1 -0
  18. package/dist/collections/PostgresCollectionRegistry.d.ts +47 -0
  19. package/dist/connection.d.ts +65 -0
  20. package/dist/data-transformer.d.ts +55 -0
  21. package/dist/databasePoolManager.d.ts +20 -0
  22. package/dist/history/HistoryService.d.ts +71 -0
  23. package/dist/history/ensure-history-table.d.ts +7 -0
  24. package/dist/index.d.ts +15 -0
  25. package/dist/index.es.js +23535 -0
  26. package/dist/index.es.js.map +1 -0
  27. package/dist/interfaces.d.ts +18 -0
  28. package/dist/schema/auth-bootstrap-sql.d.ts +24 -0
  29. package/dist/schema/auth-default-policies.d.ts +10 -0
  30. package/dist/schema/auth-schema.d.ts +2376 -0
  31. package/dist/schema/doctor-cli.d.ts +2 -0
  32. package/dist/schema/doctor.d.ts +58 -0
  33. package/dist/schema/dynamic-tables.d.ts +31 -0
  34. package/dist/schema/generate-drizzle-schema-logic.d.ts +2 -0
  35. package/dist/schema/generate-drizzle-schema.d.ts +1 -0
  36. package/dist/schema/generate-postgres-ddl-logic.d.ts +5 -0
  37. package/dist/schema/generate-postgres-ddl.d.ts +1 -0
  38. package/dist/schema/introspect-db-inference.d.ts +5 -0
  39. package/dist/schema/introspect-db-logic.d.ts +118 -0
  40. package/dist/schema/introspect-db.d.ts +1 -0
  41. package/dist/schema/introspect-runtime.d.ts +57 -0
  42. package/dist/schema/test-schema.d.ts +24 -0
  43. package/dist/security/policy-drift.d.ts +57 -0
  44. package/dist/security/rls-enforcement.d.ts +122 -0
  45. package/dist/services/BranchService.d.ts +47 -0
  46. package/dist/services/FetchService.d.ts +214 -0
  47. package/dist/services/PersistService.d.ts +39 -0
  48. package/dist/services/RelationService.d.ts +109 -0
  49. package/dist/services/cdc/CdcListener.d.ts +54 -0
  50. package/dist/services/cdc/trigger-cdc.d.ts +64 -0
  51. package/dist/services/collection-helpers.d.ts +38 -0
  52. package/dist/services/dataService.d.ts +110 -0
  53. package/dist/services/index.d.ts +4 -0
  54. package/dist/services/realtimeService.d.ts +298 -0
  55. package/dist/src-Eh-CZosp.js +595 -0
  56. package/dist/src-Eh-CZosp.js.map +1 -0
  57. package/dist/types.d.ts +3 -0
  58. package/dist/utils/drizzle-conditions.d.ts +138 -0
  59. package/dist/utils/pg-array-null-patch.d.ts +16 -0
  60. package/dist/utils/pg-error-utils.d.ts +65 -0
  61. package/dist/utils/table-classification.d.ts +8 -0
  62. package/dist/websocket.d.ts +18 -0
  63. package/package.json +113 -0
  64. package/src/PostgresAdapter.ts +58 -0
  65. package/src/PostgresBackendDriver.ts +1387 -0
  66. package/src/PostgresBootstrapper.ts +581 -0
  67. package/src/auth/ensure-tables.ts +367 -0
  68. package/src/auth/services.ts +1321 -0
  69. package/src/backup/backup-cli.ts +383 -0
  70. package/src/backup/backup-cron.ts +189 -0
  71. package/src/backup/backup-service.ts +299 -0
  72. package/src/backup/index.ts +12 -0
  73. package/src/backup/pg-tools.ts +231 -0
  74. package/src/backup/retention.ts +75 -0
  75. package/src/cli-errors.ts +265 -0
  76. package/src/cli-helpers.ts +196 -0
  77. package/src/cli.ts +786 -0
  78. package/src/collections/PostgresCollectionRegistry.ts +103 -0
  79. package/src/connection.ts +166 -0
  80. package/src/data-transformer.ts +733 -0
  81. package/src/databasePoolManager.ts +87 -0
  82. package/src/history/HistoryService.ts +272 -0
  83. package/src/history/ensure-history-table.ts +46 -0
  84. package/src/index.ts +15 -0
  85. package/src/interfaces.ts +60 -0
  86. package/src/schema/auth-bootstrap-sql.ts +41 -0
  87. package/src/schema/auth-default-policies.ts +125 -0
  88. package/src/schema/auth-schema.ts +233 -0
  89. package/src/schema/doctor-cli.ts +113 -0
  90. package/src/schema/doctor.ts +733 -0
  91. package/src/schema/dynamic-tables.test.ts +302 -0
  92. package/src/schema/dynamic-tables.ts +293 -0
  93. package/src/schema/generate-drizzle-schema-logic.ts +850 -0
  94. package/src/schema/generate-drizzle-schema.ts +131 -0
  95. package/src/schema/generate-postgres-ddl-logic.ts +490 -0
  96. package/src/schema/generate-postgres-ddl.ts +92 -0
  97. package/src/schema/introspect-db-inference.ts +238 -0
  98. package/src/schema/introspect-db-logic.ts +910 -0
  99. package/src/schema/introspect-db.ts +266 -0
  100. package/src/schema/introspect-runtime.test.ts +212 -0
  101. package/src/schema/introspect-runtime.ts +293 -0
  102. package/src/schema/test-schema.ts +11 -0
  103. package/src/security/policy-drift.test.ts +122 -0
  104. package/src/security/policy-drift.ts +159 -0
  105. package/src/security/rls-enforcement.ts +295 -0
  106. package/src/services/BranchService.ts +251 -0
  107. package/src/services/FetchService.ts +1661 -0
  108. package/src/services/PersistService.ts +329 -0
  109. package/src/services/RelationService.ts +1306 -0
  110. package/src/services/cdc/CdcListener.ts +167 -0
  111. package/src/services/cdc/trigger-cdc.ts +169 -0
  112. package/src/services/collection-helpers.ts +151 -0
  113. package/src/services/dataService.ts +246 -0
  114. package/src/services/index.ts +13 -0
  115. package/src/services/realtimeService.ts +1502 -0
  116. package/src/types.ts +4 -0
  117. package/src/utils/drizzle-conditions.ts +1162 -0
  118. package/src/utils/pg-array-null-patch.ts +42 -0
  119. package/src/utils/pg-error-utils.ts +227 -0
  120. package/src/utils/table-classification.ts +16 -0
  121. package/src/websocket.ts +640 -0
@@ -0,0 +1,302 @@
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
+ });
@@ -0,0 +1,293 @@
1
+ /**
2
+ * Build drizzle tables at runtime from an introspected schema.
3
+ *
4
+ * CMS mode 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
+ }