@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.mjs
CHANGED
|
@@ -1,25 +1,8 @@
|
|
|
1
|
-
import { BaseDbAdapter, DbSpace, getKeyProps, walkFilter } from "@atscript/utils-db";
|
|
2
|
-
import { MongoClient, ObjectId } from "mongodb";
|
|
3
|
-
import { defineAnnotatedType, isAnnotatedTypeOfPrimitive } from "@atscript/typescript/utils";
|
|
1
|
+
import { AtscriptDbView, BaseDbAdapter, DbError, DbSpace, getKeyProps, walkFilter } from "@atscript/utils-db";
|
|
2
|
+
import { MongoClient, MongoServerError, ObjectId } from "mongodb";
|
|
4
3
|
|
|
5
|
-
//#region packages/mongo/src/lib/logger.ts
|
|
6
|
-
const NoopLogger = {
|
|
7
|
-
error: () => {},
|
|
8
|
-
warn: () => {},
|
|
9
|
-
log: () => {},
|
|
10
|
-
info: () => {},
|
|
11
|
-
debug: () => {}
|
|
12
|
-
};
|
|
13
|
-
|
|
14
|
-
//#endregion
|
|
15
|
-
//#region packages/mongo/src/lib/validate-plugins.ts
|
|
16
|
-
const validateMongoIdPlugin = (ctx, def, value) => {
|
|
17
|
-
if (ctx.path === "_id" && def.type.tags.has("objectId")) return ctx.validateAnnotatedType(def, value instanceof ObjectId ? value.toString() : value);
|
|
18
|
-
};
|
|
19
|
-
|
|
20
|
-
//#endregion
|
|
21
4
|
//#region packages/mongo/src/lib/collection-patcher.ts
|
|
22
|
-
function _define_property$
|
|
5
|
+
function _define_property$1(obj, key, value) {
|
|
23
6
|
if (key in obj) Object.defineProperty(obj, key, {
|
|
24
7
|
value,
|
|
25
8
|
enumerable: true,
|
|
@@ -29,50 +12,7 @@ function _define_property$2(obj, key, value) {
|
|
|
29
12
|
else obj[key] = value;
|
|
30
13
|
return obj;
|
|
31
14
|
}
|
|
32
|
-
var CollectionPatcher = class
|
|
33
|
-
/**
|
|
34
|
-
* Build a runtime *Validator* that understands the extended patch payload.
|
|
35
|
-
*
|
|
36
|
-
* * Adds per‑array *patch* wrappers (the `$replace`, `$insert`, … fields).
|
|
37
|
-
* * Honors `db.patch.strategy === "merge"` metadata.
|
|
38
|
-
*
|
|
39
|
-
* @param collection Target collection wrapper
|
|
40
|
-
* @returns Atscript Validator
|
|
41
|
-
*/ static prepareValidator(context) {
|
|
42
|
-
return context.createValidator({
|
|
43
|
-
plugins: [validateMongoIdPlugin],
|
|
44
|
-
replace: (def, path) => {
|
|
45
|
-
if (path === "" && def.type.kind === "object") {
|
|
46
|
-
const obj = defineAnnotatedType("object").copyMetadata(def.metadata);
|
|
47
|
-
for (const [prop, type] of def.type.props.entries()) obj.prop(prop, defineAnnotatedType().refTo(type).copyMetadata(type.metadata).optional(prop !== "_id").$type);
|
|
48
|
-
return obj.$type;
|
|
49
|
-
}
|
|
50
|
-
if (def.type.kind === "array" && context.flatMap.get(path)?.metadata.get("db.mongo.__topLevelArray") && !def.metadata.has("db.mongo.__patchArrayValue")) {
|
|
51
|
-
const defArray = def;
|
|
52
|
-
const mergeStrategy = defArray.metadata.get("db.patch.strategy") === "merge";
|
|
53
|
-
function getPatchType() {
|
|
54
|
-
const isPrimitive = isAnnotatedTypeOfPrimitive(defArray.type.of);
|
|
55
|
-
if (isPrimitive) return defineAnnotatedType().refTo(def).copyMetadata(def.metadata).annotate("db.mongo.__patchArrayValue").optional().$type;
|
|
56
|
-
if (defArray.type.of.type.kind === "object") {
|
|
57
|
-
const objType = defArray.type.of.type;
|
|
58
|
-
const t = defineAnnotatedType("object").copyMetadata(defArray.type.of.metadata);
|
|
59
|
-
const keyProps = CollectionPatcher.getKeyProps(defArray);
|
|
60
|
-
for (const [key, val] of objType.props.entries()) if (keyProps.size > 0) if (keyProps.has(key)) t.prop(key, defineAnnotatedType().refTo(val).copyMetadata(def.metadata).$type);
|
|
61
|
-
else t.prop(key, defineAnnotatedType().refTo(val).copyMetadata(def.metadata).optional().$type);
|
|
62
|
-
else t.prop(key, defineAnnotatedType().refTo(val).copyMetadata(def.metadata).optional(!!val.optional).$type);
|
|
63
|
-
return defineAnnotatedType("array").of(t.$type).copyMetadata(def.metadata).annotate("db.mongo.__patchArrayValue").optional().$type;
|
|
64
|
-
}
|
|
65
|
-
return undefined;
|
|
66
|
-
}
|
|
67
|
-
const fullType = defineAnnotatedType().refTo(def).copyMetadata(def.metadata).annotate("db.mongo.__patchArrayValue").optional().$type;
|
|
68
|
-
const patchType = getPatchType();
|
|
69
|
-
return patchType ? defineAnnotatedType("object").prop("$replace", fullType).prop("$insert", fullType).prop("$upsert", fullType).prop("$update", mergeStrategy ? patchType : fullType).prop("$remove", patchType).optional().$type : defineAnnotatedType("object").prop("$replace", fullType).prop("$insert", fullType).optional().$type;
|
|
70
|
-
}
|
|
71
|
-
return def;
|
|
72
|
-
},
|
|
73
|
-
partial: (def, path) => path !== "" && def.metadata.get("db.patch.strategy") === "merge"
|
|
74
|
-
});
|
|
75
|
-
}
|
|
15
|
+
var CollectionPatcher = class {
|
|
76
16
|
/**
|
|
77
17
|
* Entry point – walk the payload, build `filter`, `update` and `options`.
|
|
78
18
|
*
|
|
@@ -119,8 +59,8 @@ else t.prop(key, defineAnnotatedType().refTo(val).copyMetadata(def.metadata).opt
|
|
|
119
59
|
for (const [_key, value] of Object.entries(payload)) {
|
|
120
60
|
const key = evalKey(_key);
|
|
121
61
|
const flatType = this.collection.flatMap.get(key);
|
|
122
|
-
const topLevelArray = flatType?.metadata?.get("db.
|
|
123
|
-
if (typeof value === "object" && topLevelArray) this.parseArrayPatch(key, value, flatType);
|
|
62
|
+
const topLevelArray = flatType?.metadata?.get("db.__topLevelArray");
|
|
63
|
+
if (typeof value === "object" && !Array.isArray(value) && topLevelArray && !flatType?.metadata?.has("db.json")) this.parseArrayPatch(key, value, flatType);
|
|
124
64
|
else if (typeof value === "object" && flatType?.metadata?.get("db.patch.strategy") === "merge") this.flattenPayload(value, key);
|
|
125
65
|
else if (key !== "_id") this._set(key, value);
|
|
126
66
|
}
|
|
@@ -184,22 +124,37 @@ else this._set(key, { $concatArrays: [{ $ifNull: [`$${key}`, []] }, input] });
|
|
|
184
124
|
* `$upsert`
|
|
185
125
|
* - keyed → remove existing matching by key(s) then append candidate
|
|
186
126
|
* - unique → $setUnion (deep equality)
|
|
187
|
-
*/ _upsert(key, input, keys,
|
|
127
|
+
*/ _upsert(key, input, keys, flatType) {
|
|
188
128
|
if (!input?.length) return;
|
|
189
129
|
if (keys.length > 0) {
|
|
130
|
+
const mergeStrategy = flatType.metadata?.get("db.patch.strategy") === "merge";
|
|
131
|
+
const vars = {
|
|
132
|
+
acc: "$$value",
|
|
133
|
+
cand: "$$this"
|
|
134
|
+
};
|
|
135
|
+
let appendExpr = "$$cand";
|
|
136
|
+
if (mergeStrategy) {
|
|
137
|
+
vars.existing = { $arrayElemAt: [{ $filter: {
|
|
138
|
+
input: "$$value",
|
|
139
|
+
as: "el",
|
|
140
|
+
cond: this._keysEqual(keys, "$$el", "$$this")
|
|
141
|
+
} }, 0] };
|
|
142
|
+
appendExpr = { $cond: [
|
|
143
|
+
{ $ifNull: ["$$existing", false] },
|
|
144
|
+
{ $mergeObjects: ["$$existing", "$$cand"] },
|
|
145
|
+
"$$cand"
|
|
146
|
+
] };
|
|
147
|
+
}
|
|
190
148
|
this._set(key, { $reduce: {
|
|
191
149
|
input,
|
|
192
150
|
initialValue: { $ifNull: [`$${key}`, []] },
|
|
193
151
|
in: { $let: {
|
|
194
|
-
vars
|
|
195
|
-
acc: "$$value",
|
|
196
|
-
cand: "$$this"
|
|
197
|
-
},
|
|
152
|
+
vars,
|
|
198
153
|
in: { $concatArrays: [{ $filter: {
|
|
199
154
|
input: "$$acc",
|
|
200
155
|
as: "el",
|
|
201
156
|
cond: { $not: this._keysEqual(keys, "$$el", "$$cand") }
|
|
202
|
-
} }, [
|
|
157
|
+
} }, [appendExpr]] }
|
|
203
158
|
} }
|
|
204
159
|
} });
|
|
205
160
|
return;
|
|
@@ -250,15 +205,15 @@ else this._set(key, { $concatArrays: [{ $ifNull: [`$${key}`, []] }, input] });
|
|
|
250
205
|
else this._set(key, { $setDifference: [{ $ifNull: [`$${key}`, []] }, input] });
|
|
251
206
|
}
|
|
252
207
|
constructor(collection, payload) {
|
|
253
|
-
_define_property$
|
|
254
|
-
_define_property$
|
|
208
|
+
_define_property$1(this, "collection", void 0);
|
|
209
|
+
_define_property$1(this, "payload", void 0);
|
|
255
210
|
/**
|
|
256
211
|
* Internal accumulator: filter passed to `updateOne()`.
|
|
257
212
|
* Filled only with the `_id` field right now.
|
|
258
|
-
*/ _define_property$
|
|
259
|
-
/** MongoDB *update* document being built. */ _define_property$
|
|
260
|
-
/** Current `$set` stage being populated. */ _define_property$
|
|
261
|
-
/** Additional *options* (mainly `arrayFilters`). */ _define_property$
|
|
213
|
+
*/ _define_property$1(this, "filterObj", void 0);
|
|
214
|
+
/** MongoDB *update* document being built. */ _define_property$1(this, "updatePipeline", void 0);
|
|
215
|
+
/** Current `$set` stage being populated. */ _define_property$1(this, "currentSetStage", void 0);
|
|
216
|
+
/** Additional *options* (mainly `arrayFilters`). */ _define_property$1(this, "optionsObj", void 0);
|
|
262
217
|
this.collection = collection;
|
|
263
218
|
this.payload = payload;
|
|
264
219
|
this.filterObj = {};
|
|
@@ -267,7 +222,7 @@ else this._set(key, { $setDifference: [{ $ifNull: [`$${key}`, []] }, input] });
|
|
|
267
222
|
this.optionsObj = {};
|
|
268
223
|
}
|
|
269
224
|
};
|
|
270
|
-
_define_property$
|
|
225
|
+
_define_property$1(CollectionPatcher, "getKeyProps", getKeyProps);
|
|
271
226
|
|
|
272
227
|
//#endregion
|
|
273
228
|
//#region packages/mongo/src/lib/mongo-filter.ts
|
|
@@ -296,9 +251,21 @@ function buildMongoFilter(filter) {
|
|
|
296
251
|
return walkFilter(filter, mongoVisitor) ?? EMPTY;
|
|
297
252
|
}
|
|
298
253
|
|
|
254
|
+
//#endregion
|
|
255
|
+
//#region packages/mongo/src/lib/validate-plugins.ts
|
|
256
|
+
const validateMongoIdPlugin = (ctx, def, value) => {
|
|
257
|
+
if (def.type.tags?.has("objectId")) {
|
|
258
|
+
if (ctx.path === "_id" && (value === undefined || value === null)) {
|
|
259
|
+
const dbCtx = ctx.context;
|
|
260
|
+
if (dbCtx && (dbCtx.mode === "insert" || dbCtx.mode === "replace")) return true;
|
|
261
|
+
}
|
|
262
|
+
return ctx.validateAnnotatedType(def, value instanceof ObjectId ? value.toString() : value);
|
|
263
|
+
}
|
|
264
|
+
};
|
|
265
|
+
|
|
299
266
|
//#endregion
|
|
300
267
|
//#region packages/mongo/src/lib/mongo-adapter.ts
|
|
301
|
-
function _define_property
|
|
268
|
+
function _define_property(obj, key, value) {
|
|
302
269
|
if (key in obj) Object.defineProperty(obj, key, {
|
|
303
270
|
value,
|
|
304
271
|
enumerable: true,
|
|
@@ -310,13 +277,14 @@ else obj[key] = value;
|
|
|
310
277
|
}
|
|
311
278
|
const INDEX_PREFIX = "atscript__";
|
|
312
279
|
const DEFAULT_INDEX_NAME = "DEFAULT";
|
|
280
|
+
const JOINED_PREFIX = "__joined_";
|
|
313
281
|
function mongoIndexKey(type, name) {
|
|
314
282
|
const cleanName = name.replace(/[^a-z0-9_.-]/gi, "_").replace(/_+/g, "_").slice(0, 127 - INDEX_PREFIX.length - type.length - 2);
|
|
315
283
|
return `${INDEX_PREFIX}${type}__${cleanName}`;
|
|
316
284
|
}
|
|
317
285
|
var MongoAdapter = class MongoAdapter extends BaseDbAdapter {
|
|
318
286
|
get _client() {
|
|
319
|
-
return this.
|
|
287
|
+
return this.client;
|
|
320
288
|
}
|
|
321
289
|
async _beginTransaction() {
|
|
322
290
|
if (this._txDisabled || !this._client) return undefined;
|
|
@@ -403,30 +371,260 @@ var MongoAdapter = class MongoAdapter extends BaseDbAdapter {
|
|
|
403
371
|
getValidatorPlugins() {
|
|
404
372
|
return [validateMongoIdPlugin];
|
|
405
373
|
}
|
|
406
|
-
getTopLevelArrayTag() {
|
|
407
|
-
return "db.mongo.__topLevelArray";
|
|
408
|
-
}
|
|
409
374
|
getAdapterTableName(type) {
|
|
410
375
|
return undefined;
|
|
411
376
|
}
|
|
412
|
-
|
|
413
|
-
return
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
377
|
+
supportsNativeRelations() {
|
|
378
|
+
return true;
|
|
379
|
+
}
|
|
380
|
+
async loadRelations(rows, withRelations, relations, foreignKeys, tableResolver) {
|
|
381
|
+
if (rows.length === 0 || withRelations.length === 0) return;
|
|
382
|
+
const primaryKeys = this._table.primaryKeys;
|
|
383
|
+
const relMeta = [];
|
|
384
|
+
for (const withRel of withRelations) {
|
|
385
|
+
if (withRel.name.includes(".")) continue;
|
|
386
|
+
const relation = relations.get(withRel.name);
|
|
387
|
+
if (!relation) throw new Error(`Unknown relation "${withRel.name}" in $with. Available relations: ${[...relations.keys()].join(", ") || "(none)"}`);
|
|
388
|
+
const lookupResult = this._buildRelationLookup(withRel, relation, foreignKeys, tableResolver);
|
|
389
|
+
if (!lookupResult) continue;
|
|
390
|
+
relMeta.push({
|
|
391
|
+
name: withRel.name,
|
|
392
|
+
isArray: lookupResult.isArray,
|
|
393
|
+
relation,
|
|
394
|
+
nestedWith: this._extractNestedWith(withRel),
|
|
395
|
+
stages: lookupResult.stages
|
|
396
|
+
});
|
|
397
|
+
}
|
|
398
|
+
if (relMeta.length === 0) return;
|
|
399
|
+
const pkMatchFilter = this._buildPKMatchFilter(rows, primaryKeys);
|
|
400
|
+
if (pkMatchFilter) {
|
|
401
|
+
const pipeline = [{ $match: pkMatchFilter }];
|
|
402
|
+
for (const meta of relMeta) pipeline.push(...meta.stages);
|
|
403
|
+
const results = await this.collection.aggregate(pipeline, this._getSessionOpts()).toArray();
|
|
404
|
+
this._mergeRelationResults(rows, results, primaryKeys, relMeta);
|
|
405
|
+
} else for (const row of rows) for (const meta of relMeta) row[meta.name] = meta.isArray ? [] : null;
|
|
406
|
+
await this._loadNestedRelations(rows, relMeta, tableResolver);
|
|
407
|
+
}
|
|
408
|
+
/** Builds a $match filter to re-select source rows by PK. */ _buildPKMatchFilter(rows, primaryKeys) {
|
|
409
|
+
if (primaryKeys.length === 1) {
|
|
410
|
+
const pk = primaryKeys[0];
|
|
411
|
+
const values = new Set();
|
|
412
|
+
for (const row of rows) {
|
|
413
|
+
const v = row[pk];
|
|
414
|
+
if (v !== null && v !== undefined) values.add(v);
|
|
425
415
|
}
|
|
426
|
-
|
|
416
|
+
if (values.size === 0) return undefined;
|
|
417
|
+
return { [pk]: { $in: [...values] } };
|
|
418
|
+
}
|
|
419
|
+
const seen = new Set();
|
|
420
|
+
const orFilters = [];
|
|
421
|
+
for (const row of rows) {
|
|
422
|
+
const key = primaryKeys.map((pk) => String(row[pk] ?? "")).join("\0");
|
|
423
|
+
if (seen.has(key)) continue;
|
|
424
|
+
seen.add(key);
|
|
425
|
+
const condition = {};
|
|
426
|
+
let valid = true;
|
|
427
|
+
for (const pk of primaryKeys) {
|
|
428
|
+
const val = row[pk];
|
|
429
|
+
if (val === null || val === undefined) {
|
|
430
|
+
valid = false;
|
|
431
|
+
break;
|
|
432
|
+
}
|
|
433
|
+
condition[pk] = val;
|
|
434
|
+
}
|
|
435
|
+
if (valid) orFilters.push(condition);
|
|
436
|
+
}
|
|
437
|
+
if (orFilters.length === 0) return undefined;
|
|
438
|
+
return orFilters.length === 1 ? orFilters[0] : { $or: orFilters };
|
|
439
|
+
}
|
|
440
|
+
/** Dispatches to the correct $lookup builder based on relation direction. */ _buildRelationLookup(withRel, relation, foreignKeys, tableResolver) {
|
|
441
|
+
switch (relation.direction) {
|
|
442
|
+
case "to": return this._buildToLookup(withRel, relation, foreignKeys);
|
|
443
|
+
case "from": return this._buildFromLookup(withRel, relation, tableResolver);
|
|
444
|
+
case "via": return this._buildViaLookup(withRel, relation, tableResolver);
|
|
445
|
+
default: return undefined;
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
/** Builds `let` variable bindings and the corresponding `$expr` match for `$lookup`. */ _buildLookupJoin(localFields, remoteFields, varPrefix) {
|
|
449
|
+
const letVars = Object.fromEntries(localFields.map((f, i) => [`${varPrefix}${i}`, `$${f}`]));
|
|
450
|
+
const exprMatch = remoteFields.length === 1 ? { $eq: [`$${remoteFields[0]}`, `$$${varPrefix}0`] } : { $and: remoteFields.map((rf, i) => ({ $eq: [`$${rf}`, `$$${varPrefix}${i}`] })) };
|
|
451
|
+
return {
|
|
452
|
+
letVars,
|
|
453
|
+
exprMatch
|
|
454
|
+
};
|
|
455
|
+
}
|
|
456
|
+
/** $lookup for TO relations (FK is on this table → target). Always single-valued. */ _buildToLookup(withRel, relation, foreignKeys) {
|
|
457
|
+
const fk = this._findFKForRelationLookup(relation, foreignKeys);
|
|
458
|
+
if (!fk) return undefined;
|
|
459
|
+
const innerPipeline = this._buildLookupInnerPipeline(withRel, fk.targetFields);
|
|
460
|
+
const { letVars, exprMatch } = this._buildLookupJoin(fk.localFields, fk.targetFields, "fk_");
|
|
461
|
+
const stages = [{ $lookup: {
|
|
462
|
+
from: fk.targetTable,
|
|
463
|
+
let: letVars,
|
|
464
|
+
pipeline: [{ $match: { $expr: exprMatch } }, ...innerPipeline],
|
|
465
|
+
as: withRel.name
|
|
466
|
+
} }, { $unwind: {
|
|
467
|
+
path: `$${withRel.name}`,
|
|
468
|
+
preserveNullAndEmptyArrays: true
|
|
469
|
+
} }];
|
|
470
|
+
return {
|
|
471
|
+
stages,
|
|
472
|
+
isArray: false
|
|
473
|
+
};
|
|
474
|
+
}
|
|
475
|
+
/** $lookup for FROM relations (FK is on target → this table). */ _buildFromLookup(withRel, relation, tableResolver) {
|
|
476
|
+
const targetType = relation.targetType();
|
|
477
|
+
if (!targetType || !tableResolver) return undefined;
|
|
478
|
+
const targetMeta = tableResolver(targetType);
|
|
479
|
+
if (!targetMeta) return undefined;
|
|
480
|
+
const remoteFK = this._findRemoteFKFromMeta(targetMeta, this._table.tableName, relation.alias);
|
|
481
|
+
if (!remoteFK) return undefined;
|
|
482
|
+
const targetTableName = this._resolveRelTargetTableName(relation);
|
|
483
|
+
const innerPipeline = this._buildLookupInnerPipeline(withRel, remoteFK.fields);
|
|
484
|
+
const { letVars, exprMatch } = this._buildLookupJoin(remoteFK.targetFields, remoteFK.fields, "pk_");
|
|
485
|
+
const stages = [{ $lookup: {
|
|
486
|
+
from: targetTableName,
|
|
487
|
+
let: letVars,
|
|
488
|
+
pipeline: [{ $match: { $expr: exprMatch } }, ...innerPipeline],
|
|
489
|
+
as: withRel.name
|
|
490
|
+
} }];
|
|
491
|
+
if (!relation.isArray) stages.push({ $unwind: {
|
|
492
|
+
path: `$${withRel.name}`,
|
|
493
|
+
preserveNullAndEmptyArrays: true
|
|
494
|
+
} });
|
|
495
|
+
return {
|
|
496
|
+
stages,
|
|
497
|
+
isArray: relation.isArray
|
|
498
|
+
};
|
|
499
|
+
}
|
|
500
|
+
/** $lookup for VIA relations (M:N through junction table). Always array. */ _buildViaLookup(withRel, relation, tableResolver) {
|
|
501
|
+
if (!relation.viaType || !tableResolver) return undefined;
|
|
502
|
+
const junctionType = relation.viaType();
|
|
503
|
+
if (!junctionType) return undefined;
|
|
504
|
+
const junctionMeta = tableResolver(junctionType);
|
|
505
|
+
if (!junctionMeta) return undefined;
|
|
506
|
+
const junctionTableName = junctionType.metadata?.get("db.table") || junctionType.id || "";
|
|
507
|
+
const targetTableName = this._resolveRelTargetTableName(relation);
|
|
508
|
+
const fkToThis = this._findRemoteFKFromMeta(junctionMeta, this._table.tableName);
|
|
509
|
+
if (!fkToThis) return undefined;
|
|
510
|
+
const fkToTarget = this._findRemoteFKFromMeta(junctionMeta, targetTableName);
|
|
511
|
+
if (!fkToTarget) return undefined;
|
|
512
|
+
const innerPipeline = this._buildLookupInnerPipeline(withRel, fkToTarget.targetFields);
|
|
513
|
+
const { letVars, exprMatch } = this._buildLookupJoin(fkToThis.targetFields, fkToThis.fields, "pk_");
|
|
514
|
+
const stages = [{ $lookup: {
|
|
515
|
+
from: junctionTableName,
|
|
516
|
+
let: letVars,
|
|
517
|
+
pipeline: [
|
|
518
|
+
{ $match: { $expr: exprMatch } },
|
|
519
|
+
{ $lookup: {
|
|
520
|
+
from: targetTableName,
|
|
521
|
+
localField: fkToTarget.fields[0],
|
|
522
|
+
foreignField: fkToTarget.targetFields[0],
|
|
523
|
+
pipeline: innerPipeline,
|
|
524
|
+
as: "__target"
|
|
525
|
+
} },
|
|
526
|
+
{ $unwind: {
|
|
527
|
+
path: "$__target",
|
|
528
|
+
preserveNullAndEmptyArrays: false
|
|
529
|
+
} },
|
|
530
|
+
{ $replaceRoot: { newRoot: "$__target" } }
|
|
531
|
+
],
|
|
532
|
+
as: withRel.name
|
|
533
|
+
} }];
|
|
534
|
+
return {
|
|
535
|
+
stages,
|
|
536
|
+
isArray: true
|
|
537
|
+
};
|
|
427
538
|
}
|
|
428
|
-
|
|
429
|
-
|
|
539
|
+
/** Builds inner pipeline stages for relation controls ($sort, $limit, $skip, $select, filter). */ _buildLookupInnerPipeline(withRel, requiredFields) {
|
|
540
|
+
const pipeline = [];
|
|
541
|
+
const flatRel = withRel;
|
|
542
|
+
const nested = withRel.controls || {};
|
|
543
|
+
const filter = withRel.filter;
|
|
544
|
+
const sort = nested.$sort || flatRel.$sort;
|
|
545
|
+
const limit = nested.$limit ?? flatRel.$limit;
|
|
546
|
+
const skip = nested.$skip ?? flatRel.$skip;
|
|
547
|
+
const select = nested.$select || flatRel.$select;
|
|
548
|
+
if (filter && Object.keys(filter).length > 0) pipeline.push({ $match: buildMongoFilter(filter) });
|
|
549
|
+
if (sort) pipeline.push({ $sort: sort });
|
|
550
|
+
if (skip) pipeline.push({ $skip: skip });
|
|
551
|
+
if (limit !== null && limit !== undefined) pipeline.push({ $limit: limit });
|
|
552
|
+
if (select) {
|
|
553
|
+
const projection = {};
|
|
554
|
+
for (const f of select) projection[f] = 1;
|
|
555
|
+
for (const f of requiredFields) projection[f] = 1;
|
|
556
|
+
if (!select.includes("_id") && !requiredFields.includes("_id")) projection["_id"] = 0;
|
|
557
|
+
pipeline.push({ $project: projection });
|
|
558
|
+
}
|
|
559
|
+
return pipeline;
|
|
560
|
+
}
|
|
561
|
+
/** Extracts nested $with from a WithRelation's controls. */ _extractNestedWith(withRel) {
|
|
562
|
+
const flatRel = withRel;
|
|
563
|
+
const nested = withRel.controls || {};
|
|
564
|
+
const nestedWith = nested.$with || flatRel.$with;
|
|
565
|
+
return nestedWith && nestedWith.length > 0 ? nestedWith : undefined;
|
|
566
|
+
}
|
|
567
|
+
/** Post-processes nested $with by delegating to the target table's own relation loading. */ async _loadNestedRelations(rows, relMeta, tableResolver) {
|
|
568
|
+
if (!tableResolver) return;
|
|
569
|
+
const tasks = [];
|
|
570
|
+
for (const meta of relMeta) {
|
|
571
|
+
if (!meta.nestedWith || meta.nestedWith.length === 0) continue;
|
|
572
|
+
const targetType = meta.relation.targetType();
|
|
573
|
+
if (!targetType) continue;
|
|
574
|
+
const targetTable = tableResolver(targetType);
|
|
575
|
+
if (!targetTable) continue;
|
|
576
|
+
const subRows = [];
|
|
577
|
+
for (const row of rows) {
|
|
578
|
+
const val = row[meta.name];
|
|
579
|
+
if (meta.isArray && Array.isArray(val)) for (const item of val) subRows.push(item);
|
|
580
|
+
else if (val && typeof val === "object") subRows.push(val);
|
|
581
|
+
}
|
|
582
|
+
if (subRows.length === 0) continue;
|
|
583
|
+
tasks.push(targetTable.loadRelations(subRows, meta.nestedWith));
|
|
584
|
+
}
|
|
585
|
+
await Promise.all(tasks);
|
|
586
|
+
}
|
|
587
|
+
/** Merges aggregation results back onto the original rows by PK. */ _mergeRelationResults(rows, results, primaryKeys, relMeta) {
|
|
588
|
+
const resultIndex = new Map();
|
|
589
|
+
for (const doc of results) {
|
|
590
|
+
const key = primaryKeys.map((pk) => String(doc[pk] ?? "")).join("\0");
|
|
591
|
+
resultIndex.set(key, doc);
|
|
592
|
+
}
|
|
593
|
+
for (const row of rows) {
|
|
594
|
+
const key = primaryKeys.map((pk) => String(row[pk] ?? "")).join("\0");
|
|
595
|
+
const enriched = resultIndex.get(key);
|
|
596
|
+
for (const meta of relMeta) if (enriched) {
|
|
597
|
+
const value = enriched[meta.name];
|
|
598
|
+
if (!meta.isArray && Array.isArray(value)) row[meta.name] = value[0] ?? null;
|
|
599
|
+
else row[meta.name] = value ?? (meta.isArray ? [] : null);
|
|
600
|
+
} else row[meta.name] = meta.isArray ? [] : null;
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
/** Finds FK entry for a TO relation from this table's foreignKeys map. */ _findFKForRelationLookup(relation, foreignKeys) {
|
|
604
|
+
const targetTableName = this._resolveRelTargetTableName(relation);
|
|
605
|
+
for (const fk of foreignKeys.values()) if (relation.alias) {
|
|
606
|
+
if (fk.alias === relation.alias) return {
|
|
607
|
+
localFields: fk.fields,
|
|
608
|
+
targetFields: fk.targetFields,
|
|
609
|
+
targetTable: fk.targetTable
|
|
610
|
+
};
|
|
611
|
+
} else if (fk.targetTable === targetTableName) return {
|
|
612
|
+
localFields: fk.fields,
|
|
613
|
+
targetFields: fk.targetFields,
|
|
614
|
+
targetTable: fk.targetTable
|
|
615
|
+
};
|
|
616
|
+
return undefined;
|
|
617
|
+
}
|
|
618
|
+
/** Finds a FK on a remote table that points back to the given table name. */ _findRemoteFKFromMeta(target, thisTableName, alias) {
|
|
619
|
+
for (const fk of target.foreignKeys.values()) {
|
|
620
|
+
if (alias && fk.alias === alias && fk.targetTable === thisTableName) return fk;
|
|
621
|
+
if (!alias && fk.targetTable === thisTableName) return fk;
|
|
622
|
+
}
|
|
623
|
+
return undefined;
|
|
624
|
+
}
|
|
625
|
+
/** Resolves the target table/collection name from a relation's target type. */ _resolveRelTargetTableName(relation) {
|
|
626
|
+
const targetType = relation.targetType();
|
|
627
|
+
return targetType?.metadata?.get("db.table") || targetType?.id || "";
|
|
430
628
|
}
|
|
431
629
|
/** Returns the context object used by CollectionPatcher. */ getPatcherContext() {
|
|
432
630
|
return {
|
|
@@ -469,8 +667,8 @@ var MongoAdapter = class MongoAdapter extends BaseDbAdapter {
|
|
|
469
667
|
});
|
|
470
668
|
}
|
|
471
669
|
onFieldScanned(field, type, metadata) {
|
|
670
|
+
if (field === "_id") this._hasExplicitId = true;
|
|
472
671
|
if (field !== "_id" && metadata.has("meta.id")) {
|
|
473
|
-
this._table.removePrimaryKey(field);
|
|
474
672
|
this._addMongoIndexField("unique", "__pk", field);
|
|
475
673
|
this._table.addUniqueField(field);
|
|
476
674
|
}
|
|
@@ -495,7 +693,28 @@ var MongoAdapter = class MongoAdapter extends BaseDbAdapter {
|
|
|
495
693
|
for (const index of metadata.get("db.mongo.search.filter") || []) this._vectorFilters.set(mongoIndexKey("vector", index.indexName), field);
|
|
496
694
|
}
|
|
497
695
|
onAfterFlatten() {
|
|
498
|
-
this.
|
|
696
|
+
if (this._hasExplicitId) {
|
|
697
|
+
this._table.addPrimaryKey("_id");
|
|
698
|
+
for (const field of this._table.originalMetaIdFields) if (field !== "_id") this._table.removePrimaryKey(field);
|
|
699
|
+
} else {
|
|
700
|
+
this._table.flatMap.set("_id", {
|
|
701
|
+
__is_atscript_annotated_type: true,
|
|
702
|
+
type: {
|
|
703
|
+
kind: "",
|
|
704
|
+
designType: "string",
|
|
705
|
+
tags: new Set(["objectId", "mongo"])
|
|
706
|
+
},
|
|
707
|
+
metadata: new Map()
|
|
708
|
+
});
|
|
709
|
+
this._table.addUniqueField("_id");
|
|
710
|
+
}
|
|
711
|
+
if (this._table.navFields.size > 0) {
|
|
712
|
+
const isUnderNav = (path) => {
|
|
713
|
+
for (const nav of this._table.navFields) if (path.startsWith(`${nav}.`)) return true;
|
|
714
|
+
return false;
|
|
715
|
+
};
|
|
716
|
+
for (const field of this._incrementFields) if (isUnderNav(field)) this._incrementFields.delete(field);
|
|
717
|
+
}
|
|
499
718
|
for (const [key, value] of this._vectorFilters.entries()) {
|
|
500
719
|
const index = this._mongoIndexes.get(key);
|
|
501
720
|
if (index && index.type === "vector") index.definition.fields?.push({
|
|
@@ -553,7 +772,7 @@ var MongoAdapter = class MongoAdapter extends BaseDbAdapter {
|
|
|
553
772
|
/**
|
|
554
773
|
* Builds a MongoDB `$search` pipeline stage.
|
|
555
774
|
* Override `buildVectorSearchStage` in subclasses to provide embeddings.
|
|
556
|
-
*/ buildSearchStage(text, indexName) {
|
|
775
|
+
*/ async buildSearchStage(text, indexName) {
|
|
557
776
|
const index = this.getMongoSearchIndex(indexName);
|
|
558
777
|
if (!index) return undefined;
|
|
559
778
|
if (index.type === "vector") return this.buildVectorSearchStage(text, index);
|
|
@@ -568,11 +787,11 @@ var MongoAdapter = class MongoAdapter extends BaseDbAdapter {
|
|
|
568
787
|
/**
|
|
569
788
|
* Builds a vector search stage. Override in subclasses to generate embeddings.
|
|
570
789
|
* Returns `undefined` by default (vector search requires custom implementation).
|
|
571
|
-
*/ buildVectorSearchStage(text, index) {
|
|
790
|
+
*/ async buildVectorSearchStage(text, index) {
|
|
572
791
|
return undefined;
|
|
573
792
|
}
|
|
574
793
|
async search(text, query, indexName) {
|
|
575
|
-
const searchStage = this.buildSearchStage(text, indexName);
|
|
794
|
+
const searchStage = await this.buildSearchStage(text, indexName);
|
|
576
795
|
if (!searchStage) throw new Error(indexName ? `Search index "${indexName}" not found` : "No search index available");
|
|
577
796
|
const filter = buildMongoFilter(query.filter);
|
|
578
797
|
const controls = query.controls || {};
|
|
@@ -586,7 +805,7 @@ else pipeline.push({ $limit: 1e3 });
|
|
|
586
805
|
return this.collection.aggregate(pipeline, this._getSessionOpts()).toArray();
|
|
587
806
|
}
|
|
588
807
|
async searchWithCount(text, query, indexName) {
|
|
589
|
-
const searchStage = this.buildSearchStage(text, indexName);
|
|
808
|
+
const searchStage = await this.buildSearchStage(text, indexName);
|
|
590
809
|
if (!searchStage) throw new Error(indexName ? `Search index "${indexName}" not found` : "No search index available");
|
|
591
810
|
const filter = buildMongoFilter(query.filter);
|
|
592
811
|
const controls = query.controls || {};
|
|
@@ -630,7 +849,6 @@ else pipeline.push({ $limit: 1e3 });
|
|
|
630
849
|
};
|
|
631
850
|
}
|
|
632
851
|
async collectionExists() {
|
|
633
|
-
if (this.asMongo) return this.asMongo.collectionExists(this._table.tableName);
|
|
634
852
|
const cols = await this.db.listCollections({ name: this._table.tableName }).toArray();
|
|
635
853
|
return cols.length > 0;
|
|
636
854
|
}
|
|
@@ -647,39 +865,48 @@ else pipeline.push({ $limit: 1e3 });
|
|
|
647
865
|
await this.db.createCollection(this._table.tableName, opts);
|
|
648
866
|
}
|
|
649
867
|
}
|
|
868
|
+
/**
|
|
869
|
+
* Wraps an async operation to catch MongoDB duplicate key errors
|
|
870
|
+
* (code 11000) and rethrow as structured `DbError`.
|
|
871
|
+
*/ async _wrapDuplicateKeyError(fn) {
|
|
872
|
+
try {
|
|
873
|
+
return await fn();
|
|
874
|
+
} catch (e) {
|
|
875
|
+
if (e instanceof MongoServerError && e.code === 11e3) {
|
|
876
|
+
const field = e.keyPattern ? Object.keys(e.keyPattern)[0] ?? "" : "";
|
|
877
|
+
throw new DbError("CONFLICT", [{
|
|
878
|
+
path: field,
|
|
879
|
+
message: e.message
|
|
880
|
+
}]);
|
|
881
|
+
}
|
|
882
|
+
throw e;
|
|
883
|
+
}
|
|
884
|
+
}
|
|
650
885
|
async insertOne(data) {
|
|
651
886
|
if (this._incrementFields.size > 0) {
|
|
652
887
|
const fields = this._fieldsNeedingIncrement(data);
|
|
653
888
|
if (fields.length > 0) {
|
|
654
|
-
const
|
|
655
|
-
for (const physical of fields) data[physical] =
|
|
889
|
+
const nextValues = await this._allocateIncrementValues(fields, 1);
|
|
890
|
+
for (const physical of fields) data[physical] = nextValues.get(physical) ?? 1;
|
|
656
891
|
}
|
|
657
892
|
}
|
|
658
893
|
this._log("insertOne", data);
|
|
659
|
-
const result = await this.collection.insertOne(data, this._getSessionOpts());
|
|
660
|
-
|
|
894
|
+
const result = await this._wrapDuplicateKeyError(() => this.collection.insertOne(data, this._getSessionOpts()));
|
|
895
|
+
const metaIdPhysical = this._getMetaIdPhysical();
|
|
896
|
+
return { insertedId: metaIdPhysical ? data[metaIdPhysical] ?? result.insertedId : result.insertedId };
|
|
661
897
|
}
|
|
662
898
|
async insertMany(data) {
|
|
663
899
|
if (this._incrementFields.size > 0) {
|
|
664
900
|
const allFields = new Set();
|
|
665
901
|
for (const item of data) for (const f of this._fieldsNeedingIncrement(item)) allFields.add(f);
|
|
666
|
-
if (allFields.size > 0)
|
|
667
|
-
const maxValues = await this._getMaxValues([...allFields]);
|
|
668
|
-
for (const item of data) for (const physical of allFields) if (item[physical] === undefined || item[physical] === null) {
|
|
669
|
-
const next = (maxValues.get(physical) ?? 0) + 1;
|
|
670
|
-
item[physical] = next;
|
|
671
|
-
maxValues.set(physical, next);
|
|
672
|
-
} else if (typeof item[physical] === "number") {
|
|
673
|
-
const current = maxValues.get(physical) ?? 0;
|
|
674
|
-
if (item[physical] > current) maxValues.set(physical, item[physical]);
|
|
675
|
-
}
|
|
676
|
-
}
|
|
902
|
+
if (allFields.size > 0) await this._assignBatchIncrements(data, allFields);
|
|
677
903
|
}
|
|
678
904
|
this._log("insertMany", `${data.length} docs`);
|
|
679
|
-
const result = await this.collection.insertMany(data, this._getSessionOpts());
|
|
905
|
+
const result = await this._wrapDuplicateKeyError(() => this.collection.insertMany(data, this._getSessionOpts()));
|
|
906
|
+
const metaIdPhysical = this._getMetaIdPhysical();
|
|
680
907
|
return {
|
|
681
908
|
insertedCount: result.insertedCount,
|
|
682
|
-
insertedIds: Object.values(result.insertedIds)
|
|
909
|
+
insertedIds: metaIdPhysical ? data.map((item, i) => item[metaIdPhysical] ?? result.insertedIds[i]) : Object.values(result.insertedIds)
|
|
683
910
|
};
|
|
684
911
|
}
|
|
685
912
|
async findOne(query) {
|
|
@@ -717,7 +944,7 @@ else pipeline.push({ $limit: 1e3 });
|
|
|
717
944
|
async replaceOne(filter, data) {
|
|
718
945
|
const mongoFilter = buildMongoFilter(filter);
|
|
719
946
|
this._log("replaceOne", mongoFilter, data);
|
|
720
|
-
const result = await this.collection.replaceOne(mongoFilter, data, this._getSessionOpts());
|
|
947
|
+
const result = await this._wrapDuplicateKeyError(() => this.collection.replaceOne(mongoFilter, data, this._getSessionOpts()));
|
|
721
948
|
return {
|
|
722
949
|
matchedCount: result.matchedCount,
|
|
723
950
|
modifiedCount: result.modifiedCount
|
|
@@ -753,14 +980,180 @@ else pipeline.push({ $limit: 1e3 });
|
|
|
753
980
|
const result = await this.collection.deleteMany(mongoFilter, this._getSessionOpts());
|
|
754
981
|
return { deletedCount: result.deletedCount };
|
|
755
982
|
}
|
|
983
|
+
async tableExists() {
|
|
984
|
+
return this.collectionExists();
|
|
985
|
+
}
|
|
986
|
+
async detectTableOptionDrift() {
|
|
987
|
+
if (!this._cappedOptions) return false;
|
|
988
|
+
const cols = await this.db.listCollections({ name: this._table.tableName }, { nameOnly: false }).toArray();
|
|
989
|
+
if (cols.length === 0) return false;
|
|
990
|
+
const opts = cols[0].options;
|
|
991
|
+
if (!opts?.capped) return true;
|
|
992
|
+
if (opts.size !== this._cappedOptions.size) return true;
|
|
993
|
+
if ((opts.max ?? undefined) !== (this._cappedOptions.max ?? undefined)) return true;
|
|
994
|
+
return false;
|
|
995
|
+
}
|
|
756
996
|
async ensureTable() {
|
|
997
|
+
if (this._table instanceof AtscriptDbView && !this._table.isExternal) return this._ensureView(this._table);
|
|
757
998
|
return this.ensureCollectionExists();
|
|
758
999
|
}
|
|
1000
|
+
/**
|
|
1001
|
+
* Creates a MongoDB view from the AtscriptDbView's view plan.
|
|
1002
|
+
* Translates joins → $lookup/$unwind, filter → $match, columns → $project.
|
|
1003
|
+
*/ async _ensureView(view) {
|
|
1004
|
+
const exists = await this.collectionExists();
|
|
1005
|
+
if (exists) return;
|
|
1006
|
+
const plan = view.viewPlan;
|
|
1007
|
+
const columns = view.getViewColumnMappings();
|
|
1008
|
+
const pipeline = [];
|
|
1009
|
+
for (const join of plan.joins) {
|
|
1010
|
+
const { localField, foreignField } = this._resolveJoinFields(join.condition, plan.entryTable, join.targetTable);
|
|
1011
|
+
pipeline.push({ $lookup: {
|
|
1012
|
+
from: join.targetTable,
|
|
1013
|
+
localField,
|
|
1014
|
+
foreignField,
|
|
1015
|
+
as: `${JOINED_PREFIX}${join.targetTable}`
|
|
1016
|
+
} });
|
|
1017
|
+
pipeline.push({ $unwind: {
|
|
1018
|
+
path: `$__joined_${join.targetTable}`,
|
|
1019
|
+
preserveNullAndEmptyArrays: true
|
|
1020
|
+
} });
|
|
1021
|
+
}
|
|
1022
|
+
if (plan.filter) {
|
|
1023
|
+
const matchExpr = this._queryNodeToMatch(plan.filter, plan.entryTable);
|
|
1024
|
+
pipeline.push({ $match: matchExpr });
|
|
1025
|
+
}
|
|
1026
|
+
const project = { _id: 0 };
|
|
1027
|
+
for (const col of columns) if (col.sourceTable === plan.entryTable) project[col.viewColumn] = `$${col.sourceColumn}`;
|
|
1028
|
+
else project[col.viewColumn] = `$${JOINED_PREFIX}${col.sourceTable}.${col.sourceColumn}`;
|
|
1029
|
+
pipeline.push({ $project: project });
|
|
1030
|
+
this._log("createView", this._table.tableName, plan.entryTable, pipeline);
|
|
1031
|
+
await this.db.createCollection(this._table.tableName, {
|
|
1032
|
+
viewOn: plan.entryTable,
|
|
1033
|
+
pipeline
|
|
1034
|
+
});
|
|
1035
|
+
}
|
|
1036
|
+
/**
|
|
1037
|
+
* Extracts localField/foreignField from a join condition like `User.id = Task.assigneeId`.
|
|
1038
|
+
* The condition is a comparison node with two field refs.
|
|
1039
|
+
*/ _resolveJoinFields(condition, entryTable, joinTable) {
|
|
1040
|
+
const comp = "$and" in condition ? condition.$and[0] : condition;
|
|
1041
|
+
const c = comp;
|
|
1042
|
+
const leftTable = c.left.type ? c.left.type()?.metadata?.get("db.table") || "" : entryTable;
|
|
1043
|
+
if (leftTable === joinTable) return {
|
|
1044
|
+
localField: c.right.field,
|
|
1045
|
+
foreignField: c.left.field
|
|
1046
|
+
};
|
|
1047
|
+
return {
|
|
1048
|
+
localField: c.left.field,
|
|
1049
|
+
foreignField: c.right.field
|
|
1050
|
+
};
|
|
1051
|
+
}
|
|
1052
|
+
/**
|
|
1053
|
+
* Translates an AtscriptQueryNode to a MongoDB $match expression.
|
|
1054
|
+
* Field refs are resolved to dot-path references (joined fields use JOINED_PREFIX).
|
|
1055
|
+
*/ _queryNodeToMatch(node, entryTable) {
|
|
1056
|
+
if ("$and" in node) return { $and: node.$and.map((n) => this._queryNodeToMatch(n, entryTable)) };
|
|
1057
|
+
if ("$or" in node) return { $or: node.$or.map((n) => this._queryNodeToMatch(n, entryTable)) };
|
|
1058
|
+
if ("$not" in node) return { $not: this._queryNodeToMatch(node.$not, entryTable) };
|
|
1059
|
+
const comp = node;
|
|
1060
|
+
const fieldPath = this._resolveViewFieldPath(comp.left, entryTable);
|
|
1061
|
+
if (comp.right && typeof comp.right === "object" && "field" in comp.right) {
|
|
1062
|
+
const rightPath = this._resolveViewFieldPath(comp.right, entryTable);
|
|
1063
|
+
return { $expr: { [comp.op]: [`$${fieldPath}`, `$${rightPath}`] } };
|
|
1064
|
+
}
|
|
1065
|
+
if (comp.op === "$eq") return { [fieldPath]: comp.right };
|
|
1066
|
+
if (comp.op === "$ne") return { [fieldPath]: { $ne: comp.right } };
|
|
1067
|
+
return { [fieldPath]: { [comp.op]: comp.right } };
|
|
1068
|
+
}
|
|
1069
|
+
/**
|
|
1070
|
+
* Resolves a field ref to a MongoDB dot path for view pipeline expressions.
|
|
1071
|
+
*/ _resolveViewFieldPath(ref, entryTable) {
|
|
1072
|
+
if (!ref.type) return ref.field;
|
|
1073
|
+
const table = ref.type()?.metadata?.get("db.table") || "";
|
|
1074
|
+
if (table === entryTable) return ref.field;
|
|
1075
|
+
return `${JOINED_PREFIX}${table}.${ref.field}`;
|
|
1076
|
+
}
|
|
759
1077
|
async dropTable() {
|
|
760
1078
|
this._log("drop", this._table.tableName);
|
|
761
1079
|
await this.collection.drop();
|
|
762
1080
|
this._collection = undefined;
|
|
763
1081
|
}
|
|
1082
|
+
async dropViewByName(viewName) {
|
|
1083
|
+
this._log("dropView", viewName);
|
|
1084
|
+
try {
|
|
1085
|
+
await this.db.collection(viewName).drop();
|
|
1086
|
+
} catch {}
|
|
1087
|
+
}
|
|
1088
|
+
async dropTableByName(tableName) {
|
|
1089
|
+
this._log("dropByName", tableName);
|
|
1090
|
+
try {
|
|
1091
|
+
await this.db.collection(tableName).drop();
|
|
1092
|
+
} catch {}
|
|
1093
|
+
}
|
|
1094
|
+
async recreateTable() {
|
|
1095
|
+
const tableName = this._table.tableName;
|
|
1096
|
+
this._log("recreateTable", tableName);
|
|
1097
|
+
const tempName = `${tableName}__tmp_${Date.now()}`;
|
|
1098
|
+
const source = this.db.collection(tableName);
|
|
1099
|
+
const count = await source.countDocuments();
|
|
1100
|
+
if (count > 0) await source.aggregate([{ $out: tempName }]).toArray();
|
|
1101
|
+
await this.collection.drop();
|
|
1102
|
+
this._collection = undefined;
|
|
1103
|
+
await this.ensureCollectionExists();
|
|
1104
|
+
if (count > 0) {
|
|
1105
|
+
const temp = this.db.collection(tempName);
|
|
1106
|
+
await temp.aggregate([{ $merge: { into: tableName } }]).toArray();
|
|
1107
|
+
await temp.drop();
|
|
1108
|
+
}
|
|
1109
|
+
}
|
|
1110
|
+
async syncColumns(diff) {
|
|
1111
|
+
const renamed = [];
|
|
1112
|
+
const added = [];
|
|
1113
|
+
const update = {};
|
|
1114
|
+
if (diff.renamed.length > 0) {
|
|
1115
|
+
const renameSpec = {};
|
|
1116
|
+
for (const r of diff.renamed) {
|
|
1117
|
+
renameSpec[r.oldName] = r.field.physicalName;
|
|
1118
|
+
renamed.push(r.field.physicalName);
|
|
1119
|
+
}
|
|
1120
|
+
update.$rename = renameSpec;
|
|
1121
|
+
}
|
|
1122
|
+
if (diff.added.length > 0) {
|
|
1123
|
+
const setSpec = {};
|
|
1124
|
+
for (const field of diff.added) {
|
|
1125
|
+
const defaultVal = this._resolveSyncDefault(field);
|
|
1126
|
+
if (defaultVal !== undefined) setSpec[field.physicalName] = defaultVal;
|
|
1127
|
+
added.push(field.physicalName);
|
|
1128
|
+
}
|
|
1129
|
+
if (Object.keys(setSpec).length > 0) update.$set = setSpec;
|
|
1130
|
+
}
|
|
1131
|
+
if (Object.keys(update).length > 0) await this.collection.updateMany({}, update, this._getSessionOpts());
|
|
1132
|
+
return {
|
|
1133
|
+
added,
|
|
1134
|
+
renamed
|
|
1135
|
+
};
|
|
1136
|
+
}
|
|
1137
|
+
async dropColumns(columns) {
|
|
1138
|
+
if (columns.length === 0) return;
|
|
1139
|
+
const unsetSpec = {};
|
|
1140
|
+
for (const col of columns) unsetSpec[col] = "";
|
|
1141
|
+
await this.collection.updateMany({}, { $unset: unsetSpec }, this._getSessionOpts());
|
|
1142
|
+
}
|
|
1143
|
+
async renameTable(oldName) {
|
|
1144
|
+
const newName = this.resolveTableName(false);
|
|
1145
|
+
this._log("renameTable", oldName, "→", newName);
|
|
1146
|
+
await this.db.renameCollection(oldName, newName);
|
|
1147
|
+
this._collection = undefined;
|
|
1148
|
+
}
|
|
1149
|
+
/**
|
|
1150
|
+
* Resolves a field's default value for bulk $set during column sync.
|
|
1151
|
+
* Returns `undefined` if no concrete default can be determined.
|
|
1152
|
+
*/ _resolveSyncDefault(field) {
|
|
1153
|
+
if (!field.defaultValue) return field.optional ? null : undefined;
|
|
1154
|
+
if (field.defaultValue.kind === "value") return field.defaultValue.value;
|
|
1155
|
+
return undefined;
|
|
1156
|
+
}
|
|
764
1157
|
async syncIndexes() {
|
|
765
1158
|
await this.ensureCollectionExists();
|
|
766
1159
|
const allIndexes = new Map();
|
|
@@ -893,25 +1286,84 @@ else toUpdate.add(remote.name);
|
|
|
893
1286
|
}
|
|
894
1287
|
} catch {}
|
|
895
1288
|
}
|
|
1289
|
+
/**
|
|
1290
|
+
* Returns the physical column name of the single @meta.id field (if any).
|
|
1291
|
+
* Used to return the user's logical ID instead of MongoDB's _id on insert.
|
|
1292
|
+
*/ _getMetaIdPhysical() {
|
|
1293
|
+
if (this._metaIdPhysical === undefined) {
|
|
1294
|
+
const fields = this._table.originalMetaIdFields;
|
|
1295
|
+
if (fields.length === 1) {
|
|
1296
|
+
const field = fields[0];
|
|
1297
|
+
this._metaIdPhysical = this._table.columnMap.get(field) ?? field;
|
|
1298
|
+
} else this._metaIdPhysical = null;
|
|
1299
|
+
}
|
|
1300
|
+
return this._metaIdPhysical;
|
|
1301
|
+
}
|
|
1302
|
+
/** Returns the counters collection used for atomic auto-increment. */ get _countersCollection() {
|
|
1303
|
+
return this.db.collection("__atscript_counters");
|
|
1304
|
+
}
|
|
896
1305
|
/** Returns physical field names of increment fields that are undefined in the data. */ _fieldsNeedingIncrement(data) {
|
|
897
1306
|
const result = [];
|
|
898
1307
|
for (const physical of this._incrementFields) if (data[physical] === undefined || data[physical] === null) result.push(physical);
|
|
899
1308
|
return result;
|
|
900
1309
|
}
|
|
901
|
-
/**
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
const
|
|
906
|
-
const
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
1310
|
+
/**
|
|
1311
|
+
* Atomically allocates `count` sequential values for each increment field
|
|
1312
|
+
* using a counter collection. Returns a map of field → first allocated value.
|
|
1313
|
+
*/ async _allocateIncrementValues(physicalFields, count) {
|
|
1314
|
+
const counters = this._countersCollection;
|
|
1315
|
+
const collectionName = this._table.tableName;
|
|
1316
|
+
const result = new Map();
|
|
1317
|
+
for (const field of physicalFields) {
|
|
1318
|
+
const counterId = `${collectionName}.${field}`;
|
|
1319
|
+
const doc = await counters.findOneAndUpdate({ _id: counterId }, { $inc: { seq: count } }, {
|
|
1320
|
+
upsert: true,
|
|
1321
|
+
returnDocument: "after",
|
|
1322
|
+
...this._getSessionOpts()
|
|
1323
|
+
});
|
|
1324
|
+
const seq = doc?.seq ?? count;
|
|
1325
|
+
if (seq === count) {
|
|
1326
|
+
const currentMax = await this._getCurrentFieldMax(field);
|
|
1327
|
+
if (currentMax >= seq) {
|
|
1328
|
+
const adjusted = currentMax + count;
|
|
1329
|
+
await counters.updateOne({ _id: counterId }, { $max: { seq: adjusted } }, this._getSessionOpts());
|
|
1330
|
+
result.set(field, currentMax + 1);
|
|
1331
|
+
continue;
|
|
1332
|
+
}
|
|
912
1333
|
}
|
|
1334
|
+
result.set(field, seq - count + 1);
|
|
1335
|
+
}
|
|
1336
|
+
return result;
|
|
1337
|
+
}
|
|
1338
|
+
/** Reads current max value for a single field via $group aggregation. */ async _getCurrentFieldMax(field) {
|
|
1339
|
+
const alias = `max__${field.replace(/\./g, "__")}`;
|
|
1340
|
+
const agg = await this.collection.aggregate([{ $group: {
|
|
1341
|
+
_id: null,
|
|
1342
|
+
[alias]: { $max: `$${field}` }
|
|
1343
|
+
} }], this._getSessionOpts()).toArray();
|
|
1344
|
+
if (agg.length > 0) {
|
|
1345
|
+
const val = agg[0][alias];
|
|
1346
|
+
if (typeof val === "number") return val;
|
|
1347
|
+
}
|
|
1348
|
+
return 0;
|
|
1349
|
+
}
|
|
1350
|
+
/** Allocates increment values for a batch of items, assigning in order. */ async _assignBatchIncrements(data, allFields) {
|
|
1351
|
+
const fieldCounts = new Map();
|
|
1352
|
+
for (const physical of allFields) {
|
|
1353
|
+
let count = 0;
|
|
1354
|
+
for (const item of data) if (item[physical] === undefined || item[physical] === null) count++;
|
|
1355
|
+
if (count > 0) fieldCounts.set(physical, count);
|
|
1356
|
+
}
|
|
1357
|
+
const fieldCounters = new Map();
|
|
1358
|
+
for (const [physical, count] of fieldCounts) {
|
|
1359
|
+
const allocated = await this._allocateIncrementValues([physical], count);
|
|
1360
|
+
fieldCounters.set(physical, allocated.get(physical) ?? 1);
|
|
1361
|
+
}
|
|
1362
|
+
for (const item of data) for (const physical of allFields) if (item[physical] === undefined || item[physical] === null) {
|
|
1363
|
+
const next = fieldCounters.get(physical) ?? 1;
|
|
1364
|
+
item[physical] = next;
|
|
1365
|
+
fieldCounters.set(physical, next + 1);
|
|
913
1366
|
}
|
|
914
|
-
return maxMap;
|
|
915
1367
|
}
|
|
916
1368
|
_buildFindOptions(controls) {
|
|
917
1369
|
const opts = {};
|
|
@@ -963,11 +1415,11 @@ else {
|
|
|
963
1415
|
if (analyzer) index.definition.mappings.fields[fieldName].analyzer = analyzer;
|
|
964
1416
|
}
|
|
965
1417
|
}
|
|
966
|
-
constructor(db,
|
|
967
|
-
super(), _define_property
|
|
1418
|
+
constructor(db, client) {
|
|
1419
|
+
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;
|
|
968
1420
|
}
|
|
969
1421
|
};
|
|
970
|
-
_define_property
|
|
1422
|
+
_define_property(MongoAdapter, "_noSession", Object.freeze({}));
|
|
971
1423
|
function objMatch(o1, o2) {
|
|
972
1424
|
const keys1 = Object.keys(o1);
|
|
973
1425
|
const keys2 = Object.keys(o2);
|
|
@@ -996,48 +1448,13 @@ function vectorFieldsMatch(left, right) {
|
|
|
996
1448
|
return true;
|
|
997
1449
|
}
|
|
998
1450
|
|
|
999
|
-
//#endregion
|
|
1000
|
-
//#region packages/mongo/src/lib/as-mongo.ts
|
|
1001
|
-
function _define_property(obj, key, value) {
|
|
1002
|
-
if (key in obj) Object.defineProperty(obj, key, {
|
|
1003
|
-
value,
|
|
1004
|
-
enumerable: true,
|
|
1005
|
-
configurable: true,
|
|
1006
|
-
writable: true
|
|
1007
|
-
});
|
|
1008
|
-
else obj[key] = value;
|
|
1009
|
-
return obj;
|
|
1010
|
-
}
|
|
1011
|
-
var AsMongo = class extends DbSpace {
|
|
1012
|
-
get db() {
|
|
1013
|
-
return this.client.db();
|
|
1014
|
-
}
|
|
1015
|
-
getCollectionsList() {
|
|
1016
|
-
if (!this.collectionsList) this.collectionsList = this.db.listCollections().toArray().then((c) => new Set(c.map((c$1) => c$1.name)));
|
|
1017
|
-
return this.collectionsList;
|
|
1018
|
-
}
|
|
1019
|
-
async collectionExists(name) {
|
|
1020
|
-
const list = await this.getCollectionsList();
|
|
1021
|
-
return list.has(name);
|
|
1022
|
-
}
|
|
1023
|
-
/**
|
|
1024
|
-
* Returns the MongoAdapter for the given type.
|
|
1025
|
-
* Convenience accessor for Mongo-specific adapter operations.
|
|
1026
|
-
*/ getAdapter(type) {
|
|
1027
|
-
return super.getAdapter(type);
|
|
1028
|
-
}
|
|
1029
|
-
constructor(client, logger = NoopLogger) {
|
|
1030
|
-
const resolvedClient = typeof client === "string" ? new MongoClient(client) : client;
|
|
1031
|
-
super(() => new MongoAdapter(this.db, this), logger), _define_property(this, "client", void 0), _define_property(this, "collectionsList", void 0);
|
|
1032
|
-
this.client = resolvedClient;
|
|
1033
|
-
}
|
|
1034
|
-
};
|
|
1035
|
-
|
|
1036
1451
|
//#endregion
|
|
1037
1452
|
//#region packages/mongo/src/lib/index.ts
|
|
1038
1453
|
function createAdapter(connection, _options) {
|
|
1039
|
-
|
|
1454
|
+
const client = new MongoClient(connection);
|
|
1455
|
+
const db = client.db();
|
|
1456
|
+
return new DbSpace(() => new MongoAdapter(db, client));
|
|
1040
1457
|
}
|
|
1041
1458
|
|
|
1042
1459
|
//#endregion
|
|
1043
|
-
export {
|
|
1460
|
+
export { CollectionPatcher, MongoAdapter, buildMongoFilter, createAdapter, validateMongoIdPlugin };
|