ddd-ts-kit 0.14.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 (369) hide show
  1. package/README.md +28 -1
  2. package/dist/check/explain/tenancy-docs.d.ts.map +1 -1
  3. package/dist/check/explain/tenancy-docs.js +3 -1
  4. package/dist/check/explain/tenancy-docs.js.map +1 -1
  5. package/dist/check/fixtures/tenancy-fixtures.d.ts.map +1 -1
  6. package/dist/check/fixtures/tenancy-fixtures.js +28 -0
  7. package/dist/check/fixtures/tenancy-fixtures.js.map +1 -1
  8. package/dist/check/tenancy/knex-unscoped.d.ts +6 -1
  9. package/dist/check/tenancy/knex-unscoped.d.ts.map +1 -1
  10. package/dist/check/tenancy/knex-unscoped.js +19 -6
  11. package/dist/check/tenancy/knex-unscoped.js.map +1 -1
  12. package/dist/check/tenancy/raw-reaches-rows.d.ts +8 -0
  13. package/dist/check/tenancy/raw-reaches-rows.d.ts.map +1 -0
  14. package/dist/check/tenancy/raw-reaches-rows.js +91 -0
  15. package/dist/check/tenancy/raw-reaches-rows.js.map +1 -0
  16. package/dist/check/tenancy/raw-sql-text.d.ts +9 -0
  17. package/dist/check/tenancy/raw-sql-text.d.ts.map +1 -0
  18. package/dist/check/tenancy/raw-sql-text.js +29 -0
  19. package/dist/check/tenancy/raw-sql-text.js.map +1 -0
  20. package/dist/commands/generate.js +2 -0
  21. package/dist/commands/generate.js.map +1 -1
  22. package/dist/commands/init.d.ts.map +1 -1
  23. package/dist/commands/init.js +11 -1
  24. package/dist/commands/init.js.map +1 -1
  25. package/dist/declared/injection/laid-out-calls.d.ts +7 -0
  26. package/dist/declared/injection/laid-out-calls.d.ts.map +1 -0
  27. package/dist/declared/injection/laid-out-calls.js +14 -0
  28. package/dist/declared/injection/laid-out-calls.js.map +1 -0
  29. package/dist/declared/render-barrel.d.ts.map +1 -1
  30. package/dist/declared/render-barrel.js +2 -1
  31. package/dist/declared/render-barrel.js.map +1 -1
  32. package/dist/declared/render-dependencies-file.d.ts.map +1 -1
  33. package/dist/declared/render-dependencies-file.js +2 -1
  34. package/dist/declared/render-dependencies-file.js.map +1 -1
  35. package/dist/generate/kind.d.ts +2 -0
  36. package/dist/generate/kind.d.ts.map +1 -1
  37. package/dist/generate/kinds/context-kind.d.ts.map +1 -1
  38. package/dist/generate/kinds/context-kind.js +13 -6
  39. package/dist/generate/kinds/context-kind.js.map +1 -1
  40. package/dist/generate/kinds/next-adr-number.d.ts +4 -0
  41. package/dist/generate/kinds/next-adr-number.d.ts.map +1 -0
  42. package/dist/generate/kinds/next-adr-number.js +7 -0
  43. package/dist/generate/kinds/next-adr-number.js.map +1 -0
  44. package/dist/generate/kinds/write-adr.js +3 -6
  45. package/dist/generate/kinds/write-adr.js.map +1 -1
  46. package/dist/help/usage.d.ts +1 -1
  47. package/dist/help/usage.d.ts.map +1 -1
  48. package/dist/help/usage.js +1 -1
  49. package/dist/init/manifest/versions.js +24 -24
  50. package/dist/init/run-init.d.ts +2 -0
  51. package/dist/init/run-init.d.ts.map +1 -1
  52. package/dist/init/run-init.js +15 -1
  53. package/dist/init/run-init.js.map +1 -1
  54. package/dist/presets/apply-preset.d.ts +15 -0
  55. package/dist/presets/apply-preset.d.ts.map +1 -0
  56. package/dist/presets/apply-preset.js +94 -0
  57. package/dist/presets/apply-preset.js.map +1 -0
  58. package/dist/presets/grant-to-signed-in.d.ts +9 -0
  59. package/dist/presets/grant-to-signed-in.d.ts.map +1 -0
  60. package/dist/presets/grant-to-signed-in.js +61 -0
  61. package/dist/presets/grant-to-signed-in.js.map +1 -0
  62. package/dist/presets/identity-preset.d.ts +9 -0
  63. package/dist/presets/identity-preset.d.ts.map +1 -0
  64. package/dist/presets/identity-preset.js +44 -0
  65. package/dist/presets/identity-preset.js.map +1 -0
  66. package/dist/presets/identity-refusal.d.ts +5 -0
  67. package/dist/presets/identity-refusal.d.ts.map +1 -0
  68. package/dist/presets/identity-refusal.js +29 -0
  69. package/dist/presets/identity-refusal.js.map +1 -0
  70. package/dist/presets/identity-terms.d.ts +4 -0
  71. package/dist/presets/identity-terms.d.ts.map +1 -0
  72. package/dist/presets/identity-terms.js +57 -0
  73. package/dist/presets/identity-terms.js.map +1 -0
  74. package/dist/presets/no-flags.d.ts +4 -0
  75. package/dist/presets/no-flags.d.ts.map +1 -0
  76. package/dist/presets/no-flags.js +18 -0
  77. package/dist/presets/no-flags.js.map +1 -0
  78. package/dist/presets/organisations-as-tenants.d.ts +9 -0
  79. package/dist/presets/organisations-as-tenants.d.ts.map +1 -0
  80. package/dist/presets/organisations-as-tenants.js +43 -0
  81. package/dist/presets/organisations-as-tenants.js.map +1 -0
  82. package/dist/presets/preset-decision.d.ts +8 -0
  83. package/dist/presets/preset-decision.d.ts.map +1 -0
  84. package/dist/presets/preset-decision.js +19 -0
  85. package/dist/presets/preset-decision.js.map +1 -0
  86. package/dist/presets/preset-file.d.ts +13 -0
  87. package/dist/presets/preset-file.d.ts.map +1 -0
  88. package/dist/presets/preset-file.js +2 -0
  89. package/dist/presets/preset-file.js.map +1 -0
  90. package/dist/presets/preset-files.d.ts +9 -0
  91. package/dist/presets/preset-files.d.ts.map +1 -0
  92. package/dist/presets/preset-files.js +34 -0
  93. package/dist/presets/preset-files.js.map +1 -0
  94. package/dist/presets/preset-named.d.ts +3 -0
  95. package/dist/presets/preset-named.d.ts.map +1 -0
  96. package/dist/presets/preset-named.js +12 -0
  97. package/dist/presets/preset-named.js.map +1 -0
  98. package/dist/presets/preset-refusal.d.ts +6 -0
  99. package/dist/presets/preset-refusal.d.ts.map +1 -0
  100. package/dist/presets/preset-refusal.js +2 -0
  101. package/dist/presets/preset-refusal.js.map +1 -0
  102. package/dist/presets/preset-step.d.ts +8 -0
  103. package/dist/presets/preset-step.d.ts.map +1 -0
  104. package/dist/presets/preset-step.js +2 -0
  105. package/dist/presets/preset-step.js.map +1 -0
  106. package/dist/presets/preset.d.ts +36 -0
  107. package/dist/presets/preset.d.ts.map +1 -0
  108. package/dist/presets/preset.js +2 -0
  109. package/dist/presets/preset.js.map +1 -0
  110. package/dist/presets/with-authorizer-import.d.ts +6 -0
  111. package/dist/presets/with-authorizer-import.d.ts.map +1 -0
  112. package/dist/presets/with-authorizer-import.js +27 -0
  113. package/dist/presets/with-authorizer-import.js.map +1 -0
  114. package/dist/version.d.ts +1 -1
  115. package/dist/version.js +1 -1
  116. package/dist/workspace/workspace.d.ts +2 -0
  117. package/dist/workspace/workspace.d.ts.map +1 -1
  118. package/dist/workspace/workspace.js +4 -0
  119. package/dist/workspace/workspace.js.map +1 -1
  120. package/package.json +2 -2
  121. package/templates/pipeline/package-scripts.json +1 -1
  122. package/templates/presets/identity/context/application/authorization/__context__.permissions.ts.tpl +23 -0
  123. package/templates/presets/identity/context/application/authorization/change-member-roles.policy.ts.tpl +17 -0
  124. package/templates/presets/identity/context/application/authorization/grant-delegations.policy.ts.tpl +17 -0
  125. package/templates/presets/identity/context/application/authorization/invite-members.policy.ts.tpl +17 -0
  126. package/templates/presets/identity/context/application/authorization/read-members.policy.ts.tpl +17 -0
  127. package/templates/presets/identity/context/application/authorization/remove-members.policy.ts.tpl +17 -0
  128. package/templates/presets/identity/context/application/authorization/revoke-delegations.policy.ts.tpl +17 -0
  129. package/templates/presets/identity/context/application/authorization/revoke-invitations.policy.ts.tpl +17 -0
  130. package/templates/presets/identity/context/application/commands/accept-invitation/accept-invitation.command.ts.tpl +14 -0
  131. package/templates/presets/identity/context/application/commands/accept-invitation/accept-invitation.handler.ts.tpl +56 -0
  132. package/templates/presets/identity/context/application/commands/change-member-role/change-member-role.command.ts.tpl +17 -0
  133. package/templates/presets/identity/context/application/commands/change-member-role/change-member-role.handler.ts.tpl +82 -0
  134. package/templates/presets/identity/context/application/commands/deactivate-account/deactivate-account.command.ts.tpl +7 -0
  135. package/templates/presets/identity/context/application/commands/deactivate-account/deactivate-account.handler.ts.tpl +69 -0
  136. package/templates/presets/identity/context/application/commands/decline-invitation/decline-invitation.command.ts.tpl +14 -0
  137. package/templates/presets/identity/context/application/commands/decline-invitation/decline-invitation.handler.ts.tpl +36 -0
  138. package/templates/presets/identity/context/application/commands/grant-delegation/grant-delegation.command.ts.tpl +21 -0
  139. package/templates/presets/identity/context/application/commands/grant-delegation/grant-delegation.handler.ts.tpl +43 -0
  140. package/templates/presets/identity/context/application/commands/invite-member/invite-member.command.ts.tpl +16 -0
  141. package/templates/presets/identity/context/application/commands/invite-member/invite-member.handler.ts.tpl +53 -0
  142. package/templates/presets/identity/context/application/commands/open-organisation/open-organisation.command.ts.tpl +14 -0
  143. package/templates/presets/identity/context/application/commands/open-organisation/open-organisation.handler.ts.tpl +50 -0
  144. package/templates/presets/identity/context/application/commands/provision-account/provision-account.command.ts.tpl +14 -0
  145. package/templates/presets/identity/context/application/commands/provision-account/provision-account.handler.ts.tpl +41 -0
  146. package/templates/presets/identity/context/application/commands/remove-member/remove-member.command.ts.tpl +15 -0
  147. package/templates/presets/identity/context/application/commands/remove-member/remove-member.handler.ts.tpl +79 -0
  148. package/templates/presets/identity/context/application/commands/revoke-delegation/revoke-delegation.command.ts.tpl +15 -0
  149. package/templates/presets/identity/context/application/commands/revoke-delegation/revoke-delegation.handler.ts.tpl +39 -0
  150. package/templates/presets/identity/context/application/commands/revoke-invitation/revoke-invitation.command.ts.tpl +15 -0
  151. package/templates/presets/identity/context/application/commands/revoke-invitation/revoke-invitation.handler.ts.tpl +42 -0
  152. package/templates/presets/identity/context/application/integration/handlers/account-deactivated.integration-handler.ts.tpl +19 -0
  153. package/templates/presets/identity/context/application/integration/handlers/member-joined.integration-handler.ts.tpl +33 -0
  154. package/templates/presets/identity/context/application/integration/handlers/member-removed.integration-handler.ts.tpl +30 -0
  155. package/templates/presets/identity/context/application/integration/handlers/organisation-opened.integration-handler.ts.tpl +19 -0
  156. package/templates/presets/identity/context/application/integration/publishers/account-deactivated.publisher.ts.tpl +24 -0
  157. package/templates/presets/identity/context/application/integration/publishers/delegation-granted.publisher.ts.tpl +36 -0
  158. package/templates/presets/identity/context/application/integration/publishers/delegation-revoked.publisher.ts.tpl +33 -0
  159. package/templates/presets/identity/context/application/integration/publishers/member-joined.publisher.ts.tpl +33 -0
  160. package/templates/presets/identity/context/application/integration/publishers/member-removed.publisher.ts.tpl +33 -0
  161. package/templates/presets/identity/context/application/integration/publishers/member-role-changed.publisher.ts.tpl +35 -0
  162. package/templates/presets/identity/context/application/integration/publishers/organisation-opened.publisher.ts.tpl +32 -0
  163. package/templates/presets/identity/context/application/ports/identity-provider-admin.port.ts.tpl +17 -0
  164. package/templates/presets/identity/context/application/ports/identity-provider-connection.port.ts.tpl +14 -0
  165. package/templates/presets/identity/context/application/ports/invitation-tokens.port.ts.tpl +7 -0
  166. package/templates/presets/identity/context/application/ports/search-members-reader.port.ts.tpl +10 -0
  167. package/templates/presets/identity/context/application/queries/search-members/search-members.handler.ts.tpl +33 -0
  168. package/templates/presets/identity/context/application/queries/search-members/search-members.query.ts.tpl +19 -0
  169. package/templates/presets/identity/context/application/queries/search-members/search-members.result.ts.tpl +17 -0
  170. package/templates/presets/identity/context/domain/aggregates/account.aggregate.ts.tpl +92 -0
  171. package/templates/presets/identity/context/domain/aggregates/delegation.aggregate.ts.tpl +117 -0
  172. package/templates/presets/identity/context/domain/aggregates/invitation.aggregate.ts.tpl +128 -0
  173. package/templates/presets/identity/context/domain/aggregates/member.aggregate.ts.tpl +149 -0
  174. package/templates/presets/identity/context/domain/aggregates/organisation.aggregate.ts.tpl +52 -0
  175. package/templates/presets/identity/context/domain/errors/account.error.ts.tpl +40 -0
  176. package/templates/presets/identity/context/domain/errors/delegation.error.ts.tpl +45 -0
  177. package/templates/presets/identity/context/domain/errors/invitation.error.ts.tpl +30 -0
  178. package/templates/presets/identity/context/domain/errors/member.error.ts.tpl +50 -0
  179. package/templates/presets/identity/context/domain/errors/organisation.error.ts.tpl +32 -0
  180. package/templates/presets/identity/context/domain/events/account-deactivated.event.ts.tpl +13 -0
  181. package/templates/presets/identity/context/domain/events/account-provisioned.event.ts.tpl +17 -0
  182. package/templates/presets/identity/context/domain/events/delegation-granted.event.ts.tpl +22 -0
  183. package/templates/presets/identity/context/domain/events/delegation-revoked.event.ts.tpl +18 -0
  184. package/templates/presets/identity/context/domain/events/invitation-accepted.event.ts.tpl +17 -0
  185. package/templates/presets/identity/context/domain/events/invitation-declined.event.ts.tpl +16 -0
  186. package/templates/presets/identity/context/domain/events/invitation-issued.event.ts.tpl +22 -0
  187. package/templates/presets/identity/context/domain/events/invitation-revoked.event.ts.tpl +16 -0
  188. package/templates/presets/identity/context/domain/events/member-joined.event.ts.tpl +19 -0
  189. package/templates/presets/identity/context/domain/events/member-removed.event.ts.tpl +19 -0
  190. package/templates/presets/identity/context/domain/events/member-role-changed.event.ts.tpl +22 -0
  191. package/templates/presets/identity/context/domain/events/organisation-opened.event.ts.tpl +17 -0
  192. package/templates/presets/identity/context/domain/repositories/account.repository.ts.tpl +5 -0
  193. package/templates/presets/identity/context/domain/repositories/delegation.repository.ts.tpl +10 -0
  194. package/templates/presets/identity/context/domain/repositories/invitation.repository.ts.tpl +7 -0
  195. package/templates/presets/identity/context/domain/repositories/member.repository.ts.tpl +13 -0
  196. package/templates/presets/identity/context/domain/repositories/organisation.repository.ts.tpl +5 -0
  197. package/templates/presets/identity/context/domain/services/role-rights.service.ts.tpl +59 -0
  198. package/templates/presets/identity/context/domain/value-objects/delegation-period.vo.ts.tpl +42 -0
  199. package/templates/presets/identity/context/domain/value-objects/display-name.vo.ts.tpl +29 -0
  200. package/templates/presets/identity/context/domain/value-objects/email-address.vo.ts.tpl +31 -0
  201. package/templates/presets/identity/context/domain/value-objects/organisation-name.vo.ts.tpl +29 -0
  202. package/templates/presets/identity/context/domain/value-objects/organisation-rights.vo.ts.tpl +43 -0
  203. package/templates/presets/identity/context/domain/value-objects/organisation-slug.vo.ts.tpl +30 -0
  204. package/templates/presets/identity/context/domain/vocabularies/account-status.vocabulary.ts.tpl +7 -0
  205. package/templates/presets/identity/context/domain/vocabularies/invitation-status.vocabulary.ts.tpl +9 -0
  206. package/templates/presets/identity/context/domain/vocabularies/member-right.vocabulary.ts.tpl +9 -0
  207. package/templates/presets/identity/context/domain/vocabularies/member-role.vocabulary.ts.tpl +8 -0
  208. package/templates/presets/identity/context/infrastructure/external/crypto-invitation-tokens.adapter.ts.tpl +20 -0
  209. package/templates/presets/identity/context/infrastructure/external/environment-identity-provider-connection.adapter.ts.tpl +37 -0
  210. package/templates/presets/identity/context/infrastructure/external/keycloak-identity-provider-admin.adapter.ts.tpl +149 -0
  211. package/templates/presets/identity/context/infrastructure/persistence/__context__.schema.drizzle.ts.tpl +102 -0
  212. package/templates/presets/identity/context/infrastructure/persistence/account-row.schema.knex.ts.tpl +15 -0
  213. package/templates/presets/identity/context/infrastructure/persistence/account.mapper.drizzle.ts.tpl +38 -0
  214. package/templates/presets/identity/context/infrastructure/persistence/account.mapper.knex.ts.tpl +33 -0
  215. package/templates/presets/identity/context/infrastructure/persistence/create-__context__-tables.schema.knex.ts.tpl +107 -0
  216. package/templates/presets/identity/context/infrastructure/persistence/delegation-row.schema.knex.ts.tpl +17 -0
  217. package/templates/presets/identity/context/infrastructure/persistence/delegation.mapper.drizzle.ts.tpl +50 -0
  218. package/templates/presets/identity/context/infrastructure/persistence/delegation.mapper.knex.ts.tpl +45 -0
  219. package/templates/presets/identity/context/infrastructure/persistence/drizzle-account.repository.drizzle.ts.tpl +59 -0
  220. package/templates/presets/identity/context/infrastructure/persistence/drizzle-delegation.repository.drizzle.ts.tpl +77 -0
  221. package/templates/presets/identity/context/infrastructure/persistence/drizzle-invitation.repository.drizzle.ts.tpl +65 -0
  222. package/templates/presets/identity/context/infrastructure/persistence/drizzle-member.repository.drizzle.ts.tpl +84 -0
  223. package/templates/presets/identity/context/infrastructure/persistence/drizzle-organisation.repository.drizzle.ts.tpl +59 -0
  224. package/templates/presets/identity/context/infrastructure/persistence/drizzle-search-members-reader.repository.drizzle.ts.tpl +53 -0
  225. package/templates/presets/identity/context/infrastructure/persistence/invitation-row.schema.knex.ts.tpl +20 -0
  226. package/templates/presets/identity/context/infrastructure/persistence/invitation.mapper.drizzle.ts.tpl +45 -0
  227. package/templates/presets/identity/context/infrastructure/persistence/invitation.mapper.knex.ts.tpl +40 -0
  228. package/templates/presets/identity/context/infrastructure/persistence/knex-account.repository.knex.ts.tpl +61 -0
  229. package/templates/presets/identity/context/infrastructure/persistence/knex-delegation.repository.knex.ts.tpl +80 -0
  230. package/templates/presets/identity/context/infrastructure/persistence/knex-invitation.repository.knex.ts.tpl +67 -0
  231. package/templates/presets/identity/context/infrastructure/persistence/knex-member.repository.knex.ts.tpl +82 -0
  232. package/templates/presets/identity/context/infrastructure/persistence/knex-organisation.repository.knex.ts.tpl +61 -0
  233. package/templates/presets/identity/context/infrastructure/persistence/knex-search-members-reader.repository.knex.ts.tpl +77 -0
  234. package/templates/presets/identity/context/infrastructure/persistence/member-row.schema.knex.ts.tpl +15 -0
  235. package/templates/presets/identity/context/infrastructure/persistence/member.mapper.drizzle.ts.tpl +36 -0
  236. package/templates/presets/identity/context/infrastructure/persistence/member.mapper.knex.ts.tpl +31 -0
  237. package/templates/presets/identity/context/infrastructure/persistence/organisation-row.schema.knex.ts.tpl +12 -0
  238. package/templates/presets/identity/context/infrastructure/persistence/organisation.mapper.drizzle.ts.tpl +28 -0
  239. package/templates/presets/identity/context/infrastructure/persistence/organisation.mapper.knex.ts.tpl +26 -0
  240. package/templates/presets/identity/context/presentation/http/accounts/deactivate-account/deactivate-account.accepted.class-validator.per-route.ts.tpl +6 -0
  241. package/templates/presets/identity/context/presentation/http/accounts/deactivate-account/deactivate-account.controller.ts.tpl +71 -0
  242. package/templates/presets/identity/context/presentation/http/accounts/provision-account/provision-account.accepted.class-validator.per-route.ts.tpl +6 -0
  243. package/templates/presets/identity/context/presentation/http/accounts/provision-account/provision-account.body.class-validator.ts.tpl +9 -0
  244. package/templates/presets/identity/context/presentation/http/accounts/provision-account/provision-account.controller.ts.tpl +85 -0
  245. package/templates/presets/identity/context/presentation/http/accounts.routes.ts.tpl +5 -0
  246. package/templates/presets/identity/context/presentation/http/invitations/accept-invitation/accept-invitation.accepted.class-validator.per-route.ts.tpl +6 -0
  247. package/templates/presets/identity/context/presentation/http/invitations/accept-invitation/accept-invitation.body.class-validator.per-route.ts.tpl +8 -0
  248. package/templates/presets/identity/context/presentation/http/invitations/accept-invitation/accept-invitation.controller.ts.tpl +100 -0
  249. package/templates/presets/identity/context/presentation/http/invitations/decline-invitation/decline-invitation.accepted.class-validator.per-route.ts.tpl +6 -0
  250. package/templates/presets/identity/context/presentation/http/invitations/decline-invitation/decline-invitation.body.class-validator.per-route.ts.tpl +8 -0
  251. package/templates/presets/identity/context/presentation/http/invitations/decline-invitation/decline-invitation.controller.ts.tpl +98 -0
  252. package/templates/presets/identity/context/presentation/http/invitations.routes.ts.tpl +5 -0
  253. package/templates/presets/identity/context/presentation/http/organisations/change-member-role/change-member-role.accepted.class-validator.per-route.ts.tpl +6 -0
  254. package/templates/presets/identity/context/presentation/http/organisations/change-member-role/change-member-role.body.class-validator.ts.tpl +7 -0
  255. package/templates/presets/identity/context/presentation/http/organisations/change-member-role/change-member-role.controller.ts.tpl +83 -0
  256. package/templates/presets/identity/context/presentation/http/organisations/change-member-role/change-member-role.params.class-validator.ts.tpl +11 -0
  257. package/templates/presets/identity/context/presentation/http/organisations/grant-delegation/grant-delegation.accepted.class-validator.per-route.ts.tpl +6 -0
  258. package/templates/presets/identity/context/presentation/http/organisations/grant-delegation/grant-delegation.body.class-validator.ts.tpl +18 -0
  259. package/templates/presets/identity/context/presentation/http/organisations/grant-delegation/grant-delegation.controller.ts.tpl +103 -0
  260. package/templates/presets/identity/context/presentation/http/organisations/grant-delegation/grant-delegation.params.class-validator.ts.tpl +7 -0
  261. package/templates/presets/identity/context/presentation/http/organisations/invite-member/invite-member.accepted.class-validator.ts.tpl +10 -0
  262. package/templates/presets/identity/context/presentation/http/organisations/invite-member/invite-member.body.class-validator.ts.tpl +10 -0
  263. package/templates/presets/identity/context/presentation/http/organisations/invite-member/invite-member.controller.ts.tpl +65 -0
  264. package/templates/presets/identity/context/presentation/http/organisations/invite-member/invite-member.params.class-validator.ts.tpl +7 -0
  265. package/templates/presets/identity/context/presentation/http/organisations/open-organisation/open-organisation.accepted.class-validator.per-route.ts.tpl +6 -0
  266. package/templates/presets/identity/context/presentation/http/organisations/open-organisation/open-organisation.body.class-validator.ts.tpl +12 -0
  267. package/templates/presets/identity/context/presentation/http/organisations/open-organisation/open-organisation.controller.ts.tpl +79 -0
  268. package/templates/presets/identity/context/presentation/http/organisations/remove-member/remove-member.accepted.class-validator.per-route.ts.tpl +6 -0
  269. package/templates/presets/identity/context/presentation/http/organisations/remove-member/remove-member.controller.ts.tpl +73 -0
  270. package/templates/presets/identity/context/presentation/http/organisations/remove-member/remove-member.params.class-validator.ts.tpl +11 -0
  271. package/templates/presets/identity/context/presentation/http/organisations/revoke-delegation/revoke-delegation.accepted.class-validator.per-route.ts.tpl +6 -0
  272. package/templates/presets/identity/context/presentation/http/organisations/revoke-delegation/revoke-delegation.controller.ts.tpl +73 -0
  273. package/templates/presets/identity/context/presentation/http/organisations/revoke-delegation/revoke-delegation.params.class-validator.ts.tpl +11 -0
  274. package/templates/presets/identity/context/presentation/http/organisations/revoke-invitation/revoke-invitation.accepted.class-validator.per-route.ts.tpl +6 -0
  275. package/templates/presets/identity/context/presentation/http/organisations/revoke-invitation/revoke-invitation.controller.ts.tpl +73 -0
  276. package/templates/presets/identity/context/presentation/http/organisations/revoke-invitation/revoke-invitation.params.class-validator.ts.tpl +11 -0
  277. package/templates/presets/identity/context/presentation/http/organisations/search-members/search-members.controller.ts.tpl +86 -0
  278. package/templates/presets/identity/context/presentation/http/organisations/search-members/search-members.line.class-validator.ts.tpl +22 -0
  279. package/templates/presets/identity/context/presentation/http/organisations/search-members/search-members.params.class-validator.ts.tpl +7 -0
  280. package/templates/presets/identity/context/presentation/http/organisations.routes.ts.tpl +5 -0
  281. package/templates/presets/identity/context/presentation/http/shared/invitation-token.schema.class-validator.shared.ts.tpl +9 -0
  282. package/templates/presets/identity/context/presentation/http/shared/invitation-token.schema.zod.shared.ts.tpl +9 -0
  283. package/templates/presets/identity/context/published-language/account-deactivated.v1.integration-event.ts.tpl +33 -0
  284. package/templates/presets/identity/context/published-language/account-deactivated.v1.payload.class-validator.ts.tpl +7 -0
  285. package/templates/presets/identity/context/published-language/delegation-granted.v1.integration-event.ts.tpl +39 -0
  286. package/templates/presets/identity/context/published-language/delegation-granted.v1.payload.class-validator.ts.tpl +28 -0
  287. package/templates/presets/identity/context/published-language/delegation-revoked.v1.integration-event.ts.tpl +36 -0
  288. package/templates/presets/identity/context/published-language/delegation-revoked.v1.payload.class-validator.ts.tpl +16 -0
  289. package/templates/presets/identity/context/published-language/member-joined.v1.integration-event.ts.tpl +36 -0
  290. package/templates/presets/identity/context/published-language/member-joined.v1.payload.class-validator.ts.tpl +16 -0
  291. package/templates/presets/identity/context/published-language/member-removed.v1.integration-event.ts.tpl +36 -0
  292. package/templates/presets/identity/context/published-language/member-removed.v1.payload.class-validator.ts.tpl +18 -0
  293. package/templates/presets/identity/context/published-language/member-role-changed.v1.integration-event.ts.tpl +38 -0
  294. package/templates/presets/identity/context/published-language/member-role-changed.v1.payload.class-validator.ts.tpl +24 -0
  295. package/templates/presets/identity/context/published-language/organisation-opened.v1.integration-event.ts.tpl +35 -0
  296. package/templates/presets/identity/context/published-language/organisation-opened.v1.payload.class-validator.ts.tpl +13 -0
  297. package/templates/presets/identity/context/test/builders/account.builder.ts.tpl +59 -0
  298. package/templates/presets/identity/context/test/builders/delegation.builder.ts.tpl +67 -0
  299. package/templates/presets/identity/context/test/builders/invitation.builder.ts.tpl +60 -0
  300. package/templates/presets/identity/context/test/builders/member.builder.ts.tpl +59 -0
  301. package/templates/presets/identity/context/test/builders/organisation.builder.ts.tpl +44 -0
  302. package/templates/presets/identity/context/test/fakes/account.repository.fake.ts.tpl +10 -0
  303. package/templates/presets/identity/context/test/fakes/delegation.repository.fake.ts.tpl +26 -0
  304. package/templates/presets/identity/context/test/fakes/identity-provider-admin.fake.ts.tpl +41 -0
  305. package/templates/presets/identity/context/test/fakes/invitation-tokens.fake.ts.tpl +18 -0
  306. package/templates/presets/identity/context/test/fakes/invitation.repository.fake.ts.tpl +14 -0
  307. package/templates/presets/identity/context/test/fakes/member.repository.fake.ts.tpl +29 -0
  308. package/templates/presets/identity/context/test/fakes/organisation.repository.fake.ts.tpl +10 -0
  309. package/templates/presets/identity/context/test/fakes/search-members-reader.fake.ts.tpl +25 -0
  310. package/templates/presets/identity/context/test/integration/account.repository.contract.spec.ts.tpl +48 -0
  311. package/templates/presets/identity/context/test/integration/delegation.repository.contract.spec.ts.tpl +84 -0
  312. package/templates/presets/identity/context/test/integration/invitation.repository.contract.spec.ts.tpl +76 -0
  313. package/templates/presets/identity/context/test/integration/keycloak-identity-provider-admin.adapter.spec.ts.tpl +123 -0
  314. package/templates/presets/identity/context/test/integration/member.repository.contract.spec.ts.tpl +92 -0
  315. package/templates/presets/identity/context/test/integration/organisation.repository.contract.spec.ts.tpl +46 -0
  316. package/templates/presets/identity/context/test/integration/search-members-reader.spec.ts.tpl +86 -0
  317. package/templates/presets/identity/context/test/unit/accept-invitation.controller.spec.ts.tpl +57 -0
  318. package/templates/presets/identity/context/test/unit/accept-invitation.handler.spec.ts.tpl +69 -0
  319. package/templates/presets/identity/context/test/unit/account-deactivated.integration-handler.spec.ts.tpl +24 -0
  320. package/templates/presets/identity/context/test/unit/account-deactivated.publisher.spec.ts.tpl +25 -0
  321. package/templates/presets/identity/context/test/unit/account.aggregate.spec.ts.tpl +72 -0
  322. package/templates/presets/identity/context/test/unit/change-member-role.controller.spec.ts.tpl +57 -0
  323. package/templates/presets/identity/context/test/unit/change-member-role.handler.spec.ts.tpl +135 -0
  324. package/templates/presets/identity/context/test/unit/crypto-invitation-tokens.adapter.spec.ts.tpl +15 -0
  325. package/templates/presets/identity/context/test/unit/deactivate-account.controller.spec.ts.tpl +44 -0
  326. package/templates/presets/identity/context/test/unit/deactivate-account.handler.spec.ts.tpl +106 -0
  327. package/templates/presets/identity/context/test/unit/decline-invitation.controller.spec.ts.tpl +57 -0
  328. package/templates/presets/identity/context/test/unit/decline-invitation.handler.spec.ts.tpl +40 -0
  329. package/templates/presets/identity/context/test/unit/delegation-granted.publisher.spec.ts.tpl +36 -0
  330. package/templates/presets/identity/context/test/unit/delegation-period.vo.spec.ts.tpl +32 -0
  331. package/templates/presets/identity/context/test/unit/delegation-revoked.publisher.spec.ts.tpl +35 -0
  332. package/templates/presets/identity/context/test/unit/delegation.aggregate.spec.ts.tpl +111 -0
  333. package/templates/presets/identity/context/test/unit/display-name.vo.spec.ts.tpl +13 -0
  334. package/templates/presets/identity/context/test/unit/email-address.vo.spec.ts.tpl +24 -0
  335. package/templates/presets/identity/context/test/unit/environment-identity-provider-connection.adapter.spec.ts.tpl +41 -0
  336. package/templates/presets/identity/context/test/unit/grant-delegation.controller.spec.ts.tpl +57 -0
  337. package/templates/presets/identity/context/test/unit/grant-delegation.handler.spec.ts.tpl +66 -0
  338. package/templates/presets/identity/context/test/unit/identity-application.ts.tpl +79 -0
  339. package/templates/presets/identity/context/test/unit/invitation.aggregate.spec.ts.tpl +118 -0
  340. package/templates/presets/identity/context/test/unit/invite-member.controller.spec.ts.tpl +57 -0
  341. package/templates/presets/identity/context/test/unit/invite-member.handler.spec.ts.tpl +110 -0
  342. package/templates/presets/identity/context/test/unit/member-joined.integration-handler.spec.ts.tpl +38 -0
  343. package/templates/presets/identity/context/test/unit/member-joined.publisher.spec.ts.tpl +36 -0
  344. package/templates/presets/identity/context/test/unit/member-removed.integration-handler.spec.ts.tpl +45 -0
  345. package/templates/presets/identity/context/test/unit/member-removed.publisher.spec.ts.tpl +35 -0
  346. package/templates/presets/identity/context/test/unit/member-role-changed.publisher.spec.ts.tpl +36 -0
  347. package/templates/presets/identity/context/test/unit/member.aggregate.spec.ts.tpl +142 -0
  348. package/templates/presets/identity/context/test/unit/open-organisation.controller.spec.ts.tpl +57 -0
  349. package/templates/presets/identity/context/test/unit/open-organisation.handler.spec.ts.tpl +52 -0
  350. package/templates/presets/identity/context/test/unit/organisation-name.vo.spec.ts.tpl +13 -0
  351. package/templates/presets/identity/context/test/unit/organisation-opened.integration-handler.spec.ts.tpl +24 -0
  352. package/templates/presets/identity/context/test/unit/organisation-opened.publisher.spec.ts.tpl +35 -0
  353. package/templates/presets/identity/context/test/unit/organisation-rights.vo.spec.ts.tpl +23 -0
  354. package/templates/presets/identity/context/test/unit/organisation-slug.vo.spec.ts.tpl +17 -0
  355. package/templates/presets/identity/context/test/unit/organisation.aggregate.spec.ts.tpl +33 -0
  356. package/templates/presets/identity/context/test/unit/provision-account.controller.spec.ts.tpl +73 -0
  357. package/templates/presets/identity/context/test/unit/provision-account.handler.spec.ts.tpl +65 -0
  358. package/templates/presets/identity/context/test/unit/remove-member.controller.spec.ts.tpl +44 -0
  359. package/templates/presets/identity/context/test/unit/remove-member.handler.spec.ts.tpl +125 -0
  360. package/templates/presets/identity/context/test/unit/revoke-delegation.controller.spec.ts.tpl +44 -0
  361. package/templates/presets/identity/context/test/unit/revoke-delegation.handler.spec.ts.tpl +51 -0
  362. package/templates/presets/identity/context/test/unit/revoke-invitation.controller.spec.ts.tpl +44 -0
  363. package/templates/presets/identity/context/test/unit/revoke-invitation.handler.spec.ts.tpl +44 -0
  364. package/templates/presets/identity/context/test/unit/role-rights.service.spec.ts.tpl +54 -0
  365. package/templates/presets/identity/context/test/unit/search-members.controller.spec.ts.tpl +43 -0
  366. package/templates/presets/identity/context/test/unit/search-members.handler.spec.ts.tpl +38 -0
  367. package/templates/presets/identity/decision.md.tpl +37 -0
  368. package/templates/presets/identity/design.md.tpl +71 -0
  369. package/templates/presets/signed-in-grants.ts.tpl +12 -0
