@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
package/README.md CHANGED
@@ -2,17 +2,17 @@
2
2
 
3
3
  Domain-Driven Design building blocks for TypeScript, one entry point per layer:
4
4
 
5
- | Entry point | Layer | What it gives you |
6
- | -------------------------------- | -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
7
- | `@archi-code/ddd/exceptions` | Shared | Exceptions of every layer with codes, causes and `instanceof` across builds |
8
- | `@archi-code/ddd/domain` | Domain | Value objects, `Required(...)`, `Composite(...)`, `Immutable`, `Entity`, `AggregateRoot`, `EventBase`, `Specification` and the repository contracts (`AggregateFinder`, `AggregateSaver`, `AggregateRemover`) |
9
- | `@archi-code/ddd/application` | Application | `UseCase`, the event contract (`EventListener`, `@ListensTo` to listen, `EventBus` to publish) and the ports `Clock` and `Logger` |
10
- | `@archi-code/ddd/infrastructure` | Infrastructure | The event wiring (`DomainEventBus`, `EventRegistry`, the `EventChannel` port, `InMemoryEventChannel`), `HttpErrorMapper`, clocks, loggers |
11
-
12
- The layers only depend inwards: `exceptions` imports nothing, `domain` imports only `exceptions`,
13
- `application` imports `domain` and `exceptions`, and `infrastructure` imports all three. The
14
- exceptions are a module of their own because every layer throws them: a use case wraps unknown
15
- errors in an infrastructure exception, a use case throws `AggregateNotFoundException`. Import each file of your project from the layer it
5
+ | Entry point | Layer | What it gives you |
6
+ | -------------------------------- | -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
7
+ | `@archi-code/ddd/domain` | Domain | Value objects, `Required(...)`, `Composite(...)`, `Entity`, `AggregateRoot`, `EventBase`, `Specification`, the repository contracts (`AggregateFinder`, `AggregateSaver`, `AggregateRemover`) and the domain exceptions (`DomainException`, `ValidationException`…) |
8
+ | `@archi-code/ddd/application` | Application | `UseCase`, `ValueObjectBuilder`, the event contract (`EventListener`, `@ListensTo` to listen, `EventBus` to publish), the ports `Clock` and `Logger` and the application exceptions (`AggregateNotFoundException`, `NotAllowedException`…) |
9
+ | `@archi-code/ddd/infrastructure` | Infrastructure | The event wiring (`DomainEventBus`, `EventRegistry`, the `EventChannel` port, `InMemoryEventChannel`), clocks, loggers, the infrastructure exceptions and `ExceptionClassifier`, which tells any entry point the `ExceptionKind` of an error |
10
+
11
+ The layers only depend inwards: `domain` imports nothing of the package, `application` imports
12
+ `domain`, and `infrastructure` imports both. Each layer declares and exports its own exceptions,
13
+ with codes, causes and `instanceof` across builds: a value object throws a `DomainException`, a
14
+ use case throws `AggregateNotFoundException` and wraps unknown errors in
15
+ `UnexpectedErrorException`, an adapter throws an `InfrastructureException`. Import each file of your project from the layer it
16
16
  belongs to and the same rule holds in your code (see [Architecture rules](#architecture-rules)). Conversions and validations come from
17
17
  [`@archi-code/validation`](https://www.npmjs.com/package/@archi-code/validation). The package is
18
18
  published with CJS and ESM builds.
@@ -23,7 +23,10 @@ published with CJS and ESM builds.
23
23
  npm install @archi-code/ddd
24
24
  ```
25
25
 
26
- - Node.js `>=24` (`engines` field).
26
+ - Node.js `>=24` (`engines` field) and browsers: every subpath runs unchanged in both. Its ESM
27
+ build imports `@archi-code/validation` and `uuid` by name, so in a browser use a bundler (Vite,
28
+ webpack, esbuild…) or an import map. `/domain`, `/application` and `/infrastructure` are checked
29
+ in Chromium and WebKit, `/domain` under a strict Content-Security-Policy without `unsafe-eval`.
27
30
  - Runtime dependencies, installed by npm automatically: `@archi-code/validation` and `uuid`.
28
31
  - To declare validations with `@Validations`, also install `@archi-code/validation` as a direct
29
32
  dependency of your project (the decorator is imported from it):
@@ -49,11 +52,11 @@ There is no root entry point: import from the layer.
49
52
  ```ts
50
53
  // ESM
51
54
  import { AggregateRoot, EventBase, IdType, Required, StringType } from '@archi-code/ddd/domain';
52
- import { DomainException, ExceptionCode } from '@archi-code/ddd/exceptions';
55
+ import { DomainException, DomainExceptionCode } from '@archi-code/ddd/domain';
53
56
  import type { AggregateSaver } from '@archi-code/ddd/domain';
54
57
  import { EventListener, ListensTo, UseCase } from '@archi-code/ddd/application';
55
58
  import type { EventBus } from '@archi-code/ddd/application';
56
- import { HttpErrorMapper } from '@archi-code/ddd/infrastructure';
59
+ import { ExceptionClassifier } from '@archi-code/ddd/infrastructure';
57
60
  ```
58
61
 
59
62
  ```js
@@ -67,43 +70,44 @@ subpaths also resolve with `"moduleResolution": "node"` (through `typesVersions`
67
70
 
68
71
  Both builds can be loaded in the same process (for example a CommonJS application that uses an ESM
69
72
  library built on this package): the exceptions of one build are `instanceof` the classes of the other
70
- (see [Exceptions](#exceptions-archi-codedddexceptions)).
73
+ (see [Exceptions](#exceptions)).
71
74
 
72
75
  ## Architecture rules
73
76
 
74
77
  Every class of a project built on this package goes in exactly one layer. The table says where,
75
78
  what it may import and what it must never do; the package itself is organized the same way.
76
79
 
77
- | Class | Layer | Responsibility | May depend on | Never |
78
- | ------------------------------------------------------------------------------ | -------------- | -------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------- | ---------------------------------------------------------------------------- |
79
- | Value object (`extends StringType`…) | domain | One concept of the business; converts and validates its own value | other value objects, `@archi-code/validation`, exceptions | mutate, know about storage or transport |
80
- | Aggregate (`extends AggregateRoot`) | domain | Business rules and invariants; records an event for every fact | its value objects, child entities, events, domain services, domain ports | read a repository, publish events, use `new Date()` |
81
- | Composite value object (`Composite({...})`) | domain | Several values that only make sense together (money, a date range, an order line) | value objects, other composites | change, hold an id, be loaded or saved on its own |
82
- | Child entity (`extends Entity`) | domain | A part of an aggregate with its own id; value objects and business methods only the root calls | value objects | setters, be called from outside its aggregate, be loaded or saved on its own |
83
- | Specification (`extends Specification`) | domain | A named business rule that validates, selects or builds; combinable | value objects, aggregates, data already loaded | read a repository, protect an aggregate invariant |
84
- | Domain event (`extends EventBase`) | domain | A fact that happened, named in the past (`OrderPlaced`, `'OrderPlaced'`), with primitive data | value objects (to take its primitives from them) | carry behavior or value objects, reference infrastructure |
85
- | Repository contract (`AggregateFinder`…) | domain | Only gets and persists aggregates, one role per interface; no business logic | the aggregate | be implemented in the domain, hold rules, answer business queries |
86
- | Domain service | domain | A business rule that spans several aggregates or fits none | aggregates, value objects, `AggregateFinder` (to read), domain ports | I/O, saving, transactions, publishing |
87
- | Domain port (`ProductPricing`) | domain | What the model needs from another context or an external service, in its own words | value objects of this context | contain code, expose the other system's model |
88
- | Use case (`extends UseCase`) | application | One business action: input → command of value objects → change one aggregate | domain, application ports, exceptions | import another use case or a listener, persist or call the network by itself |
89
- | Listener (`extends EventListener`) | application | What happens after an event: turns it into the input of one use case; lives next to that use case | one use case (by constructor), the event | hold business rules, save aggregates, call several use cases |
90
- | Application port (`EventBus`, `Clock`, `Logger`, your own such as `Warehouse`) | application | A technical capability the application needs, as a type | domain types | contain code |
91
- | Read query (`OrderSummaries`) | application | An interface that answers a screen or a list with DTOs or ids, outside the aggregates | domain types, DTOs | load aggregates, go through a repository, change data |
92
- | Adapter (repository, outbox, read query, clock…) | infrastructure | Implements a contract with a database, a queue, a broker, HTTP or memory | domain, application, exceptions, drivers | contain business rules |
93
- | Anti-corruption layer (adapter + translator) | infrastructure | Implements a domain port by calling another context or service and translating its model into ours | domain, application, exceptions, clients | leak the other model into the domain, hold business rules |
94
- | Entry point (controller, message consumer, CLI) | interfaces | Turns HTTP, a message or a command line into the input of a use case, and back | application, exceptions, `HttpErrorMapper` (domain types only if unavoidable) | hold business rules, use a repository or another adapter directly |
95
- | Composition root (`composition.ts`) | (per context) | Reads the configuration, builds adapters, use cases and entry points | everything | be imported by the domain, the application or the interfaces |
96
-
97
- Dependencies only point inwards. Your domain imports only `@archi-code/ddd/domain`,
98
- `@archi-code/ddd/exceptions` and `@archi-code/validation`; the application adds
80
+ | Class | Layer | Responsibility | May depend on | Never |
81
+ | ------------------------------------------------------------------------------ | -------------- | -------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------- |
82
+ | Value object (`extends StringType`…) | domain | One concept of the business; converts and validates its own value | other value objects, `@archi-code/validation`, domain exceptions | mutate, know about storage or transport |
83
+ | Aggregate (`extends AggregateRoot`) | domain | Business rules and invariants; records an event for every fact | its value objects, child entities, events, domain services, domain ports | read a repository, publish events, use `new Date()` |
84
+ | Composite value object (`Composite({...})`) | domain | Several values that only make sense together (money, a date range, an order line) | value objects, other composites | change, hold an id, be loaded or saved on its own |
85
+ | Child entity (`extends Entity`) | domain | A part of an aggregate with its own id; value objects and business methods only the root calls | value objects | setters, be called from outside its aggregate, be loaded or saved on its own |
86
+ | Specification (`extends Specification`) | domain | A named business rule that validates, selects or builds; combinable | value objects, aggregates, data already loaded | read a repository, protect an aggregate invariant |
87
+ | Domain event (`extends EventBase`) | domain | A fact that happened, named in the past (`OrderPlaced`, `'OrderPlaced'`), with primitive data | value objects (to take its primitives from them) | carry behavior or value objects, reference infrastructure |
88
+ | Repository contract (`AggregateFinder`…) | domain | Only gets and persists aggregates, one role per interface; no business logic | the aggregate | be implemented in the domain, hold rules, answer business queries |
89
+ | Domain service | domain | A business rule that spans several aggregates or fits none | aggregates, value objects, `AggregateFinder` (to read), domain ports | I/O, saving, transactions, publishing |
90
+ | Domain port (`ProductPricing`) | domain | What the model needs from another context or an external service, in its own words | value objects of this context | contain code, expose the other system's model |
91
+ | Use case (`extends UseCase`) | application | One business action: input → command of value objects → change one aggregate | domain, application ports and exceptions | import another use case or a listener, persist or call the network by itself |
92
+ | Listener (`extends EventListener`) | application | What happens after an event: turns it into the input of one use case; lives next to that use case | one use case (by constructor), the event | hold business rules, save aggregates, call several use cases |
93
+ | Application port (`EventBus`, `Clock`, `Logger`, your own such as `Warehouse`) | application | A technical capability the application needs, as a type | domain types | contain code |
94
+ | Read query (`OrderSummaries`) | application | An interface that answers a screen or a list with DTOs or ids, outside the aggregates | domain types, DTOs | load aggregates, go through a repository, change data |
95
+ | Adapter (repository, outbox, read query, clock…) | infrastructure | Implements a contract with a database, a queue, a broker, HTTP or memory | domain, application, exceptions, drivers | contain business rules |
96
+ | Anti-corruption layer (adapter + translator) | infrastructure | Implements a domain port by calling another context or service and translating its model into ours | domain, application, exceptions, clients | leak the other model into the domain, hold business rules |
97
+ | Entry point (controller, message consumer, CLI) | interfaces | Turns HTTP, a message or a command line into the input of a use case, and back | application, `ExceptionClassifier` and `ExceptionKind` (domain types only if unavoidable) | hold business rules, use a repository or another adapter directly |
98
+ | Composition root (`composition.ts`) | (per context) | Reads the configuration, builds adapters, use cases and entry points | everything | be imported by the domain, the application or the interfaces |
99
+
100
+ Dependencies only point inwards. Your domain imports only `@archi-code/ddd/domain` and
101
+ `@archi-code/validation`; the application adds
99
102
  `@archi-code/ddd/application`; infrastructure implements the contracts of both; and `interfaces`
100
103
  (controllers, consumers) only calls the application. The application never imports
101
104
  infrastructure, the domain never imports the application, and an entry point never reaches a
102
- repository or an adapter: only the composition root knows every piece [IDDD ch. 4]. The one
103
- infrastructure import an entry point may have is `HttpErrorMapper` (or it receives the mapper from
104
- the composition root). A controller validates only the shape of the request, maps the use case
105
- result (an id, a DTO or nothing) to the response and the errors with `HttpErrorMapper`; it never
106
- returns an aggregate.
105
+ repository or an adapter: only the composition root knows every piece [IDDD ch. 4]. An entry
106
+ point imports nothing from infrastructure but `ExceptionClassifier` and `ExceptionKind` of
107
+ `@archi-code/ddd/infrastructure`, which hold no adapter. A controller validates only the shape of the request,
108
+ maps the use case result (an id, a DTO or nothing) to the response and the errors with
109
+ `ExceptionClassifier` (see [Translating errors at an entry point](#translating-errors-at-an-entry-point));
110
+ it never returns an aggregate.
107
111
 
108
112
  ### Folder structure
109
113
 
@@ -149,7 +153,7 @@ test/ mirrors src/
149
153
  without npm packages but the allowed ones, no imports from `application`, `infrastructure` or
150
154
  `interfaces` into the domain, none from `infrastructure` or `interfaces` into the application, no
151
155
  `@archi-code/ddd/infrastructure` in the domain or the application, none from `interfaces` into
152
- your `infrastructure` (`HttpErrorMapper` comes from the package), isolated contexts, no cycles,
156
+ your `infrastructure` nor `@archi-code/ddd/infrastructure`, isolated contexts, no cycles,
153
157
  and `tsPreCompilationDeps: true` so type imports count too). Adapt its paths to your layout.
154
158
  - Environment variables are read only in the composition root and reach the adapters as typed
155
159
  configuration; the domain never logs nor reads `process.env`.
@@ -258,8 +262,7 @@ the browser? of the server?). Send it with `Z` or an offset, or send only the da
258
262
  | `value` | `T \| null` (or `T`) | The converted value; `T` if the type is required (`Required`) |
259
263
  | `isNull` | `boolean` | `true` if there is no value |
260
264
  | `toString()` | `string` | Text of the value; `''` if it is `null`. `DateType` gives ISO 8601, `JsonType` the JSON |
261
- | `toJSON()` | `T \| null` (or `T`) | The value (a date as its ISO `string`), so `JSON.stringify` writes the plain value |
262
- | `equals(other)` | `boolean` | Same class and same value (dates by instant, arrays and objects by content) |
265
+ | `equals(other)` | `boolean` | Same class and same value: `===`; dates by instant, JSON by content, lists item by item |
263
266
  | `validate()` | `ValidationResult<T>` | Runs the `@Validations` rules again; never throws (always ok on a built value object) |
264
267
 
265
268
  ```ts
@@ -268,11 +271,11 @@ class Country extends StringType {}
268
271
 
269
272
  const city = new City('Huelva');
270
273
  `Shipping to ${city}`; // 'Shipping to Huelva'
271
- JSON.stringify({ city }); // '{"city":"Huelva"}'
274
+ JSON.stringify({ city: city.value }); // '{"city":"Huelva"}': write `value`, not the value object
272
275
  city.equals(new City('Huelva')); // true
273
276
  city.equals(new Country('Huelva')); // false: another concept, even with the same text
274
277
  new NumberType().toString(); // ''
275
- new StringType(' ').value; // null (blank strings count as empty)
278
+ new StringType(' ').value; // ' ' (blank text is a value: only null and undefined are empty)
276
279
  new StringType(null).isNull; // true
277
280
  ```
278
281
 
@@ -299,11 +302,15 @@ const nickname: string | null = new Nickname().value; // null
299
302
 
300
303
  new Name(null); // type error and, at runtime:
301
304
  // RequiredValueException: Validation Error: Name is required, but received null.
302
- new Name(' ');
303
- // RequiredValueException: Validation Error: Name is required, but received " ".
305
+ new Name(' ').value; // ' ': blank text is a value, not a missing one
306
+ new NumberType(' ');
307
+ // TypePrimitiveException: Validation Error: Expected a valid Number, but received " ".
304
308
  ```
305
309
 
306
- `null`, `undefined` and blank strings count as empty in every type. A subclass of a required value
310
+ Only `null` and `undefined` count as empty. Any other input, a blank text included, goes through
311
+ the type's conversion: `StringType` keeps it as written and the other types refuse it with
312
+ `TypePrimitiveException`. Add a rule (`isLength`, a `custom` one) when a blank text must not be
313
+ accepted. A subclass of a required value
307
314
  object is also required (`class FullName extends Name {}`). `Required(...)` accepts any subclass of
308
315
  `AbstractType`, including your own and abstract ones (`Required(StatusType)`, `Required(Tags)`).
309
316
 
@@ -364,77 +371,18 @@ Validator names and their `properties` are in the `@archi-code/validation` docum
364
371
  `validate()` runs the rules again and returns a frozen `{ ok, value, errors }` without throwing;
365
372
  on a value object that was built it is always `ok`.
366
373
 
367
- ### Building every value object of an input at once: `ValueObjects.build`
368
-
369
- The constructor of each value object stops at its own errors. To report **every field** of an
370
- input (a form, a JSON body, a message) at once, build the value objects with
371
- `ValueObjects.build`: it runs every factory, even after one fails, and throws one
372
- `ValidationException` whose `errors` name the field of each problem. It collects the three ways a
373
- value can be wrong: empty in a required type (`RequiredValueException`), not convertible
374
- (`TypePrimitiveException`) and broken rules (`ValidationException`), plus any other
375
- `DomainException` a factory throws. Any other error is a bug and goes through untouched, and it
376
- wins: the validation errors already collected are dropped, so a bug is never hidden behind a 422.
377
- `ValueObjects.buildList` does the same for a list and names each item by its position, so nested
378
- inputs get paths such as `lines.1.quantity`. Factories are synchronous: a factory that returns a
379
- Promise does not compile in `build`, and both methods refuse it at run time with
380
- `InternalErrorException` (`ValueObjects: the factory of "email" returned a Promise; build value
381
- objects synchronously`), so an asynchronous check never skips the validation. Every field name is
382
- kept as written, `__proto__` included.
383
-
384
- ```ts
385
- import { IdType, NumberType, Required, StringType, ValueObjects } from '@archi-code/ddd/domain';
386
- import { Validations } from '@archi-code/validation';
387
-
388
- // Email as in the previous block; input is the raw request body
389
-
390
- class OrderId extends IdType<'OrderId'> {}
391
- class Sku extends Required(StringType) {}
392
-
393
- @Validations([
394
- { validator: 'isInt', properties: { min: 1, max: 20 }, message: 'From 1 to 20 units' },
395
- ])
396
- class Quantity extends Required(NumberType) {}
397
-
398
- const command = ValueObjects.build({
399
- orderId: () => new OrderId(input.orderId),
400
- email: () => new Email(input.email),
401
- lines: () =>
402
- ValueObjects.buildList(input.lines, (line) =>
403
- ValueObjects.build({
404
- sku: () => new Sku(line.sku),
405
- quantity: () => new Quantity(line.quantity),
406
- }),
407
- ),
408
- });
409
- // command: { orderId: OrderId; email: Email; lines: { sku: Sku; quantity: Quantity }[] }
410
-
411
- // with input = { orderId: 'x', email: 'ana', lines: [{ sku: 'TORCH-01', quantity: 2 }, { sku: '', quantity: 0 }] }:
412
- // ValidationException, error.errors:
413
- // [
414
- // { field: 'orderId', code: 'DOM003', message: 'Validation Error: Expected a valid UUID, but received "x".' },
415
- // { field: 'email', code: 'isEmail', message: 'Email: Value does not satisfy isEmail' },
416
- // { field: 'lines.1.sku', code: 'DOM004', message: 'Validation Error: Sku is required, but received "".' },
417
- // { field: 'lines.1.quantity', code: 'isInt', message: 'Quantity: From 1 to 20 units' },
418
- // ]
419
- ```
420
-
421
- `HttpErrorMapper` answers it with a 422 whose body carries the same `errors`, so a client can mark
422
- each input. To report a problem of your own in the same shape, throw
423
- `new ValidationException([{ field: 'deliveryDate', code: 'ORD003', message: '…' }])`; `code` is
424
- optional and defaults to `'DOM001'`.
425
-
426
374
  ### Which layer validates what
427
375
 
428
376
  The input that reaches a use case is not validated yet, and that is on purpose: every adapter
429
377
  (HTTP, a queue, a CLI, a test) goes through the use case, so the rules live in one place.
430
378
 
431
- | What | Where |
432
- | --------------------------------------------------------- | --------------------------------------------------------------------------- |
433
- | The shape of the request (it parses, a field is a string) | The input adapter (controller, consumer); no business rule |
434
- | One value: required, convertible, its rules | The value object's constructor, through `ValueObjects.build` in `toCommand` |
435
- | Rules over several values of one aggregate (invariants) | The aggregate's methods, with a `DomainException` |
436
- | Rules over several aggregates | A domain service |
437
- | Whether the caller may run the action | The use case (application) |
379
+ | What | Where |
380
+ | --------------------------------------------------------- | --------------------------------------------------------------------------------- |
381
+ | The shape of the request (it parses, a field is a string) | The input adapter (controller, consumer); no business rule |
382
+ | One value: required, convertible, its rules | The value object's constructor, through `ValueObjectBuilder.build` in `toCommand` |
383
+ | Rules over several values of one aggregate (invariants) | The aggregate's methods, with a `DomainException` |
384
+ | Rules over several aggregates | A domain service |
385
+ | Whether the caller may run the action | The use case (application) |
438
386
 
439
387
  Do not repeat a value object's rule in the request schema: the adapter checks that the email is a
440
388
  string, `Email` checks that it is an email. Invariants run after the fields are valid, because they
@@ -452,13 +400,13 @@ one:
452
400
  // TypeError: Cannot call setFullYear: the date is immutable
453
401
  ```
454
402
 
455
- - `JsonType` and `ArrayType` hold deeply frozen copies (`Object.isFrozen(vo.value)` is `true`).
403
+ - `JsonType` holds a deeply frozen copy and `ArrayType` a frozen list of its items' values
404
+ (`Object.isFrozen(vo.value)` is `true`).
456
405
  - `ArrayType.withItem()` and `withoutItem()` return a **new** instance and leave the original
457
406
  untouched, so a value object shared by two aggregates never changes under one of them.
458
407
  - To touch an `UpdatedAt`, replace it: `this._updatedAt = UpdatedAt.now()`.
459
- - The same helpers are exported for your own value objects: `Immutable.freeze(value)` deep-freezes
460
- an object or array and returns it, and `Immutable.date(date)` returns a copy of a `Date` whose
461
- setters throw.
408
+ - Each value object guarantees its own immutability: a custom type that holds an object copies and
409
+ freezes it in its `filter` (see the `GeoPoint` example below).
462
410
 
463
411
  ### `UuidType` and `IdType`
464
412
 
@@ -624,28 +572,26 @@ new Tags('abc' as any);
624
572
  // TypePrimitiveException: Validation Error: Expected a valid Array, but received "abc".
625
573
  ```
626
574
 
627
- An `ArrayType` builds every item and reports, at once, the rules of the list (`field: null`) and
628
- the errors of each item with its position (`field: '1'`, from 0):
575
+ An `ArrayType` receives items that are already valid (checked by `toCommand` or read from
576
+ storage), so it does not collect item errors: it builds the items in order and lets the error of
577
+ the first invalid one through as the item throws it, and then runs the rules of the list
578
+ (`field: null`):
629
579
 
630
580
  ```ts
631
- new Tags(['ok', 'x', '']);
581
+ new Tags(['ok', 'x', null] as any);
632
582
  // ValidationException, error.errors:
633
- // [
634
- // { field: '1', code: 'isLength', message: 'Tag: Value does not satisfy isLength' },
635
- // { field: '2', code: 'DOM004', message: 'Validation Error: Tag is required, but received "".' },
636
- // ]
583
+ // [{ field: null, code: 'isLength', message: 'Tag: Value does not satisfy isLength' }]
637
584
  ```
638
585
 
639
586
  ### Custom value objects with `AbstractType`
640
587
 
641
588
  For a new type, extend `AbstractType<T>` and implement `filter(value)`: it receives the input
642
- already known to be non-empty, returns the converted value and throws `TypePrimitiveException` if it
643
- is not valid. Empty input (`null`, `undefined`, blanks) and `Required(...)` are handled by the base
644
- class.
589
+ already known not to be `null` nor `undefined` (a blank text does reach it), returns the converted
590
+ value and throws `TypePrimitiveException` if it is not valid. Empty input (`null`, `undefined`) and
591
+ `Required(...)` are handled by the base class.
645
592
 
646
593
  ```ts
647
- import { AbstractType, Required } from '@archi-code/ddd/domain';
648
- import { TypePrimitiveException } from '@archi-code/ddd/exceptions';
594
+ import { AbstractType, Required, TypePrimitiveException } from '@archi-code/ddd/domain';
649
595
 
650
596
  class Percentage extends AbstractType<number> {
651
597
  protected filter(value: unknown): number {
@@ -664,6 +610,27 @@ new Percentage(140);
664
610
  class Discount extends Required(Percentage) {}
665
611
  ```
666
612
 
613
+ `equals` compares the class and then the values with `===`. A type whose value is an object or a
614
+ list says how two of them compare by overriding `protected hasSameValue(other)` (it receives the other
615
+ value object, already known to be of the same class, and compares `this._value` with
616
+ `other._value`, either of them possibly `null`), like `DateType` (by instant), `JsonType` (by content) and
617
+ `ArrayType` (each item with its `equals`) do:
618
+
619
+ ```ts
620
+ class GeoPoint extends AbstractType<{ lat: number; lng: number }> {
621
+ protected filter(value: unknown): { lat: number; lng: number } {
622
+ const { lat, lng } = value as { lat: number; lng: number };
623
+ return Object.freeze({ lat: Number(lat), lng: Number(lng) });
624
+ }
625
+
626
+ protected hasSameValue(other: this): boolean {
627
+ return this._value?.lat === other._value?.lat && this._value?.lng === other._value?.lng;
628
+ }
629
+ }
630
+
631
+ new GeoPoint({ lat: 37.26, lng: -6.95 }).equals(new GeoPoint({ lng: -6.95, lat: 37.26 })); // true
632
+ ```
633
+
667
634
  To restrict an existing type, it is enough to extend it and declare `@Validations`, like `Email`.
668
635
 
669
636
  ### Composite value objects: `Composite({...})`
@@ -681,8 +648,14 @@ class of each field:
681
648
 
682
649
  ```ts
683
650
  import { Validations } from '@archi-code/validation';
684
- import { Composite, DateType, NumberType, Required, StringType } from '@archi-code/ddd/domain';
685
- import { DomainException } from '@archi-code/ddd/exceptions';
651
+ import {
652
+ Composite,
653
+ DateType,
654
+ DomainException,
655
+ NumberType,
656
+ Required,
657
+ StringType,
658
+ } from '@archi-code/ddd/domain';
686
659
 
687
660
  @Validations([{ validator: 'isInt' }])
688
661
  class Cents extends Required(NumberType) {}
@@ -716,7 +689,7 @@ price.add(new Money({ amount: new Cents(100), currency: new Currency('USD') }));
716
689
  new Cents(12.5);
717
690
  // ValidationException: Validation failed, Cents: Value does not satisfy isInt
718
691
  price.equals(Money.fromPrimitives({ amount: 1250, currency: 'EUR' })); // true
719
- JSON.stringify(price); // '{"amount":1250,"currency":"EUR"}'
692
+ JSON.stringify(price.toPrimitives()); // '{"amount":1250,"currency":"EUR"}'
720
693
  ```
721
694
 
722
695
  - **The constructor** takes value objects only. A missing field throws `RequiredValueException`
@@ -750,7 +723,7 @@ JSON.stringify(price); // '{"amount":1250,"currency":"EUR"}'
750
723
  - **`X.fromPrimitives(data)`** rebuilds it from stored or received data, converting each primitive
751
724
  like the value objects do, and reports every invalid field with its path, also nested ones.
752
725
  `data` is a `CompositeInput`: each field takes what its value object's constructor takes (a date
753
- field takes a `Date` or ISO text). It works inside `ValueObjects.build`, so a command reports
726
+ field takes a `Date` or ISO text). It works inside `ValueObjectBuilder.build`, so a command reports
754
727
  `price.amount`:
755
728
 
756
729
  ```ts
@@ -760,11 +733,11 @@ JSON.stringify(price); // '{"amount":1250,"currency":"EUR"}'
760
733
  class Cart extends Composite({ lines: [OrderLine] }) {}
761
734
 
762
735
  Cart.fromPrimitives({
763
- lines: [{ sku: '', quantity: 'two', unitPrice: { amount: 1250, currency: 'GBP' } }],
736
+ lines: [{ sku: null, quantity: 'two', unitPrice: { amount: 1250, currency: 'GBP' } }],
764
737
  } as any);
765
738
  // ValidationException, error.errors:
766
739
  // [
767
- // { field: 'lines.0.sku', code: 'DOM004', message: 'Validation Error: Sku is required, but received "".' },
740
+ // { field: 'lines.0.sku', code: 'DOM004', message: 'Validation Error: Sku is required, but received null.' },
768
741
  // { field: 'lines.0.quantity', code: 'DOM003', message: 'Validation Error: Expected a valid Number, but received "two".' },
769
742
  // { field: 'lines.0.unitPrice.currency', code: 'isIn', message: 'Currency: Value does not satisfy isIn' },
770
743
  // ]
@@ -772,7 +745,7 @@ JSON.stringify(price); // '{"amount":1250,"currency":"EUR"}'
772
745
 
773
746
  - **`equals(other)`** compares the class and every field (dates by instant, lists in order):
774
747
  a `Price` and a `Money` with the same fields are different. **`toPrimitives()`** gives plain data
775
- for repositories (dates stay `Date`); `JSON.stringify` writes ISO dates.
748
+ for repositories (dates stay `Date`); `JSON.stringify` of them writes ISO dates.
776
749
  - **In an aggregate**, a composite is a field like any value object:
777
750
  `PrimitiveTypes<Order>` types it as its primitives, and an optional one is a `Money | null`
778
751
  field. Write `this.price.toPrimitives()` in `toPrimitives()` and `Money.fromPrimitives(...)` in
@@ -819,14 +792,11 @@ repository. It does not protect the invariants of an aggregate: those stay in th
819
792
  - `AggregateRoot<Id = IdType, Event = EventBase>`: abstract class for aggregates. `new (id)` stores
820
793
  `readonly id`; `equals(other)` is true for the same class and an equal id, whatever the other
821
794
  attributes are. `Id` may be any required value object (`EntityId`: `IdType`, a `Required(StringType)` code…); an optional value object cannot be an id, and an id that holds no value throws `RequiredValueException`, so two new entities are never taken for the same one [IDDD ch. 5]; `record(event)` is `protected` and deep-freezes the event (a fact never changes [IDDD ch. 8]); `pullDomainEvents()` returns the events typed as `Event[]`, in order, and empties
822
- the list. Pending events are not an enumerable property: they never show up in
823
- `JSON.stringify(aggregate)`, `{ ...aggregate }` or `Object.keys`.
824
- - Every aggregate has a `version` for optimistic concurrency (see
825
- [Optimistic concurrency](#optimistic-concurrency)): `0` for a new one, the stored version for a
826
- loaded one, one more after each successful save. The aggregate does nothing with it: it keeps
827
- calling `super(id)`, and the version is neither a field, nor in its primitives, nor in its JSON.
828
- Only repositories touch it, with `restoreVersion(version)` when they load and `markPersisted()`
829
- after they save.
795
+ the list. Pending events are a plain private array: repositories store `toPrimitives()`, never
796
+ the aggregate itself.
797
+ - The aggregate carries no version: optimistic concurrency belongs to the repository, which
798
+ remembers the version of every aggregate it loaded or saved (see
799
+ [Optimistic concurrency](#optimistic-concurrency)). The aggregate only calls `super(id)`.
830
800
  - Parts of an aggregate are composite value objects when nobody looks them up by an id of their
831
801
  own (the lines of an order). A part that does need its own identity inside the aggregate (a seat
832
802
  of a room) is a child entity: it extends `Entity<Id>`, which only stores `readonly id` and
@@ -840,13 +810,13 @@ repository. It does not protect the invariants of an aggregate: those stay in th
840
810
  import {
841
811
  AggregateRoot,
842
812
  BooleanType,
813
+ DomainException,
843
814
  Entity,
844
815
  IdType,
845
816
  NumberType,
846
817
  Required,
847
818
  StringType,
848
819
  } from '@archi-code/ddd/domain';
849
- import { DomainException } from '@archi-code/ddd/exceptions';
850
820
 
851
821
  class SeatNumber extends Required(NumberType) {}
852
822
  class SeatLabel extends Required(StringType) {}
@@ -898,25 +868,30 @@ repository. It does not protect the invariants of an aggregate: those stay in th
898
868
 
899
869
  - `EventBase`: abstract class for events; declare
900
870
  `static readonly eventName = '<ClassName>'` (`'OrderPlaced'` [IDDD ch. 8]; the
901
- `'<context>.<module>.<Event>'` names are for the contracts other contexts read) (listeners and registries refer to the event by its class, without an instance; `eventName()`
902
- returns it, and a subclass inherits it). Creating an event whose class has no non-blank
903
- `eventName` throws `InternalErrorException`. `new (aggregateId, metadata?)` sets `aggregateId`, a new `eventId` (UUID v4) and an immutable `occurredOn`. Pass
871
+ `'<context>.<module>.<Event>'` names are for the contracts other contexts read) (listeners and registries refer to the event by its class, without an instance, and a subclass
872
+ inherits it). An event is a plain object of primitive data with no methods: `new (metadata?)`
873
+ copies the static name into the `readonly eventName` field and sets `eventId` (a new UUID v4)
874
+ and `occurredOn` (ISO text in UTC, `'2026-10-17T10:00:00.000Z'`); everything else is data the
875
+ subclass declares as `readonly` constructor fields, including the id of the aggregate
876
+ (`readonly orderId: string`). Creating an event whose class has no non-blank `eventName` throws
877
+ `InvalidEventDefinitionException` (`OrderPlaced must declare a static eventName`). Pass
904
878
  `{ clock }` (any `{ now(): Date }`, such as the `Clock` port) to take the moment from it; without
905
879
  a clock it is the system time. When you rebuild an event a channel delivered, pass
906
880
  `{ eventId, occurredOn }` (`Date` or ISO string with zone) to keep them; a stored `occurredOn`
907
- wins over a clock. The clock is not kept in the event nor in its JSON. `toJSON()` gives
908
- `{ eventName, eventVersion, eventId, aggregateId, occurredOn (ISO), ...payload }`.
909
- - The shape of an event's data has a version: `static readonly eventVersion = 2` when it changes
910
- in a way old readers cannot follow (renaming or removing a field, changing its meaning; adding an
911
- optional field needs no new version). It is 1 when not declared, `event.eventVersion()` returns
912
- it, and a version below 1 or not whole throws `InternalErrorException`. Stored events keep the
913
- version they were written with, so the `EventRegistry` factory can read old shapes
914
- (`json.eventVersion === 1 ? … : …`).
915
- - The data of an event are primitives taken from value objects (`id.value`, money as
916
- `{ amount, currency }`), never value objects nor the whole state of the aggregate: the event is
917
- serialized as it is, and its readers must not depend on the model.
918
- - `version` counts saves, not events: it grows by 1 on each save, however many events the change
919
- recorded. Event Sourcing is not provided: an event-sourced aggregate manages the version of its
881
+ wins over a clock and is stored as UTC ISO text. The clock is not kept in the event. The event
882
+ is its own JSON: `{ ...event }` and `JSON.stringify(event)` give
883
+ `{ eventName, eventId, occurredOn, ...fields }`, in that order.
884
+ - Rule: each event declares only the primitive data it needs (text, finite numbers, booleans,
885
+ `null`, and lists and plain objects of them), taken from value objects: a value object becomes
886
+ its `.value`, money `{ amount, currency }`, a date its ISO text. Never value objects, `Date`s,
887
+ class instances nor the whole state of the aggregate: the event travels as it is, and its
888
+ readers must not depend on the model. Nothing checks this at run time.
889
+ - An event has no schema version. Adding an optional field that old readers can ignore is
890
+ compatible; a change old readers cannot follow (renaming or removing a field, changing its
891
+ meaning) is published as a new event with its own `eventName` (`CustomerRenamedV2`), or the
892
+ `EventRegistry` factory of the event reads both shapes (`typeof json.name === 'string' ? … : …`).
893
+ - The version a repository stores counts saves, not events: it grows by 1 on each save, however
894
+ many events the change recorded. Event Sourcing is not provided: an event-sourced aggregate manages the version of its
920
895
  stream itself.
921
896
 
922
897
  ```ts
@@ -931,11 +906,11 @@ class OrderPlaced extends EventBase {
931
906
  static readonly eventName = 'OrderPlaced';
932
907
 
933
908
  constructor(
934
- orderId: string,
909
+ readonly orderId: string,
935
910
  readonly customer: string,
936
911
  metadata?: EventMetadata,
937
912
  ) {
938
- super(orderId, metadata);
913
+ super(metadata);
939
914
  }
940
915
  }
941
916
 
@@ -955,7 +930,7 @@ const order = Order.place(
955
930
  const [placed] = order.pullDomainEvents(); // OrderPlaced[]
956
931
  order.pullDomainEvents(); // []
957
932
  JSON.stringify(placed);
958
- // {"eventName":"OrderPlaced","eventVersion":1,"eventId":"…","aggregateId":"9b2f6c1e-…","occurredOn":"2026-10-17T10:00:00.000Z","customer":"Ana"}
933
+ // {"eventName":"OrderPlaced","eventId":"…","occurredOn":"2026-10-17T10:00:00.000Z","orderId":"9b2f6c1e-…","customer":"Ana"}
959
934
  ```
960
935
 
961
936
  ### Primitive types
@@ -1017,7 +992,7 @@ interface AggregateRemover<Aggregate extends AggregateRoot<any, any>> {
1017
992
  ```
1018
993
 
1019
994
  `remove` receives the loaded aggregate, not its id: removing is a change like any other, so the
1020
- repository removes it only where the stored version still equals `aggregate.version`, and throws
995
+ repository removes it only where the stored version still equals the one it loaded, and throws
1021
996
  `ConcurrencyException` when someone changed or removed it after it was loaded [IDDD ch. 10]. A
1022
997
  use case loads it with `findById`, decides, and removes it.
1023
998
 
@@ -1049,17 +1024,19 @@ interface OrderSummaries {
1049
1024
  ### Optimistic concurrency
1050
1025
 
1051
1026
  Two users load the same order, both change it and both save: without a check, the second save
1052
- silently erases the first. `AggregateRoot` carries the version that prevents it, and every
1027
+ silently erases the first. The repository prevents it with a version it keeps itself: the
1028
+ aggregate knows nothing about it. It remembers the version of each aggregate object it loaded or
1029
+ saved in a `WeakMap<Order, number>` (an object it never saw is new, version `0`), and every
1053
1030
  repository follows the same contract:
1054
1031
 
1055
- | Moment | The repository |
1056
- | ---------------------------- | ------------------------------------------------------------------------------------------------------------ |
1057
- | Load | Rebuilds with `Order.fromPrimitives(row).restoreVersion(storedVersion)`: `aggregate.version` is what it read |
1058
- | Save a new aggregate | `aggregate.version` is `0`: inserts with version `1`; if the id already exists, it is a conflict |
1059
- | Save an existing aggregate | Writes only where the stored version still equals `aggregate.version`, and stores `aggregate.version + 1` |
1060
- | Nothing written (stale data) | Throws `ConcurrencyException('Order', id, aggregate.version)` (HTTP 409) |
1061
- | Written | Calls `aggregate.markPersisted()`, so the same object can be changed and saved again |
1062
- | Remove | Deletes only where the stored version still equals `aggregate.version`; otherwise `ConcurrencyException` |
1032
+ | Moment | The repository |
1033
+ | ---------------------------- | ------------------------------------------------------------------------------------------- |
1034
+ | Load | Rebuilds with `Order.fromPrimitives(row)` and remembers `row.version` for that object |
1035
+ | Save a new aggregate | Version `0`: inserts with version `1`; if the id already exists, it is a conflict |
1036
+ | Save an existing aggregate | Writes only where the stored version still equals the remembered one, and stores one more |
1037
+ | Nothing written (stale data) | Throws `ConcurrencyException('Order', id, version)` (HTTP 409) with the version it expected |
1038
+ | Written | Remembers the new version, so the same object can be changed and saved again |
1039
+ | Remove | Deletes only where the stored version still equals the remembered one; otherwise a conflict |
1063
1040
 
1064
1041
  The version lives next to the primitives (a `version` column or field), never inside them. After a
1065
1042
  `ConcurrencyException`, either tell the user that someone else changed it, or run the command
@@ -1070,8 +1047,11 @@ case. A use case run by a listener does not retry by itself: it lets the conflic
1070
1047
  channel's redelivery tries again:
1071
1048
 
1072
1049
  ```ts
1073
- import { UseCase } from '@archi-code/ddd/application';
1074
- import { AggregateNotFoundException, ConcurrencyException } from '@archi-code/ddd/exceptions';
1050
+ import {
1051
+ AggregateNotFoundException,
1052
+ ConcurrencyException,
1053
+ UseCase,
1054
+ } from '@archi-code/ddd/application';
1075
1055
 
1076
1056
  interface AddOrderLineInput {
1077
1057
  orderId: string;
@@ -1087,7 +1067,7 @@ interface AddOrderLineCommand {
1087
1067
 
1088
1068
  class AddOrderLine extends UseCase<AddOrderLineInput, AddOrderLineCommand, void> {
1089
1069
  // constructor (orders, events, clock) as in "Persistence belongs to the repository";
1090
- // toCommand builds orderId, sku and quantity with ValueObjects.build;
1070
+ // toCommand builds orderId, sku and quantity with ValueObjectBuilder.build;
1091
1071
  // Order, OrderLine, Sku and Quantity as in "Persisting aggregates"
1092
1072
 
1093
1073
  protected async handle(command: AddOrderLineCommand): Promise<void> {
@@ -1115,9 +1095,12 @@ changed) does not retry: the `ConcurrencyException` reaches the entry point (HTT
1115
1095
 
1116
1096
  ```ts
1117
1097
  import type { AggregateFinder, AggregateSaver } from '@archi-code/ddd/domain';
1118
- import { ConcurrencyException, InternalErrorException } from '@archi-code/ddd/exceptions';
1098
+ import { ConcurrencyException } from '@archi-code/ddd/application';
1099
+ import { InternalErrorException } from '@archi-code/ddd/infrastructure';
1119
1100
 
1120
1101
  class MongoOrderRepository implements AggregateFinder<Order>, AggregateSaver<Order> {
1102
+ private readonly versions = new WeakMap<Order, number>();
1103
+
1121
1104
  constructor(private readonly collection: Collection<OrderDocument>) {}
1122
1105
 
1123
1106
  async findById(id: OrderId): Promise<Order | null> {
@@ -1126,20 +1109,22 @@ class MongoOrderRepository implements AggregateFinder<Order>, AggregateSaver<Ord
1126
1109
  return null;
1127
1110
  }
1128
1111
  const { _id, version, ...primitives } = document;
1129
- return Order.fromPrimitives(primitives).restoreVersion(version);
1112
+ const order = Order.fromPrimitives(primitives);
1113
+ this.versions.set(order, version);
1114
+ return order;
1130
1115
  }
1131
1116
 
1132
1117
  async save(order: Order): Promise<void> {
1133
- const data = { ...order.toPrimitives(), version: order.version + 1 };
1118
+ const version = this.versions.get(order) ?? 0;
1119
+ const data = { ...order.toPrimitives(), version: version + 1 };
1134
1120
  const written =
1135
- order.version === 0
1121
+ version === 0
1136
1122
  ? await this.insert(data)
1137
- : (await this.collection.replaceOne({ _id: data.id, version: order.version }, data))
1138
- .matchedCount === 1;
1123
+ : (await this.collection.replaceOne({ _id: data.id, version }, data)).matchedCount === 1;
1139
1124
  if (!written) {
1140
- throw new ConcurrencyException('Order', order.id.value, order.version);
1125
+ throw new ConcurrencyException('Order', order.id.value, version);
1141
1126
  }
1142
- order.markPersisted();
1127
+ this.versions.set(order, version + 1);
1143
1128
  }
1144
1129
 
1145
1130
  private async insert(data: OrderDocument): Promise<boolean> {
@@ -1169,7 +1154,7 @@ the one of [Persisting aggregates](#persisting-aggregates)):
1169
1154
  ```ts
1170
1155
  import { describe, expect, it } from 'vitest';
1171
1156
  import type { AggregateFinder, AggregateSaver } from '@archi-code/ddd/domain';
1172
- import { ConcurrencyException } from '@archi-code/ddd/exceptions';
1157
+ import { ConcurrencyException } from '@archi-code/ddd/application';
1173
1158
  import { FixedClock } from '@archi-code/ddd/infrastructure';
1174
1159
 
1175
1160
  const clock = new FixedClock('2026-10-17T10:00:00Z');
@@ -1191,7 +1176,7 @@ export function orderRepositoryContract(
1191
1176
  await orders.save(order);
1192
1177
  order.ship(clock);
1193
1178
  await orders.save(order);
1194
- expect((await orders.findById(order.id))?.version).toBe(2);
1179
+ expect((await orders.findById(order.id))?.toPrimitives()).toStrictEqual(order.toPrimitives());
1195
1180
  });
1196
1181
 
1197
1182
  it('refuses a stale save', async () => {
@@ -1243,7 +1228,7 @@ A use case has two steps, and `execute(input)` (which always returns a promise)
1243
1228
  order:
1244
1229
 
1245
1230
  1. `protected toCommand(input: Input): Command` builds the value objects the action needs with
1246
- `ValueObjects.build({ orderId: () => new OrderId(input.orderId), email: () => new Email(input.email) })`
1231
+ `ValueObjectBuilder.build({ orderId: () => new OrderId(input.orderId), email: () => new Email(input.email) })`
1247
1232
  (one value object alone can be built with `new`). If any field is invalid it throws one
1248
1233
  `ValidationException` listing every field, and `handle` never runs.
1249
1234
  2. `protected handle(command: Command)` (sync or async) coordinates the action with value objects
@@ -1256,8 +1241,8 @@ Errors of both steps are treated the same way:
1256
1241
  - Exceptions of the package (`DomainException`, `ValidationException`, `ApplicationException`,
1257
1242
  `AggregateNotFoundException`…) pass through as they are, so the caller can map them.
1258
1243
  - Anything else (a driver error, a `TypeError`, a thrown string) is wrapped in
1259
- `InternalErrorException` with the original as `cause` and the use case name in the message:
1260
- `Failed, internal error: PlaceOrder: connection reset`.
1244
+ `UnexpectedErrorException` with the original as `cause` and the use case name in the message:
1245
+ `Failed, unexpected error: PlaceOrder: connection reset`.
1261
1246
 
1262
1247
  `UseCase` does nothing else: loading, checking, saving and publishing are written in `handle`,
1263
1248
  so every step of the use case is visible in one place:
@@ -1293,6 +1278,67 @@ nothing and events are never sent twice. A command with several values is a smal
1293
1278
  value objects (`interface PlaceOrderCommand { orderId: OrderId; customer: CustomerName }`); a
1294
1279
  use case never imports another use case.
1295
1280
 
1281
+ ### Building every value object of an input at once: `ValueObjectBuilder.build`
1282
+
1283
+ `ValueObjectBuilder` lives in `@archi-code/ddd/application`: it is used in `toCommand`, never in
1284
+ the domain. The constructor of each value object stops at its own errors. To report **every field** of an
1285
+ input (a form, a JSON body, a message) at once, build the value objects with
1286
+ `ValueObjectBuilder.build`: it runs every factory, even after one fails, and throws one
1287
+ `ValidationException` whose `errors` name the field of each problem. It collects the three ways a
1288
+ value can be wrong: empty in a required type (`RequiredValueException`), not convertible
1289
+ (`TypePrimitiveException`) and broken rules (`ValidationException`), plus any other
1290
+ `DomainException` a factory throws. Any other error is a bug and goes through untouched, and it
1291
+ wins: the validation errors already collected are dropped, so a bug is never hidden behind a 422.
1292
+ `ValueObjectBuilder.buildList` does the same for a list and names each item by its position, so nested
1293
+ inputs get paths such as `lines.1.quantity`. Factories are synchronous: a factory that returns a
1294
+ Promise does not compile in `build`, and both methods refuse it at run time with
1295
+ `UnexpectedErrorException` (`Failed, unexpected error: ValueObjectBuilder: the factory of "email"
1296
+ returned a Promise; build value objects synchronously`), so an asynchronous check never skips the validation. Every field name is
1297
+ kept as written, `__proto__` included.
1298
+
1299
+ ```ts
1300
+ import { IdType, NumberType, Required, StringType } from '@archi-code/ddd/domain';
1301
+ import { ValueObjectBuilder } from '@archi-code/ddd/application';
1302
+ import { Validations } from '@archi-code/validation';
1303
+
1304
+ // Email as in "Validations: always valid from the constructor"; input is the raw request body
1305
+
1306
+ class OrderId extends IdType<'OrderId'> {}
1307
+ class Sku extends Required(StringType) {}
1308
+
1309
+ @Validations([
1310
+ { validator: 'isInt', properties: { min: 1, max: 20 }, message: 'From 1 to 20 units' },
1311
+ ])
1312
+ class Quantity extends Required(NumberType) {}
1313
+
1314
+ const command = ValueObjectBuilder.build({
1315
+ orderId: () => new OrderId(input.orderId),
1316
+ email: () => new Email(input.email),
1317
+ lines: () =>
1318
+ ValueObjectBuilder.buildList(input.lines, (line) =>
1319
+ ValueObjectBuilder.build({
1320
+ sku: () => new Sku(line.sku),
1321
+ quantity: () => new Quantity(line.quantity),
1322
+ }),
1323
+ ),
1324
+ });
1325
+ // command: { orderId: OrderId; email: Email; lines: { sku: Sku; quantity: Quantity }[] }
1326
+
1327
+ // with input = { orderId: 'x', email: 'ana', lines: [{ sku: 'TORCH-01', quantity: 2 }, { sku: null, quantity: 0 }] }:
1328
+ // ValidationException, error.errors:
1329
+ // [
1330
+ // { field: 'orderId', code: 'DOM003', message: 'Validation Error: Expected a valid UUID, but received "x".' },
1331
+ // { field: 'email', code: 'isEmail', message: 'Email: Value does not satisfy isEmail' },
1332
+ // { field: 'lines.1.sku', code: 'DOM004', message: 'Validation Error: Sku is required, but received null.' },
1333
+ // { field: 'lines.1.quantity', code: 'isInt', message: 'Quantity: From 1 to 20 units' },
1334
+ // ]
1335
+ ```
1336
+
1337
+ `ExceptionClassifier` classifies it as `ExceptionKind.Invalid` (a 422 in HTTP) and the entry point
1338
+ returns the same `errors`, so a client can mark each input. To report a problem of your own in the same shape, throw
1339
+ `new ValidationException([{ field: 'deliveryDate', code: 'ORD003', message: '…' }])`; `code` is
1340
+ optional and defaults to `'DOM001'`.
1341
+
1296
1342
  ### Use cases run by a listener
1297
1343
 
1298
1344
  A use case that a listener runs registers a fact that already happened elsewhere, so it does not
@@ -1342,7 +1388,7 @@ wiring stay as they are.
1342
1388
  ```
1343
1389
  use case ──► EventBus (port, @archi-code/ddd/application)
1344
1390
  ▲ implements
1345
- wiring ──► DomainEventBus (toJSON, @ListensTo, rebuild, listener.handle) (@archi-code/ddd/infrastructure)
1391
+ wiring ──► DomainEventBus ({ ...event }, @ListensTo, rebuild, listener.handle) (@archi-code/ddd/infrastructure)
1346
1392
  └──► EventChannel (port) ◄── InMemoryEventChannel (@archi-code/ddd/infrastructure)
1347
1393
  send / receive ◄── your RabbitMQ, database or Kafka channel
1348
1394
  ```
@@ -1384,7 +1430,7 @@ class ReserveStockOnOrderPlaced extends EventListener<OrderPlaced> {
1384
1430
  }
1385
1431
 
1386
1432
  async handle(event: OrderPlaced): Promise<void> {
1387
- await this.reserveStock.execute({ orderId: event.aggregateId });
1433
+ await this.reserveStock.execute({ orderId: event.orderId });
1388
1434
  }
1389
1435
  }
1390
1436
  ```
@@ -1426,7 +1472,7 @@ interface EventChannel {
1426
1472
  }
1427
1473
  ```
1428
1474
 
1429
- A channel only moves serialized events (`toJSON()`), so the same contract fits a process, a broker
1475
+ A channel only moves serialized events (`{ ...event }`), so the same contract fits a process, a broker
1430
1476
  and a table. The rules every channel follows:
1431
1477
 
1432
1478
  | Concern | Rule |
@@ -1452,8 +1498,11 @@ import {
1452
1498
  } from '@archi-code/ddd/infrastructure';
1453
1499
 
1454
1500
  const events = new EventRegistry<OrderEvent>()
1455
- .register(OrderPlaced, (json) => new OrderPlaced(json.aggregateId, json.customer as string, json))
1456
- .register(OrderShipped, (json) => new OrderShipped(json.aggregateId, json));
1501
+ .register(
1502
+ OrderPlaced,
1503
+ (json) => new OrderPlaced(json.orderId as string, json.customer as string, json),
1504
+ )
1505
+ .register(OrderShipped, (json) => new OrderShipped(json.orderId as string, json));
1457
1506
 
1458
1507
  const bus = new DomainEventBus<OrderEvent>(new InMemoryEventChannel(), events);
1459
1508
  const reserving = await bus.subscribe(new ReserveStockOnOrderPlaced(reserveStock));
@@ -1466,14 +1515,13 @@ await reserving.unsubscribe();
1466
1515
 
1467
1516
  - `DomainEventBus` implements `EventBus`; `subscribe(listener)` is its own method, called only by
1468
1517
  the wiring.
1469
- - `publish(events)` sends the `toJSON()` of each event to the channel in one `send` (events
1470
- recorded by an aggregate are already frozen by `record()`).
1518
+ - `publish(events)` sends a plain copy of each event (`{ ...event }`) to the channel in one `send`.
1471
1519
  - `subscribe(listener)` reads the event and the consumer with `ListensTo.subscriptionOf` (`ApplicationException` without
1472
1520
  `@ListensTo`), refuses an event the registry cannot rebuild (`UnknownEventException`, at wiring
1473
1521
  time rather than at the first delivery) and calls
1474
1522
  `channel.receive(eventName, consumer, handler)`; the handler rebuilds the event
1475
1523
  with the registry and calls `listener.handle`. `unsubscribe()` closes the channel subscription.
1476
- - Listeners always receive an event rebuilt from its JSON (frozen, equal by `toJSON()`), also in
1524
+ - Listeners always receive an event rebuilt from its JSON (equal field by field), also in
1477
1525
  memory, so what works with the in-memory channel works the same with a broker.
1478
1526
  - An optional third argument, a `Logger`, logs what was published, the subscriptions and every
1479
1527
  listener failure with its event and consumer (see [Logging](#logging)); without it, nothing is
@@ -1489,9 +1537,8 @@ and throws `InternalErrorException`
1489
1537
  (`the factory of "OrderPlaced" must pass the serialized event as metadata, so it keeps its eventId and occurredOn`)
1490
1538
  when the rebuilt `eventId` (compared without case) or the `occurredOn` instant differs from the
1491
1539
  JSON. It also throws `UnknownEventException` for a name nobody registered and
1492
- `InternalErrorException` for text that is not a JSON object or has no `eventName`. The rebuilt
1493
- event is deep-frozen, like the ones an aggregate records. The factory receives the stored
1494
- `eventVersion`, so it can read older shapes (`json.eventVersion === 1 ? … : …`).
1540
+ `InternalErrorException` for text that is not a JSON object or has no `eventName`. The factory receives the JSON as it was
1541
+ stored, so it can also read an older shape of the same event.
1495
1542
 
1496
1543
  ### Example: from the use case to the listener, in memory
1497
1544
 
@@ -1519,11 +1566,11 @@ class OrderPlaced extends EventBase {
1519
1566
  static readonly eventName = 'OrderPlaced';
1520
1567
 
1521
1568
  constructor(
1522
- orderId: string,
1569
+ readonly orderId: string,
1523
1570
  readonly customer: string,
1524
1571
  metadata?: EventMetadata,
1525
1572
  ) {
1526
- super(orderId, metadata);
1573
+ super(metadata);
1527
1574
  }
1528
1575
  }
1529
1576
 
@@ -1596,7 +1643,7 @@ class SendConfirmationOnOrderPlaced extends EventListener<OrderPlaced> {
1596
1643
  }
1597
1644
 
1598
1645
  async handle(event: OrderPlaced): Promise<void> {
1599
- await this.sendConfirmation.execute({ orderId: event.aggregateId, customer: event.customer });
1646
+ await this.sendConfirmation.execute({ orderId: event.orderId, customer: event.customer });
1600
1647
  }
1601
1648
  }
1602
1649
 
@@ -1604,7 +1651,7 @@ class SendConfirmationOnOrderPlaced extends EventListener<OrderPlaced> {
1604
1651
  const mails: string[] = [];
1605
1652
  const events = new EventRegistry<OrderPlaced>().register(
1606
1653
  OrderPlaced,
1607
- (json) => new OrderPlaced(json.aggregateId, json.customer as string, json),
1654
+ (json) => new OrderPlaced(json.orderId as string, json.customer as string, json),
1608
1655
  );
1609
1656
  const channel = new InMemoryEventChannel();
1610
1657
  const bus = new DomainEventBus<OrderPlaced>(channel, events);
@@ -1618,8 +1665,8 @@ const orderId = await placeOrder.execute({ customer: 'Ana' });
1618
1665
 
1619
1666
  mails; // ['Order <orderId> confirmed to Ana']: the listener ran before publish resolved
1620
1667
  channel.sent;
1621
- // [{ eventName: 'OrderPlaced', eventVersion: 1, eventId: '…', aggregateId: orderId,
1622
- // occurredOn: '2026-10-19T09:30:00.000Z', customer: 'Ana' }]
1668
+ // [{ eventName: 'OrderPlaced', eventId: '…', occurredOn: '2026-10-19T09:30:00.000Z',
1669
+ // orderId, customer: 'Ana' }]
1623
1670
 
1624
1671
  await bus.subscribe(new SendConfirmationOnOrderPlaced(new SendConfirmation(mails)));
1625
1672
  // InfrastructureException: SendConfirmationOnOrderPlaced already receives "OrderPlaced"
@@ -1811,8 +1858,8 @@ The repository is responsible for the transaction and for everything else about
1811
1858
  database transaction that the repository opens and commits (or rolls back) within `save` itself,
1812
1859
  with its own connection.
1813
1860
  - **Concurrency**: it checks the stored version and throws `ConcurrencyException` (see
1814
- [Optimistic concurrency](#optimistic-concurrency)), calls `markPersisted()` after a write and
1815
- rebuilds with `Order.fromPrimitives(row).restoreVersion(version)` on load.
1861
+ [Optimistic concurrency](#optimistic-concurrency)) and remembers the version of every aggregate
1862
+ it loaded or saved, outside the aggregate.
1816
1863
  - **Connections and errors**: pools, sessions and driver retries stay inside the adapter, and driver
1817
1864
  errors are wrapped in `InternalErrorException` with `{ cause }`.
1818
1865
  - **Events that must not be lost**: `publish` runs after `save`, so a failure in between loses the
@@ -1827,7 +1874,7 @@ follows the same contract: every `save` and `remove` is applied whole or not at
1827
1874
  ## Event Sourcing and CQRS: only for a concrete risk
1828
1875
 
1829
1876
  Neither is required, and both add complexity [IDDD ch. 4]. This package persists the state of
1830
- aggregates; it does not ship an event store nor Event Sourcing support: `version` grows by 1 per
1877
+ aggregates; it does not ship an event store nor Event Sourcing support: the stored version grows by 1 per
1831
1878
  save, not per event, and an event-sourced aggregate manages the version of its stream itself.
1832
1879
 
1833
1880
  | Situation | Recommendation |
@@ -1847,35 +1894,16 @@ just sent, show how old the data is, or say that the request is being processed.
1847
1894
 
1848
1895
  Ready adapters for tests, prototypes and the edges of an API:
1849
1896
 
1850
- | Export | What it is |
1851
- | ---------------------------------------------- | ----------------------------------------------------------------------------------------------------------- |
1852
- | `InMemoryEventChannel({ delivery }?)` | `EventChannel` inside the process, built on emittery's techniques; `sent` lists what was sent |
1853
- | `HttpErrorMapper({ statusByCode?, logger? }?)` | `map(error)` → `{ status, body: { code, message, errors? } }` (`errors`: `{ field, code, message }[]`) |
1854
- | `ConsoleLogger({ level?, clock? }?)` | `Logger` that writes one JSON line per entry from `level` up (`info` by default) |
1855
- | `InMemoryLogger`, `SilentLogger` | `Logger` that keeps `entries` for tests, and one that writes nothing (the default of every piece that logs) |
1856
- | `SystemClock`, `FixedClock(at)` | `Clock` with the real time, and a test clock with `set(at)` and `advance(ms)` |
1897
+ | Export | What it is |
1898
+ | ------------------------------------- | ----------------------------------------------------------------------------------------------------------- |
1899
+ | `InMemoryEventChannel({ delivery }?)` | `EventChannel` inside the process, built on emittery's techniques; `sent` lists what was sent |
1900
+ | `ConsoleLogger({ level?, clock? }?)` | `Logger` that writes one JSON line per entry from `level` up (`info` by default) |
1901
+ | `InMemoryLogger`, `SilentLogger` | `Logger` that keeps `entries` for tests, and one that writes nothing (the default of every piece that logs) |
1902
+ | `SystemClock`, `FixedClock(at)` | `Clock` with the real time, and a test clock with `set(at)` and `advance(ms)` |
1857
1903
 
1858
1904
  The package ships no repository: each project writes one per aggregate and technology (see
1859
1905
  [Writing a repository](#writing-a-repository)), including the in-memory one its tests use.
1860
1906
 
1861
- `HttpErrorMapper` statuses, overridable by code (`new HttpErrorMapper({ statusByCode: { ORD001: 403 } })`):
1862
-
1863
- | Error | Status |
1864
- | ---------------------------------------------------- | ------ |
1865
- | `TypePrimitiveException`, `RequiredValueException` | 400 |
1866
- | `NotAllowedException` | 403 |
1867
- | `AggregateNotFoundException` | 404 |
1868
- | `ConcurrencyException` | 409 |
1869
- | `ValidationException` (body includes `errors`) | 422 |
1870
- | any other `DomainException` (a broken business rule) | 422 |
1871
- | any other `ApplicationException` | 400 |
1872
- | `InfrastructureException` and anything unknown | 500 |
1873
-
1874
- Every 5xx answer has the message `Internal server error`: messages of infrastructure errors (SQL,
1875
- hosts, secrets…) never reach the client. Unknown errors get the code `E000`. The overrides are
1876
- read only from the object you pass (a code such as `constructor` or `toString` is not taken from
1877
- `Object.prototype`).
1878
-
1879
1907
  `InMemoryEventChannel` is the in-process `EventChannel` behind `DomainEventBus` (see
1880
1908
  [Events](#events-the-contract-in-applicationevent)). It reimplements the techniques of
1881
1909
  [emittery](https://github.com/sindresorhus/emittery) (MIT) that a channel needs: a `Map` from event
@@ -1889,15 +1917,11 @@ delivery is not called for it; one closed before its turn is skipped), and two d
1889
1917
  | `'parallel'` | `emit` | Consumers start together; all of them run, and the failures reject `send` with one `EventDeliveryException` (`errors` holds each) |
1890
1918
 
1891
1919
  Events go one after another in the order of the `send`; a failure stops the events after it. Every
1892
- consumer gets the same frozen JSON copy, `send` resolves once the handlers finished, and `sent`
1920
+ consumer gets its own JSON copy, `send` resolves once the handlers finished, and `sent`
1893
1921
  returns a copy of what was sent, in order (for tests).
1894
1922
 
1895
1923
  ```ts
1896
- import {
1897
- DomainEventBus,
1898
- HttpErrorMapper,
1899
- InMemoryEventChannel,
1900
- } from '@archi-code/ddd/infrastructure';
1924
+ import { DomainEventBus, InMemoryEventChannel } from '@archi-code/ddd/infrastructure';
1901
1925
 
1902
1926
  const channel = new InMemoryEventChannel({ delivery: 'parallel' });
1903
1927
  const bus = new DomainEventBus<OrderEvent>(channel, events); // events: the EventRegistry of "Events"
@@ -1905,12 +1929,6 @@ await bus.subscribe(new ReserveStockOnOrderPlaced(reserveStock));
1905
1929
  await bus.subscribe(new NotifyCustomerOnOrderPlaced(notifyCustomer));
1906
1930
  await bus.publish(order.pullDomainEvents());
1907
1931
  channel.sent.map((event) => event.eventName); // ['OrderPlaced']
1908
-
1909
- const errors = new HttpErrorMapper({ statusByCode: { ORD001: 403 } });
1910
- app.use((error, req, res, next) => {
1911
- const { status, body } = errors.map(error);
1912
- res.status(status).json(body);
1913
- });
1914
1932
  ```
1915
1933
 
1916
1934
  ## Logging
@@ -1926,37 +1944,29 @@ logs on its own only where nobody else sees the error:
1926
1944
 
1927
1945
  ```ts
1928
1946
  import type { Logger } from '@archi-code/ddd/application';
1929
- import {
1930
- ConsoleLogger,
1931
- DomainEventBus,
1932
- HttpErrorMapper,
1933
- SystemClock,
1934
- } from '@archi-code/ddd/infrastructure';
1947
+ import { ConsoleLogger, DomainEventBus, SystemClock } from '@archi-code/ddd/infrastructure';
1935
1948
 
1936
1949
  const logger: Logger = new ConsoleLogger({ level: 'info', clock: new SystemClock() });
1937
1950
  logger.warn('stock_low', { sku: 'TORCH-01', units: 2 });
1938
1951
  // console.warn: {"time":"2026-10-19T09:30:00.000Z","level":"warn","message":"stock_low","context":{"sku":"TORCH-01","units":2}}
1939
1952
 
1940
1953
  const bus = new DomainEventBus<OrderEvent>(channel, registry, logger);
1941
- const errors = new HttpErrorMapper({ statusByCode: { ORD001: 409 }, logger });
1942
1954
  ```
1943
1955
 
1944
1956
  `ConsoleLogger` drops entries below `level` (`debug` < `info` < `warn` < `error`; an unknown level
1945
1957
  throws `InternalErrorException`), writes an `Error` in the context with its `name`, `code`,
1946
- `message`, `errors`, `stack` and `cause` (which `JSON.stringify` would turn into `{}`), dates and
1947
- value objects through their `toJSON`, a `bigint` as text and a circular reference as
1958
+ `message`, `errors`, `stack` and `cause` (which `JSON.stringify` would turn into `{}`), dates
1959
+ through their `toJSON`, a `bigint` as text and a circular reference as
1948
1960
  `'[Circular]'`; a context it cannot read becomes `'[Unserializable]'`, so logging never throws.
1949
1961
 
1950
1962
  What the package logs when it receives a logger:
1951
1963
 
1952
- | Where | Level | Message | Context |
1953
- | ----------------- | ------- | --------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------- |
1954
- | `DomainEventBus` | `debug` | `domain_events_published` (after the channel accepted them) | `events`: `{ eventName, eventId, aggregateId }[]`, no payload |
1955
- | `DomainEventBus` | `info` | `event_listener_subscribed`, `event_listener_unsubscribed` | `eventName`, `consumer` |
1956
- | `DomainEventBus` | `warn` | `event_listener_rejected`: a `DomainException` or `ApplicationException` (such as `ConcurrencyException`) | `eventName`, `consumer`, `eventId`, `aggregateId`, `error` |
1957
- | `DomainEventBus` | `error` | `event_listener_failed`: any other error, also an event the registry cannot rebuild | `eventName`, `consumer`, `eventId`, `aggregateId`, `error` |
1958
- | `HttpErrorMapper` | `error` | `http_request_failed`: every 5xx | `status`, `code`, `error` (with its cause) |
1959
- | `HttpErrorMapper` | `debug` | `http_request_rejected`: every 4xx | `status`, `code` |
1964
+ | Where | Level | Message | Context |
1965
+ | ---------------- | ------- | --------------------------------------------------------------------------------------------------------- | ------------------------------------------------ |
1966
+ | `DomainEventBus` | `debug` | `domain_events_published` (after the channel accepted them) | `events`: `{ eventName, eventId }[]`, no payload |
1967
+ | `DomainEventBus` | `info` | `event_listener_subscribed`, `event_listener_unsubscribed` | `eventName`, `consumer` |
1968
+ | `DomainEventBus` | `warn` | `event_listener_rejected`: a `DomainException` or `ApplicationException` (such as `ConcurrencyException`) | `eventName`, `consumer`, `eventId`, `error` |
1969
+ | `DomainEventBus` | `error` | `event_listener_failed`: any other error, also an event the registry cannot rebuild | `eventName`, `consumer`, `eventId`, `error` |
1960
1970
 
1961
1971
  The bus still rethrows a listener's error after logging it, so the channel keeps its delivery
1962
1972
  semantics (the in-memory channel rejects `send`, a broker retries). The rule for your own code is
@@ -2027,6 +2037,7 @@ import {
2027
2037
  BooleanType,
2028
2038
  Composite,
2029
2039
  CreatedAt,
2040
+ DomainException,
2030
2041
  EventBase,
2031
2042
  IdType,
2032
2043
  NumberType,
@@ -2034,7 +2045,6 @@ import {
2034
2045
  StringType,
2035
2046
  } from '@archi-code/ddd/domain';
2036
2047
  import type { EventMetadata, PrimitiveTypes } from '@archi-code/ddd/domain';
2037
- import { DomainException } from '@archi-code/ddd/exceptions';
2038
2048
 
2039
2049
  class OrderId extends IdType<'OrderId'> {}
2040
2050
  class CustomerName extends Required(StringType) {}
@@ -2048,16 +2058,23 @@ class OrderPlaced extends EventBase {
2048
2058
  static readonly eventName = 'OrderPlaced';
2049
2059
 
2050
2060
  constructor(
2051
- orderId: string,
2061
+ readonly orderId: string,
2052
2062
  readonly customer: string,
2053
2063
  metadata?: EventMetadata,
2054
2064
  ) {
2055
- super(orderId, metadata);
2065
+ super(metadata);
2056
2066
  }
2057
2067
  }
2058
2068
 
2059
2069
  class OrderShipped extends EventBase {
2060
2070
  static readonly eventName = 'OrderShipped';
2071
+
2072
+ constructor(
2073
+ readonly orderId: string,
2074
+ metadata?: EventMetadata,
2075
+ ) {
2076
+ super(metadata);
2077
+ }
2061
2078
  }
2062
2079
 
2063
2080
  type OrderEvent = OrderPlaced | OrderShipped;
@@ -2142,8 +2159,8 @@ class Order extends AggregateRoot<OrderId, OrderEvent> {
2142
2159
  }
2143
2160
 
2144
2161
  const order = Order.place(OrderId.generate(), new CustomerName('Ana'), { now: () => new Date() });
2145
- const stored = order.toPrimitives(); // plain data the repository writes with order.version + 1
2146
- Order.fromPrimitives(stored).restoreVersion(1); // what the repository returns on load
2162
+ const stored = order.toPrimitives(); // plain data the repository writes next to its version
2163
+ Order.fromPrimitives(stored); // what the repository returns on load
2147
2164
  ```
2148
2165
 
2149
2166
  ### Writing a repository
@@ -2158,8 +2175,8 @@ domain it supports (`AggregateFinder`, `AggregateSaver`, `AggregateRemover`):
2158
2175
  `remove(order)`, `findById(id)` → `Order | null`.
2159
2176
  - **The aggregate converts itself**: `order.toPrimitives()` and `Order.fromPrimitives(row)` are
2160
2177
  the aggregate's snapshot, never a domain service nor a mapper. The repository calls them
2161
- directly, gives back the stored version with `restoreVersion(version)` and maps the primitives to
2162
- its storage format.
2178
+ directly, maps the primitives to its storage format and keeps the version of each aggregate
2179
+ itself.
2163
2180
  - **It owns persistence**: atomic writes, the version check, connections and driver errors (see
2164
2181
  [Persistence belongs to the repository](#persistence-belongs-to-the-repository)).
2165
2182
 
@@ -2172,37 +2189,43 @@ import type {
2172
2189
  AggregateSaver,
2173
2190
  PrimitiveTypes,
2174
2191
  } from '@archi-code/ddd/domain';
2175
- import { ConcurrencyException } from '@archi-code/ddd/exceptions';
2192
+ import { ConcurrencyException } from '@archi-code/ddd/application';
2176
2193
 
2177
2194
  export class InMemoryOrderRepository
2178
2195
  implements AggregateFinder<Order>, AggregateSaver<Order>, AggregateRemover<Order>
2179
2196
  {
2180
2197
  private readonly rows = new Map<string, { version: number; primitives: PrimitiveTypes<Order> }>();
2198
+ private readonly versions = new WeakMap<Order, number>();
2181
2199
 
2182
2200
  async findById(id: OrderId): Promise<Order | null> {
2183
2201
  const row = this.rows.get(id.value);
2184
- return row === undefined
2185
- ? null
2186
- : Order.fromPrimitives(structuredClone(row.primitives)).restoreVersion(row.version);
2202
+ if (row === undefined) {
2203
+ return null;
2204
+ }
2205
+ const order = Order.fromPrimitives(structuredClone(row.primitives));
2206
+ this.versions.set(order, row.version);
2207
+ return order;
2187
2208
  }
2188
2209
 
2189
2210
  async save(order: Order): Promise<void> {
2190
- const key = this.currentKey(order);
2211
+ const version = this.checkedVersion(order);
2191
2212
  const primitives = structuredClone(order.toPrimitives());
2192
- this.rows.set(key, { version: order.version + 1, primitives });
2193
- order.markPersisted();
2213
+ this.rows.set(order.id.value, { version: version + 1, primitives });
2214
+ this.versions.set(order, version + 1);
2194
2215
  }
2195
2216
 
2196
2217
  async remove(order: Order): Promise<void> {
2197
- this.rows.delete(this.currentKey(order));
2218
+ this.checkedVersion(order);
2219
+ this.rows.delete(order.id.value);
2198
2220
  }
2199
2221
 
2200
- private currentKey(order: Order): string {
2222
+ private checkedVersion(order: Order): number {
2201
2223
  const key = order.id.value;
2202
- if ((this.rows.get(key)?.version ?? 0) !== order.version) {
2203
- throw new ConcurrencyException('Order', key, order.version);
2224
+ const version = this.versions.get(order) ?? 0;
2225
+ if ((this.rows.get(key)?.version ?? 0) !== version) {
2226
+ throw new ConcurrencyException('Order', key, version);
2204
2227
  }
2205
- return key;
2228
+ return version;
2206
2229
  }
2207
2230
  }
2208
2231
 
@@ -2223,23 +2246,33 @@ A Mongo repository has the same shape; only the storage changes:
2223
2246
  class MongoOrderRepository
2224
2247
  implements AggregateFinder<Order>, AggregateSaver<Order>, AggregateRemover<Order>
2225
2248
  {
2249
+ private readonly versions = new WeakMap<Order, number>();
2250
+
2226
2251
  constructor(private readonly collection: Collection<OrderDocument>) {}
2227
2252
 
2228
2253
  async findById(id: OrderId): Promise<Order | null> {
2229
2254
  const document = await this.collection.findOne({ _id: id.value });
2230
- return document === null
2231
- ? null
2232
- : Order.fromPrimitives(document.data).restoreVersion(document.version);
2255
+ if (document === null) {
2256
+ return null;
2257
+ }
2258
+ const order = Order.fromPrimitives(document.data);
2259
+ this.versions.set(order, document.version);
2260
+ return order;
2233
2261
  }
2234
2262
 
2235
- // save: updateOne({ _id, version: order.version }, { $set: { data, version: order.version + 1 } }),
2236
- // or insertOne when order.version is 0; no match → ConcurrencyException; then order.markPersisted()
2263
+ // save: version = versions.get(order) ?? 0; updateOne({ _id, version }, { $set: { data, version: version + 1 } }),
2264
+ // or insertOne when version is 0; no match → ConcurrencyException; then versions.set(order, version + 1)
2237
2265
  }
2238
2266
  ```
2239
2267
 
2240
- # Exceptions: `@archi-code/ddd/exceptions`
2268
+ # Exceptions
2241
2269
 
2242
- All of them extend `AbstractException` (which extends `Error`) and have:
2270
+ Each layer declares and exports its own exceptions: the domain ones come from
2271
+ `@archi-code/ddd/domain`, the application ones from `@archi-code/ddd/application` and the
2272
+ infrastructure ones, with `ExceptionClassifier`, from `@archi-code/ddd/infrastructure`. Every
2273
+ exception extends the base class of its layer (`DomainException`, `ApplicationException` or
2274
+ `InfrastructureException`, which extend `Error` through an internal base class the package does
2275
+ not export) and has:
2243
2276
 
2244
2277
  | Member | Description |
2245
2278
  | ------------- | --------------------------------------------------------------------------------------------- |
@@ -2252,20 +2285,22 @@ All of them extend `AbstractException` (which extends `Error`) and have:
2252
2285
  | `print()` | `'[<description>]: <message>, <ISO timestamp>'` |
2253
2286
  | `toJSON()` | `{ name, message, code, description, timestamp }` (`timestamp` in ISO 8601) |
2254
2287
 
2255
- | Class | Extends | Constructor | Codes | Message |
2256
- | ---------------------------- | ------------------------- | -------------------------------------------------------------------- | ------------------ | --------------------------------------------------------------------------------- |
2257
- | `DomainException` | `AbstractException` | `(message, exceptionCodes = [], options?)` | `DOM000` + yours | `message` |
2258
- | `ValidationException` | `DomainException` | `(errors: ValidationIssueInput[])`, keeps them as `ValidationIssue`s | `DOM000`, `DOM001` | `Validation failed, <field: message, joined with ", ">` |
2259
- | `TypePrimitiveException` | `DomainException` | `(expectedType, receivedValue, template?)` | `DOM000`, `DOM003` | `Validation Error: Expected a valid <expectedType>, but received <value>.` |
2260
- | `RequiredValueException` | `DomainException` | `(typeName, receivedValue)` | `DOM000`, `DOM004` | `Validation Error: <typeName> is required, but received <value>.` |
2261
- | `ApplicationException` | `AbstractException` | `(message, exceptionCodes = [], options?)` | `APP000` + yours | `message` |
2262
- | `InfrastructureException` | `AbstractException` | `(message, exceptionCodes = [], options?)` | `INF000` + yours | `message` |
2263
- | `InternalErrorException` | `InfrastructureException` | `(message, options?)` | `INF000`, `INF001` | `Failed, internal error: <message>` |
2264
- | `ConcurrencyException` | `ApplicationException` | `(aggregateName, id, expectedVersion, options?)` | `APP000`, `APP001` | `<aggregateName> with ID <id> was changed by someone else: expected version <n>.` |
2265
- | `AggregateNotFoundException` | `ApplicationException` | `(entity: string, id: string, options?)` | `APP000`, `APP002` | `<entity> with ID <id> not found.` |
2266
- | `NotAllowedException` | `ApplicationException` | `(message, options?)` | `APP000`, `APP003` | `message` |
2267
- | `UnknownEventException` | `InfrastructureException` | `(eventName)` | `INF000`, `INF002` | `Unknown event "<eventName>": register it before reading it.` |
2268
- | `EventDeliveryException` | `InfrastructureException` | `(eventName, errors, consumers)` | `INF000`, `INF003` | `<n> of <consumers> consumers of "<eventName>" failed.` (`errors`: each failure) |
2288
+ | Class | Subpath | Extends | Constructor | Codes | Message |
2289
+ | --------------------------------- | ----------------- | ------------------------- | -------------------------------------------------------------------- | ------------------ | --------------------------------------------------------------------------------- |
2290
+ | `DomainException` | `/domain` | `Error` | `(message, exceptionCodes = [], options?)` | `DOM000` + yours | `message` |
2291
+ | `ValidationException` | `/domain` | `DomainException` | `(errors: ValidationIssueInput[])`, keeps them as `ValidationIssue`s | `DOM000`, `DOM001` | `Validation failed, <field: message, joined with ", ">` |
2292
+ | `TypePrimitiveException` | `/domain` | `DomainException` | `(expectedType, receivedValue, template?)` | `DOM000`, `DOM003` | `Validation Error: Expected a valid <expectedType>, but received <value>.` |
2293
+ | `RequiredValueException` | `/domain` | `DomainException` | `(typeName, receivedValue)` | `DOM000`, `DOM004` | `Validation Error: <typeName> is required, but received <value>.` |
2294
+ | `InvalidEventDefinitionException` | `/domain` | `DomainException` | `(eventClassName)` | `DOM000`, `DOM005` | `<eventClassName> must declare a static eventName` |
2295
+ | `ApplicationException` | `/application` | `Error` | `(message, exceptionCodes = [], options?)` | `APP000` + yours | `message` |
2296
+ | `ConcurrencyException` | `/application` | `ApplicationException` | `(aggregateName, id, expectedVersion, options?)` | `APP000`, `APP001` | `<aggregateName> with ID <id> was changed by someone else: expected version <n>.` |
2297
+ | `AggregateNotFoundException` | `/application` | `ApplicationException` | `(entity: string, id: string, options?)` | `APP000`, `APP002` | `<entity> with ID <id> not found.` |
2298
+ | `NotAllowedException` | `/application` | `ApplicationException` | `(message, options?)` | `APP000`, `APP003` | `message` |
2299
+ | `UnexpectedErrorException` | `/application` | `ApplicationException` | `(message, options?)` | `APP000`, `APP004` | `Failed, unexpected error: <message>` |
2300
+ | `InfrastructureException` | `/infrastructure` | `Error` | `(message, exceptionCodes = [], options?)` | `INF000` + yours | `message` |
2301
+ | `InternalErrorException` | `/infrastructure` | `InfrastructureException` | `(message, options?)` | `INF000`, `INF001` | `Failed, internal error: <message>` |
2302
+ | `UnknownEventException` | `/infrastructure` | `InfrastructureException` | `(eventName)` | `INF000`, `INF002` | `Unknown event "<eventName>": register it before reading it.` |
2303
+ | `EventDeliveryException` | `/infrastructure` | `InfrastructureException` | `(eventName, errors, consumers)` | `INF000`, `INF003` | `<n> of <consumers> consumers of "<eventName>" failed.` (`errors`: each failure) |
2269
2304
 
2270
2305
  `options` is `{ cause }`: keep the original error when you wrap it. A `ValidationIssueInput` is a
2271
2306
  `ValidationIssue` (`{ field: string | null; code: string; message: string }`) whose `code` is
@@ -2273,8 +2308,15 @@ optional: it defaults to `'DOM001'`. `TypePrimitiveException` and
2273
2308
  `RequiredValueException` are the ones value objects throw; the `template` of
2274
2309
  `TypePrimitiveException` replaces `'Expected a valid '`. Both quote at most 64 characters of the
2275
2310
  value they received (`"…"` marks the cut), so a long text pasted into a field, or a whole object,
2276
- is never copied into logs or HTTP answers. The built-in codes are exported as the
2277
- `ExceptionCode` enum (`ExceptionCode.AggregateNotFound === 'APP002'`).
2311
+ is never copied into logs or answers. The built-in codes are exported by each layer as the
2312
+ `DomainExceptionCode`, `ApplicationExceptionCode` and `InfrastructureExceptionCode` enums
2313
+ (`ApplicationExceptionCode.AggregateNotFound === 'APP002'`).
2314
+
2315
+ `UnexpectedErrorException` is what `UseCase.execute` wraps any error that is not an exception of
2316
+ the package in (a bug or a driver error an adapter did not wrap), and `InvalidEventDefinitionException`
2317
+ is thrown when an event class has no `eventName`: both are mistakes in the code, so
2318
+ `ExceptionClassifier` reports them as `Internal`. `InternalErrorException` is the one adapters wrap
2319
+ their failures in.
2278
2320
 
2279
2321
  `AggregateNotFoundException` and `NotAllowedException` belong to the application: a use case
2280
2322
  throws the first when the aggregate it needs is not there (a repository returns `null`) and the
@@ -2286,16 +2328,16 @@ The built-in classes recognize their instances across the CJS and ESM builds: an
2286
2328
  `AggregateNotFoundException` (they match by code). Your own subclasses use the normal `instanceof`.
2287
2329
 
2288
2330
  ```ts
2331
+ import { DomainException } from '@archi-code/ddd/domain';
2289
2332
  import {
2290
2333
  AggregateNotFoundException,
2291
2334
  ApplicationException,
2292
- DomainException,
2293
- ExceptionCode,
2294
- InternalErrorException,
2295
- } from '@archi-code/ddd/exceptions';
2335
+ ApplicationExceptionCode,
2336
+ } from '@archi-code/ddd/application';
2337
+ import { InternalErrorException } from '@archi-code/ddd/infrastructure';
2296
2338
 
2297
2339
  const notFound = new AggregateNotFoundException('User', '42');
2298
- notFound.code === ExceptionCode.AggregateNotFound; // true
2340
+ notFound.code === ApplicationExceptionCode.AggregateNotFound; // true
2299
2341
  notFound.message; // 'User with ID 42 not found.'
2300
2342
  notFound.code; // 'APP002'
2301
2343
  notFound.description; // 'Application Exception (APP000), Aggregate Not Found (APP002)'
@@ -2332,6 +2374,64 @@ error.code; // 'ORD001'
2332
2374
  error.description; // 'Domain Exception (DOM000), OrderCancelledException (ORD001)'
2333
2375
  ```
2334
2376
 
2377
+ ### Translating errors at an entry point
2378
+
2379
+ The package speaks no protocol: HTTP, gRPC, a queue consumer or a CLI each translate errors in
2380
+ their own entry point. `ExceptionClassifier.classify(error)` gives them the one decision every
2381
+ protocol shares, the kind of failure, as an `ExceptionKind`; it takes any thrown value and
2382
+ recognizes the exceptions of the other build (CJS/ESM) too:
2383
+
2384
+ | Error | `ExceptionKind` | Usual HTTP status |
2385
+ | ------------------------------------------------------------- | -------------------------- | ----------------- |
2386
+ | `AggregateNotFoundException` | `NotFound` (`'not_found'`) | 404 |
2387
+ | `NotAllowedException` | `NotAllowed` | 403 |
2388
+ | `ConcurrencyException` | `Conflict` | 409 |
2389
+ | `UnexpectedErrorException`, `InvalidEventDefinitionException` | `Internal` | 500 |
2390
+ | `TypePrimitiveException`, `RequiredValueException` | `Malformed` | 400 |
2391
+ | `ValidationException` and any other `DomainException` | `Invalid` | 422 |
2392
+ | any other `ApplicationException` | `Rejected` | 400 |
2393
+ | `InfrastructureException` and anything unknown | `Internal` | 500 |
2394
+
2395
+ The entry point decides the rest: the status or code of its protocol, the codes of your own
2396
+ exceptions (`error.code`), the body, and logging the failure once. Never send the message of an
2397
+ `Internal` failure to the client (SQL, hosts, secrets…), and send the `errors` of a
2398
+ `ValidationException` so it can mark each field:
2399
+
2400
+ ```ts
2401
+ import { DomainException, ValidationException } from '@archi-code/ddd/domain';
2402
+ import { ApplicationException } from '@archi-code/ddd/application';
2403
+ import {
2404
+ ExceptionClassifier,
2405
+ ExceptionKind,
2406
+ InfrastructureException,
2407
+ } from '@archi-code/ddd/infrastructure';
2408
+
2409
+ const STATUS: Record<ExceptionKind, number> = {
2410
+ [ExceptionKind.NotFound]: 404,
2411
+ [ExceptionKind.NotAllowed]: 403,
2412
+ [ExceptionKind.Conflict]: 409,
2413
+ [ExceptionKind.Malformed]: 400,
2414
+ [ExceptionKind.Invalid]: 422,
2415
+ [ExceptionKind.Rejected]: 400,
2416
+ [ExceptionKind.Internal]: 500,
2417
+ };
2418
+
2419
+ app.use((error, req, res, next) => {
2420
+ const kind = ExceptionClassifier.classify(error);
2421
+ const known =
2422
+ error instanceof DomainException ||
2423
+ error instanceof ApplicationException ||
2424
+ error instanceof InfrastructureException;
2425
+ const code = known ? error.code : 'E000';
2426
+ if (kind === ExceptionKind.Internal) {
2427
+ logger.error('http_request_failed', { code, error });
2428
+ return res.status(500).json({ code, message: 'Internal server error' });
2429
+ }
2430
+ const errors = error instanceof ValidationException ? error.errors : undefined;
2431
+ res.status(STATUS[kind]).json({ code, message: error.message, errors });
2432
+ });
2433
+ ```
2434
+
2335
2435
  ## Full example
2336
2436
 
2337
2437
  ```ts
@@ -2340,26 +2440,31 @@ import {
2340
2440
  AggregateRoot,
2341
2441
  BooleanType,
2342
2442
  CreatedAt,
2443
+ DomainException,
2343
2444
  EventBase,
2344
2445
  IdType,
2345
2446
  Required,
2346
2447
  StringType,
2347
- ValueObjects,
2348
2448
  } from '@archi-code/ddd/domain';
2349
- import { AggregateNotFoundException, DomainException } from '@archi-code/ddd/exceptions';
2350
2449
  import type {
2351
2450
  AggregateFinder,
2352
2451
  AggregateSaver,
2353
2452
  EventMetadata,
2354
2453
  PrimitiveTypes,
2355
2454
  } from '@archi-code/ddd/domain';
2356
- import { EventListener, ListensTo, UseCase } from '@archi-code/ddd/application';
2455
+ import {
2456
+ AggregateNotFoundException,
2457
+ EventListener,
2458
+ ListensTo,
2459
+ UseCase,
2460
+ ValueObjectBuilder,
2461
+ } from '@archi-code/ddd/application';
2357
2462
  import type { Clock, EventBus } from '@archi-code/ddd/application';
2358
2463
  import {
2359
2464
  DomainEventBus,
2360
2465
  EventRegistry,
2466
+ ExceptionClassifier,
2361
2467
  FixedClock,
2362
- HttpErrorMapper,
2363
2468
  InMemoryEventChannel,
2364
2469
  } from '@archi-code/ddd/infrastructure';
2365
2470
  import { InMemoryUserRepository } from './test/support/in-memory-user-repository';
@@ -2375,11 +2480,11 @@ class UserRegistered extends EventBase {
2375
2480
  static readonly eventName = 'UserRegistered';
2376
2481
 
2377
2482
  constructor(
2378
- userId: string,
2483
+ readonly userId: string,
2379
2484
  readonly email: string,
2380
2485
  metadata?: EventMetadata,
2381
2486
  ) {
2382
- super(userId, metadata);
2487
+ super(metadata);
2383
2488
  }
2384
2489
  }
2385
2490
 
@@ -2387,11 +2492,11 @@ class UserRenamed extends EventBase {
2387
2492
  static readonly eventName = 'UserRenamed';
2388
2493
 
2389
2494
  constructor(
2390
- userId: string,
2495
+ readonly userId: string,
2391
2496
  readonly name: string,
2392
2497
  metadata?: EventMetadata,
2393
2498
  ) {
2394
- super(userId, metadata);
2499
+ super(metadata);
2395
2500
  }
2396
2501
  }
2397
2502
 
@@ -2472,7 +2577,7 @@ class RegisterUser extends UseCase<{ name: string; email: string }, RegisterUser
2472
2577
  }
2473
2578
 
2474
2579
  protected toCommand(input: { name: string; email: string }): RegisterUserCommand {
2475
- return ValueObjects.build({
2580
+ return ValueObjectBuilder.build({
2476
2581
  name: () => new Name(input.name),
2477
2582
  email: () => new Email(input.email),
2478
2583
  });
@@ -2546,9 +2651,12 @@ const channel = new InMemoryEventChannel();
2546
2651
  const events = new EventRegistry<UserEvent>()
2547
2652
  .register(
2548
2653
  UserRegistered,
2549
- (json) => new UserRegistered(json.aggregateId, json.email as string, json),
2654
+ (json) => new UserRegistered(json.userId as string, json.email as string, json),
2550
2655
  )
2551
- .register(UserRenamed, (json) => new UserRenamed(json.aggregateId, json.name as string, json));
2656
+ .register(
2657
+ UserRenamed,
2658
+ (json) => new UserRenamed(json.userId as string, json.name as string, json),
2659
+ );
2552
2660
  const bus = new DomainEventBus<UserEvent>(channel, events);
2553
2661
  await bus.subscribe(new SendWelcomeEmailOnUserRegistered(new SendWelcomeEmail(emails)));
2554
2662
  const clock = new FixedClock('2026-01-01T09:00:00Z');
@@ -2564,73 +2672,67 @@ emails; // ['Welcome, ana@example.com']
2564
2672
  (await users.findById(new UserId(id)))?.toPrimitives();
2565
2673
  // { id, name: 'Ana María', email: 'ana@example.com', blocked: false, createdAt: 2026-01-01T09:00:00.000Z }
2566
2674
 
2567
- const errors = new HttpErrorMapper();
2568
2675
  await new RenameUser(users, bus, clock)
2569
2676
  .execute({ id: UserId.random(), name: 'Eva' })
2570
- .catch((error) => errors.map(error));
2571
- // { status: 404, body: { code: 'APP002', message: 'User with ID … not found.' } }
2677
+ .catch((error) => ExceptionClassifier.classify(error));
2678
+ // 'not_found'
2572
2679
  await new RegisterUser(users, bus, clock)
2573
2680
  .execute({ name: 'Eva', email: 'x' })
2574
- .catch((error) => errors.map(error));
2575
- // { status: 422, body: { code: 'DOM001', message: 'Validation failed, email: Email: …', errors: [{ field: 'email', code: 'isEmail', message: 'Email: Value does not satisfy isEmail' }] } }
2681
+ .catch((error) => ExceptionClassifier.classify(error));
2682
+ // 'invalid'
2576
2683
  ```
2577
2684
 
2578
2685
  ## Exports
2579
2686
 
2580
- `@archi-code/ddd/exceptions`:
2581
-
2582
- | Export | Kind |
2583
- | -------------------------------------------------------------------------------------------------------------------------------------------- | -------------- |
2584
- | `AbstractException` | abstract class |
2585
- | `DomainException`, `ApplicationException`, `InfrastructureException` | classes |
2586
- | `ValidationException`, `AggregateNotFoundException`, `TypePrimitiveException`, `RequiredValueException`, `InternalErrorException` | classes |
2587
- | `ConcurrencyException`, `NotAllowedException`, `UnknownEventException`, `EventDeliveryException` | classes |
2588
- | `ExceptionCode` | enum |
2589
- | `ExceptionOptions`, `ValidationIssue` (`{ field: string \| null; code: string; message: string }`), `ValidationIssueInput` (`code` optional) | types |
2590
-
2591
2687
  `@archi-code/ddd/domain`:
2592
2688
 
2593
- | Export | Kind |
2594
- | --------------------------------------------------------------------------------------------------------------- | ------------------ |
2595
- | `AbstractType` | abstract class |
2596
- | `StringType`, `NumberType`, `BooleanType`, `DateType`, `UuidType`, `JsonType` | classes |
2597
- | `EnumType`, `ArrayType` | abstract classes |
2598
- | `CreatedAt`, `UpdatedAt`, `IdType` | classes (required) |
2599
- | `Required`, `Composite` | functions |
2600
- | `Specification` | abstract class |
2601
- | `CompositeSchema`, `CompositeProps`, `CompositePrimitives`, `CompositeInput`, `CompositeType`, `CompositeValue` | types |
2602
- | `ValueObjects` (`build`, `buildList`) | class |
2603
- | `Immutable` (`freeze`, `date`) | class |
2604
- | `Entity`, `AggregateRoot`, `EventBase` | abstract classes |
2605
- | `AggregateFinder`, `AggregateSaver`, `AggregateRemover` | types (contracts) |
2606
- | `RequiredType`, `JsonTypeValue`, `PrimitiveType`, `PrimitiveTypes`, `DataTypes` | types |
2607
- | `EventMetadata`, `SerializedEvent`, `EventClass` | types |
2608
- | `EntityId`, `SatisfiedBy` | types |
2609
- | `Properties`, `PropertiesData`, `MutablePropertiesData`, `Methods`, `MethodsAndProperties` | types |
2689
+ | Export | Kind |
2690
+ | ------------------------------------------------------------------------------------------------------------------------------- | -------------------- |
2691
+ | `AbstractType` | abstract class |
2692
+ | `StringType`, `NumberType`, `BooleanType`, `DateType`, `UuidType`, `JsonType` | classes |
2693
+ | `EnumType`, `ArrayType` | abstract classes |
2694
+ | `CreatedAt`, `UpdatedAt`, `IdType` | classes (required) |
2695
+ | `Required`, `Composite` | functions |
2696
+ | `Specification` | abstract class |
2697
+ | `CompositeSchema`, `CompositeProps`, `CompositePrimitives`, `CompositeInput`, `CompositeType`, `CompositeValue` | types |
2698
+ | `Entity`, `AggregateRoot`, `EventBase` | abstract classes |
2699
+ | `AggregateFinder`, `AggregateSaver`, `AggregateRemover` | types (contracts) |
2700
+ | `RequiredType`, `JsonTypeValue`, `PrimitiveType`, `PrimitiveTypes`, `DataTypes` | types |
2701
+ | `EventMetadata`, `SerializedEvent`, `EventClass` | types |
2702
+ | `EntityId`, `SatisfiedBy` | types |
2703
+ | `Properties`, `PropertiesData`, `MutablePropertiesData`, `Methods`, `MethodsAndProperties` | types |
2704
+ | `DomainException`, `ValidationException`, `TypePrimitiveException`, `RequiredValueException`, `InvalidEventDefinitionException` | classes (exceptions) |
2705
+ | `DomainExceptionCode` | enum |
2706
+ | `ValidationIssue` (`{ field: string \| null; code: string; message: string }`), `ValidationIssueInput` (`code` optional) | types |
2610
2707
 
2611
2708
  `@archi-code/ddd/application`:
2612
2709
 
2613
- | Export | Kind |
2614
- | -------------------------- | -------------------------------- |
2615
- | `UseCase`, `EventListener` | abstract classes |
2616
- | `ListensTo` | decorator |
2617
- | `ListensToOptions` | type (`{ consumer }`) |
2618
- | `ListenerSubscription` | type (`{ eventName, consumer }`) |
2619
- | `EventBus` | type (event port) |
2620
- | `Clock`, `Logger` | types (ports) |
2621
- | `LogLevel`, `LogContext` | types (logger port) |
2710
+ | Export | Kind |
2711
+ | ------------------------------------------------------------------------------------------------------------------------------- | -------------------------------- |
2712
+ | `UseCase`, `EventListener` | abstract classes |
2713
+ | `ValueObjectBuilder` (`build`, `buildList`) | class |
2714
+ | `ListensTo` | decorator |
2715
+ | `ListensToOptions` | type (`{ consumer }`) |
2716
+ | `ListenerSubscription` | type (`{ eventName, consumer }`) |
2717
+ | `EventBus` | type (event port) |
2718
+ | `Clock`, `Logger` | types (ports) |
2719
+ | `LogLevel`, `LogContext` | types (logger port) |
2720
+ | `ApplicationException`, `ConcurrencyException`, `AggregateNotFoundException`, `NotAllowedException`, `UnexpectedErrorException` | classes (exceptions) |
2721
+ | `ApplicationExceptionCode` | enum |
2622
2722
 
2623
2723
  `@archi-code/ddd/infrastructure`:
2624
2724
 
2625
- | Export | Kind |
2626
- | ---------------------------------------------------------------------- | ---------------------- |
2627
- | `InMemoryEventChannel`, `HttpErrorMapper`, `SystemClock`, `FixedClock` | classes |
2628
- | `ConsoleLogger`, `InMemoryLogger`, `SilentLogger` | classes (loggers) |
2629
- | `DomainEventBus`, `EventRegistry` | classes (event wiring) |
2630
- | `EventChannel`, `ChannelHandler`, `ChannelSubscription` | types (channel port) |
2631
- | `Subscription`, `EventFactory` | types |
2632
- | `HttpError`, `HttpErrorBody`, `InMemoryEventChannelOptions` | types |
2633
- | `HttpErrorMapperOptions`, `ConsoleLoggerOptions`, `LogEntry` | types |
2725
+ | Export | Kind |
2726
+ | ------------------------------------------------------------------------------------------------------ | ---------------------- |
2727
+ | `InMemoryEventChannel`, `SystemClock`, `FixedClock` | classes |
2728
+ | `ConsoleLogger`, `InMemoryLogger`, `SilentLogger` | classes (loggers) |
2729
+ | `DomainEventBus`, `EventRegistry` | classes (event wiring) |
2730
+ | `EventChannel`, `ChannelHandler`, `ChannelSubscription` | types (channel port) |
2731
+ | `Subscription`, `EventFactory` | types |
2732
+ | `InMemoryEventChannelOptions`, `ConsoleLoggerOptions`, `LogEntry` | types |
2733
+ | `InfrastructureException`, `InternalErrorException`, `UnknownEventException`, `EventDeliveryException` | classes (exceptions) |
2734
+ | `InfrastructureExceptionCode`, `ExceptionKind` | enums |
2735
+ | `ExceptionClassifier` (`classify`) | class |
2634
2736
 
2635
2737
  ## AI assistant skill
2636
2738