@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.
Files changed (282) hide show
  1. package/README.md +478 -376
  2. package/SKILL.md +10 -8
  3. package/ddd-skills/archi-code-ddd/imports.md +28 -18
  4. package/ddd-skills/archi-code-ddd/layers.md +68 -53
  5. package/ddd-skills/archi-code-ddd/patterns.md +50 -43
  6. package/ddd-skills/archi-code-ddd/what-not-to-do.md +25 -18
  7. package/ddd-skills/archi-code-ddd/where-each-class-goes.md +11 -10
  8. package/ddd-skills/ddd-agregados/GUIA.md +2 -2
  9. package/ddd-skills/ddd-agregados/references/consistencia-eventual.md +5 -6
  10. package/ddd-skills/ddd-agregados/references/fabricas.md +5 -8
  11. package/ddd-skills/ddd-agregados/references/implementacion-ts.md +22 -27
  12. package/ddd-skills/ddd-arquitectura/GUIA.md +4 -4
  13. package/ddd-skills/ddd-arquitectura/assets/dependency-cruiser.cjs +13 -6
  14. package/ddd-skills/ddd-arquitectura/references/estilos-adicionales.md +2 -2
  15. package/ddd-skills/ddd-arquitectura/references/estructura-hexagonal-ts.md +70 -16
  16. package/ddd-skills/ddd-auditoria-refactorizacion/GUIA.md +1 -1
  17. package/ddd-skills/ddd-auditoria-refactorizacion/references/ejemplo-auditoria.md +9 -11
  18. package/ddd-skills/ddd-auditoria-refactorizacion/references/lista-comprobacion.md +4 -4
  19. package/ddd-skills/ddd-auditoria-refactorizacion/references/plan-migracion.md +2 -2
  20. package/ddd-skills/ddd-context-mapping/GUIA.md +5 -5
  21. package/ddd-skills/ddd-context-mapping/references/anticorrupcion-ts.md +33 -18
  22. package/ddd-skills/ddd-context-mapping/references/estilos-integracion.md +4 -7
  23. package/ddd-skills/ddd-event-sourcing-cqrs/GUIA.md +2 -2
  24. package/ddd-skills/ddd-event-sourcing-cqrs/references/event-sourcing-ts.md +32 -27
  25. package/ddd-skills/ddd-event-sourcing-cqrs/references/proyecciones-cqrs-ts.md +7 -7
  26. package/ddd-skills/ddd-eventos-dominio/GUIA.md +3 -3
  27. package/ddd-skills/ddd-eventos-dominio/references/diseno-eventos.md +15 -16
  28. package/ddd-skills/ddd-eventos-dominio/references/ejemplo-ecommerce.md +11 -11
  29. package/ddd-skills/ddd-eventos-dominio/references/publicacion-ts.md +34 -31
  30. package/ddd-skills/ddd-repositorios-persistencia/GUIA.md +7 -7
  31. package/ddd-skills/ddd-repositorios-persistencia/references/implementacion-ts.md +50 -32
  32. package/ddd-skills/ddd-repositorios-persistencia/references/tests-repositorios.md +31 -17
  33. package/ddd-skills/ddd-repositorios-persistencia/references/transacciones-ts.md +21 -14
  34. package/ddd-skills/ddd-servicios-capa-aplicacion/GUIA.md +2 -2
  35. package/ddd-skills/ddd-servicios-capa-aplicacion/references/capa-aplicacion-ts.md +19 -19
  36. package/ddd-skills/ddd-servicios-capa-aplicacion/references/ejemplo-ecommerce.md +12 -15
  37. package/ddd-skills/ddd-servicios-capa-aplicacion/references/servicios-dominio-ts.md +5 -6
  38. package/ddd-skills/ddd-value-objects-entidades/GUIA.md +3 -3
  39. package/ddd-skills/ddd-value-objects-entidades/references/entidades-ts.md +7 -9
  40. package/ddd-skills/ddd-value-objects-entidades/references/persistencia-valores.md +5 -5
  41. package/ddd-skills/ddd-value-objects-entidades/references/validacion.md +9 -9
  42. package/ddd-skills/ddd-value-objects-entidades/references/value-objects-ts.md +2 -3
  43. package/dist/cjs/application/event/listens-to.js +4 -4
  44. package/dist/cjs/{exceptions/application → application/exceptions}/aggregate-not-found.exception.d.ts +1 -1
  45. package/dist/cjs/{exceptions/application → application/exceptions}/aggregate-not-found.exception.js +4 -4
  46. package/dist/cjs/application/exceptions/application-exception-code.d.ts +7 -0
  47. package/dist/cjs/application/exceptions/application-exception-code.js +11 -0
  48. package/dist/cjs/application/exceptions/application.exception.d.ts +6 -0
  49. package/dist/cjs/application/exceptions/application.exception.js +16 -0
  50. package/dist/cjs/{exceptions/application → application/exceptions}/concurrency.exception.d.ts +1 -1
  51. package/dist/cjs/{exceptions/application → application/exceptions}/concurrency.exception.js +4 -4
  52. package/dist/cjs/{exceptions/application → application/exceptions}/not-allowed.exception.d.ts +1 -1
  53. package/dist/cjs/{exceptions/application → application/exceptions}/not-allowed.exception.js +4 -4
  54. package/dist/cjs/application/exceptions/unexpected-error.exception.d.ts +5 -0
  55. package/dist/cjs/application/exceptions/unexpected-error.exception.js +17 -0
  56. package/dist/cjs/application/index.d.ts +7 -0
  57. package/dist/cjs/application/index.js +15 -1
  58. package/dist/cjs/application/use-case/use-case.js +3 -2
  59. package/dist/{esm/domain/type/value-objects/value-objects.d.ts → cjs/application/value-object-builder/value-object-builder.d.ts} +3 -3
  60. package/dist/cjs/application/value-object-builder/value-object-builder.js +51 -0
  61. package/dist/cjs/domain/aggregate/aggregate-root.d.ts +0 -5
  62. package/dist/cjs/domain/aggregate/aggregate-root.js +4 -20
  63. package/dist/cjs/domain/entity/entity.js +2 -2
  64. package/dist/cjs/domain/event/event-base.d.ts +3 -9
  65. package/dist/cjs/domain/event/event-base.js +8 -38
  66. package/dist/cjs/domain/exceptions/domain-exception-code.d.ts +7 -0
  67. package/dist/cjs/domain/exceptions/domain-exception-code.js +11 -0
  68. package/dist/cjs/domain/exceptions/domain.exception.d.ts +6 -0
  69. package/dist/cjs/domain/exceptions/domain.exception.js +16 -0
  70. package/dist/cjs/domain/exceptions/invalid-event-definition.exception.d.ts +4 -0
  71. package/dist/cjs/domain/exceptions/invalid-event-definition.exception.js +19 -0
  72. package/dist/cjs/{exceptions/domain → domain/exceptions}/required-value.exception.js +5 -6
  73. package/dist/cjs/{exceptions/domain → domain/exceptions}/type-primitive.exception.js +5 -6
  74. package/dist/cjs/{exceptions/domain → domain/exceptions}/validation.exception.js +5 -5
  75. package/dist/cjs/domain/index.d.ts +8 -1
  76. package/dist/cjs/domain/index.js +14 -2
  77. package/dist/cjs/domain/primitive/primitive-types.d.ts +1 -3
  78. package/dist/cjs/domain/type/abstract-type.d.ts +6 -8
  79. package/dist/cjs/domain/type/abstract-type.js +15 -23
  80. package/dist/cjs/domain/type/array-type.d.ts +2 -1
  81. package/dist/cjs/domain/type/array-type.js +17 -18
  82. package/dist/cjs/domain/type/boolean-type.js +2 -2
  83. package/dist/cjs/domain/type/composite/composite-brand.d.ts +4 -0
  84. package/dist/cjs/domain/type/composite/composite-brand.js +15 -0
  85. package/dist/cjs/domain/type/composite/composite-field.d.ts +7 -8
  86. package/dist/cjs/domain/type/composite/composite-field.js +70 -47
  87. package/dist/cjs/domain/type/composite/composite-issues.d.ts +6 -0
  88. package/dist/cjs/domain/type/composite/composite-issues.js +35 -0
  89. package/dist/cjs/domain/type/composite/composite.js +15 -19
  90. package/dist/cjs/domain/type/date-type.d.ts +3 -2
  91. package/dist/cjs/domain/type/date-type.js +42 -7
  92. package/dist/cjs/domain/type/enum-type.js +2 -2
  93. package/dist/cjs/domain/type/id-type.d.ts +1 -1
  94. package/dist/cjs/domain/type/id-type.js +4 -4
  95. package/dist/cjs/domain/type/index.d.ts +2 -4
  96. package/dist/cjs/domain/type/index.js +3 -7
  97. package/dist/cjs/domain/type/json-type.d.ts +4 -0
  98. package/dist/cjs/domain/type/json-type.js +33 -4
  99. package/dist/cjs/domain/type/number-type.js +2 -2
  100. package/dist/cjs/domain/type/required/required-mark.d.ts +0 -1
  101. package/dist/cjs/domain/type/string-type.js +2 -2
  102. package/dist/cjs/domain/type/uuid-type.js +2 -2
  103. package/dist/cjs/infrastructure/event/domain-event-bus.js +9 -9
  104. package/dist/cjs/infrastructure/event/event-registry.js +9 -9
  105. package/dist/cjs/infrastructure/event/in-memory/emitter.js +2 -2
  106. package/dist/cjs/infrastructure/event/in-memory/in-memory-event-channel.d.ts +1 -1
  107. package/dist/cjs/infrastructure/event/in-memory/in-memory-event-channel.js +6 -7
  108. package/dist/cjs/{exceptions/infrastructure → infrastructure/exceptions}/event-delivery.exception.js +4 -4
  109. package/dist/cjs/infrastructure/exceptions/infrastructure-exception-code.d.ts +6 -0
  110. package/dist/cjs/infrastructure/exceptions/infrastructure-exception-code.js +10 -0
  111. package/dist/cjs/infrastructure/exceptions/infrastructure.exception.d.ts +6 -0
  112. package/dist/cjs/infrastructure/exceptions/infrastructure.exception.js +16 -0
  113. package/dist/cjs/{exceptions/infrastructure → infrastructure/exceptions}/internal-error.exception.d.ts +1 -1
  114. package/dist/cjs/{exceptions/infrastructure → infrastructure/exceptions}/internal-error.exception.js +4 -4
  115. package/dist/cjs/infrastructure/exceptions/kind/exception-classifier.d.ts +4 -0
  116. package/dist/cjs/infrastructure/exceptions/kind/exception-classifier.js +34 -0
  117. package/dist/cjs/infrastructure/exceptions/kind/exception-kind.d.ts +9 -0
  118. package/dist/cjs/infrastructure/exceptions/kind/exception-kind.js +13 -0
  119. package/dist/cjs/{exceptions/infrastructure → infrastructure/exceptions}/unknown-event.exception.js +4 -4
  120. package/dist/cjs/infrastructure/index.d.ts +7 -2
  121. package/dist/cjs/infrastructure/index.js +15 -3
  122. package/dist/cjs/infrastructure/logging/console-logger.js +2 -2
  123. package/dist/{esm → cjs/shared}/exceptions/abstract.exception.d.ts +6 -4
  124. package/dist/cjs/{exceptions → shared/exceptions}/abstract.exception.js +9 -16
  125. package/dist/cjs/shared/index.d.ts +8 -0
  126. package/dist/cjs/shared/index.js +13 -0
  127. package/dist/cjs/shared/issue-collection-error.d.ts +9 -0
  128. package/dist/cjs/shared/issue-collection-error.js +11 -0
  129. package/dist/cjs/shared/issue-collector.d.ts +8 -0
  130. package/dist/cjs/shared/issue-collector.js +41 -0
  131. package/dist/esm/application/event/listens-to.js +1 -1
  132. package/dist/esm/{exceptions/application → application/exceptions}/aggregate-not-found.exception.d.ts +1 -1
  133. package/dist/esm/application/exceptions/aggregate-not-found.exception.js +13 -0
  134. package/dist/esm/application/exceptions/application-exception-code.d.ts +7 -0
  135. package/dist/esm/application/exceptions/application-exception-code.js +8 -0
  136. package/dist/esm/application/exceptions/application.exception.d.ts +6 -0
  137. package/dist/esm/application/exceptions/application.exception.js +12 -0
  138. package/dist/esm/{exceptions/application → application/exceptions}/concurrency.exception.d.ts +1 -1
  139. package/dist/esm/{exceptions/application → application/exceptions}/concurrency.exception.js +4 -4
  140. package/dist/esm/{exceptions/application → application/exceptions}/not-allowed.exception.d.ts +1 -1
  141. package/dist/esm/application/exceptions/not-allowed.exception.js +13 -0
  142. package/dist/esm/application/exceptions/unexpected-error.exception.d.ts +5 -0
  143. package/dist/esm/application/exceptions/unexpected-error.exception.js +13 -0
  144. package/dist/esm/application/index.d.ts +7 -0
  145. package/dist/esm/application/index.js +7 -0
  146. package/dist/esm/application/use-case/use-case.js +3 -2
  147. package/dist/{cjs/domain/type/value-objects/value-objects.d.ts → esm/application/value-object-builder/value-object-builder.d.ts} +3 -3
  148. package/dist/esm/application/value-object-builder/value-object-builder.js +47 -0
  149. package/dist/esm/domain/aggregate/aggregate-root.d.ts +0 -5
  150. package/dist/esm/domain/aggregate/aggregate-root.js +4 -20
  151. package/dist/esm/domain/entity/entity.js +1 -1
  152. package/dist/esm/domain/event/event-base.d.ts +3 -9
  153. package/dist/esm/domain/event/event-base.js +8 -38
  154. package/dist/esm/domain/exceptions/domain-exception-code.d.ts +7 -0
  155. package/dist/esm/domain/exceptions/domain-exception-code.js +8 -0
  156. package/dist/esm/domain/exceptions/domain.exception.d.ts +6 -0
  157. package/dist/esm/domain/exceptions/domain.exception.js +12 -0
  158. package/dist/esm/domain/exceptions/invalid-event-definition.exception.d.ts +4 -0
  159. package/dist/esm/domain/exceptions/invalid-event-definition.exception.js +15 -0
  160. package/dist/esm/{exceptions/domain → domain/exceptions}/required-value.exception.js +4 -5
  161. package/dist/esm/{exceptions/domain → domain/exceptions}/type-primitive.exception.js +4 -5
  162. package/dist/esm/{exceptions/domain → domain/exceptions}/validation.exception.js +5 -5
  163. package/dist/esm/domain/index.d.ts +7 -0
  164. package/dist/esm/domain/index.js +6 -0
  165. package/dist/esm/domain/primitive/primitive-types.d.ts +1 -3
  166. package/dist/esm/domain/type/abstract-type.d.ts +6 -8
  167. package/dist/esm/domain/type/abstract-type.js +14 -22
  168. package/dist/esm/domain/type/array-type.d.ts +2 -1
  169. package/dist/esm/domain/type/array-type.js +16 -17
  170. package/dist/esm/domain/type/boolean-type.js +1 -1
  171. package/dist/esm/domain/type/composite/composite-brand.d.ts +4 -0
  172. package/dist/esm/domain/type/composite/composite-brand.js +11 -0
  173. package/dist/esm/domain/type/composite/composite-field.d.ts +7 -8
  174. package/dist/esm/domain/type/composite/composite-field.js +69 -46
  175. package/dist/esm/domain/type/composite/composite-issues.d.ts +6 -0
  176. package/dist/esm/domain/type/composite/composite-issues.js +31 -0
  177. package/dist/esm/domain/type/composite/composite.js +13 -17
  178. package/dist/esm/domain/type/date-type.d.ts +3 -2
  179. package/dist/esm/domain/type/date-type.js +39 -4
  180. package/dist/esm/domain/type/enum-type.js +1 -1
  181. package/dist/esm/domain/type/id-type.d.ts +1 -1
  182. package/dist/esm/domain/type/id-type.js +2 -2
  183. package/dist/esm/domain/type/index.d.ts +2 -4
  184. package/dist/esm/domain/type/index.js +1 -3
  185. package/dist/esm/domain/type/json-type.d.ts +4 -0
  186. package/dist/esm/domain/type/json-type.js +32 -3
  187. package/dist/esm/domain/type/number-type.js +1 -1
  188. package/dist/esm/domain/type/required/required-mark.d.ts +0 -1
  189. package/dist/esm/domain/type/string-type.js +1 -1
  190. package/dist/esm/domain/type/uuid-type.js +1 -1
  191. package/dist/esm/infrastructure/event/domain-event-bus.js +5 -5
  192. package/dist/esm/infrastructure/event/event-registry.js +4 -4
  193. package/dist/esm/infrastructure/event/in-memory/emitter.js +1 -1
  194. package/dist/esm/infrastructure/event/in-memory/in-memory-event-channel.d.ts +1 -1
  195. package/dist/esm/infrastructure/event/in-memory/in-memory-event-channel.js +5 -6
  196. package/dist/esm/{exceptions/infrastructure → infrastructure/exceptions}/event-delivery.exception.js +4 -4
  197. package/dist/esm/infrastructure/exceptions/infrastructure-exception-code.d.ts +6 -0
  198. package/dist/esm/infrastructure/exceptions/infrastructure-exception-code.js +7 -0
  199. package/dist/esm/infrastructure/exceptions/infrastructure.exception.d.ts +6 -0
  200. package/dist/esm/infrastructure/exceptions/infrastructure.exception.js +12 -0
  201. package/dist/esm/{exceptions/infrastructure → infrastructure/exceptions}/internal-error.exception.d.ts +1 -1
  202. package/dist/esm/infrastructure/exceptions/internal-error.exception.js +13 -0
  203. package/dist/esm/infrastructure/exceptions/kind/exception-classifier.d.ts +4 -0
  204. package/dist/esm/infrastructure/exceptions/kind/exception-classifier.js +30 -0
  205. package/dist/esm/infrastructure/exceptions/kind/exception-kind.d.ts +9 -0
  206. package/dist/esm/infrastructure/exceptions/kind/exception-kind.js +10 -0
  207. package/dist/esm/{exceptions/infrastructure → infrastructure/exceptions}/unknown-event.exception.js +4 -4
  208. package/dist/esm/infrastructure/index.d.ts +7 -2
  209. package/dist/esm/infrastructure/index.js +7 -1
  210. package/dist/esm/infrastructure/logging/console-logger.js +1 -1
  211. package/dist/{cjs → esm/shared}/exceptions/abstract.exception.d.ts +6 -4
  212. package/dist/esm/{exceptions → shared/exceptions}/abstract.exception.js +9 -16
  213. package/dist/esm/shared/index.d.ts +8 -0
  214. package/dist/esm/shared/index.js +5 -0
  215. package/dist/esm/shared/issue-collection-error.d.ts +9 -0
  216. package/dist/esm/shared/issue-collection-error.js +7 -0
  217. package/dist/esm/shared/issue-collector.d.ts +8 -0
  218. package/dist/esm/shared/issue-collector.js +37 -0
  219. package/package.json +7 -18
  220. package/dist/cjs/domain/type/equality/value-equality.d.ts +0 -7
  221. package/dist/cjs/domain/type/equality/value-equality.js +0 -41
  222. package/dist/cjs/domain/type/immutable/immutable-date.d.ts +0 -4
  223. package/dist/cjs/domain/type/immutable/immutable-date.js +0 -37
  224. package/dist/cjs/domain/type/immutable/immutable.d.ts +0 -4
  225. package/dist/cjs/domain/type/immutable/immutable.js +0 -19
  226. package/dist/cjs/domain/type/value-objects/value-objects.js +0 -70
  227. package/dist/cjs/exceptions/application/application.exception.d.ts +0 -6
  228. package/dist/cjs/exceptions/application/application.exception.js +0 -17
  229. package/dist/cjs/exceptions/domain/domain.exception.d.ts +0 -6
  230. package/dist/cjs/exceptions/domain/domain.exception.js +0 -17
  231. package/dist/cjs/exceptions/exception-code.d.ts +0 -15
  232. package/dist/cjs/exceptions/exception-code.js +0 -19
  233. package/dist/cjs/exceptions/index.d.ts +0 -14
  234. package/dist/cjs/exceptions/index.js +0 -30
  235. package/dist/cjs/exceptions/infrastructure/infrastructure.exception.d.ts +0 -6
  236. package/dist/cjs/exceptions/infrastructure/infrastructure.exception.js +0 -17
  237. package/dist/cjs/infrastructure/http/http-error-mapper.d.ts +0 -23
  238. package/dist/cjs/infrastructure/http/http-error-mapper.js +0 -63
  239. package/dist/esm/domain/type/equality/value-equality.d.ts +0 -7
  240. package/dist/esm/domain/type/equality/value-equality.js +0 -37
  241. package/dist/esm/domain/type/immutable/immutable-date.d.ts +0 -4
  242. package/dist/esm/domain/type/immutable/immutable-date.js +0 -33
  243. package/dist/esm/domain/type/immutable/immutable.d.ts +0 -4
  244. package/dist/esm/domain/type/immutable/immutable.js +0 -15
  245. package/dist/esm/domain/type/value-objects/value-objects.js +0 -66
  246. package/dist/esm/exceptions/application/aggregate-not-found.exception.js +0 -13
  247. package/dist/esm/exceptions/application/application.exception.d.ts +0 -6
  248. package/dist/esm/exceptions/application/application.exception.js +0 -13
  249. package/dist/esm/exceptions/application/not-allowed.exception.js +0 -13
  250. package/dist/esm/exceptions/domain/domain.exception.d.ts +0 -6
  251. package/dist/esm/exceptions/domain/domain.exception.js +0 -13
  252. package/dist/esm/exceptions/exception-code.d.ts +0 -15
  253. package/dist/esm/exceptions/exception-code.js +0 -16
  254. package/dist/esm/exceptions/index.d.ts +0 -14
  255. package/dist/esm/exceptions/index.js +0 -14
  256. package/dist/esm/exceptions/infrastructure/infrastructure.exception.d.ts +0 -6
  257. package/dist/esm/exceptions/infrastructure/infrastructure.exception.js +0 -13
  258. package/dist/esm/exceptions/infrastructure/internal-error.exception.js +0 -13
  259. package/dist/esm/infrastructure/http/http-error-mapper.d.ts +0 -23
  260. package/dist/esm/infrastructure/http/http-error-mapper.js +0 -59
  261. /package/dist/cjs/{exceptions/domain → domain/exceptions}/required-value.exception.d.ts +0 -0
  262. /package/dist/cjs/{exceptions/domain → domain/exceptions}/type-primitive.exception.d.ts +0 -0
  263. /package/dist/cjs/{exceptions/domain → domain/exceptions}/validation.exception.d.ts +0 -0
  264. /package/dist/cjs/domain/type/required/{required.d.ts → required-type.d.ts} +0 -0
  265. /package/dist/cjs/domain/type/required/{required.js → required-type.js} +0 -0
  266. /package/dist/cjs/{exceptions/infrastructure → infrastructure/exceptions}/event-delivery.exception.d.ts +0 -0
  267. /package/dist/cjs/{exceptions/infrastructure → infrastructure/exceptions}/unknown-event.exception.d.ts +0 -0
  268. /package/dist/cjs/{exceptions → shared/exceptions}/brand/exception-brand.d.ts +0 -0
  269. /package/dist/cjs/{exceptions → shared/exceptions}/brand/exception-brand.js +0 -0
  270. /package/dist/cjs/{exceptions → shared/exceptions}/received/received-value.d.ts +0 -0
  271. /package/dist/cjs/{exceptions → shared/exceptions}/received/received-value.js +0 -0
  272. /package/dist/esm/{exceptions/domain → domain/exceptions}/required-value.exception.d.ts +0 -0
  273. /package/dist/esm/{exceptions/domain → domain/exceptions}/type-primitive.exception.d.ts +0 -0
  274. /package/dist/esm/{exceptions/domain → domain/exceptions}/validation.exception.d.ts +0 -0
  275. /package/dist/esm/domain/type/required/{required.d.ts → required-type.d.ts} +0 -0
  276. /package/dist/esm/domain/type/required/{required.js → required-type.js} +0 -0
  277. /package/dist/esm/{exceptions/infrastructure → infrastructure/exceptions}/event-delivery.exception.d.ts +0 -0
  278. /package/dist/esm/{exceptions/infrastructure → infrastructure/exceptions}/unknown-event.exception.d.ts +0 -0
  279. /package/dist/esm/{exceptions → shared/exceptions}/brand/exception-brand.d.ts +0 -0
  280. /package/dist/esm/{exceptions → shared/exceptions}/brand/exception-brand.js +0 -0
  281. /package/dist/esm/{exceptions → shared/exceptions}/received/received-value.d.ts +0 -0
  282. /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 ValueObjects.build({
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
- aggregateId: orderId,
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` que produjo `toJSON()`.
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, InternalErrorException } from '@archi-code/ddd/exceptions';
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
- return rows[0] ? Order.fromPrimitives(rows[0].data).restoreVersion(rows[0].version) : null;
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.toJSON());
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
- order.version === 0
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, order.version, primitives.status, primitives],
232
+ [primitives.id, version, primitives.status, primitives],
227
233
  );
228
234
  if (rowCount === 0) {
229
- throw new ConcurrencyException('Order', order.id.value, order.version);
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, event_version, occurred_on, body)
235
- VALUES ($1, $2, $3, $4, $5, $6)`,
240
+ (event_id, aggregate_id, event_name, occurred_on, body)
241
+ VALUES ($1, $2, $3, $4, $5)`,
236
242
  [
237
243
  event.eventId,
238
- event.aggregateId,
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
- order.markPersisted();
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]. `markPersisted()` se llama 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`.
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/exceptions';
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 las clases ni el `toJSON()` del dominio: 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:
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.aggregateId,
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.aggregateId,
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 y la versión del contrato (`sales.order.OrderPlaced`, v1), 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.
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, ValueObjects } from '@archi-code/ddd/domain';
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 ValueObjects.build({
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/exceptions';
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 `aggregateId` (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].
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 `order.version + 1` al lado y carga con `Order.fromPrimitives(row).restoreVersion(row.version)`. 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 vive en la base `AggregateRoot` (`version`, `restoreVersion()`, `markPersisted()`): el agregado no la declara ni la mapea, y solo los repositorios la tocan. 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.
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, comprobación de versión, `markPersisted()`), 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.
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 y llama a `markPersisted()` |
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).restoreVersion(version)` `[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, y tras escribir el repositorio llama a `order.markPersisted()`.
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, `fromPrimitives(row)` sin la versión almacenada o `markPersisted()` olvidado tras escribir.
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 `PrimitiveTypes` la deja fuera `[Fuera de los libros: convención de @archi-code/ddd]`.
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: un agregado nuevo tiene `version` 0; al insertarlo se guarda con 1 y `markPersisted()` lo lleva a 1. Cada actualización exige que la versión almacenada coincida con `order.version`, guarda `order.version + 1` y llama a `markPersisted()`. Al cargar, el repositorio llama a `Order.fromPrimitives(primitives).restoreVersion(storedVersion)`. El repositorio llama a `markPersisted()` solo cuando la escritura se completó (en SQL, tras el `COMMIT`); si `save` lanza, el objeto conserva su versión y el caso de uso lo descarta y vuelve a cargarlo.
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 `order.version + 1` al lado y, tras escribir, llama a `order.markPersisted()`.
39
- * Al leer: `Order.fromPrimitives(row).restoreVersion(row.version)`.
40
- * En un conflicto: `throw new ConcurrencyException('Order', key, order.version)`, con el nombre del agregado como texto literal, no como nombre de clase: los minificadores cambian los nombres de las clases.
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, InternalErrorException } from '@archi-code/ddd/exceptions';
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
- return Order.fromPrimitives(primitives).restoreVersion(version);
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: order.version + 1,
90
+ version: version + 1,
85
91
  };
