@alveolus/arch 0.2.0 → 0.4.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 +11 -0
  2. package/dist/bin.mjs +4 -2
  3. package/dist/bin.mjs.map +1 -1
  4. package/dist/{cli-P5PwH9OE.mjs → docs-DcFgskuN.mjs} +214 -43
  5. package/dist/docs-DcFgskuN.mjs.map +1 -0
  6. package/dist/index.d.mts +53 -12
  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 +108 -0
  35. package/docs/guide/getting-started.md +286 -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 +185 -0
  42. package/docs/rules/layers/no-driving-shortcut.md +119 -0
  43. package/docs/rules/layers/no-impure-domain.md +191 -0
  44. package/docs/rules/layers/no-outward-import.md +186 -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 +114 -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 +201 -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-P5PwH9OE.mjs.map +0 -1
@@ -0,0 +1,335 @@
1
+ ---
2
+ description: "Repositories in Domain-Driven Design with TypeScript: ports that load and save aggregates for commands and views for queries, as if they were collections."
3
+ ---
4
+
5
+ # Repositories
6
+
7
+ A repository is a port that loads and saves what the application works on, as if it were a
8
+ collection: aggregates for commands, views for queries.
9
+
10
+ <dl class="al-glance">
11
+ <dt>Layer</dt><dd>Domain, implemented by a driven adapter</dd>
12
+ <dt>File</dt><dd><code>domain/repositories/orders.repository.ts</code></dd>
13
+ <dt>Extends</dt><dd><a href="#api"><code>CommandRepository&lt;Aggregate&gt;</code></a>, <a href="#api"><code>QueryRepository&lt;View&gt;</code></a></dd>
14
+ <dt>Used by</dt><dd><a href="/core/application/command-handlers">Command handlers</a>, <a href="/core/application/query-handlers">query 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-foreign-command-dependency"><code>tactical/no-foreign-command-dependency</code></a>, <a href="/rules/tactical/no-foreign-query-dependency"><code>tactical/no-foreign-query-dependency</code></a>, <a href="/rules/layers/no-portless-adapter"><code>layers/no-portless-adapter</code></a></dd>
16
+ </dl>
17
+
18
+ ## Why
19
+
20
+ Placing an order loads it, changes it and saves it. If the handler writes the SQL, it must reach
21
+ into the private fields of `Order` to store them, every handler that touches orders repeats the
22
+ mapping, and no handler runs without a database.
23
+
24
+ ::: tip The fix
25
+ The domain declares `Orders`, a collection of orders: `findById` and `save`. An adapter stores the
26
+ order's snapshot in its tables. The handler asks the collection and never sees a table.
27
+ :::
28
+
29
+ ## How it works
30
+
31
+ Alveolus splits repositories by side. A command needs the aggregate, with its rules. A query needs
32
+ a shape to show, without loading the aggregate.
33
+
34
+ | | Command repository | Query repository |
35
+ | --- | --- | --- |
36
+ | **Hands out** | an [aggregate](./aggregates.md) | a [view](./views.md) |
37
+ | **Extends** | `CommandRepository<Order>` | `QueryRepository<OrderSummary>` |
38
+ | **Used by** | [command handlers](../application/command-handlers.md) | [query handlers](../application/query-handlers.md) |
39
+ | **Methods** | `findById`, `save`, and yours | yours, read only |
40
+
41
+ The adapter of a command repository goes through the aggregate's snapshot, never its fields:
42
+
43
+ <div class="al-cards">
44
+ <div class="al-card"><span class="al-card-title"><span class="al-card-step">1</span>findById</span>Read the row, rebuild the aggregate with <code>Order.fromSnapshot(…)</code>.</div>
45
+ <div class="al-card"><span class="al-card-title"><span class="al-card-step">2</span>save</span>Take <code>order.toSnapshot()</code>, write it to the tables.</div>
46
+ </div>
47
+
48
+ ```ts
49
+ export abstract class Orders extends CommandRepository<Order> {}
50
+
51
+ export abstract class OrderSummaries extends QueryRepository<
52
+ OrderSummary
53
+ > {
54
+ abstract summaryOf(id: OrderId): Promise<OrderSummary | undefined>;
55
+ }
56
+ ```
57
+
58
+ ## Where it fits
59
+
60
+ In the PlaceOrder flow, the command handler uses the repository twice: to load the order, and to
61
+ save it once changed.
62
+
63
+ <div class="al-diagram">
64
+ <svg viewBox="0 0 680 300" role="img" aria-label="The PlaceOrderHandler loads the Order through the Orders repository, calls order.place, saves the order through the repository and adds its events to the outbox.">
65
+ <defs>
66
+ <marker id="repository-flow-arrow" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
67
+ <path class="arrow" d="M 0 0 L 10 5 L 0 10 z" />
68
+ </marker>
69
+ </defs>
70
+ <rect class="box" x="8" y="122" width="130" height="56" rx="8" />
71
+ <text class="label" x="73" y="146" text-anchor="middle">Controller</text>
72
+ <text class="note" x="73" y="166" text-anchor="middle">driving adapter</text>
73
+ <path class="link" d="M 138 150 L 178 150" marker-end="url(#repository-flow-arrow)" />
74
+ <rect class="box" x="180" y="122" width="180" height="56" rx="8" />
75
+ <text class="label" x="270" y="146" text-anchor="middle">PlaceOrderHandler</text>
76
+ <text class="note" x="270" y="166" text-anchor="middle">command handler</text>
77
+ <text class="note" x="270" y="204" text-anchor="middle">one unit of work</text>
78
+ <rect class="boundary" x="440" y="24" width="232" height="48" rx="8" />
79
+ <text class="label" x="556" y="44" text-anchor="middle">1 · orders.findById(id)</text>
80
+ <text class="note" x="556" y="62" text-anchor="middle">this page: loads it</text>
81
+ <rect class="box" x="440" y="92" width="232" height="48" rx="8" />
82
+ <text class="label" x="556" y="112" text-anchor="middle">2 · order.place(…)</text>
83
+ <text class="note" x="556" y="130" text-anchor="middle">rules + event</text>
84
+ <rect class="boundary" x="440" y="160" width="232" height="48" rx="8" />
85
+ <text class="label" x="556" y="180" text-anchor="middle">3 · orders.save(order)</text>
86
+ <text class="note" x="556" y="198" text-anchor="middle">this page: stores it</text>
87
+ <rect class="box" x="440" y="228" width="232" height="48" rx="8" />
88
+ <text class="label" x="556" y="248" text-anchor="middle">4 · outbox.add(events)</text>
89
+ <text class="note" x="556" y="266" text-anchor="middle">hands over its events</text>
90
+ <path class="link" d="M 360 150 L 438 48" marker-end="url(#repository-flow-arrow)" />
91
+ <path class="link" d="M 360 150 L 438 116" marker-end="url(#repository-flow-arrow)" />
92
+ <path class="link" d="M 360 150 L 438 184" marker-end="url(#repository-flow-arrow)" />
93
+ <path class="link" d="M 360 150 L 438 252" marker-end="url(#repository-flow-arrow)" />
94
+ </svg>
95
+ </div>
96
+
97
+ ::: tip
98
+ A query takes the other side: the [query handler](../application/query-handlers.md) asks a query
99
+ repository for a [view](./views.md), and no aggregate is loaded.
100
+ :::
101
+
102
+ ## API
103
+
104
+ ```ts
105
+ import { CommandRepository, QueryRepository } from "@alveolus/core";
106
+ // or: from "@alveolus/core/repositories"
107
+ ```
108
+
109
+ ### Type parameters
110
+
111
+ ```ts
112
+ abstract class CommandRepository<
113
+ Aggregate extends AnyAggregateRoot,
114
+ > extends Port { … }
115
+
116
+ abstract class QueryRepository<View extends object> extends Port {}
117
+ ```
118
+
119
+ | Parameter | What it is | Constraint |
120
+ | --- | --- | --- |
121
+ | `Aggregate` | The aggregate a command repository holds. | extends `AggregateRoot` |
122
+ | `View` | The view a query repository reads. | an object type |
123
+
124
+ ### CommandRepository
125
+
126
+ #### `findById(id)` <Badge type="info" text="abstract" /> <Badge type="tip" text="called by the command handler" />
127
+
128
+ ```ts
129
+ abstract findById(
130
+ id: Aggregate["id"],
131
+ ): Promise<Aggregate | undefined>
132
+ ```
133
+
134
+ Loads the aggregate by its identifier; `undefined` when it is missing. Only the identifier of its
135
+ aggregate compiles.
136
+
137
+ #### `save(aggregate)` <Badge type="info" text="abstract" /> <Badge type="tip" text="called by the command handler" />
138
+
139
+ ```ts
140
+ abstract save(aggregate: Aggregate): Promise<void>
141
+ ```
142
+
143
+ Stores the aggregate. It leaves the pending events: the handler pulls them after.
144
+
145
+ Declare the repository abstract in `domain/repositories/`, with the methods your commands need,
146
+ and implement it in `driven/<technology>/adapters/`:
147
+
148
+ ```ts
149
+ abstract class Orders extends CommandRepository<Order> {}
150
+ class PgOrders extends Orders { … }
151
+ ```
152
+
153
+ ### QueryRepository
154
+
155
+ ```ts
156
+ abstract class OrderSummaries extends QueryRepository<OrderSummary> {
157
+ abstract findById(id: OrderId): Promise<OrderSummary | undefined>;
158
+ }
159
+ ```
160
+
161
+ `QueryRepository` has no members of its own: declare the abstract methods your queries need. They
162
+ are called by the query handler and hand out the view, or a collection of it.
163
+
164
+ ::: warning Caveats
165
+ - `findById` only accepts the identifier of its aggregate: passing an `OrderId` to a
166
+ `Customers` repository does not compile.
167
+ - A command handler depends on command repositories only, a query handler on query repositories
168
+ only ([`tactical/no-foreign-command-dependency`](../../rules/tactical/no-foreign-command-dependency.md), [`tactical/no-foreign-query-dependency`](../../rules/tactical/no-foreign-query-dependency.md)).
169
+ - No version is kept: to prevent lost updates, put a `version` in the snapshot and check it in the
170
+ adapter's `UPDATE`.
171
+ :::
172
+
173
+ ## Usage
174
+
175
+ Build `Orders`, the command repository of the `Order` aggregate, then implement it with
176
+ PostgreSQL. Each step shows the whole file it changes: added lines are highlighted, replaced lines are struck out.
177
+
178
+ <div class="al-cards">
179
+ <div class="al-card"><span class="al-card-title"><span class="al-card-step">1</span><a href="#_1-know-what-it-stores">Know what it stores</a></span>One aggregate, as a snapshot.</div>
180
+ <div class="al-card"><span class="al-card-title"><span class="al-card-step">2</span><a href="#_2-declare-the-repository">Declare the repository</a></span>An abstract class in the domain.</div>
181
+ <div class="al-card"><span class="al-card-title"><span class="al-card-step">3</span><a href="#_3-add-what-the-commands-need">Add what the commands need</a></span>And nothing for screens.</div>
182
+ <div class="al-card"><span class="al-card-title"><span class="al-card-step">4</span><a href="#_4-implement-it-in-a-driven-adapter">Implement it in a driven adapter</a></span>Snapshot in, snapshot out.</div>
183
+ <div class="al-card"><span class="al-card-title"><span class="al-card-step">5</span><a href="#_5-use-it-from-a-handler">Use it from a handler</a></span>Load, change, save.</div>
184
+ <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>
185
+ </div>
186
+
187
+ ### 1. Know what it stores
188
+
189
+ A command repository loads and saves one aggregate as a whole: here the
190
+ [`Order` aggregate](./aggregates.md), saved as its `OrderSnapshot`.
191
+
192
+ ### 2. Declare the repository
193
+
194
+ So that the domain loads and saves an order without knowing the database, the repository is an
195
+ abstract class named after the collection. `findById` and `save` come with `CommandRepository`.
196
+
197
+ ```ts [src/ordering/domain/repositories/orders.repository.ts]
198
+ import { CommandRepository } from "@alveolus/core";
199
+
200
+ import type { Order } from "../aggregates/order.aggregate";
201
+
202
+ export abstract class Orders extends CommandRepository<Order> {}
203
+ ```
204
+
205
+ ### 3. Add what the commands need
206
+
207
+ Add only what a command needs, in the words of the domain: here, the drafts of a customer. Reads
208
+ for a screen go to a [query repository](#queryrepository) instead.
209
+
210
+ ```ts [src/ordering/domain/repositories/orders.repository.ts]
211
+ import { CommandRepository } from "@alveolus/core";
212
+
213
+ import type { Order } from "../aggregates/order.aggregate";
214
+ import type { // [!code ++]
215
+ CustomerId, // [!code ++]
216
+ } from "../value-objects/customer-id.identifier"; // [!code ++]
217
+
218
+ export abstract class Orders extends CommandRepository<Order> {} // [!code --]
219
+ export abstract class Orders extends CommandRepository<Order> { // [!code ++]
220
+ abstract findDraftsOf( // [!code ++]
221
+ customerId: CustomerId, // [!code ++]
222
+ ): Promise<readonly Order[]>; // [!code ++]
223
+ } // [!code ++]
224
+ ```
225
+
226
+ ### 4. Implement it in a driven adapter
227
+
228
+ The adapter maps the snapshot to storage: `toSnapshot()` to write, `fromSnapshot()` to read. The
229
+ aggregate never sees a row.
230
+
231
+ ```ts [src/ordering/driven/pg/adapters/pg-orders.adapter.ts]
232
+ import { Order } from "../../../domain/aggregates/order.aggregate";
233
+ import {
234
+ Orders,
235
+ } from "../../../domain/repositories/orders.repository";
236
+ import type {
237
+ CustomerId,
238
+ } from "../../../domain/value-objects/customer-id.identifier";
239
+ import type {
240
+ OrderId,
241
+ } from "../../../domain/value-objects/order-id.identifier";
242
+
243
+ export class PgOrders extends Orders {
244
+ constructor(private readonly db: Database) {
245
+ super();
246
+ }
247
+
248
+ async findById(id: OrderId): Promise<Order | undefined> {
249
+ const row = await this.db
250
+ .selectFrom("orders")
251
+ .where("id", "=", id.value)
252
+ .selectAll()
253
+ .executeTakeFirst();
254
+ if (row === undefined) {
255
+ return undefined;
256
+ }
257
+ return Order.fromSnapshot({
258
+ id: row.id,
259
+ customerId: row.customer_id,
260
+ status: row.status,
261
+ lines: row.lines,
262
+ });
263
+ }
264
+
265
+ async save(order: Order): Promise<void> {
266
+ const snapshot = order.toSnapshot();
267
+ const row = {
268
+ id: snapshot.id,
269
+ customer_id: snapshot.customerId,
270
+ status: snapshot.status,
271
+ lines: JSON.stringify(snapshot.lines),
272
+ };
273
+ await this.db
274
+ .insertInto("orders")
275
+ .values(row)
276
+ .onConflict((conflict) =>
277
+ conflict.column("id").doUpdateSet(row),
278
+ )
279
+ .execute();
280
+ }
281
+
282
+ async findDraftsOf(
283
+ customerId: CustomerId,
284
+ ): Promise<readonly Order[]> { … }
285
+ }
286
+ ```
287
+
288
+ ### 5. Use it from a handler
289
+
290
+ A [command handler](../application/command-handlers.md) loads the order, calls one business
291
+ method and saves it, in one unit of work.
292
+
293
+ ```ts [src/ordering/application/commands/place-order.command.ts]
294
+ const order = await this.orders.findById(new OrderId(orderId));
295
+ if (order === undefined) {
296
+ return err(new OrderNotFound({ orderId }));
297
+ }
298
+ const placed = order.place(this.ids.next(), this.clock.now());
299
+ if (!placed.ok) {
300
+ return placed;
301
+ }
302
+ await this.orders.save(order);
303
+ ```
304
+
305
+ ### 6. Check it
306
+
307
+ Run the checks. Three rules keep the repository the way it is now:
308
+
309
+ ```sh
310
+ npx alveolus arch check
311
+ ```
312
+
313
+ <div class="al-cards">
314
+ <div class="al-card"><span class="al-card-title"><a href="../../rules/layers/no-portless-adapter"><code>no-portless-adapter</code></a></span><code>PgOrders</code> extends <code>Orders</code>, declared in <code>domain/repositories/</code>.</div>
315
+ <div class="al-card"><span class="al-card-title"><a href="../../rules/tactical/no-foreign-query-dependency"><code>no-foreign-query-dependency</code></a></span>Only command handlers receive <code>Orders</code>; queries read views.</div>
316
+ <div class="al-card"><span class="al-card-title"><a href="../../rules/tactical/no-misplaced-class"><code>no-misplaced-class</code></a></span>It stays alone in <code>domain/repositories/*.repository.ts</code>.</div>
317
+ </div>
318
+
319
+ A query handler that receives it is reported:
320
+
321
+ ```
322
+ src/ordering/application/queries/get-order-summary.query.ts
323
+ 20 error tactical/no-foreign-query-dependency: The QueryHandler
324
+ GetOrderSummaryHandler receives Orders, a CommandRepository: a
325
+ query handler receives query repositories, ports that do not
326
+ write, and value objects.
327
+ ```
328
+
329
+ ## See also
330
+
331
+ - [Aggregates](./aggregates.md), what a command repository holds, and their snapshots
332
+ - [Views](./views.md), what a query repository returns
333
+ - [Ports](./ports.md), the other dependencies of the domain
334
+ - Rules: [`tactical/no-foreign-command-dependency`](../../rules/tactical/no-foreign-command-dependency.md), [`tactical/no-foreign-query-dependency`](../../rules/tactical/no-foreign-query-dependency.md), [`layers/no-portless-adapter`](../../rules/layers/no-portless-adapter.md)
335
+ - Vaughn Vernon, *Implementing Domain-Driven Design*, chapter 12, "Repositories"