@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.
Files changed (60) hide show
  1. package/README.md +8 -1
  2. package/dist/bin.mjs +4 -2
  3. package/dist/bin.mjs.map +1 -1
  4. package/dist/{cli-CwPCGjDg.mjs → docs-DsQHpTtV.mjs} +287 -38
  5. package/dist/docs-DsQHpTtV.mjs.map +1 -0
  6. package/dist/index.d.mts +90 -36
  7. package/dist/index.d.mts.map +1 -1
  8. package/dist/index.mjs +2 -2
  9. package/docs/core/application/command-handlers.md +617 -0
  10. package/docs/core/application/event-publishers.md +234 -0
  11. package/docs/core/application/event-translators.md +329 -0
  12. package/docs/core/application/index.md +99 -0
  13. package/docs/core/application/integration-events.md +277 -0
  14. package/docs/core/application/outbox.md +416 -0
  15. package/docs/core/application/query-handlers.md +292 -0
  16. package/docs/core/application/unit-of-work.md +352 -0
  17. package/docs/core/domain/aggregates.md +822 -0
  18. package/docs/core/domain/domain-errors.md +251 -0
  19. package/docs/core/domain/domain-events.md +292 -0
  20. package/docs/core/domain/domain-services.md +249 -0
  21. package/docs/core/domain/entities.md +431 -0
  22. package/docs/core/domain/index.md +93 -0
  23. package/docs/core/domain/ports.md +284 -0
  24. package/docs/core/domain/repositories.md +335 -0
  25. package/docs/core/domain/value-objects.md +425 -0
  26. package/docs/core/domain/views.md +265 -0
  27. package/docs/core/index.md +108 -0
  28. package/docs/core/strategic/anti-corruption-layers.md +349 -0
  29. package/docs/core/strategic/index.md +83 -0
  30. package/docs/core/strategic/open-host-services.md +287 -0
  31. package/docs/core/strategic/published-language.md +265 -0
  32. package/docs/core/utilities/result.md +413 -0
  33. package/docs/guide/agents.md +68 -0
  34. package/docs/guide/existing-project.md +105 -0
  35. package/docs/guide/getting-started.md +275 -0
  36. package/docs/guide/learning-path.md +123 -0
  37. package/docs/guide/project-layout.md +324 -0
  38. package/docs/guide/versioning.md +42 -0
  39. package/docs/integrations/index.md +112 -0
  40. package/docs/integrations/nestjs.md +169 -0
  41. package/docs/rules/index.md +183 -0
  42. package/docs/rules/layers/no-driving-shortcut.md +119 -0
  43. package/docs/rules/layers/no-impure-domain.md +189 -0
  44. package/docs/rules/layers/no-outward-import.md +184 -0
  45. package/docs/rules/layers/no-portless-adapter.md +123 -0
  46. package/docs/rules/strategic/no-cross-context-import.md +140 -0
  47. package/docs/rules/strategic/no-fat-shared-kernel.md +81 -0
  48. package/docs/rules/strategic/no-leaky-host-service.md +107 -0
  49. package/docs/rules/strategic/no-unmapped-context.md +111 -0
  50. package/docs/rules/tactical/no-aggregate-reference.md +139 -0
  51. package/docs/rules/tactical/no-foreign-command-dependency.md +119 -0
  52. package/docs/rules/tactical/no-foreign-query-dependency.md +106 -0
  53. package/docs/rules/tactical/no-loose-code.md +171 -0
  54. package/docs/rules/tactical/no-misplaced-class.md +146 -0
  55. package/docs/rules/tactical/no-public-field.md +113 -0
  56. package/docs/rules/tactical/no-stateful-service.md +102 -0
  57. package/docs/rules/tactical/no-thrown-failure.md +162 -0
  58. package/docs/rules/tooling/no-loose-disable.md +98 -0
  59. package/package.json +4 -3
  60. package/dist/cli-CwPCGjDg.mjs.map +0 -1
