@classytic/mongokit 3.24.0 → 3.25.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. package/README.md +11 -2
  2. package/dist/{PaginationEngine-BnZ24ehC.d.mts → PaginationEngine-CBS1XGzp.d.mts} +2 -1
  3. package/dist/{Repository-ClOraFjT.mjs → Repository-KKR8jxfD.mjs} +70 -41
  4. package/dist/actions/index.d.mts +1 -1
  5. package/dist/adapter/index.d.mts +1 -1
  6. package/dist/adapter/index.mjs +1 -1
  7. package/dist/{adapter-BWgJ_Jdc.mjs → adapter-CLvUHiTW.mjs} +1 -1
  8. package/dist/ai/index.d.mts +1 -1
  9. package/dist/better-auth/index.mjs +2 -2
  10. package/dist/core-62oo3N1C.d.mts +50 -0
  11. package/dist/{create-repository-CVupgeRG.mjs → create-repository-DA5VJh-3.mjs} +2 -2
  12. package/dist/{create-repository-Cn1LkvL7.d.mts → create-repository-HjMD9h9_.d.mts} +18 -5
  13. package/dist/{id-resolution-P2OzcyRl.mjs → id-resolution-BqDrk0S2.mjs} +12 -1
  14. package/dist/{index-DLQ6pjal.d.mts → index-DceInCTX.d.mts} +3 -1
  15. package/dist/index.d.mts +53 -213
  16. package/dist/index.mjs +1071 -922
  17. package/dist/{mongooseToJsonSchema-CVnrKFDq.mjs → mongooseToJsonSchema-CByt1VhQ.mjs} +1 -1
  18. package/dist/{multi-tenant.plugin-BjXXuc_g.d.mts → multi-tenant.plugin-DVyVGREb.d.mts} +117 -18
  19. package/dist/operations-C8IcCHF9.d.mts +463 -0
  20. package/dist/pagination/PaginationEngine.d.mts +1 -1
  21. package/dist/pagination-jSUdrACK.d.mts +158 -0
  22. package/dist/plugin-options-vFufQZMZ.d.mts +125 -0
  23. package/dist/plugins/index.d.mts +4 -4
  24. package/dist/plugins/index.mjs +3 -3
  25. package/dist/query/primitives/coercion.mjs +1 -1
  26. package/dist/query/primitives/geo.d.mts +1 -1
  27. package/dist/repository-DCHRWRXw.d.mts +511 -0
  28. package/dist/testkit/index.d.mts +1 -1
  29. package/dist/testkit/index.mjs +1 -1
  30. package/dist/{timestamp.plugin-BhSX0Lu_.mjs → timestamp.plugin-B8252GuQ.mjs} +192 -46
  31. package/dist/{to-plain-g8-gsVxZ.d.mts → to-plain-DkF5px0R.d.mts} +12 -16
  32. package/dist/type-utils-CLzRFtIA.d.mts +163 -0
  33. package/dist/utils/index.d.mts +2 -2
  34. package/dist/utils/index.mjs +3 -3
  35. package/dist/{validation-chain.plugin-Dtt4tO7R.mjs → validation-chain.plugin-C3Vwp-Rk.mjs} +1 -1
  36. package/dist/{validation-chain.plugin-CFJbr964.d.mts → validation-chain.plugin-DxD15h-2.d.mts} +3 -1
  37. package/package.json +5 -3
  38. package/dist/types-BtMuhoX5.d.mts +0 -1830
