@classytic/mongokit 3.2.5 → 3.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -1,1151 +1,12 @@
1
1
  import { i as createError, r as warn, t as configureLogger } from "./logger-D8ily-PP.mjs";
2
- import { n as createMany, t as create } from "./create-BuO6xt0v.mjs";
3
- import { a as deleteById, d as getById, f as getByQuery, i as LookupBuilder, l as count, p as getOrCreate, r as distinct, s as update, t as aggregate, u as exists } from "./aggregate-BAi4Do-X.mjs";
2
+ import { r as LookupBuilder } from "./aggregate-BClp040M.mjs";
4
3
  import { PaginationEngine } from "./pagination/PaginationEngine.mjs";
5
- import { c as filterResponseData, l as getFieldsForUser, s as createFieldPreset, u as getMongooseProjection } from "./cache-keys-C8Z9B5sw.mjs";
6
- import { C as methodRegistryPlugin, D as fieldFilterPlugin, E as timestampPlugin, S as validationChainPlugin, T as auditLogPlugin, _ as autoInject, a as sequentialId, b as requireField, c as auditTrailPlugin, d as cascadePlugin, f as cachePlugin, g as mongoOperationsPlugin, h as batchOperationsPlugin, i as prefixedId, l as observabilityPlugin, m as aggregateHelpersPlugin, n as dateSequentialId, o as elasticSearchPlugin, p as subdocumentPlugin, r as getNextSequence, s as AuditTrailQuery, t as customIdPlugin, u as multiTenantPlugin, v as blockIf, w as softDeletePlugin, x as uniqueField, y as immutableField } from "./custom-id.plugin-m0VW6yYm.mjs";
7
- import { a as isFieldUpdateAllowed, i as getSystemManagedFields, n as buildCrudSchemasFromMongooseSchema, o as validateUpdateBody, r as getImmutableFields, s as createMemoryCache, t as buildCrudSchemasFromModel } from "./mongooseToJsonSchema-COdDEkIJ.mjs";
4
+ import { A as AggregationBuilder, C as methodRegistryPlugin, D as fieldFilterPlugin, E as timestampPlugin, O as HOOK_PRIORITY, S as validationChainPlugin, T as auditLogPlugin, _ as autoInject, a as sequentialId, b as requireField, c as auditTrailPlugin, d as cascadePlugin, f as cachePlugin, g as mongoOperationsPlugin, h as batchOperationsPlugin, i as prefixedId, k as Repository, l as observabilityPlugin, m as aggregateHelpersPlugin, n as dateSequentialId, o as elasticSearchPlugin, p as subdocumentPlugin, r as getNextSequence, s as AuditTrailQuery, t as customIdPlugin, u as multiTenantPlugin, v as blockIf, w as softDeletePlugin, x as uniqueField, y as immutableField } from "./custom-id.plugin-FInXDsUX.mjs";
5
+ import { c as filterResponseData, l as getFieldsForUser, s as createFieldPreset, u as getMongooseProjection } from "./cache-keys-CzFwVnLy.mjs";
6
+ import { a as isFieldUpdateAllowed, i as getSystemManagedFields, n as buildCrudSchemasFromMongooseSchema, o as validateUpdateBody, r as getImmutableFields, s as createMemoryCache, t as buildCrudSchemasFromModel } from "./mongooseToJsonSchema-D_i2Am_O.mjs";
8
7
  import { t as actions_exports } from "./actions/index.mjs";
9
8
  import mongoose from "mongoose";
10
9
 
