@classytic/mongokit 3.11.1 → 3.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. package/README.md +75 -5
  2. package/dist/{PaginationEngine-CvDFfVXT.mjs → PaginationEngine-CfQRM1yz.mjs} +16 -16
  3. package/dist/Repository-BY40uhcv.mjs +3603 -0
  4. package/dist/actions/index.d.mts +1 -1
  5. package/dist/actions/index.mjs +1 -1
  6. package/dist/adapter/index.d.mts +190 -0
  7. package/dist/adapter/index.mjs +2 -0
  8. package/dist/adapter-DTDMnUOH.mjs +158 -0
  9. package/dist/ai/index.d.mts +1 -1
  10. package/dist/ai/index.mjs +3 -3
  11. package/dist/better-auth/index.d.mts +149 -0
  12. package/dist/better-auth/index.mjs +81 -0
  13. package/dist/field-selection-Br3WPOEb.mjs +99 -0
  14. package/dist/{index-dNTmpgK8.d.mts → index-BbHaKwi5.d.mts} +33 -13
  15. package/dist/index.d.mts +678 -36
  16. package/dist/index.mjs +240 -17
  17. package/dist/{logger-DJ7egMnd.mjs → logger-DprijP5h.mjs} +1 -5
  18. package/dist/{mongooseToJsonSchema-D1sdCOmj.mjs → mongooseToJsonSchema-BYbG894y.mjs} +68 -20
  19. package/dist/{mongooseToJsonSchema-DXxjFkLB.d.mts → mongooseToJsonSchema-BgRAaXX-.d.mts} +10 -2
  20. package/dist/pagination/PaginationEngine.d.mts +3 -2
  21. package/dist/pagination/PaginationEngine.mjs +1 -1
  22. package/dist/plugins/index.d.mts +2 -2
  23. package/dist/plugins/index.mjs +2 -2
  24. package/dist/query/primitives/coercion.d.mts +1 -1
  25. package/dist/query/primitives/coercion.mjs +1 -1
  26. package/dist/query/primitives/indexes.d.mts +1 -1
  27. package/dist/{types-Dc2aDVUi.d.mts → types-BbaPZleN.d.mts} +419 -205
  28. package/dist/{update-DEJYcgfV.mjs → update-DLcSSa5b.mjs} +184 -45
  29. package/dist/utils/index.d.mts +2 -91
  30. package/dist/utils/index.mjs +5 -5
  31. package/dist/{validation-chain.plugin-B0VSdcwa.d.mts → validation-chain.plugin-CAPYCj_T.d.mts} +228 -98
  32. package/dist/validation-chain.plugin-_-WuNml2.mjs +2399 -0
  33. package/package.json +25 -5
  34. package/dist/field-selection-dI0m4RBs.mjs +0 -221
  35. package/dist/validation-chain.plugin-Dllv-cml.mjs +0 -5122
  36. /package/dist/{coercion-oM8G4b6m.d.mts → coercion-TRMmR49h.d.mts} +0 -0
  37. /package/dist/{id-resolution-e4BT9zAV.mjs → id-resolution-B0Odvvou.mjs} +0 -0
  38. /package/dist/{indexes-C26GNZRU.d.mts → indexes-CgOF7Wjd.d.mts} +0 -0
@@ -1,9 +1,13 @@
1
+ import * as _$_classytic_repo_core_filter0 from "@classytic/repo-core/filter";
2
+ import { Filter } from "@classytic/repo-core/filter";
1
3
  import * as _$mongoose from "mongoose";
2
4
  import { Document, Model, PipelineStage, PopulateOptions, Types } from "mongoose";
3
- import * as _$_classytic_repo_core_filter0 from "@classytic/repo-core/filter";
5
+ import * as _$_classytic_repo_core_cache0 from "@classytic/repo-core/cache";
4
6
  import * as _$_classytic_repo_core_repository0 from "@classytic/repo-core/repository";
5
- import { DeleteManyResult, DeleteResult, LookupPopulateResult, UpdateManyResult } from "@classytic/repo-core/repository";
7
+ import { DeleteManyResult, DeleteResult, LookupPopulateResult, RepositoryBase, UpdateManyResult } from "@classytic/repo-core/repository";
6
8
  import { ValidationResult } from "@classytic/repo-core/schema";
9
+ import { AnyPaginationResult, OffsetPaginationResult } from "@classytic/repo-core/pagination";
10
+ import { HttpError } from "@classytic/repo-core/errors";
7
11
 
8
12
  //#region src/query/LookupBuilder.d.ts
9
13
  interface LookupOptions {
@@ -17,8 +21,15 @@ interface LookupOptions {
17
21
  as?: string;
18
22
  /** Whether to unwrap array to single object */
19
23
  single?: boolean;
20
- /** Field selection on the joined collection (shorthand for $project pipeline) */
21
- select?: string | Record<string, 0 | 1>;
24
+ /**
25
+ * Field selection on the joined collection (shorthand for `$project`).
26
+ * Three accepted shapes — all map to a single `$project` stage:
27
+ * - `string` — CSV form (`'name,price'`); leading `-` marks an exclusion.
28
+ * - `readonly string[]` — array form (`['name', 'price']` / `['-status']`);
29
+ * mirrors `repo-core/lookup` `LookupSpec.select` for cross-kit parity.
30
+ * - `Record<string, 0 | 1>` — explicit Mongo projection map.
31
+ */
32
+ select?: string | readonly string[] | Record<string, 0 | 1>;
22
33
  /** Additional pipeline to run on the joined collection */
23
34
  pipeline?: PipelineStage[];
24
35
  /** Optional let variables for pipeline */
@@ -31,6 +42,18 @@ interface LookupOptions {
31
42
  };
32
43
  /** Sanitize pipeline stages (default: true). Set false only for trusted server-side pipelines */
33
44
  sanitize?: boolean;
45
+ /**
46
+ * Joined-side filter, compiled into a `$match` stage inside the lookup
47
+ * pipeline AFTER the auto-generated join correlation. Mirrors
48
+ * `repo-core/lookup/types.ts#LookupSpec.where` and sqlitekit's
49
+ * predicate-on-the-JOIN behavior, so cross-kit code that narrows the
50
+ * foreign side (`where: eq('status', 'active')`) gets identical rows
51
+ * from both backends.
52
+ *
53
+ * Accepts Filter IR or a plain mongo query — `compileFilterToMongo`
54
+ * routes each correctly.
55
+ */
56
+ where?: Filter | Record<string, unknown>;
34
57
  }
35
58
  /**
36
59
  * Fluent builder for MongoDB $lookup aggregation stage
@@ -298,7 +321,7 @@ interface IController<TDoc> {
298
321
  * success: true,
299
322
  * data: {
300
323
  * method: 'offset',
301
- * docs: [...],
324
+ * data: [...],
302
325
  * total: 100,
303
326
  * page: 1,
304
327
  * pages: 5
@@ -307,7 +330,7 @@ interface IController<TDoc> {
307
330
  * }
308
331
  * ```
309
332
  */
310
- list(context: IRequestContext): Promise<IControllerResponse<PaginationResult<TDoc>>>;
333
+ list(context: IRequestContext): Promise<IControllerResponse<AnyPaginationResult<TDoc>>>;
311
334
  /**
312
335
  * Get single resource by ID
313
336
  *
@@ -457,7 +480,7 @@ interface IResponseFormatter {
457
480
  * Format paginated response
458
481
  * @param result - Pagination result from Repository.getAll()
459
482
  */
460
- paginated<T>(result: PaginationResult<T>): IControllerResponse<PaginationResult<T>>;
483
+ paginated<T>(result: AnyPaginationResult<T>): IControllerResponse<AnyPaginationResult<T>>;
461
484
  }
