@alveolus/arch 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. package/README.md +6 -0
  2. package/dist/bin.mjs +4 -2
  3. package/dist/bin.mjs.map +1 -1
  4. package/dist/{cli-P5PwH9OE.mjs → docs-DsQHpTtV.mjs} +190 -5
  5. package/dist/docs-DsQHpTtV.mjs.map +1 -0
  6. package/dist/index.d.mts +44 -2
  7. package/dist/index.d.mts.map +1 -1
  8. package/dist/index.mjs +2 -2
  9. package/docs/core/application/command-handlers.md +617 -0
  10. package/docs/core/application/event-publishers.md +234 -0
  11. package/docs/core/application/event-translators.md +329 -0
  12. package/docs/core/application/index.md +99 -0
  13. package/docs/core/application/integration-events.md +277 -0
  14. package/docs/core/application/outbox.md +416 -0
  15. package/docs/core/application/query-handlers.md +292 -0
  16. package/docs/core/application/unit-of-work.md +352 -0
  17. package/docs/core/domain/aggregates.md +822 -0
  18. package/docs/core/domain/domain-errors.md +251 -0
  19. package/docs/core/domain/domain-events.md +292 -0
  20. package/docs/core/domain/domain-services.md +249 -0
  21. package/docs/core/domain/entities.md +431 -0
  22. package/docs/core/domain/index.md +93 -0
  23. package/docs/core/domain/ports.md +284 -0
  24. package/docs/core/domain/repositories.md +335 -0
  25. package/docs/core/domain/value-objects.md +425 -0
  26. package/docs/core/domain/views.md +265 -0
  27. package/docs/core/index.md +108 -0
  28. package/docs/core/strategic/anti-corruption-layers.md +349 -0
  29. package/docs/core/strategic/index.md +83 -0
  30. package/docs/core/strategic/open-host-services.md +287 -0
  31. package/docs/core/strategic/published-language.md +265 -0
  32. package/docs/core/utilities/result.md +413 -0
  33. package/docs/guide/agents.md +68 -0
  34. package/docs/guide/existing-project.md +105 -0
  35. package/docs/guide/getting-started.md +275 -0
  36. package/docs/guide/learning-path.md +123 -0
  37. package/docs/guide/project-layout.md +324 -0
  38. package/docs/guide/versioning.md +42 -0
  39. package/docs/integrations/index.md +112 -0
  40. package/docs/integrations/nestjs.md +169 -0
  41. package/docs/rules/index.md +183 -0
  42. package/docs/rules/layers/no-driving-shortcut.md +119 -0
  43. package/docs/rules/layers/no-impure-domain.md +189 -0
  44. package/docs/rules/layers/no-outward-import.md +184 -0
  45. package/docs/rules/layers/no-portless-adapter.md +123 -0
  46. package/docs/rules/strategic/no-cross-context-import.md +140 -0
  47. package/docs/rules/strategic/no-fat-shared-kernel.md +81 -0
  48. package/docs/rules/strategic/no-leaky-host-service.md +107 -0
  49. package/docs/rules/strategic/no-unmapped-context.md +111 -0
  50. package/docs/rules/tactical/no-aggregate-reference.md +139 -0
  51. package/docs/rules/tactical/no-foreign-command-dependency.md +119 -0
  52. package/docs/rules/tactical/no-foreign-query-dependency.md +106 -0
  53. package/docs/rules/tactical/no-loose-code.md +171 -0
  54. package/docs/rules/tactical/no-misplaced-class.md +146 -0
  55. package/docs/rules/tactical/no-public-field.md +113 -0
  56. package/docs/rules/tactical/no-stateful-service.md +102 -0
  57. package/docs/rules/tactical/no-thrown-failure.md +162 -0
  58. package/docs/rules/tooling/no-loose-disable.md +98 -0
  59. package/package.json +4 -3
  60. package/dist/cli-P5PwH9OE.mjs.map +0 -1
