@rebasepro/server-postgres 0.9.1-canary.0fce67c → 0.9.1-canary.1d2d8b5

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 (43) hide show
  1. package/dist/PostgresBackendDriver.d.ts +2 -25
  2. package/dist/PostgresBootstrapper.d.ts +0 -10
  3. package/dist/auth/services.d.ts +0 -16
  4. package/dist/connection.d.ts +0 -21
  5. package/dist/data-transformer.d.ts +2 -9
  6. package/dist/index.es.js +548 -1329
  7. package/dist/index.es.js.map +1 -1
  8. package/dist/schema/auth-default-policies.d.ts +10 -0
  9. package/dist/schema/doctor.d.ts +1 -1
  10. package/dist/services/FetchService.d.ts +24 -4
  11. package/dist/services/PersistService.d.ts +1 -9
  12. package/dist/services/RelationService.d.ts +1 -34
  13. package/dist/services/collection-helpers.d.ts +14 -79
  14. package/dist/services/dataService.d.ts +1 -3
  15. package/dist/services/index.d.ts +1 -1
  16. package/dist/services/realtimeService.d.ts +0 -7
  17. package/package.json +8 -10
  18. package/src/PostgresBackendDriver.ts +13 -127
  19. package/src/PostgresBootstrapper.ts +25 -62
  20. package/src/auth/ensure-tables.ts +11 -73
  21. package/src/auth/services.ts +19 -49
  22. package/src/connection.ts +1 -61
  23. package/src/data-transformer.ts +9 -11
  24. package/src/databasePoolManager.ts +0 -2
  25. package/src/schema/auth-default-policies.ts +132 -0
  26. package/src/schema/doctor.ts +20 -45
  27. package/src/schema/generate-drizzle-schema-logic.ts +29 -24
  28. package/src/schema/generate-postgres-ddl-logic.ts +28 -76
  29. package/src/schema/introspect-db.ts +2 -19
  30. package/src/services/BranchService.ts +10 -42
  31. package/src/services/FetchService.ts +270 -65
  32. package/src/services/PersistService.ts +9 -62
  33. package/src/services/RelationService.ts +94 -153
  34. package/src/services/collection-helpers.ts +47 -164
  35. package/src/services/dataService.ts +2 -3
  36. package/src/services/index.ts +0 -1
  37. package/src/services/realtimeService.ts +19 -40
  38. package/src/utils/drizzle-conditions.ts +0 -13
  39. package/src/websocket.ts +1 -4
  40. package/dist/collections/buildRegistry.d.ts +0 -27
  41. package/dist/services/row-pipeline.d.ts +0 -63
  42. package/src/collections/buildRegistry.ts +0 -59
  43. package/src/services/row-pipeline.ts +0 -239
@@ -2,15 +2,6 @@ import { PgTable, AnyPgColumn } from "drizzle-orm/pg-core";
2
2
  import { CollectionConfig, Property } from "@rebasepro/types";
3
3
  import { PostgresCollectionRegistry } from "../collections/PostgresCollectionRegistry";
4
4
  import { getTableName } from "@rebasepro/common";
5
- import { logger } from "@rebasepro/server";
6
-
7
- // Row identity is derived on both sides of the wire — the driver parses an
8
- // incoming address into key columns, the admin derives one from a served row —
9
- // so the implementation lives in `common` and both agree by construction.
10
- export { buildCompositeId, parseIdValues, COMPOSITE_ID_SEPARATOR } from "@rebasepro/common";
11
- export type { PrimaryKeyInfo } from "@rebasepro/common";
12
- import { buildCompositeId, COMPOSITE_ID_SEPARATOR, getDeclaredPrimaryKeys } from "@rebasepro/common";
13
- import type { PrimaryKeyInfo } from "@rebasepro/common";
14
5
 