11
- //#region src/query/AggregationBuilder.ts
12
- /**
13
- * Normalize SortSpec to MongoDB's strict format (1 | -1)
14
- * Converts 'asc' -> 1, 'desc' -> -1
15
- */
16
- function normalizeSortSpec(sortSpec) {
17
- const normalized = {};
18
- for (const [field, order] of Object.entries(sortSpec)) if (order === "asc") normalized[field] = 1;
19
- else if (order === "desc") normalized[field] = -1;
20
- else normalized[field] = order;
21
- return normalized;
22
- }
23
- /**
24
- * Fluent builder for MongoDB aggregation pipelines
25
- * Optimized for complex queries at scale
26
- */
27
- var AggregationBuilder = class AggregationBuilder {
28
- pipeline = [];
29
- _diskUse = false;
30
- /**
31
- * Get the current pipeline
32
- */
33
- get() {
34
- return [...this.pipeline];
35
- }
36
- /**
37
- * Build and return the final pipeline
38
- */
39
- build() {
40
- return this.get();
41
- }
42
- /**
43
- * Build pipeline with execution options (allowDiskUse, etc.)
44
- */
45
- plan() {
46
- return {
47
- pipeline: this.get(),
48
- allowDiskUse: this._diskUse
49
- };
50
- }
51
- /**
52
- * Build and execute the pipeline against a model
53
- *
54
- * @example
55
- * ```typescript
56
- * const results = await new AggregationBuilder()
57
- * .match({ status: 'active' })
58
- * .allowDiskUse()
59
- * .exec(MyModel);
60
- * ```
61
- */
62
- async exec(model, session) {
63
- const agg = model.aggregate(this.build());
64
- if (this._diskUse) agg.allowDiskUse(true);
65
- if (session) agg.session(session);
66
- return agg.exec();
67
- }
68
- /**
69
- * Reset the pipeline
70
- */
71
- reset() {
72
- this.pipeline = [];
73
- this._diskUse = false;
74
- return this;
75
- }
76
- /**
77
- * Add a raw pipeline stage
78
- */
79
- addStage(stage) {
80
- this.pipeline.push(stage);
81
- return this;
82
- }
83
- /**
84
- * Add multiple raw pipeline stages
85
- */
86
- addStages(stages) {
87
- this.pipeline.push(...stages);
88
- return this;
89
- }
90
- /**
91
- * $match - Filter documents
92
- * IMPORTANT: Place $match as early as possible for performance
93
- */
94
- match(query) {
95
- this.pipeline.push({ $match: query });
96
- return this;
97
- }
98
- /**
99
- * $project - Include/exclude fields or compute new fields
100
- */
101
- project(projection) {
102
- this.pipeline.push({ $project: projection });
103
- return this;
104
- }
105
- /**
106
- * $group - Group documents and compute aggregations
107
- *
108
- * @example
109
- * ```typescript
110
- * .group({
111
- * _id: '$department',
112
- * count: { $sum: 1 },
113
- * avgSalary: { $avg: '$salary' }
114
- * })
115
- * ```
116
- */
117
- group(groupSpec) {
118
- this.pipeline.push({ $group: groupSpec });
119
- return this;
120
- }
121
- /**
122
- * $sort - Sort documents
123
- */
124
- sort(sortSpec) {
125
- if (typeof sortSpec === "string") {
126
- const order = sortSpec.startsWith("-") ? -1 : 1;
127
- const field = sortSpec.startsWith("-") ? sortSpec.substring(1) : sortSpec;
128
- this.pipeline.push({ $sort: { [field]: order } });
129
- } else this.pipeline.push({ $sort: normalizeSortSpec(sortSpec) });
130
- return this;
131
- }
132
- /**
133
- * $limit - Limit number of documents
134
- */
135
- limit(count) {
136
- this.pipeline.push({ $limit: count });
137
- return this;
138
- }
139
- /**
140
- * $skip - Skip documents
141
- */
142
- skip(count) {
143
- this.pipeline.push({ $skip: count });
144
- return this;
145
- }
146
- /**
147
- * $unwind - Deconstruct array field
148
- */
149
- unwind(path, preserveNullAndEmptyArrays = false) {
150
- this.pipeline.push({ $unwind: {
151
- path: path.startsWith("$") ? path : `$${path}`,
152
- preserveNullAndEmptyArrays
153
- } });
154
- return this;
155
- }
156
- /**
157
- * $addFields - Add new fields or replace existing fields
158
- */
159
- addFields(fields) {
160
- this.pipeline.push({ $addFields: fields });
161
- return this;
162
- }
163
- /**
164
- * $set - Alias for $addFields
165
- */
166
- set(fields) {
167
- return this.addFields(fields);
168
- }
169
- /**
170
- * $unset - Remove fields
171
- */
172
- unset(fields) {
173
- this.pipeline.push({ $unset: fields });
174
- return this;
175
- }
176
- /**
177
- * $replaceRoot - Replace the root document
178
- */
179
- replaceRoot(newRoot) {
180
- this.pipeline.push({ $replaceRoot: { newRoot: typeof newRoot === "string" ? `$${newRoot}` : newRoot } });
181
- return this;
182
- }
183
- /**
184
- * $lookup - Join with another collection (simple form)
185
- *
186
- * @param from - Collection to join with
187
- * @param localField - Field from source collection
188
- * @param foreignField - Field from target collection
189
- * @param as - Output field name
190
- * @param single - Unwrap array to single object
191
- *
192
- * @example
193
- * ```typescript
194
- * // Join employees with departments by slug
195
- * .lookup('departments', 'deptSlug', 'slug', 'department', true)
196
- * ```
197
- */
198
- lookup(from, localField, foreignField, as, single) {
199
- const stages = new LookupBuilder(from).localField(localField).foreignField(foreignField).as(as || from).single(single || false).build();
200
- this.pipeline.push(...stages);
201
- return this;
202
- }
203
- /**
204
- * $lookup - Join with another collection (advanced form with pipeline)
205
- *
206
- * @example
207
- * ```typescript
208
- * .lookupWithPipeline({
209
- * from: 'products',
210
- * localField: 'productIds',
211
- * foreignField: 'sku',
212
- * as: 'products',
213
- * pipeline: [
214
- * { $match: { status: 'active' } },
215
- * { $project: { name: 1, price: 1 } }
216
- * ]
217
- * })
218
- * ```
219
- */
220
- lookupWithPipeline(options) {
221
- const builder = new LookupBuilder(options.from).localField(options.localField).foreignField(options.foreignField);
222
- if (options.as) builder.as(options.as);
223
- if (options.single) builder.single(options.single);
224
- if (options.pipeline) builder.pipeline(options.pipeline);
225
- if (options.let) builder.let(options.let);
226
- this.pipeline.push(...builder.build());
227
- return this;
228
- }
229
- /**
230
- * Multiple lookups at once
231
- *
232
- * @example
233
- * ```typescript
234
- * .multiLookup([
235
- * { from: 'departments', localField: 'deptSlug', foreignField: 'slug', single: true },
236
- * { from: 'managers', localField: 'managerId', foreignField: '_id', single: true }
237
- * ])
238
- * ```
239
- */
240
- multiLookup(lookups) {
241
- const stages = LookupBuilder.multiple(lookups);
242
- this.pipeline.push(...stages);
243
- return this;
244
- }
245
- /**
246
- * $facet - Process multiple aggregation pipelines in a single stage
247
- * Useful for computing multiple aggregations in parallel
248
- *
249
- * @example
250
- * ```typescript
251
- * .facet({
252
- * totalCount: [{ $count: 'count' }],
253
- * avgPrice: [{ $group: { _id: null, avg: { $avg: '$price' } } }],
254
- * topProducts: [{ $sort: { sales: -1 } }, { $limit: 10 }]
255
- * })
256
- * ```
257
- */
258
- facet(facets) {
259
- this.pipeline.push({ $facet: facets });
260
- return this;
261
- }
262
- /**
263
- * $bucket - Categorize documents into buckets
264
- *
265
- * @example
266
- * ```typescript
267
- * .bucket({
268
- * groupBy: '$price',
269
- * boundaries: [0, 50, 100, 200],
270
- * default: 'Other',
271
- * output: {
272
- * count: { $sum: 1 },
273
- * products: { $push: '$name' }
274
- * }
275
- * })
276
- * ```
277
- */
278
- bucket(options) {
279
- this.pipeline.push({ $bucket: options });
280
- return this;
281
- }
282
- /**
283
- * $bucketAuto - Automatically determine bucket boundaries
284
- */
285
- bucketAuto(options) {
286
- this.pipeline.push({ $bucketAuto: options });
287
- return this;
288
- }
289
- /**
290
- * $setWindowFields - Perform window functions (MongoDB 5.0+)
291
- * Useful for rankings, running totals, moving averages
292
- *
293
- * @example
294
- * ```typescript
295
- * .setWindowFields({
296
- * partitionBy: '$department',
297
- * sortBy: { salary: -1 },
298
- * output: {
299
- * rank: { $rank: {} },
300
- * runningTotal: { $sum: '$salary', window: { documents: ['unbounded', 'current'] } }
301
- * }
302
- * })
303
- * ```
304
- */
305
- setWindowFields(options) {
306
- const normalizedOptions = {
307
- ...options,
308
- sortBy: options.sortBy ? normalizeSortSpec(options.sortBy) : void 0
309
- };
310
- this.pipeline.push({ $setWindowFields: normalizedOptions });
311
- return this;
312
- }
313
- /**
314
- * $unionWith - Combine results from multiple collections (MongoDB 4.4+)
315
- *
316
- * @example
317
- * ```typescript
318
- * .unionWith({
319
- * coll: 'archivedOrders',
320
- * pipeline: [{ $match: { year: 2024 } }]
321
- * })
322
- * ```
323
- */
324
- unionWith(options) {
325
- this.pipeline.push({ $unionWith: options });
326
- return this;
327
- }
328
- /**
329
- * $densify - Fill gaps in data (MongoDB 5.1+)
330
- * Useful for time series data with missing points
331
- */
332
- densify(options) {
333
- this.pipeline.push({ $densify: options });
334
- return this;
335
- }
336
- /**
337
- * $fill - Fill null or missing field values (MongoDB 5.3+)
338
- */
339
- fill(options) {
340
- const normalizedOptions = {
341
- ...options,
342
- sortBy: options.sortBy ? normalizeSortSpec(options.sortBy) : void 0
343
- };
344
- this.pipeline.push({ $fill: normalizedOptions });
345
- return this;
346
- }
347
- /**
348
- * Enable allowDiskUse for large aggregations that exceed 100MB memory limit
349
- *
350
- * @example
351
- * ```typescript
352
- * const results = await new AggregationBuilder()
353
- * .match({ status: 'active' })
354
- * .group({ _id: '$category', total: { $sum: '$amount' } })
355
- * .allowDiskUse()
356
- * .exec(Model);
357
- * ```
358
- */
359
- allowDiskUse(enable = true) {
360
- this._diskUse = enable;
361
- return this;
362
- }
363
- /**
364
- * Paginate - Add skip and limit for offset-based pagination
365
- */
366
- paginate(page, limit) {
367
- const skip = (page - 1) * limit;
368
- return this.skip(skip).limit(limit);
369
- }
370
- /**
371
- * Count total documents (useful with $facet for pagination metadata)
372
- */
373
- count(outputField = "count") {
374
- this.pipeline.push({ $count: outputField });
375
- return this;
376
- }
377
- /**
378
- * Sample - Randomly select N documents
379
- */
380
- sample(size) {
381
- this.pipeline.push({ $sample: { size } });
382
- return this;
383
- }
384
- /**
385
- * Out - Write results to a collection
386
- */
387
- out(collection) {
388
- this.pipeline.push({ $out: collection });
389
- return this;
390
- }
391
- /**
392
- * Merge - Merge results into a collection
393
- */
394
- merge(options) {
395
- this.pipeline.push({ $merge: typeof options === "string" ? { into: options } : options });
396
- return this;
397
- }
398
- /**
399
- * GeoNear - Perform geospatial queries
400
- */
401
- geoNear(options) {
402
- this.pipeline.push({ $geoNear: options });
403
- return this;
404
- }
405
- /**
406
- * GraphLookup - Perform recursive search (graph traversal)
407
- */
408
- graphLookup(options) {
409
- this.pipeline.push({ $graphLookup: options });
410
- return this;
411
- }
412
- /**
413
- * $search - Atlas Search full-text search (Atlas only)
414
- *
415
- * @example
416
- * ```typescript
417
- * .search({
418
- * index: 'default',
419
- * text: {
420
- * query: 'laptop computer',
421
- * path: ['title', 'description'],
422
- * fuzzy: { maxEdits: 2 }
423
- * }
424
- * })
425
- * ```
426
- */
427
- search(options) {
428
- this.pipeline.push({ $search: options });
429
- return this;
430
- }
431
- /**
432
- * $searchMeta - Get Atlas Search metadata (Atlas only)
433
- */
434
- searchMeta(options) {
435
- this.pipeline.push({ $searchMeta: options });
436
- return this;
437
- }
438
- /**
439
- * $vectorSearch - Semantic similarity search using vector embeddings (Atlas only)
440
- *
441
- * Requires an Atlas Vector Search index on the target field.
442
- * Must be the first stage in the pipeline.
443
- *
444
- * @example
445
- * ```typescript
446
- * const results = await new AggregationBuilder()
447
- * .vectorSearch({
448
- * index: 'vector_index',
449
- * path: 'embedding',
450
- * queryVector: await getEmbedding('running shoes'),
451
- * limit: 10,
452
- * numCandidates: 100,
453
- * filter: { category: 'footwear' }
454
- * })
455
- * .project({ embedding: 0, score: { $meta: 'vectorSearchScore' } })
456
- * .exec(ProductModel);
457
- * ```
458
- */
459
- vectorSearch(options) {
460
- if (this.pipeline.length > 0) throw new Error("[mongokit] $vectorSearch must be the first stage in the pipeline");
461
- const rawCandidates = options.numCandidates ?? Math.max(options.limit * 10, 100);
462
- const numCandidates = Math.min(Math.max(rawCandidates, options.limit), 1e4);
463
- this.pipeline.push({ $vectorSearch: {
464
- index: options.index,
465
- path: options.path,
466
- queryVector: options.queryVector,
467
- numCandidates,
468
- limit: options.limit,
469
- ...options.filter && { filter: options.filter },
470
- ...options.exact && { exact: options.exact }
471
- } });
472
- return this;
473
- }
474
- /**
475
- * Add vectorSearchScore as a field after $vectorSearch
476
- * Convenience for `.addFields({ score: { $meta: 'vectorSearchScore' } })`
477
- */
478
- withVectorScore(fieldName = "score") {
479
- return this.addFields({ [fieldName]: { $meta: "vectorSearchScore" } });
480
- }
481
- /**
482
- * Create a builder from an existing pipeline
483
- */
484
- static from(pipeline) {
485
- const builder = new AggregationBuilder();
486
- builder.pipeline = [...pipeline];
487
- return builder;
488
- }
489
- /**
490
- * Create a builder with initial match stage
491
- */
492
- static startWith(query) {
493
- return new AggregationBuilder().match(query);
494
- }
495
- };
496
-
497
- //#endregion
498
- //#region src/Repository.ts
499
- /**
500
- * Repository Pattern - Data Access Layer
501
- *
502
- * Event-driven, plugin-based abstraction for MongoDB operations
503
- * Inspired by Meta & Stripe's repository patterns
504
- *
505
- * @example
506
- * ```typescript
507
- * const userRepo = new Repository(UserModel, [
508
- * timestampPlugin(),
509
- * softDeletePlugin(),
510
- * ]);
511
- *
512
- * // Create
513
- * const user = await userRepo.create({ name: 'John', email: 'john@example.com' });
514
- *
515
- * // Read with pagination
516
- * const users = await userRepo.getAll({ page: 1, limit: 20, filters: { status: 'active' } });
517
- *
518
- * // Update
519
- * const updated = await userRepo.update(user._id, { name: 'John Doe' });
520
- *
521
- * // Delete
522
- * await userRepo.delete(user._id);
523
- * ```
524
- */
525
- /**
526
- * Production-grade repository for MongoDB
527
- * Event-driven, plugin-based, with smart pagination
528
- */
529
- var Repository = class {
530
- Model;
531
- model;
532
- _hooks;
533
- _pagination;
534
- _hookMode;
535
- _hasTextIndex = null;
536
- constructor(Model, plugins = [], paginationConfig = {}, options = {}) {
537
- this.Model = Model;
538
- this.model = Model.modelName;
539
- this._hooks = /* @__PURE__ */ new Map();
540
- this._pagination = new PaginationEngine(Model, paginationConfig);
541
- this._hookMode = options.hooks ?? "async";
542
- plugins.forEach((plugin) => this.use(plugin));
543
- }
544
- /**
545
- * Register a plugin
546
- */
547
- use(plugin) {
548
- if (typeof plugin === "function") plugin(this);
549
- else if (plugin && typeof plugin.apply === "function") plugin.apply(this);
550
- return this;
551
- }
552
- /**
553
- * Register event listener
554
- */
555
- on(event, listener) {
556
- if (!this._hooks.has(event)) this._hooks.set(event, []);
557
- this._hooks.get(event).push(listener);
558
- return this;
559
- }
560
- /**
561
- * Remove a specific event listener
562
- */
563
- off(event, listener) {
564
- const listeners = this._hooks.get(event);
565
- if (listeners) {
566
- const idx = listeners.indexOf(listener);
567
- if (idx !== -1) listeners.splice(idx, 1);
568
- }
569
- return this;
570
- }
571
- /**
572
- * Remove all listeners for an event, or all listeners entirely
573
- */
574
- removeAllListeners(event) {
575
- if (event) this._hooks.delete(event);
576
- else this._hooks.clear();
577
- return this;
578
- }
579
- /**
580
- * Emit event (sync - for backwards compatibility)
581
- */
582
- emit(event, data) {
583
- const listeners = this._hooks.get(event) || [];
584
- for (const listener of listeners) try {
585
- const result = listener(data);
586
- if (result && typeof result.then === "function") result.catch((error) => {
587
- if (event === "error:hook") return;
588
- const err = error instanceof Error ? error : new Error(String(error));
589
- this.emit("error:hook", {
590
- event,
591
- error: err
592
- });
593
- });
594
- } catch (error) {
595
- if (event === "error:hook") continue;
596
- const err = error instanceof Error ? error : new Error(String(error));
597
- this.emit("error:hook", {
598
- event,
599
- error: err
600
- });
601
- }
602
- }
603
- /**
604
- * Emit event and await all async handlers
605
- */
606
- async emitAsync(event, data) {
607
- const listeners = this._hooks.get(event) || [];
608
- for (const listener of listeners) await listener(data);
609
- }
610
- async _emitHook(event, data) {
611
- if (this._hookMode === "async") {
612
- await this.emitAsync(event, data);
613
- return;
614
- }
615
- this.emit(event, data);
616
- }
617
- async _emitErrorHook(event, data) {
618
- try {
619
- await this._emitHook(event, data);
620
- } catch (hookError) {
621
- warn(`[${this.model}] Error hook '${event}' threw: ${hookError instanceof Error ? hookError.message : String(hookError)}`);
622
- }
623
- }
624
- /**
625
- * Create single document
626
- */
627
- async create(data, options = {}) {
628
- const context = await this._buildContext("create", {
629
- data,
630
- ...options
631
- });
632
- try {
633
- const result = await create(this.Model, context.data || data, options);
634
- await this._emitHook("after:create", {
635
- context,
636
- result
637
- });
638
- return result;
639
- } catch (error) {
640
- await this._emitErrorHook("error:create", {
641
- context,
642
- error
643
- });
644
- throw this._handleError(error);
645
- }
646
- }
647
- /**
648
- * Create multiple documents
649
- */
650
- async createMany(dataArray, options = {}) {
651
- const context = await this._buildContext("createMany", {
652
- dataArray,
653
- ...options
654
- });
655
- try {
656
- const result = await createMany(this.Model, context.dataArray || dataArray, options);
657
- await this._emitHook("after:createMany", {
658
- context,
659
- result
660
- });
661
- return result;
662
- } catch (error) {
663
- await this._emitErrorHook("error:createMany", {
664
- context,
665
- error
666
- });
667
- throw this._handleError(error);
668
- }
669
- }
670
- /**
671
- * Get document by ID
672
- */
673
- async getById(id, options = {}) {
674
- const populateSpec = options.populateOptions || options.populate;
675
- const context = await this._buildContext("getById", {
676
- id,
677
- ...options,
678
- populate: populateSpec
679
- });
680
- if (context._cacheHit) return context._cachedResult;
681
- try {
682
- const result = await getById(this.Model, id, context);
683
- await this._emitHook("after:getById", {
684
- context,
685
- result
686
- });
687
- return result;
688
- } catch (error) {
689
- await this._emitErrorHook("error:getById", {
690
- context,
691
- error
692
- });
693
- throw this._handleError(error);
694
- }
695
- }
696
- /**
697
- * Get single document by query
698
- */
699
- async getByQuery(query, options = {}) {
700
- const populateSpec = options.populateOptions || options.populate;
701
- const context = await this._buildContext("getByQuery", {
702
- query,
703
- ...options,
704
- populate: populateSpec
705
- });
706
- if (context._cacheHit) return context._cachedResult;
707
- const finalQuery = context.query || query;
708
- try {
709
- const result = await getByQuery(this.Model, finalQuery, context);
710
- await this._emitHook("after:getByQuery", {
711
- context,
712
- result
713
- });
714
- return result;
715
- } catch (error) {
716
- await this._emitErrorHook("error:getByQuery", {
717
- context,
718
- error
719
- });
720
- throw this._handleError(error);
721
- }
722
- }
723
- /**
724
- * Unified pagination - auto-detects offset vs keyset based on params
725
- *
726
- * Auto-detection logic:
727
- * - If params has 'cursor' or 'after' → uses keyset pagination (stream)
728
- * - If params has 'pagination' or 'page' → uses offset pagination (paginate)
729
- * - Else → defaults to offset pagination with page=1
730
- *
731
- * @example
732
- * // Offset pagination (page-based)
733
- * await repo.getAll({ page: 1, limit: 50, filters: { status: 'active' } });
734
- * await repo.getAll({ pagination: { page: 2, limit: 20 } });
735
- *
736
- * // Keyset pagination (cursor-based)
737
- * await repo.getAll({ cursor: 'eyJ2Ij...', limit: 50 });
738
- * await repo.getAll({ after: 'eyJ2Ij...', sort: { createdAt: -1 } });
739
- *
740
- * // Simple query (defaults to page 1)
741
- * await repo.getAll({ filters: { status: 'active' } });
742
- *
743
- * // Skip cache for fresh data
744
- * await repo.getAll({ filters: { status: 'active' } }, { skipCache: true });
745
- */
746
- async getAll(params = {}, options = {}) {
747
- const context = await this._buildContext("getAll", {
748
- ...params,
749
- ...options
750
- });
751
- if (context._cacheHit) return context._cachedResult;
752
- const filters = context.filters ?? params.filters ?? {};
753
- const search = context.search ?? params.search;
754
- const sort = context.sort ?? params.sort ?? "-createdAt";
755
- const limit = context.limit ?? params.limit ?? params.pagination?.limit ?? this._pagination.config.defaultLimit;
756
- const page = context.page ?? params.pagination?.page ?? params.page;
757
- const after = context.after ?? params.cursor ?? params.after;
758
- const mode = context.mode ?? params.mode;
759
- let useKeyset = false;
760
- if (mode) useKeyset = mode === "keyset";
761
- else useKeyset = !page && !!(after || sort !== "-createdAt" && (context.sort ?? params.sort));
762
- let query = { ...filters };
763
- if (search) {
764
- if (this._hasTextIndex === null) this._hasTextIndex = this.Model.schema.indexes().some((idx) => idx[0] && Object.values(idx[0]).includes("text"));
765
- if (this._hasTextIndex) query.$text = { $search: search };
766
- else throw createError(400, `No text index found for ${this.model}. Cannot perform text search.`);
767
- }
768
- const populateSpec = options.populateOptions || params.populateOptions || context.populate || options.populate;
769
- const paginationOptions = {
770
- filters: query,
771
- sort: this._parseSort(sort),
772
- limit,
773
- populate: this._parsePopulate(populateSpec),
774
- select: context.select || options.select,
775
- lean: context.lean ?? options.lean ?? true,
776
- session: options.session,
777
- hint: context.hint ?? params.hint,
778
- maxTimeMS: context.maxTimeMS ?? params.maxTimeMS,
779
- readPreference: context.readPreference ?? options.readPreference ?? params.readPreference
780
- };
781
- try {
782
- let result;
783
- if (useKeyset) result = await this._pagination.stream({
784
- ...paginationOptions,
785
- sort: paginationOptions.sort,
786
- after
787
- });
788
- else result = await this._pagination.paginate({
789
- ...paginationOptions,
790
- page: page || 1,
791
- countStrategy: context.countStrategy ?? params.countStrategy
792
- });
793
- await this._emitHook("after:getAll", {
794
- context,
795
- result
796
- });
797
- return result;
798
- } catch (error) {
799
- await this._emitErrorHook("error:getAll", {
800
- context,
801
- error
802
- });
803
- throw this._handleError(error);
804
- }
805
- }
806
- /**
807
- * Get or create document
808
- */
809
- async getOrCreate(query, createData, options = {}) {
810
- return getOrCreate(this.Model, query, createData, options);
811
- }
812
- /**
813
- * Count documents
814
- */
815
- async count(query = {}, options = {}) {
816
- return count(this.Model, query, options);
817
- }
818
- /**
819
- * Check if document exists
820
- */
821
- async exists(query, options = {}) {
822
- return exists(this.Model, query, options);
823
- }
824
- /**
825
- * Update document by ID
826
- */
827
- async update(id, data, options = {}) {
828
- const context = await this._buildContext("update", {
829
- id,
830
- data,
831
- ...options
832
- });
833
- try {
834
- const result = await update(this.Model, id, context.data || data, context);
835
- await this._emitHook("after:update", {
836
- context,
837
- result
838
- });
839
- return result;
840
- } catch (error) {
841
- await this._emitErrorHook("error:update", {
842
- context,
843
- error
844
- });
845
- throw this._handleError(error);
846
- }
847
- }
848
- /**
849
- * Delete document by ID
850
- */
851
- async delete(id, options = {}) {
852
- const context = await this._buildContext("delete", {
853
- id,
854
- ...options
855
- });
856
- try {
857
- if (context.softDeleted) {
858
- const result = {
859
- success: true,
860
- message: "Soft deleted successfully"
861
- };
862
- await this._emitHook("after:delete", {
863
- context,
864
- result
865
- });
866
- return result;
867
- }
868
- const result = await deleteById(this.Model, id, {
869
- session: options.session,
870
- query: context.query
871
- });
872
- await this._emitHook("after:delete", {
873
- context,
874
- result
875
- });
876
- return result;
877
- } catch (error) {
878
- await this._emitErrorHook("error:delete", {
879
- context,
880
- error
881
- });
882
- throw this._handleError(error);
883
- }
884
- }
885
- /**
886
- * Execute aggregation pipeline
887
- */
888
- async aggregate(pipeline, options = {}) {
889
- return aggregate(this.Model, pipeline, options);
890
- }
891
- /**
892
- * Aggregate pipeline with pagination
893
- * Best for: Complex queries, grouping, joins
894
- */
895
- async aggregatePaginate(options = {}) {
896
- const context = await this._buildContext("aggregatePaginate", options);
897
- return this._pagination.aggregatePaginate(context);
898
- }
899
- /**
900
- * Get distinct values
901
- */
902
- async distinct(field, query = {}, options = {}) {
903
- return distinct(this.Model, field, query, options);
904
- }
905
- /**
906
- * Query with custom field lookups ($lookup)
907
- * Best for: Joins on slugs, SKUs, codes, or other indexed custom fields
908
- *
909
- * @example
910
- * ```typescript
911
- * // Join employees with departments using slug instead of ObjectId
912
- * const employees = await employeeRepo.lookupPopulate({
913
- * filters: { status: 'active' },
914
- * lookups: [
915
- * {
916
- * from: 'departments',
917
- * localField: 'departmentSlug',
918
- * foreignField: 'slug',
919
- * as: 'department',
920
- * single: true
921
- * }
922
- * ],
923
- * sort: '-createdAt',
924
- * page: 1,
925
- * limit: 50
926
- * });
927
- * ```
928
- */
929
- async lookupPopulate(options) {
930
- const context = await this._buildContext("lookupPopulate", options);
931
- try {
932
- const builder = new AggregationBuilder();
933
- const filters = context.filters ?? options.filters;
934
- if (filters && Object.keys(filters).length > 0) builder.match(filters);
935
- builder.multiLookup(options.lookups);
936
- if (options.sort) builder.sort(this._parseSort(options.sort));
937
- const page = options.page || 1;
938
- const limit = options.limit || this._pagination.config.defaultLimit || 20;
939
- const skip = (page - 1) * limit;
940
- const SAFE_LIMIT = 1e3;
941
- const SAFE_MAX_OFFSET = 1e4;
942
- if (limit > SAFE_LIMIT) warn(`[mongokit] Large limit (${limit}) in lookupPopulate. $facet results must be <16MB. Consider using smaller limits or stream-based pagination for large datasets.`);
943
- if (skip > SAFE_MAX_OFFSET) warn(`[mongokit] Large offset (${skip}) in lookupPopulate. $facet with high offsets can exceed 16MB. For deep pagination, consider using keyset/cursor-based pagination instead.`);
944
- const dataStages = [{ $skip: skip }, { $limit: limit }];
945
- if (options.select) {
946
- let projection;
947
- if (typeof options.select === "string") {
948
- projection = {};
949
- const fields = options.select.split(",").map((f) => f.trim());
950
- for (const field of fields) if (field.startsWith("-")) projection[field.substring(1)] = 0;
951
- else projection[field] = 1;
952
- } else if (Array.isArray(options.select)) {
953
- projection = {};
954
- for (const field of options.select) if (field.startsWith("-")) projection[field.substring(1)] = 0;
955
- else projection[field] = 1;
956
- } else projection = options.select;
957
- dataStages.push({ $project: projection });
958
- }
959
- builder.facet({
960
- metadata: [{ $count: "total" }],
961
- data: dataStages
962
- });
963
- const pipeline = builder.build();
964
- const result = (await this.Model.aggregate(pipeline).session(options.session || null))[0] || {
965
- metadata: [],
966
- data: []
967
- };
968
- const total = result.metadata[0]?.total || 0;
969
- const data = result.data || [];
970
- await this._emitHook("after:lookupPopulate", {
971
- context,
972
- result: data
973
- });
974
- return {
975
- data,
976
- total,
977
- page,
978
- limit
979
- };
980
- } catch (error) {
981
- await this._emitErrorHook("error:lookupPopulate", {
982
- context,
983
- error
984
- });
985
- throw this._handleError(error);
986
- }
987
- }
988
- /**
989
- * Create an aggregation builder for this model
990
- * Useful for building complex custom aggregations
991
- *
992
- * @example
993
- * ```typescript
994
- * const pipeline = repo.buildAggregation()
995
- * .match({ status: 'active' })
996
- * .lookup('departments', 'deptSlug', 'slug', 'department', true)
997
- * .group({ _id: '$department', count: { $sum: 1 } })
998
- * .sort({ count: -1 })
999
- * .build();
1000
- *
1001
- * const results = await repo.Model.aggregate(pipeline);
1002
- * ```
1003
- */
1004
- buildAggregation() {
1005
- return new AggregationBuilder();
1006
- }
1007
- /**
1008
- * Create a lookup builder
1009
- * Useful for building $lookup stages independently
1010
- *
1011
- * @example
1012
- * ```typescript
1013
- * const lookupStages = repo.buildLookup('departments')
1014
- * .localField('deptSlug')
1015
- * .foreignField('slug')
1016
- * .as('department')
1017
- * .single()
1018
- * .build();
1019
- *
1020
- * const pipeline = [
1021
- * { $match: { status: 'active' } },
1022
- * ...lookupStages
1023
- * ];
1024
- * ```
1025
- */
1026
- buildLookup(from) {
1027
- return new LookupBuilder(from);
1028
- }
1029
- /**
1030
- * Execute callback within a transaction with automatic retry on transient failures.
1031
- *
1032
- * Uses the MongoDB driver's `session.withTransaction()` which automatically retries
1033
- * on `TransientTransactionError` and `UnknownTransactionCommitResult`.
1034
- *
1035
- * The callback always receives a `ClientSession`. When `allowFallback` is true
1036
- * and the MongoDB deployment doesn't support transactions (e.g., standalone),
1037
- * the callback runs without a transaction on the same session.
1038
- *
1039
- * @param callback - Receives a `ClientSession` to pass to repository operations
1040
- * @param options.allowFallback - Run without transaction on standalone MongoDB (default: false)
1041
- * @param options.onFallback - Called when falling back to non-transactional execution
1042
- * @param options.transactionOptions - MongoDB driver transaction options (readConcern, writeConcern, etc.)
1043
- *
1044
- * @example
1045
- * ```typescript
1046
- * const result = await repo.withTransaction(async (session) => {
1047
- * const order = await repo.create({ total: 100 }, { session });
1048
- * await paymentRepo.create({ orderId: order._id }, { session });
1049
- * return order;
1050
- * });
1051
- *
1052
- * // With fallback for standalone/dev environments
1053
- * await repo.withTransaction(callback, {
1054
- * allowFallback: true,
1055
- * onFallback: (err) => logger.warn('Running without transaction', err),
1056
- * });
1057
- * ```
1058
- */
1059
- async withTransaction(callback, options = {}) {
1060
- const session = await mongoose.startSession();
1061
- try {
1062
- return await session.withTransaction(() => callback(session), options.transactionOptions);
1063
- } catch (error) {
1064
- const err = error;
1065
- if (options.allowFallback && this._isTransactionUnsupported(err)) {
1066
- options.onFallback?.(err);
1067
- return await callback(session);
1068
- }
1069
- throw err;
1070
- } finally {
1071
- await session.endSession();
1072
- }
1073
- }
1074
- _isTransactionUnsupported(error) {
1075
- const message = (error.message || "").toLowerCase();
1076
- return message.includes("transaction numbers are only allowed on a replica set member") || message.includes("replica set") || message.includes("mongos");
1077
- }
1078
- /**
1079
- * Execute custom query with event emission
1080
- */
1081
- async _executeQuery(buildQuery) {
1082
- const operation = buildQuery.name || "custom";
1083
- const context = await this._buildContext(operation, {});
1084
- try {
1085
- const result = await buildQuery(this.Model);
1086
- await this._emitHook(`after:${operation}`, {
1087
- context,
1088
- result
1089
- });
1090
- return result;
1091
- } catch (error) {
1092
- await this._emitErrorHook(`error:${operation}`, {
1093
- context,
1094
- error
1095
- });
1096
- throw this._handleError(error);
1097
- }
1098
- }
1099
- /**
1100
- * Build operation context and run before hooks
1101
- */
1102
- async _buildContext(operation, options) {
1103
- const context = {
1104
- operation,
1105
- model: this.model,
1106
- ...options
1107
- };
1108
- const event = `before:${operation}`;
1109
- const hooks = this._hooks.get(event) || [];
1110
- for (const hook of hooks) await hook(context);
1111
- return context;
1112
- }
1113
- /**
1114
- * Parse sort string or object
1115
- */
1116
- _parseSort(sort) {
1117
- if (!sort) return { createdAt: -1 };
1118
- if (typeof sort === "object") {
1119
- if (Object.keys(sort).length === 0) return { createdAt: -1 };
1120
- return sort;
1121
- }
1122
- const sortObj = {};
1123
- const fields = sort.split(",").map((s) => s.trim());
1124
- for (const field of fields) if (field.startsWith("-")) sortObj[field.substring(1)] = -1;
1125
- else sortObj[field] = 1;
1126
- return sortObj;
1127
- }
1128
- /**
1129
- * Parse populate specification
1130
- */
1131
- _parsePopulate(populate) {
1132
- if (!populate) return [];
1133
- if (typeof populate === "string") return populate.split(",").map((p) => p.trim());
1134
- if (Array.isArray(populate)) return populate.map((p) => typeof p === "string" ? p.trim() : p);
1135
- return [populate];
1136
- }
1137
- /**
1138
- * Handle errors with proper HTTP status codes
1139
- */
1140
- _handleError(error) {
1141
- if (error instanceof mongoose.Error.ValidationError) return createError(400, `Validation Error: ${Object.values(error.errors).map((err) => err.message).join(", ")}`);
1142
- if (error instanceof mongoose.Error.CastError) return createError(400, `Invalid ${error.path}: ${error.value}`);
1143
- if (error.status && error.message) return error;
1144
- return createError(500, error.message || "Internal Server Error");
1145
- }
1146
- };
1147
-
1148
- //#endregion
1149
10
  //#region src/query/QueryParser.ts
