monsqlize 3.1.14 → 3.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -56,6 +56,14 @@ var ErrorCodes = {
56
56
  INVALID_EXPRESSION: "INVALID_EXPRESSION",
57
57
  INVALID_PAGINATION: "INVALID_PAGINATION",
58
58
  INVALID_OPERATION: "INVALID_OPERATION",
59
+ /** Invalid local Vector Search options before the driver is called. */
60
+ INVALID_VECTOR_SEARCH: "INVALID_VECTOR_SEARCH",
61
+ /** Relation select is outside the supported inclusion-only top-level contract. */
62
+ INVALID_RELATION_SELECT: "INVALID_RELATION_SELECT",
63
+ /** A protected delete found declared Model relations that still reference its target. */
64
+ RELATION_IN_USE: "RELATION_IN_USE",
65
+ /** Relation usage coverage is incomplete, so a protected delete must not continue. */
66
+ RELATION_USAGE_UNAVAILABLE: "RELATION_USAGE_UNAVAILABLE",
59
67
  CACHE_UNAVAILABLE: "CACHE_UNAVAILABLE",
60
68
  MANAGEMENT_OPERATION_FAILED: "MANAGEMENT_OPERATION_FAILED",
61
69
  NOT_CONNECTED: "NOT_CONNECTED",
@@ -501,6 +509,40 @@ function validateRelationConfig(name, config) {
501
509
  if (config.single !== void 0 && typeof config.single !== "boolean") {
502
510
  throw createError(ErrorCodes.INVALID_ARGUMENT, `relations.single must be a boolean`);
503
511
  }
512
+ normalizeRelationSelect(config.select, { relation: name });
513
+ }
514
+ function normalizeRelationSelect(value, context = {}) {
515
+ if (value === void 0) {
516
+ return void 0;
517
+ }
518
+ const invalid2 = (reason) => {
519
+ throw createError(
520
+ ErrorCodes.INVALID_RELATION_SELECT,
521
+ `Relation select must use non-empty top-level inclusion fields: ${reason}`,
522
+ [{ ...context, value, reason }]
523
+ );
524
+ };
525
+ const rawTokens = typeof value === "string" ? [value] : Array.isArray(value) ? value : invalid2("select must be a string or string array");
526
+ const tokens = [];
527
+ for (const rawToken of rawTokens) {
528
+ if (typeof rawToken !== "string") {
529
+ invalid2("select array entries must be strings");
530
+ }
531
+ const parts = rawToken.trim().split(/\s+/).filter(Boolean);
532
+ if (parts.length === 0) {
533
+ invalid2("select must not be empty");
534
+ }
535
+ tokens.push(...parts);
536
+ }
537
+ for (const token of tokens) {
538
+ if (token.startsWith("-") || token.startsWith("+")) {
539
+ invalid2("exclusion and prefix tokens are not supported");
540
+ }
541
+ if (token.includes(".")) {
542
+ invalid2("dot-path fields are not supported");
543
+ }
544
+ }
545
+ return [...new Set(tokens)];
504
546
  }
505
547
  function normalizePopulateConfig(path5) {
506
548
  return typeof path5 === "string" ? { path: path5 } : path5;
@@ -1077,6 +1119,56 @@ function resolveRegisteredCollectionName(registered, fallback) {
1077
1119
  const definition = registered.definition;
1078
1120
  return definition.collection ?? definition.name ?? registered.collectionName;
1079
1121
  }
1122
+ function flattenComparableValues(value) {
1123
+ if (Array.isArray(value)) {
1124
+ return value.flatMap((entry) => flattenComparableValues(entry));
1125
+ }
1126
+ return value === void 0 || value === null ? [] : [value];
1127
+ }
1128
+ function resolveNestedPopulatePaths(value) {
1129
+ if (value === void 0) {
1130
+ return [];
1131
+ }
1132
+ const paths = Array.isArray(value) ? value : [value];
1133
+ for (const path5 of paths) {
1134
+ const valid = typeof path5 === "string" || typeof path5 === "object" && path5 !== null && !Array.isArray(path5) && Boolean(path5.path);
1135
+ if (!valid) {
1136
+ throw createError(ErrorCodes.INVALID_ARGUMENT, "nested populate must be a string, array, or object");
1137
+ }
1138
+ }
1139
+ return paths;
1140
+ }
1141
+ function collectNestedLocalFields(relatedModel, nestedPaths) {
1142
+ if (!relatedModel) {
1143
+ return [];
1144
+ }
1145
+ const relations = relatedModel.getRelations();
1146
+ return unique(
1147
+ nestedPaths.map((path5) => relations[normalizePopulateConfig(path5).path]?.localField).filter((field) => Boolean(field))
1148
+ );
1149
+ }
1150
+ function dedupeDocuments(documents) {
1151
+ const seen = /* @__PURE__ */ new Set();
1152
+ return documents.filter((document) => {
1153
+ if (seen.has(document)) {
1154
+ return false;
1155
+ }
1156
+ seen.add(document);
1157
+ return true;
1158
+ });
1159
+ }
1160
+ function rebuildPopulateMatches(localValue, grouped, sort) {
1161
+ const matches = flattenComparableValues(localValue).flatMap((value) => grouped.get(toKey(value)) ?? []);
1162
+ const deduped = dedupeDocuments(matches);
1163
+ return sort ? applySort(deduped, sort) : deduped;
1164
+ }
1165
+ function projectPopulatedDocument(document, select, nestedPaths) {
1166
+ if (!select) {
1167
+ return document;
1168
+ }
1169
+ const nestedRelationFields = nestedPaths.map((path5) => normalizePopulateConfig(path5).path);
1170
+ return pickFields(document, [.../* @__PURE__ */ new Set([...select, ...nestedRelationFields])]);
1171
+ }
1080
1172
  async function populateModelPath(context, docs, path5, traversalState) {
1081
1173
  const config = normalizePopulateConfig(path5);
1082
1174
  const state = resolvePopulateState(config, traversalState);
@@ -1095,9 +1187,8 @@ async function populateModelPath(context, docs, path5, traversalState) {
1095
1187
  if (!relation) {
1096
1188
  throw createError(ErrorCodes.INVALID_ARGUMENT, `Undefined relation: ${config.path}`);
1097
1189
  }
1098
- const keys = unique(
1099
- docs.map((doc) => getByPath(doc, relation.localField)).filter((value) => value !== void 0 && value !== null)
1100
- );
1190
+ const localValues = docs.map((doc) => getByPath(doc, relation.localField));
1191
+ const keys = unique(localValues.flatMap((value) => flattenComparableValues(value)));
1101
1192
  if (keys.length === 0) {
1102
1193
  for (const doc of docs) {
1103
1194
  doc[config.path] = relation.single ? null : [];
@@ -1112,51 +1203,42 @@ async function populateModelPath(context, docs, path5, traversalState) {
1112
1203
  const relatedCollectionName = resolveRegisteredCollectionName(registered, relation.from);
1113
1204
  const relatedCollection = context.runtime.scopedCollection(relatedCollectionName, scope);
1114
1205
  const relatedModel = Model.has(relation.from) ? context.runtime.scopedModel(relation.from, scope) : null;
1206
+ const nestedPaths = resolveNestedPopulatePaths(config.populate);
1207
+ const effectiveSelect = normalizeRelationSelect(config.select, { path: config.path }) ?? normalizeRelationSelect(relation.select, { relation: config.path });
1208
+ const projectionFields = effectiveSelect ? [.../* @__PURE__ */ new Set([
1209
+ ...effectiveSelect,
1210
+ relation.foreignField,
1211
+ "_id",
1212
+ ...collectNestedLocalFields(relatedModel, nestedPaths)
1213
+ ])] : [];
1115
1214
  const relatedDocs = await relatedCollection.find({
1116
1215
  [relation.foreignField]: { $in: keys },
1117
1216
  ...config.match ?? {}
1118
- });
1217
+ }, effectiveSelect ? {
1218
+ projection: Object.fromEntries(projectionFields.map((field) => [field, 1]))
1219
+ } : void 0);
1119
1220
  let hydrated = relatedModel ? relatedModel.hydrateDocuments(relatedDocs) : relatedDocs.map((item) => ({ ...item }));
1120
- if (config.sort) {
1121
- hydrated = applySort(hydrated, config.sort);
1122
- }
1123
- if (config.select) {
1124
- const select = config.select;
1125
- hydrated = hydrated.map((item) => pickFields(item, select, [relation.foreignField]));
1126
- }
1127
- if (config.populate) {
1128
- const nestedRaw = config.populate;
1129
- const isValidNestedConfig = (value) => {
1130
- if (typeof value === "string" || Array.isArray(value)) return true;
1131
- if (typeof value === "object" && value !== null && value.path) return true;
1132
- return false;
1133
- };
1134
- if (!isValidNestedConfig(nestedRaw)) {
1135
- throw createError(ErrorCodes.INVALID_ARGUMENT, "nested populate must be a string, array, or object");
1136
- }
1137
- if (relatedModel) {
1138
- if (state.depth + 1 > state.maxDepth) {
1139
- throw createError(ErrorCodes.INVALID_ARGUMENT, `populate maxDepth exceeded: ${state.maxDepth}`);
1140
- }
1141
- const nestedPaths = Array.isArray(config.populate) ? config.populate : [config.populate];
1142
- hydrated = await relatedModel.populateDocuments(hydrated, nestedPaths, {
1143
- depth: state.depth + 1,
1144
- maxDepth: state.maxDepth,
1145
- activeConfigs: state.activeConfigs
1146
- });
1221
+ if (nestedPaths.length > 0 && relatedModel) {
1222
+ if (state.depth + 1 > state.maxDepth) {
1223
+ throw createError(ErrorCodes.INVALID_ARGUMENT, `populate maxDepth exceeded: ${state.maxDepth}`);
1147
1224
  }
1225
+ hydrated = await relatedModel.populateDocuments(hydrated, nestedPaths, {
1226
+ depth: state.depth + 1,
1227
+ maxDepth: state.maxDepth,
1228
+ activeConfigs: state.activeConfigs
1229
+ });
1148
1230
  }
1149
1231
  const grouped = groupBy(hydrated, (item) => getByPath(item, relation.foreignField));
1150
- for (const doc of docs) {
1151
- const localValue = getByPath(doc, relation.localField);
1152
- let matches = grouped.get(toKey(localValue)) ?? [];
1232
+ for (const [index, doc] of docs.entries()) {
1233
+ let matches = rebuildPopulateMatches(localValues[index], grouped, config.sort);
1153
1234
  if (config.skip) {
1154
1235
  matches = matches.slice(config.skip);
1155
1236
  }
1156
1237
  if (config.limit !== void 0) {
1157
1238
  matches = matches.slice(0, config.limit);
1158
1239
  }
1159
- doc[config.path] = relation.single ? matches[0] ?? null : [...matches];
1240
+ const projectedMatches = matches.map((item) => projectPopulatedDocument(item, effectiveSelect, nestedPaths));
1241
+ doc[config.path] = relation.single ? projectedMatches[0] ?? null : projectedMatches;
1160
1242
  }
1161
1243
  return docs;
1162
1244
  } finally {
@@ -5574,7 +5656,382 @@ async function orchestrateModelDeleteMany(context, filter, options) {
5574
5656
  return result;
5575
5657
  }
5576
5658
 
5659
+ // src/capabilities/model/relation-usage.ts
5660
+ var DEFAULT_MAX_TARGETS = 100;
5661
+ var DEFAULT_MAX_SAMPLES = 20;
5662
+ function isNonArrayObject(value) {
5663
+ return typeof value === "object" && value !== null && !Array.isArray(value);
5664
+ }
5665
+ function normalizePositiveInteger(value, field, fallback) {
5666
+ if (value === void 0) {
5667
+ return fallback;
5668
+ }
5669
+ if (!Number.isInteger(value) || value <= 0) {
5670
+ throw createError(ErrorCodes.INVALID_ARGUMENT, `${field} must be a positive integer`, [{ field, value }]);
5671
+ }
5672
+ return value;
5673
+ }
5674
+ function normalizeRelationNames(value, field) {
5675
+ if (value === void 0) {
5676
+ return void 0;
5677
+ }
5678
+ if (!Array.isArray(value) || value.some((entry) => typeof entry !== "string" || entry.trim().length === 0)) {
5679
+ throw createError(ErrorCodes.INVALID_ARGUMENT, `${field} must be an array of non-empty strings`, [{ field, value }]);
5680
+ }
5681
+ return [...new Set(value.map((entry) => entry.trim()))];
5682
+ }
5683
+ function normalizeRelationUsageOptions(options) {
5684
+ if (options !== void 0 && !isNonArrayObject(options)) {
5685
+ throw createError(ErrorCodes.INVALID_ARGUMENT, "relation usage options must be a non-array object", [{ field: "options", value: options }]);
5686
+ }
5687
+ const value = options ?? {};
5688
+ if (value.includeSoftDeletedReferences !== void 0 && typeof value.includeSoftDeletedReferences !== "boolean") {
5689
+ throw createError(
5690
+ ErrorCodes.INVALID_ARGUMENT,
5691
+ "includeSoftDeletedReferences must be a boolean",
5692
+ [{ field: "includeSoftDeletedReferences", value: value.includeSoftDeletedReferences }]
5693
+ );
5694
+ }
5695
+ return {
5696
+ session: value.session,
5697
+ maxTargets: normalizePositiveInteger(value.maxTargets, "maxTargets", DEFAULT_MAX_TARGETS),
5698
+ maxSamples: normalizePositiveInteger(value.maxSamples, "maxSamples", DEFAULT_MAX_SAMPLES),
5699
+ includeRelations: normalizeRelationNames(value.includeRelations, "includeRelations"),
5700
+ excludeRelations: normalizeRelationNames(value.excludeRelations, "excludeRelations"),
5701
+ includeSoftDeletedReferences: value.includeSoftDeletedReferences !== false
5702
+ };
5703
+ }
5704
+ function resolveRegisteredCollectionName2(registered) {
5705
+ const definition = registered.definition;
5706
+ return definition.collection ?? definition.name ?? registered.collectionName;
5707
+ }
5708
+ function flattenComparableValues2(value) {
5709
+ if (Array.isArray(value)) {
5710
+ return value.flatMap((entry) => flattenComparableValues2(entry));
5711
+ }
5712
+ return value === void 0 || value === null ? [] : [value];
5713
+ }
5714
+ function isRelationSelected(options, sourceModel, relation) {
5715
+ const qualified = `${sourceModel}.${relation}`;
5716
+ const matches = (names) => Boolean(names?.includes(qualified) || names?.includes(relation));
5717
+ if (options.includeRelations && !matches(options.includeRelations)) {
5718
+ return false;
5719
+ }
5720
+ return !matches(options.excludeRelations);
5721
+ }
5722
+ function describeError(error) {
5723
+ return error instanceof Error ? error.message : String(error);
5724
+ }
5725
+ function findDeclaredInboundRelations(targetModelName) {
5726
+ const relations = [];
5727
+ for (const sourceModel of Model.list()) {
5728
+ const registered = Model.get(sourceModel);
5729
+ if (!registered) {
5730
+ continue;
5731
+ }
5732
+ for (const [relation, config] of Object.entries(registered.definition.relations ?? {})) {
5733
+ if (config.from !== targetModelName) {
5734
+ continue;
5735
+ }
5736
+ relations.push({
5737
+ sourceModel,
5738
+ sourceCollection: resolveRegisteredCollectionName2(registered),
5739
+ relation,
5740
+ definition: registered.definition,
5741
+ localField: config.localField,
5742
+ foreignField: config.foreignField
5743
+ });
5744
+ }
5745
+ }
5746
+ return relations;
5747
+ }
5748
+ function buildRelationUsageTargetProjection(targetModelName) {
5749
+ const fields = /* @__PURE__ */ new Set(["_id"]);
5750
+ for (const relation of findDeclaredInboundRelations(targetModelName)) {
5751
+ fields.add(relation.foreignField);
5752
+ }
5753
+ return Object.fromEntries([...fields].map((field) => [field, 1]));
5754
+ }
5755
+ function createUnavailableRelationUsageReport(context, reason) {
5756
+ return {
5757
+ used: false,
5758
+ target: {
5759
+ matchedCount: 0,
5760
+ truncated: false,
5761
+ sampleIds: []
5762
+ },
5763
+ checkedRelations: 0,
5764
+ usages: [],
5765
+ coverage: {
5766
+ mode: "registered-declared",
5767
+ complete: false,
5768
+ registryModels: Model.list(),
5769
+ checkedModels: [],
5770
+ skipped: [{ sourceModel: context.modelName, reason }]
5771
+ }
5772
+ };
5773
+ }
5774
+ async function inspectDeclaredRelationUsage(context, targets, inputOptions, targetTruncated = false) {
5775
+ const options = normalizeRelationUsageOptions(inputOptions);
5776
+ const registryModels = Model.list();
5777
+ const checkedModels = /* @__PURE__ */ new Set();
5778
+ const skipped = [];
5779
+ const usages = [];
5780
+ let checkedRelations = 0;
5781
+ for (const inbound of findDeclaredInboundRelations(context.modelName)) {
5782
+ if (!isRelationSelected(options, inbound.sourceModel, inbound.relation)) {
5783
+ skipped.push({
5784
+ sourceModel: inbound.sourceModel,
5785
+ reason: `relation '${inbound.sourceModel}.${inbound.relation}' was excluded by relation selection`
5786
+ });
5787
+ continue;
5788
+ }
5789
+ checkedRelations += 1;
5790
+ const targetValues = unique(
5791
+ targets.flatMap((target) => flattenComparableValues2(getByPath(target, inbound.foreignField)))
5792
+ );
5793
+ if (targetValues.length === 0) {
5794
+ checkedModels.add(inbound.sourceModel);
5795
+ continue;
5796
+ }
5797
+ const scope = {
5798
+ database: inbound.definition.connection?.database ?? context.dbName,
5799
+ pool: inbound.definition.connection?.pool ?? context.poolName
5800
+ };
5801
+ const baseQuery = {
5802
+ [inbound.localField]: { $in: targetValues }
5803
+ };
5804
+ const sourceSoftDelete = resolveModelSoftDeleteConfig(inbound.definition);
5805
+ const query = !options.includeSoftDeletedReferences && sourceSoftDelete?.enabled ? { $and: [baseQuery, { [sourceSoftDelete.field]: null }] } : baseQuery;
5806
+ try {
5807
+ const collection = context.runtime.scopedCollection(inbound.sourceCollection, scope);
5808
+ if (!collection) {
5809
+ throw new Error("scopedCollection returned no collection");
5810
+ }
5811
+ checkedModels.add(inbound.sourceModel);
5812
+ const count = await collection.count(query, { session: options.session });
5813
+ if (typeof count !== "number" || !Number.isFinite(count) || count < 0) {
5814
+ throw new Error("relation usage count did not return a non-negative number");
5815
+ }
5816
+ if (count === 0) {
5817
+ continue;
5818
+ }
5819
+ const samples = await collection.find(query, {
5820
+ session: options.session,
5821
+ projection: { _id: 1 },
5822
+ limit: options.maxSamples
5823
+ });
5824
+ usages.push({
5825
+ sourceModel: inbound.sourceModel,
5826
+ sourceCollection: inbound.sourceCollection,
5827
+ relation: inbound.relation,
5828
+ localField: inbound.localField,
5829
+ foreignField: inbound.foreignField,
5830
+ count,
5831
+ sampleIds: samples.filter((sample) => isNonArrayObject(sample)).map((sample) => sample._id).filter((id) => id !== void 0).slice(0, options.maxSamples)
5832
+ });
5833
+ } catch (error) {
5834
+ skipped.push({
5835
+ sourceModel: inbound.sourceModel,
5836
+ reason: describeError(error)
5837
+ });
5838
+ }
5839
+ }
5840
+ const targetSampleIds = targets.map((target) => target._id).filter((id) => id !== void 0).slice(0, options.maxSamples);
5841
+ return {
5842
+ used: usages.length > 0,
5843
+ target: {
5844
+ matchedCount: targets.length,
5845
+ truncated: targetTruncated,
5846
+ sampleIds: targetSampleIds
5847
+ },
5848
+ checkedRelations,
5849
+ usages,
5850
+ coverage: {
5851
+ mode: "registered-declared",
5852
+ complete: !targetTruncated && skipped.length === 0,
5853
+ registryModels,
5854
+ checkedModels: [...checkedModels],
5855
+ skipped
5856
+ }
5857
+ };
5858
+ }
5859
+ function stripRelationUsageOptions(options) {
5860
+ if (!isNonArrayObject(options)) {
5861
+ return options;
5862
+ }
5863
+ const {
5864
+ maxTargets: _maxTargets,
5865
+ maxSamples: _maxSamples,
5866
+ includeRelations: _includeRelations,
5867
+ excludeRelations: _excludeRelations,
5868
+ includeSoftDeletedReferences: _includeSoftDeletedReferences,
5869
+ ...deleteOptions
5870
+ } = options;
5871
+ void _maxTargets;
5872
+ void _maxSamples;
5873
+ void _includeRelations;
5874
+ void _excludeRelations;
5875
+ void _includeSoftDeletedReferences;
5876
+ return deleteOptions;
5877
+ }
5878
+
5879
+ // src/capabilities/model/model-p0-operations.ts
5880
+ function relationUsageContext(host) {
5881
+ return {
5882
+ modelName: host.modelName,
5883
+ runtime: host.runtime,
5884
+ dbName: host.dbName,
5885
+ poolName: host.poolName
5886
+ };
5887
+ }
5888
+ function isNonEmptyFilter(filter) {
5889
+ return typeof filter === "object" && filter !== null && !Array.isArray(filter) && Object.keys(filter).length > 0;
5890
+ }
5891
+ function relationTargetReadOptions(options, projection) {
5892
+ const source = options ?? {};
5893
+ const result = { projection };
5894
+ for (const key of ["session", "collation", "hint", "comment", "let", "maxTimeMS", "readConcern", "readPreference"]) {
5895
+ if (source[key] !== void 0) {
5896
+ result[key] = source[key];
5897
+ }
5898
+ }
5899
+ return result;
5900
+ }
5901
+ function hydrateVectorSearchHits(host, hits) {
5902
+ return hits.map((hit) => {
5903
+ const document = host.hydrateDocument(hit.document);
5904
+ if (!document) {
5905
+ throw createError(ErrorCodes.INVALID_OPERATION, "Vector Search returned a non-document result");
5906
+ }
5907
+ return { document, score: hit.score };
5908
+ });
5909
+ }
5910
+ async function executeModelVectorSearch(host, options) {
5911
+ if (typeof options !== "object" || options === null || Array.isArray(options)) {
5912
+ const hits2 = await host.collection.vectorSearch(options);
5913
+ return hydrateVectorSearchHits(host, hits2);
5914
+ }
5915
+ const { withDeleted, onlyDeleted, ...collectionOptions } = options;
5916
+ if (withDeleted !== void 0 && typeof withDeleted !== "boolean") {
5917
+ throw createError(
5918
+ ErrorCodes.INVALID_VECTOR_SEARCH,
5919
+ "withDeleted must be a boolean",
5920
+ [{ field: "withDeleted", reason: "boolean required" }]
5921
+ );
5922
+ }
5923
+ if (onlyDeleted !== void 0 && typeof onlyDeleted !== "boolean") {
5924
+ throw createError(
5925
+ ErrorCodes.INVALID_VECTOR_SEARCH,
5926
+ "onlyDeleted must be a boolean",
5927
+ [{ field: "onlyDeleted", reason: "boolean required" }]
5928
+ );
5929
+ }
5930
+ const filter = host.softDeleteConfig?.enabled && !withDeleted ? applyModelSoftDeleteFilter(
5931
+ collectionOptions.filter,
5932
+ { withDeleted, onlyDeleted },
5933
+ host.softDeleteConfig
5934
+ ) : collectionOptions.filter;
5935
+ const hits = await host.collection.vectorSearch({
5936
+ ...collectionOptions,
5937
+ ...filter === void 0 ? {} : { filter }
5938
+ });
5939
+ return hydrateVectorSearchHits(host, hits);
5940
+ }
5941
+ async function executeRelationUsageCheck(host, filter, options) {
5942
+ const normalizedOptions = normalizeRelationUsageOptions(options);
5943
+ const context = relationUsageContext(host);
5944
+ try {
5945
+ const rawTargets = await host.collection.find(
5946
+ applyModelSoftDeleteFilter(filter, void 0, host.softDeleteConfig),
5947
+ {
5948
+ session: normalizedOptions.session,
5949
+ projection: buildRelationUsageTargetProjection(host.modelName),
5950
+ limit: normalizedOptions.maxTargets + 1
5951
+ }
5952
+ );
5953
+ const targets = rawTargets.filter(
5954
+ (target) => typeof target === "object" && target !== null && !Array.isArray(target)
5955
+ );
5956
+ return inspectDeclaredRelationUsage(
5957
+ context,
5958
+ targets.slice(0, normalizedOptions.maxTargets),
5959
+ normalizedOptions,
5960
+ targets.length > normalizedOptions.maxTargets
5961
+ );
5962
+ } catch (error) {
5963
+ return createUnavailableRelationUsageReport(context, error instanceof Error ? error.message : String(error));
5964
+ }
5965
+ }
5966
+ async function executeProtectedRelationDelete(host, filter, options, force) {
5967
+ if (!isNonEmptyFilter(filter)) {
5968
+ throw createError(
5969
+ ErrorCodes.INVALID_ARGUMENT,
5970
+ "protected relation deletion requires a non-empty filter",
5971
+ [{ field: "filter", reason: "non-empty object required" }]
5972
+ );
5973
+ }
5974
+ const rawOptions = options ?? {};
5975
+ if (rawOptions.includeRelations !== void 0 || rawOptions.excludeRelations !== void 0) {
5976
+ throw createError(
5977
+ ErrorCodes.INVALID_ARGUMENT,
5978
+ "protected relation deletion cannot narrow the declared relation scan",
5979
+ [{ field: "includeRelations/excludeRelations", reason: "protected deletion always scans all declared relations" }]
5980
+ );
5981
+ }
5982
+ const usageOptions = normalizeRelationUsageOptions(options);
5983
+ const context = relationUsageContext(host);
5984
+ const deleteOptions = stripRelationUsageOptions(options);
5985
+ let target;
5986
+ try {
5987
+ const targetFilter = force || !host.softDeleteConfig?.enabled ? filter : { ...filter, [host.softDeleteConfig.field]: null };
5988
+ const rawTarget = await host.collection.findOne(
5989
+ targetFilter,
5990
+ relationTargetReadOptions(options, buildRelationUsageTargetProjection(host.modelName))
5991
+ );
5992
+ target = typeof rawTarget === "object" && rawTarget !== null && !Array.isArray(rawTarget) ? rawTarget : null;
5993
+ } catch (error) {
5994
+ const usage2 = createUnavailableRelationUsageReport(
5995
+ context,
5996
+ error instanceof Error ? error.message : String(error)
5997
+ );
5998
+ throw createError(
5999
+ ErrorCodes.RELATION_USAGE_UNAVAILABLE,
6000
+ "Unable to resolve a target for protected relation deletion",
6001
+ [{ coverage: usage2.coverage, reason: usage2.coverage.skipped[0]?.reason, skipped: usage2.coverage.skipped }]
6002
+ );
6003
+ }
6004
+ const usage = await inspectDeclaredRelationUsage(context, target ? [target] : [], usageOptions);
6005
+ if (!usage.coverage.complete) {
6006
+ throw createError(
6007
+ ErrorCodes.RELATION_USAGE_UNAVAILABLE,
6008
+ "Declared relation usage coverage is incomplete; protected deletion was not performed",
6009
+ [{ coverage: usage.coverage, reason: "incomplete relation usage coverage", skipped: usage.coverage.skipped }]
6010
+ );
6011
+ }
6012
+ if (usage.used) {
6013
+ throw createError(
6014
+ ErrorCodes.RELATION_IN_USE,
6015
+ "Protected deletion was blocked because declared relations still reference the target",
6016
+ [{ used: usage.used, usages: usage.usages, coverage: usage.coverage }]
6017
+ );
6018
+ }
6019
+ if (target && target._id === void 0) {
6020
+ throw createError(
6021
+ ErrorCodes.RELATION_USAGE_UNAVAILABLE,
6022
+ "Protected deletion target did not include an _id",
6023
+ [{ coverage: usage.coverage, reason: "target _id missing", skipped: usage.coverage.skipped }]
6024
+ );
6025
+ }
6026
+ const deleteFilter = target ? { _id: target._id } : { _id: { $in: [] } };
6027
+ const result = force ? await host.forceDelete(deleteFilter, deleteOptions) : await host.deleteOne(deleteFilter, deleteOptions);
6028
+ return { result, usage };
6029
+ }
6030
+
5577
6031
  // src/capabilities/model/index.ts
6032
+ function resolveModelRegistryName(definition, fallback) {
6033
+ return Model.list().find((name) => Model.get(name)?.definition === definition) ?? fallback;
6034
+ }
5578
6035
  var ModelInstance = class {
5579
6036
  constructor(collection, runtime, options) {
5580
6037
  this.collection = collection;
@@ -5595,6 +6052,7 @@ var ModelInstance = class {
5595
6052
  this.dbName = options.dbName;
5596
6053
  this.poolName = options.poolName;
5597
6054
  this.definition = options.definition;
6055
+ this.modelName = options.modelName ?? resolveModelRegistryName(options.definition, options.collectionName);
5598
6056
  this.relations = new Map(Object.entries(options.definition.relations ?? {}));
5599
6057
  for (const [name, config] of this.relations) {
5600
6058
  validateRelationConfig(name, config);
@@ -5778,6 +6236,9 @@ var ModelInstance = class {
5778
6236
  };
5779
6237
  });
5780
6238
  }
6239
+ async vectorSearch(options) {
6240
+ return executeModelVectorSearch(this.p0OperationsContext(), options);
6241
+ }
5781
6242
  count(query, options) {
5782
6243
  const filteredQuery = applyModelSoftDeleteFilter(query, options, this._softDeleteConfig);
5783
6244
  return this.runV1HookedOperation("find", [filteredQuery, options], (nextQuery, nextOptions) => this.collection.count(nextQuery, nextOptions));
@@ -5827,6 +6288,19 @@ var ModelInstance = class {
5827
6288
  async deleteMany(filter, options) {
5828
6289
  return this.runModelWrite(() => orchestrateModelDeleteMany(this.mutationContext(), filter, options));
5829
6290
  }
6291
+ /**
6292
+ * Inspects only registered and declared inbound relations. It is read-only;
6293
+ * callers must check coverage before treating an empty result as safe.
6294
+ */
6295
+ async checkRelationUsage(filter, options) {
6296
+ return executeRelationUsageCheck(this.p0OperationsContext(), filter, options);
6297
+ }
6298
+ async deleteOneWithRelations(filter, options) {
6299
+ return executeProtectedRelationDelete(this.p0OperationsContext(), filter, options, false);
6300
+ }
6301
+ async forceDeleteWithRelations(filter, options) {
6302
+ return executeProtectedRelationDelete(this.p0OperationsContext(), filter, options, true);
6303
+ }
5830
6304
  // ── soft-delete extended methods (only meaningful when softDelete is enabled) ──
5831
6305
  findWithDeleted(query, options) {
5832
6306
  return findWithDeletedDocuments(this.softDeleteContext(), query, options);
@@ -5995,6 +6469,19 @@ var ModelInstance = class {
5995
6469
  populateDocument: (document, paths) => this.populateSingle(document, paths)
5996
6470
  }, doc);
5997
6471
  }
6472
+ p0OperationsContext() {
6473
+ return {
6474
+ collection: this.collection,
6475
+ softDeleteConfig: this._softDeleteConfig,
6476
+ modelName: this.modelName,
6477
+ runtime: this.runtime,
6478
+ dbName: this.dbName,
6479
+ poolName: this.poolName,
6480
+ hydrateDocument: (doc) => this.hydrateDocument(doc),
6481
+ deleteOne: (filter, options) => this.deleteOne(filter, options),
6482
+ forceDelete: (filter, options) => this.forceDelete(filter, options)
6483
+ };
6484
+ }
5998
6485
  softDeleteContext() {
5999
6486
  return {
6000
6487
  collection: this.collection,
@@ -9554,7 +10041,7 @@ async function computeTotals(coll, query, limit, totals, defaults = {}, queryCac
9554
10041
  }
9555
10042
 
9556
10043
  // src/adapters/mongodb/queries/find-page.ts
9557
- function normalizePositiveInteger(value, fallback, field) {
10044
+ function normalizePositiveInteger2(value, fallback, field) {
9558
10045
  if (value === void 0 || value === null) {
9559
10046
  return fallback;
9560
10047
  }
@@ -9602,8 +10089,8 @@ async function executeFindPage(collection, options = {}, defaults = {}, queryCac
9602
10089
  const metaStartTs = Date.now();
9603
10090
  const metaSteps = [];
9604
10091
  const ext = options;
9605
- const page = normalizePositiveInteger(options.page, 1, "page");
9606
- const rawLimit = normalizePositiveInteger(options.limit, DEFAULT_FIND_PAGE_LIMIT, "limit");
10092
+ const page = normalizePositiveInteger2(options.page, 1, "page");
10093
+ const rawLimit = normalizePositiveInteger2(options.limit, DEFAULT_FIND_PAGE_LIMIT, "limit");
9607
10094
  const limit = defaults.findPageMaxLimit !== void 0 && rawLimit > defaults.findPageMaxLimit ? defaults.findPageMaxLimit : rawLimit;
9608
10095
  const sort = normalizeSortShape(options.sort);
9609
10096
  const baseQuery = options.query ?? {};
@@ -10477,6 +10964,138 @@ function explainDocuments(collection, query, options, defaults) {
10477
10964
  return createFindChain(collection, query, options, defaults).explain(options?.explain ?? "queryPlanner");
10478
10965
  }
10479
10966
 
10967
+ // src/adapters/mongodb/queries/vector-search.ts
10968
+ var VECTOR_SEARCH_SCORE_FIELD = "__monsqlize_vector_search_score__";
10969
+ function attachVectorSearchOperationContext(error, namespace) {
10970
+ if (error instanceof Error) {
10971
+ const contextualized = error;
10972
+ const existing = contextualized.monsqlize;
10973
+ try {
10974
+ contextualized.monsqlize = {
10975
+ ...existing && typeof existing === "object" ? existing : {},
10976
+ operation: "vectorSearch",
10977
+ namespace
10978
+ };
10979
+ } catch {
10980
+ }
10981
+ return error;
10982
+ }
10983
+ return createError(
10984
+ ErrorCodes.MONGODB_ERROR,
10985
+ "vectorSearch execution failed",
10986
+ [{ operation: "vectorSearch", namespace, reason: String(error) }]
10987
+ );
10988
+ }
10989
+ function assertNonEmptyString(value, field) {
10990
+ if (typeof value !== "string" || value.trim().length === 0) {
10991
+ throw createError(ErrorCodes.INVALID_VECTOR_SEARCH, `${field} must be a non-empty string`, [{ field, reason: "non-empty string required" }]);
10992
+ }
10993
+ }
10994
+ function assertPositiveInteger(value, field) {
10995
+ if (!Number.isInteger(value) || value <= 0) {
10996
+ throw createError(ErrorCodes.INVALID_VECTOR_SEARCH, `${field} must be a positive integer`, [{ field, reason: "positive integer required" }]);
10997
+ }
10998
+ }
10999
+ function isNonArrayObject2(value) {
11000
+ return typeof value === "object" && value !== null && !Array.isArray(value);
11001
+ }
11002
+ function normalizeVectorProjection(value) {
11003
+ if (value === void 0) return void 0;
11004
+ if (Array.isArray(value)) {
11005
+ if (value.some((field) => typeof field !== "string" || field.length === 0)) {
11006
+ throw createError(ErrorCodes.INVALID_VECTOR_SEARCH, "projection array entries must be non-empty strings", [{ field: "projection", reason: "array entries must be non-empty strings" }]);
11007
+ }
11008
+ return normalizeProjection(value);
11009
+ }
11010
+ if (!isNonArrayObject2(value)) {
11011
+ throw createError(ErrorCodes.INVALID_VECTOR_SEARCH, "projection must be an object or string array", [{ field: "projection", reason: "object or string array required" }]);
11012
+ }
11013
+ return normalizeProjection(value);
11014
+ }
11015
+ function normalizeAggregateOptions(value) {
11016
+ if (value === void 0) return void 0;
11017
+ if (!isNonArrayObject2(value)) {
11018
+ throw createError(ErrorCodes.INVALID_VECTOR_SEARCH, "aggregateOptions must be a non-array object", [{ field: "aggregateOptions", reason: "non-array object required" }]);
11019
+ }
11020
+ for (const key of ["meta", "stream", "explain"]) {
11021
+ if (Object.prototype.hasOwnProperty.call(value, key)) {
11022
+ throw createError(ErrorCodes.INVALID_VECTOR_SEARCH, `aggregateOptions.${key} is not supported by vectorSearch()`, [{ field: `aggregateOptions.${key}`, reason: "reserved aggregate option" }]);
11023
+ }
11024
+ }
11025
+ return { ...value };
11026
+ }
11027
+ function buildVectorSearchPipeline(options) {
11028
+ if (!isNonArrayObject2(options)) {
11029
+ throw createError(ErrorCodes.INVALID_VECTOR_SEARCH, "vectorSearch options must be a non-array object", [{ field: "options", reason: "non-array object required" }]);
11030
+ }
11031
+ assertNonEmptyString(options.index, "index");
11032
+ assertNonEmptyString(options.path, "path");
11033
+ assertPositiveInteger(options.limit, "limit");
11034
+ if (!Array.isArray(options.queryVector) || options.queryVector.length === 0) {
11035
+ throw createError(ErrorCodes.INVALID_VECTOR_SEARCH, "queryVector must be a non-empty number array", [{ field: "queryVector", reason: "non-empty number array required" }]);
11036
+ }
11037
+ if (options.queryVector.some((value) => typeof value !== "number" || !Number.isFinite(value))) {
11038
+ throw createError(ErrorCodes.INVALID_VECTOR_SEARCH, "queryVector values must be finite numbers", [{ field: "queryVector", reason: "finite numbers required" }]);
11039
+ }
11040
+ if (options.exact !== void 0 && typeof options.exact !== "boolean") {
11041
+ throw createError(ErrorCodes.INVALID_VECTOR_SEARCH, "exact must be a boolean", [{ field: "exact", reason: "boolean required" }]);
11042
+ }
11043
+ if (options.filter !== void 0 && !isNonArrayObject2(options.filter)) {
11044
+ throw createError(ErrorCodes.INVALID_VECTOR_SEARCH, "filter must be a non-array object", [{ field: "filter", reason: "non-array object required" }]);
11045
+ }
11046
+ const isExactSearch = options.exact === true;
11047
+ if (isExactSearch) {
11048
+ if (options.numCandidates !== void 0) {
11049
+ throw createError(ErrorCodes.INVALID_VECTOR_SEARCH, "numCandidates is not supported when exact is true", [{ field: "numCandidates", reason: "ANN-only option" }]);
11050
+ }
11051
+ } else {
11052
+ assertPositiveInteger(options.numCandidates, "numCandidates");
11053
+ if (options.numCandidates < options.limit) {
11054
+ throw createError(ErrorCodes.INVALID_VECTOR_SEARCH, "numCandidates must be greater than or equal to limit", [{ field: "numCandidates", reason: "must be greater than or equal to limit" }]);
11055
+ }
11056
+ }
11057
+ const vectorSearchStage = {
11058
+ index: options.index.trim(),
11059
+ path: options.path.trim(),
11060
+ queryVector: [...options.queryVector],
11061
+ limit: options.limit,
11062
+ ...isExactSearch ? { exact: true } : { numCandidates: options.numCandidates },
11063
+ ...options.filter === void 0 ? {} : { filter: options.filter }
11064
+ };
11065
+ const projection = normalizeVectorProjection(options.projection);
11066
+ const scoreStage = projection ? {
11067
+ $project: {
11068
+ ...projection,
11069
+ [VECTOR_SEARCH_SCORE_FIELD]: { $meta: "vectorSearchScore" }
11070
+ }
11071
+ } : {
11072
+ $set: {
11073
+ [VECTOR_SEARCH_SCORE_FIELD]: { $meta: "vectorSearchScore" }
11074
+ }
11075
+ };
11076
+ return {
11077
+ pipeline: [
11078
+ { $vectorSearch: vectorSearchStage },
11079
+ scoreStage
11080
+ ],
11081
+ aggregateOptions: normalizeAggregateOptions(options.aggregateOptions)
11082
+ };
11083
+ }
11084
+ function mapVectorSearchRows(rows) {
11085
+ return rows.map((row) => {
11086
+ const score = row[VECTOR_SEARCH_SCORE_FIELD];
11087
+ if (typeof score !== "number" || !Number.isFinite(score)) {
11088
+ throw createError(ErrorCodes.INVALID_OPERATION, "Vector Search result did not contain a finite score");
11089
+ }
11090
+ const document = { ...row };
11091
+ delete document[VECTOR_SEARCH_SCORE_FIELD];
11092
+ return {
11093
+ document,
11094
+ score
11095
+ };
11096
+ });
11097
+ }
11098
+
10480
11099
  // src/adapters/mongodb/writes/write-utils.ts
10481
11100
  function sleep2(ms) {
10482
11101
  return new Promise((resolve) => setTimeout(resolve, ms));
@@ -11650,6 +12269,19 @@ var MongoCollectionAccessor = class {
11650
12269
  } : void 0
11651
12270
  );
11652
12271
  }
12272
+ /** Searches a MongoDB Vector Search index through a constrained aggregate pipeline. */
12273
+ async vectorSearch(options) {
12274
+ const { pipeline, aggregateOptions } = buildVectorSearchPipeline(options);
12275
+ try {
12276
+ const rows = await this.aggregate(
12277
+ pipeline,
12278
+ aggregateOptions
12279
+ );
12280
+ return mapVectorSearchRows(rows);
12281
+ } catch (error) {
12282
+ throw attachVectorSearchOperationContext(error, this.getNamespace());
12283
+ }
12284
+ }
11653
12285
  /** Returns an array of distinct values for the given field key. */
11654
12286
  async distinct(key, query, options) {
11655
12287
  const startTs = Date.now();
@@ -12184,6 +12816,7 @@ function createCompatModelInstance(config) {
12184
12816
  config.runtime,
12185
12817
  {
12186
12818
  collectionName: config.collectionName,
12819
+ modelName: config.modelName,
12187
12820
  dbName: config.dbName,
12188
12821
  poolName: config.poolName,
12189
12822
  definition: config.definition,
@@ -12221,6 +12854,7 @@ function createRuntimeModelInstance(host, name, scope) {
12221
12854
  host.runtime,
12222
12855
  {
12223
12856
  collectionName: actualCollectionName,
12857
+ modelName: registered.collectionName,
12224
12858
  dbName: databaseName,
12225
12859
  poolName,
12226
12860
  definition: registered.definition,
@@ -16357,6 +16991,7 @@ var MonSQLizeRuntime = class {
16357
16991
  collection,
16358
16992
  runtime: this,
16359
16993
  collectionName: actualCollectionName,
16994
+ modelName: registered.collectionName,
16360
16995
  dbName: database ?? getRuntimeDatabaseName(this),
16361
16996
  poolName: pool,
16362
16997
  definition: registered.definition,
@@ -16389,6 +17024,7 @@ var MonSQLizeRuntime = class {
16389
17024
  collection,
16390
17025
  runtime: this,
16391
17026
  collectionName: actualCollectionName,
17027
+ modelName: registered.collectionName,
16392
17028
  dbName: getRuntimeDatabaseName(this),
16393
17029
  definition: registered.definition,
16394
17030
  schemaEngine: this._schemaDslEngine