@rebasepro/server-postgres 0.9.1-canary.58368ce → 0.9.1-canary.73476f2

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.
@@ -162,12 +162,11 @@ export class RelationService {
162
162
  const rows: RelatedRow<M>[] = [];
163
163
  for (const row of results as Array<Record<string, unknown>>) {
164
164
  const targetRow = (row[targetTableName] as Record<string, unknown>) || row;
165
+ const id = targetRow[idInfo[0].fieldName as string];
165
166
  const parsedValues = await parseDataFromServer(targetRow, targetCollection, this.db, this.registry);
166
167
 
167
168
  rows.push({
168
- // The whole key: the first column of a composite one names
169
- // every row that shares it.
170
- id: buildCompositeId(targetRow, idInfo),
169
+ id: id?.toString() || "",
171
170
  path: targetCollection.slug,
172
171
  values: parsedValues as M
173
172
  });
@@ -229,10 +228,11 @@ export class RelationService {
229
228
  const rows: RelatedRow<M>[] = [];
230
229
  for (const row of results) {
231
230
  const targetRow = row[getTableName(targetCollection)] || row;
231
+ const id = targetRow[idInfo[0].fieldName];
232
232
  const parsedValues = await parseDataFromServer(targetRow, targetCollection, this.db, this.registry);
233
233
 
234
234
  rows.push({
235
- id: buildCompositeId(targetRow as Record<string, unknown>, idInfo),
235
+ id: id?.toString() || "",
236
236
  path: targetCollection.slug,
237
237
  values: parsedValues as M
238
238
  });
@@ -371,7 +371,7 @@ export class RelationService {
371
371
  const parsedValues = await parseDataFromServer(targetRow, targetCollection);
372
372
 
373
373
  resultMap.set(String(parentId), {
374
- id: buildCompositeId(targetRow, targetPks),
374
+ id: String(targetRow[targetIdInfo.fieldName]),
375
375
  path: targetCollection.slug,
376
376
  values: parsedValues as Record<string, unknown>
377
377
  });
@@ -438,7 +438,7 @@ export class RelationService {
438
438
  if (targetRow) {
439
439
  const parsedValues = await parseDataFromServer(targetRow, targetCollection);
440
440
  resultMap.set(parentIdStr, {
441
- id: buildCompositeId(targetRow, targetPks),
441
+ id: String(targetRow[targetIdInfo.fieldName]),
442
442
  path: targetCollection.slug,
443
443
  values: parsedValues as Record<string, unknown>
444
444
  });
@@ -490,7 +490,7 @@ export class RelationService {
490
490
  if (parentId !== undefined && parentIdSet.has(String(parentId))) {
491
491
  const parsedValues = await parseDataFromServer(targetRow, targetCollection);
492
492
  resultMap.set(String(parentId), {
493
- id: buildCompositeId(targetRow, targetPks),
493
+ id: String(targetRow[targetIdInfo.fieldName]),
494
494
  path: targetCollection.slug,
495
495
  values: parsedValues as Record<string, unknown>
496
496
  });
@@ -565,7 +565,7 @@ export class RelationService {
565
565
 
566
566
  const arr = resultMap.get(parentId) || [];
567
567
  arr.push({
568
- id: buildCompositeId(targetRow, targetPks),
568
+ id: String(targetRow[targetIdInfo.fieldName]),
569
569
  path: targetCollection.slug,
570
570
  values: parsedValues as Record<string, unknown>
571
571
  });
@@ -616,7 +616,7 @@ export class RelationService {
616
616
 
617
617
  const arr = resultMap.get(parentId) || [];
618
618
  arr.push({
619
- id: buildCompositeId(targetData, targetPks),
619
+ id: String(targetData[targetIdInfo.fieldName]),
620
620
  path: targetCollection.slug,
621
621
  values: parsedValues as Record<string, unknown>
622
622
  });
@@ -672,7 +672,7 @@ export class RelationService {
672
672
  const key = String(parentId);
673
673
  const arr = resultMap.get(key) || [];
674
674
  arr.push({
675
- id: buildCompositeId(targetRow, targetPks),
675
+ id: String(targetRow[targetIdInfo.fieldName]),
676
676
  path: targetCollection.slug,
677
677
  values: parsedValues as Record<string, unknown>
678
678
  });
@@ -865,6 +865,7 @@ export class RelationService {
865
865
  relationKey: string;
866
866
  relation: Relation;
867
867
  newValue: unknown;
868
+ currentId?: string | number;
868
869
  }>
869
870
  ) {
870
871
  for (const update of inverseRelationUpdates) {
@@ -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,7 +49,7 @@ export function getTableForCollection(collection: CollectionConfig, registry: Po
58
49
  return table;
59
50
  }
60
51
 
61
- export function getPrimaryKeys(collection: CollectionConfig, registry: PostgresCollectionRegistry): PrimaryKeyInfo[] {
52
+ export function getPrimaryKeys(collection: CollectionConfig, registry: PostgresCollectionRegistry): { fieldName: string; type: "string" | "number"; isUUID?: boolean }[] {
62
53
  const table = getTableForCollection(collection, registry);
63
54
 
64
55
  // Fallback to explicitly defined isId properties
@@ -77,7 +68,7 @@ export function getPrimaryKeys(collection: CollectionConfig, registry: PostgresC
77
68
  }
78
69
 
79
70
  // Otherwise infer from Drizzle schema
80
- const keys: PrimaryKeyInfo[] = [];
71
+ const keys: { fieldName: string; type: "string" | "number"; isUUID?: boolean }[] = [];
81
72
  for (const [key, colRaw] of Object.entries(table)) {
82
73
  const col = colRaw as AnyPgColumn;
83
74
  if (col && typeof col === "object" && "primary" in col && col.primary) {
@@ -105,132 +96,56 @@ isUUID });
105
96
  return keys;
106
97
  }
107
98
 
108
- /**
109
- * Collections whose key the *browser* cannot resolve, and what it will do
110
- * instead.
111
- *
112
- * The two sides resolve keys from different evidence. This driver reads, in
113
- * order: properties marked `isId`, the primary keys of the Drizzle schema, then
114
- * a column literally named `id`. The admin shares the `CollectionConfig` — it
115
- * compiles the same collection files into its bundle — but never the Drizzle
116
- * schema, so the middle tier is invisible to it.
117
- *
118
- * Nothing can normalize this at runtime: the server does not serve the admin
119
- * its collections, so a key resolved here cannot be handed over there. The
120
- * config files are the only thing both sides read, so the fix is an edit to
121
- * them, and the most this can do is say exactly which edit.
122
- *
123
- * Two shapes, and the second is the dangerous one:
124
- *
125
- * - No `isId`, no `id` property → the admin resolves no address, warns in the
126
- * console, and rows cannot be opened or linked.
127
- * - No `isId`, but an `id` property that is *not* the key → the admin addresses
128
- * rows by `id` while this driver reads the address as the real key. Nothing
129
- * errors: the addresses look right and route wrong.
130
- */
131
- export function findUnresolvableKeyCollections(
132
- collections: CollectionConfig[],
133
- registry: PostgresCollectionRegistry
134
- ): { collection: CollectionConfig; keys: PrimaryKeyInfo[]; shadowedByIdProperty: boolean }[] {
135
- const findings: { collection: CollectionConfig; keys: PrimaryKeyInfo[]; shadowedByIdProperty: boolean }[] = [];
136
-
137
- for (const collection of collections) {
138
- // Declared `isId` is the tier both sides share: if it is there, they agree.
139
- if (getDeclaredPrimaryKeys(collection).length > 0) continue;
140
-
141
- let keys: PrimaryKeyInfo[];
142
- try {
143
- keys = getPrimaryKeys(collection, registry);
144
- } catch {
145
- // No registered table — nothing resolved here either, so there is no
146
- // disagreement to report.
147
- continue;
148
- }
149
- if (keys.length === 0) continue;
150
-
151
- // A single key named `id` is the last tier on both sides: they agree
152
- // without anything being declared.
153
- if (keys.length === 1 && keys[0].fieldName === "id") 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> = {};
154
101
 
155
- findings.push({
156
- collection,
157
- keys,
158
- shadowedByIdProperty: Boolean(collection.properties?.id)
159
- });
102
+ if (primaryKeys.length === 0) {
103
+ return result;
160
104
  }
161
105
 
162
- return findings;
163
- }
164
-
165
- /**
166
- * Report the collections from {@link findUnresolvableKeyCollections} at boot,
167
- * with the edit that fixes each one.
168
- *
169
- * Grouped by failure, not by collection: the shadowed case is a routing bug and
170
- * the silent case is a missing feature, and they deserve different urgency.
171
- */
172
- export function warnOnKeysTheAdminCannotResolve(
173
- collections: CollectionConfig[],
174
- registry: PostgresCollectionRegistry
175
- ): void {
176
- const findings = findUnresolvableKeyCollections(collections, registry);
177
- if (findings.length === 0) return;
178
-
179
- const edit = (f: { collection: CollectionConfig; keys: PrimaryKeyInfo[] }) =>
180
- `${f.collection.slug}: mark ${f.keys.map(k => `\`${k.fieldName}\``).join(" and ")} with ` +
181
- `\`isId: ${f.keys[0].isUUID ? "\"uuid\"" : f.keys[0].type === "number" ? "\"increment\"" : "true"}\``;
182
-
183
- const shadowed = findings.filter(f => f.shadowedByIdProperty);
184
- const silent = findings.filter(f => !f.shadowedByIdProperty);
185
-
186
- if (shadowed.length > 0) {
187
- logger.warn(
188
- `⚠️ These collections declare no \`isId\`, and their key is only in the drizzle schema — but they ` +
189
- `do have a property called \`id\`. The admin has no way to know \`id\` is not the key, so it will ` +
190
- `address rows by it while this server reads the address as the real key: the links look right and ` +
191
- `route wrong. Nothing will error.\n\n` +
192
- shadowed.map(f => ` • ${edit(f)}`).join("\n") + "\n"
193
- );
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;
194
118
  }
195
119
 
196
- if (silent.length > 0) {
197
- logger.warn(
198
- `⚠️ These collections declare no \`isId\`, and their key is only in the drizzle schema, which the ` +
199
- `admin never sees. It will resolve no address for their rows, so detail links, caching and ` +
200
- `relations will not work for them:\n\n` +
201
- silent.map(f => ` • ${edit(f)}`).join("\n") + "\n"
202
- );
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}`);
203
124
  }
204
- }
205
125
 
206
- /**
207
- * The address of a row: derived from the collection's primary keys, because a
208
- * row does not carry one — it is exactly its columns.
209
- *
210
- * Falls back to a literal `id` column, which covers the two cases where the
211
- * keys cannot be resolved: a row that reached us from somewhere other than the
212
- * postgres driver, and a collection whose table the registry cannot look up
213
- * (`getPrimaryKeys` throws for an unregistered table rather than returning
214
- * nothing). Returns `""` when there is no key and no `id` — callers decide what
215
- * that means, since "unaddressable" is a different answer in a notification
216
- * (broadcast a wildcard) than in a save (fail).
217
- */
218
- export function deriveRowAddress(
219
- row: Record<string, unknown>,
220
- collection: CollectionConfig,
221
- registry: PostgresCollectionRegistry
222
- ): string {
223
- try {
224
- const composite = buildCompositeId(row, getPrimaryKeys(collection, registry));
225
- // An all-empty composite is indistinguishable from a missing key, and
226
- // `buildCompositeId` returns "" for no keys at all.
227
- if (composite && composite.split(COMPOSITE_ID_SEPARATOR).some(part => part !== "")) {
228
- return composite;
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;
229
137
  }
230
- } catch {
231
- /* unresolvable table or keys — fall through to the literal column */
232
138
  }
233
- if (row.id !== undefined && row.id !== null) return String(row.id);
234
- return "";
139
+
140
+ return result;
235
141
  }
236
142
 
143
+ export function buildCompositeId(values: Record<string, unknown>, primaryKeys: { fieldName: string; type: "string" | "number"; isUUID?: boolean }[]): string {
144
+ if (primaryKeys.length === 0) {
145
+ return "";
146
+ }
147
+ if (primaryKeys.length === 1) {
148
+ return String(values[primaryKeys[0].fieldName] ?? "");
149
+ }
150
+ return primaryKeys.map(pk => String(values[pk.fieldName] ?? "")).join(":::");
151
+ }
@@ -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,43 +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
- // Unregistered table, or a relation path with no collection of its
967
- // own: the client keeps its pre-existing `id` behaviour.
968
- return undefined;
969
- }
970
- }
971
-
972
945
  private sendError(clientId: string, error: string, subscriptionId?: string, code?: string) {
973
946
  const message = {
974
947
  type: "error" as const,
@@ -1321,9 +1294,17 @@ lastSeen: Date.now() });
1321
1294
 
1322
1295
  /** Compute the canonical (possibly composite) id string from a captured row. */
1323
1296
  private extractIdFromCdcRow(collection: CollectionConfig, row: Record<string, unknown>): string {
1324
- // Unaddressable falls back to a collection-level invalidation: single-row
1325
- // subs won't match, but collection subs still refetch.
1326
- 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 "*";
1327
1308
  }
1328
1309
 
1329
1310
  // ── App/CDC de-duplication ──
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,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
- }