@classytic/mongokit 3.2.0 → 3.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +470 -193
- package/dist/actions/index.d.mts +9 -0
- package/dist/actions/index.mjs +15 -0
- package/dist/aggregate-BAi4Do-X.mjs +767 -0
- package/dist/aggregate-CCHI7F51.d.mts +269 -0
- package/dist/ai/index.d.mts +125 -0
- package/dist/ai/index.mjs +203 -0
- package/dist/cache-keys-C8Z9B5sw.mjs +204 -0
- package/dist/chunk-DQk6qfdC.mjs +18 -0
- package/dist/create-BuO6xt0v.mjs +55 -0
- package/dist/custom-id.plugin-B_zIs6gE.mjs +1818 -0
- package/dist/custom-id.plugin-BzZI4gnE.d.mts +893 -0
- package/dist/index.d.mts +1012 -0
- package/dist/index.mjs +1906 -0
- package/dist/limits-DsNeCx4D.mjs +299 -0
- package/dist/logger-D8ily-PP.mjs +51 -0
- package/dist/mongooseToJsonSchema-COdDEkIJ.mjs +317 -0
- package/dist/{mongooseToJsonSchema-CaRF_bCN.d.ts → mongooseToJsonSchema-Wbvjfwkn.d.mts} +16 -89
- package/dist/pagination/PaginationEngine.d.mts +93 -0
- package/dist/pagination/PaginationEngine.mjs +196 -0
- package/dist/plugins/index.d.mts +3 -0
- package/dist/plugins/index.mjs +3 -0
- package/dist/types-D-gploPr.d.mts +1241 -0
- package/dist/utils/{index.d.ts → index.d.mts} +14 -21
- package/dist/utils/index.mjs +5 -0
- package/package.json +21 -21
- package/dist/actions/index.d.ts +0 -3
- package/dist/actions/index.js +0 -5
- package/dist/ai/index.d.ts +0 -175
- package/dist/ai/index.js +0 -206
- package/dist/chunks/chunk-2ZN65ZOP.js +0 -93
- package/dist/chunks/chunk-44KXLGPO.js +0 -388
- package/dist/chunks/chunk-DEVXDBRL.js +0 -1226
- package/dist/chunks/chunk-I7CWNAJB.js +0 -46
- package/dist/chunks/chunk-JWUAVZ3L.js +0 -8
- package/dist/chunks/chunk-UE2IEXZJ.js +0 -306
- package/dist/chunks/chunk-URLJFIR7.js +0 -22
- package/dist/chunks/chunk-VWKIKZYF.js +0 -737
- package/dist/chunks/chunk-WSFCRVEQ.js +0 -7
- package/dist/index-BDn5fSTE.d.ts +0 -516
- package/dist/index.d.ts +0 -1422
- package/dist/index.js +0 -1893
- package/dist/pagination/PaginationEngine.d.ts +0 -117
- package/dist/pagination/PaginationEngine.js +0 -3
- package/dist/plugins/index.d.ts +0 -922
- package/dist/plugins/index.js +0 -6
- package/dist/types-Jni1KgkP.d.ts +0 -780
- package/dist/utils/index.js +0 -5
|
@@ -0,0 +1,1241 @@
|
|
|
1
|
+
import * as mongoose$1 from "mongoose";
|
|
2
|
+
import { ClientSession, Document, Model, PipelineStage, PopulateOptions, Types } from "mongoose";
|
|
3
|
+
|
|
4
|
+
//#region src/query/LookupBuilder.d.ts
|
|
5
|
+
interface LookupOptions {
|
|
6
|
+
/** Collection to join with */
|
|
7
|
+
from: string;
|
|
8
|
+
/** Field from the input documents */
|
|
9
|
+
localField: string;
|
|
10
|
+
/** Field from the documents of the "from" collection */
|
|
11
|
+
foreignField: string;
|
|
12
|
+
/** Name of the new array field to add to the input documents */
|
|
13
|
+
as?: string;
|
|
14
|
+
/** Whether to unwrap array to single object */
|
|
15
|
+
single?: boolean;
|
|
16
|
+
/** Additional pipeline to run on the joined collection */
|
|
17
|
+
pipeline?: PipelineStage[];
|
|
18
|
+
/** Optional let variables for pipeline */
|
|
19
|
+
let?: Record<string, string>;
|
|
20
|
+
/** Query filter to apply before join (legacy, for aggregate.ts compatibility) */
|
|
21
|
+
query?: Record<string, unknown>;
|
|
22
|
+
/** Query options (legacy, for aggregate.ts compatibility) */
|
|
23
|
+
options?: {
|
|
24
|
+
session?: ClientSession;
|
|
25
|
+
};
|
|
26
|
+
/** Sanitize pipeline stages (default: true). Set false only for trusted server-side pipelines */
|
|
27
|
+
sanitize?: boolean;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Fluent builder for MongoDB $lookup aggregation stage
|
|
31
|
+
* Optimized for custom field joins at scale
|
|
32
|
+
*/
|
|
33
|
+
declare class LookupBuilder {
|
|
34
|
+
private options;
|
|
35
|
+
constructor(from?: string);
|
|
36
|
+
/**
|
|
37
|
+
* Set the collection to join with
|
|
38
|
+
*/
|
|
39
|
+
from(collection: string): this;
|
|
40
|
+
/**
|
|
41
|
+
* Set the local field (source collection)
|
|
42
|
+
* IMPORTANT: This field should be indexed for optimal performance
|
|
43
|
+
*/
|
|
44
|
+
localField(field: string): this;
|
|
45
|
+
/**
|
|
46
|
+
* Set the foreign field (target collection)
|
|
47
|
+
* IMPORTANT: This field should be indexed (preferably unique) for optimal performance
|
|
48
|
+
*/
|
|
49
|
+
foreignField(field: string): this;
|
|
50
|
+
/**
|
|
51
|
+
* Set the output field name
|
|
52
|
+
* Defaults to the collection name if not specified
|
|
53
|
+
*/
|
|
54
|
+
as(fieldName: string): this;
|
|
55
|
+
/**
|
|
56
|
+
* Mark this lookup as returning a single document
|
|
57
|
+
* Automatically unwraps the array result to a single object or null
|
|
58
|
+
*/
|
|
59
|
+
single(isSingle?: boolean): this;
|
|
60
|
+
/**
|
|
61
|
+
* Add a pipeline to filter/transform joined documents
|
|
62
|
+
* Useful for filtering, sorting, or limiting joined results
|
|
63
|
+
*
|
|
64
|
+
* @example
|
|
65
|
+
* ```typescript
|
|
66
|
+
* lookup.pipeline([
|
|
67
|
+
* { $match: { status: 'active' } },
|
|
68
|
+
* { $sort: { priority: -1 } },
|
|
69
|
+
* { $limit: 5 }
|
|
70
|
+
* ]);
|
|
71
|
+
* ```
|
|
72
|
+
*/
|
|
73
|
+
pipeline(stages: PipelineStage[]): this;
|
|
74
|
+
/**
|
|
75
|
+
* Set let variables for use in pipeline
|
|
76
|
+
* Allows referencing local document fields in the pipeline
|
|
77
|
+
*/
|
|
78
|
+
let(variables: Record<string, string>): this;
|
|
79
|
+
/**
|
|
80
|
+
* Build the $lookup aggregation stage(s)
|
|
81
|
+
* Returns an array of pipeline stages including $lookup and optional $unwind
|
|
82
|
+
*
|
|
83
|
+
* IMPORTANT: MongoDB $lookup has two mutually exclusive forms:
|
|
84
|
+
* 1. Simple form: { from, localField, foreignField, as }
|
|
85
|
+
* 2. Pipeline form: { from, let, pipeline, as }
|
|
86
|
+
*
|
|
87
|
+
* When pipeline or let is specified, we use the pipeline form.
|
|
88
|
+
* Otherwise, we use the simpler localField/foreignField form.
|
|
89
|
+
*/
|
|
90
|
+
build(): PipelineStage[];
|
|
91
|
+
/**
|
|
92
|
+
* Build and return only the $lookup stage (without $unwind)
|
|
93
|
+
* Useful when you want to handle unwrapping yourself
|
|
94
|
+
*/
|
|
95
|
+
buildLookupOnly(): PipelineStage.Lookup;
|
|
96
|
+
/**
|
|
97
|
+
* Static helper: Create a simple lookup in one line
|
|
98
|
+
*/
|
|
99
|
+
static simple(from: string, localField: string, foreignField: string, options?: {
|
|
100
|
+
as?: string;
|
|
101
|
+
single?: boolean;
|
|
102
|
+
}): PipelineStage[];
|
|
103
|
+
/**
|
|
104
|
+
* Static helper: Create multiple lookups at once
|
|
105
|
+
*
|
|
106
|
+
* @example
|
|
107
|
+
* ```typescript
|
|
108
|
+
* const pipeline = LookupBuilder.multiple([
|
|
109
|
+
* { from: 'departments', localField: 'deptSlug', foreignField: 'slug', single: true },
|
|
110
|
+
* { from: 'managers', localField: 'managerId', foreignField: '_id', single: true }
|
|
111
|
+
* ]);
|
|
112
|
+
* ```
|
|
113
|
+
*/
|
|
114
|
+
static multiple(lookups: LookupOptions[]): PipelineStage[];
|
|
115
|
+
/**
|
|
116
|
+
* Static helper: Create a nested lookup (lookup within lookup)
|
|
117
|
+
* Useful for multi-level joins like Order -> Product -> Category
|
|
118
|
+
*
|
|
119
|
+
* @example
|
|
120
|
+
* ```typescript
|
|
121
|
+
* // Join orders with products, then products with categories
|
|
122
|
+
* const pipeline = LookupBuilder.nested([
|
|
123
|
+
* { from: 'products', localField: 'productSku', foreignField: 'sku', as: 'product', single: true },
|
|
124
|
+
* { from: 'categories', localField: 'product.categorySlug', foreignField: 'slug', as: 'product.category', single: true }
|
|
125
|
+
* ]);
|
|
126
|
+
* ```
|
|
127
|
+
*/
|
|
128
|
+
static nested(lookups: LookupOptions[]): PipelineStage[];
|
|
129
|
+
/**
|
|
130
|
+
* Sanitize pipeline stages by blocking dangerous stages and operators.
|
|
131
|
+
* Used internally by build() and available for external use (e.g., aggregate.ts).
|
|
132
|
+
*/
|
|
133
|
+
static sanitizePipeline(stages: PipelineStage[]): PipelineStage[];
|
|
134
|
+
/**
|
|
135
|
+
* Recursively remove dangerous operators from an expression object.
|
|
136
|
+
*/
|
|
137
|
+
private static _sanitizeDeep;
|
|
138
|
+
}
|
|
139
|
+
//#endregion
|
|
140
|
+
//#region src/types/controller.types.d.ts
|
|
141
|
+
/**
|
|
142
|
+
* Framework-agnostic request context
|
|
143
|
+
*
|
|
144
|
+
* Extract this from your framework's request object:
|
|
145
|
+
* - Express: { query: req.query, body: req.body, params: req.params, user: req.user }
|
|
146
|
+
* - Fastify: { query: request.query, body: request.body, params: request.params, user: request.user }
|
|
147
|
+
* - Next.js: { query: searchParams, body: await request.json(), params: params, user: await getUser() }
|
|
148
|
+
*
|
|
149
|
+
* @example
|
|
150
|
+
* ```typescript
|
|
151
|
+
* // Express
|
|
152
|
+
* const context: IRequestContext = {
|
|
153
|
+
* query: req.query,
|
|
154
|
+
* body: req.body,
|
|
155
|
+
* params: req.params,
|
|
156
|
+
* user: req.user,
|
|
157
|
+
* context: { organizationId: req.headers['x-org-id'] }
|
|
158
|
+
* };
|
|
159
|
+
*
|
|
160
|
+
* // Fastify
|
|
161
|
+
* const context: IRequestContext = {
|
|
162
|
+
* query: request.query,
|
|
163
|
+
* body: request.body,
|
|
164
|
+
* params: request.params,
|
|
165
|
+
* user: request.user,
|
|
166
|
+
* };
|
|
167
|
+
*
|
|
168
|
+
* // Next.js App Router
|
|
169
|
+
* const context: IRequestContext = {
|
|
170
|
+
* query: Object.fromEntries(request.nextUrl.searchParams),
|
|
171
|
+
* body: await request.json(),
|
|
172
|
+
* params: params,
|
|
173
|
+
* user: await auth(),
|
|
174
|
+
* };
|
|
175
|
+
* ```
|
|
176
|
+
*/
|
|
177
|
+
interface IRequestContext {
|
|
178
|
+
/** Query parameters (from URL query string) */
|
|
179
|
+
query: Record<string, unknown>;
|
|
180
|
+
/** Request body (parsed JSON) */
|
|
181
|
+
body: Record<string, unknown>;
|
|
182
|
+
/** Route parameters (dynamic segments in URL) */
|
|
183
|
+
params: Record<string, string>;
|
|
184
|
+
/**
|
|
185
|
+
* Authenticated user (from your auth middleware)
|
|
186
|
+
* Shape depends on your auth system
|
|
187
|
+
*/
|
|
188
|
+
user?: {
|
|
189
|
+
id: string;
|
|
190
|
+
role?: string;
|
|
191
|
+
[key: string]: unknown;
|
|
192
|
+
};
|
|
193
|
+
/**
|
|
194
|
+
* Custom context (tenant ID, organization, locale, etc.)
|
|
195
|
+
* Use this for multi-tenant apps, feature flags, etc.
|
|
196
|
+
*/
|
|
197
|
+
context?: Record<string, unknown>;
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* Framework-agnostic controller response
|
|
201
|
+
*
|
|
202
|
+
* Your controller methods should return this shape.
|
|
203
|
+
* Adapt it to your framework's response format in the handler layer.
|
|
204
|
+
*
|
|
205
|
+
* @example
|
|
206
|
+
* ```typescript
|
|
207
|
+
* // In controller method
|
|
208
|
+
* async list(context: IRequestContext): Promise<IControllerResponse> {
|
|
209
|
+
* const result = await this.repository.getAll(context.query);
|
|
210
|
+
* return {
|
|
211
|
+
* success: true,
|
|
212
|
+
* data: result,
|
|
213
|
+
* status: 200,
|
|
214
|
+
* };
|
|
215
|
+
* }
|
|
216
|
+
*
|
|
217
|
+
* // In Express handler
|
|
218
|
+
* const response = await controller.list(context);
|
|
219
|
+
* res.status(response.status).json(response);
|
|
220
|
+
*
|
|
221
|
+
* // In Fastify handler
|
|
222
|
+
* const response = await controller.list(context);
|
|
223
|
+
* return reply.code(response.status).send(response);
|
|
224
|
+
*
|
|
225
|
+
* // In Next.js handler
|
|
226
|
+
* const response = await controller.list(context);
|
|
227
|
+
* return NextResponse.json(response.data, { status: response.status });
|
|
228
|
+
* ```
|
|
229
|
+
*/
|
|
230
|
+
interface IControllerResponse<T = unknown> {
|
|
231
|
+
/** Whether the operation succeeded */
|
|
232
|
+
success: boolean;
|
|
233
|
+
/** Response data (undefined on error) */
|
|
234
|
+
data?: T;
|
|
235
|
+
/** Error message (only if success = false) */
|
|
236
|
+
error?: string;
|
|
237
|
+
/** Additional error details (validation errors, stack trace, etc.) */
|
|
238
|
+
details?: unknown;
|
|
239
|
+
/** HTTP status code */
|
|
240
|
+
status: number;
|
|
241
|
+
/** Optional metadata (pagination info, warnings, etc.) */
|
|
242
|
+
meta?: Record<string, unknown>;
|
|
243
|
+
}
|
|
244
|
+
/**
|
|
245
|
+
* Framework-agnostic CRUD controller interface
|
|
246
|
+
*
|
|
247
|
+
* Implement this interface in your controller classes.
|
|
248
|
+
* Each method receives a framework-agnostic context and returns a framework-agnostic response.
|
|
249
|
+
*
|
|
250
|
+
* @template TDoc - The Mongoose document type
|
|
251
|
+
*
|
|
252
|
+
* @example
|
|
253
|
+
* ```typescript
|
|
254
|
+
* import { IController, IRequestContext, IControllerResponse } from '@mongokit/core';
|
|
255
|
+
*
|
|
256
|
+
* class UserController implements IController<IUser> {
|
|
257
|
+
* async list(context: IRequestContext): Promise<IControllerResponse> {
|
|
258
|
+
* // Implementation
|
|
259
|
+
* }
|
|
260
|
+
*
|
|
261
|
+
* async get(context: IRequestContext): Promise<IControllerResponse> {
|
|
262
|
+
* // Implementation
|
|
263
|
+
* }
|
|
264
|
+
*
|
|
265
|
+
* // ... other methods
|
|
266
|
+
* }
|
|
267
|
+
* ```
|
|
268
|
+
*/
|
|
269
|
+
interface IController<TDoc> {
|
|
270
|
+
/**
|
|
271
|
+
* List resources with filtering, pagination, sorting, lookups
|
|
272
|
+
*
|
|
273
|
+
* @param context - Framework-agnostic request context
|
|
274
|
+
* @returns Promise resolving to controller response with paginated data
|
|
275
|
+
*
|
|
276
|
+
* @example
|
|
277
|
+
* ```typescript
|
|
278
|
+
* // URL: GET /users?status=active&sort=-createdAt&limit=20
|
|
279
|
+
* const response = await controller.list({
|
|
280
|
+
* query: { status: 'active', sort: '-createdAt', limit: '20' },
|
|
281
|
+
* body: {},
|
|
282
|
+
* params: {},
|
|
283
|
+
* });
|
|
284
|
+
*
|
|
285
|
+
* // Response:
|
|
286
|
+
* {
|
|
287
|
+
* success: true,
|
|
288
|
+
* data: {
|
|
289
|
+
* method: 'offset',
|
|
290
|
+
* docs: [...],
|
|
291
|
+
* total: 100,
|
|
292
|
+
* page: 1,
|
|
293
|
+
* pages: 5
|
|
294
|
+
* },
|
|
295
|
+
* status: 200
|
|
296
|
+
* }
|
|
297
|
+
* ```
|
|
298
|
+
*/
|
|
299
|
+
list(context: IRequestContext): Promise<IControllerResponse<PaginationResult<TDoc>>>;
|
|
300
|
+
/**
|
|
301
|
+
* Get single resource by ID
|
|
302
|
+
*
|
|
303
|
+
* @param context - Framework-agnostic request context (id in params)
|
|
304
|
+
* @returns Promise resolving to controller response with single document
|
|
305
|
+
*
|
|
306
|
+
* @example
|
|
307
|
+
* ```typescript
|
|
308
|
+
* // URL: GET /users/507f1f77bcf86cd799439011
|
|
309
|
+
* const response = await controller.get({
|
|
310
|
+
* query: {},
|
|
311
|
+
* body: {},
|
|
312
|
+
* params: { id: '507f1f77bcf86cd799439011' },
|
|
313
|
+
* });
|
|
314
|
+
*
|
|
315
|
+
* // Success response:
|
|
316
|
+
* {
|
|
317
|
+
* success: true,
|
|
318
|
+
* data: { _id: '...', name: 'John', ... },
|
|
319
|
+
* status: 200
|
|
320
|
+
* }
|
|
321
|
+
*
|
|
322
|
+
* // Not found response:
|
|
323
|
+
* {
|
|
324
|
+
* success: false,
|
|
325
|
+
* error: 'Resource not found',
|
|
326
|
+
* status: 404
|
|
327
|
+
* }
|
|
328
|
+
* ```
|
|
329
|
+
*/
|
|
330
|
+
get(context: IRequestContext): Promise<IControllerResponse<TDoc>>;
|
|
331
|
+
/**
|
|
332
|
+
* Create new resource
|
|
333
|
+
*
|
|
334
|
+
* @param context - Framework-agnostic request context (data in body)
|
|
335
|
+
* @returns Promise resolving to controller response with created document
|
|
336
|
+
*
|
|
337
|
+
* @example
|
|
338
|
+
* ```typescript
|
|
339
|
+
* // URL: POST /users
|
|
340
|
+
* // Body: { name: 'John', email: 'john@example.com' }
|
|
341
|
+
* const response = await controller.create({
|
|
342
|
+
* query: {},
|
|
343
|
+
* body: { name: 'John', email: 'john@example.com' },
|
|
344
|
+
* params: {},
|
|
345
|
+
* });
|
|
346
|
+
*
|
|
347
|
+
* // Response:
|
|
348
|
+
* {
|
|
349
|
+
* success: true,
|
|
350
|
+
* data: { _id: '...', name: 'John', email: '...', createdAt: '...' },
|
|
351
|
+
* status: 201
|
|
352
|
+
* }
|
|
353
|
+
* ```
|
|
354
|
+
*/
|
|
355
|
+
create(context: IRequestContext): Promise<IControllerResponse<TDoc>>;
|
|
356
|
+
/**
|
|
357
|
+
* Update existing resource
|
|
358
|
+
*
|
|
359
|
+
* @param context - Framework-agnostic request context (id in params, updates in body)
|
|
360
|
+
* @returns Promise resolving to controller response with updated document
|
|
361
|
+
*
|
|
362
|
+
* @example
|
|
363
|
+
* ```typescript
|
|
364
|
+
* // URL: PATCH /users/507f1f77bcf86cd799439011
|
|
365
|
+
* // Body: { name: 'Jane' }
|
|
366
|
+
* const response = await controller.update({
|
|
367
|
+
* query: {},
|
|
368
|
+
* body: { name: 'Jane' },
|
|
369
|
+
* params: { id: '507f1f77bcf86cd799439011' },
|
|
370
|
+
* });
|
|
371
|
+
*
|
|
372
|
+
* // Response:
|
|
373
|
+
* {
|
|
374
|
+
* success: true,
|
|
375
|
+
* data: { _id: '...', name: 'Jane', ... },
|
|
376
|
+
* status: 200
|
|
377
|
+
* }
|
|
378
|
+
* ```
|
|
379
|
+
*/
|
|
380
|
+
update(context: IRequestContext): Promise<IControllerResponse<TDoc>>;
|
|
381
|
+
/**
|
|
382
|
+
* Delete resource
|
|
383
|
+
*
|
|
384
|
+
* @param context - Framework-agnostic request context (id in params)
|
|
385
|
+
* @returns Promise resolving to controller response with deletion result
|
|
386
|
+
*
|
|
387
|
+
* @example
|
|
388
|
+
* ```typescript
|
|
389
|
+
* // URL: DELETE /users/507f1f77bcf86cd799439011
|
|
390
|
+
* const response = await controller.delete({
|
|
391
|
+
* query: {},
|
|
392
|
+
* body: {},
|
|
393
|
+
* params: { id: '507f1f77bcf86cd799439011' },
|
|
394
|
+
* });
|
|
395
|
+
*
|
|
396
|
+
* // Response:
|
|
397
|
+
* {
|
|
398
|
+
* success: true,
|
|
399
|
+
* data: { message: 'Resource deleted successfully' },
|
|
400
|
+
* status: 200
|
|
401
|
+
* }
|
|
402
|
+
* ```
|
|
403
|
+
*/
|
|
404
|
+
delete(context: IRequestContext): Promise<IControllerResponse<{
|
|
405
|
+
message: string;
|
|
406
|
+
}>>;
|
|
407
|
+
}
|
|
408
|
+
/**
|
|
409
|
+
* Optional: Response formatter utilities
|
|
410
|
+
*
|
|
411
|
+
* Helper functions to create standardized controller responses.
|
|
412
|
+
* Use these to maintain consistent response shapes across your API.
|
|
413
|
+
*
|
|
414
|
+
* @example
|
|
415
|
+
* ```typescript
|
|
416
|
+
* class BaseController {
|
|
417
|
+
* protected success<T>(data: T, status = 200): IControllerResponse<T> {
|
|
418
|
+
* return { success: true, data, status };
|
|
419
|
+
* }
|
|
420
|
+
*
|
|
421
|
+
* protected error(message: string, status = 500, details?: unknown): IControllerResponse {
|
|
422
|
+
* return { success: false, error: message, status, details };
|
|
423
|
+
* }
|
|
424
|
+
*
|
|
425
|
+
* protected paginated<T>(result: PaginationResult<T>): IControllerResponse<PaginationResult<T>> {
|
|
426
|
+
* return { success: true, data: result, status: 200 };
|
|
427
|
+
* }
|
|
428
|
+
* }
|
|
429
|
+
* ```
|
|
430
|
+
*/
|
|
431
|
+
interface IResponseFormatter {
|
|
432
|
+
/**
|
|
433
|
+
* Format successful response
|
|
434
|
+
* @param data - Response data
|
|
435
|
+
* @param status - HTTP status code (default: 200)
|
|
436
|
+
*/
|
|
437
|
+
success<T>(data: T, status?: number): IControllerResponse<T>;
|
|
438
|
+
/**
|
|
439
|
+
* Format error response
|
|
440
|
+
* @param message - Error message
|
|
441
|
+
* @param status - HTTP status code (default: 500)
|
|
442
|
+
* @param details - Additional error details
|
|
443
|
+
*/
|
|
444
|
+
error(message: string, status?: number, details?: unknown): IControllerResponse;
|
|
445
|
+
/**
|
|
446
|
+
* Format paginated response
|
|
447
|
+
* @param result - Pagination result from Repository.getAll()
|
|
448
|
+
*/
|
|
449
|
+
paginated<T>(result: PaginationResult<T>): IControllerResponse<PaginationResult<T>>;
|
|
450
|
+
}
|
|
451
|
+
//#endregion
|
|
452
|
+
//#region src/types.d.ts
|
|
453
|
+
/** Read Preference Type for replica sets */
|
|
454
|
+
type ReadPreferenceType = "primary" | "primaryPreferred" | "secondary" | "secondaryPreferred" | "nearest" | (string & {});
|
|
455
|
+
/** Re-export mongoose ObjectId */
|
|
456
|
+
type ObjectId = Types.ObjectId;
|
|
457
|
+
/** Generic document type */
|
|
458
|
+
type AnyDocument = Document & Record<string, unknown>;
|
|
459
|
+
/** Generic model type */
|
|
460
|
+
type AnyModel = Model<AnyDocument>;
|
|
461
|
+
/** Sort direction */
|
|
462
|
+
type SortDirection = 1 | -1;
|
|
463
|
+
/** Sort specification for MongoDB queries */
|
|
464
|
+
type SortSpec = Record<string, SortDirection>;
|
|
465
|
+
/** Populate specification */
|
|
466
|
+
type PopulateSpec = string | string[] | PopulateOptions | PopulateOptions[];
|
|
467
|
+
/** Select specification */
|
|
468
|
+
type SelectSpec = string | string[] | Record<string, 0 | 1>;
|
|
469
|
+
/** Filter query type for MongoDB queries (compatible with Mongoose 8 & 9) */
|
|
470
|
+
type FilterQuery<_T = unknown> = Record<string, unknown>;
|
|
471
|
+
/**
|
|
472
|
+
* Infer document type from a Mongoose Model
|
|
473
|
+
* @example
|
|
474
|
+
* type UserDoc = InferDocument<typeof UserModel>;
|
|
475
|
+
*/
|
|
476
|
+
type InferDocument<TModel> = TModel extends Model<infer TDoc> ? TDoc : never;
|
|
477
|
+
/**
|
|
478
|
+
* Infer raw document shape (without Mongoose Document methods)
|
|
479
|
+
* @example
|
|
480
|
+
* type User = InferRawDoc<typeof UserModel>;
|
|
481
|
+
*/
|
|
482
|
+
type InferRawDoc<TModel> = TModel extends Model<infer TDoc> ? TDoc extends Document ? Omit<TDoc, keyof Document> : TDoc : never;
|
|
483
|
+
/**
|
|
484
|
+
* Make specific fields optional
|
|
485
|
+
* @example
|
|
486
|
+
* type CreateUser = PartialBy<User, 'createdAt' | 'updatedAt'>;
|
|
487
|
+
*/
|
|
488
|
+
type PartialBy<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>;
|
|
489
|
+
/**
|
|
490
|
+
* Make specific fields required
|
|
491
|
+
* @example
|
|
492
|
+
* type UserWithId = RequiredBy<User, '_id'>;
|
|
493
|
+
*/
|
|
494
|
+
type RequiredBy<T, K extends keyof T> = Omit<T, K> & Required<Pick<T, K>>;
|
|
495
|
+
/**
|
|
496
|
+
* Extract keys of type T that have values of type V
|
|
497
|
+
* @example
|
|
498
|
+
* type StringFields = KeysOfType<User, string>; // 'name' | 'email'
|
|
499
|
+
*/
|
|
500
|
+
type KeysOfType<T, V> = { [K in keyof T]: T[K] extends V ? K : never }[keyof T];
|
|
501
|
+
/**
|
|
502
|
+
* Deep partial - makes all nested properties optional
|
|
503
|
+
*/
|
|
504
|
+
type DeepPartial<T> = T extends object ? { [P in keyof T]?: DeepPartial<T[P]> } : T;
|
|
505
|
+
/**
|
|
506
|
+
* Strict object type - prevents excess properties
|
|
507
|
+
* Use with `satisfies` for compile-time validation
|
|
508
|
+
*/
|
|
509
|
+
type Strict<T> = T & { [K in Exclude<string, keyof T>]?: never };
|
|
510
|
+
/**
|
|
511
|
+
* NonNullable fields extractor
|
|
512
|
+
*/
|
|
513
|
+
type NonNullableFields<T> = { [K in keyof T]: NonNullable<T[K]> };
|
|
514
|
+
/**
|
|
515
|
+
* Create/Update input types from document
|
|
516
|
+
*/
|
|
517
|
+
type CreateInput<TDoc> = Omit<TDoc, "_id" | "createdAt" | "updatedAt" | "__v">;
|
|
518
|
+
type UpdateInput<TDoc> = Partial<Omit<TDoc, "_id" | "createdAt" | "__v">>;
|
|
519
|
+
/** Hook execution mode */
|
|
520
|
+
type HookMode = "sync" | "async";
|
|
521
|
+
/** Repository options */
|
|
522
|
+
interface RepositoryOptions {
|
|
523
|
+
/** Whether repository event hooks are awaited */
|
|
524
|
+
hooks?: HookMode;
|
|
525
|
+
}
|
|
526
|
+
/** Pagination configuration */
|
|
527
|
+
interface PaginationConfig {
|
|
528
|
+
/** Default number of documents per page (default: 10) */
|
|
529
|
+
defaultLimit?: number;
|
|
530
|
+
/** Maximum allowed limit (default: 100) */
|
|
531
|
+
maxLimit?: number;
|
|
532
|
+
/** Maximum allowed page number (default: 10000) */
|
|
533
|
+
maxPage?: number;
|
|
534
|
+
/** Page number that triggers performance warning (default: 100) */
|
|
535
|
+
deepPageThreshold?: number;
|
|
536
|
+
/** Cursor version for forward compatibility (default: 1) */
|
|
537
|
+
cursorVersion?: number;
|
|
538
|
+
/** Use estimatedDocumentCount for faster counts on large collections */
|
|
539
|
+
useEstimatedCount?: boolean;
|
|
540
|
+
}
|
|
541
|
+
/** Base pagination options */
|
|
542
|
+
interface BasePaginationOptions {
|
|
543
|
+
/** Pagination mode (explicit override) */
|
|
544
|
+
mode?: "offset" | "keyset";
|
|
545
|
+
/** MongoDB query filters */
|
|
546
|
+
filters?: FilterQuery<AnyDocument>;
|
|
547
|
+
/** Sort specification */
|
|
548
|
+
sort?: SortSpec;
|
|
549
|
+
/** Number of documents per page */
|
|
550
|
+
limit?: number;
|
|
551
|
+
/** Fields to select */
|
|
552
|
+
select?: SelectSpec;
|
|
553
|
+
/** Fields to populate */
|
|
554
|
+
populate?: PopulateSpec;
|
|
555
|
+
/** Return plain JavaScript objects */
|
|
556
|
+
lean?: boolean;
|
|
557
|
+
/** MongoDB session for transactions */
|
|
558
|
+
session?: ClientSession;
|
|
559
|
+
/** Query hint (index name or document) */
|
|
560
|
+
hint?: string | Record<string, 1 | -1>;
|
|
561
|
+
/** Maximum execution time in milliseconds */
|
|
562
|
+
maxTimeMS?: number;
|
|
563
|
+
/** Read preference for replica sets (e.g. 'secondaryPreferred') */
|
|
564
|
+
readPreference?: ReadPreferenceType;
|
|
565
|
+
}
|
|
566
|
+
/** Offset pagination options */
|
|
567
|
+
interface OffsetPaginationOptions extends BasePaginationOptions {
|
|
568
|
+
/** Page number (1-indexed) */
|
|
569
|
+
page?: number;
|
|
570
|
+
/** Count strategy for filtered queries (default: 'exact') */
|
|
571
|
+
countStrategy?: "exact" | "estimated" | "none";
|
|
572
|
+
}
|
|
573
|
+
/** Keyset (cursor) pagination options */
|
|
574
|
+
interface KeysetPaginationOptions extends BasePaginationOptions {
|
|
575
|
+
/** Cursor token for next page */
|
|
576
|
+
after?: string;
|
|
577
|
+
/** Sort is required for keyset pagination */
|
|
578
|
+
sort: SortSpec;
|
|
579
|
+
}
|
|
580
|
+
/** Aggregate pagination options */
|
|
581
|
+
interface AggregatePaginationOptions {
|
|
582
|
+
/** Aggregation pipeline stages */
|
|
583
|
+
pipeline?: PipelineStage[];
|
|
584
|
+
/** Page number (1-indexed) */
|
|
585
|
+
page?: number;
|
|
586
|
+
/** Number of documents per page */
|
|
587
|
+
limit?: number;
|
|
588
|
+
/** MongoDB session for transactions */
|
|
589
|
+
session?: ClientSession;
|
|
590
|
+
/** Query hint (index name or document) for aggregation */
|
|
591
|
+
hint?: string | Record<string, 1 | -1>;
|
|
592
|
+
/** Maximum execution time in milliseconds */
|
|
593
|
+
maxTimeMS?: number;
|
|
594
|
+
/** Count strategy (default: 'exact' via $facet) */
|
|
595
|
+
countStrategy?: "exact" | "none";
|
|
596
|
+
/** Pagination mode (reserved for API consistency) */
|
|
597
|
+
mode?: "offset";
|
|
598
|
+
/** Read preference for replica sets (e.g. 'secondaryPreferred') */
|
|
599
|
+
readPreference?: ReadPreferenceType;
|
|
600
|
+
}
|
|
601
|
+
/** Offset pagination result */
|
|
602
|
+
interface OffsetPaginationResult<T = unknown> {
|
|
603
|
+
/** Pagination method used */
|
|
604
|
+
method: "offset";
|
|
605
|
+
/** Array of documents */
|
|
606
|
+
docs: T[];
|
|
607
|
+
/** Current page number */
|
|
608
|
+
page: number;
|
|
609
|
+
/** Documents per page */
|
|
610
|
+
limit: number;
|
|
611
|
+
/** Total document count */
|
|
612
|
+
total: number;
|
|
613
|
+
/** Total page count */
|
|
614
|
+
pages: number;
|
|
615
|
+
/** Whether next page exists */
|
|
616
|
+
hasNext: boolean;
|
|
617
|
+
/** Whether previous page exists */
|
|
618
|
+
hasPrev: boolean;
|
|
619
|
+
/** Performance warning for deep pagination */
|
|
620
|
+
warning?: string;
|
|
621
|
+
}
|
|
622
|
+
/** Keyset pagination result */
|
|
623
|
+
interface KeysetPaginationResult<T = unknown> {
|
|
624
|
+
/** Pagination method used */
|
|
625
|
+
method: "keyset";
|
|
626
|
+
/** Array of documents */
|
|
627
|
+
docs: T[];
|
|
628
|
+
/** Documents per page */
|
|
629
|
+
limit: number;
|
|
630
|
+
/** Whether more documents exist */
|
|
631
|
+
hasMore: boolean;
|
|
632
|
+
/** Cursor token for next page */
|
|
633
|
+
next: string | null;
|
|
634
|
+
}
|
|
635
|
+
/** Aggregate pagination result */
|
|
636
|
+
interface AggregatePaginationResult<T = unknown> {
|
|
637
|
+
/** Pagination method used */
|
|
638
|
+
method: "aggregate";
|
|
639
|
+
/** Array of documents */
|
|
640
|
+
docs: T[];
|
|
641
|
+
/** Current page number */
|
|
642
|
+
page: number;
|
|
643
|
+
/** Documents per page */
|
|
644
|
+
limit: number;
|
|
645
|
+
/** Total document count */
|
|
646
|
+
total: number;
|
|
647
|
+
/** Total page count */
|
|
648
|
+
pages: number;
|
|
649
|
+
/** Whether next page exists */
|
|
650
|
+
hasNext: boolean;
|
|
651
|
+
/** Whether previous page exists */
|
|
652
|
+
hasPrev: boolean;
|
|
653
|
+
/** Performance warning for deep pagination */
|
|
654
|
+
warning?: string;
|
|
655
|
+
}
|
|
656
|
+
/** Union type for all pagination results */
|
|
657
|
+
type PaginationResult<T = unknown> = OffsetPaginationResult<T> | KeysetPaginationResult<T> | AggregatePaginationResult<T>;
|
|
658
|
+
/** Repository operation options */
|
|
659
|
+
interface OperationOptions {
|
|
660
|
+
/** MongoDB session for transactions */
|
|
661
|
+
session?: ClientSession;
|
|
662
|
+
/** Fields to select */
|
|
663
|
+
select?: SelectSpec;
|
|
664
|
+
/** Fields to populate */
|
|
665
|
+
populate?: PopulateSpec;
|
|
666
|
+
/** Return plain JavaScript objects */
|
|
667
|
+
lean?: boolean;
|
|
668
|
+
/** Throw error if document not found (default: true) */
|
|
669
|
+
throwOnNotFound?: boolean;
|
|
670
|
+
/** Additional query filters (e.g., for soft delete) */
|
|
671
|
+
query?: Record<string, unknown>;
|
|
672
|
+
/** Read preference for replica sets (e.g. 'secondaryPreferred') */
|
|
673
|
+
readPreference?: "primary" | "primaryPreferred" | "secondary" | "secondaryPreferred" | "nearest" | string;
|
|
674
|
+
}
|
|
675
|
+
/** withTransaction options */
|
|
676
|
+
interface WithTransactionOptions {
|
|
677
|
+
/** Allow non-transactional fallback when transactions are unsupported */
|
|
678
|
+
allowFallback?: boolean;
|
|
679
|
+
/** Optional hook to observe fallback triggers */
|
|
680
|
+
onFallback?: (error: Error) => void;
|
|
681
|
+
/** MongoDB transaction options (readConcern, writeConcern, readPreference, maxCommitTimeMS) */
|
|
682
|
+
transactionOptions?: mongoose$1.mongo.TransactionOptions;
|
|
683
|
+
}
|
|
684
|
+
/** Create operation options */
|
|
685
|
+
interface CreateOptions {
|
|
686
|
+
/** MongoDB session for transactions */
|
|
687
|
+
session?: ClientSession;
|
|
688
|
+
/** Keep insertion order on error (default: true) */
|
|
689
|
+
ordered?: boolean;
|
|
690
|
+
}
|
|
691
|
+
/** Update operation options */
|
|
692
|
+
interface UpdateOptions extends OperationOptions {
|
|
693
|
+
/** Enable update pipeline syntax */
|
|
694
|
+
updatePipeline?: boolean;
|
|
695
|
+
}
|
|
696
|
+
/** Delete result */
|
|
697
|
+
interface DeleteResult {
|
|
698
|
+
success: boolean;
|
|
699
|
+
message: string;
|
|
700
|
+
count?: number;
|
|
701
|
+
}
|
|
702
|
+
/** Update many result */
|
|
703
|
+
interface UpdateManyResult {
|
|
704
|
+
matchedCount: number;
|
|
705
|
+
modifiedCount: number;
|
|
706
|
+
}
|
|
707
|
+
/** Validation result */
|
|
708
|
+
interface ValidationResult {
|
|
709
|
+
valid: boolean;
|
|
710
|
+
violations?: Array<{
|
|
711
|
+
field: string;
|
|
712
|
+
reason: string;
|
|
713
|
+
}>;
|
|
714
|
+
message?: string;
|
|
715
|
+
}
|
|
716
|
+
/** Update with validation result */
|
|
717
|
+
type UpdateWithValidationResult<T> = {
|
|
718
|
+
success: true;
|
|
719
|
+
data: T;
|
|
720
|
+
} | {
|
|
721
|
+
success: false;
|
|
722
|
+
error: {
|
|
723
|
+
code: number;
|
|
724
|
+
message: string;
|
|
725
|
+
violations?: ValidationResult["violations"];
|
|
726
|
+
};
|
|
727
|
+
};
|
|
728
|
+
/** User context for operations */
|
|
729
|
+
interface UserContext {
|
|
730
|
+
_id?: ObjectId | string;
|
|
731
|
+
id?: string;
|
|
732
|
+
roles?: string | string[];
|
|
733
|
+
[key: string]: unknown;
|
|
734
|
+
}
|
|
735
|
+
/** Repository operation context */
|
|
736
|
+
interface RepositoryContext {
|
|
737
|
+
/** Operation name */
|
|
738
|
+
operation: string;
|
|
739
|
+
/** Model name */
|
|
740
|
+
model: string;
|
|
741
|
+
/** Document data (for create/update) */
|
|
742
|
+
data?: Record<string, unknown>;
|
|
743
|
+
/** Array of documents (for createMany) */
|
|
744
|
+
dataArray?: Record<string, unknown>[];
|
|
745
|
+
/** Document ID (for update/delete/getById) */
|
|
746
|
+
id?: string | ObjectId;
|
|
747
|
+
/** Query filters */
|
|
748
|
+
query?: FilterQuery<AnyDocument>;
|
|
749
|
+
/** User making the request */
|
|
750
|
+
user?: UserContext;
|
|
751
|
+
/** Organization ID for multi-tenancy */
|
|
752
|
+
organizationId?: string | ObjectId;
|
|
753
|
+
/** Fields to select */
|
|
754
|
+
select?: SelectSpec;
|
|
755
|
+
/** Fields to populate */
|
|
756
|
+
populate?: PopulateSpec;
|
|
757
|
+
/** Return lean documents */
|
|
758
|
+
lean?: boolean;
|
|
759
|
+
/** MongoDB session */
|
|
760
|
+
session?: ClientSession;
|
|
761
|
+
/** Read preference for replica sets */
|
|
762
|
+
readPreference?: ReadPreferenceType;
|
|
763
|
+
/** Pagination filters */
|
|
764
|
+
filters?: Record<string, unknown>;
|
|
765
|
+
/** Sort specification */
|
|
766
|
+
sort?: SortSpec;
|
|
767
|
+
/** Page number (offset pagination) */
|
|
768
|
+
page?: number;
|
|
769
|
+
/** Items per page */
|
|
770
|
+
limit?: number;
|
|
771
|
+
/** Cursor for next page (keyset pagination) */
|
|
772
|
+
after?: string;
|
|
773
|
+
/** Search query string */
|
|
774
|
+
search?: string;
|
|
775
|
+
/** Pagination mode */
|
|
776
|
+
mode?: "offset" | "keyset";
|
|
777
|
+
/** Query hint */
|
|
778
|
+
hint?: string | Record<string, 1 | -1>;
|
|
779
|
+
/** Maximum execution time in milliseconds */
|
|
780
|
+
maxTimeMS?: number;
|
|
781
|
+
/** Count strategy for offset pagination */
|
|
782
|
+
countStrategy?: "exact" | "estimated" | "none";
|
|
783
|
+
/** Whether this is a soft delete operation (set by softDeletePlugin) */
|
|
784
|
+
softDeleted?: boolean;
|
|
785
|
+
/** Include soft-deleted documents in queries */
|
|
786
|
+
includeDeleted?: boolean;
|
|
787
|
+
/** Skip cache for this operation */
|
|
788
|
+
skipCache?: boolean;
|
|
789
|
+
/** Custom TTL for this operation (seconds) */
|
|
790
|
+
cacheTtl?: number;
|
|
791
|
+
/** Whether result was served from cache (internal) */
|
|
792
|
+
_cacheHit?: boolean;
|
|
793
|
+
/** Cached result (internal) */
|
|
794
|
+
_cachedResult?: unknown;
|
|
795
|
+
/** IDs to cascade delete (internal) */
|
|
796
|
+
_cascadeIds?: unknown[];
|
|
797
|
+
/** Custom context data from plugins */
|
|
798
|
+
[key: string]: unknown;
|
|
799
|
+
}
|
|
800
|
+
/** Plugin interface */
|
|
801
|
+
interface Plugin {
|
|
802
|
+
/** Plugin name */
|
|
803
|
+
name: string;
|
|
804
|
+
/** Apply plugin to repository */
|
|
805
|
+
apply(repo: RepositoryInstance): void;
|
|
806
|
+
}
|
|
807
|
+
/** Plugin function signature */
|
|
808
|
+
type PluginFunction = (repo: RepositoryInstance) => void;
|
|
809
|
+
/** Plugin type (object or function) */
|
|
810
|
+
type PluginType = Plugin | PluginFunction;
|
|
811
|
+
/** Repository instance for plugin type reference */
|
|
812
|
+
interface RepositoryInstance {
|
|
813
|
+
Model: Model<any>;
|
|
814
|
+
model: string;
|
|
815
|
+
_hooks: Map<string, Array<(data: any) => void | Promise<void>>>;
|
|
816
|
+
_pagination: unknown;
|
|
817
|
+
use(plugin: PluginType): this;
|
|
818
|
+
on(event: string, listener: (data: any) => void | Promise<void>): this;
|
|
819
|
+
off(event: string, listener: (data: any) => void | Promise<void>): this;
|
|
820
|
+
removeAllListeners(event?: string): this;
|
|
821
|
+
emit(event: string, data: unknown): void;
|
|
822
|
+
emitAsync(event: string, data: unknown): Promise<void>;
|
|
823
|
+
registerMethod?(name: string, fn: Function): void;
|
|
824
|
+
hasMethod?(name: string): boolean;
|
|
825
|
+
[key: string]: unknown;
|
|
826
|
+
}
|
|
827
|
+
/** Repository operation names */
|
|
828
|
+
type RepositoryOperation = "create" | "createMany" | "update" | "updateMany" | "delete" | "deleteMany" | "getById" | "getByQuery" | "getAll" | "aggregatePaginate" | "lookupPopulate";
|
|
829
|
+
/** Event lifecycle phases */
|
|
830
|
+
type EventPhase = "before" | "after" | "error";
|
|
831
|
+
/** Repository event names (generated from template literals) */
|
|
832
|
+
type RepositoryEvent = `${EventPhase}:${RepositoryOperation}` | "method:registered" | "error:hook";
|
|
833
|
+
/**
|
|
834
|
+
* Type-safe event handler map
|
|
835
|
+
*
|
|
836
|
+
* Hook signature contract:
|
|
837
|
+
* - `before:*` — receives `context: RepositoryContext` directly (not wrapped).
|
|
838
|
+
* Plugins mutate context in-place to inject filters, data, etc.
|
|
839
|
+
* - `after:*` — receives `{ context, result }` where result is the operation output.
|
|
840
|
+
* - `error:*` — receives `{ context, error }` where error is the caught Error.
|
|
841
|
+
*/
|
|
842
|
+
type EventHandlers<TDoc = unknown> = { [K in RepositoryEvent]?: K extends `before:${string}` ? (context: RepositoryContext) => void | Promise<void> : K extends `after:${string}` ? (payload: {
|
|
843
|
+
context: RepositoryContext;
|
|
844
|
+
result: TDoc | TDoc[];
|
|
845
|
+
}) => void | Promise<void> : K extends `error:${string}` ? (payload: {
|
|
846
|
+
context: RepositoryContext;
|
|
847
|
+
error: Error;
|
|
848
|
+
}) => void | Promise<void> : (payload: {
|
|
849
|
+
context: RepositoryContext;
|
|
850
|
+
}) => void | Promise<void> };
|
|
851
|
+
/** Event payload */
|
|
852
|
+
interface EventPayload {
|
|
853
|
+
context: RepositoryContext;
|
|
854
|
+
result?: unknown;
|
|
855
|
+
error?: Error;
|
|
856
|
+
}
|
|
857
|
+
/** Field preset configuration */
|
|
858
|
+
interface FieldPreset {
|
|
859
|
+
/** Fields visible to everyone */
|
|
860
|
+
public: string[];
|
|
861
|
+
/** Additional fields for authenticated users */
|
|
862
|
+
authenticated?: string[];
|
|
863
|
+
/** Additional fields for admins */
|
|
864
|
+
admin?: string[];
|
|
865
|
+
}
|
|
866
|
+
/** Field rules for schema building */
|
|
867
|
+
interface FieldRules {
|
|
868
|
+
[fieldName: string]: {
|
|
869
|
+
/** Field cannot be updated */immutable?: boolean; /** Alias for immutable */
|
|
870
|
+
immutableAfterCreate?: boolean; /** System-only field (omitted from create/update) */
|
|
871
|
+
systemManaged?: boolean; /** Remove from required array */
|
|
872
|
+
optional?: boolean;
|
|
873
|
+
};
|
|
874
|
+
}
|
|
875
|
+
/** Schema builder options */
|
|
876
|
+
interface SchemaBuilderOptions {
|
|
877
|
+
/** Field rules for create/update */
|
|
878
|
+
fieldRules?: FieldRules;
|
|
879
|
+
/** Strict additional properties (default: false) */
|
|
880
|
+
strictAdditionalProperties?: boolean;
|
|
881
|
+
/** Date format: 'date' | 'datetime' */
|
|
882
|
+
dateAs?: "date" | "datetime";
|
|
883
|
+
/** Create schema options */
|
|
884
|
+
create?: {
|
|
885
|
+
/** Fields to omit from create schema */omitFields?: string[]; /** Override required status */
|
|
886
|
+
requiredOverrides?: Record<string, boolean>; /** Override optional status */
|
|
887
|
+
optionalOverrides?: Record<string, boolean>; /** Schema overrides */
|
|
888
|
+
schemaOverrides?: Record<string, unknown>;
|
|
889
|
+
};
|
|
890
|
+
/** Update schema options */
|
|
891
|
+
update?: {
|
|
892
|
+
/** Fields to omit from update schema */omitFields?: string[]; /** Require at least one field to be provided (default: false) */
|
|
893
|
+
requireAtLeastOne?: boolean;
|
|
894
|
+
};
|
|
895
|
+
/** Query schema options */
|
|
896
|
+
query?: {
|
|
897
|
+
/** Filterable fields */filterableFields?: Record<string, {
|
|
898
|
+
type: string;
|
|
899
|
+
} | unknown>;
|
|
900
|
+
};
|
|
901
|
+
}
|
|
902
|
+
/** JSON Schema type */
|
|
903
|
+
interface JsonSchema {
|
|
904
|
+
type: string;
|
|
905
|
+
properties?: Record<string, unknown>;
|
|
906
|
+
required?: string[];
|
|
907
|
+
additionalProperties?: boolean | unknown;
|
|
908
|
+
items?: unknown;
|
|
909
|
+
enum?: string[];
|
|
910
|
+
format?: string;
|
|
911
|
+
pattern?: string;
|
|
912
|
+
minProperties?: number;
|
|
913
|
+
maxProperties?: number;
|
|
914
|
+
minLength?: number;
|
|
915
|
+
maxLength?: number;
|
|
916
|
+
minimum?: number;
|
|
917
|
+
maximum?: number;
|
|
918
|
+
}
|
|
919
|
+
/** CRUD schemas result - framework-agnostic JSON schemas */
|
|
920
|
+
interface CrudSchemas {
|
|
921
|
+
/** JSON Schema for create request body */
|
|
922
|
+
createBody: JsonSchema;
|
|
923
|
+
/** JSON Schema for update request body */
|
|
924
|
+
updateBody: JsonSchema;
|
|
925
|
+
/** JSON Schema for route params (id validation) */
|
|
926
|
+
params: JsonSchema;
|
|
927
|
+
/** JSON Schema for list/query parameters */
|
|
928
|
+
listQuery: JsonSchema;
|
|
929
|
+
}
|
|
930
|
+
/** Decoded cursor */
|
|
931
|
+
interface DecodedCursor {
|
|
932
|
+
/** Primary sort field value (rehydrated) */
|
|
933
|
+
value: unknown;
|
|
934
|
+
/** Document ID (rehydrated) */
|
|
935
|
+
id: ObjectId | string;
|
|
936
|
+
/** Sort specification */
|
|
937
|
+
sort: SortSpec;
|
|
938
|
+
/** Cursor version */
|
|
939
|
+
version: number;
|
|
940
|
+
}
|
|
941
|
+
/** Validator definition */
|
|
942
|
+
interface ValidatorDefinition {
|
|
943
|
+
/** Validator name */
|
|
944
|
+
name: string;
|
|
945
|
+
/** Operations to apply validator to */
|
|
946
|
+
operations?: Array<"create" | "createMany" | "update" | "delete">;
|
|
947
|
+
/** Validation function */
|
|
948
|
+
validate: (context: RepositoryContext, repo?: RepositoryInstance) => void | Promise<void>;
|
|
949
|
+
}
|
|
950
|
+
/** Validation chain options */
|
|
951
|
+
interface ValidationChainOptions {
|
|
952
|
+
/** Stop on first validation error (default: true) */
|
|
953
|
+
stopOnFirstError?: boolean;
|
|
954
|
+
}
|
|
955
|
+
/** Logger interface for audit plugin */
|
|
956
|
+
interface Logger {
|
|
957
|
+
info?(message: string, meta?: Record<string, unknown>): void;
|
|
958
|
+
error?(message: string, meta?: Record<string, unknown>): void;
|
|
959
|
+
warn?(message: string, meta?: Record<string, unknown>): void;
|
|
960
|
+
debug?(message: string, meta?: Record<string, unknown>): void;
|
|
961
|
+
}
|
|
962
|
+
/** Filter mode for soft delete queries */
|
|
963
|
+
type SoftDeleteFilterMode = "null" | "exists";
|
|
964
|
+
/** Soft delete plugin options */
|
|
965
|
+
interface SoftDeleteOptions {
|
|
966
|
+
/** Field name for deletion timestamp (default: 'deletedAt') */
|
|
967
|
+
deletedField?: string;
|
|
968
|
+
/** Field name for deleting user (default: 'deletedBy') */
|
|
969
|
+
deletedByField?: string;
|
|
970
|
+
/** Enable soft delete (default: true) */
|
|
971
|
+
soft?: boolean;
|
|
972
|
+
/**
|
|
973
|
+
* Filter mode for excluding deleted documents (default: 'null')
|
|
974
|
+
* - 'null': Filters where deletedField is null (works with `default: null` in schema)
|
|
975
|
+
* - 'exists': Filters where deletedField does not exist (legacy behavior)
|
|
976
|
+
*/
|
|
977
|
+
filterMode?: SoftDeleteFilterMode;
|
|
978
|
+
/** Add restore method to repository (default: true) */
|
|
979
|
+
addRestoreMethod?: boolean;
|
|
980
|
+
/** Add getDeleted method to repository (default: true) */
|
|
981
|
+
addGetDeletedMethod?: boolean;
|
|
982
|
+
/**
|
|
983
|
+
* TTL in days for auto-cleanup of deleted documents.
|
|
984
|
+
* When set, creates a TTL index on the deletedField.
|
|
985
|
+
* Documents will be automatically removed after the specified days.
|
|
986
|
+
*/
|
|
987
|
+
ttlDays?: number;
|
|
988
|
+
}
|
|
989
|
+
/** Repository with soft delete methods */
|
|
990
|
+
interface SoftDeleteRepository {
|
|
991
|
+
/**
|
|
992
|
+
* Restore a soft-deleted document by setting deletedAt to null
|
|
993
|
+
* @param id - Document ID to restore
|
|
994
|
+
* @param options - Optional session for transactions
|
|
995
|
+
* @returns The restored document
|
|
996
|
+
*/
|
|
997
|
+
restore(id: string | ObjectId, options?: {
|
|
998
|
+
session?: ClientSession;
|
|
999
|
+
}): Promise<unknown>;
|
|
1000
|
+
/**
|
|
1001
|
+
* Get all soft-deleted documents
|
|
1002
|
+
* @param params - Query parameters (filters, pagination, etc.)
|
|
1003
|
+
* @param options - Query options (select, populate, etc.)
|
|
1004
|
+
* @returns Paginated result of deleted documents
|
|
1005
|
+
*/
|
|
1006
|
+
getDeleted(params?: {
|
|
1007
|
+
filters?: Record<string, unknown>;
|
|
1008
|
+
sort?: SortSpec | string;
|
|
1009
|
+
page?: number;
|
|
1010
|
+
limit?: number;
|
|
1011
|
+
}, options?: {
|
|
1012
|
+
select?: SelectSpec;
|
|
1013
|
+
populate?: PopulateSpec;
|
|
1014
|
+
lean?: boolean;
|
|
1015
|
+
session?: ClientSession;
|
|
1016
|
+
}): Promise<OffsetPaginationResult<unknown>>;
|
|
1017
|
+
}
|
|
1018
|
+
/** Group result */
|
|
1019
|
+
interface GroupResult {
|
|
1020
|
+
_id: unknown;
|
|
1021
|
+
count: number;
|
|
1022
|
+
}
|
|
1023
|
+
/** Min/Max result */
|
|
1024
|
+
interface MinMaxResult {
|
|
1025
|
+
min: unknown;
|
|
1026
|
+
max: unknown;
|
|
1027
|
+
}
|
|
1028
|
+
/**
|
|
1029
|
+
* Cache adapter interface - bring your own cache implementation
|
|
1030
|
+
* Works with Redis, Memcached, in-memory, or any key-value store
|
|
1031
|
+
*
|
|
1032
|
+
* @example Redis implementation:
|
|
1033
|
+
* ```typescript
|
|
1034
|
+
* const redisCache: CacheAdapter = {
|
|
1035
|
+
* async get(key) { return JSON.parse(await redis.get(key) || 'null'); },
|
|
1036
|
+
* async set(key, value, ttl) { await redis.setex(key, ttl, JSON.stringify(value)); },
|
|
1037
|
+
* async del(key) { await redis.del(key); },
|
|
1038
|
+
* async clear(pattern) {
|
|
1039
|
+
* const keys = await redis.keys(pattern || '*');
|
|
1040
|
+
* if (keys.length) await redis.del(...keys);
|
|
1041
|
+
* }
|
|
1042
|
+
* };
|
|
1043
|
+
* ```
|
|
1044
|
+
*/
|
|
1045
|
+
interface CacheAdapter {
|
|
1046
|
+
/** Get value by key, returns null if not found or expired */
|
|
1047
|
+
get<T = unknown>(key: string): Promise<T | null>;
|
|
1048
|
+
/** Set value with TTL in seconds */
|
|
1049
|
+
set<T = unknown>(key: string, value: T, ttl: number): Promise<void>;
|
|
1050
|
+
/** Delete single key */
|
|
1051
|
+
del(key: string): Promise<void>;
|
|
1052
|
+
/** Clear keys matching pattern (optional, used for bulk invalidation) */
|
|
1053
|
+
clear?(pattern?: string): Promise<void>;
|
|
1054
|
+
}
|
|
1055
|
+
/** Cache plugin options */
|
|
1056
|
+
interface CacheOptions {
|
|
1057
|
+
/** Cache adapter implementation (required) */
|
|
1058
|
+
adapter: CacheAdapter;
|
|
1059
|
+
/** Default TTL in seconds (default: 60) */
|
|
1060
|
+
ttl?: number;
|
|
1061
|
+
/** TTL for byId queries in seconds (default: same as ttl) */
|
|
1062
|
+
byIdTtl?: number;
|
|
1063
|
+
/** TTL for query/list results in seconds (default: same as ttl) */
|
|
1064
|
+
queryTtl?: number;
|
|
1065
|
+
/** Key prefix for namespacing (default: 'mk') */
|
|
1066
|
+
prefix?: string;
|
|
1067
|
+
/** Enable debug logging (default: false) */
|
|
1068
|
+
debug?: boolean;
|
|
1069
|
+
/**
|
|
1070
|
+
* Skip caching for queries with these characteristics:
|
|
1071
|
+
* - largeLimit: Skip if limit > value (default: 100)
|
|
1072
|
+
*/
|
|
1073
|
+
skipIf?: {
|
|
1074
|
+
largeLimit?: number;
|
|
1075
|
+
};
|
|
1076
|
+
}
|
|
1077
|
+
/** Options for cache-aware operations */
|
|
1078
|
+
interface CacheOperationOptions {
|
|
1079
|
+
/** Skip cache for this operation (read from DB directly) */
|
|
1080
|
+
skipCache?: boolean;
|
|
1081
|
+
/** Custom TTL for this operation in seconds */
|
|
1082
|
+
cacheTtl?: number;
|
|
1083
|
+
}
|
|
1084
|
+
/** Cache statistics (for debugging/monitoring) */
|
|
1085
|
+
interface CacheStats {
|
|
1086
|
+
hits: number;
|
|
1087
|
+
misses: number;
|
|
1088
|
+
sets: number;
|
|
1089
|
+
invalidations: number;
|
|
1090
|
+
}
|
|
1091
|
+
/** Cascade relation definition */
|
|
1092
|
+
interface CascadeRelation {
|
|
1093
|
+
/** Model name to cascade delete to */
|
|
1094
|
+
model: string;
|
|
1095
|
+
/** Foreign key field in the related model that references the deleted document */
|
|
1096
|
+
foreignKey: string;
|
|
1097
|
+
/** Whether to use soft delete if available (default: follows parent behavior) */
|
|
1098
|
+
softDelete?: boolean;
|
|
1099
|
+
}
|
|
1100
|
+
/** Cascade delete plugin options */
|
|
1101
|
+
interface CascadeOptions {
|
|
1102
|
+
/** Relations to cascade delete */
|
|
1103
|
+
relations: CascadeRelation[];
|
|
1104
|
+
/** Run cascade deletes in parallel (default: true) */
|
|
1105
|
+
parallel?: boolean;
|
|
1106
|
+
/** Logger for cascade operations */
|
|
1107
|
+
logger?: Logger;
|
|
1108
|
+
}
|
|
1109
|
+
/** HTTP Error with status code */
|
|
1110
|
+
interface HttpError extends Error {
|
|
1111
|
+
status: number;
|
|
1112
|
+
validationErrors?: Array<{
|
|
1113
|
+
validator: string;
|
|
1114
|
+
error: string;
|
|
1115
|
+
}>;
|
|
1116
|
+
}
|
|
1117
|
+
/**
|
|
1118
|
+
* Combines all plugin method types into a single type
|
|
1119
|
+
* Useful when you're using all plugins and want full type safety
|
|
1120
|
+
*
|
|
1121
|
+
* @example
|
|
1122
|
+
* ```typescript
|
|
1123
|
+
* import { Repository } from '@classytic/mongokit';
|
|
1124
|
+
* import type { AllPluginMethods } from '@classytic/mongokit';
|
|
1125
|
+
*
|
|
1126
|
+
* class UserRepo extends Repository<IUser> {}
|
|
1127
|
+
*
|
|
1128
|
+
* const repo = new UserRepo(Model, [...allPlugins]) as UserRepo & AllPluginMethods<IUser>;
|
|
1129
|
+
*
|
|
1130
|
+
* // TypeScript knows about all plugin methods!
|
|
1131
|
+
* await repo.increment(id, 'views', 1);
|
|
1132
|
+
* await repo.restore(id);
|
|
1133
|
+
* await repo.invalidateCache(id);
|
|
1134
|
+
* ```
|
|
1135
|
+
*
|
|
1136
|
+
* Note: Import the individual plugin method types if you need them:
|
|
1137
|
+
* ```typescript
|
|
1138
|
+
* import type {
|
|
1139
|
+
* MongoOperationsMethods,
|
|
1140
|
+
* BatchOperationsMethods,
|
|
1141
|
+
* AggregateHelpersMethods,
|
|
1142
|
+
* SubdocumentMethods,
|
|
1143
|
+
* SoftDeleteMethods,
|
|
1144
|
+
* CacheMethods,
|
|
1145
|
+
* } from '@classytic/mongokit';
|
|
1146
|
+
* ```
|
|
1147
|
+
*/
|
|
1148
|
+
type AllPluginMethods<TDoc> = {
|
|
1149
|
+
upsert(query: Record<string, unknown>, data: Record<string, unknown>, options?: Record<string, unknown>): Promise<TDoc>;
|
|
1150
|
+
increment(id: string | ObjectId, field: string, value?: number, options?: Record<string, unknown>): Promise<TDoc>;
|
|
1151
|
+
decrement(id: string | ObjectId, field: string, value?: number, options?: Record<string, unknown>): Promise<TDoc>;
|
|
1152
|
+
pushToArray(id: string | ObjectId, field: string, value: unknown, options?: Record<string, unknown>): Promise<TDoc>;
|
|
1153
|
+
pullFromArray(id: string | ObjectId, field: string, value: unknown, options?: Record<string, unknown>): Promise<TDoc>;
|
|
1154
|
+
addToSet(id: string | ObjectId, field: string, value: unknown, options?: Record<string, unknown>): Promise<TDoc>;
|
|
1155
|
+
setField(id: string | ObjectId, field: string, value: unknown, options?: Record<string, unknown>): Promise<TDoc>;
|
|
1156
|
+
unsetField(id: string | ObjectId, fields: string | string[], options?: Record<string, unknown>): Promise<TDoc>;
|
|
1157
|
+
renameField(id: string | ObjectId, oldName: string, newName: string, options?: Record<string, unknown>): Promise<TDoc>;
|
|
1158
|
+
multiplyField(id: string | ObjectId, field: string, multiplier: number, options?: Record<string, unknown>): Promise<TDoc>;
|
|
1159
|
+
setMin(id: string | ObjectId, field: string, value: unknown, options?: Record<string, unknown>): Promise<TDoc>;
|
|
1160
|
+
setMax(id: string | ObjectId, field: string, value: unknown, options?: Record<string, unknown>): Promise<TDoc>;
|
|
1161
|
+
updateMany(query: Record<string, unknown>, data: Record<string, unknown>, options?: {
|
|
1162
|
+
session?: ClientSession;
|
|
1163
|
+
updatePipeline?: boolean;
|
|
1164
|
+
}): Promise<{
|
|
1165
|
+
acknowledged: boolean;
|
|
1166
|
+
matchedCount: number;
|
|
1167
|
+
modifiedCount: number;
|
|
1168
|
+
upsertedCount: number;
|
|
1169
|
+
upsertedId: unknown;
|
|
1170
|
+
}>;
|
|
1171
|
+
deleteMany(query: Record<string, unknown>, options?: Record<string, unknown>): Promise<{
|
|
1172
|
+
acknowledged: boolean;
|
|
1173
|
+
deletedCount: number;
|
|
1174
|
+
}>;
|
|
1175
|
+
groupBy(field: string, options?: {
|
|
1176
|
+
limit?: number;
|
|
1177
|
+
session?: unknown;
|
|
1178
|
+
}): Promise<Array<{
|
|
1179
|
+
_id: unknown;
|
|
1180
|
+
count: number;
|
|
1181
|
+
}>>;
|
|
1182
|
+
sum(field: string, query?: Record<string, unknown>, options?: Record<string, unknown>): Promise<number>;
|
|
1183
|
+
average(field: string, query?: Record<string, unknown>, options?: Record<string, unknown>): Promise<number>;
|
|
1184
|
+
min(field: string, query?: Record<string, unknown>, options?: Record<string, unknown>): Promise<number>;
|
|
1185
|
+
max(field: string, query?: Record<string, unknown>, options?: Record<string, unknown>): Promise<number>;
|
|
1186
|
+
addSubdocument(parentId: string | ObjectId, arrayPath: string, subData: Record<string, unknown>, options?: Record<string, unknown>): Promise<TDoc>;
|
|
1187
|
+
getSubdocument(parentId: string | ObjectId, arrayPath: string, subId: string | ObjectId, options?: {
|
|
1188
|
+
lean?: boolean;
|
|
1189
|
+
session?: unknown;
|
|
1190
|
+
}): Promise<Record<string, unknown>>;
|
|
1191
|
+
updateSubdocument(parentId: string | ObjectId, arrayPath: string, subId: string | ObjectId, updateData: Record<string, unknown>, options?: {
|
|
1192
|
+
session?: unknown;
|
|
1193
|
+
}): Promise<TDoc>;
|
|
1194
|
+
deleteSubdocument(parentId: string | ObjectId, arrayPath: string, subId: string | ObjectId, options?: Record<string, unknown>): Promise<TDoc>;
|
|
1195
|
+
restore(id: string | ObjectId, options?: {
|
|
1196
|
+
session?: ClientSession;
|
|
1197
|
+
}): Promise<TDoc>;
|
|
1198
|
+
getDeleted(params?: {
|
|
1199
|
+
filters?: Record<string, unknown>;
|
|
1200
|
+
sort?: SortSpec | string;
|
|
1201
|
+
page?: number;
|
|
1202
|
+
limit?: number;
|
|
1203
|
+
}, options?: {
|
|
1204
|
+
select?: SelectSpec;
|
|
1205
|
+
populate?: PopulateSpec;
|
|
1206
|
+
lean?: boolean;
|
|
1207
|
+
session?: ClientSession;
|
|
1208
|
+
}): Promise<OffsetPaginationResult<TDoc>>;
|
|
1209
|
+
invalidateCache(id: string): Promise<void>;
|
|
1210
|
+
invalidateListCache(): Promise<void>;
|
|
1211
|
+
invalidateAllCache(): Promise<void>;
|
|
1212
|
+
getCacheStats(): CacheStats;
|
|
1213
|
+
resetCacheStats(): void;
|
|
1214
|
+
};
|
|
1215
|
+
/**
|
|
1216
|
+
* Helper type to add all plugin methods to a repository class
|
|
1217
|
+
* Cleaner than manually typing the intersection
|
|
1218
|
+
*
|
|
1219
|
+
* @example
|
|
1220
|
+
* ```typescript
|
|
1221
|
+
* import { Repository } from '@classytic/mongokit';
|
|
1222
|
+
* import type { WithPlugins } from '@classytic/mongokit';
|
|
1223
|
+
*
|
|
1224
|
+
* class OrderRepo extends Repository<IOrder> {
|
|
1225
|
+
* async getCustomerOrders(customerId: string) {
|
|
1226
|
+
* return this.getAll({ filters: { customerId } });
|
|
1227
|
+
* }
|
|
1228
|
+
* }
|
|
1229
|
+
*
|
|
1230
|
+
* const orderRepo = new OrderRepo(Model, [
|
|
1231
|
+
* ...allPlugins
|
|
1232
|
+
* ]) as WithPlugins<IOrder, OrderRepo>;
|
|
1233
|
+
*
|
|
1234
|
+
* // Works: custom methods + plugin methods
|
|
1235
|
+
* await orderRepo.getCustomerOrders('123');
|
|
1236
|
+
* await orderRepo.increment(orderId, 'total', 100);
|
|
1237
|
+
* ```
|
|
1238
|
+
*/
|
|
1239
|
+
type WithPlugins<TDoc, TRepo extends RepositoryInstance = RepositoryInstance> = TRepo & AllPluginMethods<TDoc>;
|
|
1240
|
+
//#endregion
|
|
1241
|
+
export { SchemaBuilderOptions as $, KeysetPaginationOptions as A, PaginationResult as B, GroupResult as C, InferRawDoc as D, InferDocument as E, ObjectId as F, PopulateSpec as G, Plugin as H, OffsetPaginationOptions as I, RepositoryEvent as J, ReadPreferenceType as K, OffsetPaginationResult as L, Logger as M, MinMaxResult as N, JsonSchema as O, NonNullableFields as P, RequiredBy as Q, OperationOptions as R, FieldRules as S, LookupOptions as St, HttpError as T, PluginFunction as U, PartialBy as V, PluginType as W, RepositoryOperation as X, RepositoryInstance as Y, RepositoryOptions as Z, DeleteResult as _, IController as _t, AnyModel as a, SortSpec as at, EventPhase as b, IResponseFormatter as bt, CacheOptions as c, UpdateManyResult as ct, CascadeRelation as d, UserContext as dt, SelectSpec as et, CreateInput as f, ValidationChainOptions as ft, DeepPartial as g, WithTransactionOptions as gt, DecodedCursor as h, WithPlugins as ht, AnyDocument as i, SortDirection as it, KeysetPaginationResult as j, KeysOfType as k, CacheStats as l, UpdateOptions as lt, CrudSchemas as m, ValidatorDefinition as mt, AggregatePaginationResult as n, SoftDeleteOptions as nt, CacheAdapter as o, Strict as ot, CreateOptions as p, ValidationResult as pt, RepositoryContext as q, AllPluginMethods as r, SoftDeleteRepository as rt, CacheOperationOptions as s, UpdateInput as st, AggregatePaginationOptions as t, SoftDeleteFilterMode as tt, CascadeOptions as u, UpdateWithValidationResult as ut, EventHandlers as v, IControllerResponse as vt, HookMode as w, FieldPreset as x, LookupBuilder as xt, EventPayload as y, IRequestContext as yt, PaginationConfig as z };
|