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/cjs/index.cjs
CHANGED
|
@@ -56,6 +56,14 @@ var ErrorCodes = {
|
|
|
56
56
|
INVALID_EXPRESSION: "INVALID_EXPRESSION",
|
|
57
57
|
INVALID_PAGINATION: "INVALID_PAGINATION",
|
|
58
58
|
INVALID_OPERATION: "INVALID_OPERATION",
|
|
59
|
+
/** Invalid local Vector Search options before the driver is called. */
|
|
60
|
+
INVALID_VECTOR_SEARCH: "INVALID_VECTOR_SEARCH",
|
|
61
|
+
/** Relation select is outside the supported inclusion-only top-level contract. */
|
|
62
|
+
INVALID_RELATION_SELECT: "INVALID_RELATION_SELECT",
|
|
63
|
+
/** A protected delete found declared Model relations that still reference its target. */
|
|
64
|
+
RELATION_IN_USE: "RELATION_IN_USE",
|
|
65
|
+
/** Relation usage coverage is incomplete, so a protected delete must not continue. */
|
|
66
|
+
RELATION_USAGE_UNAVAILABLE: "RELATION_USAGE_UNAVAILABLE",
|
|
59
67
|
CACHE_UNAVAILABLE: "CACHE_UNAVAILABLE",
|
|
60
68
|
MANAGEMENT_OPERATION_FAILED: "MANAGEMENT_OPERATION_FAILED",
|
|
61
69
|
NOT_CONNECTED: "NOT_CONNECTED",
|
|
@@ -501,12 +509,49 @@ function validateRelationConfig(name, config) {
|
|
|
501
509
|
if (config.single !== void 0 && typeof config.single !== "boolean") {
|
|
502
510
|
throw createError(ErrorCodes.INVALID_ARGUMENT, `relations.single must be a boolean`);
|
|
503
511
|
}
|
|
512
|
+
normalizeRelationSelect(config.select, { relation: name });
|
|
513
|
+
}
|
|
514
|
+
function normalizeRelationSelect(value, context = {}) {
|
|
515
|
+
if (value === void 0) {
|
|
516
|
+
return void 0;
|
|
517
|
+
}
|
|
518
|
+
const invalid2 = (reason) => {
|
|
519
|
+
throw createError(
|
|
520
|
+
ErrorCodes.INVALID_RELATION_SELECT,
|
|
521
|
+
`Relation select must use non-empty top-level inclusion fields: ${reason}`,
|
|
522
|
+
[{ ...context, value, reason }]
|
|
523
|
+
);
|
|
524
|
+
};
|
|
525
|
+
const rawTokens = typeof value === "string" ? [value] : Array.isArray(value) ? value : invalid2("select must be a string or string array");
|
|
526
|
+
const tokens = [];
|
|
527
|
+
for (const rawToken of rawTokens) {
|
|
528
|
+
if (typeof rawToken !== "string") {
|
|
529
|
+
invalid2("select array entries must be strings");
|
|
530
|
+
}
|
|
531
|
+
const parts = rawToken.trim().split(/\s+/).filter(Boolean);
|
|
532
|
+
if (parts.length === 0) {
|
|
533
|
+
invalid2("select must not be empty");
|
|
534
|
+
}
|
|
535
|
+
tokens.push(...parts);
|
|
536
|
+
}
|
|
537
|
+
for (const token of tokens) {
|
|
538
|
+
if (token.startsWith("-") || token.startsWith("+")) {
|
|
539
|
+
invalid2("exclusion and prefix tokens are not supported");
|
|
540
|
+
}
|
|
541
|
+
if (token.includes(".")) {
|
|
542
|
+
invalid2("dot-path fields are not supported");
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
return [...new Set(tokens)];
|
|
504
546
|
}
|
|
505
547
|
function normalizePopulateConfig(path5) {
|
|
506
548
|
return typeof path5 === "string" ? { path: path5 } : path5;
|
|
507
549
|
}
|
|
508
550
|
|
|
509
551
|
// src/capabilities/model/model-registry.ts
|
|
552
|
+
function defineModel(collectionName, definition) {
|
|
553
|
+
return { collectionName, definition };
|
|
554
|
+
}
|
|
510
555
|
var Model = class {
|
|
511
556
|
static {
|
|
512
557
|
// collection name → registered model definition
|
|
@@ -521,21 +566,19 @@ var Model = class {
|
|
|
521
566
|
/** Collection names that have been redefined (used by the runtime to trigger hot-reload). */
|
|
522
567
|
this._redefinedNames = /* @__PURE__ */ new Set();
|
|
523
568
|
}
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
* Validates the definition and resolves the timestamps option.
|
|
528
|
-
*/
|
|
529
|
-
static define(collectionName, definition) {
|
|
569
|
+
static define(collectionNameOrDescriptor, definition) {
|
|
570
|
+
const collectionName = typeof collectionNameOrDescriptor === "string" ? collectionNameOrDescriptor : collectionNameOrDescriptor.collectionName;
|
|
571
|
+
const modelDefinition = typeof collectionNameOrDescriptor === "string" ? definition : collectionNameOrDescriptor.definition;
|
|
530
572
|
const normalizedName = validateCollectionName(collectionName);
|
|
531
573
|
if (this.registry.has(normalizedName)) {
|
|
532
574
|
throw createError(ErrorCodes.MODEL_ALREADY_EXISTS, `Model '${normalizedName}' is already defined.`);
|
|
533
575
|
}
|
|
534
|
-
validateDefinition(
|
|
535
|
-
|
|
576
|
+
validateDefinition(modelDefinition);
|
|
577
|
+
const resolvedDefinition = modelDefinition;
|
|
578
|
+
processTimestamps(resolvedDefinition);
|
|
536
579
|
this.registry.set(normalizedName, {
|
|
537
580
|
collectionName: normalizedName,
|
|
538
|
-
definition
|
|
581
|
+
definition: resolvedDefinition
|
|
539
582
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
540
583
|
});
|
|
541
584
|
this.bumpRevision(normalizedName);
|
|
@@ -1077,6 +1120,56 @@ function resolveRegisteredCollectionName(registered, fallback) {
|
|
|
1077
1120
|
const definition = registered.definition;
|
|
1078
1121
|
return definition.collection ?? definition.name ?? registered.collectionName;
|
|
1079
1122
|
}
|
|
1123
|
+
function flattenComparableValues(value) {
|
|
1124
|
+
if (Array.isArray(value)) {
|
|
1125
|
+
return value.flatMap((entry) => flattenComparableValues(entry));
|
|
1126
|
+
}
|
|
1127
|
+
return value === void 0 || value === null ? [] : [value];
|
|
1128
|
+
}
|
|
1129
|
+
function resolveNestedPopulatePaths(value) {
|
|
1130
|
+
if (value === void 0) {
|
|
1131
|
+
return [];
|
|
1132
|
+
}
|
|
1133
|
+
const paths = Array.isArray(value) ? value : [value];
|
|
1134
|
+
for (const path5 of paths) {
|
|
1135
|
+
const valid = typeof path5 === "string" || typeof path5 === "object" && path5 !== null && !Array.isArray(path5) && Boolean(path5.path);
|
|
1136
|
+
if (!valid) {
|
|
1137
|
+
throw createError(ErrorCodes.INVALID_ARGUMENT, "nested populate must be a string, array, or object");
|
|
1138
|
+
}
|
|
1139
|
+
}
|
|
1140
|
+
return paths;
|
|
1141
|
+
}
|
|
1142
|
+
function collectNestedLocalFields(relatedModel, nestedPaths) {
|
|
1143
|
+
if (!relatedModel) {
|
|
1144
|
+
return [];
|
|
1145
|
+
}
|
|
1146
|
+
const relations = relatedModel.getRelations();
|
|
1147
|
+
return unique(
|
|
1148
|
+
nestedPaths.map((path5) => relations[normalizePopulateConfig(path5).path]?.localField).filter((field) => Boolean(field))
|
|
1149
|
+
);
|
|
1150
|
+
}
|
|
1151
|
+
function dedupeDocuments(documents) {
|
|
1152
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1153
|
+
return documents.filter((document) => {
|
|
1154
|
+
if (seen.has(document)) {
|
|
1155
|
+
return false;
|
|
1156
|
+
}
|
|
1157
|
+
seen.add(document);
|
|
1158
|
+
return true;
|
|
1159
|
+
});
|
|
1160
|
+
}
|
|
1161
|
+
function rebuildPopulateMatches(localValue, grouped, sort) {
|
|
1162
|
+
const matches = flattenComparableValues(localValue).flatMap((value) => grouped.get(toKey(value)) ?? []);
|
|
1163
|
+
const deduped = dedupeDocuments(matches);
|
|
1164
|
+
return sort ? applySort(deduped, sort) : deduped;
|
|
1165
|
+
}
|
|
1166
|
+
function projectPopulatedDocument(document, select, nestedPaths) {
|
|
1167
|
+
if (!select) {
|
|
1168
|
+
return document;
|
|
1169
|
+
}
|
|
1170
|
+
const nestedRelationFields = nestedPaths.map((path5) => normalizePopulateConfig(path5).path);
|
|
1171
|
+
return pickFields(document, [.../* @__PURE__ */ new Set([...select, ...nestedRelationFields])]);
|
|
1172
|
+
}
|
|
1080
1173
|
async function populateModelPath(context, docs, path5, traversalState) {
|
|
1081
1174
|
const config = normalizePopulateConfig(path5);
|
|
1082
1175
|
const state = resolvePopulateState(config, traversalState);
|
|
@@ -1095,9 +1188,8 @@ async function populateModelPath(context, docs, path5, traversalState) {
|
|
|
1095
1188
|
if (!relation) {
|
|
1096
1189
|
throw createError(ErrorCodes.INVALID_ARGUMENT, `Undefined relation: ${config.path}`);
|
|
1097
1190
|
}
|
|
1098
|
-
const
|
|
1099
|
-
|
|
1100
|
-
);
|
|
1191
|
+
const localValues = docs.map((doc) => getByPath(doc, relation.localField));
|
|
1192
|
+
const keys = unique(localValues.flatMap((value) => flattenComparableValues(value)));
|
|
1101
1193
|
if (keys.length === 0) {
|
|
1102
1194
|
for (const doc of docs) {
|
|
1103
1195
|
doc[config.path] = relation.single ? null : [];
|
|
@@ -1112,51 +1204,42 @@ async function populateModelPath(context, docs, path5, traversalState) {
|
|
|
1112
1204
|
const relatedCollectionName = resolveRegisteredCollectionName(registered, relation.from);
|
|
1113
1205
|
const relatedCollection = context.runtime.scopedCollection(relatedCollectionName, scope);
|
|
1114
1206
|
const relatedModel = Model.has(relation.from) ? context.runtime.scopedModel(relation.from, scope) : null;
|
|
1207
|
+
const nestedPaths = resolveNestedPopulatePaths(config.populate);
|
|
1208
|
+
const effectiveSelect = normalizeRelationSelect(config.select, { path: config.path }) ?? normalizeRelationSelect(relation.select, { relation: config.path });
|
|
1209
|
+
const projectionFields = effectiveSelect ? [.../* @__PURE__ */ new Set([
|
|
1210
|
+
...effectiveSelect,
|
|
1211
|
+
relation.foreignField,
|
|
1212
|
+
"_id",
|
|
1213
|
+
...collectNestedLocalFields(relatedModel, nestedPaths)
|
|
1214
|
+
])] : [];
|
|
1115
1215
|
const relatedDocs = await relatedCollection.find({
|
|
1116
1216
|
[relation.foreignField]: { $in: keys },
|
|
1117
1217
|
...config.match ?? {}
|
|
1118
|
-
}
|
|
1218
|
+
}, effectiveSelect ? {
|
|
1219
|
+
projection: Object.fromEntries(projectionFields.map((field) => [field, 1]))
|
|
1220
|
+
} : void 0);
|
|
1119
1221
|
let hydrated = relatedModel ? relatedModel.hydrateDocuments(relatedDocs) : relatedDocs.map((item) => ({ ...item }));
|
|
1120
|
-
if (
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
if (config.select) {
|
|
1124
|
-
const select = config.select;
|
|
1125
|
-
hydrated = hydrated.map((item) => pickFields(item, select, [relation.foreignField]));
|
|
1126
|
-
}
|
|
1127
|
-
if (config.populate) {
|
|
1128
|
-
const nestedRaw = config.populate;
|
|
1129
|
-
const isValidNestedConfig = (value) => {
|
|
1130
|
-
if (typeof value === "string" || Array.isArray(value)) return true;
|
|
1131
|
-
if (typeof value === "object" && value !== null && value.path) return true;
|
|
1132
|
-
return false;
|
|
1133
|
-
};
|
|
1134
|
-
if (!isValidNestedConfig(nestedRaw)) {
|
|
1135
|
-
throw createError(ErrorCodes.INVALID_ARGUMENT, "nested populate must be a string, array, or object");
|
|
1136
|
-
}
|
|
1137
|
-
if (relatedModel) {
|
|
1138
|
-
if (state.depth + 1 > state.maxDepth) {
|
|
1139
|
-
throw createError(ErrorCodes.INVALID_ARGUMENT, `populate maxDepth exceeded: ${state.maxDepth}`);
|
|
1140
|
-
}
|
|
1141
|
-
const nestedPaths = Array.isArray(config.populate) ? config.populate : [config.populate];
|
|
1142
|
-
hydrated = await relatedModel.populateDocuments(hydrated, nestedPaths, {
|
|
1143
|
-
depth: state.depth + 1,
|
|
1144
|
-
maxDepth: state.maxDepth,
|
|
1145
|
-
activeConfigs: state.activeConfigs
|
|
1146
|
-
});
|
|
1222
|
+
if (nestedPaths.length > 0 && relatedModel) {
|
|
1223
|
+
if (state.depth + 1 > state.maxDepth) {
|
|
1224
|
+
throw createError(ErrorCodes.INVALID_ARGUMENT, `populate maxDepth exceeded: ${state.maxDepth}`);
|
|
1147
1225
|
}
|
|
1226
|
+
hydrated = await relatedModel.populateDocuments(hydrated, nestedPaths, {
|
|
1227
|
+
depth: state.depth + 1,
|
|
1228
|
+
maxDepth: state.maxDepth,
|
|
1229
|
+
activeConfigs: state.activeConfigs
|
|
1230
|
+
});
|
|
1148
1231
|
}
|
|
1149
1232
|
const grouped = groupBy(hydrated, (item) => getByPath(item, relation.foreignField));
|
|
1150
|
-
for (const doc of docs) {
|
|
1151
|
-
|
|
1152
|
-
let matches = grouped.get(toKey(localValue)) ?? [];
|
|
1233
|
+
for (const [index, doc] of docs.entries()) {
|
|
1234
|
+
let matches = rebuildPopulateMatches(localValues[index], grouped, config.sort);
|
|
1153
1235
|
if (config.skip) {
|
|
1154
1236
|
matches = matches.slice(config.skip);
|
|
1155
1237
|
}
|
|
1156
1238
|
if (config.limit !== void 0) {
|
|
1157
1239
|
matches = matches.slice(0, config.limit);
|
|
1158
1240
|
}
|
|
1159
|
-
|
|
1241
|
+
const projectedMatches = matches.map((item) => projectPopulatedDocument(item, effectiveSelect, nestedPaths));
|
|
1242
|
+
doc[config.path] = relation.single ? projectedMatches[0] ?? null : projectedMatches;
|
|
1160
1243
|
}
|
|
1161
1244
|
return docs;
|
|
1162
1245
|
} finally {
|
|
@@ -5574,7 +5657,382 @@ async function orchestrateModelDeleteMany(context, filter, options) {
|
|
|
5574
5657
|
return result;
|
|
5575
5658
|
}
|
|
5576
5659
|
|
|
5660
|
+
// src/capabilities/model/relation-usage.ts
|
|
5661
|
+
var DEFAULT_MAX_TARGETS = 100;
|
|
5662
|
+
var DEFAULT_MAX_SAMPLES = 20;
|
|
5663
|
+
function isNonArrayObject(value) {
|
|
5664
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
5665
|
+
}
|
|
5666
|
+
function normalizePositiveInteger(value, field, fallback) {
|
|
5667
|
+
if (value === void 0) {
|
|
5668
|
+
return fallback;
|
|
5669
|
+
}
|
|
5670
|
+
if (!Number.isInteger(value) || value <= 0) {
|
|
5671
|
+
throw createError(ErrorCodes.INVALID_ARGUMENT, `${field} must be a positive integer`, [{ field, value }]);
|
|
5672
|
+
}
|
|
5673
|
+
return value;
|
|
5674
|
+
}
|
|
5675
|
+
function normalizeRelationNames(value, field) {
|
|
5676
|
+
if (value === void 0) {
|
|
5677
|
+
return void 0;
|
|
5678
|
+
}
|
|
5679
|
+
if (!Array.isArray(value) || value.some((entry) => typeof entry !== "string" || entry.trim().length === 0)) {
|
|
5680
|
+
throw createError(ErrorCodes.INVALID_ARGUMENT, `${field} must be an array of non-empty strings`, [{ field, value }]);
|
|
5681
|
+
}
|
|
5682
|
+
return [...new Set(value.map((entry) => entry.trim()))];
|
|
5683
|
+
}
|
|
5684
|
+
function normalizeRelationUsageOptions(options) {
|
|
5685
|
+
if (options !== void 0 && !isNonArrayObject(options)) {
|
|
5686
|
+
throw createError(ErrorCodes.INVALID_ARGUMENT, "relation usage options must be a non-array object", [{ field: "options", value: options }]);
|
|
5687
|
+
}
|
|
5688
|
+
const value = options ?? {};
|
|
5689
|
+
if (value.includeSoftDeletedReferences !== void 0 && typeof value.includeSoftDeletedReferences !== "boolean") {
|
|
5690
|
+
throw createError(
|
|
5691
|
+
ErrorCodes.INVALID_ARGUMENT,
|
|
5692
|
+
"includeSoftDeletedReferences must be a boolean",
|
|
5693
|
+
[{ field: "includeSoftDeletedReferences", value: value.includeSoftDeletedReferences }]
|
|
5694
|
+
);
|
|
5695
|
+
}
|
|
5696
|
+
return {
|
|
5697
|
+
session: value.session,
|
|
5698
|
+
maxTargets: normalizePositiveInteger(value.maxTargets, "maxTargets", DEFAULT_MAX_TARGETS),
|
|
5699
|
+
maxSamples: normalizePositiveInteger(value.maxSamples, "maxSamples", DEFAULT_MAX_SAMPLES),
|
|
5700
|
+
includeRelations: normalizeRelationNames(value.includeRelations, "includeRelations"),
|
|
5701
|
+
excludeRelations: normalizeRelationNames(value.excludeRelations, "excludeRelations"),
|
|
5702
|
+
includeSoftDeletedReferences: value.includeSoftDeletedReferences !== false
|
|
5703
|
+
};
|
|
5704
|
+
}
|
|
5705
|
+
function resolveRegisteredCollectionName2(registered) {
|
|
5706
|
+
const definition = registered.definition;
|
|
5707
|
+
return definition.collection ?? definition.name ?? registered.collectionName;
|
|
5708
|
+
}
|
|
5709
|
+
function flattenComparableValues2(value) {
|
|
5710
|
+
if (Array.isArray(value)) {
|
|
5711
|
+
return value.flatMap((entry) => flattenComparableValues2(entry));
|
|
5712
|
+
}
|
|
5713
|
+
return value === void 0 || value === null ? [] : [value];
|
|
5714
|
+
}
|
|
5715
|
+
function isRelationSelected(options, sourceModel, relation) {
|
|
5716
|
+
const qualified = `${sourceModel}.${relation}`;
|
|
5717
|
+
const matches = (names) => Boolean(names?.includes(qualified) || names?.includes(relation));
|
|
5718
|
+
if (options.includeRelations && !matches(options.includeRelations)) {
|
|
5719
|
+
return false;
|
|
5720
|
+
}
|
|
5721
|
+
return !matches(options.excludeRelations);
|
|
5722
|
+
}
|
|
5723
|
+
function describeError(error) {
|
|
5724
|
+
return error instanceof Error ? error.message : String(error);
|
|
5725
|
+
}
|
|
5726
|
+
function findDeclaredInboundRelations(targetModelName) {
|
|
5727
|
+
const relations = [];
|
|
5728
|
+
for (const sourceModel of Model.list()) {
|
|
5729
|
+
const registered = Model.get(sourceModel);
|
|
5730
|
+
if (!registered) {
|
|
5731
|
+
continue;
|
|
5732
|
+
}
|
|
5733
|
+
for (const [relation, config] of Object.entries(registered.definition.relations ?? {})) {
|
|
5734
|
+
if (config.from !== targetModelName) {
|
|
5735
|
+
continue;
|
|
5736
|
+
}
|
|
5737
|
+
relations.push({
|
|
5738
|
+
sourceModel,
|
|
5739
|
+
sourceCollection: resolveRegisteredCollectionName2(registered),
|
|
5740
|
+
relation,
|
|
5741
|
+
definition: registered.definition,
|
|
5742
|
+
localField: config.localField,
|
|
5743
|
+
foreignField: config.foreignField
|
|
5744
|
+
});
|
|
5745
|
+
}
|
|
5746
|
+
}
|
|
5747
|
+
return relations;
|
|
5748
|
+
}
|
|
5749
|
+
function buildRelationUsageTargetProjection(targetModelName) {
|
|
5750
|
+
const fields = /* @__PURE__ */ new Set(["_id"]);
|
|
5751
|
+
for (const relation of findDeclaredInboundRelations(targetModelName)) {
|
|
5752
|
+
fields.add(relation.foreignField);
|
|
5753
|
+
}
|
|
5754
|
+
return Object.fromEntries([...fields].map((field) => [field, 1]));
|
|
5755
|
+
}
|
|
5756
|
+
function createUnavailableRelationUsageReport(context, reason) {
|
|
5757
|
+
return {
|
|
5758
|
+
used: false,
|
|
5759
|
+
target: {
|
|
5760
|
+
matchedCount: 0,
|
|
5761
|
+
truncated: false,
|
|
5762
|
+
sampleIds: []
|
|
5763
|
+
},
|
|
5764
|
+
checkedRelations: 0,
|
|
5765
|
+
usages: [],
|
|
5766
|
+
coverage: {
|
|
5767
|
+
mode: "registered-declared",
|
|
5768
|
+
complete: false,
|
|
5769
|
+
registryModels: Model.list(),
|
|
5770
|
+
checkedModels: [],
|
|
5771
|
+
skipped: [{ sourceModel: context.modelName, reason }]
|
|
5772
|
+
}
|
|
5773
|
+
};
|
|
5774
|
+
}
|
|
5775
|
+
async function inspectDeclaredRelationUsage(context, targets, inputOptions, targetTruncated = false) {
|
|
5776
|
+
const options = normalizeRelationUsageOptions(inputOptions);
|
|
5777
|
+
const registryModels = Model.list();
|
|
5778
|
+
const checkedModels = /* @__PURE__ */ new Set();
|
|
5779
|
+
const skipped = [];
|
|
5780
|
+
const usages = [];
|
|
5781
|
+
let checkedRelations = 0;
|
|
5782
|
+
for (const inbound of findDeclaredInboundRelations(context.modelName)) {
|
|
5783
|
+
if (!isRelationSelected(options, inbound.sourceModel, inbound.relation)) {
|
|
5784
|
+
skipped.push({
|
|
5785
|
+
sourceModel: inbound.sourceModel,
|
|
5786
|
+
reason: `relation '${inbound.sourceModel}.${inbound.relation}' was excluded by relation selection`
|
|
5787
|
+
});
|
|
5788
|
+
continue;
|
|
5789
|
+
}
|
|
5790
|
+
checkedRelations += 1;
|
|
5791
|
+
const targetValues = unique(
|
|
5792
|
+
targets.flatMap((target) => flattenComparableValues2(getByPath(target, inbound.foreignField)))
|
|
5793
|
+
);
|
|
5794
|
+
if (targetValues.length === 0) {
|
|
5795
|
+
checkedModels.add(inbound.sourceModel);
|
|
5796
|
+
continue;
|
|
5797
|
+
}
|
|
5798
|
+
const scope = {
|
|
5799
|
+
database: inbound.definition.connection?.database ?? context.dbName,
|
|
5800
|
+
pool: inbound.definition.connection?.pool ?? context.poolName
|
|
5801
|
+
};
|
|
5802
|
+
const baseQuery = {
|
|
5803
|
+
[inbound.localField]: { $in: targetValues }
|
|
5804
|
+
};
|
|
5805
|
+
const sourceSoftDelete = resolveModelSoftDeleteConfig(inbound.definition);
|
|
5806
|
+
const query = !options.includeSoftDeletedReferences && sourceSoftDelete?.enabled ? { $and: [baseQuery, { [sourceSoftDelete.field]: null }] } : baseQuery;
|
|
5807
|
+
try {
|
|
5808
|
+
const collection = context.runtime.scopedCollection(inbound.sourceCollection, scope);
|
|
5809
|
+
if (!collection) {
|
|
5810
|
+
throw new Error("scopedCollection returned no collection");
|
|
5811
|
+
}
|
|
5812
|
+
checkedModels.add(inbound.sourceModel);
|
|
5813
|
+
const count = await collection.count(query, { session: options.session });
|
|
5814
|
+
if (typeof count !== "number" || !Number.isFinite(count) || count < 0) {
|
|
5815
|
+
throw new Error("relation usage count did not return a non-negative number");
|
|
5816
|
+
}
|
|
5817
|
+
if (count === 0) {
|
|
5818
|
+
continue;
|
|
5819
|
+
}
|
|
5820
|
+
const samples = await collection.find(query, {
|
|
5821
|
+
session: options.session,
|
|
5822
|
+
projection: { _id: 1 },
|
|
5823
|
+
limit: options.maxSamples
|
|
5824
|
+
});
|
|
5825
|
+
usages.push({
|
|
5826
|
+
sourceModel: inbound.sourceModel,
|
|
5827
|
+
sourceCollection: inbound.sourceCollection,
|
|
5828
|
+
relation: inbound.relation,
|
|
5829
|
+
localField: inbound.localField,
|
|
5830
|
+
foreignField: inbound.foreignField,
|
|
5831
|
+
count,
|
|
5832
|
+
sampleIds: samples.filter((sample) => isNonArrayObject(sample)).map((sample) => sample._id).filter((id) => id !== void 0).slice(0, options.maxSamples)
|
|
5833
|
+
});
|
|
5834
|
+
} catch (error) {
|
|
5835
|
+
skipped.push({
|
|
5836
|
+
sourceModel: inbound.sourceModel,
|
|
5837
|
+
reason: describeError(error)
|
|
5838
|
+
});
|
|
5839
|
+
}
|
|
5840
|
+
}
|
|
5841
|
+
const targetSampleIds = targets.map((target) => target._id).filter((id) => id !== void 0).slice(0, options.maxSamples);
|
|
5842
|
+
return {
|
|
5843
|
+
used: usages.length > 0,
|
|
5844
|
+
target: {
|
|
5845
|
+
matchedCount: targets.length,
|
|
5846
|
+
truncated: targetTruncated,
|
|
5847
|
+
sampleIds: targetSampleIds
|
|
5848
|
+
},
|
|
5849
|
+
checkedRelations,
|
|
5850
|
+
usages,
|
|
5851
|
+
coverage: {
|
|
5852
|
+
mode: "registered-declared",
|
|
5853
|
+
complete: !targetTruncated && skipped.length === 0,
|
|
5854
|
+
registryModels,
|
|
5855
|
+
checkedModels: [...checkedModels],
|
|
5856
|
+
skipped
|
|
5857
|
+
}
|
|
5858
|
+
};
|
|
5859
|
+
}
|
|
5860
|
+
function stripRelationUsageOptions(options) {
|
|
5861
|
+
if (!isNonArrayObject(options)) {
|
|
5862
|
+
return options;
|
|
5863
|
+
}
|
|
5864
|
+
const {
|
|
5865
|
+
maxTargets: _maxTargets,
|
|
5866
|
+
maxSamples: _maxSamples,
|
|
5867
|
+
includeRelations: _includeRelations,
|
|
5868
|
+
excludeRelations: _excludeRelations,
|
|
5869
|
+
includeSoftDeletedReferences: _includeSoftDeletedReferences,
|
|
5870
|
+
...deleteOptions
|
|
5871
|
+
} = options;
|
|
5872
|
+
void _maxTargets;
|
|
5873
|
+
void _maxSamples;
|
|
5874
|
+
void _includeRelations;
|
|
5875
|
+
void _excludeRelations;
|
|
5876
|
+
void _includeSoftDeletedReferences;
|
|
5877
|
+
return deleteOptions;
|
|
5878
|
+
}
|
|
5879
|
+
|
|
5880
|
+
// src/capabilities/model/model-p0-operations.ts
|
|
5881
|
+
function relationUsageContext(host) {
|
|
5882
|
+
return {
|
|
5883
|
+
modelName: host.modelName,
|
|
5884
|
+
runtime: host.runtime,
|
|
5885
|
+
dbName: host.dbName,
|
|
5886
|
+
poolName: host.poolName
|
|
5887
|
+
};
|
|
5888
|
+
}
|
|
5889
|
+
function isNonEmptyFilter(filter) {
|
|
5890
|
+
return typeof filter === "object" && filter !== null && !Array.isArray(filter) && Object.keys(filter).length > 0;
|
|
5891
|
+
}
|
|
5892
|
+
function relationTargetReadOptions(options, projection) {
|
|
5893
|
+
const source = options ?? {};
|
|
5894
|
+
const result = { projection };
|
|
5895
|
+
for (const key of ["session", "collation", "hint", "comment", "let", "maxTimeMS", "readConcern", "readPreference"]) {
|
|
5896
|
+
if (source[key] !== void 0) {
|
|
5897
|
+
result[key] = source[key];
|
|
5898
|
+
}
|
|
5899
|
+
}
|
|
5900
|
+
return result;
|
|
5901
|
+
}
|
|
5902
|
+
function hydrateVectorSearchHits(host, hits) {
|
|
5903
|
+
return hits.map((hit) => {
|
|
5904
|
+
const document = host.hydrateDocument(hit.document);
|
|
5905
|
+
if (!document) {
|
|
5906
|
+
throw createError(ErrorCodes.INVALID_OPERATION, "Vector Search returned a non-document result");
|
|
5907
|
+
}
|
|
5908
|
+
return { document, score: hit.score };
|
|
5909
|
+
});
|
|
5910
|
+
}
|
|
5911
|
+
async function executeModelVectorSearch(host, options) {
|
|
5912
|
+
if (typeof options !== "object" || options === null || Array.isArray(options)) {
|
|
5913
|
+
const hits2 = await host.collection.vectorSearch(options);
|
|
5914
|
+
return hydrateVectorSearchHits(host, hits2);
|
|
5915
|
+
}
|
|
5916
|
+
const { withDeleted, onlyDeleted, ...collectionOptions } = options;
|
|
5917
|
+
if (withDeleted !== void 0 && typeof withDeleted !== "boolean") {
|
|
5918
|
+
throw createError(
|
|
5919
|
+
ErrorCodes.INVALID_VECTOR_SEARCH,
|
|
5920
|
+
"withDeleted must be a boolean",
|
|
5921
|
+
[{ field: "withDeleted", reason: "boolean required" }]
|
|
5922
|
+
);
|
|
5923
|
+
}
|
|
5924
|
+
if (onlyDeleted !== void 0 && typeof onlyDeleted !== "boolean") {
|
|
5925
|
+
throw createError(
|
|
5926
|
+
ErrorCodes.INVALID_VECTOR_SEARCH,
|
|
5927
|
+
"onlyDeleted must be a boolean",
|
|
5928
|
+
[{ field: "onlyDeleted", reason: "boolean required" }]
|
|
5929
|
+
);
|
|
5930
|
+
}
|
|
5931
|
+
const filter = host.softDeleteConfig?.enabled && !withDeleted ? applyModelSoftDeleteFilter(
|
|
5932
|
+
collectionOptions.filter,
|
|
5933
|
+
{ withDeleted, onlyDeleted },
|
|
5934
|
+
host.softDeleteConfig
|
|
5935
|
+
) : collectionOptions.filter;
|
|
5936
|
+
const hits = await host.collection.vectorSearch({
|
|
5937
|
+
...collectionOptions,
|
|
5938
|
+
...filter === void 0 ? {} : { filter }
|
|
5939
|
+
});
|
|
5940
|
+
return hydrateVectorSearchHits(host, hits);
|
|
5941
|
+
}
|
|
5942
|
+
async function executeRelationUsageCheck(host, filter, options) {
|
|
5943
|
+
const normalizedOptions = normalizeRelationUsageOptions(options);
|
|
5944
|
+
const context = relationUsageContext(host);
|
|
5945
|
+
try {
|
|
5946
|
+
const rawTargets = await host.collection.find(
|
|
5947
|
+
applyModelSoftDeleteFilter(filter, void 0, host.softDeleteConfig),
|
|
5948
|
+
{
|
|
5949
|
+
session: normalizedOptions.session,
|
|
5950
|
+
projection: buildRelationUsageTargetProjection(host.modelName),
|
|
5951
|
+
limit: normalizedOptions.maxTargets + 1
|
|
5952
|
+
}
|
|
5953
|
+
);
|
|
5954
|
+
const targets = rawTargets.filter(
|
|
5955
|
+
(target) => typeof target === "object" && target !== null && !Array.isArray(target)
|
|
5956
|
+
);
|
|
5957
|
+
return inspectDeclaredRelationUsage(
|
|
5958
|
+
context,
|
|
5959
|
+
targets.slice(0, normalizedOptions.maxTargets),
|
|
5960
|
+
normalizedOptions,
|
|
5961
|
+
targets.length > normalizedOptions.maxTargets
|
|
5962
|
+
);
|
|
5963
|
+
} catch (error) {
|
|
5964
|
+
return createUnavailableRelationUsageReport(context, error instanceof Error ? error.message : String(error));
|
|
5965
|
+
}
|
|
5966
|
+
}
|
|
5967
|
+
async function executeProtectedRelationDelete(host, filter, options, force) {
|
|
5968
|
+
if (!isNonEmptyFilter(filter)) {
|
|
5969
|
+
throw createError(
|
|
5970
|
+
ErrorCodes.INVALID_ARGUMENT,
|
|
5971
|
+
"protected relation deletion requires a non-empty filter",
|
|
5972
|
+
[{ field: "filter", reason: "non-empty object required" }]
|
|
5973
|
+
);
|
|
5974
|
+
}
|
|
5975
|
+
const rawOptions = options ?? {};
|
|
5976
|
+
if (rawOptions.includeRelations !== void 0 || rawOptions.excludeRelations !== void 0) {
|
|
5977
|
+
throw createError(
|
|
5978
|
+
ErrorCodes.INVALID_ARGUMENT,
|
|
5979
|
+
"protected relation deletion cannot narrow the declared relation scan",
|
|
5980
|
+
[{ field: "includeRelations/excludeRelations", reason: "protected deletion always scans all declared relations" }]
|
|
5981
|
+
);
|
|
5982
|
+
}
|
|
5983
|
+
const usageOptions = normalizeRelationUsageOptions(options);
|
|
5984
|
+
const context = relationUsageContext(host);
|
|
5985
|
+
const deleteOptions = stripRelationUsageOptions(options);
|
|
5986
|
+
let target;
|
|
5987
|
+
try {
|
|
5988
|
+
const targetFilter = force || !host.softDeleteConfig?.enabled ? filter : { ...filter, [host.softDeleteConfig.field]: null };
|
|
5989
|
+
const rawTarget = await host.collection.findOne(
|
|
5990
|
+
targetFilter,
|
|
5991
|
+
relationTargetReadOptions(options, buildRelationUsageTargetProjection(host.modelName))
|
|
5992
|
+
);
|
|
5993
|
+
target = typeof rawTarget === "object" && rawTarget !== null && !Array.isArray(rawTarget) ? rawTarget : null;
|
|
5994
|
+
} catch (error) {
|
|
5995
|
+
const usage2 = createUnavailableRelationUsageReport(
|
|
5996
|
+
context,
|
|
5997
|
+
error instanceof Error ? error.message : String(error)
|
|
5998
|
+
);
|
|
5999
|
+
throw createError(
|
|
6000
|
+
ErrorCodes.RELATION_USAGE_UNAVAILABLE,
|
|
6001
|
+
"Unable to resolve a target for protected relation deletion",
|
|
6002
|
+
[{ coverage: usage2.coverage, reason: usage2.coverage.skipped[0]?.reason, skipped: usage2.coverage.skipped }]
|
|
6003
|
+
);
|
|
6004
|
+
}
|
|
6005
|
+
const usage = await inspectDeclaredRelationUsage(context, target ? [target] : [], usageOptions);
|
|
6006
|
+
if (!usage.coverage.complete) {
|
|
6007
|
+
throw createError(
|
|
6008
|
+
ErrorCodes.RELATION_USAGE_UNAVAILABLE,
|
|
6009
|
+
"Declared relation usage coverage is incomplete; protected deletion was not performed",
|
|
6010
|
+
[{ coverage: usage.coverage, reason: "incomplete relation usage coverage", skipped: usage.coverage.skipped }]
|
|
6011
|
+
);
|
|
6012
|
+
}
|
|
6013
|
+
if (usage.used) {
|
|
6014
|
+
throw createError(
|
|
6015
|
+
ErrorCodes.RELATION_IN_USE,
|
|
6016
|
+
"Protected deletion was blocked because declared relations still reference the target",
|
|
6017
|
+
[{ used: usage.used, usages: usage.usages, coverage: usage.coverage }]
|
|
6018
|
+
);
|
|
6019
|
+
}
|
|
6020
|
+
if (target && target._id === void 0) {
|
|
6021
|
+
throw createError(
|
|
6022
|
+
ErrorCodes.RELATION_USAGE_UNAVAILABLE,
|
|
6023
|
+
"Protected deletion target did not include an _id",
|
|
6024
|
+
[{ coverage: usage.coverage, reason: "target _id missing", skipped: usage.coverage.skipped }]
|
|
6025
|
+
);
|
|
6026
|
+
}
|
|
6027
|
+
const deleteFilter = target ? { _id: target._id } : { _id: { $in: [] } };
|
|
6028
|
+
const result = force ? await host.forceDelete(deleteFilter, deleteOptions) : await host.deleteOne(deleteFilter, deleteOptions);
|
|
6029
|
+
return { result, usage };
|
|
6030
|
+
}
|
|
6031
|
+
|
|
5577
6032
|
// src/capabilities/model/index.ts
|
|
6033
|
+
function resolveModelRegistryName(definition, fallback) {
|
|
6034
|
+
return Model.list().find((name) => Model.get(name)?.definition === definition) ?? fallback;
|
|
6035
|
+
}
|
|
5578
6036
|
var ModelInstance = class {
|
|
5579
6037
|
constructor(collection, runtime, options) {
|
|
5580
6038
|
this.collection = collection;
|
|
@@ -5595,6 +6053,7 @@ var ModelInstance = class {
|
|
|
5595
6053
|
this.dbName = options.dbName;
|
|
5596
6054
|
this.poolName = options.poolName;
|
|
5597
6055
|
this.definition = options.definition;
|
|
6056
|
+
this.modelName = options.modelName ?? resolveModelRegistryName(options.definition, options.collectionName);
|
|
5598
6057
|
this.relations = new Map(Object.entries(options.definition.relations ?? {}));
|
|
5599
6058
|
for (const [name, config] of this.relations) {
|
|
5600
6059
|
validateRelationConfig(name, config);
|
|
@@ -5778,6 +6237,9 @@ var ModelInstance = class {
|
|
|
5778
6237
|
};
|
|
5779
6238
|
});
|
|
5780
6239
|
}
|
|
6240
|
+
async vectorSearch(options) {
|
|
6241
|
+
return executeModelVectorSearch(this.p0OperationsContext(), options);
|
|
6242
|
+
}
|
|
5781
6243
|
count(query, options) {
|
|
5782
6244
|
const filteredQuery = applyModelSoftDeleteFilter(query, options, this._softDeleteConfig);
|
|
5783
6245
|
return this.runV1HookedOperation("find", [filteredQuery, options], (nextQuery, nextOptions) => this.collection.count(nextQuery, nextOptions));
|
|
@@ -5827,6 +6289,19 @@ var ModelInstance = class {
|
|
|
5827
6289
|
async deleteMany(filter, options) {
|
|
5828
6290
|
return this.runModelWrite(() => orchestrateModelDeleteMany(this.mutationContext(), filter, options));
|
|
5829
6291
|
}
|
|
6292
|
+
/**
|
|
6293
|
+
* Inspects only registered and declared inbound relations. It is read-only;
|
|
6294
|
+
* callers must check coverage before treating an empty result as safe.
|
|
6295
|
+
*/
|
|
6296
|
+
async checkRelationUsage(filter, options) {
|
|
6297
|
+
return executeRelationUsageCheck(this.p0OperationsContext(), filter, options);
|
|
6298
|
+
}
|
|
6299
|
+
async deleteOneWithRelations(filter, options) {
|
|
6300
|
+
return executeProtectedRelationDelete(this.p0OperationsContext(), filter, options, false);
|
|
6301
|
+
}
|
|
6302
|
+
async forceDeleteWithRelations(filter, options) {
|
|
6303
|
+
return executeProtectedRelationDelete(this.p0OperationsContext(), filter, options, true);
|
|
6304
|
+
}
|
|
5830
6305
|
// ── soft-delete extended methods (only meaningful when softDelete is enabled) ──
|
|
5831
6306
|
findWithDeleted(query, options) {
|
|
5832
6307
|
return findWithDeletedDocuments(this.softDeleteContext(), query, options);
|
|
@@ -5995,6 +6470,19 @@ var ModelInstance = class {
|
|
|
5995
6470
|
populateDocument: (document, paths) => this.populateSingle(document, paths)
|
|
5996
6471
|
}, doc);
|
|
5997
6472
|
}
|
|
6473
|
+
p0OperationsContext() {
|
|
6474
|
+
return {
|
|
6475
|
+
collection: this.collection,
|
|
6476
|
+
softDeleteConfig: this._softDeleteConfig,
|
|
6477
|
+
modelName: this.modelName,
|
|
6478
|
+
runtime: this.runtime,
|
|
6479
|
+
dbName: this.dbName,
|
|
6480
|
+
poolName: this.poolName,
|
|
6481
|
+
hydrateDocument: (doc) => this.hydrateDocument(doc),
|
|
6482
|
+
deleteOne: (filter, options) => this.deleteOne(filter, options),
|
|
6483
|
+
forceDelete: (filter, options) => this.forceDelete(filter, options)
|
|
6484
|
+
};
|
|
6485
|
+
}
|
|
5998
6486
|
softDeleteContext() {
|
|
5999
6487
|
return {
|
|
6000
6488
|
collection: this.collection,
|
|
@@ -7190,6 +7678,7 @@ var CountQueue = class {
|
|
|
7190
7678
|
};
|
|
7191
7679
|
|
|
7192
7680
|
// src/capabilities/pool/pool-health-checker.ts
|
|
7681
|
+
var SILENT_HEALTH_CHECKER_LOGGER = {};
|
|
7193
7682
|
var HealthChecker = class {
|
|
7194
7683
|
constructor(options = {}) {
|
|
7195
7684
|
this._healthStatus = /* @__PURE__ */ new Map();
|
|
@@ -7199,7 +7688,7 @@ var HealthChecker = class {
|
|
|
7199
7688
|
this._inProgress = /* @__PURE__ */ new Set();
|
|
7200
7689
|
this._started = false;
|
|
7201
7690
|
this._poolManager = options.poolManager ?? null;
|
|
7202
|
-
this._logger = options.logger ??
|
|
7691
|
+
this._logger = options.logger ?? SILENT_HEALTH_CHECKER_LOGGER;
|
|
7203
7692
|
}
|
|
7204
7693
|
register(poolNameOrConfig, configOrClient) {
|
|
7205
7694
|
let poolName;
|
|
@@ -7323,6 +7812,7 @@ var HealthChecker = class {
|
|
|
7323
7812
|
};
|
|
7324
7813
|
|
|
7325
7814
|
// src/capabilities/pool/pool-selector.ts
|
|
7815
|
+
var SILENT_POOL_SELECTOR_LOGGER = {};
|
|
7326
7816
|
var POOL_STRATEGY_ALIASES = {
|
|
7327
7817
|
"round-robin": "roundRobin",
|
|
7328
7818
|
"least-connections": "leastConnections",
|
|
@@ -7336,7 +7826,7 @@ var PoolSelector = class {
|
|
|
7336
7826
|
constructor(options = {}) {
|
|
7337
7827
|
this._roundRobinIndex = /* @__PURE__ */ new Map();
|
|
7338
7828
|
this._strategy = normalizePoolStrategy(options.strategy);
|
|
7339
|
-
this._logger = options.logger ??
|
|
7829
|
+
this._logger = options.logger ?? SILENT_POOL_SELECTOR_LOGGER;
|
|
7340
7830
|
}
|
|
7341
7831
|
select(pools, context) {
|
|
7342
7832
|
if (!pools || pools.length === 0) {
|
|
@@ -7467,11 +7957,12 @@ var PoolSelector = class {
|
|
|
7467
7957
|
};
|
|
7468
7958
|
|
|
7469
7959
|
// src/capabilities/pool/pool-stats-manager.ts
|
|
7960
|
+
var SILENT_POOL_STATS_LOGGER = {};
|
|
7470
7961
|
var PoolStatsManager = class {
|
|
7471
7962
|
constructor(options = {}) {
|
|
7472
7963
|
this._stats = /* @__PURE__ */ new Map();
|
|
7473
7964
|
this._buffer = [];
|
|
7474
|
-
this._logger = options.logger ??
|
|
7965
|
+
this._logger = options.logger ?? SILENT_POOL_STATS_LOGGER;
|
|
7475
7966
|
this._batchInterval = setInterval(() => {
|
|
7476
7967
|
this._flush();
|
|
7477
7968
|
}, 100);
|
|
@@ -7719,9 +8210,9 @@ var ConnectionPoolManager = class {
|
|
|
7719
8210
|
this._fallbackConfig = this.fallback;
|
|
7720
8211
|
this._fallback = this.fallback;
|
|
7721
8212
|
this._pools = this.pools;
|
|
7722
|
-
this._selector = new PoolSelector({ strategy: this.strategy });
|
|
7723
|
-
this._healthChecker = new HealthChecker({ poolManager: this });
|
|
7724
|
-
this._stats = new PoolStatsManager({ logger:
|
|
8213
|
+
this._selector = new PoolSelector({ strategy: this.strategy, logger: this.logger ?? void 0 });
|
|
8214
|
+
this._healthChecker = new HealthChecker({ poolManager: this, logger: this.logger ?? void 0 });
|
|
8215
|
+
this._stats = new PoolStatsManager({ logger: this.logger ?? void 0 });
|
|
7725
8216
|
}
|
|
7726
8217
|
/**
|
|
7727
8218
|
* Add a connection pool.
|
|
@@ -9554,7 +10045,7 @@ async function computeTotals(coll, query, limit, totals, defaults = {}, queryCac
|
|
|
9554
10045
|
}
|
|
9555
10046
|
|
|
9556
10047
|
// src/adapters/mongodb/queries/find-page.ts
|
|
9557
|
-
function
|
|
10048
|
+
function normalizePositiveInteger2(value, fallback, field) {
|
|
9558
10049
|
if (value === void 0 || value === null) {
|
|
9559
10050
|
return fallback;
|
|
9560
10051
|
}
|
|
@@ -9602,8 +10093,8 @@ async function executeFindPage(collection, options = {}, defaults = {}, queryCac
|
|
|
9602
10093
|
const metaStartTs = Date.now();
|
|
9603
10094
|
const metaSteps = [];
|
|
9604
10095
|
const ext = options;
|
|
9605
|
-
const page =
|
|
9606
|
-
const rawLimit =
|
|
10096
|
+
const page = normalizePositiveInteger2(options.page, 1, "page");
|
|
10097
|
+
const rawLimit = normalizePositiveInteger2(options.limit, DEFAULT_FIND_PAGE_LIMIT, "limit");
|
|
9607
10098
|
const limit = defaults.findPageMaxLimit !== void 0 && rawLimit > defaults.findPageMaxLimit ? defaults.findPageMaxLimit : rawLimit;
|
|
9608
10099
|
const sort = normalizeSortShape(options.sort);
|
|
9609
10100
|
const baseQuery = options.query ?? {};
|
|
@@ -10477,6 +10968,138 @@ function explainDocuments(collection, query, options, defaults) {
|
|
|
10477
10968
|
return createFindChain(collection, query, options, defaults).explain(options?.explain ?? "queryPlanner");
|
|
10478
10969
|
}
|
|
10479
10970
|
|
|
10971
|
+
// src/adapters/mongodb/queries/vector-search.ts
|
|
10972
|
+
var VECTOR_SEARCH_SCORE_FIELD = "__monsqlize_vector_search_score__";
|
|
10973
|
+
function attachVectorSearchOperationContext(error, namespace) {
|
|
10974
|
+
if (error instanceof Error) {
|
|
10975
|
+
const contextualized = error;
|
|
10976
|
+
const existing = contextualized.monsqlize;
|
|
10977
|
+
try {
|
|
10978
|
+
contextualized.monsqlize = {
|
|
10979
|
+
...existing && typeof existing === "object" ? existing : {},
|
|
10980
|
+
operation: "vectorSearch",
|
|
10981
|
+
namespace
|
|
10982
|
+
};
|
|
10983
|
+
} catch {
|
|
10984
|
+
}
|
|
10985
|
+
return error;
|
|
10986
|
+
}
|
|
10987
|
+
return createError(
|
|
10988
|
+
ErrorCodes.MONGODB_ERROR,
|
|
10989
|
+
"vectorSearch execution failed",
|
|
10990
|
+
[{ operation: "vectorSearch", namespace, reason: String(error) }]
|
|
10991
|
+
);
|
|
10992
|
+
}
|
|
10993
|
+
function assertNonEmptyString(value, field) {
|
|
10994
|
+
if (typeof value !== "string" || value.trim().length === 0) {
|
|
10995
|
+
throw createError(ErrorCodes.INVALID_VECTOR_SEARCH, `${field} must be a non-empty string`, [{ field, reason: "non-empty string required" }]);
|
|
10996
|
+
}
|
|
10997
|
+
}
|
|
10998
|
+
function assertPositiveInteger(value, field) {
|
|
10999
|
+
if (!Number.isInteger(value) || value <= 0) {
|
|
11000
|
+
throw createError(ErrorCodes.INVALID_VECTOR_SEARCH, `${field} must be a positive integer`, [{ field, reason: "positive integer required" }]);
|
|
11001
|
+
}
|
|
11002
|
+
}
|
|
11003
|
+
function isNonArrayObject2(value) {
|
|
11004
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
11005
|
+
}
|
|
11006
|
+
function normalizeVectorProjection(value) {
|
|
11007
|
+
if (value === void 0) return void 0;
|
|
11008
|
+
if (Array.isArray(value)) {
|
|
11009
|
+
if (value.some((field) => typeof field !== "string" || field.length === 0)) {
|
|
11010
|
+
throw createError(ErrorCodes.INVALID_VECTOR_SEARCH, "projection array entries must be non-empty strings", [{ field: "projection", reason: "array entries must be non-empty strings" }]);
|
|
11011
|
+
}
|
|
11012
|
+
return normalizeProjection(value);
|
|
11013
|
+
}
|
|
11014
|
+
if (!isNonArrayObject2(value)) {
|
|
11015
|
+
throw createError(ErrorCodes.INVALID_VECTOR_SEARCH, "projection must be an object or string array", [{ field: "projection", reason: "object or string array required" }]);
|
|
11016
|
+
}
|
|
11017
|
+
return normalizeProjection(value);
|
|
11018
|
+
}
|
|
11019
|
+
function normalizeAggregateOptions(value) {
|
|
11020
|
+
if (value === void 0) return void 0;
|
|
11021
|
+
if (!isNonArrayObject2(value)) {
|
|
11022
|
+
throw createError(ErrorCodes.INVALID_VECTOR_SEARCH, "aggregateOptions must be a non-array object", [{ field: "aggregateOptions", reason: "non-array object required" }]);
|
|
11023
|
+
}
|
|
11024
|
+
for (const key of ["meta", "stream", "explain"]) {
|
|
11025
|
+
if (Object.prototype.hasOwnProperty.call(value, key)) {
|
|
11026
|
+
throw createError(ErrorCodes.INVALID_VECTOR_SEARCH, `aggregateOptions.${key} is not supported by vectorSearch()`, [{ field: `aggregateOptions.${key}`, reason: "reserved aggregate option" }]);
|
|
11027
|
+
}
|
|
11028
|
+
}
|
|
11029
|
+
return { ...value };
|
|
11030
|
+
}
|
|
11031
|
+
function buildVectorSearchPipeline(options) {
|
|
11032
|
+
if (!isNonArrayObject2(options)) {
|
|
11033
|
+
throw createError(ErrorCodes.INVALID_VECTOR_SEARCH, "vectorSearch options must be a non-array object", [{ field: "options", reason: "non-array object required" }]);
|
|
11034
|
+
}
|
|
11035
|
+
assertNonEmptyString(options.index, "index");
|
|
11036
|
+
assertNonEmptyString(options.path, "path");
|
|
11037
|
+
assertPositiveInteger(options.limit, "limit");
|
|
11038
|
+
if (!Array.isArray(options.queryVector) || options.queryVector.length === 0) {
|
|
11039
|
+
throw createError(ErrorCodes.INVALID_VECTOR_SEARCH, "queryVector must be a non-empty number array", [{ field: "queryVector", reason: "non-empty number array required" }]);
|
|
11040
|
+
}
|
|
11041
|
+
if (options.queryVector.some((value) => typeof value !== "number" || !Number.isFinite(value))) {
|
|
11042
|
+
throw createError(ErrorCodes.INVALID_VECTOR_SEARCH, "queryVector values must be finite numbers", [{ field: "queryVector", reason: "finite numbers required" }]);
|
|
11043
|
+
}
|
|
11044
|
+
if (options.exact !== void 0 && typeof options.exact !== "boolean") {
|
|
11045
|
+
throw createError(ErrorCodes.INVALID_VECTOR_SEARCH, "exact must be a boolean", [{ field: "exact", reason: "boolean required" }]);
|
|
11046
|
+
}
|
|
11047
|
+
if (options.filter !== void 0 && !isNonArrayObject2(options.filter)) {
|
|
11048
|
+
throw createError(ErrorCodes.INVALID_VECTOR_SEARCH, "filter must be a non-array object", [{ field: "filter", reason: "non-array object required" }]);
|
|
11049
|
+
}
|
|
11050
|
+
const isExactSearch = options.exact === true;
|
|
11051
|
+
if (isExactSearch) {
|
|
11052
|
+
if (options.numCandidates !== void 0) {
|
|
11053
|
+
throw createError(ErrorCodes.INVALID_VECTOR_SEARCH, "numCandidates is not supported when exact is true", [{ field: "numCandidates", reason: "ANN-only option" }]);
|
|
11054
|
+
}
|
|
11055
|
+
} else {
|
|
11056
|
+
assertPositiveInteger(options.numCandidates, "numCandidates");
|
|
11057
|
+
if (options.numCandidates < options.limit) {
|
|
11058
|
+
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" }]);
|
|
11059
|
+
}
|
|
11060
|
+
}
|
|
11061
|
+
const vectorSearchStage = {
|
|
11062
|
+
index: options.index.trim(),
|
|
11063
|
+
path: options.path.trim(),
|
|
11064
|
+
queryVector: [...options.queryVector],
|
|
11065
|
+
limit: options.limit,
|
|
11066
|
+
...isExactSearch ? { exact: true } : { numCandidates: options.numCandidates },
|
|
11067
|
+
...options.filter === void 0 ? {} : { filter: options.filter }
|
|
11068
|
+
};
|
|
11069
|
+
const projection = normalizeVectorProjection(options.projection);
|
|
11070
|
+
const scoreStage = projection ? {
|
|
11071
|
+
$project: {
|
|
11072
|
+
...projection,
|
|
11073
|
+
[VECTOR_SEARCH_SCORE_FIELD]: { $meta: "vectorSearchScore" }
|
|
11074
|
+
}
|
|
11075
|
+
} : {
|
|
11076
|
+
$set: {
|
|
11077
|
+
[VECTOR_SEARCH_SCORE_FIELD]: { $meta: "vectorSearchScore" }
|
|
11078
|
+
}
|
|
11079
|
+
};
|
|
11080
|
+
return {
|
|
11081
|
+
pipeline: [
|
|
11082
|
+
{ $vectorSearch: vectorSearchStage },
|
|
11083
|
+
scoreStage
|
|
11084
|
+
],
|
|
11085
|
+
aggregateOptions: normalizeAggregateOptions(options.aggregateOptions)
|
|
11086
|
+
};
|
|
11087
|
+
}
|
|
11088
|
+
function mapVectorSearchRows(rows) {
|
|
11089
|
+
return rows.map((row) => {
|
|
11090
|
+
const score = row[VECTOR_SEARCH_SCORE_FIELD];
|
|
11091
|
+
if (typeof score !== "number" || !Number.isFinite(score)) {
|
|
11092
|
+
throw createError(ErrorCodes.INVALID_OPERATION, "Vector Search result did not contain a finite score");
|
|
11093
|
+
}
|
|
11094
|
+
const document = { ...row };
|
|
11095
|
+
delete document[VECTOR_SEARCH_SCORE_FIELD];
|
|
11096
|
+
return {
|
|
11097
|
+
document,
|
|
11098
|
+
score
|
|
11099
|
+
};
|
|
11100
|
+
});
|
|
11101
|
+
}
|
|
11102
|
+
|
|
10480
11103
|
// src/adapters/mongodb/writes/write-utils.ts
|
|
10481
11104
|
function sleep2(ms) {
|
|
10482
11105
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
@@ -11650,6 +12273,19 @@ var MongoCollectionAccessor = class {
|
|
|
11650
12273
|
} : void 0
|
|
11651
12274
|
);
|
|
11652
12275
|
}
|
|
12276
|
+
/** Searches a MongoDB Vector Search index through a constrained aggregate pipeline. */
|
|
12277
|
+
async vectorSearch(options) {
|
|
12278
|
+
const { pipeline, aggregateOptions } = buildVectorSearchPipeline(options);
|
|
12279
|
+
try {
|
|
12280
|
+
const rows = await this.aggregate(
|
|
12281
|
+
pipeline,
|
|
12282
|
+
aggregateOptions
|
|
12283
|
+
);
|
|
12284
|
+
return mapVectorSearchRows(rows);
|
|
12285
|
+
} catch (error) {
|
|
12286
|
+
throw attachVectorSearchOperationContext(error, this.getNamespace());
|
|
12287
|
+
}
|
|
12288
|
+
}
|
|
11653
12289
|
/** Returns an array of distinct values for the given field key. */
|
|
11654
12290
|
async distinct(key, query, options) {
|
|
11655
12291
|
const startTs = Date.now();
|
|
@@ -12151,10 +12787,10 @@ function assertCompatPoolExists(poolManager, poolName) {
|
|
|
12151
12787
|
function createPoolScope(runtime, poolName) {
|
|
12152
12788
|
return {
|
|
12153
12789
|
collection: (name) => runtime.scopedCollection(name, { pool: poolName }),
|
|
12154
|
-
model: (
|
|
12790
|
+
model: (model) => runtime.scopedModel(model, { pool: poolName }),
|
|
12155
12791
|
use: (dbName) => ({
|
|
12156
12792
|
collection: (name) => runtime.scopedCollection(name, { pool: poolName, database: dbName }),
|
|
12157
|
-
model: (
|
|
12793
|
+
model: (model) => runtime.scopedModel(model, { pool: poolName, database: dbName })
|
|
12158
12794
|
})
|
|
12159
12795
|
};
|
|
12160
12796
|
}
|
|
@@ -12184,6 +12820,7 @@ function createCompatModelInstance(config) {
|
|
|
12184
12820
|
config.runtime,
|
|
12185
12821
|
{
|
|
12186
12822
|
collectionName: config.collectionName,
|
|
12823
|
+
modelName: config.modelName,
|
|
12187
12824
|
dbName: config.dbName,
|
|
12188
12825
|
poolName: config.poolName,
|
|
12189
12826
|
definition: config.definition,
|
|
@@ -12221,6 +12858,7 @@ function createRuntimeModelInstance(host, name, scope) {
|
|
|
12221
12858
|
host.runtime,
|
|
12222
12859
|
{
|
|
12223
12860
|
collectionName: actualCollectionName,
|
|
12861
|
+
modelName: registered.collectionName,
|
|
12224
12862
|
dbName: databaseName,
|
|
12225
12863
|
poolName,
|
|
12226
12864
|
definition: registered.definition,
|
|
@@ -16308,7 +16946,7 @@ var MonSQLizeRuntime = class {
|
|
|
16308
16946
|
requireCompatDbInstance(this);
|
|
16309
16947
|
return {
|
|
16310
16948
|
collection: (collectionName) => this.scopedCollection(collectionName, { database: name }),
|
|
16311
|
-
model: (
|
|
16949
|
+
model: (model) => this.scopedModel(model, { database: name })
|
|
16312
16950
|
};
|
|
16313
16951
|
}
|
|
16314
16952
|
pool(poolName) {
|
|
@@ -16340,7 +16978,8 @@ var MonSQLizeRuntime = class {
|
|
|
16340
16978
|
});
|
|
16341
16979
|
}
|
|
16342
16980
|
// Model accessors ---------------------------------------------------------
|
|
16343
|
-
scopedModel(
|
|
16981
|
+
scopedModel(model, options = {}) {
|
|
16982
|
+
const name = typeof model === "string" ? model : model.collectionName;
|
|
16344
16983
|
const dbInstance = requireCompatDbInstance(this);
|
|
16345
16984
|
if (this._client) {
|
|
16346
16985
|
return this.createModelInstance(name, options);
|
|
@@ -16357,13 +16996,15 @@ var MonSQLizeRuntime = class {
|
|
|
16357
16996
|
collection,
|
|
16358
16997
|
runtime: this,
|
|
16359
16998
|
collectionName: actualCollectionName,
|
|
16999
|
+
modelName: registered.collectionName,
|
|
16360
17000
|
dbName: database ?? getRuntimeDatabaseName(this),
|
|
16361
17001
|
poolName: pool,
|
|
16362
17002
|
definition: registered.definition,
|
|
16363
17003
|
schemaEngine: this._schemaDslEngine
|
|
16364
17004
|
});
|
|
16365
17005
|
}
|
|
16366
|
-
model(
|
|
17006
|
+
model(model) {
|
|
17007
|
+
const name = typeof model === "string" ? model : model.collectionName;
|
|
16367
17008
|
if (this._client) {
|
|
16368
17009
|
this.ensureConnected();
|
|
16369
17010
|
return this.createModelInstance(name, {
|
|
@@ -16389,6 +17030,7 @@ var MonSQLizeRuntime = class {
|
|
|
16389
17030
|
collection,
|
|
16390
17031
|
runtime: this,
|
|
16391
17032
|
collectionName: actualCollectionName,
|
|
17033
|
+
modelName: registered.collectionName,
|
|
16392
17034
|
dbName: getRuntimeDatabaseName(this),
|
|
16393
17035
|
definition: registered.definition,
|
|
16394
17036
|
schemaEngine: this._schemaDslEngine
|
|
@@ -18163,6 +18805,7 @@ MonSQLize.PoolSelector = PoolSelector;
|
|
|
18163
18805
|
MonSQLize.PoolStats = PoolStatsManager;
|
|
18164
18806
|
MonSQLize.validatePoolConfig = validatePoolConfig;
|
|
18165
18807
|
MonSQLize.validatePoolConfigSafe = validatePoolConfigSafe;
|
|
18808
|
+
MonSQLize.defineModel = defineModel;
|
|
18166
18809
|
MonSQLize.Model = Model;
|
|
18167
18810
|
MonSQLize.expr = expr;
|
|
18168
18811
|
MonSQLize.createExpression = createExpression;
|