@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,327 +0,0 @@
1
- /**
2
- * Runtime introspection — builds collections in memory from the live database.
3
- *
4
- * This is what makes BaaS mode work with zero configuration: instead of loading
5
- * collection files from disk, the server reads `information_schema` at boot and
6
- * derives a collection per table, so any database is served over REST without a
7
- * single config file.
8
- *
9
- * Distinct from `introspect-db.ts`, which runs the same queries but emits
10
- * TypeScript *source* for a developer to edit and commit (declared collections). The two
11
- * share the mapping helpers in `introspect-db-logic.ts` so a table is described
12
- * the same way whether it was generated or introspected.
13
- */
14
- import type { PostgresCollectionConfig } from "@rebasepro/types";
15
-
16
- import {
17
- TableRow,
18
- TableColumn,
19
- EnumValue,
20
- PrimaryKeyRow,
21
- ForeignKeyRow,
22
- TableMeta,
23
- buildTablesMap,
24
- buildEnumMap,
25
- identifyJoinTables,
26
- singularize,
27
- mapPgType,
28
- getIconForTable
29
- } from "./introspect-db-logic";
30
- import { humanize } from "./introspect-db-naming";
31
- import { firstFreeKey, toWireKey } from "@rebasepro/utils";
32
-
33
- export interface IntrospectedSchema {
34
- tablesMap: Map<string, TableMeta>;
35
- enumMap: Map<string, string[]>;
36
- joinTables: Set<string>;
37
- }
38
-
39
- /** Whether a table carries an authorization model of its own. */
40
- export interface TableRlsStatus {
41
- table: string;
42
- /** ALTER TABLE … ENABLE ROW LEVEL SECURITY has been run. */
43
- rlsEnabled: boolean;
44
- /** Policies attached to it. RLS enabled with none = nothing is visible. */
45
- policyCount: number;
46
- }
47
-
48
- /**
49
- * Read the RLS posture of each table in a schema.
50
- *
51
- * This is what decides whether baas mode may serve a table. A table with RLS
52
- * disabled has no authorization model: since every authenticated request runs
53
- * as `rebase_user`, and that role is granted DML on the schema, serving such a
54
- * table hands every row to every logged-in user.
55
- */
56
- export async function readRlsStatus(client: Queryable, pgSchema: string): Promise<Map<string, TableRlsStatus>> {
57
- const { rows } = await client.query<{ table: string; rls_enabled: boolean; policy_count: string | number }>(
58
- `SELECT c.relname AS table,
59
- c.relrowsecurity AS rls_enabled,
60
- (SELECT count(*) FROM pg_policy p WHERE p.polrelid = c.oid) AS policy_count
61
- FROM pg_class c
62
- JOIN pg_namespace n ON n.oid = c.relnamespace
63
- WHERE n.nspname = $1 AND c.relkind = 'r'`,
64
- [pgSchema]
65
- );
66
-
67
- return new Map(
68
- rows.map((r) => [
69
- r.table,
70
- { table: r.table, rlsEnabled: r.rls_enabled === true, policyCount: Number(r.policy_count ?? 0) }
71
- ])
72
- );
73
- }
74
-
75
- /** Minimal query surface — satisfied by pg.Client and pg.Pool alike. */
76
- export interface Queryable {
77
- query<R>(text: string, values?: unknown[]): Promise<{ rows: R[] }>;
78
- }
79
-
80
- /**
81
- * Read tables, columns, enums, primary keys and foreign keys for a schema.
82
- * Mirrors the queries in introspect-db.ts.
83
- */
84
- export async function introspectSchema(client: Queryable, pgSchema: string): Promise<IntrospectedSchema> {
85
- const { rows: tables } = await client.query<TableRow>(
86
- `SELECT table_name
87
- FROM information_schema.tables
88
- WHERE table_schema = $1 AND table_type = 'BASE TABLE'
89
- AND table_name NOT LIKE 'drizzle_%'
90
- AND table_name NOT LIKE 'rebase_%'
91
- ORDER BY table_name`,
92
- [pgSchema]
93
- );
94
-
95
- const { rows: columns } = await client.query<TableColumn>(
96
- `SELECT
97
- c.table_name,
98
- c.column_name,
99
- c.data_type,
100
- c.udt_name,
101
- c.is_nullable,
102
- c.column_default,
103
- (SELECT a.atttypmod FROM pg_attribute a
104
- JOIN pg_class pc ON a.attrelid = pc.oid
105
- WHERE pc.relname = c.table_name
106
- AND a.attname = c.column_name
107
- AND pc.relnamespace = (SELECT oid FROM pg_namespace WHERE nspname = c.table_schema)) as atttypmod
108
- FROM information_schema.columns c
109
- WHERE c.table_schema = $1`,
110
- [pgSchema]
111
- );
112
-
113
- const { rows: enumValues } = await client.query<EnumValue>(
114
- `SELECT t.typname AS enum_name,
115
- e.enumlabel AS enum_value,
116
- e.enumsortorder AS sort_order
117
- FROM pg_type t
118
- JOIN pg_enum e ON t.oid = e.enumtypid
119
- JOIN pg_namespace n ON t.typnamespace = n.oid
120
- WHERE n.nspname = $1
121
- ORDER BY t.typname, e.enumsortorder`,
122
- [pgSchema]
123
- );
124
-
125
- const { rows: pks } = await client.query<PrimaryKeyRow>(
126
- `SELECT t.relname as table_name, a.attname as column_name
127
- FROM pg_index i
128
- JOIN pg_attribute a ON a.attrelid = i.indrelid
129
- AND a.attnum = ANY(i.indkey)
130
- JOIN pg_class t ON t.oid = i.indrelid
131
- JOIN pg_namespace n ON n.oid = t.relnamespace
132
- WHERE i.indisprimary AND n.nspname = $1`,
133
- [pgSchema]
134
- );
135
-
136
- const { rows: fks } = await client.query<ForeignKeyRow>(
137
- `SELECT
138
- tc.table_name,
139
- kcu.column_name,
140
- ccu.table_name AS foreign_table_name,
141
- ccu.column_name AS foreign_column_name
142
- FROM information_schema.table_constraints AS tc
143
- JOIN information_schema.key_column_usage AS kcu
144
- ON tc.constraint_name = kcu.constraint_name
145
- AND tc.table_schema = kcu.table_schema
146
- JOIN information_schema.constraint_column_usage AS ccu
147
- ON ccu.constraint_name = tc.constraint_name
148
- AND ccu.table_schema = tc.table_schema
149
- WHERE tc.constraint_type = 'FOREIGN KEY' AND tc.table_schema = $1`,
150
- [pgSchema]
151
- );
152
-
153
- const tablesMap = buildTablesMap(tables, columns, pks, fks);
154
- return {
155
- tablesMap,
156
- enumMap: buildEnumMap(enumValues),
157
- joinTables: identifyJoinTables(tablesMap)
158
- };
159
- }
160
-
161
- /** Derive the `isId` flavour for a primary-key column. */
162
- function idKindFor(col: TableColumn, propType: string): "uuid" | "increment" | true {
163
- if (col.data_type.toLowerCase() === "uuid") return "uuid";
164
- if (propType === "number") return "increment";
165
- return true;
166
- }
167
-
168
- function buildProperties(
169
- meta: TableMeta,
170
- enumMap: Map<string, string[]>
171
- ): Record<string, Record<string, unknown>> {
172
- const properties: Record<string, Record<string, unknown>> = {};
173
- const takenKeys = new Set<string>();
174
-
175
- for (const col of meta.columns) {
176
- const isPk = meta.pks.includes(col.column_name);
177
- // Foreign keys surface as relations below, unless they are also part of
178
- // the primary key, in which case the column itself must stay.
179
- const isFk = meta.fks.some((fk) => fk.column_name === col.column_name);
180
- if (isFk && !isPk) continue;
181
-
182
- const enumValues = enumMap.get(col.udt_name);
183
- const isEnum = col.data_type === "USER-DEFINED" && enumValues !== undefined;
184
- const isVector = col.udt_name === "vector";
185
- const propType = isEnum ? "string" : isVector ? "vector" : mapPgType(col.data_type);
186
-
187
- const property: Record<string, unknown> = {
188
- name: humanize(col.column_name),
189
- columnName: col.column_name,
190
- type: propType
191
- };
192
-
193
- // The wire name, with `columnName` above carrying the column. The two
194
- // are different names for different things — this used to serve the
195
- // column, so a runtime-introspected collection answered `user_id` while
196
- // every authored one beside it answered `displayName`.
197
- //
198
- // `user_id` and `userId` as two real columns camel-case to one key, so
199
- // the first free candidate wins and the second falls back to its own
200
- // column name, then to a numbered tail. Never dropped, and stable for a
201
- // given database: columns arrive in ordinal order.
202
- const key = firstFreeKey([toWireKey(col.column_name), col.column_name], takenKeys);
203
- takenKeys.add(key);
204
-
205
- if (isPk) {
206
- property.isId = idKindFor(col, propType);
207
- } else if (col.is_nullable === "NO" && col.column_default === null) {
208
- property.validation = { required: true };
209
- }
210
-
211
- if (isEnum && enumValues) {
212
- property.enum = enumValues.map((value) => ({ id: value, label: humanize(value) }));
213
- }
214
-
215
- properties[key] = property;
216
- }
217
-
218
- return properties;
219
- }
220
-
221
- /**
222
- * Owning relations, derived from this table's foreign keys — the same shape
223
- * `generateCollectionFile` writes into a collection file: a `relation` property
224
- * whose nested descriptor carries `kind`, a `target` thunk and the `localKey`.
225
- *
226
- * The shape is load-bearing, not cosmetic. `resolveCollectionRelations` reads
227
- * relations from `property.relation` and `resolveRelation` requires `target` to
228
- * be a thunk; this used to emit `target`/`cardinality`/`localKey` flat on the
229
- * property with the slug as a bare string, which satisfies neither. Nothing
230
- * threw — the resolver simply skipped every such property and reported that the
231
- * collection had no relations. So an introspected BaaS collection had its FK
232
- * columns removed from `properties` (they "surface as relations") and then no
233
- * resolvable relation to surface as, which is why writing the FK column
234
- * directly came back as `has no field 'product_id'`: `assertKnownWriteFields`
235
- * learns that column from the resolved relation's `localKey`.
236
- *
237
- * The thunk closes over the collections being built in this same pass rather
238
- * than importing a module, which is what a runtime introspection has instead of
239
- * generated files. It is called lazily, after the map is fully populated, so a
240
- * table may reference one introspected later.
241
- */
242
- function buildRelations(
243
- meta: TableMeta,
244
- slugByTable: Map<string, string>,
245
- collectionBySlug: Map<string, PostgresCollectionConfig>
246
- ): Record<string, Record<string, unknown>> {
247
- const relations: Record<string, Record<string, unknown>> = {};
248
-
249
- for (const fk of meta.fks) {
250
- const targetSlug = slugByTable.get(fk.foreign_table_name);
251
- if (!targetSlug) continue;
252
-
253
- // Strip the conventional _id suffix: author_id -> author
254
- let key = toWireKey(fk.column_name.replace(/_id$/, ""));
255
- if (meta.pks.includes(fk.column_name) && key === fk.column_name) {
256
- // The FK is also the PK and its name doesn't imply a relation (e.g.
257
- // "id"), so naming the relation after the column would collide with
258
- // the primary-key property.
259
- key = fk.foreign_table_name;
260
- }
261
-
262
- relations[key] = {
263
- name: humanize(key),
264
- type: "relation",
265
- relation: {
266
- kind: "belongsTo",
267
- target: () => collectionBySlug.get(targetSlug),
268
- localKey: fk.column_name
269
- }
270
- };
271
- }
272
-
273
- return relations;
274
- }
275
-
276
- /**
277
- * Turn an introspected schema into collections.
278
- *
279
- * Join tables are skipped: they carry no identity of their own and exist to
280
- * express a many-to-many edge between two other tables.
281
- */
282
- export function buildCollectionsFromSchema(
283
- { tablesMap, enumMap, joinTables }: IntrospectedSchema,
284
- pgSchema: string
285
- ): PostgresCollectionConfig[] {
286
- const slugByTable = new Map<string, string>();
287
- for (const tableName of tablesMap.keys()) {
288
- if (!joinTables.has(tableName)) slugByTable.set(tableName, tableName);
289
- }
290
-
291
- const collections: PostgresCollectionConfig[] = [];
292
- // Filled as we go; the relation thunks read it lazily, so a table may point
293
- // at one that has not been built yet at the moment its relation is created.
294
- const collectionBySlug = new Map<string, PostgresCollectionConfig>();
295
-
296
- for (const [tableName, meta] of tablesMap) {
297
- if (joinTables.has(tableName)) continue;
298
-
299
- const collectionName = humanize(tableName);
300
- const collection = {
301
- name: collectionName,
302
- singularName: singularize(collectionName),
303
- slug: tableName,
304
- table: tableName,
305
- schema: pgSchema,
306
- icon: getIconForTable(tableName),
307
- properties: {
308
- ...buildProperties(meta, enumMap),
309
- ...buildRelations(meta, slugByTable, collectionBySlug)
310
- }
311
- } as unknown as PostgresCollectionConfig;
312
-
313
- collections.push(collection);
314
- collectionBySlug.set(tableName, collection);
315
- }
316
-
317
- return collections;
318
- }
319
-
320
- /** Introspect the database and return ready-to-serve collections. */
321
- export async function introspectCollections(
322
- client: Queryable,
323
- pgSchema: string
324
- ): Promise<PostgresCollectionConfig[]> {
325
- const schema = await introspectSchema(client, pgSchema);
326
- return buildCollectionsFromSchema(schema, pgSchema);
327
- }
@@ -1,131 +0,0 @@
1
- /**
2
- * A collection served by another engine is not this engine's to provision.
3
- *
4
- * Firestore and MongoDB collections are declared in the same
5
- * `config/collections` directory as the Postgres ones — that is what
6
- * `dataSource` routing is for — and every stage of the SQL toolchain used to
7
- * read "the collections" as all of them. So a Firestore collection got a
8
- * `pgTable` in the generated schema, a `CREATE TABLE` at boot, RLS policies,
9
- * and an entry in the `db push` include list, while the app went on reading its
10
- * documents from Firestore.
11
- *
12
- * The include list is the one that can lose data: a name on it is a name Atlas
13
- * is *allowed to drop*, so a Firestore collection called `exercises` removed a
14
- * real, unrelated `public.exercises` table's protection from the next
15
- * auto-approved `db push`.
16
- */
17
- import { describe, expect, it } from "@jest/globals";
18
- import type { CollectionConfig } from "@rebasepro/types";
19
-
20
- import { generateSchema } from "./generate-drizzle-schema-logic";
21
- import {
22
- generatePostgresDdl,
23
- generatePostgresPoliciesDdl,
24
- planCollectionPolicies
25
- } from "./generate-postgres-ddl-logic";
26
- import { planCollectionSchemaEnsure, type ExistingSchema } from "./ensure-collection-tables";
27
- import { getTableIncludesFromCollections } from "../cli-helpers";
28
-
29
- const sqlCollection = {
30
- name: "Customers",
31
- slug: "customers",
32
- properties: {
33
- id: { name: "ID", type: "string", isId: "uuid" },
34
- email: { name: "Email", type: "string" }
35
- },
36
- securityRules: [{ operation: "read", policy: "true" }]
37
- } as unknown as CollectionConfig;
38
-
39
- const firestoreCollection = {
40
- name: "Exercises",
41
- slug: "exercises",
42
- engine: "firestore",
43
- dataSource: "firestore",
44
- properties: {
45
- id: { name: "ID", type: "string" },
46
- title: { name: "Title", type: "string" },
47
- difficulty: {
48
- name: "Difficulty",
49
- type: "string",
50
- enum: [{ id: "easy", label: "Easy" }, { id: "hard", label: "Hard" }]
51
- }
52
- }
53
- } as unknown as CollectionConfig;
54
-
55
- const mongoCollection = {
56
- name: "Events",
57
- slug: "events",
58
- engine: "mongodb",
59
- properties: { id: { name: "ID", type: "string" } }
60
- } as unknown as CollectionConfig;
61
-
62
- /** Routed by `dataSource` alone — no `engine` field to read. */
63
- const dataSourceOnlyCollection = {
64
- name: "Articles",
65
- slug: "articles",
66
- dataSource: "firestore",
67
- properties: { id: { name: "ID", type: "string" } }
68
- } as unknown as CollectionConfig;
69
-
70
- const all = [sqlCollection, firestoreCollection, mongoCollection, dataSourceOnlyCollection];
71
-
72
- describe("the Drizzle schema generator", () => {
73
- it("emits a table for the Postgres collection only", async () => {
74
- const schema = await generateSchema(all);
75
-
76
- expect(schema).toContain("customers");
77
- expect(schema).not.toContain("exercises");
78
- expect(schema).not.toContain("events");
79
- expect(schema).not.toContain("articles");
80
- });
81
-
82
- it("does not emit enum types for a document store's fields", async () => {
83
- const schema = await generateSchema(all);
84
- expect(schema).not.toContain("difficulty");
85
- });
86
- });
87
-
88
- describe("the Postgres DDL generator", () => {
89
- it("creates no table for a non-SQL collection", async () => {
90
- const ddl = await generatePostgresDdl(all, { includePolicies: false });
91
-
92
- expect(ddl).toContain('CREATE TABLE "public"."customers"');
93
- expect(ddl).not.toContain("exercises");
94
- expect(ddl).not.toContain("events");
95
- });
96
-
97
- it("plans no RLS for a store that has none", () => {
98
- const policies = generatePostgresPoliciesDdl(all);
99
- expect(policies).not.toContain("exercises");
100
-
101
- const plans = planCollectionPolicies(all);
102
- expect(plans.map(p => p.qualified)).toEqual(["public.customers"]);
103
- });
104
- });
105
-
106
- describe("the boot-time schema ensure", () => {
107
- const empty = (): ExistingSchema => ({ tables: new Map(), enums: new Set() });
108
-
109
- it("creates nothing for collections another engine stores", () => {
110
- const plan = planCollectionSchemaEnsure(all, empty());
111
-
112
- expect(plan.actions.map(a => a.target)).toEqual(
113
- expect.arrayContaining(["public.customers"])
114
- );
115
- for (const action of plan.actions) {
116
- expect(action.target).not.toContain("exercises");
117
- expect(action.target).not.toContain("events");
118
- expect(action.target).not.toContain("articles");
119
- }
120
- });
121
- });
122
-
123
- describe("the db push include list", () => {
124
- it("does not claim a table for a Firestore collection", async () => {
125
- // Anything absent from this list is *excluded* from the declarative
126
- // apply, which is what protects it from being dropped.
127
- const includes = await getTableIncludesFromCollections(all);
128
-
129
- expect(includes).toEqual(["public.customers"]);
130
- });
131
- });