@ignex/ninox 0.1.7 → 0.2.0

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.js CHANGED
@@ -1,7 +1,9 @@
1
- import { ObjectId, Decimal128, Long, Int32, Double, Timestamp, Binary, MinKey, MaxKey, DBRef, Code, MongoClient } from 'mongodb';
1
+ import { BSON, ObjectId, Decimal128, Long, Int32, Double, Timestamp, Binary, MinKey, MaxKey, DBRef, Code, MongoClient } from 'mongodb';
2
+ import { createHash, randomUUID } from 'crypto';
2
3
  import { writeFile, mkdir, readdir } from 'fs/promises';
3
4
  import { basename, join } from 'path';
4
5
  import { pathToFileURL } from 'url';
6
+ import { isIP } from 'net';
5
7
 
6
8
  // src/utils/timeout.ts
7
9
  var withTimeout = (promise, timeoutMs, label) => {
@@ -228,7 +230,7 @@ var isMongoDuplicateKeyError = (error) => {
228
230
  };
229
231
  var isTransactionUnsupportedError = (error) => {
230
232
  const code = error?.code;
231
- if (code === 20) return true;
233
+ if (code === 20 || code === "TRANSACTIONS_UNSUPPORTED") return true;
232
234
  const message = error instanceof Error ? error.message : "";
233
235
  return /Transaction numbers are only allowed/i.test(message) || /transactions are not supported/i.test(message) || /does not support retryable writes/i.test(message);
234
236
  };
@@ -313,6 +315,12 @@ var mapMongoDriverError = (error, ctx = {}) => {
313
315
  }
314
316
  const code = error.code;
315
317
  switch (code) {
318
+ case 20:
319
+ return new InfraError(
320
+ "TRANSACTIONS_UNSUPPORTED",
321
+ "MongoDB deployment does not support transactions; enable allowNonTransactionalFallback to run non-atomically",
322
+ context
323
+ );
316
324
  case 112:
317
325
  return new DomainError("VERSION_CONFLICT", "Document version conflict", context);
318
326
  case 50:
@@ -406,46 +414,81 @@ var cloneDeep = (value) => {
406
414
  }
407
415
  return value;
408
416
  };
409
-
410
- // src/utils/hash.ts
411
- var isPlainObject = (v) => typeof v === "object" && v !== null && !Array.isArray(v) && !(v instanceof Date) && !(v instanceof RegExp) && // Mongo driver ObjectId (and other BSON values with a toHexString marker)
412
- // must fall through to `toPrimitive` — treating them as plain objects
413
- // would serialize every ObjectId as `{}` and collapse distinct values.
414
- typeof v.toHexString !== "function";
415
- var toPrimitive = (v) => {
416
- if (v === void 0 || v === null) return null;
417
- if (typeof v === "bigint") return v.toString();
418
- if (v instanceof Date) return `date:${v.getTime()}`;
419
- if (typeof v === "object") {
420
- const anyV = v;
421
- if (typeof anyV.toHexString === "function") return `oid:${anyV.toHexString()}`;
422
- if (anyV instanceof RegExp) return `re:${anyV.source}/${anyV.flags}`;
423
- }
424
- return v;
417
+ var { EJSON } = BSON;
418
+ var bsonTypeOf = (value) => {
419
+ const type = value._bsontype;
420
+ return typeof type === "string" ? type : void 0;
421
+ };
422
+ var isPlainObject = (value) => {
423
+ if (value === null || typeof value !== "object") return false;
424
+ const prototype = Object.getPrototypeOf(value);
425
+ return (prototype === Object.prototype || prototype === null) && bsonTypeOf(value) === void 0;
425
426
  };
426
427
  var stableStringify = (value) => {
427
- const visit = (v) => {
428
- if (Array.isArray(v)) return v.map(visit);
429
- if (isPlainObject(v)) {
430
- const out = {};
431
- for (const key of Object.keys(v).sort()) out[key] = visit(v[key]);
432
- return out;
428
+ const ancestors = /* @__PURE__ */ new WeakSet();
429
+ const visit = (input) => {
430
+ if (input === null) return { t: "null" };
431
+ if (input === void 0) return { t: "undefined" };
432
+ switch (typeof input) {
433
+ case "string":
434
+ case "boolean":
435
+ return { t: typeof input, v: input };
436
+ case "number":
437
+ if (Number.isNaN(input)) return { t: "number", v: "NaN" };
438
+ if (input === Infinity) return { t: "number", v: "Infinity" };
439
+ if (input === -Infinity) return { t: "number", v: "-Infinity" };
440
+ if (Object.is(input, -0)) return { t: "number", v: "-0" };
441
+ return { t: "number", v: input };
442
+ case "bigint":
443
+ return { t: "bigint", v: input.toString() };
444
+ case "symbol":
445
+ case "function":
446
+ throw new TypeError(`stableStringify: unsupported ${typeof input} value`);
447
+ }
448
+ const object = input;
449
+ if (ancestors.has(object)) throw new TypeError("stableStringify: cyclic value");
450
+ ancestors.add(object);
451
+ try {
452
+ if (input instanceof Date) {
453
+ return { t: "date", v: Number.isNaN(input.getTime()) ? "Invalid" : input.toISOString() };
454
+ }
455
+ if (input instanceof RegExp) return { t: "regexp", v: `re:${input.source}/${input.flags}` };
456
+ const bsonType = bsonTypeOf(object);
457
+ if (bsonType !== void 0) {
458
+ return { t: "bson", kind: bsonType, v: EJSON.stringify(input, { relaxed: false }) };
459
+ }
460
+ if (Array.isArray(input)) return { t: "array", v: input.map(visit) };
461
+ if (input instanceof Map) {
462
+ return {
463
+ t: "map",
464
+ v: [...input.entries()].map(([key, entry]) => [visit(key), visit(entry)])
465
+ };
466
+ }
467
+ if (input instanceof Set) return { t: "set", v: [...input].map(visit) };
468
+ if (Buffer.isBuffer(input)) return { t: "buffer", v: input.toString("base64") };
469
+ if (ArrayBuffer.isView(input)) {
470
+ return {
471
+ t: "bytes",
472
+ kind: input.constructor.name,
473
+ v: Buffer.from(input.buffer, input.byteOffset, input.byteLength).toString("base64")
474
+ };
475
+ }
476
+ if (input instanceof ArrayBuffer) {
477
+ return { t: "arraybuffer", v: Buffer.from(input).toString("base64") };
478
+ }
479
+ if (isPlainObject(input)) {
480
+ const out = {};
481
+ for (const key of Object.keys(input).sort()) out[key] = visit(input[key]);
482
+ return { t: "object", v: out };
483
+ }
484
+ throw new TypeError(`stableStringify: unsupported object ${input.constructor.name}`);
485
+ } finally {
486
+ ancestors.delete(object);
433
487
  }
434
- return toPrimitive(v);
435
488
  };
436
489
  return JSON.stringify(visit(value));
437
490
  };
438
- var hashString = (input) => {
439
- let h1 = 5381;
440
- let h2 = 2166136261;
441
- for (let i = 0; i < input.length; i++) {
442
- const c = input.charCodeAt(i);
443
- h1 = (h1 << 5) + h1 + c | 0;
444
- h2 = (h2 ^ c) * 16777619 | 0;
445
- h2 = h2 << 3 | h2 >>> 29;
446
- }
447
- return `${(h1 >>> 0).toString(36)}${(h2 >>> 0).toString(36)}`;
448
- };
491
+ var hashString = (input) => createHash("sha256").update(input, "utf8").digest("base64url");
449
492
  var stableHash = (value) => hashString(stableStringify(value));
450
493
 
451
494
  // src/utils/lru.ts
@@ -590,7 +633,6 @@ var RefreshTicker = class {
590
633
  this.host = host;
591
634
  this.intervalMs = tickIntervalMs;
592
635
  }
593
- host;
594
636
  timer;
595
637
  intervalMs;
596
638
  /** Whether the timer is currently running. */
@@ -725,7 +767,6 @@ var WatchCoordinator = class {
725
767
  constructor(host) {
726
768
  this.host = host;
727
769
  }
728
- host;
729
770
  streams = /* @__PURE__ */ new Map();
730
771
  failedStreams = /* @__PURE__ */ new Set();
731
772
  stopped = true;
@@ -1274,7 +1315,7 @@ var createHotCache = (options) => new HotCache(options);
1274
1315
  // src/cache/query-cache.ts
1275
1316
  var SEP = "\0";
1276
1317
  var NS_SEP = "";
1277
- var cacheCollectionKey = (dbName, physical) => `${dbName}${NS_SEP}${physical}`;
1318
+ var cacheCollectionKey = (dbName, physical, clusterNamespace = "") => `${clusterNamespace}${NS_SEP}${dbName}${NS_SEP}${physical}`;
1278
1319
  var QueryCache = class {
1279
1320
  lru;
1280
1321
  index = /* @__PURE__ */ new Map();
@@ -1299,8 +1340,8 @@ var QueryCache = class {
1299
1340
  max: options.maxSize ?? 500,
1300
1341
  onEvict: (key) => this.unregisterKey(key)
1301
1342
  });
1302
- this.ttlMs = options.ttlMs ?? 0;
1303
- this.clone = options.clone ?? false;
1343
+ this.ttlMs = options.ttlMs ?? 3e4;
1344
+ this.clone = options.clone ?? true;
1304
1345
  }
1305
1346
  get size() {
1306
1347
  return this.lru.size;
@@ -1375,6 +1416,7 @@ var QueryCache = class {
1375
1416
  this.sets++;
1376
1417
  const ttl = ttlMs ?? this.ttlMs;
1377
1418
  const expiresAt = ttl > 0 ? Date.now() + ttl : Infinity;
1419
+ this.unregisterKey(key);
1378
1420
  this.lru.set(key, {
1379
1421
  value,
1380
1422
  expiresAt,
@@ -1433,14 +1475,24 @@ var withGracefulMongoTransaction = async (runner, fn, options = {}) => {
1433
1475
  }
1434
1476
  }
1435
1477
  }
1436
- if (!enabled) return fn(null);
1478
+ const unsupported = () => new InfraError(
1479
+ "TRANSACTIONS_UNSUPPORTED",
1480
+ "MongoDB deployment does not support transactions; set allowNonTransactionalFallback to run non-atomically"
1481
+ );
1482
+ if (!enabled) {
1483
+ if (options.allowNonTransactionalFallback === true) return fn(null);
1484
+ throw unsupported();
1485
+ }
1437
1486
  try {
1438
- return await runner.transaction(fn);
1487
+ return await runner.transaction(fn, {
1488
+ ...options.allowNonTransactionalFallback !== void 0 ? { allowNonTransactionalFallback: options.allowNonTransactionalFallback } : {}
1489
+ });
1439
1490
  } catch (err) {
1440
1491
  if (isTransactionUnsupportedError(err)) {
1492
+ if (options.allowNonTransactionalFallback !== true) throw err;
1441
1493
  options.logger?.warn?.(
1442
1494
  { error: err instanceof Error ? err.message : String(err) },
1443
- "transactions unsupported, falling back"
1495
+ "transactions unsupported; running explicitly requested non-transactional fallback"
1444
1496
  );
1445
1497
  return fn(null);
1446
1498
  }
@@ -1607,89 +1659,134 @@ var nextMigrationNumber = async (dir) => {
1607
1659
  return match ? Math.max(max, Number(match[1])) : max;
1608
1660
  }, 0) + 1;
1609
1661
  };
1610
-
1611
- // src/migrations/journal.ts
1612
1662
  var isDuplicateKeyError = (err) => {
1613
1663
  const code = err?.code;
1614
1664
  if (code === 11e3 || code === 11001) return true;
1615
1665
  return /E11000 duplicate key/.test(String(err?.message ?? ""));
1616
1666
  };
1667
+ var expiredLeaseFilter = (now, staleCutoff) => ({
1668
+ $or: [
1669
+ { expiresAt: { $lt: now } },
1670
+ { expiresAt: { $exists: false }, appliedAt: { $lt: staleCutoff } }
1671
+ ]
1672
+ });
1617
1673
  var createMigrationJournal = (manager) => {
1618
1674
  let indexReady = false;
1619
1675
  const ensureIndex = async () => {
1620
1676
  if (indexReady) return;
1621
- try {
1622
- await manager.createIndex?.(JOURNAL_COLLECTION, { name: 1 }, { unique: true });
1623
- } catch {
1677
+ if (manager.createIndex) {
1678
+ await manager.createIndex(JOURNAL_COLLECTION, { name: 1 }, { unique: true });
1624
1679
  }
1625
1680
  indexReady = true;
1626
1681
  };
1627
1682
  const appliedNames = async () => {
1628
- const rows = await manager.findMany(JOURNAL_COLLECTION);
1683
+ const rows = await manager.findMany(JOURNAL_COLLECTION, {}, { cache: false });
1629
1684
  return new Set(
1630
- rows.filter((row) => row.status === void 0 || row.status === "applied").map((row) => row.name)
1685
+ rows.filter(
1686
+ (row) => row.status === void 0 || row.status === "applied" || row.status === "rolling_back"
1687
+ ).map((row) => row.name)
1631
1688
  );
1632
1689
  };
1633
- const stealableFilter = (name, now, staleCutoff) => ({
1634
- name,
1635
- status: { $ne: "applied" },
1636
- $or: [
1637
- { expiresAt: { $lt: now } },
1638
- { expiresAt: { $exists: false }, appliedAt: { $lt: staleCutoff } },
1639
- { status: { $nin: ["running", "applied"] } }
1640
- ]
1641
- });
1642
1690
  const claim = async (name, file, leaseMs) => {
1643
1691
  await ensureIndex();
1644
1692
  const now = /* @__PURE__ */ new Date();
1645
- const expiresAt = new Date(Date.now() + leaseMs);
1693
+ const staleCutoff = new Date(Date.now() - leaseMs);
1694
+ const claimId = randomUUID();
1646
1695
  const stole = await manager.findOneAndUpdate(
1647
1696
  JOURNAL_COLLECTION,
1648
- stealableFilter(name, now, new Date(Date.now() - leaseMs)),
1649
- { $set: { status: "running", appliedAt: now, file, expiresAt } },
1650
- { returnDocument: "before" }
1697
+ {
1698
+ name,
1699
+ status: { $nin: ["applied", "rolling_back"] },
1700
+ ...expiredLeaseFilter(now, staleCutoff)
1701
+ },
1702
+ {
1703
+ $set: {
1704
+ status: "running",
1705
+ appliedAt: now,
1706
+ file,
1707
+ expiresAt: new Date(Date.now() + leaseMs),
1708
+ claimId
1709
+ }
1710
+ },
1711
+ { returnDocument: "before", cache: false }
1651
1712
  );
1652
- if (stole !== null) return true;
1713
+ if (stole !== null) return claimId;
1653
1714
  try {
1654
1715
  await manager.insertOne(JOURNAL_COLLECTION, {
1655
1716
  name,
1656
1717
  appliedAt: now,
1657
1718
  status: "running",
1658
1719
  file,
1659
- expiresAt
1720
+ expiresAt: new Date(Date.now() + leaseMs),
1721
+ claimId
1660
1722
  });
1661
- return true;
1723
+ return claimId;
1662
1724
  } catch (err) {
1663
- if (isDuplicateKeyError(err)) return false;
1725
+ if (isDuplicateKeyError(err)) return null;
1664
1726
  throw err;
1665
1727
  }
1666
1728
  };
1667
- const renew = async (name, leaseMs) => {
1729
+ const claimRollback = async (name, file, leaseMs) => {
1730
+ await ensureIndex();
1731
+ const now = /* @__PURE__ */ new Date();
1732
+ const claimId = randomUUID();
1733
+ const claimed = await manager.findOneAndUpdate(
1734
+ JOURNAL_COLLECTION,
1735
+ {
1736
+ name,
1737
+ $or: [
1738
+ { status: "applied" },
1739
+ { status: "rolling_back", ...expiredLeaseFilter(now, new Date(Date.now() - leaseMs)) }
1740
+ ]
1741
+ },
1742
+ {
1743
+ $set: {
1744
+ status: "rolling_back",
1745
+ file,
1746
+ claimId,
1747
+ expiresAt: new Date(Date.now() + leaseMs)
1748
+ }
1749
+ },
1750
+ { returnDocument: "before", cache: false }
1751
+ );
1752
+ return claimed === null ? null : claimId;
1753
+ };
1754
+ const renew = async (name, claimId, leaseMs) => {
1668
1755
  await manager.updateOne(
1669
1756
  JOURNAL_COLLECTION,
1670
- { name, status: "running" },
1671
- { $set: { expiresAt: new Date(Date.now() + leaseMs) } }
1757
+ { name, claimId, status: { $in: ["running", "rolling_back"] } },
1758
+ { $set: { expiresAt: new Date(Date.now() + leaseMs) } },
1759
+ { cache: false }
1672
1760
  );
1673
1761
  };
1674
- const markApplied = async (name) => {
1762
+ const markApplied = async (name, claimId) => {
1675
1763
  const res = await manager.updateOne(
1676
1764
  JOURNAL_COLLECTION,
1677
- { name, status: "running" },
1678
- { $set: { status: "applied" } }
1765
+ { name, claimId, status: "running" },
1766
+ { $set: { status: "applied" }, $unset: { expiresAt: "", claimId: "" } },
1767
+ { cache: false }
1679
1768
  );
1680
1769
  if (res?.matchedCount === 0) {
1681
1770
  await manager.insertOne(JOURNAL_COLLECTION, { name, appliedAt: /* @__PURE__ */ new Date(), status: "applied" }).catch(() => {
1682
1771
  });
1772
+ throw new Error(`Migration "${name}" lost its claim before it could be marked applied`);
1683
1773
  }
1684
1774
  };
1685
- const removeRunning = async (name) => {
1686
- await manager.deleteOne(JOURNAL_COLLECTION, { name, status: "running" }).catch(() => {
1775
+ const removeRunning = async (name, claimId) => {
1776
+ await manager.deleteOne(JOURNAL_COLLECTION, { name, claimId, status: "running" }, { cache: false }).catch(() => {
1687
1777
  });
1688
1778
  };
1689
- const unrecord = async (name) => {
1690
- await manager.deleteOne(JOURNAL_COLLECTION, { name });
1779
+ const unrecord = async (name, claimId) => {
1780
+ const result = await manager.deleteOne(
1781
+ JOURNAL_COLLECTION,
1782
+ { name, claimId, status: "rolling_back" },
1783
+ { cache: false }
1784
+ );
1785
+ if (result?.deletedCount === 0) {
1786
+ throw new Error(`Migration "${name}" lost its rollback claim`);
1787
+ }
1691
1788
  };
1692
- return { appliedNames, claim, renew, markApplied, removeRunning, unrecord };
1789
+ return { appliedNames, claim, claimRollback, renew, markApplied, removeRunning, unrecord };
1693
1790
  };
1694
1791
 
1695
1792
  // src/migrations/index.ts
@@ -1709,10 +1806,10 @@ var createMongoMigrationRunner = (service, options = {}) => {
1709
1806
  }
1710
1807
  return db;
1711
1808
  };
1712
- const startHeartbeat = (journal, name) => {
1809
+ const startHeartbeat = (journal, name, claimId) => {
1713
1810
  const timer = setInterval(
1714
1811
  () => {
1715
- void journal.renew(name, leaseMs).catch(() => {
1812
+ void journal.renew(name, claimId, leaseMs).catch(() => {
1716
1813
  });
1717
1814
  },
1718
1815
  Math.max(leaseMs / 2, 1e3)
@@ -1729,19 +1826,20 @@ var createMongoMigrationRunner = (service, options = {}) => {
1729
1826
  for (const file of pending) {
1730
1827
  const migration = await loadMigration(migrationDir, file);
1731
1828
  logger.info({ name: migration.name }, "running migration up");
1732
- if (!await journal.claim(migration.name, basename(file), leaseMs)) {
1829
+ const claimId = await journal.claim(migration.name, basename(file), leaseMs);
1830
+ if (claimId === null) {
1733
1831
  logger.info(
1734
1832
  { name: migration.name },
1735
1833
  "migration claimed by another runner or already applied, skipping"
1736
1834
  );
1737
1835
  continue;
1738
1836
  }
1739
- const stopHeartbeat = startHeartbeat(journal, migration.name);
1837
+ const stopHeartbeat = startHeartbeat(journal, migration.name, claimId);
1740
1838
  try {
1741
1839
  await migration.up({ service, logger, name: migration.name });
1742
- await journal.markApplied(migration.name);
1840
+ await journal.markApplied(migration.name, claimId);
1743
1841
  } catch (err) {
1744
- await journal.removeRunning(migration.name);
1842
+ await journal.removeRunning(migration.name, claimId);
1745
1843
  throw err;
1746
1844
  } finally {
1747
1845
  stopHeartbeat();
@@ -1764,9 +1862,19 @@ var createMongoMigrationRunner = (service, options = {}) => {
1764
1862
  const appliedFiles = files.filter((file) => applied.has(migrationNameOf(file))).reverse();
1765
1863
  for (const file of appliedFiles) {
1766
1864
  const migration = await loadMigration(migrationDir, file);
1865
+ const claimId = await journal.claimRollback(migration.name, basename(file), leaseMs);
1866
+ if (claimId === null) {
1867
+ logger.info({ name: migration.name }, "migration rollback already claimed, skipping");
1868
+ continue;
1869
+ }
1870
+ const stopHeartbeat = startHeartbeat(journal, migration.name, claimId);
1767
1871
  logger.info({ name: migration.name }, "running migration down");
1768
- await migration.down({ service, logger, name: migration.name });
1769
- await journal.unrecord(migration.name);
1872
+ try {
1873
+ await migration.down({ service, logger, name: migration.name });
1874
+ await journal.unrecord(migration.name, claimId);
1875
+ } finally {
1876
+ stopHeartbeat();
1877
+ }
1770
1878
  logger.info({ name: migration.name }, "migration rolled back");
1771
1879
  if (targetName !== void 0 && migration.name === targetName) break;
1772
1880
  }
@@ -1867,7 +1975,6 @@ var QueryBuilder = class _QueryBuilder {
1867
1975
  constructor(ctx) {
1868
1976
  this.ctx = ctx;
1869
1977
  }
1870
- ctx;
1871
1978
  _filter = {};
1872
1979
  _sort;
1873
1980
  _skip;
@@ -2308,6 +2415,16 @@ var toMongoValidator = (schema) => ({
2308
2415
  });
2309
2416
 
2310
2417
  // src/schema/types.ts
2418
+ var SUPPORTED_STRING_FORMATS = /* @__PURE__ */ new Set([
2419
+ "email",
2420
+ "uri",
2421
+ "date-time",
2422
+ "date",
2423
+ "uuid",
2424
+ "hostname",
2425
+ "ipv4",
2426
+ "ipv6"
2427
+ ]);
2311
2428
  var chain = (field) => {
2312
2429
  const obj = field;
2313
2430
  obj.optional = () => chain({ ...field, flags: { ...field.flags, optional: true } });
@@ -2321,6 +2438,11 @@ function objectField(properties, opts = {}) {
2321
2438
  var s = {
2322
2439
  /** UTF-8 string field (optional `format` / length / regex constraints). */
2323
2440
  string(opts = {}) {
2441
+ if (opts.format !== void 0 && !SUPPORTED_STRING_FORMATS.has(opts.format)) {
2442
+ throw new Error(
2443
+ `Unsupported string format "${opts.format}"; supported: ${[...SUPPORTED_STRING_FORMATS].join(", ")}`
2444
+ );
2445
+ }
2324
2446
  return chain({ kind: "string", ...BASE, ...opts });
2325
2447
  },
2326
2448
  /** 32-bit integer field with optional bounds (min/max, exclusive, multipleOf). */
@@ -2486,6 +2608,200 @@ var checkReservedValue = (key, value, path, issues) => {
2486
2608
  `Reserved field "${key}" must be ${types.join(" | ")}`
2487
2609
  );
2488
2610
  };
2611
+ var typeMatches = (value, type) => {
2612
+ if (type === "number") return typeof value === "number" || value instanceof BSON.Long;
2613
+ if (type === "int") {
2614
+ return value instanceof BSON.Int32 || typeof value === "number" && Number.isInteger(value);
2615
+ }
2616
+ if (type === "long") {
2617
+ return value instanceof BSON.Long || typeof value === "number" && Number.isInteger(value);
2618
+ }
2619
+ if (type === "double") return typeof value === "number" || value instanceof BSON.Double;
2620
+ if (type === "decimal") return value instanceof BSON.Decimal128;
2621
+ if (type === "object")
2622
+ return value !== null && typeof value === "object" && !Array.isArray(value);
2623
+ if (type === "array") return Array.isArray(value);
2624
+ if (type === "string") return typeof value === "string";
2625
+ if (type === "bool" || type === "boolean") return typeof value === "boolean";
2626
+ if (type === "date") return value instanceof Date;
2627
+ if (type === "objectId") return value instanceof BSON.ObjectId;
2628
+ if (type === "null") return value === null;
2629
+ if (type === "binData") return value instanceof BSON.Binary;
2630
+ if (type === "regex") return value instanceof RegExp;
2631
+ return true;
2632
+ };
2633
+ var comparable = (value) => {
2634
+ if (typeof value === "number") return value;
2635
+ if (value instanceof BSON.Long || value instanceof BSON.Int32 || value instanceof BSON.Double) {
2636
+ return Number(value.toString());
2637
+ }
2638
+ if (value instanceof BSON.Decimal128) return Number(value.toString());
2639
+ return void 0;
2640
+ };
2641
+ var validateRawSchema = (schema, value, path, issues) => {
2642
+ const types = schema.bsonType === void 0 ? [] : [schema.bsonType].flat();
2643
+ if (types.length > 0 && !types.some((type) => typeMatches(value, type))) {
2644
+ push(issues, path, "type", types.join(" | "), "BSON type does not match raw schema");
2645
+ return;
2646
+ }
2647
+ if (schema.enum && !schema.enum.some(
2648
+ (candidate) => BSON.EJSON.stringify(candidate) === BSON.EJSON.stringify(value)
2649
+ )) {
2650
+ push(issues, path, "enum", "raw enum", "Value is not in the allowed set");
2651
+ }
2652
+ if (typeof value === "string") {
2653
+ if (schema.minLength !== void 0 && value.length < schema.minLength) {
2654
+ push(issues, path, "constraint", `minLength ${schema.minLength}`, "String is too short");
2655
+ }
2656
+ if (schema.maxLength !== void 0 && value.length > schema.maxLength) {
2657
+ push(issues, path, "constraint", `maxLength ${schema.maxLength}`, "String is too long");
2658
+ }
2659
+ if (schema.pattern && !new RegExp(schema.pattern).test(value)) {
2660
+ push(
2661
+ issues,
2662
+ path,
2663
+ "constraint",
2664
+ `pattern ${schema.pattern}`,
2665
+ "String does not match pattern"
2666
+ );
2667
+ }
2668
+ }
2669
+ const numeric = comparable(value);
2670
+ if (numeric !== void 0) {
2671
+ const minimum = typeof schema.exclusiveMinimum === "number" ? schema.exclusiveMinimum : schema.minimum;
2672
+ const maximum = typeof schema.exclusiveMaximum === "number" ? schema.exclusiveMaximum : schema.maximum;
2673
+ if (minimum !== void 0 && (numeric < minimum || schema.exclusiveMinimum === true && numeric === minimum)) {
2674
+ push(issues, path, "constraint", `minimum ${minimum}`, "Number is below minimum");
2675
+ }
2676
+ if (maximum !== void 0 && (numeric > maximum || schema.exclusiveMaximum === true && numeric === maximum)) {
2677
+ push(issues, path, "constraint", `maximum ${maximum}`, "Number is above maximum");
2678
+ }
2679
+ if (schema.multipleOf !== void 0 && Math.abs(numeric / schema.multipleOf - Math.round(numeric / schema.multipleOf)) > 1e-9) {
2680
+ push(
2681
+ issues,
2682
+ path,
2683
+ "constraint",
2684
+ `multipleOf ${schema.multipleOf}`,
2685
+ "Number is not a multiple"
2686
+ );
2687
+ }
2688
+ }
2689
+ if (Array.isArray(value)) {
2690
+ if (schema.minItems !== void 0 && value.length < schema.minItems) {
2691
+ push(issues, path, "constraint", `minItems ${schema.minItems}`, "Array is too short");
2692
+ }
2693
+ if (schema.maxItems !== void 0 && value.length > schema.maxItems) {
2694
+ push(issues, path, "constraint", `maxItems ${schema.maxItems}`, "Array is too long");
2695
+ }
2696
+ if (schema.uniqueItems) {
2697
+ const unique = new Set(value.map((item) => BSON.EJSON.stringify(item, { relaxed: false })));
2698
+ if (unique.size !== value.length) {
2699
+ push(issues, path, "constraint", "uniqueItems", "Array contains duplicate items");
2700
+ }
2701
+ }
2702
+ if (Array.isArray(schema.items)) {
2703
+ schema.items.forEach((item, index) => {
2704
+ if (index < value.length)
2705
+ validateRawSchema(item, value[index], `${path}[${index}]`, issues);
2706
+ });
2707
+ } else if (schema.items) {
2708
+ value.forEach((item, index) => {
2709
+ validateRawSchema(schema.items, item, `${path}[${index}]`, issues);
2710
+ });
2711
+ }
2712
+ }
2713
+ if (value !== null && typeof value === "object" && !Array.isArray(value)) {
2714
+ const object = value;
2715
+ for (const required of schema.required ?? []) {
2716
+ if (object[required] === void 0)
2717
+ push(issues, `${path}.${required}`, "missing", "required", "Required field is missing");
2718
+ }
2719
+ for (const [key, child] of Object.entries(schema.properties ?? {})) {
2720
+ if (key in object)
2721
+ validateRawSchema(child, object[key], path ? `${path}.${key}` : key, issues);
2722
+ }
2723
+ for (const [pattern, child] of Object.entries(schema.patternProperties ?? {})) {
2724
+ for (const key of Object.keys(object)) {
2725
+ if (new RegExp(pattern).test(key))
2726
+ validateRawSchema(child, object[key], `${path}.${key}`, issues);
2727
+ }
2728
+ }
2729
+ if (schema.additionalProperties === false) {
2730
+ const known = /* @__PURE__ */ new Set([
2731
+ ...Object.keys(schema.properties ?? {}),
2732
+ ...Object.keys(object).filter(
2733
+ (key) => Object.keys(schema.patternProperties ?? {}).some(
2734
+ (pattern) => new RegExp(pattern).test(key)
2735
+ )
2736
+ )
2737
+ ]);
2738
+ for (const key of Object.keys(object)) {
2739
+ if (!known.has(key))
2740
+ push(
2741
+ issues,
2742
+ `${path}.${key}`,
2743
+ "unknown_key",
2744
+ "no additional properties",
2745
+ "Unexpected field"
2746
+ );
2747
+ }
2748
+ } else if (typeof schema.additionalProperties === "object") {
2749
+ for (const key of Object.keys(object)) {
2750
+ if (!(key in (schema.properties ?? {})))
2751
+ validateRawSchema(schema.additionalProperties, object[key], `${path}.${key}`, issues);
2752
+ }
2753
+ }
2754
+ const count = Object.keys(object).length;
2755
+ if (schema.minProperties !== void 0 && count < schema.minProperties)
2756
+ push(
2757
+ issues,
2758
+ path,
2759
+ "constraint",
2760
+ `minProperties ${schema.minProperties}`,
2761
+ "Too few properties"
2762
+ );
2763
+ if (schema.maxProperties !== void 0 && count > schema.maxProperties)
2764
+ push(
2765
+ issues,
2766
+ path,
2767
+ "constraint",
2768
+ `maxProperties ${schema.maxProperties}`,
2769
+ "Too many properties"
2770
+ );
2771
+ for (const [key, dependency] of Object.entries(schema.dependencies ?? {})) {
2772
+ if (!(key in object)) continue;
2773
+ if (Array.isArray(dependency)) {
2774
+ for (const required of dependency)
2775
+ if (!(required in object))
2776
+ push(issues, path, "constraint", "dependencies", `${key} requires ${required}`);
2777
+ } else validateRawSchema(dependency, object, path, issues);
2778
+ }
2779
+ }
2780
+ for (const child of schema.allOf ?? []) validateRawSchema(child, value, path, issues);
2781
+ if (schema.anyOf) {
2782
+ const matches = schema.anyOf.some((child) => {
2783
+ const candidate = [];
2784
+ validateRawSchema(child, value, path, candidate);
2785
+ return candidate.length === 0;
2786
+ });
2787
+ if (!matches) push(issues, path, "constraint", "anyOf", "No anyOf schema matched");
2788
+ }
2789
+ if (schema.oneOf) {
2790
+ const matches = schema.oneOf.filter((child) => {
2791
+ const candidate = [];
2792
+ validateRawSchema(child, value, path, candidate);
2793
+ return candidate.length === 0;
2794
+ }).length;
2795
+ if (matches !== 1)
2796
+ push(issues, path, "constraint", "oneOf", "Exactly one oneOf schema must match");
2797
+ }
2798
+ if (schema.not) {
2799
+ const candidate = [];
2800
+ validateRawSchema(schema.not, value, path, candidate);
2801
+ if (candidate.length === 0)
2802
+ push(issues, path, "constraint", "not", "Value matched a forbidden schema");
2803
+ }
2804
+ };
2489
2805
 
2490
2806
  // src/schema/validate-doc/field-validators.ts
2491
2807
  var patternCache = /* @__PURE__ */ new WeakMap();
@@ -2501,6 +2817,22 @@ var compiledPattern = (field) => {
2501
2817
  return void 0;
2502
2818
  }
2503
2819
  };
2820
+ var STRING_FORMAT_VALIDATORS = {
2821
+ email: (value) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value),
2822
+ uri: (value) => {
2823
+ try {
2824
+ return new URL(value).protocol.length > 0;
2825
+ } catch {
2826
+ return false;
2827
+ }
2828
+ },
2829
+ "date-time": (value) => /^\d{4}-\d{2}-\d{2}T/.test(value) && !Number.isNaN(Date.parse(value)),
2830
+ date: (value) => /^\d{4}-\d{2}-\d{2}$/.test(value) && !Number.isNaN(Date.parse(`${value}T00:00:00Z`)),
2831
+ uuid: (value) => /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(value),
2832
+ hostname: (value) => /^(?=.{1,253}$)[a-z0-9](?:[a-z0-9.-]*[a-z0-9])?$/i.test(value),
2833
+ ipv4: (value) => isIP(value) === 4,
2834
+ ipv6: (value) => isIP(value) === 6
2835
+ };
2504
2836
  var stringValidator = (_deps, field, value, path, issues) => {
2505
2837
  const f = field;
2506
2838
  if (typeof value !== "string") {
@@ -2535,6 +2867,16 @@ var stringValidator = (_deps, field, value, path, issues) => {
2535
2867
  `"${value}" does not match pattern`
2536
2868
  );
2537
2869
  }
2870
+ const format = f.format ? STRING_FORMAT_VALIDATORS[f.format] : void 0;
2871
+ if (format && !format(value)) {
2872
+ push(
2873
+ issues,
2874
+ path,
2875
+ "constraint",
2876
+ `string(format ${f.format})`,
2877
+ `"${value}" is not a valid ${f.format}`
2878
+ );
2879
+ }
2538
2880
  };
2539
2881
  var numberValidator = (_deps, field, value, path, issues) => {
2540
2882
  const f = field;
@@ -2605,7 +2947,8 @@ var nullValidator = (_deps, _field, value, path, issues) => {
2605
2947
  };
2606
2948
  var anyValidator = () => {
2607
2949
  };
2608
- var rawValidator = () => {
2950
+ var rawValidator = (_deps, field, value, path, issues) => {
2951
+ validateRawSchema(field.fragment, value, path, issues);
2609
2952
  };
2610
2953
  var enumValidator = (_deps, field, value, path, issues) => {
2611
2954
  const f = field;
@@ -2875,13 +3218,13 @@ var CacheInvalidator = class {
2875
3218
  connectedAt = Date.now();
2876
3219
  if (backoff > 0) {
2877
3220
  this.cache.invalidateByCollection(
2878
- cacheCollectionKey(ref.db.databaseName, ref.collection)
3221
+ cacheCollectionKey(ref.db.databaseName, ref.collection, ref.namespace)
2879
3222
  );
2880
3223
  }
2881
3224
  const iterator = stream[Symbol.asyncIterator]();
2882
3225
  for await (const _change of iterator) {
2883
3226
  this.cache.invalidateByCollection(
2884
- cacheCollectionKey(ref.db.databaseName, ref.collection)
3227
+ cacheCollectionKey(ref.db.databaseName, ref.collection, ref.namespace)
2885
3228
  );
2886
3229
  }
2887
3230
  } catch (err) {
@@ -2998,7 +3341,8 @@ var makeGetDbUrl = (dbClients, config) => (name) => {
2998
3341
  };
2999
3342
  var makeConnectionFactory = (dbClients, db, getDbUrl, buildManager2, openedClients = /* @__PURE__ */ new Map()) => {
3000
3343
  const connecting = /* @__PURE__ */ new Map();
3001
- return async (names) => {
3344
+ let transition = Promise.resolve();
3345
+ const connect = async (names) => {
3002
3346
  const keys = names && names.length > 0 ? names : Object.keys(dbClients);
3003
3347
  const openedThisCall = [];
3004
3348
  const assignedThisCall = [];
@@ -3016,6 +3360,7 @@ var makeConnectionFactory = (dbClients, db, getDbUrl, buildManager2, openedClien
3016
3360
  const c = new MongoClient(dbUrl, {
3017
3361
  connectTimeoutMS: definition.connectTimeoutMs ?? 1e4,
3018
3362
  serverSelectionTimeoutMS: definition.connectTimeoutMs ?? 1e4,
3363
+ monitorCommands: definition.monitorCommands ?? false,
3019
3364
  ...definition.readPreference && { readPreference: definition.readPreference },
3020
3365
  ...definition.readConcern && { readConcern: definition.readConcern },
3021
3366
  ...definition.writeConcern && { writeConcern: definition.writeConcern }
@@ -3046,18 +3391,20 @@ var makeConnectionFactory = (dbClients, db, getDbUrl, buildManager2, openedClien
3046
3391
  throw err;
3047
3392
  }
3048
3393
  };
3394
+ return (names) => {
3395
+ const result = transition.then(() => connect(names));
3396
+ transition = result.catch(() => {
3397
+ });
3398
+ return result;
3399
+ };
3049
3400
  };
3050
3401
  var closeConnections = async (openedClients) => {
3051
- const closes = [...openedClients.values()].map((client) => {
3052
- try {
3053
- return Promise.resolve(client.close()).catch(() => {
3054
- });
3055
- } catch {
3056
- return Promise.resolve();
3057
- }
3058
- });
3059
- await Promise.allSettled(closes);
3402
+ const results = await Promise.allSettled(
3403
+ [...openedClients.values()].map((client) => client.close())
3404
+ );
3060
3405
  openedClients.clear();
3406
+ const errors = results.filter((result) => result.status === "rejected").map((result) => result.reason);
3407
+ if (errors.length > 0) throw new AggregateError(errors, "Failed to close MongoDB connections");
3061
3408
  };
3062
3409
 
3063
3410
  // src/service/health.ts
@@ -3099,9 +3446,19 @@ var createForeignKeyLoader = (deps2, collection, foreignField, options = {}) =>
3099
3446
  collection,
3100
3447
  { [foreignField]: { $in: keys } },
3101
3448
  {
3102
- ...options.maxDocs !== void 0 ? { limit: options.maxDocs } : {}
3449
+ limit: options.maxDocs !== void 0 ? options.maxDocs + 1 : 0,
3450
+ ...options.session !== void 0 ? { session: options.session } : {},
3451
+ ...options.maxTimeMS !== void 0 ? { maxTimeMS: options.maxTimeMS } : {},
3452
+ ...options.readPreference !== void 0 ? { readPreference: options.readPreference } : {},
3453
+ ...options.readConcern !== void 0 ? { readConcern: options.readConcern } : {},
3454
+ ...options.queryCache !== void 0 ? { queryCache: options.queryCache } : {}
3103
3455
  }
3104
3456
  );
3457
+ if (options.maxDocs !== void 0 && docs.length > options.maxDocs) {
3458
+ throw new BadRequest(
3459
+ `populate: ${collection}.${foreignField} exceeded maxDocs ${options.maxDocs}; raise the cap or narrow the relation`
3460
+ );
3461
+ }
3105
3462
  const index = /* @__PURE__ */ new Map();
3106
3463
  for (const doc of docs) {
3107
3464
  const value = doc[foreignField];
@@ -3264,8 +3621,11 @@ var traceDbOp = async (logger, meta, fn, options = {}) => {
3264
3621
  };
3265
3622
 
3266
3623
  // src/service/op-deps.ts
3267
- var makeOpDeps = (logger, dbLabel, resolve, wrapMongoErrors) => {
3268
- const trace = (meta2, fn) => traceDbOp(logger, meta2, fn, { wrapMongoErrors });
3624
+ var makeOpDeps = (logger, dbLabel, resolve, wrapMongoErrors, debugQuery) => {
3625
+ const trace = (meta2, fn) => traceDbOp(logger, meta2, fn, {
3626
+ wrapMongoErrors,
3627
+ ...debugQuery !== void 0 ? { debugQuery } : {}
3628
+ });
3269
3629
  const meta = (collection, op, params) => ({
3270
3630
  collection: String(collection),
3271
3631
  physicalCollection: resolve(String(collection)),
@@ -3335,8 +3695,6 @@ var PipelineBuilder = class _PipelineBuilder {
3335
3695
  this.deps = deps2;
3336
3696
  this.stages = stages;
3337
3697
  }
3338
- deps;
3339
- stages;
3340
3698
  /** Raw accumulated stages — embeddable into `$lookup` / `$facet` branches. */
3341
3699
  raw() {
3342
3700
  return this.stages;
@@ -3669,20 +4027,20 @@ var createCachedAggregate = (ctx) => {
3669
4027
  const shouldDedupe = resolved.dedupe !== false && (resolved.dedupe === true || opts.dedupeReads === true && noSession) && opts.inFlight !== void 0;
3670
4028
  const runDriver = () => defineCrudOp(deps2, collection, opName, execute, options, false, { pipeline });
3671
4029
  const key = useCache ? cache.key(
3672
- cacheCollectionKey(client.databaseName, physical),
4030
+ cacheCollectionKey(client.databaseName, physical, opts.cacheNamespace),
3673
4031
  stableHash([opName, pipeline, options])
3674
4032
  ) : void 0;
3675
4033
  const toSourceKeys = () => collectAggSources(String(collection), pipeline, sources).map((name) => {
3676
4034
  try {
3677
- return cacheCollectionKey(client.databaseName, resolve(name));
4035
+ return cacheCollectionKey(client.databaseName, resolve(name), opts.cacheNamespace);
3678
4036
  } catch {
3679
- return cacheCollectionKey(client.databaseName, name);
4037
+ return cacheCollectionKey(client.databaseName, name, opts.cacheNamespace);
3680
4038
  }
3681
4039
  });
3682
4040
  const runOnce = () => {
3683
4041
  if (useCache && key) {
3684
4042
  const sourceKeys = toSourceKeys();
3685
- const primaryKey = cacheCollectionKey(client.databaseName, physical);
4043
+ const primaryKey = cacheCollectionKey(client.databaseName, physical, opts.cacheNamespace);
3686
4044
  const versions = { [primaryKey]: cache.versionOf(primaryKey) };
3687
4045
  for (const col of sourceKeys) {
3688
4046
  if (!(col in versions)) versions[col] = cache.versionOf(col);
@@ -3697,8 +4055,8 @@ var createCachedAggregate = (ctx) => {
3697
4055
  return runDriver();
3698
4056
  };
3699
4057
  if (shouldDedupe) {
3700
- const primaryKey = cacheCollectionKey(client.databaseName, physical);
3701
- const gen = cache !== void 0 ? cache.versionOf(primaryKey) : "";
4058
+ const primaryKey = cacheCollectionKey(client.databaseName, physical, opts.cacheNamespace);
4059
+ const gen = cache !== void 0 ? toSourceKeys().map((source) => `${source}:${cache.versionOf(source)}`).join("|") : "";
3702
4060
  const dedupeKey = `${primaryKey}|${gen}|${opName}|${stableHash([pipeline, options])}`;
3703
4061
  return await opts.inFlight.run(dedupeKey, runOnce);
3704
4062
  }
@@ -3907,7 +4265,13 @@ var makeTextSearchOp = (ctx) => {
3907
4265
  // src/service/aggregation/index.ts
3908
4266
  var makeAggregationOps = (client, dbLabel, logger, opts) => {
3909
4267
  const resolve = opts.resolveCollectionName;
3910
- const deps2 = makeOpDeps(logger, dbLabel, resolve, opts.wrapMongoErrors === true);
4268
+ const deps2 = makeOpDeps(
4269
+ logger,
4270
+ dbLabel,
4271
+ resolve,
4272
+ opts.wrapMongoErrors === true,
4273
+ opts.debugQuery
4274
+ );
3911
4275
  const coll = (collection) => client.collection(resolve(String(collection)));
3912
4276
  const ctx = {
3913
4277
  resolve,
@@ -3925,11 +4289,7 @@ var makeAggregationOps = (client, dbLabel, logger, opts) => {
3925
4289
  };
3926
4290
 
3927
4291
  // src/schema/apply-defaults.ts
3928
- var cloneDefault = (value) => {
3929
- if (value === null || typeof value !== "object") return value;
3930
- if (typeof structuredClone === "function") return structuredClone(value);
3931
- return Array.isArray(value) ? [...value] : { ...value };
3932
- };
4292
+ var cloneDefault = (value) => cloneDeep(value);
3933
4293
  var applySchemaDefaults = (schema, doc) => {
3934
4294
  if (!schema || doc === null || typeof doc !== "object" || Array.isArray(doc)) return;
3935
4295
  for (const [key, prop] of Object.entries(schema.properties)) {
@@ -3978,7 +4338,9 @@ var checkDocsDrift = (options, collection, op, docs) => {
3978
4338
  const schema = options.getSchema?.(collection);
3979
4339
  if (!schema) return;
3980
4340
  for (const doc of docs) {
3981
- const issues = validateDoc(schema, doc);
4341
+ const issues = validateDoc(schema, doc).filter(
4342
+ (issue) => options.partial !== true || issue.code !== "missing"
4343
+ );
3982
4344
  if (issues.length === 0) continue;
3983
4345
  const detail = issues.map((i) => `${i.path}: ${i.message}`).join("; ");
3984
4346
  const extra = { collection, db: options.db, op, issues };
@@ -4012,7 +4374,8 @@ var createCrudContext = (client, dbLabel, logger, opts) => {
4012
4374
  logger,
4013
4375
  dbLabel,
4014
4376
  resolve,
4015
- opts.wrapMongoErrors === true
4377
+ opts.wrapMongoErrors === true,
4378
+ opts.debugQuery
4016
4379
  );
4017
4380
  const coll = (name) => client.collection(resolve(String(name)));
4018
4381
  const toDriverFilter = (filter) => filter;
@@ -4020,7 +4383,7 @@ var createCrudContext = (client, dbLabel, logger, opts) => {
4020
4383
  const writeRun = (collection, opName, execute, options, params) => defineCrudOp(deps2, collection, opName, execute, options, true, params);
4021
4384
  const invalidate = (collection) => {
4022
4385
  opts.cache?.invalidateByCollection(
4023
- cacheCollectionKey(client.databaseName, resolve(String(collection)))
4386
+ cacheCollectionKey(client.databaseName, resolve(String(collection)), opts.cacheNamespace)
4024
4387
  );
4025
4388
  };
4026
4389
  const timestampsFor = (collection) => opts.timestamps?.[String(collection)];
@@ -4050,6 +4413,26 @@ var createCrudContext = (client, dbLabel, logger, opts) => {
4050
4413
  if (!ts?.updatedAt) return doc;
4051
4414
  return { ...doc, [ts.updatedAt]: /* @__PURE__ */ new Date() };
4052
4415
  };
4416
+ const validateWrite = (collection, doc, generateId = true) => {
4417
+ if (opts.validateWrites === false) return;
4418
+ const schema = opts.getSchema?.(String(collection));
4419
+ if (!schema) return;
4420
+ if (generateId && doc._id === void 0 && schema.properties._id?.kind === "objectId") {
4421
+ doc._id = new ObjectId();
4422
+ }
4423
+ const issues = validateDoc(schema, doc).filter(
4424
+ (issue) => generateId || issue.path !== "_id" || issue.code !== "missing"
4425
+ );
4426
+ if (issues.length > 0) {
4427
+ throw new DomainError("VALIDATION_FAILED", "Document failed schema validation", {
4428
+ db: client.databaseName,
4429
+ collection: String(collection),
4430
+ ...doc._id !== void 0 ? { documentId: doc._id } : {},
4431
+ fields: [...new Set(issues.map((issue) => issue.path))],
4432
+ issues
4433
+ });
4434
+ }
4435
+ };
4053
4436
  const read = async (collection, opName, filter, options, execute, shape = "none", keyExtra) => {
4054
4437
  const physical = resolve(String(collection));
4055
4438
  const resolved = resolveQueryOptions(options);
@@ -4062,14 +4445,15 @@ var createCrudContext = (client, dbLabel, logger, opts) => {
4062
4445
  const checkDrift = (value) => {
4063
4446
  const mode = resolveDriftMode(resolved.sdk.drift, opts.drift);
4064
4447
  if (mode === "off") return;
4065
- if (resolved.driverOpts.projection !== void 0) return;
4448
+ const partial = resolved.driverOpts.projection !== void 0;
4066
4449
  const docs = shape === "many" ? value : shape === "one" && value !== null ? [value] : [];
4067
4450
  checkDocsDrift(
4068
4451
  {
4069
4452
  logger,
4070
4453
  db: dbLabel,
4071
4454
  drift: mode,
4072
- ...opts.getSchema !== void 0 ? { getSchema: opts.getSchema } : {}
4455
+ ...opts.getSchema !== void 0 ? { getSchema: opts.getSchema } : {},
4456
+ partial
4073
4457
  },
4074
4458
  String(collection),
4075
4459
  opName,
@@ -4098,14 +4482,17 @@ var createCrudContext = (client, dbLabel, logger, opts) => {
4098
4482
  }
4099
4483
  return payloadHash;
4100
4484
  };
4101
- const key = useCache ? opts.cache.key(cacheCollectionKey(client.databaseName, physical), getPayloadHash()) : void 0;
4485
+ const key = useCache ? opts.cache.key(
4486
+ cacheCollectionKey(client.databaseName, physical, opts.cacheNamespace),
4487
+ getPayloadHash()
4488
+ ) : void 0;
4102
4489
  const fetchAndCheck = () => executeWrapped().then((value) => {
4103
4490
  checkDrift(value);
4104
4491
  return value;
4105
4492
  });
4106
4493
  const runOnce = () => {
4107
4494
  if (useCache && key) {
4108
- const colKey = cacheCollectionKey(client.databaseName, physical);
4495
+ const colKey = cacheCollectionKey(client.databaseName, physical, opts.cacheNamespace);
4109
4496
  const versions = { [colKey]: opts.cache.versionOf(colKey) };
4110
4497
  const hit = opts.cache.get(key);
4111
4498
  if (hit !== void 0) return Promise.resolve(hit);
@@ -4117,7 +4504,7 @@ var createCrudContext = (client, dbLabel, logger, opts) => {
4117
4504
  return fetchAndCheck();
4118
4505
  };
4119
4506
  if (shouldDedupe) {
4120
- const colKey = cacheCollectionKey(client.databaseName, physical);
4507
+ const colKey = cacheCollectionKey(client.databaseName, physical, opts.cacheNamespace);
4121
4508
  const gen = opts.cache !== void 0 ? opts.cache.versionOf(colKey) : "";
4122
4509
  return await opts.inFlight.run(`${colKey}|${gen}|${getPayloadHash()}`, runOnce);
4123
4510
  }
@@ -4147,6 +4534,7 @@ var createCrudContext = (client, dbLabel, logger, opts) => {
4147
4534
  stampCreate,
4148
4535
  stampUpdate,
4149
4536
  stampReplace,
4537
+ validateWrite,
4150
4538
  read,
4151
4539
  normalizeFindOptions
4152
4540
  };
@@ -4371,7 +4759,16 @@ var makeWatchOps = (ctx) => {
4371
4759
  // src/service/crud/write-ops.ts
4372
4760
  var makeWriteOps = (ctx, readOps) => {
4373
4761
  const { opts } = ctx;
4374
- const { coll, toDriverFilter, writeRun, invalidate, stampCreate, stampUpdate, stampReplace } = ctx;
4762
+ const {
4763
+ coll,
4764
+ toDriverFilter,
4765
+ writeRun,
4766
+ invalidate,
4767
+ stampCreate,
4768
+ stampUpdate,
4769
+ stampReplace,
4770
+ validateWrite
4771
+ } = ctx;
4375
4772
  const { getOne } = readOps;
4376
4773
  const safeAfterHooks = async (collection, name, hookCtx) => {
4377
4774
  try {
@@ -4393,6 +4790,7 @@ var makeWriteOps = (ctx, readOps) => {
4393
4790
  doc
4394
4791
  });
4395
4792
  stampCreate(collection, doc);
4793
+ validateWrite(collection, doc);
4396
4794
  const result = await writeRun(
4397
4795
  collection,
4398
4796
  "mongo.insertOne",
@@ -4426,7 +4824,10 @@ var makeWriteOps = (ctx, readOps) => {
4426
4824
  collection: String(collection),
4427
4825
  docs
4428
4826
  });
4429
- for (const d of docs) stampCreate(collection, d);
4827
+ for (const d of docs) {
4828
+ stampCreate(collection, d);
4829
+ validateWrite(collection, d);
4830
+ }
4430
4831
  const result = await writeRun(
4431
4832
  collection,
4432
4833
  "mongo.insertMany",
@@ -4550,6 +4951,7 @@ var makeWriteOps = (ctx, readOps) => {
4550
4951
  filter
4551
4952
  });
4552
4953
  const effective = stampReplace(collection, replacement);
4954
+ validateWrite(collection, effective, false);
4553
4955
  const result = await writeRun(
4554
4956
  collection,
4555
4957
  "mongo.findOneAndReplace",
@@ -4583,6 +4985,8 @@ var makeWriteOps = (ctx, readOps) => {
4583
4985
  collection: String(collection),
4584
4986
  filter
4585
4987
  });
4988
+ const effective = stampReplace(collection, replacement);
4989
+ validateWrite(collection, effective, false);
4586
4990
  const result = await writeRun(
4587
4991
  collection,
4588
4992
  "mongo.replaceOne",
@@ -4593,7 +4997,7 @@ var makeWriteOps = (ctx, readOps) => {
4593
4997
  };
4594
4998
  return coll(collection).replaceOne(
4595
4999
  toDriverFilter(filter),
4596
- stampReplace(collection, replacement),
5000
+ effective,
4597
5001
  repOpts
4598
5002
  );
4599
5003
  },
@@ -4849,6 +5253,7 @@ var makeCrudOps = (client, dbLabel, logger, opts) => {
4849
5253
  ...makeWatchOps(ctx)
4850
5254
  };
4851
5255
  };
5256
+ var { EJSON: EJSON2 } = BSON;
4852
5257
  var tag = (value) => {
4853
5258
  if (value instanceof ObjectId) return { t: "oid", v: value.toHexString() };
4854
5259
  if (value instanceof Date) return { t: "date", v: value.toISOString() };
@@ -4856,6 +5261,9 @@ var tag = (value) => {
4856
5261
  if (typeof value === "number") return { t: "number", v: value };
4857
5262
  if (typeof value === "boolean") return { t: "boolean", v: value };
4858
5263
  if (value === null || value === void 0) return { t: "null" };
5264
+ if (typeof value === "object" && "_bsontype" in value) {
5265
+ return { t: "bson", v: EJSON2.stringify(value, { relaxed: false }) };
5266
+ }
4859
5267
  throw new BadRequest(
4860
5268
  `paginateCursor: unsupported sort value type "${typeof value}" \u2014 key values must be ObjectId, Date, string, number, boolean, or null`
4861
5269
  );
@@ -4889,6 +5297,11 @@ var untag = (tagged) => {
4889
5297
  return tagged.v;
4890
5298
  case "null":
4891
5299
  return null;
5300
+ case "bson":
5301
+ if (typeof tagged.v !== "string") {
5302
+ throw new BadRequest("paginateCursor: invalid cursor \u2014 BSON value must be a string");
5303
+ }
5304
+ return EJSON2.parse(tagged.v, { relaxed: false });
4892
5305
  }
4893
5306
  };
4894
5307
  var encodeCursor = (cursor) => Buffer.from(
@@ -4933,11 +5346,17 @@ var makePaginateCursor = (ctx) => {
4933
5346
  const { client, dbLabel, logger, opts, resolve, deps: deps2 } = ctx;
4934
5347
  const paginateCursor = async (collection, baseFilter, config) => {
4935
5348
  const sortKeys = Object.entries(config.sort);
5349
+ const uniqueTiebreaker = config.uniqueTiebreaker ?? "_id";
4936
5350
  if (sortKeys.length === 0) {
4937
5351
  throw new BadRequest(
4938
5352
  "paginateCursor: `sort` is required (include a unique tiebreaker like `_id`)"
4939
5353
  );
4940
5354
  }
5355
+ if (sortKeys.at(-1)?.[0] !== uniqueTiebreaker) {
5356
+ throw new BadRequest(
5357
+ `paginateCursor: the final sort key must be the unique tiebreaker "${uniqueTiebreaker}"`
5358
+ );
5359
+ }
4941
5360
  if (config.before !== void 0 && config.after !== void 0) {
4942
5361
  throw new BadRequest("paginateCursor: provide either `after` or `before`, not both");
4943
5362
  }
@@ -4993,10 +5412,15 @@ var makePaginateCursor = (ctx) => {
4993
5412
  "mongo.paginateCursor",
4994
5413
  page
4995
5414
  );
4996
- const boundary = backwards ? rows[limit] : page[page.length - 1];
5415
+ const boundary = backwards ? page[0] : page[page.length - 1];
4997
5416
  let nextCursor = null;
4998
5417
  if (hasMore && boundary !== void 0) {
4999
- const values = sortKeys.map(([f]) => boundary[f]);
5418
+ const values = sortKeys.map(
5419
+ ([f]) => f.split(".").reduce(
5420
+ (value, part) => value !== null && typeof value === "object" ? value[part] : void 0,
5421
+ boundary
5422
+ )
5423
+ );
5000
5424
  if (values.some((v) => v === void 0)) {
5001
5425
  throw new BadRequest(
5002
5426
  "paginateCursor: boundary document is missing a sort field \u2014 every sort key must exist on every document for keyset pagination"
@@ -5023,6 +5447,12 @@ var makePaginateFlexible = (ctx) => {
5023
5447
  // APIs used to disagree silently (10 vs 100).
5024
5448
  { limit: DEFAULT_FIND_LIMIT }
5025
5449
  );
5450
+ const maxOffset = config.maxOffset ?? 1e4;
5451
+ if (normalized.offset > maxOffset) {
5452
+ throw new BadRequest(
5453
+ `paginateFlexible: offset ${normalized.offset} exceeds maxOffset ${maxOffset}; use paginateCursor for deep pages`
5454
+ );
5455
+ }
5026
5456
  const dataStages = [
5027
5457
  ...config.prePipeline ?? [],
5028
5458
  ...config.sort && Object.keys(config.sort).length > 0 ? [{ $sort: config.sort }] : [],
@@ -5057,19 +5487,18 @@ var makePaginateFlexible = (ctx) => {
5057
5487
  const row = rows[0];
5058
5488
  const data = row?.data ?? [];
5059
5489
  const totalCount = row?.totalCount?.[0]?.count ?? 0;
5060
- if (config.projection === void 0) {
5061
- checkDocsDrift(
5062
- {
5063
- logger,
5064
- db: dbLabel,
5065
- drift: resolveDriftMode(config.queryOptions?.drift, opts.drift),
5066
- ...opts.getSchema !== void 0 ? { getSchema: opts.getSchema } : {}
5067
- },
5068
- String(collection),
5069
- "mongo.paginateFlexible",
5070
- data
5071
- );
5072
- }
5490
+ checkDocsDrift(
5491
+ {
5492
+ logger,
5493
+ db: dbLabel,
5494
+ drift: resolveDriftMode(config.queryOptions?.drift, opts.drift),
5495
+ ...opts.getSchema !== void 0 ? { getSchema: opts.getSchema } : {},
5496
+ partial: config.projection !== void 0
5497
+ },
5498
+ String(collection),
5499
+ "mongo.paginateFlexible",
5500
+ data
5501
+ );
5073
5502
  return buildPaginationResult(data, totalCount, normalized.page, normalized.limit);
5074
5503
  }
5075
5504
  });
@@ -5080,7 +5509,13 @@ var makePaginateFlexible = (ctx) => {
5080
5509
  // src/service/pagination/index.ts
5081
5510
  var makePaginationOps = (client, dbLabel, logger, opts) => {
5082
5511
  const resolve = opts.resolveCollectionName;
5083
- const deps2 = makeOpDeps(logger, dbLabel, resolve, opts.wrapMongoErrors === true);
5512
+ const deps2 = makeOpDeps(
5513
+ logger,
5514
+ dbLabel,
5515
+ resolve,
5516
+ opts.wrapMongoErrors === true,
5517
+ opts.debugQuery
5518
+ );
5084
5519
  const ctx = {
5085
5520
  client,
5086
5521
  dbLabel,
@@ -5097,16 +5532,37 @@ var makePaginationOps = (client, dbLabel, logger, opts) => {
5097
5532
  };
5098
5533
 
5099
5534
  // src/service/schema-ops.ts
5100
- var optionSignature = (o) => JSON.stringify([
5101
- Boolean(o?.unique),
5102
- Boolean(o?.sparse),
5103
- typeof o?.expireAfterSeconds === "number" ? o.expireAfterSeconds : null
5104
- ]);
5535
+ var optionSignature = (options) => {
5536
+ const normalized = {
5537
+ unique: Boolean(options?.unique),
5538
+ sparse: Boolean(options?.sparse),
5539
+ hidden: Boolean(options?.hidden),
5540
+ expireAfterSeconds: typeof options?.expireAfterSeconds === "number" ? options.expireAfterSeconds : null
5541
+ };
5542
+ for (const key of [
5543
+ "partialFilterExpression",
5544
+ "collation",
5545
+ "wildcardProjection",
5546
+ "weights",
5547
+ "default_language",
5548
+ "language_override",
5549
+ "textIndexVersion",
5550
+ "2dsphereIndexVersion",
5551
+ "bits",
5552
+ "min",
5553
+ "max",
5554
+ "bucketSize",
5555
+ "storageEngine"
5556
+ ]) {
5557
+ if (options?.[key] !== void 0) normalized[key] = options[key];
5558
+ }
5559
+ return stableStringify(normalized);
5560
+ };
5105
5561
  var indexKeyOf = (key, options) => `${JSON.stringify(Object.keys(key).map((k) => [k, key[k]]))}|${optionSignature(options)}`;
5106
5562
  var makeSchemaOps = (client, _logger, opts) => {
5107
- const readCollectionValidator = async (physical) => {
5563
+ const readCollectionValidation = async (physical) => {
5108
5564
  const info = await client.listCollections({ name: physical }).next();
5109
- return info?.options?.validator;
5565
+ return info?.options ?? {};
5110
5566
  };
5111
5567
  const ensureDeclaredIndexes = async (collection, physical, def) => {
5112
5568
  const declared = def?.indexes ?? [];
@@ -5120,7 +5576,7 @@ var makeSchemaOps = (client, _logger, opts) => {
5120
5576
  try {
5121
5577
  await handle.createIndex(index.key, index.options);
5122
5578
  } catch (err) {
5123
- if (err?.code === 85) {
5579
+ if ([85, 86].includes(err?.code ?? -1)) {
5124
5580
  throw new Error(
5125
5581
  `Cannot apply declared index on "${collection}": an index with the same name/key already exists with different options. Run db.syncIndexes("${collection}") to reconcile index drift.`,
5126
5582
  { cause: err }
@@ -5145,19 +5601,21 @@ var makeSchemaOps = (client, _logger, opts) => {
5145
5601
  created = false;
5146
5602
  }
5147
5603
  if (created) {
5148
- try {
5149
- await ensureDeclaredIndexes(collection, physical, def);
5150
- } catch (err) {
5151
- await client.dropCollection(physical).catch(() => {
5152
- });
5153
- throw err;
5154
- }
5604
+ await ensureDeclaredIndexes(collection, physical, def);
5155
5605
  return;
5156
5606
  }
5157
5607
  if (createOptions.validator) {
5158
- const current = await readCollectionValidator(physical);
5159
- if (JSON.stringify(current) !== JSON.stringify(createOptions.validator)) {
5160
- await client.command({ collMod: physical, validator: createOptions.validator });
5608
+ const current = await readCollectionValidation(physical);
5609
+ const collMod = {};
5610
+ if (stableStringify(current.validator) !== stableStringify(createOptions.validator)) {
5611
+ collMod.validator = createOptions.validator;
5612
+ }
5613
+ for (const key of ["validationLevel", "validationAction"]) {
5614
+ const requested = createOptions[key];
5615
+ if (requested !== void 0 && current[key] !== requested) collMod[key] = requested;
5616
+ }
5617
+ if (Object.keys(collMod).length > 0) {
5618
+ await client.command({ collMod: physical, ...collMod });
5161
5619
  }
5162
5620
  }
5163
5621
  await ensureDeclaredIndexes(collection, physical, def);
@@ -5173,6 +5631,7 @@ var makeSchemaOps = (client, _logger, opts) => {
5173
5631
  collMod: physical,
5174
5632
  validator: toMongoValidator(resolved)
5175
5633
  });
5634
+ if (def) def.schema = resolved;
5176
5635
  };
5177
5636
  const syncIndexes = async (collection) => {
5178
5637
  const physical = opts.resolveCollectionName(collection);
@@ -5186,52 +5645,68 @@ var makeSchemaOps = (client, _logger, opts) => {
5186
5645
  const declaredByKey = new Map(
5187
5646
  declared.map((d) => [indexKeyOf(d.key, d.options), d])
5188
5647
  );
5189
- const created = [];
5648
+ const keyPattern = (key) => JSON.stringify(Object.entries(key));
5190
5649
  const dropped = [];
5191
- for (const [key] of declaredByKey) {
5192
- if (!existingByKey.has(key)) {
5193
- await handle.createIndex(
5194
- declaredByKey.get(key).key,
5195
- declaredByKey.get(key).options
5196
- );
5197
- }
5198
- }
5199
- const after = (await handle.listIndexes().toArray()).filter((i) => i.name !== "_id_");
5200
- const afterByKey = new Map(
5201
- after.map((i) => [indexKeyOf(i.key, i), i.name])
5202
- );
5203
- for (const [key, spec] of declaredByKey) {
5204
- if (!existingByKey.has(key)) {
5205
- created.push(spec.options?.name ?? afterByKey.get(key) ?? JSON.stringify(spec.key));
5650
+ const droppedNames = /* @__PURE__ */ new Set();
5651
+ for (const spec of declaredByKey.values()) {
5652
+ const signature = indexKeyOf(spec.key, spec.options);
5653
+ if (existingByKey.has(signature)) continue;
5654
+ for (const index of existing) {
5655
+ const name = index.name;
5656
+ if (droppedNames.has(name)) continue;
5657
+ const sameName = spec.options?.name !== void 0 && index.name === spec.options.name;
5658
+ if (sameName || keyPattern(index.key) === keyPattern(spec.key)) {
5659
+ await handle.dropIndex(name);
5660
+ droppedNames.add(name);
5661
+ dropped.push(name);
5662
+ }
5206
5663
  }
5664
+ await handle.createIndex(spec.key, spec.options);
5207
5665
  }
5208
- for (const [key, name] of existingByKey) {
5209
- if (!declaredByKey.has(key)) {
5666
+ for (const [signature, name] of existingByKey) {
5667
+ if (!declaredByKey.has(signature) && !droppedNames.has(name)) {
5210
5668
  await handle.dropIndex(name);
5211
5669
  dropped.push(name);
5212
5670
  }
5213
5671
  }
5672
+ const after = (await handle.listIndexes().toArray()).filter((i) => i.name !== "_id_");
5673
+ const afterByKey = new Map(
5674
+ after.map((i) => [indexKeyOf(i.key, i), i.name])
5675
+ );
5676
+ const created = [...declaredByKey].filter(([signature]) => !existingByKey.has(signature)).map(
5677
+ ([signature, spec]) => spec.options?.name ?? afterByKey.get(signature) ?? JSON.stringify(spec.key)
5678
+ );
5214
5679
  return { created, dropped };
5215
5680
  };
5216
5681
  return { createSchema, updateSchema, syncIndexes };
5217
5682
  };
5218
-
5219
- // src/service/transaction.ts
5220
5683
  var makeTransactionOps = (client, handle, logger, _config, opts) => {
5221
5684
  let warnedUnsupported = false;
5222
5685
  let journalIndexReady = false;
5223
5686
  const MIGRATE_LEASE_MS = 12e4;
5224
- const transaction = async (fn, options) => {
5687
+ const transaction = async (fn, options = {}) => {
5688
+ const { allowNonTransactionalFallback, ...driverOptions } = options;
5689
+ const allowFallback = allowNonTransactionalFallback ?? opts.allowNonTransactionalFallback ?? false;
5225
5690
  const session = client.startSession();
5226
5691
  try {
5227
- return await session.withTransaction(async () => fn(session), options);
5692
+ return await session.withTransaction(async () => fn(session), driverOptions);
5228
5693
  } catch (err) {
5229
5694
  if (isTransactionUnsupportedError(err)) {
5695
+ if (!allowFallback) {
5696
+ if (opts.wrapMongoErrors) {
5697
+ throw new InfraError(
5698
+ "TRANSACTIONS_UNSUPPORTED",
5699
+ "MongoDB deployment does not support transactions; enable allowNonTransactionalFallback to run non-atomically",
5700
+ { db: handle.databaseName, op: "transaction" }
5701
+ );
5702
+ }
5703
+ throw err;
5704
+ }
5230
5705
  if (!warnedUnsupported) {
5231
5706
  warnedUnsupported = true;
5232
5707
  logger.warn?.(
5233
5708
  { error: err instanceof Error ? err.message : String(err) },
5234
- "transactions unsupported on this deployment, falling back to non-transactional"
5709
+ "transactions unsupported; running explicitly requested non-transactional fallback"
5235
5710
  );
5236
5711
  }
5237
5712
  return fn(null);
@@ -5241,16 +5716,14 @@ var makeTransactionOps = (client, handle, logger, _config, opts) => {
5241
5716
  }
5242
5717
  throw err;
5243
5718
  } finally {
5244
- await session.endSession();
5719
+ await session.endSession().catch(() => {
5720
+ });
5245
5721
  }
5246
5722
  };
5247
- const migrate = async (name, fn, filePath) => {
5723
+ const migrate = async (name, fn, filePath, options = {}) => {
5248
5724
  const journal = handle.collection(opts.migrationCollectionPhysical);
5249
5725
  if (!journalIndexReady) {
5250
- try {
5251
- await journal.createIndex({ name: 1 }, { unique: true });
5252
- } catch {
5253
- }
5726
+ await journal.createIndex({ name: 1 }, { unique: true });
5254
5727
  journalIndexReady = true;
5255
5728
  }
5256
5729
  const isDupKeyError = (err) => {
@@ -5262,10 +5735,11 @@ var makeTransactionOps = (client, handle, logger, _config, opts) => {
5262
5735
  const sessionOpts = session ? { session } : {};
5263
5736
  const now = /* @__PURE__ */ new Date();
5264
5737
  const expiresAt = new Date(Date.now() + MIGRATE_LEASE_MS);
5738
+ const claimId = randomUUID();
5265
5739
  const stole = await journal.findOneAndUpdate(
5266
5740
  {
5267
5741
  name,
5268
- status: { $exists: true, $nin: ["applied"] },
5742
+ status: { $exists: true, $nin: ["applied", "rolling_back"] },
5269
5743
  $or: [
5270
5744
  { expiresAt: { $lt: now } },
5271
5745
  {
@@ -5279,13 +5753,14 @@ var makeTransactionOps = (client, handle, logger, _config, opts) => {
5279
5753
  status: "running",
5280
5754
  appliedAt: now,
5281
5755
  expiresAt,
5756
+ claimId,
5282
5757
  ...filePath ? { file: filePath } : {}
5283
5758
  }
5284
5759
  },
5285
5760
  { returnDocument: "before", ...sessionOpts }
5286
5761
  );
5287
5762
  if (stole !== null) {
5288
- await runClaimed(session);
5763
+ await runClaimed(session, claimId);
5289
5764
  return;
5290
5765
  }
5291
5766
  try {
@@ -5295,6 +5770,7 @@ var makeTransactionOps = (client, handle, logger, _config, opts) => {
5295
5770
  appliedAt: now,
5296
5771
  status: "running",
5297
5772
  expiresAt,
5773
+ claimId,
5298
5774
  ...filePath ? { file: filePath } : {}
5299
5775
  },
5300
5776
  sessionOpts
@@ -5313,13 +5789,13 @@ var makeTransactionOps = (client, handle, logger, _config, opts) => {
5313
5789
  }
5314
5790
  return;
5315
5791
  }
5316
- await runClaimed(session);
5317
- });
5318
- async function runClaimed(session) {
5792
+ await runClaimed(session, claimId);
5793
+ }, options);
5794
+ async function runClaimed(session, claimId) {
5319
5795
  const sessionOpts = session ? { session } : {};
5320
5796
  const heartbeat = session === null ? setInterval(() => {
5321
5797
  void journal.updateOne(
5322
- { name, status: "running" },
5798
+ { name, claimId, status: "running" },
5323
5799
  { $set: { expiresAt: new Date(Date.now() + MIGRATE_LEASE_MS) } }
5324
5800
  ).catch(() => {
5325
5801
  });
@@ -5328,16 +5804,15 @@ var makeTransactionOps = (client, handle, logger, _config, opts) => {
5328
5804
  try {
5329
5805
  await fn(session);
5330
5806
  const res = await journal.updateOne(
5331
- { name, status: "running" },
5332
- { $set: { status: "applied" } },
5807
+ { name, claimId, status: "running" },
5808
+ { $set: { status: "applied" }, $unset: { expiresAt: "", claimId: "" } },
5333
5809
  sessionOpts
5334
5810
  );
5335
- if (res?.matchedCount === 0) {
5336
- await journal.insertOne({ name, appliedAt: /* @__PURE__ */ new Date(), status: "applied" }, sessionOpts).catch(() => {
5337
- });
5811
+ if (res.matchedCount === 0) {
5812
+ throw new Error(`Migration "${name}" lost its claim before it could be marked applied`);
5338
5813
  }
5339
5814
  } catch (err) {
5340
- await journal.deleteOne({ name }, sessionOpts).catch(() => {
5815
+ await journal.deleteOne({ name, claimId, status: "running" }, sessionOpts).catch(() => {
5341
5816
  });
5342
5817
  throw err;
5343
5818
  } finally {
@@ -5349,7 +5824,7 @@ var makeTransactionOps = (client, handle, logger, _config, opts) => {
5349
5824
  };
5350
5825
 
5351
5826
  // src/service/manager.ts
5352
- var buildManager = (deps2, dbKey, dbName, client) => {
5827
+ var buildManager = (deps2, dbKey, dbName, client, cacheNamespace = "") => {
5353
5828
  const { dbClients, config, logger, sharedCache, dedupeReads, inFlight, collectionRefs } = deps2;
5354
5829
  const definition = dbClients[dbKey];
5355
5830
  const handle = client.db(dbName);
@@ -5362,7 +5837,11 @@ var buildManager = (deps2, dbKey, dbName, client) => {
5362
5837
  registry.set(logical, def);
5363
5838
  hooks[logical] = def.hooks;
5364
5839
  if (def.timestamps) timestamps[logical] = def.timestamps;
5365
- collectionRefs.push({ db: handle, collection: baseResolveCollectionName(logical) });
5840
+ collectionRefs.push({
5841
+ db: handle,
5842
+ collection: baseResolveCollectionName(logical),
5843
+ namespace: cacheNamespace
5844
+ });
5366
5845
  }
5367
5846
  const getDefinition = (logical) => registry.get(logical);
5368
5847
  const resolveCollectionName = (logical) => {
@@ -5373,18 +5852,23 @@ var buildManager = (deps2, dbKey, dbName, client) => {
5373
5852
  };
5374
5853
  const crud = makeCrudOps(handle, dbName, logger, {
5375
5854
  resolveCollectionName,
5855
+ cacheNamespace,
5376
5856
  wrapMongoErrors: deps2.wrapMongoErrors,
5857
+ ...deps2.debugQuery !== void 0 ? { debugQuery: deps2.debugQuery } : {},
5377
5858
  ...sharedCache !== void 0 ? { cache: sharedCache } : {},
5378
5859
  dedupeReads,
5379
5860
  inFlight,
5380
5861
  hooks,
5381
5862
  timestamps,
5863
+ ...deps2.validateWrites !== void 0 ? { validateWrites: deps2.validateWrites } : {},
5382
5864
  ...deps2.drift !== void 0 ? { drift: deps2.drift } : {},
5383
5865
  ...getDefinition !== void 0 ? { getSchema: (logical) => getDefinition(logical)?.schema } : {}
5384
5866
  });
5385
5867
  const pagination = makePaginationOps(handle, dbName, logger, {
5386
5868
  resolveCollectionName,
5869
+ cacheNamespace,
5387
5870
  wrapMongoErrors: deps2.wrapMongoErrors,
5871
+ ...deps2.debugQuery !== void 0 ? { debugQuery: deps2.debugQuery } : {},
5388
5872
  ...deps2.drift !== void 0 ? { drift: deps2.drift } : {},
5389
5873
  ...getDefinition !== void 0 ? { getSchema: (logical) => getDefinition(logical)?.schema } : {},
5390
5874
  ...sharedCache !== void 0 ? { cache: sharedCache } : {},
@@ -5393,7 +5877,9 @@ var buildManager = (deps2, dbKey, dbName, client) => {
5393
5877
  });
5394
5878
  const aggregation = makeAggregationOps(handle, dbName, logger, {
5395
5879
  resolveCollectionName,
5880
+ cacheNamespace,
5396
5881
  wrapMongoErrors: deps2.wrapMongoErrors,
5882
+ ...deps2.debugQuery !== void 0 ? { debugQuery: deps2.debugQuery } : {},
5397
5883
  ...sharedCache !== void 0 ? { cache: sharedCache } : {},
5398
5884
  dedupeReads,
5399
5885
  inFlight
@@ -5401,16 +5887,14 @@ var buildManager = (deps2, dbKey, dbName, client) => {
5401
5887
  const schema = makeSchemaOps(handle, logger, { resolveCollectionName, getDefinition });
5402
5888
  const transactionOps = makeTransactionOps(client, handle, logger, config, {
5403
5889
  migrationCollectionPhysical: resolveCollectionName("_migrations"),
5404
- wrapMongoErrors: deps2.wrapMongoErrors
5890
+ wrapMongoErrors: deps2.wrapMongoErrors,
5891
+ ...deps2.allowNonTransactionalFallback !== void 0 ? { allowNonTransactionalFallback: deps2.allowNonTransactionalFallback } : {}
5405
5892
  });
5406
5893
  const populator = makePopulator({
5407
- findMany: async (logical, filter, options) => {
5408
- const physical = resolveCollectionName(logical);
5409
- return handle.collection(physical).find(filter, {
5410
- ...options?.projection !== void 0 ? { projection: options.projection } : {},
5411
- limit: options?.limit ?? 1e4
5412
- }).toArray();
5413
- }
5894
+ findMany: async (logical, filter, options) => crud.findMany(logical, filter, {
5895
+ ...options,
5896
+ cache: options?.queryCache ?? false
5897
+ })
5414
5898
  });
5415
5899
  const populate = (async (docs, relations, options = {}) => {
5416
5900
  const result = await populator.populate(docs, relations, options);
@@ -5424,6 +5908,7 @@ var buildManager = (deps2, dbKey, dbName, client) => {
5424
5908
  ...transactionOps,
5425
5909
  populate,
5426
5910
  client: handle,
5911
+ mongoClient: client,
5427
5912
  db: deps2.db
5428
5913
  };
5429
5914
  };
@@ -5451,25 +5936,44 @@ var createMongoService = (dbClients, configOverrides = {}) => {
5451
5936
  dedupeReads,
5452
5937
  inFlight,
5453
5938
  wrapMongoErrors: runtime.wrapMongoErrors,
5939
+ ...configOverrides.debugQuery !== void 0 ? { debugQuery: configOverrides.debugQuery } : {},
5940
+ ...configOverrides.allowNonTransactionalFallback !== void 0 ? { allowNonTransactionalFallback: configOverrides.allowNonTransactionalFallback } : {},
5941
+ ...configOverrides.validateWrites !== void 0 ? { validateWrites: configOverrides.validateWrites } : {},
5454
5942
  ...configOverrides.drift !== void 0 ? { drift: configOverrides.drift } : {},
5455
5943
  collectionRefs,
5456
5944
  db
5457
5945
  },
5458
5946
  key,
5459
5947
  dbName,
5460
- client
5948
+ client,
5949
+ stableHash(getDbUrl(key))
5461
5950
  )),
5462
5951
  openedClients
5463
5952
  );
5464
- const makeConnections = async (names) => {
5953
+ let lifecycle = Promise.resolve();
5954
+ const serializeLifecycle = (operation) => {
5955
+ const result = lifecycle.then(operation, operation);
5956
+ lifecycle = result.then(
5957
+ () => {
5958
+ },
5959
+ () => {
5960
+ }
5961
+ );
5962
+ return result;
5963
+ };
5964
+ const makeConnections = (names) => serializeLifecycle(async () => {
5465
5965
  await baseMakeConnections(names);
5466
5966
  await cacheInvalidator?.start(collectionRefs);
5467
- };
5468
- const closeConnections2 = async () => {
5967
+ });
5968
+ const closeConnections2 = () => serializeLifecycle(async () => {
5469
5969
  await cacheInvalidator?.stop();
5470
5970
  collectionRefs.length = 0;
5471
- await closeConnections(openedClients);
5472
- };
5971
+ try {
5972
+ await closeConnections(openedClients);
5973
+ } finally {
5974
+ for (const key of Object.keys(db)) delete db[key];
5975
+ }
5976
+ });
5473
5977
  const eachDb = async (fn) => {
5474
5978
  return Promise.all(Object.keys(db).map((key) => fn(db[key], key.replace(/Client$/, ""))));
5475
5979
  };