@@ -0,0 +1,32 @@
1
+ import type { IdGenerator, IntegrationEventPublisher } from '@ddd-ts-kit/kernel';
2
+ import { DomainEventHandler } from '@ddd-ts-kit/kernel/declarations';
3
+ import { OrganisationOpened } from '{{alias.context}}/domain/events/organisation-opened.event{{alias.ext}}';
4
+ import { organisationOpenedV1 } from '{{alias.context}}/published-language/organisation-opened.v1.integration-event{{alias.ext}}';
5
+
6
+ /** Tells the other contexts, through the outbox, in the transaction of the fact. */
7
+ @DomainEventHandler(OrganisationOpened, { name: OrganisationOpened.eventName })
8
+ export class OrganisationOpenedPublisher implements DomainEventHandler<OrganisationOpened> {
9
+ constructor(
10
+ private readonly publisher: IntegrationEventPublisher,
11
+ private readonly ids: IdGenerator,
12
+ ) {}
13
+
14
+ async handle(event: OrganisationOpened): Promise<void> {
15
+ const published = organisationOpenedV1.create(
16
+ {
17
+ organisationId: event.aggregateId,
18
+ name: event.facts.name,
19
+ openedBy: event.facts.openedBy,
20
+ },
21
+ { id: this.ids.next(), occurredAt: event.occurredAt },
22
+ );
23
+
24
+ {{#if tenancy.any}}
25
+ // The organisation is the tenant of the fact: its consumers run in it, whoever acted.
26
+ await this.publisher.publish([{ ...published, tenantId: event.aggregateId }]);
27
+ {{/if}}
28
+ {{#unless tenancy.any}}
29
+ await this.publisher.publish([published]);
30
+ {{/unless}}
31
+ }
32
+ }
@@ -0,0 +1,17 @@
1
+ /**
2
+ * The administration API of the identity provider: it authenticates, and follows what this
3
+ * context decides, so that its tokens name the organisations of each person and a deactivated
4
+ * account signs in no more. Called by the consumers of the facts the context publishes through its
5
+ * outbox (`application/integration/handlers/`), so at least once and in no guaranteed order: every
6
+ * method is idempotent, and a failure is retried by whoever delivers the fact.
7
+ */
8
+ export abstract class IdentityProviderAdmin {
9
+ /** The provider knows the organisation under its slug, which its tokens then name. */
10
+ abstract openOrganisation(organisation: { readonly id: string; readonly name: string }): Promise<void>;
11
+
12
+ abstract addMember(organisationId: string, accountId: string): Promise<void>;
13
+
14
+ abstract removeMember(organisationId: string, accountId: string): Promise<void>;
15
+
16
+ abstract disableAccount(accountId: string): Promise<void>;
17
+ }
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Where the administration API of the identity provider is, and the client it is called as: the
3
+ * settings of `KeycloakIdentityProviderAdmin`. The default adapter reads them from the
4
+ * environment when a call is made; the platform may bind a value of its validated config instead.
5
+ */
6
+ export abstract class IdentityProviderConnection {
7
+ /** `baseUrl` of the provider, its realm, and a confidential client allowed to manage users and organisations. */
8
+ abstract settings(): Promise<{
9
+ readonly baseUrl: string;
10
+ readonly realm: string;
11
+ readonly clientId: string;
12
+ readonly clientSecret: string;
13
+ }>;
14
+ }
@@ -0,0 +1,7 @@
1
+ /** The tokens of the invitations: unguessable, sent to the person, never stored but as a digest. */
2
+ export abstract class InvitationTokens {
3
+ /** A new token, and the digest the invitation keeps of it. */
4
+ abstract issue(): { readonly token: string; readonly digest: string };
5
+
6
+ abstract digestOf(token: string): string;
7
+ }
@@ -0,0 +1,10 @@
1
+ import type { PageRequest } from '@ddd-ts-kit/kernel';
2
+ import type {
3
+ SearchMembersResult,
4
+ SearchMembersSort,
5
+ } from '{{alias.context}}/application/queries/search-members/search-members.result{{alias.ext}}';
6
+
7
+ export abstract class SearchMembersReader {
8
+ /** One page of the current members of an organisation, with the profile of their account, by the instant they joined then by id. */
9
+ abstract search(organisationId: string, page: PageRequest<SearchMembersSort>): Promise<SearchMembersResult>;
10
+ }
@@ -0,0 +1,33 @@
1
+ import type { Access } from '@ddd-ts-kit/auth';
2
+ import type { Clock } from '@ddd-ts-kit/kernel';
3
+ import { QueryHandler } from '@ddd-ts-kit/kernel/declarations';
4
+ import type { DelegationRepository } from '{{alias.context}}/domain/repositories/delegation.repository{{alias.ext}}';
5
+ import type { MemberRepository } from '{{alias.context}}/domain/repositories/member.repository{{alias.ext}}';
6
+ import { RoleRightsService } from '{{alias.context}}/domain/services/role-rights.service{{alias.ext}}';
7
+ import { {{context.camel}}Permissions } from '{{alias.context}}/application/authorization/{{context.kebab}}.permissions{{alias.ext}}';
8
+ import type { SearchMembersReader } from '{{alias.context}}/application/ports/search-members-reader.port{{alias.ext}}';
9
+ import { SearchMembersQuery } from '{{alias.context}}/application/queries/search-members/search-members.query{{alias.ext}}';
10
+ import type { SearchMembersResult } from '{{alias.context}}/application/queries/search-members/search-members.result{{alias.ext}}';
11
+
12
+ @QueryHandler(SearchMembersQuery)
13
+ export class SearchMembersHandler implements QueryHandler<SearchMembersQuery> {
14
+ constructor(
15
+ private readonly reader: SearchMembersReader,
16
+ private readonly memberRepository: MemberRepository,
17
+ private readonly delegationRepository: DelegationRepository,
18
+ private readonly access: Access,
19
+ private readonly clock: Clock,
20
+ ) {}
21
+
22
+ /** The members of an organisation are read by its members, and by nobody else: the read names the organisation the right was decided on. */
23
+ async execute({ actor, organisationId, page }: SearchMembersQuery): Promise<SearchMembersResult> {
24
+ const now = this.clock.now();
25
+ const member = await this.memberRepository.findOf(organisationId, actor.id);
26
+ const lent = await this.delegationRepository.findUnendedTo(organisationId, actor.id);
27
+ const rights = new RoleRightsService().rightsIn(organisationId, actor.id, { member, lent, now });
28
+
29
+ await this.access.authorize(actor, {{context.camel}}Permissions['member.search'], rights);
30
+
31
+ return this.reader.search(organisationId, page);
32
+ }
33
+ }
@@ -0,0 +1,19 @@
1
+ import { AuthorizedQuery, type Actor, type PageRequest } from '@ddd-ts-kit/kernel';
2
+ import { Requires } from '@ddd-ts-kit/kernel/declarations';
3
+ import { {{context.camel}}Permissions } from '{{alias.context}}/application/authorization/{{context.kebab}}.permissions{{alias.ext}}';
4
+ import type {
5
+ SearchMembersResult,
6
+ SearchMembersSort,
7
+ } from '{{alias.context}}/application/queries/search-members/search-members.result{{alias.ext}}';
8
+
9
+ /** One page of the current members of an organisation, for its members. */
10
+ @Requires({{context.camel}}Permissions['member.search'], { resource: () => ({ type: 'context', id: '{{context.kebab}}' }) })
11
+ export class SearchMembersQuery extends AuthorizedQuery<SearchMembersResult> {
12
+ constructor(
13
+ actor: Actor,
14
+ readonly organisationId: string,
15
+ readonly page: PageRequest<SearchMembersSort>,
16
+ ) {
17
+ super(actor);
18
+ }
19
+ }
@@ -0,0 +1,17 @@
1
+ import type { Page } from '@ddd-ts-kit/kernel';
2
+ import type { MemberRole } from '{{alias.context}}/domain/vocabularies/member-role.vocabulary{{alias.ext}}';
3
+
4
+ /** A current member of an organisation, with the profile of their account. */
5
+ export interface SearchMembersItem {
6
+ readonly memberId: string;
7
+ readonly accountId: string;
8
+ readonly displayName: string;
9
+ readonly email: string;
10
+ readonly role: MemberRole;
11
+ readonly joinedAt: Date;
12
+ }
13
+
14
+ /** By the instant they joined, earliest first unless the client asks otherwise. */
15
+ export type SearchMembersSort = 'joinedAt';
16
+
17
+ export type SearchMembersResult = Page<SearchMembersItem>;
@@ -0,0 +1,92 @@
1
+ import { AggregateRoot } from '@ddd-ts-kit/kernel';
2
+ import { AccountError } from '{{alias.context}}/domain/errors/account.error{{alias.ext}}';
3
+ import { AccountDeactivated } from '{{alias.context}}/domain/events/account-deactivated.event{{alias.ext}}';
4
+ import { AccountProvisioned } from '{{alias.context}}/domain/events/account-provisioned.event{{alias.ext}}';
5
+ import type { DisplayName } from '{{alias.context}}/domain/value-objects/display-name.vo{{alias.ext}}';
6
+ import type { EmailAddress } from '{{alias.context}}/domain/value-objects/email-address.vo{{alias.ext}}';
7
+ import { AccountStatus } from '{{alias.context}}/domain/vocabularies/account-status.vocabulary{{alias.ext}}';
8
+
9
+ /** What the identity provider says of a person, as the application keeps it: refreshed at each sign-in. */
10
+ export interface AccountProfile {
11
+ readonly email: EmailAddress;
12
+ readonly displayName: DisplayName;
13
+ }
14
+
15
+ export interface AccountState extends AccountProfile {
16
+ readonly status: AccountStatus;
17
+ readonly provisionedAt: Date;
18
+ readonly deactivatedAt: Date | undefined;
19
+ }
20
+
21
+ /**
22
+ * One person as the application knows them, under the subject the identity provider gives them:
23
+ * the provider authenticates, the application keeps the profile and authorizes. No password is
24
+ * stored here, nor anything the provider owns.
25
+ */
26
+ export class Account extends AggregateRoot<string> {
27
+ private constructor(
28
+ id: string,
29
+ private state: AccountState,
30
+ ) {
31
+ super(id);
32
+ }
33
+
34
+ /** At the first sign-in of a person: the account takes the subject of the provider as its identity. */
35
+ static provision(subject: string, profile: AccountProfile, now: Date): Account {
36
+ const status = AccountStatus.Active;
37
+ const provisioned = new Account(subject, { ...profile, status, provisionedAt: now, deactivatedAt: undefined });
38
+
39
+ provisioned.record(
40
+ new AccountProvisioned(subject, now, { email: profile.email.value, displayName: profile.displayName.value }),
41
+ );
42
+
43
+ return provisioned;
44
+ }
45
+
46
+ static rehydrate(id: string, state: AccountState): Account {
47
+ return new Account(id, state);
48
+ }
49
+
50
+ /** At a later sign-in: the profile the provider holds now replaces the one kept. */
51
+ refreshProfile(profile: AccountProfile): void {
52
+ this.assertActive();
53
+ this.state = { ...this.state, ...profile };
54
+ }
55
+
56
+ /** The person signs in no more; their memberships and their history stay. */
57
+ deactivate(now: Date): void {
58
+ if (!this.isActive) {
59
+ throw AccountError.alreadyDeactivated(this.id);
60
+ }
61
+
62
+ this.state = { ...this.state, status: AccountStatus.Deactivated, deactivatedAt: now };
63
+ this.record(new AccountDeactivated(this.id, now));
64
+ }
65
+
66
+ /** Refuses every intention of a deactivated account. */
67
+ assertActive(): void {
68
+ if (!this.isActive) {
69
+ throw AccountError.deactivated(this.id);
70
+ }
71
+ }
72
+
73
+ get isActive(): boolean {
74
+ return this.state.status === AccountStatus.Active;
75
+ }
76
+
77
+ get email(): EmailAddress {
78
+ return this.state.email;
79
+ }
80
+
81
+ get displayName(): DisplayName {
82
+ return this.state.displayName;
83
+ }
84
+
85
+ get provisionedAt(): Date {
86
+ return this.state.provisionedAt;
87
+ }
88
+
89
+ toState(): AccountState {
90
+ return { ...this.state };
91
+ }
92
+ }
@@ -0,0 +1,117 @@
1
+ import { AggregateRoot } from '@ddd-ts-kit/kernel';
2
+ import { DelegationError } from '{{alias.context}}/domain/errors/delegation.error{{alias.ext}}';
3
+ import { DelegationGranted } from '{{alias.context}}/domain/events/delegation-granted.event{{alias.ext}}';
4
+ import { DelegationRevoked } from '{{alias.context}}/domain/events/delegation-revoked.event{{alias.ext}}';
5
+ import type { DelegationPeriod } from '{{alias.context}}/domain/value-objects/delegation-period.vo{{alias.ext}}';
6
+ import type { OrganisationRights } from '{{alias.context}}/domain/value-objects/organisation-rights.vo{{alias.ext}}';
7
+ import type { MemberRight } from '{{alias.context}}/domain/vocabularies/member-right.vocabulary{{alias.ext}}';
8
+
9
+ export interface DelegationState {
10
+ readonly organisationId: string;
11
+ readonly delegatorId: string;
12
+ readonly delegateId: string;
13
+ readonly rights: readonly MemberRight[];
14
+ readonly period: DelegationPeriod;
15
+ readonly endedAt: Date | undefined;
16
+ }
17
+
18
+ /** What a delegation lends, and what its delegator may do by their role: a delegation lends nothing beyond it. */
19
+ export interface DelegationTerms extends Omit<DelegationState, 'endedAt'> {
20
+ readonly held: OrganisationRights;
21
+ }
22
+
23
+ /**
24
+ * A member lends some of the rights of their role to another member of the organisation, for a
25
+ * bounded period: the delegate acts in their own name, with those rights, until the end of the
26
+ * period or until the delegation is revoked. Every grant and every end is a fact, and every use
27
+ * case is recorded in the audit trail of the project.
28
+ */
29
+ export class Delegation extends AggregateRoot<string> {
30
+ private constructor(
31
+ id: string,
32
+ private state: DelegationState,
33
+ ) {
34
+ super(id);
35
+ }
36
+
37
+ static grant(id: string, { held, ...terms }: DelegationTerms, now: Date): Delegation {
38
+ assertLendable(terms, held);
39
+
40
+ const granted = new Delegation(id, { ...terms, endedAt: undefined });
41
+ const { organisationId, delegatorId, delegateId, rights, period } = terms;
42
+ const facts = { organisationId, delegatorId, delegateId, rights, startsAt: period.startsAt, endsAt: period.endsAt };
43
+
44
+ granted.record(new DelegationGranted(id, now, facts));
45
+
46
+ return granted;
47
+ }
48
+
49
+ static rehydrate(id: string, state: DelegationState): Delegation {
50
+ return new Delegation(id, state);
51
+ }
52
+
53
+ /** Withdrawn by hand while it holds or before it starts: an ended delegation cannot be. */
54
+ revoke(now: Date): void {
55
+ if (this.state.endedAt !== undefined || now.getTime() >= this.state.period.endsAt.getTime()) {
56
+ throw DelegationError.notActive(this.id);
57
+ }
58
+
59
+ this.end(now);
60
+ }
61
+
62
+ /** Ends because the delegator or the delegate no longer stands as it did when it was granted; already ended, nothing changes. */
63
+ end(now: Date): void {
64
+ if (this.state.endedAt !== undefined) {
65
+ return;
66
+ }
67
+
68
+ const { organisationId, delegatorId, delegateId } = this.state;
69
+
70
+ this.state = { ...this.state, endedAt: now };
71
+ this.record(new DelegationRevoked(this.id, now, { organisationId, delegatorId, delegateId }));
72
+ }
73
+
74
+ isActiveAt(instant: Date): boolean {
75
+ return this.state.endedAt === undefined && this.state.period.includes(instant);
76
+ }
77
+
78
+ get organisationId(): string {
79
+ return this.state.organisationId;
80
+ }
81
+
82
+ get delegatorId(): string {
83
+ return this.state.delegatorId;
84
+ }
85
+
86
+ get delegateId(): string {
87
+ return this.state.delegateId;
88
+ }
89
+
90
+ get rights(): readonly MemberRight[] {
91
+ return this.state.rights;
92
+ }
93
+
94
+ get period(): DelegationPeriod {
95
+ return this.state.period;
96
+ }
97
+
98
+ toState(): DelegationState {
99
+ return { ...this.state };
100
+ }
101
+ }
102
+
103
+ function assertLendable(terms: Omit<DelegationState, 'endedAt'>, held: OrganisationRights): void {
104
+ if (terms.delegateId === terms.delegatorId) {
105
+ throw DelegationError.toOneself(terms.delegatorId);
106
+ }
107
+
108
+ if (terms.rights.length === 0) {
109
+ throw DelegationError.withoutRights();
110
+ }
111
+
112
+ const beyond = terms.rights.find((right) => !held.includes(right));
113
+
114
+ if (beyond !== undefined) {
115
+ throw DelegationError.beyondRights(beyond);
116
+ }
117
+ }
@@ -0,0 +1,128 @@
1
+ import { AggregateRoot } from '@ddd-ts-kit/kernel';
2
+ import { InvitationError } from '{{alias.context}}/domain/errors/invitation.error{{alias.ext}}';
3
+ import { InvitationAccepted } from '{{alias.context}}/domain/events/invitation-accepted.event{{alias.ext}}';
4
+ import { InvitationDeclined } from '{{alias.context}}/domain/events/invitation-declined.event{{alias.ext}}';
5
+ import { InvitationIssued } from '{{alias.context}}/domain/events/invitation-issued.event{{alias.ext}}';
6
+ import { InvitationRevoked } from '{{alias.context}}/domain/events/invitation-revoked.event{{alias.ext}}';
7
+ import type { EmailAddress } from '{{alias.context}}/domain/value-objects/email-address.vo{{alias.ext}}';
8
+ import { InvitationStatus } from '{{alias.context}}/domain/vocabularies/invitation-status.vocabulary{{alias.ext}}';
9
+ import type { MemberRole } from '{{alias.context}}/domain/vocabularies/member-role.vocabulary{{alias.ext}}';
10
+
11
+ /** How long an invitation can be answered: a week, then a new one is sent. */
12
+ const VALIDITY_MS = 7 * 86_400_000;
13
+
14
+ export interface InvitationState {
15
+ readonly organisationId: string;
16
+ readonly email: EmailAddress;
17
+ readonly role: MemberRole;
18
+ /** The digest of the token sent to the person: the token itself is never stored. */
19
+ readonly tokenHash: string;
20
+ readonly invitedBy: string;
21
+ readonly issuedAt: Date;
22
+ readonly expiresAt: Date;
23
+ readonly status: InvitationStatus;
24
+ readonly answeredAt: Date | undefined;
25
+ }
26
+
27
+ /** What an invitation is issued with. */
28
+ export type InvitationTerms = Pick<InvitationState, 'organisationId' | 'email' | 'role' | 'tokenHash' | 'invitedBy'> & {
29
+ readonly delegationId?: string;
30
+ };
31
+
32
+ /** The person who answers an invitation: an account, with the email address its provider knows. */
33
+ export interface Invitee {
34
+ readonly id: string;
35
+ readonly email: EmailAddress;
36
+ }
37
+
38
+ /** An organisation asks a person, by email address, to join it in a role; the answer is theirs alone. */
39
+ export class Invitation extends AggregateRoot<string> {
40
+ private constructor(
41
+ id: string,
42
+ private state: InvitationState,
43
+ ) {
44
+ super(id);
45
+ }
46
+
47
+ /** `delegationId` names the delegation that lent the right to invite, when the role of the inviter does not grant it. */
48
+ static issue(id: string, terms: InvitationTerms, now: Date): Invitation {
49
+ const expiresAt = new Date(now.getTime() + VALIDITY_MS);
50
+ const pending = { status: InvitationStatus.Pending, issuedAt: now, expiresAt, answeredAt: undefined };
51
+ const { organisationId, email, role, tokenHash, invitedBy, delegationId } = terms;
52
+ const issued = new Invitation(id, { organisationId, email, role, tokenHash, invitedBy, ...pending });
53
+ const under = delegationId === undefined ? {} : { delegationId };
54
+
55
+ issued.record(new InvitationIssued(id, now, { organisationId, email: email.value, role, expiresAt, ...under }));
56
+
57
+ return issued;
58
+ }
59
+
60
+ static rehydrate(id: string, state: InvitationState): Invitation {
61
+ return new Invitation(id, state);
62
+ }
63
+
64
+ /** By the person it was addressed to, before it expires: they become a member (see `AcceptInvitationHandler`). */
65
+ accept(invitee: Invitee, now: Date): void {
66
+ this.assertAnswerableBy(invitee, now);
67
+ this.answer(InvitationStatus.Accepted, now);
68
+ this.record(new InvitationAccepted(this.id, now, { organisationId: this.organisationId, accountId: invitee.id }));
69
+ }
70
+
71
+ decline(invitee: Invitee, now: Date): void {
72
+ this.assertAnswerableBy(invitee, now);
73
+ this.answer(InvitationStatus.Declined, now);
74
+ this.record(new InvitationDeclined(this.id, now, { organisationId: this.organisationId }));
75
+ }
76
+
77
+ /** Taken back by the organisation while it waits for an answer, expired or not. */
78
+ revoke(now: Date): void {
79
+ if (this.state.status !== InvitationStatus.Pending) {
80
+ throw InvitationError.notPending(this.id, this.state.status);
81
+ }
82
+
83
+ this.answer(InvitationStatus.Revoked, now);
84
+ this.record(new InvitationRevoked(this.id, now, { organisationId: this.organisationId }));
85
+ }
86
+
87
+ get organisationId(): string {
88
+ return this.state.organisationId;
89
+ }
90
+
91
+ get email(): EmailAddress {
92
+ return this.state.email;
93
+ }
94
+
95
+ get role(): MemberRole {
96
+ return this.state.role;
97
+ }
98
+
99
+ get status(): InvitationStatus {
100
+ return this.state.status;
101
+ }
102
+
103
+ get expiresAt(): Date {
104
+ return this.state.expiresAt;
105
+ }
106
+
107
+ toState(): InvitationState {
108
+ return { ...this.state };
109
+ }
110
+
111
+ private assertAnswerableBy(invitee: Invitee, now: Date): void {
112
+ if (this.state.status !== InvitationStatus.Pending) {
113
+ throw InvitationError.notPending(this.id, this.state.status);
114
+ }
115
+
116
+ if (now.getTime() >= this.state.expiresAt.getTime()) {
117
+ throw InvitationError.expired(this.id, this.state.expiresAt);
118
+ }
119
+
120
+ if (!invitee.email.equals(this.state.email)) {
121
+ throw InvitationError.addressedToAnother(this.id);
122
+ }
123
+ }
124
+
125
+ private answer(status: InvitationStatus, now: Date): void {
126
+ this.state = { ...this.state, status, answeredAt: now };
127
+ }
128
+ }
@@ -0,0 +1,149 @@
1
+ import { AggregateRoot } from '@ddd-ts-kit/kernel';
2
+ import { MemberError } from '{{alias.context}}/domain/errors/member.error{{alias.ext}}';
3
+ import { MemberJoined } from '{{alias.context}}/domain/events/member-joined.event{{alias.ext}}';
4
+ import { MemberRemoved } from '{{alias.context}}/domain/events/member-removed.event{{alias.ext}}';
5
+ import { MemberRoleChanged } from '{{alias.context}}/domain/events/member-role-changed.event{{alias.ext}}';
6
+ import { MemberRole } from '{{alias.context}}/domain/vocabularies/member-role.vocabulary{{alias.ext}}';
7
+
8
+ export interface MemberState {
9
+ readonly organisationId: string;
10
+ readonly accountId: string;
11
+ readonly role: MemberRole;
12
+ readonly joinedAt: Date;
13
+ readonly removedAt: Date | undefined;
14
+ }
15
+
16
+ /**
17
+ * Who asks for a change of membership: their account and their own role (never the rights a
18
+ * delegation lends them, so that nobody appoints above themselves), the delegation that lends
19
+ * them the right when their role does not grant it, and how many owners the organisation has.
20
+ */
21
+ export interface MembershipChange {
22
+ readonly by: MemberRole;
23
+ readonly byAccount: string;
24
+ readonly delegationId: string | undefined;
25
+ readonly owners: number;
26
+ }
27
+
28
+ /** The roles from the least to the most: a member appoints, changes and removes no one above their own role. */
29
+ const FROM_LEAST: readonly MemberRole[] = [MemberRole.Member, MemberRole.Admin, MemberRole.Owner];
30
+
31
+ /** The membership of one account in one organisation, in a role. */
32
+ export class Member extends AggregateRoot<string> {
33
+ private constructor(
34
+ id: string,
35
+ private state: MemberState,
36
+ ) {
37
+ super(id);
38
+ }
39
+
40
+ static join(id: string, terms: Pick<MemberState, 'organisationId' | 'accountId' | 'role'>, now: Date): Member {
41
+ const joined = new Member(id, { ...terms, joinedAt: now, removedAt: undefined });
42
+
43
+ joined.record(new MemberJoined(id, now, terms));
44
+
45
+ return joined;
46
+ }
47
+
48
+ static rehydrate(id: string, state: MemberState): Member {
49
+ return new Member(id, state);
50
+ }
51
+
52
+ /** Whoever invites or appoints in `role` holds that role or a higher one: only an owner makes an owner. */
53
+ static assertAppointable(organisationId: string, role: MemberRole, by: MemberRole): void {
54
+ if (!outranks(role, by)) {
55
+ return;
56
+ }
57
+
58
+ throw role === MemberRole.Owner ? MemberError.ownersOnly(organisationId) : MemberError.aboveOwnRole(organisationId);
59
+ }
60
+
61
+ /** Never one's own role, never above one's own role, and the last owner stays one. */
62
+ changeRole(role: MemberRole, change: MembershipChange, now: Date): void {
63
+ this.assertActive();
64
+
65
+ if (change.byAccount === this.state.accountId) {
66
+ throw MemberError.ownRole(this.state.organisationId);
67
+ }
68
+
69
+ if (role === this.state.role) {
70
+ return;
71
+ }
72
+
73
+ this.assertWithinRole(change.by, role);
74
+ this.assertOwnersKept(change, role);
75
+
76
+ const { organisationId, accountId, role: from } = this.state;
77
+
78
+ this.state = { ...this.state, role };
79
+ this.record(new MemberRoleChanged(this.id, now, { organisationId, accountId, from, to: role, ...under(change) }));
80
+ }
81
+
82
+ /** A member leaves, or is removed by someone of their role or above; the last owner stays. */
83
+ remove(change: MembershipChange, now: Date): void {
84
+ this.assertActive();
85
+
86
+ if (change.byAccount !== this.state.accountId) {
87
+ this.assertWithinRole(change.by, this.state.role);
88
+ }
89
+
90
+ this.assertOwnersKept(change, undefined);
91
+
92
+ const { organisationId, accountId } = this.state;
93
+
94
+ this.state = { ...this.state, removedAt: now };
95
+ this.record(new MemberRemoved(this.id, now, { organisationId, accountId, ...under(change) }));
96
+ }
97
+
98
+ get isActive(): boolean {
99
+ return this.state.removedAt === undefined;
100
+ }
101
+
102
+ get organisationId(): string {
103
+ return this.state.organisationId;
104
+ }
105
+
106
+ get accountId(): string {
107
+ return this.state.accountId;
108
+ }
109
+
110
+ get role(): MemberRole {
111
+ return this.state.role;
112
+ }
113
+
114
+ get joinedAt(): Date {
115
+ return this.state.joinedAt;
116
+ }
117
+
118
+ toState(): MemberState {
119
+ return { ...this.state };
120
+ }
121
+
122
+ private assertActive(): void {
123
+ if (!this.isActive) {
124
+ throw MemberError.notFound(this.state.organisationId, this.state.accountId);
125
+ }
126
+ }
127
+
128
+ /** The role the member holds, and the one they are given, are both within the role of who decides. */
129
+ private assertWithinRole(by: MemberRole, next: MemberRole): void {
130
+ Member.assertAppointable(this.state.organisationId, this.state.role, by);
131
+ Member.assertAppointable(this.state.organisationId, next, by);
132
+ }
133
+
134
+ /** `next` is the role after the change, none for a removal. */
135
+ private assertOwnersKept(change: MembershipChange, next: MemberRole | undefined): void {
136
+ if (this.state.role === MemberRole.Owner && next !== MemberRole.Owner && change.owners <= 1) {
137
+ throw MemberError.lastOwner(this.state.organisationId);
138
+ }
139
+ }
140
+ }
141
+
142
+ function outranks(role: MemberRole, other: MemberRole): boolean {
143
+ return FROM_LEAST.indexOf(role) > FROM_LEAST.indexOf(other);
144
+ }
145
+
146
+ /** The delegation a change was decided under, named in its fact so that the trail says whose rights were used. */
147
+ function under(change: MembershipChange): { readonly delegationId?: string } {
148
+ return change.delegationId === undefined ? {} : { delegationId: change.delegationId };
149
+ }