@classytic/mongokit 3.3.2 → 3.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (28) hide show
  1. package/README.md +137 -7
  2. package/dist/PaginationEngine-nY04eGUM.mjs +290 -0
  3. package/dist/actions/index.d.mts +2 -9
  4. package/dist/actions/index.mjs +3 -5
  5. package/dist/ai/index.d.mts +1 -1
  6. package/dist/ai/index.mjs +3 -3
  7. package/dist/chunk-CfYAbeIz.mjs +13 -0
  8. package/dist/{limits-s1-d8rWb.mjs → cursor-CHToazHy.mjs} +122 -171
  9. package/dist/{logger-D8ily-PP.mjs → error-Bpbi_NKo.mjs} +34 -22
  10. package/dist/{cache-keys-CzFwVnLy.mjs → field-selection-reyDRzXf.mjs} +110 -112
  11. package/dist/{aggregate-BkOG9qwr.d.mts → index-BuoZIZ15.d.mts} +132 -129
  12. package/dist/index.d.mts +549 -543
  13. package/dist/index.mjs +33 -101
  14. package/dist/{mongooseToJsonSchema-D_i2Am_O.mjs → mongooseToJsonSchema-B6Qyl8BK.mjs} +13 -12
  15. package/dist/{mongooseToJsonSchema-B6O2ED3n.d.mts → mongooseToJsonSchema-RX9YfJLu.d.mts} +24 -17
  16. package/dist/pagination/PaginationEngine.d.mts +1 -1
  17. package/dist/pagination/PaginationEngine.mjs +2 -209
  18. package/dist/plugins/index.d.mts +1 -2
  19. package/dist/plugins/index.mjs +2 -3
  20. package/dist/sort-C-BJEWUZ.mjs +57 -0
  21. package/dist/{types-pVY0w1Pp.d.mts → types-COINbsdL.d.mts} +57 -27
  22. package/dist/{aggregate-BClp040M.mjs → update-DGKMmBgG.mjs} +575 -565
  23. package/dist/utils/index.d.mts +2 -2
  24. package/dist/utils/index.mjs +4 -5
  25. package/dist/{custom-id.plugin-BJ3FSnzt.d.mts → validation-chain.plugin-BNoaKDOm.d.mts} +832 -832
  26. package/dist/{custom-id.plugin-FInXDsUX.mjs → validation-chain.plugin-da3fOo8A.mjs} +2410 -2246
  27. package/package.json +11 -6
  28. package/dist/chunk-DQk6qfdC.mjs +0 -18
package/dist/index.d.mts CHANGED
@@ -1,10 +1,9 @@
1
- import { $ as SchemaBuilderOptions, A as KeysetPaginationOptions, B as PaginationResult, C as GroupResult, D as InferRawDoc, E as InferDocument, F as ObjectId, G as PopulateSpec, H as Plugin, I as OffsetPaginationOptions, J as RepositoryEvent, K as ReadPreferenceType, L as OffsetPaginationResult, M as Logger, N as MinMaxResult, O as JsonSchema, P as NonNullableFields, Q as RequiredBy, R as OperationOptions, S as FieldRules, St as LookupOptions, T as HttpError, U as PluginFunction, V as PartialBy, W as PluginType, X as RepositoryOperation, Y as RepositoryInstance, Z as RepositoryOptions, _ as DeleteResult, _t as IController, a as AnyModel, at as SortSpec, b as EventPhase, bt as IResponseFormatter, c as CacheOptions, ct as UpdateManyResult, d as CascadeRelation, dt as UserContext, et as SelectSpec, f as CreateInput, ft as ValidationChainOptions, g as DeepPartial, gt as WithTransactionOptions, h as DecodedCursor, ht as WithPlugins, i as AnyDocument, it as SortDirection, j as KeysetPaginationResult, k as KeysOfType, l as CacheStats, lt as UpdateOptions, m as CrudSchemas, mt as ValidatorDefinition, n as AggregatePaginationResult, nt as SoftDeleteOptions, o as CacheAdapter, ot as Strict, p as CreateOptions, pt as ValidationResult, q as RepositoryContext, r as AllPluginMethods, rt as SoftDeleteRepository, s as CacheOperationOptions, st as UpdateInput, t as AggregatePaginationOptions, tt as SoftDeleteFilterMode, u as CascadeOptions, ut as UpdateWithValidationResult, v as EventHandlers, vt as IControllerResponse, w as HookMode, x as FieldPreset, xt as LookupBuilder, y as EventPayload, yt as IRequestContext, z as PaginationConfig } from "./types-pVY0w1Pp.mjs";
2
- import "./aggregate-BkOG9qwr.mjs";
3
- import { t as index_d_exports } from "./actions/index.mjs";
1
+ import { $ as RequiredBy, A as KeysOfType, B as PaginationConfig, C as FieldRules, Ct as LookupOptions, D as InferDocument, E as HttpError, F as NonNullableFields, G as PluginType, H as PartialBy, I as ObjectId, J as RepositoryContext, K as PopulateSpec, L as OffsetPaginationOptions, M as KeysetPaginationResult, N as Logger, O as InferRawDoc, P as MinMaxResult, Q as RepositoryOptions, R as OffsetPaginationResult, S as FieldPreset, St as LookupBuilder, T as HookMode, U as Plugin, V as PaginationResult, W as PluginFunction, X as RepositoryInstance, Y as RepositoryEvent, Z as RepositoryOperation, _ as DeepPartial, _t as WithTransactionOptions, a as AnyModel, at as SortDirection, b as EventPayload, bt as IRequestContext, c as CacheOptions, ct as UpdateInput, d as CascadeRelation, dt as UpdateWithValidationResult, et as SchemaBuilderOptions, f as CollationOptions, ft as UserContext, g as DecodedCursor, gt as WithPlugins, h as CrudSchemas, ht as ValidatorDefinition, i as AnyDocument, it as SoftDeleteRepository, j as KeysetPaginationOptions, k as JsonSchema, l as CacheStats, lt as UpdateManyResult, m as CreateOptions, mt as ValidationResult, n as AggregatePaginationResult, nt as SoftDeleteFilterMode, o as CacheAdapter, ot as SortSpec, p as CreateInput, pt as ValidationChainOptions, q as ReadPreferenceType, r as AllPluginMethods, rt as SoftDeleteOptions, s as CacheOperationOptions, st as Strict, t as AggregatePaginationOptions, tt as SelectSpec, u as CascadeOptions, ut as UpdateOptions, v as DeleteResult, vt as IController, w as GroupResult, x as EventPhase, xt as IResponseFormatter, y as EventHandlers, yt as IControllerResponse, z as OperationOptions } from "./types-COINbsdL.mjs";
2
+ import { t as index_d_exports } from "./index-BuoZIZ15.mjs";
4
3
  import { PaginationEngine } from "./pagination/PaginationEngine.mjs";
5
- import { A as subdocumentPlugin, B as immutableField, C as observabilityPlugin, D as CacheMethods, E as cascadePlugin, F as batchOperationsPlugin, G as methodRegistryPlugin, H as uniqueField, I as MongoOperationsMethods, J as auditLogPlugin, K as SoftDeleteMethods, L as mongoOperationsPlugin, M as aggregateHelpersPlugin, N as BatchOperationsMethods, O as cachePlugin, P as BulkWriteResult, R as autoInject, S as OperationMetric, T as multiTenantPlugin, U as validationChainPlugin, V as requireField, X as fieldFilterPlugin, Y as timestampPlugin, _ as AuditTrailMethods, a as SequentialIdOptions, b as auditTrailPlugin, c as getNextSequence, d as ElasticSearchOptions, f as elasticSearchPlugin, g as AuditQueryResult, h as AuditQueryOptions, i as PrefixedIdOptions, j as AggregateHelpersMethods, k as SubdocumentMethods, l as prefixedId, m as AuditOperation, n as DateSequentialIdOptions, o as customIdPlugin, p as AuditEntry, q as softDeletePlugin, r as IdGenerator, s as dateSequentialId, t as CustomIdOptions, u as sequentialId, v as AuditTrailOptions, w as MultiTenantOptions, x as ObservabilityOptions, y as AuditTrailQuery, z as blockIf } from "./custom-id.plugin-BJ3FSnzt.mjs";
6
- import { a as isFieldUpdateAllowed, c as configureLogger, d as filterResponseData, f as getFieldsForUser, i as getSystemManagedFields, l as createError, n as buildCrudSchemasFromMongooseSchema, o as validateUpdateBody, p as getMongooseProjection, r as getImmutableFields, s as createMemoryCache, t as buildCrudSchemasFromModel, u as createFieldPreset } from "./mongooseToJsonSchema-B6O2ED3n.mjs";
7
- import * as mongoose$1 from "mongoose";
4
+ import { A as dateSequentialId, B as AuditEntry, C as elasticSearchPlugin, D as PrefixedIdOptions, E as IdGenerator, F as CacheMethods, G as AuditTrailOptions, H as AuditQueryOptions, I as cachePlugin, J as auditLogPlugin, K as AuditTrailQuery, L as BatchOperationsMethods, M as prefixedId, N as sequentialId, O as SequentialIdOptions, P as cascadePlugin, R as BulkWriteResult, S as ElasticSearchOptions, T as DateSequentialIdOptions, U as AuditQueryResult, V as AuditOperation, W as AuditTrailMethods, X as aggregateHelpersPlugin, Y as AggregateHelpersMethods, _ as MongoOperationsMethods, a as uniqueField, b as methodRegistryPlugin, c as SubdocumentMethods, d as softDeletePlugin, f as ObservabilityOptions, g as multiTenantPlugin, h as MultiTenantOptions, i as requireField, j as getNextSequence, k as customIdPlugin, l as subdocumentPlugin, m as observabilityPlugin, n as blockIf, o as validationChainPlugin, p as OperationMetric, q as auditTrailPlugin, r as immutableField, s as timestampPlugin, t as autoInject, u as SoftDeleteMethods, v as mongoOperationsPlugin, w as CustomIdOptions, x as fieldFilterPlugin, z as batchOperationsPlugin } from "./validation-chain.plugin-BNoaKDOm.mjs";
5
+ import { a as isFieldUpdateAllowed, c as configureLogger, d as getFieldsForUser, f as getMongooseProjection, i as getSystemManagedFields, l as createFieldPreset, m as parseDuplicateKeyError, n as buildCrudSchemasFromMongooseSchema, o as validateUpdateBody, p as createError, r as getImmutableFields, s as createMemoryCache, t as buildCrudSchemasFromModel, u as filterResponseData } from "./mongooseToJsonSchema-RX9YfJLu.mjs";
6
+ import * as _$mongoose from "mongoose";
8
7
  import { ClientSession, Expression, Model, PipelineStage, PopulateOptions } from "mongoose";
