monsqlize 3.1.14 → 3.3.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 +8 -2
- package/README.md +25 -1
- package/changelogs/v3.2.0.md +24 -0
- package/changelogs/v3.3.0.md +29 -0
- package/dist/cjs/cli/data-task.cjs +13 -2
- package/dist/cjs/index.cjs +701 -58
- package/dist/cjs/transaction/Transaction.cjs +8 -0
- package/dist/cjs/transaction/TransactionManager.cjs +8 -0
- package/dist/esm/index.mjs +702 -58
- 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 +126 -1
- package/dist/types/model.d.ts +126 -1
- package/dist/types/monsqlize.d.mts +28 -9
- package/dist/types/monsqlize.d.ts +28 -9
- package/dist/types/runtime.d.mts +7 -1
- package/dist/types/runtime.d.ts +7 -1
- package/licenses/production-dependencies.json +1 -1
- package/package.json +13 -2
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,12 +492,49 @@ 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;
|
|
490
532
|
}
|
|
491
533
|
|
|
492
534
|
// src/capabilities/model/model-registry.ts
|
|
535
|
+
function defineModel(collectionName, definition) {
|
|
536
|
+
return { collectionName, definition };
|
|
537
|
+
}
|
|
493
538
|
var Model = class {
|
|
494
539
|
static {
|
|
495
540
|
// collection name → registered model definition
|
|
@@ -504,21 +549,19 @@ var Model = class {
|
|
|
504
549
|
/** Collection names that have been redefined (used by the runtime to trigger hot-reload). */
|
|
505
550
|
this._redefinedNames = /* @__PURE__ */ new Set();
|
|
506
551
|
}
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
* Validates the definition and resolves the timestamps option.
|
|
511
|
-
*/
|
|
512
|
-
static define(collectionName, definition) {
|
|
552
|
+
static define(collectionNameOrDescriptor, definition) {
|
|
553
|
+
const collectionName = typeof collectionNameOrDescriptor === "string" ? collectionNameOrDescriptor : collectionNameOrDescriptor.collectionName;
|
|
554
|
+
const modelDefinition = typeof collectionNameOrDescriptor === "string" ? definition : collectionNameOrDescriptor.definition;
|
|
513
555
|
const normalizedName = validateCollectionName(collectionName);
|
|
514
556
|
if (this.registry.has(normalizedName)) {
|
|
515
557
|
throw createError(ErrorCodes.MODEL_ALREADY_EXISTS, `Model '${normalizedName}' is already defined.`);
|
|
516
558
|
}
|
|
517
|
-
validateDefinition(
|
|
518
|
-
|
|
559
|
+
validateDefinition(modelDefinition);
|
|
560
|
+
const resolvedDefinition = modelDefinition;
|
|
561
|
+
processTimestamps(resolvedDefinition);
|
|
519
562
|
this.registry.set(normalizedName, {
|
|
520
563
|
collectionName: normalizedName,
|
|
521
|
-
definition
|
|
564
|
+
definition: resolvedDefinition
|
|
522
565
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
523
566
|
});
|
|
524
567
|
this.bumpRevision(normalizedName);
|
|
@@ -1060,6 +1103,56 @@ function resolveRegisteredCollectionName(registered, fallback) {
|
|
|
1060
1103
|
const definition = registered.definition;
|
|
1061
1104
|
return definition.collection ?? definition.name ?? registered.collectionName;
|
|
1062
1105
|
}
|
|
1106
|
+
function flattenComparableValues(value) {
|
|
1107
|
+
if (Array.isArray(value)) {
|
|
1108
|
+
return value.flatMap((entry) => flattenComparableValues(entry));
|
|
1109
|
+
}
|
|
1110
|
+
return value === void 0 || value === null ? [] : [value];
|
|
1111
|
+
}
|
|
1112
|
+
function resolveNestedPopulatePaths(value) {
|
|
1113
|
+
if (value === void 0) {
|
|
1114
|
+
return [];
|
|
1115
|
+
}
|
|
1116
|
+
const paths = Array.isArray(value) ? value : [value];
|
|
1117
|
+
for (const path5 of paths) {
|
|
1118
|
+
const valid = typeof path5 === "string" || typeof path5 === "object" && path5 !== null && !Array.isArray(path5) && Boolean(path5.path);
|
|
1119
|
+
if (!valid) {
|
|
1120
|
+
throw createError(ErrorCodes.INVALID_ARGUMENT, "nested populate must be a string, array, or object");
|
|
1121
|
+
}
|
|
1122
|
+
}
|
|
1123
|
+
return paths;
|
|
1124
|
+
}
|
|
1125
|
+
function collectNestedLocalFields(relatedModel, nestedPaths) {
|
|
1126
|
+
if (!relatedModel) {
|
|
1127
|
+
return [];
|
|
1128
|
+
}
|
|
1129
|
+
const relations = relatedModel.getRelations();
|
|
1130
|
+
return unique(
|
|
1131
|
+
nestedPaths.map((path5) => relations[normalizePopulateConfig(path5).path]?.localField).filter((field) => Boolean(field))
|
|
1132
|
+
);
|
|
1133
|
+
}
|
|
1134
|
+
function dedupeDocuments(documents) {
|
|
1135
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1136
|
+
return documents.filter((document) => {
|
|
1137
|
+
if (seen.has(document)) {
|
|
1138
|
+
return false;
|
|
1139
|
+
}
|
|
1140
|
+
seen.add(document);
|
|
1141
|
+
return true;
|
|
1142
|
+
});
|
|
1143
|
+
}
|
|
1144
|
+
function rebuildPopulateMatches(localValue, grouped, sort) {
|
|
1145
|
+
const matches = flattenComparableValues(localValue).flatMap((value) => grouped.get(toKey(value)) ?? []);
|
|
1146
|
+
const deduped = dedupeDocuments(matches);
|
|
1147
|
+
return sort ? applySort(deduped, sort) : deduped;
|
|
1148
|
+
}
|
|
1149
|
+
function projectPopulatedDocument(document, select, nestedPaths) {
|
|
1150
|
+
if (!select) {
|
|
1151
|
+
return document;
|
|
1152
|
+
}
|
|
1153
|
+
const nestedRelationFields = nestedPaths.map((path5) => normalizePopulateConfig(path5).path);
|
|
1154
|
+
return pickFields(document, [.../* @__PURE__ */ new Set([...select, ...nestedRelationFields])]);
|
|
1155
|
+
}
|
|
1063
1156
|
async function populateModelPath(context, docs, path5, traversalState) {
|
|
1064
1157
|
const config = normalizePopulateConfig(path5);
|
|
1065
1158
|
const state = resolvePopulateState(config, traversalState);
|
|
@@ -1078,9 +1171,8 @@ async function populateModelPath(context, docs, path5, traversalState) {
|
|
|
1078
1171
|
if (!relation) {
|
|
1079
1172
|
throw createError(ErrorCodes.INVALID_ARGUMENT, `Undefined relation: ${config.path}`);
|
|
1080
1173
|
}
|
|
1081
|
-
const
|
|
1082
|
-
|
|
1083
|
-
);
|
|
1174
|
+
const localValues = docs.map((doc) => getByPath(doc, relation.localField));
|
|
1175
|
+
const keys = unique(localValues.flatMap((value) => flattenComparableValues(value)));
|
|
1084
1176
|
if (keys.length === 0) {
|
|
1085
1177
|
for (const doc of docs) {
|
|
1086
1178
|
doc[config.path] = relation.single ? null : [];
|
|
@@ -1095,51 +1187,42 @@ async function populateModelPath(context, docs, path5, traversalState) {
|
|
|
1095
1187
|
const relatedCollectionName = resolveRegisteredCollectionName(registered, relation.from);
|
|
1096
1188
|
const relatedCollection = context.runtime.scopedCollection(relatedCollectionName, scope);
|
|
1097
1189
|
const relatedModel = Model.has(relation.from) ? context.runtime.scopedModel(relation.from, scope) : null;
|
|
1190
|
+
const nestedPaths = resolveNestedPopulatePaths(config.populate);
|
|
1191
|
+
const effectiveSelect = normalizeRelationSelect(config.select, { path: config.path }) ?? normalizeRelationSelect(relation.select, { relation: config.path });
|
|
1192
|
+
const projectionFields = effectiveSelect ? [.../* @__PURE__ */ new Set([
|
|
1193
|
+
...effectiveSelect,
|
|
1194
|
+
relation.foreignField,
|
|
1195
|
+
"_id",
|
|
1196
|
+
...collectNestedLocalFields(relatedModel, nestedPaths)
|
|
1197
|
+
])] : [];
|
|
1098
1198
|
const relatedDocs = await relatedCollection.find({
|
|
1099
1199
|
[relation.foreignField]: { $in: keys },
|
|
1100
1200
|
...config.match ?? {}
|
|
1101
|
-
}
|
|
1201
|
+
}, effectiveSelect ? {
|
|
1202
|
+
projection: Object.fromEntries(projectionFields.map((field) => [field, 1]))
|
|
1203
|
+
} : void 0);
|
|
1102
1204
|
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
|
-
});
|
|
1205
|
+
if (nestedPaths.length > 0 && relatedModel) {
|
|
1206
|
+
if (state.depth + 1 > state.maxDepth) {
|
|
1207
|
+
throw createError(ErrorCodes.INVALID_ARGUMENT, `populate maxDepth exceeded: ${state.maxDepth}`);
|
|
1130
1208
|
}
|
|
1209
|
+
hydrated = await relatedModel.populateDocuments(hydrated, nestedPaths, {
|
|
1210
|
+
depth: state.depth + 1,
|
|
1211
|
+
maxDepth: state.maxDepth,
|
|
1212
|
+
activeConfigs: state.activeConfigs
|
|
1213
|
+
});
|
|
1131
1214
|
}
|
|
1132
1215
|
const grouped = groupBy(hydrated, (item) => getByPath(item, relation.foreignField));
|
|
1133
|
-
for (const doc of docs) {
|
|
1134
|
-
|
|
1135
|
-
let matches = grouped.get(toKey(localValue)) ?? [];
|
|
1216
|
+
for (const [index, doc] of docs.entries()) {
|
|
1217
|
+
let matches = rebuildPopulateMatches(localValues[index], grouped, config.sort);
|
|
1136
1218
|
if (config.skip) {
|
|
1137
1219
|
matches = matches.slice(config.skip);
|
|
1138
1220
|
}
|
|
1139
1221
|
if (config.limit !== void 0) {
|
|
1140
1222
|
matches = matches.slice(0, config.limit);
|
|
1141
1223
|
}
|
|
1142
|
-
|
|
1224
|
+
const projectedMatches = matches.map((item) => projectPopulatedDocument(item, effectiveSelect, nestedPaths));
|
|
1225
|
+
doc[config.path] = relation.single ? projectedMatches[0] ?? null : projectedMatches;
|
|
1143
1226
|
}
|
|
1144
1227
|
return docs;
|
|
1145
1228
|
} finally {
|
|
@@ -5557,7 +5640,382 @@ async function orchestrateModelDeleteMany(context, filter, options) {
|
|
|
5557
5640
|
return result;
|
|
5558
5641
|
}
|
|
5559
5642
|
|
|
5643
|
+
// src/capabilities/model/relation-usage.ts
|
|
5644
|
+
var DEFAULT_MAX_TARGETS = 100;
|
|
5645
|
+
var DEFAULT_MAX_SAMPLES = 20;
|
|
5646
|
+
function isNonArrayObject(value) {
|
|
5647
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
5648
|
+
}
|
|
5649
|
+
function normalizePositiveInteger(value, field, fallback) {
|
|
5650
|
+
if (value === void 0) {
|
|
5651
|
+
return fallback;
|
|
5652
|
+
}
|
|
5653
|
+
if (!Number.isInteger(value) || value <= 0) {
|
|
5654
|
+
throw createError(ErrorCodes.INVALID_ARGUMENT, `${field} must be a positive integer`, [{ field, value }]);
|
|
5655
|
+
}
|
|
5656
|
+
return value;
|
|
5657
|
+
}
|
|
5658
|
+
function normalizeRelationNames(value, field) {
|
|
5659
|
+
if (value === void 0) {
|
|
5660
|
+
return void 0;
|
|
5661
|
+
}
|
|
5662
|
+
if (!Array.isArray(value) || value.some((entry) => typeof entry !== "string" || entry.trim().length === 0)) {
|
|
5663
|
+
throw createError(ErrorCodes.INVALID_ARGUMENT, `${field} must be an array of non-empty strings`, [{ field, value }]);
|
|
5664
|
+
}
|
|
5665
|
+
return [...new Set(value.map((entry) => entry.trim()))];
|
|
5666
|
+
}
|
|
5667
|
+
function normalizeRelationUsageOptions(options) {
|
|
5668
|
+
if (options !== void 0 && !isNonArrayObject(options)) {
|
|
5669
|
+
throw createError(ErrorCodes.INVALID_ARGUMENT, "relation usage options must be a non-array object", [{ field: "options", value: options }]);
|
|
5670
|
+
}
|
|
5671
|
+
const value = options ?? {};
|
|
5672
|
+
if (value.includeSoftDeletedReferences !== void 0 && typeof value.includeSoftDeletedReferences !== "boolean") {
|
|
5673
|
+
throw createError(
|
|
5674
|
+
ErrorCodes.INVALID_ARGUMENT,
|
|
5675
|
+
"includeSoftDeletedReferences must be a boolean",
|
|
5676
|
+
[{ field: "includeSoftDeletedReferences", value: value.includeSoftDeletedReferences }]
|
|
5677
|
+
);
|
|
5678
|
+
}
|
|
5679
|
+
return {
|
|
5680
|
+
session: value.session,
|
|
5681
|
+
maxTargets: normalizePositiveInteger(value.maxTargets, "maxTargets", DEFAULT_MAX_TARGETS),
|
|
5682
|
+
maxSamples: normalizePositiveInteger(value.maxSamples, "maxSamples", DEFAULT_MAX_SAMPLES),
|
|
5683
|
+
includeRelations: normalizeRelationNames(value.includeRelations, "includeRelations"),
|
|
5684
|
+
excludeRelations: normalizeRelationNames(value.excludeRelations, "excludeRelations"),
|
|
5685
|
+
includeSoftDeletedReferences: value.includeSoftDeletedReferences !== false
|
|
5686
|
+
};
|
|
5687
|
+
}
|
|
5688
|
+
function resolveRegisteredCollectionName2(registered) {
|
|
5689
|
+
const definition = registered.definition;
|
|
5690
|
+
return definition.collection ?? definition.name ?? registered.collectionName;
|
|
5691
|
+
}
|
|
5692
|
+
function flattenComparableValues2(value) {
|
|
5693
|
+
if (Array.isArray(value)) {
|
|
5694
|
+
return value.flatMap((entry) => flattenComparableValues2(entry));
|
|
5695
|
+
}
|
|
5696
|
+
return value === void 0 || value === null ? [] : [value];
|
|
5697
|
+
}
|
|
5698
|
+
function isRelationSelected(options, sourceModel, relation) {
|
|
5699
|
+
const qualified = `${sourceModel}.${relation}`;
|
|
5700
|
+
const matches = (names) => Boolean(names?.includes(qualified) || names?.includes(relation));
|
|
5701
|
+
if (options.includeRelations && !matches(options.includeRelations)) {
|
|
5702
|
+
return false;
|
|
5703
|
+
}
|
|
5704
|
+
return !matches(options.excludeRelations);
|
|
5705
|
+
}
|
|
5706
|
+
function describeError(error) {
|
|
5707
|
+
return error instanceof Error ? error.message : String(error);
|
|
5708
|
+
}
|
|
5709
|
+
function findDeclaredInboundRelations(targetModelName) {
|
|
5710
|
+
const relations = [];
|
|
5711
|
+
for (const sourceModel of Model.list()) {
|
|
5712
|
+
const registered = Model.get(sourceModel);
|
|
5713
|
+
if (!registered) {
|
|
5714
|
+
continue;
|
|
5715
|
+
}
|
|
5716
|
+
for (const [relation, config] of Object.entries(registered.definition.relations ?? {})) {
|
|
5717
|
+
if (config.from !== targetModelName) {
|
|
5718
|
+
continue;
|
|
5719
|
+
}
|
|
5720
|
+
relations.push({
|
|
5721
|
+
sourceModel,
|
|
5722
|
+
sourceCollection: resolveRegisteredCollectionName2(registered),
|
|
5723
|
+
relation,
|
|
5724
|
+
definition: registered.definition,
|
|
5725
|
+
localField: config.localField,
|
|
5726
|
+
foreignField: config.foreignField
|
|
5727
|
+
});
|
|
5728
|
+
}
|
|
5729
|
+
}
|
|
5730
|
+
return relations;
|
|
5731
|
+
}
|
|
5732
|
+
function buildRelationUsageTargetProjection(targetModelName) {
|
|
5733
|
+
const fields = /* @__PURE__ */ new Set(["_id"]);
|
|
5734
|
+
for (const relation of findDeclaredInboundRelations(targetModelName)) {
|
|
5735
|
+
fields.add(relation.foreignField);
|
|
5736
|
+
}
|
|
5737
|
+
return Object.fromEntries([...fields].map((field) => [field, 1]));
|
|
5738
|
+
}
|
|
5739
|
+
function createUnavailableRelationUsageReport(context, reason) {
|
|
5740
|
+
return {
|
|
5741
|
+
used: false,
|
|
5742
|
+
target: {
|
|
5743
|
+
matchedCount: 0,
|
|
5744
|
+
truncated: false,
|
|
5745
|
+
sampleIds: []
|
|
5746
|
+
},
|
|
5747
|
+
checkedRelations: 0,
|
|
5748
|
+
usages: [],
|
|
5749
|
+
coverage: {
|
|
5750
|
+
mode: "registered-declared",
|
|
5751
|
+
complete: false,
|
|
5752
|
+
registryModels: Model.list(),
|
|
5753
|
+
checkedModels: [],
|
|
5754
|
+
skipped: [{ sourceModel: context.modelName, reason }]
|
|
5755
|
+
}
|
|
5756
|
+
};
|
|
5757
|
+
}
|
|
5758
|
+
async function inspectDeclaredRelationUsage(context, targets, inputOptions, targetTruncated = false) {
|
|
5759
|
+
const options = normalizeRelationUsageOptions(inputOptions);
|
|
5760
|
+
const registryModels = Model.list();
|
|
5761
|
+
const checkedModels = /* @__PURE__ */ new Set();
|
|
5762
|
+
const skipped = [];
|
|
5763
|
+
const usages = [];
|
|
5764
|
+
let checkedRelations = 0;
|
|
5765
|
+
for (const inbound of findDeclaredInboundRelations(context.modelName)) {
|
|
5766
|
+
if (!isRelationSelected(options, inbound.sourceModel, inbound.relation)) {
|
|
5767
|
+
skipped.push({
|
|
5768
|
+
sourceModel: inbound.sourceModel,
|
|
5769
|
+
reason: `relation '${inbound.sourceModel}.${inbound.relation}' was excluded by relation selection`
|
|
5770
|
+
});
|
|
5771
|
+
continue;
|
|
5772
|
+
}
|
|
5773
|
+
checkedRelations += 1;
|
|
5774
|
+
const targetValues = unique(
|
|
5775
|
+
targets.flatMap((target) => flattenComparableValues2(getByPath(target, inbound.foreignField)))
|
|
5776
|
+
);
|
|
5777
|
+
if (targetValues.length === 0) {
|
|
5778
|
+
checkedModels.add(inbound.sourceModel);
|
|
5779
|
+
continue;
|
|
5780
|
+
}
|
|
5781
|
+
const scope = {
|
|
5782
|
+
database: inbound.definition.connection?.database ?? context.dbName,
|
|
5783
|
+
pool: inbound.definition.connection?.pool ?? context.poolName
|
|
5784
|
+
};
|
|
5785
|
+
const baseQuery = {
|
|
5786
|
+
[inbound.localField]: { $in: targetValues }
|
|
5787
|
+
};
|
|
5788
|
+
const sourceSoftDelete = resolveModelSoftDeleteConfig(inbound.definition);
|
|
5789
|
+
const query = !options.includeSoftDeletedReferences && sourceSoftDelete?.enabled ? { $and: [baseQuery, { [sourceSoftDelete.field]: null }] } : baseQuery;
|
|
5790
|
+
try {
|
|
5791
|
+
const collection = context.runtime.scopedCollection(inbound.sourceCollection, scope);
|
|
5792
|
+
if (!collection) {
|
|
5793
|
+
throw new Error("scopedCollection returned no collection");
|
|
5794
|
+
}
|
|
5795
|
+
checkedModels.add(inbound.sourceModel);
|
|
5796
|
+
const count = await collection.count(query, { session: options.session });
|
|
5797
|
+
if (typeof count !== "number" || !Number.isFinite(count) || count < 0) {
|
|
5798
|
+
throw new Error("relation usage count did not return a non-negative number");
|
|
5799
|
+
}
|
|
5800
|
+
if (count === 0) {
|
|
5801
|
+
continue;
|
|
5802
|
+
}
|
|
5803
|
+
const samples = await collection.find(query, {
|
|
5804
|
+
session: options.session,
|
|
5805
|
+
projection: { _id: 1 },
|
|
5806
|
+
limit: options.maxSamples
|
|
5807
|
+
});
|
|
5808
|
+
usages.push({
|
|
5809
|
+
sourceModel: inbound.sourceModel,
|
|
5810
|
+
sourceCollection: inbound.sourceCollection,
|
|
5811
|
+
relation: inbound.relation,
|
|
5812
|
+
localField: inbound.localField,
|
|
5813
|
+
foreignField: inbound.foreignField,
|
|
5814
|
+
count,
|
|
5815
|
+
sampleIds: samples.filter((sample) => isNonArrayObject(sample)).map((sample) => sample._id).filter((id) => id !== void 0).slice(0, options.maxSamples)
|
|
5816
|
+
});
|
|
5817
|
+
} catch (error) {
|
|
5818
|
+
skipped.push({
|
|
5819
|
+
sourceModel: inbound.sourceModel,
|
|
5820
|
+
reason: describeError(error)
|
|
5821
|
+
});
|
|
5822
|
+
}
|
|
5823
|
+
}
|
|
5824
|
+
const targetSampleIds = targets.map((target) => target._id).filter((id) => id !== void 0).slice(0, options.maxSamples);
|
|
5825
|
+
return {
|
|
5826
|
+
used: usages.length > 0,
|
|
5827
|
+
target: {
|
|
5828
|
+
matchedCount: targets.length,
|
|
5829
|
+
truncated: targetTruncated,
|
|
5830
|
+
sampleIds: targetSampleIds
|
|
5831
|
+
},
|
|
5832
|
+
checkedRelations,
|
|
5833
|
+
usages,
|
|
5834
|
+
coverage: {
|
|
5835
|
+
mode: "registered-declared",
|
|
5836
|
+
complete: !targetTruncated && skipped.length === 0,
|
|
5837
|
+
registryModels,
|
|
5838
|
+
checkedModels: [...checkedModels],
|
|
5839
|
+
skipped
|
|
5840
|
+
}
|
|
5841
|
+
};
|
|
5842
|
+
}
|
|
5843
|
+
function stripRelationUsageOptions(options) {
|
|
5844
|
+
if (!isNonArrayObject(options)) {
|
|
5845
|
+
return options;
|
|
5846
|
+
}
|
|
5847
|
+
const {
|
|
5848
|
+
maxTargets: _maxTargets,
|
|
5849
|
+
maxSamples: _maxSamples,
|
|
5850
|
+
includeRelations: _includeRelations,
|
|
5851
|
+
excludeRelations: _excludeRelations,
|
|
5852
|
+
includeSoftDeletedReferences: _includeSoftDeletedReferences,
|
|
5853
|
+
...deleteOptions
|
|
5854
|
+
} = options;
|
|
5855
|
+
void _maxTargets;
|
|
5856
|
+
void _maxSamples;
|
|
5857
|
+
void _includeRelations;
|
|
5858
|
+
void _excludeRelations;
|
|
5859
|
+
void _includeSoftDeletedReferences;
|
|
5860
|
+
return deleteOptions;
|
|
5861
|
+
}
|
|
5862
|
+
|
|
5863
|
+
// src/capabilities/model/model-p0-operations.ts
|
|
5864
|
+
function relationUsageContext(host) {
|
|
5865
|
+
return {
|
|
5866
|
+
modelName: host.modelName,
|
|
5867
|
+
runtime: host.runtime,
|
|
5868
|
+
dbName: host.dbName,
|
|
5869
|
+
poolName: host.poolName
|
|
5870
|
+
};
|
|
5871
|
+
}
|
|
5872
|
+
function isNonEmptyFilter(filter) {
|
|
5873
|
+
return typeof filter === "object" && filter !== null && !Array.isArray(filter) && Object.keys(filter).length > 0;
|
|
5874
|
+
}
|
|
5875
|
+
function relationTargetReadOptions(options, projection) {
|
|
5876
|
+
const source = options ?? {};
|
|
5877
|
+
const result = { projection };
|
|
5878
|
+
for (const key of ["session", "collation", "hint", "comment", "let", "maxTimeMS", "readConcern", "readPreference"]) {
|
|
5879
|
+
if (source[key] !== void 0) {
|
|
5880
|
+
result[key] = source[key];
|
|
5881
|
+
}
|
|
5882
|
+
}
|
|
5883
|
+
return result;
|
|
5884
|
+
}
|
|
5885
|
+
function hydrateVectorSearchHits(host, hits) {
|
|
5886
|
+
return hits.map((hit) => {
|
|
5887
|
+
const document = host.hydrateDocument(hit.document);
|
|
5888
|
+
if (!document) {
|
|
5889
|
+
throw createError(ErrorCodes.INVALID_OPERATION, "Vector Search returned a non-document result");
|
|
5890
|
+
}
|
|
5891
|
+
return { document, score: hit.score };
|
|
5892
|
+
});
|
|
5893
|
+
}
|
|
5894
|
+
async function executeModelVectorSearch(host, options) {
|
|
5895
|
+
if (typeof options !== "object" || options === null || Array.isArray(options)) {
|
|
5896
|
+
const hits2 = await host.collection.vectorSearch(options);
|
|
5897
|
+
return hydrateVectorSearchHits(host, hits2);
|
|
5898
|
+
}
|
|
5899
|
+
const { withDeleted, onlyDeleted, ...collectionOptions } = options;
|
|
5900
|
+
if (withDeleted !== void 0 && typeof withDeleted !== "boolean") {
|
|
5901
|
+
throw createError(
|
|
5902
|
+
ErrorCodes.INVALID_VECTOR_SEARCH,
|
|
5903
|
+
"withDeleted must be a boolean",
|
|
5904
|
+
[{ field: "withDeleted", reason: "boolean required" }]
|
|
5905
|
+
);
|
|
5906
|
+
}
|
|
5907
|
+
if (onlyDeleted !== void 0 && typeof onlyDeleted !== "boolean") {
|
|
5908
|
+
throw createError(
|
|
5909
|
+
ErrorCodes.INVALID_VECTOR_SEARCH,
|
|
5910
|
+
"onlyDeleted must be a boolean",
|
|
5911
|
+
[{ field: "onlyDeleted", reason: "boolean required" }]
|
|
5912
|
+
);
|
|
5913
|
+
}
|
|
5914
|
+
const filter = host.softDeleteConfig?.enabled && !withDeleted ? applyModelSoftDeleteFilter(
|
|
5915
|
+
collectionOptions.filter,
|
|
5916
|
+
{ withDeleted, onlyDeleted },
|
|
5917
|
+
host.softDeleteConfig
|
|
5918
|
+
) : collectionOptions.filter;
|
|
5919
|
+
const hits = await host.collection.vectorSearch({
|
|
5920
|
+
...collectionOptions,
|
|
5921
|
+
...filter === void 0 ? {} : { filter }
|
|
5922
|
+
});
|
|
5923
|
+
return hydrateVectorSearchHits(host, hits);
|
|
5924
|
+
}
|
|
5925
|
+
async function executeRelationUsageCheck(host, filter, options) {
|
|
5926
|
+
const normalizedOptions = normalizeRelationUsageOptions(options);
|
|
5927
|
+
const context = relationUsageContext(host);
|
|
5928
|
+
try {
|
|
5929
|
+
const rawTargets = await host.collection.find(
|
|
5930
|
+
applyModelSoftDeleteFilter(filter, void 0, host.softDeleteConfig),
|
|
5931
|
+
{
|
|
5932
|
+
session: normalizedOptions.session,
|
|
5933
|
+
projection: buildRelationUsageTargetProjection(host.modelName),
|
|
5934
|
+
limit: normalizedOptions.maxTargets + 1
|
|
5935
|
+
}
|
|
5936
|
+
);
|
|
5937
|
+
const targets = rawTargets.filter(
|
|
5938
|
+
(target) => typeof target === "object" && target !== null && !Array.isArray(target)
|
|
5939
|
+
);
|
|
5940
|
+
return inspectDeclaredRelationUsage(
|
|
5941
|
+
context,
|
|
5942
|
+
targets.slice(0, normalizedOptions.maxTargets),
|
|
5943
|
+
normalizedOptions,
|
|
5944
|
+
targets.length > normalizedOptions.maxTargets
|
|
5945
|
+
);
|
|
5946
|
+
} catch (error) {
|
|
5947
|
+
return createUnavailableRelationUsageReport(context, error instanceof Error ? error.message : String(error));
|
|
5948
|
+
}
|
|
5949
|
+
}
|
|
5950
|
+
async function executeProtectedRelationDelete(host, filter, options, force) {
|
|
5951
|
+
if (!isNonEmptyFilter(filter)) {
|
|
5952
|
+
throw createError(
|
|
5953
|
+
ErrorCodes.INVALID_ARGUMENT,
|
|
5954
|
+
"protected relation deletion requires a non-empty filter",
|
|
5955
|
+
[{ field: "filter", reason: "non-empty object required" }]
|
|
5956
|
+
);
|
|
5957
|
+
}
|
|
5958
|
+
const rawOptions = options ?? {};
|
|
5959
|
+
if (rawOptions.includeRelations !== void 0 || rawOptions.excludeRelations !== void 0) {
|
|
5960
|
+
throw createError(
|
|
5961
|
+
ErrorCodes.INVALID_ARGUMENT,
|
|
5962
|
+
"protected relation deletion cannot narrow the declared relation scan",
|
|
5963
|
+
[{ field: "includeRelations/excludeRelations", reason: "protected deletion always scans all declared relations" }]
|
|
5964
|
+
);
|
|
5965
|
+
}
|
|
5966
|
+
const usageOptions = normalizeRelationUsageOptions(options);
|
|
5967
|
+
const context = relationUsageContext(host);
|
|
5968
|
+
const deleteOptions = stripRelationUsageOptions(options);
|
|
5969
|
+
let target;
|
|
5970
|
+
try {
|
|
5971
|
+
const targetFilter = force || !host.softDeleteConfig?.enabled ? filter : { ...filter, [host.softDeleteConfig.field]: null };
|
|
5972
|
+
const rawTarget = await host.collection.findOne(
|
|
5973
|
+
targetFilter,
|
|
5974
|
+
relationTargetReadOptions(options, buildRelationUsageTargetProjection(host.modelName))
|
|
5975
|
+
);
|
|
5976
|
+
target = typeof rawTarget === "object" && rawTarget !== null && !Array.isArray(rawTarget) ? rawTarget : null;
|
|
5977
|
+
} catch (error) {
|
|
5978
|
+
const usage2 = createUnavailableRelationUsageReport(
|
|
5979
|
+
context,
|
|
5980
|
+
error instanceof Error ? error.message : String(error)
|
|
5981
|
+
);
|
|
5982
|
+
throw createError(
|
|
5983
|
+
ErrorCodes.RELATION_USAGE_UNAVAILABLE,
|
|
5984
|
+
"Unable to resolve a target for protected relation deletion",
|
|
5985
|
+
[{ coverage: usage2.coverage, reason: usage2.coverage.skipped[0]?.reason, skipped: usage2.coverage.skipped }]
|
|
5986
|
+
);
|
|
5987
|
+
}
|
|
5988
|
+
const usage = await inspectDeclaredRelationUsage(context, target ? [target] : [], usageOptions);
|
|
5989
|
+
if (!usage.coverage.complete) {
|
|
5990
|
+
throw createError(
|
|
5991
|
+
ErrorCodes.RELATION_USAGE_UNAVAILABLE,
|
|
5992
|
+
"Declared relation usage coverage is incomplete; protected deletion was not performed",
|
|
5993
|
+
[{ coverage: usage.coverage, reason: "incomplete relation usage coverage", skipped: usage.coverage.skipped }]
|
|
5994
|
+
);
|
|
5995
|
+
}
|
|
5996
|
+
if (usage.used) {
|
|
5997
|
+
throw createError(
|
|
5998
|
+
ErrorCodes.RELATION_IN_USE,
|
|
5999
|
+
"Protected deletion was blocked because declared relations still reference the target",
|
|
6000
|
+
[{ used: usage.used, usages: usage.usages, coverage: usage.coverage }]
|
|
6001
|
+
);
|
|
6002
|
+
}
|
|
6003
|
+
if (target && target._id === void 0) {
|
|
6004
|
+
throw createError(
|
|
6005
|
+
ErrorCodes.RELATION_USAGE_UNAVAILABLE,
|
|
6006
|
+
"Protected deletion target did not include an _id",
|
|
6007
|
+
[{ coverage: usage.coverage, reason: "target _id missing", skipped: usage.coverage.skipped }]
|
|
6008
|
+
);
|
|
6009
|
+
}
|
|
6010
|
+
const deleteFilter = target ? { _id: target._id } : { _id: { $in: [] } };
|
|
6011
|
+
const result = force ? await host.forceDelete(deleteFilter, deleteOptions) : await host.deleteOne(deleteFilter, deleteOptions);
|
|
6012
|
+
return { result, usage };
|
|
6013
|
+
}
|
|
6014
|
+
|
|
5560
6015
|
// src/capabilities/model/index.ts
|
|
6016
|
+
function resolveModelRegistryName(definition, fallback) {
|
|
6017
|
+
return Model.list().find((name) => Model.get(name)?.definition === definition) ?? fallback;
|
|
6018
|
+
}
|
|
5561
6019
|
var ModelInstance = class {
|
|
5562
6020
|
constructor(collection, runtime, options) {
|
|
5563
6021
|
this.collection = collection;
|
|
@@ -5578,6 +6036,7 @@ var ModelInstance = class {
|
|
|
5578
6036
|
this.dbName = options.dbName;
|
|
5579
6037
|
this.poolName = options.poolName;
|
|
5580
6038
|
this.definition = options.definition;
|
|
6039
|
+
this.modelName = options.modelName ?? resolveModelRegistryName(options.definition, options.collectionName);
|
|
5581
6040
|
this.relations = new Map(Object.entries(options.definition.relations ?? {}));
|
|
5582
6041
|
for (const [name, config] of this.relations) {
|
|
5583
6042
|
validateRelationConfig(name, config);
|
|
@@ -5761,6 +6220,9 @@ var ModelInstance = class {
|
|
|
5761
6220
|
};
|
|
5762
6221
|
});
|
|
5763
6222
|
}
|
|
6223
|
+
async vectorSearch(options) {
|
|
6224
|
+
return executeModelVectorSearch(this.p0OperationsContext(), options);
|
|
6225
|
+
}
|
|
5764
6226
|
count(query, options) {
|
|
5765
6227
|
const filteredQuery = applyModelSoftDeleteFilter(query, options, this._softDeleteConfig);
|
|
5766
6228
|
return this.runV1HookedOperation("find", [filteredQuery, options], (nextQuery, nextOptions) => this.collection.count(nextQuery, nextOptions));
|
|
@@ -5810,6 +6272,19 @@ var ModelInstance = class {
|
|
|
5810
6272
|
async deleteMany(filter, options) {
|
|
5811
6273
|
return this.runModelWrite(() => orchestrateModelDeleteMany(this.mutationContext(), filter, options));
|
|
5812
6274
|
}
|
|
6275
|
+
/**
|
|
6276
|
+
* Inspects only registered and declared inbound relations. It is read-only;
|
|
6277
|
+
* callers must check coverage before treating an empty result as safe.
|
|
6278
|
+
*/
|
|
6279
|
+
async checkRelationUsage(filter, options) {
|
|
6280
|
+
return executeRelationUsageCheck(this.p0OperationsContext(), filter, options);
|
|
6281
|
+
}
|
|
6282
|
+
async deleteOneWithRelations(filter, options) {
|
|
6283
|
+
return executeProtectedRelationDelete(this.p0OperationsContext(), filter, options, false);
|
|
6284
|
+
}
|
|
6285
|
+
async forceDeleteWithRelations(filter, options) {
|
|
6286
|
+
return executeProtectedRelationDelete(this.p0OperationsContext(), filter, options, true);
|
|
6287
|
+
}
|
|
5813
6288
|
// ── soft-delete extended methods (only meaningful when softDelete is enabled) ──
|
|
5814
6289
|
findWithDeleted(query, options) {
|
|
5815
6290
|
return findWithDeletedDocuments(this.softDeleteContext(), query, options);
|
|
@@ -5978,6 +6453,19 @@ var ModelInstance = class {
|
|
|
5978
6453
|
populateDocument: (document, paths) => this.populateSingle(document, paths)
|
|
5979
6454
|
}, doc);
|
|
5980
6455
|
}
|
|
6456
|
+
p0OperationsContext() {
|
|
6457
|
+
return {
|
|
6458
|
+
collection: this.collection,
|
|
6459
|
+
softDeleteConfig: this._softDeleteConfig,
|
|
6460
|
+
modelName: this.modelName,
|
|
6461
|
+
runtime: this.runtime,
|
|
6462
|
+
dbName: this.dbName,
|
|
6463
|
+
poolName: this.poolName,
|
|
6464
|
+
hydrateDocument: (doc) => this.hydrateDocument(doc),
|
|
6465
|
+
deleteOne: (filter, options) => this.deleteOne(filter, options),
|
|
6466
|
+
forceDelete: (filter, options) => this.forceDelete(filter, options)
|
|
6467
|
+
};
|
|
6468
|
+
}
|
|
5981
6469
|
softDeleteContext() {
|
|
5982
6470
|
return {
|
|
5983
6471
|
collection: this.collection,
|
|
@@ -7173,6 +7661,7 @@ var CountQueue = class {
|
|
|
7173
7661
|
};
|
|
7174
7662
|
|
|
7175
7663
|
// src/capabilities/pool/pool-health-checker.ts
|
|
7664
|
+
var SILENT_HEALTH_CHECKER_LOGGER = {};
|
|
7176
7665
|
var HealthChecker = class {
|
|
7177
7666
|
constructor(options = {}) {
|
|
7178
7667
|
this._healthStatus = /* @__PURE__ */ new Map();
|
|
@@ -7182,7 +7671,7 @@ var HealthChecker = class {
|
|
|
7182
7671
|
this._inProgress = /* @__PURE__ */ new Set();
|
|
7183
7672
|
this._started = false;
|
|
7184
7673
|
this._poolManager = options.poolManager ?? null;
|
|
7185
|
-
this._logger = options.logger ??
|
|
7674
|
+
this._logger = options.logger ?? SILENT_HEALTH_CHECKER_LOGGER;
|
|
7186
7675
|
}
|
|
7187
7676
|
register(poolNameOrConfig, configOrClient) {
|
|
7188
7677
|
let poolName;
|
|
@@ -7306,6 +7795,7 @@ var HealthChecker = class {
|
|
|
7306
7795
|
};
|
|
7307
7796
|
|
|
7308
7797
|
// src/capabilities/pool/pool-selector.ts
|
|
7798
|
+
var SILENT_POOL_SELECTOR_LOGGER = {};
|
|
7309
7799
|
var POOL_STRATEGY_ALIASES = {
|
|
7310
7800
|
"round-robin": "roundRobin",
|
|
7311
7801
|
"least-connections": "leastConnections",
|
|
@@ -7319,7 +7809,7 @@ var PoolSelector = class {
|
|
|
7319
7809
|
constructor(options = {}) {
|
|
7320
7810
|
this._roundRobinIndex = /* @__PURE__ */ new Map();
|
|
7321
7811
|
this._strategy = normalizePoolStrategy(options.strategy);
|
|
7322
|
-
this._logger = options.logger ??
|
|
7812
|
+
this._logger = options.logger ?? SILENT_POOL_SELECTOR_LOGGER;
|
|
7323
7813
|
}
|
|
7324
7814
|
select(pools, context) {
|
|
7325
7815
|
if (!pools || pools.length === 0) {
|
|
@@ -7450,11 +7940,12 @@ var PoolSelector = class {
|
|
|
7450
7940
|
};
|
|
7451
7941
|
|
|
7452
7942
|
// src/capabilities/pool/pool-stats-manager.ts
|
|
7943
|
+
var SILENT_POOL_STATS_LOGGER = {};
|
|
7453
7944
|
var PoolStatsManager = class {
|
|
7454
7945
|
constructor(options = {}) {
|
|
7455
7946
|
this._stats = /* @__PURE__ */ new Map();
|
|
7456
7947
|
this._buffer = [];
|
|
7457
|
-
this._logger = options.logger ??
|
|
7948
|
+
this._logger = options.logger ?? SILENT_POOL_STATS_LOGGER;
|
|
7458
7949
|
this._batchInterval = setInterval(() => {
|
|
7459
7950
|
this._flush();
|
|
7460
7951
|
}, 100);
|
|
@@ -7702,9 +8193,9 @@ var ConnectionPoolManager = class {
|
|
|
7702
8193
|
this._fallbackConfig = this.fallback;
|
|
7703
8194
|
this._fallback = this.fallback;
|
|
7704
8195
|
this._pools = this.pools;
|
|
7705
|
-
this._selector = new PoolSelector({ strategy: this.strategy });
|
|
7706
|
-
this._healthChecker = new HealthChecker({ poolManager: this });
|
|
7707
|
-
this._stats = new PoolStatsManager({ logger:
|
|
8196
|
+
this._selector = new PoolSelector({ strategy: this.strategy, logger: this.logger ?? void 0 });
|
|
8197
|
+
this._healthChecker = new HealthChecker({ poolManager: this, logger: this.logger ?? void 0 });
|
|
8198
|
+
this._stats = new PoolStatsManager({ logger: this.logger ?? void 0 });
|
|
7708
8199
|
}
|
|
7709
8200
|
/**
|
|
7710
8201
|
* Add a connection pool.
|
|
@@ -9537,7 +10028,7 @@ async function computeTotals(coll, query, limit, totals, defaults = {}, queryCac
|
|
|
9537
10028
|
}
|
|
9538
10029
|
|
|
9539
10030
|
// src/adapters/mongodb/queries/find-page.ts
|
|
9540
|
-
function
|
|
10031
|
+
function normalizePositiveInteger2(value, fallback, field) {
|
|
9541
10032
|
if (value === void 0 || value === null) {
|
|
9542
10033
|
return fallback;
|
|
9543
10034
|
}
|
|
@@ -9585,8 +10076,8 @@ async function executeFindPage(collection, options = {}, defaults = {}, queryCac
|
|
|
9585
10076
|
const metaStartTs = Date.now();
|
|
9586
10077
|
const metaSteps = [];
|
|
9587
10078
|
const ext = options;
|
|
9588
|
-
const page =
|
|
9589
|
-
const rawLimit =
|
|
10079
|
+
const page = normalizePositiveInteger2(options.page, 1, "page");
|
|
10080
|
+
const rawLimit = normalizePositiveInteger2(options.limit, DEFAULT_FIND_PAGE_LIMIT, "limit");
|
|
9590
10081
|
const limit = defaults.findPageMaxLimit !== void 0 && rawLimit > defaults.findPageMaxLimit ? defaults.findPageMaxLimit : rawLimit;
|
|
9591
10082
|
const sort = normalizeSortShape(options.sort);
|
|
9592
10083
|
const baseQuery = options.query ?? {};
|
|
@@ -10460,6 +10951,138 @@ function explainDocuments(collection, query, options, defaults) {
|
|
|
10460
10951
|
return createFindChain(collection, query, options, defaults).explain(options?.explain ?? "queryPlanner");
|
|
10461
10952
|
}
|
|
10462
10953
|
|
|
10954
|
+
// src/adapters/mongodb/queries/vector-search.ts
|
|
10955
|
+
var VECTOR_SEARCH_SCORE_FIELD = "__monsqlize_vector_search_score__";
|
|
10956
|
+
function attachVectorSearchOperationContext(error, namespace) {
|
|
10957
|
+
if (error instanceof Error) {
|
|
10958
|
+
const contextualized = error;
|
|
10959
|
+
const existing = contextualized.monsqlize;
|
|
10960
|
+
try {
|
|
10961
|
+
contextualized.monsqlize = {
|
|
10962
|
+
...existing && typeof existing === "object" ? existing : {},
|
|
10963
|
+
operation: "vectorSearch",
|
|
10964
|
+
namespace
|
|
10965
|
+
};
|
|
10966
|
+
} catch {
|
|
10967
|
+
}
|
|
10968
|
+
return error;
|
|
10969
|
+
}
|
|
10970
|
+
return createError(
|
|
10971
|
+
ErrorCodes.MONGODB_ERROR,
|
|
10972
|
+
"vectorSearch execution failed",
|
|
10973
|
+
[{ operation: "vectorSearch", namespace, reason: String(error) }]
|
|
10974
|
+
);
|
|
10975
|
+
}
|
|
10976
|
+
function assertNonEmptyString(value, field) {
|
|
10977
|
+
if (typeof value !== "string" || value.trim().length === 0) {
|
|
10978
|
+
throw createError(ErrorCodes.INVALID_VECTOR_SEARCH, `${field} must be a non-empty string`, [{ field, reason: "non-empty string required" }]);
|
|
10979
|
+
}
|
|
10980
|
+
}
|
|
10981
|
+
function assertPositiveInteger(value, field) {
|
|
10982
|
+
if (!Number.isInteger(value) || value <= 0) {
|
|
10983
|
+
throw createError(ErrorCodes.INVALID_VECTOR_SEARCH, `${field} must be a positive integer`, [{ field, reason: "positive integer required" }]);
|
|
10984
|
+
}
|
|
10985
|
+
}
|
|
10986
|
+
function isNonArrayObject2(value) {
|
|
10987
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
10988
|
+
}
|
|
10989
|
+
function normalizeVectorProjection(value) {
|
|
10990
|
+
if (value === void 0) return void 0;
|
|
10991
|
+
if (Array.isArray(value)) {
|
|
10992
|
+
if (value.some((field) => typeof field !== "string" || field.length === 0)) {
|
|
10993
|
+
throw createError(ErrorCodes.INVALID_VECTOR_SEARCH, "projection array entries must be non-empty strings", [{ field: "projection", reason: "array entries must be non-empty strings" }]);
|
|
10994
|
+
}
|
|
10995
|
+
return normalizeProjection(value);
|
|
10996
|
+
}
|
|
10997
|
+
if (!isNonArrayObject2(value)) {
|
|
10998
|
+
throw createError(ErrorCodes.INVALID_VECTOR_SEARCH, "projection must be an object or string array", [{ field: "projection", reason: "object or string array required" }]);
|
|
10999
|
+
}
|
|
11000
|
+
return normalizeProjection(value);
|
|
11001
|
+
}
|
|
11002
|
+
function normalizeAggregateOptions(value) {
|
|
11003
|
+
if (value === void 0) return void 0;
|
|
11004
|
+
if (!isNonArrayObject2(value)) {
|
|
11005
|
+
throw createError(ErrorCodes.INVALID_VECTOR_SEARCH, "aggregateOptions must be a non-array object", [{ field: "aggregateOptions", reason: "non-array object required" }]);
|
|
11006
|
+
}
|
|
11007
|
+
for (const key of ["meta", "stream", "explain"]) {
|
|
11008
|
+
if (Object.prototype.hasOwnProperty.call(value, key)) {
|
|
11009
|
+
throw createError(ErrorCodes.INVALID_VECTOR_SEARCH, `aggregateOptions.${key} is not supported by vectorSearch()`, [{ field: `aggregateOptions.${key}`, reason: "reserved aggregate option" }]);
|
|
11010
|
+
}
|
|
11011
|
+
}
|
|
11012
|
+
return { ...value };
|
|
11013
|
+
}
|
|
11014
|
+
function buildVectorSearchPipeline(options) {
|
|
11015
|
+
if (!isNonArrayObject2(options)) {
|
|
11016
|
+
throw createError(ErrorCodes.INVALID_VECTOR_SEARCH, "vectorSearch options must be a non-array object", [{ field: "options", reason: "non-array object required" }]);
|
|
11017
|
+
}
|
|
11018
|
+
assertNonEmptyString(options.index, "index");
|
|
11019
|
+
assertNonEmptyString(options.path, "path");
|
|
11020
|
+
assertPositiveInteger(options.limit, "limit");
|
|
11021
|
+
if (!Array.isArray(options.queryVector) || options.queryVector.length === 0) {
|
|
11022
|
+
throw createError(ErrorCodes.INVALID_VECTOR_SEARCH, "queryVector must be a non-empty number array", [{ field: "queryVector", reason: "non-empty number array required" }]);
|
|
11023
|
+
}
|
|
11024
|
+
if (options.queryVector.some((value) => typeof value !== "number" || !Number.isFinite(value))) {
|
|
11025
|
+
throw createError(ErrorCodes.INVALID_VECTOR_SEARCH, "queryVector values must be finite numbers", [{ field: "queryVector", reason: "finite numbers required" }]);
|
|
11026
|
+
}
|
|
11027
|
+
if (options.exact !== void 0 && typeof options.exact !== "boolean") {
|
|
11028
|
+
throw createError(ErrorCodes.INVALID_VECTOR_SEARCH, "exact must be a boolean", [{ field: "exact", reason: "boolean required" }]);
|
|
11029
|
+
}
|
|
11030
|
+
if (options.filter !== void 0 && !isNonArrayObject2(options.filter)) {
|
|
11031
|
+
throw createError(ErrorCodes.INVALID_VECTOR_SEARCH, "filter must be a non-array object", [{ field: "filter", reason: "non-array object required" }]);
|
|
11032
|
+
}
|
|
11033
|
+
const isExactSearch = options.exact === true;
|
|
11034
|
+
if (isExactSearch) {
|
|
11035
|
+
if (options.numCandidates !== void 0) {
|
|
11036
|
+
throw createError(ErrorCodes.INVALID_VECTOR_SEARCH, "numCandidates is not supported when exact is true", [{ field: "numCandidates", reason: "ANN-only option" }]);
|
|
11037
|
+
}
|
|
11038
|
+
} else {
|
|
11039
|
+
assertPositiveInteger(options.numCandidates, "numCandidates");
|
|
11040
|
+
if (options.numCandidates < options.limit) {
|
|
11041
|
+
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" }]);
|
|
11042
|
+
}
|
|
11043
|
+
}
|
|
11044
|
+
const vectorSearchStage = {
|
|
11045
|
+
index: options.index.trim(),
|
|
11046
|
+
path: options.path.trim(),
|
|
11047
|
+
queryVector: [...options.queryVector],
|
|
11048
|
+
limit: options.limit,
|
|
11049
|
+
...isExactSearch ? { exact: true } : { numCandidates: options.numCandidates },
|
|
11050
|
+
...options.filter === void 0 ? {} : { filter: options.filter }
|
|
11051
|
+
};
|
|
11052
|
+
const projection = normalizeVectorProjection(options.projection);
|
|
11053
|
+
const scoreStage = projection ? {
|
|
11054
|
+
$project: {
|
|
11055
|
+
...projection,
|
|
11056
|
+
[VECTOR_SEARCH_SCORE_FIELD]: { $meta: "vectorSearchScore" }
|
|
11057
|
+
}
|
|
11058
|
+
} : {
|
|
11059
|
+
$set: {
|
|
11060
|
+
[VECTOR_SEARCH_SCORE_FIELD]: { $meta: "vectorSearchScore" }
|
|
11061
|
+
}
|
|
11062
|
+
};
|
|
11063
|
+
return {
|
|
11064
|
+
pipeline: [
|
|
11065
|
+
{ $vectorSearch: vectorSearchStage },
|
|
11066
|
+
scoreStage
|
|
11067
|
+
],
|
|
11068
|
+
aggregateOptions: normalizeAggregateOptions(options.aggregateOptions)
|
|
11069
|
+
};
|
|
11070
|
+
}
|
|
11071
|
+
function mapVectorSearchRows(rows) {
|
|
11072
|
+
return rows.map((row) => {
|
|
11073
|
+
const score = row[VECTOR_SEARCH_SCORE_FIELD];
|
|
11074
|
+
if (typeof score !== "number" || !Number.isFinite(score)) {
|
|
11075
|
+
throw createError(ErrorCodes.INVALID_OPERATION, "Vector Search result did not contain a finite score");
|
|
11076
|
+
}
|
|
11077
|
+
const document = { ...row };
|
|
11078
|
+
delete document[VECTOR_SEARCH_SCORE_FIELD];
|
|
11079
|
+
return {
|
|
11080
|
+
document,
|
|
11081
|
+
score
|
|
11082
|
+
};
|
|
11083
|
+
});
|
|
11084
|
+
}
|
|
11085
|
+
|
|
10463
11086
|
// src/adapters/mongodb/writes/write-utils.ts
|
|
10464
11087
|
function sleep2(ms) {
|
|
10465
11088
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
@@ -11633,6 +12256,19 @@ var MongoCollectionAccessor = class {
|
|
|
11633
12256
|
} : void 0
|
|
11634
12257
|
);
|
|
11635
12258
|
}
|
|
12259
|
+
/** Searches a MongoDB Vector Search index through a constrained aggregate pipeline. */
|
|
12260
|
+
async vectorSearch(options) {
|
|
12261
|
+
const { pipeline, aggregateOptions } = buildVectorSearchPipeline(options);
|
|
12262
|
+
try {
|
|
12263
|
+
const rows = await this.aggregate(
|
|
12264
|
+
pipeline,
|
|
12265
|
+
aggregateOptions
|
|
12266
|
+
);
|
|
12267
|
+
return mapVectorSearchRows(rows);
|
|
12268
|
+
} catch (error) {
|
|
12269
|
+
throw attachVectorSearchOperationContext(error, this.getNamespace());
|
|
12270
|
+
}
|
|
12271
|
+
}
|
|
11636
12272
|
/** Returns an array of distinct values for the given field key. */
|
|
11637
12273
|
async distinct(key, query, options) {
|
|
11638
12274
|
const startTs = Date.now();
|
|
@@ -12134,10 +12770,10 @@ function assertCompatPoolExists(poolManager, poolName) {
|
|
|
12134
12770
|
function createPoolScope(runtime, poolName) {
|
|
12135
12771
|
return {
|
|
12136
12772
|
collection: (name) => runtime.scopedCollection(name, { pool: poolName }),
|
|
12137
|
-
model: (
|
|
12773
|
+
model: (model) => runtime.scopedModel(model, { pool: poolName }),
|
|
12138
12774
|
use: (dbName) => ({
|
|
12139
12775
|
collection: (name) => runtime.scopedCollection(name, { pool: poolName, database: dbName }),
|
|
12140
|
-
model: (
|
|
12776
|
+
model: (model) => runtime.scopedModel(model, { pool: poolName, database: dbName })
|
|
12141
12777
|
})
|
|
12142
12778
|
};
|
|
12143
12779
|
}
|
|
@@ -12167,6 +12803,7 @@ function createCompatModelInstance(config) {
|
|
|
12167
12803
|
config.runtime,
|
|
12168
12804
|
{
|
|
12169
12805
|
collectionName: config.collectionName,
|
|
12806
|
+
modelName: config.modelName,
|
|
12170
12807
|
dbName: config.dbName,
|
|
12171
12808
|
poolName: config.poolName,
|
|
12172
12809
|
definition: config.definition,
|
|
@@ -12204,6 +12841,7 @@ function createRuntimeModelInstance(host, name, scope) {
|
|
|
12204
12841
|
host.runtime,
|
|
12205
12842
|
{
|
|
12206
12843
|
collectionName: actualCollectionName,
|
|
12844
|
+
modelName: registered.collectionName,
|
|
12207
12845
|
dbName: databaseName,
|
|
12208
12846
|
poolName,
|
|
12209
12847
|
definition: registered.definition,
|
|
@@ -16294,7 +16932,7 @@ var MonSQLizeRuntime = class {
|
|
|
16294
16932
|
requireCompatDbInstance(this);
|
|
16295
16933
|
return {
|
|
16296
16934
|
collection: (collectionName) => this.scopedCollection(collectionName, { database: name }),
|
|
16297
|
-
model: (
|
|
16935
|
+
model: (model) => this.scopedModel(model, { database: name })
|
|
16298
16936
|
};
|
|
16299
16937
|
}
|
|
16300
16938
|
pool(poolName) {
|
|
@@ -16326,7 +16964,8 @@ var MonSQLizeRuntime = class {
|
|
|
16326
16964
|
});
|
|
16327
16965
|
}
|
|
16328
16966
|
// Model accessors ---------------------------------------------------------
|
|
16329
|
-
scopedModel(
|
|
16967
|
+
scopedModel(model, options = {}) {
|
|
16968
|
+
const name = typeof model === "string" ? model : model.collectionName;
|
|
16330
16969
|
const dbInstance = requireCompatDbInstance(this);
|
|
16331
16970
|
if (this._client) {
|
|
16332
16971
|
return this.createModelInstance(name, options);
|
|
@@ -16343,13 +16982,15 @@ var MonSQLizeRuntime = class {
|
|
|
16343
16982
|
collection,
|
|
16344
16983
|
runtime: this,
|
|
16345
16984
|
collectionName: actualCollectionName,
|
|
16985
|
+
modelName: registered.collectionName,
|
|
16346
16986
|
dbName: database ?? getRuntimeDatabaseName(this),
|
|
16347
16987
|
poolName: pool,
|
|
16348
16988
|
definition: registered.definition,
|
|
16349
16989
|
schemaEngine: this._schemaDslEngine
|
|
16350
16990
|
});
|
|
16351
16991
|
}
|
|
16352
|
-
model(
|
|
16992
|
+
model(model) {
|
|
16993
|
+
const name = typeof model === "string" ? model : model.collectionName;
|
|
16353
16994
|
if (this._client) {
|
|
16354
16995
|
this.ensureConnected();
|
|
16355
16996
|
return this.createModelInstance(name, {
|
|
@@ -16375,6 +17016,7 @@ var MonSQLizeRuntime = class {
|
|
|
16375
17016
|
collection,
|
|
16376
17017
|
runtime: this,
|
|
16377
17018
|
collectionName: actualCollectionName,
|
|
17019
|
+
modelName: registered.collectionName,
|
|
16378
17020
|
dbName: getRuntimeDatabaseName(this),
|
|
16379
17021
|
definition: registered.definition,
|
|
16380
17022
|
schemaEngine: this._schemaDslEngine
|
|
@@ -18149,6 +18791,7 @@ MonSQLize.PoolSelector = PoolSelector;
|
|
|
18149
18791
|
MonSQLize.PoolStats = PoolStatsManager;
|
|
18150
18792
|
MonSQLize.validatePoolConfig = validatePoolConfig;
|
|
18151
18793
|
MonSQLize.validatePoolConfigSafe = validatePoolConfigSafe;
|
|
18794
|
+
MonSQLize.defineModel = defineModel;
|
|
18152
18795
|
MonSQLize.Model = Model;
|
|
18153
18796
|
MonSQLize.ModelInstance = ModelInstance;
|
|
18154
18797
|
MonSQLize.expr = expr;
|
|
@@ -18238,6 +18881,7 @@ export {
|
|
|
18238
18881
|
dataTasks,
|
|
18239
18882
|
decodeCursor2 as decodeCursor,
|
|
18240
18883
|
index_default as default,
|
|
18884
|
+
defineModel,
|
|
18241
18885
|
encodeCursor2 as encodeCursor,
|
|
18242
18886
|
expr,
|
|
18243
18887
|
generateQueryHash,
|