@classytic/mongokit 1.0.2 → 2.1.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 +772 -151
- package/dist/actions/index.cjs +479 -0
- package/dist/actions/index.cjs.map +1 -0
- package/dist/actions/index.d.cts +3 -0
- package/dist/actions/index.d.ts +3 -0
- package/dist/actions/index.js +473 -0
- package/dist/actions/index.js.map +1 -0
- package/dist/index-BfVJZF-3.d.cts +337 -0
- package/dist/index-CgOJ2pqz.d.ts +337 -0
- package/dist/index.cjs +2142 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +239 -0
- package/dist/index.d.ts +239 -0
- package/dist/index.js +2108 -0
- package/dist/index.js.map +1 -0
- package/dist/memory-cache-DG2oSSbx.d.ts +142 -0
- package/dist/memory-cache-DqfFfKes.d.cts +142 -0
- package/dist/pagination/PaginationEngine.cjs +375 -0
- package/dist/pagination/PaginationEngine.cjs.map +1 -0
- package/dist/pagination/PaginationEngine.d.cts +117 -0
- package/dist/pagination/PaginationEngine.d.ts +117 -0
- package/dist/pagination/PaginationEngine.js +369 -0
- package/dist/pagination/PaginationEngine.js.map +1 -0
- package/dist/plugins/index.cjs +874 -0
- package/dist/plugins/index.cjs.map +1 -0
- package/dist/plugins/index.d.cts +275 -0
- package/dist/plugins/index.d.ts +275 -0
- package/dist/plugins/index.js +857 -0
- package/dist/plugins/index.js.map +1 -0
- package/dist/types-Nxhmi1aI.d.cts +510 -0
- package/dist/types-Nxhmi1aI.d.ts +510 -0
- package/dist/utils/index.cjs +667 -0
- package/dist/utils/index.cjs.map +1 -0
- package/dist/utils/index.d.cts +189 -0
- package/dist/utils/index.d.ts +189 -0
- package/dist/utils/index.js +643 -0
- package/dist/utils/index.js.map +1 -0
- package/package.json +54 -24
- package/src/Repository.js +0 -225
- package/src/actions/aggregate.js +0 -191
- package/src/actions/create.js +0 -59
- package/src/actions/delete.js +0 -88
- package/src/actions/index.js +0 -11
- package/src/actions/read.js +0 -156
- package/src/actions/update.js +0 -176
- package/src/hooks/lifecycle.js +0 -146
- package/src/index.js +0 -60
- package/src/plugins/aggregate-helpers.plugin.js +0 -71
- package/src/plugins/audit-log.plugin.js +0 -60
- package/src/plugins/batch-operations.plugin.js +0 -66
- package/src/plugins/field-filter.plugin.js +0 -27
- package/src/plugins/index.js +0 -19
- package/src/plugins/method-registry.plugin.js +0 -140
- package/src/plugins/mongo-operations.plugin.js +0 -313
- package/src/plugins/soft-delete.plugin.js +0 -46
- package/src/plugins/subdocument.plugin.js +0 -66
- package/src/plugins/timestamp.plugin.js +0 -19
- package/src/plugins/validation-chain.plugin.js +0 -145
- package/src/utils/field-selection.js +0 -156
- package/src/utils/index.js +0 -12
- package/types/actions/index.d.ts +0 -121
- package/types/index.d.ts +0 -104
- package/types/plugins/index.d.ts +0 -88
- package/types/utils/index.d.ts +0 -24
|
@@ -0,0 +1,857 @@
|
|
|
1
|
+
// src/utils/field-selection.ts
|
|
2
|
+
function getFieldsForUser(user, preset) {
|
|
3
|
+
if (!preset) {
|
|
4
|
+
throw new Error("Field preset is required");
|
|
5
|
+
}
|
|
6
|
+
const fields = [...preset.public || []];
|
|
7
|
+
if (user) {
|
|
8
|
+
fields.push(...preset.authenticated || []);
|
|
9
|
+
const roles = Array.isArray(user.roles) ? user.roles : user.roles ? [user.roles] : [];
|
|
10
|
+
if (roles.includes("admin") || roles.includes("superadmin")) {
|
|
11
|
+
fields.push(...preset.admin || []);
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
return [...new Set(fields)];
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// src/plugins/field-filter.plugin.ts
|
|
18
|
+
function fieldFilterPlugin(fieldPreset) {
|
|
19
|
+
return {
|
|
20
|
+
name: "fieldFilter",
|
|
21
|
+
apply(repo) {
|
|
22
|
+
const applyFieldFiltering = (context) => {
|
|
23
|
+
if (!fieldPreset) return;
|
|
24
|
+
const user = context.context?.user || context.user;
|
|
25
|
+
const fields = getFieldsForUser(user, fieldPreset);
|
|
26
|
+
const presetSelect = fields.join(" ");
|
|
27
|
+
if (context.select) {
|
|
28
|
+
context.select = `${presetSelect} ${context.select}`;
|
|
29
|
+
} else {
|
|
30
|
+
context.select = presetSelect;
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
repo.on("before:getAll", applyFieldFiltering);
|
|
34
|
+
repo.on("before:getById", applyFieldFiltering);
|
|
35
|
+
repo.on("before:getByQuery", applyFieldFiltering);
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// src/plugins/timestamp.plugin.ts
|
|
41
|
+
function timestampPlugin() {
|
|
42
|
+
return {
|
|
43
|
+
name: "timestamp",
|
|
44
|
+
apply(repo) {
|
|
45
|
+
repo.on("before:create", (context) => {
|
|
46
|
+
if (!context.data) return;
|
|
47
|
+
const now = /* @__PURE__ */ new Date();
|
|
48
|
+
if (!context.data.createdAt) context.data.createdAt = now;
|
|
49
|
+
if (!context.data.updatedAt) context.data.updatedAt = now;
|
|
50
|
+
});
|
|
51
|
+
repo.on("before:update", (context) => {
|
|
52
|
+
if (!context.data) return;
|
|
53
|
+
context.data.updatedAt = /* @__PURE__ */ new Date();
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// src/plugins/audit-log.plugin.ts
|
|
60
|
+
function auditLogPlugin(logger) {
|
|
61
|
+
return {
|
|
62
|
+
name: "auditLog",
|
|
63
|
+
apply(repo) {
|
|
64
|
+
repo.on("after:create", ({ context, result }) => {
|
|
65
|
+
logger?.info?.("Document created", {
|
|
66
|
+
model: context.model || repo.model,
|
|
67
|
+
id: result?._id,
|
|
68
|
+
userId: context.user?._id || context.user?.id,
|
|
69
|
+
organizationId: context.organizationId
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
repo.on("after:update", ({ context, result }) => {
|
|
73
|
+
logger?.info?.("Document updated", {
|
|
74
|
+
model: context.model || repo.model,
|
|
75
|
+
id: context.id || result?._id,
|
|
76
|
+
userId: context.user?._id || context.user?.id,
|
|
77
|
+
organizationId: context.organizationId
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
repo.on("after:delete", ({ context }) => {
|
|
81
|
+
logger?.info?.("Document deleted", {
|
|
82
|
+
model: context.model || repo.model,
|
|
83
|
+
id: context.id,
|
|
84
|
+
userId: context.user?._id || context.user?.id,
|
|
85
|
+
organizationId: context.organizationId
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
repo.on("error:create", ({ context, error }) => {
|
|
89
|
+
logger?.error?.("Create failed", {
|
|
90
|
+
model: context.model || repo.model,
|
|
91
|
+
error: error.message,
|
|
92
|
+
userId: context.user?._id || context.user?.id
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
repo.on("error:update", ({ context, error }) => {
|
|
96
|
+
logger?.error?.("Update failed", {
|
|
97
|
+
model: context.model || repo.model,
|
|
98
|
+
id: context.id,
|
|
99
|
+
error: error.message,
|
|
100
|
+
userId: context.user?._id || context.user?.id
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
repo.on("error:delete", ({ context, error }) => {
|
|
104
|
+
logger?.error?.("Delete failed", {
|
|
105
|
+
model: context.model || repo.model,
|
|
106
|
+
id: context.id,
|
|
107
|
+
error: error.message,
|
|
108
|
+
userId: context.user?._id || context.user?.id
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// src/plugins/soft-delete.plugin.ts
|
|
116
|
+
function softDeletePlugin(options = {}) {
|
|
117
|
+
const deletedField = options.deletedField || "deletedAt";
|
|
118
|
+
const deletedByField = options.deletedByField || "deletedBy";
|
|
119
|
+
return {
|
|
120
|
+
name: "softDelete",
|
|
121
|
+
apply(repo) {
|
|
122
|
+
repo.on("before:delete", async (context) => {
|
|
123
|
+
if (options.soft !== false) {
|
|
124
|
+
const updateData = {
|
|
125
|
+
[deletedField]: /* @__PURE__ */ new Date()
|
|
126
|
+
};
|
|
127
|
+
if (context.user) {
|
|
128
|
+
updateData[deletedByField] = context.user._id || context.user.id;
|
|
129
|
+
}
|
|
130
|
+
await repo.Model.findByIdAndUpdate(context.id, updateData, { session: context.session });
|
|
131
|
+
context.softDeleted = true;
|
|
132
|
+
}
|
|
133
|
+
});
|
|
134
|
+
repo.on("before:getAll", (context) => {
|
|
135
|
+
if (!context.includeDeleted && options.soft !== false) {
|
|
136
|
+
const queryParams = context.queryParams || {};
|
|
137
|
+
queryParams.filters = {
|
|
138
|
+
...queryParams.filters || {},
|
|
139
|
+
[deletedField]: { $exists: false }
|
|
140
|
+
};
|
|
141
|
+
context.queryParams = queryParams;
|
|
142
|
+
}
|
|
143
|
+
});
|
|
144
|
+
repo.on("before:getById", (context) => {
|
|
145
|
+
if (!context.includeDeleted && options.soft !== false) {
|
|
146
|
+
context.query = {
|
|
147
|
+
...context.query || {},
|
|
148
|
+
[deletedField]: { $exists: false }
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// src/plugins/method-registry.plugin.ts
|
|
157
|
+
function methodRegistryPlugin() {
|
|
158
|
+
return {
|
|
159
|
+
name: "method-registry",
|
|
160
|
+
apply(repo) {
|
|
161
|
+
const registeredMethods = [];
|
|
162
|
+
repo.registerMethod = function(name, fn) {
|
|
163
|
+
if (repo[name]) {
|
|
164
|
+
throw new Error(
|
|
165
|
+
`Cannot register method '${name}': Method already exists on repository. Choose a different name or use a plugin that doesn't conflict.`
|
|
166
|
+
);
|
|
167
|
+
}
|
|
168
|
+
if (!name || typeof name !== "string") {
|
|
169
|
+
throw new Error("Method name must be a non-empty string");
|
|
170
|
+
}
|
|
171
|
+
if (typeof fn !== "function") {
|
|
172
|
+
throw new Error(`Method '${name}' must be a function`);
|
|
173
|
+
}
|
|
174
|
+
repo[name] = fn.bind(repo);
|
|
175
|
+
registeredMethods.push(name);
|
|
176
|
+
repo.emit("method:registered", { name, fn });
|
|
177
|
+
};
|
|
178
|
+
repo.hasMethod = function(name) {
|
|
179
|
+
return typeof repo[name] === "function";
|
|
180
|
+
};
|
|
181
|
+
repo.getRegisteredMethods = function() {
|
|
182
|
+
return [...registeredMethods];
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// src/utils/error.ts
|
|
189
|
+
function createError(status, message) {
|
|
190
|
+
const error = new Error(message);
|
|
191
|
+
error.status = status;
|
|
192
|
+
return error;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// src/plugins/validation-chain.plugin.ts
|
|
196
|
+
function validationChainPlugin(validators = [], options = {}) {
|
|
197
|
+
const { stopOnFirstError = true } = options;
|
|
198
|
+
validators.forEach((v, idx) => {
|
|
199
|
+
if (!v.name || typeof v.name !== "string") {
|
|
200
|
+
throw new Error(`Validator at index ${idx} missing 'name' (string)`);
|
|
201
|
+
}
|
|
202
|
+
if (typeof v.validate !== "function") {
|
|
203
|
+
throw new Error(`Validator '${v.name}' missing 'validate' function`);
|
|
204
|
+
}
|
|
205
|
+
});
|
|
206
|
+
const validatorsByOperation = {
|
|
207
|
+
create: [],
|
|
208
|
+
update: [],
|
|
209
|
+
delete: [],
|
|
210
|
+
createMany: []
|
|
211
|
+
};
|
|
212
|
+
const allOperationsValidators = [];
|
|
213
|
+
validators.forEach((v) => {
|
|
214
|
+
if (!v.operations || v.operations.length === 0) {
|
|
215
|
+
allOperationsValidators.push(v);
|
|
216
|
+
} else {
|
|
217
|
+
v.operations.forEach((op) => {
|
|
218
|
+
if (validatorsByOperation[op]) {
|
|
219
|
+
validatorsByOperation[op].push(v);
|
|
220
|
+
}
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
});
|
|
224
|
+
return {
|
|
225
|
+
name: "validation-chain",
|
|
226
|
+
apply(repo) {
|
|
227
|
+
const getValidatorsForOperation = (operation) => {
|
|
228
|
+
const specific = validatorsByOperation[operation] || [];
|
|
229
|
+
return [...allOperationsValidators, ...specific];
|
|
230
|
+
};
|
|
231
|
+
const runValidators = async (operation, context) => {
|
|
232
|
+
const operationValidators = getValidatorsForOperation(operation);
|
|
233
|
+
const errors = [];
|
|
234
|
+
for (const validator of operationValidators) {
|
|
235
|
+
try {
|
|
236
|
+
await validator.validate(context, repo);
|
|
237
|
+
} catch (error) {
|
|
238
|
+
if (stopOnFirstError) {
|
|
239
|
+
throw error;
|
|
240
|
+
}
|
|
241
|
+
errors.push({
|
|
242
|
+
validator: validator.name,
|
|
243
|
+
error: error.message || String(error)
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
if (errors.length > 0) {
|
|
248
|
+
const err = createError(
|
|
249
|
+
400,
|
|
250
|
+
`Validation failed: ${errors.map((e) => `[${e.validator}] ${e.error}`).join("; ")}`
|
|
251
|
+
);
|
|
252
|
+
err.validationErrors = errors;
|
|
253
|
+
throw err;
|
|
254
|
+
}
|
|
255
|
+
};
|
|
256
|
+
repo.on("before:create", async (context) => runValidators("create", context));
|
|
257
|
+
repo.on("before:createMany", async (context) => runValidators("createMany", context));
|
|
258
|
+
repo.on("before:update", async (context) => runValidators("update", context));
|
|
259
|
+
repo.on("before:delete", async (context) => runValidators("delete", context));
|
|
260
|
+
}
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
function blockIf(name, operations, condition, errorMessage) {
|
|
264
|
+
return {
|
|
265
|
+
name,
|
|
266
|
+
operations,
|
|
267
|
+
validate: (context) => {
|
|
268
|
+
if (condition(context)) {
|
|
269
|
+
throw createError(403, errorMessage);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
function requireField(field, operations = ["create"]) {
|
|
275
|
+
return {
|
|
276
|
+
name: `require-${field}`,
|
|
277
|
+
operations,
|
|
278
|
+
validate: (context) => {
|
|
279
|
+
if (!context.data || context.data[field] === void 0 || context.data[field] === null) {
|
|
280
|
+
throw createError(400, `Field '${field}' is required`);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
function autoInject(field, getter, operations = ["create"]) {
|
|
286
|
+
return {
|
|
287
|
+
name: `auto-inject-${field}`,
|
|
288
|
+
operations,
|
|
289
|
+
validate: (context) => {
|
|
290
|
+
if (context.data && !(field in context.data)) {
|
|
291
|
+
const value = getter(context);
|
|
292
|
+
if (value !== null && value !== void 0) {
|
|
293
|
+
context.data[field] = value;
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
function immutableField(field) {
|
|
300
|
+
return {
|
|
301
|
+
name: `immutable-${field}`,
|
|
302
|
+
operations: ["update"],
|
|
303
|
+
validate: (context) => {
|
|
304
|
+
if (context.data && field in context.data) {
|
|
305
|
+
throw createError(400, `Field '${field}' cannot be modified`);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
function uniqueField(field, errorMessage) {
|
|
311
|
+
return {
|
|
312
|
+
name: `unique-${field}`,
|
|
313
|
+
operations: ["create", "update"],
|
|
314
|
+
validate: async (context, repo) => {
|
|
315
|
+
if (!context.data || !context.data[field] || !repo) return;
|
|
316
|
+
const query = { [field]: context.data[field] };
|
|
317
|
+
const getByQuery = repo.getByQuery;
|
|
318
|
+
if (typeof getByQuery !== "function") return;
|
|
319
|
+
const existing = await getByQuery.call(repo, query, {
|
|
320
|
+
select: "_id",
|
|
321
|
+
lean: true,
|
|
322
|
+
throwOnNotFound: false
|
|
323
|
+
});
|
|
324
|
+
if (existing && String(existing._id) !== String(context.id)) {
|
|
325
|
+
throw createError(409, errorMessage || `${field} already exists`);
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// src/actions/create.ts
|
|
332
|
+
async function upsert(Model, query, data, options = {}) {
|
|
333
|
+
return Model.findOneAndUpdate(
|
|
334
|
+
query,
|
|
335
|
+
{ $setOnInsert: data },
|
|
336
|
+
{
|
|
337
|
+
upsert: true,
|
|
338
|
+
new: true,
|
|
339
|
+
runValidators: true,
|
|
340
|
+
session: options.session,
|
|
341
|
+
...options.updatePipeline !== void 0 ? { updatePipeline: options.updatePipeline } : {}
|
|
342
|
+
}
|
|
343
|
+
);
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// src/plugins/mongo-operations.plugin.ts
|
|
347
|
+
function mongoOperationsPlugin() {
|
|
348
|
+
return {
|
|
349
|
+
name: "mongo-operations",
|
|
350
|
+
apply(repo) {
|
|
351
|
+
if (!repo.registerMethod) {
|
|
352
|
+
throw new Error(
|
|
353
|
+
"mongoOperationsPlugin requires methodRegistryPlugin. Add methodRegistryPlugin() before mongoOperationsPlugin() in plugins array."
|
|
354
|
+
);
|
|
355
|
+
}
|
|
356
|
+
repo.registerMethod("upsert", async function(query, data, options = {}) {
|
|
357
|
+
return upsert(this.Model, query, data, options);
|
|
358
|
+
});
|
|
359
|
+
const validateAndUpdateNumeric = async function(id, field, value, operator, operationName, options) {
|
|
360
|
+
if (typeof value !== "number") {
|
|
361
|
+
throw createError(400, `${operationName} value must be a number`);
|
|
362
|
+
}
|
|
363
|
+
return this.update(id, { [operator]: { [field]: value } }, options);
|
|
364
|
+
};
|
|
365
|
+
repo.registerMethod("increment", async function(id, field, value = 1, options = {}) {
|
|
366
|
+
return validateAndUpdateNumeric.call(this, id, field, value, "$inc", "Increment", options);
|
|
367
|
+
});
|
|
368
|
+
repo.registerMethod("decrement", async function(id, field, value = 1, options = {}) {
|
|
369
|
+
return validateAndUpdateNumeric.call(this, id, field, -value, "$inc", "Decrement", options);
|
|
370
|
+
});
|
|
371
|
+
const applyOperator = function(id, field, value, operator, options) {
|
|
372
|
+
return this.update(id, { [operator]: { [field]: value } }, options);
|
|
373
|
+
};
|
|
374
|
+
repo.registerMethod("pushToArray", async function(id, field, value, options = {}) {
|
|
375
|
+
return applyOperator.call(this, id, field, value, "$push", options);
|
|
376
|
+
});
|
|
377
|
+
repo.registerMethod("pullFromArray", async function(id, field, value, options = {}) {
|
|
378
|
+
return applyOperator.call(this, id, field, value, "$pull", options);
|
|
379
|
+
});
|
|
380
|
+
repo.registerMethod("addToSet", async function(id, field, value, options = {}) {
|
|
381
|
+
return applyOperator.call(this, id, field, value, "$addToSet", options);
|
|
382
|
+
});
|
|
383
|
+
repo.registerMethod("setField", async function(id, field, value, options = {}) {
|
|
384
|
+
return applyOperator.call(this, id, field, value, "$set", options);
|
|
385
|
+
});
|
|
386
|
+
repo.registerMethod("unsetField", async function(id, fields, options = {}) {
|
|
387
|
+
const fieldArray = Array.isArray(fields) ? fields : [fields];
|
|
388
|
+
const unsetObj = fieldArray.reduce((acc, field) => {
|
|
389
|
+
acc[field] = "";
|
|
390
|
+
return acc;
|
|
391
|
+
}, {});
|
|
392
|
+
return this.update(id, { $unset: unsetObj }, options);
|
|
393
|
+
});
|
|
394
|
+
repo.registerMethod("renameField", async function(id, oldName, newName, options = {}) {
|
|
395
|
+
return this.update(id, { $rename: { [oldName]: newName } }, options);
|
|
396
|
+
});
|
|
397
|
+
repo.registerMethod("multiplyField", async function(id, field, multiplier, options = {}) {
|
|
398
|
+
return validateAndUpdateNumeric.call(this, id, field, multiplier, "$mul", "Multiplier", options);
|
|
399
|
+
});
|
|
400
|
+
repo.registerMethod("setMin", async function(id, field, value, options = {}) {
|
|
401
|
+
return applyOperator.call(this, id, field, value, "$min", options);
|
|
402
|
+
});
|
|
403
|
+
repo.registerMethod("setMax", async function(id, field, value, options = {}) {
|
|
404
|
+
return applyOperator.call(this, id, field, value, "$max", options);
|
|
405
|
+
});
|
|
406
|
+
}
|
|
407
|
+
};
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
// src/plugins/batch-operations.plugin.ts
|
|
411
|
+
function batchOperationsPlugin() {
|
|
412
|
+
return {
|
|
413
|
+
name: "batch-operations",
|
|
414
|
+
apply(repo) {
|
|
415
|
+
if (!repo.registerMethod) {
|
|
416
|
+
throw new Error("batchOperationsPlugin requires methodRegistryPlugin");
|
|
417
|
+
}
|
|
418
|
+
repo.registerMethod("updateMany", async function(query, data, options = {}) {
|
|
419
|
+
const _buildContext = this._buildContext;
|
|
420
|
+
const context = await _buildContext.call(this, "updateMany", { query, data, options });
|
|
421
|
+
try {
|
|
422
|
+
this.emit("before:updateMany", context);
|
|
423
|
+
const result = await this.Model.updateMany(query, data, {
|
|
424
|
+
runValidators: true,
|
|
425
|
+
session: options.session
|
|
426
|
+
}).exec();
|
|
427
|
+
this.emit("after:updateMany", { context, result });
|
|
428
|
+
return result;
|
|
429
|
+
} catch (error) {
|
|
430
|
+
this.emit("error:updateMany", { context, error });
|
|
431
|
+
const _handleError = this._handleError;
|
|
432
|
+
throw _handleError.call(this, error);
|
|
433
|
+
}
|
|
434
|
+
});
|
|
435
|
+
repo.registerMethod("deleteMany", async function(query, options = {}) {
|
|
436
|
+
const _buildContext = this._buildContext;
|
|
437
|
+
const context = await _buildContext.call(this, "deleteMany", { query, options });
|
|
438
|
+
try {
|
|
439
|
+
this.emit("before:deleteMany", context);
|
|
440
|
+
const result = await this.Model.deleteMany(query, {
|
|
441
|
+
session: options.session
|
|
442
|
+
}).exec();
|
|
443
|
+
this.emit("after:deleteMany", { context, result });
|
|
444
|
+
return result;
|
|
445
|
+
} catch (error) {
|
|
446
|
+
this.emit("error:deleteMany", { context, error });
|
|
447
|
+
const _handleError = this._handleError;
|
|
448
|
+
throw _handleError.call(this, error);
|
|
449
|
+
}
|
|
450
|
+
});
|
|
451
|
+
}
|
|
452
|
+
};
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
// src/plugins/aggregate-helpers.plugin.ts
|
|
456
|
+
function aggregateHelpersPlugin() {
|
|
457
|
+
return {
|
|
458
|
+
name: "aggregate-helpers",
|
|
459
|
+
apply(repo) {
|
|
460
|
+
if (!repo.registerMethod) {
|
|
461
|
+
throw new Error("aggregateHelpersPlugin requires methodRegistryPlugin");
|
|
462
|
+
}
|
|
463
|
+
repo.registerMethod("groupBy", async function(field, options = {}) {
|
|
464
|
+
const pipeline = [
|
|
465
|
+
{ $group: { _id: `$${field}`, count: { $sum: 1 } } },
|
|
466
|
+
{ $sort: { count: -1 } }
|
|
467
|
+
];
|
|
468
|
+
if (options.limit) {
|
|
469
|
+
pipeline.push({ $limit: options.limit });
|
|
470
|
+
}
|
|
471
|
+
const aggregate = this.aggregate;
|
|
472
|
+
return aggregate.call(this, pipeline, options);
|
|
473
|
+
});
|
|
474
|
+
const aggregateOperation = async function(field, operator, resultKey, query = {}, options = {}) {
|
|
475
|
+
const pipeline = [
|
|
476
|
+
{ $match: query },
|
|
477
|
+
{ $group: { _id: null, [resultKey]: { [operator]: `$${field}` } } }
|
|
478
|
+
];
|
|
479
|
+
const aggregate = this.aggregate;
|
|
480
|
+
const result = await aggregate.call(this, pipeline, options);
|
|
481
|
+
return result[0]?.[resultKey] || 0;
|
|
482
|
+
};
|
|
483
|
+
repo.registerMethod("sum", async function(field, query = {}, options = {}) {
|
|
484
|
+
return aggregateOperation.call(this, field, "$sum", "total", query, options);
|
|
485
|
+
});
|
|
486
|
+
repo.registerMethod("average", async function(field, query = {}, options = {}) {
|
|
487
|
+
return aggregateOperation.call(this, field, "$avg", "avg", query, options);
|
|
488
|
+
});
|
|
489
|
+
repo.registerMethod("min", async function(field, query = {}, options = {}) {
|
|
490
|
+
return aggregateOperation.call(this, field, "$min", "min", query, options);
|
|
491
|
+
});
|
|
492
|
+
repo.registerMethod("max", async function(field, query = {}, options = {}) {
|
|
493
|
+
return aggregateOperation.call(this, field, "$max", "max", query, options);
|
|
494
|
+
});
|
|
495
|
+
}
|
|
496
|
+
};
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
// src/plugins/subdocument.plugin.ts
|
|
500
|
+
function subdocumentPlugin() {
|
|
501
|
+
return {
|
|
502
|
+
name: "subdocument",
|
|
503
|
+
apply(repo) {
|
|
504
|
+
if (!repo.registerMethod) {
|
|
505
|
+
throw new Error("subdocumentPlugin requires methodRegistryPlugin");
|
|
506
|
+
}
|
|
507
|
+
repo.registerMethod("addSubdocument", async function(parentId, arrayPath, subData, options = {}) {
|
|
508
|
+
const update = this.update;
|
|
509
|
+
return update.call(this, parentId, { $push: { [arrayPath]: subData } }, options);
|
|
510
|
+
});
|
|
511
|
+
repo.registerMethod("getSubdocument", async function(parentId, arrayPath, subId, options = {}) {
|
|
512
|
+
const _executeQuery = this._executeQuery;
|
|
513
|
+
return _executeQuery.call(this, async (Model) => {
|
|
514
|
+
const parent = await Model.findById(parentId).session(options.session).exec();
|
|
515
|
+
if (!parent) throw createError(404, "Parent not found");
|
|
516
|
+
const parentObj = parent;
|
|
517
|
+
const arrayField = parentObj[arrayPath];
|
|
518
|
+
if (!arrayField || typeof arrayField.id !== "function") {
|
|
519
|
+
throw createError(404, "Array field not found");
|
|
520
|
+
}
|
|
521
|
+
const sub = arrayField.id(subId);
|
|
522
|
+
if (!sub) throw createError(404, "Subdocument not found");
|
|
523
|
+
return options.lean && typeof sub.toObject === "function" ? sub.toObject() : sub;
|
|
524
|
+
});
|
|
525
|
+
});
|
|
526
|
+
repo.registerMethod("updateSubdocument", async function(parentId, arrayPath, subId, updateData, options = {}) {
|
|
527
|
+
const _executeQuery = this._executeQuery;
|
|
528
|
+
return _executeQuery.call(this, async (Model) => {
|
|
529
|
+
const query = { _id: parentId, [`${arrayPath}._id`]: subId };
|
|
530
|
+
const update = { $set: { [`${arrayPath}.$`]: { ...updateData, _id: subId } } };
|
|
531
|
+
const result = await Model.findOneAndUpdate(query, update, {
|
|
532
|
+
new: true,
|
|
533
|
+
runValidators: true,
|
|
534
|
+
session: options.session
|
|
535
|
+
}).exec();
|
|
536
|
+
if (!result) throw createError(404, "Parent or subdocument not found");
|
|
537
|
+
return result;
|
|
538
|
+
});
|
|
539
|
+
});
|
|
540
|
+
repo.registerMethod("deleteSubdocument", async function(parentId, arrayPath, subId, options = {}) {
|
|
541
|
+
const update = this.update;
|
|
542
|
+
return update.call(this, parentId, { $pull: { [arrayPath]: { _id: subId } } }, options);
|
|
543
|
+
});
|
|
544
|
+
}
|
|
545
|
+
};
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
// src/utils/cache-keys.ts
|
|
549
|
+
function hashString(str) {
|
|
550
|
+
let hash = 5381;
|
|
551
|
+
for (let i = 0; i < str.length; i++) {
|
|
552
|
+
hash = (hash << 5) + hash ^ str.charCodeAt(i);
|
|
553
|
+
}
|
|
554
|
+
return (hash >>> 0).toString(16);
|
|
555
|
+
}
|
|
556
|
+
function stableStringify(obj) {
|
|
557
|
+
if (obj === null || obj === void 0) return "";
|
|
558
|
+
if (typeof obj !== "object") return String(obj);
|
|
559
|
+
if (Array.isArray(obj)) {
|
|
560
|
+
return "[" + obj.map(stableStringify).join(",") + "]";
|
|
561
|
+
}
|
|
562
|
+
const sorted = Object.keys(obj).sort().map((key) => `${key}:${stableStringify(obj[key])}`);
|
|
563
|
+
return "{" + sorted.join(",") + "}";
|
|
564
|
+
}
|
|
565
|
+
function byIdKey(prefix, model, id) {
|
|
566
|
+
return `${prefix}:id:${model}:${id}`;
|
|
567
|
+
}
|
|
568
|
+
function byQueryKey(prefix, model, query, options) {
|
|
569
|
+
const hashInput = stableStringify({ q: query, s: options?.select, p: options?.populate });
|
|
570
|
+
return `${prefix}:one:${model}:${hashString(hashInput)}`;
|
|
571
|
+
}
|
|
572
|
+
function listQueryKey(prefix, model, version, params) {
|
|
573
|
+
const hashInput = stableStringify({
|
|
574
|
+
f: params.filters,
|
|
575
|
+
s: params.sort,
|
|
576
|
+
pg: params.page,
|
|
577
|
+
lm: params.limit,
|
|
578
|
+
af: params.after,
|
|
579
|
+
sl: params.select,
|
|
580
|
+
pp: params.populate
|
|
581
|
+
});
|
|
582
|
+
return `${prefix}:list:${model}:${version}:${hashString(hashInput)}`;
|
|
583
|
+
}
|
|
584
|
+
function versionKey(prefix, model) {
|
|
585
|
+
return `${prefix}:ver:${model}`;
|
|
586
|
+
}
|
|
587
|
+
function modelPattern(prefix, model) {
|
|
588
|
+
return `${prefix}:*:${model}:*`;
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
// src/plugins/cache.plugin.ts
|
|
592
|
+
function cachePlugin(options) {
|
|
593
|
+
const config = {
|
|
594
|
+
adapter: options.adapter,
|
|
595
|
+
ttl: options.ttl ?? 60,
|
|
596
|
+
byIdTtl: options.byIdTtl ?? options.ttl ?? 60,
|
|
597
|
+
queryTtl: options.queryTtl ?? options.ttl ?? 60,
|
|
598
|
+
prefix: options.prefix ?? "mk",
|
|
599
|
+
debug: options.debug ?? false,
|
|
600
|
+
skipIfLargeLimit: options.skipIf?.largeLimit ?? 100
|
|
601
|
+
};
|
|
602
|
+
const stats = {
|
|
603
|
+
hits: 0,
|
|
604
|
+
misses: 0,
|
|
605
|
+
sets: 0,
|
|
606
|
+
invalidations: 0
|
|
607
|
+
};
|
|
608
|
+
let collectionVersion = 0;
|
|
609
|
+
const log = (msg, data) => {
|
|
610
|
+
if (config.debug) {
|
|
611
|
+
console.log(`[mongokit:cache] ${msg}`, data ?? "");
|
|
612
|
+
}
|
|
613
|
+
};
|
|
614
|
+
return {
|
|
615
|
+
name: "cache",
|
|
616
|
+
apply(repo) {
|
|
617
|
+
const model = repo.model;
|
|
618
|
+
(async () => {
|
|
619
|
+
try {
|
|
620
|
+
const cached = await config.adapter.get(versionKey(config.prefix, model));
|
|
621
|
+
if (cached !== null) {
|
|
622
|
+
collectionVersion = cached;
|
|
623
|
+
log(`Initialized version for ${model}:`, collectionVersion);
|
|
624
|
+
}
|
|
625
|
+
} catch (e) {
|
|
626
|
+
log(`Failed to initialize version for ${model}:`, e);
|
|
627
|
+
}
|
|
628
|
+
})();
|
|
629
|
+
async function bumpVersion() {
|
|
630
|
+
collectionVersion++;
|
|
631
|
+
try {
|
|
632
|
+
await config.adapter.set(versionKey(config.prefix, model), collectionVersion, config.ttl * 10);
|
|
633
|
+
stats.invalidations++;
|
|
634
|
+
log(`Bumped version for ${model} to:`, collectionVersion);
|
|
635
|
+
} catch (e) {
|
|
636
|
+
log(`Failed to bump version for ${model}:`, e);
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
async function invalidateById(id) {
|
|
640
|
+
const key = byIdKey(config.prefix, model, id);
|
|
641
|
+
try {
|
|
642
|
+
await config.adapter.del(key);
|
|
643
|
+
stats.invalidations++;
|
|
644
|
+
log(`Invalidated byId cache:`, key);
|
|
645
|
+
} catch (e) {
|
|
646
|
+
log(`Failed to invalidate byId cache:`, e);
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
repo.on("before:getById", async (context) => {
|
|
650
|
+
if (context.skipCache) {
|
|
651
|
+
log(`Skipping cache for getById: ${context.id}`);
|
|
652
|
+
return;
|
|
653
|
+
}
|
|
654
|
+
const id = String(context.id);
|
|
655
|
+
const key = byIdKey(config.prefix, model, id);
|
|
656
|
+
try {
|
|
657
|
+
const cached = await config.adapter.get(key);
|
|
658
|
+
if (cached !== null) {
|
|
659
|
+
stats.hits++;
|
|
660
|
+
log(`Cache HIT for getById:`, key);
|
|
661
|
+
context._cacheHit = true;
|
|
662
|
+
context._cachedResult = cached;
|
|
663
|
+
} else {
|
|
664
|
+
stats.misses++;
|
|
665
|
+
log(`Cache MISS for getById:`, key);
|
|
666
|
+
}
|
|
667
|
+
} catch (e) {
|
|
668
|
+
log(`Cache error for getById:`, e);
|
|
669
|
+
stats.misses++;
|
|
670
|
+
}
|
|
671
|
+
});
|
|
672
|
+
repo.on("before:getByQuery", async (context) => {
|
|
673
|
+
if (context.skipCache) {
|
|
674
|
+
log(`Skipping cache for getByQuery`);
|
|
675
|
+
return;
|
|
676
|
+
}
|
|
677
|
+
const query = context.query || {};
|
|
678
|
+
const key = byQueryKey(config.prefix, model, query, {
|
|
679
|
+
select: context.select,
|
|
680
|
+
populate: context.populate
|
|
681
|
+
});
|
|
682
|
+
try {
|
|
683
|
+
const cached = await config.adapter.get(key);
|
|
684
|
+
if (cached !== null) {
|
|
685
|
+
stats.hits++;
|
|
686
|
+
log(`Cache HIT for getByQuery:`, key);
|
|
687
|
+
context._cacheHit = true;
|
|
688
|
+
context._cachedResult = cached;
|
|
689
|
+
} else {
|
|
690
|
+
stats.misses++;
|
|
691
|
+
log(`Cache MISS for getByQuery:`, key);
|
|
692
|
+
}
|
|
693
|
+
} catch (e) {
|
|
694
|
+
log(`Cache error for getByQuery:`, e);
|
|
695
|
+
stats.misses++;
|
|
696
|
+
}
|
|
697
|
+
});
|
|
698
|
+
repo.on("before:getAll", async (context) => {
|
|
699
|
+
if (context.skipCache) {
|
|
700
|
+
log(`Skipping cache for getAll`);
|
|
701
|
+
return;
|
|
702
|
+
}
|
|
703
|
+
const limit = context.limit;
|
|
704
|
+
if (limit && limit > config.skipIfLargeLimit) {
|
|
705
|
+
log(`Skipping cache for large query (limit: ${limit})`);
|
|
706
|
+
return;
|
|
707
|
+
}
|
|
708
|
+
const params = {
|
|
709
|
+
filters: context.filters,
|
|
710
|
+
sort: context.sort,
|
|
711
|
+
page: context.page,
|
|
712
|
+
limit,
|
|
713
|
+
after: context.after,
|
|
714
|
+
select: context.select,
|
|
715
|
+
populate: context.populate
|
|
716
|
+
};
|
|
717
|
+
const key = listQueryKey(config.prefix, model, collectionVersion, params);
|
|
718
|
+
try {
|
|
719
|
+
const cached = await config.adapter.get(key);
|
|
720
|
+
if (cached !== null) {
|
|
721
|
+
stats.hits++;
|
|
722
|
+
log(`Cache HIT for getAll:`, key);
|
|
723
|
+
context._cacheHit = true;
|
|
724
|
+
context._cachedResult = cached;
|
|
725
|
+
} else {
|
|
726
|
+
stats.misses++;
|
|
727
|
+
log(`Cache MISS for getAll:`, key);
|
|
728
|
+
}
|
|
729
|
+
} catch (e) {
|
|
730
|
+
log(`Cache error for getAll:`, e);
|
|
731
|
+
stats.misses++;
|
|
732
|
+
}
|
|
733
|
+
});
|
|
734
|
+
repo.on("after:getById", async (payload) => {
|
|
735
|
+
const { context, result } = payload;
|
|
736
|
+
if (context._cacheHit) return;
|
|
737
|
+
if (context.skipCache) return;
|
|
738
|
+
if (result === null) return;
|
|
739
|
+
const id = String(context.id);
|
|
740
|
+
const key = byIdKey(config.prefix, model, id);
|
|
741
|
+
const ttl = context.cacheTtl ?? config.byIdTtl;
|
|
742
|
+
try {
|
|
743
|
+
await config.adapter.set(key, result, ttl);
|
|
744
|
+
stats.sets++;
|
|
745
|
+
log(`Cached getById result:`, key);
|
|
746
|
+
} catch (e) {
|
|
747
|
+
log(`Failed to cache getById:`, e);
|
|
748
|
+
}
|
|
749
|
+
});
|
|
750
|
+
repo.on("after:getByQuery", async (payload) => {
|
|
751
|
+
const { context, result } = payload;
|
|
752
|
+
if (context._cacheHit) return;
|
|
753
|
+
if (context.skipCache) return;
|
|
754
|
+
if (result === null) return;
|
|
755
|
+
const query = context.query || {};
|
|
756
|
+
const key = byQueryKey(config.prefix, model, query, {
|
|
757
|
+
select: context.select,
|
|
758
|
+
populate: context.populate
|
|
759
|
+
});
|
|
760
|
+
const ttl = context.cacheTtl ?? config.queryTtl;
|
|
761
|
+
try {
|
|
762
|
+
await config.adapter.set(key, result, ttl);
|
|
763
|
+
stats.sets++;
|
|
764
|
+
log(`Cached getByQuery result:`, key);
|
|
765
|
+
} catch (e) {
|
|
766
|
+
log(`Failed to cache getByQuery:`, e);
|
|
767
|
+
}
|
|
768
|
+
});
|
|
769
|
+
repo.on("after:getAll", async (payload) => {
|
|
770
|
+
const { context, result } = payload;
|
|
771
|
+
if (context._cacheHit) return;
|
|
772
|
+
if (context.skipCache) return;
|
|
773
|
+
const limit = context.limit;
|
|
774
|
+
if (limit && limit > config.skipIfLargeLimit) return;
|
|
775
|
+
const params = {
|
|
776
|
+
filters: context.filters,
|
|
777
|
+
sort: context.sort,
|
|
778
|
+
page: context.page,
|
|
779
|
+
limit,
|
|
780
|
+
after: context.after,
|
|
781
|
+
select: context.select,
|
|
782
|
+
populate: context.populate
|
|
783
|
+
};
|
|
784
|
+
const key = listQueryKey(config.prefix, model, collectionVersion, params);
|
|
785
|
+
const ttl = context.cacheTtl ?? config.queryTtl;
|
|
786
|
+
try {
|
|
787
|
+
await config.adapter.set(key, result, ttl);
|
|
788
|
+
stats.sets++;
|
|
789
|
+
log(`Cached getAll result:`, key);
|
|
790
|
+
} catch (e) {
|
|
791
|
+
log(`Failed to cache getAll:`, e);
|
|
792
|
+
}
|
|
793
|
+
});
|
|
794
|
+
repo.on("after:create", async () => {
|
|
795
|
+
await bumpVersion();
|
|
796
|
+
});
|
|
797
|
+
repo.on("after:createMany", async () => {
|
|
798
|
+
await bumpVersion();
|
|
799
|
+
});
|
|
800
|
+
repo.on("after:update", async (payload) => {
|
|
801
|
+
const { context } = payload;
|
|
802
|
+
const id = String(context.id);
|
|
803
|
+
await Promise.all([
|
|
804
|
+
invalidateById(id),
|
|
805
|
+
bumpVersion()
|
|
806
|
+
]);
|
|
807
|
+
});
|
|
808
|
+
repo.on("after:updateMany", async () => {
|
|
809
|
+
await bumpVersion();
|
|
810
|
+
});
|
|
811
|
+
repo.on("after:delete", async (payload) => {
|
|
812
|
+
const { context } = payload;
|
|
813
|
+
const id = String(context.id);
|
|
814
|
+
await Promise.all([
|
|
815
|
+
invalidateById(id),
|
|
816
|
+
bumpVersion()
|
|
817
|
+
]);
|
|
818
|
+
});
|
|
819
|
+
repo.on("after:deleteMany", async () => {
|
|
820
|
+
await bumpVersion();
|
|
821
|
+
});
|
|
822
|
+
repo.invalidateCache = async (id) => {
|
|
823
|
+
await invalidateById(id);
|
|
824
|
+
log(`Manual invalidation for ID:`, id);
|
|
825
|
+
};
|
|
826
|
+
repo.invalidateListCache = async () => {
|
|
827
|
+
await bumpVersion();
|
|
828
|
+
log(`Manual list cache invalidation for ${model}`);
|
|
829
|
+
};
|
|
830
|
+
repo.invalidateAllCache = async () => {
|
|
831
|
+
if (config.adapter.clear) {
|
|
832
|
+
try {
|
|
833
|
+
await config.adapter.clear(modelPattern(config.prefix, model));
|
|
834
|
+
stats.invalidations++;
|
|
835
|
+
log(`Full cache invalidation for ${model}`);
|
|
836
|
+
} catch (e) {
|
|
837
|
+
log(`Failed full cache invalidation for ${model}:`, e);
|
|
838
|
+
}
|
|
839
|
+
} else {
|
|
840
|
+
await bumpVersion();
|
|
841
|
+
log(`Partial cache invalidation for ${model} (adapter.clear not available)`);
|
|
842
|
+
}
|
|
843
|
+
};
|
|
844
|
+
repo.getCacheStats = () => ({ ...stats });
|
|
845
|
+
repo.resetCacheStats = () => {
|
|
846
|
+
stats.hits = 0;
|
|
847
|
+
stats.misses = 0;
|
|
848
|
+
stats.sets = 0;
|
|
849
|
+
stats.invalidations = 0;
|
|
850
|
+
};
|
|
851
|
+
}
|
|
852
|
+
};
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
export { aggregateHelpersPlugin, auditLogPlugin, autoInject, batchOperationsPlugin, blockIf, cachePlugin, fieldFilterPlugin, immutableField, methodRegistryPlugin, mongoOperationsPlugin, requireField, softDeletePlugin, subdocumentPlugin, timestampPlugin, uniqueField, validationChainPlugin };
|
|
856
|
+
//# sourceMappingURL=index.js.map
|
|
857
|
+
//# sourceMappingURL=index.js.map
|