@rebasepro/server-postgres 0.9.1-canary.73476f2 → 0.9.1-canary.74adfbe

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 (62) hide show
  1. package/README.md +21 -0
  2. package/dist/PostgresBackendDriver.d.ts +43 -2
  3. package/dist/PostgresBootstrapper.d.ts +17 -1
  4. package/dist/auth/services.d.ts +68 -52
  5. package/dist/collections/buildRegistry.d.ts +27 -0
  6. package/dist/connection.d.ts +21 -0
  7. package/dist/data-transformer.d.ts +9 -2
  8. package/dist/index.es.js +2185 -775
  9. package/dist/index.es.js.map +1 -1
  10. package/dist/schema/auth-bootstrap-sql.d.ts +1 -1
  11. package/dist/schema/auth-schema.d.ts +24 -24
  12. package/dist/schema/doctor.d.ts +1 -1
  13. package/dist/schema/introspect-db-logic.d.ts +0 -5
  14. package/dist/schema/introspect-db-naming.d.ts +10 -0
  15. package/dist/security/policy-drift.d.ts +30 -0
  16. package/dist/security/rls-enforcement.d.ts +2 -2
  17. package/dist/services/FetchService.d.ts +4 -24
  18. package/dist/services/PersistService.d.ts +27 -1
  19. package/dist/services/RelationService.d.ts +34 -1
  20. package/dist/services/channel-history.d.ts +118 -0
  21. package/dist/services/collection-helpers.d.ts +79 -14
  22. package/dist/services/dataService.d.ts +3 -1
  23. package/dist/services/index.d.ts +1 -1
  24. package/dist/services/realtimeService.d.ts +76 -2
  25. package/dist/services/row-pipeline.d.ts +63 -0
  26. package/package.json +13 -34
  27. package/src/PostgresBackendDriver.ts +183 -18
  28. package/src/PostgresBootstrapper.ts +80 -26
  29. package/src/auth/ensure-tables.ts +170 -28
  30. package/src/auth/services.ts +181 -150
  31. package/src/cli.ts +60 -0
  32. package/src/collections/buildRegistry.ts +59 -0
  33. package/src/connection.ts +61 -1
  34. package/src/data-transformer.ts +11 -9
  35. package/src/databasePoolManager.ts +2 -0
  36. package/src/schema/auth-bootstrap-sql.ts +7 -1
  37. package/src/schema/auth-schema.ts +13 -13
  38. package/src/schema/doctor.ts +45 -20
  39. package/src/schema/generate-drizzle-schema-logic.ts +24 -29
  40. package/src/schema/generate-postgres-ddl-logic.ts +76 -28
  41. package/src/schema/introspect-db-inference.ts +1 -1
  42. package/src/schema/introspect-db-logic.ts +1 -10
  43. package/src/schema/introspect-db-naming.ts +15 -0
  44. package/src/schema/introspect-db.ts +19 -2
  45. package/src/schema/introspect-runtime.ts +1 -1
  46. package/src/security/policy-drift.test.ts +106 -1
  47. package/src/security/policy-drift.ts +56 -0
  48. package/src/security/rls-enforcement.ts +11 -5
  49. package/src/services/BranchService.ts +42 -10
  50. package/src/services/FetchService.ts +65 -270
  51. package/src/services/PersistService.ts +130 -14
  52. package/src/services/RelationService.ts +153 -94
  53. package/src/services/channel-history.ts +343 -0
  54. package/src/services/collection-helpers.ts +164 -47
  55. package/src/services/dataService.ts +3 -2
  56. package/src/services/index.ts +1 -0
  57. package/src/services/realtimeService.ts +238 -29
  58. package/src/services/row-pipeline.ts +239 -0
  59. package/src/utils/drizzle-conditions.ts +13 -0
  60. package/src/websocket.ts +34 -12
  61. package/dist/schema/auth-default-policies.d.ts +0 -10
  62. package/src/schema/auth-default-policies.ts +0 -132
@@ -1,4 +1,4 @@
1
- import { and, eq, inArray, sql, SQL } from "drizzle-orm";
1
+ import { and, eq, inArray, or, sql, SQL } from "drizzle-orm";
2
2
  import { AnyPgColumn, PgTable } from "drizzle-orm/pg-core";
3
3
  import { DrizzleClient } from "../interfaces";
