@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,526 @@
|
|
|
1
|
+
# Guardar y publicar eventos en TypeScript
|
|
2
|
+
|
|
3
|
+
## Flujo completo
|
|
4
|
+
|
|
5
|
+
```
|
|
6
|
+
Caso de uso ─┬─ orders.findById
|
|
7
|
+
├─ comportamiento → el agregado hace record(evento)
|
|
8
|
+
├─ orders.save(order)
|
|
9
|
+
│ PostgresOrderRepository: BEGIN → UPDATE orders (con versión)
|
|
10
|
+
│ → INSERT en stored_events (eventos pendientes) → COMMIT
|
|
11
|
+
└─ events.publish(order.pullDomainEvents()) (ya vacío: el repositorio los guardó)
|
|
12
|
+
OutboxForwarder (aparte) ── lee lo no publicado en orden → delivery.send(json) → marca publicado
|
|
13
|
+
delivery (EventChannel) ── traduce al lenguaje publicado y lo manda al broker
|
|
14
|
+
└─ entrega a los listeners del propio contexto (receive)
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Base en los libros: guardar agregado y evento en la misma transacción y publicar después [Destilado cap. 6]; event store propio en la base del modelo con un componente fuera de banda que reenvía lo no publicado [IDDD cap. 8]. `[Fuera de los libros: convención de @archi-code/ddd]` Con el paquete la aplicación no abre transacciones: "la misma transacción" es la escritura atómica que el adaptador del repositorio hace dentro de `save`.
|
|
18
|
+
|
|
19
|
+
Qué pone `@archi-code/ddd` y qué escribe el proyecto:
|
|
20
|
+
|
|
21
|
+
| Pieza | Dónde |
|
|
22
|
+
|---|---|
|
|
23
|
+
| `EventBase`, `AggregateRoot.record()`/`pullDomainEvents()` | `@archi-code/ddd/domain` |
|
|
24
|
+
| `EventBus` (lo único que ve el caso de uso), `EventListener`, `@ListensTo` | `@archi-code/ddd/application` |
|
|
25
|
+
| `DomainEventBus`, `EventRegistry`, `EventChannel`, `ChannelHandler`, `ChannelSubscription` (cableado del composition root y puerto del canal) | `@archi-code/ddd/infrastructure` |
|
|
26
|
+
| `InMemoryEventChannel` (tests) | `@archi-code/ddd/infrastructure` |
|
|
27
|
+
| Repositorio en memoria (tests), uno por agregado (`InMemoryOrderRepository`) | `test/support/` del proyecto (`ddd-skills/ddd-repositorios-persistencia/references/tests-repositorios.md`) |
|
|
28
|
+
| Repositorio con outbox, reenviador, deduplicación, traducción al lenguaje publicado, canal del broker | tu infraestructura, sobre esos puertos |
|
|
29
|
+
|
|
30
|
+
## Publicar desde el caso de uso
|
|
31
|
+
|
|
32
|
+
El caso de uso guarda y después publica lo que devuelve `pullDomainEvents()`: si `save` falla no se publica nada, y los eventos nunca salen dos veces. No hay puerto de transacciones: el caso de uso recibe solo los roles de repositorio que usa, el `EventBus` y el `Clock`, y el repositorio garantiza que `save` es una escritura atómica `[Fuera de los libros: convención de @archi-code/ddd]`.
|
|
33
|
+
|
|
34
|
+
```ts
|
|
35
|
+
import { ValueObjects } from '@archi-code/ddd/domain';
|
|
36
|
+
import type { AggregateFinder, AggregateSaver } from '@archi-code/ddd/domain';
|
|
37
|
+
import { UseCase } from '@archi-code/ddd/application';
|
|
38
|
+
import type { Clock, EventBus } from '@archi-code/ddd/application';
|
|
39
|
+
import { AggregateNotFoundException } from '@archi-code/ddd/exceptions';
|
|
40
|
+
|
|
41
|
+
interface AddOrderLineInput {
|
|
42
|
+
orderId: string;
|
|
43
|
+
sku: string;
|
|
44
|
+
quantity: number;
|
|
45
|
+
unitPrice: { amount: number; currency: string };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
interface AddOrderLineCommand {
|
|
49
|
+
orderId: OrderId;
|
|
50
|
+
line: OrderLine;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export class AddOrderLine extends UseCase<AddOrderLineInput, AddOrderLineCommand, void> {
|
|
54
|
+
constructor(
|
|
55
|
+
private readonly orders: AggregateFinder<Order> & AggregateSaver<Order>,
|
|
56
|
+
private readonly events: EventBus<OrderEvent>,
|
|
57
|
+
private readonly clock: Clock,
|
|
58
|
+
) {
|
|
59
|
+
super();
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
protected toCommand(input: AddOrderLineInput): AddOrderLineCommand {
|
|
63
|
+
return ValueObjects.build({
|
|
64
|
+
orderId: () => new OrderId(input.orderId),
|
|
65
|
+
line: () =>
|
|
66
|
+
OrderLine.fromPrimitives({
|
|
67
|
+
sku: input.sku,
|
|
68
|
+
quantity: input.quantity,
|
|
69
|
+
unitPrice: input.unitPrice,
|
|
70
|
+
}),
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
protected async handle(command: AddOrderLineCommand): Promise<void> {
|
|
75
|
+
const order = await this.orders.findById(command.orderId);
|
|
76
|
+
if (order === null) {
|
|
77
|
+
throw new AggregateNotFoundException('Order', command.orderId.value);
|
|
78
|
+
}
|
|
79
|
+
order.addLine(command.line, this.clock);
|
|
80
|
+
await this.orders.save(order);
|
|
81
|
+
await this.events.publish(order.pullDomainEvents());
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Si `publish` falla después de `save`, el agregado ya está guardado y el evento se pierde. Cuando eso no es aceptable, el repositorio vacía los eventos pendientes y los guarda con el agregado en la misma escritura atómica (el outbox de abajo), una opción que los libros permiten; guardarlos es persistencia, así que es trabajo del adaptador del repositorio, nunca del caso de uso `[Fuera de los libros: convención de @archi-code/ddd]`. Cambiar de memoria a outbox o a un broker cambia solo el repositorio y el `EventChannel` del cableado; el caso de uso no se toca. El repositorio con control de versión está en `ddd-skills/ddd-repositorios-persistencia/GUIA.md`.
|
|
87
|
+
|
|
88
|
+
## Probarlo en memoria
|
|
89
|
+
|
|
90
|
+
En los tests, `InMemoryEventChannel` guarda lo publicado en `channel.sent` (JSON congelado) y entrega a los listeners en cuanto se llama a `publish`:
|
|
91
|
+
|
|
92
|
+
```ts
|
|
93
|
+
import { describe, expect, it } from 'vitest';
|
|
94
|
+
import {
|
|
95
|
+
DomainEventBus,
|
|
96
|
+
FixedClock,
|
|
97
|
+
InMemoryEventChannel,
|
|
98
|
+
} from '@archi-code/ddd/infrastructure';
|
|
99
|
+
import { InMemoryOrderRepository } from '../support/in-memory-order-repository';
|
|
100
|
+
|
|
101
|
+
const orderId = '9b2f6c1e-4d3a-4f8b-a1c2-3e4d5f6a7b8c';
|
|
102
|
+
const customerId = '5c1d2e3f-4a5b-4c6d-8e7f-9a0b1c2d3e4f';
|
|
103
|
+
|
|
104
|
+
describe('Adding a line to an order', () => {
|
|
105
|
+
it('publishes OrderLineAdded with the new total', async () => {
|
|
106
|
+
const clock = new FixedClock('2026-03-01T09:30:00Z');
|
|
107
|
+
const orders = new InMemoryOrderRepository();
|
|
108
|
+
const channel = new InMemoryEventChannel();
|
|
109
|
+
const bus = new DomainEventBus<OrderEvent>(channel, orderEvents);
|
|
110
|
+
const order = Order.place(
|
|
111
|
+
new OrderId(orderId),
|
|
112
|
+
new CustomerId(customerId),
|
|
113
|
+
OrderLines.fromPrimitives({
|
|
114
|
+
items: [{ sku: 'TORCH-01', quantity: 1, unitPrice: { amount: 4500, currency: 'EUR' } }],
|
|
115
|
+
}),
|
|
116
|
+
ShippingAddress.fromPrimitives({ street: 'Calle Mayor 1', city: 'Huelva', country: 'ES' }),
|
|
117
|
+
clock,
|
|
118
|
+
);
|
|
119
|
+
await orders.save(order);
|
|
120
|
+
|
|
121
|
+
await new AddOrderLine(orders, bus, clock).execute({
|
|
122
|
+
orderId,
|
|
123
|
+
sku: 'GLOVES-02',
|
|
124
|
+
quantity: 2,
|
|
125
|
+
unitPrice: { amount: 1250, currency: 'EUR' },
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
expect(channel.sent).toMatchObject([
|
|
129
|
+
{
|
|
130
|
+
eventName: 'OrderLineAdded',
|
|
131
|
+
aggregateId: orderId,
|
|
132
|
+
sku: 'GLOVES-02',
|
|
133
|
+
quantity: 2,
|
|
134
|
+
unitPrice: { amount: 1250, currency: 'EUR' },
|
|
135
|
+
total: { amount: 7000, currency: 'EUR' },
|
|
136
|
+
occurredOn: '2026-03-01T09:30:00.000Z',
|
|
137
|
+
},
|
|
138
|
+
]);
|
|
139
|
+
});
|
|
140
|
+
});
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
`Order`, sus value objects (`OrderId`, `CustomerId`, `OrderLine`, `OrderLines`, `Sku`, `Quantity`, `Money`, `ShippingAddress`) y `OrderEvent` son los de `ddd-skills/ddd-agregados/references/implementacion-ts.md`. `orderEvents` es el `EventRegistry` del contexto, con una fábrica por evento que pasa el JSON como metadatos (está en `ddd-skills/ddd-eventos-dominio/references/ejemplo-ecommerce.md`).
|
|
144
|
+
|
|
145
|
+
## Outbox: el repositorio guarda los eventos
|
|
146
|
+
|
|
147
|
+
El paquete no trae outbox: es el adaptador del repositorio del proyecto. Su `save` abre una transacción con su propia conexión, escribe el agregado con su versión y los eventos pendientes en la tabla `stored_events`, y la confirma; un reenviador los entrega después del commit por un `EventChannel`. Con MongoDB, el mismo papel lo cumple un array de eventos pendientes dentro del documento del agregado, escrito en el mismo `updateOne` `[Fuera de los libros: convención de @archi-code/ddd]`.
|
|
148
|
+
|
|
149
|
+
### Tabla
|
|
150
|
+
|
|
151
|
+
`[Fuera de los libros: esquema propio; IDDD cap. 8 usa una tabla equivalente con id secuencial, tipo, cuerpo y fecha]`
|
|
152
|
+
|
|
153
|
+
```sql
|
|
154
|
+
CREATE TABLE stored_events (
|
|
155
|
+
sequence BIGSERIAL PRIMARY KEY,
|
|
156
|
+
event_id UUID NOT NULL UNIQUE,
|
|
157
|
+
aggregate_id TEXT NOT NULL,
|
|
158
|
+
event_name TEXT NOT NULL,
|
|
159
|
+
event_version INT NOT NULL,
|
|
160
|
+
occurred_on TIMESTAMPTZ NOT NULL,
|
|
161
|
+
body JSONB NOT NULL,
|
|
162
|
+
published_at TIMESTAMPTZ NULL
|
|
163
|
+
);
|
|
164
|
+
CREATE INDEX stored_events_unpublished ON stored_events (sequence) WHERE published_at IS NULL;
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
`sequence` conserva el orden causal dentro del contexto; `body` es el `SerializedEvent` que produjo `toJSON()`.
|
|
168
|
+
|
|
169
|
+
### El repositorio
|
|
170
|
+
|
|
171
|
+
```ts
|
|
172
|
+
import type { AggregateFinder, AggregateSaver, PrimitiveTypes } from '@archi-code/ddd/domain';
|
|
173
|
+
import { ConcurrencyException, InternalErrorException } from '@archi-code/ddd/exceptions';
|
|
174
|
+
|
|
175
|
+
interface QueryResult<Row> {
|
|
176
|
+
rows: Row[];
|
|
177
|
+
rowCount: number | null;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
interface SqlClient {
|
|
181
|
+
query<Row = Record<string, unknown>>(text: string, values?: unknown[]): Promise<QueryResult<Row>>;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
interface PoolClient extends SqlClient {
|
|
185
|
+
release(): void;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
interface Pool extends SqlClient {
|
|
189
|
+
connect(): Promise<PoolClient>;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
interface OrderRow {
|
|
193
|
+
version: number;
|
|
194
|
+
data: PrimitiveTypes<Order>;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
export class PostgresOrderRepository implements AggregateFinder<Order>, AggregateSaver<Order> {
|
|
198
|
+
constructor(private readonly pool: Pool) {}
|
|
199
|
+
|
|
200
|
+
async findById(id: OrderId): Promise<Order | null> {
|
|
201
|
+
try {
|
|
202
|
+
const { rows } = await this.pool.query<OrderRow>(
|
|
203
|
+
'SELECT version, data FROM orders WHERE id = $1',
|
|
204
|
+
[id.value],
|
|
205
|
+
);
|
|
206
|
+
return rows[0] ? Order.fromPrimitives(rows[0].data).restoreVersion(rows[0].version) : null;
|
|
207
|
+
} catch (error) {
|
|
208
|
+
throw new InternalErrorException('loading order', { cause: error });
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
async save(order: Order): Promise<void> {
|
|
213
|
+
const primitives = order.toPrimitives();
|
|
214
|
+
const events = order.pullDomainEvents().map((event) => event.toJSON());
|
|
215
|
+
const client = await this.pool.connect();
|
|
216
|
+
try {
|
|
217
|
+
await client.query('BEGIN');
|
|
218
|
+
const { rowCount } =
|
|
219
|
+
order.version === 0
|
|
220
|
+
? await client.query(
|
|
221
|
+
'INSERT INTO orders (id, version, customer_id, status, data) VALUES ($1, 1, $2, $3, $4) ON CONFLICT (id) DO NOTHING',
|
|
222
|
+
[primitives.id, primitives.customerId, primitives.status, primitives],
|
|
223
|
+
)
|
|
224
|
+
: await client.query(
|
|
225
|
+
'UPDATE orders SET version = version + 1, status = $3, data = $4 WHERE id = $1 AND version = $2',
|
|
226
|
+
[primitives.id, order.version, primitives.status, primitives],
|
|
227
|
+
);
|
|
228
|
+
if (rowCount === 0) {
|
|
229
|
+
throw new ConcurrencyException('Order', order.id.value, order.version);
|
|
230
|
+
}
|
|
231
|
+
for (const event of events) {
|
|
232
|
+
await client.query(
|
|
233
|
+
`INSERT INTO stored_events
|
|
234
|
+
(event_id, aggregate_id, event_name, event_version, occurred_on, body)
|
|
235
|
+
VALUES ($1, $2, $3, $4, $5, $6)`,
|
|
236
|
+
[
|
|
237
|
+
event.eventId,
|
|
238
|
+
event.aggregateId,
|
|
239
|
+
event.eventName,
|
|
240
|
+
event.eventVersion,
|
|
241
|
+
event.occurredOn,
|
|
242
|
+
JSON.stringify(event),
|
|
243
|
+
],
|
|
244
|
+
);
|
|
245
|
+
}
|
|
246
|
+
await client.query('COMMIT');
|
|
247
|
+
} catch (error) {
|
|
248
|
+
await client.query('ROLLBACK');
|
|
249
|
+
if (error instanceof ConcurrencyException) {
|
|
250
|
+
throw error;
|
|
251
|
+
}
|
|
252
|
+
throw new InternalErrorException('saving order', { cause: error });
|
|
253
|
+
} finally {
|
|
254
|
+
client.release();
|
|
255
|
+
}
|
|
256
|
+
order.markPersisted();
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
La transacción empieza y termina dentro de `save`: el pedido y sus eventos se confirman o se deshacen juntos, que es lo que pide "un agregado por transacción" [IDDD cap. 10]. `markPersisted()` se llama solo tras el `COMMIT`. Si la escritura falla, el caso de uso termina con el error y el agregado se descarta con sus eventos. El caso de uso no cambia: su `publish` recibe una lista vacía, porque los eventos de este pedido ya están en el outbox. El esquema de `orders` y el repositorio sin outbox están en `ddd-skills/ddd-repositorios-persistencia/references/implementacion-ts.md`.
|
|
262
|
+
|
|
263
|
+
## Reenviador
|
|
264
|
+
|
|
265
|
+
```ts
|
|
266
|
+
import type { EventChannel } from '@archi-code/ddd/infrastructure';
|
|
267
|
+
import type { SerializedEvent } from '@archi-code/ddd/domain';
|
|
268
|
+
import { InternalErrorException } from '@archi-code/ddd/exceptions';
|
|
269
|
+
|
|
270
|
+
export class OutboxForwarder {
|
|
271
|
+
private static readonly lockKey = 8_201_001;
|
|
272
|
+
|
|
273
|
+
constructor(
|
|
274
|
+
private readonly pool: Pool,
|
|
275
|
+
private readonly delivery: EventChannel,
|
|
276
|
+
) {}
|
|
277
|
+
|
|
278
|
+
async forwardBatch(limit = 100): Promise<number> {
|
|
279
|
+
const client = await this.pool.connect();
|
|
280
|
+
try {
|
|
281
|
+
await client.query('BEGIN');
|
|
282
|
+
const { rows: [lock] } = await client.query(
|
|
283
|
+
'SELECT pg_try_advisory_xact_lock($1) AS locked',
|
|
284
|
+
[OutboxForwarder.lockKey],
|
|
285
|
+
);
|
|
286
|
+
if (lock.locked !== true) {
|
|
287
|
+
await client.query('COMMIT');
|
|
288
|
+
return 0;
|
|
289
|
+
}
|
|
290
|
+
const { rows } = await client.query(
|
|
291
|
+
`SELECT sequence, body FROM stored_events WHERE published_at IS NULL
|
|
292
|
+
ORDER BY sequence LIMIT $1`,
|
|
293
|
+
[limit],
|
|
294
|
+
);
|
|
295
|
+
await this.delivery.send(rows.map((row) => row.body as SerializedEvent));
|
|
296
|
+
await client.query(`UPDATE stored_events SET published_at = now() WHERE sequence = ANY($1)`, [
|
|
297
|
+
rows.map((row) => row.sequence),
|
|
298
|
+
]);
|
|
299
|
+
await client.query('COMMIT');
|
|
300
|
+
return rows.length;
|
|
301
|
+
} catch (error) {
|
|
302
|
+
await client.query('ROLLBACK');
|
|
303
|
+
throw new InternalErrorException('forwarding stored events', { cause: error });
|
|
304
|
+
} finally {
|
|
305
|
+
client.release();
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
```
|
|
310
|
+
|
|
311
|
+
`body` es `JSONB` y el driver de Postgres lo devuelve ya como objeto: no se pasa por `JSON.parse`. El cierre consultivo de la transacción (`pg_try_advisory_xact_lock`) deja un solo reenviador entregando a la vez: si otra instancia, o una pasada del temporizador que se solapa con la anterior, leyera las filas siguientes mientras la primera aún entrega su lote, los eventos saldrían desordenados; la que no obtiene el cierre sale sin hacer nada `[Fuera de los libros: el cierre consultivo]`.
|
|
312
|
+
|
|
313
|
+
Si el proceso cae tras entregar y antes de marcar, o si un listener falla y el lote se deshace, los eventos se vuelven a enviar, también a los consumidores que ya los procesaron. Es la razón por la que los consumidores deben deduplicar [IDDD cap. 8]. Un evento que falla siempre bloquearía el lote: añade un contador de intentos y, pasado un límite, apártalo y avisa, para compensar o informar [IDDD cap. 10] `[Fuera de los libros: el contador y el límite]`.
|
|
314
|
+
|
|
315
|
+
Cada reacción hace su propia escritura: el reenviador entrega después del commit y el caso de uso que lanza el listener carga y guarda su agregado con su propio repositorio, cuya escritura atómica no tiene nada que ver con la del pedido [IDDD cap. 8].
|
|
316
|
+
|
|
317
|
+
## Cableado
|
|
318
|
+
|
|
319
|
+
El composition root elige los canales; los casos de uso y los listeners no cambian:
|
|
320
|
+
|
|
321
|
+
```ts
|
|
322
|
+
import { DomainEventBus, InMemoryEventChannel, SystemClock } from '@archi-code/ddd/infrastructure';
|
|
323
|
+
|
|
324
|
+
const orders = new PostgresOrderRepository(pool);
|
|
325
|
+
const delivery = new ContractPublishingChannel(broker, new InMemoryEventChannel());
|
|
326
|
+
const bus = new DomainEventBus<OrderEvent>(delivery, orderEvents);
|
|
327
|
+
await bus.subscribe(
|
|
328
|
+
new PrepareShipmentOnOrderLineAdded(new PrepareShipment(new PostgresWarehouse(pool))),
|
|
329
|
+
);
|
|
330
|
+
const forwarder = new OutboxForwarder(pool, delivery);
|
|
331
|
+
setInterval(() => {
|
|
332
|
+
forwarder.forwardBatch().catch((error) => console.error('forwarding stored events failed', error));
|
|
333
|
+
}, 1000);
|
|
334
|
+
const addOrderLine = new AddOrderLine(orders, bus, new SystemClock());
|
|
335
|
+
```
|
|
336
|
+
|
|
337
|
+
Un lote que falla se queda en el outbox y lo reintenta la pasada siguiente; el `catch` evita que el rechazo quede sin atender y tumbe el proceso de Node. Aquí `InMemoryEventChannel` solo entrega en proceso lo que el reenviador ya sacó del outbox, después del commit; el `publish` del caso de uso llega vacío desde `PostgresOrderRepository`, y los eventos de un agregado cuyo repositorio no tenga outbox saldrían directamente por `delivery`, sin esa garantía. Si los listeners viven en otro proceso, `delivery` es un canal de broker (el `RabbitMqEventChannel` de `ddd-skills/archi-code-ddd/patterns.md`).
|
|
338
|
+
|
|
339
|
+
## Lenguaje publicado en el adaptador del broker
|
|
340
|
+
|
|
341
|
+
Los otros contextos no reciben las clases ni el `toJSON()` del dominio: reciben un contrato versionado [IDDD cap. 13]. El paquete no trae esa traducción; se hace en el adaptador que habla con el broker, a partir del `SerializedEvent`, con `messageId` = `eventId`. Un evento interno no produce ningún mensaje, y durante una transición un evento puede producir un mensaje por versión del contrato:
|
|
342
|
+
|
|
343
|
+
```ts
|
|
344
|
+
import type {
|
|
345
|
+
ChannelHandler,
|
|
346
|
+
ChannelSubscription,
|
|
347
|
+
EventChannel,
|
|
348
|
+
} from '@archi-code/ddd/infrastructure';
|
|
349
|
+
import type { SerializedEvent } from '@archi-code/ddd/domain';
|
|
350
|
+
|
|
351
|
+
interface Broker {
|
|
352
|
+
publish(message: SerializedEvent, messageId: string): Promise<void>;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
export class SalesContracts {
|
|
356
|
+
static toPublished(event: SerializedEvent): SerializedEvent[] {
|
|
357
|
+
const envelope = {
|
|
358
|
+
eventVersion: 1,
|
|
359
|
+
eventId: event.eventId,
|
|
360
|
+
aggregateId: event.aggregateId,
|
|
361
|
+
occurredOn: event.occurredOn,
|
|
362
|
+
};
|
|
363
|
+
switch (event.eventName) {
|
|
364
|
+
case 'OrderPlaced':
|
|
365
|
+
return [
|
|
366
|
+
{
|
|
367
|
+
...envelope,
|
|
368
|
+
eventName: 'sales.order.OrderPlaced',
|
|
369
|
+
orderId: event.aggregateId,
|
|
370
|
+
customerId: event.customerId,
|
|
371
|
+
lines: (event.lines as OrderLineData[]).map((line) => ({
|
|
372
|
+
sku: line.sku,
|
|
373
|
+
quantity: line.quantity,
|
|
374
|
+
unitPrice: SalesContracts.money(line.unitPrice),
|
|
375
|
+
})),
|
|
376
|
+
total: SalesContracts.money(event.total as MoneyData),
|
|
377
|
+
},
|
|
378
|
+
];
|
|
379
|
+
case 'OrderConfirmed':
|
|
380
|
+
return [
|
|
381
|
+
{
|
|
382
|
+
...envelope,
|
|
383
|
+
eventName: 'sales.order.OrderConfirmed',
|
|
384
|
+
orderId: event.aggregateId,
|
|
385
|
+
total: SalesContracts.money(event.total as MoneyData),
|
|
386
|
+
},
|
|
387
|
+
];
|
|
388
|
+
default:
|
|
389
|
+
return [];
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
private static money(money: MoneyData): MoneyData {
|
|
394
|
+
return { amount: money.amount, currency: money.currency };
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
export class ContractPublishingChannel implements EventChannel {
|
|
399
|
+
constructor(
|
|
400
|
+
private readonly broker: Broker,
|
|
401
|
+
private readonly inner: EventChannel,
|
|
402
|
+
) {}
|
|
403
|
+
|
|
404
|
+
async send(events: readonly SerializedEvent[]): Promise<void> {
|
|
405
|
+
for (const message of events.flatMap((event) => SalesContracts.toPublished(event))) {
|
|
406
|
+
await this.broker.publish(message, message.eventId);
|
|
407
|
+
}
|
|
408
|
+
await this.inner.send(events);
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
receive(
|
|
412
|
+
eventName: string,
|
|
413
|
+
consumer: string,
|
|
414
|
+
handler: ChannelHandler,
|
|
415
|
+
): Promise<ChannelSubscription> {
|
|
416
|
+
return this.inner.receive(eventName, consumer, handler);
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
```
|
|
420
|
+
|
|
421
|
+
`SalesContracts` decide qué eventos salen del contexto y con qué forma, campo a campo (`MoneyData` y `OrderLineData` son los de `ddd-skills/ddd-eventos-dominio/references/ejemplo-ecommerce.md`), para que un campo nuevo del evento interno no se filtre al contrato (ver "Eventos internos y eventos publicados" en `ddd-skills/ddd-eventos-dominio/GUIA.md`); el contrato no cambia cuando cambia el evento interno, solo cuando se publica una versión nueva. El mensaje es un `SerializedEvent` plano con el nombre y la versión del contrato (`sales.order.OrderPlaced`, v1), que el consumidor lee con su propia clase `EventBase` (`ddd-skills/ddd-context-mapping/references/anticorrupcion-ts.md`). `Broker` es la parte del driver que el adaptador usa, declarada de forma estructural.
|
|
422
|
+
|
|
423
|
+
## Consumidor idempotente
|
|
424
|
+
|
|
425
|
+
La entrega es al menos una vez, así que el consumidor se salta un `eventId` que ya procesó, y lo anota **en la misma transacción** que su cambio [IDDD cap. 8]. El paquete no trae inbox. `[Fuera de los libros: convención de @archi-code/ddd]` Anotar el `eventId` y escribir el efecto es persistencia, así que lo hace el adaptador del puerto que escribe, en una sola escritura atómica; el listener solo pasa el `eventId` al caso de uso (como en `ddd-skills/ddd-eventos-dominio/GUIA.md`, paso 3), y el caso de uso se lo pasa al puerto:
|
|
426
|
+
|
|
427
|
+
```ts
|
|
428
|
+
import { IdType, ValueObjects } from '@archi-code/ddd/domain';
|
|
429
|
+
import { UseCase } from '@archi-code/ddd/application';
|
|
430
|
+
|
|
431
|
+
export class EventId extends IdType<'EventId'> {}
|
|
432
|
+
|
|
433
|
+
export interface Warehouse {
|
|
434
|
+
prepare(eventId: EventId, orderId: OrderId, sku: Sku, quantity: Quantity): Promise<void>;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
interface PrepareShipmentInput {
|
|
438
|
+
eventId: string;
|
|
439
|
+
orderId: string;
|
|
440
|
+
sku: string;
|
|
441
|
+
quantity: number;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
interface PrepareShipmentCommand {
|
|
445
|
+
eventId: EventId;
|
|
446
|
+
orderId: OrderId;
|
|
447
|
+
sku: Sku;
|
|
448
|
+
quantity: Quantity;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
export class PrepareShipment extends UseCase<PrepareShipmentInput, PrepareShipmentCommand, void> {
|
|
452
|
+
constructor(private readonly warehouse: Warehouse) {
|
|
453
|
+
super();
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
protected toCommand(input: PrepareShipmentInput): PrepareShipmentCommand {
|
|
457
|
+
return ValueObjects.build({
|
|
458
|
+
eventId: () => new EventId(input.eventId),
|
|
459
|
+
orderId: () => new OrderId(input.orderId),
|
|
460
|
+
sku: () => new Sku(input.sku),
|
|
461
|
+
quantity: () => new Quantity(input.quantity),
|
|
462
|
+
});
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
protected async handle(command: PrepareShipmentCommand): Promise<void> {
|
|
466
|
+
await this.warehouse.prepare(command.eventId, command.orderId, command.sku, command.quantity);
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
```
|
|
470
|
+
|
|
471
|
+
```ts
|
|
472
|
+
import { InternalErrorException } from '@archi-code/ddd/exceptions';
|
|
473
|
+
|
|
474
|
+
export class PostgresWarehouse implements Warehouse {
|
|
475
|
+
private static readonly consumer = 'prepare-shipment';
|
|
476
|
+
|
|
477
|
+
constructor(private readonly pool: Pool) {}
|
|
478
|
+
|
|
479
|
+
async prepare(eventId: EventId, orderId: OrderId, sku: Sku, quantity: Quantity): Promise<void> {
|
|
480
|
+
const client = await this.pool.connect();
|
|
481
|
+
try {
|
|
482
|
+
await client.query('BEGIN');
|
|
483
|
+
const { rowCount } = await client.query(
|
|
484
|
+
`INSERT INTO processed_events (consumer, event_id) VALUES ($1, $2) ON CONFLICT DO NOTHING`,
|
|
485
|
+
[PostgresWarehouse.consumer, eventId.value],
|
|
486
|
+
);
|
|
487
|
+
if (rowCount === 1) {
|
|
488
|
+
await client.query(
|
|
489
|
+
`INSERT INTO shipment_preparations (order_id, sku, quantity) VALUES ($1, $2, $3)`,
|
|
490
|
+
[orderId.value, sku.value, quantity.value],
|
|
491
|
+
);
|
|
492
|
+
}
|
|
493
|
+
await client.query('COMMIT');
|
|
494
|
+
} catch (error) {
|
|
495
|
+
await client.query('ROLLBACK');
|
|
496
|
+
throw new InternalErrorException('preparing shipment', { cause: error });
|
|
497
|
+
} finally {
|
|
498
|
+
client.release();
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
```
|
|
503
|
+
|
|
504
|
+
```sql
|
|
505
|
+
CREATE TABLE processed_events (
|
|
506
|
+
consumer TEXT NOT NULL,
|
|
507
|
+
event_id UUID NOT NULL,
|
|
508
|
+
PRIMARY KEY (consumer, event_id)
|
|
509
|
+
);
|
|
510
|
+
```
|
|
511
|
+
|
|
512
|
+
La anotación y el efecto van en la misma transacción, abierta y cerrada dentro del adaptador, así que se confirman o se deshacen juntos; si el `eventId` ya estaba, el adaptador no hace nada. `shipment_preparations` es la tabla donde el almacén guarda lo que tiene que preparar. Cuando el efecto ya es idempotente por sí mismo (un método del agregado que registra un hecho, como `registerStockReservation` en `RegisterReservation`, que no hace nada si la reserva ya está registrada), basta con eso; un caso de uso que lanza un listener tampoco rechaza el hecho: si el agregado no existe, retorna en vez de lanzar `AggregateNotFoundException`, para que el canal no lo reentregue sin fin.
|
|
513
|
+
|
|
514
|
+
## Orden y causalidad
|
|
515
|
+
|
|
516
|
+
* El `sequence` de la tabla conserva el orden causal dentro del contexto [Destilado cap. 6], y el reenviador entrega en ese orden. Un mismo consumidor recibe los eventos de un `send` en su orden.
|
|
517
|
+
* El bus puede desordenar. Opciones `[Fuera de los libros]`: particionar por `aggregateId` (Kafka, sesiones de Service Bus, un consumidor activo por cola en RabbitMQ) para mantener el orden por agregado; o que el consumidor compare `occurredOn` o una versión y aparque los eventos que llegan antes que su causa [Destilado cap. 6 describe la responsabilidad del consumidor].
|
|
518
|
+
* Si un evento posterior hace irrelevante uno anterior, el consumidor puede ignorar el anterior [Destilado cap. 6].
|
|
519
|
+
|
|
520
|
+
## Despacho interno
|
|
521
|
+
|
|
522
|
+
Para políticas del mismo contexto se puede usar el mismo reenviador (más simple y robusto, como en el cableado de arriba) o un despachador en proceso que se ejecute **después** del commit. Nunca dentro de la transacción si la política modifica otro agregado [IDDD cap. 8]. `DomainEventBus` sobre `InMemoryEventChannel` entrega en el acto, antes de que termine el `execute` del caso de uso que publica y sin guardar nada: solo sirve para tests y prototipos; en producción, el repositorio con outbox de arriba `[Fuera de los libros: convención de @archi-code/ddd]`.
|
|
523
|
+
|
|
524
|
+
## Notificaciones por REST
|
|
525
|
+
|
|
526
|
+
Alternativa a un bus: exponer el event store como un log de notificaciones REST (actual y archivados), que los consumidores leen a su ritmo [IDDD cap. 8 y cap. 13]. Útil cuando los consumidores no comparten infraestructura de mensajería. La tabla `stored_events` ya tiene lo necesario: `sequence` para paginar y `body` para servir cada evento traducido con `SalesContracts`. Ver `ddd-skills/ddd-context-mapping/GUIA.md`.
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# Orquestador de la suite DDD
|
|
2
|
+
|
|
3
|
+
> Parte del skill `archi-code-ddd`: léela solo cuando ese skill te dirija aquí. Es la guía de entrada de la suite: dice qué guía leer para cada tarea de código y las convenciones que siguen todas las demás guías.
|
|
4
|
+
|
|
5
|
+
## Convenciones de la suite
|
|
6
|
+
|
|
7
|
+
* **Trazabilidad.** Cada regla de las guías indica su origen: `[Destilado cap. N]` (Domain-Driven Design Destilado / Distilled, Vernon), `[IDDD cap. N]` (Implementing Domain-Driven Design, Vernon), `[PHP: capítulo]` (Domain-Driven Design in PHP). Lo que no sale de esos libros va marcado `[Fuera de los libros]`; las convenciones del paquete, `[Fuera de los libros: convención de @archi-code/ddd]`. Si una decisión de código se apoya en algo que no es de los libros, dilo.
|
|
8
|
+
* **Precedencia.** Las guías son la fuente de verdad para escribir código. El código se escribe con `@archi-code/ddd`: los ejemplos de las guías ya usan su API (las clases de su `src`), nunca clases base genéricas escritas a mano. El paquete solo se aparta de una guía donde lo marca `[Fuera de los libros: convención de @archi-code/ddd]` (el recuadro "Con @archi-code/ddd" de cada guía y las notas marcadas); cualquier otra discrepancia entre una guía y el paquete (`ddd-skills/archi-code-ddd/` o su código) es un defecto del paquete: sigue la guía.
|
|
9
|
+
* **Vacíos.** Si las guías no cubren algo o se contradicen entre sí, usa tu conocimiento de DDD, márcalo como tal y no inventes conceptos ni prácticas no estándar sin decirlo.
|
|
10
|
+
* **API.** El código es TypeScript con `@archi-code/ddd`, sigue las reglas del paquete y no inventa API (`ddd-skills/archi-code-ddd/what-not-to-do.md`).
|
|
11
|
+
|
|
12
|
+
## Enrutado
|
|
13
|
+
|
|
14
|
+
Lee el `GUIA.md` correspondiente antes de escribir o revisar el código y después solo las `references/` que necesites. Si la tarea toca varias guías, léelas todas y empieza por la de integración o arquitectura.
|
|
15
|
+
|
|
16
|
+
| Situación | Guía |
|
|
17
|
+
|---|---|
|
|
18
|
+
| Cómo se integran en el código contextos y sistemas externos, ACL | `ddd-skills/ddd-context-mapping/GUIA.md` |
|
|
19
|
+
| Carpetas, capas, dependencias, módulos, composition root | `ddd-skills/ddd-arquitectura/GUIA.md` |
|
|
20
|
+
| Valor o entidad, ids, primitivos, validación, Specification | `ddd-skills/ddd-value-objects-entidades/GUIA.md` |
|
|
21
|
+
| Qué va junto, invariantes, transacciones, concurrencia, fábricas | `ddd-skills/ddd-agregados/GUIA.md` |
|
|
22
|
+
| Eventos, mensajería, outbox, idempotencia, versiones | `ddd-skills/ddd-eventos-dominio/GUIA.md` |
|
|
23
|
+
| Cargar y guardar agregados, mappers, transacciones (son del repositorio), tests de repositorio | `ddd-skills/ddd-repositorios-persistencia/GUIA.md` |
|
|
24
|
+
| Servicios de dominio, casos de uso, dónde va la lógica, autorización | `ddd-skills/ddd-servicios-capa-aplicacion/GUIA.md` |
|
|
25
|
+
| Historial completo, read models, CQRS, Event Sourcing | `ddd-skills/ddd-event-sourcing-cqrs/GUIA.md` |
|
|
26
|
+
| Revisar o migrar código existente | `ddd-skills/ddd-auditoria-refactorizacion/GUIA.md` |
|
|
27
|
+
|
|
28
|
+
Las reglas esenciales con su fuente están en `ddd-skills/ddd-orquestador/references/chuleta-reglas.md`.
|
|
29
|
+
|
|
30
|
+
Secuencias tipo:
|
|
31
|
+
|
|
32
|
+
* **Contexto nuevo:** arquitectura → agregados con value objects → eventos → repositorios → casos de uso → Event Sourcing o CQRS solo si hay motivo. Para un contexto sin reglas de negocio, basta la arquitectura y un CRUD bien encapsulado [IDDD cap. 1].
|
|
33
|
+
* **Código existente:** auditoría primero (escaneo, hallazgos, plan) y después las guías que indique el resultado.
|
|
34
|
+
* **Duda concreta:** directamente a la guía correspondiente.
|
|
35
|
+
|
|
36
|
+
## Restricciones
|
|
37
|
+
|
|
38
|
+
* No metas DDD táctico, CQRS, Event Sourcing ni microservicios donde no hay complejidad que lo justifique.
|
|
39
|
+
* No mezcles contextos en el código sin justificarlo ni diseñes agregados sin pensar en consistencia y límites.
|
|
40
|
+
* Los ejemplos van en un dominio realista (pedidos, pagos, usuarios o el del proyecto), escritos con `@archi-code/ddd` (`ddd-skills/archi-code-ddd/patterns.md`).
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# Chuleta de reglas DDD
|
|
2
|
+
|
|
3
|
+
## Integración entre contextos
|
|
4
|
+
* Un Bounded Context es una frontera lingüística; un lenguaje ubicuo por contexto [IDDD cap. 2; Destilado cap. 2].
|
|
5
|
+
* El lenguaje ubicuo se implementa en el código: clases y métodos con sus nombres [IDDD cap. 1].
|
|
6
|
+
* No partir contextos por tecnología ni por reparto de tareas; usar módulos [IDDD cap. 2 y cap. 9].
|
|
7
|
+
* ACL frente a sistemas heredados y externos; no hablar su lenguaje [Destilado cap. 4].
|
|
8
|
+
* Consumidores dependen del esquema publicado, no de clases del productor [Destilado cap. 4; IDDD cap. 13].
|
|
9
|
+
* Mensajería: al menos una vez + receptor idempotente [Destilado cap. 4].
|
|
10
|
+
* Nada de cadenas de peticiones síncronas bloqueantes entre contextos [Destilado cap. 4].
|
|
11
|
+
|
|
12
|
+
## Arquitectura
|
|
13
|
+
* Dominio libre de tecnología; los libros ponen las transacciones en la capa de aplicación [Destilado cap. 2; IDDD cap. 12]. Con el paquete, la transacción y todo lo de persistencia son del adaptador del repositorio; la aplicación no abre, une ni cierra transacciones `[Fuera de los libros: convención de @archi-code/ddd]`.
|
|
14
|
+
* Hexagonal con inversión de dependencias [IDDD cap. 4].
|
|
15
|
+
* Servicios de aplicación finos [IDDD cap. 4 y cap. 14].
|
|
16
|
+
* No exponer el modelo directamente por REST [IDDD cap. 4].
|
|
17
|
+
* Módulos con nombres del lenguaje, no de patrones [IDDD cap. 9; PHP: Modules].
|
|
18
|
+
* CQRS y Event Sourcing solo para mitigar riesgos concretos [IDDD cap. 4].
|
|
19
|
+
|
|
20
|
+
## Táctico
|
|
21
|
+
* Preferir value objects; inmutables, igualdad por valor, sin efectos secundarios [IDDD cap. 6].
|
|
22
|
+
* Entidades: identidad estable, comportamiento con nombres del negocio, sin setters públicos [IDDD cap. 5].
|
|
23
|
+
* Identidad temprana si hay eventos de creación [IDDD cap. 5].
|
|
24
|
+
* El modelo de datos se subordina al de dominio [IDDD cap. 6].
|
|
25
|
+
* Agregados: invariantes reales dentro, pequeños, referencias por identidad, consistencia eventual fuera [Destilado cap. 5; IDDD cap. 10].
|
|
26
|
+
* Una transacción, un agregado [Destilado cap. 5; IDDD cap. 10]. Con el paquete: una escritura atómica por agregado, garantizada por el repositorio en cada `save`/`remove` `[Fuera de los libros: convención de @archi-code/ddd]`.
|
|
27
|
+
* "¿De quién es el trabajo?" para desempatar [IDDD cap. 10].
|
|
28
|
+
* No inyectar repositorios en agregados [IDDD cap. 10].
|
|
29
|
+
* Eventos en pasado, con lo necesario, sin estado completo [Destilado cap. 6].
|
|
30
|
+
* Agregado y evento en la misma transacción; publicar después [Destilado cap. 6; IDDD cap. 8]. Con el paquete, si el evento no puede perderse, el repositorio escribe el agregado y sus eventos pendientes en la misma escritura atómica (outbox) y un relay los entrega por un `EventChannel` `[Fuera de los libros: convención de @archi-code/ddd]`.
|
|
31
|
+
* Un repositorio por agregado; no es un DAO [IDDD cap. 12].
|
|
32
|
+
* Servicio de dominio solo si la operación no encaja en una entidad o valor; sin estado [IDDD cap. 7].
|
|
33
|
+
* Métodos que revelan intención; funciones sin efectos secundarios [IDDD cap. 5 y cap. 6].
|
|
34
|
+
* Reglas con nombre del negocio que se combinan: Specification [IDDD cap. 5; PHP: Repositories].
|
|
35
|
+
* Un hecho de otro contexto no se rechaza: el agregado lo registra y, si no encaja, emite un evento para compensar [Destilado cap. 6].
|