@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/README.md
CHANGED
|
@@ -2,17 +2,17 @@
|
|
|
2
2
|
|
|
3
3
|
Domain-Driven Design building blocks for TypeScript, one entry point per layer:
|
|
4
4
|
|
|
5
|
-
| Entry point | Layer | What it gives you
|
|
6
|
-
| -------------------------------- | -------------- |
|
|
7
|
-
| `@archi-code/ddd/
|
|
8
|
-
| `@archi-code/ddd/
|
|
9
|
-
| `@archi-code/ddd/
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
5
|
+
| Entry point | Layer | What it gives you |
|
|
6
|
+
| -------------------------------- | -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
7
|
+
| `@archi-code/ddd/domain` | Domain | Value objects, `Required(...)`, `Composite(...)`, `Entity`, `AggregateRoot`, `EventBase`, `Specification`, the repository contracts (`AggregateFinder`, `AggregateSaver`, `AggregateRemover`) and the domain exceptions (`DomainException`, `ValidationException`…) |
|
|
8
|
+
| `@archi-code/ddd/application` | Application | `UseCase`, `ValueObjectBuilder`, the event contract (`EventListener`, `@ListensTo` to listen, `EventBus` to publish), the ports `Clock` and `Logger` and the application exceptions (`AggregateNotFoundException`, `NotAllowedException`…) |
|
|
9
|
+
| `@archi-code/ddd/infrastructure` | Infrastructure | The event wiring (`DomainEventBus`, `EventRegistry`, the `EventChannel` port, `InMemoryEventChannel`), clocks, loggers, the infrastructure exceptions and `ExceptionClassifier`, which tells any entry point the `ExceptionKind` of an error |
|
|
10
|
+
|
|
11
|
+
The layers only depend inwards: `domain` imports nothing of the package, `application` imports
|
|
12
|
+
`domain`, and `infrastructure` imports both. Each layer declares and exports its own exceptions,
|
|
13
|
+
with codes, causes and `instanceof` across builds: a value object throws a `DomainException`, a
|
|
14
|
+
use case throws `AggregateNotFoundException` and wraps unknown errors in
|
|
15
|
+
`UnexpectedErrorException`, an adapter throws an `InfrastructureException`. Import each file of your project from the layer it
|
|
16
16
|
belongs to and the same rule holds in your code (see [Architecture rules](#architecture-rules)). Conversions and validations come from
|
|
17
17
|
[`@archi-code/validation`](https://www.npmjs.com/package/@archi-code/validation). The package is
|
|
18
18
|
published with CJS and ESM builds.
|
|
@@ -23,7 +23,10 @@ published with CJS and ESM builds.
|
|
|
23
23
|
npm install @archi-code/ddd
|
|
24
24
|
```
|
|
25
25
|
|
|
26
|
-
- Node.js `>=24` (`engines` field).
|
|
26
|
+
- Node.js `>=24` (`engines` field) and browsers: every subpath runs unchanged in both. Its ESM
|
|
27
|
+
build imports `@archi-code/validation` and `uuid` by name, so in a browser use a bundler (Vite,
|
|
28
|
+
webpack, esbuild…) or an import map. `/domain`, `/application` and `/infrastructure` are checked
|
|
29
|
+
in Chromium and WebKit, `/domain` under a strict Content-Security-Policy without `unsafe-eval`.
|
|
27
30
|
- Runtime dependencies, installed by npm automatically: `@archi-code/validation` and `uuid`.
|
|
28
31
|
- To declare validations with `@Validations`, also install `@archi-code/validation` as a direct
|
|
29
32
|
dependency of your project (the decorator is imported from it):
|
|
@@ -49,11 +52,11 @@ There is no root entry point: import from the layer.
|
|
|
49
52
|
```ts
|
|
50
53
|
// ESM
|
|
51
54
|
import { AggregateRoot, EventBase, IdType, Required, StringType } from '@archi-code/ddd/domain';
|
|
52
|
-
import { DomainException,
|
|
55
|
+
import { DomainException, DomainExceptionCode } from '@archi-code/ddd/domain';
|
|
53
56
|
import type { AggregateSaver } from '@archi-code/ddd/domain';
|
|
54
57
|
import { EventListener, ListensTo, UseCase } from '@archi-code/ddd/application';
|
|
55
58
|
import type { EventBus } from '@archi-code/ddd/application';
|
|
56
|
-
import {
|
|
59
|
+
import { ExceptionClassifier } from '@archi-code/ddd/infrastructure';
|
|
57
60
|
```
|
|
58
61
|
|
|
59
62
|
```js
|
|
@@ -67,43 +70,44 @@ subpaths also resolve with `"moduleResolution": "node"` (through `typesVersions`
|
|
|
67
70
|
|
|
68
71
|
Both builds can be loaded in the same process (for example a CommonJS application that uses an ESM
|
|
69
72
|
library built on this package): the exceptions of one build are `instanceof` the classes of the other
|
|
70
|
-
(see [Exceptions](#exceptions
|
|
73
|
+
(see [Exceptions](#exceptions)).
|
|
71
74
|
|
|
72
75
|
## Architecture rules
|
|
73
76
|
|
|
74
77
|
Every class of a project built on this package goes in exactly one layer. The table says where,
|
|
75
78
|
what it may import and what it must never do; the package itself is organized the same way.
|
|
76
79
|
|
|
77
|
-
| Class | Layer | Responsibility | May depend on
|
|
78
|
-
| ------------------------------------------------------------------------------ | -------------- | -------------------------------------------------------------------------------------------------- |
|
|
79
|
-
| Value object (`extends StringType`…) | domain | One concept of the business; converts and validates its own value | other value objects, `@archi-code/validation`, exceptions
|
|
80
|
-
| Aggregate (`extends AggregateRoot`) | domain | Business rules and invariants; records an event for every fact | its value objects, child entities, events, domain services, domain ports
|
|
81
|
-
| Composite value object (`Composite({...})`) | domain | Several values that only make sense together (money, a date range, an order line) | value objects, other composites
|
|
82
|
-
| Child entity (`extends Entity`) | domain | A part of an aggregate with its own id; value objects and business methods only the root calls | value objects
|
|
83
|
-
| Specification (`extends Specification`) | domain | A named business rule that validates, selects or builds; combinable | value objects, aggregates, data already loaded
|
|
84
|
-
| Domain event (`extends EventBase`) | domain | A fact that happened, named in the past (`OrderPlaced`, `'OrderPlaced'`), with primitive data | value objects (to take its primitives from them)
|
|
85
|
-
| Repository contract (`AggregateFinder`…) | domain | Only gets and persists aggregates, one role per interface; no business logic | the aggregate
|
|
86
|
-
| Domain service | domain | A business rule that spans several aggregates or fits none | aggregates, value objects, `AggregateFinder` (to read), domain ports
|
|
87
|
-
| Domain port (`ProductPricing`) | domain | What the model needs from another context or an external service, in its own words | value objects of this context
|
|
88
|
-
| Use case (`extends UseCase`) | application | One business action: input → command of value objects → change one aggregate | domain, application ports
|
|
89
|
-
| Listener (`extends EventListener`) | application | What happens after an event: turns it into the input of one use case; lives next to that use case | one use case (by constructor), the event
|
|
90
|
-
| Application port (`EventBus`, `Clock`, `Logger`, your own such as `Warehouse`) | application | A technical capability the application needs, as a type | domain types
|
|
91
|
-
| Read query (`OrderSummaries`) | application | An interface that answers a screen or a list with DTOs or ids, outside the aggregates | domain types, DTOs
|
|
92
|
-
| Adapter (repository, outbox, read query, clock…) | infrastructure | Implements a contract with a database, a queue, a broker, HTTP or memory | domain, application, exceptions, drivers
|
|
93
|
-
| Anti-corruption layer (adapter + translator) | infrastructure | Implements a domain port by calling another context or service and translating its model into ours | domain, application, exceptions, clients
|
|
94
|
-
| Entry point (controller, message consumer, CLI) | interfaces | Turns HTTP, a message or a command line into the input of a use case, and back | application,
|
|
95
|
-
| Composition root (`composition.ts`) | (per context) | Reads the configuration, builds adapters, use cases and entry points | everything
|
|
96
|
-
|
|
97
|
-
Dependencies only point inwards. Your domain imports only `@archi-code/ddd/domain
|
|
98
|
-
`@archi-code/
|
|
80
|
+
| Class | Layer | Responsibility | May depend on | Never |
|
|
81
|
+
| ------------------------------------------------------------------------------ | -------------- | -------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------- |
|
|
82
|
+
| Value object (`extends StringType`…) | domain | One concept of the business; converts and validates its own value | other value objects, `@archi-code/validation`, domain exceptions | mutate, know about storage or transport |
|
|
83
|
+
| Aggregate (`extends AggregateRoot`) | domain | Business rules and invariants; records an event for every fact | its value objects, child entities, events, domain services, domain ports | read a repository, publish events, use `new Date()` |
|
|
84
|
+
| Composite value object (`Composite({...})`) | domain | Several values that only make sense together (money, a date range, an order line) | value objects, other composites | change, hold an id, be loaded or saved on its own |
|
|
85
|
+
| Child entity (`extends Entity`) | domain | A part of an aggregate with its own id; value objects and business methods only the root calls | value objects | setters, be called from outside its aggregate, be loaded or saved on its own |
|
|
86
|
+
| Specification (`extends Specification`) | domain | A named business rule that validates, selects or builds; combinable | value objects, aggregates, data already loaded | read a repository, protect an aggregate invariant |
|
|
87
|
+
| Domain event (`extends EventBase`) | domain | A fact that happened, named in the past (`OrderPlaced`, `'OrderPlaced'`), with primitive data | value objects (to take its primitives from them) | carry behavior or value objects, reference infrastructure |
|
|
88
|
+
| Repository contract (`AggregateFinder`…) | domain | Only gets and persists aggregates, one role per interface; no business logic | the aggregate | be implemented in the domain, hold rules, answer business queries |
|
|
89
|
+
| Domain service | domain | A business rule that spans several aggregates or fits none | aggregates, value objects, `AggregateFinder` (to read), domain ports | I/O, saving, transactions, publishing |
|
|
90
|
+
| Domain port (`ProductPricing`) | domain | What the model needs from another context or an external service, in its own words | value objects of this context | contain code, expose the other system's model |
|
|
91
|
+
| Use case (`extends UseCase`) | application | One business action: input → command of value objects → change one aggregate | domain, application ports and exceptions | import another use case or a listener, persist or call the network by itself |
|
|
92
|
+
| Listener (`extends EventListener`) | application | What happens after an event: turns it into the input of one use case; lives next to that use case | one use case (by constructor), the event | hold business rules, save aggregates, call several use cases |
|
|
93
|
+
| Application port (`EventBus`, `Clock`, `Logger`, your own such as `Warehouse`) | application | A technical capability the application needs, as a type | domain types | contain code |
|
|
94
|
+
| Read query (`OrderSummaries`) | application | An interface that answers a screen or a list with DTOs or ids, outside the aggregates | domain types, DTOs | load aggregates, go through a repository, change data |
|
|
95
|
+
| Adapter (repository, outbox, read query, clock…) | infrastructure | Implements a contract with a database, a queue, a broker, HTTP or memory | domain, application, exceptions, drivers | contain business rules |
|
|
96
|
+
| Anti-corruption layer (adapter + translator) | infrastructure | Implements a domain port by calling another context or service and translating its model into ours | domain, application, exceptions, clients | leak the other model into the domain, hold business rules |
|
|
97
|
+
| Entry point (controller, message consumer, CLI) | interfaces | Turns HTTP, a message or a command line into the input of a use case, and back | application, `ExceptionClassifier` and `ExceptionKind` (domain types only if unavoidable) | hold business rules, use a repository or another adapter directly |
|
|
98
|
+
| Composition root (`composition.ts`) | (per context) | Reads the configuration, builds adapters, use cases and entry points | everything | be imported by the domain, the application or the interfaces |
|
|
99
|
+
|
|
100
|
+
Dependencies only point inwards. Your domain imports only `@archi-code/ddd/domain` and
|
|
101
|
+
`@archi-code/validation`; the application adds
|
|
99
102
|
`@archi-code/ddd/application`; infrastructure implements the contracts of both; and `interfaces`
|
|
100
103
|
(controllers, consumers) only calls the application. The application never imports
|
|
101
104
|
infrastructure, the domain never imports the application, and an entry point never reaches a
|
|
102
|
-
repository or an adapter: only the composition root knows every piece [IDDD ch. 4].
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
result (an id, a DTO or nothing) to the response and the errors with
|
|
106
|
-
|
|
105
|
+
repository or an adapter: only the composition root knows every piece [IDDD ch. 4]. An entry
|
|
106
|
+
point imports nothing from infrastructure but `ExceptionClassifier` and `ExceptionKind` of
|
|
107
|
+
`@archi-code/ddd/infrastructure`, which hold no adapter. A controller validates only the shape of the request,
|
|
108
|
+
maps the use case result (an id, a DTO or nothing) to the response and the errors with
|
|
109
|
+
`ExceptionClassifier` (see [Translating errors at an entry point](#translating-errors-at-an-entry-point));
|
|
110
|
+
it never returns an aggregate.
|
|
107
111
|
|
|
108
112
|
### Folder structure
|
|
109
113
|
|
|
@@ -149,7 +153,7 @@ test/ mirrors src/
|
|
|
149
153
|
without npm packages but the allowed ones, no imports from `application`, `infrastructure` or
|
|
150
154
|
`interfaces` into the domain, none from `infrastructure` or `interfaces` into the application, no
|
|
151
155
|
`@archi-code/ddd/infrastructure` in the domain or the application, none from `interfaces` into
|
|
152
|
-
your `infrastructure`
|
|
156
|
+
your `infrastructure` nor `@archi-code/ddd/infrastructure`, isolated contexts, no cycles,
|
|
153
157
|
and `tsPreCompilationDeps: true` so type imports count too). Adapt its paths to your layout.
|
|
154
158
|
- Environment variables are read only in the composition root and reach the adapters as typed
|
|
155
159
|
configuration; the domain never logs nor reads `process.env`.
|
|
@@ -258,8 +262,7 @@ the browser? of the server?). Send it with `Z` or an offset, or send only the da
|
|
|
258
262
|
| `value` | `T \| null` (or `T`) | The converted value; `T` if the type is required (`Required`) |
|
|
259
263
|
| `isNull` | `boolean` | `true` if there is no value |
|
|
260
264
|
| `toString()` | `string` | Text of the value; `''` if it is `null`. `DateType` gives ISO 8601, `JsonType` the JSON |
|
|
261
|
-
| `
|
|
262
|
-
| `equals(other)` | `boolean` | Same class and same value (dates by instant, arrays and objects by content) |
|
|
265
|
+
| `equals(other)` | `boolean` | Same class and same value: `===`; dates by instant, JSON by content, lists item by item |
|
|
263
266
|
| `validate()` | `ValidationResult<T>` | Runs the `@Validations` rules again; never throws (always ok on a built value object) |
|
|
264
267
|
|
|
265
268
|
```ts
|
|
@@ -268,11 +271,11 @@ class Country extends StringType {}
|
|
|
268
271
|
|
|
269
272
|
const city = new City('Huelva');
|
|
270
273
|
`Shipping to ${city}`; // 'Shipping to Huelva'
|
|
271
|
-
JSON.stringify({ city }); // '{"city":"Huelva"}'
|
|
274
|
+
JSON.stringify({ city: city.value }); // '{"city":"Huelva"}': write `value`, not the value object
|
|
272
275
|
city.equals(new City('Huelva')); // true
|
|
273
276
|
city.equals(new Country('Huelva')); // false: another concept, even with the same text
|
|
274
277
|
new NumberType().toString(); // ''
|
|
275
|
-
new StringType(' ').value; //
|
|
278
|
+
new StringType(' ').value; // ' ' (blank text is a value: only null and undefined are empty)
|
|
276
279
|
new StringType(null).isNull; // true
|
|
277
280
|
```
|
|
278
281
|
|
|
@@ -299,11 +302,15 @@ const nickname: string | null = new Nickname().value; // null
|
|
|
299
302
|
|
|
300
303
|
new Name(null); // type error and, at runtime:
|
|
301
304
|
// RequiredValueException: Validation Error: Name is required, but received null.
|
|
302
|
-
new Name(' ');
|
|
303
|
-
|
|
305
|
+
new Name(' ').value; // ' ': blank text is a value, not a missing one
|
|
306
|
+
new NumberType(' ');
|
|
307
|
+
// TypePrimitiveException: Validation Error: Expected a valid Number, but received " ".
|
|
304
308
|
```
|
|
305
309
|
|
|
306
|
-
`null
|
|
310
|
+
Only `null` and `undefined` count as empty. Any other input, a blank text included, goes through
|
|
311
|
+
the type's conversion: `StringType` keeps it as written and the other types refuse it with
|
|
312
|
+
`TypePrimitiveException`. Add a rule (`isLength`, a `custom` one) when a blank text must not be
|
|
313
|
+
accepted. A subclass of a required value
|
|
307
314
|
object is also required (`class FullName extends Name {}`). `Required(...)` accepts any subclass of
|
|
308
315
|
`AbstractType`, including your own and abstract ones (`Required(StatusType)`, `Required(Tags)`).
|
|
309
316
|
|
|
@@ -364,77 +371,18 @@ Validator names and their `properties` are in the `@archi-code/validation` docum
|
|
|
364
371
|
`validate()` runs the rules again and returns a frozen `{ ok, value, errors }` without throwing;
|
|
365
372
|
on a value object that was built it is always `ok`.
|
|
366
373
|
|
|
367
|
-
### Building every value object of an input at once: `ValueObjects.build`
|
|
368
|
-
|
|
369
|
-
The constructor of each value object stops at its own errors. To report **every field** of an
|
|
370
|
-
input (a form, a JSON body, a message) at once, build the value objects with
|
|
371
|
-
`ValueObjects.build`: it runs every factory, even after one fails, and throws one
|
|
372
|
-
`ValidationException` whose `errors` name the field of each problem. It collects the three ways a
|
|
373
|
-
value can be wrong: empty in a required type (`RequiredValueException`), not convertible
|
|
374
|
-
(`TypePrimitiveException`) and broken rules (`ValidationException`), plus any other
|
|
375
|
-
`DomainException` a factory throws. Any other error is a bug and goes through untouched, and it
|
|
376
|
-
wins: the validation errors already collected are dropped, so a bug is never hidden behind a 422.
|
|
377
|
-
`ValueObjects.buildList` does the same for a list and names each item by its position, so nested
|
|
378
|
-
inputs get paths such as `lines.1.quantity`. Factories are synchronous: a factory that returns a
|
|
379
|
-
Promise does not compile in `build`, and both methods refuse it at run time with
|
|
380
|
-
`InternalErrorException` (`ValueObjects: the factory of "email" returned a Promise; build value
|
|
381
|
-
objects synchronously`), so an asynchronous check never skips the validation. Every field name is
|
|
382
|
-
kept as written, `__proto__` included.
|
|
383
|
-
|
|
384
|
-
```ts
|
|
385
|
-
import { IdType, NumberType, Required, StringType, ValueObjects } from '@archi-code/ddd/domain';
|
|
386
|
-
import { Validations } from '@archi-code/validation';
|
|
387
|
-
|
|
388
|
-
// Email as in the previous block; input is the raw request body
|
|
389
|
-
|
|
390
|
-
class OrderId extends IdType<'OrderId'> {}
|
|
391
|
-
class Sku extends Required(StringType) {}
|
|
392
|
-
|
|
393
|
-
@Validations([
|
|
394
|
-
{ validator: 'isInt', properties: { min: 1, max: 20 }, message: 'From 1 to 20 units' },
|
|
395
|
-
])
|
|
396
|
-
class Quantity extends Required(NumberType) {}
|
|
397
|
-
|
|
398
|
-
const command = ValueObjects.build({
|
|
399
|
-
orderId: () => new OrderId(input.orderId),
|
|
400
|
-
email: () => new Email(input.email),
|
|
401
|
-
lines: () =>
|
|
402
|
-
ValueObjects.buildList(input.lines, (line) =>
|
|
403
|
-
ValueObjects.build({
|
|
404
|
-
sku: () => new Sku(line.sku),
|
|
405
|
-
quantity: () => new Quantity(line.quantity),
|
|
406
|
-
}),
|
|
407
|
-
),
|
|
408
|
-
});
|
|
409
|
-
// command: { orderId: OrderId; email: Email; lines: { sku: Sku; quantity: Quantity }[] }
|
|
410
|
-
|
|
411
|
-
// with input = { orderId: 'x', email: 'ana', lines: [{ sku: 'TORCH-01', quantity: 2 }, { sku: '', quantity: 0 }] }:
|
|
412
|
-
// ValidationException, error.errors:
|
|
413
|
-
// [
|
|
414
|
-
// { field: 'orderId', code: 'DOM003', message: 'Validation Error: Expected a valid UUID, but received "x".' },
|
|
415
|
-
// { field: 'email', code: 'isEmail', message: 'Email: Value does not satisfy isEmail' },
|
|
416
|
-
// { field: 'lines.1.sku', code: 'DOM004', message: 'Validation Error: Sku is required, but received "".' },
|
|
417
|
-
// { field: 'lines.1.quantity', code: 'isInt', message: 'Quantity: From 1 to 20 units' },
|
|
418
|
-
// ]
|
|
419
|
-
```
|
|
420
|
-
|
|
421
|
-
`HttpErrorMapper` answers it with a 422 whose body carries the same `errors`, so a client can mark
|
|
422
|
-
each input. To report a problem of your own in the same shape, throw
|
|
423
|
-
`new ValidationException([{ field: 'deliveryDate', code: 'ORD003', message: '…' }])`; `code` is
|
|
424
|
-
optional and defaults to `'DOM001'`.
|
|
425
|
-
|
|
426
374
|
### Which layer validates what
|
|
427
375
|
|
|
428
376
|
The input that reaches a use case is not validated yet, and that is on purpose: every adapter
|
|
429
377
|
(HTTP, a queue, a CLI, a test) goes through the use case, so the rules live in one place.
|
|
430
378
|
|
|
431
|
-
| What | Where
|
|
432
|
-
| --------------------------------------------------------- |
|
|
433
|
-
| The shape of the request (it parses, a field is a string) | The input adapter (controller, consumer); no business rule
|
|
434
|
-
| One value: required, convertible, its rules | The value object's constructor, through `
|
|
435
|
-
| Rules over several values of one aggregate (invariants) | The aggregate's methods, with a `DomainException`
|
|
436
|
-
| Rules over several aggregates | A domain service
|
|
437
|
-
| Whether the caller may run the action | The use case (application)
|
|
379
|
+
| What | Where |
|
|
380
|
+
| --------------------------------------------------------- | --------------------------------------------------------------------------------- |
|
|
381
|
+
| The shape of the request (it parses, a field is a string) | The input adapter (controller, consumer); no business rule |
|
|
382
|
+
| One value: required, convertible, its rules | The value object's constructor, through `ValueObjectBuilder.build` in `toCommand` |
|
|
383
|
+
| Rules over several values of one aggregate (invariants) | The aggregate's methods, with a `DomainException` |
|
|
384
|
+
| Rules over several aggregates | A domain service |
|
|
385
|
+
| Whether the caller may run the action | The use case (application) |
|
|
438
386
|
|
|
439
387
|
Do not repeat a value object's rule in the request schema: the adapter checks that the email is a
|
|
440
388
|
string, `Email` checks that it is an email. Invariants run after the fields are valid, because they
|
|
@@ -452,13 +400,13 @@ one:
|
|
|
452
400
|
// TypeError: Cannot call setFullYear: the date is immutable
|
|
453
401
|
```
|
|
454
402
|
|
|
455
|
-
- `JsonType` and `ArrayType`
|
|
403
|
+
- `JsonType` holds a deeply frozen copy and `ArrayType` a frozen list of its items' values
|
|
404
|
+
(`Object.isFrozen(vo.value)` is `true`).
|
|
456
405
|
- `ArrayType.withItem()` and `withoutItem()` return a **new** instance and leave the original
|
|
457
406
|
untouched, so a value object shared by two aggregates never changes under one of them.
|
|
458
407
|
- To touch an `UpdatedAt`, replace it: `this._updatedAt = UpdatedAt.now()`.
|
|
459
|
-
-
|
|
460
|
-
|
|
461
|
-
setters throw.
|
|
408
|
+
- Each value object guarantees its own immutability: a custom type that holds an object copies and
|
|
409
|
+
freezes it in its `filter` (see the `GeoPoint` example below).
|
|
462
410
|
|
|
463
411
|
### `UuidType` and `IdType`
|
|
464
412
|
|
|
@@ -624,28 +572,26 @@ new Tags('abc' as any);
|
|
|
624
572
|
// TypePrimitiveException: Validation Error: Expected a valid Array, but received "abc".
|
|
625
573
|
```
|
|
626
574
|
|
|
627
|
-
An `ArrayType`
|
|
628
|
-
|
|
575
|
+
An `ArrayType` receives items that are already valid (checked by `toCommand` or read from
|
|
576
|
+
storage), so it does not collect item errors: it builds the items in order and lets the error of
|
|
577
|
+
the first invalid one through as the item throws it, and then runs the rules of the list
|
|
578
|
+
(`field: null`):
|
|
629
579
|
|
|
630
580
|
```ts
|
|
631
|
-
new Tags(['ok', 'x',
|
|
581
|
+
new Tags(['ok', 'x', null] as any);
|
|
632
582
|
// ValidationException, error.errors:
|
|
633
|
-
// [
|
|
634
|
-
// { field: '1', code: 'isLength', message: 'Tag: Value does not satisfy isLength' },
|
|
635
|
-
// { field: '2', code: 'DOM004', message: 'Validation Error: Tag is required, but received "".' },
|
|
636
|
-
// ]
|
|
583
|
+
// [{ field: null, code: 'isLength', message: 'Tag: Value does not satisfy isLength' }]
|
|
637
584
|
```
|
|
638
585
|
|
|
639
586
|
### Custom value objects with `AbstractType`
|
|
640
587
|
|
|
641
588
|
For a new type, extend `AbstractType<T>` and implement `filter(value)`: it receives the input
|
|
642
|
-
already known to be
|
|
643
|
-
is not valid. Empty input (`null`, `undefined
|
|
644
|
-
class.
|
|
589
|
+
already known not to be `null` nor `undefined` (a blank text does reach it), returns the converted
|
|
590
|
+
value and throws `TypePrimitiveException` if it is not valid. Empty input (`null`, `undefined`) and
|
|
591
|
+
`Required(...)` are handled by the base class.
|
|
645
592
|
|
|
646
593
|
```ts
|
|
647
|
-
import { AbstractType, Required } from '@archi-code/ddd/domain';
|
|
648
|
-
import { TypePrimitiveException } from '@archi-code/ddd/exceptions';
|
|
594
|
+
import { AbstractType, Required, TypePrimitiveException } from '@archi-code/ddd/domain';
|
|
649
595
|
|
|
650
596
|
class Percentage extends AbstractType<number> {
|
|
651
597
|
protected filter(value: unknown): number {
|
|
@@ -664,6 +610,27 @@ new Percentage(140);
|
|
|
664
610
|
class Discount extends Required(Percentage) {}
|
|
665
611
|
```
|
|
666
612
|
|
|
613
|
+
`equals` compares the class and then the values with `===`. A type whose value is an object or a
|
|
614
|
+
list says how two of them compare by overriding `protected hasSameValue(other)` (it receives the other
|
|
615
|
+
value object, already known to be of the same class, and compares `this._value` with
|
|
616
|
+
`other._value`, either of them possibly `null`), like `DateType` (by instant), `JsonType` (by content) and
|
|
617
|
+
`ArrayType` (each item with its `equals`) do:
|
|
618
|
+
|
|
619
|
+
```ts
|
|
620
|
+
class GeoPoint extends AbstractType<{ lat: number; lng: number }> {
|
|
621
|
+
protected filter(value: unknown): { lat: number; lng: number } {
|
|
622
|
+
const { lat, lng } = value as { lat: number; lng: number };
|
|
623
|
+
return Object.freeze({ lat: Number(lat), lng: Number(lng) });
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
protected hasSameValue(other: this): boolean {
|
|
627
|
+
return this._value?.lat === other._value?.lat && this._value?.lng === other._value?.lng;
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
new GeoPoint({ lat: 37.26, lng: -6.95 }).equals(new GeoPoint({ lng: -6.95, lat: 37.26 })); // true
|
|
632
|
+
```
|
|
633
|
+
|
|
667
634
|
To restrict an existing type, it is enough to extend it and declare `@Validations`, like `Email`.
|
|
668
635
|
|
|
669
636
|
### Composite value objects: `Composite({...})`
|
|
@@ -681,8 +648,14 @@ class of each field:
|
|
|
681
648
|
|
|
682
649
|
```ts
|
|
683
650
|
import { Validations } from '@archi-code/validation';
|
|
684
|
-
import {
|
|
685
|
-
|
|
651
|
+
import {
|
|
652
|
+
Composite,
|
|
653
|
+
DateType,
|
|
654
|
+
DomainException,
|
|
655
|
+
NumberType,
|
|
656
|
+
Required,
|
|
657
|
+
StringType,
|
|
658
|
+
} from '@archi-code/ddd/domain';
|
|
686
659
|
|
|
687
660
|
@Validations([{ validator: 'isInt' }])
|
|
688
661
|
class Cents extends Required(NumberType) {}
|
|
@@ -716,7 +689,7 @@ price.add(new Money({ amount: new Cents(100), currency: new Currency('USD') }));
|
|
|
716
689
|
new Cents(12.5);
|
|
717
690
|
// ValidationException: Validation failed, Cents: Value does not satisfy isInt
|
|
718
691
|
price.equals(Money.fromPrimitives({ amount: 1250, currency: 'EUR' })); // true
|
|
719
|
-
JSON.stringify(price); // '{"amount":1250,"currency":"EUR"}'
|
|
692
|
+
JSON.stringify(price.toPrimitives()); // '{"amount":1250,"currency":"EUR"}'
|
|
720
693
|
```
|
|
721
694
|
|
|
722
695
|
- **The constructor** takes value objects only. A missing field throws `RequiredValueException`
|
|
@@ -750,7 +723,7 @@ JSON.stringify(price); // '{"amount":1250,"currency":"EUR"}'
|
|
|
750
723
|
- **`X.fromPrimitives(data)`** rebuilds it from stored or received data, converting each primitive
|
|
751
724
|
like the value objects do, and reports every invalid field with its path, also nested ones.
|
|
752
725
|
`data` is a `CompositeInput`: each field takes what its value object's constructor takes (a date
|
|
753
|
-
field takes a `Date` or ISO text). It works inside `
|
|
726
|
+
field takes a `Date` or ISO text). It works inside `ValueObjectBuilder.build`, so a command reports
|
|
754
727
|
`price.amount`:
|
|
755
728
|
|
|
756
729
|
```ts
|
|
@@ -760,11 +733,11 @@ JSON.stringify(price); // '{"amount":1250,"currency":"EUR"}'
|
|
|
760
733
|
class Cart extends Composite({ lines: [OrderLine] }) {}
|
|
761
734
|
|
|
762
735
|
Cart.fromPrimitives({
|
|
763
|
-
lines: [{ sku:
|
|
736
|
+
lines: [{ sku: null, quantity: 'two', unitPrice: { amount: 1250, currency: 'GBP' } }],
|
|
764
737
|
} as any);
|
|
765
738
|
// ValidationException, error.errors:
|
|
766
739
|
// [
|
|
767
|
-
// { field: 'lines.0.sku', code: 'DOM004', message: 'Validation Error: Sku is required, but received
|
|
740
|
+
// { field: 'lines.0.sku', code: 'DOM004', message: 'Validation Error: Sku is required, but received null.' },
|
|
768
741
|
// { field: 'lines.0.quantity', code: 'DOM003', message: 'Validation Error: Expected a valid Number, but received "two".' },
|
|
769
742
|
// { field: 'lines.0.unitPrice.currency', code: 'isIn', message: 'Currency: Value does not satisfy isIn' },
|
|
770
743
|
// ]
|
|
@@ -772,7 +745,7 @@ JSON.stringify(price); // '{"amount":1250,"currency":"EUR"}'
|
|
|
772
745
|
|
|
773
746
|
- **`equals(other)`** compares the class and every field (dates by instant, lists in order):
|
|
774
747
|
a `Price` and a `Money` with the same fields are different. **`toPrimitives()`** gives plain data
|
|
775
|
-
for repositories (dates stay `Date`); `JSON.stringify` writes ISO dates.
|
|
748
|
+
for repositories (dates stay `Date`); `JSON.stringify` of them writes ISO dates.
|
|
776
749
|
- **In an aggregate**, a composite is a field like any value object:
|
|
777
750
|
`PrimitiveTypes<Order>` types it as its primitives, and an optional one is a `Money | null`
|
|
778
751
|
field. Write `this.price.toPrimitives()` in `toPrimitives()` and `Money.fromPrimitives(...)` in
|
|
@@ -819,14 +792,11 @@ repository. It does not protect the invariants of an aggregate: those stay in th
|
|
|
819
792
|
- `AggregateRoot<Id = IdType, Event = EventBase>`: abstract class for aggregates. `new (id)` stores
|
|
820
793
|
`readonly id`; `equals(other)` is true for the same class and an equal id, whatever the other
|
|
821
794
|
attributes are. `Id` may be any required value object (`EntityId`: `IdType`, a `Required(StringType)` code…); an optional value object cannot be an id, and an id that holds no value throws `RequiredValueException`, so two new entities are never taken for the same one [IDDD ch. 5]; `record(event)` is `protected` and deep-freezes the event (a fact never changes [IDDD ch. 8]); `pullDomainEvents()` returns the events typed as `Event[]`, in order, and empties
|
|
822
|
-
the list. Pending events are
|
|
823
|
-
|
|
824
|
-
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
calling `super(id)`, and the version is neither a field, nor in its primitives, nor in its JSON.
|
|
828
|
-
Only repositories touch it, with `restoreVersion(version)` when they load and `markPersisted()`
|
|
829
|
-
after they save.
|
|
795
|
+
the list. Pending events are a plain private array: repositories store `toPrimitives()`, never
|
|
796
|
+
the aggregate itself.
|
|
797
|
+
- The aggregate carries no version: optimistic concurrency belongs to the repository, which
|
|
798
|
+
remembers the version of every aggregate it loaded or saved (see
|
|
799
|
+
[Optimistic concurrency](#optimistic-concurrency)). The aggregate only calls `super(id)`.
|
|
830
800
|
- Parts of an aggregate are composite value objects when nobody looks them up by an id of their
|
|
831
801
|
own (the lines of an order). A part that does need its own identity inside the aggregate (a seat
|
|
832
802
|
of a room) is a child entity: it extends `Entity<Id>`, which only stores `readonly id` and
|
|
@@ -840,13 +810,13 @@ repository. It does not protect the invariants of an aggregate: those stay in th
|
|
|
840
810
|
import {
|
|
841
811
|
AggregateRoot,
|
|
842
812
|
BooleanType,
|
|
813
|
+
DomainException,
|
|
843
814
|
Entity,
|
|
844
815
|
IdType,
|
|
845
816
|
NumberType,
|
|
846
817
|
Required,
|
|
847
818
|
StringType,
|
|
848
819
|
} from '@archi-code/ddd/domain';
|
|
849
|
-
import { DomainException } from '@archi-code/ddd/exceptions';
|
|
850
820
|
|
|
851
821
|
class SeatNumber extends Required(NumberType) {}
|
|
852
822
|
class SeatLabel extends Required(StringType) {}
|
|
@@ -898,25 +868,30 @@ repository. It does not protect the invariants of an aggregate: those stay in th
|
|
|
898
868
|
|
|
899
869
|
- `EventBase`: abstract class for events; declare
|
|
900
870
|
`static readonly eventName = '<ClassName>'` (`'OrderPlaced'` [IDDD ch. 8]; the
|
|
901
|
-
`'<context>.<module>.<Event>'` names are for the contracts other contexts read) (listeners and registries refer to the event by its class, without an instance
|
|
902
|
-
|
|
903
|
-
|
|
871
|
+
`'<context>.<module>.<Event>'` names are for the contracts other contexts read) (listeners and registries refer to the event by its class, without an instance, and a subclass
|
|
872
|
+
inherits it). An event is a plain object of primitive data with no methods: `new (metadata?)`
|
|
873
|
+
copies the static name into the `readonly eventName` field and sets `eventId` (a new UUID v4)
|
|
874
|
+
and `occurredOn` (ISO text in UTC, `'2026-10-17T10:00:00.000Z'`); everything else is data the
|
|
875
|
+
subclass declares as `readonly` constructor fields, including the id of the aggregate
|
|
876
|
+
(`readonly orderId: string`). Creating an event whose class has no non-blank `eventName` throws
|
|
877
|
+
`InvalidEventDefinitionException` (`OrderPlaced must declare a static eventName`). Pass
|
|
904
878
|
`{ clock }` (any `{ now(): Date }`, such as the `Clock` port) to take the moment from it; without
|
|
905
879
|
a clock it is the system time. When you rebuild an event a channel delivered, pass
|
|
906
880
|
`{ eventId, occurredOn }` (`Date` or ISO string with zone) to keep them; a stored `occurredOn`
|
|
907
|
-
wins over a clock. The clock is not kept in the event
|
|
908
|
-
`{
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
881
|
+
wins over a clock and is stored as UTC ISO text. The clock is not kept in the event. The event
|
|
882
|
+
is its own JSON: `{ ...event }` and `JSON.stringify(event)` give
|
|
883
|
+
`{ eventName, eventId, occurredOn, ...fields }`, in that order.
|
|
884
|
+
- Rule: each event declares only the primitive data it needs (text, finite numbers, booleans,
|
|
885
|
+
`null`, and lists and plain objects of them), taken from value objects: a value object becomes
|
|
886
|
+
its `.value`, money `{ amount, currency }`, a date its ISO text. Never value objects, `Date`s,
|
|
887
|
+
class instances nor the whole state of the aggregate: the event travels as it is, and its
|
|
888
|
+
readers must not depend on the model. Nothing checks this at run time.
|
|
889
|
+
- An event has no schema version. Adding an optional field that old readers can ignore is
|
|
890
|
+
compatible; a change old readers cannot follow (renaming or removing a field, changing its
|
|
891
|
+
meaning) is published as a new event with its own `eventName` (`CustomerRenamedV2`), or the
|
|
892
|
+
`EventRegistry` factory of the event reads both shapes (`typeof json.name === 'string' ? … : …`).
|
|
893
|
+
- The version a repository stores counts saves, not events: it grows by 1 on each save, however
|
|
894
|
+
many events the change recorded. Event Sourcing is not provided: an event-sourced aggregate manages the version of its
|
|
920
895
|
stream itself.
|
|
921
896
|
|
|
922
897
|
```ts
|
|
@@ -931,11 +906,11 @@ class OrderPlaced extends EventBase {
|
|
|
931
906
|
static readonly eventName = 'OrderPlaced';
|
|
932
907
|
|
|
933
908
|
constructor(
|
|
934
|
-
orderId: string,
|
|
909
|
+
readonly orderId: string,
|
|
935
910
|
readonly customer: string,
|
|
936
911
|
metadata?: EventMetadata,
|
|
937
912
|
) {
|
|
938
|
-
super(
|
|
913
|
+
super(metadata);
|
|
939
914
|
}
|
|
940
915
|
}
|
|
941
916
|
|
|
@@ -955,7 +930,7 @@ const order = Order.place(
|
|
|
955
930
|
const [placed] = order.pullDomainEvents(); // OrderPlaced[]
|
|
956
931
|
order.pullDomainEvents(); // []
|
|
957
932
|
JSON.stringify(placed);
|
|
958
|
-
// {"eventName":"OrderPlaced","
|
|
933
|
+
// {"eventName":"OrderPlaced","eventId":"…","occurredOn":"2026-10-17T10:00:00.000Z","orderId":"9b2f6c1e-…","customer":"Ana"}
|
|
959
934
|
```
|
|
960
935
|
|
|
961
936
|
### Primitive types
|
|
@@ -1017,7 +992,7 @@ interface AggregateRemover<Aggregate extends AggregateRoot<any, any>> {
|
|
|
1017
992
|
```
|
|
1018
993
|
|
|
1019
994
|
`remove` receives the loaded aggregate, not its id: removing is a change like any other, so the
|
|
1020
|
-
repository removes it only where the stored version still equals
|
|
995
|
+
repository removes it only where the stored version still equals the one it loaded, and throws
|
|
1021
996
|
`ConcurrencyException` when someone changed or removed it after it was loaded [IDDD ch. 10]. A
|
|
1022
997
|
use case loads it with `findById`, decides, and removes it.
|
|
1023
998
|
|
|
@@ -1049,17 +1024,19 @@ interface OrderSummaries {
|
|
|
1049
1024
|
### Optimistic concurrency
|
|
1050
1025
|
|
|
1051
1026
|
Two users load the same order, both change it and both save: without a check, the second save
|
|
1052
|
-
silently erases the first.
|
|
1027
|
+
silently erases the first. The repository prevents it with a version it keeps itself: the
|
|
1028
|
+
aggregate knows nothing about it. It remembers the version of each aggregate object it loaded or
|
|
1029
|
+
saved in a `WeakMap<Order, number>` (an object it never saw is new, version `0`), and every
|
|
1053
1030
|
repository follows the same contract:
|
|
1054
1031
|
|
|
1055
|
-
| Moment | The repository
|
|
1056
|
-
| ---------------------------- |
|
|
1057
|
-
| Load | Rebuilds with `Order.fromPrimitives(row)
|
|
1058
|
-
| Save a new aggregate |
|
|
1059
|
-
| Save an existing aggregate | Writes only where the stored version still equals
|
|
1060
|
-
| Nothing written (stale data) | Throws `ConcurrencyException('Order', id,
|
|
1061
|
-
| Written |
|
|
1062
|
-
| Remove | Deletes only where the stored version still equals
|
|
1032
|
+
| Moment | The repository |
|
|
1033
|
+
| ---------------------------- | ------------------------------------------------------------------------------------------- |
|
|
1034
|
+
| Load | Rebuilds with `Order.fromPrimitives(row)` and remembers `row.version` for that object |
|
|
1035
|
+
| Save a new aggregate | Version `0`: inserts with version `1`; if the id already exists, it is a conflict |
|
|
1036
|
+
| Save an existing aggregate | Writes only where the stored version still equals the remembered one, and stores one more |
|
|
1037
|
+
| Nothing written (stale data) | Throws `ConcurrencyException('Order', id, version)` (HTTP 409) with the version it expected |
|
|
1038
|
+
| Written | Remembers the new version, so the same object can be changed and saved again |
|
|
1039
|
+
| Remove | Deletes only where the stored version still equals the remembered one; otherwise a conflict |
|
|
1063
1040
|
|
|
1064
1041
|
The version lives next to the primitives (a `version` column or field), never inside them. After a
|
|
1065
1042
|
`ConcurrencyException`, either tell the user that someone else changed it, or run the command
|
|
@@ -1070,8 +1047,11 @@ case. A use case run by a listener does not retry by itself: it lets the conflic
|
|
|
1070
1047
|
channel's redelivery tries again:
|
|
1071
1048
|
|
|
1072
1049
|
```ts
|
|
1073
|
-
import {
|
|
1074
|
-
|
|
1050
|
+
import {
|
|
1051
|
+
AggregateNotFoundException,
|
|
1052
|
+
ConcurrencyException,
|
|
1053
|
+
UseCase,
|
|
1054
|
+
} from '@archi-code/ddd/application';
|
|
1075
1055
|
|
|
1076
1056
|
interface AddOrderLineInput {
|
|
1077
1057
|
orderId: string;
|
|
@@ -1087,7 +1067,7 @@ interface AddOrderLineCommand {
|
|
|
1087
1067
|
|
|
1088
1068
|
class AddOrderLine extends UseCase<AddOrderLineInput, AddOrderLineCommand, void> {
|
|
1089
1069
|
// constructor (orders, events, clock) as in "Persistence belongs to the repository";
|
|
1090
|
-
// toCommand builds orderId, sku and quantity with
|
|
1070
|
+
// toCommand builds orderId, sku and quantity with ValueObjectBuilder.build;
|
|
1091
1071
|
// Order, OrderLine, Sku and Quantity as in "Persisting aggregates"
|
|
1092
1072
|
|
|
1093
1073
|
protected async handle(command: AddOrderLineCommand): Promise<void> {
|
|
@@ -1115,9 +1095,12 @@ changed) does not retry: the `ConcurrencyException` reaches the entry point (HTT
|
|
|
1115
1095
|
|
|
1116
1096
|
```ts
|
|
1117
1097
|
import type { AggregateFinder, AggregateSaver } from '@archi-code/ddd/domain';
|
|
1118
|
-
import { ConcurrencyException
|
|
1098
|
+
import { ConcurrencyException } from '@archi-code/ddd/application';
|
|
1099
|
+
import { InternalErrorException } from '@archi-code/ddd/infrastructure';
|
|
1119
1100
|
|
|
1120
1101
|
class MongoOrderRepository implements AggregateFinder<Order>, AggregateSaver<Order> {
|
|
1102
|
+
private readonly versions = new WeakMap<Order, number>();
|
|
1103
|
+
|
|
1121
1104
|
constructor(private readonly collection: Collection<OrderDocument>) {}
|
|
1122
1105
|
|
|
1123
1106
|
async findById(id: OrderId): Promise<Order | null> {
|
|
@@ -1126,20 +1109,22 @@ class MongoOrderRepository implements AggregateFinder<Order>, AggregateSaver<Ord
|
|
|
1126
1109
|
return null;
|
|
1127
1110
|
}
|
|
1128
1111
|
const { _id, version, ...primitives } = document;
|
|
1129
|
-
|
|
1112
|
+
const order = Order.fromPrimitives(primitives);
|
|
1113
|
+
this.versions.set(order, version);
|
|
1114
|
+
return order;
|
|
1130
1115
|
}
|
|
1131
1116
|
|
|
1132
1117
|
async save(order: Order): Promise<void> {
|
|
1133
|
-
const
|
|
1118
|
+
const version = this.versions.get(order) ?? 0;
|
|
1119
|
+
const data = { ...order.toPrimitives(), version: version + 1 };
|
|
1134
1120
|
const written =
|
|
1135
|
-
|
|
1121
|
+
version === 0
|
|
1136
1122
|
? await this.insert(data)
|
|
1137
|
-
: (await this.collection.replaceOne({ _id: data.id, version
|
|
1138
|
-
.matchedCount === 1;
|
|
1123
|
+
: (await this.collection.replaceOne({ _id: data.id, version }, data)).matchedCount === 1;
|
|
1139
1124
|
if (!written) {
|
|
1140
|
-
throw new ConcurrencyException('Order', order.id.value,
|
|
1125
|
+
throw new ConcurrencyException('Order', order.id.value, version);
|
|
1141
1126
|
}
|
|
1142
|
-
|
|
1127
|
+
this.versions.set(order, version + 1);
|
|
1143
1128
|
}
|
|
1144
1129
|
|
|
1145
1130
|
private async insert(data: OrderDocument): Promise<boolean> {
|
|
@@ -1169,7 +1154,7 @@ the one of [Persisting aggregates](#persisting-aggregates)):
|
|
|
1169
1154
|
```ts
|
|
1170
1155
|
import { describe, expect, it } from 'vitest';
|
|
1171
1156
|
import type { AggregateFinder, AggregateSaver } from '@archi-code/ddd/domain';
|
|
1172
|
-
import { ConcurrencyException } from '@archi-code/ddd/
|
|
1157
|
+
import { ConcurrencyException } from '@archi-code/ddd/application';
|
|
1173
1158
|
import { FixedClock } from '@archi-code/ddd/infrastructure';
|
|
1174
1159
|
|
|
1175
1160
|
const clock = new FixedClock('2026-10-17T10:00:00Z');
|
|
@@ -1191,7 +1176,7 @@ export function orderRepositoryContract(
|
|
|
1191
1176
|
await orders.save(order);
|
|
1192
1177
|
order.ship(clock);
|
|
1193
1178
|
await orders.save(order);
|
|
1194
|
-
expect((await orders.findById(order.id))?.
|
|
1179
|
+
expect((await orders.findById(order.id))?.toPrimitives()).toStrictEqual(order.toPrimitives());
|
|
1195
1180
|
});
|
|
1196
1181
|
|
|
1197
1182
|
it('refuses a stale save', async () => {
|
|
@@ -1243,7 +1228,7 @@ A use case has two steps, and `execute(input)` (which always returns a promise)
|
|
|
1243
1228
|
order:
|
|
1244
1229
|
|
|
1245
1230
|
1. `protected toCommand(input: Input): Command` builds the value objects the action needs with
|
|
1246
|
-
`
|
|
1231
|
+
`ValueObjectBuilder.build({ orderId: () => new OrderId(input.orderId), email: () => new Email(input.email) })`
|
|
1247
1232
|
(one value object alone can be built with `new`). If any field is invalid it throws one
|
|
1248
1233
|
`ValidationException` listing every field, and `handle` never runs.
|
|
1249
1234
|
2. `protected handle(command: Command)` (sync or async) coordinates the action with value objects
|
|
@@ -1256,8 +1241,8 @@ Errors of both steps are treated the same way:
|
|
|
1256
1241
|
- Exceptions of the package (`DomainException`, `ValidationException`, `ApplicationException`,
|
|
1257
1242
|
`AggregateNotFoundException`…) pass through as they are, so the caller can map them.
|
|
1258
1243
|
- Anything else (a driver error, a `TypeError`, a thrown string) is wrapped in
|
|
1259
|
-
`
|
|
1260
|
-
`Failed,
|
|
1244
|
+
`UnexpectedErrorException` with the original as `cause` and the use case name in the message:
|
|
1245
|
+
`Failed, unexpected error: PlaceOrder: connection reset`.
|
|
1261
1246
|
|
|
1262
1247
|
`UseCase` does nothing else: loading, checking, saving and publishing are written in `handle`,
|
|
1263
1248
|
so every step of the use case is visible in one place:
|
|
@@ -1293,6 +1278,67 @@ nothing and events are never sent twice. A command with several values is a smal
|
|
|
1293
1278
|
value objects (`interface PlaceOrderCommand { orderId: OrderId; customer: CustomerName }`); a
|
|
1294
1279
|
use case never imports another use case.
|
|
1295
1280
|
|
|
1281
|
+
### Building every value object of an input at once: `ValueObjectBuilder.build`
|
|
1282
|
+
|
|
1283
|
+
`ValueObjectBuilder` lives in `@archi-code/ddd/application`: it is used in `toCommand`, never in
|
|
1284
|
+
the domain. The constructor of each value object stops at its own errors. To report **every field** of an
|
|
1285
|
+
input (a form, a JSON body, a message) at once, build the value objects with
|
|
1286
|
+
`ValueObjectBuilder.build`: it runs every factory, even after one fails, and throws one
|
|
1287
|
+
`ValidationException` whose `errors` name the field of each problem. It collects the three ways a
|
|
1288
|
+
value can be wrong: empty in a required type (`RequiredValueException`), not convertible
|
|
1289
|
+
(`TypePrimitiveException`) and broken rules (`ValidationException`), plus any other
|
|
1290
|
+
`DomainException` a factory throws. Any other error is a bug and goes through untouched, and it
|
|
1291
|
+
wins: the validation errors already collected are dropped, so a bug is never hidden behind a 422.
|
|
1292
|
+
`ValueObjectBuilder.buildList` does the same for a list and names each item by its position, so nested
|
|
1293
|
+
inputs get paths such as `lines.1.quantity`. Factories are synchronous: a factory that returns a
|
|
1294
|
+
Promise does not compile in `build`, and both methods refuse it at run time with
|
|
1295
|
+
`UnexpectedErrorException` (`Failed, unexpected error: ValueObjectBuilder: the factory of "email"
|
|
1296
|
+
returned a Promise; build value objects synchronously`), so an asynchronous check never skips the validation. Every field name is
|
|
1297
|
+
kept as written, `__proto__` included.
|
|
1298
|
+
|
|
1299
|
+
```ts
|
|
1300
|
+
import { IdType, NumberType, Required, StringType } from '@archi-code/ddd/domain';
|
|
1301
|
+
import { ValueObjectBuilder } from '@archi-code/ddd/application';
|
|
1302
|
+
import { Validations } from '@archi-code/validation';
|
|
1303
|
+
|
|
1304
|
+
// Email as in "Validations: always valid from the constructor"; input is the raw request body
|
|
1305
|
+
|
|
1306
|
+
class OrderId extends IdType<'OrderId'> {}
|
|
1307
|
+
class Sku extends Required(StringType) {}
|
|
1308
|
+
|
|
1309
|
+
@Validations([
|
|
1310
|
+
{ validator: 'isInt', properties: { min: 1, max: 20 }, message: 'From 1 to 20 units' },
|
|
1311
|
+
])
|
|
1312
|
+
class Quantity extends Required(NumberType) {}
|
|
1313
|
+
|
|
1314
|
+
const command = ValueObjectBuilder.build({
|
|
1315
|
+
orderId: () => new OrderId(input.orderId),
|
|
1316
|
+
email: () => new Email(input.email),
|
|
1317
|
+
lines: () =>
|
|
1318
|
+
ValueObjectBuilder.buildList(input.lines, (line) =>
|
|
1319
|
+
ValueObjectBuilder.build({
|
|
1320
|
+
sku: () => new Sku(line.sku),
|
|
1321
|
+
quantity: () => new Quantity(line.quantity),
|
|
1322
|
+
}),
|
|
1323
|
+
),
|
|
1324
|
+
});
|
|
1325
|
+
// command: { orderId: OrderId; email: Email; lines: { sku: Sku; quantity: Quantity }[] }
|
|
1326
|
+
|
|
1327
|
+
// with input = { orderId: 'x', email: 'ana', lines: [{ sku: 'TORCH-01', quantity: 2 }, { sku: null, quantity: 0 }] }:
|
|
1328
|
+
// ValidationException, error.errors:
|
|
1329
|
+
// [
|
|
1330
|
+
// { field: 'orderId', code: 'DOM003', message: 'Validation Error: Expected a valid UUID, but received "x".' },
|
|
1331
|
+
// { field: 'email', code: 'isEmail', message: 'Email: Value does not satisfy isEmail' },
|
|
1332
|
+
// { field: 'lines.1.sku', code: 'DOM004', message: 'Validation Error: Sku is required, but received null.' },
|
|
1333
|
+
// { field: 'lines.1.quantity', code: 'isInt', message: 'Quantity: From 1 to 20 units' },
|
|
1334
|
+
// ]
|
|
1335
|
+
```
|
|
1336
|
+
|
|
1337
|
+
`ExceptionClassifier` classifies it as `ExceptionKind.Invalid` (a 422 in HTTP) and the entry point
|
|
1338
|
+
returns the same `errors`, so a client can mark each input. To report a problem of your own in the same shape, throw
|
|
1339
|
+
`new ValidationException([{ field: 'deliveryDate', code: 'ORD003', message: '…' }])`; `code` is
|
|
1340
|
+
optional and defaults to `'DOM001'`.
|
|
1341
|
+
|
|
1296
1342
|
### Use cases run by a listener
|
|
1297
1343
|
|
|
1298
1344
|
A use case that a listener runs registers a fact that already happened elsewhere, so it does not
|
|
@@ -1342,7 +1388,7 @@ wiring stay as they are.
|
|
|
1342
1388
|
```
|
|
1343
1389
|
use case ──► EventBus (port, @archi-code/ddd/application)
|
|
1344
1390
|
▲ implements
|
|
1345
|
-
wiring ──► DomainEventBus (
|
|
1391
|
+
wiring ──► DomainEventBus ({ ...event }, @ListensTo, rebuild, listener.handle) (@archi-code/ddd/infrastructure)
|
|
1346
1392
|
└──► EventChannel (port) ◄── InMemoryEventChannel (@archi-code/ddd/infrastructure)
|
|
1347
1393
|
send / receive ◄── your RabbitMQ, database or Kafka channel
|
|
1348
1394
|
```
|
|
@@ -1384,7 +1430,7 @@ class ReserveStockOnOrderPlaced extends EventListener<OrderPlaced> {
|
|
|
1384
1430
|
}
|
|
1385
1431
|
|
|
1386
1432
|
async handle(event: OrderPlaced): Promise<void> {
|
|
1387
|
-
await this.reserveStock.execute({ orderId: event.
|
|
1433
|
+
await this.reserveStock.execute({ orderId: event.orderId });
|
|
1388
1434
|
}
|
|
1389
1435
|
}
|
|
1390
1436
|
```
|
|
@@ -1426,7 +1472,7 @@ interface EventChannel {
|
|
|
1426
1472
|
}
|
|
1427
1473
|
```
|
|
1428
1474
|
|
|
1429
|
-
A channel only moves serialized events (`
|
|
1475
|
+
A channel only moves serialized events (`{ ...event }`), so the same contract fits a process, a broker
|
|
1430
1476
|
and a table. The rules every channel follows:
|
|
1431
1477
|
|
|
1432
1478
|
| Concern | Rule |
|
|
@@ -1452,8 +1498,11 @@ import {
|
|
|
1452
1498
|
} from '@archi-code/ddd/infrastructure';
|
|
1453
1499
|
|
|
1454
1500
|
const events = new EventRegistry<OrderEvent>()
|
|
1455
|
-
.register(
|
|
1456
|
-
|
|
1501
|
+
.register(
|
|
1502
|
+
OrderPlaced,
|
|
1503
|
+
(json) => new OrderPlaced(json.orderId as string, json.customer as string, json),
|
|
1504
|
+
)
|
|
1505
|
+
.register(OrderShipped, (json) => new OrderShipped(json.orderId as string, json));
|
|
1457
1506
|
|
|
1458
1507
|
const bus = new DomainEventBus<OrderEvent>(new InMemoryEventChannel(), events);
|
|
1459
1508
|
const reserving = await bus.subscribe(new ReserveStockOnOrderPlaced(reserveStock));
|
|
@@ -1466,14 +1515,13 @@ await reserving.unsubscribe();
|
|
|
1466
1515
|
|
|
1467
1516
|
- `DomainEventBus` implements `EventBus`; `subscribe(listener)` is its own method, called only by
|
|
1468
1517
|
the wiring.
|
|
1469
|
-
- `publish(events)` sends
|
|
1470
|
-
recorded by an aggregate are already frozen by `record()`).
|
|
1518
|
+
- `publish(events)` sends a plain copy of each event (`{ ...event }`) to the channel in one `send`.
|
|
1471
1519
|
- `subscribe(listener)` reads the event and the consumer with `ListensTo.subscriptionOf` (`ApplicationException` without
|
|
1472
1520
|
`@ListensTo`), refuses an event the registry cannot rebuild (`UnknownEventException`, at wiring
|
|
1473
1521
|
time rather than at the first delivery) and calls
|
|
1474
1522
|
`channel.receive(eventName, consumer, handler)`; the handler rebuilds the event
|
|
1475
1523
|
with the registry and calls `listener.handle`. `unsubscribe()` closes the channel subscription.
|
|
1476
|
-
- Listeners always receive an event rebuilt from its JSON (
|
|
1524
|
+
- Listeners always receive an event rebuilt from its JSON (equal field by field), also in
|
|
1477
1525
|
memory, so what works with the in-memory channel works the same with a broker.
|
|
1478
1526
|
- An optional third argument, a `Logger`, logs what was published, the subscriptions and every
|
|
1479
1527
|
listener failure with its event and consumer (see [Logging](#logging)); without it, nothing is
|
|
@@ -1489,9 +1537,8 @@ and throws `InternalErrorException`
|
|
|
1489
1537
|
(`the factory of "OrderPlaced" must pass the serialized event as metadata, so it keeps its eventId and occurredOn`)
|
|
1490
1538
|
when the rebuilt `eventId` (compared without case) or the `occurredOn` instant differs from the
|
|
1491
1539
|
JSON. It also throws `UnknownEventException` for a name nobody registered and
|
|
1492
|
-
`InternalErrorException` for text that is not a JSON object or has no `eventName`. The
|
|
1493
|
-
|
|
1494
|
-
`eventVersion`, so it can read older shapes (`json.eventVersion === 1 ? … : …`).
|
|
1540
|
+
`InternalErrorException` for text that is not a JSON object or has no `eventName`. The factory receives the JSON as it was
|
|
1541
|
+
stored, so it can also read an older shape of the same event.
|
|
1495
1542
|
|
|
1496
1543
|
### Example: from the use case to the listener, in memory
|
|
1497
1544
|
|
|
@@ -1519,11 +1566,11 @@ class OrderPlaced extends EventBase {
|
|
|
1519
1566
|
static readonly eventName = 'OrderPlaced';
|
|
1520
1567
|
|
|
1521
1568
|
constructor(
|
|
1522
|
-
orderId: string,
|
|
1569
|
+
readonly orderId: string,
|
|
1523
1570
|
readonly customer: string,
|
|
1524
1571
|
metadata?: EventMetadata,
|
|
1525
1572
|
) {
|
|
1526
|
-
super(
|
|
1573
|
+
super(metadata);
|
|
1527
1574
|
}
|
|
1528
1575
|
}
|
|
1529
1576
|
|
|
@@ -1596,7 +1643,7 @@ class SendConfirmationOnOrderPlaced extends EventListener<OrderPlaced> {
|
|
|
1596
1643
|
}
|
|
1597
1644
|
|
|
1598
1645
|
async handle(event: OrderPlaced): Promise<void> {
|
|
1599
|
-
await this.sendConfirmation.execute({ orderId: event.
|
|
1646
|
+
await this.sendConfirmation.execute({ orderId: event.orderId, customer: event.customer });
|
|
1600
1647
|
}
|
|
1601
1648
|
}
|
|
1602
1649
|
|
|
@@ -1604,7 +1651,7 @@ class SendConfirmationOnOrderPlaced extends EventListener<OrderPlaced> {
|
|
|
1604
1651
|
const mails: string[] = [];
|
|
1605
1652
|
const events = new EventRegistry<OrderPlaced>().register(
|
|
1606
1653
|
OrderPlaced,
|
|
1607
|
-
(json) => new OrderPlaced(json.
|
|
1654
|
+
(json) => new OrderPlaced(json.orderId as string, json.customer as string, json),
|
|
1608
1655
|
);
|
|
1609
1656
|
const channel = new InMemoryEventChannel();
|
|
1610
1657
|
const bus = new DomainEventBus<OrderPlaced>(channel, events);
|
|
@@ -1618,8 +1665,8 @@ const orderId = await placeOrder.execute({ customer: 'Ana' });
|
|
|
1618
1665
|
|
|
1619
1666
|
mails; // ['Order <orderId> confirmed to Ana']: the listener ran before publish resolved
|
|
1620
1667
|
channel.sent;
|
|
1621
|
-
// [{ eventName: 'OrderPlaced',
|
|
1622
|
-
//
|
|
1668
|
+
// [{ eventName: 'OrderPlaced', eventId: '…', occurredOn: '2026-10-19T09:30:00.000Z',
|
|
1669
|
+
// orderId, customer: 'Ana' }]
|
|
1623
1670
|
|
|
1624
1671
|
await bus.subscribe(new SendConfirmationOnOrderPlaced(new SendConfirmation(mails)));
|
|
1625
1672
|
// InfrastructureException: SendConfirmationOnOrderPlaced already receives "OrderPlaced"
|
|
@@ -1811,8 +1858,8 @@ The repository is responsible for the transaction and for everything else about
|
|
|
1811
1858
|
database transaction that the repository opens and commits (or rolls back) within `save` itself,
|
|
1812
1859
|
with its own connection.
|
|
1813
1860
|
- **Concurrency**: it checks the stored version and throws `ConcurrencyException` (see
|
|
1814
|
-
[Optimistic concurrency](#optimistic-concurrency))
|
|
1815
|
-
|
|
1861
|
+
[Optimistic concurrency](#optimistic-concurrency)) and remembers the version of every aggregate
|
|
1862
|
+
it loaded or saved, outside the aggregate.
|
|
1816
1863
|
- **Connections and errors**: pools, sessions and driver retries stay inside the adapter, and driver
|
|
1817
1864
|
errors are wrapped in `InternalErrorException` with `{ cause }`.
|
|
1818
1865
|
- **Events that must not be lost**: `publish` runs after `save`, so a failure in between loses the
|
|
@@ -1827,7 +1874,7 @@ follows the same contract: every `save` and `remove` is applied whole or not at
|
|
|
1827
1874
|
## Event Sourcing and CQRS: only for a concrete risk
|
|
1828
1875
|
|
|
1829
1876
|
Neither is required, and both add complexity [IDDD ch. 4]. This package persists the state of
|
|
1830
|
-
aggregates; it does not ship an event store nor Event Sourcing support:
|
|
1877
|
+
aggregates; it does not ship an event store nor Event Sourcing support: the stored version grows by 1 per
|
|
1831
1878
|
save, not per event, and an event-sourced aggregate manages the version of its stream itself.
|
|
1832
1879
|
|
|
1833
1880
|
| Situation | Recommendation |
|
|
@@ -1847,35 +1894,16 @@ just sent, show how old the data is, or say that the request is being processed.
|
|
|
1847
1894
|
|
|
1848
1895
|
Ready adapters for tests, prototypes and the edges of an API:
|
|
1849
1896
|
|
|
1850
|
-
| Export
|
|
1851
|
-
|
|
|
1852
|
-
| `InMemoryEventChannel({ delivery }?)`
|
|
1853
|
-
| `
|
|
1854
|
-
| `
|
|
1855
|
-
| `
|
|
1856
|
-
| `SystemClock`, `FixedClock(at)` | `Clock` with the real time, and a test clock with `set(at)` and `advance(ms)` |
|
|
1897
|
+
| Export | What it is |
|
|
1898
|
+
| ------------------------------------- | ----------------------------------------------------------------------------------------------------------- |
|
|
1899
|
+
| `InMemoryEventChannel({ delivery }?)` | `EventChannel` inside the process, built on emittery's techniques; `sent` lists what was sent |
|
|
1900
|
+
| `ConsoleLogger({ level?, clock? }?)` | `Logger` that writes one JSON line per entry from `level` up (`info` by default) |
|
|
1901
|
+
| `InMemoryLogger`, `SilentLogger` | `Logger` that keeps `entries` for tests, and one that writes nothing (the default of every piece that logs) |
|
|
1902
|
+
| `SystemClock`, `FixedClock(at)` | `Clock` with the real time, and a test clock with `set(at)` and `advance(ms)` |
|
|
1857
1903
|
|
|
1858
1904
|
The package ships no repository: each project writes one per aggregate and technology (see
|
|
1859
1905
|
[Writing a repository](#writing-a-repository)), including the in-memory one its tests use.
|
|
1860
1906
|
|
|
1861
|
-
`HttpErrorMapper` statuses, overridable by code (`new HttpErrorMapper({ statusByCode: { ORD001: 403 } })`):
|
|
1862
|
-
|
|
1863
|
-
| Error | Status |
|
|
1864
|
-
| ---------------------------------------------------- | ------ |
|
|
1865
|
-
| `TypePrimitiveException`, `RequiredValueException` | 400 |
|
|
1866
|
-
| `NotAllowedException` | 403 |
|
|
1867
|
-
| `AggregateNotFoundException` | 404 |
|
|
1868
|
-
| `ConcurrencyException` | 409 |
|
|
1869
|
-
| `ValidationException` (body includes `errors`) | 422 |
|
|
1870
|
-
| any other `DomainException` (a broken business rule) | 422 |
|
|
1871
|
-
| any other `ApplicationException` | 400 |
|
|
1872
|
-
| `InfrastructureException` and anything unknown | 500 |
|
|
1873
|
-
|
|
1874
|
-
Every 5xx answer has the message `Internal server error`: messages of infrastructure errors (SQL,
|
|
1875
|
-
hosts, secrets…) never reach the client. Unknown errors get the code `E000`. The overrides are
|
|
1876
|
-
read only from the object you pass (a code such as `constructor` or `toString` is not taken from
|
|
1877
|
-
`Object.prototype`).
|
|
1878
|
-
|
|
1879
1907
|
`InMemoryEventChannel` is the in-process `EventChannel` behind `DomainEventBus` (see
|
|
1880
1908
|
[Events](#events-the-contract-in-applicationevent)). It reimplements the techniques of
|
|
1881
1909
|
[emittery](https://github.com/sindresorhus/emittery) (MIT) that a channel needs: a `Map` from event
|
|
@@ -1889,15 +1917,11 @@ delivery is not called for it; one closed before its turn is skipped), and two d
|
|
|
1889
1917
|
| `'parallel'` | `emit` | Consumers start together; all of them run, and the failures reject `send` with one `EventDeliveryException` (`errors` holds each) |
|
|
1890
1918
|
|
|
1891
1919
|
Events go one after another in the order of the `send`; a failure stops the events after it. Every
|
|
1892
|
-
consumer gets
|
|
1920
|
+
consumer gets its own JSON copy, `send` resolves once the handlers finished, and `sent`
|
|
1893
1921
|
returns a copy of what was sent, in order (for tests).
|
|
1894
1922
|
|
|
1895
1923
|
```ts
|
|
1896
|
-
import {
|
|
1897
|
-
DomainEventBus,
|
|
1898
|
-
HttpErrorMapper,
|
|
1899
|
-
InMemoryEventChannel,
|
|
1900
|
-
} from '@archi-code/ddd/infrastructure';
|
|
1924
|
+
import { DomainEventBus, InMemoryEventChannel } from '@archi-code/ddd/infrastructure';
|
|
1901
1925
|
|
|
1902
1926
|
const channel = new InMemoryEventChannel({ delivery: 'parallel' });
|
|
1903
1927
|
const bus = new DomainEventBus<OrderEvent>(channel, events); // events: the EventRegistry of "Events"
|
|
@@ -1905,12 +1929,6 @@ await bus.subscribe(new ReserveStockOnOrderPlaced(reserveStock));
|
|
|
1905
1929
|
await bus.subscribe(new NotifyCustomerOnOrderPlaced(notifyCustomer));
|
|
1906
1930
|
await bus.publish(order.pullDomainEvents());
|
|
1907
1931
|
channel.sent.map((event) => event.eventName); // ['OrderPlaced']
|
|
1908
|
-
|
|
1909
|
-
const errors = new HttpErrorMapper({ statusByCode: { ORD001: 403 } });
|
|
1910
|
-
app.use((error, req, res, next) => {
|
|
1911
|
-
const { status, body } = errors.map(error);
|
|
1912
|
-
res.status(status).json(body);
|
|
1913
|
-
});
|
|
1914
1932
|
```
|
|
1915
1933
|
|
|
1916
1934
|
## Logging
|
|
@@ -1926,37 +1944,29 @@ logs on its own only where nobody else sees the error:
|
|
|
1926
1944
|
|
|
1927
1945
|
```ts
|
|
1928
1946
|
import type { Logger } from '@archi-code/ddd/application';
|
|
1929
|
-
import {
|
|
1930
|
-
ConsoleLogger,
|
|
1931
|
-
DomainEventBus,
|
|
1932
|
-
HttpErrorMapper,
|
|
1933
|
-
SystemClock,
|
|
1934
|
-
} from '@archi-code/ddd/infrastructure';
|
|
1947
|
+
import { ConsoleLogger, DomainEventBus, SystemClock } from '@archi-code/ddd/infrastructure';
|
|
1935
1948
|
|
|
1936
1949
|
const logger: Logger = new ConsoleLogger({ level: 'info', clock: new SystemClock() });
|
|
1937
1950
|
logger.warn('stock_low', { sku: 'TORCH-01', units: 2 });
|
|
1938
1951
|
// console.warn: {"time":"2026-10-19T09:30:00.000Z","level":"warn","message":"stock_low","context":{"sku":"TORCH-01","units":2}}
|
|
1939
1952
|
|
|
1940
1953
|
const bus = new DomainEventBus<OrderEvent>(channel, registry, logger);
|
|
1941
|
-
const errors = new HttpErrorMapper({ statusByCode: { ORD001: 409 }, logger });
|
|
1942
1954
|
```
|
|
1943
1955
|
|
|
1944
1956
|
`ConsoleLogger` drops entries below `level` (`debug` < `info` < `warn` < `error`; an unknown level
|
|
1945
1957
|
throws `InternalErrorException`), writes an `Error` in the context with its `name`, `code`,
|
|
1946
|
-
`message`, `errors`, `stack` and `cause` (which `JSON.stringify` would turn into `{}`), dates
|
|
1947
|
-
|
|
1958
|
+
`message`, `errors`, `stack` and `cause` (which `JSON.stringify` would turn into `{}`), dates
|
|
1959
|
+
through their `toJSON`, a `bigint` as text and a circular reference as
|
|
1948
1960
|
`'[Circular]'`; a context it cannot read becomes `'[Unserializable]'`, so logging never throws.
|
|
1949
1961
|
|
|
1950
1962
|
What the package logs when it receives a logger:
|
|
1951
1963
|
|
|
1952
|
-
| Where
|
|
1953
|
-
|
|
|
1954
|
-
| `DomainEventBus`
|
|
1955
|
-
| `DomainEventBus`
|
|
1956
|
-
| `DomainEventBus`
|
|
1957
|
-
| `DomainEventBus`
|
|
1958
|
-
| `HttpErrorMapper` | `error` | `http_request_failed`: every 5xx | `status`, `code`, `error` (with its cause) |
|
|
1959
|
-
| `HttpErrorMapper` | `debug` | `http_request_rejected`: every 4xx | `status`, `code` |
|
|
1964
|
+
| Where | Level | Message | Context |
|
|
1965
|
+
| ---------------- | ------- | --------------------------------------------------------------------------------------------------------- | ------------------------------------------------ |
|
|
1966
|
+
| `DomainEventBus` | `debug` | `domain_events_published` (after the channel accepted them) | `events`: `{ eventName, eventId }[]`, no payload |
|
|
1967
|
+
| `DomainEventBus` | `info` | `event_listener_subscribed`, `event_listener_unsubscribed` | `eventName`, `consumer` |
|
|
1968
|
+
| `DomainEventBus` | `warn` | `event_listener_rejected`: a `DomainException` or `ApplicationException` (such as `ConcurrencyException`) | `eventName`, `consumer`, `eventId`, `error` |
|
|
1969
|
+
| `DomainEventBus` | `error` | `event_listener_failed`: any other error, also an event the registry cannot rebuild | `eventName`, `consumer`, `eventId`, `error` |
|
|
1960
1970
|
|
|
1961
1971
|
The bus still rethrows a listener's error after logging it, so the channel keeps its delivery
|
|
1962
1972
|
semantics (the in-memory channel rejects `send`, a broker retries). The rule for your own code is
|
|
@@ -2027,6 +2037,7 @@ import {
|
|
|
2027
2037
|
BooleanType,
|
|
2028
2038
|
Composite,
|
|
2029
2039
|
CreatedAt,
|
|
2040
|
+
DomainException,
|
|
2030
2041
|
EventBase,
|
|
2031
2042
|
IdType,
|
|
2032
2043
|
NumberType,
|
|
@@ -2034,7 +2045,6 @@ import {
|
|
|
2034
2045
|
StringType,
|
|
2035
2046
|
} from '@archi-code/ddd/domain';
|
|
2036
2047
|
import type { EventMetadata, PrimitiveTypes } from '@archi-code/ddd/domain';
|
|
2037
|
-
import { DomainException } from '@archi-code/ddd/exceptions';
|
|
2038
2048
|
|
|
2039
2049
|
class OrderId extends IdType<'OrderId'> {}
|
|
2040
2050
|
class CustomerName extends Required(StringType) {}
|
|
@@ -2048,16 +2058,23 @@ class OrderPlaced extends EventBase {
|
|
|
2048
2058
|
static readonly eventName = 'OrderPlaced';
|
|
2049
2059
|
|
|
2050
2060
|
constructor(
|
|
2051
|
-
orderId: string,
|
|
2061
|
+
readonly orderId: string,
|
|
2052
2062
|
readonly customer: string,
|
|
2053
2063
|
metadata?: EventMetadata,
|
|
2054
2064
|
) {
|
|
2055
|
-
super(
|
|
2065
|
+
super(metadata);
|
|
2056
2066
|
}
|
|
2057
2067
|
}
|
|
2058
2068
|
|
|
2059
2069
|
class OrderShipped extends EventBase {
|
|
2060
2070
|
static readonly eventName = 'OrderShipped';
|
|
2071
|
+
|
|
2072
|
+
constructor(
|
|
2073
|
+
readonly orderId: string,
|
|
2074
|
+
metadata?: EventMetadata,
|
|
2075
|
+
) {
|
|
2076
|
+
super(metadata);
|
|
2077
|
+
}
|
|
2061
2078
|
}
|
|
2062
2079
|
|
|
2063
2080
|
type OrderEvent = OrderPlaced | OrderShipped;
|
|
@@ -2142,8 +2159,8 @@ class Order extends AggregateRoot<OrderId, OrderEvent> {
|
|
|
2142
2159
|
}
|
|
2143
2160
|
|
|
2144
2161
|
const order = Order.place(OrderId.generate(), new CustomerName('Ana'), { now: () => new Date() });
|
|
2145
|
-
const stored = order.toPrimitives(); // plain data the repository writes
|
|
2146
|
-
Order.fromPrimitives(stored)
|
|
2162
|
+
const stored = order.toPrimitives(); // plain data the repository writes next to its version
|
|
2163
|
+
Order.fromPrimitives(stored); // what the repository returns on load
|
|
2147
2164
|
```
|
|
2148
2165
|
|
|
2149
2166
|
### Writing a repository
|
|
@@ -2158,8 +2175,8 @@ domain it supports (`AggregateFinder`, `AggregateSaver`, `AggregateRemover`):
|
|
|
2158
2175
|
`remove(order)`, `findById(id)` → `Order | null`.
|
|
2159
2176
|
- **The aggregate converts itself**: `order.toPrimitives()` and `Order.fromPrimitives(row)` are
|
|
2160
2177
|
the aggregate's snapshot, never a domain service nor a mapper. The repository calls them
|
|
2161
|
-
directly,
|
|
2162
|
-
|
|
2178
|
+
directly, maps the primitives to its storage format and keeps the version of each aggregate
|
|
2179
|
+
itself.
|
|
2163
2180
|
- **It owns persistence**: atomic writes, the version check, connections and driver errors (see
|
|
2164
2181
|
[Persistence belongs to the repository](#persistence-belongs-to-the-repository)).
|
|
2165
2182
|
|
|
@@ -2172,37 +2189,43 @@ import type {
|
|
|
2172
2189
|
AggregateSaver,
|
|
2173
2190
|
PrimitiveTypes,
|
|
2174
2191
|
} from '@archi-code/ddd/domain';
|
|
2175
|
-
import { ConcurrencyException } from '@archi-code/ddd/
|
|
2192
|
+
import { ConcurrencyException } from '@archi-code/ddd/application';
|
|
2176
2193
|
|
|
2177
2194
|
export class InMemoryOrderRepository
|
|
2178
2195
|
implements AggregateFinder<Order>, AggregateSaver<Order>, AggregateRemover<Order>
|
|
2179
2196
|
{
|
|
2180
2197
|
private readonly rows = new Map<string, { version: number; primitives: PrimitiveTypes<Order> }>();
|
|
2198
|
+
private readonly versions = new WeakMap<Order, number>();
|
|
2181
2199
|
|
|
2182
2200
|
async findById(id: OrderId): Promise<Order | null> {
|
|
2183
2201
|
const row = this.rows.get(id.value);
|
|
2184
|
-
|
|
2185
|
-
|
|
2186
|
-
|
|
2202
|
+
if (row === undefined) {
|
|
2203
|
+
return null;
|
|
2204
|
+
}
|
|
2205
|
+
const order = Order.fromPrimitives(structuredClone(row.primitives));
|
|
2206
|
+
this.versions.set(order, row.version);
|
|
2207
|
+
return order;
|
|
2187
2208
|
}
|
|
2188
2209
|
|
|
2189
2210
|
async save(order: Order): Promise<void> {
|
|
2190
|
-
const
|
|
2211
|
+
const version = this.checkedVersion(order);
|
|
2191
2212
|
const primitives = structuredClone(order.toPrimitives());
|
|
2192
|
-
this.rows.set(
|
|
2193
|
-
|
|
2213
|
+
this.rows.set(order.id.value, { version: version + 1, primitives });
|
|
2214
|
+
this.versions.set(order, version + 1);
|
|
2194
2215
|
}
|
|
2195
2216
|
|
|
2196
2217
|
async remove(order: Order): Promise<void> {
|
|
2197
|
-
this.
|
|
2218
|
+
this.checkedVersion(order);
|
|
2219
|
+
this.rows.delete(order.id.value);
|
|
2198
2220
|
}
|
|
2199
2221
|
|
|
2200
|
-
private
|
|
2222
|
+
private checkedVersion(order: Order): number {
|
|
2201
2223
|
const key = order.id.value;
|
|
2202
|
-
|
|
2203
|
-
|
|
2224
|
+
const version = this.versions.get(order) ?? 0;
|
|
2225
|
+
if ((this.rows.get(key)?.version ?? 0) !== version) {
|
|
2226
|
+
throw new ConcurrencyException('Order', key, version);
|
|
2204
2227
|
}
|
|
2205
|
-
return
|
|
2228
|
+
return version;
|
|
2206
2229
|
}
|
|
2207
2230
|
}
|
|
2208
2231
|
|
|
@@ -2223,23 +2246,33 @@ A Mongo repository has the same shape; only the storage changes:
|
|
|
2223
2246
|
class MongoOrderRepository
|
|
2224
2247
|
implements AggregateFinder<Order>, AggregateSaver<Order>, AggregateRemover<Order>
|
|
2225
2248
|
{
|
|
2249
|
+
private readonly versions = new WeakMap<Order, number>();
|
|
2250
|
+
|
|
2226
2251
|
constructor(private readonly collection: Collection<OrderDocument>) {}
|
|
2227
2252
|
|
|
2228
2253
|
async findById(id: OrderId): Promise<Order | null> {
|
|
2229
2254
|
const document = await this.collection.findOne({ _id: id.value });
|
|
2230
|
-
|
|
2231
|
-
|
|
2232
|
-
|
|
2255
|
+
if (document === null) {
|
|
2256
|
+
return null;
|
|
2257
|
+
}
|
|
2258
|
+
const order = Order.fromPrimitives(document.data);
|
|
2259
|
+
this.versions.set(order, document.version);
|
|
2260
|
+
return order;
|
|
2233
2261
|
}
|
|
2234
2262
|
|
|
2235
|
-
// save: updateOne({ _id, version
|
|
2236
|
-
// or insertOne when
|
|
2263
|
+
// save: version = versions.get(order) ?? 0; updateOne({ _id, version }, { $set: { data, version: version + 1 } }),
|
|
2264
|
+
// or insertOne when version is 0; no match → ConcurrencyException; then versions.set(order, version + 1)
|
|
2237
2265
|
}
|
|
2238
2266
|
```
|
|
2239
2267
|
|
|
2240
|
-
# Exceptions
|
|
2268
|
+
# Exceptions
|
|
2241
2269
|
|
|
2242
|
-
|
|
2270
|
+
Each layer declares and exports its own exceptions: the domain ones come from
|
|
2271
|
+
`@archi-code/ddd/domain`, the application ones from `@archi-code/ddd/application` and the
|
|
2272
|
+
infrastructure ones, with `ExceptionClassifier`, from `@archi-code/ddd/infrastructure`. Every
|
|
2273
|
+
exception extends the base class of its layer (`DomainException`, `ApplicationException` or
|
|
2274
|
+
`InfrastructureException`, which extend `Error` through an internal base class the package does
|
|
2275
|
+
not export) and has:
|
|
2243
2276
|
|
|
2244
2277
|
| Member | Description |
|
|
2245
2278
|
| ------------- | --------------------------------------------------------------------------------------------- |
|
|
@@ -2252,20 +2285,22 @@ All of them extend `AbstractException` (which extends `Error`) and have:
|
|
|
2252
2285
|
| `print()` | `'[<description>]: <message>, <ISO timestamp>'` |
|
|
2253
2286
|
| `toJSON()` | `{ name, message, code, description, timestamp }` (`timestamp` in ISO 8601) |
|
|
2254
2287
|
|
|
2255
|
-
| Class
|
|
2256
|
-
|
|
|
2257
|
-
| `DomainException`
|
|
2258
|
-
| `ValidationException`
|
|
2259
|
-
| `TypePrimitiveException`
|
|
2260
|
-
| `RequiredValueException`
|
|
2261
|
-
| `
|
|
2262
|
-
| `
|
|
2263
|
-
| `
|
|
2264
|
-
| `
|
|
2265
|
-
| `
|
|
2266
|
-
| `
|
|
2267
|
-
| `
|
|
2268
|
-
| `
|
|
2288
|
+
| Class | Subpath | Extends | Constructor | Codes | Message |
|
|
2289
|
+
| --------------------------------- | ----------------- | ------------------------- | -------------------------------------------------------------------- | ------------------ | --------------------------------------------------------------------------------- |
|
|
2290
|
+
| `DomainException` | `/domain` | `Error` | `(message, exceptionCodes = [], options?)` | `DOM000` + yours | `message` |
|
|
2291
|
+
| `ValidationException` | `/domain` | `DomainException` | `(errors: ValidationIssueInput[])`, keeps them as `ValidationIssue`s | `DOM000`, `DOM001` | `Validation failed, <field: message, joined with ", ">` |
|
|
2292
|
+
| `TypePrimitiveException` | `/domain` | `DomainException` | `(expectedType, receivedValue, template?)` | `DOM000`, `DOM003` | `Validation Error: Expected a valid <expectedType>, but received <value>.` |
|
|
2293
|
+
| `RequiredValueException` | `/domain` | `DomainException` | `(typeName, receivedValue)` | `DOM000`, `DOM004` | `Validation Error: <typeName> is required, but received <value>.` |
|
|
2294
|
+
| `InvalidEventDefinitionException` | `/domain` | `DomainException` | `(eventClassName)` | `DOM000`, `DOM005` | `<eventClassName> must declare a static eventName` |
|
|
2295
|
+
| `ApplicationException` | `/application` | `Error` | `(message, exceptionCodes = [], options?)` | `APP000` + yours | `message` |
|
|
2296
|
+
| `ConcurrencyException` | `/application` | `ApplicationException` | `(aggregateName, id, expectedVersion, options?)` | `APP000`, `APP001` | `<aggregateName> with ID <id> was changed by someone else: expected version <n>.` |
|
|
2297
|
+
| `AggregateNotFoundException` | `/application` | `ApplicationException` | `(entity: string, id: string, options?)` | `APP000`, `APP002` | `<entity> with ID <id> not found.` |
|
|
2298
|
+
| `NotAllowedException` | `/application` | `ApplicationException` | `(message, options?)` | `APP000`, `APP003` | `message` |
|
|
2299
|
+
| `UnexpectedErrorException` | `/application` | `ApplicationException` | `(message, options?)` | `APP000`, `APP004` | `Failed, unexpected error: <message>` |
|
|
2300
|
+
| `InfrastructureException` | `/infrastructure` | `Error` | `(message, exceptionCodes = [], options?)` | `INF000` + yours | `message` |
|
|
2301
|
+
| `InternalErrorException` | `/infrastructure` | `InfrastructureException` | `(message, options?)` | `INF000`, `INF001` | `Failed, internal error: <message>` |
|
|
2302
|
+
| `UnknownEventException` | `/infrastructure` | `InfrastructureException` | `(eventName)` | `INF000`, `INF002` | `Unknown event "<eventName>": register it before reading it.` |
|
|
2303
|
+
| `EventDeliveryException` | `/infrastructure` | `InfrastructureException` | `(eventName, errors, consumers)` | `INF000`, `INF003` | `<n> of <consumers> consumers of "<eventName>" failed.` (`errors`: each failure) |
|
|
2269
2304
|
|
|
2270
2305
|
`options` is `{ cause }`: keep the original error when you wrap it. A `ValidationIssueInput` is a
|
|
2271
2306
|
`ValidationIssue` (`{ field: string | null; code: string; message: string }`) whose `code` is
|
|
@@ -2273,8 +2308,15 @@ optional: it defaults to `'DOM001'`. `TypePrimitiveException` and
|
|
|
2273
2308
|
`RequiredValueException` are the ones value objects throw; the `template` of
|
|
2274
2309
|
`TypePrimitiveException` replaces `'Expected a valid '`. Both quote at most 64 characters of the
|
|
2275
2310
|
value they received (`"…"` marks the cut), so a long text pasted into a field, or a whole object,
|
|
2276
|
-
is never copied into logs or
|
|
2277
|
-
`
|
|
2311
|
+
is never copied into logs or answers. The built-in codes are exported by each layer as the
|
|
2312
|
+
`DomainExceptionCode`, `ApplicationExceptionCode` and `InfrastructureExceptionCode` enums
|
|
2313
|
+
(`ApplicationExceptionCode.AggregateNotFound === 'APP002'`).
|
|
2314
|
+
|
|
2315
|
+
`UnexpectedErrorException` is what `UseCase.execute` wraps any error that is not an exception of
|
|
2316
|
+
the package in (a bug or a driver error an adapter did not wrap), and `InvalidEventDefinitionException`
|
|
2317
|
+
is thrown when an event class has no `eventName`: both are mistakes in the code, so
|
|
2318
|
+
`ExceptionClassifier` reports them as `Internal`. `InternalErrorException` is the one adapters wrap
|
|
2319
|
+
their failures in.
|
|
2278
2320
|
|
|
2279
2321
|
`AggregateNotFoundException` and `NotAllowedException` belong to the application: a use case
|
|
2280
2322
|
throws the first when the aggregate it needs is not there (a repository returns `null`) and the
|
|
@@ -2286,16 +2328,16 @@ The built-in classes recognize their instances across the CJS and ESM builds: an
|
|
|
2286
2328
|
`AggregateNotFoundException` (they match by code). Your own subclasses use the normal `instanceof`.
|
|
2287
2329
|
|
|
2288
2330
|
```ts
|
|
2331
|
+
import { DomainException } from '@archi-code/ddd/domain';
|
|
2289
2332
|
import {
|
|
2290
2333
|
AggregateNotFoundException,
|
|
2291
2334
|
ApplicationException,
|
|
2292
|
-
|
|
2293
|
-
|
|
2294
|
-
|
|
2295
|
-
} from '@archi-code/ddd/exceptions';
|
|
2335
|
+
ApplicationExceptionCode,
|
|
2336
|
+
} from '@archi-code/ddd/application';
|
|
2337
|
+
import { InternalErrorException } from '@archi-code/ddd/infrastructure';
|
|
2296
2338
|
|
|
2297
2339
|
const notFound = new AggregateNotFoundException('User', '42');
|
|
2298
|
-
notFound.code ===
|
|
2340
|
+
notFound.code === ApplicationExceptionCode.AggregateNotFound; // true
|
|
2299
2341
|
notFound.message; // 'User with ID 42 not found.'
|
|
2300
2342
|
notFound.code; // 'APP002'
|
|
2301
2343
|
notFound.description; // 'Application Exception (APP000), Aggregate Not Found (APP002)'
|
|
@@ -2332,6 +2374,64 @@ error.code; // 'ORD001'
|
|
|
2332
2374
|
error.description; // 'Domain Exception (DOM000), OrderCancelledException (ORD001)'
|
|
2333
2375
|
```
|
|
2334
2376
|
|
|
2377
|
+
### Translating errors at an entry point
|
|
2378
|
+
|
|
2379
|
+
The package speaks no protocol: HTTP, gRPC, a queue consumer or a CLI each translate errors in
|
|
2380
|
+
their own entry point. `ExceptionClassifier.classify(error)` gives them the one decision every
|
|
2381
|
+
protocol shares, the kind of failure, as an `ExceptionKind`; it takes any thrown value and
|
|
2382
|
+
recognizes the exceptions of the other build (CJS/ESM) too:
|
|
2383
|
+
|
|
2384
|
+
| Error | `ExceptionKind` | Usual HTTP status |
|
|
2385
|
+
| ------------------------------------------------------------- | -------------------------- | ----------------- |
|
|
2386
|
+
| `AggregateNotFoundException` | `NotFound` (`'not_found'`) | 404 |
|
|
2387
|
+
| `NotAllowedException` | `NotAllowed` | 403 |
|
|
2388
|
+
| `ConcurrencyException` | `Conflict` | 409 |
|
|
2389
|
+
| `UnexpectedErrorException`, `InvalidEventDefinitionException` | `Internal` | 500 |
|
|
2390
|
+
| `TypePrimitiveException`, `RequiredValueException` | `Malformed` | 400 |
|
|
2391
|
+
| `ValidationException` and any other `DomainException` | `Invalid` | 422 |
|
|
2392
|
+
| any other `ApplicationException` | `Rejected` | 400 |
|
|
2393
|
+
| `InfrastructureException` and anything unknown | `Internal` | 500 |
|
|
2394
|
+
|
|
2395
|
+
The entry point decides the rest: the status or code of its protocol, the codes of your own
|
|
2396
|
+
exceptions (`error.code`), the body, and logging the failure once. Never send the message of an
|
|
2397
|
+
`Internal` failure to the client (SQL, hosts, secrets…), and send the `errors` of a
|
|
2398
|
+
`ValidationException` so it can mark each field:
|
|
2399
|
+
|
|
2400
|
+
```ts
|
|
2401
|
+
import { DomainException, ValidationException } from '@archi-code/ddd/domain';
|
|
2402
|
+
import { ApplicationException } from '@archi-code/ddd/application';
|
|
2403
|
+
import {
|
|
2404
|
+
ExceptionClassifier,
|
|
2405
|
+
ExceptionKind,
|
|
2406
|
+
InfrastructureException,
|
|
2407
|
+
} from '@archi-code/ddd/infrastructure';
|
|
2408
|
+
|
|
2409
|
+
const STATUS: Record<ExceptionKind, number> = {
|
|
2410
|
+
[ExceptionKind.NotFound]: 404,
|
|
2411
|
+
[ExceptionKind.NotAllowed]: 403,
|
|
2412
|
+
[ExceptionKind.Conflict]: 409,
|
|
2413
|
+
[ExceptionKind.Malformed]: 400,
|
|
2414
|
+
[ExceptionKind.Invalid]: 422,
|
|
2415
|
+
[ExceptionKind.Rejected]: 400,
|
|
2416
|
+
[ExceptionKind.Internal]: 500,
|
|
2417
|
+
};
|
|
2418
|
+
|
|
2419
|
+
app.use((error, req, res, next) => {
|
|
2420
|
+
const kind = ExceptionClassifier.classify(error);
|
|
2421
|
+
const known =
|
|
2422
|
+
error instanceof DomainException ||
|
|
2423
|
+
error instanceof ApplicationException ||
|
|
2424
|
+
error instanceof InfrastructureException;
|
|
2425
|
+
const code = known ? error.code : 'E000';
|
|
2426
|
+
if (kind === ExceptionKind.Internal) {
|
|
2427
|
+
logger.error('http_request_failed', { code, error });
|
|
2428
|
+
return res.status(500).json({ code, message: 'Internal server error' });
|
|
2429
|
+
}
|
|
2430
|
+
const errors = error instanceof ValidationException ? error.errors : undefined;
|
|
2431
|
+
res.status(STATUS[kind]).json({ code, message: error.message, errors });
|
|
2432
|
+
});
|
|
2433
|
+
```
|
|
2434
|
+
|
|
2335
2435
|
## Full example
|
|
2336
2436
|
|
|
2337
2437
|
```ts
|
|
@@ -2340,26 +2440,31 @@ import {
|
|
|
2340
2440
|
AggregateRoot,
|
|
2341
2441
|
BooleanType,
|
|
2342
2442
|
CreatedAt,
|
|
2443
|
+
DomainException,
|
|
2343
2444
|
EventBase,
|
|
2344
2445
|
IdType,
|
|
2345
2446
|
Required,
|
|
2346
2447
|
StringType,
|
|
2347
|
-
ValueObjects,
|
|
2348
2448
|
} from '@archi-code/ddd/domain';
|
|
2349
|
-
import { AggregateNotFoundException, DomainException } from '@archi-code/ddd/exceptions';
|
|
2350
2449
|
import type {
|
|
2351
2450
|
AggregateFinder,
|
|
2352
2451
|
AggregateSaver,
|
|
2353
2452
|
EventMetadata,
|
|
2354
2453
|
PrimitiveTypes,
|
|
2355
2454
|
} from '@archi-code/ddd/domain';
|
|
2356
|
-
import {
|
|
2455
|
+
import {
|
|
2456
|
+
AggregateNotFoundException,
|
|
2457
|
+
EventListener,
|
|
2458
|
+
ListensTo,
|
|
2459
|
+
UseCase,
|
|
2460
|
+
ValueObjectBuilder,
|
|
2461
|
+
} from '@archi-code/ddd/application';
|
|
2357
2462
|
import type { Clock, EventBus } from '@archi-code/ddd/application';
|
|
2358
2463
|
import {
|
|
2359
2464
|
DomainEventBus,
|
|
2360
2465
|
EventRegistry,
|
|
2466
|
+
ExceptionClassifier,
|
|
2361
2467
|
FixedClock,
|
|
2362
|
-
HttpErrorMapper,
|
|
2363
2468
|
InMemoryEventChannel,
|
|
2364
2469
|
} from '@archi-code/ddd/infrastructure';
|
|
2365
2470
|
import { InMemoryUserRepository } from './test/support/in-memory-user-repository';
|
|
@@ -2375,11 +2480,11 @@ class UserRegistered extends EventBase {
|
|
|
2375
2480
|
static readonly eventName = 'UserRegistered';
|
|
2376
2481
|
|
|
2377
2482
|
constructor(
|
|
2378
|
-
userId: string,
|
|
2483
|
+
readonly userId: string,
|
|
2379
2484
|
readonly email: string,
|
|
2380
2485
|
metadata?: EventMetadata,
|
|
2381
2486
|
) {
|
|
2382
|
-
super(
|
|
2487
|
+
super(metadata);
|
|
2383
2488
|
}
|
|
2384
2489
|
}
|
|
2385
2490
|
|
|
@@ -2387,11 +2492,11 @@ class UserRenamed extends EventBase {
|
|
|
2387
2492
|
static readonly eventName = 'UserRenamed';
|
|
2388
2493
|
|
|
2389
2494
|
constructor(
|
|
2390
|
-
userId: string,
|
|
2495
|
+
readonly userId: string,
|
|
2391
2496
|
readonly name: string,
|
|
2392
2497
|
metadata?: EventMetadata,
|
|
2393
2498
|
) {
|
|
2394
|
-
super(
|
|
2499
|
+
super(metadata);
|
|
2395
2500
|
}
|
|
2396
2501
|
}
|
|
2397
2502
|
|
|
@@ -2472,7 +2577,7 @@ class RegisterUser extends UseCase<{ name: string; email: string }, RegisterUser
|
|
|
2472
2577
|
}
|
|
2473
2578
|
|
|
2474
2579
|
protected toCommand(input: { name: string; email: string }): RegisterUserCommand {
|
|
2475
|
-
return
|
|
2580
|
+
return ValueObjectBuilder.build({
|
|
2476
2581
|
name: () => new Name(input.name),
|
|
2477
2582
|
email: () => new Email(input.email),
|
|
2478
2583
|
});
|
|
@@ -2546,9 +2651,12 @@ const channel = new InMemoryEventChannel();
|
|
|
2546
2651
|
const events = new EventRegistry<UserEvent>()
|
|
2547
2652
|
.register(
|
|
2548
2653
|
UserRegistered,
|
|
2549
|
-
(json) => new UserRegistered(json.
|
|
2654
|
+
(json) => new UserRegistered(json.userId as string, json.email as string, json),
|
|
2550
2655
|
)
|
|
2551
|
-
.register(
|
|
2656
|
+
.register(
|
|
2657
|
+
UserRenamed,
|
|
2658
|
+
(json) => new UserRenamed(json.userId as string, json.name as string, json),
|
|
2659
|
+
);
|
|
2552
2660
|
const bus = new DomainEventBus<UserEvent>(channel, events);
|
|
2553
2661
|
await bus.subscribe(new SendWelcomeEmailOnUserRegistered(new SendWelcomeEmail(emails)));
|
|
2554
2662
|
const clock = new FixedClock('2026-01-01T09:00:00Z');
|
|
@@ -2564,73 +2672,67 @@ emails; // ['Welcome, ana@example.com']
|
|
|
2564
2672
|
(await users.findById(new UserId(id)))?.toPrimitives();
|
|
2565
2673
|
// { id, name: 'Ana María', email: 'ana@example.com', blocked: false, createdAt: 2026-01-01T09:00:00.000Z }
|
|
2566
2674
|
|
|
2567
|
-
const errors = new HttpErrorMapper();
|
|
2568
2675
|
await new RenameUser(users, bus, clock)
|
|
2569
2676
|
.execute({ id: UserId.random(), name: 'Eva' })
|
|
2570
|
-
.catch((error) =>
|
|
2571
|
-
//
|
|
2677
|
+
.catch((error) => ExceptionClassifier.classify(error));
|
|
2678
|
+
// 'not_found'
|
|
2572
2679
|
await new RegisterUser(users, bus, clock)
|
|
2573
2680
|
.execute({ name: 'Eva', email: 'x' })
|
|
2574
|
-
.catch((error) =>
|
|
2575
|
-
//
|
|
2681
|
+
.catch((error) => ExceptionClassifier.classify(error));
|
|
2682
|
+
// 'invalid'
|
|
2576
2683
|
```
|
|
2577
2684
|
|
|
2578
2685
|
## Exports
|
|
2579
2686
|
|
|
2580
|
-
`@archi-code/ddd/exceptions`:
|
|
2581
|
-
|
|
2582
|
-
| Export | Kind |
|
|
2583
|
-
| -------------------------------------------------------------------------------------------------------------------------------------------- | -------------- |
|
|
2584
|
-
| `AbstractException` | abstract class |
|
|
2585
|
-
| `DomainException`, `ApplicationException`, `InfrastructureException` | classes |
|
|
2586
|
-
| `ValidationException`, `AggregateNotFoundException`, `TypePrimitiveException`, `RequiredValueException`, `InternalErrorException` | classes |
|
|
2587
|
-
| `ConcurrencyException`, `NotAllowedException`, `UnknownEventException`, `EventDeliveryException` | classes |
|
|
2588
|
-
| `ExceptionCode` | enum |
|
|
2589
|
-
| `ExceptionOptions`, `ValidationIssue` (`{ field: string \| null; code: string; message: string }`), `ValidationIssueInput` (`code` optional) | types |
|
|
2590
|
-
|
|
2591
2687
|
`@archi-code/ddd/domain`:
|
|
2592
2688
|
|
|
2593
|
-
| Export
|
|
2594
|
-
|
|
|
2595
|
-
| `AbstractType`
|
|
2596
|
-
| `StringType`, `NumberType`, `BooleanType`, `DateType`, `UuidType`, `JsonType`
|
|
2597
|
-
| `EnumType`, `ArrayType`
|
|
2598
|
-
| `CreatedAt`, `UpdatedAt`, `IdType`
|
|
2599
|
-
| `Required`, `Composite`
|
|
2600
|
-
| `Specification`
|
|
2601
|
-
| `CompositeSchema`, `CompositeProps`, `CompositePrimitives`, `CompositeInput`, `CompositeType`, `CompositeValue`
|
|
2602
|
-
| `
|
|
2603
|
-
| `
|
|
2604
|
-
| `
|
|
2605
|
-
| `
|
|
2606
|
-
| `
|
|
2607
|
-
| `
|
|
2608
|
-
| `
|
|
2609
|
-
| `
|
|
2689
|
+
| Export | Kind |
|
|
2690
|
+
| ------------------------------------------------------------------------------------------------------------------------------- | -------------------- |
|
|
2691
|
+
| `AbstractType` | abstract class |
|
|
2692
|
+
| `StringType`, `NumberType`, `BooleanType`, `DateType`, `UuidType`, `JsonType` | classes |
|
|
2693
|
+
| `EnumType`, `ArrayType` | abstract classes |
|
|
2694
|
+
| `CreatedAt`, `UpdatedAt`, `IdType` | classes (required) |
|
|
2695
|
+
| `Required`, `Composite` | functions |
|
|
2696
|
+
| `Specification` | abstract class |
|
|
2697
|
+
| `CompositeSchema`, `CompositeProps`, `CompositePrimitives`, `CompositeInput`, `CompositeType`, `CompositeValue` | types |
|
|
2698
|
+
| `Entity`, `AggregateRoot`, `EventBase` | abstract classes |
|
|
2699
|
+
| `AggregateFinder`, `AggregateSaver`, `AggregateRemover` | types (contracts) |
|
|
2700
|
+
| `RequiredType`, `JsonTypeValue`, `PrimitiveType`, `PrimitiveTypes`, `DataTypes` | types |
|
|
2701
|
+
| `EventMetadata`, `SerializedEvent`, `EventClass` | types |
|
|
2702
|
+
| `EntityId`, `SatisfiedBy` | types |
|
|
2703
|
+
| `Properties`, `PropertiesData`, `MutablePropertiesData`, `Methods`, `MethodsAndProperties` | types |
|
|
2704
|
+
| `DomainException`, `ValidationException`, `TypePrimitiveException`, `RequiredValueException`, `InvalidEventDefinitionException` | classes (exceptions) |
|
|
2705
|
+
| `DomainExceptionCode` | enum |
|
|
2706
|
+
| `ValidationIssue` (`{ field: string \| null; code: string; message: string }`), `ValidationIssueInput` (`code` optional) | types |
|
|
2610
2707
|
|
|
2611
2708
|
`@archi-code/ddd/application`:
|
|
2612
2709
|
|
|
2613
|
-
| Export
|
|
2614
|
-
|
|
|
2615
|
-
| `UseCase`, `EventListener`
|
|
2616
|
-
| `
|
|
2617
|
-
| `
|
|
2618
|
-
| `
|
|
2619
|
-
| `
|
|
2620
|
-
| `
|
|
2621
|
-
| `
|
|
2710
|
+
| Export | Kind |
|
|
2711
|
+
| ------------------------------------------------------------------------------------------------------------------------------- | -------------------------------- |
|
|
2712
|
+
| `UseCase`, `EventListener` | abstract classes |
|
|
2713
|
+
| `ValueObjectBuilder` (`build`, `buildList`) | class |
|
|
2714
|
+
| `ListensTo` | decorator |
|
|
2715
|
+
| `ListensToOptions` | type (`{ consumer }`) |
|
|
2716
|
+
| `ListenerSubscription` | type (`{ eventName, consumer }`) |
|
|
2717
|
+
| `EventBus` | type (event port) |
|
|
2718
|
+
| `Clock`, `Logger` | types (ports) |
|
|
2719
|
+
| `LogLevel`, `LogContext` | types (logger port) |
|
|
2720
|
+
| `ApplicationException`, `ConcurrencyException`, `AggregateNotFoundException`, `NotAllowedException`, `UnexpectedErrorException` | classes (exceptions) |
|
|
2721
|
+
| `ApplicationExceptionCode` | enum |
|
|
2622
2722
|
|
|
2623
2723
|
`@archi-code/ddd/infrastructure`:
|
|
2624
2724
|
|
|
2625
|
-
| Export
|
|
2626
|
-
|
|
|
2627
|
-
| `InMemoryEventChannel`, `
|
|
2628
|
-
| `ConsoleLogger`, `InMemoryLogger`, `SilentLogger`
|
|
2629
|
-
| `DomainEventBus`, `EventRegistry`
|
|
2630
|
-
| `EventChannel`, `ChannelHandler`, `ChannelSubscription`
|
|
2631
|
-
| `Subscription`, `EventFactory`
|
|
2632
|
-
| `
|
|
2633
|
-
| `
|
|
2725
|
+
| Export | Kind |
|
|
2726
|
+
| ------------------------------------------------------------------------------------------------------ | ---------------------- |
|
|
2727
|
+
| `InMemoryEventChannel`, `SystemClock`, `FixedClock` | classes |
|
|
2728
|
+
| `ConsoleLogger`, `InMemoryLogger`, `SilentLogger` | classes (loggers) |
|
|
2729
|
+
| `DomainEventBus`, `EventRegistry` | classes (event wiring) |
|
|
2730
|
+
| `EventChannel`, `ChannelHandler`, `ChannelSubscription` | types (channel port) |
|
|
2731
|
+
| `Subscription`, `EventFactory` | types |
|
|
2732
|
+
| `InMemoryEventChannelOptions`, `ConsoleLoggerOptions`, `LogEntry` | types |
|
|
2733
|
+
| `InfrastructureException`, `InternalErrorException`, `UnknownEventException`, `EventDeliveryException` | classes (exceptions) |
|
|
2734
|
+
| `InfrastructureExceptionCode`, `ExceptionKind` | enums |
|
|
2735
|
+
| `ExceptionClassifier` (`classify`) | class |
|
|
2634
2736
|
|
|
2635
2737
|
## AI assistant skill
|
|
2636
2738
|
|