@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.
- package/README.md +74 -4
- package/dist/{PaginationEngine-BDaAsyPn.mjs → PaginationEngine-CfQRM1yz.mjs} +16 -16
- package/dist/Repository-BY40uhcv.mjs +3603 -0
- package/dist/actions/index.d.mts +1 -1
- package/dist/actions/index.mjs +1 -1
- package/dist/adapter/index.d.mts +190 -0
- package/dist/adapter/index.mjs +2 -0
- package/dist/adapter-DTDMnUOH.mjs +158 -0
- package/dist/ai/index.d.mts +1 -1
- package/dist/ai/index.mjs +1 -1
- package/dist/better-auth/index.d.mts +149 -0
- package/dist/better-auth/index.mjs +81 -0
- package/dist/field-selection-Br3WPOEb.mjs +99 -0
- package/dist/{index-D9cORR-o.d.mts → index-BbHaKwi5.d.mts} +33 -13
- package/dist/index.d.mts +667 -30
- package/dist/index.mjs +240 -17
- package/dist/{logger-8-VS1za6.mjs → logger-DprijP5h.mjs} +1 -5
- package/dist/{mongooseToJsonSchema-B187ufOP.mjs → mongooseToJsonSchema-BYbG894y.mjs} +1 -1
- package/dist/{mongooseToJsonSchema-rtkdbBhf.d.mts → mongooseToJsonSchema-BgRAaXX-.d.mts} +2 -1
- package/dist/pagination/PaginationEngine.d.mts +2 -2
- package/dist/pagination/PaginationEngine.mjs +1 -1
- package/dist/plugins/index.d.mts +2 -2
- package/dist/plugins/index.mjs +2 -2
- package/dist/query/primitives/coercion.d.mts +1 -1
- package/dist/query/primitives/coercion.mjs +1 -1
- package/dist/query/primitives/indexes.d.mts +1 -1
- package/dist/{types-DXscZnLu.d.mts → types-BbaPZleN.d.mts} +371 -112
- package/dist/{update-CDxPtpsk.mjs → update-DLcSSa5b.mjs} +41 -27
- package/dist/utils/index.d.mts +2 -91
- package/dist/utils/index.mjs +5 -5
- package/dist/{validation-chain.plugin-CH-xPoKM.d.mts → validation-chain.plugin-CAPYCj_T.d.mts} +212 -80
- package/dist/validation-chain.plugin-_-WuNml2.mjs +2399 -0
- package/package.json +20 -3
- package/dist/field-selection-dI0m4RBs.mjs +0 -221
- package/dist/validation-chain.plugin-BicIgzzt.mjs +0 -4970
- /package/dist/{coercion-oM8G4b6m.d.mts → coercion-TRMmR49h.d.mts} +0 -0
- /package/dist/{id-resolution-e4BT9zAV.mjs → id-resolution-B0Odvvou.mjs} +0 -0
- /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
|
-
*
|
|
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
|
-
*
|
|
593
|
-
*
|
|
594
|
-
*
|
|
595
|
-
*
|
|
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
|
-
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
792
|
-
|
|
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
|
-
/**
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
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
|
-
/**
|
|
1026
|
-
|
|
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:
|
|
1347
|
+
apply(repo: TRepo): void;
|
|
1031
1348
|
}
|
|
1032
|
-
/** Plugin function signature */
|
|
1033
|
-
type PluginFunction = (repo:
|
|
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
|
-
/**
|
|
1042
|
-
|
|
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
|
-
*
|
|
1227
|
-
*
|
|
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
|
-
/**
|
|
1293
|
-
|
|
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.
|
|
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
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
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 {
|
|
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 };
|