86
- const written = await this.write(order, async () => {
87
- if (order.version === 0) {
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: order.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, order.version);
104
+ throw new ConcurrencyException('Order', order.id.value, version);
99
105
  }
100
- order.markPersisted();
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 (order.version === 0) {
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: order.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, order.version);
120
+ throw new ConcurrencyException('Order', order.id.value, version);
114
121
  }
115
122
  }
116
123
 
117
- private async write(order: Order, operation: () => Promise<boolean>): Promise<boolean> {
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, order.version, {
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: order.version }`; si no casa ningún documento, otro guardó antes.
134
- * Un conflicto es `ConcurrencyException`: el caso de uso puede reintentar volviendo a cargar y a guardar, y `HttpErrorMapper` responde 409, no 500.
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, InternalErrorException } from '@archi-code/ddd/exceptions';
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
- return rows[0] ? Order.fromPrimitives(rows[0].data).restoreVersion(rows[0].version) : null;
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
- order.version === 0
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, order.version, primitives.status, primitives],
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, order.version);
211
+ throw new ConcurrencyException('Order', order.id.value, version);
194
212
  }
195
- order.markPersisted();
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).restoreVersion(row.version)`, nunca modelos de Prisma.
206
- * La concurrencia optimista con Prisma: `updateMany({ where: { id, version: order.version }, data: { ..., version: { increment: 1 } } })`; si `count === 0`, `ConcurrencyException('Order', order.id.value, order.version)`; si no, `order.markPersisted()`.
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) y llama a `order.toPrimitives()` y `Order.fromPrimitives(row).restoreVersion(version)`, igual que un adaptador real.
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/exceptions';
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, { version: number; primitives: PrimitiveTypes<Order> }>();
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 = this.currentKey(order);
36
- this.rows.set(key, { version: order.version + 1, primitives: structuredClone(order.toPrimitives()) });
37
- order.markPersisted();
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.rows.delete(this.currentKey(order));
47
+ this.checkedVersion(order);
48
+ this.rows.delete(order.id.value);
42
49
  }
43
50
 
44
- private currentKey(order: Order): string {
51
+ private checkedVersion(order: Order): number {
45
52
  const key = order.id.value;
46
- if ((this.rows.get(key)?.version ?? 0) !== order.version) {
47
- throw new ConcurrencyException('Order', key, order.version);
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 key;
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).restoreVersion(version)`: cambiar un agregado sin `save` no persiste nada.
55
- * Sigue el mismo contrato de concurrencia que un repositorio real: `save` y `remove` lanzan `ConcurrencyException` si la versión almacenada no coincide con `order.version` (también al dar de alta un id ocupado), y tras guardar llama a `markPersisted()`.
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/exceptions';
84
+ import { ConcurrencyException } from '@archi-code/ddd/application';
71
85
  import { FixedClock } from '@archi-code/ddd/infrastructure';
72
86
  import {
73
87
  CustomerId,