@classytic/mongokit 3.38.1 → 3.39.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{PaginationEngine-CBS1XGzp.d.mts → PaginationEngine-D-P19Y9m.d.mts} +34 -9
- package/dist/PaginationEngine-DQ7beTCR.mjs +1182 -0
- package/dist/{Repository-Ck5x43wF.mjs → Repository-D7fDpyFx.mjs} +400 -37
- package/dist/{Repository-BdSkQ8vc.d.mts → Repository-IxI983_m.d.mts} +55 -8
- package/dist/actions/index.d.mts +1 -1
- package/dist/actions/index.mjs +1 -1
- package/dist/adapter/index.d.mts +21 -0
- package/dist/adapter/index.mjs +1 -1
- package/dist/{adapter-Bfqz8yHE.mjs → adapter-BrVfPFSW.mjs} +45 -1
- package/dist/ai/index.d.mts +1 -1
- package/dist/better-auth/index.mjs +2 -2
- package/dist/{create-repository-BU4x9Kb0.d.mts → create-repository-CRXjDa3N.d.mts} +5 -5
- package/dist/{create-repository-B5EulQ31.mjs → create-repository-DOe9E_5F.mjs} +3 -3
- package/dist/{index-DKEql4cL.d.mts → index-CmjNyulu.d.mts} +67 -8
- package/dist/index.d.mts +77 -13
- package/dist/index.mjs +8 -8
- package/dist/{method-registry.plugin-DblM7V6j.mjs → method-registry.plugin-C63pGn1d.mjs} +54 -4
- package/dist/{multi-tenant.plugin-BwSvTirB.d.mts → multi-tenant.plugin-B-tzNeYL.d.mts} +60 -4
- package/dist/{operations-DZodvjC-.d.mts → operations-CrRIoc8Z.d.mts} +22 -7
- package/dist/outbox/index.d.mts +1 -1
- package/dist/outbox/index.mjs +2 -2
- package/dist/pagination/PaginationEngine.d.mts +1 -1
- package/dist/pagination/PaginationEngine.mjs +1 -1
- package/dist/pagination/utils/limits.d.mts +1 -1
- package/dist/pagination/utils/limits.mjs +22 -1
- package/dist/pagination-BpIv2bdS.d.mts +364 -0
- package/dist/{plugin-options-DXnlKM3B.d.mts → plugin-options-BuvtOSZf.d.mts} +2 -2
- package/dist/plugins/index.d.mts +3 -3
- package/dist/plugins/index.mjs +3 -3
- package/dist/{repository-H_ffGgpE.d.mts → repository-ZbLtL7Nk.d.mts} +30 -4
- package/dist/tenant/index.d.mts +89 -0
- package/dist/tenant/index.mjs +94 -0
- package/dist/testkit/index.d.mts +2 -2
- package/dist/testkit/index.mjs +1 -1
- package/dist/{timestamp.plugin-CBQukQzV.mjs → timestamp.plugin-B5eiaItQ.mjs} +31 -8
- package/dist/{to-plain-Dyz_qvuo.d.mts → to-plain-B8ufk3Qz.d.mts} +2 -2
- package/dist/{update-jSXp3nu3.mjs → update-c_01A0if.mjs} +126 -73
- package/dist/utils/index.d.mts +1 -1
- package/dist/{validation-chain.plugin-DhNyxNCw.mjs → validation-chain.plugin-Bju1D1Sn.mjs} +2 -2
- package/dist/{validation-chain.plugin-yQRob3Av.d.mts → validation-chain.plugin-DvrLJIbJ.d.mts} +3 -19
- package/package.json +7 -3
- package/dist/PaginationEngine-BX9o_-4R.mjs +0 -735
- package/dist/core-62oo3N1C.d.mts +0 -50
- package/dist/pagination-jSUdrACK.d.mts +0 -158
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { i as isValidIdForType, t as getSchemaIdType } from "./id-resolution-BqDrk0S2.mjs";
|
|
2
2
|
import { a as narrowToIds, i as keysetFilter, o as selectKeysetChunk } from "./id-chunks-BmAwsqNk.mjs";
|
|
3
|
-
import {
|
|
3
|
+
import { _ as compileFilterToMongo, a as count, 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 create, y as createMany } from "./update-c_01A0if.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 { t as PaginationEngine } from "./PaginationEngine-
|
|
6
|
+
import { c as resolveCursorSecrets, l as verifySignature, s as attachSignature, t as PaginationEngine } from "./PaginationEngine-DQ7beTCR.mjs";
|
|
7
7
|
import { calculateTotalPages } from "./pagination/utils/limits.mjs";
|
|
8
8
|
import { hasNearOperator, rewriteNearForCount } from "./query/primitives/geo.mjs";
|
|
9
|
-
import { RepositoryBase, runChunkedArchive, runChunkedPurge, throwIfAborted, validatePluginOrder, withRetry } from "@classytic/repo-core/repository";
|
|
9
|
+
import { RepositoryBase, createDistributionGuard, runChunkedArchive, runChunkedPurge, throwIfAborted, validatePluginOrder, withRetry } from "@classytic/repo-core/repository";
|
|
10
10
|
import mongoose from "mongoose";
|
|
11
11
|
import { isFilter } from "@classytic/repo-core/filter";
|
|
12
12
|
import { VersionConflictError, conservativeMongoIsTransientConflict } from "@classytic/repo-core/errors";
|
|
@@ -806,8 +806,35 @@ function formatForUnit(unit) {
|
|
|
806
806
|
*/
|
|
807
807
|
function compileFilterToMongoExpr(input) {
|
|
808
808
|
if (input === void 0 || input === null) return true;
|
|
809
|
-
if (
|
|
810
|
-
|
|
809
|
+
if (isFilter(input)) return compile(input);
|
|
810
|
+
if (typeof input === "boolean") return input;
|
|
811
|
+
/**
|
|
812
|
+
* Anything else must be an aggregation EXPRESSION, and an aggregation
|
|
813
|
+
* expression's operator keys are `$`-prefixed (`$eq`, `$and`, `$gt`).
|
|
814
|
+
*
|
|
815
|
+
* This used to `return input` for everything that was not Filter IR,
|
|
816
|
+
* described as "already an expression — pass through unchanged". A plain
|
|
817
|
+
* query object is not an expression: `{ status: { eq: 'paid' } }` reaches
|
|
818
|
+
* `$cond` as a non-empty object, which MongoDB evaluates as TRUTHY. So a
|
|
819
|
+
* measure's `where` written in query syntax silently matched every row and
|
|
820
|
+
* the filtered aggregate came back equal to the unfiltered one, with nothing
|
|
821
|
+
* raised.
|
|
822
|
+
*
|
|
823
|
+
* That syntax is not imaginary — it is exactly what `AggRequest.filter`
|
|
824
|
+
* accepts, because `compileFilterToMongo` runs `expandShorthands` over it.
|
|
825
|
+
* Two filter surfaces on one request object, one of which honoured the
|
|
826
|
+
* syntax and the other quietly ignored it.
|
|
827
|
+
*
|
|
828
|
+
* Refused rather than translated: a partial query-to-expression translator
|
|
829
|
+
* would get the common operators right and fail the same silent way on the
|
|
830
|
+
* rest, which is the failure being fixed.
|
|
831
|
+
*/
|
|
832
|
+
if (typeof input === "object") {
|
|
833
|
+
const keys = Object.keys(input);
|
|
834
|
+
if (keys.length > 0 && keys.every((k) => k.startsWith("$"))) return input;
|
|
835
|
+
throw createError(400, `mongokit/filter: \`where\` must be Filter IR (\`{ op, field, value }\`) or a MongoDB aggregation expression (\`{ \$eq: ['\$field', value] }\`), got ${keys.length === 0 ? "an empty object" : `an object keyed by ${keys.map((k) => `'${k}'`).join(", ")}`}. Query syntax such as \`{ field: { eq: value } }\` works in \`filter\` but not here — evaluated as an expression it is always TRUE, so the filter would silently match every row.`);
|
|
836
|
+
}
|
|
837
|
+
return input;
|
|
811
838
|
}
|
|
812
839
|
function compile(filter) {
|
|
813
840
|
switch (filter.op) {
|
|
@@ -983,20 +1010,26 @@ function buildAggPipeline(req, schema) {
|
|
|
983
1010
|
for (const alias of bucketAliases) addFields[bucketInternalKey(alias)] = compileDateBucket(req.dateBuckets[alias]);
|
|
984
1011
|
stages.push({ $addFields: addFields });
|
|
985
1012
|
}
|
|
986
|
-
const
|
|
1013
|
+
const groupId = groupCols.length === 0 && bucketAliases.length === 0 ? null : (() => {
|
|
987
1014
|
const id = {};
|
|
988
1015
|
for (const field of groupCols) id[safeIdKey(field)] = `$${field}`;
|
|
989
1016
|
for (const alias of bucketAliases) id[alias] = `$${bucketInternalKey(alias)}`;
|
|
990
1017
|
return id;
|
|
991
|
-
})()
|
|
1018
|
+
})();
|
|
1019
|
+
const groupStage = { _id: groupId };
|
|
992
1020
|
const distinctSetAliases = [];
|
|
993
1021
|
const percentileAliases = [];
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
1022
|
+
const groupedDistinct = req.countDistinctStrategy === "grouped";
|
|
1023
|
+
const distinctAliases = Object.entries(req.measures).filter(([, m]) => m.op === "countDistinct").map(([alias]) => alias);
|
|
1024
|
+
if (groupedDistinct && distinctAliases.length > 0) appendGroupedDistinctStages(stages, req, groupId, distinctAliases);
|
|
1025
|
+
else {
|
|
1026
|
+
for (const [alias, measure] of Object.entries(req.measures)) {
|
|
1027
|
+
groupStage[alias] = compileMeasure(measure).groupExpr;
|
|
1028
|
+
if (measure.op === "countDistinct") distinctSetAliases.push(alias);
|
|
1029
|
+
else if (measure.op === "percentile") percentileAliases.push(alias);
|
|
1030
|
+
}
|
|
1031
|
+
stages.push({ $group: groupStage });
|
|
998
1032
|
}
|
|
999
|
-
stages.push({ $group: groupStage });
|
|
1000
1033
|
if (distinctSetAliases.length > 0) {
|
|
1001
1034
|
const addFields = {};
|
|
1002
1035
|
for (const alias of distinctSetAliases) addFields[alias] = { $size: { $ifNull: [`$${alias}`, []] } };
|
|
@@ -1065,12 +1098,108 @@ function validateTopN(topN, groupCols, bucketAliases, measures) {
|
|
|
1065
1098
|
* - `'dense_rank'` → `$denseRank` (no gaps)
|
|
1066
1099
|
* - `'row_number'` → `$documentNumber` (each row unique)
|
|
1067
1100
|
*/
|
|
1101
|
+
/**
|
|
1102
|
+
* Accumulators that survive being computed over a FINER partition and then
|
|
1103
|
+
* combined — the whole constraint on the grouped strategy.
|
|
1104
|
+
*
|
|
1105
|
+
* Pre-grouping by the distinct field splits every group into one sub-group per
|
|
1106
|
+
* distinct value, so each remaining measure must answer the same over the split
|
|
1107
|
+
* as it did whole. Summing is associative over a partition, and so are min and
|
|
1108
|
+
* max, so those four are exact.
|
|
1109
|
+
*
|
|
1110
|
+
* `avg` is not (the mean of means is not the mean); `percentile` and `stddev`
|
|
1111
|
+
* are not; and a SECOND `countDistinct` would need the partition to carry both
|
|
1112
|
+
* fields, which multiplies the rows and breaks the first count. Those are
|
|
1113
|
+
* REFUSED rather than approximated — a distinct count that is quietly wrong is
|
|
1114
|
+
* the exact failure this strategy exists to avoid.
|
|
1115
|
+
*/
|
|
1116
|
+
const REAGGREGATABLE = {
|
|
1117
|
+
count: "$sum",
|
|
1118
|
+
sum: "$sum",
|
|
1119
|
+
min: "$min",
|
|
1120
|
+
max: "$max"
|
|
1121
|
+
};
|
|
1122
|
+
/** Internal `_id` slot holding one distinct value during the pre-group. */
|
|
1123
|
+
const distinctIdKey = (alias) => `__dv_${alias}`;
|
|
1124
|
+
/**
|
|
1125
|
+
* Compile `countDistinct` as pre-group + count instead of accumulate + size.
|
|
1126
|
+
*
|
|
1127
|
+
* `$addToSet` holds every distinct value of a group in that group's accumulator,
|
|
1128
|
+
* so memory grows with cardinality and MongoDB caps it at 100MB. Grouping by the
|
|
1129
|
+
* value instead turns each distinct value into a ROW, and rows spill. Nothing
|
|
1130
|
+
* accumulates, so cardinality stops being a memory question.
|
|
1131
|
+
*
|
|
1132
|
+
* The two semantics that had to be matched exactly, both measured against
|
|
1133
|
+
* `$addToSet` rather than assumed:
|
|
1134
|
+
*
|
|
1135
|
+
* - a MISSING field is not a distinct value, but an explicit `null` IS. A
|
|
1136
|
+
* naive `$group` disagrees on both, and on a group whose field is always
|
|
1137
|
+
* missing it answers 1 where `$addToSet` answers 0. The `$type` check is
|
|
1138
|
+
* what separates them: a missing field leaves the `_id` slot absent, an
|
|
1139
|
+
* explicit null leaves it `null`.
|
|
1140
|
+
* - `where` excludes a row from the COUNT without excluding it from the other
|
|
1141
|
+
* measures. `compileMeasure` already emits `$$REMOVE` for a non-matching
|
|
1142
|
+
* row, which lands it in the same "missing" slot — so the same `$type`
|
|
1143
|
+
* check covers the filtered case with no extra branch.
|
|
1144
|
+
*/
|
|
1145
|
+
function appendGroupedDistinctStages(stages, req, groupId, distinctAliases) {
|
|
1146
|
+
if (distinctAliases.length > 1) throw createError(400, `mongokit/aggregate: countDistinctStrategy 'grouped' supports one countDistinct per request, got ${distinctAliases.length} (${distinctAliases.join(", ")}). Two distinct fields would have to share one partition, which multiplies the rows and makes both counts wrong. Issue them as separate requests, or use the default 'accumulator'.`);
|
|
1147
|
+
const distinctAlias = distinctAliases[0];
|
|
1148
|
+
const blocked = Object.entries(req.measures).filter(([alias, m]) => alias !== distinctAlias && !REAGGREGATABLE[m.op]);
|
|
1149
|
+
if (blocked.length > 0) throw createError(400, `mongokit/aggregate: countDistinctStrategy 'grouped' cannot combine with ${blocked.map(([a, m]) => `'${a}' (${m.op})`).join(", ")}. Pre-grouping by the distinct field splits each group, and only count/sum/min/max survive being recombined — an ${blocked[0][1].op} over the split would be silently wrong. Compute it in a separate request, or use the default 'accumulator' strategy.`);
|
|
1150
|
+
const dvKey = distinctIdKey(distinctAlias);
|
|
1151
|
+
const preId = { ...groupId ?? {} };
|
|
1152
|
+
preId[dvKey] = compileMeasure(req.measures[distinctAlias]).groupExpr;
|
|
1153
|
+
preId[dvKey] = preId[dvKey].$addToSet;
|
|
1154
|
+
const preGroup = { _id: preId };
|
|
1155
|
+
for (const [alias, measure] of Object.entries(req.measures)) {
|
|
1156
|
+
if (alias === distinctAlias) continue;
|
|
1157
|
+
preGroup[alias] = compileMeasure(measure).groupExpr;
|
|
1158
|
+
}
|
|
1159
|
+
stages.push({ $group: preGroup });
|
|
1160
|
+
const regroup = { _id: groupId === null ? null : Object.fromEntries(Object.keys(groupId).map((k) => [k, `$_id.${k}`])) };
|
|
1161
|
+
regroup[distinctAlias] = { $sum: { $cond: [
|
|
1162
|
+
{ $eq: [{ $type: `$_id.${dvKey}` }, "missing"] },
|
|
1163
|
+
0,
|
|
1164
|
+
1
|
|
1165
|
+
] } };
|
|
1166
|
+
for (const [alias, measure] of Object.entries(req.measures)) {
|
|
1167
|
+
if (alias === distinctAlias) continue;
|
|
1168
|
+
regroup[alias] = { [REAGGREGATABLE[measure.op]]: `$${alias}` };
|
|
1169
|
+
}
|
|
1170
|
+
stages.push({ $group: regroup });
|
|
1171
|
+
}
|
|
1068
1172
|
function appendTopNStages(stages, topN) {
|
|
1069
1173
|
const partitionList = Array.isArray(topN.partitionBy) ? topN.partitionBy : [topN.partitionBy];
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1174
|
+
/**
|
|
1175
|
+
* A compound partition is a TUPLE, and a tuple's identity cannot be a string.
|
|
1176
|
+
*
|
|
1177
|
+
* This built the key by `$toString`-ing each value and joining with U+0001,
|
|
1178
|
+
* mapping null to the literal `'__NULL__'`. Both halves lose information, and
|
|
1179
|
+
* the loss merges partitions that are not the same — a value CONTAINING the
|
|
1180
|
+
* separator, or a document genuinely holding the string `'__NULL__'`, lands
|
|
1181
|
+
* on another partition's key. Two unrelated partitions then rank as one, so
|
|
1182
|
+
* `topN` returns rows that are not the top N of anything. Measured: four rows
|
|
1183
|
+
* across two partitions came back ranked 1, 2, 3, 4.
|
|
1184
|
+
*
|
|
1185
|
+
* A DOCUMENT expression compares by value, field by field, with no
|
|
1186
|
+
* serialization in between, so distinct tuples stay distinct whatever they
|
|
1187
|
+
* contain. An array would read more naturally and is not available: Mongo
|
|
1188
|
+
* refuses it outright with "An expression used to partition cannot evaluate
|
|
1189
|
+
* to value of type array".
|
|
1190
|
+
*
|
|
1191
|
+
* Keys are positional (`k0`, `k1`, ...) rather than the field names, because
|
|
1192
|
+
* a partition field may be a dotted path and a dot is not legal in an
|
|
1193
|
+
* expression document's key. They are internal to this stage and never
|
|
1194
|
+
* surface in output.
|
|
1195
|
+
*
|
|
1196
|
+
* `$ifNull` to null keeps arity fixed, so a missing field and an explicit
|
|
1197
|
+
* null partition together the way SQL's `PARTITION BY` groups NULLs, while a
|
|
1198
|
+
* real `'__NULL__'` string stays separate from both.
|
|
1199
|
+
*
|
|
1200
|
+
* Single-key partitions keep the bare path: same semantics, cleaner plan.
|
|
1201
|
+
*/
|
|
1202
|
+
const partitionExpr = partitionList.length === 1 ? `$${partitionList[0]}` : Object.fromEntries(partitionList.map((field, i) => [`k${i}`, { $ifNull: [`$${field}`, null] }]));
|
|
1074
1203
|
const tiesOp = topN.ties === "dense_rank" ? "$denseRank" : topN.ties === "row_number" ? "$documentNumber" : "$rank";
|
|
1075
1204
|
const RANK_FIELD = "__topNRank";
|
|
1076
1205
|
stages.push({ $setWindowFields: {
|
|
@@ -1308,8 +1437,22 @@ async function executeAgg(Model, req, options = {}) {
|
|
|
1308
1437
|
* keyset predicate runs AFTER the post-group `$project` stage, which
|
|
1309
1438
|
* is when the output column names exist.
|
|
1310
1439
|
*/
|
|
1311
|
-
|
|
1312
|
-
|
|
1440
|
+
/**
|
|
1441
|
+
* The aggregate cursor is signed by the SAME key as the find-based one.
|
|
1442
|
+
*
|
|
1443
|
+
* Two cursor codecs are exposed on the wire, and a deployment that signed only
|
|
1444
|
+
* the one on `getAll` would still hand out a tamperable position on every
|
|
1445
|
+
* aggregate keyset endpoint. A guarantee with an unsigned second door is not a
|
|
1446
|
+
* guarantee — it is a guarantee-shaped thing that reviewers stop checking.
|
|
1447
|
+
*
|
|
1448
|
+
* The signature wraps repo-core's kit-neutral codec rather than living inside
|
|
1449
|
+
* it: repo-core's codec is browser-safe by contract and `node:crypto` is not.
|
|
1450
|
+
*/
|
|
1451
|
+
function encodeAggCursor$1(row, sort, secret) {
|
|
1452
|
+
return attachSignature(encodeAggCursor(row, sort), resolveCursorSecrets(secret));
|
|
1453
|
+
}
|
|
1454
|
+
function decodeAggCursor$1(cursor, secret) {
|
|
1455
|
+
return decodeAggCursor(verifySignature(cursor, resolveCursorSecrets(secret)), "mongokit");
|
|
1313
1456
|
}
|
|
1314
1457
|
/**
|
|
1315
1458
|
* Build a `$match` stage that selects rows AFTER the cursor row given
|
|
@@ -2232,6 +2375,41 @@ function assertCasOwnsVersionField(opName, patch, versionField) {
|
|
|
2232
2375
|
throw createError(400, `[${opName}] payload's ${op} targets '${versionField}', but the optimistic-concurrency CAS owns that field — it increments it as part of the same write. ` + (op === "$inc" ? `Only \`$inc: { ${versionField}: 1 }\` is accepted (it matches what the CAS does); any other increment would be silently overridden. ` : `A conflicting ${op} would either fail the update with a Mongo path conflict or be silently overridden. `) + `Remove it; the expected version belongs in \`options.ifVersion\`.`);
|
|
2233
2376
|
}
|
|
2234
2377
|
}
|
|
2378
|
+
/** Filter-bearing ops — those a policy plugin can inject a scope into. */
|
|
2379
|
+
function carriesAFilter(op) {
|
|
2380
|
+
const policyKey = OP_REGISTRY[op].policyKey;
|
|
2381
|
+
return policyKey === "query" || policyKey === "filters";
|
|
2382
|
+
}
|
|
2383
|
+
/**
|
|
2384
|
+
* Shapes the guard can never say anything useful about, whatever the key:
|
|
2385
|
+
* `watch` (a change stream is cluster-wide however it is filtered) and
|
|
2386
|
+
* `getOrCreate` (its filter is the unique key by construction, and a unique
|
|
2387
|
+
* index on a sharded collection must already contain the shard key).
|
|
2388
|
+
*/
|
|
2389
|
+
const DISTRIBUTION_EXEMPT_SHAPES = /* @__PURE__ */ new Set(["watch", "getOrCreate"]);
|
|
2390
|
+
/**
|
|
2391
|
+
* Verbs the distribution guard inspects — every op whose filter the CALLER
|
|
2392
|
+
* assembles and can therefore omit the shard key.
|
|
2393
|
+
*
|
|
2394
|
+
* DERIVED from `OP_REGISTRY`, not hardcoded: a future query-shaped op is
|
|
2395
|
+
* covered by its one registry entry, which is the same rule the strict-query
|
|
2396
|
+
* diagnostic below follows and the redundancy the registry exists to remove.
|
|
2397
|
+
*/
|
|
2398
|
+
const DISTRIBUTION_GUARDED_OPERATIONS = new Set(ALL_OPERATIONS.filter((op) => carriesAFilter(op) && !OP_REGISTRY[op].hasIdContext && !DISTRIBUTION_EXEMPT_SHAPES.has(op)));
|
|
2399
|
+
/**
|
|
2400
|
+
* Id-addressed verbs (`getById`, `update`, `delete`, `claim*`, `restore`) —
|
|
2401
|
+
* guarded ONLY when the distribution key is not the id field.
|
|
2402
|
+
*
|
|
2403
|
+
* The reasoning they were first excluded under — "the `_id` match is routed,
|
|
2404
|
+
* not scattered" — holds only when `_id` IS the shard key. Sharded on anything
|
|
2405
|
+
* else (`organizationId`, the tenant-prefixed design this guard mostly exists
|
|
2406
|
+
* for), a by-id update is a broadcast to every shard; MongoDB before 7.0
|
|
2407
|
+
* rejected `updateOne`/`deleteOne` without the shard key outright. These verbs
|
|
2408
|
+
* all take a filter slot too, which is where tenant scope lands — so a scoped
|
|
2409
|
+
* write satisfies the guard and only a `bypassTenant` one is flagged, exactly
|
|
2410
|
+
* the call that deserves the scrutiny.
|
|
2411
|
+
*/
|
|
2412
|
+
const ID_ADDRESSED_OPERATIONS = new Set(ALL_OPERATIONS.filter((op) => carriesAFilter(op) && OP_REGISTRY[op].hasIdContext && !DISTRIBUTION_EXEMPT_SHAPES.has(op)));
|
|
2235
2413
|
/** Mongo `operationType` → portable `ChangeEvent.operation`. */
|
|
2236
2414
|
const CHANGE_OPERATION_MAP = {
|
|
2237
2415
|
insert: "create",
|
|
@@ -2269,6 +2447,24 @@ function changeEventTimestamp(change) {
|
|
|
2269
2447
|
* OBSERVABILITY=300, DEFAULT=500) and the two refs are reference-equal.
|
|
2270
2448
|
*/
|
|
2271
2449
|
const HOOK_PRIORITY$1 = HOOK_PRIORITY;
|
|
2450
|
+
/**
|
|
2451
|
+
* `capped` needs an envelope that can say "this total is a FLOOR".
|
|
2452
|
+
*
|
|
2453
|
+
* `paginate()` and `aggregatePaginate()` carry `totalIsLowerBound` through
|
|
2454
|
+
* mongokit's `TExtra` slot. `lookupPopulate` and the aggregate-IR offset path
|
|
2455
|
+
* return repo-core's CORE envelope, which has no such slot — so a bounded count
|
|
2456
|
+
* there would be indistinguishable from a real one, and a caller would render a
|
|
2457
|
+
* ceiling as a total. That is the failure the strategy exists to prevent.
|
|
2458
|
+
*
|
|
2459
|
+
* So they count exactly, and SAY they did. A deployment-wide `capped` policy
|
|
2460
|
+
* keeps working (an exact count is slower, never wrong); the log line is what
|
|
2461
|
+
* makes the one path that ignored the policy findable instead of mysterious.
|
|
2462
|
+
*/
|
|
2463
|
+
function downgradeCappedCount(strategy, where) {
|
|
2464
|
+
if (strategy !== "capped") return strategy;
|
|
2465
|
+
warn(`[mongokit] countStrategy 'capped' is not supported by ${where} — counting exactly. Its result envelope cannot carry \`totalIsLowerBound\`, and an unflagged ceiling reads as a total. Use getAll()/aggregatePaginate() where a bounded count matters.`);
|
|
2466
|
+
return "exact";
|
|
2467
|
+
}
|
|
2272
2468
|
var Repository = class extends RepositoryBase {
|
|
2273
2469
|
/**
|
|
2274
2470
|
* Runtime capability descriptor required by `StandardRepo<TDoc>`
|
|
@@ -2320,6 +2516,18 @@ var Repository = class extends RepositoryBase {
|
|
|
2320
2516
|
* the `Middleware<TDoc>` type for the protocol.
|
|
2321
2517
|
*/
|
|
2322
2518
|
_middlewares = [];
|
|
2519
|
+
/**
|
|
2520
|
+
* Shard-key guard built once from `options.distribution`; `undefined`
|
|
2521
|
+
* when not configured so the per-op check is a single falsy test.
|
|
2522
|
+
*/
|
|
2523
|
+
_checkDistribution;
|
|
2524
|
+
/**
|
|
2525
|
+
* Whether id-addressed verbs are guarded too — true unless the
|
|
2526
|
+
* distribution key IS the id field, where an `_id` match is genuinely
|
|
2527
|
+
* routed and guarding would flag every by-id call. See
|
|
2528
|
+
* {@link ID_ADDRESSED_OPERATIONS}.
|
|
2529
|
+
*/
|
|
2530
|
+
_guardIdAddressed = false;
|
|
2323
2531
|
constructor(Model, plugins = [], paginationConfig = {}, options = {}) {
|
|
2324
2532
|
super({
|
|
2325
2533
|
name: Model.modelName,
|
|
@@ -2341,6 +2549,10 @@ var Repository = class extends RepositoryBase {
|
|
|
2341
2549
|
this.searchMode = options.searchMode ?? "text";
|
|
2342
2550
|
this.searchFields = options.searchFields;
|
|
2343
2551
|
this._wireStrictQueryStripDiagnostic(options.warnOnStrictQueryStrip === true);
|
|
2552
|
+
this._checkDistribution = options.distribution ? createDistributionGuard(options.distribution, options.distribution.onMiss ?? ((info) => {
|
|
2553
|
+
warn(`[mongokit] Repository "${this.model}": ${info.operation} filter omits the distribution key "${info.key}" — this fans out across every shard. Include the key, list the op in distribution.exemptOperations, or set onMissingKey: 'off'.`);
|
|
2554
|
+
})) : void 0;
|
|
2555
|
+
this._guardIdAddressed = options.distribution !== void 0 && options.distribution.key !== this.idField;
|
|
2344
2556
|
if (this.searchMode === "regex" && (!this.searchFields || this.searchFields.length === 0)) warn(`[mongokit] Repository "${this.model}" configured with searchMode: 'regex' but no searchFields provided. getAll({ search }) will throw until searchFields is set.`);
|
|
2345
2557
|
for (const plugin of plugins) this.use(plugin);
|
|
2346
2558
|
if (options.requirePlugins && options.requirePlugins.length > 0) {
|
|
@@ -2872,8 +3084,12 @@ var Repository = class extends RepositoryBase {
|
|
|
2872
3084
|
* repo without a filter to observe deletes.
|
|
2873
3085
|
* - `options.signal` ends the iterator (closes the stream). A
|
|
2874
3086
|
* pre-aborted signal rejects at the op boundary like every other op.
|
|
2875
|
-
* - `
|
|
2876
|
-
*
|
|
3087
|
+
* - Every yielded event carries `resumeToken` (the change document's
|
|
3088
|
+
* `_id`). Persist the last one you finished processing and pass it
|
|
3089
|
+
* back as `options.resumeAfter` — or `options.startAfter` to also
|
|
3090
|
+
* step past an invalidate event — for at-least-once consumption
|
|
3091
|
+
* across restarts. Setting both is rejected up front (Mongo forbids
|
|
3092
|
+
* the combination; the driver error is far less legible).
|
|
2877
3093
|
*
|
|
2878
3094
|
* Context keys for policy plugins (`organizationId`, `bypassTenant`,
|
|
2879
3095
|
* `includeDeleted`, ...) ride the options bag at runtime — same as every
|
|
@@ -2890,6 +3106,7 @@ var Repository = class extends RepositoryBase {
|
|
|
2890
3106
|
* ```
|
|
2891
3107
|
*/
|
|
2892
3108
|
async *watch(filter, options = {}) {
|
|
3109
|
+
if (options.resumeAfter !== void 0 && options.startAfter !== void 0) throw createError(400, "watch() accepts either `resumeAfter` or `startAfter`, not both — they are alternative resume positions for the same stream.");
|
|
2893
3110
|
const context = await this._buildContext("watch", {
|
|
2894
3111
|
query: filter ?? {},
|
|
2895
3112
|
...options
|
|
@@ -2904,9 +3121,60 @@ var Repository = class extends RepositoryBase {
|
|
|
2904
3121
|
if (Object.keys(compiled).length > 0) pipeline.push({ $match: prefixMatchPaths(compiled, "fullDocument") });
|
|
2905
3122
|
const stream = this.Model.watch(pipeline, {
|
|
2906
3123
|
fullDocument: "updateLookup",
|
|
2907
|
-
...options.resumeAfter !== void 0 ? { resumeAfter: options.resumeAfter } : {}
|
|
3124
|
+
...options.resumeAfter !== void 0 ? { resumeAfter: options.resumeAfter } : {},
|
|
3125
|
+
...options.startAfter !== void 0 ? { startAfter: options.startAfter } : {}
|
|
2908
3126
|
});
|
|
3127
|
+
/**
|
|
3128
|
+
* A change feed has NO backpressure — the server pushes, and this bridge
|
|
3129
|
+
* buffers whatever the consumer has not reached yet. Unbounded, a consumer
|
|
3130
|
+
* merely slower than the write rate becomes unbounded memory growth whose
|
|
3131
|
+
* symptom (an OOM) appears nowhere near its cause.
|
|
3132
|
+
*
|
|
3133
|
+
* `head` rather than `queue.shift()`: shift is O(n) on a real array, so a
|
|
3134
|
+
* backlog made draining quadratic exactly when the process was already
|
|
3135
|
+
* struggling. The array is compacted once the read head passes halfway, so
|
|
3136
|
+
* the retained slots stay bounded by the live depth rather than by
|
|
3137
|
+
* everything ever enqueued.
|
|
3138
|
+
*/
|
|
3139
|
+
/**
|
|
3140
|
+
* An unusable bound must not read as "no bound".
|
|
3141
|
+
*
|
|
3142
|
+
* `maxBuffered > 0` is the guard below, and a negative, a fraction,
|
|
3143
|
+
* `Infinity` and `NaN` all slip past it into UNBOUNDED buffering — the
|
|
3144
|
+
* failure this option exists to prevent, silently, in a deployment that
|
|
3145
|
+
* believed it had configured a ceiling. Same shape as the `countLimit`
|
|
3146
|
+
* ceiling, and refused the same way.
|
|
3147
|
+
*
|
|
3148
|
+
* `0` stays legal because it is the documented, explicit escape hatch;
|
|
3149
|
+
* every other non-positive-integer is a mistake.
|
|
3150
|
+
*/
|
|
3151
|
+
const maxBuffered = options.maxBufferedEvents ?? 1024;
|
|
3152
|
+
if (maxBuffered !== 0 && !(Number.isInteger(maxBuffered) && maxBuffered > 0)) throw createError(400, `watch(): maxBufferedEvents must be a positive integer, or 0 to disable the bound — got ${String(maxBuffered)}. A negative, fractional, infinite or NaN value would silently buffer without limit, which is the failure the option exists to prevent.`);
|
|
2909
3153
|
const queue = [];
|
|
3154
|
+
let head = 0;
|
|
3155
|
+
let overflowed = false;
|
|
3156
|
+
/**
|
|
3157
|
+
* Delivered is not processed.
|
|
3158
|
+
*
|
|
3159
|
+
* `lastDelivered` is set before the `yield`; `lastAcknowledged` only when
|
|
3160
|
+
* execution RESUMES after it, which happens when the consumer comes back
|
|
3161
|
+
* for the next value — the one observable proof it finished with the
|
|
3162
|
+
* previous one.
|
|
3163
|
+
*
|
|
3164
|
+
* The distinction is the whole difference between at-least-once and
|
|
3165
|
+
* silent loss. Resuming after a merely DELIVERED token skips an event the
|
|
3166
|
+
* consumer may have been halfway through when the stream ended: it was
|
|
3167
|
+
* handed over, never processed, and never seen again. Resuming after the
|
|
3168
|
+
* ACKNOWLEDGED one redelivers at most the in-flight event, which an
|
|
3169
|
+
* idempotent consumer is already built to absorb.
|
|
3170
|
+
*
|
|
3171
|
+
* The asymmetry is the point: redelivering costs a duplicate, skipping
|
|
3172
|
+
* costs a lost write. For money, inventory and outbox consumers those are
|
|
3173
|
+
* not comparable.
|
|
3174
|
+
*/
|
|
3175
|
+
let lastDeliveredToken;
|
|
3176
|
+
let lastAcknowledgedToken;
|
|
3177
|
+
const depth = () => queue.length - head;
|
|
2910
3178
|
let streamError = null;
|
|
2911
3179
|
let ended = false;
|
|
2912
3180
|
let notify = null;
|
|
@@ -2916,6 +3184,26 @@ var Repository = class extends RepositoryBase {
|
|
|
2916
3184
|
resolve?.();
|
|
2917
3185
|
};
|
|
2918
3186
|
const onChange = (change) => {
|
|
3187
|
+
if (overflowed) return;
|
|
3188
|
+
if (maxBuffered > 0 && depth() >= maxBuffered) {
|
|
3189
|
+
overflowed = true;
|
|
3190
|
+
streamError = createError(503, `watch(): consumer fell more than ${maxBuffered} events behind and the buffer is full. Events are never dropped, so the stream ends here — resume with \`startAfter\` from this error's \`resumeToken\`, which is the last event the consumer ACKNOWLEDGED (not the last delivered: resuming past an event still being processed would lose it). Raise \`maxBufferedEvents\` for burst tolerance, or move consumption off the iterator onto a durable queue.`);
|
|
3191
|
+
/**
|
|
3192
|
+
* `resumeToken` is the ACKNOWLEDGED token, never the delivered one:
|
|
3193
|
+
* restarting after an event the consumer had not finished would drop
|
|
3194
|
+
* it. `deliveredToken` rides along for diagnostics only — a supervisor
|
|
3195
|
+
* that resumes from it reintroduces the loss this separation prevents.
|
|
3196
|
+
*
|
|
3197
|
+
* Both may be `undefined` when overflow arrives before the consumer
|
|
3198
|
+
* completed anything; the caller then restarts from whatever position
|
|
3199
|
+
* it opened the stream with, not from here.
|
|
3200
|
+
*/
|
|
3201
|
+
const overflowError = streamError;
|
|
3202
|
+
overflowError.resumeToken = lastAcknowledgedToken;
|
|
3203
|
+
overflowError.deliveredToken = lastDeliveredToken;
|
|
3204
|
+
wake();
|
|
3205
|
+
return;
|
|
3206
|
+
}
|
|
2919
3207
|
queue.push(change);
|
|
2920
3208
|
wake();
|
|
2921
3209
|
};
|
|
@@ -2948,16 +3236,23 @@ var Repository = class extends RepositoryBase {
|
|
|
2948
3236
|
}
|
|
2949
3237
|
while (true) {
|
|
2950
3238
|
if (signal?.aborted) return;
|
|
2951
|
-
const change = queue.
|
|
3239
|
+
const change = head < queue.length ? queue[head++] : void 0;
|
|
3240
|
+
if (head > 32 && head * 2 >= queue.length) {
|
|
3241
|
+
queue.splice(0, head);
|
|
3242
|
+
head = 0;
|
|
3243
|
+
}
|
|
2952
3244
|
if (change) {
|
|
2953
3245
|
const operation = CHANGE_OPERATION_MAP[change.operationType];
|
|
2954
3246
|
if (!operation) continue;
|
|
3247
|
+
lastDeliveredToken = change._id;
|
|
2955
3248
|
yield {
|
|
2956
3249
|
operation,
|
|
2957
3250
|
id: change.documentKey?._id,
|
|
2958
3251
|
...change.fullDocument !== void 0 ? { doc: change.fullDocument } : {},
|
|
2959
|
-
timestamp: changeEventTimestamp(change)
|
|
3252
|
+
timestamp: changeEventTimestamp(change),
|
|
3253
|
+
...change._id !== void 0 ? { resumeToken: change._id } : {}
|
|
2960
3254
|
};
|
|
3255
|
+
lastAcknowledgedToken = change._id;
|
|
2961
3256
|
continue;
|
|
2962
3257
|
}
|
|
2963
3258
|
if (streamError) {
|
|
@@ -3034,8 +3329,9 @@ var Repository = class extends RepositoryBase {
|
|
|
3034
3329
|
const limit = context.limit ?? params.limit ?? params.pagination?.limit ?? this._pagination.config.defaultLimit;
|
|
3035
3330
|
const page = context.page ?? params.pagination?.page ?? params.page;
|
|
3036
3331
|
const after = context.after ?? params.cursor ?? params.after;
|
|
3332
|
+
const before = context.before ?? params.before;
|
|
3037
3333
|
const mode = context.mode ?? params.mode;
|
|
3038
|
-
const useKeyset = this._detectPaginationMode(mode, page, after, sort, context, params);
|
|
3334
|
+
const useKeyset = this._detectPaginationMode(mode, page, after ?? before, sort, context, params);
|
|
3039
3335
|
const query = this._buildSearchQuery(filters, search);
|
|
3040
3336
|
const populateSpec = options.populateOptions || params.populateOptions || context.populate || options.populate;
|
|
3041
3337
|
const paginationOptions = {
|
|
@@ -3048,6 +3344,7 @@ var Repository = class extends RepositoryBase {
|
|
|
3048
3344
|
session: options.session,
|
|
3049
3345
|
hint: context.hint ?? params.hint,
|
|
3050
3346
|
maxTimeMS: context.maxTimeMS ?? params.maxTimeMS,
|
|
3347
|
+
countLimit: context.countLimit ?? params.countLimit,
|
|
3051
3348
|
readPreference: context.readPreference ?? options.readPreference ?? params.readPreference,
|
|
3052
3349
|
collation: context.collation ?? params.collation
|
|
3053
3350
|
};
|
|
@@ -3064,7 +3361,8 @@ var Repository = class extends RepositoryBase {
|
|
|
3064
3361
|
session: options.session,
|
|
3065
3362
|
readPreference: paginationOptions.readPreference,
|
|
3066
3363
|
collation: paginationOptions.collation,
|
|
3067
|
-
countStrategy: context.countStrategy ?? params.countStrategy
|
|
3364
|
+
countStrategy: context.countStrategy ?? params.countStrategy,
|
|
3365
|
+
countLimit: context.countLimit ?? params.countLimit
|
|
3068
3366
|
});
|
|
3069
3367
|
});
|
|
3070
3368
|
return this._runOp("getAll", context, async () => {
|
|
@@ -3072,7 +3370,8 @@ var Repository = class extends RepositoryBase {
|
|
|
3072
3370
|
if (useKeyset) result = await this._pagination.stream({
|
|
3073
3371
|
...paginationOptions,
|
|
3074
3372
|
sort: paginationOptions.sort,
|
|
3075
|
-
after
|
|
3373
|
+
after,
|
|
3374
|
+
before
|
|
3076
3375
|
});
|
|
3077
3376
|
else {
|
|
3078
3377
|
let countFilters;
|
|
@@ -4260,7 +4559,7 @@ var Repository = class extends RepositoryBase {
|
|
|
4260
4559
|
async _executeAggregatePaginate(finalReq, useKeyset, limit, session) {
|
|
4261
4560
|
if (useKeyset) {
|
|
4262
4561
|
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");
|
|
4263
|
-
const cursor = finalReq.after ? decodeAggCursor$1(finalReq.after) : void 0;
|
|
4562
|
+
const cursor = finalReq.after ? decodeAggCursor$1(finalReq.after, this._pagination.config.cursorSecret) : void 0;
|
|
4264
4563
|
const keysetMatch = cursor ? buildKeysetPredicate(finalReq.sort, cursor) : void 0;
|
|
4265
4564
|
const { pipeline, prePaginationIndex } = buildAggPipeline(finalReq, this.Model.schema);
|
|
4266
4565
|
const headStages = pipeline.slice(0, prePaginationIndex);
|
|
@@ -4279,11 +4578,11 @@ var Repository = class extends RepositoryBase {
|
|
|
4279
4578
|
data,
|
|
4280
4579
|
limit,
|
|
4281
4580
|
hasMore,
|
|
4282
|
-
next: hasMore && data.length > 0 ? encodeAggCursor(data[data.length - 1], finalReq.sort) : null
|
|
4581
|
+
next: hasMore && data.length > 0 ? encodeAggCursor$1(data[data.length - 1], finalReq.sort, this._pagination.config.cursorSecret) : null
|
|
4283
4582
|
};
|
|
4284
4583
|
}
|
|
4285
4584
|
const page = Math.max(1, finalReq.page ?? 1);
|
|
4286
|
-
const countStrategy = finalReq.countStrategy ?? "
|
|
4585
|
+
const countStrategy = downgradeCappedCount(finalReq.countStrategy ?? this._pagination.config.defaultCountStrategy, "aggregatePaginateIr");
|
|
4287
4586
|
const offset = (page - 1) * limit;
|
|
4288
4587
|
if (countStrategy === "none") {
|
|
4289
4588
|
const peek = await executeAgg(this.Model, {
|
|
@@ -4412,18 +4711,18 @@ var Repository = class extends RepositoryBase {
|
|
|
4412
4711
|
const after = context.after ?? options.after;
|
|
4413
4712
|
const pageFromContext = context.page ?? options.page;
|
|
4414
4713
|
const isKeyset = !!after || !pageFromContext && !!sort;
|
|
4415
|
-
const countStrategy = context.countStrategy ?? options.countStrategy ?? "
|
|
4714
|
+
const countStrategy = downgradeCappedCount(context.countStrategy ?? options.countStrategy ?? this._pagination.config.defaultCountStrategy, "lookupPopulate");
|
|
4416
4715
|
const projection = buildLookupProjection(context.select ?? options.select, lookups);
|
|
4417
4716
|
const appendLookupStages$2 = (pipeline) => appendLookupStages(pipeline, lookups, projection, isKeyset && sort ? this._parseSort(sort) : void 0);
|
|
4418
4717
|
if (isKeyset && sort) {
|
|
4419
4718
|
const parsedSort = this._parseSort(sort);
|
|
4420
|
-
const { validateKeysetSort } = await import("./PaginationEngine-
|
|
4421
|
-
const { encodeCursor, resolveCursorFilter } = await import("./PaginationEngine-
|
|
4422
|
-
const { getPrimaryField } = await import("./PaginationEngine-
|
|
4719
|
+
const { validateKeysetSort } = await import("./PaginationEngine-DQ7beTCR.mjs").then((n) => n.n);
|
|
4720
|
+
const { encodeCursor, resolveCursorFilter } = await import("./PaginationEngine-DQ7beTCR.mjs").then((n) => n.r);
|
|
4721
|
+
const { getPrimaryField } = await import("./PaginationEngine-DQ7beTCR.mjs").then((n) => n.n);
|
|
4423
4722
|
const normalizedSort = validateKeysetSort(parsedSort, this._pagination.config.strictKeysetSortFields);
|
|
4424
4723
|
const cursorVersion = this._pagination.config.cursorVersion ?? 1;
|
|
4425
4724
|
const minCursorVersion = this._pagination.config.minCursorVersion ?? 1;
|
|
4426
|
-
const matchFilters = after ? resolveCursorFilter(after, normalizedSort, cursorVersion, { ...filters || {} }, minCursorVersion) : { ...filters || {} };
|
|
4725
|
+
const matchFilters = after ? resolveCursorFilter(after, normalizedSort, cursorVersion, { ...filters || {} }, minCursorVersion, normalizedSort, this._pagination.config.cursorSecret) : { ...filters || {} };
|
|
4427
4726
|
if (projection) {
|
|
4428
4727
|
if (Object.values(projection).some((v) => v === 1)) {
|
|
4429
4728
|
for (const sortField of Object.keys(normalizedSort)) if (!(sortField in projection)) projection[sortField] = 1;
|
|
@@ -4446,7 +4745,7 @@ var Repository = class extends RepositoryBase {
|
|
|
4446
4745
|
data,
|
|
4447
4746
|
limit,
|
|
4448
4747
|
hasMore,
|
|
4449
|
-
next: hasMore && data.length > 0 ? encodeCursor(data[data.length - 1], primaryField, normalizedSort, cursorVersion) : null
|
|
4748
|
+
next: hasMore && data.length > 0 ? encodeCursor(data[data.length - 1], primaryField, normalizedSort, cursorVersion, this._pagination.config.cursorSecret) : null
|
|
4450
4749
|
};
|
|
4451
4750
|
await this._emitHook("after:lookupPopulate", {
|
|
4452
4751
|
context,
|
|
@@ -4646,7 +4945,42 @@ var Repository = class extends RepositoryBase {
|
|
|
4646
4945
|
async _buildContext(operation, options) {
|
|
4647
4946
|
throwIfAborted(options.signal);
|
|
4648
4947
|
const normalized = this._normalizeFilterSlots(options);
|
|
4649
|
-
|
|
4948
|
+
const base = await super._buildContext(operation, normalized);
|
|
4949
|
+
this._guardDistribution(operation, base);
|
|
4950
|
+
return base;
|
|
4951
|
+
}
|
|
4952
|
+
/**
|
|
4953
|
+
* Feed the resolved filter of a guarded verb to the distribution guard.
|
|
4954
|
+
* The slot follows `OP_REGISTRY[op].policyKey` (`query` vs `filters`);
|
|
4955
|
+
* the portable aggregate verbs ALSO carry the caller's filter on
|
|
4956
|
+
* `aggRequest.filter` (merged into the pipeline later by
|
|
4957
|
+
* `_injectPolicyScopeIntoAgg`), and the kit-native pipeline verbs may
|
|
4958
|
+
* carry it as leading `$match` stages — both are folded in so a key the
|
|
4959
|
+
* caller did supply is never reported missing.
|
|
4960
|
+
*/
|
|
4961
|
+
_guardDistribution(operation, context) {
|
|
4962
|
+
if (!this._checkDistribution) return;
|
|
4963
|
+
if (!(DISTRIBUTION_GUARDED_OPERATIONS.has(operation) || this._guardIdAddressed && ID_ADDRESSED_OPERATIONS.has(operation))) return;
|
|
4964
|
+
const policyKey = OP_REGISTRY[operation]?.policyKey;
|
|
4965
|
+
const parts = [];
|
|
4966
|
+
const push = (candidate) => {
|
|
4967
|
+
if (!candidate || typeof candidate !== "object") return;
|
|
4968
|
+
const compiled = compileFilterToMongo(candidate);
|
|
4969
|
+
if (Object.keys(compiled).length > 0) parts.push(compiled);
|
|
4970
|
+
};
|
|
4971
|
+
push(policyKey === "filters" ? context.filters : context.query);
|
|
4972
|
+
push(context.aggRequest?.filter);
|
|
4973
|
+
const pipeline = context.pipeline;
|
|
4974
|
+
if (Array.isArray(pipeline)) for (const stage of pipeline) {
|
|
4975
|
+
if (!stage || typeof stage !== "object") break;
|
|
4976
|
+
if ("$geoNear" in stage) {
|
|
4977
|
+
push(stage.$geoNear?.query);
|
|
4978
|
+
continue;
|
|
4979
|
+
}
|
|
4980
|
+
if (!("$match" in stage)) break;
|
|
4981
|
+
push(stage.$match);
|
|
4982
|
+
}
|
|
4983
|
+
this._checkDistribution(operation, parts.length === 0 ? void 0 : parts.length === 1 ? parts[0] : { $and: parts });
|
|
4650
4984
|
}
|
|
4651
4985
|
/**
|
|
4652
4986
|
* Walk the well-known filter-carrying keys (`query`, `filters`,
|
|
@@ -4683,10 +5017,17 @@ var Repository = class extends RepositoryBase {
|
|
|
4683
5017
|
}
|
|
4684
5018
|
/**
|
|
4685
5019
|
* Detect whether to use keyset (cursor) or offset (page) pagination.
|
|
5020
|
+
* Precedence: explicit `mode` → `page` (offset) → `after` (keyset) →
|
|
5021
|
+
* `PaginationConfig.defaultMode` → the sort heuristic (a non-default
|
|
5022
|
+
* sort with no page means the caller wants a cursor).
|
|
4686
5023
|
*/
|
|
4687
5024
|
_detectPaginationMode(mode, page, after, sort, context, params) {
|
|
4688
5025
|
if (mode) return mode === "keyset";
|
|
4689
|
-
|
|
5026
|
+
if (page) return false;
|
|
5027
|
+
if (after) return true;
|
|
5028
|
+
const defaultMode = this._pagination.config.defaultMode;
|
|
5029
|
+
if (defaultMode) return defaultMode === "keyset";
|
|
5030
|
+
return sort !== "-createdAt" && !!(context.sort ?? params.sort);
|
|
4690
5031
|
}
|
|
4691
5032
|
/**
|
|
4692
5033
|
* Build the MongoDB query filter with search merged in. Handles text
|
|
@@ -4816,7 +5157,29 @@ var Repository = class extends RepositoryBase {
|
|
|
4816
5157
|
* `errorLabels` exactly as we received it. The caller still sees a
|
|
4817
5158
|
* thrown error — only the wrapping layer is skipped.
|
|
4818
5159
|
*/
|
|
5160
|
+
/**
|
|
5161
|
+
* Translate a driver error into the repository's error contract, WITHOUT
|
|
5162
|
+
* losing structured diagnostics an action attached to it.
|
|
5163
|
+
*
|
|
5164
|
+
* The translation replaces the object — an E11000 becomes a 409 — so any
|
|
5165
|
+
* property a caller was meant to read went with it. `createMany` attaches a
|
|
5166
|
+
* `partial` report naming which documents landed, and it arrived nowhere:
|
|
5167
|
+
* the report existed, the test for it failed, and the only evidence was the
|
|
5168
|
+
* absence.
|
|
5169
|
+
*
|
|
5170
|
+
* Carried explicitly rather than by copying every own-property, because the
|
|
5171
|
+
* wrap is deliberate: the point is to hide driver internals, and a blanket
|
|
5172
|
+
* copy would put them back.
|
|
5173
|
+
*/
|
|
4819
5174
|
_handleError(error) {
|
|
5175
|
+
const translated = this._translateError(error);
|
|
5176
|
+
if (translated !== error) {
|
|
5177
|
+
const partial = error.partial;
|
|
5178
|
+
if (partial !== void 0) translated.partial = partial;
|
|
5179
|
+
}
|
|
5180
|
+
return translated;
|
|
5181
|
+
}
|
|
5182
|
+
_translateError(error) {
|
|
4820
5183
|
const labels = error.errorLabels;
|
|
4821
5184
|
const hasLabel = error.hasErrorLabel;
|
|
4822
5185
|
if (Array.isArray(labels) && (labels.includes("TransientTransactionError") || labels.includes("UnknownTransactionCommitResult")) || typeof hasLabel === "function" && (hasLabel.call(error, "TransientTransactionError") || hasLabel.call(error, "UnknownTransactionCommitResult"))) return error;
|