@classytic/mongokit 3.14.0 → 3.16.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 +193 -319
- package/dist/{PaginationEngine-CfQRM1yz.mjs → PaginationEngine-0XXTW-9b.mjs} +2 -2
- package/dist/{Repository-BGL1Pq8w.mjs → Repository-DE7D2QeS.mjs} +240 -19
- package/dist/actions/index.d.mts +1 -1
- package/dist/ai/index.d.mts +1 -1
- package/dist/ai/index.mjs +14 -5
- package/dist/better-auth/index.d.mts +55 -1
- package/dist/better-auth/index.mjs +32 -2
- package/dist/{index-Dccha9dw.d.mts → index-CV5vv-8E.d.mts} +1 -1
- package/dist/index.d.mts +1182 -1079
- package/dist/index.mjs +32 -19
- package/dist/{mongooseToJsonSchema-BYbG894y.mjs → mongooseToJsonSchema-Bi0q0pYW.mjs} +1 -1
- package/dist/{mongooseToJsonSchema-BgRAaXX-.d.mts → mongooseToJsonSchema-GhvZUjRO.d.mts} +1 -1
- package/dist/pagination/PaginationEngine.d.mts +1 -1
- package/dist/pagination/PaginationEngine.mjs +1 -1
- package/dist/plugins/index.d.mts +1 -1
- package/dist/plugins/index.mjs +1 -1
- package/dist/query/primitives/coercion.mjs +1 -1
- package/dist/{types-BbaPZleN.d.mts → types-BWZILovv.d.mts} +26 -1
- package/dist/utils/index.d.mts +1 -1
- package/dist/utils/index.mjs +3 -3
- package/dist/{validation-chain.plugin-CWhvBRej.mjs → validation-chain.plugin-Crl5nQAY.mjs} +1172 -1137
- package/dist/{validation-chain.plugin-CAPYCj_T.d.mts → validation-chain.plugin-Iee2_puD.d.mts} +368 -344
- package/package.json +6 -6
- /package/dist/{cursor-J84rR6wG.mjs → cursor-4LMYa4ga.mjs} +0 -0
- /package/dist/{field-selection-Br3WPOEb.mjs → field-selection-CQW3WBiK.mjs} +0 -0
- /package/dist/{id-resolution-B0Odvvou.mjs → id-resolution-DduoO4LI.mjs} +0 -0
- /package/dist/{sort-CrZjTK25.mjs → sort-RPVz5u0w.mjs} +0 -0
package/dist/index.d.mts
CHANGED
|
@@ -1,75 +1,19 @@
|
|
|
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-
|
|
2
|
-
import { t as index_d_exports } from "./index-
|
|
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-BWZILovv.mjs";
|
|
2
|
+
import { t as index_d_exports } from "./index-CV5vv-8E.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-Iee2_puD.mjs";
|
|
3
4
|
import { PaginationEngine } from "./pagination/PaginationEngine.mjs";
|
|
4
|
-
import { $ as AuditQueryResult, A as elasticSearchPlugin, B as sequentialId, D as leasePlugin, E as LeasePluginOptions, F as SequentialIdOptions, G as RepositoryCachePluginOptions, H as CacheAdapter, I as customIdPlugin, J as BulkWriteResult, K as cachePlugin, L as dateSequentialId, M as DateSequentialIdOptions, N as IdGenerator, O as fieldFilterPlugin, P as PrefixedIdOptions, Q as AuditQueryOptions, R as getNextSequence, S as mongoOperationsPlugin, T as LeaseMethods, U as CacheOptions, V as cascadePlugin, W as RepositoryCacheHandle, X as AuditEntry, Y as batchOperationsPlugin, Z as AuditOperation, _ as observabilityPlugin, a as uniqueField, at as AggregateHelpersMethods, b as multiTenantPlugin, c as TenantContext, d as SubdocumentMethods, et as AuditTrailMethods, f as subdocumentPlugin, g as OperationMetric, h as ObservabilityOptions, i as requireField, it as auditLogPlugin, j as CustomIdOptions, k as ElasticSearchOptions, l as TenantStore, m as softDeletePlugin, n as blockIf, nt as AuditTrailQuery, o as validationChainPlugin, ot as aggregateHelpersPlugin, p as SoftDeleteMethods, q as BatchOperationsMethods, r as immutableField, rt as auditTrailPlugin, s as timestampPlugin, t as autoInject, tt as AuditTrailOptions, u as createTenantContext, v as MultiTenantOptions, w as methodRegistryPlugin, x as MongoOperationsMethods, y as adminBypass, z as prefixedId } from "./validation-chain.plugin-CAPYCj_T.mjs";
|
|
5
5
|
import { n as SchemaPathsLike, t as FieldType$1 } from "./coercion-TRMmR49h.mjs";
|
|
6
6
|
import { n as SchemaIndexes } from "./indexes-CgOF7Wjd.mjs";
|
|
7
|
-
import { a as createFieldPreset, c as getMongooseProjection, d as parseDuplicateKeyError, i as configureLogger, l as createError, n as buildCrudSchemasFromMongooseSchema, o as filterResponseData, r as createMemoryCache, s as getFieldsForUser, t as buildCrudSchemasFromModel, u as isDuplicateKeyError } from "./mongooseToJsonSchema-
|
|
7
|
+
import { a as createFieldPreset, c as getMongooseProjection, d as parseDuplicateKeyError, i as configureLogger, l as createError, n as buildCrudSchemasFromMongooseSchema, o as filterResponseData, r as createMemoryCache, s as getFieldsForUser, t as buildCrudSchemasFromModel, u as isDuplicateKeyError } from "./mongooseToJsonSchema-GhvZUjRO.mjs";
|
|
8
8
|
import * as _$mongoose from "mongoose";
|
|
9
9
|
import { ClientSession, Expression, Model, PipelineStage, PopulateOptions } from "mongoose";
|
|
10
|
-
import { AggPaginationRequest, AggRequest, AggResult, KeysetAggPaginationResult, QueryOptions, RepositoryBase, TenantPurgeOptions, TenantPurgeResult, TenantPurgeStrategy } from "@classytic/repo-core/repository";
|
|
10
|
+
import { AggPaginationRequest, AggRequest, AggResult, ChangeEvent, FilterInput, KeysetAggPaginationResult, QueryOptions, RepoCapabilities, RepositoryBase, TenantPurgeOptions, TenantPurgeResult, TenantPurgeStrategy, WatchOptions } from "@classytic/repo-core/repository";
|
|
11
11
|
import { UpdateInput } from "@classytic/repo-core/update";
|
|
12
12
|
import { AggregatePaginationResult, KeysetPaginationResult, OffsetPaginationResult, OffsetPaginationResultCore } from "@classytic/repo-core/pagination";
|
|
13
13
|
import { HttpError } from "@classytic/repo-core/errors";
|
|
14
14
|
|
|
15
|
-
//#region src/
|
|
16
|
-
|
|
17
|
-
* Filter IR → MongoDB query compiler.
|
|
18
|
-
*
|
|
19
|
-
* Mirrors sqlitekit's `compileFilterToDrizzle`: takes a repo-core
|
|
20
|
-
* `Filter` tree and emits a MongoDB query object. Used by `groupBy` /
|
|
21
|
-
* `groupByPaginate` to translate the portable AggRequest's `filter`
|
|
22
|
-
* + `having` into `$match` stages.
|
|
23
|
-
*
|
|
24
|
-
* Scope: the same operator set sqlitekit supports (`eq`, `ne`, `gt`,
|
|
25
|
-
* `gte`, `lt`, `lte`, `in`, `nin`, `exists`, `like`, `regex`, `and`,
|
|
26
|
-
* `or`, `not`, `raw`). Anything else throws so we fail loud instead
|
|
27
|
-
* of silently dropping predicates.
|
|
28
|
-
*
|
|
29
|
-
* Why only this op set here and not everywhere in mongokit? Historic
|
|
30
|
-
* reason: mongokit pre-dates repo-core's Filter IR, so its existing
|
|
31
|
-
* policy plugins (multi-tenant, soft-delete) speak MongoDB query
|
|
32
|
-
* language directly. Converting mongokit's entire filter pipeline
|
|
33
|
-
* to Filter IR is out-of-scope for this change — we introduce the
|
|
34
|
-
* compiler only where the new portable `groupBy` IR needs it.
|
|
35
|
-
*/
|
|
36
|
-
declare function compileFilterToMongo(input: unknown): Record<string, unknown>;
|
|
37
|
-
//#endregion
|
|
38
|
-
//#region src/operations.d.ts
|
|
39
|
-
/**
|
|
40
|
-
* Where a plugin should inject its scoping filter (multi-tenant scope,
|
|
41
|
-
* soft-delete filter, etc.) on the RepositoryContext.
|
|
42
|
-
*
|
|
43
|
-
* - `data` — single-doc create payload (`context.data`)
|
|
44
|
-
* - `dataArray` — multi-doc create payload (`context.dataArray`)
|
|
45
|
-
* - `query` — raw filter (`context.query`); the dominant convention
|
|
46
|
-
* - `filters` — paginated list options' filter sub-bag (`context.filters`)
|
|
47
|
-
* - `operations` — bulkWrite per-sub-op (special — plugins walk each entry)
|
|
48
|
-
* - `none` — no scoping target (op accepts no filter input)
|
|
49
|
-
*/
|
|
50
|
-
type PolicyKey = 'data' | 'dataArray' | 'query' | 'filters' | 'operations' | 'none';
|
|
51
|
-
interface OperationDescriptor {
|
|
52
|
-
/** Where multi-tenant / soft-delete plugins inject their scoping filter. */
|
|
53
|
-
policyKey: PolicyKey;
|
|
54
|
-
/** Whether this op writes to the database. Drives audit + cache invalidation. */
|
|
55
|
-
mutates: boolean;
|
|
56
|
-
/** True when `context.id` is populated by the time before/after hooks fire. */
|
|
57
|
-
hasIdContext: boolean;
|
|
58
|
-
}
|
|
59
|
-
/**
|
|
60
|
-
* The registry. Order intentionally groups by category for readability.
|
|
61
|
-
* Adding a new RepositoryOperation requires exactly one entry here, after
|
|
62
|
-
* which every bundled plugin auto-classifies it correctly.
|
|
63
|
-
*/
|
|
64
|
-
declare const OP_REGISTRY: Readonly<Record<RepositoryOperation, OperationDescriptor>>;
|
|
65
|
-
/** All known repository operations, in registry order. */
|
|
66
|
-
declare const ALL_OPERATIONS: RepositoryOperation[];
|
|
67
|
-
/** Operations that mutate the database. Drives audit + cache invalidation lists. */
|
|
68
|
-
declare const MUTATING_OPERATIONS: readonly RepositoryOperation[];
|
|
69
|
-
/** Operations that don't mutate. Drives default cacheable ops. */
|
|
70
|
-
declare const READ_OPERATIONS: readonly RepositoryOperation[];
|
|
71
|
-
/** Filter ops by their policy injection key. */
|
|
72
|
-
declare function operationsByPolicyKey(key: PolicyKey): RepositoryOperation[];
|
|
15
|
+
//#region src/capabilities.d.ts
|
|
16
|
+
declare const MONGOKIT_CAPABILITIES: RepoCapabilities;
|
|
73
17
|
//#endregion
|
|
74
18
|
//#region src/query/AggregationBuilder.d.ts
|
|
75
19
|
/** Options for $vectorSearch stage (Atlas only) */
|
|
@@ -492,745 +436,407 @@ declare class AggregationBuilder {
|
|
|
492
436
|
static startWith(query: Record<string, unknown>): AggregationBuilder;
|
|
493
437
|
}
|
|
494
438
|
//#endregion
|
|
495
|
-
//#region src/
|
|
496
|
-
type SortSpec$1 = Record<string, 1 | -1>;
|
|
497
|
-
type FilterQuery = Record<string, unknown>;
|
|
439
|
+
//#region src/Repository.d.ts
|
|
498
440
|
/**
|
|
499
|
-
*
|
|
500
|
-
* Supports advanced populate with select, match, limit, sort, and nested populate
|
|
441
|
+
* Plugin phase priorities (lower = runs first).
|
|
501
442
|
*
|
|
502
|
-
*
|
|
503
|
-
*
|
|
504
|
-
*
|
|
505
|
-
*
|
|
506
|
-
* ```
|
|
507
|
-
*/
|
|
508
|
-
interface PopulateOption {
|
|
509
|
-
/** Field path to populate */
|
|
510
|
-
path: string;
|
|
511
|
-
/** Fields to select (space-separated) */
|
|
512
|
-
select?: string;
|
|
513
|
-
/** Filter conditions for populated documents */
|
|
514
|
-
match?: Record<string, unknown>;
|
|
515
|
-
/** Query options (limit, sort, skip) */
|
|
516
|
-
options?: {
|
|
517
|
-
limit?: number;
|
|
518
|
-
sort?: SortSpec$1;
|
|
519
|
-
skip?: number;
|
|
520
|
-
};
|
|
521
|
-
/** Nested populate configuration */
|
|
522
|
-
populate?: PopulateOption;
|
|
523
|
-
}
|
|
524
|
-
/** Parsed query result with optional lookup configuration */
|
|
525
|
-
interface ParsedQuery {
|
|
526
|
-
/** MongoDB filter query */
|
|
527
|
-
filters: FilterQuery;
|
|
528
|
-
/** Sort specification */
|
|
529
|
-
sort?: SortSpec$1;
|
|
530
|
-
/** Fields to populate (simple comma-separated string) */
|
|
531
|
-
populate?: string;
|
|
532
|
-
/**
|
|
533
|
-
* Advanced populate options (Mongoose-compatible)
|
|
534
|
-
* When this is set, `populate` will be undefined
|
|
535
|
-
* @example [{ path: 'author', select: 'name email' }]
|
|
536
|
-
*/
|
|
537
|
-
populateOptions?: PopulateOption[];
|
|
538
|
-
/** Page number for offset pagination */
|
|
539
|
-
page?: number;
|
|
540
|
-
/** Cursor for keyset pagination */
|
|
541
|
-
after?: string;
|
|
542
|
-
/** Limit */
|
|
543
|
-
limit: number;
|
|
544
|
-
/** Full-text search query */
|
|
545
|
-
search?: string;
|
|
546
|
-
/** Lookup configurations for custom field joins */
|
|
547
|
-
lookups?: LookupOptions[];
|
|
548
|
-
/** Aggregation pipeline stages (advanced) */
|
|
549
|
-
aggregation?: PipelineStage[];
|
|
550
|
-
/** Select/project fields */
|
|
551
|
-
select?: Record<string, 0 | 1>;
|
|
552
|
-
}
|
|
553
|
-
/** Search mode for query parser */
|
|
554
|
-
type SearchMode = 'text' | 'regex';
|
|
555
|
-
/**
|
|
556
|
-
* Normalized field type used for schema-aware value coercion. Re-exported
|
|
557
|
-
* from `primitives/coercion` so the public QueryParser API stays unchanged
|
|
558
|
-
* while the actual logic lives in a single, unit-tested primitive module.
|
|
443
|
+
* Re-exported from `@classytic/repo-core/hooks` so mongokit 3.10 callers see
|
|
444
|
+
* the same constant whether they import from mongokit or directly from
|
|
445
|
+
* repo-core — priorities are identical (POLICY=100, CACHE=200,
|
|
446
|
+
* OBSERVABILITY=300, DEFAULT=500) and the two refs are reference-equal.
|
|
559
447
|
*/
|
|
560
|
-
|
|
448
|
+
declare const HOOK_PRIORITY: {
|
|
449
|
+
readonly POLICY: 100;
|
|
450
|
+
readonly VALIDATION: 150;
|
|
451
|
+
readonly CACHE: 200;
|
|
452
|
+
readonly OBSERVABILITY: 300;
|
|
453
|
+
readonly DEFAULT: 500;
|
|
454
|
+
};
|
|
561
455
|
/**
|
|
562
|
-
*
|
|
563
|
-
*
|
|
564
|
-
*
|
|
565
|
-
*
|
|
456
|
+
* Production-grade repository for MongoDB.
|
|
457
|
+
*
|
|
458
|
+
* Extends `@classytic/repo-core/repository`'s `RepositoryBase` for the
|
|
459
|
+
* driver-agnostic hook + plugin plumbing (context builder, priority-sorted
|
|
460
|
+
* event engine, plugin-order validator) while layering every Mongo-specific
|
|
461
|
+
* concern — populate, aggregate, lookup, keyset + cursor pagination, $near
|
|
462
|
+
* rewriting — on top.
|
|
463
|
+
*
|
|
464
|
+
* Public surface is byte-stable with mongokit 3.9 (the `_hooks` property is
|
|
465
|
+
* preserved as a live read-through getter; all CRUD, event, and plugin method
|
|
466
|
+
* signatures unchanged). 3.10 is an internal re-plumbing release; consumers
|
|
467
|
+
* upgrading from 3.9 need no code changes.
|
|
566
468
|
*/
|
|
567
|
-
|
|
568
|
-
/** Optional — when present, schema indexes are introspected for geo/text fields */
|
|
569
|
-
indexes?: () => Array<[Record<string, unknown>, Record<string, unknown>?]>;
|
|
570
|
-
}
|
|
571
|
-
interface QueryParserOptions {
|
|
572
|
-
/** Maximum allowed regex pattern length (default: 500) */
|
|
573
|
-
maxRegexLength?: number;
|
|
574
|
-
/** Maximum allowed text search query length (default: 200) */
|
|
575
|
-
maxSearchLength?: number;
|
|
576
|
-
/** Maximum allowed filter depth (default: 10) */
|
|
577
|
-
maxFilterDepth?: number;
|
|
578
|
-
/** Maximum allowed limit value (default: 1000) */
|
|
579
|
-
maxLimit?: number;
|
|
580
|
-
/** Additional operators to block */
|
|
581
|
-
additionalDangerousOperators?: string[];
|
|
582
|
-
/** Enable lookup parsing (default: true) */
|
|
583
|
-
enableLookups?: boolean;
|
|
584
|
-
/** Enable aggregation parsing (default: false - requires explicit opt-in) */
|
|
585
|
-
enableAggregations?: boolean;
|
|
469
|
+
declare class Repository<TDoc = unknown> extends RepositoryBase {
|
|
586
470
|
/**
|
|
587
|
-
*
|
|
588
|
-
* -
|
|
589
|
-
*
|
|
471
|
+
* Runtime capability descriptor required by `StandardRepo<TDoc>`
|
|
472
|
+
* (repo-core 0.6.0). Hosts feature-detect once at boot instead of
|
|
473
|
+
* try/catching `UnsupportedOperationError` per call.
|
|
590
474
|
*/
|
|
591
|
-
|
|
475
|
+
readonly capabilities: RepoCapabilities;
|
|
476
|
+
readonly Model: Model<TDoc>;
|
|
592
477
|
/**
|
|
593
|
-
*
|
|
594
|
-
*
|
|
595
|
-
*
|
|
478
|
+
* Mongoose model name. Duplicated on the instance for BC — arc / catalog /
|
|
479
|
+
* user code read `repo.model`. `RepositoryBase.modelName` points to the
|
|
480
|
+
* same string; prefer `modelName` in new code.
|
|
596
481
|
*/
|
|
597
|
-
|
|
482
|
+
readonly model: string;
|
|
483
|
+
readonly _pagination: PaginationEngine<TDoc>;
|
|
484
|
+
readonly idField: string;
|
|
485
|
+
readonly searchMode: 'text' | 'regex' | 'auto';
|
|
486
|
+
readonly searchFields: string[] | undefined;
|
|
487
|
+
[key: string]: unknown;
|
|
488
|
+
private _hasTextIndex;
|
|
598
489
|
/**
|
|
599
|
-
*
|
|
600
|
-
*
|
|
601
|
-
*
|
|
602
|
-
*
|
|
490
|
+
* Wrap-style middleware chain. Composed around every `_runOp`
|
|
491
|
+
* invocation in registration order — earlier-registered middlewares
|
|
492
|
+
* are outer (run first / unwind last). See `useMiddleware()` and
|
|
493
|
+
* the `Middleware<TDoc>` type for the protocol.
|
|
603
494
|
*/
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
allowedFilterFields?: string[];
|
|
607
|
-
/** Allowed fields for sorting. If set, ignores unknown fields. */
|
|
608
|
-
allowedSortFields?: string[];
|
|
495
|
+
private readonly _middlewares;
|
|
496
|
+
constructor(Model: Model<TDoc, any, any, any>, plugins?: PluginType[], paginationConfig?: PaginationConfig, options?: RepositoryOptions);
|
|
609
497
|
/**
|
|
610
|
-
*
|
|
611
|
-
*
|
|
612
|
-
*
|
|
613
|
-
*
|
|
614
|
-
* '
|
|
615
|
-
*
|
|
498
|
+
* Live read-through view of the hook engine's listener registry.
|
|
499
|
+
*
|
|
500
|
+
* Preserved as a public field on the Repository for back-compat with
|
|
501
|
+
* mongokit ≤3.9 — existing tests and observability code pattern-match
|
|
502
|
+
* `repo._hooks.get('before:getAll')` / `repo._hooks.size`. Each access
|
|
503
|
+
* returns a fresh snapshot of the underlying HookEngine state.
|
|
504
|
+
*
|
|
505
|
+
* Read-only. To register listeners use `repo.on(event, listener, { priority })`.
|
|
616
506
|
*/
|
|
617
|
-
|
|
507
|
+
get _hooks(): Map<string, PrioritizedHook[]>;
|
|
618
508
|
/**
|
|
619
|
-
*
|
|
620
|
-
*
|
|
621
|
-
*
|
|
622
|
-
*
|
|
623
|
-
* `?name=12345` stays a string against a `String` field. Nested paths like
|
|
624
|
-
* `address.zip` are looked up via dot-notation, matching Mongoose's
|
|
625
|
-
* `schema.paths` reflection. Unknown fields fall through to the heuristic.
|
|
509
|
+
* Fire-and-forget emit aliased through RepositoryBase's async path so
|
|
510
|
+
* error hooks converted to/from sync mode still route uniformly. Mongokit
|
|
511
|
+
* ≤3.9 made this a private method on its subclass; the base class exposes
|
|
512
|
+
* it publicly and wraps it in `_emitHook` below for internal use.
|
|
626
513
|
*/
|
|
627
|
-
|
|
514
|
+
private _emitHook;
|
|
515
|
+
private _emitErrorHook;
|
|
628
516
|
/**
|
|
629
|
-
*
|
|
630
|
-
*
|
|
631
|
-
*
|
|
632
|
-
*
|
|
633
|
-
*
|
|
634
|
-
*
|
|
517
|
+
* Wire the `warnOnStrictQueryStrip` diagnostic. When enabled, every
|
|
518
|
+
* filter-shaped op (`getById`, `getByQuery`, `getOne`, `findAll`,
|
|
519
|
+
* `count`, `exists`, `update`, `delete`, `findOneAndUpdate`,
|
|
520
|
+
* `updateMany`, `deleteMany`, `claim`, `claimVersion`, `cursor`)
|
|
521
|
+
* checks its `context.query` keys against the cached set of schema
|
|
522
|
+
* paths. Top-level keys not on the schema get logged once per
|
|
523
|
+
* `(model, key)` pair — so a single bad caller surfaces in dev /
|
|
524
|
+
* staging instead of returning the wrong row in prod.
|
|
525
|
+
*
|
|
526
|
+
* Only runs when `strictQuery !== false` is in effect — if mongoose's
|
|
527
|
+
* strictQuery is off, unknown keys flow through to the driver and
|
|
528
|
+
* the trap doesn't apply.
|
|
635
529
|
*/
|
|
636
|
-
|
|
637
|
-
}
|
|
638
|
-
/**
|
|
639
|
-
* Modern Query Parser
|
|
640
|
-
* Converts URL parameters to MongoDB queries with $lookup support
|
|
641
|
-
*/
|
|
642
|
-
declare class QueryParser {
|
|
643
|
-
private readonly options;
|
|
644
|
-
private readonly operators;
|
|
645
|
-
private readonly dangerousOperators;
|
|
646
|
-
/**
|
|
647
|
-
* Normalized dot-notation field-type map. Built once at construction from
|
|
648
|
-
* `options.schema` (Mongoose) and `options.fieldTypes` (override map), with
|
|
649
|
-
* `fieldTypes` taking precedence. An empty map means schema-aware coercion
|
|
650
|
-
* is disabled and the heuristic in `_convertValue` runs unchanged — this
|
|
651
|
-
* preserves backwards compatibility for callers that don't opt in.
|
|
652
|
-
*/
|
|
653
|
-
private readonly _fieldTypes;
|
|
654
|
-
/**
|
|
655
|
-
* Structured schema-index info (geo / text / other), built once from
|
|
656
|
-
* `options.schema?.indexes()`. Used by the operator router to validate
|
|
657
|
-
* geo operators against actual geo-indexed fields, and exposed publicly
|
|
658
|
-
* via `parser.schemaIndexes` for downstream tools (Arc MCP, query planners).
|
|
659
|
-
*/
|
|
660
|
-
private readonly _schemaIndexes;
|
|
530
|
+
private _wireStrictQueryStripDiagnostic;
|
|
661
531
|
/**
|
|
662
|
-
*
|
|
663
|
-
*
|
|
664
|
-
*
|
|
665
|
-
*
|
|
666
|
-
* - Nested quantifiers: (a+)+, (a*)*
|
|
667
|
-
* - Backreferences: \1, \2, etc.
|
|
668
|
-
* - Complex character classes: [...]...[...]
|
|
532
|
+
* Register a wrap-style middleware. Middleware composes AROUND every
|
|
533
|
+
* `_runOp` invocation — it sees the operation name, the live
|
|
534
|
+
* `RepositoryContext`, and a `next()` continuation that runs the
|
|
535
|
+
* remaining chain + the actual op + after/error hooks.
|
|
669
536
|
*
|
|
670
|
-
*
|
|
671
|
-
*
|
|
672
|
-
*
|
|
673
|
-
*
|
|
674
|
-
*
|
|
675
|
-
|
|
676
|
-
private readonly dangerousRegexPatterns;
|
|
677
|
-
/**
|
|
678
|
-
* Static complexity budget for user-supplied regex patterns.
|
|
679
|
-
* Returns `true` if the pattern crosses a budget that empirically correlates
|
|
680
|
-
* with ReDoS risk — independent of the heuristic regex detector.
|
|
537
|
+
* **Composes with `repo.on()` and plugins, doesn't replace them.**
|
|
538
|
+
* Middleware is for ergonomics (timing, short-circuit, input/output
|
|
539
|
+
* mutation in a single closure) — NOT for security policy. Use the
|
|
540
|
+
* `before:*` hook engine for tenant scope, soft-delete filtering,
|
|
541
|
+
* cache invalidation, and audit; those plugins MUST run before
|
|
542
|
+
* middleware sees the op.
|
|
681
543
|
*
|
|
682
|
-
*
|
|
683
|
-
*
|
|
684
|
-
*
|
|
685
|
-
* - alternations `|` — hard cap 10
|
|
686
|
-
* - combined: (quantifiers * groups) — hard cap 40
|
|
544
|
+
* **Execution order on every call** (build/before hooks fire BEFORE
|
|
545
|
+
* the middleware chain dispatches, so middleware cannot wrap a
|
|
546
|
+
* before-hook failure):
|
|
687
547
|
*
|
|
688
|
-
*
|
|
689
|
-
*
|
|
690
|
-
*
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
*
|
|
696
|
-
*
|
|
697
|
-
* provided. Stable across the parser's lifetime; mutating the returned
|
|
698
|
-
* object does not affect parser behavior.
|
|
548
|
+
* ```text
|
|
549
|
+
* _buildContext + before:<op> ← repo.on('before:*') hooks
|
|
550
|
+
* [outer middleware pre] ← repo.useMiddleware() registrations
|
|
551
|
+
* [...inner middleware pre]
|
|
552
|
+
* fn (driver call)
|
|
553
|
+
* after:<op> | error:<op> ← repo.on('after:*' / 'error:*')
|
|
554
|
+
* [...inner middleware post]
|
|
555
|
+
* [outer middleware post]
|
|
556
|
+
* ```
|
|
699
557
|
*
|
|
700
|
-
*
|
|
701
|
-
*
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
*
|
|
706
|
-
*
|
|
558
|
+
* Why this order matters: a `before:create` hook that injects a
|
|
559
|
+
* tenant filter into `context.query` runs BEFORE the middleware chain
|
|
560
|
+
* is composed. Middleware sees a context that was already scoped by
|
|
561
|
+
* the policy plugins — it never has the authority to short-circuit a
|
|
562
|
+
* tenant check, because the throw from a `before:*` policy hook
|
|
563
|
+
* unwinds before middleware ever fires. That's by design — middleware
|
|
564
|
+
* as a security boundary would be impossible to audit, since
|
|
565
|
+
* registration order would determine whether scope wins.
|
|
707
566
|
*
|
|
708
|
-
*
|
|
709
|
-
*
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
/**
|
|
713
|
-
* Get the configured allowed sort fields.
|
|
714
|
-
* Returns `undefined` if no whitelist is set (all fields allowed).
|
|
715
|
-
*/
|
|
716
|
-
get allowedSortFields(): string[] | undefined;
|
|
717
|
-
/**
|
|
718
|
-
* Get the configured allowed operators.
|
|
719
|
-
* Returns `undefined` if no whitelist is set (all built-in operators allowed).
|
|
720
|
-
*/
|
|
721
|
-
get allowedOperators(): string[] | undefined;
|
|
722
|
-
/**
|
|
723
|
-
* Parse URL query parameters into MongoDB query format
|
|
567
|
+
* Practical consequence: if you want middleware to observe a policy
|
|
568
|
+
* failure (e.g. metric a tenant-scope rejection), use `repo.on('error:
|
|
569
|
+
* <op>', ...)` instead — that fires from inside the middleware chain
|
|
570
|
+
* and is reachable.
|
|
724
571
|
*
|
|
725
|
-
*
|
|
726
|
-
*
|
|
727
|
-
*
|
|
728
|
-
*
|
|
729
|
-
*
|
|
572
|
+
* Registration order = composition order: the first middleware
|
|
573
|
+
* registered runs outermost (wraps everything else).
|
|
574
|
+
*
|
|
575
|
+
* @example Time every op
|
|
576
|
+
* ```ts
|
|
577
|
+
* repo.useMiddleware(async ({ operation, next }) => {
|
|
578
|
+
* const start = performance.now();
|
|
579
|
+
* try { return await next(); }
|
|
580
|
+
* finally { metrics.record(operation, performance.now() - start); }
|
|
581
|
+
* });
|
|
582
|
+
* ```
|
|
583
|
+
*
|
|
584
|
+
* @example Inject `tenantId` on every create
|
|
585
|
+
* ```ts
|
|
586
|
+
* repo.useMiddleware(async ({ operation, context, next }) => {
|
|
587
|
+
* if (operation === 'create' && context.data) {
|
|
588
|
+
* context.data.tenantId = currentTenant();
|
|
589
|
+
* }
|
|
590
|
+
* return next();
|
|
591
|
+
* });
|
|
592
|
+
* ```
|
|
593
|
+
*
|
|
594
|
+
* @example Short-circuit (return without `next()` to skip the actual op)
|
|
595
|
+
* ```ts
|
|
596
|
+
* repo.useMiddleware(async ({ operation, context, next }) => {
|
|
597
|
+
* if (operation === 'getById' && readOnlyMaintenance) {
|
|
598
|
+
* return cachedReadOnlyResponse(context.id);
|
|
599
|
+
* }
|
|
600
|
+
* return next();
|
|
601
|
+
* });
|
|
730
602
|
* ```
|
|
731
603
|
*/
|
|
732
|
-
|
|
604
|
+
useMiddleware(middleware: Middleware<TDoc>): this;
|
|
733
605
|
/**
|
|
734
|
-
*
|
|
735
|
-
*
|
|
736
|
-
*
|
|
606
|
+
* Compose the registered middleware chain around `exec`. Outermost
|
|
607
|
+
* middleware (first registered) wraps innermost. Each middleware sees
|
|
608
|
+
* the live `RepositoryContext` (already populated + run through
|
|
609
|
+
* `before:*` hooks at the call site — see `useMiddleware` JSDoc for
|
|
610
|
+
* the exact order). `after:` / `error:` hooks fire from inside
|
|
611
|
+
* `exec`, so middleware composes WITH plugins, not instead of them.
|
|
737
612
|
*
|
|
738
|
-
*
|
|
739
|
-
*
|
|
740
|
-
*
|
|
741
|
-
*
|
|
742
|
-
*
|
|
613
|
+
* `_runOp` calls this with its standard try/catch envelope; inline-try
|
|
614
|
+
* methods (`getById`, `update`, `delete`, `deleteMany`, `aggregatePaginate`,
|
|
615
|
+
* `lookupPopulate`, plus `getOne`/`getAll` cache-hit branches) call it
|
|
616
|
+
* directly so middleware sees every op — including cached reads — not
|
|
617
|
+
* just the ones routed through `_runOp`.
|
|
743
618
|
*/
|
|
744
|
-
|
|
745
|
-
type: 'object';
|
|
746
|
-
properties: Record<string, unknown>;
|
|
747
|
-
required?: string[];
|
|
748
|
-
};
|
|
619
|
+
private _composeMiddleware;
|
|
749
620
|
/**
|
|
750
|
-
*
|
|
751
|
-
*
|
|
752
|
-
*
|
|
753
|
-
*
|
|
621
|
+
* Standard operation envelope: compose middleware around an exec that
|
|
622
|
+
* runs `fn`, emits `after:<op>` on success or `error:<op>` on failure.
|
|
623
|
+
* Methods with branched in-try logic that emit `after:*` from multiple
|
|
624
|
+
* paths (`getById`, `update`, `delete`, `deleteMany`, `aggregatePaginate`,
|
|
625
|
+
* `lookupPopulate`) keep their inline try/catch and call
|
|
626
|
+
* `_composeMiddleware` directly instead of being forced through this
|
|
627
|
+
* single-result helper.
|
|
754
628
|
*/
|
|
755
|
-
|
|
756
|
-
type: 'object';
|
|
757
|
-
properties: Record<string, unknown>;
|
|
758
|
-
};
|
|
629
|
+
private _runOp;
|
|
759
630
|
/**
|
|
760
|
-
*
|
|
631
|
+
* Resilience envelope around the DRIVER round-trip only — the repo-core
|
|
632
|
+
* 0.6.0 `QueryOptions.signal` / `QueryOptions.retryPolicy` contract.
|
|
633
|
+
*
|
|
634
|
+
* `throwIfAborted` stops a cancelled request before the next driver call;
|
|
635
|
+
* `withRetry` retries transient failures with exponential backoff (and is
|
|
636
|
+
* a zero-cost passthrough when no policy was passed, so we wrap
|
|
637
|
+
* unconditionally). Crucially this wraps `fn` AFTER `_buildContext` ran —
|
|
638
|
+
* before-hooks (validation, tenant scope, audit, events) execute exactly
|
|
639
|
+
* once per logical call and are NEVER re-run on retry; only the driver
|
|
640
|
+
* call repeats.
|
|
641
|
+
*
|
|
642
|
+
* Both knobs are read from the context (the options bag is spread into
|
|
643
|
+
* `_buildContext` inputs), so every op routed through `_runOp` — and the
|
|
644
|
+
* inline-try methods that call this directly — honors them uniformly.
|
|
761
645
|
*/
|
|
762
|
-
private
|
|
646
|
+
private _withResilience;
|
|
763
647
|
/**
|
|
764
|
-
*
|
|
648
|
+
* Create single document
|
|
765
649
|
*/
|
|
766
|
-
|
|
650
|
+
create(data: Record<string, unknown>, options?: CreateOptions): Promise<TDoc>;
|
|
767
651
|
/**
|
|
768
|
-
*
|
|
652
|
+
* Create multiple documents
|
|
769
653
|
*/
|
|
770
|
-
|
|
654
|
+
createMany(dataArray: Record<string, unknown>[], options?: CreateOptions): Promise<TDoc[]>;
|
|
771
655
|
/**
|
|
772
|
-
*
|
|
773
|
-
*
|
|
774
|
-
* Supported formats:
|
|
775
|
-
* 1. Simple: ?lookup[department]=slug
|
|
776
|
-
* → Join with 'departments' collection on slug field
|
|
777
|
-
*
|
|
778
|
-
* 2. Detailed: ?lookup[department][localField]=deptSlug&lookup[department][foreignField]=slug
|
|
779
|
-
* → Full control over join configuration
|
|
780
|
-
*
|
|
781
|
-
* 3. Multiple: ?lookup[department]=slug&lookup[category]=categorySlug
|
|
782
|
-
* → Multiple lookups
|
|
783
|
-
*
|
|
784
|
-
* @example
|
|
785
|
-
* ```typescript
|
|
786
|
-
* // URL: ?lookup[department][localField]=deptSlug&lookup[department][foreignField]=slug&lookup[department][single]=true
|
|
787
|
-
* const lookups = parser._parseLookups({
|
|
788
|
-
* department: { localField: 'deptSlug', foreignField: 'slug', single: 'true' }
|
|
789
|
-
* });
|
|
790
|
-
* // Returns: [{ from: 'departments', localField: 'deptSlug', foreignField: 'slug', single: true }]
|
|
791
|
-
* ```
|
|
656
|
+
* Get document by ID
|
|
792
657
|
*/
|
|
793
|
-
|
|
658
|
+
getById(id: string | ObjectId, options?: CacheableOptions): Promise<TDoc | null>;
|
|
794
659
|
/**
|
|
795
|
-
*
|
|
660
|
+
* Get single document by query
|
|
796
661
|
*/
|
|
797
|
-
|
|
662
|
+
getByQuery(query: Record<string, unknown>, options?: CacheableOptions & {
|
|
663
|
+
sort?: SortSpec;
|
|
664
|
+
}): Promise<TDoc | null>;
|
|
798
665
|
/**
|
|
799
|
-
*
|
|
666
|
+
* Get single document by arbitrary filter.
|
|
667
|
+
* Unlike getByQuery, this method is designed for Arc/controller use where
|
|
668
|
+
* compound filters (org scope + policy + id) are pre-built.
|
|
800
669
|
*
|
|
801
670
|
* @example
|
|
802
|
-
*
|
|
803
|
-
* // URL: ?aggregate[group][_id]=$status&aggregate[group][count]=$sum:1
|
|
804
|
-
* const pipeline = parser._parseAggregation({
|
|
805
|
-
* group: { _id: '$status', count: '$sum:1' }
|
|
806
|
-
* });
|
|
807
|
-
* ```
|
|
671
|
+
* const product = await repo.getOne({ slug: 'laptop', organizationId: 'org_1' });
|
|
808
672
|
*/
|
|
809
|
-
|
|
673
|
+
getOne(query: Record<string, unknown>, options?: CacheableOptions & {
|
|
674
|
+
sort?: SortSpec;
|
|
675
|
+
}): Promise<TDoc | null>;
|
|
810
676
|
/**
|
|
811
|
-
*
|
|
677
|
+
* Fetch ALL documents matching filters without pagination.
|
|
678
|
+
* Use for background jobs, exports, batch processing where you need every doc.
|
|
812
679
|
*
|
|
813
680
|
* @example
|
|
814
|
-
*
|
|
815
|
-
*
|
|
816
|
-
* // Returns: { name: 1, email: 1, password: 0 }
|
|
817
|
-
* ```
|
|
681
|
+
* const all = await repo.findAll({ status: 'active' });
|
|
682
|
+
* const allLean = await repo.findAll({}, { select: 'name email', lean: true });
|
|
818
683
|
*/
|
|
819
|
-
|
|
684
|
+
findAll(filters?: Record<string, unknown>, options?: OperationOptions & {
|
|
685
|
+
sort?: SortSpec | string;
|
|
686
|
+
limit?: number;
|
|
687
|
+
}): Promise<TDoc[]>;
|
|
820
688
|
/**
|
|
821
|
-
*
|
|
689
|
+
* Streaming reads — async iterator over data, suitable for migrations,
|
|
690
|
+
* backfills, schema audits, and any once-a-quarter "touch every row"
|
|
691
|
+
* job. Goes through the standard `before:cursor` hook pipeline so
|
|
692
|
+
* multi-tenant scope, soft-delete filter, and access-control plugins
|
|
693
|
+
* inject scope BEFORE the underlying mongoose cursor is built.
|
|
822
694
|
*
|
|
823
|
-
*
|
|
824
|
-
*
|
|
825
|
-
* // Simple: ?populate=author,category
|
|
826
|
-
* // Returns: { simplePopulate: 'author,category', populateOptions: undefined }
|
|
695
|
+
* Replaces direct `Model.find().cursor()` usage which bypasses every
|
|
696
|
+
* plugin — that's how cross-tenant data ends up in migrations.
|
|
827
697
|
*
|
|
828
|
-
*
|
|
829
|
-
*
|
|
698
|
+
* Returns an `AsyncIterableIterator<TDoc>` — drive it with `for await`.
|
|
699
|
+
* The mongoose cursor is closed when iteration completes or breaks.
|
|
700
|
+
*
|
|
701
|
+
* @example Backfill a missing field across the whole tenant scope
|
|
702
|
+
* ```ts
|
|
703
|
+
* for await (const doc of repo.cursor({ filter: { migrated: { $ne: true }}}, { batchSize: 1000 })) {
|
|
704
|
+
* await transformer(doc);
|
|
705
|
+
* await repo.update(doc._id, { migrated: true });
|
|
706
|
+
* }
|
|
830
707
|
* ```
|
|
831
|
-
*/
|
|
832
|
-
private _parsePopulate;
|
|
833
|
-
/**
|
|
834
|
-
* Parse a single populate configuration
|
|
835
|
-
*/
|
|
836
|
-
private _parseSinglePopulate;
|
|
837
|
-
/**
|
|
838
|
-
* Convert populate match values (handles boolean strings, etc.)
|
|
839
|
-
*/
|
|
840
|
-
private _convertPopulateMatch;
|
|
841
|
-
/**
|
|
842
|
-
* Parse filter parameters
|
|
843
|
-
*/
|
|
844
|
-
private _parseFilters;
|
|
845
|
-
/**
|
|
846
|
-
* Handle operator syntax: field[operator]=value
|
|
847
|
-
*/
|
|
848
|
-
private _handleOperatorSyntax;
|
|
849
|
-
/**
|
|
850
|
-
* Handle bracket syntax with object value
|
|
851
|
-
*/
|
|
852
|
-
private _handleBracketSyntax;
|
|
853
|
-
private _parseSort;
|
|
854
|
-
private _toMongoOperator;
|
|
855
|
-
private _createSafeRegex;
|
|
856
|
-
private _escapeRegex;
|
|
857
|
-
/**
|
|
858
|
-
* Sanitize $match configuration to prevent dangerous operators
|
|
859
|
-
* Recursively filters out operators like $where, $function, $accumulator
|
|
860
|
-
*/
|
|
861
|
-
private _sanitizeMatchConfig;
|
|
862
|
-
/**
|
|
863
|
-
* Sanitize pipeline stages for use in $lookup.
|
|
864
|
-
* Blocks dangerous stages ($out, $merge, etc.) and recursively sanitizes
|
|
865
|
-
* operator expressions within $match, $addFields, and $set stages.
|
|
866
|
-
*/
|
|
867
|
-
private _sanitizePipeline;
|
|
868
|
-
/**
|
|
869
|
-
* Recursively sanitize expression objects, blocking dangerous operators
|
|
870
|
-
* like $where, $function, $accumulator inside $addFields/$set stages.
|
|
871
708
|
*
|
|
872
|
-
*
|
|
873
|
-
*
|
|
874
|
-
*
|
|
875
|
-
*
|
|
709
|
+
* @example Audit unscoped — opt out of multi-tenant for global sweeps
|
|
710
|
+
* ```ts
|
|
711
|
+
* for await (const doc of repo.cursor({}, { batchSize: 500, organizationId: undefined })) {
|
|
712
|
+
* audit(doc);
|
|
713
|
+
* }
|
|
714
|
+
* ```
|
|
876
715
|
*/
|
|
877
|
-
|
|
878
|
-
|
|
716
|
+
cursor(filter?: Record<string, unknown>, options?: ReadOptions & {
|
|
717
|
+
sort?: SortSpec | string;
|
|
718
|
+
batchSize?: number;
|
|
719
|
+
select?: SelectSpec;
|
|
720
|
+
lean?: boolean;
|
|
721
|
+
}): AsyncIterableIterator<TDoc>;
|
|
879
722
|
/**
|
|
880
|
-
*
|
|
881
|
-
*
|
|
723
|
+
* Portable change feed — `StandardRepo.watch()` over Mongo change
|
|
724
|
+
* streams (`Model.watch`). Requires a replica set (or mongos); a
|
|
725
|
+
* standalone mongod rejects change streams at open time — gate on
|
|
726
|
+
* `repo.capabilities.changeStreams` plus your deployment topology.
|
|
727
|
+
*
|
|
728
|
+
* Semantics:
|
|
729
|
+
* - **Plugin-routed like every other read.** The filter goes through
|
|
730
|
+
* the standard `before:watch` hook pipeline (`OP_REGISTRY.watch`,
|
|
731
|
+
* policyKey `'query'`) BEFORE the change-stream pipeline is built —
|
|
732
|
+
* `multiTenantPlugin` injects the tenant scope (and throws under
|
|
733
|
+
* `required: true` exactly like other reads), `softDeletePlugin`
|
|
734
|
+
* injects the deletion-state predicate. A tenant-scoped repo never
|
|
735
|
+
* streams cross-tenant changes.
|
|
736
|
+
* - `fullDocument: 'updateLookup'` — update events carry the post-
|
|
737
|
+
* image document when it still exists at lookup time.
|
|
738
|
+
* - `filter` (record or Filter IR) is compiled with the standard
|
|
739
|
+
* filter compiler and applied against `fullDocument.*` paths, so
|
|
740
|
+
* `watch({ status: 'pending' })` matches the post-image. Delete
|
|
741
|
+
* events have no `fullDocument` and therefore don't match a
|
|
742
|
+
* non-empty post-policy filter — use `bypassTenant` / an unscoped
|
|
743
|
+
* repo without a filter to observe deletes.
|
|
744
|
+
* - `options.signal` ends the iterator (closes the stream). A
|
|
745
|
+
* pre-aborted signal rejects at the op boundary like every other op.
|
|
746
|
+
* - `options.resumeAfter` forwards a previously captured Mongo
|
|
747
|
+
* resume token for at-least-once consumption across restarts.
|
|
748
|
+
*
|
|
749
|
+
* Context keys for policy plugins (`organizationId`, `bypassTenant`,
|
|
750
|
+
* `includeDeleted`, ...) ride the options bag at runtime — same as every
|
|
751
|
+
* other op. The contract's `WatchOptions` type doesn't declare an index
|
|
752
|
+
* signature, so typed callers widen at the call site (or rely on an
|
|
753
|
+
* ALS `resolveContext` so no per-call tenant key is needed).
|
|
882
754
|
*
|
|
883
755
|
* @example
|
|
884
|
-
*
|
|
885
|
-
*
|
|
886
|
-
*
|
|
887
|
-
*
|
|
888
|
-
*
|
|
889
|
-
*
|
|
890
|
-
* // ]
|
|
891
|
-
*/
|
|
892
|
-
private _buildRegexSearch;
|
|
893
|
-
private _parseOr;
|
|
894
|
-
private _enhanceWithBetween;
|
|
895
|
-
private _pluralize;
|
|
896
|
-
private _capitalize;
|
|
897
|
-
}
|
|
898
|
-
//#endregion
|
|
899
|
-
//#region src/Repository.d.ts
|
|
900
|
-
/**
|
|
901
|
-
* Plugin phase priorities (lower = runs first).
|
|
902
|
-
*
|
|
903
|
-
* Re-exported from `@classytic/repo-core/hooks` so mongokit 3.10 callers see
|
|
904
|
-
* the same constant whether they import from mongokit or directly from
|
|
905
|
-
* repo-core — priorities are identical (POLICY=100, CACHE=200,
|
|
906
|
-
* OBSERVABILITY=300, DEFAULT=500) and the two refs are reference-equal.
|
|
907
|
-
*/
|
|
908
|
-
declare const HOOK_PRIORITY: {
|
|
909
|
-
readonly POLICY: 100;
|
|
910
|
-
readonly CACHE: 200;
|
|
911
|
-
readonly OBSERVABILITY: 300;
|
|
912
|
-
readonly DEFAULT: 500;
|
|
913
|
-
};
|
|
914
|
-
/**
|
|
915
|
-
* Production-grade repository for MongoDB.
|
|
916
|
-
*
|
|
917
|
-
* Extends `@classytic/repo-core/repository`'s `RepositoryBase` for the
|
|
918
|
-
* driver-agnostic hook + plugin plumbing (context builder, priority-sorted
|
|
919
|
-
* event engine, plugin-order validator) while layering every Mongo-specific
|
|
920
|
-
* concern — populate, aggregate, lookup, keyset + cursor pagination, $near
|
|
921
|
-
* rewriting — on top.
|
|
922
|
-
*
|
|
923
|
-
* Public surface is byte-stable with mongokit 3.9 (the `_hooks` property is
|
|
924
|
-
* preserved as a live read-through getter; all CRUD, event, and plugin method
|
|
925
|
-
* signatures unchanged). 3.10 is an internal re-plumbing release; consumers
|
|
926
|
-
* upgrading from 3.9 need no code changes.
|
|
927
|
-
*/
|
|
928
|
-
declare class Repository<TDoc = unknown> extends RepositoryBase {
|
|
929
|
-
readonly Model: Model<TDoc>;
|
|
930
|
-
/**
|
|
931
|
-
* Mongoose model name. Duplicated on the instance for BC — arc / catalog /
|
|
932
|
-
* user code read `repo.model`. `RepositoryBase.modelName` points to the
|
|
933
|
-
* same string; prefer `modelName` in new code.
|
|
756
|
+
* ```ts
|
|
757
|
+
* const ac = new AbortController();
|
|
758
|
+
* for await (const change of repo.watch({ status: 'pending' }, { signal: ac.signal })) {
|
|
759
|
+
* if (change.operation === 'create') enqueue(change.doc!);
|
|
760
|
+
* }
|
|
761
|
+
* ```
|
|
934
762
|
*/
|
|
935
|
-
|
|
936
|
-
readonly _pagination: PaginationEngine<TDoc>;
|
|
937
|
-
readonly idField: string;
|
|
938
|
-
readonly searchMode: 'text' | 'regex' | 'auto';
|
|
939
|
-
readonly searchFields: string[] | undefined;
|
|
940
|
-
[key: string]: unknown;
|
|
941
|
-
private _hasTextIndex;
|
|
763
|
+
watch(filter?: FilterInput, options?: WatchOptions): AsyncIterable<ChangeEvent<TDoc>>;
|
|
942
764
|
/**
|
|
943
|
-
*
|
|
944
|
-
*
|
|
945
|
-
*
|
|
946
|
-
*
|
|
765
|
+
* Unified pagination - auto-detects offset vs keyset based on params
|
|
766
|
+
*
|
|
767
|
+
* Auto-detection logic:
|
|
768
|
+
* - If params has 'cursor' or 'after' → uses keyset pagination (stream)
|
|
769
|
+
* - If params has 'pagination' or 'page' → uses offset pagination (paginate)
|
|
770
|
+
* - Else → defaults to offset pagination with page=1
|
|
771
|
+
*
|
|
772
|
+
* @example
|
|
773
|
+
* // Offset pagination (page-based)
|
|
774
|
+
* await repo.getAll({ page: 1, limit: 50, filters: { status: 'active' } });
|
|
775
|
+
* await repo.getAll({ pagination: { page: 2, limit: 20 } });
|
|
776
|
+
*
|
|
777
|
+
* // Keyset pagination (cursor-based)
|
|
778
|
+
* await repo.getAll({ cursor: 'eyJ2Ij...', limit: 50 });
|
|
779
|
+
* await repo.getAll({ after: 'eyJ2Ij...', sort: { createdAt: -1 } });
|
|
780
|
+
*
|
|
781
|
+
* // Simple query (defaults to page 1)
|
|
782
|
+
* await repo.getAll({ filters: { status: 'active' } });
|
|
783
|
+
*
|
|
784
|
+
* // Skip cache for fresh data
|
|
785
|
+
* await repo.getAll({ filters: { status: 'active' } }, { skipCache: true });
|
|
947
786
|
*/
|
|
948
|
-
|
|
949
|
-
|
|
787
|
+
getAll(params?: {
|
|
788
|
+
filters?: Record<string, unknown>;
|
|
789
|
+
sort?: SortSpec | string;
|
|
790
|
+
cursor?: string;
|
|
791
|
+
after?: string;
|
|
792
|
+
page?: number;
|
|
793
|
+
pagination?: {
|
|
794
|
+
page?: number;
|
|
795
|
+
limit?: number;
|
|
796
|
+
};
|
|
797
|
+
limit?: number;
|
|
798
|
+
search?: string;
|
|
799
|
+
mode?: 'offset' | 'keyset';
|
|
800
|
+
hint?: string | Record<string, 1 | -1>;
|
|
801
|
+
maxTimeMS?: number;
|
|
802
|
+
countStrategy?: 'exact' | 'estimated' | 'none';
|
|
803
|
+
readPreference?: ReadPreferenceType; /** Advanced populate options (from QueryParser or Arc's BaseController) */
|
|
804
|
+
populateOptions?: PopulateOptions[]; /** Collation for locale-aware string comparison */
|
|
805
|
+
collation?: CollationOptions; /** Lookup configurations for $lookup joins (from QueryParser or manual) */
|
|
806
|
+
lookups?: LookupOptions[]; /** Skip pagination entirely — returns raw TDoc[] (same as findAll) */
|
|
807
|
+
noPagination?: boolean;
|
|
808
|
+
}, options?: CacheableOptions): Promise<OffsetPaginationResult<TDoc> | KeysetPaginationResult<TDoc> | TDoc[]>;
|
|
950
809
|
/**
|
|
951
|
-
*
|
|
810
|
+
* Atomic "look up by filter, insert `data` if missing, return the doc."
|
|
952
811
|
*
|
|
953
|
-
*
|
|
954
|
-
*
|
|
955
|
-
* `
|
|
956
|
-
*
|
|
812
|
+
* Returns `{ doc, created }` so callers can tell whether *this* call
|
|
813
|
+
* inserted (race-win) or matched an existing row. Implements
|
|
814
|
+
* `StandardRepo.getOrCreate()` from `@classytic/repo-core/repository`
|
|
815
|
+
* — see that interface's JSDoc for the cross-kit contract.
|
|
957
816
|
*
|
|
958
|
-
*
|
|
817
|
+
* Routes through the hook system for policy enforcement (multi-tenant,
|
|
818
|
+
* soft-delete, audit). The hook context carries the resolved query +
|
|
819
|
+
* data; the action layer adds the `created` discriminator from the
|
|
820
|
+
* driver's `lastErrorObject.upserted`.
|
|
959
821
|
*/
|
|
960
|
-
|
|
822
|
+
getOrCreate(query: Record<string, unknown>, createData: Record<string, unknown>, options?: SessionOptions): Promise<{
|
|
823
|
+
doc: TDoc;
|
|
824
|
+
created: boolean;
|
|
825
|
+
}>;
|
|
961
826
|
/**
|
|
962
|
-
*
|
|
963
|
-
*
|
|
964
|
-
* ≤3.9 made this a private method on its subclass; the base class exposes
|
|
965
|
-
* it publicly and wraps it in `_emitHook` below for internal use.
|
|
827
|
+
* Count documents
|
|
828
|
+
* Routes through hook system for policy enforcement (multi-tenant, soft-delete)
|
|
966
829
|
*/
|
|
967
|
-
|
|
968
|
-
private _emitErrorHook;
|
|
830
|
+
count(query?: Record<string, unknown>, options?: ReadOptions): Promise<number>;
|
|
969
831
|
/**
|
|
970
|
-
*
|
|
971
|
-
*
|
|
972
|
-
* `count`, `exists`, `update`, `delete`, `findOneAndUpdate`,
|
|
973
|
-
* `updateMany`, `deleteMany`, `claim`, `claimVersion`, `cursor`)
|
|
974
|
-
* checks its `context.query` keys against the cached set of schema
|
|
975
|
-
* paths. Top-level keys not on the schema get logged once per
|
|
976
|
-
* `(model, key)` pair — so a single bad caller surfaces in dev /
|
|
977
|
-
* staging instead of returning the wrong row in prod.
|
|
978
|
-
*
|
|
979
|
-
* Only runs when `strictQuery !== false` is in effect — if mongoose's
|
|
980
|
-
* strictQuery is off, unknown keys flow through to the driver and
|
|
981
|
-
* the trap doesn't apply.
|
|
832
|
+
* Check if document exists
|
|
833
|
+
* Routes through hook system for policy enforcement (multi-tenant, soft-delete)
|
|
982
834
|
*/
|
|
983
|
-
|
|
835
|
+
exists(query: Record<string, unknown>, options?: ReadOptions): Promise<{
|
|
836
|
+
_id: unknown;
|
|
837
|
+
} | null>;
|
|
984
838
|
/**
|
|
985
|
-
*
|
|
986
|
-
* `_runOp` invocation — it sees the operation name, the live
|
|
987
|
-
* `RepositoryContext`, and a `next()` continuation that runs the
|
|
988
|
-
* remaining chain + the actual op + after/error hooks.
|
|
989
|
-
*
|
|
990
|
-
* **Composes with `repo.on()` and plugins, doesn't replace them.**
|
|
991
|
-
* Middleware is for ergonomics (timing, short-circuit, input/output
|
|
992
|
-
* mutation in a single closure) — NOT for security policy. Use the
|
|
993
|
-
* `before:*` hook engine for tenant scope, soft-delete filtering,
|
|
994
|
-
* cache invalidation, and audit; those plugins MUST run before
|
|
995
|
-
* middleware sees the op.
|
|
996
|
-
*
|
|
997
|
-
* **Execution order on every call** (build/before hooks fire BEFORE
|
|
998
|
-
* the middleware chain dispatches, so middleware cannot wrap a
|
|
999
|
-
* before-hook failure):
|
|
1000
|
-
*
|
|
1001
|
-
* ```text
|
|
1002
|
-
* _buildContext + before:<op> ← repo.on('before:*') hooks
|
|
1003
|
-
* [outer middleware pre] ← repo.useMiddleware() registrations
|
|
1004
|
-
* [...inner middleware pre]
|
|
1005
|
-
* fn (driver call)
|
|
1006
|
-
* after:<op> | error:<op> ← repo.on('after:*' / 'error:*')
|
|
1007
|
-
* [...inner middleware post]
|
|
1008
|
-
* [outer middleware post]
|
|
1009
|
-
* ```
|
|
1010
|
-
*
|
|
1011
|
-
* Why this order matters: a `before:create` hook that injects a
|
|
1012
|
-
* tenant filter into `context.query` runs BEFORE the middleware chain
|
|
1013
|
-
* is composed. Middleware sees a context that was already scoped by
|
|
1014
|
-
* the policy plugins — it never has the authority to short-circuit a
|
|
1015
|
-
* tenant check, because the throw from a `before:*` policy hook
|
|
1016
|
-
* unwinds before middleware ever fires. That's by design — middleware
|
|
1017
|
-
* as a security boundary would be impossible to audit, since
|
|
1018
|
-
* registration order would determine whether scope wins.
|
|
1019
|
-
*
|
|
1020
|
-
* Practical consequence: if you want middleware to observe a policy
|
|
1021
|
-
* failure (e.g. metric a tenant-scope rejection), use `repo.on('error:
|
|
1022
|
-
* <op>', ...)` instead — that fires from inside the middleware chain
|
|
1023
|
-
* and is reachable.
|
|
1024
|
-
*
|
|
1025
|
-
* Registration order = composition order: the first middleware
|
|
1026
|
-
* registered runs outermost (wraps everything else).
|
|
1027
|
-
*
|
|
1028
|
-
* @example Time every op
|
|
1029
|
-
* ```ts
|
|
1030
|
-
* repo.useMiddleware(async ({ operation, next }) => {
|
|
1031
|
-
* const start = performance.now();
|
|
1032
|
-
* try { return await next(); }
|
|
1033
|
-
* finally { metrics.record(operation, performance.now() - start); }
|
|
1034
|
-
* });
|
|
1035
|
-
* ```
|
|
1036
|
-
*
|
|
1037
|
-
* @example Inject `tenantId` on every create
|
|
1038
|
-
* ```ts
|
|
1039
|
-
* repo.useMiddleware(async ({ operation, context, next }) => {
|
|
1040
|
-
* if (operation === 'create' && context.data) {
|
|
1041
|
-
* context.data.tenantId = currentTenant();
|
|
1042
|
-
* }
|
|
1043
|
-
* return next();
|
|
1044
|
-
* });
|
|
1045
|
-
* ```
|
|
1046
|
-
*
|
|
1047
|
-
* @example Short-circuit (return without `next()` to skip the actual op)
|
|
1048
|
-
* ```ts
|
|
1049
|
-
* repo.useMiddleware(async ({ operation, context, next }) => {
|
|
1050
|
-
* if (operation === 'getById' && readOnlyMaintenance) {
|
|
1051
|
-
* return cachedReadOnlyResponse(context.id);
|
|
1052
|
-
* }
|
|
1053
|
-
* return next();
|
|
1054
|
-
* });
|
|
1055
|
-
* ```
|
|
1056
|
-
*/
|
|
1057
|
-
useMiddleware(middleware: Middleware<TDoc>): this;
|
|
1058
|
-
/**
|
|
1059
|
-
* Compose the registered middleware chain around `exec`. Outermost
|
|
1060
|
-
* middleware (first registered) wraps innermost. Each middleware sees
|
|
1061
|
-
* the live `RepositoryContext` (already populated + run through
|
|
1062
|
-
* `before:*` hooks at the call site — see `useMiddleware` JSDoc for
|
|
1063
|
-
* the exact order). `after:` / `error:` hooks fire from inside
|
|
1064
|
-
* `exec`, so middleware composes WITH plugins, not instead of them.
|
|
1065
|
-
*
|
|
1066
|
-
* `_runOp` calls this with its standard try/catch envelope; inline-try
|
|
1067
|
-
* methods (`getById`, `update`, `delete`, `deleteMany`, `aggregatePaginate`,
|
|
1068
|
-
* `lookupPopulate`, plus `getOne`/`getAll` cache-hit branches) call it
|
|
1069
|
-
* directly so middleware sees every op — including cached reads — not
|
|
1070
|
-
* just the ones routed through `_runOp`.
|
|
1071
|
-
*/
|
|
1072
|
-
private _composeMiddleware;
|
|
1073
|
-
/**
|
|
1074
|
-
* Standard operation envelope: compose middleware around an exec that
|
|
1075
|
-
* runs `fn`, emits `after:<op>` on success or `error:<op>` on failure.
|
|
1076
|
-
* Methods with branched in-try logic that emit `after:*` from multiple
|
|
1077
|
-
* paths (`getById`, `update`, `delete`, `deleteMany`, `aggregatePaginate`,
|
|
1078
|
-
* `lookupPopulate`) keep their inline try/catch and call
|
|
1079
|
-
* `_composeMiddleware` directly instead of being forced through this
|
|
1080
|
-
* single-result helper.
|
|
1081
|
-
*/
|
|
1082
|
-
private _runOp;
|
|
1083
|
-
/**
|
|
1084
|
-
* Create single document
|
|
1085
|
-
*/
|
|
1086
|
-
create(data: Record<string, unknown>, options?: CreateOptions): Promise<TDoc>;
|
|
1087
|
-
/**
|
|
1088
|
-
* Create multiple documents
|
|
1089
|
-
*/
|
|
1090
|
-
createMany(dataArray: Record<string, unknown>[], options?: CreateOptions): Promise<TDoc[]>;
|
|
1091
|
-
/**
|
|
1092
|
-
* Get document by ID
|
|
1093
|
-
*/
|
|
1094
|
-
getById(id: string | ObjectId, options?: CacheableOptions): Promise<TDoc | null>;
|
|
1095
|
-
/**
|
|
1096
|
-
* Get single document by query
|
|
1097
|
-
*/
|
|
1098
|
-
getByQuery(query: Record<string, unknown>, options?: CacheableOptions & {
|
|
1099
|
-
sort?: SortSpec;
|
|
1100
|
-
}): Promise<TDoc | null>;
|
|
1101
|
-
/**
|
|
1102
|
-
* Get single document by arbitrary filter.
|
|
1103
|
-
* Unlike getByQuery, this method is designed for Arc/controller use where
|
|
1104
|
-
* compound filters (org scope + policy + id) are pre-built.
|
|
1105
|
-
*
|
|
1106
|
-
* @example
|
|
1107
|
-
* const product = await repo.getOne({ slug: 'laptop', organizationId: 'org_1' });
|
|
1108
|
-
*/
|
|
1109
|
-
getOne(query: Record<string, unknown>, options?: CacheableOptions & {
|
|
1110
|
-
sort?: SortSpec;
|
|
1111
|
-
}): Promise<TDoc | null>;
|
|
1112
|
-
/**
|
|
1113
|
-
* Fetch ALL documents matching filters without pagination.
|
|
1114
|
-
* Use for background jobs, exports, batch processing where you need every doc.
|
|
1115
|
-
*
|
|
1116
|
-
* @example
|
|
1117
|
-
* const all = await repo.findAll({ status: 'active' });
|
|
1118
|
-
* const allLean = await repo.findAll({}, { select: 'name email', lean: true });
|
|
1119
|
-
*/
|
|
1120
|
-
findAll(filters?: Record<string, unknown>, options?: OperationOptions & {
|
|
1121
|
-
sort?: SortSpec | string;
|
|
1122
|
-
limit?: number;
|
|
1123
|
-
}): Promise<TDoc[]>;
|
|
1124
|
-
/**
|
|
1125
|
-
* Streaming reads — async iterator over data, suitable for migrations,
|
|
1126
|
-
* backfills, schema audits, and any once-a-quarter "touch every row"
|
|
1127
|
-
* job. Goes through the standard `before:cursor` hook pipeline so
|
|
1128
|
-
* multi-tenant scope, soft-delete filter, and access-control plugins
|
|
1129
|
-
* inject scope BEFORE the underlying mongoose cursor is built.
|
|
1130
|
-
*
|
|
1131
|
-
* Replaces direct `Model.find().cursor()` usage which bypasses every
|
|
1132
|
-
* plugin — that's how cross-tenant data ends up in migrations.
|
|
1133
|
-
*
|
|
1134
|
-
* Returns an `AsyncIterableIterator<TDoc>` — drive it with `for await`.
|
|
1135
|
-
* The mongoose cursor is closed when iteration completes or breaks.
|
|
1136
|
-
*
|
|
1137
|
-
* @example Backfill a missing field across the whole tenant scope
|
|
1138
|
-
* ```ts
|
|
1139
|
-
* for await (const doc of repo.cursor({ filter: { migrated: { $ne: true }}}, { batchSize: 1000 })) {
|
|
1140
|
-
* await transformer(doc);
|
|
1141
|
-
* await repo.update(doc._id, { migrated: true });
|
|
1142
|
-
* }
|
|
1143
|
-
* ```
|
|
1144
|
-
*
|
|
1145
|
-
* @example Audit unscoped — opt out of multi-tenant for global sweeps
|
|
1146
|
-
* ```ts
|
|
1147
|
-
* for await (const doc of repo.cursor({}, { batchSize: 500, organizationId: undefined })) {
|
|
1148
|
-
* audit(doc);
|
|
1149
|
-
* }
|
|
1150
|
-
* ```
|
|
1151
|
-
*/
|
|
1152
|
-
cursor(filter?: Record<string, unknown>, options?: ReadOptions & {
|
|
1153
|
-
sort?: SortSpec | string;
|
|
1154
|
-
batchSize?: number;
|
|
1155
|
-
select?: SelectSpec;
|
|
1156
|
-
lean?: boolean;
|
|
1157
|
-
}): AsyncIterableIterator<TDoc>;
|
|
1158
|
-
/**
|
|
1159
|
-
* Unified pagination - auto-detects offset vs keyset based on params
|
|
1160
|
-
*
|
|
1161
|
-
* Auto-detection logic:
|
|
1162
|
-
* - If params has 'cursor' or 'after' → uses keyset pagination (stream)
|
|
1163
|
-
* - If params has 'pagination' or 'page' → uses offset pagination (paginate)
|
|
1164
|
-
* - Else → defaults to offset pagination with page=1
|
|
1165
|
-
*
|
|
1166
|
-
* @example
|
|
1167
|
-
* // Offset pagination (page-based)
|
|
1168
|
-
* await repo.getAll({ page: 1, limit: 50, filters: { status: 'active' } });
|
|
1169
|
-
* await repo.getAll({ pagination: { page: 2, limit: 20 } });
|
|
1170
|
-
*
|
|
1171
|
-
* // Keyset pagination (cursor-based)
|
|
1172
|
-
* await repo.getAll({ cursor: 'eyJ2Ij...', limit: 50 });
|
|
1173
|
-
* await repo.getAll({ after: 'eyJ2Ij...', sort: { createdAt: -1 } });
|
|
1174
|
-
*
|
|
1175
|
-
* // Simple query (defaults to page 1)
|
|
1176
|
-
* await repo.getAll({ filters: { status: 'active' } });
|
|
1177
|
-
*
|
|
1178
|
-
* // Skip cache for fresh data
|
|
1179
|
-
* await repo.getAll({ filters: { status: 'active' } }, { skipCache: true });
|
|
1180
|
-
*/
|
|
1181
|
-
getAll(params?: {
|
|
1182
|
-
filters?: Record<string, unknown>;
|
|
1183
|
-
sort?: SortSpec | string;
|
|
1184
|
-
cursor?: string;
|
|
1185
|
-
after?: string;
|
|
1186
|
-
page?: number;
|
|
1187
|
-
pagination?: {
|
|
1188
|
-
page?: number;
|
|
1189
|
-
limit?: number;
|
|
1190
|
-
};
|
|
1191
|
-
limit?: number;
|
|
1192
|
-
search?: string;
|
|
1193
|
-
mode?: 'offset' | 'keyset';
|
|
1194
|
-
hint?: string | Record<string, 1 | -1>;
|
|
1195
|
-
maxTimeMS?: number;
|
|
1196
|
-
countStrategy?: 'exact' | 'estimated' | 'none';
|
|
1197
|
-
readPreference?: ReadPreferenceType; /** Advanced populate options (from QueryParser or Arc's BaseController) */
|
|
1198
|
-
populateOptions?: PopulateOptions[]; /** Collation for locale-aware string comparison */
|
|
1199
|
-
collation?: CollationOptions; /** Lookup configurations for $lookup joins (from QueryParser or manual) */
|
|
1200
|
-
lookups?: LookupOptions[]; /** Skip pagination entirely — returns raw TDoc[] (same as findAll) */
|
|
1201
|
-
noPagination?: boolean;
|
|
1202
|
-
}, options?: CacheableOptions): Promise<OffsetPaginationResult<TDoc> | KeysetPaginationResult<TDoc> | TDoc[]>;
|
|
1203
|
-
/**
|
|
1204
|
-
* Atomic "look up by filter, insert `data` if missing, return the doc."
|
|
1205
|
-
*
|
|
1206
|
-
* Returns `{ doc, created }` so callers can tell whether *this* call
|
|
1207
|
-
* inserted (race-win) or matched an existing row. Implements
|
|
1208
|
-
* `StandardRepo.getOrCreate()` from `@classytic/repo-core/repository`
|
|
1209
|
-
* — see that interface's JSDoc for the cross-kit contract.
|
|
1210
|
-
*
|
|
1211
|
-
* Routes through the hook system for policy enforcement (multi-tenant,
|
|
1212
|
-
* soft-delete, audit). The hook context carries the resolved query +
|
|
1213
|
-
* data; the action layer adds the `created` discriminator from the
|
|
1214
|
-
* driver's `lastErrorObject.upserted`.
|
|
1215
|
-
*/
|
|
1216
|
-
getOrCreate(query: Record<string, unknown>, createData: Record<string, unknown>, options?: SessionOptions): Promise<{
|
|
1217
|
-
doc: TDoc;
|
|
1218
|
-
created: boolean;
|
|
1219
|
-
}>;
|
|
1220
|
-
/**
|
|
1221
|
-
* Count documents
|
|
1222
|
-
* Routes through hook system for policy enforcement (multi-tenant, soft-delete)
|
|
1223
|
-
*/
|
|
1224
|
-
count(query?: Record<string, unknown>, options?: ReadOptions): Promise<number>;
|
|
1225
|
-
/**
|
|
1226
|
-
* Check if document exists
|
|
1227
|
-
* Routes through hook system for policy enforcement (multi-tenant, soft-delete)
|
|
1228
|
-
*/
|
|
1229
|
-
exists(query: Record<string, unknown>, options?: ReadOptions): Promise<{
|
|
1230
|
-
_id: unknown;
|
|
1231
|
-
} | null>;
|
|
1232
|
-
/**
|
|
1233
|
-
* Update a document by id and return the post-update doc.
|
|
839
|
+
* Update a document by id and return the post-update doc.
|
|
1234
840
|
*
|
|
1235
841
|
* Compiles to `Model.findOneAndUpdate(filter, { $set: data }, { new:
|
|
1236
842
|
* true, runValidators: true })` under the hood and routes through the
|
|
@@ -1603,415 +1209,925 @@ declare class Repository<TDoc = unknown> extends RepositoryBase {
|
|
|
1603
1209
|
* );
|
|
1604
1210
|
* ```
|
|
1605
1211
|
*/
|
|
1606
|
-
claimVersion(id: string | ObjectId, transition: {
|
|
1607
|
-
field?: string;
|
|
1608
|
-
from: number | undefined;
|
|
1609
|
-
by?: number;
|
|
1610
|
-
where?: Record<string, unknown>;
|
|
1611
|
-
}, update: Record<string, unknown>, options?: SessionOptions & {
|
|
1612
|
-
idField?: string;
|
|
1613
|
-
}): Promise<TDoc | null>;
|
|
1212
|
+
claimVersion(id: string | ObjectId, transition: {
|
|
1213
|
+
field?: string;
|
|
1214
|
+
from: number | undefined;
|
|
1215
|
+
by?: number;
|
|
1216
|
+
where?: Record<string, unknown>;
|
|
1217
|
+
}, update: Record<string, unknown>, options?: SessionOptions & {
|
|
1218
|
+
idField?: string;
|
|
1219
|
+
}): Promise<TDoc | null>;
|
|
1220
|
+
/**
|
|
1221
|
+
* Delete a document by ID.
|
|
1222
|
+
*
|
|
1223
|
+
* By default the behavior is decided by the plugin stack:
|
|
1224
|
+
* - With `softDeletePlugin` wired → soft delete (sets `deletedAt`)
|
|
1225
|
+
* - Without the plugin → physical delete
|
|
1226
|
+
*
|
|
1227
|
+
* Pass `mode: 'hard'` to force physical deletion regardless of plugins.
|
|
1228
|
+
* All policy hooks (multi-tenant scope, audit trails, cache invalidation,
|
|
1229
|
+
* cascade) still fire — only the soft-delete interception is skipped. This
|
|
1230
|
+
* is the GDPR / admin-cleanup path.
|
|
1231
|
+
*
|
|
1232
|
+
* @example
|
|
1233
|
+
* ```ts
|
|
1234
|
+
* // Respects softDeletePlugin — soft if wired
|
|
1235
|
+
* await userRepo.delete(userId);
|
|
1236
|
+
*
|
|
1237
|
+
* // GDPR erasure — physical delete, audit hooks still fire
|
|
1238
|
+
* await userRepo.delete(userId, { mode: 'hard', organizationId: 'org_123' });
|
|
1239
|
+
* ```
|
|
1240
|
+
*/
|
|
1241
|
+
delete(id: string | ObjectId, options?: SessionOptions & {
|
|
1242
|
+
idField?: string;
|
|
1243
|
+
mode?: 'hard' | 'soft'; /** Legacy opt-in: throw a 404 error on miss instead of returning `null`. */
|
|
1244
|
+
throwOnNotFound?: boolean;
|
|
1245
|
+
}): Promise<DeleteResult | null>;
|
|
1246
|
+
/**
|
|
1247
|
+
* Update every document matching the filter.
|
|
1248
|
+
*
|
|
1249
|
+
* Promoted from `batchOperationsPlugin` to a class primitive in 3.11.0 so
|
|
1250
|
+
* the surface matches sqlitekit's always-available `updateMany`. Consumers
|
|
1251
|
+
* who forget to wire `batchOperationsPlugin` no longer get a runtime
|
|
1252
|
+
* `TypeError: repo.updateMany is not a function` — the method is always
|
|
1253
|
+
* here.
|
|
1254
|
+
*
|
|
1255
|
+
* Accepts three `update` forms via `UpdateInput`:
|
|
1256
|
+
* - Portable `UpdateSpec` from `@classytic/repo-core/update` (compiled to
|
|
1257
|
+
* `$set`/`$unset`/`$inc`/`$setOnInsert` before hooks run).
|
|
1258
|
+
* - Raw Mongo operator record (`{ $set, $inc, ... }`) — passed through.
|
|
1259
|
+
* - Mongo aggregation pipeline (`[...]`) — requires `updatePipeline: true`
|
|
1260
|
+
* to guard against accidental pipeline updates.
|
|
1261
|
+
*
|
|
1262
|
+
* Refuses empty filters (defense-in-depth against mass-update accidents)
|
|
1263
|
+
* both before and after policy hooks inject tenant scope.
|
|
1264
|
+
*/
|
|
1265
|
+
updateMany(filter: Record<string, unknown>, update: UpdateInput, options?: SessionOptions & {
|
|
1266
|
+
updatePipeline?: boolean;
|
|
1267
|
+
[key: string]: unknown;
|
|
1268
|
+
}): Promise<UpdateManyResult>;
|
|
1269
|
+
/**
|
|
1270
|
+
* Delete every document matching the filter.
|
|
1271
|
+
*
|
|
1272
|
+
* Promoted from `batchOperationsPlugin` to a class primitive in 3.11.0 for
|
|
1273
|
+
* the same reason as `updateMany` — sqlitekit parity and no silent "method
|
|
1274
|
+
* is undefined" footgun when the batch plugin isn't wired.
|
|
1275
|
+
*
|
|
1276
|
+
* Behavior mirrors `delete()`: defaults to soft delete when
|
|
1277
|
+
* `softDeletePlugin` is wired, physical delete otherwise. Pass
|
|
1278
|
+
* `{ mode: 'hard' }` to bypass soft-delete — multi-tenant scoping, audit,
|
|
1279
|
+
* and cache hooks still fire.
|
|
1280
|
+
*
|
|
1281
|
+
* Rejects empty filters up front AND after policy hooks, so a policy-plugin
|
|
1282
|
+
* bug that zeroes out the query can't be exploited to wipe a collection.
|
|
1283
|
+
*/
|
|
1284
|
+
deleteMany(filter: Record<string, unknown>, options?: SessionOptions & {
|
|
1285
|
+
mode?: 'hard' | 'soft';
|
|
1286
|
+
[key: string]: unknown;
|
|
1287
|
+
}): Promise<DeleteManyResult>;
|
|
1288
|
+
/**
|
|
1289
|
+
* Compliance-grade cleanup primitive — see `StandardRepo.purgeByField`
|
|
1290
|
+
* for the cross-kit contract. Mongokit composes the kit-agnostic
|
|
1291
|
+
* `runChunkedPurge` orchestrator (loop, signal, progress, result
|
|
1292
|
+
* envelope) with `createMongoPurgePort` (driver-specific id selection
|
|
1293
|
+
* + strategy writes). See [actions/purge.ts](./actions/purge.ts) for
|
|
1294
|
+
* the port implementation.
|
|
1295
|
+
*/
|
|
1296
|
+
purgeByField(field: string, value: unknown, strategy: TenantPurgeStrategy, options?: TenantPurgeOptions): Promise<TenantPurgeResult>;
|
|
1297
|
+
/**
|
|
1298
|
+
* Kit-native aggregation pipeline — takes a MongoDB stage array and
|
|
1299
|
+
* returns the raw pipeline output. Use this for `$lookup`, `$unwind`,
|
|
1300
|
+
* `$facet`, `$graphLookup`, and any other mongo-specific power
|
|
1301
|
+
* features that don't translate to SQL.
|
|
1302
|
+
*
|
|
1303
|
+
* For backend-portable aggregations (filter + group + measures +
|
|
1304
|
+
* sort + limit) use `aggregate(req: AggRequest)` instead — it
|
|
1305
|
+
* compiles the repo-core IR to the equivalent pipeline internally
|
|
1306
|
+
* and produces the same row shape sqlitekit emits.
|
|
1307
|
+
*
|
|
1308
|
+
* Routes through the hook system for policy enforcement
|
|
1309
|
+
* (multi-tenant, soft-delete).
|
|
1310
|
+
*
|
|
1311
|
+
* @param pipeline - MongoDB aggregation stage array
|
|
1312
|
+
* @param options - Aggregation options including governance controls
|
|
1313
|
+
*/
|
|
1314
|
+
aggregatePipeline<TResult = unknown>(pipeline: PipelineStage[], options?: AggregateOptions): Promise<TResult[]>;
|
|
1315
|
+
/**
|
|
1316
|
+
* Paginated kit-native aggregation pipeline. Same mongo-specific
|
|
1317
|
+
* scope as `aggregatePipeline` — reach for this when you need
|
|
1318
|
+
* `$lookup` / `$unwind` / `$facet` alongside pagination.
|
|
1319
|
+
*
|
|
1320
|
+
* Prefer `aggregatePaginate(req: AggPaginationRequest)` for
|
|
1321
|
+
* cross-backend dashboard code; that method compiles the portable
|
|
1322
|
+
* IR to an equivalent pipeline.
|
|
1323
|
+
*
|
|
1324
|
+
* Policy hooks (multi-tenant, soft-delete) inject context.filters
|
|
1325
|
+
* which are prepended as a `$match` stage.
|
|
1326
|
+
*/
|
|
1327
|
+
aggregatePipelinePaginate(options?: AggregatePaginationOptions): Promise<AggregatePaginationResult<TDoc>>;
|
|
1328
|
+
/**
|
|
1329
|
+
* Portable aggregation. Compiles the repo-core `AggRequest` IR to
|
|
1330
|
+
* `[{$match}, {$group}, {$addFields}, {$project}, {$match}, {$sort},
|
|
1331
|
+
* {$skip}, {$limit}]` against this repo's collection. Output rows
|
|
1332
|
+
* carry one key per `groupBy` column plus one key per measure alias
|
|
1333
|
+
* — identical shape to sqlitekit's `aggregate(req)`, so dashboards
|
|
1334
|
+
* work unchanged across backends.
|
|
1335
|
+
*
|
|
1336
|
+
* Without `groupBy`: returns a single-row result with just the
|
|
1337
|
+
* measures (scalar aggregation). Pass
|
|
1338
|
+
* `{ measures: { total: { op: 'sum', field: 'amount' } } }` for a
|
|
1339
|
+
* simple summary.
|
|
1340
|
+
*
|
|
1341
|
+
* For mongo-specific power features (`$lookup`, `$unwind`, `$facet`,
|
|
1342
|
+
* `$graphLookup`, window operators) reach for `aggregatePipeline`
|
|
1343
|
+
* — that's the kit-native escape hatch.
|
|
1344
|
+
*
|
|
1345
|
+
* Routes through the hook system for policy enforcement
|
|
1346
|
+
* (multi-tenant, soft-delete) — plugins inject their scope into
|
|
1347
|
+
* `context.filter`, which is merged into the pre-aggregate `$match`
|
|
1348
|
+
* before compilation.
|
|
1349
|
+
*/
|
|
1350
|
+
aggregate<TRow extends Record<string, unknown> = Record<string, unknown>>(req: AggRequest, options?: QueryOptions): Promise<AggResult<TRow>>;
|
|
1351
|
+
/**
|
|
1352
|
+
* Invalidate every aggregate-cache entry tagged with ANY of the
|
|
1353
|
+
* given tags. Delegates to the unified cache plugin's handle
|
|
1354
|
+
* (`repo.cache.invalidateByTags`) — wired by `cachePlugin({ adapter })`.
|
|
1355
|
+
*
|
|
1356
|
+
* Pass no tags to wipe the entire cache namespace (requires the
|
|
1357
|
+
* adapter to implement `clear`).
|
|
1358
|
+
*
|
|
1359
|
+
* Returns the count of distinct entries cleared (`-1` when the call
|
|
1360
|
+
* routed through `adapter.clear()` and the adapter doesn't report a
|
|
1361
|
+
* count). No-op (returns `0`) when no cache plugin is wired.
|
|
1362
|
+
*/
|
|
1363
|
+
invalidateAggregateCache(tags?: readonly string[]): Promise<number>;
|
|
1364
|
+
/**
|
|
1365
|
+
* Paginated portable aggregation. Two pagination modes, picked by
|
|
1366
|
+
* the request shape:
|
|
1367
|
+
*
|
|
1368
|
+
* - **Offset (default)** — `page` + `limit`. Returns the standard
|
|
1369
|
+
* `{ method: 'offset', data, total, pages, hasNext, hasPrev, ... }`
|
|
1370
|
+
* envelope. `countStrategy: 'none'` skips the second round-trip
|
|
1371
|
+
* that computes `total`; the envelope reports `total: 0`,
|
|
1372
|
+
* `pages: 0`, and derives `hasNext` from a `LIMIT N+1` peek on
|
|
1373
|
+
* the data pipeline.
|
|
1374
|
+
* - **Keyset** — `pagination: 'keyset'` (or `after` set). Returns
|
|
1375
|
+
* `{ method: 'keyset', data, hasMore, next, limit }`. `sort` is
|
|
1376
|
+
* required — the cursor encodes the sort-key tuple of the last
|
|
1377
|
+
* row. Each subsequent page passes the previous `next` back as
|
|
1378
|
+
* `after`. Scales to arbitrary group counts because the planner
|
|
1379
|
+
* uses `(sort_keys) > (cursor)` instead of `OFFSET N`.
|
|
1380
|
+
*/
|
|
1381
|
+
aggregatePaginate<TRow extends Record<string, unknown> = Record<string, unknown>>(req: AggPaginationRequest, options?: QueryOptions): Promise<OffsetPaginationResultCore<TRow> | KeysetAggPaginationResult<TRow>>;
|
|
1382
|
+
/**
|
|
1383
|
+
* Internal: the actual aggregate-paginate execution body. Extracted
|
|
1384
|
+
* so `aggregatePaginate` can wrap it in the cache layer without
|
|
1385
|
+
* duplicating the keyset / offset branch logic.
|
|
1386
|
+
*
|
|
1387
|
+
* Receives `finalReq` (already policy-scoped) so the cache key
|
|
1388
|
+
* built by the outer wrapper matches the request the executor
|
|
1389
|
+
* sees — no cross-tenant cache poisoning.
|
|
1390
|
+
*/
|
|
1391
|
+
private _executeAggregatePaginate;
|
|
1392
|
+
/**
|
|
1393
|
+
* Merge policy-hook-injected filters (multi-tenant scope,
|
|
1394
|
+
* soft-delete) into the pre-aggregate `filter` slot of the request.
|
|
1395
|
+
* Plugins speak mongo-query shape (`context.query` / `context.filters`);
|
|
1396
|
+
* the caller's `req.filter` may be either Filter IR or a mongo query.
|
|
1397
|
+
*
|
|
1398
|
+
* Each part is compiled through `compileFilterToMongo` BEFORE the
|
|
1399
|
+
* `$and` merge — otherwise an IR node like `eq('status', 'paid')`
|
|
1400
|
+
* (`{ op, field, value }`) gets nested inside `$and` as a literal,
|
|
1401
|
+
* `compileFilterToMongo` of the merged value finds no top-level `op`
|
|
1402
|
+
* (so `isFilter` is false), and Mongo receives the IR object as a
|
|
1403
|
+
* raw query clause that matches no real document. Symptom: aggregates
|
|
1404
|
+
* silently return 0 rows whenever a callerpasses IR + a policy
|
|
1405
|
+
* plugin is installed.
|
|
1406
|
+
*/
|
|
1407
|
+
private _injectPolicyScopeIntoAgg;
|
|
1408
|
+
/**
|
|
1409
|
+
* Get distinct values
|
|
1410
|
+
* Routes through hook system for policy enforcement (multi-tenant, soft-delete)
|
|
1411
|
+
*/
|
|
1412
|
+
distinct<T = unknown>(field: string, query?: Record<string, unknown>, options?: ReadOptions): Promise<T[]>;
|
|
1413
|
+
/**
|
|
1414
|
+
* Query with custom field lookups ($lookup)
|
|
1415
|
+
* Best for: Joins on slugs, SKUs, codes, or other indexed custom fields
|
|
1416
|
+
*
|
|
1417
|
+
* @example
|
|
1418
|
+
* ```typescript
|
|
1419
|
+
* // Join employees with departments using slug instead of ObjectId
|
|
1420
|
+
* const employees = await employeeRepo.lookupPopulate({
|
|
1421
|
+
* filters: { status: 'active' },
|
|
1422
|
+
* lookups: [
|
|
1423
|
+
* {
|
|
1424
|
+
* from: 'departments',
|
|
1425
|
+
* localField: 'departmentSlug',
|
|
1426
|
+
* foreignField: 'slug',
|
|
1427
|
+
* as: 'department',
|
|
1428
|
+
* single: true
|
|
1429
|
+
* }
|
|
1430
|
+
* ],
|
|
1431
|
+
* sort: '-createdAt',
|
|
1432
|
+
* page: 1,
|
|
1433
|
+
* limit: 50
|
|
1434
|
+
* });
|
|
1435
|
+
* ```
|
|
1436
|
+
*/
|
|
1437
|
+
lookupPopulate<TExtra extends Record<string, unknown> = Record<string, unknown>>(options: LookupPopulateOptions<TDoc>): Promise<LookupPopulateResult<TDoc, TExtra>>;
|
|
1438
|
+
/**
|
|
1439
|
+
* Create an aggregation builder for this model
|
|
1440
|
+
* Useful for building complex custom aggregations
|
|
1441
|
+
*
|
|
1442
|
+
* @example
|
|
1443
|
+
* ```typescript
|
|
1444
|
+
* const pipeline = repo.buildAggregation()
|
|
1445
|
+
* .match({ status: 'active' })
|
|
1446
|
+
* .lookup('departments', 'deptSlug', 'slug', 'department', true)
|
|
1447
|
+
* .group({ _id: '$department', count: { $sum: 1 } })
|
|
1448
|
+
* .sort({ count: -1 })
|
|
1449
|
+
* .build();
|
|
1450
|
+
*
|
|
1451
|
+
* const results = await repo.Model.aggregate(pipeline);
|
|
1452
|
+
* ```
|
|
1453
|
+
*/
|
|
1454
|
+
buildAggregation(): AggregationBuilder;
|
|
1455
|
+
/**
|
|
1456
|
+
* Create a lookup builder
|
|
1457
|
+
* Useful for building $lookup stages independently
|
|
1458
|
+
*
|
|
1459
|
+
* @example
|
|
1460
|
+
* ```typescript
|
|
1461
|
+
* const lookupStages = repo.buildLookup('departments')
|
|
1462
|
+
* .localField('deptSlug')
|
|
1463
|
+
* .foreignField('slug')
|
|
1464
|
+
* .as('department')
|
|
1465
|
+
* .single()
|
|
1466
|
+
* .build();
|
|
1467
|
+
*
|
|
1468
|
+
* const pipeline = [
|
|
1469
|
+
* { $match: { status: 'active' } },
|
|
1470
|
+
* ...lookupStages
|
|
1471
|
+
* ];
|
|
1472
|
+
* ```
|
|
1473
|
+
*/
|
|
1474
|
+
buildLookup(from?: string): LookupBuilder;
|
|
1475
|
+
/**
|
|
1476
|
+
* Execute callback within a MongoDB transaction with automatic retry on
|
|
1477
|
+
* transient failures.
|
|
1478
|
+
*
|
|
1479
|
+
* **BREAKING (3.10):** the callback now receives a **tx-bound repository**,
|
|
1480
|
+
* not a raw `ClientSession`. This matches repo-core's `StandardRepo.withTransaction`
|
|
1481
|
+
* contract that every kit (sqlitekit, pgkit, prismakit) already implements
|
|
1482
|
+
* — so cross-kit plugins and apps are portable. Session threading happens
|
|
1483
|
+
* transparently: every CRUD call on `txRepo` auto-injects the session into
|
|
1484
|
+
* its options bag, including plugin-added methods (`upsert`, `increment`,
|
|
1485
|
+
* `updateMany`, `restore`, ...). Nested `txRepo.withTransaction(...)` throws.
|
|
1486
|
+
*
|
|
1487
|
+
* For raw-session cross-repo workflows — multiple repositories coordinating
|
|
1488
|
+
* writes — use the standalone `withTransaction(connection, fn)` export
|
|
1489
|
+
* from `@classytic/mongokit`. That helper remains session-based by design.
|
|
1490
|
+
*
|
|
1491
|
+
* @param callback - Receives a tx-bound `Repository<TDoc>` — call its
|
|
1492
|
+
* methods directly, no need to pass `{ session }` manually
|
|
1493
|
+
* @param options.allowFallback - Run without transaction on standalone MongoDB (default: false)
|
|
1494
|
+
* @param options.onFallback - Called when falling back to non-transactional execution
|
|
1495
|
+
* @param options.transactionOptions - MongoDB driver transaction options (readConcern, writeConcern, etc.)
|
|
1496
|
+
*
|
|
1497
|
+
* @example
|
|
1498
|
+
* ```typescript
|
|
1499
|
+
* const order = await repo.withTransaction(async (txRepo) => {
|
|
1500
|
+
* const created = await txRepo.create({ total: 100 });
|
|
1501
|
+
* await txRepo.update(created._id, { confirmed: true });
|
|
1502
|
+
* return created;
|
|
1503
|
+
* });
|
|
1504
|
+
*
|
|
1505
|
+
* // With fallback for standalone/dev environments
|
|
1506
|
+
* await repo.withTransaction(
|
|
1507
|
+
* async (txRepo) => {
|
|
1508
|
+
* await txRepo.create(doc);
|
|
1509
|
+
* },
|
|
1510
|
+
* {
|
|
1511
|
+
* allowFallback: true,
|
|
1512
|
+
* onFallback: (err) => logger.warn('Running without transaction', err),
|
|
1513
|
+
* },
|
|
1514
|
+
* );
|
|
1515
|
+
*
|
|
1516
|
+
* // Cross-repo with explicit session:
|
|
1517
|
+
* import { withTransaction } from '@classytic/mongokit';
|
|
1518
|
+
* await withTransaction(mongoose.connection, async (session) => {
|
|
1519
|
+
* const order = await orderRepo.create(data, { session });
|
|
1520
|
+
* await inventoryRepo.decrement(..., { session });
|
|
1521
|
+
* });
|
|
1522
|
+
* ```
|
|
1523
|
+
*/
|
|
1524
|
+
withTransaction<T>(callback: (txRepo: this) => Promise<T>, options?: WithTransactionOptions): Promise<T>;
|
|
1525
|
+
/**
|
|
1526
|
+
* Execute custom query with event emission
|
|
1527
|
+
*/
|
|
1528
|
+
_executeQuery<T>(buildQuery: (Model: Model<TDoc>) => Promise<T>): Promise<T>;
|
|
1529
|
+
/**
|
|
1530
|
+
* Build operation context and run before-hooks (sorted by priority).
|
|
1531
|
+
*
|
|
1532
|
+
* Narrows `RepositoryBase._buildContext`'s return type to mongokit's
|
|
1533
|
+
* richer `RepositoryContext` (which carries `softDeleted`, `_cacheHit`,
|
|
1534
|
+
* populate / readPreference / session, etc.). Runtime is inherited from
|
|
1535
|
+
* the base class — this override is purely a type-narrowing shim.
|
|
1536
|
+
*
|
|
1537
|
+
* Hook execution order is deterministic:
|
|
1538
|
+
* 1. POLICY (100) — tenant isolation, soft-delete filtering, validation
|
|
1539
|
+
* 2. CACHE (200) — cache lookup (after policy filters are injected)
|
|
1540
|
+
* 3. OBSERVABILITY (300) — audit logging, metrics
|
|
1541
|
+
* 4. DEFAULT (500) — user-registered hooks
|
|
1542
|
+
*/
|
|
1543
|
+
_buildContext(operation: string, options: Record<string, unknown> | object): Promise<RepositoryContext>;
|
|
1544
|
+
/**
|
|
1545
|
+
* Walk the well-known filter-carrying keys (`query`, `filters`,
|
|
1546
|
+
* `having`) and route any Filter IR through `compileFilterToMongo`.
|
|
1547
|
+
* Plain Mongo queries pass through untouched — `isFilter` is the
|
|
1548
|
+
* discriminator. Returns a shallow clone; never mutates input.
|
|
1549
|
+
*/
|
|
1550
|
+
private _normalizeFilterSlots;
|
|
1551
|
+
/**
|
|
1552
|
+
* Resolve the effective sort for getAll. Handles $near conflict detection
|
|
1553
|
+
* (warns + drops explicit sort when $near is in filters).
|
|
1554
|
+
*/
|
|
1555
|
+
private _resolveSort;
|
|
1556
|
+
/**
|
|
1557
|
+
* Detect whether to use keyset (cursor) or offset (page) pagination.
|
|
1558
|
+
*/
|
|
1559
|
+
private _detectPaginationMode;
|
|
1560
|
+
/**
|
|
1561
|
+
* Build the MongoDB query filter with search merged in. Handles text
|
|
1562
|
+
* search ($text), regex search ($or of $regex), and the search-resolver
|
|
1563
|
+
* plugin contract (search already cleared by a before:getAll hook).
|
|
1564
|
+
*/
|
|
1565
|
+
private _buildSearchQuery;
|
|
1566
|
+
/**
|
|
1567
|
+
* Parse sort string or object
|
|
1568
|
+
*/
|
|
1569
|
+
_parseSort(sort: SortSpec | string | undefined): SortSpec;
|
|
1570
|
+
/**
|
|
1571
|
+
* Parse populate specification
|
|
1572
|
+
*/
|
|
1573
|
+
_parsePopulate(populate: PopulateSpec | undefined): string[] | PopulateOptions[];
|
|
1574
|
+
/**
|
|
1575
|
+
* Classify a driver error as an authoritative duplicate-key conflict.
|
|
1576
|
+
*
|
|
1577
|
+
* Used by arc's outbox / idempotency adapters to distinguish
|
|
1578
|
+
* "write already landed (idempotent no-op)" from "transient DB error
|
|
1579
|
+
* (must retry)". Every backend signals duplicate keys differently —
|
|
1580
|
+
* MongoDB `code: 11000`, Prisma `P2002`, Postgres `23505` — so this
|
|
1581
|
+
* predicate lives on the kit that knows its driver. Cross-driver
|
|
1582
|
+
* adapters then depend only on the boolean outcome, not the shape of
|
|
1583
|
+
* the underlying error.
|
|
1584
|
+
*
|
|
1585
|
+
* Deliberately narrow: matches ONLY `code === 11000` and
|
|
1586
|
+
* `codeName === 'DuplicateKey'`. Does NOT match
|
|
1587
|
+
* `err.name === 'MongoServerError'`, which is also true for
|
|
1588
|
+
* WriteConflict, NotWritablePrimary, ExceededTimeLimit, and every
|
|
1589
|
+
* other server-side error — treating those as duplicate keys would
|
|
1590
|
+
* silently swallow transactional retries.
|
|
1591
|
+
*
|
|
1592
|
+
* @example
|
|
1593
|
+
* try {
|
|
1594
|
+
* await outboxRepo.create(event);
|
|
1595
|
+
* } catch (err) {
|
|
1596
|
+
* if (outboxRepo.isDuplicateKeyError(err)) {
|
|
1597
|
+
* // Idempotent no-op — event was already written by a prior attempt.
|
|
1598
|
+
* return;
|
|
1599
|
+
* }
|
|
1600
|
+
* throw err; // transient DB error — upstream retries
|
|
1601
|
+
* }
|
|
1602
|
+
*/
|
|
1603
|
+
isDuplicateKeyError(err: unknown): boolean;
|
|
1604
|
+
/**
|
|
1605
|
+
* Handle errors with proper HTTP status codes.
|
|
1606
|
+
*
|
|
1607
|
+
* **Transactional retry signal preservation.** MongoDB driver errors
|
|
1608
|
+
* carry `errorLabels` (e.g. `'TransientTransactionError'`,
|
|
1609
|
+
* `'UnknownTransactionCommitResult'`) that `mongoose.Connection.
|
|
1610
|
+
* withTransaction()` reads via `error.hasErrorLabel(label)` to decide
|
|
1611
|
+
* whether to auto-retry the transaction. Wrapping such an error in a
|
|
1612
|
+
* fresh `Error` via `createError(...)` strips both the labels and the
|
|
1613
|
+
* `hasErrorLabel` method — the retry signal vanishes and concurrent
|
|
1614
|
+
* writers see WriteConflicts surface to userland instead of being
|
|
1615
|
+
* retried by the driver. To keep the standard MongoDB transaction-
|
|
1616
|
+
* retry mechanism intact, we re-throw any error that carries
|
|
1617
|
+
* `errorLabels` exactly as we received it. The caller still sees a
|
|
1618
|
+
* thrown error — only the wrapping layer is skipped.
|
|
1619
|
+
*/
|
|
1620
|
+
_handleError(error: Error): HttpError;
|
|
1621
|
+
}
|
|
1622
|
+
//#endregion
|
|
1623
|
+
//#region src/create-repository.d.ts
|
|
1624
|
+
/**
|
|
1625
|
+
* Audit config — either a `Logger` (routes to `auditLogPlugin`, which
|
|
1626
|
+
* writes structured log lines through your logger) or an
|
|
1627
|
+
* `AuditTrailOptions` object (routes to `auditTrailPlugin`, which
|
|
1628
|
+
* persists a queryable compliance ledger in its own collection).
|
|
1629
|
+
*
|
|
1630
|
+
* Discrimination: a value carrying any of the `Logger` function members
|
|
1631
|
+
* (`info` / `warn` / `error` / `debug`) is treated as a logger; anything
|
|
1632
|
+
* else is treated as `AuditTrailOptions` (all of whose members are
|
|
1633
|
+
* data-shaped except `metadata` / `onError`, which don't overlap with
|
|
1634
|
+
* the logger member names).
|
|
1635
|
+
*/
|
|
1636
|
+
type AuditConfig = Logger | AuditTrailOptions;
|
|
1637
|
+
/**
|
|
1638
|
+
* Declarative repository config. Every key is optional; a feature's
|
|
1639
|
+
* plugin is installed only when its key is present. The remaining
|
|
1640
|
+
* `RepositoryOptions` keys (`idField`, `hooks`, `schema`, `updateSchema`,
|
|
1641
|
+
* `events`, `requirePlugins`, ...) pass through to the `Repository`
|
|
1642
|
+
* constructor unchanged.
|
|
1643
|
+
*/
|
|
1644
|
+
interface CreateRepositoryConfig extends RepositoryOptions {
|
|
1645
|
+
/** Multi-tenant scoping — forwarded to `multiTenantPlugin`. */
|
|
1646
|
+
tenant?: MultiTenantOptions;
|
|
1647
|
+
/** Soft-delete — `true` for defaults, or `SoftDeleteOptions`. */
|
|
1648
|
+
softDelete?: boolean | SoftDeleteOptions;
|
|
1649
|
+
/** createdAt/updatedAt stamping — `timestampPlugin` takes no options. */
|
|
1650
|
+
timestamps?: boolean;
|
|
1651
|
+
/** Batch ops (`updateMany`/`deleteMany`/`bulkWrite`) via `batchOperationsPlugin`. */
|
|
1652
|
+
batch?: boolean;
|
|
1653
|
+
/** Read-through cache — forwarded to `cachePlugin`. */
|
|
1654
|
+
cache?: RepositoryCachePluginOptions;
|
|
1655
|
+
/** Audit — `Logger` → `auditLogPlugin`; `AuditTrailOptions` → `auditTrailPlugin`. */
|
|
1656
|
+
audit?: AuditConfig;
|
|
1657
|
+
/** Custom ID generation — forwarded to `customIdPlugin`. */
|
|
1658
|
+
customId?: CustomIdOptions;
|
|
1659
|
+
/** Extra plugins appended AFTER the canonical stack. */
|
|
1660
|
+
plugins?: PluginType[];
|
|
1661
|
+
/** Pagination engine config (defaultLimit, maxLimit, ...). */
|
|
1662
|
+
pagination?: PaginationConfig;
|
|
1663
|
+
}
|
|
1664
|
+
/**
|
|
1665
|
+
* Build a `Repository` from a declarative config. See module JSDoc for
|
|
1666
|
+
* ordering guarantees and an example.
|
|
1667
|
+
*/
|
|
1668
|
+
declare function createRepository<TDoc>(model: Model<TDoc, any, any, any>, config?: CreateRepositoryConfig): Repository<TDoc>;
|
|
1669
|
+
//#endregion
|
|
1670
|
+
//#region src/filter/compile.d.ts
|
|
1671
|
+
/**
|
|
1672
|
+
* Filter IR → MongoDB query compiler.
|
|
1673
|
+
*
|
|
1674
|
+
* Mirrors sqlitekit's `compileFilterToDrizzle`: takes a repo-core
|
|
1675
|
+
* `Filter` tree and emits a MongoDB query object. Used by `groupBy` /
|
|
1676
|
+
* `groupByPaginate` to translate the portable AggRequest's `filter`
|
|
1677
|
+
* + `having` into `$match` stages.
|
|
1678
|
+
*
|
|
1679
|
+
* Scope: the same operator set sqlitekit supports (`eq`, `ne`, `gt`,
|
|
1680
|
+
* `gte`, `lt`, `lte`, `in`, `nin`, `exists`, `like`, `regex`, `and`,
|
|
1681
|
+
* `or`, `not`, `raw`). Anything else throws so we fail loud instead
|
|
1682
|
+
* of silently dropping predicates.
|
|
1683
|
+
*
|
|
1684
|
+
* Why only this op set here and not everywhere in mongokit? Historic
|
|
1685
|
+
* reason: mongokit pre-dates repo-core's Filter IR, so its existing
|
|
1686
|
+
* policy plugins (multi-tenant, soft-delete) speak MongoDB query
|
|
1687
|
+
* language directly. Converting mongokit's entire filter pipeline
|
|
1688
|
+
* to Filter IR is out-of-scope for this change — we introduce the
|
|
1689
|
+
* compiler only where the new portable `groupBy` IR needs it.
|
|
1690
|
+
*/
|
|
1691
|
+
declare function compileFilterToMongo(input: unknown): Record<string, unknown>;
|
|
1692
|
+
//#endregion
|
|
1693
|
+
//#region src/operations.d.ts
|
|
1694
|
+
/**
|
|
1695
|
+
* Where a plugin should inject its scoping filter (multi-tenant scope,
|
|
1696
|
+
* soft-delete filter, etc.) on the RepositoryContext.
|
|
1697
|
+
*
|
|
1698
|
+
* - `data` — single-doc create payload (`context.data`)
|
|
1699
|
+
* - `dataArray` — multi-doc create payload (`context.dataArray`)
|
|
1700
|
+
* - `query` — raw filter (`context.query`); the dominant convention
|
|
1701
|
+
* - `filters` — paginated list options' filter sub-bag (`context.filters`)
|
|
1702
|
+
* - `operations` — bulkWrite per-sub-op (special — plugins walk each entry)
|
|
1703
|
+
* - `none` — no scoping target (op accepts no filter input)
|
|
1704
|
+
*/
|
|
1705
|
+
type PolicyKey = 'data' | 'dataArray' | 'query' | 'filters' | 'operations' | 'none';
|
|
1706
|
+
interface OperationDescriptor {
|
|
1707
|
+
/** Where multi-tenant / soft-delete plugins inject their scoping filter. */
|
|
1708
|
+
policyKey: PolicyKey;
|
|
1709
|
+
/** Whether this op writes to the database. Drives audit + cache invalidation. */
|
|
1710
|
+
mutates: boolean;
|
|
1711
|
+
/** True when `context.id` is populated by the time before/after hooks fire. */
|
|
1712
|
+
hasIdContext: boolean;
|
|
1713
|
+
}
|
|
1714
|
+
/**
|
|
1715
|
+
* The registry. Order intentionally groups by category for readability.
|
|
1716
|
+
* Adding a new RepositoryOperation requires exactly one entry here, after
|
|
1717
|
+
* which every bundled plugin auto-classifies it correctly.
|
|
1718
|
+
*/
|
|
1719
|
+
declare const OP_REGISTRY: Readonly<Record<RepositoryOperation, OperationDescriptor>>;
|
|
1720
|
+
/** All known repository operations, in registry order. */
|
|
1721
|
+
declare const ALL_OPERATIONS: RepositoryOperation[];
|
|
1722
|
+
/** Operations that mutate the database. Drives audit + cache invalidation lists. */
|
|
1723
|
+
declare const MUTATING_OPERATIONS: readonly RepositoryOperation[];
|
|
1724
|
+
/** Operations that don't mutate. Drives default cacheable ops. */
|
|
1725
|
+
declare const READ_OPERATIONS: readonly RepositoryOperation[];
|
|
1726
|
+
/** Filter ops by their policy injection key. */
|
|
1727
|
+
declare function operationsByPolicyKey(key: PolicyKey): RepositoryOperation[];
|
|
1728
|
+
//#endregion
|
|
1729
|
+
//#region src/query/QueryParser.d.ts
|
|
1730
|
+
type SortSpec$1 = Record<string, 1 | -1>;
|
|
1731
|
+
type FilterQuery = Record<string, unknown>;
|
|
1732
|
+
/**
|
|
1733
|
+
* Mongoose-compatible populate option
|
|
1734
|
+
* Supports advanced populate with select, match, limit, sort, and nested populate
|
|
1735
|
+
*
|
|
1736
|
+
* @example
|
|
1737
|
+
* ```typescript
|
|
1738
|
+
* // URL: ?populate[author][select]=name,email&populate[author][match][active]=true
|
|
1739
|
+
* // Generates: { path: 'author', select: 'name email', match: { active: true } }
|
|
1740
|
+
* ```
|
|
1741
|
+
*/
|
|
1742
|
+
interface PopulateOption {
|
|
1743
|
+
/** Field path to populate */
|
|
1744
|
+
path: string;
|
|
1745
|
+
/** Fields to select (space-separated) */
|
|
1746
|
+
select?: string;
|
|
1747
|
+
/** Filter conditions for populated documents */
|
|
1748
|
+
match?: Record<string, unknown>;
|
|
1749
|
+
/** Query options (limit, sort, skip) */
|
|
1750
|
+
options?: {
|
|
1751
|
+
limit?: number;
|
|
1752
|
+
sort?: SortSpec$1;
|
|
1753
|
+
skip?: number;
|
|
1754
|
+
};
|
|
1755
|
+
/** Nested populate configuration */
|
|
1756
|
+
populate?: PopulateOption;
|
|
1757
|
+
}
|
|
1758
|
+
/** Parsed query result with optional lookup configuration */
|
|
1759
|
+
interface ParsedQuery {
|
|
1760
|
+
/** MongoDB filter query */
|
|
1761
|
+
filters: FilterQuery;
|
|
1762
|
+
/** Sort specification */
|
|
1763
|
+
sort?: SortSpec$1;
|
|
1764
|
+
/** Fields to populate (simple comma-separated string) */
|
|
1765
|
+
populate?: string;
|
|
1766
|
+
/**
|
|
1767
|
+
* Advanced populate options (Mongoose-compatible)
|
|
1768
|
+
* When this is set, `populate` will be undefined
|
|
1769
|
+
* @example [{ path: 'author', select: 'name email' }]
|
|
1770
|
+
*/
|
|
1771
|
+
populateOptions?: PopulateOption[];
|
|
1772
|
+
/** Page number for offset pagination */
|
|
1773
|
+
page?: number;
|
|
1774
|
+
/** Cursor for keyset pagination */
|
|
1775
|
+
after?: string;
|
|
1776
|
+
/** Limit */
|
|
1777
|
+
limit: number;
|
|
1778
|
+
/** Full-text search query */
|
|
1779
|
+
search?: string;
|
|
1780
|
+
/** Lookup configurations for custom field joins */
|
|
1781
|
+
lookups?: LookupOptions[];
|
|
1782
|
+
/** Aggregation pipeline stages (advanced) */
|
|
1783
|
+
aggregation?: PipelineStage[];
|
|
1784
|
+
/** Select/project fields */
|
|
1785
|
+
select?: Record<string, 0 | 1>;
|
|
1786
|
+
}
|
|
1787
|
+
/** Search mode for query parser */
|
|
1788
|
+
type SearchMode = 'text' | 'regex';
|
|
1789
|
+
/**
|
|
1790
|
+
* Normalized field type used for schema-aware value coercion. Re-exported
|
|
1791
|
+
* from `primitives/coercion` so the public QueryParser API stays unchanged
|
|
1792
|
+
* while the actual logic lives in a single, unit-tested primitive module.
|
|
1793
|
+
*/
|
|
1794
|
+
type FieldType = FieldType$1;
|
|
1795
|
+
/**
|
|
1796
|
+
* Minimal structural type for a Mongoose schema. We only read `.paths` and
|
|
1797
|
+
* (optionally) `.indexes()`, so the parser stays decoupled from a specific
|
|
1798
|
+
* Mongoose major version and is easy to mock in tests. Re-exported from the
|
|
1799
|
+
* primitives module — the canonical definition lives there.
|
|
1800
|
+
*/
|
|
1801
|
+
interface SchemaLike extends SchemaPathsLike {
|
|
1802
|
+
/** Optional — when present, schema indexes are introspected for geo/text fields */
|
|
1803
|
+
indexes?: () => Array<[Record<string, unknown>, Record<string, unknown>?]>;
|
|
1804
|
+
}
|
|
1805
|
+
interface QueryParserOptions {
|
|
1806
|
+
/** Maximum allowed regex pattern length (default: 500) */
|
|
1807
|
+
maxRegexLength?: number;
|
|
1808
|
+
/** Maximum allowed text search query length (default: 200) */
|
|
1809
|
+
maxSearchLength?: number;
|
|
1810
|
+
/** Maximum allowed filter depth (default: 10) */
|
|
1811
|
+
maxFilterDepth?: number;
|
|
1812
|
+
/** Maximum allowed limit value (default: 1000) */
|
|
1813
|
+
maxLimit?: number;
|
|
1814
|
+
/** Additional operators to block */
|
|
1815
|
+
additionalDangerousOperators?: string[];
|
|
1816
|
+
/** Enable lookup parsing (default: true) */
|
|
1817
|
+
enableLookups?: boolean;
|
|
1818
|
+
/** Enable aggregation parsing (default: false - requires explicit opt-in) */
|
|
1819
|
+
enableAggregations?: boolean;
|
|
1820
|
+
/**
|
|
1821
|
+
* Search mode (default: 'text')
|
|
1822
|
+
* - 'text': Uses MongoDB $text search (requires text index)
|
|
1823
|
+
* - 'regex': Uses $or with $regex across searchFields (no index required)
|
|
1824
|
+
*/
|
|
1825
|
+
searchMode?: SearchMode;
|
|
1826
|
+
/**
|
|
1827
|
+
* Fields to search when searchMode is 'regex'
|
|
1828
|
+
* Required when searchMode is 'regex'
|
|
1829
|
+
* @example ['name', 'description', 'sku', 'tags']
|
|
1830
|
+
*/
|
|
1831
|
+
searchFields?: string[];
|
|
1832
|
+
/**
|
|
1833
|
+
* Whitelist of collection names allowed in lookups.
|
|
1834
|
+
* When set, only these collections can be used in $lookup stages.
|
|
1835
|
+
* When undefined, all collection names are allowed.
|
|
1836
|
+
* @example ['departments', 'categories', 'users']
|
|
1837
|
+
*/
|
|
1838
|
+
allowedLookupCollections?: string[];
|
|
1839
|
+
/** Allowed fields for filtering. If set, ignores unknown fields. */
|
|
1840
|
+
allowedFilterFields?: string[];
|
|
1841
|
+
/** Allowed fields for sorting. If set, ignores unknown fields. */
|
|
1842
|
+
allowedSortFields?: string[];
|
|
1843
|
+
/**
|
|
1844
|
+
* Whitelist of allowed filter operators.
|
|
1845
|
+
* When set, only these operators can be used in filters.
|
|
1846
|
+
* When undefined, all built-in operators are allowed.
|
|
1847
|
+
* Values are human-readable keys: 'eq', 'ne', 'gt', 'gte', 'lt', 'lte', 'in', 'nin',
|
|
1848
|
+
* 'like', 'contains', 'regex', 'exists', 'size', 'type'
|
|
1849
|
+
* @example ['eq', 'ne', 'gt', 'gte', 'lt', 'lte', 'in']
|
|
1850
|
+
*/
|
|
1851
|
+
allowedOperators?: string[];
|
|
1852
|
+
/**
|
|
1853
|
+
* Mongoose schema (or schema-like object exposing `.paths`) used for
|
|
1854
|
+
* authoritative, schema-aware value coercion. When provided, filter values
|
|
1855
|
+
* are coerced to the declared field type instead of guessed from the
|
|
1856
|
+
* string shape — `?stock=50` becomes a number against a `Number` field but
|
|
1857
|
+
* `?name=12345` stays a string against a `String` field. Nested paths like
|
|
1858
|
+
* `address.zip` are looked up via dot-notation, matching Mongoose's
|
|
1859
|
+
* `schema.paths` reflection. Unknown fields fall through to the heuristic.
|
|
1860
|
+
*/
|
|
1861
|
+
schema?: SchemaLike;
|
|
1862
|
+
/**
|
|
1863
|
+
* Plain field-type map used for authoritative coercion when no Mongoose
|
|
1864
|
+
* schema is available (raw MongoDB, Prisma, computed fields, upstream
|
|
1865
|
+
* models you don't own). Takes precedence over `schema` for paths declared
|
|
1866
|
+
* in both — useful for runtime overrides. Use dot-notation for nested
|
|
1867
|
+
* paths (`'address.zip': 'string'`).
|
|
1868
|
+
* @example { stock: 'number', active: 'boolean', releasedAt: 'date' }
|
|
1869
|
+
*/
|
|
1870
|
+
fieldTypes?: Record<string, FieldType>;
|
|
1871
|
+
}
|
|
1872
|
+
/**
|
|
1873
|
+
* Modern Query Parser
|
|
1874
|
+
* Converts URL parameters to MongoDB queries with $lookup support
|
|
1875
|
+
*/
|
|
1876
|
+
declare class QueryParser {
|
|
1877
|
+
private readonly options;
|
|
1878
|
+
private readonly operators;
|
|
1879
|
+
private readonly dangerousOperators;
|
|
1880
|
+
/**
|
|
1881
|
+
* Normalized dot-notation field-type map. Built once at construction from
|
|
1882
|
+
* `options.schema` (Mongoose) and `options.fieldTypes` (override map), with
|
|
1883
|
+
* `fieldTypes` taking precedence. An empty map means schema-aware coercion
|
|
1884
|
+
* is disabled and the heuristic in `_convertValue` runs unchanged — this
|
|
1885
|
+
* preserves backwards compatibility for callers that don't opt in.
|
|
1886
|
+
*/
|
|
1887
|
+
private readonly _fieldTypes;
|
|
1614
1888
|
/**
|
|
1615
|
-
*
|
|
1616
|
-
*
|
|
1617
|
-
*
|
|
1618
|
-
*
|
|
1619
|
-
* - Without the plugin → physical delete
|
|
1620
|
-
*
|
|
1621
|
-
* Pass `mode: 'hard'` to force physical deletion regardless of plugins.
|
|
1622
|
-
* All policy hooks (multi-tenant scope, audit trails, cache invalidation,
|
|
1623
|
-
* cascade) still fire — only the soft-delete interception is skipped. This
|
|
1624
|
-
* is the GDPR / admin-cleanup path.
|
|
1625
|
-
*
|
|
1626
|
-
* @example
|
|
1627
|
-
* ```ts
|
|
1628
|
-
* // Respects softDeletePlugin — soft if wired
|
|
1629
|
-
* await userRepo.delete(userId);
|
|
1630
|
-
*
|
|
1631
|
-
* // GDPR erasure — physical delete, audit hooks still fire
|
|
1632
|
-
* await userRepo.delete(userId, { mode: 'hard', organizationId: 'org_123' });
|
|
1633
|
-
* ```
|
|
1889
|
+
* Structured schema-index info (geo / text / other), built once from
|
|
1890
|
+
* `options.schema?.indexes()`. Used by the operator router to validate
|
|
1891
|
+
* geo operators against actual geo-indexed fields, and exposed publicly
|
|
1892
|
+
* via `parser.schemaIndexes` for downstream tools (Arc MCP, query planners).
|
|
1634
1893
|
*/
|
|
1635
|
-
|
|
1636
|
-
idField?: string;
|
|
1637
|
-
mode?: 'hard' | 'soft'; /** Legacy opt-in: throw a 404 error on miss instead of returning `null`. */
|
|
1638
|
-
throwOnNotFound?: boolean;
|
|
1639
|
-
}): Promise<DeleteResult | null>;
|
|
1894
|
+
private readonly _schemaIndexes;
|
|
1640
1895
|
/**
|
|
1641
|
-
*
|
|
1642
|
-
*
|
|
1643
|
-
*
|
|
1644
|
-
*
|
|
1645
|
-
*
|
|
1646
|
-
*
|
|
1647
|
-
*
|
|
1648
|
-
*
|
|
1649
|
-
* Accepts three `update` forms via `UpdateInput`:
|
|
1650
|
-
* - Portable `UpdateSpec` from `@classytic/repo-core/update` (compiled to
|
|
1651
|
-
* `$set`/`$unset`/`$inc`/`$setOnInsert` before hooks run).
|
|
1652
|
-
* - Raw Mongo operator record (`{ $set, $inc, ... }`) — passed through.
|
|
1653
|
-
* - Mongo aggregation pipeline (`[...]`) — requires `updatePipeline: true`
|
|
1654
|
-
* to guard against accidental pipeline updates.
|
|
1896
|
+
* Regex patterns that can cause catastrophic backtracking (ReDoS attacks).
|
|
1897
|
+
* Detects common footguns:
|
|
1898
|
+
* - Quantifiers: {n,m}
|
|
1899
|
+
* - Possessive quantifiers: *+, ++, ?+
|
|
1900
|
+
* - Nested quantifiers: (a+)+, (a*)*
|
|
1901
|
+
* - Backreferences: \1, \2, etc.
|
|
1902
|
+
* - Complex character classes: [...]...[...]
|
|
1655
1903
|
*
|
|
1656
|
-
*
|
|
1657
|
-
*
|
|
1904
|
+
* This pattern detector is FAIL-SAFE (on match, the input is escaped), but
|
|
1905
|
+
* it is not exhaustive. {@link _regexComplexityExceedsBudget} complements
|
|
1906
|
+
* it with a deterministic static complexity budget so patterns that happen
|
|
1907
|
+
* to evade the detector still fail over when their unbounded-quantifier
|
|
1908
|
+
* density is pathological.
|
|
1658
1909
|
*/
|
|
1659
|
-
|
|
1660
|
-
updatePipeline?: boolean;
|
|
1661
|
-
[key: string]: unknown;
|
|
1662
|
-
}): Promise<UpdateManyResult>;
|
|
1910
|
+
private readonly dangerousRegexPatterns;
|
|
1663
1911
|
/**
|
|
1664
|
-
*
|
|
1665
|
-
*
|
|
1666
|
-
*
|
|
1667
|
-
* the same reason as `updateMany` — sqlitekit parity and no silent "method
|
|
1668
|
-
* is undefined" footgun when the batch plugin isn't wired.
|
|
1912
|
+
* Static complexity budget for user-supplied regex patterns.
|
|
1913
|
+
* Returns `true` if the pattern crosses a budget that empirically correlates
|
|
1914
|
+
* with ReDoS risk — independent of the heuristic regex detector.
|
|
1669
1915
|
*
|
|
1670
|
-
*
|
|
1671
|
-
*
|
|
1672
|
-
*
|
|
1673
|
-
*
|
|
1916
|
+
* Budget components (any single threshold crossed → over budget):
|
|
1917
|
+
* - unbounded quantifiers (`*`, `+`, `?`) — hard cap 20
|
|
1918
|
+
* - nested groups `(` — hard cap 8
|
|
1919
|
+
* - alternations `|` — hard cap 10
|
|
1920
|
+
* - combined: (quantifiers * groups) — hard cap 40
|
|
1674
1921
|
*
|
|
1675
|
-
*
|
|
1676
|
-
*
|
|
1922
|
+
* These thresholds are intentionally permissive for legitimate queries and
|
|
1923
|
+
* tight enough to refuse known-pathological patterns like `(a+)+b` with
|
|
1924
|
+
* heavy repetition.
|
|
1677
1925
|
*/
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
[key: string]: unknown;
|
|
1681
|
-
}): Promise<DeleteManyResult>;
|
|
1926
|
+
private _regexComplexityExceedsBudget;
|
|
1927
|
+
constructor(options?: QueryParserOptions);
|
|
1682
1928
|
/**
|
|
1683
|
-
*
|
|
1684
|
-
*
|
|
1685
|
-
*
|
|
1686
|
-
*
|
|
1687
|
-
*
|
|
1688
|
-
*
|
|
1929
|
+
* Structured view of the configured schema's indexes — geo fields, text
|
|
1930
|
+
* fields, and other compound indexes. Empty arrays when no schema was
|
|
1931
|
+
* provided. Stable across the parser's lifetime; mutating the returned
|
|
1932
|
+
* object does not affect parser behavior.
|
|
1933
|
+
*
|
|
1934
|
+
* Useful for downstream tooling (Arc MCP, query planners, doc generators)
|
|
1935
|
+
* that needs to know which fields support which query types.
|
|
1689
1936
|
*/
|
|
1690
|
-
|
|
1937
|
+
get schemaIndexes(): SchemaIndexes;
|
|
1691
1938
|
/**
|
|
1692
|
-
*
|
|
1693
|
-
*
|
|
1694
|
-
* `$facet`, `$graphLookup`, and any other mongo-specific power
|
|
1695
|
-
* features that don't translate to SQL.
|
|
1696
|
-
*
|
|
1697
|
-
* For backend-portable aggregations (filter + group + measures +
|
|
1698
|
-
* sort + limit) use `aggregate(req: AggRequest)` instead — it
|
|
1699
|
-
* compiles the repo-core IR to the equivalent pipeline internally
|
|
1700
|
-
* and produces the same row shape sqlitekit emits.
|
|
1701
|
-
*
|
|
1702
|
-
* Routes through the hook system for policy enforcement
|
|
1703
|
-
* (multi-tenant, soft-delete).
|
|
1939
|
+
* Get the configured allowed filter fields.
|
|
1940
|
+
* Returns `undefined` if no whitelist is set (all fields allowed).
|
|
1704
1941
|
*
|
|
1705
|
-
*
|
|
1706
|
-
*
|
|
1942
|
+
* Used by Arc's MCP integration to auto-derive `filterableFields`
|
|
1943
|
+
* from the QueryParser when `schemaOptions.filterableFields` is not set.
|
|
1707
1944
|
*/
|
|
1708
|
-
|
|
1945
|
+
get allowedFilterFields(): string[] | undefined;
|
|
1709
1946
|
/**
|
|
1710
|
-
*
|
|
1711
|
-
*
|
|
1712
|
-
* `$lookup` / `$unwind` / `$facet` alongside pagination.
|
|
1713
|
-
*
|
|
1714
|
-
* Prefer `aggregatePaginate(req: AggPaginationRequest)` for
|
|
1715
|
-
* cross-backend dashboard code; that method compiles the portable
|
|
1716
|
-
* IR to an equivalent pipeline.
|
|
1717
|
-
*
|
|
1718
|
-
* Policy hooks (multi-tenant, soft-delete) inject context.filters
|
|
1719
|
-
* which are prepended as a `$match` stage.
|
|
1947
|
+
* Get the configured allowed sort fields.
|
|
1948
|
+
* Returns `undefined` if no whitelist is set (all fields allowed).
|
|
1720
1949
|
*/
|
|
1721
|
-
|
|
1950
|
+
get allowedSortFields(): string[] | undefined;
|
|
1722
1951
|
/**
|
|
1723
|
-
*
|
|
1724
|
-
* `
|
|
1725
|
-
* {$skip}, {$limit}]` against this repo's collection. Output rows
|
|
1726
|
-
* carry one key per `groupBy` column plus one key per measure alias
|
|
1727
|
-
* — identical shape to sqlitekit's `aggregate(req)`, so dashboards
|
|
1728
|
-
* work unchanged across backends.
|
|
1729
|
-
*
|
|
1730
|
-
* Without `groupBy`: returns a single-row result with just the
|
|
1731
|
-
* measures (scalar aggregation). Pass
|
|
1732
|
-
* `{ measures: { total: { op: 'sum', field: 'amount' } } }` for a
|
|
1733
|
-
* simple summary.
|
|
1734
|
-
*
|
|
1735
|
-
* For mongo-specific power features (`$lookup`, `$unwind`, `$facet`,
|
|
1736
|
-
* `$graphLookup`, window operators) reach for `aggregatePipeline`
|
|
1737
|
-
* — that's the kit-native escape hatch.
|
|
1738
|
-
*
|
|
1739
|
-
* Routes through the hook system for policy enforcement
|
|
1740
|
-
* (multi-tenant, soft-delete) — plugins inject their scope into
|
|
1741
|
-
* `context.filter`, which is merged into the pre-aggregate `$match`
|
|
1742
|
-
* before compilation.
|
|
1952
|
+
* Get the configured allowed operators.
|
|
1953
|
+
* Returns `undefined` if no whitelist is set (all built-in operators allowed).
|
|
1743
1954
|
*/
|
|
1744
|
-
|
|
1955
|
+
get allowedOperators(): string[] | undefined;
|
|
1745
1956
|
/**
|
|
1746
|
-
*
|
|
1747
|
-
* given tags. Delegates to the unified cache plugin's handle
|
|
1748
|
-
* (`repo.cache.invalidateByTags`) — wired by `cachePlugin({ adapter })`.
|
|
1749
|
-
*
|
|
1750
|
-
* Pass no tags to wipe the entire cache namespace (requires the
|
|
1751
|
-
* adapter to implement `clear`).
|
|
1957
|
+
* Parse URL query parameters into MongoDB query format
|
|
1752
1958
|
*
|
|
1753
|
-
*
|
|
1754
|
-
*
|
|
1755
|
-
*
|
|
1959
|
+
* @example
|
|
1960
|
+
* ```typescript
|
|
1961
|
+
* // URL: ?status=active&lookup[department][foreignField]=slug&sort=-createdAt&page=1
|
|
1962
|
+
* const query = parser.parse(req.query);
|
|
1963
|
+
* // Returns: { filters: {...}, lookups: [...], sort: {...}, page: 1 }
|
|
1964
|
+
* ```
|
|
1756
1965
|
*/
|
|
1757
|
-
|
|
1966
|
+
parse(query: Record<string, unknown> | null | undefined): ParsedQuery;
|
|
1758
1967
|
/**
|
|
1759
|
-
*
|
|
1760
|
-
*
|
|
1968
|
+
* Generate OpenAPI-compatible JSON Schema for query parameters.
|
|
1969
|
+
* Arc's defineResource() auto-detects this method and uses it
|
|
1970
|
+
* to document list endpoint query parameters in OpenAPI/Swagger.
|
|
1761
1971
|
*
|
|
1762
|
-
*
|
|
1763
|
-
*
|
|
1764
|
-
*
|
|
1765
|
-
*
|
|
1766
|
-
*
|
|
1767
|
-
* the data pipeline.
|
|
1768
|
-
* - **Keyset** — `pagination: 'keyset'` (or `after` set). Returns
|
|
1769
|
-
* `{ method: 'keyset', data, hasMore, next, limit }`. `sort` is
|
|
1770
|
-
* required — the cursor encodes the sort-key tuple of the last
|
|
1771
|
-
* row. Each subsequent page passes the previous `next` back as
|
|
1772
|
-
* `after`. Scales to arbitrary group counts because the planner
|
|
1773
|
-
* uses `(sort_keys) > (cursor)` instead of `OFFSET N`.
|
|
1972
|
+
* The schema respects parser configuration:
|
|
1973
|
+
* - `allowedOperators`: only documents allowed operators
|
|
1974
|
+
* - `allowedFilterFields`: generates explicit field[op] entries
|
|
1975
|
+
* - `enableLookups` / `enableAggregations`: includes/excludes lookup/aggregate params
|
|
1976
|
+
* - `maxLimit` / `maxSearchLength`: reflected in schema constraints
|
|
1774
1977
|
*/
|
|
1775
|
-
|
|
1978
|
+
getQuerySchema(): {
|
|
1979
|
+
type: 'object';
|
|
1980
|
+
properties: Record<string, unknown>;
|
|
1981
|
+
required?: string[];
|
|
1982
|
+
};
|
|
1776
1983
|
/**
|
|
1777
|
-
*
|
|
1778
|
-
*
|
|
1779
|
-
*
|
|
1780
|
-
*
|
|
1781
|
-
* Receives `finalReq` (already policy-scoped) so the cache key
|
|
1782
|
-
* built by the outer wrapper matches the request the executor
|
|
1783
|
-
* sees — no cross-tenant cache poisoning.
|
|
1984
|
+
* Get the query schema with OpenAPI extensions (x-internal metadata).
|
|
1985
|
+
* Use this when generating OpenAPI/Swagger docs — it includes a documentary
|
|
1986
|
+
* `_filterOperators` property describing available filter operators.
|
|
1987
|
+
* For validation-only schemas, use `getQuerySchema()` instead.
|
|
1784
1988
|
*/
|
|
1785
|
-
|
|
1989
|
+
getOpenAPIQuerySchema(): {
|
|
1990
|
+
type: 'object';
|
|
1991
|
+
properties: Record<string, unknown>;
|
|
1992
|
+
};
|
|
1786
1993
|
/**
|
|
1787
|
-
*
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
*
|
|
1792
|
-
* Each part is compiled through `compileFilterToMongo` BEFORE the
|
|
1793
|
-
* `$and` merge — otherwise an IR node like `eq('status', 'paid')`
|
|
1794
|
-
* (`{ op, field, value }`) gets nested inside `$and` as a literal,
|
|
1795
|
-
* `compileFilterToMongo` of the merged value finds no top-level `op`
|
|
1796
|
-
* (so `isFilter` is false), and Mongo receives the IR object as a
|
|
1797
|
-
* raw query clause that matches no real document. Symptom: aggregates
|
|
1798
|
-
* silently return 0 rows whenever a callerpasses IR + a policy
|
|
1799
|
-
* plugin is installed.
|
|
1994
|
+
* Get the JSON Schema type for a filter operator
|
|
1995
|
+
*/
|
|
1996
|
+
private _getOperatorSchemaType;
|
|
1997
|
+
/**
|
|
1998
|
+
* Get a human-readable description for a filter operator
|
|
1800
1999
|
*/
|
|
1801
|
-
private
|
|
2000
|
+
private _getOperatorDescription;
|
|
1802
2001
|
/**
|
|
1803
|
-
*
|
|
1804
|
-
* Routes through hook system for policy enforcement (multi-tenant, soft-delete)
|
|
2002
|
+
* Build a summary description of all available filter operators
|
|
1805
2003
|
*/
|
|
1806
|
-
|
|
2004
|
+
private _buildOperatorDescription;
|
|
1807
2005
|
/**
|
|
1808
|
-
*
|
|
1809
|
-
*
|
|
2006
|
+
* Parse lookup configurations from URL parameters
|
|
2007
|
+
*
|
|
2008
|
+
* Supported formats:
|
|
2009
|
+
* 1. Simple: ?lookup[department]=slug
|
|
2010
|
+
* → Join with 'departments' collection on slug field
|
|
2011
|
+
*
|
|
2012
|
+
* 2. Detailed: ?lookup[department][localField]=deptSlug&lookup[department][foreignField]=slug
|
|
2013
|
+
* → Full control over join configuration
|
|
2014
|
+
*
|
|
2015
|
+
* 3. Multiple: ?lookup[department]=slug&lookup[category]=categorySlug
|
|
2016
|
+
* → Multiple lookups
|
|
1810
2017
|
*
|
|
1811
2018
|
* @example
|
|
1812
2019
|
* ```typescript
|
|
1813
|
-
* //
|
|
1814
|
-
* const
|
|
1815
|
-
*
|
|
1816
|
-
* lookups: [
|
|
1817
|
-
* {
|
|
1818
|
-
* from: 'departments',
|
|
1819
|
-
* localField: 'departmentSlug',
|
|
1820
|
-
* foreignField: 'slug',
|
|
1821
|
-
* as: 'department',
|
|
1822
|
-
* single: true
|
|
1823
|
-
* }
|
|
1824
|
-
* ],
|
|
1825
|
-
* sort: '-createdAt',
|
|
1826
|
-
* page: 1,
|
|
1827
|
-
* limit: 50
|
|
2020
|
+
* // URL: ?lookup[department][localField]=deptSlug&lookup[department][foreignField]=slug&lookup[department][single]=true
|
|
2021
|
+
* const lookups = parser._parseLookups({
|
|
2022
|
+
* department: { localField: 'deptSlug', foreignField: 'slug', single: 'true' }
|
|
1828
2023
|
* });
|
|
2024
|
+
* // Returns: [{ from: 'departments', localField: 'deptSlug', foreignField: 'slug', single: true }]
|
|
1829
2025
|
* ```
|
|
1830
2026
|
*/
|
|
1831
|
-
|
|
2027
|
+
private _parseLookups;
|
|
1832
2028
|
/**
|
|
1833
|
-
*
|
|
1834
|
-
|
|
2029
|
+
* Parse a single lookup configuration
|
|
2030
|
+
*/
|
|
2031
|
+
private _parseSingleLookup;
|
|
2032
|
+
/**
|
|
2033
|
+
* Parse aggregation pipeline from URL (advanced feature)
|
|
1835
2034
|
*
|
|
1836
2035
|
* @example
|
|
1837
2036
|
* ```typescript
|
|
1838
|
-
*
|
|
1839
|
-
*
|
|
1840
|
-
*
|
|
1841
|
-
*
|
|
1842
|
-
* .sort({ count: -1 })
|
|
1843
|
-
* .build();
|
|
1844
|
-
*
|
|
1845
|
-
* const results = await repo.Model.aggregate(pipeline);
|
|
2037
|
+
* // URL: ?aggregate[group][_id]=$status&aggregate[group][count]=$sum:1
|
|
2038
|
+
* const pipeline = parser._parseAggregation({
|
|
2039
|
+
* group: { _id: '$status', count: '$sum:1' }
|
|
2040
|
+
* });
|
|
1846
2041
|
* ```
|
|
1847
2042
|
*/
|
|
1848
|
-
|
|
2043
|
+
private _parseAggregation;
|
|
1849
2044
|
/**
|
|
1850
|
-
*
|
|
1851
|
-
* Useful for building $lookup stages independently
|
|
2045
|
+
* Parse select/project fields
|
|
1852
2046
|
*
|
|
1853
2047
|
* @example
|
|
1854
2048
|
* ```typescript
|
|
1855
|
-
*
|
|
1856
|
-
*
|
|
1857
|
-
* .foreignField('slug')
|
|
1858
|
-
* .as('department')
|
|
1859
|
-
* .single()
|
|
1860
|
-
* .build();
|
|
1861
|
-
*
|
|
1862
|
-
* const pipeline = [
|
|
1863
|
-
* { $match: { status: 'active' } },
|
|
1864
|
-
* ...lookupStages
|
|
1865
|
-
* ];
|
|
2049
|
+
* // URL: ?select=name,email,-password
|
|
2050
|
+
* // Returns: { name: 1, email: 1, password: 0 }
|
|
1866
2051
|
* ```
|
|
1867
2052
|
*/
|
|
1868
|
-
|
|
2053
|
+
private _parseSelect;
|
|
1869
2054
|
/**
|
|
1870
|
-
*
|
|
1871
|
-
* transient failures.
|
|
1872
|
-
*
|
|
1873
|
-
* **BREAKING (3.10):** the callback now receives a **tx-bound repository**,
|
|
1874
|
-
* not a raw `ClientSession`. This matches repo-core's `StandardRepo.withTransaction`
|
|
1875
|
-
* contract that every kit (sqlitekit, pgkit, prismakit) already implements
|
|
1876
|
-
* — so cross-kit plugins and apps are portable. Session threading happens
|
|
1877
|
-
* transparently: every CRUD call on `txRepo` auto-injects the session into
|
|
1878
|
-
* its options bag, including plugin-added methods (`upsert`, `increment`,
|
|
1879
|
-
* `updateMany`, `restore`, ...). Nested `txRepo.withTransaction(...)` throws.
|
|
1880
|
-
*
|
|
1881
|
-
* For raw-session cross-repo workflows — multiple repositories coordinating
|
|
1882
|
-
* writes — use the standalone `withTransaction(connection, fn)` export
|
|
1883
|
-
* from `@classytic/mongokit`. That helper remains session-based by design.
|
|
1884
|
-
*
|
|
1885
|
-
* @param callback - Receives a tx-bound `Repository<TDoc>` — call its
|
|
1886
|
-
* methods directly, no need to pass `{ session }` manually
|
|
1887
|
-
* @param options.allowFallback - Run without transaction on standalone MongoDB (default: false)
|
|
1888
|
-
* @param options.onFallback - Called when falling back to non-transactional execution
|
|
1889
|
-
* @param options.transactionOptions - MongoDB driver transaction options (readConcern, writeConcern, etc.)
|
|
2055
|
+
* Parse populate parameter - handles both simple string and advanced object format
|
|
1890
2056
|
*
|
|
1891
2057
|
* @example
|
|
1892
2058
|
* ```typescript
|
|
1893
|
-
*
|
|
1894
|
-
*
|
|
1895
|
-
* await txRepo.update(created._id, { confirmed: true });
|
|
1896
|
-
* return created;
|
|
1897
|
-
* });
|
|
1898
|
-
*
|
|
1899
|
-
* // With fallback for standalone/dev environments
|
|
1900
|
-
* await repo.withTransaction(
|
|
1901
|
-
* async (txRepo) => {
|
|
1902
|
-
* await txRepo.create(doc);
|
|
1903
|
-
* },
|
|
1904
|
-
* {
|
|
1905
|
-
* allowFallback: true,
|
|
1906
|
-
* onFallback: (err) => logger.warn('Running without transaction', err),
|
|
1907
|
-
* },
|
|
1908
|
-
* );
|
|
2059
|
+
* // Simple: ?populate=author,category
|
|
2060
|
+
* // Returns: { simplePopulate: 'author,category', populateOptions: undefined }
|
|
1909
2061
|
*
|
|
1910
|
-
* //
|
|
1911
|
-
*
|
|
1912
|
-
* await withTransaction(mongoose.connection, async (session) => {
|
|
1913
|
-
* const order = await orderRepo.create(data, { session });
|
|
1914
|
-
* await inventoryRepo.decrement(..., { session });
|
|
1915
|
-
* });
|
|
2062
|
+
* // Advanced: ?populate[author][select]=name,email
|
|
2063
|
+
* // Returns: { simplePopulate: undefined, populateOptions: [{ path: 'author', select: 'name email' }] }
|
|
1916
2064
|
* ```
|
|
1917
2065
|
*/
|
|
1918
|
-
|
|
1919
|
-
/**
|
|
1920
|
-
* Execute custom query with event emission
|
|
1921
|
-
*/
|
|
1922
|
-
_executeQuery<T>(buildQuery: (Model: Model<TDoc>) => Promise<T>): Promise<T>;
|
|
2066
|
+
private _parsePopulate;
|
|
1923
2067
|
/**
|
|
1924
|
-
*
|
|
1925
|
-
*
|
|
1926
|
-
* Narrows `RepositoryBase._buildContext`'s return type to mongokit's
|
|
1927
|
-
* richer `RepositoryContext` (which carries `softDeleted`, `_cacheHit`,
|
|
1928
|
-
* populate / readPreference / session, etc.). Runtime is inherited from
|
|
1929
|
-
* the base class — this override is purely a type-narrowing shim.
|
|
1930
|
-
*
|
|
1931
|
-
* Hook execution order is deterministic:
|
|
1932
|
-
* 1. POLICY (100) — tenant isolation, soft-delete filtering, validation
|
|
1933
|
-
* 2. CACHE (200) — cache lookup (after policy filters are injected)
|
|
1934
|
-
* 3. OBSERVABILITY (300) — audit logging, metrics
|
|
1935
|
-
* 4. DEFAULT (500) — user-registered hooks
|
|
2068
|
+
* Parse a single populate configuration
|
|
1936
2069
|
*/
|
|
1937
|
-
|
|
2070
|
+
private _parseSinglePopulate;
|
|
1938
2071
|
/**
|
|
1939
|
-
*
|
|
1940
|
-
* `having`) and route any Filter IR through `compileFilterToMongo`.
|
|
1941
|
-
* Plain Mongo queries pass through untouched — `isFilter` is the
|
|
1942
|
-
* discriminator. Returns a shallow clone; never mutates input.
|
|
2072
|
+
* Convert populate match values (handles boolean strings, etc.)
|
|
1943
2073
|
*/
|
|
1944
|
-
private
|
|
2074
|
+
private _convertPopulateMatch;
|
|
1945
2075
|
/**
|
|
1946
|
-
*
|
|
1947
|
-
* (warns + drops explicit sort when $near is in filters).
|
|
2076
|
+
* Parse filter parameters
|
|
1948
2077
|
*/
|
|
1949
|
-
private
|
|
2078
|
+
private _parseFilters;
|
|
1950
2079
|
/**
|
|
1951
|
-
*
|
|
2080
|
+
* Handle operator syntax: field[operator]=value
|
|
1952
2081
|
*/
|
|
1953
|
-
private
|
|
2082
|
+
private _handleOperatorSyntax;
|
|
1954
2083
|
/**
|
|
1955
|
-
*
|
|
1956
|
-
* search ($text), regex search ($or of $regex), and the search-resolver
|
|
1957
|
-
* plugin contract (search already cleared by a before:getAll hook).
|
|
2084
|
+
* Handle bracket syntax with object value
|
|
1958
2085
|
*/
|
|
1959
|
-
private
|
|
2086
|
+
private _handleBracketSyntax;
|
|
2087
|
+
private _parseSort;
|
|
2088
|
+
private _toMongoOperator;
|
|
2089
|
+
private _createSafeRegex;
|
|
2090
|
+
private _escapeRegex;
|
|
1960
2091
|
/**
|
|
1961
|
-
*
|
|
2092
|
+
* Sanitize $match configuration to prevent dangerous operators
|
|
2093
|
+
* Recursively filters out operators like $where, $function, $accumulator
|
|
1962
2094
|
*/
|
|
1963
|
-
|
|
2095
|
+
private _sanitizeMatchConfig;
|
|
1964
2096
|
/**
|
|
1965
|
-
*
|
|
2097
|
+
* Sanitize pipeline stages for use in $lookup.
|
|
2098
|
+
* Blocks dangerous stages ($out, $merge, etc.) and recursively sanitizes
|
|
2099
|
+
* operator expressions within $match, $addFields, and $set stages.
|
|
1966
2100
|
*/
|
|
1967
|
-
|
|
2101
|
+
private _sanitizePipeline;
|
|
1968
2102
|
/**
|
|
1969
|
-
*
|
|
1970
|
-
*
|
|
1971
|
-
* Used by arc's outbox / idempotency adapters to distinguish
|
|
1972
|
-
* "write already landed (idempotent no-op)" from "transient DB error
|
|
1973
|
-
* (must retry)". Every backend signals duplicate keys differently —
|
|
1974
|
-
* MongoDB `code: 11000`, Prisma `P2002`, Postgres `23505` — so this
|
|
1975
|
-
* predicate lives on the kit that knows its driver. Cross-driver
|
|
1976
|
-
* adapters then depend only on the boolean outcome, not the shape of
|
|
1977
|
-
* the underlying error.
|
|
1978
|
-
*
|
|
1979
|
-
* Deliberately narrow: matches ONLY `code === 11000` and
|
|
1980
|
-
* `codeName === 'DuplicateKey'`. Does NOT match
|
|
1981
|
-
* `err.name === 'MongoServerError'`, which is also true for
|
|
1982
|
-
* WriteConflict, NotWritablePrimary, ExceededTimeLimit, and every
|
|
1983
|
-
* other server-side error — treating those as duplicate keys would
|
|
1984
|
-
* silently swallow transactional retries.
|
|
2103
|
+
* Recursively sanitize expression objects, blocking dangerous operators
|
|
2104
|
+
* like $where, $function, $accumulator inside $addFields/$set stages.
|
|
1985
2105
|
*
|
|
1986
|
-
*
|
|
1987
|
-
*
|
|
1988
|
-
*
|
|
1989
|
-
*
|
|
1990
|
-
* if (outboxRepo.isDuplicateKeyError(err)) {
|
|
1991
|
-
* // Idempotent no-op — event was already written by a prior attempt.
|
|
1992
|
-
* return;
|
|
1993
|
-
* }
|
|
1994
|
-
* throw err; // transient DB error — upstream retries
|
|
1995
|
-
* }
|
|
2106
|
+
* Depth-guarded — same protection `_sanitizeMatchConfig` ships, since
|
|
2107
|
+
* a hostile `$cond`/`$switch` chain can recurse arbitrarily deep and
|
|
2108
|
+
* exhaust JS stack. Without the guard, this is a one-liner DoS against
|
|
2109
|
+
* any host that exposes pipeline parsing to untrusted callers.
|
|
1996
2110
|
*/
|
|
1997
|
-
|
|
2111
|
+
private _sanitizeExpressions;
|
|
2112
|
+
private _sanitizeSearch;
|
|
1998
2113
|
/**
|
|
1999
|
-
*
|
|
2114
|
+
* Build regex-based multi-field search filters
|
|
2115
|
+
* Creates an $or query with case-insensitive regex across all searchFields
|
|
2000
2116
|
*
|
|
2001
|
-
*
|
|
2002
|
-
*
|
|
2003
|
-
*
|
|
2004
|
-
*
|
|
2005
|
-
*
|
|
2006
|
-
*
|
|
2007
|
-
*
|
|
2008
|
-
*
|
|
2009
|
-
* retried by the driver. To keep the standard MongoDB transaction-
|
|
2010
|
-
* retry mechanism intact, we re-throw any error that carries
|
|
2011
|
-
* `errorLabels` exactly as we received it. The caller still sees a
|
|
2012
|
-
* thrown error — only the wrapping layer is skipped.
|
|
2117
|
+
* @example
|
|
2118
|
+
* // searchFields: ['name', 'description', 'sku']
|
|
2119
|
+
* // search: 'azure'
|
|
2120
|
+
* // Returns: [
|
|
2121
|
+
* // { name: { $regex: /azure/i } },
|
|
2122
|
+
* // { description: { $regex: /azure/i } },
|
|
2123
|
+
* // { sku: { $regex: /azure/i } }
|
|
2124
|
+
* // ]
|
|
2013
2125
|
*/
|
|
2014
|
-
|
|
2126
|
+
private _buildRegexSearch;
|
|
2127
|
+
private _parseOr;
|
|
2128
|
+
private _enhanceWithBetween;
|
|
2129
|
+
private _pluralize;
|
|
2130
|
+
private _capitalize;
|
|
2015
2131
|
}
|
|
2016
2132
|
//#endregion
|
|
2017
2133
|
//#region src/transaction.d.ts
|
|
@@ -2313,17 +2429,4 @@ declare function createOptionsExtractor<TCtx extends Record<string, unknown>>(fi
|
|
|
2313
2429
|
*/
|
|
2314
2430
|
declare function systemContext(extra?: SessionOptions): SessionOptions;
|
|
2315
2431
|
//#endregion
|
|
2316
|
-
|
|
2317
|
-
/**
|
|
2318
|
-
* Factory function to create a repository instance
|
|
2319
|
-
*
|
|
2320
|
-
* @param Model - Mongoose model
|
|
2321
|
-
* @param plugins - Array of plugins to apply
|
|
2322
|
-
* @returns Repository instance
|
|
2323
|
-
*
|
|
2324
|
-
* @example
|
|
2325
|
-
* const userRepo = createRepository(UserModel, [timestampPlugin()]);
|
|
2326
|
-
*/
|
|
2327
|
-
declare function createRepository<TDoc>(Model: _$mongoose.Model<TDoc, any, any, any>, plugins?: PluginType[], paginationConfig?: PaginationConfig, options?: RepositoryOptions): Repository<TDoc>;
|
|
2328
|
-
//#endregion
|
|
2329
|
-
export { ALL_OPERATIONS, type AggregateHelpersMethods, type AggregateOptions, type AggregatePaginationOptions, AggregationBuilder, type AllPluginMethods, type AnyDocument, type AnyModel, type AuditEntry, type AuditOperation, type AuditQueryOptions, type AuditQueryResult, type AuditTrailMethods, type AuditTrailOptions, AuditTrailQuery, type BasePaginationOptions, type BatchOperationsMethods, type BulkWriteResult, type CacheAdapter, type CacheOperationOptions, type CacheOptions, type CacheableOptions, type CascadeOptions, type CascadeRelation, type CollationOptions, type CreateInput, type CreateOptions, type CursorPayload, type CustomIdOptions, type DateSequentialIdOptions, type DecodedCursor, type DeepPartial, type DeleteResult, type DocField, type ElasticSearchOptions, type EventHandlers, type EventPayload, type EventPhase, type FieldPreset, type FieldType, type FilterQuery, type FindOneAndUpdateOptions, type GroupResult, HOOK_PRIORITY, type HookMode, type IController, type IControllerResponse, type IRequestContext, type IResponseFormatter, type IdGenerator, type InferDocument, type InferRawDoc, type KeysOfType, type KeysetPaginationOptions, type LeaseMethods, type LeasePluginOptions, type Logger, LookupBuilder, type LookupOptions, type LookupPopulateOptions, type LookupPopulateResult, MUTATING_OPERATIONS, type Middleware, type MiddlewareContext, type MinMaxResult, type MinimalRepoView, type MongoOperationsMethods, type MongoOperatorUpdate, type MultiTenantOptions, type NonNullableFields, OP_REGISTRY, type ObjectId, type ObservabilityOptions, type OffsetPaginationOptions, type OperationDescriptor, type OperationMetric, type OperationOptions, type PaginationConfig, PaginationEngine, type ParsedQuery, type PartialBy, type Plugin, type PluginFunction, type PluginType, type PolicyKey, type PopulateOption, type PopulateSpec, type PrefixedIdOptions, type PrioritizedHook, QueryParser, type QueryParserOptions, READ_OPERATIONS, type ReadOptions, type ReadPreferenceType, Repository, Repository as default, type RepositoryCacheHandle, type RepositoryCachePluginOptions, type RepositoryContext, type RepositoryEvent, type RepositoryInstance, type RepositoryOperation, type RepositoryOptions, type RequiredBy, type SchemaLike, type SearchMode, type SelectSpec, type SequentialIdOptions, type SessionOptions, type SoftDeleteFilterMode, type SoftDeleteMethods, type SoftDeleteOptions, type SoftDeleteRepository, type SortDirection, type SortSpec, type Strict, type SubdocumentMethods, type TenantContext, type TenantStore, type UpdateManyResult, type UpdateOptions, type UpdatePatch, type UpdateWithValidationResult, type UserContext, type ValidationChainOptions, type ValidatorDefinition, type ValueType, type WithPlugins, type WithTransactionOptions, index_d_exports as actions, adminBypass, aggregateHelpersPlugin, auditLogPlugin, auditTrailPlugin, autoInject, batchOperationsPlugin, batchTransaction, blockIf, buildCrudSchemasFromModel, buildCrudSchemasFromMongooseSchema, cachePlugin, cascadePlugin, compileFilterToMongo, configureLogger, createError, createFieldPreset, createMemoryCache, createOptionsExtractor, createRepository, createTenantContext, customIdPlugin, dateSequentialId, elasticSearchPlugin, fieldFilterPlugin, filterResponseData, getFieldsForUser, getMongooseProjection, getNextSequence, immutableField, isDuplicateKeyError, isTransactionUnsupported, leasePlugin, methodRegistryPlugin, mongoOperationsPlugin, multiTenantPlugin, observabilityPlugin, operationsByPolicyKey, parseDuplicateKeyError, prefixedId, repoOptionsFromCtx, requireField, sequentialId, softDeletePlugin, subdocumentPlugin, systemContext, timestampPlugin, uniqueField, validationChainPlugin, withTransaction };
|
|
2432
|
+
export { ALL_OPERATIONS, type AggregateHelpersMethods, type AggregateOptions, type AggregatePaginationOptions, AggregationBuilder, type AllPluginMethods, type AnyDocument, type AnyModel, type AuditConfig, type AuditEntry, type AuditOperation, type AuditQueryOptions, type AuditQueryResult, type AuditTrailMethods, type AuditTrailOptions, AuditTrailQuery, type BasePaginationOptions, type BatchOperationsMethods, type BulkWriteResult, type CacheAdapter, type CacheOperationOptions, type CacheOptions, type CacheableOptions, type CascadeOptions, type CascadeRelation, type CollationOptions, type CreateInput, type CreateOptions, type CreateRepositoryConfig, type CursorPayload, type CustomIdOptions, type DateSequentialIdOptions, type DecodedCursor, type DeepPartial, type DeleteResult, type DocField, type ElasticSearchOptions, type EventHandlers, type EventPayload, type EventPhase, type FieldPreset, type FieldType, type FilterQuery, type FindOneAndUpdateOptions, type GroupResult, HOOK_PRIORITY, type HookMode, type IController, type IControllerResponse, type IRequestContext, type IResponseFormatter, type IdGenerator, type InferDocument, type InferRawDoc, type KeysOfType, type KeysetPaginationOptions, type LeaseMethods, type LeasePluginOptions, type Logger, LookupBuilder, type LookupOptions, type LookupPopulateOptions, type LookupPopulateResult, MONGOKIT_CAPABILITIES, MUTATING_OPERATIONS, type Middleware, type MiddlewareContext, type MinMaxResult, type MinimalRepoView, type MongoOperationsMethods, type MongoOperatorUpdate, type MultiTenantOptions, type NonNullableFields, OP_REGISTRY, type ObjectId, type ObservabilityOptions, type OffsetPaginationOptions, type OperationDescriptor, type OperationMetric, type OperationOptions, type PaginationConfig, PaginationEngine, type ParsedQuery, type PartialBy, type Plugin, type PluginFunction, type PluginType, type PolicyKey, type PopulateOption, type PopulateSpec, type PrefixedIdOptions, type PrioritizedHook, QueryParser, type QueryParserOptions, READ_OPERATIONS, type ReadOptions, type ReadPreferenceType, Repository, Repository as default, type RepositoryCacheHandle, type RepositoryCachePluginOptions, type RepositoryContext, type RepositoryEvent, type RepositoryInstance, type RepositoryOperation, type RepositoryOptions, type RequiredBy, type SchemaLike, type SearchMode, type SelectSpec, type SequentialIdOptions, type SessionOptions, type SoftDeleteFilterMode, type SoftDeleteMethods, type SoftDeleteOptions, type SoftDeleteRepository, type SortDirection, type SortSpec, type Strict, type SubdocumentMethods, type TenantContext, type TenantStore, type UpdateManyResult, type UpdateOptions, type UpdatePatch, type UpdateWithValidationResult, type UserContext, type ValidationChainOptions, type ValidatorDefinition, type ValueType, type WithPlugins, type WithTransactionOptions, index_d_exports as actions, adminBypass, aggregateHelpersPlugin, auditLogPlugin, auditTrailPlugin, autoInject, batchOperationsPlugin, batchTransaction, blockIf, buildCrudSchemasFromModel, buildCrudSchemasFromMongooseSchema, cachePlugin, cascadePlugin, compileFilterToMongo, configureLogger, createError, createFieldPreset, createMemoryCache, createOptionsExtractor, createRepository, createTenantContext, customIdPlugin, dateSequentialId, elasticSearchPlugin, fieldFilterPlugin, filterResponseData, getFieldsForUser, getMongooseProjection, getNextSequence, immutableField, isDuplicateKeyError, isTransactionUnsupported, leasePlugin, methodRegistryPlugin, mongoOperationsPlugin, multiTenantPlugin, observabilityPlugin, operationsByPolicyKey, parseDuplicateKeyError, prefixedId, repoOptionsFromCtx, requireField, sequentialId, softDeletePlugin, subdocumentPlugin, systemContext, timestampPlugin, uniqueField, validationChainPlugin, withTransaction };
|