@rebasepro/server-postgres 0.9.1-canary.f2f61da → 0.9.1-canary.fd3754b
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/PostgresBackendDriver.d.ts +2 -25
- package/dist/PostgresBootstrapper.d.ts +0 -10
- package/dist/auth/services.d.ts +0 -16
- package/dist/chunk-DSJWtz9O.js +40 -0
- package/dist/connection.d.ts +0 -21
- package/dist/data-transformer.d.ts +2 -9
- package/dist/index.es.js +2598 -2020
- package/dist/index.es.js.map +1 -1
- package/dist/schema/auth-default-policies.d.ts +10 -0
- package/dist/schema/doctor.d.ts +1 -1
- package/dist/security/policy-drift.d.ts +5 -16
- package/dist/security/rls-enforcement.d.ts +2 -27
- package/dist/services/FetchService.d.ts +24 -4
- package/dist/services/PersistService.d.ts +1 -27
- package/dist/services/RelationService.d.ts +1 -34
- package/dist/services/collection-helpers.d.ts +14 -79
- package/dist/services/dataService.d.ts +1 -3
- package/dist/services/index.d.ts +1 -1
- package/dist/services/realtimeService.d.ts +0 -7
- package/dist/src-Eh-CZosp.js +595 -0
- package/dist/src-Eh-CZosp.js.map +1 -0
- package/package.json +17 -15
- package/src/PostgresBackendDriver.ts +13 -127
- package/src/PostgresBootstrapper.ts +26 -68
- package/src/auth/ensure-tables.ts +11 -73
- package/src/auth/services.ts +19 -49
- package/src/cli-helpers.ts +20 -2
- package/src/connection.ts +1 -61
- package/src/data-transformer.ts +9 -11
- package/src/databasePoolManager.ts +0 -2
- package/src/schema/auth-default-policies.ts +125 -0
- package/src/schema/doctor-cli.ts +1 -5
- package/src/schema/doctor.ts +20 -45
- package/src/schema/generate-drizzle-schema-logic.ts +29 -24
- package/src/schema/generate-postgres-ddl-logic.ts +28 -76
- package/src/schema/introspect-db.ts +2 -19
- package/src/security/policy-drift.test.ts +14 -48
- package/src/security/policy-drift.ts +10 -72
- package/src/security/rls-enforcement.ts +3 -64
- package/src/services/BranchService.ts +10 -42
- package/src/services/FetchService.ts +270 -65
- package/src/services/PersistService.ts +14 -130
- package/src/services/RelationService.ts +94 -153
- package/src/services/collection-helpers.ts +47 -164
- package/src/services/dataService.ts +2 -3
- package/src/services/index.ts +0 -1
- package/src/services/realtimeService.ts +19 -40
- package/src/utils/drizzle-conditions.ts +0 -13
- package/src/websocket.ts +1 -4
- package/dist/collections/buildRegistry.d.ts +0 -27
- package/dist/services/row-pipeline.d.ts +0 -63
- package/src/collections/buildRegistry.ts +0 -59
- 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,14 @@ 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 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
|
+
});
|
|
255
173
|
}
|
|
256
174
|
|
|
257
175
|
return rows;
|
|
@@ -310,7 +228,14 @@ export class RelationService {
|
|
|
310
228
|
const rows: RelatedRow<M>[] = [];
|
|
311
229
|
for (const row of results) {
|
|
312
230
|
const targetRow = row[getTableName(targetCollection)] || row;
|
|
313
|
-
|
|
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
|
+
});
|
|
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
|
-
|
|
447
|
-
|
|
448
|
-
|
|
373
|
+
resultMap.set(String(parentId), {
|
|
374
|
+
id: String(targetRow[targetIdInfo.fieldName]),
|
|
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: String(targetRow[targetIdInfo.fieldName]),
|
|
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: String(targetRow[targetIdInfo.fieldName]),
|
|
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: String(targetRow[targetIdInfo.fieldName]),
|
|
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: String(targetData[targetIdInfo.fieldName]),
|
|
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: String(targetRow[targetIdInfo.fieldName]),
|
|
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];
|
|
@@ -925,6 +865,7 @@ export class RelationService {
|
|
|
925
865
|
relationKey: string;
|
|
926
866
|
relation: Relation;
|
|
927
867
|
newValue: unknown;
|
|
868
|
+
currentId?: string | number;
|
|
928
869
|
}>
|
|
929
870
|
) {
|
|
930
871
|
for (const update of inverseRelationUpdates) {
|
|
@@ -933,9 +874,9 @@ export class RelationService {
|
|
|
933
874
|
try {
|
|
934
875
|
const targetCollection = relation.target();
|
|
935
876
|
const targetTable = getTableForCollection(targetCollection, this.registry);
|
|
936
|
-
const targetPks =
|
|
877
|
+
const targetPks = getPrimaryKeys(targetCollection, this.registry);
|
|
937
878
|
const targetIdInfo = targetPks[0];
|
|
938
|
-
const sourcePks =
|
|
879
|
+
const sourcePks = getPrimaryKeys(sourceCollection, this.registry);
|
|
939
880
|
const sourceIdInfo = sourcePks[0];
|
|
940
881
|
|
|
941
882
|
// Handle inverse relations with joinPath
|
|
@@ -1092,7 +1033,7 @@ export class RelationService {
|
|
|
1092
1033
|
}
|
|
1093
1034
|
|
|
1094
1035
|
// Perform the junction table update
|
|
1095
|
-
const sourcePks =
|
|
1036
|
+
const sourcePks = getPrimaryKeys(sourceCollection, this.registry);
|
|
1096
1037
|
const sourceIdInfo = sourcePks[0];
|
|
1097
1038
|
const parsedSourceIdObj = parseIdValues(sourceEntityId, sourcePks);
|
|
1098
1039
|
const parsedSourceId = parsedSourceIdObj[sourceIdInfo.fieldName];
|
|
@@ -1102,7 +1043,7 @@ export class RelationService {
|
|
|
1102
1043
|
|
|
1103
1044
|
// Add new entries if newValue is provided
|
|
1104
1045
|
if (newValue && Array.isArray(newValue) && newValue.length > 0) {
|
|
1105
|
-
const targetPks =
|
|
1046
|
+
const targetPks = getPrimaryKeys(targetCollection, this.registry);
|
|
1106
1047
|
const targetIdInfo = targetPks[0];
|
|
1107
1048
|
const targetEntityIds = (newValue as Array<{ id: string | number } | string | number>).map((rel) => typeof rel === "object" && rel !== null ? rel.id : rel);
|
|
1108
1049
|
const parsedTargetIds = targetEntityIds.map(id => parseIdValues(id, targetPks)[targetIdInfo.fieldName]);
|
|
@@ -1117,7 +1058,7 @@ export class RelationService {
|
|
|
1117
1058
|
}
|
|
1118
1059
|
} else if (newValue && !Array.isArray(newValue)) {
|
|
1119
1060
|
// Single value for one-to-one
|
|
1120
|
-
const targetPks =
|
|
1061
|
+
const targetPks = getPrimaryKeys(targetCollection, this.registry);
|
|
1121
1062
|
const targetIdInfo = targetPks[0];
|
|
1122
1063
|
const targetId = typeof newValue === "object" && newValue !== null ? (newValue as Record<string, unknown>).id as string | number : newValue as string | number;
|
|
1123
1064
|
const parsedTargetIdObj = parseIdValues(targetId, targetPks);
|
|
@@ -1164,7 +1105,7 @@ export class RelationService {
|
|
|
1164
1105
|
return;
|
|
1165
1106
|
}
|
|
1166
1107
|
|
|
1167
|
-
const sourcePks =
|
|
1108
|
+
const sourcePks = getPrimaryKeys(sourceCollection, this.registry);
|
|
1168
1109
|
const sourceIdInfo = sourcePks[0];
|
|
1169
1110
|
const parsedSourceIdObj = parseIdValues(sourceEntityId, sourcePks);
|
|
1170
1111
|
const parsedSourceId = parsedSourceIdObj[sourceIdInfo.fieldName];
|
|
@@ -1174,7 +1115,7 @@ export class RelationService {
|
|
|
1174
1115
|
|
|
1175
1116
|
// Add new entries if newValue is provided
|
|
1176
1117
|
if (newValue && Array.isArray(newValue) && newValue.length > 0) {
|
|
1177
|
-
const targetPks =
|
|
1118
|
+
const targetPks = getPrimaryKeys(targetCollection, this.registry);
|
|
1178
1119
|
const targetIdInfo = targetPks[0];
|
|
1179
1120
|
const targetEntityIds = (newValue as Array<{ id: string | number }>).map((rel) => rel.id);
|
|
1180
1121
|
const parsedTargetIds = targetEntityIds.map(id => parseIdValues(id, targetPks)[targetIdInfo.fieldName]);
|
|
@@ -1211,14 +1152,14 @@ export class RelationService {
|
|
|
1211
1152
|
const { relation, newTargetId } = upd;
|
|
1212
1153
|
const targetCollection = relation.target();
|
|
1213
1154
|
const targetTable = getTableForCollection(targetCollection, this.registry);
|
|
1214
|
-
const targetPks =
|
|
1155
|
+
const targetPks = getPrimaryKeys(targetCollection, this.registry);
|
|
1215
1156
|
const targetIdInfo = targetPks[0];
|
|
1216
1157
|
const targetIdCol = targetTable[targetIdInfo.fieldName as keyof typeof targetTable] as AnyPgColumn;
|
|
1217
1158
|
|
|
1218
1159
|
// Determine mapping of columns
|
|
1219
1160
|
const { targetFKColName, parentSourceColName } = this.resolveJoinPathWriteMapping(parentCollection, relation);
|
|
1220
1161
|
const parentTable = getTableForCollection(parentCollection, this.registry);
|
|
1221
|
-
const parentPks =
|
|
1162
|
+
const parentPks = getPrimaryKeys(parentCollection, this.registry);
|
|
1222
1163
|
const parentIdInfo = parentPks[0];
|
|
1223
1164
|
const parsedParentIdObj = parseIdValues(parentId, parentPks);
|
|
1224
1165
|
const parsedParentId = parsedParentIdObj[parentIdInfo.fieldName];
|
|
@@ -1343,7 +1284,7 @@ parentSourceColName };
|
|
|
1343
1284
|
}
|
|
1344
1285
|
|
|
1345
1286
|
// Parse the new row ID to the correct type
|
|
1346
|
-
const targetPks =
|
|
1287
|
+
const targetPks = getPrimaryKeys(targetCollection, this.registry);
|
|
1347
1288
|
const targetIdInfo = targetPks[0];
|
|
1348
1289
|
const parsedNewEntityIdObj = parseIdValues(newEntityId, targetPks);
|
|
1349
1290
|
const parsedNewEntityId = parsedNewEntityIdObj[targetIdInfo.fieldName];
|