@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.
- package/README.md +11 -0
- package/dist/bin.mjs +4 -2
- package/dist/bin.mjs.map +1 -1
- package/dist/{cli-P5PwH9OE.mjs → docs-DcFgskuN.mjs} +214 -43
- package/dist/docs-DcFgskuN.mjs.map +1 -0
- package/dist/index.d.mts +53 -12
- 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 +108 -0
- package/docs/guide/getting-started.md +286 -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 +185 -0
- package/docs/rules/layers/no-driving-shortcut.md +119 -0
- package/docs/rules/layers/no-impure-domain.md +191 -0
- package/docs/rules/layers/no-outward-import.md +186 -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 +114 -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 +201 -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-P5PwH9OE.mjs.map +0 -1
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: "Architecture rule: a business failure is returned in a Result, the domain and the application never throw, and exceptions stay in adapters."
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
# no-thrown-failure
|
|
6
|
+
|
|
7
|
+
A business failure is a value: the aggregate returns it in a `Result`. The domain and the
|
|
8
|
+
application never throw; exceptions stay in adapters, for technical failures.
|
|
9
|
+
|
|
10
|
+
<dl class="al-glance">
|
|
11
|
+
<dt>Rule</dt><dd><code>tactical/no-thrown-failure</code></dd>
|
|
12
|
+
<dt>Category</dt><dd><a href="/rules/#tactical">Tactical</a>: how building blocks are written</dd>
|
|
13
|
+
<dt>Reports</dt><dd>A public method or function property of an aggregate or entity that returns no <code>Result</code>, a public setter, any <code>throw</code> or <code>Promise.reject</code> in the domain or the application</dd>
|
|
14
|
+
<dt>Applies to</dt><dd>Classes that extend <code>AggregateRoot</code> or <code>Entity</code>; every file in <code>domain/</code> and <code>application/</code>; in core bounded contexts and the shared kernel</dd>
|
|
15
|
+
<dt>Turn off</dt><dd><a href="#turn-it-off"><code>"tactical/no-thrown-failure": "off"</code></a></dd>
|
|
16
|
+
</dl>
|
|
17
|
+
|
|
18
|
+
## Why
|
|
19
|
+
|
|
20
|
+
`order.place()` throws `InvalidTotal` when the total is zero. Nothing in its signature says so: the
|
|
21
|
+
controller that calls it does not catch it, and a customer gets an error 500 for a mistake they
|
|
22
|
+
could have fixed.
|
|
23
|
+
|
|
24
|
+
::: tip The fix
|
|
25
|
+
The method returns `Result<void, InvalidTotal>`. The failure is in the type, and TypeScript makes
|
|
26
|
+
every caller deal with it before it reaches the value. A method that changes state says whether it
|
|
27
|
+
worked; reads are getters.
|
|
28
|
+
:::
|
|
29
|
+
|
|
30
|
+
## What it checks
|
|
31
|
+
|
|
32
|
+
<div class="al-cards">
|
|
33
|
+
<div class="al-card"><span class="al-card-title"><span class="al-card-step">1</span>Public methods return a Result</span>Every public method of a class that extends <code>AggregateRoot</code> or <code>Entity</code>, and every public property holding a function, such as <code>place = () => …</code>, even when its return type is inferred. A <code>Promise</code> of a <code>Result</code> counts. Getters, static methods, <code>toSnapshot</code>, <code>equals</code> and the protocol methods <code>toString</code>, <code>toJSON</code>, <code>valueOf</code> and <code>[Symbol.…]</code> are left out.</div>
|
|
34
|
+
<div class="al-card"><span class="al-card-title"><span class="al-card-step">2</span>No setter</span>A public setter changes the state without saying whether it worked: a business method does it instead.</div>
|
|
35
|
+
<div class="al-card"><span class="al-card-title"><span class="al-card-step">3</span>Nothing is thrown</span>No <code>throw</code> and no <code>Promise.reject</code> in <code>domain/</code> or <code>application/</code>, whatever is thrown: an <code>Error</code>, a domain error or an <code>unknown</code>. Adapters may throw on a technical failure, such as a lost connection.</div>
|
|
36
|
+
</div>
|
|
37
|
+
|
|
38
|
+
## What it reports
|
|
39
|
+
|
|
40
|
+
```
|
|
41
|
+
src/ordering/domain/aggregates/order.aggregate.ts
|
|
42
|
+
2 error tactical/no-thrown-failure: Order.place must return a Result:
|
|
43
|
+
expose reads as getters and return business failures as
|
|
44
|
+
values.
|
|
45
|
+
4 error tactical/no-thrown-failure: A failure is thrown: return it in a
|
|
46
|
+
Result instead.
|
|
47
|
+
9 error tactical/no-thrown-failure: Order.status is a setter: change the
|
|
48
|
+
state through a business method that returns a Result.
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## Fix it
|
|
52
|
+
|
|
53
|
+
### Return the failure in a Result
|
|
54
|
+
|
|
55
|
+
So that the caller sees what can go wrong, return the domain error with `err(…)` and success with
|
|
56
|
+
`ok()`, and declare both in the return type.
|
|
57
|
+
|
|
58
|
+
<div class="al-compare">
|
|
59
|
+
|
|
60
|
+
```ts [❌ Avoid: src/ordering/domain/aggregates/order.aggregate.ts]
|
|
61
|
+
export class Order extends AggregateRoot<OrderId> {
|
|
62
|
+
place(total: number): void {
|
|
63
|
+
if (total <= 0) {
|
|
64
|
+
throw new InvalidTotal({ total });
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
```ts [✅ Prefer: src/ordering/domain/aggregates/order.aggregate.ts]
|
|
71
|
+
export class Order extends AggregateRoot<OrderId> {
|
|
72
|
+
place(total: number): Result<void, InvalidTotal> {
|
|
73
|
+
if (total <= 0) {
|
|
74
|
+
return err(new InvalidTotal({ total }));
|
|
75
|
+
}
|
|
76
|
+
return ok();
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
</div>
|
|
82
|
+
|
|
83
|
+
### Expose reads as getters
|
|
84
|
+
|
|
85
|
+
So that the public methods are the ones that change state, a read without parameters is a getter,
|
|
86
|
+
which the rule leaves out.
|
|
87
|
+
|
|
88
|
+
<div class="al-compare">
|
|
89
|
+
|
|
90
|
+
```ts [❌ Avoid: src/ordering/domain/aggregates/order.aggregate.ts]
|
|
91
|
+
isPlaced(): boolean {
|
|
92
|
+
return this.status === "placed";
|
|
93
|
+
}
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
```ts [✅ Prefer: src/ordering/domain/aggregates/order.aggregate.ts]
|
|
97
|
+
get isPlaced(): boolean {
|
|
98
|
+
return this.status === "placed";
|
|
99
|
+
}
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
</div>
|
|
103
|
+
|
|
104
|
+
A read that needs parameters, such as `canShip(date)`, returns `ok(…)`, or moves to a
|
|
105
|
+
[`DomainService`](../../core/domain/domain-services.md) when it involves more than the aggregate.
|
|
106
|
+
|
|
107
|
+
### Make the impossible state unrepresentable
|
|
108
|
+
|
|
109
|
+
So that a guard against an impossible state needs no `throw`, keep the constructor private and
|
|
110
|
+
build through a static factory that returns a `Result`: an invalid value never exists.
|
|
111
|
+
|
|
112
|
+
<div class="al-compare">
|
|
113
|
+
|
|
114
|
+
```ts [❌ Avoid: src/ordering/domain/value-objects/quantity.value-object.ts]
|
|
115
|
+
export class Quantity extends ValueObject<{ value: number }> {
|
|
116
|
+
constructor(value: number) {
|
|
117
|
+
if (value <= 0) {
|
|
118
|
+
throw new Error("A quantity is positive.");
|
|
119
|
+
}
|
|
120
|
+
super({ value });
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
```ts [✅ Prefer: src/ordering/domain/value-objects/quantity.value-object.ts]
|
|
126
|
+
export class Quantity extends ValueObject<{ value: number }> {
|
|
127
|
+
private constructor(value: number) {
|
|
128
|
+
super({ value });
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
static of(value: number): Result<Quantity, InvalidQuantity> {
|
|
132
|
+
return value > 0 ? ok(new Quantity(value)) : err(new InvalidQuantity({ value }));
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
</div>
|
|
138
|
+
|
|
139
|
+
## Limits
|
|
140
|
+
|
|
141
|
+
::: warning What the rule cannot see
|
|
142
|
+
- A promise rejected from its executor, `new Promise((_, reject) => reject(…))`, is not seen: only
|
|
143
|
+
`throw` and `Promise.reject` are.
|
|
144
|
+
- A function of a package that throws is not seen either: the rule reads your code, not what it
|
|
145
|
+
calls. Wrap such a call in a `Result` where it happens.
|
|
146
|
+
:::
|
|
147
|
+
|
|
148
|
+
## Turn it off
|
|
149
|
+
|
|
150
|
+
```ts [alveolus.config.ts]
|
|
151
|
+
rules: { "tactical/no-thrown-failure": "off" },
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
On an existing project, prefer a [baseline](../../guide/getting-started.md#adopt-it-on-an-existing-project):
|
|
155
|
+
new methods return a `Result` while you convert the old ones.
|
|
156
|
+
|
|
157
|
+
## See also
|
|
158
|
+
|
|
159
|
+
- [Result](../../core/utilities/result.md) and [Domain errors](../../core/domain/domain-errors.md), what the methods return
|
|
160
|
+
- [Aggregates](../../core/domain/aggregates.md), whose business methods this rule checks
|
|
161
|
+
- [`tactical/no-loose-code`](./no-loose-code.md), which keeps `extends Error` out of the domain
|
|
162
|
+
- [Rules](../index.md), every rule by category
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: "Architecture rule: a disable comment names a rule and gives a reason, and is removed once the line below breaks the rule no more."
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
# no-loose-disable
|
|
6
|
+
|
|
7
|
+
A disable comment turns one violation off, says which rule and why, and goes away with the
|
|
8
|
+
violation.
|
|
9
|
+
|
|
10
|
+
<dl class="al-glance">
|
|
11
|
+
<dt>Rule</dt><dd><code>tooling/no-loose-disable</code></dd>
|
|
12
|
+
<dt>Category</dt><dd><a href="/rules/#tooling">Tooling</a>: how the checks themselves are used</dd>
|
|
13
|
+
<dt>Reports</dt><dd>A <code>// alveolus-disable-next-line</code> comment that names no known rule, gives no reason, or disables nothing</dd>
|
|
14
|
+
<dt>Applies to</dt><dd>Every analysed file</dd>
|
|
15
|
+
<dt>Turn off</dt><dd><a href="#turn-it-off"><code>"tooling/no-loose-disable": "off"</code></a></dd>
|
|
16
|
+
</dl>
|
|
17
|
+
|
|
18
|
+
## Why
|
|
19
|
+
|
|
20
|
+
A violation is sometimes right to keep for a while: a legacy import that a ticket will remove, a
|
|
21
|
+
rule that does not fit one file yet. A comment above the line turns it off, where a reviewer sees
|
|
22
|
+
it. Without a rule and a reason, the comment says nothing, and once the line is fixed it stays
|
|
23
|
+
behind and hides the next violation.
|
|
24
|
+
|
|
25
|
+
::: tip The fix
|
|
26
|
+
Write the rule and the reason: `// alveolus-disable-next-line layers/no-impure-domain: legacy pool, ORD-412`.
|
|
27
|
+
When the line breaks the rule no more, the comment is reported until it is removed.
|
|
28
|
+
:::
|
|
29
|
+
|
|
30
|
+
## What it checks
|
|
31
|
+
|
|
32
|
+
Every `// alveolus-disable-next-line` comment:
|
|
33
|
+
|
|
34
|
+
<div class="al-cards al-cards-2">
|
|
35
|
+
<div class="al-card"><span class="al-card-title"><span class="al-card-step">1</span>Names a rule</span>The id of a rule, as the rules page lists it. One rule per comment.</div>
|
|
36
|
+
<div class="al-card"><span class="al-card-title"><span class="al-card-step">2</span>Gives a reason</span>After a colon: why this line keeps its violation. Free text, read in review.</div>
|
|
37
|
+
<div class="al-card"><span class="al-card-title"><span class="al-card-step">3</span>Turns something off</span>The line below breaks that rule. A comment that disables nothing is reported.</div>
|
|
38
|
+
<div class="al-card"><span class="al-card-title"><span class="al-card-step">4</span>Is counted</span>The summary says how many violations are disabled, and <code>--format json</code> lists them with their reason.</div>
|
|
39
|
+
</div>
|
|
40
|
+
|
|
41
|
+
A comment that names no rule, an unknown rule, or no reason disables nothing: the violation below
|
|
42
|
+
is reported as well.
|
|
43
|
+
|
|
44
|
+
## What it reports
|
|
45
|
+
|
|
46
|
+
```
|
|
47
|
+
src/ordering/domain/services/pricing.service.ts
|
|
48
|
+
1 error tooling/no-loose-disable: The disable comment gives no reason:
|
|
49
|
+
write `// alveolus-disable-next-line layers/no-impure-domain:
|
|
50
|
+
<why this line keeps its violation>`.
|
|
51
|
+
4 error tooling/no-loose-disable: The disable comment disables nothing:
|
|
52
|
+
the line below breaks layers/no-impure-domain no more; remove
|
|
53
|
+
the comment.
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## Fix it
|
|
57
|
+
|
|
58
|
+
### Say which rule, and why
|
|
59
|
+
|
|
60
|
+
<div class="al-compare">
|
|
61
|
+
|
|
62
|
+
```ts [❌ Avoid: src/ordering/domain/services/pricing.service.ts]
|
|
63
|
+
// alveolus-disable-next-line
|
|
64
|
+
import { Pool } from "pg";
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
```ts [✅ Prefer: src/ordering/domain/services/pricing.service.ts]
|
|
68
|
+
// alveolus-disable-next-line layers/no-impure-domain: legacy pool, removed with ORD-412
|
|
69
|
+
import { Pool } from "pg";
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
</div>
|
|
73
|
+
|
|
74
|
+
### Remove the comment with the violation
|
|
75
|
+
|
|
76
|
+
Once the line is fixed, the comment is reported: delete it. A disable comment never outlives what
|
|
77
|
+
it disables.
|
|
78
|
+
|
|
79
|
+
## Limits
|
|
80
|
+
|
|
81
|
+
::: warning What the rule cannot see
|
|
82
|
+
- Whether the reason is a good one: `: because` passes. The reason is for the reviewer.
|
|
83
|
+
- A violation turned off is still a violation: for a whole file or a whole rule, prefer `ignore`
|
|
84
|
+
or `rules` in `alveolus.config.ts`, and for the past, the baseline.
|
|
85
|
+
:::
|
|
86
|
+
|
|
87
|
+
## Turn it off
|
|
88
|
+
|
|
89
|
+
```ts [alveolus.config.ts]
|
|
90
|
+
rules: { "tooling/no-loose-disable": "off" },
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
Disable comments then still work, but nothing checks them.
|
|
94
|
+
|
|
95
|
+
## See also
|
|
96
|
+
|
|
97
|
+
- [Getting started: turn a violation off](../../guide/getting-started.md#turn-a-violation-off)
|
|
98
|
+
- [Rules](../index.md), every rule by category
|
package/package.json
CHANGED
|
@@ -22,7 +22,8 @@
|
|
|
22
22
|
"./package.json": "./package.json"
|
|
23
23
|
},
|
|
24
24
|
"files": [
|
|
25
|
-
"dist"
|
|
25
|
+
"dist",
|
|
26
|
+
"docs"
|
|
26
27
|
],
|
|
27
28
|
"homepage": "https://alveolus.dev/",
|
|
28
29
|
"keywords": [
|
|
@@ -47,9 +48,9 @@
|
|
|
47
48
|
},
|
|
48
49
|
"sideEffects": false,
|
|
49
50
|
"type": "module",
|
|
50
|
-
"version": "0.
|
|
51
|
+
"version": "0.4.0",
|
|
51
52
|
"scripts": {
|
|
52
|
-
"build": "tsdown",
|
|
53
|
+
"build": "tsdown && biome check --write package.json",
|
|
53
54
|
"typecheck": "tsc -p tsconfig.json"
|
|
54
55
|
}
|
|
55
56
|
}
|