@rebasepro/server-postgres 0.9.1-canary.1d2d8b5 → 0.9.1-canary.26fe4b2
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/README.md +21 -0
- package/dist/PostgresBackendDriver.d.ts +43 -2
- package/dist/PostgresBootstrapper.d.ts +17 -1
- package/dist/auth/services.d.ts +68 -52
- package/dist/collections/buildRegistry.d.ts +27 -0
- package/dist/connection.d.ts +21 -0
- package/dist/data-transformer.d.ts +9 -2
- package/dist/index.es.js +2060 -762
- package/dist/index.es.js.map +1 -1
- package/dist/schema/auth-bootstrap-sql.d.ts +1 -1
- package/dist/schema/auth-schema.d.ts +24 -24
- package/dist/schema/doctor.d.ts +1 -1
- package/dist/schema/introspect-db-logic.d.ts +0 -5
- package/dist/schema/introspect-db-naming.d.ts +10 -0
- package/dist/security/policy-drift.d.ts +30 -0
- package/dist/security/rls-enforcement.d.ts +2 -2
- package/dist/services/FetchService.d.ts +4 -24
- package/dist/services/PersistService.d.ts +9 -1
- package/dist/services/RelationService.d.ts +34 -1
- package/dist/services/channel-history.d.ts +118 -0
- package/dist/services/collection-helpers.d.ts +79 -14
- package/dist/services/dataService.d.ts +3 -1
- package/dist/services/index.d.ts +1 -1
- package/dist/services/realtimeService.d.ts +76 -2
- package/dist/services/row-pipeline.d.ts +63 -0
- package/package.json +12 -33
- package/src/PostgresBackendDriver.ts +183 -18
- package/src/PostgresBootstrapper.ts +80 -26
- package/src/auth/ensure-tables.ts +170 -28
- package/src/auth/services.ts +181 -150
- package/src/cli.ts +60 -0
- package/src/collections/buildRegistry.ts +59 -0
- package/src/connection.ts +61 -1
- package/src/data-transformer.ts +11 -9
- package/src/databasePoolManager.ts +2 -0
- package/src/schema/auth-bootstrap-sql.ts +7 -1
- package/src/schema/auth-schema.ts +13 -13
- package/src/schema/doctor.ts +45 -20
- package/src/schema/generate-drizzle-schema-logic.ts +24 -29
- package/src/schema/generate-postgres-ddl-logic.ts +76 -28
- package/src/schema/introspect-db-inference.ts +1 -1
- package/src/schema/introspect-db-logic.ts +1 -10
- package/src/schema/introspect-db-naming.ts +15 -0
- package/src/schema/introspect-db.ts +19 -2
- package/src/schema/introspect-runtime.ts +1 -1
- package/src/security/policy-drift.test.ts +106 -1
- package/src/security/policy-drift.ts +56 -0
- package/src/security/rls-enforcement.ts +11 -5
- package/src/services/BranchService.ts +42 -10
- package/src/services/FetchService.ts +65 -270
- package/src/services/PersistService.ts +62 -9
- package/src/services/RelationService.ts +153 -94
- package/src/services/channel-history.ts +343 -0
- package/src/services/collection-helpers.ts +164 -47
- package/src/services/dataService.ts +3 -2
- package/src/services/index.ts +1 -0
- package/src/services/realtimeService.ts +238 -29
- package/src/services/row-pipeline.ts +239 -0
- package/src/utils/drizzle-conditions.ts +13 -0
- package/src/websocket.ts +34 -12
- package/dist/schema/auth-default-policies.d.ts +0 -10
- 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
|
-
|
|
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 =
|
|
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 =
|
|
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[
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
-
//
|
|
358
|
-
|
|
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
|
|
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(
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
936
|
+
const targetPks = requirePrimaryKeys(targetCollection, this.registry);
|
|
878
937
|
const targetIdInfo = targetPks[0];
|
|
879
|
-
const sourcePks =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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];
|