@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
package/dist/index.d.mts CHANGED
@@ -1,15 +1,16 @@
1
- import { $ as ReadPreferenceType, A as InferDocument, B as ObjectId, C as EventPayload, Ct as WithTransactionOptions, D as GroupResult, Dt as IResponseFormatter, E as FindOneAndUpdateOptions, Et as IRequestContext, F as Logger, G as PaginationResult, H as OffsetPaginationResult, I as LookupPopulateOptions, J as PluginFunction, K as PartialBy, L as LookupPopulateResult, M as KeysOfType, N as KeysetPaginationOptions, O as HookMode, Ot as LookupBuilder, P as KeysetPaginationResult, Q as ReadOptions, R as MinMaxResult, S as EventHandlers, St as WithPlugins, T as FieldPreset, Tt as IControllerResponse, U as OperationOptions, V as OffsetPaginationOptions, W as PaginationConfig, X as PopulateSpec, Y as PluginType, Z as PrioritizedHook, _ as DecodedCursor, _t as UpdatePatch, a as AnyDocument, at as RequiredBy, b as DeleteResult, bt as ValidationChainOptions, c as CacheOperationOptions, ct as SoftDeleteFilterMode, d as CacheableOptions, dt as SortDirection, et as RepositoryContext, f as CascadeOptions, ft as SortSpec, g as CreateOptions, gt as UpdateOptions, h as CreateInput, ht as UpdateManyResult, i as AllPluginMethods, it as RepositoryOptions, j as InferRawDoc, k as HttpError, kt as LookupOptions, l as CacheOptions, lt as SoftDeleteOptions, m as CollationOptions, mt as UpdateInput, n as AggregatePaginationOptions, nt as RepositoryInstance, o as AnyModel, ot as SelectSpec, p as CascadeRelation, pt as Strict, q as Plugin, r as AggregatePaginationResult, rt as RepositoryOperation, s as CacheAdapter, st as SessionOptions, t as AggregateOptions, tt as RepositoryEvent, u as CacheStats, ut as SoftDeleteRepository, v as DeepPartial, vt as UpdateWithValidationResult, w as EventPhase, wt as IController, x as DocField, xt as ValidatorDefinition, y as DeleteManyResult, yt as UserContext, z as NonNullableFields } from "./types-Dc2aDVUi.mjs";
2
- import { t as index_d_exports } from "./index-dNTmpgK8.mjs";
1
+ import { $ as RepositoryEvent, A as KeysetPaginationOptions, B as ObjectId, C as FieldPreset, Ct as IControllerResponse, D as InferDocument, Dt as LookupOptions, E as HookMode, Et as LookupBuilder, F as MiddlewareContext, G as Plugin, H as OperationOptions, I as MinMaxResult, J as PopulateSpec, K as PluginFunction, L as MinimalRepoView, M as LookupPopulateOptions, N as LookupPopulateResult, O as InferRawDoc, P as Middleware, Q as RepositoryContext, R as MongoOperatorUpdate, S as EventPhase, St as IController, T as GroupResult, Tt as IResponseFormatter, U as PaginationConfig, V as OffsetPaginationOptions, W as PartialBy, X as ReadOptions, Y as PrioritizedHook, Z as ReadPreferenceType, _ as DeleteManyResult, _t as ValidationChainOptions, a as AnyModel, at as SessionOptions, b as EventHandlers, bt as WithPlugins, c as CacheableOptions, ct as SoftDeleteRepository, d as CollationOptions, dt as Strict, et as RepositoryInstance, f as CreateInput, ft as UpdateManyResult, g as DeepPartial, gt as UserContext, h as DecodedCursor, ht as UpdateWithValidationResult, i as AnyDocument, it as SelectSpec, j as Logger, k as KeysOfType, l as CascadeOptions, lt as SortDirection, m as CursorPayload, mt as UpdatePatch, n as AggregatePaginationOptions, nt as RepositoryOptions, o as BasePaginationOptions, ot as SoftDeleteFilterMode, p as CreateOptions, pt as UpdateOptions, q as PluginType, r as AllPluginMethods, rt as RequiredBy, s as CacheOperationOptions, st as SoftDeleteOptions, t as AggregateOptions, tt as RepositoryOperation, u as CascadeRelation, ut as SortSpec, v as DeleteResult, vt as ValidatorDefinition, w as FindOneAndUpdateOptions, wt as IRequestContext, x as EventPayload, xt as WithTransactionOptions, y as DocField, yt as ValueType, z as NonNullableFields } from "./types-BbaPZleN.mjs";
2
+ import { t as index_d_exports } from "./index-BbHaKwi5.mjs";
3
3
  import { PaginationEngine } from "./pagination/PaginationEngine.mjs";
4
- import { $ as aggregateHelpersPlugin, A as PrefixedIdOptions, B as BatchOperationsMethods, C as methodRegistryPlugin, D as CustomIdOptions, E as elasticSearchPlugin, F as prefixedId, G as AuditQueryOptions, H as batchOperationsPlugin, I as sequentialId, J as AuditTrailOptions, K as AuditQueryResult, L as cascadePlugin, M as customIdPlugin, N as dateSequentialId, O as DateSequentialIdOptions, P as getNextSequence, Q as AggregateHelpersMethods, R as CacheMethods, T as ElasticSearchOptions, U as AuditEntry, V as BulkWriteResult, W as AuditOperation, X as auditTrailPlugin, Y as AuditTrailQuery, Z as auditLogPlugin, _ as observabilityPlugin, a as uniqueField, b as MongoOperationsMethods, c as TenantContext, d as SubdocumentMethods, f as subdocumentPlugin, g as OperationMetric, h as ObservabilityOptions, i as requireField, j as SequentialIdOptions, k as IdGenerator, l as TenantStore, m as softDeletePlugin, n as blockIf, o as validationChainPlugin, p as SoftDeleteMethods, q as AuditTrailMethods, r as immutableField, s as timestampPlugin, t as autoInject, u as createTenantContext, v as MultiTenantOptions, w as fieldFilterPlugin, x as mongoOperationsPlugin, y as multiTenantPlugin, z as cachePlugin } from "./validation-chain.plugin-B0VSdcwa.mjs";
5
- import { n as SchemaPathsLike, t as FieldType$1 } from "./coercion-oM8G4b6m.mjs";
6
- import { n as SchemaIndexes } from "./indexes-C26GNZRU.mjs";
7
- import { a as createFieldPreset, c as getMongooseProjection, d as parseDuplicateKeyError, i as configureLogger, l as createError, n as buildCrudSchemasFromMongooseSchema, o as filterResponseData, r as createMemoryCache, s as getFieldsForUser, t as buildCrudSchemasFromModel, u as isDuplicateKeyError } from "./mongooseToJsonSchema-DXxjFkLB.mjs";
4
+ import { $ as AuditQueryResult, A as elasticSearchPlugin, B as sequentialId, D as leasePlugin, E as LeasePluginOptions, F as SequentialIdOptions, G as RepositoryCachePluginOptions, H as CacheAdapter, I as customIdPlugin, J as BulkWriteResult, K as cachePlugin, L as dateSequentialId, M as DateSequentialIdOptions, N as IdGenerator, O as fieldFilterPlugin, P as PrefixedIdOptions, Q as AuditQueryOptions, R as getNextSequence, S as mongoOperationsPlugin, T as LeaseMethods, U as CacheOptions, V as cascadePlugin, W as RepositoryCacheHandle, X as AuditEntry, Y as batchOperationsPlugin, Z as AuditOperation, _ as observabilityPlugin, a as uniqueField, at as AggregateHelpersMethods, b as multiTenantPlugin, c as TenantContext, d as SubdocumentMethods, et as AuditTrailMethods, f as subdocumentPlugin, g as OperationMetric, h as ObservabilityOptions, i as requireField, it as auditLogPlugin, j as CustomIdOptions, k as ElasticSearchOptions, l as TenantStore, m as softDeletePlugin, n as blockIf, nt as AuditTrailQuery, o as validationChainPlugin, ot as aggregateHelpersPlugin, p as SoftDeleteMethods, q as BatchOperationsMethods, r as immutableField, rt as auditTrailPlugin, s as timestampPlugin, t as autoInject, tt as AuditTrailOptions, u as createTenantContext, v as MultiTenantOptions, w as methodRegistryPlugin, x as MongoOperationsMethods, y as adminBypass, z as prefixedId } from "./validation-chain.plugin-CAPYCj_T.mjs";
5
+ import { n as SchemaPathsLike, t as FieldType$1 } from "./coercion-TRMmR49h.mjs";
6
+ import { n as SchemaIndexes } from "./indexes-CgOF7Wjd.mjs";
7
+ import { a as createFieldPreset, c as getMongooseProjection, d as parseDuplicateKeyError, i as configureLogger, l as createError, n as buildCrudSchemasFromMongooseSchema, o as filterResponseData, r as createMemoryCache, s as getFieldsForUser, t as buildCrudSchemasFromModel, u as isDuplicateKeyError } from "./mongooseToJsonSchema-BgRAaXX-.mjs";
8
8
  import * as _$mongoose from "mongoose";
