@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.
- package/README.md +75 -5
- package/dist/{PaginationEngine-CvDFfVXT.mjs → PaginationEngine-CfQRM1yz.mjs} +16 -16
- package/dist/Repository-BY40uhcv.mjs +3603 -0
- package/dist/actions/index.d.mts +1 -1
- package/dist/actions/index.mjs +1 -1
- package/dist/adapter/index.d.mts +190 -0
- package/dist/adapter/index.mjs +2 -0
- package/dist/adapter-DTDMnUOH.mjs +158 -0
- package/dist/ai/index.d.mts +1 -1
- package/dist/ai/index.mjs +3 -3
- package/dist/better-auth/index.d.mts +149 -0
- package/dist/better-auth/index.mjs +81 -0
- package/dist/field-selection-Br3WPOEb.mjs +99 -0
- package/dist/{index-dNTmpgK8.d.mts → index-BbHaKwi5.d.mts} +33 -13
- package/dist/index.d.mts +678 -36
- package/dist/index.mjs +240 -17
- package/dist/{logger-DJ7egMnd.mjs → logger-DprijP5h.mjs} +1 -5
- package/dist/{mongooseToJsonSchema-D1sdCOmj.mjs → mongooseToJsonSchema-BYbG894y.mjs} +68 -20
- package/dist/{mongooseToJsonSchema-DXxjFkLB.d.mts → mongooseToJsonSchema-BgRAaXX-.d.mts} +10 -2
- package/dist/pagination/PaginationEngine.d.mts +3 -2
- package/dist/pagination/PaginationEngine.mjs +1 -1
- package/dist/plugins/index.d.mts +2 -2
- package/dist/plugins/index.mjs +2 -2
- package/dist/query/primitives/coercion.d.mts +1 -1
- package/dist/query/primitives/coercion.mjs +1 -1
- package/dist/query/primitives/indexes.d.mts +1 -1
- package/dist/{types-Dc2aDVUi.d.mts → types-BbaPZleN.d.mts} +419 -205
- package/dist/{update-DEJYcgfV.mjs → update-DLcSSa5b.mjs} +184 -45
- package/dist/utils/index.d.mts +2 -91
- package/dist/utils/index.mjs +5 -5
- package/dist/{validation-chain.plugin-B0VSdcwa.d.mts → validation-chain.plugin-CAPYCj_T.d.mts} +228 -98
- package/dist/validation-chain.plugin-_-WuNml2.mjs +2399 -0
- package/package.json +25 -5
- package/dist/field-selection-dI0m4RBs.mjs +0 -221
- package/dist/validation-chain.plugin-Dllv-cml.mjs +0 -5122
- /package/dist/{coercion-oM8G4b6m.d.mts → coercion-TRMmR49h.d.mts} +0 -0
- /package/dist/{id-resolution-e4BT9zAV.mjs → id-resolution-B0Odvvou.mjs} +0 -0
- /package/dist/{indexes-C26GNZRU.d.mts → indexes-CgOF7Wjd.d.mts} +0 -0
package/dist/index.d.mts
CHANGED
|
@@ -1,15 +1,16 @@
|
|
|
1
|
-
import { $ as
|
|
2
|
-
import { t as index_d_exports } from "./index-
|
|
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
|
|
5
|
-
import { n as SchemaPathsLike, t as FieldType$1 } from "./coercion-
|
|
6
|
-
import { n as SchemaIndexes } from "./indexes-
|
|
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-
|
|
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
|
|
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
|
-
*
|
|
935
|
-
*
|
|
936
|
-
*
|
|
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`)
|
|
941
|
-
*
|
|
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
|
-
*
|
|
1027
|
-
*
|
|
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<
|
|
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
|
|
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 `
|
|
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
|
|
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
|
-
*
|
|
1200
|
-
*
|
|
1201
|
-
*
|
|
1202
|
-
*
|
|
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
|
-
*
|
|
1205
|
-
*
|
|
1206
|
-
*
|
|
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
|
-
|
|
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
|
-
*
|
|
1665
|
+
* the caller's `req.filter` may be either Filter IR or a mongo query.
|
|
1214
1666
|
*
|
|
1215
|
-
*
|
|
1216
|
-
*
|
|
1217
|
-
*
|
|
1218
|
-
*
|
|
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
|
-
*
|
|
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,
|
|
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 };
|