@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.
Files changed (64) hide show
  1. package/README.md +772 -151
  2. package/dist/actions/index.cjs +479 -0
  3. package/dist/actions/index.cjs.map +1 -0
  4. package/dist/actions/index.d.cts +3 -0
  5. package/dist/actions/index.d.ts +3 -0
  6. package/dist/actions/index.js +473 -0
  7. package/dist/actions/index.js.map +1 -0
  8. package/dist/index-BfVJZF-3.d.cts +337 -0
  9. package/dist/index-CgOJ2pqz.d.ts +337 -0
  10. package/dist/index.cjs +2142 -0
  11. package/dist/index.cjs.map +1 -0
  12. package/dist/index.d.cts +239 -0
  13. package/dist/index.d.ts +239 -0
  14. package/dist/index.js +2108 -0
  15. package/dist/index.js.map +1 -0
  16. package/dist/memory-cache-DG2oSSbx.d.ts +142 -0
  17. package/dist/memory-cache-DqfFfKes.d.cts +142 -0
  18. package/dist/pagination/PaginationEngine.cjs +375 -0
  19. package/dist/pagination/PaginationEngine.cjs.map +1 -0
  20. package/dist/pagination/PaginationEngine.d.cts +117 -0
  21. package/dist/pagination/PaginationEngine.d.ts +117 -0
  22. package/dist/pagination/PaginationEngine.js +369 -0
  23. package/dist/pagination/PaginationEngine.js.map +1 -0
  24. package/dist/plugins/index.cjs +874 -0
  25. package/dist/plugins/index.cjs.map +1 -0
  26. package/dist/plugins/index.d.cts +275 -0
  27. package/dist/plugins/index.d.ts +275 -0
  28. package/dist/plugins/index.js +857 -0
  29. package/dist/plugins/index.js.map +1 -0
  30. package/dist/types-Nxhmi1aI.d.cts +510 -0
  31. package/dist/types-Nxhmi1aI.d.ts +510 -0
  32. package/dist/utils/index.cjs +667 -0
  33. package/dist/utils/index.cjs.map +1 -0
  34. package/dist/utils/index.d.cts +189 -0
  35. package/dist/utils/index.d.ts +189 -0
  36. package/dist/utils/index.js +643 -0
  37. package/dist/utils/index.js.map +1 -0
  38. package/package.json +54 -24
  39. package/src/Repository.js +0 -225
  40. package/src/actions/aggregate.js +0 -191
  41. package/src/actions/create.js +0 -59
  42. package/src/actions/delete.js +0 -88
  43. package/src/actions/index.js +0 -11
  44. package/src/actions/read.js +0 -156
  45. package/src/actions/update.js +0 -176
  46. package/src/hooks/lifecycle.js +0 -146
  47. package/src/index.js +0 -60
  48. package/src/plugins/aggregate-helpers.plugin.js +0 -71
  49. package/src/plugins/audit-log.plugin.js +0 -60
  50. package/src/plugins/batch-operations.plugin.js +0 -66
  51. package/src/plugins/field-filter.plugin.js +0 -27
  52. package/src/plugins/index.js +0 -19
  53. package/src/plugins/method-registry.plugin.js +0 -140
  54. package/src/plugins/mongo-operations.plugin.js +0 -313
  55. package/src/plugins/soft-delete.plugin.js +0 -46
  56. package/src/plugins/subdocument.plugin.js +0 -66
  57. package/src/plugins/timestamp.plugin.js +0 -19
  58. package/src/plugins/validation-chain.plugin.js +0 -145
  59. package/src/utils/field-selection.js +0 -156
  60. package/src/utils/index.js +0 -12
  61. package/types/actions/index.d.ts +0 -121
  62. package/types/index.d.ts +0 -104
  63. package/types/plugins/index.d.ts +0 -88
  64. package/types/utils/index.d.ts +0 -24
