monsqlize 3.1.0 → 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.
@@ -39,6 +39,14 @@ var ErrorCodes = {
39
39
  INVALID_EXPRESSION: "INVALID_EXPRESSION",
40
40
  INVALID_PAGINATION: "INVALID_PAGINATION",
41
41
  INVALID_OPERATION: "INVALID_OPERATION",
42
+ /** Invalid local Vector Search options before the driver is called. */
43
+ INVALID_VECTOR_SEARCH: "INVALID_VECTOR_SEARCH",
44
+ /** Relation select is outside the supported inclusion-only top-level contract. */
45
+ INVALID_RELATION_SELECT: "INVALID_RELATION_SELECT",
46
+ /** A protected delete found declared Model relations that still reference its target. */
47
+ RELATION_IN_USE: "RELATION_IN_USE",
48
+ /** Relation usage coverage is incomplete, so a protected delete must not continue. */
49
+ RELATION_USAGE_UNAVAILABLE: "RELATION_USAGE_UNAVAILABLE",
42
50
  CACHE_UNAVAILABLE: "CACHE_UNAVAILABLE",
43
51
  MANAGEMENT_OPERATION_FAILED: "MANAGEMENT_OPERATION_FAILED",
44
52
  NOT_CONNECTED: "NOT_CONNECTED",
@@ -484,6 +492,40 @@ function validateRelationConfig(name, config) {
484
492
  if (config.single !== void 0 && typeof config.single !== "boolean") {
485
493
  throw createError(ErrorCodes.INVALID_ARGUMENT, `relations.single must be a boolean`);
486
494
  }
495
+ normalizeRelationSelect(config.select, { relation: name });
496
+ }
497
+ function normalizeRelationSelect(value, context = {}) {
498
+ if (value === void 0) {
499
+ return void 0;
500
+ }
501
+ const invalid2 = (reason) => {
502
+ throw createError(
503
+ ErrorCodes.INVALID_RELATION_SELECT,
504
+ `Relation select must use non-empty top-level inclusion fields: ${reason}`,
505
+ [{ ...context, value, reason }]
506
+ );
507
+ };
508
+ const rawTokens = typeof value === "string" ? [value] : Array.isArray(value) ? value : invalid2("select must be a string or string array");
509
+ const tokens = [];
510
+ for (const rawToken of rawTokens) {
511
+ if (typeof rawToken !== "string") {
512
+ invalid2("select array entries must be strings");
513
+ }
514
+ const parts = rawToken.trim().split(/\s+/).filter(Boolean);
515
+ if (parts.length === 0) {
516
+ invalid2("select must not be empty");
517
+ }
518
+ tokens.push(...parts);
519
+ }
520
+ for (const token of tokens) {
521
+ if (token.startsWith("-") || token.startsWith("+")) {
522
+ invalid2("exclusion and prefix tokens are not supported");
523
+ }
524
+ if (token.includes(".")) {
525
+ invalid2("dot-path fields are not supported");
526
+ }
527
+ }
528
+ return [...new Set(tokens)];
487
529
  }
488
530
  function normalizePopulateConfig(path5) {
489
531
  return typeof path5 === "string" ? { path: path5 } : path5;
@@ -1060,6 +1102,56 @@ function resolveRegisteredCollectionName(registered, fallback) {
1060
1102
  const definition = registered.definition;
1061
1103
  return definition.collection ?? definition.name ?? registered.collectionName;
1062
1104
  }
1105
+ function flattenComparableValues(value) {
1106
+ if (Array.isArray(value)) {
1107
+ return value.flatMap((entry) => flattenComparableValues(entry));
1108
+ }
1109
+ return value === void 0 || value === null ? [] : [value];
1110
+ }
1111
+ function resolveNestedPopulatePaths(value) {
1112
+ if (value === void 0) {
1113
+ return [];
1114
+ }
1115
+ const paths = Array.isArray(value) ? value : [value];
1116
+ for (const path5 of paths) {
1117
+ const valid = typeof path5 === "string" || typeof path5 === "object" && path5 !== null && !Array.isArray(path5) && Boolean(path5.path);
1118
+ if (!valid) {
1119
+ throw createError(ErrorCodes.INVALID_ARGUMENT, "nested populate must be a string, array, or object");
1120
+ }
1121
+ }
1122
+ return paths;
1123
+ }
1124
+ function collectNestedLocalFields(relatedModel, nestedPaths) {
1125
+ if (!relatedModel) {
1126
+ return [];
1127
+ }
1128
+ const relations = relatedModel.getRelations();
1129
+ return unique(
1130
+ nestedPaths.map((path5) => relations[normalizePopulateConfig(path5).path]?.localField).filter((field) => Boolean(field))
1131
+ );
1132
+ }
1133
+ function dedupeDocuments(documents) {
1134
+ const seen = /* @__PURE__ */ new Set();
1135
+ return documents.filter((document) => {
1136
+ if (seen.has(document)) {
1137
+ return false;
1138
+ }
1139
+ seen.add(document);
1140
+ return true;
1141
+ });
1142
+ }
1143
+ function rebuildPopulateMatches(localValue, grouped, sort) {
1144
+ const matches = flattenComparableValues(localValue).flatMap((value) => grouped.get(toKey(value)) ?? []);
1145
+ const deduped = dedupeDocuments(matches);
1146
+ return sort ? applySort(deduped, sort) : deduped;
1147
+ }
1148
+ function projectPopulatedDocument(document, select, nestedPaths) {
1149
+ if (!select) {
1150
+ return document;
1151
+ }
1152
+ const nestedRelationFields = nestedPaths.map((path5) => normalizePopulateConfig(path5).path);
1153
+ return pickFields(document, [.../* @__PURE__ */ new Set([...select, ...nestedRelationFields])]);
1154
+ }
1063
1155
  async function populateModelPath(context, docs, path5, traversalState) {
1064
1156
  const config = normalizePopulateConfig(path5);
1065
1157
  const state = resolvePopulateState(config, traversalState);
@@ -1078,9 +1170,8 @@ async function populateModelPath(context, docs, path5, traversalState) {
1078
1170
  if (!relation) {
1079
1171
  throw createError(ErrorCodes.INVALID_ARGUMENT, `Undefined relation: ${config.path}`);
1080
1172
  }
1081
- const keys = unique(
1082
- docs.map((doc) => getByPath(doc, relation.localField)).filter((value) => value !== void 0 && value !== null)
1083
- );
1173
+ const localValues = docs.map((doc) => getByPath(doc, relation.localField));
1174
+ const keys = unique(localValues.flatMap((value) => flattenComparableValues(value)));
1084
1175
  if (keys.length === 0) {
1085
1176
  for (const doc of docs) {
1086
1177
  doc[config.path] = relation.single ? null : [];
@@ -1095,51 +1186,42 @@ async function populateModelPath(context, docs, path5, traversalState) {
1095
1186
  const relatedCollectionName = resolveRegisteredCollectionName(registered, relation.from);
1096
1187
  const relatedCollection = context.runtime.scopedCollection(relatedCollectionName, scope);
1097
1188
  const relatedModel = Model.has(relation.from) ? context.runtime.scopedModel(relation.from, scope) : null;
1189
+ const nestedPaths = resolveNestedPopulatePaths(config.populate);
1190
+ const effectiveSelect = normalizeRelationSelect(config.select, { path: config.path }) ?? normalizeRelationSelect(relation.select, { relation: config.path });
1191
+ const projectionFields = effectiveSelect ? [.../* @__PURE__ */ new Set([
1192
+ ...effectiveSelect,
1193
+ relation.foreignField,
1194
+ "_id",
1195
+ ...collectNestedLocalFields(relatedModel, nestedPaths)
1196
+ ])] : [];
1098
1197
  const relatedDocs = await relatedCollection.find({
1099
1198
  [relation.foreignField]: { $in: keys },
1100
1199
  ...config.match ?? {}
1101
- });
1200
+ }, effectiveSelect ? {
1201
+ projection: Object.fromEntries(projectionFields.map((field) => [field, 1]))
1202
+ } : void 0);
1102
1203
  let hydrated = relatedModel ? relatedModel.hydrateDocuments(relatedDocs) : relatedDocs.map((item) => ({ ...item }));
1103
- if (config.sort) {
1104
- hydrated = applySort(hydrated, config.sort);
1105
- }
1106
- if (config.select) {
1107
- const select = config.select;
1108
- hydrated = hydrated.map((item) => pickFields(item, select, [relation.foreignField]));
1109
- }
1110
- if (config.populate) {
1111
- const nestedRaw = config.populate;
1112
- const isValidNestedConfig = (value) => {
1113
- if (typeof value === "string" || Array.isArray(value)) return true;
1114
- if (typeof value === "object" && value !== null && value.path) return true;
1115
- return false;
1116
- };
1117
- if (!isValidNestedConfig(nestedRaw)) {
1118
- throw createError(ErrorCodes.INVALID_ARGUMENT, "nested populate must be a string, array, or object");
1119
- }
1120
- if (relatedModel) {
1121
- if (state.depth + 1 > state.maxDepth) {
1122
- throw createError(ErrorCodes.INVALID_ARGUMENT, `populate maxDepth exceeded: ${state.maxDepth}`);
1123
- }
1124
- const nestedPaths = Array.isArray(config.populate) ? config.populate : [config.populate];
1125
- hydrated = await relatedModel.populateDocuments(hydrated, nestedPaths, {
1126
- depth: state.depth + 1,
1127
- maxDepth: state.maxDepth,
1128
- activeConfigs: state.activeConfigs
1129
- });
1204
+ if (nestedPaths.length > 0 && relatedModel) {
1205
+ if (state.depth + 1 > state.maxDepth) {
1206
+ throw createError(ErrorCodes.INVALID_ARGUMENT, `populate maxDepth exceeded: ${state.maxDepth}`);
1130
1207
  }
1208
+ hydrated = await relatedModel.populateDocuments(hydrated, nestedPaths, {
1209
+ depth: state.depth + 1,
1210
+ maxDepth: state.maxDepth,
1211
+ activeConfigs: state.activeConfigs
1212
+ });
1131
1213
  }
1132
1214
  const grouped = groupBy(hydrated, (item) => getByPath(item, relation.foreignField));
1133
- for (const doc of docs) {
1134
- const localValue = getByPath(doc, relation.localField);
1135
- let matches = grouped.get(toKey(localValue)) ?? [];
1215
+ for (const [index, doc] of docs.entries()) {
1216
+ let matches = rebuildPopulateMatches(localValues[index], grouped, config.sort);
1136
1217
  if (config.skip) {
1137
1218
  matches = matches.slice(config.skip);
1138
1219
  }
1139
1220
  if (config.limit !== void 0) {
1140
1221
  matches = matches.slice(0, config.limit);
1141
1222
  }
1142
- doc[config.path] = relation.single ? matches[0] ?? null : [...matches];
1223
+ const projectedMatches = matches.map((item) => projectPopulatedDocument(item, effectiveSelect, nestedPaths));
1224
+ doc[config.path] = relation.single ? projectedMatches[0] ?? null : projectedMatches;
1143
1225
  }
1144
1226
  return docs;
1145
1227
  } finally {
@@ -5557,7 +5639,382 @@ async function orchestrateModelDeleteMany(context, filter, options) {
5557
5639
  return result;
5558
5640
  }
5559
5641
 
5642
+ // src/capabilities/model/relation-usage.ts
5643
+ var DEFAULT_MAX_TARGETS = 100;
5644
+ var DEFAULT_MAX_SAMPLES = 20;
5645
+ function isNonArrayObject(value) {
5646
+ return typeof value === "object" && value !== null && !Array.isArray(value);
5647
+ }
5648
+ function normalizePositiveInteger(value, field, fallback) {
5649
+ if (value === void 0) {
5650
+ return fallback;
5651
+ }
5652
+ if (!Number.isInteger(value) || value <= 0) {
5653
+ throw createError(ErrorCodes.INVALID_ARGUMENT, `${field} must be a positive integer`, [{ field, value }]);
5654
+ }
5655
+ return value;
5656
+ }
5657
+ function normalizeRelationNames(value, field) {
5658
+ if (value === void 0) {
5659
+ return void 0;
5660
+ }
5661
+ if (!Array.isArray(value) || value.some((entry) => typeof entry !== "string" || entry.trim().length === 0)) {
5662
+ throw createError(ErrorCodes.INVALID_ARGUMENT, `${field} must be an array of non-empty strings`, [{ field, value }]);
5663
+ }
5664
+ return [...new Set(value.map((entry) => entry.trim()))];
5665
+ }
5666
+ function normalizeRelationUsageOptions(options) {
5667
+ if (options !== void 0 && !isNonArrayObject(options)) {
5668
+ throw createError(ErrorCodes.INVALID_ARGUMENT, "relation usage options must be a non-array object", [{ field: "options", value: options }]);
5669
+ }
5670
+ const value = options ?? {};
5671
+ if (value.includeSoftDeletedReferences !== void 0 && typeof value.includeSoftDeletedReferences !== "boolean") {
5672
+ throw createError(
5673
+ ErrorCodes.INVALID_ARGUMENT,
5674
+ "includeSoftDeletedReferences must be a boolean",
5675
+ [{ field: "includeSoftDeletedReferences", value: value.includeSoftDeletedReferences }]
5676
+ );
5677
+ }
5678
+ return {
5679
+ session: value.session,
5680
+ maxTargets: normalizePositiveInteger(value.maxTargets, "maxTargets", DEFAULT_MAX_TARGETS),
5681
+ maxSamples: normalizePositiveInteger(value.maxSamples, "maxSamples", DEFAULT_MAX_SAMPLES),
5682
+ includeRelations: normalizeRelationNames(value.includeRelations, "includeRelations"),
5683
+ excludeRelations: normalizeRelationNames(value.excludeRelations, "excludeRelations"),
5684
+ includeSoftDeletedReferences: value.includeSoftDeletedReferences !== false
5685
+ };
5686
+ }
5687
+ function resolveRegisteredCollectionName2(registered) {
5688
+ const definition = registered.definition;
5689
+ return definition.collection ?? definition.name ?? registered.collectionName;
5690
+ }
5691
+ function flattenComparableValues2(value) {
5692
+ if (Array.isArray(value)) {
5693
+ return value.flatMap((entry) => flattenComparableValues2(entry));
5694
+ }
5695
+ return value === void 0 || value === null ? [] : [value];
5696
+ }
5697
+ function isRelationSelected(options, sourceModel, relation) {
5698
+ const qualified = `${sourceModel}.${relation}`;
5699
+ const matches = (names) => Boolean(names?.includes(qualified) || names?.includes(relation));
5700
+ if (options.includeRelations && !matches(options.includeRelations)) {
5701
+ return false;
5702
+ }
5703
+ return !matches(options.excludeRelations);
5704
+ }
5705
+ function describeError(error) {
5706
+ return error instanceof Error ? error.message : String(error);
5707
+ }
5708
+ function findDeclaredInboundRelations(targetModelName) {
5709
+ const relations = [];
5710
+ for (const sourceModel of Model.list()) {
5711
+ const registered = Model.get(sourceModel);
5712
+ if (!registered) {
5713
+ continue;
5714
+ }
5715
+ for (const [relation, config] of Object.entries(registered.definition.relations ?? {})) {
5716
+ if (config.from !== targetModelName) {
5717
+ continue;
5718
+ }
5719
+ relations.push({
5720
+ sourceModel,
5721
+ sourceCollection: resolveRegisteredCollectionName2(registered),
5722
+ relation,
5723
+ definition: registered.definition,
5724
+ localField: config.localField,
5725
+ foreignField: config.foreignField
5726
+ });
5727
+ }
5728
+ }
5729
+ return relations;
5730
+ }
5731
+ function buildRelationUsageTargetProjection(targetModelName) {
5732
+ const fields = /* @__PURE__ */ new Set(["_id"]);
5733
+ for (const relation of findDeclaredInboundRelations(targetModelName)) {
5734
+ fields.add(relation.foreignField);
5735
+ }
5736
+ return Object.fromEntries([...fields].map((field) => [field, 1]));
5737
+ }
5738
+ function createUnavailableRelationUsageReport(context, reason) {
5739
+ return {
5740
+ used: false,
5741
+ target: {
5742
+ matchedCount: 0,
5743
+ truncated: false,
5744
+ sampleIds: []
5745
+ },
5746
+ checkedRelations: 0,
5747
+ usages: [],
5748
+ coverage: {
5749
+ mode: "registered-declared",
5750
+ complete: false,
5751
+ registryModels: Model.list(),
5752
+ checkedModels: [],
5753
+ skipped: [{ sourceModel: context.modelName, reason }]
5754
+ }
5755
+ };
5756
+ }
5757
+ async function inspectDeclaredRelationUsage(context, targets, inputOptions, targetTruncated = false) {
5758
+ const options = normalizeRelationUsageOptions(inputOptions);
5759
+ const registryModels = Model.list();
5760
+ const checkedModels = /* @__PURE__ */ new Set();
5761
+ const skipped = [];
5762
+ const usages = [];
5763
+ let checkedRelations = 0;
5764
+ for (const inbound of findDeclaredInboundRelations(context.modelName)) {
5765
+ if (!isRelationSelected(options, inbound.sourceModel, inbound.relation)) {
5766
+ skipped.push({
5767
+ sourceModel: inbound.sourceModel,
5768
+ reason: `relation '${inbound.sourceModel}.${inbound.relation}' was excluded by relation selection`
5769
+ });
5770
+ continue;
5771
+ }
5772
+ checkedRelations += 1;
5773
+ const targetValues = unique(
5774
+ targets.flatMap((target) => flattenComparableValues2(getByPath(target, inbound.foreignField)))
5775
+ );
5776
+ if (targetValues.length === 0) {
5777
+ checkedModels.add(inbound.sourceModel);
5778
+ continue;
5779
+ }
5780
+ const scope = {
5781
+ database: inbound.definition.connection?.database ?? context.dbName,
5782
+ pool: inbound.definition.connection?.pool ?? context.poolName
5783
+ };
5784
+ const baseQuery = {
5785
+ [inbound.localField]: { $in: targetValues }
5786
+ };
5787
+ const sourceSoftDelete = resolveModelSoftDeleteConfig(inbound.definition);
5788
+ const query = !options.includeSoftDeletedReferences && sourceSoftDelete?.enabled ? { $and: [baseQuery, { [sourceSoftDelete.field]: null }] } : baseQuery;
5789
+ try {
5790
+ const collection = context.runtime.scopedCollection(inbound.sourceCollection, scope);
5791
+ if (!collection) {
5792
+ throw new Error("scopedCollection returned no collection");
5793
+ }
5794
+ checkedModels.add(inbound.sourceModel);
5795
+ const count = await collection.count(query, { session: options.session });
5796
+ if (typeof count !== "number" || !Number.isFinite(count) || count < 0) {
5797
+ throw new Error("relation usage count did not return a non-negative number");
5798
+ }
5799
+ if (count === 0) {
5800
+ continue;
5801
+ }
5802
+ const samples = await collection.find(query, {
5803
+ session: options.session,
5804
+ projection: { _id: 1 },
5805
+ limit: options.maxSamples
5806
+ });
5807
+ usages.push({
5808
+ sourceModel: inbound.sourceModel,
5809
+ sourceCollection: inbound.sourceCollection,
5810
+ relation: inbound.relation,
5811
+ localField: inbound.localField,
5812
+ foreignField: inbound.foreignField,
5813
+ count,
5814
+ sampleIds: samples.filter((sample) => isNonArrayObject(sample)).map((sample) => sample._id).filter((id) => id !== void 0).slice(0, options.maxSamples)
5815
+ });
5816
+ } catch (error) {
5817
+ skipped.push({
5818
+ sourceModel: inbound.sourceModel,
5819
+ reason: describeError(error)
5820
+ });
5821
+ }
5822
+ }
5823
+ const targetSampleIds = targets.map((target) => target._id).filter((id) => id !== void 0).slice(0, options.maxSamples);
5824
+ return {
5825
+ used: usages.length > 0,
5826
+ target: {
5827
+ matchedCount: targets.length,
5828
+ truncated: targetTruncated,
5829
+ sampleIds: targetSampleIds
5830
+ },
5831
+ checkedRelations,
5832
+ usages,
5833
+ coverage: {
5834
+ mode: "registered-declared",
5835
+ complete: !targetTruncated && skipped.length === 0,
5836
+ registryModels,
5837
+ checkedModels: [...checkedModels],
5838
+ skipped
5839
+ }
5840
+ };
5841
+ }
5842
+ function stripRelationUsageOptions(options) {
5843
+ if (!isNonArrayObject(options)) {
5844
+ return options;
5845
+ }
5846
+ const {
5847
+ maxTargets: _maxTargets,
5848
+ maxSamples: _maxSamples,
5849
+ includeRelations: _includeRelations,
5850
+ excludeRelations: _excludeRelations,
5851
+ includeSoftDeletedReferences: _includeSoftDeletedReferences,
5852
+ ...deleteOptions
5853
+ } = options;
5854
+ void _maxTargets;
5855
+ void _maxSamples;
5856
+ void _includeRelations;
5857
+ void _excludeRelations;
5858
+ void _includeSoftDeletedReferences;
5859
+ return deleteOptions;
5860
+ }
5861
+
5862
+ // src/capabilities/model/model-p0-operations.ts
5863
+ function relationUsageContext(host) {
5864
+ return {
5865
+ modelName: host.modelName,
5866
+ runtime: host.runtime,
5867
+ dbName: host.dbName,
5868
+ poolName: host.poolName
5869
+ };
5870
+ }
5871
+ function isNonEmptyFilter(filter) {
5872
+ return typeof filter === "object" && filter !== null && !Array.isArray(filter) && Object.keys(filter).length > 0;
5873
+ }
5874
+ function relationTargetReadOptions(options, projection) {
5875
+ const source = options ?? {};
5876
+ const result = { projection };
5877
+ for (const key of ["session", "collation", "hint", "comment", "let", "maxTimeMS", "readConcern", "readPreference"]) {
5878
+ if (source[key] !== void 0) {
5879
+ result[key] = source[key];
5880
+ }
5881
+ }
5882
+ return result;
5883
+ }
5884
+ function hydrateVectorSearchHits(host, hits) {
5885
+ return hits.map((hit) => {
5886
+ const document = host.hydrateDocument(hit.document);
5887
+ if (!document) {
5888
+ throw createError(ErrorCodes.INVALID_OPERATION, "Vector Search returned a non-document result");
5889
+ }
5890
+ return { document, score: hit.score };
5891
+ });
5892
+ }
5893
+ async function executeModelVectorSearch(host, options) {
5894
+ if (typeof options !== "object" || options === null || Array.isArray(options)) {
5895
+ const hits2 = await host.collection.vectorSearch(options);
5896
+ return hydrateVectorSearchHits(host, hits2);
5897
+ }
5898
+ const { withDeleted, onlyDeleted, ...collectionOptions } = options;
5899
+ if (withDeleted !== void 0 && typeof withDeleted !== "boolean") {
5900
+ throw createError(
5901
+ ErrorCodes.INVALID_VECTOR_SEARCH,
5902
+ "withDeleted must be a boolean",
5903
+ [{ field: "withDeleted", reason: "boolean required" }]
5904
+ );
5905
+ }
5906
+ if (onlyDeleted !== void 0 && typeof onlyDeleted !== "boolean") {
5907
+ throw createError(
5908
+ ErrorCodes.INVALID_VECTOR_SEARCH,
5909
+ "onlyDeleted must be a boolean",
5910
+ [{ field: "onlyDeleted", reason: "boolean required" }]
5911
+ );
5912
+ }
5913
+ const filter = host.softDeleteConfig?.enabled && !withDeleted ? applyModelSoftDeleteFilter(
5914
+ collectionOptions.filter,
5915
+ { withDeleted, onlyDeleted },
5916
+ host.softDeleteConfig
5917
+ ) : collectionOptions.filter;
5918
+ const hits = await host.collection.vectorSearch({
5919
+ ...collectionOptions,
5920
+ ...filter === void 0 ? {} : { filter }
5921
+ });
5922
+ return hydrateVectorSearchHits(host, hits);
5923
+ }
5924
+ async function executeRelationUsageCheck(host, filter, options) {
5925
+ const normalizedOptions = normalizeRelationUsageOptions(options);
5926
+ const context = relationUsageContext(host);
5927
+ try {
5928
+ const rawTargets = await host.collection.find(
5929
+ applyModelSoftDeleteFilter(filter, void 0, host.softDeleteConfig),
5930
+ {
5931
+ session: normalizedOptions.session,
5932
+ projection: buildRelationUsageTargetProjection(host.modelName),
5933
+ limit: normalizedOptions.maxTargets + 1
5934
+ }
5935
+ );
5936
+ const targets = rawTargets.filter(
5937
+ (target) => typeof target === "object" && target !== null && !Array.isArray(target)
5938
+ );
5939
+ return inspectDeclaredRelationUsage(
5940
+ context,
5941
+ targets.slice(0, normalizedOptions.maxTargets),
5942
+ normalizedOptions,
5943
+ targets.length > normalizedOptions.maxTargets
5944
+ );
5945
+ } catch (error) {
5946
+ return createUnavailableRelationUsageReport(context, error instanceof Error ? error.message : String(error));
5947
+ }
5948
+ }
5949
+ async function executeProtectedRelationDelete(host, filter, options, force) {
5950
+ if (!isNonEmptyFilter(filter)) {
5951
+ throw createError(
5952
+ ErrorCodes.INVALID_ARGUMENT,
5953
+ "protected relation deletion requires a non-empty filter",
5954
+ [{ field: "filter", reason: "non-empty object required" }]
5955
+ );
5956
+ }
5957
+ const rawOptions = options ?? {};
5958
+ if (rawOptions.includeRelations !== void 0 || rawOptions.excludeRelations !== void 0) {
5959
+ throw createError(
5960
+ ErrorCodes.INVALID_ARGUMENT,
5961
+ "protected relation deletion cannot narrow the declared relation scan",
5962
+ [{ field: "includeRelations/excludeRelations", reason: "protected deletion always scans all declared relations" }]
5963
+ );
5964
+ }
5965
+ const usageOptions = normalizeRelationUsageOptions(options);
5966
+ const context = relationUsageContext(host);
5967
+ const deleteOptions = stripRelationUsageOptions(options);
5968
+ let target;
5969
+ try {
5970
+ const targetFilter = force || !host.softDeleteConfig?.enabled ? filter : { ...filter, [host.softDeleteConfig.field]: null };
5971
+ const rawTarget = await host.collection.findOne(
5972
+ targetFilter,
5973
+ relationTargetReadOptions(options, buildRelationUsageTargetProjection(host.modelName))
5974
+ );
5975
+ target = typeof rawTarget === "object" && rawTarget !== null && !Array.isArray(rawTarget) ? rawTarget : null;
5976
+ } catch (error) {
5977
+ const usage2 = createUnavailableRelationUsageReport(
5978
+ context,
5979
+ error instanceof Error ? error.message : String(error)
5980
+ );
5981
+ throw createError(
5982
+ ErrorCodes.RELATION_USAGE_UNAVAILABLE,
5983
+ "Unable to resolve a target for protected relation deletion",
5984
+ [{ coverage: usage2.coverage, reason: usage2.coverage.skipped[0]?.reason, skipped: usage2.coverage.skipped }]
5985
+ );
5986
+ }
5987
+ const usage = await inspectDeclaredRelationUsage(context, target ? [target] : [], usageOptions);
5988
+ if (!usage.coverage.complete) {
5989
+ throw createError(
5990
+ ErrorCodes.RELATION_USAGE_UNAVAILABLE,
5991
+ "Declared relation usage coverage is incomplete; protected deletion was not performed",
5992
+ [{ coverage: usage.coverage, reason: "incomplete relation usage coverage", skipped: usage.coverage.skipped }]
5993
+ );
5994
+ }
5995
+ if (usage.used) {
5996
+ throw createError(
5997
+ ErrorCodes.RELATION_IN_USE,
5998
+ "Protected deletion was blocked because declared relations still reference the target",
5999
+ [{ used: usage.used, usages: usage.usages, coverage: usage.coverage }]
6000
+ );
6001
+ }
6002
+ if (target && target._id === void 0) {
6003
+ throw createError(
6004
+ ErrorCodes.RELATION_USAGE_UNAVAILABLE,
6005
+ "Protected deletion target did not include an _id",
6006
+ [{ coverage: usage.coverage, reason: "target _id missing", skipped: usage.coverage.skipped }]
6007
+ );
6008
+ }
6009
+ const deleteFilter = target ? { _id: target._id } : { _id: { $in: [] } };
6010
+ const result = force ? await host.forceDelete(deleteFilter, deleteOptions) : await host.deleteOne(deleteFilter, deleteOptions);
6011
+ return { result, usage };
6012
+ }
6013
+
5560
6014
  // src/capabilities/model/index.ts
6015
+ function resolveModelRegistryName(definition, fallback) {
6016
+ return Model.list().find((name) => Model.get(name)?.definition === definition) ?? fallback;
6017
+ }
5561
6018
  var ModelInstance = class {
5562
6019
  constructor(collection, runtime, options) {
5563
6020
  this.collection = collection;
@@ -5578,6 +6035,7 @@ var ModelInstance = class {
5578
6035
  this.dbName = options.dbName;
5579
6036
  this.poolName = options.poolName;
5580
6037
  this.definition = options.definition;
6038
+ this.modelName = options.modelName ?? resolveModelRegistryName(options.definition, options.collectionName);
5581
6039
  this.relations = new Map(Object.entries(options.definition.relations ?? {}));
5582
6040
  for (const [name, config] of this.relations) {
5583
6041
  validateRelationConfig(name, config);
@@ -5761,6 +6219,9 @@ var ModelInstance = class {
5761
6219
  };
5762
6220
  });
5763
6221
  }
6222
+ async vectorSearch(options) {
6223
+ return executeModelVectorSearch(this.p0OperationsContext(), options);
6224
+ }
5764
6225
  count(query, options) {
5765
6226
  const filteredQuery = applyModelSoftDeleteFilter(query, options, this._softDeleteConfig);
5766
6227
  return this.runV1HookedOperation("find", [filteredQuery, options], (nextQuery, nextOptions) => this.collection.count(nextQuery, nextOptions));
@@ -5810,6 +6271,19 @@ var ModelInstance = class {
5810
6271
  async deleteMany(filter, options) {
5811
6272
  return this.runModelWrite(() => orchestrateModelDeleteMany(this.mutationContext(), filter, options));
5812
6273
  }
6274
+ /**
6275
+ * Inspects only registered and declared inbound relations. It is read-only;
6276
+ * callers must check coverage before treating an empty result as safe.
6277
+ */
6278
+ async checkRelationUsage(filter, options) {
6279
+ return executeRelationUsageCheck(this.p0OperationsContext(), filter, options);
6280
+ }
6281
+ async deleteOneWithRelations(filter, options) {
6282
+ return executeProtectedRelationDelete(this.p0OperationsContext(), filter, options, false);
6283
+ }
6284
+ async forceDeleteWithRelations(filter, options) {
6285
+ return executeProtectedRelationDelete(this.p0OperationsContext(), filter, options, true);
6286
+ }
5813
6287
  // ── soft-delete extended methods (only meaningful when softDelete is enabled) ──
5814
6288
  findWithDeleted(query, options) {
5815
6289
  return findWithDeletedDocuments(this.softDeleteContext(), query, options);
@@ -5978,6 +6452,19 @@ var ModelInstance = class {
5978
6452
  populateDocument: (document, paths) => this.populateSingle(document, paths)
5979
6453
  }, doc);
5980
6454
  }
6455
+ p0OperationsContext() {
6456
+ return {
6457
+ collection: this.collection,
6458
+ softDeleteConfig: this._softDeleteConfig,
6459
+ modelName: this.modelName,
6460
+ runtime: this.runtime,
6461
+ dbName: this.dbName,
6462
+ poolName: this.poolName,
6463
+ hydrateDocument: (doc) => this.hydrateDocument(doc),
6464
+ deleteOne: (filter, options) => this.deleteOne(filter, options),
6465
+ forceDelete: (filter, options) => this.forceDelete(filter, options)
6466
+ };
6467
+ }
5981
6468
  softDeleteContext() {
5982
6469
  return {
5983
6470
  collection: this.collection,
@@ -9537,7 +10024,7 @@ async function computeTotals(coll, query, limit, totals, defaults = {}, queryCac
9537
10024
  }
9538
10025
 
9539
10026
  // src/adapters/mongodb/queries/find-page.ts
9540
- function normalizePositiveInteger(value, fallback, field) {
10027
+ function normalizePositiveInteger2(value, fallback, field) {
9541
10028
  if (value === void 0 || value === null) {
9542
10029
  return fallback;
9543
10030
  }
@@ -9585,8 +10072,8 @@ async function executeFindPage(collection, options = {}, defaults = {}, queryCac
9585
10072
  const metaStartTs = Date.now();
9586
10073
  const metaSteps = [];
9587
10074
  const ext = options;
9588
- const page = normalizePositiveInteger(options.page, 1, "page");
9589
- const rawLimit = normalizePositiveInteger(options.limit, DEFAULT_FIND_PAGE_LIMIT, "limit");
10075
+ const page = normalizePositiveInteger2(options.page, 1, "page");
10076
+ const rawLimit = normalizePositiveInteger2(options.limit, DEFAULT_FIND_PAGE_LIMIT, "limit");
9590
10077
  const limit = defaults.findPageMaxLimit !== void 0 && rawLimit > defaults.findPageMaxLimit ? defaults.findPageMaxLimit : rawLimit;
9591
10078
  const sort = normalizeSortShape(options.sort);
9592
10079
  const baseQuery = options.query ?? {};
@@ -10460,6 +10947,138 @@ function explainDocuments(collection, query, options, defaults) {
10460
10947
  return createFindChain(collection, query, options, defaults).explain(options?.explain ?? "queryPlanner");
10461
10948
  }
10462
10949
 
10950
+ // src/adapters/mongodb/queries/vector-search.ts
10951
+ var VECTOR_SEARCH_SCORE_FIELD = "__monsqlize_vector_search_score__";
10952
+ function attachVectorSearchOperationContext(error, namespace) {
10953
+ if (error instanceof Error) {
10954
+ const contextualized = error;
10955
+ const existing = contextualized.monsqlize;
10956
+ try {
10957
+ contextualized.monsqlize = {
10958
+ ...existing && typeof existing === "object" ? existing : {},
10959
+ operation: "vectorSearch",
10960
+ namespace
10961
+ };
10962
+ } catch {
10963
+ }
10964
+ return error;
10965
+ }
10966
+ return createError(
10967
+ ErrorCodes.MONGODB_ERROR,
10968
+ "vectorSearch execution failed",
10969
+ [{ operation: "vectorSearch", namespace, reason: String(error) }]
10970
+ );
10971
+ }
10972
+ function assertNonEmptyString(value, field) {
10973
+ if (typeof value !== "string" || value.trim().length === 0) {
10974
+ throw createError(ErrorCodes.INVALID_VECTOR_SEARCH, `${field} must be a non-empty string`, [{ field, reason: "non-empty string required" }]);
10975
+ }
10976
+ }
10977
+ function assertPositiveInteger(value, field) {
10978
+ if (!Number.isInteger(value) || value <= 0) {
10979
+ throw createError(ErrorCodes.INVALID_VECTOR_SEARCH, `${field} must be a positive integer`, [{ field, reason: "positive integer required" }]);
10980
+ }
10981
+ }
10982
+ function isNonArrayObject2(value) {
10983
+ return typeof value === "object" && value !== null && !Array.isArray(value);
10984
+ }
10985
+ function normalizeVectorProjection(value) {
10986
+ if (value === void 0) return void 0;
10987
+ if (Array.isArray(value)) {
10988
+ if (value.some((field) => typeof field !== "string" || field.length === 0)) {
10989
+ throw createError(ErrorCodes.INVALID_VECTOR_SEARCH, "projection array entries must be non-empty strings", [{ field: "projection", reason: "array entries must be non-empty strings" }]);
10990
+ }
10991
+ return normalizeProjection(value);
10992
+ }
10993
+ if (!isNonArrayObject2(value)) {
10994
+ throw createError(ErrorCodes.INVALID_VECTOR_SEARCH, "projection must be an object or string array", [{ field: "projection", reason: "object or string array required" }]);
10995
+ }
10996
+ return normalizeProjection(value);
10997
+ }
10998
+ function normalizeAggregateOptions(value) {
10999
+ if (value === void 0) return void 0;
11000
+ if (!isNonArrayObject2(value)) {
11001
+ throw createError(ErrorCodes.INVALID_VECTOR_SEARCH, "aggregateOptions must be a non-array object", [{ field: "aggregateOptions", reason: "non-array object required" }]);
11002
+ }
11003
+ for (const key of ["meta", "stream", "explain"]) {
11004
+ if (Object.prototype.hasOwnProperty.call(value, key)) {
11005
+ throw createError(ErrorCodes.INVALID_VECTOR_SEARCH, `aggregateOptions.${key} is not supported by vectorSearch()`, [{ field: `aggregateOptions.${key}`, reason: "reserved aggregate option" }]);
11006
+ }
11007
+ }
11008
+ return { ...value };
11009
+ }
11010
+ function buildVectorSearchPipeline(options) {
11011
+ if (!isNonArrayObject2(options)) {
11012
+ throw createError(ErrorCodes.INVALID_VECTOR_SEARCH, "vectorSearch options must be a non-array object", [{ field: "options", reason: "non-array object required" }]);
11013
+ }
11014
+ assertNonEmptyString(options.index, "index");
11015
+ assertNonEmptyString(options.path, "path");
11016
+ assertPositiveInteger(options.limit, "limit");
11017
+ if (!Array.isArray(options.queryVector) || options.queryVector.length === 0) {
11018
+ throw createError(ErrorCodes.INVALID_VECTOR_SEARCH, "queryVector must be a non-empty number array", [{ field: "queryVector", reason: "non-empty number array required" }]);
11019
+ }
11020
+ if (options.queryVector.some((value) => typeof value !== "number" || !Number.isFinite(value))) {
11021
+ throw createError(ErrorCodes.INVALID_VECTOR_SEARCH, "queryVector values must be finite numbers", [{ field: "queryVector", reason: "finite numbers required" }]);
11022
+ }
11023
+ if (options.exact !== void 0 && typeof options.exact !== "boolean") {
11024
+ throw createError(ErrorCodes.INVALID_VECTOR_SEARCH, "exact must be a boolean", [{ field: "exact", reason: "boolean required" }]);
11025
+ }
11026
+ if (options.filter !== void 0 && !isNonArrayObject2(options.filter)) {
11027
+ throw createError(ErrorCodes.INVALID_VECTOR_SEARCH, "filter must be a non-array object", [{ field: "filter", reason: "non-array object required" }]);
11028
+ }
11029
+ const isExactSearch = options.exact === true;
11030
+ if (isExactSearch) {
11031
+ if (options.numCandidates !== void 0) {
11032
+ throw createError(ErrorCodes.INVALID_VECTOR_SEARCH, "numCandidates is not supported when exact is true", [{ field: "numCandidates", reason: "ANN-only option" }]);
11033
+ }
11034
+ } else {
11035
+ assertPositiveInteger(options.numCandidates, "numCandidates");
11036
+ if (options.numCandidates < options.limit) {
11037
+ 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" }]);
11038
+ }
11039
+ }
11040
+ const vectorSearchStage = {
11041
+ index: options.index.trim(),
11042
+ path: options.path.trim(),
11043
+ queryVector: [...options.queryVector],
11044
+ limit: options.limit,
11045
+ ...isExactSearch ? { exact: true } : { numCandidates: options.numCandidates },
11046
+ ...options.filter === void 0 ? {} : { filter: options.filter }
11047
+ };
11048
+ const projection = normalizeVectorProjection(options.projection);
11049
+ const scoreStage = projection ? {
11050
+ $project: {
11051
+ ...projection,
11052
+ [VECTOR_SEARCH_SCORE_FIELD]: { $meta: "vectorSearchScore" }
11053
+ }
11054
+ } : {
11055
+ $set: {
11056
+ [VECTOR_SEARCH_SCORE_FIELD]: { $meta: "vectorSearchScore" }
11057
+ }
11058
+ };
11059
+ return {
11060
+ pipeline: [
11061
+ { $vectorSearch: vectorSearchStage },
11062
+ scoreStage
11063
+ ],
11064
+ aggregateOptions: normalizeAggregateOptions(options.aggregateOptions)
11065
+ };
11066
+ }
11067
+ function mapVectorSearchRows(rows) {
11068
+ return rows.map((row) => {
11069
+ const score = row[VECTOR_SEARCH_SCORE_FIELD];
11070
+ if (typeof score !== "number" || !Number.isFinite(score)) {
11071
+ throw createError(ErrorCodes.INVALID_OPERATION, "Vector Search result did not contain a finite score");
11072
+ }
11073
+ const document = { ...row };
11074
+ delete document[VECTOR_SEARCH_SCORE_FIELD];
11075
+ return {
11076
+ document,
11077
+ score
11078
+ };
11079
+ });
11080
+ }
11081
+
10463
11082
  // src/adapters/mongodb/writes/write-utils.ts
10464
11083
  function sleep2(ms) {
10465
11084
  return new Promise((resolve) => setTimeout(resolve, ms));
@@ -11633,6 +12252,19 @@ var MongoCollectionAccessor = class {
11633
12252
  } : void 0
11634
12253
  );
11635
12254
  }
12255
+ /** Searches a MongoDB Vector Search index through a constrained aggregate pipeline. */
12256
+ async vectorSearch(options) {
12257
+ const { pipeline, aggregateOptions } = buildVectorSearchPipeline(options);
12258
+ try {
12259
+ const rows = await this.aggregate(
12260
+ pipeline,
12261
+ aggregateOptions
12262
+ );
12263
+ return mapVectorSearchRows(rows);
12264
+ } catch (error) {
12265
+ throw attachVectorSearchOperationContext(error, this.getNamespace());
12266
+ }
12267
+ }
11636
12268
  /** Returns an array of distinct values for the given field key. */
11637
12269
  async distinct(key, query, options) {
11638
12270
  const startTs = Date.now();
@@ -12167,6 +12799,7 @@ function createCompatModelInstance(config) {
12167
12799
  config.runtime,
12168
12800
  {
12169
12801
  collectionName: config.collectionName,
12802
+ modelName: config.modelName,
12170
12803
  dbName: config.dbName,
12171
12804
  poolName: config.poolName,
12172
12805
  definition: config.definition,
@@ -12204,6 +12837,7 @@ function createRuntimeModelInstance(host, name, scope) {
12204
12837
  host.runtime,
12205
12838
  {
12206
12839
  collectionName: actualCollectionName,
12840
+ modelName: registered.collectionName,
12207
12841
  dbName: databaseName,
12208
12842
  poolName,
12209
12843
  definition: registered.definition,
@@ -16343,6 +16977,7 @@ var MonSQLizeRuntime = class {
16343
16977
  collection,
16344
16978
  runtime: this,
16345
16979
  collectionName: actualCollectionName,
16980
+ modelName: registered.collectionName,
16346
16981
  dbName: database ?? getRuntimeDatabaseName(this),
16347
16982
  poolName: pool,
16348
16983
  definition: registered.definition,
@@ -16375,6 +17010,7 @@ var MonSQLizeRuntime = class {
16375
17010
  collection,
16376
17011
  runtime: this,
16377
17012
  collectionName: actualCollectionName,
17013
+ modelName: registered.collectionName,
16378
17014
  dbName: getRuntimeDatabaseName(this),
16379
17015
  definition: registered.definition,
16380
17016
  schemaEngine: this._schemaDslEngine