@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.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$2(obj, key, value) {
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 CollectionPatcher {
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.mongo.__topLevelArray");
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, _flatType) {
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
- } }, ["$$cand"]] }
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$2(this, "collection", void 0);
254
- _define_property$2(this, "payload", void 0);
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$2(this, "filterObj", void 0);
259
- /** MongoDB *update* document being built. */ _define_property$2(this, "updatePipeline", void 0);
260
- /** Current `$set` stage being populated. */ _define_property$2(this, "currentSetStage", void 0);
261
- /** Additional *options* (mainly `arrayFilters`). */ _define_property$2(this, "optionsObj", void 0);
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$2(CollectionPatcher, "getKeyProps", getKeyProps);
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$1(obj, key, value) {
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.asMongo?.client;
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
- buildInsertValidator(table) {
413
- return table.createValidator({
414
- plugins: this.getValidatorPlugins(),
415
- replace: (type, path) => {
416
- if (path === "_id" && type.type.tags?.has("objectId")) return {
417
- ...type,
418
- optional: true
419
- };
420
- if (table.defaults.has(path)) return {
421
- ...type,
422
- optional: true
423
- };
424
- return type;
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
- buildPatchValidator(table) {
429
- return CollectionPatcher.prepareValidator(this.getPatcherContext());
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._table.addPrimaryKey("_id");
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 maxValues = await this._getMaxValues(fields);
655
- for (const physical of fields) data[physical] = (maxValues.get(physical) ?? 0) + 1;
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
- return { insertedId: result.insertedId };
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
- /** Reads current max value for each field via $group aggregation. */ async _getMaxValues(physicalFields) {
902
- const aliases = physicalFields.map((f) => [`max__${f.replace(/\./g, "__")}`, f]);
903
- const group = { _id: null };
904
- for (const [alias, field] of aliases) group[alias] = { $max: `$${field}` };
905
- const result = await this.collection.aggregate([{ $group: group }], this._getSessionOpts()).toArray();
906
- const maxMap = new Map();
907
- if (result.length > 0) {
908
- const row = result[0];
909
- for (const [alias, field] of aliases) {
910
- const val = row[alias];
911
- maxMap.set(field, typeof val === "number" ? val : 0);
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, asMongo) {
967
- super(), _define_property$1(this, "db", void 0), _define_property$1(this, "asMongo", void 0), _define_property$1(this, "_collection", void 0), _define_property$1(this, "_mongoIndexes", void 0), _define_property$1(this, "_vectorFilters", void 0), _define_property$1(this, "_searchIndexesMap", void 0), _define_property$1(this, "_incrementFields", void 0), _define_property$1(this, "_cappedOptions", void 0), _define_property$1(this, "_txDisabled", void 0), this.db = db, this.asMongo = asMongo, this._mongoIndexes = new Map(), this._vectorFilters = new Map(), this._incrementFields = new Set(), this._txDisabled = false;
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$1(MongoAdapter, "_noSession", Object.freeze({}));
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
- return new AsMongo(connection);
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 { AsMongo, CollectionPatcher, MongoAdapter, buildMongoFilter, createAdapter, validateMongoIdPlugin };
1460
+ export { CollectionPatcher, MongoAdapter, buildMongoFilter, createAdapter, validateMongoIdPlugin };