@cullet/erp-core 1.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 +39 -0
- package/README.md +110 -0
- package/dist/errors/index.d.ts +2 -0
- package/dist/errors/index.js +3 -0
- package/dist/exceptions/validation-field.d.ts +2 -0
- package/dist/exceptions/validation-field.js +2 -0
- package/dist/gate-engine-registry.js +260 -0
- package/dist/gate-engine-registry.js.map +1 -0
- package/dist/gate-types.d.ts +307 -0
- package/dist/gate-v1-payload.schema.js +677 -0
- package/dist/gate-v1-payload.schema.js.map +1 -0
- package/dist/index.d.ts +88 -0
- package/dist/index.js +153 -0
- package/dist/index.js.map +1 -0
- package/dist/outcome.js +92 -0
- package/dist/outcome.js.map +1 -0
- package/dist/parse-gate-payload.d.ts +136 -0
- package/dist/policies/engines/index.d.ts +3 -0
- package/dist/policies/engines/index.js +2 -0
- package/dist/policies/engines/v1/gate/index.d.ts +115 -0
- package/dist/policies/engines/v1/gate/index.js +84 -0
- package/dist/policies/engines/v1/gate/index.js.map +1 -0
- package/dist/policies/index.d.ts +4 -0
- package/dist/policies/index.js +5 -0
- package/dist/policy-service.d.ts +541 -0
- package/dist/policy-service.js +992 -0
- package/dist/policy-service.js.map +1 -0
- package/dist/validation-code.js +210 -0
- package/dist/validation-code.js.map +1 -0
- package/dist/validation-error.d.ts +686 -0
- package/dist/validation-error.js +757 -0
- package/dist/validation-error.js.map +1 -0
- package/dist/validation-field.d.ts +12 -0
- package/dist/validation-field.js +23 -0
- package/dist/validation-field.js.map +1 -0
- package/meta.json +66 -0
- package/package.json +59 -0
- package/src/core/application/commands/command.ts +28 -0
- package/src/core/application/commands/index.ts +2 -0
- package/src/core/application/commands/requested-by.ts +97 -0
- package/src/core/application/index.ts +31 -0
- package/src/core/application/policy-error-mapper.ts +60 -0
- package/src/core/application/ports/index.ts +14 -0
- package/src/core/application/ports/logger.port.ts +10 -0
- package/src/core/application/ports/metrics.port.ts +9 -0
- package/src/core/application/ports/policy-port.ts +20 -0
- package/src/core/application/ports/repository.port.ts +7 -0
- package/src/core/application/ports/temporal-repository.port.ts +14 -0
- package/src/core/application/ports/tracer.port.ts +16 -0
- package/src/core/application/queries/index.ts +2 -0
- package/src/core/application/queries/query.ts +53 -0
- package/src/core/application/temporal/index.ts +11 -0
- package/src/core/application/temporal/temporal-context.ts +41 -0
- package/src/core/application/temporal/temporal-use-case.ts +41 -0
- package/src/core/application/use-case.ts +24 -0
- package/src/core/config/core-config.instance.ts +14 -0
- package/src/core/config/core-config.ts +87 -0
- package/src/core/config/index.ts +12 -0
- package/src/core/config/policy-reporter.ts +60 -0
- package/src/core/config/silent-policy-reporter.ts +5 -0
- package/src/core/domain/entity.ts +69 -0
- package/src/core/domain/rulesets/entity-ruleset.contracts.ts +16 -0
- package/src/core/domain/rulesets/ruleset-registry.ts +71 -0
- package/src/core/domain/rulesets/ruleset.contracts.ts +15 -0
- package/src/core/domain/rulesets/value-object-ruleset.contracts.ts +13 -0
- package/src/core/domain/temporal/index.ts +18 -0
- package/src/core/domain/temporal/temporal-range.ts +53 -0
- package/src/core/domain/temporal/temporal-snapshot.ts +47 -0
- package/src/core/domain/temporal/transaction-time.ts +60 -0
- package/src/core/domain/temporal/valid-time.ts +57 -0
- package/src/core/domain/value-object.ts +30 -0
- package/src/core/errors/app-error.ts +85 -0
- package/src/core/errors/authentication-error.ts +171 -0
- package/src/core/errors/authorization-error.ts +204 -0
- package/src/core/errors/business-rule-violation-error.ts +28 -0
- package/src/core/errors/conflict-error.ts +246 -0
- package/src/core/errors/error-codes.ts +60 -0
- package/src/core/errors/idempotency-error.ts +372 -0
- package/src/core/errors/index.ts +99 -0
- package/src/core/errors/integration-error.ts +157 -0
- package/src/core/errors/legacy-incompatible-error.ts +26 -0
- package/src/core/errors/not-found-error.ts +27 -0
- package/src/core/errors/serialization-error.ts +233 -0
- package/src/core/errors/temporal-error.ts +145 -0
- package/src/core/errors/types.ts +74 -0
- package/src/core/errors/unexpected-error.ts +22 -0
- package/src/core/errors/utils/index.ts +4 -0
- package/src/core/errors/utils/json-safe.ts +93 -0
- package/src/core/errors/validation-error.ts +34 -0
- package/src/core/exceptions/business-rule-violation-exception.ts +12 -0
- package/src/core/exceptions/domain-exception.ts +9 -0
- package/src/core/exceptions/entity-not-found-exception.ts +12 -0
- package/src/core/exceptions/invalid-state-transition-exception.ts +15 -0
- package/src/core/exceptions/invariant-violation-exception.ts +9 -0
- package/src/core/exceptions/validation-code.ts +39 -0
- package/src/core/exceptions/validation-exception.ts +39 -0
- package/src/core/exceptions/validation-field.ts +32 -0
- package/src/core/index.ts +15 -0
- package/src/core/policies/asof/asof.ts +68 -0
- package/src/core/policies/asof/index.ts +2 -0
- package/src/core/policies/catalog/catalog-types.ts +12 -0
- package/src/core/policies/catalog/catalog.instance.ts +21 -0
- package/src/core/policies/catalog/index.ts +11 -0
- package/src/core/policies/catalog/policy-catalog-entry.ts +220 -0
- package/src/core/policies/catalog/policy-catalog.ts +124 -0
- package/src/core/policies/catalog/policy-key.ts +78 -0
- package/src/core/policies/context/context-builder.ts +391 -0
- package/src/core/policies/context/context-registry.instance.ts +26 -0
- package/src/core/policies/context/context-registry.ts +95 -0
- package/src/core/policies/context/context-resolver.ts +31 -0
- package/src/core/policies/context/context-seed.ts +32 -0
- package/src/core/policies/context/index.ts +21 -0
- package/src/core/policies/context/path.ts +152 -0
- package/src/core/policies/defs/in-memory-policy-definition-repo.ts +73 -0
- package/src/core/policies/defs/index.ts +20 -0
- package/src/core/policies/defs/policy-definition-repository.ts +8 -0
- package/src/core/policies/defs/policy-definition.ts +192 -0
- package/src/core/policies/defs/policy-payload.contracts.ts +13 -0
- package/src/core/policies/defs/policy-scope.ts +53 -0
- package/src/core/policies/defs/policy-semver.ts +83 -0
- package/src/core/policies/engines/compute-engine-registry.ts +57 -0
- package/src/core/policies/engines/compute-evaluator-registry.ts +28 -0
- package/src/core/policies/engines/compute-payload.ts +5 -0
- package/src/core/policies/engines/compute-registry.ts +67 -0
- package/src/core/policies/engines/compute-types.ts +51 -0
- package/src/core/policies/engines/condition-evaluator-reporter.ts +34 -0
- package/src/core/policies/engines/gate-engine-registry.ts +38 -0
- package/src/core/policies/engines/gate-payload.ts +5 -0
- package/src/core/policies/engines/gate-types.ts +63 -0
- package/src/core/policies/engines/index.ts +48 -0
- package/src/core/policies/engines/parse-compute-payload.ts +71 -0
- package/src/core/policies/engines/parse-gate-payload.ts +68 -0
- package/src/core/policies/engines/v1/compute/compute-engine-v1.ts +55 -0
- package/src/core/policies/engines/v1/compute/compute-payload.schema.ts +75 -0
- package/src/core/policies/engines/v1/compute/index.ts +12 -0
- package/src/core/policies/engines/v1/compute/resolve-decision-table-v1.ts +54 -0
- package/src/core/policies/engines/v1/condition-evaluator.ts +592 -0
- package/src/core/policies/engines/v1/condition-schema.ts +50 -0
- package/src/core/policies/engines/v1/condition-types.ts +41 -0
- package/src/core/policies/engines/v1/gate/gate-engine-v1.ts +157 -0
- package/src/core/policies/engines/v1/gate/gate-types-v1.ts +12 -0
- package/src/core/policies/engines/v1/gate/gate-v1-payload.schema.ts +46 -0
- package/src/core/policies/engines/v1/gate/index.ts +9 -0
- package/src/core/policies/engines/v1/index.ts +13 -0
- package/src/core/policies/index.ts +132 -0
- package/src/core/policies/package/policy-package.ts +17 -0
- package/src/core/policies/policy-ids.ts +60 -0
- package/src/core/policies/resolver/index.ts +1 -0
- package/src/core/policies/resolver/policy-resolver.ts +57 -0
- package/src/core/policies/service/index.ts +10 -0
- package/src/core/policies/service/policy-evaluation-error.ts +148 -0
- package/src/core/policies/service/policy-service.ts +493 -0
- package/src/core/policies/utils/date.ts +7 -0
- package/src/core/policies/utils/hash.ts +90 -0
- package/src/core/policies/utils/index.ts +3 -0
- package/src/core/policies/utils/result.ts +4 -0
- package/src/core/result/outcome.ts +158 -0
- package/src/core/result/result.ts +153 -0
- package/src/core/shared/aggregate-version.ts +11 -0
- package/src/core/shared/hashing.ts +38 -0
- package/src/core/shared/immutable.ts +46 -0
- package/src/core/shared/temporal-guards.ts +22 -0
- package/src/core/versioning/domain-event-contracts.ts +73 -0
- package/src/core/versioning/version.ts +34 -0
- package/src/errors/index.ts +1 -0
- package/src/examples/rulesets/entity/order-creation-rules-v1.ts +31 -0
- package/src/examples/rulesets/entity/order-invariants-v1.ts +48 -0
- package/src/examples/rulesets/entity/order-invariants-v2.ts +28 -0
- package/src/examples/rulesets/entity/order.ts +162 -0
- package/src/examples/rulesets/value-object/cpf-rules-v1.ts +52 -0
- package/src/examples/rulesets/value-object/cpf-rules-v2.ts +27 -0
- package/src/examples/rulesets/value-object/cpf.ts +37 -0
- package/src/examples/rulesets/value-object/customer.ts +67 -0
- package/src/examples/rulesets/value-object/person-name-rules-v1.ts +37 -0
- package/src/examples/rulesets/value-object/person-name-rules-v2.ts +27 -0
- package/src/examples/rulesets/value-object/person-name.ts +39 -0
- package/src/exceptions/validation-field.ts +1 -0
- package/src/index.ts +11 -0
- package/src/policies/engines/index.ts +1 -0
- package/src/policies/engines/v1/gate/index.ts +1 -0
- package/src/policies/index.ts +1 -0
- package/src/version.ts +7 -0
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
// core/domain/outcome.ts
|
|
2
|
+
|
|
3
|
+
import { InvariantViolationException } from "../exceptions/invariant-violation-exception";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Outcome — Business decision abstraction.
|
|
7
|
+
*
|
|
8
|
+
* Represents the semantic result of a domain decision, policy evaluation,
|
|
9
|
+
* or business rule. NOT for technical success/failure — use Result for that.
|
|
10
|
+
*
|
|
11
|
+
* Mental model:
|
|
12
|
+
* "What was the business decision?"
|
|
13
|
+
* NOT "Did the operation succeed?"
|
|
14
|
+
*
|
|
15
|
+
* The status axis represents valid business outcomes (APPROVED, REJECTED,
|
|
16
|
+
* ALLOW, DENY, NO_OP, etc.), never technical Ok/Err.
|
|
17
|
+
*
|
|
18
|
+
* @typeParam S - Union of valid status literals for this outcome
|
|
19
|
+
* @typeParam D - Shape of the decision data carried by this outcome
|
|
20
|
+
*/
|
|
21
|
+
export class Outcome<S extends string, D = undefined> {
|
|
22
|
+
readonly status: S;
|
|
23
|
+
readonly data: D;
|
|
24
|
+
readonly reason: string | undefined;
|
|
25
|
+
readonly metadata: Readonly<Record<string, unknown>>;
|
|
26
|
+
|
|
27
|
+
private constructor(
|
|
28
|
+
status: S,
|
|
29
|
+
data: D,
|
|
30
|
+
reason: string | undefined,
|
|
31
|
+
metadata: Readonly<Record<string, unknown>>,
|
|
32
|
+
) {
|
|
33
|
+
this.status = status;
|
|
34
|
+
this.data = data;
|
|
35
|
+
this.reason = reason;
|
|
36
|
+
this.metadata = metadata;
|
|
37
|
+
Object.freeze(this);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// ─── Generic factory ────────────────────────────────────────────────
|
|
41
|
+
|
|
42
|
+
static of<S extends string, D = undefined>(
|
|
43
|
+
status: S,
|
|
44
|
+
data: D,
|
|
45
|
+
reason?: string,
|
|
46
|
+
metadata?: Record<string, unknown>,
|
|
47
|
+
): Outcome<S, D> {
|
|
48
|
+
return new Outcome(status, data, reason, Object.freeze({ ...metadata }));
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// ─── Convenience factories for common business statuses ─────────────
|
|
52
|
+
|
|
53
|
+
static approved<D = undefined>(
|
|
54
|
+
data: D,
|
|
55
|
+
reason?: string,
|
|
56
|
+
): Outcome<"APPROVED", D> {
|
|
57
|
+
return new Outcome("APPROVED", data, reason, Object.freeze({}));
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
static rejected<D = undefined>(
|
|
61
|
+
data: D,
|
|
62
|
+
reason: string,
|
|
63
|
+
): Outcome<"REJECTED", D> {
|
|
64
|
+
return new Outcome("REJECTED", data, reason, Object.freeze({}));
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
static noOp<D = undefined>(data: D, reason?: string): Outcome<"NO_OP", D> {
|
|
68
|
+
return new Outcome("NO_OP", data, reason, Object.freeze({}));
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
static deferred<D = undefined>(
|
|
72
|
+
data: D,
|
|
73
|
+
reason: string,
|
|
74
|
+
): Outcome<"DEFERRED", D> {
|
|
75
|
+
return new Outcome("DEFERRED", data, reason, Object.freeze({}));
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
static requiresReview<D = undefined>(
|
|
79
|
+
data: D,
|
|
80
|
+
reason: string,
|
|
81
|
+
): Outcome<"REQUIRES_REVIEW", D> {
|
|
82
|
+
return new Outcome("REQUIRES_REVIEW", data, reason, Object.freeze({}));
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// ─── Type narrowing ────────────────────────────────────────────────
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Narrows this Outcome to a specific status.
|
|
89
|
+
*
|
|
90
|
+
* @example
|
|
91
|
+
* if (outcome.is('DENY')) {
|
|
92
|
+
* outcome.data.violations // TS knows status is 'DENY'
|
|
93
|
+
* }
|
|
94
|
+
*/
|
|
95
|
+
is<T extends S>(status: T): this is Outcome<T, D> {
|
|
96
|
+
return (this.status as string) === status;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// ─── Exhaustive match ──────────────────────────────────────────────
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Exhaustive pattern match over all possible statuses.
|
|
103
|
+
* TypeScript enforces that every status has a handler.
|
|
104
|
+
*
|
|
105
|
+
* @example
|
|
106
|
+
* outcome.match({
|
|
107
|
+
* ALLOW: (o) => handleAllow(o.data),
|
|
108
|
+
* DENY: (o) => handleDeny(o.data.violations),
|
|
109
|
+
* })
|
|
110
|
+
*/
|
|
111
|
+
match<THandlers extends { [K in S]: (outcome: Outcome<K, D>) => unknown }>(
|
|
112
|
+
handlers: THandlers,
|
|
113
|
+
): ReturnType<THandlers[S]> {
|
|
114
|
+
const handler = (
|
|
115
|
+
handlers as Partial<Record<string, (o: unknown) => unknown>>
|
|
116
|
+
)[this.status];
|
|
117
|
+
|
|
118
|
+
if (typeof handler !== "function") {
|
|
119
|
+
const availableStatuses = Object.keys(handlers);
|
|
120
|
+
throw new InvariantViolationException(
|
|
121
|
+
`Outcome.match is missing a handler for runtime status "${this.status}". Available handlers: ${availableStatuses.join(", ") || "(none)"}`,
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return handler(this) as ReturnType<THandlers[S]>;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// ─── Derived copies ────────────────────────────────────────────────
|
|
129
|
+
|
|
130
|
+
withMetadata(extra: Record<string, unknown>): Outcome<S, D> {
|
|
131
|
+
return new Outcome(
|
|
132
|
+
this.status,
|
|
133
|
+
this.data,
|
|
134
|
+
this.reason,
|
|
135
|
+
Object.freeze({ ...this.metadata, ...extra }),
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
withReason(reason: string): Outcome<S, D> {
|
|
140
|
+
return new Outcome(this.status, this.data, reason, this.metadata);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// ─── Debug ─────────────────────────────────────────────────────────
|
|
144
|
+
|
|
145
|
+
toString(): string {
|
|
146
|
+
const parts: string[] = [this.status];
|
|
147
|
+
if (this.reason) parts.push(`reason="${this.reason}"`);
|
|
148
|
+
return `Outcome(${parts.join(", ")})`;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/** Common business outcome statuses. Extend per domain as needed. */
|
|
153
|
+
export type CommonOutcomeStatus =
|
|
154
|
+
| "APPROVED"
|
|
155
|
+
| "REJECTED"
|
|
156
|
+
| "NO_OP"
|
|
157
|
+
| "DEFERRED"
|
|
158
|
+
| "REQUIRES_REVIEW";
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Represents the outcome of an operation that may succeed or fail.
|
|
3
|
+
* Useful for avoiding excessive exceptions and making error flows explicit.
|
|
4
|
+
*/
|
|
5
|
+
export abstract class Result<T, E> {
|
|
6
|
+
protected constructor() {
|
|
7
|
+
// prevent direct instantiation
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Creates a success result containing a value.
|
|
12
|
+
*/
|
|
13
|
+
static ok<T>(value: T): Result<T, never> {
|
|
14
|
+
return new Ok(value);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Creates an error result containing an error object.
|
|
19
|
+
*/
|
|
20
|
+
static err<E>(error: E): Result<never, E> {
|
|
21
|
+
return new Err(error);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Returns true when this is a success result.
|
|
26
|
+
*/
|
|
27
|
+
abstract isOk(): this is Ok<T>;
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Returns true when this is an error result.
|
|
31
|
+
*/
|
|
32
|
+
abstract isErr(): this is Err<E>;
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Runs a handler based on the result state (similar to pattern matching).
|
|
36
|
+
*/
|
|
37
|
+
abstract match<R>(handlers: { ok: (value: T) => R; err: (error: E) => R }): R;
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Returns the value when this is success, or null when this is an error.
|
|
41
|
+
*/
|
|
42
|
+
getOrNull(): T | null {
|
|
43
|
+
return this.match({
|
|
44
|
+
ok: (value) => value,
|
|
45
|
+
err: () => null,
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Returns the error when this is a failure, or null when this is a success.
|
|
51
|
+
*/
|
|
52
|
+
errorOrNull(): E | null {
|
|
53
|
+
return this.match({
|
|
54
|
+
ok: () => null,
|
|
55
|
+
err: (error) => error,
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Returns the value when this is a success, or throws the error when this is a failure.
|
|
61
|
+
*/
|
|
62
|
+
getOrThrow(): T {
|
|
63
|
+
return this.match({
|
|
64
|
+
ok: (value) => value,
|
|
65
|
+
err: (error) => {
|
|
66
|
+
throw error instanceof Error ? error : new Error(String(error));
|
|
67
|
+
},
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Transforms the success value using the provided function. Keeps the error when this is a failure.
|
|
73
|
+
*/
|
|
74
|
+
map<R>(transform: (value: T) => R): Result<R, E> {
|
|
75
|
+
return this.match<Result<R, E>>({
|
|
76
|
+
ok: (value) => Result.ok(transform(value)),
|
|
77
|
+
// TS does not "see" variance here; the cast is intentional and safe:
|
|
78
|
+
// Err carries no T, so it can be widened to Result<R, E> safely.
|
|
79
|
+
err: (error) => Result.err(error) as unknown as Result<R, E>,
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Transforms the error using the provided function. Keeps the value when this is a success.
|
|
85
|
+
*/
|
|
86
|
+
mapError<F>(transform: (error: E) => F): Result<T, F> {
|
|
87
|
+
return this.match<Result<T, F>>({
|
|
88
|
+
// Ok carries no E, so it can be widened to Result<T, F>.
|
|
89
|
+
ok: (value) => Result.ok(value) as unknown as Result<T, F>,
|
|
90
|
+
err: (error) => Result.err(transform(error)),
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Chains another operation that returns a Result when this result is a success.
|
|
96
|
+
*/
|
|
97
|
+
flatMap<R>(transform: (value: T) => Result<R, E>): Result<R, E> {
|
|
98
|
+
return this.match<Result<R, E>>({
|
|
99
|
+
ok: (value) => transform(value),
|
|
100
|
+
err: (error) => Result.err(error) as unknown as Result<R, E>,
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Success variant of Result.
|
|
107
|
+
*/
|
|
108
|
+
export class Ok<T> extends Result<T, never> {
|
|
109
|
+
public readonly value: T;
|
|
110
|
+
|
|
111
|
+
constructor(value: T) {
|
|
112
|
+
super();
|
|
113
|
+
this.value = value;
|
|
114
|
+
Object.freeze(this);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
isOk(): this is Ok<T> {
|
|
118
|
+
return true;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
isErr(): this is Err<never> {
|
|
122
|
+
return false;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
match<R>(handlers: { ok: (value: T) => R; err: (error: never) => R }): R {
|
|
126
|
+
return handlers.ok(this.value);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Error variant of Result.
|
|
132
|
+
*/
|
|
133
|
+
export class Err<E> extends Result<never, E> {
|
|
134
|
+
public readonly error: E;
|
|
135
|
+
|
|
136
|
+
constructor(error: E) {
|
|
137
|
+
super();
|
|
138
|
+
this.error = error;
|
|
139
|
+
Object.freeze(this);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
isOk(): this is Ok<never> {
|
|
143
|
+
return false;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
isErr(): this is Err<E> {
|
|
147
|
+
return true;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
match<R>(handlers: { ok: (value: never) => R; err: (error: E) => R }): R {
|
|
151
|
+
return handlers.err(this.error);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { InvariantViolationException } from "../exceptions/invariant-violation-exception";
|
|
2
|
+
|
|
3
|
+
function assertValidAggregateVersion(aggregateVersion: number): void {
|
|
4
|
+
if (!Number.isInteger(aggregateVersion) || aggregateVersion < 0) {
|
|
5
|
+
throw new InvariantViolationException(
|
|
6
|
+
`aggregateVersion must be a non-negative integer. Received: ${aggregateVersion}`,
|
|
7
|
+
);
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export { assertValidAggregateVersion };
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
|
|
3
|
+
function sha256Hex(value: string): string {
|
|
4
|
+
return createHash("sha256").update(value, "utf8").digest("hex");
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
function sortKeysDeep(value: unknown): unknown {
|
|
8
|
+
if (Array.isArray(value)) {
|
|
9
|
+
return value.map((item) => sortKeysDeep(item));
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
if (value instanceof Date) {
|
|
13
|
+
return value;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
if (value && typeof value === "object") {
|
|
17
|
+
const record = value as Record<string, unknown>;
|
|
18
|
+
const ordered = Object.create(null) as Record<string, unknown>;
|
|
19
|
+
|
|
20
|
+
for (const key of Object.keys(record).sort()) {
|
|
21
|
+
ordered[key] = sortKeysDeep(record[key]);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return ordered;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return value;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function stableStringify(value: unknown): string {
|
|
31
|
+
return JSON.stringify(sortKeysDeep(value));
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function payloadHash(value: unknown): string {
|
|
35
|
+
return sha256Hex(stableStringify(value));
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export { payloadHash, sha256Hex, stableStringify };
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
type PrimitiveValue =
|
|
2
|
+
| bigint
|
|
3
|
+
| boolean
|
|
4
|
+
| null
|
|
5
|
+
| number
|
|
6
|
+
| string
|
|
7
|
+
| symbol
|
|
8
|
+
| undefined;
|
|
9
|
+
|
|
10
|
+
type DeepReadonly<T> = T extends PrimitiveValue | Date
|
|
11
|
+
? T
|
|
12
|
+
: T extends readonly (infer TItem)[]
|
|
13
|
+
? readonly DeepReadonly<TItem>[]
|
|
14
|
+
: { readonly [TKey in keyof T]: DeepReadonly<T[TKey]> };
|
|
15
|
+
|
|
16
|
+
function deepFreeze<T>(value: T): T {
|
|
17
|
+
if (typeof value !== "object" || value === null || value instanceof Date) {
|
|
18
|
+
return value;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
if (Array.isArray(value)) {
|
|
22
|
+
for (const item of value) {
|
|
23
|
+
deepFreeze(item);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return Object.freeze(value);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const objectValue = value as Record<PropertyKey, object | PrimitiveValue>;
|
|
30
|
+
|
|
31
|
+
for (const propertyKey of Reflect.ownKeys(objectValue)) {
|
|
32
|
+
deepFreeze(objectValue[propertyKey]);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return Object.freeze(value);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function makeImmutable<T>(value: T): DeepReadonly<T> {
|
|
39
|
+
if (typeof value !== "object" || value === null) {
|
|
40
|
+
return value as DeepReadonly<T>;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return deepFreeze(structuredClone(value)) as DeepReadonly<T>;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export { deepFreeze, makeImmutable, type DeepReadonly };
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { InvariantViolationException } from "../exceptions/invariant-violation-exception";
|
|
2
|
+
|
|
3
|
+
function isValidDate(value: unknown): value is Date {
|
|
4
|
+
return value instanceof Date && !Number.isNaN(value.getTime());
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
function cloneDate(date: Date): Date {
|
|
8
|
+
return new Date(date.getTime());
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function assertValidDate(
|
|
12
|
+
fieldName: string,
|
|
13
|
+
value: unknown,
|
|
14
|
+
): asserts value is Date {
|
|
15
|
+
if (!isValidDate(value)) {
|
|
16
|
+
throw new InvariantViolationException(
|
|
17
|
+
`${fieldName} must be a valid Date instance`,
|
|
18
|
+
);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export { assertValidDate, cloneDate, isValidDate };
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { UseCase } from "../application/use-case";
|
|
2
|
+
import { Entity } from "../domain/entity";
|
|
3
|
+
import { ValueObject } from "../domain/value-object";
|
|
4
|
+
import { assertValidAggregateVersion } from "../shared/aggregate-version";
|
|
5
|
+
import { type ContractVersion } from "./version";
|
|
6
|
+
|
|
7
|
+
interface DomainEventContractSelection {
|
|
8
|
+
readonly entity?: boolean;
|
|
9
|
+
readonly valueObject?: boolean;
|
|
10
|
+
readonly useCase?: boolean;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
interface DomainEventContractVersions {
|
|
14
|
+
readonly entity_contract?: ContractVersion;
|
|
15
|
+
readonly value_object_contract?: ContractVersion;
|
|
16
|
+
readonly use_case_contract?: ContractVersion;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
interface CreateDomainEventEnvelopeInput<TPayload> {
|
|
20
|
+
readonly aggregateVersion: number;
|
|
21
|
+
readonly payload: TPayload;
|
|
22
|
+
readonly contracts: DomainEventContractSelection;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
interface DomainEventEnvelope<TPayload> extends DomainEventContractVersions {
|
|
26
|
+
readonly aggregate_version: number;
|
|
27
|
+
readonly payload: TPayload;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function buildDomainEventContractVersions(
|
|
31
|
+
selection: DomainEventContractSelection,
|
|
32
|
+
): DomainEventContractVersions {
|
|
33
|
+
const contractVersions: {
|
|
34
|
+
entity_contract?: ContractVersion;
|
|
35
|
+
value_object_contract?: ContractVersion;
|
|
36
|
+
use_case_contract?: ContractVersion;
|
|
37
|
+
} = {};
|
|
38
|
+
|
|
39
|
+
if (selection.entity) {
|
|
40
|
+
contractVersions.entity_contract = Entity.CONTRACT_VERSION;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (selection.valueObject) {
|
|
44
|
+
contractVersions.value_object_contract = ValueObject.CONTRACT_VERSION;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (selection.useCase) {
|
|
48
|
+
contractVersions.use_case_contract = UseCase.CONTRACT_VERSION;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return Object.freeze(contractVersions);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function createDomainEventEnvelope<TPayload>(
|
|
55
|
+
input: CreateDomainEventEnvelopeInput<TPayload>,
|
|
56
|
+
): DomainEventEnvelope<TPayload> {
|
|
57
|
+
assertValidAggregateVersion(input.aggregateVersion);
|
|
58
|
+
|
|
59
|
+
return Object.freeze({
|
|
60
|
+
aggregate_version: input.aggregateVersion,
|
|
61
|
+
payload: input.payload,
|
|
62
|
+
...buildDomainEventContractVersions(input.contracts),
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export {
|
|
67
|
+
buildDomainEventContractVersions,
|
|
68
|
+
createDomainEventEnvelope,
|
|
69
|
+
type CreateDomainEventEnvelopeInput,
|
|
70
|
+
type DomainEventContractSelection,
|
|
71
|
+
type DomainEventContractVersions,
|
|
72
|
+
type DomainEventEnvelope,
|
|
73
|
+
};
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
type ContractVersion = `${number}.${number}`;
|
|
2
|
+
|
|
3
|
+
type VersionedTarget = {
|
|
4
|
+
readonly prototype: object;
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
const CONTRACT_VERSION_PROPERTY = "CONTRACT_VERSION" as const;
|
|
8
|
+
const CONTRACT_VERSION_PATTERN = /^\d+\.\d+$/;
|
|
9
|
+
|
|
10
|
+
function version<const TVersion extends ContractVersion>(
|
|
11
|
+
contractVersion: TVersion,
|
|
12
|
+
) {
|
|
13
|
+
if (!CONTRACT_VERSION_PATTERN.test(contractVersion)) {
|
|
14
|
+
throw new TypeError(
|
|
15
|
+
`Invalid contract version "${contractVersion}". Expected MAJOR.MINOR.`,
|
|
16
|
+
);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
return (target: VersionedTarget): void => {
|
|
20
|
+
Object.defineProperty(target, CONTRACT_VERSION_PROPERTY, {
|
|
21
|
+
value: contractVersion,
|
|
22
|
+
writable: false,
|
|
23
|
+
configurable: false,
|
|
24
|
+
enumerable: false,
|
|
25
|
+
});
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export {
|
|
30
|
+
CONTRACT_VERSION_PROPERTY,
|
|
31
|
+
type ContractVersion,
|
|
32
|
+
version,
|
|
33
|
+
type VersionedTarget,
|
|
34
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "../core/errors/index.js";
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { DomainException } from "../../../core/domain/rulesets/entity-ruleset.contracts";
|
|
2
|
+
import { type OrderCreationData, type OrderCreationRuleset } from "./order";
|
|
3
|
+
|
|
4
|
+
class OrderCreationValidationError extends DomainException {}
|
|
5
|
+
|
|
6
|
+
class OrderCreationRulesV1 implements OrderCreationRuleset {
|
|
7
|
+
readonly id = "order-creation@1.0" as const;
|
|
8
|
+
readonly description = "Order creation rules — version 1.0";
|
|
9
|
+
|
|
10
|
+
validate(data: OrderCreationData): void {
|
|
11
|
+
if (!data.customerId) {
|
|
12
|
+
throw new OrderCreationValidationError("customerId is required.");
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
if (!data.items || data.items.length === 0) {
|
|
16
|
+
throw new OrderCreationValidationError(
|
|
17
|
+
"The order must contain at least one item.",
|
|
18
|
+
);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
for (const item of data.items) {
|
|
22
|
+
if (item.quantity <= 0) {
|
|
23
|
+
throw new OrderCreationValidationError(
|
|
24
|
+
`Item "${item.sku}" quantity must be greater than zero. Received: ${item.quantity}.`,
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export { OrderCreationRulesV1 };
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import {
|
|
2
|
+
DomainException,
|
|
3
|
+
type RulesetId,
|
|
4
|
+
} from "../../../core/domain/rulesets/entity-ruleset.contracts";
|
|
5
|
+
import {
|
|
6
|
+
type Order,
|
|
7
|
+
type OrderInvariantRuleset,
|
|
8
|
+
type OrderItem,
|
|
9
|
+
} from "./order";
|
|
10
|
+
|
|
11
|
+
class OrderInvariantError extends DomainException {}
|
|
12
|
+
|
|
13
|
+
class OrderInvariantsV1 implements OrderInvariantRuleset {
|
|
14
|
+
readonly id: RulesetId = "order-invariants@1.0";
|
|
15
|
+
readonly description: string = "Order invariants — version 1.0";
|
|
16
|
+
|
|
17
|
+
assertCanAddItem(order: Order, _item: OrderItem): void {
|
|
18
|
+
if (order.status !== "open") {
|
|
19
|
+
throw new OrderInvariantError(
|
|
20
|
+
`Cannot add an item to an order with status "${order.status}". The order must be open.`,
|
|
21
|
+
);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
assertCanCancel(order: Order): void {
|
|
26
|
+
if (order.status === "delivered") {
|
|
27
|
+
throw new OrderInvariantError(
|
|
28
|
+
"Cannot cancel an order that has already been delivered.",
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
assertCanCheckout(order: Order): void {
|
|
34
|
+
if (order.items.length === 0) {
|
|
35
|
+
throw new OrderInvariantError("Cannot check out an order with no items.");
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
assertCanMarkAsDelivered(order: Order): void {
|
|
40
|
+
if (order.status !== "shipped") {
|
|
41
|
+
throw new OrderInvariantError(
|
|
42
|
+
`Cannot mark as delivered an order with status "${order.status}". The order must be shipped.`,
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export { OrderInvariantsV1 };
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import {
|
|
2
|
+
DomainException,
|
|
3
|
+
type RulesetId,
|
|
4
|
+
} from "../../../core/domain/rulesets/entity-ruleset.contracts";
|
|
5
|
+
import { type Order } from "./order";
|
|
6
|
+
import { OrderInvariantsV1 } from "./order-invariants-v1";
|
|
7
|
+
|
|
8
|
+
class OrderCancellationWindowError extends DomainException {}
|
|
9
|
+
|
|
10
|
+
const FORTY_EIGHT_HOURS_MS = 48 * 60 * 60 * 1000;
|
|
11
|
+
|
|
12
|
+
class OrderInvariantsV2 extends OrderInvariantsV1 {
|
|
13
|
+
override readonly id: RulesetId = "order-invariants@2.0";
|
|
14
|
+
override readonly description = "Order invariants — version 2.0";
|
|
15
|
+
|
|
16
|
+
override assertCanCancel(order: Order): void {
|
|
17
|
+
super.assertCanCancel(order);
|
|
18
|
+
|
|
19
|
+
const ageMs = Date.now() - order.createdAt.getTime();
|
|
20
|
+
if (ageMs > FORTY_EIGHT_HOURS_MS) {
|
|
21
|
+
throw new OrderCancellationWindowError(
|
|
22
|
+
"Cannot cancel an order created more than 48 hours ago.",
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export { OrderInvariantsV2 };
|