@classytic/mongokit 3.0.6 → 3.1.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.d.ts CHANGED
@@ -1,11 +1,704 @@
1
- import { A as AnyDocument, g as PluginType, P as PaginationConfig, R as RepositoryOptions, h as ObjectId, S as SelectSpec, e as PopulateSpec, f as SortSpec, a as OffsetPaginationResult, b as KeysetPaginationResult, U as UpdateOptions, d as AggregatePaginationResult, W as WithTransactionOptions, i as RepositoryContext, H as HttpError } from './types-DDDYo18H.js';
2
- export { c as AggregatePaginationOptions, j as AnyModel, Z as CacheAdapter, $ as CacheOperationOptions, _ as CacheOptions, a0 as CacheStats, a2 as CascadeOptions, a1 as CascadeRelation, C as CreateOptions, z as CrudSchemas, B as DecodedCursor, D as DeleteResult, E as EventPayload, F as FieldPreset, x as FieldRules, w as FilterQuery, X as GroupResult, l as HookMode, J as JsonSchema, K as KeysetPaginationOptions, L as Logger, T as LookupOptions, Y as MinMaxResult, O as OffsetPaginationOptions, n as OperationOptions, m as PaginationResult, v as ParsedQuery, r as Plugin, s as PluginFunction, u as RepositoryEvent, t as RepositoryInstance, y as SchemaBuilderOptions, N as SoftDeleteFilterMode, M as SoftDeleteOptions, Q as SoftDeleteRepository, k as SortDirection, o as UpdateManyResult, p as UpdateWithValidationResult, q as UserContext, I as ValidationChainOptions, V as ValidationResult, G as ValidatorDefinition } from './types-DDDYo18H.js';
1
+ import { i as PaginationResult, A as AnyDocument, j as PluginType, P as PaginationConfig, R as RepositoryOptions, k as ObjectId, S as SelectSpec, e as PopulateSpec, f as SortSpec$2, a as OffsetPaginationResult, b as KeysetPaginationResult, l as UpdateOptions, d as AggregatePaginationResult, W as WithTransactionOptions, m as RepositoryContext, H as HttpError } from './types-DA0rs2Jh.js';
2
+ export { c as AggregatePaginationOptions, n as AnyModel, C as CacheAdapter, a9 as CacheOperationOptions, a8 as CacheOptions, aa as CacheStats, ac as CascadeOptions, ab as CascadeRelation, v as CreateInput, y as CreateOptions, h as CrudSchemas, $ as DecodedCursor, D as DeepPartial, z as DeleteResult, X as EventHandlers, Y as EventPayload, Q as EventPhase, F as FieldPreset, Z as FieldRules, a6 as GroupResult, p as HookMode, I as InferDocument, q as InferRawDoc, _ as JsonSchema, t as KeysOfType, K as KeysetPaginationOptions, a2 as Logger, a7 as MinMaxResult, N as NonNullableFields, O as OffsetPaginationOptions, x as OperationOptions, r as PartialBy, G as Plugin, J as PluginFunction, T as RepositoryEvent, L as RepositoryInstance, M as RepositoryOperation, s as RequiredBy, g as SchemaBuilderOptions, a4 as SoftDeleteFilterMode, a3 as SoftDeleteOptions, a5 as SoftDeleteRepository, o as SortDirection, u as Strict, w as UpdateInput, B as UpdateManyResult, E as UpdateWithValidationResult, U as UserContext, a1 as ValidationChainOptions, V as ValidationResult, a0 as ValidatorDefinition } from './types-DA0rs2Jh.js';
3
3
  import * as mongoose from 'mongoose';
4
- import { Model, ClientSession, PipelineStage, PopulateOptions } from 'mongoose';
4
+ import { PipelineStage, Expression, Model, ClientSession, PopulateOptions } from 'mongoose';
5
5
  import { PaginationEngine } from './pagination/PaginationEngine.js';
6
+ import { L as LookupOptions, a as LookupBuilder } from './index-C2NCVxJK.js';
7
+ export { i as actions } from './index-C2NCVxJK.js';
6
8
  export { aggregateHelpersPlugin, auditLogPlugin, autoInject, batchOperationsPlugin, blockIf, cachePlugin, cascadePlugin, fieldFilterPlugin, immutableField, methodRegistryPlugin, mongoOperationsPlugin, requireField, softDeletePlugin, subdocumentPlugin, timestampPlugin, uniqueField, validationChainPlugin } from './plugins/index.js';
