@classytic/mongokit 3.3.2 → 3.4.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/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 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-BlCwDszq.mjs";
2
+ import { t as index_d_exports } from "./index-Df3ernpC.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-DxqiHv-E.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-BqgVOlrR.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,668 @@ 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[]; /** Lookup configurations for $lookup joins (from QueryParser or manual) */
877
+ lookups?: LookupOptions[];
878
+ }, options?: {
879
+ select?: SelectSpec;
880
+ populate?: PopulateSpec;
881
+ populateOptions?: PopulateOptions[];
882
+ lean?: boolean;
883
+ session?: ClientSession;
884
+ skipCache?: boolean;
885
+ cacheTtl?: number;
886
+ readPreference?: ReadPreferenceType;
887
+ }): Promise<OffsetPaginationResult<TDoc> | KeysetPaginationResult<TDoc>>;
932
888
  /**
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
889
+ * Get or create document
890
+ * Routes through hook system for policy enforcement (multi-tenant, soft-delete)
942
891
  */
943
- getQuerySchema(): {
944
- type: "object";
945
- properties: Record<string, unknown>;
946
- required?: string[];
947
- };
892
+ getOrCreate(query: Record<string, unknown>, createData: Record<string, unknown>, options?: {
893
+ session?: ClientSession;
894
+ }): Promise<TDoc | null>;
948
895
  /**
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.
896
+ * Count documents
897
+ * Routes through hook system for policy enforcement (multi-tenant, soft-delete)
953
898
  */
954
- getOpenAPIQuerySchema(): {
955
- type: "object";
956
- properties: Record<string, unknown>;
957
- };
899
+ count(query?: Record<string, unknown>, options?: {
900
+ session?: ClientSession;
901
+ readPreference?: ReadPreferenceType;
902
+ }): Promise<number>;
958
903
  /**
959
- * Get the JSON Schema type for a filter operator
904
+ * Check if document exists
905
+ * Routes through hook system for policy enforcement (multi-tenant, soft-delete)
960
906
  */
961
- private _getOperatorSchemaType;
907
+ exists(query: Record<string, unknown>, options?: {
908
+ session?: ClientSession;
909
+ readPreference?: ReadPreferenceType;
910
+ }): Promise<{
911
+ _id: unknown;
912
+ } | null>;
962
913
  /**
963
- * Get a human-readable description for a filter operator
914
+ * Update document by ID
964
915
  */
965
- private _getOperatorDescription;
916
+ update(id: string | ObjectId, data: Record<string, unknown>, options?: UpdateOptions): Promise<TDoc>;
966
917
  /**
967
- * Build a summary description of all available filter operators
918
+ * Delete document by ID
968
919
  */
969
- private _buildOperatorDescription;
920
+ delete(id: string | ObjectId, options?: {
921
+ session?: ClientSession;
922
+ }): Promise<DeleteResult>;
970
923
  /**
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
924
+ * Execute aggregation pipeline
925
+ * Routes through hook system for policy enforcement (multi-tenant, soft-delete)
979
926
  *
980
- * 3. Multiple: ?lookup[department]=slug&lookup[category]=categorySlug
981
- * Multiple lookups
927
+ * @param pipeline - Aggregation pipeline stages
928
+ * @param options - Aggregation options including governance controls
929
+ */
930
+ aggregate<TResult = unknown>(pipeline: PipelineStage[], options?: {
931
+ session?: ClientSession;
932
+ allowDiskUse?: boolean;
933
+ comment?: string;
934
+ readPreference?: ReadPreferenceType;
935
+ maxTimeMS?: number;
936
+ readConcern?: {
937
+ level: string;
938
+ };
939
+ collation?: Record<string, unknown>;
940
+ maxPipelineStages?: number;
941
+ }): Promise<TResult[]>;
942
+ /**
943
+ * Aggregate pipeline with pagination
944
+ * Best for: Complex queries, grouping, joins
982
945
  *
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
- * ```
946
+ * Policy hooks (multi-tenant, soft-delete) inject context.filters which are
947
+ * prepended as a $match stage to the pipeline, ensuring tenant isolation.
991
948
  */
992
- private _parseLookups;
949
+ aggregatePaginate(options?: AggregatePaginationOptions): Promise<AggregatePaginationResult<TDoc>>;
993
950
  /**
994
- * Parse a single lookup configuration
951
+ * Get distinct values
952
+ * Routes through hook system for policy enforcement (multi-tenant, soft-delete)
995
953
  */
