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.
@@ -83,6 +83,13 @@ var ErrorCodes = {
83
83
  INVALID_OPERATION: "INVALID_OPERATION",
84
84
  /** Invalid local Vector Search options before the driver is called. */
85
85
  INVALID_VECTOR_SEARCH: "INVALID_VECTOR_SEARCH",
86
+ /** Search index lifecycle errors (v3.4.0). */
87
+ INVALID_SEARCH_INDEX_DEFINITION: "INVALID_SEARCH_INDEX_DEFINITION",
88
+ SEARCH_INDEX_EXISTS: "SEARCH_INDEX_EXISTS",
89
+ SEARCH_INDEX_NOT_FOUND: "SEARCH_INDEX_NOT_FOUND",
90
+ SEARCH_INDEX_UNSUPPORTED: "SEARCH_INDEX_UNSUPPORTED",
91
+ SEARCH_INDEX_BUILD_FAILED: "SEARCH_INDEX_BUILD_FAILED",
92
+ SEARCH_INDEX_WAIT_TIMEOUT: "SEARCH_INDEX_WAIT_TIMEOUT",
86
93
  /** Relation select is outside the supported inclusion-only top-level contract. */
87
94
  INVALID_RELATION_SELECT: "INVALID_RELATION_SELECT",
88
95
  /** A protected delete found declared Model relations that still reference its target. */
@@ -1015,31 +1022,31 @@ function normalizeRelationSelect(value, context = {}) {
1015
1022
  if (value === void 0) {
1016
1023
  return void 0;
1017
1024
  }
1018
- const invalid2 = (reason) => {
1025
+ const invalid3 = (reason) => {
1019
1026
  throw createError(
1020
1027
  ErrorCodes.INVALID_RELATION_SELECT,
1021
1028
  `Relation select must use non-empty top-level inclusion fields: ${reason}`,
1022
1029
  [{ ...context, value, reason }]
1023
1030
  );
1024
1031
  };
1025
- const rawTokens = typeof value === "string" ? [value] : Array.isArray(value) ? value : invalid2("select must be a string or string array");
1032
+ const rawTokens = typeof value === "string" ? [value] : Array.isArray(value) ? value : invalid3("select must be a string or string array");
1026
1033
  const tokens = [];
1027
1034
  for (const rawToken of rawTokens) {
1028
1035
  if (typeof rawToken !== "string") {
1029
- invalid2("select array entries must be strings");
1036
+ invalid3("select array entries must be strings");
1030
1037
  }
1031
1038
  const parts = rawToken.trim().split(/\s+/).filter(Boolean);
1032
1039
  if (parts.length === 0) {
1033
- invalid2("select must not be empty");
1040
+ invalid3("select must not be empty");
1034
1041
  }
1035
1042
  tokens.push(...parts);
1036
1043
  }
1037
1044
  for (const token of tokens) {
1038
1045
  if (token.startsWith("-") || token.startsWith("+")) {
1039
- invalid2("exclusion and prefix tokens are not supported");
1046
+ invalid3("exclusion and prefix tokens are not supported");
1040
1047
  }
1041
1048
  if (token.includes(".")) {
1042
- invalid2("dot-path fields are not supported");
1049
+ invalid3("dot-path fields are not supported");
1043
1050
  }
1044
1051
  }
1045
1052
  return [...new Set(tokens)];
@@ -3594,8 +3601,8 @@ function normalizeExtensionValue(value, stack) {
3594
3601
  return `circular:${getExtensionObjectKey(objectValue)}`;
3595
3602
  }
3596
3603
  const prototype = Object.getPrototypeOf(objectValue);
3597
- const isPlainObject4 = prototype === Object.prototype || prototype === null;
3598
- if (!Array.isArray(objectValue) && !isPlainObject4) {
3604
+ const isPlainObject5 = prototype === Object.prototype || prototype === null;
3605
+ if (!Array.isArray(objectValue) && !isPlainObject5) {
3599
3606
  return `object:${prototype?.constructor?.name ?? "unknown"}:${getExtensionObjectKey(objectValue)}`;
3600
3607
  }
3601
3608
  stack.add(objectValue);
@@ -6709,6 +6716,28 @@ var ModelInstance = class {
6709
6716
  dropIndexes() {
6710
6717
  return this.runModelWrite(() => this.collection.dropIndexes());
6711
6718
  }
6719
+ /** Search index lifecycle (v3.4.0): writes run as model writes for writePathPolicy. */
6720
+ createSearchIndex(spec, options) {
6721
+ return this.runModelWrite(() => this.collection.createSearchIndex(spec, options));
6722
+ }
6723
+ createSearchIndexes(specs) {
6724
+ return this.runModelWrite(() => this.collection.createSearchIndexes(specs));
6725
+ }
6726
+ listSearchIndexes(options) {
6727
+ return this.collection.listSearchIndexes(options);
6728
+ }
6729
+ getSearchIndex(name) {
6730
+ return this.collection.getSearchIndex(name);
6731
+ }
6732
+ updateSearchIndex(name, definition) {
6733
+ return this.runModelWrite(() => this.collection.updateSearchIndex(name, definition));
6734
+ }
6735
+ dropSearchIndex(name, options) {
6736
+ return this.runModelWrite(() => this.collection.dropSearchIndex(name, options));
6737
+ }
6738
+ waitForSearchIndex(name, options) {
6739
+ return this.collection.waitForSearchIndex(name, options);
6740
+ }
6712
6741
  prewarmBookmarks(keyDims, pages) {
6713
6742
  return this.extendedCollection().prewarmBookmarks(keyDims, pages);
6714
6743
  }
@@ -8265,18 +8294,26 @@ var HealthChecker = class {
8265
8294
  const status = this._healthStatus.get(poolName);
8266
8295
  if (!status) return;
8267
8296
  const client = this._clients.get(poolName) ?? this._poolManager?._getPool(poolName);
8268
- if (this._inProgress.get(poolName)?.client === client && this._inProgress.has(poolName)) return;
8297
+ const pending = this._inProgress.get(poolName);
8298
+ if (pending && pending.client === client && pending.awaited) return;
8269
8299
  const epoch = this._epochs.get(poolName);
8270
- const promise = Promise.resolve().then(() => {
8271
- if (this._epochs.get(poolName) !== epoch || this._healthStatus.get(poolName) !== status) return;
8272
- return this._pingOverride ? this._pingOverride(poolName) : this._pingPool(poolName);
8273
- });
8274
- const operation = { client, promise };
8275
- this._inProgress.set(poolName, operation);
8276
- const clearPending = () => {
8277
- if (this._inProgress.get(poolName) === operation) this._inProgress.delete(poolName);
8278
- };
8279
- void promise.then(clearPending, clearPending);
8300
+ let operation;
8301
+ if (pending && pending.client === client) {
8302
+ operation = pending;
8303
+ } else {
8304
+ const promise2 = Promise.resolve().then(() => {
8305
+ if (this._epochs.get(poolName) !== epoch || this._healthStatus.get(poolName) !== status) return;
8306
+ return this._pingOverride ? this._pingOverride(poolName) : this._pingPool(poolName);
8307
+ });
8308
+ operation = { client, promise: promise2, awaited: true };
8309
+ this._inProgress.set(poolName, operation);
8310
+ const clearPending = () => {
8311
+ if (this._inProgress.get(poolName) === operation) this._inProgress.delete(poolName);
8312
+ };
8313
+ void promise2.then(clearPending, clearPending);
8314
+ }
8315
+ operation.awaited = true;
8316
+ const promise = operation.promise;
8280
8317
  let timer;
8281
8318
  let lastError = null;
8282
8319
  try {
@@ -8288,6 +8325,7 @@ var HealthChecker = class {
8288
8325
  lastError = error instanceof Error ? error : new Error(String(error));
8289
8326
  } finally {
8290
8327
  if (timer) clearTimeout(timer);
8328
+ operation.awaited = false;
8291
8329
  }
8292
8330
  if (this._epochs.get(poolName) !== epoch || this._healthStatus.get(poolName) !== status) return;
8293
8331
  status.lastCheck = /* @__PURE__ */ new Date();
@@ -10745,27 +10783,28 @@ function normalizeFindPageLimit(limit, defaults2) {
10745
10783
  }
10746
10784
  return rawLimit;
10747
10785
  }
10748
- function documentReadKey(operation, collection, defaults2, query, options, field) {
10749
- const captured = snapshotReadInputs(normalizeQueryForCache(query, defaults2), options, collection.bsonOptions);
10786
+ function documentReadKey(operation, collection, defaults2, query, options, field, queryPrepared = false) {
10787
+ const keyQuery = queryPrepared ? query ?? {} : normalizeQueryForCache(query, defaults2);
10788
+ const captured = snapshotReadInputs(keyQuery, options, collection.bsonOptions);
10750
10789
  const keyOptions = buildResultCacheKeyOptions(captured.options, collection.bsonOptions);
10751
10790
  if (operation === "count") delete keyOptions.fieldsAsRaw;
10752
10791
  const body = field === void 0 ? stableCacheKeyString(captured.query, "document") : stableCacheKeyString({ key: field, query: captured.query });
10753
10792
  return `${operation}:${buildCollectionCacheNamespace(collection, defaults2)}:${body}:${stableCacheKeyString(keyOptions)}`;
10754
10793
  }
10755
- function buildFindCacheKey(collection, defaults2 = {}, query, options) {
10794
+ function buildFindCacheKey(collection, defaults2 = {}, query, options, queryPrepared = false) {
10756
10795
  return documentReadKey("find", collection, defaults2, query, normalizeFindProjectionOptions({
10757
10796
  limit: defaults2.findLimit,
10758
10797
  ...options ?? {}
10759
- }));
10798
+ }), void 0, queryPrepared);
10760
10799
  }
10761
- function buildFindOneCacheKey(collection, defaults2 = {}, query, options) {
10762
- return documentReadKey("findOne", collection, defaults2, query, normalizeFindProjectionOptions(options));
10800
+ function buildFindOneCacheKey(collection, defaults2 = {}, query, options, queryPrepared = false) {
10801
+ return documentReadKey("findOne", collection, defaults2, query, normalizeFindProjectionOptions(options), void 0, queryPrepared);
10763
10802
  }
10764
- function buildCountCacheKey(collection, defaults2 = {}, query, options) {
10765
- return documentReadKey("count", collection, defaults2, query, options);
10803
+ function buildCountCacheKey(collection, defaults2 = {}, query, options, queryPrepared = false) {
10804
+ return documentReadKey("count", collection, defaults2, query, options, void 0, queryPrepared);
10766
10805
  }
10767
- function buildDistinctCacheKey(collection, defaults2 = {}, field, query, options) {
10768
- return documentReadKey("distinct", collection, defaults2, query, options, field);
10806
+ function buildDistinctCacheKey(collection, defaults2 = {}, field, query, options, queryPrepared = false) {
10807
+ return documentReadKey("distinct", collection, defaults2, query, options, field, queryPrepared);
10769
10808
  }
10770
10809
  function buildPreparedAggregateCacheKey(collection, defaults2 = {}, pipeline = [], options) {
10771
10810
  const namespace = buildCollectionCacheNamespace(collection, defaults2);
@@ -11777,7 +11816,7 @@ var FindChain = class {
11777
11816
  const cacheTTL = typeof this.options.cache === "number" ? this.options.cache : 0;
11778
11817
  const executeOptions = prepared.options;
11779
11818
  if (cacheTTL > 0 && this.queryCache && canCacheBsonResult(executeOptions) && !hasSessionOption(executeOptions) && !await isCollectionCacheBarrierActive(this.queryCache, this.collection, this.defaults)) {
11780
- const cacheKey = buildFindCacheKey(this.collection, this.defaults, prepared.query, executeOptions);
11819
+ const cacheKey = buildFindCacheKey(this.collection, this.defaults, prepared.query, executeOptions, true);
11781
11820
  const result = await readThroughCache(this.queryCache, cacheKey, cacheTTL, () => this.runToArray(prepared));
11782
11821
  return this.wrapResult("find", startTs, result);
11783
11822
  }
@@ -12118,6 +12157,432 @@ function mapVectorSearchRows(rows) {
12118
12157
  });
12119
12158
  }
12120
12159
 
12160
+ // src/adapters/mongodb/search-indexes/errors.ts
12161
+ var CODE_INDEX_ALREADY_EXISTS = 68;
12162
+ var CODE_INDEX_NOT_FOUND = 27;
12163
+ var CODE_NAMESPACE_NOT_FOUND = 26;
12164
+ var CODE_COMMAND_NOT_FOUND = 59;
12165
+ var CODE_COMMAND_NOT_SUPPORTED = 115;
12166
+ var CODE_LIST_SEARCH_ATLAS_ONLY = 6047401;
12167
+ var UNSUPPORTED_MESSAGE = /search index commands are only supported|\$listSearchIndexes stage is only allowed/i;
12168
+ function readCode(error) {
12169
+ const e = error;
12170
+ return {
12171
+ code: typeof e?.code === "number" ? e.code : void 0,
12172
+ codeName: typeof e?.codeName === "string" ? e.codeName : void 0,
12173
+ message: typeof e?.message === "string" ? e.message : String(error)
12174
+ };
12175
+ }
12176
+ function classifySearchIndexError(error) {
12177
+ const { code, codeName, message } = readCode(error);
12178
+ if (code === CODE_INDEX_ALREADY_EXISTS || codeName === "IndexAlreadyExists") return "exists";
12179
+ if (code === CODE_NAMESPACE_NOT_FOUND || codeName === "NamespaceNotFound") return "namespaceNotFound";
12180
+ if (code === CODE_INDEX_NOT_FOUND || codeName === "IndexNotFound") return "notFound";
12181
+ 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";
12182
+ if (code === void 0 && codeName === void 0 && /already exists/i.test(message)) return "exists";
12183
+ if (UNSUPPORTED_MESSAGE.test(message)) return "unsupported";
12184
+ return "other";
12185
+ }
12186
+ function attachContext(error, ctx) {
12187
+ const contextualized = error;
12188
+ try {
12189
+ contextualized.monsqlize = {
12190
+ ...contextualized.monsqlize && typeof contextualized.monsqlize === "object" ? contextualized.monsqlize : {},
12191
+ operation: ctx.operation,
12192
+ namespace: ctx.namespace,
12193
+ indexName: ctx.indexName
12194
+ };
12195
+ } catch {
12196
+ }
12197
+ return error;
12198
+ }
12199
+ function searchIndexError(code, message, ctx, extra = {}, cause) {
12200
+ return createError(code, message, [{ operation: ctx.operation, namespace: ctx.namespace, indexName: ctx.indexName, ...extra }], cause);
12201
+ }
12202
+ function mapSearchIndexError(error, ctx) {
12203
+ if (!(error instanceof Error)) {
12204
+ return createError(ErrorCodes.MONGODB_ERROR, `${ctx.operation} failed`, [{ ...ctx, reason: String(error) }]);
12205
+ }
12206
+ if (error.code !== void 0 && typeof error.code === "string" && String(error.code).startsWith("SEARCH_INDEX_")) {
12207
+ return error;
12208
+ }
12209
+ const { code, codeName, message } = readCode(error);
12210
+ const server = { serverCode: code, serverCodeName: codeName, serverMessage: message };
12211
+ switch (classifySearchIndexError(error)) {
12212
+ case "exists":
12213
+ return searchIndexError(ErrorCodes.SEARCH_INDEX_EXISTS, `Search index "${ctx.indexName}" already exists`, ctx, server, error);
12214
+ case "notFound":
12215
+ return searchIndexError(ErrorCodes.SEARCH_INDEX_NOT_FOUND, `Search index "${ctx.indexName}" was not found`, ctx, server, error);
12216
+ case "unsupported":
12217
+ 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);
12218
+ default:
12219
+ return attachContext(searchIndexError(ErrorCodes.MONGODB_ERROR, `${ctx.operation} failed: ${message}`, ctx, server, error), ctx);
12220
+ }
12221
+ }
12222
+
12223
+ // src/adapters/mongodb/search-indexes/normalize.ts
12224
+ function missingSearchIndexInfo(name) {
12225
+ return { name, status: "DOES_NOT_EXIST", queryable: false, typeSource: "unknown", raw: {} };
12226
+ }
12227
+ function normalizeSearchIndexInfo(doc) {
12228
+ const info = {
12229
+ name: String(doc.name),
12230
+ // Unknown future statuses are passed through verbatim rather than rewritten.
12231
+ status: typeof doc.status === "string" && doc.status.length > 0 ? doc.status : "PENDING",
12232
+ queryable: doc.queryable === true,
12233
+ typeSource: "unknown",
12234
+ raw: doc
12235
+ };
12236
+ if (typeof doc.id === "string") info.id = doc.id;
12237
+ const resolved = resolveSearchIndexType(doc);
12238
+ info.typeSource = resolved.typeSource;
12239
+ if (resolved.type !== void 0) info.type = resolved.type;
12240
+ if (doc.latestDefinition && typeof doc.latestDefinition === "object") info.latestDefinition = doc.latestDefinition;
12241
+ const version = doc.latestDefinitionVersion;
12242
+ if (version && typeof version === "object") {
12243
+ info.latestDefinitionVersion = { version: Number(version.version), createdAt: version.createdAt };
12244
+ }
12245
+ if (Array.isArray(doc.statusDetail)) info.statusDetail = doc.statusDetail;
12246
+ return info;
12247
+ }
12248
+ function resolveSearchIndexType(doc) {
12249
+ if (doc.type === "search" || doc.type === "vectorSearch") return { type: doc.type, typeSource: "server" };
12250
+ const definition = doc.latestDefinition;
12251
+ if (definition && typeof definition === "object" && !Array.isArray(definition)) {
12252
+ const fields = definition.fields;
12253
+ if (Array.isArray(fields) && fields.some((field) => field && typeof field === "object" && field.type === "vector")) {
12254
+ return { type: "vectorSearch", typeSource: "inferred" };
12255
+ }
12256
+ if (Object.prototype.hasOwnProperty.call(definition, "mappings")) return { type: "search", typeSource: "inferred" };
12257
+ }
12258
+ return { typeSource: "unknown" };
12259
+ }
12260
+ function matchesSearchIndexType(info, type) {
12261
+ return info.typeSource !== "unknown" && info.type === type;
12262
+ }
12263
+
12264
+ // src/adapters/mongodb/search-indexes/validate.ts
12265
+ var TYPES = /* @__PURE__ */ new Set(["search", "vectorSearch"]);
12266
+ var VECTOR_FIELD_TYPES = /* @__PURE__ */ new Set(["vector", "filter"]);
12267
+ var SIMILARITIES = /* @__PURE__ */ new Set(["euclidean", "cosine", "dotProduct"]);
12268
+ function invalid(operation, namespace, indexName, field, reason) {
12269
+ throw createError(
12270
+ ErrorCodes.INVALID_SEARCH_INDEX_DEFINITION,
12271
+ `${operation}: invalid ${field} (${reason})`,
12272
+ [{ operation, namespace, indexName: typeof indexName === "string" ? indexName : void 0, field, reason }]
12273
+ );
12274
+ }
12275
+ function isPlainObject4(value) {
12276
+ if (value === null || typeof value !== "object" || Array.isArray(value)) return false;
12277
+ const proto = Object.getPrototypeOf(value);
12278
+ return proto === Object.prototype || proto === null;
12279
+ }
12280
+ function assertIndexName(operation, namespace, name) {
12281
+ if (typeof name !== "string" || name.length === 0 || name.trim() !== name) {
12282
+ invalid(operation, namespace, name, "name", "non-empty string without leading or trailing whitespace required");
12283
+ }
12284
+ }
12285
+ function assertDefinition(operation, namespace, name, definition, type) {
12286
+ if (!isPlainObject4(definition)) invalid(operation, namespace, name, "definition", "plain object required");
12287
+ if (type !== "vectorSearch") return;
12288
+ const fields = definition.fields;
12289
+ if (!Array.isArray(fields) || fields.length === 0) {
12290
+ invalid(operation, namespace, name, "definition.fields", "non-empty array required for vectorSearch");
12291
+ }
12292
+ fields.forEach((field, index) => {
12293
+ const path5 = `definition.fields[${index}]`;
12294
+ if (!isPlainObject4(field)) invalid(operation, namespace, name, path5, "object required");
12295
+ if (typeof field.type !== "string" || !VECTOR_FIELD_TYPES.has(field.type)) {
12296
+ invalid(operation, namespace, name, `${path5}.type`, 'must be "vector" or "filter"');
12297
+ }
12298
+ if (typeof field.path !== "string" || field.path.length === 0) {
12299
+ invalid(operation, namespace, name, `${path5}.path`, "non-empty string required");
12300
+ }
12301
+ if (field.type === "vector") {
12302
+ const dims = field.numDimensions;
12303
+ if (typeof dims !== "number" || !Number.isInteger(dims) || dims <= 0) {
12304
+ invalid(operation, namespace, name, `${path5}.numDimensions`, "positive integer required");
12305
+ }
12306
+ if (typeof field.similarity !== "string" || !SIMILARITIES.has(field.similarity)) {
12307
+ invalid(operation, namespace, name, `${path5}.similarity`, 'must be "euclidean", "cosine", or "dotProduct"');
12308
+ }
12309
+ }
12310
+ });
12311
+ }
12312
+ function assertSpec(operation, namespace, spec) {
12313
+ if (!isPlainObject4(spec)) invalid(operation, namespace, void 0, "spec", "object required");
12314
+ assertIndexName(operation, namespace, spec.name);
12315
+ if (typeof spec.type !== "string" || !TYPES.has(spec.type)) {
12316
+ invalid(operation, namespace, spec.name, "type", 'must be "search" or "vectorSearch"');
12317
+ }
12318
+ assertDefinition(operation, namespace, spec.name, spec.definition, spec.type);
12319
+ }
12320
+ function assertOptionsObject(operation, namespace, name, options) {
12321
+ if (options !== void 0 && !isPlainObject4(options)) invalid(operation, namespace, name, "options", "object required");
12322
+ }
12323
+ function assertEnumOption(operation, namespace, name, field, value, allowed) {
12324
+ if (value !== void 0 && (typeof value !== "string" || !allowed.includes(value))) {
12325
+ invalid(operation, namespace, name, field, `must be one of ${allowed.map((v) => `"${v}"`).join(", ")}`);
12326
+ }
12327
+ }
12328
+ function assertBooleanOption(operation, namespace, name, field, value) {
12329
+ if (value !== void 0 && typeof value !== "boolean") invalid(operation, namespace, name, field, "boolean required");
12330
+ }
12331
+ function resolveWaitOptions(namespace, name, options) {
12332
+ const operation = "waitForSearchIndex";
12333
+ assertOptionsObject(operation, namespace, name, options);
12334
+ const opts = options ?? {};
12335
+ assertEnumOption(operation, namespace, name, "options.until", opts.until, ["queryable", "ready", "dropped"]);
12336
+ const timeoutMs = opts.timeoutMs ?? 3e5;
12337
+ const intervalMs = opts.intervalMs ?? 2e3;
12338
+ if (!Number.isInteger(timeoutMs) || timeoutMs <= 0) {
12339
+ invalid(operation, namespace, name, "options.timeoutMs", "positive integer required");
12340
+ }
12341
+ if (!Number.isInteger(intervalMs) || intervalMs < 100) {
12342
+ invalid(operation, namespace, name, "options.intervalMs", "integer >= 100 required");
12343
+ }
12344
+ const signal = opts.signal;
12345
+ if (signal !== void 0 && (signal === null || typeof signal !== "object" || typeof signal.aborted !== "boolean" || typeof signal.addEventListener !== "function")) {
12346
+ invalid(operation, namespace, name, "options.signal", "AbortSignal required");
12347
+ }
12348
+ return { until: opts.until ?? "queryable", timeoutMs, intervalMs, signal };
12349
+ }
12350
+
12351
+ // src/adapters/mongodb/search-indexes/operations.ts
12352
+ var SEARCH_INDEX_MISSING_GRACE_MS = 3e4;
12353
+ async function fetchByName(ctx, operation, name, signal) {
12354
+ try {
12355
+ const docs = await ctx.collection.listSearchIndexes(name, signal ? { signal } : {}).toArray();
12356
+ const match = docs.find((doc) => doc.name === name);
12357
+ return match ? normalizeSearchIndexInfo(match) : missingSearchIndexInfo(name);
12358
+ } catch (error) {
12359
+ if (signal?.aborted) throw signal.reason;
12360
+ if (classifySearchIndexError(error) === "namespaceNotFound") return missingSearchIndexInfo(name);
12361
+ throw mapSearchIndexError(error, { operation, namespace: ctx.namespace, indexName: name });
12362
+ }
12363
+ }
12364
+ async function getSearchIndexOp(ctx, name) {
12365
+ assertIndexName("getSearchIndex", ctx.namespace, name);
12366
+ return fetchByName(ctx, "getSearchIndex", name);
12367
+ }
12368
+ async function listSearchIndexesOp(ctx, options) {
12369
+ const operation = "listSearchIndexes";
12370
+ assertOptionsObject(operation, ctx.namespace, void 0, options);
12371
+ assertEnumOption(operation, ctx.namespace, void 0, "options.type", options?.type, ["search", "vectorSearch"]);
12372
+ let docs;
12373
+ try {
12374
+ docs = await ctx.collection.listSearchIndexes().toArray();
12375
+ } catch (error) {
12376
+ if (classifySearchIndexError(error) === "namespaceNotFound") return [];
12377
+ throw mapSearchIndexError(error, { operation, namespace: ctx.namespace });
12378
+ }
12379
+ const infos = docs.map(normalizeSearchIndexInfo);
12380
+ return options?.type ? infos.filter((info) => matchesSearchIndexType(info, options.type)) : infos;
12381
+ }
12382
+ async function createSearchIndexOp(ctx, spec, options) {
12383
+ const operation = "createSearchIndex";
12384
+ assertSpec(operation, ctx.namespace, spec);
12385
+ assertOptionsObject(operation, ctx.namespace, spec.name, options);
12386
+ assertEnumOption(operation, ctx.namespace, spec.name, "options.ifExists", options?.ifExists, ["error", "skip", "update"]);
12387
+ const ifExists = options?.ifExists ?? "error";
12388
+ const errCtx = { operation, namespace: ctx.namespace, indexName: spec.name };
12389
+ const existing = await fetchByName(ctx, operation, spec.name);
12390
+ if (existing.status !== "DOES_NOT_EXIST") {
12391
+ if (ifExists === "skip") return { name: spec.name, action: "skipped", created: false };
12392
+ if (ifExists === "error") {
12393
+ throw searchIndexError(ErrorCodes.SEARCH_INDEX_EXISTS, `Search index "${spec.name}" already exists`, errCtx);
12394
+ }
12395
+ if (existing.type !== void 0 && existing.type !== spec.type) {
12396
+ throw searchIndexError(
12397
+ ErrorCodes.INVALID_SEARCH_INDEX_DEFINITION,
12398
+ `Search index "${spec.name}" has type "${existing.type}"; drop it before recreating as "${spec.type}"`,
12399
+ errCtx,
12400
+ { field: "type", reason: "type change requires drop and create" }
12401
+ );
12402
+ }
12403
+ try {
12404
+ await ctx.collection.updateSearchIndex(spec.name, spec.definition);
12405
+ } catch (error) {
12406
+ throw mapSearchIndexError(error, errCtx);
12407
+ }
12408
+ return { name: spec.name, action: "updated", created: false };
12409
+ }
12410
+ try {
12411
+ const name = await ctx.collection.createSearchIndex({ name: spec.name, type: spec.type, definition: spec.definition });
12412
+ return { name: typeof name === "string" && name.length > 0 ? name : spec.name, action: "created", created: true };
12413
+ } catch (error) {
12414
+ const mapped = mapSearchIndexError(error, errCtx);
12415
+ if (mapped.code === ErrorCodes.SEARCH_INDEX_EXISTS && ifExists === "skip") {
12416
+ return { name: spec.name, action: "skipped", created: false };
12417
+ }
12418
+ throw mapped;
12419
+ }
12420
+ }
12421
+ async function createSearchIndexesOp(ctx, specs) {
12422
+ const operation = "createSearchIndexes";
12423
+ if (!Array.isArray(specs) || specs.length === 0) {
12424
+ throw searchIndexError(
12425
+ ErrorCodes.INVALID_SEARCH_INDEX_DEFINITION,
12426
+ `${operation}: specs must be a non-empty array`,
12427
+ { operation, namespace: ctx.namespace },
12428
+ { field: "specs", reason: "non-empty array required" }
12429
+ );
12430
+ }
12431
+ const seen = /* @__PURE__ */ new Set();
12432
+ for (const spec of specs) {
12433
+ assertSpec(operation, ctx.namespace, spec);
12434
+ if (seen.has(spec.name)) {
12435
+ throw searchIndexError(
12436
+ ErrorCodes.SEARCH_INDEX_EXISTS,
12437
+ `Search index "${spec.name}" is listed more than once`,
12438
+ { operation, namespace: ctx.namespace, indexName: spec.name }
12439
+ );
12440
+ }
12441
+ seen.add(spec.name);
12442
+ }
12443
+ try {
12444
+ return await ctx.collection.createSearchIndexes(specs.map(({ name, type, definition }) => ({ name, type, definition })));
12445
+ } catch (error) {
12446
+ throw mapSearchIndexError(error, { operation, namespace: ctx.namespace });
12447
+ }
12448
+ }
12449
+ async function updateSearchIndexOp(ctx, name, definition) {
12450
+ const operation = "updateSearchIndex";
12451
+ assertIndexName(operation, ctx.namespace, name);
12452
+ assertDefinition(operation, ctx.namespace, name, definition);
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
+ throw searchIndexError(ErrorCodes.SEARCH_INDEX_NOT_FOUND, `Search index "${name}" was not found`, errCtx);
12457
+ }
12458
+ if (existing.type !== void 0) assertDefinition(operation, ctx.namespace, name, definition, existing.type);
12459
+ try {
12460
+ await ctx.collection.updateSearchIndex(name, definition);
12461
+ } catch (error) {
12462
+ throw mapSearchIndexError(error, errCtx);
12463
+ }
12464
+ }
12465
+ async function dropSearchIndexOp(ctx, name, options) {
12466
+ const operation = "dropSearchIndex";
12467
+ assertIndexName(operation, ctx.namespace, name);
12468
+ assertOptionsObject(operation, ctx.namespace, name, options);
12469
+ assertBooleanOption(operation, ctx.namespace, name, "options.ignoreMissing", options?.ignoreMissing);
12470
+ const errCtx = { operation, namespace: ctx.namespace, indexName: name };
12471
+ const existing = await fetchByName(ctx, operation, name);
12472
+ if (existing.status === "DOES_NOT_EXIST") {
12473
+ if (options?.ignoreMissing) return;
12474
+ throw searchIndexError(ErrorCodes.SEARCH_INDEX_NOT_FOUND, `Search index "${name}" was not found`, errCtx);
12475
+ }
12476
+ try {
12477
+ await ctx.collection.dropSearchIndex(name);
12478
+ } catch (error) {
12479
+ const mapped = mapSearchIndexError(error, errCtx);
12480
+ if (mapped.code === ErrorCodes.SEARCH_INDEX_NOT_FOUND && options?.ignoreMissing) return;
12481
+ throw mapped;
12482
+ }
12483
+ }
12484
+ function sleep2(ms, signal) {
12485
+ return new Promise((resolve, reject) => {
12486
+ if (signal?.aborted) {
12487
+ reject(signal.reason);
12488
+ return;
12489
+ }
12490
+ const onAbort = () => {
12491
+ clearTimeout(timer);
12492
+ reject(signal?.reason);
12493
+ };
12494
+ const timer = setTimeout(() => {
12495
+ signal?.removeEventListener("abort", onAbort);
12496
+ resolve();
12497
+ }, ms);
12498
+ signal?.addEventListener("abort", onAbort, { once: true });
12499
+ });
12500
+ }
12501
+ function linkSignal(userSignal, remainingMs) {
12502
+ const controller = new AbortController();
12503
+ let expired = false;
12504
+ const onUserAbort = () => controller.abort(userSignal?.reason);
12505
+ userSignal?.addEventListener("abort", onUserAbort, { once: true });
12506
+ const timer = setTimeout(() => {
12507
+ expired = true;
12508
+ controller.abort(new Error("search index wait deadline reached"));
12509
+ }, Math.max(0, remainingMs));
12510
+ return {
12511
+ signal: controller.signal,
12512
+ timedOut: () => expired,
12513
+ dispose: () => {
12514
+ clearTimeout(timer);
12515
+ userSignal?.removeEventListener("abort", onUserAbort);
12516
+ }
12517
+ };
12518
+ }
12519
+ async function waitForSearchIndexOp(ctx, name, options, clock = Date.now) {
12520
+ const operation = "waitForSearchIndex";
12521
+ assertIndexName(operation, ctx.namespace, name);
12522
+ const { until, timeoutMs, intervalMs, signal } = resolveWaitOptions(ctx.namespace, name, options);
12523
+ const errCtx = { operation, namespace: ctx.namespace, indexName: name };
12524
+ if (signal?.aborted) throw signal.reason;
12525
+ const start = clock();
12526
+ const grace = Math.min(SEARCH_INDEX_MISSING_GRACE_MS, timeoutMs);
12527
+ let last;
12528
+ const timeoutError = () => searchIndexError(
12529
+ ErrorCodes.SEARCH_INDEX_WAIT_TIMEOUT,
12530
+ `Timed out after ${timeoutMs}ms waiting for Search index "${name}" to become ${until}`,
12531
+ errCtx,
12532
+ { until, timeoutMs, last }
12533
+ );
12534
+ for (; ; ) {
12535
+ const link = linkSignal(signal, timeoutMs - (clock() - start));
12536
+ let info;
12537
+ try {
12538
+ info = await fetchByName(ctx, operation, name, link.signal);
12539
+ } catch (error) {
12540
+ if (signal?.aborted) throw signal.reason;
12541
+ if (link.timedOut()) throw timeoutError();
12542
+ throw error;
12543
+ } finally {
12544
+ link.dispose();
12545
+ }
12546
+ last = info;
12547
+ if (signal?.aborted) throw signal.reason;
12548
+ if (until === "dropped") {
12549
+ if (info.status === "DOES_NOT_EXIST") return info;
12550
+ } else if (info.status === "DOES_NOT_EXIST") {
12551
+ if (clock() - start >= grace) {
12552
+ throw searchIndexError(
12553
+ ErrorCodes.SEARCH_INDEX_NOT_FOUND,
12554
+ `Search index "${name}" was not found within ${grace}ms`,
12555
+ errCtx,
12556
+ { until, graceMs: grace }
12557
+ );
12558
+ }
12559
+ } else if (until === "queryable") {
12560
+ if (info.queryable) return info;
12561
+ if (info.status === "FAILED") {
12562
+ throw searchIndexError(
12563
+ ErrorCodes.SEARCH_INDEX_BUILD_FAILED,
12564
+ `Search index "${name}" build failed`,
12565
+ errCtx,
12566
+ { status: info.status, statusDetail: info.statusDetail }
12567
+ );
12568
+ }
12569
+ } else {
12570
+ if (info.status === "READY") return info;
12571
+ if (info.status === "FAILED") {
12572
+ throw searchIndexError(
12573
+ ErrorCodes.SEARCH_INDEX_BUILD_FAILED,
12574
+ `Search index "${name}" build failed`,
12575
+ errCtx,
12576
+ { status: info.status, statusDetail: info.statusDetail }
12577
+ );
12578
+ }
12579
+ }
12580
+ const remaining = timeoutMs - (clock() - start);
12581
+ if (remaining <= 0) throw timeoutError();
12582
+ await sleep2(Math.min(intervalMs, remaining), signal);
12583
+ }
12584
+ }
12585
+
12121
12586
  // src/adapters/mongodb/common/collection-accessor-cache-helpers.ts
12122
12587
  var import_node_util4 = require("node:util");
12123
12588
  function isRecord2(value) {
@@ -12478,7 +12943,7 @@ async function invalidateReadCachesAfterWrite(queryCache, collection, dbName, co
12478
12943
  }
12479
12944
 
12480
12945
  // src/adapters/mongodb/writes/write-utils.ts
12481
- function sleep2(ms) {
12946
+ function sleep3(ms) {
12482
12947
  return new Promise((resolve) => setTimeout(resolve, ms));
12483
12948
  }
12484
12949
  function splitIntoBatches(items, batchSize) {
@@ -12898,7 +13363,7 @@ async function insertBatchDocuments(collection, documents, options = {}) {
12898
13363
  result.retries.push(retryInfo);
12899
13364
  onRetry?.(retryInfo);
12900
13365
  if (retryDelay > 0) {
12901
- await sleep2(retryDelay);
13366
+ await sleep3(retryDelay);
12902
13367
  }
12903
13368
  continue;
12904
13369
  }
@@ -13027,7 +13492,7 @@ async function updateBatchDocuments(collection, filter, update, options = {}) {
13027
13492
  result.retries.push(retryInfo);
13028
13493
  onRetry?.(retryInfo);
13029
13494
  if (retryDelay > 0) {
13030
- await sleep2(retryDelay);
13495
+ await sleep3(retryDelay);
13031
13496
  }
13032
13497
  continue;
13033
13498
  }
@@ -13122,7 +13587,7 @@ async function deleteBatchDocuments(collection, filter, options = {}) {
13122
13587
  result.retries.push(retryInfo);
13123
13588
  onRetry?.(retryInfo);
13124
13589
  if (retryDelay > 0) {
13125
- await sleep2(retryDelay);
13590
+ await sleep3(retryDelay);
13126
13591
  }
13127
13592
  continue;
13128
13593
  }
@@ -13619,7 +14084,7 @@ var MongoCollectionAccessor = class {
13619
14084
  const { query: normalizedQuery, options: driverOptions } = snapshotReadInputs(this._cvFilter(query), normalizeFindProjectionOptions(rawOptions), this.collectionRef.bsonOptions);
13620
14085
  const cacheTTL = rawOptions.explain ? 0 : typeof rawOptions.cache === "number" ? rawOptions.cache : 0;
13621
14086
  if (cacheTTL > 0 && this.management.queryCache && canCacheBsonResult(driverOptions) && !hasSessionOption(driverOptions) && !await isCollectionCacheBarrierActive(this.management.queryCache, this.collectionRef, this.management.defaults)) {
13622
- const cacheKey = buildFindOneCacheKey(this.collectionRef, this.management.defaults, normalizedQuery, driverOptions);
14087
+ const cacheKey = buildFindOneCacheKey(this.collectionRef, this.management.defaults, normalizedQuery, driverOptions, true);
13623
14088
  const result2 = await readThroughCache(this.management.queryCache, cacheKey, cacheTTL, () => findOneDocument(this.collectionRef, normalizedQuery, driverOptions));
13624
14089
  return wrapQueryResultWithMeta(this.collectionRef, this.management.defaults, "findOne", rawOptions, startTs, result2);
13625
14090
  }
@@ -13676,7 +14141,7 @@ var MongoCollectionAccessor = class {
13676
14141
  );
13677
14142
  const countQueue = this.management.defaults?.countQueue;
13678
14143
  if (cacheTTL > 0 && this.management.queryCache && canCacheBsonCount(keyOptions) && !hasSessionOption(keyOptions) && !await isCollectionCacheBarrierActive(this.management.queryCache, this.collectionRef, this.management.defaults)) {
13679
- const cacheKey = buildCountCacheKey(this.collectionRef, this.management.defaults, normalizedQuery, keyOptions);
14144
+ const cacheKey = buildCountCacheKey(this.collectionRef, this.management.defaults, normalizedQuery, keyOptions, true);
13680
14145
  const result = await readThroughCache(
13681
14146
  this.management.queryCache,
13682
14147
  cacheKey,
@@ -13769,7 +14234,7 @@ var MongoCollectionAccessor = class {
13769
14234
  driverOptions
13770
14235
  );
13771
14236
  if (cacheTTL > 0 && this.management.queryCache && canCacheBsonResult(driverOptions) && !hasSessionOption(driverOptions) && !await isCollectionCacheBarrierActive(this.management.queryCache, this.collectionRef, this.management.defaults)) {
13772
- const cacheKey = buildDistinctCacheKey(this.collectionRef, this.management.defaults, key, normalizedQuery, driverOptions);
14237
+ const cacheKey = buildDistinctCacheKey(this.collectionRef, this.management.defaults, key, normalizedQuery, driverOptions, true);
13773
14238
  const result2 = await readThroughCache(this.management.queryCache, cacheKey, cacheTTL, executeDistinct);
13774
14239
  return wrapQueryResultWithMeta(this.collectionRef, this.management.defaults, "distinct", rawOptions, startTs, result2);
13775
14240
  }
@@ -13890,6 +14355,35 @@ var MongoCollectionAccessor = class {
13890
14355
  this.assertWritePath("dropIndexes", "management");
13891
14356
  return dropIndexesForAccessor(this.collectionRef);
13892
14357
  }
14358
+ /** Search index lifecycle (v3.4.0); writes are governed by writePathPolicy `management`. */
14359
+ searchIndexContext() {
14360
+ return { collection: this.collectionRef, namespace: this.getNamespace() };
14361
+ }
14362
+ async createSearchIndex(spec, options) {
14363
+ this.assertWritePath("createSearchIndex", "management");
14364
+ return createSearchIndexOp(this.searchIndexContext(), spec, options);
14365
+ }
14366
+ async createSearchIndexes(specs) {
14367
+ this.assertWritePath("createSearchIndexes", "management");
14368
+ return createSearchIndexesOp(this.searchIndexContext(), specs);
14369
+ }
14370
+ async listSearchIndexes(options) {
14371
+ return listSearchIndexesOp(this.searchIndexContext(), options);
14372
+ }
14373
+ async getSearchIndex(name) {
14374
+ return getSearchIndexOp(this.searchIndexContext(), name);
14375
+ }
14376
+ async updateSearchIndex(name, definition) {
14377
+ this.assertWritePath("updateSearchIndex", "management");
14378
+ return updateSearchIndexOp(this.searchIndexContext(), name, definition);
14379
+ }
14380
+ async dropSearchIndex(name, options) {
14381
+ this.assertWritePath("dropSearchIndex", "management");
14382
+ return dropSearchIndexOp(this.searchIndexContext(), name, options);
14383
+ }
14384
+ async waitForSearchIndex(name, options) {
14385
+ return waitForSearchIndexOp(this.searchIndexContext(), name, options);
14386
+ }
13893
14387
  /** Pre-populates bookmark cache entries for the specified key dimensions and page numbers. */
13894
14388
  async prewarmBookmarks(keyDims = {}, pages = []) {
13895
14389
  return prewarmBookmarksForAccessor(this.bookmarkContext(), keyDims, pages);
@@ -14774,7 +15268,7 @@ var LockManager = class {
14774
15268
  break;
14775
15269
  }
14776
15270
  const delay3 = retryDelay * Math.pow(retryBackoff, attempt);
14777
- await sleep3(delay3);
15271
+ await sleep4(delay3);
14778
15272
  }
14779
15273
  this.stats.errors += 1;
14780
15274
  if (options.fallbackToNoLock) {
@@ -14886,7 +15380,7 @@ var LockManager = class {
14886
15380
  }
14887
15381
  }
14888
15382
  };
14889
- async function sleep3(ms) {
15383
+ async function sleep4(ms) {
14890
15384
  await new Promise((resolve) => setTimeout(resolve, ms));
14891
15385
  }
14892
15386
  var DistributedCacheLockManager = class {
@@ -15011,10 +15505,10 @@ var DistributedCacheLockManager = class {
15011
15505
  return new Lock(key, lockId, this, ttl, startedAt);
15012
15506
  }
15013
15507
  if (attempt === retryTimes) break;
15014
- await sleep3(retryDelay);
15508
+ await sleep4(retryDelay);
15015
15509
  } catch (error) {
15016
15510
  if (attempt === retryTimes) throw error;
15017
- await sleep3(retryDelay);
15511
+ await sleep4(retryDelay);
15018
15512
  }
15019
15513
  }
15020
15514
  this.stats.errors++;
@@ -18040,7 +18534,7 @@ var DEFAULT_DATA_TASK_MAX_DOCUMENTS = 1e4;
18040
18534
  var DEFAULT_DATA_TASK_BACKUP_MAX_BYTES = 256 * 1024 * 1024;
18041
18535
  var ENVIRONMENTS = /* @__PURE__ */ new Set(["development", "test", "staging", "production", "prod", "live"]);
18042
18536
  var PRODUCTION_ENVIRONMENTS = /* @__PURE__ */ new Set(["production", "prod", "live"]);
18043
- function invalid(message, collection) {
18537
+ function invalid2(message, collection) {
18044
18538
  throw new DataTaskJobError("INVALID_JOB", message, "validate", collection);
18045
18539
  }
18046
18540
  function canonicalize(value) {
@@ -18078,15 +18572,15 @@ function connectionDescriptor(value) {
18078
18572
  }
18079
18573
  function validateConnection(value, field) {
18080
18574
  if (isDataTaskJobRuntime(value)) return;
18081
- if (!isRecord3(value) || value.type !== "mongodb") invalid(`${field} must be a monSQLize runtime or MongoDB MonSQLizeOptions.`);
18082
- if (typeof value.databaseName !== "string" || value.databaseName.trim() === "") invalid(`${field}.databaseName is required.`);
18083
- if (!isRecord3(value.config) || typeof value.config.uri !== "string" || value.config.uri.trim() === "") invalid(`${field}.config.uri is required.`);
18575
+ if (!isRecord3(value) || value.type !== "mongodb") invalid2(`${field} must be a monSQLize runtime or MongoDB MonSQLizeOptions.`);
18576
+ if (typeof value.databaseName !== "string" || value.databaseName.trim() === "") invalid2(`${field}.databaseName is required.`);
18577
+ if (!isRecord3(value.config) || typeof value.config.uri !== "string" || value.config.uri.trim() === "") invalid2(`${field}.config.uri is required.`);
18084
18578
  }
18085
18579
  function uniqueFields(value, pathName, collection) {
18086
- if (!Array.isArray(value) || value.length === 0) invalid(`${pathName} must be a non-empty string array.`, collection);
18580
+ if (!Array.isArray(value) || value.length === 0) invalid2(`${pathName} must be a non-empty string array.`, collection);
18087
18581
  const fields = value.map((item) => typeof item === "string" ? item.trim() : "");
18088
- if (fields.some((item) => item === "")) invalid(`${pathName} must contain non-empty strings.`, collection);
18089
- if (new Set(fields).size !== fields.length) invalid(`${pathName} must not contain duplicates.`, collection);
18582
+ if (fields.some((item) => item === "")) invalid2(`${pathName} must contain non-empty strings.`, collection);
18583
+ if (new Set(fields).size !== fields.length) invalid2(`${pathName} must not contain duplicates.`, collection);
18090
18584
  return fields;
18091
18585
  }
18092
18586
  var FORBIDDEN_FIELD_PATH_SEGMENTS = /* @__PURE__ */ new Set(["__proto__", "prototype", "constructor"]);
@@ -18097,31 +18591,31 @@ function assertBsonLiteral(value, pathName, collection, stack = /* @__PURE__ */
18097
18591
  const valueType = typeof value;
18098
18592
  if (value === null || BSON_PRIMITIVE_TYPES.has(valueType)) return;
18099
18593
  if (BSON_UNSUPPORTED_TYPES.has(valueType)) {
18100
- invalid(`${pathName} must contain only BSON-compatible literals.`, collection);
18594
+ invalid2(`${pathName} must contain only BSON-compatible literals.`, collection);
18101
18595
  }
18102
18596
  if (value instanceof Date || value instanceof RegExp || Buffer.isBuffer(value)) return;
18103
18597
  if (Array.isArray(value)) {
18104
- if (stack.has(value)) invalid(`${pathName} must not contain circular values.`, collection);
18598
+ if (stack.has(value)) invalid2(`${pathName} must not contain circular values.`, collection);
18105
18599
  stack.add(value);
18106
18600
  value.forEach((item, position) => assertBsonLiteral(item, `${pathName}[${position}]`, collection, stack));
18107
18601
  stack.delete(value);
18108
18602
  return;
18109
18603
  }
18110
- if (!isRecord3(value)) invalid(`${pathName} must contain only BSON-compatible literals.`, collection);
18604
+ if (!isRecord3(value)) invalid2(`${pathName} must contain only BSON-compatible literals.`, collection);
18111
18605
  if (typeof value._bsontype === "string") return;
18112
18606
  const prototype = Object.getPrototypeOf(value);
18113
- if (!PLAIN_OBJECT_PROTOTYPES.has(prototype)) invalid(`${pathName} must contain only BSON-compatible literals.`, collection);
18114
- if (stack.has(value)) invalid(`${pathName} must not contain circular values.`, collection);
18607
+ if (!PLAIN_OBJECT_PROTOTYPES.has(prototype)) invalid2(`${pathName} must contain only BSON-compatible literals.`, collection);
18608
+ if (stack.has(value)) invalid2(`${pathName} must not contain circular values.`, collection);
18115
18609
  stack.add(value);
18116
18610
  for (const [key, item] of Object.entries(value)) assertBsonLiteral(item, `${pathName}.${key}`, collection, stack);
18117
18611
  stack.delete(value);
18118
18612
  }
18119
18613
  function normalizeFieldPath(value, pathName, collection) {
18120
- if (typeof value !== "string" || value.trim() === "") invalid(`${pathName} must be a non-empty field path.`, collection);
18614
+ if (typeof value !== "string" || value.trim() === "") invalid2(`${pathName} must be a non-empty field path.`, collection);
18121
18615
  const field = value.trim();
18122
18616
  const segments = field.split(".");
18123
18617
  if (segments.some((segment) => segment === "" || segment.startsWith("$") || FORBIDDEN_FIELD_PATH_SEGMENTS.has(segment))) {
18124
- invalid(`${pathName} contains an unsafe field path.`, collection);
18618
+ invalid2(`${pathName} contains an unsafe field path.`, collection);
18125
18619
  }
18126
18620
  return field;
18127
18621
  }
@@ -18137,13 +18631,13 @@ function projectionIncludes(projection, field) {
18137
18631
  return included.some((key) => key === field || field.startsWith(`${key}.`));
18138
18632
  }
18139
18633
  function normalizeFieldPatch(data, identityFields, collection) {
18140
- if (data.rename !== void 0 && !isRecord3(data.rename)) invalid("data.rename must be an object.", collection);
18141
- if (data.set !== void 0 && !isRecord3(data.set)) invalid("data.set must be an object.", collection);
18142
- if (data.unset !== void 0 && !Array.isArray(data.unset)) invalid("data.unset must be an array.", collection);
18634
+ if (data.rename !== void 0 && !isRecord3(data.rename)) invalid2("data.rename must be an object.", collection);
18635
+ if (data.set !== void 0 && !isRecord3(data.set)) invalid2("data.set must be an object.", collection);
18636
+ if (data.unset !== void 0 && !Array.isArray(data.unset)) invalid2("data.unset must be an array.", collection);
18143
18637
  const renameEntries = Object.entries(data.rename ?? {}).map(([source, destination], position) => {
18144
18638
  const normalizedSource = normalizeFieldPath(source, `data.rename source ${position}`, collection);
18145
18639
  const normalizedDestination = normalizeFieldPath(destination, `data.rename.${source}`, collection);
18146
- if (pathsOverlap(normalizedSource, normalizedDestination)) invalid("data.rename source and destination must not overlap.", collection);
18640
+ if (pathsOverlap(normalizedSource, normalizedDestination)) invalid2("data.rename source and destination must not overlap.", collection);
18147
18641
  return [normalizedSource, normalizedDestination];
18148
18642
  });
18149
18643
  const setEntries = Object.entries(data.set ?? {}).map(([field, value], position) => {
@@ -18152,14 +18646,14 @@ function normalizeFieldPatch(data, identityFields, collection) {
18152
18646
  try {
18153
18647
  canonicalStringify(value);
18154
18648
  } catch {
18155
- invalid(`data.set.${field} must be a BSON-compatible literal.`, collection);
18649
+ invalid2(`data.set.${field} must be a BSON-compatible literal.`, collection);
18156
18650
  }
18157
18651
  return [normalizedField, value];
18158
18652
  });
18159
18653
  const unset = (data.unset ?? []).map((field, position) => normalizeFieldPath(field, `data.unset[${position}]`, collection));
18160
- if (new Set(unset).size !== unset.length) invalid("data.unset must not contain duplicates.", collection);
18654
+ if (new Set(unset).size !== unset.length) invalid2("data.unset must not contain duplicates.", collection);
18161
18655
  const destinations = renameEntries.map(([, destination]) => destination);
18162
- if (new Set(destinations).size !== destinations.length) invalid("data.rename destinations must be unique.", collection);
18656
+ if (new Set(destinations).size !== destinations.length) invalid2("data.rename destinations must be unique.", collection);
18163
18657
  const configuredPaths = [
18164
18658
  ...renameEntries.flatMap(([source, destination]) => [
18165
18659
  { path: source, label: `rename source "${source}"` },
@@ -18171,18 +18665,18 @@ function normalizeFieldPatch(data, identityFields, collection) {
18171
18665
  for (let left = 0; left < configuredPaths.length; left += 1) {
18172
18666
  for (let right = left + 1; right < configuredPaths.length; right += 1) {
18173
18667
  if (pathsOverlap(configuredPaths[left].path, configuredPaths[right].path)) {
18174
- invalid(`${configuredPaths[left].label} conflicts with ${configuredPaths[right].label}.`, collection);
18668
+ invalid2(`${configuredPaths[left].label} conflicts with ${configuredPaths[right].label}.`, collection);
18175
18669
  }
18176
18670
  }
18177
18671
  }
18178
18672
  for (const configured of configuredPaths) {
18179
18673
  if (configured.path === "_id" || configured.path.startsWith("_id.") || identityFields.some((identity) => pathsOverlap(configured.path, identity))) {
18180
- invalid(`${configured.label} must not overlap _id or an identity field.`, collection);
18674
+ invalid2(`${configured.label} must not overlap _id or an identity field.`, collection);
18181
18675
  }
18182
18676
  }
18183
18677
  if (data.projection) {
18184
18678
  for (const required of [...identityFields, ...renameEntries.map(([source]) => source)]) {
18185
- if (!projectionIncludes(data.projection, required)) invalid(`data.projection must include required field "${required}".`, collection);
18679
+ if (!projectionIncludes(data.projection, required)) invalid2(`data.projection must include required field "${required}".`, collection);
18186
18680
  }
18187
18681
  }
18188
18682
  return {
@@ -18193,105 +18687,105 @@ function normalizeFieldPatch(data, identityFields, collection) {
18193
18687
  }
18194
18688
  function normalizeData(data, collection) {
18195
18689
  if (data === void 0) return void 0;
18196
- if (!isRecord3(data)) invalid("data must be an object.", collection);
18690
+ if (!isRecord3(data)) invalid2("data must be an object.", collection);
18197
18691
  const hasFilter = isRecord3(data.filter) && Object.keys(data.filter).length > 0;
18198
- if (hasFilter === (data.all === true)) invalid("data requires exactly one of a non-empty filter or all: true.", collection);
18692
+ if (hasFilter === (data.all === true)) invalid2("data requires exactly one of a non-empty filter or all: true.", collection);
18199
18693
  if (!isRecord3(data.identity) || data.identity.mode !== "fields" && data.identity.mode !== "source-id") {
18200
- invalid("data.identity.mode must be fields or source-id.", collection);
18694
+ invalid2("data.identity.mode must be fields or source-id.", collection);
18201
18695
  }
18202
18696
  const identity = data.identity.mode === "fields" ? { mode: "fields", fields: uniqueFields(data.identity.fields, "identity.fields", collection) } : {
18203
18697
  mode: "source-id",
18204
18698
  ...data.identity.conflictBy === void 0 ? {} : { conflictBy: uniqueFields(data.identity.conflictBy, "identity.conflictBy", collection) }
18205
18699
  };
18206
- if (data.projection !== void 0 && !isRecord3(data.projection)) invalid("data.projection must be an object.", collection);
18700
+ if (data.projection !== void 0 && !isRecord3(data.projection)) invalid2("data.projection must be an object.", collection);
18207
18701
  if (data.projection) assertBsonLiteral(data.projection, "data.projection", collection);
18208
18702
  if (hasFilter) assertBsonLiteral(data.filter, "data.filter", collection);
18209
18703
  const identityFields = identity.mode === "fields" ? identity.fields : ["_id", ...identity.conflictBy ?? []];
18210
18704
  const patch = normalizeFieldPatch(data, identityFields, collection);
18211
18705
  const batchSize = data.batchSize ?? 500;
18212
- if (!Number.isInteger(batchSize) || batchSize < 1 || batchSize > 1e4) invalid("data.batchSize must be an integer from 1 to 10000.", collection);
18706
+ if (!Number.isInteger(batchSize) || batchSize < 1 || batchSize > 1e4) invalid2("data.batchSize must be an integer from 1 to 10000.", collection);
18213
18707
  const maxDocuments = data.maxDocuments ?? DEFAULT_DATA_TASK_MAX_DOCUMENTS;
18214
18708
  if (!Number.isInteger(maxDocuments) || maxDocuments < 1 || maxDocuments > 1e6) {
18215
- invalid("data.maxDocuments must be an integer from 1 to 1000000.", collection);
18709
+ invalid2("data.maxDocuments must be an integer from 1 to 1000000.", collection);
18216
18710
  }
18217
18711
  const strategy = data.strategy ?? "upsert";
18218
- if (strategy !== "upsert" && strategy !== "insert") invalid("data.strategy must be upsert or insert.", collection);
18712
+ if (strategy !== "upsert" && strategy !== "insert") invalid2("data.strategy must be upsert or insert.", collection);
18219
18713
  return { ...data, ...patch, filter: hasFilter ? data.filter : {}, all: data.all === true, identity, strategy, batchSize, maxDocuments };
18220
18714
  }
18221
18715
  function normalizeIndexes(indexes, collection) {
18222
18716
  if (indexes === void 0) return [];
18223
- if (!Array.isArray(indexes)) invalid("indexes must be an array.", collection);
18717
+ if (!Array.isArray(indexes)) invalid2("indexes must be an array.", collection);
18224
18718
  const names = /* @__PURE__ */ new Set();
18225
18719
  return indexes.map((index, position) => {
18226
- if (!isRecord3(index) || !isRecord3(index.key) || Object.keys(index.key).length === 0) invalid(`indexes[${position}].key must be non-empty.`, collection);
18720
+ if (!isRecord3(index) || !isRecord3(index.key) || Object.keys(index.key).length === 0) invalid2(`indexes[${position}].key must be non-empty.`, collection);
18227
18721
  assertBsonLiteral(index.key, `indexes[${position}].key`, collection);
18228
- if (index.name !== void 0 && (typeof index.name !== "string" || index.name.trim() === "")) invalid(`indexes[${position}].name must be non-empty.`, collection);
18229
- if (index.name && names.has(index.name)) invalid(`duplicate index name "${index.name}".`, collection);
18722
+ if (index.name !== void 0 && (typeof index.name !== "string" || index.name.trim() === "")) invalid2(`indexes[${position}].name must be non-empty.`, collection);
18723
+ if (index.name && names.has(index.name)) invalid2(`duplicate index name "${index.name}".`, collection);
18230
18724
  if (index.name) names.add(index.name);
18231
- if (index.options !== void 0 && !isRecord3(index.options)) invalid(`indexes[${position}].options must be an object.`, collection);
18725
+ if (index.options !== void 0 && !isRecord3(index.options)) invalid2(`indexes[${position}].options must be an object.`, collection);
18232
18726
  if (index.options) assertBsonLiteral(index.options, `indexes[${position}].options`, collection);
18233
- if (index.options && "name" in index.options) invalid(`indexes[${position}].options.name is not allowed; use indexes[${position}].name.`, collection);
18234
- 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);
18727
+ if (index.options && "name" in index.options) invalid2(`indexes[${position}].options.name is not allowed; use indexes[${position}].name.`, collection);
18728
+ 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);
18235
18729
  return { key: { ...index.key }, ...index.name ? { name: index.name } : {}, options: { ...index.options ?? {} } };
18236
18730
  });
18237
18731
  }
18238
18732
  function normalizeVerify(verify, collection) {
18239
- if (verify !== void 0 && !isRecord3(verify)) invalid("verify must be an object.", collection);
18733
+ if (verify !== void 0 && !isRecord3(verify)) invalid2("verify must be an object.", collection);
18240
18734
  const input = verify;
18241
18735
  const mode = input?.mode ?? "sample";
18242
- if (mode !== "sample" && mode !== "full") invalid("verify.mode must be sample or full.", collection);
18736
+ if (mode !== "sample" && mode !== "full") invalid2("verify.mode must be sample or full.", collection);
18243
18737
  const sampleSize = input?.sampleSize ?? 20;
18244
- if (!Number.isInteger(sampleSize) || sampleSize < 1 || sampleSize > 1e3) invalid("verify.sampleSize must be an integer from 1 to 1000.", collection);
18738
+ if (!Number.isInteger(sampleSize) || sampleSize < 1 || sampleSize > 1e3) invalid2("verify.sampleSize must be an integer from 1 to 1000.", collection);
18245
18739
  const fields = input?.fields === void 0 ? void 0 : uniqueFields(input.fields, "verify.fields", collection);
18246
18740
  return { mode, sampleSize, ...fields ? { fields } : {} };
18247
18741
  }
18248
18742
  function normalizeBackup(backup, production) {
18249
- if (backup !== void 0 && !isRecord3(backup)) invalid("backup must be an object.");
18743
+ if (backup !== void 0 && !isRecord3(backup)) invalid2("backup must be an object.");
18250
18744
  const input = backup;
18251
- if (production && (typeof input?.dir !== "string" || input.dir.trim() === "")) invalid("production jobs require an explicit durable backup.dir.");
18745
+ if (production && (typeof input?.dir !== "string" || input.dir.trim() === "")) invalid2("production jobs require an explicit durable backup.dir.");
18252
18746
  const dir = typeof input?.dir === "string" && input.dir.trim() ? input.dir : import_node_path3.default.join(import_node_os2.default.tmpdir(), "monsqlize-data-tasks");
18253
18747
  const compression = input?.compression ?? "gzip";
18254
- if (compression !== "gzip" && compression !== "none") invalid("backup.compression must be gzip or none.");
18255
- if (input?.format !== void 0 && input.format !== "extended-jsonl") invalid("backup.format must be extended-jsonl.");
18748
+ if (compression !== "gzip" && compression !== "none") invalid2("backup.compression must be gzip or none.");
18749
+ if (input?.format !== void 0 && input.format !== "extended-jsonl") invalid2("backup.format must be extended-jsonl.");
18256
18750
  const retentionDays = input?.retentionDays ?? 7;
18257
- if (!Number.isInteger(retentionDays) || retentionDays < 1) invalid("backup.retentionDays must be a positive integer.");
18751
+ if (!Number.isInteger(retentionDays) || retentionDays < 1) invalid2("backup.retentionDays must be a positive integer.");
18258
18752
  const maxBytes = input?.maxBytes ?? DEFAULT_DATA_TASK_BACKUP_MAX_BYTES;
18259
- if (!Number.isSafeInteger(maxBytes) || maxBytes < 1) invalid("backup.maxBytes must be a positive safe integer.");
18753
+ if (!Number.isSafeInteger(maxBytes) || maxBytes < 1) invalid2("backup.maxBytes must be a positive safe integer.");
18260
18754
  return { dir, format: "extended-jsonl", compression, retentionDays, maxBytes };
18261
18755
  }
18262
18756
  function normalizeLock(lock) {
18263
18757
  if (!lock) return false;
18264
- if (lock !== true && !isRecord3(lock)) invalid("lock must be a boolean or lease options object.");
18758
+ if (lock !== true && !isRecord3(lock)) invalid2("lock must be a boolean or lease options object.");
18265
18759
  const options = lock === true ? {} : lock;
18266
18760
  const ttlMs = options.ttlMs ?? 12e4;
18267
18761
  const waitTimeoutMs = options.waitTimeoutMs ?? 0;
18268
- if (!Number.isInteger(ttlMs) || ttlMs < 1e3) invalid("lock.ttlMs must be an integer of at least 1000.");
18269
- if (!Number.isInteger(waitTimeoutMs) || waitTimeoutMs < 0) invalid("lock.waitTimeoutMs must be a non-negative integer.");
18762
+ if (!Number.isInteger(ttlMs) || ttlMs < 1e3) invalid2("lock.ttlMs must be an integer of at least 1000.");
18763
+ if (!Number.isInteger(waitTimeoutMs) || waitTimeoutMs < 0) invalid2("lock.waitTimeoutMs must be a non-negative integer.");
18270
18764
  return { ttlMs, waitTimeoutMs };
18271
18765
  }
18272
18766
  function normalizeDataTaskJob(job) {
18273
- if (!isRecord3(job)) invalid("job must be an object.");
18274
- if (typeof job.name !== "string" || job.name.trim() === "") invalid("job.name is required.");
18767
+ if (!isRecord3(job)) invalid2("job must be an object.");
18768
+ if (typeof job.name !== "string" || job.name.trim() === "") invalid2("job.name is required.");
18275
18769
  validateConnection(job.source, "source");
18276
18770
  validateConnection(job.target, "target");
18277
- if (job.source === job.target) invalid("source and target must be independent runtimes or connection options.");
18771
+ if (job.source === job.target) invalid2("source and target must be independent runtimes or connection options.");
18278
18772
  const sourceOptions = connectionOptions(job.source);
18279
18773
  const targetOptions = connectionOptions(job.target);
18280
- if (sourceOptions && targetOptions && sourceOptions.databaseName === targetOptions.databaseName && sourceOptions.config?.uri === targetOptions.config?.uri) invalid("source and target resolve to the same MongoDB database.");
18774
+ if (sourceOptions && targetOptions && sourceOptions.databaseName === targetOptions.databaseName && sourceOptions.config?.uri === targetOptions.config?.uri) invalid2("source and target resolve to the same MongoDB database.");
18281
18775
  if (typeof job.targetEnvironment !== "string" || !ENVIRONMENTS.has(job.targetEnvironment.toLowerCase())) {
18282
- invalid("targetEnvironment must be development, test, staging, production, prod, or live.");
18776
+ invalid2("targetEnvironment must be development, test, staging, production, prod, or live.");
18283
18777
  }
18284
- if (!Array.isArray(job.collections) || job.collections.length === 0) invalid("collections must be a non-empty array.");
18778
+ if (!Array.isArray(job.collections) || job.collections.length === 0) invalid2("collections must be a non-empty array.");
18285
18779
  const targetNames = /* @__PURE__ */ new Set();
18286
18780
  const collections = job.collections.map((collection, position) => {
18287
- if (!isRecord3(collection) || typeof collection.name !== "string" || collection.name.trim() === "") invalid(`collections[${position}].name is required.`);
18781
+ if (!isRecord3(collection) || typeof collection.name !== "string" || collection.name.trim() === "") invalid2(`collections[${position}].name is required.`);
18288
18782
  const name = collection.name.trim();
18289
18783
  const targetName = typeof collection.targetName === "string" && collection.targetName.trim() ? collection.targetName.trim() : name;
18290
- if (targetNames.has(targetName)) invalid(`target collection "${targetName}" is configured more than once.`, name);
18784
+ if (targetNames.has(targetName)) invalid2(`target collection "${targetName}" is configured more than once.`, name);
18291
18785
  targetNames.add(targetName);
18292
18786
  const indexes = normalizeIndexes(collection.indexes, name);
18293
18787
  const data = normalizeData(collection.data, name);
18294
- if (indexes.length === 0 && !data) invalid("each collection requires indexes or data.", name);
18788
+ if (indexes.length === 0 && !data) invalid2("each collection requires indexes or data.", name);
18295
18789
  return { name, targetName, indexes, ...data ? { data } : {}, verify: normalizeVerify(collection.verify, name) };
18296
18790
  });
18297
18791
  const production = PRODUCTION_ENVIRONMENTS.has(job.targetEnvironment.toLowerCase());