@classytic/mongokit 3.2.0 → 3.2.2

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.
Files changed (48) hide show
  1. package/README.md +470 -193
  2. package/dist/actions/index.d.mts +9 -0
  3. package/dist/actions/index.mjs +15 -0
  4. package/dist/aggregate-BAi4Do-X.mjs +767 -0
  5. package/dist/aggregate-CCHI7F51.d.mts +269 -0
  6. package/dist/ai/index.d.mts +125 -0
  7. package/dist/ai/index.mjs +203 -0
  8. package/dist/cache-keys-C8Z9B5sw.mjs +204 -0
  9. package/dist/chunk-DQk6qfdC.mjs +18 -0
  10. package/dist/create-BuO6xt0v.mjs +55 -0
  11. package/dist/custom-id.plugin-B_zIs6gE.mjs +1818 -0
  12. package/dist/custom-id.plugin-BzZI4gnE.d.mts +893 -0
  13. package/dist/index.d.mts +1012 -0
  14. package/dist/index.mjs +1906 -0
  15. package/dist/limits-DsNeCx4D.mjs +299 -0
  16. package/dist/logger-D8ily-PP.mjs +51 -0
  17. package/dist/mongooseToJsonSchema-COdDEkIJ.mjs +317 -0
  18. package/dist/{mongooseToJsonSchema-CaRF_bCN.d.ts → mongooseToJsonSchema-Wbvjfwkn.d.mts} +16 -89
  19. package/dist/pagination/PaginationEngine.d.mts +93 -0
  20. package/dist/pagination/PaginationEngine.mjs +196 -0
  21. package/dist/plugins/index.d.mts +3 -0
  22. package/dist/plugins/index.mjs +3 -0
  23. package/dist/types-D-gploPr.d.mts +1241 -0
  24. package/dist/utils/{index.d.ts → index.d.mts} +14 -21
  25. package/dist/utils/index.mjs +5 -0
  26. package/package.json +21 -21
  27. package/dist/actions/index.d.ts +0 -3
  28. package/dist/actions/index.js +0 -5
  29. package/dist/ai/index.d.ts +0 -175
  30. package/dist/ai/index.js +0 -206
  31. package/dist/chunks/chunk-2ZN65ZOP.js +0 -93
  32. package/dist/chunks/chunk-44KXLGPO.js +0 -388
  33. package/dist/chunks/chunk-DEVXDBRL.js +0 -1226
  34. package/dist/chunks/chunk-I7CWNAJB.js +0 -46
  35. package/dist/chunks/chunk-JWUAVZ3L.js +0 -8
  36. package/dist/chunks/chunk-UE2IEXZJ.js +0 -306
  37. package/dist/chunks/chunk-URLJFIR7.js +0 -22
  38. package/dist/chunks/chunk-VWKIKZYF.js +0 -737
  39. package/dist/chunks/chunk-WSFCRVEQ.js +0 -7
  40. package/dist/index-BDn5fSTE.d.ts +0 -516
  41. package/dist/index.d.ts +0 -1422
  42. package/dist/index.js +0 -1893
  43. package/dist/pagination/PaginationEngine.d.ts +0 -117
  44. package/dist/pagination/PaginationEngine.js +0 -3
  45. package/dist/plugins/index.d.ts +0 -922
  46. package/dist/plugins/index.js +0 -6
  47. package/dist/types-Jni1KgkP.d.ts +0 -780
  48. package/dist/utils/index.js +0 -5