996
- private _parseSingleLookup;
954
+ distinct<T = unknown>(field: string, query?: Record<string, unknown>, options?: {
955
+ session?: ClientSession;
956
+ readPreference?: ReadPreferenceType;
957
+ }): Promise<T[]>;
997
958
  /**
998
- * Parse aggregation pipeline from URL (advanced feature)
959
+ * Query with custom field lookups ($lookup)
960
+ * Best for: Joins on slugs, SKUs, codes, or other indexed custom fields
999
961
  *
1000
962
  * @example
1001
963
  * ```typescript
1002
- * // URL: ?aggregate[group][_id]=$status&aggregate[group][count]=$sum:1
1003
- * const pipeline = parser._parseAggregation({
1004
- * group: { _id: '$status', count: '$sum:1' }
964
+ * // Join employees with departments using slug instead of ObjectId
965
+ * const employees = await employeeRepo.lookupPopulate({
966
+ * filters: { status: 'active' },
967
+ * lookups: [
968
+ * {
969
+ * from: 'departments',
970
+ * localField: 'departmentSlug',
971
+ * foreignField: 'slug',
972
+ * as: 'department',
973
+ * single: true
974
+ * }
975
+ * ],
976
+ * sort: '-createdAt',
977
+ * page: 1,
978
+ * limit: 50
1005
979
  * });
1006
980
  * ```
1007
981
  */
1008
- private _parseAggregation;
982
+ lookupPopulate(options: {
983
+ filters?: Record<string, unknown>;
984
+ lookups: LookupOptions[];
985
+ sort?: SortSpec | string;
986
+ page?: number;
987
+ limit?: number;
988
+ select?: SelectSpec;
989
+ session?: ClientSession;
990
+ readPreference?: ReadPreferenceType;
991
+ }): Promise<{
992
+ data: TDoc[];
993
+ total?: number;
994
+ page?: number;
995
+ limit?: number;
996
+ }>;
1009
997
  /**
1010
- * Parse select/project fields
998
+ * Create an aggregation builder for this model
999
+ * Useful for building complex custom aggregations
1011
1000
  *
1012
1001
  * @example
1013
1002
  * ```typescript
1014
- * // URL: ?select=name,email,-password
1015
- * // Returns: { name: 1, email: 1, password: 0 }
1003
+ * const pipeline = repo.buildAggregation()
1004
+ * .match({ status: 'active' })
1005
+ * .lookup('departments', 'deptSlug', 'slug', 'department', true)
1006
+ * .group({ _id: '$department', count: { $sum: 1 } })
1007
+ * .sort({ count: -1 })
1008
+ * .build();
1009
+ *
1010
+ * const results = await repo.Model.aggregate(pipeline);
1016
1011
  * ```
1017
1012
  */
1018
- private _parseSelect;
1013
+ buildAggregation(): AggregationBuilder;
1019
1014
  /**
1020
- * Parse populate parameter - handles both simple string and advanced object format
1015
+ * Create a lookup builder
1016
+ * Useful for building $lookup stages independently
1021
1017
  *
1022
1018
  * @example
1023
1019
  * ```typescript
1024
- * // Simple: ?populate=author,category
1025
- * // Returns: { simplePopulate: 'author,category', populateOptions: undefined }
1020
+ * const lookupStages = repo.buildLookup('departments')
1021
+ * .localField('deptSlug')
1022
+ * .foreignField('slug')
1023
+ * .as('department')
1024
+ * .single()
1025
+ * .build();
1026
1026
  *
1027
- * // Advanced: ?populate[author][select]=name,email
1028
- * // Returns: { simplePopulate: undefined, populateOptions: [{ path: 'author', select: 'name email' }] }
1027
+ * const pipeline = [
1028
+ * { $match: { status: 'active' } },
1029
+ * ...lookupStages
1030
+ * ];
1029
1031
  * ```
1030
1032
  */
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;
1033
+ buildLookup(from?: string): LookupBuilder;
1044
1034
  /**
1045
- * Handle operator syntax: field[operator]=value
1035
+ * Execute callback within a transaction with automatic retry on transient failures.
1036
+ *
1037
+ * Uses the MongoDB driver's `session.withTransaction()` which automatically retries
1038
+ * on `TransientTransactionError` and `UnknownTransactionCommitResult`.
1039
+ *
1040
+ * The callback always receives a `ClientSession`. When `allowFallback` is true
1041
+ * and the MongoDB deployment doesn't support transactions (e.g., standalone),
1042
+ * the callback runs without a transaction on the same session.
1043
+ *
1044
+ * @param callback - Receives a `ClientSession` to pass to repository operations
1045
+ * @param options.allowFallback - Run without transaction on standalone MongoDB (default: false)
1046
+ * @param options.onFallback - Called when falling back to non-transactional execution
1047
+ * @param options.transactionOptions - MongoDB driver transaction options (readConcern, writeConcern, etc.)
1048
+ *
1049
+ * @example
1050
+ * ```typescript
1051
+ * const result = await repo.withTransaction(async (session) => {
1052
+ * const order = await repo.create({ total: 100 }, { session });
1053
+ * await paymentRepo.create({ orderId: order._id }, { session });
1054
+ * return order;
1055
+ * });
1056
+ *
1057
+ * // With fallback for standalone/dev environments
1058
+ * await repo.withTransaction(callback, {
1059
+ * allowFallback: true,
1060
+ * onFallback: (err) => logger.warn('Running without transaction', err),
1061
+ * });
1062
+ * ```
1046
1063
  */
