@classytic/mongokit 3.18.0 → 3.19.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (32) hide show
  1. package/README.md +66 -0
  2. package/dist/PaginationEngine-4SjzfBQE.d.mts +103 -0
  3. package/dist/actions/index.d.mts +1 -1
  4. package/dist/adapter/index.mjs +1 -1
  5. package/dist/ai/index.d.mts +1 -1
  6. package/dist/better-auth/index.mjs +1 -1
  7. package/dist/create-repository-D5YX0wGs.mjs +29 -0
  8. package/dist/create-repository-K0yft2ht.d.mts +1663 -0
  9. package/dist/{index-fi91ycQc.d.mts → index-BgSBwlWu.d.mts} +1 -1
  10. package/dist/index.d.mts +11 -1665
  11. package/dist/index.mjs +5 -29
  12. package/dist/{mongooseToJsonSchema-BdQJU39w.d.mts → mongooseToJsonSchema-Bz47aMfH.d.mts} +1 -1
  13. package/dist/multi-tenant.plugin-D6lw3BsI.d.mts +487 -0
  14. package/dist/pagination/PaginationEngine.d.mts +1 -102
  15. package/dist/plugins/index.d.mts +3 -2
  16. package/dist/plugins/index.mjs +2 -1
  17. package/dist/query/primitives/coercion.d.mts +1 -1
  18. package/dist/query/primitives/indexes.d.mts +1 -1
  19. package/dist/testkit/index.d.mts +129 -0
  20. package/dist/testkit/index.mjs +139 -0
  21. package/dist/{validation-chain.plugin-DJtC0KJ-.mjs → timestamp.plugin-B1A9U-P7.mjs} +1 -1052
  22. package/dist/utils/index.d.mts +1 -1
  23. package/dist/utils/index.mjs +2 -2
  24. package/dist/{validation-chain.plugin-BpMfy18N.d.mts → validation-chain.plugin-CYOPfFGF.d.mts} +2 -486
  25. package/dist/validation-chain.plugin-DHZbpb4t.mjs +1056 -0
  26. package/package.json +9 -1
  27. /package/dist/{adapter-DYEMkLwK.mjs → adapter-BSxhT0Ip.mjs} +0 -0
  28. /package/dist/{coercion-TRMmR49h.d.mts → coercion-BxxHyLjP.d.mts} +0 -0
  29. /package/dist/{field-selection-CQW3WBiK.mjs → field-selection-d1qdRiMr.mjs} +0 -0
  30. /package/dist/{indexes-CgOF7Wjd.d.mts → indexes-3eHBWNZ2.d.mts} +0 -0
  31. /package/dist/{mongooseToJsonSchema-CBEQmeEa.mjs → mongooseToJsonSchema-ChtXSrLT.mjs} +0 -0
  32. /package/dist/{types-CU3SEDbD.d.mts → types-BA8oXjFe.d.mts} +0 -0
package/dist/index.d.mts CHANGED
@@ -1,16 +1,14 @@
1
- import { $ as RepositoryEvent, A as KeysetPaginationOptions, B as ObjectId, C as FieldPreset, Ct as IControllerResponse, D as InferDocument, Dt as LookupOptions, E as HookMode, Et as LookupBuilder, F as MiddlewareContext, G as Plugin, H as OperationOptions, I as MinMaxResult, J as PopulateSpec, K as PluginFunction, L as MinimalRepoView, M as LookupPopulateOptions, N as LookupPopulateResult, O as InferRawDoc, P as Middleware, Q as RepositoryContext, R as MongoOperatorUpdate, S as EventPhase, St as IController, T as GroupResult, Tt as IResponseFormatter, U as PaginationConfig, V as OffsetPaginationOptions, W as PartialBy, X as ReadOptions, Y as PrioritizedHook, Z as ReadPreferenceType, _ as DeleteManyResult, _t as ValidationChainOptions, a as AnyModel, at as SessionOptions, b as EventHandlers, bt as WithPlugins, c as CacheableOptions, ct as SoftDeleteRepository, d as CollationOptions, dt as Strict, et as RepositoryInstance, f as CreateInput, ft as UpdateManyResult, g as DeepPartial, gt as UserContext, h as DecodedCursor, ht as UpdateWithValidationResult, i as AnyDocument, it as SelectSpec, j as Logger, k as KeysOfType, l as CascadeOptions, lt as SortDirection, m as CursorPayload, mt as UpdatePatch, n as AggregatePaginationOptions, nt as RepositoryOptions, o as BasePaginationOptions, ot as SoftDeleteFilterMode, p as CreateOptions, pt as UpdateOptions, q as PluginType, r as AllPluginMethods, rt as RequiredBy, s as CacheOperationOptions, st as SoftDeleteOptions, t as AggregateOptions, tt as RepositoryOperation, u as CascadeRelation, ut as SortSpec, v as DeleteResult, vt as ValidatorDefinition, w as FindOneAndUpdateOptions, wt as IRequestContext, x as EventPayload, xt as WithTransactionOptions, y as DocField, yt as ValueType, z as NonNullableFields } from "./types-CU3SEDbD.mjs";
2
- import { t as index_d_exports } from "./index-fi91ycQc.mjs";
3
- import { $ as AuditEntry, A as BulkWriteResult, B as IdGenerator, C as LeasePluginOptions, D as elasticSearchPlugin, E as ElasticSearchOptions, F as MultiTenantOptions, G as getNextSequence, H as SequentialIdOptions, I as adminBypass, J as CacheAdapter, K as prefixedId, L as multiTenantPlugin, M as auditLogPlugin, N as AggregateHelpersMethods, O as cascadePlugin, P as aggregateHelpersPlugin, Q as cachePlugin, R as CustomIdOptions, S as LeaseMethods, T as fieldFilterPlugin, U as customIdPlugin, V as PrefixedIdOptions, W as dateSequentialId, X as RepositoryCacheHandle, Y as CacheOptions, Z as RepositoryCachePluginOptions, _ as observabilityPlugin, a as uniqueField, at as AuditTrailQuery, c as TenantContext, d as SubdocumentMethods, et as AuditOperation, f as subdocumentPlugin, g as OperationMetric, h as ObservabilityOptions, i as requireField, it as AuditTrailOptions, j as batchOperationsPlugin, k as BatchOperationsMethods, l as TenantStore, m as softDeletePlugin, n as blockIf, nt as AuditQueryResult, o as validationChainPlugin, ot as auditTrailPlugin, p as SoftDeleteMethods, q as sequentialId, r as immutableField, rt as AuditTrailMethods, s as timestampPlugin, t as autoInject, tt as AuditQueryOptions, u as createTenantContext, v as MongoOperationsMethods, w as leasePlugin, x as methodRegistryPlugin, y as mongoOperationsPlugin, z as DateSequentialIdOptions } from "./validation-chain.plugin-BpMfy18N.mjs";
4
- import { PaginationEngine } from "./pagination/PaginationEngine.mjs";
5
- import { n as SchemaPathsLike, t as FieldType$1 } from "./coercion-TRMmR49h.mjs";
6
- import { n as SchemaIndexes } from "./indexes-CgOF7Wjd.mjs";
7
- import { d as filterResponseData, f as getFieldsForUser, g as parseDuplicateKeyError, h as isDuplicateKeyError, i as configureLogger, m as createError, n as buildCrudSchemasFromMongooseSchema, p as getMongooseProjection, r as createMemoryCache, s as idVariants, t as buildCrudSchemasFromModel, u as createFieldPreset } from "./mongooseToJsonSchema-BdQJU39w.mjs";
8
- import { AggPaginationRequest, AggRequest, AggResult, ChangeEvent, FilterInput, KeysetAggPaginationResult, QueryOptions, RepoCapabilities, RepositoryBase, TenantPurgeOptions, TenantPurgeResult, TenantPurgeStrategy, WatchOptions } from "@classytic/repo-core/repository";
9
- import * as _$mongoose from "mongoose";
10
- import { ClientSession, Expression, Model, PipelineStage, PopulateOptions } from "mongoose";
11
- import { UpdateInput } from "@classytic/repo-core/update";
12
- import { AggregatePaginationResult, KeysetPaginationResult, OffsetPaginationResult, OffsetPaginationResultCore } from "@classytic/repo-core/pagination";
13
- import { HttpError } from "@classytic/repo-core/errors";
1
+ import { $ as RepositoryEvent, A as KeysetPaginationOptions, B as ObjectId, C as FieldPreset, Ct as IControllerResponse, D as InferDocument, Dt as LookupOptions, E as HookMode, Et as LookupBuilder, F as MiddlewareContext, G as Plugin, H as OperationOptions, I as MinMaxResult, J as PopulateSpec, K as PluginFunction, L as MinimalRepoView, M as LookupPopulateOptions, N as LookupPopulateResult, O as InferRawDoc, P as Middleware, Q as RepositoryContext, R as MongoOperatorUpdate, S as EventPhase, St as IController, T as GroupResult, Tt as IResponseFormatter, U as PaginationConfig, V as OffsetPaginationOptions, W as PartialBy, X as ReadOptions, Y as PrioritizedHook, Z as ReadPreferenceType, _t as ValidationChainOptions, a as AnyModel, at as SessionOptions, b as EventHandlers, bt as WithPlugins, c as CacheableOptions, ct as SoftDeleteRepository, d as CollationOptions, dt as Strict, et as RepositoryInstance, f as CreateInput, ft as UpdateManyResult, g as DeepPartial, gt as UserContext, h as DecodedCursor, ht as UpdateWithValidationResult, i as AnyDocument, it as SelectSpec, j as Logger, k as KeysOfType, l as CascadeOptions, lt as SortDirection, m as CursorPayload, mt as UpdatePatch, n as AggregatePaginationOptions, nt as RepositoryOptions, o as BasePaginationOptions, ot as SoftDeleteFilterMode, p as CreateOptions, pt as UpdateOptions, q as PluginType, r as AllPluginMethods, rt as RequiredBy, s as CacheOperationOptions, st as SoftDeleteOptions, t as AggregateOptions, tt as RepositoryOperation, u as CascadeRelation, ut as SortSpec, v as DeleteResult, vt as ValidatorDefinition, w as FindOneAndUpdateOptions, wt as IRequestContext, x as EventPayload, xt as WithTransactionOptions, y as DocField, yt as ValueType, z as NonNullableFields } from "./types-BA8oXjFe.mjs";
2
+ import { t as index_d_exports } from "./index-BgSBwlWu.mjs";
3
+ import { C as AuditTrailMethods, E as auditTrailPlugin, S as AuditQueryResult, T as AuditTrailQuery, _ as RepositoryCachePluginOptions, a as DateSequentialIdOptions, b as AuditOperation, c as SequentialIdOptions, d as getNextSequence, f as prefixedId, g as RepositoryCacheHandle, h as CacheOptions, i as CustomIdOptions, l as customIdPlugin, m as CacheAdapter, n as adminBypass, o as IdGenerator, p as sequentialId, r as multiTenantPlugin, s as PrefixedIdOptions, t as MultiTenantOptions, u as dateSequentialId, v as cachePlugin, w as AuditTrailOptions, x as AuditQueryOptions, y as AuditEntry } from "./multi-tenant.plugin-D6lw3BsI.mjs";
4
+ import { t as PaginationEngine } from "./PaginationEngine-4SjzfBQE.mjs";
5
+ import { a as Repository, i as HOOK_PRIORITY, n as CreateRepositoryConfig, o as AggregationBuilder, r as createRepository, t as AuditConfig } from "./create-repository-K0yft2ht.mjs";
6
+ import { A as BulkWriteResult, C as LeasePluginOptions, D as elasticSearchPlugin, E as ElasticSearchOptions, M as auditLogPlugin, N as AggregateHelpersMethods, O as cascadePlugin, P as aggregateHelpersPlugin, S as LeaseMethods, T as fieldFilterPlugin, _ as observabilityPlugin, a as uniqueField, c as TenantContext, d as SubdocumentMethods, f as subdocumentPlugin, g as OperationMetric, h as ObservabilityOptions, i as requireField, j as batchOperationsPlugin, k as BatchOperationsMethods, l as TenantStore, m as softDeletePlugin, n as blockIf, o as validationChainPlugin, p as SoftDeleteMethods, r as immutableField, s as timestampPlugin, t as autoInject, u as createTenantContext, v as MongoOperationsMethods, w as leasePlugin, x as methodRegistryPlugin, y as mongoOperationsPlugin } from "./validation-chain.plugin-CYOPfFGF.mjs";
7
+ import { n as SchemaPathsLike, t as FieldType$1 } from "./coercion-BxxHyLjP.mjs";
8
+ import { n as SchemaIndexes } from "./indexes-3eHBWNZ2.mjs";
9
+ import { d as filterResponseData, f as getFieldsForUser, g as parseDuplicateKeyError, h as isDuplicateKeyError, i as configureLogger, m as createError, n as buildCrudSchemasFromMongooseSchema, p as getMongooseProjection, r as createMemoryCache, s as idVariants, t as buildCrudSchemasFromModel, u as createFieldPreset } from "./mongooseToJsonSchema-Bz47aMfH.mjs";
10
+ import { RepoCapabilities } from "@classytic/repo-core/repository";
11
+ import { ClientSession, PipelineStage } from "mongoose";
14
12
 
