@rebasepro/server-postgres 0.9.1-canary.5809e39 → 0.9.1-canary.58368ce
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.
- package/dist/PostgresBootstrapper.d.ts +0 -10
- package/dist/auth/services.d.ts +0 -16
- package/dist/connection.d.ts +0 -21
- package/dist/index.es.js +243 -455
- package/dist/index.es.js.map +1 -1
- package/dist/schema/doctor.d.ts +1 -1
- package/dist/services/FetchService.d.ts +38 -4
- package/dist/services/RelationService.d.ts +0 -34
- package/dist/services/collection-helpers.d.ts +7 -34
- package/package.json +7 -10
- package/src/PostgresBackendDriver.ts +5 -18
- package/src/PostgresBootstrapper.ts +2 -51
- package/src/auth/ensure-tables.ts +11 -73
- package/src/auth/services.ts +19 -49
- package/src/connection.ts +1 -61
- package/src/databasePoolManager.ts +0 -2
- package/src/schema/doctor.ts +20 -45
- package/src/schema/introspect-db.ts +2 -19
- package/src/services/FetchService.ts +163 -29
- package/src/services/PersistService.ts +2 -9
- package/src/services/RelationService.ts +93 -153
- package/src/services/collection-helpers.ts +27 -59
- package/src/services/realtimeService.ts +2 -4
- package/src/utils/drizzle-conditions.ts +0 -13
- package/dist/services/row-pipeline.d.ts +0 -63
- package/src/services/row-pipeline.ts +0 -239
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { and, eq, inArray,
|
|
1
|
+
import { and, eq, inArray, 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,10 +7,9 @@ import { DrizzleConditionBuilder } from "../utils/drizzle-conditions";
|
|
|
7
7
|
import {
|
|
8
8
|
getCollectionByPath,
|
|
9
9
|
getTableForCollection,
|
|
10
|
-
|
|
10
|
+
getPrimaryKeys,
|
|
11
11
|
parseIdValues,
|
|
12
|
-
buildCompositeId
|
|
13
|
-
type PrimaryKeyInfo
|
|
12
|
+
buildCompositeId
|
|
14
13
|
} from "./collection-helpers";
|
|
15
14
|
import { parseDataFromServer } from "../data-transformer";
|
|
16
15
|
import { PostgresCollectionRegistry } from "../collections/PostgresCollectionRegistry";
|
|
@@ -57,94 +56,6 @@ export interface RelatedRow<M extends Record<string, unknown> = Record<string, u
|
|
|
57
56
|
export class RelationService {
|
|
58
57
|
constructor(private db: DrizzleClient, private registry: PostgresCollectionRegistry) { }
|
|
59
58
|
|
|
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
|
-
|
|
148
59
|
/**
|
|
149
60
|
* Fetch rows related to a parent row through a specific relation
|
|
150
61
|
*/
|
|
@@ -193,10 +104,10 @@ export class RelationService {
|
|
|
193
104
|
): Promise<RelatedRow<M>[]> {
|
|
194
105
|
const targetCollection = relation.target();
|
|
195
106
|
const targetTable = getTableForCollection(targetCollection, this.registry);
|
|
196
|
-
const idInfo =
|
|
107
|
+
const idInfo = getPrimaryKeys(targetCollection, this.registry);
|
|
197
108
|
const idField = targetTable[idInfo[0].fieldName as keyof typeof targetTable] as AnyPgColumn;
|
|
198
109
|
|
|
199
|
-
const parentPks =
|
|
110
|
+
const parentPks = getPrimaryKeys(parentCollection, this.registry);
|
|
200
111
|
const parentIdInfo = parentPks[0];
|
|
201
112
|
const parsedParentIdObj = parseIdValues(parentId, parentPks);
|
|
202
113
|
const parsedParentId = parsedParentIdObj[parentIdInfo.fieldName];
|
|
@@ -237,7 +148,7 @@ export class RelationService {
|
|
|
237
148
|
}
|
|
238
149
|
|
|
239
150
|
// Add where condition for the parent row
|
|
240
|
-
const parentIdField = parentTable[
|
|
151
|
+
const parentIdField = parentTable[getPrimaryKeys(parentCollection, this.registry)[0].fieldName as keyof typeof parentTable] as AnyPgColumn;
|
|
241
152
|
query = query.where(eq(parentIdField, parsedParentId));
|
|
242
153
|
|
|
243
154
|
if (options.limit) {
|
|
@@ -251,7 +162,15 @@ export class RelationService {
|
|
|
251
162
|
const rows: RelatedRow<M>[] = [];
|
|
252
163
|
for (const row of results as Array<Record<string, unknown>>) {
|
|
253
164
|
const targetRow = (row[targetTableName] as Record<string, unknown>) || row;
|
|
254
|
-
|
|
165
|
+
const parsedValues = await parseDataFromServer(targetRow, targetCollection, this.db, this.registry);
|
|
166
|
+
|
|
167
|
+
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),
|
|
171
|
+
path: targetCollection.slug,
|
|
172
|
+
values: parsedValues as M
|
|
173
|
+
});
|
|
255
174
|
}
|
|
256
175
|
|
|
257
176
|
return rows;
|
|
@@ -310,7 +229,13 @@ export class RelationService {
|
|
|
310
229
|
const rows: RelatedRow<M>[] = [];
|
|
311
230
|
for (const row of results) {
|
|
312
231
|
const targetRow = row[getTableName(targetCollection)] || row;
|
|
313
|
-
|
|
232
|
+
const parsedValues = await parseDataFromServer(targetRow, targetCollection, this.db, this.registry);
|
|
233
|
+
|
|
234
|
+
rows.push({
|
|
235
|
+
id: buildCompositeId(targetRow as Record<string, unknown>, idInfo),
|
|
236
|
+
path: targetCollection.slug,
|
|
237
|
+
values: parsedValues as M
|
|
238
|
+
});
|
|
314
239
|
}
|
|
315
240
|
|
|
316
241
|
return rows;
|
|
@@ -335,11 +260,11 @@ export class RelationService {
|
|
|
335
260
|
|
|
336
261
|
const targetCollection = relation.target();
|
|
337
262
|
const targetTable = getTableForCollection(targetCollection, this.registry);
|
|
338
|
-
const targetPks =
|
|
263
|
+
const targetPks = getPrimaryKeys(targetCollection, this.registry);
|
|
339
264
|
const targetIdInfo = targetPks[0];
|
|
340
265
|
const targetIdField = targetTable[targetIdInfo.fieldName as keyof typeof targetTable] as AnyPgColumn;
|
|
341
266
|
|
|
342
|
-
const parentPks =
|
|
267
|
+
const parentPks = getPrimaryKeys(parentCollection, this.registry);
|
|
343
268
|
const parentIdInfo = parentPks[0];
|
|
344
269
|
const parsedParentIdObj = parseIdValues(parentId, parentPks);
|
|
345
270
|
const parsedParentId = parsedParentIdObj[parentIdInfo.fieldName];
|
|
@@ -384,11 +309,11 @@ export class RelationService {
|
|
|
384
309
|
const parentCollection = getCollectionByPath(parentCollectionPath, this.registry);
|
|
385
310
|
const targetCollection = relation.target();
|
|
386
311
|
const targetTable = getTableForCollection(targetCollection, this.registry);
|
|
387
|
-
const targetPks =
|
|
312
|
+
const targetPks = getPrimaryKeys(targetCollection, this.registry);
|
|
388
313
|
const targetIdInfo = targetPks[0];
|
|
389
314
|
const targetIdField = targetTable[targetIdInfo.fieldName as keyof typeof targetTable] as AnyPgColumn;
|
|
390
315
|
|
|
391
|
-
const parentPks =
|
|
316
|
+
const parentPks = getPrimaryKeys(parentCollection, this.registry);
|
|
392
317
|
const parentIdInfo = parentPks[0];
|
|
393
318
|
const parentTable = this.registry.getTable(getTableName(parentCollection));
|
|
394
319
|
if (!parentTable) throw new Error("Parent table not found");
|
|
@@ -429,23 +354,27 @@ export class RelationService {
|
|
|
429
354
|
currentTable = joinTable;
|
|
430
355
|
}
|
|
431
356
|
|
|
432
|
-
//
|
|
433
|
-
|
|
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));
|
|
434
360
|
|
|
435
361
|
const results = await query;
|
|
436
362
|
const targetTableName = relation.joinPath[relation.joinPath.length - 1].table;
|
|
437
363
|
const resultMap = new Map<string, RelatedRow<Record<string, unknown>>>();
|
|
438
364
|
|
|
439
|
-
// Group by
|
|
440
|
-
// results up by, derived the same way on both sides.
|
|
365
|
+
// Group results by parent ID
|
|
441
366
|
for (const row of results as Array<Record<string, unknown>>) {
|
|
442
367
|
const parentRow = (row[getTableName(parentCollection)] || row) as Record<string, unknown>;
|
|
443
368
|
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);
|
|
444
372
|
|
|
445
|
-
resultMap.set(
|
|
446
|
-
buildCompositeId(
|
|
447
|
-
|
|
448
|
-
|
|
373
|
+
resultMap.set(String(parentId), {
|
|
374
|
+
id: buildCompositeId(targetRow, targetPks),
|
|
375
|
+
path: targetCollection.slug,
|
|
376
|
+
values: parsedValues as Record<string, unknown>
|
|
377
|
+
});
|
|
449
378
|
}
|
|
450
379
|
|
|
451
380
|
return resultMap;
|
|
@@ -458,7 +387,6 @@ export class RelationService {
|
|
|
458
387
|
// 2. Query the target table with unique FK values
|
|
459
388
|
// 3. Map results back to parent rows via their FK values
|
|
460
389
|
if (relation.direction === "owning" && relation.localKey) {
|
|
461
|
-
this.assertSingleKeyAddressable(parentCollection, parentPks, relation.localKey);
|
|
462
390
|
const localKeyCol = parentTable[relation.localKey as keyof typeof parentTable] as AnyPgColumn;
|
|
463
391
|
if (!localKeyCol) {
|
|
464
392
|
throw new Error(`Local key column '${relation.localKey}' not found in parent table`);
|
|
@@ -508,21 +436,19 @@ export class RelationService {
|
|
|
508
436
|
for (const [parentIdStr, fkValue] of parentToFk) {
|
|
509
437
|
const targetRow = targetById.get(String(fkValue));
|
|
510
438
|
if (targetRow) {
|
|
511
|
-
|
|
439
|
+
const parsedValues = await parseDataFromServer(targetRow, targetCollection);
|
|
440
|
+
resultMap.set(parentIdStr, {
|
|
441
|
+
id: buildCompositeId(targetRow, targetPks),
|
|
442
|
+
path: targetCollection.slug,
|
|
443
|
+
values: parsedValues as Record<string, unknown>
|
|
444
|
+
});
|
|
512
445
|
}
|
|
513
446
|
}
|
|
514
447
|
|
|
515
448
|
return resultMap;
|
|
516
449
|
}
|
|
517
450
|
|
|
518
|
-
// Handle inverse relation types with batching
|
|
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
|
-
);
|
|
451
|
+
// Handle inverse relation types with batching
|
|
526
452
|
let query = this.db.select().from(targetTable).$dynamic();
|
|
527
453
|
|
|
528
454
|
// Build the relation query with ALL parent IDs
|
|
@@ -562,7 +488,12 @@ export class RelationService {
|
|
|
562
488
|
}
|
|
563
489
|
|
|
564
490
|
if (parentId !== undefined && parentIdSet.has(String(parentId))) {
|
|
565
|
-
|
|
491
|
+
const parsedValues = await parseDataFromServer(targetRow, targetCollection);
|
|
492
|
+
resultMap.set(String(parentId), {
|
|
493
|
+
id: buildCompositeId(targetRow, targetPks),
|
|
494
|
+
path: targetCollection.slug,
|
|
495
|
+
values: parsedValues as Record<string, unknown>
|
|
496
|
+
});
|
|
566
497
|
}
|
|
567
498
|
}
|
|
568
499
|
|
|
@@ -585,11 +516,11 @@ export class RelationService {
|
|
|
585
516
|
const parentCollection = getCollectionByPath(parentCollectionPath, this.registry);
|
|
586
517
|
const targetCollection = relation.target();
|
|
587
518
|
const targetTable = getTableForCollection(targetCollection, this.registry);
|
|
588
|
-
const targetPks =
|
|
519
|
+
const targetPks = getPrimaryKeys(targetCollection, this.registry);
|
|
589
520
|
const targetIdInfo = targetPks[0];
|
|
590
521
|
const targetIdField = targetTable[targetIdInfo.fieldName as keyof typeof targetTable] as AnyPgColumn;
|
|
591
522
|
|
|
592
|
-
const parentPks =
|
|
523
|
+
const parentPks = getPrimaryKeys(parentCollection, this.registry);
|
|
593
524
|
const parentIdInfo = parentPks[0];
|
|
594
525
|
const parentTable = this.registry.getTable(getTableName(parentCollection));
|
|
595
526
|
if (!parentTable) throw new Error("Parent table not found");
|
|
@@ -619,7 +550,8 @@ export class RelationService {
|
|
|
619
550
|
currentTable = joinTable;
|
|
620
551
|
}
|
|
621
552
|
|
|
622
|
-
|
|
553
|
+
const parentIdField = parentTable[getPrimaryKeys(parentCollection, this.registry)[0].fieldName as keyof typeof parentTable] as AnyPgColumn;
|
|
554
|
+
query = query.where(inArray(parentIdField, parsedParentIds));
|
|
623
555
|
|
|
624
556
|
const results = await query;
|
|
625
557
|
const targetTableName = relation.joinPath[relation.joinPath.length - 1].table;
|
|
@@ -628,9 +560,15 @@ export class RelationService {
|
|
|
628
560
|
for (const row of results as Array<Record<string, unknown>>) {
|
|
629
561
|
const parentRow = (row[getTableName(parentCollection)] || row) as Record<string, unknown>;
|
|
630
562
|
const targetRow = (row[targetTableName] || row) as Record<string, unknown>;
|
|
631
|
-
const parentId =
|
|
563
|
+
const parentId = String(parentRow[parentIdInfo.fieldName]);
|
|
564
|
+
const parsedValues = await parseDataFromServer(targetRow, targetCollection);
|
|
565
|
+
|
|
632
566
|
const arr = resultMap.get(parentId) || [];
|
|
633
|
-
arr.push(
|
|
567
|
+
arr.push({
|
|
568
|
+
id: buildCompositeId(targetRow, targetPks),
|
|
569
|
+
path: targetCollection.slug,
|
|
570
|
+
values: parsedValues as Record<string, unknown>
|
|
571
|
+
});
|
|
634
572
|
resultMap.set(parentId, arr);
|
|
635
573
|
}
|
|
636
574
|
|
|
@@ -641,9 +579,6 @@ export class RelationService {
|
|
|
641
579
|
// This is the standard path for posts→tags style relations where
|
|
642
580
|
// sanitizeRelation populated the `through` config.
|
|
643
581
|
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}`);
|
|
647
582
|
const junctionTable = this.registry.getTable(relation.through.table);
|
|
648
583
|
if (!junctionTable) {
|
|
649
584
|
logger.warn(`[batchFetchRelatedEntitiesMany] Junction table '${relation.through.table}' not found`);
|
|
@@ -677,21 +612,21 @@ export class RelationService {
|
|
|
677
612
|
const targetData = (row[targetTableName] || row) as Record<string, unknown>;
|
|
678
613
|
|
|
679
614
|
const parentId = String(junctionData[relation.through.sourceColumn]);
|
|
615
|
+
const parsedValues = await parseDataFromServer(targetData, targetCollection);
|
|
616
|
+
|
|
680
617
|
const arr = resultMap.get(parentId) || [];
|
|
681
|
-
arr.push(
|
|
618
|
+
arr.push({
|
|
619
|
+
id: buildCompositeId(targetData, targetPks),
|
|
620
|
+
path: targetCollection.slug,
|
|
621
|
+
values: parsedValues as Record<string, unknown>
|
|
622
|
+
});
|
|
682
623
|
resultMap.set(parentId, arr);
|
|
683
624
|
}
|
|
684
625
|
|
|
685
626
|
return resultMap;
|
|
686
627
|
}
|
|
687
628
|
|
|
688
|
-
// Handle FK-based relations (one-to-many inverse)
|
|
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
|
-
);
|
|
629
|
+
// Handle FK-based relations (one-to-many inverse)
|
|
695
630
|
let query = this.db.select().from(targetTable).$dynamic();
|
|
696
631
|
|
|
697
632
|
query = applyDynamicRelationQuery(
|
|
@@ -733,9 +668,14 @@ export class RelationService {
|
|
|
733
668
|
}
|
|
734
669
|
|
|
735
670
|
if (parentId !== undefined && parentIdSet.has(String(parentId))) {
|
|
671
|
+
const parsedValues = await parseDataFromServer(targetRow, targetCollection);
|
|
736
672
|
const key = String(parentId);
|
|
737
673
|
const arr = resultMap.get(key) || [];
|
|
738
|
-
arr.push(
|
|
674
|
+
arr.push({
|
|
675
|
+
id: buildCompositeId(targetRow, targetPks),
|
|
676
|
+
path: targetCollection.slug,
|
|
677
|
+
values: parsedValues as Record<string, unknown>
|
|
678
|
+
});
|
|
739
679
|
resultMap.set(key, arr);
|
|
740
680
|
}
|
|
741
681
|
}
|
|
@@ -806,7 +746,7 @@ export class RelationService {
|
|
|
806
746
|
continue;
|
|
807
747
|
}
|
|
808
748
|
|
|
809
|
-
const parentPks =
|
|
749
|
+
const parentPks = getPrimaryKeys(collection, this.registry);
|
|
810
750
|
const parentIdInfo = parentPks[0];
|
|
811
751
|
const parsedParentIdObj = parseIdValues(id, parentPks);
|
|
812
752
|
const parsedParentId = parsedParentIdObj[parentIdInfo.fieldName];
|
|
@@ -815,7 +755,7 @@ export class RelationService {
|
|
|
815
755
|
await tx.delete(junctionTable).where(eq(sourceJunctionColumn, parsedParentId));
|
|
816
756
|
|
|
817
757
|
if (targetEntityIds.length > 0) {
|
|
818
|
-
const targetPks =
|
|
758
|
+
const targetPks = getPrimaryKeys(targetCollection, this.registry);
|
|
819
759
|
const targetIdInfo = targetPks[0];
|
|
820
760
|
const parsedTargetIds = targetEntityIds.map(id => parseIdValues(id, targetPks)[targetIdInfo.fieldName]);
|
|
821
761
|
|
|
@@ -844,7 +784,7 @@ export class RelationService {
|
|
|
844
784
|
continue;
|
|
845
785
|
}
|
|
846
786
|
|
|
847
|
-
const parentPks =
|
|
787
|
+
const parentPks = getPrimaryKeys(collection, this.registry);
|
|
848
788
|
const parentIdInfo = parentPks[0];
|
|
849
789
|
const parsedParentIdObj = parseIdValues(id, parentPks);
|
|
850
790
|
const parsedParentId = parsedParentIdObj[parentIdInfo.fieldName];
|
|
@@ -853,7 +793,7 @@ export class RelationService {
|
|
|
853
793
|
await tx.delete(junctionTable).where(eq(sourceJunctionColumn, parsedParentId));
|
|
854
794
|
|
|
855
795
|
if (targetEntityIds.length > 0) {
|
|
856
|
-
const targetPks =
|
|
796
|
+
const targetPks = getPrimaryKeys(targetCollection, this.registry);
|
|
857
797
|
const targetIdInfo = targetPks[0];
|
|
858
798
|
const parsedTargetIds = targetEntityIds.map(id => parseIdValues(id, targetPks)[targetIdInfo.fieldName]);
|
|
859
799
|
|
|
@@ -873,7 +813,7 @@ export class RelationService {
|
|
|
873
813
|
} else if (relation.cardinality === "many" && relation.direction === "inverse" && relation.foreignKeyOnTarget) {
|
|
874
814
|
// Handle one-to-many (inverse) by updating target FK to point to parent
|
|
875
815
|
const targetTable = getTableForCollection(targetCollection, this.registry);
|
|
876
|
-
const targetPks =
|
|
816
|
+
const targetPks = getPrimaryKeys(targetCollection, this.registry);
|
|
877
817
|
const targetIdInfo = targetPks[0];
|
|
878
818
|
const targetIdCol = targetTable[targetIdInfo.fieldName as keyof typeof targetTable] as AnyPgColumn;
|
|
879
819
|
const fkCol = targetTable[relation.foreignKeyOnTarget as keyof typeof targetTable] as AnyPgColumn;
|
|
@@ -883,7 +823,7 @@ export class RelationService {
|
|
|
883
823
|
continue;
|
|
884
824
|
}
|
|
885
825
|
|
|
886
|
-
const parentPks =
|
|
826
|
+
const parentPks = getPrimaryKeys(collection, this.registry);
|
|
887
827
|
const parentIdInfo = parentPks[0];
|
|
888
828
|
const parsedParentIdObj = parseIdValues(id, parentPks);
|
|
889
829
|
const parsedParentId = parsedParentIdObj[parentIdInfo.fieldName];
|
|
@@ -933,9 +873,9 @@ export class RelationService {
|
|
|
933
873
|
try {
|
|
934
874
|
const targetCollection = relation.target();
|
|
935
875
|
const targetTable = getTableForCollection(targetCollection, this.registry);
|
|
936
|
-
const targetPks =
|
|
876
|
+
const targetPks = getPrimaryKeys(targetCollection, this.registry);
|
|
937
877
|
const targetIdInfo = targetPks[0];
|
|
938
|
-
const sourcePks =
|
|
878
|
+
const sourcePks = getPrimaryKeys(sourceCollection, this.registry);
|
|
939
879
|
const sourceIdInfo = sourcePks[0];
|
|
940
880
|
|
|
941
881
|
// Handle inverse relations with joinPath
|
|
@@ -1092,7 +1032,7 @@ export class RelationService {
|
|
|
1092
1032
|
}
|
|
1093
1033
|
|
|
1094
1034
|
// Perform the junction table update
|
|
1095
|
-
const sourcePks =
|
|
1035
|
+
const sourcePks = getPrimaryKeys(sourceCollection, this.registry);
|
|
1096
1036
|
const sourceIdInfo = sourcePks[0];
|
|
1097
1037
|
const parsedSourceIdObj = parseIdValues(sourceEntityId, sourcePks);
|
|
1098
1038
|
const parsedSourceId = parsedSourceIdObj[sourceIdInfo.fieldName];
|
|
@@ -1102,7 +1042,7 @@ export class RelationService {
|
|
|
1102
1042
|
|
|
1103
1043
|
// Add new entries if newValue is provided
|
|
1104
1044
|
if (newValue && Array.isArray(newValue) && newValue.length > 0) {
|
|
1105
|
-
const targetPks =
|
|
1045
|
+
const targetPks = getPrimaryKeys(targetCollection, this.registry);
|
|
1106
1046
|
const targetIdInfo = targetPks[0];
|
|
1107
1047
|
const targetEntityIds = (newValue as Array<{ id: string | number } | string | number>).map((rel) => typeof rel === "object" && rel !== null ? rel.id : rel);
|
|
1108
1048
|
const parsedTargetIds = targetEntityIds.map(id => parseIdValues(id, targetPks)[targetIdInfo.fieldName]);
|
|
@@ -1117,7 +1057,7 @@ export class RelationService {
|
|
|
1117
1057
|
}
|
|
1118
1058
|
} else if (newValue && !Array.isArray(newValue)) {
|
|
1119
1059
|
// Single value for one-to-one
|
|
1120
|
-
const targetPks =
|
|
1060
|
+
const targetPks = getPrimaryKeys(targetCollection, this.registry);
|
|
1121
1061
|
const targetIdInfo = targetPks[0];
|
|
1122
1062
|
const targetId = typeof newValue === "object" && newValue !== null ? (newValue as Record<string, unknown>).id as string | number : newValue as string | number;
|
|
1123
1063
|
const parsedTargetIdObj = parseIdValues(targetId, targetPks);
|
|
@@ -1164,7 +1104,7 @@ export class RelationService {
|
|
|
1164
1104
|
return;
|
|
1165
1105
|
}
|
|
1166
1106
|
|
|
1167
|
-
const sourcePks =
|
|
1107
|
+
const sourcePks = getPrimaryKeys(sourceCollection, this.registry);
|
|
1168
1108
|
const sourceIdInfo = sourcePks[0];
|
|
1169
1109
|
const parsedSourceIdObj = parseIdValues(sourceEntityId, sourcePks);
|
|
1170
1110
|
const parsedSourceId = parsedSourceIdObj[sourceIdInfo.fieldName];
|
|
@@ -1174,7 +1114,7 @@ export class RelationService {
|
|
|
1174
1114
|
|
|
1175
1115
|
// Add new entries if newValue is provided
|
|
1176
1116
|
if (newValue && Array.isArray(newValue) && newValue.length > 0) {
|
|
1177
|
-
const targetPks =
|
|
1117
|
+
const targetPks = getPrimaryKeys(targetCollection, this.registry);
|
|
1178
1118
|
const targetIdInfo = targetPks[0];
|
|
1179
1119
|
const targetEntityIds = (newValue as Array<{ id: string | number }>).map((rel) => rel.id);
|
|
1180
1120
|
const parsedTargetIds = targetEntityIds.map(id => parseIdValues(id, targetPks)[targetIdInfo.fieldName]);
|
|
@@ -1211,14 +1151,14 @@ export class RelationService {
|
|
|
1211
1151
|
const { relation, newTargetId } = upd;
|
|
1212
1152
|
const targetCollection = relation.target();
|
|
1213
1153
|
const targetTable = getTableForCollection(targetCollection, this.registry);
|
|
1214
|
-
const targetPks =
|
|
1154
|
+
const targetPks = getPrimaryKeys(targetCollection, this.registry);
|
|
1215
1155
|
const targetIdInfo = targetPks[0];
|
|
1216
1156
|
const targetIdCol = targetTable[targetIdInfo.fieldName as keyof typeof targetTable] as AnyPgColumn;
|
|
1217
1157
|
|
|
1218
1158
|
// Determine mapping of columns
|
|
1219
1159
|
const { targetFKColName, parentSourceColName } = this.resolveJoinPathWriteMapping(parentCollection, relation);
|
|
1220
1160
|
const parentTable = getTableForCollection(parentCollection, this.registry);
|
|
1221
|
-
const parentPks =
|
|
1161
|
+
const parentPks = getPrimaryKeys(parentCollection, this.registry);
|
|
1222
1162
|
const parentIdInfo = parentPks[0];
|
|
1223
1163
|
const parsedParentIdObj = parseIdValues(parentId, parentPks);
|
|
1224
1164
|
const parsedParentId = parsedParentIdObj[parentIdInfo.fieldName];
|
|
@@ -1343,7 +1283,7 @@ parentSourceColName };
|
|
|
1343
1283
|
}
|
|
1344
1284
|
|
|
1345
1285
|
// Parse the new row ID to the correct type
|
|
1346
|
-
const targetPks =
|
|
1286
|
+
const targetPks = getPrimaryKeys(targetCollection, this.registry);
|
|
1347
1287
|
const targetIdInfo = targetPks[0];
|
|
1348
1288
|
const parsedNewEntityIdObj = parseIdValues(newEntityId, targetPks);
|
|
1349
1289
|
const parsedNewEntityId = parsedNewEntityIdObj[targetIdInfo.fieldName];
|
|
@@ -58,27 +58,10 @@ export function getTableForCollection(collection: CollectionConfig, registry: Po
|
|
|
58
58
|
return table;
|
|
59
59
|
}
|
|
60
60
|
|
|
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
61
|
export function getPrimaryKeys(collection: CollectionConfig, registry: PostgresCollectionRegistry): PrimaryKeyInfo[] {
|
|
81
|
-
|
|
62
|
+
const table = getTableForCollection(collection, registry);
|
|
63
|
+
|
|
64
|
+
// Fallback to explicitly defined isId properties
|
|
82
65
|
if (collection.properties) {
|
|
83
66
|
const idProps = Object.entries(collection.properties)
|
|
84
67
|
.filter(([_, prop]) => "isId" in (prop as object) && Boolean((prop as { isId?: unknown }).isId))
|
|
@@ -93,12 +76,6 @@ export function getPrimaryKeys(collection: CollectionConfig, registry: PostgresC
|
|
|
93
76
|
}
|
|
94
77
|
}
|
|
95
78
|
|
|
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
79
|
// Otherwise infer from Drizzle schema
|
|
103
80
|
const keys: PrimaryKeyInfo[] = [];
|
|
104
81
|
for (const [key, colRaw] of Object.entries(table)) {
|
|
@@ -128,27 +105,6 @@ isUUID });
|
|
|
128
105
|
return keys;
|
|
129
106
|
}
|
|
130
107
|
|
|
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
108
|
/**
|
|
153
109
|
* Collections whose key the *browser* cannot resolve, and what it will do
|
|
154
110
|
* instead.
|
|
@@ -182,9 +138,14 @@ export function findUnresolvableKeyCollections(
|
|
|
182
138
|
// Declared `isId` is the tier both sides share: if it is there, they agree.
|
|
183
139
|
if (getDeclaredPrimaryKeys(collection).length > 0) continue;
|
|
184
140
|
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
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
|
+
}
|
|
188
149
|
if (keys.length === 0) continue;
|
|
189
150
|
|
|
190
151
|
// A single key named `id` is the last tier on both sides: they agree
|
|
@@ -246,21 +207,28 @@ export function warnOnKeysTheAdminCannotResolve(
|
|
|
246
207
|
* The address of a row: derived from the collection's primary keys, because a
|
|
247
208
|
* row does not carry one — it is exactly its columns.
|
|
248
209
|
*
|
|
249
|
-
* Falls back to a literal `id` column,
|
|
250
|
-
*
|
|
251
|
-
*
|
|
252
|
-
*
|
|
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).
|
|
253
217
|
*/
|
|
254
218
|
export function deriveRowAddress(
|
|
255
219
|
row: Record<string, unknown>,
|
|
256
220
|
collection: CollectionConfig,
|
|
257
221
|
registry: PostgresCollectionRegistry
|
|
258
222
|
): string {
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
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;
|
|
229
|
+
}
|
|
230
|
+
} catch {
|
|
231
|
+
/* unresolvable table or keys — fall through to the literal column */
|
|
264
232
|
}
|
|
265
233
|
if (row.id !== undefined && row.id !== null) return String(row.id);
|
|
266
234
|
return "";
|
|
@@ -963,10 +963,8 @@ roles: activeAuth.roles },
|
|
|
963
963
|
const keys = getPrimaryKeys(collection, this.registry);
|
|
964
964
|
return keys.length > 0 ? keys : undefined;
|
|
965
965
|
} catch {
|
|
966
|
-
//
|
|
967
|
-
//
|
|
968
|
-
// `posts/1` that name no collection. Telling the subscriber nothing
|
|
969
|
-
// is right here; letting it throw would drop the notification.
|
|
966
|
+
// Unregistered table, or a relation path with no collection of its
|
|
967
|
+
// own: the client keeps its pre-existing `id` behaviour.
|
|
970
968
|
return undefined;
|
|
971
969
|
}
|
|
972
970
|
}
|