@archi-code/ddd 0.0.0-stage → 3.0.0-alpha.1
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/LICENSE +46 -0
- package/README.md +2693 -2
- package/SKILL.md +104 -0
- package/ddd-skills/archi-code-ddd/imports.md +84 -0
- package/ddd-skills/archi-code-ddd/layers.md +299 -0
- package/ddd-skills/archi-code-ddd/patterns.md +676 -0
- package/ddd-skills/archi-code-ddd/what-not-to-do.md +189 -0
- package/ddd-skills/archi-code-ddd/where-each-class-goes.md +63 -0
- package/ddd-skills/ddd-agregados/GUIA.md +114 -0
- package/ddd-skills/ddd-agregados/references/consistencia-eventual.md +159 -0
- package/ddd-skills/ddd-agregados/references/ejemplo-ecommerce.md +52 -0
- package/ddd-skills/ddd-agregados/references/fabricas.md +196 -0
- package/ddd-skills/ddd-agregados/references/implementacion-ts.md +711 -0
- package/ddd-skills/ddd-arquitectura/GUIA.md +80 -0
- package/ddd-skills/ddd-arquitectura/assets/dependency-cruiser.cjs +92 -0
- package/ddd-skills/ddd-arquitectura/references/estilos-adicionales.md +51 -0
- package/ddd-skills/ddd-arquitectura/references/estructura-hexagonal-ts.md +352 -0
- package/ddd-skills/ddd-arquitectura/references/modulos.md +48 -0
- package/ddd-skills/ddd-auditoria-refactorizacion/GUIA.md +85 -0
- package/ddd-skills/ddd-auditoria-refactorizacion/references/ejemplo-auditoria.md +316 -0
- package/ddd-skills/ddd-auditoria-refactorizacion/references/lista-comprobacion.md +54 -0
- package/ddd-skills/ddd-auditoria-refactorizacion/references/plan-migracion.md +73 -0
- package/ddd-skills/ddd-auditoria-refactorizacion/scripts/scan_ddd_smells.py +264 -0
- package/ddd-skills/ddd-context-mapping/GUIA.md +83 -0
- package/ddd-skills/ddd-context-mapping/references/anticorrupcion-ts.md +404 -0
- package/ddd-skills/ddd-context-mapping/references/estilos-integracion.md +155 -0
- package/ddd-skills/ddd-event-sourcing-cqrs/GUIA.md +92 -0
- package/ddd-skills/ddd-event-sourcing-cqrs/references/event-sourcing-ts.md +471 -0
- package/ddd-skills/ddd-event-sourcing-cqrs/references/proyecciones-cqrs-ts.md +285 -0
- package/ddd-skills/ddd-eventos-dominio/GUIA.md +106 -0
- package/ddd-skills/ddd-eventos-dominio/references/diseno-eventos.md +131 -0
- package/ddd-skills/ddd-eventos-dominio/references/ejemplo-ecommerce.md +240 -0
- package/ddd-skills/ddd-eventos-dominio/references/publicacion-ts.md +526 -0
- package/ddd-skills/ddd-orquestador/GUIA.md +40 -0
- package/ddd-skills/ddd-orquestador/references/chuleta-reglas.md +35 -0
- package/ddd-skills/ddd-repositorios-persistencia/GUIA.md +121 -0
- package/ddd-skills/ddd-repositorios-persistencia/references/implementacion-ts.md +296 -0
- package/ddd-skills/ddd-repositorios-persistencia/references/tests-repositorios.md +177 -0
- package/ddd-skills/ddd-repositorios-persistencia/references/transacciones-ts.md +294 -0
- package/ddd-skills/ddd-servicios-capa-aplicacion/GUIA.md +99 -0
- package/ddd-skills/ddd-servicios-capa-aplicacion/references/capa-aplicacion-ts.md +264 -0
- package/ddd-skills/ddd-servicios-capa-aplicacion/references/ejemplo-ecommerce.md +324 -0
- package/ddd-skills/ddd-servicios-capa-aplicacion/references/servicios-dominio-ts.md +191 -0
- package/ddd-skills/ddd-value-objects-entidades/GUIA.md +118 -0
- package/ddd-skills/ddd-value-objects-entidades/references/ejemplo-ecommerce.md +87 -0
- package/ddd-skills/ddd-value-objects-entidades/references/entidades-ts.md +220 -0
- package/ddd-skills/ddd-value-objects-entidades/references/persistencia-valores.md +88 -0
- package/ddd-skills/ddd-value-objects-entidades/references/specification-y-diseno-flexible.md +119 -0
- package/ddd-skills/ddd-value-objects-entidades/references/validacion.md +96 -0
- package/ddd-skills/ddd-value-objects-entidades/references/value-objects-ts.md +377 -0
- package/dist/cjs/application/event/event-bus.d.ts +4 -0
- package/dist/cjs/application/event/event-bus.js +2 -0
- package/dist/cjs/application/event/event-listener.d.ts +4 -0
- package/dist/cjs/application/event/event-listener.js +6 -0
- package/dist/cjs/application/event/listens-to.d.ts +15 -0
- package/dist/cjs/application/event/listens-to.js +28 -0
- package/dist/cjs/application/index.d.ts +7 -0
- package/dist/cjs/application/index.js +9 -0
- package/dist/cjs/application/ports/clock.d.ts +3 -0
- package/dist/cjs/application/ports/clock.js +2 -0
- package/dist/cjs/application/ports/logger.d.ts +8 -0
- package/dist/cjs/application/ports/logger.js +2 -0
- package/dist/cjs/application/use-case/use-case.d.ts +6 -0
- package/dist/cjs/application/use-case/use-case.js +24 -0
- package/dist/cjs/domain/aggregate/aggregate-root.d.ts +14 -0
- package/dist/cjs/domain/aggregate/aggregate-root.js +33 -0
- package/dist/cjs/domain/entity/entity.d.ts +9 -0
- package/dist/cjs/domain/entity/entity.js +19 -0
- package/dist/cjs/domain/event/event-base.d.ts +29 -0
- package/dist/cjs/domain/event/event-base.js +51 -0
- package/dist/cjs/domain/index.d.ts +10 -0
- package/dist/cjs/domain/index.js +24 -0
- package/dist/cjs/domain/primitive/primitive-types.d.ts +22 -0
- package/dist/cjs/domain/primitive/primitive-types.js +2 -0
- package/dist/cjs/domain/repository/aggregate-finder.d.ts +4 -0
- package/dist/cjs/domain/repository/aggregate-finder.js +2 -0
- package/dist/cjs/domain/repository/aggregate-remover.d.ts +4 -0
- package/dist/cjs/domain/repository/aggregate-remover.js +2 -0
- package/dist/cjs/domain/repository/aggregate-saver.d.ts +4 -0
- package/dist/cjs/domain/repository/aggregate-saver.js +2 -0
- package/dist/cjs/domain/specification/specification.d.ts +9 -0
- package/dist/cjs/domain/specification/specification.js +44 -0
- package/dist/cjs/domain/type/abstract-type.d.ts +20 -0
- package/dist/cjs/domain/type/abstract-type.js +69 -0
- package/dist/cjs/domain/type/array-type.d.ts +16 -0
- package/dist/cjs/domain/type/array-type.js +65 -0
- package/dist/cjs/domain/type/boolean-type.d.ts +4 -0
- package/dist/cjs/domain/type/boolean-type.js +16 -0
- package/dist/cjs/domain/type/composite/composite-field.d.ts +19 -0
- package/dist/cjs/domain/type/composite/composite-field.js +63 -0
- package/dist/cjs/domain/type/composite/composite.d.ts +26 -0
- package/dist/cjs/domain/type/composite/composite.js +55 -0
- package/dist/cjs/domain/type/date-type.d.ts +18 -0
- package/dist/cjs/domain/type/date-type.js +35 -0
- package/dist/cjs/domain/type/enum-type.d.ts +7 -0
- package/dist/cjs/domain/type/enum-type.js +30 -0
- package/dist/cjs/domain/type/equality/value-equality.d.ts +7 -0
- package/dist/cjs/domain/type/equality/value-equality.js +41 -0
- package/dist/cjs/domain/type/id-type.d.ts +7 -0
- package/dist/cjs/domain/type/id-type.js +17 -0
- package/dist/cjs/domain/type/immutable/immutable-date.d.ts +4 -0
- package/dist/cjs/domain/type/immutable/immutable-date.js +37 -0
- package/dist/cjs/domain/type/immutable/immutable.d.ts +4 -0
- package/dist/cjs/domain/type/immutable/immutable.js +19 -0
- package/dist/cjs/domain/type/index.d.ts +17 -0
- package/dist/cjs/domain/type/index.js +33 -0
- package/dist/cjs/domain/type/json-type.d.ts +5 -0
- package/dist/cjs/domain/type/json-type.js +17 -0
- package/dist/cjs/domain/type/number-type.d.ts +4 -0
- package/dist/cjs/domain/type/number-type.js +16 -0
- package/dist/cjs/domain/type/required/required-mark.d.ts +9 -0
- package/dist/cjs/domain/type/required/required-mark.js +13 -0
- package/dist/cjs/domain/type/required/required.d.ts +7 -0
- package/dist/cjs/domain/type/required/required.js +21 -0
- package/dist/cjs/domain/type/string-type.d.ts +4 -0
- package/dist/cjs/domain/type/string-type.js +16 -0
- package/dist/cjs/domain/type/uuid-type.d.ts +7 -0
- package/dist/cjs/domain/type/uuid-type.js +27 -0
- package/dist/cjs/domain/type/value-objects/value-objects.d.ts +16 -0
- package/dist/cjs/domain/type/value-objects/value-objects.js +70 -0
- package/dist/cjs/exceptions/abstract.exception.d.ts +23 -0
- package/dist/cjs/exceptions/abstract.exception.js +66 -0
- package/dist/cjs/exceptions/application/aggregate-not-found.exception.d.ts +5 -0
- package/dist/cjs/exceptions/application/aggregate-not-found.exception.js +17 -0
- package/dist/cjs/exceptions/application/application.exception.d.ts +6 -0
- package/dist/cjs/exceptions/application/application.exception.js +17 -0
- package/dist/cjs/exceptions/application/concurrency.exception.d.ts +5 -0
- package/dist/cjs/exceptions/application/concurrency.exception.js +17 -0
- package/dist/cjs/exceptions/application/not-allowed.exception.d.ts +5 -0
- package/dist/cjs/exceptions/application/not-allowed.exception.js +17 -0
- package/dist/cjs/exceptions/brand/exception-brand.d.ts +6 -0
- package/dist/cjs/exceptions/brand/exception-brand.js +29 -0
- package/dist/cjs/exceptions/domain/domain.exception.d.ts +6 -0
- package/dist/cjs/exceptions/domain/domain.exception.js +17 -0
- package/dist/cjs/exceptions/domain/required-value.exception.d.ts +4 -0
- package/dist/cjs/exceptions/domain/required-value.exception.js +21 -0
- package/dist/cjs/exceptions/domain/type-primitive.exception.d.ts +4 -0
- package/dist/cjs/exceptions/domain/type-primitive.exception.js +21 -0
- package/dist/cjs/exceptions/domain/validation.exception.d.ts +14 -0
- package/dist/cjs/exceptions/domain/validation.exception.js +23 -0
- package/dist/cjs/exceptions/exception-code.d.ts +15 -0
- package/dist/cjs/exceptions/exception-code.js +19 -0
- package/dist/cjs/exceptions/index.d.ts +14 -0
- package/dist/cjs/exceptions/index.js +30 -0
- package/dist/cjs/exceptions/infrastructure/event-delivery.exception.d.ts +5 -0
- package/dist/cjs/exceptions/infrastructure/event-delivery.exception.js +20 -0
- package/dist/cjs/exceptions/infrastructure/infrastructure.exception.d.ts +6 -0
- package/dist/cjs/exceptions/infrastructure/infrastructure.exception.js +17 -0
- package/dist/cjs/exceptions/infrastructure/internal-error.exception.d.ts +5 -0
- package/dist/cjs/exceptions/infrastructure/internal-error.exception.js +17 -0
- package/dist/cjs/exceptions/infrastructure/unknown-event.exception.d.ts +4 -0
- package/dist/cjs/exceptions/infrastructure/unknown-event.exception.js +19 -0
- package/dist/cjs/exceptions/received/received-value.d.ts +4 -0
- package/dist/cjs/exceptions/received/received-value.js +13 -0
- package/dist/cjs/infrastructure/clock/fixed-clock.d.ts +8 -0
- package/dist/cjs/infrastructure/clock/fixed-clock.js +19 -0
- package/dist/cjs/infrastructure/clock/system-clock.d.ts +4 -0
- package/dist/cjs/infrastructure/clock/system-clock.js +9 -0
- package/dist/cjs/infrastructure/event/domain-event-bus.d.ts +16 -0
- package/dist/cjs/infrastructure/event/domain-event-bus.js +60 -0
- package/dist/cjs/infrastructure/event/event-channel.d.ts +9 -0
- package/dist/cjs/infrastructure/event/event-channel.js +2 -0
- package/dist/cjs/infrastructure/event/event-registry.d.ts +9 -0
- package/dist/cjs/infrastructure/event/event-registry.js +43 -0
- package/dist/cjs/infrastructure/event/in-memory/emitter.d.ts +11 -0
- package/dist/cjs/infrastructure/event/in-memory/emitter.js +54 -0
- package/dist/cjs/infrastructure/event/in-memory/in-memory-event-channel.d.ts +16 -0
- package/dist/cjs/infrastructure/event/in-memory/in-memory-event-channel.js +51 -0
- package/dist/cjs/infrastructure/http/http-error-mapper.d.ts +23 -0
- package/dist/cjs/infrastructure/http/http-error-mapper.js +63 -0
- package/dist/cjs/infrastructure/index.d.ts +16 -0
- package/dist/cjs/infrastructure/index.js +21 -0
- package/dist/cjs/infrastructure/logging/console-logger.d.ts +17 -0
- package/dist/cjs/infrastructure/logging/console-logger.js +37 -0
- package/dist/cjs/infrastructure/logging/in-memory-logger.d.ts +15 -0
- package/dist/cjs/infrastructure/logging/in-memory-logger.js +27 -0
- package/dist/cjs/infrastructure/logging/log-serializer.d.ts +7 -0
- package/dist/cjs/infrastructure/logging/log-serializer.js +59 -0
- package/dist/cjs/infrastructure/logging/silent-logger.d.ts +7 -0
- package/dist/cjs/infrastructure/logging/silent-logger.js +18 -0
- package/dist/esm/application/event/event-bus.d.ts +4 -0
- package/dist/esm/application/event/event-bus.js +1 -0
- package/dist/esm/application/event/event-listener.d.ts +4 -0
- package/dist/esm/application/event/event-listener.js +2 -0
- package/dist/esm/application/event/listens-to.d.ts +15 -0
- package/dist/esm/application/event/listens-to.js +25 -0
- package/dist/esm/application/index.d.ts +7 -0
- package/dist/esm/application/index.js +3 -0
- package/dist/esm/application/ports/clock.d.ts +3 -0
- package/dist/esm/application/ports/clock.js +1 -0
- package/dist/esm/application/ports/logger.d.ts +8 -0
- package/dist/esm/application/ports/logger.js +1 -0
- package/dist/esm/application/use-case/use-case.d.ts +6 -0
- package/dist/esm/application/use-case/use-case.js +20 -0
- package/dist/esm/domain/aggregate/aggregate-root.d.ts +14 -0
- package/dist/esm/domain/aggregate/aggregate-root.js +29 -0
- package/dist/esm/domain/entity/entity.d.ts +9 -0
- package/dist/esm/domain/entity/entity.js +15 -0
- package/dist/esm/domain/event/event-base.d.ts +29 -0
- package/dist/esm/domain/event/event-base.js +47 -0
- package/dist/esm/domain/index.d.ts +10 -0
- package/dist/esm/domain/index.js +6 -0
- package/dist/esm/domain/primitive/primitive-types.d.ts +22 -0
- package/dist/esm/domain/primitive/primitive-types.js +1 -0
- package/dist/esm/domain/repository/aggregate-finder.d.ts +4 -0
- package/dist/esm/domain/repository/aggregate-finder.js +1 -0
- package/dist/esm/domain/repository/aggregate-remover.d.ts +4 -0
- package/dist/esm/domain/repository/aggregate-remover.js +1 -0
- package/dist/esm/domain/repository/aggregate-saver.d.ts +4 -0
- package/dist/esm/domain/repository/aggregate-saver.js +1 -0
- package/dist/esm/domain/specification/specification.d.ts +9 -0
- package/dist/esm/domain/specification/specification.js +40 -0
- package/dist/esm/domain/type/abstract-type.d.ts +20 -0
- package/dist/esm/domain/type/abstract-type.js +65 -0
- package/dist/esm/domain/type/array-type.d.ts +16 -0
- package/dist/esm/domain/type/array-type.js +61 -0
- package/dist/esm/domain/type/boolean-type.d.ts +4 -0
- package/dist/esm/domain/type/boolean-type.js +12 -0
- package/dist/esm/domain/type/composite/composite-field.d.ts +19 -0
- package/dist/esm/domain/type/composite/composite-field.js +59 -0
- package/dist/esm/domain/type/composite/composite.d.ts +26 -0
- package/dist/esm/domain/type/composite/composite.js +52 -0
- package/dist/esm/domain/type/date-type.d.ts +18 -0
- package/dist/esm/domain/type/date-type.js +29 -0
- package/dist/esm/domain/type/enum-type.d.ts +7 -0
- package/dist/esm/domain/type/enum-type.js +26 -0
- package/dist/esm/domain/type/equality/value-equality.d.ts +7 -0
- package/dist/esm/domain/type/equality/value-equality.js +37 -0
- package/dist/esm/domain/type/id-type.d.ts +7 -0
- package/dist/esm/domain/type/id-type.js +13 -0
- package/dist/esm/domain/type/immutable/immutable-date.d.ts +4 -0
- package/dist/esm/domain/type/immutable/immutable-date.js +33 -0
- package/dist/esm/domain/type/immutable/immutable.d.ts +4 -0
- package/dist/esm/domain/type/immutable/immutable.js +15 -0
- package/dist/esm/domain/type/index.d.ts +17 -0
- package/dist/esm/domain/type/index.js +14 -0
- package/dist/esm/domain/type/json-type.d.ts +5 -0
- package/dist/esm/domain/type/json-type.js +13 -0
- package/dist/esm/domain/type/number-type.d.ts +4 -0
- package/dist/esm/domain/type/number-type.js +12 -0
- package/dist/esm/domain/type/required/required-mark.d.ts +9 -0
- package/dist/esm/domain/type/required/required-mark.js +9 -0
- package/dist/esm/domain/type/required/required.d.ts +7 -0
- package/dist/esm/domain/type/required/required.js +18 -0
- package/dist/esm/domain/type/string-type.d.ts +4 -0
- package/dist/esm/domain/type/string-type.js +12 -0
- package/dist/esm/domain/type/uuid-type.d.ts +7 -0
- package/dist/esm/domain/type/uuid-type.js +23 -0
- package/dist/esm/domain/type/value-objects/value-objects.d.ts +16 -0
- package/dist/esm/domain/type/value-objects/value-objects.js +66 -0
- package/dist/esm/exceptions/abstract.exception.d.ts +23 -0
- package/dist/esm/exceptions/abstract.exception.js +62 -0
- package/dist/esm/exceptions/application/aggregate-not-found.exception.d.ts +5 -0
- package/dist/esm/exceptions/application/aggregate-not-found.exception.js +13 -0
- package/dist/esm/exceptions/application/application.exception.d.ts +6 -0
- package/dist/esm/exceptions/application/application.exception.js +13 -0
- package/dist/esm/exceptions/application/concurrency.exception.d.ts +5 -0
- package/dist/esm/exceptions/application/concurrency.exception.js +13 -0
- package/dist/esm/exceptions/application/not-allowed.exception.d.ts +5 -0
- package/dist/esm/exceptions/application/not-allowed.exception.js +13 -0
- package/dist/esm/exceptions/brand/exception-brand.d.ts +6 -0
- package/dist/esm/exceptions/brand/exception-brand.js +25 -0
- package/dist/esm/exceptions/domain/domain.exception.d.ts +6 -0
- package/dist/esm/exceptions/domain/domain.exception.js +13 -0
- package/dist/esm/exceptions/domain/required-value.exception.d.ts +4 -0
- package/dist/esm/exceptions/domain/required-value.exception.js +17 -0
- package/dist/esm/exceptions/domain/type-primitive.exception.d.ts +4 -0
- package/dist/esm/exceptions/domain/type-primitive.exception.js +17 -0
- package/dist/esm/exceptions/domain/validation.exception.d.ts +14 -0
- package/dist/esm/exceptions/domain/validation.exception.js +19 -0
- package/dist/esm/exceptions/exception-code.d.ts +15 -0
- package/dist/esm/exceptions/exception-code.js +16 -0
- package/dist/esm/exceptions/index.d.ts +14 -0
- package/dist/esm/exceptions/index.js +14 -0
- package/dist/esm/exceptions/infrastructure/event-delivery.exception.d.ts +5 -0
- package/dist/esm/exceptions/infrastructure/event-delivery.exception.js +16 -0
- package/dist/esm/exceptions/infrastructure/infrastructure.exception.d.ts +6 -0
- package/dist/esm/exceptions/infrastructure/infrastructure.exception.js +13 -0
- package/dist/esm/exceptions/infrastructure/internal-error.exception.d.ts +5 -0
- package/dist/esm/exceptions/infrastructure/internal-error.exception.js +13 -0
- package/dist/esm/exceptions/infrastructure/unknown-event.exception.d.ts +4 -0
- package/dist/esm/exceptions/infrastructure/unknown-event.exception.js +15 -0
- package/dist/esm/exceptions/received/received-value.d.ts +4 -0
- package/dist/esm/exceptions/received/received-value.js +9 -0
- package/dist/esm/infrastructure/clock/fixed-clock.d.ts +8 -0
- package/dist/esm/infrastructure/clock/fixed-clock.js +15 -0
- package/dist/esm/infrastructure/clock/system-clock.d.ts +4 -0
- package/dist/esm/infrastructure/clock/system-clock.js +5 -0
- package/dist/esm/infrastructure/event/domain-event-bus.d.ts +16 -0
- package/dist/esm/infrastructure/event/domain-event-bus.js +56 -0
- package/dist/esm/infrastructure/event/event-channel.d.ts +9 -0
- package/dist/esm/infrastructure/event/event-channel.js +1 -0
- package/dist/esm/infrastructure/event/event-registry.d.ts +9 -0
- package/dist/esm/infrastructure/event/event-registry.js +39 -0
- package/dist/esm/infrastructure/event/in-memory/emitter.d.ts +11 -0
- package/dist/esm/infrastructure/event/in-memory/emitter.js +50 -0
- package/dist/esm/infrastructure/event/in-memory/in-memory-event-channel.d.ts +16 -0
- package/dist/esm/infrastructure/event/in-memory/in-memory-event-channel.js +47 -0
- package/dist/esm/infrastructure/http/http-error-mapper.d.ts +23 -0
- package/dist/esm/infrastructure/http/http-error-mapper.js +59 -0
- package/dist/esm/infrastructure/index.d.ts +16 -0
- package/dist/esm/infrastructure/index.js +9 -0
- package/dist/esm/infrastructure/logging/console-logger.d.ts +17 -0
- package/dist/esm/infrastructure/logging/console-logger.js +33 -0
- package/dist/esm/infrastructure/logging/in-memory-logger.d.ts +15 -0
- package/dist/esm/infrastructure/logging/in-memory-logger.js +23 -0
- package/dist/esm/infrastructure/logging/log-serializer.d.ts +7 -0
- package/dist/esm/infrastructure/logging/log-serializer.js +55 -0
- package/dist/esm/infrastructure/logging/silent-logger.d.ts +7 -0
- package/dist/esm/infrastructure/logging/silent-logger.js +14 -0
- package/dist/esm/package.json +3 -0
- package/package.json +106 -4
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# Arquitectura de un Bounded Context
|
|
2
|
+
|
|
3
|
+
> Parte del skill `archi-code-ddd`: léela solo cuando ese skill te dirija aquí. Los ejemplos están escritos con la API de `@archi-code/ddd` (las clases de su `src`) y se copian tal cual; las guías son la fuente de verdad: el paquete solo se aparta de ellas donde lo marca `[Fuera de los libros: convención de @archi-code/ddd]`, y cualquier otra discrepancia es un defecto del paquete.
|
|
4
|
+
>
|
|
5
|
+
> **Cuándo consultar esta guía.** Arquitectura interna de un Bounded Context con DDD en TypeScript/Node. Cubre capas e inversión de dependencias, arquitectura hexagonal (puertos y adaptadores), qué va en dominio, aplicación, infraestructura e interfaz, módulos con nombres del lenguaje ubicuo, estructura de carpetas, composition root e inyección de dependencias, reglas automáticas de dependencias, cómo exponer REST sin exponer el modelo y cuándo (y cuándo no) añadir CQRS, arquitectura dirigida por eventos o Event Sourcing. Consúltala siempre que haya que crear o reorganizar la estructura de un proyecto o servicio con DDD, decidir dónde va un fichero o una clase, separar dominio de framework u ORM, montar la inyección de dependencias, configurar límites entre módulos, o cuando el dominio importe Express, Prisma, TypeORM o similares, aunque no se diga "arquitectura hexagonal".
|
|
6
|
+
|
|
7
|
+
## Con @archi-code/ddd
|
|
8
|
+
|
|
9
|
+
[Fuera de los libros: convención de @archi-code/ddd]
|
|
10
|
+
|
|
11
|
+
* El dominio importa `@archi-code/ddd/domain`, `@archi-code/ddd/exceptions` y `@archi-code/validation` (no "nada externo"); la aplicación añade `@archi-code/ddd/application`; solo infraestructura y el composition root importan `@archi-code/ddd/infrastructure`, salvo `HttpErrorMapper` en los puntos de entrada.
|
|
12
|
+
* No se escriben clases base ni puertos genéricos en el proyecto: `AggregateRoot`, `Entity`, `EventBase`, los tipos de value object y `Composite` vienen de `@archi-code/ddd/domain`; los contratos de repositorio son los roles `AggregateFinder`, `AggregateSaver` y `AggregateRemover` de `@archi-code/ddd/domain` (no hay un `OrderRepository` propio); `EventBus` y `Clock` vienen de `@archi-code/ddd/application` y `EventChannel` de `@archi-code/ddd/infrastructure`. No hay puerto de transacciones: el repositorio es responsable de la transacción y de todo lo relacionado con la persistencia.
|
|
13
|
+
* Un mecanismo de mensajería es solo un `EventChannel` en `infrastructure/messaging/`; el bus es siempre `DomainEventBus`, y cambiar de tecnología cambia una línea del composition root. `DomainEventBus`, `EventRegistry` y el puerto `EventChannel` (con `ChannelHandler` y `ChannelSubscription`) se importan de `@archi-code/ddd/infrastructure`: son cableado de infraestructura. La aplicación solo ve el contrato para publicar (`EventBus`) y para escuchar (`EventListener` + `@ListensTo`) de `@archi-code/ddd/application`.
|
|
14
|
+
* El composition root de cada contexto es el único que construye y conecta todo (repositorios, canal, `DomainEventBus`, suscripción de listeners con `bus.subscribe`) y lee `process.env`; nadie lo importa. El `dependency-cruiser.cjs` de esta guía ya aplica estas reglas.
|
|
15
|
+
* Capa, dependencias permitidas y nombres de cada clase: `ddd-skills/archi-code-ddd/where-each-class-goes.md`.
|
|
16
|
+
|
|
17
|
+
La arquitectura existe para proteger el modelo de dominio. El modelo debe estar libre de tecnología; por eso, por ejemplo, las transacciones las gestionan los servicios de aplicación y no el dominio [Destilado cap. 2]. Con el paquete tampoco las gestiona la aplicación: cada `save` y `remove` del adaptador del repositorio es una escritura atómica `[Fuera de los libros: convención de @archi-code/ddd]`. Y ningún estilo arquitectónico es una colección de herramientas para usar en todas partes: se aplica donde mitiga un riesgo concreto [IDDD cap. 4].
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
## La arquitectura por defecto: hexagonal
|
|
21
|
+
|
|
22
|
+
Recomendación de estas fuentes para cualquier contexto con modelo de dominio: **puertos y adaptadores** con inversión de dependencias. Motivos:
|
|
23
|
+
|
|
24
|
+
* Con capas tradicionales, la infraestructura queda debajo del dominio y implementar repositorios obliga a romper las reglas o a hacer apaños [IDDD cap. 4].
|
|
25
|
+
* Con DIP, módulos de alto y bajo nivel dependen de abstracciones; la infraestructura implementa interfaces definidas por las capas interiores [IDDD cap. 4].
|
|
26
|
+
* Hexagonal da simetría: cada cliente o mecanismo de salida tiene su adaptador, y todos llegan al interior por la misma API de aplicación [IDDD cap. 4].
|
|
27
|
+
* *DDD in PHP* la adopta como arquitectura base de sus ejemplos [PHP: Modules; PHP: Appendix Hexagonal].
|
|
28
|
+
|
|
29
|
+
### Las cuatro zonas y la regla de dependencia
|
|
30
|
+
|
|
31
|
+
```
|
|
32
|
+
interfaces (adaptadores de entrada) ─┐
|
|
33
|
+
├──▶ application ──▶ domain
|
|
34
|
+
infrastructure (adaptadores salida) ─┘ ▲
|
|
35
|
+
└──────────── implementa interfaces de ───────────┘
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
| Zona | Contiene | Puede depender de | Nunca |
|
|
39
|
+
|---|---|---|---|
|
|
40
|
+
| `domain` | Agregados (`AggregateRoot`), entidades (`Entity`), value objects, eventos de dominio (`EventBase`), servicios de dominio, puertos hacia servicios externos (`ProductPricing`); los contratos de repositorio son los roles `AggregateFinder`/`AggregateSaver`/`AggregateRemover` del paquete | Nada externo salvo `@archi-code/ddd/domain`, `@archi-code/ddd/exceptions` y `@archi-code/validation` | Framework web, ORM, cliente HTTP, `process.env`, logger |
|
|
41
|
+
| `application` | Casos de uso (`UseCase`), listeners (`EventListener` + `@ListensTo`), consultas de lectura con sus DTOs, puertos de salida no de dominio (`EventBus` y `Clock` del paquete; los tuyos, como `Warehouse`) | `domain`, `@archi-code/ddd/application` | Express/Fastify, SQL, detalles de mensajería |
|
|
42
|
+
| `infrastructure` | Repositorios concretos (`MongoOrderRepository`), ACL, canales de eventos (`EventChannel`) y el cableado de eventos (`DomainEventBus`, `EventRegistry`, en el composition root), clientes HTTP; el repositorio se encarga de la atomicidad de cada escritura, la versión, la conexión y los errores del driver | `domain`, `application`, `@archi-code/ddd/infrastructure` | Lógica de negocio |
|
|
43
|
+
| `interfaces` | Controladores REST, consumidores de mensajes, CLI, resolvers GraphQL | `application`, `HttpErrorMapper` (y tipos de `domain` solo si es imprescindible) | Lógica de negocio, acceso directo a repositorios |
|
|
44
|
+
|
|
45
|
+
Responsabilidades de los servicios de aplicación según IDDD: sin lógica de dominio, controlan transacciones y seguridad (en el paquete, la seguridad con `NotAllowedException`; la transacción pasa al adaptador del repositorio, que hace atómico cada `save` y `remove` `[Fuera de los libros: convención de @archi-code/ddd]`), pueden enviar notificaciones; son clientes directos del modelo y deben ser **finos**. Si crecen, la lógica de dominio se está escapando y el modelo se vuelve anémico [IDDD cap. 4]. Detalle en `ddd-skills/ddd-servicios-capa-aplicacion/GUIA.md`.
|
|
46
|
+
|
|
47
|
+
En la interfaz se valida formato (campo obligatorio, longitud), no conocimiento profundo del negocio, que se queda en el modelo [IDDD cap. 4].
|
|
48
|
+
|
|
49
|
+
## Flujo de trabajo
|
|
50
|
+
|
|
51
|
+
1. **Decide si el contexto merece modelo de dominio.** Si no tiene reglas de negocio, un CRUD sencillo y bien encapsulado es legítimo [IDDD cap. 1]. La estructura hexagonal sigue siendo útil, pero sin agregados artificiales.
|
|
52
|
+
2. **Crea la estructura de carpetas** con `ddd-skills/ddd-arquitectura/references/estructura-hexagonal-ts.md`. Si el proyecto ya reserva `src/app/` para la lógica principal, usa la variante descrita allí.
|
|
53
|
+
3. **Nombra los módulos** con el lenguaje ubicuo, nunca por tipo de patrón (`entities/`, `services/`, `valueobjects/`). Guía en `ddd-skills/ddd-arquitectura/references/modulos.md`.
|
|
54
|
+
4. **Monta el composition root**: un único punto donde se instancian adaptadores, se construye el `DomainEventBus` sobre un `EventChannel`, se inyectan los puertos en los casos de uso y se suscriben los listeners con `bus.subscribe`. Sin contenedor mágico salvo necesidad.
|
|
55
|
+
5. **Automatiza las reglas de dependencia** con `ddd-skills/ddd-arquitectura/assets/dependency-cruiser.cjs`. Una regla que no se comprueba en CI acaba violándose `[Fuera de los libros]`.
|
|
56
|
+
6. **Decide los estilos adicionales** (CQRS, eventos, Event Sourcing) con `ddd-skills/ddd-arquitectura/references/estilos-adicionales.md`. Por defecto, ninguno.
|
|
57
|
+
|
|
58
|
+
## REST sin exponer el modelo
|
|
59
|
+
|
|
60
|
+
Exponer el modelo directamente por REST produce interfaces frágiles: cada cambio del modelo rompe a los clientes. IDDD prefiere separar el modelo de la interfaz: recursos diseñados por casos de uso, construidos a partir de uno o varios agregados [IDDD cap. 4]. En TypeScript: el controlador comprueba la forma de la petición, llama a `execute` del caso de uso con su input, responde con lo que este devuelve (un id, un DTO o nada; nunca el agregado) y traduce los errores con `HttpErrorMapper` de `@archi-code/ddd/infrastructure` (404, 403, 409, 422…). Ejemplo en `ddd-skills/ddd-arquitectura/references/estructura-hexagonal-ts.md`.
|
|
61
|
+
|
|
62
|
+
## Señales de alarma
|
|
63
|
+
|
|
64
|
+
* `import` de `express`, `@prisma/client`, `typeorm`, `axios`, `pg`, `kafkajs` o similares dentro de `domain/`.
|
|
65
|
+
* Decoradores de ORM (`@Entity`, `@Column`) en clases de dominio. Mezclan el modelo con la persistencia.
|
|
66
|
+
* Clases base propias (`domain/shared/AggregateRoot.ts`, `ValueObject.ts`, `DomainEvent.ts`), un `OrderRepository` que lo hace todo, un `OrderMapper` a mano o un `EventPublisher` propio: el paquete ya da `AggregateRoot`, los roles de repositorio y `EventBus`, y la conversión a primitivos es del propio agregado (`toPrimitives()` y `static fromPrimitives()`).
|
|
67
|
+
* `@archi-code/ddd/infrastructure` importado desde `domain/` o `application/`.
|
|
68
|
+
* Carpetas `domain/entities`, `domain/services`, `domain/valueobjects`: módulos por tipo de patrón [PHP: Modules].
|
|
69
|
+
* Controladores que llaman a repositorios o contienen `if` de negocio.
|
|
70
|
+
* Servicios de aplicación de cientos de líneas [IDDD cap. 4].
|
|
71
|
+
* Un único `utils/` o `common/` compartido entre contextos con lógica de negocio.
|
|
72
|
+
* CQRS o Event Sourcing "porque es la arquitectura moderna", sin un riesgo concreto que mitiguen [IDDD cap. 4].
|
|
73
|
+
* Un contexto nuevo creado para separar algo que bastaba con un módulo [IDDD cap. 9].
|
|
74
|
+
|
|
75
|
+
## Siguiente guía
|
|
76
|
+
|
|
77
|
+
* Diseñar lo que va dentro de `domain/` → `ddd-skills/ddd-value-objects-entidades/GUIA.md` y `ddd-skills/ddd-agregados/GUIA.md`.
|
|
78
|
+
* Implementar repositorios en `infrastructure/` → `ddd-skills/ddd-repositorios-persistencia/GUIA.md`.
|
|
79
|
+
* Casos de uso en `application/` → `ddd-skills/ddd-servicios-capa-aplicacion/GUIA.md`.
|
|
80
|
+
* CQRS y Event Sourcing en profundidad → `ddd-skills/ddd-event-sourcing-cqrs/GUIA.md`.
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reglas de dependencia DDD para dependency-cruiser, adaptadas a @archi-code/ddd.
|
|
3
|
+
* Uso: npx depcruise src --config .dependency-cruiser.cjs
|
|
4
|
+
* Funciona con src/contexts/<contexto>/ y con src/app/contexts/<contexto>/ (proyectos con la lógica en src/app/).
|
|
5
|
+
* Probado con dependency-cruiser 16.10.0 y TypeScript 5.6, con un paquete simulado que expone
|
|
6
|
+
* @archi-code/ddd/{domain,application,infrastructure,exceptions}. Si la estructura publicada del
|
|
7
|
+
* paquete difiere, ajusta ARCHI_LAYER.
|
|
8
|
+
*/
|
|
9
|
+
// Grupo no capturante para app/: así $1 en las reglas es siempre el nombre del contexto.
|
|
10
|
+
const CTX = '^src/(?:app/)?contexts/([^/]+)/';
|
|
11
|
+
// Ruta resuelta de un punto de entrada del paquete: node_modules/@archi-code/ddd/<...>/<capa>/...
|
|
12
|
+
const ARCHI_LAYER = (layers) => `node_modules/@archi-code/ddd/.*(?:${layers})[/.]`;
|
|
13
|
+
|
|
14
|
+
module.exports = {
|
|
15
|
+
forbidden: [
|
|
16
|
+
{
|
|
17
|
+
name: 'domain-solo-paquetes-permitidos',
|
|
18
|
+
comment:
|
|
19
|
+
'El dominio solo usa @archi-code/ddd (domain y exceptions) y @archi-code/validation; nada más de npm.',
|
|
20
|
+
severity: 'error',
|
|
21
|
+
from: { path: CTX + 'domain/' },
|
|
22
|
+
to: {
|
|
23
|
+
dependencyTypes: ['npm', 'npm-dev', 'npm-optional', 'npm-peer'],
|
|
24
|
+
pathNot: ['node_modules/@archi-code/(?:ddd|validation)/'],
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
name: 'domain-sin-capas-exteriores-del-paquete',
|
|
29
|
+
comment: 'El dominio nunca importa @archi-code/ddd/application ni @archi-code/ddd/infrastructure.',
|
|
30
|
+
severity: 'error',
|
|
31
|
+
from: { path: CTX + 'domain/' },
|
|
32
|
+
to: { path: ARCHI_LAYER('application|infrastructure') },
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
name: 'domain-no-mira-afuera',
|
|
36
|
+
severity: 'error',
|
|
37
|
+
from: { path: CTX + 'domain/' },
|
|
38
|
+
to: { path: CTX + '(?:application|infrastructure|interfaces)/' },
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
name: 'application-sin-infraestructura',
|
|
42
|
+
comment: 'La aplicación no conoce adaptadores propios ni @archi-code/ddd/infrastructure.',
|
|
43
|
+
severity: 'error',
|
|
44
|
+
from: { path: CTX + 'application/' },
|
|
45
|
+
to: { path: [CTX + '(?:infrastructure|interfaces)/', ARCHI_LAYER('infrastructure')] },
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
name: 'interfaces-no-saltan-a-infraestructura',
|
|
49
|
+
comment:
|
|
50
|
+
'Los puntos de entrada llaman a casos de uso; no importan repositorios ni adaptadores. ' +
|
|
51
|
+
'Pueden importar HttpErrorMapper de @archi-code/ddd/infrastructure.',
|
|
52
|
+
severity: 'error',
|
|
53
|
+
from: { path: CTX + 'interfaces/' },
|
|
54
|
+
to: { path: CTX + 'infrastructure/' },
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
name: 'nadie-importa-composition',
|
|
58
|
+
comment: 'El composition root construye y conecta todo; ninguna capa lo importa.',
|
|
59
|
+
severity: 'error',
|
|
60
|
+
from: { path: CTX },
|
|
61
|
+
to: { path: CTX + 'composition\\.ts$' },
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
name: 'contextos-aislados',
|
|
65
|
+
comment: 'Un contexto no importa código de otro. Se integra por API, mensajes o shared-kernel acordado.',
|
|
66
|
+
severity: 'error',
|
|
67
|
+
from: { path: '^src/(?:app/)?contexts/([^/]+)/' },
|
|
68
|
+
to: {
|
|
69
|
+
path: '^src/(?:app/)?contexts/',
|
|
70
|
+
pathNot: '^src/(?:app/)?contexts/$1/', // $1 = contexto del fichero de origen
|
|
71
|
+
},
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
name: 'sin-ciclos',
|
|
75
|
+
severity: 'error',
|
|
76
|
+
from: {},
|
|
77
|
+
to: { circular: true },
|
|
78
|
+
},
|
|
79
|
+
],
|
|
80
|
+
options: {
|
|
81
|
+
tsConfig: { fileName: 'tsconfig.json' },
|
|
82
|
+
// Imprescindible en TypeScript: sin esto se ignoran los imports usados solo como tipos
|
|
83
|
+
// (import type { AggregateSaver } ...), y un dominio que importa un tipo prohibido pasaría.
|
|
84
|
+
tsPreCompilationDeps: true,
|
|
85
|
+
doNotFollow: { path: 'node_modules' },
|
|
86
|
+
// Resolver los subpaths del campo "exports" del paquete (@archi-code/ddd/domain, etc.).
|
|
87
|
+
enhancedResolveOptions: {
|
|
88
|
+
exportsFields: ['exports'],
|
|
89
|
+
conditionNames: ['import', 'require', 'node', 'default', 'types'],
|
|
90
|
+
},
|
|
91
|
+
},
|
|
92
|
+
};
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# Estilos adicionales: cuándo sí y cuándo no
|
|
2
|
+
|
|
3
|
+
Ninguno es obligatorio. Úsalos cuando mitiguen un riesgo con alta probabilidad de causar fallo si se ignora [IDDD cap. 4]. Destilado los presenta como combinables con puertos y adaptadores según las necesidades [Destilado cap. 2].
|
|
4
|
+
|
|
5
|
+
## CQRS
|
|
6
|
+
|
|
7
|
+
**Problema que resuelve:** las pantallas cruzan muchos agregados y consultarlas desde repositorios obliga a juntar varios agregados en DTOs, a crear buscadores especiales o a empobrecer la interfaz para que siga los límites de los agregados [IDDD cap. 4].
|
|
8
|
+
|
|
9
|
+
**Cómo es:** un modelo de comandos (los agregados, que ejecutan comportamiento) y un modelo de consultas (vistas desnormalizadas) actualizado por suscriptores a eventos [IDDD cap. 4]. Con el paquete, cada suscriptor es un `EventListener` con `@ListensTo(OrderPlaced, { consumer: '...' })` suscrito al `DomainEventBus`, que escribe la tabla de lectura y salta un `eventId` que ya aplicó (la entrega es al menos una vez). Reconstruir una vista desde el principio necesita un registro de eventos propio: un `EventChannel` no guarda historial legible. `[Fuera de los libros: convención de @archi-code/ddd]`
|
|
10
|
+
|
|
11
|
+
**Cuándo no:** si la interfaz no es compleja ni cruza a menudo varios agregados en una vista, CQRS añade complejidad accidental [IDDD cap. 4].
|
|
12
|
+
|
|
13
|
+
**Paso intermedio** `[Fuera de los libros]`: consultas de lectura que van directas a la base de datos con SQL o vistas, sin pasar por repositorios ni agregados, en la misma base. Ya separa lectura y escritura sin eventos ni consistencia eventual. Suele bastar. La consulta es una interfaz de `application/` que devuelve DTOs, implementada en `infrastructure/` sobre la base (`MongoOrderSummaries`), nunca con `AggregateFinder`:
|
|
14
|
+
|
|
15
|
+
```ts
|
|
16
|
+
export interface OrderSummary {
|
|
17
|
+
readonly id: string;
|
|
18
|
+
readonly customerId: string;
|
|
19
|
+
readonly lines: number;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface OrderSummaries {
|
|
23
|
+
ofCustomer(customerId: CustomerId, page: { limit: number; offset: number }): Promise<OrderSummary[]>;
|
|
24
|
+
}
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
**Si el modelo de consultas es eventualmente consistente**, la interfaz debe asumir lo peor. Opciones de IDDD: mostrar temporalmente los datos enviados en el comando; mostrar la fecha y hora de los datos para que el usuario sepa su antigüedad; notificaciones push; o simplemente avisar de que la petición se procesará [IDDD cap. 4].
|
|
28
|
+
|
|
29
|
+
Más detalle en `ddd-skills/ddd-event-sourcing-cqrs/GUIA.md`.
|
|
30
|
+
|
|
31
|
+
## Arquitectura dirigida por eventos
|
|
32
|
+
|
|
33
|
+
Contextos que se comunican publicando y consumiendo eventos de dominio. Es la base de la integración robusta descrita en `ddd-skills/ddd-context-mapping/GUIA.md` y del diseño de `ddd-skills/ddd-eventos-dominio/GUIA.md`. Dentro de un contexto, los eventos también desacoplan efectos secundarios del caso de uso: el caso de uso llama a `events.publish(order.pullDomainEvents())` después de `await this.orders.save(order)` y un listener (`PrepareShipmentOnOrderLineAdded`) ejecuta la acción siguiente.
|
|
34
|
+
|
|
35
|
+
Con el paquete, la tecnología es un `EventChannel` del proyecto (`RabbitMqEventChannel`, un relay que entrega el outbox que el repositorio escribió junto al agregado…) bajo el mismo `DomainEventBus`; en tests, `InMemoryEventChannel`. Hacia otros contextos no viaja `event.toJSON()` de la clase de dominio: el adaptador que habla con el broker lo traduce a un contrato publicado y versionado (`sales.order.OrderPlaced`) [IDDD cap. 13], algo que el paquete no da y escribe el proyecto. `[Fuera de los libros: convención de @archi-code/ddd]`
|
|
36
|
+
|
|
37
|
+
## Event Sourcing
|
|
38
|
+
|
|
39
|
+
El estado de un agregado se guarda como la secuencia de sus eventos y se reconstruye aplicándolos [Destilado cap. 6; IDDD cap. 4]. Aporta historial completo y auditoría, pero cambia por completo la persistencia y las consultas. Ver `ddd-skills/ddd-event-sourcing-cqrs/GUIA.md` antes de decidir.
|
|
40
|
+
|
|
41
|
+
El paquete no trae event store: cada repositorio guarda el `toPrimitives()` del agregado junto a su `version`, que crece 1 por cada `save`, no por evento. Un agregado con Event Sourcing sigue extendiendo `AggregateRoot` y registrando con `record()`, pero su repositorio (en `infrastructure/`, implementando `AggregateFinder`/`AggregateSaver`) lleva la versión del flujo él mismo y reconstruye el agregado aplicando sus eventos, no con `toPrimitives()`/`fromPrimitives()`. `[Fuera de los libros: convención de @archi-code/ddd]`
|
|
42
|
+
|
|
43
|
+
## Tabla de decisión
|
|
44
|
+
|
|
45
|
+
| Situación | Recomendación |
|
|
46
|
+
|---|---|
|
|
47
|
+
| Pantallas simples que siguen a un agregado | Solo hexagonal |
|
|
48
|
+
| Listados y búsquedas que cruzan agregados | Consultas SQL directas de solo lectura |
|
|
49
|
+
| Vistas muy distintas del modelo, alto volumen de lectura, otra base para leer | CQRS con read models por eventos |
|
|
50
|
+
| Requisito legal o de negocio de historial completo de cambios del agregado | Valorar Event Sourcing en ese agregado |
|
|
51
|
+
| Integración entre contextos | Eventos de dominio por un `EventChannel` de mensajería, traducidos a un contrato publicado |
|
|
@@ -0,0 +1,352 @@
|
|
|
1
|
+
# Estructura hexagonal en TypeScript
|
|
2
|
+
|
|
3
|
+
## Estructura base (un contexto por carpeta)
|
|
4
|
+
|
|
5
|
+
`[Fuera de los libros: traslado a TypeScript de las estructuras de IDDD cap. 9 y PHP: Modules]`
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
src/
|
|
9
|
+
contexts/
|
|
10
|
+
sales/ ← Bounded Context (nombre del modelo, no del producto comercial)
|
|
11
|
+
domain/
|
|
12
|
+
order/ ← módulo del lenguaje ubicuo
|
|
13
|
+
Order.ts (agregado raíz: extends AggregateRoot<OrderId, OrderEvent>)
|
|
14
|
+
OrderId.ts (extends IdType<'OrderId'>)
|
|
15
|
+
OrderLine.ts (Composite({ sku: Sku, quantity: Quantity, unitPrice: Money }))
|
|
16
|
+
OrderLines.ts (Composite({ items: [OrderLine] }) con sus reglas)
|
|
17
|
+
OrderStatus.ts (Required(StatusType), StatusType extends EnumType<Status>)
|
|
18
|
+
Sku.ts, Quantity.ts (Required(StringType) / Required(NumberType) + @Validations)
|
|
19
|
+
ShippingAddress.ts (Composite({ street: Street, city: City, country: Country }))
|
|
20
|
+
events.ts (OrderPlaced, OrderLineAdded, OrderConfirmed…: extends EventBase)
|
|
21
|
+
errors.ts (OrderNoLongerEditable extends DomainException, código ORD001)
|
|
22
|
+
customer/
|
|
23
|
+
CustomerId.ts
|
|
24
|
+
pricing/
|
|
25
|
+
ProductPricing.ts (puerto hacia Catálogo, ver ddd-skills/ddd-context-mapping/GUIA.md)
|
|
26
|
+
shared/ ← solo para este contexto
|
|
27
|
+
Money.ts (Composite({ amount: Cents, currency: Currency }))
|
|
28
|
+
application/
|
|
29
|
+
place-order/
|
|
30
|
+
PlaceOrder.ts (extends UseCase)
|
|
31
|
+
add-order-line/
|
|
32
|
+
AddOrderLine.ts
|
|
33
|
+
prepare-shipment/
|
|
34
|
+
PrepareShipment.ts
|
|
35
|
+
PrepareShipmentOnOrderLineAdded.ts (listener junto al caso de uso que ejecuta)
|
|
36
|
+
Warehouse.ts (puerto de salida de aplicación)
|
|
37
|
+
order-summaries/
|
|
38
|
+
OrderSummaries.ts (consulta de lectura: interfaz + DTOs)
|
|
39
|
+
infrastructure/
|
|
40
|
+
persistence/
|
|
41
|
+
MongoOrderRepository.ts (AggregateFinder/AggregateSaver/AggregateRemover<Order>; constructor solo con la collection; guarda order.toPrimitives() con su version y carga con Order.fromPrimitives(row); cada save/remove es atómico)
|
|
42
|
+
MongoOrderSummaries.ts
|
|
43
|
+
catalog/ (ACL hacia Catálogo: CatalogHttpProductPricing, CatalogTranslator)
|
|
44
|
+
warehouse/
|
|
45
|
+
HttpWarehouse.ts
|
|
46
|
+
messaging/
|
|
47
|
+
RabbitMqEventChannel.ts (implements EventChannel)
|
|
48
|
+
interfaces/
|
|
49
|
+
http/
|
|
50
|
+
ordersRouter.ts
|
|
51
|
+
messaging/
|
|
52
|
+
StockReservedConsumer.ts (traduce el contrato publicado por Almacén al input de un caso de uso)
|
|
53
|
+
composition.ts ← composition root del contexto
|
|
54
|
+
warehouse/
|
|
55
|
+
...
|
|
56
|
+
shared-kernel/ ← solo si hay un Shared Kernel acordado (ver ddd-skills/ddd-context-mapping/GUIA.md)
|
|
57
|
+
main.ts ← arranque del proceso
|
|
58
|
+
test/
|
|
59
|
+
support/
|
|
60
|
+
in-memory-order-repository.ts (doble de test: AggregateFinder/AggregateSaver/AggregateRemover<Order>, constructor vacío)
|
|
61
|
+
contexts/sales/
|
|
62
|
+
domain/ (tests unitarios y de aceptación del modelo)
|
|
63
|
+
application/ (casos de uso con InMemoryOrderRepository, InMemoryEventChannel)
|
|
64
|
+
infrastructure/ (integración con BD real o contenedor)
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Lo que no aparece porque lo da `@archi-code/ddd`: clases base (`AggregateRoot`, `Entity`, `EventBase`, los tipos de value object, `Composite`), una interfaz `OrderRepository` (los roles `AggregateFinder`, `AggregateSaver` y `AggregateRemover` vienen de `@archi-code/ddd/domain`), un `OrderMapper` (la conversión es del agregado, `toPrimitives()` y `static fromPrimitives()`, y cada repositorio los llama directamente: guarda `order.toPrimitives()` con `order.version + 1` y carga con `Order.fromPrimitives(row).restoreVersion(row.version)`), un publicador de eventos (`DomainEventBus` implementa `EventBus`) y el puerto `Clock`. Tampoco hay un runner de transacciones ni una unidad de trabajo: la transacción es del repositorio `[Fuera de los libros: convención de @archi-code/ddd]`.
|
|
68
|
+
|
|
69
|
+
Lo que el paquete no da y escribe el proyecto: cada repositorio, uno por agregado. Su constructor recibe solo las dependencias de su tecnología (la `collection` de Mongo, el `pool` de Postgres), nunca la clase del agregado ni un mapper; el agregado solo entra y sale por sus métodos. Nada comprueba en ejecución la forma del agregado: que su estado sean value objects expuestos por getters y que `toPrimitives()`/`fromPrimitives()` no pierdan campos lo garantizan el agregado y su test de ida y vuelta. El repositorio en memoria también es del proyecto y es un doble de test: vive en `test/support/`, nunca en `src/` (ver `InMemoryOrderRepository` en `ddd-skills/ddd-repositorios-persistencia/references/tests-repositorios.md`) `[Fuera de los libros: convención de @archi-code/ddd]`.
|
|
70
|
+
|
|
71
|
+
Decisiones:
|
|
72
|
+
* Los tests de aceptación del modelo están junto al contexto, aunque en carpeta `test/` espejo [Destilado cap. 2 pide tenerlos con el código del contexto; la carpeta espejo mantiene esa correspondencia].
|
|
73
|
+
* `domain/shared/` es compartido **dentro** del contexto y solo guarda conceptos del modelo (`Money`). Nada de un `shared/` global con modelos de varios contextos.
|
|
74
|
+
* Un listener vive en el módulo del caso de uso que ejecuta, no en una carpeta `listeners/`.
|
|
75
|
+
* El composition root vive por contexto para que cada contexto se pueda desplegar por separado si un día hace falta.
|
|
76
|
+
|
|
77
|
+
## Variante para proyectos con la lógica en `src/app/`
|
|
78
|
+
|
|
79
|
+
Algunos proyectos fijan `src/app/` para la lógica principal, `tests/` en raíz y `docs/` para las decisiones, y piden no crear carpetas raíz nuevas sin justificación `[Fuera de los libros]`. La estructura DDD encaja así:
|
|
80
|
+
|
|
81
|
+
```
|
|
82
|
+
src/
|
|
83
|
+
app/
|
|
84
|
+
contexts/
|
|
85
|
+
sales/ (domain, application, infrastructure, interfaces, composition.ts)
|
|
86
|
+
warehouse/
|
|
87
|
+
main.ts
|
|
88
|
+
prompts/ (si el proyecto usa IA; nunca dentro del dominio)
|
|
89
|
+
config/
|
|
90
|
+
tests/
|
|
91
|
+
contexts/sales/...
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
Reglas de encaje:
|
|
95
|
+
* El logger es el puerto `Logger` de `@archi-code/ddd/application` (`debug`, `info`, `warn`, `error`, cada uno con un mensaje estable y un contexto). Lo reciben por constructor `application`, `infrastructure` e `interfaces`; el proveedor (`ConsoleLogger` del paquete, o un adaptador sobre pino, winston…) se elige en el composition root. El dominio no registra logs: registra eventos (`record`) o lanza una `DomainException` con nombre y código. `[Fuera de los libros: convención de @archi-code/ddd]`
|
|
96
|
+
* Las variables de entorno se leen en el composition root y llegan a los adaptadores como configuración tipada. Nunca `process.env` fuera de ahí.
|
|
97
|
+
* Los prompts de IA (si los hay) son infraestructura o aplicación; un modelo de lenguaje es un adaptador detrás de un puerto con nombre de negocio.
|
|
98
|
+
|
|
99
|
+
## Puertos y adaptadores
|
|
100
|
+
|
|
101
|
+
| Tipo | Ejemplo en el contexto de Ventas | Dónde se define la interfaz | Dónde se implementa |
|
|
102
|
+
|---|---|---|---|
|
|
103
|
+
| Entrada (driving) | HTTP `POST /orders`, consumidor de `StockReserved` | El caso de uso es la API de entrada (`execute(input)`) | `interfaces/` |
|
|
104
|
+
| Salida de dominio | `AggregateFinder<Order>`, `AggregateSaver<Order>` (del paquete); `ProductPricing` (tuyo) | `@archi-code/ddd/domain`; `domain/` | `infrastructure/` (`MongoOrderRepository`, `CatalogHttpProductPricing`) |
|
|
105
|
+
| Salida de aplicación | `EventBus`, `Clock`, `Logger` (del paquete); `Warehouse` (tuyo) | `@archi-code/ddd/application`; `application/` | `DomainEventBus` sobre tu `EventChannel`, `SystemClock`, `ConsoleLogger` o tu adaptador; `HttpWarehouse` |
|
|
106
|
+
|
|
107
|
+
En IDDD los puertos no se implementan "a mano": el puerto es HTTP o el bus de mensajes, y el adaptador es el controlador o el listener que traduce a la API de aplicación [IDDD cap. 4]. El interior se diseña por casos de uso, no por número de clientes [IDDD cap. 4].
|
|
108
|
+
|
|
109
|
+
Con el paquete, una tecnología de mensajería solo implementa `EventChannel` (`send` y `receive`); `DomainEventBus`, los casos de uso y los listeners no cambian. `[Fuera de los libros: convención de @archi-code/ddd]`
|
|
110
|
+
|
|
111
|
+
## Composition root sin contenedor
|
|
112
|
+
|
|
113
|
+
```ts
|
|
114
|
+
// src/contexts/sales/composition.ts
|
|
115
|
+
import { MongoClient } from 'mongodb';
|
|
116
|
+
import {
|
|
117
|
+
DomainEventBus,
|
|
118
|
+
EventRegistry,
|
|
119
|
+
HttpErrorMapper,
|
|
120
|
+
SystemClock,
|
|
121
|
+
} from '@archi-code/ddd/infrastructure';
|
|
122
|
+
import { OrderLineAdded, OrderPlaced } from './domain/order/events';
|
|
123
|
+
import type { MoneyData, OrderEvent, OrderLineData } from './domain/order/events';
|
|
124
|
+
import { PlaceOrder } from './application/place-order/PlaceOrder';
|
|
125
|
+
import { AddOrderLine } from './application/add-order-line/AddOrderLine';
|
|
126
|
+
import { PrepareShipment } from './application/prepare-shipment/PrepareShipment';
|
|
127
|
+
import { PrepareShipmentOnOrderLineAdded } from './application/prepare-shipment/PrepareShipmentOnOrderLineAdded';
|
|
128
|
+
import { MongoOrderRepository } from './infrastructure/persistence/MongoOrderRepository';
|
|
129
|
+
import type { OrderDocument } from './infrastructure/persistence/MongoOrderRepository';
|
|
130
|
+
import { HttpWarehouse } from './infrastructure/warehouse/HttpWarehouse';
|
|
131
|
+
import type { HttpClient } from './infrastructure/warehouse/HttpWarehouse';
|
|
132
|
+
import { RabbitMqEventChannel } from './infrastructure/messaging/RabbitMqEventChannel';
|
|
133
|
+
import type { AmqpChannel } from './infrastructure/messaging/RabbitMqEventChannel';
|
|
134
|
+
import { ordersRouter } from './interfaces/http/ordersRouter';
|
|
135
|
+
import type { Logger } from '@archi-code/ddd/application';
|
|
136
|
+
|
|
137
|
+
export interface SalesConfig {
|
|
138
|
+
mongoUrl: string;
|
|
139
|
+
warehouseBaseUrl: string;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export async function composeSales(
|
|
143
|
+
config: SalesConfig,
|
|
144
|
+
deps: { amqp: AmqpChannel; http: HttpClient; logger: Logger },
|
|
145
|
+
) {
|
|
146
|
+
const mongo = await new MongoClient(config.mongoUrl).connect();
|
|
147
|
+
const orders = new MongoOrderRepository(mongo.db().collection<OrderDocument>('orders'));
|
|
148
|
+
const clock = new SystemClock();
|
|
149
|
+
|
|
150
|
+
const registry = new EventRegistry<OrderEvent>()
|
|
151
|
+
.register(
|
|
152
|
+
OrderPlaced,
|
|
153
|
+
(json) =>
|
|
154
|
+
new OrderPlaced(
|
|
155
|
+
json.aggregateId,
|
|
156
|
+
json.customerId as string,
|
|
157
|
+
json.lines as OrderLineData[],
|
|
158
|
+
json.total as MoneyData,
|
|
159
|
+
json,
|
|
160
|
+
),
|
|
161
|
+
)
|
|
162
|
+
.register(
|
|
163
|
+
OrderLineAdded,
|
|
164
|
+
(json) =>
|
|
165
|
+
new OrderLineAdded(
|
|
166
|
+
json.aggregateId,
|
|
167
|
+
json.sku as string,
|
|
168
|
+
json.quantity as number,
|
|
169
|
+
json.unitPrice as MoneyData,
|
|
170
|
+
json.total as MoneyData,
|
|
171
|
+
json,
|
|
172
|
+
),
|
|
173
|
+
);
|
|
174
|
+
const bus = new DomainEventBus<OrderEvent>(
|
|
175
|
+
new RabbitMqEventChannel(deps.amqp),
|
|
176
|
+
registry,
|
|
177
|
+
deps.logger,
|
|
178
|
+
);
|
|
179
|
+
|
|
180
|
+
const placeOrder = new PlaceOrder(orders, bus, clock);
|
|
181
|
+
const addOrderLine = new AddOrderLine(orders, bus, clock);
|
|
182
|
+
const prepareShipment = new PrepareShipment(
|
|
183
|
+
new HttpWarehouse(deps.http, config.warehouseBaseUrl),
|
|
184
|
+
);
|
|
185
|
+
|
|
186
|
+
const subscriptions = [
|
|
187
|
+
await bus.subscribe(new PrepareShipmentOnOrderLineAdded(prepareShipment)),
|
|
188
|
+
];
|
|
189
|
+
|
|
190
|
+
return {
|
|
191
|
+
httpRouter: ordersRouter(
|
|
192
|
+
{ placeOrder, addOrderLine },
|
|
193
|
+
new HttpErrorMapper({ statusByCode: { ORD001: 409 }, logger: deps.logger }),
|
|
194
|
+
),
|
|
195
|
+
close: async () => {
|
|
196
|
+
await Promise.all(subscriptions.map((subscription) => subscription.unsubscribe()));
|
|
197
|
+
await mongo.close();
|
|
198
|
+
},
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
Qué hace cada pieza:
|
|
204
|
+
* Del paquete, en `@archi-code/ddd/infrastructure`: `EventRegistry` (reconstruye cada evento desde su JSON; la factoría pasa el JSON como metadata para conservar `eventId` y `occurredOn`), `DomainEventBus` (implementa `EventBus`, lo único que reciben los casos de uso; su `subscribe(listener)` solo lo llama el composition root), `SystemClock` (implementa `Clock`) y `HttpErrorMapper` (códigos propios como `ORD001` se sobrescriben por código).
|
|
205
|
+
* Código del proyecto sobre los puertos del paquete `[Fuera de los libros: convención de @archi-code/ddd]`: `MongoOrderRepository` (roles de repositorio de `Order`; su constructor recibe solo la `collection`; llama a `order.toPrimitives()` y a `Order.fromPrimitives(row).restoreVersion(row.version)` y controla la `version`; responsable de toda la persistencia: cada `save`/`remove` es una escritura atómica de un documento, y si un agregado ocupara varias tablas SQL abriría y cerraría la transacción de la base de datos dentro de `save`/`remove`; ver `ddd-skills/ddd-repositorios-persistencia/GUIA.md`), `RabbitMqEventChannel` (implementa `EventChannel`, ver `ddd-skills/ddd-eventos-dominio/GUIA.md`) y `HttpWarehouse` (implementa el puerto `Warehouse`).
|
|
206
|
+
* Cada listener se suscribe con `bus.subscribe(listener)`, que lee su `@ListensTo(OrderLineAdded, { consumer: 'prepare-shipment-on-order-line-added' })` y rechaza un evento que el registro no sabe reconstruir.
|
|
207
|
+
* Cambiar RabbitMQ por un outbox o por memoria cambia solo la línea del canal.
|
|
208
|
+
* Ni los casos de uso ni los repositorios reciben un runner de transacciones: el composition root no construye ninguno.
|
|
209
|
+
|
|
210
|
+
Por qué sin contenedor: en TypeScript las interfaces desaparecen en tiempo de ejecución y los contenedores necesitan tokens o decoradores. Para la mayoría de contextos, construir el grafo a mano es más legible y no mete decoradores de inyección en el dominio `[Fuera de los libros]`. Si usas NestJS, sus módulos y proveedores van en `interfaces/` e `infrastructure/`; el dominio sigue sin decoradores salvo `@Validations` de `@archi-code/validation` en los value objects.
|
|
211
|
+
|
|
212
|
+
## El mismo grafo en los tests de aplicación
|
|
213
|
+
|
|
214
|
+
Los tests de `application/` montan el contexto con el repositorio en memoria del proyecto (`test/support/in-memory-order-repository.ts`) y los adaptadores en memoria del paquete; los casos de uso y los listeners son los mismos (`test/contexts/sales/application/salesForTests.ts`):
|
|
215
|
+
|
|
216
|
+
```ts
|
|
217
|
+
import {
|
|
218
|
+
DomainEventBus,
|
|
219
|
+
EventRegistry,
|
|
220
|
+
FixedClock,
|
|
221
|
+
InMemoryEventChannel,
|
|
222
|
+
} from '@archi-code/ddd/infrastructure';
|
|
223
|
+
import { InMemoryOrderRepository } from '../../../support/in-memory-order-repository';
|
|
224
|
+
import { OrderLineAdded } from '@sales/domain/order/events';
|
|
225
|
+
import type { MoneyData, OrderEvent } from '@sales/domain/order/events';
|
|
226
|
+
import { AddOrderLine } from '@sales/application/add-order-line/AddOrderLine';
|
|
227
|
+
import { PlaceOrder } from '@sales/application/place-order/PlaceOrder';
|
|
228
|
+
import { PrepareShipment } from '@sales/application/prepare-shipment/PrepareShipment';
|
|
229
|
+
import { PrepareShipmentOnOrderLineAdded } from '@sales/application/prepare-shipment/PrepareShipmentOnOrderLineAdded';
|
|
230
|
+
import type { Warehouse } from '@sales/application/prepare-shipment/Warehouse';
|
|
231
|
+
|
|
232
|
+
export async function salesForTests(warehouse: Warehouse) {
|
|
233
|
+
const orders = new InMemoryOrderRepository();
|
|
234
|
+
const channel = new InMemoryEventChannel();
|
|
235
|
+
const registry = new EventRegistry<OrderEvent>().register(
|
|
236
|
+
OrderLineAdded,
|
|
237
|
+
(json) =>
|
|
238
|
+
new OrderLineAdded(
|
|
239
|
+
json.aggregateId,
|
|
240
|
+
json.sku as string,
|
|
241
|
+
json.quantity as number,
|
|
242
|
+
json.unitPrice as MoneyData,
|
|
243
|
+
json.total as MoneyData,
|
|
244
|
+
json,
|
|
245
|
+
),
|
|
246
|
+
);
|
|
247
|
+
const bus = new DomainEventBus<OrderEvent>(channel, registry);
|
|
248
|
+
await bus.subscribe(new PrepareShipmentOnOrderLineAdded(new PrepareShipment(warehouse)));
|
|
249
|
+
const clock = new FixedClock('2026-03-01T09:30:00Z');
|
|
250
|
+
return {
|
|
251
|
+
orders,
|
|
252
|
+
channel,
|
|
253
|
+
placeOrder: new PlaceOrder(orders, bus, clock),
|
|
254
|
+
addOrderLine: new AddOrderLine(orders, bus, clock),
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
El test comprueba lo que causó el listener (el `Warehouse` de prueba) y lo que llevó el canal (`channel.sent`), nunca que el listener fue llamado. `InMemoryOrderRepository` (cada `save` y `remove` atómico, con comprobación de versión) es un doble de test del proyecto: vive en `test/support/` y nunca se usa en producción `[Fuera de los libros: convención de @archi-code/ddd]`.
|
|
260
|
+
|
|
261
|
+
## Controlador como adaptador de entrada
|
|
262
|
+
|
|
263
|
+
```ts
|
|
264
|
+
// src/contexts/sales/interfaces/http/ordersRouter.ts
|
|
265
|
+
import { Router } from 'express';
|
|
266
|
+
import type { Response } from 'express';
|
|
267
|
+
import type { HttpErrorMapper } from '@archi-code/ddd/infrastructure';
|
|
268
|
+
import type { AddOrderLine } from '../../application/add-order-line/AddOrderLine';
|
|
269
|
+
import type { PlaceOrder } from '../../application/place-order/PlaceOrder';
|
|
270
|
+
|
|
271
|
+
export function ordersRouter(
|
|
272
|
+
useCases: { placeOrder: PlaceOrder; addOrderLine: AddOrderLine },
|
|
273
|
+
errors: HttpErrorMapper,
|
|
274
|
+
): Router {
|
|
275
|
+
const router = Router();
|
|
276
|
+
|
|
277
|
+
const fail = (res: Response, error: unknown) => {
|
|
278
|
+
const { status, body } = errors.map(error);
|
|
279
|
+
return res.status(status).json(body);
|
|
280
|
+
};
|
|
281
|
+
|
|
282
|
+
router.post('/orders', async (req, res) => {
|
|
283
|
+
const body = req.body as {
|
|
284
|
+
lines?: unknown;
|
|
285
|
+
shippingAddress?: { street?: unknown; city?: unknown; country?: unknown };
|
|
286
|
+
} | null;
|
|
287
|
+
const address = body?.shippingAddress;
|
|
288
|
+
if (
|
|
289
|
+
!Array.isArray(body?.lines) ||
|
|
290
|
+
typeof address?.street !== 'string' ||
|
|
291
|
+
typeof address.city !== 'string' ||
|
|
292
|
+
typeof address.country !== 'string'
|
|
293
|
+
) {
|
|
294
|
+
return res
|
|
295
|
+
.status(400)
|
|
296
|
+
.json({ code: 'BAD_REQUEST', message: 'lines and shippingAddress are required' });
|
|
297
|
+
}
|
|
298
|
+
try {
|
|
299
|
+
const orderId = await useCases.placeOrder.execute({
|
|
300
|
+
customerId: req.auth.customerId,
|
|
301
|
+
lines: body.lines,
|
|
302
|
+
shippingAddress: { street: address.street, city: address.city, country: address.country },
|
|
303
|
+
});
|
|
304
|
+
return res.status(201).location(`/orders/${orderId}`).json({ orderId });
|
|
305
|
+
} catch (error) {
|
|
306
|
+
return fail(res, error);
|
|
307
|
+
}
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
router.post('/orders/:orderId/lines', async (req, res) => {
|
|
311
|
+
const body = req.body as {
|
|
312
|
+
sku?: unknown;
|
|
313
|
+
quantity?: unknown;
|
|
314
|
+
unitPrice?: { amount?: unknown; currency?: unknown };
|
|
315
|
+
} | null;
|
|
316
|
+
if (
|
|
317
|
+
typeof body?.sku !== 'string' ||
|
|
318
|
+
typeof body.quantity !== 'number' ||
|
|
319
|
+
typeof body.unitPrice?.amount !== 'number' ||
|
|
320
|
+
typeof body.unitPrice.currency !== 'string'
|
|
321
|
+
) {
|
|
322
|
+
return res
|
|
323
|
+
.status(400)
|
|
324
|
+
.json({ code: 'BAD_REQUEST', message: 'sku, quantity and unitPrice are required' });
|
|
325
|
+
}
|
|
326
|
+
try {
|
|
327
|
+
await useCases.addOrderLine.execute({
|
|
328
|
+
orderId: req.params.orderId,
|
|
329
|
+
sku: body.sku,
|
|
330
|
+
quantity: body.quantity,
|
|
331
|
+
unitPrice: { amount: body.unitPrice.amount, currency: body.unitPrice.currency },
|
|
332
|
+
});
|
|
333
|
+
return res.status(204).json(null);
|
|
334
|
+
} catch (error) {
|
|
335
|
+
return fail(res, error);
|
|
336
|
+
}
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
return router;
|
|
340
|
+
}
|
|
341
|
+
```
|
|
342
|
+
|
|
343
|
+
El controlador no sabe qué es un `Order`. Comprueba solo la forma de la petición (un campo es texto o número) [IDDD cap. 4]; las reglas de cada valor son de los value objects que `toCommand` construye con `ValueObjects.build`, y llegan como una `ValidationException` con todos los campos (422). La identidad del cliente sale del token, no del body. `HttpErrorMapper` traduce los errores: 404 `AggregateNotFoundException`, 403 `NotAllowedException`, 409 `ConcurrencyException`, 422 `ValidationException` y cualquier otra `DomainException`, 500 con un mensaje genérico para lo demás. Con `logger`, el mapper registra cada 5xx una vez como `error` (con su causa) y los 4xx solo en `debug`; el controlador no vuelve a registrarlos.
|
|
344
|
+
|
|
345
|
+
## Configuración de TypeScript
|
|
346
|
+
|
|
347
|
+
`[Fuera de los libros]`
|
|
348
|
+
* `strict: true`. El dominio se apoya en el tipado para expresar invariantes.
|
|
349
|
+
* `experimentalDecorators: true`: `@Validations` (value objects) y `@ListensTo` (listeners) son decoradores de clase.
|
|
350
|
+
* `moduleResolution` `node16`, `nodenext` o `bundler` para resolver los subpaths `@archi-code/ddd/domain`, `/application`, `/infrastructure` y `/exceptions`; con `node` los resuelve el `typesVersions` del paquete.
|
|
351
|
+
* Alias por contexto (`"@sales/*": ["src/contexts/sales/*"]`, usado como `@sales/domain/order/Order`) para que los imports revelen la zona.
|
|
352
|
+
* `exactOptionalPropertyTypes` y `noUncheckedIndexedAccess` ayudan a que los value objects no acepten estados a medias.
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# Módulos
|
|
2
|
+
|
|
3
|
+
## Qué son
|
|
4
|
+
|
|
5
|
+
En DDD, un módulo es un contenedor con nombre para clases del dominio muy cohesionadas entre sí, buscando bajo acoplamiento con las de otros módulos. No es un cajón genérico: su nombre forma parte del lenguaje ubicuo [IDDD cap. 9]. Deben contar la historia del sistema y reflejar conocimiento del dominio [IDDD cap. 9, citando a Evans].
|
|
6
|
+
|
|
7
|
+
## Reglas
|
|
8
|
+
|
|
9
|
+
* Nombres del lenguaje ubicuo [IDDD cap. 9; PHP: Modules].
|
|
10
|
+
* Nunca nombres de patrones o piezas (`valueobjects`, `services`, `entities`) [PHP: Modules].
|
|
11
|
+
* Lo de dentro, lo menos acoplado posible con otros módulos [PHP: Modules].
|
|
12
|
+
* Refactoriza módulos como el código: muévelos, renómbralos, agrúpalos [IDDD cap. 9; PHP: Modules].
|
|
13
|
+
* Nada de nombres comerciales de producto: cambian. Usa el nombre del contexto [IDDD cap. 9; PHP: Modules].
|
|
14
|
+
* El segundo nivel identifica el Bounded Context, luego la zona (`domain`, `application`...), luego los módulos del modelo [IDDD cap. 9].
|
|
15
|
+
* En aplicación e interfaz, submódulos solo si hay suficientes servicios o recursos para que ayude (IDDD sugiere pensarlo a partir de media docena) [IDDD cap. 9].
|
|
16
|
+
|
|
17
|
+
## Módulo antes que Bounded Context
|
|
18
|
+
|
|
19
|
+
Si la terminología es difusa y no está claro que haga falta una frontera de contexto, mantén los conceptos juntos y sepáralos con un módulo, que es una frontera más fina. Los contextos no sustituyen a los módulos; los módulos separan lo poco cohesionado dentro de un mismo lenguaje [IDDD cap. 9].
|
|
20
|
+
|
|
21
|
+
Y en sentido contrario: un módulo no arregla un lenguaje mezclado. Si un término significa dos cosas, hace falta otro contexto [IDDD cap. 2].
|
|
22
|
+
|
|
23
|
+
## En TypeScript
|
|
24
|
+
|
|
25
|
+
`[Fuera de los libros]`
|
|
26
|
+
* Un módulo es una carpeta con un `index.ts` que exporta solo su API pública. Lo demás no se importa desde fuera del módulo.
|
|
27
|
+
* Un agregado y todo lo que solo él usa viven en el mismo módulo: `Order`, `OrderId`, `OrderLine` (un `Composite`), `OrderStatus`, sus eventos (`OrderPlaced`, `OrderLineAdded`) y sus excepciones de negocio (`OrderNoLongerEditable extends DomainException`).
|
|
28
|
+
* Las clases base (`AggregateRoot`, `EventBase`, `Composite`, los tipos de value object) y los roles de repositorio (`AggregateFinder`, `AggregateSaver`, `AggregateRemover`) no forman un módulo del proyecto: se importan de `@archi-code/ddd/domain` donde se usan. `[Fuera de los libros: convención de @archi-code/ddd]`
|
|
29
|
+
* En `application/`, un módulo por acción de negocio (`place-order/`, `add-order-line/`); el listener vive en el módulo del caso de uso que ejecuta (`prepare-shipment/PrepareShipmentOnOrderLineAdded.ts`), nunca en una carpeta `listeners/`.
|
|
30
|
+
* Si dos módulos se importan mutuamente, el modelo está enredado: revisa los conceptos antes de mover ficheros.
|
|
31
|
+
|
|
32
|
+
Ejemplo bueno y malo:
|
|
33
|
+
|
|
34
|
+
```
|
|
35
|
+
domain/ domain/
|
|
36
|
+
order/ entities/
|
|
37
|
+
Order.ts Order.ts
|
|
38
|
+
OrderLine.ts Invoice.ts
|
|
39
|
+
events.ts valueobjects/
|
|
40
|
+
invoicing/ Money.ts
|
|
41
|
+
Invoice.ts OrderLine.ts
|
|
42
|
+
shared/ events/
|
|
43
|
+
Money.ts OrderPlaced.ts
|
|
44
|
+
shared/
|
|
45
|
+
AggregateRoot.ts
|
|
46
|
+
(cuenta la historia) (cuenta los patrones usados y
|
|
47
|
+
reescribe lo que da el paquete)
|
|
48
|
+
```
|