462
485
  //#endregion
463
486
  //#region src/types.d.ts
@@ -567,14 +590,161 @@ type CreateInput<TDoc> = Omit<TDoc, '_id' | 'createdAt' | 'updatedAt' | '__v'>;
567
590
  */
568
591
  type UpdatePatch<TDoc> = Partial<Omit<TDoc, '_id' | 'createdAt' | '__v'>>;
569
592
  /**
570
- * @deprecated Renamed to {@link UpdatePatch} in 3.11.0. This alias exists
571
- * for one release only and will be removed in 3.12. If you actually wanted
572
- * the bulk/find-and-update union (`UpdateSpec | Record | Record[]`), import
573
- * `UpdateInput` from `@classytic/repo-core/update` instead.
593
+ * Typed Mongo update document — the operator-shape that `findOneAndUpdate`
594
+ * / `updateMany` accept on the wire (`{ $set, $inc, $unset, ... }`).
595
+ *
596
+ * The explicit operator keys give IDE autocomplete + per-operator value
597
+ * typing; the trailing `[op: string]: unknown` index signature lets a
598
+ * caller-built `MongoOperatorUpdate` value assign to `Record<string,
599
+ * unknown>` without a cast — matching repo-core's `UpdateInput` slot
600
+ * (`UpdateSpec | Record<string, unknown> | Record<string, unknown>[]`)
601
+ * and unblocking the historic
602
+ *
603
+ * `normalizeUpdate(update) as unknown as Record<string, unknown>`
604
+ *
605
+ * footgun every domain package eventually wrote.
606
+ *
607
+ * Trade-off: with the index signature, an operator typo (`$st` instead
608
+ * of `$set`) won't fail TypeScript — Mongo would silently drop it. The
609
+ * common operators are listed explicitly so `obj.$set` / `obj.$inc`
610
+ * autocomplete stays useful; the escape hatch covers everything else
611
+ * (`$min`, `$max`, `$rename`, `$pop`, etc.).
612
+ *
613
+ * @example
614
+ * ```ts
615
+ * import type { MongoOperatorUpdate } from '@classytic/mongokit';
616
+ *
617
+ * const patch: MongoOperatorUpdate = {
618
+ * $set: { status: 'paid', paidAt: new Date() },
619
+ * $inc: { revision: 1 },
620
+ * };
621
+ * await repo.findOneAndUpdate({ _id: id }, patch); // no cast
622
+ * ```
574
623
  */
575
- type UpdateInput<TDoc> = UpdatePatch<TDoc>;
624
+ interface MongoOperatorUpdate {
625
+ /** Assign top-level or dotted-path values. */
626
+ $set?: Record<string, unknown>;
627
+ /** Set only on insert (upsert path). */
628
+ $setOnInsert?: Record<string, unknown>;
629
+ /** Remove fields (`Mongo` accepts `''`, `1`, or `true`). */
630
+ $unset?: Record<string, '' | 1 | true>;
631
+ /** Atomic numeric increment / decrement. */
632
+ $inc?: Record<string, number>;
633
+ /** Atomic numeric multiplication. */
634
+ $mul?: Record<string, number>;
635
+ /** Push to array (or `{ $each, $position, $slice, $sort }`). */
636
+ $push?: Record<string, unknown>;
637
+ /** Pull matching values from array. */
638
+ $pull?: Record<string, unknown>;
639
+ /** Pull a list of literals from array. */
640
+ $pullAll?: Record<string, unknown[]>;
641
+ /** Push only when not already present. */
642
+ $addToSet?: Record<string, unknown>;
643
+ /** Remove first / last element (`-1` / `1`). */
644
+ $pop?: Record<string, -1 | 1>;
645
+ /** Set only when the new value is smaller / larger. */
646
+ $min?: Record<string, unknown>;
647
+ $max?: Record<string, unknown>;
648
+ /** Rename top-level fields. */
649
+ $rename?: Record<string, string>;
650
+ /** Auto-set Date / Timestamp on this update. */
651
+ $currentDate?: Record<string, true | {
652
+ $type: 'date' | 'timestamp';
653
+ }>;
654
+ /** Bitwise ops on integer fields. */
655
+ $bit?: Record<string, {
656
+ and?: number;
657
+ or?: number;
658
+ xor?: number;
659
+ }>;
660
+ /**
661
+ * Escape hatch for any operator not explicitly listed above (e.g.
662
+ * positional-array operators like `$[<identifier>]` inside a path
663
+ * key). Index signature is what makes the type assign to
664
+ * `Record<string, unknown>` without a cast.
665
+ */
666
+ [op: string]: unknown;
667
+ }
576
668
  /** Hook execution mode */
577
669
  type HookMode = 'sync' | 'async';
