@cullet/erp-core 1.0.10 → 1.1.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 +67 -0
- package/README.md +128 -1
- package/dist/app-error.d.ts +108 -0
- package/dist/application/index.d.ts +2 -0
- package/dist/application/index.js +2 -0
- package/dist/errors/index.d.ts +2 -1
- package/dist/errors/index.js +4 -2
- package/dist/gate-engine-registry.d.ts +80 -0
- package/dist/gate-v1-payload.schema.js +2 -28
- package/dist/gate-v1-payload.schema.js.map +1 -1
- package/dist/hashing.js +49 -0
- package/dist/hashing.js.map +1 -0
- package/dist/index.d.ts +122 -45
- package/dist/index.js +104 -85
- package/dist/index.js.map +1 -1
- package/dist/not-found-error.js +54 -0
- package/dist/not-found-error.js.map +1 -0
- package/dist/outcome.js +1 -1
- package/dist/parse-gate-payload.d.ts +3 -78
- package/dist/path.d.ts +89 -0
- package/dist/policies/engines/index.d.ts +2 -1
- package/dist/policies/index.d.ts +4 -2
- package/dist/policy-service.d.ts +102 -86
- package/dist/policy-service.js +78 -16
- package/dist/policy-service.js.map +1 -1
- package/dist/temporal-guards.js +30 -0
- package/dist/temporal-guards.js.map +1 -0
- package/dist/temporal-use-case.d.ts +309 -0
- package/dist/temporal-use-case.js +284 -0
- package/dist/temporal-use-case.js.map +1 -0
- package/dist/validation-code.js +34 -47
- package/dist/validation-code.js.map +1 -1
- package/dist/validation-error.d.ts +171 -74
- package/dist/validation-error.js +152 -38
- package/dist/validation-error.js.map +1 -1
- package/dist/validation-exception.js +18 -0
- package/dist/validation-exception.js.map +1 -0
- package/meta.json +4 -2
- package/package.json +5 -1
- package/src/application/index.ts +1 -0
- package/src/core/application/commands/index.ts +1 -0
- package/src/core/application/index.ts +12 -3
- package/src/core/application/ports/index.ts +2 -2
- package/src/core/application/ports/policy-port.ts +13 -3
- package/src/core/application/ports/repository.port.ts +37 -1
- package/src/core/application/temporal/temporal-use-case.ts +14 -2
- package/src/core/application/use-case.ts +133 -2
- package/src/core/domain/entity.ts +71 -0
- package/src/core/domain/value-object.ts +41 -0
- package/src/core/errors/app-error.ts +33 -0
- package/src/core/errors/authorization-error.ts +43 -0
- package/src/core/errors/conflict-error.ts +69 -0
- package/src/core/errors/integration-error.ts +25 -0
- package/src/core/errors/not-found-error.ts +14 -0
- package/src/core/errors/validation-error.ts +18 -0
- package/src/core/index.ts +1 -10
- package/src/core/policies/catalog/policy-catalog.ts +36 -0
- package/src/core/policies/resolver/policy-resolver.ts +10 -0
- package/src/core/policies/service/policy-service.ts +53 -0
- package/src/examples/application/cancel-order.example.ts +124 -0
- package/src/examples/application/in-memory-account-repository.example.ts +73 -0
- package/src/version.ts +1 -1
|
@@ -9,7 +9,25 @@ import type { AppErrorOptions } from "./types.js";
|
|
|
9
9
|
// ValidationError
|
|
10
10
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
11
11
|
|
|
12
|
+
/**
|
|
13
|
+
* Raised when a single input fails a validation rule — the canonical
|
|
14
|
+
* "this field is wrong, and here is why" error.
|
|
15
|
+
*
|
|
16
|
+
* It pins the offending `field` and a machine-readable `validationCode` into
|
|
17
|
+
* the metadata (merged ahead of any caller-supplied metadata) so an API layer
|
|
18
|
+
* can map the failure straight onto a form field without parsing the message.
|
|
19
|
+
* The fixed {@link ErrorCodes.validation} code lets callers catch all
|
|
20
|
+
* validation failures uniformly while still distinguishing the specific rule
|
|
21
|
+
* through `validationCode`.
|
|
22
|
+
*/
|
|
12
23
|
class ValidationError extends AppError {
|
|
24
|
+
/**
|
|
25
|
+
* @param field - The input that failed validation; surfaced as `metadata.field`.
|
|
26
|
+
* @param code - The specific rule that was violated; surfaced as `metadata.validationCode`.
|
|
27
|
+
* @param message - The developer-facing description of the failure.
|
|
28
|
+
* @param options - Optional cause, correlation ids, and extra metadata
|
|
29
|
+
* (merged over the field/code metadata, never overwriting it by accident).
|
|
30
|
+
*/
|
|
13
31
|
constructor(
|
|
14
32
|
field: ValidationField,
|
|
15
33
|
code: ValidationCode,
|
package/src/core/index.ts
CHANGED
|
@@ -1,15 +1,6 @@
|
|
|
1
1
|
export * from "./errors/index.js";
|
|
2
2
|
export * from "./exceptions/validation-field.js";
|
|
3
|
-
export
|
|
4
|
-
LogPayload,
|
|
5
|
-
LoggerPort,
|
|
6
|
-
MetricLabels,
|
|
7
|
-
MetricsPort,
|
|
8
|
-
TraceAttributeValue,
|
|
9
|
-
TraceSpan,
|
|
10
|
-
TracerPort,
|
|
11
|
-
} from "./application/index.js";
|
|
12
|
-
export { mapPolicyEvaluationError } from "./application/policy-error-mapper.js";
|
|
3
|
+
export * from "./application/index.js";
|
|
13
4
|
export { Entity, type EntityState } from "./domain/entity.js";
|
|
14
5
|
export { ValueObject, type DeepReadonly } from "./domain/value-object.js";
|
|
15
6
|
export * from "./policies/index.js";
|
|
@@ -14,6 +14,15 @@ export class PolicyCatalog {
|
|
|
14
14
|
readonly PolicyCatalogEntry[]
|
|
15
15
|
>;
|
|
16
16
|
|
|
17
|
+
/**
|
|
18
|
+
* Indexes the given entries up front into two lookups — one per exact
|
|
19
|
+
* variant, one per policy key (the "family") — and validates them eagerly:
|
|
20
|
+
* a duplicate variant or an internally inconsistent family throws here, at
|
|
21
|
+
* wiring time, so a malformed catalog fails fast at startup rather than on
|
|
22
|
+
* the first evaluation.
|
|
23
|
+
*
|
|
24
|
+
* @throws {UnexpectedError} When two entries resolve to the same variant key.
|
|
25
|
+
*/
|
|
17
26
|
constructor(entries: readonly PolicyCatalogEntry[]) {
|
|
18
27
|
const families = new Map<string, PolicyCatalogEntry[]>();
|
|
19
28
|
const versionedMap = new Map<string, PolicyCatalogEntry>();
|
|
@@ -43,6 +52,14 @@ export class PolicyCatalog {
|
|
|
43
52
|
this.entriesByKey = families;
|
|
44
53
|
}
|
|
45
54
|
|
|
55
|
+
/**
|
|
56
|
+
* Returns the single entry for a key. Deliberately strict: if the key has
|
|
57
|
+
* more than one variant it errs rather than guessing, steering the caller to
|
|
58
|
+
* {@link getVersioned} or {@link getFamily}. Use this only when a key is
|
|
59
|
+
* known to be unambiguous.
|
|
60
|
+
*
|
|
61
|
+
* @returns `ok` with the entry, or `err` when the key is unknown or ambiguous.
|
|
62
|
+
*/
|
|
46
63
|
get(key: string): Result<PolicyCatalogEntry, string> {
|
|
47
64
|
const familyResult = this.getFamily(key);
|
|
48
65
|
if (familyResult.isErr()) {
|
|
@@ -64,6 +81,13 @@ export class PolicyCatalog {
|
|
|
64
81
|
return Result.ok(entry);
|
|
65
82
|
}
|
|
66
83
|
|
|
84
|
+
/**
|
|
85
|
+
* Returns every variant registered under a key — the whole "family". This is
|
|
86
|
+
* the lookup the evaluation pipeline uses, since a single key may host
|
|
87
|
+
* several engine/schema variants that the resolver then chooses between.
|
|
88
|
+
*
|
|
89
|
+
* @returns `ok` with the family (always non-empty), or `err` when the key is unknown.
|
|
90
|
+
*/
|
|
67
91
|
getFamily(key: string): Result<readonly PolicyCatalogEntry[], string> {
|
|
68
92
|
const family = this.entriesByKey.get(key);
|
|
69
93
|
if (!family) {
|
|
@@ -73,6 +97,16 @@ export class PolicyCatalog {
|
|
|
73
97
|
return Result.ok(family);
|
|
74
98
|
}
|
|
75
99
|
|
|
100
|
+
/**
|
|
101
|
+
* Resolves the one entry matching an exact variant — kind plus engine and
|
|
102
|
+
* payload-schema versions. Falls back to the sole family member when a key
|
|
103
|
+
* has exactly one entry and that entry declares no explicit version
|
|
104
|
+
* selector, so unversioned single-variant policies "just work" without the
|
|
105
|
+
* caller spelling out versions.
|
|
106
|
+
*
|
|
107
|
+
* @param params - The key and the variant coordinates to match on.
|
|
108
|
+
* @returns `ok` with the matching entry, or `err` when no variant matches.
|
|
109
|
+
*/
|
|
76
110
|
getVersioned(params: {
|
|
77
111
|
readonly key: string;
|
|
78
112
|
readonly kind: "GATE" | "COMPUTE";
|
|
@@ -114,10 +148,12 @@ export class PolicyCatalog {
|
|
|
114
148
|
);
|
|
115
149
|
}
|
|
116
150
|
|
|
151
|
+
/** Lists every registered variant across all keys — useful for introspection and diagnostics. */
|
|
117
152
|
list(): readonly PolicyCatalogEntry[] {
|
|
118
153
|
return Array.from(this.versionedEntries.values());
|
|
119
154
|
}
|
|
120
155
|
|
|
156
|
+
/** Returns whether any variant is registered under the given key. */
|
|
121
157
|
has(key: string): boolean {
|
|
122
158
|
return this.entriesByKey.has(key);
|
|
123
159
|
}
|
|
@@ -14,6 +14,16 @@ import { Result } from "../../result/result.js";
|
|
|
14
14
|
* 5. policyVersion descending (semver — final deterministic tiebreaker)
|
|
15
15
|
*/
|
|
16
16
|
export class PolicyResolver {
|
|
17
|
+
/**
|
|
18
|
+
* Picks the single winning definition from an already-filtered candidate
|
|
19
|
+
* list by applying the class's ordering criteria in priority order. The sort
|
|
20
|
+
* runs over a copy, so the caller's array is left untouched, and the final
|
|
21
|
+
* semver tiebreaker guarantees the winner is fully deterministic — it never
|
|
22
|
+
* depends on the order the repository returned the candidates in.
|
|
23
|
+
*
|
|
24
|
+
* @param candidates - Definitions already filtered to match key, scope, and as-of.
|
|
25
|
+
* @returns `ok` with the highest-ranked definition, or `err` when the list is empty.
|
|
26
|
+
*/
|
|
17
27
|
resolveBest(
|
|
18
28
|
candidates: readonly PolicyDefinition[],
|
|
19
29
|
): Result<PolicyDefinition, string> {
|
|
@@ -36,6 +36,13 @@ import {
|
|
|
36
36
|
|
|
37
37
|
// ─── Input ──────────────────────────────────────────────────────────────────
|
|
38
38
|
|
|
39
|
+
/**
|
|
40
|
+
* Everything a single policy evaluation needs. `policyKey` names the policy,
|
|
41
|
+
* `scopeChain` narrows the search from most-specific to least-specific scope,
|
|
42
|
+
* `contextVersion` pins which context schema applies, and `seed` carries the
|
|
43
|
+
* raw facts the context builder expands. `decisionId` is the caller's
|
|
44
|
+
* idempotency/audit handle, echoed back on the result.
|
|
45
|
+
*/
|
|
39
46
|
export interface EvaluateInput {
|
|
40
47
|
readonly decisionId: PolicyDecisionId;
|
|
41
48
|
readonly policyKey: string;
|
|
@@ -44,11 +51,23 @@ export interface EvaluateInput {
|
|
|
44
51
|
readonly seed: ContextSeed;
|
|
45
52
|
}
|
|
46
53
|
|
|
54
|
+
/**
|
|
55
|
+
* Cross-cutting knobs for a {@link PolicyService}: how the as-of instant is
|
|
56
|
+
* derived ({@link asOf}) and where evaluation telemetry is sent
|
|
57
|
+
* ({@link reporter}). Both are optional; the service defaults to a silent
|
|
58
|
+
* reporter so telemetry is opt-in.
|
|
59
|
+
*/
|
|
47
60
|
export interface PolicyServiceOptions {
|
|
48
61
|
readonly asOf?: DeriveAsOfOptions;
|
|
49
62
|
readonly reporter?: PolicyReporter;
|
|
50
63
|
}
|
|
51
64
|
|
|
65
|
+
/**
|
|
66
|
+
* The collaborators a {@link PolicyService} is wired with. Each is an injected
|
|
67
|
+
* port so the service stays storage- and engine-agnostic: the catalog defines
|
|
68
|
+
* the universe of policies, `defRepo` supplies their versioned definitions, the
|
|
69
|
+
* `resolver` picks the winning definition, and the engine registries execute it.
|
|
70
|
+
*/
|
|
52
71
|
export interface PolicyServiceParams {
|
|
53
72
|
readonly catalog: PolicyCatalog;
|
|
54
73
|
readonly contextBuilder: PolicyContextBuilder;
|
|
@@ -66,6 +85,13 @@ export type PolicyDecision = GateOutcome | ComputeOutcome;
|
|
|
66
85
|
|
|
67
86
|
export type { PolicyEvaluationError } from "./policy-evaluation-error.js";
|
|
68
87
|
|
|
88
|
+
/**
|
|
89
|
+
* The full record of one successful evaluation. Beyond the business
|
|
90
|
+
* {@link decision}, it pins the exact definition that produced it — id, key,
|
|
91
|
+
* version, payload hash, and engine version — together with the `asOf` instant
|
|
92
|
+
* the policy was selected for and the wall-clock `evaluatedAt`. That provenance
|
|
93
|
+
* is what makes a decision reproducible and auditable after the fact.
|
|
94
|
+
*/
|
|
69
95
|
export interface PolicyEvaluationResult {
|
|
70
96
|
readonly decisionId: PolicyDecisionId;
|
|
71
97
|
readonly ref: {
|
|
@@ -91,6 +117,18 @@ interface ResolvedPolicyCandidate {
|
|
|
91
117
|
|
|
92
118
|
// ─── Service ────────────────────────────────────────────────────────────────
|
|
93
119
|
|
|
120
|
+
/**
|
|
121
|
+
* Orchestrates the end-to-end evaluation of a policy: it parses and validates
|
|
122
|
+
* the context seed, derives the as-of instant, resolves the best matching
|
|
123
|
+
* definition for the requested key/scope/version, builds the context the policy
|
|
124
|
+
* needs, and runs it through the right engine (gate or compute).
|
|
125
|
+
*
|
|
126
|
+
* The service never throws for an expected failure: every step returns a
|
|
127
|
+
* {@link Result}, and the failures are folded into a typed
|
|
128
|
+
* {@link PolicyEvaluationError} so callers branch on `isErr()` rather than
|
|
129
|
+
* catching. Telemetry is best-effort — a throwing reporter can never abort an
|
|
130
|
+
* evaluation — which keeps observability strictly side-band.
|
|
131
|
+
*/
|
|
94
132
|
export class PolicyService {
|
|
95
133
|
readonly #reporter: PolicyReporter;
|
|
96
134
|
private readonly catalog: PolicyCatalog;
|
|
@@ -136,6 +174,21 @@ export class PolicyService {
|
|
|
136
174
|
return Result.ok(candidate);
|
|
137
175
|
}
|
|
138
176
|
|
|
177
|
+
/**
|
|
178
|
+
* Evaluates a policy and returns its decision.
|
|
179
|
+
*
|
|
180
|
+
* This is the single public entry point. It delegates the actual work to the
|
|
181
|
+
* private pipeline and wraps it with telemetry: a completed or failed event
|
|
182
|
+
* is reported either way, but reporting is guarded so it can never change the
|
|
183
|
+
* outcome. The returned {@link Result} is `ok` with a
|
|
184
|
+
* {@link PolicyEvaluationResult} on success, or `err` with a typed
|
|
185
|
+
* {@link PolicyEvaluationError} describing which stage rejected the input —
|
|
186
|
+
* the method itself does not throw for expected failures.
|
|
187
|
+
*
|
|
188
|
+
* @param input - The policy key, scope chain, context version, and seed to
|
|
189
|
+
* evaluate, plus the caller's `decisionId`.
|
|
190
|
+
* @returns A `Result` carrying the decision or the evaluation error.
|
|
191
|
+
*/
|
|
139
192
|
async evaluate(
|
|
140
193
|
input: EvaluateInput,
|
|
141
194
|
): Promise<Result<PolicyEvaluationResult, PolicyEvaluationError>> {
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* End-to-end reference: a concrete {@link Command} wiring a
|
|
3
|
+
* {@link ResultRepository} and a {@link PolicyPort} together.
|
|
4
|
+
*
|
|
5
|
+
* Flow: load the aggregate → ask a declarative policy whether the action is
|
|
6
|
+
* allowed → mutate and persist. Every recoverable failure (infra error from
|
|
7
|
+
* the repository, a policy `DENY`, a missing aggregate, an optimistic-locking
|
|
8
|
+
* conflict on save) is returned as a typed `Result.err(...)` rather than
|
|
9
|
+
* thrown — the "errors as values" contract that `Command`/`UseCase` enforce.
|
|
10
|
+
*
|
|
11
|
+
* Consumers import these symbols from the package root (`@cullet/erp-core`);
|
|
12
|
+
* inside the kit we use relative paths so the example stays compilable and
|
|
13
|
+
* test-covered, guarding the snippet in the README/KIT_CONTEXT against rot.
|
|
14
|
+
*/
|
|
15
|
+
import { Command, type CommandInput } from "../../core/application/index.js";
|
|
16
|
+
import type {
|
|
17
|
+
PolicyPort,
|
|
18
|
+
ResultRepository,
|
|
19
|
+
} from "../../core/application/index.js";
|
|
20
|
+
import { AuthorizationError } from "../../core/errors/authorization-error.js";
|
|
21
|
+
import { NotFoundError } from "../../core/errors/not-found-error.js";
|
|
22
|
+
import {
|
|
23
|
+
asPolicyDecisionId,
|
|
24
|
+
asSchoolId,
|
|
25
|
+
asTenantId,
|
|
26
|
+
} from "../../core/policies/index.js";
|
|
27
|
+
import { Result } from "../../core/result/result.js";
|
|
28
|
+
|
|
29
|
+
// ─── Minimal domain ─────────────────────────────────────────────────────────
|
|
30
|
+
|
|
31
|
+
type OrderStatus = "OPEN" | "CANCELLED";
|
|
32
|
+
|
|
33
|
+
interface Order {
|
|
34
|
+
readonly id: string;
|
|
35
|
+
readonly status: OrderStatus;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// ─── Command contract ────────────────────────────────────────────────────────
|
|
39
|
+
|
|
40
|
+
// `CommandInput` forces every mutation to record *who* triggered it.
|
|
41
|
+
interface CancelOrderInput extends CommandInput {
|
|
42
|
+
readonly orderId: string;
|
|
43
|
+
readonly tenantId: string;
|
|
44
|
+
readonly schoolId: string;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
type CancelOrderError = NotFoundError | AuthorizationError;
|
|
48
|
+
|
|
49
|
+
// ─── The use case ─────────────────────────────────────────────────────────────
|
|
50
|
+
|
|
51
|
+
class CancelOrder extends Command<
|
|
52
|
+
CancelOrderInput,
|
|
53
|
+
Result<Order, CancelOrderError>
|
|
54
|
+
> {
|
|
55
|
+
constructor(
|
|
56
|
+
private readonly orders: ResultRepository<
|
|
57
|
+
Order,
|
|
58
|
+
string,
|
|
59
|
+
CancelOrderError
|
|
60
|
+
>,
|
|
61
|
+
private readonly policies: PolicyPort,
|
|
62
|
+
) {
|
|
63
|
+
super();
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
protected async execute(
|
|
67
|
+
input: CancelOrderInput,
|
|
68
|
+
): Promise<Result<Order, CancelOrderError>> {
|
|
69
|
+
// 1. Load the aggregate. Infra failures stay in band as Result.err.
|
|
70
|
+
const found = await this.orders.findById(input.orderId);
|
|
71
|
+
if (found.isErr()) {
|
|
72
|
+
return found;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const order = found.getOrThrow();
|
|
76
|
+
if (!order) {
|
|
77
|
+
return Result.err(
|
|
78
|
+
new NotFoundError("Order", { id: input.orderId }),
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// 2. Decide via a declarative policy (a GATE returns ALLOW/DENY).
|
|
83
|
+
const evaluated = await this.policies.evaluate({
|
|
84
|
+
decisionId: asPolicyDecisionId(input.orderId),
|
|
85
|
+
policyKey: "order.cancel",
|
|
86
|
+
scopeChain: [],
|
|
87
|
+
contextVersion: 1,
|
|
88
|
+
seed: {
|
|
89
|
+
tenantId: asTenantId(input.tenantId),
|
|
90
|
+
schoolId: asSchoolId(input.schoolId),
|
|
91
|
+
fields: { orderStatus: order.status },
|
|
92
|
+
},
|
|
93
|
+
});
|
|
94
|
+
if (evaluated.isErr()) {
|
|
95
|
+
return evaluated;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const result = evaluated.getOrThrow();
|
|
99
|
+
if (result.kind === "GATE" && result.decision.status === "DENY") {
|
|
100
|
+
return Result.err(
|
|
101
|
+
AuthorizationError.policyDenied({
|
|
102
|
+
action: "order.cancel",
|
|
103
|
+
resource: { type: "Order", id: order.id },
|
|
104
|
+
policyId: result.ref.definitionId,
|
|
105
|
+
policyVersion: Number(result.ref.policyVersion),
|
|
106
|
+
evaluatedAtIso: result.evaluatedAt.toISOString(),
|
|
107
|
+
}),
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// 3. Mutate and persist. A version conflict on save also surfaces as
|
|
112
|
+
// Result.err — never an exception crossing the boundary.
|
|
113
|
+
const cancelled: Order = { ...order, status: "CANCELLED" };
|
|
114
|
+
const saved = await this.orders.save(cancelled);
|
|
115
|
+
if (saved.isErr()) {
|
|
116
|
+
return saved;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return Result.ok(cancelled);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export { CancelOrder };
|
|
124
|
+
export type { CancelOrderError, CancelOrderInput, Order, OrderStatus };
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reference implementation of a {@link ResultRepository}: shows how a
|
|
3
|
+
* repository signals recoverable persistence outcomes as `Result` values
|
|
4
|
+
* instead of throwing.
|
|
5
|
+
*
|
|
6
|
+
* - `save` returns a `ConflictError` on an optimistic-concurrency version
|
|
7
|
+
* mismatch, otherwise persists and bumps the version.
|
|
8
|
+
* - `delete` returns a `NotFoundError` when the row is absent.
|
|
9
|
+
* - `findById` keeps `null` as the "not present" answer (a valid lookup
|
|
10
|
+
* result, not an error), still wrapped in a `Result` so genuine infra
|
|
11
|
+
* failures could stay in band.
|
|
12
|
+
*
|
|
13
|
+
* Consumers import `ResultRepository` from the package root
|
|
14
|
+
* (`@cullet/erp-core`); the relative paths here keep the example compilable
|
|
15
|
+
* and test-covered.
|
|
16
|
+
*/
|
|
17
|
+
import type { ResultRepository } from "../../core/application/index.js";
|
|
18
|
+
import {
|
|
19
|
+
AlreadyExistsError,
|
|
20
|
+
type ConflictError,
|
|
21
|
+
NotFoundError,
|
|
22
|
+
} from "../../core/errors/index.js";
|
|
23
|
+
import { Result } from "../../core/result/result.js";
|
|
24
|
+
|
|
25
|
+
interface Account {
|
|
26
|
+
readonly id: string;
|
|
27
|
+
readonly version: number;
|
|
28
|
+
readonly balance: number;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
type AccountError = NotFoundError | ConflictError;
|
|
32
|
+
|
|
33
|
+
class InMemoryAccountRepository implements ResultRepository<
|
|
34
|
+
Account,
|
|
35
|
+
string,
|
|
36
|
+
AccountError
|
|
37
|
+
> {
|
|
38
|
+
private readonly store = new Map<string, Account>();
|
|
39
|
+
|
|
40
|
+
seed(account: Account): void {
|
|
41
|
+
this.store.set(account.id, account);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async findById(id: string): Promise<Result<Account | null, AccountError>> {
|
|
45
|
+
return Result.ok(this.store.get(id) ?? null);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async save(entity: Account): Promise<Result<void, AccountError>> {
|
|
49
|
+
const current = this.store.get(entity.id);
|
|
50
|
+
if (current && current.version !== entity.version) {
|
|
51
|
+
return Result.err(
|
|
52
|
+
AlreadyExistsError.detected({
|
|
53
|
+
entity: "Account",
|
|
54
|
+
operation: "update",
|
|
55
|
+
existingId: entity.id,
|
|
56
|
+
}),
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
this.store.set(entity.id, { ...entity, version: entity.version + 1 });
|
|
60
|
+
return Result.ok(undefined);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async delete(id: string): Promise<Result<void, AccountError>> {
|
|
64
|
+
if (!this.store.has(id)) {
|
|
65
|
+
return Result.err(new NotFoundError("Account", { id }));
|
|
66
|
+
}
|
|
67
|
+
this.store.delete(id);
|
|
68
|
+
return Result.ok(undefined);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export { InMemoryAccountRepository };
|
|
73
|
+
export type { Account, AccountError };
|
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 = "1.0
|
|
7
|
+
export const version = "1.1.0";
|