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.
- package/CHANGELOG.md +3 -2
- package/changelogs/v3.2.0.md +24 -0
- package/dist/cjs/cli/data-task.cjs +11 -1
- package/dist/cjs/index.cjs +674 -38
- package/dist/cjs/transaction/Transaction.cjs +8 -0
- package/dist/cjs/transaction/TransactionManager.cjs +8 -0
- package/dist/esm/index.mjs +674 -38
- package/dist/types/aggregation.d.mts +58 -0
- package/dist/types/aggregation.d.ts +58 -0
- package/dist/types/base.d.mts +4 -0
- package/dist/types/base.d.ts +4 -0
- package/dist/types/collection.d.mts +20 -20
- package/dist/types/collection.d.ts +20 -20
- package/dist/types/model.d.mts +87 -1
- package/dist/types/model.d.ts +87 -1
- package/dist/types/runtime.d.mts +4 -0
- package/dist/types/runtime.d.ts +4 -0
- package/package.json +11 -1
package/dist/cjs/index.cjs
CHANGED
|
@@ -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
|
|
1099
|
-
|
|
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 (
|
|
1121
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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 =
|
|
9606
|
-
const rawLimit =
|
|
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
|