@atscript/db 0.1.142 → 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
|
@@ -1,661 +0,0 @@
|
|
|
1
|
-
import { i as DepthLimitExceededError, r as DbError } from "./db-error-D5uilS_A.mjs";
|
|
2
|
-
import { ValidatorError } from "@atscript/typescript/utils";
|
|
3
|
-
//#region src/rel/relation-helpers.ts
|
|
4
|
-
/**
|
|
5
|
-
* Finds the FK entry that connects a `@db.rel.to` relation to its target.
|
|
6
|
-
*/
|
|
7
|
-
function findFKForRelation(relation, foreignKeys) {
|
|
8
|
-
const targetTable = resolveRelationTargetTable(relation);
|
|
9
|
-
for (const fk of foreignKeys.values()) if (relation.alias) {
|
|
10
|
-
if (fk.alias === relation.alias) return {
|
|
11
|
-
localFields: fk.fields,
|
|
12
|
-
targetFields: fk.targetFields
|
|
13
|
-
};
|
|
14
|
-
} else if (fk.targetTable === targetTable) return {
|
|
15
|
-
localFields: fk.fields,
|
|
16
|
-
targetFields: fk.targetFields
|
|
17
|
-
};
|
|
18
|
-
}
|
|
19
|
-
/**
|
|
20
|
-
* Finds a FK on a remote table that points back to a given table name.
|
|
21
|
-
*/
|
|
22
|
-
function findRemoteFK(targetTable, thisTableName, alias) {
|
|
23
|
-
for (const fk of targetTable.foreignKeys.values()) {
|
|
24
|
-
if (alias && fk.alias === alias && fk.targetTable === thisTableName) return fk;
|
|
25
|
-
if (!alias && fk.targetTable === thisTableName) return fk;
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
/**
|
|
29
|
-
* Physical name of an annotated type — its `@db.table`, else its `@db.view`
|
|
30
|
-
* name (a view is a readable source too, since 0.1.141), else its type id
|
|
31
|
-
* (`""` when none is set). The rule relations, foreign keys and view plans
|
|
32
|
-
* use to name a referenced table or view.
|
|
33
|
-
* @since 0.1.136
|
|
34
|
-
*/
|
|
35
|
-
function tableNameOf(type) {
|
|
36
|
-
const table = type?.metadata?.get("db.table");
|
|
37
|
-
if (table) return table;
|
|
38
|
-
const view = type?.metadata?.get("db.view");
|
|
39
|
-
return typeof view === "string" && view || type?.id || "";
|
|
40
|
-
}
|
|
41
|
-
/**
|
|
42
|
-
* Resolves the target table name from a relation's target type metadata.
|
|
43
|
-
*/
|
|
44
|
-
function resolveRelationTargetTable(relation) {
|
|
45
|
-
return tableNameOf(relation.targetType());
|
|
46
|
-
}
|
|
47
|
-
//#endregion
|
|
48
|
-
//#region src/table/error-utils.ts
|
|
49
|
-
/**
|
|
50
|
-
* Prefixes error paths with a nav field context.
|
|
51
|
-
* Ensures errors from child table operations (e.g., FK violations on a comment)
|
|
52
|
-
* get paths like `comments[0].authorId` instead of just `authorId`.
|
|
53
|
-
*/
|
|
54
|
-
function prefixErrorPaths(errors, prefix) {
|
|
55
|
-
return errors.map((err) => ({
|
|
56
|
-
...err,
|
|
57
|
-
path: err.path ? `${prefix}.${err.path}` : prefix
|
|
58
|
-
}));
|
|
59
|
-
}
|
|
60
|
-
/**
|
|
61
|
-
* Wraps an async nested operation and prefixes error paths with the nav field context.
|
|
62
|
-
*/
|
|
63
|
-
async function wrapNestedError(navField, fn) {
|
|
64
|
-
try {
|
|
65
|
-
return await fn();
|
|
66
|
-
} catch (error) {
|
|
67
|
-
if (error instanceof ValidatorError) throw new ValidatorError(prefixErrorPaths(error.errors, navField));
|
|
68
|
-
if (error instanceof DbError) throw new DbError(error.code, prefixErrorPaths(error.errors, navField));
|
|
69
|
-
throw error;
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
/**
|
|
73
|
-
* Catches `DbError('FK_VIOLATION')` with empty paths (from adapters that
|
|
74
|
-
* enforce FKs natively but can't report which field failed) and enriches
|
|
75
|
-
* the error with all FK field names from table metadata.
|
|
76
|
-
*/
|
|
77
|
-
async function enrichFkViolation(meta, fn) {
|
|
78
|
-
try {
|
|
79
|
-
return await fn();
|
|
80
|
-
} catch (error) {
|
|
81
|
-
if (error instanceof DbError && error.code === "FK_VIOLATION" && error.errors.every((err) => !err.path)) {
|
|
82
|
-
const msg = error.errors[0]?.message ?? error.message;
|
|
83
|
-
const errors = [];
|
|
84
|
-
for (const [, fk] of meta.foreignKeys) for (const field of fk.fields) errors.push({
|
|
85
|
-
path: field,
|
|
86
|
-
message: msg
|
|
87
|
-
});
|
|
88
|
-
throw new DbError("FK_VIOLATION", errors.length > 0 ? errors : error.errors);
|
|
89
|
-
}
|
|
90
|
-
throw error;
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
/**
|
|
94
|
-
* Wraps a delete operation: catches native `FK_VIOLATION` errors (e.g. SQLite
|
|
95
|
-
* RESTRICT) and re-throws as `CONFLICT` (409) with a descriptive message.
|
|
96
|
-
*/
|
|
97
|
-
async function remapDeleteFkViolation(tableName, fn) {
|
|
98
|
-
try {
|
|
99
|
-
return await fn();
|
|
100
|
-
} catch (error) {
|
|
101
|
-
if (error instanceof DbError && error.code === "FK_VIOLATION") throw new DbError("CONFLICT", [{
|
|
102
|
-
path: tableName,
|
|
103
|
-
message: `Cannot delete from "${tableName}": referenced by child records (RESTRICT)`
|
|
104
|
-
}]);
|
|
105
|
-
throw error;
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
//#endregion
|
|
109
|
-
//#region src/rel/nested-writer.ts
|
|
110
|
-
/**
|
|
111
|
-
* Checks if any payload contains navigational data that would be silently
|
|
112
|
-
* dropped because maxDepth is 0.
|
|
113
|
-
*/
|
|
114
|
-
function checkDepthOverflow(payloads, maxDepth, meta) {
|
|
115
|
-
if (meta.navFields.size === 0) return;
|
|
116
|
-
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.`);
|
|
117
|
-
}
|
|
118
|
-
/**
|
|
119
|
-
* Validates a batch of items using the given validator and context.
|
|
120
|
-
* Wraps per-item validation errors with array index paths for batch operations.
|
|
121
|
-
*/
|
|
122
|
-
function validateBatch(validator, items, ctx) {
|
|
123
|
-
for (let i = 0; i < items.length; i++) try {
|
|
124
|
-
validator.validate(items[i], false, ctx);
|
|
125
|
-
} catch (error) {
|
|
126
|
-
if (items.length > 1) {
|
|
127
|
-
if (error instanceof ValidatorError) throw new ValidatorError(error.errors.map((err) => ({
|
|
128
|
-
...err,
|
|
129
|
-
path: `[${i}].${err.path}`
|
|
130
|
-
})));
|
|
131
|
-
if (error instanceof DepthLimitExceededError) throw new DepthLimitExceededError(`[${i}].${error.field}`, error.declared, error.actual);
|
|
132
|
-
}
|
|
133
|
-
throw error;
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
/**
|
|
137
|
-
* Pre-validates FROM children (type + FK constraints) before the main insert.
|
|
138
|
-
* Catches errors early before the parent record is committed.
|
|
139
|
-
*/
|
|
140
|
-
async function preValidateNestedFrom(host, originals) {
|
|
141
|
-
for (const [navField, relation] of host._meta.relations) {
|
|
142
|
-
if (relation.direction !== "from") continue;
|
|
143
|
-
if (!host._writeTableResolver) continue;
|
|
144
|
-
const targetTable = host._writeTableResolver(relation.targetType());
|
|
145
|
-
if (!targetTable) continue;
|
|
146
|
-
const remoteFK = host._findRemoteFK(targetTable, host.tableName, relation.alias);
|
|
147
|
-
const allChildren = [];
|
|
148
|
-
for (const orig of originals) {
|
|
149
|
-
const children = orig[navField];
|
|
150
|
-
if (!Array.isArray(children)) continue;
|
|
151
|
-
for (const child of children) {
|
|
152
|
-
const childData = { ...child };
|
|
153
|
-
if (remoteFK) {
|
|
154
|
-
for (const field of remoteFK.fields) if (!(field in childData)) childData[field] = 0;
|
|
155
|
-
}
|
|
156
|
-
allChildren.push(childData);
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
if (allChildren.length === 0) continue;
|
|
160
|
-
await wrapNestedError(navField, () => targetTable.preValidateItems(allChildren, { excludeFkTargetTable: host.tableName }));
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
/**
|
|
164
|
-
* Batch-creates TO dependencies before the main insert.
|
|
165
|
-
*/
|
|
166
|
-
async function batchInsertNestedTo(host, items, maxDepth, depth) {
|
|
167
|
-
for (const [navField, relation] of host._meta.relations) {
|
|
168
|
-
if (relation.direction !== "to") continue;
|
|
169
|
-
const targetTable = host._writeTableResolver(relation.targetType());
|
|
170
|
-
if (!targetTable) continue;
|
|
171
|
-
const fk = host._findFKForRelation(relation);
|
|
172
|
-
if (!fk) continue;
|
|
173
|
-
const parents = [];
|
|
174
|
-
const sourceIndices = [];
|
|
175
|
-
for (let i = 0; i < items.length; i++) {
|
|
176
|
-
const nested = items[i][navField];
|
|
177
|
-
if (nested && typeof nested === "object" && !Array.isArray(nested)) {
|
|
178
|
-
parents.push(nested);
|
|
179
|
-
sourceIndices.push(i);
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
|
-
if (parents.length === 0) continue;
|
|
183
|
-
const result = await targetTable.insertMany(parents, {
|
|
184
|
-
maxDepth,
|
|
185
|
-
_depth: depth + 1
|
|
186
|
-
});
|
|
187
|
-
for (let j = 0; j < sourceIndices.length; j++) if (fk.localFields.length === 1) items[sourceIndices[j]][fk.localFields[0]] = result.insertedIds[j];
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
/**
|
|
191
|
-
* Batch-creates FROM dependents after the main insert.
|
|
192
|
-
*/
|
|
193
|
-
async function batchInsertNestedFrom(host, originals, parentIds, maxDepth, depth) {
|
|
194
|
-
for (const [navField, relation] of host._meta.relations) {
|
|
195
|
-
if (relation.direction !== "from") continue;
|
|
196
|
-
const targetTable = host._writeTableResolver(relation.targetType());
|
|
197
|
-
if (!targetTable) continue;
|
|
198
|
-
const remoteFK = host._findRemoteFK(targetTable, host.tableName, relation.alias);
|
|
199
|
-
if (!remoteFK) continue;
|
|
200
|
-
const allChildren = [];
|
|
201
|
-
for (let i = 0; i < originals.length; i++) {
|
|
202
|
-
const children = originals[i][navField];
|
|
203
|
-
if (!Array.isArray(children)) continue;
|
|
204
|
-
for (const child of children) {
|
|
205
|
-
const childData = { ...child };
|
|
206
|
-
if (remoteFK.fields.length === 1) childData[remoteFK.fields[0]] = parentIds[i];
|
|
207
|
-
allChildren.push(childData);
|
|
208
|
-
}
|
|
209
|
-
}
|
|
210
|
-
if (allChildren.length === 0) continue;
|
|
211
|
-
await wrapNestedError(navField, () => targetTable.insertMany(allChildren, {
|
|
212
|
-
maxDepth,
|
|
213
|
-
_depth: depth + 1
|
|
214
|
-
}));
|
|
215
|
-
}
|
|
216
|
-
}
|
|
217
|
-
/**
|
|
218
|
-
* Batch-creates VIA (M:N) targets and junction entries after the main insert.
|
|
219
|
-
*/
|
|
220
|
-
async function batchInsertNestedVia(host, originals, parentIds, maxDepth, depth) {
|
|
221
|
-
for (const [navField, relation] of host._meta.relations) {
|
|
222
|
-
if (relation.direction !== "via" || !relation.viaType) continue;
|
|
223
|
-
const targetTable = host._writeTableResolver(relation.targetType());
|
|
224
|
-
if (!targetTable) continue;
|
|
225
|
-
const junctionTable = host._writeTableResolver(relation.viaType());
|
|
226
|
-
if (!junctionTable) continue;
|
|
227
|
-
const targetTableName = resolveRelationTargetTable(relation);
|
|
228
|
-
const fkToThis = host._findRemoteFK(junctionTable, host.tableName);
|
|
229
|
-
if (!fkToThis) continue;
|
|
230
|
-
const fkToTarget = host._findRemoteFK(junctionTable, targetTableName);
|
|
231
|
-
if (!fkToTarget) continue;
|
|
232
|
-
const targetPKField = targetTable.primaryKeys[0];
|
|
233
|
-
if (!targetPKField || fkToTarget.fields.length !== 1 || fkToThis.fields.length !== 1) continue;
|
|
234
|
-
for (let i = 0; i < originals.length; i++) {
|
|
235
|
-
const targets = originals[i][navField];
|
|
236
|
-
if (!Array.isArray(targets) || targets.length === 0) continue;
|
|
237
|
-
const parentPK = parentIds[i];
|
|
238
|
-
if (parentPK === void 0) continue;
|
|
239
|
-
const newTargets = [];
|
|
240
|
-
const existingIds = [];
|
|
241
|
-
for (const t of targets) {
|
|
242
|
-
const rec = t;
|
|
243
|
-
const pk = rec[targetPKField];
|
|
244
|
-
if (pk !== void 0 && pk !== null) existingIds.push(pk);
|
|
245
|
-
else newTargets.push({ ...rec });
|
|
246
|
-
}
|
|
247
|
-
const allTargetIds = [...existingIds];
|
|
248
|
-
if (newTargets.length > 0) {
|
|
249
|
-
const targetResult = await targetTable.insertMany(newTargets, {
|
|
250
|
-
maxDepth,
|
|
251
|
-
_depth: depth + 1
|
|
252
|
-
});
|
|
253
|
-
allTargetIds.push(...targetResult.insertedIds);
|
|
254
|
-
}
|
|
255
|
-
if (allTargetIds.length > 0) {
|
|
256
|
-
const junctionRows = allTargetIds.map((targetId) => ({
|
|
257
|
-
[fkToThis.fields[0]]: parentPK,
|
|
258
|
-
[fkToTarget.fields[0]]: targetId
|
|
259
|
-
}));
|
|
260
|
-
await junctionTable.insertMany(junctionRows, { maxDepth: 0 });
|
|
261
|
-
}
|
|
262
|
-
}
|
|
263
|
-
}
|
|
264
|
-
}
|
|
265
|
-
/**
|
|
266
|
-
* Batch-replaces TO dependencies before the main replace.
|
|
267
|
-
*/
|
|
268
|
-
async function batchReplaceNestedTo(host, items, maxDepth, depth) {
|
|
269
|
-
for (const [navField, relation] of host._meta.relations) {
|
|
270
|
-
if (relation.direction !== "to") continue;
|
|
271
|
-
const targetTable = host._writeTableResolver(relation.targetType());
|
|
272
|
-
if (!targetTable) continue;
|
|
273
|
-
const fk = host._findFKForRelation(relation);
|
|
274
|
-
if (!fk) continue;
|
|
275
|
-
const parents = [];
|
|
276
|
-
const sourceIndices = [];
|
|
277
|
-
for (let i = 0; i < items.length; i++) {
|
|
278
|
-
const nested = items[i][navField];
|
|
279
|
-
if (nested && typeof nested === "object" && !Array.isArray(nested)) {
|
|
280
|
-
parents.push(nested);
|
|
281
|
-
sourceIndices.push(i);
|
|
282
|
-
}
|
|
283
|
-
}
|
|
284
|
-
if (parents.length === 0) continue;
|
|
285
|
-
await targetTable.bulkReplace(parents, {
|
|
286
|
-
maxDepth,
|
|
287
|
-
_depth: depth + 1
|
|
288
|
-
});
|
|
289
|
-
for (let j = 0; j < sourceIndices.length; j++) if (fk.localFields.length === 1 && fk.targetFields.length === 1) items[sourceIndices[j]][fk.localFields[0]] = parents[j][fk.targetFields[0]];
|
|
290
|
-
}
|
|
291
|
-
}
|
|
292
|
-
/**
|
|
293
|
-
* Batch-replaces FROM dependents after the main replace.
|
|
294
|
-
*/
|
|
295
|
-
async function batchReplaceNestedFrom(host, originals, maxDepth, depth) {
|
|
296
|
-
for (const [navField, relation] of host._meta.relations) {
|
|
297
|
-
if (relation.direction !== "from") continue;
|
|
298
|
-
const targetTable = host._writeTableResolver(relation.targetType());
|
|
299
|
-
if (!targetTable) continue;
|
|
300
|
-
const remoteFK = host._findRemoteFK(targetTable, host.tableName, relation.alias);
|
|
301
|
-
if (!remoteFK) continue;
|
|
302
|
-
const childPKs = [...targetTable.primaryKeys];
|
|
303
|
-
for (const original of originals) {
|
|
304
|
-
const children = original[navField];
|
|
305
|
-
if (!Array.isArray(children)) continue;
|
|
306
|
-
const parentPK = host._meta.primaryKeys.length === 1 ? original[host._meta.primaryKeys[0]] : void 0;
|
|
307
|
-
if (parentPK === void 0 || remoteFK.fields.length !== 1) continue;
|
|
308
|
-
await fromReplace(targetTable, children, parentPK, remoteFK.fields[0], childPKs, navField, maxDepth, depth);
|
|
309
|
-
}
|
|
310
|
-
}
|
|
311
|
-
}
|
|
312
|
-
/**
|
|
313
|
-
* Handles VIA (M:N) relations during replace.
|
|
314
|
-
*/
|
|
315
|
-
async function batchReplaceNestedVia(host, originals, maxDepth, depth) {
|
|
316
|
-
for (const [navField, relation] of host._meta.relations) {
|
|
317
|
-
if (relation.direction !== "via" || !relation.viaType) continue;
|
|
318
|
-
const targetTable = host._writeTableResolver(relation.targetType());
|
|
319
|
-
if (!targetTable) continue;
|
|
320
|
-
const junctionTable = host._writeTableResolver(relation.viaType());
|
|
321
|
-
if (!junctionTable) continue;
|
|
322
|
-
const targetTableName = resolveRelationTargetTable(relation);
|
|
323
|
-
const fkToThis = host._findRemoteFK(junctionTable, host.tableName);
|
|
324
|
-
if (!fkToThis) continue;
|
|
325
|
-
const fkToTarget = host._findRemoteFK(junctionTable, targetTableName);
|
|
326
|
-
if (!fkToTarget) continue;
|
|
327
|
-
const targetPKField = targetTable.primaryKeys[0];
|
|
328
|
-
if (!targetPKField || fkToTarget.fields.length !== 1 || fkToThis.fields.length !== 1) continue;
|
|
329
|
-
for (const original of originals) {
|
|
330
|
-
const targets = original[navField];
|
|
331
|
-
if (!Array.isArray(targets)) continue;
|
|
332
|
-
const parentPK = host._meta.primaryKeys.length === 1 ? original[host._meta.primaryKeys[0]] : void 0;
|
|
333
|
-
if (parentPK === void 0) continue;
|
|
334
|
-
await viaReplace(targetTable, junctionTable, targets, parentPK, targetPKField, fkToThis.fields[0], fkToTarget.fields[0], maxDepth, depth);
|
|
335
|
-
}
|
|
336
|
-
}
|
|
337
|
-
}
|
|
338
|
-
/**
|
|
339
|
-
* Batch-patches TO dependencies before the main patch.
|
|
340
|
-
* Reads FK values from DB if not present in the payload.
|
|
341
|
-
*/
|
|
342
|
-
async function batchPatchNestedTo(host, items, maxDepth, depth) {
|
|
343
|
-
for (const [navField, relation] of host._meta.relations) {
|
|
344
|
-
if (relation.direction !== "to") continue;
|
|
345
|
-
const targetTable = host._writeTableResolver(relation.targetType());
|
|
346
|
-
if (!targetTable) continue;
|
|
347
|
-
const fk = host._findFKForRelation(relation);
|
|
348
|
-
if (!fk) continue;
|
|
349
|
-
const patches = [];
|
|
350
|
-
for (const item of items) {
|
|
351
|
-
const nested = item[navField];
|
|
352
|
-
if (!nested || typeof nested !== "object" || Array.isArray(nested)) continue;
|
|
353
|
-
const patch = { ...nested };
|
|
354
|
-
let fkValue = fk.localFields.length === 1 ? item[fk.localFields[0]] : void 0;
|
|
355
|
-
if (fkValue === void 0) {
|
|
356
|
-
const pkFilter = host._extractRecordFilter(item);
|
|
357
|
-
const current = await host.findOne({
|
|
358
|
-
filter: pkFilter,
|
|
359
|
-
controls: {}
|
|
360
|
-
});
|
|
361
|
-
if (!current) throw new DbError("NOT_FOUND", [{
|
|
362
|
-
path: navField,
|
|
363
|
-
message: `Cannot patch relation '${navField}' — source record not found`
|
|
364
|
-
}]);
|
|
365
|
-
fkValue = fk.localFields.length === 1 ? current[fk.localFields[0]] : void 0;
|
|
366
|
-
}
|
|
367
|
-
if (fkValue === null || fkValue === void 0) throw new DbError("FK_VIOLATION", [{
|
|
368
|
-
path: fk.localFields[0],
|
|
369
|
-
message: `Cannot patch relation '${navField}' — foreign key '${fk.localFields[0]}' is null`
|
|
370
|
-
}]);
|
|
371
|
-
if (fk.targetFields.length === 1) patch[fk.targetFields[0]] = fkValue;
|
|
372
|
-
patches.push(patch);
|
|
373
|
-
}
|
|
374
|
-
if (patches.length === 0) continue;
|
|
375
|
-
await targetTable.bulkUpdate(patches, {
|
|
376
|
-
maxDepth,
|
|
377
|
-
_depth: depth + 1
|
|
378
|
-
});
|
|
379
|
-
}
|
|
380
|
-
}
|
|
381
|
-
/**
|
|
382
|
-
* Batch-patches FROM (1:N) dependencies after the main patch.
|
|
383
|
-
* Supports patch operators: $replace, $insert, $remove, $update, $upsert.
|
|
384
|
-
*/
|
|
385
|
-
async function batchPatchNestedFrom(host, originals, maxDepth, depth) {
|
|
386
|
-
for (const [navField, relation] of host._meta.relations) {
|
|
387
|
-
if (relation.direction !== "from") continue;
|
|
388
|
-
const targetTable = host._writeTableResolver(relation.targetType());
|
|
389
|
-
if (!targetTable) continue;
|
|
390
|
-
const remoteFK = host._findRemoteFK(targetTable, host.tableName, relation.alias);
|
|
391
|
-
if (!remoteFK) continue;
|
|
392
|
-
const childPKs = [...targetTable.primaryKeys];
|
|
393
|
-
for (const original of originals) {
|
|
394
|
-
const navValue = original[navField];
|
|
395
|
-
if (navValue === void 0 || navValue === null) continue;
|
|
396
|
-
const parentPK = host._meta.primaryKeys.length === 1 ? original[host._meta.primaryKeys[0]] : void 0;
|
|
397
|
-
if (parentPK === void 0 || remoteFK.fields.length !== 1) continue;
|
|
398
|
-
const fkField = remoteFK.fields[0];
|
|
399
|
-
const ops = extractNavPatchOps(navValue);
|
|
400
|
-
if (ops.replace) await fromReplace(targetTable, ops.replace, parentPK, fkField, childPKs, navField, maxDepth, depth);
|
|
401
|
-
if (ops.remove && ops.remove.length > 0) {
|
|
402
|
-
const removeFilters = ops.remove.map((child) => {
|
|
403
|
-
const rec = child;
|
|
404
|
-
const f = {};
|
|
405
|
-
for (const pk of childPKs) f[pk] = rec[pk];
|
|
406
|
-
f[fkField] = parentPK;
|
|
407
|
-
return f;
|
|
408
|
-
});
|
|
409
|
-
if (removeFilters.length === 1) await targetTable.deleteMany(removeFilters[0]);
|
|
410
|
-
else await targetTable.deleteMany({ $or: removeFilters });
|
|
411
|
-
}
|
|
412
|
-
if (ops.update && ops.update.length > 0) {
|
|
413
|
-
const items = ops.update.map((child) => {
|
|
414
|
-
const rec = { ...child };
|
|
415
|
-
rec[fkField] = parentPK;
|
|
416
|
-
return rec;
|
|
417
|
-
});
|
|
418
|
-
await wrapNestedError(navField, () => targetTable.bulkUpdate(items, {
|
|
419
|
-
maxDepth,
|
|
420
|
-
_depth: depth + 1
|
|
421
|
-
}));
|
|
422
|
-
}
|
|
423
|
-
if (ops.upsert) {
|
|
424
|
-
const toUpdate = [];
|
|
425
|
-
const toInsert = [];
|
|
426
|
-
for (const child of ops.upsert) {
|
|
427
|
-
const rec = { ...child };
|
|
428
|
-
rec[fkField] = parentPK;
|
|
429
|
-
if (childPKs.length > 0 && childPKs.every((pk) => rec[pk] !== void 0)) toUpdate.push(rec);
|
|
430
|
-
else toInsert.push(rec);
|
|
431
|
-
}
|
|
432
|
-
if (toUpdate.length > 0) await wrapNestedError(navField, () => targetTable.bulkUpdate(toUpdate, {
|
|
433
|
-
maxDepth,
|
|
434
|
-
_depth: depth + 1
|
|
435
|
-
}));
|
|
436
|
-
if (toInsert.length > 0) await wrapNestedError(navField, () => targetTable.insertMany(toInsert, {
|
|
437
|
-
maxDepth,
|
|
438
|
-
_depth: depth + 1
|
|
439
|
-
}));
|
|
440
|
-
}
|
|
441
|
-
if (ops.insert && ops.insert.length > 0) {
|
|
442
|
-
const items = [];
|
|
443
|
-
for (let i = 0; i < ops.insert.length; i++) {
|
|
444
|
-
const rec = { ...ops.insert[i] };
|
|
445
|
-
rec[fkField] = parentPK;
|
|
446
|
-
items.push(rec);
|
|
447
|
-
}
|
|
448
|
-
await wrapNestedError(navField, () => targetTable.insertMany(items, {
|
|
449
|
-
maxDepth,
|
|
450
|
-
_depth: depth + 1
|
|
451
|
-
}));
|
|
452
|
-
}
|
|
453
|
-
}
|
|
454
|
-
}
|
|
455
|
-
}
|
|
456
|
-
/**
|
|
457
|
-
* Batch-patches VIA (M:N) dependencies after the main patch.
|
|
458
|
-
* Supports patch operators: $replace, $insert, $remove, $update, $upsert.
|
|
459
|
-
*/
|
|
460
|
-
async function batchPatchNestedVia(host, originals, maxDepth, depth) {
|
|
461
|
-
for (const [navField, relation] of host._meta.relations) {
|
|
462
|
-
if (relation.direction !== "via" || !relation.viaType) continue;
|
|
463
|
-
const targetTable = host._writeTableResolver(relation.targetType());
|
|
464
|
-
if (!targetTable) continue;
|
|
465
|
-
const junctionTable = host._writeTableResolver(relation.viaType());
|
|
466
|
-
if (!junctionTable) continue;
|
|
467
|
-
const targetTableName = resolveRelationTargetTable(relation);
|
|
468
|
-
const fkToThis = host._findRemoteFK(junctionTable, host.tableName);
|
|
469
|
-
if (!fkToThis) continue;
|
|
470
|
-
const fkToTarget = host._findRemoteFK(junctionTable, targetTableName);
|
|
471
|
-
if (!fkToTarget) continue;
|
|
472
|
-
const targetPKField = targetTable.primaryKeys[0];
|
|
473
|
-
if (!targetPKField || fkToTarget.fields.length !== 1 || fkToThis.fields.length !== 1) continue;
|
|
474
|
-
for (const original of originals) {
|
|
475
|
-
const navValue = original[navField];
|
|
476
|
-
if (navValue === void 0 || navValue === null) continue;
|
|
477
|
-
const parentPK = host._meta.primaryKeys.length === 1 ? original[host._meta.primaryKeys[0]] : void 0;
|
|
478
|
-
if (parentPK === void 0) continue;
|
|
479
|
-
const ops = extractNavPatchOps(navValue);
|
|
480
|
-
if (ops.replace) await viaReplace(targetTable, junctionTable, ops.replace, parentPK, targetPKField, fkToThis.fields[0], fkToTarget.fields[0], maxDepth, depth);
|
|
481
|
-
if (ops.remove && ops.remove.length > 0) {
|
|
482
|
-
const targetPKs = ops.remove.map((t) => t[targetPKField]).filter((pk) => pk !== void 0 && pk !== null);
|
|
483
|
-
if (targetPKs.length === 1) await junctionTable.deleteMany({
|
|
484
|
-
[fkToThis.fields[0]]: parentPK,
|
|
485
|
-
[fkToTarget.fields[0]]: targetPKs[0]
|
|
486
|
-
});
|
|
487
|
-
else if (targetPKs.length > 1) await junctionTable.deleteMany({
|
|
488
|
-
[fkToThis.fields[0]]: parentPK,
|
|
489
|
-
[fkToTarget.fields[0]]: { $in: targetPKs }
|
|
490
|
-
});
|
|
491
|
-
}
|
|
492
|
-
if (ops.update && ops.update.length > 0) await targetTable.bulkUpdate(ops.update.map((t) => ({ ...t })), {
|
|
493
|
-
maxDepth,
|
|
494
|
-
_depth: depth + 1
|
|
495
|
-
});
|
|
496
|
-
if (ops.upsert && ops.upsert.length > 0) {
|
|
497
|
-
const toUpdate = [];
|
|
498
|
-
const toInsert = [];
|
|
499
|
-
const existingPKs = [];
|
|
500
|
-
for (const target of ops.upsert) {
|
|
501
|
-
const rec = { ...target };
|
|
502
|
-
const pk = rec[targetPKField];
|
|
503
|
-
if (pk !== void 0 && pk !== null) {
|
|
504
|
-
toUpdate.push(rec);
|
|
505
|
-
existingPKs.push(pk);
|
|
506
|
-
} else toInsert.push(rec);
|
|
507
|
-
}
|
|
508
|
-
if (toUpdate.length > 0) await targetTable.bulkUpdate(toUpdate, {
|
|
509
|
-
maxDepth,
|
|
510
|
-
_depth: depth + 1
|
|
511
|
-
});
|
|
512
|
-
if (existingPKs.length > 0) {
|
|
513
|
-
const existingJunctions = await junctionTable.findMany({
|
|
514
|
-
filter: {
|
|
515
|
-
[fkToThis.fields[0]]: parentPK,
|
|
516
|
-
[fkToTarget.fields[0]]: existingPKs.length === 1 ? existingPKs[0] : { $in: existingPKs }
|
|
517
|
-
},
|
|
518
|
-
controls: { $select: [fkToTarget.fields[0]] }
|
|
519
|
-
});
|
|
520
|
-
const existingSet = new Set(existingJunctions.map((j) => String(j[fkToTarget.fields[0]])));
|
|
521
|
-
const missingPKs = existingPKs.filter((pk) => !existingSet.has(String(pk)));
|
|
522
|
-
if (missingPKs.length > 0) {
|
|
523
|
-
const junctionRows = missingPKs.map((pk) => ({
|
|
524
|
-
[fkToThis.fields[0]]: parentPK,
|
|
525
|
-
[fkToTarget.fields[0]]: pk
|
|
526
|
-
}));
|
|
527
|
-
await junctionTable.insertMany(junctionRows, { maxDepth: 0 });
|
|
528
|
-
}
|
|
529
|
-
}
|
|
530
|
-
if (toInsert.length > 0) {
|
|
531
|
-
const junctionRows = (await targetTable.insertMany(toInsert, {
|
|
532
|
-
maxDepth,
|
|
533
|
-
_depth: depth + 1
|
|
534
|
-
})).insertedIds.map((newId) => ({
|
|
535
|
-
[fkToThis.fields[0]]: parentPK,
|
|
536
|
-
[fkToTarget.fields[0]]: newId
|
|
537
|
-
}));
|
|
538
|
-
await junctionTable.insertMany(junctionRows, { maxDepth: 0 });
|
|
539
|
-
}
|
|
540
|
-
}
|
|
541
|
-
if (ops.insert && ops.insert.length > 0) {
|
|
542
|
-
const toInsert = [];
|
|
543
|
-
const existingIds = [];
|
|
544
|
-
for (const target of ops.insert) {
|
|
545
|
-
const rec = { ...target };
|
|
546
|
-
const pk = rec[targetPKField];
|
|
547
|
-
if (pk !== void 0 && pk !== null) existingIds.push(pk);
|
|
548
|
-
else toInsert.push(rec);
|
|
549
|
-
}
|
|
550
|
-
const allIds = [...existingIds];
|
|
551
|
-
if (toInsert.length > 0) {
|
|
552
|
-
const insertResult = await targetTable.insertMany(toInsert, {
|
|
553
|
-
maxDepth,
|
|
554
|
-
_depth: depth + 1
|
|
555
|
-
});
|
|
556
|
-
allIds.push(...insertResult.insertedIds);
|
|
557
|
-
}
|
|
558
|
-
if (allIds.length > 0) {
|
|
559
|
-
const junctionRows = allIds.map((targetId) => ({
|
|
560
|
-
[fkToThis.fields[0]]: parentPK,
|
|
561
|
-
[fkToTarget.fields[0]]: targetId
|
|
562
|
-
}));
|
|
563
|
-
await junctionTable.insertMany(junctionRows, { maxDepth: 0 });
|
|
564
|
-
}
|
|
565
|
-
}
|
|
566
|
-
}
|
|
567
|
-
}
|
|
568
|
-
}
|
|
569
|
-
/**
|
|
570
|
-
* Extracts patch operations from a nav field value.
|
|
571
|
-
* Plain array → $replace. Object with $insert, $remove, etc. → individual ops.
|
|
572
|
-
*/
|
|
573
|
-
function extractNavPatchOps(navValue) {
|
|
574
|
-
if (Array.isArray(navValue)) return { replace: navValue };
|
|
575
|
-
if (typeof navValue !== "object" || navValue === null) return {};
|
|
576
|
-
const obj = navValue;
|
|
577
|
-
return {
|
|
578
|
-
replace: obj.$replace !== void 0 ? obj.$replace : void 0,
|
|
579
|
-
insert: obj.$insert !== void 0 ? obj.$insert : void 0,
|
|
580
|
-
remove: obj.$remove !== void 0 ? obj.$remove : void 0,
|
|
581
|
-
update: obj.$update !== void 0 ? obj.$update : void 0,
|
|
582
|
-
upsert: obj.$upsert !== void 0 ? obj.$upsert : void 0
|
|
583
|
-
};
|
|
584
|
-
}
|
|
585
|
-
/**
|
|
586
|
-
* FROM $replace helper: delete orphans, replace existing, insert new.
|
|
587
|
-
*/
|
|
588
|
-
async function fromReplace(targetTable, children, parentPK, fkField, childPKs, navField, maxDepth, depth) {
|
|
589
|
-
const toReplace = [];
|
|
590
|
-
const toInsert = [];
|
|
591
|
-
const newPKSet = /* @__PURE__ */ new Set();
|
|
592
|
-
for (const child of children) {
|
|
593
|
-
const childData = { ...child };
|
|
594
|
-
childData[fkField] = parentPK;
|
|
595
|
-
if (childPKs.length > 0 && childPKs.every((pk) => childData[pk] !== void 0)) {
|
|
596
|
-
newPKSet.add(childPKs.map((pk) => String(childData[pk])).join("\0"));
|
|
597
|
-
toReplace.push(childData);
|
|
598
|
-
} else toInsert.push(childData);
|
|
599
|
-
}
|
|
600
|
-
const existing = await targetTable.findMany({
|
|
601
|
-
filter: { [fkField]: parentPK },
|
|
602
|
-
controls: childPKs.length > 0 ? { $select: [...childPKs] } : {}
|
|
603
|
-
});
|
|
604
|
-
const orphanFilters = [];
|
|
605
|
-
for (const row of existing) {
|
|
606
|
-
const pkKey = childPKs.map((pk) => String(row[pk])).join("\0");
|
|
607
|
-
if (!newPKSet.has(pkKey)) {
|
|
608
|
-
const f = {};
|
|
609
|
-
for (const pk of childPKs) f[pk] = row[pk];
|
|
610
|
-
orphanFilters.push(f);
|
|
611
|
-
}
|
|
612
|
-
}
|
|
613
|
-
if (orphanFilters.length === 1) await targetTable.deleteMany(orphanFilters[0]);
|
|
614
|
-
else if (orphanFilters.length > 1) await targetTable.deleteMany({ $or: orphanFilters });
|
|
615
|
-
if (toReplace.length > 0) await wrapNestedError(navField, () => targetTable.bulkReplace(toReplace, {
|
|
616
|
-
maxDepth,
|
|
617
|
-
_depth: depth + 1
|
|
618
|
-
}));
|
|
619
|
-
if (toInsert.length > 0) await wrapNestedError(navField, () => targetTable.insertMany(toInsert, {
|
|
620
|
-
maxDepth,
|
|
621
|
-
_depth: depth + 1
|
|
622
|
-
}));
|
|
623
|
-
}
|
|
624
|
-
/**
|
|
625
|
-
* VIA $replace helper: clear junctions, replace/insert targets, rebuild junctions.
|
|
626
|
-
*/
|
|
627
|
-
async function viaReplace(targetTable, junctionTable, targets, parentPK, targetPKField, fkToThisField, fkToTargetField, maxDepth, depth) {
|
|
628
|
-
await junctionTable.deleteMany({ [fkToThisField]: parentPK });
|
|
629
|
-
const toReplace = [];
|
|
630
|
-
const toInsert = [];
|
|
631
|
-
const existingIds = [];
|
|
632
|
-
for (const t of targets) {
|
|
633
|
-
const rec = t;
|
|
634
|
-
const pk = rec[targetPKField];
|
|
635
|
-
if (pk !== void 0 && pk !== null) {
|
|
636
|
-
if (Object.keys(rec).filter((k) => k !== targetPKField).length > 0) toReplace.push({ ...rec });
|
|
637
|
-
existingIds.push(pk);
|
|
638
|
-
} else toInsert.push({ ...rec });
|
|
639
|
-
}
|
|
640
|
-
if (toReplace.length > 0) await targetTable.bulkReplace(toReplace, {
|
|
641
|
-
maxDepth,
|
|
642
|
-
_depth: depth + 1
|
|
643
|
-
});
|
|
644
|
-
const allTargetIds = [...existingIds];
|
|
645
|
-
if (toInsert.length > 0) {
|
|
646
|
-
const insertResult = await targetTable.insertMany(toInsert, {
|
|
647
|
-
maxDepth,
|
|
648
|
-
_depth: depth + 1
|
|
649
|
-
});
|
|
650
|
-
allTargetIds.push(...insertResult.insertedIds);
|
|
651
|
-
}
|
|
652
|
-
if (allTargetIds.length > 0) {
|
|
653
|
-
const junctionRows = allTargetIds.map((targetId) => ({
|
|
654
|
-
[fkToThisField]: parentPK,
|
|
655
|
-
[fkToTargetField]: targetId
|
|
656
|
-
}));
|
|
657
|
-
await junctionTable.insertMany(junctionRows, { maxDepth: 0 });
|
|
658
|
-
}
|
|
659
|
-
}
|
|
660
|
-
//#endregion
|
|
661
|
-
export { resolveRelationTargetTable as _, batchPatchNestedTo as a, batchReplaceNestedTo as c, preValidateNestedFrom as d, validateBatch as f, findRemoteFK as g, findFKForRelation as h, batchPatchNestedFrom as i, batchReplaceNestedVia as l, remapDeleteFkViolation as m, batchInsertNestedTo as n, batchPatchNestedVia as o, enrichFkViolation as p, batchInsertNestedVia as r, batchReplaceNestedFrom as s, batchInsertNestedFrom as t, checkDepthOverflow as u, tableNameOf as v };
|