@alveolus/arch 0.1.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -1
- package/dist/bin.mjs +4 -2
- package/dist/bin.mjs.map +1 -1
- package/dist/{cli-CwPCGjDg.mjs → docs-DsQHpTtV.mjs} +287 -38
- package/dist/docs-DsQHpTtV.mjs.map +1 -0
- package/dist/index.d.mts +90 -36
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +2 -2
- package/docs/core/application/command-handlers.md +617 -0
- package/docs/core/application/event-publishers.md +234 -0
- package/docs/core/application/event-translators.md +329 -0
- package/docs/core/application/index.md +99 -0
- package/docs/core/application/integration-events.md +277 -0
- package/docs/core/application/outbox.md +416 -0
- package/docs/core/application/query-handlers.md +292 -0
- package/docs/core/application/unit-of-work.md +352 -0
- package/docs/core/domain/aggregates.md +822 -0
- package/docs/core/domain/domain-errors.md +251 -0
- package/docs/core/domain/domain-events.md +292 -0
- package/docs/core/domain/domain-services.md +249 -0
- package/docs/core/domain/entities.md +431 -0
- package/docs/core/domain/index.md +93 -0
- package/docs/core/domain/ports.md +284 -0
- package/docs/core/domain/repositories.md +335 -0
- package/docs/core/domain/value-objects.md +425 -0
- package/docs/core/domain/views.md +265 -0
- package/docs/core/index.md +108 -0
- package/docs/core/strategic/anti-corruption-layers.md +349 -0
- package/docs/core/strategic/index.md +83 -0
- package/docs/core/strategic/open-host-services.md +287 -0
- package/docs/core/strategic/published-language.md +265 -0
- package/docs/core/utilities/result.md +413 -0
- package/docs/guide/agents.md +68 -0
- package/docs/guide/existing-project.md +105 -0
- package/docs/guide/getting-started.md +275 -0
- package/docs/guide/learning-path.md +123 -0
- package/docs/guide/project-layout.md +324 -0
- package/docs/guide/versioning.md +42 -0
- package/docs/integrations/index.md +112 -0
- package/docs/integrations/nestjs.md +169 -0
- package/docs/rules/index.md +183 -0
- package/docs/rules/layers/no-driving-shortcut.md +119 -0
- package/docs/rules/layers/no-impure-domain.md +189 -0
- package/docs/rules/layers/no-outward-import.md +184 -0
- package/docs/rules/layers/no-portless-adapter.md +123 -0
- package/docs/rules/strategic/no-cross-context-import.md +140 -0
- package/docs/rules/strategic/no-fat-shared-kernel.md +81 -0
- package/docs/rules/strategic/no-leaky-host-service.md +107 -0
- package/docs/rules/strategic/no-unmapped-context.md +111 -0
- package/docs/rules/tactical/no-aggregate-reference.md +139 -0
- package/docs/rules/tactical/no-foreign-command-dependency.md +119 -0
- package/docs/rules/tactical/no-foreign-query-dependency.md +106 -0
- package/docs/rules/tactical/no-loose-code.md +171 -0
- package/docs/rules/tactical/no-misplaced-class.md +146 -0
- package/docs/rules/tactical/no-public-field.md +113 -0
- package/docs/rules/tactical/no-stateful-service.md +102 -0
- package/docs/rules/tactical/no-thrown-failure.md +162 -0
- package/docs/rules/tooling/no-loose-disable.md +98 -0
- package/package.json +4 -3
- package/dist/cli-CwPCGjDg.mjs.map +0 -1
|
@@ -0,0 +1,413 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: "The Result type in TypeScript: the outcome of an operation that can fail for a business reason, ok with a value or err with a domain error."
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
# Result
|
|
6
|
+
|
|
7
|
+
A `Result` is the outcome of an operation that can fail for a business reason: `ok` with a value,
|
|
8
|
+
or `err` with a [domain error](../domain/domain-errors.md).
|
|
9
|
+
|
|
10
|
+
<dl class="al-glance">
|
|
11
|
+
<dt>Layer</dt><dd>Every layer</dd>
|
|
12
|
+
<dt>Type</dt><dd><a href="#api"><code>Result<T, E> = Ok<T> | Err<E></code></a></dd>
|
|
13
|
+
<dt>Returned by</dt><dd><a href="/core/domain/aggregates">Aggregates</a>, <a href="/core/domain/entities">entities</a>, <a href="/core/domain/value-objects">value object</a> factories, <a href="/core/domain/domain-services">domain services</a>, <a href="/core/application/command-handlers">command handlers</a></dd>
|
|
14
|
+
<dt>Checked by</dt><dd><a href="/rules/tactical/no-thrown-failure"><code>tactical/no-thrown-failure</code></a></dd>
|
|
15
|
+
</dl>
|
|
16
|
+
|
|
17
|
+
## Why
|
|
18
|
+
|
|
19
|
+
`order.place()` can fail: the order is already placed, or it has no line. If it throws, nothing in
|
|
20
|
+
its signature says so. The handler forgets the `try`, the controller answers 500 instead of 422,
|
|
21
|
+
and the client never learns why.
|
|
22
|
+
|
|
23
|
+
::: tip The fix
|
|
24
|
+
`place` returns `Result<void, OrderAlreadyPlaced | EmptyOrder>`. The failures are part of the type:
|
|
25
|
+
TypeScript makes every caller look at them, and each one passes them on or turns them into a
|
|
26
|
+
response.
|
|
27
|
+
:::
|
|
28
|
+
|
|
29
|
+
## How it works
|
|
30
|
+
|
|
31
|
+
A `Result` is a plain object, not a class. Two functions build it, and its `ok` flag tells them
|
|
32
|
+
apart.
|
|
33
|
+
|
|
34
|
+
<div class="al-cards">
|
|
35
|
+
<div class="al-card"><span class="al-card-title"><span class="al-card-step">1</span>Build it</span><code>ok(value)</code> for a success, <code>ok()</code> when there is no value, <code>err(error)</code> for a failure.</div>
|
|
36
|
+
<div class="al-card"><span class="al-card-title"><span class="al-card-step">2</span>Check it</span>After <code>if (!result.ok)</code>, TypeScript knows <code>result.error</code>. After it, <code>result.value</code>.</div>
|
|
37
|
+
<div class="al-card"><span class="al-card-title"><span class="al-card-step">3</span>Pass it on</span>Return the failure as is: its type joins the error union of the caller.</div>
|
|
38
|
+
</div>
|
|
39
|
+
|
|
40
|
+
```ts
|
|
41
|
+
place(
|
|
42
|
+
eventId: string,
|
|
43
|
+
now: Date,
|
|
44
|
+
): Result<void, OrderAlreadyPlaced | EmptyOrder> {
|
|
45
|
+
if (this.isPlaced) {
|
|
46
|
+
return err(new OrderAlreadyPlaced());
|
|
47
|
+
}
|
|
48
|
+
if (this.lines.length === 0) {
|
|
49
|
+
return err(new EmptyOrder());
|
|
50
|
+
}
|
|
51
|
+
// … change the state and record OrderPlaced
|
|
52
|
+
return ok();
|
|
53
|
+
}
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## Where it fits
|
|
57
|
+
|
|
58
|
+
A failure travels as a value from the aggregate to the edge, where a driving adapter turns it into
|
|
59
|
+
a response.
|
|
60
|
+
|
|
61
|
+
<div class="al-diagram">
|
|
62
|
+
<svg viewBox="0 0 680 372" role="img" aria-label="Order.place returns err(EmptyOrder). The PlaceOrderHandler returns the same err. The OrdersController maps it to an HTTP 422 response that names the error.">
|
|
63
|
+
<defs>
|
|
64
|
+
<marker id="result-flow-arrow" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
|
|
65
|
+
<path class="arrow" d="M 0 0 L 10 5 L 0 10 z" />
|
|
66
|
+
</marker>
|
|
67
|
+
</defs>
|
|
68
|
+
<rect class="box" x="40" y="16" width="260" height="52" rx="8" />
|
|
69
|
+
<text class="label" x="170" y="38" text-anchor="middle">order.place(…)</text>
|
|
70
|
+
<text class="note" x="170" y="56" text-anchor="middle">aggregate: checks the rules</text>
|
|
71
|
+
<path class="link" d="M 170 68 L 170 110" marker-end="url(#result-flow-arrow)" />
|
|
72
|
+
<rect class="boundary" x="196" y="76" width="200" height="28" rx="8" />
|
|
73
|
+
<text class="note" x="296" y="95" text-anchor="middle">err(EmptyOrder)</text>
|
|
74
|
+
<rect class="box" x="40" y="112" width="260" height="52" rx="8" />
|
|
75
|
+
<text class="label" x="170" y="134" text-anchor="middle">PlaceOrderHandler</text>
|
|
76
|
+
<text class="note" x="170" y="152" text-anchor="middle">returns the failure as is</text>
|
|
77
|
+
<path class="link" d="M 170 164 L 170 206" marker-end="url(#result-flow-arrow)" />
|
|
78
|
+
<rect class="boundary" x="196" y="172" width="200" height="28" rx="8" />
|
|
79
|
+
<text class="note" x="296" y="191" text-anchor="middle">the same err</text>
|
|
80
|
+
<rect class="box" x="40" y="208" width="260" height="52" rx="8" />
|
|
81
|
+
<text class="label" x="170" y="230" text-anchor="middle">OrdersController</text>
|
|
82
|
+
<text class="note" x="170" y="248" text-anchor="middle">driving adapter: maps it</text>
|
|
83
|
+
<path class="link" d="M 170 260 L 170 302" marker-end="url(#result-flow-arrow)" />
|
|
84
|
+
<text class="note" x="196" y="287">422 { error: "EmptyOrder" }</text>
|
|
85
|
+
<rect class="box" x="40" y="304" width="260" height="52" rx="8" />
|
|
86
|
+
<text class="label" x="170" y="326" text-anchor="middle">HTTP response</text>
|
|
87
|
+
<text class="note" x="170" y="344" text-anchor="middle">the client sees why</text>
|
|
88
|
+
</svg>
|
|
89
|
+
</div>
|
|
90
|
+
|
|
91
|
+
::: tip
|
|
92
|
+
Nobody in between wraps, logs or rethrows the failure. Only the edge decides what it becomes.
|
|
93
|
+
:::
|
|
94
|
+
|
|
95
|
+
## API
|
|
96
|
+
|
|
97
|
+
```ts
|
|
98
|
+
import {
|
|
99
|
+
andThen,
|
|
100
|
+
combine,
|
|
101
|
+
err,
|
|
102
|
+
map,
|
|
103
|
+
mapErr,
|
|
104
|
+
ok,
|
|
105
|
+
type Result,
|
|
106
|
+
} from "@alveolus/core";
|
|
107
|
+
// or: import { … } from "@alveolus/core/result";
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
### Type parameters
|
|
111
|
+
|
|
112
|
+
```ts
|
|
113
|
+
type Result<T, E> = Ok<T> | Err<E>;
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
| Parameter | What it is |
|
|
117
|
+
| --- | --- |
|
|
118
|
+
| `T` | The value of a success. `void` when there is none. |
|
|
119
|
+
| `E` | The error of a failure: usually a union of domain errors. |
|
|
120
|
+
|
|
121
|
+
### `ok()` <Badge type="tip" text="returned on a success" />
|
|
122
|
+
|
|
123
|
+
```ts
|
|
124
|
+
function ok(): Ok<void>;
|
|
125
|
+
function ok<T>(value: T): Ok<T>;
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
Builds a success. Without an argument, it carries no value: `ok()` is a `Result<void, E>`.
|
|
129
|
+
|
|
130
|
+
### `err(error)` <Badge type="tip" text="returned on a failure" />
|
|
131
|
+
|
|
132
|
+
```ts
|
|
133
|
+
function err<E>(error: E): Err<E>;
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
Builds a failure that carries `error`, usually a [domain error](../domain/domain-errors.md).
|
|
137
|
+
|
|
138
|
+
### `result.ok` <Badge type="info" text="readonly" /> <Badge type="tip" text="read by the caller" />
|
|
139
|
+
|
|
140
|
+
```ts
|
|
141
|
+
readonly ok: true; // on Ok<T>
|
|
142
|
+
readonly ok: false; // on Err<E>
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
`true` on a success, `false` on a failure. Checking it narrows the type to `Ok<T>` or `Err<E>`.
|
|
146
|
+
|
|
147
|
+
### `result.value` <Badge type="info" text="readonly" /> <Badge type="tip" text="read by the caller" />
|
|
148
|
+
|
|
149
|
+
```ts
|
|
150
|
+
readonly value: T; // on Ok<T>
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
The value of a success. It exists only after checking `result.ok`.
|
|
154
|
+
|
|
155
|
+
### `result.error` <Badge type="info" text="readonly" /> <Badge type="tip" text="read by the caller" />
|
|
156
|
+
|
|
157
|
+
```ts
|
|
158
|
+
readonly error: E; // on Err<E>
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
The error of a failure. It exists only after checking `!result.ok`.
|
|
162
|
+
|
|
163
|
+
### `map(result, transform)` <Badge type="tip" text="to combine results" />
|
|
164
|
+
|
|
165
|
+
```ts
|
|
166
|
+
function map<T, E, U>(
|
|
167
|
+
result: Result<T, E>,
|
|
168
|
+
transform: (value: T) => U,
|
|
169
|
+
): Result<U, E>;
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
Transforms the value of a success. A failure passes through unchanged.
|
|
173
|
+
|
|
174
|
+
### `mapErr(result, transform)` <Badge type="tip" text="to combine results" />
|
|
175
|
+
|
|
176
|
+
```ts
|
|
177
|
+
function mapErr<T, E, F>(
|
|
178
|
+
result: Result<T, E>,
|
|
179
|
+
transform: (error: E) => F,
|
|
180
|
+
): Result<T, F>;
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
Transforms the error of a failure. A success passes through unchanged.
|
|
184
|
+
|
|
185
|
+
### `andThen(result, next)` <Badge type="tip" text="to combine results" />
|
|
186
|
+
|
|
187
|
+
```ts
|
|
188
|
+
function andThen<T, E, U, F>(
|
|
189
|
+
result: Result<T, E>,
|
|
190
|
+
next: (value: T) => Result<U, F>,
|
|
191
|
+
): Result<U, E | F>;
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
Runs `next` on the value of a success and returns its result. A failure passes through, and the
|
|
195
|
+
errors of `next` add to the union.
|
|
196
|
+
|
|
197
|
+
### `combine(results)` <Badge type="tip" text="to combine results" />
|
|
198
|
+
|
|
199
|
+
```ts
|
|
200
|
+
function combine<
|
|
201
|
+
const Results extends
|
|
202
|
+
| readonly AnyResult[]
|
|
203
|
+
| Readonly<Record<string, AnyResult>>,
|
|
204
|
+
>(
|
|
205
|
+
results: Results,
|
|
206
|
+
): Result<CombinedValues<Results>, CombinedErrors<Results>>;
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
Takes an array or an object of results. Returns all the values in the same shape, or the first
|
|
210
|
+
failure in order. The error type is the union of the errors of every result.
|
|
211
|
+
|
|
212
|
+
::: warning Caveats
|
|
213
|
+
- `Result` is a discriminated union, not a class: there are no methods, and narrowing on `ok`
|
|
214
|
+
works as for any union.
|
|
215
|
+
- `combine` stops at the first failure in order: it does not collect every error.
|
|
216
|
+
- A `Result` carries expected failures. Technical failures, such as a lost connection, are thrown
|
|
217
|
+
by adapters and handled like any other exception; the domain and the application never throw.
|
|
218
|
+
:::
|
|
219
|
+
|
|
220
|
+
## Usage
|
|
221
|
+
|
|
222
|
+
Build the command handler that places an order, one idea at a time. Each step shows the whole file:
|
|
223
|
+
added lines are highlighted, replaced lines are struck out.
|
|
224
|
+
|
|
225
|
+
<div class="al-cards">
|
|
226
|
+
<div class="al-card"><span class="al-card-title"><span class="al-card-step">1</span><a href="#_1-start-from-a-method-that-can-fail">Start from a method that can fail</a></span>The domain already returns a Result.</div>
|
|
227
|
+
<div class="al-card"><span class="al-card-title"><span class="al-card-step">2</span><a href="#_2-return-success-and-failure">Return success and failure</a></span>ok for success, err for failure.</div>
|
|
228
|
+
<div class="al-card"><span class="al-card-title"><span class="al-card-step">3</span><a href="#_3-pass-a-failure-on-as-is">Pass a failure on as is</a></span>Narrow on ok, return the rest.</div>
|
|
229
|
+
<div class="al-card"><span class="al-card-title"><span class="al-card-step">4</span><a href="#_4-combine-several-results">Combine several results</a></span>One check for many values.</div>
|
|
230
|
+
<div class="al-card"><span class="al-card-title"><span class="al-card-step">5</span><a href="#_5-turn-it-into-a-response">Turn it into a response</a></span>Only the edge knows the transport.</div>
|
|
231
|
+
<div class="al-card"><span class="al-card-title"><span class="al-card-step">6</span><a href="#_6-check-it">Check it</a></span>Let the rules keep it that way.</div>
|
|
232
|
+
</div>
|
|
233
|
+
|
|
234
|
+
### 1. Start from a method that can fail
|
|
235
|
+
|
|
236
|
+
`Order.place` returns `Result<void, OrderAlreadyPlaced | EmptyOrder>`: see the
|
|
237
|
+
[aggregate](../domain/aggregates.md). The handler that calls it builds on the same type.
|
|
238
|
+
|
|
239
|
+
### 2. Return success and failure
|
|
240
|
+
|
|
241
|
+
So that the caller sees in the signature what can go wrong, the handler declares its error union
|
|
242
|
+
and returns a `Result`: `err` when the order does not exist, `ok()` when all went well. Nothing
|
|
243
|
+
is thrown.
|
|
244
|
+
|
|
245
|
+
```ts [src/ordering/application/commands/place-order.command.ts]
|
|
246
|
+
import {
|
|
247
|
+
CommandHandler,
|
|
248
|
+
err,
|
|
249
|
+
ok,
|
|
250
|
+
type Result,
|
|
251
|
+
} from "@alveolus/core";
|
|
252
|
+
|
|
253
|
+
import { OrderNotFound } from
|
|
254
|
+
"../../domain/errors/order-not-found.error";
|
|
255
|
+
import { Orders } from "../../domain/repositories/orders.repository";
|
|
256
|
+
import { OrderId } from
|
|
257
|
+
"../../domain/value-objects/order-id.identifier";
|
|
258
|
+
|
|
259
|
+
export interface PlaceOrder {
|
|
260
|
+
readonly orderId: string;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
export type PlaceOrderError = OrderNotFound;
|
|
264
|
+
|
|
265
|
+
export class PlaceOrderHandler extends CommandHandler<
|
|
266
|
+
PlaceOrder,
|
|
267
|
+
void,
|
|
268
|
+
PlaceOrderError
|
|
269
|
+
> {
|
|
270
|
+
constructor(private readonly orders: Orders) {
|
|
271
|
+
super();
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
async handle({
|
|
275
|
+
orderId,
|
|
276
|
+
}: PlaceOrder): Promise<Result<void, PlaceOrderError>> {
|
|
277
|
+
const order = await this.orders.findById(new OrderId(orderId));
|
|
278
|
+
if (order === undefined) {
|
|
279
|
+
return err(new OrderNotFound({ orderId }));
|
|
280
|
+
}
|
|
281
|
+
return ok();
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
### 3. Pass a failure on as is
|
|
287
|
+
|
|
288
|
+
When `place` fails, the handler has nothing to add: it returns the failed `Result` as is, after
|
|
289
|
+
checking `ok`. Its errors join the union of the handler, so the caller still sees every one.
|
|
290
|
+
|
|
291
|
+
```ts [src/ordering/application/commands/place-order.command.ts]
|
|
292
|
+
import {
|
|
293
|
+
Clock, // [!code ++]
|
|
294
|
+
CommandHandler,
|
|
295
|
+
err,
|
|
296
|
+
IdGenerator, // [!code ++]
|
|
297
|
+
ok,
|
|
298
|
+
type Result,
|
|
299
|
+
} from "@alveolus/core";
|
|
300
|
+
|
|
301
|
+
import { EmptyOrder } from "../../domain/errors/empty-order.error"; // [!code ++]
|
|
302
|
+
import { OrderAlreadyPlaced } from // [!code ++]
|
|
303
|
+
"../../domain/errors/order-already-placed.error"; // [!code ++]
|
|
304
|
+
import { OrderNotFound } from
|
|
305
|
+
"../../domain/errors/order-not-found.error";
|
|
306
|
+
import { Orders } from "../../domain/repositories/orders.repository";
|
|
307
|
+
import { OrderId } from
|
|
308
|
+
"../../domain/value-objects/order-id.identifier";
|
|
309
|
+
|
|
310
|
+
export interface PlaceOrder {
|
|
311
|
+
readonly orderId: string;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
export type PlaceOrderError = OrderNotFound; // [!code --]
|
|
315
|
+
export type PlaceOrderError = // [!code ++]
|
|
316
|
+
| OrderNotFound // [!code ++]
|
|
317
|
+
| OrderAlreadyPlaced // [!code ++]
|
|
318
|
+
| EmptyOrder; // [!code ++]
|
|
319
|
+
|
|
320
|
+
export class PlaceOrderHandler extends CommandHandler<
|
|
321
|
+
PlaceOrder,
|
|
322
|
+
void,
|
|
323
|
+
PlaceOrderError
|
|
324
|
+
> {
|
|
325
|
+
constructor(private readonly orders: Orders) { // [!code --]
|
|
326
|
+
constructor( // [!code ++]
|
|
327
|
+
private readonly orders: Orders, // [!code ++]
|
|
328
|
+
private readonly clock: Clock, // [!code ++]
|
|
329
|
+
private readonly ids: IdGenerator, // [!code ++]
|
|
330
|
+
) { // [!code ++]
|
|
331
|
+
super();
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
async handle({
|
|
335
|
+
orderId,
|
|
336
|
+
}: PlaceOrder): Promise<Result<void, PlaceOrderError>> {
|
|
337
|
+
const order = await this.orders.findById(new OrderId(orderId));
|
|
338
|
+
if (order === undefined) {
|
|
339
|
+
return err(new OrderNotFound({ orderId }));
|
|
340
|
+
}
|
|
341
|
+
const placed = order.place(this.ids.next(), this.clock.now()); // [!code ++]
|
|
342
|
+
if (!placed.ok) { // [!code ++]
|
|
343
|
+
return placed; // [!code ++]
|
|
344
|
+
} // [!code ++]
|
|
345
|
+
await this.orders.save(order); // [!code ++]
|
|
346
|
+
return ok();
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
```
|
|
350
|
+
|
|
351
|
+
### 4. Combine several results
|
|
352
|
+
|
|
353
|
+
When a use case needs several results, `combine` takes an array or an object of them and returns
|
|
354
|
+
all the values, or the first failure. `map`, `mapErr` and `andThen` transform or chain a single
|
|
355
|
+
one: see the [API](#api).
|
|
356
|
+
|
|
357
|
+
```ts
|
|
358
|
+
const prices = combine({
|
|
359
|
+
unit: Money.of(input.unitPrice, currency),
|
|
360
|
+
shipping: Money.of(input.shipping, currency),
|
|
361
|
+
});
|
|
362
|
+
if (!prices.ok) {
|
|
363
|
+
return prices;
|
|
364
|
+
}
|
|
365
|
+
const total = prices.value.unit.add(prices.value.shipping);
|
|
366
|
+
```
|
|
367
|
+
|
|
368
|
+
### 5. Turn it into a response
|
|
369
|
+
|
|
370
|
+
A driving adapter turns the failure into what its transport expects. Domain errors become HTTP
|
|
371
|
+
errors there, and nowhere else.
|
|
372
|
+
|
|
373
|
+
```ts [src/ordering/driving/http/controllers/orders.controller.ts]
|
|
374
|
+
const placed = await this.placeOrder.handle({ orderId });
|
|
375
|
+
if (!placed.ok) {
|
|
376
|
+
return {
|
|
377
|
+
status: 422,
|
|
378
|
+
body: {
|
|
379
|
+
error: placed.error.type,
|
|
380
|
+
details: placed.error.payload,
|
|
381
|
+
},
|
|
382
|
+
};
|
|
383
|
+
}
|
|
384
|
+
return { status: 204 };
|
|
385
|
+
```
|
|
386
|
+
|
|
387
|
+
### 6. Check it
|
|
388
|
+
|
|
389
|
+
Run the checks. One rule keeps business failures as values:
|
|
390
|
+
|
|
391
|
+
```sh
|
|
392
|
+
npx alveolus arch check
|
|
393
|
+
```
|
|
394
|
+
|
|
395
|
+
<div class="al-cards">
|
|
396
|
+
<div class="al-card"><span class="al-card-title"><a href="../../rules/tactical/no-thrown-failure"><code>tactical/no-thrown-failure</code></a></span>Public methods of aggregates and entities return a <code>Result</code>, and nothing is thrown in the domain or the application.</div>
|
|
397
|
+
</div>
|
|
398
|
+
|
|
399
|
+
A failure thrown instead of returned is reported:
|
|
400
|
+
|
|
401
|
+
```
|
|
402
|
+
src/ordering/domain/aggregates/order.aggregate.ts
|
|
403
|
+
58 error tactical/no-thrown-failure: A failure is thrown: return it in a
|
|
404
|
+
Result instead.
|
|
405
|
+
```
|
|
406
|
+
|
|
407
|
+
## See also
|
|
408
|
+
|
|
409
|
+
- [Domain errors](../domain/domain-errors.md), what a failure carries
|
|
410
|
+
- [Aggregates](../domain/aggregates.md) and [Command handlers](../application/command-handlers.md),
|
|
411
|
+
which return results
|
|
412
|
+
- [Unit of Work](../application/unit-of-work.md), which commits on `ok` and rolls back on `err`
|
|
413
|
+
- Rules: [`tactical/no-thrown-failure`](../../rules/tactical/no-thrown-failure.md)
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: "Give Claude Code, Cursor or Codex the Alveolus documentation offline: npx alveolus init writes a skill and AGENTS.md, npx alveolus explain prints any page in the terminal."
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
# Coding agents
|
|
6
|
+
|
|
7
|
+
An agent that writes code in your project needs the same two things as a new teammate: what each
|
|
8
|
+
building block is for, and what a rule reports and how to fix it. Both are installed with
|
|
9
|
+
`@alveolus/arch`, so the agent reads them in the terminal rather than on the web, and reads the
|
|
10
|
+
page of the version it has.
|
|
11
|
+
|
|
12
|
+
<dl class="al-glance">
|
|
13
|
+
<dt>Read a page</dt><dd><code>npx alveolus explain <topic></code>: a rule id, a building block or a guide</dd>
|
|
14
|
+
<dt>List the topics</dt><dd><code>npx alveolus explain</code></dd>
|
|
15
|
+
<dt>Tell the agent</dt><dd><code>npx alveolus init</code> writes <code>.claude/skills/alveolus/SKILL.md</code> and a section of <code>AGENTS.md</code></dd>
|
|
16
|
+
</dl>
|
|
17
|
+
|
|
18
|
+
## Read the documentation in the terminal
|
|
19
|
+
|
|
20
|
+
Every page of this site, except the home page, ships in the package under `docs/`:
|
|
21
|
+
|
|
22
|
+
```sh
|
|
23
|
+
npx alveolus explain layers/no-impure-domain # a rule, by the id the report prints
|
|
24
|
+
npx alveolus explain aggregates # a building block
|
|
25
|
+
npx alveolus explain project-layout # a guide
|
|
26
|
+
npx alveolus explain rules # an overview, by its folder
|
|
27
|
+
npx alveolus explain # every topic
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
A name matches the end of a topic: `no-impure-domain`, `layers/no-impure-domain` and
|
|
31
|
+
`rules/layers/no-impure-domain` print the same page. When a name matches several pages, the
|
|
32
|
+
command lists them.
|
|
33
|
+
|
|
34
|
+
The report of `alveolus arch check` ends with the command, so an agent that reads a violation
|
|
35
|
+
knows where the fix is written:
|
|
36
|
+
|
|
37
|
+
```
|
|
38
|
+
src/ordering/domain/aggregates/order.aggregate.ts
|
|
39
|
+
12 error layers/no-impure-domain: The domain reads the system clock with
|
|
40
|
+
Date.now: receive the time from the Clock port.
|
|
41
|
+
|
|
42
|
+
1 error in 142 files
|
|
43
|
+
|
|
44
|
+
Why, and how to fix it: npx alveolus explain <rule>
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## Tell the agent where to look
|
|
48
|
+
|
|
49
|
+
Nothing in `node_modules` is read by an agent on its own: the project has to say so. `npx alveolus
|
|
50
|
+
init` writes two short files, and leaves alone any that exists:
|
|
51
|
+
|
|
52
|
+
| File | Read by | What it says |
|
|
53
|
+
| --- | --- | --- |
|
|
54
|
+
| `.claude/skills/alveolus/SKILL.md` | Claude Code, and any tool that follows the [Agent Skills](https://agentskills.io) format | Read the building block before writing a class, run the checks after each change, read the rule before fixing a violation, never silence a rule without asking. |
|
|
55
|
+
| `AGENTS.md`, a section | Cursor, Codex, Copilot, Claude Code when there is no `CLAUDE.md` | The same, in a paragraph, with a pointer to the skill. |
|
|
56
|
+
|
|
57
|
+
The skill is a map, not the documentation: ten lines that say which command to run and when. The
|
|
58
|
+
pages stay in the package, so they follow its version and never load into the agent's context
|
|
59
|
+
unless it needs one.
|
|
60
|
+
|
|
61
|
+
If the project has a `CLAUDE.md`, Claude Code reads it instead of `AGENTS.md`: add a line with
|
|
62
|
+
`@AGENTS.md` to it, and `init` reminds you.
|
|
63
|
+
|
|
64
|
+
## See also
|
|
65
|
+
|
|
66
|
+
- [Getting started](./getting-started.md), the configuration and the checks
|
|
67
|
+
- [Rules](../rules/index.md), what each one reports
|
|
68
|
+
- [Learning path](./learning-path.md), the order in which to read the docs
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: "Adopt the Alveolus architecture checks on an existing TypeScript project, context by context, without stopping the team."
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
# Adopt it on an existing project
|
|
6
|
+
|
|
7
|
+
An existing project rarely has bounded contexts, layers and building blocks already. The checks
|
|
8
|
+
can still start today: record what is, choose what to fix first, and move one context at a time.
|
|
9
|
+
Nothing here requires a rewrite.
|
|
10
|
+
|
|
11
|
+
<dl class="al-glance">
|
|
12
|
+
<dt>Time to the first green check</dt><dd>An hour</dd>
|
|
13
|
+
<dt>Tools</dt><dd>The baseline, the levels of the rules, <code>layout.extraFolders</code>, <code>ignore</code></dd>
|
|
14
|
+
<dt>Order</dt><dd>Declare, record, lower, raise, move</dd>
|
|
15
|
+
</dl>
|
|
16
|
+
|
|
17
|
+
## 1. Declare what exists
|
|
18
|
+
|
|
19
|
+
Name the bounded contexts as the code has them today, even when they are folders named
|
|
20
|
+
`modules/orders` or `features/billing`: `boundedContexts` takes any folder under `root`. Say under
|
|
21
|
+
`subdomains` which ones are the [core domain](./project-layout.md#core-supporting-generic): only
|
|
22
|
+
those are checked inside; a supporting or generic context is checked at its boundary only. Leave
|
|
23
|
+
the context map for later. Put under `ignore` what has nothing to do with the architecture:
|
|
24
|
+
scripts, generated code, migrations.
|
|
25
|
+
|
|
26
|
+
```ts [alveolus.config.ts]
|
|
27
|
+
export default defineConfig({
|
|
28
|
+
boundedContexts: { billing: "features/billing", orders: "modules/orders" },
|
|
29
|
+
ignore: ["src/migrations/**", "src/generated/**"],
|
|
30
|
+
root: "src",
|
|
31
|
+
subdomains: { core: ["orders"], supporting: ["billing"] },
|
|
32
|
+
});
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
::: tip A legacy module is a supporting context first
|
|
36
|
+
A module you will not rewrite soon goes under `supporting`: the check keeps it closed and makes
|
|
37
|
+
it reach the others through their open host services, and leaves the rest alone. Move it to `core`
|
|
38
|
+
the day you model it.
|
|
39
|
+
:::
|
|
40
|
+
|
|
41
|
+
Run `npx alveolus arch check`: it fails, and the number of violations is your starting point.
|
|
42
|
+
|
|
43
|
+
## 2. Record the baseline
|
|
44
|
+
|
|
45
|
+
`npx alveolus arch baseline` writes every current violation to `alveolus.baseline.json`. Commit
|
|
46
|
+
it: from now on, `check` fails only on what is new. The team keeps shipping, and the baseline
|
|
47
|
+
can only shrink: `baseline` refuses to grow unless asked to.
|
|
48
|
+
|
|
49
|
+
## 3. Lower what you will not fix yet
|
|
50
|
+
|
|
51
|
+
A rule that reports hundreds of violations the team will fix over months goes to `warn`: it keeps
|
|
52
|
+
reporting, without failing the check. A rule that does not fit a part of the project yet goes to
|
|
53
|
+
`info`, or `off` with a date to come back.
|
|
54
|
+
|
|
55
|
+
```ts [alveolus.config.ts]
|
|
56
|
+
rules: {
|
|
57
|
+
"tactical/no-public-field": "warn",
|
|
58
|
+
"tactical/no-thrown-failure": "warn",
|
|
59
|
+
"layers/no-outward-import": "info",
|
|
60
|
+
},
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Keep as errors, from day one, the rules that stop the worst: `strategic/no-cross-context-import`,
|
|
64
|
+
`layers/no-impure-domain`, `tactical/no-aggregate-reference`. New code follows them; old code is
|
|
65
|
+
in the baseline.
|
|
66
|
+
|
|
67
|
+
## 4. Keep the folders you have
|
|
68
|
+
|
|
69
|
+
Folders that will stay, such as `domain/specifications/` or `application/dto/`, go in
|
|
70
|
+
`layout.extraFolders`. Folders that will go stay reported, in the baseline.
|
|
71
|
+
|
|
72
|
+
## 5. Move one context
|
|
73
|
+
|
|
74
|
+
Pick the context the team touches most. In this order, each step leaving the check green:
|
|
75
|
+
|
|
76
|
+
| Step | Rules that go green | What moves |
|
|
77
|
+
| --- | --- | --- |
|
|
78
|
+
| Layers | `layers/no-outward-import`, `layers/no-driving-shortcut` | Files into `domain/`, `application/`, `driven/<technology>/`, `driving/<technology>/`; controllers call handlers. |
|
|
79
|
+
| Purity | `layers/no-impure-domain`, `layers/no-portless-adapter` | Framework and database out of the domain, behind ports. |
|
|
80
|
+
| Building blocks | `tactical/no-loose-code`, `tactical/no-misplaced-class`, `tactical/no-public-field` | Each class extends its block, in its folder, with its state private. |
|
|
81
|
+
| Behaviour | `tactical/no-thrown-failure`, `tactical/no-foreign-*-dependency` | Failures as `Result`, handlers receive what they should. |
|
|
82
|
+
| Boundaries | `strategic/*` | Open host services, anti-corruption layers, the context map. |
|
|
83
|
+
|
|
84
|
+
After each step, `npx alveolus arch baseline` drops what is fixed, and the `fixed` count in the
|
|
85
|
+
summary tells the team how far it got.
|
|
86
|
+
|
|
87
|
+
## 6. Raise the levels
|
|
88
|
+
|
|
89
|
+
When a rule reports nothing for the contexts already moved, it goes back to `error`. When every
|
|
90
|
+
context is moved, delete the baseline.
|
|
91
|
+
|
|
92
|
+
## What to expect
|
|
93
|
+
|
|
94
|
+
- The first `check` on a large project reports a lot: that is the point of the baseline. Read
|
|
95
|
+
the counts per rule in `--format json` before deciding what to lower.
|
|
96
|
+
- A rule reported where the team disagrees with it is a conversation, not a `disable`: lower it,
|
|
97
|
+
write why in the configuration, and come back to it.
|
|
98
|
+
- `--format sarif` in the pull requests shows new violations where they are written, which is
|
|
99
|
+
what keeps the baseline from growing again.
|
|
100
|
+
|
|
101
|
+
## See also
|
|
102
|
+
|
|
103
|
+
- [Getting started](./getting-started.md), the configuration and the commands
|
|
104
|
+
- [Project layout](./project-layout.md), where things end up
|
|
105
|
+
- [Rules](../rules/index.md), what each one reports and what it cannot see
|