9
9
  import { ClientSession, Expression, Model, PipelineStage, PopulateOptions } from "mongoose";
10
- import { AggPaginationRequest, AggRequest, AggResult, RepositoryBase } from "@classytic/repo-core/repository";
11
- import { UpdateInput as UpdateInput$1 } from "@classytic/repo-core/update";
12
- import { OffsetPaginationResultCore } from "@classytic/repo-core/pagination";
10
+ import { AggPaginationRequest, AggRequest, AggResult, KeysetAggPaginationResult, RepositoryBase } from "@classytic/repo-core/repository";
11
+ import { UpdateInput } from "@classytic/repo-core/update";
12
+ import { AggregatePaginationResult, KeysetPaginationResult, OffsetPaginationResult, OffsetPaginationResultCore } from "@classytic/repo-core/pagination";
13
+ import { HttpError } from "@classytic/repo-core/errors";
13
14
 
14
15
  //#region src/operations.d.ts
15
16
  /**
@@ -844,6 +845,11 @@ declare class QueryParser {
844
845
  /**
845
846
  * Recursively sanitize expression objects, blocking dangerous operators
846
847
  * like $where, $function, $accumulator inside $addFields/$set stages.
848
+ *
849
+ * Depth-guarded — same protection `_sanitizeMatchConfig` ships, since
850
+ * a hostile `$cond`/`$switch` chain can recurse arbitrarily deep and
851
+ * exhaust JS stack. Without the guard, this is a one-liner DoS against
852
+ * any host that exposes pipeline parsing to untrusted callers.
847
853
  */
848
854
  private _sanitizeExpressions;
849
855
  private _sanitizeSearch;