@@ -1,1226 +0,0 @@
1
- import { upsert } from './chunk-I7CWNAJB.js';
2
- import { versionKey, byIdKey, byQueryKey, listQueryKey, modelPattern, getFieldsForUser } from './chunk-2ZN65ZOP.js';
3
- import { warn, debug } from './chunk-URLJFIR7.js';
4
- import { createError } from './chunk-JWUAVZ3L.js';
5
- import mongoose from 'mongoose';
6
-
7
- // src/plugins/field-filter.plugin.ts
8
- function fieldFilterPlugin(fieldPreset) {
9
- return {
10
- name: "fieldFilter",
11
- apply(repo) {
12
- const applyFieldFiltering = (context) => {
13
- if (!fieldPreset) return;
14
- const user = context.context?.user || context.user;
15
- const fields = getFieldsForUser(user, fieldPreset);
16
- const presetSelect = fields.join(" ");
17
- if (context.select) {
18
- context.select = `${presetSelect} ${context.select}`;
19
- } else {
20
- context.select = presetSelect;
21
- }
22
- };
23
- repo.on("before:getAll", applyFieldFiltering);
24
- repo.on("before:getById", applyFieldFiltering);
25
- repo.on("before:getByQuery", applyFieldFiltering);
26
- }
27
- };
28
- }
29
-
30
- // src/plugins/timestamp.plugin.ts
31
- function timestampPlugin() {
32
- return {
33
- name: "timestamp",
34
- apply(repo) {
35
- repo.on("before:create", (context) => {
36
- if (!context.data) return;
37
- const now = /* @__PURE__ */ new Date();
38
- if (!context.data.createdAt) context.data.createdAt = now;
39
- if (!context.data.updatedAt) context.data.updatedAt = now;
40
- });
41
- repo.on("before:update", (context) => {
42
- if (!context.data) return;
43
- context.data.updatedAt = /* @__PURE__ */ new Date();
44
- });
45
- }
46
- };
47
- }
48
-
49
- // src/plugins/audit-log.plugin.ts
50
- function auditLogPlugin(logger) {
51
- return {
52
- name: "auditLog",
53
- apply(repo) {
54
- repo.on("after:create", ({ context, result }) => {
55
- logger?.info?.("Document created", {
56
- model: context.model || repo.model,
57
- id: result?._id,
58
- userId: context.user?._id || context.user?.id,
59
- organizationId: context.organizationId
60
- });
61
- });
62
- repo.on("after:update", ({ context, result }) => {
63
- logger?.info?.("Document updated", {
64
- model: context.model || repo.model,
65
- id: context.id || result?._id,
66
- userId: context.user?._id || context.user?.id,
67
- organizationId: context.organizationId
68
- });
69
- });
70
- repo.on("after:delete", ({ context }) => {
71
- logger?.info?.("Document deleted", {
72
- model: context.model || repo.model,
73
- id: context.id,
74
- userId: context.user?._id || context.user?.id,
75
- organizationId: context.organizationId
76
- });
77
- });
78
- repo.on("error:create", ({ context, error }) => {
79
- logger?.error?.("Create failed", {
80
- model: context.model || repo.model,
81
- error: error.message,
82
- userId: context.user?._id || context.user?.id
83
- });
84
- });
85
- repo.on("error:update", ({ context, error }) => {
86
- logger?.error?.("Update failed", {
87
- model: context.model || repo.model,
88
- id: context.id,
89
- error: error.message,
90
- userId: context.user?._id || context.user?.id
91
- });
92
- });
93
- repo.on("error:delete", ({ context, error }) => {
94
- logger?.error?.("Delete failed", {
95
- model: context.model || repo.model,
96
- id: context.id,
97
- error: error.message,
98
- userId: context.user?._id || context.user?.id
99
- });
100
- });
101
- }
102
- };
103
- }
104
-
105
- // src/plugins/soft-delete.plugin.ts
106
- function buildDeletedFilter(deletedField, filterMode, includeDeleted) {
107
- if (includeDeleted) {
108
- return {};
109
- }
110
- if (filterMode === "exists") {
111
- return { [deletedField]: { $exists: false } };
112
- }
113
- return { [deletedField]: null };
114
- }
115
- function buildGetDeletedFilter(deletedField, filterMode) {
116
- if (filterMode === "exists") {
117
- return { [deletedField]: { $exists: true, $ne: null } };
118
- }
119
- return { [deletedField]: { $ne: null } };
120
- }
121
- function softDeletePlugin(options = {}) {
122
- const deletedField = options.deletedField || "deletedAt";
123
- const deletedByField = options.deletedByField || "deletedBy";
124
- const filterMode = options.filterMode || "null";
125
- const addRestoreMethod = options.addRestoreMethod !== false;
126
- const addGetDeletedMethod = options.addGetDeletedMethod !== false;
127
- const ttlDays = options.ttlDays;
128
- return {
129
- name: "softDelete",
130
- apply(repo) {
131
- if (ttlDays !== void 0 && ttlDays > 0) {
132
- const ttlSeconds = ttlDays * 24 * 60 * 60;
133
- repo.Model.collection.createIndex(
134
- { [deletedField]: 1 },
135
- {
136
- expireAfterSeconds: ttlSeconds,
137
- partialFilterExpression: { [deletedField]: { $type: "date" } }
138
- }
139
- ).catch((err) => {
140
- if (!err.message.includes("already exists")) {
141
- warn(`[softDeletePlugin] Failed to create TTL index: ${err.message}`);
142
- }
143
- });
144
- }
145
- repo.on("before:delete", async (context) => {
146
- if (options.soft !== false) {
147
- const updateData = {
148
- [deletedField]: /* @__PURE__ */ new Date()
149
- };
150
- if (context.user) {
151
- updateData[deletedByField] = context.user._id || context.user.id;
152
- }
153
- await repo.Model.findByIdAndUpdate(context.id, updateData, { session: context.session });
154
- context.softDeleted = true;
155
- }
156
- });
157
- repo.on("before:getAll", (context) => {
158
- if (options.soft !== false) {
159
- const deleteFilter = buildDeletedFilter(deletedField, filterMode, !!context.includeDeleted);
160
- if (Object.keys(deleteFilter).length > 0) {
161
- const existingFilters = context.filters || {};
162
- context.filters = {
163
- ...existingFilters,
164
- ...deleteFilter
165
- };
166
- }
167
- }
168
- });
169
- repo.on("before:getById", (context) => {
170
- if (options.soft !== false) {
171
- const deleteFilter = buildDeletedFilter(deletedField, filterMode, !!context.includeDeleted);
172
- if (Object.keys(deleteFilter).length > 0) {
173
- context.query = {
174
- ...context.query || {},
175
- ...deleteFilter
176
- };
177
- }
178
- }
179
- });
180
- repo.on("before:getByQuery", (context) => {
181
- if (options.soft !== false) {
182
- const deleteFilter = buildDeletedFilter(deletedField, filterMode, !!context.includeDeleted);
183
- if (Object.keys(deleteFilter).length > 0) {
184
- context.query = {
185
- ...context.query || {},
186
- ...deleteFilter
187
- };
188
- }
189
- }
190
- });
191
- if (addRestoreMethod) {
192
- const restoreMethod = async function(id, restoreOptions = {}) {
193
- const updateData = {
194
- [deletedField]: null,
195
- [deletedByField]: null
196
- };
197
- const result = await this.Model.findByIdAndUpdate(id, { $set: updateData }, {
198
- new: true,
199
- session: restoreOptions.session
200
- });
201
- if (!result) {
202
- const error = new Error(`Document with id '${id}' not found`);
203
- error.status = 404;
204
- throw error;
205
- }
206
- await this.emitAsync("after:restore", { id, result });
207
- return result;
208
- };
209
- if (typeof repo.registerMethod === "function") {
210
- repo.registerMethod("restore", restoreMethod);
211
- } else {
212
- repo.restore = restoreMethod.bind(repo);
213
- }
214
- }
215
- if (addGetDeletedMethod) {
216
- const getDeletedMethod = async function(params = {}, getDeletedOptions = {}) {
217
- const deletedFilter = buildGetDeletedFilter(deletedField, filterMode);
218
- const combinedFilters = {
219
- ...params.filters || {},
220
- ...deletedFilter
221
- };
222
- const page = params.page || 1;
223
- const limit = params.limit || 20;
224
- const skip = (page - 1) * limit;
225
- let sortSpec = { [deletedField]: -1 };
226
- if (params.sort) {
227
- if (typeof params.sort === "string") {
228
- const sortOrder = params.sort.startsWith("-") ? -1 : 1;
229
- const sortField = params.sort.startsWith("-") ? params.sort.substring(1) : params.sort;
230
- sortSpec = { [sortField]: sortOrder };
231
- } else {
232
- sortSpec = params.sort;
233
- }
234
- }
235
- let query = this.Model.find(combinedFilters).sort(sortSpec).skip(skip).limit(limit);
236
- if (getDeletedOptions.session) {
237
- query = query.session(getDeletedOptions.session);
238
- }
239
- if (getDeletedOptions.select) {
240
- const selectValue = Array.isArray(getDeletedOptions.select) ? getDeletedOptions.select.join(" ") : getDeletedOptions.select;
241
- query = query.select(selectValue);
242
- }
243
- if (getDeletedOptions.populate) {
244
- const populateSpec = getDeletedOptions.populate;
245
- if (typeof populateSpec === "string") {
246
- query = query.populate(populateSpec.split(",").map((p) => p.trim()));
247
- } else if (Array.isArray(populateSpec)) {
248
- query = query.populate(populateSpec);
249
- } else {
250
- query = query.populate(populateSpec);
251
- }
252
- }
253
- if (getDeletedOptions.lean !== false) {
254
- query = query.lean();
255
- }
256
- const [docs, total] = await Promise.all([
257
- query.exec(),
258
- this.Model.countDocuments(combinedFilters)
259
- ]);
260
- const pages = Math.ceil(total / limit);
261
- return {
262
- method: "offset",
263
- docs,
264
- page,
265
- limit,
266
- total,
267
- pages,
268
- hasNext: page < pages,
269
- hasPrev: page > 1
270
- };
271
- };
272
- if (typeof repo.registerMethod === "function") {
273
- repo.registerMethod("getDeleted", getDeletedMethod);
274
- } else {
275
- repo.getDeleted = getDeletedMethod.bind(repo);
276
- }
277
- }
278
- }
279
- };
280
- }
281
-
282
- // src/plugins/method-registry.plugin.ts
283
- function methodRegistryPlugin() {
284
- return {
285
- name: "method-registry",
286
- apply(repo) {
287
- const registeredMethods = [];
288
- repo.registerMethod = function(name, fn) {
289
- if (repo[name]) {
290
- throw new Error(
291
- `Cannot register method '${name}': Method already exists on repository. Choose a different name or use a plugin that doesn't conflict.`
292
- );
293
- }
294
- if (!name || typeof name !== "string") {
295
- throw new Error("Method name must be a non-empty string");
296
- }
297
- if (typeof fn !== "function") {
298
- throw new Error(`Method '${name}' must be a function`);
299
- }
300
- repo[name] = fn.bind(repo);
301
- registeredMethods.push(name);
302
- repo.emit("method:registered", { name, fn });
303
- };
304
- repo.hasMethod = function(name) {
305
- return typeof repo[name] === "function";
306
- };
307
- repo.getRegisteredMethods = function() {
308
- return [...registeredMethods];
309
- };
310
- }
311
- };
312
- }
313
-
314
- // src/plugins/validation-chain.plugin.ts
315
- function validationChainPlugin(validators = [], options = {}) {
316
- const { stopOnFirstError = true } = options;
317
- validators.forEach((v, idx) => {
318
- if (!v.name || typeof v.name !== "string") {
319
- throw new Error(`Validator at index ${idx} missing 'name' (string)`);
320
- }
321
- if (typeof v.validate !== "function") {
322
- throw new Error(`Validator '${v.name}' missing 'validate' function`);
323
- }
324
- });
325
- const validatorsByOperation = {
326
- create: [],
327
- update: [],
328
- delete: [],
329
- createMany: []
330
- };
331
- const allOperationsValidators = [];
332
- validators.forEach((v) => {
333
- if (!v.operations || v.operations.length === 0) {
334
- allOperationsValidators.push(v);
335
- } else {
336
- v.operations.forEach((op) => {
337
- if (validatorsByOperation[op]) {
338
- validatorsByOperation[op].push(v);
339
- }
340
- });
341
- }
342
- });
343
- return {
344
- name: "validation-chain",
345
- apply(repo) {
346
- const getValidatorsForOperation = (operation) => {
347
- const specific = validatorsByOperation[operation] || [];
348
- return [...allOperationsValidators, ...specific];
349
- };
350
- const runValidators = async (operation, context) => {
351
- const operationValidators = getValidatorsForOperation(operation);
352
- const errors = [];
353
- for (const validator of operationValidators) {
354
- try {
355
- await validator.validate(context, repo);
356
- } catch (error) {
357
- if (stopOnFirstError) {
358
- throw error;
359
- }
360
- errors.push({
361
- validator: validator.name,
362
- error: error.message || String(error)
363
- });
364
- }
365
- }
366
- if (errors.length > 0) {
367
- const err = createError(
368
- 400,
369
- `Validation failed: ${errors.map((e) => `[${e.validator}] ${e.error}`).join("; ")}`
370
- );
371
- err.validationErrors = errors;
372
- throw err;
373
- }
374
- };
375
- repo.on("before:create", async (context) => runValidators("create", context));
376
- repo.on("before:createMany", async (context) => runValidators("createMany", context));
377
- repo.on("before:update", async (context) => runValidators("update", context));
378
- repo.on("before:delete", async (context) => runValidators("delete", context));
379
- }
380
- };
381
- }
382
- function blockIf(name, operations, condition, errorMessage) {
383
- return {
384
- name,
385
- operations,
386
- validate: (context) => {
387
- if (condition(context)) {
388
- throw createError(403, errorMessage);
389
- }
390
- }
391
- };
392
- }
393
- function requireField(field, operations = ["create"]) {
394
- return {
395
- name: `require-${field}`,
396
- operations,
397
- validate: (context) => {
398
- if (!context.data || context.data[field] === void 0 || context.data[field] === null) {
399
- throw createError(400, `Field '${field}' is required`);
400
- }
401
- }
402
- };
403
- }
404
- function autoInject(field, getter, operations = ["create"]) {
405
- return {
406
- name: `auto-inject-${field}`,
407
- operations,
408
- validate: (context) => {
409
- if (context.data && !(field in context.data)) {
410
- const value = getter(context);
411
- if (value !== null && value !== void 0) {
412
- context.data[field] = value;
413
- }
414
- }
415
- }
416
- };
417
- }
418
- function immutableField(field) {
419
- return {
420
- name: `immutable-${field}`,
421
- operations: ["update"],
422
- validate: (context) => {
423
- if (context.data && field in context.data) {
424
- throw createError(400, `Field '${field}' cannot be modified`);
425
- }
426
- }
427
- };
428
- }
429
- function uniqueField(field, errorMessage) {
430
- return {
431
- name: `unique-${field}`,
432
- operations: ["create", "update"],
433
- validate: async (context, repo) => {
434
- if (!context.data || !context.data[field] || !repo) return;
435
- const query = { [field]: context.data[field] };
436
- const getByQuery = repo.getByQuery;
437
- if (typeof getByQuery !== "function") return;
438
- const existing = await getByQuery.call(repo, query, {
439
- select: "_id",
440
- lean: true,
441
- throwOnNotFound: false
442
- });
443
- if (existing && String(existing._id) !== String(context.id)) {
444
- throw createError(409, errorMessage || `${field} already exists`);
445
- }
446
- }
447
- };
448
- }
449
-
450
- // src/plugins/mongo-operations.plugin.ts
451
- function mongoOperationsPlugin() {
452
- return {
453
- name: "mongo-operations",
454
- apply(repo) {
455
- if (!repo.registerMethod) {
456
- throw new Error(
457
- "mongoOperationsPlugin requires methodRegistryPlugin. Add methodRegistryPlugin() before mongoOperationsPlugin() in plugins array."
458
- );
459
- }
460
- repo.registerMethod("upsert", async function(query, data, options = {}) {
461
- return upsert(this.Model, query, data, options);
462
- });
463
- const validateAndUpdateNumeric = async function(id, field, value, operator, operationName, options) {
464
- if (typeof value !== "number") {
465
- throw createError(400, `${operationName} value must be a number`);
466
- }
467
- return this.update(id, { [operator]: { [field]: value } }, options);
468
- };
469
- repo.registerMethod("increment", async function(id, field, value = 1, options = {}) {
470
- return validateAndUpdateNumeric.call(this, id, field, value, "$inc", "Increment", options);
471
- });
472
- repo.registerMethod("decrement", async function(id, field, value = 1, options = {}) {
473
- return validateAndUpdateNumeric.call(this, id, field, -value, "$inc", "Decrement", options);
474
- });
475
- const applyOperator = function(id, field, value, operator, options) {
476
- return this.update(id, { [operator]: { [field]: value } }, options);
477
- };
478
- repo.registerMethod("pushToArray", async function(id, field, value, options = {}) {
479
- return applyOperator.call(this, id, field, value, "$push", options);
480
- });
481
- repo.registerMethod("pullFromArray", async function(id, field, value, options = {}) {
482
- return applyOperator.call(this, id, field, value, "$pull", options);
483
- });
484
- repo.registerMethod("addToSet", async function(id, field, value, options = {}) {
485
- return applyOperator.call(this, id, field, value, "$addToSet", options);
486
- });
487
- repo.registerMethod("setField", async function(id, field, value, options = {}) {
488
- return applyOperator.call(this, id, field, value, "$set", options);
489
- });
490
- repo.registerMethod("unsetField", async function(id, fields, options = {}) {
491
- const fieldArray = Array.isArray(fields) ? fields : [fields];
492
- const unsetObj = fieldArray.reduce((acc, field) => {
493
- acc[field] = "";
494
- return acc;
495
- }, {});
496
- return this.update(id, { $unset: unsetObj }, options);
497
- });
498
- repo.registerMethod("renameField", async function(id, oldName, newName, options = {}) {
499
- return this.update(id, { $rename: { [oldName]: newName } }, options);
500
- });
501
- repo.registerMethod("multiplyField", async function(id, field, multiplier, options = {}) {
502
- return validateAndUpdateNumeric.call(this, id, field, multiplier, "$mul", "Multiplier", options);
503
- });
504
- repo.registerMethod("setMin", async function(id, field, value, options = {}) {
505
- return applyOperator.call(this, id, field, value, "$min", options);
506
- });
507
- repo.registerMethod("setMax", async function(id, field, value, options = {}) {
508
- return applyOperator.call(this, id, field, value, "$max", options);
509
- });
510
- }
511
- };
512
- }
513
-
514
- // src/plugins/batch-operations.plugin.ts
515
- function batchOperationsPlugin() {
516
- return {
517
- name: "batch-operations",
518
- apply(repo) {
519
- if (!repo.registerMethod) {
520
- throw new Error("batchOperationsPlugin requires methodRegistryPlugin");
521
- }
522
- repo.registerMethod("updateMany", async function(query, data, options = {}) {
523
- const _buildContext = this._buildContext;
524
- const context = await _buildContext.call(this, "updateMany", { query, data, options });
525
- try {
526
- this.emit("before:updateMany", context);
527
- if (Array.isArray(data) && options.updatePipeline !== true) {
528
- throw createError(
529
- 400,
530
- "Update pipelines (array updates) are disabled by default; pass `{ updatePipeline: true }` to explicitly allow pipeline-style updates."
531
- );
532
- }
533
- const result = await this.Model.updateMany(query, data, {
534
- runValidators: true,
535
- session: options.session,
536
- ...options.updatePipeline !== void 0 ? { updatePipeline: options.updatePipeline } : {}
537
- }).exec();
538
- this.emit("after:updateMany", { context, result });
539
- return result;
540
- } catch (error) {
541
- this.emit("error:updateMany", { context, error });
542
- const _handleError = this._handleError;
543
- throw _handleError.call(this, error);
544
- }
545
- });
546
- repo.registerMethod("deleteMany", async function(query, options = {}) {
547
- const _buildContext = this._buildContext;
548
- const context = await _buildContext.call(this, "deleteMany", { query, options });
549
- try {
550
- this.emit("before:deleteMany", context);
551
- const result = await this.Model.deleteMany(query, {
552
- session: options.session
553
- }).exec();
554
- this.emit("after:deleteMany", { context, result });
555
- return result;
556
- } catch (error) {
557
- this.emit("error:deleteMany", { context, error });
558
- const _handleError = this._handleError;
559
- throw _handleError.call(this, error);
560
- }
561
- });
562
- }
563
- };
564
- }
565
-
566
- // src/plugins/aggregate-helpers.plugin.ts
567
- function aggregateHelpersPlugin() {
568
- return {
569
- name: "aggregate-helpers",
570
- apply(repo) {
571
- if (!repo.registerMethod) {
572
- throw new Error("aggregateHelpersPlugin requires methodRegistryPlugin");
573
- }
574
- repo.registerMethod("groupBy", async function(field, options = {}) {
575
- const pipeline = [
576
- { $group: { _id: `$${field}`, count: { $sum: 1 } } },
577
- { $sort: { count: -1 } }
578
- ];
579
- if (options.limit) {
580
- pipeline.push({ $limit: options.limit });
581
- }
582
- const aggregate = this.aggregate;
583
- return aggregate.call(this, pipeline, options);
584
- });
585
- const aggregateOperation = async function(field, operator, resultKey, query = {}, options = {}) {
586
- const pipeline = [
587
- { $match: query },
588
- { $group: { _id: null, [resultKey]: { [operator]: `$${field}` } } }
589
- ];
590
- const aggregate = this.aggregate;
591
- const result = await aggregate.call(this, pipeline, options);
592
- return result[0]?.[resultKey] || 0;
593
- };
594
- repo.registerMethod("sum", async function(field, query = {}, options = {}) {
595
- return aggregateOperation.call(this, field, "$sum", "total", query, options);
596
- });
597
- repo.registerMethod("average", async function(field, query = {}, options = {}) {
598
- return aggregateOperation.call(this, field, "$avg", "avg", query, options);
599
- });
600
- repo.registerMethod("min", async function(field, query = {}, options = {}) {
601
- return aggregateOperation.call(this, field, "$min", "min", query, options);
602
- });
603
- repo.registerMethod("max", async function(field, query = {}, options = {}) {
604
- return aggregateOperation.call(this, field, "$max", "max", query, options);
605
- });
606
- }
607
- };
608
- }
609
-
610
- // src/plugins/subdocument.plugin.ts
611
- function subdocumentPlugin() {
612
- return {
613
- name: "subdocument",
614
- apply(repo) {
615
- if (!repo.registerMethod) {
616
- throw new Error("subdocumentPlugin requires methodRegistryPlugin");
617
- }
618
- repo.registerMethod("addSubdocument", async function(parentId, arrayPath, subData, options = {}) {
619
- const update = this.update;
620
- return update.call(this, parentId, { $push: { [arrayPath]: subData } }, options);
621
- });
622
- repo.registerMethod("getSubdocument", async function(parentId, arrayPath, subId, options = {}) {
623
- const _executeQuery = this._executeQuery;
624
- return _executeQuery.call(this, async (Model) => {
625
- const parent = await Model.findById(parentId).session(options.session).exec();
626
- if (!parent) throw createError(404, "Parent not found");
627
- const parentObj = parent;
628
- const arrayField = parentObj[arrayPath];
629
- if (!arrayField || typeof arrayField.id !== "function") {
630
- throw createError(404, "Array field not found");
631
- }
632
- const sub = arrayField.id(subId);
633
- if (!sub) throw createError(404, "Subdocument not found");
634
- return options.lean && typeof sub.toObject === "function" ? sub.toObject() : sub;
635
- });
636
- });
637
- repo.registerMethod("updateSubdocument", async function(parentId, arrayPath, subId, updateData, options = {}) {
638
- const _executeQuery = this._executeQuery;
639
- return _executeQuery.call(this, async (Model) => {
640
- const query = { _id: parentId, [`${arrayPath}._id`]: subId };
641
- const update = { $set: { [`${arrayPath}.$`]: { ...updateData, _id: subId } } };
642
- const result = await Model.findOneAndUpdate(query, update, {
643
- new: true,
644
- runValidators: true,
645
- session: options.session
646
- }).exec();
647
- if (!result) throw createError(404, "Parent or subdocument not found");
648
- return result;
649
- });
650
- });
651
- repo.registerMethod("deleteSubdocument", async function(parentId, arrayPath, subId, options = {}) {
652
- const update = this.update;
653
- return update.call(this, parentId, { $pull: { [arrayPath]: { _id: subId } } }, options);
654
- });
655
- }
656
- };
657
- }
658
-
659
- // src/plugins/cache.plugin.ts
660
- function cachePlugin(options) {
661
- const config = {
662
- adapter: options.adapter,
663
- ttl: options.ttl ?? 60,
664
- byIdTtl: options.byIdTtl ?? options.ttl ?? 60,
665
- queryTtl: options.queryTtl ?? options.ttl ?? 60,
666
- prefix: options.prefix ?? "mk",
667
- debug: options.debug ?? false,
668
- skipIfLargeLimit: options.skipIf?.largeLimit ?? 100
669
- };
670
- const stats = {
671
- hits: 0,
672
- misses: 0,
673
- sets: 0,
674
- invalidations: 0
675
- };
676
- let collectionVersion = 0;
677
- const log = (msg, data) => {
678
- if (config.debug) {
679
- debug(`[mongokit:cache] ${msg}`, data ?? "");
680
- }
681
- };
682
- return {
683
- name: "cache",
684
- apply(repo) {
685
- const model = repo.model;
686
- (async () => {
687
- try {
688
- const cached = await config.adapter.get(versionKey(config.prefix, model));
689
- if (cached !== null) {
690
- collectionVersion = cached;
691
- log(`Initialized version for ${model}:`, collectionVersion);
692
- }
693
- } catch (e) {
694
- log(`Failed to initialize version for ${model}:`, e);
695
- }
696
- })();
697
- async function bumpVersion() {
698
- collectionVersion++;
699
- try {
700
- await config.adapter.set(versionKey(config.prefix, model), collectionVersion, config.ttl * 10);
701
- stats.invalidations++;
702
- log(`Bumped version for ${model} to:`, collectionVersion);
703
- } catch (e) {
704
- log(`Failed to bump version for ${model}:`, e);
705
- }
706
- }
707
- async function invalidateById(id) {
708
- const key = byIdKey(config.prefix, model, id);
709
- try {
710
- await config.adapter.del(key);
711
- stats.invalidations++;
712
- log(`Invalidated byId cache:`, key);
713
- } catch (e) {
714
- log(`Failed to invalidate byId cache:`, e);
715
- }
716
- }
717
- repo.on("before:getById", async (context) => {
718
- if (context.skipCache) {
719
- log(`Skipping cache for getById: ${context.id}`);
720
- return;
721
- }
722
- const id = String(context.id);
723
- const key = byIdKey(config.prefix, model, id);
724
- try {
725
- const cached = await config.adapter.get(key);
726
- if (cached !== null) {
727
- stats.hits++;
728
- log(`Cache HIT for getById:`, key);
729
- context._cacheHit = true;
730
- context._cachedResult = cached;
731
- } else {
732
- stats.misses++;
733
- log(`Cache MISS for getById:`, key);
734
- }
735
- } catch (e) {
736
- log(`Cache error for getById:`, e);
737
- stats.misses++;
738
- }
739
- });
740
- repo.on("before:getByQuery", async (context) => {
741
- if (context.skipCache) {
742
- log(`Skipping cache for getByQuery`);
743
- return;
744
- }
745
- const query = context.query || {};
746
- const key = byQueryKey(config.prefix, model, query, {
747
- select: context.select,
748
- populate: context.populate
749
- });
750
- try {
751
- const cached = await config.adapter.get(key);
752
- if (cached !== null) {
753
- stats.hits++;
754
- log(`Cache HIT for getByQuery:`, key);
755
- context._cacheHit = true;
756
- context._cachedResult = cached;
757
- } else {
758
- stats.misses++;
759
- log(`Cache MISS for getByQuery:`, key);
760
- }
761
- } catch (e) {
762
- log(`Cache error for getByQuery:`, e);
763
- stats.misses++;
764
- }
765
- });
766
- repo.on("before:getAll", async (context) => {
767
- if (context.skipCache) {
768
- log(`Skipping cache for getAll`);
769
- return;
770
- }
771
- const limit = context.limit;
772
- if (limit && limit > config.skipIfLargeLimit) {
773
- log(`Skipping cache for large query (limit: ${limit})`);
774
- return;
775
- }
776
- const params = {
777
- filters: context.filters,
778
- sort: context.sort,
779
- page: context.page,
780
- limit,
781
- after: context.after,
782
- select: context.select,
783
- populate: context.populate,
784
- search: context.search
785
- };
786
- const key = listQueryKey(config.prefix, model, collectionVersion, params);
787
- try {
788
- const cached = await config.adapter.get(key);
789
- if (cached !== null) {
790
- stats.hits++;
791
- log(`Cache HIT for getAll:`, key);
792
- context._cacheHit = true;
793
- context._cachedResult = cached;
794
- } else {
795
- stats.misses++;
796
- log(`Cache MISS for getAll:`, key);
797
- }
798
- } catch (e) {
799
- log(`Cache error for getAll:`, e);
800
- stats.misses++;
801
- }
802
- });
803
- repo.on("after:getById", async (payload) => {
804
- const { context, result } = payload;
805
- if (context._cacheHit) return;
806
- if (context.skipCache) return;
807
- if (result === null) return;
808
- const id = String(context.id);
809
- const key = byIdKey(config.prefix, model, id);
810
- const ttl = context.cacheTtl ?? config.byIdTtl;
811
- try {
812
- await config.adapter.set(key, result, ttl);
813
- stats.sets++;
814
- log(`Cached getById result:`, key);
815
- } catch (e) {
816
- log(`Failed to cache getById:`, e);
817
- }
818
- });
819
- repo.on("after:getByQuery", async (payload) => {
820
- const { context, result } = payload;
821
- if (context._cacheHit) return;
822
- if (context.skipCache) return;
823
- if (result === null) return;
824
- const query = context.query || {};
825
- const key = byQueryKey(config.prefix, model, query, {
826
- select: context.select,
827
- populate: context.populate
828
- });
829
- const ttl = context.cacheTtl ?? config.queryTtl;
830
- try {
831
- await config.adapter.set(key, result, ttl);
832
- stats.sets++;
833
- log(`Cached getByQuery result:`, key);
834
- } catch (e) {
835
- log(`Failed to cache getByQuery:`, e);
836
- }
837
- });
838
- repo.on("after:getAll", async (payload) => {
839
- const { context, result } = payload;
840
- if (context._cacheHit) return;
841
- if (context.skipCache) return;
842
- const limit = context.limit;
843
- if (limit && limit > config.skipIfLargeLimit) return;
844
- const params = {
845
- filters: context.filters,
846
- sort: context.sort,
847
- page: context.page,
848
- limit,
849
- after: context.after,
850
- select: context.select,
851
- populate: context.populate,
852
- search: context.search
853
- };
854
- const key = listQueryKey(config.prefix, model, collectionVersion, params);
855
- const ttl = context.cacheTtl ?? config.queryTtl;
856
- try {
857
- await config.adapter.set(key, result, ttl);
858
- stats.sets++;
859
- log(`Cached getAll result:`, key);
860
- } catch (e) {
861
- log(`Failed to cache getAll:`, e);
862
- }
863
- });
864
- repo.on("after:create", async () => {
865
- await bumpVersion();
866
- });
867
- repo.on("after:createMany", async () => {
868
- await bumpVersion();
869
- });
870
- repo.on("after:update", async (payload) => {
871
- const { context } = payload;
872
- const id = String(context.id);
873
- await Promise.all([
874
- invalidateById(id),
875
- bumpVersion()
876
- ]);
877
- });
878
- repo.on("after:updateMany", async () => {
879
- await bumpVersion();
880
- });
881
- repo.on("after:delete", async (payload) => {
882
- const { context } = payload;
883
- const id = String(context.id);
884
- await Promise.all([
885
- invalidateById(id),
886
- bumpVersion()
887
- ]);
888
- });
889
- repo.on("after:deleteMany", async () => {
890
- await bumpVersion();
891
- });
892
- repo.invalidateCache = async (id) => {
893
- await invalidateById(id);
894
- log(`Manual invalidation for ID:`, id);
895
- };
896
- repo.invalidateListCache = async () => {
897
- await bumpVersion();
898
- log(`Manual list cache invalidation for ${model}`);
899
- };
900
- repo.invalidateAllCache = async () => {
901
- if (config.adapter.clear) {
902
- try {
903
- await config.adapter.clear(modelPattern(config.prefix, model));
904
- stats.invalidations++;
905
- log(`Full cache invalidation for ${model}`);
906
- } catch (e) {
907
- log(`Failed full cache invalidation for ${model}:`, e);
908
- }
909
- } else {
910
- await bumpVersion();
911
- log(`Partial cache invalidation for ${model} (adapter.clear not available)`);
912
- }
913
- };
914
- repo.getCacheStats = () => ({ ...stats });
915
- repo.resetCacheStats = () => {
916
- stats.hits = 0;
917
- stats.misses = 0;
918
- stats.sets = 0;
919
- stats.invalidations = 0;
920
- };
921
- }
922
- };
923
- }
924
- function cascadePlugin(options) {
925
- const { relations, parallel = true, logger } = options;
926
- if (!relations || relations.length === 0) {
927
- throw new Error("cascadePlugin requires at least one relation");
928
- }
929
- return {
930
- name: "cascade",
931
- apply(repo) {
932
- repo.on("after:delete", async (payload) => {
933
- const { context } = payload;
934
- const deletedId = context.id;
935
- if (!deletedId) {
936
- logger?.warn?.("Cascade delete skipped: no document ID in context", {
937
- model: context.model
938
- });
939
- return;
940
- }
941
- const isSoftDelete = context.softDeleted === true;
942
- const cascadeDelete = async (relation) => {
943
- const RelatedModel = mongoose.models[relation.model];
944
- if (!RelatedModel) {
945
- logger?.warn?.(`Cascade delete skipped: model '${relation.model}' not found`, {
946
- parentModel: context.model,
947
- parentId: String(deletedId)
948
- });
949
- return;
950
- }
951
- const query = { [relation.foreignKey]: deletedId };
952
- try {
953
- const shouldSoftDelete = relation.softDelete ?? isSoftDelete;
954
- if (shouldSoftDelete) {
955
- const updateResult = await RelatedModel.updateMany(
956
- query,
957
- {
958
- deletedAt: /* @__PURE__ */ new Date(),
959
- ...context.user ? { deletedBy: context.user._id || context.user.id } : {}
960
- },
961
- { session: context.session }
962
- );
963
- logger?.info?.(`Cascade soft-deleted ${updateResult.modifiedCount} documents`, {
964
- parentModel: context.model,
965
- parentId: String(deletedId),
966
- relatedModel: relation.model,
967
- foreignKey: relation.foreignKey,
968
- count: updateResult.modifiedCount
969
- });
970
- } else {
971
- const deleteResult = await RelatedModel.deleteMany(query, {
972
- session: context.session
973
- });
974
- logger?.info?.(`Cascade deleted ${deleteResult.deletedCount} documents`, {
975
- parentModel: context.model,
976
- parentId: String(deletedId),
977
- relatedModel: relation.model,
978
- foreignKey: relation.foreignKey,
979
- count: deleteResult.deletedCount
980
- });
981
- }
982
- } catch (error) {
983
- logger?.error?.(`Cascade delete failed for model '${relation.model}'`, {
984
- parentModel: context.model,
985
- parentId: String(deletedId),
986
- relatedModel: relation.model,
987
- foreignKey: relation.foreignKey,
988
- error: error.message
989
- });
990
- throw error;
991
- }
992
- };
993
- if (parallel) {
994
- const results = await Promise.allSettled(relations.map(cascadeDelete));
995
- const failures = results.filter((r) => r.status === "rejected");
996
- if (failures.length) {
997
- const err = failures[0].reason;
998
- if (failures.length > 1) {
999
- err.message = `${failures.length} cascade deletes failed. First: ${err.message}`;
1000
- }
1001
- throw err;
1002
- }
1003
- } else {
1004
- for (const relation of relations) {
1005
- await cascadeDelete(relation);
1006
- }
1007
- }
1008
- });
1009
- repo.on("after:deleteMany", async (payload) => {
1010
- const { context, result } = payload;
1011
- const query = context.query;
1012
- if (!query || Object.keys(query).length === 0) {
1013
- logger?.warn?.("Cascade deleteMany skipped: empty query", {
1014
- model: context.model
1015
- });
1016
- return;
1017
- }
1018
- logger?.warn?.("Cascade deleteMany: use before:deleteMany hook for complete cascade support", {
1019
- model: context.model
1020
- });
1021
- });
1022
- repo.on("before:deleteMany", async (context) => {
1023
- const query = context.query;
1024
- if (!query || Object.keys(query).length === 0) {
1025
- return;
1026
- }
1027
- const docs = await repo.Model.find(query, { _id: 1 }).lean().session(context.session ?? null);
1028
- const ids = docs.map((doc) => doc._id);
1029
- context._cascadeIds = ids;
1030
- });
1031
- const originalAfterDeleteMany = repo._hooks.get("after:deleteMany") || [];
1032
- repo._hooks.set("after:deleteMany", [
1033
- ...originalAfterDeleteMany,
1034
- async (payload) => {
1035
- const { context } = payload;
1036
- const ids = context._cascadeIds;
1037
- if (!ids || ids.length === 0) {
1038
- return;
1039
- }
1040
- const isSoftDelete = context.softDeleted === true;
1041
- const cascadeDeleteMany = async (relation) => {
1042
- const RelatedModel = mongoose.models[relation.model];
1043
- if (!RelatedModel) {
1044
- logger?.warn?.(`Cascade deleteMany skipped: model '${relation.model}' not found`, {
1045
- parentModel: context.model
1046
- });
1047
- return;
1048
- }
1049
- const query = { [relation.foreignKey]: { $in: ids } };
1050
- const shouldSoftDelete = relation.softDelete ?? isSoftDelete;
1051
- try {
1052
- if (shouldSoftDelete) {
1053
- const updateResult = await RelatedModel.updateMany(
1054
- query,
1055
- {
1056
- deletedAt: /* @__PURE__ */ new Date(),
1057
- ...context.user ? { deletedBy: context.user._id || context.user.id } : {}
1058
- },
1059
- { session: context.session }
1060
- );
1061
- logger?.info?.(`Cascade soft-deleted ${updateResult.modifiedCount} documents (bulk)`, {
1062
- parentModel: context.model,
1063
- parentCount: ids.length,
1064
- relatedModel: relation.model,
1065
- foreignKey: relation.foreignKey,
1066
- count: updateResult.modifiedCount
1067
- });
1068
- } else {
1069
- const deleteResult = await RelatedModel.deleteMany(query, {
1070
- session: context.session
1071
- });
1072
- logger?.info?.(`Cascade deleted ${deleteResult.deletedCount} documents (bulk)`, {
1073
- parentModel: context.model,
1074
- parentCount: ids.length,
1075
- relatedModel: relation.model,
1076
- foreignKey: relation.foreignKey,
1077
- count: deleteResult.deletedCount
1078
- });
1079
- }
1080
- } catch (error) {
1081
- logger?.error?.(`Cascade deleteMany failed for model '${relation.model}'`, {
1082
- parentModel: context.model,
1083
- relatedModel: relation.model,
1084
- foreignKey: relation.foreignKey,
1085
- error: error.message
1086
- });
1087
- throw error;
1088
- }
1089
- };
1090
- if (parallel) {
1091
- const results = await Promise.allSettled(relations.map(cascadeDeleteMany));
1092
- const failures = results.filter((r) => r.status === "rejected");
1093
- if (failures.length) {
1094
- const err = failures[0].reason;
1095
- if (failures.length > 1) {
1096
- err.message = `${failures.length} cascade deletes failed. First: ${err.message}`;
1097
- }
1098
- throw err;
1099
- }
1100
- } else {
1101
- for (const relation of relations) {
1102
- await cascadeDeleteMany(relation);
1103
- }
1104
- }
1105
- }
1106
- ]);
1107
- }
1108
- };
1109
- }
1110
-
1111
- // src/plugins/multi-tenant.plugin.ts
1112
- function multiTenantPlugin(options = {}) {
1113
- const {
1114
- tenantField = "organizationId",
1115
- contextKey = "organizationId",
1116
- required = true,
1117
- skipOperations = [],
1118
- skipWhen,
1119
- resolveContext
1120
- } = options;
1121
- const readOps = ["getById", "getByQuery", "getAll", "aggregatePaginate", "lookupPopulate"];
1122
- const writeOps = ["create", "createMany", "update", "delete"];
1123
- const allOps = [...readOps, ...writeOps];
1124
- return {
1125
- name: "multi-tenant",
1126
- apply(repo) {
1127
- for (const op of allOps) {
1128
- if (skipOperations.includes(op)) continue;
1129
- repo.on(`before:${op}`, (context) => {
1130
- if (skipWhen?.(context, op)) return;
1131
- let tenantId = context[contextKey];
1132
- if (!tenantId && resolveContext) {
1133
- tenantId = resolveContext();
1134
- if (tenantId) context[contextKey] = tenantId;
1135
- }
1136
- if (!tenantId && required) {
1137
- throw new Error(
1138
- `[mongokit] Multi-tenant: Missing '${contextKey}' in context for '${op}'. Pass it via options or set required: false.`
1139
- );
1140
- }
1141
- if (!tenantId) return;
1142
- if (readOps.includes(op)) {
1143
- if (op === "getAll" || op === "aggregatePaginate" || op === "lookupPopulate") {
1144
- context.filters = { ...context.filters, [tenantField]: tenantId };
1145
- } else {
1146
- context.query = { ...context.query, [tenantField]: tenantId };
1147
- }
1148
- }
1149
- if (op === "create" && context.data) {
1150
- context.data[tenantField] = tenantId;
1151
- }
1152
- if (op === "createMany" && context.dataArray) {
1153
- for (const doc of context.dataArray) {
1154
- doc[tenantField] = tenantId;
1155
- }
1156
- }
1157
- if (op === "update" || op === "delete") {
1158
- context.query = { ...context.query, [tenantField]: tenantId };
1159
- }
1160
- });
1161
- }
1162
- }
1163
- };
1164
- }
1165
-
1166
- // src/plugins/observability.plugin.ts
1167
- var DEFAULT_OPS = [
1168
- "create",
1169
- "createMany",
1170
- "update",
1171
- "delete",
1172
- "getById",
1173
- "getByQuery",
1174
- "getAll",
1175
- "aggregatePaginate",
1176
- "lookupPopulate"
1177
- ];
1178
- var timers = /* @__PURE__ */ new WeakMap();
1179
- function observabilityPlugin(options) {
1180
- const { onMetric, slowThresholdMs } = options;
1181
- const ops = options.operations ?? DEFAULT_OPS;
1182
- return {
1183
- name: "observability",
1184
- apply(repo) {
1185
- for (const op of ops) {
1186
- repo.on(`before:${op}`, (context) => {
1187
- timers.set(context, performance.now());
1188
- });
1189
- repo.on(`after:${op}`, ({ context }) => {
1190
- const start = timers.get(context);
1191
- if (start == null) return;
1192
- const durationMs = Math.round((performance.now() - start) * 100) / 100;
1193
- timers.delete(context);
1194
- if (slowThresholdMs != null && durationMs < slowThresholdMs) return;
1195
- onMetric({
1196
- operation: op,
1197
- model: context.model || repo.model,
1198
- durationMs,
1199
- success: true,
1200
- startedAt: new Date(Date.now() - durationMs),
1201
- userId: context.user?._id?.toString() || context.user?.id?.toString(),
1202
- organizationId: context.organizationId?.toString()
1203
- });
1204
- });
1205
- repo.on(`error:${op}`, ({ context, error }) => {
1206
- const start = timers.get(context);
1207
- if (start == null) return;
1208
- const durationMs = Math.round((performance.now() - start) * 100) / 100;
1209
- timers.delete(context);
1210
- onMetric({
1211
- operation: op,
1212
- model: context.model || repo.model,
1213
- durationMs,
1214
- success: false,
1215
- error: error.message,
1216
- startedAt: new Date(Date.now() - durationMs),
1217
- userId: context.user?._id?.toString() || context.user?.id?.toString(),
1218
- organizationId: context.organizationId?.toString()
1219
- });
1220
- });
1221
- }
1222
- }
1223
- };
1224
- }
1225
-
1226
- export { aggregateHelpersPlugin, auditLogPlugin, autoInject, batchOperationsPlugin, blockIf, cachePlugin, cascadePlugin, fieldFilterPlugin, immutableField, methodRegistryPlugin, mongoOperationsPlugin, multiTenantPlugin, observabilityPlugin, requireField, softDeletePlugin, subdocumentPlugin, timestampPlugin, uniqueField, validationChainPlugin };