@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,265 @@
1
+ ---
2
+ description: "Views in CQRS with TypeScript: the read-only shape a query returns, built for the screen or API that shows it, without loading aggregates."
3
+ ---
4
+
5
+ # Views
6
+
7
+ A view is the read-only shape a query returns, built for the screen or the API that shows it.
8
+
9
+ <dl class="al-glance">
10
+ <dt>Layer</dt><dd>Domain</dd>
11
+ <dt>File</dt><dd><code>domain/views/order-summary.view.ts</code></dd>
12
+ <dt>Type</dt><dd><a href="#api"><code>View&lt;Props&gt;</code></a></dd>
13
+ <dt>Read by</dt><dd><a href="/core/domain/repositories">Query repositories</a></dd>
14
+ <dt>Returned by</dt><dd><a href="/core/application/query-handlers">Query handlers</a></dd>
15
+ <dt>Checked by</dt><dd><a href="/rules/tactical/no-foreign-command-dependency"><code>tactical/no-foreign-command-dependency</code></a></dd>
16
+ </dl>
17
+
18
+ ## Why
19
+
20
+ The orders screen shows each order's status and number of lines. Loading fifty `Order` aggregates,
21
+ with all their lines, just to count them is slow. It also hands the screen an object with
22
+ `place()` and `addLine()`: a read could change an order.
23
+
24
+ ::: tip The fix
25
+ A view is a plain read-only type shaped for that screen: `OrderSummary`. A query repository builds
26
+ it straight from storage, without loading the aggregate. It carries data, so reading can never
27
+ write.
28
+ :::
29
+
30
+ ## How it works
31
+
32
+ An aggregate and a view describe the same order for two different jobs.
33
+
34
+ | | Aggregate | View |
35
+ | --- | --- | --- |
36
+ | **Shaped for** | the business rules | a screen or an API |
37
+ | **Has methods** | yes, the business methods | no, only fields |
38
+ | **Read through** | a command repository | a query repository |
39
+ | **Can change** | through its methods | never |
40
+
41
+ ```ts
42
+ export type OrderSummary = View<{
43
+ readonly id: OrderId;
44
+ readonly customerId: CustomerId;
45
+ readonly status: "draft" | "placed";
46
+ readonly lineCount: number;
47
+ }>;
48
+ ```
49
+
50
+ ## Where it fits
51
+
52
+ A query never touches the aggregate. The [query handler](../application/query-handlers.md) asks a
53
+ [query repository](./repositories.md) for the view, and returns it.
54
+
55
+ <div class="al-diagram">
56
+ <svg viewBox="0 0 680 200" role="img" aria-label="A request goes from a controller to the GetOrderSummaryHandler, which asks the OrderSummaries query repository and returns an OrderSummary view.">
57
+ <defs>
58
+ <marker id="view-flow-arrow" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
59
+ <path class="arrow" d="M 0 0 L 10 5 L 0 10 z" />
60
+ </marker>
61
+ </defs>
62
+ <rect class="box" x="8" y="72" width="130" height="56" rx="8" />
63
+ <text class="label" x="73" y="96" text-anchor="middle">Controller</text>
64
+ <text class="note" x="73" y="116" text-anchor="middle">driving adapter</text>
65
+ <path class="link" d="M 138 100 L 168 100" marker-end="url(#view-flow-arrow)" />
66
+ <rect class="box" x="170" y="72" width="210" height="56" rx="8" />
67
+ <text class="label" x="275" y="96" text-anchor="middle">GetOrderSummaryHandler</text>
68
+ <text class="note" x="275" y="116" text-anchor="middle">query handler</text>
69
+ <rect class="box" x="440" y="36" width="232" height="48" rx="8" />
70
+ <text class="label" x="556" y="56" text-anchor="middle">1 · summaries.summaryOf(id)</text>
71
+ <text class="note" x="556" y="74" text-anchor="middle">reads storage, no aggregate</text>
72
+ <rect class="boundary" x="440" y="116" width="232" height="48" rx="8" />
73
+ <text class="label" x="556" y="136" text-anchor="middle">2 · OrderSummary</text>
74
+ <text class="note" x="556" y="154" text-anchor="middle">this page: what it returns</text>
75
+ <path class="link" d="M 380 100 L 438 60" marker-end="url(#view-flow-arrow)" />
76
+ <path class="link" d="M 438 140 L 382 104" marker-end="url(#view-flow-arrow)" />
77
+ </svg>
78
+ </div>
79
+
80
+ ::: tip
81
+ The driving adapter turns the view into its response, or into a
82
+ [representation](../strategic/published-language.md) for another context.
83
+ :::
84
+
85
+ ## API
86
+
87
+ ```ts
88
+ import type { View } from "@alveolus/core";
89
+ // or: import type { View } from "@alveolus/core/views";
90
+ ```
91
+
92
+ ### Type parameters
93
+
94
+ ```ts
95
+ type View<Props extends object> = Readonly<Props>;
96
+ ```
97
+
98
+ | Parameter | What it is | Constraint |
99
+ | --- | --- | --- |
100
+ | `Props` | The fields of the view. | an object type |
101
+
102
+ ### Declaration
103
+
104
+ ```ts
105
+ type OrderSummary = View<{
106
+ readonly id: OrderId;
107
+ readonly lineCount: number;
108
+ }>;
109
+ ```
110
+
111
+ The view, in `domain/views/`: the fields the reader needs, read-only. It has no members.
112
+
113
+ ::: warning Caveats
114
+ - `View` changes nothing at runtime: it is `Readonly<Props>`. It marks the type as the answer of a
115
+ query, for you and for your reviewers.
116
+ - `Readonly` is shallow: nested arrays and objects stay mutable unless you declare them `readonly`.
117
+ - No separate read model is implied: views are read from the same storage as the aggregates. CQRS
118
+ with separate stores is out of scope.
119
+ :::
120
+
121
+ ## Usage
122
+
123
+ Build `OrderSummary`, what a screen shows for one order, then the query repository and the adapter
124
+ that read it. Each step shows the whole file it changes: added lines are highlighted, replaced lines are struck out.
125
+
126
+ <div class="al-cards">
127
+ <div class="al-card"><span class="al-card-title"><span class="al-card-step">1</span><a href="#_1-know-who-reads-it">Know who reads it</a></span>One screen, one answer.</div>
128
+ <div class="al-card"><span class="al-card-title"><span class="al-card-step">2</span><a href="#_2-declare-the-view">Declare the view</a></span>Read-only data, no behaviour.</div>
129
+ <div class="al-card"><span class="al-card-title"><span class="al-card-step">3</span><a href="#_3-declare-where-it-comes-from">Declare where it comes from</a></span>A query repository.</div>
130
+ <div class="al-card"><span class="al-card-title"><span class="al-card-step">4</span><a href="#_4-build-it-from-storage">Build it from storage</a></span>No aggregate loaded.</div>
131
+ <div class="al-card"><span class="al-card-title"><span class="al-card-step">5</span><a href="#_5-return-it-from-a-query-handler">Return it from a query handler</a></span>The query side only.</div>
132
+ <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>
133
+ </div>
134
+
135
+ ### 1. Know who reads it
136
+
137
+ A view answers one screen or one API response: here, the summary of an order, read without
138
+ loading the [`Order` aggregate](./aggregates.md).
139
+
140
+ ### 2. Declare the view
141
+
142
+ A view is plain, read-only data shaped for its reader: a `View` type, not a class, so it has no
143
+ behaviour to keep in sync with the model.
144
+
145
+ ```ts [src/ordering/domain/views/order-summary.view.ts]
146
+ import type { View } from "@alveolus/core";
147
+
148
+ import type {
149
+ CustomerId,
150
+ } from "../value-objects/customer-id.identifier";
151
+ import type { OrderId } from "../value-objects/order-id.identifier";
152
+
153
+ export type OrderSummary = View<{
154
+ readonly id: OrderId;
155
+ readonly customerId: CustomerId;
156
+ readonly status: "draft" | "placed";
157
+ readonly lineCount: number;
158
+ }>;
159
+ ```
160
+
161
+ ### 3. Declare where it comes from
162
+
163
+ The query side gets its own [repository](./repositories.md#queryrepository), declared in the
164
+ domain, that returns views only.
165
+
166
+ ```ts [src/ordering/domain/repositories/order-summaries.repository.ts]
167
+ import { QueryRepository } from "@alveolus/core";
168
+
169
+ import type { OrderId } from "../value-objects/order-id.identifier";
170
+ import type { OrderSummary } from "../views/order-summary.view";
171
+
172
+ export abstract class OrderSummaries extends QueryRepository<
173
+ OrderSummary
174
+ > {
175
+ abstract summaryOf(id: OrderId): Promise<OrderSummary | undefined>;
176
+ }
177
+ ```
178
+
179
+ ### 4. Build it from storage
180
+
181
+ The adapter reads the storage and shapes the view directly: no aggregate is loaded, no rule runs.
182
+
183
+ ```ts [src/ordering/driven/pg/adapters/pg-order-summaries.adapter.ts]
184
+ import {
185
+ OrderSummaries,
186
+ } from "../../../domain/repositories/order-summaries.repository";
187
+ import {
188
+ CustomerId,
189
+ } from "../../../domain/value-objects/customer-id.identifier";
190
+ import {
191
+ OrderId,
192
+ } from "../../../domain/value-objects/order-id.identifier";
193
+ import type {
194
+ OrderSummary,
195
+ } from "../../../domain/views/order-summary.view";
196
+
197
+ export class PgOrderSummaries extends OrderSummaries {
198
+ constructor(private readonly db: Database) {
199
+ super();
200
+ }
201
+
202
+ async summaryOf(id: OrderId): Promise<OrderSummary | undefined> {
203
+ const row = await this.db
204
+ .selectFrom("orders")
205
+ .where("id", "=", id.value)
206
+ .selectAll()
207
+ .executeTakeFirst();
208
+ if (row === undefined) {
209
+ return undefined;
210
+ }
211
+ return {
212
+ id: new OrderId(row.id),
213
+ customerId: new CustomerId(row.customer_id),
214
+ status: row.status,
215
+ lineCount: row.lines.length,
216
+ };
217
+ }
218
+ }
219
+ ```
220
+
221
+ ### 5. Return it from a query handler
222
+
223
+ A [query handler](../application/query-handlers.md) asks the query repository and returns the
224
+ view as it is.
225
+
226
+ ```ts [src/ordering/application/queries/get-order-summary.query.ts]
227
+ const summary = await this.summaries.summaryOf(
228
+ new OrderId(orderId),
229
+ );
230
+ if (summary === undefined) {
231
+ return err(new OrderNotFound({ orderId }));
232
+ }
233
+ return ok(summary);
234
+ ```
235
+
236
+ ### 6. Check it
237
+
238
+ Run the checks. Two rules keep the view on the query side:
239
+
240
+ ```sh
241
+ npx alveolus arch check
242
+ ```
243
+
244
+ <div class="al-cards">
245
+ <div class="al-card"><span class="al-card-title"><a href="../../rules/tactical/no-foreign-command-dependency"><code>no-foreign-command-dependency</code></a></span>Only query handlers receive <code>OrderSummaries</code>.</div>
246
+ <div class="al-card"><span class="al-card-title"><a href="../../rules/layers/no-portless-adapter"><code>no-portless-adapter</code></a></span><code>PgOrderSummaries</code> extends the query repository it implements.</div>
247
+ </div>
248
+
249
+ A command handler that reads it is reported:
250
+
251
+ ```
252
+ src/ordering/application/commands/place-order.command.ts
253
+ 47 error tactical/no-foreign-command-dependency: The CommandHandler
254
+ PlaceOrderHandler receives OrderSummaries, a QueryRepository: a
255
+ command handler receives command repositories, ports, event
256
+ translators, domain services and value objects.
257
+ ```
258
+
259
+ ## See also
260
+
261
+ - [Repositories](./repositories.md), the query repository that returns a view
262
+ - [Query handlers](../application/query-handlers.md), which return views
263
+ - [Aggregates](./aggregates.md), the write side of the same data
264
+ - [Published Language](../strategic/published-language.md), when a view leaves the context
265
+ - Rules: [`tactical/no-foreign-command-dependency`](../../rules/tactical/no-foreign-command-dependency.md)
@@ -0,0 +1,108 @@
1
+ ---
2
+ description: "The Domain-Driven Design building blocks of @alveolus/core as TypeScript abstract classes: aggregates, entities, value objects, domain events, repositories."
3
+ ---
4
+
5
+ # Building blocks
6
+
7
+ `@alveolus/core` gives you the building blocks of Domain-Driven Design as abstract classes. Your
8
+ classes extend them: `Order extends AggregateRoot`, `Money extends ValueObject`. The class says what
9
+ it is, and [`alveolus arch check`](../rules/index.md) knows where it belongs and what it may depend
10
+ on.
11
+
12
+ ```ts
13
+ import { AggregateRoot, err, ok, type Result } from "@alveolus/core";
14
+
15
+ export class Order extends AggregateRoot<OrderId, OrderPlaced, OrderSnapshot> {
16
+ place(
17
+ eventId: string,
18
+ now: Date,
19
+ ): Result<void, OrderAlreadyPlaced | EmptyOrder> {
20
+ if (this.isPlaced) {
21
+ return err(new OrderAlreadyPlaced());
22
+ }
23
+ if (this.lines.length === 0) {
24
+ return err(new EmptyOrder());
25
+ }
26
+ this.status = "placed";
27
+ this.record(
28
+ new OrderPlaced({
29
+ id: eventId,
30
+ aggregateId: this.id,
31
+ occurredAt: now,
32
+ payload: { customerId: this.customerId.value },
33
+ }),
34
+ );
35
+ return ok();
36
+ }
37
+ }
38
+ ```
39
+
40
+ ## Strategic
41
+
42
+ How bounded contexts meet without sharing a model. See the [overview](./strategic/index.md) and the
43
+ [bounded contexts](../guide/project-layout.md#bounded-contexts) of the project layout.
44
+
45
+ | Building block | What it is |
46
+ | --- | --- |
47
+ | [Published Language](./strategic/published-language.md) | The JSON format exchanged between contexts. |
48
+ | [Open host services](./strategic/open-host-services.md) | The documented entry point of a context, the only class others may import. |
49
+ | [Anti-corruption layers](./strategic/anti-corruption-layers.md) | The adapter that reads another context and translates it into yours. |
50
+
51
+ ## Domain
52
+
53
+ The model and what it needs from the outside world. Everything here lives in `domain/` and imports
54
+ nothing but the domain. See the [overview](./domain/index.md).
55
+
56
+ | Building block | What it is |
57
+ | --- | --- |
58
+ | [Aggregates](./domain/aggregates.md) | A cluster of objects changed as one unit, through its root, which records domain events. |
59
+ | [Entities](./domain/entities.md) | An object defined by its identity, inside an aggregate. |
60
+ | [Value objects](./domain/value-objects.md) | An immutable value compared by its attributes, and the typed identifiers. |
61
+ | [Domain events](./domain/domain-events.md) | Something that happened in the domain, in the past tense. |
62
+ | [Domain errors](./domain/domain-errors.md) | An expected business failure, returned as a value. |
63
+ | [Domain services](./domain/domain-services.md) | A stateless operation that belongs to no single object. |
64
+ | [Ports](./domain/ports.md) | What the domain needs from the outside world, in its own words. |
65
+ | [Repositories](./domain/repositories.md) | How aggregates are loaded and saved, and how views are read. |
66
+ | [Views](./domain/views.md) | What a query returns. |
67
+
68
+ ## Application
69
+
70
+ The use cases, and the contracts that make them atomic and reliable. Everything here lives in
71
+ `application/`, except the adapters that implement the contracts. See the
72
+ [overview](./application/index.md).
73
+
74
+ | Building block | What it is |
75
+ | --- | --- |
76
+ | [Command handlers](./application/command-handlers.md) | The application service of one use case that changes the system. |
77
+ | [Query handlers](./application/query-handlers.md) | The application service of one read. |
78
+ | [Event translators](./application/event-translators.md) | Turns domain events into integration events. |
79
+ | [Integration events](./application/integration-events.md) | What other contexts receive when something happens: JSON. |
80
+ | [Event publishers](./application/event-publishers.md) | Sends integration events to the rest of the system. |
81
+ | [Unit of Work](./application/unit-of-work.md) | Makes a use case atomic: commit on success, roll back otherwise. |
82
+ | [Outbox](./application/outbox.md) | Stores integration events with the change, then relays them, so none is lost. |
83
+
84
+ ## Utilities
85
+
86
+ | Building block | What it is |
87
+ | --- | --- |
88
+ | [Result](./utilities/result.md) | Success or failure as a value, with the helpers to combine them. |
89
+
90
+ ## Principles
91
+
92
+ - **You extend, nothing is configured.** No decorator, no registry, no reflection: the class you
93
+ extend says what your class is. A class extending your own base class counts too.
94
+ - **Business errors are values.** An expected failure is a `DomainError` returned in a `Result`,
95
+ never thrown. Each signature lists what can go wrong. Exceptions stay in adapters.
96
+ - **The domain never reads the clock nor generates ids.** Dates and ids are parameters of business
97
+ methods; `Clock` and `IdGenerator` give them to the application.
98
+ - **No infrastructure.** No bus, no container, no ORM. Repositories, ports and publishers are
99
+ abstract classes your adapters extend with the tools you already use. Handlers take them in
100
+ their constructor: wire them by hand or with any container, where the same classes are the
101
+ injection tokens.
102
+ - **No runtime dependency.** `@alveolus/core` ends up in your domain and brings nothing with it.
103
+
104
+ ## Import paths
105
+
106
+ Import everything from `@alveolus/core`, or each building block from its own entry point, named
107
+ after its page: `@alveolus/core/aggregates`, `@alveolus/core/value-objects`,
108
+ `@alveolus/core/command-handlers`, `@alveolus/core/result`… Both give the same classes.