7
- export { F as FilterValue, O as OperatorMap, Q as QueryParser, b as QueryParserOptions, h as buildCrudSchemasFromModel, e as buildCrudSchemasFromMongooseSchema, l as createError, c as createFieldPreset, m as createMemoryCache, f as filterResponseData, g as getFieldsForUser, i as getImmutableFields, a as getMongooseProjection, j as getSystemManagedFields, k as isFieldUpdateAllowed, d as queryParser, v as validateUpdateBody } from './queryParser-Do3SgsyJ.js';
8
- export { i as actions } from './index-CkwbNdpJ.js';
9
+ export { d as buildCrudSchemasFromModel, b as buildCrudSchemasFromMongooseSchema, j as createError, c as createFieldPreset, k as createMemoryCache, f as filterResponseData, g as getFieldsForUser, e as getImmutableFields, a as getMongooseProjection, h as getSystemManagedFields, i as isFieldUpdateAllowed, v as validateUpdateBody } from './mongooseToJsonSchema-BKMxPbPp.js';
10
+
11
+ /**
12
+ * Framework-Agnostic Controller Interfaces
13
+ *
14
+ * These interfaces define contracts for building controllers that work with any framework.
15
+ * Implement these in your chosen framework (Express, Fastify, Next.js, etc.)
16
+ *
17
+ * @see examples/api/baseController.ts for Express implementation
18
+ * @see examples/fastify for Fastify implementation
19
+ * @see examples/nextjs for Next.js implementation
20
+ */
21
+
22
+ /**
23
+ * Framework-agnostic request context
24
+ *
25
+ * Extract this from your framework's request object:
26
+ * - Express: { query: req.query, body: req.body, params: req.params, user: req.user }
27
+ * - Fastify: { query: request.query, body: request.body, params: request.params, user: request.user }
28
+ * - Next.js: { query: searchParams, body: await request.json(), params: params, user: await getUser() }
29
+ *
30
+ * @example
31
+ * ```typescript
32
+ * // Express
33
+ * const context: IRequestContext = {
34
+ * query: req.query,
35
+ * body: req.body,
36
+ * params: req.params,
37
+ * user: req.user,
38
+ * context: { organizationId: req.headers['x-org-id'] }
39
+ * };
40
+ *
41
+ * // Fastify
42
+ * const context: IRequestContext = {
43
+ * query: request.query,
44
+ * body: request.body,
45
+ * params: request.params,
46
+ * user: request.user,
47
+ * };
48
+ *
49
+ * // Next.js App Router
50
+ * const context: IRequestContext = {
51
+ * query: Object.fromEntries(request.nextUrl.searchParams),
52
+ * body: await request.json(),
53
+ * params: params,
54
+ * user: await auth(),
55
+ * };
56
+ * ```
57
+ */
58
+ interface IRequestContext {
59
+ /** Query parameters (from URL query string) */
60
+ query: Record<string, unknown>;
61
+ /** Request body (parsed JSON) */
62
+ body: Record<string, unknown>;
63
+ /** Route parameters (dynamic segments in URL) */
64
+ params: Record<string, string>;
65
+ /**
66
+ * Authenticated user (from your auth middleware)
67
+ * Shape depends on your auth system
68
+ */
69
+ user?: {
70
+ id: string;
71
+ role?: string;
72
+ [key: string]: unknown;
73
+ };
74
+ /**
75
+ * Custom context (tenant ID, organization, locale, etc.)
76
+ * Use this for multi-tenant apps, feature flags, etc.
77
+ */
78
+ context?: Record<string, unknown>;
79
+ }
80
+ /**
81
+ * Framework-agnostic controller response
82
+ *
83
+ * Your controller methods should return this shape.
84
+ * Adapt it to your framework's response format in the handler layer.
85
+ *
86
+ * @example
87
+ * ```typescript
88
+ * // In controller method
89
+ * async list(context: IRequestContext): Promise<IControllerResponse> {
90
+ * const result = await this.repository.getAll(context.query);
91
+ * return {
92
+ * success: true,
93
+ * data: result,
94
+ * status: 200,
95
+ * };
96
+ * }
97
+ *
98
+ * // In Express handler
99
+ * const response = await controller.list(context);
100
+ * res.status(response.status).json(response);
101
+ *
102
+ * // In Fastify handler
103
+ * const response = await controller.list(context);
104
+ * return reply.code(response.status).send(response);
105
+ *
106
+ * // In Next.js handler
107
+ * const response = await controller.list(context);
108
+ * return NextResponse.json(response.data, { status: response.status });
109
+ * ```
110
+ */
111
+ interface IControllerResponse<T = unknown> {
112
+ /** Whether the operation succeeded */
113
+ success: boolean;
114
+ /** Response data (undefined on error) */
115
+ data?: T;
116
+ /** Error message (only if success = false) */
117
+ error?: string;
118
+ /** Additional error details (validation errors, stack trace, etc.) */
119
+ details?: unknown;
120
+ /** HTTP status code */
121
+ status: number;
122
+ /** Optional metadata (pagination info, warnings, etc.) */
123
+ meta?: Record<string, unknown>;
124
+ }
125
+ /**
126
+ * Framework-agnostic CRUD controller interface
127
+ *
128
+ * Implement this interface in your controller classes.
129
+ * Each method receives a framework-agnostic context and returns a framework-agnostic response.
130
+ *
131
+ * @template TDoc - The Mongoose document type
132
+ *
133
+ * @example
134
+ * ```typescript
135
+ * import { IController, IRequestContext, IControllerResponse } from '@mongokit/core';
136
+ *
137
+ * class UserController implements IController<IUser> {
138
+ * async list(context: IRequestContext): Promise<IControllerResponse> {
139
+ * // Implementation
140
+ * }
141
+ *
142
+ * async get(context: IRequestContext): Promise<IControllerResponse> {
143
+ * // Implementation
144
+ * }
145
+ *
146
+ * // ... other methods
147
+ * }
148
+ * ```
149
+ */
150
+ interface IController<TDoc> {
151
+ /**
152
+ * List resources with filtering, pagination, sorting, lookups
153
+ *
154
+ * @param context - Framework-agnostic request context
155
+ * @returns Promise resolving to controller response with paginated data
156
+ *
157
+ * @example
158
+ * ```typescript
159
+ * // URL: GET /users?status=active&sort=-createdAt&limit=20
160
+ * const response = await controller.list({
161
+ * query: { status: 'active', sort: '-createdAt', limit: '20' },
162
+ * body: {},
163
+ * params: {},
164
+ * });
165
+ *
166
+ * // Response:
167
+ * {
168
+ * success: true,
169
+ * data: {
170
+ * method: 'offset',
171
+ * docs: [...],
172
+ * total: 100,
173
+ * page: 1,
174
+ * pages: 5
175
+ * },
176
+ * status: 200
177
+ * }
178
+ * ```
179
+ */
180
+ list(context: IRequestContext): Promise<IControllerResponse<PaginationResult<TDoc>>>;
181
+ /**
182
+ * Get single resource by ID
183
+ *
184
+ * @param context - Framework-agnostic request context (id in params)
185
+ * @returns Promise resolving to controller response with single document
186
+ *
187
+ * @example
188
+ * ```typescript
189
+ * // URL: GET /users/507f1f77bcf86cd799439011
190
+ * const response = await controller.get({
191
+ * query: {},
192
+ * body: {},
193
+ * params: { id: '507f1f77bcf86cd799439011' },
194
+ * });
195
+ *
196
+ * // Success response:
197
+ * {
198
+ * success: true,
199
+ * data: { _id: '...', name: 'John', ... },
200
+ * status: 200
201
+ * }
202
+ *
203
+ * // Not found response:
204
+ * {
205
+ * success: false,
206
+ * error: 'Resource not found',
207
+ * status: 404
208
+ * }
209
+ * ```
210
+ */
211
+ get(context: IRequestContext): Promise<IControllerResponse<TDoc>>;
212
+ /**
213
+ * Create new resource
214
+ *
215
+ * @param context - Framework-agnostic request context (data in body)
216
+ * @returns Promise resolving to controller response with created document
217
+ *
218
+ * @example
219
+ * ```typescript
220
+ * // URL: POST /users
221
+ * // Body: { name: 'John', email: 'john@example.com' }
222
+ * const response = await controller.create({
223
+ * query: {},
224
+ * body: { name: 'John', email: 'john@example.com' },
225
+ * params: {},
226
+ * });
227
+ *
228
+ * // Response:
229
+ * {
230
+ * success: true,
231
+ * data: { _id: '...', name: 'John', email: '...', createdAt: '...' },
232
+ * status: 201
233
+ * }
234
+ * ```
235
+ */
236
+ create(context: IRequestContext): Promise<IControllerResponse<TDoc>>;
237
+ /**
238
+ * Update existing resource
239
+ *
240
+ * @param context - Framework-agnostic request context (id in params, updates in body)
241
+ * @returns Promise resolving to controller response with updated document
242
+ *
243
+ * @example
244
+ * ```typescript
245
+ * // URL: PATCH /users/507f1f77bcf86cd799439011
246
+ * // Body: { name: 'Jane' }
247
+ * const response = await controller.update({
248
+ * query: {},
249
+ * body: { name: 'Jane' },
250
+ * params: { id: '507f1f77bcf86cd799439011' },
251
+ * });
252
+ *
253
+ * // Response:
254
+ * {
255
+ * success: true,
256
+ * data: { _id: '...', name: 'Jane', ... },
257
+ * status: 200
258
+ * }
259
+ * ```
260
+ */
261
+ update(context: IRequestContext): Promise<IControllerResponse<TDoc>>;
262
+ /**
263
+ * Delete resource
264
+ *
265
+ * @param context - Framework-agnostic request context (id in params)
266
+ * @returns Promise resolving to controller response with deletion result
267
+ *
268
+ * @example
269
+ * ```typescript
270
+ * // URL: DELETE /users/507f1f77bcf86cd799439011
271
+ * const response = await controller.delete({
272
+ * query: {},
273
+ * body: {},
274
+ * params: { id: '507f1f77bcf86cd799439011' },
275
+ * });
276
+ *
277
+ * // Response:
278
+ * {
279
+ * success: true,
280
+ * data: { message: 'Resource deleted successfully' },
281
+ * status: 200
282
+ * }
283
+ * ```
284
+ */
285
+ delete(context: IRequestContext): Promise<IControllerResponse<{
286
+ message: string;
287
+ }>>;
288
+ }
289
+ /**
290
+ * Optional: Response formatter utilities
291
+ *
292
+ * Helper functions to create standardized controller responses.
293
+ * Use these to maintain consistent response shapes across your API.
294
+ *
295
+ * @example
296
+ * ```typescript
297
+ * class BaseController {
298
+ * protected success<T>(data: T, status = 200): IControllerResponse<T> {
299
+ * return { success: true, data, status };
300
+ * }
301
+ *
302
+ * protected error(message: string, status = 500, details?: unknown): IControllerResponse {
303
+ * return { success: false, error: message, status, details };
304
+ * }
305
+ *
306
+ * protected paginated<T>(result: PaginationResult<T>): IControllerResponse<PaginationResult<T>> {
307
+ * return { success: true, data: result, status: 200 };
308
+ * }
309
+ * }
310
+ * ```
311
+ */
312
+ interface IResponseFormatter {
313
+ /**
314
+ * Format successful response
315
+ * @param data - Response data
316
+ * @param status - HTTP status code (default: 200)
317
+ */
318
+ success<T>(data: T, status?: number): IControllerResponse<T>;
319
+ /**
320
+ * Format error response
321
+ * @param message - Error message
322
+ * @param status - HTTP status code (default: 500)
323
+ * @param details - Additional error details
324
+ */
325
+ error(message: string, status?: number, details?: unknown): IControllerResponse;
326
+ /**
327
+ * Format paginated response
328
+ * @param result - Pagination result from Repository.getAll()
329
+ */
330
+ paginated<T>(result: PaginationResult<T>): IControllerResponse<PaginationResult<T>>;
331
+ }
332
+
333
+ /**
334
+ * AggregationBuilder - Fluent MongoDB Aggregation Pipeline Builder
335
+ *
336
+ * Modern, type-safe builder for complex MongoDB aggregations.
337
+ * Supports MongoDB 6+ features with optimized query patterns.
338
+ *
339
+ * Features:
340
+ * - Fluent, chainable API
341
+ * - $lookup with custom field joins
342
+ * - Faceted search
343
+ * - Window functions ($setWindowFields)
344
+ * - Atlas Search ($search)
345
+ * - Union queries ($unionWith)
346
+ * - Full aggregation operator support
347
+ *
348
+ * @example
349
+ * ```typescript
350
+ * const pipeline = new AggregationBuilder()
351
+ * .match({ status: 'active' })
352
+ * .lookup('departments', 'deptSlug', 'slug', 'department', true)
353
+ * .sort({ createdAt: -1 })
354
+ * .limit(50)
355
+ * .project({ password: 0 })
356
+ * .build();
357
+ *
358
+ * const results = await Model.aggregate(pipeline);
359
+ * ```
360
+ */
361
+
362
+ type SortOrder = 1 | -1 | 'asc' | 'desc';
363
+ type SortSpec$1 = Record<string, SortOrder>;
364
+ type ProjectionSpec = Record<string, 0 | 1 | Expression>;
365
+ type GroupSpec = {
366
+ _id: string | Record<string, unknown> | null;
367
+ [key: string]: unknown;
368
+ };
369
+ /**
370
+ * Fluent builder for MongoDB aggregation pipelines
371
+ * Optimized for complex queries at scale
372
+ */
373
+ declare class AggregationBuilder {
374
+ private pipeline;
375
+ /**
376
+ * Get the current pipeline
377
+ */
378
+ get(): PipelineStage[];
379
+ /**
380
+ * Build and return the final pipeline
381
+ */
382
+ build(): PipelineStage[];
383
+ /**
384
+ * Reset the pipeline
385
+ */
386
+ reset(): this;
387
+ /**
388
+ * Add a raw pipeline stage
389
+ */
390
+ addStage(stage: PipelineStage): this;
391
+ /**
392
+ * Add multiple raw pipeline stages
393
+ */
394
+ addStages(stages: PipelineStage[]): this;
395
+ /**
396
+ * $match - Filter documents
397
+ * IMPORTANT: Place $match as early as possible for performance
398
+ */
399
+ match(query: Record<string, unknown>): this;
400
+ /**
401
+ * $project - Include/exclude fields or compute new fields
402
+ */
403
+ project(projection: ProjectionSpec): this;
404
+ /**
405
+ * $group - Group documents and compute aggregations
406
+ *
407
+ * @example
408
+ * ```typescript
409
+ * .group({
410
+ * _id: '$department',
411
+ * count: { $sum: 1 },
412
+ * avgSalary: { $avg: '$salary' }
413
+ * })
414
+ * ```
415
+ */
416
+ group(groupSpec: GroupSpec): this;
417
+ /**
418
+ * $sort - Sort documents
419
+ */
420
+ sort(sortSpec: SortSpec$1 | string): this;
421
+ /**
422
+ * $limit - Limit number of documents
423
+ */
424
+ limit(count: number): this;
425
+ /**
426
+ * $skip - Skip documents
427
+ */
428
+ skip(count: number): this;
429
+ /**
430
+ * $unwind - Deconstruct array field
431
+ */
432
+ unwind(path: string, preserveNullAndEmptyArrays?: boolean): this;
433
+ /**
434
+ * $addFields - Add new fields or replace existing fields
435
+ */
436
+ addFields(fields: Record<string, unknown>): this;
437
+ /**
438
+ * $set - Alias for $addFields
439
+ */
440
+ set(fields: Record<string, unknown>): this;
441
+ /**
442
+ * $unset - Remove fields
443
+ */
444
+ unset(fields: string | string[]): this;
445
+ /**
446
+ * $replaceRoot - Replace the root document
447
+ */
448
+ replaceRoot(newRoot: string | Record<string, unknown>): this;
449
+ /**
450
+ * $lookup - Join with another collection (simple form)
451
+ *
452
+ * @param from - Collection to join with
453
+ * @param localField - Field from source collection
454
+ * @param foreignField - Field from target collection
455
+ * @param as - Output field name
456
+ * @param single - Unwrap array to single object
457
+ *
458
+ * @example
459
+ * ```typescript
460
+ * // Join employees with departments by slug
461
+ * .lookup('departments', 'deptSlug', 'slug', 'department', true)
462
+ * ```
463
+ */
464
+ lookup(from: string, localField: string, foreignField: string, as?: string, single?: boolean): this;
465
+ /**
466
+ * $lookup - Join with another collection (advanced form with pipeline)
467
+ *
468
+ * @example
469
+ * ```typescript
470
+ * .lookupWithPipeline({
471
+ * from: 'products',
472
+ * localField: 'productIds',
473
+ * foreignField: 'sku',
474
+ * as: 'products',
475
+ * pipeline: [
476
+ * { $match: { status: 'active' } },
477
+ * { $project: { name: 1, price: 1 } }
478
+ * ]
479
+ * })
480
+ * ```
481
+ */
482
+ lookupWithPipeline(options: LookupOptions): this;
483
+ /**
484
+ * Multiple lookups at once
485
+ *
486
+ * @example
487
+ * ```typescript
488
+ * .multiLookup([
489
+ * { from: 'departments', localField: 'deptSlug', foreignField: 'slug', single: true },
490
+ * { from: 'managers', localField: 'managerId', foreignField: '_id', single: true }
491
+ * ])
492
+ * ```
493
+ */
494
+ multiLookup(lookups: LookupOptions[]): this;
495
+ /**
496
+ * $facet - Process multiple aggregation pipelines in a single stage
497
+ * Useful for computing multiple aggregations in parallel
498
+ *
499
+ * @example
500
+ * ```typescript
501
+ * .facet({
502
+ * totalCount: [{ $count: 'count' }],
503
+ * avgPrice: [{ $group: { _id: null, avg: { $avg: '$price' } } }],
504
+ * topProducts: [{ $sort: { sales: -1 } }, { $limit: 10 }]
505
+ * })
506
+ * ```
507
+ */
508
+ facet(facets: Record<string, PipelineStage[]>): this;
509
+ /**
510
+ * $bucket - Categorize documents into buckets
511
+ *
512
+ * @example
513
+ * ```typescript
514
+ * .bucket({
515
+ * groupBy: '$price',
516
+ * boundaries: [0, 50, 100, 200],
517
+ * default: 'Other',
518
+ * output: {
519
+ * count: { $sum: 1 },
520
+ * products: { $push: '$name' }
521
+ * }
522
+ * })
523
+ * ```
524
+ */
525
+ bucket(options: {
526
+ groupBy: string | Expression;
527
+ boundaries: unknown[];
528
+ default?: string;
529
+ output?: Record<string, unknown>;
530
+ }): this;
531
+ /**
532
+ * $bucketAuto - Automatically determine bucket boundaries
533
+ */
534
+ bucketAuto(options: {
535
+ groupBy: string | Expression;
536
+ buckets: number;
537
+ output?: Record<string, unknown>;
538
+ granularity?: string;
539
+ }): this;
540
+ /**
541
+ * $setWindowFields - Perform window functions (MongoDB 5.0+)
542
+ * Useful for rankings, running totals, moving averages
543
+ *
544
+ * @example
545
+ * ```typescript
546
+ * .setWindowFields({
547
+ * partitionBy: '$department',
548
+ * sortBy: { salary: -1 },
549
+ * output: {
550
+ * rank: { $rank: {} },
551
+ * runningTotal: { $sum: '$salary', window: { documents: ['unbounded', 'current'] } }
552
+ * }
553
+ * })
554
+ * ```
555
+ */
556
+ setWindowFields(options: {
557
+ partitionBy?: string | Expression;
558
+ sortBy?: SortSpec$1;
559
+ output: Record<string, unknown>;
560
+ }): this;
561
+ /**
562
+ * $unionWith - Combine results from multiple collections (MongoDB 4.4+)
563
+ *
564
+ * @example
565
+ * ```typescript
566
+ * .unionWith({
567
+ * coll: 'archivedOrders',
568
+ * pipeline: [{ $match: { year: 2024 } }]
569
+ * })
570
+ * ```
571
+ */
572
+ unionWith(options: {
573
+ coll: string;
574
+ pipeline?: PipelineStage[];
575
+ }): this;
576
+ /**
577
+ * $densify - Fill gaps in data (MongoDB 5.1+)
578
+ * Useful for time series data with missing points
579
+ */
580
+ densify(options: {
581
+ field: string;
582
+ range: {
583
+ step: number;
584
+ unit?: 'millisecond' | 'second' | 'minute' | 'hour' | 'day' | 'week' | 'month' | 'quarter' | 'year';
585
+ bounds: 'full' | 'partition' | [unknown, unknown];
586
+ };
587
+ }): this;
588
+ /**
589
+ * $fill - Fill null or missing field values (MongoDB 5.3+)
590
+ */
591
+ fill(options: {
592
+ sortBy?: SortSpec$1;
593
+ output: Record<string, {
594
+ method: 'linear' | 'locf' | 'value';
595
+ value?: unknown;
596
+ }>;
597
+ }): this;
598
+ /**
599
+ * Paginate - Add skip and limit for offset-based pagination
600
+ */
601
+ paginate(page: number, limit: number): this;
602
+ /**
603
+ * Count total documents (useful with $facet for pagination metadata)
604
+ */
605
+ count(outputField?: string): this;
606
+ /**
607
+ * Sample - Randomly select N documents
608
+ */
609
+ sample(size: number): this;
610
+ /**
611
+ * Out - Write results to a collection
612
+ */
613
+ out(collection: string): this;
614
+ /**
615
+ * Merge - Merge results into a collection
616
+ */
617
+ merge(options: string | {
618
+ into: string;
619
+ on?: string | string[];
620
+ whenMatched?: string;
621
+ whenNotMatched?: string;
622
+ }): this;
623
+ /**
624
+ * GeoNear - Perform geospatial queries
625
+ */
626
+ geoNear(options: {
627
+ near: {
628
+ type: 'Point';
629
+ coordinates: [number, number];
630
+ };
631
+ distanceField: string;
632
+ maxDistance?: number;
633
+ query?: Record<string, unknown>;
634
+ spherical?: boolean;
635
+ }): this;
636
+ /**
637
+ * GraphLookup - Perform recursive search (graph traversal)
638
+ */
639
+ graphLookup(options: {
640
+ from: string;
641
+ startWith: string | Expression;
642
+ connectFromField: string;
643
+ connectToField: string;
644
+ as: string;
645
+ maxDepth?: number;
646
+ depthField?: string;
647
+ restrictSearchWithMatch?: Record<string, unknown>;
648
+ }): this;
649
+ /**
650
+ * $search - Atlas Search full-text search (Atlas only)
651
+ *
652
+ * @example
653
+ * ```typescript
654
+ * .search({
655
+ * index: 'default',
656
+ * text: {
657
+ * query: 'laptop computer',
658
+ * path: ['title', 'description'],
659
+ * fuzzy: { maxEdits: 2 }
660
+ * }
661
+ * })
662
+ * ```
663
+ */
664
+ search(options: {
665
+ index?: string;
666
+ text?: {
667
+ query: string;
668
+ path: string | string[];
669
+ fuzzy?: {
670
+ maxEdits?: number;
671
+ prefixLength?: number;
672
+ };
673
+ score?: {
674
+ boost?: {
675
+ value?: number;
676
+ };
677
+ };
678
+ };
679
+ compound?: {
680
+ must?: unknown[];
681
+ mustNot?: unknown[];
682
+ should?: unknown[];
683
+ filter?: unknown[];
684
+ };
685
+ autocomplete?: unknown;
686
+ near?: unknown;
687
+ range?: unknown;
688
+ }): this;
689
+ /**
690
+ * $searchMeta - Get Atlas Search metadata (Atlas only)
691
+ */
692
+ searchMeta(options: Record<string, unknown>): this;
693
+ /**
694
+ * Create a builder from an existing pipeline
695
+ */
696
+ static from(pipeline: PipelineStage[]): AggregationBuilder;
697
+ /**
698
+ * Create a builder with initial match stage
699
+ */
700
+ static startWith(query: Record<string, unknown>): AggregationBuilder;
701
+ }
9
702
 
