@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,113 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: "Architecture rule: an aggregate, an entity, a value object or an identifier keeps its state private and exposes it through getters."
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
# no-public-field
|
|
6
|
+
|
|
7
|
+
An aggregate, an entity, a value object or an identifier keeps its state private: nothing outside
|
|
8
|
+
changes it, and what callers need is read through a getter.
|
|
9
|
+
|
|
10
|
+
<dl class="al-glance">
|
|
11
|
+
<dt>Rule</dt><dd><code>tactical/no-public-field</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 instance field, declared or as a constructor parameter, <code>readonly</code> or not</dd>
|
|
14
|
+
<dt>Applies to</dt><dd>Every class that extends <code>AggregateRoot</code>, <code>Entity</code>, <code>ValueObject</code> or <code>Identifier</code>, in core bounded contexts and the shared kernel</dd>
|
|
15
|
+
<dt>Turn off</dt><dd><a href="#turn-it-off"><code>"tactical/no-public-field": "off"</code></a></dd>
|
|
16
|
+
</dl>
|
|
17
|
+
|
|
18
|
+
## Why
|
|
19
|
+
|
|
20
|
+
`Account` has `public balance = 0`. The withdraw handler checks the balance and subtracts the
|
|
21
|
+
amount itself; the next handler does the same with a slightly different check. The rule "an
|
|
22
|
+
account never goes below its overdraft" now lives in four handlers, and the aggregate is a bag of
|
|
23
|
+
fields: the model is anemic, and the day the rule changes, nobody finds every copy.
|
|
24
|
+
|
|
25
|
+
::: tip The fix
|
|
26
|
+
The state is private, and changes through a method that returns a `Result`: `account.withdraw(amount)`.
|
|
27
|
+
A value the outside needs to read is a getter. The rule lives once, where the state is.
|
|
28
|
+
:::
|
|
29
|
+
|
|
30
|
+
## What it checks
|
|
31
|
+
|
|
32
|
+
Every instance field of a class that extends one of the four building blocks:
|
|
33
|
+
|
|
34
|
+
| Field | Allowed |
|
|
35
|
+
| --- | --- |
|
|
36
|
+
| `private balance`, `protected readonly opened` | ✅ |
|
|
37
|
+
| `constructor(private readonly currency: string)` | ✅ |
|
|
38
|
+
| `get balance(): Money` | ✅ |
|
|
39
|
+
| `public static readonly limit = 100` | ✅ |
|
|
40
|
+
| `public balance = 0`, `public readonly currency` | ❌ |
|
|
41
|
+
| `constructor(public readonly owner: string)` | ❌ |
|
|
42
|
+
|
|
43
|
+
A `readonly` public field is reported too: a value object of it can still be mutated, and the
|
|
44
|
+
getter keeps the shape of the class free to change.
|
|
45
|
+
|
|
46
|
+
## What it reports
|
|
47
|
+
|
|
48
|
+
```
|
|
49
|
+
src/ledger/domain/aggregates/account.aggregate.ts
|
|
50
|
+
4 error tactical/no-public-field: Account.balance is a public field:
|
|
51
|
+
keep the state private, and expose what callers need through a
|
|
52
|
+
getter.
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## Fix it
|
|
56
|
+
|
|
57
|
+
### Change the state through a method
|
|
58
|
+
|
|
59
|
+
<div class="al-compare">
|
|
60
|
+
|
|
61
|
+
```ts [❌ Avoid: src/ledger/domain/aggregates/account.aggregate.ts]
|
|
62
|
+
export class Account extends AggregateRoot<AccountId> {
|
|
63
|
+
public balance = 0;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// in a handler
|
|
67
|
+
if (account.balance >= amount) {
|
|
68
|
+
account.balance -= amount;
|
|
69
|
+
}
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
```ts [✅ Prefer: src/ledger/domain/aggregates/account.aggregate.ts]
|
|
73
|
+
export class Account extends AggregateRoot<AccountId> {
|
|
74
|
+
private balance = 0;
|
|
75
|
+
|
|
76
|
+
get currentBalance(): number {
|
|
77
|
+
return this.balance;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
withdraw(amount: number): Result<void, InsufficientFunds> {
|
|
81
|
+
if (this.balance < amount) {
|
|
82
|
+
return err(new InsufficientFunds({ amount }));
|
|
83
|
+
}
|
|
84
|
+
this.balance -= amount;
|
|
85
|
+
return ok();
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
</div>
|
|
91
|
+
|
|
92
|
+
## Limits
|
|
93
|
+
|
|
94
|
+
::: warning What the rule cannot see
|
|
95
|
+
- A getter that returns a mutable object, such as the array of lines: a caller can push into it.
|
|
96
|
+
Return a copy, or a readonly type.
|
|
97
|
+
- A `protected` field: a subclass may change it. The rule stops the outside, not the hierarchy.
|
|
98
|
+
:::
|
|
99
|
+
|
|
100
|
+
## Turn it off
|
|
101
|
+
|
|
102
|
+
```ts [alveolus.config.ts]
|
|
103
|
+
rules: { "tactical/no-public-field": "off" },
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
On an existing project, prefer a [baseline](../../guide/getting-started.md#adopt-it-on-an-existing-project):
|
|
107
|
+
new fields stay private while you move the rules back into the aggregates.
|
|
108
|
+
|
|
109
|
+
## See also
|
|
110
|
+
|
|
111
|
+
- [Aggregates](../../core/domain/aggregates.md) and [Entities](../../core/domain/entities.md), what is checked
|
|
112
|
+
- [`tactical/no-thrown-failure`](./no-thrown-failure.md), which makes every change return a `Result`
|
|
113
|
+
- [Rules](../index.md), every rule by category
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: "Architecture rule for domain services: a domain service is stateless and holds configuration only, never a port, a repository or another service."
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
# no-stateful-service
|
|
6
|
+
|
|
7
|
+
A domain service holds configuration only: the command handler passes it what it needs.
|
|
8
|
+
|
|
9
|
+
<dl class="al-glance">
|
|
10
|
+
<dt>Rule</dt><dd><code>tactical/no-stateful-service</code></dd>
|
|
11
|
+
<dt>Category</dt><dd><a href="/rules/#tactical">Tactical</a>: how building blocks are written</dd>
|
|
12
|
+
<dt>Reports</dt><dd>A domain service that holds a port, a repository, another service or any class other than a value object</dd>
|
|
13
|
+
<dt>Applies to</dt><dd>The constructor parameters and fields of every <code>DomainService</code> of a core bounded context or the shared kernel</dd>
|
|
14
|
+
<dt>Turn off</dt><dd><a href="#turn-it-off"><code>"tactical/no-stateful-service": "off"</code></a></dd>
|
|
15
|
+
</dl>
|
|
16
|
+
|
|
17
|
+
## Why
|
|
18
|
+
|
|
19
|
+
`OrderLimit` receives `Orders` to count the orders of a customer. The rule now loads data on its
|
|
20
|
+
own, a query handler that uses it can write, and testing it needs a repository. A domain service
|
|
21
|
+
is a stateless operation: it is given the aggregates and values it works on.
|
|
22
|
+
|
|
23
|
+
::: tip The fix
|
|
24
|
+
The command handler loads what the rule needs and passes it to the service method. The service
|
|
25
|
+
keeps only its configuration, such as a limit or a rate.
|
|
26
|
+
:::
|
|
27
|
+
|
|
28
|
+
## What it checks
|
|
29
|
+
|
|
30
|
+
Every constructor parameter and every field of each `DomainService`:
|
|
31
|
+
|
|
32
|
+
| Holds | Allowed |
|
|
33
|
+
| --- | --- |
|
|
34
|
+
| A plain value such as a `number` or a `string` | ✅ |
|
|
35
|
+
| A value object, an identifier | ✅ |
|
|
36
|
+
| A class of a package listed in `domainDependencies` | ✅ |
|
|
37
|
+
| A `Port`, a `CommandRepository`, a `QueryRepository` | ❌ |
|
|
38
|
+
| Another `DomainService`, an aggregate, an entity, a handler | ❌ |
|
|
39
|
+
| Any other class of the project | ❌ |
|
|
40
|
+
|
|
41
|
+
## What it reports
|
|
42
|
+
|
|
43
|
+
```
|
|
44
|
+
src/ordering/domain/services/order-limit.service.ts
|
|
45
|
+
4 error tactical/no-stateful-service: The DomainService OrderLimit holds
|
|
46
|
+
Orders, a CommandRepository: a domain service holds configuration
|
|
47
|
+
only; the command handler passes it what it needs.
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## Fix it
|
|
51
|
+
|
|
52
|
+
### Pass what the rule needs to the method
|
|
53
|
+
|
|
54
|
+
So that the service stays a pure operation, the command handler loads the data and passes it in.
|
|
55
|
+
|
|
56
|
+
<div class="al-compare">
|
|
57
|
+
|
|
58
|
+
```ts [❌ Avoid: src/ordering/domain/services/order-limit.service.ts]
|
|
59
|
+
export class OrderLimit extends DomainService {
|
|
60
|
+
constructor(private readonly orders: Orders) {
|
|
61
|
+
super();
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
```ts [✅ Prefer: src/ordering/domain/services/order-limit.service.ts]
|
|
67
|
+
export class OrderLimit extends DomainService {
|
|
68
|
+
constructor(private readonly maxLinesForNewCustomers: number) {
|
|
69
|
+
super();
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
check(order: Order, customer: Customer): Result<void, OrderTooLarge> {
|
|
73
|
+
// …
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
</div>
|
|
79
|
+
|
|
80
|
+
## Limits
|
|
81
|
+
|
|
82
|
+
::: warning What the rule cannot see
|
|
83
|
+
- A port passed to a method of the service, call after call: the rule checks what the service
|
|
84
|
+
holds, not what it receives. In review, a domain service method takes aggregates and values.
|
|
85
|
+
- State reached through a module: the service has no field, but reads a constant that holds a
|
|
86
|
+
connection. `tactical/no-loose-code` reports the constant; the rule does not see the read.
|
|
87
|
+
:::
|
|
88
|
+
|
|
89
|
+
## Turn it off
|
|
90
|
+
|
|
91
|
+
```ts [alveolus.config.ts]
|
|
92
|
+
rules: { "tactical/no-stateful-service": "off" },
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
On an existing project, prefer a [baseline](../../guide/getting-started.md#adopt-it-on-an-existing-project):
|
|
96
|
+
new services stay stateless while you rework the old ones.
|
|
97
|
+
|
|
98
|
+
## See also
|
|
99
|
+
|
|
100
|
+
- [Domain services](../../core/domain/domain-services.md), what is checked
|
|
101
|
+
- [Command handlers](../../core/application/command-handlers.md), which load what a service needs
|
|
102
|
+
- [Rules](../index.md), every rule by category
|
|
@@ -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.3.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
|
}
|