@rebasepro/server-postgres 0.11.1-canary.gfd39654 → 0.12.1-canary.g009ed95
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 +1 -1
- package/dist/PostgresBootstrapper.d.ts +33 -1
- package/dist/auth/services.d.ts +21 -0
- package/dist/backup/backup-service.d.ts +10 -1
- package/dist/backup/pg-tools.d.ts +47 -0
- package/dist/backup-service-CD8o_1Sl.js +8999 -0
- package/dist/backup-service-CD8o_1Sl.js.map +1 -0
- package/dist/cli-helpers.d.ts +39 -0
- package/dist/collections/buildRegistry.d.ts +1 -1
- package/dist/connection-BuZ97wsr.js +250 -0
- package/dist/connection-BuZ97wsr.js.map +1 -0
- package/dist/connection.d.ts +42 -0
- package/dist/ensure-collection-policies-BrUVgjz3.js +57 -0
- package/dist/ensure-collection-policies-BrUVgjz3.js.map +1 -0
- package/dist/ensure-collection-tables-Da2oGkX2.js +650 -0
- package/dist/ensure-collection-tables-Da2oGkX2.js.map +1 -0
- package/dist/history/HistoryService.d.ts +9 -29
- package/dist/index.es.js +1234 -9753
- package/dist/index.es.js.map +1 -1
- package/dist/policy-CeA1JcxP.js +105 -0
- package/dist/policy-CeA1JcxP.js.map +1 -0
- package/dist/schema/auth-schema.d.ts +83 -144
- package/dist/schema/dynamic-tables.d.ts +1 -1
- package/dist/schema/ensure-collection-policies.d.ts +60 -0
- package/dist/schema/ensure-collection-tables.d.ts +44 -2
- package/dist/schema/generate-postgres-ddl-logic.d.ts +135 -1
- package/dist/schema/introspect-db-constraints.d.ts +57 -0
- package/dist/schema/introspect-db-logic.d.ts +94 -5
- package/dist/schema/introspect-db-queries.d.ts +119 -0
- package/dist/schema/introspect-db-structure.d.ts +263 -0
- package/dist/schema/introspect-db-types.d.ts +11 -0
- package/dist/schema/introspect-runtime.d.ts +1 -1
- package/dist/services/FetchService.d.ts +40 -2
- package/dist/services/RelationService.d.ts +24 -1
- package/dist/services/channel-bus/index.d.ts +1 -7
- package/dist/services/collection-helpers.d.ts +24 -1
- package/dist/services/dataService.d.ts +3 -1
- package/dist/services/row-pipeline.d.ts +4 -2
- package/dist/{src-3VmUJ8Xn.js → src-CzbghKwf.js} +464 -187
- package/dist/src-CzbghKwf.js.map +1 -0
- package/dist/{src-D5xBTl32.js → src-DoU9yPqq.js} +79 -189
- package/dist/src-DoU9yPqq.js.map +1 -0
- package/dist/utils/connection-string.d.ts +29 -0
- package/dist/utils/drizzle-conditions.d.ts +162 -7
- package/dist/utils/pg-error-utils.d.ts +25 -3
- package/dist/websocket-B2LsrINK.js +530 -0
- package/dist/websocket-B2LsrINK.js.map +1 -0
- package/package.json +14 -14
- package/src/PostgresAdapter.ts +21 -2
- package/src/PostgresBackendDriver.ts +4 -0
- package/src/PostgresBootstrapper.ts +212 -36
- package/src/auth/ensure-tables.ts +164 -9
- package/src/auth/services.ts +24 -2
- package/src/backup/backup-cli.ts +41 -2
- package/src/backup/backup-service.ts +38 -5
- package/src/backup/pg-tools.ts +96 -3
- package/src/cli-helpers.ts +70 -0
- package/src/cli.ts +44 -26
- package/src/collections/buildRegistry.ts +1 -1
- package/src/collections/validate-relations.ts +15 -0
- package/src/connection.ts +73 -0
- package/src/data-transformer.ts +9 -3
- package/src/databasePoolManager.ts +5 -2
- package/src/history/HistoryService.ts +13 -31
- package/src/schema/auth-schema.ts +30 -19
- package/src/schema/dynamic-tables.ts +1 -1
- package/src/schema/ensure-collection-policies.ts +105 -0
- package/src/schema/ensure-collection-tables.test.ts +105 -9
- package/src/schema/ensure-collection-tables.ts +220 -32
- package/src/schema/generate-drizzle-schema-logic.ts +33 -8
- package/src/schema/generate-postgres-ddl-logic.ts +382 -19
- package/src/schema/introspect-db-constraints.ts +385 -0
- package/src/schema/introspect-db-inference.ts +18 -8
- package/src/schema/introspect-db-logic.ts +385 -71
- package/src/schema/introspect-db-queries.ts +326 -0
- package/src/schema/introspect-db-structure.ts +670 -0
- package/src/schema/introspect-db-types.ts +56 -0
- package/src/schema/introspect-db.ts +37 -80
- package/src/schema/introspect-runtime.test.ts +56 -8
- package/src/schema/introspect-runtime.ts +32 -10
- package/src/security/policy-drift.test.ts +11 -3
- package/src/services/FetchService.ts +148 -18
- package/src/services/PersistService.ts +20 -6
- package/src/services/RelationService.ts +249 -48
- package/src/services/channel-bus/index.ts +0 -9
- package/src/services/collection-helpers.ts +40 -1
- package/src/services/dataService.ts +3 -1
- package/src/services/realtimeService.ts +3 -3
- package/src/services/row-pipeline.ts +4 -2
- package/src/utils/connection-string.ts +58 -0
- package/src/utils/drizzle-conditions.ts +539 -50
- package/src/utils/pg-error-utils.ts +98 -3
- package/src/websocket.ts +18 -9
- package/dist/chunk-DSJWtz9O.js +0 -40
- package/dist/ensure-collection-tables-DGMYK0fr.js +0 -304
- package/dist/ensure-collection-tables-DGMYK0fr.js.map +0 -1
- package/dist/src-3VmUJ8Xn.js.map +0 -1
- package/dist/src-D5xBTl32.js.map +0 -1
|
@@ -1,12 +1,14 @@
|
|
|
1
|
-
import { and, asc, count, desc, eq, getTableName, gt, lt, or, SQL, TableRelationalConfig, TablesRelationalConfig } from "drizzle-orm";
|
|
1
|
+
import { and, asc, count, desc, eq, getTableColumns, getTableName, gt, lt, or, SQL, TableRelationalConfig, TablesRelationalConfig } from "drizzle-orm";
|
|
2
2
|
import { AnyPgColumn, PgTable } from "drizzle-orm/pg-core";
|
|
3
3
|
import { CollectionConfig, FilterValues, ResolvedRelation, LogicalCondition, isManyToMany } from "@rebasepro/types";
|
|
4
4
|
import type { VectorSearchParams } from "@rebasepro/types";
|
|
5
5
|
import { resolveCollectionRelations, findRelation, createRelationRef, createRelationRefWithData } from "@rebasepro/common";
|
|
6
|
-
import {
|
|
6
|
+
import { generateForeignKeyName } from "@rebasepro/utils";
|
|
7
|
+
import { DrizzleConditionBuilder, getUnknownFilterFieldsMode, type FilterCompilationOptions } from "../utils/drizzle-conditions";
|
|
7
8
|
import {
|
|
8
9
|
getCollectionByPath,
|
|
9
10
|
getTableForCollection,
|
|
11
|
+
getPrimaryKeys,
|
|
10
12
|
requirePrimaryKeys,
|
|
11
13
|
deriveRowAddress,
|
|
12
14
|
parseIdValues,
|
|
@@ -18,9 +20,9 @@ import { RelationService } from "./RelationService";
|
|
|
18
20
|
import { RelationalQueryBuilder } from "drizzle-orm/pg-core/query-builders/query";
|
|
19
21
|
import { DrizzleClient } from "../interfaces";
|
|
20
22
|
import { PostgresCollectionRegistry } from "../collections/PostgresCollectionRegistry";
|
|
21
|
-
import {
|
|
23
|
+
import { toFlatRow, toRestRow, isJunctionRelation } from "./row-pipeline";
|
|
22
24
|
import { isNestedPath, resolveNestedPath, type NestedPathHop } from "./nested-path";
|
|
23
|
-
import { logger } from "@rebasepro/server";
|
|
25
|
+
import { ApiError, logger } from "@rebasepro/server";
|
|
24
26
|
|
|
25
27
|
/** Type-safe accessor for Drizzle's relational query API via dynamic table name */
|
|
26
28
|
type DbQueryAccessor = Record<string, RelationalQueryBuilder<any, any>> | undefined;
|
|
@@ -45,6 +47,45 @@ export class FetchService {
|
|
|
45
47
|
return query?.[tableName] as RelationalQueryBuilder<TablesRelationalConfig, TableRelationalConfig> | undefined;
|
|
46
48
|
}
|
|
47
49
|
|
|
50
|
+
/**
|
|
51
|
+
* The context the condition builder needs to compile a filter key that is
|
|
52
|
+
* not a column name outright.
|
|
53
|
+
*
|
|
54
|
+
* Two such keys. An owning relation's key resolves through the collection's
|
|
55
|
+
* relations to its foreign-key column; a relation whose link lives on the
|
|
56
|
+
* target table or in a junction resolves to a correlated `EXISTS`, which
|
|
57
|
+
* needs the registry to reach that other table and this table's key column
|
|
58
|
+
* to correlate back.
|
|
59
|
+
*
|
|
60
|
+
* Looked up rather than passed: every read path already has the path, only
|
|
61
|
+
* some have the collection, and a path that names no registered collection
|
|
62
|
+
* (a nested/derived one) is not an error here — the builder simply falls
|
|
63
|
+
* back to guessing the default key shapes, and a relation filter it cannot
|
|
64
|
+
* compile stays unresolvable and so fails closed.
|
|
65
|
+
*/
|
|
66
|
+
private filterContext(collectionPath: string, table: PgTable<any>): FilterCompilationOptions {
|
|
67
|
+
const collection = this.registry.getCollectionByPath(collectionPath) ?? undefined;
|
|
68
|
+
return {
|
|
69
|
+
collection,
|
|
70
|
+
registry: this.registry,
|
|
71
|
+
sourceIdColumn: collection ? this.resolveIdColumn(collection, table) : undefined
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* The table column this collection's rows are keyed by, or `undefined`.
|
|
77
|
+
*
|
|
78
|
+
* `getPrimaryKeys` rather than `requirePrimaryKeys`: a collection with no
|
|
79
|
+
* resolvable key is not an error on the filter path — it only means the
|
|
80
|
+
* relation filters that would correlate on it cannot be compiled, which
|
|
81
|
+
* the builder already handles by failing that field closed.
|
|
82
|
+
*/
|
|
83
|
+
private resolveIdColumn(collection: CollectionConfig, table: PgTable<any>): AnyPgColumn | undefined {
|
|
84
|
+
const [idInfo] = getPrimaryKeys(collection, this.registry);
|
|
85
|
+
if (!idInfo) return undefined;
|
|
86
|
+
return table[idInfo.fieldName as keyof typeof table] as AnyPgColumn | undefined;
|
|
87
|
+
}
|
|
88
|
+
|
|
48
89
|
/**
|
|
49
90
|
* Build filter conditions from FilterValues
|
|
50
91
|
* Delegates to DrizzleConditionBuilder.buildFilterConditions
|
|
@@ -54,7 +95,9 @@ export class FetchService {
|
|
|
54
95
|
table: PgTable<any>,
|
|
55
96
|
collectionPath: string
|
|
56
97
|
): SQL[] {
|
|
57
|
-
return DrizzleConditionBuilder.buildFilterConditions(
|
|
98
|
+
return DrizzleConditionBuilder.buildFilterConditions(
|
|
99
|
+
filter, table, collectionPath, this.filterContext(collectionPath, table)
|
|
100
|
+
);
|
|
58
101
|
}
|
|
59
102
|
|
|
60
103
|
// =============================================================
|
|
@@ -64,20 +107,97 @@ export class FetchService {
|
|
|
64
107
|
/**
|
|
65
108
|
* Resolves the correct Drizzle column for sorting.
|
|
66
109
|
* Automatically maps owning relation property keys to their underlying foreign key column.
|
|
110
|
+
*
|
|
111
|
+
* The relation's own `localKey` is the authority for that foreign key, not
|
|
112
|
+
* `<field>_id`. The default local key comes from `generateForeignKeyName`,
|
|
113
|
+
* which snake-cases *and singularises* — `userProfile` → `user_profile_id`,
|
|
114
|
+
* `users` → `user_id` — and an author can override it outright. A wrong
|
|
115
|
+
* guess resolves to nothing, the caller drops the `ORDER BY`, and the rows
|
|
116
|
+
* come back in whatever order Postgres pleases: paging over that repeats
|
|
117
|
+
* and skips rows rather than erroring. The guesses stay, last, for a
|
|
118
|
+
* caller that hands over no collection to resolve against.
|
|
67
119
|
*/
|
|
68
120
|
private resolveOrderByField(
|
|
69
121
|
table: PgTable<any>,
|
|
70
122
|
orderBy: string,
|
|
71
123
|
collection?: CollectionConfig
|
|
72
124
|
): AnyPgColumn | undefined {
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
125
|
+
const columnAt = (key: string): AnyPgColumn | undefined =>
|
|
126
|
+
(key in table ? table[key as keyof typeof table] as AnyPgColumn : undefined) || undefined;
|
|
127
|
+
|
|
128
|
+
const direct = columnAt(orderBy);
|
|
129
|
+
if (direct) return direct;
|
|
130
|
+
|
|
131
|
+
// Owning relation, resolved: the relation names its own local key.
|
|
132
|
+
const declaredRelation = collection ? resolveCollectionRelations(collection)[orderBy] : undefined;
|
|
133
|
+
if (declaredRelation?.kind === "belongsTo") {
|
|
134
|
+
const foreignKey = columnAt(declaredRelation.localKey);
|
|
135
|
+
if (foreignKey) return foreignKey;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// No collection in hand — the two shapes an owning relation's key takes
|
|
139
|
+
// by default (e.g. `project` → `project_id`, `userProfile` →
|
|
140
|
+
// `user_profile_id`).
|
|
141
|
+
for (const guess of [`${orderBy}_id`, generateForeignKeyName(orderBy)]) {
|
|
142
|
+
const foreignKey = columnAt(guess);
|
|
143
|
+
if (foreignKey) return foreignKey;
|
|
79
144
|
}
|
|
80
|
-
|
|
145
|
+
|
|
146
|
+
// Nothing resolved. Returning `undefined` is exactly what the docblock
|
|
147
|
+
// above describes: the caller drops the ORDER BY and hands back rows in
|
|
148
|
+
// whatever order Postgres pleases, while the requester believes they
|
|
149
|
+
// are sorted. `?orderBy=titel` answered 200 with unsorted data and no
|
|
150
|
+
// hint that the sort had been ignored.
|
|
151
|
+
//
|
|
152
|
+
// A *filter* naming a field that does not exist is already refused for
|
|
153
|
+
// precisely this reason — it "used to widen results silently". An
|
|
154
|
+
// unresolvable sort field is the same drift between a query and the
|
|
155
|
+
// schema, so it answers the same way and honours the same switch: one
|
|
156
|
+
// knob, because a deployment that wants the lenient behaviour wants it
|
|
157
|
+
// for both.
|
|
158
|
+
const collectionName = collection?.slug ?? collection?.name;
|
|
159
|
+
const onCollection = collectionName ? ` on collection '${collectionName}'` : "";
|
|
160
|
+
|
|
161
|
+
// A declared to-many relation is a different mistake from a typo, and
|
|
162
|
+
// saying "unknown field" about a field the collection plainly declares
|
|
163
|
+
// sends the reader looking for a spelling error that is not there.
|
|
164
|
+
// There is simply no single value per row to order by — `posts.tags` is
|
|
165
|
+
// a set — so no ORDER BY exists to write, with or without a typo.
|
|
166
|
+
if (declaredRelation && declaredRelation.kind !== "belongsTo") {
|
|
167
|
+
throw ApiError.badRequest(
|
|
168
|
+
`Cannot sort by '${orderBy}'${onCollection}: it is a to-many relation ` +
|
|
169
|
+
`(${declaredRelation.kind}), which has no single value per row to order by.`,
|
|
170
|
+
"ORDER_BY_FIELD_NOT_SORTABLE",
|
|
171
|
+
{ field: orderBy, kind: declaredRelation.kind, ...(collectionName && { collection: collectionName }) }
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
if (getUnknownFilterFieldsMode() === "warn") {
|
|
176
|
+
logger.warn(
|
|
177
|
+
`Sorting by field '${orderBy}'${onCollection}, but it does not exist in the table — ` +
|
|
178
|
+
"the ORDER BY was dropped and these rows are unsorted."
|
|
179
|
+
);
|
|
180
|
+
return undefined;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
let validFields: string[] = [];
|
|
184
|
+
try {
|
|
185
|
+
validFields = Object.keys(getTableColumns(table)).sort();
|
|
186
|
+
} catch {
|
|
187
|
+
// A table stand-in without Drizzle's column symbols — the message
|
|
188
|
+
// is worth less without the list, but not worth failing over.
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
throw ApiError.badRequest(
|
|
192
|
+
`Unknown orderBy field '${orderBy}'${onCollection}` +
|
|
193
|
+
(validFields.length > 0 ? `. Valid fields: ${validFields.join(", ")}` : ""),
|
|
194
|
+
"UNKNOWN_ORDER_BY_FIELD",
|
|
195
|
+
{
|
|
196
|
+
field: orderBy,
|
|
197
|
+
...(collectionName && { collection: collectionName }),
|
|
198
|
+
...(validFields.length > 0 && { validFields })
|
|
199
|
+
}
|
|
200
|
+
);
|
|
81
201
|
}
|
|
82
202
|
|
|
83
203
|
/**
|
|
@@ -85,7 +205,7 @@ export class FetchService {
|
|
|
85
205
|
* Converts collection relations to a Drizzle-compatible `with` object.
|
|
86
206
|
*
|
|
87
207
|
* When `include` is provided, only those relations are loaded.
|
|
88
|
-
* When `include` is absent, ALL relations are loaded (
|
|
208
|
+
* When `include` is absent, ALL relations are loaded (the admin path).
|
|
89
209
|
*
|
|
90
210
|
* Automatically detects many-to-many junction tables and nests
|
|
91
211
|
* the target relation so actual row data is returned.
|
|
@@ -311,7 +431,7 @@ export class FetchService {
|
|
|
311
431
|
}
|
|
312
432
|
|
|
313
433
|
if (options.logical) {
|
|
314
|
-
const logicalCondition = DrizzleConditionBuilder.buildLogicalConditions(options.logical, table, collectionPath);
|
|
434
|
+
const logicalCondition = DrizzleConditionBuilder.buildLogicalConditions(options.logical, table, collectionPath, this.filterContext(collectionPath, table));
|
|
315
435
|
if (logicalCondition) allConditions.push(logicalCondition);
|
|
316
436
|
}
|
|
317
437
|
|
|
@@ -487,7 +607,7 @@ idColumn };
|
|
|
487
607
|
|
|
488
608
|
if (!row) return undefined;
|
|
489
609
|
|
|
490
|
-
const flatRow =
|
|
610
|
+
const flatRow = toFlatRow(row, collection, this.registry);
|
|
491
611
|
|
|
492
612
|
// Post-fetch joinPath relations that Drizzle's `with` can't express
|
|
493
613
|
await this.resolveJoinPathRelations<M>(flatRow, collection, collectionPath, parsedId, databaseId);
|
|
@@ -614,7 +734,7 @@ idColumn };
|
|
|
614
734
|
const results = await qb.findMany(queryOpts as Parameters<NonNullable<typeof qb>["findMany"]>[0]);
|
|
615
735
|
|
|
616
736
|
const rows = (results as Record<string, unknown>[]).map(row =>
|
|
617
|
-
|
|
737
|
+
toFlatRow(row, collection, this.registry)
|
|
618
738
|
);
|
|
619
739
|
|
|
620
740
|
return rows;
|
|
@@ -656,7 +776,7 @@ _distance: vectorMeta.distanceSelect }).from(table).$dynamic()
|
|
|
656
776
|
}
|
|
657
777
|
|
|
658
778
|
if (options.logical) {
|
|
659
|
-
const logicalCondition = DrizzleConditionBuilder.buildLogicalConditions(options.logical, table, collectionPath);
|
|
779
|
+
const logicalCondition = DrizzleConditionBuilder.buildLogicalConditions(options.logical, table, collectionPath, this.filterContext(collectionPath, table));
|
|
660
780
|
if (logicalCondition) allConditions.push(logicalCondition);
|
|
661
781
|
}
|
|
662
782
|
|
|
@@ -717,7 +837,7 @@ _distance: vectorMeta.distanceSelect }).from(table).$dynamic()
|
|
|
717
837
|
/**
|
|
718
838
|
* Fallback path used when db.query is unavailable.
|
|
719
839
|
*
|
|
720
|
-
* The primary path runs the results through `
|
|
840
|
+
* The primary path runs the results through `toFlatRow`, which maps
|
|
721
841
|
* relations from what drizzle already nested — no query per row. This one
|
|
722
842
|
* has no nesting to read, so it resolves relations itself, in batches.
|
|
723
843
|
*
|
|
@@ -874,6 +994,7 @@ relatedTo: hop });
|
|
|
874
994
|
collectionPath: string,
|
|
875
995
|
options: {
|
|
876
996
|
filter?: FilterValues<Extract<keyof M, string>>;
|
|
997
|
+
logical?: LogicalCondition;
|
|
877
998
|
searchString?: string;
|
|
878
999
|
databaseId?: string;
|
|
879
1000
|
} = {}
|
|
@@ -905,6 +1026,13 @@ relatedTo: hop });
|
|
|
905
1026
|
if (filterConditions.length > 0) allConditions.push(...filterConditions);
|
|
906
1027
|
}
|
|
907
1028
|
|
|
1029
|
+
if (options.logical) {
|
|
1030
|
+
const logicalCondition = DrizzleConditionBuilder.buildLogicalConditions(
|
|
1031
|
+
options.logical, table, effectivePath, this.filterContext(effectivePath, table)
|
|
1032
|
+
);
|
|
1033
|
+
if (logicalCondition) allConditions.push(logicalCondition);
|
|
1034
|
+
}
|
|
1035
|
+
|
|
908
1036
|
if (allConditions.length > 0) {
|
|
909
1037
|
const finalCondition = DrizzleConditionBuilder.combineConditionsWithAnd(allConditions);
|
|
910
1038
|
if (finalCondition) query = query.where(finalCondition);
|
|
@@ -975,6 +1103,8 @@ relatedTo: hop });
|
|
|
975
1103
|
collectionPath: string,
|
|
976
1104
|
options: {
|
|
977
1105
|
filter?: FilterValues<Extract<keyof M, string>>;
|
|
1106
|
+
/** An `or(...)`/`and(...)` group, applied alongside `filter`. */
|
|
1107
|
+
logical?: LogicalCondition;
|
|
978
1108
|
orderBy?: string;
|
|
979
1109
|
order?: "desc" | "asc";
|
|
980
1110
|
limit?: number;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { eq, and, sql, SQL } from "drizzle-orm";
|
|
2
2
|
import { AnyPgColumn, PgTable } from "drizzle-orm/pg-core";
|
|
3
3
|
// import { NodePgDatabase } from "drizzle-orm/node-postgres";
|
|
4
|
-
import { CollectionConfig, Properties, ResolvedRelation, type ResolvedManyToMany, isManyToMany } from "@rebasepro/types";
|
|
4
|
+
import { CollectionConfig, Properties, ResolvedRelation, type ResolvedManyToMany, isManyToMany, hasForeignKeyOnTarget } from "@rebasepro/types";
|
|
5
5
|
import { getTableName, resolveCollectionRelations } from "@rebasepro/common";
|
|
6
6
|
import { DrizzleConditionBuilder } from "../utils/drizzle-conditions";
|
|
7
7
|
import {
|
|
@@ -240,16 +240,30 @@ export class PersistService {
|
|
|
240
240
|
throw ApiError.notFound(`No row "${id}" in "${collectionPath}" to update.`);
|
|
241
241
|
}
|
|
242
242
|
} else {
|
|
243
|
-
// One-to-many create: stamp the parent's
|
|
243
|
+
// One-to-many create: stamp the parent's key onto the child's FK.
|
|
244
244
|
const targetColumnName = this.resolveParentForeignKeyColumn(hop);
|
|
245
245
|
|
|
246
246
|
if (targetColumnName) {
|
|
247
|
-
|
|
247
|
+
// Not necessarily the id in the path: a link with a
|
|
248
|
+
// `sourceKey` is pointed at a column on the parent row, and
|
|
249
|
+
// stamping the id would create a child that belongs to
|
|
250
|
+
// nobody — a row the read path would never return again.
|
|
251
|
+
const parentKeyValue = hasForeignKeyOnTarget(hop.relation)
|
|
252
|
+
? await this.relationService.parentKeyValue(hop.parentCollection, hop.relation, hop.parentId)
|
|
253
|
+
: parentIdForWrite();
|
|
254
|
+
|
|
255
|
+
if (parentKeyValue === undefined) {
|
|
256
|
+
throw ApiError.badRequest(
|
|
257
|
+
`Cannot create under "${collectionPath}": the parent row has no value in ` +
|
|
258
|
+
`\`sourceKey\`, so the new row has nothing to point at.`
|
|
259
|
+
);
|
|
260
|
+
}
|
|
261
|
+
|
|
248
262
|
const existingValue = (effectiveValues as Record<string, unknown>)[targetColumnName];
|
|
249
|
-
if (existingValue !== undefined && existingValue !== null && existingValue !==
|
|
250
|
-
logger.warn(`Overriding provided value '${existingValue}' for FK '${targetColumnName}' with path parent
|
|
263
|
+
if (existingValue !== undefined && existingValue !== null && existingValue !== parentKeyValue) {
|
|
264
|
+
logger.warn(`Overriding provided value '${existingValue}' for FK '${targetColumnName}' with path parent key '${parentKeyValue}'.`);
|
|
251
265
|
}
|
|
252
|
-
(effectiveValues as Record<string, unknown>)[targetColumnName] =
|
|
266
|
+
(effectiveValues as Record<string, unknown>)[targetColumnName] = parentKeyValue;
|
|
253
267
|
}
|
|
254
268
|
}
|
|
255
269
|
}
|