@cullet/erp-core 1.4.0 → 2.0.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/KIT_CONTEXT.md +7 -1
- package/dist/abac/index.d.cts +2 -2
- package/dist/abac/index.d.ts +2 -2
- package/dist/aggregate-version.cjs +14 -0
- package/dist/aggregate-version.cjs.map +1 -0
- package/dist/aggregate-version.js +9 -0
- package/dist/aggregate-version.js.map +1 -0
- package/dist/app-error.cjs +21 -6
- package/dist/app-error.cjs.map +1 -1
- package/dist/app-error.js +21 -6
- package/dist/app-error.js.map +1 -1
- package/dist/application/index.cjs +8 -4
- package/dist/application/index.d.cts +2 -2
- package/dist/application/index.d.ts +2 -2
- package/dist/application/index.js +1 -2
- package/dist/authorization-error.cjs +67 -17
- package/dist/authorization-error.cjs.map +1 -1
- package/dist/authorization-error.d.cts +6 -2
- package/dist/authorization-error.d.ts +6 -2
- package/dist/authorization-error.js +50 -18
- package/dist/authorization-error.js.map +1 -1
- package/dist/authorizer.port.d.cts +18 -3
- package/dist/authorizer.port.d.ts +18 -3
- package/dist/composite-authorizer.d.cts +63 -10
- package/dist/composite-authorizer.d.ts +63 -10
- package/dist/condition-evaluator.cjs +117 -172
- package/dist/condition-evaluator.cjs.map +1 -1
- package/dist/condition-evaluator.js +118 -167
- package/dist/condition-evaluator.js.map +1 -1
- package/dist/core-config.d.cts +53 -6
- package/dist/core-config.d.ts +53 -6
- package/dist/decorate.cjs +14 -0
- package/dist/decorate.js +9 -0
- package/dist/domain/index.cjs +107 -2
- package/dist/domain/index.cjs.map +1 -0
- package/dist/domain/index.d.cts +25 -1
- package/dist/domain/index.d.ts +25 -1
- package/dist/domain/index.js +99 -3
- package/dist/domain/index.js.map +1 -0
- package/dist/domain-event-contracts.cjs +12 -8
- package/dist/domain-event-contracts.cjs.map +1 -1
- package/dist/domain-event-contracts.d.cts +29 -4
- package/dist/domain-event-contracts.d.ts +29 -4
- package/dist/domain-event-contracts.js +11 -7
- package/dist/domain-event-contracts.js.map +1 -1
- package/dist/domain-exception.cjs +2 -1
- package/dist/domain-exception.cjs.map +1 -1
- package/dist/domain-exception.js +2 -1
- package/dist/domain-exception.js.map +1 -1
- package/dist/errors/index.cjs +3 -2
- package/dist/errors/index.d.cts +1 -1
- package/dist/errors/index.d.ts +1 -1
- package/dist/errors/index.js +3 -2
- package/dist/exceptions/index.cjs +2 -2
- package/dist/exceptions/index.d.cts +1 -2
- package/dist/exceptions/index.d.ts +1 -2
- package/dist/exceptions/index.js +2 -2
- package/dist/gate-engine-registry.cjs.map +1 -1
- package/dist/gate-engine-registry.d.cts +1 -1
- package/dist/gate-engine-registry.d.ts +1 -1
- package/dist/gate-engine-registry.js.map +1 -1
- package/dist/gate-v1-payload.schema.cjs +11 -6
- package/dist/gate-v1-payload.schema.cjs.map +1 -1
- package/dist/gate-v1-payload.schema.js +11 -6
- package/dist/gate-v1-payload.schema.js.map +1 -1
- package/dist/hashing.cjs +2 -44
- package/dist/hashing.cjs.map +1 -1
- package/dist/hashing.js +3 -39
- package/dist/hashing.js.map +1 -1
- package/dist/immutable.cjs +25 -12
- package/dist/immutable.cjs.map +1 -1
- package/dist/immutable.js +24 -11
- package/dist/immutable.js.map +1 -1
- package/dist/index.cjs +15 -11
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +9 -10
- package/dist/index.d.ts +9 -10
- package/dist/index.js +8 -9
- package/dist/index.js.map +1 -1
- package/dist/invalid-state-transition-exception.cjs +6 -6
- package/dist/invalid-state-transition-exception.cjs.map +1 -1
- package/dist/invalid-state-transition-exception.js +6 -6
- package/dist/invalid-state-transition-exception.js.map +1 -1
- package/dist/invariant-violation-exception.cjs +2 -2
- package/dist/invariant-violation-exception.cjs.map +1 -1
- package/dist/invariant-violation-exception.js +2 -2
- package/dist/invariant-violation-exception.js.map +1 -1
- package/dist/not-found-error.cjs +6 -4
- package/dist/not-found-error.cjs.map +1 -1
- package/dist/not-found-error.js +6 -4
- package/dist/not-found-error.js.map +1 -1
- package/dist/outcome.cjs +5 -5
- package/dist/outcome.cjs.map +1 -1
- package/dist/outcome.js +5 -5
- package/dist/outcome.js.map +1 -1
- package/dist/parse-gate-payload.d.cts +1 -1
- package/dist/parse-gate-payload.d.ts +1 -1
- package/dist/path.d.cts +2 -2
- package/dist/path.d.ts +2 -2
- package/dist/plugin.cjs +23 -13
- package/dist/plugin.cjs.map +1 -1
- package/dist/plugin.d.cts +22 -8
- package/dist/plugin.d.ts +22 -8
- package/dist/plugin.js +23 -13
- package/dist/plugin.js.map +1 -1
- package/dist/policies/engines/index.d.cts +1 -1
- package/dist/policies/engines/index.d.ts +1 -1
- package/dist/policies/engines/v1/gate/index.d.cts +3 -13
- package/dist/policies/engines/v1/gate/index.d.ts +3 -13
- package/dist/policies/index.d.cts +1 -1
- package/dist/policies/index.d.ts +1 -1
- package/dist/policy-bridge.cjs +30 -2
- package/dist/policy-bridge.cjs.map +1 -1
- package/dist/policy-bridge.d.cts +18 -0
- package/dist/policy-bridge.d.ts +18 -0
- package/dist/policy-bridge.js +30 -2
- package/dist/policy-bridge.js.map +1 -1
- package/dist/policy-service.cjs +41 -46
- package/dist/policy-service.cjs.map +1 -1
- package/dist/policy-service.d.cts +72 -9
- package/dist/policy-service.d.ts +72 -9
- package/dist/policy-service.js +43 -48
- package/dist/policy-service.js.map +1 -1
- package/dist/requested-by.cjs +4 -4
- package/dist/requested-by.cjs.map +1 -1
- package/dist/requested-by.js +2 -2
- package/dist/requested-by.js.map +1 -1
- package/dist/result.cjs +29 -1
- package/dist/result.cjs.map +1 -1
- package/dist/result.d.cts +12 -0
- package/dist/result.d.ts +12 -0
- package/dist/result.js +29 -1
- package/dist/result.js.map +1 -1
- package/dist/rule.cjs +94 -24
- package/dist/rule.cjs.map +1 -1
- package/dist/rule.js +94 -24
- package/dist/rule.js.map +1 -1
- package/dist/ruleset-registry.cjs +19 -0
- package/dist/ruleset-registry.cjs.map +1 -1
- package/dist/ruleset-registry.js +19 -0
- package/dist/ruleset-registry.js.map +1 -1
- package/dist/stable-stringify.cjs +87 -0
- package/dist/stable-stringify.cjs.map +1 -0
- package/dist/stable-stringify.js +76 -0
- package/dist/stable-stringify.js.map +1 -0
- package/dist/temporal-snapshot.d.cts +87 -0
- package/dist/temporal-snapshot.d.ts +87 -0
- package/dist/temporal-use-case.cjs +174 -16
- package/dist/temporal-use-case.cjs.map +1 -1
- package/dist/temporal-use-case.d.cts +82 -44
- package/dist/temporal-use-case.d.ts +82 -44
- package/dist/temporal-use-case.js +167 -15
- package/dist/temporal-use-case.js.map +1 -1
- package/dist/unexpected-error.cjs +4 -2
- package/dist/unexpected-error.cjs.map +1 -1
- package/dist/unexpected-error.js +4 -2
- package/dist/unexpected-error.js.map +1 -1
- package/dist/uuid-identifier.cjs +141 -8
- package/dist/uuid-identifier.cjs.map +1 -1
- package/dist/uuid-identifier.d.cts +26 -7
- package/dist/uuid-identifier.d.ts +26 -7
- package/dist/uuid-identifier.js +135 -8
- package/dist/uuid-identifier.js.map +1 -1
- package/dist/uuid.cjs +23 -0
- package/dist/uuid.cjs.map +1 -0
- package/dist/uuid.js +18 -0
- package/dist/uuid.js.map +1 -0
- package/dist/validation-code.cjs +9 -0
- package/dist/validation-code.cjs.map +1 -1
- package/dist/validation-code.d.cts +3 -0
- package/dist/validation-code.d.ts +3 -0
- package/dist/validation-code.js +9 -0
- package/dist/validation-code.js.map +1 -1
- package/dist/validation-error.cjs +42 -67
- package/dist/validation-error.cjs.map +1 -1
- package/dist/validation-error.d.cts +29 -8
- package/dist/validation-error.d.ts +29 -8
- package/dist/validation-error.js +41 -66
- package/dist/validation-error.js.map +1 -1
- package/dist/validation-exception.cjs +17 -6
- package/dist/validation-exception.cjs.map +1 -1
- package/dist/validation-exception.d.cts +33 -9
- package/dist/validation-exception.d.ts +33 -9
- package/dist/validation-exception.js +17 -6
- package/dist/validation-exception.js.map +1 -1
- package/dist/validation-field.cjs +3 -0
- package/dist/validation-field.cjs.map +1 -1
- package/dist/validation-field.d.cts +1 -0
- package/dist/validation-field.d.ts +1 -0
- package/dist/validation-field.js +3 -0
- package/dist/validation-field.js.map +1 -1
- package/dist/value-object-ruleset.contracts.d.cts +10 -0
- package/dist/value-object-ruleset.contracts.d.ts +10 -0
- package/dist/value-object.cjs +23 -4
- package/dist/value-object.cjs.map +1 -1
- package/dist/value-object.d.cts +25 -1
- package/dist/value-object.d.ts +25 -1
- package/dist/value-object.js +23 -4
- package/dist/value-object.js.map +1 -1
- package/dist/version.d.cts +27 -0
- package/dist/version.d.ts +27 -0
- package/dist/versioning/index.d.cts +2 -2
- package/dist/versioning/index.d.ts +2 -2
- package/meta.json +5 -2
- package/package.json +1 -1
- package/src/core/abac/authorizer.ts +60 -10
- package/src/core/abac/domain/policy-set.ts +52 -5
- package/src/core/abac/domain/rule.ts +28 -16
- package/src/core/abac/index.ts +7 -1
- package/src/core/application/commands/command.ts +14 -1
- package/src/core/application/commands/requested-by.ts +13 -5
- package/src/core/application/index.ts +2 -1
- package/src/core/application/policy-error-mapper.ts +6 -0
- package/src/core/application/ports/index.ts +7 -0
- package/src/core/application/ports/temporal-repository.port.ts +35 -2
- package/src/core/application/queries/index.ts +1 -1
- package/src/core/application/queries/query.ts +25 -3
- package/src/core/application/temporal/temporal-use-case.ts +31 -4
- package/src/core/application/use-case.ts +45 -8
- package/src/core/config/core-config.ts +46 -25
- package/src/core/config/index.ts +1 -0
- package/src/core/config/policy-reporter.ts +32 -1
- package/src/core/domain/entity.ts +51 -8
- package/src/core/domain/rulesets/entity-ruleset.contracts.ts +0 -2
- package/src/core/domain/rulesets/ruleset-registry.ts +24 -0
- package/src/core/domain/rulesets/value-object-ruleset.contracts.ts +1 -7
- package/src/core/domain/temporal/half-open-interval.ts +34 -0
- package/src/core/domain/temporal/temporal-snapshot.ts +13 -2
- package/src/core/domain/temporal/transaction-time.ts +19 -15
- package/src/core/domain/temporal/valid-time.ts +20 -15
- package/src/core/domain/uuid-identifier.ts +6 -11
- package/src/core/domain/value-object.ts +29 -3
- package/src/core/errors/authentication-error.ts +5 -31
- package/src/core/errors/authorization-error.ts +12 -20
- package/src/core/errors/business-rule-violation-error.ts +4 -1
- package/src/core/errors/idempotency-error.ts +5 -2
- package/src/core/errors/index.ts +4 -0
- package/src/core/errors/integration-error.ts +4 -24
- package/src/core/errors/legacy-incompatible-error.ts +1 -0
- package/src/core/errors/not-found-error.ts +4 -1
- package/src/core/errors/temporal-error.ts +22 -20
- package/src/core/errors/unexpected-error.ts +5 -1
- package/src/core/errors/utils/factory-helpers.ts +70 -0
- package/src/core/errors/utils/index.ts +5 -0
- package/src/core/errors/utils/json-safe.ts +35 -12
- package/src/core/errors/validation-error.ts +4 -1
- package/src/core/exceptions/business-rule-violation-exception.ts +2 -1
- package/src/core/exceptions/domain-exception.ts +9 -1
- package/src/core/exceptions/entity-not-found-exception.ts +5 -1
- package/src/core/exceptions/invalid-state-transition-exception.ts +5 -2
- package/src/core/exceptions/invariant-violation-exception.ts +2 -2
- package/src/core/exceptions/validation-code.ts +14 -0
- package/src/core/exceptions/validation-exception.ts +44 -5
- package/src/core/exceptions/validation-field.ts +11 -1
- package/src/core/plugins/plugin.ts +25 -15
- package/src/core/plugins/types.ts +4 -2
- package/src/core/policies/asof/asof.ts +12 -0
- package/src/core/policies/catalog/policy-catalog-entry.ts +3 -1
- package/src/core/policies/catalog/policy-catalog.ts +5 -1
- package/src/core/policies/context/context-builder.ts +20 -0
- package/src/core/policies/context/context-resolver.ts +5 -0
- package/src/core/policies/context/context-seed.ts +11 -0
- package/src/core/policies/defs/in-memory-policy-definition-repo.ts +0 -2
- package/src/core/policies/engines/parse-gate-payload.ts +9 -0
- package/src/core/policies/engines/v1/condition-date-operands.ts +112 -0
- package/src/core/policies/engines/v1/condition-evaluator.ts +120 -291
- package/src/core/policies/engines/v1/condition-schema.ts +23 -19
- package/src/core/policies/engines/v1/condition-types.ts +18 -11
- package/src/core/policies/index.ts +4 -6
- package/src/core/policies/service/policy-service.ts +46 -35
- package/src/core/policies/utils/hash.ts +5 -69
- package/src/core/policies/utils/result.ts +1 -1
- package/src/core/rbac/access-request.ts +12 -2
- package/src/core/rbac/authorizer.ts +8 -1
- package/src/core/rbac/domain/role.ts +20 -0
- package/src/core/rbac/domain/scope.ts +6 -1
- package/src/core/result/index.ts +5 -0
- package/src/core/result/outcome.ts +5 -7
- package/src/core/result/result.ts +34 -1
- package/src/core/shared/hashing.ts +6 -57
- package/src/core/shared/immutable.ts +22 -4
- package/src/core/shared/stable-stringify.ts +144 -0
- package/src/core/shared/uuid.ts +16 -0
- package/src/core/versioning/domain-event-contracts.ts +43 -15
- package/src/core/versioning/index.ts +1 -0
- package/src/core/versioning/version.ts +30 -1
- package/src/domain/index.ts +5 -0
- package/src/examples/rulesets/entity/order-creation-rules-v1.ts +1 -1
- package/src/examples/rulesets/entity/order-invariants-v1.ts +2 -4
- package/src/examples/rulesets/entity/order-invariants-v2.ts +2 -4
- package/src/examples/rulesets/value-object/cpf-rules-v1.ts +2 -4
- package/src/examples/rulesets/value-object/cpf-rules-v2.ts +2 -4
- package/src/examples/rulesets/value-object/person-name-rules-v1.ts +2 -4
- package/src/examples/rulesets/value-object/person-name-rules-v2.ts +2 -4
- package/src/result/index.ts +7 -2
- package/src/version.ts +1 -1
- package/dist/domain-exception.d.cts +0 -7
- package/dist/domain-exception.d.ts +0 -7
- package/dist/entity.cjs +0 -126
- package/dist/entity.cjs.map +0 -1
- package/dist/entity.js +0 -115
- package/dist/entity.js.map +0 -1
- package/dist/use-case.cjs +0 -96
- package/dist/use-case.cjs.map +0 -1
- package/dist/use-case.js +0 -91
- package/dist/use-case.js.map +0 -1
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import { isValidDate } from "./temporal-guards.js";
|
|
2
|
+
|
|
3
|
+
const CIRCULAR_STRUCTURE_MESSAGE =
|
|
4
|
+
"Cannot stably stringify a circular structure";
|
|
5
|
+
|
|
6
|
+
// `seen` holds the current traversal branch so true cycles are rejected while
|
|
7
|
+
// repeated (acyclic) references are still serialized — matching JSON.stringify.
|
|
8
|
+
function sortKeysDeep(value: unknown, seen: WeakSet<object>): unknown {
|
|
9
|
+
if (Array.isArray(value)) {
|
|
10
|
+
if (seen.has(value)) {
|
|
11
|
+
throw new TypeError(CIRCULAR_STRUCTURE_MESSAGE);
|
|
12
|
+
}
|
|
13
|
+
seen.add(value);
|
|
14
|
+
const mapped = value.map((item) => sortKeysDeep(item, seen));
|
|
15
|
+
seen.delete(value);
|
|
16
|
+
return mapped;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
if (value instanceof Date) {
|
|
20
|
+
return value;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
if (value && typeof value === "object") {
|
|
24
|
+
if (seen.has(value)) {
|
|
25
|
+
throw new TypeError(CIRCULAR_STRUCTURE_MESSAGE);
|
|
26
|
+
}
|
|
27
|
+
seen.add(value);
|
|
28
|
+
const record = value as Record<string, unknown>;
|
|
29
|
+
const ordered = Object.create(null) as Record<string, unknown>;
|
|
30
|
+
|
|
31
|
+
for (const key of Object.keys(record).sort()) {
|
|
32
|
+
ordered[key] = sortKeysDeep(record[key], seen);
|
|
33
|
+
}
|
|
34
|
+
seen.delete(value);
|
|
35
|
+
|
|
36
|
+
return ordered;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return value;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Deterministic JSON string with object keys sorted recursively, so key order
|
|
44
|
+
* does not affect the output.
|
|
45
|
+
*
|
|
46
|
+
* Follows `JSON.stringify` semantics for the values it serializes: `undefined`,
|
|
47
|
+
* functions and symbols are dropped, non-finite numbers become `null`, `bigint`
|
|
48
|
+
* throws, and `Map`/`Set` serialize as `{}`. Distinct payloads can therefore
|
|
49
|
+
* collapse to the same string — this is fine for the structural-equality use it
|
|
50
|
+
* serves (comparing two value objects), but **not** for hashing. For a
|
|
51
|
+
* collision-resistant canonical form use {@link canonicalStringify}, which
|
|
52
|
+
* rejects those lossy values up front.
|
|
53
|
+
*
|
|
54
|
+
* @throws {TypeError} On circular references (mirroring `JSON.stringify`).
|
|
55
|
+
*/
|
|
56
|
+
function stableStringify(value: unknown): string {
|
|
57
|
+
return JSON.stringify(sortKeysDeep(value, new WeakSet<object>()));
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Rejects every value `JSON.stringify` would silently drop or coerce, so
|
|
61
|
+
// semantically different payloads can never collapse to the same canonical
|
|
62
|
+
// string. Owned here (rather than in policies) so `payloadHash` and
|
|
63
|
+
// `PolicyHashing` share one strict definition instead of each re-implementing
|
|
64
|
+
// the deep guard.
|
|
65
|
+
function assertHashable(
|
|
66
|
+
value: unknown,
|
|
67
|
+
path: string,
|
|
68
|
+
seen: WeakSet<object>,
|
|
69
|
+
): void {
|
|
70
|
+
if (value === null) {
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const type = typeof value;
|
|
75
|
+
|
|
76
|
+
if (type === "undefined") {
|
|
77
|
+
throw new TypeError(
|
|
78
|
+
`canonicalStringify does not accept undefined values (at ${path})`,
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (type === "number" && !Number.isFinite(value)) {
|
|
83
|
+
throw new TypeError(
|
|
84
|
+
`canonicalStringify does not accept non-finite numbers (at ${path}, value: ${String(value)})`,
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (type === "bigint" || type === "function" || type === "symbol") {
|
|
89
|
+
throw new TypeError(
|
|
90
|
+
`canonicalStringify does not accept ${type} values (at ${path})`,
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (type !== "object") {
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (value instanceof Date) {
|
|
99
|
+
if (!isValidDate(value)) {
|
|
100
|
+
throw new TypeError(
|
|
101
|
+
`canonicalStringify does not accept Invalid Date (at ${path})`,
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (seen.has(value as object)) {
|
|
108
|
+
throw new TypeError(
|
|
109
|
+
`canonicalStringify does not accept circular references (at ${path})`,
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
seen.add(value as object);
|
|
113
|
+
|
|
114
|
+
if (Array.isArray(value)) {
|
|
115
|
+
value.forEach((item, index) => {
|
|
116
|
+
assertHashable(item, `${path}[${index}]`, seen);
|
|
117
|
+
});
|
|
118
|
+
} else {
|
|
119
|
+
for (const [key, nested] of Object.entries(
|
|
120
|
+
value as Record<string, unknown>,
|
|
121
|
+
)) {
|
|
122
|
+
assertHashable(nested, `${path}.${key}`, seen);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
seen.delete(value as object);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Strict counterpart to {@link stableStringify}: same deterministic output, but
|
|
131
|
+
* throws on any value `JSON.stringify` would drop or coerce (`undefined`,
|
|
132
|
+
* non-finite numbers, `bigint`, functions, symbols, `Invalid Date`) and on
|
|
133
|
+
* circular references. Use this whenever the string feeds a hash or integrity
|
|
134
|
+
* check, so distinct payloads cannot collide.
|
|
135
|
+
*
|
|
136
|
+
* @throws {TypeError} On lossy values or circular references.
|
|
137
|
+
*/
|
|
138
|
+
function canonicalStringify(value: unknown): string {
|
|
139
|
+
assertHashable(value, "$", new WeakSet<object>());
|
|
140
|
+
|
|
141
|
+
return stableStringify(value);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export { assertHashable, canonicalStringify, stableStringify };
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Canonical RFC-4122 / RFC-9562 UUID, matched case-insensitively. Accepts
|
|
3
|
+
* versions 1–8 (variant 8/9/a/b) — notably UUIDv7, the time-ordered default for
|
|
4
|
+
* sortable primary keys in modern schemas. The nil (`0000…`) and max (`ffff…`)
|
|
5
|
+
* UUIDs are intentionally rejected: they are sentinels, not real identities, and
|
|
6
|
+
* a domain id should never carry one.
|
|
7
|
+
*
|
|
8
|
+
* The single source of truth for the UUID format — `UuidIdentifier` (domain ids)
|
|
9
|
+
* and `RequestedBy` (command actor) both validate against this pattern, so an
|
|
10
|
+
* identity accepted at the application boundary is always one the domain also
|
|
11
|
+
* considers valid.
|
|
12
|
+
*/
|
|
13
|
+
const UUID_PATTERN =
|
|
14
|
+
/^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
15
|
+
|
|
16
|
+
export { UUID_PATTERN };
|
|
@@ -1,27 +1,52 @@
|
|
|
1
|
-
import { UseCase } from "../application/use-case.js";
|
|
2
|
-
import { Entity } from "../domain/entity.js";
|
|
3
|
-
import { ValueObject } from "../domain/value-object.js";
|
|
4
1
|
import { assertValidAggregateVersion } from "../shared/aggregate-version.js";
|
|
2
|
+
import { deepFreeze } from "../shared/immutable.js";
|
|
5
3
|
import { type ContractVersion } from "./version.js";
|
|
6
4
|
|
|
5
|
+
/** Anything carrying a `CONTRACT_VERSION` — a class decorated with `@version`. */
|
|
6
|
+
interface ContractVersioned {
|
|
7
|
+
readonly CONTRACT_VERSION: ContractVersion;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* The classes whose contract version should be recorded on the envelope.
|
|
12
|
+
*
|
|
13
|
+
* Pass the **concrete** aggregate/value-object/use-case class (not the base),
|
|
14
|
+
* so the envelope records the version the caller actually annotated with
|
|
15
|
+
* `@version`. A base class handed here would report only the framework's
|
|
16
|
+
* version, not the concrete shape that produced the event.
|
|
17
|
+
*/
|
|
7
18
|
interface DomainEventContractSelection {
|
|
8
|
-
readonly entity?:
|
|
9
|
-
readonly valueObject?:
|
|
10
|
-
readonly useCase?:
|
|
19
|
+
readonly entity?: ContractVersioned;
|
|
20
|
+
readonly valueObject?: ContractVersioned;
|
|
21
|
+
readonly useCase?: ContractVersioned;
|
|
11
22
|
}
|
|
12
23
|
|
|
24
|
+
// Wire-format shape: `snake_case` keys, deliberately distinct from the
|
|
25
|
+
// `camelCase` builder input above — these travel in the serialized event.
|
|
13
26
|
interface DomainEventContractVersions {
|
|
14
27
|
readonly entity_contract?: ContractVersion;
|
|
15
28
|
readonly value_object_contract?: ContractVersion;
|
|
16
29
|
readonly use_case_contract?: ContractVersion;
|
|
17
30
|
}
|
|
18
31
|
|
|
32
|
+
type MutableContractVersions = {
|
|
33
|
+
-readonly [K in keyof DomainEventContractVersions]: DomainEventContractVersions[K];
|
|
34
|
+
};
|
|
35
|
+
|
|
19
36
|
interface CreateDomainEventEnvelopeInput<TPayload> {
|
|
20
37
|
readonly aggregateVersion: number;
|
|
21
38
|
readonly payload: TPayload;
|
|
22
39
|
readonly contracts: DomainEventContractSelection;
|
|
23
40
|
}
|
|
24
41
|
|
|
42
|
+
/**
|
|
43
|
+
* A deliberately minimal domain-event envelope: it carries only the aggregate
|
|
44
|
+
* version and the structural contract versions needed for state/event
|
|
45
|
+
* migration. It intentionally omits `eventId`, `eventType`, `occurredAt` and
|
|
46
|
+
* correlation ids — those require a clock/id source and belong to the
|
|
47
|
+
* consumer's event infrastructure. Treat this as a versioning core to extend,
|
|
48
|
+
* not a complete event record.
|
|
49
|
+
*/
|
|
25
50
|
interface DomainEventEnvelope<TPayload> extends DomainEventContractVersions {
|
|
26
51
|
readonly aggregate_version: number;
|
|
27
52
|
readonly payload: TPayload;
|
|
@@ -30,33 +55,35 @@ interface DomainEventEnvelope<TPayload> extends DomainEventContractVersions {
|
|
|
30
55
|
function buildDomainEventContractVersions(
|
|
31
56
|
selection: DomainEventContractSelection,
|
|
32
57
|
): DomainEventContractVersions {
|
|
33
|
-
const contractVersions: {
|
|
34
|
-
entity_contract?: ContractVersion;
|
|
35
|
-
value_object_contract?: ContractVersion;
|
|
36
|
-
use_case_contract?: ContractVersion;
|
|
37
|
-
} = {};
|
|
58
|
+
const contractVersions: MutableContractVersions = {};
|
|
38
59
|
|
|
39
60
|
if (selection.entity) {
|
|
40
|
-
contractVersions.entity_contract =
|
|
61
|
+
contractVersions.entity_contract = selection.entity.CONTRACT_VERSION;
|
|
41
62
|
}
|
|
42
63
|
|
|
43
64
|
if (selection.valueObject) {
|
|
44
|
-
contractVersions.value_object_contract =
|
|
65
|
+
contractVersions.value_object_contract =
|
|
66
|
+
selection.valueObject.CONTRACT_VERSION;
|
|
45
67
|
}
|
|
46
68
|
|
|
47
69
|
if (selection.useCase) {
|
|
48
|
-
contractVersions.use_case_contract =
|
|
70
|
+
contractVersions.use_case_contract = selection.useCase.CONTRACT_VERSION;
|
|
49
71
|
}
|
|
50
72
|
|
|
51
73
|
return Object.freeze(contractVersions);
|
|
52
74
|
}
|
|
53
75
|
|
|
76
|
+
/**
|
|
77
|
+
* Builds a frozen domain-event envelope. The whole envelope is deep-frozen,
|
|
78
|
+
* so the `payload` handed in becomes immutable too — an event is an immutable
|
|
79
|
+
* fact, and the caller must not mutate the payload after wrapping it.
|
|
80
|
+
*/
|
|
54
81
|
function createDomainEventEnvelope<TPayload>(
|
|
55
82
|
input: CreateDomainEventEnvelopeInput<TPayload>,
|
|
56
83
|
): DomainEventEnvelope<TPayload> {
|
|
57
84
|
assertValidAggregateVersion(input.aggregateVersion);
|
|
58
85
|
|
|
59
|
-
return
|
|
86
|
+
return deepFreeze({
|
|
60
87
|
aggregate_version: input.aggregateVersion,
|
|
61
88
|
payload: input.payload,
|
|
62
89
|
...buildDomainEventContractVersions(input.contracts),
|
|
@@ -66,6 +93,7 @@ function createDomainEventEnvelope<TPayload>(
|
|
|
66
93
|
export {
|
|
67
94
|
buildDomainEventContractVersions,
|
|
68
95
|
createDomainEventEnvelope,
|
|
96
|
+
type ContractVersioned,
|
|
69
97
|
type CreateDomainEventEnvelopeInput,
|
|
70
98
|
type DomainEventContractSelection,
|
|
71
99
|
type DomainEventContractVersions,
|
|
@@ -1,5 +1,16 @@
|
|
|
1
|
+
import { InvariantViolationException } from "../exceptions/invariant-violation-exception.js";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* A structural contract version in `MAJOR.MINOR` form (e.g. `"1.0"`, `"2.5"`).
|
|
5
|
+
*
|
|
6
|
+
* The template-literal type is intentionally loose — it also accepts strings
|
|
7
|
+
* the module rejects at runtime (e.g. `"1.0.0"` matches as `1` · `.` · `0.0`).
|
|
8
|
+
* The runtime regex in {@link version} is the real guard; the type is only a
|
|
9
|
+
* light hint.
|
|
10
|
+
*/
|
|
1
11
|
type ContractVersion = `${number}.${number}`;
|
|
2
12
|
|
|
13
|
+
/** Anything a class decorator can stamp a static property onto. */
|
|
3
14
|
type VersionedTarget = {
|
|
4
15
|
readonly prototype: object;
|
|
5
16
|
};
|
|
@@ -7,11 +18,29 @@ type VersionedTarget = {
|
|
|
7
18
|
const CONTRACT_VERSION_PROPERTY = "CONTRACT_VERSION" as const;
|
|
8
19
|
const CONTRACT_VERSION_PATTERN = /^\d+\.\d+$/;
|
|
9
20
|
|
|
21
|
+
/**
|
|
22
|
+
* Class decorator that stamps a `MAJOR.MINOR` structural contract version onto
|
|
23
|
+
* the class as a static `CONTRACT_VERSION` property.
|
|
24
|
+
*
|
|
25
|
+
* The property is defined **non-writable, non-configurable and non-enumerable**
|
|
26
|
+
* on purpose: it must not be reassigned or redefined (tampering with a contract
|
|
27
|
+
* version silently corrupts migration decisions), and keeping it off enumeration
|
|
28
|
+
* excludes it from `JSON.stringify`/`for..in`. A consequence of
|
|
29
|
+
* `configurable: false` is that decorating the same class twice throws
|
|
30
|
+
* `TypeError: Cannot redefine property` — a deliberate fail-fast.
|
|
31
|
+
*
|
|
32
|
+
* Note on inheritance: a subclass that is not re-decorated inherits its base's
|
|
33
|
+
* `CONTRACT_VERSION` through the static prototype chain. Re-decorate any
|
|
34
|
+
* subclass whose persisted shape diverges from its base.
|
|
35
|
+
*
|
|
36
|
+
* @throws {InvariantViolationException} When `contractVersion` is not
|
|
37
|
+
* `MAJOR.MINOR` (validated at module-load time, when the decorator runs).
|
|
38
|
+
*/
|
|
10
39
|
function version<const TVersion extends ContractVersion>(
|
|
11
40
|
contractVersion: TVersion,
|
|
12
41
|
) {
|
|
13
42
|
if (!CONTRACT_VERSION_PATTERN.test(contractVersion)) {
|
|
14
|
-
throw new
|
|
43
|
+
throw new InvariantViolationException(
|
|
15
44
|
`Invalid contract version "${contractVersion}". Expected MAJOR.MINOR.`,
|
|
16
45
|
);
|
|
17
46
|
}
|
package/src/domain/index.ts
CHANGED
|
@@ -5,3 +5,8 @@ export {
|
|
|
5
5
|
ValueObject,
|
|
6
6
|
type ValueObjectPluginContract,
|
|
7
7
|
} from "../core/domain/value-object.js";
|
|
8
|
+
// Bitemporal primitives. Published on this subpath (not the root barrel)
|
|
9
|
+
// because the public `TemporalRepository` port already exposes
|
|
10
|
+
// `TemporalSnapshot` — implementers need the validating factories that build
|
|
11
|
+
// it, not just the type.
|
|
12
|
+
export * from "../core/domain/temporal/index.js";
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { DomainException } from "../../../core/domain
|
|
1
|
+
import { DomainException } from "../../../core/exceptions/domain-exception.js";
|
|
2
2
|
import { type OrderCreationData, type OrderCreationRuleset } from "./order.js";
|
|
3
3
|
|
|
4
4
|
class OrderCreationValidationError extends DomainException {}
|
|
@@ -1,7 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
type RulesetId,
|
|
4
|
-
} from "../../../core/domain/rulesets/entity-ruleset.contracts.js";
|
|
1
|
+
import { DomainException } from "../../../core/exceptions/domain-exception.js";
|
|
2
|
+
import { type RulesetId } from "../../../core/domain/rulesets/entity-ruleset.contracts.js";
|
|
5
3
|
import {
|
|
6
4
|
type Order,
|
|
7
5
|
type OrderInvariantRuleset,
|
|
@@ -1,7 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
type RulesetId,
|
|
4
|
-
} from "../../../core/domain/rulesets/entity-ruleset.contracts.js";
|
|
1
|
+
import { DomainException } from "../../../core/exceptions/domain-exception.js";
|
|
2
|
+
import { type RulesetId } from "../../../core/domain/rulesets/entity-ruleset.contracts.js";
|
|
5
3
|
import { type Order } from "./order.js";
|
|
6
4
|
import { OrderInvariantsV1 } from "./order-invariants-v1.js";
|
|
7
5
|
|
|
@@ -1,7 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
type ValueObjectRuleset,
|
|
4
|
-
} from "../../../core/domain/rulesets/value-object-ruleset.contracts.js";
|
|
1
|
+
import { DomainException } from "../../../core/exceptions/domain-exception.js";
|
|
2
|
+
import { type ValueObjectRuleset } from "../../../core/domain/rulesets/value-object-ruleset.contracts.js";
|
|
5
3
|
|
|
6
4
|
class CPFValidationError extends DomainException {}
|
|
7
5
|
|
|
@@ -1,7 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
type ValueObjectRuleset,
|
|
4
|
-
} from "../../../core/domain/rulesets/value-object-ruleset.contracts.js";
|
|
1
|
+
import { DomainException } from "../../../core/exceptions/domain-exception.js";
|
|
2
|
+
import { type ValueObjectRuleset } from "../../../core/domain/rulesets/value-object-ruleset.contracts.js";
|
|
5
3
|
import { CPFRulesV1 } from "./cpf-rules-v1.js";
|
|
6
4
|
|
|
7
5
|
class CPFBlocklistError extends DomainException {}
|
|
@@ -1,7 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
type ValueObjectRuleset,
|
|
4
|
-
} from "../../../core/domain/rulesets/value-object-ruleset.contracts.js";
|
|
1
|
+
import { DomainException } from "../../../core/exceptions/domain-exception.js";
|
|
2
|
+
import { type ValueObjectRuleset } from "../../../core/domain/rulesets/value-object-ruleset.contracts.js";
|
|
5
3
|
|
|
6
4
|
class PersonNameValidationError extends DomainException {}
|
|
7
5
|
|
|
@@ -1,7 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
type ValueObjectRuleset,
|
|
4
|
-
} from "../../../core/domain/rulesets/value-object-ruleset.contracts.js";
|
|
1
|
+
import { DomainException } from "../../../core/exceptions/domain-exception.js";
|
|
2
|
+
import { type ValueObjectRuleset } from "../../../core/domain/rulesets/value-object-ruleset.contracts.js";
|
|
5
3
|
import { PersonNameRulesV1 } from "./person-name-rules-v1.js";
|
|
6
4
|
|
|
7
5
|
class PersonNameCharacterError extends DomainException {}
|
package/src/result/index.ts
CHANGED
package/src/version.ts
CHANGED
|
@@ -4,4 +4,4 @@
|
|
|
4
4
|
// Mantemos a versao aqui, dentro de src/, para que a copia full-control seja
|
|
5
5
|
// auto-contida: ao copiar so o conteudo de src/, o entry nao depende de um
|
|
6
6
|
// `../package.json` que deixaria de existir no projeto consumidor.
|
|
7
|
-
export const version = "
|
|
7
|
+
export const version = "2.0.0";
|
package/dist/entity.cjs
DELETED
|
@@ -1,126 +0,0 @@
|
|
|
1
|
-
const require_invariant_violation_exception = require("./invariant-violation-exception.cjs");
|
|
2
|
-
const require_immutable = require("./immutable.cjs");
|
|
3
|
-
const require_temporal_guards = require("./temporal-guards.cjs");
|
|
4
|
-
//#region src/core/shared/aggregate-version.ts
|
|
5
|
-
function assertValidAggregateVersion(aggregateVersion) {
|
|
6
|
-
if (!Number.isInteger(aggregateVersion) || aggregateVersion < 0) throw new require_invariant_violation_exception.InvariantViolationException(`aggregateVersion must be a non-negative integer. Received: ${aggregateVersion}`);
|
|
7
|
-
}
|
|
8
|
-
//#endregion
|
|
9
|
-
//#region src/core/domain/entity.ts
|
|
10
|
-
var _Entity;
|
|
11
|
-
/**
|
|
12
|
-
* Base class for domain entities — objects defined by a stable identity rather
|
|
13
|
-
* than by their attributes. Two entities are "the same" when their `id`
|
|
14
|
-
* matches, even if every other field differs; this is the opposite of a
|
|
15
|
-
* {@link ValueObject}, which is defined entirely by its contents.
|
|
16
|
-
*
|
|
17
|
-
* The class owns the bookkeeping common to every aggregate root: a creation
|
|
18
|
-
* timestamp that never changes, a last-modified timestamp, and an
|
|
19
|
-
* `aggregateVersion` counter used for optimistic concurrency control. All three
|
|
20
|
-
* are validated on construction and the dates are defensively cloned, so an
|
|
21
|
-
* entity can never be built into — or leak — an inconsistent temporal state.
|
|
22
|
-
*
|
|
23
|
-
* @typeParam TIdentifier - The identity type (e.g. a branded string id or a VO).
|
|
24
|
-
*/
|
|
25
|
-
let Entity = class Entity {
|
|
26
|
-
static {
|
|
27
|
-
_Entity = this;
|
|
28
|
-
}
|
|
29
|
-
/**
|
|
30
|
-
* Reconstitutes an entity from its persisted {@link EntityState}.
|
|
31
|
-
*
|
|
32
|
-
* Validates the temporal invariants up front so an invalid entity is
|
|
33
|
-
* impossible to construct: both dates must be valid, the aggregate version
|
|
34
|
-
* must be a non-negative integer, and `updatedAt` may not predate
|
|
35
|
-
* `createdAt`. Both dates are cloned on the way in so a later mutation of
|
|
36
|
-
* the caller's `Date` objects cannot reach into the entity's internal state.
|
|
37
|
-
*
|
|
38
|
-
* Declared `protected` because entities are reconstituted through a
|
|
39
|
-
* subclass factory, never instantiated directly by application code.
|
|
40
|
-
*
|
|
41
|
-
* @throws {InvariantViolationException} When `updatedAt` is earlier than `createdAt`.
|
|
42
|
-
*/
|
|
43
|
-
constructor(state) {
|
|
44
|
-
require_temporal_guards.assertValidDate("createdAt", state.createdAt);
|
|
45
|
-
require_temporal_guards.assertValidDate("updatedAt", state.updatedAt);
|
|
46
|
-
assertValidAggregateVersion(state.aggregateVersion);
|
|
47
|
-
if (state.updatedAt.getTime() < state.createdAt.getTime()) throw new require_invariant_violation_exception.InvariantViolationException("updatedAt cannot be earlier than createdAt");
|
|
48
|
-
this._id = state.id;
|
|
49
|
-
this._createdAt = require_temporal_guards.cloneDate(state.createdAt);
|
|
50
|
-
this._updatedAt = require_temporal_guards.cloneDate(state.updatedAt);
|
|
51
|
-
this._aggregateVersion = state.aggregateVersion;
|
|
52
|
-
}
|
|
53
|
-
/** The entity's stable identity — the basis for equality between entities. */
|
|
54
|
-
get id() {
|
|
55
|
-
return this._id;
|
|
56
|
-
}
|
|
57
|
-
/**
|
|
58
|
-
* When the entity was first created.
|
|
59
|
-
*
|
|
60
|
-
* Returns a clone so callers cannot mutate the entity's internal `Date`;
|
|
61
|
-
* the value is fixed at construction and never changes thereafter.
|
|
62
|
-
*/
|
|
63
|
-
get createdAt() {
|
|
64
|
-
return require_temporal_guards.cloneDate(this._createdAt);
|
|
65
|
-
}
|
|
66
|
-
/**
|
|
67
|
-
* When the entity was last modified.
|
|
68
|
-
*
|
|
69
|
-
* Returns a clone for the same encapsulation reason as {@link createdAt};
|
|
70
|
-
* the underlying value advances only through {@link markAsModified}.
|
|
71
|
-
*/
|
|
72
|
-
get updatedAt() {
|
|
73
|
-
return require_temporal_guards.cloneDate(this._updatedAt);
|
|
74
|
-
}
|
|
75
|
-
/**
|
|
76
|
-
* Monotonic counter incremented on every mutation, used for optimistic
|
|
77
|
-
* concurrency control: a writer reads this value, and the persistence layer
|
|
78
|
-
* rejects the write if the stored version has moved on in the meantime.
|
|
79
|
-
*/
|
|
80
|
-
get aggregateVersion() {
|
|
81
|
-
return this._aggregateVersion;
|
|
82
|
-
}
|
|
83
|
-
/**
|
|
84
|
-
* The schema/contract version stamped on this entity type by the
|
|
85
|
-
* `@version` decorator — used to detect and migrate state persisted under
|
|
86
|
-
* an older shape.
|
|
87
|
-
*/
|
|
88
|
-
get contractVersion() {
|
|
89
|
-
return _Entity.CONTRACT_VERSION;
|
|
90
|
-
}
|
|
91
|
-
/**
|
|
92
|
-
* The single seam through which an entity records a mutation: it advances
|
|
93
|
-
* `updatedAt` and bumps {@link aggregateVersion} by one. Subclasses must
|
|
94
|
-
* call this from every state-changing method so the version counter stays
|
|
95
|
-
* an accurate optimistic-lock token.
|
|
96
|
-
*
|
|
97
|
-
* @param updatedAt - The modification instant; defaults to now. Validated
|
|
98
|
-
* and required not to predate `createdAt`, preserving the same invariant
|
|
99
|
-
* the constructor enforces.
|
|
100
|
-
* @returns The new aggregate version after the increment.
|
|
101
|
-
* @throws {InvariantViolationException} When `updatedAt` is earlier than `createdAt`.
|
|
102
|
-
*/
|
|
103
|
-
markAsModified(updatedAt = /* @__PURE__ */ new Date()) {
|
|
104
|
-
require_temporal_guards.assertValidDate("updatedAt", updatedAt);
|
|
105
|
-
if (updatedAt.getTime() < this._createdAt.getTime()) throw new require_invariant_violation_exception.InvariantViolationException("updatedAt cannot be earlier than createdAt");
|
|
106
|
-
this._updatedAt = require_temporal_guards.cloneDate(updatedAt);
|
|
107
|
-
this._aggregateVersion += 1;
|
|
108
|
-
return this._aggregateVersion;
|
|
109
|
-
}
|
|
110
|
-
};
|
|
111
|
-
Entity = _Entity = require_immutable.__decorate([require_immutable.version("1.0")], Entity);
|
|
112
|
-
//#endregion
|
|
113
|
-
Object.defineProperty(exports, "Entity", {
|
|
114
|
-
enumerable: true,
|
|
115
|
-
get: function() {
|
|
116
|
-
return Entity;
|
|
117
|
-
}
|
|
118
|
-
});
|
|
119
|
-
Object.defineProperty(exports, "assertValidAggregateVersion", {
|
|
120
|
-
enumerable: true,
|
|
121
|
-
get: function() {
|
|
122
|
-
return assertValidAggregateVersion;
|
|
123
|
-
}
|
|
124
|
-
});
|
|
125
|
-
|
|
126
|
-
//# sourceMappingURL=entity.cjs.map
|
package/dist/entity.cjs.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"entity.cjs","names":["InvariantViolationException","InvariantViolationException","cloneDate","version"],"sources":["../src/core/shared/aggregate-version.ts","../src/core/domain/entity.ts"],"sourcesContent":["import { InvariantViolationException } from \"../exceptions/invariant-violation-exception.js\";\n\nfunction assertValidAggregateVersion(aggregateVersion: number): void {\n if (!Number.isInteger(aggregateVersion) || aggregateVersion < 0) {\n throw new InvariantViolationException(\n `aggregateVersion must be a non-negative integer. Received: ${aggregateVersion}`,\n );\n }\n}\n\nexport { assertValidAggregateVersion };\n","import { InvariantViolationException } from \"../exceptions/invariant-violation-exception.js\";\nimport { assertValidAggregateVersion } from \"../shared/aggregate-version.js\";\nimport { assertValidDate, cloneDate } from \"../shared/temporal-guards.js\";\nimport { type ContractVersion, version } from \"../versioning/version.js\";\n\n/**\n * The minimal persisted shape needed to reconstitute an {@link Entity}.\n *\n * This is the contract between storage and the domain: a repository maps a row\n * (or document) onto these four fields and hands them to a subclass constructor.\n * `aggregateVersion` travels with the state so optimistic-concurrency checks\n * survive a round-trip through the database.\n */\ninterface EntityState<TIdentifier> {\n readonly id: TIdentifier;\n readonly createdAt: Date;\n readonly updatedAt: Date;\n readonly aggregateVersion: number;\n}\n\n/**\n * Base class for domain entities — objects defined by a stable identity rather\n * than by their attributes. Two entities are \"the same\" when their `id`\n * matches, even if every other field differs; this is the opposite of a\n * {@link ValueObject}, which is defined entirely by its contents.\n *\n * The class owns the bookkeeping common to every aggregate root: a creation\n * timestamp that never changes, a last-modified timestamp, and an\n * `aggregateVersion` counter used for optimistic concurrency control. All three\n * are validated on construction and the dates are defensively cloned, so an\n * entity can never be built into — or leak — an inconsistent temporal state.\n *\n * @typeParam TIdentifier - The identity type (e.g. a branded string id or a VO).\n */\n@version(\"1.0\")\nabstract class Entity<TIdentifier> {\n declare public static readonly CONTRACT_VERSION: ContractVersion;\n\n private readonly _id: TIdentifier;\n private readonly _createdAt: Date;\n private _updatedAt: Date;\n private _aggregateVersion: number;\n\n /**\n * Reconstitutes an entity from its persisted {@link EntityState}.\n *\n * Validates the temporal invariants up front so an invalid entity is\n * impossible to construct: both dates must be valid, the aggregate version\n * must be a non-negative integer, and `updatedAt` may not predate\n * `createdAt`. Both dates are cloned on the way in so a later mutation of\n * the caller's `Date` objects cannot reach into the entity's internal state.\n *\n * Declared `protected` because entities are reconstituted through a\n * subclass factory, never instantiated directly by application code.\n *\n * @throws {InvariantViolationException} When `updatedAt` is earlier than `createdAt`.\n */\n protected constructor(state: EntityState<TIdentifier>) {\n assertValidDate(\"createdAt\", state.createdAt);\n assertValidDate(\"updatedAt\", state.updatedAt);\n assertValidAggregateVersion(state.aggregateVersion);\n\n if (state.updatedAt.getTime() < state.createdAt.getTime()) {\n throw new InvariantViolationException(\n \"updatedAt cannot be earlier than createdAt\",\n );\n }\n\n this._id = state.id;\n this._createdAt = cloneDate(state.createdAt);\n this._updatedAt = cloneDate(state.updatedAt);\n this._aggregateVersion = state.aggregateVersion;\n }\n\n /** The entity's stable identity — the basis for equality between entities. */\n public get id(): TIdentifier {\n return this._id;\n }\n\n /**\n * When the entity was first created.\n *\n * Returns a clone so callers cannot mutate the entity's internal `Date`;\n * the value is fixed at construction and never changes thereafter.\n */\n public get createdAt(): Date {\n return cloneDate(this._createdAt);\n }\n\n /**\n * When the entity was last modified.\n *\n * Returns a clone for the same encapsulation reason as {@link createdAt};\n * the underlying value advances only through {@link markAsModified}.\n */\n public get updatedAt(): Date {\n return cloneDate(this._updatedAt);\n }\n\n /**\n * Monotonic counter incremented on every mutation, used for optimistic\n * concurrency control: a writer reads this value, and the persistence layer\n * rejects the write if the stored version has moved on in the meantime.\n */\n public get aggregateVersion(): number {\n return this._aggregateVersion;\n }\n\n /**\n * The schema/contract version stamped on this entity type by the\n * `@version` decorator — used to detect and migrate state persisted under\n * an older shape.\n */\n public get contractVersion(): ContractVersion {\n return Entity.CONTRACT_VERSION;\n }\n\n /**\n * The single seam through which an entity records a mutation: it advances\n * `updatedAt` and bumps {@link aggregateVersion} by one. Subclasses must\n * call this from every state-changing method so the version counter stays\n * an accurate optimistic-lock token.\n *\n * @param updatedAt - The modification instant; defaults to now. Validated\n * and required not to predate `createdAt`, preserving the same invariant\n * the constructor enforces.\n * @returns The new aggregate version after the increment.\n * @throws {InvariantViolationException} When `updatedAt` is earlier than `createdAt`.\n */\n protected markAsModified(updatedAt: Date = new Date()): number {\n assertValidDate(\"updatedAt\", updatedAt);\n\n if (updatedAt.getTime() < this._createdAt.getTime()) {\n throw new InvariantViolationException(\n \"updatedAt cannot be earlier than createdAt\",\n );\n }\n\n this._updatedAt = cloneDate(updatedAt);\n this._aggregateVersion += 1;\n\n return this._aggregateVersion;\n }\n}\n\nexport { Entity, type EntityState };\n"],"mappings":";;;;AAEA,SAAS,4BAA4B,kBAAgC;CACjE,IAAI,CAAC,OAAO,UAAU,gBAAgB,KAAK,mBAAmB,GAC1D,MAAM,IAAIA,sCAAAA,4BACN,8DAA8D,kBAClE;AAER;;;;;;;;;;;;;;;;;;AC0BA,IAAA,SAAA,MACe,OAAoB;;;;;;;;;;;;;;;;;;CAsB/B,YAAsB,OAAiC;EACnD,wBAAA,gBAAgB,aAAa,MAAM,SAAS;EAC5C,wBAAA,gBAAgB,aAAa,MAAM,SAAS;EAC5C,4BAA4B,MAAM,gBAAgB;EAElD,IAAI,MAAM,UAAU,QAAQ,IAAI,MAAM,UAAU,QAAQ,GACpD,MAAM,IAAIC,sCAAAA,4BACN,4CACJ;EAGJ,KAAK,MAAM,MAAM;EACjB,KAAK,aAAaC,wBAAAA,UAAU,MAAM,SAAS;EAC3C,KAAK,aAAaA,wBAAAA,UAAU,MAAM,SAAS;EAC3C,KAAK,oBAAoB,MAAM;CACnC;;CAGA,IAAW,KAAkB;EACzB,OAAO,KAAK;CAChB;;;;;;;CAQA,IAAW,YAAkB;EACzB,OAAOA,wBAAAA,UAAU,KAAK,UAAU;CACpC;;;;;;;CAQA,IAAW,YAAkB;EACzB,OAAOA,wBAAAA,UAAU,KAAK,UAAU;CACpC;;;;;;CAOA,IAAW,mBAA2B;EAClC,OAAO,KAAK;CAChB;;;;;;CAOA,IAAW,kBAAmC;EAC1C,OAAA,QAAc;CAClB;;;;;;;;;;;;;CAcA,eAAyB,4BAAkB,IAAI,KAAK,GAAW;EAC3D,wBAAA,gBAAgB,aAAa,SAAS;EAEtC,IAAI,UAAU,QAAQ,IAAI,KAAK,WAAW,QAAQ,GAC9C,MAAM,IAAID,sCAAAA,4BACN,4CACJ;EAGJ,KAAK,aAAaC,wBAAAA,UAAU,SAAS;EACrC,KAAK,qBAAqB;EAE1B,OAAO,KAAK;CAChB;AACJ;iDA7GCC,kBAAAA,QAAQ,KAAK,CAAA,GAAA,MAAA"}
|