@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,285 @@
|
|
|
1
|
+
# Proyecciones y CQRS en TypeScript
|
|
2
|
+
|
|
3
|
+
El paquete da las piezas de la entrega (`EventListener`, `@ListensTo`, `DomainEventBus`, `EventRegistry`, `EventChannel`); el modelo de lectura, su escritura idempotente, la reconstrucción y las consultas son código del proyecto. Los ejemplos usan los eventos `OrderPlaced` (`customerId`, `lines`, `total`) y `OrderLineAdded` (`sku`, `quantity`, `unitPrice`, `total`) del pedido de `ddd-skills/ddd-agregados/references/implementacion-ts.md`, subclases de `EventBase` como en `ddd-skills/ddd-eventos-dominio/GUIA.md`.
|
|
4
|
+
|
|
5
|
+
## Modelo de lectura
|
|
6
|
+
|
|
7
|
+
Pensado para la pantalla: desnormalizado, una tabla o documento por vista, tantas vistas como hagan falta (sin caer en una por usuario: IDDD recomienda filtros cuando el número de vistas sería impracticable) [IDDD cap. 4].
|
|
8
|
+
|
|
9
|
+
```sql
|
|
10
|
+
CREATE TABLE order_summaries (
|
|
11
|
+
order_id TEXT PRIMARY KEY,
|
|
12
|
+
customer TEXT NOT NULL,
|
|
13
|
+
line_count INT NOT NULL,
|
|
14
|
+
units INT NOT NULL,
|
|
15
|
+
total_cents BIGINT NOT NULL,
|
|
16
|
+
currency TEXT NOT NULL,
|
|
17
|
+
updated_at TIMESTAMPTZ NOT NULL
|
|
18
|
+
);
|
|
19
|
+
CREATE TABLE projection_inbox (
|
|
20
|
+
projection TEXT NOT NULL,
|
|
21
|
+
event_id UUID NOT NULL,
|
|
22
|
+
PRIMARY KEY (projection, event_id)
|
|
23
|
+
);
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
`updated_at` (el `occurredOn` del último evento aplicado) permite mostrar la antigüedad del dato [IDDD cap. 4]. `projection_inbox` guarda el `eventId` de cada evento ya aplicado: la entrega es al menos una vez, así que la proyección es idempotente por `eventId`.
|
|
27
|
+
|
|
28
|
+
## Proyección idempotente
|
|
29
|
+
|
|
30
|
+
`[Fuera de los libros: convención de @archi-code/ddd]` Una proyección se suscribe al bus como cualquier listener: un `EventListener` por evento, marcado con `@ListensTo(Event, { consumer })`, con nombre `<Action>On<Event>`. Como no cambia ningún agregado, no ejecuta un caso de uso: llama a un puerto de la aplicación (`OrderSummaryProjection`) que la infraestructura implementa sobre la tabla de lectura.
|
|
31
|
+
|
|
32
|
+
```ts
|
|
33
|
+
import { EventListener, ListensTo } from '@archi-code/ddd/application';
|
|
34
|
+
|
|
35
|
+
export interface OrderSummaryProjection {
|
|
36
|
+
orderPlaced(event: OrderPlaced): Promise<void>;
|
|
37
|
+
orderLineAdded(event: OrderLineAdded): Promise<void>;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
@ListensTo(OrderPlaced, { consumer: 'order-summaries.v1.order-placed' })
|
|
41
|
+
export class ProjectOrderSummaryOnOrderPlaced extends EventListener<OrderPlaced> {
|
|
42
|
+
constructor(private readonly summaries: OrderSummaryProjection) {
|
|
43
|
+
super();
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
handle(event: OrderPlaced): Promise<void> {
|
|
47
|
+
return this.summaries.orderPlaced(event);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
@ListensTo(OrderLineAdded, { consumer: 'order-summaries.v1.order-line-added' })
|
|
52
|
+
export class ProjectOrderSummaryOnOrderLineAdded extends EventListener<OrderLineAdded> {
|
|
53
|
+
constructor(private readonly summaries: OrderSummaryProjection) {
|
|
54
|
+
super();
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
handle(event: OrderLineAdded): Promise<void> {
|
|
58
|
+
return this.summaries.orderLineAdded(event);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
La implementación registra el `eventId` en `projection_inbox` y aplica el cambio en la misma transacción; si el `eventId` ya estaba, no hace nada `[Fuera de los libros]`:
|
|
64
|
+
|
|
65
|
+
```ts
|
|
66
|
+
import type { EventBase } from '@archi-code/ddd/domain';
|
|
67
|
+
import { InfrastructureException } from '@archi-code/ddd/exceptions';
|
|
68
|
+
|
|
69
|
+
interface Sql {
|
|
70
|
+
query<Row>(text: string, values: readonly unknown[]): Promise<{ rows: Row[]; rowCount: number }>;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
interface SqlDatabase {
|
|
74
|
+
transaction<Result>(work: (sql: Sql) => Promise<Result>): Promise<Result>;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export class PgOrderSummaryProjection implements OrderSummaryProjection {
|
|
78
|
+
constructor(
|
|
79
|
+
private readonly database: SqlDatabase,
|
|
80
|
+
private readonly table = 'order_summaries',
|
|
81
|
+
) {}
|
|
82
|
+
|
|
83
|
+
orderPlaced(event: OrderPlaced): Promise<void> {
|
|
84
|
+
return this.once(event, async (sql) => {
|
|
85
|
+
await sql.query(
|
|
86
|
+
`INSERT INTO ${this.table}
|
|
87
|
+
(order_id, customer, line_count, units, total_cents, currency, updated_at)
|
|
88
|
+
VALUES ($1, $2, $3, $4, $5, $6, $7) ON CONFLICT (order_id) DO NOTHING`,
|
|
89
|
+
[
|
|
90
|
+
event.aggregateId,
|
|
91
|
+
event.customerId,
|
|
92
|
+
event.lines.length,
|
|
93
|
+
event.lines.reduce((units, line) => units + line.quantity, 0),
|
|
94
|
+
event.total.amount,
|
|
95
|
+
event.total.currency,
|
|
96
|
+
event.occurredOn,
|
|
97
|
+
],
|
|
98
|
+
);
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
orderLineAdded(event: OrderLineAdded): Promise<void> {
|
|
103
|
+
return this.once(event, async (sql) => {
|
|
104
|
+
const { rowCount } = await sql.query(
|
|
105
|
+
`UPDATE ${this.table}
|
|
106
|
+
SET line_count = line_count + 1, units = units + $2, total_cents = total_cents + $3,
|
|
107
|
+
updated_at = GREATEST(updated_at, $4)
|
|
108
|
+
WHERE order_id = $1`,
|
|
109
|
+
[
|
|
110
|
+
event.aggregateId,
|
|
111
|
+
event.quantity,
|
|
112
|
+
event.quantity * event.unitPrice.amount,
|
|
113
|
+
event.occurredOn,
|
|
114
|
+
],
|
|
115
|
+
);
|
|
116
|
+
if (rowCount === 0) {
|
|
117
|
+
throw new InfrastructureException(
|
|
118
|
+
`${this.table}: order ${event.aggregateId} is not projected yet`,
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
private once(event: EventBase, work: (sql: Sql) => Promise<void>): Promise<void> {
|
|
125
|
+
return this.database.transaction(async (sql) => {
|
|
126
|
+
const { rowCount } = await sql.query(
|
|
127
|
+
'INSERT INTO projection_inbox (projection, event_id) VALUES ($1, $2) ON CONFLICT DO NOTHING',
|
|
128
|
+
[this.table, event.eventId],
|
|
129
|
+
);
|
|
130
|
+
if (rowCount === 1) {
|
|
131
|
+
await work(sql);
|
|
132
|
+
}
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
* Sumar unidades e importes no es idempotente por sí mismo; la bandeja por `eventId` lo hace idempotente. Se suma el subtotal de la línea (`quantity` × `unitPrice`) en vez de copiar el `total` del evento, para que dos `OrderLineAdded` del mismo pedido entregados en otro orden dejen el mismo resultado.
|
|
139
|
+
* Un canal solo garantiza el orden de los eventos de un mismo `send` para un consumidor. Si `OrderLineAdded` llega antes que `OrderPlaced`, la actualización no encuentra fila: lanzar deshace la transacción (también la marca en la bandeja) y el canal lo entrega de nuevo más tarde.
|
|
140
|
+
* El listener recibe siempre el evento reconstruido por el `EventRegistry` desde su JSON, también en memoria: lo que funciona con `InMemoryEventChannel` funciona igual con un broker.
|
|
141
|
+
|
|
142
|
+
En la composición se suscriben como cualquier listener (`channel` es el `EventChannel` del proyecto: `InMemoryEventChannel` en pruebas, tu canal duradero o de broker en producción):
|
|
143
|
+
|
|
144
|
+
```ts
|
|
145
|
+
import { DomainEventBus, EventRegistry } from '@archi-code/ddd/infrastructure';
|
|
146
|
+
|
|
147
|
+
export const orderEvents = new EventRegistry<OrderEvent>()
|
|
148
|
+
.register(
|
|
149
|
+
OrderPlaced,
|
|
150
|
+
(json) =>
|
|
151
|
+
new OrderPlaced(
|
|
152
|
+
json.aggregateId,
|
|
153
|
+
json.customerId as string,
|
|
154
|
+
json.lines as OrderLineData[],
|
|
155
|
+
json.total as MoneyData,
|
|
156
|
+
json,
|
|
157
|
+
),
|
|
158
|
+
)
|
|
159
|
+
.register(
|
|
160
|
+
OrderLineAdded,
|
|
161
|
+
(json) =>
|
|
162
|
+
new OrderLineAdded(
|
|
163
|
+
json.aggregateId,
|
|
164
|
+
json.sku as string,
|
|
165
|
+
json.quantity as number,
|
|
166
|
+
json.unitPrice as MoneyData,
|
|
167
|
+
json.total as MoneyData,
|
|
168
|
+
json,
|
|
169
|
+
),
|
|
170
|
+
);
|
|
171
|
+
|
|
172
|
+
const bus = new DomainEventBus<OrderEvent>(channel, orderEvents);
|
|
173
|
+
const projection = new PgOrderSummaryProjection(database);
|
|
174
|
+
await bus.subscribe(new ProjectOrderSummaryOnOrderPlaced(projection));
|
|
175
|
+
await bus.subscribe(new ProjectOrderSummaryOnOrderLineAdded(projection));
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
## Reconstruir
|
|
179
|
+
|
|
180
|
+
Para cambiar una vista: crear `order_summaries_v2`, proyectar desde la posición 0 en segundo plano, cambiar la consulta cuando alcance el presente y borrar la antigua. Es la ventaja que destaca IDDD: cambiar lo que se ofrece sin modificar el agregado [IDDD apéndice A].
|
|
181
|
+
|
|
182
|
+
`[Fuera de los libros: convención de @archi-code/ddd]` Un `EventChannel` es un camino de entrega, no un historial: reconstruir exige un log de eventos propio, ordenado. Con Event Sourcing es el propio event store (por `global_seq`, ver `ddd-skills/ddd-event-sourcing-cqrs/references/event-sourcing-ts.md`); sin él, una tabla donde tu canal duradero o un listener anexa cada `SerializedEvent` y que nunca se borra. Para reproducirlo con los mismos listeners, súbelos a un `DomainEventBus` sobre un `InMemoryEventChannel` aparte y envía el log por páginas:
|
|
183
|
+
|
|
184
|
+
```ts
|
|
185
|
+
import type { SerializedEvent } from '@archi-code/ddd/domain';
|
|
186
|
+
import type { EventRegistry } from '@archi-code/ddd/infrastructure';
|
|
187
|
+
import { DomainEventBus, InMemoryEventChannel } from '@archi-code/ddd/infrastructure';
|
|
188
|
+
|
|
189
|
+
export interface EventLog {
|
|
190
|
+
read(after: number, limit: number): Promise<{ position: number; event: SerializedEvent }[]>;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
export async function rebuild(
|
|
194
|
+
log: EventLog,
|
|
195
|
+
projection: OrderSummaryProjection,
|
|
196
|
+
registry: EventRegistry<OrderEvent>,
|
|
197
|
+
): Promise<void> {
|
|
198
|
+
const replay = new InMemoryEventChannel();
|
|
199
|
+
const bus = new DomainEventBus<OrderEvent>(replay, registry);
|
|
200
|
+
const subscriptions = [
|
|
201
|
+
await bus.subscribe(new ProjectOrderSummaryOnOrderPlaced(projection)),
|
|
202
|
+
await bus.subscribe(new ProjectOrderSummaryOnOrderLineAdded(projection)),
|
|
203
|
+
];
|
|
204
|
+
for (let after = 0; ; ) {
|
|
205
|
+
const page = await log.read(after, 500);
|
|
206
|
+
if (page.length === 0) {
|
|
207
|
+
break;
|
|
208
|
+
}
|
|
209
|
+
await replay.send(page.map((entry) => entry.event));
|
|
210
|
+
after = page[page.length - 1].position;
|
|
211
|
+
}
|
|
212
|
+
await Promise.all(subscriptions.map((subscription) => subscription.unsubscribe()));
|
|
213
|
+
}
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
`rebuild(log, new PgOrderSummaryProjection(database, 'order_summaries_v2'), orderEvents)` llena la tabla nueva; el canal en memoria entrega en serie y en orden, y si un evento falla, `send` rechaza y la reconstrucción se detiene. Al desplegar la versión 2, cambia también el `consumer` de sus listeners (`order-summaries.v2.order-placed`): es su identidad en el canal (su cola en un broker), y la bandeja por `eventId` absorbe los eventos que lleguen por las dos vías mientras la reconstrucción alcanza el presente.
|
|
217
|
+
|
|
218
|
+
## Lado de consulta
|
|
219
|
+
|
|
220
|
+
Una consulta de lectura es una interfaz de la aplicación que devuelve DTOs, implementada en infraestructura directamente sobre la base de datos:
|
|
221
|
+
|
|
222
|
+
```ts
|
|
223
|
+
import { InternalErrorException } from '@archi-code/ddd/exceptions';
|
|
224
|
+
|
|
225
|
+
export interface OrderSummary {
|
|
226
|
+
readonly id: string;
|
|
227
|
+
readonly customer: string;
|
|
228
|
+
readonly lines: number;
|
|
229
|
+
readonly units: number;
|
|
230
|
+
readonly total: { readonly amount: number; readonly currency: string };
|
|
231
|
+
readonly updatedAt: string;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
export interface OrderSummaries {
|
|
235
|
+
ofCustomer(customer: string, page: { limit: number; offset: number }): Promise<OrderSummary[]>;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
interface SummaryRow {
|
|
239
|
+
order_id: string;
|
|
240
|
+
customer: string;
|
|
241
|
+
line_count: number;
|
|
242
|
+
units: number;
|
|
243
|
+
total_cents: string;
|
|
244
|
+
currency: string;
|
|
245
|
+
updated_at: Date;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
export class PgOrderSummaries implements OrderSummaries {
|
|
249
|
+
constructor(private readonly sql: Sql) {}
|
|
250
|
+
|
|
251
|
+
async ofCustomer(
|
|
252
|
+
customer: string,
|
|
253
|
+
page: { limit: number; offset: number },
|
|
254
|
+
): Promise<OrderSummary[]> {
|
|
255
|
+
try {
|
|
256
|
+
const { rows } = await this.sql.query<SummaryRow>(
|
|
257
|
+
`SELECT order_id, customer, line_count, units, total_cents, currency, updated_at
|
|
258
|
+
FROM order_summaries
|
|
259
|
+
WHERE customer = $1 ORDER BY updated_at DESC, order_id LIMIT $2 OFFSET $3`,
|
|
260
|
+
[customer, page.limit, page.offset],
|
|
261
|
+
);
|
|
262
|
+
return rows.map((row) => ({
|
|
263
|
+
id: row.order_id,
|
|
264
|
+
customer: row.customer,
|
|
265
|
+
lines: row.line_count,
|
|
266
|
+
units: row.units,
|
|
267
|
+
total: { amount: Number(row.total_cents), currency: row.currency },
|
|
268
|
+
updatedAt: row.updated_at.toISOString(),
|
|
269
|
+
}));
|
|
270
|
+
} catch (error) {
|
|
271
|
+
throw new InternalErrorException('reading order summaries', { cause: error });
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
Las consultas no pasan por agregados ni repositorios (`AggregateFinder`). Los comandos sí. Si la pantalla filtra y ordena por campos que elige el usuario, describe la búsqueda con `@archi-code/criteria` (`Criteria<OrderSummary>`, con `defineCriteriaSchema` para la entrada de fuera) en vez de añadir un método por combinación.
|
|
278
|
+
|
|
279
|
+
## CQRS sin Event Sourcing
|
|
280
|
+
|
|
281
|
+
Con persistencia de estado normal (repositorios que guardan `toPrimitives()` de cada agregado), las proyecciones se alimentan de los mismos eventos que los casos de uso publican con `EventBus.publish(aggregate.pullDomainEvents())` (`ddd-skills/ddd-eventos-dominio/GUIA.md`). Mismo patrón: idempotencia por `eventId` y reconstrucción desde un log de eventos guardados. `[Fuera de los libros: convención de @archi-code/ddd]` El canal no conserva historial legible: si la vista debe poder reconstruirse desde cero, guarda cada evento en un log propio desde el primer día.
|
|
282
|
+
|
|
283
|
+
## Proyecciones para otros agregados
|
|
284
|
+
|
|
285
|
+
IDDD muestra una proyección (`CustomerBillingView`) que un agregado `Invoice` consulta a través de un servicio de dominio para obtener datos de cliente sin acoplarse al agregado `Customer` [IDDD apéndice A]. Con el paquete, el servicio de dominio declara un puerto en `domain/` con value objects, la infraestructura lo implementa sobre la tabla de la proyección y el caso de uso pasa los datos ya cargados al método del agregado. Úsalo para datos de referencia, nunca para proteger invariantes que exijan consistencia inmediata.
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
# Eventos de dominio
|
|
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.** Eventos de dominio en DDD con TypeScript. Cubre cómo nombrarlos en pasado con el lenguaje ubicuo, decidir qué datos llevan (ni el objeto entero ni eventos "XxxUpdated"), eventos causados por el paso del tiempo, diferencia entre comando y evento, registrarlos en el agregado, guardarlos en la misma escritura atómica que el agregado (event store u outbox) y publicarlos después, consumidores idempotentes, orden causal, versionado y eventos internos frente a eventos publicados a otros contextos. Consúltala siempre que se diseñe un evento, un mensaje entre servicios, una notificación de "cuando pase X", un webhook propio, una cola o un bus, que se pierdan o dupliquen mensajes, o que un cambio deba disparar acciones en otra parte del sistema, aunque no se diga "domain event".
|
|
6
|
+
|
|
7
|
+
## Con @archi-code/ddd
|
|
8
|
+
|
|
9
|
+
[Fuera de los libros: convención de @archi-code/ddd]
|
|
10
|
+
|
|
11
|
+
* Los eventos extienden `EventBase`, declaran `static readonly eventName` con el nombre de su clase en pasado (`'OrderPlaced'`) y `static readonly eventVersion` cuando sus datos cambian de forma incompatible (1 si no se declara); llaman a `super(aggregateId, { clock })` y la base pone `eventId` (UUID v4) y `occurredOn`. El payload son primitivos sacados de los value objects (el dinero como `{ amount, currency }`), nunca value objects ni `toPrimitives()` del agregado.
|
|
12
|
+
* El agregado registra con `record()` (que congela el evento) y el caso de uso publica con `await this.events.publish(order.pullDomainEvents())` justo después de `await this.orders.save(order)`. La aplicación nunca abre, une ni cierra transacciones y no hay puerto para ello: cada `save` es una escritura atómica que garantiza el repositorio. Si `publish` falla después de `save`, el evento se puede perder; cuando un evento no puede perderse, el adaptador del repositorio escribe el agregado y sus eventos pendientes en la misma escritura atómica (un outbox en el mismo documento o en la misma transacción de base de datos de `save`) y un reenviador los entrega después por un `EventChannel`. Es persistencia, así que va en el repositorio, y el caso de uso no cambia. Fuera de ese caso, el repositorio no toca los eventos.
|
|
13
|
+
* La aplicación solo tiene el contrato de eventos, en `@archi-code/ddd/application`: para publicar, el puerto `EventBus` (lo único que ve el caso de uso); para escuchar, `EventListener` + `@ListensTo` (`ListensTo.subscriptionOf(listener)` da `{ eventName, consumer }`). Lo que mueve los eventos es infraestructura y lo usa solo el composition root, desde `@archi-code/ddd/infrastructure`: `DomainEventBus(channel, registry)` implementa `EventBus` sobre un `EventChannel` (con `ChannelHandler` y `ChannelSubscription`), que es lo único que implementa cada tecnología (`InMemoryEventChannel` en el paquete; un outbox o un broker en tu infraestructura). Los listeners reciben siempre el evento reconstruido desde su JSON por `EventRegistry`, cuya fábrica pasa el JSON como metadatos para conservar `eventId` y `occurredOn`.
|
|
14
|
+
* Un listener extiende `EventListener<Event>`, escucha un único evento con `@ListensTo(EventClass, { consumer })` (un nombre de consumidor estable y explícito, su identidad en el canal), se llama `<Acción>On<Evento>` y solo traduce el evento a la entrada de un caso de uso.
|
|
15
|
+
* El paquete no trae outbox, canal de broker, deduplicación ni traducción al lenguaje publicado: se escriben en el proyecto sobre el puerto `EventChannel` y los puertos propios, como en `ddd-skills/ddd-eventos-dominio/references/publicacion-ts.md`.
|
|
16
|
+
* `InMemoryEventChannel` entrega en cuanto se llama a `publish`, antes de que termine el `execute` del caso de uso que publica: sirve para tests (`channel.sent` lista lo publicado; `{ delivery: 'parallel' }` ejecuta juntos los listeners de un evento), nunca como entrega durable en producción.
|
|
17
|
+
|
|
18
|
+
Un evento de dominio es el registro de algo que ha ocurrido y que importa al negocio en un Bounded Context [Destilado cap. 6]. Es parte de pleno derecho del modelo [IDDD cap. 8, citando a Evans] y la pieza que hace posible la consistencia eventual entre agregados y la integración entre contextos.
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
## Paso 1. Diseñarlos
|
|
22
|
+
|
|
23
|
+
Guía completa y ejemplos en `ddd-skills/ddd-eventos-dominio/references/diseno-eventos.md`. Lo esencial:
|
|
24
|
+
|
|
25
|
+
* **Nombre:** verbo en pasado, con el lenguaje del contexto que lo origina, normalmente derivado del comando (`commitTo` → `BacklogItemCommitted`) [Destilado cap. 6; IDDD cap. 8].
|
|
26
|
+
* **Datos:** cuándo ocurrió, la identidad del agregado afectado y de los implicados, y lo necesario para entender qué pasó: a menudo los parámetros del comando y algún valor del estado resultante [Destilado cap. 6; IDDD cap. 8].
|
|
27
|
+
* **Sin el estado completo:** un evento lleno de datos pierde su significado, y un `XxxUpdated` con todo el estado obliga al consumidor a comparar versiones para adivinar qué cambió [Destilado cap. 6].
|
|
28
|
+
* **Inmutables**, con constructor completo y solo lectura [IDDD cap. 8].
|
|
29
|
+
* **El tiempo también causa eventos:** el fin de un periodo es un hecho, no un comando, y suele tener nombre en el lenguaje ("cierre de mercado", "fin de ejercicio fiscal") [Destilado cap. 6].
|
|
30
|
+
* **Comando ≠ evento:** un comando se puede rechazar; un evento es un hecho y no se puede negar [Destilado cap. 6].
|
|
31
|
+
|
|
32
|
+
## Paso 2. Guardar y publicar sin perder ni inventar
|
|
33
|
+
|
|
34
|
+
Regla de oro: el agregado modificado y su evento se guardan **en la misma transacción**; después se publica [Destilado cap. 6]. IDDD da tres formas de mantener modelo y mensajería consistentes [IDDD cap. 8]:
|
|
35
|
+
|
|
36
|
+
| Opción | Ventaja | Inconveniente |
|
|
37
|
+
|---|---|---|
|
|
38
|
+
| Modelo y mensajería comparten almacén | Una transacción local | Tablas del bus en tu base; no siempre posible |
|
|
39
|
+
| Transacción global (XA, dos fases) | Almacenes separados | Cara, lenta, no siempre soportada |
|
|
40
|
+
| **Event store propio en la base del modelo + reenviador** | Consistencia garantizada en una transacción local; permite feeds REST | Hay que construir el reenviador y los consumidores deben deduplicar |
|
|
41
|
+
|
|
42
|
+
Recomendación de estas fuentes: la tercera, conocida hoy como **outbox transaccional** `[Fuera de los libros como nombre]`. `[Fuera de los libros: convención de @archi-code/ddd]` Con el paquete, "la misma transacción" es la escritura atómica del repositorio: el adaptador guarda el agregado y sus eventos pendientes juntos dentro de su `save`, y la aplicación no ve ninguna transacción. Implementación en `ddd-skills/ddd-eventos-dominio/references/publicacion-ts.md`.
|
|
43
|
+
|
|
44
|
+
## Paso 3. Consumirlos bien
|
|
45
|
+
|
|
46
|
+
* El suscriptor que reacciona dentro de la misma transacción **no debe modificar otro agregado**; eso rompe la regla de un agregado por transacción. Las reacciones que modifican agregados van fuera de banda, cada una en su transacción [IDDD cap. 8].
|
|
47
|
+
* Entrega al menos una vez → consumidor idempotente. Identidad única del mensaje o del evento para deduplicar [IDDD cap. 8; Destilado cap. 4].
|
|
48
|
+
* Guardar en orden causal no garantiza recibir en ese orden en otros nodos. El consumidor es responsable de respetar la causalidad, usando el tipo de evento o metadatos (secuencia, identificador de causa); a veces puede ignorar eventos ya superados por otros posteriores [Destilado cap. 6].
|
|
49
|
+
* Los consumidores de otros contextos no copian objetos: ejecutan comportamiento de **su** modelo según el significado del evento en su contexto [IDDD cap. 8].
|
|
50
|
+
* Si un evento no lleva lo que un suscriptor necesita, el contrato debe cambiar, probablemente con una nueva versión o un evento distinto [IDDD cap. 8].
|
|
51
|
+
|
|
52
|
+
Con el paquete, la reacción es un listener que lanza un caso de uso, y ese caso de uso carga y guarda su propio agregado con su repositorio, en su propia escritura atómica. Con `InMemoryEventChannel` el listener corre antes de que termine el `execute` del caso de uso que publicó, lo que solo vale en tests; en producción un canal durable lo entrega después de que la escritura del primero se haya confirmado, y cada reacción hace su propia escritura [IDDD cap. 8] `[Fuera de los libros: convención de @archi-code/ddd]`. El listener le pasa el `eventId` para que el caso de uso pueda saltarse una entrega repetida (ver "Consumidor idempotente" en `ddd-skills/ddd-eventos-dominio/references/publicacion-ts.md`):
|
|
53
|
+
|
|
54
|
+
```ts
|
|
55
|
+
import { EventListener, ListensTo } from '@archi-code/ddd/application';
|
|
56
|
+
|
|
57
|
+
@ListensTo(OrderLineAdded, { consumer: 'prepare-shipment-on-order-line-added' })
|
|
58
|
+
export class PrepareShipmentOnOrderLineAdded extends EventListener<OrderLineAdded> {
|
|
59
|
+
constructor(private readonly prepareShipment: PrepareShipment) {
|
|
60
|
+
super();
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async handle(event: OrderLineAdded): Promise<void> {
|
|
64
|
+
await this.prepareShipment.execute({
|
|
65
|
+
eventId: event.eventId,
|
|
66
|
+
orderId: event.aggregateId,
|
|
67
|
+
sku: event.sku,
|
|
68
|
+
quantity: event.quantity,
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## Eventos internos y eventos publicados
|
|
75
|
+
|
|
76
|
+
`[Fuera de los libros como terminología; la idea está en IDDD cap. 13 (Published Language) y cap. 8]`
|
|
77
|
+
|
|
78
|
+
| | Evento de dominio interno | Evento publicado (integración) |
|
|
79
|
+
|---|---|---|
|
|
80
|
+
| Consumidores | El propio contexto | Otros contextos |
|
|
81
|
+
| Forma | Clase `EventBase` del dominio | Esquema JSON versionado (Published Language), `sales.order.OrderPlaced` |
|
|
82
|
+
| Puede cambiar | Con el modelo | Solo con versión nueva y compatibilidad |
|
|
83
|
+
| Contenido | Lo que el modelo necesita | Lo que el contrato promete; sin datos personales innecesarios |
|
|
84
|
+
|
|
85
|
+
No publiques las clases de dominio ni su `toJSON()` tal cual hacia fuera. Traduce el evento serializado (`SerializedEvent`) al contrato publicado en el adaptador que habla con el broker, con `messageId` = `eventId` [IDDD cap. 13]; el paquete no trae esa traducción (ver "Lenguaje publicado en el adaptador del broker" en `ddd-skills/ddd-eventos-dominio/references/publicacion-ts.md`).
|
|
86
|
+
|
|
87
|
+
## Señales de alarma
|
|
88
|
+
|
|
89
|
+
* Eventos en presente o imperativo (`CreateOrder`, `OrderUpdate`).
|
|
90
|
+
* `OrderUpdated` con el pedido entero [Destilado cap. 6].
|
|
91
|
+
* Publicar al bus dentro del método del agregado o antes de `save`; abrir o recibir una transacción en el caso de uso para "publicar dentro"; o confiar en que `InMemoryEventChannel` es una entrega durable. Un evento que no puede perderse va al outbox en la misma escritura atómica del repositorio.
|
|
92
|
+
* Escribir en el proyecto `DomainEvent`, `EventPublisher` o un segundo bus (`RabbitMqEventBus`): los eventos extienden `EventBase` y cada tecnología implementa solo `EventChannel`.
|
|
93
|
+
* Una fábrica de `EventRegistry` que no pasa el JSON como metadatos: el evento reconstruido tendría otro `eventId` y no se podría deduplicar (`fromJSON` lo rechaza).
|
|
94
|
+
* Un handler síncrono que modifica otro agregado en la misma transacción o escritura [IDDD cap. 8].
|
|
95
|
+
* Consumidores sin deduplicación.
|
|
96
|
+
* Eventos con datos personales que el consumidor no necesita.
|
|
97
|
+
* Otros contextos que importan la clase del evento del productor.
|
|
98
|
+
|
|
99
|
+
## Ejemplo
|
|
100
|
+
|
|
101
|
+
`ddd-skills/ddd-eventos-dominio/references/ejemplo-ecommerce.md` contiene el catálogo de eventos de la tienda online con sus decisiones.
|
|
102
|
+
|
|
103
|
+
## Siguiente guía
|
|
104
|
+
|
|
105
|
+
* Persistir el agregado como secuencia de eventos → `ddd-skills/ddd-event-sourcing-cqrs/GUIA.md`.
|
|
106
|
+
* Cómo encaja la publicación en el caso de uso → `ddd-skills/ddd-servicios-capa-aplicacion/GUIA.md`.
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
# Diseñar eventos de dominio
|
|
2
|
+
|
|
3
|
+
## Nombre
|
|
4
|
+
|
|
5
|
+
* Pasado, con el lenguaje del contexto de origen [Destilado cap. 6; IDDD cap. 8].
|
|
6
|
+
* Normalmente derivado del comando que lo causa: `PlaceOrder` → `OrderPlaced`, `AddOrderLine` → `OrderLineAdded` [Destilado cap. 6].
|
|
7
|
+
* Tan compacto como permita el lenguaje. IDDD prefiere `BacklogItemCommitted` a `BacklogItemCommittedToSprint` porque en Scrum un elemento solo se compromete en un sprint; si el equipo prefiere el nombre largo en algún caso, vale [IDDD cap. 8].
|
|
8
|
+
* La combinación del nombre y sus propiedades cuenta por completo lo que pasó en el modelo [Destilado cap. 6].
|
|
9
|
+
* Con el paquete, el nombre es la clase y su `static readonly eventName` (`'OrderPlaced'`); el nombre `sales.order.OrderPlaced` queda para el contrato publicado a otros contextos [IDDD cap. 13] `[Fuera de los libros: convención de @archi-code/ddd]`.
|
|
10
|
+
|
|
11
|
+
| Mal | Bien | Por qué |
|
|
12
|
+
|---|---|---|
|
|
13
|
+
| `OrderUpdated` | `OrderLineAdded`, `ShippingAddressChanged` | Dice qué cambió |
|
|
14
|
+
| `PlaceOrder` | `OrderPlaced` | Es un hecho, no una orden |
|
|
15
|
+
| `OrderStatusChangedToConfirmed` | `OrderConfirmed` | El lenguaje ya tiene la palabra |
|
|
16
|
+
| `PaymentWebhookReceived` | `PaymentAuthorized` | Lenguaje del dominio, no del transporte |
|
|
17
|
+
| `TimerElapsed` | `PaymentAuthorizationExpired` | El paso del tiempo con nombre de negocio [Destilado cap. 6] |
|
|
18
|
+
|
|
19
|
+
## Datos
|
|
20
|
+
|
|
21
|
+
Pregunta guía: ¿qué estimuló la publicación del evento? Normalmente un comando, y el evento debería llevar las propiedades que llevaba ese comando [Destilado cap. 6]. IDDD añade [IDDD cap. 8]:
|
|
22
|
+
|
|
23
|
+
* Marca de tiempo de cuándo ocurrió (`occurredOn`, que pone `EventBase`).
|
|
24
|
+
* Lo necesario para "volver a disparar" el evento: identidad del agregado (`aggregateId`, el primer argumento de `super`) y de los agregados implicados.
|
|
25
|
+
* Parámetros del comando si se demuestran útiles.
|
|
26
|
+
* Valores del estado resultante que ayuden a los suscriptores.
|
|
27
|
+
* En entornos multi-tenant, siempre el `tenantId`, aunque no viniera en el comando.
|
|
28
|
+
|
|
29
|
+
Ejemplo de IDDD: `BacklogItemCommitted` lleva `tenantId`, `backlogItemId` y `sprintId`. El `sprintId` es imprescindible porque el requisito dice que el sprint debe enterarse [IDDD cap. 8].
|
|
30
|
+
|
|
31
|
+
Lista de comprobación `[Fuera de los libros]`:
|
|
32
|
+
* ¿Un consumidor puede decidir qué hacer sin consultar al origen? Si no, ¿es aceptable que consulte (query-back)?
|
|
33
|
+
* ¿Hay datos personales? ¿Los necesita cada consumidor?
|
|
34
|
+
* ¿Los importes llevan moneda? ¿Las fechas, zona horaria?
|
|
35
|
+
* ¿Algún valor cambia a menudo y quedará obsoleto enseguida? Mejor fuera.
|
|
36
|
+
|
|
37
|
+
## Forma en TypeScript
|
|
38
|
+
|
|
39
|
+
Un evento extiende `EventBase` de `@archi-code/ddd/domain`, declara `static readonly eventName` y recibe sus datos como primitivos sacados de los value objects (el dinero como `{ amount, currency }` en céntimos), nunca value objects ni el `toPrimitives()` del agregado `[Fuera de los libros: convención de @archi-code/ddd]`:
|
|
40
|
+
|
|
41
|
+
```ts
|
|
42
|
+
import { EventBase } from '@archi-code/ddd/domain';
|
|
43
|
+
import type { EventMetadata } from '@archi-code/ddd/domain';
|
|
44
|
+
|
|
45
|
+
export interface MoneyData {
|
|
46
|
+
readonly amount: number;
|
|
47
|
+
readonly currency: string;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export class OrderConfirmed extends EventBase {
|
|
51
|
+
static readonly eventName = 'OrderConfirmed';
|
|
52
|
+
|
|
53
|
+
constructor(
|
|
54
|
+
orderId: string,
|
|
55
|
+
readonly total: MoneyData,
|
|
56
|
+
metadata?: EventMetadata,
|
|
57
|
+
) {
|
|
58
|
+
super(orderId, metadata);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
El agregado lo registra en el método que causa el hecho, pasando el reloj que recibió para que `occurredOn` sea determinista en los tests. En el `Order` de `ddd-skills/ddd-agregados/references/implementacion-ts.md` el pedido se confirma solo cuando ya tiene reserva y autorización, desde `registerStockReservation` o `registerPaymentAuthorization`:
|
|
64
|
+
|
|
65
|
+
```ts
|
|
66
|
+
private tryToConfirm(clock: { now(): Date }): void {
|
|
67
|
+
if (this._stockReservation !== null && this._paymentAuthorization !== null) {
|
|
68
|
+
this._status = new OrderStatus(Status.Confirmed);
|
|
69
|
+
this.record(new OrderConfirmed(this.id.value, Order.moneyData(this.total()), { clock }));
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
`EventBase` pone `eventId` (UUID v4) y `occurredOn` (del `clock`, o la hora del sistema), y `record()` congela el evento: inmutable y con constructor completo [IDDD cap. 8]. No generes tú el id ni la fecha de un evento nuevo; pasa `{ eventId, occurredOn }` (o el JSON entero) solo al reconstruir uno que entregó un canal. `toJSON()` da la forma que viaja:
|
|
75
|
+
|
|
76
|
+
```ts
|
|
77
|
+
const [confirmed] = order.pullDomainEvents();
|
|
78
|
+
confirmed.toJSON();
|
|
79
|
+
// { eventName: 'OrderConfirmed', eventVersion: 1, eventId: '…', aggregateId: '9b2f6c1e-…',
|
|
80
|
+
// occurredOn: '2026-03-01T09:30:00.000Z', total: { amount: 7000, currency: 'EUR' } }
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
## Identidad del evento
|
|
84
|
+
|
|
85
|
+
* Dentro del contexto rara vez hace falta comparar eventos; el tipo, las identidades implicadas y la fecha suelen bastar, como en un value object [IDDD cap. 8].
|
|
86
|
+
* Fuera, con mensajería que puede reenviar, una identidad única permite deduplicar. Algunos buses la ponen en la cabecera; si no, la asigna el publicador [IDDD cap. 8]. Con el paquete es el `eventId` que pone `EventBase`: viaja en el JSON, `EventRegistry` lo conserva al reconstruir el evento (la fábrica pasa el JSON como metadatos) y el adaptador del broker lo usa como `messageId`.
|
|
87
|
+
|
|
88
|
+
## Eventos con características de agregado
|
|
89
|
+
|
|
90
|
+
A veces un evento no resulta de un comando sobre un agregado sino que un usuario lo origina directamente. Entonces puede modelarse como agregado inmutable en su propio repositorio, que no permite borrarlo, con identidad generada [IDDD cap. 8]. Ejemplo: una `IncidenciaDeEntregaReportada` por el transportista que se archiva y se consulta. Con el paquete sería un `AggregateRoot` cuyo repositorio implementa solo `AggregateFinder` y `AggregateSaver` (nunca `AggregateRemover`), con id `XId.generate()`.
|
|
91
|
+
|
|
92
|
+
## Versionado
|
|
93
|
+
|
|
94
|
+
* Los eventos publicados llevan versión. Diseñados con cuidado, muchos consumidores pueden leer cualquier versión como si fuera la 1 [IDDD cap. 13].
|
|
95
|
+
* Cambios compatibles `[Fuera de los libros]`: añadir campos opcionales. Incompatibles: renombrar, quitar o cambiar el significado → nueva versión o nuevo evento, publicando ambas durante la transición.
|
|
96
|
+
* Con el paquete, la versión de la forma interna es `static readonly eventVersion` (1 si no se declara; `toJSON()` la escribe). Los eventos guardados conservan la versión con la que se escribieron, así que la fábrica del `EventRegistry` lee las formas antiguas a partir de `json.eventVersion`:
|
|
97
|
+
|
|
98
|
+
```ts
|
|
99
|
+
import { EventBase } from '@archi-code/ddd/domain';
|
|
100
|
+
import type { EventMetadata } from '@archi-code/ddd/domain';
|
|
101
|
+
import { EventRegistry } from '@archi-code/ddd/infrastructure';
|
|
102
|
+
|
|
103
|
+
export class OrderCancelled extends EventBase {
|
|
104
|
+
static readonly eventName = 'OrderCancelled';
|
|
105
|
+
static readonly eventVersion = 2;
|
|
106
|
+
|
|
107
|
+
constructor(
|
|
108
|
+
orderId: string,
|
|
109
|
+
readonly reason: string,
|
|
110
|
+
readonly refund: MoneyData,
|
|
111
|
+
metadata?: EventMetadata,
|
|
112
|
+
) {
|
|
113
|
+
super(orderId, metadata);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export const cancellations = new EventRegistry<OrderCancelled>().register(
|
|
118
|
+
OrderCancelled,
|
|
119
|
+
(json) =>
|
|
120
|
+
json.eventVersion === 1
|
|
121
|
+
? new OrderCancelled(
|
|
122
|
+
json.aggregateId,
|
|
123
|
+
json.reason as string,
|
|
124
|
+
{ amount: json.refundCents as number, currency: 'EUR' },
|
|
125
|
+
json,
|
|
126
|
+
)
|
|
127
|
+
: new OrderCancelled(json.aggregateId, json.reason as string, json.refund as MoneyData, json),
|
|
128
|
+
);
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
La versión del contrato publicado a otros contextos es otra cosa: la decide el adaptador del broker, que puede seguir emitiendo la v1 del contrato aunque el evento interno esté en la v2 (ver `ddd-skills/ddd-eventos-dominio/references/publicacion-ts.md`).
|