tshex-cli 1.0.16 → 1.0.18

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/README.es.md +1449 -0
  2. package/README.md +1449 -0
  3. package/build/main.js +11 -38
  4. package/package.json +8 -10
  5. package/source/main.ts +11 -44
  6. package/templates/ctx/example-ports.ts +5 -0
  7. package/templates/lib/shared/application/data-sources.ts +102 -0
  8. package/templates/lib/shared/application/http.ts +28 -40
  9. package/templates/lib/shared/application/loggers.ts +6 -30
  10. package/templates/lib/shared/application/validations.ts +10 -0
  11. package/readme.md +0 -229
  12. package/templates/ctx/index.ts +0 -0
  13. package/templates/lib/shared/application/databases.ts +0 -81
  14. package/templates/react-project/core/context/adapters/api/.gitkeep +0 -0
  15. package/templates/react-project/core/context/adapters/hooks/.gitkeep +0 -0
  16. package/templates/react-project/core/context/adapters/schemas/.gitkeep +0 -0
  17. package/templates/react-project/core/context/application/.gitkeep +0 -0
  18. package/templates/react-project/core/context/domain/.gitkeep +0 -0
  19. package/templates/react-project/core/context/languages/.gitkeep +0 -0
  20. package/templates/react-project/core/shared/adapters/i18n/.gitkeep +0 -0
  21. package/templates/react-project/dom-client/context/Example.tsx +0 -11
  22. package/templates/react-project/dom-client/context/assets/.gitkeep +0 -0
  23. package/templates/react-project/dom-client/context/styles/.gitkeep +0 -0
  24. package/templates/react-project/dom-server/context/Example.tsx +0 -11
  25. package/templates/react-project/dom-server/context/assets/.gitkeep +0 -0
  26. package/templates/react-project/dom-server/context/styles/.gitkeep +0 -0
  27. package/templates/react-project/index.d.ts +0 -1
  28. package/templates/react-project/native/context/Example.tsx +0 -11
  29. package/templates/react-project/native/context/assets/.gitkeep +0 -0
  30. package/templates/react-project/native/context/styles/.gitkeep +0 -0
  31. package/templates/react-project/tests/core/.gitkeep +0 -0
  32. package/templates/react-project/tests/dom-client/.gitkeep +0 -0
  33. package/templates/react-project/tests/dom-server/.gitkeep +0 -0
  34. package/templates/react-project/tests/native/.gitkeep +0 -0
  35. /package/templates/{react-context → ctx-react}/adapters/api/.gitkeep +0 -0
  36. /package/templates/{react-context → ctx-react}/adapters/hooks/.gitkeep +0 -0
  37. /package/templates/{react-context → ctx-react}/adapters/schemas/.gitkeep +0 -0
  38. /package/templates/{react-context → ctx-react}/application/.gitkeep +0 -0
  39. /package/templates/{react-context → ctx-react}/domain/.gitkeep +0 -0
  40. /package/templates/{react-context → ctx-react}/languages/.gitkeep +0 -0