@@ -0,0 +1,287 @@
1
+ ---
2
+ description: "Open host services in Domain-Driven Design with TypeScript: the documented entry point other bounded contexts call, answering in the published language."
3
+ ---
4
+
5
+ # Open host services
6
+
7
+ An open host service is the documented entry point of a bounded context: the one class other
8
+ contexts may call, answering in the [published language](./published-language.md).
9
+
10
+ <dl class="al-glance">
11
+ <dt>Layer</dt><dd>Driving</dd>
12
+ <dt>File</dt><dd><code>src/catalog/driving/in-process/catalog-api.ts</code></dd>
13
+ <dt>Implements</dt><dd><a href="#api"><code>OpenHostService</code></a></dd>
14
+ <dt>Called by</dt><dd><a href="/core/strategic/anti-corruption-layers">Anti-corruption layers</a> of other contexts</dd>
15
+ <dt>Checked by</dt><dd><a href="/rules/strategic/no-cross-context-import"><code>strategic/no-cross-context-import</code></a>, <a href="/rules/strategic/no-leaky-host-service"><code>strategic/no-leaky-host-service</code></a>, <a href="/rules/tactical/no-misplaced-class"><code>tactical/no-misplaced-class</code></a></dd>
16
+ </dl>
17
+
18
+ ## Why
19
+
20
+ Ordering needs the price of a product. Without a declared entry point, it imports whatever it finds
21
+ in the catalog: a repository here, a query handler there, the `Product` aggregate itself. The
22
+ catalog now has entry points nobody chose, and cannot change any of them without breaking ordering.
23
+
24
+ ::: tip The fix
25
+ The catalog opens one door: `CatalogApi`. It is the only class other contexts may import, it says
26
+ what the catalog offers, and it answers in JSON. Everything behind it stays free to change.
27
+ :::
28
+
29
+ ## How it works
30
+
31
+ An open host service is a class of `driving/`, like a controller: it receives a request, calls the
32
+ application of its own context and answers. It marks itself with `implements OpenHostService`.
33
+
34
+ <div class="al-cards">
35
+ <div class="al-card"><span class="al-card-title"><span class="al-card-step">1</span>Receive plain values</span>The caller passes strings and numbers, never objects of the catalog domain.</div>
36
+ <div class="al-card"><span class="al-card-title"><span class="al-card-step">2</span>Call the application</span>It calls a <a href="/core/application/query-handlers">query</a> or <a href="/core/application/command-handlers">command handler</a> of its own context. The rules stay there.</div>
37
+ <div class="al-card"><span class="al-card-title"><span class="al-card-step">3</span>Answer in the published language</span>It maps the result to a representation: JSON, never an aggregate.</div>
38
+ </div>
39
+
40
+ ## Where it fits
41
+
42
+ The open host service is the catalog side of the meeting point. On the ordering side, an
43
+ [anti-corruption layer](./anti-corruption-layers.md) calls it and translates its answer.
44
+
45
+ <div class="al-diagram">
46
+ <svg viewBox="0 0 720 260" role="img" aria-label="In ordering, AddLineHandler asks the PriceList port for a price. CatalogPriceList, the anti-corruption layer, extends the port and reads the JSON answered by CatalogApi, the open host service of catalog, which calls GetProductHandler.">
47
+ <defs>
48
+ <marker id="ohs-flow-arrow" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
49
+ <path class="arrow" d="M 0 0 L 10 5 L 0 10 z" />
50
+ </marker>
51
+ </defs>
52
+ <rect class="box" x="8" y="8" width="272" height="244" rx="14" />
53
+ <text class="note" x="24" y="30">src/ordering/</text>
54
+ <rect class="box" x="24" y="44" width="240" height="48" rx="8" />
55
+ <text class="label" x="144" y="64" text-anchor="middle">AddLineHandler</text>
56
+ <text class="note" x="144" y="82" text-anchor="middle">asks for a price</text>
57
+ <rect class="box" x="24" y="116" width="240" height="48" rx="8" />
58
+ <text class="label" x="144" y="136" text-anchor="middle">PriceList</text>
59
+ <text class="note" x="144" y="154" text-anchor="middle">port · in your words</text>
60
+ <rect class="box" x="24" y="188" width="240" height="48" rx="8" />
61
+ <text class="label" x="144" y="208" text-anchor="middle">CatalogPriceList</text>
62
+ <text class="note" x="144" y="226" text-anchor="middle">anti-corruption layer</text>
63
+ <path class="link" d="M 144 92 L 144 114" marker-end="url(#ohs-flow-arrow)" />
64
+ <path class="link" d="M 144 188 L 144 166" marker-end="url(#ohs-flow-arrow)" />
65
+ <text class="note" x="154" y="181">extends</text>
66
+ <rect class="box" x="300" y="188" width="156" height="48" rx="8" />
67
+ <text class="label" x="378" y="208" text-anchor="middle">JSON</text>
68
+ <text class="note" x="378" y="226" text-anchor="middle">the contract</text>
69
+ <path class="link" d="M 300 212 L 266 212" marker-end="url(#ohs-flow-arrow)" />
70
+ <path class="link" d="M 492 212 L 458 212" marker-end="url(#ohs-flow-arrow)" />
71
+ <rect class="box" x="476" y="8" width="236" height="244" rx="14" />
72
+ <text class="note" x="492" y="30">src/catalog/</text>
73
+ <rect class="box" x="492" y="44" width="204" height="48" rx="8" />
74
+ <text class="label" x="594" y="64" text-anchor="middle">GetProductHandler</text>
75
+ <text class="note" x="594" y="82" text-anchor="middle">reads the catalog</text>
76
+ <rect class="boundary" x="492" y="188" width="204" height="48" rx="8" />
77
+ <text class="label" x="594" y="208" text-anchor="middle">CatalogApi</text>
78
+ <text class="note" x="594" y="226" text-anchor="middle">open host service</text>
79
+ <path class="link" d="M 594 188 L 594 94" marker-end="url(#ohs-flow-arrow)" />
80
+ <text class="note" x="604" y="145">calls</text>
81
+ </svg>
82
+ </div>
83
+
84
+ ::: tip
85
+ The open host service does not know who calls it. It describes what the catalog offers, in the
86
+ catalog's words; each caller translates on its own side.
87
+ :::
88
+
89
+ ## API
90
+
91
+ ```ts
92
+ import type { OpenHostService } from "@alveolus/core";
93
+ // or: import type { OpenHostService } from
94
+ // "@alveolus/core/open-host-services";
95
+ ```
96
+
97
+ ### `OpenHostService` <Badge type="info" text="abstract · no members" /> <Badge type="tip" text="you implement it" />
98
+
99
+ ```ts
100
+ abstract class OpenHostService {}
101
+ ```
102
+
103
+ Marks the class as the entry point of its context. It has no members: the class declares its role
104
+ with `implements`, and the rules recognise it.
105
+
106
+ ```ts
107
+ export class CatalogApi implements OpenHostService { … }
108
+ ```
109
+
110
+ ### Your methods <Badge type="tip" text="called by anti-corruption layers" />
111
+
112
+ ```ts
113
+ async productById(
114
+ productId: string,
115
+ ): Promise<ProductRepresentation | undefined>
116
+ ```
117
+
118
+ Named after what the context offers, such as `productById`. They take plain values and answer in
119
+ the published language, never with the classes of the model. Anti-corruption layers of other
120
+ contexts call them.
121
+
122
+ ### The class itself <Badge type="tip" text="passed by the composition root" />
123
+
124
+ ```ts
125
+ new CatalogPriceList(catalog.api)
126
+ ```
127
+
128
+ The composition root of a downstream context receives the open host service and passes it to the
129
+ anti-corruption layers it builds.
130
+
131
+ ::: warning Caveats
132
+ - It is the only class another bounded context may import, and only from an anti-corruption layer
133
+ or from its composition root: see [`strategic/no-cross-context-import`](../../rules/strategic/no-cross-context-import.md).
134
+ - It lives in `driving/`, under the name of its technology: see
135
+ [`tactical/no-misplaced-class`](../../rules/tactical/no-misplaced-class.md).
136
+ - Use `implements`, not `extends`: the class stays free to extend something else, such as a base
137
+ controller.
138
+ :::
139
+
140
+ ## Usage
141
+
142
+ Build `CatalogApi`, the open host service through which other contexts read the catalog, one idea at
143
+ a time. Each step shows the whole file: added lines are highlighted, replaced lines are struck out.
144
+
145
+ <div class="al-cards">
146
+ <div class="al-card"><span class="al-card-title"><span class="al-card-step">1</span><a href="#_1-decide-what-you-publish">Decide what you publish</a></span>Agree on the contract first.</div>
147
+ <div class="al-card"><span class="al-card-title"><span class="al-card-step">2</span><a href="#_2-declare-the-service">Declare the service</a></span>Mark the one way in.</div>
148
+ <div class="al-card"><span class="al-card-title"><span class="al-card-step">3</span><a href="#_3-delegate-to-a-use-case">Delegate to a use case</a></span>Reuse the application, decide nothing.</div>
149
+ <div class="al-card"><span class="al-card-title"><span class="al-card-step">4</span><a href="#_4-answer-in-the-published-language">Answer in the published language</a></span>Never hand out the model.</div>
150
+ <div class="al-card"><span class="al-card-title"><span class="al-card-step">5</span><a href="#_5-wire-it">Wire it</a></span>Expose it, and nothing else.</div>
151
+ <div class="al-card"><span class="al-card-title"><span class="al-card-step">6</span><a href="#_6-check-it">Check it</a></span>Let the rules keep it that way.</div>
152
+ </div>
153
+
154
+ ### 1. Decide what you publish
155
+
156
+ What other contexts receive is the catalog's [published language](./published-language.md):
157
+ `ProductRepresentation`, plain JSON declared in `published-language/product.representation.ts`.
158
+
159
+ ### 2. Declare the service
160
+
161
+ So that other contexts know which class they may call, the catalog declares one class under
162
+ `driving/` that implements `OpenHostService`. Nothing else of the catalog may be imported from
163
+ outside.
164
+
165
+ ```ts [src/catalog/driving/in-process/catalog-api.ts]
166
+ import type { OpenHostService } from "@alveolus/core";
167
+
168
+ export class CatalogApi implements OpenHostService {}
169
+ ```
170
+
171
+ ### 3. Delegate to a use case
172
+
173
+ The open host service holds no rule: it calls a [query handler](../application/query-handlers.md)
174
+ that the catalog already has. It takes plain values, so callers need none of the catalog's
175
+ classes, and answers `undefined` when the product does not exist.
176
+
177
+ ```ts [src/catalog/driving/in-process/catalog-api.ts]
178
+ import type { OpenHostService } from "@alveolus/core";
179
+
180
+ export class CatalogApi implements OpenHostService {} // [!code --]
181
+ import { GetProductHandler } from // [!code ++]
182
+ "../../application/queries/get-product.query"; // [!code ++]
183
+ import type { ProductRepresentation } from // [!code ++]
184
+ "../../published-language/product.representation"; // [!code ++]
185
+
186
+ export class CatalogApi implements OpenHostService { // [!code ++]
187
+ constructor(private readonly getProduct: GetProductHandler) {} // [!code ++]
188
+
189
+ async productById( // [!code ++]
190
+ productId: string, // [!code ++]
191
+ ): Promise<ProductRepresentation | undefined> { // [!code ++]
192
+ const product = await this.getProduct.handle({ productId }); // [!code ++]
193
+ if (!product.ok) { // [!code ++]
194
+ return undefined; // [!code ++]
195
+ } // [!code ++]
196
+ } // [!code ++]
197
+ } // [!code ++]
198
+ ```
199
+
200
+ ### 4. Answer in the published language
201
+
202
+ So that the catalog can change its model without breaking anyone, the service answers with plain
203
+ JSON, never with a view, an aggregate or a value object.
204
+
205
+ ```ts [src/catalog/driving/in-process/catalog-api.ts]
206
+ import type { OpenHostService } from "@alveolus/core";
207
+
208
+ import { GetProductHandler } from
209
+ "../../application/queries/get-product.query";
210
+ import type { ProductRepresentation } from
211
+ "../../published-language/product.representation";
212
+
213
+ export class CatalogApi implements OpenHostService {
214
+ constructor(private readonly getProduct: GetProductHandler) {}
215
+
216
+ async productById(
217
+ productId: string,
218
+ ): Promise<ProductRepresentation | undefined> {
219
+ const product = await this.getProduct.handle({ productId });
220
+ if (!product.ok) {
221
+ return undefined;
222
+ }
223
+ const { id, name, price } = product.value; // [!code ++]
224
+ return { // [!code ++]
225
+ id: id.value, // [!code ++]
226
+ name, // [!code ++]
227
+ price: { amount: price.amount, currency: price.currency }, // [!code ++]
228
+ }; // [!code ++]
229
+ }
230
+ }
231
+ ```
232
+
233
+ ### 5. Wire it
234
+
235
+ The composition root of the catalog builds the service and exposes it, and nothing else. The one
236
+ of ordering receives it to build its [anti-corruption layer](./anti-corruption-layers.md).
237
+
238
+ ```ts [src/catalog/catalog.module.ts]
239
+ export class CatalogModule {
240
+ readonly api: CatalogApi;
241
+
242
+ constructor(db: Pool) {
243
+ const getProduct = new GetProductHandler(new PgProducts(db));
244
+ this.api = new CatalogApi(getProduct);
245
+ }
246
+ }
247
+ ```
248
+
249
+ ```ts [src/ordering/ordering.module.ts]
250
+ export class OrderingModule {
251
+ constructor(db: Pool, catalog: CatalogModule) {
252
+ const prices = new CatalogPriceList(catalog.api);
253
+ …
254
+ }
255
+ }
256
+ ```
257
+
258
+ ### 6. Check it
259
+
260
+ Run the checks. Three rules keep the service the only door of the catalog:
261
+
262
+ ```sh
263
+ npx alveolus arch check
264
+ ```
265
+
266
+ <div class="al-cards">
267
+ <div class="al-card"><span class="al-card-title"><a href="../../rules/strategic/no-cross-context-import"><code>strategic/no-cross-context-import</code></a></span>Another context imports this class, and nothing else of the catalog.</div>
268
+ <div class="al-card"><span class="al-card-title"><a href="../../rules/strategic/no-leaky-host-service"><code>strategic/no-leaky-host-service</code></a></span>It answers in the published language, never with a class of the catalog.</div>
269
+ <div class="al-card"><span class="al-card-title"><a href="../../rules/tactical/no-misplaced-class"><code>tactical/no-misplaced-class</code></a></span>It stays under <code>driving/</code>.</div>
270
+ </div>
271
+
272
+ An import that reaches past it, into the catalog's domain, is reported:
273
+
274
+ ```
275
+ src/ordering/driven/catalog/adapters/catalog-price-list.adapter.ts
276
+ 4 error strategic/no-cross-context-import: Imports
277
+ src/catalog/domain/aggregates/product.aggregate.ts (catalog domain):
278
+ only an OpenHostService of another bounded context may be imported.
279
+ ```
280
+
281
+ ## See also
282
+
283
+ - [Published Language](./published-language.md), what it answers in
284
+ - [Anti-corruption layers](./anti-corruption-layers.md), how another context reads it
285
+ - [Query handlers](../application/query-handlers.md), what it usually calls
286
+ - [Project layout: bounded contexts](../../guide/project-layout.md#bounded-contexts)
287
+ - Rules: [`strategic/no-cross-context-import`](../../rules/strategic/no-cross-context-import.md), [`strategic/no-leaky-host-service`](../../rules/strategic/no-leaky-host-service.md), [`tactical/no-misplaced-class`](../../rules/tactical/no-misplaced-class.md)
@@ -0,0 +1,265 @@
1
+ ---
2
+ description: "Published Language in Domain-Driven Design: the JSON contract that bounded contexts exchange instead of importing each other's code."
3
+ ---
4
+
5
+ # Published Language
6
+
7
+ The published language is the JSON that bounded contexts exchange: the contract between them,
8
+ instead of their code.
9
+
10
+ <dl class="al-glance">
11
+ <dt>Layer</dt><dd>Published language</dd>
12
+ <dt>File</dt><dd><code>src/catalog/published-language/product.representation.ts</code></dd>
13
+ <dt>Wraps</dt><dd><a href="#api"><code>PublishedLanguage&lt;Representation&gt;</code></a></dd>
14
+ <dt>Used by</dt><dd><a href="/core/strategic/open-host-services">Open host services</a>, <a href="/core/strategic/anti-corruption-layers">anti-corruption layers</a>, <a href="/core/application/integration-events">integration events</a></dd>
15
+ <dt>Checked by</dt><dd><a href="/rules/strategic/no-cross-context-import"><code>strategic/no-cross-context-import</code></a>, <a href="/rules/layers/no-outward-import"><code>layers/no-outward-import</code></a></dd>
16
+ </dl>
17
+
18
+ ## Why
19
+
20
+ Ordering needs the price of a product. The quickest way is to import the `Product` aggregate of
21
+ the catalog, or its repository. From then on, every rename in the catalog breaks ordering, and the
22
+ catalog can no longer change its model without asking every context that reads it.
23
+
24
+ ::: tip The fix
25
+ The catalog publishes a format: plain JSON, named and typed, one file per representation. Other
26
+ contexts depend on that format, never on the code behind it, and the catalog changes its model
27
+ freely as long as the format holds.
28
+ :::
29
+
30
+ ## How it works
31
+
32
+ A representation is a type wrapped in `PublishedLanguage<…>`. The wrapper changes nothing at
33
+ runtime: it checks, at compile time, that the type is JSON. Each side of the exchange declares the
34
+ representation in its own `published-language/` folder.
35
+
36
+ <div class="al-cards">
37
+ <div class="al-card"><span class="al-card-title"><span class="al-card-step">1</span>The upstream publishes</span>The catalog declares <code>ProductRepresentation</code>: everything it agrees to expose.</div>
38
+ <div class="al-card"><span class="al-card-title"><span class="al-card-step">2</span>Its open host service answers</span><a href="/core/strategic/open-host-services">CatalogApi</a> maps the domain to the representation.</div>
39
+ <div class="al-card"><span class="al-card-title"><span class="al-card-step">3</span>The downstream reads</span>Ordering declares only the fields it reads; its <a href="/core/strategic/anti-corruption-layers">anti-corruption layer</a> turns them into its own objects.</div>
40
+ </div>
41
+
42
+ ## Where it fits
43
+
44
+ The published language is what crosses the line between two contexts: the answer of an open host
45
+ service, the payload of an [integration event](../application/integration-events.md). Nothing else
46
+ crosses it.
47
+
48
+ <div class="al-diagram">
49
+ <svg viewBox="0 0 720 260" role="img" aria-label="In ordering, AddLineHandler asks the PriceList port for a price. CatalogPriceList, the anti-corruption layer, extends the port and reads the JSON answered by CatalogApi, the open host service of catalog, which calls GetProductHandler.">
50
+ <defs>
51
+ <marker id="pl-flow-arrow" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
52
+ <path class="arrow" d="M 0 0 L 10 5 L 0 10 z" />
53
+ </marker>
54
+ </defs>
55
+ <rect class="box" x="8" y="8" width="272" height="244" rx="14" />
56
+ <text class="note" x="24" y="30">src/ordering/</text>
57
+ <rect class="box" x="24" y="44" width="240" height="48" rx="8" />
58
+ <text class="label" x="144" y="64" text-anchor="middle">AddLineHandler</text>
59
+ <text class="note" x="144" y="82" text-anchor="middle">asks for a price</text>
60
+ <rect class="box" x="24" y="116" width="240" height="48" rx="8" />
61
+ <text class="label" x="144" y="136" text-anchor="middle">PriceList</text>
62
+ <text class="note" x="144" y="154" text-anchor="middle">port · in your words</text>
63
+ <rect class="box" x="24" y="188" width="240" height="48" rx="8" />
64
+ <text class="label" x="144" y="208" text-anchor="middle">CatalogPriceList</text>
65
+ <text class="note" x="144" y="226" text-anchor="middle">anti-corruption layer</text>
66
+ <path class="link" d="M 144 92 L 144 114" marker-end="url(#pl-flow-arrow)" />
67
+ <path class="link" d="M 144 188 L 144 166" marker-end="url(#pl-flow-arrow)" />
68
+ <text class="note" x="154" y="181">extends</text>
69
+ <rect class="boundary" x="300" y="188" width="156" height="48" rx="8" />
70
+ <text class="label" x="378" y="208" text-anchor="middle">JSON</text>
71
+ <text class="note" x="378" y="226" text-anchor="middle">the contract</text>
72
+ <path class="link" d="M 300 212 L 266 212" marker-end="url(#pl-flow-arrow)" />
73
+ <path class="link" d="M 492 212 L 458 212" marker-end="url(#pl-flow-arrow)" />
74
+ <rect class="box" x="476" y="8" width="236" height="244" rx="14" />
75
+ <text class="note" x="492" y="30">src/catalog/</text>
76
+ <rect class="box" x="492" y="44" width="204" height="48" rx="8" />
77
+ <text class="label" x="594" y="64" text-anchor="middle">GetProductHandler</text>
78
+ <text class="note" x="594" y="82" text-anchor="middle">reads the catalog</text>
79
+ <rect class="box" x="492" y="188" width="204" height="48" rx="8" />
80
+ <text class="label" x="594" y="208" text-anchor="middle">CatalogApi</text>
81
+ <text class="note" x="594" y="226" text-anchor="middle">open host service</text>
82
+ <path class="link" d="M 594 188 L 594 94" marker-end="url(#pl-flow-arrow)" />
83
+ <text class="note" x="604" y="145">calls</text>
84
+ </svg>
85
+ </div>
86
+
87
+ ::: tip
88
+ Inside a context, use the objects of the domain: aggregates, value objects, identifiers. The
89
+ published language exists only where a context meets another one.
90
+ :::
91
+
92
+ ## API
93
+
94
+ ```ts
95
+ import type { PublishedLanguage } from "@alveolus/core";
96
+ // or: import type { PublishedLanguage } from
97
+ // "@alveolus/core/published-language";
98
+ ```
99
+
100
+ ### Type parameters
101
+
102
+ ```ts
103
+ type PublishedLanguage<Representation extends JsonValue> =
104
+ Representation;
105
+ ```
106
+
107
+ | Parameter | What it is | Constraint |
108
+ | --- | --- | --- |
109
+ | `Representation` | The JSON shape of what is exchanged. | extends `JsonValue` |
110
+
111
+ ### `PublishedLanguage<Representation>` <Badge type="info" text="type" /> <Badge type="tip" text="you write it" />
112
+
113
+ ```ts
114
+ type ProductRepresentation = PublishedLanguage<{
115
+ readonly id: string;
116
+ readonly name: string;
117
+ }>;
118
+ ```
119
+
120
+ Wraps one representation and checks at compile time that it is JSON. It returns the same type.
121
+
122
+ ### `JsonValue` <Badge type="info" text="type" />
123
+
124
+ ```ts
125
+ type JsonValue =
126
+ | string
127
+ | number
128
+ | boolean
129
+ | null
130
+ | readonly JsonValue[]
131
+ | { readonly [key: string]: JsonValue };
132
+ ```
133
+
134
+ Any JSON value: string, number, boolean, `null`, and arrays and objects of them. It is the
135
+ constraint of `Representation`.
136
+
137
+ ::: warning Caveats
138
+ - `PublishedLanguage` changes nothing at runtime: the type is the JSON type it wraps.
139
+ - Declare representations with `type`, not `interface`: an interface does not satisfy the index
140
+ signature of `JsonValue`.
141
+ - Files in `published-language/` import only their own published language, the published-language
142
+ types of core (`PublishedLanguage`, `JsonValue`, `IntegrationEvent`, `AnyIntegrationEvent`) and
143
+ packages such as a schema library: see [`layers/no-outward-import`](../../rules/layers/no-outward-import.md).
144
+ :::
145
+
146
+ ## Usage
147
+
148
+ Build the contract the catalog publishes and the copy ordering reads, one idea at a time. Each step
149
+ shows the whole file: added lines are highlighted.
150
+
151
+ <div class="al-cards">
152
+ <div class="al-card"><span class="al-card-title"><span class="al-card-step">1</span><a href="#_1-find-what-crosses-the-boundary">Find what crosses the boundary</a></span>Start from what others need.</div>
153
+ <div class="al-card"><span class="al-card-title"><span class="al-card-step">2</span><a href="#_2-declare-what-you-publish">Declare what you publish</a></span>A JSON type, in its own folder.</div>
154
+ <div class="al-card"><span class="al-card-title"><span class="al-card-step">3</span><a href="#_3-write-values-as-plain-json">Write values as plain JSON</a></span>No value object crosses.</div>
155
+ <div class="al-card"><span class="al-card-title"><span class="al-card-step">4</span><a href="#_4-redeclare-what-you-read">Redeclare what you read</a></span>Downstream keeps its own copy.</div>
156
+ <div class="al-card"><span class="al-card-title"><span class="al-card-step">5</span><a href="#_5-use-it-on-both-sides">Use it on both sides</a></span>Answer with one, read with the other.</div>
157
+ <div class="al-card"><span class="al-card-title"><span class="al-card-step">6</span><a href="#_6-check-it">Check it</a></span>Let the rules keep it that way.</div>
158
+ </div>
159
+
160
+ ### 1. Find what crosses the boundary
161
+
162
+ Ordering needs the price of a product, and the catalog owns products: the catalog publishes them
163
+ through its [open host service](./open-host-services.md).
164
+
165
+ ### 2. Declare what you publish
166
+
167
+ So that other contexts get a contract and not the model, the catalog declares the shape it sends
168
+ in `published-language/`, with `PublishedLanguage`. Identifiers are written as raw strings.
169
+
170
+ ```ts [src/catalog/published-language/product.representation.ts]
171
+ import type { PublishedLanguage } from "@alveolus/core";
172
+
173
+ export type ProductRepresentation = PublishedLanguage<{
174
+ readonly id: string;
175
+ readonly name: string;
176
+ }>;
177
+ ```
178
+
179
+ ### 3. Write values as plain JSON
180
+
181
+ A `Money` is a class of the catalog's model: it is sent as plain fields. `PublishedLanguage` only
182
+ accepts JSON values, so a class, a `Date` or `undefined` does not compile.
183
+
184
+ ```ts [src/catalog/published-language/product.representation.ts]
185
+ import type { PublishedLanguage } from "@alveolus/core";
186
+
187
+ export type ProductRepresentation = PublishedLanguage<{
188
+ readonly id: string;
189
+ readonly name: string;
190
+ readonly price: { // [!code ++]
191
+ readonly amount: number; // [!code ++]
192
+ readonly currency: string; // [!code ++]
193
+ }; // [!code ++]
194
+ }>;
195
+ ```
196
+
197
+ ### 4. Redeclare what you read
198
+
199
+ Ordering does not import the catalog's type: it redeclares, in its own `published-language/`, only
200
+ the fields it reads. The catalog may add fields without touching ordering.
201
+
202
+ ```ts [src/ordering/published-language/catalog-product.representation.ts]
203
+ import type { PublishedLanguage } from "@alveolus/core";
204
+
205
+ export type CatalogProductRepresentation = PublishedLanguage<{
206
+ readonly id: string;
207
+ readonly price: {
208
+ readonly amount: number;
209
+ readonly currency: string;
210
+ };
211
+ }>;
212
+ ```
213
+
214
+ ### 5. Use it on both sides
215
+
216
+ The open host service of the catalog answers with the first type; the
217
+ [anti-corruption layer](./anti-corruption-layers.md) of ordering reads the answer as the second,
218
+ and TypeScript checks that both shapes still match.
219
+
220
+ ```ts [src/catalog/driving/in-process/catalog-api.ts]
221
+ async productById(
222
+ productId: string,
223
+ ): Promise<ProductRepresentation | undefined> {
224
+ ```
225
+
226
+ ```ts [src/ordering/driven/catalog/adapters/catalog-price-list.adapter.ts]
227
+ const product: CatalogProductRepresentation | undefined =
228
+ await this.catalog.productById(productId.value);
229
+ ```
230
+
231
+ ### 6. Check it
232
+
233
+ Run the checks. Two rules keep the published language a contract:
234
+
235
+ ```sh
236
+ npx alveolus arch check
237
+ ```
238
+
239
+ <div class="al-cards">
240
+ <div class="al-card"><span class="al-card-title"><a href="../../rules/strategic/no-cross-context-import"><code>strategic/no-cross-context-import</code></a></span>No context imports the published language of another: it redeclares it.</div>
241
+ <div class="al-card"><span class="al-card-title"><a href="../../rules/layers/no-outward-import"><code>layers/no-outward-import</code></a></span>The published language imports only published-language types.</div>
242
+ </div>
243
+
244
+ Importing the catalog's type instead of redeclaring it is reported:
245
+
246
+ ```
247
+ src/ordering/driven/catalog/adapters/catalog-price-list.adapter.ts
248
+ 8 error strategic/no-cross-context-import: Imports the published language
249
+ of catalog: redeclare the fields you read in your own
250
+ published-language/.
251
+ ```
252
+
253
+ ## Troubleshooting
254
+
255
+ **`Type '…' does not satisfy the constraint 'JsonValue'`**: the representation holds something that
256
+ is not JSON (a `Date`, a class instance, a function), or it is declared with `interface`. Convert the
257
+ value to JSON, or declare the shape with `type`.
258
+
259
+ ## See also
260
+
261
+ - [Open host services](./open-host-services.md), which answer in the published language
262
+ - [Anti-corruption layers](./anti-corruption-layers.md), which read it
263
+ - [Integration events](../application/integration-events.md), the events in the published language
264
+ - [Project layout: bounded contexts](../../guide/project-layout.md#bounded-contexts)
265
+ - Rules: [`strategic/no-cross-context-import`](../../rules/strategic/no-cross-context-import.md), [`layers/no-outward-import`](../../rules/layers/no-outward-import.md)