tshex-cli 1.0.18 → 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 +336 -546
- package/package.json +1 -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
|
|
@@ -146,7 +148,7 @@ The library is divided into a root, a shared directory, and one or more contexts
|
|
|
146
148
|
|
|
147
149
|
The root contains `index.d.ts` and `main.ts`.
|
|
148
150
|
|
|
149
|
-
`index.d.ts` declares the types available to the library. `main.ts`
|
|
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.
|
|
150
152
|
|
|
151
153
|
### Shared code
|
|
152
154
|
|
|
@@ -195,7 +197,7 @@ An application service combines capabilities to complete an operation. For examp
|
|
|
195
197
|
|
|
196
198
|
#### Context root
|
|
197
199
|
|
|
198
|
-
The root contains `index.ts` and other `.ts` files
|
|
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.
|
|
199
201
|
|
|
200
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.
|
|
201
203
|
|
|
@@ -225,7 +227,7 @@ The application occupies the middle layer and uses those capabilities to build p
|
|
|
225
227
|
|
|
226
228
|
Ports and adapters occupy the outer layer and handle communication between systems.
|
|
227
229
|
|
|
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
|
|
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`.
|
|
229
231
|
|
|
230
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.
|
|
231
233
|
|
|
@@ -237,7 +239,7 @@ adapter → application
|
|
|
237
239
|
application → domain
|
|
238
240
|
```
|
|
239
241
|
|
|
240
|
-
|
|
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.
|
|
241
243
|
|
|
242
244
|
> **Tip:** start the implementation inside the context and move a component to `shared` when its meaning and use belong to multiple contexts.
|
|
243
245
|
|
|
@@ -286,16 +288,17 @@ The `shared/domain/value-objects.ts` file generates the `ValueObject<T>` base cl
|
|
|
286
288
|
```ts
|
|
287
289
|
import { Email } from './core/shared/domain/value-objects.js'
|
|
288
290
|
|
|
289
|
-
const email = Email.from('
|
|
291
|
+
const email = Email.from('ana@example.com')
|
|
290
292
|
|
|
291
293
|
email.value
|
|
294
|
+
email.username
|
|
292
295
|
email.domain
|
|
293
296
|
```
|
|
294
297
|
|
|
295
298
|
Creation is performed with `Email.from()`. This method runs `Email.isValid()` before constructing the instance.
|
|
296
299
|
|
|
297
300
|
```ts
|
|
298
|
-
Email.isValid('
|
|
301
|
+
Email.isValid('ana@example.com')
|
|
299
302
|
```
|
|
300
303
|
|
|
301
304
|
Every creation follows the same validity rule:
|
|
@@ -321,53 +324,53 @@ When validation fails, `from()` throws `ValueError`.
|
|
|
321
324
|
```ts
|
|
322
325
|
import { NullableBoolean } from './core/shared/domain/value-objects.js'
|
|
323
326
|
|
|
324
|
-
const
|
|
327
|
+
const active = NullableBoolean.from(null)
|
|
325
328
|
|
|
326
|
-
|
|
327
|
-
|
|
329
|
+
active.value
|
|
330
|
+
active.isIndeterminate()
|
|
328
331
|
```
|
|
329
332
|
|
|
330
333
|
The `isIndeterminate()` method expresses an operation specific to the concept and allows the `null` state to be checked with explicit intent.
|
|
331
334
|
|
|
332
335
|
#### Implement a value object
|
|
333
336
|
|
|
334
|
-
We will create a
|
|
335
|
-
|
|
336
|
-
**`sales/domain/discount-percentage.ts`**
|
|
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.
|
|
337
338
|
|
|
338
339
|
```ts
|
|
339
340
|
import { ValueError } from '../../shared/domain/errors.js'
|
|
340
341
|
import { ValueObject } from '../../shared/domain/value-objects.js'
|
|
341
342
|
|
|
342
|
-
export class
|
|
343
|
-
public override readonly value:
|
|
343
|
+
export class UserName extends ValueObject<string> {
|
|
344
|
+
public override readonly value: string
|
|
344
345
|
|
|
345
|
-
protected constructor(value:
|
|
346
|
+
protected constructor(value: string) {
|
|
346
347
|
super()
|
|
347
348
|
this.value = value
|
|
348
349
|
}
|
|
349
350
|
|
|
350
351
|
public override equals(
|
|
351
|
-
other:
|
|
352
|
+
other: UserName | null | undefined
|
|
352
353
|
): boolean {
|
|
353
|
-
return other instanceof
|
|
354
|
+
return other instanceof UserName &&
|
|
354
355
|
this.value === other.value
|
|
355
356
|
}
|
|
356
357
|
|
|
357
|
-
public
|
|
358
|
-
return
|
|
358
|
+
public normalized(): string {
|
|
359
|
+
return this.value.trim().toLowerCase()
|
|
359
360
|
}
|
|
360
361
|
|
|
361
362
|
public static override isValid(value: unknown): boolean {
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
363
|
+
if (super.isValid(value) === false) {
|
|
364
|
+
return false
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
return typeof value === 'string' &&
|
|
368
|
+
value.trim().length > 0
|
|
366
369
|
}
|
|
367
370
|
|
|
368
|
-
public static from(value:
|
|
371
|
+
public static from(value: string): UserName {
|
|
369
372
|
if (this.isValid(value) === false) {
|
|
370
|
-
throw new ValueError(
|
|
373
|
+
throw new ValueError(value, this.name)
|
|
371
374
|
}
|
|
372
375
|
|
|
373
376
|
return new this(value)
|
|
@@ -378,21 +381,19 @@ export class DiscountPercentage extends ValueObject<number> {
|
|
|
378
381
|
Now we can create and use the concept:
|
|
379
382
|
|
|
380
383
|
```ts
|
|
381
|
-
const
|
|
382
|
-
const
|
|
384
|
+
const name = UserName.from('Ana')
|
|
385
|
+
const normalized = name.normalized()
|
|
383
386
|
```
|
|
384
387
|
|
|
385
|
-
`isValid()` centralizes the rule. `from()` creates the valid instance. `
|
|
388
|
+
`isValid()` centralizes the rule. `from()` creates the valid instance. `normalized()` adds behavior specific to the concept.
|
|
386
389
|
|
|
387
390
|
### Entities
|
|
388
391
|
|
|
389
392
|
An entity represents a concept with its own identity. Two instances represent the same element when they share that identity.
|
|
390
393
|
|
|
391
|
-
The `shared/domain/entities.ts` file generates the `Entity` base class. Each entity implements `equals()
|
|
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.
|
|
392
395
|
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
**`users/domain/user.ts`**
|
|
396
|
+
This example creates an entity for the `users` context.
|
|
396
397
|
|
|
397
398
|
```ts
|
|
398
399
|
import { Entity } from '../../shared/domain/entities.js'
|
|
@@ -401,9 +402,14 @@ import {
|
|
|
401
402
|
NullableBoolean
|
|
402
403
|
} from '../../shared/domain/value-objects.js'
|
|
403
404
|
|
|
405
|
+
type UserName = {
|
|
406
|
+
value: string
|
|
407
|
+
}
|
|
408
|
+
|
|
404
409
|
export class User extends Entity {
|
|
405
410
|
public constructor(
|
|
406
411
|
public readonly id: string,
|
|
412
|
+
public readonly name: UserName,
|
|
407
413
|
public readonly email: Email,
|
|
408
414
|
public readonly active: NullableBoolean
|
|
409
415
|
) {
|
|
@@ -417,6 +423,7 @@ export class User extends Entity {
|
|
|
417
423
|
public override toJSON(): Record<string, unknown> {
|
|
418
424
|
return {
|
|
419
425
|
id: this.id,
|
|
426
|
+
name: this.name.value,
|
|
420
427
|
email: this.email.value,
|
|
421
428
|
active: this.active.value
|
|
422
429
|
}
|
|
@@ -431,50 +438,48 @@ The `id` property defines the identity. The `equals()` method compares entities
|
|
|
431
438
|
```ts
|
|
432
439
|
const user = new User(
|
|
433
440
|
'user-1',
|
|
434
|
-
|
|
441
|
+
UserName.from('Ana'),
|
|
442
|
+
Email.from('ana@example.com'),
|
|
435
443
|
NullableBoolean.from(true)
|
|
436
444
|
)
|
|
437
445
|
|
|
438
446
|
const json = user.toJSON()
|
|
439
447
|
```
|
|
440
448
|
|
|
441
|
-
The result uses the internal values of `Email
|
|
449
|
+
The result uses the internal values of `UserName`, `Email`, and `NullableBoolean`.
|
|
442
450
|
|
|
443
451
|
### Aggregates
|
|
444
452
|
|
|
445
453
|
An aggregate groups multiple entities into a logical unit. The aggregate's operations depend on all the identities that compose it.
|
|
446
454
|
|
|
447
|
-
A
|
|
455
|
+
A user list can group several users:
|
|
448
456
|
|
|
449
457
|
```text
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
└── members
|
|
458
|
+
UserList
|
|
459
|
+
└── items
|
|
453
460
|
```
|
|
454
461
|
|
|
455
462
|
Each element preserves its own identity within the unit.
|
|
456
463
|
|
|
457
|
-
**`users/domain/team.ts`**
|
|
458
|
-
|
|
459
464
|
```ts
|
|
460
465
|
import { Aggregate } from '../../shared/domain/aggregates.js'
|
|
461
|
-
import type { User } from './user.js'
|
|
462
466
|
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
467
|
+
type User = {
|
|
468
|
+
id: string
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
export class UserList extends Aggregate {
|
|
472
|
+
public constructor(public readonly items: User[]) {
|
|
468
473
|
super()
|
|
469
474
|
}
|
|
470
475
|
|
|
471
|
-
public
|
|
472
|
-
return this.
|
|
476
|
+
public count(): number {
|
|
477
|
+
return this.items.length
|
|
473
478
|
}
|
|
474
479
|
}
|
|
475
480
|
```
|
|
476
481
|
|
|
477
|
-
`
|
|
482
|
+
`UserList` groups multiple `User` entities into a single logical unit. The `count()` method operates on that group.
|
|
478
483
|
|
|
479
484
|
### Domain errors
|
|
480
485
|
|
|
@@ -483,8 +488,8 @@ The `shared/domain/errors.ts` file generates `ValueError`. This error represents
|
|
|
483
488
|
```ts
|
|
484
489
|
import { ValueError } from '../../shared/domain/errors.js'
|
|
485
490
|
|
|
486
|
-
if (
|
|
487
|
-
throw new ValueError(
|
|
491
|
+
if (userId.trim().length === 0) {
|
|
492
|
+
throw new ValueError(userId, 'UserId')
|
|
488
493
|
}
|
|
489
494
|
```
|
|
490
495
|
|
|
@@ -500,35 +505,34 @@ The `shared/application/validations.ts` file declares the `Validatable` contract
|
|
|
500
505
|
|
|
501
506
|
```ts
|
|
502
507
|
isValid(): boolean
|
|
503
|
-
validate(): unknown
|
|
504
508
|
```
|
|
505
509
|
|
|
506
|
-
`isValid()` checks the
|
|
510
|
+
`isValid()` checks whether the data is valid according to the rule defined by the implementation.
|
|
507
511
|
|
|
508
512
|
We will create a validation for the use case that registers users.
|
|
509
513
|
|
|
510
|
-
**`users/application/create-user-validation.ts`**
|
|
511
|
-
|
|
512
514
|
```ts
|
|
513
515
|
import type { Validatable } from '../../shared/application/validations.js'
|
|
514
516
|
import { Email } from '../../shared/domain/value-objects.js'
|
|
515
517
|
|
|
518
|
+
export type CreateUserValidationData = {
|
|
519
|
+
name: string
|
|
520
|
+
email: string
|
|
521
|
+
}
|
|
522
|
+
|
|
516
523
|
export class CreateUserValidation implements Validatable {
|
|
517
|
-
public constructor(
|
|
524
|
+
public constructor(
|
|
525
|
+
private readonly data: CreateUserValidationData
|
|
526
|
+
) {}
|
|
518
527
|
|
|
519
528
|
public isValid(): boolean {
|
|
520
|
-
return
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
public validate(): string[] {
|
|
524
|
-
return this.isValid()
|
|
525
|
-
? []
|
|
526
|
-
: ['The email is invalid.']
|
|
529
|
+
return this.data.name.trim().length > 0 &&
|
|
530
|
+
Email.isValid(this.data.email)
|
|
527
531
|
}
|
|
528
532
|
}
|
|
529
533
|
```
|
|
530
534
|
|
|
531
|
-
|
|
535
|
+
In this example, the application service runs this validation before constructing the `User` entity.
|
|
532
536
|
|
|
533
537
|
### Services
|
|
534
538
|
|
|
@@ -538,30 +542,59 @@ The `shared/application/services.ts` file generates the `Service` base class. A
|
|
|
538
542
|
|
|
539
543
|
We will begin with a service that creates a user.
|
|
540
544
|
|
|
541
|
-
**`users/application/create-user-service.ts`**
|
|
542
|
-
|
|
543
545
|
```ts
|
|
546
|
+
import type { Validatable } from '../../shared/application/validations.js'
|
|
544
547
|
import { Service } from '../../shared/application/services.js'
|
|
545
548
|
import {
|
|
546
549
|
Email,
|
|
547
550
|
NullableBoolean
|
|
548
551
|
} 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
552
|
|
|
556
|
-
export type
|
|
553
|
+
export type CreateUserData = {
|
|
557
554
|
id: string
|
|
555
|
+
name: string
|
|
558
556
|
email: string
|
|
559
557
|
}
|
|
560
558
|
|
|
561
|
-
export
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
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
|
+
}
|
|
565
598
|
}
|
|
566
599
|
|
|
567
600
|
export class CreateUserService extends Service {
|
|
@@ -569,78 +602,102 @@ export class CreateUserService extends Service {
|
|
|
569
602
|
super()
|
|
570
603
|
}
|
|
571
604
|
|
|
572
|
-
public async execute(
|
|
573
|
-
|
|
574
|
-
): Promise<CreatedUser> {
|
|
575
|
-
const validation = new CreateUserValidation(command.email)
|
|
576
|
-
const errors = validation.validate()
|
|
605
|
+
public async execute(data: CreateUserData): Promise<CreateUserResult> {
|
|
606
|
+
const validation = new CreateUserValidation(data)
|
|
577
607
|
|
|
578
|
-
if (
|
|
579
|
-
throw new Error(
|
|
608
|
+
if (validation.isValid() === false) {
|
|
609
|
+
throw new Error('The name or email is invalid.')
|
|
580
610
|
}
|
|
581
611
|
|
|
582
612
|
const user = new User(
|
|
583
|
-
|
|
584
|
-
|
|
613
|
+
data.id,
|
|
614
|
+
UserName.from(data.name),
|
|
615
|
+
Email.from(data.email),
|
|
585
616
|
NullableBoolean.from(true)
|
|
586
617
|
)
|
|
587
618
|
|
|
588
|
-
await this.users.save(user)
|
|
619
|
+
await this.users.save({ user })
|
|
589
620
|
|
|
590
621
|
return {
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
622
|
+
user: {
|
|
623
|
+
id: user.id,
|
|
624
|
+
name: user.name.value,
|
|
625
|
+
email: user.email.value,
|
|
626
|
+
active: user.active.value
|
|
627
|
+
}
|
|
594
628
|
}
|
|
595
629
|
}
|
|
596
630
|
}
|
|
597
631
|
```
|
|
598
632
|
|
|
599
|
-
|
|
633
|
+
In this example, the service receives one object, validates it, constructs the entity, delegates persistence, and returns another object with the result.
|
|
600
634
|
|
|
601
635
|
### HTTP responses
|
|
602
636
|
|
|
603
|
-
The `shared/application/http.ts` file generates `
|
|
637
|
+
The `shared/application/http.ts` file generates lightweight HTTP contracts: `HttpRequest`, `HttpResponse`, `HttpResponseBody`, `HttpRequestHandler`, and `HttpMiddleware`.
|
|
604
638
|
|
|
605
|
-
|
|
639
|
+
`HttpResponseBody` is an interface organized into three properties:
|
|
606
640
|
|
|
607
641
|
```ts
|
|
608
|
-
|
|
642
|
+
import type { HttpResponseBody } from '../../shared/application/http.js'
|
|
643
|
+
|
|
644
|
+
const body: HttpResponseBody = {
|
|
645
|
+
data,
|
|
646
|
+
errors,
|
|
647
|
+
links
|
|
648
|
+
}
|
|
609
649
|
```
|
|
610
650
|
|
|
611
651
|
`data` contains the operation data and accepts `null` when the response has no data:
|
|
612
652
|
|
|
613
653
|
```ts
|
|
614
|
-
const body =
|
|
654
|
+
const body: HttpResponseBody = {
|
|
655
|
+
data: {
|
|
656
|
+
id: 'user-1',
|
|
657
|
+
name: 'Ana'
|
|
658
|
+
},
|
|
659
|
+
errors: null,
|
|
660
|
+
links: null
|
|
661
|
+
}
|
|
615
662
|
```
|
|
616
663
|
|
|
617
664
|
`errors` contains a list of messages:
|
|
618
665
|
|
|
619
666
|
```ts
|
|
620
|
-
const body =
|
|
621
|
-
null,
|
|
622
|
-
['The email is invalid.']
|
|
623
|
-
|
|
667
|
+
const body: HttpResponseBody = {
|
|
668
|
+
data: null,
|
|
669
|
+
errors: ['The email is invalid.'],
|
|
670
|
+
links: null
|
|
671
|
+
}
|
|
624
672
|
```
|
|
625
673
|
|
|
626
674
|
`links` contains HATEOAS links related to the resource and its available operations:
|
|
627
675
|
|
|
628
676
|
```ts
|
|
629
|
-
const body =
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
677
|
+
const body: HttpResponseBody = {
|
|
678
|
+
data: {
|
|
679
|
+
id: 'user-1',
|
|
680
|
+
name: 'Ana'
|
|
681
|
+
},
|
|
682
|
+
errors: null,
|
|
683
|
+
links: {
|
|
633
684
|
self: new URL('https://api.example.com/users/user-1')
|
|
634
685
|
}
|
|
635
|
-
|
|
686
|
+
}
|
|
636
687
|
```
|
|
637
688
|
|
|
638
689
|
An entity's plain output can be used as `data`:
|
|
639
690
|
|
|
640
691
|
```ts
|
|
641
|
-
const body =
|
|
692
|
+
const body: HttpResponseBody = {
|
|
693
|
+
data: user.toJSON(),
|
|
694
|
+
errors: null,
|
|
695
|
+
links: null
|
|
696
|
+
}
|
|
642
697
|
```
|
|
643
698
|
|
|
699
|
+
`HttpRequestHandler` and `HttpMiddleware` define the contracts for adapters that receive a request, delegate to a handler, and produce a response.
|
|
700
|
+
|
|
644
701
|
### Logs
|
|
645
702
|
|
|
646
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.
|
|
@@ -649,8 +706,6 @@ The contract includes the `debug`, `info`, `warning`, `error`, and `critical` le
|
|
|
649
706
|
|
|
650
707
|
We will create a simple adapter that connects the contract to the console.
|
|
651
708
|
|
|
652
|
-
**`users/adapters/console-logger-adapter.ts`**
|
|
653
|
-
|
|
654
709
|
```ts
|
|
655
710
|
import { Logger } from '../../shared/application/loggers.js'
|
|
656
711
|
|
|
@@ -677,7 +732,7 @@ export class ConsoleLoggerAdapter extends Logger {
|
|
|
677
732
|
}
|
|
678
733
|
```
|
|
679
734
|
|
|
680
|
-
|
|
735
|
+
In this example, the service receives `Logger` as a dependency. The adapter decides where to send each level.
|
|
681
736
|
|
|
682
737
|
### Events
|
|
683
738
|
|
|
@@ -701,14 +756,12 @@ EventHandler
|
|
|
701
756
|
|
|
702
757
|
`Event` represents something that occurred in the application. It contains the event time and its plain details.
|
|
703
758
|
|
|
704
|
-
**`users/application/user-created.ts`**
|
|
705
|
-
|
|
706
759
|
```ts
|
|
707
760
|
import { Event } from '../../shared/application/events.js'
|
|
708
761
|
|
|
709
762
|
export class UserCreated extends Event {
|
|
710
|
-
public constructor(userId: string) {
|
|
711
|
-
super(Date.now(),
|
|
763
|
+
public constructor(details: { userId: string }) {
|
|
764
|
+
super(Date.now(), details)
|
|
712
765
|
}
|
|
713
766
|
}
|
|
714
767
|
```
|
|
@@ -717,8 +770,6 @@ export class UserCreated extends Event {
|
|
|
717
770
|
|
|
718
771
|
`EventHandler` represents a reaction to the event.
|
|
719
772
|
|
|
720
|
-
**`users/application/log-user-created.ts`**
|
|
721
|
-
|
|
722
773
|
```ts
|
|
723
774
|
import {
|
|
724
775
|
Event,
|
|
@@ -747,16 +798,16 @@ unsubscribe(key, handler)
|
|
|
747
798
|
dispatch(event)
|
|
748
799
|
```
|
|
749
800
|
|
|
750
|
-
|
|
801
|
+
In one possible implementation, the service can receive `EventDispatcher` and publish `UserCreated` after completing the use case.
|
|
751
802
|
|
|
752
803
|
## Data sources
|
|
753
804
|
|
|
754
|
-
The `shared/application/data
|
|
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`.
|
|
755
806
|
|
|
756
807
|
The complete flow looks like this:
|
|
757
808
|
|
|
758
809
|
```text
|
|
759
|
-
|
|
810
|
+
DriverAdapter
|
|
760
811
|
↓ connects and enables
|
|
761
812
|
DataManager or DatasetManager
|
|
762
813
|
↓ provides plain data to
|
|
@@ -769,37 +820,47 @@ Service
|
|
|
769
820
|
|
|
770
821
|
We will begin with the connection and proceed to the use case.
|
|
771
822
|
|
|
772
|
-
### Driver
|
|
823
|
+
### Driver adapter
|
|
773
824
|
|
|
774
|
-
`
|
|
825
|
+
`DriverAdapter` connects and disconnects the source through a driver. When the connection is available, `connect()` returns an enabled data manager.
|
|
775
826
|
|
|
776
827
|
```ts
|
|
777
828
|
connect(...args): Promise<DataManager>
|
|
778
829
|
disconnect(): Promise<unknown>
|
|
779
830
|
```
|
|
780
831
|
|
|
781
|
-
We will create
|
|
782
|
-
|
|
783
|
-
**`users/adapters/people-driver-manager.ts`**
|
|
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.
|
|
784
833
|
|
|
785
834
|
```ts
|
|
786
|
-
import {
|
|
787
|
-
import {
|
|
835
|
+
import { DriverAdapter } from '../../shared/application/data/drivers.js'
|
|
836
|
+
import { DataManager } from '../../shared/application/data/managers.js'
|
|
788
837
|
|
|
789
|
-
|
|
838
|
+
type UserRecord = {
|
|
790
839
|
id: string
|
|
791
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
|
+
}
|
|
792
853
|
}
|
|
793
854
|
|
|
794
|
-
export class
|
|
795
|
-
extends
|
|
855
|
+
export class UserDriverAdapter
|
|
856
|
+
extends DriverAdapter<UserDataManager> {
|
|
796
857
|
|
|
797
|
-
public constructor(private readonly records:
|
|
858
|
+
public constructor(private readonly records: UserRecord[]) {
|
|
798
859
|
super()
|
|
799
860
|
}
|
|
800
861
|
|
|
801
|
-
public async connect(): Promise<
|
|
802
|
-
return new
|
|
862
|
+
public async connect(): Promise<UserDataManager> {
|
|
863
|
+
return new UserDataManager(this.records)
|
|
803
864
|
}
|
|
804
865
|
|
|
805
866
|
public async disconnect(): Promise<void> {
|
|
@@ -811,54 +872,52 @@ The concrete implementation can encapsulate a database driver, an HTTP client, a
|
|
|
811
872
|
|
|
812
873
|
### Data manager
|
|
813
874
|
|
|
814
|
-
`DataManager` operates on the source and works with plain objects and arrays. Its base form
|
|
875
|
+
`DataManager` operates on the source and works with plain objects and arrays. Its base form exposes one abstract operation and one default operation:
|
|
815
876
|
|
|
816
877
|
```ts
|
|
817
878
|
all(): Promise<Array<T>>
|
|
818
879
|
none(): Array<T>
|
|
819
880
|
```
|
|
820
881
|
|
|
821
|
-
`all()` retrieves the available records. `none()` creates an empty typed collection.
|
|
882
|
+
`all()` retrieves the available records. `none()` already creates an empty typed collection.
|
|
822
883
|
|
|
823
884
|
First, we will define the shape used by the source:
|
|
824
885
|
|
|
825
886
|
```ts
|
|
826
|
-
type
|
|
887
|
+
type UserRecord = {
|
|
827
888
|
id: string
|
|
828
889
|
name: string
|
|
890
|
+
email: string
|
|
891
|
+
active: boolean | null
|
|
829
892
|
}
|
|
830
893
|
```
|
|
831
894
|
|
|
832
895
|
Now we will implement the data manager.
|
|
833
896
|
|
|
834
|
-
**`users/adapters/people-data-manager.ts`**
|
|
835
|
-
|
|
836
897
|
```ts
|
|
837
|
-
import { DataManager } from '../../shared/application/data
|
|
898
|
+
import { DataManager } from '../../shared/application/data/managers.js'
|
|
838
899
|
|
|
839
|
-
export type
|
|
900
|
+
export type UserRecord = {
|
|
840
901
|
id: string
|
|
841
902
|
name: string
|
|
903
|
+
email: string
|
|
904
|
+
active: boolean | null
|
|
842
905
|
}
|
|
843
906
|
|
|
844
|
-
export class
|
|
845
|
-
extends DataManager<
|
|
907
|
+
export class UserDataManager
|
|
908
|
+
extends DataManager<UserRecord> {
|
|
846
909
|
|
|
847
|
-
public constructor(private readonly records:
|
|
910
|
+
public constructor(private readonly records: UserRecord[]) {
|
|
848
911
|
super()
|
|
849
912
|
}
|
|
850
913
|
|
|
851
|
-
public async all(): Promise<
|
|
914
|
+
public async all(): Promise<UserRecord[]> {
|
|
852
915
|
return this.records
|
|
853
916
|
}
|
|
854
|
-
|
|
855
|
-
public none(): PersonRecord[] {
|
|
856
|
-
return []
|
|
857
|
-
}
|
|
858
917
|
}
|
|
859
918
|
```
|
|
860
919
|
|
|
861
|
-
The data manager reflects the structure of the source. In this example, it only
|
|
920
|
+
The data manager reflects the structure of the source. In this example, it only exposes plain records.
|
|
862
921
|
|
|
863
922
|
#### Source operations
|
|
864
923
|
|
|
@@ -881,36 +940,32 @@ import {
|
|
|
881
940
|
Creatable,
|
|
882
941
|
DataManager,
|
|
883
942
|
Filterable
|
|
884
|
-
} from '../../shared/application/data
|
|
943
|
+
} from '../../shared/application/data/managers.js'
|
|
885
944
|
|
|
886
|
-
export class
|
|
887
|
-
extends DataManager<
|
|
945
|
+
export class UserDataManager
|
|
946
|
+
extends DataManager<UserRecord>
|
|
888
947
|
implements
|
|
889
|
-
Filterable<Partial<
|
|
890
|
-
Creatable<
|
|
948
|
+
Filterable<Partial<UserRecord>>,
|
|
949
|
+
Creatable<UserRecord> {
|
|
891
950
|
|
|
892
|
-
public constructor(private readonly records:
|
|
951
|
+
public constructor(private readonly records: UserRecord[]) {
|
|
893
952
|
super()
|
|
894
953
|
}
|
|
895
954
|
|
|
896
|
-
public async all(): Promise<
|
|
955
|
+
public async all(): Promise<UserRecord[]> {
|
|
897
956
|
return this.records
|
|
898
957
|
}
|
|
899
958
|
|
|
900
|
-
public none(): PersonRecord[] {
|
|
901
|
-
return []
|
|
902
|
-
}
|
|
903
|
-
|
|
904
959
|
public async filter(
|
|
905
|
-
selector: Partial<
|
|
906
|
-
): Promise<
|
|
960
|
+
selector: Partial<UserRecord>
|
|
961
|
+
): Promise<UserRecord[]> {
|
|
907
962
|
return this.records.filter((record) =>
|
|
908
963
|
(selector.id === undefined || record.id === selector.id) &&
|
|
909
|
-
(selector.
|
|
964
|
+
(selector.email === undefined || record.email === selector.email)
|
|
910
965
|
)
|
|
911
966
|
}
|
|
912
967
|
|
|
913
|
-
public async create(data:
|
|
968
|
+
public async create(data: UserRecord): Promise<void> {
|
|
914
969
|
this.records.push(data)
|
|
915
970
|
}
|
|
916
971
|
}
|
|
@@ -919,8 +974,8 @@ export class PeopleDataManager
|
|
|
919
974
|
It can also declare operations specific to source queries:
|
|
920
975
|
|
|
921
976
|
```ts
|
|
922
|
-
public async
|
|
923
|
-
return this.filter({
|
|
977
|
+
public async findByEmail(query: { email: string }): Promise<UserRecord[]> {
|
|
978
|
+
return this.filter({ email: query.email })
|
|
924
979
|
}
|
|
925
980
|
```
|
|
926
981
|
|
|
@@ -934,7 +989,7 @@ The application layer decides when to execute these operations, combines their r
|
|
|
934
989
|
union()
|
|
935
990
|
intersection()
|
|
936
991
|
difference()
|
|
937
|
-
|
|
992
|
+
symmetricDifference()
|
|
938
993
|
complement()
|
|
939
994
|
```
|
|
940
995
|
|
|
@@ -945,143 +1000,138 @@ This implementation is useful when an operation works with unions, intersections
|
|
|
945
1000
|
`Repository` acts as an intermediary between plain data and domain objects.
|
|
946
1001
|
|
|
947
1002
|
```text
|
|
948
|
-
|
|
1003
|
+
UserRecord
|
|
949
1004
|
↓ transform
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
Person
|
|
953
|
-
↓ toRecord
|
|
954
|
-
PersonRecord
|
|
1005
|
+
User
|
|
955
1006
|
```
|
|
956
1007
|
|
|
957
|
-
|
|
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.
|
|
958
1009
|
|
|
959
|
-
|
|
1010
|
+
Now we will implement the repository.
|
|
960
1011
|
|
|
961
1012
|
```ts
|
|
962
|
-
import {
|
|
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'
|
|
963
1020
|
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
}
|
|
1021
|
+
type UserRecord = {
|
|
1022
|
+
id: string
|
|
1023
|
+
name: string
|
|
1024
|
+
email: string
|
|
1025
|
+
active: boolean | null
|
|
1026
|
+
}
|
|
971
1027
|
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
}
|
|
1028
|
+
class UserName {
|
|
1029
|
+
public constructor(public readonly value: string) {}
|
|
975
1030
|
|
|
976
|
-
public
|
|
977
|
-
return
|
|
978
|
-
id: this.id,
|
|
979
|
-
name: this.name
|
|
980
|
-
}
|
|
1031
|
+
public static from(value: string): UserName {
|
|
1032
|
+
return new UserName(value)
|
|
981
1033
|
}
|
|
982
1034
|
}
|
|
983
|
-
```
|
|
984
|
-
|
|
985
|
-
Now we will implement the repository.
|
|
986
1035
|
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
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
|
|
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
|
+
}
|
|
1002
1044
|
|
|
1003
|
-
|
|
1004
|
-
|
|
1045
|
+
export class UserRepository
|
|
1046
|
+
extends Repository<UserRecord, User> {
|
|
1005
1047
|
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
}
|
|
1048
|
+
public constructor(
|
|
1049
|
+
driver: DriverAdapter<DataManager<UserRecord>>
|
|
1050
|
+
) {
|
|
1051
|
+
super(driver)
|
|
1011
1052
|
}
|
|
1012
1053
|
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
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)
|
|
1019
1060
|
)
|
|
1020
1061
|
}
|
|
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
1062
|
}
|
|
1028
1063
|
```
|
|
1029
1064
|
|
|
1030
|
-
`transform()` converts
|
|
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.
|
|
1031
1066
|
|
|
1032
1067
|
### Queries and errors in the application
|
|
1033
1068
|
|
|
1034
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.
|
|
1035
1070
|
|
|
1036
|
-
**`users/application/list-people-service.ts`**
|
|
1037
|
-
|
|
1038
1071
|
```ts
|
|
1039
1072
|
import { Service } from '../../shared/application/services.js'
|
|
1040
|
-
import type { Person } from '../domain/person.js'
|
|
1041
1073
|
|
|
1042
|
-
|
|
1043
|
-
|
|
1074
|
+
type User = {
|
|
1075
|
+
id: string
|
|
1044
1076
|
}
|
|
1045
1077
|
|
|
1046
|
-
export
|
|
1047
|
-
|
|
1078
|
+
export interface UsersReader {
|
|
1079
|
+
all(): Promise<User[]>
|
|
1080
|
+
}
|
|
1081
|
+
|
|
1082
|
+
export class ListUsersService extends Service {
|
|
1083
|
+
public constructor(private readonly users: UsersReader) {
|
|
1048
1084
|
super()
|
|
1049
1085
|
}
|
|
1050
1086
|
|
|
1051
|
-
public async execute(): Promise<
|
|
1087
|
+
public async execute(): Promise<{ users: User[] }> {
|
|
1052
1088
|
try {
|
|
1053
|
-
return
|
|
1089
|
+
return {
|
|
1090
|
+
users: await this.users.all()
|
|
1091
|
+
}
|
|
1054
1092
|
} catch {
|
|
1055
|
-
throw new Error('Could not list
|
|
1093
|
+
throw new Error('Could not list users.')
|
|
1056
1094
|
}
|
|
1057
1095
|
}
|
|
1058
1096
|
}
|
|
1059
1097
|
```
|
|
1060
1098
|
|
|
1061
|
-
`
|
|
1099
|
+
`UsersReader` expresses the collaboration required by the process. `UserRepository`, located in `adapters`, already satisfies that collaboration through the inherited `all()` method.
|
|
1062
1100
|
|
|
1063
1101
|
## Context ports
|
|
1064
1102
|
|
|
1065
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.
|
|
1066
1104
|
|
|
1067
|
-
The context generates `
|
|
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.
|
|
1068
1106
|
|
|
1069
1107
|
### Main port
|
|
1070
1108
|
|
|
1071
|
-
|
|
1109
|
+
The generated `example-ports.ts` file is only a placeholder:
|
|
1072
1110
|
|
|
1073
|
-
|
|
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:
|
|
1074
1118
|
|
|
1075
1119
|
```ts
|
|
1076
1120
|
export type CreateUserRequest = {
|
|
1077
|
-
|
|
1078
|
-
|
|
1121
|
+
user: {
|
|
1122
|
+
id: string
|
|
1123
|
+
name: string
|
|
1124
|
+
email: string
|
|
1125
|
+
}
|
|
1079
1126
|
}
|
|
1080
1127
|
|
|
1081
1128
|
export type CreateUserResponse = {
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1129
|
+
user: {
|
|
1130
|
+
id: string
|
|
1131
|
+
name: string
|
|
1132
|
+
email: string
|
|
1133
|
+
active: boolean | null
|
|
1134
|
+
}
|
|
1085
1135
|
}
|
|
1086
1136
|
|
|
1087
1137
|
export interface CreateUserPort {
|
|
@@ -1095,20 +1145,22 @@ export interface CreateUserPort {
|
|
|
1095
1145
|
|
|
1096
1146
|
### Adapt the port to the application process
|
|
1097
1147
|
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
**`users/adapters/create-user-adapter.ts`**
|
|
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.
|
|
1101
1149
|
|
|
1102
1150
|
```ts
|
|
1103
1151
|
import type {
|
|
1104
1152
|
CreateUserPort,
|
|
1105
1153
|
CreateUserRequest,
|
|
1106
1154
|
CreateUserResponse
|
|
1107
|
-
} from '../
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
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
|
+
}
|
|
1112
1164
|
|
|
1113
1165
|
export class CreateUserAdapter implements CreateUserPort {
|
|
1114
1166
|
public constructor(
|
|
@@ -1118,332 +1170,70 @@ export class CreateUserAdapter implements CreateUserPort {
|
|
|
1118
1170
|
public async create(
|
|
1119
1171
|
request: CreateUserRequest
|
|
1120
1172
|
): Promise<CreateUserResponse> {
|
|
1121
|
-
|
|
1122
|
-
id: request.id,
|
|
1123
|
-
|
|
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
|
-
}
|
|
1173
|
+
return this.service.execute({
|
|
1174
|
+
id: request.user.id,
|
|
1175
|
+
name: request.user.name,
|
|
1176
|
+
email: request.user.email
|
|
1177
|
+
})
|
|
1133
1178
|
}
|
|
1134
1179
|
}
|
|
1135
1180
|
```
|
|
1136
1181
|
|
|
1137
|
-
|
|
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.
|
|
1138
1183
|
|
|
1139
1184
|
### Additional ports
|
|
1140
1185
|
|
|
1141
1186
|
A context can organize its communications across several files at the root. Each file declares the ports for a group of interactions.
|
|
1142
1187
|
|
|
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
1188
|
```ts
|
|
1165
|
-
|
|
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
|
|
1189
|
+
export type ListUsersRequest = {
|
|
1268
1190
|
active: boolean | null
|
|
1269
1191
|
}
|
|
1270
1192
|
|
|
1271
|
-
export
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
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
|
|
1193
|
+
export type ListUsersResponse = {
|
|
1194
|
+
users: Array<{
|
|
1195
|
+
id: string
|
|
1196
|
+
name: string
|
|
1197
|
+
email: string
|
|
1198
|
+
active: boolean | null
|
|
1199
|
+
}>
|
|
1321
1200
|
}
|
|
1322
1201
|
|
|
1323
|
-
export interface
|
|
1324
|
-
|
|
1325
|
-
request: CreateUserRequest
|
|
1326
|
-
): Promise<CreateUserResponse>
|
|
1202
|
+
export interface ListUsersPort {
|
|
1203
|
+
list(request: ListUsersRequest): Promise<ListUsersResponse>
|
|
1327
1204
|
}
|
|
1328
1205
|
```
|
|
1329
1206
|
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
The adapter imports the port, receives the external request, and executes the application process.
|
|
1333
|
-
|
|
1334
|
-
**`users/adapters/create-user-adapter.ts`**
|
|
1207
|
+
An adapter can import the contract from the file where it is declared:
|
|
1335
1208
|
|
|
1336
1209
|
```ts
|
|
1337
1210
|
import type {
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
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
|
-
})
|
|
1211
|
+
ListUsersPort,
|
|
1212
|
+
ListUsersResponse
|
|
1213
|
+
} from '../example-ports.js'
|
|
1425
1214
|
```
|
|
1426
1215
|
|
|
1427
|
-
|
|
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.
|
|
1428
1217
|
|
|
1429
1218
|
## Generated file reference
|
|
1430
1219
|
|
|
1431
1220
|
| File | Purpose |
|
|
1432
1221
|
| --- | --- |
|
|
1433
1222
|
| `core/index.d.ts` | Declares `Generic<T>` for plain objects. |
|
|
1434
|
-
| `core/main.ts` |
|
|
1223
|
+
| `core/main.ts` | Starts as a placeholder for the library's main implementation and exports. |
|
|
1435
1224
|
| `shared/domain/value-objects.ts` | Declares `ValueObject<T>` and implements `Email` and `NullableBoolean`. |
|
|
1436
1225
|
| `shared/domain/entities.ts` | Declares the `Entity` base class. |
|
|
1437
1226
|
| `shared/domain/aggregates.ts` | Declares the `Aggregate` base class. |
|
|
1438
1227
|
| `shared/domain/errors.ts` | Implements `ValueError`. |
|
|
1439
1228
|
| `shared/application/validations.ts` | Declares `Validatable`. |
|
|
1440
1229
|
| `shared/application/services.ts` | Declares `Service` as the base class for use cases. |
|
|
1441
|
-
| `shared/application/http.ts` |
|
|
1230
|
+
| `shared/application/http.ts` | Declares HTTP request, response, body, handler, and middleware contracts. |
|
|
1442
1231
|
| `shared/application/loggers.ts` | Declares log levels and the `Logger` contract. |
|
|
1443
1232
|
| `shared/application/events.ts` | Declares `Event`, `EventHandler`, and `EventDispatcher`. |
|
|
1444
|
-
| `shared/application/data
|
|
1445
|
-
| `
|
|
1446
|
-
| `
|
|
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. |
|
|
1447
1237
|
| `users/domain/` | Contains the context's capabilities. |
|
|
1448
1238
|
| `users/application/` | Contains processes that apply domain capabilities to fulfill purposes. |
|
|
1449
1239
|
| `users/adapters/` | Contains integrations that import ports and connect the context with other systems. |
|