@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
package/SKILL.md
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: archi-code-ddd
|
|
3
|
+
description: The single entry point for Domain-Driven Design with @archi-code/ddd. It routes every DDD question to the guide suite shipped in the package (integration between contexts, architecture, value objects and entities, aggregates, domain events, repositories, application services, event sourcing and CQRS, audits) and gives the rules, patterns and common mistakes for TypeScript code with @archi-code/ddd - where each class goes and what it may import, value objects, Composite, Specification, Entity, AggregateRoot, EventBase, repository roles, exceptions, UseCase, the event contract (EventListener, @ListensTo, DomainEventBus, event channels), ports, persistence owned by the repository and HttpErrorMapper. Use it whenever someone applies DDD, designs or reviews a domain model, integrates contexts, writes or changes value objects, aggregates, events, use cases, listeners, repositories or adapters, decides which layer a class belongs to, or whenever the code imports @archi-code/ddd.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# archi-code-ddd
|
|
7
|
+
|
|
8
|
+
`@archi-code/ddd` is archi-code's Domain-Driven Design package for TypeScript, one entry point per
|
|
9
|
+
layer: `@archi-code/ddd/exceptions` (the exceptions every layer throws), `@archi-code/ddd/domain`
|
|
10
|
+
(value objects, `Composite`, `Immutable`, `Specification`, `Entity`, `AggregateRoot`, `EventBase`
|
|
11
|
+
and the repository contracts `AggregateFinder`, `AggregateSaver`, `AggregateRemover`),
|
|
12
|
+
`@archi-code/ddd/application` (`UseCase`, the event contract to listen, `EventListener` and
|
|
13
|
+
`@ListensTo`, and to publish, `EventBus`, and the ports `Clock`, `Logger`) and
|
|
14
|
+
`@archi-code/ddd/infrastructure` (aggregate mapper for your repositories, the
|
|
15
|
+
event wiring `DomainEventBus`, `EventRegistry` and the `EventChannel` port, in-memory event
|
|
16
|
+
channel, HTTP error mapper, clocks, loggers).
|
|
17
|
+
|
|
18
|
+
## Full API
|
|
19
|
+
|
|
20
|
+
Before using a class, function, option or type that does not appear in this skill, read
|
|
21
|
+
`node_modules/@archi-code/ddd/README.md`: it matches the installed version. Do not assume
|
|
22
|
+
signatures, options or exports. If the package is not installed: `npm install @archi-code/ddd`
|
|
23
|
+
(and `npm install @archi-code/validation` if validations are declared with `@Validations`).
|
|
24
|
+
|
|
25
|
+
If that path does not exist (monorepo with workspaces, pnpm…), locate the package with
|
|
26
|
+
`node -p "require.resolve('@archi-code/ddd/package.json')"` and read the `README.md` in that
|
|
27
|
+
folder.
|
|
28
|
+
|
|
29
|
+
## The DDD guide suite
|
|
30
|
+
|
|
31
|
+
This is the **only skill to load** for Domain-Driven Design with this package. It brings a suite
|
|
32
|
+
of DDD guides (in Spanish, with the source of each rule: Vernon's _Implementing Domain-Driven
|
|
33
|
+
Design_ and _Domain-Driven Design Distilled_, and _DDD in PHP_) inside the package, in
|
|
34
|
+
`ddd-skills/`; never copy the suite into `.claude/skills/`: read it from the package when this
|
|
35
|
+
skill says so. Locate it as the README: `node_modules/@archi-code/ddd/ddd-skills/`, or the
|
|
36
|
+
`ddd-skills/` folder next to the `package.json` that
|
|
37
|
+
`node -p "require.resolve('@archi-code/ddd/package.json')"` prints (in this monorepo,
|
|
38
|
+
`packages/ddd/ddd-skills/`). Each guide is a folder with a `GUIA.md` (no skill frontmatter), its
|
|
39
|
+
`references/` and, when it has them, `assets/` (tool configuration) and `scripts/`: they are never loaded
|
|
40
|
+
on their own, only read when this skill routes to them. Paths inside the guides (`ddd-skills/ddd-agregados/GUIA.md`) are relative to the package
|
|
41
|
+
folder.
|
|
42
|
+
|
|
43
|
+
How to use it, for every design question or task:
|
|
44
|
+
|
|
45
|
+
1. **Start at `ddd-skills/ddd-orquestador/GUIA.md`**: it says which guide covers each coding task
|
|
46
|
+
(integration between contexts, architecture, classes, aggregates, events, repositories, use
|
|
47
|
+
cases) and holds the conventions of the suite; read it for every task that is not a quick code
|
|
48
|
+
fix. Its `references/chuleta-reglas.md` has every essential rule with its source.
|
|
49
|
+
2. **Read the `GUIA.md` of the matching guide** before answering, and the `references/` it points
|
|
50
|
+
to for the case at hand:
|
|
51
|
+
|
|
52
|
+
| Situation | Read `ddd-skills/…/GUIA.md` |
|
|
53
|
+
| ---------------------------------------------------------------------- | ------------------------------- |
|
|
54
|
+
| Unclear request, "where do I start", the essential rules | `ddd-orquestador` |
|
|
55
|
+
| How contexts and external systems relate and integrate, ACL | `ddd-context-mapping` |
|
|
56
|
+
| Folder structure, layers, dependencies, modules, composition root | `ddd-arquitectura` |
|
|
57
|
+
| Value object or entity, ids, primitives, validation, Specification | `ddd-value-objects-entidades` |
|
|
58
|
+
| What goes together, invariants, transactions, concurrency, factories | `ddd-agregados` |
|
|
59
|
+
| Events, messaging, outbox, idempotence, versions | `ddd-eventos-dominio` |
|
|
60
|
+
| Loading and saving aggregates, mappers, transactions, repository tests | `ddd-repositorios-persistencia` |
|
|
61
|
+
| Domain services, use cases, where the logic goes, authorization | `ddd-servicios-capa-aplicacion` |
|
|
62
|
+
| Full history, read models, CQRS, Event Sourcing | `ddd-event-sourcing-cqrs` |
|
|
63
|
+
| Review or migrate existing code | `ddd-auditoria-refactorizacion` |
|
|
64
|
+
|
|
65
|
+
A new project follows the orchestrator's sequence (architecture → aggregates and value objects →
|
|
66
|
+
events → repositories → use cases).
|
|
67
|
+
|
|
68
|
+
3. **Design with the suite, write the code with this package.** The guides' examples are already
|
|
69
|
+
written with this package's API (the classes of its `src`): copy them as they are, never write
|
|
70
|
+
generic base classes (`ValueObject`, `Entity`, `AggregateRoot`, `DomainEvent`) of your own.
|
|
71
|
+
Every guide but the orchestrator opens with a "Con @archi-code/ddd" box that lists where it
|
|
72
|
+
diverges from the books, marked `[Fuera de los libros: convención de @archi-code/ddd]`. The
|
|
73
|
+
guides are the source of truth: the package departs from them only where that mark says so,
|
|
74
|
+
and then the package rule wins. Any other disagreement between a guide and this package
|
|
75
|
+
(`ddd-skills/archi-code-ddd/` or its code) is a defect of the package: follow the guide.
|
|
76
|
+
4. **Audits of code built on this package** (`ddd-auditoria-refactorizacion`): apply its checklist
|
|
77
|
+
and also every item of `ddd-skills/archi-code-ddd/what-not-to-do.md`. Its scanner needs Python 3:
|
|
78
|
+
`python3 -I "$(dirname "$(node -p "require.resolve('@archi-code/ddd/package.json')")")/ddd-skills/ddd-auditoria-refactorizacion/scripts/scan_ddd_smells.py" <folder>`.
|
|
79
|
+
It flags a use case that saves two aggregates (`this.orders.save(…)` and `this.invoices.save(…)`
|
|
80
|
+
or `remove(…)`), transactions handled in the domain or the application (they belong to the
|
|
81
|
+
repository), domain code importing `@archi-code/ddd/application` or `/infrastructure`, and
|
|
82
|
+
`new Date()` in domain or application code; its findings are hints to confirm by reading the
|
|
83
|
+
code, and a use case that saves through a helper still needs a manual check. Without Python, apply the checklist by hand. For
|
|
84
|
+
the import direction, use `ddd-skills/ddd-arquitectura/assets/dependency-cruiser.cjs`.
|
|
85
|
+
5. Keep the suite's conventions: say which rule comes from which book and mark what does not come
|
|
86
|
+
from them.
|
|
87
|
+
|
|
88
|
+
If they are installed in the project, use them together with this one:
|
|
89
|
+
|
|
90
|
+
- `archi-code-validation`: available validators, their options and `@Validations` rules.
|
|
91
|
+
|
|
92
|
+
## Rules and patterns of this package
|
|
93
|
+
|
|
94
|
+
The rules, patterns and common mistakes of `@archi-code/ddd` live next to the suite, in
|
|
95
|
+
`ddd-skills/archi-code-ddd/` (located like the suite). They are part of this skill: read the file
|
|
96
|
+
that matches the task before writing or reviewing code, and every file when unsure.
|
|
97
|
+
|
|
98
|
+
| Read `ddd-skills/archi-code-ddd/…` | When |
|
|
99
|
+
| ---------------------------------- | ---------------------------------------------------------------------------------------- |
|
|
100
|
+
| `imports.md` | Which entry point exports each class, function and type |
|
|
101
|
+
| `where-each-class-goes.md` | The layer of each class, what it may import, the folder layout and the names |
|
|
102
|
+
| `layers.md` | The rules of value objects, aggregates, events, use cases, listeners, ports and adapters |
|
|
103
|
+
| `patterns.md` | Code to start from: value objects, aggregates, use cases, listeners, repositories, tests |
|
|
104
|
+
| `what-not-to-do.md` | The common mistakes; also the checklist of every audit of code built on this package |
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# Import
|
|
2
|
+
|
|
3
|
+
Part of the `archi-code-ddd` skill: read it only when that skill routes here.
|
|
4
|
+
|
|
5
|
+
```ts
|
|
6
|
+
import {
|
|
7
|
+
AbstractType,
|
|
8
|
+
AggregateRoot,
|
|
9
|
+
ArrayType,
|
|
10
|
+
BooleanType,
|
|
11
|
+
Composite,
|
|
12
|
+
CreatedAt,
|
|
13
|
+
DateType,
|
|
14
|
+
Entity,
|
|
15
|
+
EnumType,
|
|
16
|
+
EventBase,
|
|
17
|
+
IdType,
|
|
18
|
+
JsonType,
|
|
19
|
+
NumberType,
|
|
20
|
+
Required,
|
|
21
|
+
Specification,
|
|
22
|
+
StringType,
|
|
23
|
+
UpdatedAt,
|
|
24
|
+
UuidType,
|
|
25
|
+
ValueObjects,
|
|
26
|
+
} from '@archi-code/ddd/domain';
|
|
27
|
+
import {
|
|
28
|
+
AggregateNotFoundException,
|
|
29
|
+
ApplicationException,
|
|
30
|
+
ConcurrencyException,
|
|
31
|
+
DomainException,
|
|
32
|
+
ExceptionCode,
|
|
33
|
+
InfrastructureException,
|
|
34
|
+
InternalErrorException,
|
|
35
|
+
NotAllowedException,
|
|
36
|
+
RequiredValueException,
|
|
37
|
+
TypePrimitiveException,
|
|
38
|
+
UnknownEventException,
|
|
39
|
+
ValidationException,
|
|
40
|
+
} from '@archi-code/ddd/exceptions';
|
|
41
|
+
import type { ValidationIssue } from '@archi-code/ddd/exceptions';
|
|
42
|
+
import type {
|
|
43
|
+
AggregateFinder,
|
|
44
|
+
AggregateRemover,
|
|
45
|
+
AggregateSaver,
|
|
46
|
+
EntityId,
|
|
47
|
+
EventClass,
|
|
48
|
+
EventMetadata,
|
|
49
|
+
PrimitiveTypes,
|
|
50
|
+
SatisfiedBy,
|
|
51
|
+
SerializedEvent,
|
|
52
|
+
} from '@archi-code/ddd/domain';
|
|
53
|
+
import { EventListener, ListensTo, UseCase } from '@archi-code/ddd/application';
|
|
54
|
+
import type {
|
|
55
|
+
Clock,
|
|
56
|
+
EventBus,
|
|
57
|
+
ListenerSubscription,
|
|
58
|
+
LogContext,
|
|
59
|
+
Logger,
|
|
60
|
+
LogLevel,
|
|
61
|
+
} from '@archi-code/ddd/application';
|
|
62
|
+
import {
|
|
63
|
+
ConsoleLogger,
|
|
64
|
+
DomainEventBus,
|
|
65
|
+
EventRegistry,
|
|
66
|
+
FixedClock,
|
|
67
|
+
HttpErrorMapper,
|
|
68
|
+
InMemoryEventChannel,
|
|
69
|
+
InMemoryLogger,
|
|
70
|
+
SilentLogger,
|
|
71
|
+
SystemClock,
|
|
72
|
+
} from '@archi-code/ddd/infrastructure';
|
|
73
|
+
import type {
|
|
74
|
+
ChannelHandler,
|
|
75
|
+
ChannelSubscription,
|
|
76
|
+
EventChannel,
|
|
77
|
+
Subscription,
|
|
78
|
+
} from '@archi-code/ddd/infrastructure';
|
|
79
|
+
import { Validations } from '@archi-code/validation';
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
```js
|
|
83
|
+
const { Required, StringType } = require('@archi-code/ddd/domain');
|
|
84
|
+
```
|
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
# Domain, application and infrastructure
|
|
2
|
+
|
|
3
|
+
Part of the `archi-code-ddd` skill: read it only when that skill routes here.
|
|
4
|
+
|
|
5
|
+
- Value objects are **optional by default** (`value: T | null`) `[Fuera de los libros: convención
|
|
6
|
+
de @archi-code/ddd]`: the books build every value object with a value and model an optional one
|
|
7
|
+
as a field that may be `null` (`reservation: ReservationId | null`) [IDDD ch. 5 and 6]; both
|
|
8
|
+
forms work here. `Required(XType)` makes them
|
|
9
|
+
required: the constructor demands `T` and, if it receives an empty value (`null`, `undefined` or a
|
|
10
|
+
blank string), throws `RequiredValueException` with the class name. `Required(...)` also declares
|
|
11
|
+
the `isRequired` validation. An id is always required (see the aggregate root below).
|
|
12
|
+
- A domain VO is always a named subclass (`class Email extends ...`): the name appears in the error
|
|
13
|
+
messages, and `equals` compares the class too (`BillingCountry('ES')` is not a
|
|
14
|
+
`ShippingCountry('ES')`).
|
|
15
|
+
- **A value object that exists is valid.** The constructor converts (or throws
|
|
16
|
+
`TypePrimitiveException`), checks `Required` (or throws `RequiredValueException`) and runs every
|
|
17
|
+
`@Validations` rule, throwing one `ValidationException` with all the broken rules
|
|
18
|
+
(`errors: [{ field: null, code: 'isEmail', message: '<Class>: <message>' }]`; `code` is the
|
|
19
|
+
validator or custom rule name, or the code of the exception). There is no `assertValid()`;
|
|
20
|
+
`validate()` only re-runs the rules and is always ok on a built VO.
|
|
21
|
+
- **Report every field, never only the first.** Build the value objects of an input with
|
|
22
|
+
`ValueObjects.build({ field: () => new X(input.field), … })`: it runs every factory and throws one
|
|
23
|
+
`ValidationException` whose `errors` carry the `field` and `code` of each problem (required
|
|
24
|
+
`DOM004`, conversion `DOM003`, a rule name, or the code of any `DomainException`, such as your
|
|
25
|
+
`ORD003`); `ValueObjects.buildList(list, (item) => …)` names list items by position
|
|
26
|
+
(`lines.1.quantity`). It returns the command typed field by field. Factories are synchronous (one
|
|
27
|
+
that returns a Promise does not compile and is refused at run time with
|
|
28
|
+
`InternalErrorException`), and an error that is not a `DomainException` is a bug that wins over the
|
|
29
|
+
validation errors already collected.
|
|
30
|
+
- **Which layer validates what:** the input adapter checks the shape and format of the request (it
|
|
31
|
+
parses, a field is a string, an empty field or a length for a good user experience [IDDD ch. 4]),
|
|
32
|
+
never a business rule, and that never replaces the value object; value objects check each value (built in
|
|
33
|
+
`toCommand`); the aggregate checks invariants over several values; a domain service checks rules
|
|
34
|
+
over several aggregates; the use case checks authorization. The input of a use case arrives
|
|
35
|
+
unvalidated on purpose: every adapter goes through it.
|
|
36
|
+
- `@Validations` is imported from `@archi-code/validation`; a validator's parameters go in a single
|
|
37
|
+
`properties` object. Subclasses inherit validations, and one with the same name replaces the
|
|
38
|
+
parent's.
|
|
39
|
+
- Values are **immutable**: `_value` is `readonly`; to change a value, build a new VO.
|
|
40
|
+
`ArrayType.withItem/withoutItem` return a new instance; `UpdatedAt` is replaced with
|
|
41
|
+
`UpdatedAt.now()`.
|
|
42
|
+
- `toString()` is a method, `toJSON()` returns the plain value, `equals(other)` compares class and
|
|
43
|
+
value (dates by instant, arrays and objects by content). Template literals and `JSON.stringify`
|
|
44
|
+
work on VOs.
|
|
45
|
+
- Dates: `DateType` accepts `Date`, ISO dates (`'2024-01-31'`, UTC midnight) and ISO date-times
|
|
46
|
+
**with** a zone (`Z` or offset); a date-time without a zone throws `TypePrimitiveException`.
|
|
47
|
+
`UuidType`/`IdType` store lowercase. New ids: `OrderId.generate()` (an `OrderId`). Every id class
|
|
48
|
+
passes its own name as the brand (`class OrderId extends IdType<'OrderId'> {}`) so the compiler never
|
|
49
|
+
takes a `CustomerId` for an `OrderId`.
|
|
50
|
+
- Time: pass a `Clock` to `CreatedAt.now(clock)` / `UpdatedAt.now(clock)` and to events
|
|
51
|
+
(`new OrderPlaced(id, …, { clock })`) from use cases; tests use `FixedClock`. Aggregate methods
|
|
52
|
+
type it as `clock: { now(): Date }`: the `Clock` port matches it, and the domain never imports
|
|
53
|
+
`/application`.
|
|
54
|
+
- A custom type extends `AbstractType<T>` and implements only `protected filter(value: unknown): T`
|
|
55
|
+
(it receives input already known to be non-empty; it returns the value or throws
|
|
56
|
+
`TypePrimitiveException`). If restricting an existing type is enough, extend it and use
|
|
57
|
+
`@Validations`; to normalize the value (trim, lower case) override `filter` and call
|
|
58
|
+
`super.filter(value)` first: `IdType` does the same, and the rules run on the normalized value.
|
|
59
|
+
- **Concepts of several attributes are composite value objects**, not entities nor loose fields:
|
|
60
|
+
`class Money extends Composite({ amount: Cents, currency: Currency }) {}`. The schema names each
|
|
61
|
+
field's class: a value object class, another composite, or a list `[OrderLine]`. Fields are
|
|
62
|
+
exposed read-only with their value object type; the instance is frozen; `equals` compares class
|
|
63
|
+
and every field; `toPrimitives()` gives plain data (dates stay `Date`); `JSON.stringify` writes
|
|
64
|
+
the values; `X.fromPrimitives(data)` rebuilds it reporting every invalid field with its path
|
|
65
|
+
(`lines.1.quantity`). Rules over several fields (end ≥ start, one currency) are `@Validations`
|
|
66
|
+
`custom` rules on the composite class: `fn` receives the built composite, after its fields are
|
|
67
|
+
valid. Behavior returns new instances (`add(other): Money`). Prefer a composite over a child
|
|
68
|
+
entity: an order line with no id anybody looks it up by is a value [IDDD ch. 6]. A collection
|
|
69
|
+
with rules of its own (`OrderLines`: not empty, unique SKU, `total()`) is a composite with a list
|
|
70
|
+
field (`Composite({ items: [OrderLine] })`); `ArrayType` holds only simple value objects. A field
|
|
71
|
+
of a composite may hold an optional value object whose value is `null` (`new Line2(null)`); only
|
|
72
|
+
a missing instance is refused.
|
|
73
|
+
- The aggregate root passes the id to `super(id)`, exposed as `readonly id`; do not redeclare it
|
|
74
|
+
(no `_id` + getter). `equals` compares class and id only. The id is a required value object
|
|
75
|
+
(`EntityId`: `IdType` or a `Required(...)` type); an optional type does not compile as `Id`, and an
|
|
76
|
+
id that holds no value throws `RequiredValueException`, so two new entities are never equal by an
|
|
77
|
+
empty id [IDDD ch. 5]. A child entity (a part with an identity
|
|
78
|
+
of its own inside the aggregate) extends `Entity<Id>` (`super(id)`, `equals` by class and id)
|
|
79
|
+
and holds its id and value objects; it may have business methods named after the language
|
|
80
|
+
(`seat.relabel(label)`) with the rules of that part, never setters, and only its root calls them
|
|
81
|
+
(Tell, Don't Ask: `room.relabelSeat(id, label)` finds the seat and tells it) [IDDD ch. 5 and
|
|
82
|
+
10]; rules over several parts stay in the root. Its identity is local: unique only inside its
|
|
83
|
+
aggregate, never used from outside [IDDD ch. 10] (`class SeatNumber extends Required(NumberType)`). `AggregateRoot` extends `Entity` too.
|
|
84
|
+
- **Derived values are methods, never getters** (`total()`, `isEditable()`), on aggregates, child
|
|
85
|
+
entities and composites: `PrimitiveTypes` sees every getter as a field, so a computed getter
|
|
86
|
+
breaks `toPrimitives()` at compile time.
|
|
87
|
+
- **A named business rule that selects, validates or combines is a `Specification<T>`**:
|
|
88
|
+
`class EligibleForExpressShipping extends Specification<Order> { isSatisfiedBy(order) {…} }`,
|
|
89
|
+
combined with `and`, `or` and `not`; `and`/`or` accept any `SatisfiedBy<T>` (an object with
|
|
90
|
+
`isSatisfiedBy`), the books' `Specification` interface [IDDD ch. 5]. It receives data already loaded (never a repository) and is
|
|
91
|
+
never used to protect an invariant of an aggregate: invariants stay inside the aggregate.
|
|
92
|
+
- **The domain knows nothing about persistence**: no mapper, repository nor storage concern in
|
|
93
|
+
domain code; a repository receives an aggregate and stores it, calling the aggregate's own
|
|
94
|
+
`toPrimitives()` / `fromPrimitives()`.
|
|
95
|
+
- **Aggregates and their primitives** (every aggregate follows these six rules so infrastructure can
|
|
96
|
+
store it; rules 1, 2, 4 and 5, and the name `fromPrimitives` of rule 3, are package conventions, `[Fuera de los libros: convención de
|
|
97
|
+
@archi-code/ddd]`: the books allow a primitive without behavior [IDDD ch. 6], getters only where
|
|
98
|
+
needed [Destilado ch. 5] and a mapper in infrastructure [IDDD ch. 6]):
|
|
99
|
+
1. All its state is value objects: each persisted field is a value object, a composite, a list
|
|
100
|
+
of them or a child entity (`extends Entity<Id>`) whose fields are value objects. A flag is
|
|
101
|
+
`class Shipped extends Required(BooleanType) {}`, never `boolean`; a moment is `DateType` /
|
|
102
|
+
`CreatedAt`, never `Date`.
|
|
103
|
+
2. Private field + public getter with the same name without `_` (or a public `readonly`). No
|
|
104
|
+
private state without its getter: the primitives use those names.
|
|
105
|
+
3. Private constructor and two factories: a business one that validates and records events, and
|
|
106
|
+
`static fromPrimitives(primitives: PrimitiveTypes<Order>)` that rebuilds each value object
|
|
107
|
+
from its primitive and records nothing.
|
|
108
|
+
4. `toPrimitives(): PrimitiveTypes<Order>` returns only plain data (`vo.value`, child primitives),
|
|
109
|
+
never a value object.
|
|
110
|
+
5. Repositories only call `order.toPrimitives()` and `Order.fromPrimitives(row)` (plus
|
|
111
|
+
`.restoreVersion(version)`); they never read the aggregate's fields. There is no mapper.
|
|
112
|
+
6. One round-trip test per aggregate: `Order.fromPrimitives(order.toPrimitives()).toPrimitives()`
|
|
113
|
+
equals `order.toPrimitives()`.
|
|
114
|
+
The compiler rejects public raw state (`PrimitiveTypes` gives `never`); nothing checks private
|
|
115
|
+
state at run time: a private field left out of `toPrimitives()` compiles and is lost when
|
|
116
|
+
saving, so rule 2 and the round-trip test of rule 6 are the aggregate's responsibility. An enum value object stores its option (`PrimitiveType` is the enum type),
|
|
117
|
+
so `new OrderStatus(primitives.status)` needs no cast.
|
|
118
|
+
- Aggregates extend `AggregateRoot<Id, Event>` and call `super(id)`. **Every aggregate has a
|
|
119
|
+
`version`** for optimistic concurrency: `0` when new, the stored one when loaded, `+1` after each
|
|
120
|
+
save. The aggregate never declares, stores nor maps it (it is not a field nor in
|
|
121
|
+
`PrimitiveTypes`); only repositories touch it.
|
|
122
|
+
- **Every repository checks the version** (contract): load with
|
|
123
|
+
`Order.fromPrimitives(row).restoreVersion(storedVersion)`; save a new aggregate (`version === 0`) as version 1
|
|
124
|
+
and treat an existing id as a conflict; save an existing one only where the stored version still
|
|
125
|
+
equals `aggregate.version`, storing `aggregate.version + 1` next to (never inside) the
|
|
126
|
+
primitives; when nothing was written throw
|
|
127
|
+
`ConcurrencyException('Order', id, aggregate.version)` (HTTP 409); after a write
|
|
128
|
+
call `aggregate.markPersisted()`. Keep the driver error: `ConcurrencyException(…, { cause })`.
|
|
129
|
+
Removing is a change too: `remove(aggregate)` receives the loaded aggregate and deletes only
|
|
130
|
+
where the stored version still equals `aggregate.version`, or throws `ConcurrencyException`
|
|
131
|
+
(someone changed or removed it after it was loaded) [IDDD ch. 10].
|
|
132
|
+
After a conflict, the use case may repeat its own work when the command is safe against the new
|
|
133
|
+
state [IDDD ch. 10; IDDD appendix A]: a loop in `handle` where each attempt loads the aggregate
|
|
134
|
+
again and saves it again (a few attempts, then rethrow); otherwise the conflict reaches the user
|
|
135
|
+
(409). A use case run by a listener does not retry by itself: the channel's redelivery tries
|
|
136
|
+
again.
|
|
137
|
+
- `record(event)` is `protected` and deep-freezes the event (facts never change [IDDD ch. 8]); `pullDomainEvents()` returns the events (typed `Event[]`) in order
|
|
138
|
+
and empties the list. Pull them after persisting, to publish. `DomainEventBus.publish` does not freeze
|
|
139
|
+
them again: it only sends the `toJSON()` of each event to the channel.
|
|
140
|
+
- Events extend `EventBase`, declare `static readonly eventName` with their class name, a fact in
|
|
141
|
+
the past tense (`'OrderPlaced'`) [IDDD ch. 8] (a published contract is named
|
|
142
|
+
`<context>.<module>.<Event>`, `sales.order.OrderPlaced` [IDDD ch. 13]) and
|
|
143
|
+
call `super(aggregateId, metadata)`; `event.eventName()` returns it. The payload is primitives
|
|
144
|
+
taken from value objects (`customer.value`, money as `{ amount, currency }`), only what
|
|
145
|
+
consumers need: never value objects, `toPrimitives()` nor personal data nobody needs. Listeners and registries refer to
|
|
146
|
+
the event by its class (`@ListensTo(OrderPlaced, { consumer: 'reserve-stock-on-order-placed' })`, `register(OrderPlaced, …)`). The
|
|
147
|
+
base sets `eventId` (UUID v4) and `occurredOn` (immutable `Date`, from `metadata.clock` when
|
|
148
|
+
given, otherwise the system time); pass `{ eventId, occurredOn }` only when rebuilding an event
|
|
149
|
+
a channel delivered. Inside an aggregate method, always pass the clock it received:
|
|
150
|
+
`new OrderPlaced(id.value, customer.value, { clock })`. Serialize with `JSON.stringify` /
|
|
151
|
+
`toJSON()` (`eventName`, `eventVersion`, `eventId`, `aggregateId`, ISO `occurredOn` and the
|
|
152
|
+
payload).
|
|
153
|
+
- `PrimitiveTypes<T>` only sees **public** non-method members (it strips the leading `_`): expose
|
|
154
|
+
VOs with public getters or public `readonly` properties. For an aggregate it includes `id`;
|
|
155
|
+
child entities become their primitives; raw `string`/`number`/`boolean`/`Date`
|
|
156
|
+
members become `never`.
|
|
157
|
+
- Where to throw each exception:
|
|
158
|
+
- Domain (aggregates, VOs, business rules): `DomainException` or a custom subclass with its own
|
|
159
|
+
code; `ValidationException` (thrown by value objects and `ValueObjects.build`).
|
|
160
|
+
- Use cases: `AggregateNotFoundException` when a repository returns `null` (except a use case
|
|
161
|
+
run by a listener to register a fact: it returns, so the listener is not retried forever),
|
|
162
|
+
`NotAllowedException` when the requester may not run the action (authorization belongs to the
|
|
163
|
+
use case), `ApplicationException` for other application errors.
|
|
164
|
+
- Adapters (DB, network…): `InfrastructureException` / `InternalErrorException`, always with
|
|
165
|
+
`{ cause: error }` when wrapping another error.
|
|
166
|
+
- `TypePrimitiveException` and `RequiredValueException` are thrown by the VOs; throw them yourself
|
|
167
|
+
only inside the `filter` of a custom type.
|
|
168
|
+
- Map errors to HTTP with `HttpErrorMapper` (overrides by code for your own codes): 400 for a
|
|
169
|
+
conversion or required error thrown alone (a value object built with `new` outside
|
|
170
|
+
`ValueObjects.build`; inside it they become a `ValidationException`, 422, with `DOM003`/`DOM004`
|
|
171
|
+
in `errors`), 403 `NotAllowedException`, 404 `AggregateNotFoundException`, 409
|
|
172
|
+
`ConcurrencyException`, 422 `ValidationException` and any other `DomainException` (a broken
|
|
173
|
+
business rule), 400 any other `ApplicationException`, 500 otherwise; never hand-write status tables that leak infrastructure
|
|
174
|
+
messages. For logs or messages use `error.code` and the exported `ExceptionCode` enum, or
|
|
175
|
+
`instanceof`: the built-in exceptions are recognized across the CJS and ESM builds.
|
|
176
|
+
- A use case does **one thing**: one business action, one class, one `handle`. It changes one
|
|
177
|
+
aggregate and saves it once [Destilado ch. 5; IDDD ch. 10] (the exceptions IDDD ch. 10 accepts, such
|
|
178
|
+
as creating several aggregates in one batch that changes nothing semantically, are written down
|
|
179
|
+
as an explicit decision); a flow that needs several actions is several use cases (chained by
|
|
180
|
+
domain events or called one after another by whoever orchestrates them), never one use case with
|
|
181
|
+
flags or a `type` field in its input that switch between behaviors.
|
|
182
|
+
- A use case is **named after the business action**, a verb and a noun from the ubiquitous language
|
|
183
|
+
that a domain expert would say: `PlaceOrder`, `ShipOrder`, `CancelSubscription`,
|
|
184
|
+
`RegisterUser`, `ApproveRefund`. Never technical or CRUD names (`CreateOrder` when the business
|
|
185
|
+
says "place", `UpdateOrder`, `SaveUser`, `OrderService`, `OrderManager`, `ProcessOrder`,
|
|
186
|
+
`HandleData`, `OrderUseCase`).
|
|
187
|
+
- The repository contracts belong to the domain
|
|
188
|
+
(`import type { AggregateSaver } from '@archi-code/ddd/domain'`). Interface segregation: a use case depends only on the repository roles it uses —
|
|
189
|
+
`AggregateFinder<A>` (`findById`), `AggregateSaver<A>` (`save`), `AggregateRemover<A>`
|
|
190
|
+
(`remove`), combined with `&` when it needs several. There is no all-in-one `Repository` port:
|
|
191
|
+
an adapter implements each role it supports.
|
|
192
|
+
- Use cases extend `UseCase<Input, Command, Output>`, receive ports by constructor
|
|
193
|
+
(`AggregateSaver<Order>`, `EventBus<OrderEvent>`, `Clock`) and are called
|
|
194
|
+
with `execute(input)`.
|
|
195
|
+
They implement two steps: `protected toCommand(input)` builds the value objects with
|
|
196
|
+
`ValueObjects.build` and returns the command (a small interface of value objects, or one value
|
|
197
|
+
object built with `new`); `protected handle(command)` works only with value objects. `UseCase` has no helpers:
|
|
198
|
+
write every step in `handle` (load with `findById` and throw `AggregateNotFoundException` when it
|
|
199
|
+
is `null` — or return, in a use case a listener runs —, change the aggregate, `save`, then
|
|
200
|
+
`publish(aggregate.pullDomainEvents())`); it never opens a transaction. Let errors
|
|
201
|
+
propagate: `execute` keeps package exceptions and wraps any other error (also from `toCommand`)
|
|
202
|
+
in `InternalErrorException` with `cause`.
|
|
203
|
+
- A use case never imports nor calls another use case, and **returns an id, a DTO or nothing**,
|
|
204
|
+
never an aggregate. Screens that read several aggregates use read queries, not repositories.
|
|
205
|
+
- **Authorization belongs to the use case**: the entry point takes the requester id from the token
|
|
206
|
+
and puts it in the input, `toCommand` builds it as a value object, and an application policy
|
|
207
|
+
(`SalesAccessPolicy`) throws `NotAllowedException`. A rule that is business (only the owner may
|
|
208
|
+
change the order) stays in the aggregate.
|
|
209
|
+
- **Domain services** are plain classes in `domain/` (no base class) named after the business
|
|
210
|
+
operation; they receive aggregates and loaded data, and may read other aggregates through an
|
|
211
|
+
`AggregateFinder` to validate or calculate over several of them [IDDD ch. 5 and 7]; they never
|
|
212
|
+
save, open transactions nor publish. A
|
|
213
|
+
technical one (`ExchangeRates`) is an interface in `domain/` implemented in infrastructure.
|
|
214
|
+
- **Repositories only get and persist** (`findById`, `save`, `remove`): no business logic, no
|
|
215
|
+
business queries (`pendingOlderThan`), no `nextId()`; new ids come from the value object
|
|
216
|
+
(`OrderId.generate()`). A `findAll()` exists only in the in-memory repository of the tests, if
|
|
217
|
+
they need it (`[Fuera de los libros: convención de @archi-code/ddd]`; a real repository has no
|
|
218
|
+
`findAll()`).
|
|
219
|
+
- **One repository per aggregate and technology** (user rule): it implements the roles it supports,
|
|
220
|
+
knows the aggregate it rebuilds, and its constructor receives only its technology (a collection,
|
|
221
|
+
a pool), never an aggregate class nor a mapper; aggregates only go in and out through its
|
|
222
|
+
methods. The aggregate converts itself (`toPrimitives()` / `static fromPrimitives()`), never a
|
|
223
|
+
domain service nor a mapper; the repository calls those methods directly
|
|
224
|
+
(`Order.fromPrimitives(row).restoreVersion(row.version)` on load, `order.toPrimitives()` with
|
|
225
|
+
`order.version + 1` on save, `ConcurrencyException('Order', …)` on a conflict) and maps the
|
|
226
|
+
primitives to its storage.
|
|
227
|
+
The package ships no repository: the in-memory one is written in the project's `test/support/`.
|
|
228
|
+
- **Another aggregate is referenced by its id** (`customerId: CustomerId`), never by the object.
|
|
229
|
+
- **A command can be refused, a fact cannot**: a method that registers something that already
|
|
230
|
+
happened elsewhere (`registerStockReservation`, called by a listener) is idempotent and never
|
|
231
|
+
throws a business error; if the fact does not fit, it records an event so another listener
|
|
232
|
+
compensates.
|
|
233
|
+
- **Money is a composite with whole minor units** (`Money({ amount: Cents, currency })`, `Cents` a
|
|
234
|
+
`Required(NumberType)` with `isInt`), never a decimal amount; it is exact up to
|
|
235
|
+
`Number.MAX_SAFE_INTEGER` cents (the books use `bigint`, `[Fuera de los libros: convención de
|
|
236
|
+
@archi-code/ddd]`); rounding is explicit.
|
|
237
|
+
- **The repository owns the transaction and everything about persistence** (`[Fuera de los
|
|
238
|
+
libros: convención de @archi-code/ddd]`; IDDD ch. 14 gives it to the application service): there
|
|
239
|
+
is no transaction port, and neither the domain nor the application opens, joins or closes a
|
|
240
|
+
transaction. Each `save`/`remove` is atomic: one document per aggregate is one write; an
|
|
241
|
+
aggregate over several tables is written in a database transaction the repository opens and
|
|
242
|
+
commits inside `save`. The repository also checks the version, manages connections and sessions
|
|
243
|
+
and wraps driver errors. When events must not be lost between `save` and `publish` [Destilado
|
|
244
|
+
ch. 6; IDDD ch. 8], the repository stores them in the same atomic write (an outbox) and a relay
|
|
245
|
+
delivers them through an `EventChannel`: use cases do not change.
|
|
246
|
+
- **Events have a schema version** (`static readonly eventVersion = 2` when their data changes
|
|
247
|
+
incompatibly; 1 by default, written in `toJSON()`); an `EventRegistry` factory reads older
|
|
248
|
+
stored versions from `json.eventVersion`.
|
|
249
|
+
- **The application holds only the event contract, in `application/event`**: to publish, the
|
|
250
|
+
port `EventBus` (`publish`, the only thing use cases see); to listen, `EventListener` and
|
|
251
|
+
`@ListensTo` (`ListensTo.subscriptionOf(listener)` → `{ eventName, consumer }`). What moves the
|
|
252
|
+
events is infrastructure, used only by the wiring (composition root), and is imported from
|
|
253
|
+
`@archi-code/ddd/infrastructure`: `DomainEventBus(channel, registry)` (implements `EventBus`;
|
|
254
|
+
`subscribe(listener)` → `Subscription { unsubscribe() }`, both async), `EventRegistry` and the
|
|
255
|
+
port `EventChannel` (`send(serialized)`, `receive(eventName, consumer, handler)` →
|
|
256
|
+
`ChannelSubscription { close() }`, with `ChannelHandler`). A messaging technology implements only
|
|
257
|
+
`EventChannel` (`InMemoryEventChannel` in the package; RabbitMQ, a table or Kafka in your
|
|
258
|
+
infrastructure); swapping it changes one line of the wiring.
|
|
259
|
+
- **Consumers are idempotent**: delivery is at least once, so a listener ignores an `eventId` it
|
|
260
|
+
already processed. Listeners always receive the event rebuilt from its JSON (frozen, equal by
|
|
261
|
+
`toJSON()`), also in memory, so nothing changes when the channel does.
|
|
262
|
+
- **Other contexts never receive domain classes**: translate the serialized event to a published,
|
|
263
|
+
versioned contract (`sales.order.OrderPlaced`, only what consumers need, `messageId` = `eventId`)
|
|
264
|
+
in the adapter that talks to the broker [IDDD ch. 13]; expose a new contract version next to the
|
|
265
|
+
old one while consumers move.
|
|
266
|
+
- Event Sourcing and CQRS are optional, only for a concrete need (full history, read models very
|
|
267
|
+
different from the model); read queries straight to the database come first. A projection is
|
|
268
|
+
an `EventListener` that writes a read table and skips an `eventId` it already applied; a read query is an application
|
|
269
|
+
interface implemented in infrastructure that returns DTOs or ids.
|
|
270
|
+
- Logging goes through the `Logger` port of `/application` (`debug`, `info`, `warn`, `error`,
|
|
271
|
+
each `(message, context?)`): the message is a stable snake_case name (`order_shipped`) and the
|
|
272
|
+
data goes in the context. Use cases, listeners and adapters that need it receive it by
|
|
273
|
+
constructor; the domain never logs (it records events or throws). The composition root picks the
|
|
274
|
+
provider: `ConsoleLogger({ level })` (one JSON line per entry), an adapter over pino or winston,
|
|
275
|
+
`InMemoryLogger` in tests (`entries`), `SilentLogger` by default. Pass it to
|
|
276
|
+
`new DomainEventBus(channel, registry, logger)` (listener failures, subscriptions, what was
|
|
277
|
+
published) and `new HttpErrorMapper({ statusByCode, logger })` (5xx as `error`, 4xx at `debug`):
|
|
278
|
+
each error is logged once, where it is handled, never logged and rethrown in every layer.
|
|
279
|
+
- Tests use the project's `InMemoryOrderRepository` (in `test/support/`) and
|
|
280
|
+
`new DomainEventBus(new InMemoryEventChannel(), registry)` (`channel.sent` lists
|
|
281
|
+
what was published; `{ delivery: 'parallel' }` runs the listeners of an event together).
|
|
282
|
+
- Listeners extend `EventListener<Event>`, are decorated with `@ListensTo(EventClass, { consumer })`
|
|
283
|
+
(one event per listener, any number of listeners per event; `consumer` is a stable name, unique
|
|
284
|
+
per listener, that the channel uses as its identity, such as its queue; never derived from the
|
|
285
|
+
class name, which a minifier changes), receive the one use case they run by
|
|
286
|
+
constructor and their `handle(event)` only maps the event to that use case's input.
|
|
287
|
+
- The bus is built and connected in the composition root:
|
|
288
|
+
`const bus = new DomainEventBus(channel, registry)`, then `await bus.subscribe(listener)` for
|
|
289
|
+
each listener; `subscribe` refuses a listener without `@ListensTo` and an event the registry
|
|
290
|
+
cannot rebuild. The use cases only receive `EventBus`.
|
|
291
|
+
- A real repository implements the roles it supports (`AggregateFinder<A>`, `AggregateSaver<A>`,
|
|
292
|
+
`AggregateRemover<A>`) and wraps driver errors in `InternalErrorException` with `cause`.
|
|
293
|
+
The in-memory repository of the tests (`InMemoryOrderRepository`, see `patterns.md`) stores copies
|
|
294
|
+
of the primitives with their version and follows the same concurrency contract.
|
|
295
|
+
- `DomainEventBus` rebuilds events with `EventRegistry`: register a factory per
|
|
296
|
+
event class that passes the JSON as metadata
|
|
297
|
+
(`register(OrderPlaced, (json) => new OrderPlaced(json.aggregateId, …, json))`); `fromJSON`
|
|
298
|
+
throws `InternalErrorException` when the rebuilt event lost its `eventId` or `occurredOn`, which
|
|
299
|
+
listeners need to skip a redelivery.
|