ddd-ts-kit 0.13.0 → 0.15.0

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 (457) hide show
  1. package/README.md +34 -3
  2. package/dist/check/catalogue/architecture-rules.d.ts.map +1 -1
  3. package/dist/check/catalogue/architecture-rules.js +2 -0
  4. package/dist/check/catalogue/architecture-rules.js.map +1 -1
  5. package/dist/check/errors/kit-error-codes.d.ts.map +1 -1
  6. package/dist/check/errors/kit-error-codes.js +1 -0
  7. package/dist/check/errors/kit-error-codes.js.map +1 -1
  8. package/dist/check/explain/authz-docs.d.ts.map +1 -1
  9. package/dist/check/explain/authz-docs.js +12 -0
  10. package/dist/check/explain/authz-docs.js.map +1 -1
  11. package/dist/check/explain/errors-docs.js +1 -1
  12. package/dist/check/explain/errors-docs.js.map +1 -1
  13. package/dist/check/explain/tenancy-docs.d.ts.map +1 -1
  14. package/dist/check/explain/tenancy-docs.js +3 -1
  15. package/dist/check/explain/tenancy-docs.js.map +1 -1
  16. package/dist/check/fixtures/authz-fixtures.d.ts.map +1 -1
  17. package/dist/check/fixtures/authz-fixtures.js +38 -1
  18. package/dist/check/fixtures/authz-fixtures.js.map +1 -1
  19. package/dist/check/fixtures/tenancy-fixtures.d.ts.map +1 -1
  20. package/dist/check/fixtures/tenancy-fixtures.js +28 -0
  21. package/dist/check/fixtures/tenancy-fixtures.js.map +1 -1
  22. package/dist/check/rules/authz-fields-by-schema.d.ts +3 -0
  23. package/dist/check/rules/authz-fields-by-schema.d.ts.map +1 -0
  24. package/dist/check/rules/authz-fields-by-schema.js +33 -0
  25. package/dist/check/rules/authz-fields-by-schema.js.map +1 -0
  26. package/dist/check/tenancy/knex-unscoped.d.ts +6 -1
  27. package/dist/check/tenancy/knex-unscoped.d.ts.map +1 -1
  28. package/dist/check/tenancy/knex-unscoped.js +19 -6
  29. package/dist/check/tenancy/knex-unscoped.js.map +1 -1
  30. package/dist/check/tenancy/raw-reaches-rows.d.ts +8 -0
  31. package/dist/check/tenancy/raw-reaches-rows.d.ts.map +1 -0
  32. package/dist/check/tenancy/raw-reaches-rows.js +91 -0
  33. package/dist/check/tenancy/raw-reaches-rows.js.map +1 -0
  34. package/dist/check/tenancy/raw-sql-text.d.ts +9 -0
  35. package/dist/check/tenancy/raw-sql-text.d.ts.map +1 -0
  36. package/dist/check/tenancy/raw-sql-text.js +29 -0
  37. package/dist/check/tenancy/raw-sql-text.js.map +1 -0
  38. package/dist/commands/generate.js +2 -0
  39. package/dist/commands/generate.js.map +1 -1
  40. package/dist/commands/init.d.ts.map +1 -1
  41. package/dist/commands/init.js +13 -1
  42. package/dist/commands/init.js.map +1 -1
  43. package/dist/config/index.d.ts +1 -1
  44. package/dist/config/index.d.ts.map +1 -1
  45. package/dist/config/index.js +1 -1
  46. package/dist/config/index.js.map +1 -1
  47. package/dist/config/parse-stack.d.ts.map +1 -1
  48. package/dist/config/parse-stack.js +3 -1
  49. package/dist/config/parse-stack.js.map +1 -1
  50. package/dist/config/stack.d.ts +14 -0
  51. package/dist/config/stack.d.ts.map +1 -1
  52. package/dist/config/stack.js +2 -0
  53. package/dist/config/stack.js.map +1 -1
  54. package/dist/declared/injection/laid-out-calls.d.ts +7 -0
  55. package/dist/declared/injection/laid-out-calls.d.ts.map +1 -0
  56. package/dist/declared/injection/laid-out-calls.js +14 -0
  57. package/dist/declared/injection/laid-out-calls.js.map +1 -0
  58. package/dist/declared/render-barrel.d.ts.map +1 -1
  59. package/dist/declared/render-barrel.js +2 -1
  60. package/dist/declared/render-barrel.js.map +1 -1
  61. package/dist/declared/render-dependencies-file.d.ts.map +1 -1
  62. package/dist/declared/render-dependencies-file.js +2 -1
  63. package/dist/declared/render-dependencies-file.js.map +1 -1
  64. package/dist/describe/context-description.d.ts +6 -0
  65. package/dist/describe/context-description.d.ts.map +1 -1
  66. package/dist/describe/format-description.js +9 -1
  67. package/dist/describe/format-description.js.map +1 -1
  68. package/dist/describe/presentation-of-route.d.ts +4 -0
  69. package/dist/describe/presentation-of-route.d.ts.map +1 -0
  70. package/dist/describe/presentation-of-route.js +15 -0
  71. package/dist/describe/presentation-of-route.js.map +1 -0
  72. package/dist/describe/read-controllers.d.ts.map +1 -1
  73. package/dist/describe/read-controllers.js +4 -1
  74. package/dist/describe/read-controllers.js.map +1 -1
  75. package/dist/describe/read-routes.d.ts.map +1 -1
  76. package/dist/describe/read-routes.js +2 -0
  77. package/dist/describe/read-routes.js.map +1 -1
  78. package/dist/generate/kind.d.ts +2 -0
  79. package/dist/generate/kind.d.ts.map +1 -1
  80. package/dist/generate/kinds/context-kind.d.ts.map +1 -1
  81. package/dist/generate/kinds/context-kind.js +13 -6
  82. package/dist/generate/kinds/context-kind.js.map +1 -1
  83. package/dist/generate/kinds/form-kind.d.ts +3 -0
  84. package/dist/generate/kinds/form-kind.d.ts.map +1 -0
  85. package/dist/generate/kinds/form-kind.js +38 -0
  86. package/dist/generate/kinds/form-kind.js.map +1 -0
  87. package/dist/generate/kinds/next-adr-number.d.ts +4 -0
  88. package/dist/generate/kinds/next-adr-number.d.ts.map +1 -0
  89. package/dist/generate/kinds/next-adr-number.js +7 -0
  90. package/dist/generate/kinds/next-adr-number.js.map +1 -0
  91. package/dist/generate/kinds/presentation/controller-of.d.ts +10 -0
  92. package/dist/generate/kinds/presentation/controller-of.d.ts.map +1 -0
  93. package/dist/generate/kinds/presentation/controller-of.js +13 -0
  94. package/dist/generate/kinds/presentation/controller-of.js.map +1 -0
  95. package/dist/generate/kinds/presentation/controller-source.d.ts +13 -0
  96. package/dist/generate/kinds/presentation/controller-source.d.ts.map +1 -0
  97. package/dist/generate/kinds/presentation/controller-source.js +44 -0
  98. package/dist/generate/kinds/presentation/controller-source.js.map +1 -0
  99. package/dist/generate/kinds/representation-kind.d.ts +3 -0
  100. package/dist/generate/kinds/representation-kind.d.ts.map +1 -0
  101. package/dist/generate/kinds/representation-kind.js +54 -0
  102. package/dist/generate/kinds/representation-kind.js.map +1 -0
  103. package/dist/generate/kinds/write-adr.js +3 -6
  104. package/dist/generate/kinds/write-adr.js.map +1 -1
  105. package/dist/generate/kinds.d.ts.map +1 -1
  106. package/dist/generate/kinds.js +4 -0
  107. package/dist/generate/kinds.js.map +1 -1
  108. package/dist/help/usage.d.ts +1 -1
  109. package/dist/help/usage.d.ts.map +1 -1
  110. package/dist/help/usage.js +3 -2
  111. package/dist/help/usage.js.map +1 -1
  112. package/dist/init/files/platform-files.js +3 -1
  113. package/dist/init/files/platform-files.js.map +1 -1
  114. package/dist/init/manifest/versions.js +24 -24
  115. package/dist/init/run-init.d.ts +2 -0
  116. package/dist/init/run-init.d.ts.map +1 -1
  117. package/dist/init/run-init.js +15 -1
  118. package/dist/init/run-init.js.map +1 -1
  119. package/dist/init/stack/resolve-stack.d.ts.map +1 -1
  120. package/dist/init/stack/resolve-stack.js +15 -1
  121. package/dist/init/stack/resolve-stack.js.map +1 -1
  122. package/dist/init/stack/stack-options.d.ts +2 -0
  123. package/dist/init/stack/stack-options.d.ts.map +1 -1
  124. package/dist/presets/apply-preset.d.ts +15 -0
  125. package/dist/presets/apply-preset.d.ts.map +1 -0
  126. package/dist/presets/apply-preset.js +94 -0
  127. package/dist/presets/apply-preset.js.map +1 -0
  128. package/dist/presets/grant-to-signed-in.d.ts +9 -0
  129. package/dist/presets/grant-to-signed-in.d.ts.map +1 -0
  130. package/dist/presets/grant-to-signed-in.js +61 -0
  131. package/dist/presets/grant-to-signed-in.js.map +1 -0
  132. package/dist/presets/identity-preset.d.ts +9 -0
  133. package/dist/presets/identity-preset.d.ts.map +1 -0
  134. package/dist/presets/identity-preset.js +44 -0
  135. package/dist/presets/identity-preset.js.map +1 -0
  136. package/dist/presets/identity-refusal.d.ts +5 -0
  137. package/dist/presets/identity-refusal.d.ts.map +1 -0
  138. package/dist/presets/identity-refusal.js +29 -0
  139. package/dist/presets/identity-refusal.js.map +1 -0
  140. package/dist/presets/identity-terms.d.ts +4 -0
  141. package/dist/presets/identity-terms.d.ts.map +1 -0
  142. package/dist/presets/identity-terms.js +57 -0
  143. package/dist/presets/identity-terms.js.map +1 -0
  144. package/dist/presets/no-flags.d.ts +4 -0
  145. package/dist/presets/no-flags.d.ts.map +1 -0
  146. package/dist/presets/no-flags.js +18 -0
  147. package/dist/presets/no-flags.js.map +1 -0
  148. package/dist/presets/organisations-as-tenants.d.ts +9 -0
  149. package/dist/presets/organisations-as-tenants.d.ts.map +1 -0
  150. package/dist/presets/organisations-as-tenants.js +43 -0
  151. package/dist/presets/organisations-as-tenants.js.map +1 -0
  152. package/dist/presets/preset-decision.d.ts +8 -0
  153. package/dist/presets/preset-decision.d.ts.map +1 -0
  154. package/dist/presets/preset-decision.js +19 -0
  155. package/dist/presets/preset-decision.js.map +1 -0
  156. package/dist/presets/preset-file.d.ts +13 -0
  157. package/dist/presets/preset-file.d.ts.map +1 -0
  158. package/dist/presets/preset-file.js +2 -0
  159. package/dist/presets/preset-file.js.map +1 -0
  160. package/dist/presets/preset-files.d.ts +9 -0
  161. package/dist/presets/preset-files.d.ts.map +1 -0
  162. package/dist/presets/preset-files.js +34 -0
  163. package/dist/presets/preset-files.js.map +1 -0
  164. package/dist/presets/preset-named.d.ts +3 -0
  165. package/dist/presets/preset-named.d.ts.map +1 -0
  166. package/dist/presets/preset-named.js +12 -0
  167. package/dist/presets/preset-named.js.map +1 -0
  168. package/dist/presets/preset-refusal.d.ts +6 -0
  169. package/dist/presets/preset-refusal.d.ts.map +1 -0
  170. package/dist/presets/preset-refusal.js +2 -0
  171. package/dist/presets/preset-refusal.js.map +1 -0
  172. package/dist/presets/preset-step.d.ts +8 -0
  173. package/dist/presets/preset-step.d.ts.map +1 -0
  174. package/dist/presets/preset-step.js +2 -0
  175. package/dist/presets/preset-step.js.map +1 -0
  176. package/dist/presets/preset.d.ts +36 -0
  177. package/dist/presets/preset.d.ts.map +1 -0
  178. package/dist/presets/preset.js +2 -0
  179. package/dist/presets/preset.js.map +1 -0
  180. package/dist/presets/with-authorizer-import.d.ts +6 -0
  181. package/dist/presets/with-authorizer-import.d.ts.map +1 -0
  182. package/dist/presets/with-authorizer-import.js +27 -0
  183. package/dist/presets/with-authorizer-import.js.map +1 -0
  184. package/dist/stack/stack-vars.js +19 -2
  185. package/dist/stack/stack-vars.js.map +1 -1
  186. package/dist/version.d.ts +1 -1
  187. package/dist/version.js +1 -1
  188. package/dist/workspace/workspace.d.ts +2 -0
  189. package/dist/workspace/workspace.d.ts.map +1 -1
  190. package/dist/workspace/workspace.js +4 -0
  191. package/dist/workspace/workspace.js.map +1 -1
  192. package/package.json +2 -2
  193. package/templates/docs/openapi.json.tpl +15 -0
  194. package/templates/init/CLAUDE.md.tpl +2 -2
  195. package/templates/init/platform/auth/http-boundary.ts.tpl +20 -3
  196. package/templates/init/platform/compose-application.manual.ts.tpl +18 -0
  197. package/templates/init/platform/compose-application.tsyringe.ts.tpl +23 -0
  198. package/templates/init/platform/http/presentation-of.ts.tpl +27 -0
  199. package/templates/init/platform/http/start-http-server.express.ts.tpl +1 -1
  200. package/templates/init/platform/http/start-http-server.fastify.ts.tpl +1 -1
  201. package/templates/init/platform/http/start-http-server.hono.ts.tpl +1 -1
  202. package/templates/init/platform/http/start-http-server.nest.ts.tpl +2 -2
  203. package/templates/init/platform/http/with-documentation.ts.tpl +13 -3
  204. package/templates/init/platform/http/with-forms.ts.tpl +7 -0
  205. package/templates/init/rules/application.md.tpl +1 -1
  206. package/templates/init/rules/presentation.md.tpl +1 -1
  207. package/templates/init/scripts/openapi.ts.tpl +27 -6
  208. package/templates/pipeline/biome.json.tpl +1 -0
  209. package/templates/pipeline/package-scripts.json +1 -1
  210. package/templates/presets/identity/context/application/authorization/__context__.permissions.ts.tpl +23 -0
  211. package/templates/presets/identity/context/application/authorization/change-member-roles.policy.ts.tpl +17 -0
  212. package/templates/presets/identity/context/application/authorization/grant-delegations.policy.ts.tpl +17 -0
  213. package/templates/presets/identity/context/application/authorization/invite-members.policy.ts.tpl +17 -0
  214. package/templates/presets/identity/context/application/authorization/read-members.policy.ts.tpl +17 -0
  215. package/templates/presets/identity/context/application/authorization/remove-members.policy.ts.tpl +17 -0
  216. package/templates/presets/identity/context/application/authorization/revoke-delegations.policy.ts.tpl +17 -0
  217. package/templates/presets/identity/context/application/authorization/revoke-invitations.policy.ts.tpl +17 -0
  218. package/templates/presets/identity/context/application/commands/accept-invitation/accept-invitation.command.ts.tpl +14 -0
  219. package/templates/presets/identity/context/application/commands/accept-invitation/accept-invitation.handler.ts.tpl +56 -0
  220. package/templates/presets/identity/context/application/commands/change-member-role/change-member-role.command.ts.tpl +17 -0
  221. package/templates/presets/identity/context/application/commands/change-member-role/change-member-role.handler.ts.tpl +82 -0
  222. package/templates/presets/identity/context/application/commands/deactivate-account/deactivate-account.command.ts.tpl +7 -0
  223. package/templates/presets/identity/context/application/commands/deactivate-account/deactivate-account.handler.ts.tpl +69 -0
  224. package/templates/presets/identity/context/application/commands/decline-invitation/decline-invitation.command.ts.tpl +14 -0
  225. package/templates/presets/identity/context/application/commands/decline-invitation/decline-invitation.handler.ts.tpl +36 -0
  226. package/templates/presets/identity/context/application/commands/grant-delegation/grant-delegation.command.ts.tpl +21 -0
  227. package/templates/presets/identity/context/application/commands/grant-delegation/grant-delegation.handler.ts.tpl +43 -0
  228. package/templates/presets/identity/context/application/commands/invite-member/invite-member.command.ts.tpl +16 -0
  229. package/templates/presets/identity/context/application/commands/invite-member/invite-member.handler.ts.tpl +53 -0
  230. package/templates/presets/identity/context/application/commands/open-organisation/open-organisation.command.ts.tpl +14 -0
  231. package/templates/presets/identity/context/application/commands/open-organisation/open-organisation.handler.ts.tpl +50 -0
  232. package/templates/presets/identity/context/application/commands/provision-account/provision-account.command.ts.tpl +14 -0
  233. package/templates/presets/identity/context/application/commands/provision-account/provision-account.handler.ts.tpl +41 -0
  234. package/templates/presets/identity/context/application/commands/remove-member/remove-member.command.ts.tpl +15 -0
  235. package/templates/presets/identity/context/application/commands/remove-member/remove-member.handler.ts.tpl +79 -0
  236. package/templates/presets/identity/context/application/commands/revoke-delegation/revoke-delegation.command.ts.tpl +15 -0
  237. package/templates/presets/identity/context/application/commands/revoke-delegation/revoke-delegation.handler.ts.tpl +39 -0
  238. package/templates/presets/identity/context/application/commands/revoke-invitation/revoke-invitation.command.ts.tpl +15 -0
  239. package/templates/presets/identity/context/application/commands/revoke-invitation/revoke-invitation.handler.ts.tpl +42 -0
  240. package/templates/presets/identity/context/application/integration/handlers/account-deactivated.integration-handler.ts.tpl +19 -0
  241. package/templates/presets/identity/context/application/integration/handlers/member-joined.integration-handler.ts.tpl +33 -0
  242. package/templates/presets/identity/context/application/integration/handlers/member-removed.integration-handler.ts.tpl +30 -0
  243. package/templates/presets/identity/context/application/integration/handlers/organisation-opened.integration-handler.ts.tpl +19 -0
  244. package/templates/presets/identity/context/application/integration/publishers/account-deactivated.publisher.ts.tpl +24 -0
  245. package/templates/presets/identity/context/application/integration/publishers/delegation-granted.publisher.ts.tpl +36 -0
  246. package/templates/presets/identity/context/application/integration/publishers/delegation-revoked.publisher.ts.tpl +33 -0
  247. package/templates/presets/identity/context/application/integration/publishers/member-joined.publisher.ts.tpl +33 -0
  248. package/templates/presets/identity/context/application/integration/publishers/member-removed.publisher.ts.tpl +33 -0
  249. package/templates/presets/identity/context/application/integration/publishers/member-role-changed.publisher.ts.tpl +35 -0
  250. package/templates/presets/identity/context/application/integration/publishers/organisation-opened.publisher.ts.tpl +32 -0
  251. package/templates/presets/identity/context/application/ports/identity-provider-admin.port.ts.tpl +17 -0
  252. package/templates/presets/identity/context/application/ports/identity-provider-connection.port.ts.tpl +14 -0
  253. package/templates/presets/identity/context/application/ports/invitation-tokens.port.ts.tpl +7 -0
  254. package/templates/presets/identity/context/application/ports/search-members-reader.port.ts.tpl +10 -0
  255. package/templates/presets/identity/context/application/queries/search-members/search-members.handler.ts.tpl +33 -0
  256. package/templates/presets/identity/context/application/queries/search-members/search-members.query.ts.tpl +19 -0
  257. package/templates/presets/identity/context/application/queries/search-members/search-members.result.ts.tpl +17 -0
  258. package/templates/presets/identity/context/domain/aggregates/account.aggregate.ts.tpl +92 -0
  259. package/templates/presets/identity/context/domain/aggregates/delegation.aggregate.ts.tpl +117 -0
  260. package/templates/presets/identity/context/domain/aggregates/invitation.aggregate.ts.tpl +128 -0
  261. package/templates/presets/identity/context/domain/aggregates/member.aggregate.ts.tpl +149 -0
  262. package/templates/presets/identity/context/domain/aggregates/organisation.aggregate.ts.tpl +52 -0
  263. package/templates/presets/identity/context/domain/errors/account.error.ts.tpl +40 -0
  264. package/templates/presets/identity/context/domain/errors/delegation.error.ts.tpl +45 -0
  265. package/templates/presets/identity/context/domain/errors/invitation.error.ts.tpl +30 -0
  266. package/templates/presets/identity/context/domain/errors/member.error.ts.tpl +50 -0
  267. package/templates/presets/identity/context/domain/errors/organisation.error.ts.tpl +32 -0
  268. package/templates/presets/identity/context/domain/events/account-deactivated.event.ts.tpl +13 -0
  269. package/templates/presets/identity/context/domain/events/account-provisioned.event.ts.tpl +17 -0
  270. package/templates/presets/identity/context/domain/events/delegation-granted.event.ts.tpl +22 -0
  271. package/templates/presets/identity/context/domain/events/delegation-revoked.event.ts.tpl +18 -0
  272. package/templates/presets/identity/context/domain/events/invitation-accepted.event.ts.tpl +17 -0
  273. package/templates/presets/identity/context/domain/events/invitation-declined.event.ts.tpl +16 -0
  274. package/templates/presets/identity/context/domain/events/invitation-issued.event.ts.tpl +22 -0
  275. package/templates/presets/identity/context/domain/events/invitation-revoked.event.ts.tpl +16 -0
  276. package/templates/presets/identity/context/domain/events/member-joined.event.ts.tpl +19 -0
  277. package/templates/presets/identity/context/domain/events/member-removed.event.ts.tpl +19 -0
  278. package/templates/presets/identity/context/domain/events/member-role-changed.event.ts.tpl +22 -0
  279. package/templates/presets/identity/context/domain/events/organisation-opened.event.ts.tpl +17 -0
  280. package/templates/presets/identity/context/domain/repositories/account.repository.ts.tpl +5 -0
  281. package/templates/presets/identity/context/domain/repositories/delegation.repository.ts.tpl +10 -0
  282. package/templates/presets/identity/context/domain/repositories/invitation.repository.ts.tpl +7 -0
  283. package/templates/presets/identity/context/domain/repositories/member.repository.ts.tpl +13 -0
  284. package/templates/presets/identity/context/domain/repositories/organisation.repository.ts.tpl +5 -0
  285. package/templates/presets/identity/context/domain/services/role-rights.service.ts.tpl +59 -0
  286. package/templates/presets/identity/context/domain/value-objects/delegation-period.vo.ts.tpl +42 -0
  287. package/templates/presets/identity/context/domain/value-objects/display-name.vo.ts.tpl +29 -0
  288. package/templates/presets/identity/context/domain/value-objects/email-address.vo.ts.tpl +31 -0
  289. package/templates/presets/identity/context/domain/value-objects/organisation-name.vo.ts.tpl +29 -0
  290. package/templates/presets/identity/context/domain/value-objects/organisation-rights.vo.ts.tpl +43 -0
  291. package/templates/presets/identity/context/domain/value-objects/organisation-slug.vo.ts.tpl +30 -0
  292. package/templates/presets/identity/context/domain/vocabularies/account-status.vocabulary.ts.tpl +7 -0
  293. package/templates/presets/identity/context/domain/vocabularies/invitation-status.vocabulary.ts.tpl +9 -0
  294. package/templates/presets/identity/context/domain/vocabularies/member-right.vocabulary.ts.tpl +9 -0
  295. package/templates/presets/identity/context/domain/vocabularies/member-role.vocabulary.ts.tpl +8 -0
  296. package/templates/presets/identity/context/infrastructure/external/crypto-invitation-tokens.adapter.ts.tpl +20 -0
  297. package/templates/presets/identity/context/infrastructure/external/environment-identity-provider-connection.adapter.ts.tpl +37 -0
  298. package/templates/presets/identity/context/infrastructure/external/keycloak-identity-provider-admin.adapter.ts.tpl +149 -0
  299. package/templates/presets/identity/context/infrastructure/persistence/__context__.schema.drizzle.ts.tpl +102 -0
  300. package/templates/presets/identity/context/infrastructure/persistence/account-row.schema.knex.ts.tpl +15 -0
  301. package/templates/presets/identity/context/infrastructure/persistence/account.mapper.drizzle.ts.tpl +38 -0
  302. package/templates/presets/identity/context/infrastructure/persistence/account.mapper.knex.ts.tpl +33 -0
  303. package/templates/presets/identity/context/infrastructure/persistence/create-__context__-tables.schema.knex.ts.tpl +107 -0
  304. package/templates/presets/identity/context/infrastructure/persistence/delegation-row.schema.knex.ts.tpl +17 -0
  305. package/templates/presets/identity/context/infrastructure/persistence/delegation.mapper.drizzle.ts.tpl +50 -0
  306. package/templates/presets/identity/context/infrastructure/persistence/delegation.mapper.knex.ts.tpl +45 -0
  307. package/templates/presets/identity/context/infrastructure/persistence/drizzle-account.repository.drizzle.ts.tpl +59 -0
  308. package/templates/presets/identity/context/infrastructure/persistence/drizzle-delegation.repository.drizzle.ts.tpl +77 -0
  309. package/templates/presets/identity/context/infrastructure/persistence/drizzle-invitation.repository.drizzle.ts.tpl +65 -0
  310. package/templates/presets/identity/context/infrastructure/persistence/drizzle-member.repository.drizzle.ts.tpl +84 -0
  311. package/templates/presets/identity/context/infrastructure/persistence/drizzle-organisation.repository.drizzle.ts.tpl +59 -0
  312. package/templates/presets/identity/context/infrastructure/persistence/drizzle-search-members-reader.repository.drizzle.ts.tpl +53 -0
  313. package/templates/presets/identity/context/infrastructure/persistence/invitation-row.schema.knex.ts.tpl +20 -0
  314. package/templates/presets/identity/context/infrastructure/persistence/invitation.mapper.drizzle.ts.tpl +45 -0
  315. package/templates/presets/identity/context/infrastructure/persistence/invitation.mapper.knex.ts.tpl +40 -0
  316. package/templates/presets/identity/context/infrastructure/persistence/knex-account.repository.knex.ts.tpl +61 -0
  317. package/templates/presets/identity/context/infrastructure/persistence/knex-delegation.repository.knex.ts.tpl +80 -0
  318. package/templates/presets/identity/context/infrastructure/persistence/knex-invitation.repository.knex.ts.tpl +67 -0
  319. package/templates/presets/identity/context/infrastructure/persistence/knex-member.repository.knex.ts.tpl +82 -0
  320. package/templates/presets/identity/context/infrastructure/persistence/knex-organisation.repository.knex.ts.tpl +61 -0
  321. package/templates/presets/identity/context/infrastructure/persistence/knex-search-members-reader.repository.knex.ts.tpl +77 -0
  322. package/templates/presets/identity/context/infrastructure/persistence/member-row.schema.knex.ts.tpl +15 -0
  323. package/templates/presets/identity/context/infrastructure/persistence/member.mapper.drizzle.ts.tpl +36 -0
  324. package/templates/presets/identity/context/infrastructure/persistence/member.mapper.knex.ts.tpl +31 -0
  325. package/templates/presets/identity/context/infrastructure/persistence/organisation-row.schema.knex.ts.tpl +12 -0
  326. package/templates/presets/identity/context/infrastructure/persistence/organisation.mapper.drizzle.ts.tpl +28 -0
  327. package/templates/presets/identity/context/infrastructure/persistence/organisation.mapper.knex.ts.tpl +26 -0
  328. package/templates/presets/identity/context/presentation/http/accounts/deactivate-account/deactivate-account.accepted.class-validator.per-route.ts.tpl +6 -0
  329. package/templates/presets/identity/context/presentation/http/accounts/deactivate-account/deactivate-account.controller.ts.tpl +71 -0
  330. package/templates/presets/identity/context/presentation/http/accounts/provision-account/provision-account.accepted.class-validator.per-route.ts.tpl +6 -0
  331. package/templates/presets/identity/context/presentation/http/accounts/provision-account/provision-account.body.class-validator.ts.tpl +9 -0
  332. package/templates/presets/identity/context/presentation/http/accounts/provision-account/provision-account.controller.ts.tpl +85 -0
  333. package/templates/presets/identity/context/presentation/http/accounts.routes.ts.tpl +5 -0
  334. package/templates/presets/identity/context/presentation/http/invitations/accept-invitation/accept-invitation.accepted.class-validator.per-route.ts.tpl +6 -0
  335. package/templates/presets/identity/context/presentation/http/invitations/accept-invitation/accept-invitation.body.class-validator.per-route.ts.tpl +8 -0
  336. package/templates/presets/identity/context/presentation/http/invitations/accept-invitation/accept-invitation.controller.ts.tpl +100 -0
  337. package/templates/presets/identity/context/presentation/http/invitations/decline-invitation/decline-invitation.accepted.class-validator.per-route.ts.tpl +6 -0
  338. package/templates/presets/identity/context/presentation/http/invitations/decline-invitation/decline-invitation.body.class-validator.per-route.ts.tpl +8 -0
  339. package/templates/presets/identity/context/presentation/http/invitations/decline-invitation/decline-invitation.controller.ts.tpl +98 -0
  340. package/templates/presets/identity/context/presentation/http/invitations.routes.ts.tpl +5 -0
  341. package/templates/presets/identity/context/presentation/http/organisations/change-member-role/change-member-role.accepted.class-validator.per-route.ts.tpl +6 -0
  342. package/templates/presets/identity/context/presentation/http/organisations/change-member-role/change-member-role.body.class-validator.ts.tpl +7 -0
  343. package/templates/presets/identity/context/presentation/http/organisations/change-member-role/change-member-role.controller.ts.tpl +83 -0
  344. package/templates/presets/identity/context/presentation/http/organisations/change-member-role/change-member-role.params.class-validator.ts.tpl +11 -0
  345. package/templates/presets/identity/context/presentation/http/organisations/grant-delegation/grant-delegation.accepted.class-validator.per-route.ts.tpl +6 -0
  346. package/templates/presets/identity/context/presentation/http/organisations/grant-delegation/grant-delegation.body.class-validator.ts.tpl +18 -0
  347. package/templates/presets/identity/context/presentation/http/organisations/grant-delegation/grant-delegation.controller.ts.tpl +103 -0
  348. package/templates/presets/identity/context/presentation/http/organisations/grant-delegation/grant-delegation.params.class-validator.ts.tpl +7 -0
  349. package/templates/presets/identity/context/presentation/http/organisations/invite-member/invite-member.accepted.class-validator.ts.tpl +10 -0
  350. package/templates/presets/identity/context/presentation/http/organisations/invite-member/invite-member.body.class-validator.ts.tpl +10 -0
  351. package/templates/presets/identity/context/presentation/http/organisations/invite-member/invite-member.controller.ts.tpl +65 -0
  352. package/templates/presets/identity/context/presentation/http/organisations/invite-member/invite-member.params.class-validator.ts.tpl +7 -0
  353. package/templates/presets/identity/context/presentation/http/organisations/open-organisation/open-organisation.accepted.class-validator.per-route.ts.tpl +6 -0
  354. package/templates/presets/identity/context/presentation/http/organisations/open-organisation/open-organisation.body.class-validator.ts.tpl +12 -0
  355. package/templates/presets/identity/context/presentation/http/organisations/open-organisation/open-organisation.controller.ts.tpl +79 -0
  356. package/templates/presets/identity/context/presentation/http/organisations/remove-member/remove-member.accepted.class-validator.per-route.ts.tpl +6 -0
  357. package/templates/presets/identity/context/presentation/http/organisations/remove-member/remove-member.controller.ts.tpl +73 -0
  358. package/templates/presets/identity/context/presentation/http/organisations/remove-member/remove-member.params.class-validator.ts.tpl +11 -0
  359. package/templates/presets/identity/context/presentation/http/organisations/revoke-delegation/revoke-delegation.accepted.class-validator.per-route.ts.tpl +6 -0
  360. package/templates/presets/identity/context/presentation/http/organisations/revoke-delegation/revoke-delegation.controller.ts.tpl +73 -0
  361. package/templates/presets/identity/context/presentation/http/organisations/revoke-delegation/revoke-delegation.params.class-validator.ts.tpl +11 -0
  362. package/templates/presets/identity/context/presentation/http/organisations/revoke-invitation/revoke-invitation.accepted.class-validator.per-route.ts.tpl +6 -0
  363. package/templates/presets/identity/context/presentation/http/organisations/revoke-invitation/revoke-invitation.controller.ts.tpl +73 -0
  364. package/templates/presets/identity/context/presentation/http/organisations/revoke-invitation/revoke-invitation.params.class-validator.ts.tpl +11 -0
  365. package/templates/presets/identity/context/presentation/http/organisations/search-members/search-members.controller.ts.tpl +86 -0
  366. package/templates/presets/identity/context/presentation/http/organisations/search-members/search-members.line.class-validator.ts.tpl +22 -0
  367. package/templates/presets/identity/context/presentation/http/organisations/search-members/search-members.params.class-validator.ts.tpl +7 -0
  368. package/templates/presets/identity/context/presentation/http/organisations.routes.ts.tpl +5 -0
  369. package/templates/presets/identity/context/presentation/http/shared/invitation-token.schema.class-validator.shared.ts.tpl +9 -0
  370. package/templates/presets/identity/context/presentation/http/shared/invitation-token.schema.zod.shared.ts.tpl +9 -0
  371. package/templates/presets/identity/context/published-language/account-deactivated.v1.integration-event.ts.tpl +33 -0
  372. package/templates/presets/identity/context/published-language/account-deactivated.v1.payload.class-validator.ts.tpl +7 -0
  373. package/templates/presets/identity/context/published-language/delegation-granted.v1.integration-event.ts.tpl +39 -0
  374. package/templates/presets/identity/context/published-language/delegation-granted.v1.payload.class-validator.ts.tpl +28 -0
  375. package/templates/presets/identity/context/published-language/delegation-revoked.v1.integration-event.ts.tpl +36 -0
  376. package/templates/presets/identity/context/published-language/delegation-revoked.v1.payload.class-validator.ts.tpl +16 -0
  377. package/templates/presets/identity/context/published-language/member-joined.v1.integration-event.ts.tpl +36 -0
  378. package/templates/presets/identity/context/published-language/member-joined.v1.payload.class-validator.ts.tpl +16 -0
  379. package/templates/presets/identity/context/published-language/member-removed.v1.integration-event.ts.tpl +36 -0
  380. package/templates/presets/identity/context/published-language/member-removed.v1.payload.class-validator.ts.tpl +18 -0
  381. package/templates/presets/identity/context/published-language/member-role-changed.v1.integration-event.ts.tpl +38 -0
  382. package/templates/presets/identity/context/published-language/member-role-changed.v1.payload.class-validator.ts.tpl +24 -0
  383. package/templates/presets/identity/context/published-language/organisation-opened.v1.integration-event.ts.tpl +35 -0
  384. package/templates/presets/identity/context/published-language/organisation-opened.v1.payload.class-validator.ts.tpl +13 -0
  385. package/templates/presets/identity/context/test/builders/account.builder.ts.tpl +59 -0
  386. package/templates/presets/identity/context/test/builders/delegation.builder.ts.tpl +67 -0
  387. package/templates/presets/identity/context/test/builders/invitation.builder.ts.tpl +60 -0
  388. package/templates/presets/identity/context/test/builders/member.builder.ts.tpl +59 -0
  389. package/templates/presets/identity/context/test/builders/organisation.builder.ts.tpl +44 -0
  390. package/templates/presets/identity/context/test/fakes/account.repository.fake.ts.tpl +10 -0
  391. package/templates/presets/identity/context/test/fakes/delegation.repository.fake.ts.tpl +26 -0
  392. package/templates/presets/identity/context/test/fakes/identity-provider-admin.fake.ts.tpl +41 -0
  393. package/templates/presets/identity/context/test/fakes/invitation-tokens.fake.ts.tpl +18 -0
  394. package/templates/presets/identity/context/test/fakes/invitation.repository.fake.ts.tpl +14 -0
  395. package/templates/presets/identity/context/test/fakes/member.repository.fake.ts.tpl +29 -0
  396. package/templates/presets/identity/context/test/fakes/organisation.repository.fake.ts.tpl +10 -0
  397. package/templates/presets/identity/context/test/fakes/search-members-reader.fake.ts.tpl +25 -0
  398. package/templates/presets/identity/context/test/integration/account.repository.contract.spec.ts.tpl +48 -0
  399. package/templates/presets/identity/context/test/integration/delegation.repository.contract.spec.ts.tpl +84 -0
  400. package/templates/presets/identity/context/test/integration/invitation.repository.contract.spec.ts.tpl +76 -0
  401. package/templates/presets/identity/context/test/integration/keycloak-identity-provider-admin.adapter.spec.ts.tpl +123 -0
  402. package/templates/presets/identity/context/test/integration/member.repository.contract.spec.ts.tpl +92 -0
  403. package/templates/presets/identity/context/test/integration/organisation.repository.contract.spec.ts.tpl +46 -0
  404. package/templates/presets/identity/context/test/integration/search-members-reader.spec.ts.tpl +86 -0
  405. package/templates/presets/identity/context/test/unit/accept-invitation.controller.spec.ts.tpl +57 -0
  406. package/templates/presets/identity/context/test/unit/accept-invitation.handler.spec.ts.tpl +69 -0
  407. package/templates/presets/identity/context/test/unit/account-deactivated.integration-handler.spec.ts.tpl +24 -0
  408. package/templates/presets/identity/context/test/unit/account-deactivated.publisher.spec.ts.tpl +25 -0
  409. package/templates/presets/identity/context/test/unit/account.aggregate.spec.ts.tpl +72 -0
  410. package/templates/presets/identity/context/test/unit/change-member-role.controller.spec.ts.tpl +57 -0
  411. package/templates/presets/identity/context/test/unit/change-member-role.handler.spec.ts.tpl +135 -0
  412. package/templates/presets/identity/context/test/unit/crypto-invitation-tokens.adapter.spec.ts.tpl +15 -0
  413. package/templates/presets/identity/context/test/unit/deactivate-account.controller.spec.ts.tpl +44 -0
  414. package/templates/presets/identity/context/test/unit/deactivate-account.handler.spec.ts.tpl +106 -0
  415. package/templates/presets/identity/context/test/unit/decline-invitation.controller.spec.ts.tpl +57 -0
  416. package/templates/presets/identity/context/test/unit/decline-invitation.handler.spec.ts.tpl +40 -0
  417. package/templates/presets/identity/context/test/unit/delegation-granted.publisher.spec.ts.tpl +36 -0
  418. package/templates/presets/identity/context/test/unit/delegation-period.vo.spec.ts.tpl +32 -0
  419. package/templates/presets/identity/context/test/unit/delegation-revoked.publisher.spec.ts.tpl +35 -0
  420. package/templates/presets/identity/context/test/unit/delegation.aggregate.spec.ts.tpl +111 -0
  421. package/templates/presets/identity/context/test/unit/display-name.vo.spec.ts.tpl +13 -0
  422. package/templates/presets/identity/context/test/unit/email-address.vo.spec.ts.tpl +24 -0
  423. package/templates/presets/identity/context/test/unit/environment-identity-provider-connection.adapter.spec.ts.tpl +41 -0
  424. package/templates/presets/identity/context/test/unit/grant-delegation.controller.spec.ts.tpl +57 -0
  425. package/templates/presets/identity/context/test/unit/grant-delegation.handler.spec.ts.tpl +66 -0
  426. package/templates/presets/identity/context/test/unit/identity-application.ts.tpl +79 -0
  427. package/templates/presets/identity/context/test/unit/invitation.aggregate.spec.ts.tpl +118 -0
  428. package/templates/presets/identity/context/test/unit/invite-member.controller.spec.ts.tpl +57 -0
  429. package/templates/presets/identity/context/test/unit/invite-member.handler.spec.ts.tpl +110 -0
  430. package/templates/presets/identity/context/test/unit/member-joined.integration-handler.spec.ts.tpl +38 -0
  431. package/templates/presets/identity/context/test/unit/member-joined.publisher.spec.ts.tpl +36 -0
  432. package/templates/presets/identity/context/test/unit/member-removed.integration-handler.spec.ts.tpl +45 -0
  433. package/templates/presets/identity/context/test/unit/member-removed.publisher.spec.ts.tpl +35 -0
  434. package/templates/presets/identity/context/test/unit/member-role-changed.publisher.spec.ts.tpl +36 -0
  435. package/templates/presets/identity/context/test/unit/member.aggregate.spec.ts.tpl +142 -0
  436. package/templates/presets/identity/context/test/unit/open-organisation.controller.spec.ts.tpl +57 -0
  437. package/templates/presets/identity/context/test/unit/open-organisation.handler.spec.ts.tpl +52 -0
  438. package/templates/presets/identity/context/test/unit/organisation-name.vo.spec.ts.tpl +13 -0
  439. package/templates/presets/identity/context/test/unit/organisation-opened.integration-handler.spec.ts.tpl +24 -0
  440. package/templates/presets/identity/context/test/unit/organisation-opened.publisher.spec.ts.tpl +35 -0
  441. package/templates/presets/identity/context/test/unit/organisation-rights.vo.spec.ts.tpl +23 -0
  442. package/templates/presets/identity/context/test/unit/organisation-slug.vo.spec.ts.tpl +17 -0
  443. package/templates/presets/identity/context/test/unit/organisation.aggregate.spec.ts.tpl +33 -0
  444. package/templates/presets/identity/context/test/unit/provision-account.controller.spec.ts.tpl +73 -0
  445. package/templates/presets/identity/context/test/unit/provision-account.handler.spec.ts.tpl +65 -0
  446. package/templates/presets/identity/context/test/unit/remove-member.controller.spec.ts.tpl +44 -0
  447. package/templates/presets/identity/context/test/unit/remove-member.handler.spec.ts.tpl +125 -0
  448. package/templates/presets/identity/context/test/unit/revoke-delegation.controller.spec.ts.tpl +44 -0
  449. package/templates/presets/identity/context/test/unit/revoke-delegation.handler.spec.ts.tpl +51 -0
  450. package/templates/presets/identity/context/test/unit/revoke-invitation.controller.spec.ts.tpl +44 -0
  451. package/templates/presets/identity/context/test/unit/revoke-invitation.handler.spec.ts.tpl +44 -0
  452. package/templates/presets/identity/context/test/unit/role-rights.service.spec.ts.tpl +54 -0
  453. package/templates/presets/identity/context/test/unit/search-members.controller.spec.ts.tpl +43 -0
  454. package/templates/presets/identity/context/test/unit/search-members.handler.spec.ts.tpl +38 -0
  455. package/templates/presets/identity/decision.md.tpl +37 -0
  456. package/templates/presets/identity/design.md.tpl +71 -0
  457. package/templates/presets/signed-in-grants.ts.tpl +12 -0
