@classytic/mongokit 3.40.0 → 3.40.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/README.md +71 -0
- package/dist/{PaginationEngine-D-P19Y9m.d.mts → PaginationEngine-CRHyuUtw.d.mts} +17 -3
- package/dist/{PaginationEngine-Dvl8jcRY.mjs → PaginationEngine-CfT2Cxkj.mjs} +296 -137
- package/dist/{Repository-BMgPSkqZ.mjs → Repository-CYhSC9ut.mjs} +571 -175
- package/dist/{Repository-CR8Y9g2E.d.mts → Repository-pnLw1rG5.d.mts} +57 -16
- package/dist/actions/index.d.mts +1 -1
- package/dist/actions/index.mjs +1 -1
- package/dist/adapter/index.mjs +1 -1
- package/dist/{adapter-BrVfPFSW.mjs → adapter-CkMoMF83.mjs} +1 -1
- package/dist/ai/index.d.mts +1 -1
- package/dist/better-auth/index.mjs +2 -2
- package/dist/{create-repository-CAle1MSU.d.mts → create-repository-BUx9vfG9.d.mts} +5 -5
- package/dist/{create-repository-BzBUMvd1.mjs → create-repository-BYyWzLbt.mjs} +2 -2
- package/dist/{index-CmjNyulu.d.mts → index-SYF8N28D.d.mts} +37 -19
- package/dist/index.d.mts +55 -15
- package/dist/index.mjs +57 -11
- package/dist/join-scope-DSAznE8E.mjs +168 -0
- package/dist/{mongooseToJsonSchema-waUVQwK4.mjs → mongooseToJsonSchema-C3k4ide0.mjs} +5 -1
- package/dist/{multi-tenant.plugin-CBgCo9im.d.mts → multi-tenant.plugin-a3UhQARj.d.mts} +2 -2
- package/dist/{operations-CrRIoc8Z.d.mts → operations-XiFgG5US.d.mts} +36 -13
- package/dist/outbox/index.d.mts +1 -1
- package/dist/outbox/index.mjs +1 -1
- package/dist/pagination/PaginationEngine.d.mts +2 -2
- package/dist/pagination/PaginationEngine.mjs +1 -1
- package/dist/pagination/utils/limits.d.mts +1 -1
- package/dist/{pagination-BpIv2bdS.d.mts → pagination-D6psses0.d.mts} +116 -13
- package/dist/{plugin-options-Zk46lD6F.d.mts → plugin-options-CRhIvSzx.d.mts} +2 -2
- package/dist/plugins/index.d.mts +3 -3
- package/dist/plugins/index.mjs +2 -2
- package/dist/query-defaults-BxUcSOoF.mjs +208 -0
- package/dist/read-model/index.d.mts +66 -0
- package/dist/read-model/index.mjs +193 -0
- package/dist/{repository-odl03582.d.mts → repository-DNHfjAZX.d.mts} +16 -2
- package/dist/sync/index.d.mts +15 -2
- package/dist/sync/index.mjs +38 -1
- package/dist/testkit/index.d.mts +40 -3
- package/dist/testkit/index.mjs +113 -2
- package/dist/{timestamp.plugin-DF0gJB6p.mjs → timestamp.plugin-zfdLYZiO.mjs} +50 -31
- package/dist/{to-plain-k7SjNlSS.d.mts → to-plain-CjgEInWV.d.mts} +2 -2
- package/dist/{transaction-B5L9glO0.mjs → transaction-BrRYtH_b.mjs} +127 -5
- package/dist/{update-DG0fCwRJ.mjs → update-WgeAUJ_I.mjs} +44 -22
- package/dist/utils/index.d.mts +1 -1
- package/dist/utils/index.mjs +1 -1
- package/dist/{validation-chain.plugin-BLY2gLEk.mjs → validation-chain.plugin-CHUHWMtk.mjs} +15 -3
- package/dist/{validation-chain.plugin-DYp9E7K2.d.mts → validation-chain.plugin-CTcNJCmV.d.mts} +4 -4
- package/package.json +8 -3
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
import { i as isValidIdForType, t as getSchemaIdType } from "./id-resolution-BqDrk0S2.mjs";
|
|
2
|
-
import { a as narrowToIds, i as keysetFilter, o as selectKeysetChunk } from "./id-chunks-BmAwsqNk.mjs";
|
|
3
|
-
import { _ as SHORTHAND_OPS, a as count, b as create, c as getByQuery, d as deleteById, f as deleteByQuery, g as LookupBuilder, h as distinct, l as getOrCreate, n as update, o as exists, r as updateByQuery, s as getById, t as findOneAndUpdate, v as compileFilterToMongo, x as createMany, y as castFilterToSchema } from "./update-
|
|
2
|
+
import { a as narrowToIds, i as keysetFilter, o as selectKeysetChunk, r as idChunks, t as DEFAULT_ID_CHUNK } from "./id-chunks-BmAwsqNk.mjs";
|
|
3
|
+
import { _ as SHORTHAND_OPS, a as count, b as create, c as getByQuery, d as deleteById, f as deleteByQuery, g as LookupBuilder, h as distinct, l as getOrCreate, n as update, o as exists, r as updateByQuery, s as getById, t as findOneAndUpdate, v as compileFilterToMongo, x as createMany, y as castFilterToSchema } from "./update-WgeAUJ_I.mjs";
|
|
4
4
|
import { n as warn } from "./logger-K7dPmu2U.mjs";
|
|
5
5
|
import { n as isDuplicateKeyError, r as parseDuplicateKeyError, t as createError } from "./error-BuByZwKU.mjs";
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
6
|
+
import { i as applyToQuery, n as applyCursorTimeoutMode, r as applyToAggregate, u as resolveQueryOptions } from "./query-defaults-BxUcSOoF.mjs";
|
|
7
|
+
import { a as createTxBoundRepo, d as resolveMongoCapabilities, i as withTransaction } from "./transaction-BrRYtH_b.mjs";
|
|
8
|
+
import { _ as resolveCursorSecrets, d as withGroupTiebreak, f as withIdTiebreak, g as attachSignature, i as validateKeysetSort, l as assertOffsetWithinCap, n as getPrimaryField, o as encodeCursor, s as resolveCursorFilter, t as PaginationEngine, u as cursorScope, v as verifySignature } from "./PaginationEngine-CfT2Cxkj.mjs";
|
|
8
9
|
import { calculateTotalPages } from "./pagination/utils/limits.mjs";
|
|
9
10
|
import { hasNearOperator, rewriteNearForCount } from "./query/primitives/geo.mjs";
|
|
10
|
-
import {
|
|
11
|
+
import { a as scopeJoins, r as markGoverned } from "./join-scope-DSAznE8E.mjs";
|
|
12
|
+
import { BULK_UPSERT_ERROR_CODES, BULK_UPSERT_FAILURE_CODES, RepositoryBase, bulkUpsertOptionsError, createDistributionGuard, runChunkedArchive, runChunkedPurge, summarizeBulkUpsert, throwIfAborted, validatePluginOrder, withRetry } from "@classytic/repo-core/repository";
|
|
11
13
|
import mongoose from "mongoose";
|
|
12
14
|
import { isFilter } from "@classytic/repo-core/filter";
|
|
13
15
|
import { VersionConflictError, conservativeMongoIsTransientConflict } from "@classytic/repo-core/errors";
|
|
@@ -191,6 +193,11 @@ const OP_REGISTRY = {
|
|
|
191
193
|
mutates: true,
|
|
192
194
|
hasIdContext: false
|
|
193
195
|
},
|
|
196
|
+
bulkUpsert: {
|
|
197
|
+
policyKey: "operations",
|
|
198
|
+
mutates: true,
|
|
199
|
+
hasIdContext: false
|
|
200
|
+
},
|
|
194
201
|
getById: {
|
|
195
202
|
policyKey: "query",
|
|
196
203
|
mutates: false,
|
|
@@ -246,6 +253,11 @@ const OP_REGISTRY = {
|
|
|
246
253
|
mutates: false,
|
|
247
254
|
hasIdContext: false
|
|
248
255
|
},
|
|
256
|
+
iterate: {
|
|
257
|
+
policyKey: "query",
|
|
258
|
+
mutates: false,
|
|
259
|
+
hasIdContext: false
|
|
260
|
+
},
|
|
249
261
|
watch: {
|
|
250
262
|
policyKey: "query",
|
|
251
263
|
mutates: false,
|
|
@@ -316,20 +328,6 @@ function assertExpectedIndex(model, leadingKeys) {
|
|
|
316
328
|
checked.set(model, (done ?? /* @__PURE__ */ new Set()).add(id));
|
|
317
329
|
}
|
|
318
330
|
//#endregion
|
|
319
|
-
//#region src/actions/aggregate-ir/normalize.ts
|
|
320
|
-
/**
|
|
321
|
-
* AggRequest → input normalization (mongokit binding).
|
|
322
|
-
*
|
|
323
|
-
* Re-exports the kit-neutral helpers from `@classytic/repo-core/aggregate`
|
|
324
|
-
* with the mongokit error-prefix pre-bound, so call sites stay free of
|
|
325
|
-
* boilerplate. The actual logic lives in repo-core — when the rules
|
|
326
|
-
* for what a valid AggRequest looks like change, update the IR
|
|
327
|
-
* contract there.
|
|
328
|
-
*/
|
|
329
|
-
function validateMeasures$1(measures) {
|
|
330
|
-
validateMeasures(measures, "mongokit");
|
|
331
|
-
}
|
|
332
|
-
//#endregion
|
|
333
331
|
//#region src/actions/aggregate-ir/dateBucket.ts
|
|
334
332
|
/**
|
|
335
333
|
* Compile a `AggDateBucket` to the mongo expression that produces the
|
|
@@ -845,6 +843,20 @@ function compileMeasure(measure) {
|
|
|
845
843
|
}
|
|
846
844
|
}
|
|
847
845
|
//#endregion
|
|
846
|
+
//#region src/actions/aggregate-ir/normalize.ts
|
|
847
|
+
/**
|
|
848
|
+
* AggRequest → input normalization (mongokit binding).
|
|
849
|
+
*
|
|
850
|
+
* Re-exports the kit-neutral helpers from `@classytic/repo-core/aggregate`
|
|
851
|
+
* with the mongokit error-prefix pre-bound, so call sites stay free of
|
|
852
|
+
* boilerplate. The actual logic lives in repo-core — when the rules
|
|
853
|
+
* for what a valid AggRequest looks like change, update the IR
|
|
854
|
+
* contract there.
|
|
855
|
+
*/
|
|
856
|
+
function validateMeasures$1(measures) {
|
|
857
|
+
validateMeasures(measures, "mongokit");
|
|
858
|
+
}
|
|
859
|
+
//#endregion
|
|
848
860
|
//#region src/actions/aggregate-ir/pipeline.ts
|
|
849
861
|
function buildAggPipeline(req, schema) {
|
|
850
862
|
validateMeasures$1(req.measures);
|
|
@@ -1239,19 +1251,34 @@ function irReferencesAlias(filter, aliasSet) {
|
|
|
1239
1251
|
return false;
|
|
1240
1252
|
}
|
|
1241
1253
|
//#endregion
|
|
1254
|
+
//#region src/actions/aggregate-ir/execute.ts
|
|
1255
|
+
/** Build the aggregate for `stages` with every execution option applied, unexecuted. */
|
|
1256
|
+
function prepareAgg(Model, stages, req, options = {}) {
|
|
1257
|
+
const aggregation = Model.aggregate(options.finalize ? options.finalize(stages) : stages);
|
|
1258
|
+
if (options.session) aggregation.session(options.session);
|
|
1259
|
+
applyToAggregate(aggregation, options.queryOptions ?? {});
|
|
1260
|
+
applyExecutionHints(aggregation, options.queryOptions && req.executionHints ? {
|
|
1261
|
+
...req.executionHints,
|
|
1262
|
+
maxTimeMs: void 0
|
|
1263
|
+
} : req.executionHints);
|
|
1264
|
+
return aggregation;
|
|
1265
|
+
}
|
|
1266
|
+
async function runAgg(Model, stages, req, options = {}) {
|
|
1267
|
+
return await prepareAgg(Model, stages, req, options).exec();
|
|
1268
|
+
}
|
|
1269
|
+
async function executeAgg(Model, req, options = {}) {
|
|
1270
|
+
const { pipeline } = buildAggPipeline(req, Model.schema);
|
|
1271
|
+
return runAgg(Model, pipeline, req, options);
|
|
1272
|
+
}
|
|
1273
|
+
//#endregion
|
|
1242
1274
|
//#region src/actions/aggregate-ir/count.ts
|
|
1243
1275
|
async function countAggGroups(Model, req, options = {}) {
|
|
1244
|
-
const session = options.session;
|
|
1245
1276
|
const groupCols = normalizeGroupBy(req.groupBy);
|
|
1246
1277
|
const hasDottedGroupBy = groupCols.some((f) => f.includes("."));
|
|
1247
1278
|
const hasDateBuckets = !!req.dateBuckets && Object.keys(req.dateBuckets).length > 0;
|
|
1248
1279
|
if (req.having !== void 0 || req.lookups !== void 0 && req.lookups.length > 0 || hasDottedGroupBy || hasDateBuckets) {
|
|
1249
1280
|
const { pipeline, prePaginationIndex } = buildAggPipeline(req, Model.schema);
|
|
1250
|
-
const
|
|
1251
|
-
const aggregation = Model.aggregate(finalPipeline);
|
|
1252
|
-
if (session) aggregation.session(session);
|
|
1253
|
-
applyExecutionHints(aggregation, req.executionHints);
|
|
1254
|
-
const [row] = await aggregation.exec();
|
|
1281
|
+
const [row] = await runAgg(Model, [...pipeline.slice(0, prePaginationIndex), { $count: "n" }], req, options);
|
|
1255
1282
|
return row?.n ?? 0;
|
|
1256
1283
|
}
|
|
1257
1284
|
const match = req.filter ? compileFilterToMongo(castFilterToSchema(req.filter, Model.schema), Model.schema) : {};
|
|
@@ -1260,10 +1287,7 @@ async function countAggGroups(Model, req, options = {}) {
|
|
|
1260
1287
|
if (Object.keys(match).length > 0) pipeline.push({ $match: match });
|
|
1261
1288
|
pipeline.push({ $limit: 1 });
|
|
1262
1289
|
pipeline.push({ $count: "n" });
|
|
1263
|
-
const
|
|
1264
|
-
if (session) aggregation.session(session);
|
|
1265
|
-
applyExecutionHints(aggregation, req.executionHints);
|
|
1266
|
-
const [row] = await aggregation.exec();
|
|
1290
|
+
const [row] = await runAgg(Model, pipeline, req, options);
|
|
1267
1291
|
return row?.n ?? 0;
|
|
1268
1292
|
}
|
|
1269
1293
|
const groupId = groupCols.reduce((acc, field) => {
|
|
@@ -1274,22 +1298,10 @@ async function countAggGroups(Model, req, options = {}) {
|
|
|
1274
1298
|
if (Object.keys(match).length > 0) pipeline.push({ $match: match });
|
|
1275
1299
|
pipeline.push({ $group: { _id: groupId } });
|
|
1276
1300
|
pipeline.push({ $count: "n" });
|
|
1277
|
-
const
|
|
1278
|
-
if (session) aggregation.session(session);
|
|
1279
|
-
const [row] = await aggregation.exec();
|
|
1301
|
+
const [row] = await runAgg(Model, pipeline, req, options);
|
|
1280
1302
|
return row?.n ?? 0;
|
|
1281
1303
|
}
|
|
1282
1304
|
//#endregion
|
|
1283
|
-
//#region src/actions/aggregate-ir/execute.ts
|
|
1284
|
-
async function executeAgg(Model, req, options = {}) {
|
|
1285
|
-
const session = options.session;
|
|
1286
|
-
const { pipeline } = buildAggPipeline(req, Model.schema);
|
|
1287
|
-
const aggregation = Model.aggregate(pipeline);
|
|
1288
|
-
if (session) aggregation.session(session);
|
|
1289
|
-
applyExecutionHints(aggregation, req.executionHints);
|
|
1290
|
-
return await aggregation.exec();
|
|
1291
|
-
}
|
|
1292
|
-
//#endregion
|
|
1293
1305
|
//#region src/actions/aggregate-ir/keyset.ts
|
|
1294
1306
|
/**
|
|
1295
1307
|
* Keyset (cursor) pagination helpers — mongokit binding.
|
|
@@ -1375,6 +1387,188 @@ function createMongoArchivePort(repo, filter, session) {
|
|
|
1375
1387
|
};
|
|
1376
1388
|
}
|
|
1377
1389
|
//#endregion
|
|
1390
|
+
//#region src/actions/bulk-upsert.ts
|
|
1391
|
+
/**
|
|
1392
|
+
* `bulkUpsert` execution: keyed `updateOne` + `upsert` sub-ops in ONE `client.bulkWrite` with
|
|
1393
|
+
* `verboseResults`, the only form that reports matched / modified / upserted PER OP (server 8.0,
|
|
1394
|
+
* driver ClientBulkWrite). Mongoose's unordered `Connection.bulkWrite` returns without rethrowing a
|
|
1395
|
+
* driver error (lib/connection.js, the `[res, error]` branch), so it is not used. Each row is cast
|
|
1396
|
+
* with `Model.castObject` (a bad value fails that row as `validation`) and schema timestamps are
|
|
1397
|
+
* stamped here, since the driver path runs no mongoose middleware.
|
|
1398
|
+
*/
|
|
1399
|
+
/** Split rows into sendable sub-ops and rows refused before sending (`invalid_row`). */
|
|
1400
|
+
function planUpserts(rows, key, routing) {
|
|
1401
|
+
const planned = [];
|
|
1402
|
+
const refused = [];
|
|
1403
|
+
rows.forEach((row, index) => {
|
|
1404
|
+
const keyVals = Object.fromEntries(key.map((k) => [k, row[k]]));
|
|
1405
|
+
const missingKey = key.some((k) => row[k] === void 0 || row[k] === null);
|
|
1406
|
+
const badInc = routing.inc.some((f) => row[f] !== void 0 && typeof row[f] !== "number");
|
|
1407
|
+
if (missingKey || badInc) {
|
|
1408
|
+
refused.push({
|
|
1409
|
+
index,
|
|
1410
|
+
key: keyVals,
|
|
1411
|
+
outcome: "failed",
|
|
1412
|
+
code: BULK_UPSERT_FAILURE_CODES.INVALID_ROW
|
|
1413
|
+
});
|
|
1414
|
+
return;
|
|
1415
|
+
}
|
|
1416
|
+
const $set = {};
|
|
1417
|
+
const $inc = {};
|
|
1418
|
+
const $setOnInsert = {};
|
|
1419
|
+
for (const [field, value] of Object.entries(row)) {
|
|
1420
|
+
if (key.includes(field) || value === void 0) continue;
|
|
1421
|
+
if (routing.inc.includes(field)) $inc[field] = value;
|
|
1422
|
+
else if (routing.setOnInsert.includes(field)) $setOnInsert[field] = value;
|
|
1423
|
+
else if (!routing.set || routing.set.includes(field)) $set[field] = value;
|
|
1424
|
+
}
|
|
1425
|
+
const update = {};
|
|
1426
|
+
if (Object.keys($set).length > 0) update.$set = $set;
|
|
1427
|
+
if (Object.keys($inc).length > 0) update.$inc = $inc;
|
|
1428
|
+
if (Object.keys($setOnInsert).length > 0) update.$setOnInsert = $setOnInsert;
|
|
1429
|
+
if (Object.keys(update).length === 0) update.$set = {};
|
|
1430
|
+
planned.push({
|
|
1431
|
+
index,
|
|
1432
|
+
key: keyVals,
|
|
1433
|
+
filter: { ...keyVals },
|
|
1434
|
+
update
|
|
1435
|
+
});
|
|
1436
|
+
});
|
|
1437
|
+
return {
|
|
1438
|
+
planned,
|
|
1439
|
+
refused
|
|
1440
|
+
};
|
|
1441
|
+
}
|
|
1442
|
+
/** Field sets a unique index covers; `_id` always. */
|
|
1443
|
+
function uniqueIndexes(model) {
|
|
1444
|
+
const out = [["_id"]];
|
|
1445
|
+
for (const [fields, options] of model.schema.indexes()) if (options.unique === true) out.push(Object.keys(fields));
|
|
1446
|
+
return out;
|
|
1447
|
+
}
|
|
1448
|
+
/** Is some unique index entirely pinned by the filter's equality fields (so it matches <= 1 row)? */
|
|
1449
|
+
function keyIsUnique(model, filterFields) {
|
|
1450
|
+
return uniqueIndexes(model).some((fields) => fields.every((f) => filterFields.includes(f)));
|
|
1451
|
+
}
|
|
1452
|
+
function timestampFields(model) {
|
|
1453
|
+
const ts = model.schema.get("timestamps");
|
|
1454
|
+
if (!ts) return {};
|
|
1455
|
+
if (ts === true) return {
|
|
1456
|
+
createdAt: "createdAt",
|
|
1457
|
+
updatedAt: "updatedAt"
|
|
1458
|
+
};
|
|
1459
|
+
const name = (v, d) => v === false ? void 0 : typeof v === "string" ? v : d;
|
|
1460
|
+
return {
|
|
1461
|
+
createdAt: name(ts.createdAt, "createdAt"),
|
|
1462
|
+
updatedAt: name(ts.updatedAt, "updatedAt")
|
|
1463
|
+
};
|
|
1464
|
+
}
|
|
1465
|
+
function isRecord(value) {
|
|
1466
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1467
|
+
}
|
|
1468
|
+
/** `Model.castObject`, narrowed to the plain object it returns for a plain object input. */
|
|
1469
|
+
function castRecord(model, fields) {
|
|
1470
|
+
const casted = model.castObject(fields);
|
|
1471
|
+
if (!isRecord(casted)) throw new TypeError("[mongokit] castObject returned a non-object");
|
|
1472
|
+
return casted;
|
|
1473
|
+
}
|
|
1474
|
+
/** A server before 8.0 has no `bulkWrite` command (CommandNotFound, 59): a closed refusal. */
|
|
1475
|
+
function throwIfUnsupported(err) {
|
|
1476
|
+
if (err instanceof mongoose.mongo.MongoServerError && err.code === 59) throw createError(501, "[mongokit] bulkUpsert needs MongoDB 8.0 (client bulkWrite with per-op results)", { code: BULK_UPSERT_ERROR_CODES.UNSUPPORTED });
|
|
1477
|
+
}
|
|
1478
|
+
function failureCode(code) {
|
|
1479
|
+
if (code === 11e3 || code === 11001) return BULK_UPSERT_FAILURE_CODES.DUPLICATE;
|
|
1480
|
+
if (code === 121) return BULK_UPSERT_FAILURE_CODES.VALIDATION;
|
|
1481
|
+
return BULK_UPSERT_FAILURE_CODES.WRITE_ERROR;
|
|
1482
|
+
}
|
|
1483
|
+
/** Cast, stamp, send in one command, and map every sub-op to its exact outcome. */
|
|
1484
|
+
async function executeUpserts(model, ops, options) {
|
|
1485
|
+
const results = [];
|
|
1486
|
+
const sendable = [];
|
|
1487
|
+
const namespace = `${model.db.name}.${model.collection.collectionName}`;
|
|
1488
|
+
const ts = timestampFields(model);
|
|
1489
|
+
const now = /* @__PURE__ */ new Date();
|
|
1490
|
+
for (const op of ops) try {
|
|
1491
|
+
const update = {};
|
|
1492
|
+
for (const [operator, fields] of Object.entries(op.update)) update[operator] = Object.keys(fields).length > 0 ? castRecord(model, fields) : {};
|
|
1493
|
+
if (ts.updatedAt) update.$set = {
|
|
1494
|
+
...update.$set,
|
|
1495
|
+
[ts.updatedAt]: now
|
|
1496
|
+
};
|
|
1497
|
+
if (ts.createdAt) update.$setOnInsert = {
|
|
1498
|
+
...update.$setOnInsert,
|
|
1499
|
+
[ts.createdAt]: now
|
|
1500
|
+
};
|
|
1501
|
+
sendable.push({
|
|
1502
|
+
...op,
|
|
1503
|
+
filter: castRecord(model, op.filter),
|
|
1504
|
+
update
|
|
1505
|
+
});
|
|
1506
|
+
} catch (err) {
|
|
1507
|
+
if (!(err instanceof mongoose.Error.ValidationError) && !(err instanceof mongoose.Error.CastError)) throw err;
|
|
1508
|
+
results.push({
|
|
1509
|
+
index: op.index,
|
|
1510
|
+
key: op.key,
|
|
1511
|
+
outcome: "failed",
|
|
1512
|
+
code: BULK_UPSERT_FAILURE_CODES.VALIDATION
|
|
1513
|
+
});
|
|
1514
|
+
}
|
|
1515
|
+
if (sendable.length === 0) return results;
|
|
1516
|
+
const models = sendable.map((op) => ({
|
|
1517
|
+
name: "updateOne",
|
|
1518
|
+
namespace,
|
|
1519
|
+
filter: op.filter,
|
|
1520
|
+
update: op.update,
|
|
1521
|
+
upsert: true
|
|
1522
|
+
}));
|
|
1523
|
+
let res;
|
|
1524
|
+
let writeErrors = /* @__PURE__ */ new Map();
|
|
1525
|
+
try {
|
|
1526
|
+
res = await model.db.getClient().bulkWrite(models, {
|
|
1527
|
+
ordered: options.ordered,
|
|
1528
|
+
verboseResults: true,
|
|
1529
|
+
...options.session ? { session: options.session } : {},
|
|
1530
|
+
...options.writeConcern ? { writeConcern: options.writeConcern } : {},
|
|
1531
|
+
...options.timeoutMS !== void 0 ? { timeoutMS: options.timeoutMS } : {}
|
|
1532
|
+
});
|
|
1533
|
+
} catch (err) {
|
|
1534
|
+
throwIfUnsupported(err);
|
|
1535
|
+
if (!(err instanceof mongoose.mongo.MongoClientBulkWriteError) || err.writeConcernErrors.length > 0) throw err;
|
|
1536
|
+
res = err.partialResult;
|
|
1537
|
+
writeErrors = err.writeErrors;
|
|
1538
|
+
}
|
|
1539
|
+
const updates = res?.updateResults ?? /* @__PURE__ */ new Map();
|
|
1540
|
+
sendable.forEach((op, i) => {
|
|
1541
|
+
const failed = writeErrors.get(i);
|
|
1542
|
+
if (failed) {
|
|
1543
|
+
results.push({
|
|
1544
|
+
index: op.index,
|
|
1545
|
+
key: op.key,
|
|
1546
|
+
outcome: "failed",
|
|
1547
|
+
code: failureCode(failed.code)
|
|
1548
|
+
});
|
|
1549
|
+
return;
|
|
1550
|
+
}
|
|
1551
|
+
const r = updates.get(i);
|
|
1552
|
+
if (!r) {
|
|
1553
|
+
if (!options.ordered) throw new Error(`[mongokit] bulkUpsert: the server reported no outcome for row ${op.index}`);
|
|
1554
|
+
results.push({
|
|
1555
|
+
index: op.index,
|
|
1556
|
+
key: op.key,
|
|
1557
|
+
outcome: "failed",
|
|
1558
|
+
code: BULK_UPSERT_FAILURE_CODES.NOT_ATTEMPTED
|
|
1559
|
+
});
|
|
1560
|
+
return;
|
|
1561
|
+
}
|
|
1562
|
+
const outcome = r.upsertedId !== void 0 && r.upsertedId !== null ? "inserted" : r.modifiedCount > 0 ? "updated" : "unchanged";
|
|
1563
|
+
results.push({
|
|
1564
|
+
index: op.index,
|
|
1565
|
+
key: op.key,
|
|
1566
|
+
outcome
|
|
1567
|
+
});
|
|
1568
|
+
});
|
|
1569
|
+
return results;
|
|
1570
|
+
}
|
|
1571
|
+
//#endregion
|
|
1378
1572
|
//#region src/query/AggregationBuilder.ts
|
|
1379
1573
|
/**
|
|
1380
1574
|
* Normalize SortSpec to MongoDB's strict format (1 | -1)
|
|
@@ -2094,7 +2288,7 @@ const HOOK_PRIORITY$1 = HOOK_PRIORITY;
|
|
|
2094
2288
|
*/
|
|
2095
2289
|
function downgradeCappedCount(strategy, where) {
|
|
2096
2290
|
if (strategy !== "capped") return strategy;
|
|
2097
|
-
warn(`[mongokit] countStrategy 'capped' is not supported by ${where} — counting exactly.
|
|
2291
|
+
warn(`[mongokit] countStrategy 'capped' is not supported by ${where} — counting exactly. Group counts are exact here: a bounded group count would need its own estimate flag. Use getAll()/aggregatePaginate() where a bounded count matters.`);
|
|
2098
2292
|
return "exact";
|
|
2099
2293
|
}
|
|
2100
2294
|
var Repository = class extends RepositoryBase {
|
|
@@ -2160,6 +2354,8 @@ var Repository = class extends RepositoryBase {
|
|
|
2160
2354
|
* {@link ID_ADDRESSED_OPERATIONS}.
|
|
2161
2355
|
*/
|
|
2162
2356
|
_guardIdAddressed = false;
|
|
2357
|
+
/** This repository's own `queryDefaults` / `aggregateDefaults` (deployment ones fill the gaps). */
|
|
2358
|
+
_queryDefaults;
|
|
2163
2359
|
constructor(Model, plugins = [], paginationConfig = {}, options = {}) {
|
|
2164
2360
|
super({
|
|
2165
2361
|
name: Model.modelName,
|
|
@@ -2173,6 +2369,11 @@ var Repository = class extends RepositoryBase {
|
|
|
2173
2369
|
warn(m);
|
|
2174
2370
|
});
|
|
2175
2371
|
this.Model = Model;
|
|
2372
|
+
markGoverned(Model);
|
|
2373
|
+
this._queryDefaults = {
|
|
2374
|
+
query: options.queryDefaults,
|
|
2375
|
+
aggregate: options.aggregateDefaults
|
|
2376
|
+
};
|
|
2176
2377
|
this._capabilitiesOverride = options.capabilities;
|
|
2177
2378
|
this.model = Model.modelName;
|
|
2178
2379
|
this._pagination = new PaginationEngine(Model, paginationConfig);
|
|
@@ -2417,6 +2618,31 @@ var Repository = class extends RepositoryBase {
|
|
|
2417
2618
|
* `_buildContext` inputs), so every op routed through `_runOp` — and the
|
|
2418
2619
|
* inline-try methods that call this directly — honors them uniformly.
|
|
2419
2620
|
*/
|
|
2621
|
+
/** One command's time bound and concerns: per call, then this repository, then the deployment. */
|
|
2622
|
+
_opts(kind, perCall) {
|
|
2623
|
+
return resolveQueryOptions(kind, perCall, this._queryDefaults);
|
|
2624
|
+
}
|
|
2625
|
+
/** How an IR request runs: per-call hints, then this repository's and the deployment's defaults. */
|
|
2626
|
+
_aggRun(req, context) {
|
|
2627
|
+
return {
|
|
2628
|
+
session: context.session,
|
|
2629
|
+
queryOptions: this._opts("aggregate", {
|
|
2630
|
+
...context,
|
|
2631
|
+
maxTimeMS: req.executionHints?.maxTimeMs ?? context.maxTimeMS,
|
|
2632
|
+
allowDiskUse: req.executionHints?.allowDiskUse ?? context.allowDiskUse
|
|
2633
|
+
}),
|
|
2634
|
+
finalize: (stages) => this._scopeJoins(stages, context)
|
|
2635
|
+
};
|
|
2636
|
+
}
|
|
2637
|
+
/** Scope every join in `pipeline` under its owning collection's policy (see `join-scope.ts`). */
|
|
2638
|
+
_scopeJoins(pipeline, context) {
|
|
2639
|
+
return scopeJoins(pipeline, {
|
|
2640
|
+
connection: this.Model.db,
|
|
2641
|
+
context,
|
|
2642
|
+
operation: context.operation,
|
|
2643
|
+
unscopedJoins: context.unscopedJoins
|
|
2644
|
+
});
|
|
2645
|
+
}
|
|
2420
2646
|
_withResilience(context, fn) {
|
|
2421
2647
|
const signal = context.signal;
|
|
2422
2648
|
const retryPolicy = context.retryPolicy;
|
|
@@ -2431,7 +2657,10 @@ var Repository = class extends RepositoryBase {
|
|
|
2431
2657
|
data,
|
|
2432
2658
|
...options
|
|
2433
2659
|
});
|
|
2434
|
-
return this._runOp("create", context, () => create(this.Model, context.data || data,
|
|
2660
|
+
return this._runOp("create", context, () => create(this.Model, context.data || data, {
|
|
2661
|
+
...options,
|
|
2662
|
+
queryOptions: this._opts("write", context)
|
|
2663
|
+
}));
|
|
2435
2664
|
}
|
|
2436
2665
|
/**
|
|
2437
2666
|
* Create multiple documents
|
|
@@ -2441,7 +2670,10 @@ var Repository = class extends RepositoryBase {
|
|
|
2441
2670
|
dataArray,
|
|
2442
2671
|
...options
|
|
2443
2672
|
});
|
|
2444
|
-
return this._runOp("createMany", context, () => createMany(this.Model, context.dataArray || dataArray,
|
|
2673
|
+
return this._runOp("createMany", context, () => createMany(this.Model, context.dataArray || dataArray, {
|
|
2674
|
+
...options,
|
|
2675
|
+
queryOptions: this._opts("write", context)
|
|
2676
|
+
}));
|
|
2445
2677
|
}
|
|
2446
2678
|
/**
|
|
2447
2679
|
* Get document by ID
|
|
@@ -2469,7 +2701,10 @@ var Repository = class extends RepositoryBase {
|
|
|
2469
2701
|
const result = await this._withResilience(context, () => getByQuery(this.Model, {
|
|
2470
2702
|
[effectiveIdField]: id,
|
|
2471
2703
|
...context.query || {}
|
|
2472
|
-
},
|
|
2704
|
+
}, {
|
|
2705
|
+
...context,
|
|
2706
|
+
queryOptions: this._opts("read", context)
|
|
2707
|
+
}));
|
|
2473
2708
|
await this._emitHook("after:getById", {
|
|
2474
2709
|
context,
|
|
2475
2710
|
result
|
|
@@ -2486,7 +2721,10 @@ var Repository = class extends RepositoryBase {
|
|
|
2486
2721
|
});
|
|
2487
2722
|
return null;
|
|
2488
2723
|
}
|
|
2489
|
-
const result = await this._withResilience(context, () => getById(this.Model, id,
|
|
2724
|
+
const result = await this._withResilience(context, () => getById(this.Model, id, {
|
|
2725
|
+
...context,
|
|
2726
|
+
queryOptions: this._opts("read", context)
|
|
2727
|
+
}));
|
|
2490
2728
|
if (!result && wantsThrow) throw createError(404, "Document not found");
|
|
2491
2729
|
await this._emitHook("after:getById", {
|
|
2492
2730
|
context,
|
|
@@ -2502,39 +2740,21 @@ var Repository = class extends RepositoryBase {
|
|
|
2502
2740
|
}
|
|
2503
2741
|
});
|
|
2504
2742
|
}
|
|
2505
|
-
/**
|
|
2506
|
-
* Batch point-read — the `$in` counterpart to {@link getById}. Fetches every
|
|
2507
|
-
* doc whose id (the repo's `idField`, default `_id`) is in `ids` in ONE query,
|
|
2508
|
-
* routed through `findAll` so tenant + soft-delete scoping and hooks apply
|
|
2509
|
-
* exactly as a single read would. Returns a Map keyed by the stringified id;
|
|
2510
|
-
* ids are de-duplicated and ids with no matching doc are simply absent (no
|
|
2511
|
-
* throw — mirrors `getById(..., { throwOnNotFound: false })`). Structurally
|
|
2512
|
-
* INVALID ids (wrong ObjectId shape, non-numeric for a Number id field, ...)
|
|
2513
|
-
* are dropped the same way — an id that cannot exist in the collection is
|
|
2514
|
-
* unambiguously a miss, and one malformed id must never poison the rest of
|
|
2515
|
-
* the batch. Lean by default (findAll semantics) — pass `{ lean: false }`
|
|
2516
|
-
* for hydrated docs.
|
|
2517
|
-
*
|
|
2518
|
-
* The N+1 killer for hosts that resolve many ids in one tick (order-line
|
|
2519
|
-
* snapshotting, availability matrices, dashboard row enrichment):
|
|
2520
|
-
*
|
|
2521
|
-
* @example
|
|
2522
|
-
* const byId = await repo.getByIds(offerIds, ctx);
|
|
2523
|
-
* const doc = byId.get(offerId); // O(1); undefined if not found
|
|
2524
|
-
*
|
|
2525
|
-
* Sizing: there is no library-imposed cap — the bounds are MongoDB's (the
|
|
2526
|
-
* `$in` array must fit the 16 MB query document; `$in` on an indexed field
|
|
2527
|
-
* is n point-lookups). Keep batches ≤ ~10k ids per call and chunk beyond
|
|
2528
|
-
* that. Don't exclude the id field via `select` — result keys derive from
|
|
2529
|
-
* it.
|
|
2530
|
-
*/
|
|
2531
2743
|
async getByIds(ids, options = {}) {
|
|
2744
|
+
const { chunkSize = DEFAULT_ID_CHUNK, concurrency = 4, preserveOrder, ...readOptions } = options;
|
|
2745
|
+
if (!Number.isInteger(concurrency) || concurrency < 1) throw new TypeError(`getByIds: concurrency must be a positive integer, got ${String(concurrency)}`);
|
|
2532
2746
|
const field = options.idField ?? this.idField;
|
|
2533
2747
|
const idType = getSchemaIdType(this.Model.schema, field);
|
|
2534
2748
|
const unique = [...new Set(ids.map((id) => String(id)))].filter((id) => isValidIdForType(id, idType));
|
|
2535
|
-
|
|
2536
|
-
const
|
|
2537
|
-
|
|
2749
|
+
const byId = /* @__PURE__ */ new Map();
|
|
2750
|
+
const chunks = [...idChunks(unique, chunkSize)];
|
|
2751
|
+
const load = async (chunk) => {
|
|
2752
|
+
const docs = await this.findAll({ [field]: { $in: chunk } }, readOptions);
|
|
2753
|
+
for (const d of docs) byId.set(String(d[field]), d);
|
|
2754
|
+
};
|
|
2755
|
+
if (readOptions.session) for (const chunk of chunks) await load(chunk);
|
|
2756
|
+
else for (let i = 0; i < chunks.length; i += concurrency) await Promise.all(chunks.slice(i, i + concurrency).map(load));
|
|
2757
|
+
return preserveOrder ? ids.map((id) => byId.get(String(id))) : byId;
|
|
2538
2758
|
}
|
|
2539
2759
|
/**
|
|
2540
2760
|
* Get single document by query
|
|
@@ -2556,7 +2776,10 @@ var Repository = class extends RepositoryBase {
|
|
|
2556
2776
|
return cachedResult;
|
|
2557
2777
|
}
|
|
2558
2778
|
const finalQuery = context.query || query;
|
|
2559
|
-
return this._runOp("getByQuery", context, () => getByQuery(this.Model, finalQuery,
|
|
2779
|
+
return this._runOp("getByQuery", context, () => getByQuery(this.Model, finalQuery, {
|
|
2780
|
+
...context,
|
|
2781
|
+
queryOptions: this._opts("read", context)
|
|
2782
|
+
}));
|
|
2560
2783
|
}
|
|
2561
2784
|
/**
|
|
2562
2785
|
* Get single document by arbitrary filter.
|
|
@@ -2585,7 +2808,10 @@ var Repository = class extends RepositoryBase {
|
|
|
2585
2808
|
}
|
|
2586
2809
|
const finalQuery = context.query || query;
|
|
2587
2810
|
try {
|
|
2588
|
-
const result = await this._withResilience(context, () => getByQuery(this.Model, finalQuery,
|
|
2811
|
+
const result = await this._withResilience(context, () => getByQuery(this.Model, finalQuery, {
|
|
2812
|
+
...context,
|
|
2813
|
+
queryOptions: this._opts("read", context)
|
|
2814
|
+
}));
|
|
2589
2815
|
await this._emitHook("after:getOne", {
|
|
2590
2816
|
context,
|
|
2591
2817
|
result
|
|
@@ -2623,13 +2849,68 @@ var Repository = class extends RepositoryBase {
|
|
|
2623
2849
|
if (options.populate || context.populate) query.populate(this._parsePopulate(context.populate || options.populate));
|
|
2624
2850
|
if (context.lean ?? options.lean ?? true) query.lean();
|
|
2625
2851
|
if (options.session) query.session(options.session);
|
|
2626
|
-
|
|
2852
|
+
applyToQuery(query, this._opts("read", context));
|
|
2627
2853
|
const limitSpec = context.limit ?? options.limit;
|
|
2628
2854
|
if (typeof limitSpec === "number" && limitSpec > 0) query.limit(limitSpec);
|
|
2629
2855
|
return await query.exec();
|
|
2630
2856
|
});
|
|
2631
2857
|
}
|
|
2632
2858
|
/**
|
|
2859
|
+
* The keyset cursor `getAll({ sort, filters, mode: 'keyset' })` would mint for `row`, under the
|
|
2860
|
+
* same policy scope, so a page resumes AFTER it. For composing keyset walks (repo-core
|
|
2861
|
+
* `mergeKeysetPages` via `keysetSource`), where the last CONSUMED row is not a page's last row.
|
|
2862
|
+
*/
|
|
2863
|
+
async keysetCursor(row, options) {
|
|
2864
|
+
if (typeof row !== "object" || row === null || !("_id" in row)) throw createError(400, "keysetCursor: the row must be a document carrying _id");
|
|
2865
|
+
const { sort, filters = {}, collation, ...rest } = options;
|
|
2866
|
+
const scoped = (await this._buildContext("getAll", {
|
|
2867
|
+
filters,
|
|
2868
|
+
sort,
|
|
2869
|
+
...rest
|
|
2870
|
+
})).filters ?? filters;
|
|
2871
|
+
const cfg = this._pagination.config;
|
|
2872
|
+
const normalized = validateKeysetSort(this._parseSort(sort), cfg.strictKeysetSortFields);
|
|
2873
|
+
return encodeCursor(Object.fromEntries(Object.entries(row)), getPrimaryField(normalized), normalized, cfg.cursorVersion, cursorScope(this.Model.collection.collectionName, scoped, collation), cfg.cursorSecret);
|
|
2874
|
+
}
|
|
2875
|
+
/**
|
|
2876
|
+
* Resumable keyset batches over `_id`: each batch is ONE bounded `find` (no long-lived server
|
|
2877
|
+
* cursor, so no cursor timeout and no CSOT lifetime budget), memory is one batch, and every
|
|
2878
|
+
* batch carries a `checkpoint`. Pass a checkpoint back as `after` to resume: rows written
|
|
2879
|
+
* behind it are not revisited, rows ahead of it are reached. A checkpoint is bound to the
|
|
2880
|
+
* collection, the post-policy filter (tenant included) and is refused elsewhere.
|
|
2881
|
+
*/
|
|
2882
|
+
async *iterate(filter = {}, options = {}) {
|
|
2883
|
+
const { after, batchSize = 500, select, ...rest } = options;
|
|
2884
|
+
if (!Number.isInteger(batchSize) || batchSize < 1) throw createError(400, `iterate: batchSize must be a positive integer, got ${String(batchSize)}`);
|
|
2885
|
+
const context = await this._buildContext("iterate", {
|
|
2886
|
+
query: filter,
|
|
2887
|
+
...rest
|
|
2888
|
+
});
|
|
2889
|
+
const scoped = context.query ?? filter;
|
|
2890
|
+
const sort = { _id: 1 };
|
|
2891
|
+
const scope = cursorScope(this.Model.collection.collectionName, scoped, void 0);
|
|
2892
|
+
const cfg = this._pagination.config;
|
|
2893
|
+
let position = after ? resolveCursorFilter(after, sort, cfg.cursorVersion, scoped, cfg.minCursorVersion, sort, scope, cfg.cursorSecret) : scoped;
|
|
2894
|
+
const opts = this._opts("read", context);
|
|
2895
|
+
for (;;) {
|
|
2896
|
+
const query = this.Model.find(position).sort(sort).limit(batchSize).lean();
|
|
2897
|
+
if (select) query.select(select);
|
|
2898
|
+
if (context.session) query.session(context.session);
|
|
2899
|
+
applyToQuery(query, opts);
|
|
2900
|
+
const docs = await this._withResilience(context, () => query.exec());
|
|
2901
|
+
if (docs.length === 0) return;
|
|
2902
|
+
const last = docs[docs.length - 1];
|
|
2903
|
+
if (last._id === void 0) throw createError(400, "iterate: the checkpoint needs _id; do not exclude it with select");
|
|
2904
|
+
const checkpoint = encodeCursor(last, "_id", sort, cfg.cursorVersion, scope, cfg.cursorSecret);
|
|
2905
|
+
yield {
|
|
2906
|
+
docs,
|
|
2907
|
+
checkpoint
|
|
2908
|
+
};
|
|
2909
|
+
if (docs.length < batchSize) return;
|
|
2910
|
+
position = resolveCursorFilter(checkpoint, sort, cfg.cursorVersion, scoped, cfg.minCursorVersion, sort, scope, cfg.cursorSecret);
|
|
2911
|
+
}
|
|
2912
|
+
}
|
|
2913
|
+
/**
|
|
2633
2914
|
* Streaming reads — async iterator over data, suitable for migrations,
|
|
2634
2915
|
* backfills, schema audits, and any once-a-quarter "touch every row"
|
|
2635
2916
|
* job. Goes through the standard `before:cursor` hook pipeline so
|
|
@@ -2670,7 +2951,9 @@ var Repository = class extends RepositoryBase {
|
|
|
2670
2951
|
if (options.batchSize) query.batchSize(options.batchSize);
|
|
2671
2952
|
if (context.lean ?? options.lean ?? true) query.lean();
|
|
2672
2953
|
if (options.session) query.session(options.session);
|
|
2673
|
-
|
|
2954
|
+
const cursorOpts = this._opts("read", context);
|
|
2955
|
+
applyToQuery(query, cursorOpts);
|
|
2956
|
+
applyCursorTimeoutMode(query, cursorOpts, this.Model.db);
|
|
2674
2957
|
const stream = query.cursor();
|
|
2675
2958
|
let yieldedCount = 0;
|
|
2676
2959
|
try {
|
|
@@ -2952,7 +3235,12 @@ var Repository = class extends RepositoryBase {
|
|
|
2952
3235
|
lean: context.lean ?? options.lean ?? true,
|
|
2953
3236
|
session: options.session,
|
|
2954
3237
|
hint: context.hint ?? params.hint,
|
|
2955
|
-
|
|
3238
|
+
queryOptions: this._opts("read", {
|
|
3239
|
+
...context,
|
|
3240
|
+
maxTimeMS: context.maxTimeMS ?? params.maxTimeMS,
|
|
3241
|
+
readPreference: context.readPreference ?? options.readPreference ?? params.readPreference,
|
|
3242
|
+
session: options.session
|
|
3243
|
+
}),
|
|
2956
3244
|
countLimit: context.countLimit ?? params.countLimit,
|
|
2957
3245
|
readPreference: context.readPreference ?? options.readPreference ?? params.readPreference,
|
|
2958
3246
|
collation: context.collation ?? params.collation
|
|
@@ -3034,7 +3322,10 @@ var Repository = class extends RepositoryBase {
|
|
|
3034
3322
|
query,
|
|
3035
3323
|
...options
|
|
3036
3324
|
});
|
|
3037
|
-
return this._runOp("count", context, () => count(this.Model, context.query || query,
|
|
3325
|
+
return this._runOp("count", context, () => count(this.Model, context.query || query, {
|
|
3326
|
+
session: options.session,
|
|
3327
|
+
queryOptions: this._opts("read", context)
|
|
3328
|
+
}));
|
|
3038
3329
|
}
|
|
3039
3330
|
/**
|
|
3040
3331
|
* Check if document exists
|
|
@@ -3045,7 +3336,10 @@ var Repository = class extends RepositoryBase {
|
|
|
3045
3336
|
query,
|
|
3046
3337
|
...options
|
|
3047
3338
|
});
|
|
3048
|
-
return this._runOp("exists", context, () => exists(this.Model, context.query || query,
|
|
3339
|
+
return this._runOp("exists", context, () => exists(this.Model, context.query || query, {
|
|
3340
|
+
session: options.session,
|
|
3341
|
+
queryOptions: this._opts("read", context)
|
|
3342
|
+
}));
|
|
3049
3343
|
}
|
|
3050
3344
|
/**
|
|
3051
3345
|
* Update a document by id and return the post-update doc.
|
|
@@ -3211,7 +3505,8 @@ var Repository = class extends RepositoryBase {
|
|
|
3211
3505
|
casUpdate,
|
|
3212
3506
|
{
|
|
3213
3507
|
...context,
|
|
3214
|
-
throwOnNotFound: false
|
|
3508
|
+
throwOnNotFound: false,
|
|
3509
|
+
queryOptions: this._opts("findAndModify", context)
|
|
3215
3510
|
}
|
|
3216
3511
|
));
|
|
3217
3512
|
if (!result) {
|
|
@@ -3235,8 +3530,14 @@ var Repository = class extends RepositoryBase {
|
|
|
3235
3530
|
} else if (effectiveIdField !== "_id") result = await this._withResilience(context, () => updateByQuery(this.Model, {
|
|
3236
3531
|
[effectiveIdField]: id,
|
|
3237
3532
|
...context.query || {}
|
|
3238
|
-
}, context.data || data,
|
|
3239
|
-
|
|
3533
|
+
}, context.data || data, {
|
|
3534
|
+
...context,
|
|
3535
|
+
queryOptions: this._opts("findAndModify", context)
|
|
3536
|
+
}));
|
|
3537
|
+
else result = await this._withResilience(context, () => update(this.Model, id, context.data || data, {
|
|
3538
|
+
...context,
|
|
3539
|
+
queryOptions: this._opts("findAndModify", context)
|
|
3540
|
+
}));
|
|
3240
3541
|
if (!result && wantsThrow) throw createError(404, "Document not found");
|
|
3241
3542
|
await this._emitHook("after:update", {
|
|
3242
3543
|
context,
|
|
@@ -3296,7 +3597,8 @@ var Repository = class extends RepositoryBase {
|
|
|
3296
3597
|
sort: options.sort,
|
|
3297
3598
|
returnDocument: options.returnDocument ?? "after",
|
|
3298
3599
|
upsert: options.upsert ?? false,
|
|
3299
|
-
session: options.session
|
|
3600
|
+
session: options.session,
|
|
3601
|
+
queryOptions: this._opts("findAndModify", context)
|
|
3300
3602
|
});
|
|
3301
3603
|
});
|
|
3302
3604
|
}
|
|
@@ -3525,7 +3827,8 @@ var Repository = class extends RepositoryBase {
|
|
|
3525
3827
|
return await findOneAndUpdate(this.Model, finalQuery, finalUpdate, {
|
|
3526
3828
|
returnDocument: "after",
|
|
3527
3829
|
upsert: options.upsert ?? false,
|
|
3528
|
-
session: options.session
|
|
3830
|
+
session: options.session,
|
|
3831
|
+
queryOptions: this._opts("findAndModify", context)
|
|
3529
3832
|
});
|
|
3530
3833
|
});
|
|
3531
3834
|
}
|
|
@@ -3758,7 +4061,8 @@ var Repository = class extends RepositoryBase {
|
|
|
3758
4061
|
return await findOneAndUpdate(this.Model, finalQuery, finalUpdate, {
|
|
3759
4062
|
returnDocument: "after",
|
|
3760
4063
|
upsert: false,
|
|
3761
|
-
session: options.session
|
|
4064
|
+
session: options.session,
|
|
4065
|
+
queryOptions: this._opts("findAndModify", context)
|
|
3762
4066
|
});
|
|
3763
4067
|
});
|
|
3764
4068
|
}
|
|
@@ -3820,9 +4124,13 @@ var Repository = class extends RepositoryBase {
|
|
|
3820
4124
|
[effectiveIdField]: id,
|
|
3821
4125
|
...context.query || {}
|
|
3822
4126
|
} : void 0;
|
|
3823
|
-
const result = await this._withResilience(context, () => deleteQuery ? deleteByQuery(this.Model, deleteQuery, {
|
|
4127
|
+
const result = await this._withResilience(context, () => deleteQuery ? deleteByQuery(this.Model, deleteQuery, {
|
|
3824
4128
|
session: options.session,
|
|
3825
|
-
|
|
4129
|
+
queryOptions: this._opts("findAndModify", context)
|
|
4130
|
+
}) : deleteById(this.Model, id, {
|
|
4131
|
+
session: options.session,
|
|
4132
|
+
query: context.query,
|
|
4133
|
+
queryOptions: this._opts("findAndModify", context)
|
|
3826
4134
|
}));
|
|
3827
4135
|
if (!result && wantsThrow) throw createError(404, "Document not found");
|
|
3828
4136
|
await this._emitHook("after:delete", {
|
|
@@ -3870,11 +4178,13 @@ var Repository = class extends RepositoryBase {
|
|
|
3870
4178
|
if (!finalQuery || Object.keys(finalQuery).length === 0) throw createError(400, "updateMany requires a non-empty query filter. Pass an explicit filter to prevent accidental mass updates.");
|
|
3871
4179
|
if (Array.isArray(normalizedData) && options.updatePipeline !== true) throw createError(400, "Update pipelines (array updates) are disabled by default; pass `{ updatePipeline: true }` to explicitly allow pipeline-style updates.");
|
|
3872
4180
|
const finalData = context.data || normalizedData;
|
|
3873
|
-
|
|
4181
|
+
const updateQuery = this.Model.updateMany(finalQuery, finalData, {
|
|
3874
4182
|
runValidators: true,
|
|
3875
4183
|
session: options.session,
|
|
3876
4184
|
...options.updatePipeline !== void 0 ? { updatePipeline: options.updatePipeline } : {}
|
|
3877
|
-
})
|
|
4185
|
+
});
|
|
4186
|
+
applyToQuery(updateQuery, this._opts("write", context));
|
|
4187
|
+
return await updateQuery.exec();
|
|
3878
4188
|
});
|
|
3879
4189
|
}
|
|
3880
4190
|
/**
|
|
@@ -3916,7 +4226,11 @@ var Repository = class extends RepositoryBase {
|
|
|
3916
4226
|
}
|
|
3917
4227
|
const finalQuery = context.query || filter;
|
|
3918
4228
|
if (!finalQuery || Object.keys(finalQuery).length === 0) throw createError(400, "deleteMany requires a non-empty query filter after policy hooks.");
|
|
3919
|
-
const result = await this._withResilience(context, () =>
|
|
4229
|
+
const result = await this._withResilience(context, () => (() => {
|
|
4230
|
+
const q = this.Model.deleteMany(finalQuery, { session: options.session });
|
|
4231
|
+
applyToQuery(q, this._opts("write", context));
|
|
4232
|
+
return q.exec();
|
|
4233
|
+
})());
|
|
3920
4234
|
await this._emitHook("after:deleteMany", {
|
|
3921
4235
|
context,
|
|
3922
4236
|
result
|
|
@@ -3979,6 +4293,70 @@ var Repository = class extends RepositoryBase {
|
|
|
3979
4293
|
return runChunkedArchive(options, sink, port);
|
|
3980
4294
|
}
|
|
3981
4295
|
/**
|
|
4296
|
+
* Many keyed upserts in ONE command with an exact per-row outcome (repo-core `bulkUpsert` IR).
|
|
4297
|
+
* Sub-ops pass through the `before:bulkUpsert` policy hooks (tenant, soft-delete) like bulkWrite
|
|
4298
|
+
* sub-ops. Refused unless a unique index is pinned by the key plus the policy scope. Needs
|
|
4299
|
+
* MongoDB 8.0 (client bulkWrite with verbose results).
|
|
4300
|
+
*/
|
|
4301
|
+
async bulkUpsert(rows, options) {
|
|
4302
|
+
const invalid = bulkUpsertOptionsError(options);
|
|
4303
|
+
if (invalid) throw createError(400, `[mongokit] bulkUpsert: ${invalid}`, { code: BULK_UPSERT_ERROR_CODES.INVALID_OPTIONS });
|
|
4304
|
+
const { key, set, inc = [], setOnInsert = [], ordered = false, ...rest } = options;
|
|
4305
|
+
const { planned, refused } = planUpserts(rows, key, {
|
|
4306
|
+
set,
|
|
4307
|
+
inc,
|
|
4308
|
+
setOnInsert
|
|
4309
|
+
});
|
|
4310
|
+
const operations = planned.map((p) => ({ updateOne: {
|
|
4311
|
+
filter: p.filter,
|
|
4312
|
+
update: p.update,
|
|
4313
|
+
upsert: true
|
|
4314
|
+
} }));
|
|
4315
|
+
const context = await this._buildContext("bulkUpsert", {
|
|
4316
|
+
...rest,
|
|
4317
|
+
operations
|
|
4318
|
+
});
|
|
4319
|
+
const scoped = context.operations ?? operations;
|
|
4320
|
+
const filterFields = Object.keys(scoped[0]?.updateOne.filter ?? Object.fromEntries(key.map((k) => [k, 1])));
|
|
4321
|
+
if (!keyIsUnique(this.Model, filterFields)) throw createError(400, `[mongokit] bulkUpsert on '${this.model}': no unique index is pinned by (${filterFields.join(", ")}), so one key could match several rows`, {
|
|
4322
|
+
code: BULK_UPSERT_ERROR_CODES.KEY_NOT_UNIQUE,
|
|
4323
|
+
meta: {
|
|
4324
|
+
key,
|
|
4325
|
+
filterFields
|
|
4326
|
+
}
|
|
4327
|
+
});
|
|
4328
|
+
planned.forEach((p, i) => {
|
|
4329
|
+
const filter = scoped[i]?.updateOne.filter ?? p.filter;
|
|
4330
|
+
for (const field of Object.keys(filter)) {
|
|
4331
|
+
if (key.includes(field)) continue;
|
|
4332
|
+
for (const fields of Object.values(p.update)) {
|
|
4333
|
+
if (!(field in fields)) continue;
|
|
4334
|
+
if (String(fields[field]) !== String(filter[field])) throw createError(400, `[mongokit] bulkUpsert: '${field}' in row ${p.index} does not match the resolved tenant scope`, {
|
|
4335
|
+
code: BULK_UPSERT_ERROR_CODES.INVALID_OPTIONS,
|
|
4336
|
+
meta: {
|
|
4337
|
+
field,
|
|
4338
|
+
index: p.index
|
|
4339
|
+
}
|
|
4340
|
+
});
|
|
4341
|
+
delete fields[field];
|
|
4342
|
+
}
|
|
4343
|
+
}
|
|
4344
|
+
});
|
|
4345
|
+
return this._runOp("bulkUpsert", context, async () => {
|
|
4346
|
+
const writeOpts = this._opts("write", context);
|
|
4347
|
+
const sent = await executeUpserts(this.Model, planned.map((p, i) => ({
|
|
4348
|
+
...p,
|
|
4349
|
+
filter: scoped[i]?.updateOne.filter ?? p.filter
|
|
4350
|
+
})), {
|
|
4351
|
+
ordered,
|
|
4352
|
+
session: context.session,
|
|
4353
|
+
writeConcern: writeOpts.writeConcern,
|
|
4354
|
+
timeoutMS: writeOpts.maxTimeMS
|
|
4355
|
+
});
|
|
4356
|
+
return summarizeBulkUpsert([...refused, ...sent].sort((a, b) => a.index - b.index));
|
|
4357
|
+
});
|
|
4358
|
+
}
|
|
4359
|
+
/**
|
|
3982
4360
|
* Kit-native aggregation pipeline — takes a MongoDB stage array and
|
|
3983
4361
|
* returns the raw pipeline output. Use this for `$lookup`, `$unwind`,
|
|
3984
4362
|
* `$facet`, `$graphLookup`, and any other mongo-specific power
|
|
@@ -4012,15 +4390,10 @@ var Repository = class extends RepositoryBase {
|
|
|
4012
4390
|
const maxStages = options.maxPipelineStages;
|
|
4013
4391
|
if (maxStages && pipeline.length > maxStages) throw createError(400, `Aggregation pipeline exceeds maximum allowed stages (${pipeline.length} > ${maxStages})`);
|
|
4014
4392
|
return this._runOp("aggregatePipeline", context, async () => {
|
|
4015
|
-
const
|
|
4016
|
-
|
|
4017
|
-
const aggregation = this.Model.aggregate(finalPipeline);
|
|
4393
|
+
const scoped = context.query && Object.keys(context.query).length > 0 ? [{ $match: context.query }, ...pipeline] : pipeline;
|
|
4394
|
+
const aggregation = this.Model.aggregate(this._scopeJoins(scoped, context));
|
|
4018
4395
|
if (options.session) aggregation.session(options.session);
|
|
4019
|
-
|
|
4020
|
-
if (options.readPreference) aggregation.read(options.readPreference);
|
|
4021
|
-
if (options.maxTimeMS) aggregation.option({ maxTimeMS: options.maxTimeMS });
|
|
4022
|
-
if (options.comment) aggregation.option({ comment: options.comment });
|
|
4023
|
-
if (options.readConcern) aggregation.option({ readConcern: options.readConcern });
|
|
4396
|
+
applyToAggregate(aggregation, this._opts("aggregate", context));
|
|
4024
4397
|
if (options.collation) aggregation.collation(options.collation);
|
|
4025
4398
|
return await aggregation.exec();
|
|
4026
4399
|
});
|
|
@@ -4043,7 +4416,8 @@ var Repository = class extends RepositoryBase {
|
|
|
4043
4416
|
if (context.filters && Object.keys(context.filters).length > 0) finalPipeline.unshift({ $match: context.filters });
|
|
4044
4417
|
const aggOptions = {
|
|
4045
4418
|
...context,
|
|
4046
|
-
pipeline: finalPipeline
|
|
4419
|
+
pipeline: this._scopeJoins(finalPipeline, context),
|
|
4420
|
+
queryOptions: this._opts("aggregate", context)
|
|
4047
4421
|
};
|
|
4048
4422
|
return this._runOp("aggregatePipelinePaginate", context, () => this._pagination.aggregatePaginate(aggOptions));
|
|
4049
4423
|
}
|
|
@@ -4074,7 +4448,7 @@ var Repository = class extends RepositoryBase {
|
|
|
4074
4448
|
aggRequest: req,
|
|
4075
4449
|
...options
|
|
4076
4450
|
});
|
|
4077
|
-
const cached = this._cachedValue(context);
|
|
4451
|
+
const cached = options.explain ? void 0 : this._cachedValue(context);
|
|
4078
4452
|
if (cached !== void 0) {
|
|
4079
4453
|
await this._emitHook("after:aggregate", {
|
|
4080
4454
|
context,
|
|
@@ -4085,7 +4459,14 @@ var Repository = class extends RepositoryBase {
|
|
|
4085
4459
|
}
|
|
4086
4460
|
return this._runOp("aggregate", context, async () => {
|
|
4087
4461
|
const finalReq = this._injectPolicyScopeIntoAgg(req, context);
|
|
4088
|
-
|
|
4462
|
+
const run = this._aggRun(finalReq, context);
|
|
4463
|
+
const rows = await executeAgg(this.Model, finalReq, run);
|
|
4464
|
+
if (!options.explain) return { rows };
|
|
4465
|
+
const { pipeline } = buildAggPipeline(finalReq, this.Model.schema);
|
|
4466
|
+
return {
|
|
4467
|
+
rows,
|
|
4468
|
+
plan: await prepareAgg(this.Model, pipeline, finalReq, run).explain(options.explain)
|
|
4469
|
+
};
|
|
4089
4470
|
});
|
|
4090
4471
|
}
|
|
4091
4472
|
/**
|
|
@@ -4132,7 +4513,7 @@ var Repository = class extends RepositoryBase {
|
|
|
4132
4513
|
...options
|
|
4133
4514
|
});
|
|
4134
4515
|
const limit = Math.max(1, Math.min(req.limit ?? 20, 1e3));
|
|
4135
|
-
|
|
4516
|
+
context.session;
|
|
4136
4517
|
const useKeyset = isKeysetMode(req);
|
|
4137
4518
|
const cachedEnvelope = this._cachedValue(context);
|
|
4138
4519
|
if (cachedEnvelope !== void 0) {
|
|
@@ -4145,8 +4526,12 @@ var Repository = class extends RepositoryBase {
|
|
|
4145
4526
|
}
|
|
4146
4527
|
return this._composeMiddleware("aggregatePaginate", context, async () => {
|
|
4147
4528
|
try {
|
|
4148
|
-
const
|
|
4149
|
-
const
|
|
4529
|
+
const scopedReq = this._injectPolicyScopeIntoAgg(req, context);
|
|
4530
|
+
const finalReq = scopedReq.sort ? {
|
|
4531
|
+
...scopedReq,
|
|
4532
|
+
sort: withGroupTiebreak(scopedReq.sort, normalizeGroupBy(scopedReq.groupBy))
|
|
4533
|
+
} : scopedReq;
|
|
4534
|
+
const result = await this._executeAggregatePaginate(finalReq, useKeyset, limit, this._aggRun(finalReq, context));
|
|
4150
4535
|
await this._emitHook("after:aggregatePaginate", {
|
|
4151
4536
|
context,
|
|
4152
4537
|
result
|
|
@@ -4170,7 +4555,7 @@ var Repository = class extends RepositoryBase {
|
|
|
4170
4555
|
* built by the outer wrapper matches the request the executor
|
|
4171
4556
|
* sees — no cross-tenant cache poisoning.
|
|
4172
4557
|
*/
|
|
4173
|
-
async _executeAggregatePaginate(finalReq, useKeyset, limit,
|
|
4558
|
+
async _executeAggregatePaginate(finalReq, useKeyset, limit, run) {
|
|
4174
4559
|
if (useKeyset) {
|
|
4175
4560
|
if (!finalReq.sort || Object.keys(finalReq.sort).length === 0) throw new Error("mongokit/aggregatePaginate: keyset pagination requires `sort` — the cursor anchors on the sort-key tuple");
|
|
4176
4561
|
const cursor = finalReq.after ? decodeAggCursor$1(finalReq.after, this._pagination.config.cursorSecret) : void 0;
|
|
@@ -4181,10 +4566,7 @@ var Repository = class extends RepositoryBase {
|
|
|
4181
4566
|
if (keysetMatch) tailStages.push(keysetMatch);
|
|
4182
4567
|
tailStages.push({ $sort: finalReq.sort });
|
|
4183
4568
|
tailStages.push({ $limit: limit + 1 });
|
|
4184
|
-
const
|
|
4185
|
-
if (session) aggregation.session(session);
|
|
4186
|
-
applyExecutionHints(aggregation, finalReq.executionHints);
|
|
4187
|
-
const peeked = await aggregation.exec();
|
|
4569
|
+
const peeked = await runAgg(this.Model, [...headStages, ...tailStages], finalReq, run);
|
|
4188
4570
|
const hasMore = peeked.length > limit;
|
|
4189
4571
|
const data = hasMore ? peeked.slice(0, limit) : peeked;
|
|
4190
4572
|
return {
|
|
@@ -4198,12 +4580,13 @@ var Repository = class extends RepositoryBase {
|
|
|
4198
4580
|
const page = Math.max(1, finalReq.page ?? 1);
|
|
4199
4581
|
const countStrategy = downgradeCappedCount(finalReq.countStrategy ?? this._pagination.config.defaultCountStrategy, "aggregatePaginateIr");
|
|
4200
4582
|
const offset = (page - 1) * limit;
|
|
4583
|
+
assertOffsetWithinCap(offset, this._pagination.config.maxOffset);
|
|
4201
4584
|
if (countStrategy === "none") {
|
|
4202
4585
|
const peek = await executeAgg(this.Model, {
|
|
4203
4586
|
...finalReq,
|
|
4204
4587
|
limit: limit + 1,
|
|
4205
4588
|
offset
|
|
4206
|
-
},
|
|
4589
|
+
}, run);
|
|
4207
4590
|
const hasNext = peek.length > limit;
|
|
4208
4591
|
return {
|
|
4209
4592
|
method: "offset",
|
|
@@ -4220,7 +4603,7 @@ var Repository = class extends RepositoryBase {
|
|
|
4220
4603
|
...finalReq,
|
|
4221
4604
|
limit,
|
|
4222
4605
|
offset
|
|
4223
|
-
},
|
|
4606
|
+
}, run), countAggGroups(this.Model, finalReq, run)]);
|
|
4224
4607
|
return {
|
|
4225
4608
|
method: "offset",
|
|
4226
4609
|
data,
|
|
@@ -4278,10 +4661,9 @@ var Repository = class extends RepositoryBase {
|
|
|
4278
4661
|
});
|
|
4279
4662
|
return this._runOp("distinct", context, () => {
|
|
4280
4663
|
const finalQuery = context.query || query;
|
|
4281
|
-
const readPreference = context.readPreference ?? options.readPreference;
|
|
4282
4664
|
return distinct(this.Model, field, finalQuery, {
|
|
4283
4665
|
session: options.session,
|
|
4284
|
-
|
|
4666
|
+
queryOptions: this._opts("read", context)
|
|
4285
4667
|
});
|
|
4286
4668
|
});
|
|
4287
4669
|
}
|
|
@@ -4319,24 +4701,24 @@ var Repository = class extends RepositoryBase {
|
|
|
4319
4701
|
const filters = context.filters ?? options.filters;
|
|
4320
4702
|
const sort = context.sort ?? options.sort;
|
|
4321
4703
|
const limit = context.limit ?? options.limit ?? this._pagination.config.defaultLimit ?? 20;
|
|
4322
|
-
const readPref = context.readPreference ?? options.readPreference;
|
|
4323
4704
|
const session = context.session ?? options.session;
|
|
4324
4705
|
const collation = context.collation ?? options.collation;
|
|
4325
4706
|
const after = context.after ?? options.after;
|
|
4326
4707
|
const pageFromContext = context.page ?? options.page;
|
|
4327
4708
|
const isKeyset = !!after || !pageFromContext && !!sort;
|
|
4328
|
-
const countStrategy =
|
|
4709
|
+
const countStrategy = context.countStrategy ?? options.countStrategy ?? this._pagination.config.defaultCountStrategy;
|
|
4329
4710
|
const projection = buildLookupProjection(context.select ?? options.select, lookups);
|
|
4330
4711
|
const appendLookupStages$2 = (pipeline) => appendLookupStages(pipeline, lookups, projection, isKeyset && sort ? this._parseSort(sort) : void 0);
|
|
4331
4712
|
if (isKeyset && sort) {
|
|
4332
4713
|
const parsedSort = this._parseSort(sort);
|
|
4333
|
-
const { validateKeysetSort } = await import("./PaginationEngine-
|
|
4334
|
-
const { encodeCursor, resolveCursorFilter } = await import("./PaginationEngine-
|
|
4335
|
-
const { getPrimaryField } = await import("./PaginationEngine-
|
|
4714
|
+
const { validateKeysetSort } = await import("./PaginationEngine-CfT2Cxkj.mjs").then((n) => n.r);
|
|
4715
|
+
const { encodeCursor, resolveCursorFilter } = await import("./PaginationEngine-CfT2Cxkj.mjs").then((n) => n.a);
|
|
4716
|
+
const { getPrimaryField } = await import("./PaginationEngine-CfT2Cxkj.mjs").then((n) => n.r);
|
|
4336
4717
|
const normalizedSort = validateKeysetSort(parsedSort, this._pagination.config.strictKeysetSortFields);
|
|
4337
4718
|
const cursorVersion = this._pagination.config.cursorVersion ?? 1;
|
|
4338
4719
|
const minCursorVersion = this._pagination.config.minCursorVersion ?? 1;
|
|
4339
|
-
const
|
|
4720
|
+
const scope = cursorScope(this.Model.collection.collectionName, filters || {}, collation);
|
|
4721
|
+
const matchFilters = after ? resolveCursorFilter(after, normalizedSort, cursorVersion, { ...filters || {} }, minCursorVersion, normalizedSort, scope, this._pagination.config.cursorSecret) : { ...filters || {} };
|
|
4340
4722
|
if (projection) {
|
|
4341
4723
|
if (Object.values(projection).some((v) => v === 1)) {
|
|
4342
4724
|
for (const sortField of Object.keys(normalizedSort)) if (!(sortField in projection)) projection[sortField] = 1;
|
|
@@ -4347,9 +4729,9 @@ var Repository = class extends RepositoryBase {
|
|
|
4347
4729
|
pipeline.push({ $sort: normalizedSort });
|
|
4348
4730
|
pipeline.push({ $limit: limit + 1 });
|
|
4349
4731
|
appendLookupStages$2(pipeline);
|
|
4350
|
-
const aggregation = this.Model.aggregate(pipeline).session(session || null);
|
|
4732
|
+
const aggregation = this.Model.aggregate(this._scopeJoins(pipeline, context)).session(session || null);
|
|
4351
4733
|
if (collation) aggregation.collation(collation);
|
|
4352
|
-
|
|
4734
|
+
applyToAggregate(aggregation, this._opts("aggregate", context));
|
|
4353
4735
|
const data = await aggregation;
|
|
4354
4736
|
const hasMore = data.length > limit;
|
|
4355
4737
|
if (hasMore) data.pop();
|
|
@@ -4359,7 +4741,7 @@ var Repository = class extends RepositoryBase {
|
|
|
4359
4741
|
data,
|
|
4360
4742
|
limit,
|
|
4361
4743
|
hasMore,
|
|
4362
|
-
next: hasMore && data.length > 0 ? encodeCursor(data[data.length - 1], primaryField, normalizedSort, cursorVersion, this._pagination.config.cursorSecret) : null
|
|
4744
|
+
next: hasMore && data.length > 0 ? encodeCursor(data[data.length - 1], primaryField, normalizedSort, cursorVersion, scope, this._pagination.config.cursorSecret) : null
|
|
4363
4745
|
};
|
|
4364
4746
|
await this._emitHook("after:lookupPopulate", {
|
|
4365
4747
|
context,
|
|
@@ -4369,61 +4751,69 @@ var Repository = class extends RepositoryBase {
|
|
|
4369
4751
|
}
|
|
4370
4752
|
const page = pageFromContext ?? 1;
|
|
4371
4753
|
const skip = (page - 1) * limit;
|
|
4372
|
-
|
|
4373
|
-
const
|
|
4374
|
-
|
|
4375
|
-
if (sort) dataPipeline.push({ $sort: this._parseSort(sort) });
|
|
4376
|
-
|
|
4377
|
-
|
|
4378
|
-
|
|
4379
|
-
const aggregation = this.Model.aggregate(
|
|
4754
|
+
assertOffsetWithinCap(skip, this._pagination.config.maxOffset);
|
|
4755
|
+
const match = filters && Object.keys(filters).length > 0 ? [{ $match: filters }] : [];
|
|
4756
|
+
const dataPipeline = [...match];
|
|
4757
|
+
if (sort) dataPipeline.push({ $sort: withIdTiebreak(this._parseSort(sort)) });
|
|
4758
|
+
dataPipeline.push({ $skip: skip }, { $limit: limit + 1 });
|
|
4759
|
+
appendLookupStages$2(dataPipeline);
|
|
4760
|
+
const run = async (stages) => {
|
|
4761
|
+
const aggregation = this.Model.aggregate(this._scopeJoins(stages, context)).session(session || null);
|
|
4380
4762
|
if (collation) aggregation.collation(collation);
|
|
4381
|
-
|
|
4382
|
-
|
|
4383
|
-
|
|
4384
|
-
|
|
4385
|
-
|
|
4386
|
-
|
|
4387
|
-
|
|
4388
|
-
|
|
4389
|
-
limit,
|
|
4763
|
+
applyToAggregate(aggregation, this._opts("aggregate", context));
|
|
4764
|
+
return aggregation.exec();
|
|
4765
|
+
};
|
|
4766
|
+
const ceiling = context.countLimit ?? options.countLimit ?? this._pagination.config.defaultCountLimit;
|
|
4767
|
+
const bounded = countStrategy === "capped" || countStrategy === "cached";
|
|
4768
|
+
const countOnce = async () => {
|
|
4769
|
+
return (await run([
|
|
4770
|
+
...match,
|
|
4771
|
+
...bounded ? [{ $limit: ceiling }] : [],
|
|
4772
|
+
{ $count: "total" }
|
|
4773
|
+
]))[0]?.total ?? 0;
|
|
4774
|
+
};
|
|
4775
|
+
const runCount = async () => {
|
|
4776
|
+
if (countStrategy === "none") return {
|
|
4390
4777
|
total: 0,
|
|
4391
|
-
|
|
4392
|
-
|
|
4393
|
-
|
|
4778
|
+
totalIsEstimate: true,
|
|
4779
|
+
countedAt: null
|
|
4780
|
+
};
|
|
4781
|
+
if (countStrategy === "cached" && !session) {
|
|
4782
|
+
const key = cursorScope(this.Model.collection.collectionName, [
|
|
4783
|
+
filters ?? {},
|
|
4784
|
+
ceiling,
|
|
4785
|
+
"lookupPopulate"
|
|
4786
|
+
], collation);
|
|
4787
|
+
return {
|
|
4788
|
+
...await this._pagination._countMemo().get({
|
|
4789
|
+
key,
|
|
4790
|
+
run: countOnce
|
|
4791
|
+
}),
|
|
4792
|
+
totalIsEstimate: true
|
|
4793
|
+
};
|
|
4794
|
+
}
|
|
4795
|
+
const total = await countOnce();
|
|
4796
|
+
return {
|
|
4797
|
+
total,
|
|
4798
|
+
totalIsEstimate: bounded && total >= ceiling,
|
|
4799
|
+
countedAt: /* @__PURE__ */ new Date()
|
|
4394
4800
|
};
|
|
4395
|
-
await this._emitHook("after:lookupPopulate", {
|
|
4396
|
-
context,
|
|
4397
|
-
result
|
|
4398
|
-
});
|
|
4399
|
-
return result;
|
|
4400
|
-
}
|
|
4401
|
-
dataPipeline.push({ $skip: skip }, { $limit: limit });
|
|
4402
|
-
appendLookupStages$2(dataPipeline);
|
|
4403
|
-
const countPipeline = [];
|
|
4404
|
-
if (filters && Object.keys(filters).length > 0) countPipeline.push({ $match: filters });
|
|
4405
|
-
countPipeline.push({ $count: "total" });
|
|
4406
|
-
const pipeline = [{ $facet: {
|
|
4407
|
-
metadata: countPipeline,
|
|
4408
|
-
data: dataPipeline
|
|
4409
|
-
} }];
|
|
4410
|
-
const aggregation = this.Model.aggregate(pipeline).session(session || null);
|
|
4411
|
-
if (collation) aggregation.collation(collation);
|
|
4412
|
-
if (readPref) aggregation.read(readPref);
|
|
4413
|
-
const facetResult = (await aggregation)[0] || {
|
|
4414
|
-
metadata: [],
|
|
4415
|
-
data: []
|
|
4416
4801
|
};
|
|
4417
|
-
const
|
|
4802
|
+
const [rows, counted] = session ? [await run(dataPipeline), await runCount()] : await Promise.all([run(dataPipeline), runCount()]);
|
|
4803
|
+
const data = rows;
|
|
4804
|
+
const hasNext = data.length > limit;
|
|
4805
|
+
if (hasNext) data.pop();
|
|
4418
4806
|
const result = {
|
|
4419
4807
|
method: "offset",
|
|
4420
|
-
data
|
|
4808
|
+
data,
|
|
4421
4809
|
page,
|
|
4422
4810
|
limit,
|
|
4423
|
-
total,
|
|
4424
|
-
pages: calculateTotalPages(total, limit),
|
|
4425
|
-
hasNext
|
|
4426
|
-
hasPrev: page > 1
|
|
4811
|
+
total: counted.total,
|
|
4812
|
+
pages: countStrategy === "none" ? 0 : calculateTotalPages(counted.total, limit),
|
|
4813
|
+
hasNext,
|
|
4814
|
+
hasPrev: page > 1,
|
|
4815
|
+
totalIsEstimate: counted.totalIsEstimate,
|
|
4816
|
+
countedAt: counted.countedAt
|
|
4427
4817
|
};
|
|
4428
4818
|
await this._emitHook("after:lookupPopulate", {
|
|
4429
4819
|
context,
|
|
@@ -4839,8 +5229,14 @@ var Repository = class extends RepositoryBase {
|
|
|
4839
5229
|
if (Array.isArray(labels) && (labels.includes("TransientTransactionError") || labels.includes("UnknownTransactionCommitResult")) || typeof hasLabel === "function" && (hasLabel.call(error, "TransientTransactionError") || hasLabel.call(error, "UnknownTransactionCommitResult"))) return error;
|
|
4840
5230
|
if (error instanceof mongoose.mongo.MongoOperationTimeoutError) return error;
|
|
4841
5231
|
if (error instanceof mongoose.Error.ValidationError) {
|
|
4842
|
-
const
|
|
4843
|
-
|
|
5232
|
+
const errs = Object.values(error.errors);
|
|
5233
|
+
const http = createError(400, `Validation Error: ${errs.map((e) => e.message).join(", ")}`);
|
|
5234
|
+
http.validationErrors = errs.map((e) => ({
|
|
5235
|
+
validator: e.kind ?? "mongoose",
|
|
5236
|
+
error: e.message,
|
|
5237
|
+
...e.path ? { path: e.path } : {}
|
|
5238
|
+
}));
|
|
5239
|
+
return http;
|
|
4844
5240
|
}
|
|
4845
5241
|
if (error instanceof mongoose.Error.CastError) return createError(400, `Invalid ${error.path}: ${error.value}`);
|
|
4846
5242
|
const duplicateErr = parseDuplicateKeyError(error);
|