@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,294 @@
|
|
|
1
|
+
# La transacción es del repositorio
|
|
2
|
+
|
|
3
|
+
El dominio nunca es el lugar para gestionar transacciones; sus operaciones son demasiado finas y no deben saber que existen. Los libros las sitúan en la capa de aplicación, a menudo con una fachada transaccional o de forma declarativa [IDDD cap. 12; PHP: Repositories, "Managing Transactions"].
|
|
4
|
+
|
|
5
|
+
Con el paquete la transacción baja un nivel más: es del repositorio, igual que todo lo demás de la persistencia `[Fuera de los libros: convención de @archi-code/ddd]`. La aplicación nunca abre, une ni cierra transacciones y no hay ningún puerto para hacerlo. Un caso de uso recibe solo los roles del repositorio que usa (`AggregateFinder`, `AggregateSaver`, `AggregateRemover`), el `EventBus`, el `Clock` y sus puertos propios; nunca una dependencia `transactions`, `unitOfWork` o parecida.
|
|
6
|
+
|
|
7
|
+
## Por qué basta con el repositorio
|
|
8
|
+
|
|
9
|
+
Un caso de uso modifica un solo agregado y lo guarda una vez [IDDD cap. 10]. El agregado es el límite de consistencia, así que la "transacción por agregado" de los libros se traduce en **una escritura atómica por agregado, garantizada por el repositorio** `[Fuera de los libros: convención de @archi-code/ddd]`. No queda nada que la aplicación tenga que envolver: cargar es una lectura, y `save` o `remove` es una sola escritura atómica.
|
|
10
|
+
|
|
11
|
+
Lo que hace el adaptador del repositorio, y nadie más:
|
|
12
|
+
|
|
13
|
+
* Que cada `save` y cada `remove` sea atómico. En MongoDB, un documento por agregado: una sola escritura, atómica por sí misma. Si el agregado ocupa varias tablas SQL, el repositorio abre la transacción de la base de datos **dentro** de `save`/`remove`, con su propia conexión o sesión, y la confirma o la revierte antes de volver.
|
|
14
|
+
* La comprobación optimista de la versión: una versión almacenada distinta de `order.version` (o un id ya ocupado al dar de alta) es `ConcurrencyException('Order', order.id.value, order.version)`.
|
|
15
|
+
* La conexión, la sesión y el pool.
|
|
16
|
+
* Envolver los errores del driver en `InternalErrorException` con `{ cause }`.
|
|
17
|
+
* `markPersisted()` tras escribir con éxito y, al cargar, `Order.fromPrimitives(row).restoreVersion(version)`.
|
|
18
|
+
|
|
19
|
+
## Varias tablas en PostgreSQL
|
|
20
|
+
|
|
21
|
+
`[Fuera de los libros: esquema propio siguiendo IDDD cap. 6]`
|
|
22
|
+
|
|
23
|
+
Si las líneas del pedido viven en su propia tabla, guardar el pedido son varias sentencias, y el repositorio las agrupa en una transacción de la base de datos dentro de `save`. `SqlConnection` y `PgPool` describen, de forma estructural, los métodos de `pg` que usa; en `infrastructure/persistence/pg-pool.ts`:
|
|
24
|
+
|
|
25
|
+
```ts
|
|
26
|
+
export interface SqlConnection {
|
|
27
|
+
query<Row>(text: string, values?: unknown[]): Promise<{ rows: Row[]; rowCount: number | null }>;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface PgPool extends SqlConnection {
|
|
31
|
+
connect(): Promise<SqlConnection & { release(destroy?: boolean): void }>;
|
|
32
|
+
}
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
```sql
|
|
36
|
+
CREATE TABLE orders (
|
|
37
|
+
id UUID PRIMARY KEY,
|
|
38
|
+
version INT NOT NULL,
|
|
39
|
+
customer_id UUID NOT NULL,
|
|
40
|
+
status TEXT NOT NULL,
|
|
41
|
+
data JSONB NOT NULL
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
CREATE TABLE order_lines (
|
|
45
|
+
order_id UUID NOT NULL REFERENCES orders (id) ON DELETE CASCADE,
|
|
46
|
+
position INT NOT NULL,
|
|
47
|
+
sku TEXT NOT NULL,
|
|
48
|
+
quantity INT NOT NULL,
|
|
49
|
+
unit_price JSONB NOT NULL,
|
|
50
|
+
PRIMARY KEY (order_id, position)
|
|
51
|
+
);
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
En `infrastructure/persistence/postgres-order-repository.ts`:
|
|
55
|
+
|
|
56
|
+
```ts
|
|
57
|
+
import type { AggregateFinder, AggregateSaver, PrimitiveTypes } from '@archi-code/ddd/domain';
|
|
58
|
+
import { ConcurrencyException, InternalErrorException } from '@archi-code/ddd/exceptions';
|
|
59
|
+
import { Order, OrderId } from '../../domain/order/order';
|
|
60
|
+
import type { PgPool } from './pg-pool';
|
|
61
|
+
|
|
62
|
+
type OrderPrimitives = PrimitiveTypes<Order>;
|
|
63
|
+
|
|
64
|
+
interface OrderRow {
|
|
65
|
+
version: number;
|
|
66
|
+
data: Omit<OrderPrimitives, 'lines'>;
|
|
67
|
+
items: OrderPrimitives['lines']['items'];
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export class PostgresOrderRepository implements AggregateFinder<Order>, AggregateSaver<Order> {
|
|
71
|
+
constructor(private readonly pool: PgPool) {}
|
|
72
|
+
|
|
73
|
+
async findById(id: OrderId): Promise<Order | null> {
|
|
74
|
+
const { rows } = await this.pool.query<OrderRow>(
|
|
75
|
+
`SELECT o.version, o.data,
|
|
76
|
+
COALESCE(
|
|
77
|
+
json_agg(
|
|
78
|
+
json_build_object('sku', l.sku, 'quantity', l.quantity, 'unitPrice', l.unit_price)
|
|
79
|
+
ORDER BY l.position
|
|
80
|
+
) FILTER (WHERE l.order_id IS NOT NULL),
|
|
81
|
+
'[]'
|
|
82
|
+
) AS items
|
|
83
|
+
FROM orders o
|
|
84
|
+
LEFT JOIN order_lines l ON l.order_id = o.id
|
|
85
|
+
WHERE o.id = $1
|
|
86
|
+
GROUP BY o.id`,
|
|
87
|
+
[id.value],
|
|
88
|
+
);
|
|
89
|
+
const row = rows[0];
|
|
90
|
+
return row ? Order.fromPrimitives({ ...row.data, lines: { items: row.items } }).restoreVersion(row.version) : null;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
async save(order: Order): Promise<void> {
|
|
94
|
+
const { lines, ...data } = order.toPrimitives();
|
|
95
|
+
const client = await this.pool.connect();
|
|
96
|
+
let broken = false;
|
|
97
|
+
try {
|
|
98
|
+
await client.query('BEGIN');
|
|
99
|
+
const { rowCount } =
|
|
100
|
+
order.version === 0
|
|
101
|
+
? await client.query(
|
|
102
|
+
'INSERT INTO orders (id, version, customer_id, status, data) VALUES ($1, 1, $2, $3, $4) ON CONFLICT (id) DO NOTHING',
|
|
103
|
+
[data.id, data.customerId, data.status, data],
|
|
104
|
+
)
|
|
105
|
+
: await client.query(
|
|
106
|
+
'UPDATE orders SET version = version + 1, status = $3, data = $4 WHERE id = $1 AND version = $2',
|
|
107
|
+
[data.id, order.version, data.status, data],
|
|
108
|
+
);
|
|
109
|
+
if (rowCount === 0) {
|
|
110
|
+
throw new ConcurrencyException('Order', order.id.value, order.version);
|
|
111
|
+
}
|
|
112
|
+
await client.query('DELETE FROM order_lines WHERE order_id = $1', [data.id]);
|
|
113
|
+
for (const [position, line] of lines.items.entries()) {
|
|
114
|
+
await client.query(
|
|
115
|
+
'INSERT INTO order_lines (order_id, position, sku, quantity, unit_price) VALUES ($1, $2, $3, $4, $5)',
|
|
116
|
+
[data.id, position, line.sku, line.quantity, line.unitPrice],
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
await client.query('COMMIT');
|
|
120
|
+
} catch (error) {
|
|
121
|
+
await client.query('ROLLBACK').catch(() => {
|
|
122
|
+
broken = true;
|
|
123
|
+
});
|
|
124
|
+
if (error instanceof ConcurrencyException) {
|
|
125
|
+
throw error;
|
|
126
|
+
}
|
|
127
|
+
throw new InternalErrorException(`writing order ${order.id.value}`, { cause: error });
|
|
128
|
+
} finally {
|
|
129
|
+
client.release(broken);
|
|
130
|
+
}
|
|
131
|
+
order.markPersisted();
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
* La transacción empieza y termina dentro de `save`: el caso de uso solo ve una escritura que se hace entera o no se hace.
|
|
137
|
+
* El `UPDATE` con `version = $2` bloquea la fila del pedido hasta el `COMMIT`; otro `save` del mismo pedido espera y después no casa ninguna fila, así que recibe `ConcurrencyException`.
|
|
138
|
+
* `markPersisted()` se llama solo después del `COMMIT`: si la transacción se revierte, el objeto conserva su versión y el caso de uso puede descartarlo y volver a cargarlo.
|
|
139
|
+
* Si el `ROLLBACK` falla (la conexión se cayó), se relanza el error del trabajo, no el del `ROLLBACK`, y `release(true)` destruye la conexión en vez de devolver al pool una con la transacción a medias.
|
|
140
|
+
* La lectura es una sola sentencia, así que ve el pedido y sus líneas en el mismo instante.
|
|
141
|
+
* Un `remove` sigue el mismo patrón: `DELETE FROM orders WHERE id = $1 AND version = $2` dentro de su propia transacción (las líneas caen con `ON DELETE CASCADE`).
|
|
142
|
+
|
|
143
|
+
Con MongoDB no hace falta nada de esto: el pedido es un documento y `replaceOne` lo escribe de forma atómica (`MongoOrderRepository` en `ddd-skills/ddd-repositorios-persistencia/references/implementacion-ts.md`).
|
|
144
|
+
|
|
145
|
+
## Eventos y guardado
|
|
146
|
+
|
|
147
|
+
El caso de uso guarda y después publica: `await this.orders.save(order)` y `await this.events.publish(order.pullDomainEvents())`. Si `publish` falla después de `save`, el cambio queda guardado y el evento puede perderse.
|
|
148
|
+
|
|
149
|
+
Cuando un evento no puede perderse, el repositorio escribe el agregado y sus eventos pendientes en la misma escritura atómica (una bandeja de salida, *outbox*, dentro del mismo documento o en la misma transacción de `save`), y un proceso aparte (*relay*) los entrega después a través de un `EventChannel` `[Fuera de los libros: convención de @archi-code/ddd]`. En el repositorio de PostgreSQL de arriba es una sentencia más antes del `COMMIT`:
|
|
150
|
+
|
|
151
|
+
```ts
|
|
152
|
+
for (const event of order.pullDomainEvents()) {
|
|
153
|
+
await client.query('INSERT INTO order_outbox (event_id, body) VALUES ($1, $2)', [
|
|
154
|
+
event.eventId,
|
|
155
|
+
event.toJSON(),
|
|
156
|
+
]);
|
|
157
|
+
}
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
El repositorio toma los eventos con `pullDomainEvents()`, así que el `publish` posterior del caso de uso recibe una lista vacía y no envía nada: los casos de uso no cambian. Es persistencia, así que pertenece al adaptador del repositorio.
|
|
161
|
+
|
|
162
|
+
## Uso en un caso de uso
|
|
163
|
+
|
|
164
|
+
El caso de uso carga, cambia, guarda y publica, sin abrir nada. Si el comando es seguro de repetir contra el estado recargado, la aplicación puede reintentar una o dos veces ante `ConcurrencyException`; si no, el conflicto llega al usuario como 409 [IDDD cap. 10; IDDD apéndice A]. Cada intento vuelve a cargar y vuelve a guardar dentro de `handle`, en un bucle con límite `[Fuera de los libros: convención de @archi-code/ddd]`. Un caso de uso que ejecuta un listener no reintenta por su cuenta: reintenta la reentrega del canal.
|
|
165
|
+
|
|
166
|
+
```ts
|
|
167
|
+
import { UseCase } from '@archi-code/ddd/application';
|
|
168
|
+
import type { Clock, EventBus } from '@archi-code/ddd/application';
|
|
169
|
+
import { ValueObjects } from '@archi-code/ddd/domain';
|
|
170
|
+
import type { AggregateFinder, AggregateSaver } from '@archi-code/ddd/domain';
|
|
171
|
+
import { AggregateNotFoundException, ConcurrencyException } from '@archi-code/ddd/exceptions';
|
|
172
|
+
import { Order, OrderId, OrderLine } from '../../domain/order/order';
|
|
173
|
+
import type { OrderEvent } from '../../domain/order/order';
|
|
174
|
+
|
|
175
|
+
export interface AddOrderLineInput {
|
|
176
|
+
orderId: string;
|
|
177
|
+
sku: string;
|
|
178
|
+
quantity: number;
|
|
179
|
+
unitPrice: { amount: number; currency: string };
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
interface AddOrderLineCommand {
|
|
183
|
+
orderId: OrderId;
|
|
184
|
+
line: OrderLine;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const ATTEMPTS = 3;
|
|
188
|
+
|
|
189
|
+
export class AddOrderLine extends UseCase<AddOrderLineInput, AddOrderLineCommand, void> {
|
|
190
|
+
constructor(
|
|
191
|
+
private readonly orders: AggregateFinder<Order> & AggregateSaver<Order>,
|
|
192
|
+
private readonly events: EventBus<OrderEvent>,
|
|
193
|
+
private readonly clock: Clock,
|
|
194
|
+
) {
|
|
195
|
+
super();
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
protected toCommand(input: AddOrderLineInput): AddOrderLineCommand {
|
|
199
|
+
return ValueObjects.build({
|
|
200
|
+
orderId: () => new OrderId(input.orderId),
|
|
201
|
+
line: () =>
|
|
202
|
+
OrderLine.fromPrimitives({
|
|
203
|
+
sku: input.sku,
|
|
204
|
+
quantity: input.quantity,
|
|
205
|
+
unitPrice: input.unitPrice,
|
|
206
|
+
}),
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
protected async handle(command: AddOrderLineCommand): Promise<void> {
|
|
211
|
+
for (let attempt = 1; ; attempt += 1) {
|
|
212
|
+
const order = await this.orders.findById(command.orderId);
|
|
213
|
+
if (order === null) {
|
|
214
|
+
throw new AggregateNotFoundException('Order', command.orderId.value);
|
|
215
|
+
}
|
|
216
|
+
order.addLine(command.line, this.clock);
|
|
217
|
+
try {
|
|
218
|
+
await this.orders.save(order);
|
|
219
|
+
} catch (error) {
|
|
220
|
+
if (error instanceof ConcurrencyException && attempt < ATTEMPTS) {
|
|
221
|
+
continue;
|
|
222
|
+
}
|
|
223
|
+
throw error;
|
|
224
|
+
}
|
|
225
|
+
await this.events.publish(order.pullDomainEvents());
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
`AggregateNotFoundException` la lanza el caso de uso, no el repositorio: `findById` devuelve `null`. Solo se reintenta el `save`: un error de `publish` no vuelve a guardar el pedido.
|
|
233
|
+
|
|
234
|
+
## En tests
|
|
235
|
+
|
|
236
|
+
Los tests de la aplicación usan el `InMemoryOrderRepository` que el proyecto escribe en `test/support/` (`ddd-skills/ddd-repositorios-persistencia/references/tests-repositorios.md`) y un `DomainEventBus` sobre `InMemoryEventChannel`; no hay nada que revertir ni que contar, porque cada `save` del repositorio en memoria es atómico y comprueba la versión igual que uno real. En `test/contexts/sales/application/add-order-line.spec.ts`:
|
|
237
|
+
|
|
238
|
+
```ts
|
|
239
|
+
import { describe, expect, it } from 'vitest';
|
|
240
|
+
import {
|
|
241
|
+
DomainEventBus,
|
|
242
|
+
EventRegistry,
|
|
243
|
+
FixedClock,
|
|
244
|
+
InMemoryEventChannel,
|
|
245
|
+
} from '@archi-code/ddd/infrastructure';
|
|
246
|
+
import { AddOrderLine } from '../../../../src/contexts/sales/application/add-order-line/add-order-line';
|
|
247
|
+
import {
|
|
248
|
+
CustomerId,
|
|
249
|
+
Order,
|
|
250
|
+
OrderId,
|
|
251
|
+
OrderLines,
|
|
252
|
+
ShippingAddress,
|
|
253
|
+
} from '../../../../src/contexts/sales/domain/order/order';
|
|
254
|
+
import type { OrderEvent } from '../../../../src/contexts/sales/domain/order/order';
|
|
255
|
+
import { InMemoryOrderRepository } from '../../../support/in-memory-order-repository';
|
|
256
|
+
|
|
257
|
+
describe('Two customers add a line to the same order at once', () => {
|
|
258
|
+
it('the second one retries with the reloaded order and both lines are kept', async () => {
|
|
259
|
+
const clock = new FixedClock('2026-03-01T09:30:00Z');
|
|
260
|
+
const orders = new InMemoryOrderRepository();
|
|
261
|
+
const channel = new InMemoryEventChannel();
|
|
262
|
+
const addOrderLine = new AddOrderLine(
|
|
263
|
+
orders,
|
|
264
|
+
new DomainEventBus<OrderEvent>(channel, new EventRegistry<OrderEvent>()),
|
|
265
|
+
clock,
|
|
266
|
+
);
|
|
267
|
+
const order = Order.place(
|
|
268
|
+
OrderId.generate(),
|
|
269
|
+
CustomerId.generate(),
|
|
270
|
+
OrderLines.fromPrimitives({
|
|
271
|
+
items: [{ sku: 'TORCH-01', quantity: 2, unitPrice: { amount: 4990, currency: 'EUR' } }],
|
|
272
|
+
}),
|
|
273
|
+
ShippingAddress.fromPrimitives({ street: 'Calle Mayor 1', city: 'Huelva', country: 'ES' }),
|
|
274
|
+
clock,
|
|
275
|
+
);
|
|
276
|
+
await orders.save(order);
|
|
277
|
+
const unitPrice = { amount: 1990, currency: 'EUR' };
|
|
278
|
+
|
|
279
|
+
await Promise.all([
|
|
280
|
+
addOrderLine.execute({ orderId: order.id.value, sku: 'HELMET-02', quantity: 1, unitPrice }),
|
|
281
|
+
addOrderLine.execute({ orderId: order.id.value, sku: 'GLOVES-03', quantity: 1, unitPrice }),
|
|
282
|
+
]);
|
|
283
|
+
|
|
284
|
+
const stored = await orders.findById(order.id);
|
|
285
|
+
expect(stored?.toPrimitives().lines.items).toHaveLength(3);
|
|
286
|
+
expect(channel.sent).toMatchObject([
|
|
287
|
+
{ eventName: 'OrderLineAdded', sku: 'HELMET-02' },
|
|
288
|
+
{ eventName: 'OrderLineAdded', sku: 'GLOVES-03' },
|
|
289
|
+
]);
|
|
290
|
+
});
|
|
291
|
+
});
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
Los dos casos de uso cargan el pedido en la versión 1; el primero guarda, el segundo recibe `ConcurrencyException`, vuelve a cargar y guarda sobre la versión 2. La atomicidad de un repositorio real (sus varias tablas, su outbox) se prueba en su propio spec, con la suite de contrato de `ddd-skills/ddd-repositorios-persistencia/references/tests-repositorios.md` contra la base de datos de verdad.
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
# Servicios de dominio y capa de aplicación
|
|
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.** Servicios de dominio y servicios de aplicación (casos de uso) en DDD con TypeScript. Cubre cuándo una operación merece un servicio de dominio y cuándo pertenece a una entidad o value object, cómo evitar el modelo anémico, cómo escribir casos de uso finos (cargar, invocar, guardar, publicar), comandos y DTOs de entrada, salida mediante DTOs o read models, command bus, dónde queda la transacción (en el repositorio), seguridad y autorización, manejo de errores y tests de la capa de aplicación con adaptadores en memoria. Consúltala siempre que se escriba o revise un "service", "manager", "handler", "use case" o "interactor", cuando la lógica de negocio esté en servicios en vez de en el modelo, al decidir dónde va una regla que implica varios objetos, o al conectar un controlador con el dominio, aunque no se mencione DDD.
|
|
6
|
+
|
|
7
|
+
## Con @archi-code/ddd
|
|
8
|
+
|
|
9
|
+
[Fuera de los libros: convención de @archi-code/ddd]
|
|
10
|
+
|
|
11
|
+
* Un caso de uso extiende `UseCase<Input, Command, Output>`: un `Input` de primitivos, un `Command` de value objects que construye `toCommand` con `ValueObjects.build` (informa de todos los campos a la vez), y `handle`, sin ayudas de la clase base, carga, cambia, guarda (`await this.orders.save(order)`) y publica (`await this.events.publish(order.pullDomainEvents())`). No hay puerto de transacciones: el caso de uso recibe solo los roles de repositorio que usa (`AggregateFinder`, `AggregateSaver`, `AggregateRemover`), `EventBus`, `Clock` y sus puertos propios, y el repositorio garantiza que cada `save` o `remove` es una escritura atómica. Una acción de negocio por caso de uso, con nombre de verbo del negocio (`PlaceOrder`, `AddOrderLine`).
|
|
12
|
+
* Un caso de uso nunca importa ni llama a otro: las acciones se encadenan con eventos y listeners `<Acción>On<Evento>` decorados con `@ListensTo`, que solo mapean el evento a la entrada de un caso de uso. No hay bus de comandos: el punto de entrada llama a `execute(input)`.
|
|
13
|
+
* Los servicios de dominio son clases sin base en `domain/` que reciben agregados y value objects, y pueden leer con un contrato `AggregateFinder` o un puerto de dominio; nunca guardan ni publican, y no tocan la persistencia.
|
|
14
|
+
* Errores: `AggregateNotFoundException` (404) y `NotAllowedException` (403) en el caso de uso, `DomainException` con código propio en el dominio, y `HttpErrorMapper` en los puntos de entrada; `execute` envuelve cualquier error ajeno al paquete en `InternalErrorException` con `cause`.
|
|
15
|
+
* Un `ConcurrencyException` se reintenta, si el comando es seguro de repetir, con un bucle en `handle` con un límite de intentos en el que cada intento vuelve a cargar el agregado y vuelve a guardarlo. Un caso de uso que ejecuta un listener no reintenta por sí mismo: reintenta la reentrega del canal.
|
|
16
|
+
* Un caso de uso que ejecuta un listener para registrar un hecho no lanza si falta el agregado: retorna, para que el canal no lo reentregue sin fin.
|
|
17
|
+
|
|
18
|
+
Dos cosas con el mismo nombre ("servicio") y papeles opuestos. Confundirlas es la vía más rápida hacia un modelo anémico [IDDD cap. 7 y cap. 14].
|
|
19
|
+
|
|
20
|
+
| | Servicio de dominio | Servicio de aplicación |
|
|
21
|
+
|---|---|---|
|
|
22
|
+
| Contiene lógica de negocio | **Sí** | **No** |
|
|
23
|
+
| Vive en | `domain/` | `application/` |
|
|
24
|
+
| Nombre | Del lenguaje ubicuo | Del caso de uso |
|
|
25
|
+
| Estado | Sin estado [IDDD cap. 7] | Sin estado; coordina |
|
|
26
|
+
| Transacciones y seguridad | Nunca [IDDD cap. 7] | Seguridad sí; transacciones según IDDD sí [IDDD cap. 14], con el paquete las garantiza el repositorio `[Fuera de los libros: convención de @archi-code/ddd]` |
|
|
27
|
+
| Quién lo llama | Servicios de aplicación (u otros de dominio) | Adaptadores de entrada (HTTP, mensajes, CLI) |
|
|
28
|
+
|
|
29
|
+
Ninguno es un servicio SOA remoto ni tiene por qué ser pesado [IDDD cap. 7].
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
## Paso 1. ¿Dónde va esta lógica?
|
|
33
|
+
|
|
34
|
+
Árbol de decisión `[Fuera de los libros: síntesis de IDDD cap. 7, cap. 10 y cap. 14]`:
|
|
35
|
+
|
|
36
|
+
1. ¿Es una regla sobre el estado de **un** agregado? → método del agregado.
|
|
37
|
+
2. ¿Es un cálculo o comportamiento sobre unos valores? → método del value object.
|
|
38
|
+
3. ¿Es un proceso de negocio relevante, una transformación de una composición a otra, o un cálculo que necesita **varios** objetos de dominio y queda forzado en cualquiera de ellos? → **servicio de dominio** [IDDD cap. 7].
|
|
39
|
+
4. ¿Es coordinación: cargar, decidir qué invocar, guardar, publicar, autorizar? → **servicio de aplicación** [IDDD cap. 14].
|
|
40
|
+
5. ¿Es formato, protocolo o traducción de otro sistema? → adaptador o ACL.
|
|
41
|
+
|
|
42
|
+
Si la "lógica" es una regla con nombre que decide si algo cumple un criterio (cliente moroso, pedido apto para envío urgente), suele ser una **Specification**, no un servicio (ver `ddd-skills/ddd-value-objects-entidades/GUIA.md`).
|
|
43
|
+
|
|
44
|
+
Antes de crear un servicio de dominio, asegúrate de que lo necesitas. Usarlos por defecto lleva al modelo anémico [IDDD cap. 7]. IDDD admite una "mini capa" de servicios de dominio en algunos contextos, pero avisa de que es un camino peligroso [IDDD cap. 7].
|
|
45
|
+
|
|
46
|
+
## Paso 2. Servicios de dominio
|
|
47
|
+
|
|
48
|
+
* Interfaz con el lenguaje del modelo; el nombre de la operación forma parte del lenguaje ubicuo; sin estado [IDDD cap. 7, citando a Evans].
|
|
49
|
+
* Reciben y devuelven objetos de dominio, no DTOs ni primitivos.
|
|
50
|
+
* Algunos tienen implementación técnica en infraestructura, sobre todo los de integración, con interfaz en el dominio (ver la ACL en `ddd-skills/ddd-context-mapping/GUIA.md`) [IDDD cap. 7].
|
|
51
|
+
* Pueden leer agregados por repositorio para cálculos o validaciones de composición [IDDD cap. 5 y cap. 7], pero **no modifican varios agregados** en una misma operación. Con el paquete, el servicio lee con `AggregateFinder` (el contrato del dominio) o el caso de uso le pasa los agregados ya cargados; nunca recibe `AggregateSaver` ni `AggregateRemover`.
|
|
52
|
+
* El paquete no trae clase base: un servicio de dominio es una clase normal; una regla con nombre que decide si algo cumple un criterio es una `Specification<T>` de `@archi-code/ddd/domain`.
|
|
53
|
+
|
|
54
|
+
Ejemplos e implementación en `ddd-skills/ddd-servicios-capa-aplicacion/references/servicios-dominio-ts.md`.
|
|
55
|
+
|
|
56
|
+
## Paso 3. Servicios de aplicación
|
|
57
|
+
|
|
58
|
+
* Clientes directos del modelo; un método por flujo de caso de uso; según IDDD controlan transacciones y seguridad [IDDD cap. 14]. Con el paquete la seguridad sigue en el caso de uso, pero la transacción pasa al repositorio `[Fuera de los libros: convención de @archi-code/ddd]`.
|
|
59
|
+
* **Finos.** Si crecen, la lógica de dominio se escapa y el modelo se vuelve anémico [IDDD cap. 4 y cap. 14; PHP: Application].
|
|
60
|
+
* Forma típica: recibir parámetros, obtener el agregado del repositorio, ejecutar un comando sobre él, guardar [IDDD cap. 4].
|
|
61
|
+
* Entrada con primitivos, DTOs o, mejor, objetos comando [PHP: Application, citando a Vernon]. Con el paquete, un `Input` plano de primitivos y un `Command` de value objects que construye `toCommand` `[Fuera de los libros: convención de @archi-code/ddd]`.
|
|
62
|
+
* Salida: identificador, DTO de lectura, o nada (con un puerto de salida), según lo que necesite el cliente [IDDD cap. 14]. Es el tipo `Output` de `UseCase`; nunca el agregado.
|
|
63
|
+
* Transacciones: IDDD las pone en el servicio de aplicación [IDDD cap. 14]. Con el paquete la aplicación nunca abre, une ni cierra transacciones y no hay puerto para ello: el adaptador del repositorio es responsable de todo lo de persistencia (que cada `save`/`remove` sea atómico, la comprobación de versión con `ConcurrencyException`, la conexión o sesión, envolver los errores del driver en `InternalErrorException` con `{ cause }`). "Una transacción por agregado" se traduce en "una escritura atómica por agregado, garantizada por el repositorio" `[Fuera de los libros: convención de @archi-code/ddd]`.
|
|
64
|
+
|
|
65
|
+
Plantilla, variantes y errores frecuentes en `ddd-skills/ddd-servicios-capa-aplicacion/references/capa-aplicacion-ts.md`.
|
|
66
|
+
|
|
67
|
+
## Paso 4. Seguridad
|
|
68
|
+
|
|
69
|
+
* La autorización del caso de uso ("¿puede este usuario cancelar pedidos?") es responsabilidad de la aplicación [IDDD cap. 14]: el caso de uso consulta un puerto de aplicación propio (`SalesPermissions`) y lanza `NotAllowedException` (403).
|
|
70
|
+
* Si la regla es de negocio ("solo el cliente que hizo el pedido puede cancelarlo"), pertenece al modelo y se expresa con conceptos del contexto (`Customer`, no `User`) [IDDD cap. 2].
|
|
71
|
+
* La identidad del solicitante viene del token en el adaptador, nunca del cuerpo de la petición; el adaptador la pone en el `Input` y `toCommand` la convierte en value object.
|
|
72
|
+
* La autenticación (quién es el solicitante) la resuelve la plataforma o el adaptador de entrada según las normas del proyecto; esta guía solo cubre dónde encaja la autorización en la arquitectura.
|
|
73
|
+
|
|
74
|
+
## Paso 5. Probar
|
|
75
|
+
|
|
76
|
+
* Servicios de dominio: tests unitarios con objetos de dominio reales [IDDD cap. 7].
|
|
77
|
+
* Servicios de aplicación: con repositorios y adaptadores **en memoria**; comprueban orquestación, no reglas de negocio [IDDD cap. 12; PHP: Application]. El repositorio en memoria lo escribe el proyecto en `test/support/`, uno por agregado (`InMemoryOrderRepository`, como en `ddd-skills/ddd-repositorios-persistencia/references/tests-repositorios.md`); del paquete: `DomainEventBus` sobre `InMemoryEventChannel` (`channel.sent`) y `FixedClock`.
|
|
78
|
+
* Las reglas de negocio se prueban en el agregado o el value object, no a través del caso de uso.
|
|
79
|
+
|
|
80
|
+
## Señales de alarma
|
|
81
|
+
|
|
82
|
+
* `OrderService` con 40 métodos y entidades con solo getters y setters [Destilado cap. 5].
|
|
83
|
+
* `if` sobre el estado de una entidad dentro del servicio de aplicación.
|
|
84
|
+
* Servicios de dominio llamados `XxxManager`, `XxxHelper`, `XxxUtils` [Destilado cap. 5].
|
|
85
|
+
* Un servicio de dominio que abre transacciones o comprueba permisos de usuario.
|
|
86
|
+
* Un caso de uso que abre o maneja transacciones, o recibe un runner de transacciones o una unidad de trabajo: eso va dentro del adaptador del repositorio.
|
|
87
|
+
* Un caso de uso que guarda dos agregados.
|
|
88
|
+
* El caso de uso devuelve el agregado al controlador.
|
|
89
|
+
* Un caso de uso que importa o llama a otro caso de uso.
|
|
90
|
+
* Tests de reglas de negocio escritos contra el caso de uso con mocks de todo.
|
|
91
|
+
|
|
92
|
+
## Ejemplo
|
|
93
|
+
|
|
94
|
+
`ddd-skills/ddd-servicios-capa-aplicacion/references/ejemplo-ecommerce.md`: lo que Ventas añade al `Order` de `ddd-skills/ddd-agregados/references/implementacion-ts.md` (`OrderAbove`, `Customer`, `OrderCancellation`), caso de uso `PlaceOrder`, listener `RegisterReservationOnStockReserved` con su caso de uso `RegisterReservation` y su test. El servicio de dominio `ShippingCostCalculator` está en `ddd-skills/ddd-servicios-capa-aplicacion/references/servicios-dominio-ts.md`, y `CancelOrder` y `AddOrderLine` en `ddd-skills/ddd-servicios-capa-aplicacion/references/capa-aplicacion-ts.md`.
|
|
95
|
+
|
|
96
|
+
## Siguiente guía
|
|
97
|
+
|
|
98
|
+
* Si las lecturas se complican o hay mucho volumen → `ddd-skills/ddd-event-sourcing-cqrs/GUIA.md`.
|
|
99
|
+
* Para revisar un proyecto existente con todo esto → `ddd-skills/ddd-auditoria-refactorizacion/GUIA.md`.
|
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
# Capa de aplicación en TypeScript
|
|
2
|
+
|
|
3
|
+
## Plantilla de caso de uso
|
|
4
|
+
|
|
5
|
+
Un caso de uso extiende `UseCase<Input, Command, Output>` de `@archi-code/ddd/application`. `execute(input)` llama a `toCommand(input)` y después a `handle(command)`; no hay que escribir `execute`.
|
|
6
|
+
|
|
7
|
+
```ts
|
|
8
|
+
import { ValueObjects } from '@archi-code/ddd/domain';
|
|
9
|
+
import type { AggregateFinder, AggregateSaver } from '@archi-code/ddd/domain';
|
|
10
|
+
import { UseCase } from '@archi-code/ddd/application';
|
|
11
|
+
import type { Clock, EventBus } from '@archi-code/ddd/application';
|
|
12
|
+
import { AggregateNotFoundException, NotAllowedException } from '@archi-code/ddd/exceptions';
|
|
13
|
+
|
|
14
|
+
export interface SalesPermissions {
|
|
15
|
+
canCancelOrders(requester: CustomerId): Promise<boolean>;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface CancelOrderInput {
|
|
19
|
+
orderId: string;
|
|
20
|
+
requesterId: string;
|
|
21
|
+
reason: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
interface CancelOrderCommand {
|
|
25
|
+
orderId: OrderId;
|
|
26
|
+
requester: CustomerId;
|
|
27
|
+
reason: CancellationReason;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export class CancelOrder extends UseCase<CancelOrderInput, CancelOrderCommand, void> {
|
|
31
|
+
private readonly cancellation = new OrderCancellation();
|
|
32
|
+
|
|
33
|
+
constructor(
|
|
34
|
+
private readonly orders: AggregateFinder<Order> & AggregateSaver<Order>,
|
|
35
|
+
private readonly events: EventBus<OrderEvent>,
|
|
36
|
+
private readonly permissions: SalesPermissions,
|
|
37
|
+
private readonly clock: Clock,
|
|
38
|
+
) {
|
|
39
|
+
super();
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
protected toCommand(input: CancelOrderInput): CancelOrderCommand {
|
|
43
|
+
return ValueObjects.build({
|
|
44
|
+
orderId: () => new OrderId(input.orderId),
|
|
45
|
+
requester: () => new CustomerId(input.requesterId),
|
|
46
|
+
reason: () => new CancellationReason(input.reason),
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
protected async handle(command: CancelOrderCommand): Promise<void> {
|
|
51
|
+
if (!(await this.permissions.canCancelOrders(command.requester))) {
|
|
52
|
+
throw new NotAllowedException(`Customer ${command.requester.value} may not cancel orders`);
|
|
53
|
+
}
|
|
54
|
+
const order = await this.orders.findById(command.orderId);
|
|
55
|
+
if (order === null) {
|
|
56
|
+
throw new AggregateNotFoundException('Order', command.orderId.value);
|
|
57
|
+
}
|
|
58
|
+
this.cancellation.cancel(order, command.requester, command.reason, this.clock);
|
|
59
|
+
await this.orders.save(order);
|
|
60
|
+
await this.events.publish(order.pullDomainEvents());
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Autorizar, cargar, invocar el dominio, guardar y publicar, y ninguna regla de negocio: que solo el cliente del pedido pueda cancelarlo lo comprueba el servicio de dominio `OrderCancellation`, y que no esté ya cancelado, `order.cancel`. Coincide con la forma de IDDD: parámetros, repositorio, comando sobre el agregado [IDDD cap. 4] y con la anatomía de *DDD in PHP* [PHP: Application].
|
|
66
|
+
|
|
67
|
+
* El caso de uso pide solo los roles de repositorio que usa (`AggregateFinder<Order> & AggregateSaver<Order>`), los puertos `EventBus` y `Clock` del paquete, y los puertos de aplicación propios (`SalesPermissions`). Nunca una dependencia de transacciones (un runner de transacciones o una unidad de trabajo): no existe puerto de transacciones `[Fuera de los libros: convención de @archi-code/ddd]`.
|
|
68
|
+
* `UseCase` no trae ayudas (`findOrFail`, `saveAndPublish`): todos los pasos se escriben en `handle` `[Fuera de los libros: convención de @archi-code/ddd]`.
|
|
69
|
+
* Se guarda antes de publicar, y se publica lo que devuelve `pullDomainEvents()`.
|
|
70
|
+
* La aplicación nunca abre, une ni cierra transacciones. IDDD pone el control de la transacción en el servicio de aplicación [IDDD cap. 14]; con el paquete esa responsabilidad pasa al adaptador del repositorio, que hace atómico cada `save` y `remove` (una escritura de documento en Mongo; si el agregado ocupa varias tablas SQL, abre y confirma o deshace una transacción de la base de datos dentro de `save`/`remove`, con su propia conexión o sesión), comprueba la versión y envuelve los errores del driver en `InternalErrorException` con `{ cause }` `[Fuera de los libros: convención de @archi-code/ddd]`. Ver `ddd-skills/ddd-repositorios-persistencia/references/transacciones-ts.md`.
|
|
71
|
+
|
|
72
|
+
## Entrada
|
|
73
|
+
|
|
74
|
+
* Primitivos o DTO o comando [PHP: Application, citando a Vernon]. Con el paquete, dos tipos por caso de uso `[Fuera de los libros: convención de @archi-code/ddd]`: el `Input`, plano y de primitivos (se puede serializar y encolar si un día hace falta [PHP: Application]), y el `Command`, de value objects, que construye `toCommand`.
|
|
75
|
+
* La conversión a value objects ocurre en `toCommand` con `ValueObjects.build`: si algún campo no es válido lanza una sola `ValidationException` con el `field` y el `code` de cada problema (`ValueObjects.buildList` nombra los elementos de una lista por posición, `lines.1.quantity`), y `handle` no llega a ejecutarse.
|
|
76
|
+
* El adaptador de entrada solo comprueba la forma de la petición (que un campo sea texto); las reglas de cada valor son de los value objects.
|
|
77
|
+
* Nunca recibas el agregado desde el controlador.
|
|
78
|
+
|
|
79
|
+
## Salida
|
|
80
|
+
|
|
81
|
+
IDDD presenta varias opciones [IDDD cap. 14]:
|
|
82
|
+
|
|
83
|
+
| Opción | Cuándo |
|
|
84
|
+
|---|---|
|
|
85
|
+
| Devolver un identificador o nada | Comandos (lo normal) |
|
|
86
|
+
| DTO construido desde el agregado | El cliente necesita el resultado inmediato |
|
|
87
|
+
| Transformador de datos que elige el cliente | Varios clientes con formatos distintos |
|
|
88
|
+
| Método sin retorno y puerto de salida | Estilo hexagonal estricto |
|
|
89
|
+
|
|
90
|
+
Con el paquete, la salida es el tercer parámetro de tipo: `UseCase<PlaceOrderInput, PlaceOrderCommand, string>` devuelve el id (`order.id.value`), `void` nada. Nunca el agregado.
|
|
91
|
+
|
|
92
|
+
Para pantallas, mejor una consulta de lectura aparte que devuelve un DTO sin pasar por el agregado: una interfaz de aplicación implementada en infraestructura directamente sobre la base de datos (ver `ddd-skills/ddd-repositorios-persistencia/GUIA.md` y `ddd-skills/ddd-event-sourcing-cqrs/GUIA.md`).
|
|
93
|
+
|
|
94
|
+
## Command bus
|
|
95
|
+
|
|
96
|
+
*DDD in PHP* presenta el bus de comandos (Tactician) para enlazar cada comando con su manejador; los objetos request se parecen ya a comandos [PHP: Application]. El paquete no trae bus de comandos `[Fuera de los libros: convención de @archi-code/ddd]`: el composition root construye cada caso de uso y se lo pasa al punto de entrada, que llama a `execute(input)`. Lo común a todos los casos de uso ya está en `UseCase.execute` (dejar pasar las excepciones del paquete y envolver cualquier otro error en `InternalErrorException` con `cause`), y la atomicidad de cada escritura es del repositorio.
|
|
97
|
+
|
|
98
|
+
## Eventos en la capa de aplicación
|
|
99
|
+
|
|
100
|
+
* La aplicación puede registrar suscriptores para guardar y reenviar eventos, dejando el dominio libre de mensajería [IDDD cap. 4]. Con el paquete, los suscriptores son listeners (`EventListener` con `@ListensTo`) que el composition root conecta con `bus.subscribe(listener)`; cada uno ejecuta un solo caso de uso.
|
|
101
|
+
* Con outbox, los eventos se guardan en la transacción y la publicación ocurre fuera (`ddd-skills/ddd-eventos-dominio/GUIA.md`). Con el paquete, el caso de uso siempre llama a `events.publish(order.pullDomainEvents())` después de `save`; si `publish` falla después de `save`, el evento puede perderse. Cuando un evento no puede perderse, el repositorio escribe el agregado y sus eventos pendientes en la misma escritura atómica (un outbox dentro del mismo documento o en la misma transacción de base de datos de `save`) y un relay los entrega después por un `EventChannel`; los casos de uso no cambian `[Fuera de los libros: convención de @archi-code/ddd]`.
|
|
102
|
+
|
|
103
|
+
## Conflictos de concurrencia
|
|
104
|
+
|
|
105
|
+
Cada repositorio comprueba la versión del agregado y lanza `ConcurrencyException` (409) si otro lo cambió después de cargarlo. Si el comando es seguro de repetir sobre el nuevo estado, el caso de uso puede reintentarlo [IDDD cap. 10; IDDD apéndice A]: un bucle en `handle` en el que cada intento vuelve a cargar el agregado y vuelve a guardarlo; pocos intentos y después se relanza. Un caso de uso que ejecuta un listener no reintenta por sí mismo: reintenta la reentrega del canal.
|
|
106
|
+
|
|
107
|
+
```ts
|
|
108
|
+
import { ConcurrencyException, DomainException } from '@archi-code/ddd/exceptions';
|
|
109
|
+
|
|
110
|
+
export interface ProductPricing {
|
|
111
|
+
priceForSale(sku: Sku): Promise<Money | null>;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export class ProductNotSellable extends DomainException {
|
|
115
|
+
constructor(sku: string) {
|
|
116
|
+
super(`Product ${sku} is not for sale`, ['ORD003']);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const ATTEMPTS = 3;
|
|
121
|
+
|
|
122
|
+
export class AddOrderLine extends UseCase<AddOrderLineInput, AddOrderLineCommand, void> {
|
|
123
|
+
constructor(
|
|
124
|
+
private readonly orders: AggregateFinder<Order> & AggregateSaver<Order>,
|
|
125
|
+
private readonly pricing: ProductPricing,
|
|
126
|
+
private readonly events: EventBus<OrderEvent>,
|
|
127
|
+
private readonly clock: Clock,
|
|
128
|
+
) {
|
|
129
|
+
super();
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
protected toCommand(input: AddOrderLineInput): AddOrderLineCommand {
|
|
133
|
+
return ValueObjects.build({
|
|
134
|
+
orderId: () => new OrderId(input.orderId),
|
|
135
|
+
sku: () => new Sku(input.sku),
|
|
136
|
+
quantity: () => new Quantity(input.quantity),
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
protected async handle({ orderId, sku, quantity }: AddOrderLineCommand): Promise<void> {
|
|
141
|
+
const unitPrice = await this.pricing.priceForSale(sku);
|
|
142
|
+
if (unitPrice === null) {
|
|
143
|
+
throw new ProductNotSellable(sku.value);
|
|
144
|
+
}
|
|
145
|
+
const line = new OrderLine({ sku, quantity, unitPrice });
|
|
146
|
+
for (let attempt = 1; ; attempt += 1) {
|
|
147
|
+
try {
|
|
148
|
+
const order = await this.orders.findById(orderId);
|
|
149
|
+
if (order === null) {
|
|
150
|
+
throw new AggregateNotFoundException('Order', orderId.value);
|
|
151
|
+
}
|
|
152
|
+
order.addLine(line, this.clock);
|
|
153
|
+
await this.orders.save(order);
|
|
154
|
+
await this.events.publish(order.pullDomainEvents());
|
|
155
|
+
return;
|
|
156
|
+
} catch (error) {
|
|
157
|
+
if (!(error instanceof ConcurrencyException) || attempt === ATTEMPTS) {
|
|
158
|
+
throw error;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
`ProductPricing` es un puerto de dominio (la interfaz en `domain/` con value objects, implementada en infraestructura por la capa anticorrupción) y `ProductNotSellable` una regla de dominio. `AddOrderLineInput` (`orderId`, `sku`, `quantity` como primitivos) y `AddOrderLineCommand` (`OrderId`, `Sku`, `Quantity`) siguen el patrón de `CancelOrder`. Nunca se reintenta con el agregado ya cargado, y un caso de uso nunca se llama a sí mismo ni a otro para reintentar.
|
|
167
|
+
|
|
168
|
+
## Errores
|
|
169
|
+
|
|
170
|
+
* El dominio lanza `DomainException` con nombre y código propio (`class OrderCannotBeCancelled extends DomainException`, `super(message, ['ORD002'])`); los value objects y `ValueObjects.build` lanzan `ValidationException`.
|
|
171
|
+
* La aplicación añade las suyas: `AggregateNotFoundException('Order', id)` cuando `findById` devuelve `null`, `NotAllowedException(message)` cuando el solicitante no puede ejecutar la acción, `ApplicationException` para otros errores de aplicación. Al traducir otro error, siempre con `{ cause: error }`.
|
|
172
|
+
* Los errores se dejan propagar: no se capturan en `handle` para relanzarlos; `execute` deja pasar las excepciones del paquete y envuelve el resto en `InternalErrorException`.
|
|
173
|
+
* El punto de entrada los traduce con `HttpErrorMapper` de `@archi-code/ddd/infrastructure`: 404 `AggregateNotFoundException`, 403 `NotAllowedException`, 409 `ConcurrencyException`, 422 `ValidationException` y cualquier otra `DomainException` (regla de negocio), 400 otros errores de aplicación y 500, con mensaje genérico, para lo demás. Los códigos propios se ajustan por código (`new HttpErrorMapper({ statusByCode: { ORD001: 409 } })`) `[Fuera de los libros: convención de @archi-code/ddd]`.
|
|
174
|
+
|
|
175
|
+
```ts
|
|
176
|
+
import type { HttpErrorMapper } from '@archi-code/ddd/infrastructure';
|
|
177
|
+
|
|
178
|
+
interface HttpRequest {
|
|
179
|
+
params: { orderId: string };
|
|
180
|
+
auth: { customerId: string };
|
|
181
|
+
body: { reason?: unknown };
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
interface HttpResponse {
|
|
185
|
+
status: number;
|
|
186
|
+
body?: unknown;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
export class CancelOrderController {
|
|
190
|
+
constructor(
|
|
191
|
+
private readonly cancelOrder: CancelOrder,
|
|
192
|
+
private readonly errors: HttpErrorMapper,
|
|
193
|
+
) {}
|
|
194
|
+
|
|
195
|
+
async cancel(request: HttpRequest): Promise<HttpResponse> {
|
|
196
|
+
try {
|
|
197
|
+
await this.cancelOrder.execute({
|
|
198
|
+
orderId: request.params.orderId,
|
|
199
|
+
requesterId: request.auth.customerId,
|
|
200
|
+
reason: typeof request.body.reason === 'string' ? request.body.reason : '',
|
|
201
|
+
});
|
|
202
|
+
return { status: 204 };
|
|
203
|
+
} catch (error) {
|
|
204
|
+
return this.errors.map(error);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
La identidad del solicitante sale del token (`request.auth`), nunca del cuerpo.
|
|
211
|
+
|
|
212
|
+
## Tests
|
|
213
|
+
|
|
214
|
+
Con adaptadores en memoria: `InMemoryOrderRepository`, que el proyecto escribe en `test/support/` (uno por agregado; cada `save` y `remove` es atómico y comprueba la versión; cómo se escribe, en `ddd-skills/ddd-repositorios-persistencia/references/tests-repositorios.md`), y del paquete `DomainEventBus` sobre `InMemoryEventChannel` (`channel.sent` lista lo publicado). Los puertos propios se sustituyen por objetos simples.
|
|
215
|
+
|
|
216
|
+
```ts
|
|
217
|
+
import { describe, expect, it } from 'vitest';
|
|
218
|
+
import { NotAllowedException } from '@archi-code/ddd/exceptions';
|
|
219
|
+
import {
|
|
220
|
+
DomainEventBus,
|
|
221
|
+
EventRegistry,
|
|
222
|
+
FixedClock,
|
|
223
|
+
InMemoryEventChannel,
|
|
224
|
+
} from '@archi-code/ddd/infrastructure';
|
|
225
|
+
import { InMemoryOrderRepository } from '../support/in-memory-order-repository';
|
|
226
|
+
|
|
227
|
+
const clock = new FixedClock('2026-03-01T09:30:00Z');
|
|
228
|
+
|
|
229
|
+
function salesWith(permissions: SalesPermissions) {
|
|
230
|
+
const orders = new InMemoryOrderRepository();
|
|
231
|
+
const channel = new InMemoryEventChannel();
|
|
232
|
+
const events = new DomainEventBus<OrderEvent>(channel, new EventRegistry<OrderEvent>());
|
|
233
|
+
const cancelOrder = new CancelOrder(orders, events, permissions, clock);
|
|
234
|
+
return { orders, channel, cancelOrder };
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const everyoneMayCancel: SalesPermissions = { canCancelOrders: async () => true };
|
|
238
|
+
const nobodyMayCancel: SalesPermissions = { canCancelOrders: async () => false };
|
|
239
|
+
|
|
240
|
+
describe('Cancelling an order', () => {
|
|
241
|
+
it('a customer cancels her own pending order', async () => {
|
|
242
|
+
const { orders, channel, cancelOrder } = salesWith(everyoneMayCancel);
|
|
243
|
+
await orders.save(aPendingOrderOf(laura));
|
|
244
|
+
|
|
245
|
+
await cancelOrder.execute({ orderId, requesterId: laura, reason: 'Wrong size' });
|
|
246
|
+
|
|
247
|
+
expect(channel.sent).toMatchObject([
|
|
248
|
+
{ eventName: 'OrderCancelled', aggregateId: orderId, reason: 'Wrong size' },
|
|
249
|
+
]);
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
it('a customer whose account may not cancel orders is refused', async () => {
|
|
253
|
+
const { orders, channel, cancelOrder } = salesWith(nobodyMayCancel);
|
|
254
|
+
await orders.save(aPendingOrderOf(laura));
|
|
255
|
+
|
|
256
|
+
await expect(
|
|
257
|
+
cancelOrder.execute({ orderId, requesterId: laura, reason: 'Wrong size' }),
|
|
258
|
+
).rejects.toThrow(NotAllowedException);
|
|
259
|
+
expect(channel.sent).toEqual([]);
|
|
260
|
+
});
|
|
261
|
+
});
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
`aPendingOrderOf`, `laura` y `orderId` son fábricas y datos de `test/support/` del proyecto; el modelo (`Order`, `CancellationReason`, `OrderCancelled`…) es el de `ddd-skills/ddd-agregados/references/implementacion-ts.md` y `OrderCancellation` el de `ddd-skills/ddd-servicios-capa-aplicacion/references/ejemplo-ecommerce.md`. Estos tests comprueban orquestación. Que un pedido ya cancelado no se pueda cancelar otra vez se prueba en `Order`, y que otro cliente no pueda cancelarlo, en `OrderCancellation`; no aquí.
|