@classytic/mongokit 3.23.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.
- package/README.md +11 -2
- package/dist/{PaginationEngine-BnZ24ehC.d.mts → PaginationEngine-CBS1XGzp.d.mts} +2 -1
- package/dist/{PaginationEngine-BT1ywu9k.mjs → PaginationEngine-CczbvCAq.mjs} +64 -3
- package/dist/{Repository-CLQA3sEz.mjs → Repository-KKR8jxfD.mjs} +74 -45
- package/dist/actions/index.d.mts +1 -1
- package/dist/adapter/index.d.mts +1 -1
- package/dist/adapter/index.mjs +1 -1
- package/dist/{adapter-BWgJ_Jdc.mjs → adapter-CLvUHiTW.mjs} +1 -1
- package/dist/ai/index.d.mts +1 -1
- package/dist/better-auth/index.mjs +2 -2
- package/dist/core-62oo3N1C.d.mts +50 -0
- package/dist/{create-repository-Bx1PpcdF.mjs → create-repository-DA5VJh-3.mjs} +2 -2
- package/dist/{create-repository-Cn1LkvL7.d.mts → create-repository-HjMD9h9_.d.mts} +18 -5
- package/dist/{id-resolution-P2OzcyRl.mjs → id-resolution-BqDrk0S2.mjs} +12 -1
- package/dist/{index-DLQ6pjal.d.mts → index-DceInCTX.d.mts} +3 -1
- package/dist/index.d.mts +53 -213
- package/dist/index.mjs +1072 -923
- package/dist/{mongooseToJsonSchema-CVnrKFDq.mjs → mongooseToJsonSchema-CByt1VhQ.mjs} +1 -1
- package/dist/{multi-tenant.plugin-BjXXuc_g.d.mts → multi-tenant.plugin-DVyVGREb.d.mts} +117 -18
- package/dist/operations-C8IcCHF9.d.mts +463 -0
- package/dist/pagination/PaginationEngine.d.mts +1 -1
- package/dist/pagination/PaginationEngine.mjs +1 -1
- package/dist/pagination-jSUdrACK.d.mts +158 -0
- package/dist/plugin-options-vFufQZMZ.d.mts +125 -0
- package/dist/plugins/index.d.mts +4 -4
- package/dist/plugins/index.mjs +3 -3
- package/dist/query/primitives/coercion.mjs +1 -1
- package/dist/query/primitives/geo.d.mts +1 -1
- package/dist/repository-DCHRWRXw.d.mts +511 -0
- package/dist/testkit/index.d.mts +1 -1
- package/dist/testkit/index.mjs +1 -1
- package/dist/{timestamp.plugin-BnuuIDqJ.mjs → timestamp.plugin-B8252GuQ.mjs} +192 -46
- package/dist/{to-plain-g8-gsVxZ.d.mts → to-plain-DkF5px0R.d.mts} +12 -16
- package/dist/type-utils-CLzRFtIA.d.mts +163 -0
- package/dist/utils/index.d.mts +2 -2
- package/dist/utils/index.mjs +3 -3
- package/dist/{validation-chain.plugin-ZFDHxXLn.mjs → validation-chain.plugin-C3Vwp-Rk.mjs} +1 -1
- package/dist/{validation-chain.plugin-CFJbr964.d.mts → validation-chain.plugin-DxD15h-2.d.mts} +3 -1
- package/package.json +5 -3
- 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 };
|