@ignex/ninox 0.1.5 → 0.1.7

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
@@ -57,6 +57,17 @@ var probeMongoCapabilities = async (client) => {
57
57
  }
58
58
  };
59
59
 
60
+ // src/utils/redact.ts
61
+ var CONNECTION_CREDENTIALS = /([a-z][a-z0-9+.-]*:\/\/[^/\s:@]*):[^/\s@]*@/gi;
62
+ var SECRET_PAIR = /\b(password|passwd|pwd|secret|token|api[-_]?key|access[-_]?key)=([^&\s;,"')]+)/gi;
63
+ var maskCredentials = (value) => value.replace(CONNECTION_CREDENTIALS, "$1:***@");
64
+ var maskSecretPairs = (value) => value.replace(SECRET_PAIR, "$1=***");
65
+ var redactText = (value, max = 300) => {
66
+ const single = maskSecretPairs(maskCredentials(value)).split("\n").find((line) => line.trim().length > 0);
67
+ const text = (single ?? "").trim();
68
+ return text.length <= max ? text : `${text.slice(0, max - 1)}\u2026`;
69
+ };
70
+
60
71
  // src/errors/classes.ts
61
72
  var ERROR_HTTP_STATUS = {
62
73
  BAD_REQUEST: 400,
@@ -68,22 +79,49 @@ var ERROR_HTTP_STATUS = {
68
79
  VALIDATION_FAILED: 422,
69
80
  MONGO_TIMEOUT: 504
70
81
  };
82
+ var STATUS_MESSAGES = {
83
+ 400: "Bad Request",
84
+ 401: "Unauthorized",
85
+ 403: "Forbidden",
86
+ 404: "Not Found",
87
+ 409: "Conflict",
88
+ 422: "Unprocessable Entity",
89
+ 429: "Too Many Requests",
90
+ 500: "Internal Server Error",
91
+ 502: "Bad Gateway",
92
+ 503: "Service Unavailable",
93
+ 504: "Gateway Timeout"
94
+ };
95
+ var genericStatusMessage = (statusCode) => STATUS_MESSAGES[statusCode] ?? (statusCode >= 500 ? "Internal Server Error" : "Request failed");
71
96
  var AppError = class extends Error {
72
97
  code;
73
98
  statusCode;
74
99
  extra;
75
- constructor(code, message, statusCode = 500, extra) {
100
+ /**
101
+ * Whether `message`/`extra` may be sent to a CLIENT.
102
+ *
103
+ * Fail-closed: a 4xx message is the caller's contract, a 5xx message is an
104
+ * operator detail that may quote a driver, a host or a query. Pass `expose`
105
+ * explicitly to override — only for a 5xx whose text was written for callers
106
+ * (e.g. `new InfraError("DRAINING", "queue is draining", extra, true)`).
107
+ */
108
+ expose;
109
+ constructor(code, message, statusCode = 500, extra, expose) {
76
110
  super(message);
77
111
  this.name = "AppError";
78
112
  this.code = code;
79
113
  this.statusCode = statusCode;
114
+ this.expose = expose ?? statusCode < 500;
80
115
  if (extra !== void 0) this.extra = extra;
81
116
  }
82
117
  /**
83
- * Stable, client-safe serialization for framework error middleware: a
84
- * framework can forward `JSON.stringify(error)` (or `error.toJSON()`) straight
85
- * to the client — no parsing of server internals. `stack` is dropped; the
86
- * shape is always `{ name, code, message, statusCode, ...(extra) }`.
118
+ * Full, stack-free shape for LOGS and typed consumers:
119
+ * `{ name, code, message, statusCode, ...(extra) }`.
120
+ *
121
+ * It carries the real message, so it is NOT the client path — use
122
+ * {@link toClientPayload} (or `serializeError`) for anything a caller sees, and
123
+ * prefer `logger.error({ err })` so pino's own serializer keeps the stack in the
124
+ * log rather than in a payload.
87
125
  */
88
126
  toJSON() {
89
127
  return {
@@ -94,22 +132,42 @@ var AppError = class extends Error {
94
132
  ...this.extra !== void 0 ? { extra: this.extra } : {}
95
133
  };
96
134
  }
135
+ /**
136
+ * The client-safe payload — the same shape as {@link toJSON}, except that a
137
+ * non-exposed error's `message` is replaced by the canonical reason phrase and
138
+ * its `extra` is dropped. A framework can forward this (or `serializeError`'s
139
+ * result) without auditing what the message might contain.
140
+ *
141
+ * @param options - `expose: true` forces the real (masked) message + extra.
142
+ * @returns The client payload.
143
+ */
144
+ toClientPayload(options = {}) {
145
+ const expose = options.expose === true || this.expose;
146
+ const payload = {
147
+ name: this.name,
148
+ code: this.code,
149
+ message: expose ? maskCredentials(this.message) : genericStatusMessage(this.statusCode),
150
+ statusCode: this.statusCode
151
+ };
152
+ if (expose && this.extra !== void 0) payload.extra = this.extra;
153
+ return payload;
154
+ }
97
155
  };
98
156
  var DomainError = class extends AppError {
99
- constructor(code, message, extra) {
100
- super(code, message, ERROR_HTTP_STATUS[code] ?? 400, extra);
157
+ constructor(code, message, extra, expose) {
158
+ super(code, message, ERROR_HTTP_STATUS[code] ?? 400, extra, expose);
101
159
  this.name = "DomainError";
102
160
  }
103
161
  };
104
162
  var InfraError = class extends AppError {
105
- constructor(code, message, extra) {
106
- super(code, message, ERROR_HTTP_STATUS[code] ?? 500, extra);
163
+ constructor(code, message, extra, expose) {
164
+ super(code, message, ERROR_HTTP_STATUS[code] ?? 500, extra, expose);
107
165
  this.name = "InfraError";
108
166
  }
109
167
  };
110
168
  var BadRequest = class extends DomainError {
111
- constructor(message, extra) {
112
- super("BAD_REQUEST", message, extra);
169
+ constructor(message, extra, expose) {
170
+ super("BAD_REQUEST", message, extra, expose);
113
171
  this.name = "BadRequest";
114
172
  }
115
173
  };
@@ -279,13 +337,20 @@ var httpStatusForError = (err) => {
279
337
  if (isMongoTransientError(err)) return 503;
280
338
  return 500;
281
339
  };
282
- var serializeError = (err, ctx = {}) => {
340
+ var serializeError = (err, ctx = {}, options = {}) => {
283
341
  const mapped = mapMongoDriverError(err, ctx);
284
- if (mapped instanceof AppError) return mapped.toJSON();
285
- if (mapped instanceof Error) {
286
- return { name: mapped.name, code: "UNKNOWN", message: mapped.message, statusCode: 500 };
342
+ if (mapped instanceof AppError) return mapped.toClientPayload(options);
343
+ const statusCode = httpStatusForError(mapped);
344
+ const name = mapped instanceof Error ? mapped.name : "UnknownError";
345
+ if (options.expose !== true) {
346
+ return { name, code: "UNKNOWN", message: genericStatusMessage(statusCode), statusCode };
287
347
  }
288
- return { name: "UnknownError", code: "UNKNOWN", message: String(mapped), statusCode: 500 };
348
+ return {
349
+ name,
350
+ code: "UNKNOWN",
351
+ message: redactText(mapped instanceof Error ? mapped.message : String(mapped)),
352
+ statusCode
353
+ };
289
354
  };
290
355
  var cloneDeep = (value) => {
291
356
  if (value instanceof Date) return new Date(value.getTime());
@@ -525,6 +590,7 @@ var RefreshTicker = class {
525
590
  this.host = host;
526
591
  this.intervalMs = tickIntervalMs;
527
592
  }
593
+ host;
528
594
  timer;
529
595
  intervalMs;
530
596
  /** Whether the timer is currently running. */
@@ -618,16 +684,26 @@ var normalizeWatchId = (id) => {
618
684
  };
619
685
 
620
686
  // src/utils/watch-errors.ts
621
- var isPermanentWatchError = (message) => (
622
- // Standalone server: `$changeStream` is not supported at all.
623
- /only supported on replica sets/i.test(message) || // Credentials/authorization problems: the user cannot run `aggregate` on
624
- // this collection/server. Wrong creds, missing roles, or a user created
625
- // without the `read`/`changeStream` privilege will never start working by
626
- // retrying — fail once and stop instead of retrying forever.
627
- /requires authentication|not authorized|unauthorized|authentication failed|Command .* requires authentication/i.test(
628
- message
629
- )
630
- );
687
+ var STANDALONE = /only supported on replica sets|not supported on (?:a )?standalone/i;
688
+ var CREDENTIALS = /requires authentication|not authorized|unauthorized|authentication failed|Command .* requires authentication/i;
689
+ var explainPermanentWatchError = (message) => {
690
+ if (STANDALONE.test(message)) {
691
+ return {
692
+ reason: "standalone",
693
+ headline: "change streams need a replica set",
694
+ hint: "Point the connection at a replica set (a single-node replica set is fine for local dev), or set cacheWatch: false / cache: { ttlMs } to run write-through only."
695
+ };
696
+ }
697
+ if (CREDENTIALS.test(message)) {
698
+ return {
699
+ reason: "credentials",
700
+ headline: "the connection is not authorized for change streams",
701
+ hint: "Change streams need an authenticated user with the read + changeStream privilege (usually granted on the authSource database) \u2014 fix the credentials in .env, then restart."
702
+ };
703
+ }
704
+ return void 0;
705
+ };
706
+ var isPermanentWatchError = (message) => explainPermanentWatchError(message) !== void 0;
631
707
 
632
708
  // src/cache/hot-cache/watcher.ts
633
709
  var resolveDbSafe = (ref, logger) => {
@@ -649,6 +725,7 @@ var WatchCoordinator = class {
649
725
  constructor(host) {
650
726
  this.host = host;
651
727
  }
728
+ host;
652
729
  streams = /* @__PURE__ */ new Map();
653
730
  failedStreams = /* @__PURE__ */ new Set();
654
731
  stopped = true;
@@ -762,7 +839,7 @@ var WatchCoordinator = class {
762
839
  }
763
840
  }
764
841
  if (isPermanentWatchError(message)) {
765
- this.markStreamFailed(key);
842
+ this.markStreamFailed(key, explainPermanentWatchError(message));
766
843
  return;
767
844
  }
768
845
  }
@@ -771,12 +848,15 @@ var WatchCoordinator = class {
771
848
  await sleepJittered(backoff, 1e3, true);
772
849
  }
773
850
  }
774
- markStreamFailed(key) {
851
+ markStreamFailed(key, verdict) {
775
852
  this.failedStreams.add(key);
776
853
  if (!this.host.isReplica()) return;
777
854
  const planned = this.plannedStreamKeys();
778
855
  if (planned.size > 0 && [...planned].every((k) => this.failedStreams.has(k))) {
779
- this.host.logger.warn?.({}, "hot cache: change streams unsupported, falling back to ticker");
856
+ this.host.logger.warn?.(
857
+ verdict === void 0 ? {} : { reason: verdict.reason, hint: verdict.hint },
858
+ `hot cache: change streams unsupported${verdict === void 0 ? "" : ` \u2014 ${verdict.headline}`}, falling back to ticker`
859
+ );
780
860
  this.host.fallbackToStandalone();
781
861
  }
782
862
  }
@@ -1787,6 +1867,7 @@ var QueryBuilder = class _QueryBuilder {
1787
1867
  constructor(ctx) {
1788
1868
  this.ctx = ctx;
1789
1869
  }
1870
+ ctx;
1790
1871
  _filter = {};
1791
1872
  _sort;
1792
1873
  _skip;
@@ -2814,13 +2895,19 @@ var CacheInvalidator = class {
2814
2895
  } catch {
2815
2896
  }
2816
2897
  }
2817
- if (isPermanentWatchError(message)) {
2898
+ const verdict = explainPermanentWatchError(message);
2899
+ if (verdict) {
2818
2900
  this.disabled = true;
2819
2901
  if (!this.warnedUnsupported) {
2820
2902
  this.warnedUnsupported = true;
2821
2903
  this.logger.warn?.(
2822
- { collection: ref.collection, error: message },
2823
- "cacheWatch: change streams unavailable permanently \u2014 the query cache stays write-through only; set cache: { ttlMs } or disable cacheWatch"
2904
+ {
2905
+ collection: ref.collection,
2906
+ reason: verdict.reason,
2907
+ error: message,
2908
+ hint: verdict.hint
2909
+ },
2910
+ `cacheWatch: disabled \u2014 ${verdict.headline}; the query cache stays write-through only`
2824
2911
  );
2825
2912
  }
2826
2913
  return;
@@ -3142,13 +3229,28 @@ var probeDebugQuery = async () => {
3142
3229
  debugQueryCache = null;
3143
3230
  }
3144
3231
  };
3232
+ var sanitizeDebugParams = (value) => {
3233
+ try {
3234
+ return JSON.parse(JSON.stringify(value));
3235
+ } catch {
3236
+ try {
3237
+ return String(value);
3238
+ } catch {
3239
+ return "[unserializable]";
3240
+ }
3241
+ }
3242
+ };
3145
3243
  var traceDbOp = async (logger, meta, fn, options = {}) => {
3146
3244
  const start = performance.now();
3147
3245
  logger.debug({ ...meta, operation: "start" }, "start");
3148
3246
  try {
3149
- if (debugQueryCache === void 0) await probeDebugQuery();
3150
- const debugQuery = debugQueryCache;
3151
- const result = debugQuery ? await debugQuery(`${meta.collection}.${meta.op}`, void 0, () => fn()) : await fn();
3247
+ let debugQuery = options.debugQuery;
3248
+ if (debugQuery === void 0) {
3249
+ if (debugQueryCache === void 0) await probeDebugQuery();
3250
+ debugQuery = debugQueryCache;
3251
+ }
3252
+ const params = meta.params === void 0 ? void 0 : sanitizeDebugParams(meta.params);
3253
+ const result = debugQuery ? await debugQuery(`${meta.collection}.${meta.op}`, params, () => fn()) : await fn();
3152
3254
  const durationMs = performance.now() - start;
3153
3255
  logger.info({ ...meta, durationMs }, "ok");
3154
3256
  return result;
@@ -3164,11 +3266,12 @@ var traceDbOp = async (logger, meta, fn, options = {}) => {
3164
3266
  // src/service/op-deps.ts
3165
3267
  var makeOpDeps = (logger, dbLabel, resolve, wrapMongoErrors) => {
3166
3268
  const trace = (meta2, fn) => traceDbOp(logger, meta2, fn, { wrapMongoErrors });
3167
- const meta = (collection, op) => ({
3269
+ const meta = (collection, op, params) => ({
3168
3270
  collection: String(collection),
3169
3271
  physicalCollection: resolve(String(collection)),
3170
3272
  db: dbLabel,
3171
- op
3273
+ op,
3274
+ ...params !== void 0 ? { params } : {}
3172
3275
  });
3173
3276
  return { trace, meta };
3174
3277
  };
@@ -3214,11 +3317,11 @@ var resolveDriftMode = (perOp, serviceDefault) => {
3214
3317
  };
3215
3318
 
3216
3319
  // src/service/crud-op.ts
3217
- var defineCrudOp = async (deps2, collection, opName, execute, options, write = false) => {
3320
+ var defineCrudOp = async (deps2, collection, opName, execute, options, write = false, params) => {
3218
3321
  const resolved = resolveQueryOptions(options);
3219
3322
  const maxAttempts = write && resolved.retryWrites !== true ? 1 : resolved.maxAttempts;
3220
3323
  return deps2.trace(
3221
- deps2.meta(collection, opName),
3324
+ deps2.meta(collection, opName, params),
3222
3325
  async () => withRetry(async () => execute(resolved), {
3223
3326
  maxAttempts,
3224
3327
  delayMs: resolved.retryDelayMs
@@ -3232,6 +3335,8 @@ var PipelineBuilder = class _PipelineBuilder {
3232
3335
  this.deps = deps2;
3233
3336
  this.stages = stages;
3234
3337
  }
3338
+ deps;
3339
+ stages;
3235
3340
  /** Raw accumulated stages — embeddable into `$lookup` / `$facet` branches. */
3236
3341
  raw() {
3237
3342
  return this.stages;
@@ -3323,7 +3428,9 @@ var PipelineBuilder = class _PipelineBuilder {
3323
3428
  this.deps.logical,
3324
3429
  "mongo.pipeline",
3325
3430
  run,
3326
- this.deps.options
3431
+ this.deps.options,
3432
+ false,
3433
+ { pipeline: this.stages }
3327
3434
  );
3328
3435
  }
3329
3436
  /** Run the pipeline and return the first output document (or null). */
@@ -3350,7 +3457,9 @@ var PipelineBuilder = class _PipelineBuilder {
3350
3457
  this.deps.logical,
3351
3458
  "mongo.pipeline.first",
3352
3459
  run,
3353
- this.deps.options
3460
+ this.deps.options,
3461
+ false,
3462
+ { pipeline: this.stages }
3354
3463
  );
3355
3464
  }
3356
3465
  /** Streaming cursor (raw driver cursor; no retry / trace). */
@@ -3470,7 +3579,9 @@ var makeAggregateOps = (ctx) => {
3470
3579
  collection,
3471
3580
  "mongo.aggregate",
3472
3581
  (r) => coll(collection).aggregate(pipeline2, mergeAggOptions2(r.driverOpts, r.sdk)),
3473
- options
3582
+ options,
3583
+ false,
3584
+ { pipeline: pipeline2 }
3474
3585
  );
3475
3586
  };
3476
3587
  const pipeline = (collection, options) => new PipelineBuilder({
@@ -3556,7 +3667,7 @@ var createCachedAggregate = (ctx) => {
3556
3667
  const noSession = !resolved.sdk.session;
3557
3668
  const useCache = cache !== void 0 && noSession && resolved.cache !== false && isCacheablePipeline(pipeline);
3558
3669
  const shouldDedupe = resolved.dedupe !== false && (resolved.dedupe === true || opts.dedupeReads === true && noSession) && opts.inFlight !== void 0;
3559
- const runDriver = () => defineCrudOp(deps2, collection, opName, execute, options);
3670
+ const runDriver = () => defineCrudOp(deps2, collection, opName, execute, options, false, { pipeline });
3560
3671
  const key = useCache ? cache.key(
3561
3672
  cacheCollectionKey(client.databaseName, physical),
3562
3673
  stableHash([opName, pipeline, options])
@@ -3905,8 +4016,8 @@ var createCrudContext = (client, dbLabel, logger, opts) => {
3905
4016
  );
3906
4017
  const coll = (name) => client.collection(resolve(String(name)));
3907
4018
  const toDriverFilter = (filter) => filter;
3908
- const run = (collection, opName, execute, options) => defineCrudOp(deps2, collection, opName, execute, options);
3909
- const writeRun = (collection, opName, execute, options) => defineCrudOp(deps2, collection, opName, execute, options, true);
4019
+ const run = (collection, opName, execute, options, params) => defineCrudOp(deps2, collection, opName, execute, options, false, params);
4020
+ const writeRun = (collection, opName, execute, options, params) => defineCrudOp(deps2, collection, opName, execute, options, true, params);
3910
4021
  const invalidate = (collection) => {
3911
4022
  opts.cache?.invalidateByCollection(
3912
4023
  cacheCollectionKey(client.databaseName, resolve(String(collection)))
@@ -3942,6 +4053,9 @@ var createCrudContext = (client, dbLabel, logger, opts) => {
3942
4053
  const read = async (collection, opName, filter, options, execute, shape = "none", keyExtra) => {
3943
4054
  const physical = resolve(String(collection));
3944
4055
  const resolved = resolveQueryOptions(options);
4056
+ const sentParams = { filter };
4057
+ if (Object.keys(resolved.driverOpts).length > 0) sentParams.options = resolved.driverOpts;
4058
+ if (keyExtra !== void 0) sentParams.field = keyExtra;
3945
4059
  const noSession = !resolved.sdk.session;
3946
4060
  const useCache = opts.cache !== void 0 && noSession && resolved.cache !== false;
3947
4061
  const shouldDedupe = resolved.dedupe !== false && (resolved.dedupe === true || opts.dedupeReads === true && noSession) && opts.inFlight !== void 0;
@@ -3963,7 +4077,7 @@ var createCrudContext = (client, dbLabel, logger, opts) => {
3963
4077
  );
3964
4078
  };
3965
4079
  const executeWrapped = () => deps2.trace(
3966
- deps2.meta(collection, opName),
4080
+ deps2.meta(collection, opName, sentParams),
3967
4081
  () => withRetry(() => execute(resolved), {
3968
4082
  maxAttempts: resolved.maxAttempts,
3969
4083
  delayMs: resolved.retryDelayMs
@@ -4292,7 +4406,8 @@ var makeWriteOps = (ctx, readOps) => {
4292
4406
  insertOpts
4293
4407
  );
4294
4408
  },
4295
- options
4409
+ options,
4410
+ { doc }
4296
4411
  );
4297
4412
  invalidate(collection);
4298
4413
  await safeAfterHooks(String(collection), "afterCreate", {
@@ -4325,7 +4440,8 @@ var makeWriteOps = (ctx, readOps) => {
4325
4440
  bulkOpts
4326
4441
  );
4327
4442
  },
4328
- options
4443
+ options,
4444
+ { docs }
4329
4445
  );
4330
4446
  invalidate(collection);
4331
4447
  await safeAfterHooks(String(collection), "afterCreate", {
@@ -4354,7 +4470,8 @@ var makeWriteOps = (ctx, readOps) => {
4354
4470
  updateOpts
4355
4471
  );
4356
4472
  },
4357
- options
4473
+ options,
4474
+ { filter, update: effective }
4358
4475
  );
4359
4476
  invalidate(collection);
4360
4477
  await safeAfterHooks(String(collection), "afterUpdate", {
@@ -4383,7 +4500,8 @@ var makeWriteOps = (ctx, readOps) => {
4383
4500
  updateOpts
4384
4501
  );
4385
4502
  },
4386
- options
4503
+ options,
4504
+ { filter, update: effective }
4387
4505
  );
4388
4506
  invalidate(collection);
4389
4507
  await safeAfterHooks(String(collection), "afterUpdate", {
@@ -4416,7 +4534,8 @@ var makeWriteOps = (ctx, readOps) => {
4416
4534
  fOpts
4417
4535
  );
4418
4536
  },
4419
- options
4537
+ options,
4538
+ { filter, update: effective }
4420
4539
  );
4421
4540
  invalidate(collection);
4422
4541
  await safeAfterHooks(String(collection), "afterUpdate", {
@@ -4449,7 +4568,8 @@ var makeWriteOps = (ctx, readOps) => {
4449
4568
  fOpts
4450
4569
  );
4451
4570
  },
4452
- options
4571
+ options,
4572
+ { filter, replacement: effective }
4453
4573
  );
4454
4574
  invalidate(collection);
4455
4575
  await safeAfterHooks(String(collection), "afterUpdate", {
@@ -4477,7 +4597,8 @@ var makeWriteOps = (ctx, readOps) => {
4477
4597
  repOpts
4478
4598
  );
4479
4599
  },
4480
- options
4600
+ options,
4601
+ { filter, replacement }
4481
4602
  );
4482
4603
  invalidate(collection);
4483
4604
  await safeAfterHooks(String(collection), "afterUpdate", {
@@ -4501,7 +4622,8 @@ var makeWriteOps = (ctx, readOps) => {
4501
4622
  };
4502
4623
  return await coll(collection).deleteOne(toDriverFilter(filter), delOpts);
4503
4624
  },
4504
- options
4625
+ options,
4626
+ { filter }
4505
4627
  );
4506
4628
  invalidate(collection);
4507
4629
  await safeAfterHooks(String(collection), "afterDelete", {
@@ -4525,7 +4647,8 @@ var makeWriteOps = (ctx, readOps) => {
4525
4647
  };
4526
4648
  return await coll(collection).deleteMany(toDriverFilter(filter), delOpts);
4527
4649
  },
4528
- options
4650
+ options,
4651
+ { filter }
4529
4652
  );
4530
4653
  invalidate(collection);
4531
4654
  await safeAfterHooks(String(collection), "afterDelete", {
@@ -4554,7 +4677,8 @@ var makeWriteOps = (ctx, readOps) => {
4554
4677
  fOpts
4555
4678
  );
4556
4679
  },
4557
- options
4680
+ options,
4681
+ { filter }
4558
4682
  );
4559
4683
  invalidate(collection);
4560
4684
  await safeAfterHooks(String(collection), "afterDelete", {
@@ -4586,7 +4710,8 @@ var makeWriteOps = (ctx, readOps) => {
4586
4710
  updateOpts
4587
4711
  );
4588
4712
  },
4589
- options
4713
+ options,
4714
+ { filter, update: effective }
4590
4715
  );
4591
4716
  invalidate(collection);
4592
4717
  await safeAfterHooks(String(collection), "afterDelete", {
@@ -4612,7 +4737,8 @@ var makeWriteOps = (ctx, readOps) => {
4612
4737
  };
4613
4738
  return await coll(collection).updateOne(toDriverFilter(filter), formatted, updateOpts);
4614
4739
  },
4615
- options
4740
+ options,
4741
+ { filter, update: formatted }
4616
4742
  );
4617
4743
  invalidate(collection);
4618
4744
  await safeAfterHooks(String(collection), "afterUpdate", {
@@ -4644,7 +4770,8 @@ var makeWriteOps = (ctx, readOps) => {
4644
4770
  };
4645
4771
  return await coll(collection).bulkWrite(writes, bulkOpts);
4646
4772
  },
4647
- options
4773
+ options,
4774
+ { operations }
4648
4775
  );
4649
4776
  invalidate(collection);
4650
4777
  return result;
@@ -4665,7 +4792,8 @@ var makeWriteOps = (ctx, readOps) => {
4665
4792
  };
4666
4793
  return await coll(collection).bulkWrite(operations, bulkOpts);
4667
4794
  },
4668
- options
4795
+ options,
4796
+ { operations }
4669
4797
  );
4670
4798
  invalidate(collection);
4671
4799
  return result;
@@ -4848,7 +4976,9 @@ var makePaginateCursor = (ctx) => {
4848
4976
  };
4849
4977
  return client.collection(resolve(String(collection))).find(filter, findOpts).sort(effectiveSort).limit(limit + 1).toArray();
4850
4978
  },
4851
- config.queryOptions
4979
+ config.queryOptions,
4980
+ false,
4981
+ { filter, sort: effectiveSort, limit }
4852
4982
  );
4853
4983
  const hasMore = rows.length > limit;
4854
4984
  const page = backwards ? rows.slice(0, limit).reverse() : rows.slice(0, limit);
@@ -4974,40 +5104,63 @@ var optionSignature = (o) => JSON.stringify([
4974
5104
  ]);
4975
5105
  var indexKeyOf = (key, options) => `${JSON.stringify(Object.keys(key).map((k) => [k, key[k]]))}|${optionSignature(options)}`;
4976
5106
  var makeSchemaOps = (client, _logger, opts) => {
5107
+ const readCollectionValidator = async (physical) => {
5108
+ const info = await client.listCollections({ name: physical }).next();
5109
+ return info?.options?.validator;
5110
+ };
5111
+ const ensureDeclaredIndexes = async (collection, physical, def) => {
5112
+ const declared = def?.indexes ?? [];
5113
+ if (declared.length === 0) return;
5114
+ const handle = client.collection(physical);
5115
+ const existing = new Set(
5116
+ (await handle.listIndexes().toArray()).filter((i) => i.name !== "_id_").map((i) => indexKeyOf(i.key, i))
5117
+ );
5118
+ for (const index of declared) {
5119
+ if (existing.has(indexKeyOf(index.key, index.options))) continue;
5120
+ try {
5121
+ await handle.createIndex(index.key, index.options);
5122
+ } catch (err) {
5123
+ if (err?.code === 85) {
5124
+ throw new Error(
5125
+ `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
+ { cause: err }
5127
+ );
5128
+ }
5129
+ throw err;
5130
+ }
5131
+ }
5132
+ };
4977
5133
  const createSchema = async (collection, createCollectionOptions) => {
4978
5134
  const physical = opts.resolveCollectionName(collection);
4979
5135
  const def = opts.getDefinition(collection);
4980
- const validator = def?.schema ? toMongoValidator(def.schema) : void 0;
5136
+ const createOptions = {
5137
+ ...createCollectionOptions ?? {},
5138
+ ...def?.schema ? { validator: toMongoValidator(def.schema) } : {}
5139
+ };
5140
+ let created = true;
4981
5141
  try {
4982
- await client.createCollection(physical, {
4983
- // Caller options FIRST, derived validator LAST — a stale `validator`
4984
- // key in the options must not clobber the schema-derived one.
4985
- ...createCollectionOptions ?? {},
4986
- ...validator ? { validator } : {}
4987
- });
5142
+ await client.createCollection(physical, createOptions);
4988
5143
  } catch (err) {
4989
- if (err?.code === 48) {
4990
- throw new DomainError("COLLECTION_EXISTS", `Collection "${collection}" already exists`, {
4991
- physicalCollection: physical
4992
- });
4993
- }
4994
- throw err;
5144
+ if (err?.code !== 48) throw err;
5145
+ created = false;
4995
5146
  }
4996
- const indexes = def?.indexes ?? [];
4997
- if (indexes.length > 0) {
5147
+ if (created) {
4998
5148
  try {
4999
- await client.collection(physical).createIndexes(
5000
- indexes.map((index) => ({
5001
- key: index.key,
5002
- ...index.options ?? {}
5003
- }))
5004
- );
5149
+ await ensureDeclaredIndexes(collection, physical, def);
5005
5150
  } catch (err) {
5006
5151
  await client.dropCollection(physical).catch(() => {
5007
5152
  });
5008
5153
  throw err;
5009
5154
  }
5155
+ return;
5156
+ }
5157
+ 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 });
5161
+ }
5010
5162
  }
5163
+ await ensureDeclaredIndexes(collection, physical, def);
5011
5164
  };
5012
5165
  const updateSchema = async (collection, schema) => {
5013
5166
  const physical = opts.resolveCollectionName(collection);