1150
11
  /**
1151
12
  * Modern Query Parser - URL to MongoDB Query Transpiler
@@ -1376,8 +237,11 @@ var QueryParser = class {
1376
237
  description: "Fields to include/exclude (comma-separated). Prefix with - to exclude. Example: name,email,-password"
1377
238
  },
1378
239
  populate: {
1379
- type: "string",
1380
- description: "Fields to populate/join (comma-separated). Example: author,category"
240
+ oneOf: [{ type: "string" }, {
241
+ type: "object",
242
+ additionalProperties: true
243
+ }],
244
+ description: "Fields to populate/join. Simple: comma-separated string (author,category). Advanced: bracket-notation object (populate[author][select]=name,email)"
1381
245
  },
1382
246
  after: {
1383
247
  type: "string",
@@ -2049,4 +913,4 @@ function createRepository(Model, plugins = [], paginationConfig = {}, options =
2049
913
  var src_default = Repository;
2050
914
 
2051
915
  //#endregion
2052
- export { AggregationBuilder, AuditTrailQuery, LookupBuilder, PaginationEngine, QueryParser, Repository, actions_exports as actions, aggregateHelpersPlugin, auditLogPlugin, auditTrailPlugin, autoInject, batchOperationsPlugin, blockIf, buildCrudSchemasFromModel, buildCrudSchemasFromMongooseSchema, cachePlugin, cascadePlugin, configureLogger, createError, createFieldPreset, createMemoryCache, createRepository, customIdPlugin, dateSequentialId, src_default as default, elasticSearchPlugin, fieldFilterPlugin, filterResponseData, getFieldsForUser, getImmutableFields, getMongooseProjection, getNextSequence, getSystemManagedFields, immutableField, isFieldUpdateAllowed, methodRegistryPlugin, mongoOperationsPlugin, multiTenantPlugin, observabilityPlugin, prefixedId, requireField, sequentialId, softDeletePlugin, subdocumentPlugin, timestampPlugin, uniqueField, validateUpdateBody, validationChainPlugin };
916
+ export { AggregationBuilder, AuditTrailQuery, HOOK_PRIORITY, LookupBuilder, PaginationEngine, QueryParser, Repository, actions_exports as actions, aggregateHelpersPlugin, auditLogPlugin, auditTrailPlugin, autoInject, batchOperationsPlugin, blockIf, buildCrudSchemasFromModel, buildCrudSchemasFromMongooseSchema, cachePlugin, cascadePlugin, configureLogger, createError, createFieldPreset, createMemoryCache, createRepository, customIdPlugin, dateSequentialId, src_default as default, elasticSearchPlugin, fieldFilterPlugin, filterResponseData, getFieldsForUser, getImmutableFields, getMongooseProjection, getNextSequence, getSystemManagedFields, immutableField, isFieldUpdateAllowed, methodRegistryPlugin, mongoOperationsPlugin, multiTenantPlugin, observabilityPlugin, prefixedId, requireField, sequentialId, softDeletePlugin, subdocumentPlugin, timestampPlugin, uniqueField, validateUpdateBody, validationChainPlugin };