@@ -0,0 +1,59 @@
1
+ import { and, eq } from 'drizzle-orm';
2
+ import { optimisticUpdate } from '@ddd-ts-kit/drizzle/postgres';
3
+ import { stampCreation, stampUpdate, type Clock, type ExecutionScope } from '@ddd-ts-kit/kernel';
4
+ import { Repository } from '@ddd-ts-kit/kernel/declarations';
5
+ import type { Account } from '{{alias.context}}/domain/aggregates/account.aggregate{{alias.ext}}';
6
+ import { AccountRepository } from '{{alias.context}}/domain/repositories/account.repository{{alias.ext}}';
7
+ import type { Database } from '{{alias.platform}}/database/database{{alias.ext}}';
8
+ import { accounts } from '{{alias.context}}/infrastructure/persistence/{{context.kebab}}.schema{{alias.ext}}';
9
+ import { AccountMapper, type AccountColumns } from '{{alias.context}}/infrastructure/persistence/account.mapper{{alias.ext}}';
10
+
11
+ type VersionedAccount = AccountColumns & { readonly version: number };
12
+
13
+ @Repository(AccountRepository)
14
+ export class DrizzleAccountRepository implements AccountRepository {
15
+ constructor(
16
+ private readonly database: Database,
17
+ private readonly scope: ExecutionScope,
18
+ private readonly clock: Clock,
19
+ ) {}
20
+
21
+ async findById(id: string): Promise<Account | undefined> {
22
+ const [row] = await executorOf(this.database).select().from(accounts).where(eq(accounts.id, id));
23
+
24
+ return row === undefined ? undefined : AccountMapper.toDomain(row);
25
+ }
26
+
27
+ async save(saved: Account): Promise<void> {
28
+ await optimisticUpdate(saved, async ({ expected, next }) => {
29
+ const state = { ...AccountMapper.toRow(saved), version: next };
30
+ const written = expected === 0 ? await this.insert(state) : await this.update(state, expected);
31
+
32
+ return written.length;
33
+ });
34
+ }
35
+
36
+ private insert(state: VersionedAccount) {
37
+ const row = { ...state, ...stampCreation(this.scope, this.clock) };
38
+
39
+ return executorOf(this.database).insert(accounts).values(row).onConflictDoNothing().returning({ id: accounts.id });
40
+ }
41
+
42
+ private update(state: VersionedAccount, expected: number) {
43
+ const sameVersion = and(eq(accounts.id, state.id), eq(accounts.version, expected));
44
+
45
+ return executorOf(this.database)
46
+ .update(accounts)
47
+ .set({ ...state, ...stampUpdate(this.scope, this.clock) })
48
+ .where(sameVersion)
49
+ .returning({ id: accounts.id });
50
+ }
51
+ }
52
+
53
+ /** The executor of the unit of work, which every statement of this file goes through. */
54
+ function executorOf(database: Database): ReturnType<Database['executor']> {
55
+ {{#if tenancy.any}}
56
+ // ddd-ts-kit-disable-next-line tenancy/scoped-statement -- {{preset.adr}}: the {{context.kebab}} context is the control plane of the tenants, and its rows belong to none of them.
57
+ {{/if}}
58
+ return database.executor();
59
+ }
@@ -0,0 +1,77 @@
1
+ import { and, eq, isNull, or } from 'drizzle-orm';
2
+ import { optimisticUpdate } from '@ddd-ts-kit/drizzle/postgres';
3
+ import { stampCreation, stampUpdate, type Clock, type ExecutionScope } from '@ddd-ts-kit/kernel';
4
+ import { Repository } from '@ddd-ts-kit/kernel/declarations';
5
+ import type { Delegation } from '{{alias.context}}/domain/aggregates/delegation.aggregate{{alias.ext}}';
6
+ import { DelegationRepository } from '{{alias.context}}/domain/repositories/delegation.repository{{alias.ext}}';
7
+ import type { Database } from '{{alias.platform}}/database/database{{alias.ext}}';
8
+ import { delegations } from '{{alias.context}}/infrastructure/persistence/{{context.kebab}}.schema{{alias.ext}}';
9
+ import { DelegationMapper, type DelegationColumns } from '{{alias.context}}/infrastructure/persistence/delegation.mapper{{alias.ext}}';
10
+
11
+ type VersionedDelegation = DelegationColumns & { readonly version: number };
12
+
13
+ @Repository(DelegationRepository)
14
+ export class DrizzleDelegationRepository implements DelegationRepository {
15
+ constructor(
16
+ private readonly database: Database,
17
+ private readonly scope: ExecutionScope,
18
+ private readonly clock: Clock,
19
+ ) {}
20
+
21
+ async findById(id: string): Promise<Delegation | undefined> {
22
+ const [row] = await executorOf(this.database).select().from(delegations).where(eq(delegations.id, id));
23
+
24
+ return row === undefined ? undefined : DelegationMapper.toDomain(row);
25
+ }
26
+
27
+ async findUnendedTo(organisationId: string, delegateId: string): Promise<readonly Delegation[]> {
28
+ const lentTo = and(unended(organisationId), eq(delegations.delegateId, delegateId));
29
+ const rows = await executorOf(this.database).select().from(delegations).where(lentTo).orderBy(delegations.id);
30
+
31
+ return rows.map((row) => DelegationMapper.toDomain(row));
32
+ }
33
+
34
+ async findUnendedInvolving(organisationId: string, accountId: string): Promise<readonly Delegation[]> {
35
+ const either = or(eq(delegations.delegatorId, accountId), eq(delegations.delegateId, accountId));
36
+ const rows = await executorOf(this.database).select().from(delegations).where(and(unended(organisationId), either)).orderBy(delegations.id);
37
+
38
+ return rows.map((row) => DelegationMapper.toDomain(row));
39
+ }
40
+
41
+ async save(saved: Delegation): Promise<void> {
42
+ await optimisticUpdate(saved, async ({ expected, next }) => {
43
+ const state = { ...DelegationMapper.toRow(saved), version: next };
44
+ const written = expected === 0 ? await this.insert(state) : await this.update(state, expected);
45
+
46
+ return written.length;
47
+ });
48
+ }
49
+
50
+ private insert(state: VersionedDelegation) {
51
+ const row = { ...state, ...stampCreation(this.scope, this.clock) };
52
+
53
+ return executorOf(this.database).insert(delegations).values(row).onConflictDoNothing().returning({ id: delegations.id });
54
+ }
55
+
56
+ private update(state: VersionedDelegation, expected: number) {
57
+ const sameVersion = and(eq(delegations.id, state.id), eq(delegations.version, expected));
58
+
59
+ return executorOf(this.database)
60
+ .update(delegations)
61
+ .set({ ...state, ...stampUpdate(this.scope, this.clock) })
62
+ .where(sameVersion)
63
+ .returning({ id: delegations.id });
64
+ }
65
+ }
66
+
67
+ function unended(organisationId: string) {
68
+ return and(eq(delegations.organisationId, organisationId), isNull(delegations.endedAt));
69
+ }
70
+
71
+ /** The executor of the unit of work, which every statement of this file goes through. */
72
+ function executorOf(database: Database): ReturnType<Database['executor']> {
73
+ {{#if tenancy.any}}
74
+ // ddd-ts-kit-disable-next-line tenancy/scoped-statement -- {{preset.adr}}: the {{context.kebab}} context is the control plane of the tenants, and its rows belong to none of them.
75
+ {{/if}}
76
+ return database.executor();
77
+ }
@@ -0,0 +1,65 @@
1
+ import { and, eq } from 'drizzle-orm';
2
+ import { optimisticUpdate } from '@ddd-ts-kit/drizzle/postgres';
3
+ import { stampCreation, stampUpdate, type Clock, type ExecutionScope } from '@ddd-ts-kit/kernel';
4
+ import { Repository } from '@ddd-ts-kit/kernel/declarations';
5
+ import type { Invitation } from '{{alias.context}}/domain/aggregates/invitation.aggregate{{alias.ext}}';
6
+ import { InvitationRepository } from '{{alias.context}}/domain/repositories/invitation.repository{{alias.ext}}';
7
+ import type { Database } from '{{alias.platform}}/database/database{{alias.ext}}';
8
+ import { invitations } from '{{alias.context}}/infrastructure/persistence/{{context.kebab}}.schema{{alias.ext}}';
9
+ import { InvitationMapper, type InvitationColumns } from '{{alias.context}}/infrastructure/persistence/invitation.mapper{{alias.ext}}';
10
+
11
+ type VersionedInvitation = InvitationColumns & { readonly version: number };
12
+
13
+ @Repository(InvitationRepository)
14
+ export class DrizzleInvitationRepository implements InvitationRepository {
15
+ constructor(
16
+ private readonly database: Database,
17
+ private readonly scope: ExecutionScope,
18
+ private readonly clock: Clock,
19
+ ) {}
20
+
21
+ async findById(id: string): Promise<Invitation | undefined> {
22
+ const [row] = await executorOf(this.database).select().from(invitations).where(eq(invitations.id, id));
23
+
24
+ return row === undefined ? undefined : InvitationMapper.toDomain(row);
25
+ }
26
+
27
+ async findByTokenHash(tokenHash: string): Promise<Invitation | undefined> {
28
+ const [row] = await executorOf(this.database).select().from(invitations).where(eq(invitations.tokenHash, tokenHash));
29
+
30
+ return row === undefined ? undefined : InvitationMapper.toDomain(row);
31
+ }
32
+
33
+ async save(saved: Invitation): Promise<void> {
34
+ await optimisticUpdate(saved, async ({ expected, next }) => {
35
+ const state = { ...InvitationMapper.toRow(saved), version: next };
36
+ const written = expected === 0 ? await this.insert(state) : await this.update(state, expected);
37
+
38
+ return written.length;
39
+ });
40
+ }
41
+
42
+ private insert(state: VersionedInvitation) {
43
+ const row = { ...state, ...stampCreation(this.scope, this.clock) };
44
+
45
+ return executorOf(this.database).insert(invitations).values(row).onConflictDoNothing().returning({ id: invitations.id });
46
+ }
47
+
48
+ private update(state: VersionedInvitation, expected: number) {
49
+ const sameVersion = and(eq(invitations.id, state.id), eq(invitations.version, expected));
50
+
51
+ return executorOf(this.database)
52
+ .update(invitations)
53
+ .set({ ...state, ...stampUpdate(this.scope, this.clock) })
54
+ .where(sameVersion)
55
+ .returning({ id: invitations.id });
56
+ }
57
+ }
58
+
59
+ /** The executor of the unit of work, which every statement of this file goes through. */
60
+ function executorOf(database: Database): ReturnType<Database['executor']> {
61
+ {{#if tenancy.any}}
62
+ // ddd-ts-kit-disable-next-line tenancy/scoped-statement -- {{preset.adr}}: the {{context.kebab}} context is the control plane of the tenants, and its rows belong to none of them.
63
+ {{/if}}
64
+ return database.executor();
65
+ }
@@ -0,0 +1,84 @@
1
+ import { and, count, eq, isNull } from 'drizzle-orm';
2
+ import { optimisticUpdate } from '@ddd-ts-kit/drizzle/postgres';
3
+ import { stampCreation, stampUpdate, type Clock, type ExecutionScope } from '@ddd-ts-kit/kernel';
4
+ import { Repository } from '@ddd-ts-kit/kernel/declarations';
5
+ import type { Member } from '{{alias.context}}/domain/aggregates/member.aggregate{{alias.ext}}';
6
+ import { MemberRepository } from '{{alias.context}}/domain/repositories/member.repository{{alias.ext}}';
7
+ import { MemberRole } from '{{alias.context}}/domain/vocabularies/member-role.vocabulary{{alias.ext}}';
8
+ import type { Database } from '{{alias.platform}}/database/database{{alias.ext}}';
9
+ import { members } from '{{alias.context}}/infrastructure/persistence/{{context.kebab}}.schema{{alias.ext}}';
10
+ import { MemberMapper, type MemberColumns } from '{{alias.context}}/infrastructure/persistence/member.mapper{{alias.ext}}';
11
+
12
+ type VersionedMember = MemberColumns & { readonly version: number };
13
+
14
+ @Repository(MemberRepository)
15
+ export class DrizzleMemberRepository implements MemberRepository {
16
+ constructor(
17
+ private readonly database: Database,
18
+ private readonly scope: ExecutionScope,
19
+ private readonly clock: Clock,
20
+ ) {}
21
+
22
+ async findById(id: string): Promise<Member | undefined> {
23
+ const [row] = await executorOf(this.database).select().from(members).where(eq(members.id, id));
24
+
25
+ return row === undefined ? undefined : MemberMapper.toDomain(row);
26
+ }
27
+
28
+ async findOf(organisationId: string, accountId: string): Promise<Member | undefined> {
29
+ const [row] = await executorOf(this.database).select().from(members).where(current(organisationId, accountId));
30
+
31
+ return row === undefined ? undefined : MemberMapper.toDomain(row);
32
+ }
33
+
34
+ async searchCurrentOf(accountId: string): Promise<readonly Member[]> {
35
+ const ofAccount = and(eq(members.accountId, accountId), isNull(members.removedAt));
36
+ const rows = await executorOf(this.database).select().from(members).where(ofAccount).orderBy(members.id);
37
+
38
+ return rows.map((row) => MemberMapper.toDomain(row));
39
+ }
40
+
41
+ async countOwners(organisationId: string): Promise<number> {
42
+ const owners = and(eq(members.organisationId, organisationId), eq(members.role, MemberRole.Owner), isNull(members.removedAt));
43
+ const [counted] = await executorOf(this.database).select({ total: count() }).from(members).where(owners);
44
+
45
+ return counted?.total ?? 0;
46
+ }
47
+
48
+ async save(saved: Member): Promise<void> {
49
+ await optimisticUpdate(saved, async ({ expected, next }) => {
50
+ const state = { ...MemberMapper.toRow(saved), version: next };
51
+ const written = expected === 0 ? await this.insert(state) : await this.update(state, expected);
52
+
53
+ return written.length;
54
+ });
55
+ }
56
+
57
+ private insert(state: VersionedMember) {
58
+ const row = { ...state, ...stampCreation(this.scope, this.clock) };
59
+
60
+ return executorOf(this.database).insert(members).values(row).onConflictDoNothing().returning({ id: members.id });
61
+ }
62
+
63
+ private update(state: VersionedMember, expected: number) {
64
+ const sameVersion = and(eq(members.id, state.id), eq(members.version, expected));
65
+
66
+ return executorOf(this.database)
67
+ .update(members)
68
+ .set({ ...state, ...stampUpdate(this.scope, this.clock) })
69
+ .where(sameVersion)
70
+ .returning({ id: members.id });
71
+ }
72
+ }
73
+
74
+ function current(organisationId: string, accountId: string) {
75
+ return and(eq(members.organisationId, organisationId), eq(members.accountId, accountId), isNull(members.removedAt));
76
+ }
77
+
78
+ /** The executor of the unit of work, which every statement of this file goes through. */
79
+ function executorOf(database: Database): ReturnType<Database['executor']> {
80
+ {{#if tenancy.any}}
81
+ // ddd-ts-kit-disable-next-line tenancy/scoped-statement -- {{preset.adr}}: the {{context.kebab}} context is the control plane of the tenants, and its rows belong to none of them.
82
+ {{/if}}
83
+ return database.executor();
84
+ }
@@ -0,0 +1,59 @@
1
+ import { and, eq } from 'drizzle-orm';
2
+ import { optimisticUpdate } from '@ddd-ts-kit/drizzle/postgres';
3
+ import { stampCreation, stampUpdate, type Clock, type ExecutionScope } from '@ddd-ts-kit/kernel';
4
+ import { Repository } from '@ddd-ts-kit/kernel/declarations';
5
+ import type { Organisation } from '{{alias.context}}/domain/aggregates/organisation.aggregate{{alias.ext}}';
6
+ import { OrganisationRepository } from '{{alias.context}}/domain/repositories/organisation.repository{{alias.ext}}';
7
+ import type { Database } from '{{alias.platform}}/database/database{{alias.ext}}';
8
+ import { organisations } from '{{alias.context}}/infrastructure/persistence/{{context.kebab}}.schema{{alias.ext}}';
9
+ import { OrganisationMapper, type OrganisationColumns } from '{{alias.context}}/infrastructure/persistence/organisation.mapper{{alias.ext}}';
10
+
11
+ type VersionedOrganisation = OrganisationColumns & { readonly version: number };
12
+
13
+ @Repository(OrganisationRepository)
14
+ export class DrizzleOrganisationRepository implements OrganisationRepository {
15
+ constructor(
16
+ private readonly database: Database,
17
+ private readonly scope: ExecutionScope,
18
+ private readonly clock: Clock,
19
+ ) {}
20
+
21
+ async findById(id: string): Promise<Organisation | undefined> {
22
+ const [row] = await executorOf(this.database).select().from(organisations).where(eq(organisations.id, id));
23
+
24
+ return row === undefined ? undefined : OrganisationMapper.toDomain(row);
25
+ }
26
+
27
+ async save(saved: Organisation): Promise<void> {
28
+ await optimisticUpdate(saved, async ({ expected, next }) => {
29
+ const state = { ...OrganisationMapper.toRow(saved), version: next };
30
+ const written = expected === 0 ? await this.insert(state) : await this.update(state, expected);
31
+
32
+ return written.length;
33
+ });
34
+ }
35
+
36
+ private insert(state: VersionedOrganisation) {
37
+ const row = { ...state, ...stampCreation(this.scope, this.clock) };
38
+
39
+ return executorOf(this.database).insert(organisations).values(row).onConflictDoNothing().returning({ id: organisations.id });
40
+ }
41
+
42
+ private update(state: VersionedOrganisation, expected: number) {
43
+ const sameVersion = and(eq(organisations.id, state.id), eq(organisations.version, expected));
44
+
45
+ return executorOf(this.database)
46
+ .update(organisations)
47
+ .set({ ...state, ...stampUpdate(this.scope, this.clock) })
48
+ .where(sameVersion)
49
+ .returning({ id: organisations.id });
50
+ }
51
+ }
52
+
53
+ /** The executor of the unit of work, which every statement of this file goes through. */
54
+ function executorOf(database: Database): ReturnType<Database['executor']> {
55
+ {{#if tenancy.any}}
56
+ // ddd-ts-kit-disable-next-line tenancy/scoped-statement -- {{preset.adr}}: the {{context.kebab}} context is the control plane of the tenants, and its rows belong to none of them.
57
+ {{/if}}
58
+ return database.executor();
59
+ }
@@ -0,0 +1,53 @@
1
+ import { and, asc, count, desc, eq, isNull } from 'drizzle-orm';
2
+ import { InvalidPageRequestError, pageOf, type PageRequest } from '@ddd-ts-kit/kernel';
3
+ import { Repository } from '@ddd-ts-kit/kernel/declarations';
4
+ import { SearchMembersReader } from '{{alias.context}}/application/ports/search-members-reader.port{{alias.ext}}';
5
+ import type {
6
+ SearchMembersResult,
7
+ SearchMembersSort,
8
+ } from '{{alias.context}}/application/queries/search-members/search-members.result{{alias.ext}}';
9
+ import type { Database } from '{{alias.platform}}/database/database{{alias.ext}}';
10
+ import { accounts, members } from '{{alias.context}}/infrastructure/persistence/{{context.kebab}}.schema{{alias.ext}}';
11
+
12
+ /** The current members of an organisation joined to the profile of their account: one statement, the page applied by the database. */
13
+ @Repository(SearchMembersReader)
14
+ export class DrizzleSearchMembersReader implements SearchMembersReader {
15
+ constructor(private readonly database: Database) {}
16
+
17
+ async search(organisationId: string, page: PageRequest<SearchMembersSort>): Promise<SearchMembersResult> {
18
+ if (page.kind !== 'offset') {
19
+ throw new InvalidPageRequestError('The members of an organisation are read by offset');
20
+ }
21
+
22
+ const order = page.sort[0]?.direction === 'desc' ? desc : asc;
23
+ const current = and(eq(members.organisationId, organisationId), isNull(members.removedAt));
24
+
25
+ const items = await executorOf(this.database)
26
+ .select({
27
+ memberId: members.id,
28
+ accountId: members.accountId,
29
+ displayName: accounts.displayName,
30
+ email: accounts.email,
31
+ role: members.role,
32
+ joinedAt: members.joinedAt,
33
+ })
34
+ .from(members)
35
+ .innerJoin(accounts, eq(accounts.id, members.accountId))
36
+ .where(current)
37
+ .orderBy(order(members.joinedAt), order(members.id))
38
+ .limit(page.limit)
39
+ .offset(page.offset);
40
+
41
+ const [counted] = await executorOf(this.database).select({ total: count() }).from(members).where(current);
42
+
43
+ return pageOf(items, { total: counted?.total ?? 0 });
44
+ }
45
+ }
46
+
47
+ /** The executor of the unit of work, which every statement of this file goes through. */
48
+ function executorOf(database: Database): ReturnType<Database['executor']> {
49
+ {{#if tenancy.any}}
50
+ // ddd-ts-kit-disable-next-line tenancy/scoped-statement -- {{preset.adr}}: the {{context.kebab}} context is the control plane of the tenants, and its rows belong to none of them.
51
+ {{/if}}
52
+ return database.executor();
53
+ }
@@ -0,0 +1,20 @@
1
+ import type { AuditRow } from '@ddd-ts-kit/knex';
2
+ import type { InvitationStatus } from '{{alias.context}}/domain/vocabularies/invitation-status.vocabulary{{alias.ext}}';
3
+ import type { MemberRole } from '{{alias.context}}/domain/vocabularies/member-role.vocabulary{{alias.ext}}';
4
+
5
+ export interface InvitationColumns {
6
+ readonly id: string;
7
+ readonly organisation_id: string;
8
+ readonly email: string;
9
+ readonly role: MemberRole;
10
+ readonly token_hash: string;
11
+ readonly invited_by: string;
12
+ readonly issued_at: Date;
13
+ readonly expires_at: Date;
14
+ readonly status: InvitationStatus;
15
+ readonly answered_at: Date | null;
16
+ }
17
+
18
+ export interface InvitationRow extends InvitationColumns, AuditRow {
19
+ readonly version: number;
20
+ }
@@ -0,0 +1,45 @@
1
+ import type { AuditStamp } from '@ddd-ts-kit/kernel';
2
+ import { Invitation } from '{{alias.context}}/domain/aggregates/invitation.aggregate{{alias.ext}}';
3
+ import { EmailAddress } from '{{alias.context}}/domain/value-objects/email-address.vo{{alias.ext}}';
4
+ import type { invitations } from '{{alias.context}}/infrastructure/persistence/{{context.kebab}}.schema{{alias.ext}}';
5
+
6
+ type InvitationRow = typeof invitations.$inferSelect;
7
+
8
+ export type InvitationColumns = Omit<InvitationRow, 'version' | keyof AuditStamp>;
9
+
10
+ export const InvitationMapper = {
11
+ toDomain(row: InvitationRow): Invitation {
12
+ const loaded = Invitation.rehydrate(row.id, {
13
+ organisationId: row.organisationId,
14
+ email: EmailAddress.create(row.email),
15
+ role: row.role,
16
+ tokenHash: row.tokenHash,
17
+ invitedBy: row.invitedBy,
18
+ issuedAt: row.issuedAt,
19
+ expiresAt: row.expiresAt,
20
+ status: row.status,
21
+ answeredAt: row.answeredAt ?? undefined,
22
+ });
23
+
24
+ loaded.version = row.version;
25
+
26
+ return loaded;
27
+ },
28
+
29
+ toRow(saved: Invitation): InvitationColumns {
30
+ const state = saved.toState();
31
+
32
+ return {
33
+ id: saved.id,
34
+ organisationId: state.organisationId,
35
+ email: state.email.value,
36
+ role: state.role,
37
+ tokenHash: state.tokenHash,
38
+ invitedBy: state.invitedBy,
39
+ issuedAt: state.issuedAt,
40
+ expiresAt: state.expiresAt,
41
+ status: state.status,
42
+ answeredAt: state.answeredAt ?? null,
43
+ };
44
+ },
45
+ };
@@ -0,0 +1,40 @@
1
+ import { Invitation } from '{{alias.context}}/domain/aggregates/invitation.aggregate{{alias.ext}}';
2
+ import { EmailAddress } from '{{alias.context}}/domain/value-objects/email-address.vo{{alias.ext}}';
3
+ import type { InvitationColumns, InvitationRow } from '{{alias.context}}/infrastructure/persistence/invitation-row.schema{{alias.ext}}';
4
+
5
+ export const InvitationMapper = {
6
+ toDomain(row: InvitationRow): Invitation {
7
+ const loaded = Invitation.rehydrate(row.id, {
8
+ organisationId: row.organisation_id,
9
+ email: EmailAddress.create(row.email),
10
+ role: row.role,
11
+ tokenHash: row.token_hash,
12
+ invitedBy: row.invited_by,
13
+ issuedAt: row.issued_at,
14
+ expiresAt: row.expires_at,
15
+ status: row.status,
16
+ answeredAt: row.answered_at ?? undefined,
17
+ });
18
+
19
+ loaded.version = row.version;
20
+
21
+ return loaded;
22
+ },
23
+
24
+ toRow(saved: Invitation): InvitationColumns {
25
+ const state = saved.toState();
26
+
27
+ return {
28
+ id: saved.id,
29
+ organisation_id: state.organisationId,
30
+ email: state.email.value,
31
+ role: state.role,
32
+ token_hash: state.tokenHash,
33
+ invited_by: state.invitedBy,
34
+ issued_at: state.issuedAt,
35
+ expires_at: state.expiresAt,
36
+ status: state.status,
37
+ answered_at: state.answeredAt ?? null,
38
+ };
39
+ },
40
+ };
@@ -0,0 +1,61 @@
1
+ import type { Knex } from 'knex';
2
+ import { stampCreation, stampUpdate, type Clock, type ExecutionScope } from '@ddd-ts-kit/kernel';
3
+ import { Repository } from '@ddd-ts-kit/kernel/declarations';
4
+ import { type KnexContext, auditRow, insertIgnoringConflict, optimisticUpdate } from '@ddd-ts-kit/knex';
5
+ import type { Account } from '{{alias.context}}/domain/aggregates/account.aggregate{{alias.ext}}';
6
+ import { AccountRepository } from '{{alias.context}}/domain/repositories/account.repository{{alias.ext}}';
7
+ import { {{context.constant}}_SCHEMA } from '{{alias.context}}/infrastructure/persistence/{{context.kebab}}.schema{{alias.ext}}';
8
+ import { AccountMapper } from '{{alias.context}}/infrastructure/persistence/account.mapper{{alias.ext}}';
9
+ import type { AccountColumns, AccountRow } from '{{alias.context}}/infrastructure/persistence/account-row.schema{{alias.ext}}';
10
+
11
+ type VersionedAccount = AccountColumns & { readonly version: number };
12
+
13
+ @Repository(AccountRepository)
14
+ export class KnexAccountRepository implements AccountRepository {
15
+ constructor(
16
+ private readonly database: KnexContext,
17
+ private readonly scope: ExecutionScope,
18
+ private readonly clock: Clock,
19
+ ) {}
20
+
21
+ async findById(id: string): Promise<Account | undefined> {
22
+ const row = await tableOf(this.database).where({ id }).first();
23
+
24
+ return row === undefined ? undefined : AccountMapper.toDomain(row);
25
+ }
26
+
27
+ async save(saved: Account): Promise<void> {
28
+ await optimisticUpdate(saved, async ({ expected, next }) => {
29
+ const state = { ...AccountMapper.toRow(saved), version: next };
30
+
31
+ return expected === 0 ? this.insert(state) : this.update(state, expected);
32
+ });
33
+ }
34
+
35
+ private async insert(state: VersionedAccount): Promise<number> {
36
+ const row = { ...state, ...auditRow(stampCreation(this.scope, this.clock)) };
37
+
38
+ const inserted = await insertIgnoringConflict(this.database.executor(), tableOf(this.database), {
39
+ row,
40
+ conflict: 'id',
41
+ });
42
+
43
+ return inserted ? 1 : 0;
44
+ }
45
+
46
+ private async update(state: VersionedAccount, expected: number): Promise<number> {
47
+ return tableOf(this.database)
48
+ .where({ id: state.id, version: expected })
49
+ .update({ ...state, ...auditRow(stampUpdate(this.scope, this.clock)) });
50
+ }
51
+ }
52
+
53
+ /** A function and not a method: a query builder is thenable, and a method that answers one is read as asynchronous by whatever wraps the adapter (tracing), which then answers a promise. */
54
+ function tableOf(database: KnexContext): Knex.QueryBuilder<AccountRow, AccountRow[]> {
55
+ {{#if tenancy.any}}
56
+ // ddd-ts-kit-disable-next-line tenancy/scoped-statement -- {{preset.adr}}: the {{context.kebab}} context is the control plane of the tenants, and its rows belong to none of them.
57
+ {{/if}}
58
+ const table = database.executor()<AccountRow, AccountRow[]>('accounts');
59
+
60
+ return table.withSchema({{context.constant}}_SCHEMA);
61
+ }
@@ -0,0 +1,80 @@
1
+ import type { Knex } from 'knex';
2
+ import { stampCreation, stampUpdate, type Clock, type ExecutionScope } from '@ddd-ts-kit/kernel';
3
+ import { Repository } from '@ddd-ts-kit/kernel/declarations';
4
+ import { type KnexContext, auditRow, insertIgnoringConflict, optimisticUpdate } from '@ddd-ts-kit/knex';
5
+ import type { Delegation } from '{{alias.context}}/domain/aggregates/delegation.aggregate{{alias.ext}}';
6
+ import { DelegationRepository } from '{{alias.context}}/domain/repositories/delegation.repository{{alias.ext}}';
7
+ import { {{context.constant}}_SCHEMA } from '{{alias.context}}/infrastructure/persistence/{{context.kebab}}.schema{{alias.ext}}';
8
+ import { DelegationMapper } from '{{alias.context}}/infrastructure/persistence/delegation.mapper{{alias.ext}}';
9
+ import type { DelegationColumns, DelegationRow } from '{{alias.context}}/infrastructure/persistence/delegation-row.schema{{alias.ext}}';
10
+
11
+ type VersionedDelegation = DelegationColumns & { readonly version: number };
12
+
13
+ @Repository(DelegationRepository)
14
+ export class KnexDelegationRepository implements DelegationRepository {
15
+ constructor(
16
+ private readonly database: KnexContext,
17
+ private readonly scope: ExecutionScope,
18
+ private readonly clock: Clock,
19
+ ) {}
20
+
21
+ async findById(id: string): Promise<Delegation | undefined> {
22
+ const row = await tableOf(this.database).where({ id }).first();
23
+
24
+ return row === undefined ? undefined : DelegationMapper.toDomain(row);
25
+ }
26
+
27
+ async findUnendedTo(organisationId: string, delegateId: string): Promise<readonly Delegation[]> {
28
+ const lentTo = { organisation_id: organisationId, delegate_id: delegateId };
29
+ const rows = await tableOf(this.database).where(lentTo).whereNull('ended_at').orderBy('id');
30
+
31
+ return rows.map((row) => DelegationMapper.toDomain(row));
32
+ }
33
+
34
+ async findUnendedInvolving(organisationId: string, accountId: string): Promise<readonly Delegation[]> {
35
+ const rows = await tableOf(this.database)
36
+ .where({ organisation_id: organisationId })
37
+ .whereNull('ended_at')
38
+ .where((either) => {
39
+ void either.where({ delegator_id: accountId }).orWhere({ delegate_id: accountId });
40
+ })
41
+ .orderBy('id');
42
+
43
+ return rows.map((row) => DelegationMapper.toDomain(row));
44
+ }
45
+
46
+ async save(saved: Delegation): Promise<void> {
47
+ await optimisticUpdate(saved, async ({ expected, next }) => {
48
+ const state = { ...DelegationMapper.toRow(saved), version: next };
49
+
50
+ return expected === 0 ? this.insert(state) : this.update(state, expected);
51
+ });
52
+ }
53
+
54
+ private async insert(state: VersionedDelegation): Promise<number> {
55
+ const row = { ...state, ...auditRow(stampCreation(this.scope, this.clock)) };
56
+
57
+ const inserted = await insertIgnoringConflict(this.database.executor(), tableOf(this.database), {
58
+ row,
59
+ conflict: 'id',
60
+ });
61
+
62
+ return inserted ? 1 : 0;
63
+ }
64
+
65
+ private async update(state: VersionedDelegation, expected: number): Promise<number> {
66
+ return tableOf(this.database)
67
+ .where({ id: state.id, version: expected })
68
+ .update({ ...state, ...auditRow(stampUpdate(this.scope, this.clock)) });
69
+ }
70
+ }
71
+
72
+ /** A function and not a method: a query builder is thenable, and a method that answers one is read as asynchronous by whatever wraps the adapter (tracing), which then answers a promise. */
73
+ function tableOf(database: KnexContext): Knex.QueryBuilder<DelegationRow, DelegationRow[]> {
74
+ {{#if tenancy.any}}
75
+ // ddd-ts-kit-disable-next-line tenancy/scoped-statement -- {{preset.adr}}: the {{context.kebab}} context is the control plane of the tenants, and its rows belong to none of them.
76
+ {{/if}}
77
+ const table = database.executor()<DelegationRow, DelegationRow[]>('delegations');
78
+
79
+ return table.withSchema({{context.constant}}_SCHEMA);
80
+ }