@@ -910,6 +916,13 @@ declare class Repository<TDoc = unknown> extends RepositoryBase {
910
916
  readonly searchFields: string[] | undefined;
911
917
  [key: string]: unknown;
912
918
  private _hasTextIndex;
919
+ /**
920
+ * Wrap-style middleware chain. Composed around every `_runOp`
921
+ * invocation in registration order — earlier-registered middlewares
922
+ * are outer (run first / unwind last). See `useMiddleware()` and
923
+ * the `Middleware<TDoc>` type for the protocol.
924
+ */
925
+ private readonly _middlewares;
913
926
  constructor(Model: Model<TDoc, any, any, any>, plugins?: PluginType[], paginationConfig?: PaginationConfig, options?: RepositoryOptions);
914
927
  /**
915
928
  * Live read-through view of the hook engine's listener registry.
@@ -931,14 +944,117 @@ declare class Repository<TDoc = unknown> extends RepositoryBase {
931
944
  private _emitHook;
932
945
  private _emitErrorHook;
933
946
  /**
934
- * Run a Repository operation under the standard envelope:
935
- * - invoke `fn`, emit `after:<op>` with `{ context, result }` on success
936
- * - emit `error:<op>` and rethrow via `_handleError` on failure
947
+ * Wire the `warnOnStrictQueryStrip` diagnostic. When enabled, every
948
+ * filter-shaped op (`getById`, `getByQuery`, `getOne`, `findAll`,
949
+ * `count`, `exists`, `update`, `delete`, `findOneAndUpdate`,
950
+ * `updateMany`, `deleteMany`, `claim`, `claimVersion`, `cursor`)
951
+ * checks its `context.query` keys against the cached set of schema
952
+ * paths. Top-level keys not on the schema get logged once per
953
+ * `(model, key)` pair — so a single bad caller surfaces in dev /
954
+ * staging instead of returning the wrong row in prod.
955
+ *
956
+ * Only runs when `strictQuery !== false` is in effect — if mongoose's
957
+ * strictQuery is off, unknown keys flow through to the driver and
958
+ * the trap doesn't apply.
959
+ */
960
+ private _wireStrictQueryStripDiagnostic;
961
+ /**
962
+ * Register a wrap-style middleware. Middleware composes AROUND every
963
+ * `_runOp` invocation — it sees the operation name, the live
964
+ * `RepositoryContext`, and a `next()` continuation that runs the
965
+ * remaining chain + the actual op + after/error hooks.
966
+ *
967
+ * **Composes with `repo.on()` and plugins, doesn't replace them.**
968
+ * Middleware is for ergonomics (timing, short-circuit, input/output
969
+ * mutation in a single closure) — NOT for security policy. Use the
970
+ * `before:*` hook engine for tenant scope, soft-delete filtering,
971
+ * cache invalidation, and audit; those plugins MUST run before
972
+ * middleware sees the op.
973
+ *
974
+ * **Execution order on every call** (build/before hooks fire BEFORE
975
+ * the middleware chain dispatches, so middleware cannot wrap a
976
+ * before-hook failure):
977
+ *
978
+ * ```text
979
+ * _buildContext + before:<op> ← repo.on('before:*') hooks
980
+ * [outer middleware pre] ← repo.useMiddleware() registrations
981
+ * [...inner middleware pre]
982
+ * fn (driver call)
983
+ * after:<op> | error:<op> ← repo.on('after:*' / 'error:*')
984
+ * [...inner middleware post]
985
+ * [outer middleware post]
986
+ * ```
987
+ *
988
+ * Why this order matters: a `before:create` hook that injects a
989
+ * tenant filter into `context.query` runs BEFORE the middleware chain
990
+ * is composed. Middleware sees a context that was already scoped by
991
+ * the policy plugins — it never has the authority to short-circuit a
992
+ * tenant check, because the throw from a `before:*` policy hook
993
+ * unwinds before middleware ever fires. That's by design — middleware
994
+ * as a security boundary would be impossible to audit, since
995
+ * registration order would determine whether scope wins.
937
996
  *
997
+ * Practical consequence: if you want middleware to observe a policy
998
+ * failure (e.g. metric a tenant-scope rejection), use `repo.on('error:
999
+ * <op>', ...)` instead — that fires from inside the middleware chain
1000
+ * and is reachable.
1001
+ *
1002
+ * Registration order = composition order: the first middleware
1003
+ * registered runs outermost (wraps everything else).
1004
+ *
1005
+ * @example Time every op
1006
+ * ```ts
1007
+ * repo.useMiddleware(async ({ operation, next }) => {
1008
+ * const start = performance.now();
1009
+ * try { return await next(); }
1010
+ * finally { metrics.record(operation, performance.now() - start); }
1011
+ * });
1012
+ * ```
1013
+ *
1014
+ * @example Inject `tenantId` on every create
1015
+ * ```ts
1016
+ * repo.useMiddleware(async ({ operation, context, next }) => {
1017
+ * if (operation === 'create' && context.data) {
1018
+ * context.data.tenantId = currentTenant();
1019
+ * }
1020
+ * return next();
1021
+ * });
1022
+ * ```
1023
+ *
1024
+ * @example Short-circuit (return without `next()` to skip the actual op)
1025
+ * ```ts
1026
+ * repo.useMiddleware(async ({ operation, context, next }) => {
1027
+ * if (operation === 'getById' && readOnlyMaintenance) {
1028
+ * return cachedReadOnlyResponse(context.id);
1029
+ * }
1030
+ * return next();
1031
+ * });
1032
+ * ```
1033
+ */
1034
+ useMiddleware(middleware: Middleware<TDoc>): this;
1035
+ /**
1036
+ * Compose the registered middleware chain around `exec`. Outermost
1037
+ * middleware (first registered) wraps innermost. Each middleware sees
1038
+ * the live `RepositoryContext` (already populated + run through
1039
+ * `before:*` hooks at the call site — see `useMiddleware` JSDoc for
1040
+ * the exact order). `after:` / `error:` hooks fire from inside
1041
+ * `exec`, so middleware composes WITH plugins, not instead of them.
1042
+ *
1043
+ * `_runOp` calls this with its standard try/catch envelope; inline-try
1044
+ * methods (`getById`, `update`, `delete`, `deleteMany`, `aggregatePaginate`,
1045
+ * `lookupPopulate`, plus `getOne`/`getAll` cache-hit branches) call it
1046
+ * directly so middleware sees every op — including cached reads — not
1047
+ * just the ones routed through `_runOp`.
1048
+ */
1049
+ private _composeMiddleware;
1050
+ /**
1051
+ * Standard operation envelope: compose middleware around an exec that
1052
+ * runs `fn`, emits `after:<op>` on success or `error:<op>` on failure.
938
1053
  * Methods with branched in-try logic that emit `after:*` from multiple
939
1054
  * paths (`getById`, `update`, `delete`, `deleteMany`, `aggregatePaginate`,
940
- * `lookupPopulate`) intentionally keep their inline try/catch rather than
941
- * being restructured to return a single result through this helper.
1055
+ * `lookupPopulate`) keep their inline try/catch and call
1056
+ * `_composeMiddleware` directly instead of being forced through this
1057
+ * single-result helper.
942
1058
  */
943
1059
  private _runOp;
944
1060
  /**
@@ -977,6 +1093,40 @@ declare class Repository<TDoc = unknown> extends RepositoryBase {
977
1093
  findAll(filters?: Record<string, unknown>, options?: OperationOptions & {
978
1094
  sort?: SortSpec | string;
979
1095
  }): Promise<TDoc[]>;
1096
+ /**
1097
+ * Streaming reads — async iterator over data, suitable for migrations,
1098
+ * backfills, schema audits, and any once-a-quarter "touch every row"
1099
+ * job. Goes through the standard `before:cursor` hook pipeline so
1100
+ * multi-tenant scope, soft-delete filter, and access-control plugins
1101
+ * inject scope BEFORE the underlying mongoose cursor is built.
1102
+ *
1103
+ * Replaces direct `Model.find().cursor()` usage which bypasses every
1104
+ * plugin — that's how cross-tenant data ends up in migrations.
1105
+ *
1106
+ * Returns an `AsyncIterableIterator<TDoc>` — drive it with `for await`.
1107
+ * The mongoose cursor is closed when iteration completes or breaks.
1108
+ *
1109
+ * @example Backfill a missing field across the whole tenant scope
1110
+ * ```ts
1111
+ * for await (const doc of repo.cursor({ filter: { migrated: { $ne: true }}}, { batchSize: 1000 })) {
1112
+ * await transformer(doc);
1113
+ * await repo.update(doc._id, { migrated: true });
1114
+ * }
1115
+ * ```
1116
+ *
1117
+ * @example Audit unscoped — opt out of multi-tenant for global sweeps
1118
+ * ```ts
1119
+ * for await (const doc of repo.cursor({}, { batchSize: 500, organizationId: undefined })) {
1120
+ * audit(doc);
1121
+ * }
1122
+ * ```
1123
+ */
1124
+ cursor(filter?: Record<string, unknown>, options?: ReadOptions & {
1125
+ sort?: SortSpec | string;
1126
+ batchSize?: number;
1127
+ select?: SelectSpec;
1128
+ lean?: boolean;
1129
+ }): AsyncIterableIterator<TDoc>;
980
1130
  /**
981
1131
  * Unified pagination - auto-detects offset vs keyset based on params
982
1132
  *
@@ -1023,10 +1173,22 @@ declare class Repository<TDoc = unknown> extends RepositoryBase {
1023
1173
  noPagination?: boolean;
1024
1174
  }, options?: CacheableOptions): Promise<OffsetPaginationResult<TDoc> | KeysetPaginationResult<TDoc> | TDoc[]>;
1025
1175
  /**
1026
- * Get or create document
1027
- * Routes through hook system for policy enforcement (multi-tenant, soft-delete)
1176
+ * Atomic "look up by filter, insert `data` if missing, return the doc."
1177
+ *
1178
+ * Returns `{ doc, created }` so callers can tell whether *this* call
1179
+ * inserted (race-win) or matched an existing row. Implements
1180
+ * `StandardRepo.getOrCreate()` from `@classytic/repo-core/repository`
1181
+ * — see that interface's JSDoc for the cross-kit contract.
1182
+ *
1183
+ * Routes through the hook system for policy enforcement (multi-tenant,
1184
+ * soft-delete, audit). The hook context carries the resolved query +
1185
+ * data; the action layer adds the `created` discriminator from the
1186
+ * driver's `lastErrorObject.upserted`.
1028
1187
  */
1029
- getOrCreate(query: Record<string, unknown>, createData: Record<string, unknown>, options?: SessionOptions): Promise<TDoc | null>;
1188
+ getOrCreate(query: Record<string, unknown>, createData: Record<string, unknown>, options?: SessionOptions): Promise<{
1189
+ doc: TDoc;
1190
+ created: boolean;
1191
+ }>;
1030
1192
  /**
1031
1193
  * Count documents
1032
1194
  * Routes through hook system for policy enforcement (multi-tenant, soft-delete)
@@ -1072,7 +1234,267 @@ declare class Repository<TDoc = unknown> extends RepositoryBase {
1072
1234
  * );
1073
1235
  * ```
1074
1236
  */
1075
- findOneAndUpdate<TResult = TDoc>(filter: Record<string, unknown>, update: UpdateInput$1, options?: FindOneAndUpdateOptions): Promise<TResult | null>;
1237
+ findOneAndUpdate<TResult = TDoc>(filter: Record<string, unknown>, update: UpdateInput, options?: FindOneAndUpdateOptions): Promise<TResult | null>;
1238
+ /**
1239
+ * Atomic compare-and-swap state transition. Implements
1240
+ * `StandardRepo.claim()` from `@classytic/repo-core/repository` (0.4+).
1241
+ *
1242
+ * Builds a `findOneAndUpdate({ _id, [field]: from, ...where }, { $set:
1243
+ * { [field]: to, ...patch } })` in a single round-trip — race-free
1244
+ * across concurrent callers. Returns the post-update doc on success,
1245
+ * `null` when:
1246
+ * - the row exists but its current state isn't `from` (someone
1247
+ * else won the transition), OR
1248
+ * - any predicate in `transition.where` fails (paused guard,
1249
+ * retry-time guard, heartbeat-staleness, sub-document predicate),
1250
+ * OR
1251
+ * - no row matches the id at all.
1252
+ *
1253
+ * The caller can't distinguish "lost race" from "guard predicate
1254
+ * failed" — both mean "don't proceed." That's by design: the only
1255
+ * action either way is to back off.
1256
+ *
1257
+ * **Source-state — `from: T | T[]`.** Pass a literal for a single-
1258
+ * source transition (`from: 'pending'`) or an array for the
1259
+ * "transition from any of these states" pattern (`from: ['pending',
1260
+ * 'approved']`). Compiles to `[stateField]: { $in: [...] }`. Real-
1261
+ * world frequency: commission's `voidRecord` / `markClawedBack` /
1262
+ * `endAgreement` / `_transition`, media-kit's error path,
1263
+ * streamline's `cancelAt` — every state machine with more than one
1264
+ * non-terminal source state hits this. Without array support those
1265
+ * sites fall back to raw `findOneAndUpdate` and lose plugin routing.
1266
+ *
1267
+ * **`from` is value-only — no Mongo expressions.** `from` accepts a
1268
+ * literal or an array of literals (`$in`-shaped). It does NOT
1269
+ * accept `{ $ne: ... }`, `{ $lt: ... }`, `{ $exists: ... }`, or
1270
+ * arbitrary expression objects. The CAS contract is "match if the
1271
+ * state field equals one of these specific values" — admitting
1272
+ * arbitrary predicates would collapse the contract into a generic
1273
+ * filter, defeating the point of a state-machine primitive.
1274
+ *
1275
+ * For non-equality / range / existence predicates on the state
1276
+ * field, use `where`:
1277
+ *
1278
+ * ```ts
1279
+ * // ❌ from: { $ne: 'ended' } — not supported, would silently work-as-filter
1280
+ * // ✅ Use `where` for $ne / $lt / $exists / arbitrary mongo predicates:
1281
+ * await repo.claim(id, {
1282
+ * from: 'active', // exact-match state
1283
+ * to: 'closed',
1284
+ * where: { closedAt: { $exists: false } }, // additional predicates
1285
+ * });
1286
+ * ```
1287
+ *
1288
+ * **Dotted-path `field` is supported** — `field: 'scheduling.status'`
1289
+ * for nested state fields works. Mongo's filter and `$set` both
1290
+ * honor dotted paths, and the from===to optimization handles them
1291
+ * correctly (literal comparison on the path string). Use for
1292
+ * domain models that nest the state under a sub-document
1293
+ * (`lpn.state`, `package.condition.state`, `scheduling.status`).
1294
+ *
1295
+ * **`from === to` is allowed — idempotent re-claim.** Yard's
1296
+ * `reviseDeparture` writes `departed → departed` to update the
1297
+ * row's payload while asserting the row hasn't moved on. The CAS
1298
+ * still returns `null` on race-loss (row left the source state),
1299
+ * so the safety property holds. Use the same semantic as a "touch
1300
+ * with state assertion" primitive.
1301
+ *
1302
+ * When `from === to` (and `from` is a literal, not an array), the
1303
+ * implementation **skips the redundant `$set: { [stateField]: to }`
1304
+ * write** — the filter has already pinned the state field to that
1305
+ * exact value, so writing it again is a no-op disk operation.
1306
+ * Workloads doing high-replay dedup (yard's `gate-event.append`,
1307
+ * outbox replay storms, idempotent first-write CAS) save one disk
1308
+ * write + one journal flush + one replication-log entry per replay.
1309
+ * Three observable behaviours follow:
1310
+ *
1311
+ * - With a non-empty patch: only the patch fields land in `$set`;
1312
+ * the redundant state-field key is dropped.
1313
+ * - With an empty patch and `upsert: false`: the call lowers to
1314
+ * a plain `findOne` round-trip — pure assertion, zero writes.
1315
+ * Plugin pipeline (before/after `claim` hooks) still fires
1316
+ * correctly — only the internal driver shape changes.
1317
+ * - With an empty patch and `upsert: true`: the update becomes
1318
+ * `{ $setOnInsert: { [stateField]: to } }` — no-op on match,
1319
+ * populates the inserted row's state field on miss. This is
1320
+ * the "pure dedup" insert-or-confirm path.
1321
+ *
1322
+ * For pure-dedup recipes (no real state column, just an external
1323
+ * id used as both filter and `from`/`to`), see CLAUDE.md — yard's
1324
+ * `gate-event.append` is the canonical worked example.
1325
+ *
1326
+ * **`upsert: true` — insert-or-claim.** Set `options.upsert: true`
1327
+ * for the upsert-claim pattern: insert when the row doesn't exist,
1328
+ * else CAS-transition the existing row. Yard's `gate-event.append`
1329
+ * uses this for idempotent event landing. With `upsert` on, claim
1330
+ * NEVER returns null on miss — it inserts. Pair with
1331
+ * `$setOnInsert` in the operator patch for insert-only fields.
1332
+ * The default (`upsert: false`) keeps the canonical "match
1333
+ * exactly, else null" semantic.
1334
+ *
1335
+ * **Patch shape** — accepts BOTH flat (`{ field: value }`) and Mongo
1336
+ * operator (`{ $set, $inc, $unset, $setOnInsert, ... }`) forms.
1337
+ * Operator form is load-bearing for versioned docs (`$inc: {
1338
+ * version: 1 }`) and upsert-claim (`$setOnInsert: { createdBy }`);
1339
+ * flat form is the textbook ergonomic case. Mixing flat keys with
1340
+ * `$`-keys throws — Mongo would silently drop the flat keys,
1341
+ * which would mask data-loss bugs.
1342
+ *
1343
+ * Multi-tenant scope, soft-delete filter, cache invalidation, and
1344
+ * audit hooks all flow through automatically — `claim` is registered
1345
+ * in `OP_REGISTRY` (policyKey: 'query', mutates: true), so plugins
1346
+ * that iterate the registry pick it up without changes.
1347
+ *
1348
+ * **Caller responsibility for tenant context.** When
1349
+ * `multiTenantPlugin` is mounted, plugin-injected scoping reads the
1350
+ * tenant key from the **options bag** — `claim(id, transition, patch,
1351
+ * { organizationId: '...' })`. Without it, the plugin throws
1352
+ * `'Missing organizationId'` at runtime. Use
1353
+ * `repoOptionsFromCtx(ctx)` (or your own context helper) to forward
1354
+ * request-scoped tenant fields; do not assume the plugin reads them
1355
+ * from a global.
1356
+ *
1357
+ * @example Textbook transition (flat patch)
1358
+ * ```ts
1359
+ * const claimed = await runRepo.claim(runId, { from: 'waiting', to: 'running' }, {
1360
+ * lastHeartbeat: new Date(),
1361
+ * workerId: 'worker-12',
1362
+ * });
1363
+ * if (!claimed) return; // someone else got it (or no match)
1364
+ * ```
1365
+ *
1366
+ * @example Versioned doc — operator patch with `$inc`
1367
+ * ```ts
1368
+ * const claimed = await orderRepo.claim(
1369
+ * orderId,
1370
+ * { from: 'pending', to: 'shipped' },
1371
+ * {
1372
+ * $set: { shippedAt: new Date() },
1373
+ * $inc: { version: 1 },
1374
+ * },
1375
+ * );
1376
+ * ```
1377
+ *
1378
+ * @example Compound-filter claim (paused guard + retry timer)
1379
+ * ```ts
1380
+ * const claimed = await runRepo.claim(
1381
+ * runId,
1382
+ * {
1383
+ * from: 'waiting',
1384
+ * to: 'running',
1385
+ * where: {
1386
+ * paused: { $ne: true },
1387
+ * 'scheduling.retryAfter': { $lte: new Date() },
1388
+ * },
1389
+ * },
1390
+ * { lastHeartbeat: new Date() },
1391
+ * { organizationId: ctx.orgId }, // tenant fwd — see note above
1392
+ * );
1393
+ * ```
1394
+ *
1395
+ * Pairs with `defineStateMachine()` from
1396
+ * `@classytic/primitives/state-machine`:
1397
+ * - State machine validates "is from→to legal in the model?"
1398
+ * - `claim()` performs the atomic "did we win?"
1399
+ */
1400
+ claim(id: string | ObjectId, transition: {
1401
+ field?: string;
1402
+ from: unknown | readonly unknown[];
1403
+ to: unknown;
1404
+ where?: Record<string, unknown>;
1405
+ }, patch?: Record<string, unknown>, options?: SessionOptions & {
1406
+ idField?: string;
1407
+ upsert?: boolean;
1408
+ }): Promise<TDoc | null>;
1409
+ /**
1410
+ * Optimistic-concurrency CAS via a version stamp.
1411
+ *
1412
+ * Sibling to `claim()` — distinct mental model:
1413
+ * - `claim()` is a state machine: "move from status A to status B, atomically"
1414
+ * - `claimVersion()` is optimistic locking: "I expect version N; if it
1415
+ * still is, apply this update and increment the version"
1416
+ *
1417
+ * Builds `findOneAndUpdate({ _id, [versionField]: from, ...where },
1418
+ * { ...update, $inc: { [versionField]: by ?? 1 } })` in one
1419
+ * round-trip. Returns the post-update doc on success, `null` when:
1420
+ * - the row doesn't exist, OR
1421
+ * - the row's version isn't `from` (someone else committed first —
1422
+ * standard race-loss signal), OR
1423
+ * - any predicate in `transition.where` fails (state guard, tenant
1424
+ * guard expressed inline, etc.)
1425
+ *
1426
+ * **`from: undefined` is tolerated.** Lean reads return `version:
1427
+ * number | undefined` because the field defaults are absent on
1428
+ * fresh-from-mongo POJOs. Passing `from: undefined` matches docs
1429
+ * whose version field is *missing entirely* — exactly what you want
1430
+ * on a first-write CAS. Callers no longer need `?? 0` at every site.
1431
+ *
1432
+ * The caller's `update` is freeform — pass either a Mongo operator
1433
+ * shape (`{ $set: { status: 'submitted', updatedAt: now } }`) or a
1434
+ * field-shape object (auto-wrapped in `$set`). The version `$inc` is
1435
+ * MERGED into the update, so callers don't have to remember to bump
1436
+ * the counter.
1437
+ *
1438
+ * **Compound CAS via `where`.** State machines that key on both
1439
+ * version AND status (yard's `transition()`, every package whose
1440
+ * "ready to commit" gate is more than a version stamp) need to
1441
+ * AND-merge additional predicates. Pass them via
1442
+ * `transition.where` — same shape and semantics as `claim`'s
1443
+ * `where` field. Without this, claimVersion was forced back to raw
1444
+ * `findOneAndUpdate` for every state-AND-version site.
1445
+ *
1446
+ * Multi-tenant scope, soft-delete filter, cache invalidation, and
1447
+ * audit hooks all fire — `claimVersion` is in `OP_REGISTRY`
1448
+ * (policyKey: 'query', mutates: true) so plugins iterate the registry
1449
+ * and pick it up automatically.
1450
+ *
1451
+ * **Caller responsibility for tenant context.** When
1452
+ * `multiTenantPlugin` is mounted, the plugin reads the tenant key
1453
+ * from the **options bag** (`{ organizationId: '...' }`). Without it,
1454
+ * the plugin throws `'Missing organizationId'` at runtime. Use
1455
+ * `repoOptionsFromCtx(ctx)` to forward — same as `claim()` and every
1456
+ * other repo op.
1457
+ *
1458
+ * @example Order submission with version check
1459
+ * ```ts
1460
+ * const submitted = await orderRepo.claimVersion(
1461
+ * orderId,
1462
+ * { from: order.version },
1463
+ * { $set: { status: 'submitted', submittedAt: new Date() } },
1464
+ * );
1465
+ * if (!submitted) throw new ConcurrentEditError();
1466
+ * ```
1467
+ *
1468
+ * @example Compound CAS — version + status
1469
+ * ```ts
1470
+ * const transitioned = await yardRepo.claimVersion(
1471
+ * loadId,
1472
+ * {
1473
+ * from: load.version, // number | undefined OK
1474
+ * where: { status: 'queued' },
1475
+ * },
1476
+ * { $set: { status: 'in-progress', startedAt: new Date() } },
1477
+ * );
1478
+ * if (!transitioned) throw new ConcurrentEditError();
1479
+ * ```
1480
+ *
1481
+ * @example Custom version field name + step
1482
+ * ```ts
1483
+ * await runRepo.claimVersion(
1484
+ * runId,
1485
+ * { field: 'rev', from: 12, by: 1 },
1486
+ * { lastHeartbeat: new Date() }, // field-shape auto-wraps in $set
1487
+ * );
1488
+ * ```
1489
+ */
1490
+ claimVersion(id: string | ObjectId, transition: {
1491
+ field?: string;
1492
+ from: number | undefined;
1493
+ by?: number;
1494
+ where?: Record<string, unknown>;
1495
+ }, update: Record<string, unknown>, options?: SessionOptions & {
1496
+ idField?: string;
1497
+ }): Promise<TDoc | null>;
1076
1498
  /**
1077
1499
  * Delete a document by ID.
1078
1500
  *
@@ -1096,9 +1518,9 @@ declare class Repository<TDoc = unknown> extends RepositoryBase {
1096
1518
  */
1097
1519
  delete(id: string | ObjectId, options?: SessionOptions & {
1098
1520
  idField?: string;
1099
- mode?: 'hard' | 'soft'; /** Legacy opt-in: throw a 404 error on miss instead of returning `{ success: false }`. */
1521
+ mode?: 'hard' | 'soft'; /** Legacy opt-in: throw a 404 error on miss instead of returning `null`. */
1100
1522
  throwOnNotFound?: boolean;
1101
- }): Promise<DeleteResult>;
1523
+ }): Promise<DeleteResult | null>;
1102
1524
  /**
1103
1525
  * Update every document matching the filter.
1104
1526
  *
@@ -1118,7 +1540,7 @@ declare class Repository<TDoc = unknown> extends RepositoryBase {
1118
1540
  * Refuses empty filters (defense-in-depth against mass-update accidents)
1119
1541
  * both before and after policy hooks inject tenant scope.
1120
1542
  */
