@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.
- package/README.md +66 -0
- package/dist/PaginationEngine-4SjzfBQE.d.mts +103 -0
- package/dist/actions/index.d.mts +1 -1
- package/dist/adapter/index.mjs +1 -1
- package/dist/ai/index.d.mts +1 -1
- package/dist/better-auth/index.mjs +1 -1
- package/dist/create-repository-D5YX0wGs.mjs +29 -0
- package/dist/create-repository-K0yft2ht.d.mts +1663 -0
- package/dist/{index-fi91ycQc.d.mts → index-BgSBwlWu.d.mts} +1 -1
- package/dist/index.d.mts +11 -1665
- package/dist/index.mjs +5 -29
- package/dist/{mongooseToJsonSchema-BdQJU39w.d.mts → mongooseToJsonSchema-Bz47aMfH.d.mts} +1 -1
- package/dist/multi-tenant.plugin-D6lw3BsI.d.mts +487 -0
- package/dist/pagination/PaginationEngine.d.mts +1 -102
- package/dist/plugins/index.d.mts +3 -2
- package/dist/plugins/index.mjs +2 -1
- package/dist/query/primitives/coercion.d.mts +1 -1
- package/dist/query/primitives/indexes.d.mts +1 -1
- package/dist/testkit/index.d.mts +129 -0
- package/dist/testkit/index.mjs +139 -0
- package/dist/{validation-chain.plugin-DJtC0KJ-.mjs → timestamp.plugin-B1A9U-P7.mjs} +1 -1052
- package/dist/utils/index.d.mts +1 -1
- package/dist/utils/index.mjs +2 -2
- package/dist/{validation-chain.plugin-BpMfy18N.d.mts → validation-chain.plugin-CYOPfFGF.d.mts} +2 -486
- package/dist/validation-chain.plugin-DHZbpb4t.mjs +1056 -0
- package/package.json +9 -1
- /package/dist/{adapter-DYEMkLwK.mjs → adapter-BSxhT0Ip.mjs} +0 -0
- /package/dist/{coercion-TRMmR49h.d.mts → coercion-BxxHyLjP.d.mts} +0 -0
- /package/dist/{field-selection-CQW3WBiK.mjs → field-selection-d1qdRiMr.mjs} +0 -0
- /package/dist/{indexes-CgOF7Wjd.d.mts → indexes-3eHBWNZ2.d.mts} +0 -0
- /package/dist/{mongooseToJsonSchema-CBEQmeEa.mjs → mongooseToJsonSchema-ChtXSrLT.mjs} +0 -0
- /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,
|
|
2
|
-
import { t as index_d_exports } from "./index-
|
|
3
|
-
import {
|
|
4
|
-
import { PaginationEngine } from "./
|
|
5
|
-
import { n as
|
|
6
|
-
import { n as
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
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.
|