monsqlize 2.0.2 → 2.0.4
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 +13 -5
- package/README.md +66 -20
- package/changelogs/README.md +8 -4
- package/changelogs/v2.0.0.md +1 -1
- package/changelogs/v2.0.3.md +58 -0
- package/changelogs/v2.0.4.md +61 -0
- package/dist/cjs/index.cjs +911 -213
- package/dist/cjs/transaction/Transaction.cjs +88 -3
- package/dist/cjs/transaction/TransactionManager.cjs +135 -11
- package/dist/esm/index.mjs +906 -208
- package/dist/types/collection.d.ts +5 -3
- package/dist/types/model.d.ts +279 -175
- package/dist/types/mongodb.d.ts +8 -1
- package/dist/types/monsqlize.d.ts +37 -4
- package/dist/types/pool.d.ts +1 -1
- package/dist/types/runtime.d.ts +33 -8
- package/dist/types/transaction.d.ts +12 -0
- package/package.json +23 -22
package/dist/esm/index.mjs
CHANGED
|
@@ -128,8 +128,8 @@ function createQueryTimeoutError(timeoutMs) {
|
|
|
128
128
|
}
|
|
129
129
|
|
|
130
130
|
// src/capabilities/cache/redis-cache-adapter.ts
|
|
131
|
-
var LEGACY_INVALID_REDIS_ARG_ERROR = "redisUrlOrInstance
|
|
132
|
-
var LEGACY_IOREDIS_MISSING_ERROR = "ioredis
|
|
131
|
+
var LEGACY_INVALID_REDIS_ARG_ERROR = "redisUrlOrInstance must be a Redis URL string or an ioredis instance";
|
|
132
|
+
var LEGACY_IOREDIS_MISSING_ERROR = "Unable to load ioredis. monsqlize installs ioredis by default; check package installation completeness or pass an existing ioredis instance";
|
|
133
133
|
function isMissingIoredisError(error) {
|
|
134
134
|
if (!(error instanceof Error)) {
|
|
135
135
|
return false;
|
|
@@ -139,14 +139,14 @@ function isMissingIoredisError(error) {
|
|
|
139
139
|
function createLegacyRedisError(message, code = ErrorCodes.INVALID_ARGUMENT) {
|
|
140
140
|
return createError(code, message);
|
|
141
141
|
}
|
|
142
|
-
function createRedisCacheAdapter(redisUrlOrInstance) {
|
|
142
|
+
function createRedisCacheAdapter(redisUrlOrInstance, options) {
|
|
143
143
|
if (typeof redisUrlOrInstance === "string") {
|
|
144
144
|
const redisUrl = redisUrlOrInstance.trim();
|
|
145
145
|
if (!redisUrl) {
|
|
146
146
|
throw createLegacyRedisError(LEGACY_INVALID_REDIS_ARG_ERROR);
|
|
147
147
|
}
|
|
148
148
|
try {
|
|
149
|
-
return createHubRedisCacheAdapter(redisUrl);
|
|
149
|
+
return createHubRedisCacheAdapter(redisUrl, options);
|
|
150
150
|
} catch (error) {
|
|
151
151
|
if (isMissingIoredisError(error)) {
|
|
152
152
|
throw createLegacyRedisError(LEGACY_IOREDIS_MISSING_ERROR, ErrorCodes.CACHE_UNAVAILABLE);
|
|
@@ -155,7 +155,7 @@ function createRedisCacheAdapter(redisUrlOrInstance) {
|
|
|
155
155
|
}
|
|
156
156
|
}
|
|
157
157
|
if (redisUrlOrInstance && typeof redisUrlOrInstance === "object") {
|
|
158
|
-
return createHubRedisCacheAdapter(redisUrlOrInstance);
|
|
158
|
+
return createHubRedisCacheAdapter(redisUrlOrInstance, options);
|
|
159
159
|
}
|
|
160
160
|
throw createLegacyRedisError(LEGACY_INVALID_REDIS_ARG_ERROR);
|
|
161
161
|
}
|
|
@@ -165,7 +165,7 @@ import { randomBytes } from "crypto";
|
|
|
165
165
|
var DistributedCacheInvalidator = class {
|
|
166
166
|
constructor(options) {
|
|
167
167
|
if (!options.cache) {
|
|
168
|
-
throw
|
|
168
|
+
throw createError(ErrorCodes.CACHE_UNAVAILABLE, "DistributedCacheInvalidator requires a cache instance");
|
|
169
169
|
}
|
|
170
170
|
this._cache = options.cache;
|
|
171
171
|
this._logger = options.logger ?? null;
|
|
@@ -183,7 +183,7 @@ var DistributedCacheInvalidator = class {
|
|
|
183
183
|
this.pub = new Redis(options.redisUrl);
|
|
184
184
|
this.sub = new Redis(options.redisUrl);
|
|
185
185
|
} else {
|
|
186
|
-
throw
|
|
186
|
+
throw createError(ErrorCodes.INVALID_CONFIG, "DistributedCacheInvalidator requires either redis or redisUrl");
|
|
187
187
|
}
|
|
188
188
|
this._setupSubscription();
|
|
189
189
|
}
|
|
@@ -363,17 +363,17 @@ var _PopulatePromise = class _PopulatePromise {
|
|
|
363
363
|
/**
|
|
364
364
|
* Append a populate path and return a new PopulatePromise (chainable).
|
|
365
365
|
*/
|
|
366
|
-
populate(
|
|
366
|
+
populate(path3, options) {
|
|
367
367
|
const toConfig = (item) => {
|
|
368
368
|
if (typeof item !== "string" && (typeof item !== "object" || item === null || Array.isArray(item))) {
|
|
369
369
|
throw createError(ErrorCodes.INVALID_ARGUMENT, "populate param must be a string, array, or object");
|
|
370
370
|
}
|
|
371
371
|
return typeof item === "string" ? { path: item, ...options } : { ...item, ...options };
|
|
372
372
|
};
|
|
373
|
-
if (Array.isArray(
|
|
374
|
-
return new _PopulatePromise(this.executor, [...this.paths, ...
|
|
373
|
+
if (Array.isArray(path3)) {
|
|
374
|
+
return new _PopulatePromise(this.executor, [...this.paths, ...path3.map(toConfig)]);
|
|
375
375
|
}
|
|
376
|
-
const config = toConfig(
|
|
376
|
+
const config = toConfig(path3);
|
|
377
377
|
return new _PopulatePromise(this.executor, [...this.paths, config]);
|
|
378
378
|
}
|
|
379
379
|
/**
|
|
@@ -487,8 +487,8 @@ function validateRelationConfig(name, config) {
|
|
|
487
487
|
throw createError(ErrorCodes.INVALID_ARGUMENT, `relations.single must be a boolean`);
|
|
488
488
|
}
|
|
489
489
|
}
|
|
490
|
-
function normalizePopulateConfig(
|
|
491
|
-
return typeof
|
|
490
|
+
function normalizePopulateConfig(path3) {
|
|
491
|
+
return typeof path3 === "string" ? { path: path3 } : path3;
|
|
492
492
|
}
|
|
493
493
|
|
|
494
494
|
// src/capabilities/model/model-registry.ts
|
|
@@ -639,8 +639,8 @@ function groupBy(values, keySelector) {
|
|
|
639
639
|
}
|
|
640
640
|
return map;
|
|
641
641
|
}
|
|
642
|
-
function getByPath(source,
|
|
643
|
-
return
|
|
642
|
+
function getByPath(source, path3) {
|
|
643
|
+
return path3.split(".").reduce((current, key) => {
|
|
644
644
|
if (!current || typeof current !== "object") {
|
|
645
645
|
return void 0;
|
|
646
646
|
}
|
|
@@ -695,8 +695,8 @@ function resolveRegisteredCollectionName(registered, fallback) {
|
|
|
695
695
|
const definition = registered.definition;
|
|
696
696
|
return definition.collection ?? definition.name ?? registered.collectionName;
|
|
697
697
|
}
|
|
698
|
-
async function populateModelPath(context, docs,
|
|
699
|
-
const config = normalizePopulateConfig(
|
|
698
|
+
async function populateModelPath(context, docs, path3) {
|
|
699
|
+
const config = normalizePopulateConfig(path3);
|
|
700
700
|
if (docs.length === 0) {
|
|
701
701
|
return docs;
|
|
702
702
|
}
|
|
@@ -813,8 +813,8 @@ function hydrateModelDocument(context, doc) {
|
|
|
813
813
|
populate: {
|
|
814
814
|
configurable: true,
|
|
815
815
|
enumerable: false,
|
|
816
|
-
value: (
|
|
817
|
-
const paths = Array.isArray(
|
|
816
|
+
value: (path3) => {
|
|
817
|
+
const paths = Array.isArray(path3) ? path3 : [path3];
|
|
818
818
|
return new PopulatePromise(
|
|
819
819
|
(resolvedPaths) => context.populateDocument(hydrated, resolvedPaths),
|
|
820
820
|
paths
|
|
@@ -980,6 +980,75 @@ function stableIndexStringify(value) {
|
|
|
980
980
|
}
|
|
981
981
|
return JSON.stringify(value) ?? "undefined";
|
|
982
982
|
}
|
|
983
|
+
function getIndexOptionName(options) {
|
|
984
|
+
return typeof options.name === "string" && options.name.length > 0 ? options.name : void 0;
|
|
985
|
+
}
|
|
986
|
+
function summarizeIndexError(error) {
|
|
987
|
+
if (error instanceof Error) {
|
|
988
|
+
const record = error;
|
|
989
|
+
return {
|
|
990
|
+
name: error.name,
|
|
991
|
+
message: error.message,
|
|
992
|
+
code: record.code ?? record.codeName
|
|
993
|
+
};
|
|
994
|
+
}
|
|
995
|
+
return {
|
|
996
|
+
message: String(error)
|
|
997
|
+
};
|
|
998
|
+
}
|
|
999
|
+
function isRecord(value) {
|
|
1000
|
+
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
1001
|
+
}
|
|
1002
|
+
function getExistingIndexKey(index) {
|
|
1003
|
+
return index.key;
|
|
1004
|
+
}
|
|
1005
|
+
function declaredOptionEntries(options) {
|
|
1006
|
+
return Object.entries(options).filter(([name, value]) => {
|
|
1007
|
+
if (value === void 0) return false;
|
|
1008
|
+
if (name === "background") return false;
|
|
1009
|
+
return true;
|
|
1010
|
+
});
|
|
1011
|
+
}
|
|
1012
|
+
function indexOptionsMatch(existing, declared) {
|
|
1013
|
+
if (stableIndexStringify(getExistingIndexKey(existing)) !== stableIndexStringify(declared.key)) {
|
|
1014
|
+
return false;
|
|
1015
|
+
}
|
|
1016
|
+
for (const [name, value] of declaredOptionEntries(declared.options)) {
|
|
1017
|
+
const existingValue = existing[name];
|
|
1018
|
+
if (value === false && existingValue === void 0) {
|
|
1019
|
+
continue;
|
|
1020
|
+
}
|
|
1021
|
+
if (stableIndexStringify(existingValue) !== stableIndexStringify(value)) {
|
|
1022
|
+
return false;
|
|
1023
|
+
}
|
|
1024
|
+
}
|
|
1025
|
+
return true;
|
|
1026
|
+
}
|
|
1027
|
+
function findExistingIndexByName(existingIndexes, name) {
|
|
1028
|
+
if (!name) return void 0;
|
|
1029
|
+
return existingIndexes.find((index) => index.name === name);
|
|
1030
|
+
}
|
|
1031
|
+
function findExistingIndexByKey(existingIndexes, key) {
|
|
1032
|
+
const fingerprint = stableIndexStringify(key);
|
|
1033
|
+
return existingIndexes.find((index) => stableIndexStringify(getExistingIndexKey(index)) === fingerprint);
|
|
1034
|
+
}
|
|
1035
|
+
function createIndexEnsureError(message, result, cause) {
|
|
1036
|
+
return createError(ErrorCodes.MONGODB_ERROR, message, [result], cause);
|
|
1037
|
+
}
|
|
1038
|
+
function resolveModelAutoIndexOptions(definition, runtimeAutoIndex) {
|
|
1039
|
+
const modelAutoIndex = toCompatDefinition(definition).options?.autoIndex;
|
|
1040
|
+
const value = modelAutoIndex ?? runtimeAutoIndex;
|
|
1041
|
+
if (value === false) {
|
|
1042
|
+
return { enabled: false, emitEvents: true };
|
|
1043
|
+
}
|
|
1044
|
+
if (value && typeof value === "object") {
|
|
1045
|
+
return {
|
|
1046
|
+
enabled: value.enabled !== false,
|
|
1047
|
+
emitEvents: value.emitEvents !== false
|
|
1048
|
+
};
|
|
1049
|
+
}
|
|
1050
|
+
return { enabled: true, emitEvents: true };
|
|
1051
|
+
}
|
|
983
1052
|
function getIndexTaskRegistry(runtime) {
|
|
984
1053
|
if (!runtime) {
|
|
985
1054
|
return fallbackModelIndexTasks;
|
|
@@ -1007,6 +1076,20 @@ function resolveIndexTaskScope(collection, options) {
|
|
|
1007
1076
|
};
|
|
1008
1077
|
}
|
|
1009
1078
|
}
|
|
1079
|
+
function toIndexNamespace(scope) {
|
|
1080
|
+
return {
|
|
1081
|
+
db: scope.dbName,
|
|
1082
|
+
collection: scope.collectionName,
|
|
1083
|
+
poolName: scope.poolName
|
|
1084
|
+
};
|
|
1085
|
+
}
|
|
1086
|
+
function emitIndexFailure(runtime, payload, emitEvents) {
|
|
1087
|
+
if (!emitEvents) {
|
|
1088
|
+
return;
|
|
1089
|
+
}
|
|
1090
|
+
const emitter = runtime;
|
|
1091
|
+
emitter?.emit?.("model-index-error", payload);
|
|
1092
|
+
}
|
|
1010
1093
|
function warnIndexFailure(runtime, taskKey, error) {
|
|
1011
1094
|
const logger = runtime;
|
|
1012
1095
|
logger?.logger?.warn?.("[MonSQLize] model index creation failed", {
|
|
@@ -1014,9 +1097,10 @@ function warnIndexFailure(runtime, taskKey, error) {
|
|
|
1014
1097
|
error: error instanceof Error ? error.message : String(error)
|
|
1015
1098
|
});
|
|
1016
1099
|
}
|
|
1017
|
-
function scheduleIndexTask(collection,
|
|
1100
|
+
function scheduleIndexTask(collection, declaredIndex, emitEvents, options) {
|
|
1018
1101
|
const scope = resolveIndexTaskScope(collection, options);
|
|
1019
|
-
const
|
|
1102
|
+
const { key, options: indexOptions } = declaredIndex;
|
|
1103
|
+
const indexFingerprint = declaredIndex.fingerprint;
|
|
1020
1104
|
const taskKey = `${scope.poolName}:${scope.dbName}:${scope.collectionName}:${indexFingerprint}`;
|
|
1021
1105
|
const registry = getIndexTaskRegistry(options?.runtime);
|
|
1022
1106
|
const existing = registry.get(taskKey);
|
|
@@ -1034,6 +1118,14 @@ function scheduleIndexTask(collection, key, indexOptions, options) {
|
|
|
1034
1118
|
task.status = "failed";
|
|
1035
1119
|
task.error = error;
|
|
1036
1120
|
warnIndexFailure(options?.runtime, taskKey, error);
|
|
1121
|
+
emitIndexFailure(options?.runtime, {
|
|
1122
|
+
namespace: scope,
|
|
1123
|
+
taskKey,
|
|
1124
|
+
source: declaredIndex.source,
|
|
1125
|
+
key,
|
|
1126
|
+
options: indexOptions,
|
|
1127
|
+
error: summarizeIndexError(error)
|
|
1128
|
+
}, emitEvents);
|
|
1037
1129
|
resolve();
|
|
1038
1130
|
});
|
|
1039
1131
|
});
|
|
@@ -1150,6 +1242,134 @@ function resolveModelHooksFactory(definition) {
|
|
|
1150
1242
|
const hooks = toCompatDefinition(definition).hooks;
|
|
1151
1243
|
return typeof hooks === "function" ? hooks : null;
|
|
1152
1244
|
}
|
|
1245
|
+
function collectModelIndexDefinitions(definition, softDeleteConfig) {
|
|
1246
|
+
const declared = [];
|
|
1247
|
+
if (softDeleteConfig?.enabled && softDeleteConfig.type === "timestamp" && softDeleteConfig.ttl) {
|
|
1248
|
+
const key = { [softDeleteConfig.field]: 1 };
|
|
1249
|
+
const options = { expireAfterSeconds: softDeleteConfig.ttl };
|
|
1250
|
+
declared.push({
|
|
1251
|
+
source: "softDelete",
|
|
1252
|
+
key,
|
|
1253
|
+
options,
|
|
1254
|
+
name: getIndexOptionName(options),
|
|
1255
|
+
fingerprint: stableIndexStringify({ key, options })
|
|
1256
|
+
});
|
|
1257
|
+
}
|
|
1258
|
+
const indexes = toCompatDefinition(definition).indexes;
|
|
1259
|
+
if (!Array.isArray(indexes) || indexes.length === 0) {
|
|
1260
|
+
return declared;
|
|
1261
|
+
}
|
|
1262
|
+
for (const indexSpec of indexes) {
|
|
1263
|
+
if (!isRecord(indexSpec) || !indexSpec.key) {
|
|
1264
|
+
continue;
|
|
1265
|
+
}
|
|
1266
|
+
const { key, ...indexOptions } = indexSpec;
|
|
1267
|
+
declared.push({
|
|
1268
|
+
source: "definition",
|
|
1269
|
+
key,
|
|
1270
|
+
options: indexOptions,
|
|
1271
|
+
name: getIndexOptionName(indexOptions),
|
|
1272
|
+
fingerprint: stableIndexStringify({ key, options: indexOptions })
|
|
1273
|
+
});
|
|
1274
|
+
}
|
|
1275
|
+
return declared;
|
|
1276
|
+
}
|
|
1277
|
+
async function ensureModelIndexesForCollection(collection, definition, softDeleteConfig, options = {}) {
|
|
1278
|
+
const namespace = toIndexNamespace(resolveIndexTaskScope(collection, options));
|
|
1279
|
+
const declared = collectModelIndexDefinitions(definition, softDeleteConfig);
|
|
1280
|
+
const existingIndexes = await collection.listIndexes();
|
|
1281
|
+
const existing = [];
|
|
1282
|
+
const missing = [];
|
|
1283
|
+
const conflicts = [];
|
|
1284
|
+
for (const declaredIndex of declared) {
|
|
1285
|
+
const existingByName = findExistingIndexByName(existingIndexes, declaredIndex.name);
|
|
1286
|
+
if (existingByName) {
|
|
1287
|
+
if (indexOptionsMatch(existingByName, declaredIndex)) {
|
|
1288
|
+
existing.push({ declared: declaredIndex, existing: existingByName });
|
|
1289
|
+
} else {
|
|
1290
|
+
conflicts.push({
|
|
1291
|
+
declared: declaredIndex,
|
|
1292
|
+
existing: existingByName,
|
|
1293
|
+
reason: "name-conflict"
|
|
1294
|
+
});
|
|
1295
|
+
}
|
|
1296
|
+
continue;
|
|
1297
|
+
}
|
|
1298
|
+
const existingByKey = findExistingIndexByKey(existingIndexes, declaredIndex.key);
|
|
1299
|
+
if (existingByKey) {
|
|
1300
|
+
if (indexOptionsMatch(existingByKey, declaredIndex)) {
|
|
1301
|
+
existing.push({ declared: declaredIndex, existing: existingByKey });
|
|
1302
|
+
} else {
|
|
1303
|
+
conflicts.push({
|
|
1304
|
+
declared: declaredIndex,
|
|
1305
|
+
existing: existingByKey,
|
|
1306
|
+
reason: "options-conflict"
|
|
1307
|
+
});
|
|
1308
|
+
}
|
|
1309
|
+
continue;
|
|
1310
|
+
}
|
|
1311
|
+
missing.push(declaredIndex);
|
|
1312
|
+
}
|
|
1313
|
+
const result = {
|
|
1314
|
+
dryRun: options.dryRun === true,
|
|
1315
|
+
namespace,
|
|
1316
|
+
declared,
|
|
1317
|
+
existing,
|
|
1318
|
+
missing,
|
|
1319
|
+
created: [],
|
|
1320
|
+
conflicts,
|
|
1321
|
+
failed: [],
|
|
1322
|
+
skipped: options.dryRun === true ? missing.map((declaredIndex) => ({ declared: declaredIndex, reason: "dry-run" })) : conflicts.map((conflict) => ({ declared: conflict.declared, reason: conflict.reason }))
|
|
1323
|
+
};
|
|
1324
|
+
if (conflicts.length > 0 && options.throwOnError) {
|
|
1325
|
+
throw createIndexEnsureError("Model index conflicts detected.", result);
|
|
1326
|
+
}
|
|
1327
|
+
if (options.dryRun === true) {
|
|
1328
|
+
return result;
|
|
1329
|
+
}
|
|
1330
|
+
for (const declaredIndex of missing) {
|
|
1331
|
+
try {
|
|
1332
|
+
const createdName = await collection.createIndex(declaredIndex.key, declaredIndex.options);
|
|
1333
|
+
result.created.push({
|
|
1334
|
+
declared: declaredIndex,
|
|
1335
|
+
name: typeof createdName === "string" ? createdName : void 0,
|
|
1336
|
+
result: createdName
|
|
1337
|
+
});
|
|
1338
|
+
} catch (error) {
|
|
1339
|
+
result.failed.push({
|
|
1340
|
+
declared: declaredIndex,
|
|
1341
|
+
error: summarizeIndexError(error)
|
|
1342
|
+
});
|
|
1343
|
+
if (options.throwOnError) {
|
|
1344
|
+
throw createIndexEnsureError(
|
|
1345
|
+
"Model index creation failed.",
|
|
1346
|
+
result,
|
|
1347
|
+
error instanceof Error ? error : void 0
|
|
1348
|
+
);
|
|
1349
|
+
}
|
|
1350
|
+
}
|
|
1351
|
+
}
|
|
1352
|
+
return result;
|
|
1353
|
+
}
|
|
1354
|
+
function summarizeModelIndexEnsureResults(results) {
|
|
1355
|
+
return results.reduce((totals, result) => ({
|
|
1356
|
+
declared: totals.declared + result.declared.length,
|
|
1357
|
+
existing: totals.existing + result.existing.length,
|
|
1358
|
+
missing: totals.missing + result.missing.length,
|
|
1359
|
+
created: totals.created + result.created.length,
|
|
1360
|
+
conflicts: totals.conflicts + result.conflicts.length,
|
|
1361
|
+
failed: totals.failed + result.failed.length,
|
|
1362
|
+
skipped: totals.skipped + result.skipped.length
|
|
1363
|
+
}), {
|
|
1364
|
+
declared: 0,
|
|
1365
|
+
existing: 0,
|
|
1366
|
+
missing: 0,
|
|
1367
|
+
created: 0,
|
|
1368
|
+
conflicts: 0,
|
|
1369
|
+
failed: 0,
|
|
1370
|
+
skipped: 0
|
|
1371
|
+
});
|
|
1372
|
+
}
|
|
1153
1373
|
function initializeModelV1Methods(target, definition) {
|
|
1154
1374
|
const methods = toCompatDefinition(definition).methods;
|
|
1155
1375
|
if (typeof methods !== "function") {
|
|
@@ -1174,25 +1394,12 @@ function initializeModelV1Methods(target, definition) {
|
|
|
1174
1394
|
}
|
|
1175
1395
|
}
|
|
1176
1396
|
function scheduleModelIndexes(collection, definition, softDeleteConfig, options) {
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
scheduleIndexTask(
|
|
1180
|
-
collection,
|
|
1181
|
-
{ [softDeleteIndex.field]: 1 },
|
|
1182
|
-
{ expireAfterSeconds: softDeleteIndex.ttl },
|
|
1183
|
-
options
|
|
1184
|
-
);
|
|
1185
|
-
}
|
|
1186
|
-
const indexes = toCompatDefinition(definition).indexes;
|
|
1187
|
-
if (!Array.isArray(indexes) || indexes.length === 0) {
|
|
1397
|
+
const autoIndex = resolveModelAutoIndexOptions(definition, options?.autoIndex);
|
|
1398
|
+
if (!autoIndex.enabled) {
|
|
1188
1399
|
return;
|
|
1189
1400
|
}
|
|
1190
|
-
for (const
|
|
1191
|
-
|
|
1192
|
-
continue;
|
|
1193
|
-
}
|
|
1194
|
-
const { key, ...indexOptions } = indexSpec;
|
|
1195
|
-
scheduleIndexTask(collection, key, indexOptions, options);
|
|
1401
|
+
for (const declaredIndex of collectModelIndexDefinitions(definition, softDeleteConfig)) {
|
|
1402
|
+
scheduleIndexTask(collection, declaredIndex, autoIndex.emitEvents, options);
|
|
1196
1403
|
}
|
|
1197
1404
|
}
|
|
1198
1405
|
|
|
@@ -1760,7 +1967,8 @@ var ModelInstance = class {
|
|
|
1760
1967
|
runtime: this.runtime,
|
|
1761
1968
|
dbName: options.dbName,
|
|
1762
1969
|
poolName: options.poolName,
|
|
1763
|
-
collectionName: options.collectionName
|
|
1970
|
+
collectionName: options.collectionName,
|
|
1971
|
+
autoIndex: this.runtime.options?.autoIndex
|
|
1764
1972
|
});
|
|
1765
1973
|
this._v1InstanceMethods = initializeModelV1Methods(this, options.definition);
|
|
1766
1974
|
}
|
|
@@ -1964,6 +2172,15 @@ var ModelInstance = class {
|
|
|
1964
2172
|
listIndexes() {
|
|
1965
2173
|
return this.collection.listIndexes();
|
|
1966
2174
|
}
|
|
2175
|
+
ensureIndexes(options = {}) {
|
|
2176
|
+
return ensureModelIndexesForCollection(this.collection, this.definition, this._softDeleteConfig, {
|
|
2177
|
+
...options,
|
|
2178
|
+
runtime: this.runtime,
|
|
2179
|
+
dbName: this.dbName,
|
|
2180
|
+
poolName: this.poolName,
|
|
2181
|
+
collectionName: this.collectionName
|
|
2182
|
+
});
|
|
2183
|
+
}
|
|
1967
2184
|
dropIndex(name) {
|
|
1968
2185
|
return this.collection.dropIndex(name);
|
|
1969
2186
|
}
|
|
@@ -2049,18 +2266,18 @@ var ModelInstance = class {
|
|
|
2049
2266
|
}
|
|
2050
2267
|
async populateDocuments(docs, paths) {
|
|
2051
2268
|
let current = docs;
|
|
2052
|
-
for (const
|
|
2053
|
-
current = await this.populatePath(current,
|
|
2269
|
+
for (const path3 of paths) {
|
|
2270
|
+
current = await this.populatePath(current, path3);
|
|
2054
2271
|
}
|
|
2055
2272
|
return current;
|
|
2056
2273
|
}
|
|
2057
|
-
async populatePath(docs,
|
|
2274
|
+
async populatePath(docs, path3) {
|
|
2058
2275
|
return populateModelPath({
|
|
2059
2276
|
relations: this.relations,
|
|
2060
2277
|
runtime: this.runtime,
|
|
2061
2278
|
dbName: this.dbName,
|
|
2062
2279
|
poolName: this.poolName
|
|
2063
|
-
}, docs,
|
|
2280
|
+
}, docs, path3);
|
|
2064
2281
|
}
|
|
2065
2282
|
hydrateDocuments(docs) {
|
|
2066
2283
|
return docs.filter(Boolean).map((doc) => this.hydrateDocument(doc));
|
|
@@ -2233,9 +2450,9 @@ var Transaction = class {
|
|
|
2233
2450
|
*/
|
|
2234
2451
|
async start() {
|
|
2235
2452
|
if (this.state !== "pending") {
|
|
2236
|
-
throw
|
|
2453
|
+
throw createError(ErrorCodes.INVALID_OPERATION, `Cannot start transaction in state: ${this.state}`);
|
|
2237
2454
|
}
|
|
2238
|
-
this.session.startTransaction();
|
|
2455
|
+
this.session.startTransaction(this.options.transactionOptions);
|
|
2239
2456
|
this.state = "active";
|
|
2240
2457
|
this.startedAt = Date.now();
|
|
2241
2458
|
const timeout = this.options.timeout ?? 3e4;
|
|
@@ -2255,7 +2472,7 @@ var Transaction = class {
|
|
|
2255
2472
|
*/
|
|
2256
2473
|
async commit() {
|
|
2257
2474
|
if (this.state !== "active") {
|
|
2258
|
-
throw
|
|
2475
|
+
throw createError(ErrorCodes.INVALID_OPERATION, `Cannot commit transaction in state: ${this.state}`);
|
|
2259
2476
|
}
|
|
2260
2477
|
if (typeof this.session.commitTransaction === "function") {
|
|
2261
2478
|
await this.session.commitTransaction();
|
|
@@ -2353,7 +2570,9 @@ var TransactionManager = class {
|
|
|
2353
2570
|
this.stats = {
|
|
2354
2571
|
totalTransactions: 0,
|
|
2355
2572
|
successfulTransactions: 0,
|
|
2356
|
-
failedTransactions: 0
|
|
2573
|
+
failedTransactions: 0,
|
|
2574
|
+
readOnlyTransactions: 0,
|
|
2575
|
+
writeTransactions: 0
|
|
2357
2576
|
};
|
|
2358
2577
|
const options = "client" in input ? input : {
|
|
2359
2578
|
client: input,
|
|
@@ -2364,6 +2583,10 @@ var TransactionManager = class {
|
|
|
2364
2583
|
this.cache = options.cache ?? null;
|
|
2365
2584
|
this.logger = options.logger ?? null;
|
|
2366
2585
|
this.lockManager = options.lockManager ?? null;
|
|
2586
|
+
this.defaultReadConcern = options.defaultReadConcern;
|
|
2587
|
+
this.defaultWriteConcern = options.defaultWriteConcern;
|
|
2588
|
+
this.defaultReadPreference = options.defaultReadPreference;
|
|
2589
|
+
this.maxStatsSamples = options.maxStatsSamples ?? 1e3;
|
|
2367
2590
|
this.defaultOptions = {
|
|
2368
2591
|
maxDuration: options.maxDuration ?? 3e4,
|
|
2369
2592
|
enableRetry: options.enableRetry ?? true,
|
|
@@ -2380,11 +2603,17 @@ var TransactionManager = class {
|
|
|
2380
2603
|
const session = this.client.startSession({
|
|
2381
2604
|
causalConsistency: options.causalConsistency !== false
|
|
2382
2605
|
});
|
|
2606
|
+
const transactionOptions = {
|
|
2607
|
+
readConcern: options.readConcern ?? this.defaultReadConcern,
|
|
2608
|
+
writeConcern: options.writeConcern ?? this.defaultWriteConcern,
|
|
2609
|
+
readPreference: options.readPreference ?? this.defaultReadPreference
|
|
2610
|
+
};
|
|
2383
2611
|
const transaction = new Transaction(session, {
|
|
2384
2612
|
cache: this.cache,
|
|
2385
2613
|
logger: this.logger,
|
|
2386
2614
|
lockManager: options.enableCacheLock === false ? null : this.lockManager,
|
|
2387
|
-
timeout: options.timeout ?? options.maxDuration ?? this.defaultOptions.maxDuration
|
|
2615
|
+
timeout: options.timeout ?? options.maxDuration ?? this.defaultOptions.maxDuration,
|
|
2616
|
+
transactionOptions: compactUndefined(transactionOptions)
|
|
2388
2617
|
});
|
|
2389
2618
|
const originalEnd = transaction.end.bind(transaction);
|
|
2390
2619
|
transaction.end = async () => {
|
|
@@ -2411,12 +2640,12 @@ var TransactionManager = class {
|
|
|
2411
2640
|
await transaction.start();
|
|
2412
2641
|
const result = await callback(transaction);
|
|
2413
2642
|
await transaction.commit();
|
|
2414
|
-
this.recordStats(Date.now() - startedAt, true);
|
|
2643
|
+
this.recordStats(transaction, Date.now() - startedAt, true);
|
|
2415
2644
|
return result;
|
|
2416
2645
|
} catch (error) {
|
|
2417
2646
|
lastError = error;
|
|
2418
2647
|
await transaction.abort();
|
|
2419
|
-
this.recordStats(Date.now() - startedAt, false);
|
|
2648
|
+
this.recordStats(transaction, Date.now() - startedAt, false);
|
|
2420
2649
|
if (!enableRetry || attempt === maxRetries || !isTransientTransactionError(error)) {
|
|
2421
2650
|
throw error;
|
|
2422
2651
|
}
|
|
@@ -2452,27 +2681,54 @@ var TransactionManager = class {
|
|
|
2452
2681
|
*/
|
|
2453
2682
|
getStats() {
|
|
2454
2683
|
const averageDuration = this.durations.length === 0 ? 0 : this.durations.reduce((sum, item) => sum + item, 0) / this.durations.length;
|
|
2684
|
+
const sortedDurations = [...this.durations].sort((a, b) => a - b);
|
|
2685
|
+
const p95Duration = percentile(sortedDurations, 0.95);
|
|
2686
|
+
const p99Duration = percentile(sortedDurations, 0.99);
|
|
2687
|
+
const totalTransactions = this.stats.totalTransactions;
|
|
2455
2688
|
return {
|
|
2456
|
-
totalTransactions
|
|
2689
|
+
totalTransactions,
|
|
2457
2690
|
successfulTransactions: this.stats.successfulTransactions,
|
|
2458
2691
|
failedTransactions: this.stats.failedTransactions,
|
|
2692
|
+
readOnlyTransactions: this.stats.readOnlyTransactions,
|
|
2693
|
+
writeTransactions: this.stats.writeTransactions,
|
|
2459
2694
|
activeTransactions: this.activeTransactions.size,
|
|
2460
|
-
averageDuration
|
|
2695
|
+
averageDuration,
|
|
2696
|
+
p95Duration,
|
|
2697
|
+
p99Duration,
|
|
2698
|
+
successRate: totalTransactions > 0 ? `${(this.stats.successfulTransactions / totalTransactions * 100).toFixed(2)}%` : "0%",
|
|
2699
|
+
readOnlyRatio: totalTransactions > 0 ? `${(this.stats.readOnlyTransactions / totalTransactions * 100).toFixed(2)}%` : "0%",
|
|
2700
|
+
sampleCount: this.durations.length
|
|
2461
2701
|
};
|
|
2462
2702
|
}
|
|
2463
|
-
recordStats(duration, success) {
|
|
2703
|
+
recordStats(transaction, duration, success) {
|
|
2464
2704
|
this.stats.totalTransactions += 1;
|
|
2465
2705
|
if (success) {
|
|
2466
2706
|
this.stats.successfulTransactions += 1;
|
|
2467
2707
|
} else {
|
|
2468
2708
|
this.stats.failedTransactions += 1;
|
|
2469
2709
|
}
|
|
2710
|
+
if (transaction.pendingInvalidations.size > 0) {
|
|
2711
|
+
this.stats.writeTransactions += 1;
|
|
2712
|
+
} else {
|
|
2713
|
+
this.stats.readOnlyTransactions += 1;
|
|
2714
|
+
}
|
|
2470
2715
|
this.durations.push(duration);
|
|
2471
|
-
if (this.durations.length >
|
|
2716
|
+
if (this.durations.length > this.maxStatsSamples) {
|
|
2472
2717
|
this.durations.shift();
|
|
2473
2718
|
}
|
|
2474
2719
|
}
|
|
2475
2720
|
};
|
|
2721
|
+
function percentile(sortedValues, ratio) {
|
|
2722
|
+
if (sortedValues.length === 0) {
|
|
2723
|
+
return 0;
|
|
2724
|
+
}
|
|
2725
|
+
const index = Math.floor(sortedValues.length * ratio);
|
|
2726
|
+
return sortedValues[Math.min(index, sortedValues.length - 1)] ?? 0;
|
|
2727
|
+
}
|
|
2728
|
+
function compactUndefined(value) {
|
|
2729
|
+
const entries = Object.entries(value).filter(([, item]) => item !== void 0);
|
|
2730
|
+
return entries.length === 0 ? void 0 : Object.fromEntries(entries);
|
|
2731
|
+
}
|
|
2476
2732
|
function stringifySessionId(id) {
|
|
2477
2733
|
if (typeof id === "string") {
|
|
2478
2734
|
return id;
|
|
@@ -2509,18 +2765,166 @@ async function sleep(ms) {
|
|
|
2509
2765
|
}
|
|
2510
2766
|
|
|
2511
2767
|
// src/adapters/mongodb/common/connect.ts
|
|
2768
|
+
import { copyFileSync, existsSync, mkdirSync, mkdtempSync, readdirSync, rmSync } from "node:fs";
|
|
2769
|
+
import path from "node:path";
|
|
2512
2770
|
import { MongoClient } from "mongodb";
|
|
2771
|
+
var DEFAULT_MEMORY_SERVER_VERSION = "7.0.14";
|
|
2772
|
+
var MANAGED_DB_PATH_PREFIXES = ["single-", "replset-", "examples-single-", "examples-replset-", "probe-single-", "probe-replset-"];
|
|
2513
2773
|
var _memoryServerInstance = null;
|
|
2774
|
+
var _memoryServerCleanupOptions = { doCleanup: true, force: true };
|
|
2775
|
+
var _memoryServerDbPath = null;
|
|
2776
|
+
var _memoryServerClients = /* @__PURE__ */ new Set();
|
|
2777
|
+
function setDefaultEnv(name, value) {
|
|
2778
|
+
if (!process.env[name]) {
|
|
2779
|
+
process.env[name] = value;
|
|
2780
|
+
}
|
|
2781
|
+
}
|
|
2782
|
+
function sanitizePathSegment(input) {
|
|
2783
|
+
return input.replace(/[^a-zA-Z0-9_.-]+/g, "-").replace(/^-+|-+$/g, "") || "default";
|
|
2784
|
+
}
|
|
2785
|
+
function parseManagedPathPid(name) {
|
|
2786
|
+
if (!MANAGED_DB_PATH_PREFIXES.some((prefix) => name.startsWith(prefix))) {
|
|
2787
|
+
return null;
|
|
2788
|
+
}
|
|
2789
|
+
const match = /-(\d+)-[^-]+$/.exec(name);
|
|
2790
|
+
if (!match) {
|
|
2791
|
+
return null;
|
|
2792
|
+
}
|
|
2793
|
+
const pid = Number.parseInt(match[1], 10);
|
|
2794
|
+
return Number.isFinite(pid) && pid > 0 ? pid : null;
|
|
2795
|
+
}
|
|
2796
|
+
function isProcessAlive(pid) {
|
|
2797
|
+
if (pid === process.pid) {
|
|
2798
|
+
return true;
|
|
2799
|
+
}
|
|
2800
|
+
try {
|
|
2801
|
+
process.kill(pid, 0);
|
|
2802
|
+
return true;
|
|
2803
|
+
} catch (error) {
|
|
2804
|
+
return error.code === "EPERM";
|
|
2805
|
+
}
|
|
2806
|
+
}
|
|
2807
|
+
function pruneManagedDbRoot(dbRoot) {
|
|
2808
|
+
let entries;
|
|
2809
|
+
try {
|
|
2810
|
+
entries = readdirSync(dbRoot, { withFileTypes: true });
|
|
2811
|
+
} catch {
|
|
2812
|
+
return;
|
|
2813
|
+
}
|
|
2814
|
+
for (const entry of entries) {
|
|
2815
|
+
if (!entry.isDirectory()) {
|
|
2816
|
+
continue;
|
|
2817
|
+
}
|
|
2818
|
+
const pid = parseManagedPathPid(entry.name);
|
|
2819
|
+
if (!pid || isProcessAlive(pid)) {
|
|
2820
|
+
continue;
|
|
2821
|
+
}
|
|
2822
|
+
try {
|
|
2823
|
+
rmSync(path.join(dbRoot, entry.name), { recursive: true, force: true });
|
|
2824
|
+
} catch {
|
|
2825
|
+
}
|
|
2826
|
+
}
|
|
2827
|
+
}
|
|
2828
|
+
function resolveMemoryServerBinaryVersion(memoryServerOptions = {}) {
|
|
2829
|
+
return memoryServerOptions?.binary?.version || process.env.MONSQLIZE_REPLSET_BINARY_VERSION || process.env.MONSQLIZE_MEMORY_MONGO_BINARY_VERSION || process.env.MONGOMS_VERSION || DEFAULT_MEMORY_SERVER_VERSION;
|
|
2830
|
+
}
|
|
2831
|
+
function resolveMemoryServerPolicy(binaryVersion) {
|
|
2832
|
+
const cacheRoot = path.resolve(process.env.MONSQLIZE_MEMORY_SERVER_CACHE_DIR || path.join(process.cwd(), ".cache", "mongodb-memory-server"));
|
|
2833
|
+
const downloadDir = path.resolve(process.env.MONGOMS_DOWNLOAD_DIR || path.join(cacheRoot, "binaries"));
|
|
2834
|
+
const dbRoot = path.resolve(process.env.MONSQLIZE_MEMORY_SERVER_DB_DIR || path.join(cacheRoot, "db"));
|
|
2835
|
+
mkdirSync(downloadDir, { recursive: true });
|
|
2836
|
+
mkdirSync(dbRoot, { recursive: true });
|
|
2837
|
+
pruneManagedDbRoot(dbRoot);
|
|
2838
|
+
setDefaultEnv("MONGOMS_DOWNLOAD_DIR", downloadDir);
|
|
2839
|
+
setDefaultEnv("MONGOMS_PREFER_GLOBAL_PATH", "false");
|
|
2840
|
+
setDefaultEnv("MONGOMS_RUNTIME_DOWNLOAD", "true");
|
|
2841
|
+
setDefaultEnv("MONGOMS_VERSION", binaryVersion);
|
|
2842
|
+
return { downloadDir, dbRoot };
|
|
2843
|
+
}
|
|
2844
|
+
function createManagedDbPath(dbRoot, dbName) {
|
|
2845
|
+
return mkdtempSync(path.join(dbRoot, `replset-${sanitizePathSegment(dbName)}-${process.pid}-`));
|
|
2846
|
+
}
|
|
2847
|
+
function isManagedCleanupError(error, dbPath) {
|
|
2848
|
+
if (!error || typeof error !== "object") {
|
|
2849
|
+
return false;
|
|
2850
|
+
}
|
|
2851
|
+
const candidate = error;
|
|
2852
|
+
if (!candidate.code || !["ENOTEMPTY", "EBUSY", "EPERM", "ENOENT"].includes(candidate.code)) {
|
|
2853
|
+
return false;
|
|
2854
|
+
}
|
|
2855
|
+
return !candidate.path || path.resolve(candidate.path).startsWith(path.resolve(dbPath));
|
|
2856
|
+
}
|
|
2857
|
+
function delay(ms) {
|
|
2858
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
2859
|
+
}
|
|
2860
|
+
async function cleanupManagedDbPath(dbPath) {
|
|
2861
|
+
if (!dbPath) {
|
|
2862
|
+
return true;
|
|
2863
|
+
}
|
|
2864
|
+
for (const waitMs of [0, 50, 100, 200, 400, 800]) {
|
|
2865
|
+
if (waitMs > 0) {
|
|
2866
|
+
await delay(waitMs);
|
|
2867
|
+
}
|
|
2868
|
+
try {
|
|
2869
|
+
rmSync(dbPath, { recursive: true, force: true });
|
|
2870
|
+
if (!existsSync(dbPath)) {
|
|
2871
|
+
return true;
|
|
2872
|
+
}
|
|
2873
|
+
} catch {
|
|
2874
|
+
}
|
|
2875
|
+
}
|
|
2876
|
+
return !existsSync(dbPath);
|
|
2877
|
+
}
|
|
2878
|
+
function resolveLaunchTimeout() {
|
|
2879
|
+
const raw = process.env.MONSQLIZE_MEMORY_MONGO_LAUNCH_TIMEOUT_MS;
|
|
2880
|
+
if (!raw) {
|
|
2881
|
+
return void 0;
|
|
2882
|
+
}
|
|
2883
|
+
const value = Number.parseInt(raw, 10);
|
|
2884
|
+
return Number.isFinite(value) && value > 0 ? value : void 0;
|
|
2885
|
+
}
|
|
2886
|
+
async function seedMemoryServerBinaryCache(binaryVersion, downloadDir) {
|
|
2887
|
+
try {
|
|
2888
|
+
const { DryMongoBinary } = __require("mongodb-memory-server-core/lib/util/DryMongoBinary");
|
|
2889
|
+
const options = await DryMongoBinary.generateOptions({ version: binaryVersion, downloadDir });
|
|
2890
|
+
options.downloadDir = downloadDir;
|
|
2891
|
+
const paths = await DryMongoBinary.generatePaths(options);
|
|
2892
|
+
if (paths.resolveConfig && paths.homeCache && path.resolve(paths.resolveConfig) !== path.resolve(paths.homeCache) && existsSync(paths.homeCache) && !existsSync(paths.resolveConfig)) {
|
|
2893
|
+
mkdirSync(path.dirname(paths.resolveConfig), { recursive: true });
|
|
2894
|
+
copyFileSync(paths.homeCache, paths.resolveConfig);
|
|
2895
|
+
}
|
|
2896
|
+
} catch {
|
|
2897
|
+
}
|
|
2898
|
+
}
|
|
2514
2899
|
async function startMemoryServer(logger, memoryServerOptions = {}) {
|
|
2515
2900
|
if (_memoryServerInstance) {
|
|
2516
2901
|
return _memoryServerInstance.getUri();
|
|
2517
2902
|
}
|
|
2903
|
+
const binaryVersion = resolveMemoryServerBinaryVersion(memoryServerOptions);
|
|
2904
|
+
const { dbRoot, downloadDir } = resolveMemoryServerPolicy(binaryVersion);
|
|
2905
|
+
await seedMemoryServerBinaryCache(binaryVersion, downloadDir);
|
|
2518
2906
|
const { MongoMemoryReplSet } = __require("mongodb-memory-server");
|
|
2519
|
-
logger?.info?.("
|
|
2907
|
+
logger?.info?.("Starting MongoDB Memory ReplSet", { binaryVersion });
|
|
2908
|
+
const dbName = memoryServerOptions?.instance?.dbName || "monsqlize_memory";
|
|
2909
|
+
const instanceConfig = { ...memoryServerOptions?.instance ?? {} };
|
|
2910
|
+
const hasUserDbPath = typeof instanceConfig.dbPath === "string" && instanceConfig.dbPath.length > 0;
|
|
2911
|
+
if (!hasUserDbPath) {
|
|
2912
|
+
_memoryServerDbPath = createManagedDbPath(dbRoot, dbName);
|
|
2913
|
+
instanceConfig.dbPath = _memoryServerDbPath;
|
|
2914
|
+
} else {
|
|
2915
|
+
_memoryServerDbPath = null;
|
|
2916
|
+
}
|
|
2917
|
+
if (instanceConfig.launchTimeout === void 0) {
|
|
2918
|
+
const launchTimeout = resolveLaunchTimeout();
|
|
2919
|
+
if (launchTimeout) {
|
|
2920
|
+
instanceConfig.launchTimeout = launchTimeout;
|
|
2921
|
+
}
|
|
2922
|
+
}
|
|
2923
|
+
_memoryServerCleanupOptions = { doCleanup: true, force: !hasUserDbPath };
|
|
2520
2924
|
const defaultConfig = {
|
|
2521
2925
|
replSet: { count: 1, storageEngine: "wiredTiger" },
|
|
2522
|
-
binary: { version:
|
|
2523
|
-
instanceOpts: [
|
|
2926
|
+
binary: { version: binaryVersion },
|
|
2927
|
+
instanceOpts: [instanceConfig]
|
|
2524
2928
|
};
|
|
2525
2929
|
const resolvedConfig = {
|
|
2526
2930
|
...defaultConfig,
|
|
@@ -2529,11 +2933,43 @@ async function startMemoryServer(logger, memoryServerOptions = {}) {
|
|
|
2529
2933
|
try {
|
|
2530
2934
|
_memoryServerInstance = await MongoMemoryReplSet.create(resolvedConfig);
|
|
2531
2935
|
const uri = _memoryServerInstance.getUri();
|
|
2532
|
-
logger?.info?.("
|
|
2936
|
+
logger?.info?.("MongoDB Memory ReplSet started", { uri });
|
|
2533
2937
|
return uri;
|
|
2534
2938
|
} catch (err) {
|
|
2535
|
-
|
|
2536
|
-
|
|
2939
|
+
if (!hasUserDbPath) {
|
|
2940
|
+
await cleanupManagedDbPath(_memoryServerDbPath);
|
|
2941
|
+
_memoryServerDbPath = null;
|
|
2942
|
+
}
|
|
2943
|
+
logger?.error?.("Failed to start MongoDB Memory ReplSet", err);
|
|
2944
|
+
throw createConnectionError(
|
|
2945
|
+
`Failed to start MongoDB Memory ReplSet: ${err.message}`,
|
|
2946
|
+
err instanceof Error ? err : void 0
|
|
2947
|
+
);
|
|
2948
|
+
}
|
|
2949
|
+
}
|
|
2950
|
+
async function stopMemoryServer(logger) {
|
|
2951
|
+
if (!_memoryServerInstance) {
|
|
2952
|
+
return;
|
|
2953
|
+
}
|
|
2954
|
+
const instance = _memoryServerInstance;
|
|
2955
|
+
const dbPath = _memoryServerDbPath;
|
|
2956
|
+
_memoryServerInstance = null;
|
|
2957
|
+
_memoryServerDbPath = null;
|
|
2958
|
+
let stopError = null;
|
|
2959
|
+
try {
|
|
2960
|
+
await instance.stop(_memoryServerCleanupOptions);
|
|
2961
|
+
logger?.info?.("MongoDB Memory ReplSet stopped");
|
|
2962
|
+
} catch (cause) {
|
|
2963
|
+
stopError = cause;
|
|
2964
|
+
logger?.warn?.("Failed to stop MongoDB Memory ReplSet cleanly.", cause);
|
|
2965
|
+
} finally {
|
|
2966
|
+
if (_memoryServerCleanupOptions.force) {
|
|
2967
|
+
const cleaned = await cleanupManagedDbPath(dbPath);
|
|
2968
|
+
if (!cleaned && (!stopError || isManagedCleanupError(stopError, dbPath ?? ""))) {
|
|
2969
|
+
logger?.warn?.("Failed to remove MongoDB Memory ReplSet dbPath.", { dbPath });
|
|
2970
|
+
}
|
|
2971
|
+
}
|
|
2972
|
+
_memoryServerCleanupOptions = { doCleanup: true, force: true };
|
|
2537
2973
|
}
|
|
2538
2974
|
}
|
|
2539
2975
|
async function connectMongo(params) {
|
|
@@ -2542,13 +2978,15 @@ async function connectMongo(params) {
|
|
|
2542
2978
|
throw createError(ErrorCodes.INVALID_DATABASE_NAME, "Database name must be a non-empty string.");
|
|
2543
2979
|
}
|
|
2544
2980
|
let effectiveUri = params.config?.uri?.trim();
|
|
2981
|
+
let usesManagedMemoryServer = false;
|
|
2545
2982
|
if (!effectiveUri && params.config?.useMemoryServer === true) {
|
|
2546
2983
|
if (process.env["MONSQLIZE_USE_SYSTEM_MONGO"] === "true") {
|
|
2547
2984
|
const systemUri = process.env["MONSQLIZE_SYSTEM_MONGO_URI"] ?? "mongodb://127.0.0.1:27017";
|
|
2548
|
-
params.logger?.info?.("
|
|
2985
|
+
params.logger?.info?.("Using system MongoDB instead of memory server", { uri: systemUri });
|
|
2549
2986
|
effectiveUri = systemUri;
|
|
2550
2987
|
} else {
|
|
2551
2988
|
effectiveUri = await startMemoryServer(params.logger, params.config.memoryServerOptions);
|
|
2989
|
+
usesManagedMemoryServer = true;
|
|
2552
2990
|
}
|
|
2553
2991
|
}
|
|
2554
2992
|
if (!effectiveUri) {
|
|
@@ -2562,6 +3000,9 @@ async function connectMongo(params) {
|
|
|
2562
3000
|
try {
|
|
2563
3001
|
await client.connect();
|
|
2564
3002
|
const db = client.db(databaseName);
|
|
3003
|
+
if (usesManagedMemoryServer) {
|
|
3004
|
+
_memoryServerClients.add(client);
|
|
3005
|
+
}
|
|
2565
3006
|
params.logger?.info?.("MongoDB connected", { databaseName });
|
|
2566
3007
|
return { client, db };
|
|
2567
3008
|
} catch (cause) {
|
|
@@ -2569,6 +3010,9 @@ async function connectMongo(params) {
|
|
|
2569
3010
|
await client.close();
|
|
2570
3011
|
} catch {
|
|
2571
3012
|
}
|
|
3013
|
+
if (usesManagedMemoryServer && _memoryServerClients.size === 0) {
|
|
3014
|
+
await stopMemoryServer(params.logger);
|
|
3015
|
+
}
|
|
2572
3016
|
throw createConnectionError(
|
|
2573
3017
|
`Failed to connect to MongoDB database: ${databaseName}`,
|
|
2574
3018
|
cause instanceof Error ? cause : void 0
|
|
@@ -2579,17 +3023,26 @@ async function closeMongo(client, logger) {
|
|
|
2579
3023
|
if (!client) {
|
|
2580
3024
|
return;
|
|
2581
3025
|
}
|
|
3026
|
+
const shouldReleaseMemoryServer = _memoryServerClients.delete(client);
|
|
3027
|
+
let closeError = null;
|
|
2582
3028
|
try {
|
|
2583
3029
|
await client.close();
|
|
2584
3030
|
logger?.info?.("MongoDB connection closed");
|
|
2585
3031
|
} catch (cause) {
|
|
2586
|
-
|
|
3032
|
+
closeError = createError(
|
|
2587
3033
|
ErrorCodes.CONNECTION_CLOSED,
|
|
2588
3034
|
"Failed to close MongoDB connection cleanly.",
|
|
2589
3035
|
void 0,
|
|
2590
3036
|
cause instanceof Error ? cause : void 0
|
|
2591
3037
|
);
|
|
2592
|
-
logger?.warn?.(
|
|
3038
|
+
logger?.warn?.(closeError.message, closeError.cause);
|
|
3039
|
+
} finally {
|
|
3040
|
+
if (shouldReleaseMemoryServer && _memoryServerClients.size === 0) {
|
|
3041
|
+
await stopMemoryServer(logger);
|
|
3042
|
+
}
|
|
3043
|
+
}
|
|
3044
|
+
if (closeError) {
|
|
3045
|
+
return;
|
|
2593
3046
|
}
|
|
2594
3047
|
}
|
|
2595
3048
|
|
|
@@ -2601,8 +3054,9 @@ function loadSsh2Client() {
|
|
|
2601
3054
|
try {
|
|
2602
3055
|
return __require("ssh2").Client;
|
|
2603
3056
|
} catch {
|
|
2604
|
-
throw
|
|
2605
|
-
|
|
3057
|
+
throw createError(
|
|
3058
|
+
ErrorCodes.INVALID_CONFIG,
|
|
3059
|
+
"Unable to load ssh2. monsqlize installs ssh2 by default; check that the package installation is complete and that your runtime can resolve bundled dependencies."
|
|
2606
3060
|
);
|
|
2607
3061
|
}
|
|
2608
3062
|
}
|
|
@@ -2630,10 +3084,10 @@ var SSHTunnelSSH2 = class {
|
|
|
2630
3084
|
keepaliveInterval = 3e4
|
|
2631
3085
|
} = this._sshConfig;
|
|
2632
3086
|
if (!host || !username) {
|
|
2633
|
-
throw
|
|
3087
|
+
throw createError(ErrorCodes.INVALID_CONFIG, "SSH config requires: host, username");
|
|
2634
3088
|
}
|
|
2635
3089
|
if (!password && !privateKey && !privateKeyPath) {
|
|
2636
|
-
throw
|
|
3090
|
+
throw createError(ErrorCodes.INVALID_CONFIG, "SSH authentication required: provide password, privateKey, or privateKeyPath");
|
|
2637
3091
|
}
|
|
2638
3092
|
const config = { host, port, username, readyTimeout, keepaliveInterval };
|
|
2639
3093
|
if (password) {
|
|
@@ -2708,7 +3162,7 @@ var SSHTunnelSSH2 = class {
|
|
|
2708
3162
|
}
|
|
2709
3163
|
getTunnelUri(_protocol, originalUri) {
|
|
2710
3164
|
if (!this.isConnected || this.localPort === null) {
|
|
2711
|
-
throw
|
|
3165
|
+
throw createError(ErrorCodes.NOT_CONNECTED, `SSH tunnel [${this.remoteHost}:${this.remotePort}] not connected`);
|
|
2712
3166
|
}
|
|
2713
3167
|
return originalUri.replace(
|
|
2714
3168
|
`${this.remoteHost}:${this.remotePort}`,
|
|
@@ -2717,7 +3171,7 @@ var SSHTunnelSSH2 = class {
|
|
|
2717
3171
|
}
|
|
2718
3172
|
getLocalAddress() {
|
|
2719
3173
|
if (!this.isConnected || this.localPort === null) {
|
|
2720
|
-
throw
|
|
3174
|
+
throw createError(ErrorCodes.NOT_CONNECTED, `SSH tunnel [${this.remoteHost}:${this.remotePort}] not connected`);
|
|
2721
3175
|
}
|
|
2722
3176
|
return `localhost:${this.localPort}`;
|
|
2723
3177
|
}
|
|
@@ -3166,7 +3620,7 @@ var HealthChecker = class {
|
|
|
3166
3620
|
async _pingPool(poolName, timeout) {
|
|
3167
3621
|
const stored = this._clients.get(poolName);
|
|
3168
3622
|
const client = stored ?? this._poolManager?._getPool(poolName);
|
|
3169
|
-
if (!client) throw
|
|
3623
|
+
if (!client) throw createError(ErrorCodes.POOL_NOT_FOUND, `No client for pool: ${poolName}`);
|
|
3170
3624
|
const db = client.db("admin");
|
|
3171
3625
|
const pingFn = db.command ? () => db.command({ ping: 1 }) : () => db.admin().ping();
|
|
3172
3626
|
const timeoutPromise = new Promise((_, reject) => setTimeout(() => reject(new Error("Ping timeout")), timeout));
|
|
@@ -3198,7 +3652,7 @@ var PoolSelector = class {
|
|
|
3198
3652
|
}
|
|
3199
3653
|
select(pools, context) {
|
|
3200
3654
|
if (!pools || pools.length === 0) {
|
|
3201
|
-
throw
|
|
3655
|
+
throw createError(ErrorCodes.INVALID_OPERATION, "No available pools");
|
|
3202
3656
|
}
|
|
3203
3657
|
switch (this._strategy) {
|
|
3204
3658
|
case "auto":
|
|
@@ -3219,7 +3673,10 @@ var PoolSelector = class {
|
|
|
3219
3673
|
selectByAuto(pools, context) {
|
|
3220
3674
|
const { operation, poolPreference } = context;
|
|
3221
3675
|
let candidates = pools;
|
|
3222
|
-
|
|
3676
|
+
const preferred = this.filterByPreference(pools, poolPreference);
|
|
3677
|
+
if (preferred.length > 0) {
|
|
3678
|
+
candidates = preferred;
|
|
3679
|
+
} else if (operation === "read") {
|
|
3223
3680
|
const secondaries = pools.filter((pool) => pool.role === "secondary");
|
|
3224
3681
|
if (secondaries.length > 0) {
|
|
3225
3682
|
candidates = secondaries;
|
|
@@ -3230,10 +3687,19 @@ var PoolSelector = class {
|
|
|
3230
3687
|
candidates = primaries;
|
|
3231
3688
|
}
|
|
3232
3689
|
}
|
|
3690
|
+
if (candidates.length === 1) {
|
|
3691
|
+
return candidates[0].name;
|
|
3692
|
+
}
|
|
3693
|
+
return this.selectByWeighted(candidates);
|
|
3694
|
+
}
|
|
3695
|
+
filterByPreference(pools, poolPreference) {
|
|
3696
|
+
let candidates = pools;
|
|
3697
|
+
let applied = false;
|
|
3233
3698
|
if (poolPreference?.role) {
|
|
3234
3699
|
const filteredByRole = candidates.filter((pool) => pool.role === poolPreference.role);
|
|
3235
3700
|
if (filteredByRole.length > 0) {
|
|
3236
3701
|
candidates = filteredByRole;
|
|
3702
|
+
applied = true;
|
|
3237
3703
|
}
|
|
3238
3704
|
}
|
|
3239
3705
|
if (poolPreference?.tags?.length) {
|
|
@@ -3246,12 +3712,10 @@ var PoolSelector = class {
|
|
|
3246
3712
|
});
|
|
3247
3713
|
if (filteredByTags.length > 0) {
|
|
3248
3714
|
candidates = filteredByTags;
|
|
3715
|
+
applied = true;
|
|
3249
3716
|
}
|
|
3250
3717
|
}
|
|
3251
|
-
|
|
3252
|
-
return candidates[0].name;
|
|
3253
|
-
}
|
|
3254
|
-
return this.selectByWeighted(candidates);
|
|
3718
|
+
return applied ? candidates : [];
|
|
3255
3719
|
}
|
|
3256
3720
|
selectByRoundRobin(pools, context) {
|
|
3257
3721
|
let candidates = pools;
|
|
@@ -3411,43 +3875,43 @@ var DEFAULT_POOL_CONNECT_OPTIONS = {
|
|
|
3411
3875
|
serverSelectionTimeoutMS: 5e3
|
|
3412
3876
|
};
|
|
3413
3877
|
function validatePoolConfig(config) {
|
|
3414
|
-
if (!config || typeof config !== "object") throw
|
|
3415
|
-
if (!config.name || typeof config.name !== "string") throw
|
|
3416
|
-
if (!config.uri || typeof config.uri !== "string") throw
|
|
3878
|
+
if (!config || typeof config !== "object") throw createError(ErrorCodes.INVALID_CONFIG, "Pool config must be an object");
|
|
3879
|
+
if (!config.name || typeof config.name !== "string") throw createError(ErrorCodes.INVALID_CONFIG, "Pool config.name is required and must be a string");
|
|
3880
|
+
if (!config.uri || typeof config.uri !== "string") throw createError(ErrorCodes.INVALID_CONFIG, "Pool config.uri is required and must be a string");
|
|
3417
3881
|
if (!config.uri.startsWith("mongodb://") && !config.uri.startsWith("mongodb+srv://")) {
|
|
3418
|
-
throw
|
|
3882
|
+
throw createError(ErrorCodes.INVALID_CONFIG, "Pool config.uri must start with mongodb:// or mongodb+srv://");
|
|
3419
3883
|
}
|
|
3420
3884
|
if (config.role) {
|
|
3421
3885
|
const validRoles = ["primary", "secondary", "analytics", "custom"];
|
|
3422
|
-
if (!validRoles.includes(config.role)) throw
|
|
3886
|
+
if (!validRoles.includes(config.role)) throw createError(ErrorCodes.INVALID_CONFIG, `Pool config.role must be one of: ${validRoles.join(", ")}`);
|
|
3423
3887
|
}
|
|
3424
3888
|
if (config.weight !== void 0) {
|
|
3425
|
-
if (typeof config.weight !== "number") throw
|
|
3426
|
-
if (config.weight < 0) throw
|
|
3889
|
+
if (typeof config.weight !== "number") throw createError(ErrorCodes.INVALID_CONFIG, "Pool config.weight must be a number");
|
|
3890
|
+
if (config.weight < 0) throw createError(ErrorCodes.INVALID_CONFIG, "Pool config.weight must be a non-negative number");
|
|
3427
3891
|
}
|
|
3428
3892
|
if (config.options !== void 0) {
|
|
3429
|
-
if (typeof config.options !== "object" || Array.isArray(config.options)) throw
|
|
3893
|
+
if (typeof config.options !== "object" || Array.isArray(config.options)) throw createError(ErrorCodes.INVALID_CONFIG, "Pool config.options must be an object");
|
|
3430
3894
|
const opts = config.options;
|
|
3431
3895
|
for (const key of ["maxPoolSize", "minPoolSize", "maxIdleTimeMS", "waitQueueTimeoutMS", "connectTimeoutMS", "serverSelectionTimeoutMS"]) {
|
|
3432
3896
|
if (opts[key] !== void 0 && (typeof opts[key] !== "number" || opts[key] < 0)) {
|
|
3433
|
-
throw
|
|
3897
|
+
throw createError(ErrorCodes.INVALID_CONFIG, `Pool config.options.${key} must be a non-negative number`);
|
|
3434
3898
|
}
|
|
3435
3899
|
}
|
|
3436
3900
|
}
|
|
3437
3901
|
if (config.healthCheck !== void 0) {
|
|
3438
|
-
if (typeof config.healthCheck !== "object" || Array.isArray(config.healthCheck)) throw
|
|
3902
|
+
if (typeof config.healthCheck !== "object" || Array.isArray(config.healthCheck)) throw createError(ErrorCodes.INVALID_CONFIG, "Pool config.healthCheck must be an object");
|
|
3439
3903
|
const hc = config.healthCheck;
|
|
3440
|
-
if (hc.enabled !== void 0 && typeof hc.enabled !== "boolean") throw
|
|
3904
|
+
if (hc.enabled !== void 0 && typeof hc.enabled !== "boolean") throw createError(ErrorCodes.INVALID_CONFIG, "Pool config.healthCheck.enabled must be a boolean");
|
|
3441
3905
|
for (const key of ["interval", "timeout", "retries"]) {
|
|
3442
3906
|
if (hc[key] !== void 0 && (typeof hc[key] !== "number" || hc[key] < 0)) {
|
|
3443
|
-
throw
|
|
3907
|
+
throw createError(ErrorCodes.INVALID_CONFIG, `Pool config.healthCheck.${key} must be a non-negative number`);
|
|
3444
3908
|
}
|
|
3445
3909
|
}
|
|
3446
3910
|
}
|
|
3447
3911
|
if (config.tags !== void 0) {
|
|
3448
|
-
if (!Array.isArray(config.tags)) throw
|
|
3912
|
+
if (!Array.isArray(config.tags)) throw createError(ErrorCodes.INVALID_CONFIG, "Pool config.tags must be an array");
|
|
3449
3913
|
for (const tag of config.tags) {
|
|
3450
|
-
if (typeof tag !== "string") throw
|
|
3914
|
+
if (typeof tag !== "string") throw createError(ErrorCodes.INVALID_CONFIG, "Pool config.tags must be an array of strings");
|
|
3451
3915
|
}
|
|
3452
3916
|
}
|
|
3453
3917
|
}
|
|
@@ -3506,10 +3970,10 @@ function validatePoolConfigSafe(config) {
|
|
|
3506
3970
|
}
|
|
3507
3971
|
function validatePoolConfigInternal(config) {
|
|
3508
3972
|
if (!config.name?.trim()) {
|
|
3509
|
-
throw
|
|
3973
|
+
throw createError(ErrorCodes.INVALID_CONFIG, "Pool config requires a non-empty name");
|
|
3510
3974
|
}
|
|
3511
3975
|
if (!config.uri?.trim()) {
|
|
3512
|
-
throw
|
|
3976
|
+
throw createError(ErrorCodes.INVALID_CONFIG, "Pool config requires a non-empty uri");
|
|
3513
3977
|
}
|
|
3514
3978
|
}
|
|
3515
3979
|
function createEmptyPoolStats(name) {
|
|
@@ -3586,10 +4050,10 @@ var ConnectionPoolManager = class {
|
|
|
3586
4050
|
async addPool(config) {
|
|
3587
4051
|
validatePoolConfigInternal(config);
|
|
3588
4052
|
if (this.pools.has(config.name) || this._pendingAdds.has(config.name)) {
|
|
3589
|
-
throw
|
|
4053
|
+
throw createError(ErrorCodes.INVALID_CONFIG, `Pool '${config.name}' already exists`);
|
|
3590
4054
|
}
|
|
3591
4055
|
if (this.maxPoolsCount > 0 && this.pools.size >= this.maxPoolsCount) {
|
|
3592
|
-
throw
|
|
4056
|
+
throw createError(ErrorCodes.INVALID_CONFIG, `Maximum pool count (${this.maxPoolsCount}) reached`);
|
|
3593
4057
|
}
|
|
3594
4058
|
this._pendingAdds.add(config.name);
|
|
3595
4059
|
try {
|
|
@@ -3597,7 +4061,7 @@ var ConnectionPoolManager = class {
|
|
|
3597
4061
|
if (this.pools.has(config.name)) {
|
|
3598
4062
|
await client.close().catch(() => {
|
|
3599
4063
|
});
|
|
3600
|
-
throw
|
|
4064
|
+
throw createError(ErrorCodes.INVALID_CONFIG, `Pool '${config.name}' already exists`);
|
|
3601
4065
|
}
|
|
3602
4066
|
this.pools.set(config.name, {
|
|
3603
4067
|
client,
|
|
@@ -3636,7 +4100,7 @@ var ConnectionPoolManager = class {
|
|
|
3636
4100
|
async removePool(name) {
|
|
3637
4101
|
const pool = this.pools.get(name);
|
|
3638
4102
|
if (!pool) {
|
|
3639
|
-
throw
|
|
4103
|
+
throw createError(ErrorCodes.POOL_NOT_FOUND, `Pool '${name}' not found`);
|
|
3640
4104
|
}
|
|
3641
4105
|
this.stopHealthCheck(name);
|
|
3642
4106
|
await pool.client.close();
|
|
@@ -3660,27 +4124,28 @@ var ConnectionPoolManager = class {
|
|
|
3660
4124
|
selectPool(operation, options = {}) {
|
|
3661
4125
|
if (options.pool) {
|
|
3662
4126
|
const poolData2 = this.pools.get(options.pool);
|
|
3663
|
-
if (!poolData2) throw
|
|
4127
|
+
if (!poolData2) throw createError(ErrorCodes.POOL_NOT_FOUND, `Pool '${options.pool}' not found`);
|
|
3664
4128
|
return this._createPoolResult(options.pool, poolData2.client);
|
|
3665
4129
|
}
|
|
3666
4130
|
let candidates = this._getHealthyPools();
|
|
3667
4131
|
if (candidates.length === 0) {
|
|
3668
4132
|
if (!this.fallback.enabled) {
|
|
3669
|
-
throw
|
|
4133
|
+
throw createError(ErrorCodes.INVALID_OPERATION, "No available connection pool");
|
|
3670
4134
|
}
|
|
3671
4135
|
candidates = this._handleAllPoolsDown(operation);
|
|
3672
4136
|
if (candidates.length === 0) {
|
|
3673
|
-
throw
|
|
4137
|
+
throw createError(ErrorCodes.INVALID_OPERATION, "No available connection pool");
|
|
3674
4138
|
}
|
|
3675
4139
|
}
|
|
4140
|
+
const poolPreference = options.poolPreference ?? (options.tags?.length ? { tags: options.tags } : void 0);
|
|
3676
4141
|
const poolName = this._selector.select(candidates, {
|
|
3677
4142
|
operation,
|
|
3678
4143
|
stats: this._stats.getAllStats(),
|
|
3679
|
-
poolPreference
|
|
4144
|
+
poolPreference
|
|
3680
4145
|
});
|
|
3681
4146
|
const poolData = this.pools.get(poolName);
|
|
3682
4147
|
if (!poolData) {
|
|
3683
|
-
throw
|
|
4148
|
+
throw createError(ErrorCodes.INVALID_OPERATION, `Selected pool '${poolName}' not available`);
|
|
3684
4149
|
}
|
|
3685
4150
|
this._stats.recordSelection(poolName, operation);
|
|
3686
4151
|
this.recordSelection(poolName, true);
|
|
@@ -3803,10 +4268,12 @@ var ConnectionPoolManager = class {
|
|
|
3803
4268
|
_getHealthyPools() {
|
|
3804
4269
|
const result = [];
|
|
3805
4270
|
for (const [name, config] of this._configs.entries()) {
|
|
3806
|
-
const
|
|
3807
|
-
|
|
3808
|
-
|
|
4271
|
+
const compatStatus = this._healthChecker.getStatus(name);
|
|
4272
|
+
const publicStatus = this.healthStatus.get(name);
|
|
4273
|
+
if (compatStatus?.status === "down" || publicStatus?.status === "down") {
|
|
4274
|
+
continue;
|
|
3809
4275
|
}
|
|
4276
|
+
result.push(config);
|
|
3810
4277
|
}
|
|
3811
4278
|
return result;
|
|
3812
4279
|
}
|
|
@@ -3959,7 +4426,7 @@ async function initializeDistributedCacheInvalidator(options, cache, logger) {
|
|
|
3959
4426
|
logger
|
|
3960
4427
|
});
|
|
3961
4428
|
} catch (err) {
|
|
3962
|
-
logger.warn?.("[Cache] Failed to initialize distributed cache invalidator \u2014
|
|
4429
|
+
logger.warn?.("[Cache] Failed to initialize distributed cache invalidator \u2014 check Redis config or package installation completeness.", err);
|
|
3963
4430
|
return null;
|
|
3964
4431
|
}
|
|
3965
4432
|
}
|
|
@@ -3967,7 +4434,7 @@ async function loadModelFiles(options, logger, opts = {}) {
|
|
|
3967
4434
|
const modelsConfig = options.models;
|
|
3968
4435
|
if (!modelsConfig) return;
|
|
3969
4436
|
if (typeof modelsConfig !== "string" && typeof modelsConfig !== "object") return;
|
|
3970
|
-
const { readdirSync } = await import("node:fs");
|
|
4437
|
+
const { readdirSync: readdirSync2 } = await import("node:fs");
|
|
3971
4438
|
const { resolve, join, isAbsolute } = await import("node:path");
|
|
3972
4439
|
const { createRequire } = await import("node:module");
|
|
3973
4440
|
let targetPath;
|
|
@@ -3991,7 +4458,7 @@ async function loadModelFiles(options, logger, opts = {}) {
|
|
|
3991
4458
|
const collectFiles = (dir) => {
|
|
3992
4459
|
let entries;
|
|
3993
4460
|
try {
|
|
3994
|
-
entries =
|
|
4461
|
+
entries = readdirSync2(dir, { withFileTypes: true });
|
|
3995
4462
|
} catch {
|
|
3996
4463
|
logger.warn?.(`[Models] cannot read directory: ${dir}`);
|
|
3997
4464
|
return [];
|
|
@@ -4470,7 +4937,7 @@ async function indexStatsForAccessor(collectionRef) {
|
|
|
4470
4937
|
}
|
|
4471
4938
|
async function setValidatorForAccessor(collectionRef, collectionName, dbRef, validator, options = {}) {
|
|
4472
4939
|
if (validator === null || typeof validator !== "object") {
|
|
4473
|
-
throw
|
|
4940
|
+
throw createError(ErrorCodes.INVALID_ARGUMENT, "Validator must be a non-null object");
|
|
4474
4941
|
}
|
|
4475
4942
|
const isEmptyValidator = Object.keys(validator).length === 0;
|
|
4476
4943
|
const command = {
|
|
@@ -4491,14 +4958,14 @@ async function setValidatorForAccessor(collectionRef, collectionName, dbRef, val
|
|
|
4491
4958
|
}
|
|
4492
4959
|
async function setValidationLevelForAccessor(collectionRef, collectionName, dbRef, level) {
|
|
4493
4960
|
if (typeof level !== "string" || !["off", "strict", "moderate"].includes(level)) {
|
|
4494
|
-
throw
|
|
4961
|
+
throw createError(ErrorCodes.INVALID_ARGUMENT, 'Invalid validation level: must be "off", "strict", or "moderate"');
|
|
4495
4962
|
}
|
|
4496
4963
|
const result = await resolveDb(collectionRef, dbRef).command({ collMod: collectionName, validationLevel: level });
|
|
4497
4964
|
return { ok: result["ok"], validationLevel: level };
|
|
4498
4965
|
}
|
|
4499
4966
|
async function setValidationActionForAccessor(collectionRef, collectionName, dbRef, action) {
|
|
4500
4967
|
if (typeof action !== "string" || !["error", "warn"].includes(action)) {
|
|
4501
|
-
throw
|
|
4968
|
+
throw createError(ErrorCodes.INVALID_ARGUMENT, 'Invalid validation action: must be "error" or "warn"');
|
|
4502
4969
|
}
|
|
4503
4970
|
const result = await resolveDb(collectionRef, dbRef).command({ collMod: collectionName, validationAction: action });
|
|
4504
4971
|
return { ok: result["ok"], validationAction: action };
|
|
@@ -4530,14 +4997,14 @@ async function statsForAccessor(collectionRef, dbName, collectionName, options =
|
|
|
4530
4997
|
}
|
|
4531
4998
|
async function renameCollectionForAccessor(collectionRef, collectionName, newName, options = {}) {
|
|
4532
4999
|
if (!newName || typeof newName !== "string") {
|
|
4533
|
-
throw
|
|
5000
|
+
throw createError(ErrorCodes.INVALID_ARGUMENT, "New collection name is required and must be a non-empty string");
|
|
4534
5001
|
}
|
|
4535
5002
|
await collectionRef.rename(newName, { dropTarget: options.dropTarget ?? false });
|
|
4536
5003
|
return { renamed: true, from: collectionName, to: newName };
|
|
4537
5004
|
}
|
|
4538
5005
|
async function collModForAccessor(collectionRef, collectionName, dbRef, modifications) {
|
|
4539
5006
|
if (modifications === null || typeof modifications !== "object") {
|
|
4540
|
-
throw
|
|
5007
|
+
throw createError(ErrorCodes.INVALID_ARGUMENT, "Modifications must be a non-null object");
|
|
4541
5008
|
}
|
|
4542
5009
|
return resolveDb(collectionRef, dbRef).command({
|
|
4543
5010
|
collMod: collectionName,
|
|
@@ -4546,10 +5013,10 @@ async function collModForAccessor(collectionRef, collectionName, dbRef, modifica
|
|
|
4546
5013
|
}
|
|
4547
5014
|
async function convertToCappedForAccessor(collectionRef, collectionName, dbRef, size, options = {}) {
|
|
4548
5015
|
if (typeof size !== "number") {
|
|
4549
|
-
throw
|
|
5016
|
+
throw createError(ErrorCodes.INVALID_ARGUMENT, "Size must be a number");
|
|
4550
5017
|
}
|
|
4551
5018
|
if (size <= 0) {
|
|
4552
|
-
throw
|
|
5019
|
+
throw createError(ErrorCodes.INVALID_ARGUMENT, "Size must be a positive number");
|
|
4553
5020
|
}
|
|
4554
5021
|
const command = { convertToCapped: collectionName, size };
|
|
4555
5022
|
if (options.max !== void 0) {
|
|
@@ -4730,7 +5197,7 @@ function dispatchFunction(name, argsStr) {
|
|
|
4730
5197
|
}
|
|
4731
5198
|
case "REDUCE": {
|
|
4732
5199
|
const lambdaMatch = /\((\w+),\s*(\w+)\)\s*=>\s*(.+)/.exec(args[2]);
|
|
4733
|
-
if (!lambdaMatch) throw
|
|
5200
|
+
if (!lambdaMatch) throw createError(ErrorCodes.INVALID_EXPRESSION, "REDUCE requires a lambda: (acc, item) => expr");
|
|
4734
5201
|
const [, accVar, itemVar, lambdaExpr] = lambdaMatch;
|
|
4735
5202
|
const compiledExpr = lambdaExpr.replace(new RegExp(`\\b${accVar}\\b`, "g"), "$$value").replace(new RegExp(`\\b${itemVar}\\b`, "g"), "$$this");
|
|
4736
5203
|
return { $reduce: { input: parseValue(args[0]), initialValue: parseValue(args[1]), in: compileInnerExpression(compiledExpr) } };
|
|
@@ -4875,7 +5342,7 @@ function dispatchFunction(name, argsStr) {
|
|
|
4875
5342
|
return { $setUnion: unionArgs };
|
|
4876
5343
|
}
|
|
4877
5344
|
case "SWITCH": {
|
|
4878
|
-
if (args.length < 2) throw
|
|
5345
|
+
if (args.length < 2) throw createError(ErrorCodes.INVALID_EXPRESSION, "SWITCH requires at least 2 arguments");
|
|
4879
5346
|
const branches = [];
|
|
4880
5347
|
let defaultValue = null;
|
|
4881
5348
|
for (let index = 0; index < args.length - 1; index += 2) {
|
|
@@ -4893,15 +5360,15 @@ function dispatchFunction(name, argsStr) {
|
|
|
4893
5360
|
case "ANY_ELEMENT_TRUE":
|
|
4894
5361
|
return { $anyElementTrue: [parseValue(args[0])] };
|
|
4895
5362
|
case "COND": {
|
|
4896
|
-
if (args.length !== 3) throw
|
|
5363
|
+
if (args.length !== 3) throw createError(ErrorCodes.INVALID_EXPRESSION, "COND requires 3 arguments");
|
|
4897
5364
|
return { $cond: { if: compileInnerExpression(args[0]), then: parseValue(args[1]), else: parseValue(args[2]) } };
|
|
4898
5365
|
}
|
|
4899
5366
|
case "IF_NULL": {
|
|
4900
|
-
if (args.length !== 2) throw
|
|
5367
|
+
if (args.length !== 2) throw createError(ErrorCodes.INVALID_EXPRESSION, "IF_NULL requires 2 arguments");
|
|
4901
5368
|
return { $ifNull: [parseValue(args[0]), parseValue(args[1])] };
|
|
4902
5369
|
}
|
|
4903
5370
|
case "SET_FIELD": {
|
|
4904
|
-
if (args.length !== 3) throw
|
|
5371
|
+
if (args.length !== 3) throw createError(ErrorCodes.INVALID_EXPRESSION, "SET_FIELD requires 3 arguments: (field, value, input)");
|
|
4905
5372
|
return { $setField: { field: parseValue(args[0]), input: parseValue(args[2]), value: parseValue(args[1]) } };
|
|
4906
5373
|
}
|
|
4907
5374
|
case "UNSET_FIELD":
|
|
@@ -4918,7 +5385,7 @@ function dispatchFunction(name, argsStr) {
|
|
|
4918
5385
|
return { $setIsSubset: [parseValue(args[0]), parseValue(args[1])] };
|
|
4919
5386
|
case "LET": {
|
|
4920
5387
|
const varsMatch = /\{(.+)\}/.exec(args[0]);
|
|
4921
|
-
if (!varsMatch) throw
|
|
5388
|
+
if (!varsMatch) throw createError(ErrorCodes.INVALID_EXPRESSION, "LET requires an object literal for variables");
|
|
4922
5389
|
const varPairs = varsMatch[1].split(",").map((pair) => {
|
|
4923
5390
|
const [key, ...rest] = pair.split(":");
|
|
4924
5391
|
return [key.trim(), rest.join(":").trim()];
|
|
@@ -4936,7 +5403,7 @@ function dispatchFunction(name, argsStr) {
|
|
|
4936
5403
|
case "SAMPLE_RATE":
|
|
4937
5404
|
return { $sampleRate: parseValue(args[0]) };
|
|
4938
5405
|
default:
|
|
4939
|
-
throw
|
|
5406
|
+
throw createError(ErrorCodes.INVALID_EXPRESSION, `Unsupported function: ${name}`);
|
|
4940
5407
|
}
|
|
4941
5408
|
}
|
|
4942
5409
|
function compileFilterCondition(condition, varName) {
|
|
@@ -5263,7 +5730,7 @@ function decodeCursor(cursor, secret) {
|
|
|
5263
5730
|
}
|
|
5264
5731
|
const payload = JSON.parse(Buffer.from(raw, "base64url").toString("utf8"));
|
|
5265
5732
|
if (payload?.v !== 1 || !Array.isArray(payload.values)) {
|
|
5266
|
-
throw
|
|
5733
|
+
throw createError(ErrorCodes.INVALID_ARGUMENT, "Invalid cursor payload.");
|
|
5267
5734
|
}
|
|
5268
5735
|
return payload.values;
|
|
5269
5736
|
} catch (cause) {
|
|
@@ -5362,6 +5829,7 @@ function buildEffectiveProjection(projection, sort) {
|
|
|
5362
5829
|
}
|
|
5363
5830
|
|
|
5364
5831
|
// src/adapters/mongodb/queries/find-page.ts
|
|
5832
|
+
import { createHash as createHash2 } from "node:crypto";
|
|
5365
5833
|
function normalizePositiveInteger(value, fallback, field) {
|
|
5366
5834
|
if (value === void 0 || value === null) {
|
|
5367
5835
|
return fallback;
|
|
@@ -5380,54 +5848,192 @@ function mergeFilters(base, extra) {
|
|
|
5380
5848
|
}
|
|
5381
5849
|
return { $and: [base, extra] };
|
|
5382
5850
|
}
|
|
5851
|
+
function stableStringify2(value) {
|
|
5852
|
+
if (value === void 0) {
|
|
5853
|
+
return '"__undefined__"';
|
|
5854
|
+
}
|
|
5855
|
+
if (value === null) {
|
|
5856
|
+
return "null";
|
|
5857
|
+
}
|
|
5858
|
+
if (Array.isArray(value)) {
|
|
5859
|
+
return `[${value.map((item) => stableStringify2(item)).join(",")}]`;
|
|
5860
|
+
}
|
|
5861
|
+
if (value instanceof Date) {
|
|
5862
|
+
return JSON.stringify(value.toISOString());
|
|
5863
|
+
}
|
|
5864
|
+
if (typeof value === "object") {
|
|
5865
|
+
const customJson = value.toJSON;
|
|
5866
|
+
if (typeof customJson === "function" && value.constructor?.name !== "Object") {
|
|
5867
|
+
return stableStringify2(customJson.call(value));
|
|
5868
|
+
}
|
|
5869
|
+
const entries = Object.entries(value).sort(([left], [right]) => left.localeCompare(right)).map(([key, item]) => `${JSON.stringify(key)}:${stableStringify2(item)}`);
|
|
5870
|
+
return `{${entries.join(",")}}`;
|
|
5871
|
+
}
|
|
5872
|
+
return JSON.stringify(value);
|
|
5873
|
+
}
|
|
5874
|
+
function hashPayload(payload) {
|
|
5875
|
+
return createHash2("sha256").update(stableStringify2(payload)).digest("hex");
|
|
5876
|
+
}
|
|
5877
|
+
function buildFindPageCacheKey(collection, options, normalized) {
|
|
5878
|
+
const payload = {
|
|
5879
|
+
query: normalized.query,
|
|
5880
|
+
sort: normalized.sort,
|
|
5881
|
+
limit: normalized.limit,
|
|
5882
|
+
page: normalized.page,
|
|
5883
|
+
after: options.after,
|
|
5884
|
+
before: options.before,
|
|
5885
|
+
projection: options.projection,
|
|
5886
|
+
pipeline: options.pipeline ?? [],
|
|
5887
|
+
totals: options.totals,
|
|
5888
|
+
jump: options.jump,
|
|
5889
|
+
offsetJump: options.offsetJump,
|
|
5890
|
+
maxTimeMS: normalized.maxTimeMS,
|
|
5891
|
+
hint: options.hint,
|
|
5892
|
+
collation: options.collation,
|
|
5893
|
+
batchSize: options.batchSize,
|
|
5894
|
+
options: options.options
|
|
5895
|
+
};
|
|
5896
|
+
const keyHash = hashPayload(payload);
|
|
5897
|
+
return { key: `findPage:${collection.namespace}:${keyHash}`, keyHash };
|
|
5898
|
+
}
|
|
5899
|
+
function buildTotalsCacheKey(collection, query, limit, totals) {
|
|
5900
|
+
const payload = {
|
|
5901
|
+
query,
|
|
5902
|
+
limit,
|
|
5903
|
+
mode: totals.mode ?? "sync",
|
|
5904
|
+
hint: totals.hint,
|
|
5905
|
+
collation: totals.collation,
|
|
5906
|
+
maxTimeMS: totals.maxTimeMS
|
|
5907
|
+
};
|
|
5908
|
+
const token = hashPayload(payload);
|
|
5909
|
+
return { key: `findPageTotals:${collection.namespace}:${token}`, token };
|
|
5910
|
+
}
|
|
5911
|
+
function cloneFindPageResult(result) {
|
|
5912
|
+
return {
|
|
5913
|
+
...result,
|
|
5914
|
+
items: Array.isArray(result.items) ? [...result.items] : result.items,
|
|
5915
|
+
pageInfo: result.pageInfo && typeof result.pageInfo === "object" ? { ...result.pageInfo } : result.pageInfo,
|
|
5916
|
+
totals: result.totals && typeof result.totals === "object" ? { ...result.totals } : result.totals,
|
|
5917
|
+
meta: result.meta && typeof result.meta === "object" ? {
|
|
5918
|
+
...result.meta,
|
|
5919
|
+
ns: { ...result.meta.ns },
|
|
5920
|
+
steps: result.meta.steps ? [...result.meta.steps] : void 0
|
|
5921
|
+
} : result.meta
|
|
5922
|
+
};
|
|
5923
|
+
}
|
|
5924
|
+
function getPositiveTtl(value, fallback) {
|
|
5925
|
+
return typeof value === "number" && value > 0 ? value : fallback;
|
|
5926
|
+
}
|
|
5383
5927
|
var _asyncTotalsCache = new MemoryCache({
|
|
5384
5928
|
maxEntries: 1e4,
|
|
5385
5929
|
enableStats: false
|
|
5386
5930
|
});
|
|
5387
|
-
|
|
5931
|
+
var _totalsInflight = /* @__PURE__ */ new Map();
|
|
5932
|
+
function runTotalsOnce(key, task) {
|
|
5933
|
+
if (_totalsInflight.has(key)) {
|
|
5934
|
+
return;
|
|
5935
|
+
}
|
|
5936
|
+
const promise = task().catch(() => {
|
|
5937
|
+
}).finally(() => {
|
|
5938
|
+
_totalsInflight.delete(key);
|
|
5939
|
+
});
|
|
5940
|
+
_totalsInflight.set(key, promise);
|
|
5941
|
+
}
|
|
5942
|
+
async function computeTotals(coll, query, limit, totals, defaults = {}, queryCache) {
|
|
5388
5943
|
const mode = totals.mode ?? "sync";
|
|
5389
|
-
|
|
5944
|
+
const cache = queryCache ?? _asyncTotalsCache;
|
|
5945
|
+
const ttlMs = getPositiveTtl(totals.ttlMs, 10 * 6e4);
|
|
5946
|
+
const { key: cacheKey, token } = buildTotalsCacheKey(coll, query, limit, totals);
|
|
5947
|
+
const buildCountOptions = (fallbackMaxTimeMS) => {
|
|
5390
5948
|
const countOpts = {};
|
|
5391
|
-
|
|
5392
|
-
|
|
5949
|
+
const maxTimeMS = totals.maxTimeMS ?? fallbackMaxTimeMS;
|
|
5950
|
+
if (maxTimeMS !== void 0) {
|
|
5951
|
+
countOpts.maxTimeMS = maxTimeMS;
|
|
5952
|
+
}
|
|
5953
|
+
if (totals.hint !== void 0) {
|
|
5954
|
+
countOpts.hint = totals.hint;
|
|
5955
|
+
}
|
|
5956
|
+
if (totals.collation !== void 0) {
|
|
5957
|
+
countOpts.collation = totals.collation;
|
|
5393
5958
|
}
|
|
5394
|
-
|
|
5395
|
-
|
|
5959
|
+
return countOpts;
|
|
5960
|
+
};
|
|
5961
|
+
const countWithOptions = async () => {
|
|
5962
|
+
const countOpts = buildCountOptions(2e3);
|
|
5963
|
+
const countQuery = query;
|
|
5964
|
+
const runner = () => coll.countDocuments(
|
|
5965
|
+
countQuery,
|
|
5396
5966
|
countOpts
|
|
5397
5967
|
);
|
|
5398
|
-
|
|
5399
|
-
|
|
5968
|
+
return defaults.countQueue ? defaults.countQueue.execute(runner) : runner();
|
|
5969
|
+
};
|
|
5970
|
+
const buildPayload = (total, approx = false) => ({
|
|
5971
|
+
mode,
|
|
5972
|
+
total,
|
|
5973
|
+
totalPages: total > 0 ? Math.ceil(total / limit) : 0,
|
|
5974
|
+
ts: Date.now(),
|
|
5975
|
+
...approx ? { approx: true } : {}
|
|
5976
|
+
});
|
|
5977
|
+
const buildFailurePayload = (error) => ({
|
|
5978
|
+
mode,
|
|
5979
|
+
total: null,
|
|
5980
|
+
totalPages: null,
|
|
5981
|
+
ts: Date.now(),
|
|
5982
|
+
error
|
|
5983
|
+
});
|
|
5984
|
+
if (mode === "sync") {
|
|
5985
|
+
const cached = cache.get(cacheKey);
|
|
5986
|
+
if (cached !== void 0) {
|
|
5987
|
+
return { ...cached, mode: "sync" };
|
|
5988
|
+
}
|
|
5989
|
+
try {
|
|
5990
|
+
const payload = buildPayload(await countWithOptions());
|
|
5991
|
+
await Promise.resolve(cache.set(cacheKey, payload, ttlMs));
|
|
5992
|
+
return { ...payload, mode: "sync" };
|
|
5993
|
+
} catch {
|
|
5994
|
+
const payload = buildFailurePayload("count_failed");
|
|
5995
|
+
await Promise.resolve(cache.set(cacheKey, payload, ttlMs));
|
|
5996
|
+
return { ...payload, mode: "sync" };
|
|
5997
|
+
}
|
|
5400
5998
|
}
|
|
5401
5999
|
if (mode === "async") {
|
|
5402
|
-
const
|
|
5403
|
-
|
|
5404
|
-
|
|
5405
|
-
if (cachedTotal !== void 0) {
|
|
5406
|
-
return { mode: "async", total: cachedTotal, token };
|
|
6000
|
+
const cached = cache.get(cacheKey);
|
|
6001
|
+
if (cached !== void 0) {
|
|
6002
|
+
return { ...cached, mode: "async", token };
|
|
5407
6003
|
}
|
|
5408
|
-
setImmediate(
|
|
5409
|
-
|
|
5410
|
-
|
|
5411
|
-
|
|
5412
|
-
|
|
5413
|
-
|
|
5414
|
-
|
|
5415
|
-
|
|
6004
|
+
setImmediate(() => {
|
|
6005
|
+
runTotalsOnce(cacheKey, async () => {
|
|
6006
|
+
try {
|
|
6007
|
+
const payload = buildPayload(await countWithOptions());
|
|
6008
|
+
await Promise.resolve(cache.set(cacheKey, payload, ttlMs));
|
|
6009
|
+
} catch {
|
|
6010
|
+
await Promise.resolve(cache.set(cacheKey, buildFailurePayload("count_failed"), ttlMs));
|
|
6011
|
+
}
|
|
6012
|
+
});
|
|
5416
6013
|
});
|
|
5417
6014
|
return { mode: "async", total: null, token };
|
|
5418
6015
|
}
|
|
5419
6016
|
if (mode === "approx") {
|
|
5420
|
-
const
|
|
5421
|
-
if (
|
|
5422
|
-
|
|
6017
|
+
const cached = cache.get(cacheKey);
|
|
6018
|
+
if (cached !== void 0) {
|
|
6019
|
+
return { ...cached, mode: "approx" };
|
|
6020
|
+
}
|
|
6021
|
+
try {
|
|
6022
|
+
const total = Object.keys(query ?? {}).length > 0 ? await countWithOptions() : await coll.estimatedDocumentCount({
|
|
6023
|
+
maxTimeMS: totals.maxTimeMS ?? 1e3
|
|
6024
|
+
});
|
|
6025
|
+
const payload = buildPayload(total, true);
|
|
6026
|
+
await Promise.resolve(cache.set(cacheKey, payload, ttlMs));
|
|
6027
|
+
return { ...payload, mode: "approx" };
|
|
6028
|
+
} catch {
|
|
6029
|
+
const payload = buildFailurePayload("approx_failed");
|
|
6030
|
+
await Promise.resolve(cache.set(cacheKey, payload, ttlMs));
|
|
6031
|
+
return { ...payload, mode: "approx" };
|
|
5423
6032
|
}
|
|
5424
|
-
const total = await coll.estimatedDocumentCount(countOpts);
|
|
5425
|
-
const totalPages = total > 0 ? Math.ceil(total / limit) : 0;
|
|
5426
|
-
return { mode: "approx", total, totalPages, ts: Date.now() };
|
|
5427
6033
|
}
|
|
5428
6034
|
return { mode: mode ?? "sync" };
|
|
5429
6035
|
}
|
|
5430
|
-
async function executeFindPage(collection, options = {}, defaults = {}) {
|
|
6036
|
+
async function executeFindPage(collection, options = {}, defaults = {}, queryCache) {
|
|
5431
6037
|
const metaEnabled = options.meta === true || typeof options.meta === "object" && options.meta !== null;
|
|
5432
6038
|
const metaOptions = options.meta && typeof options.meta === "object" ? options.meta : {};
|
|
5433
6039
|
const metaLevel = options.meta === true ? "op" : metaOptions.level ?? "op";
|
|
@@ -5458,6 +6064,10 @@ async function executeFindPage(collection, options = {}, defaults = {}) {
|
|
|
5458
6064
|
driverOpts.projection = buildEffectiveProjection(options.projection, sort);
|
|
5459
6065
|
}
|
|
5460
6066
|
const jumpOpts = ext.jump;
|
|
6067
|
+
const cacheTTL = typeof ext.cache === "number" && ext.cache > 0 ? ext.cache : 0;
|
|
6068
|
+
const pageResultCache = cacheTTL > 0 && queryCache && options.stream !== true && (options.explain === void 0 || options.explain === false) ? buildFindPageCacheKey(collection, options, { query: baseQuery, sort, limit, page, maxTimeMS: effectiveMaxTimeMS }) : null;
|
|
6069
|
+
const shouldRefreshAsyncTotals = options.totals?.mode === "async";
|
|
6070
|
+
let findPageCacheHit = false;
|
|
5461
6071
|
const finishResult = (result2) => {
|
|
5462
6072
|
if (!metaEnabled) {
|
|
5463
6073
|
return result2;
|
|
@@ -5488,6 +6098,9 @@ async function executeFindPage(collection, options = {}, defaults = {}) {
|
|
|
5488
6098
|
endTs: metaEndTs,
|
|
5489
6099
|
durationMs: metaEndTs - metaStartTs,
|
|
5490
6100
|
...typeof effectiveMaxTimeMS === "number" ? { maxTimeMS: effectiveMaxTimeMS } : {},
|
|
6101
|
+
cacheHit: findPageCacheHit,
|
|
6102
|
+
...findPageCacheHit ? { fromCache: true } : {},
|
|
6103
|
+
...pageResultCache ? { cacheTtl: cacheTTL, keyHash: pageResultCache.keyHash } : {},
|
|
5491
6104
|
page,
|
|
5492
6105
|
after: Boolean(options.after),
|
|
5493
6106
|
before: Boolean(options.before),
|
|
@@ -5556,7 +6169,7 @@ async function executeFindPage(collection, options = {}, defaults = {}) {
|
|
|
5556
6169
|
};
|
|
5557
6170
|
const timedComputeTotals = async () => {
|
|
5558
6171
|
const stepStartTs = Date.now();
|
|
5559
|
-
const result2 = await computeTotals(collection, baseQuery, limit, options.totals);
|
|
6172
|
+
const result2 = await computeTotals(collection, baseQuery, limit, options.totals, defaults, queryCache);
|
|
5560
6173
|
pushMetaStep("computeTotals", Date.now() - stepStartTs, "totals");
|
|
5561
6174
|
return result2;
|
|
5562
6175
|
};
|
|
@@ -5575,6 +6188,32 @@ async function executeFindPage(collection, options = {}, defaults = {}) {
|
|
|
5575
6188
|
...extra.currentPage !== void 0 ? { currentPage: extra.currentPage } : {}
|
|
5576
6189
|
};
|
|
5577
6190
|
};
|
|
6191
|
+
const writePageResultCache = (result2) => {
|
|
6192
|
+
if (!pageResultCache || !queryCache) {
|
|
6193
|
+
return;
|
|
6194
|
+
}
|
|
6195
|
+
const cacheValue = cloneFindPageResult(result2);
|
|
6196
|
+
delete cacheValue.meta;
|
|
6197
|
+
if (shouldRefreshAsyncTotals) {
|
|
6198
|
+
delete cacheValue.totals;
|
|
6199
|
+
}
|
|
6200
|
+
void queryCache.set(pageResultCache.key, cacheValue, cacheTTL);
|
|
6201
|
+
};
|
|
6202
|
+
const finishAndCache = (result2) => {
|
|
6203
|
+
writePageResultCache(result2);
|
|
6204
|
+
return finishResult(result2);
|
|
6205
|
+
};
|
|
6206
|
+
if (pageResultCache && queryCache) {
|
|
6207
|
+
const cached = queryCache.get(pageResultCache.key);
|
|
6208
|
+
if (cached !== void 0) {
|
|
6209
|
+
findPageCacheHit = true;
|
|
6210
|
+
const result2 = cloneFindPageResult(cached);
|
|
6211
|
+
if (options.totals && options.totals.mode !== "none" && (shouldRefreshAsyncTotals || result2.totals === void 0)) {
|
|
6212
|
+
result2.totals = await timedComputeTotals();
|
|
6213
|
+
}
|
|
6214
|
+
return finishResult(result2);
|
|
6215
|
+
}
|
|
6216
|
+
}
|
|
5578
6217
|
if (options.stream === true) {
|
|
5579
6218
|
const direction = options.before ? "before" : "after";
|
|
5580
6219
|
const { queryFilter, effectiveSort } = buildPageQuery(options.after ?? options.before, direction);
|
|
@@ -5617,7 +6256,7 @@ async function executeFindPage(collection, options = {}, defaults = {}) {
|
|
|
5617
6256
|
if (options.totals && options.totals.mode !== "none") {
|
|
5618
6257
|
result2.totals = await timedComputeTotals();
|
|
5619
6258
|
}
|
|
5620
|
-
return
|
|
6259
|
+
return finishAndCache(result2);
|
|
5621
6260
|
}
|
|
5622
6261
|
if (options.after || options.before) {
|
|
5623
6262
|
const direction = options.after ? "after" : "before";
|
|
@@ -5627,7 +6266,7 @@ async function executeFindPage(collection, options = {}, defaults = {}) {
|
|
|
5627
6266
|
const first = items2[0] ?? null;
|
|
5628
6267
|
const last = items2[items2.length - 1] ?? null;
|
|
5629
6268
|
const enc = (item) => item ? encodeCursor(Object.keys(sort).map((f) => item[f]), cursorSecret) : null;
|
|
5630
|
-
|
|
6269
|
+
const result2 = {
|
|
5631
6270
|
items: items2,
|
|
5632
6271
|
pageInfo: {
|
|
5633
6272
|
hasNext: direction === "before" ? Boolean(options.before) : hasMore2,
|
|
@@ -5635,7 +6274,11 @@ async function executeFindPage(collection, options = {}, defaults = {}) {
|
|
|
5635
6274
|
startCursor: enc(first),
|
|
5636
6275
|
endCursor: enc(last)
|
|
5637
6276
|
}
|
|
5638
|
-
}
|
|
6277
|
+
};
|
|
6278
|
+
if (options.totals && options.totals.mode !== "none") {
|
|
6279
|
+
result2.totals = await timedComputeTotals();
|
|
6280
|
+
}
|
|
6281
|
+
return finishAndCache(result2);
|
|
5639
6282
|
}
|
|
5640
6283
|
const { queryFilter: q0, effectiveSort: es0 } = buildPageQuery();
|
|
5641
6284
|
let { items, hasMore } = await timedFetchItems("initialFetch", page > 1 ? "hop" : "fetch", q0, es0, {}, 1);
|
|
@@ -5647,15 +6290,19 @@ async function executeFindPage(collection, options = {}, defaults = {}) {
|
|
|
5647
6290
|
if (options.totals && options.totals.mode !== "none") {
|
|
5648
6291
|
result2.totals = await timedComputeTotals();
|
|
5649
6292
|
}
|
|
5650
|
-
return
|
|
6293
|
+
return finishAndCache(result2);
|
|
5651
6294
|
}
|
|
5652
6295
|
for (let cp = 2; cp <= page; cp++) {
|
|
5653
6296
|
const lastItem = items[items.length - 1];
|
|
5654
6297
|
if (!lastItem) {
|
|
5655
|
-
|
|
6298
|
+
const result2 = {
|
|
5656
6299
|
items,
|
|
5657
6300
|
pageInfo: buildPageInfo(items, false, { hasPrev: cp > 2, currentPage: cp - 1 })
|
|
5658
|
-
}
|
|
6301
|
+
};
|
|
6302
|
+
if (options.totals && options.totals.mode !== "none") {
|
|
6303
|
+
result2.totals = await timedComputeTotals();
|
|
6304
|
+
}
|
|
6305
|
+
return finishAndCache(result2);
|
|
5659
6306
|
}
|
|
5660
6307
|
const endCursor = encodeCursor(
|
|
5661
6308
|
Object.keys(sort).map((f) => lastItem[f]),
|
|
@@ -5673,10 +6320,10 @@ async function executeFindPage(collection, options = {}, defaults = {}) {
|
|
|
5673
6320
|
if (options.totals && options.totals.mode !== "none") {
|
|
5674
6321
|
result.totals = await timedComputeTotals();
|
|
5675
6322
|
}
|
|
5676
|
-
return
|
|
6323
|
+
return finishAndCache(result);
|
|
5677
6324
|
}
|
|
5678
|
-
async function findPageDocuments(collection, options = {}, defaults) {
|
|
5679
|
-
return executeFindPage(collection, options, defaults ?? {});
|
|
6325
|
+
async function findPageDocuments(collection, options = {}, defaults, queryCache) {
|
|
6326
|
+
return executeFindPage(collection, options, defaults ?? {}, queryCache);
|
|
5680
6327
|
}
|
|
5681
6328
|
|
|
5682
6329
|
// src/adapters/mongodb/queries/find-by-id.ts
|
|
@@ -5898,21 +6545,21 @@ var FindChain = class {
|
|
|
5898
6545
|
}
|
|
5899
6546
|
limit(value) {
|
|
5900
6547
|
if (typeof value !== "number" || value < 0) {
|
|
5901
|
-
throw
|
|
6548
|
+
throw createError(ErrorCodes.INVALID_ARGUMENT, `limit() requires a non-negative number, got: ${typeof value} (${value})`);
|
|
5902
6549
|
}
|
|
5903
6550
|
this.options.limit = value;
|
|
5904
6551
|
return this;
|
|
5905
6552
|
}
|
|
5906
6553
|
skip(value) {
|
|
5907
6554
|
if (typeof value !== "number" || value < 0) {
|
|
5908
|
-
throw
|
|
6555
|
+
throw createError(ErrorCodes.INVALID_ARGUMENT, `skip() requires a non-negative number, got: ${typeof value} (${value})`);
|
|
5909
6556
|
}
|
|
5910
6557
|
this.options.skip = value;
|
|
5911
6558
|
return this;
|
|
5912
6559
|
}
|
|
5913
6560
|
sort(value) {
|
|
5914
6561
|
if (!value || typeof value !== "object") {
|
|
5915
|
-
throw
|
|
6562
|
+
throw createError(ErrorCodes.INVALID_ARGUMENT, `sort() requires an object or array, got: ${typeof value}`);
|
|
5916
6563
|
}
|
|
5917
6564
|
this.options.sort = value;
|
|
5918
6565
|
return this;
|
|
@@ -5956,7 +6603,7 @@ var FindChain = class {
|
|
|
5956
6603
|
}
|
|
5957
6604
|
toArray() {
|
|
5958
6605
|
if (this.executed) {
|
|
5959
|
-
throw
|
|
6606
|
+
throw createError(ErrorCodes.INVALID_OPERATION, "Query already executed.");
|
|
5960
6607
|
}
|
|
5961
6608
|
this.executed = true;
|
|
5962
6609
|
return this.collection.find(this.normalizedQuery, buildFindDriverOptions(this.buildExecuteOptions())).toArray();
|
|
@@ -6044,7 +6691,7 @@ var AggregateChain = class {
|
|
|
6044
6691
|
}
|
|
6045
6692
|
toArray() {
|
|
6046
6693
|
if (this.executed) {
|
|
6047
|
-
throw
|
|
6694
|
+
throw createError(ErrorCodes.INVALID_OPERATION, "Query already executed.");
|
|
6048
6695
|
}
|
|
6049
6696
|
this.executed = true;
|
|
6050
6697
|
return this.collection.aggregate(this.pipeline, buildAggregateDriverOptions(this.buildExecuteOptions())).toArray();
|
|
@@ -6839,7 +7486,7 @@ async function insertOneForAccessor(context, doc, options) {
|
|
|
6839
7486
|
const threshold = context.defaults?.slowQueryMs ?? 500;
|
|
6840
7487
|
if (elapsed > threshold && context.logger) {
|
|
6841
7488
|
try {
|
|
6842
|
-
context.logger.warn("[insertOne]
|
|
7489
|
+
context.logger.warn("[insertOne] slow operation warning", {
|
|
6843
7490
|
ns: `${context.dbName}.${context.collectionName}`,
|
|
6844
7491
|
threshold,
|
|
6845
7492
|
duration: elapsed,
|
|
@@ -6884,7 +7531,7 @@ async function insertManyForAccessor(context, documents, options) {
|
|
|
6884
7531
|
const elapsed = Date.now() - startedAt;
|
|
6885
7532
|
const threshold = context.defaults?.slowQueryMs ?? 500;
|
|
6886
7533
|
if (elapsed >= threshold && context.logger) {
|
|
6887
|
-
context.logger.warn("[insertMany]
|
|
7534
|
+
context.logger.warn("[insertMany] slow operation warning", {
|
|
6888
7535
|
ns: `${context.dbName}.${context.collectionName}`,
|
|
6889
7536
|
threshold,
|
|
6890
7537
|
duration: elapsed,
|
|
@@ -7015,11 +7662,17 @@ var MongoCollectionAccessor = class {
|
|
|
7015
7662
|
const legacyNamespacePatterns = [
|
|
7016
7663
|
`${String(this.management.defaults?.namespace?.instanceId)}:mongodb:${this.dbName}:${this.collectionName}:*`
|
|
7017
7664
|
];
|
|
7018
|
-
const
|
|
7665
|
+
const findPagePatterns = [
|
|
7666
|
+
`findPage:${namespace}:*`,
|
|
7667
|
+
`findPageTotals:${namespace}:*`,
|
|
7668
|
+
`bookmark:${bookmarkNamespace}:*`,
|
|
7669
|
+
`${bookmarkNamespace}:bm:*`
|
|
7670
|
+
];
|
|
7671
|
+
const patterns = operation === "find" ? [`find:${namespace}:*`] : operation === "findOne" ? [`findOne:${namespace}:*`] : operation === "count" ? [`count:${namespace}:*`] : operation === "findPage" ? findPagePatterns : [
|
|
7019
7672
|
`find:${namespace}:*`,
|
|
7020
7673
|
`findOne:${namespace}:*`,
|
|
7021
7674
|
`count:${namespace}:*`,
|
|
7022
|
-
|
|
7675
|
+
...findPagePatterns
|
|
7023
7676
|
];
|
|
7024
7677
|
patterns.push(...legacyNamespacePatterns);
|
|
7025
7678
|
let deleted = 0;
|
|
@@ -7149,7 +7802,7 @@ var MongoCollectionAccessor = class {
|
|
|
7149
7802
|
}
|
|
7150
7803
|
async findPage(options = {}) {
|
|
7151
7804
|
const resolvedOptions = options.query ? { ...options, query: this._cvFilter(options.query) } : options;
|
|
7152
|
-
return findPageDocuments(this.collectionRef, resolvedOptions, this.management.defaults);
|
|
7805
|
+
return findPageDocuments(this.collectionRef, resolvedOptions, this.management.defaults, this.management.queryCache);
|
|
7153
7806
|
}
|
|
7154
7807
|
/** Opens a change stream on the collection with an optional aggregation pipeline. */
|
|
7155
7808
|
watch(pipeline = [], options) {
|
|
@@ -7626,6 +8279,26 @@ function createRuntimeModelInstance(host, name, scope) {
|
|
|
7626
8279
|
});
|
|
7627
8280
|
return instance;
|
|
7628
8281
|
}
|
|
8282
|
+
async function ensureRuntimeModelIndexes(host, options = {}) {
|
|
8283
|
+
const modelNames = options.models ?? Model.list();
|
|
8284
|
+
const models = [];
|
|
8285
|
+
for (const name of modelNames) {
|
|
8286
|
+
const model = host.scopedModel(name, {
|
|
8287
|
+
database: options.database,
|
|
8288
|
+
pool: options.pool
|
|
8289
|
+
});
|
|
8290
|
+
const result = await model.ensureIndexes({
|
|
8291
|
+
dryRun: options.dryRun,
|
|
8292
|
+
throwOnError: options.throwOnError
|
|
8293
|
+
});
|
|
8294
|
+
models.push({ name, result });
|
|
8295
|
+
}
|
|
8296
|
+
return {
|
|
8297
|
+
dryRun: options.dryRun === true,
|
|
8298
|
+
models,
|
|
8299
|
+
totals: summarizeModelIndexEnsureResults(models.map((item) => item.result))
|
|
8300
|
+
};
|
|
8301
|
+
}
|
|
7629
8302
|
|
|
7630
8303
|
// src/entry/runtime-core-hosts.ts
|
|
7631
8304
|
function resolveAdapterCache(state) {
|
|
@@ -7841,7 +8514,7 @@ function createRuntimeAdapterBridge(host) {
|
|
|
7841
8514
|
dropDatabase: async (name, adminOptions) => {
|
|
7842
8515
|
host.ensureConnected();
|
|
7843
8516
|
if (!name || typeof name !== "string") {
|
|
7844
|
-
throw
|
|
8517
|
+
throw createError(ErrorCodes.INVALID_DATABASE_NAME, "Database name is required and must be a non-empty string");
|
|
7845
8518
|
}
|
|
7846
8519
|
if (!adminOptions?.confirm) {
|
|
7847
8520
|
const error = new Error(
|
|
@@ -7877,7 +8550,7 @@ function createRuntimeAdapterBridge(host) {
|
|
|
7877
8550
|
runCommand: async (command, adminOptions) => {
|
|
7878
8551
|
host.ensureConnected();
|
|
7879
8552
|
if (command === null || typeof command !== "object") {
|
|
7880
|
-
throw
|
|
8553
|
+
throw createError(ErrorCodes.INVALID_ARGUMENT, "Command must be a non-null object");
|
|
7881
8554
|
}
|
|
7882
8555
|
return host.db().runCommand(command, adminOptions ?? {});
|
|
7883
8556
|
},
|
|
@@ -8003,8 +8676,8 @@ var LockManager = class {
|
|
|
8003
8676
|
if (attempt === retryTimes) {
|
|
8004
8677
|
break;
|
|
8005
8678
|
}
|
|
8006
|
-
const
|
|
8007
|
-
await sleep3(
|
|
8679
|
+
const delay2 = retryDelay * Math.pow(retryBackoff, attempt);
|
|
8680
|
+
await sleep3(delay2);
|
|
8008
8681
|
}
|
|
8009
8682
|
this.stats.errors += 1;
|
|
8010
8683
|
if (options.fallbackToNoLock) {
|
|
@@ -8128,7 +8801,7 @@ var DistributedCacheLockManager = class {
|
|
|
8128
8801
|
errors: 0
|
|
8129
8802
|
};
|
|
8130
8803
|
if (!options.redis) {
|
|
8131
|
-
throw
|
|
8804
|
+
throw createError(ErrorCodes.INVALID_CONFIG, "DistributedCacheLockManager requires a Redis instance");
|
|
8132
8805
|
}
|
|
8133
8806
|
this.redis = options.redis;
|
|
8134
8807
|
this.lockKeyPrefix = options.lockKeyPrefix ?? "monsqlize:cache:lock:";
|
|
@@ -8786,17 +9459,17 @@ var BatchQueue = class {
|
|
|
8786
9459
|
import { MongoClient as MongoDriverClient2 } from "mongodb";
|
|
8787
9460
|
|
|
8788
9461
|
// src/capabilities/slow-query-log/slow-query-log-records.ts
|
|
8789
|
-
import { createHash as
|
|
8790
|
-
function
|
|
9462
|
+
import { createHash as createHash3 } from "node:crypto";
|
|
9463
|
+
function stableStringify3(value) {
|
|
8791
9464
|
if (Array.isArray(value)) {
|
|
8792
|
-
return `[${value.map((item) =>
|
|
9465
|
+
return `[${value.map((item) => stableStringify3(item)).join(",")}]`;
|
|
8793
9466
|
}
|
|
8794
9467
|
if (value instanceof Date) {
|
|
8795
9468
|
return JSON.stringify(value.toISOString());
|
|
8796
9469
|
}
|
|
8797
9470
|
if (value && typeof value === "object") {
|
|
8798
9471
|
const entries = Object.entries(value).sort(([left], [right]) => left.localeCompare(right));
|
|
8799
|
-
return `{${entries.map(([key, current]) => `${JSON.stringify(key)}:${
|
|
9472
|
+
return `{${entries.map(([key, current]) => `${JSON.stringify(key)}:${stableStringify3(current)}`).join(",")}}`;
|
|
8800
9473
|
}
|
|
8801
9474
|
return JSON.stringify(value);
|
|
8802
9475
|
}
|
|
@@ -8813,7 +9486,7 @@ function normalizeHashInput(input) {
|
|
|
8813
9486
|
};
|
|
8814
9487
|
}
|
|
8815
9488
|
function generateQueryHash(input) {
|
|
8816
|
-
return
|
|
9489
|
+
return createHash3("sha256").update(stableStringify3(normalizeHashInput(input))).digest("hex").slice(0, 16);
|
|
8817
9490
|
}
|
|
8818
9491
|
function handleSlowQueryLogError(logger, policy, error) {
|
|
8819
9492
|
if (policy === "throw") {
|
|
@@ -9263,7 +9936,7 @@ var SlowQueryLogManager = class {
|
|
|
9263
9936
|
|
|
9264
9937
|
// src/capabilities/sync/index.ts
|
|
9265
9938
|
import { mkdir, readFile, unlink, writeFile } from "node:fs/promises";
|
|
9266
|
-
import
|
|
9939
|
+
import path2 from "node:path";
|
|
9267
9940
|
import { MongoClient as MongoDriverClient3 } from "mongodb";
|
|
9268
9941
|
function validateTargetConfig(target, index) {
|
|
9269
9942
|
if (!target || typeof target !== "object") {
|
|
@@ -9370,7 +10043,7 @@ var ResumeTokenStore = class {
|
|
|
9370
10043
|
await Promise.resolve(this.redis.set(this.redisKey, payload));
|
|
9371
10044
|
return;
|
|
9372
10045
|
}
|
|
9373
|
-
await mkdir(
|
|
10046
|
+
await mkdir(path2.dirname(this.path), { recursive: true });
|
|
9374
10047
|
await writeFile(this.path, payload, "utf8");
|
|
9375
10048
|
} catch (error) {
|
|
9376
10049
|
this.logger?.error?.("[Sync] failed to save resume token", error);
|
|
@@ -9642,7 +10315,16 @@ function getOrCreateTransactionManager(config) {
|
|
|
9642
10315
|
client: config.client,
|
|
9643
10316
|
cache: config.cache,
|
|
9644
10317
|
logger: config.logger,
|
|
9645
|
-
lockManager: config.lockManager
|
|
10318
|
+
lockManager: config.lockManager,
|
|
10319
|
+
maxDuration: config.transaction?.maxDuration ?? config.transaction?.defaultTimeout,
|
|
10320
|
+
enableRetry: config.transaction?.enableRetry,
|
|
10321
|
+
maxRetries: config.transaction?.maxRetries,
|
|
10322
|
+
retryDelay: config.transaction?.retryDelay,
|
|
10323
|
+
retryBackoff: config.transaction?.retryBackoff,
|
|
10324
|
+
defaultReadConcern: config.transaction?.defaultReadConcern,
|
|
10325
|
+
defaultWriteConcern: config.transaction?.defaultWriteConcern,
|
|
10326
|
+
defaultReadPreference: config.transaction?.defaultReadPreference,
|
|
10327
|
+
maxStatsSamples: config.transaction?.maxStatsSamples
|
|
9646
10328
|
});
|
|
9647
10329
|
}
|
|
9648
10330
|
function getOrCreateLockManager(current, logger) {
|
|
@@ -9932,7 +10614,7 @@ function resolveCacheSource(cacheOrDb) {
|
|
|
9932
10614
|
return cache;
|
|
9933
10615
|
}
|
|
9934
10616
|
}
|
|
9935
|
-
throw
|
|
10617
|
+
throw createError(ErrorCodes.CACHE_UNAVAILABLE, "Invalid cache instance from MonSQLize");
|
|
9936
10618
|
}
|
|
9937
10619
|
function toWithCacheStats(stats, totalTime = 0) {
|
|
9938
10620
|
const calls = stats.hits + stats.misses;
|
|
@@ -9961,46 +10643,46 @@ function normalizeFunctionCacheStats(stats, timings, name) {
|
|
|
9961
10643
|
}
|
|
9962
10644
|
function validateFunctionCacheOptions(options) {
|
|
9963
10645
|
if (options !== void 0 && (typeof options !== "object" || options === null || Array.isArray(options))) {
|
|
9964
|
-
throw
|
|
10646
|
+
throw createError(ErrorCodes.INVALID_ARGUMENT, "options must be an object");
|
|
9965
10647
|
}
|
|
9966
10648
|
const opts = options ?? {};
|
|
9967
10649
|
if (opts.namespace !== void 0 && typeof opts.namespace !== "string") {
|
|
9968
|
-
throw
|
|
10650
|
+
throw createError(ErrorCodes.INVALID_ARGUMENT, "namespace must be a string");
|
|
9969
10651
|
}
|
|
9970
10652
|
const ttl = opts.ttl !== void 0 ? opts.ttl : opts.defaultTTL;
|
|
9971
10653
|
if (ttl !== void 0 && (typeof ttl !== "number" || Number.isNaN(ttl) || ttl < 0)) {
|
|
9972
|
-
throw
|
|
10654
|
+
throw createError(ErrorCodes.INVALID_ARGUMENT, "defaultTTL must be a non-negative number");
|
|
9973
10655
|
}
|
|
9974
10656
|
return opts;
|
|
9975
10657
|
}
|
|
9976
10658
|
function validateFunctionCachePerFnOptions(options) {
|
|
9977
10659
|
if (options !== void 0 && (typeof options !== "object" || options === null || Array.isArray(options))) {
|
|
9978
|
-
throw
|
|
10660
|
+
throw createError(ErrorCodes.INVALID_ARGUMENT, "options must be an object");
|
|
9979
10661
|
}
|
|
9980
10662
|
const opts = options ?? {};
|
|
9981
10663
|
if (opts.keyBuilder !== void 0 && typeof opts.keyBuilder !== "function") {
|
|
9982
|
-
throw
|
|
10664
|
+
throw createError(ErrorCodes.INVALID_ARGUMENT, "keyBuilder must be a function");
|
|
9983
10665
|
}
|
|
9984
10666
|
if (opts.condition !== void 0 && typeof opts.condition !== "function") {
|
|
9985
|
-
throw
|
|
10667
|
+
throw createError(ErrorCodes.INVALID_ARGUMENT, "condition must be a function");
|
|
9986
10668
|
}
|
|
9987
10669
|
const ttl = opts.ttl !== void 0 ? opts.ttl : opts.defaultTTL;
|
|
9988
10670
|
if (ttl !== void 0 && (typeof ttl !== "number" || Number.isNaN(ttl) || ttl < 0)) {
|
|
9989
|
-
throw
|
|
10671
|
+
throw createError(ErrorCodes.INVALID_ARGUMENT, "defaultTTL must be a non-negative number");
|
|
9990
10672
|
}
|
|
9991
10673
|
return opts;
|
|
9992
10674
|
}
|
|
9993
10675
|
function withCache(fn, options = {}) {
|
|
9994
|
-
if (typeof fn !== "function") throw
|
|
10676
|
+
if (typeof fn !== "function") throw createError(ErrorCodes.INVALID_ARGUMENT, "fn must be a function");
|
|
9995
10677
|
const { ttl = 6e4, namespace, keyBuilder, condition, cache: externalCache, enableStats = true } = options;
|
|
9996
10678
|
if (typeof ttl !== "number" || Number.isNaN(ttl) || ttl < 0)
|
|
9997
|
-
throw
|
|
10679
|
+
throw createError(ErrorCodes.INVALID_ARGUMENT, "ttl must be a non-negative number");
|
|
9998
10680
|
if (keyBuilder !== void 0 && typeof keyBuilder !== "function")
|
|
9999
|
-
throw
|
|
10681
|
+
throw createError(ErrorCodes.INVALID_ARGUMENT, "keyBuilder must be a function");
|
|
10000
10682
|
if (condition !== void 0 && typeof condition !== "function")
|
|
10001
|
-
throw
|
|
10683
|
+
throw createError(ErrorCodes.INVALID_ARGUMENT, "condition must be a function");
|
|
10002
10684
|
if (externalCache !== void 0 && !isValidCache(externalCache))
|
|
10003
|
-
throw
|
|
10685
|
+
throw createError(ErrorCodes.CACHE_UNAVAILABLE, "Invalid cache instance");
|
|
10004
10686
|
const wrapped = hubWithCache(fn, {
|
|
10005
10687
|
ttl,
|
|
10006
10688
|
namespace: namespace ?? "fn",
|
|
@@ -10042,9 +10724,9 @@ var FunctionCache = class {
|
|
|
10042
10724
|
}
|
|
10043
10725
|
register(name, fn, options) {
|
|
10044
10726
|
if (!name || typeof name !== "string")
|
|
10045
|
-
throw
|
|
10727
|
+
throw createError(ErrorCodes.INVALID_ARGUMENT, "Function name must be a non-empty string");
|
|
10046
10728
|
if (typeof fn !== "function")
|
|
10047
|
-
throw
|
|
10729
|
+
throw createError(ErrorCodes.INVALID_ARGUMENT, "fn must be a function");
|
|
10048
10730
|
const opts = validateFunctionCachePerFnOptions(options);
|
|
10049
10731
|
const ttl = opts.ttl !== void 0 ? opts.ttl : opts.defaultTTL;
|
|
10050
10732
|
this._inner.register(name, fn, {
|
|
@@ -10070,13 +10752,13 @@ var FunctionCache = class {
|
|
|
10070
10752
|
}
|
|
10071
10753
|
async invalidate(name, ...args) {
|
|
10072
10754
|
if (!name || typeof name !== "string") {
|
|
10073
|
-
throw
|
|
10755
|
+
throw createError(ErrorCodes.INVALID_ARGUMENT, "Function name must be a non-empty string");
|
|
10074
10756
|
}
|
|
10075
10757
|
await this._inner.invalidate(name, ...args);
|
|
10076
10758
|
}
|
|
10077
10759
|
async invalidatePattern(pattern) {
|
|
10078
10760
|
if (!pattern || typeof pattern !== "string")
|
|
10079
|
-
throw
|
|
10761
|
+
throw createError(ErrorCodes.INVALID_ARGUMENT, "Pattern must be a non-empty string");
|
|
10080
10762
|
return this._inner.invalidatePattern(pattern);
|
|
10081
10763
|
}
|
|
10082
10764
|
list() {
|
|
@@ -10111,15 +10793,15 @@ function b64urlDecodeStr(s) {
|
|
|
10111
10793
|
const b64 = pad(String(s || "")).replace(/-/g, "+").replace(/_/g, "/");
|
|
10112
10794
|
return Buffer.from(b64, "base64").toString();
|
|
10113
10795
|
}
|
|
10114
|
-
function makeInvalidCursorError(cause) {
|
|
10115
|
-
const err = new Error(
|
|
10796
|
+
function makeInvalidCursorError(message = "Invalid cursor", cause) {
|
|
10797
|
+
const err = new Error(message);
|
|
10116
10798
|
err.code = "INVALID_CURSOR";
|
|
10117
10799
|
if (cause !== void 0) err.cause = cause;
|
|
10118
10800
|
return err;
|
|
10119
10801
|
}
|
|
10120
10802
|
function encodeCursor2(payload) {
|
|
10121
10803
|
if (!payload.s || !payload.a) {
|
|
10122
|
-
throw
|
|
10804
|
+
throw makeInvalidCursorError("encodeCursor requires sort (s) and anchor (a)");
|
|
10123
10805
|
}
|
|
10124
10806
|
const json = JSON.stringify({ v: payload.v ?? 1, s: payload.s, a: payload.a, d: payload.d });
|
|
10125
10807
|
return b64urlEncodeStr(json);
|
|
@@ -10128,11 +10810,11 @@ function decodeCursor2(str) {
|
|
|
10128
10810
|
try {
|
|
10129
10811
|
const obj = JSON.parse(b64urlDecodeStr(str));
|
|
10130
10812
|
if (!obj || obj["v"] !== 1 || !obj["s"] || !obj["a"]) {
|
|
10131
|
-
throw
|
|
10813
|
+
throw makeInvalidCursorError("bad-structure");
|
|
10132
10814
|
}
|
|
10133
10815
|
return obj;
|
|
10134
10816
|
} catch (e) {
|
|
10135
|
-
throw makeInvalidCursorError(e);
|
|
10817
|
+
throw makeInvalidCursorError("Invalid cursor", e);
|
|
10136
10818
|
}
|
|
10137
10819
|
}
|
|
10138
10820
|
|
|
@@ -10204,7 +10886,11 @@ var MonSQLizeRuntime = class {
|
|
|
10204
10886
|
} : rawCacheInput;
|
|
10205
10887
|
this._cache = normalizeRuntimeCache(cacheInput);
|
|
10206
10888
|
this._logger = Logger.create(options.logger ?? null);
|
|
10207
|
-
this._cacheLockManager = new CacheLockManager({
|
|
10889
|
+
this._cacheLockManager = new CacheLockManager({
|
|
10890
|
+
logger: options.logger ?? null,
|
|
10891
|
+
maxDuration: options.transaction?.lockMaxDuration,
|
|
10892
|
+
cleanupInterval: options.transaction?.lockCleanupInterval
|
|
10893
|
+
});
|
|
10208
10894
|
this._cache.setLockManager?.(this._cacheLockManager);
|
|
10209
10895
|
this._runtimeDefaults = buildRuntimeDefaults(options);
|
|
10210
10896
|
this._adapterCacheOverride = void 0;
|
|
@@ -10324,7 +11010,8 @@ var MonSQLizeRuntime = class {
|
|
|
10324
11010
|
namespace: d.namespace,
|
|
10325
11011
|
log: d.log,
|
|
10326
11012
|
countQueue: this.options.countQueue,
|
|
10327
|
-
models: this.options.models
|
|
11013
|
+
models: this.options.models,
|
|
11014
|
+
autoIndex: this.options.autoIndex
|
|
10328
11015
|
};
|
|
10329
11016
|
}
|
|
10330
11017
|
async close() {
|
|
@@ -10521,6 +11208,10 @@ var MonSQLizeRuntime = class {
|
|
|
10521
11208
|
cache.set(name, instance);
|
|
10522
11209
|
return instance;
|
|
10523
11210
|
}
|
|
11211
|
+
async ensureModelIndexes(options = {}) {
|
|
11212
|
+
this.ensureConnected();
|
|
11213
|
+
return ensureRuntimeModelIndexes(this, options);
|
|
11214
|
+
}
|
|
10524
11215
|
// Capability delegation ----------------------------------------------------
|
|
10525
11216
|
async startSession(options = {}) {
|
|
10526
11217
|
this.ensureConnected();
|
|
@@ -10575,9 +11266,15 @@ var MonSQLizeRuntime = class {
|
|
|
10575
11266
|
listSagas() {
|
|
10576
11267
|
return this.initializeSagaOrchestrator().listSagas();
|
|
10577
11268
|
}
|
|
11269
|
+
getTransactionStats() {
|
|
11270
|
+
return this._transactionManager?.getStats() ?? null;
|
|
11271
|
+
}
|
|
10578
11272
|
getSagaStats() {
|
|
10579
11273
|
return this.initializeSagaOrchestrator().getStats();
|
|
10580
11274
|
}
|
|
11275
|
+
getDistributedCacheInvalidatorStats() {
|
|
11276
|
+
return this._distributedInvalidator?.getStats() ?? null;
|
|
11277
|
+
}
|
|
10581
11278
|
async startSync() {
|
|
10582
11279
|
this.ensureConnected();
|
|
10583
11280
|
const manager = await this.initializeSyncManager();
|
|
@@ -10656,7 +11353,8 @@ var MonSQLizeRuntime = class {
|
|
|
10656
11353
|
client: this._client,
|
|
10657
11354
|
cache: this._cache,
|
|
10658
11355
|
logger: this.options.logger ?? null,
|
|
10659
|
-
lockManager: this._cacheLockManager
|
|
11356
|
+
lockManager: this._cacheLockManager,
|
|
11357
|
+
transaction: this.options.transaction
|
|
10660
11358
|
});
|
|
10661
11359
|
return this._transactionManager;
|
|
10662
11360
|
}
|