@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
|
@@ -48,6 +48,7 @@ src/
|
|
|
48
48
|
interfaces/
|
|
49
49
|
http/
|
|
50
50
|
ordersRouter.ts
|
|
51
|
+
httpError.ts (traduce ExceptionKind a estado HTTP)
|
|
51
52
|
messaging/
|
|
52
53
|
StockReservedConsumer.ts (traduce el contrato publicado por Almacén al input de un caso de uso)
|
|
53
54
|
composition.ts ← composition root del contexto
|
|
@@ -64,7 +65,7 @@ test/
|
|
|
64
65
|
infrastructure/ (integración con BD real o contenedor)
|
|
65
66
|
```
|
|
66
67
|
|
|
67
|
-
Lo que no aparece porque lo da `@archi-code/ddd`: clases base (`AggregateRoot`, `Entity`, `EventBase`, los tipos de value object, `Composite`), una interfaz `OrderRepository` (los roles `AggregateFinder`, `AggregateSaver` y `AggregateRemover` vienen de `@archi-code/ddd/domain`), un `OrderMapper` (la conversión es del agregado, `toPrimitives()` y `static fromPrimitives()`, y cada repositorio los llama directamente: guarda `order.toPrimitives()` con
|
|
68
|
+
Lo que no aparece porque lo da `@archi-code/ddd`: clases base (`AggregateRoot`, `Entity`, `EventBase`, los tipos de value object, `Composite`), una interfaz `OrderRepository` (los roles `AggregateFinder`, `AggregateSaver` y `AggregateRemover` vienen de `@archi-code/ddd/domain`), un `OrderMapper` (la conversión es del agregado, `toPrimitives()` y `static fromPrimitives()`, y cada repositorio los llama directamente: guarda `order.toPrimitives()` con la versión siguiente al lado y carga con `Order.fromPrimitives(row.primitives)`; la versión la recuerda el propio repositorio en un `WeakMap<Order, number>`, nunca el agregado), un publicador de eventos (`DomainEventBus` implementa `EventBus`) y el puerto `Clock`. Tampoco hay un runner de transacciones ni una unidad de trabajo: la transacción es del repositorio `[Fuera de los libros: convención de @archi-code/ddd]`.
|
|
68
69
|
|
|
69
70
|
Lo que el paquete no da y escribe el proyecto: cada repositorio, uno por agregado. Su constructor recibe solo las dependencias de su tecnología (la `collection` de Mongo, el `pool` de Postgres), nunca la clase del agregado ni un mapper; el agregado solo entra y sale por sus métodos. Nada comprueba en ejecución la forma del agregado: que su estado sean value objects expuestos por getters y que `toPrimitives()`/`fromPrimitives()` no pierdan campos lo garantizan el agregado y su test de ida y vuelta. El repositorio en memoria también es del proyecto y es un doble de test: vive en `test/support/`, nunca en `src/` (ver `InMemoryOrderRepository` en `ddd-skills/ddd-repositorios-persistencia/references/tests-repositorios.md`) `[Fuera de los libros: convención de @archi-code/ddd]`.
|
|
70
71
|
|
|
@@ -116,7 +117,6 @@ import { MongoClient } from 'mongodb';
|
|
|
116
117
|
import {
|
|
117
118
|
DomainEventBus,
|
|
118
119
|
EventRegistry,
|
|
119
|
-
HttpErrorMapper,
|
|
120
120
|
SystemClock,
|
|
121
121
|
} from '@archi-code/ddd/infrastructure';
|
|
122
122
|
import { OrderLineAdded, OrderPlaced } from './domain/order/events';
|
|
@@ -152,7 +152,7 @@ export async function composeSales(
|
|
|
152
152
|
OrderPlaced,
|
|
153
153
|
(json) =>
|
|
154
154
|
new OrderPlaced(
|
|
155
|
-
json.
|
|
155
|
+
json.orderId as string,
|
|
156
156
|
json.customerId as string,
|
|
157
157
|
json.lines as OrderLineData[],
|
|
158
158
|
json.total as MoneyData,
|
|
@@ -163,7 +163,7 @@ export async function composeSales(
|
|
|
163
163
|
OrderLineAdded,
|
|
164
164
|
(json) =>
|
|
165
165
|
new OrderLineAdded(
|
|
166
|
-
json.
|
|
166
|
+
json.orderId as string,
|
|
167
167
|
json.sku as string,
|
|
168
168
|
json.quantity as number,
|
|
169
169
|
json.unitPrice as MoneyData,
|
|
@@ -188,10 +188,7 @@ export async function composeSales(
|
|
|
188
188
|
];
|
|
189
189
|
|
|
190
190
|
return {
|
|
191
|
-
httpRouter: ordersRouter(
|
|
192
|
-
{ placeOrder, addOrderLine },
|
|
193
|
-
new HttpErrorMapper({ statusByCode: { ORD001: 409 }, logger: deps.logger }),
|
|
194
|
-
),
|
|
191
|
+
httpRouter: ordersRouter({ placeOrder, addOrderLine }, deps.logger),
|
|
195
192
|
close: async () => {
|
|
196
193
|
await Promise.all(subscriptions.map((subscription) => subscription.unsubscribe()));
|
|
197
194
|
await mongo.close();
|
|
@@ -201,8 +198,8 @@ export async function composeSales(
|
|
|
201
198
|
```
|
|
202
199
|
|
|
203
200
|
Qué hace cada pieza:
|
|
204
|
-
* Del paquete, en `@archi-code/ddd/infrastructure`: `EventRegistry` (reconstruye cada evento desde su JSON; la factoría pasa el JSON como metadata para conservar `eventId` y `occurredOn`), `DomainEventBus` (implementa `EventBus`, lo único que reciben los casos de uso; su `subscribe(listener)` solo lo llama el composition root), `SystemClock` (implementa `Clock`)
|
|
205
|
-
* Código del proyecto sobre los puertos del paquete `[Fuera de los libros: convención de @archi-code/ddd]`: `MongoOrderRepository` (roles de repositorio de `Order`; su constructor recibe solo la `collection`; llama a `order.toPrimitives()` y a `Order.fromPrimitives(row
|
|
201
|
+
* Del paquete, en `@archi-code/ddd/infrastructure`: `EventRegistry` (reconstruye cada evento desde su JSON; la factoría pasa el JSON como metadata para conservar `eventId` y `occurredOn`), `DomainEventBus` (implementa `EventBus`, lo único que reciben los casos de uso; su `subscribe(listener)` solo lo llama el composition root), y `SystemClock` (implementa `Clock`).
|
|
202
|
+
* Código del proyecto sobre los puertos del paquete `[Fuera de los libros: convención de @archi-code/ddd]`: `MongoOrderRepository` (roles de repositorio de `Order`; su constructor recibe solo la `collection`; llama a `order.toPrimitives()` y a `Order.fromPrimitives(row.primitives)` y controla la versión, que recuerda por agregado en un `WeakMap` y guarda junto a los primitivos; responsable de toda la persistencia: cada `save`/`remove` es una escritura atómica de un documento, y si un agregado ocupara varias tablas SQL abriría y cerraría la transacción de la base de datos dentro de `save`/`remove`; ver `ddd-skills/ddd-repositorios-persistencia/GUIA.md`), `RabbitMqEventChannel` (implementa `EventChannel`, ver `ddd-skills/ddd-eventos-dominio/GUIA.md`) y `HttpWarehouse` (implementa el puerto `Warehouse`).
|
|
206
203
|
* Cada listener se suscribe con `bus.subscribe(listener)`, que lee su `@ListensTo(OrderLineAdded, { consumer: 'prepare-shipment-on-order-line-added' })` y rechaza un evento que el registro no sabe reconstruir.
|
|
207
204
|
* Cambiar RabbitMQ por un outbox o por memoria cambia solo la línea del canal.
|
|
208
205
|
* Ni los casos de uso ni los repositorios reciben un runner de transacciones: el composition root no construye ninguno.
|
|
@@ -236,7 +233,7 @@ export async function salesForTests(warehouse: Warehouse) {
|
|
|
236
233
|
OrderLineAdded,
|
|
237
234
|
(json) =>
|
|
238
235
|
new OrderLineAdded(
|
|
239
|
-
json.
|
|
236
|
+
json.orderId as string,
|
|
240
237
|
json.sku as string,
|
|
241
238
|
json.quantity as number,
|
|
242
239
|
json.unitPrice as MoneyData,
|
|
@@ -264,18 +261,19 @@ El test comprueba lo que causó el listener (el `Warehouse` de prueba) y lo que
|
|
|
264
261
|
// src/contexts/sales/interfaces/http/ordersRouter.ts
|
|
265
262
|
import { Router } from 'express';
|
|
266
263
|
import type { Response } from 'express';
|
|
267
|
-
import type {
|
|
264
|
+
import type { Logger } from '@archi-code/ddd/application';
|
|
268
265
|
import type { AddOrderLine } from '../../application/add-order-line/AddOrderLine';
|
|
269
266
|
import type { PlaceOrder } from '../../application/place-order/PlaceOrder';
|
|
267
|
+
import { toHttpError } from './httpError';
|
|
270
268
|
|
|
271
269
|
export function ordersRouter(
|
|
272
270
|
useCases: { placeOrder: PlaceOrder; addOrderLine: AddOrderLine },
|
|
273
|
-
|
|
271
|
+
logger: Logger,
|
|
274
272
|
): Router {
|
|
275
273
|
const router = Router();
|
|
276
274
|
|
|
277
275
|
const fail = (res: Response, error: unknown) => {
|
|
278
|
-
const { status, body } =
|
|
276
|
+
const { status, body } = toHttpError(error, logger);
|
|
279
277
|
return res.status(status).json(body);
|
|
280
278
|
};
|
|
281
279
|
|
|
@@ -340,13 +338,69 @@ export function ordersRouter(
|
|
|
340
338
|
}
|
|
341
339
|
```
|
|
342
340
|
|
|
343
|
-
El controlador no sabe qué es un `Order`. Comprueba solo la forma de la petición (un campo es texto o número) [IDDD cap. 4]; las reglas de cada valor son de los value objects que `toCommand` construye con `
|
|
341
|
+
El controlador no sabe qué es un `Order`. Comprueba solo la forma de la petición (un campo es texto o número) [IDDD cap. 4]; las reglas de cada valor son de los value objects que `toCommand` construye con `ValueObjectBuilder.build`, y llegan como una `ValidationException` con todos los campos (422). La identidad del cliente sale del token, no del body. `toHttpError` traduce los errores. El paquete no trae nada de HTTP: `ExceptionClassifier.classify(error)` de `@archi-code/ddd/infrastructure` devuelve el `ExceptionKind` del fallo y cada protocolo (HTTP, un consumidor de mensajes, una CLI) lo traduce a sus códigos `[Fuera de los libros: convención de @archi-code/ddd]`:
|
|
342
|
+
|
|
343
|
+
```ts
|
|
344
|
+
// src/contexts/sales/interfaces/http/httpError.ts
|
|
345
|
+
import { DomainException, ValidationException } from '@archi-code/ddd/domain';
|
|
346
|
+
import type { ValidationIssue } from '@archi-code/ddd/domain';
|
|
347
|
+
import { ApplicationException, ApplicationExceptionCode } from '@archi-code/ddd/application';
|
|
348
|
+
import type { Logger } from '@archi-code/ddd/application';
|
|
349
|
+
import {
|
|
350
|
+
ExceptionClassifier,
|
|
351
|
+
ExceptionKind,
|
|
352
|
+
InfrastructureException,
|
|
353
|
+
} from '@archi-code/ddd/infrastructure';
|
|
354
|
+
|
|
355
|
+
export interface HttpError {
|
|
356
|
+
status: number;
|
|
357
|
+
body: { code: string; message: string; errors?: readonly ValidationIssue[] };
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
const STATUS_BY_KIND: Readonly<Record<ExceptionKind, number>> = {
|
|
361
|
+
[ExceptionKind.NotFound]: 404,
|
|
362
|
+
[ExceptionKind.NotAllowed]: 403,
|
|
363
|
+
[ExceptionKind.Conflict]: 409,
|
|
364
|
+
[ExceptionKind.Malformed]: 400,
|
|
365
|
+
[ExceptionKind.Invalid]: 422,
|
|
366
|
+
[ExceptionKind.Rejected]: 400,
|
|
367
|
+
[ExceptionKind.Internal]: 500,
|
|
368
|
+
};
|
|
369
|
+
|
|
370
|
+
const STATUS_BY_CODE = new Map<string, number>([['ORD001', 409]]);
|
|
371
|
+
|
|
372
|
+
type PackageException = DomainException | ApplicationException | InfrastructureException;
|
|
373
|
+
|
|
374
|
+
function isPackageException(error: unknown): error is PackageException {
|
|
375
|
+
return (
|
|
376
|
+
error instanceof DomainException ||
|
|
377
|
+
error instanceof ApplicationException ||
|
|
378
|
+
error instanceof InfrastructureException
|
|
379
|
+
);
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
export function toHttpError(error: unknown, logger: Logger): HttpError {
|
|
383
|
+
const code = isPackageException(error) ? error.code : ApplicationExceptionCode.UnexpectedError;
|
|
384
|
+
const status = STATUS_BY_CODE.get(code) ?? STATUS_BY_KIND[ExceptionClassifier.classify(error)];
|
|
385
|
+
if (status >= 500 || !isPackageException(error)) {
|
|
386
|
+
logger.error('http_request_failed', { status, code, error });
|
|
387
|
+
return { status, body: { code, message: 'Internal server error' } };
|
|
388
|
+
}
|
|
389
|
+
logger.debug('http_request_rejected', { status, code });
|
|
390
|
+
if (error instanceof ValidationException) {
|
|
391
|
+
return { status, body: { code, message: error.message, errors: error.errors } };
|
|
392
|
+
}
|
|
393
|
+
return { status, body: { code, message: error.message } };
|
|
394
|
+
}
|
|
395
|
+
```
|
|
396
|
+
|
|
397
|
+
En HTTP: 404 `AggregateNotFoundException`, 403 `NotAllowedException`, 409 `ConcurrencyException`, 400 un error de conversión u obligatoriedad, 422 `ValidationException` y cualquier otra `DomainException`, 400 otros errores de aplicación y 500 con un mensaje genérico para lo demás (también `UnexpectedErrorException`, el error ajeno que `UseCase.execute` envuelve); los códigos propios (`ORD001`) se resuelven antes que el tipo. Cada 5xx se registra una vez como `error` (con su causa) y los 4xx solo en `debug`; el controlador no vuelve a registrarlos. Un consumidor de mensajes traduce el mismo `ExceptionKind` a confirmar, descartar o reintentar.
|
|
344
398
|
|
|
345
399
|
## Configuración de TypeScript
|
|
346
400
|
|
|
347
401
|
`[Fuera de los libros]`
|
|
348
402
|
* `strict: true`. El dominio se apoya en el tipado para expresar invariantes.
|
|
349
403
|
* `experimentalDecorators: true`: `@Validations` (value objects) y `@ListensTo` (listeners) son decoradores de clase.
|
|
350
|
-
* `moduleResolution` `node16`, `nodenext` o `bundler` para resolver los subpaths `@archi-code/ddd/domain`, `/application
|
|
404
|
+
* `moduleResolution` `node16`, `nodenext` o `bundler` para resolver los subpaths `@archi-code/ddd/domain`, `/application` y `/infrastructure` (cada uno exporta las excepciones de su capa); con `node` los resuelve el `typesVersions` del paquete.
|
|
351
405
|
* Alias por contexto (`"@sales/*": ["src/contexts/sales/*"]`, usado como `@sales/domain/order/Order`) para que los imports revelen la zona.
|
|
352
406
|
* `exactOptionalPropertyTypes` y `noUncheckedIndexedAccess` ayudan a que los value objects no acepten estados a medias.
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
[Fuera de los libros: convención de @archi-code/ddd]
|
|
10
10
|
|
|
11
11
|
* En código hecho con el paquete, aplica además cada punto de `ddd-skills/archi-code-ddd/what-not-to-do.md` como lista de comprobación: el paquete da las clases base, pero no impide un agregado anémico con setters, un caso de uso CRUD, dos agregados guardados en el mismo caso de uso o un caso de uso que abre su propia transacción (la transacción es del repositorio).
|
|
12
|
-
* La dirección de dependencias es la del paquete: el dominio importa solo `@archi-code/ddd/domain
|
|
12
|
+
* La dirección de dependencias es la del paquete: el dominio importa solo `@archi-code/ddd/domain` (que exporta también las excepciones de dominio) y `@archi-code/validation`; importar `/application` o `/infrastructure` (por ejemplo `SystemClock`) o un adaptador de `infrastructure/` del proyecto desde el dominio es un hallazgo de severidad alta (A8 de la lista).
|
|
13
13
|
* El reloj es un puerto (`Clock`; `FixedClock` en tests): `new Date()` en dominio o aplicación es un hallazgo (A9).
|
|
14
14
|
* El escáner detecta guardados de cualquier variable (`this.orders.save(...)`, `this.customers.remove(...)`) dentro del mismo bloque transaccional o de la misma clase de aplicación, transacciones abiertas o recibidas en dominio y aplicación (bloques transaccionales, `startSession`, runners de transacciones, unidades de trabajo), imports de `@archi-code/ddd/application` o `/infrastructure` desde el dominio, `new Date()` en dominio y aplicación, y campos de estado primitivos con el prefijo `_` del paquete (`private _status: string`). Marca `amount: number` o `status: string` también dentro de los eventos: ahí son esperables, porque el payload de un evento son primitivos.
|
|
15
15
|
|
|
@@ -85,7 +85,7 @@ class PlaceOrder extends UseCase<PlaceOrderInput, PlaceOrderCommand, void> {
|
|
|
85
85
|
| 5 | Estado y total como primitivos (`_status: string`, `_total: number`) en vez de value objects | `domain/order/order.ts:5-6` | Media |
|
|
86
86
|
| 6 | `toCommand` devuelve el input tal cual: ningún value object se construye ni valida antes de `handle` | `application/update-order/update-order.ts:14` | Media |
|
|
87
87
|
| 7 | `new Date()` en el agregado: fechas no deterministas en tests | `domain/order/order.ts:10` | Media |
|
|
88
|
-
| 8 | `throw new Error(...)` en vez de una `DomainException` con código:
|
|
88
|
+
| 8 | `throw new Error(...)` en vez de una `DomainException` con código: `ExceptionClassifier` lo clasifica como `ExceptionKind.Internal` y el cliente recibe 500 | `application/update-order/update-order.ts:18` | Media |
|
|
89
89
|
| 9 | Los casos de uso reciben un `UnitOfWork` del proyecto y abren la transacción con `unitOfWork.run`: la transacción y todo lo de persistencia son del adaptador del repositorio, que hace atómico cada `save` | `application/update-order/update-order.ts:17`, `application/place-order/place-order.ts:27` | Media |
|
|
90
90
|
|
|
91
91
|
Los hallazgos 2, 3 y 6 se cruzan además con `ddd-skills/archi-code-ddd/what-not-to-do.md` (casos de uso CRUD, value objects fuera de `toCommand`, reglas de negocio fuera del agregado).
|
|
@@ -108,10 +108,8 @@ addLine(line: OrderLine, clock: { now(): Date }): void;
|
|
|
108
108
|
Enviar no es trabajo del pedido de Ventas: lo hace `Shipment` en Almacén, que publica `ShipmentDispatched` (`ddd-skills/ddd-agregados/references/ejemplo-ecommerce.md`), así que `status: 'shipped'` y su errata desaparecen de Ventas. `UpdateOrder` se parte en un caso de uso por acción de negocio (`AddOrderLine` aquí; el envío, en Almacén); cada uno construye sus value objects en `toCommand` y guarda un solo agregado:
|
|
109
109
|
|
|
110
110
|
```ts
|
|
111
|
-
import { ValueObjects } from '@archi-code/ddd/domain';
|
|
112
111
|
import type { AggregateFinder, AggregateSaver } from '@archi-code/ddd/domain';
|
|
113
|
-
import { AggregateNotFoundException } from '@archi-code/ddd/
|
|
114
|
-
import { UseCase } from '@archi-code/ddd/application';
|
|
112
|
+
import { AggregateNotFoundException, UseCase, ValueObjectBuilder } from '@archi-code/ddd/application';
|
|
115
113
|
import type { Clock, EventBus } from '@archi-code/ddd/application';
|
|
116
114
|
|
|
117
115
|
interface AddOrderLineInput {
|
|
@@ -136,7 +134,7 @@ class AddOrderLine extends UseCase<AddOrderLineInput, AddOrderLineCommand, void>
|
|
|
136
134
|
}
|
|
137
135
|
|
|
138
136
|
protected toCommand(input: AddOrderLineInput): AddOrderLineCommand {
|
|
139
|
-
return
|
|
137
|
+
return ValueObjectBuilder.build({
|
|
140
138
|
orderId: () => new OrderId(input.orderId),
|
|
141
139
|
line: () =>
|
|
142
140
|
OrderLine.fromPrimitives({
|
|
@@ -198,7 +196,7 @@ class PlaceOrder extends UseCase<PlaceOrderInput, PlaceOrderCommand, void> {
|
|
|
198
196
|
}
|
|
199
197
|
|
|
200
198
|
protected toCommand(input: PlaceOrderInput): PlaceOrderCommand {
|
|
201
|
-
return
|
|
199
|
+
return ValueObjectBuilder.build({
|
|
202
200
|
orderId: () => new OrderId(input.orderId),
|
|
203
201
|
customerId: () => new CustomerId(input.customerId),
|
|
204
202
|
lines: () => OrderLines.fromPrimitives({ items: input.lines }),
|
|
@@ -225,7 +223,7 @@ class AwardLoyaltyPoints extends UseCase<AwardLoyaltyPointsInput, AwardLoyaltyPo
|
|
|
225
223
|
}
|
|
226
224
|
|
|
227
225
|
protected toCommand(input: AwardLoyaltyPointsInput): AwardLoyaltyPointsCommand {
|
|
228
|
-
return
|
|
226
|
+
return ValueObjectBuilder.build({
|
|
229
227
|
customerId: () => new CustomerId(input.customerId),
|
|
230
228
|
orderId: () => new OrderId(input.orderId),
|
|
231
229
|
});
|
|
@@ -250,7 +248,7 @@ class AwardLoyaltyPointsOnOrderPlaced extends EventListener<OrderPlaced> {
|
|
|
250
248
|
async handle(event: OrderPlaced): Promise<void> {
|
|
251
249
|
await this.awardLoyaltyPoints.execute({
|
|
252
250
|
customerId: event.customerId,
|
|
253
|
-
orderId: event.
|
|
251
|
+
orderId: event.orderId,
|
|
254
252
|
});
|
|
255
253
|
}
|
|
256
254
|
}
|
|
@@ -266,7 +264,7 @@ import {
|
|
|
266
264
|
FixedClock,
|
|
267
265
|
InMemoryEventChannel,
|
|
268
266
|
} from '@archi-code/ddd/infrastructure';
|
|
269
|
-
import { ValidationException } from '@archi-code/ddd/
|
|
267
|
+
import { ValidationException } from '@archi-code/ddd/domain';
|
|
270
268
|
import { InMemoryCustomerRepository } from '../support/in-memory-customer-repository';
|
|
271
269
|
import { InMemoryOrderRepository } from '../support/in-memory-order-repository';
|
|
272
270
|
|
|
@@ -276,7 +274,7 @@ const registry = new EventRegistry<OrderEvent>().register(
|
|
|
276
274
|
OrderPlaced,
|
|
277
275
|
(json) =>
|
|
278
276
|
new OrderPlaced(
|
|
279
|
-
json.
|
|
277
|
+
json.orderId as string,
|
|
280
278
|
json.customerId as string,
|
|
281
279
|
json.lines as OrderLineData[],
|
|
282
280
|
json.total as MoneyData,
|
|
@@ -311,6 +309,6 @@ await expect(
|
|
|
311
309
|
## Plan
|
|
312
310
|
|
|
313
311
|
* **Fase 0:** tests de caracterización de alta, envío y puntos de fidelidad sobre `InMemoryOrderRepository` e `InMemoryCustomerRepository` (dobles de test del proyecto en `test/support/`), `InMemoryEventChannel` y `FixedClock`.
|
|
314
|
-
* **Fase 1:** borrar `OrderMapper`: la conversión pasa al agregado (`toPrimitives()` y `static fromPrimitives()`, con un test de ida y vuelta) y `MongoOrderRepository` la llama directamente (su constructor recibe solo la `collection`), sustituir `new Date()` por el `Clock` inyectado y `Error` por `DomainException` con código; `
|
|
312
|
+
* **Fase 1:** borrar `OrderMapper`: la conversión pasa al agregado (`toPrimitives()` y `static fromPrimitives()`, con un test de ida y vuelta) y `MongoOrderRepository` la llama directamente (su constructor recibe solo la `collection`), sustituir `new Date()` por el `Clock` inyectado y `Error` por `DomainException` con código; `ExceptionClassifier` clasifica las reglas de negocio como `ExceptionKind.Invalid` (422 en HTTP). Quitar el `UnitOfWork` de los casos de uso: `MongoOrderRepository` hace atómico cada `save` (una escritura de un documento con condición de versión) y los casos de uso solo reciben roles de repositorio, `EventBus` y `Clock`.
|
|
315
313
|
* **Fase 2:** `OrderStatus`, `OrderLines` (con `OrderLine` y `Money`) como value objects; `addLine` sustituye a los setters; `UpdateOrder` se parte en `AddOrderLine` y el envío pasa a `Shipment` en Almacén.
|
|
316
314
|
* **Fase 3:** `PlaceOrder` guarda solo el pedido; `AwardLoyaltyPointsOnOrderPlaced` ejecuta `AwardLoyaltyPoints` por evento. Para que `OrderPlaced` no se pierda si el proceso cae entre `save` y `publish`, `MongoOrderRepository` escribe el pedido y sus eventos pendientes en el mismo documento (outbox) y un relay los entrega después por el `EventChannel`; los casos de uso no cambian.
|
|
@@ -12,7 +12,7 @@ La columna "Script" indica si `scan_ddd_smells.py` lo detecta (sí / parcial / n
|
|
|
12
12
|
| E4 | ¿Las integraciones con sistemas externos pasan por una ACL (puerto del dominio con value objects, adaptador y traductor en `infrastructure/`)? | [Destilado cap. 4] | no | Alta |
|
|
13
13
|
| E5 | ¿Hay cadenas de llamadas síncronas entre servicios dentro de una petición? | [Destilado cap. 4] | no | Alta |
|
|
14
14
|
| E6 | ¿Usuarios y permisos mezclados en el modelo del núcleo? La autorización va en el caso de uso (`NotAllowedException`), no en el agregado | [IDDD cap. 2] | no | Media |
|
|
15
|
-
| E7 | ¿Se envía
|
|
15
|
+
| E7 | ¿Se envía un evento de dominio tal cual (`{ ...event }`) a otro contexto en vez de un contrato publicado y versionado? | [IDDD cap. 13] | no | Alta |
|
|
16
16
|
|
|
17
17
|
## Arquitectura
|
|
18
18
|
|
|
@@ -20,7 +20,7 @@ La columna "Script" indica si `scan_ddd_smells.py` lo detecta (sí / parcial / n
|
|
|
20
20
|
|---|---|---|---|---|
|
|
21
21
|
| A1 | ¿El dominio importa frameworks, ORM o clientes HTTP? | [Destilado cap. 2; IDDD cap. 4] | sí | Alta |
|
|
22
22
|
| A2 | ¿Decoradores del ORM en clases de dominio? | [IDDD cap. 6] | sí | Alta |
|
|
23
|
-
| A3 | ¿Controladores con lógica de negocio, que importan repositorios o acceden a la base de datos? Un punto de entrada solo llama a casos de uso y traduce errores
|
|
23
|
+
| A3 | ¿Controladores con lógica de negocio, que importan repositorios o acceden a la base de datos? Un punto de entrada solo llama a casos de uso y traduce errores según el `ExceptionKind` de `ExceptionClassifier` | [IDDD cap. 4] | parcial | Alta |
|
|
24
24
|
| A4 | ¿Un caso de uso, servicio de aplicación o el dominio abre o maneja transacciones (`BEGIN`/`COMMIT`, `startSession`, `withTransaction`) o recibe un runner de transacciones o una unidad de trabajo? IDDD pone la transacción en la aplicación [IDDD cap. 12; IDDD cap. 14]; con el paquete se mueve al adaptador del repositorio, que hace atómico cada `save`/`remove` | `[Fuera de los libros: convención de @archi-code/ddd]` | sí | Media |
|
|
25
25
|
| A5 | ¿Un caso de uso devuelve el agregado al controlador o la API expone `toPrimitives()` tal cual? | [IDDD cap. 4; Destilado cap. 4] | no | Media |
|
|
26
26
|
| A6 | ¿Módulos nombrados por patrón (`entities/`, `services/`, `listeners/`)? | [PHP: Modules] | no | Baja |
|
|
@@ -39,7 +39,7 @@ La columna "Script" indica si `scan_ddd_smells.py` lo detecta (sí / parcial / n
|
|
|
39
39
|
| T4 | ¿Agregados con colecciones que crecen sin límite? | [IDDD cap. 10] | no | Alta |
|
|
40
40
|
| T5 | ¿Referencias a objetos de otros agregados en vez de identidades (`CustomerId extends IdType<'CustomerId'>`)? | [Destilado cap. 5] | no | Media |
|
|
41
41
|
| T6 | ¿Un caso de uso guarda más de un agregado (o los junta en una misma transacción) sin una excepción de IDDD cap. 10 escrita como decisión? Una escritura atómica por agregado, garantizada por el repositorio | [Destilado cap. 5; IDDD cap. 10] | parcial | Alta |
|
|
42
|
-
| T7 | ¿El repositorio
|
|
42
|
+
| T7 | ¿El repositorio lleva la versión de concurrencia optimista él mismo: recuerda la de cada agregado que carga o guarda en un `WeakMap<Order, number>` (`0` si es nuevo), guarda con condición de versión, lanza `ConcurrencyException`, guarda `order.toPrimitives()` con `version + 1` al lado (nunca dentro) y actualiza el `WeakMap` solo tras escribir con éxito? ¿El agregado no declara ni mapea ninguna versión? | [IDDD cap. 10]; `[Fuera de los libros: convención de @archi-code/ddd]` para el `WeakMap` | no | Alta si hay concurrencia |
|
|
43
43
|
| T8 | ¿Repositorios de partes internas, una interfaz `Repository` todo en uno, métodos con nombres o reglas de negocio, o lecturas que cargan agregados? Solo `AggregateFinder`/`AggregateSaver`/`AggregateRemover`, y las lecturas son consultas de aplicación | [IDDD cap. 12] | parcial | Media |
|
|
44
44
|
| T9 | ¿Casos de uso con nombres CRUD o técnicos (`UpdateOrder`, `OrderService`), que hacen varias cosas según el input, que llaman a otro caso de uso, o clases `Manager`/`Helper`? | [IDDD cap. 14; Destilado cap. 5] | parcial | Media |
|
|
45
45
|
| T10 | ¿Eventos con nombre en pasado y del lenguaje (`static readonly eventName = 'OrderPlaced'`)? ¿O `OrderUpdated` con todo el estado o con value objects en el payload? | [Destilado cap. 6] | no | Media |
|
|
@@ -48,7 +48,7 @@ La columna "Script" indica si `scan_ddd_smells.py` lo detecta (sí / parcial / n
|
|
|
48
48
|
| T13 | ¿Identidad generada temprano (`OrderId.generate()` antes de crear) y pasada a `super(id)`? ¿Algún id opcional (`extends StringType` sin `Required`)? | [IDDD cap. 5] | no | Media |
|
|
49
49
|
| T14 | ¿Tests de aceptación con lenguaje del negocio y datos realistas (el `InMemoryOrderRepository` del proyecto en `test/support/`, `FixedClock`, `channel.sent`)? | [Destilado cap. 2; IDDD cap. 1] | no | Media |
|
|
50
50
|
| T15 | ¿Cada listener se llama `<Action>On<Event>`, tiene `@ListensTo(Event, { consumer: '…' })` con un nombre literal estable y solo traduce el evento a la entrada de un caso de uso? | `[Fuera de los libros: convención de @archi-code/ddd]` | no | Media |
|
|
51
|
-
| T16 | ¿Los value objects se construyen en `toCommand` con `
|
|
51
|
+
| T16 | ¿Los value objects se construyen en `toCommand` con `ValueObjectBuilder.build` (todos los campos a la vez) y no en `handle` ni en el controlador? | `[Fuera de los libros: convención de @archi-code/ddd]` | no | Media |
|
|
52
52
|
| T17 | ¿Un repositorio genérico compartido por varios agregados, o un repositorio cuyo constructor recibe la clase del agregado o un mapper? Cada repositorio es de un agregado (`MongoOrderRepository` implementa los roles de `Order`), su constructor recibe solo las dependencias de su tecnología (`collection`, `pool`) y llama directamente a `toPrimitives()`/`fromPrimitives()` del agregado | `[Fuera de los libros: convención de @archi-code/ddd]` | no | Media |
|
|
53
53
|
| T18 | ¿La conversión a o desde primitivos está fuera del agregado (un servicio de dominio o un mapper escrito a mano)? Es del agregado: `toPrimitives()` y `static fromPrimitives()`, que el repositorio llama directamente; un test de ida y vuelta por agregado comprueba que no se pierde ningún campo | `[Fuera de los libros: convención de @archi-code/ddd]` | no | Media |
|
|
54
54
|
| T19 | ¿Un repositorio en memoria en `src/`? Es un doble de test del proyecto (`InMemoryOrderRepository`, ver `ddd-skills/ddd-repositorios-persistencia/references/tests-repositorios.md`): se mueve a `test/support/` | `[Fuera de los libros: convención de @archi-code/ddd]` | no | Baja |
|
|
@@ -17,7 +17,7 @@ export interface ProductPricing {
|
|
|
17
17
|
```
|
|
18
18
|
|
|
19
19
|
```ts
|
|
20
|
-
import { InternalErrorException } from '@archi-code/ddd/
|
|
20
|
+
import { InternalErrorException } from '@archi-code/ddd/infrastructure';
|
|
21
21
|
|
|
22
22
|
interface LegacyArticleRow {
|
|
23
23
|
article_code: string;
|
|
@@ -60,7 +60,7 @@ La idea de Evans de un **contexto burbuja** (bubble context): un pequeño contex
|
|
|
60
60
|
## Fase 3. Refactorizar el táctico dentro de cada contexto
|
|
61
61
|
|
|
62
62
|
Orden recomendado `[Fuera de los libros, basado en dependencias entre reglas]`:
|
|
63
|
-
1. Separar dominio de infraestructura: el agregado expone `static fromPrimitives(PrimitiveTypes<X>)` y `toPrimitives()`, los repositorios implementan `AggregateFinder`/`AggregateSaver`/`AggregateRemover` de un solo agregado en `infrastructure/`, que llaman directamente a `x.toPrimitives()` y a `X.fromPrimitives(row)
|
|
63
|
+
1. Separar dominio de infraestructura: el agregado expone `static fromPrimitives(PrimitiveTypes<X>)` y `toPrimitives()`, los repositorios implementan `AggregateFinder`/`AggregateSaver`/`AggregateRemover` de un solo agregado en `infrastructure/`, que llaman directamente a `x.toPrimitives()` y a `X.fromPrimitives(row.primitives)` y recuerdan la versión de cada agregado en un `WeakMap` propio (el agregado no la declara ni la mapea), con un constructor que recibe solo las dependencias de su tecnología (nunca la clase del agregado ni un mapper), sin decoradores del ORM ni imports de `/infrastructure` en el dominio. Un repositorio genérico compartido por varios agregados se parte en uno por agregado; un servicio de dominio o un mapper escrito a mano que convierte a primitivos se sustituye por `toPrimitives()`/`fromPrimitives()` del agregado, con un test de ida y vuelta por agregado; un repositorio en memoria que esté en `src/` se mueve a `test/support/` `[Fuera de los libros: convención de @archi-code/ddd]`.
|
|
64
64
|
2. Introducir value objects para los primitivos más usados: `Money` como `Composite({ amount: Cents, currency: Currency })`, identificadores `class OrderId extends IdType<'OrderId'>` (la marca hace que no se confundan ids de agregados distintos), estados `extends Required(StatusType)` sobre `EnumType`.
|
|
65
65
|
3. Sustituir setters por métodos de negocio con nombres del lenguaje (`ship`, `addLine`) que comprueban la invariante y llaman a `record()`, moviendo la lógica desde los casos de uso; partir los casos de uso CRUD (`UpdateOrder`) en uno por acción (`ShipOrder`, `AddOrderLine`).
|
|
66
66
|
4. Partir agregados grandes con el procedimiento de `ddd-skills/ddd-agregados/GUIA.md` (plazos de consistencia).
|
|
@@ -8,10 +8,10 @@
|
|
|
8
8
|
|
|
9
9
|
[Fuera de los libros: convención de @archi-code/ddd]
|
|
10
10
|
|
|
11
|
-
* El paquete no trae capa anticorrupción, Open Host Service ni Published Language: se escriben en el proyecto sobre sus clases y puertos. El puerto hacia otro contexto (`ProductPricing`) es una interfaz en `domain/` que recibe y devuelve value objects (`Sku`, un `Composite` `SellableProduct`); adaptador (`CatalogHttpProductPricing`) y traductor (`CatalogTranslator`) van en `infrastructure/` y lanzan una `InfrastructureException` con código propio (`CAT001`), que
|
|
12
|
-
* Un webhook entrante es un punto de entrada en `interfaces/` que traduce el lenguaje del emisor a la entrada de un `UseCase` y responde
|
|
13
|
-
* Los mensajes entre contextos viajan como `SerializedEvent` plano (`{ eventName,
|
|
14
|
-
* Lo que sale se traduce al Published Language en infraestructura, con un `EventChannel` propio en el adaptador del broker (`ContractPublishingChannel` + `SalesContracts`, en `ddd-skills/ddd-eventos-dominio/references/publicacion-ts.md`); nunca se publica fuera del contexto
|
|
11
|
+
* El paquete no trae capa anticorrupción, Open Host Service ni Published Language: se escriben en el proyecto sobre sus clases y puertos. El puerto hacia otro contexto (`ProductPricing`) es una interfaz en `domain/` que recibe y devuelve value objects (`Sku`, un `Composite` `SellableProduct`); adaptador (`CatalogHttpProductPricing`) y traductor (`CatalogTranslator`) van en `infrastructure/` y lanzan una `InfrastructureException` con código propio (`CAT001`), que el punto de entrada traduce por su código (`CAT001` → 503 en HTTP; sin traducción propia, `ExceptionClassifier.classify` la clasifica como `ExceptionKind.Internal`).
|
|
12
|
+
* Un webhook entrante es un punto de entrada en `interfaces/` que traduce el lenguaje del emisor a la entrada de un `UseCase` y responde según el `ExceptionKind` que devuelve `ExceptionClassifier.classify(error)` (de `@archi-code/ddd/infrastructure`).
|
|
13
|
+
* Los mensajes entre contextos viajan como `SerializedEvent` plano (`{ eventName, eventId, occurredOn, ...datos }`, con la identidad del agregado entre los datos, como `orderId`) por un `EventChannel` (tu canal de broker o de outbox), no con el envoltorio de IDDD `{ notificationId, typeName, version, occurredOn, event }`: `eventName` hace de `typeName` (`'sales.order.OrderPlaced'`) y también identifica la versión del contrato (una versión incompatible se publica con otro nombre, como `'sales.order.OrderPlacedV2'`), y `eventId` identifica el mensaje para deduplicar. No hay `notificationId` secuencial: si un feed REST lo necesita, lo asigna tu canal duradero.
|
|
14
|
+
* Lo que sale se traduce al Published Language en infraestructura, con un `EventChannel` propio en el adaptador del broker (`ContractPublishingChannel` + `SalesContracts`, en `ddd-skills/ddd-eventos-dominio/references/publicacion-ts.md`); nunca se publica fuera del contexto un evento interno tal cual (`{ ...event }`).
|
|
15
15
|
* Lo que entra de otro contexto se lee con una clase propia `extends EventBase` que lleva el nombre publicado y solo los campos que este contexto usa; la factoría de `EventRegistry` es el traductor, y un `EventListener` con `@ListensTo(Evento, { consumer })` ejecuta un caso de uso, que valida cada campo con value objects en `toCommand`.
|
|
16
16
|
* Los procesos entre contextos se encadenan con eventos y listeners, cada uno con su caso de uso.
|
|
17
17
|
|
|
@@ -72,7 +72,7 @@ Para cada evento que cruza un contexto, decide entre **evento enriquecido** (má
|
|
|
72
72
|
* API REST que devuelve el agregado serializado con todos sus campos.
|
|
73
73
|
* Una petición de usuario que atraviesa tres servicios en cadena de forma síncrona.
|
|
74
74
|
* Un consumidor de eventos sin control de duplicados.
|
|
75
|
-
* Un contexto publica hacia fuera
|
|
75
|
+
* Un contexto publica hacia fuera sus eventos internos tal cual (`{ ...event }`), o un listener recibe la clase de evento de otro contexto importada de su código.
|
|
76
76
|
* "Shared Kernel" usado como excusa para compartir una librería de modelos que crece sin acuerdo [IDDD cap. 13].
|
|
77
77
|
* El código copia sin traducir el modelo de un proveedor.
|
|
78
78
|
|
|
@@ -104,7 +104,7 @@ La regla "solo se vende lo activo con precio web" es una decisión de traducció
|
|
|
104
104
|
|
|
105
105
|
```ts
|
|
106
106
|
// src/contexts/sales/infrastructure/catalog/CatalogHttpProductPricing.ts
|
|
107
|
-
import { InfrastructureException } from '@archi-code/ddd/
|
|
107
|
+
import { InfrastructureException } from '@archi-code/ddd/infrastructure';
|
|
108
108
|
import type { ProductPricing, SellableProduct } from '../../domain/catalog/ProductPricing';
|
|
109
109
|
import type { Sku } from '../../domain/order/OrderLine';
|
|
110
110
|
import { CatalogTranslator } from './CatalogTranslator';
|
|
@@ -166,15 +166,14 @@ export class CatalogHttpProductPricing implements ProductPricing {
|
|
|
166
166
|
Decisiones visibles:
|
|
167
167
|
* Timeout explícito: la red falla y hay latencia [IDDD cap. 13].
|
|
168
168
|
* Validación del contrato al entrar: si Catálogo rompe el esquema, el error se detecta en el borde y no dentro del modelo. Un valor inválido de Catálogo no llega al usuario como `ValidationException` de Ventas: el adaptador la envuelve en `CatalogContractViolation` con la original como `cause`.
|
|
169
|
-
* Errores con nombre y código propios (`InfrastructureException` con `CAT001`, `CAT002`).
|
|
169
|
+
* Errores con nombre y código propios (`InfrastructureException` con `CAT001`, `CAT002`). `ExceptionClassifier.classify(error)` clasifica cualquier error de infraestructura como `ExceptionKind.Internal` (500 en HTTP); para avisar de que Catálogo no está disponible, el punto de entrada traduce su código (`CAT001` → 503) antes de mirar el tipo (con 5xx el cuerpo lleva un mensaje genérico, nunca el del error).
|
|
170
170
|
|
|
171
171
|
### 5. Uso desde la capa de aplicación
|
|
172
172
|
|
|
173
173
|
```ts
|
|
174
174
|
// src/contexts/sales/application/place-order/PlaceOrder.ts
|
|
175
|
-
import { ValueObjects } from '@archi-code/ddd/domain';
|
|
176
175
|
import type { AggregateSaver } from '@archi-code/ddd/domain';
|
|
177
|
-
import { UseCase } from '@archi-code/ddd/application';
|
|
176
|
+
import { UseCase, ValueObjectBuilder } from '@archi-code/ddd/application';
|
|
178
177
|
import type { Clock, EventBus } from '@archi-code/ddd/application';
|
|
179
178
|
import type { ProductPricing } from '../../domain/catalog/ProductPricing';
|
|
180
179
|
import { CustomerId } from '../../domain/customer/CustomerId';
|
|
@@ -207,12 +206,12 @@ export class PlaceOrder extends UseCase<PlaceOrderInput, PlaceOrderCommand, stri
|
|
|
207
206
|
}
|
|
208
207
|
|
|
209
208
|
protected toCommand(input: PlaceOrderInput): PlaceOrderCommand {
|
|
210
|
-
return
|
|
209
|
+
return ValueObjectBuilder.build({
|
|
211
210
|
orderId: () => OrderId.generate(),
|
|
212
211
|
customerId: () => new CustomerId(input.customerId),
|
|
213
212
|
items: () =>
|
|
214
|
-
|
|
215
|
-
|
|
213
|
+
ValueObjectBuilder.buildList(input.items, (item) =>
|
|
214
|
+
ValueObjectBuilder.build({
|
|
216
215
|
sku: () => new Sku(item.sku),
|
|
217
216
|
quantity: () => new Quantity(item.quantity),
|
|
218
217
|
}),
|
|
@@ -246,16 +245,16 @@ export class PlaceOrder extends UseCase<PlaceOrderInput, PlaceOrderCommand, stri
|
|
|
246
245
|
}
|
|
247
246
|
```
|
|
248
247
|
|
|
249
|
-
El precio se copia en la línea del pedido: Ventas guarda el estado remoto **mínimo** que necesita y deja de depender de Catálogo para ese pedido [IDDD cap. 3]. La consulta a Catálogo va antes de crear y guardar el pedido; como la transacción solo existe dentro del `save` del repositorio, nunca queda una abierta mientras se espera a la red `[Fuera de los libros: convención de @archi-code/ddd]`. `ProductNotSellable` es una `DomainException` de Ventas con código propio (`ORD003`), que `
|
|
248
|
+
El precio se copia en la línea del pedido: Ventas guarda el estado remoto **mínimo** que necesita y deja de depender de Catálogo para ese pedido [IDDD cap. 3]. La consulta a Catálogo va antes de crear y guardar el pedido; como la transacción solo existe dentro del `save` del repositorio, nunca queda una abierta mientras se espera a la red `[Fuera de los libros: convención de @archi-code/ddd]`. `ProductNotSellable` es una `DomainException` de Ventas con código propio (`ORD003`), que `ExceptionClassifier` clasifica como `ExceptionKind.Invalid` (422 en HTTP).
|
|
250
249
|
|
|
251
250
|
## ACL con un webhook entrante
|
|
252
251
|
|
|
253
|
-
Mismo patrón con un producto externo que llama a este contexto: el punto de entrada es el adaptador, la forma del mensaje es el contrato y la traducción produce la entrada de un caso de uso en el lenguaje local. Vive en `interfaces/` (como cualquier controlador) y solo conoce casos de uso y `
|
|
252
|
+
Mismo patrón con un producto externo que llama a este contexto: el punto de entrada es el adaptador, la forma del mensaje es el contrato y la traducción produce la entrada de un caso de uso en el lenguaje local. Vive en `interfaces/` (como cualquier controlador) y solo conoce casos de uso y `ExceptionClassifier` de `@archi-code/ddd/infrastructure` [IDDD cap. 4]. Traduce cada `ExceptionKind` a lo que entiende el emisor: lo que merece reintento (un conflicto, un error interno) responde 5xx para que la pasarela vuelva a enviarlo, y lo que no cambiaría al reintentar responde 4xx `[Fuera de los libros]`. El router ya ha comprobado la firma del emisor y la forma del cuerpo antes de llamarlo `[Fuera de los libros]`.
|
|
254
253
|
|
|
255
254
|
```ts
|
|
256
255
|
// src/contexts/payments/interfaces/http/PaymentGatewayWebhook.ts
|
|
257
|
-
import {
|
|
258
|
-
import type {
|
|
256
|
+
import { ExceptionClassifier, ExceptionKind } from '@archi-code/ddd/infrastructure';
|
|
257
|
+
import type { Logger } from '@archi-code/ddd/application';
|
|
259
258
|
import type { RecordPaymentAuthorization } from '../../application/record-payment-authorization/RecordPaymentAuthorization';
|
|
260
259
|
import type { RecordPaymentRejection } from '../../application/record-payment-rejection/RecordPaymentRejection';
|
|
261
260
|
|
|
@@ -268,14 +267,26 @@ export interface GatewayEvent {
|
|
|
268
267
|
metadata: { paymentId: string };
|
|
269
268
|
}
|
|
270
269
|
|
|
270
|
+
function statusOf(kind: ExceptionKind): number {
|
|
271
|
+
switch (kind) {
|
|
272
|
+
case ExceptionKind.Conflict:
|
|
273
|
+
case ExceptionKind.Internal:
|
|
274
|
+
return 503;
|
|
275
|
+
case ExceptionKind.NotFound:
|
|
276
|
+
return 404;
|
|
277
|
+
default:
|
|
278
|
+
return 422;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
271
282
|
export class PaymentGatewayWebhook {
|
|
272
283
|
constructor(
|
|
273
284
|
private readonly recordAuthorization: RecordPaymentAuthorization,
|
|
274
285
|
private readonly recordRejection: RecordPaymentRejection,
|
|
275
|
-
private readonly
|
|
286
|
+
private readonly logger: Logger,
|
|
276
287
|
) {}
|
|
277
288
|
|
|
278
|
-
async receive(event: GatewayEvent): Promise<{ status: number
|
|
289
|
+
async receive(event: GatewayEvent): Promise<{ status: number }> {
|
|
279
290
|
try {
|
|
280
291
|
switch (event.type) {
|
|
281
292
|
case 'charge.authorized':
|
|
@@ -294,7 +305,11 @@ export class PaymentGatewayWebhook {
|
|
|
294
305
|
}
|
|
295
306
|
return { status: 204 };
|
|
296
307
|
} catch (error) {
|
|
297
|
-
|
|
308
|
+
const kind = ExceptionClassifier.classify(error);
|
|
309
|
+
if (kind === ExceptionKind.Internal) {
|
|
310
|
+
this.logger.error('payment_webhook_failed', { type: event.type, error });
|
|
311
|
+
}
|
|
312
|
+
return { status: statusOf(kind) };
|
|
298
313
|
}
|
|
299
314
|
}
|
|
300
315
|
}
|
|
@@ -315,11 +330,11 @@ export class StockReserved extends EventBase {
|
|
|
315
330
|
static readonly eventName = 'warehouse.stock.StockReserved';
|
|
316
331
|
|
|
317
332
|
constructor(
|
|
318
|
-
reservationId: string,
|
|
333
|
+
readonly reservationId: string,
|
|
319
334
|
readonly orderId: string,
|
|
320
335
|
metadata?: EventMetadata,
|
|
321
336
|
) {
|
|
322
|
-
super(
|
|
337
|
+
super(metadata);
|
|
323
338
|
}
|
|
324
339
|
}
|
|
325
340
|
```
|
|
@@ -339,7 +354,7 @@ export class RegisterStockReservationOnStockReserved extends EventListener<Stock
|
|
|
339
354
|
async handle(event: StockReserved): Promise<void> {
|
|
340
355
|
await this.registerStockReservation.execute({
|
|
341
356
|
orderId: event.orderId,
|
|
342
|
-
reservationId: event.
|
|
357
|
+
reservationId: event.reservationId,
|
|
343
358
|
});
|
|
344
359
|
}
|
|
345
360
|
}
|
|
@@ -351,7 +366,7 @@ import { DomainEventBus, EventRegistry } from '@archi-code/ddd/infrastructure';
|
|
|
351
366
|
|
|
352
367
|
const warehouseEvents = new EventRegistry<StockReserved>().register(
|
|
353
368
|
StockReserved,
|
|
354
|
-
(json) => new StockReserved(json.
|
|
369
|
+
(json) => new StockReserved(json.reservationId as string, json.orderId as string, json),
|
|
355
370
|
);
|
|
356
371
|
const warehouseBus = new DomainEventBus<StockReserved>(
|
|
357
372
|
new RabbitMqEventChannel(amqpChannel, 'warehouse.published'),
|
|
@@ -40,9 +40,8 @@ El evento de Almacén llega por un `EventChannel` a un listener (`RegisterStockR
|
|
|
40
40
|
|
|
41
41
|
```ts
|
|
42
42
|
// src/contexts/sales/application/register-stock-reservation/RegisterStockReservation.ts
|
|
43
|
-
import { ValueObjects } from '@archi-code/ddd/domain';
|
|
44
43
|
import type { AggregateFinder, AggregateSaver } from '@archi-code/ddd/domain';
|
|
45
|
-
import { UseCase } from '@archi-code/ddd/application';
|
|
44
|
+
import { UseCase, ValueObjectBuilder } from '@archi-code/ddd/application';
|
|
46
45
|
import type { Clock, EventBus } from '@archi-code/ddd/application';
|
|
47
46
|
import { Order, OrderEvent, OrderId, ReservationId } from '../../domain/order/Order';
|
|
48
47
|
|
|
@@ -70,7 +69,7 @@ export class RegisterStockReservation extends UseCase<
|
|
|
70
69
|
}
|
|
71
70
|
|
|
72
71
|
protected toCommand(input: RegisterStockReservationInput): RegisterStockReservationCommand {
|
|
73
|
-
return
|
|
72
|
+
return ValueObjectBuilder.build({
|
|
74
73
|
orderId: () => new OrderId(input.orderId),
|
|
75
74
|
reservationId: () => new ReservationId(input.reservationId),
|
|
76
75
|
});
|
|
@@ -109,14 +108,12 @@ Aplicar dos veces "reserva registrada" no cambia nada la segunda vez ni graba un
|
|
|
109
108
|
* Los eventos y notificaciones llevan versión. Si se diseñan con cuidado, muchos consumidores pueden leer cualquier versión como si fuera la 1 y no necesitan cambiar [IDDD cap. 13].
|
|
110
109
|
* Los value objects estables (identificadores) se pueden incluir en el evento; quedan congelados en la estructura [IDDD cap. 13]. Con el paquete, el payload lleva solo primitivos tomados de los value objects (`orderId: string`), también los identificadores `[Fuera de los libros: convención de @archi-code/ddd]`.
|
|
111
110
|
|
|
112
|
-
La notificación de IDDD envuelve el evento en `{ notificationId, typeName, version, occurredOn, event }` [IDDD cap. 13]. Con el paquete, el mensaje publicado es un `SerializedEvent` plano, con el nombre
|
|
111
|
+
La notificación de IDDD envuelve el evento en `{ notificationId, typeName, version, occurredOn, event }` [IDDD cap. 13]. Con el paquete, el mensaje publicado es un `SerializedEvent` plano, con el nombre del Published Language, que también identifica su versión `[Fuera de los libros: convención de @archi-code/ddd]`:
|
|
113
112
|
|
|
114
113
|
```json
|
|
115
114
|
{
|
|
116
115
|
"eventName": "sales.order.OrderPlaced",
|
|
117
|
-
"eventVersion": 1,
|
|
118
116
|
"eventId": "6f1c2a3b-8d4e-4f5a-9b6c-7d8e9f0a1b2c",
|
|
119
|
-
"aggregateId": "9b2f6c1e-4d3a-4f8b-a1c2-3e4d5f6a7b8c",
|
|
120
117
|
"occurredOn": "2026-10-18T10:15:00.000Z",
|
|
121
118
|
"orderId": "9b2f6c1e-4d3a-4f8b-a1c2-3e4d5f6a7b8c",
|
|
122
119
|
"customerId": "1c5e7a90-2b3d-4e6f-8a1b-2c3d4e5f6a7b",
|
|
@@ -127,7 +124,7 @@ La notificación de IDDD envuelve el evento en `{ notificationId, typeName, vers
|
|
|
127
124
|
}
|
|
128
125
|
```
|
|
129
126
|
|
|
130
|
-
`eventName` hace de `typeName
|
|
127
|
+
`eventName` hace de `typeName` y de `version`: el mensaje no lleva campo de versión, y una versión incompatible del contrato se publica con otro nombre (`sales.order.OrderPlacedV2`) mientras los consumidores migran. `eventId` es el identificador con el que el consumidor deduplica. No hay `notificationId` secuencial: si publicas un feed REST de notificaciones, la secuencia la asigna tu canal duradero (la fila del outbox).
|
|
131
128
|
|
|
132
129
|
El evento interno no sale tal cual. La traducción al Published Language vive en infraestructura, en el adaptador que habla con el broker: un `EventChannel` del proyecto (`ContractPublishingChannel`) que pasa cada `SerializedEvent` por `SalesContracts` (qué eventos salen y con qué contrato, uno por versión publicada, con `messageId` = `eventId`) y entrega todos al canal interno. El código está en `ddd-skills/ddd-eventos-dominio/references/publicacion-ts.md` ("Lenguaje publicado en el adaptador del broker"); los casos de uso siguen viendo solo `EventBus`, y un evento sin contrato es interno y no sale del contexto. Para no perder mensajes, la publicación al broker la hace el reenviador del outbox después del commit (ver "Publicar de forma fiable").
|
|
133
130
|
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
[Fuera de los libros: convención de @archi-code/ddd]
|
|
10
10
|
|
|
11
11
|
* El paquete no trae event store, repositorios con Event Sourcing, snapshots ni proyecciones con checkpoint: se construyen en la infraestructura del proyecto sobre sus clases, y solo si la tabla de decisión de esta guía lo justifica.
|
|
12
|
-
*
|
|
12
|
+
* `AggregateRoot` no lleva versión: la de concurrencia optimista de los repositorios de instantáneas la recuerda cada repositorio en un `WeakMap` y crece 1 por guardado, no por evento. Un agregado con Event Sourcing lleva su propia versión de stream (`streamVersion()`), se reconstruye con `fromHistory` a partir de los eventos que reconstruye `EventRegistry` y no usa `toPrimitives()`/`fromPrimitives()`.
|
|
13
13
|
* El contrato del stream (`load`/`append`) lo declara el dominio y no es `AggregateFinder`/`AggregateSaver`; su adaptador es responsable de la persistencia: anexa los eventos de un `append` en una sola escritura atómica con la versión esperada y lanza `ConcurrencyException` si otro escribió antes. La aplicación no abre transacciones; el reintento ante `ConcurrencyException` va en el `handle` del caso de uso, cada intento vuelve a cargar y vuelve a anexar.
|
|
14
14
|
* Una proyección es un `EventListener` con `@ListensTo(Event, { consumer })` que llama a un puerto de proyección (no a un caso de uso), idempotente por `eventId`; un `EventChannel` no guarda historial, así que reconstruirla exige un log de eventos propio (con Event Sourcing, el propio event store).
|
|
15
15
|
* Una consulta de lectura es una interfaz en `application/` implementada en infraestructura directamente sobre la base de datos.
|
|
@@ -82,7 +82,7 @@ En Event Sourcing los eventos sirven para persistir el agregado **y** para comun
|
|
|
82
82
|
* Eventos tipo `OrderUpdated` con el estado completo en el stream.
|
|
83
83
|
* Proyecciones que no se pueden reconstruir (dependen de estado externo no reproducible).
|
|
84
84
|
* Proyecciones no idempotentes (sin comprobar el `eventId` ya aplicado).
|
|
85
|
-
* Un agregado con Event Sourcing que usa la
|
|
85
|
+
* Un agregado con Event Sourcing que usa la versión de guardados de un repositorio de instantáneas como versión de stream o se guarda como instantánea con `toPrimitives()`.
|
|
86
86
|
* Agregados que consultan read models para proteger invariantes (las invariantes viven en el agregado).
|
|
87
87
|
* Datos personales en eventos sin estrategia de supresión.
|
|
88
88
|
* CQRS con dos bases de datos para un CRUD de cinco pantallas.
|