670
+ /**
671
+ * Minimal repo surface exposed to wrap-style middleware via the
672
+ * `repo` field of `MiddlewareContext`. Intentionally narrow — middleware
673
+ * shouldn't reach into the full `Repository` (that would couple every
674
+ * middleware to the kit). The 5 floor methods plus `Model` cover every
675
+ * legitimate middleware need (cross-doc reads, custom queries, type-
676
+ * narrowing on `Model.modelName`).
677
+ */
678
+ interface MinimalRepoView<TDoc> {
679
+ readonly Model: Model<TDoc>;
680
+ readonly model: string;
681
+ getById(id: string | ObjectId, options?: Record<string, unknown>): Promise<TDoc | null>;
682
+ getAll?(params?: Record<string, unknown>, options?: Record<string, unknown>): Promise<unknown>;
683
+ create(data: Record<string, unknown>, options?: Record<string, unknown>): Promise<TDoc>;
684
+ update(id: string | ObjectId, data: Record<string, unknown>, options?: Record<string, unknown>): Promise<unknown>;
685
+ delete(id: string | ObjectId, options?: Record<string, unknown>): Promise<unknown>;
686
+ }
687
+ /**
688
+ * Context passed to a wrap-style middleware. Middleware mutates
689
+ * `context` before calling `await next()` to alter input; inspects /
690
+ * transforms the resolved value to alter output; uses plain try/catch
691
+ * around `next()` for error handling.
692
+ *
693
+ * The `before:` / `after:` / `error:` hook engine still fires from
694
+ * within `next()` — hooks remain authoritative for policy / cache /
695
+ * audit so security and correctness plugins keep working unchanged.
696
+ * Middleware composes ergonomically on top.
697
+ */
698
+ interface MiddlewareContext<TDoc> {
699
+ /** Operation name (`'create'`, `'getById'`, ..., or any plugin op). */
700
+ readonly operation: string;
701
+ /** Live repository context — mutate fields before `next()` to alter input. */
702
+ readonly context: RepositoryContext;
703
+ /**
704
+ * Continue the chain — the next middleware (or, for the innermost
705
+ * middleware, the actual op + after/error hooks). The return value
706
+ * is the resolved op result; throwing propagates up the chain.
707
+ */
708
+ next: () => Promise<unknown>;
709
+ /** Narrow view of the repo for cross-doc reads / custom queries. */
710
+ readonly repo: MinimalRepoView<TDoc>;
711
+ }
712
+ /**
713
+ * Wrap-style middleware — Prisma `$extends.query` / Express middleware
714
+ * shape. Registered via `repo.useMiddleware(mw)`. Composes around every
715
+ * `_runOp` invocation; runs in registration order (first-registered
716
+ * runs outermost).
717
+ *
718
+ * @example Time every operation
719
+ * ```ts
720
+ * repo.useMiddleware(async ({ operation, next }) => {
721
+ * const start = performance.now();
722
+ * try { return await next(); }
723
+ * finally { console.log(`${operation} took ${performance.now() - start}ms`); }
724
+ * });
725
+ * ```
726
+ *
727
+ * @example Stamp tenant on every create
728
+ * ```ts
729
+ * repo.useMiddleware(async ({ operation, context, next }) => {
730
+ * if (operation === 'create' && context.data) {
731
+ * context.data.tenantId = currentTenant();
732
+ * }
733
+ * return next();
734
+ * });
735
+ * ```
736
+ *
737
+ * @example Short-circuit (skip the actual op) — return without calling next()
738
+ * ```ts
739
+ * repo.useMiddleware(async ({ operation, context, next }) => {
740
+ * if (operation === 'getById' && readOnlyMode) {
741
+ * return cachedReadOnlyResponse(context.id);
742
+ * }
743
+ * return next();
744
+ * });
745
+ * ```
746
+ */
747
+ type Middleware<TDoc = unknown> = (ctx: MiddlewareContext<TDoc>) => Promise<unknown>;
578
748
  /** Search strategy used by Repository.getAll when `search` is provided */
579
749
  type RepositorySearchMode = 'text' | 'regex' | 'auto';
580
750
  /** Repository options */
@@ -612,6 +782,82 @@ interface RepositoryOptions {
612
782
  * before tenant scoping → cross-tenant cache poisoning risk).
613
783
  */
614
784
  pluginOrderChecks?: 'warn' | 'throw' | 'off';
