@atscript/db 0.1.141 → 0.1.143
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/agg.d.cts +1 -1
- package/dist/agg.d.mts +1 -1
- package/dist/{buckets-Bv4pah66.d.cts → buckets-Ba5lP_o9.d.cts} +358 -17
- package/dist/{buckets-CjL7F-hp.d.mts → buckets-BlJ4cdlr.d.mts} +358 -17
- package/dist/{column-diff-DiBbXyLA.d.cts → column-diff-BlGxPobU.d.cts} +1 -1
- package/dist/{column-diff-CfPNcP6e.cjs → column-diff-D_Kyuh0S.cjs} +906 -209
- package/dist/{column-diff-n-k5KY0u.d.mts → column-diff-PqA_edXA.d.mts} +1 -1
- package/dist/{column-diff-CmFNXV8C.mjs → column-diff-e2oHc71_.mjs} +850 -177
- package/dist/index.cjs +16 -10
- package/dist/index.d.cts +30 -5
- package/dist/index.d.mts +30 -5
- package/dist/index.mjs +6 -5
- package/dist/nested-writer-CnOOAehr.mjs +880 -0
- package/dist/nested-writer-xfQwxplL.cjs +1029 -0
- package/dist/{object-DSN0h9lB.d.cts → object-CtPYTIvy.d.cts} +3 -1
- package/dist/{object-DSN0h9lB.d.mts → object-CtPYTIvy.d.mts} +3 -1
- package/dist/object-Djg28csK.cjs +101 -0
- package/dist/object-TkiJQ-Dp.mjs +66 -0
- package/dist/rel.cjs +5 -2
- package/dist/rel.d.cts +76 -10
- package/dist/rel.d.mts +76 -10
- package/dist/rel.mjs +3 -3
- package/dist/{relation-helpers-B59to_dG.d.mts → relation-helpers-JTEyZzVd.d.cts} +1 -1
- package/dist/{relation-helpers-DQ_nRsV9.d.cts → relation-helpers-MTwzaSGA.d.mts} +1 -1
- package/dist/{relation-loader-CgJ8bK6X.cjs → relation-loader-CBPY6kM7.cjs} +1 -1
- package/dist/{relation-loader-CuhEBzFU.mjs → relation-loader-D9XuXaMv.mjs} +1 -1
- package/dist/sync.cjs +1 -1
- package/dist/sync.d.cts +2 -2
- package/dist/sync.d.mts +2 -2
- package/dist/sync.mjs +1 -1
- package/dist/{validator-DASnXf1j.cjs → validator-CVS-onRg.cjs} +0 -101
- package/dist/{validator-D8bPsXPN.mjs → validator-Clu2q_7z.mjs} +1 -66
- package/dist/validator.cjs +4 -3
- package/dist/validator.d.cts +1 -1
- package/dist/validator.d.mts +1 -1
- package/dist/validator.mjs +2 -1
- package/package.json +1 -1
- package/dist/nested-writer-BO3vhbkP.mjs +0 -661
- package/dist/nested-writer-DYsRxZ5f.cjs +0 -768
|
@@ -0,0 +1,1029 @@
|
|
|
1
|
+
const require_db_error = require("./db-error-DTkkeu5b.cjs");
|
|
2
|
+
const require_object = require("./object-Djg28csK.cjs");
|
|
3
|
+
let _atscript_typescript_utils = require("@atscript/typescript/utils");
|
|
4
|
+
//#region src/rel/relation-helpers.ts
|
|
5
|
+
/**
|
|
6
|
+
* Finds the FK entry that connects a `@db.rel.to` relation to its target.
|
|
7
|
+
*/
|
|
8
|
+
function findFKForRelation(relation, foreignKeys) {
|
|
9
|
+
const fk = findFKEntryForRelation(relation, foreignKeys);
|
|
10
|
+
return fk && {
|
|
11
|
+
localFields: fk.fields,
|
|
12
|
+
targetFields: fk.targetFields
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* The `@db.rel.FK` entry {@link findFKForRelation} pairs with a relation: the
|
|
17
|
+
* one sharing its alias, else (no alias) the first one targeting its table.
|
|
18
|
+
*/
|
|
19
|
+
function findFKEntryForRelation(relation, foreignKeys) {
|
|
20
|
+
if (relation.alias) {
|
|
21
|
+
for (const fk of foreignKeys.values()) if (fk.alias === relation.alias) return fk;
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
const targetTable = resolveRelationTargetTable(relation);
|
|
25
|
+
for (const fk of foreignKeys.values()) if (fk.targetTable === targetTable) return fk;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Finds a FK on a remote table that points back to a given table name.
|
|
29
|
+
*/
|
|
30
|
+
function findRemoteFK(targetTable, thisTableName, alias) {
|
|
31
|
+
for (const fk of targetTable.foreignKeys.values()) {
|
|
32
|
+
if (alias && fk.alias === alias && fk.targetTable === thisTableName) return fk;
|
|
33
|
+
if (!alias && fk.targetTable === thisTableName) return fk;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Physical name of an annotated type — its `@db.table`, else its `@db.view`
|
|
38
|
+
* name (a view is a readable source too, since 0.1.141), else its type id
|
|
39
|
+
* (`""` when none is set). The rule relations, foreign keys and view plans
|
|
40
|
+
* use to name a referenced table or view.
|
|
41
|
+
* @since 0.1.136
|
|
42
|
+
*/
|
|
43
|
+
function tableNameOf(type) {
|
|
44
|
+
const table = type?.metadata?.get("db.table");
|
|
45
|
+
if (table) return table;
|
|
46
|
+
const view = type?.metadata?.get("db.view");
|
|
47
|
+
return typeof view === "string" && view || type?.id || "";
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Resolves the target table name from a relation's target type metadata.
|
|
51
|
+
*/
|
|
52
|
+
function resolveRelationTargetTable(relation) {
|
|
53
|
+
return tableNameOf(relation.targetType());
|
|
54
|
+
}
|
|
55
|
+
//#endregion
|
|
56
|
+
//#region src/shared/keys.ts
|
|
57
|
+
/** String form of a key value (an ObjectId stringifies to its hex, a bigint to its digits). */
|
|
58
|
+
function keyString(value) {
|
|
59
|
+
return String(value);
|
|
60
|
+
}
|
|
61
|
+
/** Key equality across driver representations (number vs numeric string, ObjectId vs hex). */
|
|
62
|
+
function sameKey(a, b) {
|
|
63
|
+
if (a === b) return true;
|
|
64
|
+
if (a === null || a === void 0 || b === null || b === void 0) return false;
|
|
65
|
+
return keyString(a) === keyString(b);
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* One string identity for the key tuple `fields` of `row` (dot-paths read
|
|
69
|
+
* nested) — equal for equal keys across driver representations. Used to
|
|
70
|
+
* dedupe / match rows by primary key in memory.
|
|
71
|
+
*/
|
|
72
|
+
function pkTupleKey(row, fields) {
|
|
73
|
+
let out = "";
|
|
74
|
+
for (let i = 0; i < fields.length; i++) {
|
|
75
|
+
if (i > 0) out += "\0";
|
|
76
|
+
out += keyString(require_object.getPath(row, fields[i]));
|
|
77
|
+
}
|
|
78
|
+
return out;
|
|
79
|
+
}
|
|
80
|
+
/** Whether `row` carries exactly the values of an equality filter `{ field: value, … }`. */
|
|
81
|
+
function rowMatchesKey(row, key) {
|
|
82
|
+
for (const field in key) if (!sameKey(require_object.getPath(row, field), key[field])) return false;
|
|
83
|
+
return true;
|
|
84
|
+
}
|
|
85
|
+
//#endregion
|
|
86
|
+
//#region src/table/error-utils.ts
|
|
87
|
+
/**
|
|
88
|
+
* Prefixes error paths with a nav field context.
|
|
89
|
+
* Ensures errors from child table operations (e.g., FK violations on a comment)
|
|
90
|
+
* get paths like `comments[0].authorId` instead of just `authorId`.
|
|
91
|
+
*/
|
|
92
|
+
function prefixErrorPaths(errors, prefix) {
|
|
93
|
+
return errors.map((err) => ({
|
|
94
|
+
...err,
|
|
95
|
+
path: err.path ? `${prefix}.${err.path}` : prefix
|
|
96
|
+
}));
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Wraps an async nested operation and prefixes error paths with the nav field context.
|
|
100
|
+
*/
|
|
101
|
+
async function wrapNestedError(navField, fn) {
|
|
102
|
+
try {
|
|
103
|
+
return await fn();
|
|
104
|
+
} catch (error) {
|
|
105
|
+
if (error instanceof _atscript_typescript_utils.ValidatorError) throw new _atscript_typescript_utils.ValidatorError(prefixErrorPaths(error.errors, navField));
|
|
106
|
+
if (error instanceof require_db_error.DbError) throw new require_db_error.DbError(error.code, prefixErrorPaths(error.errors, navField));
|
|
107
|
+
throw error;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Catches `DbError('FK_VIOLATION')` with empty paths (from adapters that
|
|
112
|
+
* enforce FKs natively but can't report which field failed) and enriches
|
|
113
|
+
* the error with all FK field names from table metadata.
|
|
114
|
+
*/
|
|
115
|
+
async function enrichFkViolation(meta, fn) {
|
|
116
|
+
try {
|
|
117
|
+
return await fn();
|
|
118
|
+
} catch (error) {
|
|
119
|
+
if (error instanceof require_db_error.DbError && error.code === "FK_VIOLATION" && error.errors.every((err) => !err.path)) {
|
|
120
|
+
const msg = error.errors[0]?.message ?? error.message;
|
|
121
|
+
const errors = [];
|
|
122
|
+
for (const [, fk] of meta.foreignKeys) for (const field of fk.fields) errors.push({
|
|
123
|
+
path: field,
|
|
124
|
+
message: msg
|
|
125
|
+
});
|
|
126
|
+
throw new require_db_error.DbError("FK_VIOLATION", errors.length > 0 ? errors : error.errors);
|
|
127
|
+
}
|
|
128
|
+
throw error;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Wraps a delete operation: catches native `FK_VIOLATION` errors (e.g. SQLite
|
|
133
|
+
* RESTRICT) and re-throws as `CONFLICT` (409) with a descriptive message.
|
|
134
|
+
*/
|
|
135
|
+
async function remapDeleteFkViolation(tableName, fn) {
|
|
136
|
+
try {
|
|
137
|
+
return await fn();
|
|
138
|
+
} catch (error) {
|
|
139
|
+
if (error instanceof require_db_error.DbError && error.code === "FK_VIOLATION") throw new require_db_error.DbError("CONFLICT", [{
|
|
140
|
+
path: tableName,
|
|
141
|
+
message: `Cannot delete from "${tableName}": referenced by child records (RESTRICT)`
|
|
142
|
+
}]);
|
|
143
|
+
throw error;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
//#endregion
|
|
147
|
+
//#region src/rel/nested-writer.ts
|
|
148
|
+
/** A TO relation's target table and the local → target foreign key. */
|
|
149
|
+
function resolveToRelation(host, relation) {
|
|
150
|
+
const targetTable = host._writeTableResolver?.(relation.targetType());
|
|
151
|
+
const fk = targetTable && host._findFKForRelation(relation);
|
|
152
|
+
return targetTable && fk ? {
|
|
153
|
+
targetTable,
|
|
154
|
+
fk
|
|
155
|
+
} : void 0;
|
|
156
|
+
}
|
|
157
|
+
/** A FROM relation's child table and the child → parent foreign key (when declared). */
|
|
158
|
+
function resolveFromRelation(host, relation) {
|
|
159
|
+
const targetTable = host._writeTableResolver?.(relation.targetType());
|
|
160
|
+
return targetTable ? {
|
|
161
|
+
targetTable,
|
|
162
|
+
remoteFK: host._findRemoteFK(targetTable, host.tableName, relation.alias)
|
|
163
|
+
} : void 0;
|
|
164
|
+
}
|
|
165
|
+
function resolveViaRelation(host, relation) {
|
|
166
|
+
if (!relation.viaType || !host._writeTableResolver) return void 0;
|
|
167
|
+
const targetTable = host._writeTableResolver(relation.targetType());
|
|
168
|
+
const junctionTable = targetTable && host._writeTableResolver(relation.viaType());
|
|
169
|
+
if (!targetTable || !junctionTable) return void 0;
|
|
170
|
+
const fkToThis = host._findRemoteFK(junctionTable, host.tableName);
|
|
171
|
+
const fkToTarget = host._findRemoteFK(junctionTable, resolveRelationTargetTable(relation));
|
|
172
|
+
const targetPKField = targetTable.primaryKeys[0];
|
|
173
|
+
if (!fkToThis || !fkToTarget || !targetPKField || fkToThis.fields.length !== 1 || fkToTarget.fields.length !== 1) return;
|
|
174
|
+
return {
|
|
175
|
+
targetTable,
|
|
176
|
+
junctionTable,
|
|
177
|
+
fkToThis: fkToThis.fields[0],
|
|
178
|
+
fkToTarget: fkToTarget.fields[0],
|
|
179
|
+
targetPKField
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
/** The parent's single primary-key field — nested FROM / VIA writes need one. */
|
|
183
|
+
function parentPKFieldOf(host) {
|
|
184
|
+
return host._meta.primaryKeys.length === 1 ? host._meta.primaryKeys[0] : void 0;
|
|
185
|
+
}
|
|
186
|
+
/** Whether any item carries a (non-null) value for `navField` — a phase's cheap skip. */
|
|
187
|
+
function hasNavValue(items, navField) {
|
|
188
|
+
return items.some((item) => item[navField] !== void 0 && item[navField] !== null);
|
|
189
|
+
}
|
|
190
|
+
/** The nested TO objects of a batch for one relation, with their source indices. */
|
|
191
|
+
function nestedToObjects(items, navField) {
|
|
192
|
+
const parents = [];
|
|
193
|
+
const sourceIndices = [];
|
|
194
|
+
for (let i = 0; i < items.length; i++) {
|
|
195
|
+
const nested = items[i][navField];
|
|
196
|
+
if (nested && typeof nested === "object" && !Array.isArray(nested)) {
|
|
197
|
+
parents.push(nested);
|
|
198
|
+
sourceIndices.push(i);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
return {
|
|
202
|
+
parents,
|
|
203
|
+
sourceIndices
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
/**
|
|
207
|
+
* Checks if any payload contains navigational data that would be silently
|
|
208
|
+
* dropped because maxDepth is 0.
|
|
209
|
+
*/
|
|
210
|
+
function checkDepthOverflow(payloads, maxDepth, meta) {
|
|
211
|
+
if (meta.navFields.size === 0) return;
|
|
212
|
+
for (const payload of payloads) for (const navField of meta.navFields) if (payload[navField] !== void 0) throw new Error(`Nested data in '${navField}' exceeds maxDepth (${maxDepth}). Increase maxDepth or strip nested data before writing.`);
|
|
213
|
+
}
|
|
214
|
+
/**
|
|
215
|
+
* Validates a batch of items using the given validator and context.
|
|
216
|
+
* Wraps per-item validation errors with array index paths for batch operations.
|
|
217
|
+
*/
|
|
218
|
+
function validateBatch(validator, items, ctx) {
|
|
219
|
+
for (let i = 0; i < items.length; i++) try {
|
|
220
|
+
validator.validate(items[i], false, ctx);
|
|
221
|
+
} catch (error) {
|
|
222
|
+
if (items.length > 1) {
|
|
223
|
+
if (error instanceof _atscript_typescript_utils.ValidatorError) throw new _atscript_typescript_utils.ValidatorError(error.errors.map((err) => ({
|
|
224
|
+
...err,
|
|
225
|
+
path: `[${i}].${err.path}`
|
|
226
|
+
})));
|
|
227
|
+
if (error instanceof require_db_error.DepthLimitExceededError) throw new require_db_error.DepthLimitExceededError(`[${i}].${error.field}`, error.declared, error.actual);
|
|
228
|
+
}
|
|
229
|
+
throw error;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
/**
|
|
233
|
+
* Pre-validates FROM children (type + FK constraints) before the main insert.
|
|
234
|
+
* Catches errors early before the parent record is committed.
|
|
235
|
+
*/
|
|
236
|
+
async function preValidateNestedFrom(host, originals) {
|
|
237
|
+
for (const [navField, relation] of host._meta.relations) {
|
|
238
|
+
if (relation.direction !== "from" || !hasNavValue(originals, navField)) continue;
|
|
239
|
+
const from = resolveFromRelation(host, relation);
|
|
240
|
+
if (!from) continue;
|
|
241
|
+
const allChildren = [];
|
|
242
|
+
for (const orig of originals) {
|
|
243
|
+
const children = orig[navField];
|
|
244
|
+
if (!Array.isArray(children)) continue;
|
|
245
|
+
for (const child of children) {
|
|
246
|
+
const childData = { ...child };
|
|
247
|
+
if (from.remoteFK) {
|
|
248
|
+
for (const field of from.remoteFK.fields) if (!(field in childData)) childData[field] = 0;
|
|
249
|
+
}
|
|
250
|
+
allChildren.push(childData);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
if (allChildren.length === 0) continue;
|
|
254
|
+
await wrapNestedError(navField, () => from.targetTable.preValidateItems(allChildren, { excludeFkTargetTable: host.tableName }));
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
/**
|
|
258
|
+
* Batch-creates TO dependencies before the main insert.
|
|
259
|
+
*/
|
|
260
|
+
async function batchInsertNestedTo(host, items, maxDepth, depth) {
|
|
261
|
+
for (const [navField, relation] of host._meta.relations) {
|
|
262
|
+
if (relation.direction !== "to" || !hasNavValue(items, navField)) continue;
|
|
263
|
+
const to = resolveToRelation(host, relation);
|
|
264
|
+
if (!to) continue;
|
|
265
|
+
const { parents, sourceIndices } = nestedToObjects(items, navField);
|
|
266
|
+
if (parents.length === 0) continue;
|
|
267
|
+
const result = await to.targetTable.insertMany(parents, {
|
|
268
|
+
maxDepth,
|
|
269
|
+
_depth: depth + 1
|
|
270
|
+
});
|
|
271
|
+
if (to.fk.localFields.length === 1) {
|
|
272
|
+
const fkField = to.fk.localFields[0];
|
|
273
|
+
for (let j = 0; j < sourceIndices.length; j++) items[sourceIndices[j]][fkField] = result.insertedIds[j];
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
/**
|
|
278
|
+
* Batch-creates FROM dependents after the main insert.
|
|
279
|
+
*/
|
|
280
|
+
async function batchInsertNestedFrom(host, originals, parentIds, maxDepth, depth) {
|
|
281
|
+
for (const [navField, relation] of host._meta.relations) {
|
|
282
|
+
if (relation.direction !== "from" || !hasNavValue(originals, navField)) continue;
|
|
283
|
+
const from = resolveFromRelation(host, relation);
|
|
284
|
+
const remoteFK = from?.remoteFK;
|
|
285
|
+
if (!from || !remoteFK) continue;
|
|
286
|
+
const allChildren = [];
|
|
287
|
+
for (let i = 0; i < originals.length; i++) {
|
|
288
|
+
const children = originals[i][navField];
|
|
289
|
+
if (!Array.isArray(children)) continue;
|
|
290
|
+
for (const child of children) {
|
|
291
|
+
const childData = { ...child };
|
|
292
|
+
if (remoteFK.fields.length === 1) childData[remoteFK.fields[0]] = parentIds[i];
|
|
293
|
+
allChildren.push(childData);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
if (allChildren.length === 0) continue;
|
|
297
|
+
await wrapNestedError(navField, () => from.targetTable.insertMany(allChildren, {
|
|
298
|
+
maxDepth,
|
|
299
|
+
_depth: depth + 1
|
|
300
|
+
}));
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
/**
|
|
304
|
+
* Batch-creates VIA (M:N) targets and junction entries after the main insert.
|
|
305
|
+
*/
|
|
306
|
+
async function batchInsertNestedVia(host, originals, parentIds, maxDepth, depth) {
|
|
307
|
+
for (const [navField, relation] of host._meta.relations) {
|
|
308
|
+
if (relation.direction !== "via" || !hasNavValue(originals, navField)) continue;
|
|
309
|
+
const via = resolveViaRelation(host, relation);
|
|
310
|
+
if (!via) continue;
|
|
311
|
+
for (let i = 0; i < originals.length; i++) {
|
|
312
|
+
const targets = originals[i][navField];
|
|
313
|
+
const parentPK = parentIds[i];
|
|
314
|
+
if (!Array.isArray(targets) || targets.length === 0 || parentPK === void 0) continue;
|
|
315
|
+
await viaLinkTargets(via, targets, parentPK, maxDepth, depth);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
/**
|
|
320
|
+
* Batch-replaces TO dependencies before the main replace.
|
|
321
|
+
*/
|
|
322
|
+
async function batchReplaceNestedTo(host, items, maxDepth, depth) {
|
|
323
|
+
for (const [navField, relation] of host._meta.relations) {
|
|
324
|
+
if (relation.direction !== "to" || !hasNavValue(items, navField)) continue;
|
|
325
|
+
const to = resolveToRelation(host, relation);
|
|
326
|
+
if (!to) continue;
|
|
327
|
+
const { parents, sourceIndices } = nestedToObjects(items, navField);
|
|
328
|
+
if (parents.length === 0) continue;
|
|
329
|
+
await to.targetTable.bulkReplace(parents, {
|
|
330
|
+
maxDepth,
|
|
331
|
+
_depth: depth + 1
|
|
332
|
+
});
|
|
333
|
+
if (to.fk.localFields.length === 1 && to.fk.targetFields.length === 1) {
|
|
334
|
+
const fkField = to.fk.localFields[0];
|
|
335
|
+
const targetField = to.fk.targetFields[0];
|
|
336
|
+
for (let j = 0; j < sourceIndices.length; j++) items[sourceIndices[j]][fkField] = parents[j][targetField];
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
/**
|
|
341
|
+
* Batch-replaces FROM dependents after the main replace. `plan` is the
|
|
342
|
+
* {@link planNestedFromVia} result for `originals` (`mode: "replace"`);
|
|
343
|
+
* omitted, the plan is made here first.
|
|
344
|
+
*/
|
|
345
|
+
async function batchReplaceNestedFrom(host, originals, maxDepth, depth, plan) {
|
|
346
|
+
plan ??= await planNestedFromVia(host, originals, "replace");
|
|
347
|
+
const parentPKField = parentPKFieldOf(host);
|
|
348
|
+
if (parentPKField === void 0) return;
|
|
349
|
+
for (const [navField, relation] of host._meta.relations) {
|
|
350
|
+
if (relation.direction !== "from" || !hasNavValue(originals, navField)) continue;
|
|
351
|
+
const target = fromWriteTarget(host, relation, navField, plan, maxDepth, depth);
|
|
352
|
+
if (!target) continue;
|
|
353
|
+
for (const original of originals) {
|
|
354
|
+
const children = original[navField];
|
|
355
|
+
const parentPK = original[parentPKField];
|
|
356
|
+
if (Array.isArray(children) && parentPK !== void 0) await fromReplace(target, children, parentPK);
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
/**
|
|
361
|
+
* Handles VIA (M:N) relations during replace.
|
|
362
|
+
*/
|
|
363
|
+
async function batchReplaceNestedVia(host, originals, maxDepth, depth) {
|
|
364
|
+
const parentPKField = parentPKFieldOf(host);
|
|
365
|
+
if (parentPKField === void 0) return;
|
|
366
|
+
for (const [navField, relation] of host._meta.relations) {
|
|
367
|
+
if (relation.direction !== "via" || !hasNavValue(originals, navField)) continue;
|
|
368
|
+
const via = resolveViaRelation(host, relation);
|
|
369
|
+
if (!via) continue;
|
|
370
|
+
for (const original of originals) {
|
|
371
|
+
const targets = original[navField];
|
|
372
|
+
const parentPK = original[parentPKField];
|
|
373
|
+
if (Array.isArray(targets) && parentPK !== void 0) await viaReplace(via, targets, parentPK, maxDepth, depth);
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
/**
|
|
378
|
+
* Validates the FROM and VIA nested writes of a batch BEFORE the main write
|
|
379
|
+
* (since 0.1.143), so a rejected nested operation never leaves the main
|
|
380
|
+
* write applied — even on adapters whose transaction is a pass-through. One
|
|
381
|
+
* read per relation for the whole batch; the entries are evaluated in memory
|
|
382
|
+
* in apply order (`$replace` → `$update` → `$upsert`). `mode: "patch"`
|
|
383
|
+
* checks the operators, `mode: "replace"` the plain arrays of a PUT:
|
|
384
|
+
* - FROM: a keyed child that exists under another parent (or none) → `CONFLICT`;
|
|
385
|
+
* a `$update` entry without the child PK → `NOT_FOUND`;
|
|
386
|
+
* - VIA: a keyed `$update` / `$upsert` target not linked to the parent →
|
|
387
|
+
* `CONFLICT`; a `$update` entry without the target PK → `NOT_FOUND`.
|
|
388
|
+
*
|
|
389
|
+
* The apply phases write the planned FROM children through filters
|
|
390
|
+
* constrained to their parent (`{ pk, fk: parentPK }`), so a child
|
|
391
|
+
* re-parented between the plan and the write is never touched.
|
|
392
|
+
*/
|
|
393
|
+
async function planNestedFromVia(host, originals, mode) {
|
|
394
|
+
const owned = /* @__PURE__ */ new Map();
|
|
395
|
+
const parentPKField = parentPKFieldOf(host);
|
|
396
|
+
if (parentPKField === void 0) return { owned };
|
|
397
|
+
for (const [navField, relation] of host._meta.relations) {
|
|
398
|
+
if (relation.direction === "to" || !hasNavValue(originals, navField)) continue;
|
|
399
|
+
if (relation.direction === "from") {
|
|
400
|
+
const from = resolveFromRelation(host, relation);
|
|
401
|
+
if (!from?.remoteFK || from.remoteFK.fields.length !== 1) continue;
|
|
402
|
+
owned.set(navField, await checkFromOwnership(from.targetTable, from.remoteFK.fields[0], originals, parentPKField, navField, mode));
|
|
403
|
+
continue;
|
|
404
|
+
}
|
|
405
|
+
const via = mode === "patch" ? resolveViaRelation(host, relation) : void 0;
|
|
406
|
+
if (via) await checkViaLinks(via, originals, parentPKField, navField);
|
|
407
|
+
}
|
|
408
|
+
return { owned };
|
|
409
|
+
}
|
|
410
|
+
/**
|
|
411
|
+
* Plans the TO relation patches of a batch BEFORE the main patch (since
|
|
412
|
+
* 0.1.143). A nested TO object always patches the row the STORED foreign key
|
|
413
|
+
* references. `targets[i]` is item `i`'s stored row (at least its foreign-key
|
|
414
|
+
* columns; `null` = missing) as the table pinned it inside the transaction;
|
|
415
|
+
* omitted, each item's row is read here. Rejected with `INVALID_QUERY`
|
|
416
|
+
* (nothing written): a payload that changes that foreign key AND carries a
|
|
417
|
+
* nested object for the same relation, and a nested object whose own key
|
|
418
|
+
* names a different row. An item whose source row does not exist is skipped
|
|
419
|
+
* (the main patch matches nothing for it). A stored `null` foreign key (or a
|
|
420
|
+
* composite one) → `FK_VIOLATION`.
|
|
421
|
+
*/
|
|
422
|
+
async function planPatchNestedTo(host, items, targets) {
|
|
423
|
+
const plans = [];
|
|
424
|
+
for (const [navField, relation] of host._meta.relations) {
|
|
425
|
+
if (relation.direction !== "to" || !hasNavValue(items, navField)) continue;
|
|
426
|
+
const to = resolveToRelation(host, relation);
|
|
427
|
+
if (!to) continue;
|
|
428
|
+
const { parents, sourceIndices } = nestedToObjects(items, navField);
|
|
429
|
+
if (parents.length === 0) continue;
|
|
430
|
+
const fkField = to.fk.localFields[0];
|
|
431
|
+
if (to.fk.localFields.length !== 1) throw new require_db_error.DbError("FK_VIOLATION", [{
|
|
432
|
+
path: fkField,
|
|
433
|
+
message: `Cannot patch relation '${navField}' — foreign key '${fkField}' is null`
|
|
434
|
+
}]);
|
|
435
|
+
const targetField = to.fk.targetFields.length === 1 ? to.fk.targetFields[0] : void 0;
|
|
436
|
+
const entries = [];
|
|
437
|
+
for (let j = 0; j < parents.length; j++) {
|
|
438
|
+
const index = sourceIndices[j];
|
|
439
|
+
const item = items[index];
|
|
440
|
+
const current = targets ? targets[index] : await host.findOne({
|
|
441
|
+
filter: host._extractRecordFilter(item),
|
|
442
|
+
controls: { $select: [fkField] }
|
|
443
|
+
});
|
|
444
|
+
if (!current) continue;
|
|
445
|
+
const stored = current[fkField];
|
|
446
|
+
const payloadFk = item[fkField];
|
|
447
|
+
if (payloadFk !== void 0 && !sameKey(payloadFk, stored)) throw new require_db_error.DbError("INVALID_QUERY", [{
|
|
448
|
+
path: fkField,
|
|
449
|
+
message: `Cannot change '${fkField}' and patch relation '${navField}' in one payload — the nested patch targets the currently referenced record`
|
|
450
|
+
}]);
|
|
451
|
+
if (stored === null || stored === void 0) throw new require_db_error.DbError("FK_VIOLATION", [{
|
|
452
|
+
path: fkField,
|
|
453
|
+
message: `Cannot patch relation '${navField}' — foreign key '${fkField}' is null`
|
|
454
|
+
}]);
|
|
455
|
+
const patch = { ...parents[j] };
|
|
456
|
+
if (targetField !== void 0) {
|
|
457
|
+
const nestedKey = patch[targetField];
|
|
458
|
+
if (nestedKey !== void 0 && !sameKey(nestedKey, stored)) throw new require_db_error.DbError("INVALID_QUERY", [{
|
|
459
|
+
path: `${navField}.${targetField}`,
|
|
460
|
+
message: `Cannot patch relation '${navField}' — '${targetField}' does not match the currently referenced record`
|
|
461
|
+
}]);
|
|
462
|
+
patch[targetField] = stored;
|
|
463
|
+
}
|
|
464
|
+
entries.push({
|
|
465
|
+
index,
|
|
466
|
+
patch
|
|
467
|
+
});
|
|
468
|
+
}
|
|
469
|
+
if (entries.length > 0) plans.push({
|
|
470
|
+
navField,
|
|
471
|
+
targetTable: to.targetTable,
|
|
472
|
+
entries
|
|
473
|
+
});
|
|
474
|
+
}
|
|
475
|
+
return plans;
|
|
476
|
+
}
|
|
477
|
+
/**
|
|
478
|
+
* Applies the TO patches {@link planPatchNestedTo} planned — after the main
|
|
479
|
+
* patch, only for the items it matched (`matched[i]`; all when omitted).
|
|
480
|
+
*/
|
|
481
|
+
async function applyPatchNestedTo(plans, maxDepth, depth, matched) {
|
|
482
|
+
for (const plan of plans) {
|
|
483
|
+
const patches = plan.entries.filter((e) => !matched || matched[e.index]).map((e) => e.patch);
|
|
484
|
+
if (patches.length === 0) continue;
|
|
485
|
+
await plan.targetTable.bulkUpdate(patches, {
|
|
486
|
+
maxDepth,
|
|
487
|
+
_depth: depth + 1
|
|
488
|
+
});
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
/**
|
|
492
|
+
* Batch-patches TO dependencies: {@link planPatchNestedTo} +
|
|
493
|
+
* {@link applyPatchNestedTo} in one step (the table itself plans before and
|
|
494
|
+
* applies after its main patch).
|
|
495
|
+
*/
|
|
496
|
+
async function batchPatchNestedTo(host, items, maxDepth, depth) {
|
|
497
|
+
await applyPatchNestedTo(await planPatchNestedTo(host, items), maxDepth, depth);
|
|
498
|
+
}
|
|
499
|
+
/**
|
|
500
|
+
* Batch-patches FROM (1:N) dependencies after the main patch.
|
|
501
|
+
* Supports patch operators: $replace, $insert, $remove, $update, $upsert.
|
|
502
|
+
* `plan` is the {@link planNestedFromVia} result for `originals`
|
|
503
|
+
* (`mode: "patch"`); omitted, the plan is made here first.
|
|
504
|
+
*/
|
|
505
|
+
async function batchPatchNestedFrom(host, originals, maxDepth, depth, plan) {
|
|
506
|
+
plan ??= await planNestedFromVia(host, originals, "patch");
|
|
507
|
+
const parentPKField = parentPKFieldOf(host);
|
|
508
|
+
if (parentPKField === void 0) return;
|
|
509
|
+
for (const [navField, relation] of host._meta.relations) {
|
|
510
|
+
if (relation.direction !== "from" || !hasNavValue(originals, navField)) continue;
|
|
511
|
+
const target = fromWriteTarget(host, relation, navField, plan, maxDepth, depth);
|
|
512
|
+
if (!target) continue;
|
|
513
|
+
const { targetTable, fkField, childPKs } = target;
|
|
514
|
+
for (const original of originals) {
|
|
515
|
+
const navValue = original[navField];
|
|
516
|
+
const parentPK = original[parentPKField];
|
|
517
|
+
if (navValue === void 0 || navValue === null || parentPK === void 0) continue;
|
|
518
|
+
const ops = extractNavPatchOps(navValue);
|
|
519
|
+
if (ops.replace) await fromReplace(target, ops.replace, parentPK);
|
|
520
|
+
if (ops.remove && ops.remove.length > 0) {
|
|
521
|
+
const removeFilters = ops.remove.map((child) => {
|
|
522
|
+
const rec = child;
|
|
523
|
+
const f = {};
|
|
524
|
+
for (const pk of childPKs) f[pk] = rec[pk];
|
|
525
|
+
f[fkField] = parentPK;
|
|
526
|
+
return f;
|
|
527
|
+
});
|
|
528
|
+
await targetTable.deleteMany(orFilter(removeFilters));
|
|
529
|
+
}
|
|
530
|
+
if (ops.update && ops.update.length > 0) await fromUpdate(target, withField(ops.update, fkField, parentPK));
|
|
531
|
+
if (ops.upsert) {
|
|
532
|
+
const toUpdate = [];
|
|
533
|
+
const toInsert = [];
|
|
534
|
+
for (const child of ops.upsert) {
|
|
535
|
+
const rec = {
|
|
536
|
+
...child,
|
|
537
|
+
[fkField]: parentPK
|
|
538
|
+
};
|
|
539
|
+
(isKeyed(rec, childPKs) ? toUpdate : toInsert).push(rec);
|
|
540
|
+
}
|
|
541
|
+
if (toUpdate.length > 0) await fromUpdate(target, toUpdate);
|
|
542
|
+
if (toInsert.length > 0) await wrapNestedError(navField, () => targetTable.insertMany(toInsert, {
|
|
543
|
+
maxDepth,
|
|
544
|
+
_depth: depth + 1
|
|
545
|
+
}));
|
|
546
|
+
}
|
|
547
|
+
if (ops.insert && ops.insert.length > 0) {
|
|
548
|
+
const items = withField(ops.insert, fkField, parentPK);
|
|
549
|
+
await wrapNestedError(navField, () => targetTable.insertMany(items, {
|
|
550
|
+
maxDepth,
|
|
551
|
+
_depth: depth + 1
|
|
552
|
+
}));
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
/**
|
|
558
|
+
* Batch-patches VIA (M:N) dependencies after the main patch.
|
|
559
|
+
* Supports patch operators: $replace, $insert, $remove, $update, $upsert.
|
|
560
|
+
* `plan` marks `originals` as already validated by {@link planNestedFromVia}
|
|
561
|
+
* (`mode: "patch"`); omitted, they are validated here first.
|
|
562
|
+
*/
|
|
563
|
+
async function batchPatchNestedVia(host, originals, maxDepth, depth, plan) {
|
|
564
|
+
if (!plan) await planNestedFromVia(host, originals, "patch");
|
|
565
|
+
const parentPKField = parentPKFieldOf(host);
|
|
566
|
+
if (parentPKField === void 0) return;
|
|
567
|
+
for (const [navField, relation] of host._meta.relations) {
|
|
568
|
+
if (relation.direction !== "via" || !hasNavValue(originals, navField)) continue;
|
|
569
|
+
const via = resolveViaRelation(host, relation);
|
|
570
|
+
if (!via) continue;
|
|
571
|
+
const { targetTable, junctionTable, fkToThis, fkToTarget, targetPKField } = via;
|
|
572
|
+
for (const original of originals) {
|
|
573
|
+
const navValue = original[navField];
|
|
574
|
+
const parentPK = original[parentPKField];
|
|
575
|
+
if (navValue === void 0 || navValue === null || parentPK === void 0) continue;
|
|
576
|
+
const ops = extractNavPatchOps(navValue);
|
|
577
|
+
if (ops.replace) await viaReplace(via, ops.replace, parentPK, maxDepth, depth);
|
|
578
|
+
if (ops.remove && ops.remove.length > 0) {
|
|
579
|
+
const targetPKs = ops.remove.map((t) => t[targetPKField]).filter((pk) => pk !== void 0 && pk !== null);
|
|
580
|
+
if (targetPKs.length > 0) await junctionTable.deleteMany({
|
|
581
|
+
[fkToThis]: parentPK,
|
|
582
|
+
[fkToTarget]: targetPKs.length === 1 ? targetPKs[0] : { $in: targetPKs }
|
|
583
|
+
});
|
|
584
|
+
}
|
|
585
|
+
if (ops.update && ops.update.length > 0) {
|
|
586
|
+
const toUpdate = ops.update.map((t) => ({ ...t }));
|
|
587
|
+
await targetTable.bulkUpdate(toUpdate, {
|
|
588
|
+
maxDepth,
|
|
589
|
+
_depth: depth + 1
|
|
590
|
+
});
|
|
591
|
+
}
|
|
592
|
+
if (ops.upsert && ops.upsert.length > 0) {
|
|
593
|
+
const toUpdate = [];
|
|
594
|
+
const toInsert = [];
|
|
595
|
+
for (const target of ops.upsert) {
|
|
596
|
+
const rec = { ...target };
|
|
597
|
+
const pk = rec[targetPKField];
|
|
598
|
+
(pk !== void 0 && pk !== null ? toUpdate : toInsert).push(rec);
|
|
599
|
+
}
|
|
600
|
+
if (toUpdate.length > 0) await targetTable.bulkUpdate(toUpdate, {
|
|
601
|
+
maxDepth,
|
|
602
|
+
_depth: depth + 1
|
|
603
|
+
});
|
|
604
|
+
if (toInsert.length > 0) await viaLinkTargets(via, toInsert, parentPK, maxDepth, depth);
|
|
605
|
+
}
|
|
606
|
+
if (ops.insert && ops.insert.length > 0) await viaLinkTargets(via, ops.insert, parentPK, maxDepth, depth);
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
/**
|
|
611
|
+
* Extracts patch operations from a nav field value.
|
|
612
|
+
* Plain array → $replace. Object with $insert, $remove, etc. → individual ops.
|
|
613
|
+
*/
|
|
614
|
+
function extractNavPatchOps(navValue) {
|
|
615
|
+
if (Array.isArray(navValue)) return { replace: navValue };
|
|
616
|
+
if (typeof navValue !== "object" || navValue === null) return {};
|
|
617
|
+
const obj = navValue;
|
|
618
|
+
return {
|
|
619
|
+
replace: obj.$replace !== void 0 ? obj.$replace : void 0,
|
|
620
|
+
insert: obj.$insert !== void 0 ? obj.$insert : void 0,
|
|
621
|
+
remove: obj.$remove !== void 0 ? obj.$remove : void 0,
|
|
622
|
+
update: obj.$update !== void 0 ? obj.$update : void 0,
|
|
623
|
+
upsert: obj.$upsert !== void 0 ? obj.$upsert : void 0
|
|
624
|
+
};
|
|
625
|
+
}
|
|
626
|
+
/** `filters` as one filter — a lone filter as-is, several `$or`-ed. */
|
|
627
|
+
function orFilter(filters) {
|
|
628
|
+
return filters.length === 1 ? filters[0] : { $or: filters };
|
|
629
|
+
}
|
|
630
|
+
/** Copies of `entries` with `field` set to `value` (a child's foreign key to its parent). */
|
|
631
|
+
function withField(entries, field, value) {
|
|
632
|
+
const out = [];
|
|
633
|
+
for (const entry of entries) out.push({
|
|
634
|
+
...entry,
|
|
635
|
+
[field]: value
|
|
636
|
+
});
|
|
637
|
+
return out;
|
|
638
|
+
}
|
|
639
|
+
/** Identity of a junction link (parent key, target key). */
|
|
640
|
+
function linkKey(parentPK, targetPK) {
|
|
641
|
+
return `${keyString(parentPK)}\0${keyString(targetPK)}`;
|
|
642
|
+
}
|
|
643
|
+
/** Whether `rec` carries every primary-key field (a table without one keys nothing). */
|
|
644
|
+
function isKeyed(rec, pkFields) {
|
|
645
|
+
return pkFields.length > 0 && pkFields.every((pk) => rec[pk] !== void 0);
|
|
646
|
+
}
|
|
647
|
+
/** The primary-key equality filter of a keyed entry. */
|
|
648
|
+
function keyFilter(rec, pkFields) {
|
|
649
|
+
const f = {};
|
|
650
|
+
for (const pk of pkFields) f[pk] = rec[pk];
|
|
651
|
+
return f;
|
|
652
|
+
}
|
|
653
|
+
/**
|
|
654
|
+
* Nested `$update` entries are identified by the related table's primary key
|
|
655
|
+
* only (since 0.1.143) — a unique key could name a row outside the relation.
|
|
656
|
+
*/
|
|
657
|
+
function requireKeys(entries, pkFields, navField, op) {
|
|
658
|
+
for (let i = 0; i < entries.length; i++) {
|
|
659
|
+
const rec = entries[i];
|
|
660
|
+
const missing = pkFields.find((pk) => rec[pk] === void 0 || rec[pk] === null);
|
|
661
|
+
if (missing !== void 0) throw new require_db_error.DbError("NOT_FOUND", [{
|
|
662
|
+
path: `${navField}.${op}[${i}].${missing}`,
|
|
663
|
+
message: `Missing primary key field "${missing}" in '${navField}' ${op} entry`
|
|
664
|
+
}]);
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
/**
|
|
668
|
+
* FROM half of {@link planNestedFromVia}: ONE read of every keyed child the
|
|
669
|
+
* batch names; a child that exists under a different parent (or none) →
|
|
670
|
+
* `CONFLICT`. Keys that match no row pass (the usual "nothing matched"
|
|
671
|
+
* semantics apply). Returns the keys of the children found under their parent.
|
|
672
|
+
*/
|
|
673
|
+
async function checkFromOwnership(targetTable, fkField, originals, parentPKField, navField, mode) {
|
|
674
|
+
const childPKs = targetTable.primaryKeys;
|
|
675
|
+
const claims = [];
|
|
676
|
+
const claim = (entries, parentPK, op) => {
|
|
677
|
+
for (const entry of entries) {
|
|
678
|
+
const child = entry;
|
|
679
|
+
if (isKeyed(child, childPKs)) claims.push({
|
|
680
|
+
parentPK,
|
|
681
|
+
child,
|
|
682
|
+
op
|
|
683
|
+
});
|
|
684
|
+
}
|
|
685
|
+
};
|
|
686
|
+
for (const original of originals) {
|
|
687
|
+
const parentPK = original[parentPKField];
|
|
688
|
+
const navValue = original[navField];
|
|
689
|
+
if (parentPK === void 0 || navValue === void 0 || navValue === null) continue;
|
|
690
|
+
if (mode === "replace") {
|
|
691
|
+
if (Array.isArray(navValue)) claim(navValue, parentPK, "replace");
|
|
692
|
+
continue;
|
|
693
|
+
}
|
|
694
|
+
const ops = extractNavPatchOps(navValue);
|
|
695
|
+
if (ops.replace) claim(ops.replace, parentPK, "replace");
|
|
696
|
+
if (ops.update && ops.update.length > 0) {
|
|
697
|
+
requireKeys(ops.update, childPKs, navField, "$update");
|
|
698
|
+
claim(ops.update, parentPK, "$update");
|
|
699
|
+
}
|
|
700
|
+
if (ops.upsert) claim(ops.upsert, parentPK, "$upsert");
|
|
701
|
+
}
|
|
702
|
+
const owned = /* @__PURE__ */ new Set();
|
|
703
|
+
if (claims.length === 0) return owned;
|
|
704
|
+
const rows = await targetTable.findMany({
|
|
705
|
+
filter: orFilter(claims.map((c) => keyFilter(c.child, childPKs))),
|
|
706
|
+
controls: { $select: [...childPKs, fkField] }
|
|
707
|
+
});
|
|
708
|
+
const parentOf = /* @__PURE__ */ new Map();
|
|
709
|
+
for (const row of rows) parentOf.set(pkTupleKey(row, childPKs), row[fkField]);
|
|
710
|
+
for (const { parentPK, child, op } of claims) {
|
|
711
|
+
const key = pkTupleKey(child, childPKs);
|
|
712
|
+
if (!parentOf.has(key)) continue;
|
|
713
|
+
if (!sameKey(parentOf.get(key), parentPK)) throw new require_db_error.DbError("CONFLICT", [{
|
|
714
|
+
path: navField,
|
|
715
|
+
message: `Cannot ${op} '${navField}' — record [${childPKs.map((pk) => keyString(child[pk])).join(", ")}] is not a child of this record`
|
|
716
|
+
}]);
|
|
717
|
+
owned.add(key);
|
|
718
|
+
}
|
|
719
|
+
return owned;
|
|
720
|
+
}
|
|
721
|
+
/**
|
|
722
|
+
* VIA half of {@link planNestedFromVia}: ONE junction read for every keyed
|
|
723
|
+
* `$update` / `$upsert` target of the batch; a target not currently linked
|
|
724
|
+
* to its parent → `CONFLICT` (since 0.1.143) — whether or not the target
|
|
725
|
+
* exists. Linking an existing target is `$insert`'s job.
|
|
726
|
+
*/
|
|
727
|
+
async function checkViaLinks(via, originals, parentPKField, navField) {
|
|
728
|
+
const { junctionTable, fkToThis, fkToTarget, targetPKField } = via;
|
|
729
|
+
const claims = [];
|
|
730
|
+
for (const original of originals) {
|
|
731
|
+
const parentPK = original[parentPKField];
|
|
732
|
+
const navValue = original[navField];
|
|
733
|
+
if (parentPK === void 0 || navValue === void 0 || navValue === null) continue;
|
|
734
|
+
const ops = extractNavPatchOps(navValue);
|
|
735
|
+
if (ops.update && ops.update.length > 0) {
|
|
736
|
+
requireKeys(ops.update, [targetPKField], navField, "$update");
|
|
737
|
+
for (const t of ops.update) claims.push({
|
|
738
|
+
parentPK,
|
|
739
|
+
targetPK: t[targetPKField],
|
|
740
|
+
op: "$update"
|
|
741
|
+
});
|
|
742
|
+
}
|
|
743
|
+
for (const t of ops.upsert ?? []) {
|
|
744
|
+
const targetPK = t[targetPKField];
|
|
745
|
+
if (targetPK !== void 0 && targetPK !== null) claims.push({
|
|
746
|
+
parentPK,
|
|
747
|
+
targetPK,
|
|
748
|
+
op: "$upsert"
|
|
749
|
+
});
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
if (claims.length === 0) return;
|
|
753
|
+
const links = await junctionTable.findMany({
|
|
754
|
+
filter: orFilter(claims.map((c) => ({
|
|
755
|
+
[fkToThis]: c.parentPK,
|
|
756
|
+
[fkToTarget]: c.targetPK
|
|
757
|
+
}))),
|
|
758
|
+
controls: { $select: [fkToThis, fkToTarget] }
|
|
759
|
+
});
|
|
760
|
+
const linked = new Set(links.map((j) => linkKey(j[fkToThis], j[fkToTarget])));
|
|
761
|
+
const unlinked = claims.find((c) => !linked.has(linkKey(c.parentPK, c.targetPK)));
|
|
762
|
+
if (unlinked) throw new require_db_error.DbError("CONFLICT", [{
|
|
763
|
+
path: navField,
|
|
764
|
+
message: `Cannot ${unlinked.op} '${navField}' — record [${keyString(unlinked.targetPK)}] is not linked to this record (link it with $insert first)`
|
|
765
|
+
}]);
|
|
766
|
+
}
|
|
767
|
+
function fromWriteTarget(host, relation, navField, plan, maxDepth, depth) {
|
|
768
|
+
const from = resolveFromRelation(host, relation);
|
|
769
|
+
if (!from?.remoteFK || from.remoteFK.fields.length !== 1) return void 0;
|
|
770
|
+
return {
|
|
771
|
+
targetTable: from.targetTable,
|
|
772
|
+
navField,
|
|
773
|
+
fkField: from.remoteFK.fields[0],
|
|
774
|
+
childPKs: from.targetTable.primaryKeys,
|
|
775
|
+
owned: plan.owned.get(navField) ?? /* @__PURE__ */ new Set(),
|
|
776
|
+
maxDepth,
|
|
777
|
+
depth
|
|
778
|
+
};
|
|
779
|
+
}
|
|
780
|
+
/**
|
|
781
|
+
* Write options that pin each child to its parent: the child table ANDs
|
|
782
|
+
* `{ fk: <item's fk> }` into every row filter (the foreign key is never
|
|
783
|
+
* SET — a child is never re-parented), and a planned-owned child that no
|
|
784
|
+
* longer matches → `CONFLICT` (rolled back).
|
|
785
|
+
*/
|
|
786
|
+
function ownedWriteOpts(target, items) {
|
|
787
|
+
return {
|
|
788
|
+
maxDepth: target.maxDepth,
|
|
789
|
+
_depth: target.depth + 1,
|
|
790
|
+
_ownedBy: {
|
|
791
|
+
field: target.fkField,
|
|
792
|
+
strict: items.map((item) => target.owned.has(pkTupleKey(item, target.childPKs)))
|
|
793
|
+
}
|
|
794
|
+
};
|
|
795
|
+
}
|
|
796
|
+
/** FROM `$update` / keyed `$upsert`: patch the parent's own children. */
|
|
797
|
+
async function fromUpdate(target, items) {
|
|
798
|
+
await wrapNestedError(target.navField, () => target.targetTable.bulkUpdate(items, ownedWriteOpts(target, items)));
|
|
799
|
+
}
|
|
800
|
+
/**
|
|
801
|
+
* FROM $replace helper: delete the parent's orphans, replace its keyed
|
|
802
|
+
* children, insert the rest.
|
|
803
|
+
*/
|
|
804
|
+
async function fromReplace(target, children, parentPK) {
|
|
805
|
+
const { targetTable, navField, fkField, childPKs, maxDepth, depth } = target;
|
|
806
|
+
const toReplace = [];
|
|
807
|
+
const toInsert = [];
|
|
808
|
+
const keep = /* @__PURE__ */ new Set();
|
|
809
|
+
for (const child of children) {
|
|
810
|
+
const childData = {
|
|
811
|
+
...child,
|
|
812
|
+
[fkField]: parentPK
|
|
813
|
+
};
|
|
814
|
+
if (isKeyed(childData, childPKs)) {
|
|
815
|
+
keep.add(pkTupleKey(childData, childPKs));
|
|
816
|
+
toReplace.push(childData);
|
|
817
|
+
} else toInsert.push(childData);
|
|
818
|
+
}
|
|
819
|
+
const existing = await targetTable.findMany({
|
|
820
|
+
filter: { [fkField]: parentPK },
|
|
821
|
+
controls: childPKs.length > 0 ? { $select: [...childPKs] } : {}
|
|
822
|
+
});
|
|
823
|
+
const orphanFilters = [];
|
|
824
|
+
for (const row of existing) if (!keep.has(pkTupleKey(row, childPKs))) orphanFilters.push({
|
|
825
|
+
...keyFilter(row, childPKs),
|
|
826
|
+
[fkField]: parentPK
|
|
827
|
+
});
|
|
828
|
+
if (orphanFilters.length > 0) await targetTable.deleteMany(orFilter(orphanFilters));
|
|
829
|
+
if (toReplace.length > 0) await wrapNestedError(navField, () => targetTable.bulkReplace(toReplace, ownedWriteOpts(target, toReplace)));
|
|
830
|
+
if (toInsert.length > 0) await wrapNestedError(navField, () => targetTable.insertMany(toInsert, {
|
|
831
|
+
maxDepth,
|
|
832
|
+
_depth: depth + 1
|
|
833
|
+
}));
|
|
834
|
+
}
|
|
835
|
+
/**
|
|
836
|
+
* Links `targets` to the parent: keyed entries by their key, unkeyed ones
|
|
837
|
+
* inserted first — then one junction insert.
|
|
838
|
+
*/
|
|
839
|
+
async function viaLinkTargets(via, targets, parentPK, maxDepth, depth) {
|
|
840
|
+
const { targetTable, junctionTable, fkToThis, fkToTarget, targetPKField } = via;
|
|
841
|
+
const toInsert = [];
|
|
842
|
+
const targetIds = [];
|
|
843
|
+
for (const t of targets) {
|
|
844
|
+
const rec = t;
|
|
845
|
+
const pk = rec[targetPKField];
|
|
846
|
+
if (pk !== void 0 && pk !== null) targetIds.push(pk);
|
|
847
|
+
else toInsert.push({ ...rec });
|
|
848
|
+
}
|
|
849
|
+
if (toInsert.length > 0) {
|
|
850
|
+
const result = await targetTable.insertMany(toInsert, {
|
|
851
|
+
maxDepth,
|
|
852
|
+
_depth: depth + 1
|
|
853
|
+
});
|
|
854
|
+
targetIds.push(...result.insertedIds);
|
|
855
|
+
}
|
|
856
|
+
if (targetIds.length > 0) await junctionTable.insertMany(targetIds.map((targetId) => ({
|
|
857
|
+
[fkToThis]: parentPK,
|
|
858
|
+
[fkToTarget]: targetId
|
|
859
|
+
})), { maxDepth: 0 });
|
|
860
|
+
}
|
|
861
|
+
/**
|
|
862
|
+
* VIA $replace helper: clear junctions, replace/insert targets, rebuild junctions.
|
|
863
|
+
*/
|
|
864
|
+
async function viaReplace(via, targets, parentPK, maxDepth, depth) {
|
|
865
|
+
const { targetTable, junctionTable, fkToThis, targetPKField } = via;
|
|
866
|
+
await junctionTable.deleteMany({ [fkToThis]: parentPK });
|
|
867
|
+
const toReplace = [];
|
|
868
|
+
for (const t of targets) {
|
|
869
|
+
const rec = t;
|
|
870
|
+
const pk = rec[targetPKField];
|
|
871
|
+
if (pk !== void 0 && pk !== null && Object.keys(rec).some((k) => k !== targetPKField)) toReplace.push({ ...rec });
|
|
872
|
+
}
|
|
873
|
+
if (toReplace.length > 0) await targetTable.bulkReplace(toReplace, {
|
|
874
|
+
maxDepth,
|
|
875
|
+
_depth: depth + 1
|
|
876
|
+
});
|
|
877
|
+
await viaLinkTargets(via, targets, parentPK, maxDepth, depth);
|
|
878
|
+
}
|
|
879
|
+
//#endregion
|
|
880
|
+
Object.defineProperty(exports, "applyPatchNestedTo", {
|
|
881
|
+
enumerable: true,
|
|
882
|
+
get: function() {
|
|
883
|
+
return applyPatchNestedTo;
|
|
884
|
+
}
|
|
885
|
+
});
|
|
886
|
+
Object.defineProperty(exports, "batchInsertNestedFrom", {
|
|
887
|
+
enumerable: true,
|
|
888
|
+
get: function() {
|
|
889
|
+
return batchInsertNestedFrom;
|
|
890
|
+
}
|
|
891
|
+
});
|
|
892
|
+
Object.defineProperty(exports, "batchInsertNestedTo", {
|
|
893
|
+
enumerable: true,
|
|
894
|
+
get: function() {
|
|
895
|
+
return batchInsertNestedTo;
|
|
896
|
+
}
|
|
897
|
+
});
|
|
898
|
+
Object.defineProperty(exports, "batchInsertNestedVia", {
|
|
899
|
+
enumerable: true,
|
|
900
|
+
get: function() {
|
|
901
|
+
return batchInsertNestedVia;
|
|
902
|
+
}
|
|
903
|
+
});
|
|
904
|
+
Object.defineProperty(exports, "batchPatchNestedFrom", {
|
|
905
|
+
enumerable: true,
|
|
906
|
+
get: function() {
|
|
907
|
+
return batchPatchNestedFrom;
|
|
908
|
+
}
|
|
909
|
+
});
|
|
910
|
+
Object.defineProperty(exports, "batchPatchNestedTo", {
|
|
911
|
+
enumerable: true,
|
|
912
|
+
get: function() {
|
|
913
|
+
return batchPatchNestedTo;
|
|
914
|
+
}
|
|
915
|
+
});
|
|
916
|
+
Object.defineProperty(exports, "batchPatchNestedVia", {
|
|
917
|
+
enumerable: true,
|
|
918
|
+
get: function() {
|
|
919
|
+
return batchPatchNestedVia;
|
|
920
|
+
}
|
|
921
|
+
});
|
|
922
|
+
Object.defineProperty(exports, "batchReplaceNestedFrom", {
|
|
923
|
+
enumerable: true,
|
|
924
|
+
get: function() {
|
|
925
|
+
return batchReplaceNestedFrom;
|
|
926
|
+
}
|
|
927
|
+
});
|
|
928
|
+
Object.defineProperty(exports, "batchReplaceNestedTo", {
|
|
929
|
+
enumerable: true,
|
|
930
|
+
get: function() {
|
|
931
|
+
return batchReplaceNestedTo;
|
|
932
|
+
}
|
|
933
|
+
});
|
|
934
|
+
Object.defineProperty(exports, "batchReplaceNestedVia", {
|
|
935
|
+
enumerable: true,
|
|
936
|
+
get: function() {
|
|
937
|
+
return batchReplaceNestedVia;
|
|
938
|
+
}
|
|
939
|
+
});
|
|
940
|
+
Object.defineProperty(exports, "checkDepthOverflow", {
|
|
941
|
+
enumerable: true,
|
|
942
|
+
get: function() {
|
|
943
|
+
return checkDepthOverflow;
|
|
944
|
+
}
|
|
945
|
+
});
|
|
946
|
+
Object.defineProperty(exports, "enrichFkViolation", {
|
|
947
|
+
enumerable: true,
|
|
948
|
+
get: function() {
|
|
949
|
+
return enrichFkViolation;
|
|
950
|
+
}
|
|
951
|
+
});
|
|
952
|
+
Object.defineProperty(exports, "findFKEntryForRelation", {
|
|
953
|
+
enumerable: true,
|
|
954
|
+
get: function() {
|
|
955
|
+
return findFKEntryForRelation;
|
|
956
|
+
}
|
|
957
|
+
});
|
|
958
|
+
Object.defineProperty(exports, "findFKForRelation", {
|
|
959
|
+
enumerable: true,
|
|
960
|
+
get: function() {
|
|
961
|
+
return findFKForRelation;
|
|
962
|
+
}
|
|
963
|
+
});
|
|
964
|
+
Object.defineProperty(exports, "findRemoteFK", {
|
|
965
|
+
enumerable: true,
|
|
966
|
+
get: function() {
|
|
967
|
+
return findRemoteFK;
|
|
968
|
+
}
|
|
969
|
+
});
|
|
970
|
+
Object.defineProperty(exports, "pkTupleKey", {
|
|
971
|
+
enumerable: true,
|
|
972
|
+
get: function() {
|
|
973
|
+
return pkTupleKey;
|
|
974
|
+
}
|
|
975
|
+
});
|
|
976
|
+
Object.defineProperty(exports, "planNestedFromVia", {
|
|
977
|
+
enumerable: true,
|
|
978
|
+
get: function() {
|
|
979
|
+
return planNestedFromVia;
|
|
980
|
+
}
|
|
981
|
+
});
|
|
982
|
+
Object.defineProperty(exports, "planPatchNestedTo", {
|
|
983
|
+
enumerable: true,
|
|
984
|
+
get: function() {
|
|
985
|
+
return planPatchNestedTo;
|
|
986
|
+
}
|
|
987
|
+
});
|
|
988
|
+
Object.defineProperty(exports, "preValidateNestedFrom", {
|
|
989
|
+
enumerable: true,
|
|
990
|
+
get: function() {
|
|
991
|
+
return preValidateNestedFrom;
|
|
992
|
+
}
|
|
993
|
+
});
|
|
994
|
+
Object.defineProperty(exports, "remapDeleteFkViolation", {
|
|
995
|
+
enumerable: true,
|
|
996
|
+
get: function() {
|
|
997
|
+
return remapDeleteFkViolation;
|
|
998
|
+
}
|
|
999
|
+
});
|
|
1000
|
+
Object.defineProperty(exports, "resolveRelationTargetTable", {
|
|
1001
|
+
enumerable: true,
|
|
1002
|
+
get: function() {
|
|
1003
|
+
return resolveRelationTargetTable;
|
|
1004
|
+
}
|
|
1005
|
+
});
|
|
1006
|
+
Object.defineProperty(exports, "rowMatchesKey", {
|
|
1007
|
+
enumerable: true,
|
|
1008
|
+
get: function() {
|
|
1009
|
+
return rowMatchesKey;
|
|
1010
|
+
}
|
|
1011
|
+
});
|
|
1012
|
+
Object.defineProperty(exports, "sameKey", {
|
|
1013
|
+
enumerable: true,
|
|
1014
|
+
get: function() {
|
|
1015
|
+
return sameKey;
|
|
1016
|
+
}
|
|
1017
|
+
});
|
|
1018
|
+
Object.defineProperty(exports, "tableNameOf", {
|
|
1019
|
+
enumerable: true,
|
|
1020
|
+
get: function() {
|
|
1021
|
+
return tableNameOf;
|
|
1022
|
+
}
|
|
1023
|
+
});
|
|
1024
|
+
Object.defineProperty(exports, "validateBatch", {
|
|
1025
|
+
enumerable: true,
|
|
1026
|
+
get: function() {
|
|
1027
|
+
return validateBatch;
|
|
1028
|
+
}
|
|
1029
|
+
});
|