15
13
  //#region src/actions/cascade-references.d.ts
16
14
  type PurgeMode = 'hard' | 'soft' | 'anonymize';
@@ -93,1658 +91,6 @@ declare function cascadePurgeReferences(options: CascadePurgeReferencesOptions):
93
91
  //#region src/capabilities.d.ts
94
92
  declare const MONGOKIT_CAPABILITIES: RepoCapabilities;
95
93
  //#endregion
96
- //#region src/query/AggregationBuilder.d.ts
97
- /** Options for $vectorSearch stage (Atlas only) */
98
- interface VectorSearchOptions {
99
- /** Atlas Search index name */
100
- index: string;
101
- /** Field path containing the vector embedding */
102
- path: string;
103
- /** Query vector to search against */
104
- queryVector: number[];
105
- /** Number of candidates to consider (higher = more accurate, slower) */
106
- numCandidates?: number;
107
- /** Max results to return */
108
- limit: number;
109
- /** Pre-filter documents before vector search */
110
- filter?: Record<string, unknown>;
111
- /** Use exact search instead of ANN (slower but precise) */
112
- exact?: boolean;
113
- }
114
- /** Result from build() including execution options */
115
- interface AggregationPlan {
116
- pipeline: PipelineStage[];
117
- allowDiskUse: boolean;
118
- }
119
- type SortOrder = 1 | -1 | 'asc' | 'desc';
120
- type SortSpec$2 = Record<string, SortOrder>;
121
- type ProjectionSpec = Record<string, 0 | 1 | Expression>;
122
- type GroupSpec = {
123
- _id: string | Record<string, unknown> | null;
124
- [key: string]: unknown;
125
- };
126
- /**
127
- * Fluent builder for MongoDB aggregation pipelines
128
- * Optimized for complex queries at scale
129
- */
130
- declare class AggregationBuilder {
131
- private pipeline;
132
- private _diskUse;
133
- /**
134
- * Get the current pipeline
135
- */
136
- get(): PipelineStage[];
137
- /**
138
- * Build and return the final pipeline
139
- */
140
- build(): PipelineStage[];
141
- /**
142
- * Build pipeline with execution options (allowDiskUse, etc.)
143
- */
144
- plan(): AggregationPlan;
145
- /**
146
- * Build and execute the pipeline against a model
147
- *
148
- * @example
149
- * ```typescript
150
- * const results = await new AggregationBuilder()
151
- * .match({ status: 'active' })
152
- * .allowDiskUse()
153
- * .exec(MyModel);
154
- * ```
155
- */
156
- exec<T = unknown>(model: Model<any>, session?: _$mongoose.ClientSession): Promise<T[]>;
157
- /**
158
- * Reset the pipeline
159
- */
160
- reset(): this;
161
- /**
162
- * Add a raw pipeline stage
163
- */
164
- addStage(stage: PipelineStage): this;
165
- /**
166
- * Add multiple raw pipeline stages
167
- */
168
- addStages(stages: PipelineStage[]): this;
169
- /**
170
- * $match - Filter documents
171
- * IMPORTANT: Place $match as early as possible for performance
172
- */
173
- match(query: Record<string, unknown>): this;
174
- /**
175
- * $project - Include/exclude fields or compute new fields
176
- */
177
- project(projection: ProjectionSpec): this;
178
- /**
179
- * $group - Group documents and compute aggregations
180
- *
181
- * @example
182
- * ```typescript
183
- * .group({
184
- * _id: '$department',
185
- * count: { $sum: 1 },
186
- * avgSalary: { $avg: '$salary' }
187
- * })
188
- * ```
189
- */
190
- group(groupSpec: GroupSpec): this;
191
- /**
192
- * $sort - Sort documents
193
- */
194
- sort(sortSpec: SortSpec$2 | string): this;
195
- /**
196
- * $limit - Limit number of documents
197
- */
198
- limit(count: number): this;
199
- /**
200
- * $skip - Skip documents
201
- */
202
- skip(count: number): this;
203
- /**
204
- * $unwind - Deconstruct array field
205
- */
206
- unwind(path: string, preserveNullAndEmptyArrays?: boolean): this;
207
- /**
208
- * $addFields - Add new fields or replace existing fields
209
- */
210
- addFields(fields: Record<string, unknown>): this;
211
- /**
212
- * $set - Alias for $addFields
213
- */
214
- set(fields: Record<string, unknown>): this;
215
- /**
216
- * $unset - Remove fields
217
- */
218
- unset(fields: string | string[]): this;
219
- /**
220
- * $replaceRoot - Replace the root document
221
- */
222
- replaceRoot(newRoot: string | Record<string, unknown>): this;
223
- /**
224
- * $lookup - Join with another collection (simple form)
225
- *
226
- * @param from - Collection to join with
227
- * @param localField - Field from source collection
228
- * @param foreignField - Field from target collection
229
- * @param as - Output field name
230
- * @param single - Unwrap array to single object
231
- *
232
- * @example
233
- * ```typescript
234
- * // Join employees with departments by slug
235
- * .lookup('departments', 'deptSlug', 'slug', 'department', true)
236
- * ```
237
- */
238
- lookup(from: string, localField: string, foreignField: string, as?: string, single?: boolean): this;
239
- /**
240
- * $lookup - Join with another collection (advanced form with pipeline)
241
- *
242
- * @example
243
- * ```typescript
244
- * .lookupWithPipeline({
245
- * from: 'products',
246
- * localField: 'productIds',
247
- * foreignField: 'sku',
248
- * as: 'products',
249
- * pipeline: [
250
- * { $match: { status: 'active' } },
251
- * { $project: { name: 1, price: 1 } }
252
- * ]
253
- * })
254
- * ```
255
- */
256
- lookupWithPipeline(options: LookupOptions): this;
257
- /**
258
- * Multiple lookups at once
259
- *
260
- * @example
261
- * ```typescript
262
- * .multiLookup([
263
- * { from: 'departments', localField: 'deptSlug', foreignField: 'slug', single: true },
264
- * { from: 'managers', localField: 'managerId', foreignField: '_id', single: true }
265
- * ])
266
- * ```
267
- */
268
- multiLookup(lookups: LookupOptions[]): this;
269
- /**
270
- * $facet - Process multiple aggregation pipelines in a single stage
271
- * Useful for computing multiple aggregations in parallel
272
- *
273
- * @example
274
- * ```typescript
275
- * .facet({
276
- * totalCount: [{ $count: 'count' }],
277
- * avgPrice: [{ $group: { _id: null, avg: { $avg: '$price' } } }],
278
- * topProducts: [{ $sort: { sales: -1 } }, { $limit: 10 }]
279
- * })
280
- * ```
281
- */
282
- facet(facets: Record<string, PipelineStage[]>): this;
283
- /**
284
- * $bucket - Categorize documents into buckets
285
- *
286
- * @example
287
- * ```typescript
288
- * .bucket({
289
- * groupBy: '$price',
290
- * boundaries: [0, 50, 100, 200],
291
- * default: 'Other',
292
- * output: {
293
- * count: { $sum: 1 },
294
- * products: { $push: '$name' }
295
- * }
296
- * })
297
- * ```
298
- */
299
- bucket(options: {
300
- groupBy: string | Expression;
301
- boundaries: unknown[];
302
- default?: string;
303
- output?: Record<string, unknown>;
304
- }): this;
305
- /**
306
- * $bucketAuto - Automatically determine bucket boundaries
307
- */
308
- bucketAuto(options: {
309
- groupBy: string | Expression;
310
- buckets: number;
311
- output?: Record<string, unknown>;
312
- granularity?: string;
313
- }): this;
314
- /**
315
- * $setWindowFields - Perform window functions (MongoDB 5.0+)
316
- * Useful for rankings, running totals, moving averages
317
- *
318
- * @example
319
- * ```typescript
320
- * .setWindowFields({
321
- * partitionBy: '$department',
322
- * sortBy: { salary: -1 },
323
- * output: {
324
- * rank: { $rank: {} },
325
- * runningTotal: { $sum: '$salary', window: { documents: ['unbounded', 'current'] } }
326
- * }
327
- * })
328
- * ```
329
- */
330
- setWindowFields(options: {
331
- partitionBy?: string | Expression;
332
- sortBy?: SortSpec$2;
333
- output: Record<string, unknown>;
334
- }): this;
335
- /**
336
- * $unionWith - Combine results from multiple collections (MongoDB 4.4+)
337
- *
338
- * @example
339
- * ```typescript
340
- * .unionWith({
341
- * coll: 'archivedOrders',
342
- * pipeline: [{ $match: { year: 2024 } }]
343
- * })
344
- * ```
345
- */
346
- unionWith(options: {
347
- coll: string;
348
- pipeline?: PipelineStage[];
349
- }): this;
350
- /**
351
- * $densify - Fill gaps in data (MongoDB 5.1+)
352
- * Useful for time series data with missing points
353
- */
354
- densify(options: {
355
- field: string;
356
- range: {
357
- step: number;
358
- unit?: 'millisecond' | 'second' | 'minute' | 'hour' | 'day' | 'week' | 'month' | 'quarter' | 'year';
359
- bounds: 'full' | 'partition' | [unknown, unknown];
360
- };
361
- }): this;
362
- /**
363
- * $fill - Fill null or missing field values (MongoDB 5.3+)
364
- */
365
- fill(options: {
366
- sortBy?: SortSpec$2;
367
- output: Record<string, {
368
- method: 'linear' | 'locf' | 'value';
369
- value?: unknown;
370
- }>;
371
- }): this;
372
- /**
373
- * Enable allowDiskUse for large aggregations that exceed 100MB memory limit
374
- *
375
- * @example
376
- * ```typescript
377
- * const results = await new AggregationBuilder()
378
- * .match({ status: 'active' })
379
- * .group({ _id: '$category', total: { $sum: '$amount' } })
380
- * .allowDiskUse()
381
- * .exec(Model);
382
- * ```
383
- */
384
- allowDiskUse(enable?: boolean): this;
385
- /**
386
- * Paginate - Add skip and limit for offset-based pagination
387
- */
388
- paginate(page: number, limit: number): this;
389
- /**
390
- * Count total documents (useful with $facet for pagination metadata)
391
- */
392
- count(outputField?: string): this;
393
- /**
394
- * Sample - Randomly select N documents
395
- */
396
- sample(size: number): this;
397
- /**
398
- * Out - Write results to a collection
399
- */
400
- out(collection: string): this;
401
- /**
402
- * Merge - Merge results into a collection
403
- */
404
- merge(options: string | {
405
- into: string;
406
- on?: string | string[];
407
- whenMatched?: string;
408
- whenNotMatched?: string;
409
- }): this;
410
- /**
411
- * GeoNear - Perform geospatial queries
412
- */
413
- geoNear(options: {
414
- near: {
415
- type: 'Point';
416
- coordinates: [number, number];
417
- };
418
- distanceField: string;
419
- maxDistance?: number;
420
- query?: Record<string, unknown>;
421
- spherical?: boolean;
422
- }): this;
423
- /**
424
- * GraphLookup - Perform recursive search (graph traversal)
425
- */
426
- graphLookup(options: {
427
- from: string;
428
- startWith: string | Expression;
429
- connectFromField: string;
430
- connectToField: string;
431
- as: string;
432
- maxDepth?: number;
433
- depthField?: string;
434
- restrictSearchWithMatch?: Record<string, unknown>;
435
- }): this;
436
- /**
437
- * $search - Atlas Search full-text search (Atlas only)
438
- *
439
- * @example
440
- * ```typescript
441
- * .search({
442
- * index: 'default',
443
- * text: {
444
- * query: 'laptop computer',
445
- * path: ['title', 'description'],
446
- * fuzzy: { maxEdits: 2 }
447
- * }
448
- * })
449
- * ```
450
- */
451
- search(options: {
452
- index?: string;
453
- text?: {
454
- query: string;
455
- path: string | string[];
456
- fuzzy?: {
457
- maxEdits?: number;
458
- prefixLength?: number;
459
- };
460
- score?: {
461
- boost?: {
462
- value?: number;
463
- };
464
- };
465
- };
466
- compound?: {
467
- must?: unknown[];
468
- mustNot?: unknown[];
469
- should?: unknown[];
470
- filter?: unknown[];
471
- };
472
- autocomplete?: unknown;
473
- near?: unknown;
474
- range?: unknown;
475
- }): this;
476
- /**
477
- * $searchMeta - Get Atlas Search metadata (Atlas only)
478
- */
479
- searchMeta(options: Record<string, unknown>): this;
480
- /**
481
- * $vectorSearch - Semantic similarity search using vector embeddings (Atlas only)
482
- *
483
- * Requires an Atlas Vector Search index on the target field.
484
- * Must be the first stage in the pipeline.
485
- *
486
- * @example
487
- * ```typescript
488
- * const results = await new AggregationBuilder()
489
- * .vectorSearch({
490
- * index: 'vector_index',
491
- * path: 'embedding',
492
- * queryVector: await getEmbedding('running shoes'),
493
- * limit: 10,
494
- * numCandidates: 100,
495
- * filter: { category: 'footwear' }
496
- * })
497
- * .project({ embedding: 0, score: { $meta: 'vectorSearchScore' } })
498
- * .exec(ProductModel);
499
- * ```
500
- */
501
- vectorSearch(options: VectorSearchOptions): this;
502
- /**
503
- * Add vectorSearchScore as a field after $vectorSearch
504
- * Convenience for `.addFields({ score: { $meta: 'vectorSearchScore' } })`
505
- */
506
- withVectorScore(fieldName?: string): this;
507
- /**
508
- * Create a builder from an existing pipeline
509
- */
510
- static from(pipeline: PipelineStage[]): AggregationBuilder;
511
- /**
512
- * Create a builder with initial match stage
513
- */
514
- static startWith(query: Record<string, unknown>): AggregationBuilder;
515
- }
516
- //#endregion
517
- //#region src/Repository.d.ts
518
- /**
519
- * Plugin phase priorities (lower = runs first).
520
- *
521
- * Re-exported from `@classytic/repo-core/hooks` so mongokit 3.10 callers see
522
- * the same constant whether they import from mongokit or directly from
523
- * repo-core — priorities are identical (POLICY=100, CACHE=200,
524
- * OBSERVABILITY=300, DEFAULT=500) and the two refs are reference-equal.
525
- */
526
- declare const HOOK_PRIORITY: {
527
- readonly POLICY: 100;
528
- readonly VALIDATION: 150;
529
- readonly CACHE: 200;
530
- readonly OBSERVABILITY: 300;
531
- readonly DEFAULT: 500;
532
- };
533
- /**
534
- * Production-grade repository for MongoDB.
535
- *
536
- * Extends `@classytic/repo-core/repository`'s `RepositoryBase` for the
537
- * driver-agnostic hook + plugin plumbing (context builder, priority-sorted
538
- * event engine, plugin-order validator) while layering every Mongo-specific
539
- * concern — populate, aggregate, lookup, keyset + cursor pagination, $near
540
- * rewriting — on top.
541
- *
542
- * Public surface is byte-stable with mongokit 3.9 (the `_hooks` property is
543
- * preserved as a live read-through getter; all CRUD, event, and plugin method
544
- * signatures unchanged). 3.10 is an internal re-plumbing release; consumers
545
- * upgrading from 3.9 need no code changes.
546
- */
547
- declare class Repository<TDoc = unknown> extends RepositoryBase {
548
- /**
549
- * Runtime capability descriptor required by `StandardRepo<TDoc>`
550
- * (repo-core 0.6.0). Hosts feature-detect once at boot instead of
551
- * try/catching `UnsupportedOperationError` per call.
552
- */
553
- readonly capabilities: RepoCapabilities;
554
- readonly Model: Model<TDoc>;
555
- /**
556
- * Mongoose model name. Duplicated on the instance for BC — arc / catalog /
557
- * user code read `repo.model`. `RepositoryBase.modelName` points to the
558
- * same string; prefer `modelName` in new code.
559
- */
560
- readonly model: string;
561
- readonly _pagination: PaginationEngine<TDoc>;
562
- readonly idField: string;
563
- readonly searchMode: 'text' | 'regex' | 'auto';
564
- readonly searchFields: string[] | undefined;
565
- [key: string]: unknown;
566
- private _hasTextIndex;
567
- /**
568
- * Wrap-style middleware chain. Composed around every `_runOp`
569
- * invocation in registration order — earlier-registered middlewares
570
- * are outer (run first / unwind last). See `useMiddleware()` and
571
- * the `Middleware<TDoc>` type for the protocol.
572
- */
573
- private readonly _middlewares;
574
- constructor(Model: Model<TDoc, any, any, any>, plugins?: PluginType[], paginationConfig?: PaginationConfig, options?: RepositoryOptions);
575
- /**
576
- * Live read-through view of the hook engine's listener registry.
577
- *
578
- * Preserved as a public field on the Repository for back-compat with
579
- * mongokit ≤3.9 — existing tests and observability code pattern-match
580
- * `repo._hooks.get('before:getAll')` / `repo._hooks.size`. Each access
581
- * returns a fresh snapshot of the underlying HookEngine state.
582
- *
583
- * Read-only. To register listeners use `repo.on(event, listener, { priority })`.
584
- */
585
- get _hooks(): Map<string, PrioritizedHook[]>;
586
- /**
587
- * Fire-and-forget emit aliased through RepositoryBase's async path so
588
- * error hooks converted to/from sync mode still route uniformly. Mongokit
589
- * ≤3.9 made this a private method on its subclass; the base class exposes
590
- * it publicly and wraps it in `_emitHook` below for internal use.
591
- */
592
- private _emitHook;
593
- private _emitErrorHook;
594
- /**
595
- * Wire the `warnOnStrictQueryStrip` diagnostic. When enabled, every
596
- * filter-shaped op (`getById`, `getByQuery`, `getOne`, `findAll`,
597
- * `count`, `exists`, `update`, `delete`, `findOneAndUpdate`,
598
- * `updateMany`, `deleteMany`, `claim`, `claimVersion`, `cursor`)
599
- * checks its `context.query` keys against the cached set of schema
600
- * paths. Top-level keys not on the schema get logged once per
601
- * `(model, key)` pair — so a single bad caller surfaces in dev /
602
- * staging instead of returning the wrong row in prod.
603
- *
604
- * Only runs when `strictQuery !== false` is in effect — if mongoose's
605
- * strictQuery is off, unknown keys flow through to the driver and
606
- * the trap doesn't apply.
607
- */
608
- private _wireStrictQueryStripDiagnostic;
609
- /**
610
- * Register a wrap-style middleware. Middleware composes AROUND every
611
- * `_runOp` invocation — it sees the operation name, the live
612
- * `RepositoryContext`, and a `next()` continuation that runs the
613
- * remaining chain + the actual op + after/error hooks.
614
- *
615
- * **Composes with `repo.on()` and plugins, doesn't replace them.**
616
- * Middleware is for ergonomics (timing, short-circuit, input/output
617
- * mutation in a single closure) — NOT for security policy. Use the
618
- * `before:*` hook engine for tenant scope, soft-delete filtering,
619
- * cache invalidation, and audit; those plugins MUST run before
620
- * middleware sees the op.
621
- *
622
- * **Execution order on every call** (build/before hooks fire BEFORE
623
- * the middleware chain dispatches, so middleware cannot wrap a
624
- * before-hook failure):
625
- *
626
- * ```text
627
- * _buildContext + before:<op> ← repo.on('before:*') hooks
628
- * [outer middleware pre] ← repo.useMiddleware() registrations
629
- * [...inner middleware pre]
630
- * fn (driver call)
631
- * after:<op> | error:<op> ← repo.on('after:*' / 'error:*')
632
- * [...inner middleware post]
633
- * [outer middleware post]
634
- * ```
635
- *
636
- * Why this order matters: a `before:create` hook that injects a
637
- * tenant filter into `context.query` runs BEFORE the middleware chain
638
- * is composed. Middleware sees a context that was already scoped by
639
- * the policy plugins — it never has the authority to short-circuit a
640
- * tenant check, because the throw from a `before:*` policy hook
641
- * unwinds before middleware ever fires. That's by design — middleware
642
- * as a security boundary would be impossible to audit, since
643
- * registration order would determine whether scope wins.
644
- *
645
- * Practical consequence: if you want middleware to observe a policy
646
- * failure (e.g. metric a tenant-scope rejection), use `repo.on('error:
647
- * <op>', ...)` instead — that fires from inside the middleware chain
648
- * and is reachable.
649
- *
650
- * Registration order = composition order: the first middleware
651
- * registered runs outermost (wraps everything else).
652
- *
653
- * @example Time every op
654
- * ```ts
655
- * repo.useMiddleware(async ({ operation, next }) => {
656
- * const start = performance.now();
657
- * try { return await next(); }
658
- * finally { metrics.record(operation, performance.now() - start); }
659
- * });
660
- * ```
661
- *
662
- * @example Inject `tenantId` on every create
663
- * ```ts
664
- * repo.useMiddleware(async ({ operation, context, next }) => {
665
- * if (operation === 'create' && context.data) {
666
- * context.data.tenantId = currentTenant();
667
- * }
668
- * return next();
669
- * });
670
- * ```
671
- *
672
- * @example Short-circuit (return without `next()` to skip the actual op)
673
- * ```ts
674
- * repo.useMiddleware(async ({ operation, context, next }) => {
675
- * if (operation === 'getById' && readOnlyMaintenance) {
676
- * return cachedReadOnlyResponse(context.id);
677
- * }
678
- * return next();
679
- * });
680
- * ```
681
- */
682
- useMiddleware(middleware: Middleware<TDoc>): this;
683
- /**
684
- * Compose the registered middleware chain around `exec`. Outermost
685
- * middleware (first registered) wraps innermost. Each middleware sees
686
- * the live `RepositoryContext` (already populated + run through
687
- * `before:*` hooks at the call site — see `useMiddleware` JSDoc for
688
- * the exact order). `after:` / `error:` hooks fire from inside
689
- * `exec`, so middleware composes WITH plugins, not instead of them.
690
- *
691
- * `_runOp` calls this with its standard try/catch envelope; inline-try
692
- * methods (`getById`, `update`, `delete`, `deleteMany`, `aggregatePaginate`,
693
- * `lookupPopulate`, plus `getOne`/`getAll` cache-hit branches) call it
694
- * directly so middleware sees every op — including cached reads — not
695
- * just the ones routed through `_runOp`.
696
- */
697
- private _composeMiddleware;
698
- /**
699
- * Standard operation envelope: compose middleware around an exec that
700
- * runs `fn`, emits `after:<op>` on success or `error:<op>` on failure.
701
- * Methods with branched in-try logic that emit `after:*` from multiple
702
- * paths (`getById`, `update`, `delete`, `deleteMany`, `aggregatePaginate`,
703
- * `lookupPopulate`) keep their inline try/catch and call
704
- * `_composeMiddleware` directly instead of being forced through this
705
- * single-result helper.
706
- */
707
- private _runOp;
708
- /**
709
- * Resilience envelope around the DRIVER round-trip only — the repo-core
710
- * 0.6.0 `QueryOptions.signal` / `QueryOptions.retryPolicy` contract.
711
- *
712
- * `throwIfAborted` stops a cancelled request before the next driver call;
713
- * `withRetry` retries transient failures with exponential backoff (and is
714
- * a zero-cost passthrough when no policy was passed, so we wrap
715
- * unconditionally). Crucially this wraps `fn` AFTER `_buildContext` ran —
716
- * before-hooks (validation, tenant scope, audit, events) execute exactly
717
- * once per logical call and are NEVER re-run on retry; only the driver
718
- * call repeats.
719
- *
720
- * Both knobs are read from the context (the options bag is spread into
721
- * `_buildContext` inputs), so every op routed through `_runOp` — and the
722
- * inline-try methods that call this directly — honors them uniformly.
723
- */
724
- private _withResilience;
725
- /**
726
- * Create single document
727
- */
728
- create(data: Record<string, unknown>, options?: CreateOptions): Promise<TDoc>;
729
- /**
730
- * Create multiple documents
731
- */
732
- createMany(dataArray: Record<string, unknown>[], options?: CreateOptions): Promise<TDoc[]>;
733
- /**
734
- * Get document by ID
735
- */
736
- getById(id: string | ObjectId, options?: CacheableOptions): Promise<TDoc | null>;
737
- /**
738
- * Get single document by query
739
- */
740
- getByQuery(query: Record<string, unknown>, options?: CacheableOptions & {
741
- sort?: SortSpec;
742
- }): Promise<TDoc | null>;
743
- /**
744
- * Get single document by arbitrary filter.
745
- * Unlike getByQuery, this method is designed for Arc/controller use where
746
- * compound filters (org scope + policy + id) are pre-built.
747
- *
748
- * @example
749
- * const product = await repo.getOne({ slug: 'laptop', organizationId: 'org_1' });
750
- */
751
- getOne(query: Record<string, unknown>, options?: CacheableOptions & {
752
- sort?: SortSpec;
753
- }): Promise<TDoc | null>;
754
- /**
755
- * Fetch ALL documents matching filters without pagination.
756
- * Use for background jobs, exports, batch processing where you need every doc.
757
- *
758
- * @example
759
- * const all = await repo.findAll({ status: 'active' });
760
- * const allLean = await repo.findAll({}, { select: 'name email', lean: true });
761
- */
762
- findAll(filters?: Record<string, unknown>, options?: OperationOptions & {
763
- sort?: SortSpec | string;
764
- limit?: number;
765
- }): Promise<TDoc[]>;
766
- /**
767
- * Streaming reads — async iterator over data, suitable for migrations,
768
- * backfills, schema audits, and any once-a-quarter "touch every row"
769
- * job. Goes through the standard `before:cursor` hook pipeline so
770
- * multi-tenant scope, soft-delete filter, and access-control plugins
771
- * inject scope BEFORE the underlying mongoose cursor is built.
772
- *
773
- * Replaces direct `Model.find().cursor()` usage which bypasses every
774
- * plugin — that's how cross-tenant data ends up in migrations.
775
- *
776
- * Returns an `AsyncIterableIterator<TDoc>` — drive it with `for await`.
777
- * The mongoose cursor is closed when iteration completes or breaks.
778
- *
779
- * @example Backfill a missing field across the whole tenant scope
780
- * ```ts
781
- * for await (const doc of repo.cursor({ filter: { migrated: { $ne: true }}}, { batchSize: 1000 })) {
782
- * await transformer(doc);
783
- * await repo.update(doc._id, { migrated: true });
784
- * }
785
- * ```
786
- *
787
- * @example Audit unscoped — opt out of multi-tenant for global sweeps
788
- * ```ts
789
- * for await (const doc of repo.cursor({}, { batchSize: 500, organizationId: undefined })) {
790
- * audit(doc);
791
- * }
792
- * ```
793
- */
794
- cursor(filter?: Record<string, unknown>, options?: ReadOptions & {
795
- sort?: SortSpec | string;
796
- batchSize?: number;
797
- select?: SelectSpec;
798
- lean?: boolean;
799
- }): AsyncIterableIterator<TDoc>;
800
- /**
801
- * Portable change feed — `StandardRepo.watch()` over Mongo change
802
- * streams (`Model.watch`). Requires a replica set (or mongos); a
803
- * standalone mongod rejects change streams at open time — gate on
804
- * `repo.capabilities.changeStreams` plus your deployment topology.
805
- *
806
- * Semantics:
807
- * - **Plugin-routed like every other read.** The filter goes through
808
- * the standard `before:watch` hook pipeline (`OP_REGISTRY.watch`,
809
- * policyKey `'query'`) BEFORE the change-stream pipeline is built —
810
- * `multiTenantPlugin` injects the tenant scope (and throws under
811
- * `required: true` exactly like other reads), `softDeletePlugin`
812
- * injects the deletion-state predicate. A tenant-scoped repo never
813
- * streams cross-tenant changes.
814
- * - `fullDocument: 'updateLookup'` — update events carry the post-
815
- * image document when it still exists at lookup time.
816
- * - `filter` (record or Filter IR) is compiled with the standard
817
- * filter compiler and applied against `fullDocument.*` paths, so
818
- * `watch({ status: 'pending' })` matches the post-image. Delete
819
- * events have no `fullDocument` and therefore don't match a
820
- * non-empty post-policy filter — use `bypassTenant` / an unscoped
821
- * repo without a filter to observe deletes.
822
- * - `options.signal` ends the iterator (closes the stream). A
823
- * pre-aborted signal rejects at the op boundary like every other op.
824
- * - `options.resumeAfter` forwards a previously captured Mongo
825
- * resume token for at-least-once consumption across restarts.
826
- *
827
- * Context keys for policy plugins (`organizationId`, `bypassTenant`,
828
- * `includeDeleted`, ...) ride the options bag at runtime — same as every
829
- * other op. The contract's `WatchOptions` type doesn't declare an index
830
- * signature, so typed callers widen at the call site (or rely on an
831
- * ALS `resolveContext` so no per-call tenant key is needed).
832
- *
833
- * @example
834
- * ```ts
835
- * const ac = new AbortController();
836
- * for await (const change of repo.watch({ status: 'pending' }, { signal: ac.signal })) {
837
- * if (change.operation === 'create') enqueue(change.doc!);
838
- * }
839
- * ```
840
- */
841
- watch(filter?: FilterInput, options?: WatchOptions): AsyncIterable<ChangeEvent<TDoc>>;
842
- /**
843
- * Unified pagination - auto-detects offset vs keyset based on params
844
- *
845
- * Auto-detection logic:
846
- * - If params has 'cursor' or 'after' → uses keyset pagination (stream)
847
- * - If params has 'pagination' or 'page' → uses offset pagination (paginate)
848
- * - Else → defaults to offset pagination with page=1
849
- *
850
- * @example
851
- * // Offset pagination (page-based)
852
- * await repo.getAll({ page: 1, limit: 50, filters: { status: 'active' } });
853
- * await repo.getAll({ pagination: { page: 2, limit: 20 } });
854
- *
855
- * // Keyset pagination (cursor-based)
856
- * await repo.getAll({ cursor: 'eyJ2Ij...', limit: 50 });
857
- * await repo.getAll({ after: 'eyJ2Ij...', sort: { createdAt: -1 } });
858
- *
859
- * // Simple query (defaults to page 1)
860
- * await repo.getAll({ filters: { status: 'active' } });
861
- *
862
- * // Skip cache for fresh data
863
- * await repo.getAll({ filters: { status: 'active' } }, { skipCache: true });
864
- */
865
- getAll(params?: {
866
- filters?: Record<string, unknown>;
867
- sort?: SortSpec | string;
868
- cursor?: string;
869
- after?: string;
870
- page?: number;
871
- pagination?: {
872
- page?: number;
873
- limit?: number;
874
- };
875
- limit?: number;
876
- search?: string;
877
- mode?: 'offset' | 'keyset';
878
- hint?: string | Record<string, 1 | -1>;
879
- maxTimeMS?: number;
880
- countStrategy?: 'exact' | 'estimated' | 'none';
881
- readPreference?: ReadPreferenceType; /** Advanced populate options (from QueryParser or Arc's BaseController) */
882
- populateOptions?: PopulateOptions[]; /** Collation for locale-aware string comparison */
883
- collation?: CollationOptions; /** Lookup configurations for $lookup joins (from QueryParser or manual) */
884
- lookups?: LookupOptions[]; /** Skip pagination entirely — returns raw TDoc[] (same as findAll) */
885
- noPagination?: boolean;
886
- }, options?: CacheableOptions): Promise<OffsetPaginationResult<TDoc> | KeysetPaginationResult<TDoc> | TDoc[]>;
887
- /**
888
- * Atomic "look up by filter, insert `data` if missing, return the doc."
889
- *
890
- * Returns `{ doc, created }` so callers can tell whether *this* call
891
- * inserted (race-win) or matched an existing row. Implements
892
- * `StandardRepo.getOrCreate()` from `@classytic/repo-core/repository`
893
- * — see that interface's JSDoc for the cross-kit contract.
894
- *
895
- * Routes through the hook system for policy enforcement (multi-tenant,
896
- * soft-delete, audit). The hook context carries the resolved query +
897
- * data; the action layer adds the `created` discriminator from the
898
- * driver's `lastErrorObject.upserted`.
899
- */
900
- getOrCreate(query: Record<string, unknown>, createData: Record<string, unknown>, options?: SessionOptions): Promise<{
901
- doc: TDoc;
902
- created: boolean;
903
- }>;
904
- /**
905
- * Count documents
906
- * Routes through hook system for policy enforcement (multi-tenant, soft-delete)
907
- */
908
- count(query?: Record<string, unknown>, options?: ReadOptions): Promise<number>;
909
- /**
910
- * Check if document exists
911
- * Routes through hook system for policy enforcement (multi-tenant, soft-delete)
912
- */
913
- exists(query: Record<string, unknown>, options?: ReadOptions): Promise<{
914
- _id: unknown;
915
- } | null>;
916
- /**
917
- * Update a document by id and return the post-update doc.
918
- *
919
- * Compiles to `Model.findOneAndUpdate(filter, { $set: data }, { new:
920
- * true, runValidators: true })` under the hood and routes through the
921
- * full plugin pipeline — `multiTenantPlugin` injects the tenant scope,
922
- * `auditTrailPlugin` records the change, `cachePlugin` invalidates,
923
- * `softDeletePlugin` etc. all compose. Returns `null` on miss (the
924
- * `MinimalRepo` contract; pass `throwOnNotFound: true` for the legacy
925
- * throw path).
926
- *
927
- * **Anti-pattern to avoid** — calling `Model.findOneAndUpdate()`
928
- * directly on the raw mongoose model bypasses every plugin:
929
- *
930
- * ```ts
931
- * // ❌ Bypasses multi-tenant scope, audit, cache invalidation:
932
- * const doc = await CampaignModel.findOneAndUpdate(
933
- * { _id: id, organizationId: ctx.orgId }, // hand-rolled tenant filter
934
- * { $set: patch, $currentDate: { updatedAt: 1 } },
935
- * { new: true, runValidators: true }
936
- * );
937
- * return doc ? toEntity(doc) : null;
938
- *
939
- * // ✅ Plugin-routed; tenant + audit + cache auto-applied:
940
- * const doc = await campaignRepo.update(id, patch, repoOptionsFromCtx(ctx));
941
- * return doc ? toEntity(doc) : null;
942
- * ```
943
- *
944
- * The raw-mongoose path is a recurring source of cross-tenant leaks
945
- * (tenant filter forgotten on the next refactor), audit gaps (changes
946
- * invisible to the compliance trail), and stale-cache bugs (cache
947
- * invalidation skipped). Always route mutations through this method —
948
- * the `repo.update()` keystroke count beats raw mongoose AND composes
949
- * with the policy stack.
950
- *
951
- * **CAS / status-precondition writes — use `repo.claim()`, not `update()`.**
952
- * A `findOneAndUpdate` whose filter includes an expected-state check
953
- * (`{ _id, status: 'pending' }` → `{ $set: { status: 'shipped' } }`)
954
- * is a compare-and-set, not a plain update. Reach for the `claim()`
955
- * primitive — it expresses the transition as `{ from, to }`, returns
956
- * `null` on race-loss (so callers can branch on retry), and routes
957
- * through the same plugin pipeline:
958
- *
959
- * ```ts
960
- * // ❌ Hand-rolled CAS — bypasses plugins, no typed race-loss signal:
961
- * const doc = await CampaignModel.findOneAndUpdate(
962
- * { _id: id, organizationId: ctx.orgId, status: 'pending' },
963
- * { $set: { status: 'sent', sentAt: new Date() } },
964
- * { new: true }
965
- * );
966
- * if (!doc) throw new ConcurrentTransitionError(id);
967
- *
968
- * // ✅ Plugin-routed CAS with explicit transition + race-loss = null:
969
- * const doc = await campaignRepo.claim(
970
- * id,
971
- * { from: 'pending', to: 'sent' },
972
- * { sentAt: new Date() },
973
- * repoOptionsFromCtx(ctx),
974
- * );
975
- * if (!doc) throw new ConcurrentTransitionError(id, 'pending', 'sent');
976
- * ```
977
- *
978
- * Pair `claim()` with `defineStateMachine()` from
979
- * `@classytic/primitives/state-machine` for the modelling layer
980
- * (`assertTransition` catches illegal transitions sync, `claim` enforces
981
- * the race-safe write) — see mongokit's CLAUDE.md "State-machine +
982
- * claim() pairing" recipe. For pure dedup-on-id without a state
983
- * transition (idempotency keys, webhook receivers), use `getOrCreate()`
984
- * — `claim()` with `from === to === id` works too but is heavier.
985
- *
986
- * Domain-layer mapping (`doc → entity`) belongs on top of this call,
987
- * not inside mongokit. The canonical pattern is a 3-line host helper:
988
- *
989
- * ```ts
990
- * async function updateAndMap<TDoc, TEntity>(
991
- * repo: Repository<TDoc>,
992
- * id: string,
993
- * patch: Partial<TDoc>,
994
- * ctx: RequestContext,
995
- * mapper: (doc: TDoc) => TEntity,
996
- * ): Promise<TEntity | null> {
997
- * const doc = await repo.update(id, patch, repoOptionsFromCtx(ctx));
998
- * return doc ? mapper(doc) : null;
999
- * }
1000
- * ```
1001
- *
1002
- * Mongokit deliberately doesn't ship `updateAndMap` — host mapper
1003
- * signatures vary (`Result<T, E>`, throw-on-miss, audit-stamped
1004
- * mappers that take `ctx`, tx-scoped mappers, …) and absorbing a
1005
- * single shape would lock every consumer into it.
1006
- */
1007
- update(id: string | ObjectId, data: Record<string, unknown>, options?: UpdateOptions): Promise<TDoc | null>;
1008
- /**
1009
- * Atomic findOneAndUpdate (compare-and-set primitive).
1010
- *
1011
- * Single round-trip match-and-mutate for outbox relays, distributed locks,
1012
- * and workflow semaphores. Goes through the full hook pipeline so
1013
- * multi-tenant scope, soft-delete, audit, and cache plugins all apply.
1014
- *
1015
- * Returns the matched document (post-update by default) or `null` when no
1016
- * document matches and `upsert` is false.
1017
- *
1018
- * @example FIFO claim-lease for an outbox worker
1019
- * ```ts
1020
- * const claimed = await outboxRepo.findOneAndUpdate(
1021
- * { status: 'pending', leaseExpiresAt: { $lt: new Date() } },
1022
- * { $set: { status: 'processing', leaseExpiresAt: leaseUntil, leasedBy: workerId } },
1023
- * { sort: { createdAt: 1 }, returnDocument: 'after' },
1024
- * );
1025
- * if (!claimed) return; // queue empty
1026
- * ```
1027
- *
1028
- * @example CAS upsert
1029
- * ```ts
1030
- * const lock = await locksRepo.findOneAndUpdate(
1031
- * { _id: lockKey, ownerId: { $in: [null, workerId] } },
1032
- * { $set: { ownerId: workerId, expiresAt: leaseUntil } },
1033
- * { upsert: true },
1034
- * );
1035
- * ```
1036
- */
1037
- findOneAndUpdate<TResult = TDoc>(filter: Record<string, unknown>, update: UpdateInput, options?: FindOneAndUpdateOptions): Promise<TResult | null>;
1038
- /**
1039
- * Atomic compare-and-swap state transition. Implements
1040
- * `StandardRepo.claim()` from `@classytic/repo-core/repository` (0.4+).
1041
- *
1042
- * Builds a `findOneAndUpdate({ _id, [field]: from, ...where }, { $set:
1043
- * { [field]: to, ...patch } })` in a single round-trip — race-free
1044
- * across concurrent callers. Returns the post-update doc on success,
1045
- * `null` when:
1046
- * - the row exists but its current state isn't `from` (someone
1047
- * else won the transition), OR
1048
- * - any predicate in `transition.where` fails (paused guard,
1049
- * retry-time guard, heartbeat-staleness, sub-document predicate),
1050
- * OR
1051
- * - no row matches the id at all.
1052
- *
1053
- * The caller can't distinguish "lost race" from "guard predicate
1054
- * failed" — both mean "don't proceed." That's by design: the only
1055
- * action either way is to back off.
1056
- *
1057
- * **Source-state — `from: T | T[]`.** Pass a literal for a single-
1058
- * source transition (`from: 'pending'`) or an array for the
1059
- * "transition from any of these states" pattern (`from: ['pending',
1060
- * 'approved']`). Compiles to `[stateField]: { $in: [...] }`. Real-
1061
- * world frequency: commission's `voidRecord` / `markClawedBack` /
1062
- * `endAgreement` / `_transition`, media-kit's error path,
1063
- * streamline's `cancelAt` — every state machine with more than one
1064
- * non-terminal source state hits this. Without array support those
1065
- * sites fall back to raw `findOneAndUpdate` and lose plugin routing.
1066
- *
1067
- * **`from` is value-only — no Mongo expressions.** `from` accepts a
1068
- * literal or an array of literals (`$in`-shaped). It does NOT
1069
- * accept `{ $ne: ... }`, `{ $lt: ... }`, `{ $exists: ... }`, or
1070
- * arbitrary expression objects. The CAS contract is "match if the
1071
- * state field equals one of these specific values" — admitting
1072
- * arbitrary predicates would collapse the contract into a generic
1073
- * filter, defeating the point of a state-machine primitive.
1074
- *
1075
- * For non-equality / range / existence predicates on the state
1076
- * field, use `where`:
1077
- *
1078
- * ```ts
1079
- * // ❌ from: { $ne: 'ended' } — not supported, would silently work-as-filter
1080
- * // ✅ Use `where` for $ne / $lt / $exists / arbitrary mongo predicates:
1081
- * await repo.claim(id, {
1082
- * from: 'active', // exact-match state
1083
- * to: 'closed',
1084
- * where: { closedAt: { $exists: false } }, // additional predicates
1085
- * });
1086
- * ```
1087
- *
1088
- * **Dotted-path `field` is supported** — `field: 'scheduling.status'`
1089
- * for nested state fields works. Mongo's filter and `$set` both
1090
- * honor dotted paths, and the from===to optimization handles them
1091
- * correctly (literal comparison on the path string). Use for
1092
- * domain models that nest the state under a sub-document
1093
- * (`lpn.state`, `package.condition.state`, `scheduling.status`).
1094
- *
1095
- * **`from === to` is allowed — idempotent re-claim.** Yard's
1096
- * `reviseDeparture` writes `departed → departed` to update the
1097
- * row's payload while asserting the row hasn't moved on. The CAS
1098
- * still returns `null` on race-loss (row left the source state),
1099
- * so the safety property holds. Use the same semantic as a "touch
1100
- * with state assertion" primitive.
1101
- *
1102
- * When `from === to` (and `from` is a literal, not an array), the
1103
- * implementation **skips the redundant `$set: { [stateField]: to }`
1104
- * write** — the filter has already pinned the state field to that
1105
- * exact value, so writing it again is a no-op disk operation.
1106
- * Workloads doing high-replay dedup (yard's `gate-event.append`,
1107
- * outbox replay storms, idempotent first-write CAS) save one disk
1108
- * write + one journal flush + one replication-log entry per replay.
1109
- * Three observable behaviours follow:
1110
- *
1111
- * - With a non-empty patch: only the patch fields land in `$set`;
1112
- * the redundant state-field key is dropped.
1113
- * - With an empty patch and `upsert: false`: the call lowers to
1114
- * a plain `findOne` round-trip — pure assertion, zero writes.
1115
- * Plugin pipeline (before/after `claim` hooks) still fires
1116
- * correctly — only the internal driver shape changes.
1117
- * - With an empty patch and `upsert: true`: the update becomes
1118
- * `{ $setOnInsert: { [stateField]: to } }` — no-op on match,
1119
- * populates the inserted row's state field on miss. This is
1120
- * the "pure dedup" insert-or-confirm path.
1121
- *
1122
- * For pure-dedup recipes (no real state column, just an external
1123
- * id used as both filter and `from`/`to`), see CLAUDE.md — yard's
1124
- * `gate-event.append` is the canonical worked example.
1125
- *
1126
- * **`upsert: true` — insert-or-claim.** Set `options.upsert: true`
1127
- * for the upsert-claim pattern: insert when the row doesn't exist,
1128
- * else CAS-transition the existing row. Yard's `gate-event.append`
1129
- * uses this for idempotent event landing. With `upsert` on, claim
1130
- * NEVER returns null on miss — it inserts. Pair with
1131
- * `$setOnInsert` in the operator patch for insert-only fields.
1132
- * The default (`upsert: false`) keeps the canonical "match
1133
- * exactly, else null" semantic.
1134
- *
1135
- * **Patch shape** — accepts BOTH flat (`{ field: value }`) and Mongo
1136
- * operator (`{ $set, $inc, $unset, $setOnInsert, ... }`) forms.
1137
- * Operator form is load-bearing for versioned docs (`$inc: {
1138
- * version: 1 }`) and upsert-claim (`$setOnInsert: { createdBy }`);
1139
- * flat form is the textbook ergonomic case. Mixing flat keys with
1140
- * `$`-keys throws — Mongo would silently drop the flat keys,
1141
- * which would mask data-loss bugs.
1142
- *
1143
- * Multi-tenant scope, soft-delete filter, cache invalidation, and
1144
- * audit hooks all flow through automatically — `claim` is registered
1145
- * in `OP_REGISTRY` (policyKey: 'query', mutates: true), so plugins
1146
- * that iterate the registry pick it up without changes.
1147
- *
1148
- * **Caller responsibility for tenant context.** When
1149
- * `multiTenantPlugin` is mounted, plugin-injected scoping reads the
1150
- * tenant key from the **options bag** — `claim(id, transition, patch,
1151
- * { organizationId: '...' })`. Without it, the plugin throws
1152
- * `'Missing organizationId'` at runtime. Use
1153
- * `repoOptionsFromCtx(ctx)` (or your own context helper) to forward
1154
- * request-scoped tenant fields; do not assume the plugin reads them
1155
- * from a global.
1156
- *
1157
- * @example Textbook transition (flat patch)
1158
- * ```ts
1159
- * const claimed = await runRepo.claim(runId, { from: 'waiting', to: 'running' }, {
1160
- * lastHeartbeat: new Date(),
1161
- * workerId: 'worker-12',
1162
- * });
1163
- * if (!claimed) return; // someone else got it (or no match)
1164
- * ```
1165
- *
1166
- * @example Versioned doc — operator patch with `$inc`
1167
- * ```ts
1168
- * const claimed = await orderRepo.claim(
1169
- * orderId,
1170
- * { from: 'pending', to: 'shipped' },
1171
- * {
1172
- * $set: { shippedAt: new Date() },
1173
- * $inc: { version: 1 },
1174
- * },
1175
- * );
1176
- * ```
1177
- *
1178
- * @example Compound-filter claim (paused guard + retry timer)
1179
- * ```ts
1180
- * const claimed = await runRepo.claim(
1181
- * runId,
1182
- * {
1183
- * from: 'waiting',
1184
- * to: 'running',
1185
- * where: {
1186
- * paused: { $ne: true },
1187
- * 'scheduling.retryAfter': { $lte: new Date() },
1188
- * },
1189
- * },
1190
- * { lastHeartbeat: new Date() },
1191
- * { organizationId: ctx.orgId }, // tenant fwd — see note above
1192
- * );
1193
- * ```
1194
- *
1195
- * Pairs with `defineStateMachine()` from
1196
- * `@classytic/primitives/state-machine`:
1197
- * - State machine validates "is from→to legal in the model?"
1198
- * - `claim()` performs the atomic "did we win?"
1199
- */
1200
- claim(id: string | ObjectId, transition: {
1201
- field?: string;
1202
- from: unknown | readonly unknown[];
1203
- to: unknown;
1204
- where?: Record<string, unknown>;
1205
- }, patch?: Record<string, unknown>, options?: SessionOptions & {
1206
- idField?: string;
1207
- upsert?: boolean;
1208
- }): Promise<TDoc | null>;
1209
- /**
1210
- * Optimistic-concurrency CAS via a version stamp.
1211
- *
1212
- * Sibling to `claim()` — distinct mental model:
1213
- * - `claim()` is a state machine: "move from status A to status B, atomically"
1214
- * - `claimVersion()` is optimistic locking: "I expect version N; if it
1215
- * still is, apply this update and increment the version"
1216
- *
1217
- * Builds `findOneAndUpdate({ _id, [versionField]: from, ...where },
1218
- * { ...update, $inc: { [versionField]: by ?? 1 } })` in one
1219
- * round-trip. Returns the post-update doc on success, `null` when:
1220
- * - the row doesn't exist, OR
1221
- * - the row's version isn't `from` (someone else committed first —
1222
- * standard race-loss signal), OR
1223
- * - any predicate in `transition.where` fails (state guard, tenant
1224
- * guard expressed inline, etc.)
1225
- *
1226
- * **`from: undefined` is tolerated.** Lean reads return `version:
1227
- * number | undefined` because the field defaults are absent on
1228
- * fresh-from-mongo POJOs. Passing `from: undefined` matches docs
1229
- * whose version field is *missing entirely* — exactly what you want
1230
- * on a first-write CAS. Callers no longer need `?? 0` at every site.
1231
- *
1232
- * The caller's `update` is freeform — pass either a Mongo operator
1233
- * shape (`{ $set: { status: 'submitted', updatedAt: now } }`) or a
1234
- * field-shape object (auto-wrapped in `$set`). The version `$inc` is
1235
- * MERGED into the update, so callers don't have to remember to bump
1236
- * the counter.
1237
- *
1238
- * **Compound CAS via `where`.** State machines that key on both
1239
- * version AND status (yard's `transition()`, every package whose
1240
- * "ready to commit" gate is more than a version stamp) need to
1241
- * AND-merge additional predicates. Pass them via
1242
- * `transition.where` — same shape and semantics as `claim`'s
1243
- * `where` field. Without this, claimVersion was forced back to raw
1244
- * `findOneAndUpdate` for every state-AND-version site.
1245
- *
1246
- * Multi-tenant scope, soft-delete filter, cache invalidation, and
1247
- * audit hooks all fire — `claimVersion` is in `OP_REGISTRY`
1248
- * (policyKey: 'query', mutates: true) so plugins iterate the registry
1249
- * and pick it up automatically.
1250
- *
1251
- * **Caller responsibility for tenant context.** When
1252
- * `multiTenantPlugin` is mounted, the plugin reads the tenant key
1253
- * from the **options bag** (`{ organizationId: '...' }`). Without it,
1254
- * the plugin throws `'Missing organizationId'` at runtime. Use
1255
- * `repoOptionsFromCtx(ctx)` to forward — same as `claim()` and every
1256
- * other repo op.
1257
- *
1258
- * @example Order submission with version check
1259
- * ```ts
1260
- * const submitted = await orderRepo.claimVersion(
1261
- * orderId,
1262
- * { from: order.version },
1263
- * { $set: { status: 'submitted', submittedAt: new Date() } },
1264
- * );
1265
- * if (!submitted) throw new ConcurrentEditError();
1266
- * ```
1267
- *
1268
- * @example Compound CAS — version + status
1269
- * ```ts
1270
- * const transitioned = await yardRepo.claimVersion(
1271
- * loadId,
1272
- * {
1273
- * from: load.version, // number | undefined OK
1274
- * where: { status: 'queued' },
1275
- * },
1276
- * { $set: { status: 'in-progress', startedAt: new Date() } },
1277
- * );
1278
- * if (!transitioned) throw new ConcurrentEditError();
1279
- * ```
1280
- *
1281
- * @example Custom version field name + step
1282
- * ```ts
1283
- * await runRepo.claimVersion(
1284
- * runId,
1285
- * { field: 'rev', from: 12, by: 1 },
1286
- * { lastHeartbeat: new Date() }, // field-shape auto-wraps in $set
1287
- * );
1288
- * ```
1289
- */
1290
- claimVersion(id: string | ObjectId, transition: {
1291
- field?: string;
1292
- from: number | undefined;
1293
- by?: number;
1294
- where?: Record<string, unknown>;
1295
- }, update: Record<string, unknown>, options?: SessionOptions & {
1296
- idField?: string;
1297
- }): Promise<TDoc | null>;
1298
- /**
1299
- * Delete a document by ID.
1300
- *
1301
- * By default the behavior is decided by the plugin stack:
1302
- * - With `softDeletePlugin` wired → soft delete (sets `deletedAt`)
1303
- * - Without the plugin → physical delete
1304
- *
1305
- * Pass `mode: 'hard'` to force physical deletion regardless of plugins.
1306
- * All policy hooks (multi-tenant scope, audit trails, cache invalidation,
1307
- * cascade) still fire — only the soft-delete interception is skipped. This
1308
- * is the GDPR / admin-cleanup path.
1309
- *
1310
- * @example
1311
- * ```ts
1312
- * // Respects softDeletePlugin — soft if wired
1313
- * await userRepo.delete(userId);
1314
- *
1315
- * // GDPR erasure — physical delete, audit hooks still fire
1316
- * await userRepo.delete(userId, { mode: 'hard', organizationId: 'org_123' });
1317
- * ```
1318
- */
1319
- delete(id: string | ObjectId, options?: SessionOptions & {
1320
- idField?: string;
1321
- mode?: 'hard' | 'soft'; /** Legacy opt-in: throw a 404 error on miss instead of returning `null`. */
1322
- throwOnNotFound?: boolean;
1323
- }): Promise<DeleteResult | null>;
1324
- /**
1325
- * Update every document matching the filter.
1326
- *
1327
- * Promoted from `batchOperationsPlugin` to a class primitive in 3.11.0 so
1328
- * the surface matches sqlitekit's always-available `updateMany`. Consumers
1329
- * who forget to wire `batchOperationsPlugin` no longer get a runtime
1330
- * `TypeError: repo.updateMany is not a function` — the method is always
1331
- * here.
1332
- *
1333
- * Accepts three `update` forms via `UpdateInput`:
1334
- * - Portable `UpdateSpec` from `@classytic/repo-core/update` (compiled to
1335
- * `$set`/`$unset`/`$inc`/`$setOnInsert` before hooks run).
1336
- * - Raw Mongo operator record (`{ $set, $inc, ... }`) — passed through.
1337
- * - Mongo aggregation pipeline (`[...]`) — requires `updatePipeline: true`
1338
- * to guard against accidental pipeline updates.
1339
- *
1340
- * Refuses empty filters (defense-in-depth against mass-update accidents)
1341
- * both before and after policy hooks inject tenant scope.
1342
- */
1343
- updateMany(filter: Record<string, unknown>, update: UpdateInput, options?: SessionOptions & {
1344
- updatePipeline?: boolean;
1345
- [key: string]: unknown;
1346
- }): Promise<UpdateManyResult>;
1347
- /**
1348
- * Delete every document matching the filter.
1349
- *
1350
- * Promoted from `batchOperationsPlugin` to a class primitive in 3.11.0 for
1351
- * the same reason as `updateMany` — sqlitekit parity and no silent "method
1352
- * is undefined" footgun when the batch plugin isn't wired.
1353
- *
1354
- * Behavior mirrors `delete()`: defaults to soft delete when
1355
- * `softDeletePlugin` is wired, physical delete otherwise. Pass
1356
- * `{ mode: 'hard' }` to bypass soft-delete — multi-tenant scoping, audit,
1357
- * and cache hooks still fire.
1358
- *
1359
- * Rejects empty filters up front AND after policy hooks, so a policy-plugin
1360
- * bug that zeroes out the query can't be exploited to wipe a collection.
1361
- */
1362
- deleteMany(filter: Record<string, unknown>, options?: SessionOptions & {
1363
- mode?: 'hard' | 'soft';
1364
- [key: string]: unknown;
1365
- }): Promise<DeleteManyResult>;
1366
- /**
1367
- * Compliance-grade cleanup primitive — see `StandardRepo.purgeByField`
1368
- * for the cross-kit contract. Mongokit composes the kit-agnostic
1369
- * `runChunkedPurge` orchestrator (loop, signal, progress, result
1370
- * envelope) with `createMongoPurgePort` (driver-specific id selection
1371
- * + strategy writes). See [actions/purge.ts](./actions/purge.ts) for
1372
- * the port implementation.
1373
- */
1374
- purgeByField(field: string, value: unknown, strategy: TenantPurgeStrategy, options?: TenantPurgeOptions): Promise<TenantPurgeResult>;
1375
- /**
1376
- * Kit-native aggregation pipeline — takes a MongoDB stage array and
1377
- * returns the raw pipeline output. Use this for `$lookup`, `$unwind`,
1378
- * `$facet`, `$graphLookup`, and any other mongo-specific power
1379
- * features that don't translate to SQL.
1380
- *
1381
- * For backend-portable aggregations (filter + group + measures +
1382
- * sort + limit) use `aggregate(req: AggRequest)` instead — it
1383
- * compiles the repo-core IR to the equivalent pipeline internally
1384
- * and produces the same row shape sqlitekit emits.
1385
- *
1386
- * Routes through the hook system for policy enforcement
1387
- * (multi-tenant, soft-delete).
1388
- *
1389
- * @param pipeline - MongoDB aggregation stage array
1390
- * @param options - Aggregation options including governance controls
1391
- */
1392
- aggregatePipeline<TResult = unknown>(pipeline: PipelineStage[], options?: AggregateOptions): Promise<TResult[]>;
1393
- /**
1394
- * Paginated kit-native aggregation pipeline. Same mongo-specific
1395
- * scope as `aggregatePipeline` — reach for this when you need
1396
- * `$lookup` / `$unwind` / `$facet` alongside pagination.
1397
- *
1398
- * Prefer `aggregatePaginate(req: AggPaginationRequest)` for
1399
- * cross-backend dashboard code; that method compiles the portable
1400
- * IR to an equivalent pipeline.
1401
- *
1402
- * Policy hooks (multi-tenant, soft-delete) inject context.filters
1403
- * which are prepended as a `$match` stage.
1404
- */
1405
- aggregatePipelinePaginate(options?: AggregatePaginationOptions): Promise<AggregatePaginationResult<TDoc>>;
1406
- /**
1407
- * Portable aggregation. Compiles the repo-core `AggRequest` IR to
1408
- * `[{$match}, {$group}, {$addFields}, {$project}, {$match}, {$sort},
1409
- * {$skip}, {$limit}]` against this repo's collection. Output rows
1410
- * carry one key per `groupBy` column plus one key per measure alias
1411
- * — identical shape to sqlitekit's `aggregate(req)`, so dashboards
1412
- * work unchanged across backends.
1413
- *
1414
- * Without `groupBy`: returns a single-row result with just the
1415
- * measures (scalar aggregation). Pass
1416
- * `{ measures: { total: { op: 'sum', field: 'amount' } } }` for a
1417
- * simple summary.
1418
- *
1419
- * For mongo-specific power features (`$lookup`, `$unwind`, `$facet`,
1420
- * `$graphLookup`, window operators) reach for `aggregatePipeline`
1421
- * — that's the kit-native escape hatch.
1422
- *
1423
- * Routes through the hook system for policy enforcement
1424
- * (multi-tenant, soft-delete) — plugins inject their scope into
1425
- * `context.filter`, which is merged into the pre-aggregate `$match`
1426
- * before compilation.
1427
- */
1428
- aggregate<TRow extends Record<string, unknown> = Record<string, unknown>>(req: AggRequest, options?: QueryOptions): Promise<AggResult<TRow>>;
1429
- /**
1430
- * Invalidate every aggregate-cache entry tagged with ANY of the
1431
- * given tags. Delegates to the unified cache plugin's handle
1432
- * (`repo.cache.invalidateByTags`) — wired by `cachePlugin({ adapter })`.
1433
- *
1434
- * Pass no tags to wipe the entire cache namespace (requires the
1435
- * adapter to implement `clear`).
1436
- *
1437
- * Returns the count of distinct entries cleared (`-1` when the call
1438
- * routed through `adapter.clear()` and the adapter doesn't report a
1439
- * count). No-op (returns `0`) when no cache plugin is wired.
1440
- */
1441
- invalidateAggregateCache(tags?: readonly string[]): Promise<number>;
1442
- /**
1443
- * Paginated portable aggregation. Two pagination modes, picked by
1444
- * the request shape:
1445
- *
1446
- * - **Offset (default)** — `page` + `limit`. Returns the standard
1447
- * `{ method: 'offset', data, total, pages, hasNext, hasPrev, ... }`
1448
- * envelope. `countStrategy: 'none'` skips the second round-trip
1449
- * that computes `total`; the envelope reports `total: 0`,
1450
- * `pages: 0`, and derives `hasNext` from a `LIMIT N+1` peek on
1451
- * the data pipeline.
1452
- * - **Keyset** — `pagination: 'keyset'` (or `after` set). Returns
1453
- * `{ method: 'keyset', data, hasMore, next, limit }`. `sort` is
1454
- * required — the cursor encodes the sort-key tuple of the last
1455
- * row. Each subsequent page passes the previous `next` back as
1456
- * `after`. Scales to arbitrary group counts because the planner
1457
- * uses `(sort_keys) > (cursor)` instead of `OFFSET N`.
1458
- */
1459
- aggregatePaginate<TRow extends Record<string, unknown> = Record<string, unknown>>(req: AggPaginationRequest, options?: QueryOptions): Promise<OffsetPaginationResultCore<TRow> | KeysetAggPaginationResult<TRow>>;
1460
- /**
1461
- * Internal: the actual aggregate-paginate execution body. Extracted
1462
- * so `aggregatePaginate` can wrap it in the cache layer without
1463
- * duplicating the keyset / offset branch logic.
1464
- *
1465
- * Receives `finalReq` (already policy-scoped) so the cache key
1466
- * built by the outer wrapper matches the request the executor
1467
- * sees — no cross-tenant cache poisoning.
1468
- */
1469
- private _executeAggregatePaginate;
1470
- /**
1471
- * Merge policy-hook-injected filters (multi-tenant scope,
1472
- * soft-delete) into the pre-aggregate `filter` slot of the request.
1473
- * Plugins speak mongo-query shape (`context.query` / `context.filters`);
1474
- * the caller's `req.filter` may be either Filter IR or a mongo query.
1475
- *
1476
- * Each part is compiled through `compileFilterToMongo` BEFORE the
1477
- * `$and` merge — otherwise an IR node like `eq('status', 'paid')`
1478
- * (`{ op, field, value }`) gets nested inside `$and` as a literal,
1479
- * `compileFilterToMongo` of the merged value finds no top-level `op`
1480
- * (so `isFilter` is false), and Mongo receives the IR object as a
1481
- * raw query clause that matches no real document. Symptom: aggregates
1482
- * silently return 0 rows whenever a callerpasses IR + a policy
1483
- * plugin is installed.
1484
- */
1485
- private _injectPolicyScopeIntoAgg;
1486
- /**
1487
- * Get distinct values
1488
- * Routes through hook system for policy enforcement (multi-tenant, soft-delete)
1489
- */
1490
- distinct<T = unknown>(field: string, query?: Record<string, unknown>, options?: ReadOptions): Promise<T[]>;
1491
- /**
1492
- * Query with custom field lookups ($lookup)
1493
- * Best for: Joins on slugs, SKUs, codes, or other indexed custom fields
1494
- *
1495
- * @example
1496
- * ```typescript
1497
- * // Join employees with departments using slug instead of ObjectId
1498
- * const employees = await employeeRepo.lookupPopulate({
1499
- * filters: { status: 'active' },
1500
- * lookups: [
1501
- * {
1502
- * from: 'departments',
1503
- * localField: 'departmentSlug',
1504
- * foreignField: 'slug',
1505
- * as: 'department',
1506
- * single: true
1507
- * }
1508
- * ],
1509
- * sort: '-createdAt',
1510
- * page: 1,
1511
- * limit: 50
1512
- * });
1513
- * ```
1514
- */
1515
- lookupPopulate<TExtra extends Record<string, unknown> = Record<string, unknown>>(options: LookupPopulateOptions<TDoc>): Promise<LookupPopulateResult<TDoc, TExtra>>;
1516
- /**
1517
- * Create an aggregation builder for this model
1518
- * Useful for building complex custom aggregations
1519
- *
1520
- * @example
1521
- * ```typescript
1522
- * const pipeline = repo.buildAggregation()
1523
- * .match({ status: 'active' })
1524
- * .lookup('departments', 'deptSlug', 'slug', 'department', true)
1525
- * .group({ _id: '$department', count: { $sum: 1 } })
1526
- * .sort({ count: -1 })
1527
- * .build();
1528
- *
1529
- * const results = await repo.Model.aggregate(pipeline);
1530
- * ```
1531
- */
1532
- buildAggregation(): AggregationBuilder;
1533
- /**
1534
- * Create a lookup builder
1535
- * Useful for building $lookup stages independently
1536
- *
1537
- * @example
1538
- * ```typescript
1539
- * const lookupStages = repo.buildLookup('departments')
1540
- * .localField('deptSlug')
1541
- * .foreignField('slug')
1542
- * .as('department')
1543
- * .single()
1544
- * .build();
1545
- *
1546
- * const pipeline = [
1547
- * { $match: { status: 'active' } },
1548
- * ...lookupStages
1549
- * ];
1550
- * ```
1551
- */
1552
- buildLookup(from?: string): LookupBuilder;
1553
- /**
1554
- * Execute callback within a MongoDB transaction with automatic retry on
1555
- * transient failures.
1556
- *
1557
- * **BREAKING (3.10):** the callback now receives a **tx-bound repository**,
1558
- * not a raw `ClientSession`. This matches repo-core's `StandardRepo.withTransaction`
1559
- * contract that every kit (sqlitekit, pgkit, prismakit) already implements
1560
- * — so cross-kit plugins and apps are portable. Session threading happens
1561
- * transparently: every CRUD call on `txRepo` auto-injects the session into
1562
- * its options bag, including plugin-added methods (`upsert`, `increment`,
1563
- * `updateMany`, `restore`, ...). Nested `txRepo.withTransaction(...)` throws.
1564
- *
1565
- * For raw-session cross-repo workflows — multiple repositories coordinating
1566
- * writes — use the standalone `withTransaction(connection, fn)` export
1567
- * from `@classytic/mongokit`. That helper remains session-based by design.
1568
- *
1569
- * @param callback - Receives a tx-bound `Repository<TDoc>` — call its
1570
- * methods directly, no need to pass `{ session }` manually
1571
- * @param options.allowFallback - Run without transaction on standalone MongoDB (default: false)
1572
- * @param options.onFallback - Called when falling back to non-transactional execution
1573
- * @param options.transactionOptions - MongoDB driver transaction options (readConcern, writeConcern, etc.)
1574
- *
1575
- * @example
1576
- * ```typescript
1577
- * const order = await repo.withTransaction(async (txRepo) => {
1578
- * const created = await txRepo.create({ total: 100 });
1579
- * await txRepo.update(created._id, { confirmed: true });
1580
- * return created;
1581
- * });
1582
- *
1583
- * // With fallback for standalone/dev environments
1584
- * await repo.withTransaction(
1585
- * async (txRepo) => {
1586
- * await txRepo.create(doc);
1587
- * },
1588
- * {
1589
- * allowFallback: true,
1590
- * onFallback: (err) => logger.warn('Running without transaction', err),
1591
- * },
1592
- * );
1593
- *
1594
- * // Cross-repo with explicit session:
1595
- * import { withTransaction } from '@classytic/mongokit';
1596
- * await withTransaction(mongoose.connection, async (session) => {
1597
- * const order = await orderRepo.create(data, { session });
1598
- * await inventoryRepo.decrement(..., { session });
1599
- * });
1600
- * ```
1601
- */
1602
- withTransaction<T>(callback: (txRepo: this) => Promise<T>, options?: WithTransactionOptions): Promise<T>;
1603
- /**
1604
- * Execute custom query with event emission
1605
- */
1606
- _executeQuery<T>(buildQuery: (Model: Model<TDoc>) => Promise<T>): Promise<T>;
1607
- /**
1608
- * Build operation context and run before-hooks (sorted by priority).
1609
- *
1610
- * Narrows `RepositoryBase._buildContext`'s return type to mongokit's
1611
- * richer `RepositoryContext` (which carries `softDeleted`, `_cacheHit`,
1612
- * populate / readPreference / session, etc.). Runtime is inherited from
1613
- * the base class — this override is purely a type-narrowing shim.
1614
- *
1615
- * Hook execution order is deterministic:
1616
- * 1. POLICY (100) — tenant isolation, soft-delete filtering, validation
1617
- * 2. CACHE (200) — cache lookup (after policy filters are injected)
1618
- * 3. OBSERVABILITY (300) — audit logging, metrics
1619
- * 4. DEFAULT (500) — user-registered hooks
1620
- */
1621
- _buildContext(operation: string, options: Record<string, unknown> | object): Promise<RepositoryContext>;
1622
- /**
1623
- * Walk the well-known filter-carrying keys (`query`, `filters`,
1624
- * `having`) and route any Filter IR through `compileFilterToMongo`.
1625
- * Plain Mongo queries pass through untouched — `isFilter` is the
1626
- * discriminator. Returns a shallow clone; never mutates input.
1627
- */
1628
- private _normalizeFilterSlots;
1629
- /**
1630
- * Resolve the effective sort for getAll. Handles $near conflict detection
1631
- * (warns + drops explicit sort when $near is in filters).
1632
- */
1633
- private _resolveSort;
1634
- /**
1635
- * Detect whether to use keyset (cursor) or offset (page) pagination.
1636
- */
1637
- private _detectPaginationMode;
1638
- /**
1639
- * Build the MongoDB query filter with search merged in. Handles text
1640
- * search ($text), regex search ($or of $regex), and the search-resolver
1641
- * plugin contract (search already cleared by a before:getAll hook).
1642
- */
1643
- private _buildSearchQuery;
1644
- /**
1645
- * Parse sort string or object
1646
- */
1647
- _parseSort(sort: SortSpec | string | undefined): SortSpec;
1648
- /**
1649
- * Parse populate specification
1650
- */
1651
- _parsePopulate(populate: PopulateSpec | undefined): string[] | PopulateOptions[];
1652
- /**
1653
- * Classify a driver error as an authoritative duplicate-key conflict.
1654
- *
1655
- * Used by arc's outbox / idempotency adapters to distinguish
1656
- * "write already landed (idempotent no-op)" from "transient DB error
1657
- * (must retry)". Every backend signals duplicate keys differently —
1658
- * MongoDB `code: 11000`, Prisma `P2002`, Postgres `23505` — so this
1659
- * predicate lives on the kit that knows its driver. Cross-driver
1660
- * adapters then depend only on the boolean outcome, not the shape of
1661
- * the underlying error.
1662
- *
1663
- * Deliberately narrow: matches ONLY `code === 11000` and
1664
- * `codeName === 'DuplicateKey'`. Does NOT match
1665
- * `err.name === 'MongoServerError'`, which is also true for
1666
- * WriteConflict, NotWritablePrimary, ExceededTimeLimit, and every
1667
- * other server-side error — treating those as duplicate keys would
1668
- * silently swallow transactional retries.
1669
- *
1670
- * @example
1671
- * try {
1672
- * await outboxRepo.create(event);
1673
- * } catch (err) {
1674
- * if (outboxRepo.isDuplicateKeyError(err)) {
1675
- * // Idempotent no-op — event was already written by a prior attempt.
1676
- * return;
1677
- * }
1678
- * throw err; // transient DB error — upstream retries
1679
- * }
1680
- */
1681
- isDuplicateKeyError(err: unknown): boolean;
1682
- /**
1683
- * Handle errors with proper HTTP status codes.
1684
- *
1685
- * **Transactional retry signal preservation.** MongoDB driver errors
1686
- * carry `errorLabels` (e.g. `'TransientTransactionError'`,
1687
- * `'UnknownTransactionCommitResult'`) that `mongoose.Connection.
1688
- * withTransaction()` reads via `error.hasErrorLabel(label)` to decide
1689
- * whether to auto-retry the transaction. Wrapping such an error in a
1690
- * fresh `Error` via `createError(...)` strips both the labels and the
1691
- * `hasErrorLabel` method — the retry signal vanishes and concurrent
1692
- * writers see WriteConflicts surface to userland instead of being
1693
- * retried by the driver. To keep the standard MongoDB transaction-
1694
- * retry mechanism intact, we re-throw any error that carries
1695
- * `errorLabels` exactly as we received it. The caller still sees a
1696
- * thrown error — only the wrapping layer is skipped.
1697
- */
1698
- _handleError(error: Error): HttpError;
1699
- }
1700
- //#endregion
1701
- //#region src/create-repository.d.ts
1702
- /**
1703
- * Audit config — either a `Logger` (routes to `auditLogPlugin`, which
1704
- * writes structured log lines through your logger) or an
1705
- * `AuditTrailOptions` object (routes to `auditTrailPlugin`, which
1706
- * persists a queryable compliance ledger in its own collection).
1707
- *
1708
- * Discrimination: a value carrying any of the `Logger` function members
1709
- * (`info` / `warn` / `error` / `debug`) is treated as a logger; anything
1710
- * else is treated as `AuditTrailOptions` (all of whose members are
1711
- * data-shaped except `metadata` / `onError`, which don't overlap with
1712
- * the logger member names).
1713
- */
1714
- type AuditConfig = Logger | AuditTrailOptions;
1715
- /**
1716
- * Declarative repository config. Every key is optional; a feature's
1717
- * plugin is installed only when its key is present. The remaining
1718
- * `RepositoryOptions` keys (`idField`, `hooks`, `schema`, `updateSchema`,
1719
- * `events`, `requirePlugins`, ...) pass through to the `Repository`
1720
- * constructor unchanged.
1721
- */
1722
- interface CreateRepositoryConfig extends RepositoryOptions {
1723
- /** Multi-tenant scoping — forwarded to `multiTenantPlugin`. */
1724
- tenant?: MultiTenantOptions;
1725
- /** Soft-delete — `true` for defaults, or `SoftDeleteOptions`. */
1726
- softDelete?: boolean | SoftDeleteOptions;
1727
- /** createdAt/updatedAt stamping — `timestampPlugin` takes no options. */
1728
- timestamps?: boolean;
1729
- /** Batch ops (`updateMany`/`deleteMany`/`bulkWrite`) via `batchOperationsPlugin`. */
1730
- batch?: boolean;
1731
- /** Read-through cache — forwarded to `cachePlugin`. */
1732
- cache?: RepositoryCachePluginOptions;
1733
- /** Audit — `Logger` → `auditLogPlugin`; `AuditTrailOptions` → `auditTrailPlugin`. */
1734
- audit?: AuditConfig;
1735
- /** Custom ID generation — forwarded to `customIdPlugin`. */
1736
- customId?: CustomIdOptions;
1737
- /** Extra plugins appended AFTER the canonical stack. */
1738
- plugins?: PluginType[];
1739
- /** Pagination engine config (defaultLimit, maxLimit, ...). */
1740
- pagination?: PaginationConfig;
1741
- }
1742
- /**
1743
- * Build a `Repository` from a declarative config. See module JSDoc for
1744
- * ordering guarantees and an example.
1745
- */
1746
- declare function createRepository<TDoc>(model: Model<TDoc, any, any, any>, config?: CreateRepositoryConfig): Repository<TDoc>;
1747
- //#endregion
1748
94
  //#region src/filter/compile.d.ts
1749
95
  /**
1750
96
  * Filter IR → MongoDB query compiler.