@@ -1,1830 +0,0 @@
1
- import { DeleteManyResult, DeleteResult, LookupPopulateResult, RepositoryBase, UpdateManyResult } from "@classytic/repo-core/repository";
2
- import { Document, Model, PipelineStage, PopulateOptions, Types } from "mongoose";
3
- import { Filter } from "@classytic/repo-core/filter";
4
- import { ValidationResult } from "@classytic/repo-core/schema";
5
- import { AnyPaginationResult, OffsetPaginationResult } from "@classytic/repo-core/pagination";
6
- import { HttpError } from "@classytic/repo-core/errors";
7
- //#region src/query/LookupBuilder.d.ts
8
- interface LookupOptions {
9
- /** Collection to join with */
10
- from: string;
11
- /** Field from the input documents */
12
- localField: string;
13
- /** Field from the documents of the "from" collection */
14
- foreignField: string;
15
- /** Name of the new array field to add to the input documents */
16
- as?: string;
17
- /** Whether to unwrap array to single object */
18
- single?: boolean;
19
- /**
20
- * Field selection on the joined collection (shorthand for `$project`).
21
- * Three accepted shapes — all map to a single `$project` stage:
22
- * - `string` — CSV form (`'name,price'`); leading `-` marks an exclusion.
23
- * - `readonly string[]` — array form (`['name', 'price']` / `['-status']`);
24
- * mirrors `repo-core/lookup` `LookupSpec.select` for cross-kit parity.
25
- * - `Record<string, 0 | 1>` — explicit Mongo projection map.
26
- */
27
- select?: string | readonly string[] | Record<string, 0 | 1>;
28
- /** Additional pipeline to run on the joined collection */
29
- pipeline?: PipelineStage[];
30
- /** Optional let variables for pipeline */
31
- let?: Record<string, string>;
32
- /** Query filter to apply before join (legacy, for aggregate.ts compatibility) */
33
- query?: Record<string, unknown>;
34
- /** Query options (legacy, for aggregate.ts compatibility) */
35
- options?: {
36
- session?: unknown;
37
- };
38
- /** Sanitize pipeline stages (default: true). Set false only for trusted server-side pipelines */
39
- sanitize?: boolean;
40
- /**
41
- * Joined-side filter, compiled into a `$match` stage inside the lookup
42
- * pipeline AFTER the auto-generated join correlation. Mirrors
43
- * `repo-core/lookup/types.ts#LookupSpec.where` and sqlitekit's
44
- * predicate-on-the-JOIN behavior, so cross-kit code that narrows the
45
- * foreign side (`where: eq('status', 'active')`) gets identical rows
46
- * from both backends.
47
- *
48
- * Accepts Filter IR or a plain mongo query — `compileFilterToMongo`
49
- * routes each correctly.
50
- */
51
- where?: Filter | Record<string, unknown>;
52
- }
53
- /**
54
- * Fluent builder for MongoDB $lookup aggregation stage
55
- * Optimized for custom field joins at scale
56
- */
57
- declare class LookupBuilder {
58
- private options;
59
- constructor(from?: string);
60
- /**
61
- * Set the collection to join with
62
- */
63
- from(collection: string): this;
64
- /**
65
- * Set the local field (source collection)
66
- * IMPORTANT: This field should be indexed for optimal performance
67
- */
68
- localField(field: string): this;
69
- /**
70
- * Set the foreign field (target collection)
71
- * IMPORTANT: This field should be indexed (preferably unique) for optimal performance
72
- */
73
- foreignField(field: string): this;
74
- /**
75
- * Set the output field name
76
- * Defaults to the collection name if not specified
77
- */
78
- as(fieldName: string): this;
79
- /**
80
- * Mark this lookup as returning a single document
81
- * Automatically unwraps the array result to a single object or null
82
- */
83
- single(isSingle?: boolean): this;
84
- /**
85
- * Add a pipeline to filter/transform joined documents
86
- * Useful for filtering, sorting, or limiting joined results
87
- *
88
- * @example
89
- * ```typescript
90
- * lookup.pipeline([
91
- * { $match: { status: 'active' } },
92
- * { $sort: { priority: -1 } },
93
- * { $limit: 5 }
94
- * ]);
95
- * ```
96
- */
97
- pipeline(stages: PipelineStage[]): this;
98
- /**
99
- * Set let variables for use in pipeline
100
- * Allows referencing local document fields in the pipeline
101
- */
102
- let(variables: Record<string, string>): this;
103
- /**
104
- * Control pipeline sanitization (default: true)
105
- * Set to false for auto-generated pipelines that are known safe
106
- */
107
- sanitize(enabled: boolean): this;
108
- /**
109
- * Build the $lookup aggregation stage(s)
110
- * Returns an array of pipeline stages including $lookup and optional $unwind
111
- *
112
- * IMPORTANT: MongoDB $lookup has two mutually exclusive forms:
113
- * 1. Simple form: { from, localField, foreignField, as }
114
- * 2. Pipeline form: { from, let, pipeline, as }
115
- *
116
- * When pipeline or let is specified, we use the pipeline form.
117
- * Otherwise, we use the simpler localField/foreignField form.
118
- */
119
- build(): PipelineStage[];
120
- /**
121
- * Build and return only the $lookup stage (without $unwind)
122
- * Useful when you want to handle unwrapping yourself
123
- */
124
- buildLookupOnly(): PipelineStage.Lookup;
125
- /**
126
- * Static helper: Create a simple lookup in one line
127
- */
128
- static simple(from: string, localField: string, foreignField: string, options?: {
129
- as?: string;
130
- single?: boolean;
131
- }): PipelineStage[];
132
- /**
133
- * Static helper: Create multiple lookups at once
134
- *
135
- * @example
136
- * ```typescript
137
- * const pipeline = LookupBuilder.multiple([
138
- * { from: 'departments', localField: 'deptSlug', foreignField: 'slug', single: true },
139
- * { from: 'managers', localField: 'managerId', foreignField: '_id', single: true }
140
- * ]);
141
- * ```
142
- */
143
- static multiple(lookups: LookupOptions[]): PipelineStage[];
144
- /**
145
- * Static helper: Create a nested lookup (lookup within lookup)
146
- * Useful for multi-level joins like Order -> Product -> Category
147
- *
148
- * @example
149
- * ```typescript
150
- * // Join orders with products, then products with categories
151
- * const pipeline = LookupBuilder.nested([
152
- * { from: 'products', localField: 'productSku', foreignField: 'sku', as: 'product', single: true },
153
- * { from: 'categories', localField: 'product.categorySlug', foreignField: 'slug', as: 'product.category', single: true }
154
- * ]);
155
- * ```
156
- */
157
- static nested(lookups: LookupOptions[]): PipelineStage[];
158
- /**
159
- * Sanitize pipeline stages by blocking dangerous stages and operators.
160
- * Used internally by build() and available for external use (e.g., aggregate.ts).
161
- */
162
- static sanitizePipeline(stages: PipelineStage[]): PipelineStage[];
163
- /**
164
- * Recursively remove dangerous operators from an expression object.
165
- */
166
- private static _sanitizeDeep;
167
- }
168
- /**
169
- * Performance Guidelines for $lookup at Scale:
170
- *
171
- * 1. **Index Requirements** (Critical for millions of records):
172
- * - localField should be indexed on source collection
173
- * - foreignField should be indexed on target collection (unique index preferred)
174
- *
175
- * Example:
176
- * ```typescript
177
- * // Employee collection
178
- * employeeSchema.index({ departmentSlug: 1 });
179
- *
180
- * // Department collection
181
- * departmentSchema.index({ slug: 1 }, { unique: true });
182
- * ```
183
- *
184
- * 2. **Query Performance**:
185
- * - With proper indexes: O(log n) per lookup
186
- * - Without indexes: O(n * m) - AVOID THIS!
187
- * - Use explain() to verify index usage: IXSCAN (good) vs COLLSCAN (bad)
188
- *
189
- * 3. **Pipeline Optimization**:
190
- * - Place $match stages as early as possible
191
- * - Use $project to reduce field size before lookups
192
- * - Limit joined results with pipeline: [{ $match: {...} }, { $limit: n }]
193
- *
194
- * 4. **Memory Considerations**:
195
- * - Each lookup creates a new field in memory
196
- * - Use $project after lookup to remove unnecessary fields
197
- * - Consider allowDiskUse: true for very large datasets
198
- *
199
- * 5. **Alternative Approaches**:
200
- * - For 1:1 relationships with high read frequency: Consider storing ObjectId + slug
201
- * - For read-heavy workloads: Consider caching or materialized views
202
- * - For real-time dashboards: Consider separate aggregation collections
203
- */
204
- //#endregion
205
- //#region src/plugins/names.d.ts
206
- /**
207
- * Canonical identifiers exposed by mongokit's bundled plugins.
208
- *
209
- * Use these values with `RepositoryOptions.requirePlugins` and
210
- * `OperationOptions.skipPlugins` instead of repeating string literals.
211
- * The values intentionally preserve the plugins' existing public names.
212
- */
213
- declare const MONGOKIT_PLUGIN_NAMES: {
214
- readonly aggregateHelpers: 'aggregate-helpers';
215
- readonly appendOnly: 'appendOnly';
216
- readonly auditLog: 'auditLog';
217
- readonly auditTrail: 'auditTrail';
218
- readonly batchOperations: 'batch-operations';
219
- readonly cache: 'cache';
220
- readonly cascade: 'cascade';
221
- readonly changeLog: 'changeLog';
222
- readonly customId: 'custom-id';
223
- readonly elasticSearch: 'elastic-search';
224
- readonly fieldFilter: 'fieldFilter';
225
- readonly immutableStates: 'immutableStates';
226
- readonly lease: 'lease';
227
- readonly methodRegistry: 'method-registry';
228
- readonly mongoOperations: 'mongo-operations';
229
- readonly multiTenant: 'multi-tenant';
230
- readonly observability: 'observability';
231
- readonly softDelete: 'softDelete';
232
- readonly subdocument: 'subdocument';
233
- readonly timestamp: 'timestamp';
234
- readonly validationChain: 'validation-chain';
235
- };
236
- type MongoKitPluginName = (typeof MONGOKIT_PLUGIN_NAMES)[keyof typeof MONGOKIT_PLUGIN_NAMES];
237
- /** Bundled-name autocomplete while remaining open to third-party plugins. */
238
- type RepositoryPluginName = MongoKitPluginName | (string & Record<never, never>);
239
- //#endregion
240
- //#region src/types/controller.types.d.ts
241
- /**
242
- * Framework-agnostic request context
243
- *
244
- * Extract this from your framework's request object:
245
- * - Express: { query: req.query, body: req.body, params: req.params, user: req.user }
246
- * - Fastify: { query: request.query, body: request.body, params: request.params, user: request.user }
247
- * - Next.js: { query: searchParams, body: await request.json(), params: params, user: await getUser() }
248
- *
249
- * @example
250
- * ```typescript
251
- * // Express
252
- * const context: IRequestContext = {
253
- * query: req.query,
254
- * body: req.body,
255
- * params: req.params,
256
- * user: req.user,
257
- * context: { organizationId: req.headers['x-org-id'] }
258
- * };
259
- *
260
- * // Fastify
261
- * const context: IRequestContext = {
262
- * query: request.query,
263
- * body: request.body,
264
- * params: request.params,
265
- * user: request.user,
266
- * };
267
- *
268
- * // Next.js App Router
269
- * const context: IRequestContext = {
270
- * query: Object.fromEntries(request.nextUrl.searchParams),
271
- * body: await request.json(),
272
- * params: params,
273
- * user: await auth(),
274
- * };
275
- * ```
276
- */
277
- interface IRequestContext {
278
- /** Query parameters (from URL query string) */
279
- query: Record<string, unknown>;
280
- /** Request body (parsed JSON) */
281
- body: Record<string, unknown>;
282
- /** Route parameters (dynamic segments in URL) */
283
- params: Record<string, string>;
284
- /**
285
- * Authenticated user (from your auth middleware)
286
- * Shape depends on your auth system
287
- */
288
- user?: {
289
- id: string;
290
- role?: string;
291
- [key: string]: unknown;
292
- };
293
- /**
294
- * Custom context (tenant ID, organization, locale, etc.)
295
- * Use this for multi-tenant apps, feature flags, etc.
296
- */
297
- context?: Record<string, unknown>;
298
- }
299
- /**
300
- * Framework-agnostic controller response
301
- *
302
- * Your controller methods should return this shape.
303
- * Adapt it to your framework's response format in the handler layer.
304
- *
305
- * @example
306
- * ```typescript
307
- * // In controller method
308
- * async list(context: IRequestContext): Promise<IControllerResponse> {
309
- * const result = await this.repository.getAll(context.query);
310
- * return {
311
- * success: true,
312
- * data: result,
313
- * status: 200,
314
- * };
315
- * }
316
- *
317
- * // In Express handler
318
- * const response = await controller.list(context);
319
- * res.status(response.status).json(response);
320
- *
321
- * // In Fastify handler
322
- * const response = await controller.list(context);
323
- * return reply.code(response.status).send(response);
324
- *
325
- * // In Next.js handler
326
- * const response = await controller.list(context);
327
- * return NextResponse.json(response.data, { status: response.status });
328
- * ```
329
- */
330
- interface IControllerResponse<T = unknown> {
331
- /** Whether the operation succeeded */
332
- success: boolean;
333
- /** Response data (undefined on error) */
334
- data?: T;
335
- /** Error message (only if success = false) */
336
- error?: string;
337
- /** Additional error details (validation errors, stack trace, etc.) */
338
- details?: unknown;
339
- /** HTTP status code */
340
- status: number;
341
- /** Optional metadata (pagination info, warnings, etc.) */
342
- meta?: Record<string, unknown>;
343
- }
344
- /**
345
- * Framework-agnostic CRUD controller interface
346
- *
347
- * Implement this interface in your controller classes.
348
- * Each method receives a framework-agnostic context and returns a framework-agnostic response.
349
- *
350
- * @template TDoc - The Mongoose document type
351
- *
352
- * @example
353
- * ```typescript
354
- * import { IController, IRequestContext, IControllerResponse } from '@mongokit/core';
355
- *
356
- * class UserController implements IController<IUser> {
357
- * async list(context: IRequestContext): Promise<IControllerResponse> {
358
- * // Implementation
359
- * }
360
- *
361
- * async get(context: IRequestContext): Promise<IControllerResponse> {
362
- * // Implementation
363
- * }
364
- *
365
- * // ... other methods
366
- * }
367
- * ```
368
- */
369
- interface IController<TDoc> {
370
- /**
371
- * List resources with filtering, pagination, sorting, lookups
372
- *
373
- * @param context - Framework-agnostic request context
374
- * @returns Promise resolving to controller response with paginated data
375
- *
376
- * @example
377
- * ```typescript
378
- * // URL: GET /users?status=active&sort=-createdAt&limit=20
379
- * const response = await controller.list({
380
- * query: { status: 'active', sort: '-createdAt', limit: '20' },
381
- * body: {},
382
- * params: {},
383
- * });
384
- *
385
- * // Response:
386
- * {
387
- * success: true,
388
- * data: {
389
- * method: 'offset',
390
- * data: [...],
391
- * total: 100,
392
- * page: 1,
393
- * pages: 5
394
- * },
395
- * status: 200
396
- * }
397
- * ```
398
- */
399
- list(context: IRequestContext): Promise<IControllerResponse<AnyPaginationResult<TDoc>>>;
400
- /**
401
- * Get single resource by ID
402
- *
403
- * @param context - Framework-agnostic request context (id in params)
404
- * @returns Promise resolving to controller response with single document
405
- *
406
- * @example
407
- * ```typescript
408
- * // URL: GET /users/507f1f77bcf86cd799439011
409
- * const response = await controller.get({
410
- * query: {},
411
- * body: {},
412
- * params: { id: '507f1f77bcf86cd799439011' },
413
- * });
414
- *
415
- * // Success response:
416
- * {
417
- * success: true,
418
- * data: { _id: '...', name: 'John', ... },
419
- * status: 200
420
- * }
421
- *
422
- * // Not found response:
423
- * {
424
- * success: false,
425
- * error: 'Resource not found',
426
- * status: 404
427
- * }
428
- * ```
429
- */
430
- get(context: IRequestContext): Promise<IControllerResponse<TDoc>>;
431
- /**
432
- * Create new resource
433
- *
434
- * @param context - Framework-agnostic request context (data in body)
435
- * @returns Promise resolving to controller response with created document
436
- *
437
- * @example
438
- * ```typescript
439
- * // URL: POST /users
440
- * // Body: { name: 'John', email: 'john@example.com' }
441
- * const response = await controller.create({
442
- * query: {},
443
- * body: { name: 'John', email: 'john@example.com' },
444
- * params: {},
445
- * });
446
- *
447
- * // Response:
448
- * {
449
- * success: true,
450
- * data: { _id: '...', name: 'John', email: '...', createdAt: '...' },
451
- * status: 201
452
- * }
453
- * ```
454
- */
455
- create(context: IRequestContext): Promise<IControllerResponse<TDoc>>;
456
- /**
457
- * Update existing resource
458
- *
459
- * @param context - Framework-agnostic request context (id in params, updates in body)
460
- * @returns Promise resolving to controller response with updated document
461
- *
462
- * @example
463
- * ```typescript
464
- * // URL: PATCH /users/507f1f77bcf86cd799439011
465
- * // Body: { name: 'Jane' }
466
- * const response = await controller.update({
467
- * query: {},
468
- * body: { name: 'Jane' },
469
- * params: { id: '507f1f77bcf86cd799439011' },
470
- * });
471
- *
472
- * // Response:
473
- * {
474
- * success: true,
475
- * data: { _id: '...', name: 'Jane', ... },
476
- * status: 200
477
- * }
478
- * ```
479
- */
480
- update(context: IRequestContext): Promise<IControllerResponse<TDoc>>;
481
- /**
482
- * Delete resource
483
- *
484
- * @param context - Framework-agnostic request context (id in params)
485
- * @returns Promise resolving to controller response with deletion result
486
- *
487
- * @example
488
- * ```typescript
489
- * // URL: DELETE /users/507f1f77bcf86cd799439011
490
- * const response = await controller.delete({
491
- * query: {},
492
- * body: {},
493
- * params: { id: '507f1f77bcf86cd799439011' },
494
- * });
495
- *
496
- * // Response:
497
- * {
498
- * success: true,
499
- * data: { message: 'Resource deleted successfully' },
500
- * status: 200
501
- * }
502
- * ```
503
- */
504
- delete(context: IRequestContext): Promise<IControllerResponse<{
505
- message: string;
506
- }>>;
507
- }
508
- /**
509
- * Optional: Response formatter utilities
510
- *
511
- * Helper functions to create standardized controller responses.
512
- * Use these to maintain consistent response shapes across your API.
513
- *
514
- * @example
515
- * ```typescript
516
- * class BaseController {
517
- * protected success<T>(data: T, status = 200): IControllerResponse<T> {
518
- * return { success: true, data, status };
519
- * }
520
- *
521
- * protected error(message: string, status = 500, details?: unknown): IControllerResponse {
522
- * return { success: false, error: message, status, details };
523
- * }
524
- *
525
- * protected paginated<T>(result: PaginationResult<T>): IControllerResponse<PaginationResult<T>> {
526
- * return { success: true, data: result, status: 200 };
527
- * }
528
- * }
529
- * ```
530
- */
531
- interface IResponseFormatter {
532
- /**
533
- * Format successful response
534
- * @param data - Response data
535
- * @param status - HTTP status code (default: 200)
536
- */
537
- success<T>(data: T, status?: number): IControllerResponse<T>;
538
- /**
539
- * Format error response
540
- * @param message - Error message
541
- * @param status - HTTP status code (default: 500)
542
- * @param details - Additional error details
543
- */
544
- error(message: string, status?: number, details?: unknown): IControllerResponse;
545
- /**
546
- * Format paginated response
547
- * @param result - Pagination result from Repository.getAll()
548
- */
549
- paginated<T>(result: AnyPaginationResult<T>): IControllerResponse<AnyPaginationResult<T>>;
550
- }
551
- //#endregion
552
- //#region src/types.d.ts
553
- /**
554
- * MongoKit Type Definitions
555
- *
556
- * Production-grade types for MongoDB repository pattern with TypeScript
557
- *
558
- * @module @classytic/mongokit
559
- */
560
- /**
561
- * Global declaration merge: adds `softRequired` to Mongoose's SchemaTypeOptions
562
- * so consumers can annotate paths type-safely without importing anything.
563
- *
564
- * A path with `softRequired: true` keeps its DB-level `required: true`
565
- * invariant but is excluded from the auto-generated CRUD body `required[]`
566
- * array (see buildCrudSchemasFromModel).
567
- */
568
- declare module 'mongoose' {
569
- interface SchemaTypeOptions<T, EnforcedDocType = any, THydratedDocumentType = HydratedDocument<EnforcedDocType>> {
570
- /** mongokit: omit from auto-generated CRUD body `required` array. */
571
- softRequired?: boolean;
572
- }
573
- }
574
- /** Read Preference Type for replica sets */
575
- type ReadPreferenceType = 'primary' | 'primaryPreferred' | 'secondary' | 'secondaryPreferred' | 'nearest' | (string & {});
576
- /** Re-export mongoose ObjectId */
577
- type ObjectId = Types.ObjectId;
578
- /** Generic document type */
579
- type AnyDocument = Document & Record<string, unknown>;
580
- /** Generic model type */
581
- type AnyModel = Model<AnyDocument>;
582
- /** Sort direction */
583
- type SortDirection = 1 | -1;
584
- /** Sort specification for MongoDB queries */
585
- type SortSpec = Record<string, SortDirection>;
586
- /** Populate specification */
587
- type PopulateSpec = string | string[] | PopulateOptions | PopulateOptions[];
588
- /**
589
- * Select specification. `readonly string[]` widens the legacy `string[]`
590
- * form to also accept the immutable variant repo-core's contract uses
591
- * (`LookupPopulateOptions.select: readonly string[] | Record<string, 0
592
- * | 1>`), so `Repository<TDoc>` assigns to `StandardRepo<TDoc>` without
593
- * TS2322 on the `select` field. Any existing mutable-array caller stays
594
- * compatible — `string[]` is assignable to `readonly string[]`.
595
- */
596
- type SelectSpec = string | readonly string[] | Record<string, 0 | 1>;
597
- /** Filter query type for MongoDB queries (compatible with Mongoose 8 & 9) */
598
- type FilterQuery<_T = unknown> = Record<string, unknown>;
599
- /**
600
- * Infer document type from a Mongoose Model
601
- * @example
602
- * type UserDoc = InferDocument<typeof UserModel>;
603
- */
604
- type InferDocument<TModel> = TModel extends Model<infer TDoc> ? TDoc : never;
605
- /**
606
- * Infer raw document shape (without Mongoose Document methods)
607
- * @example
608
- * type User = InferRawDoc<typeof UserModel>;
609
- */
610
- type InferRawDoc<TModel> = TModel extends Model<infer TDoc> ? TDoc extends Document ? Omit<TDoc, keyof Document> : TDoc : never;
611
- /**
612
- * Make specific fields optional
613
- * @example
614
- * type CreateUser = PartialBy<User, 'createdAt' | 'updatedAt'>;
615
- */
616
- type PartialBy<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>;
617
- /**
618
- * Make specific fields required
619
- * @example
620
- * type UserWithId = RequiredBy<User, '_id'>;
621
- */
622
- type RequiredBy<T, K extends keyof T> = Omit<T, K> & Required<Pick<T, K>>;
623
- /**
624
- * Extract keys of type T that have values of type V
625
- * @example
626
- * type StringFields = KeysOfType<User, string>; // 'name' | 'email'
627
- */
628
- type KeysOfType<T, V> = { [K in keyof T]: T[K] extends V ? K : never; }[keyof T];
629
- /**
630
- * Deep partial - makes all nested properties optional
631
- */
632
- type DeepPartial<T> = T extends object ? { [P in keyof T]?: DeepPartial<T[P]>; } : T;
633
- /**
634
- * Strict object type - prevents excess properties
635
- * Use with `satisfies` for compile-time validation
636
- */
637
- type Strict<T> = T & { [K in Exclude<string, keyof T>]?: never; };
638
- /**
639
- * NonNullable fields extractor
640
- */
641
- type NonNullableFields<T> = { [K in keyof T]: NonNullable<T[K]>; };
642
- /**
643
- * Create/Update input types from document
644
- */
645
- type CreateInput<TDoc> = Omit<TDoc, '_id' | 'createdAt' | 'updatedAt' | '__v'>;
646
- /**
647
- * Partial-document update patch — the shape `repo.update(id, data)` accepts
648
- * for its `data` parameter. A `Partial<TDoc>` minus the fields a caller must
649
- * never set directly (`_id`, `createdAt`, `__v`).
650
- *
651
- * **Renamed from `UpdateInput<TDoc>` in 3.11.0** to eliminate a name
652
- * collision with `@classytic/repo-core/update`'s `UpdateInput` (the
653
- * `UpdateSpec | Record | Record[]` union consumed by `updateMany` /
654
- * `findOneAndUpdate`). The legacy name remains as a deprecated alias for
655
- * one release — remove in 3.12.
656
- */
657
- type UpdatePatch<TDoc> = Partial<Omit<TDoc, '_id' | 'createdAt' | '__v'>>;
658
- /**
659
- * Typed Mongo update document — the operator-shape that `findOneAndUpdate`
660
- * / `updateMany` accept on the wire (`{ $set, $inc, $unset, ... }`).
661
- *
662
- * The explicit operator keys give IDE autocomplete + per-operator value
663
- * typing; the trailing `[op: string]: unknown` index signature lets a
664
- * caller-built `MongoOperatorUpdate` value assign to `Record<string,
665
- * unknown>` without a cast — matching repo-core's `UpdateInput` slot
666
- * (`UpdateSpec | Record<string, unknown> | Record<string, unknown>[]`)
667
- * and unblocking the historic
668
- *
669
- * `normalizeUpdate(update) as unknown as Record<string, unknown>`
670
- *
671
- * footgun every domain package eventually wrote.
672
- *
673
- * Trade-off: with the index signature, an operator typo (`$st` instead
674
- * of `$set`) won't fail TypeScript — Mongo would silently drop it. The
675
- * common operators are listed explicitly so `obj.$set` / `obj.$inc`
676
- * autocomplete stays useful; the escape hatch covers everything else
677
- * (`$min`, `$max`, `$rename`, `$pop`, etc.).
678
- *
679
- * @example
680
- * ```ts
681
- * import type { MongoOperatorUpdate } from '@classytic/mongokit';
682
- *
683
- * const patch: MongoOperatorUpdate = {
684
- * $set: { status: 'paid', paidAt: new Date() },
685
- * $inc: { revision: 1 },
686
- * };
687
- * await repo.findOneAndUpdate({ _id: id }, patch); // no cast
688
- * ```
689
- */
690
- interface MongoOperatorUpdate {
691
- /** Assign top-level or dotted-path values. */
692
- $set?: Record<string, unknown>;
693
- /** Set only on insert (upsert path). */
694
- $setOnInsert?: Record<string, unknown>;
695
- /** Remove fields (`Mongo` accepts `''`, `1`, or `true`). */
696
- $unset?: Record<string, '' | 1 | true>;
697
- /** Atomic numeric increment / decrement. */
698
- $inc?: Record<string, number>;
699
- /** Atomic numeric multiplication. */
700
- $mul?: Record<string, number>;
701
- /** Push to array (or `{ $each, $position, $slice, $sort }`). */
702
- $push?: Record<string, unknown>;
703
- /** Pull matching values from array. */
704
- $pull?: Record<string, unknown>;
705
- /** Pull a list of literals from array. */
706
- $pullAll?: Record<string, unknown[]>;
707
- /** Push only when not already present. */
708
- $addToSet?: Record<string, unknown>;
709
- /** Remove first / last element (`-1` / `1`). */
710
- $pop?: Record<string, -1 | 1>;
711
- /** Set only when the new value is smaller / larger. */
712
- $min?: Record<string, unknown>;
713
- $max?: Record<string, unknown>;
714
- /** Rename top-level fields. */
715
- $rename?: Record<string, string>;
716
- /** Auto-set Date / Timestamp on this update. */
717
- $currentDate?: Record<string, true | {
718
- $type: 'date' | 'timestamp';
719
- }>;
720
- /** Bitwise ops on integer fields. */
721
- $bit?: Record<string, {
722
- and?: number;
723
- or?: number;
724
- xor?: number;
725
- }>;
726
- /**
727
- * Escape hatch for any operator not explicitly listed above (e.g.
728
- * positional-array operators like `$[<identifier>]` inside a path
729
- * key). Index signature is what makes the type assign to
730
- * `Record<string, unknown>` without a cast.
731
- */
732
- [op: string]: unknown;
733
- }
734
- /** Hook execution mode */
735
- type HookMode = 'sync' | 'async';
736
- /**
737
- * Minimal repo surface exposed to wrap-style middleware via the
738
- * `repo` field of `MiddlewareContext`. Intentionally narrow — middleware
739
- * shouldn't reach into the full `Repository` (that would couple every
740
- * middleware to the kit). The 5 floor methods plus `Model` cover every
741
- * legitimate middleware need (cross-doc reads, custom queries, type-
742
- * narrowing on `Model.modelName`).
743
- */
744
- interface MinimalRepoView<TDoc> {
745
- readonly Model: Model<TDoc>;
746
- readonly model: string;
747
- getById(id: string | ObjectId, options?: Record<string, unknown>): Promise<TDoc | null>;
748
- getAll?(params?: Record<string, unknown>, options?: Record<string, unknown>): Promise<unknown>;
749
- create(data: Record<string, unknown>, options?: Record<string, unknown>): Promise<TDoc>;
750
- update(id: string | ObjectId, data: Record<string, unknown>, options?: Record<string, unknown>): Promise<unknown>;
751
- delete(id: string | ObjectId, options?: Record<string, unknown>): Promise<unknown>;
752
- }
753
- /**
754
- * Context passed to a wrap-style middleware. Middleware mutates
755
- * `context` before calling `await next()` to alter input; inspects /
756
- * transforms the resolved value to alter output; uses plain try/catch
757
- * around `next()` for error handling.
758
- *
759
- * The `before:` / `after:` / `error:` hook engine still fires from
760
- * within `next()` — hooks remain authoritative for policy / cache /
761
- * audit so security and correctness plugins keep working unchanged.
762
- * Middleware composes ergonomically on top.
763
- */
764
- interface MiddlewareContext<TDoc> {
765
- /** Operation name (`'create'`, `'getById'`, ..., or any plugin op). */
766
- readonly operation: string;
767
- /** Live repository context — mutate fields before `next()` to alter input. */
768
- readonly context: RepositoryContext;
769
- /**
770
- * Continue the chain — the next middleware (or, for the innermost
771
- * middleware, the actual op + after/error hooks). The return value
772
- * is the resolved op result; throwing propagates up the chain.
773
- */
774
- next: () => Promise<unknown>;
775
- /** Narrow view of the repo for cross-doc reads / custom queries. */
776
- readonly repo: MinimalRepoView<TDoc>;
777
- }
778
- /**
779
- * Wrap-style middleware — Prisma `$extends.query` / Express middleware
780
- * shape. Registered via `repo.useMiddleware(mw)`. Composes around every
781
- * `_runOp` invocation; runs in registration order (first-registered
782
- * runs outermost).
783
- *
784
- * @example Time every operation
785
- * ```ts
786
- * repo.useMiddleware(async ({ operation, next }) => {
787
- * const start = performance.now();
788
- * try { return await next(); }
789
- * finally { console.log(`${operation} took ${performance.now() - start}ms`); }
790
- * });
791
- * ```
792
- *
793
- * @example Stamp tenant on every create
794
- * ```ts
795
- * repo.useMiddleware(async ({ operation, context, next }) => {
796
- * if (operation === 'create' && context.data) {
797
- * context.data.tenantId = currentTenant();
798
- * }
799
- * return next();
800
- * });
801
- * ```
802
- *
803
- * @example Short-circuit (skip the actual op) — return without calling next()
804
- * ```ts
805
- * repo.useMiddleware(async ({ operation, context, next }) => {
806
- * if (operation === 'getById' && readOnlyMode) {
807
- * return cachedReadOnlyResponse(context.id);
808
- * }
809
- * return next();
810
- * });
811
- * ```
812
- */
813
- type Middleware<TDoc = unknown> = (ctx: MiddlewareContext<TDoc>) => Promise<unknown>;
814
- /** Search strategy used by Repository.getAll when `search` is provided */
815
- type RepositorySearchMode = 'text' | 'regex' | 'auto';
816
- /** Repository options */
817
- interface RepositoryOptions {
818
- /** Whether repository event hooks are awaited */
819
- hooks?: HookMode;
820
- /** Custom ID field used by getById/update/delete (default: '_id').
821
- * Set to 'slug', 'code', 'chatId', etc. for non-ObjectId lookups. */
822
- idField?: string;
823
- /**
824
- * How Repository.getAll handles the `search` parameter.
825
- * - 'text' (default): uses MongoDB $text; requires a text index.
826
- * - 'regex': builds case-insensitive $or of $regex across `searchFields`. No index required.
827
- * - 'auto': uses 'text' if a text index exists, otherwise falls back to 'regex' when `searchFields` is set.
828
- *
829
- * Works standalone (Express/Nest/etc.) or alongside QueryParser — when the parser
830
- * already consumed `search` into filters, Repository does nothing extra.
831
- */
832
- searchMode?: RepositorySearchMode;
833
- /**
834
- * Fields used when searchMode is 'regex' (or 'auto' falling back to regex).
835
- * Required for regex mode to take effect.
836
- */
837
- searchFields?: string[];
838
- /**
839
- * How to react to a plugin composition that is known to be unsafe.
840
- * - 'warn' (default): log a warning via the configured logger.
841
- * - 'throw': fail fast at construction time — recommended for production.
842
- * - 'off': disable the check entirely.
843
- *
844
- * Checks currently enforced:
845
- * - soft-delete must precede batch-operations (otherwise deleteMany /
846
- * updateMany skip the soft-delete filter injection).
847
- * - multi-tenant must precede cache (otherwise cache keys are computed
848
- * before tenant scoping → cross-tenant cache poisoning risk).
849
- */
850
- pluginOrderChecks?: 'warn' | 'throw' | 'off';
851
- /**
852
- * When `true` (default `false`), the repository checks every read/
853
- * write op's filter against the schema's declared paths and warns
854
- * once per `(modelName, fieldName)` pair if a filter key isn't on
855
- * the schema AND `strictQuery: true` is in effect.
856
- *
857
- * **Why this exists.** With `mongoose.set('strictQuery', true)` (the
858
- * mongoose 6 default; some projects pin it explicitly), filter keys
859
- * not declared on the schema are SILENTLY STRIPPED before the query
860
- * runs. `findOne({ code: 'X' })` on a schema without `code` becomes
861
- * `findOne({})` — returns the first doc, not null — and the bug
862
- * masquerades as "wrong row returned" without any error or log.
863
- *
864
- * The diagnostic catches the trap at runtime: log once per offending
865
- * key per model, so a single bad call surfaces during dev/staging
866
- * instead of in production. Costs O(filter keys) per call against
867
- * a constructor-cached set; negligible.
868
- *
869
- * Hosts who route mongokit's `warn` into their own logger (via
870
- * `configureLogger({ warn: ... })`) can ship this enabled in
871
- * production without polluting stdout.
872
- *
873
- * @default false
874
- */
875
- warnOnStrictQueryStrip?: boolean;
876
- /**
877
- * Names of plugins that MUST be present in the plugin chain at
878
- * construction time. Throws a `TypeError` from the constructor if
879
- * any listed plugin is absent — eliminates the silent-misconfig
880
- * shape where a host forgets to wire `multiTenantPlugin` and ships
881
- * a tenant-leak to production.
882
- *
883
- * Names match each plugin's exported `name` property (the plugin's
884
- * own canonical identifier, NOT the JavaScript function name).
885
- * Bundled plugins as of 3.13:
886
- * - `'multi-tenant'` (from `multiTenantPlugin`)
887
- * - `'softDelete'` (from `softDeletePlugin`)
888
- * - `'auditLog'` (from `auditLogPlugin`)
889
- * - `'auditTrail'` (from `auditTrailPlugin`)
890
- * - `'cache'` (from `cachePlugin`)
891
- * - `'observability'` (from `observabilityPlugin`)
892
- * - `'method-registry'` (from `methodRegistryPlugin`)
893
- * - `'batch-operations'`, `'mongo-operations'`,
894
- * `'aggregate-helpers'`, `'subdocument'`, `'cascade'`,
895
- * `'custom-id'`, `'elastic-search'`, `'fieldFilter'`,
896
- * `'validation-chain'`, `'lease'`, `'timestamp'`
897
- *
898
- * Naming follows each plugin's `name` field — kebab-case for some,
899
- * camelCase for others. The error message lists "Installed plugins"
900
- * verbatim so a typo surfaces as a side-by-side diff.
901
- *
902
- * **Why this exists.** "Always wire `multiTenantPlugin`" and "always
903
- * wire `softDeletePlugin`" are documented as conventions in 7+ and
904
- * 9+ CLAUDE.md files across the classytic codebase respectively,
905
- * but documentation drifts and convention-by-comment isn't
906
- * enforceable. Listing the required plugins at the boot boundary
907
- * fails closed — the constructor throws with the missing name, the
908
- * bug surfaces on first run (or first test), not in production.
909
- *
910
- * @example
911
- * ```ts
912
- * import { MONGOKIT_PLUGIN_NAMES } from '@classytic/mongokit';
913
- *
914
- * new Repository(OrderModel, [
915
- * methodRegistryPlugin(),
916
- * multiTenantPlugin({ tenantField: 'organizationId' }),
917
- * softDeletePlugin(),
918
- * auditLogPlugin({ logger }),
919
- * ], paginationConfig, {
920
- * requirePlugins: [
921
- * MONGOKIT_PLUGIN_NAMES.multiTenant,
922
- * MONGOKIT_PLUGIN_NAMES.softDelete,
923
- * MONGOKIT_PLUGIN_NAMES.auditLog,
924
- * ],
925
- * });
926
- * ```
927
- *
928
- * If any name in this list isn't matched by a plugin's `name`
929
- * property in the chain, the constructor throws with the missing
930
- * names listed.
931
- */
932
- requirePlugins?: readonly RepositoryPluginName[];
933
- /**
934
- * Standard Schema validator (Zod 3.24+, Valibot 1+, ArkType 2+, ...)
935
- * for write payloads. Forwarded to `RepositoryBase`, which validates
936
- * `create` data and every `createMany` doc at
937
- * `HOOK_PRIORITY.VALIDATION` (150) — after policy plugins, before
938
- * cache/observability. Failures throw an `HttpError` 400 with
939
- * structured `validationErrors`. Validator output replaces the
940
- * payload, so schema-declared coercions/defaults flow into the write.
941
- */
942
- schema?: import('@classytic/repo-core/schema').StandardSchemaV1;
943
- /**
944
- * Standard Schema validator for `update` payloads. Separate slot
945
- * because updates are partial — derive one explicitly (e.g.
946
- * `schema.partial()` in Zod).
947
- */
948
- updateSchema?: import('@classytic/repo-core/schema').StandardSchemaV1;
949
- /**
950
- * Domain-event emission. Pass any arc / `@classytic/primitives`-
951
- * compatible transport and every mutating op publishes
952
- * `<resource>.<verb>` events (`user.created`, `user.updated`, ...).
953
- * Omit and the wiring is inert. See `@classytic/repo-core/events`.
954
- */
955
- events?: import('@classytic/repo-core/events').RepositoryEventsOptions;
956
- }
957
- /** Pagination configuration */
958
- interface PaginationConfig {
959
- /** Default number of documents per page (default: 10) */
960
- defaultLimit?: number;
961
- /** Maximum allowed limit (default: 100) */
962
- maxLimit?: number;
963
- /** Maximum allowed page number (default: 10000) */
964
- maxPage?: number;
965
- /** Page number that triggers performance warning (default: 100) */
966
- deepPageThreshold?: number;
967
- /** Cursor version for forward compatibility (default: 1) */
968
- cursorVersion?: number;
969
- /**
970
- * Minimum cursor version accepted. Bump alongside `cursorVersion` when a
971
- * breaking format change ships so stale client cursors are rejected with a
972
- * clear error instead of silently paginating from the wrong position.
973
- * Default: 1 (accept any cursor <= cursorVersion).
974
- */
975
- minCursorVersion?: number;
976
- /**
977
- * Allowlist of primary sort fields for keyset pagination. When set, any
978
- * `getAll({ sort })` whose primary (non-_id) field is not in this list
979
- * throws at validation time.
980
- *
981
- * Use this to lock down keyset pagination to fields that are structurally
982
- * guaranteed non-null in your schema — keyset sort across null/non-null
983
- * boundaries is lossy (MongoDB's `$lt/$gt` semantics leave a gap at the
984
- * type boundary, so not every doc is reachable).
985
- *
986
- * `_id` is always allowed regardless of this list.
987
- * Undefined (default) = no allowlist, any sort field accepted.
988
- */
989
- strictKeysetSortFields?: string[];
990
- /** Use estimatedDocumentCount for faster counts on large collections */
991
- useEstimatedCount?: boolean;
992
- }
993
- /** MongoDB collation options for locale-aware string comparison */
994
- interface CollationOptions {
995
- locale: string;
996
- caseLevel?: boolean;
997
- caseFirst?: 'upper' | 'lower' | 'off';
998
- strength?: 1 | 2 | 3 | 4 | 5;
999
- numericOrdering?: boolean;
1000
- alternate?: 'non-ignorable' | 'shifted';
1001
- maxVariable?: 'punct' | 'space';
1002
- backwards?: boolean;
1003
- }
1004
- /** Base pagination options */
1005
- interface BasePaginationOptions {
1006
- /** Pagination mode (explicit override) */
1007
- mode?: 'offset' | 'keyset';
1008
- /** MongoDB query filters */
1009
- filters?: FilterQuery<AnyDocument>;
1010
- /** Sort specification */
1011
- sort?: SortSpec;
1012
- /** Number of documents per page */
1013
- limit?: number;
1014
- /** Fields to select */
1015
- select?: SelectSpec;
1016
- /** Fields to populate */
1017
- populate?: PopulateSpec;
1018
- /** Return plain JavaScript objects */
1019
- lean?: boolean;
1020
- /** MongoDB session for transactions */
1021
- session?: unknown;
1022
- /** Query hint (index name or document) */
1023
- hint?: string | Record<string, 1 | -1>;
1024
- /** Maximum execution time in milliseconds */
1025
- maxTimeMS?: number;
1026
- /** Read preference for replica sets (e.g. 'secondaryPreferred') */
1027
- readPreference?: ReadPreferenceType;
1028
- /** Collation for locale-aware string comparison and case-insensitive sorting */
1029
- collation?: CollationOptions;
1030
- }
1031
- /** Offset pagination options */
1032
- interface OffsetPaginationOptions extends BasePaginationOptions {
1033
- /** Page number (1-indexed) */
1034
- page?: number;
1035
- /** Count strategy for filtered queries (default: 'exact') */
1036
- countStrategy?: 'exact' | 'estimated' | 'none';
1037
- /**
1038
- * Optional alternate filter used for the count query only. The primary
1039
- * `filters` is still used for `.find()`. Lets callers (Repository) run a
1040
- * sort-only query like `$near` for results while counting with a
1041
- * count-compatible rewrite (e.g. `$geoWithin: $centerSphere`) that
1042
- * returns the same document set but works with `countDocuments`.
1043
- */
1044
- countFilters?: FilterQuery<AnyDocument>;
1045
- }
1046
- /** Keyset (cursor) pagination options */
1047
- interface KeysetPaginationOptions extends BasePaginationOptions {
1048
- /** Cursor token for next page */
1049
- after?: string;
1050
- /** Sort is required for keyset pagination */
1051
- sort: SortSpec;
1052
- }
1053
- /** Aggregate pagination options */
1054
- interface AggregatePaginationOptions {
1055
- /** Aggregation pipeline stages */
1056
- pipeline?: PipelineStage[];
1057
- /** Page number (1-indexed) */
1058
- page?: number;
1059
- /** Number of documents per page */
1060
- limit?: number;
1061
- /** MongoDB session for transactions */
1062
- session?: unknown;
1063
- /** Query hint (index name or document) for aggregation */
1064
- hint?: string | Record<string, 1 | -1>;
1065
- /** Maximum execution time in milliseconds */
1066
- maxTimeMS?: number;
1067
- /** Count strategy (default: 'exact' via $facet).
1068
- * 'estimated' is accepted but treated as 'exact' in aggregation context
1069
- * since estimatedDocumentCount is not available inside pipelines. */
1070
- countStrategy?: 'exact' | 'estimated' | 'none';
1071
- /** Pagination mode (reserved for API consistency) */
1072
- mode?: 'offset';
1073
- /** Read preference for replica sets (e.g. 'secondaryPreferred') */
1074
- readPreference?: ReadPreferenceType;
1075
- }
1076
- /** Session-only options — shared base for lightweight operations */
1077
- interface SessionOptions {
1078
- /** MongoDB session for transactions */
1079
- session?: unknown;
1080
- /** Organization/tenant ID for multi-tenant plugin scoping */
1081
- organizationId?: string | ObjectId;
1082
- /**
1083
- * Per-call escape hatch — plugins listed here that opt into the
1084
- * mechanism (`auditLog`, `auditTrail`, `observability`) read this
1085
- * from `RepositoryContext` and skip their work for this call. Use
1086
- * on hot paths (e.g. heartbeats / counters) where the audit overhead
1087
- * dominates.
1088
- *
1089
- * Plugins that enforce correctness or security (multi-tenant scope,
1090
- * cache invalidation, soft-delete filter) deliberately ignore this
1091
- * flag — security/correctness must not be skippable per-call.
1092
- *
1093
- * **Match exactly by `plugin.name`** (camelCase, NOT kebab-case):
1094
- * - `'auditLog'` — see `auditLogPlugin()` in
1095
- * `src/plugins/audit-log.plugin.ts` (`name: 'auditLog'`)
1096
- * - `'auditTrail'` — see `auditTrailPlugin()` in
1097
- * `src/plugins/audit-trail.plugin.ts` (`name: 'auditTrail'`)
1098
- * - `'observability'` — see `observabilityPlugin()` in
1099
- * `src/plugins/observability.plugin.ts` (`name: 'observability'`)
1100
- *
1101
- * Pre-3.13.0 docs referenced `'audit-log'` / `'audit-trail'` —
1102
- * those don't match any plugin's `name` field, so passing them
1103
- * silently fails to skip anything. The strings above are the
1104
- * authoritative ones.
1105
- */
1106
- skipPlugins?: readonly string[];
1107
- /**
1108
- * Per-call escape hatch for `multiTenantPlugin`. When `true`, the
1109
- * tenant scope hook returns without injecting `organizationId` (or
1110
- * the configured `tenantField`) into the operation's policy slot
1111
- * for THIS call only. The plugin emits `after:tenant-bypass` with
1112
- * `{ context, operation, reason: 'option' }` so audit / observability
1113
- * plugins can distinguish bypassed queries from tenant-scoped ones.
1114
- *
1115
- * Reach for this when:
1116
- * - A super-admin / platform-admin user needs cross-tenant read
1117
- * for support tooling.
1118
- * - A migration script writes / backfills across tenants
1119
- * deliberately.
1120
- * - A scheduled job (TTL cleanup, cross-tenant aggregate, billing
1121
- * rollup) needs the unscoped view.
1122
- *
1123
- * **`bypassTenant` is the most-specific decision** — runs before
1124
- * `skipWhen` (plugin-level callback) and `resolveContext` (CLS
1125
- * fallback). Distinct from `skipWhen` which fires for every call
1126
- * and reads context to decide; `bypassTenant: true` is a deliberate
1127
- * per-call opt-out with no role check, so the calling code carries
1128
- * the responsibility.
1129
- *
1130
- * **NOT a substitute for proper access control.** This flag bypasses
1131
- * tenant scoping; it does NOT bypass authentication, RBAC, or any
1132
- * other host-level access check. Wrap it with your auth boundary at
1133
- * the controller / service layer.
1134
- *
1135
- * @example
1136
- * ```ts
1137
- * // Support engineer pulling a customer's order across tenants:
1138
- * await orderRepo.findAll({}, { bypassTenant: true });
1139
- *
1140
- * // Migration backfill across the whole estate:
1141
- * for await (const doc of repo.cursor({}, { bypassTenant: true })) { ... }
1142
- * ```
1143
- */
1144
- bypassTenant?: boolean;
1145
- /**
1146
- * Per-call escape hatch for `appendOnlyPlugin` — deliberate
1147
- * maintenance on an immutable-facts collection (backfill repair,
1148
- * migration). Audited via `after:append-only-bypass`; grep for this
1149
- * flag the same way you grep `bypassTenant` / `mode: 'hard'`. NOT a
1150
- * substitute for access control — gate it at the host layer.
1151
- */
1152
- bypassAppendOnly?: boolean;
1153
- /** Extensible — plugins can read custom fields from options */
1154
- [key: string]: unknown;
1155
- }
1156
- /** Read options — session + readPreference for read-only operations */
1157
- interface ReadOptions extends SessionOptions {
1158
- /** Read preference for replica sets (e.g. 'secondaryPreferred') */
1159
- readPreference?: ReadPreferenceType;
1160
- }
1161
- /** Repository operation options */
1162
- interface OperationOptions extends ReadOptions {
1163
- /** Fields to select */
1164
- select?: SelectSpec;
1165
- /** Fields to populate */
1166
- populate?: PopulateSpec;
1167
- /** Advanced populate options (from QueryParser or Arc's BaseController) */
1168
- populateOptions?: PopulateOptions[];
1169
- /** Return plain JavaScript objects */
1170
- lean?: boolean;
1171
- /** Throw error if document not found (default: true) */
1172
- throwOnNotFound?: boolean;
1173
- /** Override the ID field for this call (default: repo.idField or '_id').
1174
- * Use when the same repo sometimes queries by _id and sometimes by slug/code. */
1175
- idField?: string;
1176
- /** Additional query filters (e.g., for soft delete) */
1177
- query?: Record<string, unknown>;
1178
- }
1179
- /**
1180
- * Cache-aware operation options — extends OperationOptions with the
1181
- * unified cache plugin's per-call shape. Pass `cache: { staleTime, swr,
1182
- * tags, bypass, ... }` to override freshness for this call.
1183
- */
1184
- interface CacheableOptions extends OperationOptions {
1185
- /** Per-call cache override forwarded to the unified cache plugin. */
1186
- cache?: import('@classytic/repo-core/cache').CacheOptions;
1187
- }
1188
- /** withTransaction options */
1189
- interface WithTransactionOptions {
1190
- /** Allow non-transactional fallback when transactions are unsupported */
1191
- allowFallback?: boolean;
1192
- /** Optional hook to observe fallback triggers */
1193
- onFallback?: (error: Error) => void;
1194
- /** MongoDB transaction options (readConcern, writeConcern, readPreference, maxCommitTimeMS) */
1195
- transactionOptions?: import('mongoose').mongo.TransactionOptions;
1196
- }
1197
- /** Create operation options */
1198
- interface CreateOptions extends SessionOptions {
1199
- /** Keep insertion order on error (default: false).
1200
- * When false, all valid documents insert even if some fail (e.g. duplicates).
1201
- * Set to true to abort remaining inserts on first error. */
1202
- ordered?: boolean;
1203
- }
1204
- /** Update operation options */
1205
- interface UpdateOptions extends OperationOptions {
1206
- /** Enable update pipeline syntax */
1207
- updatePipeline?: boolean;
1208
- /** Array filters for positional operator $[<identifier>] updates */
1209
- arrayFilters?: Record<string, unknown>[];
1210
- }
1211
- /**
1212
- * Options for atomic findOneAndUpdate (compare-and-set primitive).
1213
- *
1214
- * Designed for outbox/lock/semaphore patterns that need a single round-trip
1215
- * match-and-mutate. Goes through the full hook pipeline (multi-tenant scope,
1216
- * soft-delete, audit) the same way `update()` does.
1217
- */
1218
- interface FindOneAndUpdateOptions extends OperationOptions {
1219
- /** Sort to disambiguate when filter matches multiple docs (e.g. FIFO claim). */
1220
- sort?: SortSpec;
1221
- /** Return doc state before or after the update. Default: 'after'. */
1222
- returnDocument?: 'before' | 'after';
1223
- /** Insert if no doc matches. Default: false. */
1224
- upsert?: boolean;
1225
- /** Array filters for positional `$[<identifier>]` updates. */
1226
- arrayFilters?: Record<string, unknown>[];
1227
- /** Allow aggregation-pipeline updates (array form). Default: false. */
1228
- updatePipeline?: boolean;
1229
- /** Run mongoose schema validators on the update. Default: true. */
1230
- runValidators?: boolean;
1231
- /** Collation for locale-aware string comparison. */
1232
- collation?: CollationOptions;
1233
- /** Maximum execution time in milliseconds. */
1234
- maxTimeMS?: number;
1235
- }
1236
- /** Aggregate operation options */
1237
- interface AggregateOptions extends ReadOptions {
1238
- /** Allow aggregation to use disk for large sorts/groups */
1239
- allowDiskUse?: boolean;
1240
- /** Comment for profiler/logs */
1241
- comment?: string;
1242
- /** Maximum execution time in milliseconds */
1243
- maxTimeMS?: number;
1244
- /** Read concern level */
1245
- readConcern?: {
1246
- level: string;
1247
- };
1248
- /** Collation for locale-aware string comparison */
1249
- collation?: CollationOptions;
1250
- /** Maximum allowed pipeline stages (governance) */
1251
- maxPipelineStages?: number;
1252
- }
1253
- /** Lookup populate options */
1254
- /**
1255
- * Mongokit's `lookupPopulate` options — kit-native superset of the
1256
- * portable `LookupPopulateOptions<TBase>` from repo-core. Widens three
1257
- * slots so arc / mongoose consumers compose cleanly:
1258
- *
1259
- * - `filters`: accepts Filter IR, a `Partial<TBase> & Record<string,
1260
- * unknown>` literal (same shape repo-core's contract uses), or a
1261
- * plain `Record<string, unknown>` for legacy callers.
1262
- * - `lookups`: accepts either the portable `LookupSpec[]` (works on
1263
- * every kit) or mongokit's `LookupOptions[]` (adds `pipeline` /
1264
- * `let` / `sanitize` for mongo-correlated joins).
1265
- * - `select`: accepts mongoose's space-separated string form
1266
- * (`'name email'`) in addition to the portable array / inclusion
1267
- * map. Kept for arc + existing controller convenience.
1268
- *
1269
- * `TBase` defaults to `unknown` so legacy non-generic usage still
1270
- * compiles; pass the doc type for arc `RepositoryLike<TDoc>` structural
1271
- * assignment — `LookupPopulateOptions<TDoc>` is bit-compatible with
1272
- * `import('@classytic/repo-core/repository').LookupPopulateOptions<TDoc>`.
1273
- */
1274
- interface LookupPopulateOptions<TBase = unknown> {
1275
- /**
1276
- * Base-table filter. Accepts Filter IR from repo-core, a typed
1277
- * `Partial<TBase> & Record<string, unknown>` literal (what arc
1278
- * controllers produce when typed against `StandardRepo<TDoc>`), or
1279
- * a plain `Record<string, unknown>` for ad-hoc callers.
1280
- */
1281
- filters?: import('@classytic/repo-core/filter').Filter | (Partial<TBase> & Record<string, unknown>) | Record<string, unknown>;
1282
- /**
1283
- * Portable `LookupSpec[]` (cross-kit) or mongokit-native
1284
- * `LookupOptions[]` (with `pipeline` / `let` / `sanitize`). The
1285
- * portable shape is a structural subset, so callers can mix.
1286
- */
1287
- lookups: readonly import('@classytic/repo-core/repository').LookupSpec[] | LookupOptions[];
1288
- /** Sort specification */
1289
- sort?: SortSpec | string;
1290
- /** Page number (offset mode, 1-indexed) */
1291
- page?: number;
1292
- /** Cursor token for next page (keyset mode) */
1293
- after?: string;
1294
- /** Number of documents per page */
1295
- limit?: number;
1296
- /** Fields to select */
1297
- select?: SelectSpec;
1298
- /** Collation for locale-aware string comparison */
1299
- collation?: CollationOptions;
1300
- /** Count strategy for offset pagination */
1301
- countStrategy?: 'exact' | 'estimated' | 'none';
1302
- /**
1303
- * Transaction session. Typed `unknown` to match repo-core's
1304
- * `RepositorySession` contract — mongokit narrows to `ClientSession`
1305
- * internally. Not inherited from `ReadOptions` because the repo-core
1306
- * contract type has no `[key: string]: unknown` escape hatch, and
1307
- * arc's `Partial<StandardRepo<TDoc>>` boundary rejects index-signature
1308
- * drift under strict function-type variance.
1309
- */
1310
- session?: unknown;
1311
- /** Read preference for replica sets (e.g. 'secondaryPreferred') */
1312
- readPreference?: ReadPreferenceType;
1313
- /** Multi-tenant plugin scoping (applied via before:lookupPopulate hook). */
1314
- organizationId?: string | ObjectId;
1315
- }
1316
- /**
1317
- * Union emitted by the validation-chain plugin. Keeps the violation shape
1318
- * from `ValidationResult` without re-exporting the type — consumers that
1319
- * need `ValidationResult`, `CrudSchemas`, `JsonSchema`, `SchemaBuilderOptions`,
1320
- * `FieldRule` or `FieldRules` import them straight from
1321
- * `@classytic/repo-core/schema`.
1322
- */
1323
- type UpdateWithValidationResult<T> = {
1324
- success: true;
1325
- data: T;
1326
- } | {
1327
- success: false;
1328
- error: {
1329
- code: number;
1330
- message: string;
1331
- violations?: ValidationResult['violations'];
1332
- };
1333
- };
1334
- /** User context for operations */
1335
- interface UserContext {
1336
- _id?: ObjectId | string;
1337
- id?: string;
1338
- roles?: string | string[];
1339
- [key: string]: unknown;
1340
- }
1341
- /** Repository operation context */
1342
- interface RepositoryContext {
1343
- /** Operation name */
1344
- operation: string;
1345
- /** Model name */
1346
- model: string;
1347
- /** Document data (for create/update) */
1348
- data?: Record<string, unknown>;
1349
- /** Array of documents (for createMany) */
1350
- dataArray?: Record<string, unknown>[];
1351
- /** Document ID (for update/delete/getById) */
1352
- id?: string | ObjectId;
1353
- /** Query filters */
1354
- query?: FilterQuery<AnyDocument>;
1355
- /** User making the request */
1356
- user?: UserContext;
1357
- /** Organization ID for multi-tenancy */
1358
- organizationId?: string | ObjectId;
1359
- /** Fields to select */
1360
- select?: SelectSpec;
1361
- /** Fields to populate */
1362
- populate?: PopulateSpec;
1363
- /** Return lean documents */
1364
- lean?: boolean;
1365
- /** MongoDB session */
1366
- session?: unknown;
1367
- /** Read preference for replica sets */
1368
- readPreference?: ReadPreferenceType;
1369
- /**
1370
- * Per-call escape hatch — plugin names listed here that opt into the
1371
- * mechanism (`auditLog`, `auditTrail`, `observability`) skip their
1372
- * work for this call. Threaded from `OperationOptions.skipPlugins`
1373
- * via `_buildContext`. Security/correctness plugins (multi-tenant
1374
- * scope, cache invalidation, soft-delete filter) deliberately ignore
1375
- * this — those must not be skippable per-call.
1376
- */
1377
- skipPlugins?: readonly string[];
1378
- /** Pagination filters */
1379
- filters?: Record<string, unknown>;
1380
- /** Sort specification */
1381
- sort?: SortSpec;
1382
- /** Page number (offset pagination) */
1383
- page?: number;
1384
- /** Items per page */
1385
- limit?: number;
1386
- /** Cursor for next page (keyset pagination) */
1387
- after?: string;
1388
- /** Search query string */
1389
- search?: string;
1390
- /** Pagination mode */
1391
- mode?: 'offset' | 'keyset';
1392
- /** Query hint */
1393
- hint?: string | Record<string, 1 | -1>;
1394
- /** Maximum execution time in milliseconds */
1395
- maxTimeMS?: number;
1396
- /** Count strategy for offset pagination */
1397
- countStrategy?: 'exact' | 'estimated' | 'none';
1398
- /** Whether this is a soft delete operation (set by softDeletePlugin) */
1399
- softDeleted?: boolean;
1400
- /** Include soft-deleted documents in queries */
1401
- includeDeleted?: boolean;
1402
- /**
1403
- * Caller-requested delete semantics, read by softDeletePlugin.
1404
- *
1405
- * - `undefined` (default) — plugin decides: soft if wired, hard otherwise.
1406
- * - `'hard'` — bypass soft-delete and physically remove the document.
1407
- * All policy hooks (multi-tenant scope, audit trails, cache invalidation)
1408
- * still fire; only the soft-delete interception is skipped.
1409
- * - `'soft'` — reserved. Currently treated as default since softDeletePlugin
1410
- * is the only source of soft semantics.
1411
- */
1412
- deleteMode?: 'hard' | 'soft';
1413
- /**
1414
- * Per-call cache options forwarded by the unified cache plugin
1415
- * (`@classytic/repo-core/cache`). Kits copy this from the caller's
1416
- * `options.cache` onto the context so the plugin's `before:<op>`
1417
- * hook can read it.
1418
- */
1419
- cache?: import('@classytic/repo-core/cache').CacheOptions;
1420
- /** Whether result was served from cache (internal — set by cache plugin) */
1421
- _cacheHit?: boolean;
1422
- /** Cached result (internal — set by cache plugin) */
1423
- _cachedResult?: unknown;
1424
- /** Resolved cache options stash (internal — set by cache plugin) */
1425
- _cacheKey?: string;
1426
- /** Resolved cache options stash (internal — set by cache plugin) */
1427
- _cacheResolved?: unknown;
1428
- /** Cache hit status (internal — `'fresh'` or `'stale'`) */
1429
- _cacheStatus?: 'fresh' | 'stale' | 'miss' | 'disabled' | 'bypass';
1430
- /** IDs to cascade delete (internal) */
1431
- _cascadeIds?: unknown[];
1432
- /** Custom context data from plugins */
1433
- [key: string]: unknown;
1434
- }
1435
- /**
1436
- * Plugin interface — generic over the repo type so kit-internal plugins
1437
- * can target the narrower `RepositoryInstance` while cross-package
1438
- * plugins from `@classytic/repo-core/cache` etc. that target the
1439
- * shared `RepositoryBase` flow through the same array.
1440
- *
1441
- * `RepositoryInstance` formally `extends RepositoryBase` (see below),
1442
- * so `Plugin<RepositoryBase>` is structurally assignable to
1443
- * `Plugin<RepositoryInstance>` via TypeScript's method-bivariance
1444
- * rule for interface methods. No host-site casts required.
1445
- */
1446
- interface Plugin<TRepo extends RepositoryBase = RepositoryInstance> {
1447
- /** Plugin name */
1448
- name: string;
1449
- /** Apply plugin to repository */
1450
- apply(repo: TRepo): void;
1451
- }
1452
- /** Plugin function signature. Same TRepo widening as `Plugin`. */
1453
- type PluginFunction<TRepo extends RepositoryBase = RepositoryInstance> = (repo: TRepo) => void;
1454
- /** Plugin type (object or function). */
1455
- type PluginType<TRepo extends RepositoryBase = RepositoryInstance> = Plugin<TRepo> | PluginFunction<TRepo>;
1456
- /** Hook with priority for deterministic phase ordering */
1457
- interface PrioritizedHook {
1458
- listener: (data: any) => void | Promise<void>;
1459
- priority: number;
1460
- }
1461
- /**
1462
- * Repository instance for plugin type reference.
1463
- *
1464
- * Extends `@classytic/repo-core/repository`'s `RepositoryBase` so the
1465
- * full hook + adapter contract is included. This keeps mongokit's
1466
- * `Plugin<RepositoryInstance>` and repo-core's `Plugin<RepositoryBase>`
1467
- * structurally aligned — cross-package plugins (`cachePlugin` from
1468
- * `@classytic/repo-core/cache` etc.) flow into mongokit's `plugins[]`
1469
- * array without per-call casts at the host site.
1470
- *
1471
- * The `[key: string]: unknown` index signature at the bottom keeps
1472
- * dynamic plugin-method registration (`registerMethod`) compatible.
1473
- */
1474
- interface RepositoryInstance extends RepositoryBase {
1475
- Model: Model<any>;
1476
- model: string;
1477
- _hooks: Map<string, PrioritizedHook[]>;
1478
- _pagination: unknown;
1479
- use(plugin: PluginType): this;
1480
- on(event: RepositoryEvent | (string & {}), listener: (data: any) => void | Promise<void>, options?: {
1481
- priority?: number;
1482
- }): this;
1483
- off(event: RepositoryEvent | (string & {}), listener: (data: any) => void | Promise<void>): this;
1484
- removeAllListeners(event?: string): this;
1485
- emit(event: string, data: unknown): void;
1486
- emitAsync(event: string, data: unknown): Promise<void>;
1487
- registerMethod?(name: string, fn: (...args: any[]) => any): void;
1488
- hasMethod?(name: string): boolean;
1489
- _buildContext(operation: string, options: Record<string, unknown> | object): Promise<RepositoryContext>;
1490
- _handleError(error: Error): HttpError;
1491
- update(id: string | ObjectId, data: Record<string, unknown>, options?: Record<string, unknown>): Promise<unknown>;
1492
- findOneAndUpdate(filter: Record<string, unknown>, update: Record<string, unknown> | Record<string, unknown>[], options?: Record<string, unknown>): Promise<unknown>;
1493
- aggregatePipeline(pipeline: PipelineStage[], options?: Record<string, unknown>): Promise<unknown[]>;
1494
- getByQuery(query: Record<string, unknown>, options?: Record<string, unknown>): Promise<unknown>;
1495
- _executeQuery<T>(buildQuery: (Model: Model<any>) => Promise<T>): Promise<T>;
1496
- [key: string]: unknown;
1497
- }
1498
- /** Repository operation names */
1499
- type RepositoryOperation = 'create' | 'createMany' | 'update' | 'updateMany' | 'findOneAndUpdate' | 'delete' | 'deleteMany' | 'restore' | 'getById' | 'getByQuery' | 'getOne' | 'getAll' | 'findAll' | 'getOrCreate' | 'count' | 'exists' | 'distinct' | 'aggregate' | 'aggregatePaginate' | 'aggregatePipeline' | 'aggregatePipelinePaginate' | 'lookupPopulate' | 'bulkWrite' | 'claim' | 'claimVersion' | 'cursor' | 'watch';
1500
- /** Event lifecycle phases */
1501
- type EventPhase = 'before' | 'after' | 'error';
1502
- /** Repository event names (generated from template literals) */
1503
- type RepositoryEvent = `${EventPhase}:${RepositoryOperation}` | 'method:registered' | 'error:hook';
1504
- /**
1505
- * Type-safe event handler map
1506
- *
1507
- * Hook signature contract:
1508
- * - `before:*` — receives `context: RepositoryContext` directly (not wrapped).
1509
- * Plugins mutate context in-place to inject filters, data, etc.
1510
- * - `after:*` — receives `{ context, result }` where result is the operation output.
1511
- * - `error:*` — receives `{ context, error }` where error is the caught Error.
1512
- */
1513
- type EventHandlers<TDoc = unknown> = { [K in RepositoryEvent]?: K extends `before:${string}` ? (context: RepositoryContext) => void | Promise<void> : K extends `after:${string}` ? (payload: {
1514
- context: RepositoryContext;
1515
- result: TDoc | TDoc[];
1516
- }) => void | Promise<void> : K extends `error:${string}` ? (payload: {
1517
- context: RepositoryContext;
1518
- error: Error;
1519
- }) => void | Promise<void> : (payload: {
1520
- context: RepositoryContext;
1521
- }) => void | Promise<void>; };
1522
- /** Event payload */
1523
- interface EventPayload {
1524
- context: RepositoryContext;
1525
- result?: unknown;
1526
- error?: Error;
1527
- }
1528
- /** Field preset configuration */
1529
- interface FieldPreset {
1530
- /** Fields visible to everyone */
1531
- public: string[];
1532
- /** Additional fields for authenticated users */
1533
- authenticated?: string[];
1534
- /** Additional fields for admins */
1535
- admin?: string[];
1536
- }
1537
- /** Value type identifier for cursor serialization */
1538
- type ValueType = 'date' | 'objectid' | 'boolean' | 'number' | 'string' | 'null' | 'unknown';
1539
- /** Cursor payload */
1540
- interface CursorPayload {
1541
- /** Primary sort field value (legacy single-field) */
1542
- v: string | number | boolean | null;
1543
- /** Value type identifier (legacy single-field) */
1544
- t: ValueType;
1545
- /** Document ID */
1546
- id: string;
1547
- /** ID type identifier */
1548
- idType: ValueType;
1549
- /** Sort specification */
1550
- sort: SortSpec;
1551
- /** Cursor version */
1552
- ver: number;
1553
- /** Compound sort field values (multi-field keyset) */
1554
- vals?: Record<string, string | number | boolean | null>;
1555
- /** Compound sort value types */
1556
- types?: Record<string, ValueType>;
1557
- }
1558
- /** Decoded cursor */
1559
- interface DecodedCursor {
1560
- /** Primary sort field value (rehydrated) — legacy compat */
1561
- value: unknown;
1562
- /** Document ID (rehydrated) */
1563
- id: ObjectId | string;
1564
- /** Sort specification */
1565
- sort: SortSpec;
1566
- /** Cursor version */
1567
- version: number;
1568
- /** All sort field values (rehydrated) — for compound sort */
1569
- values?: Record<string, unknown>;
1570
- }
1571
- /** Validator definition */
1572
- interface ValidatorDefinition {
1573
- /** Validator name */
1574
- name: string;
1575
- /** Operations to apply validator to */
1576
- operations?: Array<'create' | 'createMany' | 'update' | 'findOneAndUpdate' | 'delete'>;
1577
- /** Validation function */
1578
- validate: (context: RepositoryContext, repo?: RepositoryInstance) => void | Promise<void>;
1579
- }
1580
- /** Validation chain options */
1581
- interface ValidationChainOptions {
1582
- /** Stop on first validation error (default: true) */
1583
- stopOnFirstError?: boolean;
1584
- }
1585
- /** Logger interface for audit plugin */
1586
- interface Logger {
1587
- info?(message: string, meta?: Record<string, unknown>): void;
1588
- error?(message: string, meta?: Record<string, unknown>): void;
1589
- warn?(message: string, meta?: Record<string, unknown>): void;
1590
- debug?(message: string, meta?: Record<string, unknown>): void;
1591
- }
1592
- /** Filter mode for soft delete queries */
1593
- type SoftDeleteFilterMode = 'null' | 'exists';
1594
- /** Soft delete plugin options */
1595
- interface SoftDeleteOptions {
1596
- /** Field name for deletion timestamp (default: 'deletedAt') */
1597
- deletedField?: string;
1598
- /** Field name for deleting user (default: 'deletedBy') */
1599
- deletedByField?: string;
1600
- /** Enable soft delete (default: true) */
1601
- soft?: boolean;
1602
- /**
1603
- * Filter mode for excluding deleted documents (default: 'null')
1604
- * - 'null': Filters where deletedField is null (works with `default: null` in schema)
1605
- * - 'exists': Filters where deletedField does not exist (legacy behavior)
1606
- */
1607
- filterMode?: SoftDeleteFilterMode;
1608
- /** Add restore method to repository (default: true) */
1609
- addRestoreMethod?: boolean;
1610
- /** Add getDeleted method to repository (default: true) */
1611
- addGetDeletedMethod?: boolean;
1612
- /**
1613
- * TTL in days for auto-cleanup of deleted documents.
1614
- * When set, creates a TTL index on the deletedField.
1615
- * Documents will be automatically removed after the specified days.
1616
- */
1617
- ttlDays?: number;
1618
- }
1619
- /** Repository with soft delete methods */
1620
- interface SoftDeleteRepository<TDoc = unknown> {
1621
- /**
1622
- * Restore a soft-deleted document by setting deletedAt to null
1623
- * @param id - Document ID to restore
1624
- * @param options - Optional session for transactions
1625
- * @returns The restored document
1626
- */
1627
- restore(id: string | ObjectId, options?: {
1628
- session?: unknown;
1629
- }): Promise<TDoc>;
1630
- /**
1631
- * Get all soft-deleted documents
1632
- * @param params - Query parameters (filters, pagination, etc.)
1633
- * @param options - Query options (select, populate, etc.)
1634
- * @returns Paginated result of deleted documents
1635
- */
1636
- getDeleted(params?: {
1637
- filters?: Record<string, unknown>;
1638
- sort?: SortSpec | string;
1639
- page?: number;
1640
- limit?: number;
1641
- }, options?: {
1642
- select?: SelectSpec;
1643
- populate?: PopulateSpec;
1644
- lean?: boolean;
1645
- session?: unknown;
1646
- }): Promise<OffsetPaginationResult<TDoc>>;
1647
- }
1648
- /** Group result */
1649
- interface GroupResult {
1650
- _id: unknown;
1651
- count: number;
1652
- }
1653
- /** Min/Max result */
1654
- interface MinMaxResult {
1655
- min: unknown;
1656
- max: unknown;
1657
- }
1658
- /**
1659
- * Per-call cache options — TanStack-shaped (`staleTime`, `gcTime`, `swr`,
1660
- * `tags`, `bypass`, `enabled`, `key`). Threaded through read methods via
1661
- * `options.cache` (CRUD) or `aggRequest.cache` (aggregate).
1662
- */
1663
- interface CacheOperationOptions {
1664
- /** Per-call cache override forwarded to the unified cache plugin. */
1665
- cache?: import('@classytic/repo-core/cache').CacheOptions;
1666
- }
1667
- /** Cascade relation definition */
1668
- interface CascadeRelation {
1669
- /**
1670
- * Preferred: a Repository instance for the target collection. When present,
1671
- * cascade routes deletes through `repo.delete` / `repo.deleteMany`, so the
1672
- * target's `before:delete` / `before:deleteMany` hooks fire — meaning
1673
- * multi-tenant scoping, audit logging, cache invalidation, and the target's
1674
- * own soft-delete plugin (with its configured `deletedField`) all run
1675
- * correctly. Strongly recommended for any target that has policy plugins.
1676
- */
1677
- repo?: RepositoryInstance;
1678
- /**
1679
- * Legacy: a Mongoose model name. When set without `repo`, cascade writes
1680
- * directly via `RelatedModel.updateMany` / `RelatedModel.deleteMany`. This
1681
- * path bypasses the target's hooks — it will NOT enforce tenant scoping,
1682
- * fire audit events, or honor a custom `deletedField`. Only safe for
1683
- * trivial targets with no policy plugins. Prefer `repo` for new code.
1684
- */
1685
- model?: string;
1686
- /** Foreign key field in the related model that references the deleted document */
1687
- foreignKey: string;
1688
- /** Whether to use soft delete if available (default: follows parent behavior) */
1689
- softDelete?: boolean;
1690
- }
1691
- /** Cascade delete plugin options */
1692
- interface CascadeOptions {
1693
- /** Relations to cascade delete */
1694
- relations: CascadeRelation[];
1695
- /** Run cascade deletes in parallel (default: true) */
1696
- parallel?: boolean;
1697
- /** Logger for cascade operations */
1698
- logger?: Logger;
1699
- }
1700
- /**
1701
- * Combines all plugin method types into a single type
1702
- * Useful when you're using all plugins and want full type safety
1703
- *
1704
- * @example
1705
- * ```typescript
1706
- * import { Repository } from '@classytic/mongokit';
1707
- * import type { AllPluginMethods } from '@classytic/mongokit';
1708
- *
1709
- * class UserRepo extends Repository<IUser> {}
1710
- *
1711
- * const repo = new UserRepo(Model, [...allPlugins]) as UserRepo & AllPluginMethods<IUser>;
1712
- *
1713
- * // TypeScript knows about all plugin methods!
1714
- * await repo.increment(id, 'views', 1);
1715
- * await repo.restore(id);
1716
- * await repo.cache?.invalidateByTags(['org:abc']);
1717
- * ```
1718
- *
1719
- * Note: Import the individual plugin method types if you need them:
1720
- * ```typescript
1721
- * import type {
1722
- * MongoOperationsMethods,
1723
- * BatchOperationsMethods,
1724
- * AggregateHelpersMethods,
1725
- * SubdocumentMethods,
1726
- * SoftDeleteMethods,
1727
- * } from '@classytic/mongokit';
1728
- * ```
1729
- */
1730
- /**
1731
- * Extract string keys from a document type, with `string` fallback for untyped usage.
1732
- * Provides autocomplete for known fields while still accepting arbitrary strings
1733
- * (e.g. for nested paths like 'address.city').
1734
- */
1735
- type DocField<TDoc> = (keyof TDoc & string) | (string & {});
1736
- type AllPluginMethods<TDoc> = {
1737
- upsert(query: Record<string, unknown>, data: Record<string, unknown>, options?: Record<string, unknown>): Promise<TDoc>;
1738
- increment(id: string | ObjectId, field: DocField<TDoc>, value?: number, options?: Record<string, unknown>): Promise<TDoc>;
1739
- decrement(id: string | ObjectId, field: DocField<TDoc>, value?: number, options?: Record<string, unknown>): Promise<TDoc>;
1740
- pushToArray(id: string | ObjectId, field: DocField<TDoc>, value: unknown, options?: Record<string, unknown>): Promise<TDoc>;
1741
- pullFromArray(id: string | ObjectId, field: DocField<TDoc>, value: unknown, options?: Record<string, unknown>): Promise<TDoc>;
1742
- addToSet(id: string | ObjectId, field: DocField<TDoc>, value: unknown, options?: Record<string, unknown>): Promise<TDoc>;
1743
- setField(id: string | ObjectId, field: DocField<TDoc>, value: unknown, options?: Record<string, unknown>): Promise<TDoc>;
1744
- unsetField(id: string | ObjectId, fields: DocField<TDoc> | DocField<TDoc>[], options?: Record<string, unknown>): Promise<TDoc>;
1745
- renameField(id: string | ObjectId, oldName: DocField<TDoc>, newName: string, options?: Record<string, unknown>): Promise<TDoc>;
1746
- multiplyField(id: string | ObjectId, field: DocField<TDoc>, multiplier: number, options?: Record<string, unknown>): Promise<TDoc>;
1747
- setMin(id: string | ObjectId, field: DocField<TDoc>, value: unknown, options?: Record<string, unknown>): Promise<TDoc>;
1748
- setMax(id: string | ObjectId, field: DocField<TDoc>, value: unknown, options?: Record<string, unknown>): Promise<TDoc>;
1749
- atomicUpdate(id: string | ObjectId, operators: Record<string, Record<string, unknown>>, options?: Record<string, unknown>): Promise<TDoc>;
1750
- bulkWrite(operations: Record<string, unknown>[], options?: {
1751
- session?: unknown;
1752
- ordered?: boolean;
1753
- }): Promise<{
1754
- ok: number;
1755
- insertedCount: number;
1756
- upsertedCount: number;
1757
- matchedCount: number;
1758
- modifiedCount: number;
1759
- deletedCount: number;
1760
- insertedIds: Record<number, unknown>;
1761
- upsertedIds: Record<number, unknown>;
1762
- }>;
1763
- groupBy(field: DocField<TDoc>, options?: {
1764
- limit?: number;
1765
- session?: unknown;
1766
- }): Promise<Array<{
1767
- _id: unknown;
1768
- count: number;
1769
- }>>;
1770
- sum(field: DocField<TDoc>, query?: Record<string, unknown>, options?: Record<string, unknown>): Promise<number>;
1771
- average(field: DocField<TDoc>, query?: Record<string, unknown>, options?: Record<string, unknown>): Promise<number>;
1772
- min(field: DocField<TDoc>, query?: Record<string, unknown>, options?: Record<string, unknown>): Promise<number>;
1773
- max(field: DocField<TDoc>, query?: Record<string, unknown>, options?: Record<string, unknown>): Promise<number>;
1774
- addSubdocument(parentId: string | ObjectId, arrayPath: DocField<TDoc>, subData: Record<string, unknown>, options?: Record<string, unknown>): Promise<TDoc>;
1775
- getSubdocument(parentId: string | ObjectId, arrayPath: DocField<TDoc>, subId: string | ObjectId, options?: {
1776
- lean?: boolean;
1777
- session?: unknown;
1778
- }): Promise<Record<string, unknown>>;
1779
- updateSubdocument(parentId: string | ObjectId, arrayPath: DocField<TDoc>, subId: string | ObjectId, updateData: Record<string, unknown>, options?: {
1780
- session?: unknown;
1781
- }): Promise<TDoc>;
1782
- deleteSubdocument(parentId: string | ObjectId, arrayPath: DocField<TDoc>, subId: string | ObjectId, options?: Record<string, unknown>): Promise<TDoc>;
1783
- restore(id: string | ObjectId, options?: {
1784
- session?: unknown;
1785
- }): Promise<TDoc>;
1786
- getDeleted(params?: {
1787
- filters?: Record<string, unknown>;
1788
- sort?: SortSpec | string;
1789
- page?: number;
1790
- limit?: number;
1791
- }, options?: {
1792
- select?: SelectSpec;
1793
- populate?: PopulateSpec;
1794
- lean?: boolean;
1795
- session?: unknown;
1796
- }): Promise<OffsetPaginationResult<TDoc>>;
1797
- /**
1798
- * Cache plugin handle (set by `cachePlugin({ adapter })`). Exposes the
1799
- * `CacheEngine` plus convenience invalidation helpers. `undefined` when
1800
- * no cache plugin is wired.
1801
- */
1802
- cache?: import('@classytic/repo-core/cache').RepositoryCacheHandle;
1803
- };
1804
- /**
1805
- * Helper type to add all plugin methods to a repository class
1806
- * Cleaner than manually typing the intersection
1807
- *
1808
- * @example
1809
- * ```typescript
1810
- * import { Repository } from '@classytic/mongokit';
1811
- * import type { WithPlugins } from '@classytic/mongokit';
1812
- *
1813
- * class OrderRepo extends Repository<IOrder> {
1814
- * async getCustomerOrders(customerId: string) {
1815
- * return this.getAll({ filters: { customerId } });
1816
- * }
1817
- * }
1818
- *
1819
- * const orderRepo = new OrderRepo(Model, [
1820
- * ...allPlugins
1821
- * ]) as WithPlugins<IOrder, OrderRepo>;
1822
- *
1823
- * // Works: custom methods + plugin methods
1824
- * await orderRepo.getCustomerOrders('123');
1825
- * await orderRepo.increment(orderId, 'total', 100);
1826
- * ```
1827
- */
1828
- type WithPlugins<TDoc, TRepo extends RepositoryInstance = RepositoryInstance> = TRepo & AllPluginMethods<TDoc>;
1829
- //#endregion
1830
- export { RepositoryEvent as $, KeysetPaginationOptions as A, LookupOptions as At, ObjectId as B, FieldPreset as C, IControllerResponse as Ct, InferDocument as D, MongoKitPluginName as Dt, HookMode as E, MONGOKIT_PLUGIN_NAMES as Et, MiddlewareContext as F, Plugin as G, OperationOptions as H, MinMaxResult as I, PopulateSpec as J, PluginFunction as K, MinimalRepoView as L, LookupPopulateOptions as M, LookupPopulateResult as N, InferRawDoc as O, RepositoryPluginName as Ot, Middleware as P, RepositoryContext as Q, MongoOperatorUpdate as R, EventPhase as S, IController as St, GroupResult as T, IResponseFormatter as Tt, PaginationConfig as U, OffsetPaginationOptions as V, PartialBy as W, ReadOptions as X, PrioritizedHook as Y, ReadPreferenceType as Z, DeleteManyResult as _, ValidationChainOptions as _t, AnyModel as a, SessionOptions as at, EventHandlers as b, WithPlugins as bt, CacheableOptions as c, SoftDeleteRepository as ct, CollationOptions as d, Strict as dt, RepositoryInstance as et, CreateInput as f, UpdateManyResult as ft, DeepPartial as g, UserContext as gt, DecodedCursor as h, UpdateWithValidationResult as ht, AnyDocument as i, SelectSpec as it, Logger as j, KeysOfType as k, LookupBuilder as kt, CascadeOptions as l, SortDirection as lt, CursorPayload as m, UpdatePatch as mt, AggregatePaginationOptions as n, RepositoryOptions as nt, BasePaginationOptions as o, SoftDeleteFilterMode as ot, CreateOptions as p, UpdateOptions as pt, PluginType as q, AllPluginMethods as r, RequiredBy as rt, CacheOperationOptions as s, SoftDeleteOptions as st, AggregateOptions as t, RepositoryOperation as tt, CascadeRelation as u, SortSpec as ut, DeleteResult as v, ValidatorDefinition as vt, FindOneAndUpdateOptions as w, IRequestContext as wt, EventPayload as x, WithTransactionOptions as xt, DocField as y, ValueType as yt, NonNullableFields as z };