9
8
 
10
9
  //#region src/query/AggregationBuilder.d.ts
@@ -67,7 +66,7 @@ declare class AggregationBuilder {
67
66
  * .exec(MyModel);
68
67
  * ```
69
68
  */
70
- exec<T = unknown>(model: Model<any>, session?: mongoose$1.ClientSession): Promise<T[]>;
69
+ exec<T = unknown>(model: Model<any>, session?: _$mongoose.ClientSession): Promise<T[]>;
71
70
  /**
72
71
  * Reset the pipeline
73
72
  */
@@ -428,667 +427,674 @@ declare class AggregationBuilder {
428
427
  static startWith(query: Record<string, unknown>): AggregationBuilder;
429
428
  }
430
429
  //#endregion
431
- //#region src/Repository.d.ts
432
- type HookListener = (data: any) => void | Promise<void>;
433
- /** Hook with priority for phase ordering */
434
- interface PrioritizedHook {
435
- listener: HookListener;
436
- priority: number;
437
- }
438
- /**
439
- * Plugin phase priorities (lower = runs first)
440
- * Policy hooks (multi-tenant, soft-delete, validation) MUST run before cache
441
- * to ensure filters are injected before cache keys are computed.
442
- */
443
- declare const HOOK_PRIORITY: {
444
- /** Policy enforcement: tenant isolation, soft-delete filtering, validation */readonly POLICY: 100; /** Caching: lookup/store after policy filters are applied */
445
- readonly CACHE: 200; /** Observability: audit logging, metrics, telemetry */
446
- readonly OBSERVABILITY: 300; /** Default priority for user-registered hooks */
447
- readonly DEFAULT: 500;
448
- };
430
+ //#region src/query/QueryParser.d.ts
431
+ type SortSpec$1 = Record<string, 1 | -1>;
432
+ type FilterQuery = Record<string, unknown>;
449
433
  /**
450
- * Production-grade repository for MongoDB
451
- * Event-driven, plugin-based, with smart pagination
434
+ * Mongoose-compatible populate option
435
+ * Supports advanced populate with select, match, limit, sort, and nested populate
436
+ *
437
+ * @example
438
+ * ```typescript
439
+ * // URL: ?populate[author][select]=name,email&populate[author][match][active]=true
440
+ * // Generates: { path: 'author', select: 'name email', match: { active: true } }
441
+ * ```
452
442
  */
453
- declare class Repository<TDoc = any> {
454
- readonly Model: Model<TDoc>;
455
- readonly model: string;
456
- readonly _hooks: Map<string, PrioritizedHook[]>;
457
- readonly _pagination: PaginationEngine<TDoc>;
458
- private readonly _hookMode;
459
- [key: string]: unknown;
460
- private _hasTextIndex;
461
- constructor(Model: Model<TDoc, any, any, any>, plugins?: PluginType[], paginationConfig?: PaginationConfig, options?: RepositoryOptions);
443
+ interface PopulateOption {
444
+ /** Field path to populate */
445
+ path: string;
446
+ /** Fields to select (space-separated) */
447
+ select?: string;
448
+ /** Filter conditions for populated documents */
449
+ match?: Record<string, unknown>;
450
+ /** Query options (limit, sort, skip) */
451
+ options?: {
452
+ limit?: number;
453
+ sort?: SortSpec$1;
454
+ skip?: number;
455
+ };
456
+ /** Nested populate configuration */
457
+ populate?: PopulateOption;
458
+ }
459
+ /** Parsed query result with optional lookup configuration */
460
+ interface ParsedQuery {
461
+ /** MongoDB filter query */
462
+ filters: FilterQuery;
463
+ /** Sort specification */
464
+ sort?: SortSpec$1;
465
+ /** Fields to populate (simple comma-separated string) */
466
+ populate?: string;
462
467
  /**
463
- * Register a plugin
468
+ * Advanced populate options (Mongoose-compatible)
469
+ * When this is set, `populate` will be undefined
470
+ * @example [{ path: 'author', select: 'name email' }]
464
471
  */
465
- use(plugin: PluginType): this;
472
+ populateOptions?: PopulateOption[];
473
+ /** Page number for offset pagination */
474
+ page?: number;
475
+ /** Cursor for keyset pagination */
476
+ after?: string;
477
+ /** Limit */
478
+ limit: number;
479
+ /** Full-text search query */
480
+ search?: string;
481
+ /** Lookup configurations for custom field joins */
482
+ lookups?: LookupOptions[];
483
+ /** Aggregation pipeline stages (advanced) */
484
+ aggregation?: PipelineStage[];
485
+ /** Select/project fields */
486
+ select?: Record<string, 0 | 1>;
487
+ }
488
+ /** Search mode for query parser */
489
+ type SearchMode = 'text' | 'regex';
490
+ interface QueryParserOptions {
491
+ /** Maximum allowed regex pattern length (default: 500) */
492
+ maxRegexLength?: number;
493
+ /** Maximum allowed text search query length (default: 200) */
494
+ maxSearchLength?: number;
495
+ /** Maximum allowed filter depth (default: 10) */
496
+ maxFilterDepth?: number;
497
+ /** Maximum allowed limit value (default: 1000) */
498
+ maxLimit?: number;
499
+ /** Additional operators to block */
500
+ additionalDangerousOperators?: string[];
501
+ /** Enable lookup parsing (default: true) */
502
+ enableLookups?: boolean;
503
+ /** Enable aggregation parsing (default: false - requires explicit opt-in) */
504
+ enableAggregations?: boolean;
466
505
  /**
467
- * Register event listener with optional priority for phase ordering.
468
- *
469
- * @param event - Event name (e.g. 'before:getAll')
470
- * @param listener - Hook function
471
- * @param options - Optional { priority } — use HOOK_PRIORITY constants.
472
- * Lower priority numbers run first.
473
- * Default: HOOK_PRIORITY.DEFAULT (500)
506
+ * Search mode (default: 'text')
507
+ * - 'text': Uses MongoDB $text search (requires text index)
508
+ * - 'regex': Uses $or with $regex across searchFields (no index required)
474
509
  */
475
- on(event: string, listener: HookListener, options?: {
476
- priority?: number;
477
- }): this;
510
+ searchMode?: SearchMode;
478
511
  /**
479
- * Remove a specific event listener
512
+ * Fields to search when searchMode is 'regex'
513
+ * Required when searchMode is 'regex'
514
+ * @example ['name', 'description', 'sku', 'tags']
480
515
  */
481
- off(event: string, listener: HookListener): this;
516
+ searchFields?: string[];
482
517
  /**
483
- * Remove all listeners for an event, or all listeners entirely
518
+ * Whitelist of collection names allowed in lookups.
519
+ * When set, only these collections can be used in $lookup stages.
520
+ * When undefined, all collection names are allowed.
521
+ * @example ['departments', 'categories', 'users']
484
522
  */
485
- removeAllListeners(event?: string): this;
523
+ allowedLookupCollections?: string[];
524
+ /** Allowed fields for filtering. If set, ignores unknown fields. */
525
+ allowedFilterFields?: string[];
526
+ /** Allowed fields for sorting. If set, ignores unknown fields. */
527
+ allowedSortFields?: string[];
486
528
  /**
487
- * Emit event (sync - for backwards compatibility)
529
+ * Whitelist of allowed filter operators.
530
+ * When set, only these operators can be used in filters.
531
+ * When undefined, all built-in operators are allowed.
532
+ * Values are human-readable keys: 'eq', 'ne', 'gt', 'gte', 'lt', 'lte', 'in', 'nin',
533
+ * 'like', 'contains', 'regex', 'exists', 'size', 'type'
534
+ * @example ['eq', 'ne', 'gt', 'gte', 'lt', 'lte', 'in']
488
535
  */
489
- emit(event: string, data: unknown): void;
536
+ allowedOperators?: string[];
537
+ }
538
+ /**
539
+ * Modern Query Parser
540
+ * Converts URL parameters to MongoDB queries with $lookup support
541
+ */
542
+ declare class QueryParser {
543
+ private readonly options;
544
+ private readonly operators;
545
+ private readonly dangerousOperators;
490
546
  /**
491
- * Emit event and await all async handlers (sorted by priority)
547
+ * Regex patterns that can cause catastrophic backtracking (ReDoS attacks)
548
+ * Detects:
549
+ * - Quantifiers: {n,m}
550
+ * - Possessive quantifiers: *+, ++, ?+
551
+ * - Nested quantifiers: (a+)+, (a*)*
552
+ * - Backreferences: \1, \2, etc.
553
+ * - Complex character classes: [...]...[...]
492
554
  */
493
- emitAsync(event: string, data: unknown): Promise<void>;
494
- private _emitHook;
495
- private _emitErrorHook;
555
+ private readonly dangerousRegexPatterns;
556
+ constructor(options?: QueryParserOptions);
496
557
  /**
497
- * Create single document
558
+ * Parse URL query parameters into MongoDB query format
559
+ *
560
+ * @example
561
+ * ```typescript
562
+ * // URL: ?status=active&lookup[department][foreignField]=slug&sort=-createdAt&page=1
563
+ * const query = parser.parse(req.query);
564
+ * // Returns: { filters: {...}, lookups: [...], sort: {...}, page: 1 }
565
+ * ```
498
566
  */
499
- create(data: Record<string, unknown>, options?: {
500
- session?: ClientSession;
501
- }): Promise<TDoc>;
567
+ parse(query: Record<string, unknown> | null | undefined): ParsedQuery;
502
568
  /**
503
- * Create multiple documents
569
+ * Generate OpenAPI-compatible JSON Schema for query parameters.
570
+ * Arc's defineResource() auto-detects this method and uses it
571
+ * to document list endpoint query parameters in OpenAPI/Swagger.
572
+ *
573
+ * The schema respects parser configuration:
574
+ * - `allowedOperators`: only documents allowed operators
575
+ * - `allowedFilterFields`: generates explicit field[op] entries
576
+ * - `enableLookups` / `enableAggregations`: includes/excludes lookup/aggregate params
577
+ * - `maxLimit` / `maxSearchLength`: reflected in schema constraints
504
578
  */
505
- createMany(dataArray: Record<string, unknown>[], options?: {
506
- session?: ClientSession;
507
- ordered?: boolean;
508
- }): Promise<TDoc[]>;
579
+ getQuerySchema(): {
580
+ type: 'object';
581
+ properties: Record<string, unknown>;
582
+ required?: string[];
583
+ };
509
584
  /**
510
- * Get document by ID
585
+ * Get the query schema with OpenAPI extensions (x-internal metadata).
586
+ * Use this when generating OpenAPI/Swagger docs — it includes a documentary
587
+ * `_filterOperators` property describing available filter operators.
588
+ * For validation-only schemas, use `getQuerySchema()` instead.
511
589
  */
512
- getById(id: string | ObjectId, options?: {
513
- select?: SelectSpec;
514
- populate?: PopulateSpec;
515
- populateOptions?: PopulateOptions[];
516
- lean?: boolean;
517
- session?: ClientSession;
518
- throwOnNotFound?: boolean;
519
- skipCache?: boolean;
520
- cacheTtl?: number;
521
- readPreference?: ReadPreferenceType;
522
- }): Promise<TDoc | null>;
590
+ getOpenAPIQuerySchema(): {
591
+ type: 'object';
592
+ properties: Record<string, unknown>;
593
+ };
523
594
  /**
524
- * Get single document by query
595
+ * Get the JSON Schema type for a filter operator
525
596
  */
526
- getByQuery(query: Record<string, unknown>, options?: {
527
- select?: SelectSpec;
528
- populate?: PopulateSpec;
529
- populateOptions?: PopulateOptions[];
530
- lean?: boolean;
531
- session?: ClientSession;
532
- throwOnNotFound?: boolean;
533
- skipCache?: boolean;
534
- cacheTtl?: number;
535
- readPreference?: ReadPreferenceType;
536
- }): Promise<TDoc | null>;
597
+ private _getOperatorSchemaType;
537
598
  /**
538
- * Unified pagination - auto-detects offset vs keyset based on params
599
+ * Get a human-readable description for a filter operator
600
+ */
601
+ private _getOperatorDescription;
602
+ /**
603
+ * Build a summary description of all available filter operators
604
+ */
605
+ private _buildOperatorDescription;
606
+ /**
607
+ * Parse lookup configurations from URL parameters
539
608
  *
540
- * Auto-detection logic:
541
- * - If params has 'cursor' or 'after' → uses keyset pagination (stream)
542
- * - If params has 'pagination' or 'page' uses offset pagination (paginate)
543
- * - Else → defaults to offset pagination with page=1
609
+ * Supported formats:
610
+ * 1. Simple: ?lookup[department]=slug
611
+ * Join with 'departments' collection on slug field
544
612
  *
545
- * @example
546
- * // Offset pagination (page-based)
547
- * await repo.getAll({ page: 1, limit: 50, filters: { status: 'active' } });
548
- * await repo.getAll({ pagination: { page: 2, limit: 20 } });
613
+ * 2. Detailed: ?lookup[department][localField]=deptSlug&lookup[department][foreignField]=slug
614
+ * Full control over join configuration
549
615
  *
550
- * // Keyset pagination (cursor-based)
551
- * await repo.getAll({ cursor: 'eyJ2Ij...', limit: 50 });
552
- * await repo.getAll({ after: 'eyJ2Ij...', sort: { createdAt: -1 } });
616
+ * 3. Multiple: ?lookup[department]=slug&lookup[category]=categorySlug
617
+ * Multiple lookups
553
618
  *
554
- * // Simple query (defaults to page 1)
555
- * await repo.getAll({ filters: { status: 'active' } });
556
- *
557
- * // Skip cache for fresh data
558
- * await repo.getAll({ filters: { status: 'active' } }, { skipCache: true });
559
- */
560
- getAll(params?: {
561
- filters?: Record<string, unknown>;
562
- sort?: SortSpec | string;
563
- cursor?: string;
564
- after?: string;
565
- page?: number;
566
- pagination?: {
567
- page?: number;
568
- limit?: number;
569
- };
570
- limit?: number;
571
- search?: string;
572
- mode?: "offset" | "keyset";
573
- hint?: string | Record<string, 1 | -1>;
574
- maxTimeMS?: number;
575
- countStrategy?: "exact" | "estimated" | "none";
576
- readPreference?: ReadPreferenceType; /** Advanced populate options (from QueryParser or Arc's BaseController) */
577
- populateOptions?: PopulateOptions[];
578
- }, options?: {
579
- select?: SelectSpec;
580
- populate?: PopulateSpec;
581
- populateOptions?: PopulateOptions[];
582
- lean?: boolean;
583
- session?: ClientSession;
584
- skipCache?: boolean;
585
- cacheTtl?: number;
586
- readPreference?: ReadPreferenceType;
587
- }): Promise<OffsetPaginationResult<TDoc> | KeysetPaginationResult<TDoc>>;
588
- /**
589
- * Get or create document
590
- * Routes through hook system for policy enforcement (multi-tenant, soft-delete)
591
- */
592
- getOrCreate(query: Record<string, unknown>, createData: Record<string, unknown>, options?: {
593
- session?: ClientSession;
594
- }): Promise<TDoc | null>;
595
- /**
596
- * Count documents
597
- * Routes through hook system for policy enforcement (multi-tenant, soft-delete)
598
- */
599
- count(query?: Record<string, unknown>, options?: {
600
- session?: ClientSession;
601
- readPreference?: ReadPreferenceType;
602
- }): Promise<number>;
603
- /**
604
- * Check if document exists
605
- * Routes through hook system for policy enforcement (multi-tenant, soft-delete)
606
- */
607
- exists(query: Record<string, unknown>, options?: {
608
- session?: ClientSession;
609
- readPreference?: ReadPreferenceType;
610
- }): Promise<{
611
- _id: unknown;
612
- } | null>;
613
- /**
614
- * Update document by ID
615
- */
616
- update(id: string | ObjectId, data: Record<string, unknown>, options?: UpdateOptions): Promise<TDoc>;
617
- /**
618
- * Delete document by ID
619
- */
620
- delete(id: string | ObjectId, options?: {
621
- session?: ClientSession;
622
- }): Promise<DeleteResult>;
623
- /**
624
- * Execute aggregation pipeline
625
- * Routes through hook system for policy enforcement (multi-tenant, soft-delete)
626
- *
627
- * @param pipeline - Aggregation pipeline stages
628
- * @param options - Aggregation options including governance controls
629
- */
630
- aggregate<TResult = unknown>(pipeline: PipelineStage[], options?: {
631
- session?: ClientSession;
632
- allowDiskUse?: boolean;
633
- comment?: string;
634
- readPreference?: ReadPreferenceType;
635
- maxTimeMS?: number;
636
- readConcern?: {
637
- level: string;
638
- };
639
- collation?: Record<string, unknown>;
640
- maxPipelineStages?: number;
641
- }): Promise<TResult[]>;
642
- /**
643
- * Aggregate pipeline with pagination
644
- * Best for: Complex queries, grouping, joins
645
- *
646
- * Policy hooks (multi-tenant, soft-delete) inject context.filters which are
647
- * prepended as a $match stage to the pipeline, ensuring tenant isolation.
619
+ * @example
620
+ * ```typescript
621
+ * // URL: ?lookup[department][localField]=deptSlug&lookup[department][foreignField]=slug&lookup[department][single]=true
622
+ * const lookups = parser._parseLookups({
623
+ * department: { localField: 'deptSlug', foreignField: 'slug', single: 'true' }
624
+ * });
625
+ * // Returns: [{ from: 'departments', localField: 'deptSlug', foreignField: 'slug', single: true }]
626
+ * ```
648
627
  */
649
- aggregatePaginate(options?: AggregatePaginationOptions): Promise<AggregatePaginationResult<TDoc>>;
628
+ private _parseLookups;
650
629
  /**
651
- * Get distinct values
652
- * Routes through hook system for policy enforcement (multi-tenant, soft-delete)
630
+ * Parse a single lookup configuration
653
631
  */
654
- distinct<T = unknown>(field: string, query?: Record<string, unknown>, options?: {
655
- session?: ClientSession;
656
- readPreference?: ReadPreferenceType;
657
- }): Promise<T[]>;
632
+ private _parseSingleLookup;
658
633
  /**
659
- * Query with custom field lookups ($lookup)
660
- * Best for: Joins on slugs, SKUs, codes, or other indexed custom fields
634
+ * Parse aggregation pipeline from URL (advanced feature)
661
635
  *
662
636
  * @example
663
637
  * ```typescript
664
- * // Join employees with departments using slug instead of ObjectId
665
- * const employees = await employeeRepo.lookupPopulate({
666
- * filters: { status: 'active' },
667
- * lookups: [
668
- * {
669
- * from: 'departments',
670
- * localField: 'departmentSlug',
671
- * foreignField: 'slug',
672
- * as: 'department',
673
- * single: true
674
- * }
675
- * ],
676
- * sort: '-createdAt',
677
- * page: 1,
678
- * limit: 50
638
+ * // URL: ?aggregate[group][_id]=$status&aggregate[group][count]=$sum:1
639
+ * const pipeline = parser._parseAggregation({
640
+ * group: { _id: '$status', count: '$sum:1' }
679
641
  * });
680
642
  * ```
681
643
  */
682
- lookupPopulate(options: {
683
- filters?: Record<string, unknown>;
684
- lookups: LookupOptions[];
685
- sort?: SortSpec | string;
686
- page?: number;
687
- limit?: number;
688
- select?: SelectSpec;
689
- session?: ClientSession;
690
- readPreference?: ReadPreferenceType;
691
- }): Promise<{
692
- data: TDoc[];
693
- total?: number;
694
- page?: number;
695
- limit?: number;
696
- }>;
644
+ private _parseAggregation;
697
645
  /**
698
- * Create an aggregation builder for this model
699
- * Useful for building complex custom aggregations
646
+ * Parse select/project fields
700
647
  *
701
648
  * @example
702
649
  * ```typescript
703
- * const pipeline = repo.buildAggregation()
704
- * .match({ status: 'active' })
705
- * .lookup('departments', 'deptSlug', 'slug', 'department', true)
706
- * .group({ _id: '$department', count: { $sum: 1 } })
707
- * .sort({ count: -1 })
708
- * .build();
709
- *
710
- * const results = await repo.Model.aggregate(pipeline);
650
+ * // URL: ?select=name,email,-password
651
+ * // Returns: { name: 1, email: 1, password: 0 }
711
652
  * ```
712
653
  */
713
- buildAggregation(): AggregationBuilder;
654
+ private _parseSelect;
714
655
  /**
715
- * Create a lookup builder
716
- * Useful for building $lookup stages independently
656
+ * Parse populate parameter - handles both simple string and advanced object format
717
657
  *
718
658
  * @example
719
659
  * ```typescript
720
- * const lookupStages = repo.buildLookup('departments')
721
- * .localField('deptSlug')
722
- * .foreignField('slug')
723
- * .as('department')
724
- * .single()
725
- * .build();
660
+ * // Simple: ?populate=author,category
661
+ * // Returns: { simplePopulate: 'author,category', populateOptions: undefined }
726
662
  *
727
- * const pipeline = [
728
- * { $match: { status: 'active' } },
729
- * ...lookupStages
730
- * ];
663
+ * // Advanced: ?populate[author][select]=name,email
664
+ * // Returns: { simplePopulate: undefined, populateOptions: [{ path: 'author', select: 'name email' }] }
731
665
  * ```
732
666
  */
733
- buildLookup(from?: string): LookupBuilder;
667
+ private _parsePopulate;
734
668
  /**
735
- * Execute callback within a transaction with automatic retry on transient failures.
736
- *
737
- * Uses the MongoDB driver's `session.withTransaction()` which automatically retries
738
- * on `TransientTransactionError` and `UnknownTransactionCommitResult`.
739
- *
740
- * The callback always receives a `ClientSession`. When `allowFallback` is true
741
- * and the MongoDB deployment doesn't support transactions (e.g., standalone),
742
- * the callback runs without a transaction on the same session.
743
- *
744
- * @param callback - Receives a `ClientSession` to pass to repository operations
745
- * @param options.allowFallback - Run without transaction on standalone MongoDB (default: false)
746
- * @param options.onFallback - Called when falling back to non-transactional execution
747
- * @param options.transactionOptions - MongoDB driver transaction options (readConcern, writeConcern, etc.)
748
- *
749
- * @example
750
- * ```typescript
751
- * const result = await repo.withTransaction(async (session) => {
752
- * const order = await repo.create({ total: 100 }, { session });
753
- * await paymentRepo.create({ orderId: order._id }, { session });
754
- * return order;
755
- * });
756
- *
757
- * // With fallback for standalone/dev environments
758
- * await repo.withTransaction(callback, {
759
- * allowFallback: true,
760
- * onFallback: (err) => logger.warn('Running without transaction', err),
761
- * });
762
- * ```
669
+ * Parse a single populate configuration
763
670
  */
764
- withTransaction<T>(callback: (session: ClientSession) => Promise<T>, options?: WithTransactionOptions): Promise<T>;
765
- private _isTransactionUnsupported;
671
+ private _parseSinglePopulate;
766
672
  /**
767
- * Execute custom query with event emission
673
+ * Convert populate match values (handles boolean strings, etc.)
768
674
  */
769
- _executeQuery<T>(buildQuery: (Model: Model<TDoc>) => Promise<T>): Promise<T>;
675
+ private _convertPopulateMatch;
770
676
  /**
771
- * Build operation context and run before hooks (sorted by priority).
772
- *
773
- * Hook execution order is deterministic:
774
- * 1. POLICY (100) — tenant isolation, soft-delete filtering, validation
775
- * 2. CACHE (200) — cache lookup (after policy filters are injected)
776
- * 3. OBSERVABILITY (300) — audit logging, metrics
777
- * 4. DEFAULT (500) — user-registered hooks
677
+ * Parse filter parameters
778
678
  */
779
- _buildContext(operation: string, options: Record<string, unknown>): Promise<RepositoryContext>;
679
+ private _parseFilters;
780
680
  /**
781
- * Parse sort string or object
681
+ * Handle operator syntax: field[operator]=value
782
682
  */
783
- _parseSort(sort: SortSpec | string | undefined): SortSpec;
683
+ private _handleOperatorSyntax;
784
684
  /**
785
- * Parse populate specification
685
+ * Handle bracket syntax with object value
786
686
  */
787
- _parsePopulate(populate: PopulateSpec | undefined): string[] | PopulateOptions[];
687
+ private _handleBracketSyntax;
688
+ private _parseSort;
689
+ private _toMongoOperator;
690
+ private _createSafeRegex;
691
+ private _escapeRegex;
788
692
  /**
789
- * Handle errors with proper HTTP status codes
693
+ * Sanitize $match configuration to prevent dangerous operators
694
+ * Recursively filters out operators like $where, $function, $accumulator
790
695
  */
791
- _handleError(error: Error): HttpError;
792
- }
793
- //#endregion
794
- //#region src/query/QueryParser.d.ts
795
- type SortSpec$1 = Record<string, 1 | -1>;
796
- type FilterQuery = Record<string, unknown>;
797
- /**
798
- * Mongoose-compatible populate option
799
- * Supports advanced populate with select, match, limit, sort, and nested populate
800
- *
801
- * @example
802
- * ```typescript
803
- * // URL: ?populate[author][select]=name,email&populate[author][match][active]=true
804
- * // Generates: { path: 'author', select: 'name email', match: { active: true } }
805
- * ```
806
- */
807
- interface PopulateOption {
808
- /** Field path to populate */
809
- path: string;
810
- /** Fields to select (space-separated) */
811
- select?: string;
812
- /** Filter conditions for populated documents */
813
- match?: Record<string, unknown>;
814
- /** Query options (limit, sort, skip) */
815
- options?: {
816
- limit?: number;
817
- sort?: SortSpec$1;
818
- skip?: number;
819
- };
820
- /** Nested populate configuration */
821
- populate?: PopulateOption;
822
- }
823
- /** Parsed query result with optional lookup configuration */
824
- interface ParsedQuery {
825
- /** MongoDB filter query */
826
- filters: FilterQuery;
827
- /** Sort specification */
828
- sort?: SortSpec$1;
829
- /** Fields to populate (simple comma-separated string) */
830
- populate?: string;
696
+ private _sanitizeMatchConfig;
831
697
  /**
832
- * Advanced populate options (Mongoose-compatible)
833
- * When this is set, `populate` will be undefined
834
- * @example [{ path: 'author', select: 'name email' }]
698
+ * Sanitize pipeline stages for use in $lookup.
699
+ * Blocks dangerous stages ($out, $merge, etc.) and recursively sanitizes
700
+ * operator expressions within $match, $addFields, and $set stages.
835
701
  */
836
- populateOptions?: PopulateOption[];
837
- /** Page number for offset pagination */
838
- page?: number;
839
- /** Cursor for keyset pagination */
840
- after?: string;
841
- /** Limit */
842
- limit: number;
843
- /** Full-text search query */
844
- search?: string;
845
- /** Lookup configurations for custom field joins */
846
- lookups?: LookupOptions[];
847
- /** Aggregation pipeline stages (advanced) */
848
- aggregation?: PipelineStage[];
849
- /** Select/project fields */
850
- select?: Record<string, 0 | 1>;
702
+ private _sanitizePipeline;
703
+ /**
704
+ * Recursively sanitize expression objects, blocking dangerous operators
705
+ * like $where, $function, $accumulator inside $addFields/$set stages.
706
+ */
707
+ private _sanitizeExpressions;
708
+ private _sanitizeSearch;
709
+ /**
710
+ * Build regex-based multi-field search filters
711
+ * Creates an $or query with case-insensitive regex across all searchFields
712
+ *
713
+ * @example
714
+ * // searchFields: ['name', 'description', 'sku']
715
+ * // search: 'azure'
716
+ * // Returns: [
717
+ * // { name: { $regex: /azure/i } },
718
+ * // { description: { $regex: /azure/i } },
719
+ * // { sku: { $regex: /azure/i } }
720
+ * // ]
721
+ */
722
+ private _buildRegexSearch;
723
+ private _convertValue;
724
+ private _parseOr;
725
+ private _enhanceWithBetween;
726
+ private _pluralize;
727
+ private _capitalize;
851
728
  }
852
- /** Search mode for query parser */
853
- type SearchMode = "text" | "regex";
854
- interface QueryParserOptions {
855
- /** Maximum allowed regex pattern length (default: 500) */
856
- maxRegexLength?: number;
857
- /** Maximum allowed text search query length (default: 200) */
858
- maxSearchLength?: number;
859
- /** Maximum allowed filter depth (default: 10) */
860
- maxFilterDepth?: number;
861
- /** Maximum allowed limit value (default: 1000) */
862
- maxLimit?: number;
863
- /** Additional operators to block */
864
- additionalDangerousOperators?: string[];
865
- /** Enable lookup parsing (default: true) */
866
- enableLookups?: boolean;
867
- /** Enable aggregation parsing (default: false - requires explicit opt-in) */
868
- enableAggregations?: boolean;
729
+ //#endregion
730
+ //#region src/Repository.d.ts
731
+ type HookListener = (data: any) => void | Promise<void>;
732
+ /** Hook with priority for phase ordering */
733
+ interface PrioritizedHook {
734
+ listener: HookListener;
735
+ priority: number;
736
+ }
737
+ /**
738
+ * Plugin phase priorities (lower = runs first)
739
+ * Policy hooks (multi-tenant, soft-delete, validation) MUST run before cache
740
+ * to ensure filters are injected before cache keys are computed.
741
+ */
742
+ declare const HOOK_PRIORITY: {
743
+ /** Policy enforcement: tenant isolation, soft-delete filtering, validation */readonly POLICY: 100; /** Caching: lookup/store after policy filters are applied */
744
+ readonly CACHE: 200; /** Observability: audit logging, metrics, telemetry */
745
+ readonly OBSERVABILITY: 300; /** Default priority for user-registered hooks */
746
+ readonly DEFAULT: 500;
747
+ };
748
+ /**
749
+ * Production-grade repository for MongoDB
750
+ * Event-driven, plugin-based, with smart pagination
751
+ */
752
+ declare class Repository<TDoc = any> {
753
+ readonly Model: Model<TDoc>;
754
+ readonly model: string;
755
+ readonly _hooks: Map<string, PrioritizedHook[]>;
756
+ readonly _pagination: PaginationEngine<TDoc>;
757
+ private readonly _hookMode;
758
+ [key: string]: unknown;
759
+ private _hasTextIndex;
760
+ constructor(Model: Model<TDoc, any, any, any>, plugins?: PluginType[], paginationConfig?: PaginationConfig, options?: RepositoryOptions);
869
761
  /**
870
- * Search mode (default: 'text')
871
- * - 'text': Uses MongoDB $text search (requires text index)
872
- * - 'regex': Uses $or with $regex across searchFields (no index required)
762
+ * Register a plugin
873
763
  */
874
- searchMode?: SearchMode;
764
+ use(plugin: PluginType): this;
875
765
  /**
876
- * Fields to search when searchMode is 'regex'
877
- * Required when searchMode is 'regex'
878
- * @example ['name', 'description', 'sku', 'tags']
766
+ * Register event listener with optional priority for phase ordering.
767
+ *
768
+ * @param event - Event name (e.g. 'before:getAll')
769
+ * @param listener - Hook function
770
+ * @param options - Optional { priority } — use HOOK_PRIORITY constants.
771
+ * Lower priority numbers run first.
772
+ * Default: HOOK_PRIORITY.DEFAULT (500)
879
773
  */
880
- searchFields?: string[];
774
+ on(event: string, listener: HookListener, options?: {
775
+ priority?: number;
776
+ }): this;
881
777
  /**
882
- * Whitelist of collection names allowed in lookups.
883
- * When set, only these collections can be used in $lookup stages.
884
- * When undefined, all collection names are allowed.
885
- * @example ['departments', 'categories', 'users']
778
+ * Remove a specific event listener
886
779
  */
887
- allowedLookupCollections?: string[];
888
- /** Allowed fields for filtering. If set, ignores unknown fields. */
889
- allowedFilterFields?: string[];
890
- /** Allowed fields for sorting. If set, ignores unknown fields. */
891
- allowedSortFields?: string[];
780
+ off(event: string, listener: HookListener): this;
892
781
  /**
893
- * Whitelist of allowed filter operators.
894
- * When set, only these operators can be used in filters.
895
- * When undefined, all built-in operators are allowed.
896
- * Values are human-readable keys: 'eq', 'ne', 'gt', 'gte', 'lt', 'lte', 'in', 'nin',
897
- * 'like', 'contains', 'regex', 'exists', 'size', 'type'
898
- * @example ['eq', 'ne', 'gt', 'gte', 'lt', 'lte', 'in']
782
+ * Remove all listeners for an event, or all listeners entirely
899
783
  */
900
- allowedOperators?: string[];
901
- }
902
- /**
903
- * Modern Query Parser
904
- * Converts URL parameters to MongoDB queries with $lookup support
905
- */
906
- declare class QueryParser {
907
- private readonly options;
908
- private readonly operators;
909
- private readonly dangerousOperators;
784
+ removeAllListeners(event?: string): this;
910
785
  /**
911
- * Regex patterns that can cause catastrophic backtracking (ReDoS attacks)
912
- * Detects:
913
- * - Quantifiers: {n,m}
914
- * - Possessive quantifiers: *+, ++, ?+
915
- * - Nested quantifiers: (a+)+, (a*)*
916
- * - Backreferences: \1, \2, etc.
917
- * - Complex character classes: [...]...[...]
786
+ * Emit event (sync - for backwards compatibility)
918
787
  */
919
- private readonly dangerousRegexPatterns;
920
- constructor(options?: QueryParserOptions);
788
+ emit(event: string, data: unknown): void;
921
789
  /**
922
- * Parse URL query parameters into MongoDB query format
790
+ * Emit event and await all async handlers (sorted by priority)
791
+ */
792
+ emitAsync(event: string, data: unknown): Promise<void>;
793
+ private _emitHook;
794
+ private _emitErrorHook;
795
+ /**
796
+ * Create single document
797
+ */
798
+ create(data: Record<string, unknown>, options?: {
799
+ session?: ClientSession;
800
+ }): Promise<TDoc>;
801
+ /**
802
+ * Create multiple documents
803
+ */
804
+ createMany(dataArray: Record<string, unknown>[], options?: {
805
+ session?: ClientSession;
806
+ ordered?: boolean;
807
+ }): Promise<TDoc[]>;
808
+ /**
809
+ * Get document by ID
810
+ */
811
+ getById(id: string | ObjectId, options?: {
812
+ select?: SelectSpec;
813
+ populate?: PopulateSpec;
814
+ populateOptions?: PopulateOptions[];
815
+ lean?: boolean;
816
+ session?: ClientSession;
817
+ throwOnNotFound?: boolean;
818
+ skipCache?: boolean;
819
+ cacheTtl?: number;
820
+ readPreference?: ReadPreferenceType;
821
+ }): Promise<TDoc | null>;
822
+ /**
823
+ * Get single document by query
824
+ */
825
+ getByQuery(query: Record<string, unknown>, options?: {
826
+ select?: SelectSpec;
827
+ populate?: PopulateSpec;
828
+ populateOptions?: PopulateOptions[];
829
+ lean?: boolean;
830
+ session?: ClientSession;
831
+ throwOnNotFound?: boolean;
832
+ skipCache?: boolean;
833
+ cacheTtl?: number;
834
+ readPreference?: ReadPreferenceType;
835
+ }): Promise<TDoc | null>;
836
+ /**
837
+ * Unified pagination - auto-detects offset vs keyset based on params
838
+ *
839
+ * Auto-detection logic:
840
+ * - If params has 'cursor' or 'after' → uses keyset pagination (stream)
841
+ * - If params has 'pagination' or 'page' → uses offset pagination (paginate)
842
+ * - Else → defaults to offset pagination with page=1
923
843
  *
924
844
  * @example
925
- * ```typescript
926
- * // URL: ?status=active&lookup[department][foreignField]=slug&sort=-createdAt&page=1
927
- * const query = parser.parse(req.query);
928
- * // Returns: { filters: {...}, lookups: [...], sort: {...}, page: 1 }
929
- * ```
845
+ * // Offset pagination (page-based)
846
+ * await repo.getAll({ page: 1, limit: 50, filters: { status: 'active' } });
847
+ * await repo.getAll({ pagination: { page: 2, limit: 20 } });
848
+ *
849
+ * // Keyset pagination (cursor-based)
850
+ * await repo.getAll({ cursor: 'eyJ2Ij...', limit: 50 });
851
+ * await repo.getAll({ after: 'eyJ2Ij...', sort: { createdAt: -1 } });
852
+ *
853
+ * // Simple query (defaults to page 1)
854
+ * await repo.getAll({ filters: { status: 'active' } });
855
+ *
856
+ * // Skip cache for fresh data
857
+ * await repo.getAll({ filters: { status: 'active' } }, { skipCache: true });
930
858
  */
931
- parse(query: Record<string, unknown> | null | undefined): ParsedQuery;
859
+ getAll(params?: {
860
+ filters?: Record<string, unknown>;
861
+ sort?: SortSpec | string;
862
+ cursor?: string;
863
+ after?: string;
864
+ page?: number;
865
+ pagination?: {
866
+ page?: number;
867
+ limit?: number;
868
+ };
869
+ limit?: number;
870
+ search?: string;
871
+ mode?: 'offset' | 'keyset';
872
+ hint?: string | Record<string, 1 | -1>;
873
+ maxTimeMS?: number;
874
+ countStrategy?: 'exact' | 'estimated' | 'none';
875
+ readPreference?: ReadPreferenceType; /** Advanced populate options (from QueryParser or Arc's BaseController) */
876
+ populateOptions?: PopulateOptions[]; /** Collation for locale-aware string comparison */
877
+ collation?: CollationOptions; /** Lookup configurations for $lookup joins (from QueryParser or manual) */
878
+ lookups?: LookupOptions[];
879
+ }, options?: {
880
+ select?: SelectSpec;
881
+ populate?: PopulateSpec;
882
+ populateOptions?: PopulateOptions[];
883
+ lean?: boolean;
884
+ session?: ClientSession;
885
+ skipCache?: boolean;
886
+ cacheTtl?: number;
887
+ readPreference?: ReadPreferenceType;
888
+ }): Promise<OffsetPaginationResult<TDoc> | KeysetPaginationResult<TDoc>>;
932
889
  /**
933
- * Generate OpenAPI-compatible JSON Schema for query parameters.
934
- * Arc's defineResource() auto-detects this method and uses it
935
- * to document list endpoint query parameters in OpenAPI/Swagger.
936
- *
937
- * The schema respects parser configuration:
938
- * - `allowedOperators`: only documents allowed operators
939
- * - `allowedFilterFields`: generates explicit field[op] entries
940
- * - `enableLookups` / `enableAggregations`: includes/excludes lookup/aggregate params
941
- * - `maxLimit` / `maxSearchLength`: reflected in schema constraints
890
+ * Get or create document
891
+ * Routes through hook system for policy enforcement (multi-tenant, soft-delete)
942
892
  */
943
- getQuerySchema(): {
944
- type: "object";
945
- properties: Record<string, unknown>;
946
- required?: string[];
947
- };
893
+ getOrCreate(query: Record<string, unknown>, createData: Record<string, unknown>, options?: {
894
+ session?: ClientSession;
895
+ }): Promise<TDoc | null>;
948
896
  /**
949
- * Get the query schema with OpenAPI extensions (x-internal metadata).
950
- * Use this when generating OpenAPI/Swagger docs it includes a documentary
951
- * `_filterOperators` property describing available filter operators.
952
- * For validation-only schemas, use `getQuerySchema()` instead.
897
+ * Count documents
898
+ * Routes through hook system for policy enforcement (multi-tenant, soft-delete)
953
899
  */
954
- getOpenAPIQuerySchema(): {
955
- type: "object";
956
- properties: Record<string, unknown>;
957
- };
900
+ count(query?: Record<string, unknown>, options?: {
901
+ session?: ClientSession;
902
+ readPreference?: ReadPreferenceType;
903
+ }): Promise<number>;
958
904
  /**
959
- * Get the JSON Schema type for a filter operator
905
+ * Check if document exists
906
+ * Routes through hook system for policy enforcement (multi-tenant, soft-delete)
960
907
  */
961
- private _getOperatorSchemaType;
908
+ exists(query: Record<string, unknown>, options?: {
909
+ session?: ClientSession;
910
+ readPreference?: ReadPreferenceType;
911
+ }): Promise<{
912
+ _id: unknown;
913
+ } | null>;
962
914
  /**
963
- * Get a human-readable description for a filter operator
915
+ * Update document by ID
964
916
  */
965
- private _getOperatorDescription;
917
+ update(id: string | ObjectId, data: Record<string, unknown>, options?: UpdateOptions): Promise<TDoc>;
966
918
  /**
967
- * Build a summary description of all available filter operators
919
+ * Delete document by ID
968
920
  */
969
- private _buildOperatorDescription;
921
+ delete(id: string | ObjectId, options?: {
922
+ session?: ClientSession;
923
+ }): Promise<DeleteResult>;
970
924
  /**
971
- * Parse lookup configurations from URL parameters
972
- *
973
- * Supported formats:
974
- * 1. Simple: ?lookup[department]=slug
975
- * → Join with 'departments' collection on slug field
976
- *
977
- * 2. Detailed: ?lookup[department][localField]=deptSlug&lookup[department][foreignField]=slug
978
- * → Full control over join configuration
925
+ * Execute aggregation pipeline
926
+ * Routes through hook system for policy enforcement (multi-tenant, soft-delete)
979
927
  *
980
- * 3. Multiple: ?lookup[department]=slug&lookup[category]=categorySlug
981
- * Multiple lookups
928
+ * @param pipeline - Aggregation pipeline stages
929
+ * @param options - Aggregation options including governance controls
930
+ */
931
+ aggregate<TResult = unknown>(pipeline: PipelineStage[], options?: {
932
+ session?: ClientSession;
933
+ allowDiskUse?: boolean;
934
+ comment?: string;
935
+ readPreference?: ReadPreferenceType;
936
+ maxTimeMS?: number;
937
+ readConcern?: {
938
+ level: string;
939
+ };
940
+ collation?: Record<string, unknown>;
941
+ maxPipelineStages?: number;
942
+ }): Promise<TResult[]>;
943
+ /**
944
+ * Aggregate pipeline with pagination
945
+ * Best for: Complex queries, grouping, joins
982
946
  *
983
- * @example
984
- * ```typescript
985
- * // URL: ?lookup[department][localField]=deptSlug&lookup[department][foreignField]=slug&lookup[department][single]=true
986
- * const lookups = parser._parseLookups({
987
- * department: { localField: 'deptSlug', foreignField: 'slug', single: 'true' }
988
- * });
989
- * // Returns: [{ from: 'departments', localField: 'deptSlug', foreignField: 'slug', single: true }]
990
- * ```
947
+ * Policy hooks (multi-tenant, soft-delete) inject context.filters which are
948
+ * prepended as a $match stage to the pipeline, ensuring tenant isolation.
991
949
  */
992
- private _parseLookups;
950
+ aggregatePaginate(options?: AggregatePaginationOptions): Promise<AggregatePaginationResult<TDoc>>;
993
951
  /**
994
- * Parse a single lookup configuration
952
+ * Get distinct values
953
+ * Routes through hook system for policy enforcement (multi-tenant, soft-delete)
995
954
  */
996
- private _parseSingleLookup;
955
+ distinct<T = unknown>(field: string, query?: Record<string, unknown>, options?: {
956
+ session?: ClientSession;
957
+ readPreference?: ReadPreferenceType;
958
+ }): Promise<T[]>;
997
959
  /**
998
- * Parse aggregation pipeline from URL (advanced feature)
960
+ * Query with custom field lookups ($lookup)
961
+ * Best for: Joins on slugs, SKUs, codes, or other indexed custom fields
999
962
  *
1000
963
  * @example
1001
964
  * ```typescript
1002
- * // URL: ?aggregate[group][_id]=$status&aggregate[group][count]=$sum:1
1003
- * const pipeline = parser._parseAggregation({
1004
- * group: { _id: '$status', count: '$sum:1' }
965
+ * // Join employees with departments using slug instead of ObjectId
966
+ * const employees = await employeeRepo.lookupPopulate({
967
+ * filters: { status: 'active' },
968
+ * lookups: [
969
+ * {
970
+ * from: 'departments',
971
+ * localField: 'departmentSlug',
972
+ * foreignField: 'slug',
973
+ * as: 'department',
974
+ * single: true
975
+ * }
976
+ * ],
977
+ * sort: '-createdAt',
978
+ * page: 1,
979
+ * limit: 50
1005
980
  * });
1006
981
  * ```
1007
982
  */
1008
- private _parseAggregation;
983
+ lookupPopulate(options: {
984
+ filters?: Record<string, unknown>;
985
+ lookups: LookupOptions[];
986
+ sort?: SortSpec | string;
987
+ page?: number;
988
+ after?: string;
989
+ limit?: number;
990
+ select?: SelectSpec;
991
+ session?: ClientSession;
992
+ readPreference?: ReadPreferenceType;
993
+ collation?: CollationOptions;
994
+ countStrategy?: 'exact' | 'estimated' | 'none';
995
+ }): Promise<{
996
+ data: TDoc[];
997
+ total?: number;
998
+ page?: number;
999
+ limit?: number;
1000
+ next?: string | null;
1001
+ hasMore?: boolean;
1002
+ }>;
1009
1003
  /**
1010
- * Parse select/project fields
1004
+ * Create an aggregation builder for this model
1005
+ * Useful for building complex custom aggregations
1011
1006
  *
1012
1007
  * @example
1013
1008
  * ```typescript
1014
- * // URL: ?select=name,email,-password
1015
- * // Returns: { name: 1, email: 1, password: 0 }
1009
+ * const pipeline = repo.buildAggregation()
1010
+ * .match({ status: 'active' })
1011
+ * .lookup('departments', 'deptSlug', 'slug', 'department', true)
1012
+ * .group({ _id: '$department', count: { $sum: 1 } })
1013
+ * .sort({ count: -1 })
1014
+ * .build();
1015
+ *
1016
+ * const results = await repo.Model.aggregate(pipeline);
1016
1017
  * ```
1017
1018
  */
1018
- private _parseSelect;
1019
+ buildAggregation(): AggregationBuilder;
1019
1020
  /**
1020
- * Parse populate parameter - handles both simple string and advanced object format
1021
+ * Create a lookup builder
1022
+ * Useful for building $lookup stages independently
1021
1023
  *
1022
1024
  * @example
1023
1025
  * ```typescript
1024
- * // Simple: ?populate=author,category
1025
- * // Returns: { simplePopulate: 'author,category', populateOptions: undefined }
1026
+ * const lookupStages = repo.buildLookup('departments')
1027
+ * .localField('deptSlug')
1028
+ * .foreignField('slug')
1029
+ * .as('department')
1030
+ * .single()
1031
+ * .build();
1026
1032
  *
1027
- * // Advanced: ?populate[author][select]=name,email
1028
- * // Returns: { simplePopulate: undefined, populateOptions: [{ path: 'author', select: 'name email' }] }
1033
+ * const pipeline = [
1034
+ * { $match: { status: 'active' } },
1035
+ * ...lookupStages
1036
+ * ];
1029
1037
  * ```
1030
1038
  */
1031
- private _parsePopulate;
1032
- /**
1033
- * Parse a single populate configuration
1034
- */
1035
- private _parseSinglePopulate;
1036
- /**
1037
- * Convert populate match values (handles boolean strings, etc.)
1038
- */
1039
- private _convertPopulateMatch;
1040
- /**
1041
- * Parse filter parameters
1042
- */
1043
- private _parseFilters;
1039
+ buildLookup(from?: string): LookupBuilder;
1044
1040
  /**
1045
- * Handle operator syntax: field[operator]=value
1041
+ * Execute callback within a transaction with automatic retry on transient failures.
1042
+ *
1043
+ * Uses the MongoDB driver's `session.withTransaction()` which automatically retries
1044
+ * on `TransientTransactionError` and `UnknownTransactionCommitResult`.
1045
+ *
1046
+ * The callback always receives a `ClientSession`. When `allowFallback` is true
1047
+ * and the MongoDB deployment doesn't support transactions (e.g., standalone),
1048
+ * the callback runs without a transaction on the same session.
1049
+ *
1050
+ * @param callback - Receives a `ClientSession` to pass to repository operations
1051
+ * @param options.allowFallback - Run without transaction on standalone MongoDB (default: false)
1052
+ * @param options.onFallback - Called when falling back to non-transactional execution
1053
+ * @param options.transactionOptions - MongoDB driver transaction options (readConcern, writeConcern, etc.)
1054
+ *
1055
+ * @example
1056
+ * ```typescript
1057
+ * const result = await repo.withTransaction(async (session) => {
1058
+ * const order = await repo.create({ total: 100 }, { session });
1059
+ * await paymentRepo.create({ orderId: order._id }, { session });
1060
+ * return order;
1061
+ * });
1062
+ *
1063
+ * // With fallback for standalone/dev environments
1064
+ * await repo.withTransaction(callback, {
1065
+ * allowFallback: true,
1066
+ * onFallback: (err) => logger.warn('Running without transaction', err),
1067
+ * });
1068
+ * ```
1046
1069
  */
1047
- private _handleOperatorSyntax;
1070
+ withTransaction<T>(callback: (session: ClientSession) => Promise<T>, options?: WithTransactionOptions): Promise<T>;
1071
+ private _isTransactionUnsupported;
1048
1072
  /**
1049
- * Handle bracket syntax with object value
1073
+ * Execute custom query with event emission
1050
1074
  */
1051
- private _handleBracketSyntax;
1052
- private _parseSort;
1053
- private _toMongoOperator;
1054
- private _createSafeRegex;
1055
- private _escapeRegex;
1075
+ _executeQuery<T>(buildQuery: (Model: Model<TDoc>) => Promise<T>): Promise<T>;
1056
1076
  /**
1057
- * Sanitize $match configuration to prevent dangerous operators
1058
- * Recursively filters out operators like $where, $function, $accumulator
1077
+ * Build operation context and run before hooks (sorted by priority).
1078
+ *
1079
+ * Hook execution order is deterministic:
1080
+ * 1. POLICY (100) — tenant isolation, soft-delete filtering, validation
1081
+ * 2. CACHE (200) — cache lookup (after policy filters are injected)
1082
+ * 3. OBSERVABILITY (300) — audit logging, metrics
1083
+ * 4. DEFAULT (500) — user-registered hooks
1059
1084
  */
1060
- private _sanitizeMatchConfig;
1085
+ _buildContext(operation: string, options: Record<string, unknown>): Promise<RepositoryContext>;
1061
1086
  /**
1062
- * Sanitize pipeline stages for use in $lookup.
1063
- * Blocks dangerous stages ($out, $merge, etc.) and recursively sanitizes
1064
- * operator expressions within $match, $addFields, and $set stages.
1087
+ * Parse sort string or object
1065
1088
  */
1066
- private _sanitizePipeline;
1089
+ _parseSort(sort: SortSpec | string | undefined): SortSpec;
1067
1090
  /**
1068
- * Recursively sanitize expression objects, blocking dangerous operators
1069
- * like $where, $function, $accumulator inside $addFields/$set stages.
1091
+ * Parse populate specification
1070
1092
  */
1071
- private _sanitizeExpressions;
1072
- private _sanitizeSearch;
1093
+ _parsePopulate(populate: PopulateSpec | undefined): string[] | PopulateOptions[];
1073
1094
  /**
1074
- * Build regex-based multi-field search filters
1075
- * Creates an $or query with case-insensitive regex across all searchFields
1076
- *
1077
- * @example
1078
- * // searchFields: ['name', 'description', 'sku']
1079
- * // search: 'azure'
1080
- * // Returns: [
1081
- * // { name: { $regex: /azure/i } },
1082
- * // { description: { $regex: /azure/i } },
1083
- * // { sku: { $regex: /azure/i } }
1084
- * // ]
1095
+ * Handle errors with proper HTTP status codes
1085
1096
  */
1086
- private _buildRegexSearch;
1087
- private _convertValue;
1088
- private _parseOr;
1089
- private _enhanceWithBetween;
1090
- private _pluralize;
1091
- private _capitalize;
1097
+ _handleError(error: Error): HttpError;
1092
1098
  }
1093
1099
  //#endregion
1094
1100
  //#region src/index.d.ts
@@ -1102,6 +1108,6 @@ declare class QueryParser {
1102
1108
  * @example
1103
1109
  * const userRepo = createRepository(UserModel, [timestampPlugin()]);
1104
1110
  */
1105
- declare function createRepository<TDoc>(Model: mongoose$1.Model<TDoc, any, any, any>, plugins?: PluginType[], paginationConfig?: PaginationConfig, options?: RepositoryOptions): Repository<TDoc>;
1111
+ declare function createRepository<TDoc>(Model: _$mongoose.Model<TDoc, any, any, any>, plugins?: PluginType[], paginationConfig?: PaginationConfig, options?: RepositoryOptions): Repository<TDoc>;
1106
1112
  //#endregion
1107
- export { type AggregateHelpersMethods, type AggregatePaginationOptions, type AggregatePaginationResult, AggregationBuilder, type AllPluginMethods, type AnyDocument, type AnyModel, type AuditEntry, type AuditOperation, type AuditQueryOptions, type AuditQueryResult, type AuditTrailMethods, type AuditTrailOptions, AuditTrailQuery, type BatchOperationsMethods, type BulkWriteResult, type CacheAdapter, type CacheMethods, type CacheOperationOptions, type CacheOptions, type CacheStats, type CascadeOptions, type CascadeRelation, type CreateInput, type CreateOptions, type CrudSchemas, type CustomIdOptions, type DateSequentialIdOptions, type DecodedCursor, type DeepPartial, type DeleteResult, type ElasticSearchOptions, type EventHandlers, type EventPayload, type EventPhase, type FieldPreset, type FieldRules, type FilterQuery, type GroupResult, HOOK_PRIORITY, type HookMode, type HttpError, type IController, type IControllerResponse, type IRequestContext, type IResponseFormatter, type IdGenerator, type InferDocument, type InferRawDoc, type JsonSchema, type KeysOfType, type KeysetPaginationOptions, type KeysetPaginationResult, type Logger, LookupBuilder, type LookupOptions, type MinMaxResult, type MongoOperationsMethods, type MultiTenantOptions, type NonNullableFields, type ObjectId, type ObservabilityOptions, type OffsetPaginationOptions, type OffsetPaginationResult, type OperationMetric, type OperationOptions, type PaginationConfig, PaginationEngine, type PaginationResult, type ParsedQuery, type PartialBy, type Plugin, type PluginFunction, type PluginType, type PopulateOption, type PopulateSpec, type PrefixedIdOptions, QueryParser, type QueryParserOptions, type ReadPreferenceType, Repository, Repository as default, type RepositoryContext, type RepositoryEvent, type RepositoryInstance, type RepositoryOperation, type RepositoryOptions, type RequiredBy, type SchemaBuilderOptions, type SearchMode, type SelectSpec, type SequentialIdOptions, type SoftDeleteFilterMode, type SoftDeleteMethods, type SoftDeleteOptions, type SoftDeleteRepository, type SortDirection, type SortSpec, type Strict, type SubdocumentMethods, type UpdateInput, type UpdateManyResult, type UpdateOptions, type UpdateWithValidationResult, type UserContext, type ValidationChainOptions, type ValidationResult, type ValidatorDefinition, type WithPlugins, type WithTransactionOptions, index_d_exports as actions, aggregateHelpersPlugin, auditLogPlugin, auditTrailPlugin, autoInject, batchOperationsPlugin, blockIf, buildCrudSchemasFromModel, buildCrudSchemasFromMongooseSchema, cachePlugin, cascadePlugin, configureLogger, createError, createFieldPreset, createMemoryCache, createRepository, customIdPlugin, dateSequentialId, elasticSearchPlugin, fieldFilterPlugin, filterResponseData, getFieldsForUser, getImmutableFields, getMongooseProjection, getNextSequence, getSystemManagedFields, immutableField, isFieldUpdateAllowed, methodRegistryPlugin, mongoOperationsPlugin, multiTenantPlugin, observabilityPlugin, prefixedId, requireField, sequentialId, softDeletePlugin, subdocumentPlugin, timestampPlugin, uniqueField, validateUpdateBody, validationChainPlugin };
1113
+ export { type AggregateHelpersMethods, type AggregatePaginationOptions, type AggregatePaginationResult, AggregationBuilder, type AllPluginMethods, type AnyDocument, type AnyModel, type AuditEntry, type AuditOperation, type AuditQueryOptions, type AuditQueryResult, type AuditTrailMethods, type AuditTrailOptions, AuditTrailQuery, type BatchOperationsMethods, type BulkWriteResult, type CacheAdapter, type CacheMethods, type CacheOperationOptions, type CacheOptions, type CacheStats, type CascadeOptions, type CascadeRelation, type CollationOptions, type CreateInput, type CreateOptions, type CrudSchemas, type CustomIdOptions, type DateSequentialIdOptions, type DecodedCursor, type DeepPartial, type DeleteResult, type ElasticSearchOptions, type EventHandlers, type EventPayload, type EventPhase, type FieldPreset, type FieldRules, type FilterQuery, type GroupResult, HOOK_PRIORITY, type HookMode, type HttpError, type IController, type IControllerResponse, type IRequestContext, type IResponseFormatter, type IdGenerator, type InferDocument, type InferRawDoc, type JsonSchema, type KeysOfType, type KeysetPaginationOptions, type KeysetPaginationResult, type Logger, LookupBuilder, type LookupOptions, type MinMaxResult, type MongoOperationsMethods, type MultiTenantOptions, type NonNullableFields, type ObjectId, type ObservabilityOptions, type OffsetPaginationOptions, type OffsetPaginationResult, type OperationMetric, type OperationOptions, type PaginationConfig, PaginationEngine, type PaginationResult, type ParsedQuery, type PartialBy, type Plugin, type PluginFunction, type PluginType, type PopulateOption, type PopulateSpec, type PrefixedIdOptions, QueryParser, type QueryParserOptions, type ReadPreferenceType, Repository, Repository as default, type RepositoryContext, type RepositoryEvent, type RepositoryInstance, type RepositoryOperation, type RepositoryOptions, type RequiredBy, type SchemaBuilderOptions, type SearchMode, type SelectSpec, type SequentialIdOptions, type SoftDeleteFilterMode, type SoftDeleteMethods, type SoftDeleteOptions, type SoftDeleteRepository, type SortDirection, type SortSpec, type Strict, type SubdocumentMethods, type UpdateInput, type UpdateManyResult, type UpdateOptions, type UpdateWithValidationResult, type UserContext, type ValidationChainOptions, type ValidationResult, type ValidatorDefinition, type WithPlugins, type WithTransactionOptions, index_d_exports as actions, aggregateHelpersPlugin, auditLogPlugin, auditTrailPlugin, autoInject, batchOperationsPlugin, blockIf, buildCrudSchemasFromModel, buildCrudSchemasFromMongooseSchema, cachePlugin, cascadePlugin, configureLogger, createError, createFieldPreset, createMemoryCache, createRepository, customIdPlugin, dateSequentialId, elasticSearchPlugin, fieldFilterPlugin, filterResponseData, getFieldsForUser, getImmutableFields, getMongooseProjection, getNextSequence, getSystemManagedFields, immutableField, isFieldUpdateAllowed, methodRegistryPlugin, mongoOperationsPlugin, multiTenantPlugin, observabilityPlugin, parseDuplicateKeyError, prefixedId, requireField, sequentialId, softDeletePlugin, subdocumentPlugin, timestampPlugin, uniqueField, validateUpdateBody, validationChainPlugin };