@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
|
@@ -11,10 +11,10 @@ Un caso de uso modifica un solo agregado y lo guarda una vez [IDDD cap. 10]. El
|
|
|
11
11
|
Lo que hace el adaptador del repositorio, y nadie más:
|
|
12
12
|
|
|
13
13
|
* Que cada `save` y cada `remove` sea atómico. En MongoDB, un documento por agregado: una sola escritura, atómica por sí misma. Si el agregado ocupa varias tablas SQL, el repositorio abre la transacción de la base de datos **dentro** de `save`/`remove`, con su propia conexión o sesión, y la confirma o la revierte antes de volver.
|
|
14
|
-
* La
|
|
14
|
+
* La versión de concurrencia optimista, que es solo suya: recuerda la de cada agregado que cargó o guardó en un `WeakMap<Order, number>` privado (`versions`; `0` si no lo conoce, es nuevo). Una versión almacenada distinta de la recordada (o un id ya ocupado al dar de alta) es `ConcurrencyException('Order', order.id.value, version)`.
|
|
15
15
|
* La conexión, la sesión y el pool.
|
|
16
16
|
* Envolver los errores del driver en `InternalErrorException` con `{ cause }`.
|
|
17
|
-
* `
|
|
17
|
+
* `this.versions.set(order, version + 1)` solo tras escribir con éxito y, al cargar, `Order.fromPrimitives(...)` seguido de `this.versions.set(order, row.version)`. El agregado no declara ni mapea la versión y el caso de uso nunca la ve.
|
|
18
18
|
|
|
19
19
|
## Varias tablas en PostgreSQL
|
|
20
20
|
|
|
@@ -55,7 +55,8 @@ En `infrastructure/persistence/postgres-order-repository.ts`:
|
|
|
55
55
|
|
|
56
56
|
```ts
|
|
57
57
|
import type { AggregateFinder, AggregateSaver, PrimitiveTypes } from '@archi-code/ddd/domain';
|
|
58
|
-
import { ConcurrencyException
|
|
58
|
+
import { ConcurrencyException } from '@archi-code/ddd/application';
|
|
59
|
+
import { InternalErrorException } from '@archi-code/ddd/infrastructure';
|
|
59
60
|
import { Order, OrderId } from '../../domain/order/order';
|
|
60
61
|
import type { PgPool } from './pg-pool';
|
|
61
62
|
|
|
@@ -68,6 +69,8 @@ interface OrderRow {
|
|
|
68
69
|
}
|
|
69
70
|
|
|
70
71
|
export class PostgresOrderRepository implements AggregateFinder<Order>, AggregateSaver<Order> {
|
|
72
|
+
private readonly versions = new WeakMap<Order, number>();
|
|
73
|
+
|
|
71
74
|
constructor(private readonly pool: PgPool) {}
|
|
72
75
|
|
|
73
76
|
async findById(id: OrderId): Promise<Order | null> {
|
|
@@ -87,27 +90,33 @@ export class PostgresOrderRepository implements AggregateFinder<Order>, Aggregat
|
|
|
87
90
|
[id.value],
|
|
88
91
|
);
|
|
89
92
|
const row = rows[0];
|
|
90
|
-
|
|
93
|
+
if (row === undefined) {
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
const order = Order.fromPrimitives({ ...row.data, lines: { items: row.items } });
|
|
97
|
+
this.versions.set(order, row.version);
|
|
98
|
+
return order;
|
|
91
99
|
}
|
|
92
100
|
|
|
93
101
|
async save(order: Order): Promise<void> {
|
|
102
|
+
const version = this.versions.get(order) ?? 0;
|
|
94
103
|
const { lines, ...data } = order.toPrimitives();
|
|
95
104
|
const client = await this.pool.connect();
|
|
96
105
|
let broken = false;
|
|
97
106
|
try {
|
|
98
107
|
await client.query('BEGIN');
|
|
99
108
|
const { rowCount } =
|
|
100
|
-
|
|
109
|
+
version === 0
|
|
101
110
|
? await client.query(
|
|
102
111
|
'INSERT INTO orders (id, version, customer_id, status, data) VALUES ($1, 1, $2, $3, $4) ON CONFLICT (id) DO NOTHING',
|
|
103
112
|
[data.id, data.customerId, data.status, data],
|
|
104
113
|
)
|
|
105
114
|
: await client.query(
|
|
106
115
|
'UPDATE orders SET version = version + 1, status = $3, data = $4 WHERE id = $1 AND version = $2',
|
|
107
|
-
[data.id,
|
|
116
|
+
[data.id, version, data.status, data],
|
|
108
117
|
);
|
|
109
118
|
if (rowCount === 0) {
|
|
110
|
-
throw new ConcurrencyException('Order', order.id.value,
|
|
119
|
+
throw new ConcurrencyException('Order', order.id.value, version);
|
|
111
120
|
}
|
|
112
121
|
await client.query('DELETE FROM order_lines WHERE order_id = $1', [data.id]);
|
|
113
122
|
for (const [position, line] of lines.items.entries()) {
|
|
@@ -128,14 +137,14 @@ export class PostgresOrderRepository implements AggregateFinder<Order>, Aggregat
|
|
|
128
137
|
} finally {
|
|
129
138
|
client.release(broken);
|
|
130
139
|
}
|
|
131
|
-
|
|
140
|
+
this.versions.set(order, version + 1);
|
|
132
141
|
}
|
|
133
142
|
}
|
|
134
143
|
```
|
|
135
144
|
|
|
136
145
|
* La transacción empieza y termina dentro de `save`: el caso de uso solo ve una escritura que se hace entera o no se hace.
|
|
137
146
|
* El `UPDATE` con `version = $2` bloquea la fila del pedido hasta el `COMMIT`; otro `save` del mismo pedido espera y después no casa ninguna fila, así que recibe `ConcurrencyException`.
|
|
138
|
-
* `
|
|
147
|
+
* `this.versions.set(order, version + 1)` se hace solo después del `COMMIT`: si la transacción se revierte, el repositorio sigue recordando la versión anterior y el caso de uso puede descartar el objeto y volver a cargarlo.
|
|
139
148
|
* Si el `ROLLBACK` falla (la conexión se cayó), se relanza el error del trabajo, no el del `ROLLBACK`, y `release(true)` destruye la conexión en vez de devolver al pool una con la transacción a medias.
|
|
140
149
|
* La lectura es una sola sentencia, así que ve el pedido y sus líneas en el mismo instante.
|
|
141
150
|
* Un `remove` sigue el mismo patrón: `DELETE FROM orders WHERE id = $1 AND version = $2` dentro de su propia transacción (las líneas caen con `ON DELETE CASCADE`).
|
|
@@ -152,7 +161,7 @@ Cuando un evento no puede perderse, el repositorio escribe el agregado y sus eve
|
|
|
152
161
|
for (const event of order.pullDomainEvents()) {
|
|
153
162
|
await client.query('INSERT INTO order_outbox (event_id, body) VALUES ($1, $2)', [
|
|
154
163
|
event.eventId,
|
|
155
|
-
event
|
|
164
|
+
{ ...event },
|
|
156
165
|
]);
|
|
157
166
|
}
|
|
158
167
|
```
|
|
@@ -164,11 +173,9 @@ El repositorio toma los eventos con `pullDomainEvents()`, así que el `publish`
|
|
|
164
173
|
El caso de uso carga, cambia, guarda y publica, sin abrir nada. Si el comando es seguro de repetir contra el estado recargado, la aplicación puede reintentar una o dos veces ante `ConcurrencyException`; si no, el conflicto llega al usuario como 409 [IDDD cap. 10; IDDD apéndice A]. Cada intento vuelve a cargar y vuelve a guardar dentro de `handle`, en un bucle con límite `[Fuera de los libros: convención de @archi-code/ddd]`. Un caso de uso que ejecuta un listener no reintenta por su cuenta: reintenta la reentrega del canal.
|
|
165
174
|
|
|
166
175
|
```ts
|
|
167
|
-
import { UseCase } from '@archi-code/ddd/application';
|
|
176
|
+
import { AggregateNotFoundException, ConcurrencyException, UseCase, ValueObjectBuilder } from '@archi-code/ddd/application';
|
|
168
177
|
import type { Clock, EventBus } from '@archi-code/ddd/application';
|
|
169
|
-
import { ValueObjects } from '@archi-code/ddd/domain';
|
|
170
178
|
import type { AggregateFinder, AggregateSaver } from '@archi-code/ddd/domain';
|
|
171
|
-
import { AggregateNotFoundException, ConcurrencyException } from '@archi-code/ddd/exceptions';
|
|
172
179
|
import { Order, OrderId, OrderLine } from '../../domain/order/order';
|
|
173
180
|
import type { OrderEvent } from '../../domain/order/order';
|
|
174
181
|
|
|
@@ -196,7 +203,7 @@ export class AddOrderLine extends UseCase<AddOrderLineInput, AddOrderLineCommand
|
|
|
196
203
|
}
|
|
197
204
|
|
|
198
205
|
protected toCommand(input: AddOrderLineInput): AddOrderLineCommand {
|
|
199
|
-
return
|
|
206
|
+
return ValueObjectBuilder.build({
|
|
200
207
|
orderId: () => new OrderId(input.orderId),
|
|
201
208
|
line: () =>
|
|
202
209
|
OrderLine.fromPrimitives({
|
|
@@ -8,10 +8,10 @@
|
|
|
8
8
|
|
|
9
9
|
[Fuera de los libros: convención de @archi-code/ddd]
|
|
10
10
|
|
|
11
|
-
* Un caso de uso extiende `UseCase<Input, Command, Output>`: un `Input` de primitivos, un `Command` de value objects que construye `toCommand` con `
|
|
11
|
+
* Un caso de uso extiende `UseCase<Input, Command, Output>`: un `Input` de primitivos, un `Command` de value objects que construye `toCommand` con `ValueObjectBuilder.build` (informa de todos los campos a la vez), y `handle`, sin ayudas de la clase base, carga, cambia, guarda (`await this.orders.save(order)`) y publica (`await this.events.publish(order.pullDomainEvents())`). No hay puerto de transacciones: el caso de uso recibe solo los roles de repositorio que usa (`AggregateFinder`, `AggregateSaver`, `AggregateRemover`), `EventBus`, `Clock` y sus puertos propios, y el repositorio garantiza que cada `save` o `remove` es una escritura atómica. Una acción de negocio por caso de uso, con nombre de verbo del negocio (`PlaceOrder`, `AddOrderLine`).
|
|
12
12
|
* Un caso de uso nunca importa ni llama a otro: las acciones se encadenan con eventos y listeners `<Acción>On<Evento>` decorados con `@ListensTo`, que solo mapean el evento a la entrada de un caso de uso. No hay bus de comandos: el punto de entrada llama a `execute(input)`.
|
|
13
13
|
* Los servicios de dominio son clases sin base en `domain/` que reciben agregados y value objects, y pueden leer con un contrato `AggregateFinder` o un puerto de dominio; nunca guardan ni publican, y no tocan la persistencia.
|
|
14
|
-
* Errores: `AggregateNotFoundException` (404) y `NotAllowedException` (403) en el caso de uso, `DomainException` con código propio en el dominio, y
|
|
14
|
+
* Errores: `AggregateNotFoundException` (404) y `NotAllowedException` (403) en el caso de uso, `DomainException` con código propio en el dominio, y en los puntos de entrada `ExceptionClassifier.classify(error)`, cuyo `ExceptionKind` cada protocolo traduce a sus códigos; `execute` envuelve cualquier error ajeno al paquete en `UnexpectedErrorException` con `cause`.
|
|
15
15
|
* Un `ConcurrencyException` se reintenta, si el comando es seguro de repetir, con un bucle en `handle` con un límite de intentos en el que cada intento vuelve a cargar el agregado y vuelve a guardarlo. Un caso de uso que ejecuta un listener no reintenta por sí mismo: reintenta la reentrega del canal.
|
|
16
16
|
* Un caso de uso que ejecuta un listener para registrar un hecho no lanza si falta el agregado: retorna, para que el canal no lo reentregue sin fin.
|
|
17
17
|
|
|
@@ -5,11 +5,9 @@
|
|
|
5
5
|
Un caso de uso extiende `UseCase<Input, Command, Output>` de `@archi-code/ddd/application`. `execute(input)` llama a `toCommand(input)` y después a `handle(command)`; no hay que escribir `execute`.
|
|
6
6
|
|
|
7
7
|
```ts
|
|
8
|
-
import { ValueObjects } from '@archi-code/ddd/domain';
|
|
9
8
|
import type { AggregateFinder, AggregateSaver } from '@archi-code/ddd/domain';
|
|
10
|
-
import { UseCase } from '@archi-code/ddd/application';
|
|
9
|
+
import { AggregateNotFoundException, NotAllowedException, UseCase, ValueObjectBuilder } from '@archi-code/ddd/application';
|
|
11
10
|
import type { Clock, EventBus } from '@archi-code/ddd/application';
|
|
12
|
-
import { AggregateNotFoundException, NotAllowedException } from '@archi-code/ddd/exceptions';
|
|
13
11
|
|
|
14
12
|
export interface SalesPermissions {
|
|
15
13
|
canCancelOrders(requester: CustomerId): Promise<boolean>;
|
|
@@ -18,7 +16,7 @@ export interface SalesPermissions {
|
|
|
18
16
|
export interface CancelOrderInput {
|
|
19
17
|
orderId: string;
|
|
20
18
|
requesterId: string;
|
|
21
|
-
reason: string;
|
|
19
|
+
reason: string | null;
|
|
22
20
|
}
|
|
23
21
|
|
|
24
22
|
interface CancelOrderCommand {
|
|
@@ -40,10 +38,10 @@ export class CancelOrder extends UseCase<CancelOrderInput, CancelOrderCommand, v
|
|
|
40
38
|
}
|
|
41
39
|
|
|
42
40
|
protected toCommand(input: CancelOrderInput): CancelOrderCommand {
|
|
43
|
-
return
|
|
41
|
+
return ValueObjectBuilder.build({
|
|
44
42
|
orderId: () => new OrderId(input.orderId),
|
|
45
43
|
requester: () => new CustomerId(input.requesterId),
|
|
46
|
-
reason: () => new CancellationReason(input.reason),
|
|
44
|
+
reason: () => new CancellationReason(input.reason as string),
|
|
47
45
|
});
|
|
48
46
|
}
|
|
49
47
|
|
|
@@ -72,7 +70,7 @@ Autorizar, cargar, invocar el dominio, guardar y publicar, y ninguna regla de ne
|
|
|
72
70
|
## Entrada
|
|
73
71
|
|
|
74
72
|
* Primitivos o DTO o comando [PHP: Application, citando a Vernon]. Con el paquete, dos tipos por caso de uso `[Fuera de los libros: convención de @archi-code/ddd]`: el `Input`, plano y de primitivos (se puede serializar y encolar si un día hace falta [PHP: Application]), y el `Command`, de value objects, que construye `toCommand`.
|
|
75
|
-
* La conversión a value objects ocurre en `toCommand` con `
|
|
73
|
+
* La conversión a value objects ocurre en `toCommand` con `ValueObjectBuilder.build`: si algún campo no es válido lanza una sola `ValidationException` con el `field` y el `code` de cada problema (`ValueObjectBuilder.buildList` nombra los elementos de una lista por posición, `lines.1.quantity`), y `handle` no llega a ejecutarse.
|
|
76
74
|
* El adaptador de entrada solo comprueba la forma de la petición (que un campo sea texto); las reglas de cada valor son de los value objects.
|
|
77
75
|
* Nunca recibas el agregado desde el controlador.
|
|
78
76
|
|
|
@@ -93,7 +91,7 @@ Para pantallas, mejor una consulta de lectura aparte que devuelve un DTO sin pas
|
|
|
93
91
|
|
|
94
92
|
## Command bus
|
|
95
93
|
|
|
96
|
-
*DDD in PHP* presenta el bus de comandos (Tactician) para enlazar cada comando con su manejador; los objetos request se parecen ya a comandos [PHP: Application]. El paquete no trae bus de comandos `[Fuera de los libros: convención de @archi-code/ddd]`: el composition root construye cada caso de uso y se lo pasa al punto de entrada, que llama a `execute(input)`. Lo común a todos los casos de uso ya está en `UseCase.execute` (dejar pasar las excepciones del paquete y envolver cualquier otro error en `
|
|
94
|
+
*DDD in PHP* presenta el bus de comandos (Tactician) para enlazar cada comando con su manejador; los objetos request se parecen ya a comandos [PHP: Application]. El paquete no trae bus de comandos `[Fuera de los libros: convención de @archi-code/ddd]`: el composition root construye cada caso de uso y se lo pasa al punto de entrada, que llama a `execute(input)`. Lo común a todos los casos de uso ya está en `UseCase.execute` (dejar pasar las excepciones del paquete y envolver cualquier otro error en `UnexpectedErrorException` con `cause`), y la atomicidad de cada escritura es del repositorio.
|
|
97
95
|
|
|
98
96
|
## Eventos en la capa de aplicación
|
|
99
97
|
|
|
@@ -105,7 +103,8 @@ Para pantallas, mejor una consulta de lectura aparte que devuelve un DTO sin pas
|
|
|
105
103
|
Cada repositorio comprueba la versión del agregado y lanza `ConcurrencyException` (409) si otro lo cambió después de cargarlo. Si el comando es seguro de repetir sobre el nuevo estado, el caso de uso puede reintentarlo [IDDD cap. 10; IDDD apéndice A]: un bucle en `handle` en el que cada intento vuelve a cargar el agregado y vuelve a guardarlo; pocos intentos y después se relanza. Un caso de uso que ejecuta un listener no reintenta por sí mismo: reintenta la reentrega del canal.
|
|
106
104
|
|
|
107
105
|
```ts
|
|
108
|
-
import {
|
|
106
|
+
import { DomainException } from '@archi-code/ddd/domain';
|
|
107
|
+
import { ConcurrencyException } from '@archi-code/ddd/application';
|
|
109
108
|
|
|
110
109
|
export interface ProductPricing {
|
|
111
110
|
priceForSale(sku: Sku): Promise<Money | null>;
|
|
@@ -130,7 +129,7 @@ export class AddOrderLine extends UseCase<AddOrderLineInput, AddOrderLineCommand
|
|
|
130
129
|
}
|
|
131
130
|
|
|
132
131
|
protected toCommand(input: AddOrderLineInput): AddOrderLineCommand {
|
|
133
|
-
return
|
|
132
|
+
return ValueObjectBuilder.build({
|
|
134
133
|
orderId: () => new OrderId(input.orderId),
|
|
135
134
|
sku: () => new Sku(input.sku),
|
|
136
135
|
quantity: () => new Quantity(input.quantity),
|
|
@@ -167,13 +166,14 @@ export class AddOrderLine extends UseCase<AddOrderLineInput, AddOrderLineCommand
|
|
|
167
166
|
|
|
168
167
|
## Errores
|
|
169
168
|
|
|
170
|
-
* El dominio lanza `DomainException` con nombre y código propio (`class OrderCannotBeCancelled extends DomainException`, `super(message, ['ORD002'])`); los value objects y `
|
|
169
|
+
* El dominio lanza `DomainException` con nombre y código propio (`class OrderCannotBeCancelled extends DomainException`, `super(message, ['ORD002'])`); los value objects y `ValueObjectBuilder.build` lanzan `ValidationException`.
|
|
171
170
|
* La aplicación añade las suyas: `AggregateNotFoundException('Order', id)` cuando `findById` devuelve `null`, `NotAllowedException(message)` cuando el solicitante no puede ejecutar la acción, `ApplicationException` para otros errores de aplicación. Al traducir otro error, siempre con `{ cause: error }`.
|
|
172
|
-
* Los errores se dejan propagar: no se capturan en `handle` para relanzarlos; `execute` deja pasar las excepciones del paquete y envuelve el resto en `
|
|
173
|
-
* El punto de entrada los
|
|
171
|
+
* Los errores se dejan propagar: no se capturan en `handle` para relanzarlos; `execute` deja pasar las excepciones del paquete y envuelve el resto en `UnexpectedErrorException` (de `@archi-code/ddd/application`).
|
|
172
|
+
* El punto de entrada los clasifica con `ExceptionClassifier.classify(error)` de `@archi-code/ddd/infrastructure`, que no sabe de ningún protocolo: `NotFound` (`AggregateNotFoundException`), `NotAllowed` (`NotAllowedException`), `Conflict` (`ConcurrencyException`), `Malformed` (`TypePrimitiveException`, `RequiredValueException`), `Invalid` (`ValidationException` y cualquier otra `DomainException`), `Rejected` (otros errores de aplicación) e `Internal` (`UnexpectedErrorException`, `InvalidEventDefinitionException` y lo demás). Cada protocolo traduce el `ExceptionKind` a sus códigos (en HTTP: 404, 403, 409, 400, 422, 400 y 500), usa `error.code` para los códigos propios (`ORD001` → 409), oculta el mensaje de `Internal`, devuelve los `errors` de una `ValidationException` y registra él mismo los fallos internos `[Fuera de los libros: convención de @archi-code/ddd]`. Un controlador HTTP usa el `toHttpError` del proyecto (en `interfaces/http/`; completo en `ddd-skills/ddd-arquitectura/references/estructura-hexagonal-ts.md`):
|
|
174
173
|
|
|
175
174
|
```ts
|
|
176
|
-
import type {
|
|
175
|
+
import type { Logger } from '@archi-code/ddd/application';
|
|
176
|
+
import { toHttpError } from './httpError';
|
|
177
177
|
|
|
178
178
|
interface HttpRequest {
|
|
179
179
|
params: { orderId: string };
|
|
@@ -189,7 +189,7 @@ interface HttpResponse {
|
|
|
189
189
|
export class CancelOrderController {
|
|
190
190
|
constructor(
|
|
191
191
|
private readonly cancelOrder: CancelOrder,
|
|
192
|
-
private readonly
|
|
192
|
+
private readonly logger: Logger,
|
|
193
193
|
) {}
|
|
194
194
|
|
|
195
195
|
async cancel(request: HttpRequest): Promise<HttpResponse> {
|
|
@@ -197,11 +197,11 @@ export class CancelOrderController {
|
|
|
197
197
|
await this.cancelOrder.execute({
|
|
198
198
|
orderId: request.params.orderId,
|
|
199
199
|
requesterId: request.auth.customerId,
|
|
200
|
-
reason: typeof request.body.reason === 'string' ? request.body.reason :
|
|
200
|
+
reason: typeof request.body.reason === 'string' ? request.body.reason : null,
|
|
201
201
|
});
|
|
202
202
|
return { status: 204 };
|
|
203
203
|
} catch (error) {
|
|
204
|
-
return this.
|
|
204
|
+
return toHttpError(error, this.logger);
|
|
205
205
|
}
|
|
206
206
|
}
|
|
207
207
|
}
|
|
@@ -215,7 +215,7 @@ Con adaptadores en memoria: `InMemoryOrderRepository`, que el proyecto escribe e
|
|
|
215
215
|
|
|
216
216
|
```ts
|
|
217
217
|
import { describe, expect, it } from 'vitest';
|
|
218
|
-
import { NotAllowedException } from '@archi-code/ddd/
|
|
218
|
+
import { NotAllowedException } from '@archi-code/ddd/application';
|
|
219
219
|
import {
|
|
220
220
|
DomainEventBus,
|
|
221
221
|
EventRegistry,
|
|
@@ -245,7 +245,7 @@ describe('Cancelling an order', () => {
|
|
|
245
245
|
await cancelOrder.execute({ orderId, requesterId: laura, reason: 'Wrong size' });
|
|
246
246
|
|
|
247
247
|
expect(channel.sent).toMatchObject([
|
|
248
|
-
{ eventName: 'OrderCancelled',
|
|
248
|
+
{ eventName: 'OrderCancelled', orderId, reason: 'Wrong size' },
|
|
249
249
|
]);
|
|
250
250
|
});
|
|
251
251
|
|
|
@@ -33,9 +33,8 @@ total(): Money;
|
|
|
33
33
|
Lo que añade Ventas en esta guía:
|
|
34
34
|
|
|
35
35
|
```ts
|
|
36
|
-
import { AggregateRoot, BooleanType, Required, Specification, StringType } from '@archi-code/ddd/domain';
|
|
36
|
+
import { AggregateRoot, BooleanType, DomainException, Required, Specification, StringType } from '@archi-code/ddd/domain';
|
|
37
37
|
import type { PrimitiveTypes } from '@archi-code/ddd/domain';
|
|
38
|
-
import { DomainException } from '@archi-code/ddd/exceptions';
|
|
39
38
|
|
|
40
39
|
export class OrderAbove extends Specification<Order> {
|
|
41
40
|
constructor(private readonly minimum: Money) {
|
|
@@ -123,11 +122,9 @@ export class OrderCancellation {
|
|
|
123
122
|
`ProductPricing` y `ProductNotSellable` son los de `ddd-skills/ddd-servicios-capa-aplicacion/references/capa-aplicacion-ts.md`.
|
|
124
123
|
|
|
125
124
|
```ts
|
|
126
|
-
import { ValueObjects } from '@archi-code/ddd/domain';
|
|
127
125
|
import type { AggregateFinder, AggregateSaver } from '@archi-code/ddd/domain';
|
|
128
|
-
import { UseCase } from '@archi-code/ddd/application';
|
|
126
|
+
import { AggregateNotFoundException, UseCase, ValueObjectBuilder } from '@archi-code/ddd/application';
|
|
129
127
|
import type { Clock, EventBus } from '@archi-code/ddd/application';
|
|
130
|
-
import { AggregateNotFoundException } from '@archi-code/ddd/exceptions';
|
|
131
128
|
|
|
132
129
|
export interface PlaceOrderInput {
|
|
133
130
|
customerId: string;
|
|
@@ -153,11 +150,11 @@ export class PlaceOrder extends UseCase<PlaceOrderInput, PlaceOrderCommand, stri
|
|
|
153
150
|
}
|
|
154
151
|
|
|
155
152
|
protected toCommand(input: PlaceOrderInput): PlaceOrderCommand {
|
|
156
|
-
return
|
|
153
|
+
return ValueObjectBuilder.build({
|
|
157
154
|
customerId: () => new CustomerId(input.customerId),
|
|
158
155
|
lines: () =>
|
|
159
|
-
|
|
160
|
-
|
|
156
|
+
ValueObjectBuilder.buildList(input.lines, (line) =>
|
|
157
|
+
ValueObjectBuilder.build({
|
|
161
158
|
sku: () => new Sku(line.sku),
|
|
162
159
|
quantity: () => new Quantity(line.quantity),
|
|
163
160
|
}),
|
|
@@ -206,20 +203,20 @@ Los gastos de envío se han dejado fuera de este caso de uso para mantenerlo ali
|
|
|
206
203
|
Una reacción a un evento es un listener `<Acción>On<Evento>`, decorado con `@ListensTo` y con un nombre de consumidor estable, que solo traduce el evento a la entrada de un caso de uso. Ese caso de uso registra un hecho que ya ocurrió: si el pedido no existe, retorna en vez de lanzar (el canal lo reentregaría sin fin), y `registerStockReservation` es idempotente, porque la entrega es al menos una vez.
|
|
207
204
|
|
|
208
205
|
```ts
|
|
209
|
-
import { EventBase
|
|
206
|
+
import { EventBase } from '@archi-code/ddd/domain';
|
|
210
207
|
import type { AggregateFinder, AggregateSaver, EventMetadata } from '@archi-code/ddd/domain';
|
|
211
|
-
import { EventListener, ListensTo, UseCase } from '@archi-code/ddd/application';
|
|
208
|
+
import { EventListener, ListensTo, UseCase, ValueObjectBuilder } from '@archi-code/ddd/application';
|
|
212
209
|
import type { Clock, EventBus } from '@archi-code/ddd/application';
|
|
213
210
|
|
|
214
211
|
export class StockReserved extends EventBase {
|
|
215
212
|
static readonly eventName = 'StockReserved';
|
|
216
213
|
|
|
217
214
|
constructor(
|
|
218
|
-
reservationId: string,
|
|
215
|
+
readonly reservationId: string,
|
|
219
216
|
readonly orderId: string,
|
|
220
217
|
metadata?: EventMetadata,
|
|
221
218
|
) {
|
|
222
|
-
super(
|
|
219
|
+
super(metadata);
|
|
223
220
|
}
|
|
224
221
|
}
|
|
225
222
|
|
|
@@ -247,7 +244,7 @@ export class RegisterReservation extends UseCase<
|
|
|
247
244
|
}
|
|
248
245
|
|
|
249
246
|
protected toCommand(input: RegisterReservationInput): RegisterReservationCommand {
|
|
250
|
-
return
|
|
247
|
+
return ValueObjectBuilder.build({
|
|
251
248
|
orderId: () => new OrderId(input.orderId),
|
|
252
249
|
reservationId: () => new ReservationId(input.reservationId),
|
|
253
250
|
});
|
|
@@ -273,7 +270,7 @@ export class RegisterReservationOnStockReserved extends EventListener<StockReser
|
|
|
273
270
|
async handle(event: StockReserved): Promise<void> {
|
|
274
271
|
await this.registerReservation.execute({
|
|
275
272
|
orderId: event.orderId,
|
|
276
|
-
reservationId: event.
|
|
273
|
+
reservationId: event.reservationId,
|
|
277
274
|
});
|
|
278
275
|
}
|
|
279
276
|
}
|
|
@@ -301,7 +298,7 @@ describe('Registering the stock reservation of an order', () => {
|
|
|
301
298
|
const orders = new InMemoryOrderRepository();
|
|
302
299
|
const registry = new EventRegistry<StockReserved>().register(
|
|
303
300
|
StockReserved,
|
|
304
|
-
(json) => new StockReserved(json.
|
|
301
|
+
(json) => new StockReserved(json.reservationId as string, json.orderId as string, json),
|
|
305
302
|
);
|
|
306
303
|
const channel = new InMemoryEventChannel();
|
|
307
304
|
const bus = new DomainEventBus<StockReserved>(channel, registry);
|
|
@@ -14,9 +14,8 @@ Ejemplo de IDDD: autenticar a un usuario solo si su tenant está activo. Ponerlo
|
|
|
14
14
|
El paquete no trae una clase base para servicios de dominio: son clases normales en `domain/`, con el nombre de la operación de negocio. Reciben agregados ya cargados y value objects, y devuelven un value object.
|
|
15
15
|
|
|
16
16
|
```ts
|
|
17
|
-
import { Composite, Required, StringType } from '@archi-code/ddd/domain';
|
|
17
|
+
import { Composite, DomainException, Required, StringType } from '@archi-code/ddd/domain';
|
|
18
18
|
import type { SatisfiedBy } from '@archi-code/ddd/domain';
|
|
19
|
-
import { DomainException } from '@archi-code/ddd/exceptions';
|
|
20
19
|
import { Validations } from '@archi-code/validation';
|
|
21
20
|
|
|
22
21
|
@Validations([{ validator: 'isIn', properties: { values: ['PENINSULA', 'BALEARES', 'CANARIAS'] } }])
|
|
@@ -79,7 +78,7 @@ export interface ExchangeRates {
|
|
|
79
78
|
```
|
|
80
79
|
|
|
81
80
|
```ts
|
|
82
|
-
import { InternalErrorException } from '@archi-code/ddd/
|
|
81
|
+
import { InternalErrorException } from '@archi-code/ddd/infrastructure';
|
|
83
82
|
|
|
84
83
|
interface EcbClient {
|
|
85
84
|
rate(from: string, to: string, day: string): Promise<number>;
|
|
@@ -104,11 +103,11 @@ export class EcbExchangeRates implements ExchangeRates {
|
|
|
104
103
|
|
|
105
104
|
Un servicio de dominio es buen sitio para validar que varios agregados sean coherentes entre sí [IDDD cap. 5]. Recibe los agregados que el caso de uso ya cargó y comprueba. Si detecta un problema, informa (error o evento); no corrige varios agregados a la vez: cada agregado se guarda en su propia escritura atómica.
|
|
106
105
|
|
|
107
|
-
Para informar de todos los problemas a la vez, reúne `ValidationIssue`s y lanza una sola `ValidationException` (
|
|
106
|
+
Para informar de todos los problemas a la vez, reúne `ValidationIssue`s y lanza una sola `ValidationException` (`ExceptionKind.Invalid`, 422 en HTTP, cada problema con su `field` y su `code`); si basta con uno, lanza una `DomainException` con nombre y código propio:
|
|
108
107
|
|
|
109
108
|
```ts
|
|
110
|
-
import { ValidationException } from '@archi-code/ddd/
|
|
111
|
-
import type { ValidationIssue } from '@archi-code/ddd/
|
|
109
|
+
import { ValidationException } from '@archi-code/ddd/domain';
|
|
110
|
+
import type { ValidationIssue } from '@archi-code/ddd/domain';
|
|
112
111
|
|
|
113
112
|
export class ShipmentCheck {
|
|
114
113
|
assertCanShip(order: Order, customer: Customer): void {
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
* Un value object es una subclase con nombre de un tipo del paquete (`class Email extends Required(StringType) {}`) con sus reglas en `@Validations` (de `@archi-code/validation`): se crea con `new` y su constructor lo deja siempre válido; no hay factorías `of()` ni `assertValid()`, y la normalización va en `filter`.
|
|
12
12
|
* Un valor de varios atributos (`Money`, `DateRange`, `OrderLine`, `OrderLines`) es `Composite({...})`, con las reglas que combinan campos como `custom`; un estado, un `EnumType`; un id, `IdType`, generado por el propio value object (`OrderId.generate()`), nunca por el repositorio.
|
|
13
13
|
* `Money` va en céntimos enteros (`Cents`, `Required(NumberType)` con `isInt`) en lugar de `bigint`: exacto hasta `Number.MAX_SAFE_INTEGER` (9.007.199.254.740.991 céntimos); el paquete no tiene tipo `bigint`.
|
|
14
|
-
* Los errores de una entrada se recogen con `
|
|
14
|
+
* Los errores de una entrada se recogen con `ValueObjectBuilder.build({...})` y `ValidationException.errors`, no con un manejador de notificación.
|
|
15
15
|
* Un value object es opcional por defecto (`value: T | null`) y `Required(XType)` lo hace obligatorio. Los libros crean siempre el valor con contenido y modelan lo opcional en el campo (`reservation: ReservationId | null`); con el paquete valen las dos formas.
|
|
16
16
|
* La identidad de una entidad o raíz siempre es un value object obligatorio (`IdType` o `Required(...)`): un id sin valor lanza `RequiredValueException`, así dos entidades nuevas nunca son iguales por un id vacío [IDDD cap. 5].
|
|
17
17
|
* Todo el estado de un agregado es value objects (también un booleano), cada campo guardado tiene un getter público con su nombre sin `_` y los valores derivados son métodos, nunca getters. La reconstitución es `static fromPrimitives(PrimitiveTypes<X>)` con su `toPrimitives()`, que el repositorio llama directamente y cuyo viaje de ida y vuelta prueba el spec del agregado (nada lo comprueba en ejecución). Los libros solo piden getters donde hacen falta y un mapper en infraestructura.
|
|
@@ -86,7 +86,7 @@ Recomendación por defecto: identidad generada por la aplicación, **temprana**
|
|
|
86
86
|
2. **Objeto completo:** reglas que combinan atributos (las de un valor de varios atributos, reglas `custom` de su `Composite`). IDDD sugiere un validador separado cuando esa validación cambia más que la entidad.
|
|
87
87
|
3. **Composición:** reglas sobre varios objetos o agregados. Un servicio de dominio que recibe lo cargado o lee lo necesario con `AggregateFinder`.
|
|
88
88
|
|
|
89
|
-
Detalle, cómo acumular todos los errores (`
|
|
89
|
+
Detalle, cómo acumular todos los errores (`ValueObjectBuilder.build` en lugar del patrón de notificación) y qué validar en la interfaz frente al dominio en `ddd-skills/ddd-value-objects-entidades/references/validacion.md`.
|
|
90
90
|
|
|
91
91
|
## Paso 5. Diseño flexible y Specification
|
|
92
92
|
|
|
@@ -94,7 +94,7 @@ Revisa el diseño con los patrones de diseño flexible (Supple Design): interfac
|
|
|
94
94
|
|
|
95
95
|
## Paso 6. Persistencia sin fugas
|
|
96
96
|
|
|
97
|
-
Cómo guardar value objects (columnas embebidas, JSON, tabla propia para colecciones) sin convertirlos en entidades del modelo: `ddd-skills/ddd-value-objects-entidades/references/persistencia-valores.md`. La regla: diseña el modelo de datos al servicio del modelo de dominio [IDDD cap. 6]. Con el paquete, el repositorio parte de `order.toPrimitives()` (y carga con `Order.fromPrimitives(
|
|
97
|
+
Cómo guardar value objects (columnas embebidas, JSON, tabla propia para colecciones) sin convertirlos en entidades del modelo: `ddd-skills/ddd-value-objects-entidades/references/persistencia-valores.md`. La regla: diseña el modelo de datos al servicio del modelo de dominio [IDDD cap. 6]. Con el paquete, el repositorio parte de `order.toPrimitives()` (y carga con `Order.fromPrimitives(...)`, recordando la versión de la fila en su propio `WeakMap`, nunca en el agregado) y decide en infraestructura cómo reparte los primitivos en columnas.
|
|
98
98
|
|
|
99
99
|
## Señales de alarma
|
|
100
100
|
|
|
@@ -43,7 +43,7 @@ const order = Order.place(OrderId.generate(), customerId, lines, shippingAddress
|
|
|
43
43
|
|
|
44
44
|
### Identidad sustituta
|
|
45
45
|
|
|
46
|
-
Si el ORM o la base exige su propia clave (un autoincremental), usa dos identidades: la del dominio y una sustituta para la persistencia, oculta al modelo y a sus clientes [IDDD cap. 5]. Con el paquete, la sustituta vive solo en el repositorio: se guarda junto a los primitivos (como la `version
|
|
46
|
+
Si el ORM o la base exige su propia clave (un autoincremental), usa dos identidades: la del dominio y una sustituta para la persistencia, oculta al modelo y a sus clientes [IDDD cap. 5]. Con el paquete, la sustituta vive solo en el repositorio: se guarda junto a los primitivos (como la `version`, que también es solo del repositorio), nunca dentro de `toPrimitives()` ni como campo del agregado.
|
|
47
47
|
|
|
48
48
|
### Estabilidad
|
|
49
49
|
|
|
@@ -51,13 +51,12 @@ La identidad no cambia durante la vida de la entidad. IDDD la protege con un set
|
|
|
51
51
|
|
|
52
52
|
## Patrón de implementación
|
|
53
53
|
|
|
54
|
-
No se escribe una clase base `Entity`: viene de `@archi-code/ddd/domain`. `Entity<Id>` solo guarda `readonly id` y compara por clase e id; `AggregateRoot<Id, Event>` la extiende y añade eventos (`record()`, `pullDomainEvents()`)
|
|
54
|
+
No se escribe una clase base `Entity`: viene de `@archi-code/ddd/domain`. `Entity<Id>` solo guarda `readonly id` y compara por clase e id; `AggregateRoot<Id, Event>` la extiende y añade solo los eventos (`record()`, `pullDomainEvents()`); la versión para concurrencia optimista no está en el agregado, la recuerda cada repositorio en un `WeakMap<Order, number>` `[Fuera de los libros: convención de @archi-code/ddd]`.
|
|
55
55
|
|
|
56
56
|
Una raíz de agregado extiende `AggregateRoot`. Una entidad hija (una parte con identidad propia dentro del agregado, que se busca por ese id) extiende `Entity`, guarda su id local y value objects, y sus métodos de negocio solo los llama su raíz [IDDD cap. 5 y cap. 10]:
|
|
57
57
|
|
|
58
58
|
```ts
|
|
59
|
-
import { BooleanType, Entity, NumberType, Required, StringType } from '@archi-code/ddd/domain';
|
|
60
|
-
import { DomainException } from '@archi-code/ddd/exceptions';
|
|
59
|
+
import { BooleanType, DomainException, Entity, NumberType, Required, StringType } from '@archi-code/ddd/domain';
|
|
61
60
|
|
|
62
61
|
export class SeatNumber extends Required(NumberType) {}
|
|
63
62
|
export class SeatLabel extends Required(StringType) {}
|
|
@@ -97,9 +96,8 @@ Separar dos caminos evita que la carga desde base de datos vuelva a disparar reg
|
|
|
97
96
|
|
|
98
97
|
```ts
|
|
99
98
|
import { Validations } from '@archi-code/validation';
|
|
100
|
-
import { AggregateRoot, EnumType, EventBase, Required, StringType } from '@archi-code/ddd/domain';
|
|
99
|
+
import { AggregateRoot, DomainException, EnumType, EventBase, Required, StringType } from '@archi-code/ddd/domain';
|
|
101
100
|
import type { EventMetadata, PrimitiveTypes } from '@archi-code/ddd/domain';
|
|
102
|
-
import { DomainException } from '@archi-code/ddd/exceptions';
|
|
103
101
|
|
|
104
102
|
export class CustomerName extends Required(StringType) {}
|
|
105
103
|
|
|
@@ -127,11 +125,11 @@ export class CustomerBlocked extends EventBase {
|
|
|
127
125
|
static readonly eventName = 'CustomerBlocked';
|
|
128
126
|
|
|
129
127
|
constructor(
|
|
130
|
-
customerId: string,
|
|
128
|
+
readonly customerId: string,
|
|
131
129
|
readonly reason: string,
|
|
132
130
|
metadata?: EventMetadata,
|
|
133
131
|
) {
|
|
134
|
-
super(
|
|
132
|
+
super(metadata);
|
|
135
133
|
}
|
|
136
134
|
}
|
|
137
135
|
|
|
@@ -209,7 +207,7 @@ Fíjate en:
|
|
|
209
207
|
* Ningún setter; cada cambio es un comportamiento con nombre y sus reglas. `block` es idempotente y registra `CustomerBlocked` con el reloj que recibe, nunca con `new Date()`.
|
|
210
208
|
* Todos los atributos son value objects; cambiar el correo es reemplazar un `Email`.
|
|
211
209
|
* Getters solo para lo que los clientes necesitan leer, dicen los libros. Con el paquete, cada campo que se guarda tiene un getter público con su nombre sin `_`, porque `toPrimitives()` los lee `[Fuera de los libros: convención de @archi-code/ddd]`. Los valores derivados son métodos (`isBlocked()`), nunca getters.
|
|
212
|
-
* El agregado no declara `version` ni la incluye en `toPrimitives()`: la
|
|
210
|
+
* El agregado no declara `version` ni la incluye en `toPrimitives()`: la versión es solo del repositorio, que la recuerda por agregado en un `WeakMap` y la guarda junto a los primitivos.
|
|
213
211
|
|
|
214
212
|
## Seguimiento de cambios
|
|
215
213
|
|
|
@@ -27,7 +27,7 @@ Antes de convertir un valor en entidad "porque tiene tabla", responde [IDDD cap.
|
|
|
27
27
|
## En TypeScript
|
|
28
28
|
|
|
29
29
|
`[Fuera de los libros: convención de @archi-code/ddd]`
|
|
30
|
-
* Nada de mappers escritos a mano ni decoradores de ORM en las clases de dominio. El agregado da sus datos planos con `toPrimitives()` y se rehace con `static fromPrimitives()`; el repositorio solo llama a `order.toPrimitives()` y `Order.fromPrimitives(
|
|
30
|
+
* Nada de mappers escritos a mano ni decoradores de ORM en las clases de dominio. El agregado da sus datos planos con `toPrimitives()` y se rehace con `static fromPrimitives()`; el repositorio solo llama a `order.toPrimitives()` y `Order.fromPrimitives(...)`, y la versión de concurrencia la recuerda él mismo en un `WeakMap<Order, number>`, nunca el agregado. Nada comprueba en ejecución que no se pierda ningún campo privado: lo prueba el spec del agregado con el viaje de ida y vuelta (`Order.fromPrimitives(order.toPrimitives()).toPrimitives()` igual a `order.toPrimitives()`).
|
|
31
31
|
* Los primitivos ya dan la forma de cada opción de la tabla: un value object simple es su valor, un `Composite` es un objeto (`shippingAddress: { street, city, country }`), una lista de composites es una lista de objetos (`lines: { items: [...] }`) y un `EnumType` es la opción del enum (`status: 'pending'`).
|
|
32
32
|
* Con un almacén de documentos (MongoDB), los primitivos se guardan tal cual. Con tablas, el repositorio decide cómo se reparten en columnas: el formato de la fila sigue en infraestructura y no deforma el modelo.
|
|
33
33
|
|
|
@@ -49,7 +49,7 @@ interface OrderRow {
|
|
|
49
49
|
version: number;
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
-
function rowOf(order: Order): OrderRow {
|
|
52
|
+
function rowOf(order: Order, version: number): OrderRow {
|
|
53
53
|
const primitives = order.toPrimitives();
|
|
54
54
|
return {
|
|
55
55
|
id: primitives.id,
|
|
@@ -61,7 +61,7 @@ function rowOf(order: Order): OrderRow {
|
|
|
61
61
|
status: primitives.status,
|
|
62
62
|
stock_reservation: primitives.stockReservation,
|
|
63
63
|
payment_authorization: primitives.paymentAuthorization,
|
|
64
|
-
version:
|
|
64
|
+
version: version + 1,
|
|
65
65
|
};
|
|
66
66
|
}
|
|
67
67
|
|
|
@@ -78,11 +78,11 @@ function orderOf(row: OrderRow): Order {
|
|
|
78
78
|
status: row.status as Status,
|
|
79
79
|
stockReservation: row.stock_reservation,
|
|
80
80
|
paymentAuthorization: row.payment_authorization,
|
|
81
|
-
})
|
|
81
|
+
});
|
|
82
82
|
}
|
|
83
83
|
```
|
|
84
84
|
|
|
85
|
-
`Order`, `ShippingAddress` y `Status` son los de `ddd-skills/ddd-agregados/references/implementacion-ts.md`. La dirección va en columnas de la fila del padre, las líneas y la autorización de pago en columnas JSON (`jsonb` en PostgreSQL) y el estado como su código. Con el driver `pg`, una columna `jsonb` se lee ya como objeto (la fecha de caducidad de la autorización llega como texto ISO, que `ExpiresAt` convierte), y al escribir el parámetro se pasa como `JSON.stringify(row.lines)` (un array sin convertir lo enviaría como array de PostgreSQL). La `version` se guarda junto a los primitivos, nunca dentro
|
|
85
|
+
`Order`, `ShippingAddress` y `Status` son los de `ddd-skills/ddd-agregados/references/implementacion-ts.md`. La dirección va en columnas de la fila del padre, las líneas y la autorización de pago en columnas JSON (`jsonb` en PostgreSQL) y el estado como su código. Con el driver `pg`, una columna `jsonb` se lee ya como objeto (la fecha de caducidad de la autorización llega como texto ISO, que `ExpiresAt` convierte), y al escribir el parámetro se pasa como `JSON.stringify(row.lines)` (un array sin convertir lo enviaría como array de PostgreSQL). La `version` se guarda junto a los primitivos, nunca dentro: `rowOf` recibe la que el repositorio recuerda para ese agregado en su `WeakMap` (`0` si es nuevo) y, tras cargar con `orderOf`, el repositorio anota `row.version` en ese `WeakMap`; la consulta con la condición de versión y cuándo se actualiza el `WeakMap` están en `ddd-skills/ddd-repositorios-persistencia/GUIA.md`.
|
|
86
86
|
|
|
87
87
|
* Al cargar, `fromPrimitives` rehace cada value object con su constructor, que valida: un dato corrupto falla al cargar (`ValidationException`) en vez de propagarse. No se repiten reglas de creación ni se registran eventos.
|
|
88
88
|
* Con Prisma o Drizzle, el esquema es de infraestructura y el repositorio sigue pasando por `order.toPrimitives()` y `Order.fromPrimitives()`.
|
|
@@ -24,15 +24,15 @@ Criterio práctico `[Fuera de los libros]`:
|
|
|
24
24
|
|
|
25
25
|
Para devolver todos los errores en vez de parar en el primero, IDDD y *DDD in PHP* usan un manejador de notificación que recibe los fallos [IDDD cap. 5; PHP: Entities]. Con el paquete no se escribe ese manejador `[Fuera de los libros: convención de @archi-code/ddd]`:
|
|
26
26
|
|
|
27
|
-
* **Entrada de un caso de uso:** `
|
|
27
|
+
* **Entrada de un caso de uso:** `ValueObjectBuilder.build({...})` ejecuta todas las factorías y lanza una sola `ValidationException` cuyos `errors` (`ValidationIssue`: `{ field, code, message }`) llevan el campo y el código de cada problema; `ValueObjectBuilder.buildList` nombra los elementos de una lista por su posición (`lines.1.quantity`).
|
|
28
28
|
|
|
29
29
|
```ts
|
|
30
|
-
import {
|
|
30
|
+
import { ValueObjectBuilder } from '@archi-code/ddd/application';
|
|
31
31
|
|
|
32
|
-
const command =
|
|
32
|
+
const command = ValueObjectBuilder.build({
|
|
33
33
|
customerId: () => new CustomerId(input.customerId),
|
|
34
34
|
email: () => new Email(input.email),
|
|
35
|
-
lines: () =>
|
|
35
|
+
lines: () => ValueObjectBuilder.buildList(input.lines, (line) => OrderLine.fromPrimitives(line)),
|
|
36
36
|
});
|
|
37
37
|
// input { customerId: 'x', email: 'ana', lines: [...] } → una ValidationException:
|
|
38
38
|
// errors: [
|
|
@@ -44,8 +44,8 @@ const command = ValueObjects.build({
|
|
|
44
44
|
* **Un objeto ya construido** (la ficha de un producto que se quiere publicar, en el contexto de Catálogo): un servicio de dominio recoge los `ValidationIssue` y lanza una sola `new ValidationException(issues)`.
|
|
45
45
|
|
|
46
46
|
```ts
|
|
47
|
-
import { ValidationException } from '@archi-code/ddd/
|
|
48
|
-
import type { ValidationIssue } from '@archi-code/ddd/
|
|
47
|
+
import { ValidationException } from '@archi-code/ddd/domain';
|
|
48
|
+
import type { ValidationIssue } from '@archi-code/ddd/domain';
|
|
49
49
|
|
|
50
50
|
export class ListingReadyToPublish {
|
|
51
51
|
check(listing: ProductListing): void {
|
|
@@ -80,11 +80,11 @@ Un agregado puede estar en un estado intermedio legítimo en el que no procede v
|
|
|
80
80
|
## Errores con nombre
|
|
81
81
|
|
|
82
82
|
`[Fuera de los libros]`
|
|
83
|
-
* Una clase de error por regla de negocio (`OrderMustHaveLines`, `CurrencyMismatch`), con un código estable para traducir a mensajes de usuario y a códigos HTTP en el adaptador. Con el paquete, una subclase de `DomainException` (de `@archi-code/ddd/
|
|
83
|
+
* Una clase de error por regla de negocio (`OrderMustHaveLines`, `CurrencyMismatch`), con un código estable para traducir a mensajes de usuario y a códigos HTTP en el adaptador. Con el paquete, una subclase de `DomainException` (de `@archi-code/ddd/domain`) con su código en el segundo argumento.
|
|
84
84
|
* No uses `Error('invalid')` en el dominio.
|
|
85
85
|
|
|
86
86
|
```ts
|
|
87
|
-
import { DomainException } from '@archi-code/ddd/
|
|
87
|
+
import { DomainException } from '@archi-code/ddd/domain';
|
|
88
88
|
|
|
89
89
|
export class OrderMustHaveLines extends DomainException {
|
|
90
90
|
constructor() {
|
|
@@ -93,4 +93,4 @@ export class OrderMustHaveLines extends DomainException {
|
|
|
93
93
|
}
|
|
94
94
|
```
|
|
95
95
|
|
|
96
|
-
`error.code` es `'ORD002'`. En la entrada, `
|
|
96
|
+
`error.code` es `'ORD002'`. En la entrada, `ExceptionClassifier.classify(error)` (de `@archi-code/ddd/infrastructure`) da el tipo de fallo sin tablas por excepción: `ExceptionKind.Invalid` para una `ValidationException` o cualquier `DomainException` (422 en HTTP) y `ExceptionKind.Malformed` para un error de conversión u obligatoriedad lanzado solo (400); el punto de entrada cambia el estado de un código cuando el negocio lo pide (`ORD001` → 409).
|
|
@@ -39,8 +39,7 @@ El ejemplo canónico de *DDD in PHP* (moneda e importe juntos) [PHP: Value Objec
|
|
|
39
39
|
|
|
40
40
|
```ts
|
|
41
41
|
import { Validations } from '@archi-code/validation';
|
|
42
|
-
import { Composite, NumberType, Required, StringType } from '@archi-code/ddd/domain';
|
|
43
|
-
import { DomainException } from '@archi-code/ddd/exceptions';
|
|
42
|
+
import { Composite, DomainException, NumberType, Required, StringType } from '@archi-code/ddd/domain';
|
|
44
43
|
|
|
45
44
|
export class CurrencyMismatch extends DomainException {
|
|
46
45
|
constructor(expected: string, received: string) {
|
|
@@ -374,4 +373,4 @@ describe('Money', () => {
|
|
|
374
373
|
});
|
|
375
374
|
```
|
|
376
375
|
|
|
377
|
-
`ValidationException` se importa de `@archi-code/ddd/
|
|
376
|
+
`ValidationException` se importa de `@archi-code/ddd/domain`.
|