10
703
  /**
11
704
  * Repository Pattern - Data Access Layer
@@ -127,7 +820,7 @@ declare class Repository<TDoc = AnyDocument> {
127
820
  */
128
821
  getAll(params?: {
129
822
  filters?: Record<string, unknown>;
130
- sort?: SortSpec | string;
823
+ sort?: SortSpec$2 | string;
131
824
  cursor?: string;
132
825
  after?: string;
133
826
  page?: number;
@@ -200,6 +893,81 @@ declare class Repository<TDoc = AnyDocument> {
200
893
  distinct<T = unknown>(field: string, query?: Record<string, unknown>, options?: {
201
894
  session?: ClientSession;
202
895
  }): Promise<T[]>;
896
+ /**
897
+ * Query with custom field lookups ($lookup)
898
+ * Best for: Joins on slugs, SKUs, codes, or other indexed custom fields
899
+ *
900
+ * @example
901
+ * ```typescript
902
+ * // Join employees with departments using slug instead of ObjectId
903
+ * const employees = await employeeRepo.lookupPopulate({
904
+ * filters: { status: 'active' },
905
+ * lookups: [
906
+ * {
907
+ * from: 'departments',
908
+ * localField: 'departmentSlug',
909
+ * foreignField: 'slug',
910
+ * as: 'department',
911
+ * single: true
912
+ * }
913
+ * ],
914
+ * sort: '-createdAt',
915
+ * page: 1,
916
+ * limit: 50
917
+ * });
918
+ * ```
919
+ */
920
+ lookupPopulate(options: {
921
+ filters?: Record<string, unknown>;
922
+ lookups: LookupOptions[];
923
+ sort?: SortSpec$2 | string;
924
+ page?: number;
925
+ limit?: number;
926
+ select?: SelectSpec;
927
+ session?: ClientSession;
928
+ }): Promise<{
929
+ data: TDoc[];
930
+ total?: number;
931
+ page?: number;
932
+ limit?: number;
933
+ }>;
934
+ /**
935
+ * Create an aggregation builder for this model
936
+ * Useful for building complex custom aggregations
937
+ *
938
+ * @example
939
+ * ```typescript
940
+ * const pipeline = repo.buildAggregation()
941
+ * .match({ status: 'active' })
942
+ * .lookup('departments', 'deptSlug', 'slug', 'department', true)
943
+ * .group({ _id: '$department', count: { $sum: 1 } })
944
+ * .sort({ count: -1 })
945
+ * .build();
946
+ *
947
+ * const results = await repo.Model.aggregate(pipeline);
948
+ * ```
949
+ */
950
+ buildAggregation(): AggregationBuilder;
951
+ /**
952
+ * Create a lookup builder
953
+ * Useful for building $lookup stages independently
954
+ *
955
+ * @example
956
+ * ```typescript
957
+ * const lookupStages = repo.buildLookup('departments')
958
+ * .localField('deptSlug')
959
+ * .foreignField('slug')
960
+ * .as('department')
961
+ * .single()
962
+ * .build();
963
+ *
964
+ * const pipeline = [
965
+ * { $match: { status: 'active' } },
966
+ * ...lookupStages
967
+ * ];
968
+ * ```
969
+ */
970
+ buildLookup(from?: string): LookupBuilder;
203
971
  /**
204
972
  * Execute callback within a transaction
205
973
  */
@@ -216,7 +984,7 @@ declare class Repository<TDoc = AnyDocument> {
216
984
  /**
217
985
  * Parse sort string or object
218
986
  */
219
- _parseSort(sort: SortSpec | string | undefined): SortSpec;
987
+ _parseSort(sort: SortSpec$2 | string | undefined): SortSpec$2;
220
988
  /**
221
989
  * Parse populate specification
222
990
  */
@@ -227,6 +995,227 @@ declare class Repository<TDoc = AnyDocument> {
227
995
  _handleError(error: Error): HttpError;
228
996
  }
229
997
 
998
+ /**
999
+ * Modern Query Parser - URL to MongoDB Query Transpiler
1000
+ *
1001
+ * Next-generation query parser that converts URL parameters to MongoDB aggregation pipelines.
1002
+ * Smarter than Prisma/tRPC for MongoDB with support for:
1003
+ * - Custom field lookups ($lookup)
1004
+ * - Complex filtering with operators
1005
+ * - Full-text search
1006
+ * - Aggregations via URL
1007
+ * - Security hardening
1008
+ *
1009
+ * @example
1010
+ * ```typescript
1011
+ * // Simple usage
1012
+ * const parser = new QueryParser();
1013
+ * const query = parser.parse(req.query);
1014
+ *
1015
+ * // URL: ?status=active&lookup[department]=slug&sort=-createdAt&page=1&limit=20
1016
+ * // Result: Complete MongoDB query with $lookup, filters, sort, pagination
1017
+ * ```
1018
+ *
1019
+ * ## SECURITY CONSIDERATIONS FOR PRODUCTION
1020
+ *
1021
+ * ### Aggregation Security (enableAggregations option)
1022
+ *
1023
+ * **IMPORTANT:** The `enableAggregations` option exposes powerful MongoDB aggregation
1024
+ * pipeline capabilities via URL parameters. While this feature includes sanitization
1025
+ * (blocks $where, $function, $accumulator), it should be used with caution:
1026
+ *
1027
+ * **Recommended security practices:**
1028
+ * 1. **Disable by default for public endpoints:**
1029
+ * ```typescript
1030
+ * const parser = new QueryParser({
1031
+ * enableAggregations: false // Default: disabled
1032
+ * });
1033
+ * ```
1034
+ *
1035
+ * 2. **Use per-route allowlists for trusted clients:**
1036
+ * ```typescript
1037
+ * // Admin/internal routes only
1038
+ * if (req.user?.role === 'admin') {
1039
+ * const allowedStages = ['$match', '$project', '$sort', '$limit'];
1040
+ * // Validate aggregate parameter against allowlist
1041
+ * }
1042
+ * ```
1043
+ *
1044
+ * 3. **Validate stage structure:** Even with sanitization, complex pipelines can
1045
+ * cause performance issues. Consider limiting:
1046
+ * - Number of pipeline stages (e.g., max 5)
1047
+ * - Specific allowed operators per stage
1048
+ * - Allowed fields in $project/$match
1049
+ *
1050
+ * 4. **Monitor resource usage:** Aggregation pipelines can be expensive.
1051
+ * Use MongoDB profiling to track slow operations.
1052
+ *
1053
+ * ### Lookup Security
1054
+ *
1055
+ * Lookups are sanitized by default (collection whitelists, field validation,
1056
+ * pipeline/let blocking). For maximum security, use per-collection field allowlists
1057
+ * in your controller layer (see BaseController example).
1058
+ *
1059
+ * ### Filter Security
1060
+ *
1061
+ * All filters are sanitized:
1062
+ * - Dangerous operators blocked ($where, $function, $accumulator, $expr)
1063
+ * - Regex patterns validated (ReDoS protection)
1064
+ * - Max filter depth enforced (prevents filter bombs)
1065
+ * - Max limit enforced (prevents resource exhaustion)
1066
+ *
1067
+ * @see {@link https://github.com/classytic/mongokit/blob/main/docs/SECURITY.md}
1068
+ */
1069
+
1070
+ type SortSpec = Record<string, 1 | -1>;
1071
+ type FilterQuery = Record<string, unknown>;
1072
+ /** Parsed query result with optional lookup configuration */
1073
+ interface ParsedQuery {
1074
+ /** MongoDB filter query */
1075
+ filters: FilterQuery;
1076
+ /** Sort specification */
1077
+ sort?: SortSpec;
1078
+ /** Fields to populate (ObjectId-based) */
1079
+ populate?: string;
1080
+ /** Page number for offset pagination */
1081
+ page?: number;
1082
+ /** Cursor for keyset pagination */
1083
+ after?: string;
1084
+ /** Limit */
1085
+ limit: number;
1086
+ /** Full-text search query */
1087
+ search?: string;
1088
+ /** Lookup configurations for custom field joins */
1089
+ lookups?: LookupOptions[];
1090
+ /** Aggregation pipeline stages (advanced) */
1091
+ aggregation?: PipelineStage[];
1092
+ /** Select/project fields */
1093
+ select?: Record<string, 0 | 1>;
1094
+ }
1095
+ interface QueryParserOptions {
1096
+ /** Maximum allowed regex pattern length (default: 500) */
1097
+ maxRegexLength?: number;
1098
+ /** Maximum allowed text search query length (default: 200) */
1099
+ maxSearchLength?: number;
1100
+ /** Maximum allowed filter depth (default: 10) */
1101
+ maxFilterDepth?: number;
1102
+ /** Maximum allowed limit value (default: 1000) */
1103
+ maxLimit?: number;
1104
+ /** Additional operators to block */
1105
+ additionalDangerousOperators?: string[];
1106
+ /** Enable lookup parsing (default: true) */
1107
+ enableLookups?: boolean;
1108
+ /** Enable aggregation parsing (default: false - requires explicit opt-in) */
1109
+ enableAggregations?: boolean;
1110
+ }
1111
+ /**
1112
+ * Modern Query Parser
1113
+ * Converts URL parameters to MongoDB queries with $lookup support
1114
+ */
1115
+ declare class QueryParser {
1116
+ private readonly options;
1117
+ private readonly operators;
1118
+ private readonly dangerousOperators;
1119
+ /**
1120
+ * Regex patterns that can cause catastrophic backtracking (ReDoS attacks)
1121
+ * Detects:
1122
+ * - Quantifiers: {n,m}
1123
+ * - Possessive quantifiers: *+, ++, ?+
1124
+ * - Nested quantifiers: (a+)+, (a*)*
1125
+ * - Backreferences: \1, \2, etc.
1126
+ * - Complex character classes: [...]...[...]
1127
+ */
1128
+ private readonly dangerousRegexPatterns;
1129
+ constructor(options?: QueryParserOptions);
1130
+ /**
1131
+ * Parse URL query parameters into MongoDB query format
1132
+ *
1133
+ * @example
1134
+ * ```typescript
1135
+ * // URL: ?status=active&lookup[department][foreignField]=slug&sort=-createdAt&page=1
1136
+ * const query = parser.parse(req.query);
1137
+ * // Returns: { filters: {...}, lookups: [...], sort: {...}, page: 1 }
1138
+ * ```
1139
+ */
1140
+ parse(query: Record<string, unknown> | null | undefined): ParsedQuery;
1141
+ /**
1142
+ * Parse lookup configurations from URL parameters
1143
+ *
1144
+ * Supported formats:
1145
+ * 1. Simple: ?lookup[department]=slug
1146
+ * → Join with 'departments' collection on slug field
1147
+ *
1148
+ * 2. Detailed: ?lookup[department][localField]=deptSlug&lookup[department][foreignField]=slug
1149
+ * → Full control over join configuration
1150
+ *
1151
+ * 3. Multiple: ?lookup[department]=slug&lookup[category]=categorySlug
1152
+ * → Multiple lookups
1153
+ *
1154
+ * @example
1155
+ * ```typescript
1156
+ * // URL: ?lookup[department][localField]=deptSlug&lookup[department][foreignField]=slug&lookup[department][single]=true
1157
+ * const lookups = parser._parseLookups({
1158
+ * department: { localField: 'deptSlug', foreignField: 'slug', single: 'true' }
1159
+ * });
1160
+ * // Returns: [{ from: 'departments', localField: 'deptSlug', foreignField: 'slug', single: true }]
1161
+ * ```
1162
+ */
1163
+ private _parseLookups;
1164
+ /**
1165
+ * Parse a single lookup configuration
1166
+ */
1167
+ private _parseSingleLookup;
1168
+ /**
1169
+ * Parse aggregation pipeline from URL (advanced feature)
1170
+ *
1171
+ * @example
1172
+ * ```typescript
1173
+ * // URL: ?aggregate[group][_id]=$status&aggregate[group][count]=$sum:1
1174
+ * const pipeline = parser._parseAggregation({
1175
+ * group: { _id: '$status', count: '$sum:1' }
1176
+ * });
1177
+ * ```
1178
+ */
1179
+ private _parseAggregation;
1180
+ /**
1181
+ * Parse select/project fields
1182
+ *
1183
+ * @example
1184
+ * ```typescript
1185
+ * // URL: ?select=name,email,-password
1186
+ * // Returns: { name: 1, email: 1, password: 0 }
1187
+ * ```
1188
+ */
1189
+ private _parseSelect;
1190
+ /**
1191
+ * Parse filter parameters
1192
+ */
1193
+ private _parseFilters;
1194
+ /**
1195
+ * Handle operator syntax: field[operator]=value
1196
+ */
1197
+ private _handleOperatorSyntax;
1198
+ /**
1199
+ * Handle bracket syntax with object value
1200
+ */
1201
+ private _handleBracketSyntax;
1202
+ private _parseSort;
1203
+ private _toMongoOperator;
1204
+ private _createSafeRegex;
1205
+ private _escapeRegex;
1206
+ /**
1207
+ * Sanitize $match configuration to prevent dangerous operators
1208
+ * Recursively filters out operators like $where, $function, $accumulator
1209
+ */
1210
+ private _sanitizeMatchConfig;
1211
+ private _sanitizeSearch;
1212
+ private _convertValue;
1213
+ private _parseOr;
1214
+ private _enhanceWithBetween;
1215
+ private _pluralize;
1216
+ private _capitalize;
1217
+ }
1218
+
230
1219
  /**
231
1220
  * Factory function to create a repository instance
232
1221
  *
@@ -239,4 +1228,4 @@ declare class Repository<TDoc = AnyDocument> {
239
1228
  */
240
1229
  declare function createRepository<TDoc>(Model: mongoose.Model<TDoc, any, any, any>, plugins?: PluginType[], paginationConfig?: PaginationConfig, options?: RepositoryOptions): Repository<TDoc>;
241
1230
 
242
- export { AggregatePaginationResult, AnyDocument, HttpError, KeysetPaginationResult, ObjectId, OffsetPaginationResult, PaginationConfig, PaginationEngine, PluginType, PopulateSpec, Repository, RepositoryContext, RepositoryOptions, SelectSpec, SortSpec, UpdateOptions, WithTransactionOptions, createRepository, Repository as default };
1231
+ export { AggregatePaginationResult, AggregationBuilder, AnyDocument, type FilterQuery, HttpError, type IController, type IControllerResponse, type IRequestContext, type IResponseFormatter, KeysetPaginationResult, LookupBuilder, LookupOptions, ObjectId, OffsetPaginationResult, PaginationConfig, PaginationEngine, PaginationResult, type ParsedQuery, PluginType, PopulateSpec, QueryParser, type QueryParserOptions, Repository, RepositoryContext, RepositoryOptions, SelectSpec, SortSpec$2 as SortSpec, UpdateOptions, WithTransactionOptions, createRepository, Repository as default };