@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,234 @@
1
+ ---
2
+ description: "Event publishers in TypeScript: the port that sends integration events to a message broker such as Kafka, a webhook, or other bounded contexts."
3
+ ---
4
+
5
+ # Event publishers
6
+
7
+ An event publisher is the port that sends [integration events](./integration-events.md) out of the
8
+ bounded context: to a broker, a webhook, or the other contexts of the same process.
9
+
10
+ <dl class="al-glance">
11
+ <dt>Layer</dt><dd>Application (a port)</dd>
12
+ <dt>File</dt><dd><code>shared-kernel/driven/kafka/adapters/kafka-event-publisher.adapter.ts</code> (your adapter)</dd>
13
+ <dt>Extends</dt><dd><a href="#api"><code>EventPublisher</code></a></dd>
14
+ <dt>Called by</dt><dd>The <a href="/core/application/outbox"><code>OutboxRelay</code></a>, never a command handler</dd>
15
+ <dt>Checked by</dt><dd><a href="/rules/layers/no-portless-adapter"><code>layers/no-portless-adapter</code></a>, <a href="/rules/tactical/no-misplaced-class"><code>tactical/no-misplaced-class</code></a></dd>
16
+ </dl>
17
+
18
+ ## Why
19
+
20
+ Shipping must learn that an order was placed. The ordering context should not know whether that
21
+ goes through Kafka, RabbitMQ or a function call: if the relay used the Kafka client directly,
22
+ changing the broker, or testing without one, would mean changing application code.
23
+
24
+ ::: tip The fix
25
+ The application talks to an abstract `EventPublisher` with one method, `publish(events)`. A driven
26
+ adapter implements it with the technology you use. The [outbox relay](./outbox.md) calls it; nothing
27
+ else does.
28
+ :::
29
+
30
+ ## How it works
31
+
32
+ `publish` receives a batch of integration events, already JSON, and must either send them all or
33
+ throw.
34
+
35
+ <div class="al-cards">
36
+ <div class="al-card"><span class="al-card-title"><span class="al-card-step">1</span>Send in order</span>Publish the events in the order received: <code>OrderPlaced</code> before a later <code>OrderCancelled</code>.</div>
37
+ <div class="al-card"><span class="al-card-title"><span class="al-card-step">2</span>Throw on failure</span>A broker down is technical: throw. The relay keeps the events pending and retries.</div>
38
+ <div class="al-card"><span class="al-card-title"><span class="al-card-step">3</span>Expect duplicates</span>Delivery is at least once. Consumers ignore an event whose <code>id</code> they already handled.</div>
39
+ </div>
40
+
41
+ ## Where it fits
42
+
43
+ The publisher is the second step of the outbox relay, run in the background after the order was
44
+ saved.
45
+
46
+ <div class="al-diagram">
47
+ <svg viewBox="0 0 680 240" role="img" aria-label="A timer job calls the OutboxRelay, which reads pending events from the outbox, calls the event publisher, and marks the events as published.">
48
+ <defs>
49
+ <marker id="publisher-flow-arrow" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
50
+ <path class="arrow" d="M 0 0 L 10 5 L 0 10 z" />
51
+ </marker>
52
+ </defs>
53
+ <rect class="box" x="8" y="92" width="130" height="56" rx="8" />
54
+ <text class="label" x="73" y="116" text-anchor="middle">Timer job</text>
55
+ <text class="note" x="73" y="136" text-anchor="middle">driving adapter</text>
56
+ <path class="link" d="M 138 120 L 178 120" marker-end="url(#publisher-flow-arrow)" />
57
+ <rect class="box" x="180" y="92" width="180" height="56" rx="8" />
58
+ <text class="label" x="270" y="116" text-anchor="middle">OutboxRelay</text>
59
+ <text class="note" x="270" y="136" text-anchor="middle">relay()</text>
60
+ <rect class="box" x="440" y="24" width="232" height="48" rx="8" />
61
+ <text class="label" x="556" y="44" text-anchor="middle">1 · outbox.pending(n)</text>
62
+ <text class="note" x="556" y="62" text-anchor="middle">reads a batch</text>
63
+ <rect class="boundary" x="440" y="96" width="232" height="48" rx="8" />
64
+ <text class="label" x="556" y="116" text-anchor="middle">2 · publisher.publish(events)</text>
65
+ <text class="note" x="556" y="134" text-anchor="middle">this page: sends them</text>
66
+ <rect class="box" x="440" y="168" width="232" height="48" rx="8" />
67
+ <text class="label" x="556" y="188" text-anchor="middle">3 · outbox.markPublished(ids)</text>
68
+ <text class="note" x="556" y="206" text-anchor="middle">skips them next time</text>
69
+ <path class="link" d="M 360 120 L 438 48" marker-end="url(#publisher-flow-arrow)" />
70
+ <path class="link" d="M 360 120 L 438 120" marker-end="url(#publisher-flow-arrow)" />
71
+ <path class="link" d="M 360 120 L 438 192" marker-end="url(#publisher-flow-arrow)" />
72
+ </svg>
73
+ </div>
74
+
75
+ ::: tip
76
+ If `publish` throws, step 3 never runs: the same events are published on the next call.
77
+ :::
78
+
79
+ ## API
80
+
81
+ ```ts
82
+ import { EventPublisher } from "@alveolus/core";
83
+ // or: import { EventPublisher } from "@alveolus/core/event-publishers";
84
+ ```
85
+
86
+ ### `publish(events)` <Badge type="info" text="abstract" /> <Badge type="tip" text="you implement it" /> <Badge type="tip" text="called by the OutboxRelay" />
87
+
88
+ ```ts
89
+ abstract publish(
90
+ events: readonly AnyIntegrationEvent[],
91
+ ): Promise<void>
92
+ ```
93
+
94
+ Sends the integration events, in order, and throws on failure. The `OutboxRelay` calls it with one
95
+ batch of pending events.
96
+
97
+ ::: warning Caveats
98
+ - A failure to publish is technical: the adapter throws instead of returning a `Result`.
99
+ - Delivery is at least once: an event may be published twice. Consumers ignore duplicates by `id`.
100
+ - `EventPublisher` extends `Port`: it is an injection token, and its adapters live in
101
+ `driven/<technology>/adapters/`.
102
+ :::
103
+
104
+ ## Usage
105
+
106
+ Build a publisher for Kafka. Each step shows the whole file: added lines are highlighted, replaced lines are struck out.
107
+
108
+ <div class="al-cards">
109
+ <div class="al-card"><span class="al-card-title"><span class="al-card-step">1</span><a href="#_1-pick-the-broker">Pick the broker</a></span>One adapter per technology.</div>
110
+ <div class="al-card"><span class="al-card-title"><span class="al-card-step">2</span><a href="#_2-declare-the-adapter">Declare the adapter</a></span>Extend the port.</div>
111
+ <div class="al-card"><span class="al-card-title"><span class="al-card-step">3</span><a href="#_3-publish-each-event">Publish each event</a></span>In order, one topic per event type.</div>
112
+ <div class="al-card"><span class="al-card-title"><span class="al-card-step">4</span><a href="#_4-key-each-message-by-its-id">Key each message by its id</a></span>Let consumers ignore duplicates.</div>
113
+ <div class="al-card"><span class="al-card-title"><span class="al-card-step">5</span><a href="#_5-wire-it-to-the-relay">Wire it to the relay</a></span>Only the relay calls it.</div>
114
+ <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>
115
+ </div>
116
+
117
+ ### 1. Pick the broker
118
+
119
+ `EventPublisher` is a port of `@alveolus/core`: you only write its adapter, under the name of its technology, here `driven/kafka/adapters/`, in the shared kernel.
120
+
121
+ ### 2. Declare the adapter
122
+
123
+ The adapter extends `EventPublisher` and receives the client of the broker in its constructor, built by the composition root.
124
+
125
+ ```ts [src/shared-kernel/driven/kafka/adapters/kafka-event-publisher.adapter.ts]
126
+ import { EventPublisher } from "@alveolus/core";
127
+ import type { Producer } from "kafkajs";
128
+
129
+ export class KafkaEventPublisher extends EventPublisher {
130
+ constructor(private readonly producer: Producer) {
131
+ super();
132
+ }
133
+ }
134
+ ```
135
+
136
+ TypeScript now asks for `publish()`: the next step adds it.
137
+
138
+ ### 3. Publish each event
139
+
140
+ The relay hands over a batch in the order the events were added. Sending them one by one keeps that order, and a failure throws: the events stay pending in the outbox and are sent again.
141
+
142
+ ```ts [src/shared-kernel/driven/kafka/adapters/kafka-event-publisher.adapter.ts]
143
+ import { EventPublisher } from "@alveolus/core"; // [!code --]
144
+ import { type AnyIntegrationEvent, EventPublisher } from "@alveolus/core"; // [!code ++]
145
+ import type { Producer } from "kafkajs";
146
+
147
+ export class KafkaEventPublisher extends EventPublisher {
148
+ constructor(private readonly producer: Producer) {
149
+ super();
150
+ }
151
+
152
+ async publish( // [!code ++]
153
+ events: readonly AnyIntegrationEvent[], // [!code ++]
154
+ ): Promise<void> { // [!code ++]
155
+ for (const event of events) { // [!code ++]
156
+ await this.producer.send({ // [!code ++]
157
+ messages: [{ value: JSON.stringify(event) }], // [!code ++]
158
+ topic: `${event.source}.${event.type}`, // [!code ++]
159
+ }); // [!code ++]
160
+ } // [!code ++]
161
+ } // [!code ++]
162
+ }
163
+ ```
164
+
165
+ ### 4. Key each message by its id
166
+
167
+ An event may be sent twice, after a failure between sending and marking it published. Its id, as the message key, lets consumers recognise and ignore a duplicate.
168
+
169
+ ```ts [src/shared-kernel/driven/kafka/adapters/kafka-event-publisher.adapter.ts]
170
+ import { type AnyIntegrationEvent, EventPublisher } from "@alveolus/core";
171
+ import type { Producer } from "kafkajs";
172
+
173
+ export class KafkaEventPublisher extends EventPublisher {
174
+ constructor(private readonly producer: Producer) {
175
+ super();
176
+ }
177
+
178
+ async publish(
179
+ events: readonly AnyIntegrationEvent[],
180
+ ): Promise<void> {
181
+ for (const event of events) {
182
+ await this.producer.send({
183
+ messages: [{ value: JSON.stringify(event) }], // [!code --]
184
+ messages: [ // [!code ++]
185
+ { key: event.id, value: JSON.stringify(event) }, // [!code ++]
186
+ ], // [!code ++]
187
+ topic: `${event.source}.${event.type}`,
188
+ });
189
+ }
190
+ }
191
+ }
192
+ ```
193
+
194
+ This is the complete publisher.
195
+
196
+ ### 5. Wire it to the relay
197
+
198
+ The composition root passes the publisher to the [`OutboxRelay`](./outbox.md#usage), the only caller of `publish`.
199
+
200
+ ```ts [src/app.module.ts]
201
+ const relay = new OutboxRelay(
202
+ outbox,
203
+ new KafkaEventPublisher(producer),
204
+ 100,
205
+ );
206
+ ```
207
+
208
+ ### 6. Check it
209
+
210
+ Run the checks. Two rules keep the publisher the way it is now:
211
+
212
+ ```sh
213
+ npx alveolus arch check
214
+ ```
215
+
216
+ <div class="al-cards">
217
+ <div class="al-card"><span class="al-card-title"><a href="../../rules/layers/no-portless-adapter"><code>no-portless-adapter</code></a></span>It extends the port it implements.</div>
218
+ <div class="al-card"><span class="al-card-title"><a href="../../rules/tactical/no-misplaced-class"><code>no-misplaced-class</code></a></span>It stays alone in <code>driven/kafka/adapters/*.adapter.ts</code>.</div>
219
+ </div>
220
+
221
+ The same class without <code>extends EventPublisher</code> is reported:
222
+
223
+ ```
224
+ src/shared-kernel/driven/kafka/adapters/kafka-event-publisher.adapter.ts
225
+ 4 error layers/no-portless-adapter: KafkaEventPublisher is a driven
226
+ adapter but extends no Port: extend the port it implements.
227
+ ```
228
+
229
+ ## See also
230
+
231
+ - [Outbox](./outbox.md), whose relay calls the publisher
232
+ - [Integration events](./integration-events.md), what is published
233
+ - [Ports](../domain/ports.md), what an event publisher is
234
+ - Rules: [`layers/no-portless-adapter`](../../rules/layers/no-portless-adapter.md), [`tactical/no-misplaced-class`](../../rules/tactical/no-misplaced-class.md)
@@ -0,0 +1,329 @@
1
+ ---
2
+ description: "Event translators in TypeScript: turn the domain events of an aggregate into integration events, plain JSON that other bounded contexts can read."
3
+ ---
4
+
5
+ # Event translators
6
+
7
+ An event translator turns the [domain events](../domain/domain-events.md) of an aggregate into
8
+ [integration events](./integration-events.md): plain JSON that other bounded contexts can read.
9
+
10
+ <dl class="al-glance">
11
+ <dt>Layer</dt><dd>Application</dd>
12
+ <dt>File</dt><dd><code>application/translators/order-events.translator.ts</code></dd>
13
+ <dt>Extends</dt><dd><a href="#api"><code>EventTranslator&lt;Event, Output&gt;</code></a></dd>
14
+ <dt>Called by</dt><dd><a href="/core/application/command-handlers">Command handlers</a></dd>
15
+ <dt>Checked by</dt><dd><a href="/rules/tactical/no-misplaced-class"><code>tactical/no-misplaced-class</code></a>, <a href="/rules/layers/no-outward-import"><code>layers/no-outward-import</code></a></dd>
16
+ </dl>
17
+
18
+ ## Why
19
+
20
+ The billing context wants to know when an order is placed. Send it the `OrderPlaced` domain event
21
+ as is, and billing now depends on your classes: an `OrderId` object, a `Date`, field names you
22
+ chose for your own model. Rename one of them and billing breaks, without a single line of billing
23
+ changing.
24
+
25
+ ::: tip The fix
26
+ An event translator maps each domain event to an integration event declared in your
27
+ [published language](../strategic/published-language.md): JSON, with a name and a version. It is
28
+ the one place where your model meets the contract other contexts read, so a refactoring of the
29
+ domain never reaches them by accident.
30
+ :::
31
+
32
+ ## How it works
33
+
34
+ For each domain event it receives, the translator does three things:
35
+
36
+ <div class="al-cards">
37
+ <div class="al-card"><span class="al-card-title"><span class="al-card-step">1</span>Recognize the event</span>Tell the events of the aggregate apart with <code>instanceof</code>.</div>
38
+ <div class="al-card"><span class="al-card-title"><span class="al-card-step">2</span>Write the contract</span>Choose the <code>type</code>, the <code>version</code> and a JSON <code>payload</code>: identifiers and value objects become strings and numbers.</div>
39
+ <div class="al-card"><span class="al-card-title"><span class="al-card-step">3</span>Wrap it</span><code>wrap</code> adds the id and the date of the domain event, the <code>source</code> and the correlation ids.</div>
40
+ </div>
41
+
42
+ ```ts
43
+ translate(
44
+ event: OrderPlaced,
45
+ context: IntegrationEventContext,
46
+ ): OrderPlacedRepresentation {
47
+ return this.wrap(event, context, {
48
+ type: "OrderPlaced",
49
+ version: 1,
50
+ payload: {
51
+ orderId: event.aggregateId.value,
52
+ customerId: event.payload.customerId,
53
+ },
54
+ });
55
+ }
56
+ ```
57
+
58
+ ## Where it fits
59
+
60
+ The translator runs inside the command handler, after the aggregate is saved and before its events
61
+ go to the outbox. Before it, events are domain objects; after it, they are JSON.
62
+
63
+ <div class="al-diagram">
64
+ <svg viewBox="0 0 680 130" role="img" aria-label="The Order records an OrderPlaced domain event. The PlaceOrderHandler pulls it and gives it to the OrderEventsTranslator, which returns an integration event that the handler adds to the outbox.">
65
+ <defs>
66
+ <marker id="translator-flow-arrow" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
67
+ <path class="arrow" d="M 0 0 L 10 5 L 0 10 z" />
68
+ </marker>
69
+ </defs>
70
+ <rect class="box" x="8" y="36" width="130" height="56" rx="8" />
71
+ <text class="label" x="73" y="60" text-anchor="middle">Order</text>
72
+ <text class="note" x="73" y="80" text-anchor="middle">records event</text>
73
+ <path class="link" d="M 138 64 L 168 64" marker-end="url(#translator-flow-arrow)" />
74
+ <rect class="box" x="170" y="36" width="170" height="56" rx="8" />
75
+ <text class="label" x="255" y="60" text-anchor="middle">PlaceOrderHandler</text>
76
+ <text class="note" x="255" y="80" text-anchor="middle">pulls events</text>
77
+ <path class="link" d="M 340 64 L 370 64" marker-end="url(#translator-flow-arrow)" />
78
+ <rect class="boundary" x="372" y="36" width="180" height="56" rx="8" />
79
+ <text class="label" x="462" y="60" text-anchor="middle">OrderEventsTranslator</text>
80
+ <text class="note" x="462" y="80" text-anchor="middle">this page</text>
81
+ <path class="link" d="M 552 64 L 582 64" marker-end="url(#translator-flow-arrow)" />
82
+ <rect class="box" x="584" y="36" width="88" height="56" rx="8" />
83
+ <text class="label" x="628" y="60" text-anchor="middle">Outbox</text>
84
+ <text class="note" x="628" y="80" text-anchor="middle">JSON</text>
85
+ <text class="note" x="173" y="116" text-anchor="middle">domain events</text>
86
+ <text class="note" x="560" y="116" text-anchor="middle">integration events</text>
87
+ </svg>
88
+ </div>
89
+
90
+ ::: tip
91
+ The domain never knows the translator. It records events in its own words; the application decides
92
+ what leaves the bounded context, and in which form.
93
+ :::
94
+
95
+ ## API
96
+
97
+ ```ts
98
+ import {
99
+ EventTranslator,
100
+ type IntegrationEventContext,
101
+ } from "@alveolus/core";
102
+ // or: from "@alveolus/core/event-translators"
103
+ ```
104
+
105
+ ### Type parameters
106
+
107
+ ```ts
108
+ abstract class EventTranslator<
109
+ Event extends AnyDomainEvent,
110
+ Output extends AnyIntegrationEvent = AnyIntegrationEvent,
111
+ > { … }
112
+ ```
113
+
114
+ | Parameter | What it is | Constraint |
115
+ | --- | --- | --- |
116
+ | `Event` | The domain events it translates: one class, or a union. | extends `DomainEvent` |
117
+ | `Output` | The integration events it produces, declared in the published language. | extends `IntegrationEvent`; any by default |
118
+
119
+ ### `source` <Badge type="info" text="protected · abstract · readonly" /> <Badge type="tip" text="you implement it" />
120
+
121
+ ```ts
122
+ protected abstract readonly source: string
123
+ ```
124
+
125
+ The bounded context the events come from, such as `"ordering"`. `wrap` copies it into every
126
+ integration event.
127
+
128
+ ### `translate(event, context)` <Badge type="info" text="abstract" /> <Badge type="tip" text="you implement it" /> <Badge type="tip" text="called by the command handler" />
129
+
130
+ ```ts
131
+ abstract translate(
132
+ event: Event,
133
+ context: IntegrationEventContext,
134
+ ): Output
135
+ ```
136
+
137
+ Turns one domain event into its integration event. The command handler calls it for each pulled
138
+ event, before adding the result to the outbox.
139
+
140
+ ### `wrap(event, context, contract)` <Badge type="info" text="protected" /> <Badge type="tip" text="inside your methods" />
141
+
142
+ ```ts
143
+ protected wrap<Type extends string, Payload extends JsonValue>(
144
+ event: Event,
145
+ context: IntegrationEventContext,
146
+ contract: {
147
+ readonly type: Type;
148
+ readonly version: number;
149
+ readonly payload: Payload;
150
+ },
151
+ ): IntegrationEvent<Type, Payload>
152
+ ```
153
+
154
+ Builds the integration event from the contract: copies `id` and `occurredAt` from the domain
155
+ event, and adds `source`, `correlationId` and, when the context has one, `causationId`.
156
+
157
+ ::: warning Caveats
158
+ - `occurredAt` becomes an ISO 8601 string, and `causationId` is only set when the context has one.
159
+ - The payload type is constrained to `JsonValue`: an `Identifier` or a `Money` does not compile.
160
+ - The translator lives in the application layer: it reads the domain and the published language,
161
+ and the domain never imports it.
162
+ :::
163
+
164
+ ## Usage
165
+
166
+ Build the translator of the `Order` aggregate. Each step shows the whole file: added lines are highlighted, replaced lines are struck out.
167
+
168
+ <div class="al-cards">
169
+ <div class="al-card"><span class="al-card-title"><span class="al-card-step">1</span><a href="#_1-declare-the-representation">Declare the representation</a></span>Know what you publish.</div>
170
+ <div class="al-card"><span class="al-card-title"><span class="al-card-step">2</span><a href="#_2-declare-the-translator">Declare the translator</a></span>One translator per aggregate.</div>
171
+ <div class="al-card"><span class="al-card-title"><span class="al-card-step">3</span><a href="#_3-wrap-the-event">Wrap the event</a></span>Let wrap fill the envelope.</div>
172
+ <div class="al-card"><span class="al-card-title"><span class="al-card-step">4</span><a href="#_4-publish-what-consumers-need">Publish what consumers need</a></span>Add the fields other contexts read.</div>
173
+ <div class="al-card"><span class="al-card-title"><span class="al-card-step">5</span><a href="#_5-call-it-from-the-handler">Call it from the handler</a></span>Translate after saving.</div>
174
+ <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>
175
+ </div>
176
+
177
+ ### 1. Declare the representation
178
+
179
+ The translator produces an [integration event](./integration-events.md) declared in the published language of the context: `OrderPlacedRepresentation`. Declare it first.
180
+
181
+ ### 2. Declare the translator
182
+
183
+ The translator extends `EventTranslator` with the domain events it reads and the representations it writes. `source` names the context that emits them, once for all its events.
184
+
185
+ ```ts [src/ordering/application/translators/order-events.translator.ts]
186
+ import { EventTranslator } from "@alveolus/core";
187
+
188
+ import type {
189
+ OrderPlaced,
190
+ } from "../../domain/events/order-placed.event";
191
+ import type {
192
+ OrderPlacedRepresentation,
193
+ } from "../../published-language/order-placed.representation";
194
+
195
+ export class OrderEventsTranslator extends EventTranslator<
196
+ OrderPlaced,
197
+ OrderPlacedRepresentation
198
+ > {
199
+ protected readonly source = "ordering";
200
+ }
201
+ ```
202
+
203
+ TypeScript now asks for `translate()`: the next step adds it.
204
+
205
+ ### 3. Wrap the event
206
+
207
+ So that every event carries the same envelope, `translate` gives `wrap` only the name, the version and the payload: `wrap` fills the id, the source, the date and the correlation. The payload holds plain values, never an identifier or a value object.
208
+
209
+ ```ts [src/ordering/application/translators/order-events.translator.ts]
210
+ import { EventTranslator } from "@alveolus/core"; // [!code --]
211
+ import { // [!code ++]
212
+ EventTranslator, // [!code ++]
213
+ type IntegrationEventContext, // [!code ++]
214
+ } from "@alveolus/core"; // [!code ++]
215
+
216
+ import type {
217
+ OrderPlaced,
218
+ } from "../../domain/events/order-placed.event";
219
+ import type {
220
+ OrderPlacedRepresentation,
221
+ } from "../../published-language/order-placed.representation";
222
+
223
+ export class OrderEventsTranslator extends EventTranslator<
224
+ OrderPlaced,
225
+ OrderPlacedRepresentation
226
+ > {
227
+ protected readonly source = "ordering";
228
+
229
+ translate( // [!code ++]
230
+ event: OrderPlaced, // [!code ++]
231
+ context: IntegrationEventContext, // [!code ++]
232
+ ): OrderPlacedRepresentation { // [!code ++]
233
+ return this.wrap(event, context, { // [!code ++]
234
+ type: "OrderPlaced", // [!code ++]
235
+ version: 1, // [!code ++]
236
+ payload: { // [!code ++]
237
+ orderId: event.aggregateId.value, // [!code ++]
238
+ }, // [!code ++]
239
+ }); // [!code ++]
240
+ } // [!code ++]
241
+ }
242
+ ```
243
+
244
+ ### 4. Publish what consumers need
245
+
246
+ A consumer cannot load the order: everything it needs must be in the payload. Add the customer, as a plain string read from the domain event.
247
+
248
+ ```ts [src/ordering/application/translators/order-events.translator.ts]
249
+ import {
250
+ EventTranslator,
251
+ type IntegrationEventContext,
252
+ } from "@alveolus/core";
253
+
254
+ import type {
255
+ OrderPlaced,
256
+ } from "../../domain/events/order-placed.event";
257
+ import type {
258
+ OrderPlacedRepresentation,
259
+ } from "../../published-language/order-placed.representation";
260
+
261
+ export class OrderEventsTranslator extends EventTranslator<
262
+ OrderPlaced,
263
+ OrderPlacedRepresentation
264
+ > {
265
+ protected readonly source = "ordering";
266
+
267
+ translate(
268
+ event: OrderPlaced,
269
+ context: IntegrationEventContext,
270
+ ): OrderPlacedRepresentation {
271
+ return this.wrap(event, context, {
272
+ type: "OrderPlaced",
273
+ version: 1,
274
+ payload: {
275
+ orderId: event.aggregateId.value,
276
+ customerId: event.payload.customerId, // [!code ++]
277
+ },
278
+ });
279
+ }
280
+ }
281
+ ```
282
+
283
+ This is the complete translator.
284
+
285
+ ### 5. Call it from the handler
286
+
287
+ The [command handler](./command-handlers.md#usage) translates each event it pulls from the order, after saving it, and adds the results to the [outbox](./outbox.md).
288
+
289
+ ```ts [src/ordering/application/commands/place-order.command.ts]
290
+ await this.orders.save(order);
291
+ const events = order
292
+ .pullDomainEvents()
293
+ .map((event) =>
294
+ this.translator.translate(event, {
295
+ correlationId: orderId,
296
+ }),
297
+ );
298
+ await this.outbox.add(events);
299
+ ```
300
+
301
+ ### 6. Check it
302
+
303
+ Run the checks. Two rules keep the translator the way it is now:
304
+
305
+ ```sh
306
+ npx alveolus arch check
307
+ ```
308
+
309
+ <div class="al-cards">
310
+ <div class="al-card"><span class="al-card-title"><a href="../../rules/tactical/no-misplaced-class"><code>no-misplaced-class</code></a></span>It stays alone in <code>application/translators/*.translator.ts</code>.</div>
311
+ <div class="al-card"><span class="al-card-title"><a href="../../rules/layers/no-outward-import"><code>no-outward-import</code></a></span>It imports the domain, the application and its own published language, never an adapter.</div>
312
+ </div>
313
+
314
+ An import of a database adapter is reported:
315
+
316
+ ```
317
+ src/ordering/application/translators/order-events.translator.ts
318
+ 3 error layers/no-outward-import: The application layer imports
319
+ src/ordering/driven/pg/adapters/pg-orders.adapter.ts
320
+ (ordering driven): it may only import domain, application,
321
+ published-language.
322
+ ```
323
+
324
+ ## See also
325
+
326
+ - [Integration events](./integration-events.md), what it produces
327
+ - [Domain events](../domain/domain-events.md), what it reads
328
+ - [Published Language](../strategic/published-language.md), where the contracts are declared
329
+ - [Outbox](./outbox.md), where the translated events go