@atscript/mongo 0.1.35 → 0.1.36
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/index.cjs +593 -177
- package/dist/index.d.ts +96 -57
- package/dist/index.mjs +596 -179
- package/package.json +4 -4
package/dist/index.cjs
CHANGED
|
@@ -24,26 +24,9 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
24
24
|
//#endregion
|
|
25
25
|
const __atscript_utils_db = __toESM(require("@atscript/utils-db"));
|
|
26
26
|
const mongodb = __toESM(require("mongodb"));
|
|
27
|
-
const __atscript_typescript_utils = __toESM(require("@atscript/typescript/utils"));
|
|
28
27
|
|
|
29
|
-
//#region packages/mongo/src/lib/logger.ts
|
|
30
|
-
const NoopLogger = {
|
|
31
|
-
error: () => {},
|
|
32
|
-
warn: () => {},
|
|
33
|
-
log: () => {},
|
|
34
|
-
info: () => {},
|
|
35
|
-
debug: () => {}
|
|
36
|
-
};
|
|
37
|
-
|
|
38
|
-
//#endregion
|
|
39
|
-
//#region packages/mongo/src/lib/validate-plugins.ts
|
|
40
|
-
const validateMongoIdPlugin = (ctx, def, value) => {
|
|
41
|
-
if (ctx.path === "_id" && def.type.tags.has("objectId")) return ctx.validateAnnotatedType(def, value instanceof mongodb.ObjectId ? value.toString() : value);
|
|
42
|
-
};
|
|
43
|
-
|
|
44
|
-
//#endregion
|
|
45
28
|
//#region packages/mongo/src/lib/collection-patcher.ts
|
|
46
|
-
function _define_property$
|
|
29
|
+
function _define_property$1(obj, key, value) {
|
|
47
30
|
if (key in obj) Object.defineProperty(obj, key, {
|
|
48
31
|
value,
|
|
49
32
|
enumerable: true,
|
|
@@ -53,50 +36,7 @@ function _define_property$2(obj, key, value) {
|
|
|
53
36
|
else obj[key] = value;
|
|
54
37
|
return obj;
|
|
55
38
|
}
|
|
56
|
-
var CollectionPatcher = class
|
|
57
|
-
/**
|
|
58
|
-
* Build a runtime *Validator* that understands the extended patch payload.
|
|
59
|
-
*
|
|
60
|
-
* * Adds per‑array *patch* wrappers (the `$replace`, `$insert`, … fields).
|
|
61
|
-
* * Honors `db.patch.strategy === "merge"` metadata.
|
|
62
|
-
*
|
|
63
|
-
* @param collection Target collection wrapper
|
|
64
|
-
* @returns Atscript Validator
|
|
65
|
-
*/ static prepareValidator(context) {
|
|
66
|
-
return context.createValidator({
|
|
67
|
-
plugins: [validateMongoIdPlugin],
|
|
68
|
-
replace: (def, path) => {
|
|
69
|
-
if (path === "" && def.type.kind === "object") {
|
|
70
|
-
const obj = (0, __atscript_typescript_utils.defineAnnotatedType)("object").copyMetadata(def.metadata);
|
|
71
|
-
for (const [prop, type] of def.type.props.entries()) obj.prop(prop, (0, __atscript_typescript_utils.defineAnnotatedType)().refTo(type).copyMetadata(type.metadata).optional(prop !== "_id").$type);
|
|
72
|
-
return obj.$type;
|
|
73
|
-
}
|
|
74
|
-
if (def.type.kind === "array" && context.flatMap.get(path)?.metadata.get("db.mongo.__topLevelArray") && !def.metadata.has("db.mongo.__patchArrayValue")) {
|
|
75
|
-
const defArray = def;
|
|
76
|
-
const mergeStrategy = defArray.metadata.get("db.patch.strategy") === "merge";
|
|
77
|
-
function getPatchType() {
|
|
78
|
-
const isPrimitive = (0, __atscript_typescript_utils.isAnnotatedTypeOfPrimitive)(defArray.type.of);
|
|
79
|
-
if (isPrimitive) return (0, __atscript_typescript_utils.defineAnnotatedType)().refTo(def).copyMetadata(def.metadata).annotate("db.mongo.__patchArrayValue").optional().$type;
|
|
80
|
-
if (defArray.type.of.type.kind === "object") {
|
|
81
|
-
const objType = defArray.type.of.type;
|
|
82
|
-
const t = (0, __atscript_typescript_utils.defineAnnotatedType)("object").copyMetadata(defArray.type.of.metadata);
|
|
83
|
-
const keyProps = CollectionPatcher.getKeyProps(defArray);
|
|
84
|
-
for (const [key, val] of objType.props.entries()) if (keyProps.size > 0) if (keyProps.has(key)) t.prop(key, (0, __atscript_typescript_utils.defineAnnotatedType)().refTo(val).copyMetadata(def.metadata).$type);
|
|
85
|
-
else t.prop(key, (0, __atscript_typescript_utils.defineAnnotatedType)().refTo(val).copyMetadata(def.metadata).optional().$type);
|
|
86
|
-
else t.prop(key, (0, __atscript_typescript_utils.defineAnnotatedType)().refTo(val).copyMetadata(def.metadata).optional(!!val.optional).$type);
|
|
87
|
-
return (0, __atscript_typescript_utils.defineAnnotatedType)("array").of(t.$type).copyMetadata(def.metadata).annotate("db.mongo.__patchArrayValue").optional().$type;
|
|
88
|
-
}
|
|
89
|
-
return undefined;
|
|
90
|
-
}
|
|
91
|
-
const fullType = (0, __atscript_typescript_utils.defineAnnotatedType)().refTo(def).copyMetadata(def.metadata).annotate("db.mongo.__patchArrayValue").optional().$type;
|
|
92
|
-
const patchType = getPatchType();
|
|
93
|
-
return patchType ? (0, __atscript_typescript_utils.defineAnnotatedType)("object").prop("$replace", fullType).prop("$insert", fullType).prop("$upsert", fullType).prop("$update", mergeStrategy ? patchType : fullType).prop("$remove", patchType).optional().$type : (0, __atscript_typescript_utils.defineAnnotatedType)("object").prop("$replace", fullType).prop("$insert", fullType).optional().$type;
|
|
94
|
-
}
|
|
95
|
-
return def;
|
|
96
|
-
},
|
|
97
|
-
partial: (def, path) => path !== "" && def.metadata.get("db.patch.strategy") === "merge"
|
|
98
|
-
});
|
|
99
|
-
}
|
|
39
|
+
var CollectionPatcher = class {
|
|
100
40
|
/**
|
|
101
41
|
* Entry point – walk the payload, build `filter`, `update` and `options`.
|
|
102
42
|
*
|
|
@@ -143,8 +83,8 @@ else t.prop(key, (0, __atscript_typescript_utils.defineAnnotatedType)().refTo(va
|
|
|
143
83
|
for (const [_key, value] of Object.entries(payload)) {
|
|
144
84
|
const key = evalKey(_key);
|
|
145
85
|
const flatType = this.collection.flatMap.get(key);
|
|
146
|
-
const topLevelArray = flatType?.metadata?.get("db.
|
|
147
|
-
if (typeof value === "object" && topLevelArray) this.parseArrayPatch(key, value, flatType);
|
|
86
|
+
const topLevelArray = flatType?.metadata?.get("db.__topLevelArray");
|
|
87
|
+
if (typeof value === "object" && !Array.isArray(value) && topLevelArray && !flatType?.metadata?.has("db.json")) this.parseArrayPatch(key, value, flatType);
|
|
148
88
|
else if (typeof value === "object" && flatType?.metadata?.get("db.patch.strategy") === "merge") this.flattenPayload(value, key);
|
|
149
89
|
else if (key !== "_id") this._set(key, value);
|
|
150
90
|
}
|
|
@@ -208,22 +148,37 @@ else this._set(key, { $concatArrays: [{ $ifNull: [`$${key}`, []] }, input] });
|
|
|
208
148
|
* `$upsert`
|
|
209
149
|
* - keyed → remove existing matching by key(s) then append candidate
|
|
210
150
|
* - unique → $setUnion (deep equality)
|
|
211
|
-
*/ _upsert(key, input, keys,
|
|
151
|
+
*/ _upsert(key, input, keys, flatType) {
|
|
212
152
|
if (!input?.length) return;
|
|
213
153
|
if (keys.length > 0) {
|
|
154
|
+
const mergeStrategy = flatType.metadata?.get("db.patch.strategy") === "merge";
|
|
155
|
+
const vars = {
|
|
156
|
+
acc: "$$value",
|
|
157
|
+
cand: "$$this"
|
|
158
|
+
};
|
|
159
|
+
let appendExpr = "$$cand";
|
|
160
|
+
if (mergeStrategy) {
|
|
161
|
+
vars.existing = { $arrayElemAt: [{ $filter: {
|
|
162
|
+
input: "$$value",
|
|
163
|
+
as: "el",
|
|
164
|
+
cond: this._keysEqual(keys, "$$el", "$$this")
|
|
165
|
+
} }, 0] };
|
|
166
|
+
appendExpr = { $cond: [
|
|
167
|
+
{ $ifNull: ["$$existing", false] },
|
|
168
|
+
{ $mergeObjects: ["$$existing", "$$cand"] },
|
|
169
|
+
"$$cand"
|
|
170
|
+
] };
|
|
171
|
+
}
|
|
214
172
|
this._set(key, { $reduce: {
|
|
215
173
|
input,
|
|
216
174
|
initialValue: { $ifNull: [`$${key}`, []] },
|
|
217
175
|
in: { $let: {
|
|
218
|
-
vars
|
|
219
|
-
acc: "$$value",
|
|
220
|
-
cand: "$$this"
|
|
221
|
-
},
|
|
176
|
+
vars,
|
|
222
177
|
in: { $concatArrays: [{ $filter: {
|
|
223
178
|
input: "$$acc",
|
|
224
179
|
as: "el",
|
|
225
180
|
cond: { $not: this._keysEqual(keys, "$$el", "$$cand") }
|
|
226
|
-
} }, [
|
|
181
|
+
} }, [appendExpr]] }
|
|
227
182
|
} }
|
|
228
183
|
} });
|
|
229
184
|
return;
|
|
@@ -274,15 +229,15 @@ else this._set(key, { $concatArrays: [{ $ifNull: [`$${key}`, []] }, input] });
|
|
|
274
229
|
else this._set(key, { $setDifference: [{ $ifNull: [`$${key}`, []] }, input] });
|
|
275
230
|
}
|
|
276
231
|
constructor(collection, payload) {
|
|
277
|
-
_define_property$
|
|
278
|
-
_define_property$
|
|
232
|
+
_define_property$1(this, "collection", void 0);
|
|
233
|
+
_define_property$1(this, "payload", void 0);
|
|
279
234
|
/**
|
|
280
235
|
* Internal accumulator: filter passed to `updateOne()`.
|
|
281
236
|
* Filled only with the `_id` field right now.
|
|
282
|
-
*/ _define_property$
|
|
283
|
-
/** MongoDB *update* document being built. */ _define_property$
|
|
284
|
-
/** Current `$set` stage being populated. */ _define_property$
|
|
285
|
-
/** Additional *options* (mainly `arrayFilters`). */ _define_property$
|
|
237
|
+
*/ _define_property$1(this, "filterObj", void 0);
|
|
238
|
+
/** MongoDB *update* document being built. */ _define_property$1(this, "updatePipeline", void 0);
|
|
239
|
+
/** Current `$set` stage being populated. */ _define_property$1(this, "currentSetStage", void 0);
|
|
240
|
+
/** Additional *options* (mainly `arrayFilters`). */ _define_property$1(this, "optionsObj", void 0);
|
|
286
241
|
this.collection = collection;
|
|
287
242
|
this.payload = payload;
|
|
288
243
|
this.filterObj = {};
|
|
@@ -291,7 +246,7 @@ else this._set(key, { $setDifference: [{ $ifNull: [`$${key}`, []] }, input] });
|
|
|
291
246
|
this.optionsObj = {};
|
|
292
247
|
}
|
|
293
248
|
};
|
|
294
|
-
_define_property$
|
|
249
|
+
_define_property$1(CollectionPatcher, "getKeyProps", __atscript_utils_db.getKeyProps);
|
|
295
250
|
|
|
296
251
|
//#endregion
|
|
297
252
|
//#region packages/mongo/src/lib/mongo-filter.ts
|
|
@@ -320,9 +275,21 @@ function buildMongoFilter(filter) {
|
|
|
320
275
|
return (0, __atscript_utils_db.walkFilter)(filter, mongoVisitor) ?? EMPTY;
|
|
321
276
|
}
|
|
322
277
|
|
|
278
|
+
//#endregion
|
|
279
|
+
//#region packages/mongo/src/lib/validate-plugins.ts
|
|
280
|
+
const validateMongoIdPlugin = (ctx, def, value) => {
|
|
281
|
+
if (def.type.tags?.has("objectId")) {
|
|
282
|
+
if (ctx.path === "_id" && (value === undefined || value === null)) {
|
|
283
|
+
const dbCtx = ctx.context;
|
|
284
|
+
if (dbCtx && (dbCtx.mode === "insert" || dbCtx.mode === "replace")) return true;
|
|
285
|
+
}
|
|
286
|
+
return ctx.validateAnnotatedType(def, value instanceof mongodb.ObjectId ? value.toString() : value);
|
|
287
|
+
}
|
|
288
|
+
};
|
|
289
|
+
|
|
323
290
|
//#endregion
|
|
324
291
|
//#region packages/mongo/src/lib/mongo-adapter.ts
|
|
325
|
-
function _define_property
|
|
292
|
+
function _define_property(obj, key, value) {
|
|
326
293
|
if (key in obj) Object.defineProperty(obj, key, {
|
|
327
294
|
value,
|
|
328
295
|
enumerable: true,
|
|
@@ -334,13 +301,14 @@ else obj[key] = value;
|
|
|
334
301
|
}
|
|
335
302
|
const INDEX_PREFIX = "atscript__";
|
|
336
303
|
const DEFAULT_INDEX_NAME = "DEFAULT";
|
|
304
|
+
const JOINED_PREFIX = "__joined_";
|
|
337
305
|
function mongoIndexKey(type, name) {
|
|
338
306
|
const cleanName = name.replace(/[^a-z0-9_.-]/gi, "_").replace(/_+/g, "_").slice(0, 127 - INDEX_PREFIX.length - type.length - 2);
|
|
339
307
|
return `${INDEX_PREFIX}${type}__${cleanName}`;
|
|
340
308
|
}
|
|
341
309
|
var MongoAdapter = class MongoAdapter extends __atscript_utils_db.BaseDbAdapter {
|
|
342
310
|
get _client() {
|
|
343
|
-
return this.
|
|
311
|
+
return this.client;
|
|
344
312
|
}
|
|
345
313
|
async _beginTransaction() {
|
|
346
314
|
if (this._txDisabled || !this._client) return undefined;
|
|
@@ -427,30 +395,260 @@ var MongoAdapter = class MongoAdapter extends __atscript_utils_db.BaseDbAdapter
|
|
|
427
395
|
getValidatorPlugins() {
|
|
428
396
|
return [validateMongoIdPlugin];
|
|
429
397
|
}
|
|
430
|
-
getTopLevelArrayTag() {
|
|
431
|
-
return "db.mongo.__topLevelArray";
|
|
432
|
-
}
|
|
433
398
|
getAdapterTableName(type) {
|
|
434
399
|
return undefined;
|
|
435
400
|
}
|
|
436
|
-
|
|
437
|
-
return
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
401
|
+
supportsNativeRelations() {
|
|
402
|
+
return true;
|
|
403
|
+
}
|
|
404
|
+
async loadRelations(rows, withRelations, relations, foreignKeys, tableResolver) {
|
|
405
|
+
if (rows.length === 0 || withRelations.length === 0) return;
|
|
406
|
+
const primaryKeys = this._table.primaryKeys;
|
|
407
|
+
const relMeta = [];
|
|
408
|
+
for (const withRel of withRelations) {
|
|
409
|
+
if (withRel.name.includes(".")) continue;
|
|
410
|
+
const relation = relations.get(withRel.name);
|
|
411
|
+
if (!relation) throw new Error(`Unknown relation "${withRel.name}" in $with. Available relations: ${[...relations.keys()].join(", ") || "(none)"}`);
|
|
412
|
+
const lookupResult = this._buildRelationLookup(withRel, relation, foreignKeys, tableResolver);
|
|
413
|
+
if (!lookupResult) continue;
|
|
414
|
+
relMeta.push({
|
|
415
|
+
name: withRel.name,
|
|
416
|
+
isArray: lookupResult.isArray,
|
|
417
|
+
relation,
|
|
418
|
+
nestedWith: this._extractNestedWith(withRel),
|
|
419
|
+
stages: lookupResult.stages
|
|
420
|
+
});
|
|
421
|
+
}
|
|
422
|
+
if (relMeta.length === 0) return;
|
|
423
|
+
const pkMatchFilter = this._buildPKMatchFilter(rows, primaryKeys);
|
|
424
|
+
if (pkMatchFilter) {
|
|
425
|
+
const pipeline = [{ $match: pkMatchFilter }];
|
|
426
|
+
for (const meta of relMeta) pipeline.push(...meta.stages);
|
|
427
|
+
const results = await this.collection.aggregate(pipeline, this._getSessionOpts()).toArray();
|
|
428
|
+
this._mergeRelationResults(rows, results, primaryKeys, relMeta);
|
|
429
|
+
} else for (const row of rows) for (const meta of relMeta) row[meta.name] = meta.isArray ? [] : null;
|
|
430
|
+
await this._loadNestedRelations(rows, relMeta, tableResolver);
|
|
431
|
+
}
|
|
432
|
+
/** Builds a $match filter to re-select source rows by PK. */ _buildPKMatchFilter(rows, primaryKeys) {
|
|
433
|
+
if (primaryKeys.length === 1) {
|
|
434
|
+
const pk = primaryKeys[0];
|
|
435
|
+
const values = new Set();
|
|
436
|
+
for (const row of rows) {
|
|
437
|
+
const v = row[pk];
|
|
438
|
+
if (v !== null && v !== undefined) values.add(v);
|
|
449
439
|
}
|
|
450
|
-
|
|
440
|
+
if (values.size === 0) return undefined;
|
|
441
|
+
return { [pk]: { $in: [...values] } };
|
|
442
|
+
}
|
|
443
|
+
const seen = new Set();
|
|
444
|
+
const orFilters = [];
|
|
445
|
+
for (const row of rows) {
|
|
446
|
+
const key = primaryKeys.map((pk) => String(row[pk] ?? "")).join("\0");
|
|
447
|
+
if (seen.has(key)) continue;
|
|
448
|
+
seen.add(key);
|
|
449
|
+
const condition = {};
|
|
450
|
+
let valid = true;
|
|
451
|
+
for (const pk of primaryKeys) {
|
|
452
|
+
const val = row[pk];
|
|
453
|
+
if (val === null || val === undefined) {
|
|
454
|
+
valid = false;
|
|
455
|
+
break;
|
|
456
|
+
}
|
|
457
|
+
condition[pk] = val;
|
|
458
|
+
}
|
|
459
|
+
if (valid) orFilters.push(condition);
|
|
460
|
+
}
|
|
461
|
+
if (orFilters.length === 0) return undefined;
|
|
462
|
+
return orFilters.length === 1 ? orFilters[0] : { $or: orFilters };
|
|
463
|
+
}
|
|
464
|
+
/** Dispatches to the correct $lookup builder based on relation direction. */ _buildRelationLookup(withRel, relation, foreignKeys, tableResolver) {
|
|
465
|
+
switch (relation.direction) {
|
|
466
|
+
case "to": return this._buildToLookup(withRel, relation, foreignKeys);
|
|
467
|
+
case "from": return this._buildFromLookup(withRel, relation, tableResolver);
|
|
468
|
+
case "via": return this._buildViaLookup(withRel, relation, tableResolver);
|
|
469
|
+
default: return undefined;
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
/** Builds `let` variable bindings and the corresponding `$expr` match for `$lookup`. */ _buildLookupJoin(localFields, remoteFields, varPrefix) {
|
|
473
|
+
const letVars = Object.fromEntries(localFields.map((f, i) => [`${varPrefix}${i}`, `$${f}`]));
|
|
474
|
+
const exprMatch = remoteFields.length === 1 ? { $eq: [`$${remoteFields[0]}`, `$$${varPrefix}0`] } : { $and: remoteFields.map((rf, i) => ({ $eq: [`$${rf}`, `$$${varPrefix}${i}`] })) };
|
|
475
|
+
return {
|
|
476
|
+
letVars,
|
|
477
|
+
exprMatch
|
|
478
|
+
};
|
|
479
|
+
}
|
|
480
|
+
/** $lookup for TO relations (FK is on this table → target). Always single-valued. */ _buildToLookup(withRel, relation, foreignKeys) {
|
|
481
|
+
const fk = this._findFKForRelationLookup(relation, foreignKeys);
|
|
482
|
+
if (!fk) return undefined;
|
|
483
|
+
const innerPipeline = this._buildLookupInnerPipeline(withRel, fk.targetFields);
|
|
484
|
+
const { letVars, exprMatch } = this._buildLookupJoin(fk.localFields, fk.targetFields, "fk_");
|
|
485
|
+
const stages = [{ $lookup: {
|
|
486
|
+
from: fk.targetTable,
|
|
487
|
+
let: letVars,
|
|
488
|
+
pipeline: [{ $match: { $expr: exprMatch } }, ...innerPipeline],
|
|
489
|
+
as: withRel.name
|
|
490
|
+
} }, { $unwind: {
|
|
491
|
+
path: `$${withRel.name}`,
|
|
492
|
+
preserveNullAndEmptyArrays: true
|
|
493
|
+
} }];
|
|
494
|
+
return {
|
|
495
|
+
stages,
|
|
496
|
+
isArray: false
|
|
497
|
+
};
|
|
498
|
+
}
|
|
499
|
+
/** $lookup for FROM relations (FK is on target → this table). */ _buildFromLookup(withRel, relation, tableResolver) {
|
|
500
|
+
const targetType = relation.targetType();
|
|
501
|
+
if (!targetType || !tableResolver) return undefined;
|
|
502
|
+
const targetMeta = tableResolver(targetType);
|
|
503
|
+
if (!targetMeta) return undefined;
|
|
504
|
+
const remoteFK = this._findRemoteFKFromMeta(targetMeta, this._table.tableName, relation.alias);
|
|
505
|
+
if (!remoteFK) return undefined;
|
|
506
|
+
const targetTableName = this._resolveRelTargetTableName(relation);
|
|
507
|
+
const innerPipeline = this._buildLookupInnerPipeline(withRel, remoteFK.fields);
|
|
508
|
+
const { letVars, exprMatch } = this._buildLookupJoin(remoteFK.targetFields, remoteFK.fields, "pk_");
|
|
509
|
+
const stages = [{ $lookup: {
|
|
510
|
+
from: targetTableName,
|
|
511
|
+
let: letVars,
|
|
512
|
+
pipeline: [{ $match: { $expr: exprMatch } }, ...innerPipeline],
|
|
513
|
+
as: withRel.name
|
|
514
|
+
} }];
|
|
515
|
+
if (!relation.isArray) stages.push({ $unwind: {
|
|
516
|
+
path: `$${withRel.name}`,
|
|
517
|
+
preserveNullAndEmptyArrays: true
|
|
518
|
+
} });
|
|
519
|
+
return {
|
|
520
|
+
stages,
|
|
521
|
+
isArray: relation.isArray
|
|
522
|
+
};
|
|
523
|
+
}
|
|
524
|
+
/** $lookup for VIA relations (M:N through junction table). Always array. */ _buildViaLookup(withRel, relation, tableResolver) {
|
|
525
|
+
if (!relation.viaType || !tableResolver) return undefined;
|
|
526
|
+
const junctionType = relation.viaType();
|
|
527
|
+
if (!junctionType) return undefined;
|
|
528
|
+
const junctionMeta = tableResolver(junctionType);
|
|
529
|
+
if (!junctionMeta) return undefined;
|
|
530
|
+
const junctionTableName = junctionType.metadata?.get("db.table") || junctionType.id || "";
|
|
531
|
+
const targetTableName = this._resolveRelTargetTableName(relation);
|
|
532
|
+
const fkToThis = this._findRemoteFKFromMeta(junctionMeta, this._table.tableName);
|
|
533
|
+
if (!fkToThis) return undefined;
|
|
534
|
+
const fkToTarget = this._findRemoteFKFromMeta(junctionMeta, targetTableName);
|
|
535
|
+
if (!fkToTarget) return undefined;
|
|
536
|
+
const innerPipeline = this._buildLookupInnerPipeline(withRel, fkToTarget.targetFields);
|
|
537
|
+
const { letVars, exprMatch } = this._buildLookupJoin(fkToThis.targetFields, fkToThis.fields, "pk_");
|
|
538
|
+
const stages = [{ $lookup: {
|
|
539
|
+
from: junctionTableName,
|
|
540
|
+
let: letVars,
|
|
541
|
+
pipeline: [
|
|
542
|
+
{ $match: { $expr: exprMatch } },
|
|
543
|
+
{ $lookup: {
|
|
544
|
+
from: targetTableName,
|
|
545
|
+
localField: fkToTarget.fields[0],
|
|
546
|
+
foreignField: fkToTarget.targetFields[0],
|
|
547
|
+
pipeline: innerPipeline,
|
|
548
|
+
as: "__target"
|
|
549
|
+
} },
|
|
550
|
+
{ $unwind: {
|
|
551
|
+
path: "$__target",
|
|
552
|
+
preserveNullAndEmptyArrays: false
|
|
553
|
+
} },
|
|
554
|
+
{ $replaceRoot: { newRoot: "$__target" } }
|
|
555
|
+
],
|
|
556
|
+
as: withRel.name
|
|
557
|
+
} }];
|
|
558
|
+
return {
|
|
559
|
+
stages,
|
|
560
|
+
isArray: true
|
|
561
|
+
};
|
|
451
562
|
}
|
|
452
|
-
|
|
453
|
-
|
|
563
|
+
/** Builds inner pipeline stages for relation controls ($sort, $limit, $skip, $select, filter). */ _buildLookupInnerPipeline(withRel, requiredFields) {
|
|
564
|
+
const pipeline = [];
|
|
565
|
+
const flatRel = withRel;
|
|
566
|
+
const nested = withRel.controls || {};
|
|
567
|
+
const filter = withRel.filter;
|
|
568
|
+
const sort = nested.$sort || flatRel.$sort;
|
|
569
|
+
const limit = nested.$limit ?? flatRel.$limit;
|
|
570
|
+
const skip = nested.$skip ?? flatRel.$skip;
|
|
571
|
+
const select = nested.$select || flatRel.$select;
|
|
572
|
+
if (filter && Object.keys(filter).length > 0) pipeline.push({ $match: buildMongoFilter(filter) });
|
|
573
|
+
if (sort) pipeline.push({ $sort: sort });
|
|
574
|
+
if (skip) pipeline.push({ $skip: skip });
|
|
575
|
+
if (limit !== null && limit !== undefined) pipeline.push({ $limit: limit });
|
|
576
|
+
if (select) {
|
|
577
|
+
const projection = {};
|
|
578
|
+
for (const f of select) projection[f] = 1;
|
|
579
|
+
for (const f of requiredFields) projection[f] = 1;
|
|
580
|
+
if (!select.includes("_id") && !requiredFields.includes("_id")) projection["_id"] = 0;
|
|
581
|
+
pipeline.push({ $project: projection });
|
|
582
|
+
}
|
|
583
|
+
return pipeline;
|
|
584
|
+
}
|
|
585
|
+
/** Extracts nested $with from a WithRelation's controls. */ _extractNestedWith(withRel) {
|
|
586
|
+
const flatRel = withRel;
|
|
587
|
+
const nested = withRel.controls || {};
|
|
588
|
+
const nestedWith = nested.$with || flatRel.$with;
|
|
589
|
+
return nestedWith && nestedWith.length > 0 ? nestedWith : undefined;
|
|
590
|
+
}
|
|
591
|
+
/** Post-processes nested $with by delegating to the target table's own relation loading. */ async _loadNestedRelations(rows, relMeta, tableResolver) {
|
|
592
|
+
if (!tableResolver) return;
|
|
593
|
+
const tasks = [];
|
|
594
|
+
for (const meta of relMeta) {
|
|
595
|
+
if (!meta.nestedWith || meta.nestedWith.length === 0) continue;
|
|
596
|
+
const targetType = meta.relation.targetType();
|
|
597
|
+
if (!targetType) continue;
|
|
598
|
+
const targetTable = tableResolver(targetType);
|
|
599
|
+
if (!targetTable) continue;
|
|
600
|
+
const subRows = [];
|
|
601
|
+
for (const row of rows) {
|
|
602
|
+
const val = row[meta.name];
|
|
603
|
+
if (meta.isArray && Array.isArray(val)) for (const item of val) subRows.push(item);
|
|
604
|
+
else if (val && typeof val === "object") subRows.push(val);
|
|
605
|
+
}
|
|
606
|
+
if (subRows.length === 0) continue;
|
|
607
|
+
tasks.push(targetTable.loadRelations(subRows, meta.nestedWith));
|
|
608
|
+
}
|
|
609
|
+
await Promise.all(tasks);
|
|
610
|
+
}
|
|
611
|
+
/** Merges aggregation results back onto the original rows by PK. */ _mergeRelationResults(rows, results, primaryKeys, relMeta) {
|
|
612
|
+
const resultIndex = new Map();
|
|
613
|
+
for (const doc of results) {
|
|
614
|
+
const key = primaryKeys.map((pk) => String(doc[pk] ?? "")).join("\0");
|
|
615
|
+
resultIndex.set(key, doc);
|
|
616
|
+
}
|
|
617
|
+
for (const row of rows) {
|
|
618
|
+
const key = primaryKeys.map((pk) => String(row[pk] ?? "")).join("\0");
|
|
619
|
+
const enriched = resultIndex.get(key);
|
|
620
|
+
for (const meta of relMeta) if (enriched) {
|
|
621
|
+
const value = enriched[meta.name];
|
|
622
|
+
if (!meta.isArray && Array.isArray(value)) row[meta.name] = value[0] ?? null;
|
|
623
|
+
else row[meta.name] = value ?? (meta.isArray ? [] : null);
|
|
624
|
+
} else row[meta.name] = meta.isArray ? [] : null;
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
/** Finds FK entry for a TO relation from this table's foreignKeys map. */ _findFKForRelationLookup(relation, foreignKeys) {
|
|
628
|
+
const targetTableName = this._resolveRelTargetTableName(relation);
|
|
629
|
+
for (const fk of foreignKeys.values()) if (relation.alias) {
|
|
630
|
+
if (fk.alias === relation.alias) return {
|
|
631
|
+
localFields: fk.fields,
|
|
632
|
+
targetFields: fk.targetFields,
|
|
633
|
+
targetTable: fk.targetTable
|
|
634
|
+
};
|
|
635
|
+
} else if (fk.targetTable === targetTableName) return {
|
|
636
|
+
localFields: fk.fields,
|
|
637
|
+
targetFields: fk.targetFields,
|
|
638
|
+
targetTable: fk.targetTable
|
|
639
|
+
};
|
|
640
|
+
return undefined;
|
|
641
|
+
}
|
|
642
|
+
/** Finds a FK on a remote table that points back to the given table name. */ _findRemoteFKFromMeta(target, thisTableName, alias) {
|
|
643
|
+
for (const fk of target.foreignKeys.values()) {
|
|
644
|
+
if (alias && fk.alias === alias && fk.targetTable === thisTableName) return fk;
|
|
645
|
+
if (!alias && fk.targetTable === thisTableName) return fk;
|
|
646
|
+
}
|
|
647
|
+
return undefined;
|
|
648
|
+
}
|
|
649
|
+
/** Resolves the target table/collection name from a relation's target type. */ _resolveRelTargetTableName(relation) {
|
|
650
|
+
const targetType = relation.targetType();
|
|
651
|
+
return targetType?.metadata?.get("db.table") || targetType?.id || "";
|
|
454
652
|
}
|
|
455
653
|
/** Returns the context object used by CollectionPatcher. */ getPatcherContext() {
|
|
456
654
|
return {
|
|
@@ -493,8 +691,8 @@ var MongoAdapter = class MongoAdapter extends __atscript_utils_db.BaseDbAdapter
|
|
|
493
691
|
});
|
|
494
692
|
}
|
|
495
693
|
onFieldScanned(field, type, metadata) {
|
|
694
|
+
if (field === "_id") this._hasExplicitId = true;
|
|
496
695
|
if (field !== "_id" && metadata.has("meta.id")) {
|
|
497
|
-
this._table.removePrimaryKey(field);
|
|
498
696
|
this._addMongoIndexField("unique", "__pk", field);
|
|
499
697
|
this._table.addUniqueField(field);
|
|
500
698
|
}
|
|
@@ -519,7 +717,28 @@ var MongoAdapter = class MongoAdapter extends __atscript_utils_db.BaseDbAdapter
|
|
|
519
717
|
for (const index of metadata.get("db.mongo.search.filter") || []) this._vectorFilters.set(mongoIndexKey("vector", index.indexName), field);
|
|
520
718
|
}
|
|
521
719
|
onAfterFlatten() {
|
|
522
|
-
this.
|
|
720
|
+
if (this._hasExplicitId) {
|
|
721
|
+
this._table.addPrimaryKey("_id");
|
|
722
|
+
for (const field of this._table.originalMetaIdFields) if (field !== "_id") this._table.removePrimaryKey(field);
|
|
723
|
+
} else {
|
|
724
|
+
this._table.flatMap.set("_id", {
|
|
725
|
+
__is_atscript_annotated_type: true,
|
|
726
|
+
type: {
|
|
727
|
+
kind: "",
|
|
728
|
+
designType: "string",
|
|
729
|
+
tags: new Set(["objectId", "mongo"])
|
|
730
|
+
},
|
|
731
|
+
metadata: new Map()
|
|
732
|
+
});
|
|
733
|
+
this._table.addUniqueField("_id");
|
|
734
|
+
}
|
|
735
|
+
if (this._table.navFields.size > 0) {
|
|
736
|
+
const isUnderNav = (path) => {
|
|
737
|
+
for (const nav of this._table.navFields) if (path.startsWith(`${nav}.`)) return true;
|
|
738
|
+
return false;
|
|
739
|
+
};
|
|
740
|
+
for (const field of this._incrementFields) if (isUnderNav(field)) this._incrementFields.delete(field);
|
|
741
|
+
}
|
|
523
742
|
for (const [key, value] of this._vectorFilters.entries()) {
|
|
524
743
|
const index = this._mongoIndexes.get(key);
|
|
525
744
|
if (index && index.type === "vector") index.definition.fields?.push({
|
|
@@ -577,7 +796,7 @@ var MongoAdapter = class MongoAdapter extends __atscript_utils_db.BaseDbAdapter
|
|
|
577
796
|
/**
|
|
578
797
|
* Builds a MongoDB `$search` pipeline stage.
|
|
579
798
|
* Override `buildVectorSearchStage` in subclasses to provide embeddings.
|
|
580
|
-
*/ buildSearchStage(text, indexName) {
|
|
799
|
+
*/ async buildSearchStage(text, indexName) {
|
|
581
800
|
const index = this.getMongoSearchIndex(indexName);
|
|
582
801
|
if (!index) return undefined;
|
|
583
802
|
if (index.type === "vector") return this.buildVectorSearchStage(text, index);
|
|
@@ -592,11 +811,11 @@ var MongoAdapter = class MongoAdapter extends __atscript_utils_db.BaseDbAdapter
|
|
|
592
811
|
/**
|
|
593
812
|
* Builds a vector search stage. Override in subclasses to generate embeddings.
|
|
594
813
|
* Returns `undefined` by default (vector search requires custom implementation).
|
|
595
|
-
*/ buildVectorSearchStage(text, index) {
|
|
814
|
+
*/ async buildVectorSearchStage(text, index) {
|
|
596
815
|
return undefined;
|
|
597
816
|
}
|
|
598
817
|
async search(text, query, indexName) {
|
|
599
|
-
const searchStage = this.buildSearchStage(text, indexName);
|
|
818
|
+
const searchStage = await this.buildSearchStage(text, indexName);
|
|
600
819
|
if (!searchStage) throw new Error(indexName ? `Search index "${indexName}" not found` : "No search index available");
|
|
601
820
|
const filter = buildMongoFilter(query.filter);
|
|
602
821
|
const controls = query.controls || {};
|
|
@@ -610,7 +829,7 @@ else pipeline.push({ $limit: 1e3 });
|
|
|
610
829
|
return this.collection.aggregate(pipeline, this._getSessionOpts()).toArray();
|
|
611
830
|
}
|
|
612
831
|
async searchWithCount(text, query, indexName) {
|
|
613
|
-
const searchStage = this.buildSearchStage(text, indexName);
|
|
832
|
+
const searchStage = await this.buildSearchStage(text, indexName);
|
|
614
833
|
if (!searchStage) throw new Error(indexName ? `Search index "${indexName}" not found` : "No search index available");
|
|
615
834
|
const filter = buildMongoFilter(query.filter);
|
|
616
835
|
const controls = query.controls || {};
|
|
@@ -654,7 +873,6 @@ else pipeline.push({ $limit: 1e3 });
|
|
|
654
873
|
};
|
|
655
874
|
}
|
|
656
875
|
async collectionExists() {
|
|
657
|
-
if (this.asMongo) return this.asMongo.collectionExists(this._table.tableName);
|
|
658
876
|
const cols = await this.db.listCollections({ name: this._table.tableName }).toArray();
|
|
659
877
|
return cols.length > 0;
|
|
660
878
|
}
|
|
@@ -671,39 +889,48 @@ else pipeline.push({ $limit: 1e3 });
|
|
|
671
889
|
await this.db.createCollection(this._table.tableName, opts);
|
|
672
890
|
}
|
|
673
891
|
}
|
|
892
|
+
/**
|
|
893
|
+
* Wraps an async operation to catch MongoDB duplicate key errors
|
|
894
|
+
* (code 11000) and rethrow as structured `DbError`.
|
|
895
|
+
*/ async _wrapDuplicateKeyError(fn) {
|
|
896
|
+
try {
|
|
897
|
+
return await fn();
|
|
898
|
+
} catch (e) {
|
|
899
|
+
if (e instanceof mongodb.MongoServerError && e.code === 11e3) {
|
|
900
|
+
const field = e.keyPattern ? Object.keys(e.keyPattern)[0] ?? "" : "";
|
|
901
|
+
throw new __atscript_utils_db.DbError("CONFLICT", [{
|
|
902
|
+
path: field,
|
|
903
|
+
message: e.message
|
|
904
|
+
}]);
|
|
905
|
+
}
|
|
906
|
+
throw e;
|
|
907
|
+
}
|
|
908
|
+
}
|
|
674
909
|
async insertOne(data) {
|
|
675
910
|
if (this._incrementFields.size > 0) {
|
|
676
911
|
const fields = this._fieldsNeedingIncrement(data);
|
|
677
912
|
if (fields.length > 0) {
|
|
678
|
-
const
|
|
679
|
-
for (const physical of fields) data[physical] =
|
|
913
|
+
const nextValues = await this._allocateIncrementValues(fields, 1);
|
|
914
|
+
for (const physical of fields) data[physical] = nextValues.get(physical) ?? 1;
|
|
680
915
|
}
|
|
681
916
|
}
|
|
682
917
|
this._log("insertOne", data);
|
|
683
|
-
const result = await this.collection.insertOne(data, this._getSessionOpts());
|
|
684
|
-
|
|
918
|
+
const result = await this._wrapDuplicateKeyError(() => this.collection.insertOne(data, this._getSessionOpts()));
|
|
919
|
+
const metaIdPhysical = this._getMetaIdPhysical();
|
|
920
|
+
return { insertedId: metaIdPhysical ? data[metaIdPhysical] ?? result.insertedId : result.insertedId };
|
|
685
921
|
}
|
|
686
922
|
async insertMany(data) {
|
|
687
923
|
if (this._incrementFields.size > 0) {
|
|
688
924
|
const allFields = new Set();
|
|
689
925
|
for (const item of data) for (const f of this._fieldsNeedingIncrement(item)) allFields.add(f);
|
|
690
|
-
if (allFields.size > 0)
|
|
691
|
-
const maxValues = await this._getMaxValues([...allFields]);
|
|
692
|
-
for (const item of data) for (const physical of allFields) if (item[physical] === undefined || item[physical] === null) {
|
|
693
|
-
const next = (maxValues.get(physical) ?? 0) + 1;
|
|
694
|
-
item[physical] = next;
|
|
695
|
-
maxValues.set(physical, next);
|
|
696
|
-
} else if (typeof item[physical] === "number") {
|
|
697
|
-
const current = maxValues.get(physical) ?? 0;
|
|
698
|
-
if (item[physical] > current) maxValues.set(physical, item[physical]);
|
|
699
|
-
}
|
|
700
|
-
}
|
|
926
|
+
if (allFields.size > 0) await this._assignBatchIncrements(data, allFields);
|
|
701
927
|
}
|
|
702
928
|
this._log("insertMany", `${data.length} docs`);
|
|
703
|
-
const result = await this.collection.insertMany(data, this._getSessionOpts());
|
|
929
|
+
const result = await this._wrapDuplicateKeyError(() => this.collection.insertMany(data, this._getSessionOpts()));
|
|
930
|
+
const metaIdPhysical = this._getMetaIdPhysical();
|
|
704
931
|
return {
|
|
705
932
|
insertedCount: result.insertedCount,
|
|
706
|
-
insertedIds: Object.values(result.insertedIds)
|
|
933
|
+
insertedIds: metaIdPhysical ? data.map((item, i) => item[metaIdPhysical] ?? result.insertedIds[i]) : Object.values(result.insertedIds)
|
|
707
934
|
};
|
|
708
935
|
}
|
|
709
936
|
async findOne(query) {
|
|
@@ -741,7 +968,7 @@ else pipeline.push({ $limit: 1e3 });
|
|
|
741
968
|
async replaceOne(filter, data) {
|
|
742
969
|
const mongoFilter = buildMongoFilter(filter);
|
|
743
970
|
this._log("replaceOne", mongoFilter, data);
|
|
744
|
-
const result = await this.collection.replaceOne(mongoFilter, data, this._getSessionOpts());
|
|
971
|
+
const result = await this._wrapDuplicateKeyError(() => this.collection.replaceOne(mongoFilter, data, this._getSessionOpts()));
|
|
745
972
|
return {
|
|
746
973
|
matchedCount: result.matchedCount,
|
|
747
974
|
modifiedCount: result.modifiedCount
|
|
@@ -777,14 +1004,180 @@ else pipeline.push({ $limit: 1e3 });
|
|
|
777
1004
|
const result = await this.collection.deleteMany(mongoFilter, this._getSessionOpts());
|
|
778
1005
|
return { deletedCount: result.deletedCount };
|
|
779
1006
|
}
|
|
1007
|
+
async tableExists() {
|
|
1008
|
+
return this.collectionExists();
|
|
1009
|
+
}
|
|
1010
|
+
async detectTableOptionDrift() {
|
|
1011
|
+
if (!this._cappedOptions) return false;
|
|
1012
|
+
const cols = await this.db.listCollections({ name: this._table.tableName }, { nameOnly: false }).toArray();
|
|
1013
|
+
if (cols.length === 0) return false;
|
|
1014
|
+
const opts = cols[0].options;
|
|
1015
|
+
if (!opts?.capped) return true;
|
|
1016
|
+
if (opts.size !== this._cappedOptions.size) return true;
|
|
1017
|
+
if ((opts.max ?? undefined) !== (this._cappedOptions.max ?? undefined)) return true;
|
|
1018
|
+
return false;
|
|
1019
|
+
}
|
|
780
1020
|
async ensureTable() {
|
|
1021
|
+
if (this._table instanceof __atscript_utils_db.AtscriptDbView && !this._table.isExternal) return this._ensureView(this._table);
|
|
781
1022
|
return this.ensureCollectionExists();
|
|
782
1023
|
}
|
|
1024
|
+
/**
|
|
1025
|
+
* Creates a MongoDB view from the AtscriptDbView's view plan.
|
|
1026
|
+
* Translates joins → $lookup/$unwind, filter → $match, columns → $project.
|
|
1027
|
+
*/ async _ensureView(view) {
|
|
1028
|
+
const exists = await this.collectionExists();
|
|
1029
|
+
if (exists) return;
|
|
1030
|
+
const plan = view.viewPlan;
|
|
1031
|
+
const columns = view.getViewColumnMappings();
|
|
1032
|
+
const pipeline = [];
|
|
1033
|
+
for (const join of plan.joins) {
|
|
1034
|
+
const { localField, foreignField } = this._resolveJoinFields(join.condition, plan.entryTable, join.targetTable);
|
|
1035
|
+
pipeline.push({ $lookup: {
|
|
1036
|
+
from: join.targetTable,
|
|
1037
|
+
localField,
|
|
1038
|
+
foreignField,
|
|
1039
|
+
as: `${JOINED_PREFIX}${join.targetTable}`
|
|
1040
|
+
} });
|
|
1041
|
+
pipeline.push({ $unwind: {
|
|
1042
|
+
path: `$__joined_${join.targetTable}`,
|
|
1043
|
+
preserveNullAndEmptyArrays: true
|
|
1044
|
+
} });
|
|
1045
|
+
}
|
|
1046
|
+
if (plan.filter) {
|
|
1047
|
+
const matchExpr = this._queryNodeToMatch(plan.filter, plan.entryTable);
|
|
1048
|
+
pipeline.push({ $match: matchExpr });
|
|
1049
|
+
}
|
|
1050
|
+
const project = { _id: 0 };
|
|
1051
|
+
for (const col of columns) if (col.sourceTable === plan.entryTable) project[col.viewColumn] = `$${col.sourceColumn}`;
|
|
1052
|
+
else project[col.viewColumn] = `$${JOINED_PREFIX}${col.sourceTable}.${col.sourceColumn}`;
|
|
1053
|
+
pipeline.push({ $project: project });
|
|
1054
|
+
this._log("createView", this._table.tableName, plan.entryTable, pipeline);
|
|
1055
|
+
await this.db.createCollection(this._table.tableName, {
|
|
1056
|
+
viewOn: plan.entryTable,
|
|
1057
|
+
pipeline
|
|
1058
|
+
});
|
|
1059
|
+
}
|
|
1060
|
+
/**
|
|
1061
|
+
* Extracts localField/foreignField from a join condition like `User.id = Task.assigneeId`.
|
|
1062
|
+
* The condition is a comparison node with two field refs.
|
|
1063
|
+
*/ _resolveJoinFields(condition, entryTable, joinTable) {
|
|
1064
|
+
const comp = "$and" in condition ? condition.$and[0] : condition;
|
|
1065
|
+
const c = comp;
|
|
1066
|
+
const leftTable = c.left.type ? c.left.type()?.metadata?.get("db.table") || "" : entryTable;
|
|
1067
|
+
if (leftTable === joinTable) return {
|
|
1068
|
+
localField: c.right.field,
|
|
1069
|
+
foreignField: c.left.field
|
|
1070
|
+
};
|
|
1071
|
+
return {
|
|
1072
|
+
localField: c.left.field,
|
|
1073
|
+
foreignField: c.right.field
|
|
1074
|
+
};
|
|
1075
|
+
}
|
|
1076
|
+
/**
|
|
1077
|
+
* Translates an AtscriptQueryNode to a MongoDB $match expression.
|
|
1078
|
+
* Field refs are resolved to dot-path references (joined fields use JOINED_PREFIX).
|
|
1079
|
+
*/ _queryNodeToMatch(node, entryTable) {
|
|
1080
|
+
if ("$and" in node) return { $and: node.$and.map((n) => this._queryNodeToMatch(n, entryTable)) };
|
|
1081
|
+
if ("$or" in node) return { $or: node.$or.map((n) => this._queryNodeToMatch(n, entryTable)) };
|
|
1082
|
+
if ("$not" in node) return { $not: this._queryNodeToMatch(node.$not, entryTable) };
|
|
1083
|
+
const comp = node;
|
|
1084
|
+
const fieldPath = this._resolveViewFieldPath(comp.left, entryTable);
|
|
1085
|
+
if (comp.right && typeof comp.right === "object" && "field" in comp.right) {
|
|
1086
|
+
const rightPath = this._resolveViewFieldPath(comp.right, entryTable);
|
|
1087
|
+
return { $expr: { [comp.op]: [`$${fieldPath}`, `$${rightPath}`] } };
|
|
1088
|
+
}
|
|
1089
|
+
if (comp.op === "$eq") return { [fieldPath]: comp.right };
|
|
1090
|
+
if (comp.op === "$ne") return { [fieldPath]: { $ne: comp.right } };
|
|
1091
|
+
return { [fieldPath]: { [comp.op]: comp.right } };
|
|
1092
|
+
}
|
|
1093
|
+
/**
|
|
1094
|
+
* Resolves a field ref to a MongoDB dot path for view pipeline expressions.
|
|
1095
|
+
*/ _resolveViewFieldPath(ref, entryTable) {
|
|
1096
|
+
if (!ref.type) return ref.field;
|
|
1097
|
+
const table = ref.type()?.metadata?.get("db.table") || "";
|
|
1098
|
+
if (table === entryTable) return ref.field;
|
|
1099
|
+
return `${JOINED_PREFIX}${table}.${ref.field}`;
|
|
1100
|
+
}
|
|
783
1101
|
async dropTable() {
|
|
784
1102
|
this._log("drop", this._table.tableName);
|
|
785
1103
|
await this.collection.drop();
|
|
786
1104
|
this._collection = undefined;
|
|
787
1105
|
}
|
|
1106
|
+
async dropViewByName(viewName) {
|
|
1107
|
+
this._log("dropView", viewName);
|
|
1108
|
+
try {
|
|
1109
|
+
await this.db.collection(viewName).drop();
|
|
1110
|
+
} catch {}
|
|
1111
|
+
}
|
|
1112
|
+
async dropTableByName(tableName) {
|
|
1113
|
+
this._log("dropByName", tableName);
|
|
1114
|
+
try {
|
|
1115
|
+
await this.db.collection(tableName).drop();
|
|
1116
|
+
} catch {}
|
|
1117
|
+
}
|
|
1118
|
+
async recreateTable() {
|
|
1119
|
+
const tableName = this._table.tableName;
|
|
1120
|
+
this._log("recreateTable", tableName);
|
|
1121
|
+
const tempName = `${tableName}__tmp_${Date.now()}`;
|
|
1122
|
+
const source = this.db.collection(tableName);
|
|
1123
|
+
const count = await source.countDocuments();
|
|
1124
|
+
if (count > 0) await source.aggregate([{ $out: tempName }]).toArray();
|
|
1125
|
+
await this.collection.drop();
|
|
1126
|
+
this._collection = undefined;
|
|
1127
|
+
await this.ensureCollectionExists();
|
|
1128
|
+
if (count > 0) {
|
|
1129
|
+
const temp = this.db.collection(tempName);
|
|
1130
|
+
await temp.aggregate([{ $merge: { into: tableName } }]).toArray();
|
|
1131
|
+
await temp.drop();
|
|
1132
|
+
}
|
|
1133
|
+
}
|
|
1134
|
+
async syncColumns(diff) {
|
|
1135
|
+
const renamed = [];
|
|
1136
|
+
const added = [];
|
|
1137
|
+
const update = {};
|
|
1138
|
+
if (diff.renamed.length > 0) {
|
|
1139
|
+
const renameSpec = {};
|
|
1140
|
+
for (const r of diff.renamed) {
|
|
1141
|
+
renameSpec[r.oldName] = r.field.physicalName;
|
|
1142
|
+
renamed.push(r.field.physicalName);
|
|
1143
|
+
}
|
|
1144
|
+
update.$rename = renameSpec;
|
|
1145
|
+
}
|
|
1146
|
+
if (diff.added.length > 0) {
|
|
1147
|
+
const setSpec = {};
|
|
1148
|
+
for (const field of diff.added) {
|
|
1149
|
+
const defaultVal = this._resolveSyncDefault(field);
|
|
1150
|
+
if (defaultVal !== undefined) setSpec[field.physicalName] = defaultVal;
|
|
1151
|
+
added.push(field.physicalName);
|
|
1152
|
+
}
|
|
1153
|
+
if (Object.keys(setSpec).length > 0) update.$set = setSpec;
|
|
1154
|
+
}
|
|
1155
|
+
if (Object.keys(update).length > 0) await this.collection.updateMany({}, update, this._getSessionOpts());
|
|
1156
|
+
return {
|
|
1157
|
+
added,
|
|
1158
|
+
renamed
|
|
1159
|
+
};
|
|
1160
|
+
}
|
|
1161
|
+
async dropColumns(columns) {
|
|
1162
|
+
if (columns.length === 0) return;
|
|
1163
|
+
const unsetSpec = {};
|
|
1164
|
+
for (const col of columns) unsetSpec[col] = "";
|
|
1165
|
+
await this.collection.updateMany({}, { $unset: unsetSpec }, this._getSessionOpts());
|
|
1166
|
+
}
|
|
1167
|
+
async renameTable(oldName) {
|
|
1168
|
+
const newName = this.resolveTableName(false);
|
|
1169
|
+
this._log("renameTable", oldName, "→", newName);
|
|
1170
|
+
await this.db.renameCollection(oldName, newName);
|
|
1171
|
+
this._collection = undefined;
|
|
1172
|
+
}
|
|
1173
|
+
/**
|
|
1174
|
+
* Resolves a field's default value for bulk $set during column sync.
|
|
1175
|
+
* Returns `undefined` if no concrete default can be determined.
|
|
1176
|
+
*/ _resolveSyncDefault(field) {
|
|
1177
|
+
if (!field.defaultValue) return field.optional ? null : undefined;
|
|
1178
|
+
if (field.defaultValue.kind === "value") return field.defaultValue.value;
|
|
1179
|
+
return undefined;
|
|
1180
|
+
}
|
|
788
1181
|
async syncIndexes() {
|
|
789
1182
|
await this.ensureCollectionExists();
|
|
790
1183
|
const allIndexes = new Map();
|
|
@@ -917,25 +1310,84 @@ else toUpdate.add(remote.name);
|
|
|
917
1310
|
}
|
|
918
1311
|
} catch {}
|
|
919
1312
|
}
|
|
1313
|
+
/**
|
|
1314
|
+
* Returns the physical column name of the single @meta.id field (if any).
|
|
1315
|
+
* Used to return the user's logical ID instead of MongoDB's _id on insert.
|
|
1316
|
+
*/ _getMetaIdPhysical() {
|
|
1317
|
+
if (this._metaIdPhysical === undefined) {
|
|
1318
|
+
const fields = this._table.originalMetaIdFields;
|
|
1319
|
+
if (fields.length === 1) {
|
|
1320
|
+
const field = fields[0];
|
|
1321
|
+
this._metaIdPhysical = this._table.columnMap.get(field) ?? field;
|
|
1322
|
+
} else this._metaIdPhysical = null;
|
|
1323
|
+
}
|
|
1324
|
+
return this._metaIdPhysical;
|
|
1325
|
+
}
|
|
1326
|
+
/** Returns the counters collection used for atomic auto-increment. */ get _countersCollection() {
|
|
1327
|
+
return this.db.collection("__atscript_counters");
|
|
1328
|
+
}
|
|
920
1329
|
/** Returns physical field names of increment fields that are undefined in the data. */ _fieldsNeedingIncrement(data) {
|
|
921
1330
|
const result = [];
|
|
922
1331
|
for (const physical of this._incrementFields) if (data[physical] === undefined || data[physical] === null) result.push(physical);
|
|
923
1332
|
return result;
|
|
924
1333
|
}
|
|
925
|
-
/**
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
const
|
|
930
|
-
const
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
1334
|
+
/**
|
|
1335
|
+
* Atomically allocates `count` sequential values for each increment field
|
|
1336
|
+
* using a counter collection. Returns a map of field → first allocated value.
|
|
1337
|
+
*/ async _allocateIncrementValues(physicalFields, count) {
|
|
1338
|
+
const counters = this._countersCollection;
|
|
1339
|
+
const collectionName = this._table.tableName;
|
|
1340
|
+
const result = new Map();
|
|
1341
|
+
for (const field of physicalFields) {
|
|
1342
|
+
const counterId = `${collectionName}.${field}`;
|
|
1343
|
+
const doc = await counters.findOneAndUpdate({ _id: counterId }, { $inc: { seq: count } }, {
|
|
1344
|
+
upsert: true,
|
|
1345
|
+
returnDocument: "after",
|
|
1346
|
+
...this._getSessionOpts()
|
|
1347
|
+
});
|
|
1348
|
+
const seq = doc?.seq ?? count;
|
|
1349
|
+
if (seq === count) {
|
|
1350
|
+
const currentMax = await this._getCurrentFieldMax(field);
|
|
1351
|
+
if (currentMax >= seq) {
|
|
1352
|
+
const adjusted = currentMax + count;
|
|
1353
|
+
await counters.updateOne({ _id: counterId }, { $max: { seq: adjusted } }, this._getSessionOpts());
|
|
1354
|
+
result.set(field, currentMax + 1);
|
|
1355
|
+
continue;
|
|
1356
|
+
}
|
|
936
1357
|
}
|
|
1358
|
+
result.set(field, seq - count + 1);
|
|
1359
|
+
}
|
|
1360
|
+
return result;
|
|
1361
|
+
}
|
|
1362
|
+
/** Reads current max value for a single field via $group aggregation. */ async _getCurrentFieldMax(field) {
|
|
1363
|
+
const alias = `max__${field.replace(/\./g, "__")}`;
|
|
1364
|
+
const agg = await this.collection.aggregate([{ $group: {
|
|
1365
|
+
_id: null,
|
|
1366
|
+
[alias]: { $max: `$${field}` }
|
|
1367
|
+
} }], this._getSessionOpts()).toArray();
|
|
1368
|
+
if (agg.length > 0) {
|
|
1369
|
+
const val = agg[0][alias];
|
|
1370
|
+
if (typeof val === "number") return val;
|
|
1371
|
+
}
|
|
1372
|
+
return 0;
|
|
1373
|
+
}
|
|
1374
|
+
/** Allocates increment values for a batch of items, assigning in order. */ async _assignBatchIncrements(data, allFields) {
|
|
1375
|
+
const fieldCounts = new Map();
|
|
1376
|
+
for (const physical of allFields) {
|
|
1377
|
+
let count = 0;
|
|
1378
|
+
for (const item of data) if (item[physical] === undefined || item[physical] === null) count++;
|
|
1379
|
+
if (count > 0) fieldCounts.set(physical, count);
|
|
1380
|
+
}
|
|
1381
|
+
const fieldCounters = new Map();
|
|
1382
|
+
for (const [physical, count] of fieldCounts) {
|
|
1383
|
+
const allocated = await this._allocateIncrementValues([physical], count);
|
|
1384
|
+
fieldCounters.set(physical, allocated.get(physical) ?? 1);
|
|
1385
|
+
}
|
|
1386
|
+
for (const item of data) for (const physical of allFields) if (item[physical] === undefined || item[physical] === null) {
|
|
1387
|
+
const next = fieldCounters.get(physical) ?? 1;
|
|
1388
|
+
item[physical] = next;
|
|
1389
|
+
fieldCounters.set(physical, next + 1);
|
|
937
1390
|
}
|
|
938
|
-
return maxMap;
|
|
939
1391
|
}
|
|
940
1392
|
_buildFindOptions(controls) {
|
|
941
1393
|
const opts = {};
|
|
@@ -987,11 +1439,11 @@ else {
|
|
|
987
1439
|
if (analyzer) index.definition.mappings.fields[fieldName].analyzer = analyzer;
|
|
988
1440
|
}
|
|
989
1441
|
}
|
|
990
|
-
constructor(db,
|
|
991
|
-
super(), _define_property
|
|
1442
|
+
constructor(db, client) {
|
|
1443
|
+
super(), _define_property(this, "db", void 0), _define_property(this, "client", void 0), _define_property(this, "_collection", void 0), _define_property(this, "_mongoIndexes", void 0), _define_property(this, "_vectorFilters", void 0), _define_property(this, "_searchIndexesMap", void 0), _define_property(this, "_incrementFields", void 0), _define_property(this, "_cappedOptions", void 0), _define_property(this, "_hasExplicitId", void 0), _define_property(this, "_txDisabled", void 0), _define_property(this, "_metaIdPhysical", void 0), this.db = db, this.client = client, this._mongoIndexes = new Map(), this._vectorFilters = new Map(), this._incrementFields = new Set(), this._hasExplicitId = false, this._txDisabled = false;
|
|
992
1444
|
}
|
|
993
1445
|
};
|
|
994
|
-
_define_property
|
|
1446
|
+
_define_property(MongoAdapter, "_noSession", Object.freeze({}));
|
|
995
1447
|
function objMatch(o1, o2) {
|
|
996
1448
|
const keys1 = Object.keys(o1);
|
|
997
1449
|
const keys2 = Object.keys(o2);
|
|
@@ -1020,51 +1472,15 @@ function vectorFieldsMatch(left, right) {
|
|
|
1020
1472
|
return true;
|
|
1021
1473
|
}
|
|
1022
1474
|
|
|
1023
|
-
//#endregion
|
|
1024
|
-
//#region packages/mongo/src/lib/as-mongo.ts
|
|
1025
|
-
function _define_property(obj, key, value) {
|
|
1026
|
-
if (key in obj) Object.defineProperty(obj, key, {
|
|
1027
|
-
value,
|
|
1028
|
-
enumerable: true,
|
|
1029
|
-
configurable: true,
|
|
1030
|
-
writable: true
|
|
1031
|
-
});
|
|
1032
|
-
else obj[key] = value;
|
|
1033
|
-
return obj;
|
|
1034
|
-
}
|
|
1035
|
-
var AsMongo = class extends __atscript_utils_db.DbSpace {
|
|
1036
|
-
get db() {
|
|
1037
|
-
return this.client.db();
|
|
1038
|
-
}
|
|
1039
|
-
getCollectionsList() {
|
|
1040
|
-
if (!this.collectionsList) this.collectionsList = this.db.listCollections().toArray().then((c) => new Set(c.map((c$1) => c$1.name)));
|
|
1041
|
-
return this.collectionsList;
|
|
1042
|
-
}
|
|
1043
|
-
async collectionExists(name) {
|
|
1044
|
-
const list = await this.getCollectionsList();
|
|
1045
|
-
return list.has(name);
|
|
1046
|
-
}
|
|
1047
|
-
/**
|
|
1048
|
-
* Returns the MongoAdapter for the given type.
|
|
1049
|
-
* Convenience accessor for Mongo-specific adapter operations.
|
|
1050
|
-
*/ getAdapter(type) {
|
|
1051
|
-
return super.getAdapter(type);
|
|
1052
|
-
}
|
|
1053
|
-
constructor(client, logger = NoopLogger) {
|
|
1054
|
-
const resolvedClient = typeof client === "string" ? new mongodb.MongoClient(client) : client;
|
|
1055
|
-
super(() => new MongoAdapter(this.db, this), logger), _define_property(this, "client", void 0), _define_property(this, "collectionsList", void 0);
|
|
1056
|
-
this.client = resolvedClient;
|
|
1057
|
-
}
|
|
1058
|
-
};
|
|
1059
|
-
|
|
1060
1475
|
//#endregion
|
|
1061
1476
|
//#region packages/mongo/src/lib/index.ts
|
|
1062
1477
|
function createAdapter(connection, _options) {
|
|
1063
|
-
|
|
1478
|
+
const client = new mongodb.MongoClient(connection);
|
|
1479
|
+
const db = client.db();
|
|
1480
|
+
return new __atscript_utils_db.DbSpace(() => new MongoAdapter(db, client));
|
|
1064
1481
|
}
|
|
1065
1482
|
|
|
1066
1483
|
//#endregion
|
|
1067
|
-
exports.AsMongo = AsMongo
|
|
1068
1484
|
exports.CollectionPatcher = CollectionPatcher
|
|
1069
1485
|
exports.MongoAdapter = MongoAdapter
|
|
1070
1486
|
exports.buildMongoFilter = buildMongoFilter
|