package/README.es.md ADDED
@@ -0,0 +1,1449 @@
1
+ # CLI de Arquitectura Hexagonal `tshex-cli`
2
+
3
+ `tshex-cli` crea la estructura base de una librería organizada por contextos. La estructura reúne contratos compartidos, conceptos de dominio, casos de uso y adaptadores en directorios con responsabilidades definidas.
4
+
5
+ En esta guía construiremos una librería llamada `core` con un contexto llamado `users`. El recorrido comienza con el CLI y continúa con la implementación de cada pieza generada.
6
+
7
+ Los ejemplos de esta guía son intencionalmente simples. Buscan mostrar la responsabilidad de cada pieza, no cubrir infraestructura real ni casos de producción.
8
+
9
+ ## ¿Por qué?
10
+
11
+ La arquitectura hexagonal es un patrón de diseño de software que separa el dominio central de la aplicación de las dependencias externas. Esta separación se logra al dividir la aplicación en capas. Cada capa tiene una responsabilidad específica e interactúa con las demás de una forma concreta.
12
+
13
+ El objetivo es separar el código de dominio de las dependencias instaladas. Un framework de arquitectura hexagonal iría en contra de ese objetivo, porque acopla el código de dominio al propio framework. Por eso existe este CLI: genera por ti la estructura base, creando un código que posees y controlas, en lugar de un framework.
14
+
15
+ ## Primeros pasos
16
+
17
+ ### Instalación
18
+
19
+ Instala el paquete dentro de tu proyecto Node.js:
20
+
21
+ ```bash
22
+ npm install tshex-cli
23
+ ```
24
+
25
+ O instálalo de forma global:
26
+
27
+ ```bash
28
+ npm install -g tshex-cli
29
+ ```
30
+
31
+ El paquete registra el ejecutable `tshex`. Puedes invocarlo con `npx` desde el directorio del proyecto.
32
+
33
+ ### Ayuda
34
+
35
+ Consulta los comandos y opciones disponibles con:
36
+
37
+ ```bash
38
+ npx tshex --help
39
+ ```
40
+
41
+ ### Crear una librería
42
+
43
+ La opción `--lib` recibe el nombre del directorio raíz de la librería:
44
+
45
+ ```bash
46
+ npx tshex --lib core
47
+ ```
48
+
49
+ En este ejemplo, `core` será el espacio que contendrá el código compartido, los contextos y la implementación principal de la librería.
50
+
51
+ ### Crear un contexto
52
+
53
+ La opción `--ctx` recibe el nombre del contexto:
54
+
55
+ ```bash
56
+ npx tshex --ctx users
57
+ ```
58
+
59
+ Un contexto agrupa las reglas y operaciones de una capacidad de la aplicación. `users`, `sales`, `billing` e `inventory` son ejemplos de contextos.
60
+
61
+ ### Crear la librería y el primer contexto
62
+
63
+ Puedes generar ambas piezas en una sola ejecución:
64
+
65
+ ```bash
66
+ npx tshex --lib core --ctx users
67
+ ```
68
+
69
+ El comando crea esta estructura:
70
+
71
+ ```text
72
+ core/
73
+ ├── index.d.ts
74
+ ├── main.ts
75
+ ├── shared/
76
+ │ ├── application/
77
+ │ │ ├── data-sources.ts
78
+ │ │ ├── events.ts
79
+ │ │ ├── http.ts
80
+ │ │ ├── loggers.ts
81
+ │ │ ├── services.ts
82
+ │ │ └── validations.ts
83
+ │ └── domain/
84
+ │ ├── aggregates.ts
85
+ │ ├── entities.ts
86
+ │ ├── errors.ts
87
+ │ └── value-objects.ts
88
+ └── users/
89
+ ├── adapters/
90
+ ├── application/
91
+ ├── domain/
92
+ ├── example-ports.ts
93
+ └── index.ts
94
+ ```
95
+
96
+ ### Elegir el directorio de destino
97
+
98
+ La opción `--dir` indica el directorio desde el cual se crea la estructura:
99
+
100
+ ```bash
101
+ npx tshex --dir ./src --lib core --ctx users
102
+ ```
103
+
104
+ La librería del ejemplo queda ubicada en `src/core`.
105
+
106
+ Para agregar un contexto a una librería existente, utiliza la librería como directorio de destino:
107
+
108
+ ```bash
109
+ npx tshex --dir ./core --ctx billing
110
+ ```
111
+
112
+ El contexto queda ubicado en `core/billing`.
113
+
114
+ ### Crear un contexto para React
115
+
116
+ La opción `--react` crea un contexto con directorios orientados a una aplicación React:
117
+
118
+ ```bash
119
+ npx tshex --ctx users --react
120
+ ```
121
+
122
+ También puedes usar su forma corta:
123
+
124
+ ```bash
125
+ npx tshex --ctx users -R
126
+ ```
127
+
128
+ La estructura del contexto contiene adaptadores para API, hooks y schemas, junto con aplicación, dominio y recursos de idioma:
129
+
130
+ ```text
131
+ users/
132
+ ├── adapters/
133
+ │ ├── api/
134
+ │ ├── hooks/
135
+ │ └── schemas/
136
+ ├── application/
137
+ ├── domain/
138
+ └── languages/
139
+ ```
140
+
141
+ ## Estructura de la librería
142
+
143
+ La librería se divide en una raíz, un directorio compartido y uno o más contextos. Cada nivel tiene una función dentro de la implementación.
144
+
145
+ ### Raíz
146
+
147
+ La raíz contiene `index.d.ts` y `main.ts`.
148
+
149
+ `index.d.ts` declara tipos disponibles para la librería. `main.ts` contiene la implementación principal y las piezas públicas que pertenecen directamente a esa entrada.
150
+
151
+ ### Código compartido
152
+
153
+ El directorio `shared` contiene código utilizado por varios contextos. Aquí se ubican abstracciones, interfaces, contratos, tipos base e implementaciones puntuales con significado común dentro de la librería.
154
+
155
+ ```text
156
+ shared/
157
+ ├── domain/
158
+ └── application/
159
+ ```
160
+
161
+ `shared/domain` contiene bases para modelar conceptos del negocio. `shared/application` contiene contratos para coordinar casos de uso, fuentes de datos, eventos, validaciones, respuestas HTTP y logs.
162
+
163
+ ### Contextos
164
+
165
+ Un contexto representa una capacidad de la aplicación y agrupa su vocabulario, sus reglas y sus operaciones.
166
+
167
+ ```text
168
+ users/
169
+ billing/
170
+ sales/
171
+ inventory/
172
+ ```
173
+
174
+ La separación por contextos organiza una aplicación alrededor de sus capacidades. Varios contextos pueden formar parte del mismo proyecto, del mismo proceso y de la misma fuente de datos. Cada contexto conserva sus propias reglas mientras comparte las abstracciones generales de `shared`.
175
+
176
+ Cada contexto generado contiene tres directorios:
177
+
178
+ ```text
179
+ domain/
180
+ application/
181
+ adapters/
182
+ ```
183
+
184
+ #### `domain`
185
+
186
+ Contiene las capacidades del contexto. Una capacidad reúne conocimiento del negocio que puede utilizarse en distintos procesos: validar un correo, identificar una entidad, calcular un precio, cambiar el estado de una orden o agrupar las partes de una venta.
187
+
188
+ Los objetos de valor, las entidades y los agregados materializan estas capacidades mediante datos, reglas y comportamiento.
189
+
190
+ #### `application`
191
+
192
+ Contiene la aplicación de las capacidades del dominio en procesos que cumplen propósitos del sistema.
193
+
194
+ Un servicio de aplicación combina capacidades para completar una operación. Por ejemplo, el proceso de registrar un usuario puede validar el correo, construir la entidad, guardar sus datos, publicar un evento y registrar el resultado.
195
+
196
+ #### Raíz del contexto
197
+
198
+ La raíz contiene `index.ts` y otros archivos `.ts` destinados a los puertos del contexto.
199
+
200
+ Un puerto describe una forma de comunicación entre el contexto y otro sistema. Define los datos recibidos, los datos entregados y la operación disponible en esa frontera.
201
+
202
+ #### `adapters`
203
+
204
+ Contiene las integraciones que comunican el contexto con otros sistemas. Un adaptador importa un puerto, implementa la comunicación definida por ese puerto y conecta la entrada o salida externa con un proceso de aplicación.
205
+
206
+ Un adaptador puede integrar un controlador HTTP, un consumidor de mensajes, un SDK, un driver, un cliente remoto, un event bus o un proveedor de logs.
207
+
208
+ ### Arquitectura por capas
209
+
210
+ La estructura se organiza en tres niveles: capacidades, procesos y comunicación.
211
+
212
+ ```text
213
+ Sistema externo
214
+
215
+ Puerto + adaptador
216
+
217
+ Aplicación
218
+
219
+ Dominio
220
+ ```
221
+
222
+ El dominio ocupa la capa interior y reúne las capacidades del contexto.
223
+
224
+ La aplicación ocupa la capa intermedia y utiliza esas capacidades para construir procesos que cumplen propósitos del sistema.
225
+
226
+ Los puertos y adaptadores ocupan la capa exterior y resuelven la comunicación entre sistemas.
227
+
228
+ Un puerto declara la comunicación disponible en la frontera del contexto: qué datos entran, qué datos salen y qué operación se expone. Los puertos se declaran en `index.ts` o en módulos `.ts` ubicados en la raíz del contexto.
229
+
230
+ Un adaptador implementa esa comunicación. Importa el puerto correspondiente, traduce la entrada o salida externa al formato del proceso de aplicación y delega el trabajo al servicio de aplicación.
231
+
232
+ La dirección de imports sigue este recorrido:
233
+
234
+ ```text
235
+ adapter → port
236
+ adapter → application
237
+ application → domain
238
+ ```
239
+
240
+ Por ejemplo, `users/index.ts` declara el puerto `CreateUserPort`. `users/adapters/create-user-adapter.ts` importa ese puerto y conecta una solicitud externa con `users/application/create-user-service.ts`. El servicio aplica las capacidades de `Email` y `User` para completar el registro.
241
+
242
+ > **Consejo:** empieza la implementación dentro del contexto y mueve una pieza a `shared` cuando su significado y su uso pertenecen a varios contextos.
243
+
244
+ ## Tipos de la librería
245
+
246
+ ### `index.d.ts`
247
+
248
+ El archivo declara el tipo genérico:
249
+
250
+ ```ts
251
+ type Generic<T = unknown> = Record<string, T>
252
+ ```
253
+
254
+ `Generic<T>` representa un objeto con claves de tipo `string` y valores de un tipo común.
255
+
256
+ ```ts
257
+ const filters: Generic<string> = {
258
+ status: 'active'
259
+ }
260
+ ```
261
+
262
+ Cuando el tipo se omite, los valores utilizan `unknown`:
263
+
264
+ ```ts
265
+ const metadata: Generic = {
266
+ retries: 2
267
+ }
268
+ ```
269
+
270
+ Los contratos de fuentes de datos utilizan esta forma para representar objetos planos cuya estructura concreta será definida por cada implementación.
271
+
272
+ ## Dominio compartido
273
+
274
+ El dominio comienza con conceptos pequeños y avanza hacia estructuras que reúnen varias identidades. Comenzaremos con los objetos de valor, continuaremos con las entidades y terminaremos con los agregados.
275
+
276
+ ### Objetos de valor
277
+
278
+ Un objeto de valor representa un concepto que posee reglas, semántica o comportamiento propios. Su identidad está determinada por su valor.
279
+
280
+ El archivo `shared/domain/value-objects.ts` genera la clase base `ValueObject<T>` y las implementaciones `Email` y `NullableBoolean`.
281
+
282
+ #### Crear un correo electrónico
283
+
284
+ `Email` convierte un texto en un concepto de dominio con validación y operaciones propias:
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
+ La creación se realiza con `Email.from()`. Este método ejecuta `Email.isValid()` antes de construir la instancia.
296
+
297
+ ```ts
298
+ Email.isValid('alejandro@example.com')
299
+ ```
300
+
301
+ Toda creación pasa por la misma regla de validez:
302
+
303
+ ```text
304
+ valor recibido
305
+
306
+ isValid(value)
307
+
308
+ from(value)
309
+
310
+ instancia válida
311
+ ```
312
+
313
+ Cuando la validación falla, `from()` lanza `ValueError`.
314
+
315
+ > **Consejo:** utiliza un tipo nativo cuando expresa completamente el dato. Crea un objeto de valor cuando el concepto aporta reglas u operaciones propias. Un identificador textual puede representarse con `string`; un correo electrónico se beneficia de `Email` porque incorpora validación y comportamiento.
316
+
317
+ #### Representar un estado booleano nullable
318
+
319
+ `NullableBoolean` modela los valores `true`, `false` y `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
+ El método `isIndeterminate()` expresa una operación propia del concepto y permite consultar el estado `null` con una intención explícita.
331
+
332
+ #### Implementar un objeto de valor
333
+
334
+ Crearemos un porcentaje de descuento. El ejemplo solo tiene una regla y una operación para que el foco quede en la idea del objeto de valor.
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
+ Ahora podemos crear y utilizar el concepto:
379
+
380
+ ```ts
381
+ const discount = DiscountPercentage.from(15)
382
+ const finalPrice = discount.applyTo(100)
383
+ ```
384
+
385
+ `isValid()` concentra la regla. `from()` crea la instancia válida. `applyTo()` agrega el comportamiento propio del concepto.
386
+
387
+ ### Entidades
388
+
389
+ Una entidad representa un concepto con identidad propia. Dos instancias representan el mismo elemento cuando comparten esa identidad.
390
+
391
+ El archivo `shared/domain/entities.ts` genera la clase base `Entity`. Cada entidad implementa `equals()` y `toJSON()`.
392
+
393
+ Crearemos una entidad para el contexto `users`.
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
+ La propiedad `id` define la identidad. El método `equals()` compara entidades a partir de esa propiedad.
428
+
429
+ `toJSON()` produce una representación plana de la entidad:
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
+ El resultado utiliza los valores internos de `Email` y `NullableBoolean`.
442
+
443
+ ### Agregados
444
+
445
+ Un agregado reúne varias entidades en una unidad lógica. Las operaciones del agregado dependen de todas las identidades que lo componen.
446
+
447
+ Un equipo puede reunir a una persona líder y a varias integrantes:
448
+
449
+ ```text
450
+ Team
451
+ ├── leader
452
+ └── members
453
+ ```
454
+
455
+ Cada elemento conserva su propia identidad dentro de la unidad.
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` agrupa varias entidades `User` en una sola unidad lógica. El método `size()` opera sobre ese conjunto.
478
+
479
+ ### Errores del dominio
480
+
481
+ El archivo `shared/domain/errors.ts` genera `ValueError`. Este error representa un valor recibido que incumple la regla esperada.
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
+ Al estar en `shared`, `ValueError` puede utilizarse desde cualquier contexto y desde cualquier concepto del dominio que valide valores.
492
+
493
+ ## Aplicación compartida
494
+
495
+ La capa de aplicación coordina los casos de uso. Sus contratos conectan el dominio con validaciones, fuentes de datos, respuestas HTTP, logs y eventos.
496
+
497
+ ### Validaciones
498
+
499
+ El archivo `shared/application/validations.ts` declara el contrato `Validatable`:
500
+
501
+ ```ts
502
+ isValid(): boolean
503
+ validate(): unknown
504
+ ```
505
+
506
+ `isValid()` consulta el estado de la validación. `validate()` ejecuta la validación y devuelve el resultado definido por la implementación.
507
+
508
+ Crearemos una validación para el caso de uso que registra usuarios.
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
+ El servicio de aplicación puede ejecutar esta validación antes de construir la entidad `User`.
532
+
533
+ ### Servicios
534
+
535
+ Los servicios representan los casos de uso de la aplicación. Cada servicio aplica capacidades del dominio en un proceso que cumple un propósito, como crear un usuario, confirmar una orden o registrar un pago.
536
+
537
+ El archivo `shared/application/services.ts` genera la clase base `Service`. Una implementación concreta define sus entradas, sus dependencias y el método que ejecuta el proceso.
538
+
539
+ Comenzaremos con un servicio que crea un usuario.
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
+ El servicio recibe un comando, valida la entrada, construye la entidad y delega el guardado. `CreateUserCommand` expresa la entrada del proceso. `CreatedUser` expresa la salida.
600
+
601
+ ### Respuestas HTTP
602
+
603
+ El archivo `shared/application/http.ts` genera `HttpResponseBody`, un cuerpo de respuesta diseñado para API REST.
604
+
605
+ La clase organiza la respuesta en tres propiedades:
606
+
607
+ ```ts
608
+ new HttpResponseBody(data, errors, links)
609
+ ```
610
+
611
+ `data` contiene los datos de la operación y acepta `null` cuando la respuesta carece de datos:
612
+
613
+ ```ts
614
+ const body = new HttpResponseBody({ id: 'user-1' })
615
+ ```
616
+
617
+ `errors` contiene una lista de mensajes:
618
+
619
+ ```ts
620
+ const body = new HttpResponseBody(
621
+ null,
622
+ ['The email is invalid.']
623
+ )
624
+ ```
625
+
626
+ `links` contiene enlaces HATEOAS relacionados con el recurso y sus operaciones disponibles:
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
+ La salida plana de una entidad puede utilizarse como `data`:
639
+
640
+ ```ts
641
+ const body = new HttpResponseBody(user.toJSON())
642
+ ```
643
+
644
+ ### Logs
645
+
646
+ El archivo `shared/application/loggers.ts` declara el contrato `Logger`. La aplicación utiliza esta abstracción para producir logs que un adaptador envía a servicios externos.
647
+
648
+ El contrato incluye los niveles `debug`, `info`, `warning`, `error` y `critical`, junto con las constantes numéricas `DEBUG`, `INFO`, `WARNING`, `ERROR` y `CRITICAL`.
649
+
650
+ Crearemos un adaptador sencillo que conecta el contrato con la consola.
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
+ El servicio recibe `Logger` como dependencia. El adaptador decide adónde enviar cada nivel.
681
+
682
+ ### Eventos
683
+
684
+ El archivo `shared/application/events.ts` contiene los contratos que conectan la aplicación con un event bus.
685
+
686
+ El flujo comienza con un evento, continúa con el dispatcher y termina en uno o más handlers:
687
+
688
+ ```text
689
+ Service
690
+ ↓ crea
691
+ Event
692
+ ↓ entrega a
693
+ EventDispatcher
694
+ ↓ publica en
695
+ Event bus
696
+ ↓ ejecuta
697
+ EventHandler
698
+ ```
699
+
700
+ #### Evento
701
+
702
+ `Event` representa un hecho ocurrido en la aplicación. Contiene el momento del evento y sus detalles planos.
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` representa una reacción al evento.
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` representa el contrato de interacción con el event bus. Su implementación concreta suscribe handlers, retira suscripciones y despacha eventos.
743
+
744
+ ```ts
745
+ subscribe(key, handler)
746
+ unsubscribe(key, handler)
747
+ dispatch(event)
748
+ ```
749
+
750
+ El servicio puede recibir `EventDispatcher` y publicar `UserCreated` al completar el caso de uso.
751
+
752
+ ## Fuentes de datos
753
+
754
+ El archivo `shared/application/data-sources.ts` organiza el acceso a una fuente de datos en cuatro piezas: `DriverManager`, `DataManager`, `DatasetManager` y `Repository`.
755
+
756
+ El flujo completo se ve así:
757
+
758
+ ```text
759
+ DriverManager
760
+ ↓ conecta y habilita
761
+ DataManager o DatasetManager
762
+ ↓ entrega datos planos a
763
+ Repository
764
+ ↓ transforma
765
+ Objetos de dominio
766
+ ↓ utiliza
767
+ Service
768
+ ```
769
+
770
+ Comenzaremos en la conexión y avanzaremos hasta el caso de uso.
771
+
772
+ ### Driver manager
773
+
774
+ `DriverManager` conecta y desconecta la fuente mediante un driver. Cuando la conexión está disponible, `connect()` devuelve un data manager habilitado.
775
+
776
+ ```ts
777
+ connect(...args): Promise<DataManager>
778
+ disconnect(): Promise<unknown>
779
+ ```
780
+
781
+ Crearemos un manager para una colección de personas en memoria. El ejemplo evita una base de datos para que el foco quede en la responsabilidad del manager.
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
+ La implementación concreta puede encapsular un driver de base de datos, un cliente HTTP, un sistema de archivos u otra fuente.
811
+
812
+ ### Data manager
813
+
814
+ `DataManager` actúa sobre la fuente y trabaja con objetos planos y arrays. Su forma base define dos operaciones:
815
+
816
+ ```ts
817
+ all(): Promise<Array<T>>
818
+ none(): Array<T>
819
+ ```
820
+
821
+ `all()` obtiene los registros disponibles. `none()` crea una colección vacía tipada.
822
+
823
+ Primero definiremos la forma que utiliza la fuente:
824
+
825
+ ```ts
826
+ type PersonRecord = {
827
+ id: string
828
+ name: string
829
+ }
830
+ ```
831
+
832
+ Ahora implementaremos el 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
+ El data manager refleja la estructura de la fuente. En este ejemplo solo entrega registros planos.
862
+
863
+ #### Operaciones de la fuente
864
+
865
+ El archivo también declara interfaces para ampliar las capacidades de un data manager:
866
+
867
+ | Interfaz | Operación |
868
+ | --- | --- |
869
+ | `Filterable` | Filtra registros. |
870
+ | `Sortable` | Ordena registros. |
871
+ | `Creatable` | Crea registros. |
872
+ | `Updatable` | Actualiza registros. |
873
+ | `Deletable` | Elimina registros. |
874
+ | `Aggregatable` | Ejecuta agregaciones. |
875
+ | `Relatable` | Selecciona o precarga relaciones. |
876
+
877
+ Un data manager puede implementar las interfaces que necesita su fuente:
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
+ También puede declarar operaciones específicas para consultas de la fuente:
920
+
921
+ ```ts
922
+ public async findByName(name: string): Promise<PersonRecord[]> {
923
+ return this.filter({ name })
924
+ }
925
+ ```
926
+
927
+ La capa de aplicación decide cuándo ejecutar estas operaciones, combina sus resultados y captura los errores producidos por la fuente.
928
+
929
+ ### Dataset manager
930
+
931
+ `DatasetManager` extiende `DataManager` con operaciones de conjuntos:
932
+
933
+ ```ts
934
+ union()
935
+ intersection()
936
+ difference()
937
+ symmetric_difference()
938
+ complement()
939
+ ```
940
+
941
+ Esta implementación resulta útil cuando una operación trabaja con uniones, intersecciones, diferencias y complementos entre colecciones de datos.
942
+
943
+ ### Repository
944
+
945
+ `Repository` actúa como intermediario entre los datos planos y los objetos de dominio.
946
+
947
+ ```text
948
+ PersonRecord
949
+ ↓ transform
950
+ Person
951
+
952
+ Person
953
+ ↓ toRecord
954
+ PersonRecord
955
+ ```
956
+
957
+ En este ejemplo la fuente y el dominio comparten casi la misma forma para que el foco quede en el rol del repositorio: traducir entre datos planos y objetos de dominio.
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
+ Ahora implementaremos el repositorio.
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()` convierte el registro en una entidad. `toRecord()` hace el camino inverso.
1031
+
1032
+ ### Consultas y errores en la aplicación
1033
+
1034
+ Los servicios de aplicación coordinan las consultas y capturan los errores de las fuentes de datos. El proceso expresa las capacidades de colaboración que necesita mediante contratos propios de la aplicación. Los adaptadores materializan esos contratos.
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` expresa la colaboración que necesita el proceso. `PeopleRepository`, ubicado en `adapters`, implementa esa colaboración y transforma los datos de la fuente en entidades `Person`.
1062
+
1063
+ ## Puertos del contexto
1064
+
1065
+ Los puertos describen la comunicación entre el contexto y otros sistemas. Cada puerto define la forma de una interacción en la frontera: datos de entrada, datos de salida y operación disponible.
1066
+
1067
+ El contexto genera `index.ts` como archivo principal de puertos y `example-ports.ts` como ejemplo de un archivo adicional. Los puertos son importados por los adaptadores que materializan esa comunicación.
1068
+
1069
+ ### Puerto principal
1070
+
1071
+ Declararemos la comunicación para crear un usuario directamente en `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` representa la información recibida desde otro sistema. `CreateUserResponse` representa la respuesta entregada. `CreateUserPort` define la operación disponible en la frontera del contexto.
1095
+
1096
+ ### Adaptar el puerto al proceso de aplicación
1097
+
1098
+ El adaptador importa el puerto y el servicio. Su responsabilidad consiste en traducir la solicitud externa al comando de aplicación y transformar el resultado en la respuesta del puerto.
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
+ El puerto expresa la comunicación. El adaptador la implementa. El servicio ejecuta el proceso. El dominio aporta las capacidades utilizadas por ese proceso.
1138
+
1139
+ ### Puertos adicionales
1140
+
1141
+ Un contexto puede organizar sus comunicaciones en varios archivos de la raíz. Cada archivo declara los puertos de un grupo de interacciones.
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
+ El adaptador correspondiente importa el contrato desde el archivo donde está declarado:
1163
+
1164
+ ```ts
1165
+ import type {
1166
+ FindUserPort,
1167
+ FindUserRequest,
1168
+ FindUserResponse
1169
+ } from '../example-ports.js'
1170
+ ```
1171
+
1172
+ > **Consejo:** agrupa en un mismo archivo los puertos que forman una comunicación coherente. Usa archivos adicionales cuando el contexto crece y aparecen grupos de interacciones con responsabilidades propias.
1173
+
1174
+ ## Implementar un contexto
1175
+
1176
+ Ahora recorreremos una implementación completa de `users` siguiendo el orden de las capas: capacidades, proceso y comunicación.
1177
+
1178
+ ### 1. Modelar las capacidades del dominio
1179
+
1180
+ Comenzamos con los conceptos que poseen reglas y comportamiento. Para el correo utilizamos `Email`; para la identidad utilizamos `string`; para el usuario creamos una entidad.
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` aporta la capacidad de validar y representar el correo. `User` aporta identidad y representación del usuario.
1215
+
1216
+ ### 2. Implementar la validación del proceso
1217
+
1218
+ La validación prepara la entrada utilizada por el caso de uso.
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. Implementar el proceso de aplicación
1242
+
1243
+ El servicio recibe sus colaboradores como dependencias y aplica las capacidades del dominio para completar el registro.
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` declara la operación que el servicio necesita del adaptador. El flujo queda en cuatro pasos: validar, construir la entidad, guardar y responder.
1304
+
1305
+ ### 4. Declarar el puerto de comunicación
1306
+
1307
+ El puerto define cómo otro sistema solicita la creación de un usuario.
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. Implementar el adaptador
1331
+
1332
+ El adaptador importa el puerto, recibe la solicitud externa y ejecuta el proceso de aplicación.
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
+ La comunicación completa sigue este recorrido:
1372
+
1373
+ ```text
1374
+ Solicitud externa
1375
+
1376
+ CreateUserAdapter
1377
+
1378
+ CreateUserService
1379
+
1380
+ Validación + Email + User
1381
+
1382
+ UserWriter
1383
+
1384
+ Respuesta externa
1385
+ ```
1386
+
1387
+ ### 6. Componer la implementación en `main.ts`
1388
+
1389
+ `main.ts` reúne las implementaciones principales de la librería. La composición crea el servicio y lo entrega al adaptador.
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` ofrece una composición mínima: un repositorio concreto, un servicio y un adaptador.
1411
+
1412
+ ### 7. Utilizar la implementación
1413
+
1414
+ El consumidor utiliza la entrada principal de la librería y accede al adaptador preparado en `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
+ El adaptador recibe la solicitud, aplica el puerto, ejecuta el servicio y entrega la respuesta.
1428
+
1429
+ ## Referencia de archivos generados
1430
+
1431
+ | Archivo | Propósito |
1432
+ | --- | --- |
1433
+ | `core/index.d.ts` | Declara `Generic<T>` para objetos planos. |
1434
+ | `core/main.ts` | Contiene la implementación principal de la librería. |
1435
+ | `shared/domain/value-objects.ts` | Declara `ValueObject<T>` e implementa `Email` y `NullableBoolean`. |
1436
+ | `shared/domain/entities.ts` | Declara la base `Entity`. |
1437
+ | `shared/domain/aggregates.ts` | Declara la base `Aggregate`. |
1438
+ | `shared/domain/errors.ts` | Implementa `ValueError`. |
1439
+ | `shared/application/validations.ts` | Declara `Validatable`. |
1440
+ | `shared/application/services.ts` | Declara `Service` como base de los casos de uso. |
1441
+ | `shared/application/http.ts` | Implementa `HttpResponseBody` para respuestas REST y enlaces HATEOAS. |
1442
+ | `shared/application/loggers.ts` | Declara niveles de log y el contrato `Logger`. |
1443
+ | `shared/application/events.ts` | Declara `Event`, `EventHandler` y `EventDispatcher`. |
1444
+ | `shared/application/data-sources.ts` | Declara operaciones de fuentes, managers y repositorios. |
1445
+ | `users/index.ts` | Declara los puertos principales de comunicación del contexto. |
1446
+ | `users/example-ports.ts` | Muestra un archivo adicional de puertos de comunicación. |
1447
+ | `users/domain/` | Contiene las capacidades del contexto. |
1448
+ | `users/application/` | Contiene procesos que aplican las capacidades del dominio para cumplir propósitos. |
1449
+ | `users/adapters/` | Contiene integraciones que importan puertos y comunican el contexto con otros sistemas. |