@classytic/mongokit 3.12.0 → 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 +74 -4
  2. package/dist/{PaginationEngine-BDaAsyPn.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 +1 -1
  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-D9cORR-o.d.mts → index-BbHaKwi5.d.mts} +33 -13
  15. package/dist/index.d.mts +667 -30
  16. package/dist/index.mjs +240 -17
  17. package/dist/{logger-8-VS1za6.mjs → logger-DprijP5h.mjs} +1 -5
  18. package/dist/{mongooseToJsonSchema-B187ufOP.mjs → mongooseToJsonSchema-BYbG894y.mjs} +1 -1
  19. package/dist/{mongooseToJsonSchema-rtkdbBhf.d.mts → mongooseToJsonSchema-BgRAaXX-.d.mts} +2 -1
  20. package/dist/pagination/PaginationEngine.d.mts +2 -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-DXscZnLu.d.mts → types-BbaPZleN.d.mts} +371 -112
  28. package/dist/{update-CDxPtpsk.mjs → update-DLcSSa5b.mjs} +41 -27
  29. package/dist/utils/index.d.mts +2 -91
  30. package/dist/utils/index.mjs +5 -5
  31. package/dist/{validation-chain.plugin-CH-xPoKM.d.mts → validation-chain.plugin-CAPYCj_T.d.mts} +212 -80
  32. package/dist/validation-chain.plugin-_-WuNml2.mjs +2399 -0
  33. package/package.json +20 -3
  34. package/dist/field-selection-dI0m4RBs.mjs +0 -221
  35. package/dist/validation-chain.plugin-BicIgzzt.mjs +0 -4970
  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
@@ -2,8 +2,9 @@ import * as _$_classytic_repo_core_filter0 from "@classytic/repo-core/filter";
2
2
  import { Filter } from "@classytic/repo-core/filter";
3
3
  import * as _$mongoose from "mongoose";
4
4
  import { Document, Model, PipelineStage, PopulateOptions, Types } from "mongoose";
5
+ import * as _$_classytic_repo_core_cache0 from "@classytic/repo-core/cache";
5
6
  import * as _$_classytic_repo_core_repository0 from "@classytic/repo-core/repository";
6
- import { DeleteManyResult, DeleteResult, LookupPopulateResult, UpdateManyResult } from "@classytic/repo-core/repository";
7
+ import { DeleteManyResult, DeleteResult, LookupPopulateResult, RepositoryBase, UpdateManyResult } from "@classytic/repo-core/repository";
7
8
  import { ValidationResult } from "@classytic/repo-core/schema";
8
9
  import { AnyPaginationResult, OffsetPaginationResult } from "@classytic/repo-core/pagination";
9
10
  import { HttpError } from "@classytic/repo-core/errors";
@@ -320,7 +321,7 @@ interface IController<TDoc> {
320
321
  * success: true,
321
322
  * data: {
322
323
  * method: 'offset',
323
- * docs: [...],
324
+ * data: [...],
324
325
  * total: 100,
325
326
  * page: 1,
326
327
  * pages: 5
@@ -589,14 +590,161 @@ type CreateInput<TDoc> = Omit<TDoc, '_id' | 'createdAt' | 'updatedAt' | '__v'>;
589
590
  */
590
591
  type UpdatePatch<TDoc> = Partial<Omit<TDoc, '_id' | 'createdAt' | '__v'>>;
591
592
  /**
592
- * @deprecated Renamed to {@link UpdatePatch} in 3.11.0. This alias exists
593
- * for one release only and will be removed in 3.12. If you actually wanted
594
- * the bulk/find-and-update union (`UpdateSpec | Record | Record[]`), import
595
- * `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
+ * ```
596
623
  */
597
- 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
+ }
598
668
  /** Hook execution mode */
599
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>;
600
748
  /** Search strategy used by Repository.getAll when `search` is provided */
601
749
  type RepositorySearchMode = 'text' | 'regex' | 'auto';
602
750
  /** Repository options */
@@ -634,6 +782,82 @@ interface RepositoryOptions {
634
782
  * before tenant scoping → cross-tenant cache poisoning risk).
635
783
  */
636
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[];
637
861
  }
