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 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 continues with the implementation of each generated component.
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-sources.ts
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
- ├── example-ports.ts
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
- ## Library structure
143
+ ## Documentation index
142
144
 
143
- The library is divided into a root, a shared directory, and one or more contexts. Each level has a role in the implementation.
145
+ From this point on, the guide is split into dedicated documents under `docs/`.
144
146
 
145
- ### Root
147
+ ### General
146
148
 
147
- The root contains `index.d.ts` and `main.ts`.
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
- `index.d.ts` declares the types available to the library. `main.ts` contains the main implementation and the public components that belong directly to that entry point.
154
+ ### Shared application
150
155
 
151
- ### Shared code
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
- The `shared` directory contains code used by multiple contexts. It holds abstractions, interfaces, contracts, base types, and specific implementations with a common meaning across the library.
163
+ ### Shared domain
154
164
 
155
- ```text
156
- shared/
157
- ├── domain/
158
- └── application/
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)