@archi-code/ddd 0.0.0-stage → 3.0.0-alpha.2
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 +2795 -2
- package/SKILL.md +106 -0
- package/ddd-skills/archi-code-ddd/imports.md +94 -0
- package/ddd-skills/archi-code-ddd/layers.md +314 -0
- package/ddd-skills/archi-code-ddd/patterns.md +683 -0
- package/ddd-skills/archi-code-ddd/what-not-to-do.md +196 -0
- package/ddd-skills/archi-code-ddd/where-each-class-goes.md +64 -0
- package/ddd-skills/ddd-agregados/GUIA.md +114 -0
- package/ddd-skills/ddd-agregados/references/consistencia-eventual.md +158 -0
- package/ddd-skills/ddd-agregados/references/ejemplo-ecommerce.md +52 -0
- package/ddd-skills/ddd-agregados/references/fabricas.md +193 -0
- package/ddd-skills/ddd-agregados/references/implementacion-ts.md +706 -0
- package/ddd-skills/ddd-arquitectura/GUIA.md +80 -0
- package/ddd-skills/ddd-arquitectura/assets/dependency-cruiser.cjs +99 -0
- package/ddd-skills/ddd-arquitectura/references/estilos-adicionales.md +51 -0
- package/ddd-skills/ddd-arquitectura/references/estructura-hexagonal-ts.md +406 -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 +314 -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 +419 -0
- package/ddd-skills/ddd-context-mapping/references/estilos-integracion.md +152 -0
- package/ddd-skills/ddd-event-sourcing-cqrs/GUIA.md +92 -0
- package/ddd-skills/ddd-event-sourcing-cqrs/references/event-sourcing-ts.md +476 -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 +130 -0
- package/ddd-skills/ddd-eventos-dominio/references/ejemplo-ecommerce.md +240 -0
- package/ddd-skills/ddd-eventos-dominio/references/publicacion-ts.md +529 -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 +314 -0
- package/ddd-skills/ddd-repositorios-persistencia/references/tests-repositorios.md +191 -0
- package/ddd-skills/ddd-repositorios-persistencia/references/transacciones-ts.md +301 -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 +321 -0
- package/ddd-skills/ddd-servicios-capa-aplicacion/references/servicios-dominio-ts.md +190 -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 +218 -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 +376 -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/exceptions/aggregate-not-found.exception.d.ts +5 -0
- package/dist/cjs/application/exceptions/aggregate-not-found.exception.js +17 -0
- package/dist/cjs/application/exceptions/application-exception-code.d.ts +7 -0
- package/dist/cjs/application/exceptions/application-exception-code.js +11 -0
- package/dist/cjs/application/exceptions/application.exception.d.ts +6 -0
- package/dist/cjs/application/exceptions/application.exception.js +16 -0
- package/dist/cjs/application/exceptions/concurrency.exception.d.ts +5 -0
- package/dist/cjs/application/exceptions/concurrency.exception.js +17 -0
- package/dist/cjs/application/exceptions/not-allowed.exception.d.ts +5 -0
- package/dist/cjs/application/exceptions/not-allowed.exception.js +17 -0
- package/dist/cjs/application/exceptions/unexpected-error.exception.d.ts +5 -0
- package/dist/cjs/application/exceptions/unexpected-error.exception.js +17 -0
- package/dist/cjs/application/index.d.ts +14 -0
- package/dist/cjs/application/index.js +23 -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 +25 -0
- package/dist/cjs/application/value-object-builder/value-object-builder.d.ts +16 -0
- package/dist/cjs/application/value-object-builder/value-object-builder.js +51 -0
- package/dist/cjs/domain/aggregate/aggregate-root.d.ts +9 -0
- package/dist/cjs/domain/aggregate/aggregate-root.js +17 -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 +23 -0
- package/dist/cjs/domain/event/event-base.js +21 -0
- package/dist/cjs/domain/exceptions/domain-exception-code.d.ts +7 -0
- package/dist/cjs/domain/exceptions/domain-exception-code.js +11 -0
- package/dist/cjs/domain/exceptions/domain.exception.d.ts +6 -0
- package/dist/cjs/domain/exceptions/domain.exception.js +16 -0
- package/dist/cjs/domain/exceptions/invalid-event-definition.exception.d.ts +4 -0
- package/dist/cjs/domain/exceptions/invalid-event-definition.exception.js +19 -0
- package/dist/cjs/domain/exceptions/required-value.exception.d.ts +4 -0
- package/dist/cjs/domain/exceptions/required-value.exception.js +20 -0
- package/dist/cjs/domain/exceptions/type-primitive.exception.d.ts +4 -0
- package/dist/cjs/domain/exceptions/type-primitive.exception.js +20 -0
- package/dist/cjs/domain/exceptions/validation.exception.d.ts +14 -0
- package/dist/cjs/domain/exceptions/validation.exception.js +23 -0
- package/dist/cjs/domain/index.d.ts +17 -0
- package/dist/cjs/domain/index.js +36 -0
- package/dist/cjs/domain/primitive/primitive-types.d.ts +20 -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 +18 -0
- package/dist/cjs/domain/type/abstract-type.js +61 -0
- package/dist/cjs/domain/type/array-type.d.ts +17 -0
- package/dist/cjs/domain/type/array-type.js +64 -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-brand.d.ts +4 -0
- package/dist/cjs/domain/type/composite/composite-brand.js +15 -0
- package/dist/cjs/domain/type/composite/composite-field.d.ts +18 -0
- package/dist/cjs/domain/type/composite/composite-field.js +86 -0
- package/dist/cjs/domain/type/composite/composite-issues.d.ts +6 -0
- package/dist/cjs/domain/type/composite/composite-issues.js +35 -0
- package/dist/cjs/domain/type/composite/composite.d.ts +26 -0
- package/dist/cjs/domain/type/composite/composite.js +51 -0
- package/dist/cjs/domain/type/date-type.d.ts +19 -0
- package/dist/cjs/domain/type/date-type.js +70 -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/id-type.d.ts +7 -0
- package/dist/cjs/domain/type/id-type.js +17 -0
- package/dist/cjs/domain/type/index.d.ts +15 -0
- package/dist/cjs/domain/type/index.js +29 -0
- package/dist/cjs/domain/type/json-type.d.ts +9 -0
- package/dist/cjs/domain/type/json-type.js +46 -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 +8 -0
- package/dist/cjs/domain/type/required/required-mark.js +13 -0
- package/dist/cjs/domain/type/required/required-type.d.ts +7 -0
- package/dist/cjs/domain/type/required/required-type.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/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 +50 -0
- package/dist/cjs/infrastructure/exceptions/event-delivery.exception.d.ts +5 -0
- package/dist/cjs/infrastructure/exceptions/event-delivery.exception.js +20 -0
- package/dist/cjs/infrastructure/exceptions/infrastructure-exception-code.d.ts +6 -0
- package/dist/cjs/infrastructure/exceptions/infrastructure-exception-code.js +10 -0
- package/dist/cjs/infrastructure/exceptions/infrastructure.exception.d.ts +6 -0
- package/dist/cjs/infrastructure/exceptions/infrastructure.exception.js +16 -0
- package/dist/cjs/infrastructure/exceptions/internal-error.exception.d.ts +5 -0
- package/dist/cjs/infrastructure/exceptions/internal-error.exception.js +17 -0
- package/dist/cjs/infrastructure/exceptions/kind/exception-classifier.d.ts +4 -0
- package/dist/cjs/infrastructure/exceptions/kind/exception-classifier.js +34 -0
- package/dist/cjs/infrastructure/exceptions/kind/exception-kind.d.ts +9 -0
- package/dist/cjs/infrastructure/exceptions/kind/exception-kind.js +13 -0
- package/dist/cjs/infrastructure/exceptions/unknown-event.exception.d.ts +4 -0
- package/dist/cjs/infrastructure/exceptions/unknown-event.exception.js +19 -0
- package/dist/cjs/infrastructure/index.d.ts +21 -0
- package/dist/cjs/infrastructure/index.js +33 -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/cjs/shared/exceptions/abstract.exception.d.ts +25 -0
- package/dist/cjs/shared/exceptions/abstract.exception.js +59 -0
- package/dist/cjs/shared/exceptions/brand/exception-brand.d.ts +6 -0
- package/dist/cjs/shared/exceptions/brand/exception-brand.js +29 -0
- package/dist/cjs/shared/exceptions/received/received-value.d.ts +4 -0
- package/dist/cjs/shared/exceptions/received/received-value.js +13 -0
- package/dist/cjs/shared/index.d.ts +8 -0
- package/dist/cjs/shared/index.js +13 -0
- package/dist/cjs/shared/issue-collection-error.d.ts +9 -0
- package/dist/cjs/shared/issue-collection-error.js +11 -0
- package/dist/cjs/shared/issue-collector.d.ts +8 -0
- package/dist/cjs/shared/issue-collector.js +41 -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/exceptions/aggregate-not-found.exception.d.ts +5 -0
- package/dist/esm/application/exceptions/aggregate-not-found.exception.js +13 -0
- package/dist/esm/application/exceptions/application-exception-code.d.ts +7 -0
- package/dist/esm/application/exceptions/application-exception-code.js +8 -0
- package/dist/esm/application/exceptions/application.exception.d.ts +6 -0
- package/dist/esm/application/exceptions/application.exception.js +12 -0
- package/dist/esm/application/exceptions/concurrency.exception.d.ts +5 -0
- package/dist/esm/application/exceptions/concurrency.exception.js +13 -0
- package/dist/esm/application/exceptions/not-allowed.exception.d.ts +5 -0
- package/dist/esm/application/exceptions/not-allowed.exception.js +13 -0
- package/dist/esm/application/exceptions/unexpected-error.exception.d.ts +5 -0
- package/dist/esm/application/exceptions/unexpected-error.exception.js +13 -0
- package/dist/esm/application/index.d.ts +14 -0
- package/dist/esm/application/index.js +10 -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 +21 -0
- package/dist/esm/application/value-object-builder/value-object-builder.d.ts +16 -0
- package/dist/esm/application/value-object-builder/value-object-builder.js +47 -0
- package/dist/esm/domain/aggregate/aggregate-root.d.ts +9 -0
- package/dist/esm/domain/aggregate/aggregate-root.js +13 -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 +23 -0
- package/dist/esm/domain/event/event-base.js +17 -0
- package/dist/esm/domain/exceptions/domain-exception-code.d.ts +7 -0
- package/dist/esm/domain/exceptions/domain-exception-code.js +8 -0
- package/dist/esm/domain/exceptions/domain.exception.d.ts +6 -0
- package/dist/esm/domain/exceptions/domain.exception.js +12 -0
- package/dist/esm/domain/exceptions/invalid-event-definition.exception.d.ts +4 -0
- package/dist/esm/domain/exceptions/invalid-event-definition.exception.js +15 -0
- package/dist/esm/domain/exceptions/required-value.exception.d.ts +4 -0
- package/dist/esm/domain/exceptions/required-value.exception.js +16 -0
- package/dist/esm/domain/exceptions/type-primitive.exception.d.ts +4 -0
- package/dist/esm/domain/exceptions/type-primitive.exception.js +16 -0
- package/dist/esm/domain/exceptions/validation.exception.d.ts +14 -0
- package/dist/esm/domain/exceptions/validation.exception.js +19 -0
- package/dist/esm/domain/index.d.ts +17 -0
- package/dist/esm/domain/index.js +12 -0
- package/dist/esm/domain/primitive/primitive-types.d.ts +20 -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 +18 -0
- package/dist/esm/domain/type/abstract-type.js +57 -0
- package/dist/esm/domain/type/array-type.d.ts +17 -0
- package/dist/esm/domain/type/array-type.js +60 -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-brand.d.ts +4 -0
- package/dist/esm/domain/type/composite/composite-brand.js +11 -0
- package/dist/esm/domain/type/composite/composite-field.d.ts +18 -0
- package/dist/esm/domain/type/composite/composite-field.js +82 -0
- package/dist/esm/domain/type/composite/composite-issues.d.ts +6 -0
- package/dist/esm/domain/type/composite/composite-issues.js +31 -0
- package/dist/esm/domain/type/composite/composite.d.ts +26 -0
- package/dist/esm/domain/type/composite/composite.js +48 -0
- package/dist/esm/domain/type/date-type.d.ts +19 -0
- package/dist/esm/domain/type/date-type.js +64 -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/id-type.d.ts +7 -0
- package/dist/esm/domain/type/id-type.js +13 -0
- package/dist/esm/domain/type/index.d.ts +15 -0
- package/dist/esm/domain/type/index.js +12 -0
- package/dist/esm/domain/type/json-type.d.ts +9 -0
- package/dist/esm/domain/type/json-type.js +42 -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 +8 -0
- package/dist/esm/domain/type/required/required-mark.js +9 -0
- package/dist/esm/domain/type/required/required-type.d.ts +7 -0
- package/dist/esm/domain/type/required/required-type.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/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 +46 -0
- package/dist/esm/infrastructure/exceptions/event-delivery.exception.d.ts +5 -0
- package/dist/esm/infrastructure/exceptions/event-delivery.exception.js +16 -0
- package/dist/esm/infrastructure/exceptions/infrastructure-exception-code.d.ts +6 -0
- package/dist/esm/infrastructure/exceptions/infrastructure-exception-code.js +7 -0
- package/dist/esm/infrastructure/exceptions/infrastructure.exception.d.ts +6 -0
- package/dist/esm/infrastructure/exceptions/infrastructure.exception.js +12 -0
- package/dist/esm/infrastructure/exceptions/internal-error.exception.d.ts +5 -0
- package/dist/esm/infrastructure/exceptions/internal-error.exception.js +13 -0
- package/dist/esm/infrastructure/exceptions/kind/exception-classifier.d.ts +4 -0
- package/dist/esm/infrastructure/exceptions/kind/exception-classifier.js +30 -0
- package/dist/esm/infrastructure/exceptions/kind/exception-kind.d.ts +9 -0
- package/dist/esm/infrastructure/exceptions/kind/exception-kind.js +10 -0
- package/dist/esm/infrastructure/exceptions/unknown-event.exception.d.ts +4 -0
- package/dist/esm/infrastructure/exceptions/unknown-event.exception.js +15 -0
- package/dist/esm/infrastructure/index.d.ts +21 -0
- package/dist/esm/infrastructure/index.js +15 -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/dist/esm/shared/exceptions/abstract.exception.d.ts +25 -0
- package/dist/esm/shared/exceptions/abstract.exception.js +55 -0
- package/dist/esm/shared/exceptions/brand/exception-brand.d.ts +6 -0
- package/dist/esm/shared/exceptions/brand/exception-brand.js +25 -0
- package/dist/esm/shared/exceptions/received/received-value.d.ts +4 -0
- package/dist/esm/shared/exceptions/received/received-value.js +9 -0
- package/dist/esm/shared/index.d.ts +8 -0
- package/dist/esm/shared/index.js +5 -0
- package/dist/esm/shared/issue-collection-error.d.ts +9 -0
- package/dist/esm/shared/issue-collection-error.js +7 -0
- package/dist/esm/shared/issue-collector.d.ts +8 -0
- package/dist/esm/shared/issue-collector.js +37 -0
- package/package.json +95 -4
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# Event Sourcing y CQRS
|
|
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.** Event Sourcing (agregados persistidos como secuencia de eventos, A+ES) y CQRS (modelo de comandos separado de modelos de lectura) con DDD en TypeScript. Empieza por decidir si compensan, y cubre stream de eventos por agregado, reconstitución, control de concurrencia con versión esperada, event store, snapshots, proyecciones y read models reconstruibles, consistencia eventual en la interfaz, enriquecimiento y versionado de eventos, y cómo cumplir el RGPD con eventos inmutables. Consúltala siempre que alguien mencione event sourcing, CQRS, read models, proyecciones, auditoría completa de cambios, "volver a un estado anterior", event store, o pantallas lentas que cruzan muchos agregados, aunque la pregunta sea solo "¿debería usar...?".
|
|
6
|
+
|
|
7
|
+
## Con @archi-code/ddd
|
|
8
|
+
|
|
9
|
+
[Fuera de los libros: convención de @archi-code/ddd]
|
|
10
|
+
|
|
11
|
+
* El paquete no trae event store, repositorios con Event Sourcing, snapshots ni proyecciones con checkpoint: se construyen en la infraestructura del proyecto sobre sus clases, y solo si la tabla de decisión de esta guía lo justifica.
|
|
12
|
+
* `AggregateRoot` no lleva versión: la de concurrencia optimista de los repositorios de instantáneas la recuerda cada repositorio en un `WeakMap` y crece 1 por guardado, no por evento. Un agregado con Event Sourcing lleva su propia versión de stream (`streamVersion()`), se reconstruye con `fromHistory` a partir de los eventos que reconstruye `EventRegistry` y no usa `toPrimitives()`/`fromPrimitives()`.
|
|
13
|
+
* El contrato del stream (`load`/`append`) lo declara el dominio y no es `AggregateFinder`/`AggregateSaver`; su adaptador es responsable de la persistencia: anexa los eventos de un `append` en una sola escritura atómica con la versión esperada y lanza `ConcurrencyException` si otro escribió antes. La aplicación no abre transacciones; el reintento ante `ConcurrencyException` va en el `handle` del caso de uso, cada intento vuelve a cargar y vuelve a anexar.
|
|
14
|
+
* Una proyección es un `EventListener` con `@ListensTo(Event, { consumer })` que llama a un puerto de proyección (no a un caso de uso), idempotente por `eventId`; un `EventChannel` no guarda historial, así que reconstruirla exige un log de eventos propio (con Event Sourcing, el propio event store).
|
|
15
|
+
* Una consulta de lectura es una interfaz en `application/` implementada en infraestructura directamente sobre la base de datos.
|
|
16
|
+
|
|
17
|
+
Dos patrones distintos que a menudo van juntos. **CQRS** separa el modelo que ejecuta comandos (agregados) de los modelos que responden consultas [IDDD cap. 4]. **Event Sourcing** guarda el estado de un agregado como la secuencia de eventos que le han ocurrido y lo reconstruye aplicándolos [Destilado cap. 6; IDDD apéndice A]. Event Sourcing casi siempre exige alguna forma de CQRS, porque los streams de eventos son difíciles de consultar [IDDD apéndice A; Destilado cap. 6]. CQRS no exige Event Sourcing.
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
## Paso 1. Decidir (la mayoría de las veces, no)
|
|
21
|
+
|
|
22
|
+
Ningún estilo es para todo: se usa donde mitiga un riesgo concreto [IDDD cap. 4].
|
|
23
|
+
|
|
24
|
+
### CQRS
|
|
25
|
+
|
|
26
|
+
| A favor | En contra |
|
|
27
|
+
|---|---|
|
|
28
|
+
| Vistas que cruzan muchos agregados y no encajan en repositorios [IDDD cap. 4] | Interfaz sencilla que sigue a un agregado: CQRS añade complejidad accidental [IDDD cap. 4] |
|
|
29
|
+
| Volumen de lectura muy superior al de escritura `[Fuera de los libros]` | Usuarios que necesitan ver al instante lo que acaban de escribir, sin margen de diseño |
|
|
30
|
+
| Muchos `findBy` en repositorios: señal de IDDD [IDDD cap. 12] | |
|
|
31
|
+
|
|
32
|
+
Antes de CQRS completo, prueba el paso intermedio: consultas SQL de solo lectura sobre las mismas tablas (`ddd-skills/ddd-arquitectura/GUIA.md`).
|
|
33
|
+
|
|
34
|
+
### Event Sourcing
|
|
35
|
+
|
|
36
|
+
Beneficios [IDDD apéndice A; Destilado cap. 6]:
|
|
37
|
+
* No se pierde el motivo de ningún cambio: auditoría completa, análisis, depuración mirando atrás.
|
|
38
|
+
* Escritura solo por anexión, muy rápida.
|
|
39
|
+
* Libertad estructural: la forma interna del agregado puede cambiar sin migrar datos; el stream se puede reproducir en otra infraestructura para depurar un error.
|
|
40
|
+
* Tiende a producir agregados más pequeños.
|
|
41
|
+
|
|
42
|
+
Inconvenientes que el propio apéndice reconoce [IDDD apéndice A]:
|
|
43
|
+
* Definir buenos eventos exige conocer el dominio a fondo; solo compensa en modelos complejos que dan ventaja competitiva.
|
|
44
|
+
* Obliga a CQRS: más carga cognitiva y curva de aprendizaje.
|
|
45
|
+
|
|
46
|
+
Recomendación de estas fuentes `[Fuera de los libros]`: Event Sourcing solo en agregados del Core Domain donde el historial es un requisito de negocio (o legal) o donde las preguntas "¿cómo llegó a este estado?" son frecuentes. No en todo el sistema. Si solo hace falta auditoría, el outbox de `ddd-skills/ddd-eventos-dominio/GUIA.md` ya deja un registro de eventos sin cambiar la persistencia. Con el paquete, un `EventChannel` entrega los eventos pero no los conserva: para auditar, tu canal duradero (o un listener) guarda cada `SerializedEvent` en una tabla propia que no se borre `[Fuera de los libros: convención de @archi-code/ddd]`.
|
|
47
|
+
|
|
48
|
+
## Paso 2. Implementar Event Sourcing
|
|
49
|
+
|
|
50
|
+
Guía completa en `ddd-skills/ddd-event-sourcing-cqrs/references/event-sourcing-ts.md`. Esquema:
|
|
51
|
+
|
|
52
|
+
1. El agregado (`extends AggregateRoot<PaymentId, PaymentEvent>`) separa **decidir** (métodos de comando que comprueban reglas y emiten eventos con `record`) de **aplicar** (un `apply` privado que cambia estado a partir de un evento). IDDD propone incluso dos clases, una de estado y otra de comportamiento, que solo colaboran a través de `apply` [IDDD apéndice A].
|
|
53
|
+
2. Cargar: leer el stream del agregado, reconstruir cada evento con `EventRegistry.fromJSON` y aplicarlos en orden (`Payment.fromHistory`).
|
|
54
|
+
3. Ejecutar el comando: genera eventos nuevos, que `pullDomainEvents()` devuelve como cambios pendientes.
|
|
55
|
+
4. Guardar: anexar los cambios con la **versión esperada** del stream (`streamVersion()`); si otro escribió antes, conflicto (`ConcurrencyException`) [IDDD apéndice A].
|
|
56
|
+
5. Ante conflicto: reintentar recargando y reejecutando, o propagar al cliente [IDDD apéndice A]. Con el paquete, el bucle va en el `handle` del caso de uso, con un límite de intentos, y cada intento vuelve a cargar el stream y vuelve a anexar; un caso de uso que lanza un listener no reintenta por sí mismo, lo hace la reentrega del canal.
|
|
57
|
+
6. Publicar: un proceso en segundo plano lee lo anexado y lo publica [IDDD apéndice A]. Con el paquete, el caso de uso publica con `EventBus` después de `append`; para no perder eventos si `publish` falla, el propio event store hace de bandeja (el `append` atómico ya los guardó) y un reenviador los entrega por un `EventChannel`.
|
|
58
|
+
|
|
59
|
+
Rendimiento: caché de streams en memoria (los eventos son inmutables) y **snapshots** para streams largos [IDDD apéndice A; Destilado cap. 6].
|
|
60
|
+
|
|
61
|
+
## Paso 3. Proyecciones y read models
|
|
62
|
+
|
|
63
|
+
Suscriptores que reciben eventos y mantienen modelos de lectura persistentes (con el paquete, `EventListener` suscritos al `DomainEventBus` e idempotentes por `eventId`); parecidos a un agregado, pero para consultar [IDDD apéndice A]. Se pueden **reconstruir** reproduciendo todos los eventos, lo que permite cambiar una vista sin tocar el agregado [IDDD apéndice A]. También sirven para dar a un agregado datos de otro de forma desacoplada, a través de un servicio de dominio que consulta la proyección [IDDD apéndice A].
|
|
64
|
+
|
|
65
|
+
Guía en `ddd-skills/ddd-event-sourcing-cqrs/references/proyecciones-cqrs-ts.md`.
|
|
66
|
+
|
|
67
|
+
## Paso 4. La interfaz con un modelo de lectura eventual
|
|
68
|
+
|
|
69
|
+
Diseña para el peor caso: la vista siguiente puede no reflejar todavía el comando [IDDD cap. 4]. Opciones de IDDD: mostrar temporalmente lo enviado; mostrar la antigüedad de los datos; actualizaciones push; o avisar de que la operación se está procesando [IDDD cap. 4].
|
|
70
|
+
|
|
71
|
+
## Paso 5. Eventos con doble función
|
|
72
|
+
|
|
73
|
+
En Event Sourcing los eventos sirven para persistir el agregado **y** para comunicar al resto de la empresa. Eso crea tensión: lo que necesita el agregado no es lo que necesitan los consumidores. IDDD lo trata como enriquecimiento de eventos [IDDD apéndice A]. Pauta `[Fuera de los libros]`: eventos internos del stream con lo mínimo para reconstruir, y eventos publicados traducidos al Published Language (igual que en `ddd-skills/ddd-eventos-dominio/GUIA.md`).
|
|
74
|
+
|
|
75
|
+
## Datos personales e inmutabilidad
|
|
76
|
+
|
|
77
|
+
`[Fuera de los libros]` Un stream inmutable choca con el derecho de supresión del RGPD. Opciones: no guardar datos personales en los eventos (solo identificadores y los datos en un almacén borrable); o cifrar los datos personales con una clave por persona y destruir la clave al suprimir ("crypto-shredding"). Decide esto antes de escribir el primer evento.
|
|
78
|
+
|
|
79
|
+
## Señales de alarma
|
|
80
|
+
|
|
81
|
+
* Event Sourcing en todo el sistema "por si acaso".
|
|
82
|
+
* Eventos tipo `OrderUpdated` con el estado completo en el stream.
|
|
83
|
+
* Proyecciones que no se pueden reconstruir (dependen de estado externo no reproducible).
|
|
84
|
+
* Proyecciones no idempotentes (sin comprobar el `eventId` ya aplicado).
|
|
85
|
+
* Un agregado con Event Sourcing que usa la versión de guardados de un repositorio de instantáneas como versión de stream o se guarda como instantánea con `toPrimitives()`.
|
|
86
|
+
* Agregados que consultan read models para proteger invariantes (las invariantes viven en el agregado).
|
|
87
|
+
* Datos personales en eventos sin estrategia de supresión.
|
|
88
|
+
* CQRS con dos bases de datos para un CRUD de cinco pantallas.
|
|
89
|
+
|
|
90
|
+
## Siguiente guía
|
|
91
|
+
|
|
92
|
+
* Revisar un sistema existente → `ddd-skills/ddd-auditoria-refactorizacion/GUIA.md`.
|
|
@@ -0,0 +1,476 @@
|
|
|
1
|
+
# Event Sourcing en TypeScript
|
|
2
|
+
|
|
3
|
+
`@archi-code/ddd` no trae event store, repositorios con Event Sourcing ni snapshots: todo lo de esta referencia es código del proyecto escrito sobre las clases del paquete (`AggregateRoot`, `EventBase`, `EventRegistry`, `UseCase`, `EventBus`, `ConcurrencyException`). Úsalo solo en los agregados que la tabla de decisión de `ddd-skills/ddd-event-sourcing-cqrs/GUIA.md` justifique.
|
|
4
|
+
|
|
5
|
+
## Eventos del stream
|
|
6
|
+
|
|
7
|
+
Cada evento es una subclase de `EventBase` con `static readonly eventName` y, como campos `readonly` del constructor, el id del agregado y los datos primitivos que necesita, tomados de los value objects (el dinero como `{ amount, currency }`), nunca value objects: el evento no tiene métodos y se serializa tal cual (`{ ...event }`) [IDDD cap. 8].
|
|
8
|
+
|
|
9
|
+
```ts
|
|
10
|
+
import { EventBase } from '@archi-code/ddd/domain';
|
|
11
|
+
import type { EventMetadata } from '@archi-code/ddd/domain';
|
|
12
|
+
|
|
13
|
+
export class PaymentRequested extends EventBase {
|
|
14
|
+
static readonly eventName = 'PaymentRequested';
|
|
15
|
+
|
|
16
|
+
constructor(
|
|
17
|
+
readonly paymentId: string,
|
|
18
|
+
readonly orderId: string,
|
|
19
|
+
readonly amount: { amount: number; currency: string },
|
|
20
|
+
metadata?: EventMetadata,
|
|
21
|
+
) {
|
|
22
|
+
super(metadata);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export class PaymentAuthorized extends EventBase {
|
|
27
|
+
static readonly eventName = 'PaymentAuthorized';
|
|
28
|
+
|
|
29
|
+
constructor(
|
|
30
|
+
readonly paymentId: string,
|
|
31
|
+
readonly authorizationCode: string,
|
|
32
|
+
readonly expiresAt: string,
|
|
33
|
+
metadata?: EventMetadata,
|
|
34
|
+
) {
|
|
35
|
+
super(metadata);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export class PaymentRejected extends EventBase {
|
|
40
|
+
static readonly eventName = 'PaymentRejected';
|
|
41
|
+
|
|
42
|
+
constructor(
|
|
43
|
+
readonly paymentId: string,
|
|
44
|
+
readonly reason: string,
|
|
45
|
+
metadata?: EventMetadata,
|
|
46
|
+
) {
|
|
47
|
+
super(metadata);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export class PaymentCaptured extends EventBase {
|
|
52
|
+
static readonly eventName = 'PaymentCaptured';
|
|
53
|
+
|
|
54
|
+
constructor(
|
|
55
|
+
readonly paymentId: string,
|
|
56
|
+
metadata?: EventMetadata,
|
|
57
|
+
) {
|
|
58
|
+
super(metadata);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export type PaymentEvent = PaymentRequested | PaymentAuthorized | PaymentRejected | PaymentCaptured;
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
`EventBase` pone `eventName`, `eventId` y `occurredOn` (texto ISO en UTC, del `clock` que recibe el método del agregado); la identidad del pago es un campo más del evento (`paymentId`), declarado por cada clase. `{ ...event }` da `eventName`, `eventId`, `occurredOn` y los campos, todos datos primitivos (regla del paquete, no comprobada en ejecución): esa es la fila del stream.
|
|
66
|
+
|
|
67
|
+
## Agregado con decidir y aplicar
|
|
68
|
+
|
|
69
|
+
`[Fuera de los libros como código; estructura basada en IDDD apéndice A]`
|
|
70
|
+
|
|
71
|
+
```ts
|
|
72
|
+
import {
|
|
73
|
+
AggregateRoot,
|
|
74
|
+
Composite,
|
|
75
|
+
DateType,
|
|
76
|
+
EnumType,
|
|
77
|
+
IdType,
|
|
78
|
+
NumberType,
|
|
79
|
+
Required,
|
|
80
|
+
StringType,
|
|
81
|
+
} from '@archi-code/ddd/domain';
|
|
82
|
+
import { DomainException } from '@archi-code/ddd/domain';
|
|
83
|
+
import { Validations } from '@archi-code/validation';
|
|
84
|
+
|
|
85
|
+
export class PaymentId extends IdType<'PaymentId'> {}
|
|
86
|
+
export class OrderId extends IdType<'OrderId'> {}
|
|
87
|
+
export class AuthorizationCode extends Required(StringType) {}
|
|
88
|
+
export class RejectionReason extends Required(StringType) {}
|
|
89
|
+
export class ExpiresAt extends Required(DateType) {}
|
|
90
|
+
|
|
91
|
+
@Validations([{ validator: 'isInt' }])
|
|
92
|
+
export class Cents extends Required(NumberType) {}
|
|
93
|
+
@Validations([{ validator: 'isISO4217' }])
|
|
94
|
+
export class Currency extends Required(StringType) {}
|
|
95
|
+
export class Money extends Composite({ amount: Cents, currency: Currency }) {}
|
|
96
|
+
|
|
97
|
+
export enum Status {
|
|
98
|
+
Requested = 'requested',
|
|
99
|
+
Authorized = 'authorized',
|
|
100
|
+
Rejected = 'rejected',
|
|
101
|
+
Captured = 'captured',
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
class StatusType extends EnumType<Status> {
|
|
105
|
+
protected getEnum(): typeof Status {
|
|
106
|
+
return Status;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export class PaymentStatus extends Required(StatusType) {
|
|
111
|
+
is(status: Status): boolean {
|
|
112
|
+
return this.value === status;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export class PaymentNotAuthorized extends DomainException {
|
|
117
|
+
constructor(paymentId: string) {
|
|
118
|
+
super(`Payment ${paymentId} is not authorized`, ['PAY002']);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export class AuthorizationExpired extends DomainException {
|
|
123
|
+
constructor(paymentId: string) {
|
|
124
|
+
super(`The authorization of payment ${paymentId} has expired`, ['PAY003']);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export class PaymentAlreadyDecided extends DomainException {
|
|
129
|
+
constructor(paymentId: string) {
|
|
130
|
+
super(`Payment ${paymentId} was already authorized or rejected`, ['PAY004']);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export class Payment extends AggregateRoot<PaymentId, PaymentEvent> {
|
|
135
|
+
private status = new PaymentStatus(Status.Requested);
|
|
136
|
+
private amount: Money | null = null;
|
|
137
|
+
private expiresAt: ExpiresAt | null = null;
|
|
138
|
+
private replayed = 0;
|
|
139
|
+
|
|
140
|
+
private constructor(id: PaymentId) {
|
|
141
|
+
super(id);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
static request(id: PaymentId, orderId: OrderId, amount: Money, clock: { now(): Date }): Payment {
|
|
145
|
+
const payment = new Payment(id);
|
|
146
|
+
payment.emit(
|
|
147
|
+
new PaymentRequested(
|
|
148
|
+
id.value,
|
|
149
|
+
orderId.value,
|
|
150
|
+
{ amount: amount.amount.value, currency: amount.currency.value },
|
|
151
|
+
{ clock },
|
|
152
|
+
),
|
|
153
|
+
);
|
|
154
|
+
return payment;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
static fromHistory(id: PaymentId, history: readonly PaymentEvent[]): Payment {
|
|
158
|
+
const payment = new Payment(id);
|
|
159
|
+
history.forEach((event) => payment.apply(event));
|
|
160
|
+
payment.replayed = history.length;
|
|
161
|
+
return payment;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
streamVersion(): number {
|
|
165
|
+
return this.replayed;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
authorize(code: AuthorizationCode, expiresAt: ExpiresAt, clock: { now(): Date }): void {
|
|
169
|
+
if (!this.status.is(Status.Requested)) {
|
|
170
|
+
throw new PaymentAlreadyDecided(this.id.value);
|
|
171
|
+
}
|
|
172
|
+
this.emit(
|
|
173
|
+
new PaymentAuthorized(this.id.value, code.value, expiresAt.value.toISOString(), { clock }),
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
reject(reason: RejectionReason, clock: { now(): Date }): void {
|
|
178
|
+
if (!this.status.is(Status.Requested)) {
|
|
179
|
+
throw new PaymentAlreadyDecided(this.id.value);
|
|
180
|
+
}
|
|
181
|
+
this.emit(new PaymentRejected(this.id.value, reason.value, { clock }));
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
capture(clock: { now(): Date }): void {
|
|
185
|
+
if (!this.status.is(Status.Authorized) || this.expiresAt === null) {
|
|
186
|
+
throw new PaymentNotAuthorized(this.id.value);
|
|
187
|
+
}
|
|
188
|
+
if (this.expiresAt.value < clock.now()) {
|
|
189
|
+
throw new AuthorizationExpired(this.id.value);
|
|
190
|
+
}
|
|
191
|
+
this.emit(new PaymentCaptured(this.id.value, { clock }));
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
private emit(event: PaymentEvent): void {
|
|
195
|
+
this.apply(event);
|
|
196
|
+
this.record(event);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
private apply(event: PaymentEvent): void {
|
|
200
|
+
if (event instanceof PaymentRequested) {
|
|
201
|
+
this.amount = Money.fromPrimitives(event.amount);
|
|
202
|
+
} else if (event instanceof PaymentAuthorized) {
|
|
203
|
+
this.status = new PaymentStatus(Status.Authorized);
|
|
204
|
+
this.expiresAt = new ExpiresAt(new Date(event.expiresAt));
|
|
205
|
+
} else if (event instanceof PaymentRejected) {
|
|
206
|
+
this.status = new PaymentStatus(Status.Rejected);
|
|
207
|
+
} else {
|
|
208
|
+
this.status = new PaymentStatus(Status.Captured);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
Reglas:
|
|
215
|
+
* `apply` no valida ni lanza: los eventos son hechos ya ocurridos [Destilado cap. 6].
|
|
216
|
+
* Las reglas viven en los métodos de comando (`authorize`, `capture`), antes de emitir, y lanzan subclases de `DomainException` con su código.
|
|
217
|
+
* El estado solo cambia vía eventos: `emit` aplica y después llama a `record` (protegido en `AggregateRoot`, congela el evento), así que `pullDomainEvents()` devuelve exactamente los cambios pendientes, en orden.
|
|
218
|
+
* `fromHistory` aplica sin `record`: reconstituir no genera eventos nuevos.
|
|
219
|
+
* El reloj se recibe como `{ now(): Date }` y se pasa a cada evento (`{ clock }`), para que reconstruir no dependa de la hora actual; en pruebas, `FixedClock`.
|
|
220
|
+
* IDDD propone separar estado y comportamiento en dos clases que solo colaboran a través de `apply` [IDDD apéndice A]; aquí el estado son campos privados de value objects que solo `apply` escribe. Si el agregado crece, extrae ese estado a una clase propia con su `apply`.
|
|
221
|
+
* `[Fuera de los libros: convención de @archi-code/ddd]` `AggregateRoot` no tiene versión, y la que recuerda un repositorio de instantáneas en su `WeakMap` cuenta guardados, no eventos (crece 1 por `save`), así que este agregado lleva su propia versión de stream (`streamVersion()`, los eventos que se aplicaron al cargar) y no se guarda como instantánea: no necesita `toPrimitives()`/`fromPrimitives()` ni getters para sus campos, porque lo que se persiste son los eventos.
|
|
222
|
+
|
|
223
|
+
## Event store
|
|
224
|
+
|
|
225
|
+
Un stream por agregado, identificado por la identidad de la raíz; almacenamiento solo de anexión con cualquier tecnología que ofrezca consistencia fuerte [IDDD apéndice A; Destilado cap. 6].
|
|
226
|
+
|
|
227
|
+
```sql
|
|
228
|
+
CREATE TABLE event_streams (
|
|
229
|
+
stream_id TEXT NOT NULL,
|
|
230
|
+
version INT NOT NULL,
|
|
231
|
+
body JSONB NOT NULL,
|
|
232
|
+
global_seq BIGSERIAL UNIQUE,
|
|
233
|
+
PRIMARY KEY (stream_id, version)
|
|
234
|
+
);
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
La clave primaria `(stream_id, version)` impide dos escrituras con la misma versión; `global_seq` da el orden global que necesitan la publicación y la reconstrucción de proyecciones. `body` es la copia plana del evento (`{ ...event }`).
|
|
238
|
+
|
|
239
|
+
El dominio declara el contrato del stream (solo tipos, como los contratos de repositorio del paquete) `[Fuera de los libros: convención de @archi-code/ddd]`: no es `AggregateFinder`/`AggregateSaver`, porque guardar necesita los cambios que el caso de uso ya sacó con `pullDomainEvents()` para publicarlos.
|
|
240
|
+
|
|
241
|
+
```ts
|
|
242
|
+
export interface PaymentStream {
|
|
243
|
+
load(id: PaymentId): Promise<Payment | null>;
|
|
244
|
+
append(payment: Payment, changes: readonly PaymentEvent[]): Promise<void>;
|
|
245
|
+
}
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
La infraestructura lo implementa: reconstruye cada fila con `EventRegistry.fromJSON` (que conserva `eventId` y `occurredOn` y congela el evento), anexa todos los eventos de un `append` en una sola transacción que abre y cierra con su propia conexión, y traduce la violación de la clave primaria a `ConcurrencyException`. Como cualquier repositorio del paquete, el stream es responsable de todo lo de persistencia: la escritura atómica del stream con su versión esperada, la conexión y los errores del driver `[Fuera de los libros: convención de @archi-code/ddd]`.
|
|
249
|
+
|
|
250
|
+
```ts
|
|
251
|
+
import type { SerializedEvent } from '@archi-code/ddd/domain';
|
|
252
|
+
import { EventRegistry, InternalErrorException } from '@archi-code/ddd/infrastructure';
|
|
253
|
+
import { ConcurrencyException } from '@archi-code/ddd/application';
|
|
254
|
+
|
|
255
|
+
export const paymentEvents = new EventRegistry<PaymentEvent>()
|
|
256
|
+
.register(
|
|
257
|
+
PaymentRequested,
|
|
258
|
+
(json) =>
|
|
259
|
+
new PaymentRequested(
|
|
260
|
+
json.paymentId as string,
|
|
261
|
+
json.orderId as string,
|
|
262
|
+
json.amount as { amount: number; currency: string },
|
|
263
|
+
json,
|
|
264
|
+
),
|
|
265
|
+
)
|
|
266
|
+
.register(
|
|
267
|
+
PaymentAuthorized,
|
|
268
|
+
(json) =>
|
|
269
|
+
new PaymentAuthorized(
|
|
270
|
+
json.paymentId as string,
|
|
271
|
+
json.authorizationCode as string,
|
|
272
|
+
json.expiresAt as string,
|
|
273
|
+
json,
|
|
274
|
+
),
|
|
275
|
+
)
|
|
276
|
+
.register(
|
|
277
|
+
PaymentRejected,
|
|
278
|
+
(json) => new PaymentRejected(json.paymentId as string, json.reason as string, json),
|
|
279
|
+
)
|
|
280
|
+
.register(PaymentCaptured, (json) => new PaymentCaptured(json.paymentId as string, json));
|
|
281
|
+
|
|
282
|
+
interface SqlClient {
|
|
283
|
+
query<Row>(text: string, values: readonly unknown[]): Promise<{ rows: Row[] }>;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
interface PoolClient extends SqlClient {
|
|
287
|
+
release(): void;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
interface Pool extends SqlClient {
|
|
291
|
+
connect(): Promise<PoolClient>;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
export class PgPaymentStream implements PaymentStream {
|
|
295
|
+
constructor(
|
|
296
|
+
private readonly pool: Pool,
|
|
297
|
+
private readonly registry: EventRegistry<PaymentEvent>,
|
|
298
|
+
) {}
|
|
299
|
+
|
|
300
|
+
async load(id: PaymentId): Promise<Payment | null> {
|
|
301
|
+
const { rows } = await this.pool.query<{ body: SerializedEvent }>(
|
|
302
|
+
'SELECT body FROM event_streams WHERE stream_id = $1 ORDER BY version',
|
|
303
|
+
[id.value],
|
|
304
|
+
);
|
|
305
|
+
if (rows.length === 0) {
|
|
306
|
+
return null;
|
|
307
|
+
}
|
|
308
|
+
return Payment.fromHistory(
|
|
309
|
+
id,
|
|
310
|
+
rows.map((row) => this.registry.fromJSON(row.body)),
|
|
311
|
+
);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
async append(payment: Payment, changes: readonly PaymentEvent[]): Promise<void> {
|
|
315
|
+
const expected = payment.streamVersion();
|
|
316
|
+
const client = await this.pool.connect();
|
|
317
|
+
try {
|
|
318
|
+
await client.query('BEGIN', []);
|
|
319
|
+
for (const [index, event] of changes.entries()) {
|
|
320
|
+
await client.query(
|
|
321
|
+
'INSERT INTO event_streams (stream_id, version, body) VALUES ($1, $2, $3)',
|
|
322
|
+
[payment.id.value, expected + index + 1, JSON.stringify(event)],
|
|
323
|
+
);
|
|
324
|
+
}
|
|
325
|
+
await client.query('COMMIT', []);
|
|
326
|
+
} catch (error) {
|
|
327
|
+
await client.query('ROLLBACK', []);
|
|
328
|
+
if ((error as { code?: string }).code === '23505') {
|
|
329
|
+
throw new ConcurrencyException('Payment', payment.id.value, expected, { cause: error });
|
|
330
|
+
}
|
|
331
|
+
throw new InternalErrorException('appending payment events', { cause: error });
|
|
332
|
+
} finally {
|
|
333
|
+
client.release();
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
```
|
|
338
|
+
|
|
339
|
+
`Pool` y `SqlClient` son la parte de `pg` que usa el adaptador, declarada de forma estructural. La transacción empieza y termina dentro de `append`: o entran todos los eventos del cambio o ninguno, y la aplicación no ve ninguna transacción. `'23505'` es el código de violación de unicidad de PostgreSQL; con otro motor, comprueba el suyo.
|
|
340
|
+
|
|
341
|
+
## Caso de uso y reintento
|
|
342
|
+
|
|
343
|
+
El caso de uso carga, decide, anexa y publica; no abre transacciones ni recibe un puerto para ello. Ante un conflicto puede reintentar recargando y reejecutando el comando contra el estado nuevo, como propone IDDD [IDDD apéndice A]; si el comando ya no es válido, el error de negocio sale normalmente. `[Fuera de los libros: convención de @archi-code/ddd]` El stream solo carga y anexa; el bucle de reintento va en el `handle` del caso de uso, cada intento vuelve a cargar y vuelve a anexar, unos pocos intentos y después relanza la `ConcurrencyException` (`ExceptionKind.Conflict`, 409 en HTTP). Reintenta solo comandos seguros de repetir; un caso de uso que lanza un listener no lleva este bucle: el reintento lo hace la reentrega del canal.
|
|
344
|
+
|
|
345
|
+
```ts
|
|
346
|
+
import { AggregateNotFoundException, ConcurrencyException, UseCase } from '@archi-code/ddd/application';
|
|
347
|
+
import type { Clock, EventBus } from '@archi-code/ddd/application';
|
|
348
|
+
|
|
349
|
+
export class CapturePayment extends UseCase<{ paymentId: string }, PaymentId, void> {
|
|
350
|
+
constructor(
|
|
351
|
+
private readonly payments: PaymentStream,
|
|
352
|
+
private readonly events: EventBus<PaymentEvent>,
|
|
353
|
+
private readonly clock: Clock,
|
|
354
|
+
) {
|
|
355
|
+
super();
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
protected toCommand(input: { paymentId: string }): PaymentId {
|
|
359
|
+
return new PaymentId(input.paymentId);
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
protected async handle(paymentId: PaymentId): Promise<void> {
|
|
363
|
+
for (let attempt = 1; ; attempt += 1) {
|
|
364
|
+
try {
|
|
365
|
+
const payment = await this.payments.load(paymentId);
|
|
366
|
+
if (payment === null) {
|
|
367
|
+
throw new AggregateNotFoundException('Payment', paymentId.value);
|
|
368
|
+
}
|
|
369
|
+
payment.capture(this.clock);
|
|
370
|
+
const changes = payment.pullDomainEvents();
|
|
371
|
+
await this.payments.append(payment, changes);
|
|
372
|
+
await this.events.publish(changes);
|
|
373
|
+
return;
|
|
374
|
+
} catch (error) {
|
|
375
|
+
if (!(error instanceof ConcurrencyException) || attempt === 3) {
|
|
376
|
+
throw error;
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
```
|
|
383
|
+
|
|
384
|
+
Publicar: IDDD publica con un proceso en segundo plano que lee lo anexado [IDDD apéndice A]. Con el paquete, el caso de uso solo ve el puerto `EventBus`; el `DomainEventBus` de la composición envía a un `EventChannel`. Si `publish` falla después de `append`, los eventos ya están en el stream pero no se entregan. Para no perder ninguno, el propio `event_streams` hace de bandeja, porque `append` ya los guardó en su escritura atómica: un reenviador lee por `global_seq` y entrega los `SerializedEvent` a los manejadores que registró `receive`, y el canal que recibe el bus del caso de uso no escribe nada en su `send` (los eventos ya están guardados) y delega `receive` en el canal del reenviador. Es persistencia y entrega, así que vive en la infraestructura; los casos de uso no cambian `[Fuera de los libros: convención de @archi-code/ddd]`.
|
|
385
|
+
|
|
386
|
+
## Pruebas en memoria
|
|
387
|
+
|
|
388
|
+
Una implementación en memoria del stream sirve como doble de pruebas y como referencia del contrato; anexa todos los cambios de una vez o ninguno, como `PgPaymentStream`:
|
|
389
|
+
|
|
390
|
+
```ts
|
|
391
|
+
import { DomainEventBus, FixedClock, InMemoryEventChannel } from '@archi-code/ddd/infrastructure';
|
|
392
|
+
|
|
393
|
+
export class InMemoryPaymentStream implements PaymentStream {
|
|
394
|
+
private readonly streams = new Map<string, readonly SerializedEvent[]>();
|
|
395
|
+
|
|
396
|
+
constructor(private readonly registry: EventRegistry<PaymentEvent>) {}
|
|
397
|
+
|
|
398
|
+
async load(id: PaymentId): Promise<Payment | null> {
|
|
399
|
+
const stream = this.streams.get(id.value);
|
|
400
|
+
if (stream === undefined) {
|
|
401
|
+
return null;
|
|
402
|
+
}
|
|
403
|
+
return Payment.fromHistory(
|
|
404
|
+
id,
|
|
405
|
+
stream.map((json) => this.registry.fromJSON(json)),
|
|
406
|
+
);
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
async append(payment: Payment, changes: readonly PaymentEvent[]): Promise<void> {
|
|
410
|
+
const stream = this.streams.get(payment.id.value) ?? [];
|
|
411
|
+
if (stream.length !== payment.streamVersion()) {
|
|
412
|
+
throw new ConcurrencyException('Payment', payment.id.value, payment.streamVersion());
|
|
413
|
+
}
|
|
414
|
+
this.streams.set(payment.id.value, [...stream, ...changes.map((event) => ({ ...event }))]);
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
const clock = new FixedClock('2026-10-19T09:30:00Z');
|
|
419
|
+
const payments = new InMemoryPaymentStream(paymentEvents);
|
|
420
|
+
const channel = new InMemoryEventChannel();
|
|
421
|
+
const bus = new DomainEventBus<PaymentEvent>(channel, paymentEvents);
|
|
422
|
+
|
|
423
|
+
const paymentId = PaymentId.generate();
|
|
424
|
+
const payment = Payment.request(
|
|
425
|
+
paymentId,
|
|
426
|
+
OrderId.generate(),
|
|
427
|
+
Money.fromPrimitives({ amount: 4990, currency: 'EUR' }),
|
|
428
|
+
clock,
|
|
429
|
+
);
|
|
430
|
+
payment.authorize(
|
|
431
|
+
new AuthorizationCode('AUTH-7731'),
|
|
432
|
+
new ExpiresAt(new Date('2026-10-26T09:30:00Z')),
|
|
433
|
+
clock,
|
|
434
|
+
);
|
|
435
|
+
await payments.append(payment, payment.pullDomainEvents());
|
|
436
|
+
|
|
437
|
+
await new CapturePayment(payments, bus, clock).execute({
|
|
438
|
+
paymentId: paymentId.value,
|
|
439
|
+
});
|
|
440
|
+
channel.sent.map((event) => event.eventName); // ['PaymentCaptured']
|
|
441
|
+
(await payments.load(paymentId))?.streamVersion(); // 3
|
|
442
|
+
```
|
|
443
|
+
|
|
444
|
+
Ejecuta las mismas pruebas de contrato (carga lo anexado, rechaza una versión esperada antigua con `ConcurrencyException`) contra `InMemoryPaymentStream` y `PgPaymentStream`, como el contrato de repositorio de `ddd-skills/ddd-repositorios-persistencia/GUIA.md`.
|
|
445
|
+
|
|
446
|
+
## Snapshots
|
|
447
|
+
|
|
448
|
+
Para streams largos, guarda periódicamente el estado serializado con su versión y carga desde ahí más los eventos posteriores [IDDD apéndice A; Destilado cap. 6]. Los snapshots son una optimización desechable: si cambia la forma del estado, se borran y se regeneran. El paquete no los trae: son una tabla más de tu infraestructura (`stream_id`, `version`, `state`) y una fábrica del agregado que parte del estado y aplica solo los eventos con versión mayor, dejando `streamVersion()` en la versión del último evento aplicado.
|
|
449
|
+
|
|
450
|
+
## Versionado de eventos
|
|
451
|
+
|
|
452
|
+
`[Fuera de los libros]` Los eventos del stream no se reescriben. Para cambiar su forma, convierte las versiones antiguas al leer ("upcasting") en el deserializador, o añade un tipo de evento nuevo. El apéndice de IDDD trata los serializadores (por ejemplo Protocol Buffers) como apoyo para esta evolución [IDDD apéndice A].
|
|
453
|
+
|
|
454
|
+
Con el paquete, los eventos no llevan número de versión: la fábrica del `EventRegistry` recibe cada fila tal como se guardó, así que el upcasting va ahí y reconoce la forma antigua por sus campos; un cambio que no se pueda leer así se publica como un evento nuevo con otro `eventName` (`PaymentRequestedV2`). Por ejemplo, si una primera forma de `PaymentRequested` guardaba `amountCents` en euros:
|
|
455
|
+
|
|
456
|
+
```ts
|
|
457
|
+
export class PaymentRequested extends EventBase {
|
|
458
|
+
static readonly eventName = 'PaymentRequested';
|
|
459
|
+
// constructor as above
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
new EventRegistry<PaymentEvent>().register(
|
|
463
|
+
PaymentRequested,
|
|
464
|
+
(json) =>
|
|
465
|
+
new PaymentRequested(
|
|
466
|
+
json.paymentId as string,
|
|
467
|
+
json.orderId as string,
|
|
468
|
+
typeof json.amountCents === 'number'
|
|
469
|
+
? { amount: json.amountCents, currency: 'EUR' }
|
|
470
|
+
: (json.amount as { amount: number; currency: string }),
|
|
471
|
+
json,
|
|
472
|
+
),
|
|
473
|
+
);
|
|
474
|
+
```
|
|
475
|
+
|
|
476
|
+
Pasa siempre `json` como último argumento: el evento reconstruido conserva su `eventId` y su `occurredOn`, y `fromJSON` lanza `InternalErrorException` si la fábrica los pierde.
|