tshex-cli 1.0.17 → 1.0.19
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 +1239 -0
- package/package.json +8 -10
- package/templates/lib/shared/application/data/drivers.ts +15 -0
- package/templates/lib/shared/application/data/managers.ts +81 -0
- package/templates/lib/shared/application/data/repositories.ts +29 -0
- package/templates/lib/shared/application/events.ts +5 -91
- package/templates/lib/shared/application/http.ts +28 -41
- package/templates/lib/shared/application/loggers.ts +2 -1
- package/templates/lib/shared/application/services.ts +2 -38
- package/templates/lib/shared/application/validations.ts +0 -4
- package/templates/lib/shared/domain/aggregates.ts +2 -39
- package/templates/lib/shared/domain/entities.ts +8 -37
- package/templates/lib/shared/domain/errors.ts +1 -37
- package/templates/lib/shared/domain/value-objects.ts +9 -91
- package/readme.md +0 -217
- package/templates/lib/shared/application/databases.ts +0 -102
package/README.md
ADDED
|
@@ -0,0 +1,1239 @@
|
|
|
1
|
+
# Hexagonal Architecture CLI `tshex-cli`
|
|
2
|
+
|
|
3
|
+
`tshex-cli` creates the base structure of a library organized by contexts. The structure groups shared contracts, domain concepts, use cases, and adapters into directories with defined responsibilities.
|
|
4
|
+
|
|
5
|
+
In this guide, we will build a library named `core` with a context named `users`. The walkthrough starts with the CLI and then explains the purpose of the generated components.
|
|
6
|
+
|
|
7
|
+
The examples in this guide are intentionally simple. They are designed to show the responsibility of each component, not to cover real infrastructure or production scenarios.
|
|
8
|
+
|
|
9
|
+
## Why?
|
|
10
|
+
|
|
11
|
+
Hexagonal architecture is a software design pattern that separates the core domain of the application from the external dependencies. This separation is achieved by dividing the application into layers. Each layer has a specific responsibility and interacts with the other layers in a specific way.
|
|
12
|
+
|
|
13
|
+
The goal is to separate the domain code from installed dependencies. A Hexagonal Architecture framework would work against that goal, since it couples the domain code to the framework itself. That is why this CLI exists: it scaffolds the structure for you, generating a codebase you own and control, instead of a framework.
|
|
14
|
+
|
|
15
|
+
## Getting started
|
|
16
|
+
|
|
17
|
+
### Installation
|
|
18
|
+
|
|
19
|
+
Install the package in your Node.js project:
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
npm install tshex-cli
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Or install it globally:
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
npm install -g tshex-cli
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
The package registers the `tshex` executable. You can invoke it with `npx` from the project directory.
|
|
32
|
+
|
|
33
|
+
### Help
|
|
34
|
+
|
|
35
|
+
View the available commands and options with:
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
npx tshex --help
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
### Create a library
|
|
42
|
+
|
|
43
|
+
The `--lib` option receives the name of the library's root directory:
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
npx tshex --lib core
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
In this example, `core` will contain the shared code, the contexts, and the library's main implementation.
|
|
50
|
+
|
|
51
|
+
### Create a context
|
|
52
|
+
|
|
53
|
+
The `--ctx` option receives the context name:
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
npx tshex --ctx users
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
A context groups the rules and operations of an application capability. `users`, `sales`, `billing`, and `inventory` are examples of contexts.
|
|
60
|
+
|
|
61
|
+
### Create the library and the first context
|
|
62
|
+
|
|
63
|
+
You can generate both components in a single execution:
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
npx tshex --lib core --ctx users
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
The command creates this structure:
|
|
70
|
+
|
|
71
|
+
```text
|
|
72
|
+
core/
|
|
73
|
+
├── index.d.ts
|
|
74
|
+
├── main.ts
|
|
75
|
+
├── shared/
|
|
76
|
+
│ ├── application/
|
|
77
|
+
│ │ ├── data/
|
|
78
|
+
│ │ │ ├── drivers.ts
|
|
79
|
+
│ │ │ ├── managers.ts
|
|
80
|
+
│ │ │ └── repositories.ts
|
|
81
|
+
│ │ ├── events.ts
|
|
82
|
+
│ │ ├── http.ts
|
|
83
|
+
│ │ ├── loggers.ts
|
|
84
|
+
│ │ ├── services.ts
|
|
85
|
+
│ │ └── validations.ts
|
|
86
|
+
│ └── domain/
|
|
87
|
+
│ ├── aggregates.ts
|
|
88
|
+
│ ├── entities.ts
|
|
89
|
+
│ ├── errors.ts
|
|
90
|
+
│ └── value-objects.ts
|
|
91
|
+
└── users/
|
|
92
|
+
├── adapters/
|
|
93
|
+
├── application/
|
|
94
|
+
├── domain/
|
|
95
|
+
└── example-ports.ts
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
### Choose the destination directory
|
|
99
|
+
|
|
100
|
+
The `--dir` option specifies the directory from which the structure is created:
|
|
101
|
+
|
|
102
|
+
```bash
|
|
103
|
+
npx tshex --dir ./src --lib core --ctx users
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
The example library is created at `src/core`.
|
|
107
|
+
|
|
108
|
+
To add a context to an existing library, use the library as the destination directory:
|
|
109
|
+
|
|
110
|
+
```bash
|
|
111
|
+
npx tshex --dir ./core --ctx billing
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
The context is created at `core/billing`.
|
|
115
|
+
|
|
116
|
+
### Create a context for React
|
|
117
|
+
|
|
118
|
+
The `--react` option creates a context with directories intended for a React application:
|
|
119
|
+
|
|
120
|
+
```bash
|
|
121
|
+
npx tshex --ctx users --react
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
You can also use its short form:
|
|
125
|
+
|
|
126
|
+
```bash
|
|
127
|
+
npx tshex --ctx users -R
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
The context structure includes adapters for APIs, hooks, and schemas, together with application, domain, and language resources:
|
|
131
|
+
|
|
132
|
+
```text
|
|
133
|
+
users/
|
|
134
|
+
├── adapters/
|
|
135
|
+
│ ├── api/
|
|
136
|
+
│ ├── hooks/
|
|
137
|
+
│ └── schemas/
|
|
138
|
+
├── application/
|
|
139
|
+
├── domain/
|
|
140
|
+
└── languages/
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
## Library structure
|
|
144
|
+
|
|
145
|
+
The library is divided into a root, a shared directory, and one or more contexts. Each level has a role in the implementation.
|
|
146
|
+
|
|
147
|
+
### Root
|
|
148
|
+
|
|
149
|
+
The root contains `index.d.ts` and `main.ts`.
|
|
150
|
+
|
|
151
|
+
`index.d.ts` declares the types available to the library. `main.ts` starts as a placeholder for the main implementation and the public components that belong directly to that entry point.
|
|
152
|
+
|
|
153
|
+
### Shared code
|
|
154
|
+
|
|
155
|
+
The `shared` directory contains code used by multiple contexts. It holds abstractions, interfaces, contracts, base types, and specific implementations with a common meaning across the library.
|
|
156
|
+
|
|
157
|
+
```text
|
|
158
|
+
shared/
|
|
159
|
+
├── domain/
|
|
160
|
+
└── application/
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
`shared/domain` contains foundations for modeling business concepts. `shared/application` contains contracts for coordinating use cases, data sources, events, validations, HTTP responses, and logs.
|
|
164
|
+
|
|
165
|
+
### Contexts
|
|
166
|
+
|
|
167
|
+
A context represents an application capability and groups its vocabulary, rules, and operations.
|
|
168
|
+
|
|
169
|
+
```text
|
|
170
|
+
users/
|
|
171
|
+
billing/
|
|
172
|
+
sales/
|
|
173
|
+
inventory/
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
Separating by context organizes an application around its capabilities. Multiple contexts can be part of the same project, process, and data source. Each context preserves its own rules while sharing the general abstractions in `shared`.
|
|
177
|
+
|
|
178
|
+
Each generated context contains three directories:
|
|
179
|
+
|
|
180
|
+
```text
|
|
181
|
+
domain/
|
|
182
|
+
application/
|
|
183
|
+
adapters/
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
#### `domain`
|
|
187
|
+
|
|
188
|
+
Contains the context's capabilities. A capability groups business knowledge that can be used in different processes: validating an email address, identifying an entity, calculating a price, changing an order's status, or grouping the parts of a sale.
|
|
189
|
+
|
|
190
|
+
Value objects, entities, and aggregates materialize these capabilities through data, rules, and behavior.
|
|
191
|
+
|
|
192
|
+
#### `application`
|
|
193
|
+
|
|
194
|
+
Contains the application of domain capabilities in processes that fulfill system purposes.
|
|
195
|
+
|
|
196
|
+
An application service combines capabilities to complete an operation. For example, the process of registering a user can validate the email address, construct the entity, save its data, publish an event, and log the result.
|
|
197
|
+
|
|
198
|
+
#### Context root
|
|
199
|
+
|
|
200
|
+
The generated context root contains `example-ports.ts` as a starting point for the context's ports. You can keep that file, replace it, or add `index.ts` and other `.ts` files at the context root as the communication surface grows.
|
|
201
|
+
|
|
202
|
+
A port describes a form of communication between the context and another system. It defines the received data, the returned data, and the operation available at that boundary.
|
|
203
|
+
|
|
204
|
+
#### `adapters`
|
|
205
|
+
|
|
206
|
+
Contains the integrations that connect the context with other systems. An adapter imports a port, implements the communication defined by that port, and connects an external input or output to an application process.
|
|
207
|
+
|
|
208
|
+
An adapter can integrate an HTTP controller, a message consumer, an SDK, a driver, a remote client, an event bus, or a logging provider.
|
|
209
|
+
|
|
210
|
+
### Layered architecture
|
|
211
|
+
|
|
212
|
+
The structure is organized into three levels: capabilities, processes, and communication.
|
|
213
|
+
|
|
214
|
+
```text
|
|
215
|
+
External system
|
|
216
|
+
↕
|
|
217
|
+
Port + adapter
|
|
218
|
+
↓
|
|
219
|
+
Application
|
|
220
|
+
↓
|
|
221
|
+
Domain
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
The domain occupies the inner layer and contains the context's capabilities.
|
|
225
|
+
|
|
226
|
+
The application occupies the middle layer and uses those capabilities to build processes that fulfill system purposes.
|
|
227
|
+
|
|
228
|
+
Ports and adapters occupy the outer layer and handle communication between systems.
|
|
229
|
+
|
|
230
|
+
A port declares the communication available at the context boundary: what data enters, what data leaves, and which operation is exposed. Ports are declared in `.ts` modules located at the context root. Many projects centralize them in `index.ts`, but the generated template starts with `example-ports.ts`.
|
|
231
|
+
|
|
232
|
+
An adapter implements that communication. It imports the corresponding port, translates external input or output into the application process format, and delegates the work to the application service.
|
|
233
|
+
|
|
234
|
+
The import direction follows this path:
|
|
235
|
+
|
|
236
|
+
```text
|
|
237
|
+
adapter → port
|
|
238
|
+
adapter → application
|
|
239
|
+
application → domain
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
One possible arrangement is to keep `users/example-ports.ts` at the context root and replace the starter export with your own contracts as the context grows. In this guide, the examples stay close to the generated files: value objects extend `ValueObject`, entities extend `Entity`, services extend `Service`, repositories extend `Repository`, and ports live at the context root.
|
|
243
|
+
|
|
244
|
+
> **Tip:** start the implementation inside the context and move a component to `shared` when its meaning and use belong to multiple contexts.
|
|
245
|
+
|
|
246
|
+
## Library types
|
|
247
|
+
|
|
248
|
+
### `index.d.ts`
|
|
249
|
+
|
|
250
|
+
The file declares the generic type:
|
|
251
|
+
|
|
252
|
+
```ts
|
|
253
|
+
type Generic<T = unknown> = Record<string, T>
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
`Generic<T>` represents an object with `string` keys and values of a common type.
|
|
257
|
+
|
|
258
|
+
```ts
|
|
259
|
+
const filters: Generic<string> = {
|
|
260
|
+
status: 'active'
|
|
261
|
+
}
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
When the type is omitted, the values use `unknown`:
|
|
265
|
+
|
|
266
|
+
```ts
|
|
267
|
+
const metadata: Generic = {
|
|
268
|
+
retries: 2
|
|
269
|
+
}
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
Data source contracts use this form to represent plain objects whose specific structure will be defined by each implementation.
|
|
273
|
+
|
|
274
|
+
## Shared domain
|
|
275
|
+
|
|
276
|
+
The domain starts with small concepts and progresses toward structures that combine multiple identities. We will begin with value objects, continue with entities, and finish with aggregates.
|
|
277
|
+
|
|
278
|
+
### Value objects
|
|
279
|
+
|
|
280
|
+
A value object represents a concept with its own rules, semantics, or behavior. Its identity is determined by its value.
|
|
281
|
+
|
|
282
|
+
The `shared/domain/value-objects.ts` file generates the `ValueObject<T>` base class and the `Email` and `NullableBoolean` implementations.
|
|
283
|
+
|
|
284
|
+
#### Create an email address
|
|
285
|
+
|
|
286
|
+
`Email` turns text into a domain concept with validation and its own operations:
|
|
287
|
+
|
|
288
|
+
```ts
|
|
289
|
+
import { Email } from './core/shared/domain/value-objects.js'
|
|
290
|
+
|
|
291
|
+
const email = Email.from('ana@example.com')
|
|
292
|
+
|
|
293
|
+
email.value
|
|
294
|
+
email.username
|
|
295
|
+
email.domain
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
Creation is performed with `Email.from()`. This method runs `Email.isValid()` before constructing the instance.
|
|
299
|
+
|
|
300
|
+
```ts
|
|
301
|
+
Email.isValid('ana@example.com')
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
Every creation follows the same validity rule:
|
|
305
|
+
|
|
306
|
+
```text
|
|
307
|
+
received value
|
|
308
|
+
↓
|
|
309
|
+
isValid(value)
|
|
310
|
+
↓
|
|
311
|
+
from(value)
|
|
312
|
+
↓
|
|
313
|
+
valid instance
|
|
314
|
+
```
|
|
315
|
+
|
|
316
|
+
When validation fails, `from()` throws `ValueError`.
|
|
317
|
+
|
|
318
|
+
> **Tip:** use a native type when it fully expresses the data. Create a value object when the concept provides its own rules or operations. A text identifier can be represented with `string`; an email address benefits from `Email` because it includes validation and behavior.
|
|
319
|
+
|
|
320
|
+
#### Represent a nullable Boolean state
|
|
321
|
+
|
|
322
|
+
`NullableBoolean` models the values `true`, `false`, and `null`:
|
|
323
|
+
|
|
324
|
+
```ts
|
|
325
|
+
import { NullableBoolean } from './core/shared/domain/value-objects.js'
|
|
326
|
+
|
|
327
|
+
const active = NullableBoolean.from(null)
|
|
328
|
+
|
|
329
|
+
active.value
|
|
330
|
+
active.isIndeterminate()
|
|
331
|
+
```
|
|
332
|
+
|
|
333
|
+
The `isIndeterminate()` method expresses an operation specific to the concept and allows the `null` state to be checked with explicit intent.
|
|
334
|
+
|
|
335
|
+
#### Implement a value object
|
|
336
|
+
|
|
337
|
+
We will create a user name. The example has one rule and one small operation so the focus stays on the value object itself.
|
|
338
|
+
|
|
339
|
+
```ts
|
|
340
|
+
import { ValueError } from '../../shared/domain/errors.js'
|
|
341
|
+
import { ValueObject } from '../../shared/domain/value-objects.js'
|
|
342
|
+
|
|
343
|
+
export class UserName extends ValueObject<string> {
|
|
344
|
+
public override readonly value: string
|
|
345
|
+
|
|
346
|
+
protected constructor(value: string) {
|
|
347
|
+
super()
|
|
348
|
+
this.value = value
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
public override equals(
|
|
352
|
+
other: UserName | null | undefined
|
|
353
|
+
): boolean {
|
|
354
|
+
return other instanceof UserName &&
|
|
355
|
+
this.value === other.value
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
public normalized(): string {
|
|
359
|
+
return this.value.trim().toLowerCase()
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
public static override isValid(value: unknown): boolean {
|
|
363
|
+
if (super.isValid(value) === false) {
|
|
364
|
+
return false
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
return typeof value === 'string' &&
|
|
368
|
+
value.trim().length > 0
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
public static from(value: string): UserName {
|
|
372
|
+
if (this.isValid(value) === false) {
|
|
373
|
+
throw new ValueError(value, this.name)
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
return new this(value)
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
```
|
|
380
|
+
|
|
381
|
+
Now we can create and use the concept:
|
|
382
|
+
|
|
383
|
+
```ts
|
|
384
|
+
const name = UserName.from('Ana')
|
|
385
|
+
const normalized = name.normalized()
|
|
386
|
+
```
|
|
387
|
+
|
|
388
|
+
`isValid()` centralizes the rule. `from()` creates the valid instance. `normalized()` adds behavior specific to the concept.
|
|
389
|
+
|
|
390
|
+
### Entities
|
|
391
|
+
|
|
392
|
+
An entity represents a concept with its own identity. Two instances represent the same element when they share that identity.
|
|
393
|
+
|
|
394
|
+
The `shared/domain/entities.ts` file generates the `Entity` base class. Each entity implements `equals()`. The inherited `toJSON()` method can be overridden when you want to return a plain object.
|
|
395
|
+
|
|
396
|
+
This example creates an entity for the `users` context.
|
|
397
|
+
|
|
398
|
+
```ts
|
|
399
|
+
import { Entity } from '../../shared/domain/entities.js'
|
|
400
|
+
import {
|
|
401
|
+
Email,
|
|
402
|
+
NullableBoolean
|
|
403
|
+
} from '../../shared/domain/value-objects.js'
|
|
404
|
+
|
|
405
|
+
type UserName = {
|
|
406
|
+
value: string
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
export class User extends Entity {
|
|
410
|
+
public constructor(
|
|
411
|
+
public readonly id: string,
|
|
412
|
+
public readonly name: UserName,
|
|
413
|
+
public readonly email: Email,
|
|
414
|
+
public readonly active: NullableBoolean
|
|
415
|
+
) {
|
|
416
|
+
super()
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
public override equals(other: Entity): boolean {
|
|
420
|
+
return other instanceof User && other.id === this.id
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
public override toJSON(): Record<string, unknown> {
|
|
424
|
+
return {
|
|
425
|
+
id: this.id,
|
|
426
|
+
name: this.name.value,
|
|
427
|
+
email: this.email.value,
|
|
428
|
+
active: this.active.value
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
```
|
|
433
|
+
|
|
434
|
+
The `id` property defines the identity. The `equals()` method compares entities using that property.
|
|
435
|
+
|
|
436
|
+
`toJSON()` produces a plain representation of the entity:
|
|
437
|
+
|
|
438
|
+
```ts
|
|
439
|
+
const user = new User(
|
|
440
|
+
'user-1',
|
|
441
|
+
UserName.from('Ana'),
|
|
442
|
+
Email.from('ana@example.com'),
|
|
443
|
+
NullableBoolean.from(true)
|
|
444
|
+
)
|
|
445
|
+
|
|
446
|
+
const json = user.toJSON()
|
|
447
|
+
```
|
|
448
|
+
|
|
449
|
+
The result uses the internal values of `UserName`, `Email`, and `NullableBoolean`.
|
|
450
|
+
|
|
451
|
+
### Aggregates
|
|
452
|
+
|
|
453
|
+
An aggregate groups multiple entities into a logical unit. The aggregate's operations depend on all the identities that compose it.
|
|
454
|
+
|
|
455
|
+
A user list can group several users:
|
|
456
|
+
|
|
457
|
+
```text
|
|
458
|
+
UserList
|
|
459
|
+
└── items
|
|
460
|
+
```
|
|
461
|
+
|
|
462
|
+
Each element preserves its own identity within the unit.
|
|
463
|
+
|
|
464
|
+
```ts
|
|
465
|
+
import { Aggregate } from '../../shared/domain/aggregates.js'
|
|
466
|
+
|
|
467
|
+
type User = {
|
|
468
|
+
id: string
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
export class UserList extends Aggregate {
|
|
472
|
+
public constructor(public readonly items: User[]) {
|
|
473
|
+
super()
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
public count(): number {
|
|
477
|
+
return this.items.length
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
```
|
|
481
|
+
|
|
482
|
+
`UserList` groups multiple `User` entities into a single logical unit. The `count()` method operates on that group.
|
|
483
|
+
|
|
484
|
+
### Domain errors
|
|
485
|
+
|
|
486
|
+
The `shared/domain/errors.ts` file generates `ValueError`. This error represents a received value that does not satisfy the expected rule.
|
|
487
|
+
|
|
488
|
+
```ts
|
|
489
|
+
import { ValueError } from '../../shared/domain/errors.js'
|
|
490
|
+
|
|
491
|
+
if (userId.trim().length === 0) {
|
|
492
|
+
throw new ValueError(userId, 'UserId')
|
|
493
|
+
}
|
|
494
|
+
```
|
|
495
|
+
|
|
496
|
+
Because it is in `shared`, `ValueError` can be used from any context and by any domain concept that validates values.
|
|
497
|
+
|
|
498
|
+
## Shared application
|
|
499
|
+
|
|
500
|
+
The application layer coordinates use cases. Its contracts connect the domain with validations, data sources, HTTP responses, logs, and events.
|
|
501
|
+
|
|
502
|
+
### Validations
|
|
503
|
+
|
|
504
|
+
The `shared/application/validations.ts` file declares the `Validatable` contract:
|
|
505
|
+
|
|
506
|
+
```ts
|
|
507
|
+
isValid(): boolean
|
|
508
|
+
```
|
|
509
|
+
|
|
510
|
+
`isValid()` checks whether the data is valid according to the rule defined by the implementation.
|
|
511
|
+
|
|
512
|
+
We will create a validation for the use case that registers users.
|
|
513
|
+
|
|
514
|
+
```ts
|
|
515
|
+
import type { Validatable } from '../../shared/application/validations.js'
|
|
516
|
+
import { Email } from '../../shared/domain/value-objects.js'
|
|
517
|
+
|
|
518
|
+
export type CreateUserValidationData = {
|
|
519
|
+
name: string
|
|
520
|
+
email: string
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
export class CreateUserValidation implements Validatable {
|
|
524
|
+
public constructor(
|
|
525
|
+
private readonly data: CreateUserValidationData
|
|
526
|
+
) {}
|
|
527
|
+
|
|
528
|
+
public isValid(): boolean {
|
|
529
|
+
return this.data.name.trim().length > 0 &&
|
|
530
|
+
Email.isValid(this.data.email)
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
```
|
|
534
|
+
|
|
535
|
+
In this example, the application service runs this validation before constructing the `User` entity.
|
|
536
|
+
|
|
537
|
+
### Services
|
|
538
|
+
|
|
539
|
+
Services represent application use cases. Each service applies domain capabilities in a process that fulfills a purpose, such as creating a user, confirming an order, or recording a payment.
|
|
540
|
+
|
|
541
|
+
The `shared/application/services.ts` file generates the `Service` base class. A concrete implementation defines its inputs, dependencies, and the method that executes the process.
|
|
542
|
+
|
|
543
|
+
We will begin with a service that creates a user.
|
|
544
|
+
|
|
545
|
+
```ts
|
|
546
|
+
import type { Validatable } from '../../shared/application/validations.js'
|
|
547
|
+
import { Service } from '../../shared/application/services.js'
|
|
548
|
+
import {
|
|
549
|
+
Email,
|
|
550
|
+
NullableBoolean
|
|
551
|
+
} from '../../shared/domain/value-objects.js'
|
|
552
|
+
|
|
553
|
+
export type CreateUserData = {
|
|
554
|
+
id: string
|
|
555
|
+
name: string
|
|
556
|
+
email: string
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
export class UserName {
|
|
560
|
+
public constructor(public readonly value: string) {}
|
|
561
|
+
|
|
562
|
+
public static from(value: string): UserName {
|
|
563
|
+
return new UserName(value.trim())
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
export class User {
|
|
568
|
+
public constructor(
|
|
569
|
+
public readonly id: string,
|
|
570
|
+
public readonly name: UserName,
|
|
571
|
+
public readonly email: Email,
|
|
572
|
+
public readonly active: NullableBoolean
|
|
573
|
+
) {}
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
export class CreateUserValidation implements Validatable {
|
|
577
|
+
public constructor(
|
|
578
|
+
private readonly data: CreateUserData
|
|
579
|
+
) {}
|
|
580
|
+
|
|
581
|
+
public isValid(): boolean {
|
|
582
|
+
return this.data.name.trim().length > 0 &&
|
|
583
|
+
Email.isValid(this.data.email)
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
export interface UserWriter {
|
|
588
|
+
save(data: { user: User }): Promise<void>
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
export type CreateUserResult = {
|
|
592
|
+
user: {
|
|
593
|
+
id: string
|
|
594
|
+
name: string
|
|
595
|
+
email: string
|
|
596
|
+
active: boolean | null
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
export class CreateUserService extends Service {
|
|
601
|
+
public constructor(private readonly users: UserWriter) {
|
|
602
|
+
super()
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
public async execute(data: CreateUserData): Promise<CreateUserResult> {
|
|
606
|
+
const validation = new CreateUserValidation(data)
|
|
607
|
+
|
|
608
|
+
if (validation.isValid() === false) {
|
|
609
|
+
throw new Error('The name or email is invalid.')
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
const user = new User(
|
|
613
|
+
data.id,
|
|
614
|
+
UserName.from(data.name),
|
|
615
|
+
Email.from(data.email),
|
|
616
|
+
NullableBoolean.from(true)
|
|
617
|
+
)
|
|
618
|
+
|
|
619
|
+
await this.users.save({ user })
|
|
620
|
+
|
|
621
|
+
return {
|
|
622
|
+
user: {
|
|
623
|
+
id: user.id,
|
|
624
|
+
name: user.name.value,
|
|
625
|
+
email: user.email.value,
|
|
626
|
+
active: user.active.value
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
```
|
|
632
|
+
|
|
633
|
+
In this example, the service receives one object, validates it, constructs the entity, delegates persistence, and returns another object with the result.
|
|
634
|
+
|
|
635
|
+
### HTTP responses
|
|
636
|
+
|
|
637
|
+
The `shared/application/http.ts` file generates lightweight HTTP contracts: `HttpRequest`, `HttpResponse`, `HttpResponseBody`, `HttpRequestHandler`, and `HttpMiddleware`.
|
|
638
|
+
|
|
639
|
+
`HttpResponseBody` is an interface organized into three properties:
|
|
640
|
+
|
|
641
|
+
```ts
|
|
642
|
+
import type { HttpResponseBody } from '../../shared/application/http.js'
|
|
643
|
+
|
|
644
|
+
const body: HttpResponseBody = {
|
|
645
|
+
data,
|
|
646
|
+
errors,
|
|
647
|
+
links
|
|
648
|
+
}
|
|
649
|
+
```
|
|
650
|
+
|
|
651
|
+
`data` contains the operation data and accepts `null` when the response has no data:
|
|
652
|
+
|
|
653
|
+
```ts
|
|
654
|
+
const body: HttpResponseBody = {
|
|
655
|
+
data: {
|
|
656
|
+
id: 'user-1',
|
|
657
|
+
name: 'Ana'
|
|
658
|
+
},
|
|
659
|
+
errors: null,
|
|
660
|
+
links: null
|
|
661
|
+
}
|
|
662
|
+
```
|
|
663
|
+
|
|
664
|
+
`errors` contains a list of messages:
|
|
665
|
+
|
|
666
|
+
```ts
|
|
667
|
+
const body: HttpResponseBody = {
|
|
668
|
+
data: null,
|
|
669
|
+
errors: ['The email is invalid.'],
|
|
670
|
+
links: null
|
|
671
|
+
}
|
|
672
|
+
```
|
|
673
|
+
|
|
674
|
+
`links` contains HATEOAS links related to the resource and its available operations:
|
|
675
|
+
|
|
676
|
+
```ts
|
|
677
|
+
const body: HttpResponseBody = {
|
|
678
|
+
data: {
|
|
679
|
+
id: 'user-1',
|
|
680
|
+
name: 'Ana'
|
|
681
|
+
},
|
|
682
|
+
errors: null,
|
|
683
|
+
links: {
|
|
684
|
+
self: new URL('https://api.example.com/users/user-1')
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
```
|
|
688
|
+
|
|
689
|
+
An entity's plain output can be used as `data`:
|
|
690
|
+
|
|
691
|
+
```ts
|
|
692
|
+
const body: HttpResponseBody = {
|
|
693
|
+
data: user.toJSON(),
|
|
694
|
+
errors: null,
|
|
695
|
+
links: null
|
|
696
|
+
}
|
|
697
|
+
```
|
|
698
|
+
|
|
699
|
+
`HttpRequestHandler` and `HttpMiddleware` define the contracts for adapters that receive a request, delegate to a handler, and produce a response.
|
|
700
|
+
|
|
701
|
+
### Logs
|
|
702
|
+
|
|
703
|
+
The `shared/application/loggers.ts` file declares the `Logger` contract. The application uses this abstraction to produce logs that an adapter sends to external services.
|
|
704
|
+
|
|
705
|
+
The contract includes the `debug`, `info`, `warning`, `error`, and `critical` levels, together with the numeric constants `DEBUG`, `INFO`, `WARNING`, `ERROR`, and `CRITICAL`.
|
|
706
|
+
|
|
707
|
+
We will create a simple adapter that connects the contract to the console.
|
|
708
|
+
|
|
709
|
+
```ts
|
|
710
|
+
import { Logger } from '../../shared/application/loggers.js'
|
|
711
|
+
|
|
712
|
+
export class ConsoleLoggerAdapter extends Logger {
|
|
713
|
+
public debug(data: unknown): void {
|
|
714
|
+
console.debug(data)
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
public info(data: unknown): void {
|
|
718
|
+
console.info(data)
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
public warning(data: unknown): void {
|
|
722
|
+
console.warn(data)
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
public error(data: unknown): void {
|
|
726
|
+
console.error(data)
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
public critical(data: unknown): void {
|
|
730
|
+
console.error(data)
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
```
|
|
734
|
+
|
|
735
|
+
In this example, the service receives `Logger` as a dependency. The adapter decides where to send each level.
|
|
736
|
+
|
|
737
|
+
### Events
|
|
738
|
+
|
|
739
|
+
The `shared/application/events.ts` file contains the contracts that connect the application to an event bus.
|
|
740
|
+
|
|
741
|
+
The flow starts with an event, continues through the dispatcher, and ends in one or more handlers:
|
|
742
|
+
|
|
743
|
+
```text
|
|
744
|
+
Service
|
|
745
|
+
↓ creates
|
|
746
|
+
Event
|
|
747
|
+
↓ passes to
|
|
748
|
+
EventDispatcher
|
|
749
|
+
↓ publishes to
|
|
750
|
+
Event bus
|
|
751
|
+
↓ executes
|
|
752
|
+
EventHandler
|
|
753
|
+
```
|
|
754
|
+
|
|
755
|
+
#### Event
|
|
756
|
+
|
|
757
|
+
`Event` represents something that occurred in the application. It contains the event time and its plain details.
|
|
758
|
+
|
|
759
|
+
```ts
|
|
760
|
+
import { Event } from '../../shared/application/events.js'
|
|
761
|
+
|
|
762
|
+
export class UserCreated extends Event {
|
|
763
|
+
public constructor(details: { userId: string }) {
|
|
764
|
+
super(Date.now(), details)
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
```
|
|
768
|
+
|
|
769
|
+
#### Handler
|
|
770
|
+
|
|
771
|
+
`EventHandler` represents a reaction to the event.
|
|
772
|
+
|
|
773
|
+
```ts
|
|
774
|
+
import {
|
|
775
|
+
Event,
|
|
776
|
+
EventHandler
|
|
777
|
+
} from '../../shared/application/events.js'
|
|
778
|
+
import { Logger } from '../../shared/application/loggers.js'
|
|
779
|
+
|
|
780
|
+
export class LogUserCreated extends EventHandler {
|
|
781
|
+
public constructor(private readonly logger: Logger) {
|
|
782
|
+
super()
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
public async handle(event: Event): Promise<void> {
|
|
786
|
+
this.logger.info(event.details)
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
```
|
|
790
|
+
|
|
791
|
+
#### Dispatcher
|
|
792
|
+
|
|
793
|
+
`EventDispatcher` represents the interaction contract with the event bus. Its concrete implementation subscribes handlers, removes subscriptions, and dispatches events.
|
|
794
|
+
|
|
795
|
+
```ts
|
|
796
|
+
subscribe(key, handler)
|
|
797
|
+
unsubscribe(key, handler)
|
|
798
|
+
dispatch(event)
|
|
799
|
+
```
|
|
800
|
+
|
|
801
|
+
In one possible implementation, the service can receive `EventDispatcher` and publish `UserCreated` after completing the use case.
|
|
802
|
+
|
|
803
|
+
## Data sources
|
|
804
|
+
|
|
805
|
+
The `shared/application/data/` directory organizes access to a data source into three modules: `drivers.ts`, `managers.ts`, and `repositories.ts`. Together, they define `DriverAdapter`, `DataManager`, `DatasetManager`, and `Repository`.
|
|
806
|
+
|
|
807
|
+
The complete flow looks like this:
|
|
808
|
+
|
|
809
|
+
```text
|
|
810
|
+
DriverAdapter
|
|
811
|
+
↓ connects and enables
|
|
812
|
+
DataManager or DatasetManager
|
|
813
|
+
↓ provides plain data to
|
|
814
|
+
Repository
|
|
815
|
+
↓ transforms
|
|
816
|
+
Domain objects
|
|
817
|
+
↓ used by
|
|
818
|
+
Service
|
|
819
|
+
```
|
|
820
|
+
|
|
821
|
+
We will begin with the connection and proceed to the use case.
|
|
822
|
+
|
|
823
|
+
### Driver adapter
|
|
824
|
+
|
|
825
|
+
`DriverAdapter` connects and disconnects the source through a driver. When the connection is available, `connect()` returns an enabled data manager.
|
|
826
|
+
|
|
827
|
+
```ts
|
|
828
|
+
connect(...args): Promise<DataManager>
|
|
829
|
+
disconnect(): Promise<unknown>
|
|
830
|
+
```
|
|
831
|
+
|
|
832
|
+
We will create an adapter for an in-memory collection of users. This example avoids a database so the focus stays on the adapter's responsibility, not on infrastructure details.
|
|
833
|
+
|
|
834
|
+
```ts
|
|
835
|
+
import { DriverAdapter } from '../../shared/application/data/drivers.js'
|
|
836
|
+
import { DataManager } from '../../shared/application/data/managers.js'
|
|
837
|
+
|
|
838
|
+
type UserRecord = {
|
|
839
|
+
id: string
|
|
840
|
+
name: string
|
|
841
|
+
email: string
|
|
842
|
+
active: boolean | null
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
class UserDataManager extends DataManager<UserRecord> {
|
|
846
|
+
public constructor(private readonly records: UserRecord[]) {
|
|
847
|
+
super()
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
public async all(): Promise<UserRecord[]> {
|
|
851
|
+
return this.records
|
|
852
|
+
}
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
export class UserDriverAdapter
|
|
856
|
+
extends DriverAdapter<UserDataManager> {
|
|
857
|
+
|
|
858
|
+
public constructor(private readonly records: UserRecord[]) {
|
|
859
|
+
super()
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
public async connect(): Promise<UserDataManager> {
|
|
863
|
+
return new UserDataManager(this.records)
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
public async disconnect(): Promise<void> {
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
```
|
|
870
|
+
|
|
871
|
+
The concrete implementation can encapsulate a database driver, an HTTP client, a file system, or another source.
|
|
872
|
+
|
|
873
|
+
### Data manager
|
|
874
|
+
|
|
875
|
+
`DataManager` operates on the source and works with plain objects and arrays. Its base form exposes one abstract operation and one default operation:
|
|
876
|
+
|
|
877
|
+
```ts
|
|
878
|
+
all(): Promise<Array<T>>
|
|
879
|
+
none(): Array<T>
|
|
880
|
+
```
|
|
881
|
+
|
|
882
|
+
`all()` retrieves the available records. `none()` already creates an empty typed collection.
|
|
883
|
+
|
|
884
|
+
First, we will define the shape used by the source:
|
|
885
|
+
|
|
886
|
+
```ts
|
|
887
|
+
type UserRecord = {
|
|
888
|
+
id: string
|
|
889
|
+
name: string
|
|
890
|
+
email: string
|
|
891
|
+
active: boolean | null
|
|
892
|
+
}
|
|
893
|
+
```
|
|
894
|
+
|
|
895
|
+
Now we will implement the data manager.
|
|
896
|
+
|
|
897
|
+
```ts
|
|
898
|
+
import { DataManager } from '../../shared/application/data/managers.js'
|
|
899
|
+
|
|
900
|
+
export type UserRecord = {
|
|
901
|
+
id: string
|
|
902
|
+
name: string
|
|
903
|
+
email: string
|
|
904
|
+
active: boolean | null
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
export class UserDataManager
|
|
908
|
+
extends DataManager<UserRecord> {
|
|
909
|
+
|
|
910
|
+
public constructor(private readonly records: UserRecord[]) {
|
|
911
|
+
super()
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
public async all(): Promise<UserRecord[]> {
|
|
915
|
+
return this.records
|
|
916
|
+
}
|
|
917
|
+
}
|
|
918
|
+
```
|
|
919
|
+
|
|
920
|
+
The data manager reflects the structure of the source. In this example, it only exposes plain records.
|
|
921
|
+
|
|
922
|
+
#### Source operations
|
|
923
|
+
|
|
924
|
+
The file also declares interfaces that extend a data manager's capabilities:
|
|
925
|
+
|
|
926
|
+
| Interface | Operation |
|
|
927
|
+
| --- | --- |
|
|
928
|
+
| `Filterable` | Filters records. |
|
|
929
|
+
| `Sortable` | Sorts records. |
|
|
930
|
+
| `Creatable` | Creates records. |
|
|
931
|
+
| `Updatable` | Updates records. |
|
|
932
|
+
| `Deletable` | Deletes records. |
|
|
933
|
+
| `Aggregatable` | Performs aggregations. |
|
|
934
|
+
| `Relatable` | Selects or preloads relationships. |
|
|
935
|
+
|
|
936
|
+
A data manager can implement the interfaces required by its source:
|
|
937
|
+
|
|
938
|
+
```ts
|
|
939
|
+
import {
|
|
940
|
+
Creatable,
|
|
941
|
+
DataManager,
|
|
942
|
+
Filterable
|
|
943
|
+
} from '../../shared/application/data/managers.js'
|
|
944
|
+
|
|
945
|
+
export class UserDataManager
|
|
946
|
+
extends DataManager<UserRecord>
|
|
947
|
+
implements
|
|
948
|
+
Filterable<Partial<UserRecord>>,
|
|
949
|
+
Creatable<UserRecord> {
|
|
950
|
+
|
|
951
|
+
public constructor(private readonly records: UserRecord[]) {
|
|
952
|
+
super()
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
public async all(): Promise<UserRecord[]> {
|
|
956
|
+
return this.records
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
public async filter(
|
|
960
|
+
selector: Partial<UserRecord>
|
|
961
|
+
): Promise<UserRecord[]> {
|
|
962
|
+
return this.records.filter((record) =>
|
|
963
|
+
(selector.id === undefined || record.id === selector.id) &&
|
|
964
|
+
(selector.email === undefined || record.email === selector.email)
|
|
965
|
+
)
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
public async create(data: UserRecord): Promise<void> {
|
|
969
|
+
this.records.push(data)
|
|
970
|
+
}
|
|
971
|
+
}
|
|
972
|
+
```
|
|
973
|
+
|
|
974
|
+
It can also declare operations specific to source queries:
|
|
975
|
+
|
|
976
|
+
```ts
|
|
977
|
+
public async findByEmail(query: { email: string }): Promise<UserRecord[]> {
|
|
978
|
+
return this.filter({ email: query.email })
|
|
979
|
+
}
|
|
980
|
+
```
|
|
981
|
+
|
|
982
|
+
The application layer decides when to execute these operations, combines their results, and catches errors produced by the source.
|
|
983
|
+
|
|
984
|
+
### Dataset manager
|
|
985
|
+
|
|
986
|
+
`DatasetManager` extends `DataManager` with set operations:
|
|
987
|
+
|
|
988
|
+
```ts
|
|
989
|
+
union()
|
|
990
|
+
intersection()
|
|
991
|
+
difference()
|
|
992
|
+
symmetricDifference()
|
|
993
|
+
complement()
|
|
994
|
+
```
|
|
995
|
+
|
|
996
|
+
This implementation is useful when an operation works with unions, intersections, differences, and complements between data collections.
|
|
997
|
+
|
|
998
|
+
### Repository
|
|
999
|
+
|
|
1000
|
+
`Repository` acts as an intermediary between plain data and domain objects.
|
|
1001
|
+
|
|
1002
|
+
```text
|
|
1003
|
+
UserRecord
|
|
1004
|
+
↓ transform
|
|
1005
|
+
User
|
|
1006
|
+
```
|
|
1007
|
+
|
|
1008
|
+
The generated base class focuses on reading plain data and transforming it into domain objects. Using the `UserDriverAdapter` from the previous example, a repository can stay very small.
|
|
1009
|
+
|
|
1010
|
+
Now we will implement the repository.
|
|
1011
|
+
|
|
1012
|
+
```ts
|
|
1013
|
+
import { DriverAdapter } from '../../shared/application/data/drivers.js'
|
|
1014
|
+
import { DataManager } from '../../shared/application/data/managers.js'
|
|
1015
|
+
import { Repository } from '../../shared/application/data/repositories.js'
|
|
1016
|
+
import {
|
|
1017
|
+
Email,
|
|
1018
|
+
NullableBoolean
|
|
1019
|
+
} from '../../shared/domain/value-objects.js'
|
|
1020
|
+
|
|
1021
|
+
type UserRecord = {
|
|
1022
|
+
id: string
|
|
1023
|
+
name: string
|
|
1024
|
+
email: string
|
|
1025
|
+
active: boolean | null
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
class UserName {
|
|
1029
|
+
public constructor(public readonly value: string) {}
|
|
1030
|
+
|
|
1031
|
+
public static from(value: string): UserName {
|
|
1032
|
+
return new UserName(value)
|
|
1033
|
+
}
|
|
1034
|
+
}
|
|
1035
|
+
|
|
1036
|
+
class User {
|
|
1037
|
+
public constructor(
|
|
1038
|
+
public readonly id: string,
|
|
1039
|
+
public readonly name: UserName,
|
|
1040
|
+
public readonly email: Email,
|
|
1041
|
+
public readonly active: NullableBoolean
|
|
1042
|
+
) {}
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
export class UserRepository
|
|
1046
|
+
extends Repository<UserRecord, User> {
|
|
1047
|
+
|
|
1048
|
+
public constructor(
|
|
1049
|
+
driver: DriverAdapter<DataManager<UserRecord>>
|
|
1050
|
+
) {
|
|
1051
|
+
super(driver)
|
|
1052
|
+
}
|
|
1053
|
+
|
|
1054
|
+
protected override transform(data: UserRecord): User {
|
|
1055
|
+
return new User(
|
|
1056
|
+
data.id,
|
|
1057
|
+
UserName.from(data.name),
|
|
1058
|
+
Email.from(data.email),
|
|
1059
|
+
NullableBoolean.from(data.active)
|
|
1060
|
+
)
|
|
1061
|
+
}
|
|
1062
|
+
}
|
|
1063
|
+
```
|
|
1064
|
+
|
|
1065
|
+
Here, `transform()` converts each record into an entity. The constructor receives any driver compatible with `DataManager<UserRecord>`, and the inherited `all()` method already handles the connect, read, transform, and disconnect flow.
|
|
1066
|
+
|
|
1067
|
+
### Queries and errors in the application
|
|
1068
|
+
|
|
1069
|
+
Application services coordinate queries and catch errors from data sources. The process expresses the collaboration capabilities it needs through application-specific contracts. Adapters materialize those contracts.
|
|
1070
|
+
|
|
1071
|
+
```ts
|
|
1072
|
+
import { Service } from '../../shared/application/services.js'
|
|
1073
|
+
|
|
1074
|
+
type User = {
|
|
1075
|
+
id: string
|
|
1076
|
+
}
|
|
1077
|
+
|
|
1078
|
+
export interface UsersReader {
|
|
1079
|
+
all(): Promise<User[]>
|
|
1080
|
+
}
|
|
1081
|
+
|
|
1082
|
+
export class ListUsersService extends Service {
|
|
1083
|
+
public constructor(private readonly users: UsersReader) {
|
|
1084
|
+
super()
|
|
1085
|
+
}
|
|
1086
|
+
|
|
1087
|
+
public async execute(): Promise<{ users: User[] }> {
|
|
1088
|
+
try {
|
|
1089
|
+
return {
|
|
1090
|
+
users: await this.users.all()
|
|
1091
|
+
}
|
|
1092
|
+
} catch {
|
|
1093
|
+
throw new Error('Could not list users.')
|
|
1094
|
+
}
|
|
1095
|
+
}
|
|
1096
|
+
}
|
|
1097
|
+
```
|
|
1098
|
+
|
|
1099
|
+
`UsersReader` expresses the collaboration required by the process. `UserRepository`, located in `adapters`, already satisfies that collaboration through the inherited `all()` method.
|
|
1100
|
+
|
|
1101
|
+
## Context ports
|
|
1102
|
+
|
|
1103
|
+
Ports describe communication between the context and other systems. Each port defines the shape of an interaction at the boundary: input data, output data, and the available operation.
|
|
1104
|
+
|
|
1105
|
+
The context generates `example-ports.ts` as a root-level starting point. As the context grows, you can add `index.ts` as the main port file or split ports across multiple `.ts` files. Adapters import the ports that define the communication they materialize.
|
|
1106
|
+
|
|
1107
|
+
### Main port
|
|
1108
|
+
|
|
1109
|
+
The generated `example-ports.ts` file is only a placeholder:
|
|
1110
|
+
|
|
1111
|
+
```ts
|
|
1112
|
+
export function example(): void {
|
|
1113
|
+
// ...
|
|
1114
|
+
}
|
|
1115
|
+
```
|
|
1116
|
+
|
|
1117
|
+
You can replace it with your own root-level contract. A minimal option for creating a user is:
|
|
1118
|
+
|
|
1119
|
+
```ts
|
|
1120
|
+
export type CreateUserRequest = {
|
|
1121
|
+
user: {
|
|
1122
|
+
id: string
|
|
1123
|
+
name: string
|
|
1124
|
+
email: string
|
|
1125
|
+
}
|
|
1126
|
+
}
|
|
1127
|
+
|
|
1128
|
+
export type CreateUserResponse = {
|
|
1129
|
+
user: {
|
|
1130
|
+
id: string
|
|
1131
|
+
name: string
|
|
1132
|
+
email: string
|
|
1133
|
+
active: boolean | null
|
|
1134
|
+
}
|
|
1135
|
+
}
|
|
1136
|
+
|
|
1137
|
+
export interface CreateUserPort {
|
|
1138
|
+
create(
|
|
1139
|
+
request: CreateUserRequest
|
|
1140
|
+
): Promise<CreateUserResponse>
|
|
1141
|
+
}
|
|
1142
|
+
```
|
|
1143
|
+
|
|
1144
|
+
`CreateUserRequest` represents the information received from another system. `CreateUserResponse` represents the returned response. `CreateUserPort` defines the operation available at the context boundary.
|
|
1145
|
+
|
|
1146
|
+
### Adapt the port to the application process
|
|
1147
|
+
|
|
1148
|
+
In this example, the adapter imports the port and the service. Its job here is to translate the root-level contract into the application input object and return the service result.
|
|
1149
|
+
|
|
1150
|
+
```ts
|
|
1151
|
+
import type {
|
|
1152
|
+
CreateUserPort,
|
|
1153
|
+
CreateUserRequest,
|
|
1154
|
+
CreateUserResponse
|
|
1155
|
+
} from '../example-ports.js'
|
|
1156
|
+
|
|
1157
|
+
type CreateUserService = {
|
|
1158
|
+
execute(data: {
|
|
1159
|
+
id: string
|
|
1160
|
+
name: string
|
|
1161
|
+
email: string
|
|
1162
|
+
}): Promise<CreateUserResponse>
|
|
1163
|
+
}
|
|
1164
|
+
|
|
1165
|
+
export class CreateUserAdapter implements CreateUserPort {
|
|
1166
|
+
public constructor(
|
|
1167
|
+
private readonly service: CreateUserService
|
|
1168
|
+
) {}
|
|
1169
|
+
|
|
1170
|
+
public async create(
|
|
1171
|
+
request: CreateUserRequest
|
|
1172
|
+
): Promise<CreateUserResponse> {
|
|
1173
|
+
return this.service.execute({
|
|
1174
|
+
id: request.user.id,
|
|
1175
|
+
name: request.user.name,
|
|
1176
|
+
email: request.user.email
|
|
1177
|
+
})
|
|
1178
|
+
}
|
|
1179
|
+
}
|
|
1180
|
+
```
|
|
1181
|
+
|
|
1182
|
+
In this example, the port expresses the communication, the adapter implements it, the service executes the process, and the domain provides the capabilities used by that process.
|
|
1183
|
+
|
|
1184
|
+
### Additional ports
|
|
1185
|
+
|
|
1186
|
+
A context can organize its communications across several files at the root. Each file declares the ports for a group of interactions.
|
|
1187
|
+
|
|
1188
|
+
```ts
|
|
1189
|
+
export type ListUsersRequest = {
|
|
1190
|
+
active: boolean | null
|
|
1191
|
+
}
|
|
1192
|
+
|
|
1193
|
+
export type ListUsersResponse = {
|
|
1194
|
+
users: Array<{
|
|
1195
|
+
id: string
|
|
1196
|
+
name: string
|
|
1197
|
+
email: string
|
|
1198
|
+
active: boolean | null
|
|
1199
|
+
}>
|
|
1200
|
+
}
|
|
1201
|
+
|
|
1202
|
+
export interface ListUsersPort {
|
|
1203
|
+
list(request: ListUsersRequest): Promise<ListUsersResponse>
|
|
1204
|
+
}
|
|
1205
|
+
```
|
|
1206
|
+
|
|
1207
|
+
An adapter can import the contract from the file where it is declared:
|
|
1208
|
+
|
|
1209
|
+
```ts
|
|
1210
|
+
import type {
|
|
1211
|
+
ListUsersPort,
|
|
1212
|
+
ListUsersResponse
|
|
1213
|
+
} from '../example-ports.js'
|
|
1214
|
+
```
|
|
1215
|
+
|
|
1216
|
+
> **Tip:** group ports that form a coherent communication in the same file. Use additional files when the context grows and groups of interactions with their own responsibilities emerge.
|
|
1217
|
+
|
|
1218
|
+
## Generated file reference
|
|
1219
|
+
|
|
1220
|
+
| File | Purpose |
|
|
1221
|
+
| --- | --- |
|
|
1222
|
+
| `core/index.d.ts` | Declares `Generic<T>` for plain objects. |
|
|
1223
|
+
| `core/main.ts` | Starts as a placeholder for the library's main implementation and exports. |
|
|
1224
|
+
| `shared/domain/value-objects.ts` | Declares `ValueObject<T>` and implements `Email` and `NullableBoolean`. |
|
|
1225
|
+
| `shared/domain/entities.ts` | Declares the `Entity` base class. |
|
|
1226
|
+
| `shared/domain/aggregates.ts` | Declares the `Aggregate` base class. |
|
|
1227
|
+
| `shared/domain/errors.ts` | Implements `ValueError`. |
|
|
1228
|
+
| `shared/application/validations.ts` | Declares `Validatable`. |
|
|
1229
|
+
| `shared/application/services.ts` | Declares `Service` as the base class for use cases. |
|
|
1230
|
+
| `shared/application/http.ts` | Declares HTTP request, response, body, handler, and middleware contracts. |
|
|
1231
|
+
| `shared/application/loggers.ts` | Declares log levels and the `Logger` contract. |
|
|
1232
|
+
| `shared/application/events.ts` | Declares `Event`, `EventHandler`, and `EventDispatcher`. |
|
|
1233
|
+
| `shared/application/data/drivers.ts` | Declares the `DriverAdapter` contract used to connect to a data source. |
|
|
1234
|
+
| `shared/application/data/managers.ts` | Declares source operations together with `DataManager` and `DatasetManager`. |
|
|
1235
|
+
| `shared/application/data/repositories.ts` | Declares the `Repository` base class for transforming records into domain objects. |
|
|
1236
|
+
| `users/example-ports.ts` | Provides a root-level starter file with a placeholder export for your context ports. |
|
|
1237
|
+
| `users/domain/` | Contains the context's capabilities. |
|
|
1238
|
+
| `users/application/` | Contains processes that apply domain capabilities to fulfill purposes. |
|
|
1239
|
+
| `users/adapters/` | Contains integrations that import ports and connect the context with other systems. |
|