tshex-cli 1.0.18 → 1.0.20
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 +25 -1306
- package/package.json +2 -1
- 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 +0 -1
- 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.es.md +0 -1449
- package/templates/lib/shared/application/data-sources.ts +0 -102
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
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
4
|
|
|
5
|
-
In this guide, we will build a library named `core` with a context named `users`. The walkthrough starts with the CLI and
|
|
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
6
|
|
|
7
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
8
|
|
|
@@ -74,7 +74,10 @@ core/
|
|
|
74
74
|
├── main.ts
|
|
75
75
|
├── shared/
|
|
76
76
|
│ ├── application/
|
|
77
|
-
│ │ ├── data
|
|
77
|
+
│ │ ├── data/
|
|
78
|
+
│ │ │ ├── drivers.ts
|
|
79
|
+
│ │ │ ├── managers.ts
|
|
80
|
+
│ │ │ └── repositories.ts
|
|
78
81
|
│ │ ├── events.ts
|
|
79
82
|
│ │ ├── http.ts
|
|
80
83
|
│ │ ├── loggers.ts
|
|
@@ -89,8 +92,7 @@ core/
|
|
|
89
92
|
├── adapters/
|
|
90
93
|
├── application/
|
|
91
94
|
├── domain/
|
|
92
|
-
|
|
93
|
-
└── index.ts
|
|
95
|
+
└── example-ports.ts
|
|
94
96
|
```
|
|
95
97
|
|
|
96
98
|
### Choose the destination directory
|
|
@@ -138,1312 +140,29 @@ users/
|
|
|
138
140
|
└── languages/
|
|
139
141
|
```
|
|
140
142
|
|
|
141
|
-
##
|
|
143
|
+
## Documentation index
|
|
142
144
|
|
|
143
|
-
|
|
145
|
+
From this point on, the guide is split into dedicated documents under `docs/`.
|
|
144
146
|
|
|
145
|
-
###
|
|
147
|
+
### General
|
|
146
148
|
|
|
147
|
-
|
|
149
|
+
- [Library structure](./docs/library-structure.md)
|
|
150
|
+
- [Library types](./docs/library-types.md)
|
|
151
|
+
- [Context ports](./docs/context-ports.md)
|
|
152
|
+
- [Generated file reference](./docs/generated-file-reference.md)
|
|
148
153
|
|
|
149
|
-
|
|
154
|
+
### Shared application
|
|
150
155
|
|
|
151
|
-
|
|
156
|
+
- [shared/application/data](./docs/shared/application/data.md)
|
|
157
|
+
- [shared/application/events.ts](./docs/shared/application/events.md)
|
|
158
|
+
- [shared/application/http.ts](./docs/shared/application/http.md)
|
|
159
|
+
- [shared/application/loggers.ts](./docs/shared/application/loggers.md)
|
|
160
|
+
- [shared/application/services.ts](./docs/shared/application/services.md)
|
|
161
|
+
- [shared/application/validations.ts](./docs/shared/application/validations.md)
|
|
152
162
|
|
|
153
|
-
|
|
163
|
+
### Shared domain
|
|
154
164
|
|
|
155
|
-
|
|
156
|
-
shared/
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
```
|
|
160
|
-
|
|
161
|
-
`shared/domain` contains foundations for modeling business concepts. `shared/application` contains contracts for coordinating use cases, data sources, events, validations, HTTP responses, and logs.
|
|
162
|
-
|
|
163
|
-
### Contexts
|
|
164
|
-
|
|
165
|
-
A context represents an application capability and groups its vocabulary, rules, and operations.
|
|
166
|
-
|
|
167
|
-
```text
|
|
168
|
-
users/
|
|
169
|
-
billing/
|
|
170
|
-
sales/
|
|
171
|
-
inventory/
|
|
172
|
-
```
|
|
173
|
-
|
|
174
|
-
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`.
|
|
175
|
-
|
|
176
|
-
Each generated context contains three directories:
|
|
177
|
-
|
|
178
|
-
```text
|
|
179
|
-
domain/
|
|
180
|
-
application/
|
|
181
|
-
adapters/
|
|
182
|
-
```
|
|
183
|
-
|
|
184
|
-
#### `domain`
|
|
185
|
-
|
|
186
|
-
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.
|
|
187
|
-
|
|
188
|
-
Value objects, entities, and aggregates materialize these capabilities through data, rules, and behavior.
|
|
189
|
-
|
|
190
|
-
#### `application`
|
|
191
|
-
|
|
192
|
-
Contains the application of domain capabilities in processes that fulfill system purposes.
|
|
193
|
-
|
|
194
|
-
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.
|
|
195
|
-
|
|
196
|
-
#### Context root
|
|
197
|
-
|
|
198
|
-
The root contains `index.ts` and other `.ts` files intended for the context's ports.
|
|
199
|
-
|
|
200
|
-
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.
|
|
201
|
-
|
|
202
|
-
#### `adapters`
|
|
203
|
-
|
|
204
|
-
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.
|
|
205
|
-
|
|
206
|
-
An adapter can integrate an HTTP controller, a message consumer, an SDK, a driver, a remote client, an event bus, or a logging provider.
|
|
207
|
-
|
|
208
|
-
### Layered architecture
|
|
209
|
-
|
|
210
|
-
The structure is organized into three levels: capabilities, processes, and communication.
|
|
211
|
-
|
|
212
|
-
```text
|
|
213
|
-
External system
|
|
214
|
-
↕
|
|
215
|
-
Port + adapter
|
|
216
|
-
↓
|
|
217
|
-
Application
|
|
218
|
-
↓
|
|
219
|
-
Domain
|
|
220
|
-
```
|
|
221
|
-
|
|
222
|
-
The domain occupies the inner layer and contains the context's capabilities.
|
|
223
|
-
|
|
224
|
-
The application occupies the middle layer and uses those capabilities to build processes that fulfill system purposes.
|
|
225
|
-
|
|
226
|
-
Ports and adapters occupy the outer layer and handle communication between systems.
|
|
227
|
-
|
|
228
|
-
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 `index.ts` or in `.ts` modules located at the context root.
|
|
229
|
-
|
|
230
|
-
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.
|
|
231
|
-
|
|
232
|
-
The import direction follows this path:
|
|
233
|
-
|
|
234
|
-
```text
|
|
235
|
-
adapter → port
|
|
236
|
-
adapter → application
|
|
237
|
-
application → domain
|
|
238
|
-
```
|
|
239
|
-
|
|
240
|
-
For example, `users/index.ts` declares the `CreateUserPort` port. `users/adapters/create-user-adapter.ts` imports that port and connects an external request to `users/application/create-user-service.ts`. The service applies the capabilities of `Email` and `User` to complete the registration.
|
|
241
|
-
|
|
242
|
-
> **Tip:** start the implementation inside the context and move a component to `shared` when its meaning and use belong to multiple contexts.
|
|
243
|
-
|
|
244
|
-
## Library types
|
|
245
|
-
|
|
246
|
-
### `index.d.ts`
|
|
247
|
-
|
|
248
|
-
The file declares the generic type:
|
|
249
|
-
|
|
250
|
-
```ts
|
|
251
|
-
type Generic<T = unknown> = Record<string, T>
|
|
252
|
-
```
|
|
253
|
-
|
|
254
|
-
`Generic<T>` represents an object with `string` keys and values of a common type.
|
|
255
|
-
|
|
256
|
-
```ts
|
|
257
|
-
const filters: Generic<string> = {
|
|
258
|
-
status: 'active'
|
|
259
|
-
}
|
|
260
|
-
```
|
|
261
|
-
|
|
262
|
-
When the type is omitted, the values use `unknown`:
|
|
263
|
-
|
|
264
|
-
```ts
|
|
265
|
-
const metadata: Generic = {
|
|
266
|
-
retries: 2
|
|
267
|
-
}
|
|
268
|
-
```
|
|
269
|
-
|
|
270
|
-
Data source contracts use this form to represent plain objects whose specific structure will be defined by each implementation.
|
|
271
|
-
|
|
272
|
-
## Shared domain
|
|
273
|
-
|
|
274
|
-
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.
|
|
275
|
-
|
|
276
|
-
### Value objects
|
|
277
|
-
|
|
278
|
-
A value object represents a concept with its own rules, semantics, or behavior. Its identity is determined by its value.
|
|
279
|
-
|
|
280
|
-
The `shared/domain/value-objects.ts` file generates the `ValueObject<T>` base class and the `Email` and `NullableBoolean` implementations.
|
|
281
|
-
|
|
282
|
-
#### Create an email address
|
|
283
|
-
|
|
284
|
-
`Email` turns text into a domain concept with validation and its own operations:
|
|
285
|
-
|
|
286
|
-
```ts
|
|
287
|
-
import { Email } from './core/shared/domain/value-objects.js'
|
|
288
|
-
|
|
289
|
-
const email = Email.from('alejandro@example.com')
|
|
290
|
-
|
|
291
|
-
email.value
|
|
292
|
-
email.domain
|
|
293
|
-
```
|
|
294
|
-
|
|
295
|
-
Creation is performed with `Email.from()`. This method runs `Email.isValid()` before constructing the instance.
|
|
296
|
-
|
|
297
|
-
```ts
|
|
298
|
-
Email.isValid('alejandro@example.com')
|
|
299
|
-
```
|
|
300
|
-
|
|
301
|
-
Every creation follows the same validity rule:
|
|
302
|
-
|
|
303
|
-
```text
|
|
304
|
-
received value
|
|
305
|
-
↓
|
|
306
|
-
isValid(value)
|
|
307
|
-
↓
|
|
308
|
-
from(value)
|
|
309
|
-
↓
|
|
310
|
-
valid instance
|
|
311
|
-
```
|
|
312
|
-
|
|
313
|
-
When validation fails, `from()` throws `ValueError`.
|
|
314
|
-
|
|
315
|
-
> **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.
|
|
316
|
-
|
|
317
|
-
#### Represent a nullable Boolean state
|
|
318
|
-
|
|
319
|
-
`NullableBoolean` models the values `true`, `false`, and `null`:
|
|
320
|
-
|
|
321
|
-
```ts
|
|
322
|
-
import { NullableBoolean } from './core/shared/domain/value-objects.js'
|
|
323
|
-
|
|
324
|
-
const status = NullableBoolean.from(null)
|
|
325
|
-
|
|
326
|
-
status.value
|
|
327
|
-
status.isIndeterminate()
|
|
328
|
-
```
|
|
329
|
-
|
|
330
|
-
The `isIndeterminate()` method expresses an operation specific to the concept and allows the `null` state to be checked with explicit intent.
|
|
331
|
-
|
|
332
|
-
#### Implement a value object
|
|
333
|
-
|
|
334
|
-
We will create a discount percentage. The example has only one rule and one operation so the focus remains on the value object concept.
|
|
335
|
-
|
|
336
|
-
**`sales/domain/discount-percentage.ts`**
|
|
337
|
-
|
|
338
|
-
```ts
|
|
339
|
-
import { ValueError } from '../../shared/domain/errors.js'
|
|
340
|
-
import { ValueObject } from '../../shared/domain/value-objects.js'
|
|
341
|
-
|
|
342
|
-
export class DiscountPercentage extends ValueObject<number> {
|
|
343
|
-
public override readonly value: number
|
|
344
|
-
|
|
345
|
-
protected constructor(value: number) {
|
|
346
|
-
super()
|
|
347
|
-
this.value = value
|
|
348
|
-
}
|
|
349
|
-
|
|
350
|
-
public override equals(
|
|
351
|
-
other: DiscountPercentage | null | undefined
|
|
352
|
-
): boolean {
|
|
353
|
-
return other instanceof DiscountPercentage &&
|
|
354
|
-
this.value === other.value
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
public applyTo(amount: number): number {
|
|
358
|
-
return amount - amount * (this.value / 100)
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
public static override isValid(value: unknown): boolean {
|
|
362
|
-
return typeof value === 'number' &&
|
|
363
|
-
Number.isFinite(value) &&
|
|
364
|
-
value >= 0 &&
|
|
365
|
-
value <= 100
|
|
366
|
-
}
|
|
367
|
-
|
|
368
|
-
public static from(value: number): DiscountPercentage {
|
|
369
|
-
if (this.isValid(value) === false) {
|
|
370
|
-
throw new ValueError(String(value), this.name)
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
return new this(value)
|
|
374
|
-
}
|
|
375
|
-
}
|
|
376
|
-
```
|
|
377
|
-
|
|
378
|
-
Now we can create and use the concept:
|
|
379
|
-
|
|
380
|
-
```ts
|
|
381
|
-
const discount = DiscountPercentage.from(15)
|
|
382
|
-
const finalPrice = discount.applyTo(100)
|
|
383
|
-
```
|
|
384
|
-
|
|
385
|
-
`isValid()` centralizes the rule. `from()` creates the valid instance. `applyTo()` adds behavior specific to the concept.
|
|
386
|
-
|
|
387
|
-
### Entities
|
|
388
|
-
|
|
389
|
-
An entity represents a concept with its own identity. Two instances represent the same element when they share that identity.
|
|
390
|
-
|
|
391
|
-
The `shared/domain/entities.ts` file generates the `Entity` base class. Each entity implements `equals()` and `toJSON()`.
|
|
392
|
-
|
|
393
|
-
We will create an entity for the `users` context.
|
|
394
|
-
|
|
395
|
-
**`users/domain/user.ts`**
|
|
396
|
-
|
|
397
|
-
```ts
|
|
398
|
-
import { Entity } from '../../shared/domain/entities.js'
|
|
399
|
-
import {
|
|
400
|
-
Email,
|
|
401
|
-
NullableBoolean
|
|
402
|
-
} from '../../shared/domain/value-objects.js'
|
|
403
|
-
|
|
404
|
-
export class User extends Entity {
|
|
405
|
-
public constructor(
|
|
406
|
-
public readonly id: string,
|
|
407
|
-
public readonly email: Email,
|
|
408
|
-
public readonly active: NullableBoolean
|
|
409
|
-
) {
|
|
410
|
-
super()
|
|
411
|
-
}
|
|
412
|
-
|
|
413
|
-
public override equals(other: Entity): boolean {
|
|
414
|
-
return other instanceof User && other.id === this.id
|
|
415
|
-
}
|
|
416
|
-
|
|
417
|
-
public override toJSON(): Record<string, unknown> {
|
|
418
|
-
return {
|
|
419
|
-
id: this.id,
|
|
420
|
-
email: this.email.value,
|
|
421
|
-
active: this.active.value
|
|
422
|
-
}
|
|
423
|
-
}
|
|
424
|
-
}
|
|
425
|
-
```
|
|
426
|
-
|
|
427
|
-
The `id` property defines the identity. The `equals()` method compares entities using that property.
|
|
428
|
-
|
|
429
|
-
`toJSON()` produces a plain representation of the entity:
|
|
430
|
-
|
|
431
|
-
```ts
|
|
432
|
-
const user = new User(
|
|
433
|
-
'user-1',
|
|
434
|
-
Email.from('alejandro@example.com'),
|
|
435
|
-
NullableBoolean.from(true)
|
|
436
|
-
)
|
|
437
|
-
|
|
438
|
-
const json = user.toJSON()
|
|
439
|
-
```
|
|
440
|
-
|
|
441
|
-
The result uses the internal values of `Email` and `NullableBoolean`.
|
|
442
|
-
|
|
443
|
-
### Aggregates
|
|
444
|
-
|
|
445
|
-
An aggregate groups multiple entities into a logical unit. The aggregate's operations depend on all the identities that compose it.
|
|
446
|
-
|
|
447
|
-
A team can group one leader and several members:
|
|
448
|
-
|
|
449
|
-
```text
|
|
450
|
-
Team
|
|
451
|
-
├── leader
|
|
452
|
-
└── members
|
|
453
|
-
```
|
|
454
|
-
|
|
455
|
-
Each element preserves its own identity within the unit.
|
|
456
|
-
|
|
457
|
-
**`users/domain/team.ts`**
|
|
458
|
-
|
|
459
|
-
```ts
|
|
460
|
-
import { Aggregate } from '../../shared/domain/aggregates.js'
|
|
461
|
-
import type { User } from './user.js'
|
|
462
|
-
|
|
463
|
-
export class Team extends Aggregate {
|
|
464
|
-
public constructor(
|
|
465
|
-
public readonly leader: User,
|
|
466
|
-
public readonly members: User[]
|
|
467
|
-
) {
|
|
468
|
-
super()
|
|
469
|
-
}
|
|
470
|
-
|
|
471
|
-
public size(): number {
|
|
472
|
-
return this.members.length + 1
|
|
473
|
-
}
|
|
474
|
-
}
|
|
475
|
-
```
|
|
476
|
-
|
|
477
|
-
`Team` groups multiple `User` entities into a single logical unit. The `size()` method operates on that group.
|
|
478
|
-
|
|
479
|
-
### Domain errors
|
|
480
|
-
|
|
481
|
-
The `shared/domain/errors.ts` file generates `ValueError`. This error represents a received value that does not satisfy the expected rule.
|
|
482
|
-
|
|
483
|
-
```ts
|
|
484
|
-
import { ValueError } from '../../shared/domain/errors.js'
|
|
485
|
-
|
|
486
|
-
if (quantity <= 0) {
|
|
487
|
-
throw new ValueError(String(quantity), 'PositiveQuantity')
|
|
488
|
-
}
|
|
489
|
-
```
|
|
490
|
-
|
|
491
|
-
Because it is in `shared`, `ValueError` can be used from any context and by any domain concept that validates values.
|
|
492
|
-
|
|
493
|
-
## Shared application
|
|
494
|
-
|
|
495
|
-
The application layer coordinates use cases. Its contracts connect the domain with validations, data sources, HTTP responses, logs, and events.
|
|
496
|
-
|
|
497
|
-
### Validations
|
|
498
|
-
|
|
499
|
-
The `shared/application/validations.ts` file declares the `Validatable` contract:
|
|
500
|
-
|
|
501
|
-
```ts
|
|
502
|
-
isValid(): boolean
|
|
503
|
-
validate(): unknown
|
|
504
|
-
```
|
|
505
|
-
|
|
506
|
-
`isValid()` checks the validation state. `validate()` performs the validation and returns the result defined by the implementation.
|
|
507
|
-
|
|
508
|
-
We will create a validation for the use case that registers users.
|
|
509
|
-
|
|
510
|
-
**`users/application/create-user-validation.ts`**
|
|
511
|
-
|
|
512
|
-
```ts
|
|
513
|
-
import type { Validatable } from '../../shared/application/validations.js'
|
|
514
|
-
import { Email } from '../../shared/domain/value-objects.js'
|
|
515
|
-
|
|
516
|
-
export class CreateUserValidation implements Validatable {
|
|
517
|
-
public constructor(private readonly email: string) {}
|
|
518
|
-
|
|
519
|
-
public isValid(): boolean {
|
|
520
|
-
return Email.isValid(this.email)
|
|
521
|
-
}
|
|
522
|
-
|
|
523
|
-
public validate(): string[] {
|
|
524
|
-
return this.isValid()
|
|
525
|
-
? []
|
|
526
|
-
: ['The email is invalid.']
|
|
527
|
-
}
|
|
528
|
-
}
|
|
529
|
-
```
|
|
530
|
-
|
|
531
|
-
The application service can run this validation before constructing the `User` entity.
|
|
532
|
-
|
|
533
|
-
### Services
|
|
534
|
-
|
|
535
|
-
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.
|
|
536
|
-
|
|
537
|
-
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.
|
|
538
|
-
|
|
539
|
-
We will begin with a service that creates a user.
|
|
540
|
-
|
|
541
|
-
**`users/application/create-user-service.ts`**
|
|
542
|
-
|
|
543
|
-
```ts
|
|
544
|
-
import { Service } from '../../shared/application/services.js'
|
|
545
|
-
import {
|
|
546
|
-
Email,
|
|
547
|
-
NullableBoolean
|
|
548
|
-
} from '../../shared/domain/value-objects.js'
|
|
549
|
-
import { User } from '../domain/user.js'
|
|
550
|
-
import { CreateUserValidation } from './create-user-validation.js'
|
|
551
|
-
|
|
552
|
-
export interface UserWriter {
|
|
553
|
-
save(user: User): Promise<void>
|
|
554
|
-
}
|
|
555
|
-
|
|
556
|
-
export type CreateUserCommand = {
|
|
557
|
-
id: string
|
|
558
|
-
email: string
|
|
559
|
-
}
|
|
560
|
-
|
|
561
|
-
export type CreatedUser = {
|
|
562
|
-
id: string
|
|
563
|
-
email: string
|
|
564
|
-
active: boolean | null
|
|
565
|
-
}
|
|
566
|
-
|
|
567
|
-
export class CreateUserService extends Service {
|
|
568
|
-
public constructor(private readonly users: UserWriter) {
|
|
569
|
-
super()
|
|
570
|
-
}
|
|
571
|
-
|
|
572
|
-
public async execute(
|
|
573
|
-
command: CreateUserCommand
|
|
574
|
-
): Promise<CreatedUser> {
|
|
575
|
-
const validation = new CreateUserValidation(command.email)
|
|
576
|
-
const errors = validation.validate()
|
|
577
|
-
|
|
578
|
-
if (errors.length !== 0) {
|
|
579
|
-
throw new Error(errors.join(', '))
|
|
580
|
-
}
|
|
581
|
-
|
|
582
|
-
const user = new User(
|
|
583
|
-
command.id,
|
|
584
|
-
Email.from(command.email),
|
|
585
|
-
NullableBoolean.from(true)
|
|
586
|
-
)
|
|
587
|
-
|
|
588
|
-
await this.users.save(user)
|
|
589
|
-
|
|
590
|
-
return {
|
|
591
|
-
id: user.id,
|
|
592
|
-
email: user.email.value,
|
|
593
|
-
active: user.active.value
|
|
594
|
-
}
|
|
595
|
-
}
|
|
596
|
-
}
|
|
597
|
-
```
|
|
598
|
-
|
|
599
|
-
The service receives a command, validates the input, constructs the entity, and delegates persistence. `CreateUserCommand` expresses the process input. `CreatedUser` expresses the output.
|
|
600
|
-
|
|
601
|
-
### HTTP responses
|
|
602
|
-
|
|
603
|
-
The `shared/application/http.ts` file generates `HttpResponseBody`, a response body designed for REST APIs.
|
|
604
|
-
|
|
605
|
-
The class organizes the response into three properties:
|
|
606
|
-
|
|
607
|
-
```ts
|
|
608
|
-
new HttpResponseBody(data, errors, links)
|
|
609
|
-
```
|
|
610
|
-
|
|
611
|
-
`data` contains the operation data and accepts `null` when the response has no data:
|
|
612
|
-
|
|
613
|
-
```ts
|
|
614
|
-
const body = new HttpResponseBody({ id: 'user-1' })
|
|
615
|
-
```
|
|
616
|
-
|
|
617
|
-
`errors` contains a list of messages:
|
|
618
|
-
|
|
619
|
-
```ts
|
|
620
|
-
const body = new HttpResponseBody(
|
|
621
|
-
null,
|
|
622
|
-
['The email is invalid.']
|
|
623
|
-
)
|
|
624
|
-
```
|
|
625
|
-
|
|
626
|
-
`links` contains HATEOAS links related to the resource and its available operations:
|
|
627
|
-
|
|
628
|
-
```ts
|
|
629
|
-
const body = new HttpResponseBody(
|
|
630
|
-
{ id: 'user-1' },
|
|
631
|
-
null,
|
|
632
|
-
{
|
|
633
|
-
self: new URL('https://api.example.com/users/user-1')
|
|
634
|
-
}
|
|
635
|
-
)
|
|
636
|
-
```
|
|
637
|
-
|
|
638
|
-
An entity's plain output can be used as `data`:
|
|
639
|
-
|
|
640
|
-
```ts
|
|
641
|
-
const body = new HttpResponseBody(user.toJSON())
|
|
642
|
-
```
|
|
643
|
-
|
|
644
|
-
### Logs
|
|
645
|
-
|
|
646
|
-
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.
|
|
647
|
-
|
|
648
|
-
The contract includes the `debug`, `info`, `warning`, `error`, and `critical` levels, together with the numeric constants `DEBUG`, `INFO`, `WARNING`, `ERROR`, and `CRITICAL`.
|
|
649
|
-
|
|
650
|
-
We will create a simple adapter that connects the contract to the console.
|
|
651
|
-
|
|
652
|
-
**`users/adapters/console-logger-adapter.ts`**
|
|
653
|
-
|
|
654
|
-
```ts
|
|
655
|
-
import { Logger } from '../../shared/application/loggers.js'
|
|
656
|
-
|
|
657
|
-
export class ConsoleLoggerAdapter extends Logger {
|
|
658
|
-
public debug(data: unknown): void {
|
|
659
|
-
console.debug(data)
|
|
660
|
-
}
|
|
661
|
-
|
|
662
|
-
public info(data: unknown): void {
|
|
663
|
-
console.info(data)
|
|
664
|
-
}
|
|
665
|
-
|
|
666
|
-
public warning(data: unknown): void {
|
|
667
|
-
console.warn(data)
|
|
668
|
-
}
|
|
669
|
-
|
|
670
|
-
public error(data: unknown): void {
|
|
671
|
-
console.error(data)
|
|
672
|
-
}
|
|
673
|
-
|
|
674
|
-
public critical(data: unknown): void {
|
|
675
|
-
console.error(data)
|
|
676
|
-
}
|
|
677
|
-
}
|
|
678
|
-
```
|
|
679
|
-
|
|
680
|
-
The service receives `Logger` as a dependency. The adapter decides where to send each level.
|
|
681
|
-
|
|
682
|
-
### Events
|
|
683
|
-
|
|
684
|
-
The `shared/application/events.ts` file contains the contracts that connect the application to an event bus.
|
|
685
|
-
|
|
686
|
-
The flow starts with an event, continues through the dispatcher, and ends in one or more handlers:
|
|
687
|
-
|
|
688
|
-
```text
|
|
689
|
-
Service
|
|
690
|
-
↓ creates
|
|
691
|
-
Event
|
|
692
|
-
↓ passes to
|
|
693
|
-
EventDispatcher
|
|
694
|
-
↓ publishes to
|
|
695
|
-
Event bus
|
|
696
|
-
↓ executes
|
|
697
|
-
EventHandler
|
|
698
|
-
```
|
|
699
|
-
|
|
700
|
-
#### Event
|
|
701
|
-
|
|
702
|
-
`Event` represents something that occurred in the application. It contains the event time and its plain details.
|
|
703
|
-
|
|
704
|
-
**`users/application/user-created.ts`**
|
|
705
|
-
|
|
706
|
-
```ts
|
|
707
|
-
import { Event } from '../../shared/application/events.js'
|
|
708
|
-
|
|
709
|
-
export class UserCreated extends Event {
|
|
710
|
-
public constructor(userId: string) {
|
|
711
|
-
super(Date.now(), { userId })
|
|
712
|
-
}
|
|
713
|
-
}
|
|
714
|
-
```
|
|
715
|
-
|
|
716
|
-
#### Handler
|
|
717
|
-
|
|
718
|
-
`EventHandler` represents a reaction to the event.
|
|
719
|
-
|
|
720
|
-
**`users/application/log-user-created.ts`**
|
|
721
|
-
|
|
722
|
-
```ts
|
|
723
|
-
import {
|
|
724
|
-
Event,
|
|
725
|
-
EventHandler
|
|
726
|
-
} from '../../shared/application/events.js'
|
|
727
|
-
import { Logger } from '../../shared/application/loggers.js'
|
|
728
|
-
|
|
729
|
-
export class LogUserCreated extends EventHandler {
|
|
730
|
-
public constructor(private readonly logger: Logger) {
|
|
731
|
-
super()
|
|
732
|
-
}
|
|
733
|
-
|
|
734
|
-
public async handle(event: Event): Promise<void> {
|
|
735
|
-
this.logger.info(event.details)
|
|
736
|
-
}
|
|
737
|
-
}
|
|
738
|
-
```
|
|
739
|
-
|
|
740
|
-
#### Dispatcher
|
|
741
|
-
|
|
742
|
-
`EventDispatcher` represents the interaction contract with the event bus. Its concrete implementation subscribes handlers, removes subscriptions, and dispatches events.
|
|
743
|
-
|
|
744
|
-
```ts
|
|
745
|
-
subscribe(key, handler)
|
|
746
|
-
unsubscribe(key, handler)
|
|
747
|
-
dispatch(event)
|
|
748
|
-
```
|
|
749
|
-
|
|
750
|
-
The service can receive `EventDispatcher` and publish `UserCreated` after completing the use case.
|
|
751
|
-
|
|
752
|
-
## Data sources
|
|
753
|
-
|
|
754
|
-
The `shared/application/data-sources.ts` file organizes access to a data source into four components: `DriverManager`, `DataManager`, `DatasetManager`, and `Repository`.
|
|
755
|
-
|
|
756
|
-
The complete flow looks like this:
|
|
757
|
-
|
|
758
|
-
```text
|
|
759
|
-
DriverManager
|
|
760
|
-
↓ connects and enables
|
|
761
|
-
DataManager or DatasetManager
|
|
762
|
-
↓ provides plain data to
|
|
763
|
-
Repository
|
|
764
|
-
↓ transforms
|
|
765
|
-
Domain objects
|
|
766
|
-
↓ used by
|
|
767
|
-
Service
|
|
768
|
-
```
|
|
769
|
-
|
|
770
|
-
We will begin with the connection and proceed to the use case.
|
|
771
|
-
|
|
772
|
-
### Driver manager
|
|
773
|
-
|
|
774
|
-
`DriverManager` connects and disconnects the source through a driver. When the connection is available, `connect()` returns an enabled data manager.
|
|
775
|
-
|
|
776
|
-
```ts
|
|
777
|
-
connect(...args): Promise<DataManager>
|
|
778
|
-
disconnect(): Promise<unknown>
|
|
779
|
-
```
|
|
780
|
-
|
|
781
|
-
We will create a manager for an in-memory collection of people. The example avoids a database so the focus remains on the manager's responsibility.
|
|
782
|
-
|
|
783
|
-
**`users/adapters/people-driver-manager.ts`**
|
|
784
|
-
|
|
785
|
-
```ts
|
|
786
|
-
import { DriverManager } from '../../shared/application/data-sources.js'
|
|
787
|
-
import { PeopleDataManager } from './people-data-manager.js'
|
|
788
|
-
|
|
789
|
-
export type PersonRecord = {
|
|
790
|
-
id: string
|
|
791
|
-
name: string
|
|
792
|
-
}
|
|
793
|
-
|
|
794
|
-
export class PeopleDriverManager
|
|
795
|
-
extends DriverManager<PeopleDataManager> {
|
|
796
|
-
|
|
797
|
-
public constructor(private readonly records: PersonRecord[]) {
|
|
798
|
-
super()
|
|
799
|
-
}
|
|
800
|
-
|
|
801
|
-
public async connect(): Promise<PeopleDataManager> {
|
|
802
|
-
return new PeopleDataManager(this.records)
|
|
803
|
-
}
|
|
804
|
-
|
|
805
|
-
public async disconnect(): Promise<void> {
|
|
806
|
-
}
|
|
807
|
-
}
|
|
808
|
-
```
|
|
809
|
-
|
|
810
|
-
The concrete implementation can encapsulate a database driver, an HTTP client, a file system, or another source.
|
|
811
|
-
|
|
812
|
-
### Data manager
|
|
813
|
-
|
|
814
|
-
`DataManager` operates on the source and works with plain objects and arrays. Its base form defines two operations:
|
|
815
|
-
|
|
816
|
-
```ts
|
|
817
|
-
all(): Promise<Array<T>>
|
|
818
|
-
none(): Array<T>
|
|
819
|
-
```
|
|
820
|
-
|
|
821
|
-
`all()` retrieves the available records. `none()` creates an empty typed collection.
|
|
822
|
-
|
|
823
|
-
First, we will define the shape used by the source:
|
|
824
|
-
|
|
825
|
-
```ts
|
|
826
|
-
type PersonRecord = {
|
|
827
|
-
id: string
|
|
828
|
-
name: string
|
|
829
|
-
}
|
|
830
|
-
```
|
|
831
|
-
|
|
832
|
-
Now we will implement the data manager.
|
|
833
|
-
|
|
834
|
-
**`users/adapters/people-data-manager.ts`**
|
|
835
|
-
|
|
836
|
-
```ts
|
|
837
|
-
import { DataManager } from '../../shared/application/data-sources.js'
|
|
838
|
-
|
|
839
|
-
export type PersonRecord = {
|
|
840
|
-
id: string
|
|
841
|
-
name: string
|
|
842
|
-
}
|
|
843
|
-
|
|
844
|
-
export class PeopleDataManager
|
|
845
|
-
extends DataManager<PersonRecord> {
|
|
846
|
-
|
|
847
|
-
public constructor(private readonly records: PersonRecord[]) {
|
|
848
|
-
super()
|
|
849
|
-
}
|
|
850
|
-
|
|
851
|
-
public async all(): Promise<PersonRecord[]> {
|
|
852
|
-
return this.records
|
|
853
|
-
}
|
|
854
|
-
|
|
855
|
-
public none(): PersonRecord[] {
|
|
856
|
-
return []
|
|
857
|
-
}
|
|
858
|
-
}
|
|
859
|
-
```
|
|
860
|
-
|
|
861
|
-
The data manager reflects the structure of the source. In this example, it only provides plain records.
|
|
862
|
-
|
|
863
|
-
#### Source operations
|
|
864
|
-
|
|
865
|
-
The file also declares interfaces that extend a data manager's capabilities:
|
|
866
|
-
|
|
867
|
-
| Interface | Operation |
|
|
868
|
-
| --- | --- |
|
|
869
|
-
| `Filterable` | Filters records. |
|
|
870
|
-
| `Sortable` | Sorts records. |
|
|
871
|
-
| `Creatable` | Creates records. |
|
|
872
|
-
| `Updatable` | Updates records. |
|
|
873
|
-
| `Deletable` | Deletes records. |
|
|
874
|
-
| `Aggregatable` | Performs aggregations. |
|
|
875
|
-
| `Relatable` | Selects or preloads relationships. |
|
|
876
|
-
|
|
877
|
-
A data manager can implement the interfaces required by its source:
|
|
878
|
-
|
|
879
|
-
```ts
|
|
880
|
-
import {
|
|
881
|
-
Creatable,
|
|
882
|
-
DataManager,
|
|
883
|
-
Filterable
|
|
884
|
-
} from '../../shared/application/data-sources.js'
|
|
885
|
-
|
|
886
|
-
export class PeopleDataManager
|
|
887
|
-
extends DataManager<PersonRecord>
|
|
888
|
-
implements
|
|
889
|
-
Filterable<Partial<PersonRecord>>,
|
|
890
|
-
Creatable<PersonRecord> {
|
|
891
|
-
|
|
892
|
-
public constructor(private readonly records: PersonRecord[]) {
|
|
893
|
-
super()
|
|
894
|
-
}
|
|
895
|
-
|
|
896
|
-
public async all(): Promise<PersonRecord[]> {
|
|
897
|
-
return this.records
|
|
898
|
-
}
|
|
899
|
-
|
|
900
|
-
public none(): PersonRecord[] {
|
|
901
|
-
return []
|
|
902
|
-
}
|
|
903
|
-
|
|
904
|
-
public async filter(
|
|
905
|
-
selector: Partial<PersonRecord>
|
|
906
|
-
): Promise<PersonRecord[]> {
|
|
907
|
-
return this.records.filter((record) =>
|
|
908
|
-
(selector.id === undefined || record.id === selector.id) &&
|
|
909
|
-
(selector.name === undefined || record.name === selector.name)
|
|
910
|
-
)
|
|
911
|
-
}
|
|
912
|
-
|
|
913
|
-
public async create(data: PersonRecord): Promise<void> {
|
|
914
|
-
this.records.push(data)
|
|
915
|
-
}
|
|
916
|
-
}
|
|
917
|
-
```
|
|
918
|
-
|
|
919
|
-
It can also declare operations specific to source queries:
|
|
920
|
-
|
|
921
|
-
```ts
|
|
922
|
-
public async findByName(name: string): Promise<PersonRecord[]> {
|
|
923
|
-
return this.filter({ name })
|
|
924
|
-
}
|
|
925
|
-
```
|
|
926
|
-
|
|
927
|
-
The application layer decides when to execute these operations, combines their results, and catches errors produced by the source.
|
|
928
|
-
|
|
929
|
-
### Dataset manager
|
|
930
|
-
|
|
931
|
-
`DatasetManager` extends `DataManager` with set operations:
|
|
932
|
-
|
|
933
|
-
```ts
|
|
934
|
-
union()
|
|
935
|
-
intersection()
|
|
936
|
-
difference()
|
|
937
|
-
symmetric_difference()
|
|
938
|
-
complement()
|
|
939
|
-
```
|
|
940
|
-
|
|
941
|
-
This implementation is useful when an operation works with unions, intersections, differences, and complements between data collections.
|
|
942
|
-
|
|
943
|
-
### Repository
|
|
944
|
-
|
|
945
|
-
`Repository` acts as an intermediary between plain data and domain objects.
|
|
946
|
-
|
|
947
|
-
```text
|
|
948
|
-
PersonRecord
|
|
949
|
-
↓ transform
|
|
950
|
-
Person
|
|
951
|
-
|
|
952
|
-
Person
|
|
953
|
-
↓ toRecord
|
|
954
|
-
PersonRecord
|
|
955
|
-
```
|
|
956
|
-
|
|
957
|
-
In this example, the source and the domain have nearly the same shape so the focus remains on the repository's role: translating between plain data and domain objects.
|
|
958
|
-
|
|
959
|
-
**`users/domain/person.ts`**
|
|
960
|
-
|
|
961
|
-
```ts
|
|
962
|
-
import { Entity } from '../../shared/domain/entities.js'
|
|
963
|
-
|
|
964
|
-
export class Person extends Entity {
|
|
965
|
-
public constructor(
|
|
966
|
-
public readonly id: string,
|
|
967
|
-
public readonly name: string
|
|
968
|
-
) {
|
|
969
|
-
super()
|
|
970
|
-
}
|
|
971
|
-
|
|
972
|
-
public override equals(other: Entity): boolean {
|
|
973
|
-
return other instanceof Person && other.id === this.id
|
|
974
|
-
}
|
|
975
|
-
|
|
976
|
-
public override toJSON(): Record<string, unknown> {
|
|
977
|
-
return {
|
|
978
|
-
id: this.id,
|
|
979
|
-
name: this.name
|
|
980
|
-
}
|
|
981
|
-
}
|
|
982
|
-
}
|
|
983
|
-
```
|
|
984
|
-
|
|
985
|
-
Now we will implement the repository.
|
|
986
|
-
|
|
987
|
-
**`users/adapters/people-repository.ts`**
|
|
988
|
-
|
|
989
|
-
```ts
|
|
990
|
-
import { Repository } from '../../shared/application/data-sources.js'
|
|
991
|
-
import type { PeopleReader } from '../application/list-people-service.js'
|
|
992
|
-
import { Person } from '../domain/person.js'
|
|
993
|
-
import type { PersonRecord } from './people-data-manager.js'
|
|
994
|
-
import { PeopleDriverManager } from './people-driver-manager.js'
|
|
995
|
-
|
|
996
|
-
export class PeopleRepository
|
|
997
|
-
extends Repository<PeopleDriverManager>
|
|
998
|
-
implements PeopleReader {
|
|
999
|
-
|
|
1000
|
-
protected transform<T = Person>(data: Generic): T {
|
|
1001
|
-
const record = data as PersonRecord
|
|
1002
|
-
|
|
1003
|
-
return new Person(record.id, record.name) as T
|
|
1004
|
-
}
|
|
1005
|
-
|
|
1006
|
-
private toRecord(person: Person): PersonRecord {
|
|
1007
|
-
return {
|
|
1008
|
-
id: person.id,
|
|
1009
|
-
name: person.name
|
|
1010
|
-
}
|
|
1011
|
-
}
|
|
1012
|
-
|
|
1013
|
-
public async findAll(): Promise<Person[]> {
|
|
1014
|
-
const dataManager = await this.manager.connect()
|
|
1015
|
-
const records = await dataManager.all()
|
|
1016
|
-
|
|
1017
|
-
return records.map((record) =>
|
|
1018
|
-
this.transform<Person>(record)
|
|
1019
|
-
)
|
|
1020
|
-
}
|
|
1021
|
-
|
|
1022
|
-
public async save(person: Person): Promise<void> {
|
|
1023
|
-
const dataManager = await this.manager.connect()
|
|
1024
|
-
|
|
1025
|
-
await dataManager.create(this.toRecord(person))
|
|
1026
|
-
}
|
|
1027
|
-
}
|
|
1028
|
-
```
|
|
1029
|
-
|
|
1030
|
-
`transform()` converts the record into an entity. `toRecord()` performs the reverse conversion.
|
|
1031
|
-
|
|
1032
|
-
### Queries and errors in the application
|
|
1033
|
-
|
|
1034
|
-
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.
|
|
1035
|
-
|
|
1036
|
-
**`users/application/list-people-service.ts`**
|
|
1037
|
-
|
|
1038
|
-
```ts
|
|
1039
|
-
import { Service } from '../../shared/application/services.js'
|
|
1040
|
-
import type { Person } from '../domain/person.js'
|
|
1041
|
-
|
|
1042
|
-
export interface PeopleReader {
|
|
1043
|
-
findAll(): Promise<Person[]>
|
|
1044
|
-
}
|
|
1045
|
-
|
|
1046
|
-
export class ListPeopleService extends Service {
|
|
1047
|
-
public constructor(private readonly people: PeopleReader) {
|
|
1048
|
-
super()
|
|
1049
|
-
}
|
|
1050
|
-
|
|
1051
|
-
public async execute(): Promise<Person[]> {
|
|
1052
|
-
try {
|
|
1053
|
-
return await this.people.findAll()
|
|
1054
|
-
} catch {
|
|
1055
|
-
throw new Error('Could not list people.')
|
|
1056
|
-
}
|
|
1057
|
-
}
|
|
1058
|
-
}
|
|
1059
|
-
```
|
|
1060
|
-
|
|
1061
|
-
`PeopleReader` expresses the collaboration required by the process. `PeopleRepository`, located in `adapters`, implements that collaboration and transforms source data into `Person` entities.
|
|
1062
|
-
|
|
1063
|
-
## Context ports
|
|
1064
|
-
|
|
1065
|
-
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.
|
|
1066
|
-
|
|
1067
|
-
The context generates `index.ts` as the main port file and `example-ports.ts` as an example of an additional file. Adapters import the ports that define the communication they materialize.
|
|
1068
|
-
|
|
1069
|
-
### Main port
|
|
1070
|
-
|
|
1071
|
-
We will declare the communication for creating a user directly in `users/index.ts`.
|
|
1072
|
-
|
|
1073
|
-
**`users/index.ts`**
|
|
1074
|
-
|
|
1075
|
-
```ts
|
|
1076
|
-
export type CreateUserRequest = {
|
|
1077
|
-
id: string
|
|
1078
|
-
email: string
|
|
1079
|
-
}
|
|
1080
|
-
|
|
1081
|
-
export type CreateUserResponse = {
|
|
1082
|
-
id: string
|
|
1083
|
-
email: string
|
|
1084
|
-
active: boolean | null
|
|
1085
|
-
}
|
|
1086
|
-
|
|
1087
|
-
export interface CreateUserPort {
|
|
1088
|
-
create(
|
|
1089
|
-
request: CreateUserRequest
|
|
1090
|
-
): Promise<CreateUserResponse>
|
|
1091
|
-
}
|
|
1092
|
-
```
|
|
1093
|
-
|
|
1094
|
-
`CreateUserRequest` represents the information received from another system. `CreateUserResponse` represents the returned response. `CreateUserPort` defines the operation available at the context boundary.
|
|
1095
|
-
|
|
1096
|
-
### Adapt the port to the application process
|
|
1097
|
-
|
|
1098
|
-
The adapter imports the port and the service. Its responsibility is to translate the external request into the application command and transform the result into the port response.
|
|
1099
|
-
|
|
1100
|
-
**`users/adapters/create-user-adapter.ts`**
|
|
1101
|
-
|
|
1102
|
-
```ts
|
|
1103
|
-
import type {
|
|
1104
|
-
CreateUserPort,
|
|
1105
|
-
CreateUserRequest,
|
|
1106
|
-
CreateUserResponse
|
|
1107
|
-
} from '../index.js'
|
|
1108
|
-
import {
|
|
1109
|
-
CreateUserService,
|
|
1110
|
-
type CreateUserCommand
|
|
1111
|
-
} from '../application/create-user-service.js'
|
|
1112
|
-
|
|
1113
|
-
export class CreateUserAdapter implements CreateUserPort {
|
|
1114
|
-
public constructor(
|
|
1115
|
-
private readonly service: CreateUserService
|
|
1116
|
-
) {}
|
|
1117
|
-
|
|
1118
|
-
public async create(
|
|
1119
|
-
request: CreateUserRequest
|
|
1120
|
-
): Promise<CreateUserResponse> {
|
|
1121
|
-
const command: CreateUserCommand = {
|
|
1122
|
-
id: request.id,
|
|
1123
|
-
email: request.email
|
|
1124
|
-
}
|
|
1125
|
-
|
|
1126
|
-
const result = await this.service.execute(command)
|
|
1127
|
-
|
|
1128
|
-
return {
|
|
1129
|
-
id: result.id,
|
|
1130
|
-
email: result.email,
|
|
1131
|
-
active: result.active
|
|
1132
|
-
}
|
|
1133
|
-
}
|
|
1134
|
-
}
|
|
1135
|
-
```
|
|
1136
|
-
|
|
1137
|
-
The port expresses the communication. The adapter implements it. The service executes the process. The domain provides the capabilities used by that process.
|
|
1138
|
-
|
|
1139
|
-
### Additional ports
|
|
1140
|
-
|
|
1141
|
-
A context can organize its communications across several files at the root. Each file declares the ports for a group of interactions.
|
|
1142
|
-
|
|
1143
|
-
**`users/example-ports.ts`**
|
|
1144
|
-
|
|
1145
|
-
```ts
|
|
1146
|
-
export type FindUserRequest = {
|
|
1147
|
-
id: string
|
|
1148
|
-
}
|
|
1149
|
-
|
|
1150
|
-
export type FindUserResponse = {
|
|
1151
|
-
id: string
|
|
1152
|
-
email: string
|
|
1153
|
-
} | null
|
|
1154
|
-
|
|
1155
|
-
export interface FindUserPort {
|
|
1156
|
-
find(
|
|
1157
|
-
request: FindUserRequest
|
|
1158
|
-
): Promise<FindUserResponse>
|
|
1159
|
-
}
|
|
1160
|
-
```
|
|
1161
|
-
|
|
1162
|
-
The corresponding adapter imports the contract from the file where it is declared:
|
|
1163
|
-
|
|
1164
|
-
```ts
|
|
1165
|
-
import type {
|
|
1166
|
-
FindUserPort,
|
|
1167
|
-
FindUserRequest,
|
|
1168
|
-
FindUserResponse
|
|
1169
|
-
} from '../example-ports.js'
|
|
1170
|
-
```
|
|
1171
|
-
|
|
1172
|
-
> **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.
|
|
1173
|
-
|
|
1174
|
-
## Implement a context
|
|
1175
|
-
|
|
1176
|
-
We will now walk through a complete implementation of `users`, following the layer order: capabilities, process, and communication.
|
|
1177
|
-
|
|
1178
|
-
### 1. Model the domain capabilities
|
|
1179
|
-
|
|
1180
|
-
We begin with concepts that have rules and behavior. We use `Email` for the email address, `string` for identity, and an entity for the user.
|
|
1181
|
-
|
|
1182
|
-
**`users/domain/user.ts`**
|
|
1183
|
-
|
|
1184
|
-
```ts
|
|
1185
|
-
import { Entity } from '../../shared/domain/entities.js'
|
|
1186
|
-
import {
|
|
1187
|
-
Email,
|
|
1188
|
-
NullableBoolean
|
|
1189
|
-
} from '../../shared/domain/value-objects.js'
|
|
1190
|
-
|
|
1191
|
-
export class User extends Entity {
|
|
1192
|
-
public constructor(
|
|
1193
|
-
public readonly id: string,
|
|
1194
|
-
public readonly email: Email,
|
|
1195
|
-
public readonly active: NullableBoolean
|
|
1196
|
-
) {
|
|
1197
|
-
super()
|
|
1198
|
-
}
|
|
1199
|
-
|
|
1200
|
-
public override equals(other: Entity): boolean {
|
|
1201
|
-
return other instanceof User && other.id === this.id
|
|
1202
|
-
}
|
|
1203
|
-
|
|
1204
|
-
public override toJSON(): Record<string, unknown> {
|
|
1205
|
-
return {
|
|
1206
|
-
id: this.id,
|
|
1207
|
-
email: this.email.value,
|
|
1208
|
-
active: this.active.value
|
|
1209
|
-
}
|
|
1210
|
-
}
|
|
1211
|
-
}
|
|
1212
|
-
```
|
|
1213
|
-
|
|
1214
|
-
`Email` provides the capability to validate and represent the email address. `User` provides user identity and representation.
|
|
1215
|
-
|
|
1216
|
-
### 2. Implement process validation
|
|
1217
|
-
|
|
1218
|
-
The validation prepares the input used by the use case.
|
|
1219
|
-
|
|
1220
|
-
**`users/application/create-user-validation.ts`**
|
|
1221
|
-
|
|
1222
|
-
```ts
|
|
1223
|
-
import type { Validatable } from '../../shared/application/validations.js'
|
|
1224
|
-
import { Email } from '../../shared/domain/value-objects.js'
|
|
1225
|
-
|
|
1226
|
-
export class CreateUserValidation implements Validatable {
|
|
1227
|
-
public constructor(private readonly email: string) {}
|
|
1228
|
-
|
|
1229
|
-
public isValid(): boolean {
|
|
1230
|
-
return Email.isValid(this.email)
|
|
1231
|
-
}
|
|
1232
|
-
|
|
1233
|
-
public validate(): string[] {
|
|
1234
|
-
return this.isValid()
|
|
1235
|
-
? []
|
|
1236
|
-
: ['The email is invalid.']
|
|
1237
|
-
}
|
|
1238
|
-
}
|
|
1239
|
-
```
|
|
1240
|
-
|
|
1241
|
-
### 3. Implement the application process
|
|
1242
|
-
|
|
1243
|
-
The service receives its collaborators as dependencies and applies domain capabilities to complete the registration.
|
|
1244
|
-
|
|
1245
|
-
**`users/application/create-user-service.ts`**
|
|
1246
|
-
|
|
1247
|
-
```ts
|
|
1248
|
-
import { Service } from '../../shared/application/services.js'
|
|
1249
|
-
import {
|
|
1250
|
-
Email,
|
|
1251
|
-
NullableBoolean
|
|
1252
|
-
} from '../../shared/domain/value-objects.js'
|
|
1253
|
-
import { User } from '../domain/user.js'
|
|
1254
|
-
import { CreateUserValidation } from './create-user-validation.js'
|
|
1255
|
-
|
|
1256
|
-
export interface UserWriter {
|
|
1257
|
-
save(user: User): Promise<void>
|
|
1258
|
-
}
|
|
1259
|
-
|
|
1260
|
-
export type CreateUserCommand = {
|
|
1261
|
-
id: string
|
|
1262
|
-
email: string
|
|
1263
|
-
}
|
|
1264
|
-
|
|
1265
|
-
export type CreatedUser = {
|
|
1266
|
-
id: string
|
|
1267
|
-
email: string
|
|
1268
|
-
active: boolean | null
|
|
1269
|
-
}
|
|
1270
|
-
|
|
1271
|
-
export class CreateUserService extends Service {
|
|
1272
|
-
public constructor(private readonly users: UserWriter) {
|
|
1273
|
-
super()
|
|
1274
|
-
}
|
|
1275
|
-
|
|
1276
|
-
public async execute(
|
|
1277
|
-
command: CreateUserCommand
|
|
1278
|
-
): Promise<CreatedUser> {
|
|
1279
|
-
const validation = new CreateUserValidation(command.email)
|
|
1280
|
-
const errors = validation.validate()
|
|
1281
|
-
|
|
1282
|
-
if (errors.length !== 0) {
|
|
1283
|
-
throw new Error(errors.join(', '))
|
|
1284
|
-
}
|
|
1285
|
-
|
|
1286
|
-
const user = new User(
|
|
1287
|
-
command.id,
|
|
1288
|
-
Email.from(command.email),
|
|
1289
|
-
NullableBoolean.from(true)
|
|
1290
|
-
)
|
|
1291
|
-
|
|
1292
|
-
await this.users.save(user)
|
|
1293
|
-
|
|
1294
|
-
return {
|
|
1295
|
-
id: user.id,
|
|
1296
|
-
email: user.email.value,
|
|
1297
|
-
active: user.active.value
|
|
1298
|
-
}
|
|
1299
|
-
}
|
|
1300
|
-
}
|
|
1301
|
-
```
|
|
1302
|
-
|
|
1303
|
-
`UserWriter` declares the operation the service requires from the adapter. The flow has four steps: validate, construct the entity, save, and respond.
|
|
1304
|
-
|
|
1305
|
-
### 4. Declare the communication port
|
|
1306
|
-
|
|
1307
|
-
The port defines how another system requests user creation.
|
|
1308
|
-
|
|
1309
|
-
**`users/index.ts`**
|
|
1310
|
-
|
|
1311
|
-
```ts
|
|
1312
|
-
export type CreateUserRequest = {
|
|
1313
|
-
id: string
|
|
1314
|
-
email: string
|
|
1315
|
-
}
|
|
1316
|
-
|
|
1317
|
-
export type CreateUserResponse = {
|
|
1318
|
-
id: string
|
|
1319
|
-
email: string
|
|
1320
|
-
active: boolean | null
|
|
1321
|
-
}
|
|
1322
|
-
|
|
1323
|
-
export interface CreateUserPort {
|
|
1324
|
-
create(
|
|
1325
|
-
request: CreateUserRequest
|
|
1326
|
-
): Promise<CreateUserResponse>
|
|
1327
|
-
}
|
|
1328
|
-
```
|
|
1329
|
-
|
|
1330
|
-
### 5. Implement the adapter
|
|
1331
|
-
|
|
1332
|
-
The adapter imports the port, receives the external request, and executes the application process.
|
|
1333
|
-
|
|
1334
|
-
**`users/adapters/create-user-adapter.ts`**
|
|
1335
|
-
|
|
1336
|
-
```ts
|
|
1337
|
-
import type {
|
|
1338
|
-
CreateUserPort,
|
|
1339
|
-
CreateUserRequest,
|
|
1340
|
-
CreateUserResponse
|
|
1341
|
-
} from '../index.js'
|
|
1342
|
-
import {
|
|
1343
|
-
CreateUserService,
|
|
1344
|
-
type CreateUserCommand
|
|
1345
|
-
} from '../application/create-user-service.js'
|
|
1346
|
-
|
|
1347
|
-
export class CreateUserAdapter implements CreateUserPort {
|
|
1348
|
-
public constructor(
|
|
1349
|
-
private readonly service: CreateUserService
|
|
1350
|
-
) {}
|
|
1351
|
-
|
|
1352
|
-
public async create(
|
|
1353
|
-
request: CreateUserRequest
|
|
1354
|
-
): Promise<CreateUserResponse> {
|
|
1355
|
-
const command: CreateUserCommand = {
|
|
1356
|
-
id: request.id,
|
|
1357
|
-
email: request.email
|
|
1358
|
-
}
|
|
1359
|
-
|
|
1360
|
-
const result = await this.service.execute(command)
|
|
1361
|
-
|
|
1362
|
-
return {
|
|
1363
|
-
id: result.id,
|
|
1364
|
-
email: result.email,
|
|
1365
|
-
active: result.active
|
|
1366
|
-
}
|
|
1367
|
-
}
|
|
1368
|
-
}
|
|
1369
|
-
```
|
|
1370
|
-
|
|
1371
|
-
The complete communication follows this path:
|
|
1372
|
-
|
|
1373
|
-
```text
|
|
1374
|
-
External request
|
|
1375
|
-
↓
|
|
1376
|
-
CreateUserAdapter
|
|
1377
|
-
↓
|
|
1378
|
-
CreateUserService
|
|
1379
|
-
↓
|
|
1380
|
-
Validation + Email + User
|
|
1381
|
-
↓
|
|
1382
|
-
UserWriter
|
|
1383
|
-
↓
|
|
1384
|
-
External response
|
|
1385
|
-
```
|
|
1386
|
-
|
|
1387
|
-
### 6. Compose the implementation in `main.ts`
|
|
1388
|
-
|
|
1389
|
-
`main.ts` brings together the library's main implementations. The composition creates the service and provides it to the adapter.
|
|
1390
|
-
|
|
1391
|
-
**`core/main.ts`**
|
|
1392
|
-
|
|
1393
|
-
```ts
|
|
1394
|
-
import { CreateUserService } from './users/application/create-user-service.js'
|
|
1395
|
-
import { CreateUserAdapter } from './users/adapters/create-user-adapter.js'
|
|
1396
|
-
import { InMemoryUserRepository } from './users/adapters/in-memory-user-repository.js'
|
|
1397
|
-
|
|
1398
|
-
export class CoreApplication {
|
|
1399
|
-
public readonly users: CreateUserAdapter
|
|
1400
|
-
|
|
1401
|
-
public constructor() {
|
|
1402
|
-
const users = new InMemoryUserRepository()
|
|
1403
|
-
const createUserService = new CreateUserService(users)
|
|
1404
|
-
|
|
1405
|
-
this.users = new CreateUserAdapter(createUserService)
|
|
1406
|
-
}
|
|
1407
|
-
}
|
|
1408
|
-
```
|
|
1409
|
-
|
|
1410
|
-
`CoreApplication` provides a minimal composition: a concrete repository, a service, and an adapter.
|
|
1411
|
-
|
|
1412
|
-
### 7. Use the implementation
|
|
1413
|
-
|
|
1414
|
-
The consumer uses the library's main entry point and accesses the adapter prepared in `main.ts`.
|
|
1415
|
-
|
|
1416
|
-
```ts
|
|
1417
|
-
import { CoreApplication } from './core/main.js'
|
|
1418
|
-
|
|
1419
|
-
const core = new CoreApplication()
|
|
1420
|
-
|
|
1421
|
-
const result = await core.users.create({
|
|
1422
|
-
id: 'user-1',
|
|
1423
|
-
email: 'alejandro@example.com'
|
|
1424
|
-
})
|
|
1425
|
-
```
|
|
1426
|
-
|
|
1427
|
-
The adapter receives the request, applies the port, executes the service, and returns the response.
|
|
1428
|
-
|
|
1429
|
-
## Generated file reference
|
|
1430
|
-
|
|
1431
|
-
| File | Purpose |
|
|
1432
|
-
| --- | --- |
|
|
1433
|
-
| `core/index.d.ts` | Declares `Generic<T>` for plain objects. |
|
|
1434
|
-
| `core/main.ts` | Contains the library's main implementation. |
|
|
1435
|
-
| `shared/domain/value-objects.ts` | Declares `ValueObject<T>` and implements `Email` and `NullableBoolean`. |
|
|
1436
|
-
| `shared/domain/entities.ts` | Declares the `Entity` base class. |
|
|
1437
|
-
| `shared/domain/aggregates.ts` | Declares the `Aggregate` base class. |
|
|
1438
|
-
| `shared/domain/errors.ts` | Implements `ValueError`. |
|
|
1439
|
-
| `shared/application/validations.ts` | Declares `Validatable`. |
|
|
1440
|
-
| `shared/application/services.ts` | Declares `Service` as the base class for use cases. |
|
|
1441
|
-
| `shared/application/http.ts` | Implements `HttpResponseBody` for REST responses and HATEOAS links. |
|
|
1442
|
-
| `shared/application/loggers.ts` | Declares log levels and the `Logger` contract. |
|
|
1443
|
-
| `shared/application/events.ts` | Declares `Event`, `EventHandler`, and `EventDispatcher`. |
|
|
1444
|
-
| `shared/application/data-sources.ts` | Declares source operations, managers, and repositories. |
|
|
1445
|
-
| `users/index.ts` | Declares the context's main communication ports. |
|
|
1446
|
-
| `users/example-ports.ts` | Shows an additional communication port file. |
|
|
1447
|
-
| `users/domain/` | Contains the context's capabilities. |
|
|
1448
|
-
| `users/application/` | Contains processes that apply domain capabilities to fulfill purposes. |
|
|
1449
|
-
| `users/adapters/` | Contains integrations that import ports and connect the context with other systems. |
|
|
165
|
+
- [shared/domain/aggregates.ts](./docs/shared/domain/aggregates.md)
|
|
166
|
+
- [shared/domain/entities.ts](./docs/shared/domain/entities.md)
|
|
167
|
+
- [shared/domain/errors.ts](./docs/shared/domain/errors.md)
|
|
168
|
+
- [shared/domain/value-objects.ts](./docs/shared/domain/value-objects.md)
|