4
4
  import { CollectionConfig, FilterValues, Relation } from "@rebasepro/types";
@@ -7,9 +7,10 @@ import { DrizzleConditionBuilder } from "../utils/drizzle-conditions";
7
7
  import {
8
8
  getCollectionByPath,
9
9
  getTableForCollection,
10
- getPrimaryKeys,
10
+ requirePrimaryKeys,
11
11
  parseIdValues,
12
- buildCompositeId
12
+ buildCompositeId,
13
+ type PrimaryKeyInfo
13
14
  } from "./collection-helpers";
14
15
  import { parseDataFromServer } from "../data-transformer";
15
16
  import { PostgresCollectionRegistry } from "../collections/PostgresCollectionRegistry";
@@ -56,6 +57,94 @@ export interface RelatedRow<M extends Record<string, unknown> = Record<string, u
56
57
  export class RelationService {
57
58
  constructor(private db: DrizzleClient, private registry: PostgresCollectionRegistry) { }
58
59
 
60
+ /**
61
+ * One target row, as the {@link RelatedRow} everything here returns.
62
+ *
63
+ * Eight sites built this by hand, which is how the address came to be the
64
+ * target's first key column in all eight — one edit, eight places to miss.
65
+ *
66
+ * `resolveNested` is the one thing they did not agree on, and the
67
+ * disagreement was invisible: the single-parent fetches pass `db` and
68
+ * `registry` to `parseDataFromServer`, so the target's *own* relations get
69
+ * resolved too, while the batch paths deliberately do not — a query per
70
+ * target row is the N+1 the batching exists to avoid. Naming the parameter
71
+ * makes that a decision rather than a difference between two call sites
72
+ * nobody was comparing.
73
+ */
74
+ private async toRelatedRow<M extends Record<string, unknown>>(
75
+ targetRow: Record<string, unknown>,
76
+ targetCollection: CollectionConfig,
77
+ targetPks: PrimaryKeyInfo[],
78
+ options?: { resolveNested?: boolean }
79
+ ): Promise<RelatedRow<M>> {
80
+ const values = options?.resolveNested
81
+ ? await parseDataFromServer(targetRow, targetCollection, this.db, this.registry)
82
+ : await parseDataFromServer(targetRow, targetCollection);
83
+
84
+ return {
85
+ // The whole key: a composite target addressed by its first column
86
+ // names every row that shares it.
87
+ id: buildCompositeId(targetRow, targetPks),
88
+ path: targetCollection.slug,
89
+ values: values as M
90
+ };
91
+ }
92
+
93
+ /**
94
+ * A WHERE matching any of `parentIds`, by the whole key.
95
+ *
96
+ * A single key is an `IN (…)`. A composite one cannot be: matching
97
+ * `tenant_id IN (1, 1)` collects every row of tenant 1, so two parents that
98
+ * share their first column each receive the other's relations. It becomes
99
+ * an OR of ANDs — one exact address per parent — which Postgres indexes the
100
+ * same way it would a multi-column key lookup.
101
+ */
102
+ private parentKeyCondition(
103
+ parentTable: PgTable,
104
+ parentPks: PrimaryKeyInfo[],
105
+ parentIds: (string | number)[]
106
+ ): SQL {
107
+ const columnFor = (fieldName: string) => {
108
+ const col = parentTable[fieldName as keyof typeof parentTable] as AnyPgColumn;
109
+ if (!col) throw new Error(`Key column '${fieldName}' not found in parent table`);
110
+ return col;
111
+ };
112
+
113
+ if (parentPks.length === 1) {
114
+ const values = parentIds.map(id => parseIdValues(id, parentPks)[parentPks[0].fieldName]);
115
+ return inArray(columnFor(parentPks[0].fieldName), values);
116
+ }
117
+
118
+ const perParent = parentIds.map(id => {
119
+ const values = parseIdValues(id, parentPks);
120
+ return and(...parentPks.map(pk => eq(columnFor(pk.fieldName), values[pk.fieldName])));
121
+ });
122
+ return or(...perParent) as SQL;
123
+ }
124
+
125
+ /**
126
+ * Reject a relation that cannot express a composite-keyed parent.
127
+ *
128
+ * `localKey` and `foreignKeyOnTarget` are single column names: one column
129
+ * cannot reference a two-column key, so such a relation has no correct
130
+ * reading. Left alone it would silently match on the first key column and
131
+ * hand a tenant's rows to its neighbour — say so instead.
132
+ */
133
+ private assertSingleKeyAddressable(
134
+ parentCollection: CollectionConfig,
135
+ parentPks: PrimaryKeyInfo[],
136
+ via: string
137
+ ): void {
138
+ if (parentPks.length > 1) {
139
+ throw new Error(
140
+ `Relation on '${parentCollection.slug}' uses '${via}', a single foreign-key column, but ` +
141
+ `'${parentCollection.slug}' is keyed on ${parentPks.map(k => `'${k.fieldName}'`).join(" + ")}. ` +
142
+ `One column cannot reference a composite key — express this relation with \`joinPath\`, whose ` +
143
+ `\`on.from\`/\`on.to\` take every key column.`
144
+ );
145
+ }
146
+ }
147
+
59
148
  /**
60
149
  * Fetch rows related to a parent row through a specific relation
61
150
  */
@@ -104,10 +193,10 @@ export class RelationService {
104
193
  ): Promise<RelatedRow<M>[]> {
105
194
  const targetCollection = relation.target();
106
195
  const targetTable = getTableForCollection(targetCollection, this.registry);
107
- const idInfo = getPrimaryKeys(targetCollection, this.registry);
196
+ const idInfo = requirePrimaryKeys(targetCollection, this.registry);
108
197
  const idField = targetTable[idInfo[0].fieldName as keyof typeof targetTable] as AnyPgColumn;
109
198
 
110
- const parentPks = getPrimaryKeys(parentCollection, this.registry);
199
+ const parentPks = requirePrimaryKeys(parentCollection, this.registry);
111
200
  const parentIdInfo = parentPks[0];
112
201
  const parsedParentIdObj = parseIdValues(parentId, parentPks);
113
202
  const parsedParentId = parsedParentIdObj[parentIdInfo.fieldName];
@@ -148,7 +237,7 @@ export class RelationService {
148
237
  }
149
238
 
150
239
  // Add where condition for the parent row
151
- const parentIdField = parentTable[getPrimaryKeys(parentCollection, this.registry)[0].fieldName as keyof typeof parentTable] as AnyPgColumn;
240
+ const parentIdField = parentTable[requirePrimaryKeys(parentCollection, this.registry)[0].fieldName as keyof typeof parentTable] as AnyPgColumn;
152
241
  query = query.where(eq(parentIdField, parsedParentId));
153
242
 
154
243
  if (options.limit) {
@@ -162,14 +251,7 @@ export class RelationService {
162
251
  const rows: RelatedRow<M>[] = [];
163
252
  for (const row of results as Array<Record<string, unknown>>) {
164
253
  const targetRow = (row[targetTableName] as Record<string, unknown>) || row;
165
- const id = targetRow[idInfo[0].fieldName as string];
166
- const parsedValues = await parseDataFromServer(targetRow, targetCollection, this.db, this.registry);
167
-
168
- rows.push({
169
- id: id?.toString() || "",
170
- path: targetCollection.slug,
171
- values: parsedValues as M
172
- });
254
+ rows.push(await this.toRelatedRow<M>(targetRow, targetCollection, idInfo, { resolveNested: true }));
173
255
  }
174
256
 
175
257
  return rows;
@@ -228,14 +310,7 @@ export class RelationService {
228
310
  const rows: RelatedRow<M>[] = [];
229
311
  for (const row of results) {
230
312
  const targetRow = row[getTableName(targetCollection)] || row;
231
- const id = targetRow[idInfo[0].fieldName];
232
- const parsedValues = await parseDataFromServer(targetRow, targetCollection, this.db, this.registry);
233
-
234
- rows.push({
235
- id: id?.toString() || "",
236
- path: targetCollection.slug,
237
- values: parsedValues as M
238
- });
313
+ rows.push(await this.toRelatedRow<M>(targetRow as Record<string, unknown>, targetCollection, idInfo, { resolveNested: true }));
239
314
  }
240
315
 
241
316
  return rows;
@@ -260,11 +335,11 @@ export class RelationService {
260
335
 
261
336
  const targetCollection = relation.target();
262
337
  const targetTable = getTableForCollection(targetCollection, this.registry);
263
- const targetPks = getPrimaryKeys(targetCollection, this.registry);
338
+ const targetPks = requirePrimaryKeys(targetCollection, this.registry);
264
339
  const targetIdInfo = targetPks[0];
265
340
  const targetIdField = targetTable[targetIdInfo.fieldName as keyof typeof targetTable] as AnyPgColumn;
266
341
 
267
- const parentPks = getPrimaryKeys(parentCollection, this.registry);
342
+ const parentPks = requirePrimaryKeys(parentCollection, this.registry);
268
343
  const parentIdInfo = parentPks[0];
269
344
  const parsedParentIdObj = parseIdValues(parentId, parentPks);
270
345
  const parsedParentId = parsedParentIdObj[parentIdInfo.fieldName];
@@ -309,11 +384,11 @@ export class RelationService {
309
384
  const parentCollection = getCollectionByPath(parentCollectionPath, this.registry);
310
385
  const targetCollection = relation.target();
311
386
  const targetTable = getTableForCollection(targetCollection, this.registry);
312
- const targetPks = getPrimaryKeys(targetCollection, this.registry);
387
+ const targetPks = requirePrimaryKeys(targetCollection, this.registry);
313
388
  const targetIdInfo = targetPks[0];
314
389
  const targetIdField = targetTable[targetIdInfo.fieldName as keyof typeof targetTable] as AnyPgColumn;
315
390
 
316
- const parentPks = getPrimaryKeys(parentCollection, this.registry);
391
+ const parentPks = requirePrimaryKeys(parentCollection, this.registry);
317
392
  const parentIdInfo = parentPks[0];
318
393
  const parentTable = this.registry.getTable(getTableName(parentCollection));
319
394
  if (!parentTable) throw new Error("Parent table not found");
@@ -354,27 +429,23 @@ export class RelationService {
354
429
  currentTable = joinTable;
355
430
  }
356
431
 
357
- // Add where condition for ALL parent rows at once
358
- const parentIdField = parentTable[getPrimaryKeys(parentCollection, this.registry)[0].fieldName as keyof typeof parentTable] as AnyPgColumn;
359
- query = query.where(inArray(parentIdField, parsedParentIds));
432
+ // Match every parent at once, each by its whole key.
433
+ query = query.where(this.parentKeyCondition(parentTable, parentPks, parentIds));
360
434
 
361
435
  const results = await query;
362
436
  const targetTableName = relation.joinPath[relation.joinPath.length - 1].table;
363
437
  const resultMap = new Map<string, RelatedRow<Record<string, unknown>>>();
364
438
 
365
- // Group results by parent ID
439
+ // Group by the parent's address — the same token the caller looks
440
+ // results up by, derived the same way on both sides.
366
441
  for (const row of results as Array<Record<string, unknown>>) {
367
442
  const parentRow = (row[getTableName(parentCollection)] || row) as Record<string, unknown>;
368
443
  const targetRow = (row[targetTableName] || row) as Record<string, unknown>;
369
- const parentId = parentRow[parentIdInfo.fieldName] as string | number;
370
-
371
- const parsedValues = await parseDataFromServer(targetRow, targetCollection);
372
444
 
373
- resultMap.set(String(parentId), {
374
- id: String(targetRow[targetIdInfo.fieldName]),
375
- path: targetCollection.slug,
376
- values: parsedValues as Record<string, unknown>
377
- });
445
+ resultMap.set(
446
+ buildCompositeId(parentRow, parentPks),
447
+ await this.toRelatedRow(targetRow, targetCollection, targetPks)
448
+ );
378
449
  }
379
450
 
380
451
  return resultMap;
@@ -387,6 +458,7 @@ export class RelationService {
387
458
  // 2. Query the target table with unique FK values
388
459
  // 3. Map results back to parent rows via their FK values
389
460
  if (relation.direction === "owning" && relation.localKey) {
461
+ this.assertSingleKeyAddressable(parentCollection, parentPks, relation.localKey);
390
462
  const localKeyCol = parentTable[relation.localKey as keyof typeof parentTable] as AnyPgColumn;
391
463
  if (!localKeyCol) {
392
464
  throw new Error(`Local key column '${relation.localKey}' not found in parent table`);
@@ -436,19 +508,21 @@ export class RelationService {
436
508
  for (const [parentIdStr, fkValue] of parentToFk) {
437
509
  const targetRow = targetById.get(String(fkValue));
438
510
  if (targetRow) {
439
- const parsedValues = await parseDataFromServer(targetRow, targetCollection);
440
- resultMap.set(parentIdStr, {
441
- id: String(targetRow[targetIdInfo.fieldName]),
442
- path: targetCollection.slug,
443
- values: parsedValues as Record<string, unknown>
444
- });
511
+ resultMap.set(parentIdStr, await this.toRelatedRow(targetRow, targetCollection, targetPks));
445
512
  }
446
513
  }
447
514
 
448
515
  return resultMap;
449
516
  }
450
517
 
451
- // Handle inverse relation types with batching
518
+ // Handle inverse relation types with batching. The parent is named by a
519
+ // single FK column on the target, so a composite-keyed parent has no
520
+ // correct reading here either.
521
+ this.assertSingleKeyAddressable(
522
+ parentCollection,
523
+ parentPks,
524
+ relation.foreignKeyOnTarget ?? `${relation.inverseRelationName}_id`
525
+ );
452
526
  let query = this.db.select().from(targetTable).$dynamic();
453
527
 
454
528
  // Build the relation query with ALL parent IDs
@@ -488,12 +562,7 @@ export class RelationService {
488
562
  }
489
563
 
490
564
  if (parentId !== undefined && parentIdSet.has(String(parentId))) {
491
- const parsedValues = await parseDataFromServer(targetRow, targetCollection);
492
- resultMap.set(String(parentId), {
493
- id: String(targetRow[targetIdInfo.fieldName]),
494
- path: targetCollection.slug,
495
- values: parsedValues as Record<string, unknown>
496
- });
565
+ resultMap.set(String(parentId), await this.toRelatedRow(targetRow, targetCollection, targetPks));
497
566
  }
498
567
  }
499
568
 
@@ -516,11 +585,11 @@ export class RelationService {
516
585
  const parentCollection = getCollectionByPath(parentCollectionPath, this.registry);
517
586
  const targetCollection = relation.target();
518
587
  const targetTable = getTableForCollection(targetCollection, this.registry);
519
- const targetPks = getPrimaryKeys(targetCollection, this.registry);
588
+ const targetPks = requirePrimaryKeys(targetCollection, this.registry);
520
589
  const targetIdInfo = targetPks[0];
521
590
  const targetIdField = targetTable[targetIdInfo.fieldName as keyof typeof targetTable] as AnyPgColumn;
522
591
 
523
- const parentPks = getPrimaryKeys(parentCollection, this.registry);
592
+ const parentPks = requirePrimaryKeys(parentCollection, this.registry);
524
593
  const parentIdInfo = parentPks[0];
525
594
  const parentTable = this.registry.getTable(getTableName(parentCollection));
526
595
  if (!parentTable) throw new Error("Parent table not found");
@@ -550,8 +619,7 @@ export class RelationService {
550
619
  currentTable = joinTable;
551
620
  }
552
621
 
553
- const parentIdField = parentTable[getPrimaryKeys(parentCollection, this.registry)[0].fieldName as keyof typeof parentTable] as AnyPgColumn;
554
- query = query.where(inArray(parentIdField, parsedParentIds));
622
+ query = query.where(this.parentKeyCondition(parentTable, parentPks, parentIds));
555
623
 
556
624
  const results = await query;
557
625
  const targetTableName = relation.joinPath[relation.joinPath.length - 1].table;
@@ -560,15 +628,9 @@ export class RelationService {
560
628
  for (const row of results as Array<Record<string, unknown>>) {
561
629
  const parentRow = (row[getTableName(parentCollection)] || row) as Record<string, unknown>;
562
630
  const targetRow = (row[targetTableName] || row) as Record<string, unknown>;
563
- const parentId = String(parentRow[parentIdInfo.fieldName]);
564
- const parsedValues = await parseDataFromServer(targetRow, targetCollection);
565
-
631
+ const parentId = buildCompositeId(parentRow, parentPks);
566
632
  const arr = resultMap.get(parentId) || [];
567
- arr.push({
568
- id: String(targetRow[targetIdInfo.fieldName]),
569
- path: targetCollection.slug,
570
- values: parsedValues as Record<string, unknown>
571
- });
633
+ arr.push(await this.toRelatedRow(targetRow, targetCollection, targetPks));
572
634
  resultMap.set(parentId, arr);
573
635
  }
574
636
 
@@ -579,6 +641,9 @@ export class RelationService {
579
641
  // This is the standard path for posts→tags style relations where
580
642
  // sanitizeRelation populated the `through` config.
581
643
  if (relation.through && relation.cardinality === "many" && relation.direction === "owning") {
644
+ // The junction names its parent with one column, so the same
645
+ // single-key limit applies as for a direct foreign key.
646
+ this.assertSingleKeyAddressable(parentCollection, parentPks, `${relation.through.table}.${relation.through.sourceColumn}`);
582
647
  const junctionTable = this.registry.getTable(relation.through.table);
583
648
  if (!junctionTable) {
584
649
  logger.warn(`[batchFetchRelatedEntitiesMany] Junction table '${relation.through.table}' not found`);
@@ -612,21 +677,21 @@ export class RelationService {
612
677
  const targetData = (row[targetTableName] || row) as Record<string, unknown>;
613
678
 
614
679
  const parentId = String(junctionData[relation.through.sourceColumn]);
615
- const parsedValues = await parseDataFromServer(targetData, targetCollection);
616
-
617
680
  const arr = resultMap.get(parentId) || [];
618
- arr.push({
619
- id: String(targetData[targetIdInfo.fieldName]),
620
- path: targetCollection.slug,
621
- values: parsedValues as Record<string, unknown>
622
- });
681
+ arr.push(await this.toRelatedRow(targetData, targetCollection, targetPks));
623
682
  resultMap.set(parentId, arr);
624
683
  }
625
684
 
626
685
  return resultMap;
627
686
  }
628
687
 
629
- // Handle FK-based relations (one-to-many inverse)
688
+ // Handle FK-based relations (one-to-many inverse). One column on the
689
+ // target names the parent, so a composite-keyed parent cannot be named.
690
+ this.assertSingleKeyAddressable(
691
+ parentCollection,
692
+ parentPks,
693
+ relation.foreignKeyOnTarget ?? `${relation.inverseRelationName}_id`
694
+ );
630
695
  let query = this.db.select().from(targetTable).$dynamic();
631
696
 
632
697
  query = applyDynamicRelationQuery(
@@ -668,14 +733,9 @@ export class RelationService {
668
733
  }
669
734
 
670
735
  if (parentId !== undefined && parentIdSet.has(String(parentId))) {
671
- const parsedValues = await parseDataFromServer(targetRow, targetCollection);
672
736
  const key = String(parentId);
673
737
  const arr = resultMap.get(key) || [];
674
- arr.push({
675
- id: String(targetRow[targetIdInfo.fieldName]),
676
- path: targetCollection.slug,
677
- values: parsedValues as Record<string, unknown>
678
- });
738
+ arr.push(await this.toRelatedRow(targetRow, targetCollection, targetPks));
679
739
  resultMap.set(key, arr);
680
740
  }
681
741
  }
@@ -746,7 +806,7 @@ export class RelationService {
746
806
  continue;
747
807
  }
748
808
 
749
- const parentPks = getPrimaryKeys(collection, this.registry);
809
+ const parentPks = requirePrimaryKeys(collection, this.registry);
750
810
  const parentIdInfo = parentPks[0];
751
811
  const parsedParentIdObj = parseIdValues(id, parentPks);
752
812
  const parsedParentId = parsedParentIdObj[parentIdInfo.fieldName];
@@ -755,7 +815,7 @@ export class RelationService {
755
815
  await tx.delete(junctionTable).where(eq(sourceJunctionColumn, parsedParentId));
756
816
 
757
817
  if (targetEntityIds.length > 0) {
758
- const targetPks = getPrimaryKeys(targetCollection, this.registry);
818
+ const targetPks = requirePrimaryKeys(targetCollection, this.registry);
759
819
  const targetIdInfo = targetPks[0];
760
820
  const parsedTargetIds = targetEntityIds.map(id => parseIdValues(id, targetPks)[targetIdInfo.fieldName]);
761
821
 
@@ -784,7 +844,7 @@ export class RelationService {
784
844
  continue;
785
845
  }
786
846
 
787
- const parentPks = getPrimaryKeys(collection, this.registry);
847
+ const parentPks = requirePrimaryKeys(collection, this.registry);
788
848
  const parentIdInfo = parentPks[0];
789
849
  const parsedParentIdObj = parseIdValues(id, parentPks);
790
850
  const parsedParentId = parsedParentIdObj[parentIdInfo.fieldName];
@@ -793,7 +853,7 @@ export class RelationService {
793
853
  await tx.delete(junctionTable).where(eq(sourceJunctionColumn, parsedParentId));
794
854
 
795
855
  if (targetEntityIds.length > 0) {
796
- const targetPks = getPrimaryKeys(targetCollection, this.registry);
856
+ const targetPks = requirePrimaryKeys(targetCollection, this.registry);
797
857
  const targetIdInfo = targetPks[0];
798
858
  const parsedTargetIds = targetEntityIds.map(id => parseIdValues(id, targetPks)[targetIdInfo.fieldName]);
799
859
 
@@ -813,7 +873,7 @@ export class RelationService {
813
873
  } else if (relation.cardinality === "many" && relation.direction === "inverse" && relation.foreignKeyOnTarget) {
814
874
  // Handle one-to-many (inverse) by updating target FK to point to parent
815
875
  const targetTable = getTableForCollection(targetCollection, this.registry);
816
- const targetPks = getPrimaryKeys(targetCollection, this.registry);
876
+ const targetPks = requirePrimaryKeys(targetCollection, this.registry);
817
877
  const targetIdInfo = targetPks[0];
818
878
  const targetIdCol = targetTable[targetIdInfo.fieldName as keyof typeof targetTable] as AnyPgColumn;
819
879
  const fkCol = targetTable[relation.foreignKeyOnTarget as keyof typeof targetTable] as AnyPgColumn;
@@ -823,7 +883,7 @@ export class RelationService {
823
883
  continue;
824
884
  }
825
885
 
826
- const parentPks = getPrimaryKeys(collection, this.registry);
886
+ const parentPks = requirePrimaryKeys(collection, this.registry);
827
887
  const parentIdInfo = parentPks[0];
828
888
  const parsedParentIdObj = parseIdValues(id, parentPks);
829
889
  const parsedParentId = parsedParentIdObj[parentIdInfo.fieldName];
@@ -865,7 +925,6 @@ export class RelationService {
865
925
  relationKey: string;
866
926
  relation: Relation;
867
927
  newValue: unknown;
868
- currentId?: string | number;
869
928
  }>
870
929
  ) {
871
930
  for (const update of inverseRelationUpdates) {
@@ -874,9 +933,9 @@ export class RelationService {
874
933
  try {
875
934
  const targetCollection = relation.target();
876
935
  const targetTable = getTableForCollection(targetCollection, this.registry);
877
- const targetPks = getPrimaryKeys(targetCollection, this.registry);
936
+ const targetPks = requirePrimaryKeys(targetCollection, this.registry);
878
937
  const targetIdInfo = targetPks[0];
879
- const sourcePks = getPrimaryKeys(sourceCollection, this.registry);
938
+ const sourcePks = requirePrimaryKeys(sourceCollection, this.registry);
880
939
  const sourceIdInfo = sourcePks[0];
881
940
 
882
941
  // Handle inverse relations with joinPath
@@ -1033,7 +1092,7 @@ export class RelationService {
1033
1092
  }
1034
1093
 
1035
1094
  // Perform the junction table update
1036
- const sourcePks = getPrimaryKeys(sourceCollection, this.registry);
1095
+ const sourcePks = requirePrimaryKeys(sourceCollection, this.registry);
1037
1096
  const sourceIdInfo = sourcePks[0];
1038
1097
  const parsedSourceIdObj = parseIdValues(sourceEntityId, sourcePks);
1039
1098
  const parsedSourceId = parsedSourceIdObj[sourceIdInfo.fieldName];
@@ -1043,7 +1102,7 @@ export class RelationService {
1043
1102
 
1044
1103
  // Add new entries if newValue is provided
1045
1104
  if (newValue && Array.isArray(newValue) && newValue.length > 0) {
1046
- const targetPks = getPrimaryKeys(targetCollection, this.registry);
1105
+ const targetPks = requirePrimaryKeys(targetCollection, this.registry);
1047
1106
  const targetIdInfo = targetPks[0];
1048
1107
  const targetEntityIds = (newValue as Array<{ id: string | number } | string | number>).map((rel) => typeof rel === "object" && rel !== null ? rel.id : rel);
1049
1108
  const parsedTargetIds = targetEntityIds.map(id => parseIdValues(id, targetPks)[targetIdInfo.fieldName]);
@@ -1058,7 +1117,7 @@ export class RelationService {
1058
1117
  }
1059
1118
  } else if (newValue && !Array.isArray(newValue)) {
1060
1119
  // Single value for one-to-one
1061
- const targetPks = getPrimaryKeys(targetCollection, this.registry);
1120
+ const targetPks = requirePrimaryKeys(targetCollection, this.registry);
1062
1121
  const targetIdInfo = targetPks[0];
1063
1122
  const targetId = typeof newValue === "object" && newValue !== null ? (newValue as Record<string, unknown>).id as string | number : newValue as string | number;
1064
1123
  const parsedTargetIdObj = parseIdValues(targetId, targetPks);
@@ -1105,7 +1164,7 @@ export class RelationService {
1105
1164
  return;
1106
1165
  }
1107
1166
 
1108
- const sourcePks = getPrimaryKeys(sourceCollection, this.registry);
1167
+ const sourcePks = requirePrimaryKeys(sourceCollection, this.registry);
1109
1168
  const sourceIdInfo = sourcePks[0];
1110
1169
  const parsedSourceIdObj = parseIdValues(sourceEntityId, sourcePks);
1111
1170
  const parsedSourceId = parsedSourceIdObj[sourceIdInfo.fieldName];
@@ -1115,7 +1174,7 @@ export class RelationService {
1115
1174
 
1116
1175
  // Add new entries if newValue is provided
1117
1176
  if (newValue && Array.isArray(newValue) && newValue.length > 0) {
1118
- const targetPks = getPrimaryKeys(targetCollection, this.registry);
1177
+ const targetPks = requirePrimaryKeys(targetCollection, this.registry);
1119
1178
  const targetIdInfo = targetPks[0];
1120
1179
  const targetEntityIds = (newValue as Array<{ id: string | number }>).map((rel) => rel.id);
1121
1180
  const parsedTargetIds = targetEntityIds.map(id => parseIdValues(id, targetPks)[targetIdInfo.fieldName]);
@@ -1152,14 +1211,14 @@ export class RelationService {
1152
1211
  const { relation, newTargetId } = upd;
1153
1212
  const targetCollection = relation.target();
1154
1213
  const targetTable = getTableForCollection(targetCollection, this.registry);
1155
- const targetPks = getPrimaryKeys(targetCollection, this.registry);
1214
+ const targetPks = requirePrimaryKeys(targetCollection, this.registry);
1156
1215
  const targetIdInfo = targetPks[0];
1157
1216
  const targetIdCol = targetTable[targetIdInfo.fieldName as keyof typeof targetTable] as AnyPgColumn;
1158
1217
 
1159
1218
  // Determine mapping of columns
1160
1219
  const { targetFKColName, parentSourceColName } = this.resolveJoinPathWriteMapping(parentCollection, relation);
1161
1220
  const parentTable = getTableForCollection(parentCollection, this.registry);
1162
- const parentPks = getPrimaryKeys(parentCollection, this.registry);
1221
+ const parentPks = requirePrimaryKeys(parentCollection, this.registry);
1163
1222
  const parentIdInfo = parentPks[0];
1164
1223
  const parsedParentIdObj = parseIdValues(parentId, parentPks);
1165
1224
  const parsedParentId = parsedParentIdObj[parentIdInfo.fieldName];
@@ -1284,7 +1343,7 @@ parentSourceColName };
1284
1343
  }
1285
1344
 
1286
1345
  // Parse the new row ID to the correct type
1287
- const targetPks = getPrimaryKeys(targetCollection, this.registry);
1346
+ const targetPks = requirePrimaryKeys(targetCollection, this.registry);
1288
1347
  const targetIdInfo = targetPks[0];
1289
1348
  const parsedNewEntityIdObj = parseIdValues(newEntityId, targetPks);
1290
1349
  const parsedNewEntityId = parsedNewEntityIdObj[targetIdInfo.fieldName];