@atscript/mongo 0.1.34 → 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/LICENSE +1 -1
- package/dist/index.cjs +725 -196
- package/dist/index.d.ts +114 -56
- package/dist/index.mjs +727 -198
- package/dist/plugin.cjs +16 -0
- package/dist/plugin.mjs +16 -0
- package/package.json +5 -5
package/dist/index.mjs
CHANGED
|
@@ -1,15 +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/validate-plugins.ts
|
|
6
|
-
const validateMongoIdPlugin = (ctx, def, value) => {
|
|
7
|
-
if (ctx.path === "_id" && def.type.tags.has("objectId")) return ctx.validateAnnotatedType(def, value instanceof ObjectId ? value.toString() : value);
|
|
8
|
-
};
|
|
9
|
-
|
|
10
|
-
//#endregion
|
|
11
4
|
//#region packages/mongo/src/lib/collection-patcher.ts
|
|
12
|
-
function _define_property$
|
|
5
|
+
function _define_property$1(obj, key, value) {
|
|
13
6
|
if (key in obj) Object.defineProperty(obj, key, {
|
|
14
7
|
value,
|
|
15
8
|
enumerable: true,
|
|
@@ -19,50 +12,7 @@ function _define_property$2(obj, key, value) {
|
|
|
19
12
|
else obj[key] = value;
|
|
20
13
|
return obj;
|
|
21
14
|
}
|
|
22
|
-
var CollectionPatcher = class
|
|
23
|
-
/**
|
|
24
|
-
* Build a runtime *Validator* that understands the extended patch payload.
|
|
25
|
-
*
|
|
26
|
-
* * Adds per‑array *patch* wrappers (the `$replace`, `$insert`, … fields).
|
|
27
|
-
* * Honors `db.patch.strategy === "merge"` metadata.
|
|
28
|
-
*
|
|
29
|
-
* @param collection Target collection wrapper
|
|
30
|
-
* @returns Atscript Validator
|
|
31
|
-
*/ static prepareValidator(context) {
|
|
32
|
-
return context.createValidator({
|
|
33
|
-
plugins: [validateMongoIdPlugin],
|
|
34
|
-
replace: (def, path) => {
|
|
35
|
-
if (path === "" && def.type.kind === "object") {
|
|
36
|
-
const obj = defineAnnotatedType("object").copyMetadata(def.metadata);
|
|
37
|
-
for (const [prop, type] of def.type.props.entries()) obj.prop(prop, defineAnnotatedType().refTo(type).copyMetadata(type.metadata).optional(prop !== "_id").$type);
|
|
38
|
-
return obj.$type;
|
|
39
|
-
}
|
|
40
|
-
if (def.type.kind === "array" && context.flatMap.get(path)?.metadata.get("db.mongo.__topLevelArray") && !def.metadata.has("db.mongo.__patchArrayValue")) {
|
|
41
|
-
const defArray = def;
|
|
42
|
-
const mergeStrategy = defArray.metadata.get("db.patch.strategy") === "merge";
|
|
43
|
-
function getPatchType() {
|
|
44
|
-
const isPrimitive = isAnnotatedTypeOfPrimitive(defArray.type.of);
|
|
45
|
-
if (isPrimitive) return defineAnnotatedType().refTo(def).copyMetadata(def.metadata).annotate("db.mongo.__patchArrayValue").optional().$type;
|
|
46
|
-
if (defArray.type.of.type.kind === "object") {
|
|
47
|
-
const objType = defArray.type.of.type;
|
|
48
|
-
const t = defineAnnotatedType("object").copyMetadata(defArray.type.of.metadata);
|
|
49
|
-
const keyProps = CollectionPatcher.getKeyProps(defArray);
|
|
50
|
-
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);
|
|
51
|
-
else t.prop(key, defineAnnotatedType().refTo(val).copyMetadata(def.metadata).optional().$type);
|
|
52
|
-
else t.prop(key, defineAnnotatedType().refTo(val).copyMetadata(def.metadata).optional(!!val.optional).$type);
|
|
53
|
-
return defineAnnotatedType("array").of(t.$type).copyMetadata(def.metadata).annotate("db.mongo.__patchArrayValue").optional().$type;
|
|
54
|
-
}
|
|
55
|
-
return undefined;
|
|
56
|
-
}
|
|
57
|
-
const fullType = defineAnnotatedType().refTo(def).copyMetadata(def.metadata).annotate("db.mongo.__patchArrayValue").optional().$type;
|
|
58
|
-
const patchType = getPatchType();
|
|
59
|
-
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;
|
|
60
|
-
}
|
|
61
|
-
return def;
|
|
62
|
-
},
|
|
63
|
-
partial: (def, path) => path !== "" && def.metadata.get("db.patch.strategy") === "merge"
|
|
64
|
-
});
|
|
65
|
-
}
|
|
15
|
+
var CollectionPatcher = class {
|
|
66
16
|
/**
|
|
67
17
|
* Entry point – walk the payload, build `filter`, `update` and `options`.
|
|
68
18
|
*
|
|
@@ -109,8 +59,8 @@ else t.prop(key, defineAnnotatedType().refTo(val).copyMetadata(def.metadata).opt
|
|
|
109
59
|
for (const [_key, value] of Object.entries(payload)) {
|
|
110
60
|
const key = evalKey(_key);
|
|
111
61
|
const flatType = this.collection.flatMap.get(key);
|
|
112
|
-
const topLevelArray = flatType?.metadata?.get("db.
|
|
113
|
-
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);
|
|
114
64
|
else if (typeof value === "object" && flatType?.metadata?.get("db.patch.strategy") === "merge") this.flattenPayload(value, key);
|
|
115
65
|
else if (key !== "_id") this._set(key, value);
|
|
116
66
|
}
|
|
@@ -174,22 +124,37 @@ else this._set(key, { $concatArrays: [{ $ifNull: [`$${key}`, []] }, input] });
|
|
|
174
124
|
* `$upsert`
|
|
175
125
|
* - keyed → remove existing matching by key(s) then append candidate
|
|
176
126
|
* - unique → $setUnion (deep equality)
|
|
177
|
-
*/ _upsert(key, input, keys,
|
|
127
|
+
*/ _upsert(key, input, keys, flatType) {
|
|
178
128
|
if (!input?.length) return;
|
|
179
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
|
+
}
|
|
180
148
|
this._set(key, { $reduce: {
|
|
181
149
|
input,
|
|
182
150
|
initialValue: { $ifNull: [`$${key}`, []] },
|
|
183
151
|
in: { $let: {
|
|
184
|
-
vars
|
|
185
|
-
acc: "$$value",
|
|
186
|
-
cand: "$$this"
|
|
187
|
-
},
|
|
152
|
+
vars,
|
|
188
153
|
in: { $concatArrays: [{ $filter: {
|
|
189
154
|
input: "$$acc",
|
|
190
155
|
as: "el",
|
|
191
156
|
cond: { $not: this._keysEqual(keys, "$$el", "$$cand") }
|
|
192
|
-
} }, [
|
|
157
|
+
} }, [appendExpr]] }
|
|
193
158
|
} }
|
|
194
159
|
} });
|
|
195
160
|
return;
|
|
@@ -240,15 +205,15 @@ else this._set(key, { $concatArrays: [{ $ifNull: [`$${key}`, []] }, input] });
|
|
|
240
205
|
else this._set(key, { $setDifference: [{ $ifNull: [`$${key}`, []] }, input] });
|
|
241
206
|
}
|
|
242
207
|
constructor(collection, payload) {
|
|
243
|
-
_define_property$
|
|
244
|
-
_define_property$
|
|
208
|
+
_define_property$1(this, "collection", void 0);
|
|
209
|
+
_define_property$1(this, "payload", void 0);
|
|
245
210
|
/**
|
|
246
211
|
* Internal accumulator: filter passed to `updateOne()`.
|
|
247
212
|
* Filled only with the `_id` field right now.
|
|
248
|
-
*/ _define_property$
|
|
249
|
-
/** MongoDB *update* document being built. */ _define_property$
|
|
250
|
-
/** Current `$set` stage being populated. */ _define_property$
|
|
251
|
-
/** 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);
|
|
252
217
|
this.collection = collection;
|
|
253
218
|
this.payload = payload;
|
|
254
219
|
this.filterObj = {};
|
|
@@ -257,7 +222,7 @@ else this._set(key, { $setDifference: [{ $ifNull: [`$${key}`, []] }, input] });
|
|
|
257
222
|
this.optionsObj = {};
|
|
258
223
|
}
|
|
259
224
|
};
|
|
260
|
-
_define_property$
|
|
225
|
+
_define_property$1(CollectionPatcher, "getKeyProps", getKeyProps);
|
|
261
226
|
|
|
262
227
|
//#endregion
|
|
263
228
|
//#region packages/mongo/src/lib/mongo-filter.ts
|
|
@@ -283,12 +248,24 @@ const mongoVisitor = {
|
|
|
283
248
|
};
|
|
284
249
|
function buildMongoFilter(filter) {
|
|
285
250
|
if (!filter || Object.keys(filter).length === 0) return EMPTY;
|
|
286
|
-
return walkFilter(filter, mongoVisitor);
|
|
251
|
+
return walkFilter(filter, mongoVisitor) ?? EMPTY;
|
|
287
252
|
}
|
|
288
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
|
+
|
|
289
266
|
//#endregion
|
|
290
267
|
//#region packages/mongo/src/lib/mongo-adapter.ts
|
|
291
|
-
function _define_property
|
|
268
|
+
function _define_property(obj, key, value) {
|
|
292
269
|
if (key in obj) Object.defineProperty(obj, key, {
|
|
293
270
|
value,
|
|
294
271
|
enumerable: true,
|
|
@@ -300,17 +277,63 @@ else obj[key] = value;
|
|
|
300
277
|
}
|
|
301
278
|
const INDEX_PREFIX = "atscript__";
|
|
302
279
|
const DEFAULT_INDEX_NAME = "DEFAULT";
|
|
280
|
+
const JOINED_PREFIX = "__joined_";
|
|
303
281
|
function mongoIndexKey(type, name) {
|
|
304
282
|
const cleanName = name.replace(/[^a-z0-9_.-]/gi, "_").replace(/_+/g, "_").slice(0, 127 - INDEX_PREFIX.length - type.length - 2);
|
|
305
283
|
return `${INDEX_PREFIX}${type}__${cleanName}`;
|
|
306
284
|
}
|
|
307
|
-
var MongoAdapter = class extends BaseDbAdapter {
|
|
285
|
+
var MongoAdapter = class MongoAdapter extends BaseDbAdapter {
|
|
286
|
+
get _client() {
|
|
287
|
+
return this.client;
|
|
288
|
+
}
|
|
289
|
+
async _beginTransaction() {
|
|
290
|
+
if (this._txDisabled || !this._client) return undefined;
|
|
291
|
+
try {
|
|
292
|
+
const topology = this._client.topology;
|
|
293
|
+
if (topology) {
|
|
294
|
+
const desc = topology.description ?? topology.s?.description;
|
|
295
|
+
const type = desc?.type;
|
|
296
|
+
if (type === "Single" || type === "Unknown") {
|
|
297
|
+
this._txDisabled = true;
|
|
298
|
+
return undefined;
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
const session = this._client.startSession();
|
|
302
|
+
session.startTransaction();
|
|
303
|
+
return session;
|
|
304
|
+
} catch {
|
|
305
|
+
this._txDisabled = true;
|
|
306
|
+
return undefined;
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
async _commitTransaction(state) {
|
|
310
|
+
if (!state) return;
|
|
311
|
+
const session = state;
|
|
312
|
+
try {
|
|
313
|
+
await session.commitTransaction();
|
|
314
|
+
} finally {
|
|
315
|
+
session.endSession();
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
async _rollbackTransaction(state) {
|
|
319
|
+
if (!state) return;
|
|
320
|
+
const session = state;
|
|
321
|
+
try {
|
|
322
|
+
await session.abortTransaction();
|
|
323
|
+
} finally {
|
|
324
|
+
session.endSession();
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
/** Returns `{ session }` opts if inside a transaction, empty object otherwise. */ _getSessionOpts() {
|
|
328
|
+
const session = this._getTransactionState();
|
|
329
|
+
return session ? { session } : MongoAdapter._noSession;
|
|
330
|
+
}
|
|
308
331
|
get collection() {
|
|
309
332
|
if (!this._collection) this._collection = this.db.collection(this.resolveTableName(false));
|
|
310
333
|
return this._collection;
|
|
311
334
|
}
|
|
312
335
|
aggregate(pipeline) {
|
|
313
|
-
return this.collection.aggregate(pipeline);
|
|
336
|
+
return this.collection.aggregate(pipeline, this._getSessionOpts());
|
|
314
337
|
}
|
|
315
338
|
get idType() {
|
|
316
339
|
const idProp = this._table.type.type.props.get("_id");
|
|
@@ -348,30 +371,260 @@ var MongoAdapter = class extends BaseDbAdapter {
|
|
|
348
371
|
getValidatorPlugins() {
|
|
349
372
|
return [validateMongoIdPlugin];
|
|
350
373
|
}
|
|
351
|
-
getTopLevelArrayTag() {
|
|
352
|
-
return "db.mongo.__topLevelArray";
|
|
353
|
-
}
|
|
354
374
|
getAdapterTableName(type) {
|
|
355
375
|
return undefined;
|
|
356
376
|
}
|
|
357
|
-
|
|
358
|
-
return
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
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);
|
|
370
415
|
}
|
|
371
|
-
|
|
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
|
+
}
|
|
372
447
|
}
|
|
373
|
-
|
|
374
|
-
|
|
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
|
+
};
|
|
538
|
+
}
|
|
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 || "";
|
|
375
628
|
}
|
|
376
629
|
/** Returns the context object used by CollectionPatcher. */ getPatcherContext() {
|
|
377
630
|
return {
|
|
@@ -384,7 +637,11 @@ var MongoAdapter = class extends BaseDbAdapter {
|
|
|
384
637
|
const mongoFilter = buildMongoFilter(filter);
|
|
385
638
|
const patcher = new CollectionPatcher(this.getPatcherContext(), patch);
|
|
386
639
|
const { updateFilter, updateOptions } = patcher.preparePatch();
|
|
387
|
-
|
|
640
|
+
this._log("updateOne (patch)", mongoFilter, updateFilter);
|
|
641
|
+
const result = await this.collection.updateOne(mongoFilter, updateFilter, {
|
|
642
|
+
...updateOptions,
|
|
643
|
+
...this._getSessionOpts()
|
|
644
|
+
});
|
|
388
645
|
return {
|
|
389
646
|
matchedCount: result.matchedCount,
|
|
390
647
|
modifiedCount: result.modifiedCount
|
|
@@ -392,6 +649,11 @@ var MongoAdapter = class extends BaseDbAdapter {
|
|
|
392
649
|
}
|
|
393
650
|
onBeforeFlatten(type) {
|
|
394
651
|
const typeMeta = type.metadata;
|
|
652
|
+
const capped = typeMeta.get("db.mongo.capped");
|
|
653
|
+
if (capped) this._cappedOptions = {
|
|
654
|
+
size: capped.size,
|
|
655
|
+
max: capped.max
|
|
656
|
+
};
|
|
395
657
|
const dynamicText = typeMeta.get("db.mongo.search.dynamic");
|
|
396
658
|
if (dynamicText) this._setSearchIndex("dynamic_text", "_", {
|
|
397
659
|
mappings: { dynamic: true },
|
|
@@ -405,8 +667,8 @@ var MongoAdapter = class extends BaseDbAdapter {
|
|
|
405
667
|
});
|
|
406
668
|
}
|
|
407
669
|
onFieldScanned(field, type, metadata) {
|
|
670
|
+
if (field === "_id") this._hasExplicitId = true;
|
|
408
671
|
if (field !== "_id" && metadata.has("meta.id")) {
|
|
409
|
-
this._table.removePrimaryKey(field);
|
|
410
672
|
this._addMongoIndexField("unique", "__pk", field);
|
|
411
673
|
this._table.addUniqueField(field);
|
|
412
674
|
}
|
|
@@ -431,7 +693,28 @@ var MongoAdapter = class extends BaseDbAdapter {
|
|
|
431
693
|
for (const index of metadata.get("db.mongo.search.filter") || []) this._vectorFilters.set(mongoIndexKey("vector", index.indexName), field);
|
|
432
694
|
}
|
|
433
695
|
onAfterFlatten() {
|
|
434
|
-
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
|
+
}
|
|
435
718
|
for (const [key, value] of this._vectorFilters.entries()) {
|
|
436
719
|
const index = this._mongoIndexes.get(key);
|
|
437
720
|
if (index && index.type === "vector") index.definition.fields?.push({
|
|
@@ -489,7 +772,7 @@ var MongoAdapter = class extends BaseDbAdapter {
|
|
|
489
772
|
/**
|
|
490
773
|
* Builds a MongoDB `$search` pipeline stage.
|
|
491
774
|
* Override `buildVectorSearchStage` in subclasses to provide embeddings.
|
|
492
|
-
*/ buildSearchStage(text, indexName) {
|
|
775
|
+
*/ async buildSearchStage(text, indexName) {
|
|
493
776
|
const index = this.getMongoSearchIndex(indexName);
|
|
494
777
|
if (!index) return undefined;
|
|
495
778
|
if (index.type === "vector") return this.buildVectorSearchStage(text, index);
|
|
@@ -504,11 +787,11 @@ var MongoAdapter = class extends BaseDbAdapter {
|
|
|
504
787
|
/**
|
|
505
788
|
* Builds a vector search stage. Override in subclasses to generate embeddings.
|
|
506
789
|
* Returns `undefined` by default (vector search requires custom implementation).
|
|
507
|
-
*/ buildVectorSearchStage(text, index) {
|
|
790
|
+
*/ async buildVectorSearchStage(text, index) {
|
|
508
791
|
return undefined;
|
|
509
792
|
}
|
|
510
793
|
async search(text, query, indexName) {
|
|
511
|
-
const searchStage = this.buildSearchStage(text, indexName);
|
|
794
|
+
const searchStage = await this.buildSearchStage(text, indexName);
|
|
512
795
|
if (!searchStage) throw new Error(indexName ? `Search index "${indexName}" not found` : "No search index available");
|
|
513
796
|
const filter = buildMongoFilter(query.filter);
|
|
514
797
|
const controls = query.controls || {};
|
|
@@ -518,10 +801,11 @@ var MongoAdapter = class extends BaseDbAdapter {
|
|
|
518
801
|
if (controls.$limit) pipeline.push({ $limit: controls.$limit });
|
|
519
802
|
else pipeline.push({ $limit: 1e3 });
|
|
520
803
|
if (controls.$select) pipeline.push({ $project: controls.$select.asProjection });
|
|
521
|
-
|
|
804
|
+
this._log("aggregate (search)", pipeline);
|
|
805
|
+
return this.collection.aggregate(pipeline, this._getSessionOpts()).toArray();
|
|
522
806
|
}
|
|
523
807
|
async searchWithCount(text, query, indexName) {
|
|
524
|
-
const searchStage = this.buildSearchStage(text, indexName);
|
|
808
|
+
const searchStage = await this.buildSearchStage(text, indexName);
|
|
525
809
|
if (!searchStage) throw new Error(indexName ? `Search index "${indexName}" not found` : "No search index available");
|
|
526
810
|
const filter = buildMongoFilter(query.filter);
|
|
527
811
|
const controls = query.controls || {};
|
|
@@ -538,7 +822,8 @@ else pipeline.push({ $limit: 1e3 });
|
|
|
538
822
|
meta: [{ $count: "count" }]
|
|
539
823
|
} }
|
|
540
824
|
];
|
|
541
|
-
|
|
825
|
+
this._log("aggregate (searchWithCount)", pipeline);
|
|
826
|
+
const result = await this.collection.aggregate(pipeline, this._getSessionOpts()).toArray();
|
|
542
827
|
return {
|
|
543
828
|
data: result[0]?.data || [],
|
|
544
829
|
count: result[0]?.meta[0]?.count || 0
|
|
@@ -556,71 +841,101 @@ else pipeline.push({ $limit: 1e3 });
|
|
|
556
841
|
].filter(Boolean),
|
|
557
842
|
meta: [{ $count: "count" }]
|
|
558
843
|
} }];
|
|
559
|
-
|
|
844
|
+
this._log("aggregate (findManyWithCount)", pipeline);
|
|
845
|
+
const result = await this.collection.aggregate(pipeline, this._getSessionOpts()).toArray();
|
|
560
846
|
return {
|
|
561
847
|
data: result[0]?.data || [],
|
|
562
848
|
count: result[0]?.meta[0]?.count || 0
|
|
563
849
|
};
|
|
564
850
|
}
|
|
565
851
|
async collectionExists() {
|
|
566
|
-
if (this.asMongo) return this.asMongo.collectionExists(this._table.tableName);
|
|
567
852
|
const cols = await this.db.listCollections({ name: this._table.tableName }).toArray();
|
|
568
853
|
return cols.length > 0;
|
|
569
854
|
}
|
|
570
855
|
async ensureCollectionExists() {
|
|
571
856
|
const exists = await this.collectionExists();
|
|
572
|
-
if (!exists)
|
|
857
|
+
if (!exists) {
|
|
858
|
+
this._log("createCollection", this._table.tableName);
|
|
859
|
+
const opts = { comment: "Created by Atscript Mongo Adapter" };
|
|
860
|
+
if (this._cappedOptions) {
|
|
861
|
+
opts.capped = true;
|
|
862
|
+
opts.size = this._cappedOptions.size;
|
|
863
|
+
if (this._cappedOptions.max !== null && this._cappedOptions.max !== undefined) opts.max = this._cappedOptions.max;
|
|
864
|
+
}
|
|
865
|
+
await this.db.createCollection(this._table.tableName, opts);
|
|
866
|
+
}
|
|
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
|
+
}
|
|
573
884
|
}
|
|
574
885
|
async insertOne(data) {
|
|
575
886
|
if (this._incrementFields.size > 0) {
|
|
576
887
|
const fields = this._fieldsNeedingIncrement(data);
|
|
577
888
|
if (fields.length > 0) {
|
|
578
|
-
const
|
|
579
|
-
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;
|
|
580
891
|
}
|
|
581
892
|
}
|
|
582
|
-
|
|
583
|
-
|
|
893
|
+
this._log("insertOne", data);
|
|
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 };
|
|
584
897
|
}
|
|
585
898
|
async insertMany(data) {
|
|
586
899
|
if (this._incrementFields.size > 0) {
|
|
587
900
|
const allFields = new Set();
|
|
588
901
|
for (const item of data) for (const f of this._fieldsNeedingIncrement(item)) allFields.add(f);
|
|
589
|
-
if (allFields.size > 0)
|
|
590
|
-
const maxValues = await this._getMaxValues([...allFields]);
|
|
591
|
-
for (const item of data) for (const physical of allFields) if (item[physical] === undefined || item[physical] === null) {
|
|
592
|
-
const next = (maxValues.get(physical) ?? 0) + 1;
|
|
593
|
-
item[physical] = next;
|
|
594
|
-
maxValues.set(physical, next);
|
|
595
|
-
} else if (typeof item[physical] === "number") {
|
|
596
|
-
const current = maxValues.get(physical) ?? 0;
|
|
597
|
-
if (item[physical] > current) maxValues.set(physical, item[physical]);
|
|
598
|
-
}
|
|
599
|
-
}
|
|
902
|
+
if (allFields.size > 0) await this._assignBatchIncrements(data, allFields);
|
|
600
903
|
}
|
|
601
|
-
|
|
904
|
+
this._log("insertMany", `${data.length} docs`);
|
|
905
|
+
const result = await this._wrapDuplicateKeyError(() => this.collection.insertMany(data, this._getSessionOpts()));
|
|
906
|
+
const metaIdPhysical = this._getMetaIdPhysical();
|
|
602
907
|
return {
|
|
603
908
|
insertedCount: result.insertedCount,
|
|
604
|
-
insertedIds: Object.values(result.insertedIds)
|
|
909
|
+
insertedIds: metaIdPhysical ? data.map((item, i) => item[metaIdPhysical] ?? result.insertedIds[i]) : Object.values(result.insertedIds)
|
|
605
910
|
};
|
|
606
911
|
}
|
|
607
912
|
async findOne(query) {
|
|
608
913
|
const filter = buildMongoFilter(query.filter);
|
|
609
914
|
const opts = this._buildFindOptions(query.controls);
|
|
610
|
-
|
|
915
|
+
this._log("findOne", filter, opts);
|
|
916
|
+
return this.collection.findOne(filter, {
|
|
917
|
+
...opts,
|
|
918
|
+
...this._getSessionOpts()
|
|
919
|
+
});
|
|
611
920
|
}
|
|
612
921
|
async findMany(query) {
|
|
613
922
|
const filter = buildMongoFilter(query.filter);
|
|
614
923
|
const opts = this._buildFindOptions(query.controls);
|
|
615
|
-
|
|
924
|
+
this._log("findMany", filter, opts);
|
|
925
|
+
return this.collection.find(filter, {
|
|
926
|
+
...opts,
|
|
927
|
+
...this._getSessionOpts()
|
|
928
|
+
}).toArray();
|
|
616
929
|
}
|
|
617
930
|
async count(query) {
|
|
618
931
|
const filter = buildMongoFilter(query.filter);
|
|
619
|
-
|
|
932
|
+
this._log("countDocuments", filter);
|
|
933
|
+
return this.collection.countDocuments(filter, this._getSessionOpts());
|
|
620
934
|
}
|
|
621
935
|
async updateOne(filter, data) {
|
|
622
936
|
const mongoFilter = buildMongoFilter(filter);
|
|
623
|
-
|
|
937
|
+
this._log("updateOne", mongoFilter, { $set: data });
|
|
938
|
+
const result = await this.collection.updateOne(mongoFilter, { $set: data }, this._getSessionOpts());
|
|
624
939
|
return {
|
|
625
940
|
matchedCount: result.matchedCount,
|
|
626
941
|
modifiedCount: result.modifiedCount
|
|
@@ -628,7 +943,8 @@ else pipeline.push({ $limit: 1e3 });
|
|
|
628
943
|
}
|
|
629
944
|
async replaceOne(filter, data) {
|
|
630
945
|
const mongoFilter = buildMongoFilter(filter);
|
|
631
|
-
|
|
946
|
+
this._log("replaceOne", mongoFilter, data);
|
|
947
|
+
const result = await this._wrapDuplicateKeyError(() => this.collection.replaceOne(mongoFilter, data, this._getSessionOpts()));
|
|
632
948
|
return {
|
|
633
949
|
matchedCount: result.matchedCount,
|
|
634
950
|
modifiedCount: result.modifiedCount
|
|
@@ -636,12 +952,14 @@ else pipeline.push({ $limit: 1e3 });
|
|
|
636
952
|
}
|
|
637
953
|
async deleteOne(filter) {
|
|
638
954
|
const mongoFilter = buildMongoFilter(filter);
|
|
639
|
-
|
|
955
|
+
this._log("deleteOne", mongoFilter);
|
|
956
|
+
const result = await this.collection.deleteOne(mongoFilter, this._getSessionOpts());
|
|
640
957
|
return { deletedCount: result.deletedCount };
|
|
641
958
|
}
|
|
642
959
|
async updateMany(filter, data) {
|
|
643
960
|
const mongoFilter = buildMongoFilter(filter);
|
|
644
|
-
|
|
961
|
+
this._log("updateMany", mongoFilter, { $set: data });
|
|
962
|
+
const result = await this.collection.updateMany(mongoFilter, { $set: data }, this._getSessionOpts());
|
|
645
963
|
return {
|
|
646
964
|
matchedCount: result.matchedCount,
|
|
647
965
|
modifiedCount: result.modifiedCount
|
|
@@ -649,7 +967,8 @@ else pipeline.push({ $limit: 1e3 });
|
|
|
649
967
|
}
|
|
650
968
|
async replaceMany(filter, data) {
|
|
651
969
|
const mongoFilter = buildMongoFilter(filter);
|
|
652
|
-
|
|
970
|
+
this._log("replaceMany", mongoFilter, { $set: data });
|
|
971
|
+
const result = await this.collection.updateMany(mongoFilter, { $set: data }, this._getSessionOpts());
|
|
653
972
|
return {
|
|
654
973
|
matchedCount: result.matchedCount,
|
|
655
974
|
modifiedCount: result.modifiedCount
|
|
@@ -657,12 +976,184 @@ else pipeline.push({ $limit: 1e3 });
|
|
|
657
976
|
}
|
|
658
977
|
async deleteMany(filter) {
|
|
659
978
|
const mongoFilter = buildMongoFilter(filter);
|
|
660
|
-
|
|
979
|
+
this._log("deleteMany", mongoFilter);
|
|
980
|
+
const result = await this.collection.deleteMany(mongoFilter, this._getSessionOpts());
|
|
661
981
|
return { deletedCount: result.deletedCount };
|
|
662
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
|
+
}
|
|
663
996
|
async ensureTable() {
|
|
997
|
+
if (this._table instanceof AtscriptDbView && !this._table.isExternal) return this._ensureView(this._table);
|
|
664
998
|
return this.ensureCollectionExists();
|
|
665
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
|
+
}
|
|
1077
|
+
async dropTable() {
|
|
1078
|
+
this._log("drop", this._table.tableName);
|
|
1079
|
+
await this.collection.drop();
|
|
1080
|
+
this._collection = undefined;
|
|
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
|
+
}
|
|
666
1157
|
async syncIndexes() {
|
|
667
1158
|
await this.ensureCollectionExists();
|
|
668
1159
|
const allIndexes = new Map();
|
|
@@ -706,21 +1197,29 @@ else pipeline.push({ $limit: 1e3 });
|
|
|
706
1197
|
case "unique":
|
|
707
1198
|
case "text": {
|
|
708
1199
|
if ((local.type === "text" || objMatch(local.fields, remote.key)) && objMatch(local.weights || {}, remote.weights || {})) indexesToCreate.delete(remote.name);
|
|
709
|
-
else
|
|
1200
|
+
else {
|
|
1201
|
+
this._log("dropIndex", remote.name);
|
|
1202
|
+
await this.collection.dropIndex(remote.name);
|
|
1203
|
+
}
|
|
710
1204
|
break;
|
|
711
1205
|
}
|
|
712
1206
|
default:
|
|
713
1207
|
}
|
|
714
|
-
} else
|
|
1208
|
+
} else {
|
|
1209
|
+
this._log("dropIndex", remote.name);
|
|
1210
|
+
await this.collection.dropIndex(remote.name);
|
|
1211
|
+
}
|
|
715
1212
|
}
|
|
716
1213
|
for (const [key, value] of allIndexes.entries()) switch (value.type) {
|
|
717
1214
|
case "plain": {
|
|
718
1215
|
if (!indexesToCreate.has(key)) continue;
|
|
1216
|
+
this._log("createIndex", key, value.fields);
|
|
719
1217
|
await this.collection.createIndex(value.fields, { name: key });
|
|
720
1218
|
break;
|
|
721
1219
|
}
|
|
722
1220
|
case "unique": {
|
|
723
1221
|
if (!indexesToCreate.has(key)) continue;
|
|
1222
|
+
this._log("createIndex (unique)", key, value.fields);
|
|
724
1223
|
await this.collection.createIndex(value.fields, {
|
|
725
1224
|
name: key,
|
|
726
1225
|
unique: true
|
|
@@ -729,6 +1228,7 @@ else await this.collection.dropIndex(remote.name);
|
|
|
729
1228
|
}
|
|
730
1229
|
case "text": {
|
|
731
1230
|
if (!indexesToCreate.has(key)) continue;
|
|
1231
|
+
this._log("createIndex (text)", key, value.fields);
|
|
732
1232
|
await this.collection.createIndex(value.fields, {
|
|
733
1233
|
weights: value.weights,
|
|
734
1234
|
name: key
|
|
@@ -760,43 +1260,110 @@ else toUpdate.add(remote.name);
|
|
|
760
1260
|
}
|
|
761
1261
|
default:
|
|
762
1262
|
}
|
|
763
|
-
} else if (remote.status !== "DELETING")
|
|
1263
|
+
} else if (remote.status !== "DELETING") {
|
|
1264
|
+
this._log("dropSearchIndex", remote.name);
|
|
1265
|
+
await this.collection.dropSearchIndex(remote.name);
|
|
1266
|
+
}
|
|
764
1267
|
}
|
|
765
1268
|
for (const [key, value] of indexesToCreate.entries()) switch (value.type) {
|
|
766
1269
|
case "dynamic_text":
|
|
767
1270
|
case "search_text":
|
|
768
1271
|
case "vector": {
|
|
769
|
-
if (toUpdate.has(key))
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
1272
|
+
if (toUpdate.has(key)) {
|
|
1273
|
+
this._log("updateSearchIndex", key, value.definition);
|
|
1274
|
+
await this.collection.updateSearchIndex(key, value.definition);
|
|
1275
|
+
} else {
|
|
1276
|
+
this._log("createSearchIndex", key, value.type);
|
|
1277
|
+
await this.collection.createSearchIndex({
|
|
1278
|
+
name: key,
|
|
1279
|
+
type: value.type === "vector" ? "vectorSearch" : "search",
|
|
1280
|
+
definition: value.definition
|
|
1281
|
+
});
|
|
1282
|
+
}
|
|
775
1283
|
break;
|
|
776
1284
|
}
|
|
777
1285
|
default:
|
|
778
1286
|
}
|
|
779
1287
|
} catch {}
|
|
780
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
|
+
}
|
|
781
1305
|
/** Returns physical field names of increment fields that are undefined in the data. */ _fieldsNeedingIncrement(data) {
|
|
782
1306
|
const result = [];
|
|
783
1307
|
for (const physical of this._incrementFields) if (data[physical] === undefined || data[physical] === null) result.push(physical);
|
|
784
1308
|
return result;
|
|
785
1309
|
}
|
|
786
|
-
/**
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
const
|
|
791
|
-
const
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
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
|
+
}
|
|
797
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);
|
|
798
1366
|
}
|
|
799
|
-
return maxMap;
|
|
800
1367
|
}
|
|
801
1368
|
_buildFindOptions(controls) {
|
|
802
1369
|
const opts = {};
|
|
@@ -848,10 +1415,11 @@ else {
|
|
|
848
1415
|
if (analyzer) index.definition.mappings.fields[fieldName].analyzer = analyzer;
|
|
849
1416
|
}
|
|
850
1417
|
}
|
|
851
|
-
constructor(db,
|
|
852
|
-
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;
|
|
853
1420
|
}
|
|
854
1421
|
};
|
|
1422
|
+
_define_property(MongoAdapter, "_noSession", Object.freeze({}));
|
|
855
1423
|
function objMatch(o1, o2) {
|
|
856
1424
|
const keys1 = Object.keys(o1);
|
|
857
1425
|
const keys2 = Object.keys(o2);
|
|
@@ -881,51 +1449,12 @@ function vectorFieldsMatch(left, right) {
|
|
|
881
1449
|
}
|
|
882
1450
|
|
|
883
1451
|
//#endregion
|
|
884
|
-
//#region packages/mongo/src/lib/
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
info: () => {},
|
|
890
|
-
debug: () => {}
|
|
891
|
-
};
|
|
892
|
-
|
|
893
|
-
//#endregion
|
|
894
|
-
//#region packages/mongo/src/lib/as-mongo.ts
|
|
895
|
-
function _define_property(obj, key, value) {
|
|
896
|
-
if (key in obj) Object.defineProperty(obj, key, {
|
|
897
|
-
value,
|
|
898
|
-
enumerable: true,
|
|
899
|
-
configurable: true,
|
|
900
|
-
writable: true
|
|
901
|
-
});
|
|
902
|
-
else obj[key] = value;
|
|
903
|
-
return obj;
|
|
1452
|
+
//#region packages/mongo/src/lib/index.ts
|
|
1453
|
+
function createAdapter(connection, _options) {
|
|
1454
|
+
const client = new MongoClient(connection);
|
|
1455
|
+
const db = client.db();
|
|
1456
|
+
return new DbSpace(() => new MongoAdapter(db, client));
|
|
904
1457
|
}
|
|
905
|
-
var AsMongo = class extends DbSpace {
|
|
906
|
-
get db() {
|
|
907
|
-
return this.client.db();
|
|
908
|
-
}
|
|
909
|
-
getCollectionsList() {
|
|
910
|
-
if (!this.collectionsList) this.collectionsList = this.db.listCollections().toArray().then((c) => new Set(c.map((c$1) => c$1.name)));
|
|
911
|
-
return this.collectionsList;
|
|
912
|
-
}
|
|
913
|
-
async collectionExists(name) {
|
|
914
|
-
const list = await this.getCollectionsList();
|
|
915
|
-
return list.has(name);
|
|
916
|
-
}
|
|
917
|
-
/**
|
|
918
|
-
* Returns the MongoAdapter for the given type.
|
|
919
|
-
* Convenience accessor for Mongo-specific adapter operations.
|
|
920
|
-
*/ getAdapter(type) {
|
|
921
|
-
return super.getAdapter(type);
|
|
922
|
-
}
|
|
923
|
-
constructor(client, logger = NoopLogger) {
|
|
924
|
-
const resolvedClient = typeof client === "string" ? new MongoClient(client) : client;
|
|
925
|
-
super(() => new MongoAdapter(this.db, this), logger), _define_property(this, "client", void 0), _define_property(this, "collectionsList", void 0);
|
|
926
|
-
this.client = resolvedClient;
|
|
927
|
-
}
|
|
928
|
-
};
|
|
929
1458
|
|
|
930
1459
|
//#endregion
|
|
931
|
-
export {
|
|
1460
|
+
export { CollectionPatcher, MongoAdapter, buildMongoFilter, createAdapter, validateMongoIdPlugin };
|