785
+ /**
786
+ * When `true` (default `false`), the repository checks every read/
787
+ * write op's filter against the schema's declared paths and warns
788
+ * once per `(modelName, fieldName)` pair if a filter key isn't on
789
+ * the schema AND `strictQuery: true` is in effect.
790
+ *
791
+ * **Why this exists.** With `mongoose.set('strictQuery', true)` (the
792
+ * mongoose 6 default; some projects pin it explicitly), filter keys
793
+ * not declared on the schema are SILENTLY STRIPPED before the query
794
+ * runs. `findOne({ code: 'X' })` on a schema without `code` becomes
795
+ * `findOne({})` — returns the first doc, not null — and the bug
796
+ * masquerades as "wrong row returned" without any error or log.
797
+ *
798
+ * The diagnostic catches the trap at runtime: log once per offending
799
+ * key per model, so a single bad call surfaces during dev/staging
800
+ * instead of in production. Costs O(filter keys) per call against
801
+ * a constructor-cached set; negligible.
802
+ *
803
+ * Hosts who route mongokit's `warn` into their own logger (via
804
+ * `configureLogger({ warn: ... })`) can ship this enabled in
805
+ * production without polluting stdout.
806
+ *
807
+ * @default false
808
+ */
809
+ warnOnStrictQueryStrip?: boolean;
810
+ /**
811
+ * Names of plugins that MUST be present in the plugin chain at
812
+ * construction time. Throws a `TypeError` from the constructor if
813
+ * any listed plugin is absent — eliminates the silent-misconfig
814
+ * shape where a host forgets to wire `multiTenantPlugin` and ships
815
+ * a tenant-leak to production.
816
+ *
817
+ * Names match each plugin's exported `name` property (the plugin's
818
+ * own canonical identifier, NOT the JavaScript function name).
819
+ * Bundled plugins as of 3.13:
820
+ * - `'multi-tenant'` (from `multiTenantPlugin`)
821
+ * - `'softDelete'` (from `softDeletePlugin`)
822
+ * - `'auditLog'` (from `auditLogPlugin`)
823
+ * - `'auditTrail'` (from `auditTrailPlugin`)
824
+ * - `'cache'` (from `cachePlugin`)
825
+ * - `'observability'` (from `observabilityPlugin`)
826
+ * - `'method-registry'` (from `methodRegistryPlugin`)
827
+ * - `'batch-operations'`, `'mongo-operations'`,
828
+ * `'aggregate-helpers'`, `'subdocument'`, `'cascade'`,
829
+ * `'custom-id'`, `'elastic-search'`, `'fieldFilter'`,
830
+ * `'validation-chain'`, `'lease'`, `'timestamp'`
831
+ *
832
+ * Naming follows each plugin's `name` field — kebab-case for some,
833
+ * camelCase for others. The error message lists "Installed plugins"
834
+ * verbatim so a typo surfaces as a side-by-side diff.
835
+ *
836
+ * **Why this exists.** "Always wire `multiTenantPlugin`" and "always
837
+ * wire `softDeletePlugin`" are documented as conventions in 7+ and
838
+ * 9+ CLAUDE.md files across the classytic codebase respectively,
839
+ * but documentation drifts and convention-by-comment isn't
840
+ * enforceable. Listing the required plugins at the boot boundary
841
+ * fails closed — the constructor throws with the missing name, the
842
+ * bug surfaces on first run (or first test), not in production.
843
+ *
844
+ * @example
845
+ * ```ts
846
+ * new Repository(OrderModel, [
847
+ * methodRegistryPlugin(),
848
+ * multiTenantPlugin({ tenantField: 'organizationId' }),
849
+ * softDeletePlugin(),
850
+ * auditLogPlugin({ logger }),
851
+ * ], paginationConfig, {
852
+ * requirePlugins: ['multiTenant', 'softDelete', 'auditLog'],
853
+ * });
854
+ * ```
855
+ *
856
+ * If any name in this list isn't matched by a plugin's `name`
857
+ * property in the chain, the constructor throws with the missing
858
+ * names listed.
859
+ */
860
+ requirePlugins?: readonly string[];
615
861
  }
616
862
  /** Pagination configuration */
