@archi-code/ddd 0.0.0-stage → 3.0.0-alpha.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (312) hide show
  1. package/LICENSE +46 -0
  2. package/README.md +2693 -2
  3. package/SKILL.md +104 -0
  4. package/ddd-skills/archi-code-ddd/imports.md +84 -0
  5. package/ddd-skills/archi-code-ddd/layers.md +299 -0
  6. package/ddd-skills/archi-code-ddd/patterns.md +676 -0
  7. package/ddd-skills/archi-code-ddd/what-not-to-do.md +189 -0
  8. package/ddd-skills/archi-code-ddd/where-each-class-goes.md +63 -0
  9. package/ddd-skills/ddd-agregados/GUIA.md +114 -0
  10. package/ddd-skills/ddd-agregados/references/consistencia-eventual.md +159 -0
  11. package/ddd-skills/ddd-agregados/references/ejemplo-ecommerce.md +52 -0
  12. package/ddd-skills/ddd-agregados/references/fabricas.md +196 -0
  13. package/ddd-skills/ddd-agregados/references/implementacion-ts.md +711 -0
  14. package/ddd-skills/ddd-arquitectura/GUIA.md +80 -0
  15. package/ddd-skills/ddd-arquitectura/assets/dependency-cruiser.cjs +92 -0
  16. package/ddd-skills/ddd-arquitectura/references/estilos-adicionales.md +51 -0
  17. package/ddd-skills/ddd-arquitectura/references/estructura-hexagonal-ts.md +352 -0
  18. package/ddd-skills/ddd-arquitectura/references/modulos.md +48 -0
  19. package/ddd-skills/ddd-auditoria-refactorizacion/GUIA.md +85 -0
  20. package/ddd-skills/ddd-auditoria-refactorizacion/references/ejemplo-auditoria.md +316 -0
  21. package/ddd-skills/ddd-auditoria-refactorizacion/references/lista-comprobacion.md +54 -0
  22. package/ddd-skills/ddd-auditoria-refactorizacion/references/plan-migracion.md +73 -0
  23. package/ddd-skills/ddd-auditoria-refactorizacion/scripts/scan_ddd_smells.py +264 -0
  24. package/ddd-skills/ddd-context-mapping/GUIA.md +83 -0
  25. package/ddd-skills/ddd-context-mapping/references/anticorrupcion-ts.md +404 -0
  26. package/ddd-skills/ddd-context-mapping/references/estilos-integracion.md +155 -0
  27. package/ddd-skills/ddd-event-sourcing-cqrs/GUIA.md +92 -0
  28. package/ddd-skills/ddd-event-sourcing-cqrs/references/event-sourcing-ts.md +471 -0
  29. package/ddd-skills/ddd-event-sourcing-cqrs/references/proyecciones-cqrs-ts.md +285 -0
  30. package/ddd-skills/ddd-eventos-dominio/GUIA.md +106 -0
  31. package/ddd-skills/ddd-eventos-dominio/references/diseno-eventos.md +131 -0
  32. package/ddd-skills/ddd-eventos-dominio/references/ejemplo-ecommerce.md +240 -0
  33. package/ddd-skills/ddd-eventos-dominio/references/publicacion-ts.md +526 -0
  34. package/ddd-skills/ddd-orquestador/GUIA.md +40 -0
  35. package/ddd-skills/ddd-orquestador/references/chuleta-reglas.md +35 -0
  36. package/ddd-skills/ddd-repositorios-persistencia/GUIA.md +121 -0
  37. package/ddd-skills/ddd-repositorios-persistencia/references/implementacion-ts.md +296 -0
  38. package/ddd-skills/ddd-repositorios-persistencia/references/tests-repositorios.md +177 -0
  39. package/ddd-skills/ddd-repositorios-persistencia/references/transacciones-ts.md +294 -0
  40. package/ddd-skills/ddd-servicios-capa-aplicacion/GUIA.md +99 -0
  41. package/ddd-skills/ddd-servicios-capa-aplicacion/references/capa-aplicacion-ts.md +264 -0
  42. package/ddd-skills/ddd-servicios-capa-aplicacion/references/ejemplo-ecommerce.md +324 -0
  43. package/ddd-skills/ddd-servicios-capa-aplicacion/references/servicios-dominio-ts.md +191 -0
  44. package/ddd-skills/ddd-value-objects-entidades/GUIA.md +118 -0
  45. package/ddd-skills/ddd-value-objects-entidades/references/ejemplo-ecommerce.md +87 -0
  46. package/ddd-skills/ddd-value-objects-entidades/references/entidades-ts.md +220 -0
  47. package/ddd-skills/ddd-value-objects-entidades/references/persistencia-valores.md +88 -0
  48. package/ddd-skills/ddd-value-objects-entidades/references/specification-y-diseno-flexible.md +119 -0
  49. package/ddd-skills/ddd-value-objects-entidades/references/validacion.md +96 -0
  50. package/ddd-skills/ddd-value-objects-entidades/references/value-objects-ts.md +377 -0
  51. package/dist/cjs/application/event/event-bus.d.ts +4 -0
  52. package/dist/cjs/application/event/event-bus.js +2 -0
  53. package/dist/cjs/application/event/event-listener.d.ts +4 -0
  54. package/dist/cjs/application/event/event-listener.js +6 -0
  55. package/dist/cjs/application/event/listens-to.d.ts +15 -0
  56. package/dist/cjs/application/event/listens-to.js +28 -0
  57. package/dist/cjs/application/index.d.ts +7 -0
  58. package/dist/cjs/application/index.js +9 -0
  59. package/dist/cjs/application/ports/clock.d.ts +3 -0
  60. package/dist/cjs/application/ports/clock.js +2 -0
  61. package/dist/cjs/application/ports/logger.d.ts +8 -0
  62. package/dist/cjs/application/ports/logger.js +2 -0
  63. package/dist/cjs/application/use-case/use-case.d.ts +6 -0
  64. package/dist/cjs/application/use-case/use-case.js +24 -0
  65. package/dist/cjs/domain/aggregate/aggregate-root.d.ts +14 -0
  66. package/dist/cjs/domain/aggregate/aggregate-root.js +33 -0
  67. package/dist/cjs/domain/entity/entity.d.ts +9 -0
  68. package/dist/cjs/domain/entity/entity.js +19 -0
  69. package/dist/cjs/domain/event/event-base.d.ts +29 -0
  70. package/dist/cjs/domain/event/event-base.js +51 -0
  71. package/dist/cjs/domain/index.d.ts +10 -0
  72. package/dist/cjs/domain/index.js +24 -0
  73. package/dist/cjs/domain/primitive/primitive-types.d.ts +22 -0
  74. package/dist/cjs/domain/primitive/primitive-types.js +2 -0
  75. package/dist/cjs/domain/repository/aggregate-finder.d.ts +4 -0
  76. package/dist/cjs/domain/repository/aggregate-finder.js +2 -0
  77. package/dist/cjs/domain/repository/aggregate-remover.d.ts +4 -0
  78. package/dist/cjs/domain/repository/aggregate-remover.js +2 -0
  79. package/dist/cjs/domain/repository/aggregate-saver.d.ts +4 -0
  80. package/dist/cjs/domain/repository/aggregate-saver.js +2 -0
  81. package/dist/cjs/domain/specification/specification.d.ts +9 -0
  82. package/dist/cjs/domain/specification/specification.js +44 -0
  83. package/dist/cjs/domain/type/abstract-type.d.ts +20 -0
  84. package/dist/cjs/domain/type/abstract-type.js +69 -0
  85. package/dist/cjs/domain/type/array-type.d.ts +16 -0
  86. package/dist/cjs/domain/type/array-type.js +65 -0
  87. package/dist/cjs/domain/type/boolean-type.d.ts +4 -0
  88. package/dist/cjs/domain/type/boolean-type.js +16 -0
  89. package/dist/cjs/domain/type/composite/composite-field.d.ts +19 -0
  90. package/dist/cjs/domain/type/composite/composite-field.js +63 -0
  91. package/dist/cjs/domain/type/composite/composite.d.ts +26 -0
  92. package/dist/cjs/domain/type/composite/composite.js +55 -0
  93. package/dist/cjs/domain/type/date-type.d.ts +18 -0
  94. package/dist/cjs/domain/type/date-type.js +35 -0
  95. package/dist/cjs/domain/type/enum-type.d.ts +7 -0
  96. package/dist/cjs/domain/type/enum-type.js +30 -0
  97. package/dist/cjs/domain/type/equality/value-equality.d.ts +7 -0
  98. package/dist/cjs/domain/type/equality/value-equality.js +41 -0
  99. package/dist/cjs/domain/type/id-type.d.ts +7 -0
  100. package/dist/cjs/domain/type/id-type.js +17 -0
  101. package/dist/cjs/domain/type/immutable/immutable-date.d.ts +4 -0
  102. package/dist/cjs/domain/type/immutable/immutable-date.js +37 -0
  103. package/dist/cjs/domain/type/immutable/immutable.d.ts +4 -0
  104. package/dist/cjs/domain/type/immutable/immutable.js +19 -0
  105. package/dist/cjs/domain/type/index.d.ts +17 -0
  106. package/dist/cjs/domain/type/index.js +33 -0
  107. package/dist/cjs/domain/type/json-type.d.ts +5 -0
  108. package/dist/cjs/domain/type/json-type.js +17 -0
  109. package/dist/cjs/domain/type/number-type.d.ts +4 -0
  110. package/dist/cjs/domain/type/number-type.js +16 -0
  111. package/dist/cjs/domain/type/required/required-mark.d.ts +9 -0
  112. package/dist/cjs/domain/type/required/required-mark.js +13 -0
  113. package/dist/cjs/domain/type/required/required.d.ts +7 -0
  114. package/dist/cjs/domain/type/required/required.js +21 -0
  115. package/dist/cjs/domain/type/string-type.d.ts +4 -0
  116. package/dist/cjs/domain/type/string-type.js +16 -0
  117. package/dist/cjs/domain/type/uuid-type.d.ts +7 -0
  118. package/dist/cjs/domain/type/uuid-type.js +27 -0
  119. package/dist/cjs/domain/type/value-objects/value-objects.d.ts +16 -0
  120. package/dist/cjs/domain/type/value-objects/value-objects.js +70 -0
  121. package/dist/cjs/exceptions/abstract.exception.d.ts +23 -0
  122. package/dist/cjs/exceptions/abstract.exception.js +66 -0
  123. package/dist/cjs/exceptions/application/aggregate-not-found.exception.d.ts +5 -0
  124. package/dist/cjs/exceptions/application/aggregate-not-found.exception.js +17 -0
  125. package/dist/cjs/exceptions/application/application.exception.d.ts +6 -0
  126. package/dist/cjs/exceptions/application/application.exception.js +17 -0
  127. package/dist/cjs/exceptions/application/concurrency.exception.d.ts +5 -0
  128. package/dist/cjs/exceptions/application/concurrency.exception.js +17 -0
  129. package/dist/cjs/exceptions/application/not-allowed.exception.d.ts +5 -0
  130. package/dist/cjs/exceptions/application/not-allowed.exception.js +17 -0
  131. package/dist/cjs/exceptions/brand/exception-brand.d.ts +6 -0
  132. package/dist/cjs/exceptions/brand/exception-brand.js +29 -0
  133. package/dist/cjs/exceptions/domain/domain.exception.d.ts +6 -0
  134. package/dist/cjs/exceptions/domain/domain.exception.js +17 -0
  135. package/dist/cjs/exceptions/domain/required-value.exception.d.ts +4 -0
  136. package/dist/cjs/exceptions/domain/required-value.exception.js +21 -0
  137. package/dist/cjs/exceptions/domain/type-primitive.exception.d.ts +4 -0
  138. package/dist/cjs/exceptions/domain/type-primitive.exception.js +21 -0
  139. package/dist/cjs/exceptions/domain/validation.exception.d.ts +14 -0
  140. package/dist/cjs/exceptions/domain/validation.exception.js +23 -0
  141. package/dist/cjs/exceptions/exception-code.d.ts +15 -0
  142. package/dist/cjs/exceptions/exception-code.js +19 -0
  143. package/dist/cjs/exceptions/index.d.ts +14 -0
  144. package/dist/cjs/exceptions/index.js +30 -0
  145. package/dist/cjs/exceptions/infrastructure/event-delivery.exception.d.ts +5 -0
  146. package/dist/cjs/exceptions/infrastructure/event-delivery.exception.js +20 -0
  147. package/dist/cjs/exceptions/infrastructure/infrastructure.exception.d.ts +6 -0
  148. package/dist/cjs/exceptions/infrastructure/infrastructure.exception.js +17 -0
  149. package/dist/cjs/exceptions/infrastructure/internal-error.exception.d.ts +5 -0
  150. package/dist/cjs/exceptions/infrastructure/internal-error.exception.js +17 -0
  151. package/dist/cjs/exceptions/infrastructure/unknown-event.exception.d.ts +4 -0
  152. package/dist/cjs/exceptions/infrastructure/unknown-event.exception.js +19 -0
  153. package/dist/cjs/exceptions/received/received-value.d.ts +4 -0
  154. package/dist/cjs/exceptions/received/received-value.js +13 -0
  155. package/dist/cjs/infrastructure/clock/fixed-clock.d.ts +8 -0
  156. package/dist/cjs/infrastructure/clock/fixed-clock.js +19 -0
  157. package/dist/cjs/infrastructure/clock/system-clock.d.ts +4 -0
  158. package/dist/cjs/infrastructure/clock/system-clock.js +9 -0
  159. package/dist/cjs/infrastructure/event/domain-event-bus.d.ts +16 -0
  160. package/dist/cjs/infrastructure/event/domain-event-bus.js +60 -0
  161. package/dist/cjs/infrastructure/event/event-channel.d.ts +9 -0
  162. package/dist/cjs/infrastructure/event/event-channel.js +2 -0
  163. package/dist/cjs/infrastructure/event/event-registry.d.ts +9 -0
  164. package/dist/cjs/infrastructure/event/event-registry.js +43 -0
  165. package/dist/cjs/infrastructure/event/in-memory/emitter.d.ts +11 -0
  166. package/dist/cjs/infrastructure/event/in-memory/emitter.js +54 -0
  167. package/dist/cjs/infrastructure/event/in-memory/in-memory-event-channel.d.ts +16 -0
  168. package/dist/cjs/infrastructure/event/in-memory/in-memory-event-channel.js +51 -0
  169. package/dist/cjs/infrastructure/http/http-error-mapper.d.ts +23 -0
  170. package/dist/cjs/infrastructure/http/http-error-mapper.js +63 -0
  171. package/dist/cjs/infrastructure/index.d.ts +16 -0
  172. package/dist/cjs/infrastructure/index.js +21 -0
  173. package/dist/cjs/infrastructure/logging/console-logger.d.ts +17 -0
  174. package/dist/cjs/infrastructure/logging/console-logger.js +37 -0
  175. package/dist/cjs/infrastructure/logging/in-memory-logger.d.ts +15 -0
  176. package/dist/cjs/infrastructure/logging/in-memory-logger.js +27 -0
  177. package/dist/cjs/infrastructure/logging/log-serializer.d.ts +7 -0
  178. package/dist/cjs/infrastructure/logging/log-serializer.js +59 -0
  179. package/dist/cjs/infrastructure/logging/silent-logger.d.ts +7 -0
  180. package/dist/cjs/infrastructure/logging/silent-logger.js +18 -0
  181. package/dist/esm/application/event/event-bus.d.ts +4 -0
  182. package/dist/esm/application/event/event-bus.js +1 -0
  183. package/dist/esm/application/event/event-listener.d.ts +4 -0
  184. package/dist/esm/application/event/event-listener.js +2 -0
  185. package/dist/esm/application/event/listens-to.d.ts +15 -0
  186. package/dist/esm/application/event/listens-to.js +25 -0
  187. package/dist/esm/application/index.d.ts +7 -0
  188. package/dist/esm/application/index.js +3 -0
  189. package/dist/esm/application/ports/clock.d.ts +3 -0
  190. package/dist/esm/application/ports/clock.js +1 -0
  191. package/dist/esm/application/ports/logger.d.ts +8 -0
  192. package/dist/esm/application/ports/logger.js +1 -0
  193. package/dist/esm/application/use-case/use-case.d.ts +6 -0
  194. package/dist/esm/application/use-case/use-case.js +20 -0
  195. package/dist/esm/domain/aggregate/aggregate-root.d.ts +14 -0
  196. package/dist/esm/domain/aggregate/aggregate-root.js +29 -0
  197. package/dist/esm/domain/entity/entity.d.ts +9 -0
  198. package/dist/esm/domain/entity/entity.js +15 -0
  199. package/dist/esm/domain/event/event-base.d.ts +29 -0
  200. package/dist/esm/domain/event/event-base.js +47 -0
  201. package/dist/esm/domain/index.d.ts +10 -0
  202. package/dist/esm/domain/index.js +6 -0
  203. package/dist/esm/domain/primitive/primitive-types.d.ts +22 -0
  204. package/dist/esm/domain/primitive/primitive-types.js +1 -0
  205. package/dist/esm/domain/repository/aggregate-finder.d.ts +4 -0
  206. package/dist/esm/domain/repository/aggregate-finder.js +1 -0
  207. package/dist/esm/domain/repository/aggregate-remover.d.ts +4 -0
  208. package/dist/esm/domain/repository/aggregate-remover.js +1 -0
  209. package/dist/esm/domain/repository/aggregate-saver.d.ts +4 -0
  210. package/dist/esm/domain/repository/aggregate-saver.js +1 -0
  211. package/dist/esm/domain/specification/specification.d.ts +9 -0
  212. package/dist/esm/domain/specification/specification.js +40 -0
  213. package/dist/esm/domain/type/abstract-type.d.ts +20 -0
  214. package/dist/esm/domain/type/abstract-type.js +65 -0
  215. package/dist/esm/domain/type/array-type.d.ts +16 -0
  216. package/dist/esm/domain/type/array-type.js +61 -0
  217. package/dist/esm/domain/type/boolean-type.d.ts +4 -0
  218. package/dist/esm/domain/type/boolean-type.js +12 -0
  219. package/dist/esm/domain/type/composite/composite-field.d.ts +19 -0
  220. package/dist/esm/domain/type/composite/composite-field.js +59 -0
  221. package/dist/esm/domain/type/composite/composite.d.ts +26 -0
  222. package/dist/esm/domain/type/composite/composite.js +52 -0
  223. package/dist/esm/domain/type/date-type.d.ts +18 -0
  224. package/dist/esm/domain/type/date-type.js +29 -0
  225. package/dist/esm/domain/type/enum-type.d.ts +7 -0
  226. package/dist/esm/domain/type/enum-type.js +26 -0
  227. package/dist/esm/domain/type/equality/value-equality.d.ts +7 -0
  228. package/dist/esm/domain/type/equality/value-equality.js +37 -0
  229. package/dist/esm/domain/type/id-type.d.ts +7 -0
  230. package/dist/esm/domain/type/id-type.js +13 -0
  231. package/dist/esm/domain/type/immutable/immutable-date.d.ts +4 -0
  232. package/dist/esm/domain/type/immutable/immutable-date.js +33 -0
  233. package/dist/esm/domain/type/immutable/immutable.d.ts +4 -0
  234. package/dist/esm/domain/type/immutable/immutable.js +15 -0
  235. package/dist/esm/domain/type/index.d.ts +17 -0
  236. package/dist/esm/domain/type/index.js +14 -0
  237. package/dist/esm/domain/type/json-type.d.ts +5 -0
  238. package/dist/esm/domain/type/json-type.js +13 -0
  239. package/dist/esm/domain/type/number-type.d.ts +4 -0
  240. package/dist/esm/domain/type/number-type.js +12 -0
  241. package/dist/esm/domain/type/required/required-mark.d.ts +9 -0
  242. package/dist/esm/domain/type/required/required-mark.js +9 -0
  243. package/dist/esm/domain/type/required/required.d.ts +7 -0
  244. package/dist/esm/domain/type/required/required.js +18 -0
  245. package/dist/esm/domain/type/string-type.d.ts +4 -0
  246. package/dist/esm/domain/type/string-type.js +12 -0
  247. package/dist/esm/domain/type/uuid-type.d.ts +7 -0
  248. package/dist/esm/domain/type/uuid-type.js +23 -0
  249. package/dist/esm/domain/type/value-objects/value-objects.d.ts +16 -0
  250. package/dist/esm/domain/type/value-objects/value-objects.js +66 -0
  251. package/dist/esm/exceptions/abstract.exception.d.ts +23 -0
  252. package/dist/esm/exceptions/abstract.exception.js +62 -0
  253. package/dist/esm/exceptions/application/aggregate-not-found.exception.d.ts +5 -0
  254. package/dist/esm/exceptions/application/aggregate-not-found.exception.js +13 -0
  255. package/dist/esm/exceptions/application/application.exception.d.ts +6 -0
  256. package/dist/esm/exceptions/application/application.exception.js +13 -0
  257. package/dist/esm/exceptions/application/concurrency.exception.d.ts +5 -0
  258. package/dist/esm/exceptions/application/concurrency.exception.js +13 -0
  259. package/dist/esm/exceptions/application/not-allowed.exception.d.ts +5 -0
  260. package/dist/esm/exceptions/application/not-allowed.exception.js +13 -0
  261. package/dist/esm/exceptions/brand/exception-brand.d.ts +6 -0
  262. package/dist/esm/exceptions/brand/exception-brand.js +25 -0
  263. package/dist/esm/exceptions/domain/domain.exception.d.ts +6 -0
  264. package/dist/esm/exceptions/domain/domain.exception.js +13 -0
  265. package/dist/esm/exceptions/domain/required-value.exception.d.ts +4 -0
  266. package/dist/esm/exceptions/domain/required-value.exception.js +17 -0
  267. package/dist/esm/exceptions/domain/type-primitive.exception.d.ts +4 -0
  268. package/dist/esm/exceptions/domain/type-primitive.exception.js +17 -0
  269. package/dist/esm/exceptions/domain/validation.exception.d.ts +14 -0
  270. package/dist/esm/exceptions/domain/validation.exception.js +19 -0
  271. package/dist/esm/exceptions/exception-code.d.ts +15 -0
  272. package/dist/esm/exceptions/exception-code.js +16 -0
  273. package/dist/esm/exceptions/index.d.ts +14 -0
  274. package/dist/esm/exceptions/index.js +14 -0
  275. package/dist/esm/exceptions/infrastructure/event-delivery.exception.d.ts +5 -0
  276. package/dist/esm/exceptions/infrastructure/event-delivery.exception.js +16 -0
  277. package/dist/esm/exceptions/infrastructure/infrastructure.exception.d.ts +6 -0
  278. package/dist/esm/exceptions/infrastructure/infrastructure.exception.js +13 -0
  279. package/dist/esm/exceptions/infrastructure/internal-error.exception.d.ts +5 -0
  280. package/dist/esm/exceptions/infrastructure/internal-error.exception.js +13 -0
  281. package/dist/esm/exceptions/infrastructure/unknown-event.exception.d.ts +4 -0
  282. package/dist/esm/exceptions/infrastructure/unknown-event.exception.js +15 -0
  283. package/dist/esm/exceptions/received/received-value.d.ts +4 -0
  284. package/dist/esm/exceptions/received/received-value.js +9 -0
  285. package/dist/esm/infrastructure/clock/fixed-clock.d.ts +8 -0
  286. package/dist/esm/infrastructure/clock/fixed-clock.js +15 -0
  287. package/dist/esm/infrastructure/clock/system-clock.d.ts +4 -0
  288. package/dist/esm/infrastructure/clock/system-clock.js +5 -0
  289. package/dist/esm/infrastructure/event/domain-event-bus.d.ts +16 -0
  290. package/dist/esm/infrastructure/event/domain-event-bus.js +56 -0
  291. package/dist/esm/infrastructure/event/event-channel.d.ts +9 -0
  292. package/dist/esm/infrastructure/event/event-channel.js +1 -0
  293. package/dist/esm/infrastructure/event/event-registry.d.ts +9 -0
  294. package/dist/esm/infrastructure/event/event-registry.js +39 -0
  295. package/dist/esm/infrastructure/event/in-memory/emitter.d.ts +11 -0
  296. package/dist/esm/infrastructure/event/in-memory/emitter.js +50 -0
  297. package/dist/esm/infrastructure/event/in-memory/in-memory-event-channel.d.ts +16 -0
  298. package/dist/esm/infrastructure/event/in-memory/in-memory-event-channel.js +47 -0
  299. package/dist/esm/infrastructure/http/http-error-mapper.d.ts +23 -0
  300. package/dist/esm/infrastructure/http/http-error-mapper.js +59 -0
  301. package/dist/esm/infrastructure/index.d.ts +16 -0
  302. package/dist/esm/infrastructure/index.js +9 -0
  303. package/dist/esm/infrastructure/logging/console-logger.d.ts +17 -0
  304. package/dist/esm/infrastructure/logging/console-logger.js +33 -0
  305. package/dist/esm/infrastructure/logging/in-memory-logger.d.ts +15 -0
  306. package/dist/esm/infrastructure/logging/in-memory-logger.js +23 -0
  307. package/dist/esm/infrastructure/logging/log-serializer.d.ts +7 -0
  308. package/dist/esm/infrastructure/logging/log-serializer.js +55 -0
  309. package/dist/esm/infrastructure/logging/silent-logger.d.ts +7 -0
  310. package/dist/esm/infrastructure/logging/silent-logger.js +14 -0
  311. package/dist/esm/package.json +3 -0
  312. package/package.json +106 -4
