@classytic/mongokit 3.8.0 → 3.10.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/README.md +12 -3
- package/dist/{PaginationEngine-C2_tAvAD.mjs → PaginationEngine-B93fLCcO.mjs} +1 -1
- package/dist/actions/index.d.mts +1 -1
- package/dist/actions/index.mjs +1 -1
- package/dist/ai/index.d.mts +1 -1
- package/dist/{error-Lw9GEAro.mjs → error-CzYwrfUP.mjs} +25 -3
- package/dist/{index-DlSAFIc9.d.mts → index-C7WmYP6F.d.mts} +5 -5
- package/dist/index.d.mts +197 -77
- package/dist/index.mjs +6 -6
- package/dist/{mongooseToJsonSchema-DUwXPStK.mjs → mongooseToJsonSchema-D1sdCOmj.mjs} +21 -103
- package/dist/{mongooseToJsonSchema-BZaKOXXI.d.mts → mongooseToJsonSchema-t-MoVleF.d.mts} +20 -19
- package/dist/pagination/PaginationEngine.d.mts +1 -1
- package/dist/pagination/PaginationEngine.mjs +1 -1
- package/dist/plugins/index.d.mts +1 -1
- package/dist/plugins/index.mjs +1 -1
- package/dist/{types-D57SZJeE.d.mts → types-BUS0Zj8y.d.mts} +44 -132
- package/dist/{update-yjSoNnWy.mjs → update-CWXfFC6O.mjs} +28 -12
- package/dist/utils/index.d.mts +3 -3
- package/dist/utils/index.mjs +3 -3
- package/dist/{validation-chain.plugin-DwoOOWse.mjs → validation-chain.plugin-Ccu6irtx.mjs} +749 -253
- package/dist/{validation-chain.plugin-wGF34Ym-.d.mts → validation-chain.plugin-JkipjuOR.d.mts} +2 -34
- package/package.json +5 -3
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
import { n as debug, r as warn } from "./logger-DJ7egMnd.mjs";
|
|
2
|
-
import { _ as upsert, a as count, b as LookupBuilder, c as getByQuery, d as deleteById, f as deleteByQuery, h as createMany, l as getOrCreate, m as create, n as update, o as exists, r as updateByQuery, s as getById, t as findOneAndUpdate, y as distinct } from "./update-
|
|
3
|
-
import { n as parseDuplicateKeyError, t as createError } from "./error-
|
|
4
|
-
import { t as PaginationEngine } from "./PaginationEngine-
|
|
2
|
+
import { _ as upsert, a as count, b as LookupBuilder, c as getByQuery, d as deleteById, f as deleteByQuery, h as createMany, l as getOrCreate, m as create, n as update, o as exists, r as updateByQuery, s as getById, t as findOneAndUpdate, y as distinct } from "./update-CWXfFC6O.mjs";
|
|
3
|
+
import { n as isDuplicateKeyError, r as parseDuplicateKeyError, t as createError } from "./error-CzYwrfUP.mjs";
|
|
4
|
+
import { t as PaginationEngine } from "./PaginationEngine-B93fLCcO.mjs";
|
|
5
5
|
import { hasNearOperator, rewriteNearForCount } from "./query/primitives/geo.mjs";
|
|
6
6
|
import { r as isValidIdForType, t as getSchemaIdType } from "./id-resolution-e4BT9zAV.mjs";
|
|
7
7
|
import { a as byIdKey, c as listQueryKey, l as modelPattern, o as byQueryKey, r as getFieldsForUser, u as versionKey } from "./field-selection-dI0m4RBs.mjs";
|
|
8
8
|
import mongoose from "mongoose";
|
|
9
|
+
import { isFilter } from "@classytic/repo-core/filter";
|
|
10
|
+
import { HOOK_PRIORITY } from "@classytic/repo-core/hooks";
|
|
11
|
+
import { RepositoryBase, validatePluginOrder } from "@classytic/repo-core/repository";
|
|
9
12
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
10
13
|
//#region src/operations.ts
|
|
11
14
|
/**
|
|
@@ -104,6 +107,11 @@ const OP_REGISTRY = {
|
|
|
104
107
|
mutates: false,
|
|
105
108
|
hasIdContext: false
|
|
106
109
|
},
|
|
110
|
+
aggregatePipeline: {
|
|
111
|
+
policyKey: "query",
|
|
112
|
+
mutates: false,
|
|
113
|
+
hasIdContext: false
|
|
114
|
+
},
|
|
107
115
|
getAll: {
|
|
108
116
|
policyKey: "filters",
|
|
109
117
|
mutates: false,
|
|
@@ -114,6 +122,11 @@ const OP_REGISTRY = {
|
|
|
114
122
|
mutates: false,
|
|
115
123
|
hasIdContext: false
|
|
116
124
|
},
|
|
125
|
+
aggregatePipelinePaginate: {
|
|
126
|
+
policyKey: "filters",
|
|
127
|
+
mutates: false,
|
|
128
|
+
hasIdContext: false
|
|
129
|
+
},
|
|
117
130
|
lookupPopulate: {
|
|
118
131
|
policyKey: "filters",
|
|
119
132
|
mutates: false,
|
|
@@ -158,14 +171,14 @@ function aggregateHelpersPlugin() {
|
|
|
158
171
|
count: { $sum: 1 }
|
|
159
172
|
} }, { $sort: { count: -1 } }];
|
|
160
173
|
if (options.limit) pipeline.push({ $limit: options.limit });
|
|
161
|
-
return this.
|
|
174
|
+
return this.aggregatePipeline(pipeline, options);
|
|
162
175
|
});
|
|
163
176
|
const aggregateOperation = async function(field, operator, resultKey, query = {}, options = {}) {
|
|
164
177
|
const pipeline = [{ $match: query }, { $group: {
|
|
165
178
|
_id: null,
|
|
166
179
|
[resultKey]: { [operator]: `$${field}` }
|
|
167
180
|
} }];
|
|
168
|
-
return (await this.
|
|
181
|
+
return (await this.aggregatePipeline(pipeline, options))[0]?.[resultKey] || 0;
|
|
169
182
|
};
|
|
170
183
|
/**
|
|
171
184
|
* Sum field values
|
|
@@ -789,6 +802,231 @@ function batchOperationsPlugin() {
|
|
|
789
802
|
};
|
|
790
803
|
}
|
|
791
804
|
//#endregion
|
|
805
|
+
//#region src/filter/compile.ts
|
|
806
|
+
/**
|
|
807
|
+
* Compile a Filter IR node to a MongoDB query object. Returns `{}`
|
|
808
|
+
* when the node is `TRUE` (or an empty `and`) so callers can merge
|
|
809
|
+
* with other scopes without special-casing.
|
|
810
|
+
*
|
|
811
|
+
* The `input` is typed as `unknown` so callers can pass either a
|
|
812
|
+
* Filter IR node or an already-built Mongo query and we dispatch
|
|
813
|
+
* appropriately. This keeps the portable API ergonomic for apps that
|
|
814
|
+
* write Mongo queries directly today.
|
|
815
|
+
*/
|
|
816
|
+
function compileFilterToMongo(input) {
|
|
817
|
+
if (!input) return {};
|
|
818
|
+
if (!isFilter(input)) return input;
|
|
819
|
+
return compile(input);
|
|
820
|
+
}
|
|
821
|
+
function compile(filter) {
|
|
822
|
+
switch (filter.op) {
|
|
823
|
+
case "true": return {};
|
|
824
|
+
case "false": return { $expr: false };
|
|
825
|
+
case "eq": return { [filter.field]: filter.value };
|
|
826
|
+
case "ne": return filter.value === null ? { [filter.field]: { $ne: null } } : { $and: [{ [filter.field]: { $ne: filter.value } }, { [filter.field]: { $ne: null } }] };
|
|
827
|
+
case "gt": return { [filter.field]: { $gt: filter.value } };
|
|
828
|
+
case "gte": return { [filter.field]: { $gte: filter.value } };
|
|
829
|
+
case "lt": return { [filter.field]: { $lt: filter.value } };
|
|
830
|
+
case "lte": return { [filter.field]: { $lte: filter.value } };
|
|
831
|
+
case "in":
|
|
832
|
+
if (filter.values.length === 0) return { $expr: false };
|
|
833
|
+
return { [filter.field]: { $in: [...filter.values] } };
|
|
834
|
+
case "nin":
|
|
835
|
+
if (filter.values.length === 0) return {};
|
|
836
|
+
return { [filter.field]: { $nin: [...filter.values] } };
|
|
837
|
+
case "exists": return filter.exists ? { [filter.field]: { $ne: null } } : { [filter.field]: null };
|
|
838
|
+
case "like": return { [filter.field]: likeToRegex(filter.pattern, filter.caseSensitivity) };
|
|
839
|
+
case "regex": return { [filter.field]: { $regex: filter.pattern } };
|
|
840
|
+
case "and": {
|
|
841
|
+
if (filter.children.length === 0) return {};
|
|
842
|
+
const parts = filter.children.map(compile);
|
|
843
|
+
if (parts.length === 1) return parts[0];
|
|
844
|
+
return { $and: parts };
|
|
845
|
+
}
|
|
846
|
+
case "or": {
|
|
847
|
+
if (filter.children.length === 0) return { $expr: false };
|
|
848
|
+
const parts = filter.children.map(compile);
|
|
849
|
+
if (parts.length === 1) return parts[0];
|
|
850
|
+
return { $or: parts };
|
|
851
|
+
}
|
|
852
|
+
case "not": return { $nor: [compile(filter.child)] };
|
|
853
|
+
case "raw": throw new Error("mongokit/filter: `raw` is a SQL escape hatch and has no safe MongoDB translation. Pass a Mongo query object directly to the filter slot instead.");
|
|
854
|
+
}
|
|
855
|
+
}
|
|
856
|
+
/**
|
|
857
|
+
* Translate a SQL-style LIKE pattern to a MongoDB regex.
|
|
858
|
+
*
|
|
859
|
+
* SQL semantics:
|
|
860
|
+
* - `%` → match zero or more characters (`.*`)
|
|
861
|
+
* - `_` → match exactly one character (`.`)
|
|
862
|
+
* - `\%` → literal percent (escape via backslash)
|
|
863
|
+
* - `\_` → literal underscore
|
|
864
|
+
* - `\\` → literal backslash
|
|
865
|
+
* - everything else is literal — regex metacharacters get escaped.
|
|
866
|
+
*
|
|
867
|
+
* The naive implementation `pattern.replace(/%/g, '.*')` is wrong:
|
|
868
|
+
* it also expands escaped `\%` into a wildcard. We walk the pattern
|
|
869
|
+
* char-by-char, honoring backslash escapes, so `like('notes', '50\\% off')`
|
|
870
|
+
* matches the literal string `50% off` instead of `50<anything> off`.
|
|
871
|
+
*
|
|
872
|
+
* Anchored with `^` / `$` so LIKE matches the whole field value, the
|
|
873
|
+
* same way sqlitekit's `LIKE` semantically does.
|
|
874
|
+
*/
|
|
875
|
+
function likeToRegex(pattern, caseSensitivity) {
|
|
876
|
+
let out = "^";
|
|
877
|
+
for (let i = 0; i < pattern.length; i++) {
|
|
878
|
+
const ch = pattern[i];
|
|
879
|
+
if (ch === "\\" && i + 1 < pattern.length) {
|
|
880
|
+
const next = pattern[i + 1];
|
|
881
|
+
if (next === "%" || next === "_" || next === "\\") {
|
|
882
|
+
out += escapeRegexChar(next);
|
|
883
|
+
i++;
|
|
884
|
+
continue;
|
|
885
|
+
}
|
|
886
|
+
}
|
|
887
|
+
if (ch === "%") out += ".*";
|
|
888
|
+
else if (ch === "_") out += ".";
|
|
889
|
+
else out += escapeRegexChar(ch);
|
|
890
|
+
}
|
|
891
|
+
out += "$";
|
|
892
|
+
return caseSensitivity === "sensitive" ? { $regex: out } : {
|
|
893
|
+
$regex: out,
|
|
894
|
+
$options: "i"
|
|
895
|
+
};
|
|
896
|
+
}
|
|
897
|
+
function escapeRegexChar(ch) {
|
|
898
|
+
return /[.*+?^${}()|[\]\\]/.test(ch) ? `\\${ch}` : ch;
|
|
899
|
+
}
|
|
900
|
+
//#endregion
|
|
901
|
+
//#region src/actions/aggregate-ir/normalize.ts
|
|
902
|
+
/**
|
|
903
|
+
* Normalize `AggRequest['groupBy']` into a readonly string array.
|
|
904
|
+
* Returns `[]` for scalar aggregation (no groupBy). Downstream
|
|
905
|
+
* compilers treat `[]` uniformly — one `$group: { _id: null, ... }`
|
|
906
|
+
* stage, no per-key projection.
|
|
907
|
+
*/
|
|
908
|
+
function normalizeGroupBy(groupBy) {
|
|
909
|
+
if (!groupBy) return [];
|
|
910
|
+
if (typeof groupBy === "string") return [groupBy];
|
|
911
|
+
return groupBy;
|
|
912
|
+
}
|
|
913
|
+
/**
|
|
914
|
+
* Fail loud on an empty measures bag — there's nothing to compute and
|
|
915
|
+
* the caller's code path is almost certainly a wiring bug (conditional
|
|
916
|
+
* collapsed, key renamed, etc.). Silently returning `{ rows: [] }`
|
|
917
|
+
* would mask it.
|
|
918
|
+
*/
|
|
919
|
+
function validateMeasures(measures) {
|
|
920
|
+
if (!measures || Object.keys(measures).length === 0) throw new Error("mongokit/aggregate: AggRequest requires at least one measure — empty measures bag is a wiring bug");
|
|
921
|
+
}
|
|
922
|
+
//#endregion
|
|
923
|
+
//#region src/actions/aggregate-ir/measure.ts
|
|
924
|
+
function compileMeasure(measure) {
|
|
925
|
+
switch (measure.op) {
|
|
926
|
+
case "count":
|
|
927
|
+
if (!measure.field || measure.field === "*") return { groupExpr: { $sum: 1 } };
|
|
928
|
+
return { groupExpr: { $sum: { $cond: [
|
|
929
|
+
{ $ne: [`$${measure.field}`, null] },
|
|
930
|
+
1,
|
|
931
|
+
0
|
|
932
|
+
] } } };
|
|
933
|
+
case "countDistinct": return {
|
|
934
|
+
groupExpr: { $addToSet: `$${measure.field}` },
|
|
935
|
+
projectExpr: { $size: { $ifNull: ["$$CURRENT", []] } }
|
|
936
|
+
};
|
|
937
|
+
case "sum": return { groupExpr: { $sum: `$${measure.field}` } };
|
|
938
|
+
case "avg": return { groupExpr: { $avg: `$${measure.field}` } };
|
|
939
|
+
case "min": return { groupExpr: { $min: `$${measure.field}` } };
|
|
940
|
+
case "max": return { groupExpr: { $max: `$${measure.field}` } };
|
|
941
|
+
}
|
|
942
|
+
}
|
|
943
|
+
//#endregion
|
|
944
|
+
//#region src/actions/aggregate-ir/pipeline.ts
|
|
945
|
+
function buildAggPipeline(req) {
|
|
946
|
+
validateMeasures(req.measures);
|
|
947
|
+
const groupCols = normalizeGroupBy(req.groupBy);
|
|
948
|
+
const stages = [];
|
|
949
|
+
if (req.filter) {
|
|
950
|
+
const match = compileFilterToMongo(req.filter);
|
|
951
|
+
if (Object.keys(match).length > 0) stages.push({ $match: match });
|
|
952
|
+
}
|
|
953
|
+
const groupStage = { _id: groupCols.length === 0 ? null : groupCols.reduce((acc, field) => {
|
|
954
|
+
acc[field] = `$${field}`;
|
|
955
|
+
return acc;
|
|
956
|
+
}, {}) };
|
|
957
|
+
const distinctSetAliases = [];
|
|
958
|
+
for (const [alias, measure] of Object.entries(req.measures)) {
|
|
959
|
+
groupStage[alias] = compileMeasure(measure).groupExpr;
|
|
960
|
+
if (measure.op === "countDistinct") distinctSetAliases.push(alias);
|
|
961
|
+
}
|
|
962
|
+
stages.push({ $group: groupStage });
|
|
963
|
+
if (distinctSetAliases.length > 0) {
|
|
964
|
+
const addFields = {};
|
|
965
|
+
for (const alias of distinctSetAliases) addFields[alias] = { $size: { $ifNull: [`$${alias}`, []] } };
|
|
966
|
+
stages.push({ $addFields: addFields });
|
|
967
|
+
}
|
|
968
|
+
const projection = { _id: 0 };
|
|
969
|
+
for (const field of groupCols) projection[field] = `$_id.${field}`;
|
|
970
|
+
for (const alias of Object.keys(req.measures)) projection[alias] = 1;
|
|
971
|
+
stages.push({ $project: projection });
|
|
972
|
+
if (req.having) {
|
|
973
|
+
const match = compileFilterToMongo(req.having);
|
|
974
|
+
if (Object.keys(match).length > 0) stages.push({ $match: match });
|
|
975
|
+
}
|
|
976
|
+
const prePaginationIndex = stages.length;
|
|
977
|
+
if (req.sort) stages.push({ $sort: req.sort });
|
|
978
|
+
if (typeof req.offset === "number" && req.offset > 0) stages.push({ $skip: req.offset });
|
|
979
|
+
if (typeof req.limit === "number") stages.push({ $limit: req.limit });
|
|
980
|
+
return {
|
|
981
|
+
pipeline: stages,
|
|
982
|
+
prePaginationIndex
|
|
983
|
+
};
|
|
984
|
+
}
|
|
985
|
+
//#endregion
|
|
986
|
+
//#region src/actions/aggregate-ir/count.ts
|
|
987
|
+
async function countAggGroups(Model, req, options = {}) {
|
|
988
|
+
const groupCols = normalizeGroupBy(req.groupBy);
|
|
989
|
+
if (req.having) {
|
|
990
|
+
const { pipeline, prePaginationIndex } = buildAggPipeline(req);
|
|
991
|
+
const finalPipeline = [...pipeline.slice(0, prePaginationIndex), { $count: "n" }];
|
|
992
|
+
const aggregation = Model.aggregate(finalPipeline);
|
|
993
|
+
if (options.session) aggregation.session(options.session);
|
|
994
|
+
const [row] = await aggregation.exec();
|
|
995
|
+
return row?.n ?? 0;
|
|
996
|
+
}
|
|
997
|
+
const match = req.filter ? compileFilterToMongo(req.filter) : {};
|
|
998
|
+
if (groupCols.length === 0) {
|
|
999
|
+
const pipeline = [];
|
|
1000
|
+
if (Object.keys(match).length > 0) pipeline.push({ $match: match });
|
|
1001
|
+
pipeline.push({ $limit: 1 });
|
|
1002
|
+
pipeline.push({ $count: "n" });
|
|
1003
|
+
const aggregation = Model.aggregate(pipeline);
|
|
1004
|
+
if (options.session) aggregation.session(options.session);
|
|
1005
|
+
const [row] = await aggregation.exec();
|
|
1006
|
+
return row?.n ?? 0;
|
|
1007
|
+
}
|
|
1008
|
+
const groupId = groupCols.reduce((acc, field) => {
|
|
1009
|
+
acc[field] = `$${field}`;
|
|
1010
|
+
return acc;
|
|
1011
|
+
}, {});
|
|
1012
|
+
const pipeline = [];
|
|
1013
|
+
if (Object.keys(match).length > 0) pipeline.push({ $match: match });
|
|
1014
|
+
pipeline.push({ $group: { _id: groupId } });
|
|
1015
|
+
pipeline.push({ $count: "n" });
|
|
1016
|
+
const aggregation = Model.aggregate(pipeline);
|
|
1017
|
+
if (options.session) aggregation.session(options.session);
|
|
1018
|
+
const [row] = await aggregation.exec();
|
|
1019
|
+
return row?.n ?? 0;
|
|
1020
|
+
}
|
|
1021
|
+
//#endregion
|
|
1022
|
+
//#region src/actions/aggregate-ir/execute.ts
|
|
1023
|
+
async function executeAgg(Model, req, options = {}) {
|
|
1024
|
+
const { pipeline } = buildAggPipeline(req);
|
|
1025
|
+
const aggregation = Model.aggregate(pipeline);
|
|
1026
|
+
if (options.session) aggregation.session(options.session);
|
|
1027
|
+
return await aggregation.exec();
|
|
1028
|
+
}
|
|
1029
|
+
//#endregion
|
|
792
1030
|
//#region src/query/AggregationBuilder.ts
|
|
793
1031
|
/**
|
|
794
1032
|
* Normalize SortSpec to MongoDB's strict format (1 | -1)
|
|
@@ -1375,7 +1613,117 @@ function isTransactionUnsupported(error) {
|
|
|
1375
1613
|
return message.includes("transaction numbers are only allowed on a replica set member") || message.includes("transaction is not supported");
|
|
1376
1614
|
}
|
|
1377
1615
|
//#endregion
|
|
1616
|
+
//#region src/tx-bound.ts
|
|
1617
|
+
/**
|
|
1618
|
+
* Map of session-aware method name → positional index of the `{ session }`
|
|
1619
|
+
* options bag. Keep this list in sync with the method signatures in
|
|
1620
|
+
* `Repository.ts` and the plugin method definitions. Unknown plugin
|
|
1621
|
+
* methods pass through unwrapped (callers use the standalone helper).
|
|
1622
|
+
*/
|
|
1623
|
+
const SESSION_OPTIONS_INDEX = Object.freeze({
|
|
1624
|
+
create: 1,
|
|
1625
|
+
update: 2,
|
|
1626
|
+
delete: 1,
|
|
1627
|
+
getById: 1,
|
|
1628
|
+
getAll: 1,
|
|
1629
|
+
createMany: 1,
|
|
1630
|
+
findAll: 1,
|
|
1631
|
+
getOrCreate: 2,
|
|
1632
|
+
count: 1,
|
|
1633
|
+
exists: 1,
|
|
1634
|
+
getByQuery: 1,
|
|
1635
|
+
getOne: 1,
|
|
1636
|
+
findOneAndUpdate: 2,
|
|
1637
|
+
distinct: 2,
|
|
1638
|
+
aggregate: 1,
|
|
1639
|
+
aggregatePaginate: 0,
|
|
1640
|
+
lookupPopulate: 0,
|
|
1641
|
+
upsert: 2,
|
|
1642
|
+
increment: 3,
|
|
1643
|
+
decrement: 3,
|
|
1644
|
+
multiplyField: 3,
|
|
1645
|
+
setMin: 3,
|
|
1646
|
+
setMax: 3,
|
|
1647
|
+
pushToArray: 3,
|
|
1648
|
+
pullFromArray: 3,
|
|
1649
|
+
addToSet: 3,
|
|
1650
|
+
setField: 3,
|
|
1651
|
+
unsetField: 2,
|
|
1652
|
+
renameField: 3,
|
|
1653
|
+
atomicUpdate: 2,
|
|
1654
|
+
updateMany: 2,
|
|
1655
|
+
deleteMany: 1,
|
|
1656
|
+
bulkWrite: 1,
|
|
1657
|
+
restore: 1,
|
|
1658
|
+
getDeleted: 1,
|
|
1659
|
+
addSubdocument: 3,
|
|
1660
|
+
getSubdocument: 3,
|
|
1661
|
+
updateSubdocument: 4,
|
|
1662
|
+
deleteSubdocument: 3,
|
|
1663
|
+
groupBy: 1,
|
|
1664
|
+
sum: 2,
|
|
1665
|
+
average: 2,
|
|
1666
|
+
min: 2,
|
|
1667
|
+
max: 2
|
|
1668
|
+
});
|
|
1669
|
+
/**
|
|
1670
|
+
* Build a session-threaded proxy over `outer`. The returned object has the
|
|
1671
|
+
* same method signatures, but every CRUD call auto-injects the supplied
|
|
1672
|
+
* session into the options bag. Non-CRUD properties (Model, modelName,
|
|
1673
|
+
* hook API, utility helpers) pass through.
|
|
1674
|
+
*/
|
|
1675
|
+
function createTxBoundRepo(outer, session) {
|
|
1676
|
+
return new Proxy(outer, { get(target, prop, receiver) {
|
|
1677
|
+
if (prop === "withTransaction") return () => {
|
|
1678
|
+
throw new Error("[mongokit] Nested withTransaction is not supported on a tx-bound repository. Reuse the outer `txRepo` directly, or collapse the nesting.");
|
|
1679
|
+
};
|
|
1680
|
+
const value = Reflect.get(target, prop, receiver);
|
|
1681
|
+
if (typeof value !== "function") return value;
|
|
1682
|
+
if (typeof prop === "symbol") return value.bind(target);
|
|
1683
|
+
if (prop.startsWith("_")) return value.bind(target);
|
|
1684
|
+
const optionsIndex = SESSION_OPTIONS_INDEX[prop];
|
|
1685
|
+
if (optionsIndex === void 0) return value.bind(target);
|
|
1686
|
+
return function txBoundMethod(...args) {
|
|
1687
|
+
while (args.length <= optionsIndex) args.push(void 0);
|
|
1688
|
+
const current = args[optionsIndex];
|
|
1689
|
+
if (current === void 0) args[optionsIndex] = { session };
|
|
1690
|
+
else if (typeof current === "object" && current !== null && !Array.isArray(current)) args[optionsIndex] = {
|
|
1691
|
+
...current,
|
|
1692
|
+
session
|
|
1693
|
+
};
|
|
1694
|
+
else return value.apply(target, args);
|
|
1695
|
+
return value.apply(target, args);
|
|
1696
|
+
};
|
|
1697
|
+
} });
|
|
1698
|
+
}
|
|
1699
|
+
//#endregion
|
|
1378
1700
|
//#region src/Repository.ts
|
|
1701
|
+
/**
|
|
1702
|
+
* Repository Pattern - Data Access Layer
|
|
1703
|
+
*
|
|
1704
|
+
* Event-driven, plugin-based abstraction for MongoDB operations
|
|
1705
|
+
* Inspired by Meta & Stripe's repository patterns
|
|
1706
|
+
*
|
|
1707
|
+
* @example
|
|
1708
|
+
* ```typescript
|
|
1709
|
+
* const userRepo = new Repository(UserModel, [
|
|
1710
|
+
* timestampPlugin(),
|
|
1711
|
+
* softDeletePlugin(),
|
|
1712
|
+
* ]);
|
|
1713
|
+
*
|
|
1714
|
+
* // Create
|
|
1715
|
+
* const user = await userRepo.create({ name: 'John', email: 'john@example.com' });
|
|
1716
|
+
*
|
|
1717
|
+
* // Read with pagination
|
|
1718
|
+
* const users = await userRepo.getAll({ page: 1, limit: 20, filters: { status: 'active' } });
|
|
1719
|
+
*
|
|
1720
|
+
* // Update
|
|
1721
|
+
* const updated = await userRepo.update(user._id, { name: 'John Doe' });
|
|
1722
|
+
*
|
|
1723
|
+
* // Delete
|
|
1724
|
+
* await userRepo.delete(user._id);
|
|
1725
|
+
* ```
|
|
1726
|
+
*/
|
|
1379
1727
|
function ensureLookupProjectionIncludesCursorFields(projection, sort) {
|
|
1380
1728
|
if (!projection || !sort) return projection;
|
|
1381
1729
|
if (!Object.values(projection).some((value) => value === 1)) return projection;
|
|
@@ -1384,163 +1732,80 @@ function ensureLookupProjectionIncludesCursorFields(projection, sort) {
|
|
|
1384
1732
|
return nextProjection;
|
|
1385
1733
|
}
|
|
1386
1734
|
/**
|
|
1387
|
-
*
|
|
1388
|
-
* Each tuple is `[mustComeFirst, mustComeAfter, reason]`.
|
|
1735
|
+
* Plugin phase priorities (lower = runs first).
|
|
1389
1736
|
*
|
|
1390
|
-
*
|
|
1391
|
-
*
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
"soft-delete",
|
|
1395
|
-
"batch-operations",
|
|
1396
|
-
"soft-delete must precede batch-operations so bulk deletes/updates see the soft-delete filter"
|
|
1397
|
-
], [
|
|
1398
|
-
"multi-tenant",
|
|
1399
|
-
"cache",
|
|
1400
|
-
"multi-tenant must precede cache so tenant scoping is baked into cache keys (prevents cross-tenant cache poisoning)"
|
|
1401
|
-
]];
|
|
1402
|
-
function pluginName(plugin) {
|
|
1403
|
-
if (!plugin || typeof plugin === "function") return void 0;
|
|
1404
|
-
const name = plugin.name;
|
|
1405
|
-
return typeof name === "string" ? name : void 0;
|
|
1406
|
-
}
|
|
1407
|
-
function validatePluginOrder(plugins, modelName, mode) {
|
|
1408
|
-
if (mode === "off") return;
|
|
1409
|
-
const names = plugins.map(pluginName);
|
|
1410
|
-
for (const [first, after, reason] of PLUGIN_ORDER_CONSTRAINTS) {
|
|
1411
|
-
const firstIdx = names.indexOf(first);
|
|
1412
|
-
const afterIdx = names.indexOf(after);
|
|
1413
|
-
if (firstIdx === -1 || afterIdx === -1) continue;
|
|
1414
|
-
if (firstIdx < afterIdx) continue;
|
|
1415
|
-
const message = `[mongokit] Repository "${modelName}": plugin order issue — ${reason}. Got: [..., '${after}' at index ${afterIdx}, '${first}' at index ${firstIdx}]. Swap them, or pass { pluginOrderChecks: 'off' } to silence.`;
|
|
1416
|
-
if (mode === "throw") throw new Error(message);
|
|
1417
|
-
warn(message);
|
|
1418
|
-
}
|
|
1419
|
-
}
|
|
1420
|
-
/**
|
|
1421
|
-
* Plugin phase priorities (lower = runs first)
|
|
1422
|
-
* Policy hooks (multi-tenant, soft-delete, validation) MUST run before cache
|
|
1423
|
-
* to ensure filters are injected before cache keys are computed.
|
|
1737
|
+
* Re-exported from `@classytic/repo-core/hooks` so mongokit 3.10 callers see
|
|
1738
|
+
* the same constant whether they import from mongokit or directly from
|
|
1739
|
+
* repo-core — priorities are identical (POLICY=100, CACHE=200,
|
|
1740
|
+
* OBSERVABILITY=300, DEFAULT=500) and the two refs are reference-equal.
|
|
1424
1741
|
*/
|
|
1425
|
-
const HOOK_PRIORITY =
|
|
1426
|
-
POLICY: 100,
|
|
1427
|
-
CACHE: 200,
|
|
1428
|
-
OBSERVABILITY: 300,
|
|
1429
|
-
DEFAULT: 500
|
|
1430
|
-
};
|
|
1742
|
+
const HOOK_PRIORITY$1 = HOOK_PRIORITY;
|
|
1431
1743
|
/**
|
|
1432
|
-
* Production-grade repository for MongoDB
|
|
1433
|
-
*
|
|
1744
|
+
* Production-grade repository for MongoDB.
|
|
1745
|
+
*
|
|
1746
|
+
* Extends `@classytic/repo-core/repository`'s `RepositoryBase` for the
|
|
1747
|
+
* driver-agnostic hook + plugin plumbing (context builder, priority-sorted
|
|
1748
|
+
* event engine, plugin-order validator) while layering every Mongo-specific
|
|
1749
|
+
* concern — populate, aggregate, lookup, keyset + cursor pagination, $near
|
|
1750
|
+
* rewriting — on top.
|
|
1751
|
+
*
|
|
1752
|
+
* Public surface is byte-stable with mongokit 3.9 (the `_hooks` property is
|
|
1753
|
+
* preserved as a live read-through getter; all CRUD, event, and plugin method
|
|
1754
|
+
* signatures unchanged). 3.10 is an internal re-plumbing release; consumers
|
|
1755
|
+
* upgrading from 3.9 need no code changes.
|
|
1434
1756
|
*/
|
|
1435
|
-
var Repository = class {
|
|
1757
|
+
var Repository = class extends RepositoryBase {
|
|
1436
1758
|
Model;
|
|
1759
|
+
/**
|
|
1760
|
+
* Mongoose model name. Duplicated on the instance for BC — arc / catalog /
|
|
1761
|
+
* user code read `repo.model`. `RepositoryBase.modelName` points to the
|
|
1762
|
+
* same string; prefer `modelName` in new code.
|
|
1763
|
+
*/
|
|
1437
1764
|
model;
|
|
1438
|
-
_hooks;
|
|
1439
1765
|
_pagination;
|
|
1440
|
-
_hookMode;
|
|
1441
1766
|
idField;
|
|
1442
1767
|
searchMode;
|
|
1443
1768
|
searchFields;
|
|
1444
1769
|
_hasTextIndex = null;
|
|
1445
1770
|
constructor(Model, plugins = [], paginationConfig = {}, options = {}) {
|
|
1771
|
+
super({
|
|
1772
|
+
name: Model.modelName,
|
|
1773
|
+
hooks: options.hooks ?? "async",
|
|
1774
|
+
pluginOrderChecks: "off"
|
|
1775
|
+
});
|
|
1776
|
+
validatePluginOrder(plugins, Model.modelName, options.pluginOrderChecks ?? "warn", (m) => {
|
|
1777
|
+
warn(m);
|
|
1778
|
+
});
|
|
1446
1779
|
this.Model = Model;
|
|
1447
1780
|
this.model = Model.modelName;
|
|
1448
|
-
this._hooks = /* @__PURE__ */ new Map();
|
|
1449
1781
|
this._pagination = new PaginationEngine(Model, paginationConfig);
|
|
1450
|
-
this._hookMode = options.hooks ?? "async";
|
|
1451
1782
|
this.idField = options.idField ?? "_id";
|
|
1452
1783
|
this.searchMode = options.searchMode ?? "text";
|
|
1453
1784
|
this.searchFields = options.searchFields;
|
|
1454
1785
|
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.`);
|
|
1455
|
-
|
|
1456
|
-
plugins.forEach((plugin) => {
|
|
1457
|
-
this.use(plugin);
|
|
1458
|
-
});
|
|
1459
|
-
}
|
|
1460
|
-
/**
|
|
1461
|
-
* Register a plugin
|
|
1462
|
-
*/
|
|
1463
|
-
use(plugin) {
|
|
1464
|
-
if (typeof plugin === "function") plugin(this);
|
|
1465
|
-
else if (plugin && typeof plugin.apply === "function") plugin.apply(this);
|
|
1466
|
-
return this;
|
|
1786
|
+
for (const plugin of plugins) this.use(plugin);
|
|
1467
1787
|
}
|
|
1468
1788
|
/**
|
|
1469
|
-
*
|
|
1789
|
+
* Live read-through view of the hook engine's listener registry.
|
|
1470
1790
|
*
|
|
1471
|
-
*
|
|
1472
|
-
*
|
|
1473
|
-
*
|
|
1474
|
-
*
|
|
1475
|
-
*
|
|
1476
|
-
|
|
1477
|
-
on(event, listener, options) {
|
|
1478
|
-
if (!this._hooks.has(event)) this._hooks.set(event, []);
|
|
1479
|
-
const hooks = this._hooks.get(event) ?? [];
|
|
1480
|
-
const priority = options?.priority ?? HOOK_PRIORITY.DEFAULT;
|
|
1481
|
-
hooks.push({
|
|
1482
|
-
listener,
|
|
1483
|
-
priority
|
|
1484
|
-
});
|
|
1485
|
-
hooks.sort((a, b) => a.priority - b.priority);
|
|
1486
|
-
return this;
|
|
1487
|
-
}
|
|
1488
|
-
/**
|
|
1489
|
-
* Remove a specific event listener
|
|
1490
|
-
*/
|
|
1491
|
-
off(event, listener) {
|
|
1492
|
-
const hooks = this._hooks.get(event);
|
|
1493
|
-
if (hooks) {
|
|
1494
|
-
const idx = hooks.findIndex((h) => h.listener === listener);
|
|
1495
|
-
if (idx !== -1) hooks.splice(idx, 1);
|
|
1496
|
-
}
|
|
1497
|
-
return this;
|
|
1498
|
-
}
|
|
1499
|
-
/**
|
|
1500
|
-
* Remove all listeners for an event, or all listeners entirely
|
|
1501
|
-
*/
|
|
1502
|
-
removeAllListeners(event) {
|
|
1503
|
-
if (event) this._hooks.delete(event);
|
|
1504
|
-
else this._hooks.clear();
|
|
1505
|
-
return this;
|
|
1506
|
-
}
|
|
1507
|
-
/**
|
|
1508
|
-
* Emit event (sync - for backwards compatibility)
|
|
1791
|
+
* Preserved as a public field on the Repository for back-compat with
|
|
1792
|
+
* mongokit ≤3.9 — existing tests and observability code pattern-match
|
|
1793
|
+
* `repo._hooks.get('before:getAll')` / `repo._hooks.size`. Each access
|
|
1794
|
+
* returns a fresh snapshot of the underlying HookEngine state.
|
|
1795
|
+
*
|
|
1796
|
+
* Read-only. To register listeners use `repo.on(event, listener, { priority })`.
|
|
1509
1797
|
*/
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
for (const { listener } of hooks) try {
|
|
1513
|
-
const result = listener(data);
|
|
1514
|
-
if (result && typeof result.then === "function") result.catch((error) => {
|
|
1515
|
-
if (event === "error:hook") return;
|
|
1516
|
-
const err = error instanceof Error ? error : new Error(String(error));
|
|
1517
|
-
this.emit("error:hook", {
|
|
1518
|
-
event,
|
|
1519
|
-
error: err
|
|
1520
|
-
});
|
|
1521
|
-
});
|
|
1522
|
-
} catch (error) {
|
|
1523
|
-
if (event === "error:hook") continue;
|
|
1524
|
-
const err = error instanceof Error ? error : new Error(String(error));
|
|
1525
|
-
this.emit("error:hook", {
|
|
1526
|
-
event,
|
|
1527
|
-
error: err
|
|
1528
|
-
});
|
|
1529
|
-
}
|
|
1798
|
+
get _hooks() {
|
|
1799
|
+
return this.hooks.listeners();
|
|
1530
1800
|
}
|
|
1531
1801
|
/**
|
|
1532
|
-
*
|
|
1802
|
+
* Fire-and-forget emit aliased through RepositoryBase's async path so
|
|
1803
|
+
* error hooks converted to/from sync mode still route uniformly. Mongokit
|
|
1804
|
+
* ≤3.9 made this a private method on its subclass; the base class exposes
|
|
1805
|
+
* it publicly and wraps it in `_emitHook` below for internal use.
|
|
1533
1806
|
*/
|
|
1534
|
-
async emitAsync(event, data) {
|
|
1535
|
-
const hooks = this._hooks.get(event) || [];
|
|
1536
|
-
for (const { listener } of hooks) await listener(data);
|
|
1537
|
-
}
|
|
1538
1807
|
async _emitHook(event, data) {
|
|
1539
|
-
|
|
1540
|
-
await this.emitAsync(event, data);
|
|
1541
|
-
return;
|
|
1542
|
-
}
|
|
1543
|
-
this.emit(event, data);
|
|
1808
|
+
await this.hooks.emitAccordingToMode(event, data);
|
|
1544
1809
|
}
|
|
1545
1810
|
async _emitErrorHook(event, data) {
|
|
1546
1811
|
try {
|
|
@@ -1627,11 +1892,13 @@ var Repository = class {
|
|
|
1627
1892
|
});
|
|
1628
1893
|
return result;
|
|
1629
1894
|
}
|
|
1895
|
+
const wantsThrow = context.throwOnNotFound === true || options.throwOnNotFound === true;
|
|
1630
1896
|
if (!isValidIdForType(id, getSchemaIdType(this.Model.schema))) {
|
|
1631
|
-
if (
|
|
1632
|
-
|
|
1897
|
+
if (wantsThrow) throw createError(404, "Document not found");
|
|
1898
|
+
return null;
|
|
1633
1899
|
}
|
|
1634
1900
|
const result = await getById(this.Model, id, context);
|
|
1901
|
+
if (!result && wantsThrow) throw createError(404, "Document not found");
|
|
1635
1902
|
await this._emitHook("after:getById", {
|
|
1636
1903
|
context,
|
|
1637
1904
|
result
|
|
@@ -1830,7 +2097,7 @@ var Repository = class {
|
|
|
1830
2097
|
};
|
|
1831
2098
|
const lookups = context.lookups ?? params.lookups;
|
|
1832
2099
|
if (lookups && lookups.length > 0) try {
|
|
1833
|
-
const
|
|
2100
|
+
const result = await this.lookupPopulate({
|
|
1834
2101
|
filters: query,
|
|
1835
2102
|
lookups,
|
|
1836
2103
|
sort: paginationOptions.sort,
|
|
@@ -1843,31 +2110,6 @@ var Repository = class {
|
|
|
1843
2110
|
collation: paginationOptions.collation,
|
|
1844
2111
|
countStrategy: context.countStrategy ?? params.countStrategy
|
|
1845
2112
|
});
|
|
1846
|
-
let result;
|
|
1847
|
-
if (lookupResult.next !== void 0) result = {
|
|
1848
|
-
method: "keyset",
|
|
1849
|
-
docs: lookupResult.data,
|
|
1850
|
-
limit: lookupResult.limit ?? limit,
|
|
1851
|
-
hasMore: lookupResult.hasMore ?? false,
|
|
1852
|
-
next: lookupResult.next ?? null
|
|
1853
|
-
};
|
|
1854
|
-
else {
|
|
1855
|
-
const total = lookupResult.total ?? 0;
|
|
1856
|
-
const resultLimit = lookupResult.limit ?? limit;
|
|
1857
|
-
const totalPages = Math.ceil(total / resultLimit);
|
|
1858
|
-
const currentPage = lookupResult.page ?? 1;
|
|
1859
|
-
const hasNext = lookupResult.hasMore !== void 0 ? lookupResult.hasMore : currentPage < totalPages;
|
|
1860
|
-
result = {
|
|
1861
|
-
method: "offset",
|
|
1862
|
-
docs: lookupResult.data,
|
|
1863
|
-
page: currentPage,
|
|
1864
|
-
limit: resultLimit,
|
|
1865
|
-
total,
|
|
1866
|
-
pages: totalPages,
|
|
1867
|
-
hasNext,
|
|
1868
|
-
hasPrev: currentPage > 1
|
|
1869
|
-
};
|
|
1870
|
-
}
|
|
1871
2113
|
await this._emitHook("after:getAll", {
|
|
1872
2114
|
context,
|
|
1873
2115
|
result
|
|
@@ -2002,16 +2244,25 @@ var Repository = class {
|
|
|
2002
2244
|
...options
|
|
2003
2245
|
});
|
|
2004
2246
|
try {
|
|
2005
|
-
|
|
2247
|
+
const wantsThrow = context.throwOnNotFound === true || options.throwOnNotFound === true;
|
|
2006
2248
|
const effectiveIdField = options.idField ?? this.idField;
|
|
2007
|
-
if (effectiveIdField
|
|
2008
|
-
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
|
|
2012
|
-
|
|
2013
|
-
|
|
2014
|
-
|
|
2249
|
+
if (effectiveIdField === "_id") {
|
|
2250
|
+
if (!isValidIdForType(id, getSchemaIdType(this.Model.schema))) {
|
|
2251
|
+
if (wantsThrow) throw createError(404, "Document not found");
|
|
2252
|
+
await this._emitHook("after:update", {
|
|
2253
|
+
context,
|
|
2254
|
+
result: null
|
|
2255
|
+
});
|
|
2256
|
+
return null;
|
|
2257
|
+
}
|
|
2258
|
+
}
|
|
2259
|
+
let result;
|
|
2260
|
+
if (effectiveIdField !== "_id") result = await updateByQuery(this.Model, {
|
|
2261
|
+
[effectiveIdField]: id,
|
|
2262
|
+
...context.query || {}
|
|
2263
|
+
}, context.data || data, context);
|
|
2264
|
+
else result = await update(this.Model, id, context.data || data, context);
|
|
2265
|
+
if (!result && wantsThrow) throw createError(404, "Document not found");
|
|
2015
2266
|
await this._emitHook("after:update", {
|
|
2016
2267
|
context,
|
|
2017
2268
|
result
|
|
@@ -2125,6 +2376,22 @@ var Repository = class {
|
|
|
2125
2376
|
return result;
|
|
2126
2377
|
}
|
|
2127
2378
|
const effectiveIdField = options.idField ?? this.idField;
|
|
2379
|
+
const wantsThrow = context.throwOnNotFound === true || options.throwOnNotFound === true;
|
|
2380
|
+
if (effectiveIdField === "_id") {
|
|
2381
|
+
if (!isValidIdForType(id, getSchemaIdType(this.Model.schema))) {
|
|
2382
|
+
if (wantsThrow) throw createError(404, "Document not found");
|
|
2383
|
+
const result = {
|
|
2384
|
+
success: false,
|
|
2385
|
+
message: "Document not found",
|
|
2386
|
+
id: String(id)
|
|
2387
|
+
};
|
|
2388
|
+
await this._emitHook("after:delete", {
|
|
2389
|
+
context,
|
|
2390
|
+
result
|
|
2391
|
+
});
|
|
2392
|
+
return result;
|
|
2393
|
+
}
|
|
2394
|
+
}
|
|
2128
2395
|
const deleteQuery = effectiveIdField !== "_id" ? {
|
|
2129
2396
|
[effectiveIdField]: id,
|
|
2130
2397
|
...context.query || {}
|
|
@@ -2133,6 +2400,7 @@ var Repository = class {
|
|
|
2133
2400
|
session: options.session,
|
|
2134
2401
|
query: context.query
|
|
2135
2402
|
});
|
|
2403
|
+
if (!result.success && wantsThrow) throw createError(404, "Document not found");
|
|
2136
2404
|
await this._emitHook("after:delete", {
|
|
2137
2405
|
context,
|
|
2138
2406
|
result
|
|
@@ -2147,14 +2415,24 @@ var Repository = class {
|
|
|
2147
2415
|
}
|
|
2148
2416
|
}
|
|
2149
2417
|
/**
|
|
2150
|
-
*
|
|
2151
|
-
*
|
|
2418
|
+
* Kit-native aggregation pipeline — takes a MongoDB stage array and
|
|
2419
|
+
* returns the raw pipeline output. Use this for `$lookup`, `$unwind`,
|
|
2420
|
+
* `$facet`, `$graphLookup`, and any other mongo-specific power
|
|
2421
|
+
* features that don't translate to SQL.
|
|
2422
|
+
*
|
|
2423
|
+
* For backend-portable aggregations (filter + group + measures +
|
|
2424
|
+
* sort + limit) use `aggregate(req: AggRequest)` instead — it
|
|
2425
|
+
* compiles the repo-core IR to the equivalent pipeline internally
|
|
2426
|
+
* and produces the same row shape sqlitekit emits.
|
|
2152
2427
|
*
|
|
2153
|
-
*
|
|
2154
|
-
*
|
|
2428
|
+
* Routes through the hook system for policy enforcement
|
|
2429
|
+
* (multi-tenant, soft-delete).
|
|
2430
|
+
*
|
|
2431
|
+
* @param pipeline - MongoDB aggregation stage array
|
|
2432
|
+
* @param options - Aggregation options including governance controls
|
|
2155
2433
|
*/
|
|
2156
|
-
async
|
|
2157
|
-
const context = await this._buildContext("
|
|
2434
|
+
async aggregatePipeline(pipeline, options = {}) {
|
|
2435
|
+
const context = await this._buildContext("aggregatePipeline", {
|
|
2158
2436
|
pipeline,
|
|
2159
2437
|
...options
|
|
2160
2438
|
});
|
|
@@ -2172,13 +2450,13 @@ var Repository = class {
|
|
|
2172
2450
|
if (options.readConcern) aggregation.option({ readConcern: options.readConcern });
|
|
2173
2451
|
if (options.collation) aggregation.collation(options.collation);
|
|
2174
2452
|
const result = await aggregation.exec();
|
|
2175
|
-
await this._emitHook("after:
|
|
2453
|
+
await this._emitHook("after:aggregatePipeline", {
|
|
2176
2454
|
context,
|
|
2177
2455
|
result
|
|
2178
2456
|
});
|
|
2179
2457
|
return result;
|
|
2180
2458
|
} catch (error) {
|
|
2181
|
-
await this._emitErrorHook("error:
|
|
2459
|
+
await this._emitErrorHook("error:aggregatePipeline", {
|
|
2182
2460
|
context,
|
|
2183
2461
|
error
|
|
2184
2462
|
});
|
|
@@ -2186,14 +2464,19 @@ var Repository = class {
|
|
|
2186
2464
|
}
|
|
2187
2465
|
}
|
|
2188
2466
|
/**
|
|
2189
|
-
*
|
|
2190
|
-
*
|
|
2467
|
+
* Paginated kit-native aggregation pipeline. Same mongo-specific
|
|
2468
|
+
* scope as `aggregatePipeline` — reach for this when you need
|
|
2469
|
+
* `$lookup` / `$unwind` / `$facet` alongside pagination.
|
|
2470
|
+
*
|
|
2471
|
+
* Prefer `aggregatePaginate(req: AggPaginationRequest)` for
|
|
2472
|
+
* cross-backend dashboard code; that method compiles the portable
|
|
2473
|
+
* IR to an equivalent pipeline.
|
|
2191
2474
|
*
|
|
2192
|
-
* Policy hooks (multi-tenant, soft-delete) inject context.filters
|
|
2193
|
-
* prepended as a
|
|
2475
|
+
* Policy hooks (multi-tenant, soft-delete) inject context.filters
|
|
2476
|
+
* which are prepended as a `$match` stage.
|
|
2194
2477
|
*/
|
|
2195
|
-
async
|
|
2196
|
-
const context = await this._buildContext("
|
|
2478
|
+
async aggregatePipelinePaginate(options = {}) {
|
|
2479
|
+
const context = await this._buildContext("aggregatePipelinePaginate", options);
|
|
2197
2480
|
const finalPipeline = [...context.pipeline || options.pipeline || []];
|
|
2198
2481
|
if (context.filters && Object.keys(context.filters).length > 0) finalPipeline.unshift({ $match: context.filters });
|
|
2199
2482
|
const aggOptions = {
|
|
@@ -2202,6 +2485,116 @@ var Repository = class {
|
|
|
2202
2485
|
};
|
|
2203
2486
|
try {
|
|
2204
2487
|
const result = await this._pagination.aggregatePaginate(aggOptions);
|
|
2488
|
+
await this._emitHook("after:aggregatePipelinePaginate", {
|
|
2489
|
+
context,
|
|
2490
|
+
result
|
|
2491
|
+
});
|
|
2492
|
+
return result;
|
|
2493
|
+
} catch (error) {
|
|
2494
|
+
await this._emitErrorHook("error:aggregatePipelinePaginate", {
|
|
2495
|
+
context,
|
|
2496
|
+
error
|
|
2497
|
+
});
|
|
2498
|
+
throw this._handleError(error);
|
|
2499
|
+
}
|
|
2500
|
+
}
|
|
2501
|
+
/**
|
|
2502
|
+
* Portable aggregation. Compiles the repo-core `AggRequest` IR to
|
|
2503
|
+
* `[{$match}, {$group}, {$addFields}, {$project}, {$match}, {$sort},
|
|
2504
|
+
* {$skip}, {$limit}]` against this repo's collection. Output rows
|
|
2505
|
+
* carry one key per `groupBy` column plus one key per measure alias
|
|
2506
|
+
* — identical shape to sqlitekit's `aggregate(req)`, so dashboards
|
|
2507
|
+
* work unchanged across backends.
|
|
2508
|
+
*
|
|
2509
|
+
* Without `groupBy`: returns a single-row result with just the
|
|
2510
|
+
* measures (scalar aggregation). Pass
|
|
2511
|
+
* `{ measures: { total: { op: 'sum', field: 'amount' } } }` for a
|
|
2512
|
+
* simple summary.
|
|
2513
|
+
*
|
|
2514
|
+
* For mongo-specific power features (`$lookup`, `$unwind`, `$facet`,
|
|
2515
|
+
* `$graphLookup`, window operators) reach for `aggregatePipeline`
|
|
2516
|
+
* — that's the kit-native escape hatch.
|
|
2517
|
+
*
|
|
2518
|
+
* Routes through the hook system for policy enforcement
|
|
2519
|
+
* (multi-tenant, soft-delete) — plugins inject their scope into
|
|
2520
|
+
* `context.filter`, which is merged into the pre-aggregate `$match`
|
|
2521
|
+
* before compilation.
|
|
2522
|
+
*/
|
|
2523
|
+
async aggregate(req) {
|
|
2524
|
+
const context = await this._buildContext("aggregate", { aggRequest: req });
|
|
2525
|
+
try {
|
|
2526
|
+
const finalReq = this._injectPolicyScopeIntoAgg(req, context);
|
|
2527
|
+
const result = { rows: await executeAgg(this.Model, finalReq, { session: context.session }) };
|
|
2528
|
+
await this._emitHook("after:aggregate", {
|
|
2529
|
+
context,
|
|
2530
|
+
result
|
|
2531
|
+
});
|
|
2532
|
+
return result;
|
|
2533
|
+
} catch (error) {
|
|
2534
|
+
await this._emitErrorHook("error:aggregate", {
|
|
2535
|
+
context,
|
|
2536
|
+
error
|
|
2537
|
+
});
|
|
2538
|
+
throw this._handleError(error);
|
|
2539
|
+
}
|
|
2540
|
+
}
|
|
2541
|
+
/**
|
|
2542
|
+
* Offset-paginated portable aggregation. Same IR as `aggregate`,
|
|
2543
|
+
* wrapped in the standard `OffsetPaginationResultCore` envelope so
|
|
2544
|
+
* UI code paginates aggregated dashboards with the same primitives
|
|
2545
|
+
* as raw document lists.
|
|
2546
|
+
*
|
|
2547
|
+
* `countStrategy: 'none'` skips the second round-trip that computes
|
|
2548
|
+
* `total`; the envelope reports `total: 0`, `pages: 0`, and derives
|
|
2549
|
+
* `hasNext` from a `LIMIT N+1` peek on the data pipeline.
|
|
2550
|
+
*/
|
|
2551
|
+
async aggregatePaginate(req) {
|
|
2552
|
+
const context = await this._buildContext("aggregatePaginate", { aggRequest: req });
|
|
2553
|
+
const page = Math.max(1, req.page ?? 1);
|
|
2554
|
+
const limit = Math.max(1, Math.min(req.limit ?? 20, 1e3));
|
|
2555
|
+
const countStrategy = req.countStrategy ?? "exact";
|
|
2556
|
+
const offset = (page - 1) * limit;
|
|
2557
|
+
const session = context.session;
|
|
2558
|
+
try {
|
|
2559
|
+
const finalReq = this._injectPolicyScopeIntoAgg(req, context);
|
|
2560
|
+
if (countStrategy === "none") {
|
|
2561
|
+
const peek = await executeAgg(this.Model, {
|
|
2562
|
+
...finalReq,
|
|
2563
|
+
limit: limit + 1,
|
|
2564
|
+
offset
|
|
2565
|
+
}, session ? { session } : {});
|
|
2566
|
+
const hasNext = peek.length > limit;
|
|
2567
|
+
const result = {
|
|
2568
|
+
method: "offset",
|
|
2569
|
+
docs: hasNext ? peek.slice(0, limit) : peek,
|
|
2570
|
+
page,
|
|
2571
|
+
limit,
|
|
2572
|
+
total: 0,
|
|
2573
|
+
pages: 0,
|
|
2574
|
+
hasNext,
|
|
2575
|
+
hasPrev: page > 1
|
|
2576
|
+
};
|
|
2577
|
+
await this._emitHook("after:aggregatePaginate", {
|
|
2578
|
+
context,
|
|
2579
|
+
result
|
|
2580
|
+
});
|
|
2581
|
+
return result;
|
|
2582
|
+
}
|
|
2583
|
+
const [docs, total] = await Promise.all([executeAgg(this.Model, {
|
|
2584
|
+
...finalReq,
|
|
2585
|
+
limit,
|
|
2586
|
+
offset
|
|
2587
|
+
}, session ? { session } : {}), countAggGroups(this.Model, finalReq, session ? { session } : {})]);
|
|
2588
|
+
const result = {
|
|
2589
|
+
method: "offset",
|
|
2590
|
+
docs,
|
|
2591
|
+
page,
|
|
2592
|
+
limit,
|
|
2593
|
+
total,
|
|
2594
|
+
pages: Math.max(1, Math.ceil(total / limit)),
|
|
2595
|
+
hasNext: page * limit < total,
|
|
2596
|
+
hasPrev: page > 1
|
|
2597
|
+
};
|
|
2205
2598
|
await this._emitHook("after:aggregatePaginate", {
|
|
2206
2599
|
context,
|
|
2207
2600
|
result
|
|
@@ -2216,6 +2609,29 @@ var Repository = class {
|
|
|
2216
2609
|
}
|
|
2217
2610
|
}
|
|
2218
2611
|
/**
|
|
2612
|
+
* Merge policy-hook-injected filters (multi-tenant scope,
|
|
2613
|
+
* soft-delete) into the pre-aggregate `filter` slot of the request.
|
|
2614
|
+
* Plugins speak mongo-query shape (`context.query` / `context.filters`);
|
|
2615
|
+
* we merge those with the portable `req.filter` using `$and`.
|
|
2616
|
+
*
|
|
2617
|
+
* When the caller's filter is a Filter IR node we don't unwrap it —
|
|
2618
|
+
* the pipeline compiler does Filter→mongo translation. Merging here
|
|
2619
|
+
* as a mongo-shaped `$and` is safe because `compileFilterToMongo`
|
|
2620
|
+
* passes through already-mongo queries unchanged.
|
|
2621
|
+
*/
|
|
2622
|
+
_injectPolicyScopeIntoAgg(req, context) {
|
|
2623
|
+
const scopeCandidates = [];
|
|
2624
|
+
for (const candidate of [context.filters, context.query]) if (candidate && typeof candidate === "object" && Object.keys(candidate).length > 0) scopeCandidates.push(candidate);
|
|
2625
|
+
if (scopeCandidates.length === 0) return req;
|
|
2626
|
+
const parts = [...scopeCandidates];
|
|
2627
|
+
if (req.filter) parts.push(req.filter);
|
|
2628
|
+
const merged = parts.length === 1 ? parts[0] : { $and: parts };
|
|
2629
|
+
return {
|
|
2630
|
+
...req,
|
|
2631
|
+
filter: merged
|
|
2632
|
+
};
|
|
2633
|
+
}
|
|
2634
|
+
/**
|
|
2219
2635
|
* Get distinct values
|
|
2220
2636
|
* Routes through hook system for policy enforcement (multi-tenant, soft-delete)
|
|
2221
2637
|
*/
|
|
@@ -2336,18 +2752,18 @@ var Repository = class {
|
|
|
2336
2752
|
const hasMore = docs.length > limit;
|
|
2337
2753
|
if (hasMore) docs.pop();
|
|
2338
2754
|
const primaryField = getPrimaryField(normalizedSort);
|
|
2339
|
-
const
|
|
2755
|
+
const result = {
|
|
2756
|
+
method: "keyset",
|
|
2757
|
+
docs,
|
|
2758
|
+
limit,
|
|
2759
|
+
hasMore,
|
|
2760
|
+
next: hasMore && docs.length > 0 ? encodeCursor(docs[docs.length - 1], primaryField, normalizedSort, cursorVersion) : null
|
|
2761
|
+
};
|
|
2340
2762
|
await this._emitHook("after:lookupPopulate", {
|
|
2341
2763
|
context,
|
|
2342
|
-
result
|
|
2764
|
+
result
|
|
2343
2765
|
});
|
|
2344
|
-
return
|
|
2345
|
-
data: docs,
|
|
2346
|
-
total: 0,
|
|
2347
|
-
limit,
|
|
2348
|
-
next: nextCursor,
|
|
2349
|
-
hasMore
|
|
2350
|
-
};
|
|
2766
|
+
return result;
|
|
2351
2767
|
}
|
|
2352
2768
|
const page = pageFromContext ?? 1;
|
|
2353
2769
|
const skip = (page - 1) * limit;
|
|
@@ -2364,17 +2780,21 @@ var Repository = class {
|
|
|
2364
2780
|
const docs = await aggregation;
|
|
2365
2781
|
const hasNext = docs.length > limit;
|
|
2366
2782
|
if (hasNext) docs.pop();
|
|
2367
|
-
|
|
2368
|
-
|
|
2369
|
-
|
|
2370
|
-
});
|
|
2371
|
-
return {
|
|
2372
|
-
data: docs,
|
|
2373
|
-
total: 0,
|
|
2783
|
+
const result = {
|
|
2784
|
+
method: "offset",
|
|
2785
|
+
docs,
|
|
2374
2786
|
page,
|
|
2375
2787
|
limit,
|
|
2376
|
-
|
|
2788
|
+
total: 0,
|
|
2789
|
+
pages: 0,
|
|
2790
|
+
hasNext,
|
|
2791
|
+
hasPrev: page > 1
|
|
2377
2792
|
};
|
|
2793
|
+
await this._emitHook("after:lookupPopulate", {
|
|
2794
|
+
context,
|
|
2795
|
+
result
|
|
2796
|
+
});
|
|
2797
|
+
return result;
|
|
2378
2798
|
}
|
|
2379
2799
|
dataPipeline.push({ $skip: skip }, { $limit: limit });
|
|
2380
2800
|
appendLookupStages(dataPipeline);
|
|
@@ -2388,22 +2808,26 @@ var Repository = class {
|
|
|
2388
2808
|
const aggregation = this.Model.aggregate(pipeline).session(session || null);
|
|
2389
2809
|
if (collation) aggregation.collation(collation);
|
|
2390
2810
|
if (readPref) aggregation.read(readPref);
|
|
2391
|
-
const
|
|
2811
|
+
const facetResult = (await aggregation)[0] || {
|
|
2392
2812
|
metadata: [],
|
|
2393
2813
|
data: []
|
|
2394
2814
|
};
|
|
2395
|
-
const total =
|
|
2396
|
-
const
|
|
2815
|
+
const total = facetResult.metadata[0]?.total || 0;
|
|
2816
|
+
const result = {
|
|
2817
|
+
method: "offset",
|
|
2818
|
+
docs: facetResult.data || [],
|
|
2819
|
+
page,
|
|
2820
|
+
limit,
|
|
2821
|
+
total,
|
|
2822
|
+
pages: Math.max(1, Math.ceil(total / limit)),
|
|
2823
|
+
hasNext: page * limit < total,
|
|
2824
|
+
hasPrev: page > 1
|
|
2825
|
+
};
|
|
2397
2826
|
await this._emitHook("after:lookupPopulate", {
|
|
2398
2827
|
context,
|
|
2399
|
-
result
|
|
2828
|
+
result
|
|
2400
2829
|
});
|
|
2401
|
-
return
|
|
2402
|
-
data,
|
|
2403
|
-
total,
|
|
2404
|
-
page,
|
|
2405
|
-
limit
|
|
2406
|
-
};
|
|
2830
|
+
return result;
|
|
2407
2831
|
} catch (error) {
|
|
2408
2832
|
await this._emitErrorHook("error:lookupPopulate", {
|
|
2409
2833
|
context,
|
|
@@ -2454,37 +2878,58 @@ var Repository = class {
|
|
|
2454
2878
|
return new LookupBuilder(from);
|
|
2455
2879
|
}
|
|
2456
2880
|
/**
|
|
2457
|
-
* Execute callback within a transaction with automatic retry on
|
|
2881
|
+
* Execute callback within a MongoDB transaction with automatic retry on
|
|
2882
|
+
* transient failures.
|
|
2458
2883
|
*
|
|
2459
|
-
*
|
|
2460
|
-
*
|
|
2884
|
+
* **BREAKING (3.10):** the callback now receives a **tx-bound repository**,
|
|
2885
|
+
* not a raw `ClientSession`. This matches repo-core's `StandardRepo.withTransaction`
|
|
2886
|
+
* contract that every kit (sqlitekit, pgkit, prismakit) already implements
|
|
2887
|
+
* — so cross-kit plugins and apps are portable. Session threading happens
|
|
2888
|
+
* transparently: every CRUD call on `txRepo` auto-injects the session into
|
|
2889
|
+
* its options bag, including plugin-added methods (`upsert`, `increment`,
|
|
2890
|
+
* `updateMany`, `restore`, ...). Nested `txRepo.withTransaction(...)` throws.
|
|
2461
2891
|
*
|
|
2462
|
-
*
|
|
2463
|
-
*
|
|
2464
|
-
*
|
|
2892
|
+
* For raw-session cross-repo workflows — multiple repositories coordinating
|
|
2893
|
+
* writes — use the standalone `withTransaction(connection, fn)` export
|
|
2894
|
+
* from `@classytic/mongokit`. That helper remains session-based by design.
|
|
2465
2895
|
*
|
|
2466
|
-
* @param callback - Receives a `
|
|
2896
|
+
* @param callback - Receives a tx-bound `Repository<TDoc>` — call its
|
|
2897
|
+
* methods directly, no need to pass `{ session }` manually
|
|
2467
2898
|
* @param options.allowFallback - Run without transaction on standalone MongoDB (default: false)
|
|
2468
2899
|
* @param options.onFallback - Called when falling back to non-transactional execution
|
|
2469
2900
|
* @param options.transactionOptions - MongoDB driver transaction options (readConcern, writeConcern, etc.)
|
|
2470
2901
|
*
|
|
2471
2902
|
* @example
|
|
2472
2903
|
* ```typescript
|
|
2473
|
-
* const
|
|
2474
|
-
* const
|
|
2475
|
-
* await
|
|
2476
|
-
* return
|
|
2904
|
+
* const order = await repo.withTransaction(async (txRepo) => {
|
|
2905
|
+
* const created = await txRepo.create({ total: 100 });
|
|
2906
|
+
* await txRepo.update(created._id, { confirmed: true });
|
|
2907
|
+
* return created;
|
|
2477
2908
|
* });
|
|
2478
2909
|
*
|
|
2479
2910
|
* // With fallback for standalone/dev environments
|
|
2480
|
-
* await repo.withTransaction(
|
|
2481
|
-
*
|
|
2482
|
-
*
|
|
2911
|
+
* await repo.withTransaction(
|
|
2912
|
+
* async (txRepo) => {
|
|
2913
|
+
* await txRepo.create(doc);
|
|
2914
|
+
* },
|
|
2915
|
+
* {
|
|
2916
|
+
* allowFallback: true,
|
|
2917
|
+
* onFallback: (err) => logger.warn('Running without transaction', err),
|
|
2918
|
+
* },
|
|
2919
|
+
* );
|
|
2920
|
+
*
|
|
2921
|
+
* // Cross-repo with explicit session:
|
|
2922
|
+
* import { withTransaction } from '@classytic/mongokit';
|
|
2923
|
+
* await withTransaction(mongoose.connection, async (session) => {
|
|
2924
|
+
* const order = await orderRepo.create(data, { session });
|
|
2925
|
+
* await inventoryRepo.decrement(..., { session });
|
|
2483
2926
|
* });
|
|
2484
2927
|
* ```
|
|
2485
2928
|
*/
|
|
2486
2929
|
async withTransaction(callback, options = {}) {
|
|
2487
|
-
return withTransaction(this.Model.db,
|
|
2930
|
+
return withTransaction(this.Model.db, async (session) => {
|
|
2931
|
+
return callback(createTxBoundRepo(this, session));
|
|
2932
|
+
}, options);
|
|
2488
2933
|
}
|
|
2489
2934
|
/**
|
|
2490
2935
|
* Execute custom query with event emission
|
|
@@ -2508,7 +2953,12 @@ var Repository = class {
|
|
|
2508
2953
|
}
|
|
2509
2954
|
}
|
|
2510
2955
|
/**
|
|
2511
|
-
* Build operation context and run before
|
|
2956
|
+
* Build operation context and run before-hooks (sorted by priority).
|
|
2957
|
+
*
|
|
2958
|
+
* Narrows `RepositoryBase._buildContext`'s return type to mongokit's
|
|
2959
|
+
* richer `RepositoryContext` (which carries `softDeleted`, `_cacheHit`,
|
|
2960
|
+
* populate / readPreference / session, etc.). Runtime is inherited from
|
|
2961
|
+
* the base class — this override is purely a type-narrowing shim.
|
|
2512
2962
|
*
|
|
2513
2963
|
* Hook execution order is deterministic:
|
|
2514
2964
|
* 1. POLICY (100) — tenant isolation, soft-delete filtering, validation
|
|
@@ -2517,15 +2967,29 @@ var Repository = class {
|
|
|
2517
2967
|
* 4. DEFAULT (500) — user-registered hooks
|
|
2518
2968
|
*/
|
|
2519
2969
|
async _buildContext(operation, options) {
|
|
2520
|
-
const
|
|
2521
|
-
|
|
2522
|
-
|
|
2523
|
-
|
|
2524
|
-
|
|
2525
|
-
|
|
2526
|
-
|
|
2527
|
-
|
|
2528
|
-
|
|
2970
|
+
const normalized = this._normalizeFilterSlots(options);
|
|
2971
|
+
return await super._buildContext(operation, normalized);
|
|
2972
|
+
}
|
|
2973
|
+
/**
|
|
2974
|
+
* Walk the well-known filter-carrying keys (`query`, `filters`,
|
|
2975
|
+
* `having`) and route any Filter IR through `compileFilterToMongo`.
|
|
2976
|
+
* Plain Mongo queries pass through untouched — `isFilter` is the
|
|
2977
|
+
* discriminator. Returns a shallow clone; never mutates input.
|
|
2978
|
+
*/
|
|
2979
|
+
_normalizeFilterSlots(options) {
|
|
2980
|
+
let cloned = null;
|
|
2981
|
+
for (const key of [
|
|
2982
|
+
"query",
|
|
2983
|
+
"filters",
|
|
2984
|
+
"having"
|
|
2985
|
+
]) {
|
|
2986
|
+
const value = options[key];
|
|
2987
|
+
if (value === void 0 || value === null) continue;
|
|
2988
|
+
if (!isFilter(value)) continue;
|
|
2989
|
+
cloned ??= { ...options };
|
|
2990
|
+
cloned[key] = compileFilterToMongo(value);
|
|
2991
|
+
}
|
|
2992
|
+
return cloned ?? options;
|
|
2529
2993
|
}
|
|
2530
2994
|
/**
|
|
2531
2995
|
* Resolve the effective sort for getAll. Handles $near conflict detection
|
|
@@ -2602,6 +3066,38 @@ var Repository = class {
|
|
|
2602
3066
|
return [populate];
|
|
2603
3067
|
}
|
|
2604
3068
|
/**
|
|
3069
|
+
* Classify a driver error as an authoritative duplicate-key conflict.
|
|
3070
|
+
*
|
|
3071
|
+
* Used by arc's outbox / idempotency adapters to distinguish
|
|
3072
|
+
* "write already landed (idempotent no-op)" from "transient DB error
|
|
3073
|
+
* (must retry)". Every backend signals duplicate keys differently —
|
|
3074
|
+
* MongoDB `code: 11000`, Prisma `P2002`, Postgres `23505` — so this
|
|
3075
|
+
* predicate lives on the kit that knows its driver. Cross-driver
|
|
3076
|
+
* adapters then depend only on the boolean outcome, not the shape of
|
|
3077
|
+
* the underlying error.
|
|
3078
|
+
*
|
|
3079
|
+
* Deliberately narrow: matches ONLY `code === 11000` and
|
|
3080
|
+
* `codeName === 'DuplicateKey'`. Does NOT match
|
|
3081
|
+
* `err.name === 'MongoServerError'`, which is also true for
|
|
3082
|
+
* WriteConflict, NotWritablePrimary, ExceededTimeLimit, and every
|
|
3083
|
+
* other server-side error — treating those as duplicate keys would
|
|
3084
|
+
* silently swallow transactional retries.
|
|
3085
|
+
*
|
|
3086
|
+
* @example
|
|
3087
|
+
* try {
|
|
3088
|
+
* await outboxRepo.create(event);
|
|
3089
|
+
* } catch (err) {
|
|
3090
|
+
* if (outboxRepo.isDuplicateKeyError(err)) {
|
|
3091
|
+
* // Idempotent no-op — event was already written by a prior attempt.
|
|
3092
|
+
* return;
|
|
3093
|
+
* }
|
|
3094
|
+
* throw err; // transient DB error — upstream retries
|
|
3095
|
+
* }
|
|
3096
|
+
*/
|
|
3097
|
+
isDuplicateKeyError(err) {
|
|
3098
|
+
return isDuplicateKeyError(err);
|
|
3099
|
+
}
|
|
3100
|
+
/**
|
|
2605
3101
|
* Handle errors with proper HTTP status codes
|
|
2606
3102
|
*/
|
|
2607
3103
|
_handleError(error) {
|
|
@@ -2640,7 +3136,7 @@ var Repository = class {
|
|
|
2640
3136
|
* adapter: {
|
|
2641
3137
|
* async get(key) { return JSON.parse(await redis.get(key) || 'null'); },
|
|
2642
3138
|
* async set(key, value, ttl) { await redis.setex(key, ttl, JSON.stringify(value)); },
|
|
2643
|
-
* async
|
|
3139
|
+
* async delete(key) { await redis.del(key); },
|
|
2644
3140
|
* async clear(pattern) {
|
|
2645
3141
|
* const keys = await redis.keys(pattern || '*');
|
|
2646
3142
|
* if (keys.length) await redis.del(...keys);
|
|
@@ -2750,16 +3246,16 @@ function cachePlugin(options) {
|
|
|
2750
3246
|
}
|
|
2751
3247
|
/**
|
|
2752
3248
|
* Invalidate a specific document by ID (all shape variants).
|
|
2753
|
-
* Deletes every tracked shape-variant key individually via
|
|
3249
|
+
* Deletes every tracked shape-variant key individually via delete(),
|
|
2754
3250
|
* so adapters without pattern-based clear() still get full invalidation.
|
|
2755
3251
|
*/
|
|
2756
3252
|
async function invalidateById(id) {
|
|
2757
3253
|
try {
|
|
2758
3254
|
const baseKey = byIdKey(config.prefix, model, id);
|
|
2759
|
-
await config.adapter.
|
|
3255
|
+
await config.adapter.delete(baseKey);
|
|
2760
3256
|
const trackedKeys = byIdKeyRegistry.get(id);
|
|
2761
3257
|
if (trackedKeys) {
|
|
2762
|
-
for (const key of trackedKeys) if (key !== baseKey) await config.adapter.
|
|
3258
|
+
for (const key of trackedKeys) if (key !== baseKey) await config.adapter.delete(key);
|
|
2763
3259
|
byIdKeyRegistry.delete(id);
|
|
2764
3260
|
}
|
|
2765
3261
|
stats.invalidations++;
|
|
@@ -2798,7 +3294,7 @@ function cachePlugin(options) {
|
|
|
2798
3294
|
log(`Cache error for getById:`, e);
|
|
2799
3295
|
stats.errors++;
|
|
2800
3296
|
}
|
|
2801
|
-
}, { priority: HOOK_PRIORITY.CACHE });
|
|
3297
|
+
}, { priority: HOOK_PRIORITY$1.CACHE });
|
|
2802
3298
|
/**
|
|
2803
3299
|
* before:getByQuery - Check cache for single-doc query
|
|
2804
3300
|
* Runs at CACHE priority (200) — after policy hooks inject filters
|
|
@@ -2829,7 +3325,7 @@ function cachePlugin(options) {
|
|
|
2829
3325
|
log(`Cache error for getByQuery:`, e);
|
|
2830
3326
|
stats.errors++;
|
|
2831
3327
|
}
|
|
2832
|
-
}, { priority: HOOK_PRIORITY.CACHE });
|
|
3328
|
+
}, { priority: HOOK_PRIORITY$1.CACHE });
|
|
2833
3329
|
/**
|
|
2834
3330
|
* before:getOne - Same cache logic as getByQuery (single-doc)
|
|
2835
3331
|
*/
|
|
@@ -2857,7 +3353,7 @@ function cachePlugin(options) {
|
|
|
2857
3353
|
log(`Cache error for getOne:`, e);
|
|
2858
3354
|
stats.errors++;
|
|
2859
3355
|
}
|
|
2860
|
-
}, { priority: HOOK_PRIORITY.CACHE });
|
|
3356
|
+
}, { priority: HOOK_PRIORITY$1.CACHE });
|
|
2861
3357
|
/**
|
|
2862
3358
|
* before:getAll - Check cache for list query
|
|
2863
3359
|
* Runs at CACHE priority (200) — after policy hooks inject filters
|
|
@@ -2905,7 +3401,7 @@ function cachePlugin(options) {
|
|
|
2905
3401
|
log(`Cache error for getAll:`, e);
|
|
2906
3402
|
stats.errors++;
|
|
2907
3403
|
}
|
|
2908
|
-
}, { priority: HOOK_PRIORITY.CACHE });
|
|
3404
|
+
}, { priority: HOOK_PRIORITY$1.CACHE });
|
|
2909
3405
|
/**
|
|
2910
3406
|
* after:getById - Cache the result
|
|
2911
3407
|
*/
|
|
@@ -4077,7 +4573,7 @@ function multiTenantPlugin(options = {}) {
|
|
|
4077
4573
|
}
|
|
4078
4574
|
break;
|
|
4079
4575
|
}
|
|
4080
|
-
}, { priority: HOOK_PRIORITY.POLICY });
|
|
4576
|
+
}, { priority: HOOK_PRIORITY$1.POLICY });
|
|
4081
4577
|
}
|
|
4082
4578
|
}
|
|
4083
4579
|
};
|
|
@@ -4255,7 +4751,7 @@ function softDeletePlugin(options = {}) {
|
|
|
4255
4751
|
}
|
|
4256
4752
|
context.softDeleted = true;
|
|
4257
4753
|
}
|
|
4258
|
-
}, { priority: HOOK_PRIORITY.POLICY });
|
|
4754
|
+
}, { priority: HOOK_PRIORITY$1.POLICY });
|
|
4259
4755
|
const injectDeleteFilterToFilters = (context) => {
|
|
4260
4756
|
if (options.soft !== false) {
|
|
4261
4757
|
const deleteFilter = buildDeletedFilter(deletedField, filterMode, !!context.includeDeleted);
|
|
@@ -4284,8 +4780,8 @@ function softDeletePlugin(options = {}) {
|
|
|
4284
4780
|
const desc = OP_REGISTRY[op];
|
|
4285
4781
|
const isFilterableMutation = op === "findOneAndUpdate";
|
|
4286
4782
|
if (desc.mutates && !isFilterableMutation) continue;
|
|
4287
|
-
if (desc.policyKey === "filters") repo.on(`before:${op}`, injectDeleteFilterToFilters, { priority: HOOK_PRIORITY.POLICY });
|
|
4288
|
-
else if (desc.policyKey === "query") repo.on(`before:${op}`, injectDeleteFilterToQuery, { priority: HOOK_PRIORITY.POLICY });
|
|
4783
|
+
if (desc.policyKey === "filters") repo.on(`before:${op}`, injectDeleteFilterToFilters, { priority: HOOK_PRIORITY$1.POLICY });
|
|
4784
|
+
else if (desc.policyKey === "query") repo.on(`before:${op}`, injectDeleteFilterToQuery, { priority: HOOK_PRIORITY$1.POLICY });
|
|
4289
4785
|
}
|
|
4290
4786
|
repo.on("before:updateMany", (context) => {
|
|
4291
4787
|
if (options.soft !== false) {
|
|
@@ -4295,7 +4791,7 @@ function softDeletePlugin(options = {}) {
|
|
|
4295
4791
|
...deleteFilter
|
|
4296
4792
|
};
|
|
4297
4793
|
}
|
|
4298
|
-
}, { priority: HOOK_PRIORITY.POLICY });
|
|
4794
|
+
}, { priority: HOOK_PRIORITY$1.POLICY });
|
|
4299
4795
|
repo.on("before:deleteMany", async (context) => {
|
|
4300
4796
|
if (context.deleteMode === "hard") return;
|
|
4301
4797
|
if (options.soft !== false) {
|
|
@@ -4307,7 +4803,7 @@ function softDeletePlugin(options = {}) {
|
|
|
4307
4803
|
await repo.Model.updateMany(finalQuery, { $set: { [deletedField]: /* @__PURE__ */ new Date() } }, { session: context.session });
|
|
4308
4804
|
context.softDeleted = true;
|
|
4309
4805
|
}
|
|
4310
|
-
}, { priority: HOOK_PRIORITY.POLICY });
|
|
4806
|
+
}, { priority: HOOK_PRIORITY$1.POLICY });
|
|
4311
4807
|
if (addRestoreMethod) {
|
|
4312
4808
|
const restoreMethod = async function(id, restoreOptions = {}) {
|
|
4313
4809
|
const context = await this._buildContext("restore", {
|
|
@@ -4704,4 +5200,4 @@ function uniqueField(field, errorMessage) {
|
|
|
4704
5200
|
};
|
|
4705
5201
|
}
|
|
4706
5202
|
//#endregion
|
|
4707
|
-
export { AuditTrailQuery as A, cachePlugin as C, withTransaction as D, isTransactionUnsupported as E, MUTATING_OPERATIONS as F, OP_REGISTRY as I, READ_OPERATIONS as L, auditLogPlugin as M, aggregateHelpersPlugin as N, AggregationBuilder as O, ALL_OPERATIONS as P, operationsByPolicyKey as R, cascadePlugin as S, Repository as T, customIdPlugin as _, uniqueField as a, prefixedId as b, createTenantContext as c, observabilityPlugin as d, multiTenantPlugin as f, elasticSearchPlugin as g, fieldFilterPlugin as h, requireField as i, auditTrailPlugin as j, batchOperationsPlugin as k, subdocumentPlugin as l, methodRegistryPlugin as m, blockIf as n, validationChainPlugin as o, mongoOperationsPlugin as p, immutableField as r, timestampPlugin as s, autoInject as t, softDeletePlugin as u, dateSequentialId as v, HOOK_PRIORITY as w, sequentialId as x, getNextSequence as y };
|
|
5203
|
+
export { AuditTrailQuery as A, cachePlugin as C, withTransaction as D, isTransactionUnsupported as E, MUTATING_OPERATIONS as F, OP_REGISTRY as I, READ_OPERATIONS as L, auditLogPlugin as M, aggregateHelpersPlugin as N, AggregationBuilder as O, ALL_OPERATIONS as P, operationsByPolicyKey as R, cascadePlugin as S, Repository as T, customIdPlugin as _, uniqueField as a, prefixedId as b, createTenantContext as c, observabilityPlugin as d, multiTenantPlugin as f, elasticSearchPlugin as g, fieldFilterPlugin as h, requireField as i, auditTrailPlugin as j, batchOperationsPlugin as k, subdocumentPlugin as l, methodRegistryPlugin as m, blockIf as n, validationChainPlugin as o, mongoOperationsPlugin as p, immutableField as r, timestampPlugin as s, autoInject as t, softDeletePlugin as u, dateSequentialId as v, HOOK_PRIORITY$1 as w, sequentialId as x, getNextSequence as y };
|