@archi-code/ddd 3.0.0-alpha.1 → 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/README.md +478 -376
- package/SKILL.md +10 -8
- package/ddd-skills/archi-code-ddd/imports.md +28 -18
- package/ddd-skills/archi-code-ddd/layers.md +68 -53
- package/ddd-skills/archi-code-ddd/patterns.md +50 -43
- package/ddd-skills/archi-code-ddd/what-not-to-do.md +25 -18
- package/ddd-skills/archi-code-ddd/where-each-class-goes.md +11 -10
- package/ddd-skills/ddd-agregados/GUIA.md +2 -2
- package/ddd-skills/ddd-agregados/references/consistencia-eventual.md +5 -6
- package/ddd-skills/ddd-agregados/references/fabricas.md +5 -8
- package/ddd-skills/ddd-agregados/references/implementacion-ts.md +22 -27
- package/ddd-skills/ddd-arquitectura/GUIA.md +4 -4
- package/ddd-skills/ddd-arquitectura/assets/dependency-cruiser.cjs +13 -6
- package/ddd-skills/ddd-arquitectura/references/estilos-adicionales.md +2 -2
- package/ddd-skills/ddd-arquitectura/references/estructura-hexagonal-ts.md +70 -16
- package/ddd-skills/ddd-auditoria-refactorizacion/GUIA.md +1 -1
- package/ddd-skills/ddd-auditoria-refactorizacion/references/ejemplo-auditoria.md +9 -11
- package/ddd-skills/ddd-auditoria-refactorizacion/references/lista-comprobacion.md +4 -4
- package/ddd-skills/ddd-auditoria-refactorizacion/references/plan-migracion.md +2 -2
- package/ddd-skills/ddd-context-mapping/GUIA.md +5 -5
- package/ddd-skills/ddd-context-mapping/references/anticorrupcion-ts.md +33 -18
- package/ddd-skills/ddd-context-mapping/references/estilos-integracion.md +4 -7
- package/ddd-skills/ddd-event-sourcing-cqrs/GUIA.md +2 -2
- package/ddd-skills/ddd-event-sourcing-cqrs/references/event-sourcing-ts.md +32 -27
- package/ddd-skills/ddd-event-sourcing-cqrs/references/proyecciones-cqrs-ts.md +7 -7
- package/ddd-skills/ddd-eventos-dominio/GUIA.md +3 -3
- package/ddd-skills/ddd-eventos-dominio/references/diseno-eventos.md +15 -16
- package/ddd-skills/ddd-eventos-dominio/references/ejemplo-ecommerce.md +11 -11
- package/ddd-skills/ddd-eventos-dominio/references/publicacion-ts.md +34 -31
- package/ddd-skills/ddd-repositorios-persistencia/GUIA.md +7 -7
- package/ddd-skills/ddd-repositorios-persistencia/references/implementacion-ts.md +50 -32
- package/ddd-skills/ddd-repositorios-persistencia/references/tests-repositorios.md +31 -17
- package/ddd-skills/ddd-repositorios-persistencia/references/transacciones-ts.md +21 -14
- package/ddd-skills/ddd-servicios-capa-aplicacion/GUIA.md +2 -2
- package/ddd-skills/ddd-servicios-capa-aplicacion/references/capa-aplicacion-ts.md +19 -19
- package/ddd-skills/ddd-servicios-capa-aplicacion/references/ejemplo-ecommerce.md +12 -15
- package/ddd-skills/ddd-servicios-capa-aplicacion/references/servicios-dominio-ts.md +5 -6
- package/ddd-skills/ddd-value-objects-entidades/GUIA.md +3 -3
- package/ddd-skills/ddd-value-objects-entidades/references/entidades-ts.md +7 -9
- package/ddd-skills/ddd-value-objects-entidades/references/persistencia-valores.md +5 -5
- package/ddd-skills/ddd-value-objects-entidades/references/validacion.md +9 -9
- package/ddd-skills/ddd-value-objects-entidades/references/value-objects-ts.md +2 -3
- package/dist/cjs/application/event/listens-to.js +4 -4
- package/dist/cjs/{exceptions/application → application/exceptions}/aggregate-not-found.exception.d.ts +1 -1
- package/dist/cjs/{exceptions/application → application/exceptions}/aggregate-not-found.exception.js +4 -4
- 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/{exceptions/application → application/exceptions}/concurrency.exception.d.ts +1 -1
- package/dist/cjs/{exceptions/application → application/exceptions}/concurrency.exception.js +4 -4
- package/dist/cjs/{exceptions/application → application/exceptions}/not-allowed.exception.d.ts +1 -1
- package/dist/cjs/{exceptions/application → application/exceptions}/not-allowed.exception.js +4 -4
- 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 +7 -0
- package/dist/cjs/application/index.js +15 -1
- package/dist/cjs/application/use-case/use-case.js +3 -2
- package/dist/{esm/domain/type/value-objects/value-objects.d.ts → cjs/application/value-object-builder/value-object-builder.d.ts} +3 -3
- package/dist/cjs/application/value-object-builder/value-object-builder.js +51 -0
- package/dist/cjs/domain/aggregate/aggregate-root.d.ts +0 -5
- package/dist/cjs/domain/aggregate/aggregate-root.js +4 -20
- package/dist/cjs/domain/entity/entity.js +2 -2
- package/dist/cjs/domain/event/event-base.d.ts +3 -9
- package/dist/cjs/domain/event/event-base.js +8 -38
- 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/{exceptions/domain → domain/exceptions}/required-value.exception.js +5 -6
- package/dist/cjs/{exceptions/domain → domain/exceptions}/type-primitive.exception.js +5 -6
- package/dist/cjs/{exceptions/domain → domain/exceptions}/validation.exception.js +5 -5
- package/dist/cjs/domain/index.d.ts +8 -1
- package/dist/cjs/domain/index.js +14 -2
- package/dist/cjs/domain/primitive/primitive-types.d.ts +1 -3
- package/dist/cjs/domain/type/abstract-type.d.ts +6 -8
- package/dist/cjs/domain/type/abstract-type.js +15 -23
- package/dist/cjs/domain/type/array-type.d.ts +2 -1
- package/dist/cjs/domain/type/array-type.js +17 -18
- package/dist/cjs/domain/type/boolean-type.js +2 -2
- 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 +7 -8
- package/dist/cjs/domain/type/composite/composite-field.js +70 -47
- 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.js +15 -19
- package/dist/cjs/domain/type/date-type.d.ts +3 -2
- package/dist/cjs/domain/type/date-type.js +42 -7
- package/dist/cjs/domain/type/enum-type.js +2 -2
- package/dist/cjs/domain/type/id-type.d.ts +1 -1
- package/dist/cjs/domain/type/id-type.js +4 -4
- package/dist/cjs/domain/type/index.d.ts +2 -4
- package/dist/cjs/domain/type/index.js +3 -7
- package/dist/cjs/domain/type/json-type.d.ts +4 -0
- package/dist/cjs/domain/type/json-type.js +33 -4
- package/dist/cjs/domain/type/number-type.js +2 -2
- package/dist/cjs/domain/type/required/required-mark.d.ts +0 -1
- package/dist/cjs/domain/type/string-type.js +2 -2
- package/dist/cjs/domain/type/uuid-type.js +2 -2
- package/dist/cjs/infrastructure/event/domain-event-bus.js +9 -9
- package/dist/cjs/infrastructure/event/event-registry.js +9 -9
- package/dist/cjs/infrastructure/event/in-memory/emitter.js +2 -2
- package/dist/cjs/infrastructure/event/in-memory/in-memory-event-channel.d.ts +1 -1
- package/dist/cjs/infrastructure/event/in-memory/in-memory-event-channel.js +6 -7
- package/dist/cjs/{exceptions/infrastructure → infrastructure/exceptions}/event-delivery.exception.js +4 -4
- 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/{exceptions/infrastructure → infrastructure/exceptions}/internal-error.exception.d.ts +1 -1
- package/dist/cjs/{exceptions/infrastructure → infrastructure/exceptions}/internal-error.exception.js +4 -4
- 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/{exceptions/infrastructure → infrastructure/exceptions}/unknown-event.exception.js +4 -4
- package/dist/cjs/infrastructure/index.d.ts +7 -2
- package/dist/cjs/infrastructure/index.js +15 -3
- package/dist/cjs/infrastructure/logging/console-logger.js +2 -2
- package/dist/{esm → cjs/shared}/exceptions/abstract.exception.d.ts +6 -4
- package/dist/cjs/{exceptions → shared/exceptions}/abstract.exception.js +9 -16
- 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/listens-to.js +1 -1
- package/dist/esm/{exceptions/application → application/exceptions}/aggregate-not-found.exception.d.ts +1 -1
- 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/{exceptions/application → application/exceptions}/concurrency.exception.d.ts +1 -1
- package/dist/esm/{exceptions/application → application/exceptions}/concurrency.exception.js +4 -4
- package/dist/esm/{exceptions/application → application/exceptions}/not-allowed.exception.d.ts +1 -1
- 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 +7 -0
- package/dist/esm/application/index.js +7 -0
- package/dist/esm/application/use-case/use-case.js +3 -2
- package/dist/{cjs/domain/type/value-objects/value-objects.d.ts → esm/application/value-object-builder/value-object-builder.d.ts} +3 -3
- package/dist/esm/application/value-object-builder/value-object-builder.js +47 -0
- package/dist/esm/domain/aggregate/aggregate-root.d.ts +0 -5
- package/dist/esm/domain/aggregate/aggregate-root.js +4 -20
- package/dist/esm/domain/entity/entity.js +1 -1
- package/dist/esm/domain/event/event-base.d.ts +3 -9
- package/dist/esm/domain/event/event-base.js +8 -38
- 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/{exceptions/domain → domain/exceptions}/required-value.exception.js +4 -5
- package/dist/esm/{exceptions/domain → domain/exceptions}/type-primitive.exception.js +4 -5
- package/dist/esm/{exceptions/domain → domain/exceptions}/validation.exception.js +5 -5
- package/dist/esm/domain/index.d.ts +7 -0
- package/dist/esm/domain/index.js +6 -0
- package/dist/esm/domain/primitive/primitive-types.d.ts +1 -3
- package/dist/esm/domain/type/abstract-type.d.ts +6 -8
- package/dist/esm/domain/type/abstract-type.js +14 -22
- package/dist/esm/domain/type/array-type.d.ts +2 -1
- package/dist/esm/domain/type/array-type.js +16 -17
- package/dist/esm/domain/type/boolean-type.js +1 -1
- 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 +7 -8
- package/dist/esm/domain/type/composite/composite-field.js +69 -46
- 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.js +13 -17
- package/dist/esm/domain/type/date-type.d.ts +3 -2
- package/dist/esm/domain/type/date-type.js +39 -4
- package/dist/esm/domain/type/enum-type.js +1 -1
- package/dist/esm/domain/type/id-type.d.ts +1 -1
- package/dist/esm/domain/type/id-type.js +2 -2
- package/dist/esm/domain/type/index.d.ts +2 -4
- package/dist/esm/domain/type/index.js +1 -3
- package/dist/esm/domain/type/json-type.d.ts +4 -0
- package/dist/esm/domain/type/json-type.js +32 -3
- package/dist/esm/domain/type/number-type.js +1 -1
- package/dist/esm/domain/type/required/required-mark.d.ts +0 -1
- package/dist/esm/domain/type/string-type.js +1 -1
- package/dist/esm/domain/type/uuid-type.js +1 -1
- package/dist/esm/infrastructure/event/domain-event-bus.js +5 -5
- package/dist/esm/infrastructure/event/event-registry.js +4 -4
- package/dist/esm/infrastructure/event/in-memory/emitter.js +1 -1
- package/dist/esm/infrastructure/event/in-memory/in-memory-event-channel.d.ts +1 -1
- package/dist/esm/infrastructure/event/in-memory/in-memory-event-channel.js +5 -6
- package/dist/esm/{exceptions/infrastructure → infrastructure/exceptions}/event-delivery.exception.js +4 -4
- 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/{exceptions/infrastructure → infrastructure/exceptions}/internal-error.exception.d.ts +1 -1
- 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/{exceptions/infrastructure → infrastructure/exceptions}/unknown-event.exception.js +4 -4
- package/dist/esm/infrastructure/index.d.ts +7 -2
- package/dist/esm/infrastructure/index.js +7 -1
- package/dist/esm/infrastructure/logging/console-logger.js +1 -1
- package/dist/{cjs → esm/shared}/exceptions/abstract.exception.d.ts +6 -4
- package/dist/esm/{exceptions → shared/exceptions}/abstract.exception.js +9 -16
- 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 +7 -18
- package/dist/cjs/domain/type/equality/value-equality.d.ts +0 -7
- package/dist/cjs/domain/type/equality/value-equality.js +0 -41
- package/dist/cjs/domain/type/immutable/immutable-date.d.ts +0 -4
- package/dist/cjs/domain/type/immutable/immutable-date.js +0 -37
- package/dist/cjs/domain/type/immutable/immutable.d.ts +0 -4
- package/dist/cjs/domain/type/immutable/immutable.js +0 -19
- package/dist/cjs/domain/type/value-objects/value-objects.js +0 -70
- package/dist/cjs/exceptions/application/application.exception.d.ts +0 -6
- package/dist/cjs/exceptions/application/application.exception.js +0 -17
- package/dist/cjs/exceptions/domain/domain.exception.d.ts +0 -6
- package/dist/cjs/exceptions/domain/domain.exception.js +0 -17
- package/dist/cjs/exceptions/exception-code.d.ts +0 -15
- package/dist/cjs/exceptions/exception-code.js +0 -19
- package/dist/cjs/exceptions/index.d.ts +0 -14
- package/dist/cjs/exceptions/index.js +0 -30
- package/dist/cjs/exceptions/infrastructure/infrastructure.exception.d.ts +0 -6
- package/dist/cjs/exceptions/infrastructure/infrastructure.exception.js +0 -17
- package/dist/cjs/infrastructure/http/http-error-mapper.d.ts +0 -23
- package/dist/cjs/infrastructure/http/http-error-mapper.js +0 -63
- package/dist/esm/domain/type/equality/value-equality.d.ts +0 -7
- package/dist/esm/domain/type/equality/value-equality.js +0 -37
- package/dist/esm/domain/type/immutable/immutable-date.d.ts +0 -4
- package/dist/esm/domain/type/immutable/immutable-date.js +0 -33
- package/dist/esm/domain/type/immutable/immutable.d.ts +0 -4
- package/dist/esm/domain/type/immutable/immutable.js +0 -15
- package/dist/esm/domain/type/value-objects/value-objects.js +0 -66
- package/dist/esm/exceptions/application/aggregate-not-found.exception.js +0 -13
- package/dist/esm/exceptions/application/application.exception.d.ts +0 -6
- package/dist/esm/exceptions/application/application.exception.js +0 -13
- package/dist/esm/exceptions/application/not-allowed.exception.js +0 -13
- package/dist/esm/exceptions/domain/domain.exception.d.ts +0 -6
- package/dist/esm/exceptions/domain/domain.exception.js +0 -13
- package/dist/esm/exceptions/exception-code.d.ts +0 -15
- package/dist/esm/exceptions/exception-code.js +0 -16
- package/dist/esm/exceptions/index.d.ts +0 -14
- package/dist/esm/exceptions/index.js +0 -14
- package/dist/esm/exceptions/infrastructure/infrastructure.exception.d.ts +0 -6
- package/dist/esm/exceptions/infrastructure/infrastructure.exception.js +0 -13
- package/dist/esm/exceptions/infrastructure/internal-error.exception.js +0 -13
- package/dist/esm/infrastructure/http/http-error-mapper.d.ts +0 -23
- package/dist/esm/infrastructure/http/http-error-mapper.js +0 -59
- /package/dist/cjs/{exceptions/domain → domain/exceptions}/required-value.exception.d.ts +0 -0
- /package/dist/cjs/{exceptions/domain → domain/exceptions}/type-primitive.exception.d.ts +0 -0
- /package/dist/cjs/{exceptions/domain → domain/exceptions}/validation.exception.d.ts +0 -0
- /package/dist/cjs/domain/type/required/{required.d.ts → required-type.d.ts} +0 -0
- /package/dist/cjs/domain/type/required/{required.js → required-type.js} +0 -0
- /package/dist/cjs/{exceptions/infrastructure → infrastructure/exceptions}/event-delivery.exception.d.ts +0 -0
- /package/dist/cjs/{exceptions/infrastructure → infrastructure/exceptions}/unknown-event.exception.d.ts +0 -0
- /package/dist/cjs/{exceptions → shared/exceptions}/brand/exception-brand.d.ts +0 -0
- /package/dist/cjs/{exceptions → shared/exceptions}/brand/exception-brand.js +0 -0
- /package/dist/cjs/{exceptions → shared/exceptions}/received/received-value.d.ts +0 -0
- /package/dist/cjs/{exceptions → shared/exceptions}/received/received-value.js +0 -0
- /package/dist/esm/{exceptions/domain → domain/exceptions}/required-value.exception.d.ts +0 -0
- /package/dist/esm/{exceptions/domain → domain/exceptions}/type-primitive.exception.d.ts +0 -0
- /package/dist/esm/{exceptions/domain → domain/exceptions}/validation.exception.d.ts +0 -0
- /package/dist/esm/domain/type/required/{required.d.ts → required-type.d.ts} +0 -0
- /package/dist/esm/domain/type/required/{required.js → required-type.js} +0 -0
- /package/dist/esm/{exceptions/infrastructure → infrastructure/exceptions}/event-delivery.exception.d.ts +0 -0
- /package/dist/esm/{exceptions/infrastructure → infrastructure/exceptions}/unknown-event.exception.d.ts +0 -0
- /package/dist/esm/{exceptions → shared/exceptions}/brand/exception-brand.d.ts +0 -0
- /package/dist/esm/{exceptions → shared/exceptions}/brand/exception-brand.js +0 -0
- /package/dist/esm/{exceptions → shared/exceptions}/received/received-value.d.ts +0 -0
- /package/dist/esm/{exceptions → shared/exceptions}/received/received-value.js +0 -0
package/SKILL.md
CHANGED
|
@@ -1,19 +1,21 @@
|
|
|
1
1
|
---
|
|
2
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
|
|
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 ExceptionClassifier. 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
4
|
---
|
|
5
5
|
|
|
6
6
|
# archi-code-ddd
|
|
7
7
|
|
|
8
8
|
`@archi-code/ddd` is archi-code's Domain-Driven Design package for TypeScript, one entry point per
|
|
9
|
-
layer
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
`@archi-code/ddd/application` (`UseCase`, the event contract to listen,
|
|
13
|
-
`@ListensTo`, and to publish, `EventBus`,
|
|
14
|
-
`@archi-code/ddd/infrastructure` (
|
|
9
|
+
layer, each with the exceptions of its layer: `@archi-code/ddd/domain` (value objects, `Composite`,
|
|
10
|
+
`Specification`, `Entity`, `AggregateRoot`, `EventBase`, the repository contracts
|
|
11
|
+
`AggregateFinder`, `AggregateSaver`, `AggregateRemover`, and `DomainException`,
|
|
12
|
+
`ValidationException`…), `@archi-code/ddd/application` (`UseCase`, the event contract to listen,
|
|
13
|
+
`EventListener` and `@ListensTo`, and to publish, `EventBus`, the ports `Clock`, `Logger`, and
|
|
14
|
+
`AggregateNotFoundException`, `NotAllowedException`…) and `@archi-code/ddd/infrastructure` (the
|
|
15
15
|
event wiring `DomainEventBus`, `EventRegistry` and the `EventChannel` port, in-memory event
|
|
16
|
-
channel,
|
|
16
|
+
channel, clocks, loggers, `InternalErrorException`…). `@archi-code/ddd/infrastructure` also has
|
|
17
|
+
`ExceptionClassifier`, which gives every entry point (HTTP, gRPC, queue, CLI) the `ExceptionKind`
|
|
18
|
+
of an error.
|
|
17
19
|
|
|
18
20
|
## Full API
|
|
19
21
|
|
|
@@ -11,34 +11,24 @@ import {
|
|
|
11
11
|
Composite,
|
|
12
12
|
CreatedAt,
|
|
13
13
|
DateType,
|
|
14
|
+
DomainException,
|
|
15
|
+
DomainExceptionCode,
|
|
14
16
|
Entity,
|
|
15
17
|
EnumType,
|
|
16
18
|
EventBase,
|
|
17
19
|
IdType,
|
|
20
|
+
InvalidEventDefinitionException,
|
|
18
21
|
JsonType,
|
|
19
22
|
NumberType,
|
|
20
23
|
Required,
|
|
24
|
+
RequiredValueException,
|
|
21
25
|
Specification,
|
|
22
26
|
StringType,
|
|
27
|
+
TypePrimitiveException,
|
|
23
28
|
UpdatedAt,
|
|
24
29
|
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
30
|
ValidationException,
|
|
40
|
-
} from '@archi-code/ddd/
|
|
41
|
-
import type { ValidationIssue } from '@archi-code/ddd/exceptions';
|
|
31
|
+
} from '@archi-code/ddd/domain';
|
|
42
32
|
import type {
|
|
43
33
|
AggregateFinder,
|
|
44
34
|
AggregateRemover,
|
|
@@ -49,8 +39,20 @@ import type {
|
|
|
49
39
|
PrimitiveTypes,
|
|
50
40
|
SatisfiedBy,
|
|
51
41
|
SerializedEvent,
|
|
42
|
+
ValidationIssue,
|
|
52
43
|
} from '@archi-code/ddd/domain';
|
|
53
|
-
import {
|
|
44
|
+
import {
|
|
45
|
+
AggregateNotFoundException,
|
|
46
|
+
ApplicationException,
|
|
47
|
+
ApplicationExceptionCode,
|
|
48
|
+
ConcurrencyException,
|
|
49
|
+
EventListener,
|
|
50
|
+
ListensTo,
|
|
51
|
+
NotAllowedException,
|
|
52
|
+
UnexpectedErrorException,
|
|
53
|
+
UseCase,
|
|
54
|
+
ValueObjectBuilder,
|
|
55
|
+
} from '@archi-code/ddd/application';
|
|
54
56
|
import type {
|
|
55
57
|
Clock,
|
|
56
58
|
EventBus,
|
|
@@ -63,12 +65,17 @@ import {
|
|
|
63
65
|
ConsoleLogger,
|
|
64
66
|
DomainEventBus,
|
|
65
67
|
EventRegistry,
|
|
68
|
+
ExceptionClassifier,
|
|
69
|
+
ExceptionKind,
|
|
66
70
|
FixedClock,
|
|
67
|
-
|
|
71
|
+
InfrastructureException,
|
|
72
|
+
InfrastructureExceptionCode,
|
|
68
73
|
InMemoryEventChannel,
|
|
69
74
|
InMemoryLogger,
|
|
75
|
+
InternalErrorException,
|
|
70
76
|
SilentLogger,
|
|
71
77
|
SystemClock,
|
|
78
|
+
UnknownEventException,
|
|
72
79
|
} from '@archi-code/ddd/infrastructure';
|
|
73
80
|
import type {
|
|
74
81
|
ChannelHandler,
|
|
@@ -79,6 +86,9 @@ import type {
|
|
|
79
86
|
import { Validations } from '@archi-code/validation';
|
|
80
87
|
```
|
|
81
88
|
|
|
89
|
+
Each layer exports its own exceptions and its own code enum; there is no `/exceptions` subpath
|
|
90
|
+
and the common base class of the exceptions is internal.
|
|
91
|
+
|
|
82
92
|
```js
|
|
83
93
|
const { Required, StringType } = require('@archi-code/ddd/domain');
|
|
84
94
|
```
|
|
@@ -6,8 +6,9 @@ Part of the `archi-code-ddd` skill: read it only when that skill routes here.
|
|
|
6
6
|
de @archi-code/ddd]`: the books build every value object with a value and model an optional one
|
|
7
7
|
as a field that may be `null` (`reservation: ReservationId | null`) [IDDD ch. 5 and 6]; both
|
|
8
8
|
forms work here. `Required(XType)` makes them
|
|
9
|
-
required: the constructor demands `T` and, if it receives an empty value (`null
|
|
10
|
-
|
|
9
|
+
required: the constructor demands `T` and, if it receives an empty value (`null` or `undefined`),
|
|
10
|
+
throws `RequiredValueException` with the class name. A blank text is a value: `StringType` keeps
|
|
11
|
+
it and the other types refuse it with `TypePrimitiveException`. `Required(...)` also declares
|
|
11
12
|
the `isRequired` validation. An id is always required (see the aggregate root below).
|
|
12
13
|
- A domain VO is always a named subclass (`class Email extends ...`): the name appears in the error
|
|
13
14
|
messages, and `equals` compares the class too (`BillingCountry('ES')` is not a
|
|
@@ -18,15 +19,18 @@ Part of the `archi-code-ddd` skill: read it only when that skill routes here.
|
|
|
18
19
|
(`errors: [{ field: null, code: 'isEmail', message: '<Class>: <message>' }]`; `code` is the
|
|
19
20
|
validator or custom rule name, or the code of the exception). There is no `assertValid()`;
|
|
20
21
|
`validate()` only re-runs the rules and is always ok on a built VO.
|
|
21
|
-
- **Report every field, never only the first.**
|
|
22
|
-
`
|
|
22
|
+
- **Report every field, never only the first.** In `toCommand`, build the value objects of an
|
|
23
|
+
input with `ValueObjectBuilder.build({ field: () => new X(input.field), … })` from
|
|
24
|
+
`@archi-code/ddd/application` (the domain never uses it): it runs every factory and throws one
|
|
23
25
|
`ValidationException` whose `errors` carry the `field` and `code` of each problem (required
|
|
24
26
|
`DOM004`, conversion `DOM003`, a rule name, or the code of any `DomainException`, such as your
|
|
25
|
-
`ORD003`); `
|
|
27
|
+
`ORD003`); `ValueObjectBuilder.buildList(list, (item) => …)` names list items by position
|
|
26
28
|
(`lines.1.quantity`). It returns the command typed field by field. Factories are synchronous (one
|
|
27
29
|
that returns a Promise does not compile and is refused at run time with
|
|
28
|
-
`
|
|
30
|
+
`UnexpectedErrorException`), and an error that is not a `DomainException` is a bug that wins over the
|
|
29
31
|
validation errors already collected.
|
|
32
|
+
- An `ArrayType` receives items that are already valid: it does not collect item errors and lets
|
|
33
|
+
the error of the first invalid item through as the item throws it.
|
|
30
34
|
- **Which layer validates what:** the input adapter checks the shape and format of the request (it
|
|
31
35
|
parses, a field is a string, an empty field or a length for a good user experience [IDDD ch. 4]),
|
|
32
36
|
never a business rule, and that never replaces the value object; value objects check each value (built in
|
|
@@ -39,9 +43,9 @@ Part of the `archi-code-ddd` skill: read it only when that skill routes here.
|
|
|
39
43
|
- Values are **immutable**: `_value` is `readonly`; to change a value, build a new VO.
|
|
40
44
|
`ArrayType.withItem/withoutItem` return a new instance; `UpdatedAt` is replaced with
|
|
41
45
|
`UpdatedAt.now()`.
|
|
42
|
-
- `toString()` is a method, `
|
|
43
|
-
value (dates by instant, arrays and objects by content). Template literals
|
|
44
|
-
|
|
46
|
+
- `toString()` is a method, `equals(other)` compares class and
|
|
47
|
+
value (dates by instant, arrays and objects by content). Template literals work on VOs;
|
|
48
|
+
`JSON.stringify` does not unwrap them: serialize `value` or the aggregate's `toPrimitives()`.
|
|
45
49
|
- Dates: `DateType` accepts `Date`, ISO dates (`'2024-01-31'`, UTC midnight) and ISO date-times
|
|
46
50
|
**with** a zone (`Z` or offset); a date-time without a zone throws `TypePrimitiveException`.
|
|
47
51
|
`UuidType`/`IdType` store lowercase. New ids: `OrderId.generate()` (an `OrderId`). Every id class
|
|
@@ -107,56 +111,60 @@ Part of the `archi-code-ddd` skill: read it only when that skill routes here.
|
|
|
107
111
|
from its primitive and records nothing.
|
|
108
112
|
4. `toPrimitives(): PrimitiveTypes<Order>` returns only plain data (`vo.value`, child primitives),
|
|
109
113
|
never a value object.
|
|
110
|
-
5. Repositories only call `order.toPrimitives()` and `Order.fromPrimitives(row)
|
|
111
|
-
|
|
114
|
+
5. Repositories only call `order.toPrimitives()` and `Order.fromPrimitives(row)`; they never
|
|
115
|
+
read the aggregate's fields. There is no mapper.
|
|
112
116
|
6. One round-trip test per aggregate: `Order.fromPrimitives(order.toPrimitives()).toPrimitives()`
|
|
113
117
|
equals `order.toPrimitives()`.
|
|
114
118
|
The compiler rejects public raw state (`PrimitiveTypes` gives `never`); nothing checks private
|
|
115
119
|
state at run time: a private field left out of `toPrimitives()` compiles and is lost when
|
|
116
120
|
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
121
|
so `new OrderStatus(primitives.status)` needs no cast.
|
|
118
|
-
- Aggregates extend `AggregateRoot<Id, Event>` and call `super(id)`.
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
`
|
|
122
|
-
|
|
123
|
-
`
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
`
|
|
128
|
-
call `aggregate.markPersisted()`. Keep the driver error: `ConcurrencyException(…, { cause })`.
|
|
122
|
+
- Aggregates extend `AggregateRoot<Id, Event>` and call `super(id)`. The aggregate carries **no
|
|
123
|
+
version**: it never declares, stores nor maps one; optimistic concurrency is the repository's.
|
|
124
|
+
- **Every repository checks the version** (contract): it remembers the version of each aggregate
|
|
125
|
+
object it loaded or saved in a private `WeakMap<Order, number>` (`versions`); load with
|
|
126
|
+
`Order.fromPrimitives(row)` and `versions.set(order, row.version)`; on save take
|
|
127
|
+
`versions.get(order) ?? 0`, save a new aggregate (`0`) as version 1 and treat an existing id as a
|
|
128
|
+
conflict; save an existing one only where the stored version still equals the remembered one,
|
|
129
|
+
storing one more next to (never inside) the primitives; when nothing was written throw
|
|
130
|
+
`ConcurrencyException('Order', id, version)` (`ExceptionKind.Conflict`); after a write
|
|
131
|
+
remember `version + 1`. Keep the driver error: `ConcurrencyException(…, { cause })`.
|
|
129
132
|
Removing is a change too: `remove(aggregate)` receives the loaded aggregate and deletes only
|
|
130
|
-
where the stored version still equals
|
|
133
|
+
where the stored version still equals the remembered one, or throws `ConcurrencyException`
|
|
131
134
|
(someone changed or removed it after it was loaded) [IDDD ch. 10].
|
|
132
135
|
After a conflict, the use case may repeat its own work when the command is safe against the new
|
|
133
136
|
state [IDDD ch. 10; IDDD appendix A]: a loop in `handle` where each attempt loads the aggregate
|
|
134
137
|
again and saves it again (a few attempts, then rethrow); otherwise the conflict reaches the user
|
|
135
138
|
(409). A use case run by a listener does not retry by itself: the channel's redelivery tries
|
|
136
139
|
again.
|
|
137
|
-
- `record(event)` is `protected` and
|
|
138
|
-
|
|
139
|
-
|
|
140
|
+
- `record(event)` is `protected` and keeps the event as it is (facts never change [IDDD ch. 8]: the
|
|
141
|
+
event declares its fields `readonly`); `pullDomainEvents()` returns the events (typed `Event[]`) in order
|
|
142
|
+
and empties the list. Pull them after persisting, to publish. `DomainEventBus.publish` only sends a
|
|
143
|
+
plain copy of each event (`{ ...event }`) to the channel.
|
|
140
144
|
- Events extend `EventBase`, declare `static readonly eventName` with their class name, a fact in
|
|
141
145
|
the past tense (`'OrderPlaced'`) [IDDD ch. 8] (a published contract is named
|
|
142
146
|
`<context>.<module>.<Event>`, `sales.order.OrderPlaced` [IDDD ch. 13]) and
|
|
143
|
-
call `super(
|
|
144
|
-
|
|
145
|
-
|
|
147
|
+
call `super(metadata)`; the base copies the name into the event's `eventName` field. An event
|
|
148
|
+
is plain data with no methods. The id of the aggregate is a field the event declares like any
|
|
149
|
+
other (`readonly orderId: string`). Rule (not checked at run time): each event declares as
|
|
150
|
+
`readonly` constructor fields only the primitive data it needs (text, finite numbers, booleans,
|
|
151
|
+
`null`, lists and plain objects of them), taken from value objects (`id.value`,
|
|
152
|
+
`customer.value`, money as `{ amount, currency }`, dates as ISO text): never value objects,
|
|
153
|
+
`Date`s, `toPrimitives()` nor personal data nobody needs. Listeners and registries refer to
|
|
146
154
|
the event by its class (`@ListensTo(OrderPlaced, { consumer: 'reserve-stock-on-order-placed' })`, `register(OrderPlaced, …)`). The
|
|
147
|
-
base sets `eventId` (UUID v4) and `occurredOn` (
|
|
155
|
+
base sets `eventId` (UUID v4) and `occurredOn` (ISO text in UTC, from `metadata.clock` when
|
|
148
156
|
given, otherwise the system time); pass `{ eventId, occurredOn }` only when rebuilding an event
|
|
149
157
|
a channel delivered. Inside an aggregate method, always pass the clock it received:
|
|
150
|
-
`new OrderPlaced(id.value, customer.value, { clock })`.
|
|
151
|
-
`
|
|
152
|
-
|
|
158
|
+
`new OrderPlaced(id.value, customer.value, { clock })`. The event is its own JSON:
|
|
159
|
+
`{ ...event }` / `JSON.stringify(event)` give `eventName`, `eventId`, `occurredOn` and the
|
|
160
|
+
fields, in that order.
|
|
153
161
|
- `PrimitiveTypes<T>` only sees **public** non-method members (it strips the leading `_`): expose
|
|
154
162
|
VOs with public getters or public `readonly` properties. For an aggregate it includes `id`;
|
|
155
163
|
child entities become their primitives; raw `string`/`number`/`boolean`/`Date`
|
|
156
164
|
members become `never`.
|
|
157
165
|
- Where to throw each exception:
|
|
158
166
|
- Domain (aggregates, VOs, business rules): `DomainException` or a custom subclass with its own
|
|
159
|
-
code; `ValidationException` (thrown by value objects and `
|
|
167
|
+
code; `ValidationException` (thrown by value objects and `ValueObjectBuilder.build`).
|
|
160
168
|
- Use cases: `AggregateNotFoundException` when a repository returns `null` (except a use case
|
|
161
169
|
run by a listener to register a fact: it returns, so the listener is not retried forever),
|
|
162
170
|
`NotAllowedException` when the requester may not run the action (authorization belongs to the
|
|
@@ -165,13 +173,19 @@ Part of the `archi-code-ddd` skill: read it only when that skill routes here.
|
|
|
165
173
|
`{ cause: error }` when wrapping another error.
|
|
166
174
|
- `TypePrimitiveException` and `RequiredValueException` are thrown by the VOs; throw them yourself
|
|
167
175
|
only inside the `filter` of a custom type.
|
|
168
|
-
-
|
|
169
|
-
|
|
170
|
-
`
|
|
171
|
-
|
|
172
|
-
`
|
|
173
|
-
|
|
174
|
-
|
|
176
|
+
- The package speaks no protocol: each entry point (HTTP, gRPC, queue consumer, CLI) translates
|
|
177
|
+
errors itself from `ExceptionClassifier.classify(error)` (`/infrastructure`), which returns the
|
|
178
|
+
`ExceptionKind`: `Malformed` for a conversion or required error thrown alone (a value object
|
|
179
|
+
built with `new` outside `ValueObjectBuilder.build`; inside it they become a `ValidationException` with
|
|
180
|
+
`DOM003`/`DOM004` in `errors`), `NotAllowed` for `NotAllowedException`, `NotFound` for
|
|
181
|
+
`AggregateNotFoundException`, `Conflict` for `ConcurrencyException`, `Invalid` for
|
|
182
|
+
`ValidationException` and any other `DomainException` (a broken business rule), `Rejected` for
|
|
183
|
+
any other `ApplicationException`, `Internal` for `UnexpectedErrorException`,
|
|
184
|
+
`InvalidEventDefinitionException` and anything else. The entry point maps the kind to its
|
|
185
|
+
protocol (HTTP: 400, 403, 404, 409, 422, 400, 500), handles your own codes with `error.code`,
|
|
186
|
+
returns the `errors` of a `ValidationException`, never sends the message of an `Internal`
|
|
187
|
+
failure and logs it once; never classify exceptions with hand-written `instanceof` chains. For logs or messages use `error.code` and the code enum of each layer
|
|
188
|
+
(`DomainExceptionCode`, `ApplicationExceptionCode`, `InfrastructureExceptionCode`), or
|
|
175
189
|
`instanceof`: the built-in exceptions are recognized across the CJS and ESM builds.
|
|
176
190
|
- A use case does **one thing**: one business action, one class, one `handle`. It changes one
|
|
177
191
|
aggregate and saves it once [Destilado ch. 5; IDDD ch. 10] (the exceptions IDDD ch. 10 accepts, such
|
|
@@ -193,13 +207,13 @@ Part of the `archi-code-ddd` skill: read it only when that skill routes here.
|
|
|
193
207
|
(`AggregateSaver<Order>`, `EventBus<OrderEvent>`, `Clock`) and are called
|
|
194
208
|
with `execute(input)`.
|
|
195
209
|
They implement two steps: `protected toCommand(input)` builds the value objects with
|
|
196
|
-
`
|
|
210
|
+
`ValueObjectBuilder.build` and returns the command (a small interface of value objects, or one value
|
|
197
211
|
object built with `new`); `protected handle(command)` works only with value objects. `UseCase` has no helpers:
|
|
198
212
|
write every step in `handle` (load with `findById` and throw `AggregateNotFoundException` when it
|
|
199
213
|
is `null` — or return, in a use case a listener runs —, change the aggregate, `save`, then
|
|
200
214
|
`publish(aggregate.pullDomainEvents())`); it never opens a transaction. Let errors
|
|
201
215
|
propagate: `execute` keeps package exceptions and wraps any other error (also from `toCommand`)
|
|
202
|
-
in `
|
|
216
|
+
in `UnexpectedErrorException` with `cause`.
|
|
203
217
|
- A use case never imports nor calls another use case, and **returns an id, a DTO or nothing**,
|
|
204
218
|
never an aggregate. Screens that read several aggregates use read queries, not repositories.
|
|
205
219
|
- **Authorization belongs to the use case**: the entry point takes the requester id from the token
|
|
@@ -221,9 +235,9 @@ Part of the `archi-code-ddd` skill: read it only when that skill routes here.
|
|
|
221
235
|
a pool), never an aggregate class nor a mapper; aggregates only go in and out through its
|
|
222
236
|
methods. The aggregate converts itself (`toPrimitives()` / `static fromPrimitives()`), never a
|
|
223
237
|
domain service nor a mapper; the repository calls those methods directly
|
|
224
|
-
(`Order.fromPrimitives(row)
|
|
225
|
-
|
|
226
|
-
|
|
238
|
+
(`Order.fromPrimitives(row)` on load, `order.toPrimitives()` on save, with the version it keeps
|
|
239
|
+
in its own `WeakMap`, `ConcurrencyException('Order', …)` on a conflict) and maps the primitives
|
|
240
|
+
to its storage.
|
|
227
241
|
The package ships no repository: the in-memory one is written in the project's `test/support/`.
|
|
228
242
|
- **Another aggregate is referenced by its id** (`customerId: CustomerId`), never by the object.
|
|
229
243
|
- **A command can be refused, a fact cannot**: a method that registers something that already
|
|
@@ -243,9 +257,10 @@ Part of the `archi-code-ddd` skill: read it only when that skill routes here.
|
|
|
243
257
|
and wraps driver errors. When events must not be lost between `save` and `publish` [Destilado
|
|
244
258
|
ch. 6; IDDD ch. 8], the repository stores them in the same atomic write (an outbox) and a relay
|
|
245
259
|
delivers them through an `EventChannel`: use cases do not change.
|
|
246
|
-
- **Events
|
|
247
|
-
|
|
248
|
-
|
|
260
|
+
- **Events carry no schema version.** Adding an optional field old readers can ignore is
|
|
261
|
+
compatible; a change they cannot follow is a new event with its own `eventName`
|
|
262
|
+
(`CustomerRenamedV2`), or the `EventRegistry` factory reads both shapes from the stored JSON
|
|
263
|
+
(`typeof json.name === 'string' ? … : …`).
|
|
249
264
|
- **The application holds only the event contract, in `application/event`**: to publish, the
|
|
250
265
|
port `EventBus` (`publish`, the only thing use cases see); to listen, `EventListener` and
|
|
251
266
|
`@ListensTo` (`ListensTo.subscriptionOf(listener)` → `{ eventName, consumer }`). What moves the
|
|
@@ -257,8 +272,8 @@ Part of the `archi-code-ddd` skill: read it only when that skill routes here.
|
|
|
257
272
|
`EventChannel` (`InMemoryEventChannel` in the package; RabbitMQ, a table or Kafka in your
|
|
258
273
|
infrastructure); swapping it changes one line of the wiring.
|
|
259
274
|
- **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 (
|
|
261
|
-
|
|
275
|
+
already processed. Listeners always receive the event rebuilt from its JSON (equal field
|
|
276
|
+
by field), also in memory, so nothing changes when the channel does.
|
|
262
277
|
- **Other contexts never receive domain classes**: translate the serialized event to a published,
|
|
263
278
|
versioned contract (`sales.order.OrderPlaced`, only what consumers need, `messageId` = `eventId`)
|
|
264
279
|
in the adapter that talks to the broker [IDDD ch. 13]; expose a new contract version next to the
|
|
@@ -274,7 +289,7 @@ Part of the `archi-code-ddd` skill: read it only when that skill routes here.
|
|
|
274
289
|
provider: `ConsoleLogger({ level })` (one JSON line per entry), an adapter over pino or winston,
|
|
275
290
|
`InMemoryLogger` in tests (`entries`), `SilentLogger` by default. Pass it to
|
|
276
291
|
`new DomainEventBus(channel, registry, logger)` (listener failures, subscriptions, what was
|
|
277
|
-
published) and
|
|
292
|
+
published) and to the entry points, which log an `Internal` failure as `error`:
|
|
278
293
|
each error is logged once, where it is handled, never logged and rethrown in every layer.
|
|
279
294
|
- Tests use the project's `InMemoryOrderRepository` (in `test/support/`) and
|
|
280
295
|
`new DomainEventBus(new InMemoryEventChannel(), registry)` (`channel.sent` lists
|
|
@@ -294,6 +309,6 @@ Part of the `archi-code-ddd` skill: read it only when that skill routes here.
|
|
|
294
309
|
of the primitives with their version and follows the same concurrency contract.
|
|
295
310
|
- `DomainEventBus` rebuilds events with `EventRegistry`: register a factory per
|
|
296
311
|
event class that passes the JSON as metadata
|
|
297
|
-
(`register(OrderPlaced, (json) => new OrderPlaced(json.
|
|
312
|
+
(`register(OrderPlaced, (json) => new OrderPlaced(json.orderId as string, …, json))`); `fromJSON`
|
|
298
313
|
throws `InternalErrorException` when the rebuilt event lost its `eventId` or `occurredOn`, which
|
|
299
314
|
listeners need to skip a redelivery.
|
|
@@ -29,7 +29,7 @@ new Email('x');
|
|
|
29
29
|
new PackSize(-3);
|
|
30
30
|
// ValidationException: Validation failed, PackSize: Value does not satisfy isInt, PackSize: Must be even
|
|
31
31
|
|
|
32
|
-
const command =
|
|
32
|
+
const command = ValueObjectBuilder.build({
|
|
33
33
|
email: () => new Email(input.email),
|
|
34
34
|
packSize: () => new PackSize(input.packSize),
|
|
35
35
|
});
|
|
@@ -55,11 +55,11 @@ class OrderPlaced extends EventBase {
|
|
|
55
55
|
static readonly eventName = 'OrderPlaced';
|
|
56
56
|
|
|
57
57
|
constructor(
|
|
58
|
-
orderId: string,
|
|
58
|
+
readonly orderId: string,
|
|
59
59
|
readonly customer: string,
|
|
60
60
|
metadata?: EventMetadata,
|
|
61
61
|
) {
|
|
62
|
-
super(
|
|
62
|
+
super(metadata);
|
|
63
63
|
}
|
|
64
64
|
}
|
|
65
65
|
|
|
@@ -67,12 +67,12 @@ class OrderLineAdded extends EventBase {
|
|
|
67
67
|
static readonly eventName = 'OrderLineAdded';
|
|
68
68
|
|
|
69
69
|
constructor(
|
|
70
|
-
orderId: string,
|
|
70
|
+
readonly orderId: string,
|
|
71
71
|
readonly sku: string,
|
|
72
72
|
readonly quantity: number,
|
|
73
73
|
metadata?: EventMetadata,
|
|
74
74
|
) {
|
|
75
|
-
super(
|
|
75
|
+
super(metadata);
|
|
76
76
|
}
|
|
77
77
|
}
|
|
78
78
|
|
|
@@ -180,7 +180,7 @@ class AddOrderLine extends UseCase<AddOrderLineInput, AddOrderLineCommand, void>
|
|
|
180
180
|
}
|
|
181
181
|
|
|
182
182
|
protected toCommand(input: AddOrderLineInput): AddOrderLineCommand {
|
|
183
|
-
return
|
|
183
|
+
return ValueObjectBuilder.build({
|
|
184
184
|
orderId: () => new OrderId(input.orderId),
|
|
185
185
|
sku: () => new Sku(input.sku),
|
|
186
186
|
quantity: () => new Quantity(input.quantity),
|
|
@@ -216,7 +216,7 @@ class PrepareShipment extends UseCase<
|
|
|
216
216
|
}
|
|
217
217
|
|
|
218
218
|
protected toCommand(input: { orderId: string; sku: string }): { orderId: OrderId; sku: Sku } {
|
|
219
|
-
return
|
|
219
|
+
return ValueObjectBuilder.build({
|
|
220
220
|
orderId: () => new OrderId(input.orderId),
|
|
221
221
|
sku: () => new Sku(input.sku),
|
|
222
222
|
});
|
|
@@ -234,7 +234,7 @@ class PrepareShipmentOnOrderLineAdded extends EventListener<OrderLineAdded> {
|
|
|
234
234
|
}
|
|
235
235
|
|
|
236
236
|
async handle(event: OrderLineAdded): Promise<void> {
|
|
237
|
-
await this.prepareShipment.execute({ orderId: event.
|
|
237
|
+
await this.prepareShipment.execute({ orderId: event.orderId, sku: event.sku });
|
|
238
238
|
}
|
|
239
239
|
}
|
|
240
240
|
```
|
|
@@ -246,6 +246,8 @@ which receives the loaded aggregate) and wraps driver errors with their cause:
|
|
|
246
246
|
class MongoOrderRepository
|
|
247
247
|
implements AggregateFinder<Order>, AggregateSaver<Order>, AggregateRemover<Order>
|
|
248
248
|
{
|
|
249
|
+
private readonly versions = new WeakMap<Order, number>();
|
|
250
|
+
|
|
249
251
|
constructor(private readonly collection: Collection<OrderDocument>) {}
|
|
250
252
|
|
|
251
253
|
async findById(id: OrderId): Promise<Order | null> {
|
|
@@ -254,34 +256,34 @@ class MongoOrderRepository
|
|
|
254
256
|
return null;
|
|
255
257
|
}
|
|
256
258
|
const { _id, version, ...primitives } = document;
|
|
257
|
-
|
|
259
|
+
const order = Order.fromPrimitives(primitives);
|
|
260
|
+
this.versions.set(order, version);
|
|
261
|
+
return order;
|
|
258
262
|
}
|
|
259
263
|
|
|
260
264
|
async save(order: Order): Promise<void> {
|
|
261
|
-
const
|
|
265
|
+
const version = this.versions.get(order) ?? 0;
|
|
266
|
+
const data = { ...order.toPrimitives(), version: version + 1 };
|
|
262
267
|
let written: boolean;
|
|
263
268
|
try {
|
|
264
269
|
written =
|
|
265
|
-
|
|
270
|
+
version === 0
|
|
266
271
|
? await this.insertUnlessTaken(data)
|
|
267
|
-
: (await this.collection.replaceOne({ _id: data.id, version
|
|
268
|
-
.matchedCount === 1;
|
|
272
|
+
: (await this.collection.replaceOne({ _id: data.id, version }, data)).matchedCount === 1;
|
|
269
273
|
} catch (error) {
|
|
270
274
|
throw new InternalErrorException('saving order', { cause: error });
|
|
271
275
|
}
|
|
272
276
|
if (!written) {
|
|
273
|
-
throw new ConcurrencyException('Order', order.id.value,
|
|
277
|
+
throw new ConcurrencyException('Order', order.id.value, version);
|
|
274
278
|
}
|
|
275
|
-
|
|
279
|
+
this.versions.set(order, version + 1);
|
|
276
280
|
}
|
|
277
281
|
|
|
278
282
|
async remove(order: Order): Promise<void> {
|
|
279
|
-
const
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
if (deletedCount === 0 && order.version > 0) {
|
|
284
|
-
throw new ConcurrencyException('Order', order.id.value, order.version);
|
|
283
|
+
const version = this.versions.get(order) ?? 0;
|
|
284
|
+
const { deletedCount } = await this.collection.deleteOne({ _id: order.id.value, version });
|
|
285
|
+
if (deletedCount === 0 && version > 0) {
|
|
286
|
+
throw new ConcurrencyException('Order', order.id.value, version);
|
|
285
287
|
}
|
|
286
288
|
}
|
|
287
289
|
}
|
|
@@ -298,48 +300,54 @@ import type {
|
|
|
298
300
|
AggregateSaver,
|
|
299
301
|
PrimitiveTypes,
|
|
300
302
|
} from '@archi-code/ddd/domain';
|
|
301
|
-
import { ConcurrencyException } from '@archi-code/ddd/
|
|
303
|
+
import { ConcurrencyException } from '@archi-code/ddd/application';
|
|
302
304
|
|
|
303
305
|
export class InMemoryOrderRepository
|
|
304
306
|
implements AggregateFinder<Order>, AggregateSaver<Order>, AggregateRemover<Order>
|
|
305
307
|
{
|
|
306
308
|
private readonly rows = new Map<string, { version: number; primitives: PrimitiveTypes<Order> }>();
|
|
309
|
+
private readonly versions = new WeakMap<Order, number>();
|
|
307
310
|
|
|
308
311
|
async findById(id: OrderId): Promise<Order | null> {
|
|
309
312
|
const row = this.rows.get(id.value);
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
+
if (row === undefined) {
|
|
314
|
+
return null;
|
|
315
|
+
}
|
|
316
|
+
const order = Order.fromPrimitives(structuredClone(row.primitives));
|
|
317
|
+
this.versions.set(order, row.version);
|
|
318
|
+
return order;
|
|
313
319
|
}
|
|
314
320
|
|
|
315
321
|
async save(order: Order): Promise<void> {
|
|
316
|
-
const
|
|
322
|
+
const version = this.checkedVersion(order);
|
|
317
323
|
const primitives = structuredClone(order.toPrimitives());
|
|
318
|
-
this.rows.set(
|
|
319
|
-
|
|
324
|
+
this.rows.set(order.id.value, { version: version + 1, primitives });
|
|
325
|
+
this.versions.set(order, version + 1);
|
|
320
326
|
}
|
|
321
327
|
|
|
322
328
|
async remove(order: Order): Promise<void> {
|
|
323
|
-
this.
|
|
329
|
+
this.checkedVersion(order);
|
|
330
|
+
this.rows.delete(order.id.value);
|
|
324
331
|
}
|
|
325
332
|
|
|
326
|
-
private
|
|
333
|
+
private checkedVersion(order: Order): number {
|
|
327
334
|
const key = order.id.value;
|
|
328
|
-
|
|
329
|
-
|
|
335
|
+
const version = this.versions.get(order) ?? 0;
|
|
336
|
+
if ((this.rows.get(key)?.version ?? 0) !== version) {
|
|
337
|
+
throw new ConcurrencyException('Order', key, version);
|
|
330
338
|
}
|
|
331
|
-
return
|
|
339
|
+
return version;
|
|
332
340
|
}
|
|
333
341
|
}
|
|
334
342
|
```
|
|
335
343
|
|
|
336
|
-
Wiring and tests with the in-memory adapters (repository, event channel) and the
|
|
344
|
+
Wiring and tests with the in-memory adapters (repository, event channel), and the kind of an error:
|
|
337
345
|
|
|
338
346
|
```ts
|
|
339
347
|
const orders = new InMemoryOrderRepository();
|
|
340
348
|
const registry = new EventRegistry<OrderEvent>().register(
|
|
341
349
|
OrderLineAdded,
|
|
342
|
-
(json) => new OrderLineAdded(json.
|
|
350
|
+
(json) => new OrderLineAdded(json.orderId as string, json.sku as string, json.quantity as number, json),
|
|
343
351
|
);
|
|
344
352
|
const channel = new InMemoryEventChannel(); // swap for your RabbitMQ or database channel
|
|
345
353
|
const bus = new DomainEventBus<OrderEvent>(channel, registry);
|
|
@@ -354,8 +362,7 @@ await new AddOrderLine(orders, bus, clock).execute({
|
|
|
354
362
|
});
|
|
355
363
|
channel.sent.map((event) => event.eventName); // ['OrderLineAdded']: PrepareShipment already ran
|
|
356
364
|
|
|
357
|
-
|
|
358
|
-
const { status, body } = errors.map(caughtError);
|
|
365
|
+
ExceptionClassifier.classify(new OrderShippedException(orderId)); // ExceptionKind.Invalid ('invalid')
|
|
359
366
|
```
|
|
360
367
|
|
|
361
368
|
A test of the event flow asserts on what the listener caused and on what the channel carried,
|
|
@@ -385,7 +392,7 @@ describe('Adding a line to an order', () => {
|
|
|
385
392
|
|
|
386
393
|
expect(prepared).toEqual([`${orderId}:TORCH-01`]);
|
|
387
394
|
expect(channel.sent).toMatchObject([
|
|
388
|
-
{ eventName: 'OrderLineAdded',
|
|
395
|
+
{ eventName: 'OrderLineAdded', orderId, sku: 'TORCH-01', quantity: 2 },
|
|
389
396
|
]);
|
|
390
397
|
});
|
|
391
398
|
});
|
|
@@ -478,9 +485,9 @@ integration test waits for the effect (polling until `prepared` has the line) in
|
|
|
478
485
|
right after `execute`.
|
|
479
486
|
|
|
480
487
|
Tests with Vitest: describe real situations, use a plain fixture factory in the project's
|
|
481
|
-
`test/support/` and the native matchers (there is no archi-code test package). Dates held by VOs
|
|
482
|
-
|
|
483
|
-
`toStrictEqual` against a plain `Date
|
|
488
|
+
`test/support/` and the native matchers (there is no archi-code test package). Dates held by VOs are
|
|
489
|
+
immutable `Date` subclasses: compare them with `toEqual` or `.toISOString()`, not
|
|
490
|
+
`toStrictEqual` against a plain `Date`; an event's `occurredOn` is already ISO text.
|
|
484
491
|
|
|
485
492
|
```ts
|
|
486
493
|
import { describe, expect, it } from 'vitest';
|
|
@@ -496,8 +503,8 @@ describe('Order', () => {
|
|
|
496
503
|
id: orderId,
|
|
497
504
|
lines: [{ sku: 'TORCH-01', quantity: 2 }],
|
|
498
505
|
});
|
|
499
|
-
expect(order.pullDomainEvents().map((event) => event
|
|
500
|
-
{ eventName: 'OrderPlaced',
|
|
506
|
+
expect(order.pullDomainEvents().map((event) => ({ ...event }))).toMatchObject([
|
|
507
|
+
{ eventName: 'OrderPlaced', orderId, customer: 'Ana' },
|
|
501
508
|
{ eventName: 'OrderLineAdded', sku: 'TORCH-01', quantity: 2 },
|
|
502
509
|
]);
|
|
503
510
|
});
|