@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
|
@@ -32,11 +32,9 @@ Qué pone `@archi-code/ddd` y qué escribe el proyecto:
|
|
|
32
32
|
El caso de uso guarda y después publica lo que devuelve `pullDomainEvents()`: si `save` falla no se publica nada, y los eventos nunca salen dos veces. No hay puerto de transacciones: el caso de uso recibe solo los roles de repositorio que usa, el `EventBus` y el `Clock`, y el repositorio garantiza que `save` es una escritura atómica `[Fuera de los libros: convención de @archi-code/ddd]`.
|
|
33
33
|
|
|
34
34
|
```ts
|
|
35
|
-
import { ValueObjects } from '@archi-code/ddd/domain';
|
|
36
35
|
import type { AggregateFinder, AggregateSaver } from '@archi-code/ddd/domain';
|
|
37
|
-
import { UseCase } from '@archi-code/ddd/application';
|
|
36
|
+
import { AggregateNotFoundException, UseCase, ValueObjectBuilder } from '@archi-code/ddd/application';
|
|
38
37
|
import type { Clock, EventBus } from '@archi-code/ddd/application';
|
|
39
|
-
import { AggregateNotFoundException } from '@archi-code/ddd/exceptions';
|
|
40
38
|
|
|
41
39
|
interface AddOrderLineInput {
|
|
42
40
|
orderId: string;
|
|
@@ -60,7 +58,7 @@ export class AddOrderLine extends UseCase<AddOrderLineInput, AddOrderLineCommand
|
|
|
60
58
|
}
|
|
61
59
|
|
|
62
60
|
protected toCommand(input: AddOrderLineInput): AddOrderLineCommand {
|
|
63
|
-
return
|
|
61
|
+
return ValueObjectBuilder.build({
|
|
64
62
|
orderId: () => new OrderId(input.orderId),
|
|
65
63
|
line: () =>
|
|
66
64
|
OrderLine.fromPrimitives({
|
|
@@ -128,7 +126,7 @@ describe('Adding a line to an order', () => {
|
|
|
128
126
|
expect(channel.sent).toMatchObject([
|
|
129
127
|
{
|
|
130
128
|
eventName: 'OrderLineAdded',
|
|
131
|
-
|
|
129
|
+
orderId,
|
|
132
130
|
sku: 'GLOVES-02',
|
|
133
131
|
quantity: 2,
|
|
134
132
|
unitPrice: { amount: 1250, currency: 'EUR' },
|
|
@@ -156,7 +154,6 @@ CREATE TABLE stored_events (
|
|
|
156
154
|
event_id UUID NOT NULL UNIQUE,
|
|
157
155
|
aggregate_id TEXT NOT NULL,
|
|
158
156
|
event_name TEXT NOT NULL,
|
|
159
|
-
event_version INT NOT NULL,
|
|
160
157
|
occurred_on TIMESTAMPTZ NOT NULL,
|
|
161
158
|
body JSONB NOT NULL,
|
|
162
159
|
published_at TIMESTAMPTZ NULL
|
|
@@ -164,13 +161,14 @@ CREATE TABLE stored_events (
|
|
|
164
161
|
CREATE INDEX stored_events_unpublished ON stored_events (sequence) WHERE published_at IS NULL;
|
|
165
162
|
```
|
|
166
163
|
|
|
167
|
-
`sequence` conserva el orden causal dentro del contexto; `body` es el `SerializedEvent
|
|
164
|
+
`sequence` conserva el orden causal dentro del contexto; `body` es el `SerializedEvent`, la copia plana del evento (`{ ...event }`).
|
|
168
165
|
|
|
169
166
|
### El repositorio
|
|
170
167
|
|
|
171
168
|
```ts
|
|
172
169
|
import type { AggregateFinder, AggregateSaver, PrimitiveTypes } from '@archi-code/ddd/domain';
|
|
173
|
-
import { ConcurrencyException
|
|
170
|
+
import { ConcurrencyException } from '@archi-code/ddd/application';
|
|
171
|
+
import { InternalErrorException } from '@archi-code/ddd/infrastructure';
|
|
174
172
|
|
|
175
173
|
interface QueryResult<Row> {
|
|
176
174
|
rows: Row[];
|
|
@@ -195,6 +193,8 @@ interface OrderRow {
|
|
|
195
193
|
}
|
|
196
194
|
|
|
197
195
|
export class PostgresOrderRepository implements AggregateFinder<Order>, AggregateSaver<Order> {
|
|
196
|
+
private readonly versions = new WeakMap<Order, number>();
|
|
197
|
+
|
|
198
198
|
constructor(private readonly pool: Pool) {}
|
|
199
199
|
|
|
200
200
|
async findById(id: OrderId): Promise<Order | null> {
|
|
@@ -203,41 +203,46 @@ export class PostgresOrderRepository implements AggregateFinder<Order>, Aggregat
|
|
|
203
203
|
'SELECT version, data FROM orders WHERE id = $1',
|
|
204
204
|
[id.value],
|
|
205
205
|
);
|
|
206
|
-
|
|
206
|
+
if (rows[0] === undefined) {
|
|
207
|
+
return null;
|
|
208
|
+
}
|
|
209
|
+
const order = Order.fromPrimitives(rows[0].data);
|
|
210
|
+
this.versions.set(order, rows[0].version);
|
|
211
|
+
return order;
|
|
207
212
|
} catch (error) {
|
|
208
213
|
throw new InternalErrorException('loading order', { cause: error });
|
|
209
214
|
}
|
|
210
215
|
}
|
|
211
216
|
|
|
212
217
|
async save(order: Order): Promise<void> {
|
|
218
|
+
const version = this.versions.get(order) ?? 0;
|
|
213
219
|
const primitives = order.toPrimitives();
|
|
214
|
-
const events = order.pullDomainEvents().map((event) => event
|
|
220
|
+
const events = order.pullDomainEvents().map((event) => ({ ...event }));
|
|
215
221
|
const client = await this.pool.connect();
|
|
216
222
|
try {
|
|
217
223
|
await client.query('BEGIN');
|
|
218
224
|
const { rowCount } =
|
|
219
|
-
|
|
225
|
+
version === 0
|
|
220
226
|
? await client.query(
|
|
221
227
|
'INSERT INTO orders (id, version, customer_id, status, data) VALUES ($1, 1, $2, $3, $4) ON CONFLICT (id) DO NOTHING',
|
|
222
228
|
[primitives.id, primitives.customerId, primitives.status, primitives],
|
|
223
229
|
)
|
|
224
230
|
: await client.query(
|
|
225
231
|
'UPDATE orders SET version = version + 1, status = $3, data = $4 WHERE id = $1 AND version = $2',
|
|
226
|
-
[primitives.id,
|
|
232
|
+
[primitives.id, version, primitives.status, primitives],
|
|
227
233
|
);
|
|
228
234
|
if (rowCount === 0) {
|
|
229
|
-
throw new ConcurrencyException('Order', order.id.value,
|
|
235
|
+
throw new ConcurrencyException('Order', order.id.value, version);
|
|
230
236
|
}
|
|
231
237
|
for (const event of events) {
|
|
232
238
|
await client.query(
|
|
233
239
|
`INSERT INTO stored_events
|
|
234
|
-
(event_id, aggregate_id, event_name,
|
|
235
|
-
VALUES ($1, $2, $3, $4, $5
|
|
240
|
+
(event_id, aggregate_id, event_name, occurred_on, body)
|
|
241
|
+
VALUES ($1, $2, $3, $4, $5)`,
|
|
236
242
|
[
|
|
237
243
|
event.eventId,
|
|
238
|
-
|
|
244
|
+
primitives.id,
|
|
239
245
|
event.eventName,
|
|
240
|
-
event.eventVersion,
|
|
241
246
|
event.occurredOn,
|
|
242
247
|
JSON.stringify(event),
|
|
243
248
|
],
|
|
@@ -253,19 +258,19 @@ export class PostgresOrderRepository implements AggregateFinder<Order>, Aggregat
|
|
|
253
258
|
} finally {
|
|
254
259
|
client.release();
|
|
255
260
|
}
|
|
256
|
-
|
|
261
|
+
this.versions.set(order, version + 1);
|
|
257
262
|
}
|
|
258
263
|
}
|
|
259
264
|
```
|
|
260
265
|
|
|
261
|
-
La transacción empieza y termina dentro de `save`: el pedido y sus eventos se confirman o se deshacen juntos, que es lo que pide "un agregado por transacción" [IDDD cap. 10]. `
|
|
266
|
+
La transacción empieza y termina dentro de `save`: el pedido y sus eventos se confirman o se deshacen juntos, que es lo que pide "un agregado por transacción" [IDDD cap. 10]. El repositorio actualiza la versión que recuerda para el pedido (`this.versions.set(order, version + 1)`) solo tras el `COMMIT`. Si la escritura falla, el caso de uso termina con el error y el agregado se descarta con sus eventos. El caso de uso no cambia: su `publish` recibe una lista vacía, porque los eventos de este pedido ya están en el outbox. El esquema de `orders` y el repositorio sin outbox están en `ddd-skills/ddd-repositorios-persistencia/references/implementacion-ts.md`.
|
|
262
267
|
|
|
263
268
|
## Reenviador
|
|
264
269
|
|
|
265
270
|
```ts
|
|
266
271
|
import type { EventChannel } from '@archi-code/ddd/infrastructure';
|
|
267
272
|
import type { SerializedEvent } from '@archi-code/ddd/domain';
|
|
268
|
-
import { InternalErrorException } from '@archi-code/ddd/
|
|
273
|
+
import { InternalErrorException } from '@archi-code/ddd/infrastructure';
|
|
269
274
|
|
|
270
275
|
export class OutboxForwarder {
|
|
271
276
|
private static readonly lockKey = 8_201_001;
|
|
@@ -338,7 +343,7 @@ Un lote que falla se queda en el outbox y lo reintenta la pasada siguiente; el `
|
|
|
338
343
|
|
|
339
344
|
## Lenguaje publicado en el adaptador del broker
|
|
340
345
|
|
|
341
|
-
Los otros contextos no reciben
|
|
346
|
+
Los otros contextos no reciben los eventos del dominio tal cual: reciben un contrato versionado [IDDD cap. 13]. El paquete no trae esa traducción; se hace en el adaptador que habla con el broker, a partir del `SerializedEvent`, con `messageId` = `eventId`. Un evento interno no produce ningún mensaje, y durante una transición un evento puede producir un mensaje por versión del contrato. El `SerializedEvent` no lleva campo de versión: cada versión incompatible del contrato se publica con su propio `eventName` (`sales.order.OrderPlacedV2`) `[Fuera de los libros: convención de @archi-code/ddd]`:
|
|
342
347
|
|
|
343
348
|
```ts
|
|
344
349
|
import type {
|
|
@@ -355,9 +360,7 @@ interface Broker {
|
|
|
355
360
|
export class SalesContracts {
|
|
356
361
|
static toPublished(event: SerializedEvent): SerializedEvent[] {
|
|
357
362
|
const envelope = {
|
|
358
|
-
eventVersion: 1,
|
|
359
363
|
eventId: event.eventId,
|
|
360
|
-
aggregateId: event.aggregateId,
|
|
361
364
|
occurredOn: event.occurredOn,
|
|
362
365
|
};
|
|
363
366
|
switch (event.eventName) {
|
|
@@ -366,7 +369,7 @@ export class SalesContracts {
|
|
|
366
369
|
{
|
|
367
370
|
...envelope,
|
|
368
371
|
eventName: 'sales.order.OrderPlaced',
|
|
369
|
-
orderId: event.
|
|
372
|
+
orderId: event.orderId,
|
|
370
373
|
customerId: event.customerId,
|
|
371
374
|
lines: (event.lines as OrderLineData[]).map((line) => ({
|
|
372
375
|
sku: line.sku,
|
|
@@ -381,7 +384,7 @@ export class SalesContracts {
|
|
|
381
384
|
{
|
|
382
385
|
...envelope,
|
|
383
386
|
eventName: 'sales.order.OrderConfirmed',
|
|
384
|
-
orderId: event.
|
|
387
|
+
orderId: event.orderId,
|
|
385
388
|
total: SalesContracts.money(event.total as MoneyData),
|
|
386
389
|
},
|
|
387
390
|
];
|
|
@@ -418,15 +421,15 @@ export class ContractPublishingChannel implements EventChannel {
|
|
|
418
421
|
}
|
|
419
422
|
```
|
|
420
423
|
|
|
421
|
-
`SalesContracts` decide qué eventos salen del contexto y con qué forma, campo a campo (`MoneyData` y `OrderLineData` son los de `ddd-skills/ddd-eventos-dominio/references/ejemplo-ecommerce.md`), para que un campo nuevo del evento interno no se filtre al contrato (ver "Eventos internos y eventos publicados" en `ddd-skills/ddd-eventos-dominio/GUIA.md`); el contrato no cambia cuando cambia el evento interno, solo cuando se publica una versión nueva. El mensaje es un `SerializedEvent` plano con el nombre
|
|
424
|
+
`SalesContracts` decide qué eventos salen del contexto y con qué forma, campo a campo (`MoneyData` y `OrderLineData` son los de `ddd-skills/ddd-eventos-dominio/references/ejemplo-ecommerce.md`), para que un campo nuevo del evento interno no se filtre al contrato (ver "Eventos internos y eventos publicados" en `ddd-skills/ddd-eventos-dominio/GUIA.md`); el contrato no cambia cuando cambia el evento interno, solo cuando se publica una versión nueva. El mensaje es un `SerializedEvent` plano con el nombre del contrato (`sales.order.OrderPlaced`, la v1; una v2 incompatible llevaría otro nombre), que el consumidor lee con su propia clase `EventBase` (`ddd-skills/ddd-context-mapping/references/anticorrupcion-ts.md`). `Broker` es la parte del driver que el adaptador usa, declarada de forma estructural.
|
|
422
425
|
|
|
423
426
|
## Consumidor idempotente
|
|
424
427
|
|
|
425
428
|
La entrega es al menos una vez, así que el consumidor se salta un `eventId` que ya procesó, y lo anota **en la misma transacción** que su cambio [IDDD cap. 8]. El paquete no trae inbox. `[Fuera de los libros: convención de @archi-code/ddd]` Anotar el `eventId` y escribir el efecto es persistencia, así que lo hace el adaptador del puerto que escribe, en una sola escritura atómica; el listener solo pasa el `eventId` al caso de uso (como en `ddd-skills/ddd-eventos-dominio/GUIA.md`, paso 3), y el caso de uso se lo pasa al puerto:
|
|
426
429
|
|
|
427
430
|
```ts
|
|
428
|
-
import { IdType
|
|
429
|
-
import { UseCase } from '@archi-code/ddd/application';
|
|
431
|
+
import { IdType } from '@archi-code/ddd/domain';
|
|
432
|
+
import { UseCase, ValueObjectBuilder } from '@archi-code/ddd/application';
|
|
430
433
|
|
|
431
434
|
export class EventId extends IdType<'EventId'> {}
|
|
432
435
|
|
|
@@ -454,7 +457,7 @@ export class PrepareShipment extends UseCase<PrepareShipmentInput, PrepareShipme
|
|
|
454
457
|
}
|
|
455
458
|
|
|
456
459
|
protected toCommand(input: PrepareShipmentInput): PrepareShipmentCommand {
|
|
457
|
-
return
|
|
460
|
+
return ValueObjectBuilder.build({
|
|
458
461
|
eventId: () => new EventId(input.eventId),
|
|
459
462
|
orderId: () => new OrderId(input.orderId),
|
|
460
463
|
sku: () => new Sku(input.sku),
|
|
@@ -469,7 +472,7 @@ export class PrepareShipment extends UseCase<PrepareShipmentInput, PrepareShipme
|
|
|
469
472
|
```
|
|
470
473
|
|
|
471
474
|
```ts
|
|
472
|
-
import { InternalErrorException } from '@archi-code/ddd/
|
|
475
|
+
import { InternalErrorException } from '@archi-code/ddd/infrastructure';
|
|
473
476
|
|
|
474
477
|
export class PostgresWarehouse implements Warehouse {
|
|
475
478
|
private static readonly consumer = 'prepare-shipment';
|
|
@@ -514,7 +517,7 @@ La anotación y el efecto van en la misma transacción, abierta y cerrada dentro
|
|
|
514
517
|
## Orden y causalidad
|
|
515
518
|
|
|
516
519
|
* El `sequence` de la tabla conserva el orden causal dentro del contexto [Destilado cap. 6], y el reenviador entrega en ese orden. Un mismo consumidor recibe los eventos de un `send` en su orden.
|
|
517
|
-
* El bus puede desordenar. Opciones `[Fuera de los libros]`: particionar por `
|
|
520
|
+
* El bus puede desordenar. Opciones `[Fuera de los libros]`: particionar por la identidad del agregado (`orderId`; Kafka, sesiones de Service Bus, un consumidor activo por cola en RabbitMQ) para mantener el orden por agregado; o que el consumidor compare `occurredOn` o una versión y aparque los eventos que llegan antes que su causa [Destilado cap. 6 describe la responsabilidad del consumidor].
|
|
518
521
|
* Si un evento posterior hace irrelevante uno anterior, el consumidor puede ignorar el anterior [Destilado cap. 6].
|
|
519
522
|
|
|
520
523
|
## Despacho interno
|
|
@@ -11,12 +11,12 @@
|
|
|
11
11
|
* No hay un `Repository` completo: los contratos son tres roles en `@archi-code/ddd/domain` (`AggregateFinder.findById`, `AggregateSaver.save`, `AggregateRemover.remove`) y un caso de uso pide solo los que usa, combinados con `&`.
|
|
12
12
|
* Los repositorios solo obtienen y persisten: sin `nextId()` ni consultas de negocio como `pendingOlderThan`, que pasan a ser consultas de lectura en `application/`.
|
|
13
13
|
* Cada repositorio es único para su agregado (`MongoOrderRepository implements AggregateFinder<Order>, AggregateSaver<Order>, AggregateRemover<Order>`) y sabe qué agregado reconstruye. Su constructor recibe solo las dependencias de su tecnología (una `collection` de MongoDB, un `pool` de Postgres), nunca la clase del agregado ni un mapper; los agregados solo entran y salen por sus métodos (`save(order)`, `remove(order)`, `findById(id)` → `Order | null`).
|
|
14
|
-
* La conversión es del agregado, su único dueño: `toPrimitives()` y `static fromPrimitives()` (instantánea o Memento), sin un mapper, un servicio de dominio ni una comprobación de forma en medio. El repositorio no lee campos del agregado: guarda `order.toPrimitives()` con
|
|
15
|
-
* La versión de concurrencia optimista
|
|
14
|
+
* La conversión es del agregado, su único dueño: `toPrimitives()` y `static fromPrimitives()` (instantánea o Memento), sin un mapper, un servicio de dominio ni una comprobación de forma en medio. El repositorio no lee campos del agregado: guarda `order.toPrimitives()` con la versión siguiente al lado y carga con `Order.fromPrimitives(row.primitives)`. Nada comprueba en ejecución que la conversión esté completa: lo garantiza el test del agregado con el viaje de ida y vuelta (`Order.fromPrimitives(order.toPrimitives()).toPrimitives()` igual a `order.toPrimitives()`), y quien añade un campo lo añade a los dos métodos.
|
|
15
|
+
* La versión de concurrencia optimista es solo del repositorio: el agregado no la declara ni la mapea y el caso de uso nunca la ve. El repositorio recuerda la versión de cada agregado que cargó o guardó en un `WeakMap<Order, number>` privado (`versions`): al cargar `this.versions.set(order, row.version)`; al guardar `const version = this.versions.get(order) ?? 0` (`0` = nuevo), escribe solo donde la versión guardada es `version` (nuevo: inserta, y un id ya existente es conflicto), guarda `version + 1` junto a los primitivos y hace `this.versions.set(order, version + 1)` solo tras escribir con éxito (en SQL, tras el `COMMIT`) `[Fuera de los libros: convención de @archi-code/ddd]`. Un conflicto es `ConcurrencyException('Order', id, version, { cause })` (HTTP 409), con el nombre del agregado como texto literal y no como nombre de clase, que los minificadores cambian.
|
|
16
16
|
* `remove(aggregate)` recibe el agregado cargado y comprueba su versión como `save`: borrar un agregado que otro cambió después de cargarlo es `ConcurrencyException`.
|
|
17
17
|
* El repositorio no publica eventos: lo hace el caso de uso con `events.publish(order.pullDomainEvents())` después de `save`.
|
|
18
18
|
* La transacción es del repositorio: no hay puerto de transacciones en `@archi-code/ddd/application` y la aplicación nunca abre, une ni cierra una. Cada `save`/`remove` es una escritura atómica, y si el agregado ocupa varias tablas el repositorio abre y confirma la transacción de la base de datos dentro de ese método, con su propia conexión o sesión.
|
|
19
|
-
* El paquete no da un repositorio en memoria: cada proyecto escribe el suyo en `test/support/`, uno por agregado (`InMemoryOrderRepository`), con el mismo contrato que el real (copias guardadas,
|
|
19
|
+
* El paquete no da un repositorio en memoria: cada proyecto escribe el suyo en `test/support/`, uno por agregado (`InMemoryOrderRepository`), con el mismo contrato que el real (copias guardadas, versiones en un `WeakMap` y su comprobación), y lo usa en los tests con un `DomainEventBus` sobre `InMemoryEventChannel`. Solo tiene `findAll()` si los tests del proyecto lo necesitan; un repositorio real no lo tiene.
|
|
20
20
|
|
|
21
21
|
Un repositorio da la ilusión de una colección en memoria con todos los agregados de un tipo, con una interfaz conocida para añadir, quitar y seleccionar. **Solo los agregados tienen repositorio**, normalmente uno por tipo de agregado [IDDD cap. 12, citando a Evans].
|
|
22
22
|
|
|
@@ -47,7 +47,7 @@ El proyecto no declara un `OrderRepository`: usa los contratos del paquete, que
|
|
|
47
47
|
| Rol (`@archi-code/ddd/domain`) | Método | Contrato |
|
|
48
48
|
|---|---|---|
|
|
49
49
|
| `AggregateFinder<Order>` | `findById(id: OrderId): Promise<Order \| null>` | Devuelve el agregado completo con su versión almacenada, o `null` |
|
|
50
|
-
| `AggregateSaver<Order>` | `save(order: Order): Promise<void>` | Alta (versión 0) y cambios; comprueba la versión
|
|
50
|
+
| `AggregateSaver<Order>` | `save(order: Order): Promise<void>` | Alta (versión 0) y cambios; comprueba la versión que recuerda su `WeakMap` y la actualiza tras escribir |
|
|
51
51
|
| `AggregateRemover<Order>` | `remove(order: Order): Promise<void>` | Borra el agregado cargado solo si nadie lo cambió después |
|
|
52
52
|
|
|
53
53
|
Un caso de uso pide exactamente los roles que usa. Constructor de `AddOrderLine` (la clase completa, con `toCommand` y `handle`, está en `ddd-skills/ddd-repositorios-persistencia/references/transacciones-ts.md`):
|
|
@@ -76,8 +76,8 @@ export class AddOrderLine extends UseCase<AddOrderLineInput, AddOrderLineCommand
|
|
|
76
76
|
|
|
77
77
|
Guía completa con MongoDB, PostgreSQL y Prisma en `ddd-skills/ddd-repositorios-persistencia/references/implementacion-ts.md`. Puntos clave:
|
|
78
78
|
|
|
79
|
-
* El agregado se convierte en datos planos y vuelve a construirse con sus value objects [IDDD cap. 6]: la conversión es del propio agregado y el repositorio llama a `order.toPrimitives()` y a `Order.fromPrimitives(row
|
|
80
|
-
* La **versión** se comprueba al guardar y al borrar (concurrencia optimista) [IDDD cap. 10]: se guarda junto a los primitivos, nunca dentro
|
|
79
|
+
* El agregado se convierte en datos planos y vuelve a construirse con sus value objects [IDDD cap. 6]: la conversión es del propio agregado y el repositorio llama a `order.toPrimitives()` y a `Order.fromPrimitives(row.primitives)` `[Fuera de los libros: convención de @archi-code/ddd]`. Que no falte ningún campo lo prueba el test de ida y vuelta del agregado, no una comprobación en ejecución.
|
|
80
|
+
* La **versión** se comprueba al guardar y al borrar (concurrencia optimista) [IDDD cap. 10]: se guarda junto a los primitivos, nunca dentro; el repositorio la recuerda por agregado en su `WeakMap` y la actualiza solo tras escribir con éxito `[Fuera de los libros: convención de @archi-code/ddd]`.
|
|
81
81
|
* Los **eventos** los publica el caso de uso con `EventBus.publish` después de `save`. Si un evento no puede perderse, el repositorio escribe el agregado y sus eventos pendientes en la misma escritura atómica (outbox) y un proceso aparte los entrega después por un `EventChannel` (`ddd-skills/ddd-repositorios-persistencia/references/transacciones-ts.md`, `ddd-skills/ddd-eventos-dominio/GUIA.md`).
|
|
82
82
|
* El **esquema** sirve al modelo, no al revés [IDDD cap. 6]. Un agregado pequeño encaja a menudo en un documento, o en una fila con una columna `jsonb` para sus value objects.
|
|
83
83
|
|
|
@@ -112,7 +112,7 @@ Un mismo conjunto de tests de contrato, en `test/support/` del proyecto, se ejec
|
|
|
112
112
|
* El repositorio lee campos del agregado o construye filas a mano en vez de llamar a `order.toPrimitives()` y `Order.fromPrimitives(row)`.
|
|
113
113
|
* El constructor del repositorio recibe la clase del agregado o un mapper: recibe solo las dependencias de su tecnología.
|
|
114
114
|
* Un campo nuevo del agregado que no está en `toPrimitives()`/`fromPrimitives()`, sobre todo uno privado que `PrimitiveTypes<Order>` no ve, sin un test de ida y vuelta que lo detecte.
|
|
115
|
-
* `save` con `upsert` sin condición de versión,
|
|
115
|
+
* `save` con `upsert` sin condición de versión, cargar sin recordar la versión almacenada en el `WeakMap`, olvidar actualizarla tras escribir (el segundo `save` del mismo objeto da un falso conflicto) o actualizarla antes de que la escritura termine. También la versión dentro de `toPrimitives()` o como campo del agregado.
|
|
116
116
|
* Un repositorio en memoria que no comprueba versiones ni copia objetos (los tests pasan, producción falla): escríbelo como `InMemoryOrderRepository` y ejecuta contra él la suite de contrato.
|
|
117
117
|
|
|
118
118
|
## Siguiente guía
|
|
@@ -27,17 +27,17 @@ const primitives: PrimitiveTypes<Order> = {
|
|
|
27
27
|
};
|
|
28
28
|
```
|
|
29
29
|
|
|
30
|
-
Un documento (o una fila) por agregado: lectura rápida, sin joins, y la versión protege todo el agregado [IDDD cap. 10]. La versión se guarda **junto a** los primitivos, nunca dentro: el agregado no la declara y
|
|
30
|
+
Un documento (o una fila) por agregado: lectura rápida, sin joins, y la versión protege todo el agregado [IDDD cap. 10]. La versión se guarda **junto a** los primitivos, nunca dentro: el agregado no la declara ni la mapea, y el caso de uso nunca la ve `[Fuera de los libros: convención de @archi-code/ddd]`.
|
|
31
31
|
|
|
32
|
-
Convención de versiones del paquete:
|
|
32
|
+
Convención de versiones del paquete: la versión es solo del repositorio. Cada repositorio recuerda la versión de cada agregado que cargó o guardó en un campo privado `versions = new WeakMap<Order, number>()`. Al cargar, `this.versions.set(order, row.version)`. Al guardar, `const version = this.versions.get(order) ?? 0` (`0` es un agregado nuevo): escribe solo donde la versión almacenada es `version` (un agregado nuevo se inserta y un id ya ocupado es un conflicto), guarda `version + 1` junto a los primitivos y, solo cuando la escritura se completó (en SQL, tras el `COMMIT`), hace `this.versions.set(order, version + 1)`. Si `save` lanza, el repositorio sigue recordando la versión anterior y el caso de uso descarta el objeto y vuelve a cargarlo. `remove` comprueba la versión igual. Al ser un `WeakMap`, un agregado que nadie usa ya no retiene memoria `[Fuera de los libros: convención de @archi-code/ddd]`.
|
|
33
33
|
|
|
34
34
|
## Conversión
|
|
35
35
|
|
|
36
36
|
No se escribe un mapper: la conversión es del agregado, su único dueño (`toPrimitives()` y `static fromPrimitives()`, su instantánea o Memento), no de un servicio de dominio ni de un mapper hecho a mano, y no hay comprobación de forma en medio `[Fuera de los libros: convención de @archi-code/ddd]`. El repositorio llama al agregado directamente:
|
|
37
37
|
|
|
38
|
-
* Al escribir: guarda `order.toPrimitives()` con `
|
|
39
|
-
* Al leer: `Order.fromPrimitives(row).
|
|
40
|
-
* En un conflicto: `throw new ConcurrencyException('Order', key,
|
|
38
|
+
* Al escribir: guarda `order.toPrimitives()` con `version + 1` al lado (`version` es la que recuerda su `WeakMap`, `0` si es nuevo) y, tras escribir, `this.versions.set(order, version + 1)`.
|
|
39
|
+
* Al leer: `const order = Order.fromPrimitives(row.primitives)` y `this.versions.set(order, row.version)`.
|
|
40
|
+
* En un conflicto: `throw new ConcurrencyException('Order', key, version)`, con el nombre del agregado como texto literal, no como nombre de clase: los minificadores cambian los nombres de las clases.
|
|
41
41
|
|
|
42
42
|
Nada comprueba en ejecución que la conversión esté completa. Lo garantizan el agregado y sus tests: el spec de cada agregado prueba el viaje de ida y vuelta (`Order.fromPrimitives(order.toPrimitives()).toPrimitives()` igual a `order.toPrimitives()`), y quien añade un campo lo añade a `toPrimitives()` y a `fromPrimitives()`, también un campo privado que TypeScript no ve en `PrimitiveTypes<Order>`.
|
|
43
43
|
|
|
@@ -56,7 +56,8 @@ import type {
|
|
|
56
56
|
AggregateSaver,
|
|
57
57
|
PrimitiveTypes,
|
|
58
58
|
} from '@archi-code/ddd/domain';
|
|
59
|
-
import { ConcurrencyException
|
|
59
|
+
import { ConcurrencyException } from '@archi-code/ddd/application';
|
|
60
|
+
import { InternalErrorException } from '@archi-code/ddd/infrastructure';
|
|
60
61
|
import { Order, OrderId } from '../../domain/order/order';
|
|
61
62
|
|
|
62
63
|
export type OrderDocument = PrimitiveTypes<Order> & { _id: string; version: number };
|
|
@@ -66,6 +67,8 @@ const DUPLICATE_KEY = 11000;
|
|
|
66
67
|
export class MongoOrderRepository
|
|
67
68
|
implements AggregateFinder<Order>, AggregateSaver<Order>, AggregateRemover<Order>
|
|
68
69
|
{
|
|
70
|
+
private readonly versions = new WeakMap<Order, number>();
|
|
71
|
+
|
|
69
72
|
constructor(private readonly collection: Collection<OrderDocument>) {}
|
|
70
73
|
|
|
71
74
|
async findById(id: OrderId): Promise<Order | null> {
|
|
@@ -74,54 +77,60 @@ export class MongoOrderRepository
|
|
|
74
77
|
return null;
|
|
75
78
|
}
|
|
76
79
|
const { _id, version, ...primitives } = document;
|
|
77
|
-
|
|
80
|
+
const order = Order.fromPrimitives(primitives);
|
|
81
|
+
this.versions.set(order, version);
|
|
82
|
+
return order;
|
|
78
83
|
}
|
|
79
84
|
|
|
80
85
|
async save(order: Order): Promise<void> {
|
|
86
|
+
const version = this.versions.get(order) ?? 0;
|
|
81
87
|
const document: OrderDocument = {
|
|
82
88
|
...order.toPrimitives(),
|
|
83
89
|
_id: order.id.value,
|
|
84
|
-
version:
|
|
90
|
+
version: version + 1,
|
|
85
91
|
};
|
|
86
|
-
const written = await this.write(order, async () => {
|
|
87
|
-
if (
|
|
92
|
+
const written = await this.write(order, version, async () => {
|
|
93
|
+
if (version === 0) {
|
|
88
94
|
await this.collection.insertOne(document);
|
|
89
95
|
return true;
|
|
90
96
|
}
|
|
91
97
|
const { matchedCount } = await this.collection.replaceOne(
|
|
92
|
-
{ _id: document._id, version
|
|
98
|
+
{ _id: document._id, version },
|
|
93
99
|
document,
|
|
94
100
|
);
|
|
95
101
|
return matchedCount === 1;
|
|
96
102
|
});
|
|
97
103
|
if (!written) {
|
|
98
|
-
throw new ConcurrencyException('Order', order.id.value,
|
|
104
|
+
throw new ConcurrencyException('Order', order.id.value, version);
|
|
99
105
|
}
|
|
100
|
-
|
|
106
|
+
this.versions.set(order, version + 1);
|
|
101
107
|
}
|
|
102
108
|
|
|
103
109
|
async remove(order: Order): Promise<void> {
|
|
110
|
+
const version = this.versions.get(order) ?? 0;
|
|
104
111
|
const filter = { _id: order.id.value };
|
|
105
|
-
const removed = await this.write(order, async () => {
|
|
106
|
-
if (
|
|
112
|
+
const removed = await this.write(order, version, async () => {
|
|
113
|
+
if (version === 0) {
|
|
107
114
|
return (await this.collection.countDocuments(filter, { limit: 1 })) === 0;
|
|
108
115
|
}
|
|
109
|
-
const { deletedCount } = await this.collection.deleteOne({ ...filter, version
|
|
116
|
+
const { deletedCount } = await this.collection.deleteOne({ ...filter, version });
|
|
110
117
|
return deletedCount === 1;
|
|
111
118
|
});
|
|
112
119
|
if (!removed) {
|
|
113
|
-
throw new ConcurrencyException('Order', order.id.value,
|
|
120
|
+
throw new ConcurrencyException('Order', order.id.value, version);
|
|
114
121
|
}
|
|
115
122
|
}
|
|
116
123
|
|
|
117
|
-
private async write(
|
|
124
|
+
private async write(
|
|
125
|
+
order: Order,
|
|
126
|
+
version: number,
|
|
127
|
+
operation: () => Promise<boolean>,
|
|
128
|
+
): Promise<boolean> {
|
|
118
129
|
try {
|
|
119
130
|
return await operation();
|
|
120
131
|
} catch (error) {
|
|
121
132
|
if (error instanceof MongoServerError && error.code === DUPLICATE_KEY) {
|
|
122
|
-
throw new ConcurrencyException('Order', order.id.value,
|
|
123
|
-
cause: error,
|
|
124
|
-
});
|
|
133
|
+
throw new ConcurrencyException('Order', order.id.value, version, { cause: error });
|
|
125
134
|
}
|
|
126
135
|
throw new InternalErrorException(`writing order ${order.id.value}`, { cause: error });
|
|
127
136
|
}
|
|
@@ -129,9 +138,9 @@ export class MongoOrderRepository
|
|
|
129
138
|
}
|
|
130
139
|
```
|
|
131
140
|
|
|
132
|
-
* Alta (`version === 0`): `insertOne`; un `_id` ya ocupado (error 11000) es `ConcurrencyException` con el error del driver como `cause`.
|
|
133
|
-
* Cambio: `replaceOne` con la condición `{ _id, version
|
|
134
|
-
* Un conflicto es `ConcurrencyException`: el caso de uso puede reintentar volviendo a cargar y a guardar, y `
|
|
141
|
+
* Alta (el `WeakMap` no conoce el agregado, `version === 0`): `insertOne`; un `_id` ya ocupado (error 11000) es `ConcurrencyException` con el error del driver como `cause`.
|
|
142
|
+
* Cambio: `replaceOne` con la condición `{ _id, version }`, la versión que el repositorio recordaba; si no casa ningún documento, otro guardó antes.
|
|
143
|
+
* Un conflicto es `ConcurrencyException`: el caso de uso puede reintentar volviendo a cargar y a guardar, y `ExceptionClassifier` lo clasifica como `ExceptionKind.Conflict` (409 en HTTP), no como error interno.
|
|
135
144
|
* Borrado: `deleteOne` con la misma condición. Un agregado que nunca se guardó (versión 0) no tiene nada que borrar, salvo que su id ya esté ocupado: entonces es un conflicto, igual que en el `InMemoryOrderRepository` de los tests (`ddd-skills/ddd-repositorios-persistencia/references/tests-repositorios.md`).
|
|
136
145
|
* Cualquier otro error del driver al escribir es `InternalErrorException` con el error como `cause`.
|
|
137
146
|
* Cuidado con la serialización: IDDD advierte de que en estos almacenes hay que cuidar el formato serializado, que no es gratis [IDDD cap. 12]. Los primitivos son datos planos y `Date`, que el driver guarda tal cual.
|
|
@@ -154,7 +163,8 @@ CREATE TABLE orders (
|
|
|
154
163
|
|
|
155
164
|
```ts
|
|
156
165
|
import type { AggregateFinder, AggregateSaver, PrimitiveTypes } from '@archi-code/ddd/domain';
|
|
157
|
-
import { ConcurrencyException
|
|
166
|
+
import { ConcurrencyException } from '@archi-code/ddd/application';
|
|
167
|
+
import { InternalErrorException } from '@archi-code/ddd/infrastructure';
|
|
158
168
|
import { Order, OrderId } from '../../domain/order/order';
|
|
159
169
|
import type { PgPool } from './pg-pool';
|
|
160
170
|
|
|
@@ -164,6 +174,8 @@ interface OrderRow {
|
|
|
164
174
|
}
|
|
165
175
|
|
|
166
176
|
export class PostgresOrderRepository implements AggregateFinder<Order>, AggregateSaver<Order> {
|
|
177
|
+
private readonly versions = new WeakMap<Order, number>();
|
|
178
|
+
|
|
167
179
|
constructor(private readonly pool: PgPool) {}
|
|
168
180
|
|
|
169
181
|
async findById(id: OrderId): Promise<Order | null> {
|
|
@@ -171,28 +183,34 @@ export class PostgresOrderRepository implements AggregateFinder<Order>, Aggregat
|
|
|
171
183
|
'SELECT version, data FROM orders WHERE id = $1',
|
|
172
184
|
[id.value],
|
|
173
185
|
);
|
|
174
|
-
|
|
186
|
+
if (rows[0] === undefined) {
|
|
187
|
+
return null;
|
|
188
|
+
}
|
|
189
|
+
const order = Order.fromPrimitives(rows[0].data);
|
|
190
|
+
this.versions.set(order, rows[0].version);
|
|
191
|
+
return order;
|
|
175
192
|
}
|
|
176
193
|
|
|
177
194
|
async save(order: Order): Promise<void> {
|
|
195
|
+
const version = this.versions.get(order) ?? 0;
|
|
178
196
|
const primitives = order.toPrimitives();
|
|
179
197
|
const write =
|
|
180
|
-
|
|
198
|
+
version === 0
|
|
181
199
|
? this.pool.query(
|
|
182
200
|
'INSERT INTO orders (id, version, customer_id, status, data) VALUES ($1, 1, $2, $3, $4) ON CONFLICT (id) DO NOTHING',
|
|
183
201
|
[primitives.id, primitives.customerId, primitives.status, primitives],
|
|
184
202
|
)
|
|
185
203
|
: this.pool.query(
|
|
186
204
|
'UPDATE orders SET version = version + 1, status = $3, data = $4 WHERE id = $1 AND version = $2',
|
|
187
|
-
[primitives.id,
|
|
205
|
+
[primitives.id, version, primitives.status, primitives],
|
|
188
206
|
);
|
|
189
207
|
const { rowCount } = await write.catch((error: unknown) => {
|
|
190
208
|
throw new InternalErrorException(`writing order ${order.id.value}`, { cause: error });
|
|
191
209
|
});
|
|
192
210
|
if (rowCount === 0) {
|
|
193
|
-
throw new ConcurrencyException('Order', order.id.value,
|
|
211
|
+
throw new ConcurrencyException('Order', order.id.value, version);
|
|
194
212
|
}
|
|
195
|
-
|
|
213
|
+
this.versions.set(order, version + 1);
|
|
196
214
|
}
|
|
197
215
|
}
|
|
198
216
|
```
|
|
@@ -202,8 +220,8 @@ Al leer `data` de `jsonb`, `paymentAuthorization.expiresAt` llega como texto ISO
|
|
|
202
220
|
## Con Prisma
|
|
203
221
|
|
|
204
222
|
`[Fuera de los libros]`
|
|
205
|
-
* El esquema de Prisma y su cliente viven en infraestructura. El repositorio implementa `AggregateFinder<Order>` y `AggregateSaver<Order>`, recibe el `PrismaClient` y devuelve agregados con `Order.fromPrimitives(row.data).
|
|
206
|
-
* La concurrencia optimista con Prisma: `updateMany({ where: { id, version
|
|
223
|
+
* El esquema de Prisma y su cliente viven en infraestructura. El repositorio implementa `AggregateFinder<Order>` y `AggregateSaver<Order>`, recibe el `PrismaClient` y devuelve agregados con `Order.fromPrimitives(row.data)` (y `this.versions.set(order, row.version)` en su `WeakMap`), nunca modelos de Prisma.
|
|
224
|
+
* La concurrencia optimista con Prisma: con `const version = this.versions.get(order) ?? 0`, `updateMany({ where: { id, version }, data: { ..., version: { increment: 1 } } })`; si `count === 0`, `ConcurrencyException('Order', order.id.value, version)`; si no, `this.versions.set(order, version + 1)`.
|
|
207
225
|
* Si el agregado ocupa varios modelos de Prisma, el repositorio agrupa sus escrituras con `prisma.$transaction(async (tx) => ...)` dentro de su propio `save`; la aplicación nunca la abre `[Fuera de los libros: convención de @archi-code/ddd]`.
|
|
208
226
|
|
|
209
227
|
## Consultas de lectura
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
Un mapa con la identidad como clave y el agregado como valor [IDDD cap. 12]. Dos detalles para que no mienta `[Fuera de los libros]`: guardar copias (para que modificar un objeto sin `save` no "persista") y comprobar versiones.
|
|
6
6
|
|
|
7
|
-
El paquete no lo da: cada proyecto escribe el suyo en `test/support/`, uno por agregado, como cualquier otra implementación de una tecnología `[Fuera de los libros: convención de @archi-code/ddd]`. Implementa los roles de `@archi-code/ddd/domain` para su agregado (`AggregateFinder<Order>`, `AggregateSaver<Order>`, `AggregateRemover<Order>`), su constructor no recibe nada (no hay tecnología de la que depender)
|
|
7
|
+
El paquete no lo da: cada proyecto escribe el suyo en `test/support/`, uno por agregado, como cualquier otra implementación de una tecnología `[Fuera de los libros: convención de @archi-code/ddd]`. Implementa los roles de `@archi-code/ddd/domain` para su agregado (`AggregateFinder<Order>`, `AggregateSaver<Order>`, `AggregateRemover<Order>`), su constructor no recibe nada (no hay tecnología de la que depender) llama a `order.toPrimitives()` y `Order.fromPrimitives(row.primitives)` y recuerda la versión de cada agregado en un `WeakMap`, igual que un adaptador real.
|
|
8
8
|
|
|
9
9
|
En `test/support/in-memory-order-repository.ts`:
|
|
10
10
|
|
|
@@ -15,44 +15,58 @@ import type {
|
|
|
15
15
|
AggregateSaver,
|
|
16
16
|
PrimitiveTypes,
|
|
17
17
|
} from '@archi-code/ddd/domain';
|
|
18
|
-
import { ConcurrencyException } from '@archi-code/ddd/
|
|
18
|
+
import { ConcurrencyException } from '@archi-code/ddd/application';
|
|
19
19
|
import { Order } from '../../src/contexts/sales/domain/order/order';
|
|
20
20
|
import type { OrderId } from '../../src/contexts/sales/domain/order/order';
|
|
21
21
|
|
|
22
|
+
interface StoredOrder {
|
|
23
|
+
version: number;
|
|
24
|
+
primitives: PrimitiveTypes<Order>;
|
|
25
|
+
}
|
|
26
|
+
|
|
22
27
|
export class InMemoryOrderRepository
|
|
23
28
|
implements AggregateFinder<Order>, AggregateSaver<Order>, AggregateRemover<Order>
|
|
24
29
|
{
|
|
25
|
-
private readonly rows = new Map<string,
|
|
30
|
+
private readonly rows = new Map<string, StoredOrder>();
|
|
31
|
+
private readonly versions = new WeakMap<Order, number>();
|
|
26
32
|
|
|
27
33
|
async findById(id: OrderId): Promise<Order | null> {
|
|
28
34
|
const row = this.rows.get(id.value);
|
|
29
|
-
return row === undefined
|
|
30
|
-
? null
|
|
31
|
-
: Order.fromPrimitives(structuredClone(row.primitives)).restoreVersion(row.version);
|
|
35
|
+
return row === undefined ? null : this.load(row);
|
|
32
36
|
}
|
|
33
37
|
|
|
34
38
|
async save(order: Order): Promise<void> {
|
|
35
|
-
const key =
|
|
36
|
-
|
|
37
|
-
order.
|
|
39
|
+
const key = order.id.value;
|
|
40
|
+
const version = this.checkedVersion(order);
|
|
41
|
+
const primitives = structuredClone(order.toPrimitives());
|
|
42
|
+
this.rows.set(key, { version: version + 1, primitives });
|
|
43
|
+
this.versions.set(order, version + 1);
|
|
38
44
|
}
|
|
39
45
|
|
|
40
46
|
async remove(order: Order): Promise<void> {
|
|
41
|
-
this.
|
|
47
|
+
this.checkedVersion(order);
|
|
48
|
+
this.rows.delete(order.id.value);
|
|
42
49
|
}
|
|
43
50
|
|
|
44
|
-
private
|
|
51
|
+
private checkedVersion(order: Order): number {
|
|
45
52
|
const key = order.id.value;
|
|
46
|
-
|
|
47
|
-
|
|
53
|
+
const version = this.versions.get(order) ?? 0;
|
|
54
|
+
if ((this.rows.get(key)?.version ?? 0) !== version) {
|
|
55
|
+
throw new ConcurrencyException('Order', key, version);
|
|
48
56
|
}
|
|
49
|
-
return
|
|
57
|
+
return version;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
private load(row: StoredOrder): Order {
|
|
61
|
+
const order = Order.fromPrimitives(structuredClone(row.primitives));
|
|
62
|
+
this.versions.set(order, row.version);
|
|
63
|
+
return order;
|
|
50
64
|
}
|
|
51
65
|
}
|
|
52
66
|
```
|
|
53
67
|
|
|
54
|
-
* Guarda `structuredClone` de `order.toPrimitives()` con su versión al lado, y cada `findById` reconstruye un objeto nuevo con `Order.fromPrimitives(copy)
|
|
55
|
-
* Sigue el mismo contrato de concurrencia que un repositorio real: `save` y `remove` lanzan `ConcurrencyException` si la versión almacenada no coincide con
|
|
68
|
+
* Guarda `structuredClone` de `order.toPrimitives()` con su versión al lado (nunca dentro), y cada `findById` reconstruye un objeto nuevo con `Order.fromPrimitives(copy)` y anota en `versions` la versión de la fila: cambiar un agregado sin `save` no persiste nada.
|
|
69
|
+
* Sigue el mismo contrato de concurrencia que un repositorio real: la versión es solo del repositorio, que recuerda la de cada agregado que cargó o guardó en un `WeakMap<Order, number>` (`0` si no lo conoce: es nuevo). `save` y `remove` lanzan `ConcurrencyException` si la versión almacenada no coincide con la recordada (también al dar de alta un id ocupado), y `save` actualiza el `WeakMap` solo después de escribir. El agregado no declara ni mapea ninguna versión `[Fuera de los libros: convención de @archi-code/ddd]`.
|
|
56
70
|
* Las claves son `id.value`; `IdType` y `UuidType` guardan el valor en minúsculas, así que la búsqueda no distingue mayúsculas.
|
|
57
71
|
* Cada `save` y cada `remove` es atómico, como el de un repositorio real: no hay transacción que abrir ni estado que restaurar en los tests (la transacción es del repositorio, `ddd-skills/ddd-repositorios-persistencia/references/transacciones-ts.md`).
|
|
58
72
|
* No tiene `findAll()` salvo que los tests del proyecto lo necesiten; si se añade, es solo comodidad de los tests y un repositorio real no lo tiene `[Fuera de los libros: convención de @archi-code/ddd]`. Nunca se usa en producción.
|
|
@@ -67,7 +81,7 @@ Un único conjunto de tests, en `test/support/order-repository-contract.ts` del
|
|
|
67
81
|
```ts
|
|
68
82
|
import { expect, it } from 'vitest';
|
|
69
83
|
import type { AggregateFinder, AggregateRemover, AggregateSaver } from '@archi-code/ddd/domain';
|
|
70
|
-
import { ConcurrencyException } from '@archi-code/ddd/
|
|
84
|
+
import { ConcurrencyException } from '@archi-code/ddd/application';
|
|
71
85
|
import { FixedClock } from '@archi-code/ddd/infrastructure';
|
|
72
86
|
import {
|
|
73
87
|
CustomerId,
|