@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,324 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: "The folder structure of a Domain-Driven Design project in TypeScript: bounded contexts, domain, application, adapters and the direction between layers."
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
# Project layout
|
|
6
|
+
|
|
7
|
+
Every Alveolus project has the same shape: the same folders, the same file names, the same
|
|
8
|
+
direction between layers.
|
|
9
|
+
|
|
10
|
+
<dl class="al-glance">
|
|
11
|
+
<dt>Unit</dt><dd>A <a href="#bounded-contexts">bounded context</a>: a folder under <code>src/</code>, such as <code>src/ordering/</code></dd>
|
|
12
|
+
<dt>Layers</dt><dd><a href="#layers"><code>domain/</code>, <code>application/</code>, <code>published-language/</code>, <code>driven/</code>, <code>driving/</code></a></dd>
|
|
13
|
+
<dt>Wired by</dt><dd><a href="#composition-root">The composition root</a>, <code>ordering.module.ts</code></dd>
|
|
14
|
+
<dt>Shared</dt><dd><a href="#shared-kernel"><code>src/shared-kernel/</code></a>, same shape</dd>
|
|
15
|
+
<dt>Checked by</dt><dd><a href="/rules/layers/no-outward-import"><code>layers/no-outward-import</code></a>, <a href="/rules/tactical/no-misplaced-class"><code>tactical/no-misplaced-class</code></a>, <a href="/rules/strategic/no-cross-context-import"><code>strategic/no-cross-context-import</code></a>, <a href="/rules/layers/no-impure-domain"><code>layers/no-impure-domain</code></a></dd>
|
|
16
|
+
</dl>
|
|
17
|
+
|
|
18
|
+
## Why
|
|
19
|
+
|
|
20
|
+
A new teammate looks for the rule that refuses an empty order. Is it in the controller, a service,
|
|
21
|
+
a helper, the repository? An agent asked to add a rule puts it wherever the task led it. Each
|
|
22
|
+
project invents its own layout, and each layout erodes a little with every change.
|
|
23
|
+
|
|
24
|
+
::: tip The fix
|
|
25
|
+
One layout for every project, kept by `alveolus arch check`. Knowing what a class is tells you
|
|
26
|
+
where it lives, and the other way round: the rule is in `domain/aggregates/order.aggregate.ts`,
|
|
27
|
+
because that is where it can only be.
|
|
28
|
+
:::
|
|
29
|
+
|
|
30
|
+
## How it works
|
|
31
|
+
|
|
32
|
+
A bounded context is split into layers, each in its folder. The domain sits at the center, the
|
|
33
|
+
application around it, the adapters at the edge; the composition root wires them together.
|
|
34
|
+
|
|
35
|
+
<div class="al-diagram">
|
|
36
|
+
<svg viewBox="0 0 680 380" role="img" aria-label="A bounded context. Its composition root wires four layers. Driving adapters call the application, the application uses the domain, driven adapters implement the ports of the domain. The published language is the format exchanged with other contexts. Every dependency points towards the domain.">
|
|
37
|
+
<defs>
|
|
38
|
+
<marker id="layout-arrow" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
|
|
39
|
+
<path class="arrow" d="M 0 0 L 10 5 L 0 10 z" />
|
|
40
|
+
</marker>
|
|
41
|
+
</defs>
|
|
42
|
+
<rect class="boundary" x="8" y="8" width="664" height="364" rx="14" />
|
|
43
|
+
<text class="note" x="24" y="32">src/ordering/ · a bounded context</text>
|
|
44
|
+
<rect class="box" x="200" y="46" width="280" height="48" rx="8" />
|
|
45
|
+
<text class="label" x="340" y="68" text-anchor="middle">composition root</text>
|
|
46
|
+
<text class="note" x="340" y="85" text-anchor="middle">ordering.module.ts · wires it all</text>
|
|
47
|
+
<rect class="box" x="28" y="128" width="150" height="150" rx="8" />
|
|
48
|
+
<text class="label" x="103" y="160" text-anchor="middle">driving/</text>
|
|
49
|
+
<text class="note" x="103" y="184" text-anchor="middle">controllers</text>
|
|
50
|
+
<text class="note" x="103" y="202" text-anchor="middle">consumers</text>
|
|
51
|
+
<text class="note" x="103" y="220" text-anchor="middle">jobs, CLI…</text>
|
|
52
|
+
<text class="note" x="103" y="258" text-anchor="middle">calls use cases</text>
|
|
53
|
+
<rect class="box" x="502" y="128" width="150" height="150" rx="8" />
|
|
54
|
+
<text class="label" x="577" y="160" text-anchor="middle">driven/</text>
|
|
55
|
+
<text class="note" x="577" y="184" text-anchor="middle">repositories</text>
|
|
56
|
+
<text class="note" x="577" y="202" text-anchor="middle">API clients</text>
|
|
57
|
+
<text class="note" x="577" y="220" text-anchor="middle">outbox, clock…</text>
|
|
58
|
+
<text class="note" x="577" y="258" text-anchor="middle">implements ports</text>
|
|
59
|
+
<rect class="box" x="210" y="118" width="260" height="170" rx="10" />
|
|
60
|
+
<text class="label" x="340" y="142" text-anchor="middle">application/</text>
|
|
61
|
+
<text class="note" x="340" y="160" text-anchor="middle">commands · queries</text>
|
|
62
|
+
<rect class="boundary" x="228" y="176" width="224" height="96" rx="8" />
|
|
63
|
+
<text class="label" x="340" y="204" text-anchor="middle">domain/</text>
|
|
64
|
+
<text class="note" x="340" y="224" text-anchor="middle">aggregates · entities</text>
|
|
65
|
+
<text class="note" x="340" y="242" text-anchor="middle">value objects · events</text>
|
|
66
|
+
<text class="note" x="340" y="260" text-anchor="middle">errors · services · ports</text>
|
|
67
|
+
<rect class="box" x="230" y="314" width="220" height="44" rx="8" />
|
|
68
|
+
<text class="label" x="340" y="334" text-anchor="middle">published-language/</text>
|
|
69
|
+
<text class="note" x="340" y="350" text-anchor="middle">what other contexts read</text>
|
|
70
|
+
<path class="link" d="M 178 203 L 208 203" marker-end="url(#layout-arrow)" />
|
|
71
|
+
<path class="link" d="M 502 224 L 454 224" marker-end="url(#layout-arrow)" />
|
|
72
|
+
<path class="link" d="M 340 288 L 340 312" marker-end="url(#layout-arrow)" />
|
|
73
|
+
<path class="link" d="M 103 278 L 103 336 L 228 336" marker-end="url(#layout-arrow)" />
|
|
74
|
+
<path class="link" d="M 577 278 L 577 336 L 452 336" marker-end="url(#layout-arrow)" />
|
|
75
|
+
<path class="link" d="M 230 94 L 140 126" marker-end="url(#layout-arrow)" />
|
|
76
|
+
<path class="link" d="M 450 94 L 540 126" marker-end="url(#layout-arrow)" />
|
|
77
|
+
<path class="link" d="M 340 94 L 340 116" marker-end="url(#layout-arrow)" />
|
|
78
|
+
</svg>
|
|
79
|
+
</div>
|
|
80
|
+
|
|
81
|
+
Arrows read "depends on". They all point inwards: the domain depends on nothing but itself, so the
|
|
82
|
+
business rules never change because a database, a framework or another context did.
|
|
83
|
+
|
|
84
|
+
## The tree
|
|
85
|
+
|
|
86
|
+
```
|
|
87
|
+
src/
|
|
88
|
+
main.ts # starts the application
|
|
89
|
+
app.module.ts # assembles the bounded contexts
|
|
90
|
+
ordering/ # a bounded context
|
|
91
|
+
ordering.module.ts # its composition root
|
|
92
|
+
domain/
|
|
93
|
+
aggregates/ # order.aggregate.ts
|
|
94
|
+
entities/ # order-line.entity.ts
|
|
95
|
+
value-objects/ # order-id.identifier.ts
|
|
96
|
+
events/ # order-placed.event.ts
|
|
97
|
+
errors/ # invalid-total.error.ts
|
|
98
|
+
services/ # shipping-cost.service.ts
|
|
99
|
+
repositories/ # orders.repository.ts
|
|
100
|
+
ports/ # price-list.port.ts
|
|
101
|
+
views/ # order-summary.view.ts
|
|
102
|
+
application/
|
|
103
|
+
commands/ # place-order.command.ts
|
|
104
|
+
queries/ # get-order-summary.query.ts
|
|
105
|
+
translators/ # order-events.translator.ts
|
|
106
|
+
published-language/ # order-placed.representation.ts
|
|
107
|
+
driven/
|
|
108
|
+
pg/
|
|
109
|
+
adapters/ # pg-orders.adapter.ts
|
|
110
|
+
catalog/
|
|
111
|
+
adapters/ # catalog-price-list.adapter.ts
|
|
112
|
+
driving/
|
|
113
|
+
http/
|
|
114
|
+
controllers/ # orders.controller.ts
|
|
115
|
+
rabbitmq/
|
|
116
|
+
consumers/ # payment-received.consumer.ts
|
|
117
|
+
catalog/ # another bounded context, same shape
|
|
118
|
+
shared-kernel/ # shared by every bounded context, same shape
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
Only create a folder when it gets its first file: a small context may have no `entities/`,
|
|
122
|
+
`services/` or `driving/rabbitmq/`. No other folder is expected: a file directly in `domain/`, in a
|
|
123
|
+
folder such as `domain/helpers/`, or one level too deep is reported by
|
|
124
|
+
[`layers/no-outward-import`](../rules/layers/no-outward-import.md#folders-inside-a-layer).
|
|
125
|
+
|
|
126
|
+
## Bounded contexts
|
|
127
|
+
|
|
128
|
+
A bounded context is a folder under `src/`, declared in
|
|
129
|
+
[`alveolus.config.ts`](./getting-started.md#configure-the-checks). Contexts may be nested, for
|
|
130
|
+
instance under `src/modules/`. Each one has its own model: a `Product` in the catalog and a product
|
|
131
|
+
in ordering are two different things, and neither imports the other.
|
|
132
|
+
|
|
133
|
+
<div class="al-cards">
|
|
134
|
+
<div class="al-card"><span class="al-card-title">Closed</span>The only class another context may import is its <a href="../core/strategic/open-host-services">open host service</a>.</div>
|
|
135
|
+
<div class="al-card"><span class="al-card-title">Entered at one place</span>Only an <a href="../core/strategic/anti-corruption-layers">anti-corruption layer</a> or the composition root may import that service.</div>
|
|
136
|
+
<div class="al-card"><span class="al-card-title">Talking in JSON</span>What crosses the boundary is the <a href="../core/strategic/published-language">published language</a>, redeclared by the reader, never the classes of the other model.</div>
|
|
137
|
+
</div>
|
|
138
|
+
|
|
139
|
+
<div class="al-diagram">
|
|
140
|
+
<svg viewBox="0 0 680 230" role="img" aria-label="Two bounded contexts. In ordering, the anti-corruption layer CatalogPriceList implements the PriceList port of the domain and imports CatalogApi, the open host service of catalog. A direct import from the ordering domain to the catalog domain is forbidden.">
|
|
141
|
+
<defs>
|
|
142
|
+
<marker id="boundary-arrow" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
|
|
143
|
+
<path class="arrow" d="M 0 0 L 10 5 L 0 10 z" />
|
|
144
|
+
</marker>
|
|
145
|
+
</defs>
|
|
146
|
+
<rect class="boundary" x="8" y="8" width="300" height="214" rx="14" />
|
|
147
|
+
<text class="note" x="24" y="32">src/catalog/</text>
|
|
148
|
+
<rect class="box" x="30" y="50" width="256" height="58" rx="8" />
|
|
149
|
+
<text class="label" x="158" y="74" text-anchor="middle">CatalogApi</text>
|
|
150
|
+
<text class="note" x="158" y="94" text-anchor="middle">driving/ · OpenHostService</text>
|
|
151
|
+
<rect class="box" x="30" y="146" width="256" height="58" rx="8" />
|
|
152
|
+
<text class="label" x="158" y="170" text-anchor="middle">Product</text>
|
|
153
|
+
<text class="note" x="158" y="190" text-anchor="middle">domain/aggregates/</text>
|
|
154
|
+
<rect class="boundary" x="372" y="8" width="300" height="214" rx="14" />
|
|
155
|
+
<text class="note" x="388" y="32">src/ordering/</text>
|
|
156
|
+
<rect class="box" x="394" y="50" width="256" height="58" rx="8" />
|
|
157
|
+
<text class="label" x="522" y="74" text-anchor="middle">CatalogPriceList</text>
|
|
158
|
+
<text class="note" x="522" y="94" text-anchor="middle">driven/ · AntiCorruptionLayer</text>
|
|
159
|
+
<rect class="box" x="394" y="146" width="256" height="58" rx="8" />
|
|
160
|
+
<text class="label" x="522" y="170" text-anchor="middle">PriceList</text>
|
|
161
|
+
<text class="note" x="522" y="190" text-anchor="middle">domain/ports/</text>
|
|
162
|
+
<path class="link" d="M 394 79 L 288 79" marker-end="url(#boundary-arrow)" />
|
|
163
|
+
<text class="note" x="341" y="70" text-anchor="middle">imports</text>
|
|
164
|
+
<path class="link" d="M 522 108 L 522 144" marker-end="url(#boundary-arrow)" />
|
|
165
|
+
<text class="note" x="530" y="131">extends</text>
|
|
166
|
+
<path class="link" d="M 394 175 L 288 175" stroke-dasharray="4 4" marker-end="url(#boundary-arrow)" />
|
|
167
|
+
<text class="label" x="341" y="166" text-anchor="middle">✕</text>
|
|
168
|
+
<text class="note" x="341" y="196" text-anchor="middle">never</text>
|
|
169
|
+
</svg>
|
|
170
|
+
</div>
|
|
171
|
+
|
|
172
|
+
The ordering domain asks for prices in its own words, through the `PriceList` port. The
|
|
173
|
+
anti-corruption layer is the one place that knows the catalog exists: it calls `CatalogApi`,
|
|
174
|
+
reads its JSON and answers with ordering's objects. If the catalog moves behind HTTP, only that
|
|
175
|
+
adapter changes. Checked by [`strategic/no-cross-context-import`](../rules/strategic/no-cross-context-import.md).
|
|
176
|
+
|
|
177
|
+
### Core, supporting, generic
|
|
178
|
+
|
|
179
|
+
Not every bounded context deserves the same investment. Vernon classifies the subdomains a system
|
|
180
|
+
covers, and a bounded context implements one of them: the **core domain** is what the business
|
|
181
|
+
competes on and gets the best developers and the full tactical model; a **supporting subdomain**
|
|
182
|
+
is needed but not distinctive, written in house with a lighter hand; a **generic subdomain** is
|
|
183
|
+
bought, taken off the shelf or wrapped. `alveolus.config.ts` says which is which, under
|
|
184
|
+
`subdomains`, and every context is listed once.
|
|
185
|
+
|
|
186
|
+
<div class="al-cards">
|
|
187
|
+
<div class="al-card"><span class="al-card-title">Core</span>Every rule applies: the layers, the building blocks, the boundary.</div>
|
|
188
|
+
<div class="al-card"><span class="al-card-title">Supporting and generic</span>Only the <a href="/rules/#where-a-rule-applies">boundary rules</a> apply: the context is closed, reached through its open host service, and consumes the others through theirs. Inside, any layout and any code.</div>
|
|
189
|
+
<div class="al-card"><span class="al-card-title">Shared kernel</span>Every rule applies: what it holds reaches every context.</div>
|
|
190
|
+
</div>
|
|
191
|
+
|
|
192
|
+
A supporting or generic context needs no layers, no composition root and no building block. The
|
|
193
|
+
one thing it marks is its open host service, when another context calls it. When it consumes the
|
|
194
|
+
core, it imports the open host service from anywhere; only a core context has to translate what it
|
|
195
|
+
consumes in an anti-corruption layer, because only it has a model to protect.
|
|
196
|
+
|
|
197
|
+
## Layers
|
|
198
|
+
|
|
199
|
+
<div class="al-cards">
|
|
200
|
+
<div class="al-card"><span class="al-card-title"><code>domain/</code></span>The model: aggregates, entities, value objects, events, errors, domain services, and the ports and repositories it needs. No framework, no ORM.</div>
|
|
201
|
+
<div class="al-card"><span class="al-card-title"><code>application/</code></span>One class per use case: command handlers, query handlers, and the translators that turn domain events into the published language.</div>
|
|
202
|
+
<div class="al-card"><span class="al-card-title"><code>published-language/</code></span>The JSON types exchanged with other contexts: what this context publishes, and what it reads from the others.</div>
|
|
203
|
+
<div class="al-card"><span class="al-card-title"><code>driven/</code></span>The adapters that implement the ports: database repositories, API clients, the outbox, the clock.</div>
|
|
204
|
+
<div class="al-card"><span class="al-card-title"><code>driving/</code></span>The adapters that call the use cases: HTTP controllers, message consumers, scheduled jobs, CLI commands.</div>
|
|
205
|
+
</div>
|
|
206
|
+
|
|
207
|
+
### Who may import what
|
|
208
|
+
|
|
209
|
+
Read a row as "files in this layer may import…", within the same bounded context or from the
|
|
210
|
+
shared kernel.
|
|
211
|
+
|
|
212
|
+
| From ↓ · To → | domain | application | published-language | driven | driving | composition root |
|
|
213
|
+
| --- | :-: | :-: | :-: | :-: | :-: | :-: |
|
|
214
|
+
| **domain** | ✓ | ✕ | ✕ | ✕ | ✕ | ✕ |
|
|
215
|
+
| **application** | ✓ | ✓ | ✓ | ✕ | ✕ | ✕ |
|
|
216
|
+
| **published-language** | ✕ | ✕ | ✓ | ✕ | ✕ | ✕ |
|
|
217
|
+
| **driven** | ✓ | ✓ | ✓ | ✓ | ✕ | ✕ |
|
|
218
|
+
| **driving** | ✓ | ✓ | ✓ | ✕ | ✓ | ✕ |
|
|
219
|
+
| **composition root** | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
|
|
220
|
+
|
|
221
|
+
A driving adapter calls the handlers of the application: it imports no repository, port, aggregate
|
|
222
|
+
or domain service of the domain, checked by
|
|
223
|
+
[`layers/no-driving-shortcut`](../rules/layers/no-driving-shortcut.md).
|
|
224
|
+
|
|
225
|
+
Outside the project, the domain may import the domain building blocks of `@alveolus/core` and the
|
|
226
|
+
packages listed in `domainDependencies`; the application adds the rest of `@alveolus/core` and
|
|
227
|
+
`applicationDependencies`; the published language may import the published-language types of core
|
|
228
|
+
and packages such as a schema library; adapters may import any package. Checked by
|
|
229
|
+
[`layers/no-outward-import`](../rules/layers/no-outward-import.md) and [`layers/no-impure-domain`](../rules/layers/no-impure-domain.md).
|
|
230
|
+
|
|
231
|
+
### Adapters by technology
|
|
232
|
+
|
|
233
|
+
Inside `driven/` and `driving/`, files always sit under the name of their technology:
|
|
234
|
+
`driven/pg/adapters/`, `driven/http/adapters/`, `driving/http/controllers/`. Replacing a
|
|
235
|
+
technology then means adding a folder next to the old one, never touching it. An adapter that calls
|
|
236
|
+
another bounded context in the same process sits under the name of that context:
|
|
237
|
+
`driven/catalog/adapters/`. Every class in `driven/<technology>/adapters/` extends a port of the
|
|
238
|
+
domain: checked by [`layers/no-portless-adapter`](../rules/layers/no-portless-adapter.md).
|
|
239
|
+
|
|
240
|
+
::: tip
|
|
241
|
+
Inside `domain/` and `application/`, every class extends a building block of `@alveolus/core`:
|
|
242
|
+
there are no free functions, no plain classes and no module state. Checked by
|
|
243
|
+
[`tactical/no-loose-code`](../rules/tactical/no-loose-code.md).
|
|
244
|
+
:::
|
|
245
|
+
|
|
246
|
+
## Folders and file names
|
|
247
|
+
|
|
248
|
+
Each class goes in the folder of its kind, in a file whose name ends with that kind. One class per
|
|
249
|
+
file; the types that belong to it, such as its snapshot or its command input, stay in its file.
|
|
250
|
+
Checked by [`tactical/no-misplaced-class`](../rules/tactical/no-misplaced-class.md).
|
|
251
|
+
|
|
252
|
+
### In `domain/`
|
|
253
|
+
|
|
254
|
+
| Kind | Extends | Folder | File name |
|
|
255
|
+
| --- | --- | --- | --- |
|
|
256
|
+
| Aggregate | `AggregateRoot` | `aggregates/` | `order.aggregate.ts` |
|
|
257
|
+
| Entity | `Entity` | `entities/` | `order-line.entity.ts` |
|
|
258
|
+
| Value object | `ValueObject` | `value-objects/` | `money.value-object.ts` |
|
|
259
|
+
| Identifier | `Identifier` | `value-objects/` | `order-id.identifier.ts` |
|
|
260
|
+
| Domain event | `DomainEvent` | `events/` | `order-placed.event.ts` |
|
|
261
|
+
| Domain error | `DomainError` | `errors/` | `invalid-total.error.ts` |
|
|
262
|
+
| Domain service | `DomainService` | `services/` | `shipping-cost.service.ts` |
|
|
263
|
+
| Repository | `CommandRepository`<br>`QueryRepository` | `repositories/` | `orders.repository.ts` |
|
|
264
|
+
| Port | `Port` | `ports/` | `price-list.port.ts` |
|
|
265
|
+
| View | `View<…>` type | `views/` | `order-summary.view.ts` |
|
|
266
|
+
|
|
267
|
+
### In `application/`
|
|
268
|
+
|
|
269
|
+
| Kind | Extends | Folder | File name |
|
|
270
|
+
| --- | --- | --- | --- |
|
|
271
|
+
| Command handler | `CommandHandler` | `commands/` | `place-order.command.ts` |
|
|
272
|
+
| Query handler | `QueryHandler` | `queries/` | `get-order-summary.query.ts` |
|
|
273
|
+
| Event translator | `EventTranslator` | `translators/` | `order-events.translator.ts` |
|
|
274
|
+
|
|
275
|
+
### Around them
|
|
276
|
+
|
|
277
|
+
| Kind | Folder | File name |
|
|
278
|
+
| --- | --- | --- |
|
|
279
|
+
| Representation | `published-language/` | `order-placed.representation.ts` |
|
|
280
|
+
| Driven adapter | `driven/pg/adapters/` | `pg-orders.adapter.ts` |
|
|
281
|
+
| Open host service | `driving/<technology>/` | free |
|
|
282
|
+
|
|
283
|
+
A representation is a `PublishedLanguage<…>` type, a driven adapter extends a port, an open host
|
|
284
|
+
service implements `OpenHostService`.
|
|
285
|
+
|
|
286
|
+
Tests sit next to the code they test and keep its name: `order.aggregate.spec.ts` or
|
|
287
|
+
`order.aggregate.test.ts`.
|
|
288
|
+
|
|
289
|
+
## Composition root
|
|
290
|
+
|
|
291
|
+
Each bounded context has one file at its root that wires its adapters into its use cases, its
|
|
292
|
+
module: `ordering.module.ts`. A second one is reported by
|
|
293
|
+
[`layers/no-outward-import`](../rules/layers/no-outward-import.md), and it re-exports nothing:
|
|
294
|
+
see [`strategic/no-cross-context-import`](../rules/strategic/no-cross-context-import.md). It is a class that builds everything with `new`, or the module of your
|
|
295
|
+
framework's container, such as a NestJS `@Module`: see [Integrations](../integrations/index.md).
|
|
296
|
+
|
|
297
|
+
::: tip
|
|
298
|
+
It is the only file that sees every layer, and the only one, besides an anti-corruption layer, that
|
|
299
|
+
may import from another context: another context's module, to reach its open host services.
|
|
300
|
+
:::
|
|
301
|
+
|
|
302
|
+
At the root of `src/`, `main.ts` starts the application and `app.module.ts` builds or imports the
|
|
303
|
+
module of each bounded context. These files import composition roots only.
|
|
304
|
+
|
|
305
|
+
## Shared kernel
|
|
306
|
+
|
|
307
|
+
`src/shared-kernel/` holds what every bounded context needs in the same form: value objects such
|
|
308
|
+
as `Money`, ports such as a tracer, and their adapters. It has the same layers as a bounded
|
|
309
|
+
context, possibly grouped by feature (`shared-kernel/time/driven/system/adapters/`). Every context
|
|
310
|
+
may import it; it imports none of them.
|
|
311
|
+
|
|
312
|
+
::: warning Keep it small
|
|
313
|
+
Each change to the shared kernel reaches every context. `Clock` and `IdGenerator` already come
|
|
314
|
+
with `@alveolus/core`; only their adapters live here. An aggregate, a repository or a handler in
|
|
315
|
+
the shared kernel is reported by
|
|
316
|
+
[`strategic/no-fat-shared-kernel`](../rules/strategic/no-fat-shared-kernel.md).
|
|
317
|
+
:::
|
|
318
|
+
|
|
319
|
+
## See also
|
|
320
|
+
|
|
321
|
+
- [Getting started](./getting-started.md), to configure and run the checks
|
|
322
|
+
- [Building blocks](../core/index.md), the classes each folder holds
|
|
323
|
+
- [Integrations](../integrations/index.md), to write the composition root with your framework
|
|
324
|
+
- Rules: [`layers/no-outward-import`](../rules/layers/no-outward-import.md), [`tactical/no-misplaced-class`](../rules/tactical/no-misplaced-class.md), [`strategic/no-cross-context-import`](../rules/strategic/no-cross-context-import.md), [`layers/no-impure-domain`](../rules/layers/no-impure-domain.md), [`tactical/no-loose-code`](../rules/tactical/no-loose-code.md), [`layers/no-portless-adapter`](../rules/layers/no-portless-adapter.md)
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: "What a version number of @alveolus/core and @alveolus/arch promises: what changes in a patch, a minor and a major, and how to update."
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
# Versioning
|
|
6
|
+
|
|
7
|
+
Both packages follow [semantic versioning](https://semver.org/), with the same version number,
|
|
8
|
+
released together. The changelog of each package says what changed and why; read it before you
|
|
9
|
+
update, the way you would read the notes of a linter.
|
|
10
|
+
|
|
11
|
+
## Before 1.0
|
|
12
|
+
|
|
13
|
+
A `0.x` version can change between minors: a rule renamed, a configuration key moved, a message
|
|
14
|
+
reworded. Each change is in the changelog, with what to do. `1.0.0` comes when a project has
|
|
15
|
+
lived on the checks long enough to trust them, on Linux, macOS and Windows.
|
|
16
|
+
|
|
17
|
+
## From 1.0
|
|
18
|
+
|
|
19
|
+
| Change | Version |
|
|
20
|
+
| --- | --- |
|
|
21
|
+
| A bug fixed, a message reworded, a page corrected | patch |
|
|
22
|
+
| A new rule, **reported as an error from its first version** | minor |
|
|
23
|
+
| A new configuration key, a new output format, a new option | minor |
|
|
24
|
+
| A rule that reports more than before, on code it accepted | minor, said in the changelog |
|
|
25
|
+
| A rule renamed or removed, a configuration key renamed or removed | major |
|
|
26
|
+
| A building block whose signature changes, a type removed from `@alveolus/core` | major |
|
|
27
|
+
| A higher Node.js version required | major |
|
|
28
|
+
|
|
29
|
+
A rule keeps its id for as long as it exists: there is no alias. When an id has to change, it
|
|
30
|
+
changes at a major, and the changelog gives the old and the new name.
|
|
31
|
+
|
|
32
|
+
## Update
|
|
33
|
+
|
|
34
|
+
A minor can add a rule that fails your check: that is the point of a rule. Read the changelog,
|
|
35
|
+
run `npx alveolus arch check`, and either fix what it reports, lower the rule to `warn` for a
|
|
36
|
+
while, or record it in the baseline. A major comes with migration notes in its changelog.
|
|
37
|
+
|
|
38
|
+
## See also
|
|
39
|
+
|
|
40
|
+
- [Getting started](./getting-started.md), the configuration and the levels of the rules
|
|
41
|
+
- [The changelog of `@alveolus/arch`](https://github.com/alveolusjs/alveolus/blob/main/packages/arch/CHANGELOG.md)
|
|
42
|
+
and [of `@alveolus/core`](https://github.com/alveolusjs/alveolus/blob/main/packages/core/CHANGELOG.md)
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: "Use Alveolus with any framework: only the composition root of each bounded context knows how its classes are built, by hand or with a DI container."
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
# Integrations
|
|
6
|
+
|
|
7
|
+
Alveolus imposes no framework: only the composition root of each bounded context knows how its
|
|
8
|
+
classes are built, by hand or with a container.
|
|
9
|
+
|
|
10
|
+
<dl class="al-glance">
|
|
11
|
+
<dt>Needs</dt><dd>No bus, no container, no ORM, no decorator</dd>
|
|
12
|
+
<dt>Wired in</dt><dd>The <a href="/guide/project-layout#composition-root">composition root</a>, <code>ordering.module.ts</code></dd>
|
|
13
|
+
<dt>Tokens</dt><dd>The abstract classes of ports and repositories</dd>
|
|
14
|
+
<dt>Fits</dt><dd>Express, Fastify, Hono, plain Node.js, <a href="./nestjs">NestJS</a></dd>
|
|
15
|
+
<dt>Checked by</dt><dd><a href="/rules/layers/no-outward-import"><code>layers/no-outward-import</code></a>, <a href="/rules/layers/no-impure-domain"><code>layers/no-impure-domain</code></a></dd>
|
|
16
|
+
</dl>
|
|
17
|
+
|
|
18
|
+
## Why
|
|
19
|
+
|
|
20
|
+
A framework changes more often than the business rules. When a handler carries its decorators,
|
|
21
|
+
reads the request or imports the database client, moving to another framework, or just upgrading
|
|
22
|
+
it, means touching the use cases, and their tests need the framework to run.
|
|
23
|
+
|
|
24
|
+
::: tip The fix
|
|
25
|
+
The domain and the application import no framework. Every class receives its dependencies in its
|
|
26
|
+
constructor, typed with abstract classes. The framework stays at the edge: in the adapters, and in
|
|
27
|
+
the one file that builds everything.
|
|
28
|
+
:::
|
|
29
|
+
|
|
30
|
+
## How it works
|
|
31
|
+
|
|
32
|
+
The composition root builds the adapters and passes them to the handlers, which only know the
|
|
33
|
+
abstract classes they extend. Controllers, consumers and jobs then call the handlers.
|
|
34
|
+
|
|
35
|
+
<div class="al-diagram">
|
|
36
|
+
<svg viewBox="0 0 680 220" role="img" aria-label="The composition root ordering.module.ts builds the adapters PgOrders, SystemClock and RandomIdGenerator, and passes them to the constructor of PlaceOrderHandler, which knows them only as Orders, Clock and IdGenerator.">
|
|
37
|
+
<defs>
|
|
38
|
+
<marker id="wiring-arrow" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
|
|
39
|
+
<path class="arrow" d="M 0 0 L 10 5 L 0 10 z" />
|
|
40
|
+
</marker>
|
|
41
|
+
</defs>
|
|
42
|
+
<rect class="boundary" x="8" y="82" width="170" height="56" rx="8" />
|
|
43
|
+
<text class="label" x="93" y="106" text-anchor="middle">ordering.module.ts</text>
|
|
44
|
+
<text class="note" x="93" y="126" text-anchor="middle">composition root</text>
|
|
45
|
+
<rect class="box" x="250" y="16" width="190" height="56" rx="8" />
|
|
46
|
+
<text class="label" x="345" y="40" text-anchor="middle">PgOrders</text>
|
|
47
|
+
<text class="note" x="345" y="60" text-anchor="middle">extends Orders</text>
|
|
48
|
+
<rect class="box" x="250" y="82" width="190" height="56" rx="8" />
|
|
49
|
+
<text class="label" x="345" y="106" text-anchor="middle">SystemClock</text>
|
|
50
|
+
<text class="note" x="345" y="126" text-anchor="middle">extends Clock</text>
|
|
51
|
+
<rect class="box" x="250" y="148" width="190" height="56" rx="8" />
|
|
52
|
+
<text class="label" x="345" y="172" text-anchor="middle">RandomIdGenerator</text>
|
|
53
|
+
<text class="note" x="345" y="192" text-anchor="middle">extends IdGenerator</text>
|
|
54
|
+
<rect class="box" x="500" y="82" width="172" height="56" rx="8" />
|
|
55
|
+
<text class="label" x="586" y="106" text-anchor="middle">PlaceOrderHandler</text>
|
|
56
|
+
<text class="note" x="586" y="126" text-anchor="middle">knows the abstractions</text>
|
|
57
|
+
<path class="link" d="M 178 110 L 248 44" marker-end="url(#wiring-arrow)" />
|
|
58
|
+
<path class="link" d="M 178 110 L 248 110" marker-end="url(#wiring-arrow)" />
|
|
59
|
+
<path class="link" d="M 178 110 L 248 176" marker-end="url(#wiring-arrow)" />
|
|
60
|
+
<text class="note" x="213" y="100" text-anchor="middle">new</text>
|
|
61
|
+
<path class="link" d="M 440 44 L 498 106" marker-end="url(#wiring-arrow)" />
|
|
62
|
+
<path class="link" d="M 440 110 L 498 110" marker-end="url(#wiring-arrow)" />
|
|
63
|
+
<path class="link" d="M 440 176 L 498 114" marker-end="url(#wiring-arrow)" />
|
|
64
|
+
</svg>
|
|
65
|
+
</div>
|
|
66
|
+
|
|
67
|
+
There are two ways to write the composition root:
|
|
68
|
+
|
|
69
|
+
<div class="al-cards al-cards-2">
|
|
70
|
+
<div class="al-card"><span class="al-card-title"><a href="#without-a-container">Without a container</a></span>With Express, Fastify, Hono or plain Node.js: a class builds everything with <code>new</code>.</div>
|
|
71
|
+
<div class="al-card"><span class="al-card-title"><a href="#with-a-container">With a container</a></span>With NestJS or another container: each adapter is registered under the abstract class it extends.</div>
|
|
72
|
+
</div>
|
|
73
|
+
|
|
74
|
+
## Without a container
|
|
75
|
+
|
|
76
|
+
The composition root is a plain class. Its constructor receives what comes from outside, such as
|
|
77
|
+
the database pool, and builds each handler with its adapters.
|
|
78
|
+
|
|
79
|
+
```ts [src/ordering/ordering.module.ts]
|
|
80
|
+
export class OrderingModule {
|
|
81
|
+
readonly placeOrder: PlaceOrderHandler;
|
|
82
|
+
|
|
83
|
+
constructor(db: Pool) {
|
|
84
|
+
this.placeOrder = new PlaceOrderHandler(
|
|
85
|
+
new PgOrders(db),
|
|
86
|
+
new SystemClock(),
|
|
87
|
+
new RandomIdGenerator(),
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
Routes, consumers and jobs then call `handle` and turn the `Result` into a response: see
|
|
94
|
+
[Command handlers](../core/application/command-handlers.md).
|
|
95
|
+
|
|
96
|
+
## With a container
|
|
97
|
+
|
|
98
|
+
The abstract classes of your ports and repositories are the injection tokens: register each
|
|
99
|
+
adapter under the class it extends, and the container passes it to every handler that asks for it.
|
|
100
|
+
No token constant, no string key.
|
|
101
|
+
|
|
102
|
+
::: tip
|
|
103
|
+
The [NestJS](./nestjs.md) guide shows the providers, the choice between factories and
|
|
104
|
+
`@Injectable()`, and how two modules connect.
|
|
105
|
+
:::
|
|
106
|
+
|
|
107
|
+
## See also
|
|
108
|
+
|
|
109
|
+
- [NestJS](./nestjs.md), the composition root as a NestJS module
|
|
110
|
+
- [Project layout](../guide/project-layout.md#composition-root), where the composition root lives
|
|
111
|
+
- [Command handlers](../core/application/command-handlers.md), the classes it builds
|
|
112
|
+
- Rules: [`layers/no-outward-import`](../rules/layers/no-outward-import.md), [`layers/no-impure-domain`](../rules/layers/no-impure-domain.md)
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: "Domain-Driven Design with NestJS: each bounded context is a NestJS module, and the abstract classes of ports and repositories are its injection tokens."
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
# NestJS
|
|
6
|
+
|
|
7
|
+
Each bounded context is a NestJS module, and the abstract classes of ports and repositories are its
|
|
8
|
+
injection tokens.
|
|
9
|
+
|
|
10
|
+
<dl class="al-glance">
|
|
11
|
+
<dt>Bounded context</dt><dd>One <code>@Module</code>, in <code>ordering.module.ts</code></dd>
|
|
12
|
+
<dt>Tokens</dt><dd>The abstract classes of ports and repositories</dd>
|
|
13
|
+
<dt>Handlers</dt><dd><a href="#register-the-handlers">A factory provider</a>, or <a href="#or-use-injectable-in-the-application"><code>@Injectable()</code></a> if you allow it</dd>
|
|
14
|
+
<dt>Exports</dt><dd><a href="#connect-two-bounded-contexts">Open host services</a> only</dd>
|
|
15
|
+
<dt>Checked by</dt><dd><a href="/rules/layers/no-outward-import"><code>layers/no-outward-import</code></a>, <a href="/rules/strategic/no-cross-context-import"><code>strategic/no-cross-context-import</code></a></dd>
|
|
16
|
+
</dl>
|
|
17
|
+
|
|
18
|
+
## Why
|
|
19
|
+
|
|
20
|
+
NestJS invites a decorator on every class. On a handler, `@Injectable()` makes the application
|
|
21
|
+
import `@nestjs/common`: the use cases then depend on the framework, and the build needs
|
|
22
|
+
`emitDecoratorMetadata`.
|
|
23
|
+
|
|
24
|
+
::: tip The fix
|
|
25
|
+
Keep NestJS in the module and the adapters. The module registers each adapter under its abstract
|
|
26
|
+
class and builds each handler with a factory. The application imports nothing from NestJS.
|
|
27
|
+
:::
|
|
28
|
+
|
|
29
|
+
## Design it
|
|
30
|
+
|
|
31
|
+
### Choose how to register the handlers
|
|
32
|
+
|
|
33
|
+
| If you want… | Then register handlers… |
|
|
34
|
+
| --- | --- |
|
|
35
|
+
| an application free of NestJS, built by any tool | with a [**factory**](#register-the-handlers): the default |
|
|
36
|
+
| shorter modules, and accept the framework in the application | as [**`@Injectable()` classes**](#or-use-injectable-in-the-application) |
|
|
37
|
+
|
|
38
|
+
Adapters are outside the application: they may carry `@Injectable()` either way.
|
|
39
|
+
|
|
40
|
+
## Usage
|
|
41
|
+
|
|
42
|
+
### Register the handlers
|
|
43
|
+
|
|
44
|
+
A handler has no `@Injectable()`, so NestJS cannot read its constructor: register it with a
|
|
45
|
+
factory, listing its constructor tokens in order.
|
|
46
|
+
|
|
47
|
+
```ts [src/ordering/ordering.module.ts]
|
|
48
|
+
providers: [
|
|
49
|
+
{ provide: Orders, useClass: PgOrders },
|
|
50
|
+
{ provide: Clock, useClass: SystemClock },
|
|
51
|
+
{ provide: IdGenerator, useClass: RandomIdGenerator },
|
|
52
|
+
{
|
|
53
|
+
inject: [Orders, Clock, IdGenerator],
|
|
54
|
+
provide: PlaceOrderHandler,
|
|
55
|
+
useFactory: (orders: Orders, clock: Clock, ids: IdGenerator) =>
|
|
56
|
+
new PlaceOrderHandler(orders, clock, ids),
|
|
57
|
+
},
|
|
58
|
+
],
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
`PgOrders` is a driven adapter: it may carry `@Injectable()` to receive the database client.
|
|
62
|
+
|
|
63
|
+
### Or use `@Injectable()` in the application
|
|
64
|
+
|
|
65
|
+
To list handlers as plain providers, allow `Injectable`, and only it, in the application:
|
|
66
|
+
|
|
67
|
+
```ts [alveolus.config.ts]
|
|
68
|
+
applicationDependencies: { "@nestjs/common": ["Injectable"] },
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
```ts [src/ordering/application/commands/place-order.command.ts]
|
|
72
|
+
@Injectable()
|
|
73
|
+
export class PlaceOrderHandler extends CommandHandler<
|
|
74
|
+
PlaceOrder,
|
|
75
|
+
void,
|
|
76
|
+
PlaceOrderError
|
|
77
|
+
> { … }
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
```ts [src/ordering/ordering.module.ts]
|
|
81
|
+
providers: [{ provide: Orders, useClass: PgOrders }, PlaceOrderHandler],
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
::: warning Caveats
|
|
85
|
+
- The application then depends on NestJS, and needs `emitDecoratorMetadata`: Vitest and other
|
|
86
|
+
esbuild-based tools need a SWC transform.
|
|
87
|
+
- Import the injected classes as values, not with `import type`: the metadata needs them at
|
|
88
|
+
runtime.
|
|
89
|
+
:::
|
|
90
|
+
|
|
91
|
+
### Connect two bounded contexts
|
|
92
|
+
|
|
93
|
+
So that nothing else of the catalog leaks out, a module exports its
|
|
94
|
+
[open host services](../core/strategic/open-host-services.md), and nothing else. The downstream
|
|
95
|
+
module imports it to build its [anti-corruption layer](../core/strategic/anti-corruption-layers.md).
|
|
96
|
+
|
|
97
|
+
```ts [src/catalog/catalog.module.ts]
|
|
98
|
+
@Module({
|
|
99
|
+
exports: [CatalogApi],
|
|
100
|
+
providers: [CatalogApi, …],
|
|
101
|
+
})
|
|
102
|
+
export class CatalogModule {}
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
```ts [src/ordering/ordering.module.ts]
|
|
106
|
+
@Module({
|
|
107
|
+
imports: [CatalogModule],
|
|
108
|
+
providers: [{ provide: PriceList, useClass: CatalogPriceList }, …],
|
|
109
|
+
})
|
|
110
|
+
export class OrderingModule {}
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
Exporting a repository or a handler would let another context reach the model behind the open host
|
|
114
|
+
service. Checked by [`strategic/no-cross-context-import`](../rules/strategic/no-cross-context-import.md).
|
|
115
|
+
|
|
116
|
+
### Answer with a `Result`
|
|
117
|
+
|
|
118
|
+
So that a controller stays a translation, it calls the handler, and maps the `Result` to a
|
|
119
|
+
response: a success to the status code, a failure to the HTTP error the client can act on. The
|
|
120
|
+
mapping lives once, in an exception filter or a small mapper class of `driving/http/`.
|
|
121
|
+
|
|
122
|
+
```ts [src/ordering/driving/http/orders.controller.ts]
|
|
123
|
+
@Controller("orders")
|
|
124
|
+
export class OrdersController {
|
|
125
|
+
constructor(private readonly placeOrder: PlaceOrderHandler) {}
|
|
126
|
+
|
|
127
|
+
@Post(":id/place")
|
|
128
|
+
async place(@Param("id") id: string): Promise<void> {
|
|
129
|
+
const result = await this.placeOrder.handle({ orderId: id });
|
|
130
|
+
if (!result.ok) {
|
|
131
|
+
throw new HttpFailure(result.error);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
`HttpFailure` is a class of `driving/http/` that turns a `DomainError` into an `HttpException`
|
|
138
|
+
by its `type`: `EmptyOrder` to `422`, `OrderNotFound` to `404`. The domain never knows HTTP.
|
|
139
|
+
|
|
140
|
+
### Run the check with your lint
|
|
141
|
+
|
|
142
|
+
So that a violation is seen before the review, the check runs where the lint runs:
|
|
143
|
+
|
|
144
|
+
```json [package.json]
|
|
145
|
+
{
|
|
146
|
+
"scripts": {
|
|
147
|
+
"lint": "biome check . && alveolus arch check",
|
|
148
|
+
"lint:arch": "alveolus arch check --format sarif > arch.sarif"
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
In CI, `alveolus arch check` fails the job on an error; `--format sarif` and
|
|
154
|
+
`github/codeql-action/upload-sarif` put each violation on the line it concerns in the pull
|
|
155
|
+
request.
|
|
156
|
+
|
|
157
|
+
## Troubleshooting
|
|
158
|
+
|
|
159
|
+
**`Nest can't resolve dependencies of PlaceOrderHandler (?, …)`**: the handler is listed as a plain
|
|
160
|
+
class without `@Injectable()`, an injected class is imported with `import type`, or a token of
|
|
161
|
+
`inject` has no provider.
|
|
162
|
+
|
|
163
|
+
## See also
|
|
164
|
+
|
|
165
|
+
- [Integrations](./index.md), the composition root without a framework
|
|
166
|
+
- [Command handlers](../core/application/command-handlers.md), the classes the module builds
|
|
167
|
+
- [Open host services](../core/strategic/open-host-services.md) and
|
|
168
|
+
[Anti-corruption layers](../core/strategic/anti-corruption-layers.md), to connect two modules
|
|
169
|
+
- Rules: [`layers/no-outward-import`](../rules/layers/no-outward-import.md), [`strategic/no-cross-context-import`](../rules/strategic/no-cross-context-import.md)
|