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.
- package/CHANGELOG.md +3 -2
- package/README.md +23 -23
- package/SECURITY.md +1 -1
- package/changelogs/v3.1.11.md +12 -0
- package/changelogs/v3.1.12.md +12 -0
- package/changelogs/v3.1.13.md +12 -0
- package/changelogs/v3.1.14.md +12 -0
- package/changelogs/v3.1.5.md +3 -0
- package/changelogs/v3.1.6.md +4 -0
- package/changelogs/v3.1.7.md +7 -0
- package/changelogs/v3.1.8.md +12 -0
- package/changelogs/v3.2.0.md +24 -0
- package/dist/cjs/cli/data-task.cjs +22 -4
- 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 +171 -153
package/dist/esm/index.mjs
CHANGED
|
@@ -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
|
|
1082
|
-
|
|
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 (
|
|
1104
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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 =
|
|
9589
|
-
const rawLimit =
|
|
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
|