@@ -0,0 +1,425 @@
1
+ ---
2
+ description: "Value objects in Domain-Driven Design with TypeScript: immutable values described only by their attributes, such as an amount, an email or an identifier."
3
+ ---
4
+
5
+ # Value objects
6
+
7
+ A value object is an immutable value described only by its attributes, such as an amount or an
8
+ identifier.
9
+
10
+ <dl class="al-glance">
11
+ <dt>Layer</dt><dd>Domain</dd>
12
+ <dt>File</dt><dd><code>domain/value-objects/money.value-object.ts</code>, <code>domain/value-objects/order-id.identifier.ts</code></dd>
13
+ <dt>Extends</dt><dd><a href="#api"><code>ValueObject&lt;Props&gt;</code></a> or <a href="#identifier"><code>Identifier&lt;T, Tag&gt;</code></a></dd>
14
+ <dt>Used by</dt><dd><a href="/core/domain/aggregates">Aggregates</a>, <a href="/core/domain/entities">entities</a>, <a href="/core/domain/domain-events">domain events</a>, <a href="/core/application/command-handlers">command handlers</a></dd>
15
+ <dt>Checked by</dt><dd><a href="/rules/tactical/no-misplaced-class"><code>tactical/no-misplaced-class</code></a>, <a href="/rules/tactical/no-loose-code"><code>tactical/no-loose-code</code></a>, <a href="/rules/tactical/no-aggregate-reference"><code>tactical/no-aggregate-reference</code></a>, <a href="/rules/tactical/no-public-field"><code>tactical/no-public-field</code></a></dd>
16
+ </dl>
17
+
18
+ ## Why
19
+
20
+ A price is a `number` and a currency a `string`. Every function that takes them checks that the
21
+ amount is not negative, or forgets to. Adding 12 EUR and 5 USD compiles. A function that expects an
22
+ `orderId: string` happily receives a customer id.
23
+
24
+ ::: tip The fix
25
+ `Money` holds the amount and the currency together, refuses a negative amount once, when it is
26
+ created, and only adds amounts of the same currency. `OrderId` and `CustomerId` are two types: the
27
+ compiler refuses one where the other is expected.
28
+ :::
29
+
30
+ ## How it works
31
+
32
+ A value object has no identity: two amounts of 12 EUR are the same amount, and one can replace the
33
+ other. Three properties follow:
34
+
35
+ <div class="al-cards">
36
+ <div class="al-card"><span class="al-card-title"><span class="al-card-step">1</span>Valid from the start</span>A static factory checks the input and returns a <a href="/core/utilities/result"><code>Result</code></a>. An invalid value never exists.</div>
37
+ <div class="al-card"><span class="al-card-title"><span class="al-card-step">2</span>Never changes</span>Its attributes are frozen. An operation returns a new value object.</div>
38
+ <div class="al-card"><span class="al-card-title"><span class="al-card-step">3</span>Equal by value</span>Two value objects of the same class with equal attributes are equal.</div>
39
+ </div>
40
+
41
+ ```ts
42
+ const price = Money.of(12, "EUR");
43
+ if (!price.ok) {
44
+ return price;
45
+ }
46
+ const total = price.value.add(shipping);
47
+ ```
48
+
49
+ An [identifier](#identifier) is a value object that names an entity or an aggregate.
50
+
51
+ ## Where it fits
52
+
53
+ Raw values come in at the edge, as strings and numbers. The
54
+ [command handler](../application/command-handlers.md) turns them into value objects, and from there
55
+ on the domain only sees typed values.
56
+
57
+ <div class="al-diagram">
58
+ <svg viewBox="0 0 680 120" role="img" aria-label="A controller receives orderId as a string. The command handler wraps it in an OrderId, passes it to the repository, which loads the Order.">
59
+ <defs>
60
+ <marker id="value-object-flow-arrow" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
61
+ <path class="arrow" d="M 0 0 L 10 5 L 0 10 z" />
62
+ </marker>
63
+ </defs>
64
+ <rect class="box" x="8" y="32" width="148" height="56" rx="8" />
65
+ <text class="label" x="82" y="56" text-anchor="middle">Controller</text>
66
+ <text class="note" x="82" y="76" text-anchor="middle">orderId: string</text>
67
+ <path class="link" d="M 156 60 L 178 60" marker-end="url(#value-object-flow-arrow)" />
68
+ <rect class="boundary" x="180" y="32" width="148" height="56" rx="8" />
69
+ <text class="label" x="254" y="56" text-anchor="middle">OrderId</text>
70
+ <text class="note" x="254" y="76" text-anchor="middle">new OrderId(…)</text>
71
+ <path class="link" d="M 328 60 L 350 60" marker-end="url(#value-object-flow-arrow)" />
72
+ <rect class="box" x="352" y="32" width="148" height="56" rx="8" />
73
+ <text class="label" x="426" y="56" text-anchor="middle">Orders</text>
74
+ <text class="note" x="426" y="76" text-anchor="middle">findById(id)</text>
75
+ <path class="link" d="M 500 60 L 522 60" marker-end="url(#value-object-flow-arrow)" />
76
+ <rect class="box" x="524" y="32" width="148" height="56" rx="8" />
77
+ <text class="label" x="598" y="56" text-anchor="middle">Order</text>
78
+ <text class="note" x="598" y="76" text-anchor="middle">holds CustomerId</text>
79
+ </svg>
80
+ </div>
81
+
82
+ ::: tip
83
+ Value objects are saved as their raw values: `Money` becomes `{ amount, currency }` in the snapshot
84
+ of its aggregate, and `OrderId` becomes a string.
85
+ :::
86
+
87
+ ## API
88
+
89
+ ```ts
90
+ import { Identifier, ValueObject } from "@alveolus/core";
91
+ // or: from "@alveolus/core/value-objects"
92
+ ```
93
+
94
+ ### ValueObject
95
+
96
+ #### Type parameters
97
+
98
+ ```ts
99
+ abstract class ValueObject<Props extends object> { … }
100
+ ```
101
+
102
+ | Parameter | What it is | Constraint |
103
+ | --- | --- | --- |
104
+ | `Props` | The attributes of the value object. | an object |
105
+
106
+ `AnyValueObject` is the type of any value object.
107
+
108
+ #### `constructor(props)` <Badge type="info" text="protected" /> <Badge type="tip" text="you call it" />
109
+
110
+ ```ts
111
+ protected constructor(props: Props)
112
+ ```
113
+
114
+ Copies and freezes the attributes. Declare your own constructor `private` and call
115
+ `super(props)` from it: only your factories use it.
116
+
117
+ #### `of(…)` <Badge type="info" text="static · convention" /> <Badge type="tip" text="you implement it" />
118
+
119
+ ```ts
120
+ static of(
121
+ amount: number,
122
+ currency: Currency,
123
+ ): Result<Money, InvalidAmount>
124
+ ```
125
+
126
+ A factory, or several. Checks the input and returns a `Result`.
127
+
128
+ #### `props` <Badge type="info" text="protected · readonly" /> <Badge type="tip" text="inside your methods" />
129
+
130
+ ```ts
131
+ protected readonly props: Readonly<Props>
132
+ ```
133
+
134
+ The attributes, copied and frozen. Expose what callers need through getters.
135
+
136
+ #### `equals(other)` <Badge type="tip" text="called by anyone" />
137
+
138
+ ```ts
139
+ equals(other: ValueObject<object>): boolean
140
+ ```
141
+
142
+ `true` for the same object, or the same class with deeply equal attributes. Nested value objects
143
+ use their own `equals`; arrays, dates and plain objects are compared by content.
144
+
145
+ ### Identifier
146
+
147
+ #### Type parameters
148
+
149
+ ```ts
150
+ abstract class Identifier<
151
+ T extends IdentifierValue,
152
+ Tag extends string = string,
153
+ > { … }
154
+ ```
155
+
156
+ | Parameter | What it is | Constraint |
157
+ | --- | --- | --- |
158
+ | `T` | The raw value. | `string`, `number` or `bigint` |
159
+ | `Tag` | A unique name that keeps identifier types apart at compile time. It does not exist at runtime. | a string literal; any string by default |
160
+
161
+ `AnyIdentifier` is the type of any identifier, and `IdentifierValue` the type of its raw value.
162
+
163
+ #### `constructor(value)` <Badge type="tip" text="you call it" />
164
+
165
+ ```ts
166
+ constructor(value: T)
167
+ ```
168
+
169
+ Wraps the raw value. The constructor is public and does not validate. An identifier has no body:
170
+ declare the class only.
171
+
172
+ ```ts
173
+ class OrderId extends Identifier<string, "OrderId"> {}
174
+ ```
175
+
176
+ #### `value` <Badge type="info" text="readonly" /> <Badge type="tip" text="read by anyone" />
177
+
178
+ ```ts
179
+ readonly value: T
180
+ ```
181
+
182
+ The raw value.
183
+
184
+ #### `equals(other)` <Badge type="tip" text="called by anyone" />
185
+
186
+ ```ts
187
+ equals(other: AnyIdentifier): boolean
188
+ ```
189
+
190
+ `true` for the same class with the same value.
191
+
192
+ #### `toString()` <Badge type="tip" text="called by anyone" />
193
+
194
+ ```ts
195
+ toString(): string
196
+ ```
197
+
198
+ The value as a string.
199
+
200
+ #### `toJSON()` <Badge type="tip" text="called by JSON.stringify" />
201
+
202
+ ```ts
203
+ toJSON(): T
204
+ ```
205
+
206
+ The raw value, so that an identifier serializes as its value.
207
+
208
+ ::: warning Caveats
209
+ - `props` is frozen one level deep: arrays and objects you pass in can still be changed from
210
+ outside. Do not keep references to them.
211
+ - `equals` requires the same concrete class.
212
+ - The constructor of an identifier is public and does not validate: when an identifier has a
213
+ format, check it where it enters, such as in a driving adapter.
214
+ - Without a `Tag`, two identifier classes with the same raw type are interchangeable for the
215
+ compiler.
216
+ :::
217
+
218
+ ## Usage
219
+
220
+ Build `Money`, a value object of the running example, then `OrderId`, an identifier. Each step shows the whole file it changes: added lines are highlighted, replaced lines are struck out.
221
+
222
+ <div class="al-cards">
223
+ <div class="al-card"><span class="al-card-title"><span class="al-card-step">1</span><a href="#_1-declare-it">Declare it</a></span>Attributes, copied and frozen.</div>
224
+ <div class="al-card"><span class="al-card-title"><span class="al-card-step">2</span><a href="#_2-check-it-in-a-factory">Check it in a factory</a></span>No wrong value exists.</div>
225
+ <div class="al-card"><span class="al-card-title"><span class="al-card-step">3</span><a href="#_3-expose-reads-as-getters">Expose reads as getters</a></span>Read without changing.</div>
226
+ <div class="al-card"><span class="al-card-title"><span class="al-card-step">4</span><a href="#_4-return-a-new-value">Return a new value</a></span>Never change in place.</div>
227
+ <div class="al-card"><span class="al-card-title"><span class="al-card-step">5</span><a href="#_5-name-an-identity">Name an identity</a></span>An identifier for an aggregate.</div>
228
+ <div class="al-card"><span class="al-card-title"><span class="al-card-step">6</span><a href="#_6-use-them">Use them</a></span>Build, compare, combine.</div>
229
+ <div class="al-card"><span class="al-card-title"><span class="al-card-step">7</span><a href="#_7-check-it">Check it</a></span>Let the rules keep it that way.</div>
230
+ </div>
231
+
232
+ ### 1. Declare it
233
+
234
+ A value object is described by its attributes only: they are its type parameter, read-only, and
235
+ `ValueObject` stores them, copied and frozen, in `props`. An attribute can itself be a value
236
+ object, such as `Currency`. The constructor parameters are only
237
+ passed to `super`, so they stay plain.
238
+
239
+ ```ts [src/ordering/domain/value-objects/money.value-object.ts]
240
+ import { ValueObject } from "@alveolus/core";
241
+
242
+ import { Currency } from "./currency.value-object";
243
+
244
+ export class Money extends ValueObject<{
245
+ readonly amount: number;
246
+ readonly currency: Currency;
247
+ }> {
248
+ private constructor(amount: number, currency: Currency) {
249
+ super({ amount, currency });
250
+ }
251
+ }
252
+ ```
253
+
254
+ ### 2. Check it in a factory
255
+
256
+ So that no `Money` exists with a wrong amount, the constructor is private and a factory checks the
257
+ input once. A refusal is a [domain error](./domain-errors.md) in a `Result`.
258
+
259
+ ```ts [src/ordering/domain/value-objects/money.value-object.ts]
260
+ import { ValueObject } from "@alveolus/core"; // [!code --]
261
+ import { err, ok, type Result, ValueObject } from "@alveolus/core"; // [!code ++]
262
+
263
+ import { InvalidAmount } from "../errors/invalid-amount.error"; // [!code ++]
264
+ import { Currency } from "./currency.value-object";
265
+
266
+ export class Money extends ValueObject<{
267
+ readonly amount: number;
268
+ readonly currency: Currency;
269
+ }> {
270
+ private constructor(amount: number, currency: Currency) {
271
+ super({ amount, currency });
272
+ }
273
+
274
+ static of( // [!code ++]
275
+ amount: number, // [!code ++]
276
+ currency: Currency, // [!code ++]
277
+ ): Result<Money, InvalidAmount> { // [!code ++]
278
+ if (!Number.isFinite(amount) || amount < 0) { // [!code ++]
279
+ return err(new InvalidAmount({ amount })); // [!code ++]
280
+ } // [!code ++]
281
+ return ok(new Money(amount, currency)); // [!code ++]
282
+ } // [!code ++]
283
+ }
284
+ ```
285
+
286
+ ### 3. Expose reads as getters
287
+
288
+ `props` is protected: callers read what they need through getters, and nothing can change it.
289
+
290
+ ```ts [src/ordering/domain/value-objects/money.value-object.ts]
291
+ import { err, ok, type Result, ValueObject } from "@alveolus/core";
292
+
293
+ import { InvalidAmount } from "../errors/invalid-amount.error";
294
+ import { Currency } from "./currency.value-object";
295
+
296
+ export class Money extends ValueObject<{
297
+ readonly amount: number;
298
+ readonly currency: Currency;
299
+ }> {
300
+ private constructor(amount: number, currency: Currency) {
301
+ super({ amount, currency });
302
+ }
303
+
304
+ static of(
305
+ amount: number,
306
+ currency: Currency,
307
+ ): Result<Money, InvalidAmount> {
308
+ if (!Number.isFinite(amount) || amount < 0) {
309
+ return err(new InvalidAmount({ amount }));
310
+ }
311
+ return ok(new Money(amount, currency));
312
+ }
313
+
314
+ get amount(): number { // [!code ++]
315
+ return this.props.amount; // [!code ++]
316
+ } // [!code ++]
317
+
318
+ get currency(): Currency { // [!code ++]
319
+ return this.props.currency; // [!code ++]
320
+ } // [!code ++]
321
+ }
322
+ ```
323
+
324
+ ### 4. Return a new value
325
+
326
+ A value object never changes: an operation returns a new one. Two values with the same attributes
327
+ are `equals`, whichever instance you hold.
328
+
329
+ ```ts [src/ordering/domain/value-objects/money.value-object.ts]
330
+ import { err, ok, type Result, ValueObject } from "@alveolus/core";
331
+
332
+ import { InvalidAmount } from "../errors/invalid-amount.error";
333
+ import { Currency } from "./currency.value-object";
334
+
335
+ export class Money extends ValueObject<{
336
+ readonly amount: number;
337
+ readonly currency: Currency;
338
+ }> {
339
+ private constructor(amount: number, currency: Currency) {
340
+ super({ amount, currency });
341
+ }
342
+
343
+ static of(
344
+ amount: number,
345
+ currency: Currency,
346
+ ): Result<Money, InvalidAmount> {
347
+ if (!Number.isFinite(amount) || amount < 0) {
348
+ return err(new InvalidAmount({ amount }));
349
+ }
350
+ return ok(new Money(amount, currency));
351
+ }
352
+
353
+ get amount(): number {
354
+ return this.props.amount;
355
+ }
356
+
357
+ get currency(): Currency {
358
+ return this.props.currency;
359
+ }
360
+
361
+ add(other: Money): Money { // [!code ++]
362
+ return new Money( // [!code ++]
363
+ this.props.amount + other.props.amount, // [!code ++]
364
+ this.props.currency, // [!code ++]
365
+ ); // [!code ++]
366
+ } // [!code ++]
367
+ }
368
+ ```
369
+
370
+ ### 5. Name an identity
371
+
372
+ An identifier is a value object that names an entity or an aggregate. It has no body, and its tag
373
+ keeps an `OrderId` apart from a `CustomerId` at compile time.
374
+
375
+ ```ts [src/ordering/domain/value-objects/order-id.identifier.ts]
376
+ import { Identifier } from "@alveolus/core";
377
+
378
+ export class OrderId extends Identifier<string, "OrderId"> {}
379
+ ```
380
+
381
+ ### 6. Use them
382
+
383
+ Callers build a `Money` through `of` and handle its refusal; an identifier wraps a raw value
384
+ directly, and is compared by value.
385
+
386
+ ```ts
387
+ const price = Money.of(12.5, currency);
388
+ if (!price.ok) {
389
+ return price;
390
+ }
391
+ const total = price.value.add(shipping);
392
+ const unchanged = total.equals(price.value);
393
+ const orderId = new OrderId(command.orderId);
394
+ ```
395
+
396
+ ### 7. Check it
397
+
398
+ Run the checks. Three rules keep the value objects the way they are now:
399
+
400
+ ```sh
401
+ npx alveolus arch check
402
+ ```
403
+
404
+ <div class="al-cards">
405
+ <div class="al-card"><span class="al-card-title"><a href="../../rules/tactical/no-loose-code"><code>no-loose-code</code></a></span>Behaviour on a value lives in its class, not in a free function.</div>
406
+ <div class="al-card"><span class="al-card-title"><a href="../../rules/tactical/no-misplaced-class"><code>no-misplaced-class</code></a></span><code>*.value-object.ts</code> and <code>*.identifier.ts</code>, in <code>domain/value-objects/</code>.</div>
407
+ <div class="al-card"><span class="al-card-title"><a href="../../rules/layers/no-impure-domain"><code>no-impure-domain</code></a></span>It imports the domain only, or a package listed in <code>domainDependencies</code>.</div>
408
+ </div>
409
+
410
+ An operation written as a function is reported:
411
+
412
+ ```
413
+ src/ordering/domain/value-objects/money.ts
414
+ 3 error tactical/no-loose-code: The function addMoney floats outside
415
+ any class: make it a method of a value object or of a
416
+ DomainService.
417
+ ```
418
+
419
+ ## See also
420
+
421
+ - [Entities](./entities.md) and [Aggregates](./aggregates.md), identified by identifiers
422
+ - [Domain errors](./domain-errors.md), returned by factories
423
+ - [Result](../utilities/result.md), to combine several factories
424
+ - Rules: [`tactical/no-loose-code`](../../rules/tactical/no-loose-code.md), [`tactical/no-misplaced-class`](../../rules/tactical/no-misplaced-class.md)
425
+ - Vaughn Vernon, *Implementing Domain-Driven Design*, chapter 6, "Value Objects"