@classytic/mongokit 3.6.4 → 3.8.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-CunXkR9p.mjs → PaginationEngine-C2_tAvAD.mjs} +8 -5
- package/dist/actions/index.d.mts +1 -1
- package/dist/actions/index.mjs +1 -1
- package/dist/ai/index.d.mts +23 -1
- package/dist/ai/index.mjs +194 -12
- package/dist/{cursor-CHToazHy.mjs → cursor-J84rR6wG.mjs} +11 -7
- package/dist/error-Lw9GEAro.mjs +46 -0
- package/dist/{index-jzO5hMtF.d.mts → index-DlSAFIc9.d.mts} +10 -2
- package/dist/index.d.mts +97 -9
- package/dist/index.mjs +53 -12
- package/dist/logger-DJ7egMnd.mjs +30 -0
- package/dist/{mongooseToJsonSchema-BvD1NDl9.d.mts → mongooseToJsonSchema-BZaKOXXI.d.mts} +18 -2
- package/dist/{mongooseToJsonSchema-BlfkI9LC.mjs → mongooseToJsonSchema-DUwXPStK.mjs} +1 -1
- package/dist/pagination/PaginationEngine.d.mts +3 -1
- package/dist/pagination/PaginationEngine.mjs +1 -1
- package/dist/plugins/index.d.mts +2 -2
- package/dist/plugins/index.mjs +2 -2
- package/dist/query/primitives/coercion.d.mts +1 -1
- package/dist/query/primitives/coercion.mjs +1 -1
- package/dist/query/primitives/indexes.d.mts +1 -1
- package/dist/{sort-C-BJEWUZ.mjs → sort-CrZjTK25.mjs} +19 -4
- package/dist/{types-BoH1qdHo.d.mts → types-D57SZJeE.d.mts} +83 -3
- package/dist/{update-AVfKWNGt.mjs → update-yjSoNnWy.mjs} +26 -2
- package/dist/utils/index.d.mts +2 -2
- package/dist/utils/index.mjs +5 -4
- package/dist/{validation-chain.plugin-7A5qGEQq.mjs → validation-chain.plugin-DwoOOWse.mjs} +460 -144
- package/dist/{validation-chain.plugin-BXx6Ogpp.d.mts → validation-chain.plugin-wGF34Ym-.d.mts} +27 -4
- package/package.json +8 -3
- package/dist/error-Bpbi_NKo.mjs +0 -63
- /package/dist/{coercion-B19MD38s.d.mts → coercion-oM8G4b6m.d.mts} +0 -0
- /package/dist/{field-selection-C5o7jyyA.mjs → field-selection-dI0m4RBs.mjs} +0 -0
- /package/dist/{id-resolution-LZ-cCwR5.mjs → id-resolution-e4BT9zAV.mjs} +0 -0
- /package/dist/{indexes-qIBCZ3mY.d.mts → indexes-C26GNZRU.d.mts} +0 -0
|
@@ -1,10 +1,136 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { a as
|
|
3
|
-
import { t as
|
|
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-yjSoNnWy.mjs";
|
|
3
|
+
import { n as parseDuplicateKeyError, t as createError } from "./error-Lw9GEAro.mjs";
|
|
4
|
+
import { t as PaginationEngine } from "./PaginationEngine-C2_tAvAD.mjs";
|
|
4
5
|
import { hasNearOperator, rewriteNearForCount } from "./query/primitives/geo.mjs";
|
|
5
|
-
import { r as isValidIdForType, t as getSchemaIdType } from "./id-resolution-
|
|
6
|
-
import { a as byIdKey, c as listQueryKey, l as modelPattern, o as byQueryKey, r as getFieldsForUser, u as versionKey } from "./field-selection-
|
|
6
|
+
import { r as isValidIdForType, t as getSchemaIdType } from "./id-resolution-e4BT9zAV.mjs";
|
|
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";
|
|
7
8
|
import mongoose from "mongoose";
|
|
9
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
10
|
+
//#region src/operations.ts
|
|
11
|
+
/**
|
|
12
|
+
* The registry. Order intentionally groups by category for readability.
|
|
13
|
+
* Adding a new RepositoryOperation requires exactly one entry here, after
|
|
14
|
+
* which every bundled plugin auto-classifies it correctly.
|
|
15
|
+
*/
|
|
16
|
+
const OP_REGISTRY = {
|
|
17
|
+
create: {
|
|
18
|
+
policyKey: "data",
|
|
19
|
+
mutates: true,
|
|
20
|
+
hasIdContext: false
|
|
21
|
+
},
|
|
22
|
+
update: {
|
|
23
|
+
policyKey: "query",
|
|
24
|
+
mutates: true,
|
|
25
|
+
hasIdContext: true
|
|
26
|
+
},
|
|
27
|
+
findOneAndUpdate: {
|
|
28
|
+
policyKey: "query",
|
|
29
|
+
mutates: true,
|
|
30
|
+
hasIdContext: false
|
|
31
|
+
},
|
|
32
|
+
delete: {
|
|
33
|
+
policyKey: "query",
|
|
34
|
+
mutates: true,
|
|
35
|
+
hasIdContext: true
|
|
36
|
+
},
|
|
37
|
+
restore: {
|
|
38
|
+
policyKey: "query",
|
|
39
|
+
mutates: true,
|
|
40
|
+
hasIdContext: true
|
|
41
|
+
},
|
|
42
|
+
createMany: {
|
|
43
|
+
policyKey: "dataArray",
|
|
44
|
+
mutates: true,
|
|
45
|
+
hasIdContext: false
|
|
46
|
+
},
|
|
47
|
+
updateMany: {
|
|
48
|
+
policyKey: "query",
|
|
49
|
+
mutates: true,
|
|
50
|
+
hasIdContext: false
|
|
51
|
+
},
|
|
52
|
+
deleteMany: {
|
|
53
|
+
policyKey: "query",
|
|
54
|
+
mutates: true,
|
|
55
|
+
hasIdContext: false
|
|
56
|
+
},
|
|
57
|
+
bulkWrite: {
|
|
58
|
+
policyKey: "operations",
|
|
59
|
+
mutates: true,
|
|
60
|
+
hasIdContext: false
|
|
61
|
+
},
|
|
62
|
+
getById: {
|
|
63
|
+
policyKey: "query",
|
|
64
|
+
mutates: false,
|
|
65
|
+
hasIdContext: true
|
|
66
|
+
},
|
|
67
|
+
getByQuery: {
|
|
68
|
+
policyKey: "query",
|
|
69
|
+
mutates: false,
|
|
70
|
+
hasIdContext: false
|
|
71
|
+
},
|
|
72
|
+
getOne: {
|
|
73
|
+
policyKey: "query",
|
|
74
|
+
mutates: false,
|
|
75
|
+
hasIdContext: false
|
|
76
|
+
},
|
|
77
|
+
findAll: {
|
|
78
|
+
policyKey: "query",
|
|
79
|
+
mutates: false,
|
|
80
|
+
hasIdContext: false
|
|
81
|
+
},
|
|
82
|
+
getOrCreate: {
|
|
83
|
+
policyKey: "query",
|
|
84
|
+
mutates: false,
|
|
85
|
+
hasIdContext: false
|
|
86
|
+
},
|
|
87
|
+
count: {
|
|
88
|
+
policyKey: "query",
|
|
89
|
+
mutates: false,
|
|
90
|
+
hasIdContext: false
|
|
91
|
+
},
|
|
92
|
+
exists: {
|
|
93
|
+
policyKey: "query",
|
|
94
|
+
mutates: false,
|
|
95
|
+
hasIdContext: false
|
|
96
|
+
},
|
|
97
|
+
distinct: {
|
|
98
|
+
policyKey: "query",
|
|
99
|
+
mutates: false,
|
|
100
|
+
hasIdContext: false
|
|
101
|
+
},
|
|
102
|
+
aggregate: {
|
|
103
|
+
policyKey: "query",
|
|
104
|
+
mutates: false,
|
|
105
|
+
hasIdContext: false
|
|
106
|
+
},
|
|
107
|
+
getAll: {
|
|
108
|
+
policyKey: "filters",
|
|
109
|
+
mutates: false,
|
|
110
|
+
hasIdContext: false
|
|
111
|
+
},
|
|
112
|
+
aggregatePaginate: {
|
|
113
|
+
policyKey: "filters",
|
|
114
|
+
mutates: false,
|
|
115
|
+
hasIdContext: false
|
|
116
|
+
},
|
|
117
|
+
lookupPopulate: {
|
|
118
|
+
policyKey: "filters",
|
|
119
|
+
mutates: false,
|
|
120
|
+
hasIdContext: false
|
|
121
|
+
}
|
|
122
|
+
};
|
|
123
|
+
/** All known repository operations, in registry order. */
|
|
124
|
+
const ALL_OPERATIONS = Object.keys(OP_REGISTRY);
|
|
125
|
+
/** Operations that mutate the database. Drives audit + cache invalidation lists. */
|
|
126
|
+
const MUTATING_OPERATIONS = ALL_OPERATIONS.filter((op) => OP_REGISTRY[op].mutates);
|
|
127
|
+
/** Operations that don't mutate. Drives default cacheable ops. */
|
|
128
|
+
const READ_OPERATIONS = ALL_OPERATIONS.filter((op) => !OP_REGISTRY[op].mutates);
|
|
129
|
+
/** Filter ops by their policy injection key. */
|
|
130
|
+
function operationsByPolicyKey(key) {
|
|
131
|
+
return ALL_OPERATIONS.filter((op) => OP_REGISTRY[op].policyKey === key);
|
|
132
|
+
}
|
|
133
|
+
//#endregion
|
|
8
134
|
//#region src/plugins/aggregate-helpers.plugin.ts
|
|
9
135
|
/**
|
|
10
136
|
* Aggregate helpers plugin
|
|
@@ -97,6 +223,22 @@ function auditLogPlugin(logger) {
|
|
|
97
223
|
organizationId: context.organizationId
|
|
98
224
|
});
|
|
99
225
|
});
|
|
226
|
+
repo.on("after:findOneAndUpdate", ({ context, result }) => {
|
|
227
|
+
if (!result) return;
|
|
228
|
+
logger?.info?.("Document upserted/updated (findOneAndUpdate)", {
|
|
229
|
+
model: context.model || repo.model,
|
|
230
|
+
id: result?.[repo.idField || "_id"],
|
|
231
|
+
userId: context.user?._id || context.user?.id,
|
|
232
|
+
organizationId: context.organizationId
|
|
233
|
+
});
|
|
234
|
+
});
|
|
235
|
+
repo.on("error:findOneAndUpdate", ({ context, error }) => {
|
|
236
|
+
logger?.error?.("findOneAndUpdate failed", {
|
|
237
|
+
model: context.model || repo.model,
|
|
238
|
+
error: error.message,
|
|
239
|
+
userId: context.user?._id || context.user?.id
|
|
240
|
+
});
|
|
241
|
+
});
|
|
100
242
|
repo.on("after:delete", ({ context }) => {
|
|
101
243
|
logger?.info?.("Document deleted", {
|
|
102
244
|
model: context.model || repo.model,
|
|
@@ -176,6 +318,7 @@ function getAuditModel(collectionName, ttlDays) {
|
|
|
176
318
|
enum: [
|
|
177
319
|
"create",
|
|
178
320
|
"update",
|
|
321
|
+
"findOneAndUpdate",
|
|
179
322
|
"delete"
|
|
180
323
|
]
|
|
181
324
|
},
|
|
@@ -325,6 +468,19 @@ function auditTrailPlugin(options = {}) {
|
|
|
325
468
|
});
|
|
326
469
|
});
|
|
327
470
|
}
|
|
471
|
+
if (opsSet.has("findOneAndUpdate")) repo.on("after:findOneAndUpdate", ({ context, result }) => {
|
|
472
|
+
if (!result) return;
|
|
473
|
+
const doc = result;
|
|
474
|
+
const idKey = repo.idField || "_id";
|
|
475
|
+
writeAudit(AuditModel, {
|
|
476
|
+
model: context.model || repo.model,
|
|
477
|
+
operation: "findOneAndUpdate",
|
|
478
|
+
documentId: doc?.[idKey],
|
|
479
|
+
userId: getUserId(context),
|
|
480
|
+
orgId: context.organizationId,
|
|
481
|
+
metadata: metadata?.(context)
|
|
482
|
+
});
|
|
483
|
+
});
|
|
328
484
|
if (opsSet.has("delete")) repo.on("after:delete", ({ context }) => {
|
|
329
485
|
writeAudit(AuditModel, {
|
|
330
486
|
model: context.model || repo.model,
|
|
@@ -1228,6 +1384,40 @@ function ensureLookupProjectionIncludesCursorFields(projection, sort) {
|
|
|
1228
1384
|
return nextProjection;
|
|
1229
1385
|
}
|
|
1230
1386
|
/**
|
|
1387
|
+
* Ordered pairs that produce wrong behavior when registered out of order.
|
|
1388
|
+
* Each tuple is `[mustComeFirst, mustComeAfter, reason]`.
|
|
1389
|
+
*
|
|
1390
|
+
* Plugin names are read from `plugin.name` (Plugin objects). Plain-function
|
|
1391
|
+
* plugins without a name are skipped — no false positives.
|
|
1392
|
+
*/
|
|
1393
|
+
const PLUGIN_ORDER_CONSTRAINTS = [[
|
|
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
|
+
/**
|
|
1231
1421
|
* Plugin phase priorities (lower = runs first)
|
|
1232
1422
|
* Policy hooks (multi-tenant, soft-delete, validation) MUST run before cache
|
|
1233
1423
|
* to ensure filters are injected before cache keys are computed.
|
|
@@ -1262,6 +1452,7 @@ var Repository = class {
|
|
|
1262
1452
|
this.searchMode = options.searchMode ?? "text";
|
|
1263
1453
|
this.searchFields = options.searchFields;
|
|
1264
1454
|
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
|
+
validatePluginOrder(plugins, this.model, options.pluginOrderChecks ?? "warn");
|
|
1265
1456
|
plugins.forEach((plugin) => {
|
|
1266
1457
|
this.use(plugin);
|
|
1267
1458
|
});
|
|
@@ -1539,10 +1730,10 @@ var Repository = class {
|
|
|
1539
1730
|
*/
|
|
1540
1731
|
async findAll(filters = {}, options = {}) {
|
|
1541
1732
|
const context = await this._buildContext("findAll", {
|
|
1542
|
-
filters,
|
|
1733
|
+
query: filters,
|
|
1543
1734
|
...options
|
|
1544
1735
|
});
|
|
1545
|
-
const resolvedFilters = context.
|
|
1736
|
+
const resolvedFilters = context.query ?? filters;
|
|
1546
1737
|
try {
|
|
1547
1738
|
const query = this.Model.find(resolvedFilters);
|
|
1548
1739
|
const sortSpec = context.sort || options.sort;
|
|
@@ -1835,6 +2026,64 @@ var Repository = class {
|
|
|
1835
2026
|
}
|
|
1836
2027
|
}
|
|
1837
2028
|
/**
|
|
2029
|
+
* Atomic findOneAndUpdate (compare-and-set primitive).
|
|
2030
|
+
*
|
|
2031
|
+
* Single round-trip match-and-mutate for outbox relays, distributed locks,
|
|
2032
|
+
* and workflow semaphores. Goes through the full hook pipeline so
|
|
2033
|
+
* multi-tenant scope, soft-delete, audit, and cache plugins all apply.
|
|
2034
|
+
*
|
|
2035
|
+
* Returns the matched document (post-update by default) or `null` when no
|
|
2036
|
+
* document matches and `upsert` is false.
|
|
2037
|
+
*
|
|
2038
|
+
* @example FIFO claim-lease for an outbox worker
|
|
2039
|
+
* ```ts
|
|
2040
|
+
* const claimed = await outboxRepo.findOneAndUpdate(
|
|
2041
|
+
* { status: 'pending', leaseExpiresAt: { $lt: new Date() } },
|
|
2042
|
+
* { $set: { status: 'processing', leaseExpiresAt: leaseUntil, leasedBy: workerId } },
|
|
2043
|
+
* { sort: { createdAt: 1 }, returnDocument: 'after' },
|
|
2044
|
+
* );
|
|
2045
|
+
* if (!claimed) return; // queue empty
|
|
2046
|
+
* ```
|
|
2047
|
+
*
|
|
2048
|
+
* @example CAS upsert
|
|
2049
|
+
* ```ts
|
|
2050
|
+
* const lock = await locksRepo.findOneAndUpdate(
|
|
2051
|
+
* { _id: lockKey, ownerId: { $in: [null, workerId] } },
|
|
2052
|
+
* { $set: { ownerId: workerId, expiresAt: leaseUntil } },
|
|
2053
|
+
* { upsert: true },
|
|
2054
|
+
* );
|
|
2055
|
+
* ```
|
|
2056
|
+
*/
|
|
2057
|
+
async findOneAndUpdate(filter, update, options = {}) {
|
|
2058
|
+
const context = await this._buildContext("findOneAndUpdate", {
|
|
2059
|
+
query: filter,
|
|
2060
|
+
data: update,
|
|
2061
|
+
...options
|
|
2062
|
+
});
|
|
2063
|
+
try {
|
|
2064
|
+
const finalQuery = context.query || filter;
|
|
2065
|
+
const finalUpdate = context.data || update;
|
|
2066
|
+
const result = await findOneAndUpdate(this.Model, finalQuery, finalUpdate, {
|
|
2067
|
+
...options,
|
|
2068
|
+
sort: options.sort,
|
|
2069
|
+
returnDocument: options.returnDocument ?? "after",
|
|
2070
|
+
upsert: options.upsert ?? false,
|
|
2071
|
+
session: options.session
|
|
2072
|
+
});
|
|
2073
|
+
await this._emitHook("after:findOneAndUpdate", {
|
|
2074
|
+
context,
|
|
2075
|
+
result
|
|
2076
|
+
});
|
|
2077
|
+
return result;
|
|
2078
|
+
} catch (error) {
|
|
2079
|
+
await this._emitErrorHook("error:findOneAndUpdate", {
|
|
2080
|
+
context,
|
|
2081
|
+
error
|
|
2082
|
+
});
|
|
2083
|
+
throw this._handleError(error);
|
|
2084
|
+
}
|
|
2085
|
+
}
|
|
2086
|
+
/**
|
|
1838
2087
|
* Delete a document by ID.
|
|
1839
2088
|
*
|
|
1840
2089
|
* By default the behavior is decided by the plugin stack:
|
|
@@ -2063,12 +2312,13 @@ var Repository = class {
|
|
|
2063
2312
|
};
|
|
2064
2313
|
if (isKeyset && sort) {
|
|
2065
2314
|
const parsedSort = this._parseSort(sort);
|
|
2066
|
-
const { validateKeysetSort } = await import("./sort-
|
|
2067
|
-
const { encodeCursor, resolveCursorFilter } = await import("./cursor-
|
|
2068
|
-
const { getPrimaryField } = await import("./sort-
|
|
2069
|
-
const normalizedSort = validateKeysetSort(parsedSort);
|
|
2315
|
+
const { validateKeysetSort } = await import("./sort-CrZjTK25.mjs").then((n) => n.n);
|
|
2316
|
+
const { encodeCursor, resolveCursorFilter } = await import("./cursor-J84rR6wG.mjs").then((n) => n.t);
|
|
2317
|
+
const { getPrimaryField } = await import("./sort-CrZjTK25.mjs").then((n) => n.n);
|
|
2318
|
+
const normalizedSort = validateKeysetSort(parsedSort, this._pagination.config.strictKeysetSortFields);
|
|
2070
2319
|
const cursorVersion = this._pagination.config.cursorVersion ?? 1;
|
|
2071
|
-
const
|
|
2320
|
+
const minCursorVersion = this._pagination.config.minCursorVersion ?? 1;
|
|
2321
|
+
const matchFilters = after ? resolveCursorFilter(after, normalizedSort, cursorVersion, { ...filters || {} }, minCursorVersion) : { ...filters || {} };
|
|
2072
2322
|
if (projection) {
|
|
2073
2323
|
if (Object.values(projection).some((v) => v === 1)) {
|
|
2074
2324
|
for (const sortField of Object.keys(normalizedSort)) if (!(sortField in projection)) projection[sortField] = 1;
|
|
@@ -2415,6 +2665,28 @@ var Repository = class {
|
|
|
2415
2665
|
* ```
|
|
2416
2666
|
*/
|
|
2417
2667
|
/**
|
|
2668
|
+
* Resolve the user's jitter config into a deterministic `ttl -> ttl` function.
|
|
2669
|
+
*
|
|
2670
|
+
* - `undefined` / `0`: identity (no jitter).
|
|
2671
|
+
* - number in (0, 1]: symmetric fractional jitter. `0.1` → uniform in [0.9, 1.1].
|
|
2672
|
+
* - function: used as-is; caller owns the math. Clamped to >= 1s below.
|
|
2673
|
+
*
|
|
2674
|
+
* The final TTL is always clamped to a whole second >= 1 so adapters that
|
|
2675
|
+
* treat 0 as "no expiry" (Redis SETEX semantics) don't accidentally persist
|
|
2676
|
+
* entries forever.
|
|
2677
|
+
*/
|
|
2678
|
+
function resolveJitter(jitter) {
|
|
2679
|
+
if (!jitter) return (ttl) => ttl;
|
|
2680
|
+
if (typeof jitter === "function") return (ttl) => Math.max(1, Math.round(jitter(ttl)));
|
|
2681
|
+
const fraction = Math.min(1, Math.max(0, jitter));
|
|
2682
|
+
if (fraction === 0) return (ttl) => ttl;
|
|
2683
|
+
return (ttl) => {
|
|
2684
|
+
const delta = ttl * fraction;
|
|
2685
|
+
const jittered = ttl - delta + Math.random() * 2 * delta;
|
|
2686
|
+
return Math.max(1, Math.round(jittered));
|
|
2687
|
+
};
|
|
2688
|
+
}
|
|
2689
|
+
/**
|
|
2418
2690
|
* Cache plugin factory
|
|
2419
2691
|
*
|
|
2420
2692
|
* @param options - Cache configuration
|
|
@@ -2428,7 +2700,8 @@ function cachePlugin(options) {
|
|
|
2428
2700
|
queryTtl: options.queryTtl ?? options.ttl ?? 60,
|
|
2429
2701
|
prefix: options.prefix ?? "mk",
|
|
2430
2702
|
debug: options.debug ?? false,
|
|
2431
|
-
skipIfLargeLimit: options.skipIf?.largeLimit ?? 100
|
|
2703
|
+
skipIfLargeLimit: options.skipIf?.largeLimit ?? 100,
|
|
2704
|
+
applyJitter: resolveJitter(options.jitter)
|
|
2432
2705
|
};
|
|
2433
2706
|
const stats = {
|
|
2434
2707
|
hits: 0,
|
|
@@ -2468,7 +2741,7 @@ function cachePlugin(options) {
|
|
|
2468
2741
|
async function bumpVersion() {
|
|
2469
2742
|
const newVersion = Date.now();
|
|
2470
2743
|
try {
|
|
2471
|
-
await config.adapter.set(versionKey(config.prefix, model), newVersion, config.ttl * 10);
|
|
2744
|
+
await config.adapter.set(versionKey(config.prefix, model), newVersion, config.applyJitter(config.ttl * 10));
|
|
2472
2745
|
stats.invalidations++;
|
|
2473
2746
|
log(`Bumped version for ${model} to:`, newVersion);
|
|
2474
2747
|
} catch (e) {
|
|
@@ -2647,7 +2920,7 @@ function cachePlugin(options) {
|
|
|
2647
2920
|
populate: context.populate,
|
|
2648
2921
|
lean: context.lean
|
|
2649
2922
|
});
|
|
2650
|
-
const ttl = context.cacheTtl ?? config.byIdTtl;
|
|
2923
|
+
const ttl = config.applyJitter(context.cacheTtl ?? config.byIdTtl);
|
|
2651
2924
|
try {
|
|
2652
2925
|
await config.adapter.set(key, result, ttl);
|
|
2653
2926
|
trackByIdKey(id, key);
|
|
@@ -2671,7 +2944,7 @@ function cachePlugin(options) {
|
|
|
2671
2944
|
select: context.select,
|
|
2672
2945
|
populate: context.populate
|
|
2673
2946
|
});
|
|
2674
|
-
const ttl = context.cacheTtl ?? config.queryTtl;
|
|
2947
|
+
const ttl = config.applyJitter(context.cacheTtl ?? config.queryTtl);
|
|
2675
2948
|
try {
|
|
2676
2949
|
await config.adapter.set(key, result, ttl);
|
|
2677
2950
|
stats.sets++;
|
|
@@ -2693,7 +2966,7 @@ function cachePlugin(options) {
|
|
|
2693
2966
|
populate: context.populate
|
|
2694
2967
|
});
|
|
2695
2968
|
try {
|
|
2696
|
-
await config.adapter.set(key, result, context.cacheTtl ?? config.queryTtl);
|
|
2969
|
+
await config.adapter.set(key, result, config.applyJitter(context.cacheTtl ?? config.queryTtl));
|
|
2697
2970
|
stats.sets++;
|
|
2698
2971
|
} catch (e) {
|
|
2699
2972
|
log(`Failed to cache getOne:`, e);
|
|
@@ -2726,7 +2999,7 @@ function cachePlugin(options) {
|
|
|
2726
2999
|
countStrategy: context.countStrategy
|
|
2727
3000
|
};
|
|
2728
3001
|
const key = listQueryKey(config.prefix, model, collectionVersion, params);
|
|
2729
|
-
const ttl = context.cacheTtl ?? config.queryTtl;
|
|
3002
|
+
const ttl = config.applyJitter(context.cacheTtl ?? config.queryTtl);
|
|
2730
3003
|
try {
|
|
2731
3004
|
await config.adapter.set(key, result, ttl);
|
|
2732
3005
|
stats.sets++;
|
|
@@ -2762,6 +3035,22 @@ function cachePlugin(options) {
|
|
|
2762
3035
|
await bumpVersion();
|
|
2763
3036
|
});
|
|
2764
3037
|
/**
|
|
3038
|
+
* after:findOneAndUpdate — atomic CAS may insert (upsert) or mutate.
|
|
3039
|
+
* Result carries the matched/inserted doc; invalidate that ID and bump
|
|
3040
|
+
* version. Null result (no match, no upsert) is a no-op.
|
|
3041
|
+
*/
|
|
3042
|
+
repo.on("after:findOneAndUpdate", async (payload) => {
|
|
3043
|
+
const { result } = payload;
|
|
3044
|
+
if (!result) return;
|
|
3045
|
+
const idKey = repo.idField || "_id";
|
|
3046
|
+
const id = result?.[idKey];
|
|
3047
|
+
if (id == null) {
|
|
3048
|
+
await bumpVersion();
|
|
3049
|
+
return;
|
|
3050
|
+
}
|
|
3051
|
+
await Promise.all([invalidateById(String(id)), bumpVersion()]);
|
|
3052
|
+
});
|
|
3053
|
+
/**
|
|
2765
3054
|
* after:delete - Invalidate by ID and bump version
|
|
2766
3055
|
*/
|
|
2767
3056
|
repo.on("after:delete", async (payload) => {
|
|
@@ -3720,45 +4009,20 @@ function mongoOperationsPlugin() {
|
|
|
3720
4009
|
*/
|
|
3721
4010
|
function multiTenantPlugin(options = {}) {
|
|
3722
4011
|
const { tenantField = "organizationId", contextKey = "organizationId", required = true, skipOperations = [], skipWhen, resolveContext, fieldType = "string" } = options;
|
|
3723
|
-
const
|
|
3724
|
-
"
|
|
3725
|
-
"
|
|
3726
|
-
|
|
3727
|
-
"lookupPopulate"
|
|
3728
|
-
];
|
|
3729
|
-
const queryReadOps = [
|
|
3730
|
-
"getById",
|
|
3731
|
-
"getByQuery",
|
|
3732
|
-
"getOne",
|
|
3733
|
-
"count",
|
|
3734
|
-
"exists",
|
|
3735
|
-
"getOrCreate",
|
|
3736
|
-
"distinct",
|
|
3737
|
-
"aggregate"
|
|
3738
|
-
];
|
|
3739
|
-
const constrainedWriteOps = [
|
|
3740
|
-
"update",
|
|
3741
|
-
"delete",
|
|
3742
|
-
"restore"
|
|
3743
|
-
];
|
|
3744
|
-
const filterReadOps = ["getDeleted"];
|
|
3745
|
-
const createOps = ["create", "createMany"];
|
|
3746
|
-
const batchQueryOps = ["updateMany", "deleteMany"];
|
|
3747
|
-
const bulkOps = ["bulkWrite"];
|
|
3748
|
-
const allOps = [
|
|
3749
|
-
...filterOps,
|
|
3750
|
-
...filterReadOps,
|
|
3751
|
-
...queryReadOps,
|
|
3752
|
-
...constrainedWriteOps,
|
|
3753
|
-
...createOps,
|
|
3754
|
-
...batchQueryOps,
|
|
3755
|
-
...bulkOps
|
|
3756
|
-
];
|
|
4012
|
+
const extraOps = [{
|
|
4013
|
+
op: "getDeleted",
|
|
4014
|
+
policyKey: "filters"
|
|
4015
|
+
}];
|
|
3757
4016
|
return {
|
|
3758
4017
|
name: "multi-tenant",
|
|
3759
4018
|
apply(repo) {
|
|
3760
|
-
|
|
4019
|
+
const allOps = [...ALL_OPERATIONS.map((op) => ({
|
|
4020
|
+
op,
|
|
4021
|
+
policyKey: OP_REGISTRY[op].policyKey
|
|
4022
|
+
})), ...extraOps];
|
|
4023
|
+
for (const { op, policyKey } of allOps) {
|
|
3761
4024
|
if (skipOperations.includes(op)) continue;
|
|
4025
|
+
if (policyKey === "none") continue;
|
|
3762
4026
|
repo.on(`before:${op}`, (context) => {
|
|
3763
4027
|
if (skipWhen?.(context, op)) return;
|
|
3764
4028
|
let tenantId = context[contextKey];
|
|
@@ -3769,45 +4033,49 @@ function multiTenantPlugin(options = {}) {
|
|
|
3769
4033
|
if (!tenantId && required) throw new Error(`[mongokit] Multi-tenant: Missing '${contextKey}' in context for '${op}'. Pass it via options or set required: false.`);
|
|
3770
4034
|
if (!tenantId) return;
|
|
3771
4035
|
const castId = fieldType === "objectId" ? new mongoose.Types.ObjectId(tenantId) : tenantId;
|
|
3772
|
-
|
|
3773
|
-
|
|
3774
|
-
|
|
3775
|
-
|
|
3776
|
-
|
|
3777
|
-
|
|
3778
|
-
|
|
3779
|
-
|
|
3780
|
-
|
|
3781
|
-
|
|
3782
|
-
|
|
3783
|
-
|
|
3784
|
-
|
|
3785
|
-
|
|
3786
|
-
|
|
3787
|
-
|
|
3788
|
-
|
|
3789
|
-
|
|
3790
|
-
|
|
3791
|
-
};
|
|
3792
|
-
if (op === "bulkWrite" && context.operations) {
|
|
3793
|
-
const ops = context.operations;
|
|
3794
|
-
for (const subOp of ops) {
|
|
3795
|
-
for (const key of [
|
|
3796
|
-
"updateOne",
|
|
3797
|
-
"updateMany",
|
|
3798
|
-
"deleteOne",
|
|
3799
|
-
"deleteMany",
|
|
3800
|
-
"replaceOne"
|
|
3801
|
-
]) {
|
|
3802
|
-
const opBody = subOp[key];
|
|
3803
|
-
if (opBody?.filter) opBody.filter = {
|
|
3804
|
-
...opBody.filter,
|
|
3805
|
-
[tenantField]: castId
|
|
3806
|
-
};
|
|
4036
|
+
switch (policyKey) {
|
|
4037
|
+
case "filters":
|
|
4038
|
+
context.filters = {
|
|
4039
|
+
...context.filters,
|
|
4040
|
+
[tenantField]: castId
|
|
4041
|
+
};
|
|
4042
|
+
break;
|
|
4043
|
+
case "query":
|
|
4044
|
+
context.query = {
|
|
4045
|
+
...context.query,
|
|
4046
|
+
[tenantField]: castId
|
|
4047
|
+
};
|
|
4048
|
+
break;
|
|
4049
|
+
case "data":
|
|
4050
|
+
if (context.data) context.data[tenantField] = castId;
|
|
4051
|
+
break;
|
|
4052
|
+
case "dataArray":
|
|
4053
|
+
if (context.dataArray) {
|
|
4054
|
+
for (const doc of context.dataArray) if (doc && typeof doc === "object") doc[tenantField] = castId;
|
|
3807
4055
|
}
|
|
3808
|
-
|
|
3809
|
-
|
|
3810
|
-
|
|
4056
|
+
break;
|
|
4057
|
+
case "operations":
|
|
4058
|
+
if (context.operations) {
|
|
4059
|
+
const ops = context.operations;
|
|
4060
|
+
for (const subOp of ops) {
|
|
4061
|
+
for (const key of [
|
|
4062
|
+
"updateOne",
|
|
4063
|
+
"updateMany",
|
|
4064
|
+
"deleteOne",
|
|
4065
|
+
"deleteMany",
|
|
4066
|
+
"replaceOne"
|
|
4067
|
+
]) {
|
|
4068
|
+
const opBody = subOp[key];
|
|
4069
|
+
if (opBody?.filter) opBody.filter = {
|
|
4070
|
+
...opBody.filter,
|
|
4071
|
+
[tenantField]: castId
|
|
4072
|
+
};
|
|
4073
|
+
}
|
|
4074
|
+
const insertBody = subOp.insertOne;
|
|
4075
|
+
if (insertBody?.document) insertBody.document[tenantField] = castId;
|
|
4076
|
+
}
|
|
4077
|
+
}
|
|
4078
|
+
break;
|
|
3811
4079
|
}
|
|
3812
4080
|
}, { priority: HOOK_PRIORITY.POLICY });
|
|
3813
4081
|
}
|
|
@@ -3816,24 +4084,26 @@ function multiTenantPlugin(options = {}) {
|
|
|
3816
4084
|
}
|
|
3817
4085
|
//#endregion
|
|
3818
4086
|
//#region src/plugins/observability.plugin.ts
|
|
3819
|
-
|
|
3820
|
-
|
|
3821
|
-
|
|
3822
|
-
|
|
3823
|
-
|
|
3824
|
-
|
|
3825
|
-
|
|
3826
|
-
|
|
3827
|
-
|
|
3828
|
-
|
|
3829
|
-
|
|
3830
|
-
|
|
3831
|
-
|
|
3832
|
-
|
|
3833
|
-
|
|
3834
|
-
|
|
3835
|
-
|
|
3836
|
-
|
|
4087
|
+
/**
|
|
4088
|
+
* Observability Plugin
|
|
4089
|
+
*
|
|
4090
|
+
* Adds operation timing, structured metrics, and APM hook points.
|
|
4091
|
+
* Works with any monitoring system — just provide an onMetric callback.
|
|
4092
|
+
*
|
|
4093
|
+
* @example
|
|
4094
|
+
* ```typescript
|
|
4095
|
+
* const repo = new Repository(User, [
|
|
4096
|
+
* observabilityPlugin({
|
|
4097
|
+
* onMetric: (metric) => {
|
|
4098
|
+
* // Send to DataDog, New Relic, OpenTelemetry, or console
|
|
4099
|
+
* console.log(`${metric.operation} took ${metric.durationMs}ms`);
|
|
4100
|
+
* statsd.histogram('mongokit.operation', metric.durationMs, { op: metric.operation });
|
|
4101
|
+
* },
|
|
4102
|
+
* }),
|
|
4103
|
+
* ]);
|
|
4104
|
+
* ```
|
|
4105
|
+
*/
|
|
4106
|
+
const DEFAULT_OPS = ALL_OPERATIONS;
|
|
3837
4107
|
const timers = /* @__PURE__ */ new WeakMap();
|
|
3838
4108
|
function observabilityPlugin(options) {
|
|
3839
4109
|
const { onMetric, slowThresholdMs } = options;
|
|
@@ -4004,22 +4274,19 @@ function softDeletePlugin(options = {}) {
|
|
|
4004
4274
|
};
|
|
4005
4275
|
}
|
|
4006
4276
|
};
|
|
4007
|
-
|
|
4008
|
-
"
|
|
4009
|
-
"
|
|
4010
|
-
"
|
|
4011
|
-
|
|
4012
|
-
|
|
4013
|
-
|
|
4014
|
-
|
|
4015
|
-
"
|
|
4016
|
-
|
|
4017
|
-
"
|
|
4018
|
-
"
|
|
4019
|
-
|
|
4020
|
-
"distinct",
|
|
4021
|
-
"aggregate"
|
|
4022
|
-
]) repo.on(`before:${op}`, injectDeleteFilterToQuery, { priority: HOOK_PRIORITY.POLICY });
|
|
4277
|
+
const SOFT_DELETE_SPECIAL = new Set([
|
|
4278
|
+
"updateMany",
|
|
4279
|
+
"deleteMany",
|
|
4280
|
+
"bulkWrite"
|
|
4281
|
+
]);
|
|
4282
|
+
for (const op of ALL_OPERATIONS) {
|
|
4283
|
+
if (SOFT_DELETE_SPECIAL.has(op)) continue;
|
|
4284
|
+
const desc = OP_REGISTRY[op];
|
|
4285
|
+
const isFilterableMutation = op === "findOneAndUpdate";
|
|
4286
|
+
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 });
|
|
4289
|
+
}
|
|
4023
4290
|
repo.on("before:updateMany", (context) => {
|
|
4024
4291
|
if (options.soft !== false) {
|
|
4025
4292
|
const deleteFilter = buildDeletedFilter(deletedField, filterMode, !!context.includeDeleted);
|
|
@@ -4041,24 +4308,6 @@ function softDeletePlugin(options = {}) {
|
|
|
4041
4308
|
context.softDeleted = true;
|
|
4042
4309
|
}
|
|
4043
4310
|
}, { priority: HOOK_PRIORITY.POLICY });
|
|
4044
|
-
repo.on("before:aggregate", (context) => {
|
|
4045
|
-
if (options.soft !== false) {
|
|
4046
|
-
const deleteFilter = buildDeletedFilter(deletedField, filterMode, !!context.includeDeleted);
|
|
4047
|
-
if (Object.keys(deleteFilter).length > 0) context.query = {
|
|
4048
|
-
...context.query || {},
|
|
4049
|
-
...deleteFilter
|
|
4050
|
-
};
|
|
4051
|
-
}
|
|
4052
|
-
}, { priority: HOOK_PRIORITY.POLICY });
|
|
4053
|
-
repo.on("before:aggregatePaginate", (context) => {
|
|
4054
|
-
if (options.soft !== false) {
|
|
4055
|
-
const deleteFilter = buildDeletedFilter(deletedField, filterMode, !!context.includeDeleted);
|
|
4056
|
-
if (Object.keys(deleteFilter).length > 0) context.filters = {
|
|
4057
|
-
...context.filters || {},
|
|
4058
|
-
...deleteFilter
|
|
4059
|
-
};
|
|
4060
|
-
}
|
|
4061
|
-
}, { priority: HOOK_PRIORITY.POLICY });
|
|
4062
4311
|
if (addRestoreMethod) {
|
|
4063
4312
|
const restoreMethod = async function(id, restoreOptions = {}) {
|
|
4064
4313
|
const context = await this._buildContext("restore", {
|
|
@@ -4215,6 +4464,57 @@ function subdocumentPlugin() {
|
|
|
4215
4464
|
};
|
|
4216
4465
|
}
|
|
4217
4466
|
//#endregion
|
|
4467
|
+
//#region src/plugins/tenant-context.ts
|
|
4468
|
+
/**
|
|
4469
|
+
* Request-scoped tenant context via AsyncLocalStorage.
|
|
4470
|
+
*
|
|
4471
|
+
* The multi-tenant plugin already accepts a `resolveContext` callback for
|
|
4472
|
+
* reading tenant ID from an external store. This helper wraps Node's
|
|
4473
|
+
* `AsyncLocalStorage` into a tiny, batteries-included API so apps don't need
|
|
4474
|
+
* to wire the plumbing by hand — a common source of cross-tenant leaks.
|
|
4475
|
+
*
|
|
4476
|
+
* @example
|
|
4477
|
+
* import { createTenantContext, multiTenantPlugin } from '@classytic/mongokit';
|
|
4478
|
+
*
|
|
4479
|
+
* const tenantContext = createTenantContext();
|
|
4480
|
+
*
|
|
4481
|
+
* // In your HTTP middleware (Express / Fastify / NestJS):
|
|
4482
|
+
* app.use((req, _res, next) => {
|
|
4483
|
+
* tenantContext.run({ tenantId: req.auth.organizationId }, next);
|
|
4484
|
+
* });
|
|
4485
|
+
*
|
|
4486
|
+
* // In your repository definition:
|
|
4487
|
+
* const repo = new Repository(Invoice, [
|
|
4488
|
+
* multiTenantPlugin({
|
|
4489
|
+
* tenantField: 'organizationId',
|
|
4490
|
+
* resolveContext: () => tenantContext.getTenantId(),
|
|
4491
|
+
* }),
|
|
4492
|
+
* ]);
|
|
4493
|
+
*
|
|
4494
|
+
* // In your handler — no need to pass organizationId manually:
|
|
4495
|
+
* await repo.getAll({ filters: { status: 'paid' } });
|
|
4496
|
+
*/
|
|
4497
|
+
function createTenantContext() {
|
|
4498
|
+
const storage = new AsyncLocalStorage();
|
|
4499
|
+
return {
|
|
4500
|
+
storage,
|
|
4501
|
+
run(store, fn) {
|
|
4502
|
+
return storage.run(store, fn);
|
|
4503
|
+
},
|
|
4504
|
+
getStore() {
|
|
4505
|
+
return storage.getStore();
|
|
4506
|
+
},
|
|
4507
|
+
getTenantId() {
|
|
4508
|
+
return storage.getStore()?.tenantId;
|
|
4509
|
+
},
|
|
4510
|
+
requireTenantId() {
|
|
4511
|
+
const id = storage.getStore()?.tenantId;
|
|
4512
|
+
if (!id) throw new Error("[mongokit] No tenant context active. Wrap the request in tenantContext.run({ tenantId }, fn).");
|
|
4513
|
+
return id;
|
|
4514
|
+
}
|
|
4515
|
+
};
|
|
4516
|
+
}
|
|
4517
|
+
//#endregion
|
|
4218
4518
|
//#region src/plugins/timestamp.plugin.ts
|
|
4219
4519
|
/**
|
|
4220
4520
|
* Timestamp plugin that auto-injects timestamps
|
|
@@ -4236,6 +4536,20 @@ function timestampPlugin() {
|
|
|
4236
4536
|
if (!context.data) return;
|
|
4237
4537
|
context.data.updatedAt = /* @__PURE__ */ new Date();
|
|
4238
4538
|
});
|
|
4539
|
+
repo.on("before:findOneAndUpdate", (context) => {
|
|
4540
|
+
if (!context.data) return;
|
|
4541
|
+
const now = /* @__PURE__ */ new Date();
|
|
4542
|
+
if (Array.isArray(context.data)) return;
|
|
4543
|
+
const update = context.data;
|
|
4544
|
+
if (Object.keys(update).some((k) => k.startsWith("$"))) {
|
|
4545
|
+
const set = update.$set || {};
|
|
4546
|
+
set.updatedAt = now;
|
|
4547
|
+
update.$set = set;
|
|
4548
|
+
const setOnInsert = update.$setOnInsert || {};
|
|
4549
|
+
if (!("createdAt" in setOnInsert)) setOnInsert.createdAt = now;
|
|
4550
|
+
update.$setOnInsert = setOnInsert;
|
|
4551
|
+
} else update.updatedAt = now;
|
|
4552
|
+
});
|
|
4239
4553
|
}
|
|
4240
4554
|
};
|
|
4241
4555
|
}
|
|
@@ -4262,6 +4576,7 @@ function validationChainPlugin(validators = [], options = {}) {
|
|
|
4262
4576
|
const validatorsByOperation = {
|
|
4263
4577
|
create: [],
|
|
4264
4578
|
update: [],
|
|
4579
|
+
findOneAndUpdate: [],
|
|
4265
4580
|
delete: [],
|
|
4266
4581
|
createMany: []
|
|
4267
4582
|
};
|
|
@@ -4300,6 +4615,7 @@ function validationChainPlugin(validators = [], options = {}) {
|
|
|
4300
4615
|
repo.on("before:create", async (context) => runValidators("create", context));
|
|
4301
4616
|
repo.on("before:createMany", async (context) => runValidators("createMany", context));
|
|
4302
4617
|
repo.on("before:update", async (context) => runValidators("update", context));
|
|
4618
|
+
repo.on("before:findOneAndUpdate", async (context) => runValidators("findOneAndUpdate", context));
|
|
4303
4619
|
repo.on("before:delete", async (context) => runValidators("delete", context));
|
|
4304
4620
|
}
|
|
4305
4621
|
};
|
|
@@ -4388,4 +4704,4 @@ function uniqueField(field, errorMessage) {
|
|
|
4388
4704
|
};
|
|
4389
4705
|
}
|
|
4390
4706
|
//#endregion
|
|
4391
|
-
export {
|
|
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 };
|