1121
- updateMany(filter: Record<string, unknown>, update: UpdateInput$1, options?: SessionOptions & {
1543
+ updateMany(filter: Record<string, unknown>, update: UpdateInput, options?: SessionOptions & {
1122
1544
  updatePipeline?: boolean;
1123
1545
  [key: string]: unknown;
1124
1546
  }): Promise<UpdateManyResult>;
@@ -1196,26 +1618,60 @@ declare class Repository<TDoc = unknown> extends RepositoryBase {
1196
1618
  */
1197
1619
  aggregate<TRow extends Record<string, unknown> = Record<string, unknown>>(req: AggRequest): Promise<AggResult<TRow>>;
1198
1620
  /**
1199
- * Offset-paginated portable aggregation. Same IR as `aggregate`,
1200
- * wrapped in the standard `OffsetPaginationResultCore` envelope so
1201
- * UI code paginates aggregated dashboards with the same primitives
1202
- * as raw document lists.
1621
+ * Invalidate every aggregate-cache entry tagged with ANY of the
1622
+ * given tags. Delegates to the unified cache plugin's handle
1623
+ * (`repo.cache.invalidateByTags`) — wired by `cachePlugin({ adapter })`.
1624
+ *
1625
+ * Pass no tags to wipe the entire cache namespace (requires the
1626
+ * adapter to implement `clear`).
1627
+ *
1628
+ * Returns the count of distinct entries cleared (`-1` when the call
1629
+ * routed through `adapter.clear()` and the adapter doesn't report a
1630
+ * count). No-op (returns `0`) when no cache plugin is wired.
1631
+ */
1632
+ invalidateAggregateCache(tags?: readonly string[]): Promise<number>;
1633
+ /**
1634
+ * Paginated portable aggregation. Two pagination modes, picked by
1635
+ * the request shape:
1636
+ *
1637
+ * - **Offset (default)** — `page` + `limit`. Returns the standard
1638
+ * `{ method: 'offset', data, total, pages, hasNext, hasPrev, ... }`
1639
+ * envelope. `countStrategy: 'none'` skips the second round-trip
1640
+ * that computes `total`; the envelope reports `total: 0`,
1641
+ * `pages: 0`, and derives `hasNext` from a `LIMIT N+1` peek on
1642
+ * the data pipeline.
1643
+ * - **Keyset** — `pagination: 'keyset'` (or `after` set). Returns
1644
+ * `{ method: 'keyset', data, hasMore, next, limit }`. `sort` is
1645
+ * required — the cursor encodes the sort-key tuple of the last
1646
+ * row. Each subsequent page passes the previous `next` back as
1647
+ * `after`. Scales to arbitrary group counts because the planner
1648
+ * uses `(sort_keys) > (cursor)` instead of `OFFSET N`.
1649
+ */
1650
+ aggregatePaginate<TRow extends Record<string, unknown> = Record<string, unknown>>(req: AggPaginationRequest): Promise<OffsetPaginationResultCore<TRow> | KeysetAggPaginationResult<TRow>>;
1651
+ /**
1652
+ * Internal: the actual aggregate-paginate execution body. Extracted
1653
+ * so `aggregatePaginate` can wrap it in the cache layer without
1654
+ * duplicating the keyset / offset branch logic.
1203
1655
  *
1204
- * `countStrategy: 'none'` skips the second round-trip that computes
1205
- * `total`; the envelope reports `total: 0`, `pages: 0`, and derives
1206
- * `hasNext` from a `LIMIT N+1` peek on the data pipeline.
1656
+ * Receives `finalReq` (already policy-scoped) so the cache key
1657
+ * built by the outer wrapper matches the request the executor
1658
+ * sees — no cross-tenant cache poisoning.
1207
1659
  */
1208
- aggregatePaginate<TRow extends Record<string, unknown> = Record<string, unknown>>(req: AggPaginationRequest): Promise<OffsetPaginationResultCore<TRow>>;
1660
+ private _executeAggregatePaginate;
1209
1661
  /**
1210
1662
  * Merge policy-hook-injected filters (multi-tenant scope,
1211
1663
  * soft-delete) into the pre-aggregate `filter` slot of the request.
1212
1664
  * Plugins speak mongo-query shape (`context.query` / `context.filters`);
1213
- * we merge those with the portable `req.filter` using `$and`.
1665
+ * the caller's `req.filter` may be either Filter IR or a mongo query.
1214
1666
  *
1215
- * When the caller's filter is a Filter IR node we don't unwrap it —
1216
- * the pipeline compiler does Filter→mongo translation. Merging here
1217
- * as a mongo-shaped `$and` is safe because `compileFilterToMongo`
1218
- * passes through already-mongo queries unchanged.
1667
+ * Each part is compiled through `compileFilterToMongo` BEFORE the
1668
+ * `$and` merge — otherwise an IR node like `eq('status', 'paid')`
1669
+ * (`{ op, field, value }`) gets nested inside `$and` as a literal,
1670
+ * `compileFilterToMongo` of the merged value finds no top-level `op`
1671
+ * (so `isFilter` is false), and Mongo receives the IR object as a
1672
+ * raw query clause that matches no real document. Symptom: aggregates
1673
+ * silently return 0 rows whenever a callerpasses IR + a policy
1674
+ * plugin is installed.
1219
1675
  */
1220
1676
  private _injectPolicyScopeIntoAgg;
1221
1677
  /**
@@ -1449,16 +1905,202 @@ interface SessionStarter {
1449
1905
  * ```
1450
1906
  */
1451
1907
  declare function withTransaction<T>(connection: SessionStarter, callback: (session: ClientSession) => Promise<T>, options?: WithTransactionOptions): Promise<T>;
1908
+ /**
1909
+ * Multi-repo transactional batch — every repo in `repos` becomes a
1910
+ * session-bound proxy inside the callback. Eliminates the per-call
1911
+ * `{ session }` threading that was repeated across ~20 call sites
1912
+ * (be-prod outbox writes, order placement, transfer source/dest pairs,
1913
+ * payrun saga steps).
1914
+ *
1915
+ * Same retry + fallback semantics as `withTransaction`: the
1916
+ * `transactionOptions` / `allowFallback` / `onFallback` knobs apply.
1917
+ *
1918
+ * Each property of the input `repos` map is rebound via the
1919
+ * `createTxBoundRepo` proxy — every CRUD method on the bound repo
1920
+ * auto-injects `session` into its options bag, including
1921
+ * `claim` / `claimVersion` / `findOneAndUpdate` and plugin-contributed
1922
+ * methods. Non-CRUD properties (Model, modelName, hook engine,
1923
+ * idField) pass through to the underlying repo. Nested
1924
+ * `boundRepo.withTransaction(...)` throws — reuse the outer bound
1925
+ * repos.
1926
+ *
1927
+ * @example Order placement across three repos in one transaction
1928
+ * ```ts
1929
+ * import { batchTransaction } from '@classytic/mongokit';
1930
+ *
1931
+ * const order = await batchTransaction(
1932
+ * mongoose.connection,
1933
+ * { orders: orderRepo, events: eventRepo, inventory: inventoryRepo },
1934
+ * async ({ orders, events, inventory }) => {
1935
+ * const created = await orders.create(orderData); // session auto-injected
1936
+ * await events.create({ type: 'order.placed', orderId: created._id });
1937
+ * await inventory.claim(skuId, { from: 'available', to: 'reserved' });
1938
+ * return created;
1939
+ * },
1940
+ * );
1941
+ * ```
1942
+ *
1943
+ * @example With fallback for standalone-mongo dev environments
1944
+ * ```ts
1945
+ * await batchTransaction(
1946
+ * mongoose.connection,
1947
+ * { orders, events },
1948
+ * async ({ orders, events }) => { ... },
1949
+ * { allowFallback: true, onFallback: (err) => log.warn(err) },
1950
+ * );
1951
+ * ```
1952
+ *
1953
+ * @param connection - Mongoose connection (or anything with
1954
+ * `startSession()`). Single source of session truth — every bound
1955
+ * repo shares this session.
1956
+ * @param repos - Map of repo instances to rebind. Keys become the
1957
+ * property names on the callback's argument.
1958
+ * @param callback - Receives the bound repo map. Return value flows
1959
+ * through to the outer `Promise`.
1960
+ */
1961
+ declare function batchTransaction<TRepos extends Record<string, object>, TResult>(connection: SessionStarter, repos: TRepos, callback: (bound: TRepos) => Promise<TResult>, options?: WithTransactionOptions): Promise<TResult>;
1452
1962
  /**
1453
1963
  * Detect whether an error indicates the MongoDB deployment does not support
1454
1964
  * multi-document transactions (standalone server, older topology, etc.).
1455
1965
  *
1456
1966
  * Checks MongoDB error codes first — 263 (standalone) and 20 (unsupported
1457
1967
  * topology) — with a message-matching fallback for edge cases surfaced by
1458
- * older driver versions.
1968
+ * driver versions that throw before the proper code lands.
1969
+ *
1970
+ * **Driver-version drift:** modern mongoose / mongodb-driver versions hit
1971
+ * the standalone case via the retryable-writes precondition rather than
1972
+ * the transaction precondition (`retryWrites=true` is on by default,
1973
+ * standalone Mongo rejects it before the transaction is even attempted).
1974
+ * So we accept that message as equivalent — the underlying topology
1975
+ * problem is the same, and the fallback semantics are correct either way.
1459
1976
  */
1460
1977
  declare function isTransactionUnsupported(error: Error): boolean;
1461
1978
  //#endregion
1979
+ //#region src/utils/repo-options.d.ts
1980
+ /**
1981
+ * Forward request-scoped context fields into a mongokit options bag.
1982
+ *
1983
+ * Multi-tenant scoping, audit attribution, and transaction threading
1984
+ * all read their inputs from the **options** argument of every repo
1985
+ * method — `multiTenantPlugin` looks for `organizationId`, the audit
1986
+ * plugins read `userId` / `user`, and `withTransaction` honors
1987
+ * `session`. Consumers were repeatedly hand-rolling:
1988
+ *
1989
+ * ```ts
1990
+ * await repo.update(id, data, {
1991
+ * organizationId: ctx.organizationId,
1992
+ * userId: ctx.userId,
1993
+ * session: ctx.session,
1994
+ * });
1995
+ * ```
1996
+ *
1997
+ * — sometimes correctly forwarding all three, sometimes only one,
1998
+ * sometimes constructing `Types.ObjectId(orgId)` manually because the
1999
+ * caller didn't trust `multiTenantPlugin` to handle the cast (it does).
2000
+ * The result was inconsistent forwarding across repos, with several
2001
+ * silent tenant-leak shapes traced back to a forgotten field.
2002
+ *
2003
+ * `repoOptionsFromCtx(ctx)` is the canonical extractor. Pass it the
2004
+ * request context (whatever shape your framework hands you) and it
2005
+ * returns the subset of fields mongokit's plugins read. Unknown ctx
2006
+ * keys are NOT forwarded — the options bag stays narrow and explicit.
2007
+ *
2008
+ * @example
2009
+ * ```ts
2010
+ * import { repoOptionsFromCtx } from '@classytic/mongokit';
2011
+ *
2012
+ * await orderRepo.update(id, data, repoOptionsFromCtx(ctx));
2013
+ * await orderRepo.claim(id, { from: 'pending', to: 'shipped' }, {}, repoOptionsFromCtx(ctx));
2014
+ * ```
2015
+ *
2016
+ * @example Spreading + adding kit-specific options
2017
+ * ```ts
2018
+ * await orderRepo.findOneAndUpdate(filter, update, {
2019
+ * ...repoOptionsFromCtx(ctx),
2020
+ * returnDocument: 'after',
2021
+ * upsert: true,
2022
+ * });
2023
+ * ```
2024
+ *
2025
+ * The fields read (all optional — extracted only when present and not
2026
+ * `undefined`):
2027
+ *
2028
+ * - `organizationId` — multi-tenant scope key. The
2029
+ * `multiTenantPlugin` casts to `ObjectId` automatically when
2030
+ * configured with `fieldType: 'objectId'`; do NOT pre-cast.
2031
+ * - `userId` / `user` — audit attribution (audit-log + audit-trail
2032
+ * plugins read these for the `who` column).
2033
+ * - `session` — mongoose `ClientSession` for transaction threading.
2034
+ * - `requestId` — observability correlation id.
2035
+ *
2036
+ * Fields beyond this set should be added inline at the call site —
2037
+ * `repoOptionsFromCtx` deliberately stays narrow so adding a new
2038
+ * convention to the canonical set is a deliberate API decision, not
2039
+ * a quiet drift.
2040
+ */
2041
+ declare function repoOptionsFromCtx<TCtx extends Record<string, unknown>>(ctx: TCtx | undefined | null): Record<string, unknown>;
2042
+ /**
2043
+ * Build a typed extractor for **consumer-defined** ctx → options
2044
+ * forwarding. Same conceptual shape as `repoOptionsFromCtx`, but the
2045
+ * caller declares the field set so domain-package fields beyond
2046
+ * mongokit's bundled-plugin keys land too.
2047
+ *
2048
+ * Across the classytic codebase, ~6 packages (commission, supplier-
2049
+ * performance, pos, yard, order, plus implicit in leave/payrun/
2050
+ * muster/people) ship a hand-rolled 15-line `repo-options.ts` doing
2051
+ * the same shape — drift is inevitable, "I forgot to forward
2052
+ * `correlationId`" is a recurring bug class. This builder eliminates
2053
+ * the boilerplate and pins the field list at the type level so
2054
+ * adding a new canonical field is a single-line edit.
2055
+ *
2056
+ * **Type-safe field declaration.** The `fields` array is constrained
2057
+ * to `keyof TCtx & string`, so a typo in a field name is a compile
2058
+ * error rather than a silent absent-key. The returned extractor's
2059
+ * output keys are inferred from the input field tuple — you can
2060
+ * destructure with full autocomplete.
2061
+ *
2062
+ * **Difference from `repoOptionsFromCtx`.** That helper hardcodes the
2063
+ * fields **mongokit's bundled plugins** read (`organizationId`,
2064
+ * `userId`, `user`, `session`, `requestId`). Use it when those are
2065
+ * sufficient. Reach for `createOptionsExtractor` when your domain has
2066
+ * its own canonical fields (`actorRef`, `actorKind`, `correlationId`,
2067
+ * `idempotencyKey`, `sagaRunId`, ...) that should also forward.
2068
+ *
2069
+ * @example Per-package canonical extractor
2070
+ * ```ts
2071
+ * import { createOptionsExtractor } from '@classytic/mongokit';
2072
+ *
2073
+ * type CommissionCtx = {
2074
+ * organizationId: string;
2075
+ * actorRef: string;
2076
+ * actorKind: 'user' | 'system';
2077
+ * correlationId: string;
2078
+ * session?: ClientSession;
2079
+ * idempotencyKey?: string;
2080
+ * };
2081
+ *
2082
+ * export const repoOptionsFromCtx = createOptionsExtractor<CommissionCtx>([
2083
+ * 'organizationId',
2084
+ * 'actorRef',
2085
+ * 'actorKind',
2086
+ * 'correlationId',
2087
+ * 'session',
2088
+ * 'idempotencyKey',
2089
+ * ]);
2090
+ *
2091
+ * // Use:
2092
+ * await commissionRepo.update(id, data, repoOptionsFromCtx(ctx));
2093
+ * ```
2094
+ *
2095
+ * @param fields - The canonical option keys to extract from ctx.
2096
+ * Must be `(keyof TCtx & string)[]` — TypeScript prevents typos at
2097
+ * the declaration site.
2098
+ * @returns An extractor function `(ctx: TCtx | null | undefined) → Record<string, unknown>`
2099
+ * that omits absent/undefined keys (so spreading into a parent
2100
+ * options bag never erases inherited values).
2101
+ */
2102
+ declare function createOptionsExtractor<TCtx extends Record<string, unknown>>(fields: readonly (keyof TCtx & string)[]): (ctx: TCtx | undefined | null) => Record<string, unknown>;
2103
+ //#endregion
1462
2104
  //#region src/index.d.ts
1463
2105
  /**
1464
2106
  * Factory function to create a repository instance
@@ -1472,4 +2114,4 @@ declare function isTransactionUnsupported(error: Error): boolean;
1472
2114
  */
1473
2115
  declare function createRepository<TDoc>(Model: _$mongoose.Model<TDoc, any, any, any>, plugins?: PluginType[], paginationConfig?: PaginationConfig, options?: RepositoryOptions): Repository<TDoc>;
1474
2116
  //#endregion
1475
- export { ALL_OPERATIONS, type AggregateHelpersMethods, type AggregateOptions, type AggregatePaginationOptions, type AggregatePaginationResult, AggregationBuilder, type AllPluginMethods, type AnyDocument, type AnyModel, type AuditEntry, type AuditOperation, type AuditQueryOptions, type AuditQueryResult, type AuditTrailMethods, type AuditTrailOptions, AuditTrailQuery, type BatchOperationsMethods, type BulkWriteResult, type CacheAdapter, type CacheMethods, type CacheOperationOptions, type CacheOptions, type CacheStats, type CacheableOptions, type CascadeOptions, type CascadeRelation, type CollationOptions, type CreateInput, type CreateOptions, type CustomIdOptions, type DateSequentialIdOptions, type DecodedCursor, type DeepPartial, type DeleteResult, type DocField, type ElasticSearchOptions, type EventHandlers, type EventPayload, type EventPhase, type FieldPreset, type FieldType, type FilterQuery, type FindOneAndUpdateOptions, type GroupResult, HOOK_PRIORITY, type HookMode, type HttpError, type IController, type IControllerResponse, type IRequestContext, type IResponseFormatter, type IdGenerator, type InferDocument, type InferRawDoc, type KeysOfType, type KeysetPaginationOptions, type KeysetPaginationResult, type Logger, LookupBuilder, type LookupOptions, type LookupPopulateOptions, type LookupPopulateResult, MUTATING_OPERATIONS, type MinMaxResult, type MongoOperationsMethods, type MultiTenantOptions, type NonNullableFields, OP_REGISTRY, type ObjectId, type ObservabilityOptions, type OffsetPaginationOptions, type OffsetPaginationResult, type OperationDescriptor, type OperationMetric, type OperationOptions, type PaginationConfig, PaginationEngine, type PaginationResult, type ParsedQuery, type PartialBy, type Plugin, type PluginFunction, type PluginType, type PolicyKey, type PopulateOption, type PopulateSpec, type PrefixedIdOptions, QueryParser, type QueryParserOptions, READ_OPERATIONS, type ReadOptions, type ReadPreferenceType, Repository, Repository as default, type RepositoryContext, type RepositoryEvent, type RepositoryInstance, type RepositoryOperation, type RepositoryOptions, type RequiredBy, type SchemaLike, type SearchMode, type SelectSpec, type SequentialIdOptions, type SessionOptions, type SoftDeleteFilterMode, type SoftDeleteMethods, type SoftDeleteOptions, type SoftDeleteRepository, type SortDirection, type SortSpec, type Strict, type SubdocumentMethods, type TenantContext, type TenantStore, type UpdateInput, type UpdateManyResult, type UpdateOptions, type UpdatePatch, type UpdateWithValidationResult, type UserContext, type ValidationChainOptions, type ValidatorDefinition, type WithPlugins, type WithTransactionOptions, index_d_exports as actions, aggregateHelpersPlugin, auditLogPlugin, auditTrailPlugin, autoInject, batchOperationsPlugin, blockIf, buildCrudSchemasFromModel, buildCrudSchemasFromMongooseSchema, cachePlugin, cascadePlugin, configureLogger, createError, createFieldPreset, createMemoryCache, createRepository, createTenantContext, customIdPlugin, dateSequentialId, elasticSearchPlugin, fieldFilterPlugin, filterResponseData, getFieldsForUser, getMongooseProjection, getNextSequence, immutableField, isDuplicateKeyError, isTransactionUnsupported, methodRegistryPlugin, mongoOperationsPlugin, multiTenantPlugin, observabilityPlugin, operationsByPolicyKey, parseDuplicateKeyError, prefixedId, requireField, sequentialId, softDeletePlugin, subdocumentPlugin, timestampPlugin, uniqueField, validationChainPlugin, withTransaction };
2117
+ export { ALL_OPERATIONS, type AggregateHelpersMethods, type AggregateOptions, type AggregatePaginationOptions, AggregationBuilder, type AllPluginMethods, type AnyDocument, type AnyModel, type AuditEntry, type AuditOperation, type AuditQueryOptions, type AuditQueryResult, type AuditTrailMethods, type AuditTrailOptions, AuditTrailQuery, type BasePaginationOptions, type BatchOperationsMethods, type BulkWriteResult, type CacheAdapter, type CacheOperationOptions, type CacheOptions, type CacheableOptions, type CascadeOptions, type CascadeRelation, type CollationOptions, type CreateInput, type CreateOptions, type CursorPayload, type CustomIdOptions, type DateSequentialIdOptions, type DecodedCursor, type DeepPartial, type DeleteResult, type DocField, type ElasticSearchOptions, type EventHandlers, type EventPayload, type EventPhase, type FieldPreset, type FieldType, type FilterQuery, type FindOneAndUpdateOptions, type GroupResult, HOOK_PRIORITY, type HookMode, type IController, type IControllerResponse, type IRequestContext, type IResponseFormatter, type IdGenerator, type InferDocument, type InferRawDoc, type KeysOfType, type KeysetPaginationOptions, type LeaseMethods, type LeasePluginOptions, type Logger, LookupBuilder, type LookupOptions, type LookupPopulateOptions, type LookupPopulateResult, MUTATING_OPERATIONS, type Middleware, type MiddlewareContext, type MinMaxResult, type MinimalRepoView, type MongoOperationsMethods, type MongoOperatorUpdate, type MultiTenantOptions, type NonNullableFields, OP_REGISTRY, type ObjectId, type ObservabilityOptions, type OffsetPaginationOptions, type OperationDescriptor, type OperationMetric, type OperationOptions, type PaginationConfig, PaginationEngine, type ParsedQuery, type PartialBy, type Plugin, type PluginFunction, type PluginType, type PolicyKey, type PopulateOption, type PopulateSpec, type PrefixedIdOptions, type PrioritizedHook, QueryParser, type QueryParserOptions, READ_OPERATIONS, type ReadOptions, type ReadPreferenceType, Repository, Repository as default, type RepositoryCacheHandle, type RepositoryCachePluginOptions, type RepositoryContext, type RepositoryEvent, type RepositoryInstance, type RepositoryOperation, type RepositoryOptions, type RequiredBy, type SchemaLike, type SearchMode, type SelectSpec, type SequentialIdOptions, type SessionOptions, type SoftDeleteFilterMode, type SoftDeleteMethods, type SoftDeleteOptions, type SoftDeleteRepository, type SortDirection, type SortSpec, type Strict, type SubdocumentMethods, type TenantContext, type TenantStore, type UpdateManyResult, type UpdateOptions, type UpdatePatch, type UpdateWithValidationResult, type UserContext, type ValidationChainOptions, type ValidatorDefinition, type ValueType, type WithPlugins, type WithTransactionOptions, index_d_exports as actions, adminBypass, aggregateHelpersPlugin, auditLogPlugin, auditTrailPlugin, autoInject, batchOperationsPlugin, batchTransaction, blockIf, buildCrudSchemasFromModel, buildCrudSchemasFromMongooseSchema, cachePlugin, cascadePlugin, configureLogger, createError, createFieldPreset, createMemoryCache, createOptionsExtractor, createRepository, createTenantContext, customIdPlugin, dateSequentialId, elasticSearchPlugin, fieldFilterPlugin, filterResponseData, getFieldsForUser, getMongooseProjection, getNextSequence, immutableField, isDuplicateKeyError, isTransactionUnsupported, leasePlugin, methodRegistryPlugin, mongoOperationsPlugin, multiTenantPlugin, observabilityPlugin, operationsByPolicyKey, parseDuplicateKeyError, prefixedId, repoOptionsFromCtx, requireField, sequentialId, softDeletePlugin, subdocumentPlugin, timestampPlugin, uniqueField, validationChainPlugin, withTransaction };