617
863
  interface PaginationConfig {
@@ -732,69 +978,75 @@ interface AggregatePaginationOptions {
732
978
  /** Read preference for replica sets (e.g. 'secondaryPreferred') */
733
979
  readPreference?: ReadPreferenceType;
734
980
  }
735
- /** Offset pagination result */
736
- interface OffsetPaginationResult<T = unknown> {
737
- /** Pagination method used */
738
- method: 'offset';
739
- /** Array of documents */
740
- docs: T[];
741
- /** Current page number */
742
- page: number;
743
- /** Documents per page */
744
- limit: number;
745
- /** Total document count */
746
- total: number;
747
- /** Total page count */
748
- pages: number;
749
- /** Whether next page exists */
750
- hasNext: boolean;
751
- /** Whether previous page exists */
752
- hasPrev: boolean;
753
- /** Performance warning for deep pagination */
754
- warning?: string;
755
- }
756
- /** Keyset pagination result */
757
- interface KeysetPaginationResult<T = unknown> {
758
- /** Pagination method used */
759
- method: 'keyset';
760
- /** Array of documents */
761
- docs: T[];
762
- /** Documents per page */
763
- limit: number;
764
- /** Whether more documents exist */
765
- hasMore: boolean;
766
- /** Cursor token for next page */
767
- next: string | null;
768
- }
769
- /** Aggregate pagination result */
770
- interface AggregatePaginationResult<T = unknown> {
771
- /** Pagination method used */
772
- method: 'aggregate';
773
- /** Array of documents */
774
- docs: T[];
775
- /** Current page number */
776
- page: number;
777
- /** Documents per page */
778
- limit: number;
779
- /** Total document count */
780
- total: number;
781
- /** Total page count */
782
- pages: number;
783
- /** Whether next page exists */
784
- hasNext: boolean;
785
- /** Whether previous page exists */
786
- hasPrev: boolean;
787
- /** Performance warning for deep pagination */
788
- warning?: string;
789
- }
790
- /** Union type for all pagination results */
791
- type PaginationResult<T = unknown> = OffsetPaginationResult<T> | KeysetPaginationResult<T> | AggregatePaginationResult<T>;
792
981
  /** Session-only options — shared base for lightweight operations */
793
982
  interface SessionOptions {
794
983
  /** MongoDB session for transactions */
795
984
  session?: unknown;
796
985
  /** Organization/tenant ID for multi-tenant plugin scoping */
797
986
  organizationId?: string | ObjectId;
987
+ /**
988
+ * Per-call escape hatch — plugins listed here that opt into the
989
+ * mechanism (`auditLog`, `auditTrail`, `observability`) read this
990
+ * from `RepositoryContext` and skip their work for this call. Use
991
+ * on hot paths (e.g. heartbeats / counters) where the audit overhead
992
+ * dominates.
993
+ *
994
+ * Plugins that enforce correctness or security (multi-tenant scope,
995
+ * cache invalidation, soft-delete filter) deliberately ignore this
996
+ * flag — security/correctness must not be skippable per-call.
997
+ *
998
+ * **Match exactly by `plugin.name`** (camelCase, NOT kebab-case):
999
+ * - `'auditLog'` — see `auditLogPlugin()` in
1000
+ * `src/plugins/audit-log.plugin.ts` (`name: 'auditLog'`)
1001
+ * - `'auditTrail'` — see `auditTrailPlugin()` in
1002
+ * `src/plugins/audit-trail.plugin.ts` (`name: 'auditTrail'`)
1003
+ * - `'observability'` — see `observabilityPlugin()` in
1004
+ * `src/plugins/observability.plugin.ts` (`name: 'observability'`)
1005
+ *
1006
+ * Pre-3.13.0 docs referenced `'audit-log'` / `'audit-trail'` —
1007
+ * those don't match any plugin's `name` field, so passing them
1008
+ * silently fails to skip anything. The strings above are the
1009
+ * authoritative ones.
1010
+ */
1011
+ skipPlugins?: readonly string[];
1012
+ /**
1013
+ * Per-call escape hatch for `multiTenantPlugin`. When `true`, the
1014
+ * tenant scope hook returns without injecting `organizationId` (or
1015
+ * the configured `tenantField`) into the operation's policy slot
1016
+ * for THIS call only. The plugin emits `after:tenant-bypass` with
1017
+ * `{ context, operation, reason: 'option' }` so audit / observability
1018
+ * plugins can distinguish bypassed queries from tenant-scoped ones.
1019
+ *
1020
+ * Reach for this when:
1021
+ * - A super-admin / platform-admin user needs cross-tenant read
1022
+ * for support tooling.
1023
+ * - A migration script writes / backfills across tenants
1024
+ * deliberately.
1025
+ * - A scheduled job (TTL cleanup, cross-tenant aggregate, billing
1026
+ * rollup) needs the unscoped view.
1027
+ *
1028
+ * **`bypassTenant` is the most-specific decision** — runs before
1029
+ * `skipWhen` (plugin-level callback) and `resolveContext` (CLS
1030
+ * fallback). Distinct from `skipWhen` which fires for every call
1031
+ * and reads context to decide; `bypassTenant: true` is a deliberate
1032
+ * per-call opt-out with no role check, so the calling code carries
1033
+ * the responsibility.
1034
+ *
1035
+ * **NOT a substitute for proper access control.** This flag bypasses
1036
+ * tenant scoping; it does NOT bypass authentication, RBAC, or any
1037
+ * other host-level access check. Wrap it with your auth boundary at
1038
+ * the controller / service layer.
1039
+ *
1040
+ * @example
1041
+ * ```ts
1042
+ * // Support engineer pulling a customer's order across tenants:
1043
+ * await orderRepo.findAll({}, { bypassTenant: true });
1044
+ *
1045
+ * // Migration backfill across the whole estate:
1046
+ * for await (const doc of repo.cursor({}, { bypassTenant: true })) { ... }
1047
+ * ```
1048
+ */
1049
+ bypassTenant?: boolean;
798
1050
  /** Extensible — plugins can read custom fields from options */
799
1051
  [key: string]: unknown;
800
1052
  }
@@ -821,12 +1073,14 @@ interface OperationOptions extends ReadOptions {
821
1073
  /** Additional query filters (e.g., for soft delete) */
822
1074
  query?: Record<string, unknown>;
823
1075
  }
824
- /** Cache-aware operation options — extends OperationOptions with cache controls */
1076
+ /**
1077
+ * Cache-aware operation options — extends OperationOptions with the
1078
+ * unified cache plugin's per-call shape. Pass `cache: { staleTime, swr,
1079
+ * tags, bypass, ... }` to override freshness for this call.
1080
+ */
825
1081
  interface CacheableOptions extends OperationOptions {
826
- /** Skip cache for this operation (read from DB directly) */
827
- skipCache?: boolean;
828
- /** Custom TTL for this operation in seconds */
829
- cacheTtl?: number;
1082
+ /** Per-call cache override forwarded to the unified cache plugin. */
1083
+ cache?: _$_classytic_repo_core_cache0.CacheOptions;
830
1084
  }
831
1085
  /** withTransaction options */
832
1086
  interface WithTransactionOptions {
@@ -1009,6 +1263,15 @@ interface RepositoryContext {
1009
1263
  session?: unknown;
1010
1264
  /** Read preference for replica sets */
1011
1265
  readPreference?: ReadPreferenceType;
1266
+ /**
1267
+ * Per-call escape hatch — plugin names listed here that opt into the
1268
+ * mechanism (`auditLog`, `auditTrail`, `observability`) skip their
1269
+ * work for this call. Threaded from `OperationOptions.skipPlugins`
1270
+ * via `_buildContext`. Security/correctness plugins (multi-tenant
1271
+ * scope, cache invalidation, soft-delete filter) deliberately ignore
1272
+ * this — those must not be skippable per-call.
1273
+ */
1274
+ skipPlugins?: readonly string[];
1012
1275
  /** Pagination filters */
1013
1276
  filters?: Record<string, unknown>;
1014
1277
  /** Sort specification */
@@ -1044,37 +1307,68 @@ interface RepositoryContext {
1044
1307
  * is the only source of soft semantics.
1045
1308
  */
1046
1309
  deleteMode?: 'hard' | 'soft';
1047
- /** Skip cache for this operation */
1048
- skipCache?: boolean;
1049
- /** Custom TTL for this operation (seconds) */
1050
- cacheTtl?: number;
1051
- /** Whether result was served from cache (internal) */
1310
+ /**
1311
+ * Per-call cache options forwarded by the unified cache plugin
1312
+ * (`@classytic/repo-core/cache`). Kits copy this from the caller's
1313
+ * `options.cache` onto the context so the plugin's `before:<op>`
1314
+ * hook can read it.
1315
+ */
1316
+ cache?: _$_classytic_repo_core_cache0.CacheOptions;
1317
+ /** Whether result was served from cache (internal — set by cache plugin) */
1052
1318
  _cacheHit?: boolean;
1053
- /** Cached result (internal) */
1319
+ /** Cached result (internal — set by cache plugin) */
1054
1320
  _cachedResult?: unknown;
1321
+ /** Resolved cache options stash (internal — set by cache plugin) */
1322
+ _cacheKey?: string;
1323
+ /** Resolved cache options stash (internal — set by cache plugin) */
1324
+ _cacheResolved?: unknown;
1325
+ /** Cache hit status (internal — `'fresh'` or `'stale'`) */
1326
+ _cacheStatus?: 'fresh' | 'stale' | 'miss' | 'disabled' | 'bypass';
1055
1327
  /** IDs to cascade delete (internal) */
1056
1328
  _cascadeIds?: unknown[];
1057
1329
  /** Custom context data from plugins */
1058
1330
  [key: string]: unknown;
1059
1331
  }
1060
- /** Plugin interface */
1061
- interface Plugin {
1332
+ /**
1333
+ * Plugin interface — generic over the repo type so kit-internal plugins
1334
+ * can target the narrower `RepositoryInstance` while cross-package
1335
+ * plugins from `@classytic/repo-core/cache` etc. that target the
1336
+ * shared `RepositoryBase` flow through the same array.
1337
+ *
1338
+ * `RepositoryInstance` formally `extends RepositoryBase` (see below),
1339
+ * so `Plugin<RepositoryBase>` is structurally assignable to
1340
+ * `Plugin<RepositoryInstance>` via TypeScript's method-bivariance
1341
+ * rule for interface methods. No host-site casts required.
1342
+ */
1343
+ interface Plugin<TRepo extends RepositoryBase = RepositoryInstance> {
1062
1344
  /** Plugin name */
1063
1345
  name: string;
1064
1346
  /** Apply plugin to repository */
1065
- apply(repo: RepositoryInstance): void;
1347
+ apply(repo: TRepo): void;
1066
1348
  }
1067
- /** Plugin function signature */
1068
- type PluginFunction = (repo: RepositoryInstance) => void;
1069
- /** Plugin type (object or function) */
1070
- type PluginType = Plugin | PluginFunction;
1349
+ /** Plugin function signature. Same TRepo widening as `Plugin`. */
1350
+ type PluginFunction<TRepo extends RepositoryBase = RepositoryInstance> = (repo: TRepo) => void;
1351
+ /** Plugin type (object or function). */
1352
+ type PluginType<TRepo extends RepositoryBase = RepositoryInstance> = Plugin<TRepo> | PluginFunction<TRepo>;
1071
1353
  /** Hook with priority for deterministic phase ordering */
1072
1354
  interface PrioritizedHook {
1073
1355
  listener: (data: any) => void | Promise<void>;
1074
1356
  priority: number;
1075
1357
  }
1076
- /** Repository instance for plugin type reference */
1077
- interface RepositoryInstance {
1358
+ /**
1359
+ * Repository instance for plugin type reference.
1360
+ *
1361
+ * Extends `@classytic/repo-core/repository`'s `RepositoryBase` so the
1362
+ * full hook + adapter contract is included. This keeps mongokit's
1363
+ * `Plugin<RepositoryInstance>` and repo-core's `Plugin<RepositoryBase>`
1364
+ * structurally aligned — cross-package plugins (`cachePlugin` from
1365
+ * `@classytic/repo-core/cache` etc.) flow into mongokit's `plugins[]`
1366
+ * array without per-call casts at the host site.
1367
+ *
1368
+ * The `[key: string]: unknown` index signature at the bottom keeps
1369
+ * dynamic plugin-method registration (`registerMethod`) compatible.
1370
+ */
1371
+ interface RepositoryInstance extends RepositoryBase {
1078
1372
  Model: Model<any>;
1079
1373
  model: string;
1080
1374
  _hooks: Map<string, PrioritizedHook[]>;
@@ -1092,13 +1386,14 @@ interface RepositoryInstance {
1092
1386
  _buildContext(operation: string, options: Record<string, unknown> | object): Promise<RepositoryContext>;
1093
1387
  _handleError(error: Error): HttpError;
1094
1388
  update(id: string | ObjectId, data: Record<string, unknown>, options?: Record<string, unknown>): Promise<unknown>;
1389
+ findOneAndUpdate(filter: Record<string, unknown>, update: Record<string, unknown> | Record<string, unknown>[], options?: Record<string, unknown>): Promise<unknown>;
1095
1390
  aggregatePipeline(pipeline: PipelineStage[], options?: Record<string, unknown>): Promise<unknown[]>;
1096
1391
  getByQuery(query: Record<string, unknown>, options?: Record<string, unknown>): Promise<unknown>;
1097
1392
  _executeQuery<T>(buildQuery: (Model: Model<any>) => Promise<T>): Promise<T>;
1098
1393
  [key: string]: unknown;
1099
1394
  }
1100
1395
  /** Repository operation names */
1101
- type RepositoryOperation = 'create' | 'createMany' | 'update' | 'updateMany' | 'findOneAndUpdate' | 'delete' | 'deleteMany' | 'restore' | 'getById' | 'getByQuery' | 'getOne' | 'getAll' | 'findAll' | 'getOrCreate' | 'count' | 'exists' | 'distinct' | 'aggregate' | 'aggregatePaginate' | 'aggregatePipeline' | 'aggregatePipelinePaginate' | 'lookupPopulate' | 'bulkWrite';
1396
+ type RepositoryOperation = 'create' | 'createMany' | 'update' | 'updateMany' | 'findOneAndUpdate' | 'delete' | 'deleteMany' | 'restore' | 'getById' | 'getByQuery' | 'getOne' | 'getAll' | 'findAll' | 'getOrCreate' | 'count' | 'exists' | 'distinct' | 'aggregate' | 'aggregatePaginate' | 'aggregatePipeline' | 'aggregatePipelinePaginate' | 'lookupPopulate' | 'bulkWrite' | 'claim' | 'claimVersion' | 'cursor';
1102
1397
  /** Event lifecycle phases */
1103
1398
  type EventPhase = 'before' | 'after' | 'error';
1104
1399
  /** Repository event names (generated from template literals) */
@@ -1136,6 +1431,27 @@ interface FieldPreset {
1136
1431
  /** Additional fields for admins */
1137
1432
  admin?: string[];
1138
1433
  }
1434
+ /** Value type identifier for cursor serialization */
1435
+ type ValueType = 'date' | 'objectid' | 'boolean' | 'number' | 'string' | 'null' | 'unknown';
1436
+ /** Cursor payload */
1437
+ interface CursorPayload {
1438
+ /** Primary sort field value (legacy single-field) */
1439
+ v: string | number | boolean | null;
1440
+ /** Value type identifier (legacy single-field) */
1441
+ t: ValueType;
1442
+ /** Document ID */
1443
+ id: string;
1444
+ /** ID type identifier */
1445
+ idType: ValueType;
1446
+ /** Sort specification */
1447
+ sort: SortSpec;
1448
+ /** Cursor version */
1449
+ ver: number;
1450
+ /** Compound sort field values (multi-field keyset) */
1451
+ vals?: Record<string, string | number | boolean | null>;
1452
+ /** Compound sort value types */
1453
+ types?: Record<string, ValueType>;
1454
+ }
1139
1455
  /** Decoded cursor */
1140
1456
  interface DecodedCursor {
1141
1457
  /** Primary sort field value (rehydrated) — legacy compat */
@@ -1237,84 +1553,13 @@ interface MinMaxResult {
1237
1553
  max: unknown;
1238
1554
  }
1239
1555
  /**
1240
- * Cache adapter interface — bring your own cache implementation.
1241
- * Works with Redis, Memcached, in-memory, or any key-value store.
1242
- *
1243
- * Shape is aligned with `@classytic/repo-core/cache#CacheAdapter` and arc's
1244
- * `CacheStore` so one adapter implementation satisfies every consumer. The
1245
- * `delete` method is named for consistency with `Map.delete` / `Set.delete` /
1246
- * `MinimalRepo.delete(id)` — your Redis wrapper translates to `redis.del`.
1247
- *
1248
- * @example Redis implementation:
1249
- * ```typescript
1250
- * const redisCache: CacheAdapter = {
1251
- * async get(key) { return JSON.parse(await redis.get(key) || 'null'); },
1252
- * async set(key, value, ttl) { await redis.setex(key, ttl, JSON.stringify(value)); },
1253
- * async delete(key) { await redis.del(key); },
1254
- * async clear(pattern) {
1255
- * const keys = await redis.keys(pattern || '*');
1256
- * if (keys.length) await redis.del(...keys);
1257
- * }
1258
- * };
1259
- * ```
1556
+ * Per-call cache options — TanStack-shaped (`staleTime`, `gcTime`, `swr`,
1557
+ * `tags`, `bypass`, `enabled`, `key`). Threaded through read methods via
1558
+ * `options.cache` (CRUD) or `aggRequest.cache` (aggregate).
1260
1559
  */
1261
- interface CacheAdapter {
1262
- /** Get value by key, returns null if not found or expired */
1263
- get<T = unknown>(key: string): Promise<T | null>;
1264
- /** Set value with TTL in seconds */
1265
- set<T = unknown>(key: string, value: T, ttl: number): Promise<void>;
1266
- /** Delete a single key. No-op when the key doesn't exist. */
1267
- delete(key: string): Promise<void>;
1268
- /** Clear keys matching pattern (optional, used for bulk invalidation) */
1269
- clear?(pattern?: string): Promise<void>;
1270
- }
1271
- /** Cache plugin options */
1272
- interface CacheOptions {
1273
- /** Cache adapter implementation (required) */
1274
- adapter: CacheAdapter;
1275
- /** Default TTL in seconds (default: 60) */
1276
- ttl?: number;
1277
- /** TTL for byId queries in seconds (default: same as ttl) */
1278
- byIdTtl?: number;
1279
- /** TTL for query/list results in seconds (default: same as ttl) */
1280
- queryTtl?: number;
1281
- /** Key prefix for namespacing (default: 'mk') */
1282
- prefix?: string;
1283
- /** Enable debug logging (default: false) */
1284
- debug?: boolean;
1285
- /**
1286
- * Skip caching for queries with these characteristics:
1287
- * - largeLimit: Skip if limit > value (default: 100)
1288
- */
1289
- skipIf?: {
1290
- largeLimit?: number;
1291
- };
1292
- /**
1293
- * TTL jitter to mitigate cache stampedes. When many entries share the same
1294
- * TTL and expire simultaneously, every concurrent reader misses and hammers
1295
- * the DB at once. Jitter spreads expirations over a window.
1296
- *
1297
- * - `number` in [0, 1]: fractional symmetric jitter. `0.1` multiplies the
1298
- * TTL by a random factor in [0.9, 1.1]. Default: 0 (no jitter).
1299
- * - `function(ttl)`: full control — receive the configured TTL (seconds),
1300
- * return the effective TTL (seconds) to store with.
1301
- */
1302
- jitter?: number | ((ttlSeconds: number) => number);
1303
- }
1304
- /** Options for cache-aware operations */
1305
1560
  interface CacheOperationOptions {
1306
- /** Skip cache for this operation (read from DB directly) */
1307
- skipCache?: boolean;
1308
- /** Custom TTL for this operation in seconds */
1309
- cacheTtl?: number;
1310
- }
1311
- /** Cache statistics (for debugging/monitoring) */
1312
- interface CacheStats {
1313
- hits: number;
1314
- misses: number;
1315
- sets: number;
1316
- invalidations: number;
1317
- errors: number;
1561
+ /** Per-call cache override forwarded to the unified cache plugin. */
1562
+ cache?: _$_classytic_repo_core_cache0.CacheOptions;
1318
1563
  }
1319
1564
  /** Cascade relation definition */
1320
1565
  interface CascadeRelation {
@@ -1349,37 +1594,6 @@ interface CascadeOptions {
1349
1594
  /** Logger for cascade operations */
1350
1595
  logger?: Logger;
1351
1596
  }
1352
- /** HTTP Error with status code */
1353
- interface HttpError extends Error {
1354
- status: number;
1355
- /**
1356
- * Stable machine-readable error code. Hosts can switch on this in catch
1357
- * blocks instead of grepping the message string. Currently emitted for:
1358
- * - `SEARCH_NOT_CONFIGURED` — search requested on a repo with no text
1359
- * index AND no `searchMode: 'regex' | 'auto'` + `searchFields` config.
1360
- */
1361
- code?: string;
1362
- /**
1363
- * Free-form structured metadata for diagnostics. Pairs with `code` so
1364
- * hosts can render a clearer message in their own UI without parsing
1365
- * the human-readable `message` string. Safe for logs.
1366
- */
1367
- meta?: Record<string, unknown>;
1368
- validationErrors?: Array<{
1369
- validator: string;
1370
- error: string;
1371
- }>;
1372
- /**
1373
- * Structured metadata for duplicate-key (E11000) errors. Safe to surface in
1374
- * logs/audit. Includes the offending field names and a boolean-only mirror
1375
- * of whether each field had a duplicate value — never the value itself
1376
- * unless `parseDuplicateKeyError` was called with `{ exposeValues: true }`.
1377
- */
1378
- duplicate?: {
1379
- fields: string[]; /** Only populated when parseDuplicateKeyError was called with exposeValues:true. */
1380
- values?: Record<string, unknown>;
1381
- };
1382
- }
1383
1597
  /**
1384
1598
  * Combines all plugin method types into a single type
1385
1599
  * Useful when you're using all plugins and want full type safety
@@ -1396,7 +1610,7 @@ interface HttpError extends Error {
1396
1610
  * // TypeScript knows about all plugin methods!
1397
1611
  * await repo.increment(id, 'views', 1);
1398
1612
  * await repo.restore(id);
1399
- * await repo.invalidateCache(id);
1613
+ * await repo.cache?.invalidateByTags(['org:abc']);
1400
1614
  * ```
1401
1615
  *
1402
1616
  * Note: Import the individual plugin method types if you need them:
@@ -1407,7 +1621,6 @@ interface HttpError extends Error {
1407
1621
  * AggregateHelpersMethods,
1408
1622
  * SubdocumentMethods,
1409
1623
  * SoftDeleteMethods,
1410
- * CacheMethods,
1411
1624
  * } from '@classytic/mongokit';
1412
1625
  * ```
1413
1626
  */
@@ -1478,11 +1691,12 @@ type AllPluginMethods<TDoc> = {
1478
1691
  lean?: boolean;
1479
1692
  session?: unknown;
1480
1693
  }): Promise<OffsetPaginationResult<TDoc>>;
1481
- invalidateCache(id: string): Promise<void>;
1482
- invalidateListCache(): Promise<void>;
1483
- invalidateAllCache(): Promise<void>;
1484
- getCacheStats(): CacheStats;
1485
- resetCacheStats(): void;
1694
+ /**
1695
+ * Cache plugin handle (set by `cachePlugin({ adapter })`). Exposes the
1696
+ * `CacheEngine` plus convenience invalidation helpers. `undefined` when
1697
+ * no cache plugin is wired.
1698
+ */
1699
+ cache?: _$_classytic_repo_core_cache0.RepositoryCacheHandle;
1486
1700
  };
1487
1701
  /**
1488
1702
  * Helper type to add all plugin methods to a repository class
@@ -1510,4 +1724,4 @@ type AllPluginMethods<TDoc> = {
1510
1724
  */
1511
1725
  type WithPlugins<TDoc, TRepo extends RepositoryInstance = RepositoryInstance> = TRepo & AllPluginMethods<TDoc>;
1512
1726
  //#endregion
1513
- export { ReadPreferenceType as $, InferDocument as A, ObjectId as B, EventPayload as C, WithTransactionOptions as Ct, GroupResult as D, IResponseFormatter as Dt, FindOneAndUpdateOptions as E, IRequestContext as Et, Logger as F, PaginationResult as G, OffsetPaginationResult as H, LookupPopulateOptions as I, PluginFunction as J, PartialBy as K, LookupPopulateResult as L, KeysOfType as M, KeysetPaginationOptions as N, HookMode as O, LookupBuilder as Ot, KeysetPaginationResult as P, ReadOptions as Q, MinMaxResult as R, EventHandlers as S, WithPlugins as St, FieldPreset as T, IControllerResponse as Tt, OperationOptions as U, OffsetPaginationOptions as V, PaginationConfig as W, PopulateSpec as X, PluginType as Y, PrioritizedHook as Z, DecodedCursor as _, UpdatePatch as _t, AnyDocument as a, RequiredBy as at, DeleteResult as b, ValidationChainOptions as bt, CacheOperationOptions as c, SoftDeleteFilterMode as ct, CacheableOptions as d, SortDirection as dt, RepositoryContext as et, CascadeOptions as f, SortSpec as ft, CreateOptions as g, UpdateOptions as gt, CreateInput as h, UpdateManyResult as ht, AllPluginMethods as i, RepositoryOptions as it, InferRawDoc as j, HttpError as k, LookupOptions as kt, CacheOptions as l, SoftDeleteOptions as lt, CollationOptions as m, UpdateInput as mt, AggregatePaginationOptions as n, RepositoryInstance as nt, AnyModel as o, SelectSpec as ot, CascadeRelation as p, Strict as pt, Plugin as q, AggregatePaginationResult as r, RepositoryOperation as rt, CacheAdapter as s, SessionOptions as st, AggregateOptions as t, RepositoryEvent as tt, CacheStats as u, SoftDeleteRepository as ut, DeepPartial as v, UpdateWithValidationResult as vt, EventPhase as w, IController as wt, DocField as x, ValidatorDefinition as xt, DeleteManyResult as y, UserContext as yt, NonNullableFields as z };
1727
+ export { RepositoryEvent as $, KeysetPaginationOptions as A, ObjectId as B, FieldPreset as C, IControllerResponse as Ct, InferDocument as D, LookupOptions as Dt, HookMode as E, LookupBuilder as Et, MiddlewareContext as F, Plugin as G, OperationOptions as H, MinMaxResult as I, PopulateSpec as J, PluginFunction as K, MinimalRepoView as L, LookupPopulateOptions as M, LookupPopulateResult as N, InferRawDoc as O, Middleware as P, RepositoryContext as Q, MongoOperatorUpdate as R, EventPhase as S, IController as St, GroupResult as T, IResponseFormatter as Tt, PaginationConfig as U, OffsetPaginationOptions as V, PartialBy as W, ReadOptions as X, PrioritizedHook as Y, ReadPreferenceType as Z, DeleteManyResult as _, ValidationChainOptions as _t, AnyModel as a, SessionOptions as at, EventHandlers as b, WithPlugins as bt, CacheableOptions as c, SoftDeleteRepository as ct, CollationOptions as d, Strict as dt, RepositoryInstance as et, CreateInput as f, UpdateManyResult as ft, DeepPartial as g, UserContext as gt, DecodedCursor as h, UpdateWithValidationResult as ht, AnyDocument as i, SelectSpec as it, Logger as j, KeysOfType as k, CascadeOptions as l, SortDirection as lt, CursorPayload as m, UpdatePatch as mt, AggregatePaginationOptions as n, RepositoryOptions as nt, BasePaginationOptions as o, SoftDeleteFilterMode as ot, CreateOptions as p, UpdateOptions as pt, PluginType as q, AllPluginMethods as r, RequiredBy as rt, CacheOperationOptions as s, SoftDeleteOptions as st, AggregateOptions as t, RepositoryOperation as tt, CascadeRelation as u, SortSpec as ut, DeleteResult as v, ValidatorDefinition as vt, FindOneAndUpdateOptions as w, IRequestContext as wt, EventPayload as x, WithTransactionOptions as xt, DocField as y, ValueType as yt, NonNullableFields as z };