1047
- private _handleOperatorSyntax;
1064
+ withTransaction<T>(callback: (session: ClientSession) => Promise<T>, options?: WithTransactionOptions): Promise<T>;
1065
+ private _isTransactionUnsupported;
1048
1066
  /**
1049
- * Handle bracket syntax with object value
1067
+ * Execute custom query with event emission
1050
1068
  */
1051
- private _handleBracketSyntax;
1052
- private _parseSort;
1053
- private _toMongoOperator;
1054
- private _createSafeRegex;
1055
- private _escapeRegex;
1069
+ _executeQuery<T>(buildQuery: (Model: Model<TDoc>) => Promise<T>): Promise<T>;
1056
1070
  /**
1057
- * Sanitize $match configuration to prevent dangerous operators
1058
- * Recursively filters out operators like $where, $function, $accumulator
1071
+ * Build operation context and run before hooks (sorted by priority).
1072
+ *
1073
+ * Hook execution order is deterministic:
1074
+ * 1. POLICY (100) — tenant isolation, soft-delete filtering, validation
1075
+ * 2. CACHE (200) — cache lookup (after policy filters are injected)
1076
+ * 3. OBSERVABILITY (300) — audit logging, metrics
1077
+ * 4. DEFAULT (500) — user-registered hooks
1059
1078
  */
1060
- private _sanitizeMatchConfig;
1079
+ _buildContext(operation: string, options: Record<string, unknown>): Promise<RepositoryContext>;
1061
1080
  /**
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.
1081
+ * Parse sort string or object
1065
1082
  */
1066
- private _sanitizePipeline;
1083
+ _parseSort(sort: SortSpec | string | undefined): SortSpec;
1067
1084
  /**
1068
- * Recursively sanitize expression objects, blocking dangerous operators
1069
- * like $where, $function, $accumulator inside $addFields/$set stages.
1085
+ * Parse populate specification
1070
1086
  */
1071
- private _sanitizeExpressions;
1072
- private _sanitizeSearch;
1087
+ _parsePopulate(populate: PopulateSpec | undefined): string[] | PopulateOptions[];
1073
1088
  /**
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
- * // ]
1089
+ * Handle errors with proper HTTP status codes
1085
1090
  */
1086
- private _buildRegexSearch;
1087
- private _convertValue;
1088
- private _parseOr;
1089
- private _enhanceWithBetween;
1090
- private _pluralize;
1091
- private _capitalize;
1091
+ _handleError(error: Error): HttpError;
1092
1092
  }
1093
1093
  //#endregion
1094
1094
  //#region src/index.d.ts
@@ -1102,6 +1102,6 @@ declare class QueryParser {
1102
1102
  * @example
1103
1103
  * const userRepo = createRepository(UserModel, [timestampPlugin()]);
1104
1104
  */
1105
- declare function createRepository<TDoc>(Model: mongoose$1.Model<TDoc, any, any, any>, plugins?: PluginType[], paginationConfig?: PaginationConfig, options?: RepositoryOptions): Repository<TDoc>;
1105
+ declare function createRepository<TDoc>(Model: _$mongoose.Model<TDoc, any, any, any>, plugins?: PluginType[], paginationConfig?: PaginationConfig, options?: RepositoryOptions): Repository<TDoc>;
1106
1106
  //#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 };
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, parseDuplicateKeyError, prefixedId, requireField, sequentialId, softDeletePlugin, subdocumentPlugin, timestampPlugin, uniqueField, validateUpdateBody, validationChainPlugin };