638
862
  /** Pagination configuration */
639
863
  interface PaginationConfig {
@@ -760,6 +984,69 @@ interface SessionOptions {
760
984
  session?: unknown;
761
985
  /** Organization/tenant ID for multi-tenant plugin scoping */
762
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;
763
1050
  /** Extensible — plugins can read custom fields from options */
764
1051
  [key: string]: unknown;
765
1052
  }
@@ -786,12 +1073,14 @@ interface OperationOptions extends ReadOptions {
786
1073
  /** Additional query filters (e.g., for soft delete) */
787
1074
  query?: Record<string, unknown>;
788
1075
  }
789
- /** 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
+ */
790
1081
  interface CacheableOptions extends OperationOptions {
791
- /** Skip cache for this operation (read from DB directly) */
792
- skipCache?: boolean;
793
- /** Custom TTL for this operation in seconds */
794
- cacheTtl?: number;
1082
+ /** Per-call cache override forwarded to the unified cache plugin. */
1083
+ cache?: _$_classytic_repo_core_cache0.CacheOptions;
795
1084
  }
796
1085
  /** withTransaction options */
797
1086
  interface WithTransactionOptions {
@@ -974,6 +1263,15 @@ interface RepositoryContext {
974
1263
  session?: unknown;
975
1264
  /** Read preference for replica sets */
976
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[];
977
1275
  /** Pagination filters */
978
1276
  filters?: Record<string, unknown>;
979
1277
  /** Sort specification */
@@ -1009,37 +1307,68 @@ interface RepositoryContext {
1009
1307
  * is the only source of soft semantics.
1010
1308
  */
1011
1309
  deleteMode?: 'hard' | 'soft';
1012
- /** Skip cache for this operation */
1013
- skipCache?: boolean;
1014
- /** Custom TTL for this operation (seconds) */
1015
- cacheTtl?: number;
1016
- /** 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) */
1017
1318
  _cacheHit?: boolean;
1018
- /** Cached result (internal) */
1319
+ /** Cached result (internal — set by cache plugin) */
1019
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';
1020
1327
  /** IDs to cascade delete (internal) */
1021
1328
  _cascadeIds?: unknown[];
1022
1329
  /** Custom context data from plugins */
1023
1330
  [key: string]: unknown;
1024
1331
  }
1025
- /** Plugin interface */
1026
- 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> {
1027
1344
  /** Plugin name */
1028
1345
  name: string;
1029
1346
  /** Apply plugin to repository */
1030
- apply(repo: RepositoryInstance): void;
1347
+ apply(repo: TRepo): void;
1031
1348
  }
1032
- /** Plugin function signature */
1033
- type PluginFunction = (repo: RepositoryInstance) => void;
1034
- /** Plugin type (object or function) */
1035
- 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>;
1036
1353
  /** Hook with priority for deterministic phase ordering */
1037
1354
  interface PrioritizedHook {
1038
1355
  listener: (data: any) => void | Promise<void>;
1039
1356
  priority: number;
1040
1357
  }
1041
- /** Repository instance for plugin type reference */
1042
- 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 {
1043
1372
  Model: Model<any>;
1044
1373
  model: string;
1045
1374
  _hooks: Map<string, PrioritizedHook[]>;
@@ -1057,13 +1386,14 @@ interface RepositoryInstance {
1057
1386
  _buildContext(operation: string, options: Record<string, unknown> | object): Promise<RepositoryContext>;
1058
1387
  _handleError(error: Error): HttpError;
1059
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>;
1060
1390
  aggregatePipeline(pipeline: PipelineStage[], options?: Record<string, unknown>): Promise<unknown[]>;
1061
1391
  getByQuery(query: Record<string, unknown>, options?: Record<string, unknown>): Promise<unknown>;
1062
1392
  _executeQuery<T>(buildQuery: (Model: Model<any>) => Promise<T>): Promise<T>;
1063
1393
  [key: string]: unknown;
1064
1394
  }
1065
1395
  /** Repository operation names */
1066
- 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';
1067
1397
  /** Event lifecycle phases */
1068
1398
  type EventPhase = 'before' | 'after' | 'error';
1069
1399
  /** Repository event names (generated from template literals) */
@@ -1223,84 +1553,13 @@ interface MinMaxResult {
1223
1553
  max: unknown;
1224
1554
  }
1225
1555
  /**
1226
- * Cache adapter interface — bring your own cache implementation.
1227
- * Works with Redis, Memcached, in-memory, or any key-value store.
1228
- *
1229
- * Shape is aligned with `@classytic/repo-core/cache#CacheAdapter` and arc's
1230
- * `CacheStore` so one adapter implementation satisfies every consumer. The
1231
- * `delete` method is named for consistency with `Map.delete` / `Set.delete` /
1232
- * `MinimalRepo.delete(id)` — your Redis wrapper translates to `redis.del`.
1233
- *
1234
- * @example Redis implementation:
1235
- * ```typescript
1236
- * const redisCache: CacheAdapter = {
1237
- * async get(key) { return JSON.parse(await redis.get(key) || 'null'); },
1238
- * async set(key, value, ttl) { await redis.setex(key, ttl, JSON.stringify(value)); },
1239
- * async delete(key) { await redis.del(key); },
1240
- * async clear(pattern) {
1241
- * const keys = await redis.keys(pattern || '*');
1242
- * if (keys.length) await redis.del(...keys);
1243
- * }
1244
- * };
1245
- * ```
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).
1246
1559
  */
1247
- interface CacheAdapter {
1248
- /** Get value by key, returns null if not found or expired */
1249
- get<T = unknown>(key: string): Promise<T | null>;
1250
- /** Set value with TTL in seconds */
1251
- set<T = unknown>(key: string, value: T, ttl: number): Promise<void>;
1252
- /** Delete a single key. No-op when the key doesn't exist. */
1253
- delete(key: string): Promise<void>;
1254
- /** Clear keys matching pattern (optional, used for bulk invalidation) */
1255
- clear?(pattern?: string): Promise<void>;
1256
- }
1257
- /** Cache plugin options */
1258
- interface CacheOptions {
1259
- /** Cache adapter implementation (required) */
1260
- adapter: CacheAdapter;
1261
- /** Default TTL in seconds (default: 60) */
1262
- ttl?: number;
1263
- /** TTL for byId queries in seconds (default: same as ttl) */
1264
- byIdTtl?: number;
1265
- /** TTL for query/list results in seconds (default: same as ttl) */
1266
- queryTtl?: number;
1267
- /** Key prefix for namespacing (default: 'mk') */
1268
- prefix?: string;
1269
- /** Enable debug logging (default: false) */
1270
- debug?: boolean;
1271
- /**
1272
- * Skip caching for queries with these characteristics:
1273
- * - largeLimit: Skip if limit > value (default: 100)
1274
- */
1275
- skipIf?: {
1276
- largeLimit?: number;
1277
- };
1278
- /**
1279
- * TTL jitter to mitigate cache stampedes. When many entries share the same
1280
- * TTL and expire simultaneously, every concurrent reader misses and hammers
1281
- * the DB at once. Jitter spreads expirations over a window.
1282
- *
1283
- * - `number` in [0, 1]: fractional symmetric jitter. `0.1` multiplies the
1284
- * TTL by a random factor in [0.9, 1.1]. Default: 0 (no jitter).
1285
- * - `function(ttl)`: full control — receive the configured TTL (seconds),
1286
- * return the effective TTL (seconds) to store with.
1287
- */
1288
- jitter?: number | ((ttlSeconds: number) => number);
1289
- }
1290
- /** Options for cache-aware operations */
1291
1560
  interface CacheOperationOptions {
1292
- /** Skip cache for this operation (read from DB directly) */
1293
- skipCache?: boolean;
1294
- /** Custom TTL for this operation in seconds */
1295
- cacheTtl?: number;
1296
- }
1297
- /** Cache statistics (for debugging/monitoring) */
1298
- interface CacheStats {
1299
- hits: number;
1300
- misses: number;
1301
- sets: number;
1302
- invalidations: number;
1303
- errors: number;
1561
+ /** Per-call cache override forwarded to the unified cache plugin. */
1562
+ cache?: _$_classytic_repo_core_cache0.CacheOptions;
1304
1563
  }
1305
1564
  /** Cascade relation definition */
1306
1565
  interface CascadeRelation {
@@ -1351,7 +1610,7 @@ interface CascadeOptions {
1351
1610
  * // TypeScript knows about all plugin methods!
1352
1611
  * await repo.increment(id, 'views', 1);
1353
1612
  * await repo.restore(id);
1354
- * await repo.invalidateCache(id);
1613
+ * await repo.cache?.invalidateByTags(['org:abc']);
1355
1614
  * ```
1356
1615
  *
1357
1616
  * Note: Import the individual plugin method types if you need them:
@@ -1362,7 +1621,6 @@ interface CascadeOptions {
1362
1621
  * AggregateHelpersMethods,
1363
1622
  * SubdocumentMethods,
1364
1623
  * SoftDeleteMethods,
1365
- * CacheMethods,
1366
1624
  * } from '@classytic/mongokit';
1367
1625
  * ```
1368
1626
  */
@@ -1433,11 +1691,12 @@ type AllPluginMethods<TDoc> = {
1433
1691
  lean?: boolean;
1434
1692
  session?: unknown;
1435
1693
  }): Promise<OffsetPaginationResult<TDoc>>;
1436
- invalidateCache(id: string): Promise<void>;
1437
- invalidateListCache(): Promise<void>;
1438
- invalidateAllCache(): Promise<void>;
1439
- getCacheStats(): CacheStats;
1440
- 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;
1441
1700
  };
1442
1701
  /**
1443
1702
  * Helper type to add all plugin methods to a repository class
@@ -1465,4 +1724,4 @@ type AllPluginMethods<TDoc> = {
1465
1724
  */
1466
1725
  type WithPlugins<TDoc, TRepo extends RepositoryInstance = RepositoryInstance> = TRepo & AllPluginMethods<TDoc>;
1467
1726
  //#endregion
1468
- export { RepositoryInstance as $, InferDocument as A, OffsetPaginationOptions as B, EventHandlers as C, IControllerResponse as Ct, FindOneAndUpdateOptions as D, LookupOptions as Dt, FieldPreset as E, LookupBuilder as Et, LookupPopulateOptions as F, PluginFunction as G, PaginationConfig as H, LookupPopulateResult as I, PrioritizedHook as J, PluginType as K, MinMaxResult as L, KeysOfType as M, KeysetPaginationOptions as N, GroupResult as O, Logger as P, RepositoryEvent as Q, NonNullableFields as R, DocField as S, IController as St, EventPhase as T, IResponseFormatter as Tt, PartialBy as U, OperationOptions as V, Plugin as W, ReadPreferenceType as X, ReadOptions as Y, RepositoryContext as Z, CursorPayload as _, ValidationChainOptions as _t, AnyModel as a, SoftDeleteFilterMode as at, DeleteManyResult as b, WithPlugins as bt, CacheOperationOptions as c, SortDirection as ct, CacheableOptions as d, UpdateInput as dt, RepositoryOperation as et, CascadeOptions as f, UpdateManyResult as ft, CreateOptions as g, UserContext as gt, CreateInput as h, UpdateWithValidationResult as ht, AnyDocument as i, SessionOptions as it, InferRawDoc as j, HookMode as k, CacheOptions as l, SortSpec as lt, CollationOptions as m, UpdatePatch as mt, AggregatePaginationOptions as n, RequiredBy as nt, BasePaginationOptions as o, SoftDeleteOptions as ot, CascadeRelation as p, UpdateOptions as pt, PopulateSpec as q, AllPluginMethods as r, SelectSpec as rt, CacheAdapter as s, SoftDeleteRepository as st, AggregateOptions as t, RepositoryOptions as tt, CacheStats as u, Strict as ut, DecodedCursor as v, ValidatorDefinition as vt, EventPayload as w, IRequestContext as wt, DeleteResult as x, WithTransactionOptions as xt, DeepPartial as y, ValueType as yt, ObjectId 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 };