package/dist/index.js ADDED
@@ -0,0 +1,2108 @@
1
+ import mongoose from 'mongoose';
2
+
3
+ var __defProp = Object.defineProperty;
4
+ var __export = (target, all) => {
5
+ for (var name in all)
6
+ __defProp(target, name, { get: all[name], enumerable: true });
7
+ };
8
+
9
+ // src/utils/error.ts
10
+ function createError(status, message) {
11
+ const error = new Error(message);
12
+ error.status = status;
13
+ return error;
14
+ }
15
+
16
+ // src/actions/create.ts
17
+ var create_exports = {};
18
+ __export(create_exports, {
19
+ create: () => create,
20
+ createDefault: () => createDefault,
21
+ createMany: () => createMany,
22
+ upsert: () => upsert
23
+ });
24
+ async function create(Model, data, options = {}) {
25
+ const document = new Model(data);
26
+ await document.save({ session: options.session });
27
+ return document;
28
+ }
29
+ async function createMany(Model, dataArray, options = {}) {
30
+ return Model.insertMany(dataArray, {
31
+ session: options.session,
32
+ ordered: options.ordered !== false
33
+ });
34
+ }
35
+ async function createDefault(Model, overrides = {}, options = {}) {
36
+ const defaults = {};
37
+ Model.schema.eachPath((path, schemaType) => {
38
+ const schemaOptions = schemaType.options;
39
+ if (schemaOptions.default !== void 0 && path !== "_id") {
40
+ defaults[path] = typeof schemaOptions.default === "function" ? schemaOptions.default() : schemaOptions.default;
41
+ }
42
+ });
43
+ return create(Model, { ...defaults, ...overrides }, options);
44
+ }
45
+ async function upsert(Model, query, data, options = {}) {
46
+ return Model.findOneAndUpdate(
47
+ query,
48
+ { $setOnInsert: data },
49
+ {
50
+ upsert: true,
51
+ new: true,
52
+ runValidators: true,
53
+ session: options.session,
54
+ ...options.updatePipeline !== void 0 ? { updatePipeline: options.updatePipeline } : {}
55
+ }
56
+ );
57
+ }
58
+
59
+ // src/actions/read.ts
60
+ var read_exports = {};
61
+ __export(read_exports, {
62
+ count: () => count,
63
+ exists: () => exists,
64
+ getAll: () => getAll,
65
+ getById: () => getById,
66
+ getByQuery: () => getByQuery,
67
+ getOrCreate: () => getOrCreate,
68
+ tryGetByQuery: () => tryGetByQuery
69
+ });
70
+ function parsePopulate(populate) {
71
+ if (!populate) return [];
72
+ if (typeof populate === "string") {
73
+ return populate.split(",").map((p) => p.trim());
74
+ }
75
+ if (Array.isArray(populate)) {
76
+ return populate.map((p) => typeof p === "string" ? p.trim() : p);
77
+ }
78
+ return [populate];
79
+ }
80
+ async function getById(Model, id, options = {}) {
81
+ const query = options.query ? Model.findOne({ _id: id, ...options.query }) : Model.findById(id);
82
+ if (options.select) query.select(options.select);
83
+ if (options.populate) query.populate(parsePopulate(options.populate));
84
+ if (options.lean) query.lean();
85
+ if (options.session) query.session(options.session);
86
+ const document = await query.exec();
87
+ if (!document && options.throwOnNotFound !== false) {
88
+ throw createError(404, "Document not found");
89
+ }
90
+ return document;
91
+ }
92
+ async function getByQuery(Model, query, options = {}) {
93
+ const mongoQuery = Model.findOne(query);
94
+ if (options.select) mongoQuery.select(options.select);
95
+ if (options.populate) mongoQuery.populate(parsePopulate(options.populate));
96
+ if (options.lean) mongoQuery.lean();
97
+ if (options.session) mongoQuery.session(options.session);
98
+ const document = await mongoQuery.exec();
99
+ if (!document && options.throwOnNotFound !== false) {
100
+ throw createError(404, "Document not found");
101
+ }
102
+ return document;
103
+ }
104
+ async function tryGetByQuery(Model, query, options = {}) {
105
+ return getByQuery(Model, query, { ...options, throwOnNotFound: false });
106
+ }
107
+ async function getAll(Model, query = {}, options = {}) {
108
+ let mongoQuery = Model.find(query);
109
+ if (options.select) mongoQuery = mongoQuery.select(options.select);
110
+ if (options.populate) mongoQuery = mongoQuery.populate(parsePopulate(options.populate));
111
+ if (options.sort) mongoQuery = mongoQuery.sort(options.sort);
112
+ if (options.limit) mongoQuery = mongoQuery.limit(options.limit);
113
+ if (options.skip) mongoQuery = mongoQuery.skip(options.skip);
114
+ mongoQuery = mongoQuery.lean(options.lean !== false);
115
+ if (options.session) mongoQuery = mongoQuery.session(options.session);
116
+ return mongoQuery.exec();
117
+ }
118
+ async function getOrCreate(Model, query, createData, options = {}) {
119
+ return Model.findOneAndUpdate(
120
+ query,
121
+ { $setOnInsert: createData },
122
+ {
123
+ upsert: true,
124
+ new: true,
125
+ runValidators: true,
126
+ session: options.session,
127
+ ...options.updatePipeline !== void 0 ? { updatePipeline: options.updatePipeline } : {}
128
+ }
129
+ );
130
+ }
131
+ async function count(Model, query = {}, options = {}) {
132
+ return Model.countDocuments(query).session(options.session ?? null);
133
+ }
134
+ async function exists(Model, query, options = {}) {
135
+ return Model.exists(query).session(options.session ?? null);
136
+ }
137
+
138
+ // src/actions/update.ts
139
+ var update_exports = {};
140
+ __export(update_exports, {
141
+ increment: () => increment,
142
+ pullFromArray: () => pullFromArray,
143
+ pushToArray: () => pushToArray,
144
+ update: () => update,
145
+ updateByQuery: () => updateByQuery,
146
+ updateMany: () => updateMany,
147
+ updateWithConstraints: () => updateWithConstraints,
148
+ updateWithValidation: () => updateWithValidation
149
+ });
150
+ function parsePopulate2(populate) {
151
+ if (!populate) return [];
152
+ if (typeof populate === "string") {
153
+ return populate.split(",").map((p) => p.trim());
154
+ }
155
+ if (Array.isArray(populate)) {
156
+ return populate.map((p) => typeof p === "string" ? p.trim() : p);
157
+ }
158
+ return [populate];
159
+ }
160
+ async function update(Model, id, data, options = {}) {
161
+ const document = await Model.findByIdAndUpdate(id, data, {
162
+ new: true,
163
+ runValidators: true,
164
+ session: options.session,
165
+ ...options.updatePipeline !== void 0 ? { updatePipeline: options.updatePipeline } : {}
166
+ }).select(options.select || "").populate(parsePopulate2(options.populate)).lean(options.lean ?? false);
167
+ if (!document) {
168
+ throw createError(404, "Document not found");
169
+ }
170
+ return document;
171
+ }
172
+ async function updateWithConstraints(Model, id, data, constraints = {}, options = {}) {
173
+ const query = { _id: id, ...constraints };
174
+ const document = await Model.findOneAndUpdate(query, data, {
175
+ new: true,
176
+ runValidators: true,
177
+ session: options.session,
178
+ ...options.updatePipeline !== void 0 ? { updatePipeline: options.updatePipeline } : {}
179
+ }).select(options.select || "").populate(parsePopulate2(options.populate)).lean(options.lean ?? false);
180
+ return document;
181
+ }
182
+ async function updateWithValidation(Model, id, data, validationOptions = {}, options = {}) {
183
+ const { buildConstraints, validateUpdate } = validationOptions;
184
+ if (buildConstraints) {
185
+ const constraints = buildConstraints(data);
186
+ const document = await updateWithConstraints(Model, id, data, constraints, options);
187
+ if (document) {
188
+ return { success: true, data: document };
189
+ }
190
+ }
191
+ const existing = await Model.findById(id).select(options.select || "").lean();
192
+ if (!existing) {
193
+ return {
194
+ success: false,
195
+ error: {
196
+ code: 404,
197
+ message: "Document not found"
198
+ }
199
+ };
200
+ }
201
+ if (validateUpdate) {
202
+ const validation = validateUpdate(existing, data);
203
+ if (!validation.valid) {
204
+ return {
205
+ success: false,
206
+ error: {
207
+ code: 403,
208
+ message: validation.message || "Update not allowed",
209
+ violations: validation.violations
210
+ }
211
+ };
212
+ }
213
+ }
214
+ const updated = await update(Model, id, data, options);
215
+ return { success: true, data: updated };
216
+ }
217
+ async function updateMany(Model, query, data, options = {}) {
218
+ const result = await Model.updateMany(query, data, {
219
+ runValidators: true,
220
+ session: options.session,
221
+ ...options.updatePipeline !== void 0 ? { updatePipeline: options.updatePipeline } : {}
222
+ });
223
+ return {
224
+ matchedCount: result.matchedCount,
225
+ modifiedCount: result.modifiedCount
226
+ };
227
+ }
228
+ async function updateByQuery(Model, query, data, options = {}) {
229
+ const document = await Model.findOneAndUpdate(query, data, {
230
+ new: true,
231
+ runValidators: true,
232
+ session: options.session,
233
+ ...options.updatePipeline !== void 0 ? { updatePipeline: options.updatePipeline } : {}
234
+ }).select(options.select || "").populate(parsePopulate2(options.populate)).lean(options.lean ?? false);
235
+ if (!document && options.throwOnNotFound !== false) {
236
+ throw createError(404, "Document not found");
237
+ }
238
+ return document;
239
+ }
240
+ async function increment(Model, id, field, value = 1, options = {}) {
241
+ return update(Model, id, { $inc: { [field]: value } }, options);
242
+ }
243
+ async function pushToArray(Model, id, field, value, options = {}) {
244
+ return update(Model, id, { $push: { [field]: value } }, options);
245
+ }
246
+ async function pullFromArray(Model, id, field, value, options = {}) {
247
+ return update(Model, id, { $pull: { [field]: value } }, options);
248
+ }
249
+
250
+ // src/actions/delete.ts
251
+ var delete_exports = {};
252
+ __export(delete_exports, {
253
+ deleteById: () => deleteById,
254
+ deleteByQuery: () => deleteByQuery,
255
+ deleteMany: () => deleteMany,
256
+ restore: () => restore,
257
+ softDelete: () => softDelete
258
+ });
259
+ async function deleteById(Model, id, options = {}) {
260
+ const document = await Model.findByIdAndDelete(id).session(options.session ?? null);
261
+ if (!document) {
262
+ throw createError(404, "Document not found");
263
+ }
264
+ return { success: true, message: "Deleted successfully" };
265
+ }
266
+ async function deleteMany(Model, query, options = {}) {
267
+ const result = await Model.deleteMany(query).session(options.session ?? null);
268
+ return {
269
+ success: true,
270
+ count: result.deletedCount,
271
+ message: "Deleted successfully"
272
+ };
273
+ }
274
+ async function deleteByQuery(Model, query, options = {}) {
275
+ const document = await Model.findOneAndDelete(query).session(options.session ?? null);
276
+ if (!document && options.throwOnNotFound !== false) {
277
+ throw createError(404, "Document not found");
278
+ }
279
+ return { success: true, message: "Deleted successfully" };
280
+ }
281
+ async function softDelete(Model, id, options = {}) {
282
+ const document = await Model.findByIdAndUpdate(
283
+ id,
284
+ {
285
+ deleted: true,
286
+ deletedAt: /* @__PURE__ */ new Date(),
287
+ deletedBy: options.userId
288
+ },
289
+ { new: true, session: options.session }
290
+ );
291
+ if (!document) {
292
+ throw createError(404, "Document not found");
293
+ }
294
+ return { success: true, message: "Soft deleted successfully" };
295
+ }
296
+ async function restore(Model, id, options = {}) {
297
+ const document = await Model.findByIdAndUpdate(
298
+ id,
299
+ {
300
+ deleted: false,
301
+ deletedAt: null,
302
+ deletedBy: null
303
+ },
304
+ { new: true, session: options.session }
305
+ );
306
+ if (!document) {
307
+ throw createError(404, "Document not found");
308
+ }
309
+ return { success: true, message: "Restored successfully" };
310
+ }
311
+
312
+ // src/actions/aggregate.ts
313
+ var aggregate_exports = {};
314
+ __export(aggregate_exports, {
315
+ aggregate: () => aggregate,
316
+ aggregatePaginate: () => aggregatePaginate,
317
+ average: () => average,
318
+ countBy: () => countBy,
319
+ distinct: () => distinct,
320
+ facet: () => facet,
321
+ groupBy: () => groupBy,
322
+ lookup: () => lookup,
323
+ minMax: () => minMax,
324
+ sum: () => sum,
325
+ unwind: () => unwind
326
+ });
327
+ async function aggregate(Model, pipeline, options = {}) {
328
+ const aggregation = Model.aggregate(pipeline);
329
+ if (options.session) {
330
+ aggregation.session(options.session);
331
+ }
332
+ return aggregation.exec();
333
+ }
334
+ async function aggregatePaginate(Model, pipeline, options = {}) {
335
+ const page = parseInt(String(options.page || 1), 10);
336
+ const limit = parseInt(String(options.limit || 10), 10);
337
+ const skip = (page - 1) * limit;
338
+ const SAFE_LIMIT = 1e3;
339
+ if (limit > SAFE_LIMIT) {
340
+ console.warn(
341
+ `[mongokit] Large aggregation limit (${limit}). $facet results must be <16MB. Consider using Repository.aggregatePaginate() for safer handling of large datasets.`
342
+ );
343
+ }
344
+ const facetPipeline = [
345
+ ...pipeline,
346
+ {
347
+ $facet: {
348
+ docs: [{ $skip: skip }, { $limit: limit }],
349
+ total: [{ $count: "count" }]
350
+ }
351
+ }
352
+ ];
353
+ const aggregation = Model.aggregate(facetPipeline);
354
+ if (options.session) {
355
+ aggregation.session(options.session);
356
+ }
357
+ const [result] = await aggregation.exec();
358
+ const docs = result.docs || [];
359
+ const total = result.total[0]?.count || 0;
360
+ const pages = Math.ceil(total / limit);
361
+ return {
362
+ docs,
363
+ total,
364
+ page,
365
+ limit,
366
+ pages,
367
+ hasNext: page < pages,
368
+ hasPrev: page > 1
369
+ };
370
+ }
371
+ async function groupBy(Model, field, options = {}) {
372
+ const pipeline = [
373
+ { $group: { _id: `$${field}`, count: { $sum: 1 } } },
374
+ { $sort: { count: -1 } }
375
+ ];
376
+ if (options.limit) {
377
+ pipeline.push({ $limit: options.limit });
378
+ }
379
+ return aggregate(Model, pipeline, options);
380
+ }
381
+ async function countBy(Model, field, query = {}, options = {}) {
382
+ const pipeline = [];
383
+ if (Object.keys(query).length > 0) {
384
+ pipeline.push({ $match: query });
385
+ }
386
+ pipeline.push(
387
+ { $group: { _id: `$${field}`, count: { $sum: 1 } } },
388
+ { $sort: { count: -1 } }
389
+ );
390
+ return aggregate(Model, pipeline, options);
391
+ }
392
+ async function lookup(Model, lookupOptions) {
393
+ const { from, localField, foreignField, as, pipeline = [], query = {}, options = {} } = lookupOptions;
394
+ const aggPipeline = [];
395
+ if (Object.keys(query).length > 0) {
396
+ aggPipeline.push({ $match: query });
397
+ }
398
+ aggPipeline.push({
399
+ $lookup: {
400
+ from,
401
+ localField,
402
+ foreignField,
403
+ as,
404
+ ...pipeline.length > 0 ? { pipeline } : {}
405
+ }
406
+ });
407
+ return aggregate(Model, aggPipeline, options);
408
+ }
409
+ async function unwind(Model, field, options = {}) {
410
+ const pipeline = [
411
+ {
412
+ $unwind: {
413
+ path: `$${field}`,
414
+ preserveNullAndEmptyArrays: options.preserveEmpty !== false
415
+ }
416
+ }
417
+ ];
418
+ return aggregate(Model, pipeline, { session: options.session });
419
+ }
420
+ async function facet(Model, facets, options = {}) {
421
+ const pipeline = [{ $facet: facets }];
422
+ return aggregate(Model, pipeline, options);
423
+ }
424
+ async function distinct(Model, field, query = {}, options = {}) {
425
+ return Model.distinct(field, query).session(options.session ?? null);
426
+ }
427
+ async function sum(Model, field, query = {}, options = {}) {
428
+ const pipeline = [];
429
+ if (Object.keys(query).length > 0) {
430
+ pipeline.push({ $match: query });
431
+ }
432
+ pipeline.push({
433
+ $group: {
434
+ _id: null,
435
+ total: { $sum: `$${field}` }
436
+ }
437
+ });
438
+ const result = await aggregate(Model, pipeline, options);
439
+ return result[0]?.total || 0;
440
+ }
441
+ async function average(Model, field, query = {}, options = {}) {
442
+ const pipeline = [];
443
+ if (Object.keys(query).length > 0) {
444
+ pipeline.push({ $match: query });
445
+ }
446
+ pipeline.push({
447
+ $group: {
448
+ _id: null,
449
+ average: { $avg: `$${field}` }
450
+ }
451
+ });
452
+ const result = await aggregate(Model, pipeline, options);
453
+ return result[0]?.average || 0;
454
+ }
455
+ async function minMax(Model, field, query = {}, options = {}) {
456
+ const pipeline = [];
457
+ if (Object.keys(query).length > 0) {
458
+ pipeline.push({ $match: query });
459
+ }
460
+ pipeline.push({
461
+ $group: {
462
+ _id: null,
463
+ min: { $min: `$${field}` },
464
+ max: { $max: `$${field}` }
465
+ }
466
+ });
467
+ const result = await aggregate(Model, pipeline, options);
468
+ return result[0] || { min: null, max: null };
469
+ }
470
+ function encodeCursor(doc, primaryField, sort, version = 1) {
471
+ const primaryValue = doc[primaryField];
472
+ const idValue = doc._id;
473
+ const payload = {
474
+ v: serializeValue(primaryValue),
475
+ t: getValueType(primaryValue),
476
+ id: serializeValue(idValue),
477
+ idType: getValueType(idValue),
478
+ sort,
479
+ ver: version
480
+ };
481
+ return Buffer.from(JSON.stringify(payload)).toString("base64");
482
+ }
483
+ function decodeCursor(token) {
484
+ try {
485
+ const json = Buffer.from(token, "base64").toString("utf-8");
486
+ const payload = JSON.parse(json);
487
+ return {
488
+ value: rehydrateValue(payload.v, payload.t),
489
+ id: rehydrateValue(payload.id, payload.idType),
490
+ sort: payload.sort,
491
+ version: payload.ver
492
+ };
493
+ } catch {
494
+ throw new Error("Invalid cursor token");
495
+ }
496
+ }
497
+ function validateCursorSort(cursorSort, currentSort) {
498
+ const cursorSortStr = JSON.stringify(cursorSort);
499
+ const currentSortStr = JSON.stringify(currentSort);
500
+ if (cursorSortStr !== currentSortStr) {
501
+ throw new Error("Cursor sort does not match current query sort");
502
+ }
503
+ }
504
+ function validateCursorVersion(cursorVersion, expectedVersion) {
505
+ if (cursorVersion !== expectedVersion) {
506
+ throw new Error(`Cursor version ${cursorVersion} does not match expected version ${expectedVersion}`);
507
+ }
508
+ }
509
+ function serializeValue(value) {
510
+ if (value instanceof Date) return value.toISOString();
511
+ if (value instanceof mongoose.Types.ObjectId) return value.toString();
512
+ return value;
513
+ }
514
+ function getValueType(value) {
515
+ if (value instanceof Date) return "date";
516
+ if (value instanceof mongoose.Types.ObjectId) return "objectid";
517
+ if (typeof value === "boolean") return "boolean";
518
+ if (typeof value === "number") return "number";
519
+ if (typeof value === "string") return "string";
520
+ return "unknown";
521
+ }
522
+ function rehydrateValue(serialized, type) {
523
+ switch (type) {
524
+ case "date":
525
+ return new Date(serialized);
526
+ case "objectid":
527
+ return new mongoose.Types.ObjectId(serialized);
528
+ case "boolean":
529
+ return Boolean(serialized);
530
+ case "number":
531
+ return Number(serialized);
532
+ default:
533
+ return serialized;
534
+ }
535
+ }
536
+
537
+ // src/pagination/utils/sort.ts
538
+ function normalizeSort(sort) {
539
+ const normalized = {};
540
+ Object.keys(sort).forEach((key) => {
541
+ if (key !== "_id") normalized[key] = sort[key];
542
+ });
543
+ if (sort._id !== void 0) {
544
+ normalized._id = sort._id;
545
+ }
546
+ return normalized;
547
+ }
548
+ function validateKeysetSort(sort) {
549
+ const keys = Object.keys(sort);
550
+ if (keys.length === 1 && keys[0] !== "_id") {
551
+ const field = keys[0];
552
+ const direction = sort[field];
553
+ return normalizeSort({ [field]: direction, _id: direction });
554
+ }
555
+ if (keys.length === 1 && keys[0] === "_id") {
556
+ return normalizeSort(sort);
557
+ }
558
+ if (keys.length === 2) {
559
+ if (!keys.includes("_id")) {
560
+ throw new Error("Keyset pagination requires _id as tie-breaker");
561
+ }
562
+ const primaryField = keys.find((k) => k !== "_id");
563
+ const primaryDirection = sort[primaryField];
564
+ const idDirection = sort._id;
565
+ if (primaryDirection !== idDirection) {
566
+ throw new Error("_id direction must match primary field direction");
567
+ }
568
+ return normalizeSort(sort);
569
+ }
570
+ throw new Error("Keyset pagination only supports single field + _id");
571
+ }
572
+ function getPrimaryField(sort) {
573
+ const keys = Object.keys(sort);
574
+ return keys.find((k) => k !== "_id") || "_id";
575
+ }
576
+
577
+ // src/pagination/utils/filter.ts
578
+ function buildKeysetFilter(baseFilters, sort, cursorValue, cursorId) {
579
+ const primaryField = Object.keys(sort).find((k) => k !== "_id") || "_id";
580
+ const direction = sort[primaryField];
581
+ const operator = direction === 1 ? "$gt" : "$lt";
582
+ return {
583
+ ...baseFilters,
584
+ $or: [
585
+ { [primaryField]: { [operator]: cursorValue } },
586
+ {
587
+ [primaryField]: cursorValue,
588
+ _id: { [operator]: cursorId }
589
+ }
590
+ ]
591
+ };
592
+ }
593
+
594
+ // src/pagination/utils/limits.ts
595
+ function validateLimit(limit, config) {
596
+ const parsed = Number(limit);
597
+ if (!Number.isFinite(parsed) || parsed < 1) {
598
+ return config.defaultLimit || 10;
599
+ }
600
+ return Math.min(Math.floor(parsed), config.maxLimit || 100);
601
+ }
602
+ function validatePage(page, config) {
603
+ const parsed = Number(page);
604
+ if (!Number.isFinite(parsed) || parsed < 1) {
605
+ return 1;
606
+ }
607
+ const sanitized = Math.floor(parsed);
608
+ if (sanitized > (config.maxPage || 1e4)) {
609
+ throw new Error(`Page ${sanitized} exceeds maximum ${config.maxPage || 1e4}`);
610
+ }
611
+ return sanitized;
612
+ }
613
+ function shouldWarnDeepPagination(page, threshold) {
614
+ return page > threshold;
615
+ }
616
+ function calculateSkip(page, limit) {
617
+ return (page - 1) * limit;
618
+ }
619
+ function calculateTotalPages(total, limit) {
620
+ return Math.ceil(total / limit);
621
+ }
622
+
623
+ // src/pagination/PaginationEngine.ts
624
+ var PaginationEngine = class {
625
+ Model;
626
+ config;
627
+ /**
628
+ * Create a new pagination engine
629
+ *
630
+ * @param Model - Mongoose model to paginate
631
+ * @param config - Pagination configuration
632
+ */
633
+ constructor(Model, config = {}) {
634
+ this.Model = Model;
635
+ this.config = {
636
+ defaultLimit: config.defaultLimit || 10,
637
+ maxLimit: config.maxLimit || 100,
638
+ maxPage: config.maxPage || 1e4,
639
+ deepPageThreshold: config.deepPageThreshold || 100,
640
+ cursorVersion: config.cursorVersion || 1,
641
+ useEstimatedCount: config.useEstimatedCount || false
642
+ };
643
+ }
644
+ /**
645
+ * Offset-based pagination using skip/limit
646
+ * Best for small datasets and when users need random page access
647
+ * O(n) performance - slower for deep pages
648
+ *
649
+ * @param options - Pagination options
650
+ * @returns Pagination result with total count
651
+ *
652
+ * @example
653
+ * const result = await engine.paginate({
654
+ * filters: { status: 'active' },
655
+ * sort: { createdAt: -1 },
656
+ * page: 1,
657
+ * limit: 20
658
+ * });
659
+ * console.log(result.docs, result.total, result.hasNext);
660
+ */
661
+ async paginate(options = {}) {
662
+ const {
663
+ filters = {},
664
+ sort = { _id: -1 },
665
+ page = 1,
666
+ limit = this.config.defaultLimit,
667
+ select,
668
+ populate = [],
669
+ lean = true,
670
+ session
671
+ } = options;
672
+ const sanitizedPage = validatePage(page, this.config);
673
+ const sanitizedLimit = validateLimit(limit, this.config);
674
+ const skip = calculateSkip(sanitizedPage, sanitizedLimit);
675
+ let query = this.Model.find(filters);
676
+ if (select) query = query.select(select);
677
+ if (populate && (Array.isArray(populate) ? populate.length : populate)) {
678
+ query = query.populate(populate);
679
+ }
680
+ query = query.sort(sort).skip(skip).limit(sanitizedLimit).lean(lean);
681
+ if (session) query = query.session(session);
682
+ const hasFilters = Object.keys(filters).length > 0;
683
+ const useEstimated = this.config.useEstimatedCount && !hasFilters;
684
+ const [docs, total] = await Promise.all([
685
+ query.exec(),
686
+ useEstimated ? this.Model.estimatedDocumentCount() : this.Model.countDocuments(filters).session(session ?? null)
687
+ ]);
688
+ const totalPages = calculateTotalPages(total, sanitizedLimit);
689
+ const warning = shouldWarnDeepPagination(sanitizedPage, this.config.deepPageThreshold) ? `Deep pagination (page ${sanitizedPage}). Consider getAll({ after, sort, limit }) for better performance.` : void 0;
690
+ return {
691
+ method: "offset",
692
+ docs,
693
+ page: sanitizedPage,
694
+ limit: sanitizedLimit,
695
+ total,
696
+ pages: totalPages,
697
+ hasNext: sanitizedPage < totalPages,
698
+ hasPrev: sanitizedPage > 1,
699
+ ...warning && { warning }
700
+ };
701
+ }
702
+ /**
703
+ * Keyset (cursor-based) pagination for high-performance streaming
704
+ * Best for large datasets, infinite scroll, real-time feeds
705
+ * O(1) performance - consistent speed regardless of position
706
+ *
707
+ * @param options - Pagination options (sort is required)
708
+ * @returns Pagination result with next cursor
709
+ *
710
+ * @example
711
+ * // First page
712
+ * const page1 = await engine.stream({
713
+ * sort: { createdAt: -1 },
714
+ * limit: 20
715
+ * });
716
+ *
717
+ * // Next page using cursor
718
+ * const page2 = await engine.stream({
719
+ * sort: { createdAt: -1 },
720
+ * after: page1.next,
721
+ * limit: 20
722
+ * });
723
+ */
724
+ async stream(options) {
725
+ const {
726
+ filters = {},
727
+ sort,
728
+ after,
729
+ limit = this.config.defaultLimit,
730
+ select,
731
+ populate = [],
732
+ lean = true,
733
+ session
734
+ } = options;
735
+ if (!sort) {
736
+ throw createError(400, "sort is required for keyset pagination");
737
+ }
738
+ const sanitizedLimit = validateLimit(limit, this.config);
739
+ const normalizedSort = validateKeysetSort(sort);
740
+ let query = { ...filters };
741
+ if (after) {
742
+ const cursor = decodeCursor(after);
743
+ validateCursorVersion(cursor.version, this.config.cursorVersion);
744
+ validateCursorSort(cursor.sort, normalizedSort);
745
+ query = buildKeysetFilter(query, normalizedSort, cursor.value, cursor.id);
746
+ }
747
+ let mongoQuery = this.Model.find(query);
748
+ if (select) mongoQuery = mongoQuery.select(select);
749
+ if (populate && (Array.isArray(populate) ? populate.length : populate)) {
750
+ mongoQuery = mongoQuery.populate(populate);
751
+ }
752
+ mongoQuery = mongoQuery.sort(normalizedSort).limit(sanitizedLimit + 1).lean(lean);
753
+ if (session) mongoQuery = mongoQuery.session(session);
754
+ const docs = await mongoQuery.exec();
755
+ const hasMore = docs.length > sanitizedLimit;
756
+ if (hasMore) docs.pop();
757
+ const primaryField = getPrimaryField(normalizedSort);
758
+ const nextCursor = hasMore && docs.length > 0 ? encodeCursor(docs[docs.length - 1], primaryField, normalizedSort, this.config.cursorVersion) : null;
759
+ return {
760
+ method: "keyset",
761
+ docs,
762
+ limit: sanitizedLimit,
763
+ hasMore,
764
+ next: nextCursor
765
+ };
766
+ }
767
+ /**
768
+ * Aggregate pipeline with pagination
769
+ * Best for complex queries requiring aggregation stages
770
+ * Uses $facet to combine results and count in single query
771
+ *
772
+ * @param options - Aggregation options
773
+ * @returns Pagination result with total count
774
+ *
775
+ * @example
776
+ * const result = await engine.aggregatePaginate({
777
+ * pipeline: [
778
+ * { $match: { status: 'active' } },
779
+ * { $group: { _id: '$category', count: { $sum: 1 } } },
780
+ * { $sort: { count: -1 } }
781
+ * ],
782
+ * page: 1,
783
+ * limit: 20
784
+ * });
785
+ */
786
+ async aggregatePaginate(options = {}) {
787
+ const {
788
+ pipeline = [],
789
+ page = 1,
790
+ limit = this.config.defaultLimit,
791
+ session
792
+ } = options;
793
+ const sanitizedPage = validatePage(page, this.config);
794
+ const sanitizedLimit = validateLimit(limit, this.config);
795
+ const skip = calculateSkip(sanitizedPage, sanitizedLimit);
796
+ const facetPipeline = [
797
+ ...pipeline,
798
+ {
799
+ $facet: {
800
+ docs: [{ $skip: skip }, { $limit: sanitizedLimit }],
801
+ total: [{ $count: "count" }]
802
+ }
803
+ }
804
+ ];
805
+ const aggregation = this.Model.aggregate(facetPipeline);
806
+ if (session) aggregation.session(session);
807
+ const [result] = await aggregation.exec();
808
+ const docs = result.docs;
809
+ const total = result.total[0]?.count || 0;
810
+ const totalPages = calculateTotalPages(total, sanitizedLimit);
811
+ const warning = shouldWarnDeepPagination(sanitizedPage, this.config.deepPageThreshold) ? `Deep pagination in aggregate (page ${sanitizedPage}). Uses $skip internally.` : void 0;
812
+ return {
813
+ method: "aggregate",
814
+ docs,
815
+ page: sanitizedPage,
816
+ limit: sanitizedLimit,
817
+ total,
818
+ pages: totalPages,
819
+ hasNext: sanitizedPage < totalPages,
820
+ hasPrev: sanitizedPage > 1,
821
+ ...warning && { warning }
822
+ };
823
+ }
824
+ };
825
+
826
+ // src/Repository.ts
827
+ var Repository = class {
828
+ Model;
829
+ model;
830
+ _hooks;
831
+ _pagination;
832
+ constructor(Model, plugins = [], paginationConfig = {}) {
833
+ this.Model = Model;
834
+ this.model = Model.modelName;
835
+ this._hooks = /* @__PURE__ */ new Map();
836
+ this._pagination = new PaginationEngine(Model, paginationConfig);
837
+ plugins.forEach((plugin) => this.use(plugin));
838
+ }
839
+ /**
840
+ * Register a plugin
841
+ */
842
+ use(plugin) {
843
+ if (typeof plugin === "function") {
844
+ plugin(this);
845
+ } else if (plugin && typeof plugin.apply === "function") {
846
+ plugin.apply(this);
847
+ }
848
+ return this;
849
+ }
850
+ /**
851
+ * Register event listener
852
+ */
853
+ on(event, listener) {
854
+ if (!this._hooks.has(event)) {
855
+ this._hooks.set(event, []);
856
+ }
857
+ this._hooks.get(event).push(listener);
858
+ return this;
859
+ }
860
+ /**
861
+ * Emit event
862
+ */
863
+ emit(event, data) {
864
+ const listeners = this._hooks.get(event) || [];
865
+ listeners.forEach((listener) => listener(data));
866
+ }
867
+ /**
868
+ * Create single document
869
+ */
870
+ async create(data, options = {}) {
871
+ const context = await this._buildContext("create", { data, ...options });
872
+ try {
873
+ const result = await create(this.Model, context.data || data, options);
874
+ this.emit("after:create", { context, result });
875
+ return result;
876
+ } catch (error) {
877
+ this.emit("error:create", { context, error });
878
+ throw this._handleError(error);
879
+ }
880
+ }
881
+ /**
882
+ * Create multiple documents
883
+ */
884
+ async createMany(dataArray, options = {}) {
885
+ const context = await this._buildContext("createMany", { dataArray, ...options });
886
+ try {
887
+ const result = await createMany(this.Model, context.dataArray || dataArray, options);
888
+ this.emit("after:createMany", { context, result });
889
+ return result;
890
+ } catch (error) {
891
+ this.emit("error:createMany", { context, error });
892
+ throw this._handleError(error);
893
+ }
894
+ }
895
+ /**
896
+ * Get document by ID
897
+ */
898
+ async getById(id, options = {}) {
899
+ const context = await this._buildContext("getById", { id, ...options });
900
+ if (context._cacheHit) {
901
+ return context._cachedResult;
902
+ }
903
+ const result = await getById(this.Model, id, context);
904
+ this.emit("after:getById", { context, result });
905
+ return result;
906
+ }
907
+ /**
908
+ * Get single document by query
909
+ */
910
+ async getByQuery(query, options = {}) {
911
+ const context = await this._buildContext("getByQuery", { query, ...options });
912
+ if (context._cacheHit) {
913
+ return context._cachedResult;
914
+ }
915
+ const result = await getByQuery(this.Model, query, context);
916
+ this.emit("after:getByQuery", { context, result });
917
+ return result;
918
+ }
919
+ /**
920
+ * Unified pagination - auto-detects offset vs keyset based on params
921
+ *
922
+ * Auto-detection logic:
923
+ * - If params has 'cursor' or 'after' → uses keyset pagination (stream)
924
+ * - If params has 'pagination' or 'page' → uses offset pagination (paginate)
925
+ * - Else → defaults to offset pagination with page=1
926
+ *
927
+ * @example
928
+ * // Offset pagination (page-based)
929
+ * await repo.getAll({ page: 1, limit: 50, filters: { status: 'active' } });
930
+ * await repo.getAll({ pagination: { page: 2, limit: 20 } });
931
+ *
932
+ * // Keyset pagination (cursor-based)
933
+ * await repo.getAll({ cursor: 'eyJ2Ij...', limit: 50 });
934
+ * await repo.getAll({ after: 'eyJ2Ij...', sort: { createdAt: -1 } });
935
+ *
936
+ * // Simple query (defaults to page 1)
937
+ * await repo.getAll({ filters: { status: 'active' } });
938
+ *
939
+ * // Skip cache for fresh data
940
+ * await repo.getAll({ filters: { status: 'active' } }, { skipCache: true });
941
+ */
942
+ async getAll(params = {}, options = {}) {
943
+ const context = await this._buildContext("getAll", { ...params, ...options });
944
+ if (context._cacheHit) {
945
+ return context._cachedResult;
946
+ }
947
+ const hasPageParam = params.page !== void 0 || params.pagination;
948
+ const hasCursorParam = "cursor" in params || "after" in params;
949
+ const hasExplicitSort = params.sort !== void 0;
950
+ const useKeyset = !hasPageParam && (hasCursorParam || hasExplicitSort);
951
+ const filters = params.filters || {};
952
+ const search = params.search;
953
+ const sort = params.sort || "-createdAt";
954
+ const limit = params.limit || params.pagination?.limit || this._pagination.config.defaultLimit;
955
+ let query = { ...filters };
956
+ if (search) query.$text = { $search: search };
957
+ const paginationOptions = {
958
+ filters: query,
959
+ sort: this._parseSort(sort),
960
+ limit,
961
+ populate: this._parsePopulate(context.populate || options.populate),
962
+ select: context.select || options.select,
963
+ lean: context.lean ?? options.lean ?? true,
964
+ session: options.session
965
+ };
966
+ let result;
967
+ if (useKeyset) {
968
+ result = await this._pagination.stream({
969
+ ...paginationOptions,
970
+ sort: paginationOptions.sort,
971
+ // Required for keyset
972
+ after: params.cursor || params.after
973
+ });
974
+ } else {
975
+ const page = params.pagination?.page || params.page || 1;
976
+ result = await this._pagination.paginate({
977
+ ...paginationOptions,
978
+ page
979
+ });
980
+ }
981
+ this.emit("after:getAll", { context, result });
982
+ return result;
983
+ }
984
+ /**
985
+ * Get or create document
986
+ */
987
+ async getOrCreate(query, createData, options = {}) {
988
+ return getOrCreate(this.Model, query, createData, options);
989
+ }
990
+ /**
991
+ * Count documents
992
+ */
993
+ async count(query = {}, options = {}) {
994
+ return count(this.Model, query, options);
995
+ }
996
+ /**
997
+ * Check if document exists
998
+ */
999
+ async exists(query, options = {}) {
1000
+ return exists(this.Model, query, options);
1001
+ }
1002
+ /**
1003
+ * Update document by ID
1004
+ */
1005
+ async update(id, data, options = {}) {
1006
+ const context = await this._buildContext("update", { id, data, ...options });
1007
+ try {
1008
+ const result = await update(this.Model, id, context.data || data, context);
1009
+ this.emit("after:update", { context, result });
1010
+ return result;
1011
+ } catch (error) {
1012
+ this.emit("error:update", { context, error });
1013
+ throw this._handleError(error);
1014
+ }
1015
+ }
1016
+ /**
1017
+ * Delete document by ID
1018
+ */
1019
+ async delete(id, options = {}) {
1020
+ const context = await this._buildContext("delete", { id, ...options });
1021
+ try {
1022
+ if (context.softDeleted) {
1023
+ const result2 = { success: true, message: "Soft deleted successfully" };
1024
+ this.emit("after:delete", { context, result: result2 });
1025
+ return result2;
1026
+ }
1027
+ const result = await deleteById(this.Model, id, options);
1028
+ this.emit("after:delete", { context, result });
1029
+ return result;
1030
+ } catch (error) {
1031
+ this.emit("error:delete", { context, error });
1032
+ throw this._handleError(error);
1033
+ }
1034
+ }
1035
+ /**
1036
+ * Execute aggregation pipeline
1037
+ */
1038
+ async aggregate(pipeline, options = {}) {
1039
+ return aggregate(this.Model, pipeline, options);
1040
+ }
1041
+ /**
1042
+ * Aggregate pipeline with pagination
1043
+ * Best for: Complex queries, grouping, joins
1044
+ */
1045
+ async aggregatePaginate(options = {}) {
1046
+ const context = await this._buildContext("aggregatePaginate", options);
1047
+ return this._pagination.aggregatePaginate(context);
1048
+ }
1049
+ /**
1050
+ * Get distinct values
1051
+ */
1052
+ async distinct(field, query = {}, options = {}) {
1053
+ return distinct(this.Model, field, query, options);
1054
+ }
1055
+ /**
1056
+ * Execute callback within a transaction
1057
+ */
1058
+ async withTransaction(callback) {
1059
+ const session = await mongoose.startSession();
1060
+ session.startTransaction();
1061
+ try {
1062
+ const result = await callback(session);
1063
+ await session.commitTransaction();
1064
+ return result;
1065
+ } catch (error) {
1066
+ await session.abortTransaction();
1067
+ throw error;
1068
+ } finally {
1069
+ session.endSession();
1070
+ }
1071
+ }
1072
+ /**
1073
+ * Execute custom query with event emission
1074
+ */
1075
+ async _executeQuery(buildQuery) {
1076
+ const operation = buildQuery.name || "custom";
1077
+ const context = await this._buildContext(operation, {});
1078
+ try {
1079
+ const result = await buildQuery(this.Model);
1080
+ this.emit(`after:${operation}`, { context, result });
1081
+ return result;
1082
+ } catch (error) {
1083
+ this.emit(`error:${operation}`, { context, error });
1084
+ throw this._handleError(error);
1085
+ }
1086
+ }
1087
+ /**
1088
+ * Build operation context and run before hooks
1089
+ */
1090
+ async _buildContext(operation, options) {
1091
+ const context = { operation, model: this.model, ...options };
1092
+ const event = `before:${operation}`;
1093
+ const hooks = this._hooks.get(event) || [];
1094
+ for (const hook of hooks) {
1095
+ await hook(context);
1096
+ }
1097
+ return context;
1098
+ }
1099
+ /**
1100
+ * Parse sort string or object
1101
+ */
1102
+ _parseSort(sort) {
1103
+ if (!sort) return { createdAt: -1 };
1104
+ if (typeof sort === "object") return sort;
1105
+ const sortOrder = sort.startsWith("-") ? -1 : 1;
1106
+ const sortField = sort.startsWith("-") ? sort.substring(1) : sort;
1107
+ return { [sortField]: sortOrder };
1108
+ }
1109
+ /**
1110
+ * Parse populate specification
1111
+ */
1112
+ _parsePopulate(populate) {
1113
+ if (!populate) return [];
1114
+ if (typeof populate === "string") return populate.split(",").map((p) => p.trim());
1115
+ if (Array.isArray(populate)) return populate.map((p) => typeof p === "string" ? p.trim() : p);
1116
+ return [populate];
1117
+ }
1118
+ /**
1119
+ * Handle errors with proper HTTP status codes
1120
+ */
1121
+ _handleError(error) {
1122
+ if (error instanceof mongoose.Error.ValidationError) {
1123
+ const messages = Object.values(error.errors).map((err) => err.message);
1124
+ return createError(400, `Validation Error: ${messages.join(", ")}`);
1125
+ }
1126
+ if (error instanceof mongoose.Error.CastError) {
1127
+ return createError(400, `Invalid ${error.path}: ${error.value}`);
1128
+ }
1129
+ if (error.status && error.message) return error;
1130
+ return createError(500, error.message || "Internal Server Error");
1131
+ }
1132
+ };
1133
+
1134
+ // src/utils/field-selection.ts
1135
+ function getFieldsForUser(user, preset) {
1136
+ if (!preset) {
1137
+ throw new Error("Field preset is required");
1138
+ }
1139
+ const fields = [...preset.public || []];
1140
+ if (user) {
1141
+ fields.push(...preset.authenticated || []);
1142
+ const roles = Array.isArray(user.roles) ? user.roles : user.roles ? [user.roles] : [];
1143
+ if (roles.includes("admin") || roles.includes("superadmin")) {
1144
+ fields.push(...preset.admin || []);
1145
+ }
1146
+ }
1147
+ return [...new Set(fields)];
1148
+ }
1149
+ function getMongooseProjection(user, preset) {
1150
+ const fields = getFieldsForUser(user, preset);
1151
+ return fields.join(" ");
1152
+ }
1153
+ function filterObject(obj, allowedFields) {
1154
+ if (!obj || typeof obj !== "object" || Array.isArray(obj)) {
1155
+ return obj;
1156
+ }
1157
+ const filtered = {};
1158
+ for (const field of allowedFields) {
1159
+ if (field in obj) {
1160
+ filtered[field] = obj[field];
1161
+ }
1162
+ }
1163
+ return filtered;
1164
+ }
1165
+ function filterResponseData(data, preset, user = null) {
1166
+ const allowedFields = getFieldsForUser(user, preset);
1167
+ if (Array.isArray(data)) {
1168
+ return data.map((item) => filterObject(item, allowedFields));
1169
+ }
1170
+ return filterObject(data, allowedFields);
1171
+ }
1172
+ function createFieldPreset(config) {
1173
+ return {
1174
+ public: config.public || [],
1175
+ authenticated: config.authenticated || [],
1176
+ admin: config.admin || []
1177
+ };
1178
+ }
1179
+
1180
+ // src/plugins/field-filter.plugin.ts
1181
+ function fieldFilterPlugin(fieldPreset) {
1182
+ return {
1183
+ name: "fieldFilter",
1184
+ apply(repo) {
1185
+ const applyFieldFiltering = (context) => {
1186
+ if (!fieldPreset) return;
1187
+ const user = context.context?.user || context.user;
1188
+ const fields = getFieldsForUser(user, fieldPreset);
1189
+ const presetSelect = fields.join(" ");
1190
+ if (context.select) {
1191
+ context.select = `${presetSelect} ${context.select}`;
1192
+ } else {
1193
+ context.select = presetSelect;
1194
+ }
1195
+ };
1196
+ repo.on("before:getAll", applyFieldFiltering);
1197
+ repo.on("before:getById", applyFieldFiltering);
1198
+ repo.on("before:getByQuery", applyFieldFiltering);
1199
+ }
1200
+ };
1201
+ }
1202
+
1203
+ // src/plugins/timestamp.plugin.ts
1204
+ function timestampPlugin() {
1205
+ return {
1206
+ name: "timestamp",
1207
+ apply(repo) {
1208
+ repo.on("before:create", (context) => {
1209
+ if (!context.data) return;
1210
+ const now = /* @__PURE__ */ new Date();
1211
+ if (!context.data.createdAt) context.data.createdAt = now;
1212
+ if (!context.data.updatedAt) context.data.updatedAt = now;
1213
+ });
1214
+ repo.on("before:update", (context) => {
1215
+ if (!context.data) return;
1216
+ context.data.updatedAt = /* @__PURE__ */ new Date();
1217
+ });
1218
+ }
1219
+ };
1220
+ }
1221
+
1222
+ // src/plugins/audit-log.plugin.ts
1223
+ function auditLogPlugin(logger) {
1224
+ return {
1225
+ name: "auditLog",
1226
+ apply(repo) {
1227
+ repo.on("after:create", ({ context, result }) => {
1228
+ logger?.info?.("Document created", {
1229
+ model: context.model || repo.model,
1230
+ id: result?._id,
1231
+ userId: context.user?._id || context.user?.id,
1232
+ organizationId: context.organizationId
1233
+ });
1234
+ });
1235
+ repo.on("after:update", ({ context, result }) => {
1236
+ logger?.info?.("Document updated", {
1237
+ model: context.model || repo.model,
1238
+ id: context.id || result?._id,
1239
+ userId: context.user?._id || context.user?.id,
1240
+ organizationId: context.organizationId
1241
+ });
1242
+ });
1243
+ repo.on("after:delete", ({ context }) => {
1244
+ logger?.info?.("Document deleted", {
1245
+ model: context.model || repo.model,
1246
+ id: context.id,
1247
+ userId: context.user?._id || context.user?.id,
1248
+ organizationId: context.organizationId
1249
+ });
1250
+ });
1251
+ repo.on("error:create", ({ context, error }) => {
1252
+ logger?.error?.("Create failed", {
1253
+ model: context.model || repo.model,
1254
+ error: error.message,
1255
+ userId: context.user?._id || context.user?.id
1256
+ });
1257
+ });
1258
+ repo.on("error:update", ({ context, error }) => {
1259
+ logger?.error?.("Update failed", {
1260
+ model: context.model || repo.model,
1261
+ id: context.id,
1262
+ error: error.message,
1263
+ userId: context.user?._id || context.user?.id
1264
+ });
1265
+ });
1266
+ repo.on("error:delete", ({ context, error }) => {
1267
+ logger?.error?.("Delete failed", {
1268
+ model: context.model || repo.model,
1269
+ id: context.id,
1270
+ error: error.message,
1271
+ userId: context.user?._id || context.user?.id
1272
+ });
1273
+ });
1274
+ }
1275
+ };
1276
+ }
1277
+
1278
+ // src/plugins/soft-delete.plugin.ts
1279
+ function softDeletePlugin(options = {}) {
1280
+ const deletedField = options.deletedField || "deletedAt";
1281
+ const deletedByField = options.deletedByField || "deletedBy";
1282
+ return {
1283
+ name: "softDelete",
1284
+ apply(repo) {
1285
+ repo.on("before:delete", async (context) => {
1286
+ if (options.soft !== false) {
1287
+ const updateData = {
1288
+ [deletedField]: /* @__PURE__ */ new Date()
1289
+ };
1290
+ if (context.user) {
1291
+ updateData[deletedByField] = context.user._id || context.user.id;
1292
+ }
1293
+ await repo.Model.findByIdAndUpdate(context.id, updateData, { session: context.session });
1294
+ context.softDeleted = true;
1295
+ }
1296
+ });
1297
+ repo.on("before:getAll", (context) => {
1298
+ if (!context.includeDeleted && options.soft !== false) {
1299
+ const queryParams = context.queryParams || {};
1300
+ queryParams.filters = {
1301
+ ...queryParams.filters || {},
1302
+ [deletedField]: { $exists: false }
1303
+ };
1304
+ context.queryParams = queryParams;
1305
+ }
1306
+ });
1307
+ repo.on("before:getById", (context) => {
1308
+ if (!context.includeDeleted && options.soft !== false) {
1309
+ context.query = {
1310
+ ...context.query || {},
1311
+ [deletedField]: { $exists: false }
1312
+ };
1313
+ }
1314
+ });
1315
+ }
1316
+ };
1317
+ }
1318
+
1319
+ // src/plugins/method-registry.plugin.ts
1320
+ function methodRegistryPlugin() {
1321
+ return {
1322
+ name: "method-registry",
1323
+ apply(repo) {
1324
+ const registeredMethods = [];
1325
+ repo.registerMethod = function(name, fn) {
1326
+ if (repo[name]) {
1327
+ throw new Error(
1328
+ `Cannot register method '${name}': Method already exists on repository. Choose a different name or use a plugin that doesn't conflict.`
1329
+ );
1330
+ }
1331
+ if (!name || typeof name !== "string") {
1332
+ throw new Error("Method name must be a non-empty string");
1333
+ }
1334
+ if (typeof fn !== "function") {
1335
+ throw new Error(`Method '${name}' must be a function`);
1336
+ }
1337
+ repo[name] = fn.bind(repo);
1338
+ registeredMethods.push(name);
1339
+ repo.emit("method:registered", { name, fn });
1340
+ };
1341
+ repo.hasMethod = function(name) {
1342
+ return typeof repo[name] === "function";
1343
+ };
1344
+ repo.getRegisteredMethods = function() {
1345
+ return [...registeredMethods];
1346
+ };
1347
+ }
1348
+ };
1349
+ }
1350
+
1351
+ // src/plugins/validation-chain.plugin.ts
1352
+ function validationChainPlugin(validators = [], options = {}) {
1353
+ const { stopOnFirstError = true } = options;
1354
+ validators.forEach((v, idx) => {
1355
+ if (!v.name || typeof v.name !== "string") {
1356
+ throw new Error(`Validator at index ${idx} missing 'name' (string)`);
1357
+ }
1358
+ if (typeof v.validate !== "function") {
1359
+ throw new Error(`Validator '${v.name}' missing 'validate' function`);
1360
+ }
1361
+ });
1362
+ const validatorsByOperation = {
1363
+ create: [],
1364
+ update: [],
1365
+ delete: [],
1366
+ createMany: []
1367
+ };
1368
+ const allOperationsValidators = [];
1369
+ validators.forEach((v) => {
1370
+ if (!v.operations || v.operations.length === 0) {
1371
+ allOperationsValidators.push(v);
1372
+ } else {
1373
+ v.operations.forEach((op) => {
1374
+ if (validatorsByOperation[op]) {
1375
+ validatorsByOperation[op].push(v);
1376
+ }
1377
+ });
1378
+ }
1379
+ });
1380
+ return {
1381
+ name: "validation-chain",
1382
+ apply(repo) {
1383
+ const getValidatorsForOperation = (operation) => {
1384
+ const specific = validatorsByOperation[operation] || [];
1385
+ return [...allOperationsValidators, ...specific];
1386
+ };
1387
+ const runValidators = async (operation, context) => {
1388
+ const operationValidators = getValidatorsForOperation(operation);
1389
+ const errors = [];
1390
+ for (const validator of operationValidators) {
1391
+ try {
1392
+ await validator.validate(context, repo);
1393
+ } catch (error) {
1394
+ if (stopOnFirstError) {
1395
+ throw error;
1396
+ }
1397
+ errors.push({
1398
+ validator: validator.name,
1399
+ error: error.message || String(error)
1400
+ });
1401
+ }
1402
+ }
1403
+ if (errors.length > 0) {
1404
+ const err = createError(
1405
+ 400,
1406
+ `Validation failed: ${errors.map((e) => `[${e.validator}] ${e.error}`).join("; ")}`
1407
+ );
1408
+ err.validationErrors = errors;
1409
+ throw err;
1410
+ }
1411
+ };
1412
+ repo.on("before:create", async (context) => runValidators("create", context));
1413
+ repo.on("before:createMany", async (context) => runValidators("createMany", context));
1414
+ repo.on("before:update", async (context) => runValidators("update", context));
1415
+ repo.on("before:delete", async (context) => runValidators("delete", context));
1416
+ }
1417
+ };
1418
+ }
1419
+ function blockIf(name, operations, condition, errorMessage) {
1420
+ return {
1421
+ name,
1422
+ operations,
1423
+ validate: (context) => {
1424
+ if (condition(context)) {
1425
+ throw createError(403, errorMessage);
1426
+ }
1427
+ }
1428
+ };
1429
+ }
1430
+ function requireField(field, operations = ["create"]) {
1431
+ return {
1432
+ name: `require-${field}`,
1433
+ operations,
1434
+ validate: (context) => {
1435
+ if (!context.data || context.data[field] === void 0 || context.data[field] === null) {
1436
+ throw createError(400, `Field '${field}' is required`);
1437
+ }
1438
+ }
1439
+ };
1440
+ }
1441
+ function autoInject(field, getter, operations = ["create"]) {
1442
+ return {
1443
+ name: `auto-inject-${field}`,
1444
+ operations,
1445
+ validate: (context) => {
1446
+ if (context.data && !(field in context.data)) {
1447
+ const value = getter(context);
1448
+ if (value !== null && value !== void 0) {
1449
+ context.data[field] = value;
1450
+ }
1451
+ }
1452
+ }
1453
+ };
1454
+ }
1455
+ function immutableField(field) {
1456
+ return {
1457
+ name: `immutable-${field}`,
1458
+ operations: ["update"],
1459
+ validate: (context) => {
1460
+ if (context.data && field in context.data) {
1461
+ throw createError(400, `Field '${field}' cannot be modified`);
1462
+ }
1463
+ }
1464
+ };
1465
+ }
1466
+ function uniqueField(field, errorMessage) {
1467
+ return {
1468
+ name: `unique-${field}`,
1469
+ operations: ["create", "update"],
1470
+ validate: async (context, repo) => {
1471
+ if (!context.data || !context.data[field] || !repo) return;
1472
+ const query = { [field]: context.data[field] };
1473
+ const getByQuery2 = repo.getByQuery;
1474
+ if (typeof getByQuery2 !== "function") return;
1475
+ const existing = await getByQuery2.call(repo, query, {
1476
+ select: "_id",
1477
+ lean: true,
1478
+ throwOnNotFound: false
1479
+ });
1480
+ if (existing && String(existing._id) !== String(context.id)) {
1481
+ throw createError(409, errorMessage || `${field} already exists`);
1482
+ }
1483
+ }
1484
+ };
1485
+ }
1486
+
1487
+ // src/plugins/mongo-operations.plugin.ts
1488
+ function mongoOperationsPlugin() {
1489
+ return {
1490
+ name: "mongo-operations",
1491
+ apply(repo) {
1492
+ if (!repo.registerMethod) {
1493
+ throw new Error(
1494
+ "mongoOperationsPlugin requires methodRegistryPlugin. Add methodRegistryPlugin() before mongoOperationsPlugin() in plugins array."
1495
+ );
1496
+ }
1497
+ repo.registerMethod("upsert", async function(query, data, options = {}) {
1498
+ return upsert(this.Model, query, data, options);
1499
+ });
1500
+ const validateAndUpdateNumeric = async function(id, field, value, operator, operationName, options) {
1501
+ if (typeof value !== "number") {
1502
+ throw createError(400, `${operationName} value must be a number`);
1503
+ }
1504
+ return this.update(id, { [operator]: { [field]: value } }, options);
1505
+ };
1506
+ repo.registerMethod("increment", async function(id, field, value = 1, options = {}) {
1507
+ return validateAndUpdateNumeric.call(this, id, field, value, "$inc", "Increment", options);
1508
+ });
1509
+ repo.registerMethod("decrement", async function(id, field, value = 1, options = {}) {
1510
+ return validateAndUpdateNumeric.call(this, id, field, -value, "$inc", "Decrement", options);
1511
+ });
1512
+ const applyOperator = function(id, field, value, operator, options) {
1513
+ return this.update(id, { [operator]: { [field]: value } }, options);
1514
+ };
1515
+ repo.registerMethod("pushToArray", async function(id, field, value, options = {}) {
1516
+ return applyOperator.call(this, id, field, value, "$push", options);
1517
+ });
1518
+ repo.registerMethod("pullFromArray", async function(id, field, value, options = {}) {
1519
+ return applyOperator.call(this, id, field, value, "$pull", options);
1520
+ });
1521
+ repo.registerMethod("addToSet", async function(id, field, value, options = {}) {
1522
+ return applyOperator.call(this, id, field, value, "$addToSet", options);
1523
+ });
1524
+ repo.registerMethod("setField", async function(id, field, value, options = {}) {
1525
+ return applyOperator.call(this, id, field, value, "$set", options);
1526
+ });
1527
+ repo.registerMethod("unsetField", async function(id, fields, options = {}) {
1528
+ const fieldArray = Array.isArray(fields) ? fields : [fields];
1529
+ const unsetObj = fieldArray.reduce((acc, field) => {
1530
+ acc[field] = "";
1531
+ return acc;
1532
+ }, {});
1533
+ return this.update(id, { $unset: unsetObj }, options);
1534
+ });
1535
+ repo.registerMethod("renameField", async function(id, oldName, newName, options = {}) {
1536
+ return this.update(id, { $rename: { [oldName]: newName } }, options);
1537
+ });
1538
+ repo.registerMethod("multiplyField", async function(id, field, multiplier, options = {}) {
1539
+ return validateAndUpdateNumeric.call(this, id, field, multiplier, "$mul", "Multiplier", options);
1540
+ });
1541
+ repo.registerMethod("setMin", async function(id, field, value, options = {}) {
1542
+ return applyOperator.call(this, id, field, value, "$min", options);
1543
+ });
1544
+ repo.registerMethod("setMax", async function(id, field, value, options = {}) {
1545
+ return applyOperator.call(this, id, field, value, "$max", options);
1546
+ });
1547
+ }
1548
+ };
1549
+ }
1550
+
1551
+ // src/plugins/batch-operations.plugin.ts
1552
+ function batchOperationsPlugin() {
1553
+ return {
1554
+ name: "batch-operations",
1555
+ apply(repo) {
1556
+ if (!repo.registerMethod) {
1557
+ throw new Error("batchOperationsPlugin requires methodRegistryPlugin");
1558
+ }
1559
+ repo.registerMethod("updateMany", async function(query, data, options = {}) {
1560
+ const _buildContext = this._buildContext;
1561
+ const context = await _buildContext.call(this, "updateMany", { query, data, options });
1562
+ try {
1563
+ this.emit("before:updateMany", context);
1564
+ const result = await this.Model.updateMany(query, data, {
1565
+ runValidators: true,
1566
+ session: options.session
1567
+ }).exec();
1568
+ this.emit("after:updateMany", { context, result });
1569
+ return result;
1570
+ } catch (error) {
1571
+ this.emit("error:updateMany", { context, error });
1572
+ const _handleError = this._handleError;
1573
+ throw _handleError.call(this, error);
1574
+ }
1575
+ });
1576
+ repo.registerMethod("deleteMany", async function(query, options = {}) {
1577
+ const _buildContext = this._buildContext;
1578
+ const context = await _buildContext.call(this, "deleteMany", { query, options });
1579
+ try {
1580
+ this.emit("before:deleteMany", context);
1581
+ const result = await this.Model.deleteMany(query, {
1582
+ session: options.session
1583
+ }).exec();
1584
+ this.emit("after:deleteMany", { context, result });
1585
+ return result;
1586
+ } catch (error) {
1587
+ this.emit("error:deleteMany", { context, error });
1588
+ const _handleError = this._handleError;
1589
+ throw _handleError.call(this, error);
1590
+ }
1591
+ });
1592
+ }
1593
+ };
1594
+ }
1595
+
1596
+ // src/plugins/aggregate-helpers.plugin.ts
1597
+ function aggregateHelpersPlugin() {
1598
+ return {
1599
+ name: "aggregate-helpers",
1600
+ apply(repo) {
1601
+ if (!repo.registerMethod) {
1602
+ throw new Error("aggregateHelpersPlugin requires methodRegistryPlugin");
1603
+ }
1604
+ repo.registerMethod("groupBy", async function(field, options = {}) {
1605
+ const pipeline = [
1606
+ { $group: { _id: `$${field}`, count: { $sum: 1 } } },
1607
+ { $sort: { count: -1 } }
1608
+ ];
1609
+ if (options.limit) {
1610
+ pipeline.push({ $limit: options.limit });
1611
+ }
1612
+ const aggregate2 = this.aggregate;
1613
+ return aggregate2.call(this, pipeline, options);
1614
+ });
1615
+ const aggregateOperation = async function(field, operator, resultKey, query = {}, options = {}) {
1616
+ const pipeline = [
1617
+ { $match: query },
1618
+ { $group: { _id: null, [resultKey]: { [operator]: `$${field}` } } }
1619
+ ];
1620
+ const aggregate2 = this.aggregate;
1621
+ const result = await aggregate2.call(this, pipeline, options);
1622
+ return result[0]?.[resultKey] || 0;
1623
+ };
1624
+ repo.registerMethod("sum", async function(field, query = {}, options = {}) {
1625
+ return aggregateOperation.call(this, field, "$sum", "total", query, options);
1626
+ });
1627
+ repo.registerMethod("average", async function(field, query = {}, options = {}) {
1628
+ return aggregateOperation.call(this, field, "$avg", "avg", query, options);
1629
+ });
1630
+ repo.registerMethod("min", async function(field, query = {}, options = {}) {
1631
+ return aggregateOperation.call(this, field, "$min", "min", query, options);
1632
+ });
1633
+ repo.registerMethod("max", async function(field, query = {}, options = {}) {
1634
+ return aggregateOperation.call(this, field, "$max", "max", query, options);
1635
+ });
1636
+ }
1637
+ };
1638
+ }
1639
+
1640
+ // src/plugins/subdocument.plugin.ts
1641
+ function subdocumentPlugin() {
1642
+ return {
1643
+ name: "subdocument",
1644
+ apply(repo) {
1645
+ if (!repo.registerMethod) {
1646
+ throw new Error("subdocumentPlugin requires methodRegistryPlugin");
1647
+ }
1648
+ repo.registerMethod("addSubdocument", async function(parentId, arrayPath, subData, options = {}) {
1649
+ const update2 = this.update;
1650
+ return update2.call(this, parentId, { $push: { [arrayPath]: subData } }, options);
1651
+ });
1652
+ repo.registerMethod("getSubdocument", async function(parentId, arrayPath, subId, options = {}) {
1653
+ const _executeQuery = this._executeQuery;
1654
+ return _executeQuery.call(this, async (Model) => {
1655
+ const parent = await Model.findById(parentId).session(options.session).exec();
1656
+ if (!parent) throw createError(404, "Parent not found");
1657
+ const parentObj = parent;
1658
+ const arrayField = parentObj[arrayPath];
1659
+ if (!arrayField || typeof arrayField.id !== "function") {
1660
+ throw createError(404, "Array field not found");
1661
+ }
1662
+ const sub = arrayField.id(subId);
1663
+ if (!sub) throw createError(404, "Subdocument not found");
1664
+ return options.lean && typeof sub.toObject === "function" ? sub.toObject() : sub;
1665
+ });
1666
+ });
1667
+ repo.registerMethod("updateSubdocument", async function(parentId, arrayPath, subId, updateData, options = {}) {
1668
+ const _executeQuery = this._executeQuery;
1669
+ return _executeQuery.call(this, async (Model) => {
1670
+ const query = { _id: parentId, [`${arrayPath}._id`]: subId };
1671
+ const update2 = { $set: { [`${arrayPath}.$`]: { ...updateData, _id: subId } } };
1672
+ const result = await Model.findOneAndUpdate(query, update2, {
1673
+ new: true,
1674
+ runValidators: true,
1675
+ session: options.session
1676
+ }).exec();
1677
+ if (!result) throw createError(404, "Parent or subdocument not found");
1678
+ return result;
1679
+ });
1680
+ });
1681
+ repo.registerMethod("deleteSubdocument", async function(parentId, arrayPath, subId, options = {}) {
1682
+ const update2 = this.update;
1683
+ return update2.call(this, parentId, { $pull: { [arrayPath]: { _id: subId } } }, options);
1684
+ });
1685
+ }
1686
+ };
1687
+ }
1688
+
1689
+ // src/utils/cache-keys.ts
1690
+ function hashString(str) {
1691
+ let hash = 5381;
1692
+ for (let i = 0; i < str.length; i++) {
1693
+ hash = (hash << 5) + hash ^ str.charCodeAt(i);
1694
+ }
1695
+ return (hash >>> 0).toString(16);
1696
+ }
1697
+ function stableStringify(obj) {
1698
+ if (obj === null || obj === void 0) return "";
1699
+ if (typeof obj !== "object") return String(obj);
1700
+ if (Array.isArray(obj)) {
1701
+ return "[" + obj.map(stableStringify).join(",") + "]";
1702
+ }
1703
+ const sorted = Object.keys(obj).sort().map((key) => `${key}:${stableStringify(obj[key])}`);
1704
+ return "{" + sorted.join(",") + "}";
1705
+ }
1706
+ function byIdKey(prefix, model, id) {
1707
+ return `${prefix}:id:${model}:${id}`;
1708
+ }
1709
+ function byQueryKey(prefix, model, query, options) {
1710
+ const hashInput = stableStringify({ q: query, s: options?.select, p: options?.populate });
1711
+ return `${prefix}:one:${model}:${hashString(hashInput)}`;
1712
+ }
1713
+ function listQueryKey(prefix, model, version, params) {
1714
+ const hashInput = stableStringify({
1715
+ f: params.filters,
1716
+ s: params.sort,
1717
+ pg: params.page,
1718
+ lm: params.limit,
1719
+ af: params.after,
1720
+ sl: params.select,
1721
+ pp: params.populate
1722
+ });
1723
+ return `${prefix}:list:${model}:${version}:${hashString(hashInput)}`;
1724
+ }
1725
+ function versionKey(prefix, model) {
1726
+ return `${prefix}:ver:${model}`;
1727
+ }
1728
+ function modelPattern(prefix, model) {
1729
+ return `${prefix}:*:${model}:*`;
1730
+ }
1731
+
1732
+ // src/plugins/cache.plugin.ts
1733
+ function cachePlugin(options) {
1734
+ const config = {
1735
+ adapter: options.adapter,
1736
+ ttl: options.ttl ?? 60,
1737
+ byIdTtl: options.byIdTtl ?? options.ttl ?? 60,
1738
+ queryTtl: options.queryTtl ?? options.ttl ?? 60,
1739
+ prefix: options.prefix ?? "mk",
1740
+ debug: options.debug ?? false,
1741
+ skipIfLargeLimit: options.skipIf?.largeLimit ?? 100
1742
+ };
1743
+ const stats = {
1744
+ hits: 0,
1745
+ misses: 0,
1746
+ sets: 0,
1747
+ invalidations: 0
1748
+ };
1749
+ let collectionVersion = 0;
1750
+ const log = (msg, data) => {
1751
+ if (config.debug) {
1752
+ console.log(`[mongokit:cache] ${msg}`, data ?? "");
1753
+ }
1754
+ };
1755
+ return {
1756
+ name: "cache",
1757
+ apply(repo) {
1758
+ const model = repo.model;
1759
+ (async () => {
1760
+ try {
1761
+ const cached = await config.adapter.get(versionKey(config.prefix, model));
1762
+ if (cached !== null) {
1763
+ collectionVersion = cached;
1764
+ log(`Initialized version for ${model}:`, collectionVersion);
1765
+ }
1766
+ } catch (e) {
1767
+ log(`Failed to initialize version for ${model}:`, e);
1768
+ }
1769
+ })();
1770
+ async function bumpVersion() {
1771
+ collectionVersion++;
1772
+ try {
1773
+ await config.adapter.set(versionKey(config.prefix, model), collectionVersion, config.ttl * 10);
1774
+ stats.invalidations++;
1775
+ log(`Bumped version for ${model} to:`, collectionVersion);
1776
+ } catch (e) {
1777
+ log(`Failed to bump version for ${model}:`, e);
1778
+ }
1779
+ }
1780
+ async function invalidateById(id) {
1781
+ const key = byIdKey(config.prefix, model, id);
1782
+ try {
1783
+ await config.adapter.del(key);
1784
+ stats.invalidations++;
1785
+ log(`Invalidated byId cache:`, key);
1786
+ } catch (e) {
1787
+ log(`Failed to invalidate byId cache:`, e);
1788
+ }
1789
+ }
1790
+ repo.on("before:getById", async (context) => {
1791
+ if (context.skipCache) {
1792
+ log(`Skipping cache for getById: ${context.id}`);
1793
+ return;
1794
+ }
1795
+ const id = String(context.id);
1796
+ const key = byIdKey(config.prefix, model, id);
1797
+ try {
1798
+ const cached = await config.adapter.get(key);
1799
+ if (cached !== null) {
1800
+ stats.hits++;
1801
+ log(`Cache HIT for getById:`, key);
1802
+ context._cacheHit = true;
1803
+ context._cachedResult = cached;
1804
+ } else {
1805
+ stats.misses++;
1806
+ log(`Cache MISS for getById:`, key);
1807
+ }
1808
+ } catch (e) {
1809
+ log(`Cache error for getById:`, e);
1810
+ stats.misses++;
1811
+ }
1812
+ });
1813
+ repo.on("before:getByQuery", async (context) => {
1814
+ if (context.skipCache) {
1815
+ log(`Skipping cache for getByQuery`);
1816
+ return;
1817
+ }
1818
+ const query = context.query || {};
1819
+ const key = byQueryKey(config.prefix, model, query, {
1820
+ select: context.select,
1821
+ populate: context.populate
1822
+ });
1823
+ try {
1824
+ const cached = await config.adapter.get(key);
1825
+ if (cached !== null) {
1826
+ stats.hits++;
1827
+ log(`Cache HIT for getByQuery:`, key);
1828
+ context._cacheHit = true;
1829
+ context._cachedResult = cached;
1830
+ } else {
1831
+ stats.misses++;
1832
+ log(`Cache MISS for getByQuery:`, key);
1833
+ }
1834
+ } catch (e) {
1835
+ log(`Cache error for getByQuery:`, e);
1836
+ stats.misses++;
1837
+ }
1838
+ });
1839
+ repo.on("before:getAll", async (context) => {
1840
+ if (context.skipCache) {
1841
+ log(`Skipping cache for getAll`);
1842
+ return;
1843
+ }
1844
+ const limit = context.limit;
1845
+ if (limit && limit > config.skipIfLargeLimit) {
1846
+ log(`Skipping cache for large query (limit: ${limit})`);
1847
+ return;
1848
+ }
1849
+ const params = {
1850
+ filters: context.filters,
1851
+ sort: context.sort,
1852
+ page: context.page,
1853
+ limit,
1854
+ after: context.after,
1855
+ select: context.select,
1856
+ populate: context.populate
1857
+ };
1858
+ const key = listQueryKey(config.prefix, model, collectionVersion, params);
1859
+ try {
1860
+ const cached = await config.adapter.get(key);
1861
+ if (cached !== null) {
1862
+ stats.hits++;
1863
+ log(`Cache HIT for getAll:`, key);
1864
+ context._cacheHit = true;
1865
+ context._cachedResult = cached;
1866
+ } else {
1867
+ stats.misses++;
1868
+ log(`Cache MISS for getAll:`, key);
1869
+ }
1870
+ } catch (e) {
1871
+ log(`Cache error for getAll:`, e);
1872
+ stats.misses++;
1873
+ }
1874
+ });
1875
+ repo.on("after:getById", async (payload) => {
1876
+ const { context, result } = payload;
1877
+ if (context._cacheHit) return;
1878
+ if (context.skipCache) return;
1879
+ if (result === null) return;
1880
+ const id = String(context.id);
1881
+ const key = byIdKey(config.prefix, model, id);
1882
+ const ttl = context.cacheTtl ?? config.byIdTtl;
1883
+ try {
1884
+ await config.adapter.set(key, result, ttl);
1885
+ stats.sets++;
1886
+ log(`Cached getById result:`, key);
1887
+ } catch (e) {
1888
+ log(`Failed to cache getById:`, e);
1889
+ }
1890
+ });
1891
+ repo.on("after:getByQuery", async (payload) => {
1892
+ const { context, result } = payload;
1893
+ if (context._cacheHit) return;
1894
+ if (context.skipCache) return;
1895
+ if (result === null) return;
1896
+ const query = context.query || {};
1897
+ const key = byQueryKey(config.prefix, model, query, {
1898
+ select: context.select,
1899
+ populate: context.populate
1900
+ });
1901
+ const ttl = context.cacheTtl ?? config.queryTtl;
1902
+ try {
1903
+ await config.adapter.set(key, result, ttl);
1904
+ stats.sets++;
1905
+ log(`Cached getByQuery result:`, key);
1906
+ } catch (e) {
1907
+ log(`Failed to cache getByQuery:`, e);
1908
+ }
1909
+ });
1910
+ repo.on("after:getAll", async (payload) => {
1911
+ const { context, result } = payload;
1912
+ if (context._cacheHit) return;
1913
+ if (context.skipCache) return;
1914
+ const limit = context.limit;
1915
+ if (limit && limit > config.skipIfLargeLimit) return;
1916
+ const params = {
1917
+ filters: context.filters,
1918
+ sort: context.sort,
1919
+ page: context.page,
1920
+ limit,
1921
+ after: context.after,
1922
+ select: context.select,
1923
+ populate: context.populate
1924
+ };
1925
+ const key = listQueryKey(config.prefix, model, collectionVersion, params);
1926
+ const ttl = context.cacheTtl ?? config.queryTtl;
1927
+ try {
1928
+ await config.adapter.set(key, result, ttl);
1929
+ stats.sets++;
1930
+ log(`Cached getAll result:`, key);
1931
+ } catch (e) {
1932
+ log(`Failed to cache getAll:`, e);
1933
+ }
1934
+ });
1935
+ repo.on("after:create", async () => {
1936
+ await bumpVersion();
1937
+ });
1938
+ repo.on("after:createMany", async () => {
1939
+ await bumpVersion();
1940
+ });
1941
+ repo.on("after:update", async (payload) => {
1942
+ const { context } = payload;
1943
+ const id = String(context.id);
1944
+ await Promise.all([
1945
+ invalidateById(id),
1946
+ bumpVersion()
1947
+ ]);
1948
+ });
1949
+ repo.on("after:updateMany", async () => {
1950
+ await bumpVersion();
1951
+ });
1952
+ repo.on("after:delete", async (payload) => {
1953
+ const { context } = payload;
1954
+ const id = String(context.id);
1955
+ await Promise.all([
1956
+ invalidateById(id),
1957
+ bumpVersion()
1958
+ ]);
1959
+ });
1960
+ repo.on("after:deleteMany", async () => {
1961
+ await bumpVersion();
1962
+ });
1963
+ repo.invalidateCache = async (id) => {
1964
+ await invalidateById(id);
1965
+ log(`Manual invalidation for ID:`, id);
1966
+ };
1967
+ repo.invalidateListCache = async () => {
1968
+ await bumpVersion();
1969
+ log(`Manual list cache invalidation for ${model}`);
1970
+ };
1971
+ repo.invalidateAllCache = async () => {
1972
+ if (config.adapter.clear) {
1973
+ try {
1974
+ await config.adapter.clear(modelPattern(config.prefix, model));
1975
+ stats.invalidations++;
1976
+ log(`Full cache invalidation for ${model}`);
1977
+ } catch (e) {
1978
+ log(`Failed full cache invalidation for ${model}:`, e);
1979
+ }
1980
+ } else {
1981
+ await bumpVersion();
1982
+ log(`Partial cache invalidation for ${model} (adapter.clear not available)`);
1983
+ }
1984
+ };
1985
+ repo.getCacheStats = () => ({ ...stats });
1986
+ repo.resetCacheStats = () => {
1987
+ stats.hits = 0;
1988
+ stats.misses = 0;
1989
+ stats.sets = 0;
1990
+ stats.invalidations = 0;
1991
+ };
1992
+ }
1993
+ };
1994
+ }
1995
+
1996
+ // src/utils/memory-cache.ts
1997
+ function createMemoryCache(maxEntries = 1e3) {
1998
+ const cache = /* @__PURE__ */ new Map();
1999
+ function cleanup() {
2000
+ const now = Date.now();
2001
+ for (const [key, entry] of cache) {
2002
+ if (entry.expiresAt < now) {
2003
+ cache.delete(key);
2004
+ }
2005
+ }
2006
+ }
2007
+ function evictOldest() {
2008
+ if (cache.size >= maxEntries) {
2009
+ const firstKey = cache.keys().next().value;
2010
+ if (firstKey) cache.delete(firstKey);
2011
+ }
2012
+ }
2013
+ return {
2014
+ async get(key) {
2015
+ cleanup();
2016
+ const entry = cache.get(key);
2017
+ if (!entry) return null;
2018
+ if (entry.expiresAt < Date.now()) {
2019
+ cache.delete(key);
2020
+ return null;
2021
+ }
2022
+ return entry.value;
2023
+ },
2024
+ async set(key, value, ttl) {
2025
+ cleanup();
2026
+ evictOldest();
2027
+ cache.set(key, {
2028
+ value,
2029
+ expiresAt: Date.now() + ttl * 1e3
2030
+ });
2031
+ },
2032
+ async del(key) {
2033
+ cache.delete(key);
2034
+ },
2035
+ async clear(pattern) {
2036
+ if (!pattern) {
2037
+ cache.clear();
2038
+ return;
2039
+ }
2040
+ const regex = new RegExp(
2041
+ "^" + pattern.replace(/\*/g, ".*").replace(/\?/g, ".") + "$"
2042
+ );
2043
+ for (const key of cache.keys()) {
2044
+ if (regex.test(key)) {
2045
+ cache.delete(key);
2046
+ }
2047
+ }
2048
+ }
2049
+ };
2050
+ }
2051
+
2052
+ // src/actions/index.ts
2053
+ var actions_exports = {};
2054
+ __export(actions_exports, {
2055
+ aggregate: () => aggregate_exports,
2056
+ create: () => create_exports,
2057
+ deleteActions: () => delete_exports,
2058
+ read: () => read_exports,
2059
+ update: () => update_exports
2060
+ });
2061
+
2062
+ // src/index.ts
2063
+ function createRepository(Model, plugins = []) {
2064
+ return new Repository(Model, plugins);
2065
+ }
2066
+ var index_default = Repository;
2067
+ /**
2068
+ * MongoKit - Event-driven repository pattern for MongoDB
2069
+ *
2070
+ * Production-grade MongoDB repositories with zero dependencies -
2071
+ * smart pagination, events, and plugins.
2072
+ *
2073
+ * @module @classytic/mongokit
2074
+ * @author Sadman Chowdhury (Github: @siam923)
2075
+ * @license MIT
2076
+ *
2077
+ * @example
2078
+ * ```typescript
2079
+ * import { Repository, createRepository } from '@classytic/mongokit';
2080
+ * import { timestampPlugin, softDeletePlugin } from '@classytic/mongokit';
2081
+ *
2082
+ * // Create repository with plugins
2083
+ * const userRepo = createRepository(UserModel, [
2084
+ * timestampPlugin(),
2085
+ * softDeletePlugin(),
2086
+ * ]);
2087
+ *
2088
+ * // Create
2089
+ * const user = await userRepo.create({ name: 'John', email: 'john@example.com' });
2090
+ *
2091
+ * // Read with pagination (auto-detects offset vs keyset)
2092
+ * const users = await userRepo.getAll({ page: 1, limit: 20 });
2093
+ *
2094
+ * // Keyset pagination for infinite scroll
2095
+ * const stream = await userRepo.getAll({ sort: { createdAt: -1 }, limit: 50 });
2096
+ * const nextStream = await userRepo.getAll({ after: stream.next, sort: { createdAt: -1 } });
2097
+ *
2098
+ * // Update
2099
+ * await userRepo.update(user._id, { name: 'John Doe' });
2100
+ *
2101
+ * // Delete
2102
+ * await userRepo.delete(user._id);
2103
+ * ```
2104
+ */
2105
+
2106
+ export { PaginationEngine, Repository, actions_exports as actions, aggregateHelpersPlugin, auditLogPlugin, autoInject, batchOperationsPlugin, blockIf, cachePlugin, createError, createFieldPreset, createMemoryCache, createRepository, index_default as default, fieldFilterPlugin, filterResponseData, getFieldsForUser, getMongooseProjection, immutableField, methodRegistryPlugin, mongoOperationsPlugin, requireField, softDeletePlugin, subdocumentPlugin, timestampPlugin, uniqueField, validationChainPlugin };
2107
+ //# sourceMappingURL=index.js.map
2108
+ //# sourceMappingURL=index.js.map