monsqlize 3.3.0 → 3.3.1
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 +4 -3
- package/README.md +31 -5
- package/changelogs/v3.3.1.md +28 -0
- package/dist/cjs/cli/data-task.cjs +3 -2
- package/dist/cjs/index.cjs +1010 -610
- package/dist/cjs/transaction/Transaction.cjs +33 -5
- package/dist/cjs/transaction/TransactionManager.cjs +38 -16
- package/dist/esm/index.mjs +1014 -614
- package/dist/types/collection.d.mts +6 -6
- package/dist/types/collection.d.ts +6 -6
- package/dist/types/data-tasks.d.mts +1 -0
- package/dist/types/data-tasks.d.ts +1 -0
- package/dist/types/model.d.mts +3 -0
- package/dist/types/model.d.ts +3 -0
- package/dist/types/monsqlize.d.mts +4 -0
- package/dist/types/monsqlize.d.ts +4 -0
- package/package.json +3 -2
package/dist/esm/index.mjs
CHANGED
|
@@ -170,12 +170,17 @@ function createRedisCacheAdapter(redisUrlOrInstance, options) {
|
|
|
170
170
|
|
|
171
171
|
// src/capabilities/cache/distributed-cache-invalidator.ts
|
|
172
172
|
import { randomBytes } from "crypto";
|
|
173
|
+
import { MemoryCache as BaseMemoryCache2 } from "cache-hub";
|
|
173
174
|
var DistributedCacheInvalidator = class {
|
|
174
175
|
constructor(options) {
|
|
175
176
|
if (!options.cache) {
|
|
176
177
|
throw createError(ErrorCodes.CACHE_UNAVAILABLE, "DistributedCacheInvalidator requires a cache instance");
|
|
177
178
|
}
|
|
178
179
|
this._cache = options.cache;
|
|
180
|
+
const safeMemoryCache = this._cache instanceof BaseMemoryCache2 || this._cache.local instanceof BaseMemoryCache2 && this._cache.remote === void 0;
|
|
181
|
+
if (!safeMemoryCache && typeof this._cache.invalidateFromRemote !== "function") {
|
|
182
|
+
throw createError(ErrorCodes.INVALID_CONFIG, "DistributedCacheInvalidator requires receive-only cache invalidation");
|
|
183
|
+
}
|
|
179
184
|
this._logger = options.logger ?? null;
|
|
180
185
|
this.channel = options.channel ?? "monsqlize:cache:invalidate";
|
|
181
186
|
this.instanceId = options.instanceId ?? `instance-${Date.now()}-${randomBytes(4).toString("hex")}`;
|
|
@@ -223,18 +228,10 @@ var DistributedCacheInvalidator = class {
|
|
|
223
228
|
this._stats.messagesReceived++;
|
|
224
229
|
try {
|
|
225
230
|
if (isPatternInvalidation) {
|
|
226
|
-
|
|
227
|
-
await targetCache.delPattern?.(String(message.pattern));
|
|
228
|
-
}
|
|
231
|
+
await this._invalidateFromRemote("pattern", String(message.pattern));
|
|
229
232
|
this._logger?.debug?.(`[DistributedCacheInvalidator] Invalidated cache targets: ${String(message.pattern)}`);
|
|
230
233
|
} else {
|
|
231
|
-
|
|
232
|
-
if (typeof targetCache.del === "function") {
|
|
233
|
-
await targetCache.del(String(message.key));
|
|
234
|
-
} else {
|
|
235
|
-
await targetCache.delete?.(String(message.key));
|
|
236
|
-
}
|
|
237
|
-
}
|
|
234
|
+
await this._invalidateFromRemote("key", String(message.key));
|
|
238
235
|
this._logger?.debug?.(`[DistributedCacheInvalidator] Invalidated cache key targets: ${String(message.key)}`);
|
|
239
236
|
}
|
|
240
237
|
this._stats.invalidationsTriggered++;
|
|
@@ -244,32 +241,18 @@ var DistributedCacheInvalidator = class {
|
|
|
244
241
|
}
|
|
245
242
|
});
|
|
246
243
|
}
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
if (this._cache?.local && typeof this._cache.local.delPattern === "function") {
|
|
253
|
-
targets.add(this._cache.local);
|
|
254
|
-
}
|
|
255
|
-
if (this._cache?.remote && typeof this._cache.remote.delPattern === "function") {
|
|
256
|
-
targets.add(this._cache.remote);
|
|
257
|
-
}
|
|
258
|
-
return [...targets];
|
|
259
|
-
}
|
|
260
|
-
_getKeyTargetCaches() {
|
|
261
|
-
const targets = /* @__PURE__ */ new Set();
|
|
262
|
-
const supportsKeyDelete = (cache) => typeof cache?.del === "function" || typeof cache?.delete === "function";
|
|
263
|
-
if (supportsKeyDelete(this._cache)) {
|
|
264
|
-
targets.add(this._cache);
|
|
265
|
-
}
|
|
266
|
-
if (supportsKeyDelete(this._cache?.local)) {
|
|
267
|
-
targets.add(this._cache.local);
|
|
244
|
+
async _invalidateFromRemote(kind, value) {
|
|
245
|
+
if (this._cache instanceof BaseMemoryCache2) {
|
|
246
|
+
if (kind === "pattern") await this._cache.delPattern(value);
|
|
247
|
+
else await this._cache.del(value);
|
|
248
|
+
return;
|
|
268
249
|
}
|
|
269
|
-
if (
|
|
270
|
-
|
|
250
|
+
if (this._cache.local instanceof BaseMemoryCache2 && this._cache.remote === void 0) {
|
|
251
|
+
if (kind === "pattern") await this._cache.local.delPattern(value);
|
|
252
|
+
else await this._cache.local.del(value);
|
|
253
|
+
return;
|
|
271
254
|
}
|
|
272
|
-
|
|
255
|
+
await this._cache.invalidateFromRemote?.(kind, value);
|
|
273
256
|
}
|
|
274
257
|
async invalidate(pattern) {
|
|
275
258
|
if (!pattern) return;
|
|
@@ -341,6 +324,15 @@ var MultiLevelCache = class extends BaseMultiLevelCache {
|
|
|
341
324
|
this._localCompat.setLockManager?.(lockManager);
|
|
342
325
|
this._remoteCompat?.setLockManager?.(lockManager);
|
|
343
326
|
}
|
|
327
|
+
async invalidateFromRemote(kind, value) {
|
|
328
|
+
if (kind === "pattern") {
|
|
329
|
+
await this._localCompat.delPattern(value);
|
|
330
|
+
await this._remoteCompat?.delPattern(value);
|
|
331
|
+
} else {
|
|
332
|
+
await this._localCompat.del(value);
|
|
333
|
+
await this._remoteCompat?.del(value);
|
|
334
|
+
}
|
|
335
|
+
}
|
|
344
336
|
};
|
|
345
337
|
function adaptLegacyCacheLike(v1Cache) {
|
|
346
338
|
if ("has" in v1Cache && typeof v1Cache.has === "function") {
|
|
@@ -718,6 +710,40 @@ function serializeDocument(document) {
|
|
|
718
710
|
}
|
|
719
711
|
|
|
720
712
|
// src/capabilities/model/model-write-helpers.ts
|
|
713
|
+
async function* iterateModelCandidateBatches(context, filter, options, batchSize) {
|
|
714
|
+
const lookupOptions = buildModelVersionLookupOptions(options, {
|
|
715
|
+
_id: 1,
|
|
716
|
+
[context.versionConfig?.field ?? "__v"]: 1
|
|
717
|
+
});
|
|
718
|
+
const stream = context.collection.stream(filter, {
|
|
719
|
+
...lookupOptions,
|
|
720
|
+
sort: { _id: 1 },
|
|
721
|
+
batchSize
|
|
722
|
+
});
|
|
723
|
+
let batch = [];
|
|
724
|
+
try {
|
|
725
|
+
for await (const document of stream) {
|
|
726
|
+
batch.push(document);
|
|
727
|
+
if (batch.length === batchSize) {
|
|
728
|
+
yield batch;
|
|
729
|
+
batch = [];
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
if (batch.length > 0) yield batch;
|
|
733
|
+
} finally {
|
|
734
|
+
stream.destroy();
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
function buildStrictCandidateFilter(filter, id, versionField, expectedVersion) {
|
|
738
|
+
return { $and: [filter ?? {}, { _id: id }, { [versionField]: expectedVersion }] };
|
|
739
|
+
}
|
|
740
|
+
async function isModelVersionConflict(context, filter, id, versionField, expectedVersion, options) {
|
|
741
|
+
const current = await context.collection.findOne(
|
|
742
|
+
{ $and: [filter ?? {}, { _id: id }] },
|
|
743
|
+
buildModelVersionLookupOptions(options, { [versionField]: 1 })
|
|
744
|
+
);
|
|
745
|
+
return current !== null && current !== void 0 && current[versionField] !== expectedVersion;
|
|
746
|
+
}
|
|
721
747
|
var VERSION_LOOKUP_OPTION_KEYS = [
|
|
722
748
|
"session",
|
|
723
749
|
"readConcern",
|
|
@@ -803,11 +829,14 @@ function mapModelSchemaValidationErrors(errors = []) {
|
|
|
803
829
|
}
|
|
804
830
|
function validateModelSchemaPayload(context, document, options, metadata = {}) {
|
|
805
831
|
const shouldValidate = context.validateEnabled || options?.validate === true;
|
|
806
|
-
if (!shouldValidate) {
|
|
832
|
+
if (!shouldValidate || options?.skipValidation) {
|
|
807
833
|
return document;
|
|
808
834
|
}
|
|
809
|
-
if (
|
|
810
|
-
|
|
835
|
+
if (context.schemaError) {
|
|
836
|
+
throw withModelErrorMetadata(
|
|
837
|
+
createError(ErrorCodes.VALIDATION_ERROR, `Schema initialization failed: ${context.schemaError.message}`),
|
|
838
|
+
{ errors: [{ field: "_schema", message: context.schemaError.message }], ...metadata }
|
|
839
|
+
);
|
|
811
840
|
}
|
|
812
841
|
if (!context.schemaCache || !context.schemaValidateFn) {
|
|
813
842
|
return document;
|
|
@@ -841,22 +870,31 @@ function validateModelSchemaPayload(context, document, options, metadata = {}) {
|
|
|
841
870
|
}
|
|
842
871
|
return normalized;
|
|
843
872
|
}
|
|
873
|
+
function buildModelVisibleSoftDeleteCondition(config) {
|
|
874
|
+
return { [config.field]: config.type === "boolean" ? { $ne: true } : null };
|
|
875
|
+
}
|
|
844
876
|
function applyModelSoftDeleteFilter(query, options, softDeleteConfig) {
|
|
845
877
|
if (!softDeleteConfig?.enabled) {
|
|
846
878
|
return query ?? {};
|
|
847
879
|
}
|
|
848
880
|
const resolvedOptions = options ?? {};
|
|
849
881
|
const resolvedQuery = query ?? {};
|
|
850
|
-
if (
|
|
882
|
+
if (resolvedOptions.withDeleted !== void 0 && typeof resolvedOptions.withDeleted !== "boolean") {
|
|
883
|
+
throw createError(ErrorCodes.INVALID_ARGUMENT, "withDeleted must be a boolean.");
|
|
884
|
+
}
|
|
885
|
+
if (resolvedOptions.onlyDeleted !== void 0 && typeof resolvedOptions.onlyDeleted !== "boolean") {
|
|
886
|
+
throw createError(ErrorCodes.INVALID_ARGUMENT, "onlyDeleted must be a boolean.");
|
|
887
|
+
}
|
|
888
|
+
if (Object.prototype.hasOwnProperty.call(resolvedQuery, softDeleteConfig.field)) {
|
|
851
889
|
return resolvedQuery;
|
|
852
890
|
}
|
|
853
891
|
if (resolvedOptions.withDeleted) {
|
|
854
892
|
return resolvedQuery;
|
|
855
893
|
}
|
|
856
894
|
if (resolvedOptions.onlyDeleted) {
|
|
857
|
-
return { ...resolvedQuery, [softDeleteConfig.field]: { $ne: null } };
|
|
895
|
+
return { ...resolvedQuery, [softDeleteConfig.field]: softDeleteConfig.type === "boolean" ? true : { $ne: null } };
|
|
858
896
|
}
|
|
859
|
-
return { ...resolvedQuery,
|
|
897
|
+
return { ...resolvedQuery, ...buildModelVisibleSoftDeleteCondition(softDeleteConfig) };
|
|
860
898
|
}
|
|
861
899
|
function applyModelInsertTimestamps(document, timestampsConfig, nowFactory) {
|
|
862
900
|
if (!timestampsConfig) {
|
|
@@ -1151,7 +1189,9 @@ function projectPopulatedDocument(document, select, nestedPaths) {
|
|
|
1151
1189
|
return document;
|
|
1152
1190
|
}
|
|
1153
1191
|
const nestedRelationFields = nestedPaths.map((path5) => normalizePopulateConfig(path5).path);
|
|
1154
|
-
|
|
1192
|
+
const projected = pickFields(document, [.../* @__PURE__ */ new Set([...select, ...nestedRelationFields])]);
|
|
1193
|
+
if (!select.includes("_id")) delete projected._id;
|
|
1194
|
+
return projected;
|
|
1155
1195
|
}
|
|
1156
1196
|
async function populateModelPath(context, docs, path5, traversalState) {
|
|
1157
1197
|
const config = normalizePopulateConfig(path5);
|
|
@@ -1173,7 +1213,13 @@ async function populateModelPath(context, docs, path5, traversalState) {
|
|
|
1173
1213
|
}
|
|
1174
1214
|
const localValues = docs.map((doc) => getByPath(doc, relation.localField));
|
|
1175
1215
|
const keys = unique(localValues.flatMap((value) => flattenComparableValues(value)));
|
|
1176
|
-
if (
|
|
1216
|
+
if (config.limit !== void 0 && (!Number.isSafeInteger(config.limit) || config.limit < 0)) {
|
|
1217
|
+
throw createError(ErrorCodes.INVALID_ARGUMENT, "populate limit must be a non-negative integer");
|
|
1218
|
+
}
|
|
1219
|
+
if (config.skip !== void 0 && (!Number.isSafeInteger(config.skip) || config.skip < 0)) {
|
|
1220
|
+
throw createError(ErrorCodes.INVALID_ARGUMENT, "populate skip must be a non-negative integer");
|
|
1221
|
+
}
|
|
1222
|
+
if (keys.length === 0 || config.limit === 0) {
|
|
1177
1223
|
for (const doc of docs) {
|
|
1178
1224
|
doc[config.path] = relation.single ? null : [];
|
|
1179
1225
|
}
|
|
@@ -1193,36 +1239,63 @@ async function populateModelPath(context, docs, path5, traversalState) {
|
|
|
1193
1239
|
...effectiveSelect,
|
|
1194
1240
|
relation.foreignField,
|
|
1195
1241
|
"_id",
|
|
1196
|
-
...
|
|
1242
|
+
...Object.keys(config.sort ?? {}),
|
|
1243
|
+
...collectNestedLocalFields(relatedModel, nestedPaths),
|
|
1244
|
+
...relatedModel?.softDeleteConfig?.enabled ? [relatedModel.softDeleteConfig.field] : []
|
|
1197
1245
|
])] : [];
|
|
1198
|
-
const
|
|
1199
|
-
|
|
1200
|
-
...config.
|
|
1201
|
-
}
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1246
|
+
const readOptions = {
|
|
1247
|
+
...effectiveSelect ? { projection: Object.fromEntries(projectionFields.map((field) => [field, 1])) } : {},
|
|
1248
|
+
...config.sort ? { sort: config.sort } : {}
|
|
1249
|
+
};
|
|
1250
|
+
const visibilityMatch = applyModelSoftDeleteFilter(config.match ?? {}, {}, relatedModel?.softDeleteConfig ?? null);
|
|
1251
|
+
const readForKeys = (values, options = readOptions) => {
|
|
1252
|
+
const keyFilter = { [relation.foreignField]: { $in: values } };
|
|
1253
|
+
return readPopulateRows(
|
|
1254
|
+
relatedCollection,
|
|
1255
|
+
Object.keys(visibilityMatch).length === 0 ? keyFilter : { $and: [keyFilter, visibilityMatch] },
|
|
1256
|
+
options
|
|
1257
|
+
);
|
|
1258
|
+
};
|
|
1259
|
+
const hydrateRelated = async (rows) => {
|
|
1260
|
+
let hydrated = relatedModel ? relatedModel.hydrateDocuments(rows) : rows.map((item) => ({ ...item }));
|
|
1261
|
+
if (nestedPaths.length > 0 && relatedModel) {
|
|
1262
|
+
if (state.depth + 1 > state.maxDepth) {
|
|
1263
|
+
throw createError(ErrorCodes.INVALID_ARGUMENT, `populate maxDepth exceeded: ${state.maxDepth}`);
|
|
1264
|
+
}
|
|
1265
|
+
hydrated = await relatedModel.populateDocuments(hydrated, nestedPaths, {
|
|
1266
|
+
depth: state.depth + 1,
|
|
1267
|
+
maxDepth: state.maxDepth,
|
|
1268
|
+
activeConfigs: state.activeConfigs
|
|
1269
|
+
});
|
|
1208
1270
|
}
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1271
|
+
return hydrated;
|
|
1272
|
+
};
|
|
1273
|
+
if (config.limit !== void 0 || config.skip !== void 0) {
|
|
1274
|
+
const scopedOptions = {
|
|
1275
|
+
...readOptions,
|
|
1276
|
+
...config.skip !== void 0 ? { skip: config.skip } : {},
|
|
1277
|
+
...config.limit !== void 0 ? { limit: config.limit } : {}
|
|
1278
|
+
};
|
|
1279
|
+
for (const [index, doc] of docs.entries()) {
|
|
1280
|
+
const parentKeys = unique(flattenComparableValues(localValues[index]));
|
|
1281
|
+
const rows = parentKeys.length > 0 ? await readForKeys(parentKeys, scopedOptions) : [];
|
|
1282
|
+
const hydrated = await hydrateRelated(rows);
|
|
1283
|
+
const matches = dedupeDocuments(hydrated);
|
|
1284
|
+
const projectedMatches = matches.map((item) => projectPopulatedDocument(item, effectiveSelect, nestedPaths));
|
|
1285
|
+
doc[config.path] = relation.single ? projectedMatches[0] ?? null : projectedMatches;
|
|
1220
1286
|
}
|
|
1221
|
-
|
|
1222
|
-
|
|
1287
|
+
} else {
|
|
1288
|
+
const rows = [];
|
|
1289
|
+
for (let offset = 0; offset < keys.length; offset += 200) {
|
|
1290
|
+
rows.push(...await readForKeys(keys.slice(offset, offset + 200)));
|
|
1291
|
+
}
|
|
1292
|
+
const hydrated = await hydrateRelated(rows);
|
|
1293
|
+
const grouped = groupBy(hydrated, (item) => getByPath(item, relation.foreignField));
|
|
1294
|
+
for (const [index, doc] of docs.entries()) {
|
|
1295
|
+
const matches = rebuildPopulateMatches(localValues[index], grouped, config.sort);
|
|
1296
|
+
const projectedMatches = matches.map((item) => projectPopulatedDocument(item, effectiveSelect, nestedPaths));
|
|
1297
|
+
doc[config.path] = relation.single ? projectedMatches[0] ?? null : projectedMatches;
|
|
1223
1298
|
}
|
|
1224
|
-
const projectedMatches = matches.map((item) => projectPopulatedDocument(item, effectiveSelect, nestedPaths));
|
|
1225
|
-
doc[config.path] = relation.single ? projectedMatches[0] ?? null : projectedMatches;
|
|
1226
1299
|
}
|
|
1227
1300
|
return docs;
|
|
1228
1301
|
} finally {
|
|
@@ -1423,12 +1496,28 @@ async function saveModelDocument(collection, document, options = {}) {
|
|
|
1423
1496
|
document._id = result.insertedId;
|
|
1424
1497
|
return document;
|
|
1425
1498
|
}
|
|
1426
|
-
async function removeModelDocument(collection, document) {
|
|
1499
|
+
async function removeModelDocument(collection, document, deleteOne = (filter) => collection.deleteOne(filter)) {
|
|
1427
1500
|
if (document._id === void 0) {
|
|
1428
1501
|
return false;
|
|
1429
1502
|
}
|
|
1430
|
-
const result = await
|
|
1431
|
-
|
|
1503
|
+
const result = await deleteOne({ _id: document._id });
|
|
1504
|
+
if (result.matchedCount === void 0 && result.modifiedCount === void 0 && result.deletedCount === void 0) {
|
|
1505
|
+
return result.acknowledged === true;
|
|
1506
|
+
}
|
|
1507
|
+
return Boolean(result.matchedCount || result.modifiedCount || result.deletedCount);
|
|
1508
|
+
}
|
|
1509
|
+
async function readPopulateRows(collection, query, options) {
|
|
1510
|
+
if (typeof collection.stream !== "function") {
|
|
1511
|
+
return await collection.find(query, options);
|
|
1512
|
+
}
|
|
1513
|
+
const stream = collection.stream(query, options);
|
|
1514
|
+
const rows = [];
|
|
1515
|
+
try {
|
|
1516
|
+
for await (const row of stream) rows.push(row);
|
|
1517
|
+
} finally {
|
|
1518
|
+
stream.destroy();
|
|
1519
|
+
}
|
|
1520
|
+
return rows;
|
|
1432
1521
|
}
|
|
1433
1522
|
|
|
1434
1523
|
// src/capabilities/model/model-soft-delete-helpers.ts
|
|
@@ -1436,7 +1525,53 @@ function deletedFilter(filter, softDeleteConfig) {
|
|
|
1436
1525
|
if (!softDeleteConfig) {
|
|
1437
1526
|
return filter ?? {};
|
|
1438
1527
|
}
|
|
1439
|
-
return
|
|
1528
|
+
return applyModelSoftDeleteFilter(filter, { onlyDeleted: true }, softDeleteConfig);
|
|
1529
|
+
}
|
|
1530
|
+
function filterVisibleBySoftDelete(docs, options, config) {
|
|
1531
|
+
if (!config?.enabled) return docs.filter((doc) => Boolean(doc));
|
|
1532
|
+
const rawOptions = options ?? {};
|
|
1533
|
+
applyModelSoftDeleteFilter({}, rawOptions, config);
|
|
1534
|
+
if (rawOptions.withDeleted) return docs.filter((doc) => Boolean(doc));
|
|
1535
|
+
return docs.filter((doc) => {
|
|
1536
|
+
if (!doc) return false;
|
|
1537
|
+
const value = doc[config.field];
|
|
1538
|
+
const deleted = config.type === "boolean" ? value === true : value !== void 0 && value !== null;
|
|
1539
|
+
return rawOptions.onlyDeleted ? deleted : !deleted;
|
|
1540
|
+
});
|
|
1541
|
+
}
|
|
1542
|
+
function withSoftDeleteProjection(options, config) {
|
|
1543
|
+
if (!config?.enabled) return { options, stripField: false };
|
|
1544
|
+
const rawOptions = options ?? {};
|
|
1545
|
+
const field = config.field;
|
|
1546
|
+
const projection = rawOptions.projection ?? rawOptions.project;
|
|
1547
|
+
if (Array.isArray(projection)) {
|
|
1548
|
+
if (projection.includes(field)) return { options, stripField: false };
|
|
1549
|
+
return { options: { ...rawOptions, projection: [...projection, field] }, stripField: true };
|
|
1550
|
+
}
|
|
1551
|
+
if (!projection || typeof projection !== "object") return { options, stripField: false };
|
|
1552
|
+
const projectionRecord = projection;
|
|
1553
|
+
if (projectionRecord[field] === 1 || projectionRecord[field] === true) return { options, stripField: false };
|
|
1554
|
+
const inclusion = Object.entries(projectionRecord).some(([key, value]) => key !== "_id" && (value === 1 || value === true));
|
|
1555
|
+
const nextProjection = { ...projectionRecord };
|
|
1556
|
+
if (inclusion) nextProjection[field] = 1;
|
|
1557
|
+
else delete nextProjection[field];
|
|
1558
|
+
return { options: { ...rawOptions, projection: nextProjection }, stripField: true };
|
|
1559
|
+
}
|
|
1560
|
+
function applySoftDeleteFindPageOptions(options, config) {
|
|
1561
|
+
const rawOptions = options ?? {};
|
|
1562
|
+
return { ...rawOptions, query: applyModelSoftDeleteFilter(rawOptions.query, rawOptions, config) };
|
|
1563
|
+
}
|
|
1564
|
+
function applySoftDeleteAggregatePipeline(pipeline, options, config) {
|
|
1565
|
+
if (!config?.enabled) return pipeline ?? [];
|
|
1566
|
+
const rawOptions = options ?? {};
|
|
1567
|
+
if (rawOptions.withDeleted) return pipeline ?? [];
|
|
1568
|
+
const softDeleteMatch = applyModelSoftDeleteFilter({}, rawOptions, config);
|
|
1569
|
+
const matchStage = { $match: softDeleteMatch };
|
|
1570
|
+
const stages = [...pipeline ?? []];
|
|
1571
|
+
if (stages.length > 0 && stages[0] && typeof stages[0] === "object" && "$geoNear" in stages[0]) {
|
|
1572
|
+
return [stages[0], matchStage, ...stages.slice(1)];
|
|
1573
|
+
}
|
|
1574
|
+
return [matchStage, ...stages];
|
|
1440
1575
|
}
|
|
1441
1576
|
function findWithDeletedDocuments(context, query, options) {
|
|
1442
1577
|
return new PopulatePromise(async (paths) => {
|
|
@@ -1479,7 +1614,7 @@ function restoreSoftDeletedDocuments(context, filter, options) {
|
|
|
1479
1614
|
return Promise.resolve({ modifiedCount: 0 });
|
|
1480
1615
|
}
|
|
1481
1616
|
return context.collection.updateOne(
|
|
1482
|
-
|
|
1617
|
+
deletedFilter(filter, softDeleteConfig),
|
|
1483
1618
|
{ $unset: { [softDeleteConfig.field]: 1 } },
|
|
1484
1619
|
options
|
|
1485
1620
|
);
|
|
@@ -1490,7 +1625,7 @@ function restoreManySoftDeletedDocuments(context, filter, options) {
|
|
|
1490
1625
|
return Promise.resolve({ modifiedCount: 0 });
|
|
1491
1626
|
}
|
|
1492
1627
|
return context.collection.updateMany(
|
|
1493
|
-
|
|
1628
|
+
deletedFilter(filter, softDeleteConfig),
|
|
1494
1629
|
{ $unset: { [softDeleteConfig.field]: 1 } },
|
|
1495
1630
|
options
|
|
1496
1631
|
);
|
|
@@ -3106,6 +3241,9 @@ var modelWriteSourceStorage = new AsyncLocalStorage();
|
|
|
3106
3241
|
function isPlainObject2(value) {
|
|
3107
3242
|
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
3108
3243
|
}
|
|
3244
|
+
function ownValue(record, key) {
|
|
3245
|
+
return Object.prototype.hasOwnProperty.call(record, key) ? record[key] : void 0;
|
|
3246
|
+
}
|
|
3109
3247
|
function invalidConfig(path5, reason) {
|
|
3110
3248
|
throw createError(
|
|
3111
3249
|
ErrorCodes.INVALID_OPERATION,
|
|
@@ -3126,19 +3264,19 @@ function normalizeRule(input, path5) {
|
|
|
3126
3264
|
if (!isPlainObject2(input)) {
|
|
3127
3265
|
invalidConfig(path5, "rule must be a string mode or an object");
|
|
3128
3266
|
}
|
|
3129
|
-
const mode = input
|
|
3267
|
+
const mode = ownValue(input, "mode") ?? DEFAULT_RULE.mode;
|
|
3130
3268
|
if (!ALLOWED_MODES.has(mode)) {
|
|
3131
3269
|
invalidConfig(`${path5}.mode`, 'must be "allow-both" or "model-only"');
|
|
3132
3270
|
}
|
|
3133
|
-
const raw = input
|
|
3271
|
+
const raw = ownValue(input, "raw") ?? DEFAULT_RULE.raw;
|
|
3134
3272
|
if (!ALLOWED_RAW.has(raw)) {
|
|
3135
3273
|
invalidConfig(`${path5}.raw`, 'must be "inherit", "allow", or "block"');
|
|
3136
3274
|
}
|
|
3137
|
-
const management = input
|
|
3275
|
+
const management = ownValue(input, "management") ?? DEFAULT_RULE.management;
|
|
3138
3276
|
if (!ALLOWED_MANAGEMENT.has(management)) {
|
|
3139
3277
|
invalidConfig(`${path5}.management`, 'must be "inherit", "allow", or "block"');
|
|
3140
3278
|
}
|
|
3141
|
-
const onViolation = input
|
|
3279
|
+
const onViolation = ownValue(input, "onViolation") ?? DEFAULT_RULE.onViolation;
|
|
3142
3280
|
if (!ALLOWED_VIOLATION_ACTIONS.has(onViolation)) {
|
|
3143
3281
|
invalidConfig(`${path5}.onViolation`, 'must be "throw" or "warn"');
|
|
3144
3282
|
}
|
|
@@ -3154,15 +3292,15 @@ function validateWritePathPolicyConfig(options) {
|
|
|
3154
3292
|
}
|
|
3155
3293
|
function normalizeWritePathPolicy(options) {
|
|
3156
3294
|
if (options === void 0) {
|
|
3157
|
-
return { default: { ...DEFAULT_RULE }, namespaces:
|
|
3295
|
+
return { default: { ...DEFAULT_RULE }, namespaces: /* @__PURE__ */ Object.create(null), enabled: false };
|
|
3158
3296
|
}
|
|
3159
3297
|
if (!isPlainObject2(options)) {
|
|
3160
3298
|
invalidConfig("writePathPolicy", "must be an object");
|
|
3161
3299
|
}
|
|
3162
3300
|
const typedOptions = options;
|
|
3163
|
-
const defaultRule = normalizeRule(typedOptions
|
|
3164
|
-
const namespacesInput = options
|
|
3165
|
-
const namespaces =
|
|
3301
|
+
const defaultRule = normalizeRule(ownValue(typedOptions, "default"), "writePathPolicy.default");
|
|
3302
|
+
const namespacesInput = ownValue(options, "namespaces");
|
|
3303
|
+
const namespaces = /* @__PURE__ */ Object.create(null);
|
|
3166
3304
|
if (namespacesInput !== void 0) {
|
|
3167
3305
|
if (!isPlainObject2(namespacesInput)) {
|
|
3168
3306
|
invalidConfig("writePathPolicy.namespaces", "must be an object");
|
|
@@ -3210,8 +3348,8 @@ function resolveWritePathRule(policy, namespace) {
|
|
|
3210
3348
|
if (candidate === "default") {
|
|
3211
3349
|
return { key: "default", rule: effectivePolicy.default };
|
|
3212
3350
|
}
|
|
3213
|
-
|
|
3214
|
-
|
|
3351
|
+
if (Object.prototype.hasOwnProperty.call(effectivePolicy.namespaces, candidate)) {
|
|
3352
|
+
const rule = effectivePolicy.namespaces[candidate];
|
|
3215
3353
|
return { key: candidate, rule };
|
|
3216
3354
|
}
|
|
3217
3355
|
}
|
|
@@ -3354,16 +3492,17 @@ var fallbackModelIndexTasks = /* @__PURE__ */ new Map();
|
|
|
3354
3492
|
function toCompatDefinition(definition) {
|
|
3355
3493
|
return definition;
|
|
3356
3494
|
}
|
|
3357
|
-
function stableIndexStringify(value) {
|
|
3495
|
+
function stableIndexStringify(value, mode = "options") {
|
|
3358
3496
|
if (value instanceof Date) {
|
|
3359
3497
|
return JSON.stringify(value.toISOString());
|
|
3360
3498
|
}
|
|
3361
3499
|
if (Array.isArray(value)) {
|
|
3362
|
-
return `[${value.map((item) => stableIndexStringify(item)).join(",")}]`;
|
|
3500
|
+
return `[${value.map((item) => stableIndexStringify(item, mode)).join(",")}]`;
|
|
3363
3501
|
}
|
|
3364
3502
|
if (value && typeof value === "object") {
|
|
3365
|
-
const entries = Object.entries(value).filter(([, current]) => current !== void 0)
|
|
3366
|
-
|
|
3503
|
+
const entries = Object.entries(value).filter(([, current]) => current !== void 0);
|
|
3504
|
+
if (mode === "options") entries.sort(([left], [right]) => left.localeCompare(right));
|
|
3505
|
+
return `{${entries.map(([key, current]) => `${JSON.stringify(key)}:${stableIndexStringify(current, key === "key" ? "key" : mode)}`).join(",")}}`;
|
|
3367
3506
|
}
|
|
3368
3507
|
return JSON.stringify(value) ?? "undefined";
|
|
3369
3508
|
}
|
|
@@ -3397,7 +3536,7 @@ function declaredOptionEntries(options) {
|
|
|
3397
3536
|
});
|
|
3398
3537
|
}
|
|
3399
3538
|
function indexOptionsMatch(existing, declared) {
|
|
3400
|
-
if (stableIndexStringify(getExistingIndexKey(existing)) !== stableIndexStringify(declared.key)) {
|
|
3539
|
+
if (stableIndexStringify(getExistingIndexKey(existing), "key") !== stableIndexStringify(declared.key, "key")) {
|
|
3401
3540
|
return false;
|
|
3402
3541
|
}
|
|
3403
3542
|
for (const [name, value] of declaredOptionEntries(declared.options)) {
|
|
@@ -3416,8 +3555,8 @@ function findExistingIndexByName(existingIndexes, name) {
|
|
|
3416
3555
|
return existingIndexes.find((index) => index.name === name);
|
|
3417
3556
|
}
|
|
3418
3557
|
function findExistingIndexByKey(existingIndexes, key) {
|
|
3419
|
-
const fingerprint = stableIndexStringify(key);
|
|
3420
|
-
return existingIndexes.find((index) => stableIndexStringify(getExistingIndexKey(index)) === fingerprint);
|
|
3558
|
+
const fingerprint = stableIndexStringify(key, "key");
|
|
3559
|
+
return existingIndexes.find((index) => stableIndexStringify(getExistingIndexKey(index), "key") === fingerprint);
|
|
3421
3560
|
}
|
|
3422
3561
|
function createIndexEnsureError(message, result, cause) {
|
|
3423
3562
|
return createError(ErrorCodes.MONGODB_ERROR, message, [result], cause);
|
|
@@ -4157,7 +4296,7 @@ async function readNearestBookmark(cache, namespace, keyDims, targetPage) {
|
|
|
4157
4296
|
|
|
4158
4297
|
// src/adapters/mongodb/queries/query-helpers.ts
|
|
4159
4298
|
import { createHmac, timingSafeEqual } from "node:crypto";
|
|
4160
|
-
import { ObjectId as ObjectId2 } from "mongodb";
|
|
4299
|
+
import { BSON, ObjectId as ObjectId2 } from "mongodb";
|
|
4161
4300
|
|
|
4162
4301
|
// src/utils/normalize.ts
|
|
4163
4302
|
function normalizeProjection(p) {
|
|
@@ -4255,12 +4394,17 @@ function shouldConvertQueryField(autoConvert, path5, depth) {
|
|
|
4255
4394
|
if (!config.enabled || depth > config.maxDepth) return false;
|
|
4256
4395
|
return shouldConvertObjectIdPath(path5, config);
|
|
4257
4396
|
}
|
|
4397
|
+
function isPlainQueryRecord(value) {
|
|
4398
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return false;
|
|
4399
|
+
const prototype = Object.getPrototypeOf(value);
|
|
4400
|
+
return prototype === Object.prototype || prototype === null;
|
|
4401
|
+
}
|
|
4258
4402
|
function normalizeQueryArray(value, autoConvert, path5, depth) {
|
|
4259
4403
|
return value.map((item, index) => {
|
|
4260
4404
|
if (typeof item === "string" && item.length === 24 && ObjectId2.isValid(item) && shouldConvertQueryField(autoConvert, path5, depth)) {
|
|
4261
4405
|
return new ObjectId2(item);
|
|
4262
4406
|
}
|
|
4263
|
-
if (
|
|
4407
|
+
if (isPlainQueryRecord(item)) {
|
|
4264
4408
|
return normalizeQueryFilter(item, autoConvert, `${path5}[${index}]`, depth + 1);
|
|
4265
4409
|
}
|
|
4266
4410
|
return item;
|
|
@@ -4276,7 +4420,7 @@ function normalizeQueryFilter(filter, autoConvert, fieldPath = "", depth = 0) {
|
|
|
4276
4420
|
}
|
|
4277
4421
|
for (const [key, value] of Object.entries(filter)) {
|
|
4278
4422
|
if (key === "$and" || key === "$or" || key === "$nor") {
|
|
4279
|
-
result[key] = Array.isArray(value) ? value.map((item) => item
|
|
4423
|
+
result[key] = Array.isArray(value) ? value.map((item) => isPlainQueryRecord(item) ? normalizeQueryFilter(item, autoConvert, fieldPath, depth + 1) : item) : value;
|
|
4280
4424
|
continue;
|
|
4281
4425
|
}
|
|
4282
4426
|
const currentPath = fieldPath ? `${fieldPath}.${key}` : key;
|
|
@@ -4285,7 +4429,7 @@ function normalizeQueryFilter(filter, autoConvert, fieldPath = "", depth = 0) {
|
|
|
4285
4429
|
result[key] = shouldConvert ? new ObjectId2(value) : value;
|
|
4286
4430
|
} else if (Array.isArray(value)) {
|
|
4287
4431
|
result[key] = shouldConvert ? normalizeQueryArray(value, autoConvert, currentPath, depth + 1) : value;
|
|
4288
|
-
} else if (
|
|
4432
|
+
} else if (isPlainQueryRecord(value)) {
|
|
4289
4433
|
const nested = value;
|
|
4290
4434
|
const hasOperators = Object.keys(nested).some((k) => k.startsWith("$"));
|
|
4291
4435
|
if (hasOperators) {
|
|
@@ -4297,7 +4441,7 @@ function normalizeQueryFilter(filter, autoConvert, fieldPath = "", depth = 0) {
|
|
|
4297
4441
|
);
|
|
4298
4442
|
} else if (shouldConvert && QUERY_OBJECTID_SCALAR_OPERATORS.has(op) && typeof opVal === "string" && opVal.length === 24 && ObjectId2.isValid(opVal)) {
|
|
4299
4443
|
nestedResult[op] = new ObjectId2(opVal);
|
|
4300
|
-
} else if (op === "$elemMatch" &&
|
|
4444
|
+
} else if (op === "$elemMatch" && isPlainQueryRecord(opVal)) {
|
|
4301
4445
|
nestedResult[op] = normalizeQueryFilter(opVal, autoConvert, currentPath, depth + 1);
|
|
4302
4446
|
} else {
|
|
4303
4447
|
nestedResult[op] = opVal;
|
|
@@ -4471,36 +4615,29 @@ async function isCollectionCacheBarrierActive(queryCache, collection, defaults =
|
|
|
4471
4615
|
buildCollectionCacheBarrierNamespaces(collection, defaults)
|
|
4472
4616
|
);
|
|
4473
4617
|
}
|
|
4474
|
-
|
|
4475
|
-
|
|
4476
|
-
|
|
4477
|
-
|
|
4478
|
-
if (value
|
|
4479
|
-
|
|
4480
|
-
|
|
4481
|
-
if (value instanceof
|
|
4482
|
-
|
|
4483
|
-
|
|
4484
|
-
|
|
4485
|
-
|
|
4486
|
-
|
|
4487
|
-
|
|
4488
|
-
|
|
4489
|
-
|
|
4490
|
-
|
|
4491
|
-
|
|
4492
|
-
|
|
4493
|
-
|
|
4494
|
-
|
|
4495
|
-
|
|
4496
|
-
|
|
4497
|
-
}
|
|
4498
|
-
return sorted;
|
|
4499
|
-
}
|
|
4500
|
-
return value;
|
|
4501
|
-
}
|
|
4502
|
-
function stableCacheKeyString(value) {
|
|
4503
|
-
return JSON.stringify(normalizeCacheKeyValue(value));
|
|
4618
|
+
var ORDERED_OPTION_FIELDS = /* @__PURE__ */ new Set(["query", "filter", "pipeline", "sort", "hint", "projection", "min", "max"]);
|
|
4619
|
+
function normalizeCacheKeyValue(value, mode) {
|
|
4620
|
+
if (value === null) return ["null"];
|
|
4621
|
+
if (value === void 0) return ["undefined"];
|
|
4622
|
+
if (typeof value === "number") return ["number", String(value)];
|
|
4623
|
+
if (typeof value === "bigint") return ["bigint", value.toString()];
|
|
4624
|
+
if (typeof value !== "object") return [typeof value, value];
|
|
4625
|
+
if (value instanceof Date) return ["date", String(value.getTime())];
|
|
4626
|
+
if (value instanceof RegExp) return ["regexp", value.source, value.flags];
|
|
4627
|
+
if (Array.isArray(value)) return ["array", value.map((item) => normalizeCacheKeyValue(item, mode))];
|
|
4628
|
+
const bsonType = value._bsontype;
|
|
4629
|
+
if (typeof bsonType === "string") {
|
|
4630
|
+
return ["bson", bsonType, BSON.EJSON.serialize(value, { relaxed: false })];
|
|
4631
|
+
}
|
|
4632
|
+
const entries = Object.entries(value).filter(([, item]) => item !== void 0 && typeof item !== "function" && typeof item !== "symbol");
|
|
4633
|
+
if (mode === "options") entries.sort(([left], [right]) => left.localeCompare(right));
|
|
4634
|
+
return ["object", entries.map(([key, item]) => [
|
|
4635
|
+
key,
|
|
4636
|
+
normalizeCacheKeyValue(item, mode === "options" && ORDERED_OPTION_FIELDS.has(key) ? "document" : mode)
|
|
4637
|
+
])];
|
|
4638
|
+
}
|
|
4639
|
+
function stableCacheKeyString(value, mode = "options") {
|
|
4640
|
+
return `v2:${JSON.stringify(normalizeCacheKeyValue(value, mode))}`;
|
|
4504
4641
|
}
|
|
4505
4642
|
function buildResultCacheKeyOptions(options = {}) {
|
|
4506
4643
|
const keyOptions = {};
|
|
@@ -4621,7 +4758,7 @@ function buildFindCacheKey(collection, defaults = {}, query, options) {
|
|
|
4621
4758
|
...options ?? {}
|
|
4622
4759
|
});
|
|
4623
4760
|
const namespace = buildCollectionCacheNamespace(collection, defaults);
|
|
4624
|
-
return `find:${namespace}:${stableCacheKeyString(normalizeQueryForCache(query, defaults))}:${stableCacheKeyString(buildResultCacheKeyOptions(executeOptions))}`;
|
|
4761
|
+
return `find:${namespace}:${stableCacheKeyString(normalizeQueryForCache(query, defaults), "document")}:${stableCacheKeyString(buildResultCacheKeyOptions(executeOptions))}`;
|
|
4625
4762
|
}
|
|
4626
4763
|
function buildFindOneCacheKey(collection, defaults = {}, query, options) {
|
|
4627
4764
|
const executeOptions = normalizeFindProjectionOptions({
|
|
@@ -4629,7 +4766,7 @@ function buildFindOneCacheKey(collection, defaults = {}, query, options) {
|
|
|
4629
4766
|
...options ?? {}
|
|
4630
4767
|
});
|
|
4631
4768
|
const namespace = buildCollectionCacheNamespace(collection, defaults);
|
|
4632
|
-
return `findOne:${namespace}:${stableCacheKeyString(normalizeQueryForCache(query, defaults))}:${stableCacheKeyString(buildResultCacheKeyOptions(executeOptions))}`;
|
|
4769
|
+
return `findOne:${namespace}:${stableCacheKeyString(normalizeQueryForCache(query, defaults), "document")}:${stableCacheKeyString(buildResultCacheKeyOptions(executeOptions))}`;
|
|
4633
4770
|
}
|
|
4634
4771
|
function buildCountCacheKey(collection, defaults = {}, query, options) {
|
|
4635
4772
|
const merged = {
|
|
@@ -4639,7 +4776,7 @@ function buildCountCacheKey(collection, defaults = {}, query, options) {
|
|
|
4639
4776
|
const { cache: _cache, ...keyOptions } = merged;
|
|
4640
4777
|
void _cache;
|
|
4641
4778
|
const namespace = buildCollectionCacheNamespace(collection, defaults);
|
|
4642
|
-
return `count:${namespace}:${stableCacheKeyString(normalizeQueryForCache(query, defaults))}:${stableCacheKeyString(buildResultCacheKeyOptions(keyOptions))}`;
|
|
4779
|
+
return `count:${namespace}:${stableCacheKeyString(normalizeQueryForCache(query, defaults), "document")}:${stableCacheKeyString(buildResultCacheKeyOptions(keyOptions))}`;
|
|
4643
4780
|
}
|
|
4644
4781
|
function buildDistinctCacheKey(collection, defaults = {}, field, query, options) {
|
|
4645
4782
|
const rawOptions = options ?? {};
|
|
@@ -4669,7 +4806,7 @@ function buildAggregateCacheKey(collection, defaults = {}, pipeline, options) {
|
|
|
4669
4806
|
...options ?? {}
|
|
4670
4807
|
};
|
|
4671
4808
|
const namespace = buildCollectionCacheNamespace(collection, defaults);
|
|
4672
|
-
return `aggregate:${namespace}:${stableCacheKeyString(normalizePipelineForCache(pipeline, defaults))}:${stableCacheKeyString(buildResultCacheKeyOptions(executeOptions))}`;
|
|
4809
|
+
return `aggregate:${namespace}:${stableCacheKeyString(normalizePipelineForCache(pipeline, defaults), "document")}:${stableCacheKeyString(buildResultCacheKeyOptions(executeOptions))}`;
|
|
4673
4810
|
}
|
|
4674
4811
|
function buildFindOneByIdCacheKey(collection, defaults = {}, id, options) {
|
|
4675
4812
|
const objectId = parseRequiredObjectId(id);
|
|
@@ -4684,6 +4821,8 @@ function buildFindByIdsCacheKey(collection, defaults = {}, ids, options) {
|
|
|
4684
4821
|
function buildFindPageCacheKey(collection, defaults = {}, options = {}) {
|
|
4685
4822
|
const ext = options;
|
|
4686
4823
|
const cursorTypes = ext.cursorTypes && typeof ext.cursorTypes === "object" && !Array.isArray(ext.cursorTypes) ? ext.cursorTypes : void 0;
|
|
4824
|
+
const { let: nestedLet, ...nestedOptions } = options.options ?? {};
|
|
4825
|
+
const effectiveLet = ext.let !== void 0 ? ext.let : nestedLet;
|
|
4687
4826
|
const payload = {
|
|
4688
4827
|
query: normalizeQueryForCache(options.query, defaults),
|
|
4689
4828
|
sort: normalizeSortShape(options.sort),
|
|
@@ -4705,9 +4844,10 @@ function buildFindPageCacheKey(collection, defaults = {}, options = {}) {
|
|
|
4705
4844
|
hint: ext.hint,
|
|
4706
4845
|
collation: ext.collation,
|
|
4707
4846
|
batchSize: ext.batchSize,
|
|
4708
|
-
|
|
4847
|
+
let: effectiveLet,
|
|
4848
|
+
options: nestedOptions
|
|
4709
4849
|
};
|
|
4710
|
-
const keyHash = hashPayload(payload);
|
|
4850
|
+
const keyHash = hashPayload(stableCacheKeyString(payload));
|
|
4711
4851
|
const namespace = buildCollectionCacheNamespace(collection, defaults);
|
|
4712
4852
|
return { key: `findPage:${namespace}:${keyHash}`, keyHash };
|
|
4713
4853
|
}
|
|
@@ -5054,29 +5194,30 @@ async function runStrictUpdateMany(context, filter, update, options) {
|
|
|
5054
5194
|
if (rawOptions.upsert === true) {
|
|
5055
5195
|
throw createError(ErrorCodes.INVALID_ARGUMENT, 'versionMode "strict" does not support upsert.');
|
|
5056
5196
|
}
|
|
5057
|
-
const docs = await context.collection.find(filter, {
|
|
5058
|
-
...buildModelVersionLookupOptions(rawOptions, {
|
|
5059
|
-
_id: 1,
|
|
5060
|
-
[context.versionConfig.field]: 1
|
|
5061
|
-
})
|
|
5062
|
-
});
|
|
5063
5197
|
let matchedCount = 0;
|
|
5064
5198
|
let modifiedCount = 0;
|
|
5065
5199
|
const conflictedIds = [];
|
|
5066
|
-
|
|
5067
|
-
|
|
5068
|
-
const
|
|
5069
|
-
|
|
5070
|
-
|
|
5071
|
-
|
|
5072
|
-
|
|
5073
|
-
|
|
5074
|
-
|
|
5075
|
-
|
|
5076
|
-
|
|
5200
|
+
const skippedIds = [];
|
|
5201
|
+
for await (const batch of iterateModelCandidateBatches(context, filter, rawOptions, 200)) {
|
|
5202
|
+
for (const doc of batch) {
|
|
5203
|
+
const id = doc._id;
|
|
5204
|
+
const expectedVersion = assertNumericExpectedVersion(doc[context.versionConfig.field], "updateMany");
|
|
5205
|
+
const result = await context.collection.updateOne(
|
|
5206
|
+
buildStrictCandidateFilter(filter, id, context.versionConfig.field, expectedVersion),
|
|
5207
|
+
update,
|
|
5208
|
+
options
|
|
5209
|
+
);
|
|
5210
|
+
if ((result?.matchedCount ?? 0) === 0) {
|
|
5211
|
+
if (await isModelVersionConflict(context, filter, id, context.versionConfig.field, expectedVersion, options)) {
|
|
5212
|
+
conflictedIds.push(id);
|
|
5213
|
+
} else {
|
|
5214
|
+
skippedIds.push(id);
|
|
5215
|
+
}
|
|
5216
|
+
continue;
|
|
5217
|
+
}
|
|
5218
|
+
matchedCount += result.matchedCount ?? 0;
|
|
5219
|
+
modifiedCount += result.modifiedCount ?? 0;
|
|
5077
5220
|
}
|
|
5078
|
-
matchedCount += result.matchedCount ?? 0;
|
|
5079
|
-
modifiedCount += result.modifiedCount ?? 0;
|
|
5080
5221
|
}
|
|
5081
5222
|
return {
|
|
5082
5223
|
acknowledged: true,
|
|
@@ -5085,7 +5226,9 @@ async function runStrictUpdateMany(context, filter, update, options) {
|
|
|
5085
5226
|
upsertedCount: 0,
|
|
5086
5227
|
upsertedId: null,
|
|
5087
5228
|
conflictCount: conflictedIds.length,
|
|
5088
|
-
conflictedIds
|
|
5229
|
+
conflictedIds,
|
|
5230
|
+
skippedCount: skippedIds.length,
|
|
5231
|
+
skippedIds
|
|
5089
5232
|
};
|
|
5090
5233
|
}
|
|
5091
5234
|
async function runStrictUpdateBatch(context, filter, update, options) {
|
|
@@ -5108,7 +5251,7 @@ async function runStrictUpdateBatch(context, filter, update, options) {
|
|
|
5108
5251
|
retryAttempts: _retryAttempts,
|
|
5109
5252
|
retryDelay: _retryDelay,
|
|
5110
5253
|
onRetry: _onRetry,
|
|
5111
|
-
sort
|
|
5254
|
+
sort: _sort,
|
|
5112
5255
|
...driverOptions
|
|
5113
5256
|
} = rawOptions;
|
|
5114
5257
|
void _batchSize;
|
|
@@ -5117,38 +5260,42 @@ async function runStrictUpdateBatch(context, filter, update, options) {
|
|
|
5117
5260
|
void _retryAttempts;
|
|
5118
5261
|
void _retryDelay;
|
|
5119
5262
|
void _onRetry;
|
|
5120
|
-
|
|
5121
|
-
|
|
5122
|
-
|
|
5123
|
-
|
|
5124
|
-
}),
|
|
5125
|
-
sort
|
|
5126
|
-
});
|
|
5263
|
+
void _sort;
|
|
5264
|
+
const countOptions = buildModelVersionLookupOptions(driverOptions, {});
|
|
5265
|
+
delete countOptions.projection;
|
|
5266
|
+
const totalCount = await context.collection.count(filter, countOptions);
|
|
5127
5267
|
const result = {
|
|
5128
5268
|
acknowledged: true,
|
|
5129
|
-
totalCount
|
|
5269
|
+
totalCount,
|
|
5130
5270
|
matchedCount: 0,
|
|
5131
5271
|
modifiedCount: 0,
|
|
5132
5272
|
upsertedCount: 0,
|
|
5133
|
-
batchCount:
|
|
5273
|
+
batchCount: 0,
|
|
5134
5274
|
errors: [],
|
|
5135
5275
|
retries: [],
|
|
5136
5276
|
conflictCount: 0,
|
|
5137
|
-
conflictedIds: []
|
|
5277
|
+
conflictedIds: [],
|
|
5278
|
+
skippedCount: 0,
|
|
5279
|
+
skippedIds: []
|
|
5138
5280
|
};
|
|
5139
|
-
for (
|
|
5140
|
-
|
|
5281
|
+
for await (const batch of iterateModelCandidateBatches(context, filter, driverOptions, batchSize)) {
|
|
5282
|
+
result.batchCount++;
|
|
5141
5283
|
for (const doc of batch) {
|
|
5142
5284
|
const id = doc._id;
|
|
5143
5285
|
const expectedVersion = assertNumericExpectedVersion(doc[context.versionConfig.field], "updateBatch");
|
|
5144
5286
|
const updateResult = await context.collection.updateOne(
|
|
5145
|
-
|
|
5287
|
+
buildStrictCandidateFilter(filter, id, context.versionConfig.field, expectedVersion),
|
|
5146
5288
|
update,
|
|
5147
5289
|
driverOptions
|
|
5148
5290
|
);
|
|
5149
5291
|
if ((updateResult?.matchedCount ?? 0) === 0) {
|
|
5150
|
-
|
|
5151
|
-
|
|
5292
|
+
if (await isModelVersionConflict(context, filter, id, context.versionConfig.field, expectedVersion, driverOptions)) {
|
|
5293
|
+
result.conflictCount = (result.conflictCount ?? 0) + 1;
|
|
5294
|
+
result.conflictedIds?.push(id);
|
|
5295
|
+
} else {
|
|
5296
|
+
result.skippedCount = (result.skippedCount ?? 0) + 1;
|
|
5297
|
+
result.skippedIds?.push(id);
|
|
5298
|
+
}
|
|
5152
5299
|
continue;
|
|
5153
5300
|
}
|
|
5154
5301
|
result.matchedCount += updateResult.matchedCount ?? 0;
|
|
@@ -5156,15 +5303,16 @@ async function runStrictUpdateBatch(context, filter, update, options) {
|
|
|
5156
5303
|
}
|
|
5157
5304
|
if (typeof onProgress === "function") {
|
|
5158
5305
|
onProgress({
|
|
5159
|
-
currentBatch:
|
|
5160
|
-
totalBatches:
|
|
5306
|
+
currentBatch: result.batchCount,
|
|
5307
|
+
totalBatches: Math.ceil(totalCount / batchSize),
|
|
5161
5308
|
modified: result.modifiedCount,
|
|
5162
5309
|
matched: result.matchedCount,
|
|
5163
|
-
total:
|
|
5164
|
-
percentage:
|
|
5310
|
+
total: totalCount,
|
|
5311
|
+
percentage: totalCount > 0 ? Math.round(result.modifiedCount / totalCount * 100) : null,
|
|
5165
5312
|
errors: result.errors.length,
|
|
5166
5313
|
retries: result.retries.length,
|
|
5167
|
-
conflicts: result.conflictCount ?? 0
|
|
5314
|
+
conflicts: result.conflictCount ?? 0,
|
|
5315
|
+
skipped: result.skippedCount ?? 0
|
|
5168
5316
|
});
|
|
5169
5317
|
}
|
|
5170
5318
|
}
|
|
@@ -5177,10 +5325,7 @@ function buildSoftDeletePatch(config, now) {
|
|
|
5177
5325
|
return { [config.field]: config.type === "boolean" ? true : now };
|
|
5178
5326
|
}
|
|
5179
5327
|
function buildSoftDeleteFilter(filter, config) {
|
|
5180
|
-
return {
|
|
5181
|
-
...filter ?? {},
|
|
5182
|
-
...config?.enabled ? { [config.field]: null } : {}
|
|
5183
|
-
};
|
|
5328
|
+
return applyModelSoftDeleteFilter(filter, {}, config);
|
|
5184
5329
|
}
|
|
5185
5330
|
function applyModelIncrementVersion(field, increment, versionConfig) {
|
|
5186
5331
|
if (!versionConfig?.enabled || field === void 0) {
|
|
@@ -5220,7 +5365,8 @@ async function orchestrateModelInsertOne(context, document, options) {
|
|
|
5220
5365
|
payload = validateModelSchemaPayload({
|
|
5221
5366
|
validateEnabled: context.validateEnabled,
|
|
5222
5367
|
schemaCache: context.schemaCache,
|
|
5223
|
-
schemaValidateFn: context.schemaValidateFn
|
|
5368
|
+
schemaValidateFn: context.schemaValidateFn,
|
|
5369
|
+
schemaError: context.schemaError
|
|
5224
5370
|
}, payload, options);
|
|
5225
5371
|
payload = applyModelInsertTimestamps(payload, context.timestampsConfig, () => context.nowDate());
|
|
5226
5372
|
payload = applyModelInsertVersion(payload, context.versionConfig);
|
|
@@ -5254,7 +5400,8 @@ async function orchestrateModelInsertMany(context, documents, options) {
|
|
|
5254
5400
|
doc = validateModelSchemaPayload({
|
|
5255
5401
|
validateEnabled: context.validateEnabled,
|
|
5256
5402
|
schemaCache: context.schemaCache,
|
|
5257
|
-
schemaValidateFn: context.schemaValidateFn
|
|
5403
|
+
schemaValidateFn: context.schemaValidateFn,
|
|
5404
|
+
schemaError: context.schemaError
|
|
5258
5405
|
}, doc, resolvedOptions, { index });
|
|
5259
5406
|
doc = applyModelInsertTimestamps(doc, context.timestampsConfig, () => context.nowDate());
|
|
5260
5407
|
doc = applyModelInsertVersion(doc, context.versionConfig);
|
|
@@ -5339,7 +5486,8 @@ async function orchestrateModelReplaceOne(context, filter, replacement, options)
|
|
|
5339
5486
|
let nextReplacement = validateModelSchemaPayload({
|
|
5340
5487
|
validateEnabled: context.validateEnabled,
|
|
5341
5488
|
schemaCache: context.schemaCache,
|
|
5342
|
-
schemaValidateFn: context.schemaValidateFn
|
|
5489
|
+
schemaValidateFn: context.schemaValidateFn,
|
|
5490
|
+
schemaError: context.schemaError
|
|
5343
5491
|
}, replacement, options);
|
|
5344
5492
|
nextReplacement = preserveModelReplaceCreatedAt(
|
|
5345
5493
|
replacement,
|
|
@@ -5398,7 +5546,8 @@ async function orchestrateModelFindOneAndReplace(context, filter, replacement, o
|
|
|
5398
5546
|
let nextReplacement = validateModelSchemaPayload({
|
|
5399
5547
|
validateEnabled: context.validateEnabled,
|
|
5400
5548
|
schemaCache: context.schemaCache,
|
|
5401
|
-
schemaValidateFn: context.schemaValidateFn
|
|
5549
|
+
schemaValidateFn: context.schemaValidateFn,
|
|
5550
|
+
schemaError: context.schemaError
|
|
5402
5551
|
}, replacement, options);
|
|
5403
5552
|
nextReplacement = preserveModelReplaceCreatedAt(
|
|
5404
5553
|
replacement,
|
|
@@ -5510,7 +5659,8 @@ async function orchestrateModelInsertBatch(context, docs, options) {
|
|
|
5510
5659
|
record = validateModelSchemaPayload({
|
|
5511
5660
|
validateEnabled: context.validateEnabled,
|
|
5512
5661
|
schemaCache: context.schemaCache,
|
|
5513
|
-
schemaValidateFn: context.schemaValidateFn
|
|
5662
|
+
schemaValidateFn: context.schemaValidateFn,
|
|
5663
|
+
schemaError: context.schemaError
|
|
5514
5664
|
}, record, resolvedOptions, { index });
|
|
5515
5665
|
record = applyModelInsertTimestamps(record, context.timestampsConfig, () => context.nowDate());
|
|
5516
5666
|
record = applyModelInsertVersion(record, context.versionConfig);
|
|
@@ -5588,7 +5738,7 @@ async function orchestrateModelDeleteOne(context, filter, options) {
|
|
|
5588
5738
|
let result;
|
|
5589
5739
|
if (softDeleteConfig?.enabled && !resolvedOptions._forceDelete) {
|
|
5590
5740
|
result = await context.collection.updateOne(
|
|
5591
|
-
|
|
5741
|
+
buildSoftDeleteFilter(filter, softDeleteConfig),
|
|
5592
5742
|
{ $set: { [softDeleteConfig.field]: softDeleteConfig.type === "boolean" ? true : context.nowDate() } },
|
|
5593
5743
|
options
|
|
5594
5744
|
);
|
|
@@ -5622,7 +5772,7 @@ async function orchestrateModelDeleteMany(context, filter, options) {
|
|
|
5622
5772
|
let result;
|
|
5623
5773
|
if (softDeleteConfig?.enabled && !resolvedOptions._forceDelete) {
|
|
5624
5774
|
result = await context.collection.updateMany(
|
|
5625
|
-
{
|
|
5775
|
+
{ $and: [filter ?? {}, buildModelVisibleSoftDeleteCondition(softDeleteConfig)] },
|
|
5626
5776
|
{ $set: { [softDeleteConfig.field]: softDeleteConfig.type === "boolean" ? true : context.nowDate() } },
|
|
5627
5777
|
options
|
|
5628
5778
|
);
|
|
@@ -5786,7 +5936,7 @@ async function inspectDeclaredRelationUsage(context, targets, inputOptions, targ
|
|
|
5786
5936
|
[inbound.localField]: { $in: targetValues }
|
|
5787
5937
|
};
|
|
5788
5938
|
const sourceSoftDelete = resolveModelSoftDeleteConfig(inbound.definition);
|
|
5789
|
-
const query = !options.includeSoftDeletedReferences && sourceSoftDelete?.enabled ? { $and: [baseQuery,
|
|
5939
|
+
const query = !options.includeSoftDeletedReferences && sourceSoftDelete?.enabled ? { $and: [baseQuery, buildModelVisibleSoftDeleteCondition(sourceSoftDelete)] } : baseQuery;
|
|
5790
5940
|
try {
|
|
5791
5941
|
const collection = context.runtime.scopedCollection(inbound.sourceCollection, scope);
|
|
5792
5942
|
if (!collection) {
|
|
@@ -5968,7 +6118,7 @@ async function executeProtectedRelationDelete(host, filter, options, force) {
|
|
|
5968
6118
|
const deleteOptions = stripRelationUsageOptions(options);
|
|
5969
6119
|
let target;
|
|
5970
6120
|
try {
|
|
5971
|
-
const targetFilter = force || !host.softDeleteConfig?.enabled ? filter :
|
|
6121
|
+
const targetFilter = force || !host.softDeleteConfig?.enabled ? filter : applyModelSoftDeleteFilter(filter, void 0, host.softDeleteConfig);
|
|
5972
6122
|
const rawTarget = await host.collection.findOne(
|
|
5973
6123
|
targetFilter,
|
|
5974
6124
|
relationTargetReadOptions(options, buildRelationUsageTargetProjection(host.modelName))
|
|
@@ -6052,7 +6202,7 @@ var ModelInstance = class {
|
|
|
6052
6202
|
this.softDeleteConfig = this._softDeleteConfig;
|
|
6053
6203
|
this._versionConfig = resolveModelVersionConfig(options.definition);
|
|
6054
6204
|
this._v1HooksFactory = resolveModelHooksFactory(options.definition);
|
|
6055
|
-
scheduleModelIndexes(this.collection, options.definition, this._softDeleteConfig, {
|
|
6205
|
+
if (!this._schemaError) scheduleModelIndexes(this.collection, options.definition, this._softDeleteConfig, {
|
|
6056
6206
|
runtime: this.runtime,
|
|
6057
6207
|
dbName: options.dbName,
|
|
6058
6208
|
poolName: options.poolName,
|
|
@@ -6112,46 +6262,6 @@ var ModelInstance = class {
|
|
|
6112
6262
|
}
|
|
6113
6263
|
return result;
|
|
6114
6264
|
}
|
|
6115
|
-
isVisibleBySoftDelete(doc, options) {
|
|
6116
|
-
if (!doc || !this._softDeleteConfig?.enabled) {
|
|
6117
|
-
return Boolean(doc);
|
|
6118
|
-
}
|
|
6119
|
-
const rawOptions = options ?? {};
|
|
6120
|
-
if (rawOptions.withDeleted) {
|
|
6121
|
-
return true;
|
|
6122
|
-
}
|
|
6123
|
-
const value = doc[this._softDeleteConfig.field];
|
|
6124
|
-
if (rawOptions.onlyDeleted) {
|
|
6125
|
-
return value !== void 0 && value !== null;
|
|
6126
|
-
}
|
|
6127
|
-
return value === void 0 || value === null;
|
|
6128
|
-
}
|
|
6129
|
-
filterVisibleBySoftDelete(docs, options) {
|
|
6130
|
-
return docs.filter((doc) => this.isVisibleBySoftDelete(doc, options));
|
|
6131
|
-
}
|
|
6132
|
-
applySoftDeleteFindPageOptions(options) {
|
|
6133
|
-
const rawOptions = options ?? {};
|
|
6134
|
-
return {
|
|
6135
|
-
...rawOptions,
|
|
6136
|
-
query: applyModelSoftDeleteFilter(rawOptions.query, rawOptions, this._softDeleteConfig)
|
|
6137
|
-
};
|
|
6138
|
-
}
|
|
6139
|
-
applySoftDeleteAggregatePipeline(pipeline, options) {
|
|
6140
|
-
if (!this._softDeleteConfig?.enabled) {
|
|
6141
|
-
return pipeline ?? [];
|
|
6142
|
-
}
|
|
6143
|
-
const rawOptions = options ?? {};
|
|
6144
|
-
if (rawOptions.withDeleted) {
|
|
6145
|
-
return pipeline ?? [];
|
|
6146
|
-
}
|
|
6147
|
-
const softDeleteMatch = applyModelSoftDeleteFilter({}, rawOptions, this._softDeleteConfig);
|
|
6148
|
-
const matchStage = { $match: softDeleteMatch };
|
|
6149
|
-
const stages = [...pipeline ?? []];
|
|
6150
|
-
if (stages.length > 0 && stages[0] && typeof stages[0] === "object" && "$geoNear" in stages[0]) {
|
|
6151
|
-
return [stages[0], matchStage, ...stages.slice(1)];
|
|
6152
|
-
}
|
|
6153
|
-
return [matchStage, ...stages];
|
|
6154
|
-
}
|
|
6155
6265
|
// ── public API ────────────────────────────────────────────────────────────────
|
|
6156
6266
|
find(query, options) {
|
|
6157
6267
|
return new PopulatePromise(async (paths) => {
|
|
@@ -6187,8 +6297,12 @@ var ModelInstance = class {
|
|
|
6187
6297
|
}
|
|
6188
6298
|
findOneById(id, options) {
|
|
6189
6299
|
return new PopulatePromise(async (paths) => {
|
|
6190
|
-
const
|
|
6191
|
-
|
|
6300
|
+
const lookupById = (nextId, nextOptions) => this._softDeleteConfig?.enabled ? this.collection.findOne(
|
|
6301
|
+
applyModelSoftDeleteFilter({ _id: parseRequiredObjectId(nextId) }, nextOptions, this._softDeleteConfig),
|
|
6302
|
+
nextOptions
|
|
6303
|
+
) : this.collection.findOneById(nextId, nextOptions);
|
|
6304
|
+
const doc = this._v1HooksFactory ? await this.runV1HookedOperation("find", [id, options], lookupById) : await lookupById(id, options);
|
|
6305
|
+
return this.populateSingle(this.hydrateDocument(doc), paths);
|
|
6192
6306
|
});
|
|
6193
6307
|
}
|
|
6194
6308
|
findById(id, options) {
|
|
@@ -6196,13 +6310,25 @@ var ModelInstance = class {
|
|
|
6196
6310
|
}
|
|
6197
6311
|
findByIds(ids, options) {
|
|
6198
6312
|
return new PopulatePromise(async (paths) => {
|
|
6199
|
-
|
|
6200
|
-
|
|
6313
|
+
let stripField = false;
|
|
6314
|
+
const lookupByIds = (nextIds, nextOptions) => {
|
|
6315
|
+
const projection = withSoftDeleteProjection(nextOptions, this._softDeleteConfig);
|
|
6316
|
+
stripField = projection.stripField;
|
|
6317
|
+
return this.collection.findByIds(nextIds, projection.options);
|
|
6318
|
+
};
|
|
6319
|
+
const docs = this._v1HooksFactory ? await this.runV1HookedOperation("find", [ids, options], lookupByIds) : await lookupByIds(ids, options);
|
|
6320
|
+
const visible = filterVisibleBySoftDelete(docs, options, this._softDeleteConfig);
|
|
6321
|
+
const projected = stripField && this._softDeleteConfig ? visible.map((doc) => {
|
|
6322
|
+
const copy = { ...doc };
|
|
6323
|
+
delete copy[this._softDeleteConfig.field];
|
|
6324
|
+
return copy;
|
|
6325
|
+
}) : visible;
|
|
6326
|
+
return this.populateDocuments(this.hydrateDocuments(projected), paths);
|
|
6201
6327
|
});
|
|
6202
6328
|
}
|
|
6203
6329
|
findPage(options) {
|
|
6204
6330
|
return new PopulatePromise(async (paths) => {
|
|
6205
|
-
const filteredOptions =
|
|
6331
|
+
const filteredOptions = applySoftDeleteFindPageOptions(options, this._softDeleteConfig);
|
|
6206
6332
|
const result = this._v1HooksFactory ? await this.runV1HookedOperation("find", [filteredOptions], (nextOptions) => this.collection.findPage(nextOptions)) : await this.collection.findPage(filteredOptions);
|
|
6207
6333
|
return {
|
|
6208
6334
|
...result,
|
|
@@ -6354,7 +6480,7 @@ var ModelInstance = class {
|
|
|
6354
6480
|
return this.runV1HookedOperation("find", [key, filteredQuery, options], (nextKey, nextQuery, nextOptions) => this.collection.distinct(nextKey, nextQuery, nextOptions));
|
|
6355
6481
|
}
|
|
6356
6482
|
aggregate(pipeline, options) {
|
|
6357
|
-
const filteredPipeline =
|
|
6483
|
+
const filteredPipeline = applySoftDeleteAggregatePipeline(pipeline, options, this._softDeleteConfig);
|
|
6358
6484
|
return this.runV1HookedOperation("find", [filteredPipeline, options], (nextPipeline, nextOptions) => {
|
|
6359
6485
|
const execute = () => this.collection.aggregate(nextPipeline, nextOptions);
|
|
6360
6486
|
if (resolveAggregateWriteTarget(Array.isArray(nextPipeline) ? nextPipeline : [])) {
|
|
@@ -6483,13 +6609,14 @@ var ModelInstance = class {
|
|
|
6483
6609
|
schemaValidationContext: {
|
|
6484
6610
|
validateEnabled: this._validateEnabled,
|
|
6485
6611
|
schemaCache: this._schemaCache,
|
|
6486
|
-
schemaValidateFn: this._schemaValidateFn
|
|
6612
|
+
schemaValidateFn: this._schemaValidateFn,
|
|
6613
|
+
schemaError: this._schemaError
|
|
6487
6614
|
},
|
|
6488
6615
|
nowFactory: () => this.nowDate()
|
|
6489
6616
|
}));
|
|
6490
6617
|
}
|
|
6491
6618
|
async removeDocument(document) {
|
|
6492
|
-
return this.runModelWrite(() => removeModelDocument(this.collection, document));
|
|
6619
|
+
return this.runModelWrite(() => removeModelDocument(this.collection, document, (filter) => this.deleteOne(filter)));
|
|
6493
6620
|
}
|
|
6494
6621
|
applyDefaults(document) {
|
|
6495
6622
|
return applyModelDefaults(this.definition, document);
|
|
@@ -6507,6 +6634,7 @@ var ModelInstance = class {
|
|
|
6507
6634
|
validateEnabled: this._validateEnabled,
|
|
6508
6635
|
schemaCache: this._schemaCache,
|
|
6509
6636
|
schemaValidateFn: this._schemaValidateFn,
|
|
6637
|
+
schemaError: this._schemaError,
|
|
6510
6638
|
hooksFactory: this._v1HooksFactory,
|
|
6511
6639
|
runHook: (hookName, context) => this.runHook(hookName, context)
|
|
6512
6640
|
};
|
|
@@ -6522,6 +6650,11 @@ var ModelInstance = class {
|
|
|
6522
6650
|
|
|
6523
6651
|
// src/capabilities/transaction/index.ts
|
|
6524
6652
|
import { randomBytes as randomBytes2 } from "node:crypto";
|
|
6653
|
+
function assertManagedTransactionWriteAllowed(options) {
|
|
6654
|
+
const session = options?.session;
|
|
6655
|
+
const managed = session?.__monSQLizeTransaction;
|
|
6656
|
+
if (typeof managed?.assertAcceptingWrites === "function") managed.assertAcceptingWrites();
|
|
6657
|
+
}
|
|
6525
6658
|
function toPublicTransactionStatus(state) {
|
|
6526
6659
|
return state === "active" ? "started" : state;
|
|
6527
6660
|
}
|
|
@@ -6622,6 +6755,7 @@ var Transaction = class {
|
|
|
6622
6755
|
this.options = options;
|
|
6623
6756
|
this.id = `tx_${randomBytes2(8).toString("hex")}`;
|
|
6624
6757
|
this.state = "pending";
|
|
6758
|
+
this.acceptingWrites = false;
|
|
6625
6759
|
this.startedAt = null;
|
|
6626
6760
|
this.timeoutTimer = null;
|
|
6627
6761
|
this.pendingInvalidations = /* @__PURE__ */ new Set();
|
|
@@ -6640,6 +6774,7 @@ var Transaction = class {
|
|
|
6640
6774
|
}
|
|
6641
6775
|
this.session.startTransaction(this.options.transactionOptions);
|
|
6642
6776
|
this.state = "active";
|
|
6777
|
+
this.acceptingWrites = true;
|
|
6643
6778
|
this.startedAt = Date.now();
|
|
6644
6779
|
const timeout = this.options.timeout ?? 3e4;
|
|
6645
6780
|
if (timeout > 0) {
|
|
@@ -6662,6 +6797,7 @@ var Transaction = class {
|
|
|
6662
6797
|
if (this.state !== "active") {
|
|
6663
6798
|
throw createError(ErrorCodes.INVALID_OPERATION, `Cannot commit transaction in state: ${this.state}`);
|
|
6664
6799
|
}
|
|
6800
|
+
this.acceptingWrites = false;
|
|
6665
6801
|
if (typeof this.session.commitTransaction === "function") {
|
|
6666
6802
|
await commitTransactionWithRetry(this.session, this.options.logger);
|
|
6667
6803
|
}
|
|
@@ -6687,6 +6823,7 @@ var Transaction = class {
|
|
|
6687
6823
|
* @since v1.4.0
|
|
6688
6824
|
*/
|
|
6689
6825
|
async abort() {
|
|
6826
|
+
this.acceptingWrites = false;
|
|
6690
6827
|
if (this.state !== "pending" && this.state !== "active") {
|
|
6691
6828
|
return;
|
|
6692
6829
|
}
|
|
@@ -6711,10 +6848,16 @@ var Transaction = class {
|
|
|
6711
6848
|
* @since v1.4.0
|
|
6712
6849
|
*/
|
|
6713
6850
|
async end() {
|
|
6851
|
+
this.acceptingWrites = false;
|
|
6714
6852
|
this.clearTimeout();
|
|
6715
6853
|
this.options.lockManager?.releaseLocks(this.id);
|
|
6716
6854
|
await this.session.endSession();
|
|
6717
6855
|
}
|
|
6856
|
+
assertAcceptingWrites() {
|
|
6857
|
+
if (this.state !== "active" || !this.acceptingWrites) {
|
|
6858
|
+
throw createError(ErrorCodes.INVALID_OPERATION, `Managed transaction ${this.id} is no longer accepting writes.`);
|
|
6859
|
+
}
|
|
6860
|
+
}
|
|
6718
6861
|
/**
|
|
6719
6862
|
* Record a cache invalidation intent.
|
|
6720
6863
|
* @since v1.4.0
|
|
@@ -6888,12 +7031,12 @@ var TransactionManager = class {
|
|
|
6888
7031
|
return result;
|
|
6889
7032
|
} catch (error) {
|
|
6890
7033
|
lastError = error;
|
|
6891
|
-
await transaction.abort();
|
|
6892
|
-
this.recordStats(transaction, Date.now() - startedAt, false);
|
|
6893
7034
|
if (transaction.state === "committed") {
|
|
6894
7035
|
throw error;
|
|
6895
7036
|
}
|
|
6896
|
-
|
|
7037
|
+
await transaction.abort();
|
|
7038
|
+
this.recordStats(transaction, Date.now() - startedAt, false);
|
|
7039
|
+
if (!enableRetry || attempt === maxRetries || isUnknownTransactionCommitResult(error) || !isTransientTransactionError(error)) {
|
|
6897
7040
|
throw error;
|
|
6898
7041
|
}
|
|
6899
7042
|
await sleep(retryDelay * Math.pow(retryBackoff, attempt));
|
|
@@ -6994,22 +7137,34 @@ function stringifySessionId(id) {
|
|
|
6994
7137
|
}
|
|
6995
7138
|
return String(id);
|
|
6996
7139
|
}
|
|
6997
|
-
function
|
|
6998
|
-
|
|
6999
|
-
|
|
7000
|
-
|
|
7001
|
-
|
|
7002
|
-
|
|
7003
|
-
|
|
7140
|
+
function transactionErrorChain(error) {
|
|
7141
|
+
const chain = [];
|
|
7142
|
+
const seen = /* @__PURE__ */ new Set();
|
|
7143
|
+
let current = error;
|
|
7144
|
+
while (current && typeof current === "object" && chain.length < 8 && !seen.has(current)) {
|
|
7145
|
+
seen.add(current);
|
|
7146
|
+
const candidate = current;
|
|
7147
|
+
chain.push(candidate);
|
|
7148
|
+
current = candidate.cause;
|
|
7149
|
+
}
|
|
7150
|
+
return chain;
|
|
7151
|
+
}
|
|
7152
|
+
function hasTransactionErrorLabel(candidate, label) {
|
|
7153
|
+
if (Array.isArray(candidate.errorLabels) && candidate.errorLabels.includes(label)) return true;
|
|
7154
|
+
if (typeof candidate.hasErrorLabel === "function") {
|
|
7155
|
+
try {
|
|
7156
|
+
return Boolean(candidate.hasErrorLabel.call(candidate, label));
|
|
7157
|
+
} catch {
|
|
7158
|
+
}
|
|
7004
7159
|
}
|
|
7005
|
-
return
|
|
7160
|
+
return false;
|
|
7006
7161
|
}
|
|
7007
7162
|
function isUnknownTransactionCommitResult(error) {
|
|
7008
|
-
|
|
7009
|
-
|
|
7010
|
-
|
|
7011
|
-
|
|
7012
|
-
return
|
|
7163
|
+
return transactionErrorChain(error).some((candidate) => hasTransactionErrorLabel(candidate, "UnknownTransactionCommitResult"));
|
|
7164
|
+
}
|
|
7165
|
+
function isTransientTransactionError(error) {
|
|
7166
|
+
if (isUnknownTransactionCommitResult(error)) return false;
|
|
7167
|
+
return transactionErrorChain(error).some((candidate) => hasTransactionErrorLabel(candidate, "TransientTransactionError") || candidate.code === 112 || candidate.code === 117 || candidate.codeName === "WriteConflict" || candidate.codeName === "LockTimeout");
|
|
7013
7168
|
}
|
|
7014
7169
|
async function commitTransactionWithRetry(session, logger) {
|
|
7015
7170
|
const maxAttempts = 3;
|
|
@@ -7525,26 +7680,27 @@ var CountQueue = class {
|
|
|
7525
7680
|
* @returns The result of fn
|
|
7526
7681
|
* @throws A controlled error when the queue is full or the wait times out
|
|
7527
7682
|
*/
|
|
7528
|
-
async execute(fn) {
|
|
7683
|
+
async execute(fn, options = {}) {
|
|
7529
7684
|
const startTime = Date.now();
|
|
7685
|
+
const signal = options.signal;
|
|
7686
|
+
if (signal?.aborted) throw createError(ErrorCodes.INVALID_OPERATION, "Count operation aborted");
|
|
7530
7687
|
if (this.running >= this.concurrency) {
|
|
7531
7688
|
if (this.queue.length >= this.maxQueueSize) {
|
|
7532
7689
|
this.stats.rejected += 1;
|
|
7533
7690
|
throw createError(ErrorCodes.INVALID_OPERATION, `Count queue is full (${this.maxQueueSize})`);
|
|
7534
7691
|
}
|
|
7535
7692
|
this.stats.queued += 1;
|
|
7536
|
-
await this._waitInQueue(startTime);
|
|
7693
|
+
await this._waitInQueue(startTime, signal);
|
|
7537
7694
|
}
|
|
7695
|
+
if (signal?.aborted) throw createError(ErrorCodes.INVALID_OPERATION, "Count operation aborted");
|
|
7538
7696
|
this.running += 1;
|
|
7539
7697
|
this.stats.executed += 1;
|
|
7540
|
-
|
|
7541
|
-
|
|
7542
|
-
|
|
7543
|
-
return await this._executeWithTimeout(fn, remainingMs);
|
|
7544
|
-
} finally {
|
|
7698
|
+
const elapsed = Date.now() - startTime;
|
|
7699
|
+
const remainingMs = Math.max(1, this.timeout - elapsed);
|
|
7700
|
+
return this._executeWithTimeout(fn, remainingMs, signal, () => {
|
|
7545
7701
|
this.running -= 1;
|
|
7546
7702
|
this._wakeNext();
|
|
7547
|
-
}
|
|
7703
|
+
});
|
|
7548
7704
|
}
|
|
7549
7705
|
/**
|
|
7550
7706
|
* Get queue statistics (including live state).
|
|
@@ -7579,17 +7735,28 @@ var CountQueue = class {
|
|
|
7579
7735
|
const entry = this.queue.shift();
|
|
7580
7736
|
if (entry) {
|
|
7581
7737
|
clearTimeout(entry.timer);
|
|
7738
|
+
entry.cleanup();
|
|
7582
7739
|
entry.reject(createError(ErrorCodes.INVALID_OPERATION, "Count queue was cleared before execution"));
|
|
7583
7740
|
}
|
|
7584
7741
|
}
|
|
7585
7742
|
}
|
|
7586
|
-
_waitInQueue(startTime) {
|
|
7743
|
+
_waitInQueue(startTime, signal) {
|
|
7587
7744
|
return new Promise((resolve, reject) => {
|
|
7588
7745
|
let entry;
|
|
7746
|
+
const onAbort = () => {
|
|
7747
|
+
const index = this.queue.indexOf(entry);
|
|
7748
|
+
if (index !== -1) {
|
|
7749
|
+
this.queue.splice(index, 1);
|
|
7750
|
+
clearTimeout(entry.timer);
|
|
7751
|
+
entry.cleanup();
|
|
7752
|
+
reject(createError(ErrorCodes.INVALID_OPERATION, "Count operation aborted"));
|
|
7753
|
+
}
|
|
7754
|
+
};
|
|
7589
7755
|
const timer = setTimeout(() => {
|
|
7590
7756
|
const index = this.queue.indexOf(entry);
|
|
7591
7757
|
if (index !== -1) {
|
|
7592
7758
|
this.queue.splice(index, 1);
|
|
7759
|
+
entry.cleanup();
|
|
7593
7760
|
this.stats.timeout += 1;
|
|
7594
7761
|
reject(createError(ErrorCodes.OPERATION_TIMEOUT, `Count queue wait timeout (${this.timeout}ms)`));
|
|
7595
7762
|
}
|
|
@@ -7602,9 +7769,12 @@ var CountQueue = class {
|
|
|
7602
7769
|
},
|
|
7603
7770
|
reject,
|
|
7604
7771
|
timer,
|
|
7605
|
-
startTime
|
|
7772
|
+
startTime,
|
|
7773
|
+
cleanup: () => signal?.removeEventListener("abort", onAbort)
|
|
7606
7774
|
};
|
|
7607
7775
|
this.queue.push(entry);
|
|
7776
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
7777
|
+
if (signal?.aborted) onAbort();
|
|
7608
7778
|
});
|
|
7609
7779
|
}
|
|
7610
7780
|
_wakeNext() {
|
|
@@ -7612,6 +7782,7 @@ var CountQueue = class {
|
|
|
7612
7782
|
const entry = this.queue.shift();
|
|
7613
7783
|
if (entry) {
|
|
7614
7784
|
clearTimeout(entry.timer);
|
|
7785
|
+
entry.cleanup();
|
|
7615
7786
|
entry.resolve();
|
|
7616
7787
|
}
|
|
7617
7788
|
}
|
|
@@ -7628,10 +7799,12 @@ var CountQueue = class {
|
|
|
7628
7799
|
}
|
|
7629
7800
|
return value;
|
|
7630
7801
|
}
|
|
7631
|
-
_executeWithTimeout(fn, timeoutMs
|
|
7802
|
+
_executeWithTimeout(fn, timeoutMs, signal, release) {
|
|
7632
7803
|
let timer = null;
|
|
7633
7804
|
const controller = new AbortController();
|
|
7634
|
-
|
|
7805
|
+
let rejectCancellation;
|
|
7806
|
+
const cancellation = new Promise((_, reject) => {
|
|
7807
|
+
rejectCancellation = reject;
|
|
7635
7808
|
timer = setTimeout(
|
|
7636
7809
|
() => {
|
|
7637
7810
|
controller.abort();
|
|
@@ -7641,14 +7814,22 @@ var CountQueue = class {
|
|
|
7641
7814
|
timeoutMs
|
|
7642
7815
|
);
|
|
7643
7816
|
});
|
|
7817
|
+
const onAbort = () => {
|
|
7818
|
+
controller.abort();
|
|
7819
|
+
rejectCancellation(createError(ErrorCodes.INVALID_OPERATION, "Count operation aborted"));
|
|
7820
|
+
};
|
|
7821
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
7644
7822
|
let taskPromise;
|
|
7645
7823
|
try {
|
|
7646
7824
|
taskPromise = Promise.resolve(fn(controller.signal));
|
|
7647
7825
|
} catch (error) {
|
|
7648
7826
|
taskPromise = Promise.reject(error);
|
|
7649
7827
|
}
|
|
7650
|
-
|
|
7828
|
+
void taskPromise.then(release, release);
|
|
7829
|
+
if (signal?.aborted) onAbort();
|
|
7830
|
+
return Promise.race([taskPromise, cancellation]).finally(() => {
|
|
7651
7831
|
if (timer !== null) clearTimeout(timer);
|
|
7832
|
+
signal?.removeEventListener("abort", onAbort);
|
|
7652
7833
|
});
|
|
7653
7834
|
}
|
|
7654
7835
|
_updateWaitTimeStats(waitTime) {
|
|
@@ -8177,6 +8358,9 @@ var ConnectionPoolManager = class {
|
|
|
8177
8358
|
this._closed = false;
|
|
8178
8359
|
this._configs = /* @__PURE__ */ new Map();
|
|
8179
8360
|
this._pendingAdds = /* @__PURE__ */ new Set();
|
|
8361
|
+
this._pendingAddTasks = /* @__PURE__ */ new Set();
|
|
8362
|
+
this._generation = 0;
|
|
8363
|
+
this._closePromise = null;
|
|
8180
8364
|
this.logger = options.logger ?? null;
|
|
8181
8365
|
this.maxPoolsCount = options.maxPoolsCount ?? 10;
|
|
8182
8366
|
this.strategy = options.poolStrategy ?? "auto";
|
|
@@ -8203,35 +8387,21 @@ var ConnectionPoolManager = class {
|
|
|
8203
8387
|
*/
|
|
8204
8388
|
async addPool(config) {
|
|
8205
8389
|
validatePoolConfig(config);
|
|
8390
|
+
if (this._closed) {
|
|
8391
|
+
throw createError(ErrorCodes.INVALID_OPERATION, "Connection pool manager is closed");
|
|
8392
|
+
}
|
|
8206
8393
|
if (this.pools.has(config.name) || this._pendingAdds.has(config.name)) {
|
|
8207
8394
|
throw createError(ErrorCodes.INVALID_CONFIG, `Pool '${config.name}' already exists`);
|
|
8208
8395
|
}
|
|
8209
|
-
if (this.maxPoolsCount > 0 && this.pools.size >= this.maxPoolsCount) {
|
|
8396
|
+
if (this.maxPoolsCount > 0 && this.pools.size + this._pendingAdds.size >= this.maxPoolsCount) {
|
|
8210
8397
|
throw createError(ErrorCodes.INVALID_CONFIG, `Maximum pool count (${this.maxPoolsCount}) reached`);
|
|
8211
8398
|
}
|
|
8212
8399
|
this._pendingAdds.add(config.name);
|
|
8400
|
+
const generation = this._generation;
|
|
8401
|
+
const task = this._finishAddPool(config, generation);
|
|
8402
|
+
this._pendingAddTasks.add(task);
|
|
8213
8403
|
try {
|
|
8214
|
-
|
|
8215
|
-
if (this.pools.has(config.name)) {
|
|
8216
|
-
await client.close().catch(() => {
|
|
8217
|
-
});
|
|
8218
|
-
throw createError(ErrorCodes.INVALID_CONFIG, `Pool '${config.name}' already exists`);
|
|
8219
|
-
}
|
|
8220
|
-
this.pools.set(config.name, {
|
|
8221
|
-
client,
|
|
8222
|
-
config,
|
|
8223
|
-
createdAt: Date.now()
|
|
8224
|
-
});
|
|
8225
|
-
this.healthStatus.set(config.name, {
|
|
8226
|
-
status: "up",
|
|
8227
|
-
consecutiveFailures: 0,
|
|
8228
|
-
lastCheckTime: null,
|
|
8229
|
-
lastError: null,
|
|
8230
|
-
uptime: 0
|
|
8231
|
-
});
|
|
8232
|
-
this.stats.set(config.name, createEmptyPoolStats(config.name));
|
|
8233
|
-
this._configs.set(config.name, config);
|
|
8234
|
-
this._healthChecker.register(config.name, config.healthCheck ?? {});
|
|
8404
|
+
await task;
|
|
8235
8405
|
} catch (err) {
|
|
8236
8406
|
const error = err;
|
|
8237
8407
|
const msg = error.message ?? "";
|
|
@@ -8245,7 +8415,30 @@ var ConnectionPoolManager = class {
|
|
|
8245
8415
|
throw err;
|
|
8246
8416
|
} finally {
|
|
8247
8417
|
this._pendingAdds.delete(config.name);
|
|
8418
|
+
this._pendingAddTasks.delete(task);
|
|
8419
|
+
}
|
|
8420
|
+
}
|
|
8421
|
+
async _finishAddPool(config, generation) {
|
|
8422
|
+
const client = await this.clientFactory(config);
|
|
8423
|
+
if (this._closed || generation !== this._generation || this.pools.has(config.name)) {
|
|
8424
|
+
await client.close().catch(() => {
|
|
8425
|
+
});
|
|
8426
|
+
throw createError(
|
|
8427
|
+
this._closed || generation !== this._generation ? ErrorCodes.INVALID_OPERATION : ErrorCodes.INVALID_CONFIG,
|
|
8428
|
+
this._closed || generation !== this._generation ? "Connection pool manager closed while adding a pool" : `Pool '${config.name}' already exists`
|
|
8429
|
+
);
|
|
8248
8430
|
}
|
|
8431
|
+
this.pools.set(config.name, { client, config, createdAt: Date.now() });
|
|
8432
|
+
this.healthStatus.set(config.name, {
|
|
8433
|
+
status: "up",
|
|
8434
|
+
consecutiveFailures: 0,
|
|
8435
|
+
lastCheckTime: null,
|
|
8436
|
+
lastError: null,
|
|
8437
|
+
uptime: 0
|
|
8438
|
+
});
|
|
8439
|
+
this.stats.set(config.name, createEmptyPoolStats(config.name));
|
|
8440
|
+
this._configs.set(config.name, config);
|
|
8441
|
+
this._healthChecker.register(config.name, config.healthCheck ?? {});
|
|
8249
8442
|
}
|
|
8250
8443
|
/**
|
|
8251
8444
|
* Remove a connection pool.
|
|
@@ -8402,16 +8595,24 @@ var ConnectionPoolManager = class {
|
|
|
8402
8595
|
* @since v1.0.8
|
|
8403
8596
|
*/
|
|
8404
8597
|
async close() {
|
|
8598
|
+
if (this._closePromise) return this._closePromise;
|
|
8599
|
+
this._closed = true;
|
|
8600
|
+
this._generation += 1;
|
|
8601
|
+
this._closePromise = this._closeAllPools();
|
|
8602
|
+
return this._closePromise;
|
|
8603
|
+
}
|
|
8604
|
+
async _closeAllPools() {
|
|
8405
8605
|
this.stopHealthCheck();
|
|
8406
8606
|
this._healthChecker.stop();
|
|
8407
8607
|
this._stats.close();
|
|
8608
|
+
await Promise.allSettled([...this._pendingAddTasks]);
|
|
8408
8609
|
for (const pool of this.pools.values()) {
|
|
8409
8610
|
await pool.client.close();
|
|
8410
8611
|
}
|
|
8411
8612
|
this.pools.clear();
|
|
8412
8613
|
this.healthStatus.clear();
|
|
8413
8614
|
this.stats.clear();
|
|
8414
|
-
this.
|
|
8615
|
+
this._configs.clear();
|
|
8415
8616
|
}
|
|
8416
8617
|
// ─── v1 compat methods ───────────────────────────────────────────────────────────
|
|
8417
8618
|
/** v1 compat: get pool health Map */
|
|
@@ -8599,6 +8800,7 @@ async function initializeDistributedCacheInvalidator(options, cache, logger) {
|
|
|
8599
8800
|
logger
|
|
8600
8801
|
});
|
|
8601
8802
|
} catch (err) {
|
|
8803
|
+
if (err?.code === ErrorCodes.INVALID_CONFIG) throw err;
|
|
8602
8804
|
logger.warn?.("[Cache] Failed to initialize distributed cache invalidator \u2014 check Redis config or package installation completeness.", err);
|
|
8603
8805
|
return null;
|
|
8604
8806
|
}
|
|
@@ -8677,6 +8879,33 @@ async function loadModelFiles(options, logger, opts = {}) {
|
|
|
8677
8879
|
}
|
|
8678
8880
|
}
|
|
8679
8881
|
|
|
8882
|
+
// src/entry/runtime-health.ts
|
|
8883
|
+
function summarizeRuntimeHealth(cacheOption, cache, poolManager, invalidator) {
|
|
8884
|
+
const cacheStats = cache.getStats?.();
|
|
8885
|
+
const poolHealth = poolManager?.getHealthStatus();
|
|
8886
|
+
const distributedStats = invalidator?.getStats() ?? null;
|
|
8887
|
+
const cacheInput = cacheOption;
|
|
8888
|
+
const cacheEnabled = cacheInput?.enabled !== false;
|
|
8889
|
+
const distributedEnabled = cacheInput?.distributed !== void 0 && cacheInput.distributed.enabled !== false;
|
|
8890
|
+
const cacheProbe = cacheEnabled ? cache.getHealthStatus?.() : void 0;
|
|
8891
|
+
const cacheProbeStatus = cacheProbe === true || cacheProbe === "up" ? "up" : cacheProbe === false || cacheProbe === "down" ? "down" : "unknown";
|
|
8892
|
+
const redisState = invalidator;
|
|
8893
|
+
const redisStatuses = [redisState?.pub?.status, redisState?.sub?.status];
|
|
8894
|
+
const distributedStatus = !distributedEnabled || !invalidator ? "unknown" : redisStatuses.every((status) => status === "ready") ? "up" : redisStatuses.some((status) => status === "end" || status === "close") ? "down" : "unknown";
|
|
8895
|
+
const poolStates = Object.values(poolHealth ?? {}).map((health) => health.status);
|
|
8896
|
+
const poolsStatus = poolStates.length === 0 ? "unknown" : poolStates.some((status) => status === "down") ? "down" : poolStates.every((status) => status === "up") ? "up" : "unknown";
|
|
8897
|
+
return {
|
|
8898
|
+
cacheStats,
|
|
8899
|
+
poolHealth,
|
|
8900
|
+
distributedStats,
|
|
8901
|
+
cacheEnabled,
|
|
8902
|
+
distributedEnabled,
|
|
8903
|
+
cacheProbeStatus,
|
|
8904
|
+
distributedStatus,
|
|
8905
|
+
poolsStatus
|
|
8906
|
+
};
|
|
8907
|
+
}
|
|
8908
|
+
|
|
8680
8909
|
// src/entry/runtime-defaults.ts
|
|
8681
8910
|
function isPlainObject3(value) {
|
|
8682
8911
|
if (value === null || typeof value !== "object") return false;
|
|
@@ -9327,36 +9556,36 @@ var FUNC_REGEX = /^(CONCAT|UPPER|LOWER|TRIM|SUBSTR|LENGTH|ABS|CEIL|FLOOR|ROUND|S
|
|
|
9327
9556
|
var IS_FUNC_CALL_RE = /^(CONCAT|UPPER|LOWER|TRIM|SUBSTR|LENGTH|ABS|CEIL|FLOOR|ROUND|SQRT|POW|SIZE|IN|SLICE|FIRST|LAST|ARRAY_ELEM_AT|FILTER|MAP|INDEX_OF|CONCAT_ARRAYS|TYPE|NOT|EXISTS|IS_NUMBER|IS_ARRAY|SUM|AVG|MAX|MIN|COUNT|PUSH|ADD_TO_SET|YEAR|MONTH|DAY_OF_MONTH|HOUR|MINUTE|SECOND|SPLIT|REPLACE|INDEX_OF_STR|LTRIM|RTRIM|SUBSTR_CP|REGEX|MERGE_OBJECTS|TO_INT|TO_STRING|OBJECT_TO_ARRAY|ARRAY_TO_OBJECT|SET_UNION|SWITCH)\s*\(/i;
|
|
9328
9557
|
function compileInnerExpression(expression) {
|
|
9329
9558
|
const expr2 = expression.trim();
|
|
9330
|
-
|
|
9331
|
-
|
|
9332
|
-
|
|
9333
|
-
|
|
9334
|
-
|
|
9335
|
-
|
|
9336
|
-
|
|
9559
|
+
if (!expr2) throw createError(ErrorCodes.INVALID_EXPRESSION, "Expression cannot be empty");
|
|
9560
|
+
const positions = topLevelPositions(expr2);
|
|
9561
|
+
const unwrapped = stripOuterParentheses(expr2);
|
|
9562
|
+
if (unwrapped !== expr2) return compileInnerExpression(unwrapped);
|
|
9563
|
+
if (/^[+-]?(?:\d+(?:\.\d*)?|\.\d+)(?:e[+-]?\d+)?$/i.test(expr2)) return Number(expr2);
|
|
9564
|
+
const conditional = findConditional(expr2, positions);
|
|
9565
|
+
if (conditional) {
|
|
9566
|
+
return { $cond: {
|
|
9567
|
+
if: compileInnerExpression(expr2.slice(0, conditional.question)),
|
|
9568
|
+
then: compileInnerExpression(expr2.slice(conditional.question + 1, conditional.colon)),
|
|
9569
|
+
else: compileInnerExpression(expr2.slice(conditional.colon + 1))
|
|
9570
|
+
} };
|
|
9571
|
+
}
|
|
9572
|
+
const nullCoalParts = splitTopLevel2(expr2, "??", positions);
|
|
9573
|
+
if (nullCoalParts.length > 1) {
|
|
9574
|
+
return nullCoalParts.slice(1).reduce(
|
|
9575
|
+
(left, right) => ({ $ifNull: [left, compileInnerExpression(right)] }),
|
|
9576
|
+
compileInnerExpression(nullCoalParts[0])
|
|
9577
|
+
);
|
|
9337
9578
|
}
|
|
9338
|
-
const orParts = splitTopLevel2(expr2, "||");
|
|
9579
|
+
const orParts = splitTopLevel2(expr2, "||", positions);
|
|
9339
9580
|
if (orParts.length > 1) {
|
|
9340
|
-
return { $or: orParts.map((part) => compileInnerExpression(part
|
|
9341
|
-
}
|
|
9342
|
-
const ternary = /^([^?]+)\s*\?\s*([^:]+)\s*:\s*(.+)$/.exec(expr2);
|
|
9343
|
-
if (ternary) {
|
|
9344
|
-
const [, condition, thenPart, elsePart] = ternary;
|
|
9345
|
-
return {
|
|
9346
|
-
$cond: {
|
|
9347
|
-
if: compileInnerExpression(condition.trim()),
|
|
9348
|
-
then: parseThenElse(thenPart.trim()),
|
|
9349
|
-
else: parseThenElse(elsePart.trim())
|
|
9350
|
-
}
|
|
9351
|
-
};
|
|
9581
|
+
return { $or: orParts.map((part) => compileInnerExpression(part)) };
|
|
9352
9582
|
}
|
|
9353
|
-
const
|
|
9354
|
-
if (
|
|
9355
|
-
return { $
|
|
9583
|
+
const andParts = splitTopLevel2(expr2, "&&", positions);
|
|
9584
|
+
if (andParts.length > 1) {
|
|
9585
|
+
return { $and: andParts.map((part) => compileInnerExpression(part)) };
|
|
9356
9586
|
}
|
|
9357
|
-
const
|
|
9358
|
-
if (
|
|
9359
|
-
const [, left, operator, right] = cmpMatch;
|
|
9587
|
+
const comparison = findBinaryOperator(expr2, positions, ["===", "!==", ">=", "<=", ">", "<"]);
|
|
9588
|
+
if (comparison) {
|
|
9360
9589
|
const operatorMap = {
|
|
9361
9590
|
"===": "$eq",
|
|
9362
9591
|
"!==": "$ne",
|
|
@@ -9365,24 +9594,39 @@ function compileInnerExpression(expression) {
|
|
|
9365
9594
|
">": "$gt",
|
|
9366
9595
|
"<": "$lt"
|
|
9367
9596
|
};
|
|
9368
|
-
return {
|
|
9369
|
-
|
|
9370
|
-
|
|
9371
|
-
|
|
9372
|
-
|
|
9373
|
-
|
|
9597
|
+
return { [operatorMap[comparison.operator]]: [
|
|
9598
|
+
compileInnerExpression(expr2.slice(0, comparison.index)),
|
|
9599
|
+
compileInnerExpression(expr2.slice(comparison.index + comparison.operator.length))
|
|
9600
|
+
] };
|
|
9601
|
+
}
|
|
9602
|
+
const additive = findBinaryOperator(expr2, positions, ["+", "-"], true);
|
|
9603
|
+
if (additive) {
|
|
9604
|
+
return { [additive.operator === "+" ? "$add" : "$subtract"]: [
|
|
9605
|
+
compileInnerExpression(expr2.slice(0, additive.index)),
|
|
9606
|
+
compileInnerExpression(expr2.slice(additive.index + 1))
|
|
9607
|
+
] };
|
|
9608
|
+
}
|
|
9609
|
+
const multiplicative = findBinaryOperator(expr2, positions, ["*", "/", "%"], true);
|
|
9610
|
+
if (multiplicative) {
|
|
9611
|
+
const operatorMap = { "*": "$multiply", "/": "$divide", "%": "$mod" };
|
|
9612
|
+
return { [operatorMap[multiplicative.operator]]: [
|
|
9613
|
+
compileInnerExpression(expr2.slice(0, multiplicative.index)),
|
|
9614
|
+
compileInnerExpression(expr2.slice(multiplicative.index + 1))
|
|
9615
|
+
] };
|
|
9374
9616
|
}
|
|
9375
|
-
|
|
9376
|
-
|
|
9377
|
-
const [, left, operator, right] = addSubMatch;
|
|
9378
|
-
const operatorMap = { "+": "$add", "-": "$subtract" };
|
|
9379
|
-
return { [operatorMap[operator]]: [parseOperand(left.trim()), parseOperand(right.trim())] };
|
|
9617
|
+
if (expr2.startsWith("-")) {
|
|
9618
|
+
return { $multiply: [-1, compileInnerExpression(expr2.slice(1))] };
|
|
9380
9619
|
}
|
|
9381
|
-
const
|
|
9382
|
-
if (
|
|
9383
|
-
|
|
9384
|
-
|
|
9385
|
-
|
|
9620
|
+
const funcMatch = expr2.match(FUNC_REGEX);
|
|
9621
|
+
if (funcMatch) {
|
|
9622
|
+
try {
|
|
9623
|
+
return dispatchFunction(funcMatch[1].toUpperCase(), funcMatch[2] ?? "");
|
|
9624
|
+
} catch (error) {
|
|
9625
|
+
if (error && typeof error === "object" && "code" in error && error.code === ErrorCodes.INVALID_EXPRESSION) {
|
|
9626
|
+
throw error;
|
|
9627
|
+
}
|
|
9628
|
+
throw createError(ErrorCodes.INVALID_EXPRESSION, `Invalid arguments for ${funcMatch[1].toUpperCase()}`);
|
|
9629
|
+
}
|
|
9386
9630
|
}
|
|
9387
9631
|
const genericFuncCallRe = /^[A-Za-z_][A-Za-z0-9_]*\s*\(.+\)$/;
|
|
9388
9632
|
if (genericFuncCallRe.test(expr2)) {
|
|
@@ -9391,9 +9635,6 @@ function compileInnerExpression(expression) {
|
|
|
9391
9635
|
}
|
|
9392
9636
|
return parseValue(expr2);
|
|
9393
9637
|
}
|
|
9394
|
-
function parseThenElse(source) {
|
|
9395
|
-
return source.includes("?") && source.includes(":") ? compileInnerExpression(source) : parseValue(source);
|
|
9396
|
-
}
|
|
9397
9638
|
function parseValue(value) {
|
|
9398
9639
|
const normalized = stripOuterParentheses(value.trim());
|
|
9399
9640
|
if (normalized.startsWith("'") && normalized.endsWith("'") || normalized.startsWith('"') && normalized.endsWith('"')) {
|
|
@@ -9403,18 +9644,22 @@ function parseValue(value) {
|
|
|
9403
9644
|
if (normalized === "true") return true;
|
|
9404
9645
|
if (normalized === "false") return false;
|
|
9405
9646
|
if (!isNaN(Number(normalized)) && normalized !== "") return Number(normalized);
|
|
9406
|
-
if (IS_FUNC_CALL_RE.test(normalized)) {
|
|
9647
|
+
if (normalized !== value.trim() || IS_FUNC_CALL_RE.test(normalized) || FUNC_REGEX.test(normalized)) {
|
|
9407
9648
|
return compileInnerExpression(normalized);
|
|
9408
9649
|
}
|
|
9409
|
-
if (normalized.startsWith("
|
|
9410
|
-
|
|
9411
|
-
|
|
9412
|
-
|
|
9413
|
-
|
|
9414
|
-
|
|
9650
|
+
if (normalized.startsWith("[") || normalized.startsWith("{")) {
|
|
9651
|
+
try {
|
|
9652
|
+
return JSON.parse(normalized);
|
|
9653
|
+
} catch {
|
|
9654
|
+
throw createError(ErrorCodes.INVALID_EXPRESSION, "Invalid array or object literal");
|
|
9655
|
+
}
|
|
9656
|
+
}
|
|
9657
|
+
const positions = topLevelPositions(normalized);
|
|
9658
|
+
if (positions.some((index) => /[?+\-*/%><&|]/.test(normalized[index]))) {
|
|
9415
9659
|
return compileInnerExpression(normalized);
|
|
9416
9660
|
}
|
|
9417
|
-
|
|
9661
|
+
if (normalized.startsWith("$")) return normalized;
|
|
9662
|
+
return `$${normalized}`;
|
|
9418
9663
|
}
|
|
9419
9664
|
function dispatchFunction(name, argsStr) {
|
|
9420
9665
|
const args = splitArgsStr(argsStr);
|
|
@@ -9716,72 +9961,82 @@ function compileMapExpression(exprStr, varName) {
|
|
|
9716
9961
|
return compileInnerExpression(replaced);
|
|
9717
9962
|
}
|
|
9718
9963
|
function splitArgsStr(argsStr) {
|
|
9719
|
-
|
|
9720
|
-
|
|
9721
|
-
let inString = false;
|
|
9722
|
-
let stringChar = "";
|
|
9723
|
-
let parenDepth = 0;
|
|
9724
|
-
for (let index = 0; index < argsStr.length; index++) {
|
|
9725
|
-
const ch = argsStr[index];
|
|
9726
|
-
if ((ch === '"' || ch === "'") && (index === 0 || argsStr[index - 1] !== "\\")) {
|
|
9727
|
-
if (!inString) {
|
|
9728
|
-
inString = true;
|
|
9729
|
-
stringChar = ch;
|
|
9730
|
-
} else if (ch === stringChar) {
|
|
9731
|
-
inString = false;
|
|
9732
|
-
stringChar = "";
|
|
9733
|
-
}
|
|
9734
|
-
current += ch;
|
|
9735
|
-
} else if (ch === "(" && !inString) {
|
|
9736
|
-
parenDepth++;
|
|
9737
|
-
current += ch;
|
|
9738
|
-
} else if (ch === ")" && !inString) {
|
|
9739
|
-
parenDepth--;
|
|
9740
|
-
current += ch;
|
|
9741
|
-
} else if (ch === "," && !inString && parenDepth === 0) {
|
|
9742
|
-
args.push(current.trim());
|
|
9743
|
-
current = "";
|
|
9744
|
-
} else {
|
|
9745
|
-
current += ch;
|
|
9746
|
-
}
|
|
9747
|
-
}
|
|
9748
|
-
if (current.trim()) args.push(current.trim());
|
|
9749
|
-
return args;
|
|
9964
|
+
if (!argsStr.trim()) return [];
|
|
9965
|
+
return splitTopLevel2(argsStr, ",");
|
|
9750
9966
|
}
|
|
9751
|
-
function
|
|
9752
|
-
const
|
|
9753
|
-
|
|
9754
|
-
let
|
|
9755
|
-
let inString = false;
|
|
9756
|
-
let stringChar = "";
|
|
9967
|
+
function topLevelPositions(source) {
|
|
9968
|
+
const positions = [];
|
|
9969
|
+
const stack = [];
|
|
9970
|
+
let quote = null;
|
|
9757
9971
|
for (let index = 0; index < source.length; index++) {
|
|
9758
9972
|
const ch = source[index];
|
|
9759
|
-
if (
|
|
9760
|
-
if (
|
|
9761
|
-
|
|
9762
|
-
|
|
9763
|
-
|
|
9764
|
-
|
|
9765
|
-
|
|
9973
|
+
if (quote) {
|
|
9974
|
+
if (ch === quote && source[index - 1] !== "\\") quote = null;
|
|
9975
|
+
continue;
|
|
9976
|
+
}
|
|
9977
|
+
if (ch === '"' || ch === "'") {
|
|
9978
|
+
quote = ch;
|
|
9979
|
+
continue;
|
|
9980
|
+
}
|
|
9981
|
+
if (ch === "(" || ch === "[" || ch === "{") {
|
|
9982
|
+
stack.push(ch);
|
|
9983
|
+
continue;
|
|
9984
|
+
}
|
|
9985
|
+
if (ch === ")" || ch === "]" || ch === "}") {
|
|
9986
|
+
const opening = stack.pop();
|
|
9987
|
+
if (ch === ")" && opening !== "(" || ch === "]" && opening !== "[" || ch === "}" && opening !== "{") {
|
|
9988
|
+
throw createError(ErrorCodes.INVALID_EXPRESSION, "Unbalanced expression delimiters");
|
|
9766
9989
|
}
|
|
9767
|
-
|
|
9768
|
-
} else if (!inString && ch === "(") {
|
|
9769
|
-
parenDepth++;
|
|
9770
|
-
current += ch;
|
|
9771
|
-
} else if (!inString && ch === ")") {
|
|
9772
|
-
parenDepth--;
|
|
9773
|
-
current += ch;
|
|
9774
|
-
} else if (!inString && parenDepth === 0 && source.startsWith(separator, index)) {
|
|
9775
|
-
parts.push(current);
|
|
9776
|
-
current = "";
|
|
9777
|
-
index += separator.length - 1;
|
|
9778
|
-
} else {
|
|
9779
|
-
current += ch;
|
|
9990
|
+
continue;
|
|
9780
9991
|
}
|
|
9992
|
+
if (stack.length === 0) positions.push(index);
|
|
9993
|
+
}
|
|
9994
|
+
if (quote || stack.length > 0) {
|
|
9995
|
+
throw createError(ErrorCodes.INVALID_EXPRESSION, "Unbalanced expression delimiters");
|
|
9996
|
+
}
|
|
9997
|
+
return positions;
|
|
9998
|
+
}
|
|
9999
|
+
function splitTopLevel2(source, separator, positions = topLevelPositions(source)) {
|
|
10000
|
+
const parts = [];
|
|
10001
|
+
let start = 0;
|
|
10002
|
+
for (const index of positions) {
|
|
10003
|
+
if (index < start || !source.startsWith(separator, index)) continue;
|
|
10004
|
+
parts.push(source.slice(start, index).trim());
|
|
10005
|
+
start = index + separator.length;
|
|
10006
|
+
}
|
|
10007
|
+
if (parts.length === 0) return [source];
|
|
10008
|
+
parts.push(source.slice(start).trim());
|
|
10009
|
+
if (parts.some((part) => !part)) {
|
|
10010
|
+
throw createError(ErrorCodes.INVALID_EXPRESSION, `Missing operand around ${separator}`);
|
|
9781
10011
|
}
|
|
9782
|
-
parts.push(current);
|
|
9783
10012
|
return parts;
|
|
9784
10013
|
}
|
|
10014
|
+
function findConditional(source, positions) {
|
|
10015
|
+
const question = positions.find((index) => source[index] === "?" && source[index - 1] !== "?" && source[index + 1] !== "?");
|
|
10016
|
+
if (question === void 0) return null;
|
|
10017
|
+
let nested = 0;
|
|
10018
|
+
for (const index of positions) {
|
|
10019
|
+
if (index <= question) continue;
|
|
10020
|
+
if (source[index] === "?" && source[index - 1] !== "?" && source[index + 1] !== "?") nested++;
|
|
10021
|
+
if (source[index] === ":") {
|
|
10022
|
+
if (nested === 0) return { question, colon: index };
|
|
10023
|
+
nested--;
|
|
10024
|
+
}
|
|
10025
|
+
}
|
|
10026
|
+
throw createError(ErrorCodes.INVALID_EXPRESSION, "Conditional expression is missing a branch");
|
|
10027
|
+
}
|
|
10028
|
+
function findBinaryOperator(source, positions, operators, rightmost = false) {
|
|
10029
|
+
let match = null;
|
|
10030
|
+
for (const index of positions) {
|
|
10031
|
+
const operator = operators.find((candidate) => source.startsWith(candidate, index));
|
|
10032
|
+
if (!operator) continue;
|
|
10033
|
+
if ((operator === "+" || operator === "-") && !source.slice(0, index).trim()) continue;
|
|
10034
|
+
if ((operator === "+" || operator === "-") && /[+\-*/%<>=!?&|:,]$/.test(source.slice(0, index).trimEnd())) continue;
|
|
10035
|
+
match = { index, operator };
|
|
10036
|
+
if (!rightmost) break;
|
|
10037
|
+
}
|
|
10038
|
+
return match;
|
|
10039
|
+
}
|
|
9785
10040
|
function stripOuterParentheses(source) {
|
|
9786
10041
|
let normalized = source.trim();
|
|
9787
10042
|
while (normalized.startsWith("(") && normalized.endsWith(")") && isWrappedByOuterParentheses(normalized)) {
|
|
@@ -9827,10 +10082,15 @@ function isExpressionObject(value) {
|
|
|
9827
10082
|
value && typeof value === "object" && "__expr__" in value && typeof value.__expr__ === "string" && "__compiled__" in value && typeof value.__compiled__ === "boolean"
|
|
9828
10083
|
);
|
|
9829
10084
|
}
|
|
10085
|
+
function isPlainExpressionRecord(value) {
|
|
10086
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return false;
|
|
10087
|
+
const prototype = Object.getPrototypeOf(value);
|
|
10088
|
+
return prototype === Object.prototype || prototype === null;
|
|
10089
|
+
}
|
|
9830
10090
|
function hasExpressionInObject(value) {
|
|
9831
10091
|
if (isExpressionObject(value)) return true;
|
|
9832
|
-
if (!value || typeof value !== "object") return false;
|
|
9833
10092
|
if (Array.isArray(value)) return value.some((item) => hasExpressionInObject(item));
|
|
10093
|
+
if (!isPlainExpressionRecord(value)) return false;
|
|
9834
10094
|
return Object.values(value).some((item) => hasExpressionInObject(item));
|
|
9835
10095
|
}
|
|
9836
10096
|
function hasExpressionInPipeline(pipeline) {
|
|
@@ -9847,7 +10107,7 @@ function transformExpressions(value, context) {
|
|
|
9847
10107
|
if (Array.isArray(value)) {
|
|
9848
10108
|
return value.map((item) => transformStageEntry(item));
|
|
9849
10109
|
}
|
|
9850
|
-
if (!value
|
|
10110
|
+
if (!isPlainExpressionRecord(value)) return value;
|
|
9851
10111
|
const entries = Object.entries(value);
|
|
9852
10112
|
const result = {};
|
|
9853
10113
|
for (const [key, current] of entries) {
|
|
@@ -9856,7 +10116,7 @@ function transformExpressions(value, context) {
|
|
|
9856
10116
|
return result;
|
|
9857
10117
|
}
|
|
9858
10118
|
function transformStageEntry(stage) {
|
|
9859
|
-
if (!
|
|
10119
|
+
if (!isPlainExpressionRecord(stage)) {
|
|
9860
10120
|
return transformExpressions(stage, "project");
|
|
9861
10121
|
}
|
|
9862
10122
|
const entries = Object.entries(stage);
|
|
@@ -9880,7 +10140,7 @@ function transformStageValue(value, context) {
|
|
|
9880
10140
|
if (Array.isArray(value)) {
|
|
9881
10141
|
return value.map((item) => transformStageValue(item, context));
|
|
9882
10142
|
}
|
|
9883
|
-
if (!value
|
|
10143
|
+
if (!isPlainExpressionRecord(value)) return value;
|
|
9884
10144
|
const entries = Object.entries(value);
|
|
9885
10145
|
const result = {};
|
|
9886
10146
|
for (const [key, current] of entries) {
|
|
@@ -10811,6 +11071,7 @@ var AggregateChain = class {
|
|
|
10811
11071
|
return this.collection.aggregate(this.pipeline, buildAggregateDriverOptions(this.buildExecuteOptions())).explain(verbosity === true ? "queryPlanner" : verbosity);
|
|
10812
11072
|
}
|
|
10813
11073
|
stream() {
|
|
11074
|
+
if (this.writePipelineHooks) assertManagedTransactionWriteAllowed(this.buildExecuteOptions());
|
|
10814
11075
|
const stream = this.collection.aggregate(this.pipeline, buildAggregateDriverOptions(this.buildExecuteOptions())).stream();
|
|
10815
11076
|
const onWriteComplete = this.writePipelineHooks?.onWriteComplete;
|
|
10816
11077
|
if (onWriteComplete) {
|
|
@@ -10828,6 +11089,7 @@ var AggregateChain = class {
|
|
|
10828
11089
|
throw createError(ErrorCodes.INVALID_OPERATION, "Query already executed.");
|
|
10829
11090
|
}
|
|
10830
11091
|
this.executed = true;
|
|
11092
|
+
if (this.writePipelineHooks) assertManagedTransactionWriteAllowed(this.buildExecuteOptions());
|
|
10831
11093
|
return this.collection.aggregate(this.pipeline, buildAggregateDriverOptions(this.buildExecuteOptions())).toArray().then(async (result) => {
|
|
10832
11094
|
await this.writePipelineHooks?.onWriteComplete?.();
|
|
10833
11095
|
return result;
|
|
@@ -11144,48 +11406,59 @@ function stripWriteCacheControlOptions(options) {
|
|
|
11144
11406
|
|
|
11145
11407
|
// src/adapters/mongodb/writes/write-basic.ts
|
|
11146
11408
|
async function insertOneDocument(collection, ...args) {
|
|
11409
|
+
assertManagedTransactionWriteAllowed(args[1]);
|
|
11147
11410
|
return collection.insertOne(...args);
|
|
11148
11411
|
}
|
|
11149
11412
|
async function insertManyDocuments(collection, ...args) {
|
|
11413
|
+
assertManagedTransactionWriteAllowed(args[1]);
|
|
11150
11414
|
return collection.insertMany(...args);
|
|
11151
11415
|
}
|
|
11152
11416
|
async function updateOneDocument(collection, ...args) {
|
|
11417
|
+
assertManagedTransactionWriteAllowed(args[2]);
|
|
11153
11418
|
return collection.updateOne(...args);
|
|
11154
11419
|
}
|
|
11155
11420
|
async function updateManyDocuments(collection, ...args) {
|
|
11421
|
+
assertManagedTransactionWriteAllowed(args[2]);
|
|
11156
11422
|
return collection.updateMany(...args);
|
|
11157
11423
|
}
|
|
11158
11424
|
async function replaceOneDocument(collection, ...args) {
|
|
11425
|
+
assertManagedTransactionWriteAllowed(args[2]);
|
|
11159
11426
|
return collection.replaceOne(...args);
|
|
11160
11427
|
}
|
|
11161
11428
|
async function findOneAndUpdateDocument(collection, filter, update, options) {
|
|
11429
|
+
assertManagedTransactionWriteAllowed(options);
|
|
11162
11430
|
if (options !== void 0) {
|
|
11163
11431
|
return collection.findOneAndUpdate(filter, update, options);
|
|
11164
11432
|
}
|
|
11165
11433
|
return collection.findOneAndUpdate(filter, update);
|
|
11166
11434
|
}
|
|
11167
11435
|
async function findOneAndReplaceDocument(collection, filter, replacement, options) {
|
|
11436
|
+
assertManagedTransactionWriteAllowed(options);
|
|
11168
11437
|
if (options !== void 0) {
|
|
11169
11438
|
return collection.findOneAndReplace(filter, replacement, options);
|
|
11170
11439
|
}
|
|
11171
11440
|
return collection.findOneAndReplace(filter, replacement);
|
|
11172
11441
|
}
|
|
11173
11442
|
async function findOneAndDeleteDocument(collection, filter, options) {
|
|
11443
|
+
assertManagedTransactionWriteAllowed(options);
|
|
11174
11444
|
if (options !== void 0) {
|
|
11175
11445
|
return collection.findOneAndDelete(filter, options);
|
|
11176
11446
|
}
|
|
11177
11447
|
return collection.findOneAndDelete(filter);
|
|
11178
11448
|
}
|
|
11179
11449
|
async function upsertOneDocument(collection, filter, update, options = {}) {
|
|
11450
|
+
assertManagedTransactionWriteAllowed(options);
|
|
11180
11451
|
return collection.updateOne(filter, update, {
|
|
11181
11452
|
...options,
|
|
11182
11453
|
upsert: true
|
|
11183
11454
|
});
|
|
11184
11455
|
}
|
|
11185
11456
|
async function deleteOneDocument(collection, ...args) {
|
|
11457
|
+
assertManagedTransactionWriteAllowed(args[1]);
|
|
11186
11458
|
return collection.deleteOne(...args);
|
|
11187
11459
|
}
|
|
11188
11460
|
async function deleteManyDocuments(collection, ...args) {
|
|
11461
|
+
assertManagedTransactionWriteAllowed(args[1]);
|
|
11189
11462
|
return collection.deleteMany(...args);
|
|
11190
11463
|
}
|
|
11191
11464
|
async function incrementOneDocument(collection, filter, field, incrementOrOptions, maybeOptions) {
|
|
@@ -11212,6 +11485,7 @@ async function incrementOneDocument(collection, filter, field, incrementOrOption
|
|
|
11212
11485
|
includeResultMetadata: true
|
|
11213
11486
|
};
|
|
11214
11487
|
if (normalizedProjection) findOptions.projection = normalizedProjection;
|
|
11488
|
+
assertManagedTransactionWriteAllowed(findOptions);
|
|
11215
11489
|
const rawResult = await collection.findOneAndUpdate(
|
|
11216
11490
|
filter,
|
|
11217
11491
|
updateDocument,
|
|
@@ -11344,14 +11618,25 @@ async function runWithConcurrency(items, concurrency, worker) {
|
|
|
11344
11618
|
return;
|
|
11345
11619
|
}
|
|
11346
11620
|
let nextIndex = 0;
|
|
11621
|
+
let stopAcquiring = false;
|
|
11622
|
+
let firstFatal;
|
|
11347
11623
|
const workerCount = Math.min(concurrency, items.length);
|
|
11348
11624
|
await Promise.all(Array.from({ length: workerCount }, async () => {
|
|
11349
|
-
while (nextIndex < items.length) {
|
|
11625
|
+
while (!stopAcquiring && nextIndex < items.length) {
|
|
11350
11626
|
const item = items[nextIndex];
|
|
11351
11627
|
nextIndex += 1;
|
|
11352
|
-
|
|
11628
|
+
try {
|
|
11629
|
+
await worker(item);
|
|
11630
|
+
} catch (error) {
|
|
11631
|
+
if (!stopAcquiring) {
|
|
11632
|
+
firstFatal = error;
|
|
11633
|
+
stopAcquiring = true;
|
|
11634
|
+
}
|
|
11635
|
+
break;
|
|
11636
|
+
}
|
|
11353
11637
|
}
|
|
11354
11638
|
}));
|
|
11639
|
+
if (stopAcquiring) throw firstFatal;
|
|
11355
11640
|
}
|
|
11356
11641
|
async function insertBatchDocuments(collection, documents, options = {}) {
|
|
11357
11642
|
if (!Array.isArray(documents)) {
|
|
@@ -11398,12 +11683,14 @@ async function insertBatchDocuments(collection, documents, options = {}) {
|
|
|
11398
11683
|
insertedIds: {},
|
|
11399
11684
|
retries: []
|
|
11400
11685
|
};
|
|
11686
|
+
assertManagedTransactionWriteAllowed(driverOptions);
|
|
11401
11687
|
await runWithConcurrency(batchPlans, effectiveConcurrency, async ({ batchIndex, batch, batchOffset: batchOffset2 }) => {
|
|
11402
11688
|
let pendingBatch = batch;
|
|
11403
11689
|
let pendingIndexes = batch.map((_, index) => index);
|
|
11404
11690
|
let attempts = 0;
|
|
11405
11691
|
while (true) {
|
|
11406
11692
|
try {
|
|
11693
|
+
assertManagedTransactionWriteAllowed(driverOptions);
|
|
11407
11694
|
const batchResult = await collection.insertMany(pendingBatch, {
|
|
11408
11695
|
...driverOptions,
|
|
11409
11696
|
ordered
|
|
@@ -11423,6 +11710,7 @@ async function insertBatchDocuments(collection, documents, options = {}) {
|
|
|
11423
11710
|
});
|
|
11424
11711
|
break;
|
|
11425
11712
|
} catch (cause) {
|
|
11713
|
+
assertManagedTransactionWriteAllowed(driverOptions);
|
|
11426
11714
|
const errorRecord = {
|
|
11427
11715
|
batchIndex,
|
|
11428
11716
|
message: cause instanceof Error ? cause.message : String(cause),
|
|
@@ -11494,6 +11782,10 @@ async function insertBatchDocuments(collection, documents, options = {}) {
|
|
|
11494
11782
|
});
|
|
11495
11783
|
return result;
|
|
11496
11784
|
}
|
|
11785
|
+
function batchWriteFilter(filter, batch) {
|
|
11786
|
+
const ids = { _id: { $in: batch } };
|
|
11787
|
+
return Object.prototype.hasOwnProperty.call(filter, "_id") ? { $and: [filter, ids] } : { ...filter, ...ids };
|
|
11788
|
+
}
|
|
11497
11789
|
async function updateBatchDocuments(collection, filter, update, options = {}) {
|
|
11498
11790
|
if (!filter || typeof filter !== "object" || Array.isArray(filter)) {
|
|
11499
11791
|
throw createError(ErrorCodes.INVALID_ARGUMENT, "filter must be a non-array object");
|
|
@@ -11537,6 +11829,7 @@ async function updateBatchDocuments(collection, filter, update, options = {}) {
|
|
|
11537
11829
|
"updateBatch retry requires an idempotent update; non-idempotent operators such as $inc/$push cannot be safely replayed"
|
|
11538
11830
|
);
|
|
11539
11831
|
}
|
|
11832
|
+
assertManagedTransactionWriteAllowed(driverOptions);
|
|
11540
11833
|
const estimatedTotal = await estimateBatchTotal(collection, filter, driverOptions, estimateProgress);
|
|
11541
11834
|
const totalBatches = estimatedTotal === null ? null : Math.ceil(estimatedTotal / batchSize);
|
|
11542
11835
|
const result = {
|
|
@@ -11553,8 +11846,9 @@ async function updateBatchDocuments(collection, filter, update, options = {}) {
|
|
|
11553
11846
|
let attempts = 0;
|
|
11554
11847
|
while (true) {
|
|
11555
11848
|
try {
|
|
11849
|
+
assertManagedTransactionWriteAllowed(driverOptions);
|
|
11556
11850
|
const batchResult = await collection.updateMany(
|
|
11557
|
-
|
|
11851
|
+
batchWriteFilter(filter, batch),
|
|
11558
11852
|
update,
|
|
11559
11853
|
driverOptions
|
|
11560
11854
|
);
|
|
@@ -11572,6 +11866,7 @@ async function updateBatchDocuments(collection, filter, update, options = {}) {
|
|
|
11572
11866
|
});
|
|
11573
11867
|
break;
|
|
11574
11868
|
} catch (cause) {
|
|
11869
|
+
assertManagedTransactionWriteAllowed(driverOptions);
|
|
11575
11870
|
const errorRecord = {
|
|
11576
11871
|
batchIndex,
|
|
11577
11872
|
message: cause instanceof Error ? cause.message : String(cause),
|
|
@@ -11634,6 +11929,7 @@ async function deleteBatchDocuments(collection, filter, options = {}) {
|
|
|
11634
11929
|
if (!Number.isInteger(retryAttempts) || retryAttempts < 0) {
|
|
11635
11930
|
throw createError(ErrorCodes.INVALID_ARGUMENT, "retryAttempts must be a non-negative integer");
|
|
11636
11931
|
}
|
|
11932
|
+
assertManagedTransactionWriteAllowed(driverOptions);
|
|
11637
11933
|
const estimatedTotal = await estimateBatchTotal(collection, filter, driverOptions, estimateProgress);
|
|
11638
11934
|
const totalBatches = estimatedTotal === null ? null : Math.ceil(estimatedTotal / batchSize);
|
|
11639
11935
|
const result = {
|
|
@@ -11648,8 +11944,9 @@ async function deleteBatchDocuments(collection, filter, options = {}) {
|
|
|
11648
11944
|
let attempts = 0;
|
|
11649
11945
|
while (true) {
|
|
11650
11946
|
try {
|
|
11947
|
+
assertManagedTransactionWriteAllowed(driverOptions);
|
|
11651
11948
|
const batchResult = await collection.deleteMany(
|
|
11652
|
-
|
|
11949
|
+
batchWriteFilter(filter, batch),
|
|
11653
11950
|
driverOptions
|
|
11654
11951
|
);
|
|
11655
11952
|
result.deletedCount += batchResult.deletedCount;
|
|
@@ -11664,6 +11961,7 @@ async function deleteBatchDocuments(collection, filter, options = {}) {
|
|
|
11664
11961
|
});
|
|
11665
11962
|
break;
|
|
11666
11963
|
} catch (cause) {
|
|
11964
|
+
assertManagedTransactionWriteAllowed(driverOptions);
|
|
11667
11965
|
const errorRecord = {
|
|
11668
11966
|
batchIndex,
|
|
11669
11967
|
message: cause instanceof Error ? cause.message : String(cause),
|
|
@@ -12192,9 +12490,11 @@ var MongoCollectionAccessor = class {
|
|
|
12192
12490
|
const normalizedQuery = this._cvFilter(query);
|
|
12193
12491
|
const maxTimeMS = this.management.defaults?.maxTimeMS;
|
|
12194
12492
|
const merged = { ...maxTimeMS !== void 0 ? { maxTimeMS } : {}, ...options ?? {} };
|
|
12195
|
-
const cacheTTL = typeof merged.cache === "number" ? merged.cache : 0;
|
|
12493
|
+
const cacheTTL = merged.explain === void 0 || merged.explain === false ? typeof merged.cache === "number" ? merged.cache : 0 : 0;
|
|
12196
12494
|
const { cache: _cache, ...keyOptions } = merged;
|
|
12197
12495
|
void _cache;
|
|
12496
|
+
const callerSignal = keyOptions.signal;
|
|
12497
|
+
if (callerSignal?.aborted) throw createError(ErrorCodes.INVALID_OPERATION, "Count operation aborted");
|
|
12198
12498
|
const executeCount = (signal) => countDocuments(
|
|
12199
12499
|
this.collectionRef,
|
|
12200
12500
|
normalizedQuery ?? {},
|
|
@@ -12205,15 +12505,16 @@ var MongoCollectionAccessor = class {
|
|
|
12205
12505
|
const cacheKey = buildCountCacheKey(this.collectionRef, this.management.defaults, normalizedQuery, keyOptions);
|
|
12206
12506
|
const cached = await Promise.resolve(this.management.queryCache.get(cacheKey));
|
|
12207
12507
|
if (cached !== void 0) {
|
|
12508
|
+
if (callerSignal?.aborted) throw createError(ErrorCodes.INVALID_OPERATION, "Count operation aborted");
|
|
12208
12509
|
return Promise.resolve(wrapQueryResultWithMeta(this.collectionRef, this.management.defaults, "count", merged, startTs, cached));
|
|
12209
12510
|
}
|
|
12210
|
-
const runner2 = countQueue ? countQueue.execute(executeCount) : executeCount();
|
|
12511
|
+
const runner2 = countQueue ? countQueue.execute(executeCount, { signal: callerSignal }) : executeCount();
|
|
12211
12512
|
return runner2.then(async (result) => {
|
|
12212
12513
|
await Promise.resolve(this.management.queryCache?.set(cacheKey, result, cacheTTL));
|
|
12213
12514
|
return wrapQueryResultWithMeta(this.collectionRef, this.management.defaults, "count", merged, startTs, result);
|
|
12214
12515
|
});
|
|
12215
12516
|
}
|
|
12216
|
-
const runner = countQueue ? countQueue.execute(executeCount) : executeCount();
|
|
12517
|
+
const runner = countQueue ? countQueue.execute(executeCount, { signal: callerSignal }) : executeCount();
|
|
12217
12518
|
return runner.then((result) => wrapQueryResultWithMeta(this.collectionRef, this.management.defaults, "count", merged, startTs, result));
|
|
12218
12519
|
}
|
|
12219
12520
|
/** Runs an aggregation pipeline and returns a chainable aggregate cursor. */
|
|
@@ -13502,20 +13803,21 @@ var DistributedCacheLockManager = class {
|
|
|
13502
13803
|
}
|
|
13503
13804
|
}
|
|
13504
13805
|
async withLock(key, callback, options = {}) {
|
|
13806
|
+
let lock;
|
|
13505
13807
|
try {
|
|
13506
|
-
|
|
13507
|
-
try {
|
|
13508
|
-
return await callback();
|
|
13509
|
-
} finally {
|
|
13510
|
-
await lock.release().catch(() => {
|
|
13511
|
-
});
|
|
13512
|
-
}
|
|
13808
|
+
lock = await this.acquireLock(key, options);
|
|
13513
13809
|
} catch (error) {
|
|
13514
13810
|
if (this._isRedisConnectionError(error) && options.fallbackToNoLock) {
|
|
13515
13811
|
return callback();
|
|
13516
13812
|
}
|
|
13517
13813
|
throw error;
|
|
13518
13814
|
}
|
|
13815
|
+
try {
|
|
13816
|
+
return await callback();
|
|
13817
|
+
} finally {
|
|
13818
|
+
await lock.release().catch(() => {
|
|
13819
|
+
});
|
|
13820
|
+
}
|
|
13519
13821
|
}
|
|
13520
13822
|
async acquireLock(key, options = {}) {
|
|
13521
13823
|
const ttl = options.ttl ?? 1e4;
|
|
@@ -14618,8 +14920,6 @@ var SlowQueryLogManager = class {
|
|
|
14618
14920
|
};
|
|
14619
14921
|
|
|
14620
14922
|
// src/capabilities/sync/index.ts
|
|
14621
|
-
import { copyFile, mkdir, open, readFile, rename, unlink } from "node:fs/promises";
|
|
14622
|
-
import path2 from "node:path";
|
|
14623
14923
|
import { MongoClient as MongoDriverClient3 } from "mongodb";
|
|
14624
14924
|
|
|
14625
14925
|
// src/capabilities/sync/idempotency.ts
|
|
@@ -14722,25 +15022,19 @@ function validateSyncIdempotencyConfig(config) {
|
|
|
14722
15022
|
}
|
|
14723
15023
|
}
|
|
14724
15024
|
|
|
14725
|
-
// src/capabilities/sync/
|
|
15025
|
+
// src/capabilities/sync/resume-token-store.ts
|
|
15026
|
+
import { copyFile, mkdir, open, readFile, rename, unlink } from "node:fs/promises";
|
|
15027
|
+
import path2 from "node:path";
|
|
14726
15028
|
function normalizeError(error) {
|
|
14727
15029
|
return error instanceof Error ? error : new Error(String(error));
|
|
14728
15030
|
}
|
|
14729
15031
|
function delay2(ms) {
|
|
14730
|
-
if (ms <= 0)
|
|
14731
|
-
return Promise.resolve();
|
|
14732
|
-
}
|
|
15032
|
+
if (ms <= 0) return Promise.resolve();
|
|
14733
15033
|
return new Promise((resolve) => {
|
|
14734
15034
|
const timer = setTimeout(resolve, ms);
|
|
14735
15035
|
timer.unref?.();
|
|
14736
15036
|
});
|
|
14737
15037
|
}
|
|
14738
|
-
function normalizeCollectionFilter(collections) {
|
|
14739
|
-
if (!collections?.length || collections.includes("*")) {
|
|
14740
|
-
return null;
|
|
14741
|
-
}
|
|
14742
|
-
return new Set(collections);
|
|
14743
|
-
}
|
|
14744
15038
|
async function syncDirectory(directory) {
|
|
14745
15039
|
try {
|
|
14746
15040
|
const handle = await open(directory, "r");
|
|
@@ -14752,35 +15046,6 @@ async function syncDirectory(directory) {
|
|
|
14752
15046
|
} catch {
|
|
14753
15047
|
}
|
|
14754
15048
|
}
|
|
14755
|
-
function validateTargetConfig(target, index) {
|
|
14756
|
-
if (!target || typeof target !== "object") {
|
|
14757
|
-
throw createError(ErrorCodes.INVALID_CONFIG, `[Sync] targets[${index}] must be an object.`);
|
|
14758
|
-
}
|
|
14759
|
-
if (typeof target.name !== "string" || target.name.trim() === "") {
|
|
14760
|
-
throw createError(ErrorCodes.INVALID_CONFIG, `[Sync] targets[${index}].name must be a non-empty string.`);
|
|
14761
|
-
}
|
|
14762
|
-
if (!target.apply && !target.uri && !target.pool) {
|
|
14763
|
-
throw createError(ErrorCodes.INVALID_CONFIG, `[Sync] targets[${index}] requires one of apply / uri / pool.`);
|
|
14764
|
-
}
|
|
14765
|
-
if (target.uri !== void 0 && (typeof target.uri !== "string" || target.uri.trim() === "")) {
|
|
14766
|
-
throw createError(ErrorCodes.INVALID_CONFIG, `[Sync] targets[${index}].uri must be a non-empty string when provided.`);
|
|
14767
|
-
}
|
|
14768
|
-
if (target.pool !== void 0 && (typeof target.pool !== "string" || target.pool.trim() === "")) {
|
|
14769
|
-
throw createError(ErrorCodes.INVALID_CONFIG, `[Sync] targets[${index}].pool must be a non-empty string when provided.`);
|
|
14770
|
-
}
|
|
14771
|
-
if (target.databaseName !== void 0 && (typeof target.databaseName !== "string" || target.databaseName.trim() === "")) {
|
|
14772
|
-
throw createError(ErrorCodes.INVALID_CONFIG, `[Sync] targets[${index}].databaseName must be a non-empty string when provided.`);
|
|
14773
|
-
}
|
|
14774
|
-
if (target.collections !== void 0 && (!Array.isArray(target.collections) || target.collections.length === 0)) {
|
|
14775
|
-
throw createError(ErrorCodes.INVALID_CONFIG, `[Sync] targets[${index}].collections must be a non-empty array when provided.`);
|
|
14776
|
-
}
|
|
14777
|
-
if (target.collections?.some((item) => typeof item !== "string" || item.trim() === "")) {
|
|
14778
|
-
throw createError(ErrorCodes.INVALID_CONFIG, `[Sync] targets[${index}].collections must contain non-empty strings.`);
|
|
14779
|
-
}
|
|
14780
|
-
if (target.apply !== void 0 && typeof target.apply !== "function") {
|
|
14781
|
-
throw createError(ErrorCodes.INVALID_CONFIG, `[Sync] targets[${index}].apply must be a function when provided.`);
|
|
14782
|
-
}
|
|
14783
|
-
}
|
|
14784
15049
|
function validateResumeTokenConfig(config) {
|
|
14785
15050
|
if (!config || typeof config !== "object") {
|
|
14786
15051
|
throw createError(ErrorCodes.INVALID_CONFIG, "[Sync] resumeToken must be an object.");
|
|
@@ -14811,39 +15076,6 @@ function validateResumeTokenConfig(config) {
|
|
|
14811
15076
|
throw createError(ErrorCodes.INVALID_CONFIG, "[Sync] resumeToken.saveRetryDelayMs must be a non-negative integer.");
|
|
14812
15077
|
}
|
|
14813
15078
|
}
|
|
14814
|
-
function validateSyncConfig(config) {
|
|
14815
|
-
if (!config || typeof config !== "object") {
|
|
14816
|
-
throw createError(ErrorCodes.INVALID_CONFIG, "[Sync] config must be an object.");
|
|
14817
|
-
}
|
|
14818
|
-
if (typeof config.enabled !== "boolean") {
|
|
14819
|
-
throw createError(ErrorCodes.INVALID_CONFIG, "[Sync] enabled must be a boolean.");
|
|
14820
|
-
}
|
|
14821
|
-
if (!config.enabled) {
|
|
14822
|
-
return;
|
|
14823
|
-
}
|
|
14824
|
-
if (!Array.isArray(config.targets) || config.targets.length === 0) {
|
|
14825
|
-
throw createError(ErrorCodes.INVALID_CONFIG, "[Sync] targets must be a non-empty array.");
|
|
14826
|
-
}
|
|
14827
|
-
if (config.collections !== void 0 && (!Array.isArray(config.collections) || config.collections.length === 0)) {
|
|
14828
|
-
throw createError(ErrorCodes.INVALID_CONFIG, "[Sync] collections must be a non-empty array when provided.");
|
|
14829
|
-
}
|
|
14830
|
-
if (config.collections?.some((item) => typeof item !== "string" || item.trim() === "")) {
|
|
14831
|
-
throw createError(ErrorCodes.INVALID_CONFIG, "[Sync] collections must contain non-empty strings.");
|
|
14832
|
-
}
|
|
14833
|
-
if (config.filter !== void 0 && typeof config.filter !== "function") {
|
|
14834
|
-
throw createError(ErrorCodes.INVALID_CONFIG, "[Sync] filter must be a function.");
|
|
14835
|
-
}
|
|
14836
|
-
if (config.transform !== void 0 && typeof config.transform !== "function") {
|
|
14837
|
-
throw createError(ErrorCodes.INVALID_CONFIG, "[Sync] transform must be a function.");
|
|
14838
|
-
}
|
|
14839
|
-
validateSyncIdempotencyConfig(config.idempotency);
|
|
14840
|
-
config.targets.forEach((target, index) => {
|
|
14841
|
-
validateTargetConfig(target, index);
|
|
14842
|
-
});
|
|
14843
|
-
if (config.resumeToken) {
|
|
14844
|
-
validateResumeTokenConfig(config.resumeToken);
|
|
14845
|
-
}
|
|
14846
|
-
}
|
|
14847
15079
|
var ResumeTokenStore = class {
|
|
14848
15080
|
constructor(options = {}) {
|
|
14849
15081
|
this.storage = options.storage ?? "file";
|
|
@@ -14867,16 +15099,9 @@ var ResumeTokenStore = class {
|
|
|
14867
15099
|
return JSON.parse(payload);
|
|
14868
15100
|
} catch (error) {
|
|
14869
15101
|
const code = error?.code;
|
|
14870
|
-
if (code !== "ENOENT")
|
|
14871
|
-
this.logger?.warn?.("[Sync] failed to load resume token", error);
|
|
14872
|
-
}
|
|
15102
|
+
if (code !== "ENOENT") this.logger?.warn?.("[Sync] failed to load resume token", error);
|
|
14873
15103
|
if (code !== "ENOENT" && this.strictLoad) {
|
|
14874
|
-
throw createError(
|
|
14875
|
-
ErrorCodes.DATABASE_ERROR,
|
|
14876
|
-
"[Sync] failed to load resume token",
|
|
14877
|
-
void 0,
|
|
14878
|
-
normalizeError(error)
|
|
14879
|
-
);
|
|
15104
|
+
throw createError(ErrorCodes.DATABASE_ERROR, "[Sync] failed to load resume token", void 0, normalizeError(error));
|
|
14880
15105
|
}
|
|
14881
15106
|
return null;
|
|
14882
15107
|
}
|
|
@@ -14902,12 +15127,7 @@ var ResumeTokenStore = class {
|
|
|
14902
15127
|
}
|
|
14903
15128
|
this.logger?.error?.("[Sync] failed to save resume token", lastError);
|
|
14904
15129
|
if (this.strictSave) {
|
|
14905
|
-
throw createError(
|
|
14906
|
-
ErrorCodes.DATABASE_ERROR,
|
|
14907
|
-
"[Sync] failed to save resume token",
|
|
14908
|
-
void 0,
|
|
14909
|
-
lastError ?? void 0
|
|
14910
|
-
);
|
|
15130
|
+
throw createError(ErrorCodes.DATABASE_ERROR, "[Sync] failed to save resume token", void 0, lastError ?? void 0);
|
|
14911
15131
|
}
|
|
14912
15132
|
}
|
|
14913
15133
|
async writePayload(payload) {
|
|
@@ -14940,9 +15160,7 @@ var ResumeTokenStore = class {
|
|
|
14940
15160
|
await copyFile(this.path, `${this.path}.bak`);
|
|
14941
15161
|
} catch (error) {
|
|
14942
15162
|
const code = error?.code;
|
|
14943
|
-
if (code !== "ENOENT")
|
|
14944
|
-
throw error;
|
|
14945
|
-
}
|
|
15163
|
+
if (code !== "ENOENT") throw error;
|
|
14946
15164
|
}
|
|
14947
15165
|
}
|
|
14948
15166
|
async clear() {
|
|
@@ -14954,12 +15172,80 @@ var ResumeTokenStore = class {
|
|
|
14954
15172
|
await unlink(this.path);
|
|
14955
15173
|
} catch (error) {
|
|
14956
15174
|
const code = error?.code;
|
|
14957
|
-
if (code !== "ENOENT")
|
|
14958
|
-
this.logger?.warn?.("[Sync] failed to clear resume token", error);
|
|
14959
|
-
}
|
|
15175
|
+
if (code !== "ENOENT") this.logger?.warn?.("[Sync] failed to clear resume token", error);
|
|
14960
15176
|
}
|
|
14961
15177
|
}
|
|
14962
15178
|
};
|
|
15179
|
+
|
|
15180
|
+
// src/capabilities/sync/index.ts
|
|
15181
|
+
function normalizeCollectionFilter(collections) {
|
|
15182
|
+
if (!collections?.length || collections.includes("*")) {
|
|
15183
|
+
return null;
|
|
15184
|
+
}
|
|
15185
|
+
return new Set(collections);
|
|
15186
|
+
}
|
|
15187
|
+
function validateTargetConfig(target, index) {
|
|
15188
|
+
if (!target || typeof target !== "object") {
|
|
15189
|
+
throw createError(ErrorCodes.INVALID_CONFIG, `[Sync] targets[${index}] must be an object.`);
|
|
15190
|
+
}
|
|
15191
|
+
if (typeof target.name !== "string" || target.name.trim() === "") {
|
|
15192
|
+
throw createError(ErrorCodes.INVALID_CONFIG, `[Sync] targets[${index}].name must be a non-empty string.`);
|
|
15193
|
+
}
|
|
15194
|
+
if (!target.apply && !target.uri && !target.pool) {
|
|
15195
|
+
throw createError(ErrorCodes.INVALID_CONFIG, `[Sync] targets[${index}] requires one of apply / uri / pool.`);
|
|
15196
|
+
}
|
|
15197
|
+
if (target.uri !== void 0 && (typeof target.uri !== "string" || target.uri.trim() === "")) {
|
|
15198
|
+
throw createError(ErrorCodes.INVALID_CONFIG, `[Sync] targets[${index}].uri must be a non-empty string when provided.`);
|
|
15199
|
+
}
|
|
15200
|
+
if (target.pool !== void 0 && (typeof target.pool !== "string" || target.pool.trim() === "")) {
|
|
15201
|
+
throw createError(ErrorCodes.INVALID_CONFIG, `[Sync] targets[${index}].pool must be a non-empty string when provided.`);
|
|
15202
|
+
}
|
|
15203
|
+
if (target.databaseName !== void 0 && (typeof target.databaseName !== "string" || target.databaseName.trim() === "")) {
|
|
15204
|
+
throw createError(ErrorCodes.INVALID_CONFIG, `[Sync] targets[${index}].databaseName must be a non-empty string when provided.`);
|
|
15205
|
+
}
|
|
15206
|
+
if (target.collections !== void 0 && (!Array.isArray(target.collections) || target.collections.length === 0)) {
|
|
15207
|
+
throw createError(ErrorCodes.INVALID_CONFIG, `[Sync] targets[${index}].collections must be a non-empty array when provided.`);
|
|
15208
|
+
}
|
|
15209
|
+
if (target.collections?.some((item) => typeof item !== "string" || item.trim() === "")) {
|
|
15210
|
+
throw createError(ErrorCodes.INVALID_CONFIG, `[Sync] targets[${index}].collections must contain non-empty strings.`);
|
|
15211
|
+
}
|
|
15212
|
+
if (target.apply !== void 0 && typeof target.apply !== "function") {
|
|
15213
|
+
throw createError(ErrorCodes.INVALID_CONFIG, `[Sync] targets[${index}].apply must be a function when provided.`);
|
|
15214
|
+
}
|
|
15215
|
+
}
|
|
15216
|
+
function validateSyncConfig(config) {
|
|
15217
|
+
if (!config || typeof config !== "object") {
|
|
15218
|
+
throw createError(ErrorCodes.INVALID_CONFIG, "[Sync] config must be an object.");
|
|
15219
|
+
}
|
|
15220
|
+
if (typeof config.enabled !== "boolean") {
|
|
15221
|
+
throw createError(ErrorCodes.INVALID_CONFIG, "[Sync] enabled must be a boolean.");
|
|
15222
|
+
}
|
|
15223
|
+
if (!config.enabled) {
|
|
15224
|
+
return;
|
|
15225
|
+
}
|
|
15226
|
+
if (!Array.isArray(config.targets) || config.targets.length === 0) {
|
|
15227
|
+
throw createError(ErrorCodes.INVALID_CONFIG, "[Sync] targets must be a non-empty array.");
|
|
15228
|
+
}
|
|
15229
|
+
if (config.collections !== void 0 && (!Array.isArray(config.collections) || config.collections.length === 0)) {
|
|
15230
|
+
throw createError(ErrorCodes.INVALID_CONFIG, "[Sync] collections must be a non-empty array when provided.");
|
|
15231
|
+
}
|
|
15232
|
+
if (config.collections?.some((item) => typeof item !== "string" || item.trim() === "")) {
|
|
15233
|
+
throw createError(ErrorCodes.INVALID_CONFIG, "[Sync] collections must contain non-empty strings.");
|
|
15234
|
+
}
|
|
15235
|
+
if (config.filter !== void 0 && typeof config.filter !== "function") {
|
|
15236
|
+
throw createError(ErrorCodes.INVALID_CONFIG, "[Sync] filter must be a function.");
|
|
15237
|
+
}
|
|
15238
|
+
if (config.transform !== void 0 && typeof config.transform !== "function") {
|
|
15239
|
+
throw createError(ErrorCodes.INVALID_CONFIG, "[Sync] transform must be a function.");
|
|
15240
|
+
}
|
|
15241
|
+
validateSyncIdempotencyConfig(config.idempotency);
|
|
15242
|
+
config.targets.forEach((target, index) => {
|
|
15243
|
+
validateTargetConfig(target, index);
|
|
15244
|
+
});
|
|
15245
|
+
if (config.resumeToken) {
|
|
15246
|
+
validateResumeTokenConfig(config.resumeToken);
|
|
15247
|
+
}
|
|
15248
|
+
}
|
|
14963
15249
|
var ChangeStreamSyncManager = class {
|
|
14964
15250
|
constructor(options) {
|
|
14965
15251
|
this.targets = [];
|
|
@@ -14968,7 +15254,10 @@ var ChangeStreamSyncManager = class {
|
|
|
14968
15254
|
this.closingStreamPromise = null;
|
|
14969
15255
|
this.changeQueue = Promise.resolve();
|
|
14970
15256
|
this.running = false;
|
|
15257
|
+
this.stopping = false;
|
|
15258
|
+
this.stopPromise = null;
|
|
14971
15259
|
this.fatalSyncError = null;
|
|
15260
|
+
this.generation = 0;
|
|
14972
15261
|
this.stats = {
|
|
14973
15262
|
eventCount: 0,
|
|
14974
15263
|
syncedCount: 0,
|
|
@@ -15001,40 +15290,62 @@ var ChangeStreamSyncManager = class {
|
|
|
15001
15290
|
* @since v1.0.9
|
|
15002
15291
|
*/
|
|
15003
15292
|
async start() {
|
|
15293
|
+
if (this.stopPromise) await this.stopPromise;
|
|
15004
15294
|
if (this.running || !this.config.enabled) return;
|
|
15005
15295
|
if (this.startPromise) return this.startPromise;
|
|
15296
|
+
const generation = ++this.generation;
|
|
15006
15297
|
const startup = (async () => {
|
|
15007
15298
|
await this.closingStreamPromise;
|
|
15299
|
+
await this.changeQueue.catch(() => void 0);
|
|
15300
|
+
this.assertCurrentGeneration(generation, true);
|
|
15008
15301
|
if (this.running) return;
|
|
15009
15302
|
this.fatalSyncError = null;
|
|
15010
|
-
await this.
|
|
15011
|
-
|
|
15012
|
-
|
|
15013
|
-
|
|
15014
|
-
|
|
15015
|
-
|
|
15016
|
-
|
|
15017
|
-
|
|
15018
|
-
const
|
|
15019
|
-
this.
|
|
15020
|
-
|
|
15021
|
-
|
|
15022
|
-
|
|
15023
|
-
|
|
15024
|
-
|
|
15025
|
-
|
|
15026
|
-
return;
|
|
15027
|
-
|
|
15028
|
-
|
|
15029
|
-
|
|
15030
|
-
|
|
15031
|
-
|
|
15032
|
-
|
|
15033
|
-
|
|
15034
|
-
|
|
15035
|
-
|
|
15036
|
-
|
|
15037
|
-
|
|
15303
|
+
await this.closeTargets();
|
|
15304
|
+
let stagedTargets = [];
|
|
15305
|
+
let stream = null;
|
|
15306
|
+
try {
|
|
15307
|
+
await this.validateEnvironment();
|
|
15308
|
+
this.assertCurrentGeneration(generation);
|
|
15309
|
+
stagedTargets = await this.initializeTargets();
|
|
15310
|
+
this.assertCurrentGeneration(generation);
|
|
15311
|
+
const resumeAfter = await this.tokenStore.load();
|
|
15312
|
+
this.assertCurrentGeneration(generation);
|
|
15313
|
+
const options = { fullDocument: "updateLookup" };
|
|
15314
|
+
if (resumeAfter) options.resumeAfter = resumeAfter;
|
|
15315
|
+
stream = this.db.watch(this.buildPipeline(), options);
|
|
15316
|
+
const activeStream = stream;
|
|
15317
|
+
activeStream.on("change", (event) => this.enqueueChange(event, generation));
|
|
15318
|
+
activeStream.on("error", (error) => {
|
|
15319
|
+
if (generation !== this.generation) return;
|
|
15320
|
+
const normalized = normalizeError(error);
|
|
15321
|
+
this.stats.errorCount += 1;
|
|
15322
|
+
this.stats.lastError = normalized;
|
|
15323
|
+
this.logger?.error?.("[Sync] change stream error", normalized);
|
|
15324
|
+
this.stopAfterFatalSyncError(normalized, generation);
|
|
15325
|
+
});
|
|
15326
|
+
activeStream.on("close", () => {
|
|
15327
|
+
if (generation !== this.generation || !this.running || this.changeStream !== activeStream) {
|
|
15328
|
+
this.logger?.debug?.("[Sync] change stream closed");
|
|
15329
|
+
return;
|
|
15330
|
+
}
|
|
15331
|
+
const error = createError(ErrorCodes.CONNECTION_CLOSED, "[Sync] change stream closed unexpectedly");
|
|
15332
|
+
this.stats.errorCount += 1;
|
|
15333
|
+
this.stats.lastError = error;
|
|
15334
|
+
this.logger?.warn?.("[Sync] change stream closed unexpectedly", error);
|
|
15335
|
+
this.stopAfterFatalSyncError(error, generation);
|
|
15336
|
+
});
|
|
15337
|
+
this.assertCurrentGeneration(generation);
|
|
15338
|
+
this.targets.push(...stagedTargets);
|
|
15339
|
+
stagedTargets = [];
|
|
15340
|
+
this.changeStream = activeStream;
|
|
15341
|
+
stream = null;
|
|
15342
|
+
this.running = true;
|
|
15343
|
+
this.stats.startTime = /* @__PURE__ */ new Date();
|
|
15344
|
+
} catch (error) {
|
|
15345
|
+
if (stream) await this.closeChangeStream(stream).catch(() => void 0);
|
|
15346
|
+
for (const target of stagedTargets.reverse()) await target.close().catch(() => void 0);
|
|
15347
|
+
throw error;
|
|
15348
|
+
}
|
|
15038
15349
|
})();
|
|
15039
15350
|
this.startPromise = startup;
|
|
15040
15351
|
try {
|
|
@@ -15048,16 +15359,37 @@ var ChangeStreamSyncManager = class {
|
|
|
15048
15359
|
* @since v1.0.9
|
|
15049
15360
|
*/
|
|
15050
15361
|
async stop() {
|
|
15051
|
-
this.
|
|
15052
|
-
|
|
15053
|
-
const
|
|
15054
|
-
|
|
15055
|
-
|
|
15056
|
-
|
|
15057
|
-
|
|
15362
|
+
if (this.stopPromise) return this.stopPromise;
|
|
15363
|
+
this.stopping = true;
|
|
15364
|
+
const cleanup = (async () => {
|
|
15365
|
+
if (this.startPromise && !this.running) this.generation += 1;
|
|
15366
|
+
await this.startPromise?.catch(() => void 0);
|
|
15367
|
+
await this.changeQueue.catch(() => void 0);
|
|
15368
|
+
this.generation += 1;
|
|
15369
|
+
this.running = false;
|
|
15370
|
+
const stream = this.changeStream;
|
|
15371
|
+
this.changeStream = null;
|
|
15372
|
+
if (stream) await this.closeChangeStream(stream);
|
|
15373
|
+
else await this.closingStreamPromise;
|
|
15374
|
+
await this.closeTargets();
|
|
15375
|
+
})();
|
|
15376
|
+
this.stopPromise = cleanup;
|
|
15377
|
+
try {
|
|
15378
|
+
await cleanup;
|
|
15379
|
+
} finally {
|
|
15380
|
+
if (this.stopPromise === cleanup) this.stopPromise = null;
|
|
15381
|
+
this.stopping = false;
|
|
15382
|
+
}
|
|
15383
|
+
}
|
|
15384
|
+
assertCurrentGeneration(generation, allowPreviousFatal = false) {
|
|
15385
|
+
if (generation !== this.generation) {
|
|
15386
|
+
throw createError(ErrorCodes.CONNECTION_CLOSED, "[Sync] startup was superseded by stop or restart");
|
|
15387
|
+
}
|
|
15388
|
+
if (!allowPreviousFatal && this.fatalSyncError) throw this.fatalSyncError;
|
|
15389
|
+
}
|
|
15390
|
+
async closeTargets() {
|
|
15058
15391
|
while (this.targets.length > 0) {
|
|
15059
|
-
|
|
15060
|
-
await target?.close();
|
|
15392
|
+
await this.targets.pop()?.close();
|
|
15061
15393
|
}
|
|
15062
15394
|
}
|
|
15063
15395
|
/**
|
|
@@ -15122,11 +15454,13 @@ var ChangeStreamSyncManager = class {
|
|
|
15122
15454
|
return pipeline;
|
|
15123
15455
|
}
|
|
15124
15456
|
async initializeTargets() {
|
|
15125
|
-
|
|
15126
|
-
|
|
15127
|
-
|
|
15128
|
-
|
|
15129
|
-
|
|
15457
|
+
const staged = [];
|
|
15458
|
+
try {
|
|
15459
|
+
for (const target of this.config.targets) staged.push(await this.resolveTarget(target));
|
|
15460
|
+
return staged;
|
|
15461
|
+
} catch (error) {
|
|
15462
|
+
for (const target of staged.reverse()) await target.close().catch(() => void 0);
|
|
15463
|
+
throw error;
|
|
15130
15464
|
}
|
|
15131
15465
|
}
|
|
15132
15466
|
async resolveTarget(target) {
|
|
@@ -15163,14 +15497,15 @@ var ChangeStreamSyncManager = class {
|
|
|
15163
15497
|
const client = await this.clientFactory(target.uri, target.options);
|
|
15164
15498
|
return createMongoTarget(target.name, collections, client, target.databaseName ?? this.db.databaseName, true);
|
|
15165
15499
|
}
|
|
15166
|
-
async handleChange(event) {
|
|
15167
|
-
if (this.fatalSyncError) {
|
|
15500
|
+
async handleChange(event, generation = this.generation) {
|
|
15501
|
+
if (generation !== this.generation || !this.running || this.fatalSyncError) {
|
|
15168
15502
|
this.logger?.warn?.("[Sync] skipping queued change after fatal sync error", this.fatalSyncError);
|
|
15169
15503
|
return;
|
|
15170
15504
|
}
|
|
15171
15505
|
this.stats.eventCount += 1;
|
|
15172
15506
|
this.stats.lastEventTime = /* @__PURE__ */ new Date();
|
|
15173
15507
|
if (this.config.filter && !this.config.filter(event)) {
|
|
15508
|
+
await this.saveProcessedToken(event, generation);
|
|
15174
15509
|
return;
|
|
15175
15510
|
}
|
|
15176
15511
|
let document = event.fullDocument;
|
|
@@ -15182,6 +15517,7 @@ var ChangeStreamSyncManager = class {
|
|
|
15182
15517
|
let duplicateTargets = 0;
|
|
15183
15518
|
const targetErrors = [];
|
|
15184
15519
|
for (const target of this.targets) {
|
|
15520
|
+
if (generation !== this.generation || this.fatalSyncError || !this.running) return;
|
|
15185
15521
|
if (target.collections && !target.collections.has(event.ns.coll)) {
|
|
15186
15522
|
continue;
|
|
15187
15523
|
}
|
|
@@ -15198,11 +15534,13 @@ var ChangeStreamSyncManager = class {
|
|
|
15198
15534
|
if (idempotencyKey && this.idempotency.markMode === "start") {
|
|
15199
15535
|
await this.markTargetEventApplied(idempotencyKey, event, target.name, "start");
|
|
15200
15536
|
}
|
|
15537
|
+
if (generation !== this.generation || this.fatalSyncError || !this.running) return;
|
|
15201
15538
|
await target.apply(event, document, {
|
|
15202
15539
|
targetName: target.name,
|
|
15203
15540
|
...idempotencyKey ? { idempotencyKey } : {}
|
|
15204
15541
|
});
|
|
15205
15542
|
if (idempotencyKey && this.idempotency.markMode === "success") {
|
|
15543
|
+
if (generation !== this.generation || this.fatalSyncError || !this.running) return;
|
|
15206
15544
|
await this.markTargetEventApplied(idempotencyKey, event, target.name, "success");
|
|
15207
15545
|
}
|
|
15208
15546
|
target.stats.syncCount += 1;
|
|
@@ -15223,26 +15561,30 @@ var ChangeStreamSyncManager = class {
|
|
|
15223
15561
|
}
|
|
15224
15562
|
}
|
|
15225
15563
|
if (targetErrors.length > 0) {
|
|
15226
|
-
this.stopAfterFatalSyncError(targetErrors[0]);
|
|
15564
|
+
this.stopAfterFatalSyncError(targetErrors[0], generation);
|
|
15227
15565
|
return;
|
|
15228
15566
|
}
|
|
15229
|
-
if (
|
|
15230
|
-
|
|
15231
|
-
await this.tokenStore.save(event._id);
|
|
15232
|
-
} catch (error) {
|
|
15233
|
-
const normalized = normalizeError(error);
|
|
15234
|
-
this.stats.tokenSaveErrorCount += 1;
|
|
15235
|
-
this.stats.lastTokenSaveError = normalized;
|
|
15236
|
-
this.stats.lastError = normalized;
|
|
15237
|
-
this.stopAfterFatalSyncError(normalized);
|
|
15238
|
-
throw normalized;
|
|
15239
|
-
}
|
|
15240
|
-
this.stats.syncedCount += 1;
|
|
15567
|
+
if (succeeded === eligibleTargets) {
|
|
15568
|
+
await this.saveProcessedToken(event, generation);
|
|
15241
15569
|
if (duplicateTargets > 0 && duplicateTargets === eligibleTargets) {
|
|
15242
15570
|
this.stats.duplicateEventCount += 1;
|
|
15243
15571
|
}
|
|
15244
15572
|
}
|
|
15245
15573
|
}
|
|
15574
|
+
async saveProcessedToken(event, generation) {
|
|
15575
|
+
if (generation !== this.generation || this.fatalSyncError || !this.running) return;
|
|
15576
|
+
try {
|
|
15577
|
+
await this.tokenStore.save(event._id);
|
|
15578
|
+
} catch (error) {
|
|
15579
|
+
const normalized = normalizeError(error);
|
|
15580
|
+
this.stats.tokenSaveErrorCount += 1;
|
|
15581
|
+
this.stats.lastTokenSaveError = normalized;
|
|
15582
|
+
this.stats.lastError = normalized;
|
|
15583
|
+
this.stopAfterFatalSyncError(normalized, generation);
|
|
15584
|
+
throw normalized;
|
|
15585
|
+
}
|
|
15586
|
+
if (generation === this.generation && !this.fatalSyncError && this.running) this.stats.syncedCount += 1;
|
|
15587
|
+
}
|
|
15246
15588
|
buildTargetIdempotencyKey(targetName, event) {
|
|
15247
15589
|
if (!this.idempotency.enabled) {
|
|
15248
15590
|
return null;
|
|
@@ -15265,7 +15607,8 @@ var ChangeStreamSyncManager = class {
|
|
|
15265
15607
|
ts: Date.now()
|
|
15266
15608
|
}, this.idempotency.ttl));
|
|
15267
15609
|
}
|
|
15268
|
-
stopAfterFatalSyncError(error) {
|
|
15610
|
+
stopAfterFatalSyncError(error, generation) {
|
|
15611
|
+
if (generation !== this.generation) return;
|
|
15269
15612
|
this.fatalSyncError = error;
|
|
15270
15613
|
this.running = false;
|
|
15271
15614
|
const stream = this.changeStream;
|
|
@@ -15283,12 +15626,14 @@ var ChangeStreamSyncManager = class {
|
|
|
15283
15626
|
if (this.closingStreamPromise === closing) this.closingStreamPromise = null;
|
|
15284
15627
|
});
|
|
15285
15628
|
}
|
|
15286
|
-
enqueueChange(event) {
|
|
15287
|
-
this.
|
|
15629
|
+
enqueueChange(event, generation) {
|
|
15630
|
+
if (generation !== this.generation || !this.running || this.stopping || this.fatalSyncError) return;
|
|
15631
|
+
this.changeQueue = this.changeQueue.then(() => this.handleChange(event, generation)).catch((error) => {
|
|
15288
15632
|
const normalized = normalizeError(error);
|
|
15289
15633
|
this.stats.errorCount += 1;
|
|
15290
15634
|
this.stats.lastError = normalized;
|
|
15291
15635
|
this.logger?.error?.("[Sync] change handling failed", normalized);
|
|
15636
|
+
this.stopAfterFatalSyncError(normalized, generation);
|
|
15292
15637
|
});
|
|
15293
15638
|
}
|
|
15294
15639
|
};
|
|
@@ -15717,7 +16062,9 @@ var SSHTunnelSSH2 = class {
|
|
|
15717
16062
|
passphrase,
|
|
15718
16063
|
port = 22,
|
|
15719
16064
|
readyTimeout = 2e4,
|
|
15720
|
-
keepaliveInterval = 3e4
|
|
16065
|
+
keepaliveInterval = 3e4,
|
|
16066
|
+
hostHash,
|
|
16067
|
+
hostVerifier
|
|
15721
16068
|
} = this._sshConfig;
|
|
15722
16069
|
if (!host || !username) {
|
|
15723
16070
|
throw createError(ErrorCodes.INVALID_CONFIG, "SSH config requires: host, username");
|
|
@@ -15726,6 +16073,8 @@ var SSHTunnelSSH2 = class {
|
|
|
15726
16073
|
throw createError(ErrorCodes.INVALID_CONFIG, "SSH authentication required: provide password, privateKey, or privateKeyPath");
|
|
15727
16074
|
}
|
|
15728
16075
|
const config = { host, port, username, readyTimeout, keepaliveInterval };
|
|
16076
|
+
if (hostHash !== void 0) config.hostHash = hostHash;
|
|
16077
|
+
if (hostVerifier !== void 0) config.hostVerifier = hostVerifier;
|
|
15729
16078
|
if (password) {
|
|
15730
16079
|
config.password = password;
|
|
15731
16080
|
} else if (privateKey) {
|
|
@@ -15744,6 +16093,25 @@ var SSHTunnelSSH2 = class {
|
|
|
15744
16093
|
return new Promise((resolve, reject) => {
|
|
15745
16094
|
const ssh = new SshClient();
|
|
15746
16095
|
let settled = false;
|
|
16096
|
+
const fail = (err) => {
|
|
16097
|
+
if (settled) {
|
|
16098
|
+
this.markDisconnected();
|
|
16099
|
+
return;
|
|
16100
|
+
}
|
|
16101
|
+
settled = true;
|
|
16102
|
+
this.isConnected = false;
|
|
16103
|
+
this.localPort = null;
|
|
16104
|
+
this.server = null;
|
|
16105
|
+
this.sshClient = null;
|
|
16106
|
+
for (const socket of this._activeSockets) socket.destroy();
|
|
16107
|
+
this._activeSockets.clear();
|
|
16108
|
+
void closeServer(server);
|
|
16109
|
+
try {
|
|
16110
|
+
ssh.end();
|
|
16111
|
+
} catch {
|
|
16112
|
+
}
|
|
16113
|
+
reject(err);
|
|
16114
|
+
};
|
|
15747
16115
|
const server = net.createServer((socket) => {
|
|
15748
16116
|
this._activeSockets.add(socket);
|
|
15749
16117
|
socket.on("close", () => this._activeSockets.delete(socket));
|
|
@@ -15769,12 +16137,7 @@ var SSHTunnelSSH2 = class {
|
|
|
15769
16137
|
}
|
|
15770
16138
|
);
|
|
15771
16139
|
});
|
|
15772
|
-
server.on("error",
|
|
15773
|
-
if (!settled) {
|
|
15774
|
-
settled = true;
|
|
15775
|
-
reject(err);
|
|
15776
|
-
}
|
|
15777
|
-
});
|
|
16140
|
+
server.on("error", fail);
|
|
15778
16141
|
server.listen(this._sshConfig.localPort ?? 0, "127.0.0.1", () => {
|
|
15779
16142
|
this.server = server;
|
|
15780
16143
|
const addr = server.address();
|
|
@@ -15789,25 +16152,25 @@ var SSHTunnelSSH2 = class {
|
|
|
15789
16152
|
});
|
|
15790
16153
|
ssh.on("error", (err) => {
|
|
15791
16154
|
if (!settled) {
|
|
15792
|
-
|
|
15793
|
-
server.close();
|
|
15794
|
-
reject(err);
|
|
16155
|
+
fail(err);
|
|
15795
16156
|
return;
|
|
15796
16157
|
}
|
|
15797
16158
|
this.markDisconnected();
|
|
15798
16159
|
});
|
|
15799
16160
|
const handleDisconnect = () => {
|
|
15800
16161
|
if (!settled) {
|
|
15801
|
-
|
|
15802
|
-
server.close();
|
|
15803
|
-
reject(createError(ErrorCodes.CONNECTION_FAILED, "SSH connection closed before the tunnel became ready"));
|
|
16162
|
+
fail(createError(ErrorCodes.CONNECTION_FAILED, "SSH connection closed before the tunnel became ready"));
|
|
15804
16163
|
return;
|
|
15805
16164
|
}
|
|
15806
16165
|
this.markDisconnected();
|
|
15807
16166
|
};
|
|
15808
16167
|
ssh.on("close", handleDisconnect);
|
|
15809
16168
|
ssh.on("end", handleDisconnect);
|
|
15810
|
-
|
|
16169
|
+
try {
|
|
16170
|
+
ssh.connect(authConfig);
|
|
16171
|
+
} catch (err) {
|
|
16172
|
+
fail(err);
|
|
16173
|
+
}
|
|
15811
16174
|
});
|
|
15812
16175
|
});
|
|
15813
16176
|
}
|
|
@@ -15838,6 +16201,7 @@ var SSHTunnelSSH2 = class {
|
|
|
15838
16201
|
ssh?.end();
|
|
15839
16202
|
}
|
|
15840
16203
|
markDisconnected() {
|
|
16204
|
+
const ssh = this.sshClient;
|
|
15841
16205
|
this.isConnected = false;
|
|
15842
16206
|
this.localPort = null;
|
|
15843
16207
|
this.sshClient = null;
|
|
@@ -15848,6 +16212,10 @@ var SSHTunnelSSH2 = class {
|
|
|
15848
16212
|
}
|
|
15849
16213
|
this._activeSockets.clear();
|
|
15850
16214
|
void closeServer(server);
|
|
16215
|
+
try {
|
|
16216
|
+
ssh?.end();
|
|
16217
|
+
} catch {
|
|
16218
|
+
}
|
|
15851
16219
|
}
|
|
15852
16220
|
};
|
|
15853
16221
|
function parseHostFromUri(uri) {
|
|
@@ -15934,12 +16302,17 @@ async function prepareSshTunnelConnectConfig(connectConfig, databaseName, logger
|
|
|
15934
16302
|
const remotePort = Number(cfg["dstPort"] ?? rawCfg["remotePort"] ?? rawCfg["mongoPort"] ?? parsePortFromUri(uri));
|
|
15935
16303
|
const tunnel = new SSHTunnelSSH2(sshCfg, remoteHost, remotePort, { name: databaseName });
|
|
15936
16304
|
logger.info?.(`[SSH] Establishing tunnel to ${remoteHost}:${remotePort} via ${String(cfg["host"])}`);
|
|
15937
|
-
|
|
15938
|
-
|
|
15939
|
-
|
|
15940
|
-
|
|
15941
|
-
|
|
15942
|
-
|
|
16305
|
+
try {
|
|
16306
|
+
await tunnel.connect();
|
|
16307
|
+
logger.info?.(`[SSH] Tunnel ready \u2014 local address: ${tunnel.getLocalAddress()}`);
|
|
16308
|
+
return {
|
|
16309
|
+
tunnel,
|
|
16310
|
+
connectConfig: connectConfig?.uri ? { ...connectConfig, uri: tunnel.getTunnelUri("mongodb", connectConfig.uri) } : connectConfig
|
|
16311
|
+
};
|
|
16312
|
+
} catch (error) {
|
|
16313
|
+
await tunnel.close();
|
|
16314
|
+
throw error;
|
|
16315
|
+
}
|
|
15943
16316
|
}
|
|
15944
16317
|
|
|
15945
16318
|
// src/entry/runtime-schema-dsl.ts
|
|
@@ -16234,7 +16607,7 @@ function makePageResult(rows, ctx) {
|
|
|
16234
16607
|
import { createHash as createHash4 } from "node:crypto";
|
|
16235
16608
|
import os2 from "node:os";
|
|
16236
16609
|
import path3 from "node:path";
|
|
16237
|
-
import { BSON } from "mongodb";
|
|
16610
|
+
import { BSON as BSON2 } from "mongodb";
|
|
16238
16611
|
|
|
16239
16612
|
// src/capabilities/data-tasks/support.ts
|
|
16240
16613
|
var INDEX_OPTION_KEYS = [
|
|
@@ -16342,7 +16715,7 @@ function canonicalize(value) {
|
|
|
16342
16715
|
return value;
|
|
16343
16716
|
}
|
|
16344
16717
|
function canonicalStringify(value) {
|
|
16345
|
-
return JSON.stringify(canonicalize(
|
|
16718
|
+
return JSON.stringify(canonicalize(BSON2.EJSON.serialize(value, { relaxed: false })));
|
|
16346
16719
|
}
|
|
16347
16720
|
function hashDataTaskValue(value) {
|
|
16348
16721
|
return createHash4("sha256").update(canonicalStringify(value)).digest("hex");
|
|
@@ -16831,9 +17204,8 @@ var MonSQLizeRuntime = class {
|
|
|
16831
17204
|
driverError = error instanceof Error ? error.message : String(error);
|
|
16832
17205
|
}
|
|
16833
17206
|
}
|
|
16834
|
-
const
|
|
16835
|
-
const poolHealth =
|
|
16836
|
-
const distributedStats = this._distributedInvalidator?.getStats() ?? null;
|
|
17207
|
+
const health = summarizeRuntimeHealth(this.options.cache, this._cache, this._poolManager, this._distributedInvalidator);
|
|
17208
|
+
const { cacheStats, poolHealth, distributedStats, cacheEnabled, distributedEnabled, cacheProbeStatus, distributedStatus, poolsStatus } = health;
|
|
16837
17209
|
const connected = this._connected && driverConnected;
|
|
16838
17210
|
return {
|
|
16839
17211
|
status: connected ? "up" : "down",
|
|
@@ -16841,7 +17213,7 @@ var MonSQLizeRuntime = class {
|
|
|
16841
17213
|
driver: { connected: driverConnected, ...driverError ? { error: driverError } : {} },
|
|
16842
17214
|
defaults: this.getDefaults(),
|
|
16843
17215
|
cache: {
|
|
16844
|
-
enabled:
|
|
17216
|
+
enabled: cacheEnabled,
|
|
16845
17217
|
stats: cacheStats,
|
|
16846
17218
|
pools: poolHealth,
|
|
16847
17219
|
distributed: distributedStats
|
|
@@ -16853,18 +17225,18 @@ var MonSQLizeRuntime = class {
|
|
|
16853
17225
|
...driverError ? { error: driverError } : {}
|
|
16854
17226
|
},
|
|
16855
17227
|
cache: {
|
|
16856
|
-
status:
|
|
16857
|
-
enabled:
|
|
17228
|
+
status: cacheProbeStatus,
|
|
17229
|
+
enabled: cacheEnabled,
|
|
16858
17230
|
hasStats: cacheStats !== void 0
|
|
16859
17231
|
},
|
|
16860
17232
|
distributedCacheInvalidator: {
|
|
16861
|
-
status:
|
|
16862
|
-
enabled:
|
|
17233
|
+
status: distributedStatus,
|
|
17234
|
+
enabled: distributedEnabled,
|
|
16863
17235
|
stats: distributedStats
|
|
16864
17236
|
},
|
|
16865
17237
|
pools: {
|
|
16866
|
-
status:
|
|
16867
|
-
enabled:
|
|
17238
|
+
status: poolsStatus,
|
|
17239
|
+
enabled: this._poolManager !== null,
|
|
16868
17240
|
health: poolHealth
|
|
16869
17241
|
}
|
|
16870
17242
|
},
|
|
@@ -17226,7 +17598,7 @@ var MonSQLizeRuntime = class {
|
|
|
17226
17598
|
};
|
|
17227
17599
|
|
|
17228
17600
|
// src/capabilities/data-tasks/job-planner.ts
|
|
17229
|
-
import { BSON as
|
|
17601
|
+
import { BSON as BSON3 } from "mongodb";
|
|
17230
17602
|
|
|
17231
17603
|
// src/capabilities/data-tasks/document-utils.ts
|
|
17232
17604
|
function isPlainRecord(value) {
|
|
@@ -17708,7 +18080,7 @@ async function planCollection(sourceHost, targetHost, config, sampleSize) {
|
|
|
17708
18080
|
before: change.before,
|
|
17709
18081
|
after: change.after
|
|
17710
18082
|
}));
|
|
17711
|
-
const backupBytes = changes.reduce((total, change) => total + Buffer.byteLength(`${
|
|
18083
|
+
const backupBytes = changes.reduce((total, change) => total + Buffer.byteLength(`${BSON3.EJSON.stringify({
|
|
17712
18084
|
...change,
|
|
17713
18085
|
beforeHash: hashDataTaskValue(change.before),
|
|
17714
18086
|
plannedAfterHash: hashDataTaskValue(change.after)
|
|
@@ -17793,7 +18165,7 @@ import { mkdir as mkdir2, open as open2, readFile as readFile2, rename as rename
|
|
|
17793
18165
|
import path4 from "node:path";
|
|
17794
18166
|
import { gzip, gunzip } from "node:zlib";
|
|
17795
18167
|
import { promisify } from "node:util";
|
|
17796
|
-
import { BSON as
|
|
18168
|
+
import { BSON as BSON4, Int32, Long } from "mongodb";
|
|
17797
18169
|
var gzipAsync = promisify(gzip);
|
|
17798
18170
|
var gunzipAsync = promisify(gunzip);
|
|
17799
18171
|
var BACKUP_TARGET = /* @__PURE__ */ Symbol("dataTaskBackupTarget");
|
|
@@ -17810,10 +18182,31 @@ function checksum(buffer) {
|
|
|
17810
18182
|
return createHash5("sha256").update(buffer).digest("hex");
|
|
17811
18183
|
}
|
|
17812
18184
|
function ejsonLine(value) {
|
|
17813
|
-
return
|
|
18185
|
+
return BSON4.EJSON.stringify(value, { relaxed: false });
|
|
17814
18186
|
}
|
|
17815
18187
|
function ejsonManifest(value) {
|
|
17816
|
-
return
|
|
18188
|
+
return BSON4.EJSON.stringify(value, void 0, 2, { relaxed: false });
|
|
18189
|
+
}
|
|
18190
|
+
function manifestInteger(value, field, minimum) {
|
|
18191
|
+
const numeric = value instanceof Int32 ? value.valueOf() : Long.isLong(value) ? value.toBigInt() : value;
|
|
18192
|
+
if (typeof numeric === "bigint") {
|
|
18193
|
+
if (numeric < BigInt(minimum) || numeric > BigInt(Number.MAX_SAFE_INTEGER)) {
|
|
18194
|
+
throw new Error(`invalid backup ${field}`);
|
|
18195
|
+
}
|
|
18196
|
+
return Number(numeric);
|
|
18197
|
+
}
|
|
18198
|
+
if (typeof numeric !== "number" || !Number.isSafeInteger(numeric) || numeric < minimum) {
|
|
18199
|
+
throw new Error(`invalid backup ${field}`);
|
|
18200
|
+
}
|
|
18201
|
+
return numeric;
|
|
18202
|
+
}
|
|
18203
|
+
function decodeManifestControls(raw) {
|
|
18204
|
+
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) throw new Error("invalid backup manifest");
|
|
18205
|
+
const manifest = raw;
|
|
18206
|
+
manifest.version = manifestInteger(manifest.version, "manifest", 1);
|
|
18207
|
+
manifest.entryCount = manifestInteger(manifest.entryCount, "entry count", 0);
|
|
18208
|
+
if (manifest.maxBytes !== void 0) manifest.maxBytes = manifestInteger(manifest.maxBytes, "max bytes", 1);
|
|
18209
|
+
return manifest;
|
|
17817
18210
|
}
|
|
17818
18211
|
function sanitizeName(value) {
|
|
17819
18212
|
return value.replace(/[^a-zA-Z0-9._-]+/g, "-").replace(/^-+|-+$/g, "") || "data-task";
|
|
@@ -17991,10 +18384,9 @@ async function createRestoreSafetyBackup(source, entries, beforeIndexes, targetH
|
|
|
17991
18384
|
}
|
|
17992
18385
|
async function readDataTaskBackup(ref) {
|
|
17993
18386
|
try {
|
|
17994
|
-
const manifest =
|
|
18387
|
+
const manifest = decodeManifestControls(BSON4.EJSON.parse(await readFile2(ref.manifestPath, "utf8"), { relaxed: false }));
|
|
17995
18388
|
if (manifest.kind !== "monsqlize-data-task-backup" || manifest.version !== 1 || manifest.runId !== ref.runId) throw new Error("invalid backup manifest");
|
|
17996
18389
|
if (manifest.compression !== "gzip" && manifest.compression !== "none") throw new Error("invalid backup compression");
|
|
17997
|
-
if (!Number.isSafeInteger(manifest.entryCount) || manifest.entryCount < 0) throw new Error("invalid backup entry count");
|
|
17998
18390
|
if (typeof manifest.checksum !== "string" || !/^[a-f0-9]{64}$/i.test(manifest.checksum)) throw new Error("invalid backup checksum");
|
|
17999
18391
|
if (typeof manifest.dataFile !== "string" || manifest.dataFile.length === 0 || path4.isAbsolute(manifest.dataFile) || path4.basename(manifest.dataFile) !== manifest.dataFile) {
|
|
18000
18392
|
throw new Error("backup data file must stay beside its manifest");
|
|
@@ -18007,7 +18399,7 @@ async function readDataTaskBackup(ref) {
|
|
|
18007
18399
|
if (digest !== manifest.checksum || digest !== ref.checksum) throw new Error("backup checksum mismatch");
|
|
18008
18400
|
const plain = manifest.compression === "gzip" ? await gunzipAsync(contents) : contents;
|
|
18009
18401
|
const lines = plain.toString("utf8").split(/\r?\n/).filter(Boolean);
|
|
18010
|
-
const entries = lines.map((line) =>
|
|
18402
|
+
const entries = lines.map((line) => BSON4.EJSON.parse(line, { relaxed: false }));
|
|
18011
18403
|
if (entries.length !== manifest.entryCount) throw new Error("backup entry count mismatch");
|
|
18012
18404
|
return { ref: attachTarget({ ...ref }, backupTargetHint(ref)), manifest, entries };
|
|
18013
18405
|
} catch (error) {
|
|
@@ -18325,6 +18717,7 @@ async function acquireDataTaskLease(host, options) {
|
|
|
18325
18717
|
}
|
|
18326
18718
|
|
|
18327
18719
|
// src/capabilities/data-tasks/job-restore.ts
|
|
18720
|
+
import { Int32 as Int322, Long as Long2 } from "mongodb";
|
|
18328
18721
|
async function findCurrent(collection, identity) {
|
|
18329
18722
|
let chain = collection.find(identity);
|
|
18330
18723
|
if (typeof chain.limit === "function") chain = chain.limit(2);
|
|
@@ -18345,13 +18738,20 @@ function restoreApproval(hashes) {
|
|
|
18345
18738
|
};
|
|
18346
18739
|
return { ...payload, token: hashDataTaskValue(payload) };
|
|
18347
18740
|
}
|
|
18741
|
+
function comparableIndexKey(key) {
|
|
18742
|
+
return Object.fromEntries(Object.entries(key).map(([field, value]) => {
|
|
18743
|
+
if (value instanceof Int322) return [field, value.valueOf()];
|
|
18744
|
+
if (Long2.isLong(value) && value.toBigInt() <= BigInt(Number.MAX_SAFE_INTEGER) && value.toBigInt() >= BigInt(Number.MIN_SAFE_INTEGER)) return [field, Number(value.toBigInt())];
|
|
18745
|
+
return [field, value];
|
|
18746
|
+
}));
|
|
18747
|
+
}
|
|
18348
18748
|
function exactIndex(index, current) {
|
|
18349
|
-
return classifyDataTaskIndexes([{ key: index.key, name: index.name, options: index.options }], current)[0]?.status === "existing";
|
|
18749
|
+
return classifyDataTaskIndexes([{ key: comparableIndexKey(index.key), name: index.name, options: index.options }], current)[0]?.status === "existing";
|
|
18350
18750
|
}
|
|
18351
18751
|
function pendingIndexMatches(index, current) {
|
|
18352
18752
|
return current.filter((candidate) => {
|
|
18353
18753
|
if (index.name && candidate.name !== index.name) return false;
|
|
18354
|
-
return classifyDataTaskIndexes([{ key: index.key, options: index.options }], [candidate])[0]?.status === "existing";
|
|
18754
|
+
return classifyDataTaskIndexes([{ key: comparableIndexKey(index.key), options: index.options }], [candidate])[0]?.status === "existing";
|
|
18355
18755
|
});
|
|
18356
18756
|
}
|
|
18357
18757
|
function plannedDocumentMatches(current, entry) {
|
|
@@ -18619,7 +19019,7 @@ async function restoreDataTaskPlan(plan, target, targetHint, lease) {
|
|
|
18619
19019
|
await updateDataTaskBackup(safety, (manifest) => {
|
|
18620
19020
|
manifest.pendingIndexes = [...manifest.pendingIndexes ?? [], pending];
|
|
18621
19021
|
});
|
|
18622
|
-
await collection.createIndex(index.key, { ...index.options, name: index.name });
|
|
19022
|
+
await collection.createIndex(comparableIndexKey(index.key), { ...index.options, name: index.name });
|
|
18623
19023
|
if (!exactIndex(index, await collection.listIndexes())) {
|
|
18624
19024
|
throw new DataTaskJobError("RESTORE_FAILED", `index "${index.name}" failed immediate readback.`, "restore");
|
|
18625
19025
|
}
|