@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,420 +0,0 @@
1
- import { PgTable, AnyPgColumn } from "drizzle-orm/pg-core";
2
- import { getTableColumns } from "drizzle-orm";
3
- import { CollectionConfig, Property, ResolvedHasMany, ResolvedHasOne } from "@rebasepro/types";
4
- import { PostgresCollectionRegistry } from "../collections/PostgresCollectionRegistry";
5
- import { fieldKeyForColumn, getTableName } from "@rebasepro/common";
6
- import { ApiError, logger } from "@rebasepro/server";
7
-
8
- // Row identity is derived on both sides of the wire — the driver parses an
9
- // incoming address into key columns, the admin derives one from a served row —
10
- // so the implementation lives in `common` and both agree by construction.
11
- export { buildCompositeId, parseIdValues, isAddressableId, COMPOSITE_ID_SEPARATOR } from "@rebasepro/common";
12
- export type { PrimaryKeyInfo } from "@rebasepro/common";
13
- import { buildCompositeId, COMPOSITE_ID_SEPARATOR, getDeclaredPrimaryKeys, isAddressableId } from "@rebasepro/common";
14
- import type { PrimaryKeyInfo } from "@rebasepro/common";
15
-
16
- /**
17
- * Shared helper functions for row operations.
18
- * These are used by FetchService, PersistService, and RelationService.
19
- *
20
- * All functions that need collection/table lookups require an explicit
21
- * `PostgresCollectionRegistry` instance — there is no global singleton.
22
- */
23
-
24
- /**
25
- * Interface for Drizzle column metadata introspection.
26
- * Replaces unsafe `as Record<string, unknown>` double-cast chains.
27
- */
28
- export interface DrizzleColumnMeta {
29
- columnType?: string;
30
- dataType?: string;
31
- primary?: boolean;
32
- }
33
-
34
- /** Safely extract Drizzle column metadata from a column object. */
35
- export function getColumnMeta(col: AnyPgColumn): DrizzleColumnMeta {
36
- const raw = col as unknown as Record<string | symbol, unknown>;
37
- return {
38
- columnType: typeof raw.columnType === "string" ? raw.columnType : undefined,
39
- dataType: typeof raw.dataType === "string" ? raw.dataType : undefined,
40
- primary: typeof raw.primary === "boolean" ? raw.primary : undefined
41
- };
42
- }
43
-
44
- /**
45
- * Whether an address could name a row in this table, judged by the columns.
46
- *
47
- * {@link getPrimaryKeys} lets a config's `isId: "uuid"` win over the schema, so
48
- * its `isUUID` is a claim rather than a fact — right for deriving addresses,
49
- * wrong for refusing a query. Here the Drizzle column type decides, because it
50
- * is what Postgres will enforce: a `uuid` column meets `/c/products/new` with
51
- * `22P02`, which aborts the surrounding transaction and turns every later
52
- * statement into an unrelated-looking `25P02`.
53
- */
54
- export function idCanAddressTable(
55
- id: string | number,
56
- table: PgTable,
57
- idInfoArray: PrimaryKeyInfo[]
58
- ): boolean {
59
- const columnBacked = idInfoArray.map(info => {
60
- const col = table[info.fieldName as keyof typeof table] as AnyPgColumn | undefined;
61
- const meta = col ? getColumnMeta(col) : undefined;
62
- // No column to ask (a key the schema does not carry) leaves the id
63
- // addressable: refusing on a guess would 404 rows that do exist.
64
- if (!meta?.columnType) return info;
65
- return { ...info,
66
- isUUID: meta.columnType === "PgUUID" };
67
- });
68
- return isAddressableId(id, columnBacked);
69
- }
70
-
71
- export function getCollectionByPath(collectionPath: string, registry: PostgresCollectionRegistry): CollectionConfig {
72
- const collection = registry.getCollectionByPath(collectionPath);
73
- if (!collection) {
74
- const registered = registry.getCollections().map(c => c.slug).join(", ");
75
- throw new Error(`Collection not found: ${collectionPath}. Registered collections: [${registered}]`);
76
- }
77
- return collection;
78
- }
79
-
80
- /**
81
- * Reject a write naming something that is not a column of the table.
82
- *
83
- * Drizzle builds INSERT from `Object.entries(table[Symbol.Columns])` and UPDATE
84
- * from `Object.keys(tableColumns)`, so a key the table does not carry is not
85
- * rejected by anything — it is *left out of the statement*. The insert answers
86
- * 201 having stored nothing under that name; the update, if the key was the
87
- * only one, builds `update "posts" set where …` and Postgres raises a syntax
88
- * error (SQLSTATE 42601), which is neither class 22 nor 23 and so surfaces as a
89
- * 500 for what is a caller's typo.
90
- *
91
- * That makes this the last honest place to check, and the only one every write
92
- * passes through. `assertKnownWriteFields` in the REST layer checks the same
93
- * thing against the *config* and is skipped on four paths — `strictWrites:
94
- * false`, a collection declaring no properties, an auth adapter that owns the
95
- * body's shape, and a nested route whose target cannot be walked — and it never
96
- * sees an in-process `rebase.data` write at all.
97
- *
98
- * It also gives `strictWrites: false` a truthful implementation. The flag is
99
- * documented for "a column that really does exist which the config never
100
- * declared", and skipping the config check alone could not deliver that: the
101
- * value was dropped a layer later regardless. Skipping the config check and
102
- * keeping this one does exactly what the flag says — the column must exist,
103
- * the property need not.
104
- */
105
- export function assertWritableColumns(
106
- values: Record<string, unknown>,
107
- table: PgTable,
108
- collectionPath: string
109
- ): void {
110
- // Only a real Drizzle table carries a column list. A stand-in that does not
111
- // (a test double, a registry entry built by hand) has nothing to check
112
- // against, and inventing an answer from its own keys would reject writes
113
- // over the shape of the double rather than the shape of the table.
114
- const columns = getTableColumns(table) as Record<string, unknown> | undefined;
115
- if (!columns || Object.keys(columns).length === 0) return;
116
-
117
- const unknown = Object.keys(values).filter(key => !(key in columns));
118
- if (unknown.length === 0) return;
119
-
120
- // The offending keys, and deliberately not the list of real ones: this
121
- // error is reachable on paths where the REST field check was skipped, and
122
- // an `excludeFromApi` column is documented as never being served to a
123
- // caller. Naming what was sent is the actionable half anyway.
124
- throw ApiError.badRequest(
125
- `'${collectionPath}' has no column${unknown.length > 1 ? "s" : ""} ` +
126
- `${unknown.map(key => `'${key}'`).join(", ")}, so the value${unknown.length > 1 ? "s" : ""} ` +
127
- "would have been dropped before the statement was built.",
128
- "VALIDATION_UNKNOWN_FIELDS"
129
- );
130
- }
131
-
132
- /**
133
- * A relation whose names do not resolve against the registered schema.
134
- *
135
- * Every one of these used to be a `logger.warn` followed by `continue`, so a
136
- * save reported success for a relation it had not written and a read answered
137
- * `[]` for one it could not resolve. `assertRelationsResolve` (validate-relations)
138
- * fails boot on the same defects, which is where they belong — a server that
139
- * refuses to start is recoverable in a minute. This is the second line, for the
140
- * paths that assemble a registry by hand, and it exists so that "cannot resolve"
141
- * is never again reported as "done".
142
- *
143
- * @param label `<collection>.<relation>`
144
- * @param detail what does not resolve, in terms of the schema
145
- */
146
- export function relationMisconfigured(label: string, detail: string): ApiError {
147
- return ApiError.internal(
148
- `Relation '${label}' does not resolve against the registered schema: ${detail}. ` +
149
- "The operation was refused rather than skipped — silently dropping it would report " +
150
- "success for a write that never happened, or emptiness for rows that exist. Run " +
151
- "`rebase schema generate` if the generated schema is older than the database.",
152
- "RELATION_MISCONFIGURED"
153
- );
154
- }
155
-
156
- export function getTableForCollection(collection: CollectionConfig, registry: PostgresCollectionRegistry): PgTable<any> {
157
- const tableName = getTableName(collection);
158
- const table = registry.getTable(tableName);
159
- if (!table) {
160
- throw new Error(`Table not found for collection '${collection.slug}' (table: ${tableName})`);
161
- }
162
- return table;
163
- }
164
-
165
- /**
166
- * The key columns a collection's rows are addressed by.
167
- *
168
- * Three tiers, in order: properties marked `isId`, the primary keys of the
169
- * drizzle schema, and finally a column literally named `id`. Only the first is
170
- * visible to the browser, which is why a key known only to drizzle is reported
171
- * at boot — see {@link warnOnKeysTheAdminCannotResolve}.
172
- *
173
- * Returns `[]` when nothing resolves, rather than throwing. It used to open by
174
- * resolving the table, which throws when there is none — so the `isId` tier,
175
- * which needs no table at all, was unreachable for exactly the collections
176
- * most likely to have no table registered. Every caller that wanted "no keys"
177
- * to mean "no keys" had to spell that out in a try/catch.
178
- *
179
- * Callers that cannot proceed without a key must say so themselves, naming the
180
- * collection: an empty array here means "this collection has no address", which
181
- * is a different answer in a notification (broadcast a wildcard) than in a save
182
- * (fail).
183
- */
184
- export function getPrimaryKeys(collection: CollectionConfig, registry: PostgresCollectionRegistry): PrimaryKeyInfo[] {
185
- // Explicitly declared `isId` properties win, and need no table.
186
- if (collection.properties) {
187
- const idProps = Object.entries(collection.properties)
188
- .filter(([_, prop]) => "isId" in (prop as object) && Boolean((prop as { isId?: unknown }).isId))
189
- .map(([key, prop]) => ({
190
- fieldName: key,
191
- type: prop.type === "number" ? "number" as const : "string" as const,
192
- isUUID: (prop as { isId?: unknown }).isId === "uuid"
193
- }));
194
-
195
- if (idProps.length > 0) {
196
- return idProps;
197
- }
198
- }
199
-
200
- // The remaining tiers read the drizzle schema, so they need the table. A
201
- // collection without one — another engine's, or simply unregistered — has
202
- // nothing more to offer.
203
- const table = registry.getTable(getTableName(collection));
204
- if (!table) return [];
205
-
206
- // Otherwise infer from Drizzle schema
207
- const keys: PrimaryKeyInfo[] = [];
208
- for (const [key, colRaw] of Object.entries(table)) {
209
- const col = colRaw as AnyPgColumn;
210
- if (col && typeof col === "object" && "primary" in col && col.primary) {
211
- const meta = getColumnMeta(col);
212
- const type = col.dataType === "number" || meta.columnType === "PgSerial" || meta.columnType === "PgInteger" ? "number" : "string";
213
- const isUUID = meta.columnType === "PgUUID";
214
- keys.push({ fieldName: key,
215
- type,
216
- isUUID });
217
- }
218
- }
219
-
220
- // A collection that declares no primary key gets the implicit `id` column,
221
- // which is what the schema generators emit for it — so read it back the
222
- // same way rather than reporting the table as keyless.
223
- if (keys.length === 0 && "id" in table) {
224
- const idCol = table["id" as keyof typeof table] as AnyPgColumn;
225
- const idMeta = getColumnMeta(idCol);
226
- const type = idCol.dataType === "number" || idMeta.columnType === "PgSerial" || idMeta.columnType === "PgInteger" ? "number" : "string";
227
- const isUUID = idMeta.columnType === "PgUUID";
228
- keys.push({ fieldName: "id",
229
- type,
230
- isUUID });
231
- }
232
-
233
- return keys;
234
- }
235
-
236
- /**
237
- * The key columns, for callers that cannot do their job without one.
238
- *
239
- * {@link getPrimaryKeys} answers "what keys, if any" and returns `[]` for a
240
- * collection with no address. Most of this driver, though, is building a WHERE
241
- * clause and has no meaning without a key — for those, an empty array is not an
242
- * answer, and indexing `[0]` into it produces `Cannot read properties of
243
- * undefined` three frames from where the real problem is. This says what is
244
- * wrong and which collection it is wrong about.
245
- */
246
- export function requirePrimaryKeys(collection: CollectionConfig, registry: PostgresCollectionRegistry): PrimaryKeyInfo[] {
247
- const keys = getPrimaryKeys(collection, registry);
248
- if (keys.length === 0) {
249
- throw new Error(
250
- `Collection '${collection.slug}' has no primary key, so its rows cannot be addressed. ` +
251
- `Mark the key property with \`isId\` in its config, or register a table whose schema declares one.`
252
- );
253
- }
254
- return keys;
255
- }
256
-
257
- /**
258
- * The column on the *source* table that a `hasOne`/`hasMany` link points at.
259
- *
260
- * `sourceKey` is authored when the two sides join on a natural key — an
261
- * external identity id, a SKU — and left off when they join on the row id,
262
- * which is the overwhelming majority. That makes `undefined` the only optional
263
- * field on a resolved relation, so it gets exactly one reader: this function.
264
- * Every consumer that needs the column asks here, and none of them re-derives
265
- * "or else the primary key" for itself. That is the whole point — the fallback
266
- * chains this codebase removed from relation resolution were dangerous because
267
- * they were *duplicated* and could disagree, not because they existed.
268
- */
269
- export function sourceKeyField(
270
- relation: ResolvedHasOne | ResolvedHasMany,
271
- sourceCollection: CollectionConfig,
272
- registry: PostgresCollectionRegistry
273
- ): string {
274
- // A **field**, as the name says: callers index the Drizzle table and the
275
- // rows it returns with what comes back from here, and both are keyed by the
276
- // wire name. `sourceKey` is authored in *column* terms, like every other
277
- // link on a relation, so it is translated here rather than at each of the
278
- // call sites — where it used to be `tenant_id` looked up on a table whose
279
- // key is `tenantId`, finding nothing.
280
- if (relation.sourceKey) return fieldKeyForColumn(sourceCollection, relation.sourceKey);
281
- return requirePrimaryKeys(sourceCollection, registry)[0].fieldName;
282
- }
283
-
284
- /**
285
- * Whether this link joins on something other than the source's primary key.
286
- *
287
- * Callers that hold a parent *id* — which is most of them, since an id is what
288
- * a URL carries — must translate it to the source key's value before it can be
289
- * compared with the target's foreign key. Those that hold the parent *row*, or
290
- * that build a correlated subquery over the source table, can read the column
291
- * directly and skip the lookup.
292
- */
293
- export function joinsOnNaturalKey(
294
- relation: ResolvedHasOne | ResolvedHasMany,
295
- sourceCollection: CollectionConfig,
296
- registry: PostgresCollectionRegistry
297
- ): boolean {
298
- if (!relation.sourceKey) return false;
299
- // Compared as fields, because the right-hand side is one.
300
- return fieldKeyForColumn(sourceCollection, relation.sourceKey)
301
- !== requirePrimaryKeys(sourceCollection, registry)[0].fieldName;
302
- }
303
-
304
- /**
305
- * Collections whose key the *browser* cannot resolve, and what it will do
306
- * instead.
307
- *
308
- * The two sides resolve keys from different evidence. This driver reads, in
309
- * order: properties marked `isId`, the primary keys of the Drizzle schema, then
310
- * a column literally named `id`. The admin shares the `CollectionConfig` — it
311
- * compiles the same collection files into its bundle — but never the Drizzle
312
- * schema, so the middle tier is invisible to it.
313
- *
314
- * Nothing can normalize this at runtime: the server does not serve the admin
315
- * its collections, so a key resolved here cannot be handed over there. The
316
- * config files are the only thing both sides read, so the fix is an edit to
317
- * them, and the most this can do is say exactly which edit.
318
- *
319
- * Two shapes, and the second is the dangerous one:
320
- *
321
- * - No `isId`, no `id` property → the admin resolves no address, warns in the
322
- * console, and rows cannot be opened or linked.
323
- * - No `isId`, but an `id` property that is *not* the key → the admin addresses
324
- * rows by `id` while this driver reads the address as the real key. Nothing
325
- * errors: the addresses look right and route wrong.
326
- */
327
- export function findUnresolvableKeyCollections(
328
- collections: CollectionConfig[],
329
- registry: PostgresCollectionRegistry
330
- ): { collection: CollectionConfig; keys: PrimaryKeyInfo[]; shadowedByIdProperty: boolean }[] {
331
- const findings: { collection: CollectionConfig; keys: PrimaryKeyInfo[]; shadowedByIdProperty: boolean }[] = [];
332
-
333
- for (const collection of collections) {
334
- // Declared `isId` is the tier both sides share: if it is there, they agree.
335
- if (getDeclaredPrimaryKeys(collection).length > 0) continue;
336
-
337
- // No registered table resolves to no keys, and a collection this cannot
338
- // resolve a key for is one it has nothing to say about.
339
- const keys = getPrimaryKeys(collection, registry);
340
- if (keys.length === 0) continue;
341
-
342
- // A single key named `id` is the last tier on both sides: they agree
343
- // without anything being declared.
344
- if (keys.length === 1 && keys[0].fieldName === "id") continue;
345
-
346
- findings.push({
347
- collection,
348
- keys,
349
- shadowedByIdProperty: Boolean(collection.properties?.id)
350
- });
351
- }
352
-
353
- return findings;
354
- }
355
-
356
- /**
357
- * Report the collections from {@link findUnresolvableKeyCollections} at boot,
358
- * with the edit that fixes each one.
359
- *
360
- * Grouped by failure, not by collection: the shadowed case is a routing bug and
361
- * the silent case is a missing feature, and they deserve different urgency.
362
- */
363
- export function warnOnKeysTheAdminCannotResolve(
364
- collections: CollectionConfig[],
365
- registry: PostgresCollectionRegistry
366
- ): void {
367
- const findings = findUnresolvableKeyCollections(collections, registry);
368
- if (findings.length === 0) return;
369
-
370
- const edit = (f: { collection: CollectionConfig; keys: PrimaryKeyInfo[] }) =>
371
- `${f.collection.slug}: mark ${f.keys.map(k => `\`${k.fieldName}\``).join(" and ")} with ` +
372
- `\`isId: ${f.keys[0].isUUID ? "\"uuid\"" : f.keys[0].type === "number" ? "\"increment\"" : "true"}\``;
373
-
374
- const shadowed = findings.filter(f => f.shadowedByIdProperty);
375
- const silent = findings.filter(f => !f.shadowedByIdProperty);
376
-
377
- if (shadowed.length > 0) {
378
- logger.warn(
379
- `⚠️ These collections declare no \`isId\`, and their key is only in the drizzle schema — but they ` +
380
- `do have a property called \`id\`. The admin has no way to know \`id\` is not the key, so it will ` +
381
- `address rows by it while this server reads the address as the real key: the links look right and ` +
382
- `route wrong. Nothing will error.\n\n` +
383
- shadowed.map(f => ` • ${edit(f)}`).join("\n") + "\n"
384
- );
385
- }
386
-
387
- if (silent.length > 0) {
388
- logger.warn(
389
- `⚠️ These collections declare no \`isId\`, and their key is only in the drizzle schema, which the ` +
390
- `admin never sees. It will resolve no address for their rows, so detail links, caching and ` +
391
- `relations will not work for them:\n\n` +
392
- silent.map(f => ` • ${edit(f)}`).join("\n") + "\n"
393
- );
394
- }
395
- }
396
-
397
- /**
398
- * The address of a row: derived from the collection's primary keys, because a
399
- * row does not carry one — it is exactly its columns.
400
- *
401
- * Falls back to a literal `id` column, for a row that reached us from somewhere
402
- * other than this driver. Returns `""` when there is no key and no `id` —
403
- * callers decide what that means, since "unaddressable" is a different answer
404
- * in a notification (broadcast a wildcard) than in a save (fail).
405
- */
406
- export function deriveRowAddress(
407
- row: Record<string, unknown>,
408
- collection: CollectionConfig,
409
- registry: PostgresCollectionRegistry
410
- ): string {
411
- const composite = buildCompositeId(row, getPrimaryKeys(collection, registry));
412
- // An all-empty composite is indistinguishable from a missing key, and
413
- // `buildCompositeId` returns "" for no keys at all.
414
- if (composite && composite.split(COMPOSITE_ID_SEPARATOR).some(part => part !== "")) {
415
- return composite;
416
- }
417
- if (row.id !== undefined && row.id !== null) return String(row.id);
418
- return "";
419
- }
420
-
@@ -1,256 +0,0 @@
1
- // import { NodePgDatabase } from "drizzle-orm/node-postgres";
2
- import { FilterValues, LogicalCondition, OrderByTuple } from "@rebasepro/types";
3
- import type { VectorSearchParams } from "@rebasepro/types";
4
- import { FetchService } from "./FetchService";
5
- import { PersistService } from "./PersistService";
6
- import { RelationService } from "./RelationService";
7
- import { DataRepository, FetchCollectionOptions, SearchOptions, CountOptions, DrizzleClient } from "../interfaces";
8
- import { PostgresCollectionRegistry } from "../collections/PostgresCollectionRegistry";
9
-
10
- // Re-export data transformer functions for external use
11
- export { sanitizeAndConvertDates, serializeDataToServer, parseDataFromServer } from "../data-transformer";
12
-
13
- // Re-export service classes for direct use
14
- export { FetchService } from "./FetchService";
15
- export { PersistService } from "./PersistService";
16
- export { RelationService } from "./RelationService";
17
-
18
- // Re-export interfaces
19
- export * from "../interfaces";
20
-
21
- /**
22
- * DataService - Facade for row operations.
23
- *
24
- * This class provides a unified API for row CRUD operations by delegating
25
- * to specialized services:
26
- * - FetchService: Read operations (fetch, search, count)
27
- * - PersistService: Write operations (save, delete)
28
- * - RelationService: Relation operations (fetch related, update relations)
29
- *
30
- * Implements the DataRepository interface for database abstraction.
31
- */
32
- export class DataService implements DataRepository {
33
- private fetchService: FetchService;
34
- private persistService: PersistService;
35
-
36
- constructor(private db: DrizzleClient, private registry: PostgresCollectionRegistry) {
37
- this.fetchService = new FetchService(db, registry);
38
- this.persistService = new PersistService(db, registry);
39
- }
40
-
41
- // =============================================================
42
- // READ OPERATIONS - Delegated to FetchService
43
- // =============================================================
44
-
45
- /**
46
- * Fetch a single row by ID
47
- */
48
- async fetchOne<M extends Record<string, unknown>>(
49
- collectionPath: string,
50
- id: string | number,
51
- databaseId?: string
52
- ): Promise<Record<string, unknown> | undefined> {
53
- return this.fetchService.fetchOne<M>(collectionPath, id, databaseId);
54
- }
55
-
56
- /**
57
- * Fetch a collection of rows with optional filtering, ordering, and pagination
58
- */
59
- async fetchCollection<M extends Record<string, unknown>>(
60
- collectionPath: string,
61
- options: {
62
- filter?: FilterValues<Extract<keyof M, string>>;
63
- /** An `or(...)`/`and(...)` group, applied alongside `filter`. */
64
- logical?: LogicalCondition;
65
- orderBy?: string | OrderByTuple[];
66
- order?: "desc" | "asc";
67
- limit?: number;
68
- offset?: number;
69
- startAfter?: Record<string, unknown>;
70
- searchString?: string;
71
- databaseId?: string;
72
- vectorSearch?: VectorSearchParams;
73
- } = {}
74
- ): Promise<Record<string, unknown>[]> {
75
- return this.fetchService.fetchCollection<M>(collectionPath, options);
76
- }
77
-
78
- /**
79
- * Search rows by text
80
- */
81
- async searchRows<M extends Record<string, unknown>>(
82
- collectionPath: string,
83
- searchString: string,
84
- options: {
85
- filter?: FilterValues<Extract<keyof M, string>>;
86
- /** An `or(...)`/`and(...)` group, applied alongside `filter`. */
87
- logical?: LogicalCondition;
88
- orderBy?: string | OrderByTuple[];
89
- order?: "desc" | "asc";
90
- limit?: number;
91
- databaseId?: string;
92
- searchExplain?: boolean;
93
- } = {}
94
- ): Promise<Record<string, unknown>[]> {
95
- return this.fetchService.searchRows<M>(collectionPath, searchString, options);
96
- }
97
-
98
- /**
99
- * Count rows in a collection
100
- */
101
- async count<M extends Record<string, unknown>>(
102
- collectionPath: string,
103
- options: {
104
- filter?: FilterValues<Extract<keyof M, string>>;
105
- /** An `or(...)`/`and(...)` group, applied alongside `filter`. */
106
- logical?: LogicalCondition;
107
- searchString?: string;
108
- databaseId?: string;
109
- /** Only the `threshold` narrows the count — see `FetchService.count`. */
110
- vectorSearch?: VectorSearchParams;
111
- } = {}
112
- ): Promise<number> {
113
- return this.fetchService.count<M>(collectionPath, options);
114
- }
115
-
116
- /**
117
- * Check if a field value is unique in a collection
118
- */
119
- async checkUniqueField(
120
- collectionPath: string,
121
- fieldName: string,
122
- value: unknown,
123
- excludeEntityId?: string,
124
- databaseId?: string
125
- ): Promise<boolean> {
126
- return this.fetchService.checkUniqueField(collectionPath, fieldName, value, excludeEntityId, databaseId);
127
- }
128
-
129
- /**
130
- * Fetch rows related to a parent row
131
- */
132
- async fetchRelatedEntities<M extends Record<string, unknown>>(
133
- parentCollectionPath: string,
134
- parentId: string | number,
135
- relationKey: string,
136
- options: {
137
- filter?: FilterValues<Extract<keyof M, string>>;
138
- orderBy?: string | OrderByTuple[];
139
- order?: "desc" | "asc";
140
- limit?: number;
141
- startAfter?: Record<string, unknown>;
142
- searchString?: string;
143
- databaseId?: string;
144
- } = {}
145
- ): Promise<Record<string, unknown>[]> {
146
- const rows = await this.fetchService.getRelationService().fetchRelatedEntities<M>(
147
- parentCollectionPath,
148
- parentId,
149
- relationKey,
150
- options
151
- );
152
- return rows.map(e => ({ ...e.values, id: e.id }));
153
- }
154
-
155
- // =============================================================
156
- // WRITE OPERATIONS - Delegated to PersistService
157
- // =============================================================
158
-
159
- /**
160
- * Save an row (create or update)
161
- */
162
- async save<M extends Record<string, unknown>>(
163
- collectionPath: string,
164
- values: Partial<M>,
165
- id?: string | number,
166
- databaseId?: string,
167
- options?: { upsert?: boolean }
168
- ): Promise<Record<string, unknown>> {
169
- return this.persistService.save<M>(collectionPath, values, id, databaseId, options);
170
- }
171
-
172
- /**
173
- * Delete an row by ID
174
- */
175
- async delete(
176
- collectionPath: string,
177
- id: string | number,
178
- databaseId?: string
179
- ): Promise<void> {
180
- return this.persistService.delete(collectionPath, id, databaseId);
181
- }
182
-
183
- /**
184
- * Delete all rows from a collection
185
- */
186
- async deleteAll(collectionPath: string, databaseId?: string): Promise<void> {
187
- return this.persistService.deleteAll(collectionPath, databaseId);
188
- }
189
-
190
-
191
- /**
192
- * Execute raw SQL
193
- */
194
- async executeSql(sqlText: string, params?: unknown[]): Promise<Record<string, unknown>[]> {
195
- if (process.env.NODE_ENV !== "production") {
196
- console.debug("Executing raw SQL:", sqlText, params?.length ? `with ${params.length} params` : "");
197
- }
198
- const { sql } = await import("drizzle-orm");
199
-
200
- let result;
201
- if (params && params.length > 0) {
202
- // Build a parameterized query using Drizzle's sql tagged template.
203
- // Split the SQL text on $1, $2, … placeholders and interleave
204
- // with sql.param() calls so the underlying pg driver binds them safely.
205
- const parts = sqlText.split(/\$(\d+)/);
206
- const chunks: ReturnType<typeof sql.raw | typeof sql.param>[] = [];
207
- for (let i = 0; i < parts.length; i++) {
208
- if (i % 2 === 0) {
209
- // Literal SQL text fragment
210
- if (parts[i].length > 0) {
211
- chunks.push(sql.raw(parts[i]));
212
- }
213
- } else {
214
- // Parameter reference — $N (1-indexed)
215
- const paramIndex = Number(parts[i]) - 1;
216
- chunks.push(sql.param(params[paramIndex]));
217
- }
218
- }
219
- const query = sql.join(chunks, sql.raw(""));
220
- result = await this.db.execute(query);
221
- } else {
222
- result = await this.db.execute(sql.raw(sqlText));
223
- }
224
-
225
- const rows = result.rows;
226
- if (process.env.NODE_ENV !== "production") {
227
- console.debug(`SQL executed successfully. Returned ${Array.isArray(rows) ? rows.length : "non-array"} rows.`);
228
- }
229
- return rows as Record<string, unknown>[];
230
- }
231
-
232
- // =============================================================
233
- // SERVICE ACCESSORS
234
- // =============================================================
235
-
236
- /**
237
- * Get the underlying FetchService for advanced use
238
- */
239
- getFetchService(): FetchService {
240
- return this.fetchService;
241
- }
242
-
243
- /**
244
- * Get the underlying PersistService for advanced use
245
- */
246
- getPersistService(): PersistService {
247
- return this.persistService;
248
- }
249
-
250
- /**
251
- * Get the underlying RelationService for advanced use
252
- */
253
- getRelationService(): RelationService {
254
- return this.fetchService.getRelationService();
255
- }
256
- }
@@ -1,14 +0,0 @@
1
- // Re-export all service classes
2
- export { FetchService } from "./FetchService";
3
- export { PersistService } from "./PersistService";
4
- export { RelationService } from "./RelationService";
5
-
6
- // Re-export helper functions
7
- export {
8
- getCollectionByPath,
9
- getTableForCollection,
10
- getPrimaryKeys,
11
- deriveRowAddress,
12
- parseIdValues,
13
- buildCompositeId
14
- } from "./collection-helpers";