@classytic/mongokit 3.22.0 → 3.22.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.
- package/dist/{Repository-CaDAJY98.mjs → Repository-Dr1MDAen.mjs} +5 -2
- package/dist/better-auth/index.mjs +1 -1
- package/dist/{create-repository-BFVYm48p.mjs → create-repository-DwODY4rZ.mjs} +2 -2
- package/dist/index.d.mts +2 -2
- package/dist/index.mjs +5 -5
- package/dist/plugins/index.d.mts +1 -1
- package/dist/plugins/index.mjs +2 -2
- package/dist/testkit/index.mjs +1 -1
- package/dist/{timestamp.plugin-Ch0O6DPX.mjs → timestamp.plugin-Cn6LKUIZ.mjs} +1 -1
- package/dist/{mongooseToJsonSchema-ChFKfZts.d.mts → to-plain-qzKeAC6z.d.mts} +30 -30
- package/dist/{memory-cache-D3wyN_2w.mjs → to-plain-vksRrLtc.mjs} +32 -32
- package/dist/utils/index.d.mts +1 -1
- package/dist/utils/index.mjs +1 -1
- package/dist/{validation-chain.plugin-DDQeEyk5.d.mts → validation-chain.plugin-D-EjrYpR.d.mts} +62 -62
- package/dist/{validation-chain.plugin-RxOzGuQy.mjs → validation-chain.plugin-D6qWMH5d.mjs} +220 -220
- package/package.json +1 -1
|
@@ -3037,7 +3037,10 @@ var Repository = class extends RepositoryBase {
|
|
|
3037
3037
|
async applyTransition(id, machine, args, options = {}) {
|
|
3038
3038
|
const entityId = String(id);
|
|
3039
3039
|
const sources = Array.isArray(args.from) ? args.from : [args.from];
|
|
3040
|
-
for (const from of sources)
|
|
3040
|
+
for (const from of sources) {
|
|
3041
|
+
if (from === args.to) continue;
|
|
3042
|
+
machine.assertTransition(entityId, from, args.to);
|
|
3043
|
+
}
|
|
3041
3044
|
const historyField = args.history === false ? null : args.history ?? "statusHistory";
|
|
3042
3045
|
const pushSpec = { ...args.push ?? {} };
|
|
3043
3046
|
if (historyField !== null) pushSpec[historyField] = {
|
|
@@ -3072,7 +3075,7 @@ var Repository = class extends RepositoryBase {
|
|
|
3072
3075
|
}
|
|
3073
3076
|
});
|
|
3074
3077
|
const currentState = (args.field ?? "status").split(".").reduce((acc, key) => acc?.[key], current);
|
|
3075
|
-
machine.assertTransition(entityId, String(currentState), args.to);
|
|
3078
|
+
if (String(currentState) !== args.to) machine.assertTransition(entityId, String(currentState), args.to);
|
|
3076
3079
|
throw createError(409, `${machine.name} '${entityId}' lost the transition race to '${args.to}' (currently '${String(currentState)}'); re-read and retry`, {
|
|
3077
3080
|
code: "TRANSITION_RACE_LOST",
|
|
3078
3081
|
meta: {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { n as Repository } from "../Repository-
|
|
1
|
+
import { n as Repository } from "../Repository-Dr1MDAen.mjs";
|
|
2
2
|
import { n as createMongooseAdapter } from "../adapter-rTPLUrAV.mjs";
|
|
3
3
|
import { pluralizeBetterAuthCollection, resolveBetterAuthCollections } from "@classytic/repo-core/better-auth";
|
|
4
4
|
//#region src/better-auth/index.ts
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { n as Repository } from "./Repository-
|
|
2
|
-
import { a as methodRegistryPlugin, d as cachePlugin, f as batchOperationsPlugin, h as auditLogPlugin, i as multiTenantPlugin, m as auditTrailPlugin, n as softDeletePlugin, o as customIdPlugin, t as timestampPlugin } from "./timestamp.plugin-
|
|
1
|
+
import { n as Repository } from "./Repository-Dr1MDAen.mjs";
|
|
2
|
+
import { a as methodRegistryPlugin, d as cachePlugin, f as batchOperationsPlugin, h as auditLogPlugin, i as multiTenantPlugin, m as auditTrailPlugin, n as softDeletePlugin, o as customIdPlugin, t as timestampPlugin } from "./timestamp.plugin-Cn6LKUIZ.mjs";
|
|
3
3
|
//#region src/create-repository.ts
|
|
4
4
|
function isLoggerAudit(value) {
|
|
5
5
|
const v = value;
|
package/dist/index.d.mts
CHANGED
|
@@ -3,10 +3,10 @@ import { t as index_d_exports } from "./index-CENL7LRb.mjs";
|
|
|
3
3
|
import { C as AuditTrailMethods, E as auditTrailPlugin, S as AuditQueryResult, T as AuditTrailQuery, _ as RepositoryCachePluginOptions, a as DateSequentialIdOptions, b as AuditOperation, c as SequentialIdOptions, d as getNextSequence, f as prefixedId, g as RepositoryCacheHandle, h as CacheOptions, i as CustomIdOptions, l as customIdPlugin, m as CacheAdapter, n as adminBypass, o as IdGenerator, p as sequentialId, r as multiTenantPlugin, s as PrefixedIdOptions, t as MultiTenantOptions, u as dateSequentialId, v as cachePlugin, w as AuditTrailOptions, x as AuditQueryOptions, y as AuditEntry } from "./multi-tenant.plugin-CFzMOuZP.mjs";
|
|
4
4
|
import { t as PaginationEngine } from "./PaginationEngine-CYRTLW6s.mjs";
|
|
5
5
|
import { a as Repository, i as HOOK_PRIORITY, n as CreateRepositoryConfig, o as TransitionMachine, r as createRepository, s as AggregationBuilder, t as AuditConfig } from "./create-repository-DXa3nMG6.mjs";
|
|
6
|
-
import { A as
|
|
6
|
+
import { A as elasticSearchPlugin, C as LeasePluginOptions, D as immutableStatesPlugin, E as ImmutableStatesPluginOptions, F as auditLogPlugin, I as AppendOnlyPluginOptions, L as appendOnlyPlugin, M as BatchOperationsMethods, N as BulkWriteResult, O as fieldFilterPlugin, P as batchOperationsPlugin, R as AggregateHelpersMethods, S as LeaseMethods, T as ImmutableClaimView, _ as observabilityPlugin, a as uniqueField, c as TenantContext, d as SubdocumentMethods, f as subdocumentPlugin, g as OperationMetric, h as ObservabilityOptions, i as requireField, j as cascadePlugin, k as ElasticSearchOptions, l as TenantStore, m as softDeletePlugin, n as blockIf, o as validationChainPlugin, p as SoftDeleteMethods, r as immutableField, s as timestampPlugin, t as autoInject, u as createTenantContext, v as MongoOperationsMethods, w as leasePlugin, x as methodRegistryPlugin, y as mongoOperationsPlugin, z as aggregateHelpersPlugin } from "./validation-chain.plugin-D-EjrYpR.mjs";
|
|
7
7
|
import { n as SchemaPathsLike, t as FieldType$1 } from "./coercion-BjdPX3YF.mjs";
|
|
8
8
|
import { n as SchemaIndexes } from "./indexes-DIjMpYES.mjs";
|
|
9
|
-
import { _ as parseDuplicateKeyError, a as configureLogger, c as idVariants, d as createFieldPreset, f as filterResponseData, g as isDuplicateKeyError, h as createError, i as
|
|
9
|
+
import { _ as parseDuplicateKeyError, a as configureLogger, c as idVariants, d as createFieldPreset, f as filterResponseData, g as isDuplicateKeyError, h as createError, i as createMemoryCache, m as getMongooseProjection, n as buildCrudSchemasFromModel, p as getFieldsForUser, r as buildCrudSchemasFromMongooseSchema, t as toPlain } from "./to-plain-qzKeAC6z.mjs";
|
|
10
10
|
import { RepoCapabilities } from "@classytic/repo-core/repository";
|
|
11
11
|
import { ClientSession, PipelineStage } from "mongoose";
|
|
12
12
|
//#region src/actions/cascade-references.d.ts
|
package/dist/index.mjs
CHANGED
|
@@ -1,18 +1,18 @@
|
|
|
1
1
|
import { n as idVariants } from "./id-resolution-P2OzcyRl.mjs";
|
|
2
|
-
import { a as withTransaction, c as MUTATING_OPERATIONS, d as operationsByPolicyKey, f as MONGOKIT_CAPABILITIES, i as isTransactionUnsupported, l as OP_REGISTRY, n as Repository, o as AggregationBuilder, p as createMongoPurgePort, r as batchTransaction, s as ALL_OPERATIONS, t as HOOK_PRIORITY, u as READ_OPERATIONS } from "./Repository-
|
|
2
|
+
import { a as withTransaction, c as MUTATING_OPERATIONS, d as operationsByPolicyKey, f as MONGOKIT_CAPABILITIES, i as isTransactionUnsupported, l as OP_REGISTRY, n as Repository, o as AggregationBuilder, p as createMongoPurgePort, r as batchTransaction, s as ALL_OPERATIONS, t as HOOK_PRIORITY, u as READ_OPERATIONS } from "./Repository-Dr1MDAen.mjs";
|
|
3
3
|
import { b as LookupBuilder, x as compileFilterToMongo } from "./update-1Lq9OMD2.mjs";
|
|
4
4
|
import { n as warn, t as configureLogger } from "./logger-K7dPmu2U.mjs";
|
|
5
5
|
import { n as isDuplicateKeyError, r as parseDuplicateKeyError, t as createError } from "./error-BuByZwKU.mjs";
|
|
6
6
|
import { t as actions_exports } from "./actions/index.mjs";
|
|
7
|
-
import { a as methodRegistryPlugin, c as getNextSequence, d as cachePlugin, f as batchOperationsPlugin, h as auditLogPlugin, i as multiTenantPlugin, l as prefixedId, m as auditTrailPlugin, n as softDeletePlugin, o as customIdPlugin, p as AuditTrailQuery, r as adminBypass, s as dateSequentialId, t as timestampPlugin, u as sequentialId } from "./timestamp.plugin-
|
|
7
|
+
import { a as methodRegistryPlugin, c as getNextSequence, d as cachePlugin, f as batchOperationsPlugin, h as auditLogPlugin, i as multiTenantPlugin, l as prefixedId, m as auditTrailPlugin, n as softDeletePlugin, o as customIdPlugin, p as AuditTrailQuery, r as adminBypass, s as dateSequentialId, t as timestampPlugin, u as sequentialId } from "./timestamp.plugin-Cn6LKUIZ.mjs";
|
|
8
8
|
import { t as PaginationEngine } from "./PaginationEngine-BT1ywu9k.mjs";
|
|
9
9
|
import { isGeoOperator, parseGeoFilter } from "./query/primitives/geo.mjs";
|
|
10
|
-
import { t as createRepository } from "./create-repository-
|
|
11
|
-
import { _ as aggregateHelpersPlugin, a as uniqueField, c as subdocumentPlugin, d as
|
|
10
|
+
import { t as createRepository } from "./create-repository-DwODY4rZ.mjs";
|
|
11
|
+
import { _ as aggregateHelpersPlugin, a as uniqueField, c as subdocumentPlugin, d as leasePlugin, f as immutableStatesPlugin, g as appendOnlyPlugin, h as cascadePlugin, i as requireField, l as observabilityPlugin, m as elasticSearchPlugin, n as blockIf, o as validationChainPlugin, p as fieldFilterPlugin, r as immutableField, s as createTenantContext, t as autoInject, u as mongoOperationsPlugin } from "./validation-chain.plugin-D6qWMH5d.mjs";
|
|
12
12
|
import { i as getMongooseProjection, n as filterResponseData, r as getFieldsForUser, t as createFieldPreset } from "./field-selection-FSQFLpsU.mjs";
|
|
13
13
|
import { buildFieldTypeMap, coerceFieldValue, coerceHeuristic } from "./query/primitives/coercion.mjs";
|
|
14
14
|
import { extractSchemaIndexes } from "./query/primitives/indexes.mjs";
|
|
15
|
-
import { n as
|
|
15
|
+
import { n as createMemoryCache, t as toPlain } from "./to-plain-vksRrLtc.mjs";
|
|
16
16
|
import { n as buildCrudSchemasFromMongooseSchema, t as buildCrudSchemasFromModel } from "./mongooseToJsonSchema-CVnrKFDq.mjs";
|
|
17
17
|
import { runChunkedPurge } from "@classytic/repo-core/repository";
|
|
18
18
|
import { isControlParam } from "@classytic/repo-core/query-parser";
|
package/dist/plugins/index.d.mts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { G as Plugin } from "../types-BnRqAJVa.mjs";
|
|
2
2
|
import { C as AuditTrailMethods, E as auditTrailPlugin, S as AuditQueryResult, T as AuditTrailQuery, _ as RepositoryCachePluginOptions, a as DateSequentialIdOptions, b as AuditOperation, c as SequentialIdOptions, d as getNextSequence, f as prefixedId, g as RepositoryCacheHandle, h as CacheOptions, i as CustomIdOptions, l as customIdPlugin, m as CacheAdapter, o as IdGenerator, p as sequentialId, r as multiTenantPlugin, s as PrefixedIdOptions, t as MultiTenantOptions, u as dateSequentialId, v as cachePlugin, w as AuditTrailOptions, x as AuditQueryOptions, y as AuditEntry } from "../multi-tenant.plugin-CFzMOuZP.mjs";
|
|
3
|
-
import { A as
|
|
3
|
+
import { A as elasticSearchPlugin, C as LeasePluginOptions, D as immutableStatesPlugin, E as ImmutableStatesPluginOptions, F as auditLogPlugin, I as AppendOnlyPluginOptions, L as appendOnlyPlugin, M as BatchOperationsMethods, O as fieldFilterPlugin, P as batchOperationsPlugin, R as AggregateHelpersMethods, S as LeaseMethods, T as ImmutableClaimView, _ as observabilityPlugin, a as uniqueField, b as MethodRegistryRepository, c as TenantContext, d as SubdocumentMethods, f as subdocumentPlugin, g as OperationMetric, h as ObservabilityOptions, i as requireField, j as cascadePlugin, k as ElasticSearchOptions, l as TenantStore, m as softDeletePlugin, n as blockIf, o as validationChainPlugin, p as SoftDeleteMethods, r as immutableField, s as timestampPlugin, t as autoInject, u as createTenantContext, v as MongoOperationsMethods, w as leasePlugin, x as methodRegistryPlugin, y as mongoOperationsPlugin, z as aggregateHelpersPlugin } from "../validation-chain.plugin-D-EjrYpR.mjs";
|
|
4
4
|
import { ChangeLogStore } from "@classytic/repo-core/sync";
|
|
5
5
|
//#region src/plugins/change-log.plugin.d.ts
|
|
6
6
|
interface ChangeLogPluginOptions {
|
package/dist/plugins/index.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { a as methodRegistryPlugin, c as getNextSequence, d as cachePlugin, f as batchOperationsPlugin, h as auditLogPlugin, i as multiTenantPlugin, l as prefixedId, m as auditTrailPlugin, n as softDeletePlugin, o as customIdPlugin, p as AuditTrailQuery, s as dateSequentialId, t as timestampPlugin, u as sequentialId } from "../timestamp.plugin-
|
|
2
|
-
import { _ as aggregateHelpersPlugin, a as uniqueField, c as subdocumentPlugin, d as
|
|
1
|
+
import { a as methodRegistryPlugin, c as getNextSequence, d as cachePlugin, f as batchOperationsPlugin, h as auditLogPlugin, i as multiTenantPlugin, l as prefixedId, m as auditTrailPlugin, n as softDeletePlugin, o as customIdPlugin, p as AuditTrailQuery, s as dateSequentialId, t as timestampPlugin, u as sequentialId } from "../timestamp.plugin-Cn6LKUIZ.mjs";
|
|
2
|
+
import { _ as aggregateHelpersPlugin, a as uniqueField, c as subdocumentPlugin, d as leasePlugin, f as immutableStatesPlugin, g as appendOnlyPlugin, h as cascadePlugin, i as requireField, l as observabilityPlugin, m as elasticSearchPlugin, n as blockIf, o as validationChainPlugin, p as fieldFilterPlugin, r as immutableField, s as createTenantContext, t as autoInject, u as mongoOperationsPlugin } from "../validation-chain.plugin-D6qWMH5d.mjs";
|
|
3
3
|
//#region src/plugins/change-log.plugin.ts
|
|
4
4
|
const PLUGIN_NAME = "changeLog";
|
|
5
5
|
function isSkipped(context) {
|
package/dist/testkit/index.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { l as OP_REGISTRY, s as ALL_OPERATIONS, t as HOOK_PRIORITY } from "./Repository-
|
|
1
|
+
import { l as OP_REGISTRY, s as ALL_OPERATIONS, t as HOOK_PRIORITY } from "./Repository-Dr1MDAen.mjs";
|
|
2
2
|
import { n as warn } from "./logger-K7dPmu2U.mjs";
|
|
3
3
|
import { t as createError } from "./error-BuByZwKU.mjs";
|
|
4
4
|
import { throwIfAborted, withRetry } from "@classytic/repo-core/repository";
|
|
@@ -249,35 +249,6 @@ interface LoggerConfig {
|
|
|
249
249
|
*/
|
|
250
250
|
declare function configureLogger(config: Partial<LoggerConfig> | false): void;
|
|
251
251
|
//#endregion
|
|
252
|
-
//#region src/utils/to-plain.d.ts
|
|
253
|
-
/**
|
|
254
|
-
* Convert a hydrated Mongoose (sub)document to a plain object — the
|
|
255
|
-
* spread-safety primitive for domain verbs that rebuild embedded lines.
|
|
256
|
-
*
|
|
257
|
-
* **The footgun this closes:** spreading a hydrated subdocument
|
|
258
|
-
* (`{ ...line, consumedQuantity: x }`) spreads its Mongoose INTERNAL
|
|
259
|
-
* properties (`$__`, `_doc`, ...), not its schema fields. The rebuilt
|
|
260
|
-
* object then casts to garbage on write — required fields land
|
|
261
|
-
* `undefined`, and because `findOneAndUpdate` does not run validators
|
|
262
|
-
* by default, the corrupt lines persist silently. This has bitten every
|
|
263
|
-
* package that maps over embedded arrays (`components`, `operations`,
|
|
264
|
-
* `revisions`) to produce an updated copy.
|
|
265
|
-
*
|
|
266
|
-
* Rule of thumb: **any `{ ...subdoc }` in a repository verb goes
|
|
267
|
-
* through `toPlain` first.**
|
|
268
|
-
*
|
|
269
|
-
* ```ts
|
|
270
|
-
* const updated = lines.map((line) => ({
|
|
271
|
-
* ...toPlain(line),
|
|
272
|
-
* reservedQuantity: reserved.get(line.itemSku) ?? 0,
|
|
273
|
-
* }));
|
|
274
|
-
* ```
|
|
275
|
-
*
|
|
276
|
-
* Non-documents pass through unchanged, so it is safe to apply
|
|
277
|
-
* defensively over values that may or may not be hydrated.
|
|
278
|
-
*/
|
|
279
|
-
declare function toPlain<T>(value: T): T;
|
|
280
|
-
//#endregion
|
|
281
252
|
//#region src/utils/memory-cache.d.ts
|
|
282
253
|
/**
|
|
283
254
|
* Creates an in-memory cache adapter
|
|
@@ -324,4 +295,33 @@ declare function buildCrudSchemasFromMongooseSchema(mongooseSchema: Schema, opti
|
|
|
324
295
|
*/
|
|
325
296
|
declare function buildCrudSchemasFromModel(mongooseModel: mongoose.Model<unknown>, options?: SchemaBuilderOptions): CrudSchemasFramework;
|
|
326
297
|
//#endregion
|
|
327
|
-
|
|
298
|
+
//#region src/utils/to-plain.d.ts
|
|
299
|
+
/**
|
|
300
|
+
* Convert a hydrated Mongoose (sub)document to a plain object — the
|
|
301
|
+
* spread-safety primitive for domain verbs that rebuild embedded lines.
|
|
302
|
+
*
|
|
303
|
+
* **The footgun this closes:** spreading a hydrated subdocument
|
|
304
|
+
* (`{ ...line, consumedQuantity: x }`) spreads its Mongoose INTERNAL
|
|
305
|
+
* properties (`$__`, `_doc`, ...), not its schema fields. The rebuilt
|
|
306
|
+
* object then casts to garbage on write — required fields land
|
|
307
|
+
* `undefined`, and because `findOneAndUpdate` does not run validators
|
|
308
|
+
* by default, the corrupt lines persist silently. This has bitten every
|
|
309
|
+
* package that maps over embedded arrays (`components`, `operations`,
|
|
310
|
+
* `revisions`) to produce an updated copy.
|
|
311
|
+
*
|
|
312
|
+
* Rule of thumb: **any `{ ...subdoc }` in a repository verb goes
|
|
313
|
+
* through `toPlain` first.**
|
|
314
|
+
*
|
|
315
|
+
* ```ts
|
|
316
|
+
* const updated = lines.map((line) => ({
|
|
317
|
+
* ...toPlain(line),
|
|
318
|
+
* reservedQuantity: reserved.get(line.itemSku) ?? 0,
|
|
319
|
+
* }));
|
|
320
|
+
* ```
|
|
321
|
+
*
|
|
322
|
+
* Non-documents pass through unchanged, so it is safe to apply
|
|
323
|
+
* defensively over values that may or may not be hydrated.
|
|
324
|
+
*/
|
|
325
|
+
declare function toPlain<T>(value: T): T;
|
|
326
|
+
//#endregion
|
|
327
|
+
export { parseDuplicateKeyError as _, configureLogger as a, idVariants as c, createFieldPreset as d, filterResponseData as f, isDuplicateKeyError as g, createError as h, createMemoryCache as i, isObjectIdInstance as l, getMongooseProjection as m, buildCrudSchemasFromModel as n, IdType as o, getFieldsForUser as p, buildCrudSchemasFromMongooseSchema as r, getSchemaIdType as s, toPlain as t, isValidIdForType as u };
|
|
@@ -1,34 +1,3 @@
|
|
|
1
|
-
//#region src/utils/to-plain.ts
|
|
2
|
-
/**
|
|
3
|
-
* Convert a hydrated Mongoose (sub)document to a plain object — the
|
|
4
|
-
* spread-safety primitive for domain verbs that rebuild embedded lines.
|
|
5
|
-
*
|
|
6
|
-
* **The footgun this closes:** spreading a hydrated subdocument
|
|
7
|
-
* (`{ ...line, consumedQuantity: x }`) spreads its Mongoose INTERNAL
|
|
8
|
-
* properties (`$__`, `_doc`, ...), not its schema fields. The rebuilt
|
|
9
|
-
* object then casts to garbage on write — required fields land
|
|
10
|
-
* `undefined`, and because `findOneAndUpdate` does not run validators
|
|
11
|
-
* by default, the corrupt lines persist silently. This has bitten every
|
|
12
|
-
* package that maps over embedded arrays (`components`, `operations`,
|
|
13
|
-
* `revisions`) to produce an updated copy.
|
|
14
|
-
*
|
|
15
|
-
* Rule of thumb: **any `{ ...subdoc }` in a repository verb goes
|
|
16
|
-
* through `toPlain` first.**
|
|
17
|
-
*
|
|
18
|
-
* ```ts
|
|
19
|
-
* const updated = lines.map((line) => ({
|
|
20
|
-
* ...toPlain(line),
|
|
21
|
-
* reservedQuantity: reserved.get(line.itemSku) ?? 0,
|
|
22
|
-
* }));
|
|
23
|
-
* ```
|
|
24
|
-
*
|
|
25
|
-
* Non-documents pass through unchanged, so it is safe to apply
|
|
26
|
-
* defensively over values that may or may not be hydrated.
|
|
27
|
-
*/
|
|
28
|
-
function toPlain(value) {
|
|
29
|
-
return value !== null && value !== void 0 && typeof value.toObject === "function" ? value.toObject() : value;
|
|
30
|
-
}
|
|
31
|
-
//#endregion
|
|
32
1
|
//#region src/utils/memory-cache.ts
|
|
33
2
|
/**
|
|
34
3
|
* Creates an in-memory cache adapter
|
|
@@ -95,4 +64,35 @@ function createMemoryCache(maxEntries = 1e3) {
|
|
|
95
64
|
};
|
|
96
65
|
}
|
|
97
66
|
//#endregion
|
|
98
|
-
|
|
67
|
+
//#region src/utils/to-plain.ts
|
|
68
|
+
/**
|
|
69
|
+
* Convert a hydrated Mongoose (sub)document to a plain object — the
|
|
70
|
+
* spread-safety primitive for domain verbs that rebuild embedded lines.
|
|
71
|
+
*
|
|
72
|
+
* **The footgun this closes:** spreading a hydrated subdocument
|
|
73
|
+
* (`{ ...line, consumedQuantity: x }`) spreads its Mongoose INTERNAL
|
|
74
|
+
* properties (`$__`, `_doc`, ...), not its schema fields. The rebuilt
|
|
75
|
+
* object then casts to garbage on write — required fields land
|
|
76
|
+
* `undefined`, and because `findOneAndUpdate` does not run validators
|
|
77
|
+
* by default, the corrupt lines persist silently. This has bitten every
|
|
78
|
+
* package that maps over embedded arrays (`components`, `operations`,
|
|
79
|
+
* `revisions`) to produce an updated copy.
|
|
80
|
+
*
|
|
81
|
+
* Rule of thumb: **any `{ ...subdoc }` in a repository verb goes
|
|
82
|
+
* through `toPlain` first.**
|
|
83
|
+
*
|
|
84
|
+
* ```ts
|
|
85
|
+
* const updated = lines.map((line) => ({
|
|
86
|
+
* ...toPlain(line),
|
|
87
|
+
* reservedQuantity: reserved.get(line.itemSku) ?? 0,
|
|
88
|
+
* }));
|
|
89
|
+
* ```
|
|
90
|
+
*
|
|
91
|
+
* Non-documents pass through unchanged, so it is safe to apply
|
|
92
|
+
* defensively over values that may or may not be hydrated.
|
|
93
|
+
*/
|
|
94
|
+
function toPlain(value) {
|
|
95
|
+
return value !== null && value !== void 0 && typeof value.toObject === "function" ? value.toObject() : value;
|
|
96
|
+
}
|
|
97
|
+
//#endregion
|
|
98
|
+
export { createMemoryCache as n, toPlain as t };
|
package/dist/utils/index.d.mts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { a as configureLogger, d as createFieldPreset, f as filterResponseData, h as createError, i as
|
|
1
|
+
import { a as configureLogger, d as createFieldPreset, f as filterResponseData, h as createError, i as createMemoryCache, l as isObjectIdInstance, m as getMongooseProjection, n as buildCrudSchemasFromModel, o as IdType, p as getFieldsForUser, r as buildCrudSchemasFromMongooseSchema, s as getSchemaIdType, t as toPlain, u as isValidIdForType } from "../to-plain-qzKeAC6z.mjs";
|
|
2
2
|
export { type IdType, buildCrudSchemasFromModel, buildCrudSchemasFromMongooseSchema, configureLogger, createError, createFieldPreset, createMemoryCache, filterResponseData, getFieldsForUser, getMongooseProjection, getSchemaIdType, isObjectIdInstance, isValidIdForType, toPlain };
|
package/dist/utils/index.mjs
CHANGED
|
@@ -2,6 +2,6 @@ import { i as isValidIdForType, r as isObjectIdInstance, t as getSchemaIdType }
|
|
|
2
2
|
import { t as configureLogger } from "../logger-K7dPmu2U.mjs";
|
|
3
3
|
import { t as createError } from "../error-BuByZwKU.mjs";
|
|
4
4
|
import { i as getMongooseProjection, n as filterResponseData, r as getFieldsForUser, t as createFieldPreset } from "../field-selection-FSQFLpsU.mjs";
|
|
5
|
-
import { n as
|
|
5
|
+
import { n as createMemoryCache, t as toPlain } from "../to-plain-vksRrLtc.mjs";
|
|
6
6
|
import { n as buildCrudSchemasFromMongooseSchema, t as buildCrudSchemasFromModel } from "../mongooseToJsonSchema-CVnrKFDq.mjs";
|
|
7
7
|
export { buildCrudSchemasFromModel, buildCrudSchemasFromMongooseSchema, configureLogger, createError, createFieldPreset, createMemoryCache, filterResponseData, getFieldsForUser, getMongooseProjection, getSchemaIdType, isObjectIdInstance, isValidIdForType, toPlain };
|
package/dist/{validation-chain.plugin-DDQeEyk5.d.mts → validation-chain.plugin-D-EjrYpR.d.mts}
RENAMED
|
@@ -87,6 +87,26 @@ interface AggregateHelpersMethods {
|
|
|
87
87
|
max(field: string, query?: Record<string, unknown>, options?: Record<string, unknown>): Promise<number>;
|
|
88
88
|
}
|
|
89
89
|
//#endregion
|
|
90
|
+
//#region src/plugins/append-only.plugin.d.ts
|
|
91
|
+
interface AppendOnlyPluginOptions {
|
|
92
|
+
/**
|
|
93
|
+
* Mutating ops to permit besides `create` / `createMany`. Use for a
|
|
94
|
+
* domain's ONE legitimate mutation path (e.g. GDPR `deleteMany`) —
|
|
95
|
+
* every allowed op still flows through the rest of the plugin stack
|
|
96
|
+
* (tenant scope, audit) as usual.
|
|
97
|
+
*/
|
|
98
|
+
allow?: readonly string[] | undefined;
|
|
99
|
+
/**
|
|
100
|
+
* Hook priority for the fence. Default `HOOK_PRIORITY.POLICY` (100).
|
|
101
|
+
* Ledger repos that want the append-only refusal to win over other
|
|
102
|
+
* policy errors (e.g. a missing-tenant throw) register BELOW policy —
|
|
103
|
+
* flow's StockEvent uses `50` so the violation always reads
|
|
104
|
+
* "append-only", never "missing organizationId".
|
|
105
|
+
*/
|
|
106
|
+
priority?: number | undefined;
|
|
107
|
+
}
|
|
108
|
+
declare function appendOnlyPlugin(options?: AppendOnlyPluginOptions): Plugin;
|
|
109
|
+
//#endregion
|
|
90
110
|
//#region src/plugins/audit-log.plugin.d.ts
|
|
91
111
|
declare function auditLogPlugin(logger: Logger): Plugin;
|
|
92
112
|
//#endregion
|
|
@@ -163,6 +183,47 @@ declare function elasticSearchPlugin(options: ElasticSearchOptions): Plugin;
|
|
|
163
183
|
*/
|
|
164
184
|
declare function fieldFilterPlugin(fieldPreset: FieldPreset): Plugin;
|
|
165
185
|
//#endregion
|
|
186
|
+
//#region src/plugins/immutable-states.plugin.d.ts
|
|
187
|
+
/** The slice of a `before:claim` context `allowClaim` predicates see. */
|
|
188
|
+
interface ImmutableClaimView {
|
|
189
|
+
id: unknown;
|
|
190
|
+
transition: {
|
|
191
|
+
from?: unknown | readonly unknown[];
|
|
192
|
+
to?: unknown;
|
|
193
|
+
field?: string;
|
|
194
|
+
where?: Record<string, unknown>;
|
|
195
|
+
};
|
|
196
|
+
/** Operator-form patch (`{ $set: {...}, ... }`). */
|
|
197
|
+
data: Record<string, unknown> | undefined;
|
|
198
|
+
}
|
|
199
|
+
interface ImmutableStatesPluginOptions {
|
|
200
|
+
/** States in which the document is frozen (e.g. `['posted']`). */
|
|
201
|
+
states: readonly string[];
|
|
202
|
+
/** State field on the doc — dotted paths supported. Default `'state'`. */
|
|
203
|
+
field?: string | undefined;
|
|
204
|
+
/**
|
|
205
|
+
* Context flag the OWNING ENGINE's verbs set to pass the guard
|
|
206
|
+
* (private handshake — see module doc). Default `'_immutableInternal'`.
|
|
207
|
+
*/
|
|
208
|
+
internalFlag?: string | undefined;
|
|
209
|
+
/** Tenant field used to scope state lookups (mirrors the repo's scope). */
|
|
210
|
+
tenantField?: string | undefined;
|
|
211
|
+
/**
|
|
212
|
+
* Exempt specific claim shapes that legitimately touch frozen rows
|
|
213
|
+
* (audit-trail stamps like ledger's reverse-mark). Return `true` to
|
|
214
|
+
* allow. Runs only after the claim was determined to reach a frozen
|
|
215
|
+
* state — keep the predicate a strict fingerprint.
|
|
216
|
+
*/
|
|
217
|
+
allowClaim?: ((view: ImmutableClaimView) => boolean) | undefined;
|
|
218
|
+
/** Domain error to throw. Default: 403 `IMMUTABLE_STATE_VIOLATION`. */
|
|
219
|
+
errorFactory?: ((ctx: {
|
|
220
|
+
id: unknown;
|
|
221
|
+
operation: string;
|
|
222
|
+
state?: string | undefined;
|
|
223
|
+
}) => Error) | undefined;
|
|
224
|
+
}
|
|
225
|
+
declare function immutableStatesPlugin(options: ImmutableStatesPluginOptions): Plugin;
|
|
226
|
+
//#endregion
|
|
166
227
|
//#region src/plugins/lease.plugin.d.ts
|
|
167
228
|
interface LeasePluginOptions {
|
|
168
229
|
/** Field carrying the row's status. @default `'status'` */
|
|
@@ -517,67 +578,6 @@ interface ObservabilityOptions {
|
|
|
517
578
|
}
|
|
518
579
|
declare function observabilityPlugin(options: ObservabilityOptions): Plugin;
|
|
519
580
|
//#endregion
|
|
520
|
-
//#region src/plugins/append-only.plugin.d.ts
|
|
521
|
-
interface AppendOnlyPluginOptions {
|
|
522
|
-
/**
|
|
523
|
-
* Mutating ops to permit besides `create` / `createMany`. Use for a
|
|
524
|
-
* domain's ONE legitimate mutation path (e.g. GDPR `deleteMany`) —
|
|
525
|
-
* every allowed op still flows through the rest of the plugin stack
|
|
526
|
-
* (tenant scope, audit) as usual.
|
|
527
|
-
*/
|
|
528
|
-
allow?: readonly string[] | undefined;
|
|
529
|
-
/**
|
|
530
|
-
* Hook priority for the fence. Default `HOOK_PRIORITY.POLICY` (100).
|
|
531
|
-
* Ledger repos that want the append-only refusal to win over other
|
|
532
|
-
* policy errors (e.g. a missing-tenant throw) register BELOW policy —
|
|
533
|
-
* flow's StockEvent uses `50` so the violation always reads
|
|
534
|
-
* "append-only", never "missing organizationId".
|
|
535
|
-
*/
|
|
536
|
-
priority?: number | undefined;
|
|
537
|
-
}
|
|
538
|
-
declare function appendOnlyPlugin(options?: AppendOnlyPluginOptions): Plugin;
|
|
539
|
-
//#endregion
|
|
540
|
-
//#region src/plugins/immutable-states.plugin.d.ts
|
|
541
|
-
/** The slice of a `before:claim` context `allowClaim` predicates see. */
|
|
542
|
-
interface ImmutableClaimView {
|
|
543
|
-
id: unknown;
|
|
544
|
-
transition: {
|
|
545
|
-
from?: unknown | readonly unknown[];
|
|
546
|
-
to?: unknown;
|
|
547
|
-
field?: string;
|
|
548
|
-
where?: Record<string, unknown>;
|
|
549
|
-
};
|
|
550
|
-
/** Operator-form patch (`{ $set: {...}, ... }`). */
|
|
551
|
-
data: Record<string, unknown> | undefined;
|
|
552
|
-
}
|
|
553
|
-
interface ImmutableStatesPluginOptions {
|
|
554
|
-
/** States in which the document is frozen (e.g. `['posted']`). */
|
|
555
|
-
states: readonly string[];
|
|
556
|
-
/** State field on the doc — dotted paths supported. Default `'state'`. */
|
|
557
|
-
field?: string | undefined;
|
|
558
|
-
/**
|
|
559
|
-
* Context flag the OWNING ENGINE's verbs set to pass the guard
|
|
560
|
-
* (private handshake — see module doc). Default `'_immutableInternal'`.
|
|
561
|
-
*/
|
|
562
|
-
internalFlag?: string | undefined;
|
|
563
|
-
/** Tenant field used to scope state lookups (mirrors the repo's scope). */
|
|
564
|
-
tenantField?: string | undefined;
|
|
565
|
-
/**
|
|
566
|
-
* Exempt specific claim shapes that legitimately touch frozen rows
|
|
567
|
-
* (audit-trail stamps like ledger's reverse-mark). Return `true` to
|
|
568
|
-
* allow. Runs only after the claim was determined to reach a frozen
|
|
569
|
-
* state — keep the predicate a strict fingerprint.
|
|
570
|
-
*/
|
|
571
|
-
allowClaim?: ((view: ImmutableClaimView) => boolean) | undefined;
|
|
572
|
-
/** Domain error to throw. Default: 403 `IMMUTABLE_STATE_VIOLATION`. */
|
|
573
|
-
errorFactory?: ((ctx: {
|
|
574
|
-
id: unknown;
|
|
575
|
-
operation: string;
|
|
576
|
-
state?: string | undefined;
|
|
577
|
-
}) => Error) | undefined;
|
|
578
|
-
}
|
|
579
|
-
declare function immutableStatesPlugin(options: ImmutableStatesPluginOptions): Plugin;
|
|
580
|
-
//#endregion
|
|
581
581
|
//#region src/plugins/soft-delete.plugin.d.ts
|
|
582
582
|
/**
|
|
583
583
|
* Soft delete plugin
|
|
@@ -820,4 +820,4 @@ declare function immutableField(field: string): ValidatorDefinition;
|
|
|
820
820
|
*/
|
|
821
821
|
declare function uniqueField(field: string, errorMessage?: string): ValidatorDefinition;
|
|
822
822
|
//#endregion
|
|
823
|
-
export {
|
|
823
|
+
export { elasticSearchPlugin as A, LeasePluginOptions as C, immutableStatesPlugin as D, ImmutableStatesPluginOptions as E, auditLogPlugin as F, AppendOnlyPluginOptions as I, appendOnlyPlugin as L, BatchOperationsMethods as M, BulkWriteResult as N, fieldFilterPlugin as O, batchOperationsPlugin as P, AggregateHelpersMethods as R, LeaseMethods as S, ImmutableClaimView as T, observabilityPlugin as _, uniqueField as a, MethodRegistryRepository as b, TenantContext as c, SubdocumentMethods as d, subdocumentPlugin as f, OperationMetric as g, ObservabilityOptions as h, requireField as i, cascadePlugin as j, ElasticSearchOptions as k, TenantStore as l, softDeletePlugin as m, blockIf as n, validationChainPlugin as o, SoftDeleteMethods as p, immutableField as r, timestampPlugin as s, autoInject as t, createTenantContext as u, MongoOperationsMethods as v, leasePlugin as w, methodRegistryPlugin as x, mongoOperationsPlugin as y, aggregateHelpersPlugin as z };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { l as OP_REGISTRY, s as ALL_OPERATIONS } from "./Repository-
|
|
1
|
+
import { l as OP_REGISTRY, s as ALL_OPERATIONS } from "./Repository-Dr1MDAen.mjs";
|
|
2
2
|
import { _ as upsert } from "./update-1Lq9OMD2.mjs";
|
|
3
3
|
import { n as warn } from "./logger-K7dPmu2U.mjs";
|
|
4
4
|
import { t as createError } from "./error-BuByZwKU.mjs";
|
|
@@ -70,6 +70,90 @@ function aggregateHelpersPlugin() {
|
|
|
70
70
|
};
|
|
71
71
|
}
|
|
72
72
|
//#endregion
|
|
73
|
+
//#region src/plugins/append-only.plugin.ts
|
|
74
|
+
/**
|
|
75
|
+
* Append-Only Plugin
|
|
76
|
+
*
|
|
77
|
+
* Turns a repository into an immutable-facts ledger: inserts land,
|
|
78
|
+
* everything else is refused at POLICY priority. The enforcement
|
|
79
|
+
* layer for event-sourced planes — flow's `StockEvent`,
|
|
80
|
+
* manufacturing's `ProductionEvent`, audit sinks, outbox archives —
|
|
81
|
+
* where a mutable row would silently corrupt the audit/time-travel
|
|
82
|
+
* story that the whole collection exists to provide.
|
|
83
|
+
*
|
|
84
|
+
* Grep-proof by construction: hand-fenced `update()`/`delete()`
|
|
85
|
+
* overrides only cover the verbs someone remembered to override; this
|
|
86
|
+
* plugin derives coverage from `OP_REGISTRY`, so every mutating op —
|
|
87
|
+
* including `claim`, `claimVersion`, `restore`, `bulkWrite`, and any
|
|
88
|
+
* op added to the registry later — is refused automatically.
|
|
89
|
+
*
|
|
90
|
+
* Allowed by default: `create`, `createMany` (appends). `getOrCreate`
|
|
91
|
+
* is registry-classified as a read and stays available — it can only
|
|
92
|
+
* INSERT or return the existing row, never mutate one, which is
|
|
93
|
+
* append-safe by definition (idempotency stores rely on it).
|
|
94
|
+
* Refused by default: `update`, `findOneAndUpdate`, `updateMany`,
|
|
95
|
+
* `delete`, `deleteMany`, `restore`, `claim`, `claimVersion`,
|
|
96
|
+
* `bulkWrite` (wholesale — mixed insert/update batches are ambiguous;
|
|
97
|
+
* append via `createMany` instead). Composed methods from other
|
|
98
|
+
* plugins (`incrementIfBelow`, lease ops) decompose into these
|
|
99
|
+
* registry ops internally, so the fence catches them too.
|
|
100
|
+
*
|
|
101
|
+
* TTL indexes are unaffected (server-side GC never touches the repo
|
|
102
|
+
* layer) — TTL-expiring an idempotency ledger still works. Raw
|
|
103
|
+
* `Model.*` driver calls bypass every plugin, this one included —
|
|
104
|
+
* the org-wide "never drop to Model.deleteOne" rule is what covers
|
|
105
|
+
* that hole, same as for tenant scope.
|
|
106
|
+
*
|
|
107
|
+
* **Escape hatches** (both audited via `after:append-only-bypass`):
|
|
108
|
+
* - Plugin-level `allow` — ops the DOMAIN legitimately needs, wired
|
|
109
|
+
* once at construction (e.g. `allow: ['deleteMany']` for a
|
|
110
|
+
* GDPR-erasure path the host gates behind admin roles).
|
|
111
|
+
* - Per-call `bypassAppendOnly: true` — deliberate one-off
|
|
112
|
+
* maintenance (backfill repair, migration), grep-auditable at the
|
|
113
|
+
* call site.
|
|
114
|
+
*
|
|
115
|
+
* `skipPlugins` is deliberately IGNORED — same policy as tenant scope
|
|
116
|
+
* and soft delete: integrity fences don't have a convenience bypass.
|
|
117
|
+
*
|
|
118
|
+
* @example
|
|
119
|
+
* const events = new Repository(StockEventModel, [
|
|
120
|
+
* multiTenantPlugin({ ... }),
|
|
121
|
+
* appendOnlyPlugin(),
|
|
122
|
+
* ]);
|
|
123
|
+
* await events.create({ ... }); // OK
|
|
124
|
+
* await events.update(id, { ... }); // throws 405 APPEND_ONLY_VIOLATION
|
|
125
|
+
* await events.deleteMany({ ... }, { bypassAppendOnly: true }); // audited bypass
|
|
126
|
+
*/
|
|
127
|
+
const ALWAYS_ALLOWED = /* @__PURE__ */ new Set(["create", "createMany"]);
|
|
128
|
+
function appendOnlyPlugin(options = {}) {
|
|
129
|
+
const allowed = /* @__PURE__ */ new Set([...ALWAYS_ALLOWED, ...options.allow ?? []]);
|
|
130
|
+
return {
|
|
131
|
+
name: "appendOnly",
|
|
132
|
+
apply(repo) {
|
|
133
|
+
const modelName = repo.Model?.modelName ?? "model";
|
|
134
|
+
for (const [op, meta] of Object.entries(OP_REGISTRY)) {
|
|
135
|
+
if (!meta.mutates || allowed.has(op)) continue;
|
|
136
|
+
repo.on(`before:${op}`, (context) => {
|
|
137
|
+
if (context.bypassAppendOnly === true) {
|
|
138
|
+
repo.emit("after:append-only-bypass", {
|
|
139
|
+
operation: op,
|
|
140
|
+
context
|
|
141
|
+
});
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
throw createError(405, `${modelName} is append-only: '${op}' is not permitted. Rows on this collection are immutable facts (event ledger). Append a compensating row instead; for deliberate maintenance pass bypassAppendOnly: true (audited via 'after:append-only-bypass').`, {
|
|
145
|
+
code: "APPEND_ONLY_VIOLATION",
|
|
146
|
+
meta: {
|
|
147
|
+
operation: op,
|
|
148
|
+
model: modelName
|
|
149
|
+
}
|
|
150
|
+
});
|
|
151
|
+
}, { priority: options.priority ?? HOOK_PRIORITY.POLICY });
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
//#endregion
|
|
73
157
|
//#region src/plugins/cascade.plugin.ts
|
|
74
158
|
/**
|
|
75
159
|
* Cascade Delete Plugin
|
|
@@ -392,6 +476,140 @@ function fieldFilterPlugin(fieldPreset) {
|
|
|
392
476
|
};
|
|
393
477
|
}
|
|
394
478
|
//#endregion
|
|
479
|
+
//#region src/plugins/immutable-states.plugin.ts
|
|
480
|
+
/** Ops this plugin has an explicit strategy for (see module doc). */
|
|
481
|
+
const STRATEGY_OPS = /* @__PURE__ */ new Set([
|
|
482
|
+
"create",
|
|
483
|
+
"createMany",
|
|
484
|
+
"update",
|
|
485
|
+
"delete",
|
|
486
|
+
"claimVersion",
|
|
487
|
+
"restore",
|
|
488
|
+
"findOneAndUpdate",
|
|
489
|
+
"updateMany",
|
|
490
|
+
"deleteMany",
|
|
491
|
+
"bulkWrite",
|
|
492
|
+
"claim"
|
|
493
|
+
]);
|
|
494
|
+
function immutableStatesPlugin(options) {
|
|
495
|
+
const field = options.field ?? "state";
|
|
496
|
+
const internalFlag = options.internalFlag ?? "_immutableInternal";
|
|
497
|
+
const frozen = new Set(options.states);
|
|
498
|
+
const throwViolation = (ctx) => {
|
|
499
|
+
if (options.errorFactory) throw options.errorFactory(ctx);
|
|
500
|
+
throw createError(403, `Document ${String(ctx.id)} is in immutable state${ctx.state !== void 0 ? ` '${ctx.state}'` : ""} — '${ctx.operation}' is not permitted. Frozen rows change only through the owning engine's transition verbs.`, {
|
|
501
|
+
code: "IMMUTABLE_STATE_VIOLATION",
|
|
502
|
+
meta: { ...ctx }
|
|
503
|
+
});
|
|
504
|
+
};
|
|
505
|
+
return {
|
|
506
|
+
name: "immutableStates",
|
|
507
|
+
apply(repo) {
|
|
508
|
+
const unhandled = Object.entries(OP_REGISTRY).filter(([op, meta]) => meta.mutates && !STRATEGY_OPS.has(op)).map(([op]) => op);
|
|
509
|
+
if (unhandled.length > 0) throw createError(500, `immutableStatesPlugin has no strategy for mutating op(s): ${unhandled.join(", ")}. Mongokit added write ops this plugin predates — extend the plugin before using it.`, {
|
|
510
|
+
code: "IMMUTABLE_STATES_UNHANDLED_OPS",
|
|
511
|
+
meta: { unhandled }
|
|
512
|
+
});
|
|
513
|
+
const Model = repo.Model;
|
|
514
|
+
const getState = (doc) => field.split(".").reduce((acc, key) => acc?.[key], doc);
|
|
515
|
+
const scoped = (base, ctx) => {
|
|
516
|
+
const q = { ...base };
|
|
517
|
+
if (options.tenantField && ctx.query && options.tenantField in ctx.query) q[options.tenantField] = ctx.query[options.tenantField];
|
|
518
|
+
return q;
|
|
519
|
+
};
|
|
520
|
+
/** Raw-Model read — never re-enters the repo hook pipeline. */
|
|
521
|
+
const readStateById = async (ctx) => {
|
|
522
|
+
if (ctx.id === void 0 || ctx.id === null) return void 0;
|
|
523
|
+
const doc = await Model.findOne(scoped({ _id: ctx.id }, ctx)).select({ [field]: 1 }).lean();
|
|
524
|
+
return doc ? getState(doc) : void 0;
|
|
525
|
+
};
|
|
526
|
+
const guardById = (op) => async (ctx) => {
|
|
527
|
+
if (ctx[internalFlag]) return;
|
|
528
|
+
const state = await readStateById(ctx);
|
|
529
|
+
if (state !== void 0 && frozen.has(state)) throwViolation({
|
|
530
|
+
id: ctx.id,
|
|
531
|
+
operation: op,
|
|
532
|
+
state
|
|
533
|
+
});
|
|
534
|
+
};
|
|
535
|
+
for (const op of [
|
|
536
|
+
"update",
|
|
537
|
+
"delete",
|
|
538
|
+
"claimVersion",
|
|
539
|
+
"restore"
|
|
540
|
+
]) repo.on(`before:${op}`, guardById(op));
|
|
541
|
+
repo.on("before:findOneAndUpdate", async (ctx) => {
|
|
542
|
+
if (ctx[internalFlag]) return;
|
|
543
|
+
const filter = ctx.query ?? {};
|
|
544
|
+
const doc = await Model.findOne(filter).select({
|
|
545
|
+
[field]: 1,
|
|
546
|
+
_id: 1
|
|
547
|
+
}).lean();
|
|
548
|
+
if (!doc) return;
|
|
549
|
+
const state = getState(doc);
|
|
550
|
+
if (state !== void 0 && frozen.has(state)) throwViolation({
|
|
551
|
+
id: doc._id,
|
|
552
|
+
operation: "findOneAndUpdate",
|
|
553
|
+
state
|
|
554
|
+
});
|
|
555
|
+
});
|
|
556
|
+
const guardMany = (op) => async (ctx) => {
|
|
557
|
+
if (ctx[internalFlag]) return;
|
|
558
|
+
const filter = ctx.query ?? {};
|
|
559
|
+
const hit = await Model.exists({ $and: [filter, { [field]: { $in: [...frozen] } }] });
|
|
560
|
+
if (hit) throwViolation({
|
|
561
|
+
id: hit._id,
|
|
562
|
+
operation: op,
|
|
563
|
+
state: void 0
|
|
564
|
+
});
|
|
565
|
+
};
|
|
566
|
+
repo.on("before:updateMany", guardMany("updateMany"));
|
|
567
|
+
repo.on("before:deleteMany", guardMany("deleteMany"));
|
|
568
|
+
repo.on("before:bulkWrite", (ctx) => {
|
|
569
|
+
if (ctx[internalFlag]) return;
|
|
570
|
+
throwViolation({
|
|
571
|
+
id: void 0,
|
|
572
|
+
operation: "bulkWrite",
|
|
573
|
+
state: void 0
|
|
574
|
+
});
|
|
575
|
+
});
|
|
576
|
+
repo.on("before:claim", async (rawCtx) => {
|
|
577
|
+
const ctx = rawCtx;
|
|
578
|
+
if (ctx[internalFlag]) return;
|
|
579
|
+
const t = ctx.transition;
|
|
580
|
+
if (!t) return;
|
|
581
|
+
if ((t.field ?? "status") === field) {
|
|
582
|
+
const fromSpec = t.from;
|
|
583
|
+
if (!(Array.isArray(fromSpec) ? fromSpec.some((f) => frozen.has(String(f))) : frozen.has(String(fromSpec)))) return;
|
|
584
|
+
if (options.allowClaim?.({
|
|
585
|
+
id: ctx.id,
|
|
586
|
+
transition: t,
|
|
587
|
+
data: ctx.data
|
|
588
|
+
})) return;
|
|
589
|
+
throwViolation({
|
|
590
|
+
id: ctx.id,
|
|
591
|
+
operation: "claim",
|
|
592
|
+
state: void 0
|
|
593
|
+
});
|
|
594
|
+
}
|
|
595
|
+
const state = await readStateById(ctx);
|
|
596
|
+
if (state !== void 0 && frozen.has(state)) {
|
|
597
|
+
if (options.allowClaim?.({
|
|
598
|
+
id: ctx.id,
|
|
599
|
+
transition: t,
|
|
600
|
+
data: ctx.data
|
|
601
|
+
})) return;
|
|
602
|
+
throwViolation({
|
|
603
|
+
id: ctx.id,
|
|
604
|
+
operation: "claim",
|
|
605
|
+
state
|
|
606
|
+
});
|
|
607
|
+
}
|
|
608
|
+
});
|
|
609
|
+
}
|
|
610
|
+
};
|
|
611
|
+
}
|
|
612
|
+
//#endregion
|
|
395
613
|
//#region src/plugins/lease.plugin.ts
|
|
396
614
|
function leasePlugin(options = {}) {
|
|
397
615
|
const statusField = options.statusField ?? "status";
|
|
@@ -783,224 +1001,6 @@ function observabilityPlugin(options) {
|
|
|
783
1001
|
};
|
|
784
1002
|
}
|
|
785
1003
|
//#endregion
|
|
786
|
-
//#region src/plugins/append-only.plugin.ts
|
|
787
|
-
/**
|
|
788
|
-
* Append-Only Plugin
|
|
789
|
-
*
|
|
790
|
-
* Turns a repository into an immutable-facts ledger: inserts land,
|
|
791
|
-
* everything else is refused at POLICY priority. The enforcement
|
|
792
|
-
* layer for event-sourced planes — flow's `StockEvent`,
|
|
793
|
-
* manufacturing's `ProductionEvent`, audit sinks, outbox archives —
|
|
794
|
-
* where a mutable row would silently corrupt the audit/time-travel
|
|
795
|
-
* story that the whole collection exists to provide.
|
|
796
|
-
*
|
|
797
|
-
* Grep-proof by construction: hand-fenced `update()`/`delete()`
|
|
798
|
-
* overrides only cover the verbs someone remembered to override; this
|
|
799
|
-
* plugin derives coverage from `OP_REGISTRY`, so every mutating op —
|
|
800
|
-
* including `claim`, `claimVersion`, `restore`, `bulkWrite`, and any
|
|
801
|
-
* op added to the registry later — is refused automatically.
|
|
802
|
-
*
|
|
803
|
-
* Allowed by default: `create`, `createMany` (appends). `getOrCreate`
|
|
804
|
-
* is registry-classified as a read and stays available — it can only
|
|
805
|
-
* INSERT or return the existing row, never mutate one, which is
|
|
806
|
-
* append-safe by definition (idempotency stores rely on it).
|
|
807
|
-
* Refused by default: `update`, `findOneAndUpdate`, `updateMany`,
|
|
808
|
-
* `delete`, `deleteMany`, `restore`, `claim`, `claimVersion`,
|
|
809
|
-
* `bulkWrite` (wholesale — mixed insert/update batches are ambiguous;
|
|
810
|
-
* append via `createMany` instead). Composed methods from other
|
|
811
|
-
* plugins (`incrementIfBelow`, lease ops) decompose into these
|
|
812
|
-
* registry ops internally, so the fence catches them too.
|
|
813
|
-
*
|
|
814
|
-
* TTL indexes are unaffected (server-side GC never touches the repo
|
|
815
|
-
* layer) — TTL-expiring an idempotency ledger still works. Raw
|
|
816
|
-
* `Model.*` driver calls bypass every plugin, this one included —
|
|
817
|
-
* the org-wide "never drop to Model.deleteOne" rule is what covers
|
|
818
|
-
* that hole, same as for tenant scope.
|
|
819
|
-
*
|
|
820
|
-
* **Escape hatches** (both audited via `after:append-only-bypass`):
|
|
821
|
-
* - Plugin-level `allow` — ops the DOMAIN legitimately needs, wired
|
|
822
|
-
* once at construction (e.g. `allow: ['deleteMany']` for a
|
|
823
|
-
* GDPR-erasure path the host gates behind admin roles).
|
|
824
|
-
* - Per-call `bypassAppendOnly: true` — deliberate one-off
|
|
825
|
-
* maintenance (backfill repair, migration), grep-auditable at the
|
|
826
|
-
* call site.
|
|
827
|
-
*
|
|
828
|
-
* `skipPlugins` is deliberately IGNORED — same policy as tenant scope
|
|
829
|
-
* and soft delete: integrity fences don't have a convenience bypass.
|
|
830
|
-
*
|
|
831
|
-
* @example
|
|
832
|
-
* const events = new Repository(StockEventModel, [
|
|
833
|
-
* multiTenantPlugin({ ... }),
|
|
834
|
-
* appendOnlyPlugin(),
|
|
835
|
-
* ]);
|
|
836
|
-
* await events.create({ ... }); // OK
|
|
837
|
-
* await events.update(id, { ... }); // throws 405 APPEND_ONLY_VIOLATION
|
|
838
|
-
* await events.deleteMany({ ... }, { bypassAppendOnly: true }); // audited bypass
|
|
839
|
-
*/
|
|
840
|
-
const ALWAYS_ALLOWED = /* @__PURE__ */ new Set(["create", "createMany"]);
|
|
841
|
-
function appendOnlyPlugin(options = {}) {
|
|
842
|
-
const allowed = /* @__PURE__ */ new Set([...ALWAYS_ALLOWED, ...options.allow ?? []]);
|
|
843
|
-
return {
|
|
844
|
-
name: "appendOnly",
|
|
845
|
-
apply(repo) {
|
|
846
|
-
const modelName = repo.Model?.modelName ?? "model";
|
|
847
|
-
for (const [op, meta] of Object.entries(OP_REGISTRY)) {
|
|
848
|
-
if (!meta.mutates || allowed.has(op)) continue;
|
|
849
|
-
repo.on(`before:${op}`, (context) => {
|
|
850
|
-
if (context.bypassAppendOnly === true) {
|
|
851
|
-
repo.emit("after:append-only-bypass", {
|
|
852
|
-
operation: op,
|
|
853
|
-
context
|
|
854
|
-
});
|
|
855
|
-
return;
|
|
856
|
-
}
|
|
857
|
-
throw createError(405, `${modelName} is append-only: '${op}' is not permitted. Rows on this collection are immutable facts (event ledger). Append a compensating row instead; for deliberate maintenance pass bypassAppendOnly: true (audited via 'after:append-only-bypass').`, {
|
|
858
|
-
code: "APPEND_ONLY_VIOLATION",
|
|
859
|
-
meta: {
|
|
860
|
-
operation: op,
|
|
861
|
-
model: modelName
|
|
862
|
-
}
|
|
863
|
-
});
|
|
864
|
-
}, { priority: options.priority ?? HOOK_PRIORITY.POLICY });
|
|
865
|
-
}
|
|
866
|
-
}
|
|
867
|
-
};
|
|
868
|
-
}
|
|
869
|
-
//#endregion
|
|
870
|
-
//#region src/plugins/immutable-states.plugin.ts
|
|
871
|
-
/** Ops this plugin has an explicit strategy for (see module doc). */
|
|
872
|
-
const STRATEGY_OPS = /* @__PURE__ */ new Set([
|
|
873
|
-
"create",
|
|
874
|
-
"createMany",
|
|
875
|
-
"update",
|
|
876
|
-
"delete",
|
|
877
|
-
"claimVersion",
|
|
878
|
-
"restore",
|
|
879
|
-
"findOneAndUpdate",
|
|
880
|
-
"updateMany",
|
|
881
|
-
"deleteMany",
|
|
882
|
-
"bulkWrite",
|
|
883
|
-
"claim"
|
|
884
|
-
]);
|
|
885
|
-
function immutableStatesPlugin(options) {
|
|
886
|
-
const field = options.field ?? "state";
|
|
887
|
-
const internalFlag = options.internalFlag ?? "_immutableInternal";
|
|
888
|
-
const frozen = new Set(options.states);
|
|
889
|
-
const throwViolation = (ctx) => {
|
|
890
|
-
if (options.errorFactory) throw options.errorFactory(ctx);
|
|
891
|
-
throw createError(403, `Document ${String(ctx.id)} is in immutable state${ctx.state !== void 0 ? ` '${ctx.state}'` : ""} — '${ctx.operation}' is not permitted. Frozen rows change only through the owning engine's transition verbs.`, {
|
|
892
|
-
code: "IMMUTABLE_STATE_VIOLATION",
|
|
893
|
-
meta: { ...ctx }
|
|
894
|
-
});
|
|
895
|
-
};
|
|
896
|
-
return {
|
|
897
|
-
name: "immutableStates",
|
|
898
|
-
apply(repo) {
|
|
899
|
-
const unhandled = Object.entries(OP_REGISTRY).filter(([op, meta]) => meta.mutates && !STRATEGY_OPS.has(op)).map(([op]) => op);
|
|
900
|
-
if (unhandled.length > 0) throw createError(500, `immutableStatesPlugin has no strategy for mutating op(s): ${unhandled.join(", ")}. Mongokit added write ops this plugin predates — extend the plugin before using it.`, {
|
|
901
|
-
code: "IMMUTABLE_STATES_UNHANDLED_OPS",
|
|
902
|
-
meta: { unhandled }
|
|
903
|
-
});
|
|
904
|
-
const Model = repo.Model;
|
|
905
|
-
const getState = (doc) => field.split(".").reduce((acc, key) => acc?.[key], doc);
|
|
906
|
-
const scoped = (base, ctx) => {
|
|
907
|
-
const q = { ...base };
|
|
908
|
-
if (options.tenantField && ctx.query && options.tenantField in ctx.query) q[options.tenantField] = ctx.query[options.tenantField];
|
|
909
|
-
return q;
|
|
910
|
-
};
|
|
911
|
-
/** Raw-Model read — never re-enters the repo hook pipeline. */
|
|
912
|
-
const readStateById = async (ctx) => {
|
|
913
|
-
if (ctx.id === void 0 || ctx.id === null) return void 0;
|
|
914
|
-
const doc = await Model.findOne(scoped({ _id: ctx.id }, ctx)).select({ [field]: 1 }).lean();
|
|
915
|
-
return doc ? getState(doc) : void 0;
|
|
916
|
-
};
|
|
917
|
-
const guardById = (op) => async (ctx) => {
|
|
918
|
-
if (ctx[internalFlag]) return;
|
|
919
|
-
const state = await readStateById(ctx);
|
|
920
|
-
if (state !== void 0 && frozen.has(state)) throwViolation({
|
|
921
|
-
id: ctx.id,
|
|
922
|
-
operation: op,
|
|
923
|
-
state
|
|
924
|
-
});
|
|
925
|
-
};
|
|
926
|
-
for (const op of [
|
|
927
|
-
"update",
|
|
928
|
-
"delete",
|
|
929
|
-
"claimVersion",
|
|
930
|
-
"restore"
|
|
931
|
-
]) repo.on(`before:${op}`, guardById(op));
|
|
932
|
-
repo.on("before:findOneAndUpdate", async (ctx) => {
|
|
933
|
-
if (ctx[internalFlag]) return;
|
|
934
|
-
const filter = ctx.query ?? {};
|
|
935
|
-
const doc = await Model.findOne(filter).select({
|
|
936
|
-
[field]: 1,
|
|
937
|
-
_id: 1
|
|
938
|
-
}).lean();
|
|
939
|
-
if (!doc) return;
|
|
940
|
-
const state = getState(doc);
|
|
941
|
-
if (state !== void 0 && frozen.has(state)) throwViolation({
|
|
942
|
-
id: doc._id,
|
|
943
|
-
operation: "findOneAndUpdate",
|
|
944
|
-
state
|
|
945
|
-
});
|
|
946
|
-
});
|
|
947
|
-
const guardMany = (op) => async (ctx) => {
|
|
948
|
-
if (ctx[internalFlag]) return;
|
|
949
|
-
const filter = ctx.query ?? {};
|
|
950
|
-
const hit = await Model.exists({ $and: [filter, { [field]: { $in: [...frozen] } }] });
|
|
951
|
-
if (hit) throwViolation({
|
|
952
|
-
id: hit._id,
|
|
953
|
-
operation: op,
|
|
954
|
-
state: void 0
|
|
955
|
-
});
|
|
956
|
-
};
|
|
957
|
-
repo.on("before:updateMany", guardMany("updateMany"));
|
|
958
|
-
repo.on("before:deleteMany", guardMany("deleteMany"));
|
|
959
|
-
repo.on("before:bulkWrite", (ctx) => {
|
|
960
|
-
if (ctx[internalFlag]) return;
|
|
961
|
-
throwViolation({
|
|
962
|
-
id: void 0,
|
|
963
|
-
operation: "bulkWrite",
|
|
964
|
-
state: void 0
|
|
965
|
-
});
|
|
966
|
-
});
|
|
967
|
-
repo.on("before:claim", async (rawCtx) => {
|
|
968
|
-
const ctx = rawCtx;
|
|
969
|
-
if (ctx[internalFlag]) return;
|
|
970
|
-
const t = ctx.transition;
|
|
971
|
-
if (!t) return;
|
|
972
|
-
if ((t.field ?? "status") === field) {
|
|
973
|
-
const fromSpec = t.from;
|
|
974
|
-
if (!(Array.isArray(fromSpec) ? fromSpec.some((f) => frozen.has(String(f))) : frozen.has(String(fromSpec)))) return;
|
|
975
|
-
if (options.allowClaim?.({
|
|
976
|
-
id: ctx.id,
|
|
977
|
-
transition: t,
|
|
978
|
-
data: ctx.data
|
|
979
|
-
})) return;
|
|
980
|
-
throwViolation({
|
|
981
|
-
id: ctx.id,
|
|
982
|
-
operation: "claim",
|
|
983
|
-
state: void 0
|
|
984
|
-
});
|
|
985
|
-
}
|
|
986
|
-
const state = await readStateById(ctx);
|
|
987
|
-
if (state !== void 0 && frozen.has(state)) {
|
|
988
|
-
if (options.allowClaim?.({
|
|
989
|
-
id: ctx.id,
|
|
990
|
-
transition: t,
|
|
991
|
-
data: ctx.data
|
|
992
|
-
})) return;
|
|
993
|
-
throwViolation({
|
|
994
|
-
id: ctx.id,
|
|
995
|
-
operation: "claim",
|
|
996
|
-
state
|
|
997
|
-
});
|
|
998
|
-
}
|
|
999
|
-
});
|
|
1000
|
-
}
|
|
1001
|
-
};
|
|
1002
|
-
}
|
|
1003
|
-
//#endregion
|
|
1004
1004
|
//#region src/plugins/subdocument.plugin.ts
|
|
1005
1005
|
/**
|
|
1006
1006
|
* Subdocument plugin for managing nested arrays
|
|
@@ -1272,4 +1272,4 @@ function uniqueField(field, errorMessage) {
|
|
|
1272
1272
|
};
|
|
1273
1273
|
}
|
|
1274
1274
|
//#endregion
|
|
1275
|
-
export { aggregateHelpersPlugin as _, uniqueField as a, subdocumentPlugin as c,
|
|
1275
|
+
export { aggregateHelpersPlugin as _, uniqueField as a, subdocumentPlugin as c, leasePlugin as d, immutableStatesPlugin as f, appendOnlyPlugin as g, cascadePlugin as h, requireField as i, observabilityPlugin as l, elasticSearchPlugin as m, blockIf as n, validationChainPlugin as o, fieldFilterPlugin as p, immutableField as r, createTenantContext as s, autoInject as t, mongoOperationsPlugin as u };
|
package/package.json
CHANGED