@classytic/mongokit 3.12.0 → 3.13.1

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