@@ -0,0 +1,676 @@
1
+ # Recommended patterns
2
+
3
+ Part of the `archi-code-ddd` skill: read it only when that skill routes here.
4
+
5
+ Optional by default, required with `Required(XType)`:
6
+
7
+ ```ts
8
+ class Nickname extends StringType {} // value: string | null; constructor accepts string | null | nothing
9
+ class Name extends Required(StringType) {} // value: string; constructor demands string
10
+ class FullName extends Name {} // still required
11
+ ```
12
+
13
+ Always-valid value objects: `@Validations` rules run in the constructor:
14
+
15
+ ```ts
16
+ @Validations([{ validator: 'isEmail' }])
17
+ class Email extends Required(StringType) {}
18
+
19
+ @Validations([
20
+ { validator: 'isInt', properties: { min: 0 } },
21
+ { custom: 'isEven', fn: (v) => Number(v) % 2 === 0, message: 'Must be even' },
22
+ ])
23
+ class PackSize extends NumberType {}
24
+
25
+ new Email('ana@example.com').value; // 'ana@example.com'
26
+ new Email('x');
27
+ // ValidationException: Validation failed, Email: Value does not satisfy isEmail
28
+
29
+ new PackSize(-3);
30
+ // ValidationException: Validation failed, PackSize: Value does not satisfy isInt, PackSize: Must be even
31
+
32
+ const command = ValueObjects.build({
33
+ email: () => new Email(input.email),
34
+ packSize: () => new PackSize(input.packSize),
35
+ });
36
+ // input { email: 'x', packSize: 3 } → one ValidationException with every field:
37
+ // errors: [
38
+ // { field: 'email', code: 'isEmail', message: 'Email: Value does not satisfy isEmail' },
39
+ // { field: 'packSize', code: 'isEven', message: 'PackSize: Must be even' },
40
+ // ]
41
+ ```
42
+
43
+ Aggregate with typed id, a list of composite value objects and events:
44
+
45
+ ```ts
46
+ class OrderId extends IdType<'OrderId'> {}
47
+ class Sku extends Required(StringType) {}
48
+ class Quantity extends Required(NumberType) {}
49
+ class CustomerName extends Required(StringType) {}
50
+ class Shipped extends Required(BooleanType) {}
51
+
52
+ class OrderLine extends Composite({ sku: Sku, quantity: Quantity }) {}
53
+
54
+ class OrderPlaced extends EventBase {
55
+ static readonly eventName = 'OrderPlaced';
56
+
57
+ constructor(
58
+ orderId: string,
59
+ readonly customer: string,
60
+ metadata?: EventMetadata,
61
+ ) {
62
+ super(orderId, metadata);
63
+ }
64
+ }
65
+
66
+ class OrderLineAdded extends EventBase {
67
+ static readonly eventName = 'OrderLineAdded';
68
+
69
+ constructor(
70
+ orderId: string,
71
+ readonly sku: string,
72
+ readonly quantity: number,
73
+ metadata?: EventMetadata,
74
+ ) {
75
+ super(orderId, metadata);
76
+ }
77
+ }
78
+
79
+ type OrderEvent = OrderPlaced | OrderLineAdded;
80
+
81
+ class OrderShippedException extends DomainException {
82
+ constructor(orderId: string) {
83
+ super(`Order ${orderId} is already shipped`, ['ORD001']);
84
+ }
85
+ }
86
+
87
+ class Order extends AggregateRoot<OrderId, OrderEvent> {
88
+ private constructor(
89
+ id: OrderId,
90
+ readonly customer: CustomerName,
91
+ private _lines: OrderLine[],
92
+ private _shipped: Shipped,
93
+ private _updatedAt: UpdatedAt,
94
+ readonly createdAt: CreatedAt,
95
+ ) {
96
+ super(id);
97
+ }
98
+
99
+ static place(id: OrderId, customer: CustomerName, clock: { now(): Date }): Order {
100
+ const order = new Order(
101
+ id,
102
+ customer,
103
+ [],
104
+ new Shipped(false),
105
+ UpdatedAt.now(clock),
106
+ CreatedAt.now(clock),
107
+ );
108
+ order.record(new OrderPlaced(id.value, customer.value, { clock }));
109
+ return order;
110
+ }
111
+
112
+ static fromPrimitives(primitives: PrimitiveTypes<Order>): Order {
113
+ return new Order(
114
+ new OrderId(primitives.id),
115
+ new CustomerName(primitives.customer),
116
+ primitives.lines.map((line) => OrderLine.fromPrimitives(line)),
117
+ new Shipped(primitives.shipped),
118
+ new UpdatedAt(primitives.updatedAt),
119
+ new CreatedAt(primitives.createdAt),
120
+ );
121
+ }
122
+
123
+ get lines(): OrderLine[] {
124
+ return [...this._lines];
125
+ }
126
+
127
+ get shipped(): Shipped {
128
+ return this._shipped;
129
+ }
130
+
131
+ get updatedAt(): UpdatedAt {
132
+ return this._updatedAt;
133
+ }
134
+
135
+ addLine(sku: Sku, quantity: Quantity, clock: { now(): Date }): void {
136
+ if (this._shipped.value) {
137
+ throw new OrderShippedException(this.id.value);
138
+ }
139
+ this._lines = [...this._lines, new OrderLine({ sku, quantity })];
140
+ this._updatedAt = UpdatedAt.now(clock);
141
+ this.record(new OrderLineAdded(this.id.value, sku.value, quantity.value, { clock }));
142
+ }
143
+
144
+ toPrimitives(): PrimitiveTypes<Order> {
145
+ return {
146
+ id: this.id.value,
147
+ customer: this.customer.value,
148
+ lines: this._lines.map((line) => line.toPrimitives()),
149
+ shipped: this.shipped.value,
150
+ updatedAt: this.updatedAt.value,
151
+ createdAt: this.createdAt.value,
152
+ };
153
+ }
154
+ }
155
+ ```
156
+
157
+ Use case: the input becomes a command of value objects, then `handle` loads, changes, saves and
158
+ publishes:
159
+
160
+ ```ts
161
+ interface AddOrderLineInput {
162
+ orderId: string;
163
+ sku: string;
164
+ quantity: number;
165
+ }
166
+
167
+ interface AddOrderLineCommand {
168
+ orderId: OrderId;
169
+ sku: Sku;
170
+ quantity: Quantity;
171
+ }
172
+
173
+ class AddOrderLine extends UseCase<AddOrderLineInput, AddOrderLineCommand, void> {
174
+ constructor(
175
+ private readonly orders: AggregateFinder<Order> & AggregateSaver<Order>,
176
+ private readonly events: EventBus<OrderEvent>,
177
+ private readonly clock: Clock,
178
+ ) {
179
+ super();
180
+ }
181
+
182
+ protected toCommand(input: AddOrderLineInput): AddOrderLineCommand {
183
+ return ValueObjects.build({
184
+ orderId: () => new OrderId(input.orderId),
185
+ sku: () => new Sku(input.sku),
186
+ quantity: () => new Quantity(input.quantity),
187
+ });
188
+ }
189
+
190
+ protected async handle(command: AddOrderLineCommand): Promise<void> {
191
+ const order = await this.orders.findById(command.orderId);
192
+ if (order === null) {
193
+ throw new AggregateNotFoundException('Order', command.orderId.value);
194
+ }
195
+ order.addLine(command.sku, command.quantity, this.clock);
196
+ await this.orders.save(order);
197
+ await this.events.publish(order.pullDomainEvents());
198
+ }
199
+ }
200
+ ```
201
+
202
+ Listener: named `<Action>On<Event>`, subscribed with `@ListensTo`, runs one use case:
203
+
204
+ ```ts
205
+ interface Warehouse {
206
+ prepare(orderId: OrderId, sku: Sku): Promise<void>;
207
+ }
208
+
209
+ class PrepareShipment extends UseCase<
210
+ { orderId: string; sku: string },
211
+ { orderId: OrderId; sku: Sku },
212
+ void
213
+ > {
214
+ constructor(private readonly warehouse: Warehouse) {
215
+ super();
216
+ }
217
+
218
+ protected toCommand(input: { orderId: string; sku: string }): { orderId: OrderId; sku: Sku } {
219
+ return ValueObjects.build({
220
+ orderId: () => new OrderId(input.orderId),
221
+ sku: () => new Sku(input.sku),
222
+ });
223
+ }
224
+
225
+ protected async handle(command: { orderId: OrderId; sku: Sku }): Promise<void> {
226
+ await this.warehouse.prepare(command.orderId, command.sku);
227
+ }
228
+ }
229
+
230
+ @ListensTo(OrderLineAdded, { consumer: 'prepare-shipment-on-order-line-added' })
231
+ class PrepareShipmentOnOrderLineAdded extends EventListener<OrderLineAdded> {
232
+ constructor(private readonly prepareShipment: PrepareShipment) {
233
+ super();
234
+ }
235
+
236
+ async handle(event: OrderLineAdded): Promise<void> {
237
+ await this.prepareShipment.execute({ orderId: event.aggregateId, sku: event.sku });
238
+ }
239
+ }
240
+ ```
241
+
242
+ A real repository implements only the roles it supports, checks the version (also on `remove`,
243
+ which receives the loaded aggregate) and wraps driver errors with their cause:
244
+
245
+ ```ts
246
+ class MongoOrderRepository
247
+ implements AggregateFinder<Order>, AggregateSaver<Order>, AggregateRemover<Order>
248
+ {
249
+ constructor(private readonly collection: Collection<OrderDocument>) {}
250
+
251
+ async findById(id: OrderId): Promise<Order | null> {
252
+ const document = await this.collection.findOne({ _id: id.value });
253
+ if (document === null) {
254
+ return null;
255
+ }
256
+ const { _id, version, ...primitives } = document;
257
+ return Order.fromPrimitives(primitives).restoreVersion(version);
258
+ }
259
+
260
+ async save(order: Order): Promise<void> {
261
+ const data = { ...order.toPrimitives(), version: order.version + 1 };
262
+ let written: boolean;
263
+ try {
264
+ written =
265
+ order.version === 0
266
+ ? await this.insertUnlessTaken(data)
267
+ : (await this.collection.replaceOne({ _id: data.id, version: order.version }, data))
268
+ .matchedCount === 1;
269
+ } catch (error) {
270
+ throw new InternalErrorException('saving order', { cause: error });
271
+ }
272
+ if (!written) {
273
+ throw new ConcurrencyException('Order', order.id.value, order.version);
274
+ }
275
+ order.markPersisted();
276
+ }
277
+
278
+ async remove(order: Order): Promise<void> {
279
+ const { deletedCount } = await this.collection.deleteOne({
280
+ _id: order.id.value,
281
+ version: order.version,
282
+ });
283
+ if (deletedCount === 0 && order.version > 0) {
284
+ throw new ConcurrencyException('Order', order.id.value, order.version);
285
+ }
286
+ }
287
+ }
288
+ ```
289
+
290
+ A repository per aggregate and technology: the constructor receives only its technology, the
291
+ aggregate converts itself and the repository calls it directly. The in-memory one
292
+ lives in the project's `test/support/`:
293
+
294
+ ```ts
295
+ import type {
296
+ AggregateFinder,
297
+ AggregateRemover,
298
+ AggregateSaver,
299
+ PrimitiveTypes,
300
+ } from '@archi-code/ddd/domain';
301
+ import { ConcurrencyException } from '@archi-code/ddd/exceptions';
302
+
303
+ export class InMemoryOrderRepository
304
+ implements AggregateFinder<Order>, AggregateSaver<Order>, AggregateRemover<Order>
305
+ {
306
+ private readonly rows = new Map<string, { version: number; primitives: PrimitiveTypes<Order> }>();
307
+
308
+ async findById(id: OrderId): Promise<Order | null> {
309
+ const row = this.rows.get(id.value);
310
+ return row === undefined
311
+ ? null
312
+ : Order.fromPrimitives(structuredClone(row.primitives)).restoreVersion(row.version);
313
+ }
314
+
315
+ async save(order: Order): Promise<void> {
316
+ const key = this.currentKey(order);
317
+ const primitives = structuredClone(order.toPrimitives());
318
+ this.rows.set(key, { version: order.version + 1, primitives });
319
+ order.markPersisted();
320
+ }
321
+
322
+ async remove(order: Order): Promise<void> {
323
+ this.rows.delete(this.currentKey(order));
324
+ }
325
+
326
+ private currentKey(order: Order): string {
327
+ const key = order.id.value;
328
+ if ((this.rows.get(key)?.version ?? 0) !== order.version) {
329
+ throw new ConcurrencyException('Order', key, order.version);
330
+ }
331
+ return key;
332
+ }
333
+ }
334
+ ```
335
+
336
+ Wiring and tests with the in-memory adapters (repository, event channel) and the HTTP mapper:
337
+
338
+ ```ts
339
+ const orders = new InMemoryOrderRepository();
340
+ const registry = new EventRegistry<OrderEvent>().register(
341
+ OrderLineAdded,
342
+ (json) => new OrderLineAdded(json.aggregateId, json.sku as string, json.quantity as number, json),
343
+ );
344
+ const channel = new InMemoryEventChannel(); // swap for your RabbitMQ or database channel
345
+ const bus = new DomainEventBus<OrderEvent>(channel, registry);
346
+ await bus.subscribe(new PrepareShipmentOnOrderLineAdded(new PrepareShipment(warehouse)));
347
+
348
+ const clock = new FixedClock('2026-03-01T09:30:00Z');
349
+ await orders.save(Order.place(new OrderId(orderId), new CustomerName('Ana'), clock));
350
+ await new AddOrderLine(orders, bus, clock).execute({
351
+ orderId,
352
+ sku: 'TORCH-01',
353
+ quantity: 2,
354
+ });
355
+ channel.sent.map((event) => event.eventName); // ['OrderLineAdded']: PrepareShipment already ran
356
+
357
+ const errors = new HttpErrorMapper({ statusByCode: { ORD001: 409 } }); // a domain rule defaults to 422
358
+ const { status, body } = errors.map(caughtError);
359
+ ```
360
+
361
+ A test of the event flow asserts on what the listener caused and on what the channel carried,
362
+ never on the listener being called:
363
+
364
+ ```ts
365
+ import { describe, expect, it } from 'vitest';
366
+
367
+ describe('Adding a line to an order', () => {
368
+ it('prepares the shipment of the new line', async () => {
369
+ const prepared: string[] = [];
370
+ const warehouse: Warehouse = {
371
+ prepare: async (orderId, sku) => void prepared.push(`${orderId.value}:${sku.value}`),
372
+ };
373
+ const orders = new InMemoryOrderRepository();
374
+ const channel = new InMemoryEventChannel();
375
+ const bus = new DomainEventBus<OrderEvent>(channel, registry);
376
+ await bus.subscribe(new PrepareShipmentOnOrderLineAdded(new PrepareShipment(warehouse)));
377
+ const clock = new FixedClock('2026-03-01T09:30:00Z');
378
+ await orders.save(Order.place(new OrderId(orderId), new CustomerName('Ana'), clock));
379
+
380
+ await new AddOrderLine(orders, bus, clock).execute({
381
+ orderId,
382
+ sku: 'TORCH-01',
383
+ quantity: 2,
384
+ });
385
+
386
+ expect(prepared).toEqual([`${orderId}:TORCH-01`]);
387
+ expect(channel.sent).toMatchObject([
388
+ { eventName: 'OrderLineAdded', aggregateId: orderId, sku: 'TORCH-01', quantity: 2 },
389
+ ]);
390
+ });
391
+ });
392
+ ```
393
+
394
+ Another messaging technology is one `EventChannel` class in `infrastructure/messaging/`; the
395
+ use cases, the listeners and `DomainEventBus` stay the same, and the composition root changes one
396
+ line (`new DomainEventBus(new RabbitMqEventChannel(amqpChannel), registry)`). A RabbitMQ channel
397
+ over the methods of an amqplib channel it uses, declared structurally: one durable topic exchange,
398
+ the event name as routing key, one durable queue per consumer (the `consumer` of `@ListensTo`) and event,
399
+ `ack` when the listener finished and `nack` without requeue when it failed (a dead-letter
400
+ exchange on the queue keeps those messages):
401
+
402
+ ```ts
403
+ import type { ChannelHandler, ChannelSubscription, EventChannel } from '@archi-code/ddd/infrastructure';
404
+ import type { SerializedEvent } from '@archi-code/ddd/domain';
405
+
406
+ interface AmqpMessage {
407
+ content: Buffer;
408
+ }
409
+
410
+ interface AmqpChannel {
411
+ assertExchange(exchange: string, type: 'topic', options: { durable: boolean }): Promise<unknown>;
412
+ publish(
413
+ exchange: string,
414
+ routingKey: string,
415
+ content: Buffer,
416
+ options: { persistent: boolean; messageId: string; contentType: string },
417
+ ): boolean;
418
+ assertQueue(queue: string, options: { durable: boolean }): Promise<unknown>;
419
+ bindQueue(queue: string, exchange: string, pattern: string): Promise<unknown>;
420
+ consume(
421
+ queue: string,
422
+ onMessage: (message: AmqpMessage | null) => void,
423
+ ): Promise<{ consumerTag: string }>;
424
+ ack(message: AmqpMessage): void;
425
+ nack(message: AmqpMessage, allUpTo: boolean, requeue: boolean): void;
426
+ cancel(consumerTag: string): Promise<unknown>;
427
+ }
428
+
429
+ class RabbitMqEventChannel implements EventChannel {
430
+ constructor(
431
+ private readonly amqp: AmqpChannel,
432
+ private readonly exchange = 'sales.events',
433
+ ) {}
434
+
435
+ async send(events: readonly SerializedEvent[]): Promise<void> {
436
+ await this.amqp.assertExchange(this.exchange, 'topic', { durable: true });
437
+ for (const event of events) {
438
+ this.amqp.publish(this.exchange, event.eventName, Buffer.from(JSON.stringify(event)), {
439
+ persistent: true,
440
+ messageId: event.eventId,
441
+ contentType: 'application/json',
442
+ });
443
+ }
444
+ }
445
+
446
+ async receive(
447
+ eventName: string,
448
+ consumer: string,
449
+ handler: ChannelHandler,
450
+ ): Promise<ChannelSubscription> {
451
+ const queue = `${consumer}.${eventName}`;
452
+ await this.amqp.assertExchange(this.exchange, 'topic', { durable: true });
453
+ await this.amqp.assertQueue(queue, { durable: true });
454
+ await this.amqp.bindQueue(queue, this.exchange, eventName);
455
+ const { consumerTag } = await this.amqp.consume(queue, (message) => {
456
+ if (message === null) {
457
+ return;
458
+ }
459
+ handler(JSON.parse(message.content.toString()))
460
+ .then(() => this.amqp.ack(message))
461
+ .catch(() => this.amqp.nack(message, false, false));
462
+ });
463
+ let open = true;
464
+ return {
465
+ close: async () => {
466
+ if (open) {
467
+ open = false;
468
+ await this.amqp.cancel(consumerTag);
469
+ }
470
+ },
471
+ };
472
+ }
473
+ }
474
+ ```
475
+
476
+ With a broker, `publish` resolves when RabbitMQ took the messages, not when the listener ran: an
477
+ integration test waits for the effect (polling until `prepared` has the line) instead of reading it
478
+ right after `execute`.
479
+
480
+ Tests with Vitest: describe real situations, use a plain fixture factory in the project's
481
+ `test/support/` and the native matchers (there is no archi-code test package). Dates held by VOs and
482
+ events are immutable `Date` subclasses: compare them with `toEqual` or `.toISOString()`, not
483
+ `toStrictEqual` against a plain `Date`.
484
+
485
+ ```ts
486
+ import { describe, expect, it } from 'vitest';
487
+
488
+ const orderId = '9b2f6c1e-4d3a-4f8b-a1c2-3e4d5f6a7b8c';
489
+
490
+ describe('Order', () => {
491
+ it('a customer adds a torch to a new order and the warehouse is told about it', () => {
492
+ const clock = new FixedClock('2026-03-01T09:30:00Z');
493
+ const order = Order.place(new OrderId(orderId), new CustomerName('Ana'), clock);
494
+ order.addLine(new Sku('TORCH-01'), new Quantity(2), clock);
495
+ expect(order.toPrimitives()).toMatchObject({
496
+ id: orderId,
497
+ lines: [{ sku: 'TORCH-01', quantity: 2 }],
498
+ });
499
+ expect(order.pullDomainEvents().map((event) => event.toJSON())).toMatchObject([
500
+ { eventName: 'OrderPlaced', aggregateId: orderId, customer: 'Ana' },
501
+ { eventName: 'OrderLineAdded', sku: 'TORCH-01', quantity: 2 },
502
+ ]);
503
+ });
504
+ });
505
+ ```
506
+
507
+ Composite value objects and specifications:
508
+
509
+ ```ts
510
+ @Validations([{ validator: 'isInt' }])
511
+ class Cents extends Required(NumberType) {}
512
+ @Validations([{ validator: 'isISO4217' }])
513
+ class Currency extends Required(StringType) {
514
+ protected filter(value: unknown): string {
515
+ return super.filter(value).trim().toUpperCase();
516
+ }
517
+ }
518
+
519
+ class CurrencyMismatch extends DomainException {
520
+ constructor(expected: string, received: string) {
521
+ super(`Cannot add ${received} to ${expected}`, ['PAY001']);
522
+ }
523
+ }
524
+
525
+ class Money extends Composite({ amount: Cents, currency: Currency }) {
526
+ add(other: Money): Money {
527
+ if (!this.currency.equals(other.currency)) {
528
+ throw new CurrencyMismatch(this.currency.value, other.currency.value);
529
+ }
530
+ return new Money({
531
+ amount: new Cents(this.amount.value + other.amount.value),
532
+ currency: this.currency,
533
+ });
534
+ }
535
+ }
536
+
537
+ class StartDate extends Required(DateType) {}
538
+ class EndDate extends Required(DateType) {}
539
+
540
+ @Validations([
541
+ {
542
+ custom: 'endsAfterItStarts',
543
+ fn: (range) => (range as Promotion).end.value >= (range as Promotion).start.value,
544
+ message: 'A promotion cannot end before it starts',
545
+ },
546
+ ])
547
+ class Promotion extends Composite({ start: StartDate, end: EndDate }) {}
548
+
549
+ const price = Money.fromPrimitives({ amount: 1250, currency: ' eur ' });
550
+ price.toPrimitives(); // { amount: 1250, currency: 'EUR' }
551
+ price.equals(Money.fromPrimitives({ amount: 1250, currency: 'EUR' })); // true
552
+
553
+ class OrderAbove extends Specification<Order> {
554
+ constructor(private readonly minimumQuantity: number) {
555
+ super();
556
+ }
557
+
558
+ isSatisfiedBy(order: Order): boolean {
559
+ return order.lines.reduce((sum, line) => sum + line.quantity.value, 0) >= this.minimumQuantity;
560
+ }
561
+ }
562
+
563
+ const freeShipping = new OrderAbove(10).and(new OrderAbove(100).not());
564
+ freeShipping.isSatisfiedBy(order);
565
+
566
+ const paidByCard: SatisfiedBy<Order> = { isSatisfiedBy: (candidate) => candidate.paidByCard() };
567
+ new OrderAbove(10).and(paidByCard); // any object with isSatisfiedBy combines too
568
+ ```
569
+
570
+ A state as an enum value object: the primitive is the enum option, so the round trip needs no cast:
571
+
572
+ ```ts
573
+ enum Status {
574
+ Pending = 'pending',
575
+ Shipped = 'shipped',
576
+ }
577
+
578
+ class StatusType extends EnumType<Status> {
579
+ protected getEnum(): typeof Status {
580
+ return Status;
581
+ }
582
+ }
583
+
584
+ class OrderStatus extends Required(StatusType) {
585
+ isShipped(): boolean {
586
+ return this.value === Status.Shipped;
587
+ }
588
+ }
589
+
590
+ new OrderStatus(primitives.status); // primitives.status: Status, from PrimitiveTypes<Order>
591
+ ```
592
+
593
+ A use case run by a listener registers a fact: it never refuses it, and returns when the aggregate
594
+ is missing instead of making the channel redeliver forever:
595
+
596
+ ```ts
597
+ class RegisterReservation extends UseCase<ReservationInput, ReservationCommand, void> {
598
+ // constructor and toCommand as in AddOrderLine
599
+
600
+ protected async handle(command: ReservationCommand): Promise<void> {
601
+ const order = await this.orders.findById(command.orderId);
602
+ if (order === null) {
603
+ return;
604
+ }
605
+ order.registerStockReservation(command.reservationId, this.clock);
606
+ await this.orders.save(order);
607
+ await this.events.publish(order.pullDomainEvents());
608
+ }
609
+ }
610
+ ```
611
+
612
+ A read query: an application interface returning DTOs, implemented in infrastructure on the
613
+ database, never through aggregates or repositories:
614
+
615
+ ```ts
616
+ export interface OrderSummary {
617
+ readonly id: string;
618
+ readonly customer: string;
619
+ readonly lines: number;
620
+ }
621
+
622
+ export interface OrderSummaries {
623
+ ofCustomer(
624
+ customer: CustomerName,
625
+ page: { limit: number; offset: number },
626
+ ): Promise<OrderSummary[]>;
627
+ }
628
+ ```
629
+
630
+ One repository contract, run against the in-memory reference and the real adapter:
631
+
632
+ ```ts
633
+ export function orderRepositoryContract(
634
+ make: () => Promise<AggregateFinder<Order> & AggregateSaver<Order> & AggregateRemover<Order>>,
635
+ ): void {
636
+ const clock = new FixedClock('2026-03-01T09:30:00Z');
637
+ const placeOrder = () => Order.place(new OrderId(orderId), new CustomerName('Ana'), clock);
638
+
639
+ it('loads what it saved', async () => {
640
+ const orders = await make();
641
+ const order = placeOrder();
642
+ await orders.save(order);
643
+ expect((await orders.findById(order.id))?.toPrimitives()).toEqual(order.toPrimitives());
644
+ });
645
+
646
+ it('saves the same object twice', async () => {
647
+ const orders = await make();
648
+ const order = placeOrder();
649
+ await orders.save(order);
650
+ await expect(orders.save(order)).resolves.toBeUndefined();
651
+ });
652
+
653
+ it('refuses a stale save and a new order with a taken id', async () => {
654
+ const orders = await make();
655
+ await orders.save(placeOrder());
656
+ const first = await orders.findById(new OrderId(orderId));
657
+ const second = await orders.findById(new OrderId(orderId));
658
+ await orders.save(first!);
659
+ await expect(orders.save(second!)).rejects.toThrow(ConcurrencyException);
660
+ await expect(orders.save(placeOrder())).rejects.toThrow(ConcurrencyException);
661
+ });
662
+
663
+ it('refuses to remove an order someone changed after it was loaded', async () => {
664
+ const orders = await make();
665
+ await orders.save(placeOrder());
666
+ const first = await orders.findById(new OrderId(orderId));
667
+ const second = await orders.findById(new OrderId(orderId));
668
+ await orders.save(first!);
669
+ await expect(orders.remove(second!)).rejects.toThrow(ConcurrencyException);
670
+ });
671
+ }
672
+
673
+ describe('InMemoryOrderRepository', () =>
674
+ orderRepositoryContract(async () => new InMemoryOrderRepository()));
675
+ describe('MongoOrderRepository', () => orderRepositoryContract(async () => mongoOrders()));
676
+ ```