@atscript/db 0.1.38
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/LICENSE +21 -0
- package/README.md +343 -0
- package/dist/agg-BJFJ3dFQ.mjs +8 -0
- package/dist/agg-DnUWAOK8.cjs +14 -0
- package/dist/agg.cjs +3 -0
- package/dist/agg.d.ts +13 -0
- package/dist/agg.mjs +3 -0
- package/dist/chunk-CrpGerW8.cjs +31 -0
- package/dist/control_as-BFPERAF_.cjs +28 -0
- package/dist/control_as-bjmwe24C.mjs +26 -0
- package/dist/index.cjs +2887 -0
- package/dist/index.d.ts +1706 -0
- package/dist/index.mjs +2846 -0
- package/dist/logger-B7oxCfLQ.mjs +12 -0
- package/dist/logger-Dt2v_-wb.cjs +18 -0
- package/dist/nested-writer-BkqL7cp3.cjs +667 -0
- package/dist/nested-writer-NEN51mnR.mjs +576 -0
- package/dist/plugin.cjs +993 -0
- package/dist/plugin.d.ts +5 -0
- package/dist/plugin.mjs +989 -0
- package/dist/rel.cjs +20 -0
- package/dist/rel.d.ts +1305 -0
- package/dist/rel.mjs +5 -0
- package/dist/relation-helpers-DyBIlQnB.mjs +29 -0
- package/dist/relation-helpers-guFL_oRf.cjs +47 -0
- package/dist/relation-loader-CpnDRf9k.cjs +415 -0
- package/dist/relation-loader-D4mTw6yH.cjs +4 -0
- package/dist/relation-loader-Dv7qXYq7.mjs +409 -0
- package/dist/relation-loader-Ggy1ujwR.mjs +4 -0
- package/dist/shared.cjs +13 -0
- package/dist/shared.d.ts +70 -0
- package/dist/shared.mjs +3 -0
- package/dist/sync.cjs +1205 -0
- package/dist/sync.d.ts +1878 -0
- package/dist/sync.mjs +1186 -0
- package/dist/validation-utils-DEoCMmEb.cjs +304 -0
- package/dist/validation-utils-DhR_mtKa.mjs +237 -0
- package/package.json +81 -0
|
@@ -0,0 +1,576 @@
|
|
|
1
|
+
import { resolveRelationTargetTable } from "./relation-helpers-DyBIlQnB.mjs";
|
|
2
|
+
import { ValidatorError } from "@atscript/typescript/utils";
|
|
3
|
+
|
|
4
|
+
//#region packages/db/src/db-error.ts
|
|
5
|
+
function _define_property(obj, key, value) {
|
|
6
|
+
if (key in obj) Object.defineProperty(obj, key, {
|
|
7
|
+
value,
|
|
8
|
+
enumerable: true,
|
|
9
|
+
configurable: true,
|
|
10
|
+
writable: true
|
|
11
|
+
});
|
|
12
|
+
else obj[key] = value;
|
|
13
|
+
return obj;
|
|
14
|
+
}
|
|
15
|
+
var DbError = class extends Error {
|
|
16
|
+
constructor(code, errors, message) {
|
|
17
|
+
super(message ?? errors[0]?.message ?? "Database error"), _define_property(this, "code", void 0), _define_property(this, "errors", void 0), _define_property(this, "name", void 0), this.code = code, this.errors = errors, this.name = "DbError";
|
|
18
|
+
this.stack = undefined;
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
//#endregion
|
|
23
|
+
//#region packages/db/src/table/error-utils.ts
|
|
24
|
+
function prefixErrorPaths(errors, prefix) {
|
|
25
|
+
return errors.map((err) => ({
|
|
26
|
+
...err,
|
|
27
|
+
path: err.path ? `${prefix}.${err.path}` : prefix
|
|
28
|
+
}));
|
|
29
|
+
}
|
|
30
|
+
async function wrapNestedError(navField, fn) {
|
|
31
|
+
try {
|
|
32
|
+
return await fn();
|
|
33
|
+
} catch (error) {
|
|
34
|
+
if (error instanceof ValidatorError) throw new ValidatorError(prefixErrorPaths(error.errors, navField));
|
|
35
|
+
if (error instanceof DbError) throw new DbError(error.code, prefixErrorPaths(error.errors, navField));
|
|
36
|
+
throw error;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
async function enrichFkViolation(meta, fn) {
|
|
40
|
+
try {
|
|
41
|
+
return await fn();
|
|
42
|
+
} catch (error) {
|
|
43
|
+
if (error instanceof DbError && error.code === "FK_VIOLATION" && error.errors.every((err) => !err.path)) {
|
|
44
|
+
const msg = error.errors[0]?.message ?? error.message;
|
|
45
|
+
const errors = [];
|
|
46
|
+
for (const [, fk] of meta.foreignKeys) for (const field of fk.fields) errors.push({
|
|
47
|
+
path: field,
|
|
48
|
+
message: msg
|
|
49
|
+
});
|
|
50
|
+
throw new DbError("FK_VIOLATION", errors.length > 0 ? errors : error.errors);
|
|
51
|
+
}
|
|
52
|
+
throw error;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
async function remapDeleteFkViolation(tableName, fn) {
|
|
56
|
+
try {
|
|
57
|
+
return await fn();
|
|
58
|
+
} catch (error) {
|
|
59
|
+
if (error instanceof DbError && error.code === "FK_VIOLATION") throw new DbError("CONFLICT", [{
|
|
60
|
+
path: tableName,
|
|
61
|
+
message: `Cannot delete from "${tableName}": referenced by child records (RESTRICT)`
|
|
62
|
+
}]);
|
|
63
|
+
throw error;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
//#endregion
|
|
68
|
+
//#region packages/db/src/rel/nested-writer.ts
|
|
69
|
+
function checkDepthOverflow(payloads, maxDepth, meta) {
|
|
70
|
+
if (meta.navFields.size === 0) return;
|
|
71
|
+
for (const payload of payloads) for (const navField of meta.navFields) if (payload[navField] !== undefined) throw new Error(`Nested data in '${navField}' exceeds maxDepth (${maxDepth}). ` + `Increase maxDepth or strip nested data before writing.`);
|
|
72
|
+
}
|
|
73
|
+
function validateBatch(validator, items, ctx) {
|
|
74
|
+
for (let i = 0; i < items.length; i++) try {
|
|
75
|
+
validator.validate(items[i], false, ctx);
|
|
76
|
+
} catch (error) {
|
|
77
|
+
if (error instanceof ValidatorError && items.length > 1) throw new ValidatorError(error.errors.map((err) => ({
|
|
78
|
+
...err,
|
|
79
|
+
path: `[${i}].${err.path}`
|
|
80
|
+
})));
|
|
81
|
+
throw error;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
async function preValidateNestedFrom(host, originals) {
|
|
85
|
+
for (const [navField, relation] of host._meta.relations) {
|
|
86
|
+
if (relation.direction !== "from") continue;
|
|
87
|
+
if (!host._writeTableResolver) continue;
|
|
88
|
+
const targetTable = host._writeTableResolver(relation.targetType());
|
|
89
|
+
if (!targetTable) continue;
|
|
90
|
+
const remoteFK = host._findRemoteFK(targetTable, host.tableName, relation.alias);
|
|
91
|
+
const allChildren = [];
|
|
92
|
+
for (const orig of originals) {
|
|
93
|
+
const children = orig[navField];
|
|
94
|
+
if (!Array.isArray(children)) continue;
|
|
95
|
+
for (const child of children) {
|
|
96
|
+
const childData = { ...child };
|
|
97
|
+
if (remoteFK) {
|
|
98
|
+
for (const field of remoteFK.fields) if (!(field in childData)) childData[field] = 0;
|
|
99
|
+
}
|
|
100
|
+
allChildren.push(childData);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
if (allChildren.length === 0) continue;
|
|
104
|
+
await wrapNestedError(navField, () => targetTable.preValidateItems(allChildren, { excludeFkTargetTable: host.tableName }));
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
async function batchInsertNestedTo(host, items, maxDepth, depth) {
|
|
108
|
+
for (const [navField, relation] of host._meta.relations) {
|
|
109
|
+
if (relation.direction !== "to") continue;
|
|
110
|
+
const targetTable = host._writeTableResolver(relation.targetType());
|
|
111
|
+
if (!targetTable) continue;
|
|
112
|
+
const fk = host._findFKForRelation(relation);
|
|
113
|
+
if (!fk) continue;
|
|
114
|
+
const parents = [];
|
|
115
|
+
const sourceIndices = [];
|
|
116
|
+
for (let i = 0; i < items.length; i++) {
|
|
117
|
+
const nested = items[i][navField];
|
|
118
|
+
if (nested && typeof nested === "object" && !Array.isArray(nested)) {
|
|
119
|
+
parents.push(nested);
|
|
120
|
+
sourceIndices.push(i);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
if (parents.length === 0) continue;
|
|
124
|
+
const result = await targetTable.insertMany(parents, {
|
|
125
|
+
maxDepth,
|
|
126
|
+
_depth: depth + 1
|
|
127
|
+
});
|
|
128
|
+
for (let j = 0; j < sourceIndices.length; j++) if (fk.localFields.length === 1) items[sourceIndices[j]][fk.localFields[0]] = result.insertedIds[j];
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
async function batchInsertNestedFrom(host, originals, parentIds, maxDepth, depth) {
|
|
132
|
+
for (const [navField, relation] of host._meta.relations) {
|
|
133
|
+
if (relation.direction !== "from") continue;
|
|
134
|
+
const targetTable = host._writeTableResolver(relation.targetType());
|
|
135
|
+
if (!targetTable) continue;
|
|
136
|
+
const remoteFK = host._findRemoteFK(targetTable, host.tableName, relation.alias);
|
|
137
|
+
if (!remoteFK) continue;
|
|
138
|
+
const allChildren = [];
|
|
139
|
+
for (let i = 0; i < originals.length; i++) {
|
|
140
|
+
const children = originals[i][navField];
|
|
141
|
+
if (!Array.isArray(children)) continue;
|
|
142
|
+
for (const child of children) {
|
|
143
|
+
const childData = { ...child };
|
|
144
|
+
if (remoteFK.fields.length === 1) childData[remoteFK.fields[0]] = parentIds[i];
|
|
145
|
+
allChildren.push(childData);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
if (allChildren.length === 0) continue;
|
|
149
|
+
await wrapNestedError(navField, () => targetTable.insertMany(allChildren, {
|
|
150
|
+
maxDepth,
|
|
151
|
+
_depth: depth + 1
|
|
152
|
+
}));
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
async function batchInsertNestedVia(host, originals, parentIds, maxDepth, depth) {
|
|
156
|
+
for (const [navField, relation] of host._meta.relations) {
|
|
157
|
+
if (relation.direction !== "via" || !relation.viaType) continue;
|
|
158
|
+
const targetTable = host._writeTableResolver(relation.targetType());
|
|
159
|
+
if (!targetTable) continue;
|
|
160
|
+
const junctionTable = host._writeTableResolver(relation.viaType());
|
|
161
|
+
if (!junctionTable) continue;
|
|
162
|
+
const targetTableName = resolveRelationTargetTable(relation);
|
|
163
|
+
const fkToThis = host._findRemoteFK(junctionTable, host.tableName);
|
|
164
|
+
if (!fkToThis) continue;
|
|
165
|
+
const fkToTarget = host._findRemoteFK(junctionTable, targetTableName);
|
|
166
|
+
if (!fkToTarget) continue;
|
|
167
|
+
const targetPKField = targetTable.primaryKeys[0];
|
|
168
|
+
if (!targetPKField || fkToTarget.fields.length !== 1 || fkToThis.fields.length !== 1) continue;
|
|
169
|
+
for (let i = 0; i < originals.length; i++) {
|
|
170
|
+
const targets = originals[i][navField];
|
|
171
|
+
if (!Array.isArray(targets) || targets.length === 0) continue;
|
|
172
|
+
const parentPK = parentIds[i];
|
|
173
|
+
if (parentPK === undefined) continue;
|
|
174
|
+
const newTargets = [];
|
|
175
|
+
const existingIds = [];
|
|
176
|
+
for (const t of targets) {
|
|
177
|
+
const rec = t;
|
|
178
|
+
const pk = rec[targetPKField];
|
|
179
|
+
if (pk !== undefined && pk !== null) existingIds.push(pk);
|
|
180
|
+
else newTargets.push({ ...rec });
|
|
181
|
+
}
|
|
182
|
+
const allTargetIds = [...existingIds];
|
|
183
|
+
if (newTargets.length > 0) {
|
|
184
|
+
const targetResult = await targetTable.insertMany(newTargets, {
|
|
185
|
+
maxDepth,
|
|
186
|
+
_depth: depth + 1
|
|
187
|
+
});
|
|
188
|
+
allTargetIds.push(...targetResult.insertedIds);
|
|
189
|
+
}
|
|
190
|
+
if (allTargetIds.length > 0) {
|
|
191
|
+
const junctionRows = allTargetIds.map((targetId) => ({
|
|
192
|
+
[fkToThis.fields[0]]: parentPK,
|
|
193
|
+
[fkToTarget.fields[0]]: targetId
|
|
194
|
+
}));
|
|
195
|
+
await junctionTable.insertMany(junctionRows, { maxDepth: 0 });
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
async function batchReplaceNestedTo(host, items, maxDepth, depth) {
|
|
201
|
+
for (const [navField, relation] of host._meta.relations) {
|
|
202
|
+
if (relation.direction !== "to") continue;
|
|
203
|
+
const targetTable = host._writeTableResolver(relation.targetType());
|
|
204
|
+
if (!targetTable) continue;
|
|
205
|
+
const fk = host._findFKForRelation(relation);
|
|
206
|
+
if (!fk) continue;
|
|
207
|
+
const parents = [];
|
|
208
|
+
const sourceIndices = [];
|
|
209
|
+
for (let i = 0; i < items.length; i++) {
|
|
210
|
+
const nested = items[i][navField];
|
|
211
|
+
if (nested && typeof nested === "object" && !Array.isArray(nested)) {
|
|
212
|
+
parents.push(nested);
|
|
213
|
+
sourceIndices.push(i);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
if (parents.length === 0) continue;
|
|
217
|
+
await targetTable.bulkReplace(parents, {
|
|
218
|
+
maxDepth,
|
|
219
|
+
_depth: depth + 1
|
|
220
|
+
});
|
|
221
|
+
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]];
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
async function batchReplaceNestedFrom(host, originals, maxDepth, depth) {
|
|
225
|
+
for (const [navField, relation] of host._meta.relations) {
|
|
226
|
+
if (relation.direction !== "from") continue;
|
|
227
|
+
const targetTable = host._writeTableResolver(relation.targetType());
|
|
228
|
+
if (!targetTable) continue;
|
|
229
|
+
const remoteFK = host._findRemoteFK(targetTable, host.tableName, relation.alias);
|
|
230
|
+
if (!remoteFK) continue;
|
|
231
|
+
const childPKs = [...targetTable.primaryKeys];
|
|
232
|
+
for (const original of originals) {
|
|
233
|
+
const children = original[navField];
|
|
234
|
+
if (!Array.isArray(children)) continue;
|
|
235
|
+
const parentPK = host._meta.primaryKeys.length === 1 ? original[host._meta.primaryKeys[0]] : undefined;
|
|
236
|
+
if (parentPK === undefined || remoteFK.fields.length !== 1) continue;
|
|
237
|
+
await fromReplace(targetTable, children, parentPK, remoteFK.fields[0], childPKs, navField, maxDepth, depth);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
async function batchReplaceNestedVia(host, originals, maxDepth, depth) {
|
|
242
|
+
for (const [navField, relation] of host._meta.relations) {
|
|
243
|
+
if (relation.direction !== "via" || !relation.viaType) continue;
|
|
244
|
+
const targetTable = host._writeTableResolver(relation.targetType());
|
|
245
|
+
if (!targetTable) continue;
|
|
246
|
+
const junctionTable = host._writeTableResolver(relation.viaType());
|
|
247
|
+
if (!junctionTable) continue;
|
|
248
|
+
const targetTableName = resolveRelationTargetTable(relation);
|
|
249
|
+
const fkToThis = host._findRemoteFK(junctionTable, host.tableName);
|
|
250
|
+
if (!fkToThis) continue;
|
|
251
|
+
const fkToTarget = host._findRemoteFK(junctionTable, targetTableName);
|
|
252
|
+
if (!fkToTarget) continue;
|
|
253
|
+
const targetPKField = targetTable.primaryKeys[0];
|
|
254
|
+
if (!targetPKField || fkToTarget.fields.length !== 1 || fkToThis.fields.length !== 1) continue;
|
|
255
|
+
for (const original of originals) {
|
|
256
|
+
const targets = original[navField];
|
|
257
|
+
if (!Array.isArray(targets)) continue;
|
|
258
|
+
const parentPK = host._meta.primaryKeys.length === 1 ? original[host._meta.primaryKeys[0]] : undefined;
|
|
259
|
+
if (parentPK === undefined) continue;
|
|
260
|
+
await viaReplace(targetTable, junctionTable, targets, parentPK, targetPKField, fkToThis.fields[0], fkToTarget.fields[0], maxDepth, depth);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
async function batchPatchNestedTo(host, items, maxDepth, depth) {
|
|
265
|
+
for (const [navField, relation] of host._meta.relations) {
|
|
266
|
+
if (relation.direction !== "to") continue;
|
|
267
|
+
const targetTable = host._writeTableResolver(relation.targetType());
|
|
268
|
+
if (!targetTable) continue;
|
|
269
|
+
const fk = host._findFKForRelation(relation);
|
|
270
|
+
if (!fk) continue;
|
|
271
|
+
const patches = [];
|
|
272
|
+
for (const item of items) {
|
|
273
|
+
const nested = item[navField];
|
|
274
|
+
if (!nested || typeof nested !== "object" || Array.isArray(nested)) continue;
|
|
275
|
+
const patch = { ...nested };
|
|
276
|
+
let fkValue = fk.localFields.length === 1 ? item[fk.localFields[0]] : undefined;
|
|
277
|
+
if (fkValue === undefined) {
|
|
278
|
+
const pkFilter = host._extractPrimaryKeyFilter(item);
|
|
279
|
+
const current = await host.findOne({
|
|
280
|
+
filter: pkFilter,
|
|
281
|
+
controls: {}
|
|
282
|
+
});
|
|
283
|
+
if (!current) throw new DbError("NOT_FOUND", [{
|
|
284
|
+
path: navField,
|
|
285
|
+
message: `Cannot patch relation '${navField}' — source record not found`
|
|
286
|
+
}]);
|
|
287
|
+
fkValue = fk.localFields.length === 1 ? current[fk.localFields[0]] : undefined;
|
|
288
|
+
}
|
|
289
|
+
if (fkValue === null || fkValue === undefined) throw new DbError("FK_VIOLATION", [{
|
|
290
|
+
path: fk.localFields[0],
|
|
291
|
+
message: `Cannot patch relation '${navField}' — foreign key '${fk.localFields[0]}' is null`
|
|
292
|
+
}]);
|
|
293
|
+
if (fk.targetFields.length === 1) patch[fk.targetFields[0]] = fkValue;
|
|
294
|
+
patches.push(patch);
|
|
295
|
+
}
|
|
296
|
+
if (patches.length === 0) continue;
|
|
297
|
+
await targetTable.bulkUpdate(patches, {
|
|
298
|
+
maxDepth,
|
|
299
|
+
_depth: depth + 1
|
|
300
|
+
});
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
async function batchPatchNestedFrom(host, originals, maxDepth, depth) {
|
|
304
|
+
for (const [navField, relation] of host._meta.relations) {
|
|
305
|
+
if (relation.direction !== "from") continue;
|
|
306
|
+
const targetTable = host._writeTableResolver(relation.targetType());
|
|
307
|
+
if (!targetTable) continue;
|
|
308
|
+
const remoteFK = host._findRemoteFK(targetTable, host.tableName, relation.alias);
|
|
309
|
+
if (!remoteFK) continue;
|
|
310
|
+
const childPKs = [...targetTable.primaryKeys];
|
|
311
|
+
for (const original of originals) {
|
|
312
|
+
const navValue = original[navField];
|
|
313
|
+
if (navValue === undefined || navValue === null) continue;
|
|
314
|
+
const parentPK = host._meta.primaryKeys.length === 1 ? original[host._meta.primaryKeys[0]] : undefined;
|
|
315
|
+
if (parentPK === undefined || remoteFK.fields.length !== 1) continue;
|
|
316
|
+
const fkField = remoteFK.fields[0];
|
|
317
|
+
const ops = extractNavPatchOps(navValue);
|
|
318
|
+
if (ops.replace) await fromReplace(targetTable, ops.replace, parentPK, fkField, childPKs, navField, maxDepth, depth);
|
|
319
|
+
if (ops.remove && ops.remove.length > 0) {
|
|
320
|
+
const removeFilters = ops.remove.map((child) => {
|
|
321
|
+
const rec = child;
|
|
322
|
+
const f = {};
|
|
323
|
+
for (const pk of childPKs) f[pk] = rec[pk];
|
|
324
|
+
f[fkField] = parentPK;
|
|
325
|
+
return f;
|
|
326
|
+
});
|
|
327
|
+
if (removeFilters.length === 1) await targetTable.deleteMany(removeFilters[0]);
|
|
328
|
+
else await targetTable.deleteMany({ $or: removeFilters });
|
|
329
|
+
}
|
|
330
|
+
if (ops.update && ops.update.length > 0) {
|
|
331
|
+
const items = ops.update.map((child) => {
|
|
332
|
+
const rec = { ...child };
|
|
333
|
+
rec[fkField] = parentPK;
|
|
334
|
+
return rec;
|
|
335
|
+
});
|
|
336
|
+
await wrapNestedError(navField, () => targetTable.bulkUpdate(items, {
|
|
337
|
+
maxDepth,
|
|
338
|
+
_depth: depth + 1
|
|
339
|
+
}));
|
|
340
|
+
}
|
|
341
|
+
if (ops.upsert) {
|
|
342
|
+
const toUpdate = [];
|
|
343
|
+
const toInsert = [];
|
|
344
|
+
for (const child of ops.upsert) {
|
|
345
|
+
const rec = { ...child };
|
|
346
|
+
rec[fkField] = parentPK;
|
|
347
|
+
const hasPK = childPKs.length > 0 && childPKs.every((pk) => rec[pk] !== undefined);
|
|
348
|
+
if (hasPK) toUpdate.push(rec);
|
|
349
|
+
else toInsert.push(rec);
|
|
350
|
+
}
|
|
351
|
+
if (toUpdate.length > 0) await wrapNestedError(navField, () => targetTable.bulkUpdate(toUpdate, {
|
|
352
|
+
maxDepth,
|
|
353
|
+
_depth: depth + 1
|
|
354
|
+
}));
|
|
355
|
+
if (toInsert.length > 0) await wrapNestedError(navField, () => targetTable.insertMany(toInsert, {
|
|
356
|
+
maxDepth,
|
|
357
|
+
_depth: depth + 1
|
|
358
|
+
}));
|
|
359
|
+
}
|
|
360
|
+
if (ops.insert && ops.insert.length > 0) {
|
|
361
|
+
const items = ops.insert.map((child) => {
|
|
362
|
+
const rec = { ...child };
|
|
363
|
+
rec[fkField] = parentPK;
|
|
364
|
+
return rec;
|
|
365
|
+
});
|
|
366
|
+
await wrapNestedError(navField, () => targetTable.insertMany(items, {
|
|
367
|
+
maxDepth,
|
|
368
|
+
_depth: depth + 1
|
|
369
|
+
}));
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
async function batchPatchNestedVia(host, originals, maxDepth, depth) {
|
|
375
|
+
for (const [navField, relation] of host._meta.relations) {
|
|
376
|
+
if (relation.direction !== "via" || !relation.viaType) continue;
|
|
377
|
+
const targetTable = host._writeTableResolver(relation.targetType());
|
|
378
|
+
if (!targetTable) continue;
|
|
379
|
+
const junctionTable = host._writeTableResolver(relation.viaType());
|
|
380
|
+
if (!junctionTable) continue;
|
|
381
|
+
const targetTableName = resolveRelationTargetTable(relation);
|
|
382
|
+
const fkToThis = host._findRemoteFK(junctionTable, host.tableName);
|
|
383
|
+
if (!fkToThis) continue;
|
|
384
|
+
const fkToTarget = host._findRemoteFK(junctionTable, targetTableName);
|
|
385
|
+
if (!fkToTarget) continue;
|
|
386
|
+
const targetPKField = targetTable.primaryKeys[0];
|
|
387
|
+
if (!targetPKField || fkToTarget.fields.length !== 1 || fkToThis.fields.length !== 1) continue;
|
|
388
|
+
for (const original of originals) {
|
|
389
|
+
const navValue = original[navField];
|
|
390
|
+
if (navValue === undefined || navValue === null) continue;
|
|
391
|
+
const parentPK = host._meta.primaryKeys.length === 1 ? original[host._meta.primaryKeys[0]] : undefined;
|
|
392
|
+
if (parentPK === undefined) continue;
|
|
393
|
+
const ops = extractNavPatchOps(navValue);
|
|
394
|
+
if (ops.replace) await viaReplace(targetTable, junctionTable, ops.replace, parentPK, targetPKField, fkToThis.fields[0], fkToTarget.fields[0], maxDepth, depth);
|
|
395
|
+
if (ops.remove && ops.remove.length > 0) {
|
|
396
|
+
const targetPKs = ops.remove.map((t) => t[targetPKField]).filter((pk) => pk !== undefined && pk !== null);
|
|
397
|
+
if (targetPKs.length === 1) await junctionTable.deleteMany({
|
|
398
|
+
[fkToThis.fields[0]]: parentPK,
|
|
399
|
+
[fkToTarget.fields[0]]: targetPKs[0]
|
|
400
|
+
});
|
|
401
|
+
else if (targetPKs.length > 1) await junctionTable.deleteMany({
|
|
402
|
+
[fkToThis.fields[0]]: parentPK,
|
|
403
|
+
[fkToTarget.fields[0]]: { $in: targetPKs }
|
|
404
|
+
});
|
|
405
|
+
}
|
|
406
|
+
if (ops.update && ops.update.length > 0) await targetTable.bulkUpdate(ops.update.map((t) => ({ ...t })), {
|
|
407
|
+
maxDepth,
|
|
408
|
+
_depth: depth + 1
|
|
409
|
+
});
|
|
410
|
+
if (ops.upsert && ops.upsert.length > 0) {
|
|
411
|
+
const toUpdate = [];
|
|
412
|
+
const toInsert = [];
|
|
413
|
+
const existingPKs = [];
|
|
414
|
+
for (const target of ops.upsert) {
|
|
415
|
+
const rec = { ...target };
|
|
416
|
+
const pk = rec[targetPKField];
|
|
417
|
+
if (pk !== undefined && pk !== null) {
|
|
418
|
+
toUpdate.push(rec);
|
|
419
|
+
existingPKs.push(pk);
|
|
420
|
+
} else toInsert.push(rec);
|
|
421
|
+
}
|
|
422
|
+
if (toUpdate.length > 0) await targetTable.bulkUpdate(toUpdate, {
|
|
423
|
+
maxDepth,
|
|
424
|
+
_depth: depth + 1
|
|
425
|
+
});
|
|
426
|
+
if (existingPKs.length > 0) {
|
|
427
|
+
const existingJunctions = await junctionTable.findMany({
|
|
428
|
+
filter: {
|
|
429
|
+
[fkToThis.fields[0]]: parentPK,
|
|
430
|
+
[fkToTarget.fields[0]]: existingPKs.length === 1 ? existingPKs[0] : { $in: existingPKs }
|
|
431
|
+
},
|
|
432
|
+
controls: { $select: [fkToTarget.fields[0]] }
|
|
433
|
+
});
|
|
434
|
+
const existingSet = new Set(existingJunctions.map((j) => String(j[fkToTarget.fields[0]])));
|
|
435
|
+
const missingPKs = existingPKs.filter((pk) => !existingSet.has(String(pk)));
|
|
436
|
+
if (missingPKs.length > 0) {
|
|
437
|
+
const junctionRows = missingPKs.map((pk) => ({
|
|
438
|
+
[fkToThis.fields[0]]: parentPK,
|
|
439
|
+
[fkToTarget.fields[0]]: pk
|
|
440
|
+
}));
|
|
441
|
+
await junctionTable.insertMany(junctionRows, { maxDepth: 0 });
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
if (toInsert.length > 0) {
|
|
445
|
+
const insertResult = await targetTable.insertMany(toInsert, {
|
|
446
|
+
maxDepth,
|
|
447
|
+
_depth: depth + 1
|
|
448
|
+
});
|
|
449
|
+
const junctionRows = insertResult.insertedIds.map((newId) => ({
|
|
450
|
+
[fkToThis.fields[0]]: parentPK,
|
|
451
|
+
[fkToTarget.fields[0]]: newId
|
|
452
|
+
}));
|
|
453
|
+
await junctionTable.insertMany(junctionRows, { maxDepth: 0 });
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
if (ops.insert && ops.insert.length > 0) {
|
|
457
|
+
const toInsert = [];
|
|
458
|
+
const existingIds = [];
|
|
459
|
+
for (const target of ops.insert) {
|
|
460
|
+
const rec = { ...target };
|
|
461
|
+
const pk = rec[targetPKField];
|
|
462
|
+
if (pk !== undefined && pk !== null) existingIds.push(pk);
|
|
463
|
+
else toInsert.push(rec);
|
|
464
|
+
}
|
|
465
|
+
const allIds = [...existingIds];
|
|
466
|
+
if (toInsert.length > 0) {
|
|
467
|
+
const insertResult = await targetTable.insertMany(toInsert, {
|
|
468
|
+
maxDepth,
|
|
469
|
+
_depth: depth + 1
|
|
470
|
+
});
|
|
471
|
+
allIds.push(...insertResult.insertedIds);
|
|
472
|
+
}
|
|
473
|
+
if (allIds.length > 0) {
|
|
474
|
+
const junctionRows = allIds.map((targetId) => ({
|
|
475
|
+
[fkToThis.fields[0]]: parentPK,
|
|
476
|
+
[fkToTarget.fields[0]]: targetId
|
|
477
|
+
}));
|
|
478
|
+
await junctionTable.insertMany(junctionRows, { maxDepth: 0 });
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
/**
|
|
485
|
+
* Extracts patch operations from a nav field value.
|
|
486
|
+
* Plain array → $replace. Object with $insert, $remove, etc. → individual ops.
|
|
487
|
+
*/ function extractNavPatchOps(navValue) {
|
|
488
|
+
if (Array.isArray(navValue)) return { replace: navValue };
|
|
489
|
+
if (typeof navValue !== "object" || navValue === null) return {};
|
|
490
|
+
const obj = navValue;
|
|
491
|
+
return {
|
|
492
|
+
replace: obj.$replace !== undefined ? obj.$replace : undefined,
|
|
493
|
+
insert: obj.$insert !== undefined ? obj.$insert : undefined,
|
|
494
|
+
remove: obj.$remove !== undefined ? obj.$remove : undefined,
|
|
495
|
+
update: obj.$update !== undefined ? obj.$update : undefined,
|
|
496
|
+
upsert: obj.$upsert !== undefined ? obj.$upsert : undefined
|
|
497
|
+
};
|
|
498
|
+
}
|
|
499
|
+
/**
|
|
500
|
+
* FROM $replace helper: delete orphans, replace existing, insert new.
|
|
501
|
+
*/ async function fromReplace(targetTable, children, parentPK, fkField, childPKs, navField, maxDepth, depth) {
|
|
502
|
+
const toReplace = [];
|
|
503
|
+
const toInsert = [];
|
|
504
|
+
const newPKSet = new Set();
|
|
505
|
+
for (const child of children) {
|
|
506
|
+
const childData = { ...child };
|
|
507
|
+
childData[fkField] = parentPK;
|
|
508
|
+
const hasPK = childPKs.length > 0 && childPKs.every((pk) => childData[pk] !== undefined);
|
|
509
|
+
if (hasPK) {
|
|
510
|
+
newPKSet.add(childPKs.map((pk) => String(childData[pk])).join("\0"));
|
|
511
|
+
toReplace.push(childData);
|
|
512
|
+
} else toInsert.push(childData);
|
|
513
|
+
}
|
|
514
|
+
const existing = await targetTable.findMany({
|
|
515
|
+
filter: { [fkField]: parentPK },
|
|
516
|
+
controls: childPKs.length > 0 ? { $select: [...childPKs] } : {}
|
|
517
|
+
});
|
|
518
|
+
const orphanFilters = [];
|
|
519
|
+
for (const row of existing) {
|
|
520
|
+
const pkKey = childPKs.map((pk) => String(row[pk])).join("\0");
|
|
521
|
+
if (!newPKSet.has(pkKey)) {
|
|
522
|
+
const f = {};
|
|
523
|
+
for (const pk of childPKs) f[pk] = row[pk];
|
|
524
|
+
orphanFilters.push(f);
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
if (orphanFilters.length === 1) await targetTable.deleteMany(orphanFilters[0]);
|
|
528
|
+
else if (orphanFilters.length > 1) await targetTable.deleteMany({ $or: orphanFilters });
|
|
529
|
+
if (toReplace.length > 0) await wrapNestedError(navField, () => targetTable.bulkReplace(toReplace, {
|
|
530
|
+
maxDepth,
|
|
531
|
+
_depth: depth + 1
|
|
532
|
+
}));
|
|
533
|
+
if (toInsert.length > 0) await wrapNestedError(navField, () => targetTable.insertMany(toInsert, {
|
|
534
|
+
maxDepth,
|
|
535
|
+
_depth: depth + 1
|
|
536
|
+
}));
|
|
537
|
+
}
|
|
538
|
+
/**
|
|
539
|
+
* VIA $replace helper: clear junctions, replace/insert targets, rebuild junctions.
|
|
540
|
+
*/ async function viaReplace(targetTable, junctionTable, targets, parentPK, targetPKField, fkToThisField, fkToTargetField, maxDepth, depth) {
|
|
541
|
+
await junctionTable.deleteMany({ [fkToThisField]: parentPK });
|
|
542
|
+
const toReplace = [];
|
|
543
|
+
const toInsert = [];
|
|
544
|
+
const existingIds = [];
|
|
545
|
+
for (const t of targets) {
|
|
546
|
+
const rec = t;
|
|
547
|
+
const pk = rec[targetPKField];
|
|
548
|
+
if (pk !== undefined && pk !== null) {
|
|
549
|
+
const keys = Object.keys(rec).filter((k) => k !== targetPKField);
|
|
550
|
+
if (keys.length > 0) toReplace.push({ ...rec });
|
|
551
|
+
existingIds.push(pk);
|
|
552
|
+
} else toInsert.push({ ...rec });
|
|
553
|
+
}
|
|
554
|
+
if (toReplace.length > 0) await targetTable.bulkReplace(toReplace, {
|
|
555
|
+
maxDepth,
|
|
556
|
+
_depth: depth + 1
|
|
557
|
+
});
|
|
558
|
+
const allTargetIds = [...existingIds];
|
|
559
|
+
if (toInsert.length > 0) {
|
|
560
|
+
const insertResult = await targetTable.insertMany(toInsert, {
|
|
561
|
+
maxDepth,
|
|
562
|
+
_depth: depth + 1
|
|
563
|
+
});
|
|
564
|
+
allTargetIds.push(...insertResult.insertedIds);
|
|
565
|
+
}
|
|
566
|
+
if (allTargetIds.length > 0) {
|
|
567
|
+
const junctionRows = allTargetIds.map((targetId) => ({
|
|
568
|
+
[fkToThisField]: parentPK,
|
|
569
|
+
[fkToTargetField]: targetId
|
|
570
|
+
}));
|
|
571
|
+
await junctionTable.insertMany(junctionRows, { maxDepth: 0 });
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
//#endregion
|
|
576
|
+
export { DbError, batchInsertNestedFrom, batchInsertNestedTo, batchInsertNestedVia, batchPatchNestedFrom, batchPatchNestedTo, batchPatchNestedVia, batchReplaceNestedFrom, batchReplaceNestedTo, batchReplaceNestedVia, checkDepthOverflow, enrichFkViolation, preValidateNestedFrom, remapDeleteFkViolation, validateBatch };
|