monsqlize 3.3.3 → 3.4.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.
@@ -66,6 +66,13 @@ var ErrorCodes = {
66
66
  INVALID_OPERATION: "INVALID_OPERATION",
67
67
  /** Invalid local Vector Search options before the driver is called. */
68
68
  INVALID_VECTOR_SEARCH: "INVALID_VECTOR_SEARCH",
69
+ /** Search index lifecycle errors (v3.4.0). */
70
+ INVALID_SEARCH_INDEX_DEFINITION: "INVALID_SEARCH_INDEX_DEFINITION",
71
+ SEARCH_INDEX_EXISTS: "SEARCH_INDEX_EXISTS",
72
+ SEARCH_INDEX_NOT_FOUND: "SEARCH_INDEX_NOT_FOUND",
73
+ SEARCH_INDEX_UNSUPPORTED: "SEARCH_INDEX_UNSUPPORTED",
74
+ SEARCH_INDEX_BUILD_FAILED: "SEARCH_INDEX_BUILD_FAILED",
75
+ SEARCH_INDEX_WAIT_TIMEOUT: "SEARCH_INDEX_WAIT_TIMEOUT",
69
76
  /** Relation select is outside the supported inclusion-only top-level contract. */
70
77
  INVALID_RELATION_SELECT: "INVALID_RELATION_SELECT",
71
78
  /** A protected delete found declared Model relations that still reference its target. */
@@ -998,31 +1005,31 @@ function normalizeRelationSelect(value, context = {}) {
998
1005
  if (value === void 0) {
999
1006
  return void 0;
1000
1007
  }
1001
- const invalid2 = (reason) => {
1008
+ const invalid3 = (reason) => {
1002
1009
  throw createError(
1003
1010
  ErrorCodes.INVALID_RELATION_SELECT,
1004
1011
  `Relation select must use non-empty top-level inclusion fields: ${reason}`,
1005
1012
  [{ ...context, value, reason }]
1006
1013
  );
1007
1014
  };
1008
- const rawTokens = typeof value === "string" ? [value] : Array.isArray(value) ? value : invalid2("select must be a string or string array");
1015
+ const rawTokens = typeof value === "string" ? [value] : Array.isArray(value) ? value : invalid3("select must be a string or string array");
1009
1016
  const tokens = [];
1010
1017
  for (const rawToken of rawTokens) {
1011
1018
  if (typeof rawToken !== "string") {
1012
- invalid2("select array entries must be strings");
1019
+ invalid3("select array entries must be strings");
1013
1020
  }
1014
1021
  const parts = rawToken.trim().split(/\s+/).filter(Boolean);
1015
1022
  if (parts.length === 0) {
1016
- invalid2("select must not be empty");
1023
+ invalid3("select must not be empty");
1017
1024
  }
1018
1025
  tokens.push(...parts);
1019
1026
  }
1020
1027
  for (const token of tokens) {
1021
1028
  if (token.startsWith("-") || token.startsWith("+")) {
1022
- invalid2("exclusion and prefix tokens are not supported");
1029
+ invalid3("exclusion and prefix tokens are not supported");
1023
1030
  }
1024
1031
  if (token.includes(".")) {
1025
- invalid2("dot-path fields are not supported");
1032
+ invalid3("dot-path fields are not supported");
1026
1033
  }
1027
1034
  }
1028
1035
  return [...new Set(tokens)];
@@ -3577,8 +3584,8 @@ function normalizeExtensionValue(value, stack) {
3577
3584
  return `circular:${getExtensionObjectKey(objectValue)}`;
3578
3585
  }
3579
3586
  const prototype = Object.getPrototypeOf(objectValue);
3580
- const isPlainObject4 = prototype === Object.prototype || prototype === null;
3581
- if (!Array.isArray(objectValue) && !isPlainObject4) {
3587
+ const isPlainObject5 = prototype === Object.prototype || prototype === null;
3588
+ if (!Array.isArray(objectValue) && !isPlainObject5) {
3582
3589
  return `object:${prototype?.constructor?.name ?? "unknown"}:${getExtensionObjectKey(objectValue)}`;
3583
3590
  }
3584
3591
  stack.add(objectValue);
@@ -6692,6 +6699,28 @@ var ModelInstance = class {
6692
6699
  dropIndexes() {
6693
6700
  return this.runModelWrite(() => this.collection.dropIndexes());
6694
6701
  }
6702
+ /** Search index lifecycle (v3.4.0): writes run as model writes for writePathPolicy. */
6703
+ createSearchIndex(spec, options) {
6704
+ return this.runModelWrite(() => this.collection.createSearchIndex(spec, options));
6705
+ }
6706
+ createSearchIndexes(specs) {
6707
+ return this.runModelWrite(() => this.collection.createSearchIndexes(specs));
6708
+ }
6709
+ listSearchIndexes(options) {
6710
+ return this.collection.listSearchIndexes(options);
6711
+ }
6712
+ getSearchIndex(name) {
6713
+ return this.collection.getSearchIndex(name);
6714
+ }
6715
+ updateSearchIndex(name, definition) {
6716
+ return this.runModelWrite(() => this.collection.updateSearchIndex(name, definition));
6717
+ }
6718
+ dropSearchIndex(name, options) {
6719
+ return this.runModelWrite(() => this.collection.dropSearchIndex(name, options));
6720
+ }
6721
+ waitForSearchIndex(name, options) {
6722
+ return this.collection.waitForSearchIndex(name, options);
6723
+ }
6695
6724
  prewarmBookmarks(keyDims, pages) {
6696
6725
  return this.extendedCollection().prewarmBookmarks(keyDims, pages);
6697
6726
  }
@@ -8248,18 +8277,26 @@ var HealthChecker = class {
8248
8277
  const status = this._healthStatus.get(poolName);
8249
8278
  if (!status) return;
8250
8279
  const client = this._clients.get(poolName) ?? this._poolManager?._getPool(poolName);
8251
- if (this._inProgress.get(poolName)?.client === client && this._inProgress.has(poolName)) return;
8280
+ const pending = this._inProgress.get(poolName);
8281
+ if (pending && pending.client === client && pending.awaited) return;
8252
8282
  const epoch = this._epochs.get(poolName);
8253
- const promise = Promise.resolve().then(() => {
8254
- if (this._epochs.get(poolName) !== epoch || this._healthStatus.get(poolName) !== status) return;
8255
- return this._pingOverride ? this._pingOverride(poolName) : this._pingPool(poolName);
8256
- });
8257
- const operation = { client, promise };
8258
- this._inProgress.set(poolName, operation);
8259
- const clearPending = () => {
8260
- if (this._inProgress.get(poolName) === operation) this._inProgress.delete(poolName);
8261
- };
8262
- void promise.then(clearPending, clearPending);
8283
+ let operation;
8284
+ if (pending && pending.client === client) {
8285
+ operation = pending;
8286
+ } else {
8287
+ const promise2 = Promise.resolve().then(() => {
8288
+ if (this._epochs.get(poolName) !== epoch || this._healthStatus.get(poolName) !== status) return;
8289
+ return this._pingOverride ? this._pingOverride(poolName) : this._pingPool(poolName);
8290
+ });
8291
+ operation = { client, promise: promise2, awaited: true };
8292
+ this._inProgress.set(poolName, operation);
8293
+ const clearPending = () => {
8294
+ if (this._inProgress.get(poolName) === operation) this._inProgress.delete(poolName);
8295
+ };
8296
+ void promise2.then(clearPending, clearPending);
8297
+ }
8298
+ operation.awaited = true;
8299
+ const promise = operation.promise;
8263
8300
  let timer;
8264
8301
  let lastError = null;
8265
8302
  try {
@@ -8271,6 +8308,7 @@ var HealthChecker = class {
8271
8308
  lastError = error instanceof Error ? error : new Error(String(error));
8272
8309
  } finally {
8273
8310
  if (timer) clearTimeout(timer);
8311
+ operation.awaited = false;
8274
8312
  }
8275
8313
  if (this._epochs.get(poolName) !== epoch || this._healthStatus.get(poolName) !== status) return;
8276
8314
  status.lastCheck = /* @__PURE__ */ new Date();
@@ -10728,27 +10766,28 @@ function normalizeFindPageLimit(limit, defaults2) {
10728
10766
  }
10729
10767
  return rawLimit;
10730
10768
  }
10731
- function documentReadKey(operation, collection, defaults2, query, options, field) {
10732
- const captured = snapshotReadInputs(normalizeQueryForCache(query, defaults2), options, collection.bsonOptions);
10769
+ function documentReadKey(operation, collection, defaults2, query, options, field, queryPrepared = false) {
10770
+ const keyQuery = queryPrepared ? query ?? {} : normalizeQueryForCache(query, defaults2);
10771
+ const captured = snapshotReadInputs(keyQuery, options, collection.bsonOptions);
10733
10772
  const keyOptions = buildResultCacheKeyOptions(captured.options, collection.bsonOptions);
10734
10773
  if (operation === "count") delete keyOptions.fieldsAsRaw;
10735
10774
  const body = field === void 0 ? stableCacheKeyString(captured.query, "document") : stableCacheKeyString({ key: field, query: captured.query });
10736
10775
  return `${operation}:${buildCollectionCacheNamespace(collection, defaults2)}:${body}:${stableCacheKeyString(keyOptions)}`;
10737
10776
  }
10738
- function buildFindCacheKey(collection, defaults2 = {}, query, options) {
10777
+ function buildFindCacheKey(collection, defaults2 = {}, query, options, queryPrepared = false) {
10739
10778
  return documentReadKey("find", collection, defaults2, query, normalizeFindProjectionOptions({
10740
10779
  limit: defaults2.findLimit,
10741
10780
  ...options ?? {}
10742
- }));
10781
+ }), void 0, queryPrepared);
10743
10782
  }
10744
- function buildFindOneCacheKey(collection, defaults2 = {}, query, options) {
10745
- return documentReadKey("findOne", collection, defaults2, query, normalizeFindProjectionOptions(options));
10783
+ function buildFindOneCacheKey(collection, defaults2 = {}, query, options, queryPrepared = false) {
10784
+ return documentReadKey("findOne", collection, defaults2, query, normalizeFindProjectionOptions(options), void 0, queryPrepared);
10746
10785
  }
10747
- function buildCountCacheKey(collection, defaults2 = {}, query, options) {
10748
- return documentReadKey("count", collection, defaults2, query, options);
10786
+ function buildCountCacheKey(collection, defaults2 = {}, query, options, queryPrepared = false) {
10787
+ return documentReadKey("count", collection, defaults2, query, options, void 0, queryPrepared);
10749
10788
  }
10750
- function buildDistinctCacheKey(collection, defaults2 = {}, field, query, options) {
10751
- return documentReadKey("distinct", collection, defaults2, query, options, field);
10789
+ function buildDistinctCacheKey(collection, defaults2 = {}, field, query, options, queryPrepared = false) {
10790
+ return documentReadKey("distinct", collection, defaults2, query, options, field, queryPrepared);
10752
10791
  }
10753
10792
  function buildPreparedAggregateCacheKey(collection, defaults2 = {}, pipeline = [], options) {
10754
10793
  const namespace = buildCollectionCacheNamespace(collection, defaults2);
@@ -11760,7 +11799,7 @@ var FindChain = class {
11760
11799
  const cacheTTL = typeof this.options.cache === "number" ? this.options.cache : 0;
11761
11800
  const executeOptions = prepared.options;
11762
11801
  if (cacheTTL > 0 && this.queryCache && canCacheBsonResult(executeOptions) && !hasSessionOption(executeOptions) && !await isCollectionCacheBarrierActive(this.queryCache, this.collection, this.defaults)) {
11763
- const cacheKey = buildFindCacheKey(this.collection, this.defaults, prepared.query, executeOptions);
11802
+ const cacheKey = buildFindCacheKey(this.collection, this.defaults, prepared.query, executeOptions, true);
11764
11803
  const result = await readThroughCache(this.queryCache, cacheKey, cacheTTL, () => this.runToArray(prepared));
11765
11804
  return this.wrapResult("find", startTs, result);
11766
11805
  }
@@ -12101,6 +12140,432 @@ function mapVectorSearchRows(rows) {
12101
12140
  });
12102
12141
  }
12103
12142
 
12143
+ // src/adapters/mongodb/search-indexes/errors.ts
12144
+ var CODE_INDEX_ALREADY_EXISTS = 68;
12145
+ var CODE_INDEX_NOT_FOUND = 27;
12146
+ var CODE_NAMESPACE_NOT_FOUND = 26;
12147
+ var CODE_COMMAND_NOT_FOUND = 59;
12148
+ var CODE_COMMAND_NOT_SUPPORTED = 115;
12149
+ var CODE_LIST_SEARCH_ATLAS_ONLY = 6047401;
12150
+ var UNSUPPORTED_MESSAGE = /search index commands are only supported|\$listSearchIndexes stage is only allowed/i;
12151
+ function readCode(error) {
12152
+ const e = error;
12153
+ return {
12154
+ code: typeof e?.code === "number" ? e.code : void 0,
12155
+ codeName: typeof e?.codeName === "string" ? e.codeName : void 0,
12156
+ message: typeof e?.message === "string" ? e.message : String(error)
12157
+ };
12158
+ }
12159
+ function classifySearchIndexError(error) {
12160
+ const { code, codeName, message } = readCode(error);
12161
+ if (code === CODE_INDEX_ALREADY_EXISTS || codeName === "IndexAlreadyExists") return "exists";
12162
+ if (code === CODE_NAMESPACE_NOT_FOUND || codeName === "NamespaceNotFound") return "namespaceNotFound";
12163
+ if (code === CODE_INDEX_NOT_FOUND || codeName === "IndexNotFound") return "notFound";
12164
+ if (code === CODE_COMMAND_NOT_FOUND || code === CODE_COMMAND_NOT_SUPPORTED || code === CODE_LIST_SEARCH_ATLAS_ONLY || codeName === "CommandNotFound" || codeName === "CommandNotSupported" || codeName === "SearchNotEnabled") return "unsupported";
12165
+ if (code === void 0 && codeName === void 0 && /already exists/i.test(message)) return "exists";
12166
+ if (UNSUPPORTED_MESSAGE.test(message)) return "unsupported";
12167
+ return "other";
12168
+ }
12169
+ function attachContext(error, ctx) {
12170
+ const contextualized = error;
12171
+ try {
12172
+ contextualized.monsqlize = {
12173
+ ...contextualized.monsqlize && typeof contextualized.monsqlize === "object" ? contextualized.monsqlize : {},
12174
+ operation: ctx.operation,
12175
+ namespace: ctx.namespace,
12176
+ indexName: ctx.indexName
12177
+ };
12178
+ } catch {
12179
+ }
12180
+ return error;
12181
+ }
12182
+ function searchIndexError(code, message, ctx, extra = {}, cause) {
12183
+ return createError(code, message, [{ operation: ctx.operation, namespace: ctx.namespace, indexName: ctx.indexName, ...extra }], cause);
12184
+ }
12185
+ function mapSearchIndexError(error, ctx) {
12186
+ if (!(error instanceof Error)) {
12187
+ return createError(ErrorCodes.MONGODB_ERROR, `${ctx.operation} failed`, [{ ...ctx, reason: String(error) }]);
12188
+ }
12189
+ if (error.code !== void 0 && typeof error.code === "string" && String(error.code).startsWith("SEARCH_INDEX_")) {
12190
+ return error;
12191
+ }
12192
+ const { code, codeName, message } = readCode(error);
12193
+ const server = { serverCode: code, serverCodeName: codeName, serverMessage: message };
12194
+ switch (classifySearchIndexError(error)) {
12195
+ case "exists":
12196
+ return searchIndexError(ErrorCodes.SEARCH_INDEX_EXISTS, `Search index "${ctx.indexName}" already exists`, ctx, server, error);
12197
+ case "notFound":
12198
+ return searchIndexError(ErrorCodes.SEARCH_INDEX_NOT_FOUND, `Search index "${ctx.indexName}" was not found`, ctx, server, error);
12199
+ case "unsupported":
12200
+ return searchIndexError(ErrorCodes.SEARCH_INDEX_UNSUPPORTED, "This deployment does not support Search index commands (requires Atlas, MongoDB 8.2+ with mongot, or mongodb-atlas-local)", ctx, server, error);
12201
+ default:
12202
+ return attachContext(searchIndexError(ErrorCodes.MONGODB_ERROR, `${ctx.operation} failed: ${message}`, ctx, server, error), ctx);
12203
+ }
12204
+ }
12205
+
12206
+ // src/adapters/mongodb/search-indexes/normalize.ts
12207
+ function missingSearchIndexInfo(name) {
12208
+ return { name, status: "DOES_NOT_EXIST", queryable: false, typeSource: "unknown", raw: {} };
12209
+ }
12210
+ function normalizeSearchIndexInfo(doc) {
12211
+ const info = {
12212
+ name: String(doc.name),
12213
+ // Unknown future statuses are passed through verbatim rather than rewritten.
12214
+ status: typeof doc.status === "string" && doc.status.length > 0 ? doc.status : "PENDING",
12215
+ queryable: doc.queryable === true,
12216
+ typeSource: "unknown",
12217
+ raw: doc
12218
+ };
12219
+ if (typeof doc.id === "string") info.id = doc.id;
12220
+ const resolved = resolveSearchIndexType(doc);
12221
+ info.typeSource = resolved.typeSource;
12222
+ if (resolved.type !== void 0) info.type = resolved.type;
12223
+ if (doc.latestDefinition && typeof doc.latestDefinition === "object") info.latestDefinition = doc.latestDefinition;
12224
+ const version = doc.latestDefinitionVersion;
12225
+ if (version && typeof version === "object") {
12226
+ info.latestDefinitionVersion = { version: Number(version.version), createdAt: version.createdAt };
12227
+ }
12228
+ if (Array.isArray(doc.statusDetail)) info.statusDetail = doc.statusDetail;
12229
+ return info;
12230
+ }
12231
+ function resolveSearchIndexType(doc) {
12232
+ if (doc.type === "search" || doc.type === "vectorSearch") return { type: doc.type, typeSource: "server" };
12233
+ const definition = doc.latestDefinition;
12234
+ if (definition && typeof definition === "object" && !Array.isArray(definition)) {
12235
+ const fields = definition.fields;
12236
+ if (Array.isArray(fields) && fields.some((field) => field && typeof field === "object" && field.type === "vector")) {
12237
+ return { type: "vectorSearch", typeSource: "inferred" };
12238
+ }
12239
+ if (Object.prototype.hasOwnProperty.call(definition, "mappings")) return { type: "search", typeSource: "inferred" };
12240
+ }
12241
+ return { typeSource: "unknown" };
12242
+ }
12243
+ function matchesSearchIndexType(info, type) {
12244
+ return info.typeSource !== "unknown" && info.type === type;
12245
+ }
12246
+
12247
+ // src/adapters/mongodb/search-indexes/validate.ts
12248
+ var TYPES = /* @__PURE__ */ new Set(["search", "vectorSearch"]);
12249
+ var VECTOR_FIELD_TYPES = /* @__PURE__ */ new Set(["vector", "filter"]);
12250
+ var SIMILARITIES = /* @__PURE__ */ new Set(["euclidean", "cosine", "dotProduct"]);
12251
+ function invalid(operation, namespace, indexName, field, reason) {
12252
+ throw createError(
12253
+ ErrorCodes.INVALID_SEARCH_INDEX_DEFINITION,
12254
+ `${operation}: invalid ${field} (${reason})`,
12255
+ [{ operation, namespace, indexName: typeof indexName === "string" ? indexName : void 0, field, reason }]
12256
+ );
12257
+ }
12258
+ function isPlainObject4(value) {
12259
+ if (value === null || typeof value !== "object" || Array.isArray(value)) return false;
12260
+ const proto = Object.getPrototypeOf(value);
12261
+ return proto === Object.prototype || proto === null;
12262
+ }
12263
+ function assertIndexName(operation, namespace, name) {
12264
+ if (typeof name !== "string" || name.length === 0 || name.trim() !== name) {
12265
+ invalid(operation, namespace, name, "name", "non-empty string without leading or trailing whitespace required");
12266
+ }
12267
+ }
12268
+ function assertDefinition(operation, namespace, name, definition, type) {
12269
+ if (!isPlainObject4(definition)) invalid(operation, namespace, name, "definition", "plain object required");
12270
+ if (type !== "vectorSearch") return;
12271
+ const fields = definition.fields;
12272
+ if (!Array.isArray(fields) || fields.length === 0) {
12273
+ invalid(operation, namespace, name, "definition.fields", "non-empty array required for vectorSearch");
12274
+ }
12275
+ fields.forEach((field, index) => {
12276
+ const path5 = `definition.fields[${index}]`;
12277
+ if (!isPlainObject4(field)) invalid(operation, namespace, name, path5, "object required");
12278
+ if (typeof field.type !== "string" || !VECTOR_FIELD_TYPES.has(field.type)) {
12279
+ invalid(operation, namespace, name, `${path5}.type`, 'must be "vector" or "filter"');
12280
+ }
12281
+ if (typeof field.path !== "string" || field.path.length === 0) {
12282
+ invalid(operation, namespace, name, `${path5}.path`, "non-empty string required");
12283
+ }
12284
+ if (field.type === "vector") {
12285
+ const dims = field.numDimensions;
12286
+ if (typeof dims !== "number" || !Number.isInteger(dims) || dims <= 0) {
12287
+ invalid(operation, namespace, name, `${path5}.numDimensions`, "positive integer required");
12288
+ }
12289
+ if (typeof field.similarity !== "string" || !SIMILARITIES.has(field.similarity)) {
12290
+ invalid(operation, namespace, name, `${path5}.similarity`, 'must be "euclidean", "cosine", or "dotProduct"');
12291
+ }
12292
+ }
12293
+ });
12294
+ }
12295
+ function assertSpec(operation, namespace, spec) {
12296
+ if (!isPlainObject4(spec)) invalid(operation, namespace, void 0, "spec", "object required");
12297
+ assertIndexName(operation, namespace, spec.name);
12298
+ if (typeof spec.type !== "string" || !TYPES.has(spec.type)) {
12299
+ invalid(operation, namespace, spec.name, "type", 'must be "search" or "vectorSearch"');
12300
+ }
12301
+ assertDefinition(operation, namespace, spec.name, spec.definition, spec.type);
12302
+ }
12303
+ function assertOptionsObject(operation, namespace, name, options) {
12304
+ if (options !== void 0 && !isPlainObject4(options)) invalid(operation, namespace, name, "options", "object required");
12305
+ }
12306
+ function assertEnumOption(operation, namespace, name, field, value, allowed) {
12307
+ if (value !== void 0 && (typeof value !== "string" || !allowed.includes(value))) {
12308
+ invalid(operation, namespace, name, field, `must be one of ${allowed.map((v) => `"${v}"`).join(", ")}`);
12309
+ }
12310
+ }
12311
+ function assertBooleanOption(operation, namespace, name, field, value) {
12312
+ if (value !== void 0 && typeof value !== "boolean") invalid(operation, namespace, name, field, "boolean required");
12313
+ }
12314
+ function resolveWaitOptions(namespace, name, options) {
12315
+ const operation = "waitForSearchIndex";
12316
+ assertOptionsObject(operation, namespace, name, options);
12317
+ const opts = options ?? {};
12318
+ assertEnumOption(operation, namespace, name, "options.until", opts.until, ["queryable", "ready", "dropped"]);
12319
+ const timeoutMs = opts.timeoutMs ?? 3e5;
12320
+ const intervalMs = opts.intervalMs ?? 2e3;
12321
+ if (!Number.isInteger(timeoutMs) || timeoutMs <= 0) {
12322
+ invalid(operation, namespace, name, "options.timeoutMs", "positive integer required");
12323
+ }
12324
+ if (!Number.isInteger(intervalMs) || intervalMs < 100) {
12325
+ invalid(operation, namespace, name, "options.intervalMs", "integer >= 100 required");
12326
+ }
12327
+ const signal = opts.signal;
12328
+ if (signal !== void 0 && (signal === null || typeof signal !== "object" || typeof signal.aborted !== "boolean" || typeof signal.addEventListener !== "function")) {
12329
+ invalid(operation, namespace, name, "options.signal", "AbortSignal required");
12330
+ }
12331
+ return { until: opts.until ?? "queryable", timeoutMs, intervalMs, signal };
12332
+ }
12333
+
12334
+ // src/adapters/mongodb/search-indexes/operations.ts
12335
+ var SEARCH_INDEX_MISSING_GRACE_MS = 3e4;
12336
+ async function fetchByName(ctx, operation, name, signal) {
12337
+ try {
12338
+ const docs = await ctx.collection.listSearchIndexes(name, signal ? { signal } : {}).toArray();
12339
+ const match = docs.find((doc) => doc.name === name);
12340
+ return match ? normalizeSearchIndexInfo(match) : missingSearchIndexInfo(name);
12341
+ } catch (error) {
12342
+ if (signal?.aborted) throw signal.reason;
12343
+ if (classifySearchIndexError(error) === "namespaceNotFound") return missingSearchIndexInfo(name);
12344
+ throw mapSearchIndexError(error, { operation, namespace: ctx.namespace, indexName: name });
12345
+ }
12346
+ }
12347
+ async function getSearchIndexOp(ctx, name) {
12348
+ assertIndexName("getSearchIndex", ctx.namespace, name);
12349
+ return fetchByName(ctx, "getSearchIndex", name);
12350
+ }
12351
+ async function listSearchIndexesOp(ctx, options) {
12352
+ const operation = "listSearchIndexes";
12353
+ assertOptionsObject(operation, ctx.namespace, void 0, options);
12354
+ assertEnumOption(operation, ctx.namespace, void 0, "options.type", options?.type, ["search", "vectorSearch"]);
12355
+ let docs;
12356
+ try {
12357
+ docs = await ctx.collection.listSearchIndexes().toArray();
12358
+ } catch (error) {
12359
+ if (classifySearchIndexError(error) === "namespaceNotFound") return [];
12360
+ throw mapSearchIndexError(error, { operation, namespace: ctx.namespace });
12361
+ }
12362
+ const infos = docs.map(normalizeSearchIndexInfo);
12363
+ return options?.type ? infos.filter((info) => matchesSearchIndexType(info, options.type)) : infos;
12364
+ }
12365
+ async function createSearchIndexOp(ctx, spec, options) {
12366
+ const operation = "createSearchIndex";
12367
+ assertSpec(operation, ctx.namespace, spec);
12368
+ assertOptionsObject(operation, ctx.namespace, spec.name, options);
12369
+ assertEnumOption(operation, ctx.namespace, spec.name, "options.ifExists", options?.ifExists, ["error", "skip", "update"]);
12370
+ const ifExists = options?.ifExists ?? "error";
12371
+ const errCtx = { operation, namespace: ctx.namespace, indexName: spec.name };
12372
+ const existing = await fetchByName(ctx, operation, spec.name);
12373
+ if (existing.status !== "DOES_NOT_EXIST") {
12374
+ if (ifExists === "skip") return { name: spec.name, action: "skipped", created: false };
12375
+ if (ifExists === "error") {
12376
+ throw searchIndexError(ErrorCodes.SEARCH_INDEX_EXISTS, `Search index "${spec.name}" already exists`, errCtx);
12377
+ }
12378
+ if (existing.type !== void 0 && existing.type !== spec.type) {
12379
+ throw searchIndexError(
12380
+ ErrorCodes.INVALID_SEARCH_INDEX_DEFINITION,
12381
+ `Search index "${spec.name}" has type "${existing.type}"; drop it before recreating as "${spec.type}"`,
12382
+ errCtx,
12383
+ { field: "type", reason: "type change requires drop and create" }
12384
+ );
12385
+ }
12386
+ try {
12387
+ await ctx.collection.updateSearchIndex(spec.name, spec.definition);
12388
+ } catch (error) {
12389
+ throw mapSearchIndexError(error, errCtx);
12390
+ }
12391
+ return { name: spec.name, action: "updated", created: false };
12392
+ }
12393
+ try {
12394
+ const name = await ctx.collection.createSearchIndex({ name: spec.name, type: spec.type, definition: spec.definition });
12395
+ return { name: typeof name === "string" && name.length > 0 ? name : spec.name, action: "created", created: true };
12396
+ } catch (error) {
12397
+ const mapped = mapSearchIndexError(error, errCtx);
12398
+ if (mapped.code === ErrorCodes.SEARCH_INDEX_EXISTS && ifExists === "skip") {
12399
+ return { name: spec.name, action: "skipped", created: false };
12400
+ }
12401
+ throw mapped;
12402
+ }
12403
+ }
12404
+ async function createSearchIndexesOp(ctx, specs) {
12405
+ const operation = "createSearchIndexes";
12406
+ if (!Array.isArray(specs) || specs.length === 0) {
12407
+ throw searchIndexError(
12408
+ ErrorCodes.INVALID_SEARCH_INDEX_DEFINITION,
12409
+ `${operation}: specs must be a non-empty array`,
12410
+ { operation, namespace: ctx.namespace },
12411
+ { field: "specs", reason: "non-empty array required" }
12412
+ );
12413
+ }
12414
+ const seen = /* @__PURE__ */ new Set();
12415
+ for (const spec of specs) {
12416
+ assertSpec(operation, ctx.namespace, spec);
12417
+ if (seen.has(spec.name)) {
12418
+ throw searchIndexError(
12419
+ ErrorCodes.SEARCH_INDEX_EXISTS,
12420
+ `Search index "${spec.name}" is listed more than once`,
12421
+ { operation, namespace: ctx.namespace, indexName: spec.name }
12422
+ );
12423
+ }
12424
+ seen.add(spec.name);
12425
+ }
12426
+ try {
12427
+ return await ctx.collection.createSearchIndexes(specs.map(({ name, type, definition }) => ({ name, type, definition })));
12428
+ } catch (error) {
12429
+ throw mapSearchIndexError(error, { operation, namespace: ctx.namespace });
12430
+ }
12431
+ }
12432
+ async function updateSearchIndexOp(ctx, name, definition) {
12433
+ const operation = "updateSearchIndex";
12434
+ assertIndexName(operation, ctx.namespace, name);
12435
+ assertDefinition(operation, ctx.namespace, name, definition);
12436
+ const errCtx = { operation, namespace: ctx.namespace, indexName: name };
12437
+ const existing = await fetchByName(ctx, operation, name);
12438
+ if (existing.status === "DOES_NOT_EXIST") {
12439
+ throw searchIndexError(ErrorCodes.SEARCH_INDEX_NOT_FOUND, `Search index "${name}" was not found`, errCtx);
12440
+ }
12441
+ if (existing.type !== void 0) assertDefinition(operation, ctx.namespace, name, definition, existing.type);
12442
+ try {
12443
+ await ctx.collection.updateSearchIndex(name, definition);
12444
+ } catch (error) {
12445
+ throw mapSearchIndexError(error, errCtx);
12446
+ }
12447
+ }
12448
+ async function dropSearchIndexOp(ctx, name, options) {
12449
+ const operation = "dropSearchIndex";
12450
+ assertIndexName(operation, ctx.namespace, name);
12451
+ assertOptionsObject(operation, ctx.namespace, name, options);
12452
+ assertBooleanOption(operation, ctx.namespace, name, "options.ignoreMissing", options?.ignoreMissing);
12453
+ const errCtx = { operation, namespace: ctx.namespace, indexName: name };
12454
+ const existing = await fetchByName(ctx, operation, name);
12455
+ if (existing.status === "DOES_NOT_EXIST") {
12456
+ if (options?.ignoreMissing) return;
12457
+ throw searchIndexError(ErrorCodes.SEARCH_INDEX_NOT_FOUND, `Search index "${name}" was not found`, errCtx);
12458
+ }
12459
+ try {
12460
+ await ctx.collection.dropSearchIndex(name);
12461
+ } catch (error) {
12462
+ const mapped = mapSearchIndexError(error, errCtx);
12463
+ if (mapped.code === ErrorCodes.SEARCH_INDEX_NOT_FOUND && options?.ignoreMissing) return;
12464
+ throw mapped;
12465
+ }
12466
+ }
12467
+ function sleep2(ms, signal) {
12468
+ return new Promise((resolve, reject) => {
12469
+ if (signal?.aborted) {
12470
+ reject(signal.reason);
12471
+ return;
12472
+ }
12473
+ const onAbort = () => {
12474
+ clearTimeout(timer);
12475
+ reject(signal?.reason);
12476
+ };
12477
+ const timer = setTimeout(() => {
12478
+ signal?.removeEventListener("abort", onAbort);
12479
+ resolve();
12480
+ }, ms);
12481
+ signal?.addEventListener("abort", onAbort, { once: true });
12482
+ });
12483
+ }
12484
+ function linkSignal(userSignal, remainingMs) {
12485
+ const controller = new AbortController();
12486
+ let expired = false;
12487
+ const onUserAbort = () => controller.abort(userSignal?.reason);
12488
+ userSignal?.addEventListener("abort", onUserAbort, { once: true });
12489
+ const timer = setTimeout(() => {
12490
+ expired = true;
12491
+ controller.abort(new Error("search index wait deadline reached"));
12492
+ }, Math.max(0, remainingMs));
12493
+ return {
12494
+ signal: controller.signal,
12495
+ timedOut: () => expired,
12496
+ dispose: () => {
12497
+ clearTimeout(timer);
12498
+ userSignal?.removeEventListener("abort", onUserAbort);
12499
+ }
12500
+ };
12501
+ }
12502
+ async function waitForSearchIndexOp(ctx, name, options, clock = Date.now) {
12503
+ const operation = "waitForSearchIndex";
12504
+ assertIndexName(operation, ctx.namespace, name);
12505
+ const { until, timeoutMs, intervalMs, signal } = resolveWaitOptions(ctx.namespace, name, options);
12506
+ const errCtx = { operation, namespace: ctx.namespace, indexName: name };
12507
+ if (signal?.aborted) throw signal.reason;
12508
+ const start = clock();
12509
+ const grace = Math.min(SEARCH_INDEX_MISSING_GRACE_MS, timeoutMs);
12510
+ let last;
12511
+ const timeoutError = () => searchIndexError(
12512
+ ErrorCodes.SEARCH_INDEX_WAIT_TIMEOUT,
12513
+ `Timed out after ${timeoutMs}ms waiting for Search index "${name}" to become ${until}`,
12514
+ errCtx,
12515
+ { until, timeoutMs, last }
12516
+ );
12517
+ for (; ; ) {
12518
+ const link = linkSignal(signal, timeoutMs - (clock() - start));
12519
+ let info;
12520
+ try {
12521
+ info = await fetchByName(ctx, operation, name, link.signal);
12522
+ } catch (error) {
12523
+ if (signal?.aborted) throw signal.reason;
12524
+ if (link.timedOut()) throw timeoutError();
12525
+ throw error;
12526
+ } finally {
12527
+ link.dispose();
12528
+ }
12529
+ last = info;
12530
+ if (signal?.aborted) throw signal.reason;
12531
+ if (until === "dropped") {
12532
+ if (info.status === "DOES_NOT_EXIST") return info;
12533
+ } else if (info.status === "DOES_NOT_EXIST") {
12534
+ if (clock() - start >= grace) {
12535
+ throw searchIndexError(
12536
+ ErrorCodes.SEARCH_INDEX_NOT_FOUND,
12537
+ `Search index "${name}" was not found within ${grace}ms`,
12538
+ errCtx,
12539
+ { until, graceMs: grace }
12540
+ );
12541
+ }
12542
+ } else if (until === "queryable") {
12543
+ if (info.queryable) return info;
12544
+ if (info.status === "FAILED") {
12545
+ throw searchIndexError(
12546
+ ErrorCodes.SEARCH_INDEX_BUILD_FAILED,
12547
+ `Search index "${name}" build failed`,
12548
+ errCtx,
12549
+ { status: info.status, statusDetail: info.statusDetail }
12550
+ );
12551
+ }
12552
+ } else {
12553
+ if (info.status === "READY") return info;
12554
+ if (info.status === "FAILED") {
12555
+ throw searchIndexError(
12556
+ ErrorCodes.SEARCH_INDEX_BUILD_FAILED,
12557
+ `Search index "${name}" build failed`,
12558
+ errCtx,
12559
+ { status: info.status, statusDetail: info.statusDetail }
12560
+ );
12561
+ }
12562
+ }
12563
+ const remaining = timeoutMs - (clock() - start);
12564
+ if (remaining <= 0) throw timeoutError();
12565
+ await sleep2(Math.min(intervalMs, remaining), signal);
12566
+ }
12567
+ }
12568
+
12104
12569
  // src/adapters/mongodb/common/collection-accessor-cache-helpers.ts
12105
12570
  import { types as utilTypes4 } from "node:util";
12106
12571
  function isRecord2(value) {
@@ -12461,7 +12926,7 @@ async function invalidateReadCachesAfterWrite(queryCache, collection, dbName, co
12461
12926
  }
12462
12927
 
12463
12928
  // src/adapters/mongodb/writes/write-utils.ts
12464
- function sleep2(ms) {
12929
+ function sleep3(ms) {
12465
12930
  return new Promise((resolve) => setTimeout(resolve, ms));
12466
12931
  }
12467
12932
  function splitIntoBatches(items, batchSize) {
@@ -12881,7 +13346,7 @@ async function insertBatchDocuments(collection, documents, options = {}) {
12881
13346
  result.retries.push(retryInfo);
12882
13347
  onRetry?.(retryInfo);
12883
13348
  if (retryDelay > 0) {
12884
- await sleep2(retryDelay);
13349
+ await sleep3(retryDelay);
12885
13350
  }
12886
13351
  continue;
12887
13352
  }
@@ -13010,7 +13475,7 @@ async function updateBatchDocuments(collection, filter, update, options = {}) {
13010
13475
  result.retries.push(retryInfo);
13011
13476
  onRetry?.(retryInfo);
13012
13477
  if (retryDelay > 0) {
13013
- await sleep2(retryDelay);
13478
+ await sleep3(retryDelay);
13014
13479
  }
13015
13480
  continue;
13016
13481
  }
@@ -13105,7 +13570,7 @@ async function deleteBatchDocuments(collection, filter, options = {}) {
13105
13570
  result.retries.push(retryInfo);
13106
13571
  onRetry?.(retryInfo);
13107
13572
  if (retryDelay > 0) {
13108
- await sleep2(retryDelay);
13573
+ await sleep3(retryDelay);
13109
13574
  }
13110
13575
  continue;
13111
13576
  }
@@ -13602,7 +14067,7 @@ var MongoCollectionAccessor = class {
13602
14067
  const { query: normalizedQuery, options: driverOptions } = snapshotReadInputs(this._cvFilter(query), normalizeFindProjectionOptions(rawOptions), this.collectionRef.bsonOptions);
13603
14068
  const cacheTTL = rawOptions.explain ? 0 : typeof rawOptions.cache === "number" ? rawOptions.cache : 0;
13604
14069
  if (cacheTTL > 0 && this.management.queryCache && canCacheBsonResult(driverOptions) && !hasSessionOption(driverOptions) && !await isCollectionCacheBarrierActive(this.management.queryCache, this.collectionRef, this.management.defaults)) {
13605
- const cacheKey = buildFindOneCacheKey(this.collectionRef, this.management.defaults, normalizedQuery, driverOptions);
14070
+ const cacheKey = buildFindOneCacheKey(this.collectionRef, this.management.defaults, normalizedQuery, driverOptions, true);
13606
14071
  const result2 = await readThroughCache(this.management.queryCache, cacheKey, cacheTTL, () => findOneDocument(this.collectionRef, normalizedQuery, driverOptions));
13607
14072
  return wrapQueryResultWithMeta(this.collectionRef, this.management.defaults, "findOne", rawOptions, startTs, result2);
13608
14073
  }
@@ -13659,7 +14124,7 @@ var MongoCollectionAccessor = class {
13659
14124
  );
13660
14125
  const countQueue = this.management.defaults?.countQueue;
13661
14126
  if (cacheTTL > 0 && this.management.queryCache && canCacheBsonCount(keyOptions) && !hasSessionOption(keyOptions) && !await isCollectionCacheBarrierActive(this.management.queryCache, this.collectionRef, this.management.defaults)) {
13662
- const cacheKey = buildCountCacheKey(this.collectionRef, this.management.defaults, normalizedQuery, keyOptions);
14127
+ const cacheKey = buildCountCacheKey(this.collectionRef, this.management.defaults, normalizedQuery, keyOptions, true);
13663
14128
  const result = await readThroughCache(
13664
14129
  this.management.queryCache,
13665
14130
  cacheKey,
@@ -13752,7 +14217,7 @@ var MongoCollectionAccessor = class {
13752
14217
  driverOptions
13753
14218
  );
13754
14219
  if (cacheTTL > 0 && this.management.queryCache && canCacheBsonResult(driverOptions) && !hasSessionOption(driverOptions) && !await isCollectionCacheBarrierActive(this.management.queryCache, this.collectionRef, this.management.defaults)) {
13755
- const cacheKey = buildDistinctCacheKey(this.collectionRef, this.management.defaults, key, normalizedQuery, driverOptions);
14220
+ const cacheKey = buildDistinctCacheKey(this.collectionRef, this.management.defaults, key, normalizedQuery, driverOptions, true);
13756
14221
  const result2 = await readThroughCache(this.management.queryCache, cacheKey, cacheTTL, executeDistinct);
13757
14222
  return wrapQueryResultWithMeta(this.collectionRef, this.management.defaults, "distinct", rawOptions, startTs, result2);
13758
14223
  }
@@ -13873,6 +14338,35 @@ var MongoCollectionAccessor = class {
13873
14338
  this.assertWritePath("dropIndexes", "management");
13874
14339
  return dropIndexesForAccessor(this.collectionRef);
13875
14340
  }
14341
+ /** Search index lifecycle (v3.4.0); writes are governed by writePathPolicy `management`. */
14342
+ searchIndexContext() {
14343
+ return { collection: this.collectionRef, namespace: this.getNamespace() };
14344
+ }
14345
+ async createSearchIndex(spec, options) {
14346
+ this.assertWritePath("createSearchIndex", "management");
14347
+ return createSearchIndexOp(this.searchIndexContext(), spec, options);
14348
+ }
14349
+ async createSearchIndexes(specs) {
14350
+ this.assertWritePath("createSearchIndexes", "management");
14351
+ return createSearchIndexesOp(this.searchIndexContext(), specs);
14352
+ }
14353
+ async listSearchIndexes(options) {
14354
+ return listSearchIndexesOp(this.searchIndexContext(), options);
14355
+ }
14356
+ async getSearchIndex(name) {
14357
+ return getSearchIndexOp(this.searchIndexContext(), name);
14358
+ }
14359
+ async updateSearchIndex(name, definition) {
14360
+ this.assertWritePath("updateSearchIndex", "management");
14361
+ return updateSearchIndexOp(this.searchIndexContext(), name, definition);
14362
+ }
14363
+ async dropSearchIndex(name, options) {
14364
+ this.assertWritePath("dropSearchIndex", "management");
14365
+ return dropSearchIndexOp(this.searchIndexContext(), name, options);
14366
+ }
14367
+ async waitForSearchIndex(name, options) {
14368
+ return waitForSearchIndexOp(this.searchIndexContext(), name, options);
14369
+ }
13876
14370
  /** Pre-populates bookmark cache entries for the specified key dimensions and page numbers. */
13877
14371
  async prewarmBookmarks(keyDims = {}, pages = []) {
13878
14372
  return prewarmBookmarksForAccessor(this.bookmarkContext(), keyDims, pages);
@@ -14757,7 +15251,7 @@ var LockManager = class {
14757
15251
  break;
14758
15252
  }
14759
15253
  const delay3 = retryDelay * Math.pow(retryBackoff, attempt);
14760
- await sleep3(delay3);
15254
+ await sleep4(delay3);
14761
15255
  }
14762
15256
  this.stats.errors += 1;
14763
15257
  if (options.fallbackToNoLock) {
@@ -14869,7 +15363,7 @@ var LockManager = class {
14869
15363
  }
14870
15364
  }
14871
15365
  };
14872
- async function sleep3(ms) {
15366
+ async function sleep4(ms) {
14873
15367
  await new Promise((resolve) => setTimeout(resolve, ms));
14874
15368
  }
14875
15369
  var DistributedCacheLockManager = class {
@@ -14994,10 +15488,10 @@ var DistributedCacheLockManager = class {
14994
15488
  return new Lock(key, lockId, this, ttl, startedAt);
14995
15489
  }
14996
15490
  if (attempt === retryTimes) break;
14997
- await sleep3(retryDelay);
15491
+ await sleep4(retryDelay);
14998
15492
  } catch (error) {
14999
15493
  if (attempt === retryTimes) throw error;
15000
- await sleep3(retryDelay);
15494
+ await sleep4(retryDelay);
15001
15495
  }
15002
15496
  }
15003
15497
  this.stats.errors++;
@@ -18026,7 +18520,7 @@ var DEFAULT_DATA_TASK_MAX_DOCUMENTS = 1e4;
18026
18520
  var DEFAULT_DATA_TASK_BACKUP_MAX_BYTES = 256 * 1024 * 1024;
18027
18521
  var ENVIRONMENTS = /* @__PURE__ */ new Set(["development", "test", "staging", "production", "prod", "live"]);
18028
18522
  var PRODUCTION_ENVIRONMENTS = /* @__PURE__ */ new Set(["production", "prod", "live"]);
18029
- function invalid(message, collection) {
18523
+ function invalid2(message, collection) {
18030
18524
  throw new DataTaskJobError("INVALID_JOB", message, "validate", collection);
18031
18525
  }
18032
18526
  function canonicalize(value) {
@@ -18064,15 +18558,15 @@ function connectionDescriptor(value) {
18064
18558
  }
18065
18559
  function validateConnection(value, field) {
18066
18560
  if (isDataTaskJobRuntime(value)) return;
18067
- if (!isRecord3(value) || value.type !== "mongodb") invalid(`${field} must be a monSQLize runtime or MongoDB MonSQLizeOptions.`);
18068
- if (typeof value.databaseName !== "string" || value.databaseName.trim() === "") invalid(`${field}.databaseName is required.`);
18069
- if (!isRecord3(value.config) || typeof value.config.uri !== "string" || value.config.uri.trim() === "") invalid(`${field}.config.uri is required.`);
18561
+ if (!isRecord3(value) || value.type !== "mongodb") invalid2(`${field} must be a monSQLize runtime or MongoDB MonSQLizeOptions.`);
18562
+ if (typeof value.databaseName !== "string" || value.databaseName.trim() === "") invalid2(`${field}.databaseName is required.`);
18563
+ if (!isRecord3(value.config) || typeof value.config.uri !== "string" || value.config.uri.trim() === "") invalid2(`${field}.config.uri is required.`);
18070
18564
  }
18071
18565
  function uniqueFields(value, pathName, collection) {
18072
- if (!Array.isArray(value) || value.length === 0) invalid(`${pathName} must be a non-empty string array.`, collection);
18566
+ if (!Array.isArray(value) || value.length === 0) invalid2(`${pathName} must be a non-empty string array.`, collection);
18073
18567
  const fields = value.map((item) => typeof item === "string" ? item.trim() : "");
18074
- if (fields.some((item) => item === "")) invalid(`${pathName} must contain non-empty strings.`, collection);
18075
- if (new Set(fields).size !== fields.length) invalid(`${pathName} must not contain duplicates.`, collection);
18568
+ if (fields.some((item) => item === "")) invalid2(`${pathName} must contain non-empty strings.`, collection);
18569
+ if (new Set(fields).size !== fields.length) invalid2(`${pathName} must not contain duplicates.`, collection);
18076
18570
  return fields;
18077
18571
  }
18078
18572
  var FORBIDDEN_FIELD_PATH_SEGMENTS = /* @__PURE__ */ new Set(["__proto__", "prototype", "constructor"]);
@@ -18083,31 +18577,31 @@ function assertBsonLiteral(value, pathName, collection, stack = /* @__PURE__ */
18083
18577
  const valueType = typeof value;
18084
18578
  if (value === null || BSON_PRIMITIVE_TYPES.has(valueType)) return;
18085
18579
  if (BSON_UNSUPPORTED_TYPES.has(valueType)) {
18086
- invalid(`${pathName} must contain only BSON-compatible literals.`, collection);
18580
+ invalid2(`${pathName} must contain only BSON-compatible literals.`, collection);
18087
18581
  }
18088
18582
  if (value instanceof Date || value instanceof RegExp || Buffer.isBuffer(value)) return;
18089
18583
  if (Array.isArray(value)) {
18090
- if (stack.has(value)) invalid(`${pathName} must not contain circular values.`, collection);
18584
+ if (stack.has(value)) invalid2(`${pathName} must not contain circular values.`, collection);
18091
18585
  stack.add(value);
18092
18586
  value.forEach((item, position) => assertBsonLiteral(item, `${pathName}[${position}]`, collection, stack));
18093
18587
  stack.delete(value);
18094
18588
  return;
18095
18589
  }
18096
- if (!isRecord3(value)) invalid(`${pathName} must contain only BSON-compatible literals.`, collection);
18590
+ if (!isRecord3(value)) invalid2(`${pathName} must contain only BSON-compatible literals.`, collection);
18097
18591
  if (typeof value._bsontype === "string") return;
18098
18592
  const prototype = Object.getPrototypeOf(value);
18099
- if (!PLAIN_OBJECT_PROTOTYPES.has(prototype)) invalid(`${pathName} must contain only BSON-compatible literals.`, collection);
18100
- if (stack.has(value)) invalid(`${pathName} must not contain circular values.`, collection);
18593
+ if (!PLAIN_OBJECT_PROTOTYPES.has(prototype)) invalid2(`${pathName} must contain only BSON-compatible literals.`, collection);
18594
+ if (stack.has(value)) invalid2(`${pathName} must not contain circular values.`, collection);
18101
18595
  stack.add(value);
18102
18596
  for (const [key, item] of Object.entries(value)) assertBsonLiteral(item, `${pathName}.${key}`, collection, stack);
18103
18597
  stack.delete(value);
18104
18598
  }
18105
18599
  function normalizeFieldPath(value, pathName, collection) {
18106
- if (typeof value !== "string" || value.trim() === "") invalid(`${pathName} must be a non-empty field path.`, collection);
18600
+ if (typeof value !== "string" || value.trim() === "") invalid2(`${pathName} must be a non-empty field path.`, collection);
18107
18601
  const field = value.trim();
18108
18602
  const segments = field.split(".");
18109
18603
  if (segments.some((segment) => segment === "" || segment.startsWith("$") || FORBIDDEN_FIELD_PATH_SEGMENTS.has(segment))) {
18110
- invalid(`${pathName} contains an unsafe field path.`, collection);
18604
+ invalid2(`${pathName} contains an unsafe field path.`, collection);
18111
18605
  }
18112
18606
  return field;
18113
18607
  }
@@ -18123,13 +18617,13 @@ function projectionIncludes(projection, field) {
18123
18617
  return included.some((key) => key === field || field.startsWith(`${key}.`));
18124
18618
  }
18125
18619
  function normalizeFieldPatch(data, identityFields, collection) {
18126
- if (data.rename !== void 0 && !isRecord3(data.rename)) invalid("data.rename must be an object.", collection);
18127
- if (data.set !== void 0 && !isRecord3(data.set)) invalid("data.set must be an object.", collection);
18128
- if (data.unset !== void 0 && !Array.isArray(data.unset)) invalid("data.unset must be an array.", collection);
18620
+ if (data.rename !== void 0 && !isRecord3(data.rename)) invalid2("data.rename must be an object.", collection);
18621
+ if (data.set !== void 0 && !isRecord3(data.set)) invalid2("data.set must be an object.", collection);
18622
+ if (data.unset !== void 0 && !Array.isArray(data.unset)) invalid2("data.unset must be an array.", collection);
18129
18623
  const renameEntries = Object.entries(data.rename ?? {}).map(([source, destination], position) => {
18130
18624
  const normalizedSource = normalizeFieldPath(source, `data.rename source ${position}`, collection);
18131
18625
  const normalizedDestination = normalizeFieldPath(destination, `data.rename.${source}`, collection);
18132
- if (pathsOverlap(normalizedSource, normalizedDestination)) invalid("data.rename source and destination must not overlap.", collection);
18626
+ if (pathsOverlap(normalizedSource, normalizedDestination)) invalid2("data.rename source and destination must not overlap.", collection);
18133
18627
  return [normalizedSource, normalizedDestination];
18134
18628
  });
18135
18629
  const setEntries = Object.entries(data.set ?? {}).map(([field, value], position) => {
@@ -18138,14 +18632,14 @@ function normalizeFieldPatch(data, identityFields, collection) {
18138
18632
  try {
18139
18633
  canonicalStringify(value);
18140
18634
  } catch {
18141
- invalid(`data.set.${field} must be a BSON-compatible literal.`, collection);
18635
+ invalid2(`data.set.${field} must be a BSON-compatible literal.`, collection);
18142
18636
  }
18143
18637
  return [normalizedField, value];
18144
18638
  });
18145
18639
  const unset = (data.unset ?? []).map((field, position) => normalizeFieldPath(field, `data.unset[${position}]`, collection));
18146
- if (new Set(unset).size !== unset.length) invalid("data.unset must not contain duplicates.", collection);
18640
+ if (new Set(unset).size !== unset.length) invalid2("data.unset must not contain duplicates.", collection);
18147
18641
  const destinations = renameEntries.map(([, destination]) => destination);
18148
- if (new Set(destinations).size !== destinations.length) invalid("data.rename destinations must be unique.", collection);
18642
+ if (new Set(destinations).size !== destinations.length) invalid2("data.rename destinations must be unique.", collection);
18149
18643
  const configuredPaths = [
18150
18644
  ...renameEntries.flatMap(([source, destination]) => [
18151
18645
  { path: source, label: `rename source "${source}"` },
@@ -18157,18 +18651,18 @@ function normalizeFieldPatch(data, identityFields, collection) {
18157
18651
  for (let left = 0; left < configuredPaths.length; left += 1) {
18158
18652
  for (let right = left + 1; right < configuredPaths.length; right += 1) {
18159
18653
  if (pathsOverlap(configuredPaths[left].path, configuredPaths[right].path)) {
18160
- invalid(`${configuredPaths[left].label} conflicts with ${configuredPaths[right].label}.`, collection);
18654
+ invalid2(`${configuredPaths[left].label} conflicts with ${configuredPaths[right].label}.`, collection);
18161
18655
  }
18162
18656
  }
18163
18657
  }
18164
18658
  for (const configured of configuredPaths) {
18165
18659
  if (configured.path === "_id" || configured.path.startsWith("_id.") || identityFields.some((identity) => pathsOverlap(configured.path, identity))) {
18166
- invalid(`${configured.label} must not overlap _id or an identity field.`, collection);
18660
+ invalid2(`${configured.label} must not overlap _id or an identity field.`, collection);
18167
18661
  }
18168
18662
  }
18169
18663
  if (data.projection) {
18170
18664
  for (const required of [...identityFields, ...renameEntries.map(([source]) => source)]) {
18171
- if (!projectionIncludes(data.projection, required)) invalid(`data.projection must include required field "${required}".`, collection);
18665
+ if (!projectionIncludes(data.projection, required)) invalid2(`data.projection must include required field "${required}".`, collection);
18172
18666
  }
18173
18667
  }
18174
18668
  return {
@@ -18179,105 +18673,105 @@ function normalizeFieldPatch(data, identityFields, collection) {
18179
18673
  }
18180
18674
  function normalizeData(data, collection) {
18181
18675
  if (data === void 0) return void 0;
18182
- if (!isRecord3(data)) invalid("data must be an object.", collection);
18676
+ if (!isRecord3(data)) invalid2("data must be an object.", collection);
18183
18677
  const hasFilter = isRecord3(data.filter) && Object.keys(data.filter).length > 0;
18184
- if (hasFilter === (data.all === true)) invalid("data requires exactly one of a non-empty filter or all: true.", collection);
18678
+ if (hasFilter === (data.all === true)) invalid2("data requires exactly one of a non-empty filter or all: true.", collection);
18185
18679
  if (!isRecord3(data.identity) || data.identity.mode !== "fields" && data.identity.mode !== "source-id") {
18186
- invalid("data.identity.mode must be fields or source-id.", collection);
18680
+ invalid2("data.identity.mode must be fields or source-id.", collection);
18187
18681
  }
18188
18682
  const identity = data.identity.mode === "fields" ? { mode: "fields", fields: uniqueFields(data.identity.fields, "identity.fields", collection) } : {
18189
18683
  mode: "source-id",
18190
18684
  ...data.identity.conflictBy === void 0 ? {} : { conflictBy: uniqueFields(data.identity.conflictBy, "identity.conflictBy", collection) }
18191
18685
  };
18192
- if (data.projection !== void 0 && !isRecord3(data.projection)) invalid("data.projection must be an object.", collection);
18686
+ if (data.projection !== void 0 && !isRecord3(data.projection)) invalid2("data.projection must be an object.", collection);
18193
18687
  if (data.projection) assertBsonLiteral(data.projection, "data.projection", collection);
18194
18688
  if (hasFilter) assertBsonLiteral(data.filter, "data.filter", collection);
18195
18689
  const identityFields = identity.mode === "fields" ? identity.fields : ["_id", ...identity.conflictBy ?? []];
18196
18690
  const patch = normalizeFieldPatch(data, identityFields, collection);
18197
18691
  const batchSize = data.batchSize ?? 500;
18198
- if (!Number.isInteger(batchSize) || batchSize < 1 || batchSize > 1e4) invalid("data.batchSize must be an integer from 1 to 10000.", collection);
18692
+ if (!Number.isInteger(batchSize) || batchSize < 1 || batchSize > 1e4) invalid2("data.batchSize must be an integer from 1 to 10000.", collection);
18199
18693
  const maxDocuments = data.maxDocuments ?? DEFAULT_DATA_TASK_MAX_DOCUMENTS;
18200
18694
  if (!Number.isInteger(maxDocuments) || maxDocuments < 1 || maxDocuments > 1e6) {
18201
- invalid("data.maxDocuments must be an integer from 1 to 1000000.", collection);
18695
+ invalid2("data.maxDocuments must be an integer from 1 to 1000000.", collection);
18202
18696
  }
18203
18697
  const strategy = data.strategy ?? "upsert";
18204
- if (strategy !== "upsert" && strategy !== "insert") invalid("data.strategy must be upsert or insert.", collection);
18698
+ if (strategy !== "upsert" && strategy !== "insert") invalid2("data.strategy must be upsert or insert.", collection);
18205
18699
  return { ...data, ...patch, filter: hasFilter ? data.filter : {}, all: data.all === true, identity, strategy, batchSize, maxDocuments };
18206
18700
  }
18207
18701
  function normalizeIndexes(indexes, collection) {
18208
18702
  if (indexes === void 0) return [];
18209
- if (!Array.isArray(indexes)) invalid("indexes must be an array.", collection);
18703
+ if (!Array.isArray(indexes)) invalid2("indexes must be an array.", collection);
18210
18704
  const names = /* @__PURE__ */ new Set();
18211
18705
  return indexes.map((index, position) => {
18212
- if (!isRecord3(index) || !isRecord3(index.key) || Object.keys(index.key).length === 0) invalid(`indexes[${position}].key must be non-empty.`, collection);
18706
+ if (!isRecord3(index) || !isRecord3(index.key) || Object.keys(index.key).length === 0) invalid2(`indexes[${position}].key must be non-empty.`, collection);
18213
18707
  assertBsonLiteral(index.key, `indexes[${position}].key`, collection);
18214
- if (index.name !== void 0 && (typeof index.name !== "string" || index.name.trim() === "")) invalid(`indexes[${position}].name must be non-empty.`, collection);
18215
- if (index.name && names.has(index.name)) invalid(`duplicate index name "${index.name}".`, collection);
18708
+ if (index.name !== void 0 && (typeof index.name !== "string" || index.name.trim() === "")) invalid2(`indexes[${position}].name must be non-empty.`, collection);
18709
+ if (index.name && names.has(index.name)) invalid2(`duplicate index name "${index.name}".`, collection);
18216
18710
  if (index.name) names.add(index.name);
18217
- if (index.options !== void 0 && !isRecord3(index.options)) invalid(`indexes[${position}].options must be an object.`, collection);
18711
+ if (index.options !== void 0 && !isRecord3(index.options)) invalid2(`indexes[${position}].options must be an object.`, collection);
18218
18712
  if (index.options) assertBsonLiteral(index.options, `indexes[${position}].options`, collection);
18219
- if (index.options && "name" in index.options) invalid(`indexes[${position}].options.name is not allowed; use indexes[${position}].name.`, collection);
18220
- if (Object.keys(index.key).length === 1 && index.key._id === 1 || index.name === "_id_") invalid("indexes must not declare the built-in _id index.", collection);
18713
+ if (index.options && "name" in index.options) invalid2(`indexes[${position}].options.name is not allowed; use indexes[${position}].name.`, collection);
18714
+ if (Object.keys(index.key).length === 1 && index.key._id === 1 || index.name === "_id_") invalid2("indexes must not declare the built-in _id index.", collection);
18221
18715
  return { key: { ...index.key }, ...index.name ? { name: index.name } : {}, options: { ...index.options ?? {} } };
18222
18716
  });
18223
18717
  }
18224
18718
  function normalizeVerify(verify, collection) {
18225
- if (verify !== void 0 && !isRecord3(verify)) invalid("verify must be an object.", collection);
18719
+ if (verify !== void 0 && !isRecord3(verify)) invalid2("verify must be an object.", collection);
18226
18720
  const input = verify;
18227
18721
  const mode = input?.mode ?? "sample";
18228
- if (mode !== "sample" && mode !== "full") invalid("verify.mode must be sample or full.", collection);
18722
+ if (mode !== "sample" && mode !== "full") invalid2("verify.mode must be sample or full.", collection);
18229
18723
  const sampleSize = input?.sampleSize ?? 20;
18230
- if (!Number.isInteger(sampleSize) || sampleSize < 1 || sampleSize > 1e3) invalid("verify.sampleSize must be an integer from 1 to 1000.", collection);
18724
+ if (!Number.isInteger(sampleSize) || sampleSize < 1 || sampleSize > 1e3) invalid2("verify.sampleSize must be an integer from 1 to 1000.", collection);
18231
18725
  const fields = input?.fields === void 0 ? void 0 : uniqueFields(input.fields, "verify.fields", collection);
18232
18726
  return { mode, sampleSize, ...fields ? { fields } : {} };
18233
18727
  }
18234
18728
  function normalizeBackup(backup, production) {
18235
- if (backup !== void 0 && !isRecord3(backup)) invalid("backup must be an object.");
18729
+ if (backup !== void 0 && !isRecord3(backup)) invalid2("backup must be an object.");
18236
18730
  const input = backup;
18237
- if (production && (typeof input?.dir !== "string" || input.dir.trim() === "")) invalid("production jobs require an explicit durable backup.dir.");
18731
+ if (production && (typeof input?.dir !== "string" || input.dir.trim() === "")) invalid2("production jobs require an explicit durable backup.dir.");
18238
18732
  const dir = typeof input?.dir === "string" && input.dir.trim() ? input.dir : path3.join(os2.tmpdir(), "monsqlize-data-tasks");
18239
18733
  const compression = input?.compression ?? "gzip";
18240
- if (compression !== "gzip" && compression !== "none") invalid("backup.compression must be gzip or none.");
18241
- if (input?.format !== void 0 && input.format !== "extended-jsonl") invalid("backup.format must be extended-jsonl.");
18734
+ if (compression !== "gzip" && compression !== "none") invalid2("backup.compression must be gzip or none.");
18735
+ if (input?.format !== void 0 && input.format !== "extended-jsonl") invalid2("backup.format must be extended-jsonl.");
18242
18736
  const retentionDays = input?.retentionDays ?? 7;
18243
- if (!Number.isInteger(retentionDays) || retentionDays < 1) invalid("backup.retentionDays must be a positive integer.");
18737
+ if (!Number.isInteger(retentionDays) || retentionDays < 1) invalid2("backup.retentionDays must be a positive integer.");
18244
18738
  const maxBytes = input?.maxBytes ?? DEFAULT_DATA_TASK_BACKUP_MAX_BYTES;
18245
- if (!Number.isSafeInteger(maxBytes) || maxBytes < 1) invalid("backup.maxBytes must be a positive safe integer.");
18739
+ if (!Number.isSafeInteger(maxBytes) || maxBytes < 1) invalid2("backup.maxBytes must be a positive safe integer.");
18246
18740
  return { dir, format: "extended-jsonl", compression, retentionDays, maxBytes };
18247
18741
  }
18248
18742
  function normalizeLock(lock) {
18249
18743
  if (!lock) return false;
18250
- if (lock !== true && !isRecord3(lock)) invalid("lock must be a boolean or lease options object.");
18744
+ if (lock !== true && !isRecord3(lock)) invalid2("lock must be a boolean or lease options object.");
18251
18745
  const options = lock === true ? {} : lock;
18252
18746
  const ttlMs = options.ttlMs ?? 12e4;
18253
18747
  const waitTimeoutMs = options.waitTimeoutMs ?? 0;
18254
- if (!Number.isInteger(ttlMs) || ttlMs < 1e3) invalid("lock.ttlMs must be an integer of at least 1000.");
18255
- if (!Number.isInteger(waitTimeoutMs) || waitTimeoutMs < 0) invalid("lock.waitTimeoutMs must be a non-negative integer.");
18748
+ if (!Number.isInteger(ttlMs) || ttlMs < 1e3) invalid2("lock.ttlMs must be an integer of at least 1000.");
18749
+ if (!Number.isInteger(waitTimeoutMs) || waitTimeoutMs < 0) invalid2("lock.waitTimeoutMs must be a non-negative integer.");
18256
18750
  return { ttlMs, waitTimeoutMs };
18257
18751
  }
18258
18752
  function normalizeDataTaskJob(job) {
18259
- if (!isRecord3(job)) invalid("job must be an object.");
18260
- if (typeof job.name !== "string" || job.name.trim() === "") invalid("job.name is required.");
18753
+ if (!isRecord3(job)) invalid2("job must be an object.");
18754
+ if (typeof job.name !== "string" || job.name.trim() === "") invalid2("job.name is required.");
18261
18755
  validateConnection(job.source, "source");
18262
18756
  validateConnection(job.target, "target");
18263
- if (job.source === job.target) invalid("source and target must be independent runtimes or connection options.");
18757
+ if (job.source === job.target) invalid2("source and target must be independent runtimes or connection options.");
18264
18758
  const sourceOptions = connectionOptions(job.source);
18265
18759
  const targetOptions = connectionOptions(job.target);
18266
- if (sourceOptions && targetOptions && sourceOptions.databaseName === targetOptions.databaseName && sourceOptions.config?.uri === targetOptions.config?.uri) invalid("source and target resolve to the same MongoDB database.");
18760
+ if (sourceOptions && targetOptions && sourceOptions.databaseName === targetOptions.databaseName && sourceOptions.config?.uri === targetOptions.config?.uri) invalid2("source and target resolve to the same MongoDB database.");
18267
18761
  if (typeof job.targetEnvironment !== "string" || !ENVIRONMENTS.has(job.targetEnvironment.toLowerCase())) {
18268
- invalid("targetEnvironment must be development, test, staging, production, prod, or live.");
18762
+ invalid2("targetEnvironment must be development, test, staging, production, prod, or live.");
18269
18763
  }
18270
- if (!Array.isArray(job.collections) || job.collections.length === 0) invalid("collections must be a non-empty array.");
18764
+ if (!Array.isArray(job.collections) || job.collections.length === 0) invalid2("collections must be a non-empty array.");
18271
18765
  const targetNames = /* @__PURE__ */ new Set();
18272
18766
  const collections = job.collections.map((collection, position) => {
18273
- if (!isRecord3(collection) || typeof collection.name !== "string" || collection.name.trim() === "") invalid(`collections[${position}].name is required.`);
18767
+ if (!isRecord3(collection) || typeof collection.name !== "string" || collection.name.trim() === "") invalid2(`collections[${position}].name is required.`);
18274
18768
  const name = collection.name.trim();
18275
18769
  const targetName = typeof collection.targetName === "string" && collection.targetName.trim() ? collection.targetName.trim() : name;
18276
- if (targetNames.has(targetName)) invalid(`target collection "${targetName}" is configured more than once.`, name);
18770
+ if (targetNames.has(targetName)) invalid2(`target collection "${targetName}" is configured more than once.`, name);
18277
18771
  targetNames.add(targetName);
18278
18772
  const indexes = normalizeIndexes(collection.indexes, name);
18279
18773
  const data = normalizeData(collection.data, name);
18280
- if (indexes.length === 0 && !data) invalid("each collection requires indexes or data.", name);
18774
+ if (indexes.length === 0 && !data) invalid2("each collection requires indexes or data.", name);
18281
18775
  return { name, targetName, indexes, ...data ? { data } : {}, verify: normalizeVerify(collection.verify, name) };
18282
18776
  });
18283
18777
  const production = PRODUCTION_ENVIRONMENTS.has(job.targetEnvironment.toLowerCase());