@alveolus/arch 0.2.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. package/README.md +11 -0
  2. package/dist/bin.mjs +4 -2
  3. package/dist/bin.mjs.map +1 -1
  4. package/dist/{cli-P5PwH9OE.mjs → docs-DcFgskuN.mjs} +214 -43
  5. package/dist/docs-DcFgskuN.mjs.map +1 -0
  6. package/dist/index.d.mts +53 -12
  7. package/dist/index.d.mts.map +1 -1
  8. package/dist/index.mjs +2 -2
  9. package/docs/core/application/command-handlers.md +617 -0
  10. package/docs/core/application/event-publishers.md +234 -0
  11. package/docs/core/application/event-translators.md +329 -0
  12. package/docs/core/application/index.md +99 -0
  13. package/docs/core/application/integration-events.md +277 -0
  14. package/docs/core/application/outbox.md +416 -0
  15. package/docs/core/application/query-handlers.md +292 -0
  16. package/docs/core/application/unit-of-work.md +352 -0
  17. package/docs/core/domain/aggregates.md +822 -0
  18. package/docs/core/domain/domain-errors.md +251 -0
  19. package/docs/core/domain/domain-events.md +292 -0
  20. package/docs/core/domain/domain-services.md +249 -0
  21. package/docs/core/domain/entities.md +431 -0
  22. package/docs/core/domain/index.md +93 -0
  23. package/docs/core/domain/ports.md +284 -0
  24. package/docs/core/domain/repositories.md +335 -0
  25. package/docs/core/domain/value-objects.md +425 -0
  26. package/docs/core/domain/views.md +265 -0
  27. package/docs/core/index.md +108 -0
  28. package/docs/core/strategic/anti-corruption-layers.md +349 -0
  29. package/docs/core/strategic/index.md +83 -0
  30. package/docs/core/strategic/open-host-services.md +287 -0
  31. package/docs/core/strategic/published-language.md +265 -0
  32. package/docs/core/utilities/result.md +413 -0
  33. package/docs/guide/agents.md +68 -0
  34. package/docs/guide/existing-project.md +108 -0
  35. package/docs/guide/getting-started.md +286 -0
  36. package/docs/guide/learning-path.md +123 -0
  37. package/docs/guide/project-layout.md +324 -0
  38. package/docs/guide/versioning.md +42 -0
  39. package/docs/integrations/index.md +112 -0
  40. package/docs/integrations/nestjs.md +169 -0
  41. package/docs/rules/index.md +185 -0
  42. package/docs/rules/layers/no-driving-shortcut.md +119 -0
  43. package/docs/rules/layers/no-impure-domain.md +191 -0
  44. package/docs/rules/layers/no-outward-import.md +186 -0
  45. package/docs/rules/layers/no-portless-adapter.md +123 -0
  46. package/docs/rules/strategic/no-cross-context-import.md +140 -0
  47. package/docs/rules/strategic/no-fat-shared-kernel.md +81 -0
  48. package/docs/rules/strategic/no-leaky-host-service.md +107 -0
  49. package/docs/rules/strategic/no-unmapped-context.md +114 -0
  50. package/docs/rules/tactical/no-aggregate-reference.md +139 -0
  51. package/docs/rules/tactical/no-foreign-command-dependency.md +119 -0
  52. package/docs/rules/tactical/no-foreign-query-dependency.md +201 -0
  53. package/docs/rules/tactical/no-loose-code.md +171 -0
  54. package/docs/rules/tactical/no-misplaced-class.md +146 -0
  55. package/docs/rules/tactical/no-public-field.md +113 -0
  56. package/docs/rules/tactical/no-stateful-service.md +102 -0
  57. package/docs/rules/tactical/no-thrown-failure.md +162 -0
  58. package/docs/rules/tooling/no-loose-disable.md +98 -0
  59. package/package.json +4 -3
  60. package/dist/cli-P5PwH9OE.mjs.map +0 -1
@@ -0,0 +1,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 = () =&gt; …</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.2.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
  }