@archi-code/ddd 0.0.0-stage → 3.0.0-alpha.1
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/LICENSE +46 -0
- package/README.md +2693 -2
- package/SKILL.md +104 -0
- package/ddd-skills/archi-code-ddd/imports.md +84 -0
- package/ddd-skills/archi-code-ddd/layers.md +299 -0
- package/ddd-skills/archi-code-ddd/patterns.md +676 -0
- package/ddd-skills/archi-code-ddd/what-not-to-do.md +189 -0
- package/ddd-skills/archi-code-ddd/where-each-class-goes.md +63 -0
- package/ddd-skills/ddd-agregados/GUIA.md +114 -0
- package/ddd-skills/ddd-agregados/references/consistencia-eventual.md +159 -0
- package/ddd-skills/ddd-agregados/references/ejemplo-ecommerce.md +52 -0
- package/ddd-skills/ddd-agregados/references/fabricas.md +196 -0
- package/ddd-skills/ddd-agregados/references/implementacion-ts.md +711 -0
- package/ddd-skills/ddd-arquitectura/GUIA.md +80 -0
- package/ddd-skills/ddd-arquitectura/assets/dependency-cruiser.cjs +92 -0
- package/ddd-skills/ddd-arquitectura/references/estilos-adicionales.md +51 -0
- package/ddd-skills/ddd-arquitectura/references/estructura-hexagonal-ts.md +352 -0
- package/ddd-skills/ddd-arquitectura/references/modulos.md +48 -0
- package/ddd-skills/ddd-auditoria-refactorizacion/GUIA.md +85 -0
- package/ddd-skills/ddd-auditoria-refactorizacion/references/ejemplo-auditoria.md +316 -0
- package/ddd-skills/ddd-auditoria-refactorizacion/references/lista-comprobacion.md +54 -0
- package/ddd-skills/ddd-auditoria-refactorizacion/references/plan-migracion.md +73 -0
- package/ddd-skills/ddd-auditoria-refactorizacion/scripts/scan_ddd_smells.py +264 -0
- package/ddd-skills/ddd-context-mapping/GUIA.md +83 -0
- package/ddd-skills/ddd-context-mapping/references/anticorrupcion-ts.md +404 -0
- package/ddd-skills/ddd-context-mapping/references/estilos-integracion.md +155 -0
- package/ddd-skills/ddd-event-sourcing-cqrs/GUIA.md +92 -0
- package/ddd-skills/ddd-event-sourcing-cqrs/references/event-sourcing-ts.md +471 -0
- package/ddd-skills/ddd-event-sourcing-cqrs/references/proyecciones-cqrs-ts.md +285 -0
- package/ddd-skills/ddd-eventos-dominio/GUIA.md +106 -0
- package/ddd-skills/ddd-eventos-dominio/references/diseno-eventos.md +131 -0
- package/ddd-skills/ddd-eventos-dominio/references/ejemplo-ecommerce.md +240 -0
- package/ddd-skills/ddd-eventos-dominio/references/publicacion-ts.md +526 -0
- package/ddd-skills/ddd-orquestador/GUIA.md +40 -0
- package/ddd-skills/ddd-orquestador/references/chuleta-reglas.md +35 -0
- package/ddd-skills/ddd-repositorios-persistencia/GUIA.md +121 -0
- package/ddd-skills/ddd-repositorios-persistencia/references/implementacion-ts.md +296 -0
- package/ddd-skills/ddd-repositorios-persistencia/references/tests-repositorios.md +177 -0
- package/ddd-skills/ddd-repositorios-persistencia/references/transacciones-ts.md +294 -0
- package/ddd-skills/ddd-servicios-capa-aplicacion/GUIA.md +99 -0
- package/ddd-skills/ddd-servicios-capa-aplicacion/references/capa-aplicacion-ts.md +264 -0
- package/ddd-skills/ddd-servicios-capa-aplicacion/references/ejemplo-ecommerce.md +324 -0
- package/ddd-skills/ddd-servicios-capa-aplicacion/references/servicios-dominio-ts.md +191 -0
- package/ddd-skills/ddd-value-objects-entidades/GUIA.md +118 -0
- package/ddd-skills/ddd-value-objects-entidades/references/ejemplo-ecommerce.md +87 -0
- package/ddd-skills/ddd-value-objects-entidades/references/entidades-ts.md +220 -0
- package/ddd-skills/ddd-value-objects-entidades/references/persistencia-valores.md +88 -0
- package/ddd-skills/ddd-value-objects-entidades/references/specification-y-diseno-flexible.md +119 -0
- package/ddd-skills/ddd-value-objects-entidades/references/validacion.md +96 -0
- package/ddd-skills/ddd-value-objects-entidades/references/value-objects-ts.md +377 -0
- package/dist/cjs/application/event/event-bus.d.ts +4 -0
- package/dist/cjs/application/event/event-bus.js +2 -0
- package/dist/cjs/application/event/event-listener.d.ts +4 -0
- package/dist/cjs/application/event/event-listener.js +6 -0
- package/dist/cjs/application/event/listens-to.d.ts +15 -0
- package/dist/cjs/application/event/listens-to.js +28 -0
- package/dist/cjs/application/index.d.ts +7 -0
- package/dist/cjs/application/index.js +9 -0
- package/dist/cjs/application/ports/clock.d.ts +3 -0
- package/dist/cjs/application/ports/clock.js +2 -0
- package/dist/cjs/application/ports/logger.d.ts +8 -0
- package/dist/cjs/application/ports/logger.js +2 -0
- package/dist/cjs/application/use-case/use-case.d.ts +6 -0
- package/dist/cjs/application/use-case/use-case.js +24 -0
- package/dist/cjs/domain/aggregate/aggregate-root.d.ts +14 -0
- package/dist/cjs/domain/aggregate/aggregate-root.js +33 -0
- package/dist/cjs/domain/entity/entity.d.ts +9 -0
- package/dist/cjs/domain/entity/entity.js +19 -0
- package/dist/cjs/domain/event/event-base.d.ts +29 -0
- package/dist/cjs/domain/event/event-base.js +51 -0
- package/dist/cjs/domain/index.d.ts +10 -0
- package/dist/cjs/domain/index.js +24 -0
- package/dist/cjs/domain/primitive/primitive-types.d.ts +22 -0
- package/dist/cjs/domain/primitive/primitive-types.js +2 -0
- package/dist/cjs/domain/repository/aggregate-finder.d.ts +4 -0
- package/dist/cjs/domain/repository/aggregate-finder.js +2 -0
- package/dist/cjs/domain/repository/aggregate-remover.d.ts +4 -0
- package/dist/cjs/domain/repository/aggregate-remover.js +2 -0
- package/dist/cjs/domain/repository/aggregate-saver.d.ts +4 -0
- package/dist/cjs/domain/repository/aggregate-saver.js +2 -0
- package/dist/cjs/domain/specification/specification.d.ts +9 -0
- package/dist/cjs/domain/specification/specification.js +44 -0
- package/dist/cjs/domain/type/abstract-type.d.ts +20 -0
- package/dist/cjs/domain/type/abstract-type.js +69 -0
- package/dist/cjs/domain/type/array-type.d.ts +16 -0
- package/dist/cjs/domain/type/array-type.js +65 -0
- package/dist/cjs/domain/type/boolean-type.d.ts +4 -0
- package/dist/cjs/domain/type/boolean-type.js +16 -0
- package/dist/cjs/domain/type/composite/composite-field.d.ts +19 -0
- package/dist/cjs/domain/type/composite/composite-field.js +63 -0
- package/dist/cjs/domain/type/composite/composite.d.ts +26 -0
- package/dist/cjs/domain/type/composite/composite.js +55 -0
- package/dist/cjs/domain/type/date-type.d.ts +18 -0
- package/dist/cjs/domain/type/date-type.js +35 -0
- package/dist/cjs/domain/type/enum-type.d.ts +7 -0
- package/dist/cjs/domain/type/enum-type.js +30 -0
- package/dist/cjs/domain/type/equality/value-equality.d.ts +7 -0
- package/dist/cjs/domain/type/equality/value-equality.js +41 -0
- package/dist/cjs/domain/type/id-type.d.ts +7 -0
- package/dist/cjs/domain/type/id-type.js +17 -0
- package/dist/cjs/domain/type/immutable/immutable-date.d.ts +4 -0
- package/dist/cjs/domain/type/immutable/immutable-date.js +37 -0
- package/dist/cjs/domain/type/immutable/immutable.d.ts +4 -0
- package/dist/cjs/domain/type/immutable/immutable.js +19 -0
- package/dist/cjs/domain/type/index.d.ts +17 -0
- package/dist/cjs/domain/type/index.js +33 -0
- package/dist/cjs/domain/type/json-type.d.ts +5 -0
- package/dist/cjs/domain/type/json-type.js +17 -0
- package/dist/cjs/domain/type/number-type.d.ts +4 -0
- package/dist/cjs/domain/type/number-type.js +16 -0
- package/dist/cjs/domain/type/required/required-mark.d.ts +9 -0
- package/dist/cjs/domain/type/required/required-mark.js +13 -0
- package/dist/cjs/domain/type/required/required.d.ts +7 -0
- package/dist/cjs/domain/type/required/required.js +21 -0
- package/dist/cjs/domain/type/string-type.d.ts +4 -0
- package/dist/cjs/domain/type/string-type.js +16 -0
- package/dist/cjs/domain/type/uuid-type.d.ts +7 -0
- package/dist/cjs/domain/type/uuid-type.js +27 -0
- package/dist/cjs/domain/type/value-objects/value-objects.d.ts +16 -0
- package/dist/cjs/domain/type/value-objects/value-objects.js +70 -0
- package/dist/cjs/exceptions/abstract.exception.d.ts +23 -0
- package/dist/cjs/exceptions/abstract.exception.js +66 -0
- package/dist/cjs/exceptions/application/aggregate-not-found.exception.d.ts +5 -0
- package/dist/cjs/exceptions/application/aggregate-not-found.exception.js +17 -0
- package/dist/cjs/exceptions/application/application.exception.d.ts +6 -0
- package/dist/cjs/exceptions/application/application.exception.js +17 -0
- package/dist/cjs/exceptions/application/concurrency.exception.d.ts +5 -0
- package/dist/cjs/exceptions/application/concurrency.exception.js +17 -0
- package/dist/cjs/exceptions/application/not-allowed.exception.d.ts +5 -0
- package/dist/cjs/exceptions/application/not-allowed.exception.js +17 -0
- package/dist/cjs/exceptions/brand/exception-brand.d.ts +6 -0
- package/dist/cjs/exceptions/brand/exception-brand.js +29 -0
- package/dist/cjs/exceptions/domain/domain.exception.d.ts +6 -0
- package/dist/cjs/exceptions/domain/domain.exception.js +17 -0
- package/dist/cjs/exceptions/domain/required-value.exception.d.ts +4 -0
- package/dist/cjs/exceptions/domain/required-value.exception.js +21 -0
- package/dist/cjs/exceptions/domain/type-primitive.exception.d.ts +4 -0
- package/dist/cjs/exceptions/domain/type-primitive.exception.js +21 -0
- package/dist/cjs/exceptions/domain/validation.exception.d.ts +14 -0
- package/dist/cjs/exceptions/domain/validation.exception.js +23 -0
- package/dist/cjs/exceptions/exception-code.d.ts +15 -0
- package/dist/cjs/exceptions/exception-code.js +19 -0
- package/dist/cjs/exceptions/index.d.ts +14 -0
- package/dist/cjs/exceptions/index.js +30 -0
- package/dist/cjs/exceptions/infrastructure/event-delivery.exception.d.ts +5 -0
- package/dist/cjs/exceptions/infrastructure/event-delivery.exception.js +20 -0
- package/dist/cjs/exceptions/infrastructure/infrastructure.exception.d.ts +6 -0
- package/dist/cjs/exceptions/infrastructure/infrastructure.exception.js +17 -0
- package/dist/cjs/exceptions/infrastructure/internal-error.exception.d.ts +5 -0
- package/dist/cjs/exceptions/infrastructure/internal-error.exception.js +17 -0
- package/dist/cjs/exceptions/infrastructure/unknown-event.exception.d.ts +4 -0
- package/dist/cjs/exceptions/infrastructure/unknown-event.exception.js +19 -0
- package/dist/cjs/exceptions/received/received-value.d.ts +4 -0
- package/dist/cjs/exceptions/received/received-value.js +13 -0
- package/dist/cjs/infrastructure/clock/fixed-clock.d.ts +8 -0
- package/dist/cjs/infrastructure/clock/fixed-clock.js +19 -0
- package/dist/cjs/infrastructure/clock/system-clock.d.ts +4 -0
- package/dist/cjs/infrastructure/clock/system-clock.js +9 -0
- package/dist/cjs/infrastructure/event/domain-event-bus.d.ts +16 -0
- package/dist/cjs/infrastructure/event/domain-event-bus.js +60 -0
- package/dist/cjs/infrastructure/event/event-channel.d.ts +9 -0
- package/dist/cjs/infrastructure/event/event-channel.js +2 -0
- package/dist/cjs/infrastructure/event/event-registry.d.ts +9 -0
- package/dist/cjs/infrastructure/event/event-registry.js +43 -0
- package/dist/cjs/infrastructure/event/in-memory/emitter.d.ts +11 -0
- package/dist/cjs/infrastructure/event/in-memory/emitter.js +54 -0
- package/dist/cjs/infrastructure/event/in-memory/in-memory-event-channel.d.ts +16 -0
- package/dist/cjs/infrastructure/event/in-memory/in-memory-event-channel.js +51 -0
- package/dist/cjs/infrastructure/http/http-error-mapper.d.ts +23 -0
- package/dist/cjs/infrastructure/http/http-error-mapper.js +63 -0
- package/dist/cjs/infrastructure/index.d.ts +16 -0
- package/dist/cjs/infrastructure/index.js +21 -0
- package/dist/cjs/infrastructure/logging/console-logger.d.ts +17 -0
- package/dist/cjs/infrastructure/logging/console-logger.js +37 -0
- package/dist/cjs/infrastructure/logging/in-memory-logger.d.ts +15 -0
- package/dist/cjs/infrastructure/logging/in-memory-logger.js +27 -0
- package/dist/cjs/infrastructure/logging/log-serializer.d.ts +7 -0
- package/dist/cjs/infrastructure/logging/log-serializer.js +59 -0
- package/dist/cjs/infrastructure/logging/silent-logger.d.ts +7 -0
- package/dist/cjs/infrastructure/logging/silent-logger.js +18 -0
- package/dist/esm/application/event/event-bus.d.ts +4 -0
- package/dist/esm/application/event/event-bus.js +1 -0
- package/dist/esm/application/event/event-listener.d.ts +4 -0
- package/dist/esm/application/event/event-listener.js +2 -0
- package/dist/esm/application/event/listens-to.d.ts +15 -0
- package/dist/esm/application/event/listens-to.js +25 -0
- package/dist/esm/application/index.d.ts +7 -0
- package/dist/esm/application/index.js +3 -0
- package/dist/esm/application/ports/clock.d.ts +3 -0
- package/dist/esm/application/ports/clock.js +1 -0
- package/dist/esm/application/ports/logger.d.ts +8 -0
- package/dist/esm/application/ports/logger.js +1 -0
- package/dist/esm/application/use-case/use-case.d.ts +6 -0
- package/dist/esm/application/use-case/use-case.js +20 -0
- package/dist/esm/domain/aggregate/aggregate-root.d.ts +14 -0
- package/dist/esm/domain/aggregate/aggregate-root.js +29 -0
- package/dist/esm/domain/entity/entity.d.ts +9 -0
- package/dist/esm/domain/entity/entity.js +15 -0
- package/dist/esm/domain/event/event-base.d.ts +29 -0
- package/dist/esm/domain/event/event-base.js +47 -0
- package/dist/esm/domain/index.d.ts +10 -0
- package/dist/esm/domain/index.js +6 -0
- package/dist/esm/domain/primitive/primitive-types.d.ts +22 -0
- package/dist/esm/domain/primitive/primitive-types.js +1 -0
- package/dist/esm/domain/repository/aggregate-finder.d.ts +4 -0
- package/dist/esm/domain/repository/aggregate-finder.js +1 -0
- package/dist/esm/domain/repository/aggregate-remover.d.ts +4 -0
- package/dist/esm/domain/repository/aggregate-remover.js +1 -0
- package/dist/esm/domain/repository/aggregate-saver.d.ts +4 -0
- package/dist/esm/domain/repository/aggregate-saver.js +1 -0
- package/dist/esm/domain/specification/specification.d.ts +9 -0
- package/dist/esm/domain/specification/specification.js +40 -0
- package/dist/esm/domain/type/abstract-type.d.ts +20 -0
- package/dist/esm/domain/type/abstract-type.js +65 -0
- package/dist/esm/domain/type/array-type.d.ts +16 -0
- package/dist/esm/domain/type/array-type.js +61 -0
- package/dist/esm/domain/type/boolean-type.d.ts +4 -0
- package/dist/esm/domain/type/boolean-type.js +12 -0
- package/dist/esm/domain/type/composite/composite-field.d.ts +19 -0
- package/dist/esm/domain/type/composite/composite-field.js +59 -0
- package/dist/esm/domain/type/composite/composite.d.ts +26 -0
- package/dist/esm/domain/type/composite/composite.js +52 -0
- package/dist/esm/domain/type/date-type.d.ts +18 -0
- package/dist/esm/domain/type/date-type.js +29 -0
- package/dist/esm/domain/type/enum-type.d.ts +7 -0
- package/dist/esm/domain/type/enum-type.js +26 -0
- package/dist/esm/domain/type/equality/value-equality.d.ts +7 -0
- package/dist/esm/domain/type/equality/value-equality.js +37 -0
- package/dist/esm/domain/type/id-type.d.ts +7 -0
- package/dist/esm/domain/type/id-type.js +13 -0
- package/dist/esm/domain/type/immutable/immutable-date.d.ts +4 -0
- package/dist/esm/domain/type/immutable/immutable-date.js +33 -0
- package/dist/esm/domain/type/immutable/immutable.d.ts +4 -0
- package/dist/esm/domain/type/immutable/immutable.js +15 -0
- package/dist/esm/domain/type/index.d.ts +17 -0
- package/dist/esm/domain/type/index.js +14 -0
- package/dist/esm/domain/type/json-type.d.ts +5 -0
- package/dist/esm/domain/type/json-type.js +13 -0
- package/dist/esm/domain/type/number-type.d.ts +4 -0
- package/dist/esm/domain/type/number-type.js +12 -0
- package/dist/esm/domain/type/required/required-mark.d.ts +9 -0
- package/dist/esm/domain/type/required/required-mark.js +9 -0
- package/dist/esm/domain/type/required/required.d.ts +7 -0
- package/dist/esm/domain/type/required/required.js +18 -0
- package/dist/esm/domain/type/string-type.d.ts +4 -0
- package/dist/esm/domain/type/string-type.js +12 -0
- package/dist/esm/domain/type/uuid-type.d.ts +7 -0
- package/dist/esm/domain/type/uuid-type.js +23 -0
- package/dist/esm/domain/type/value-objects/value-objects.d.ts +16 -0
- package/dist/esm/domain/type/value-objects/value-objects.js +66 -0
- package/dist/esm/exceptions/abstract.exception.d.ts +23 -0
- package/dist/esm/exceptions/abstract.exception.js +62 -0
- package/dist/esm/exceptions/application/aggregate-not-found.exception.d.ts +5 -0
- package/dist/esm/exceptions/application/aggregate-not-found.exception.js +13 -0
- package/dist/esm/exceptions/application/application.exception.d.ts +6 -0
- package/dist/esm/exceptions/application/application.exception.js +13 -0
- package/dist/esm/exceptions/application/concurrency.exception.d.ts +5 -0
- package/dist/esm/exceptions/application/concurrency.exception.js +13 -0
- package/dist/esm/exceptions/application/not-allowed.exception.d.ts +5 -0
- package/dist/esm/exceptions/application/not-allowed.exception.js +13 -0
- package/dist/esm/exceptions/brand/exception-brand.d.ts +6 -0
- package/dist/esm/exceptions/brand/exception-brand.js +25 -0
- package/dist/esm/exceptions/domain/domain.exception.d.ts +6 -0
- package/dist/esm/exceptions/domain/domain.exception.js +13 -0
- package/dist/esm/exceptions/domain/required-value.exception.d.ts +4 -0
- package/dist/esm/exceptions/domain/required-value.exception.js +17 -0
- package/dist/esm/exceptions/domain/type-primitive.exception.d.ts +4 -0
- package/dist/esm/exceptions/domain/type-primitive.exception.js +17 -0
- package/dist/esm/exceptions/domain/validation.exception.d.ts +14 -0
- package/dist/esm/exceptions/domain/validation.exception.js +19 -0
- package/dist/esm/exceptions/exception-code.d.ts +15 -0
- package/dist/esm/exceptions/exception-code.js +16 -0
- package/dist/esm/exceptions/index.d.ts +14 -0
- package/dist/esm/exceptions/index.js +14 -0
- package/dist/esm/exceptions/infrastructure/event-delivery.exception.d.ts +5 -0
- package/dist/esm/exceptions/infrastructure/event-delivery.exception.js +16 -0
- package/dist/esm/exceptions/infrastructure/infrastructure.exception.d.ts +6 -0
- package/dist/esm/exceptions/infrastructure/infrastructure.exception.js +13 -0
- package/dist/esm/exceptions/infrastructure/internal-error.exception.d.ts +5 -0
- package/dist/esm/exceptions/infrastructure/internal-error.exception.js +13 -0
- package/dist/esm/exceptions/infrastructure/unknown-event.exception.d.ts +4 -0
- package/dist/esm/exceptions/infrastructure/unknown-event.exception.js +15 -0
- package/dist/esm/exceptions/received/received-value.d.ts +4 -0
- package/dist/esm/exceptions/received/received-value.js +9 -0
- package/dist/esm/infrastructure/clock/fixed-clock.d.ts +8 -0
- package/dist/esm/infrastructure/clock/fixed-clock.js +15 -0
- package/dist/esm/infrastructure/clock/system-clock.d.ts +4 -0
- package/dist/esm/infrastructure/clock/system-clock.js +5 -0
- package/dist/esm/infrastructure/event/domain-event-bus.d.ts +16 -0
- package/dist/esm/infrastructure/event/domain-event-bus.js +56 -0
- package/dist/esm/infrastructure/event/event-channel.d.ts +9 -0
- package/dist/esm/infrastructure/event/event-channel.js +1 -0
- package/dist/esm/infrastructure/event/event-registry.d.ts +9 -0
- package/dist/esm/infrastructure/event/event-registry.js +39 -0
- package/dist/esm/infrastructure/event/in-memory/emitter.d.ts +11 -0
- package/dist/esm/infrastructure/event/in-memory/emitter.js +50 -0
- package/dist/esm/infrastructure/event/in-memory/in-memory-event-channel.d.ts +16 -0
- package/dist/esm/infrastructure/event/in-memory/in-memory-event-channel.js +47 -0
- package/dist/esm/infrastructure/http/http-error-mapper.d.ts +23 -0
- package/dist/esm/infrastructure/http/http-error-mapper.js +59 -0
- package/dist/esm/infrastructure/index.d.ts +16 -0
- package/dist/esm/infrastructure/index.js +9 -0
- package/dist/esm/infrastructure/logging/console-logger.d.ts +17 -0
- package/dist/esm/infrastructure/logging/console-logger.js +33 -0
- package/dist/esm/infrastructure/logging/in-memory-logger.d.ts +15 -0
- package/dist/esm/infrastructure/logging/in-memory-logger.js +23 -0
- package/dist/esm/infrastructure/logging/log-serializer.d.ts +7 -0
- package/dist/esm/infrastructure/logging/log-serializer.js +55 -0
- package/dist/esm/infrastructure/logging/silent-logger.d.ts +7 -0
- package/dist/esm/infrastructure/logging/silent-logger.js +14 -0
- package/dist/esm/package.json +3 -0
- package/package.json +106 -4
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# Ejemplo: Ventas en la tienda online
|
|
2
|
+
|
|
3
|
+
Objetos del contexto de Ventas, con la decisión razonada. Los límites de agregado se deciden en `ddd-skills/ddd-agregados/GUIA.md`; aquí solo se decide valor o entidad y su comportamiento.
|
|
4
|
+
|
|
5
|
+
| Concepto | Decisión | Razón | Con el paquete |
|
|
6
|
+
|---|---|---|---|
|
|
7
|
+
| `Money` | Value object | Cuantifica; se reemplaza; igualdad por valor | `Composite({ amount: Cents, currency: Currency })` |
|
|
8
|
+
| `Quantity` | Value object | Regla: entero positivo | `Required(NumberType)` + `@Validations` |
|
|
9
|
+
| `Sku` | Value object | Identifica un producto de otro contexto: integración minimalista | `Required(StringType)` |
|
|
10
|
+
| `OrderLine` | Value object | Dentro del pedido se identifica por su `Sku`; nadie la busca por id propio | `Composite` |
|
|
11
|
+
| `OrderLines` | Value object | Reglas de la colección: al menos una línea y un SKU por línea; `total()` no suma monedas distintas | `Composite({ items: [OrderLine] })` |
|
|
12
|
+
| `ShippingAddress` | Value object | Describe a dónde va este pedido; si el cliente cambia de casa, el pedido no cambia | `Composite` |
|
|
13
|
+
| `OrderStatus` | Tipo estándar con comportamiento | Reglas de transición en un solo sitio | `Required(StatusType)`, `StatusType extends EnumType<Status>` |
|
|
14
|
+
| `Order` | Entidad | Ciclo de vida largo, identidad propia, cambia de estado | `AggregateRoot<OrderId, OrderEvent>` |
|
|
15
|
+
|
|
16
|
+
`Money`, `Quantity`, `Sku`, `OrderLine`, `OrderLines`, `OrderStatus`, `OrderId` y `CustomerId` están en `ddd-skills/ddd-value-objects-entidades/references/value-objects-ts.md`. Aquí, la dirección:
|
|
17
|
+
|
|
18
|
+
```ts
|
|
19
|
+
import { Validations } from '@archi-code/validation';
|
|
20
|
+
import { Composite, Required, StringType } from '@archi-code/ddd/domain';
|
|
21
|
+
|
|
22
|
+
export class Street extends Required(StringType) {}
|
|
23
|
+
export class City extends Required(StringType) {}
|
|
24
|
+
|
|
25
|
+
@Validations([{ validator: 'isISO31661Alpha2' }])
|
|
26
|
+
export class Country extends Required(StringType) {
|
|
27
|
+
protected filter(value: unknown): string {
|
|
28
|
+
return super.filter(value).trim().toUpperCase();
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export class ShippingAddress extends Composite({ street: Street, city: City, country: Country }) {}
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
La entidad es el `Order` de `ddd-skills/ddd-agregados/references/implementacion-ts.md`, donde está la clase completa con sus eventos y su reloj; no se redefine aquí. Lo que importa para decidir valor o entidad:
|
|
36
|
+
|
|
37
|
+
```ts
|
|
38
|
+
export class Order extends AggregateRoot<OrderId, OrderEvent> {
|
|
39
|
+
private constructor(
|
|
40
|
+
id: OrderId,
|
|
41
|
+
readonly customerId: CustomerId,
|
|
42
|
+
private _lines: OrderLines,
|
|
43
|
+
readonly shippingAddress: ShippingAddress,
|
|
44
|
+
private _status: OrderStatus,
|
|
45
|
+
private _stockReservation: ReservationId | null,
|
|
46
|
+
private _paymentAuthorization: PaymentAuthorization | null,
|
|
47
|
+
) {
|
|
48
|
+
super(id);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
addLine(line: OrderLine, clock: { now(): Date }): void {
|
|
52
|
+
if (!this._status.isPending()) {
|
|
53
|
+
throw new OrderNoLongerEditable(this.id.value);
|
|
54
|
+
}
|
|
55
|
+
this._lines = this._lines.withLine(line);
|
|
56
|
+
this.record(
|
|
57
|
+
new OrderLineAdded(
|
|
58
|
+
this.id.value,
|
|
59
|
+
line.sku.value,
|
|
60
|
+
line.quantity.value,
|
|
61
|
+
Order.moneyData(line.unitPrice),
|
|
62
|
+
Order.moneyData(this.total()),
|
|
63
|
+
{ clock },
|
|
64
|
+
),
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
total(): Money {
|
|
69
|
+
return this._lines.total();
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
La entidad apenas tiene lógica propia: delega en sus valores (`OrderLines.total`, `OrderLines.withLine`, `OrderStatus.isPending`). Es lo que IDDD llama orientar la entidad a contenedor de valores [IDDD cap. 6]. `addLine` reemplaza el `OrderLines` entero por el que devuelve `withLine`; nunca cambia una línea por dentro.
|
|
75
|
+
|
|
76
|
+
El pedido referencia al cliente por su id (`customerId: CustomerId`), nunca por el objeto. Los eventos que registra (`OrderPlaced`, `OrderLineAdded`…) y el reloj que recibe cada método se explican en `ddd-skills/ddd-eventos-dominio/GUIA.md`.
|
|
77
|
+
|
|
78
|
+
Un test de ida y vuelta por agregado comprueba que se puede guardar y rehacer `[Fuera de los libros: convención de @archi-code/ddd]`:
|
|
79
|
+
|
|
80
|
+
```ts
|
|
81
|
+
import { expect, it } from 'vitest';
|
|
82
|
+
|
|
83
|
+
it('rebuilds the order it stored', () => {
|
|
84
|
+
const order = Order.place(OrderId.generate(), CustomerId.generate(), lines, address, clock);
|
|
85
|
+
expect(Order.fromPrimitives(order.toPrimitives()).toPrimitives()).toEqual(order.toPrimitives());
|
|
86
|
+
});
|
|
87
|
+
```
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
# Entidades en TypeScript
|
|
2
|
+
|
|
3
|
+
## Qué es una entidad
|
|
4
|
+
|
|
5
|
+
Un concepto que se modela como entidad cuando importa su individualidad: hay que distinguirlo de todos los demás y puede cambiar mucho durante mucho tiempo sin dejar de ser el mismo. Identidad única y mutabilidad son lo que la separa de un value object [IDDD cap. 5].
|
|
6
|
+
|
|
7
|
+
Antes de crear una, comprueba si el concepto es en realidad un valor. Y si un sistema solo edita datos, un CRUD puede ser la herramienta adecuada; el problema es aplicar CRUD a sistemas complejos que merecen DDD [IDDD cap. 5].
|
|
8
|
+
|
|
9
|
+
## Descubrir la entidad
|
|
10
|
+
|
|
11
|
+
* Empieza solo por los atributos que dan identidad y los que sirven para encontrarla; ignora el resto hasta tener eso claro [IDDD cap. 5].
|
|
12
|
+
* Luego busca el comportamiento esencial en los requisitos y nómbralo con el lenguaje. En IDDD, "los tenants pueden estar activos o desactivados" lleva a `activate()` y `deactivate()`, no a `setActive(boolean)` [IDDD cap. 5].
|
|
13
|
+
* El lenguaje no son solo sustantivos: adjetivos describen y verbos indican lo que las cosas hacen [IDDD cap. 5].
|
|
14
|
+
|
|
15
|
+
## Identidad
|
|
16
|
+
|
|
17
|
+
### Estrategias [IDDD cap. 5; PHP: Entities]
|
|
18
|
+
|
|
19
|
+
| Estrategia | Cuándo | Cuidado |
|
|
20
|
+
|---|---|---|
|
|
21
|
+
| La aporta el usuario | El negocio exige un identificador legible (código de campaña) | Puede ser única pero incorrecta. Alternativa: guardar el valor del usuario como atributo para búsquedas y usar otra identidad |
|
|
22
|
+
| La genera la aplicación | Opción por defecto | Generador fiable también en despliegues con varios nodos |
|
|
23
|
+
| La genera la persistencia | Secuencias heredadas | Llega tarde (ver abajo); coste de ir a la base por cada id |
|
|
24
|
+
| La asigna otro contexto | El concepto nace fuera (un cliente del CRM) | Sincronización por eventos; úsala lo menos posible |
|
|
25
|
+
|
|
26
|
+
Con el paquete, la identidad generada por la aplicación es un `IdType` (`class OrderId extends IdType<'OrderId'> {}`); la que aporta el usuario o asigna otro contexto, un `Required(StringType)` con nombre y sus reglas (`CampaignCode`, en `ddd-skills/ddd-value-objects-entidades/references/value-objects-ts.md`, sección 3), usado como `AggregateRoot<CampaignCode>`.
|
|
27
|
+
|
|
28
|
+
### Momento: temprana o tardía
|
|
29
|
+
|
|
30
|
+
* **Tardía** (al persistir): el evento de creación se publicaría sin identidad y, si `equals` compara ids, varios objetos nuevos parecerían el mismo en un `Set` [IDDD cap. 5].
|
|
31
|
+
* **Temprana** (al construir): el id se genera antes de crear la entidad y se pasa al constructor [IDDD cap. 5]. IDDD prefiere esta opción frente a hacer `equals` por atributos.
|
|
32
|
+
* Lo genera el propio value object de identidad, nunca el repositorio: el repositorio solo obtiene y persiste, sin `nextId()` `[Fuera de los libros: convención de @archi-code/ddd]`.
|
|
33
|
+
|
|
34
|
+
```ts
|
|
35
|
+
import { IdType } from '@archi-code/ddd/domain';
|
|
36
|
+
|
|
37
|
+
export class OrderId extends IdType<'OrderId'> {}
|
|
38
|
+
|
|
39
|
+
const order = Order.place(OrderId.generate(), customerId, lines, shippingAddress, clock);
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
`XId.generate()` crea un value object de la clase sobre la que se llama (`OrderId.generate()` es un `OrderId`) con un UUID v4. Si conviene orden temporal en índices, `new OrderId(uuidv7())` `[Fuera de los libros]`.
|
|
43
|
+
|
|
44
|
+
### Identidad sustituta
|
|
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`), nunca dentro de `toPrimitives()` ni como campo del agregado.
|
|
47
|
+
|
|
48
|
+
### Estabilidad
|
|
49
|
+
|
|
50
|
+
La identidad no cambia durante la vida de la entidad. IDDD la protege con un setter interno que solo permite asignarla una vez [IDDD cap. 5]. Con el paquete, el id se pasa a `super(id)` y queda como `readonly id`; no se redeclara (`_id` + getter). Además es siempre un value object obligatorio (`IdType` o `Required(...)`): un id sin valor lanza `RequiredValueException`, así que dos entidades nuevas nunca son iguales por un id vacío [IDDD cap. 5].
|
|
51
|
+
|
|
52
|
+
## Patrón de implementación
|
|
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()`) y la `version` para concurrencia optimista, que solo tocan los repositorios.
|
|
55
|
+
|
|
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
|
+
|
|
58
|
+
```ts
|
|
59
|
+
import { BooleanType, Entity, NumberType, Required, StringType } from '@archi-code/ddd/domain';
|
|
60
|
+
import { DomainException } from '@archi-code/ddd/exceptions';
|
|
61
|
+
|
|
62
|
+
export class SeatNumber extends Required(NumberType) {}
|
|
63
|
+
export class SeatLabel extends Required(StringType) {}
|
|
64
|
+
export class Sold extends Required(BooleanType) {}
|
|
65
|
+
|
|
66
|
+
export class Seat extends Entity<SeatNumber> {
|
|
67
|
+
constructor(
|
|
68
|
+
id: SeatNumber,
|
|
69
|
+
private _label: SeatLabel,
|
|
70
|
+
readonly sold: Sold,
|
|
71
|
+
) {
|
|
72
|
+
super(id);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
get label(): SeatLabel {
|
|
76
|
+
return this._label;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
relabel(label: SeatLabel): void {
|
|
80
|
+
if (this.sold.value) {
|
|
81
|
+
throw new DomainException(`Seat ${this.id.value} is already sold`, ['ROOM001']);
|
|
82
|
+
}
|
|
83
|
+
this._label = label;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
new Seat(new SeatNumber(7), new SeatLabel('A1'), new Sold(false)).equals(
|
|
88
|
+
new Seat(new SeatNumber(7), new SeatLabel('B7'), new Sold(true)),
|
|
89
|
+
); // true: misma clase y mismo id
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
Si nadie busca la parte por su id (las líneas de un pedido), no es una entidad hija sino un `Composite` (`ddd-skills/ddd-value-objects-entidades/references/value-objects-ts.md`, sección 7).
|
|
93
|
+
|
|
94
|
+
### Creación frente a reconstitución
|
|
95
|
+
|
|
96
|
+
Separar dos caminos evita que la carga desde base de datos vuelva a disparar reglas de creación o eventos `[Fuera de los libros como patrón explícito; IDDD lo resuelve con acceso por campo del ORM]`. Con el paquete, los dos caminos son una factoría de negocio y `static fromPrimitives(primitives: PrimitiveTypes<X>)`, con su `toPrimitives()` para guardar `[Fuera de los libros: convención de @archi-code/ddd]`:
|
|
97
|
+
|
|
98
|
+
```ts
|
|
99
|
+
import { Validations } from '@archi-code/validation';
|
|
100
|
+
import { AggregateRoot, EnumType, EventBase, Required, StringType } from '@archi-code/ddd/domain';
|
|
101
|
+
import type { EventMetadata, PrimitiveTypes } from '@archi-code/ddd/domain';
|
|
102
|
+
import { DomainException } from '@archi-code/ddd/exceptions';
|
|
103
|
+
|
|
104
|
+
export class CustomerName extends Required(StringType) {}
|
|
105
|
+
|
|
106
|
+
@Validations([{ validator: 'isLength', properties: { min: 3, max: 200 } }])
|
|
107
|
+
export class BlockReason extends Required(StringType) {}
|
|
108
|
+
|
|
109
|
+
export enum CustomerState {
|
|
110
|
+
Active = 'active',
|
|
111
|
+
Blocked = 'blocked',
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export class CustomerStateType extends EnumType<CustomerState> {
|
|
115
|
+
protected getEnum(): typeof CustomerState {
|
|
116
|
+
return CustomerState;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export class CustomerStatus extends Required(CustomerStateType) {
|
|
121
|
+
isBlocked(): boolean {
|
|
122
|
+
return this.value === CustomerState.Blocked;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export class CustomerBlocked extends EventBase {
|
|
127
|
+
static readonly eventName = 'CustomerBlocked';
|
|
128
|
+
|
|
129
|
+
constructor(
|
|
130
|
+
customerId: string,
|
|
131
|
+
readonly reason: string,
|
|
132
|
+
metadata?: EventMetadata,
|
|
133
|
+
) {
|
|
134
|
+
super(customerId, metadata);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export class BlockedCustomerCannotChangeEmail extends DomainException {
|
|
139
|
+
constructor(customerId: string) {
|
|
140
|
+
super(`Customer ${customerId} is blocked and cannot change the email`, ['CUS001']);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export class Customer extends AggregateRoot<CustomerId, CustomerBlocked> {
|
|
145
|
+
private constructor(
|
|
146
|
+
id: CustomerId,
|
|
147
|
+
private _name: CustomerName,
|
|
148
|
+
private _email: Email,
|
|
149
|
+
private _status: CustomerStatus,
|
|
150
|
+
) {
|
|
151
|
+
super(id);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
static register(id: CustomerId, name: CustomerName, email: Email): Customer {
|
|
155
|
+
return new Customer(id, name, email, new CustomerStatus(CustomerState.Active));
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
static fromPrimitives(primitives: PrimitiveTypes<Customer>): Customer {
|
|
159
|
+
return new Customer(
|
|
160
|
+
new CustomerId(primitives.id),
|
|
161
|
+
new CustomerName(primitives.name),
|
|
162
|
+
new Email(primitives.email),
|
|
163
|
+
new CustomerStatus(primitives.status),
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
get name(): CustomerName {
|
|
168
|
+
return this._name;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
get email(): Email {
|
|
172
|
+
return this._email;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
get status(): CustomerStatus {
|
|
176
|
+
return this._status;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
changeEmail(email: Email): void {
|
|
180
|
+
if (this._status.isBlocked()) {
|
|
181
|
+
throw new BlockedCustomerCannotChangeEmail(this.id.value);
|
|
182
|
+
}
|
|
183
|
+
this._email = email;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
block(reason: BlockReason, clock: { now(): Date }): void {
|
|
187
|
+
if (this._status.isBlocked()) {
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
this._status = new CustomerStatus(CustomerState.Blocked);
|
|
191
|
+
this.record(new CustomerBlocked(this.id.value, reason.value, { clock }));
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
toPrimitives(): PrimitiveTypes<Customer> {
|
|
195
|
+
return {
|
|
196
|
+
id: this.id.value,
|
|
197
|
+
name: this._name.value,
|
|
198
|
+
email: this._email.value,
|
|
199
|
+
status: this._status.value,
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
`CustomerId` y `Email` son los de `ddd-skills/ddd-value-objects-entidades/references/value-objects-ts.md`.
|
|
206
|
+
|
|
207
|
+
Fíjate en:
|
|
208
|
+
* Constructor privado y dos factorías con nombre: `register` (lenguaje del negocio, puede registrar eventos) y `fromPrimitives` (técnica, solo para repositorios: rehace cada value object desde su primitivo, que vuelve a validar, y no registra nada).
|
|
209
|
+
* 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
|
+
* Todos los atributos son value objects; cambiar el correo es reemplazar un `Email`.
|
|
211
|
+
* 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 lleva la base y la gestionan los repositorios.
|
|
213
|
+
|
|
214
|
+
## Seguimiento de cambios
|
|
215
|
+
|
|
216
|
+
Una entidad no tiene por qué guardar su historial. Si ciertos cambios importan al negocio, la forma práctica es un evento de dominio por cada comando relevante y un almacén de eventos [IDDD cap. 5]. Ver `ddd-skills/ddd-eventos-dominio/GUIA.md`.
|
|
217
|
+
|
|
218
|
+
## Roles
|
|
219
|
+
|
|
220
|
+
Un mismo objeto puede jugar varios roles; IDDD discute modelarlo con interfaces por rol y advierte de que se puede complicar [IDDD cap. 5]. Antes de hacerlo, revisa si los roles pertenecen a contextos distintos: suele ser el caso (Cliente en Ventas, Destinatario en Almacén).
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# Persistir value objects sin deformar el modelo
|
|
2
|
+
|
|
3
|
+
## Principio
|
|
4
|
+
|
|
5
|
+
El modelo de datos se subordina al de dominio. Claves, integridad referencial e índices no deben dictar cómo se modelan los objetos [IDDD cap. 6]. El informe y la analítica, mejor sobre un modelo de datos propio y no sobre las tablas de producción [IDDD cap. 6].
|
|
6
|
+
|
|
7
|
+
Antes de convertir un valor en entidad "porque tiene tabla", responde [IDDD cap. 6]:
|
|
8
|
+
1. ¿Es una cosa del dominio o mide, cuantifica o describe una cosa?
|
|
9
|
+
2. ¿Tiene la mayoría de las características de valor?
|
|
10
|
+
3. ¿Lo hago entidad solo porque el modelo de datos lo guarda como entidad?
|
|
11
|
+
4. ¿El dominio exige identidad única, me importan los ejemplares y su continuidad de cambio?
|
|
12
|
+
|
|
13
|
+
"Describe, sí, sí, no" → value object.
|
|
14
|
+
|
|
15
|
+
## Opciones
|
|
16
|
+
|
|
17
|
+
| Opción | Cuándo | Notas |
|
|
18
|
+
|---|---|---|
|
|
19
|
+
| Columnas en la fila del padre | Un único value object (`total_amount`, `total_currency`) | La opción por defecto y la más limpia [IDDD cap. 6] |
|
|
20
|
+
| Varios valores serializados en una columna (JSON) | Colecciones pequeñas que no se consultan por dentro | Cuidado con el tamaño de columna y la consulta [IDDD cap. 6]. En PostgreSQL, `jsonb` |
|
|
21
|
+
| Tabla propia para una colección | Colecciones grandes o que se consultan | La fila tiene clave técnica, pero en el modelo sigue siendo valor [IDDD cap. 6] |
|
|
22
|
+
| Tabla de unión | Colecciones referenciadas | Variante de la anterior [IDDD cap. 6] |
|
|
23
|
+
| Estado como enum | Tipos estándar | Guarda el código, no un objeto [IDDD cap. 6] |
|
|
24
|
+
|
|
25
|
+
*DDD in PHP* resume las mismas alternativas (valor embebido, serialización como LOB y mapeo ad hoc con el ORM) [PHP: Value Objects].
|
|
26
|
+
|
|
27
|
+
## En TypeScript
|
|
28
|
+
|
|
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(row).restoreVersion(version)`. 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
|
+
* 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
|
+
* 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
|
+
|
|
34
|
+
```ts
|
|
35
|
+
import type { PrimitiveTypes } from '@archi-code/ddd/domain';
|
|
36
|
+
|
|
37
|
+
type OrderPrimitives = PrimitiveTypes<Order>;
|
|
38
|
+
|
|
39
|
+
interface OrderRow {
|
|
40
|
+
id: string;
|
|
41
|
+
customer_id: string;
|
|
42
|
+
ship_street: string;
|
|
43
|
+
ship_city: string;
|
|
44
|
+
ship_country: string;
|
|
45
|
+
lines: OrderPrimitives['lines']['items'];
|
|
46
|
+
status: string;
|
|
47
|
+
stock_reservation: string | null;
|
|
48
|
+
payment_authorization: OrderPrimitives['paymentAuthorization'];
|
|
49
|
+
version: number;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function rowOf(order: Order): OrderRow {
|
|
53
|
+
const primitives = order.toPrimitives();
|
|
54
|
+
return {
|
|
55
|
+
id: primitives.id,
|
|
56
|
+
customer_id: primitives.customerId,
|
|
57
|
+
ship_street: primitives.shippingAddress.street,
|
|
58
|
+
ship_city: primitives.shippingAddress.city,
|
|
59
|
+
ship_country: primitives.shippingAddress.country,
|
|
60
|
+
lines: primitives.lines.items,
|
|
61
|
+
status: primitives.status,
|
|
62
|
+
stock_reservation: primitives.stockReservation,
|
|
63
|
+
payment_authorization: primitives.paymentAuthorization,
|
|
64
|
+
version: order.version + 1,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function orderOf(row: OrderRow): Order {
|
|
69
|
+
return Order.fromPrimitives({
|
|
70
|
+
id: row.id,
|
|
71
|
+
customerId: row.customer_id,
|
|
72
|
+
lines: { items: row.lines },
|
|
73
|
+
shippingAddress: {
|
|
74
|
+
street: row.ship_street,
|
|
75
|
+
city: row.ship_city,
|
|
76
|
+
country: row.ship_country,
|
|
77
|
+
},
|
|
78
|
+
status: row.status as Status,
|
|
79
|
+
stockReservation: row.stock_reservation,
|
|
80
|
+
paymentAuthorization: row.payment_authorization,
|
|
81
|
+
}).restoreVersion(row.version);
|
|
82
|
+
}
|
|
83
|
+
```
|
|
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; la consulta con la condición de versión y `markPersisted()` están en `ddd-skills/ddd-repositorios-persistencia/GUIA.md`.
|
|
86
|
+
|
|
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
|
+
* Con Prisma o Drizzle, el esquema es de infraestructura y el repositorio sigue pasando por `order.toPrimitives()` y `Order.fromPrimitives()`.
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
# Specification y diseño flexible (Supple Design)
|
|
2
|
+
|
|
3
|
+
Contenido: 1. Diseño flexible · 2. Specification · 3. Specification para consultar · 4. Errores frecuentes
|
|
4
|
+
|
|
5
|
+
## 1. Diseño flexible
|
|
6
|
+
|
|
7
|
+
Conjunto de patrones de Evans para que el modelo sea fácil de usar y de cambiar. IDDD aplica varios de ellos de forma explícita; los que no aparecen en las fuentes del proyecto se marcan `[Fuera de los libros: Evans, Domain-Driven Design, 2003]`.
|
|
8
|
+
|
|
9
|
+
### Intention Revealing Interfaces
|
|
10
|
+
|
|
11
|
+
Los nombres de clases y métodos dicen **qué** consiguen, en el lenguaje ubicuo, no **cómo**. IDDD lo ilustra con `Tenant.activate()` y `deactivate()` en lugar de `setActive(boolean)`: varios setters para una sola petición vuelven ambigua la intención y complican publicar un único evento con significado [IDDD cap. 5, citando a Evans]. IDDD defiende también nombres fluidos (`valuePercentage()` frente a `getValuePercentage()`) [IDDD cap. 6].
|
|
12
|
+
|
|
13
|
+
```ts
|
|
14
|
+
order.setStatus('cancelled');
|
|
15
|
+
price.amount.value * 21 / 100;
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
```ts
|
|
19
|
+
order.cancel(new CancellationReason('customer request'), clock);
|
|
20
|
+
price.percentage('21');
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
El primero revela cómo; el segundo, la intención (`CancellationReason` es un `Required(StringType)` con nombre y `percentage` el de `Money` en `ddd-skills/ddd-value-objects-entidades/references/value-objects-ts.md`).
|
|
24
|
+
|
|
25
|
+
### Side-Effect-Free Functions
|
|
26
|
+
|
|
27
|
+
Las operaciones que devuelven un resultado no cambian estado observable. En los value objects todos los métodos lo son, porque son inmutables [IDDD cap. 6]. Pasa solo valores como argumentos, no entidades: así se puede demostrar que no hay efectos secundarios [IDDD cap. 6]. Aplica también a entidades: separa consultas (devuelven algo, no cambian nada) de comandos (cambian estado, no devuelven datos de negocio).
|
|
28
|
+
|
|
29
|
+
### Assertions
|
|
30
|
+
|
|
31
|
+
`[Fuera de los libros: Evans 2003]`. Cuando una operación sí tiene efectos, haz explícitos sus resultados y las invariantes que mantiene: en los tests (tests de aceptación con lenguaje del negocio) y en guardas del código. IDDD trata las guardas de autoencapsulación como aserciones que protegen el estado [IDDD cap. 5].
|
|
32
|
+
|
|
33
|
+
### Conceptual Contours
|
|
34
|
+
|
|
35
|
+
`[Fuera de los libros: Evans 2003]`. Divide los objetos y operaciones siguiendo las líneas naturales del dominio, de modo que los cambios habituales del negocio afecten a una sola pieza. Señal práctica: si cada cambio de regla toca muchas clases a la vez, los contornos están mal trazados. Es la misma intuición que lleva a los "todos conceptuales" de los value objects [IDDD cap. 6].
|
|
36
|
+
|
|
37
|
+
### Standalone Classes
|
|
38
|
+
|
|
39
|
+
`[Fuera de los libros: Evans 2003]`. Reduce las dependencias de cada clase al mínimo imprescindible. Los value objects que solo dependen de su propio tipo y de sus atributos son el caso ideal, y IDDD recomienda exactamente eso [IDDD cap. 6].
|
|
40
|
+
|
|
41
|
+
### Closure of Operations
|
|
42
|
+
|
|
43
|
+
`[Fuera de los libros: Evans 2003]`. Cuando sea natural, una operación devuelve el mismo tipo que recibe: `Money.add(Money): Money`, `DateRange.extendTo(EndDate): DateRange`, `Specification.and(Specification): Specification`. Permite combinar operaciones sin introducir conceptos nuevos.
|
|
44
|
+
|
|
45
|
+
## 2. Specification
|
|
46
|
+
|
|
47
|
+
Un objeto que responde si otro objeto cumple un criterio del negocio. Evans y Fowler lo describen; IDDD lo propone como forma de implementar validadores de objetos completos [IDDD cap. 5, citando a Evans y Fowler] y *DDD in PHP* lo usa para consultar repositorios [PHP: Repositories; PHP: Factories].
|
|
48
|
+
|
|
49
|
+
Tres usos clásicos `[Fuera de los libros: Evans 2003]`:
|
|
50
|
+
1. **Validar:** ¿este objeto cumple la regla?
|
|
51
|
+
2. **Seleccionar:** ¿qué objetos de una colección la cumplen?
|
|
52
|
+
3. **Construir a la carta:** crear algo que cumpla la regla.
|
|
53
|
+
|
|
54
|
+
Cuándo merece la pena: la regla tiene nombre en el lenguaje ("cliente moroso", "pedido apto para envío urgente"), se usa en varios sitios o se combina con otras. Si es una condición de una línea usada una vez, un método del agregado basta.
|
|
55
|
+
|
|
56
|
+
No se escribe la base ni las combinaciones: `Specification<T>` viene de `@archi-code/ddd/domain`. Declara `isSatisfiedBy` como abstracto y da `and`, `or` y `not`, que devuelven otra `Specification<T>` (clausura de operaciones: se pueden seguir encadenando) y cortan en cuanto la respuesta está decidida. `and` y `or` aceptan cualquier `SatisfiedBy<T>` (un objeto con `isSatisfiedBy`), que es la interfaz `Specification` de los libros [IDDD cap. 5].
|
|
57
|
+
|
|
58
|
+
Reglas de negocio con nombre:
|
|
59
|
+
|
|
60
|
+
```ts
|
|
61
|
+
import { Specification } from '@archi-code/ddd/domain';
|
|
62
|
+
|
|
63
|
+
export interface StockSnapshot {
|
|
64
|
+
covers(sku: Sku, quantity: Quantity): boolean;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export class EligibleForExpressShipping extends Specification<Order> {
|
|
68
|
+
constructor(private readonly centralStock: StockSnapshot) {
|
|
69
|
+
super();
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
isSatisfiedBy(order: Order): boolean {
|
|
73
|
+
return (
|
|
74
|
+
order.shippingAddress.country.value === 'ES' &&
|
|
75
|
+
order.lines.items.every((line) => this.centralStock.covers(line.sku, line.quantity))
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export class OrderAbove extends Specification<Order> {
|
|
81
|
+
constructor(private readonly threshold: Money) {
|
|
82
|
+
super();
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
isSatisfiedBy(order: Order): boolean {
|
|
86
|
+
return order.total().covers(this.threshold);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const freeExpress = new EligibleForExpressShipping(stock).and(
|
|
91
|
+
new OrderAbove(Money.fromPrimitives({ amount: 5000, currency: 'EUR' })),
|
|
92
|
+
);
|
|
93
|
+
freeExpress.isSatisfiedBy(order);
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
`Order`, `Money`, `Sku` y `Quantity` son los de `ddd-skills/ddd-agregados/references/implementacion-ts.md`. La especificación recibe datos de otros agregados ya cargados (`StockSnapshot`), no repositorios: misma regla que para los agregados [IDDD cap. 10].
|
|
97
|
+
|
|
98
|
+
## 3. Specification para consultar
|
|
99
|
+
|
|
100
|
+
*DDD in PHP* propone sustituir los métodos `find...` que crecen sin fin en los repositorios por un método `query(specification)` [PHP: Repositories; PHP: Factories]. Aquí no se hace: el repositorio solo obtiene y persiste, sin lógica ni consultas de negocio `[Fuera de los libros: convención de @archi-code/ddd]`. Una selección con nombre del negocio ("pedidos pendientes desde hace más de un día") es una consulta de lectura: una interfaz en `application/`, implementada en infraestructura directamente sobre la base de datos, que devuelve ids o DTOs.
|
|
101
|
+
|
|
102
|
+
```ts
|
|
103
|
+
export interface StaleOrders {
|
|
104
|
+
ids(pendingSince: Date, limit: number): Promise<OrderId[]>;
|
|
105
|
+
}
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
La interfaz vive en `application/` y su implementación (`PgStaleOrders`, una consulta SQL; los filtros pueden escribirse con `@archi-code/criteria`) en infraestructura. Quien la usa (un programador de tareas, por ejemplo) ejecuta un caso de uso por cada id, y ese caso de uso carga el pedido con `AggregateFinder<Order>`.
|
|
109
|
+
|
|
110
|
+
La Specification se queda para validar, seleccionar sobre datos ya cargados o construir.
|
|
111
|
+
|
|
112
|
+
Úsalo con mesura `[Fuera de los libros]`: si acabas construyendo un lenguaje de consultas genérico, has reinventado el ORM. Para pantallas, mejor consultas de lectura (`ddd-skills/ddd-repositorios-persistencia/GUIA.md`).
|
|
113
|
+
|
|
114
|
+
## 4. Errores frecuentes
|
|
115
|
+
|
|
116
|
+
* Especificaciones genéricas sin nombre de negocio (`FieldEqualsSpecification`).
|
|
117
|
+
* Especificaciones que cargan datos por su cuenta (inyectan repositorios).
|
|
118
|
+
* Usar Specification para invariantes del agregado: la invariante se protege dentro del agregado, siempre; la especificación sirve para validar, seleccionar o construir.
|
|
119
|
+
* Interfaces que revelan intención en el nombre pero no en el comportamiento: `confirm()` que en realidad también envía correos.
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
# Validación
|
|
2
|
+
|
|
3
|
+
## Tres niveles
|
|
4
|
+
|
|
5
|
+
Que todos los atributos sean válidos no implica que el objeto lo sea, y que cada objeto sea válido no implica que su composición lo sea [IDDD cap. 5]. Por eso hay tres niveles [IDDD cap. 5; PHP: Entities]:
|
|
6
|
+
|
|
7
|
+
| Nivel | Qué comprueba | Dónde |
|
|
8
|
+
|---|---|---|
|
|
9
|
+
| Atributo | Un valor aislado: formato, rango, obligatoriedad | Constructor del value object (`Required(...)` y `@Validations`); un `Composite` con reglas `custom` para lo que combina sus campos |
|
|
10
|
+
| Objeto completo | Combinaciones de atributos de una entidad | La propia entidad en sus métodos, o un validador separado en el mismo módulo (un servicio de dominio o `Specification`s) |
|
|
11
|
+
| Composición | Varios objetos o agregados juntos | Servicio de dominio que recibe lo cargado o lee por `AggregateFinder` |
|
|
12
|
+
|
|
13
|
+
IDDD llama a lo del primer nivel más bien **aserciones** que protegen el estado, no "validación" en el sentido de un framework [IDDD cap. 5]. Con el paquete, un value object que existe es válido: su constructor convierte, comprueba `Required` y ejecuta todas sus reglas, y si falla alguna lanza una `ValidationException` con todas `[Fuera de los libros: convención de @archi-code/ddd]`. A eso se suma la autorización, que no es validación del modelo: la comprueba el caso de uso (`NotAllowedException`).
|
|
14
|
+
|
|
15
|
+
## Objeto completo: ¿dentro o fuera de la entidad?
|
|
16
|
+
|
|
17
|
+
IDDD advierte que meter toda la validación en la entidad le da demasiadas responsabilidades y que la validación suele cambiar más que la entidad; propone un validador separado (Specification o Strategy) en el mismo módulo, con validación diferida [IDDD cap. 5].
|
|
18
|
+
|
|
19
|
+
Criterio práctico `[Fuera de los libros]`:
|
|
20
|
+
* **Invariantes** (lo que nunca puede ser falso: un pedido sin líneas) → en la entidad o el agregado, siempre. Se tratan en `ddd-skills/ddd-agregados/GUIA.md`.
|
|
21
|
+
* **Reglas de completitud o calidad** que dependen del momento ("para publicar, la ficha necesita al menos tres fotos") → validador separado, invocado cuando toca.
|
|
22
|
+
|
|
23
|
+
## Patrón de notificación
|
|
24
|
+
|
|
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
|
+
|
|
27
|
+
* **Entrada de un caso de uso:** `ValueObjects.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; `ValueObjects.buildList` nombra los elementos de una lista por su posición (`lines.1.quantity`).
|
|
28
|
+
|
|
29
|
+
```ts
|
|
30
|
+
import { ValueObjects } from '@archi-code/ddd/domain';
|
|
31
|
+
|
|
32
|
+
const command = ValueObjects.build({
|
|
33
|
+
customerId: () => new CustomerId(input.customerId),
|
|
34
|
+
email: () => new Email(input.email),
|
|
35
|
+
lines: () => ValueObjects.buildList(input.lines, (line) => OrderLine.fromPrimitives(line)),
|
|
36
|
+
});
|
|
37
|
+
// input { customerId: 'x', email: 'ana', lines: [...] } → una ValidationException:
|
|
38
|
+
// errors: [
|
|
39
|
+
// { field: 'customerId', code: 'DOM003', message: 'Validation Error: Expected a valid UUID, but received "x".' },
|
|
40
|
+
// { field: 'email', code: 'isEmail', message: 'Email: Value does not satisfy isEmail' },
|
|
41
|
+
// ]
|
|
42
|
+
```
|
|
43
|
+
|
|
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
|
+
|
|
46
|
+
```ts
|
|
47
|
+
import { ValidationException } from '@archi-code/ddd/exceptions';
|
|
48
|
+
import type { ValidationIssue } from '@archi-code/ddd/exceptions';
|
|
49
|
+
|
|
50
|
+
export class ListingReadyToPublish {
|
|
51
|
+
check(listing: ProductListing): void {
|
|
52
|
+
const issues: ValidationIssue[] = [];
|
|
53
|
+
if (listing.photos.value < 3) {
|
|
54
|
+
issues.push({ field: 'photos', code: 'tooFewPhotos', message: 'At least 3 photos' });
|
|
55
|
+
}
|
|
56
|
+
if (!listing.description.isLongEnough()) {
|
|
57
|
+
issues.push({ field: 'description', code: 'tooShort', message: 'Describe the product' });
|
|
58
|
+
}
|
|
59
|
+
if (listing.price.isZero()) {
|
|
60
|
+
issues.push({ field: 'price', code: 'zeroPrice', message: 'A listing needs a price' });
|
|
61
|
+
}
|
|
62
|
+
if (issues.length > 0) {
|
|
63
|
+
throw new ValidationException(issues);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
`ProductListing` es un agregado con `photos` (`PhotoCount`), `description` (`ListingDescription`, con su método `isLongEnough()`) y `price` (`Money`). Nunca captures la `ValidationException` de un value object para quedarte con un valor inválido.
|
|
70
|
+
|
|
71
|
+
## Validación diferida y estados intermedios
|
|
72
|
+
|
|
73
|
+
Un agregado puede estar en un estado intermedio legítimo en el que no procede validar. Se puede modelar con un estado y publicar un evento cuando la validación ya tiene sentido [IDDD cap. 5].
|
|
74
|
+
|
|
75
|
+
## Interfaz frente a dominio
|
|
76
|
+
|
|
77
|
+
* La interfaz valida formato para dar buena experiencia (campo vacío, longitud). Eso no sustituye a la validación del modelo, donde vive el conocimiento profundo [IDDD cap. 4].
|
|
78
|
+
* No dupliques reglas de negocio en el esquema del DTO. Si el DTO comprueba "email válido" y `new Email(...)` también, que el DTO solo compruebe que es un `string` y deje la regla al value object `[Fuera de los libros]`. La entrada de un caso de uso llega sin validar a propósito: `toCommand` construye los value objects, así que todos los adaptadores pasan por las mismas reglas.
|
|
79
|
+
|
|
80
|
+
## Errores con nombre
|
|
81
|
+
|
|
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/exceptions`) con su código en el segundo argumento.
|
|
84
|
+
* No uses `Error('invalid')` en el dominio.
|
|
85
|
+
|
|
86
|
+
```ts
|
|
87
|
+
import { DomainException } from '@archi-code/ddd/exceptions';
|
|
88
|
+
|
|
89
|
+
export class OrderMustHaveLines extends DomainException {
|
|
90
|
+
constructor() {
|
|
91
|
+
super('An order needs at least one line', ['ORD002']);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
`error.code` es `'ORD002'`. En la entrada, `HttpErrorMapper` (de `@archi-code/ddd/infrastructure`) traduce sin tablas escritas a mano: 422 para una `ValidationException` o cualquier `DomainException`, 400 para un error de conversión u obligatoriedad lanzado solo, y cambios por código cuando el negocio lo pide (`new HttpErrorMapper({ statusByCode: { ORD001: 409 } })`).
|