@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/README.md +625 -463
- package/dist/actions/index.d.ts +2 -2
- package/dist/actions/index.js +3 -484
- package/dist/chunks/chunk-2ZN65ZOP.js +93 -0
- package/dist/chunks/chunk-CF6FLC2G.js +46 -0
- package/dist/chunks/chunk-CSLJ2PL2.js +1092 -0
- package/dist/chunks/chunk-IT7DCOKR.js +299 -0
- package/dist/chunks/chunk-M2XHQGZB.js +361 -0
- package/dist/chunks/chunk-SAKSLT47.js +470 -0
- package/dist/chunks/chunk-VJXDGP3C.js +14 -0
- package/dist/{index-CkwbNdpJ.d.ts → index-C2NCVxJK.d.ts} +170 -3
- package/dist/index.d.ts +997 -8
- package/dist/index.js +1143 -2476
- package/dist/{queryParser-Do3SgsyJ.d.ts → mongooseToJsonSchema-BKMxPbPp.d.ts} +8 -111
- package/dist/pagination/PaginationEngine.d.ts +1 -1
- package/dist/pagination/PaginationEngine.js +2 -368
- package/dist/plugins/index.d.ts +1 -1
- package/dist/plugins/index.js +4 -1170
- package/dist/{types-DDDYo18H.d.ts → types-DA0rs2Jh.d.ts} +109 -35
- package/dist/utils/index.d.ts +2 -2
- package/dist/utils/index.js +3 -711
- package/package.json +8 -3
package/dist/index.d.ts
CHANGED
|
@@ -1,11 +1,704 @@
|
|
|
1
|
-
import { A as AnyDocument,
|
|
2
|
-
export { c as AggregatePaginationOptions,
|
|
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,
|
|
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 {
|
|
8
|
-
|
|
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 };
|