15
6
  /**
16
7
  * Shared helper functions for row operations.
@@ -58,27 +49,10 @@ export function getTableForCollection(collection: CollectionConfig, registry: Po
58
49
  return table;
59
50
  }
60
51
 
61
- /**
62
- * The key columns a collection's rows are addressed by.
63
- *
64
- * Three tiers, in order: properties marked `isId`, the primary keys of the
65
- * drizzle schema, and finally a column literally named `id`. Only the first is
66
- * visible to the browser, which is why a key known only to drizzle is reported
67
- * at boot — see {@link warnOnKeysTheAdminCannotResolve}.
68
- *
69
- * Returns `[]` when nothing resolves, rather than throwing. It used to open by
70
- * resolving the table, which throws when there is none — so the `isId` tier,
71
- * which needs no table at all, was unreachable for exactly the collections
72
- * most likely to have no table registered. Every caller that wanted "no keys"
73
- * to mean "no keys" had to spell that out in a try/catch.
74
- *
75
- * Callers that cannot proceed without a key must say so themselves, naming the
76
- * collection: an empty array here means "this collection has no address", which
77
- * is a different answer in a notification (broadcast a wildcard) than in a save
78
- * (fail).
79
- */
80
- export function getPrimaryKeys(collection: CollectionConfig, registry: PostgresCollectionRegistry): PrimaryKeyInfo[] {
81
- // Explicitly declared `isId` properties win, and need no table.
52
+ export function getPrimaryKeys(collection: CollectionConfig, registry: PostgresCollectionRegistry): { fieldName: string; type: "string" | "number"; isUUID?: boolean }[] {
53
+ const table = getTableForCollection(collection, registry);
54
+
55
+ // Fallback to explicitly defined isId properties
82
56
  if (collection.properties) {
83
57
  const idProps = Object.entries(collection.properties)
84
58
  .filter(([_, prop]) => "isId" in (prop as object) && Boolean((prop as { isId?: unknown }).isId))
@@ -93,14 +67,8 @@ export function getPrimaryKeys(collection: CollectionConfig, registry: PostgresC
93
67
  }
94
68
  }
95
69
 
96
- // The remaining tiers read the drizzle schema, so they need the table. A
97
- // collection without one — another engine's, or simply unregistered — has
98
- // nothing more to offer.
99
- const table = registry.getTable(getTableName(collection));
100
- if (!table) return [];
101
-
102
70
  // Otherwise infer from Drizzle schema
103
- const keys: PrimaryKeyInfo[] = [];
71
+ const keys: { fieldName: string; type: "string" | "number"; isUUID?: boolean }[] = [];
104
72
  for (const [key, colRaw] of Object.entries(table)) {
105
73
  const col = colRaw as AnyPgColumn;
106
74
  if (col && typeof col === "object" && "primary" in col && col.primary) {
@@ -128,141 +96,56 @@ isUUID });
128
96
  return keys;
129
97
  }
130
98
 
131
- /**
132
- * The key columns, for callers that cannot do their job without one.
133
- *
134
- * {@link getPrimaryKeys} answers "what keys, if any" and returns `[]` for a
135
- * collection with no address. Most of this driver, though, is building a WHERE
136
- * clause and has no meaning without a key — for those, an empty array is not an
137
- * answer, and indexing `[0]` into it produces `Cannot read properties of
138
- * undefined` three frames from where the real problem is. This says what is
139
- * wrong and which collection it is wrong about.
140
- */
141
- export function requirePrimaryKeys(collection: CollectionConfig, registry: PostgresCollectionRegistry): PrimaryKeyInfo[] {
142
- const keys = getPrimaryKeys(collection, registry);
143
- if (keys.length === 0) {
144
- throw new Error(
145
- `Collection '${collection.slug}' has no primary key, so its rows cannot be addressed. ` +
146
- `Mark the key property with \`isId\` in its config, or register a table whose schema declares one.`
147
- );
148
- }
149
- return keys;
150
- }
151
-
152
- /**
153
- * Collections whose key the *browser* cannot resolve, and what it will do
154
- * instead.
155
- *
156
- * The two sides resolve keys from different evidence. This driver reads, in
157
- * order: properties marked `isId`, the primary keys of the Drizzle schema, then
158
- * a column literally named `id`. The admin shares the `CollectionConfig` — it
159
- * compiles the same collection files into its bundle — but never the Drizzle
160
- * schema, so the middle tier is invisible to it.
161
- *
162
- * Nothing can normalize this at runtime: the server does not serve the admin
163
- * its collections, so a key resolved here cannot be handed over there. The
164
- * config files are the only thing both sides read, so the fix is an edit to
165
- * them, and the most this can do is say exactly which edit.
166
- *
167
- * Two shapes, and the second is the dangerous one:
168
- *
169
- * - No `isId`, no `id` property → the admin resolves no address, warns in the
170
- * console, and rows cannot be opened or linked.
171
- * - No `isId`, but an `id` property that is *not* the key → the admin addresses
172
- * rows by `id` while this driver reads the address as the real key. Nothing
173
- * errors: the addresses look right and route wrong.
174
- */
175
- export function findUnresolvableKeyCollections(
176
- collections: CollectionConfig[],
177
- registry: PostgresCollectionRegistry
178
- ): { collection: CollectionConfig; keys: PrimaryKeyInfo[]; shadowedByIdProperty: boolean }[] {
179
- const findings: { collection: CollectionConfig; keys: PrimaryKeyInfo[]; shadowedByIdProperty: boolean }[] = [];
180
-
181
- for (const collection of collections) {
182
- // Declared `isId` is the tier both sides share: if it is there, they agree.
183
- if (getDeclaredPrimaryKeys(collection).length > 0) continue;
184
-
185
- // No registered table resolves to no keys, and a collection this cannot
186
- // resolve a key for is one it has nothing to say about.
187
- const keys = getPrimaryKeys(collection, registry);
188
- if (keys.length === 0) continue;
99
+ export function parseIdValues(idValue: string | number, primaryKeys: { fieldName: string; type: "string" | "number"; isUUID?: boolean }[]): Record<string, string | number> {
100
+ const result: Record<string, string | number> = {};
189
101
 
190
- // A single key named `id` is the last tier on both sides: they agree
191
- // without anything being declared.
192
- if (keys.length === 1 && keys[0].fieldName === "id") continue;
193
-
194
- findings.push({
195
- collection,
196
- keys,
197
- shadowedByIdProperty: Boolean(collection.properties?.id)
198
- });
102
+ if (primaryKeys.length === 0) {
103
+ return result;
199
104
  }
200
105
 
201
- return findings;
202
- }
203
-
204
- /**
205
- * Report the collections from {@link findUnresolvableKeyCollections} at boot,
206
- * with the edit that fixes each one.
207
- *
208
- * Grouped by failure, not by collection: the shadowed case is a routing bug and
209
- * the silent case is a missing feature, and they deserve different urgency.
210
- */
211
- export function warnOnKeysTheAdminCannotResolve(
212
- collections: CollectionConfig[],
213
- registry: PostgresCollectionRegistry
214
- ): void {
215
- const findings = findUnresolvableKeyCollections(collections, registry);
216
- if (findings.length === 0) return;
217
-
218
- const edit = (f: { collection: CollectionConfig; keys: PrimaryKeyInfo[] }) =>
219
- `${f.collection.slug}: mark ${f.keys.map(k => `\`${k.fieldName}\``).join(" and ")} with ` +
220
- `\`isId: ${f.keys[0].isUUID ? "\"uuid\"" : f.keys[0].type === "number" ? "\"increment\"" : "true"}\``;
221
-
222
- const shadowed = findings.filter(f => f.shadowedByIdProperty);
223
- const silent = findings.filter(f => !f.shadowedByIdProperty);
106
+ if (primaryKeys.length === 1) {
107
+ const pk = primaryKeys[0];
108
+ if (pk.type === "number" && !pk.isUUID) {
109
+ const parsed = typeof idValue === "number" ? idValue : parseInt(String(idValue), 10);
110
+ if (isNaN(parsed)) {
111
+ throw new Error(`Invalid numeric ID: ${idValue}`);
112
+ }
113
+ result[pk.fieldName] = parsed;
114
+ } else {
115
+ result[pk.fieldName] = String(idValue);
116
+ }
117
+ return result;
118
+ }
224
119
 
225
- if (shadowed.length > 0) {
226
- logger.warn(
227
- `⚠️ These collections declare no \`isId\`, and their key is only in the drizzle schema — but they ` +
228
- `do have a property called \`id\`. The admin has no way to know \`id\` is not the key, so it will ` +
229
- `address rows by it while this server reads the address as the real key: the links look right and ` +
230
- `route wrong. Nothing will error.\n\n` +
231
- shadowed.map(f => ` • ${edit(f)}`).join("\n") + "\n"
232
- );
120
+ // Composite key - split by :::
121
+ const parts = String(idValue).split(":::");
122
+ if (parts.length !== primaryKeys.length) {
123
+ throw new Error(`Composite ID parts mismatch. Expected ${primaryKeys.length}, got ${parts.length} for ID: ${idValue}`);
233
124
  }
234
125
 
235
- if (silent.length > 0) {
236
- logger.warn(
237
- `⚠️ These collections declare no \`isId\`, and their key is only in the drizzle schema, which the ` +
238
- `admin never sees. It will resolve no address for their rows, so detail links, caching and ` +
239
- `relations will not work for them:\n\n` +
240
- silent.map(f => ` • ${edit(f)}`).join("\n") + "\n"
241
- );
126
+ for (let i = 0; i < primaryKeys.length; i++) {
127
+ const pk = primaryKeys[i];
128
+ const val = parts[i];
129
+ if (pk.type === "number" && !pk.isUUID) {
130
+ const parsed = parseInt(val, 10);
131
+ if (isNaN(parsed)) {
132
+ throw new Error(`Invalid numeric ID component: ${val}`);
133
+ }
134
+ result[pk.fieldName] = parsed;
135
+ } else {
136
+ result[pk.fieldName] = val;
137
+ }
242
138
  }
139
+
140
+ return result;
243
141
  }
244
142
 
245
- /**
246
- * The address of a row: derived from the collection's primary keys, because a
247
- * row does not carry one — it is exactly its columns.
248
- *
249
- * Falls back to a literal `id` column, for a row that reached us from somewhere
250
- * other than this driver. Returns `""` when there is no key and no `id` —
251
- * callers decide what that means, since "unaddressable" is a different answer
252
- * in a notification (broadcast a wildcard) than in a save (fail).
253
- */
254
- export function deriveRowAddress(
255
- row: Record<string, unknown>,
256
- collection: CollectionConfig,
257
- registry: PostgresCollectionRegistry
258
- ): string {
259
- const composite = buildCompositeId(row, getPrimaryKeys(collection, registry));
260
- // An all-empty composite is indistinguishable from a missing key, and
261
- // `buildCompositeId` returns "" for no keys at all.
262
- if (composite && composite.split(COMPOSITE_ID_SEPARATOR).some(part => part !== "")) {
263
- return composite;
143
+ export function buildCompositeId(values: Record<string, unknown>, primaryKeys: { fieldName: string; type: "string" | "number"; isUUID?: boolean }[]): string {
144
+ if (primaryKeys.length === 0) {
145
+ return "";
264
146
  }
265
- if (row.id !== undefined && row.id !== null) return String(row.id);
266
- return "";
147
+ if (primaryKeys.length === 1) {
148
+ return String(values[primaryKeys[0].fieldName] ?? "");
149
+ }
150
+ return primaryKeys.map(pk => String(values[pk.fieldName] ?? "")).join(":::");
267
151
  }
268
-
@@ -154,10 +154,9 @@ export class DataService implements DataRepository {
154
154
  collectionPath: string,
155
155
  values: Partial<M>,
156
156
  id?: string | number,
157
- databaseId?: string,
158
- options?: { upsert?: boolean }
157
+ databaseId?: string
159
158
  ): Promise<Record<string, unknown>> {
160
- return this.persistService.save<M>(collectionPath, values, id, databaseId, options);
159
+ return this.persistService.save<M>(collectionPath, values, id, databaseId);
161
160
  }
162
161
 
163
162
  /**
@@ -8,7 +8,6 @@ export {
8
8
  getCollectionByPath,
9
9
  getTableForCollection,
10
10
  getPrimaryKeys,
11
- deriveRowAddress,
12
11
  parseIdValues,
13
12
  buildCompositeId
14
13
  } from "./collection-helpers";
@@ -14,7 +14,7 @@ import { applyAuthContext } from "../security/rls-enforcement";
14
14
  import { logger } from "@rebasepro/server";
15
15
  import { sanitizeErrorForClient } from "../utils/pg-error-utils";
16
16
  import { CdcListener, type CdcChangeEvent } from "./cdc/CdcListener";
17
- import { deriveRowAddress, getPrimaryKeys, type PrimaryKeyInfo } from "./collection-helpers";
17
+ import { getPrimaryKeys, buildCompositeId } from "./collection-helpers";
18
18
 
19
19
  /** Channel name used for Postgres LISTEN/NOTIFY cross-instance realtime. */
20
20
  const PG_NOTIFY_CHANNEL = "rebase_entity_changes";
@@ -390,7 +390,7 @@ export class RealtimeService extends EventEmitter implements RealtimeProvider {
390
390
  authContext
391
391
  );
392
392
 
393
- this.sendCollectionUpdate(clientId, subscriptionId, rows, request.path);
393
+ this.sendCollectionUpdate(clientId, subscriptionId, rows);
394
394
 
395
395
  } catch (error) {
396
396
  const sanitized = sanitizeErrorForClient(error, request.path);
@@ -554,7 +554,7 @@ export class RealtimeService extends EventEmitter implements RealtimeProvider {
554
554
  // Phase 1: Send instant row-level patch (no DB query)
555
555
  // This gives immediate cross-tab feedback
556
556
  if (!row || !(row as Record<string, unknown>)?._rebase_invalidated) {
557
- this.sendCollectionPatch(subscription.clientId, subscriptionId, id, row, notifyPath);
557
+ this.sendCollectionPatch(subscription.clientId, subscriptionId, id, row);
558
558
  }
559
559
 
560
560
  // Phase 2: Schedule a deferred full refetch for correctness
@@ -609,7 +609,7 @@ export class RealtimeService extends EventEmitter implements RealtimeProvider {
609
609
  if (!this._subscriptions.has(subscriptionId)) return;
610
610
  try {
611
611
  const rows = await this.fetchCollectionWithAuth(notifyPath, subscription.collectionRequest!, subscription.authContext);
612
- this.sendCollectionUpdate(subscription.clientId, subscriptionId, rows, notifyPath);
612
+ this.sendCollectionUpdate(subscription.clientId, subscriptionId, rows);
613
613
  } catch (error) {
614
614
  const sanitized = sanitizeErrorForClient(error, notifyPath);
615
615
  this.sendError(subscription.clientId, sanitized.message, subscriptionId, sanitized.code);
@@ -910,12 +910,11 @@ roles: activeAuth.roles },
910
910
  return await this.dataService.fetchOne(notifyPath, id);
911
911
  }
912
912
 
913
- private sendCollectionUpdate(clientId: string, subscriptionId: string, rows: Record<string, unknown>[], path: string) {
913
+ private sendCollectionUpdate(clientId: string, subscriptionId: string, rows: Record<string, unknown>[]) {
914
914
  const message: CollectionUpdateMessage = {
915
915
  type: "collection_update",
916
916
  subscriptionId,
917
- rows: rows,
918
- pks: this.primaryKeysForPath(path)
917
+ rows: rows
919
918
  };
920
919
  this.sendMessage(clientId, message);
921
920
  }
@@ -932,45 +931,17 @@ roles: activeAuth.roles },
932
931
  /**
933
932
  * Send a lightweight row-level patch to a collection subscriber.
934
933
  * The client can merge this into its cached data for instant feedback.
935
- *
936
- * The key columns ride along: the patch names a row by address, and the
937
- * client has to find that row among the ones it cached — which carry
938
- * columns and no address. The SDK holds no collection config to derive one
939
- * from, so this is the only place the mapping can come from.
940
934
  */
941
- private sendCollectionPatch(
942
- clientId: string,
943
- subscriptionId: string,
944
- id: string,
945
- row: Record<string, unknown> | null,
946
- notifyPath: string
947
- ) {
935
+ private sendCollectionPatch(clientId: string, subscriptionId: string, id: string, row: Record<string, unknown> | null) {
948
936
  const message: CollectionPatchMessage = {
949
937
  type: "collection_patch",
950
938
  subscriptionId,
951
939
  id,
952
- row: row,
953
- pks: this.primaryKeysForPath(notifyPath)
940
+ row: row
954
941
  };
955
942
  this.sendMessage(clientId, message);
956
943
  }
957
944
 
958
- /** The key columns of the collection at `path`, if they can be resolved. */
959
- private primaryKeysForPath(path: string): PrimaryKeyInfo[] | undefined {
960
- try {
961
- const collection = this.registry.getCollectionByPath(path);
962
- if (!collection) return undefined;
963
- const keys = getPrimaryKeys(collection, this.registry);
964
- return keys.length > 0 ? keys : undefined;
965
- } catch {
966
- // `getCollectionByPath` throws on a path it cannot walk — and this
967
- // is called for parent paths too, which include entity paths like
968
- // `posts/1` that name no collection. Telling the subscriber nothing
969
- // is right here; letting it throw would drop the notification.
970
- return undefined;
971
- }
972
- }
973
-
974
945
  private sendError(clientId: string, error: string, subscriptionId?: string, code?: string) {
975
946
  const message = {
976
947
  type: "error" as const,
@@ -1323,9 +1294,17 @@ lastSeen: Date.now() });
1323
1294
 
1324
1295
  /** Compute the canonical (possibly composite) id string from a captured row. */
1325
1296
  private extractIdFromCdcRow(collection: CollectionConfig, row: Record<string, unknown>): string {
1326
- // Unaddressable falls back to a collection-level invalidation: single-row
1327
- // subs won't match, but collection subs still refetch.
1328
- return deriveRowAddress(row, collection, this.registry) || "*";
1297
+ try {
1298
+ const primaryKeys = getPrimaryKeys(collection, this.registry);
1299
+ const composite = buildCompositeId(row, primaryKeys);
1300
+ if (composite && composite !== ":::") return composite;
1301
+ } catch {
1302
+ /* fall through to id-field / wildcard */
1303
+ }
1304
+ // Fallbacks: a plain `id` column (common), else a collection-level
1305
+ // invalidation ("*") — single-row subs won't match but collection subs refetch.
1306
+ if (row.id !== undefined && row.id !== null) return String(row.id);
1307
+ return "*";
1329
1308
  }
1330
1309
 
1331
1310
  // ── App/CDC de-duplication ──
@@ -1126,19 +1126,6 @@ export class DrizzleConditionBuilder {
1126
1126
  throw new Error(`Vector column '${vectorSearch.property}' not found in table`);
1127
1127
  }
1128
1128
 
1129
- // The vector is interpolated as a raw SQL literal below (pgvector has no
1130
- // bind form for the `::vector` cast), so every element must be a finite
1131
- // number. The REST query parser already enforces this, but this builder
1132
- // is a shared entry point — validate here too so no future caller can
1133
- // turn an unchecked value into SQL injection.
1134
- if (
1135
- !Array.isArray(vectorSearch.vector) ||
1136
- vectorSearch.vector.length === 0 ||
1137
- !vectorSearch.vector.every((n) => typeof n === "number" && Number.isFinite(n))
1138
- ) {
1139
- throw new Error("Vector search requires a non-empty array of finite numbers");
1140
- }
1141
-
1142
1129
  const vectorLiteral = `'[${vectorSearch.vector.join(",")}]'::vector`;
1143
1130
  const distanceFn = vectorSearch.distance || "cosine";
1144
1131
 
package/src/websocket.ts CHANGED
@@ -620,12 +620,9 @@ roles: ["anon"] };
620
620
  if (error instanceof Error) {
621
621
  logger.error("Stack trace", { detail: error.stack });
622
622
  }
623
- // Unwrap the cause chain: a Drizzle failure reports itself as
624
- // "Failed query: <sql> params:", which tells the user nothing and
625
- // echoes the statement back at them. The reason is in the cause.
626
623
  const errorMessage = process.env.NODE_ENV === "production"
627
624
  ? "An unexpected error occurred"
628
- : extractErrorMessage(error);
625
+ : (error instanceof Error ? error.message : "An unexpected error occurred");
629
626
  const errorResponse = {
630
627
  type: "ERROR",
631
628
  requestId,
@@ -1,27 +0,0 @@
1
- import { Relations } from "drizzle-orm";
2
- import { PgEnum } from "drizzle-orm/pg-core";
3
- import { CollectionConfig } from "@rebasepro/types";
4
- import { PostgresCollectionRegistry } from "./PostgresCollectionRegistry";
5
- /**
6
- * Everything a registry is built from: the collections, and the drizzle schema
7
- * they are backed by. In BaaS mode all of it is introspected from the live
8
- * database; in CMS mode it comes from the config and the generated schema.
9
- */
10
- export interface RegistrySchema {
11
- collections?: CollectionConfig[];
12
- tables?: Record<string, unknown>;
13
- enums?: Record<string, PgEnum<[string, ...string[]]>>;
14
- relations?: Record<string, Relations>;
15
- }
16
- /**
17
- * Build the collection registry for a driver.
18
- *
19
- * The order matters and is the reason this is one function rather than a run of
20
- * statements in the bootstrapper. Keys are resolved from the drizzle schema, so
21
- * anything that inspects them has to run *after* the tables are registered —
22
- * and `warnOnKeysTheAdminCannotResolve` fails open if it does not, because a
23
- * collection whose table it cannot look up is one it has nothing to say about.
24
- * Warned too early, it would skip every collection and report nothing, which
25
- * reads exactly like having nothing to report.
26
- */
27
- export declare function buildCollectionRegistry(schema: RegistrySchema): PostgresCollectionRegistry;
@@ -1,63 +0,0 @@
1
- import { CollectionConfig, Relation } from "@rebasepro/types";
2
- import { PostgresCollectionRegistry } from "../collections/PostgresCollectionRegistry";
3
- /**
4
- * Turning a drizzle result into a row we serve.
5
- *
6
- * There are two shapes, and they are the same walk. Both take a row whose
7
- * relation fields hold nested objects, both unwrap junction rows to reach the
8
- * target behind them, and both leave every other column alone. They differ only
9
- * in what they put where the relation was:
10
- *
11
- * - `"ref"` — a `{ id, path, __type: "relation" }` reference carrying the
12
- * target's values. This is what the admin renders.
13
- * - `"inline"` — the target's own columns, flat. This is what REST serves.
14
- *
15
- * They used to be two functions that happened to agree, and the agreement was
16
- * not enforced by anything: the row-identity bug had to be fixed five times
17
- * across differently-shaped copies of this walk, and one of the copies was
18
- * dead code nobody had noticed. Whatever the next cross-cutting change is, it
19
- * is one edit here.
20
- */
21
- export type RelationStyle = "ref" | "inline";
22
- /**
23
- * Whether a many-relation reaches its target through a junction table.
24
- *
25
- * Also used to build the drizzle `with` config, which is why it is exported:
26
- * the query has to nest one level deeper for a junction, and the row walk has
27
- * to unwrap that same level back out.
28
- */
29
- export declare function isJunctionRelation(relation: Relation): boolean;
30
- /**
31
- * The address a relation ref points at.
32
- *
33
- * The whole key, not its first column: a composite-keyed target addressed by
34
- * `tenant_id` alone points at every row that shares it. A target whose key
35
- * cannot be resolved at all used to throw here — reading `[0]` of an empty
36
- * array — taking down the parent's fetch over a relation it may not even have
37
- * asked for. The first column is a guess, but a ref that resolves to nothing
38
- * beats no rows at all.
39
- */
40
- export declare function relationTargetAddress(targetRow: Record<string, unknown>, targetCollection: CollectionConfig, registry: PostgresCollectionRegistry): string;
41
- /**
42
- * The row the admin renders: every column, with relations as references.
43
- *
44
- * Values are normalized (dates, numbers, NaN) because the admin's view-model
45
- * expects real types. The row's own address is *not* among the columns — it is
46
- * derived by the consumer from the collection's primary keys.
47
- */
48
- export declare function toCmsRow(row: Record<string, unknown>, collection: CollectionConfig, registry: PostgresCollectionRegistry): Record<string, unknown>;
49
- /**
50
- * The row REST serves: every column under its own name, with the value Postgres
51
- * returned, and relations inlined as the target's columns.
52
- *
53
- * Values are the ones the database returned, except where that contradicts the
54
- * declared type: a `number` property is served as a number (see
55
- * {@link coerceDeclaredNumber}). Dates stay as the database returned them —
56
- * JSON has its own opinions about dates that the admin's view-model does not
57
- * share.
58
- *
59
- * Keyed by the row rather than by the relation list — a REST fetch only loads
60
- * the relations `include` asked for, so the row is the authority on which are
61
- * actually there.
62
- */
63
- export declare function toRestRow(row: Record<string, unknown>, collection: CollectionConfig, registry: PostgresCollectionRegistry): Record<string, unknown>;
@@ -1,59 +0,0 @@
1
- import { isTable, getTableName, Relations } from "drizzle-orm";
2
- import { PgEnum, PgTable } from "drizzle-orm/pg-core";
3
- import { CollectionConfig } from "@rebasepro/types";
4
- import { logger } from "@rebasepro/server";
5
- import { PostgresCollectionRegistry } from "./PostgresCollectionRegistry";
6
- import { warnOnKeysTheAdminCannotResolve } from "../services/collection-helpers";
7
-
8
- /**
9
- * Everything a registry is built from: the collections, and the drizzle schema
10
- * they are backed by. In BaaS mode all of it is introspected from the live
11
- * database; in CMS mode it comes from the config and the generated schema.
12
- */
13
- export interface RegistrySchema {
14
- collections?: CollectionConfig[];
15
- tables?: Record<string, unknown>;
16
- enums?: Record<string, PgEnum<[string, ...string[]]>>;
17
- relations?: Record<string, Relations>;
18
- }
19
-
20
- /**
21
- * Build the collection registry for a driver.
22
- *
23
- * The order matters and is the reason this is one function rather than a run of
24
- * statements in the bootstrapper. Keys are resolved from the drizzle schema, so
25
- * anything that inspects them has to run *after* the tables are registered —
26
- * and `warnOnKeysTheAdminCannotResolve` fails open if it does not, because a
27
- * collection whose table it cannot look up is one it has nothing to say about.
28
- * Warned too early, it would skip every collection and report nothing, which
29
- * reads exactly like having nothing to report.
30
- */
31
- export function buildCollectionRegistry(schema: RegistrySchema): PostgresCollectionRegistry {
32
- const registry = new PostgresCollectionRegistry();
33
-
34
- if (schema.collections) {
35
- registry.registerMultiple(schema.collections);
36
- logger.info(
37
- `📋 [PostgresRegistry] Registered ${registry.getCollections().length} collections: ` +
38
- `[${registry.getCollections().map(c => c.slug).join(", ")}]`
39
- );
40
- }
41
-
42
- if (schema.tables) {
43
- Object.values(schema.tables).forEach((table) => {
44
- if (isTable(table)) {
45
- registry.registerTable(table as PgTable, getTableName(table));
46
- }
47
- });
48
- }
49
-
50
- if (schema.enums) registry.registerEnums(schema.enums);
51
- if (schema.relations) registry.registerRelations(schema.relations);
52
-
53
- // Now that the keys resolve: say which of them the admin cannot see. It
54
- // compiles the same collection files into its bundle but never the drizzle
55
- // schema, and nothing serves it one, so only an edit to the config fixes it.
56
- warnOnKeysTheAdminCannotResolve(registry.getCollections(), registry);
57
-
58
- return registry;
59
- }