ddd-ts-kit 0.14.0 → 0.16.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.
- package/README.md +31 -2
- package/dist/check/catalogue/architecture-rules.d.ts.map +1 -1
- package/dist/check/catalogue/architecture-rules.js +2 -0
- package/dist/check/catalogue/architecture-rules.js.map +1 -1
- package/dist/check/explain/domain-docs.d.ts +3 -0
- package/dist/check/explain/domain-docs.d.ts.map +1 -0
- package/dist/check/explain/domain-docs.js +30 -0
- package/dist/check/explain/domain-docs.js.map +1 -0
- package/dist/check/explain/rule-docs.d.ts.map +1 -1
- package/dist/check/explain/rule-docs.js +2 -0
- package/dist/check/explain/rule-docs.js.map +1 -1
- package/dist/check/explain/tenancy-docs.d.ts.map +1 -1
- package/dist/check/explain/tenancy-docs.js +3 -1
- package/dist/check/explain/tenancy-docs.js.map +1 -1
- package/dist/check/fixtures/fixtures.d.ts.map +1 -1
- package/dist/check/fixtures/fixtures.js +4 -0
- package/dist/check/fixtures/fixtures.js.map +1 -1
- package/dist/check/fixtures/model-function-fixtures.d.ts +4 -0
- package/dist/check/fixtures/model-function-fixtures.d.ts.map +1 -0
- package/dist/check/fixtures/model-function-fixtures.js +52 -0
- package/dist/check/fixtures/model-function-fixtures.js.map +1 -0
- package/dist/check/fixtures/tenancy-fixtures.d.ts.map +1 -1
- package/dist/check/fixtures/tenancy-fixtures.js +28 -0
- package/dist/check/fixtures/tenancy-fixtures.js.map +1 -1
- package/dist/check/fixtures/value-object-file-fixtures.d.ts +4 -0
- package/dist/check/fixtures/value-object-file-fixtures.d.ts.map +1 -0
- package/dist/check/fixtures/value-object-file-fixtures.js +54 -0
- package/dist/check/fixtures/value-object-file-fixtures.js.map +1 -0
- package/dist/check/fixtures/value-object-files.d.ts +34 -0
- package/dist/check/fixtures/value-object-files.d.ts.map +1 -0
- package/dist/check/fixtures/value-object-files.js +46 -0
- package/dist/check/fixtures/value-object-files.js.map +1 -0
- package/dist/check/rules/domain-value-object-class.d.ts +3 -0
- package/dist/check/rules/domain-value-object-class.d.ts.map +1 -0
- package/dist/check/rules/domain-value-object-class.js +21 -0
- package/dist/check/rules/domain-value-object-class.js.map +1 -0
- package/dist/check/structure/class-body-brace.d.ts +6 -0
- package/dist/check/structure/class-body-brace.d.ts.map +1 -0
- package/dist/check/structure/class-body-brace.js +22 -0
- package/dist/check/structure/class-body-brace.js.map +1 -0
- package/dist/check/structure/read-structure.d.ts.map +1 -1
- package/dist/check/structure/read-structure.js +3 -1
- package/dist/check/structure/read-structure.js.map +1 -1
- package/dist/check/tenancy/knex-unscoped.d.ts +6 -1
- package/dist/check/tenancy/knex-unscoped.d.ts.map +1 -1
- package/dist/check/tenancy/knex-unscoped.js +19 -6
- package/dist/check/tenancy/knex-unscoped.js.map +1 -1
- package/dist/check/tenancy/raw-reaches-rows.d.ts +8 -0
- package/dist/check/tenancy/raw-reaches-rows.d.ts.map +1 -0
- package/dist/check/tenancy/raw-reaches-rows.js +91 -0
- package/dist/check/tenancy/raw-reaches-rows.js.map +1 -0
- package/dist/check/tenancy/raw-sql-text.d.ts +9 -0
- package/dist/check/tenancy/raw-sql-text.d.ts.map +1 -0
- package/dist/check/tenancy/raw-sql-text.js +29 -0
- package/dist/check/tenancy/raw-sql-text.js.map +1 -0
- package/dist/check/values/answered-literals.d.ts +8 -0
- package/dist/check/values/answered-literals.d.ts.map +1 -0
- package/dist/check/values/answered-literals.js +20 -0
- package/dist/check/values/answered-literals.js.map +1 -0
- package/dist/check/values/arrow-body.d.ts +4 -0
- package/dist/check/values/arrow-body.d.ts.map +1 -0
- package/dist/check/values/arrow-body.js +52 -0
- package/dist/check/values/arrow-body.js.map +1 -0
- package/dist/check/values/entry-role.d.ts +10 -0
- package/dist/check/values/entry-role.d.ts.map +1 -0
- package/dist/check/values/entry-role.js +40 -0
- package/dist/check/values/entry-role.js.map +1 -0
- package/dist/check/values/exported-function-body.d.ts +9 -0
- package/dist/check/values/exported-function-body.d.ts.map +1 -0
- package/dist/check/values/exported-function-body.js +29 -0
- package/dist/check/values/exported-function-body.js.map +1 -0
- package/dist/check/values/exported-values.d.ts +9 -0
- package/dist/check/values/exported-values.d.ts.map +1 -0
- package/dist/check/values/exported-values.js +48 -0
- package/dist/check/values/exported-values.js.map +1 -0
- package/dist/check/values/extends-value-object.d.ts +9 -0
- package/dist/check/values/extends-value-object.d.ts.map +1 -0
- package/dist/check/values/extends-value-object.js +25 -0
- package/dist/check/values/extends-value-object.js.map +1 -0
- package/dist/check/values/function-body.d.ts +10 -0
- package/dist/check/values/function-body.d.ts.map +1 -0
- package/dist/check/values/function-body.js +2 -0
- package/dist/check/values/function-body.js.map +1 -0
- package/dist/check/values/generate-hint.d.ts +4 -0
- package/dist/check/values/generate-hint.d.ts.map +1 -0
- package/dist/check/values/generate-hint.js +8 -0
- package/dist/check/values/generate-hint.js.map +1 -0
- package/dist/check/values/initializer-of.d.ts +3 -0
- package/dist/check/values/initializer-of.d.ts.map +1 -0
- package/dist/check/values/initializer-of.js +15 -0
- package/dist/check/values/initializer-of.js.map +1 -0
- package/dist/check/values/is-model-file.d.ts +8 -0
- package/dist/check/values/is-model-file.d.ts.map +1 -0
- package/dist/check/values/is-model-file.js +15 -0
- package/dist/check/values/is-model-file.js.map +1 -0
- package/dist/check/values/is-value-object-file.d.ts +7 -0
- package/dist/check/values/is-value-object-file.d.ts.map +1 -0
- package/dist/check/values/is-value-object-file.js +14 -0
- package/dist/check/values/is-value-object-file.js.map +1 -0
- package/dist/check/values/javascript-globals.d.ts +3 -0
- package/dist/check/values/javascript-globals.d.ts.map +1 -0
- package/dist/check/values/javascript-globals.js +22 -0
- package/dist/check/values/javascript-globals.js.map +1 -0
- package/dist/check/values/literal-at.d.ts +3 -0
- package/dist/check/values/literal-at.d.ts.map +1 -0
- package/dist/check/values/literal-at.js +7 -0
- package/dist/check/values/literal-at.js.map +1 -0
- package/dist/check/values/local-declaration.d.ts +8 -0
- package/dist/check/values/local-declaration.d.ts.map +1 -0
- package/dist/check/values/local-declaration.js +31 -0
- package/dist/check/values/local-declaration.js.map +1 -0
- package/dist/check/values/nested-bodies.d.ts +8 -0
- package/dist/check/values/nested-bodies.d.ts.map +1 -0
- package/dist/check/values/nested-bodies.js +28 -0
- package/dist/check/values/nested-bodies.js.map +1 -0
- package/dist/check/values/plain-value-functions.d.ts +10 -0
- package/dist/check/values/plain-value-functions.d.ts.map +1 -0
- package/dist/check/values/plain-value-functions.js +33 -0
- package/dist/check/values/plain-value-functions.js.map +1 -0
- package/dist/check/values/returns-package-type.d.ts +9 -0
- package/dist/check/values/returns-package-type.d.ts.map +1 -0
- package/dist/check/values/returns-package-type.js +12 -0
- package/dist/check/values/returns-package-type.js.map +1 -0
- package/dist/check/values/stands-for-a-value.d.ts +9 -0
- package/dist/check/values/stands-for-a-value.d.ts.map +1 -0
- package/dist/check/values/stands-for-a-value.js +28 -0
- package/dist/check/values/stands-for-a-value.js.map +1 -0
- package/dist/check/values/value-object-file-findings.d.ts +5 -0
- package/dist/check/values/value-object-file-findings.d.ts.map +1 -0
- package/dist/check/values/value-object-file-findings.js +96 -0
- package/dist/check/values/value-object-file-findings.js.map +1 -0
- package/dist/commands/generate.js +2 -0
- package/dist/commands/generate.js.map +1 -1
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +12 -1
- package/dist/commands/init.js.map +1 -1
- package/dist/config/index.d.ts +1 -1
- package/dist/config/index.d.ts.map +1 -1
- package/dist/config/index.js +1 -1
- package/dist/config/index.js.map +1 -1
- package/dist/config/parse-stack.d.ts.map +1 -1
- package/dist/config/parse-stack.js +2 -1
- package/dist/config/parse-stack.js.map +1 -1
- package/dist/config/stack.d.ts +8 -0
- package/dist/config/stack.d.ts.map +1 -1
- package/dist/config/stack.js +1 -0
- package/dist/config/stack.js.map +1 -1
- package/dist/declared/injection/laid-out-calls.d.ts +7 -0
- package/dist/declared/injection/laid-out-calls.d.ts.map +1 -0
- package/dist/declared/injection/laid-out-calls.js +14 -0
- package/dist/declared/injection/laid-out-calls.js.map +1 -0
- package/dist/declared/render-barrel.d.ts.map +1 -1
- package/dist/declared/render-barrel.js +2 -1
- package/dist/declared/render-barrel.js.map +1 -1
- package/dist/declared/render-dependencies-file.d.ts.map +1 -1
- package/dist/declared/render-dependencies-file.js +2 -1
- package/dist/declared/render-dependencies-file.js.map +1 -1
- package/dist/generate/kind.d.ts +2 -0
- package/dist/generate/kind.d.ts.map +1 -1
- package/dist/generate/kinds/context-kind.d.ts.map +1 -1
- package/dist/generate/kinds/context-kind.js +13 -6
- package/dist/generate/kinds/context-kind.js.map +1 -1
- package/dist/generate/kinds/next-adr-number.d.ts +4 -0
- package/dist/generate/kinds/next-adr-number.d.ts.map +1 -0
- package/dist/generate/kinds/next-adr-number.js +7 -0
- package/dist/generate/kinds/next-adr-number.js.map +1 -0
- package/dist/generate/kinds/write-adr.js +3 -6
- package/dist/generate/kinds/write-adr.js.map +1 -1
- package/dist/help/usage.d.ts +1 -1
- package/dist/help/usage.d.ts.map +1 -1
- package/dist/help/usage.js +2 -2
- package/dist/init/manifest/versions.js +24 -24
- package/dist/init/run-init.d.ts +2 -0
- package/dist/init/run-init.d.ts.map +1 -1
- package/dist/init/run-init.js +15 -1
- package/dist/init/run-init.js.map +1 -1
- package/dist/init/stack/resolve-stack.d.ts.map +1 -1
- package/dist/init/stack/resolve-stack.js +3 -1
- package/dist/init/stack/resolve-stack.js.map +1 -1
- package/dist/init/stack/stack-options.d.ts +2 -0
- package/dist/init/stack/stack-options.d.ts.map +1 -1
- package/dist/presets/apply-preset.d.ts +15 -0
- package/dist/presets/apply-preset.d.ts.map +1 -0
- package/dist/presets/apply-preset.js +94 -0
- package/dist/presets/apply-preset.js.map +1 -0
- package/dist/presets/grant-to-signed-in.d.ts +9 -0
- package/dist/presets/grant-to-signed-in.d.ts.map +1 -0
- package/dist/presets/grant-to-signed-in.js +61 -0
- package/dist/presets/grant-to-signed-in.js.map +1 -0
- package/dist/presets/identity-preset.d.ts +9 -0
- package/dist/presets/identity-preset.d.ts.map +1 -0
- package/dist/presets/identity-preset.js +44 -0
- package/dist/presets/identity-preset.js.map +1 -0
- package/dist/presets/identity-refusal.d.ts +5 -0
- package/dist/presets/identity-refusal.d.ts.map +1 -0
- package/dist/presets/identity-refusal.js +29 -0
- package/dist/presets/identity-refusal.js.map +1 -0
- package/dist/presets/identity-terms.d.ts +4 -0
- package/dist/presets/identity-terms.d.ts.map +1 -0
- package/dist/presets/identity-terms.js +57 -0
- package/dist/presets/identity-terms.js.map +1 -0
- package/dist/presets/no-flags.d.ts +4 -0
- package/dist/presets/no-flags.d.ts.map +1 -0
- package/dist/presets/no-flags.js +18 -0
- package/dist/presets/no-flags.js.map +1 -0
- package/dist/presets/organisations-as-tenants.d.ts +9 -0
- package/dist/presets/organisations-as-tenants.d.ts.map +1 -0
- package/dist/presets/organisations-as-tenants.js +43 -0
- package/dist/presets/organisations-as-tenants.js.map +1 -0
- package/dist/presets/preset-decision.d.ts +8 -0
- package/dist/presets/preset-decision.d.ts.map +1 -0
- package/dist/presets/preset-decision.js +19 -0
- package/dist/presets/preset-decision.js.map +1 -0
- package/dist/presets/preset-file.d.ts +13 -0
- package/dist/presets/preset-file.d.ts.map +1 -0
- package/dist/presets/preset-file.js +2 -0
- package/dist/presets/preset-file.js.map +1 -0
- package/dist/presets/preset-files.d.ts +9 -0
- package/dist/presets/preset-files.d.ts.map +1 -0
- package/dist/presets/preset-files.js +34 -0
- package/dist/presets/preset-files.js.map +1 -0
- package/dist/presets/preset-named.d.ts +3 -0
- package/dist/presets/preset-named.d.ts.map +1 -0
- package/dist/presets/preset-named.js +12 -0
- package/dist/presets/preset-named.js.map +1 -0
- package/dist/presets/preset-refusal.d.ts +6 -0
- package/dist/presets/preset-refusal.d.ts.map +1 -0
- package/dist/presets/preset-refusal.js +2 -0
- package/dist/presets/preset-refusal.js.map +1 -0
- package/dist/presets/preset-step.d.ts +8 -0
- package/dist/presets/preset-step.d.ts.map +1 -0
- package/dist/presets/preset-step.js +2 -0
- package/dist/presets/preset-step.js.map +1 -0
- package/dist/presets/preset.d.ts +36 -0
- package/dist/presets/preset.d.ts.map +1 -0
- package/dist/presets/preset.js +2 -0
- package/dist/presets/preset.js.map +1 -0
- package/dist/presets/with-authorizer-import.d.ts +6 -0
- package/dist/presets/with-authorizer-import.d.ts.map +1 -0
- package/dist/presets/with-authorizer-import.js +27 -0
- package/dist/presets/with-authorizer-import.js.map +1 -0
- package/dist/stack/stack-vars.js +2 -1
- package/dist/stack/stack-vars.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/workspace/workspace.d.ts +2 -0
- package/dist/workspace/workspace.d.ts.map +1 -1
- package/dist/workspace/workspace.js +4 -0
- package/dist/workspace/workspace.js.map +1 -1
- package/package.json +2 -2
- package/templates/init/rules/domain.md.tpl +1 -1
- package/templates/pipeline/package-scripts.json +1 -1
- package/templates/presets/identity/context/application/authorization/__context__.permissions.ts.tpl +23 -0
- package/templates/presets/identity/context/application/authorization/change-member-roles.policy.ts.tpl +17 -0
- package/templates/presets/identity/context/application/authorization/grant-delegations.policy.ts.tpl +17 -0
- package/templates/presets/identity/context/application/authorization/invite-members.policy.ts.tpl +17 -0
- package/templates/presets/identity/context/application/authorization/read-members.policy.ts.tpl +17 -0
- package/templates/presets/identity/context/application/authorization/remove-members.policy.ts.tpl +17 -0
- package/templates/presets/identity/context/application/authorization/revoke-delegations.policy.ts.tpl +17 -0
- package/templates/presets/identity/context/application/authorization/revoke-invitations.policy.ts.tpl +17 -0
- package/templates/presets/identity/context/application/commands/accept-invitation/accept-invitation.command.ts.tpl +14 -0
- package/templates/presets/identity/context/application/commands/accept-invitation/accept-invitation.handler.ts.tpl +56 -0
- package/templates/presets/identity/context/application/commands/change-member-role/change-member-role.command.ts.tpl +17 -0
- package/templates/presets/identity/context/application/commands/change-member-role/change-member-role.handler.ts.tpl +82 -0
- package/templates/presets/identity/context/application/commands/deactivate-account/deactivate-account.command.ts.tpl +7 -0
- package/templates/presets/identity/context/application/commands/deactivate-account/deactivate-account.handler.ts.tpl +69 -0
- package/templates/presets/identity/context/application/commands/decline-invitation/decline-invitation.command.ts.tpl +14 -0
- package/templates/presets/identity/context/application/commands/decline-invitation/decline-invitation.handler.ts.tpl +36 -0
- package/templates/presets/identity/context/application/commands/grant-delegation/grant-delegation.command.ts.tpl +21 -0
- package/templates/presets/identity/context/application/commands/grant-delegation/grant-delegation.handler.ts.tpl +43 -0
- package/templates/presets/identity/context/application/commands/invite-member/invite-member.command.ts.tpl +16 -0
- package/templates/presets/identity/context/application/commands/invite-member/invite-member.handler.ts.tpl +53 -0
- package/templates/presets/identity/context/application/commands/open-organisation/open-organisation.command.ts.tpl +14 -0
- package/templates/presets/identity/context/application/commands/open-organisation/open-organisation.handler.ts.tpl +50 -0
- package/templates/presets/identity/context/application/commands/provision-account/provision-account.command.ts.tpl +14 -0
- package/templates/presets/identity/context/application/commands/provision-account/provision-account.handler.ts.tpl +41 -0
- package/templates/presets/identity/context/application/commands/remove-member/remove-member.command.ts.tpl +15 -0
- package/templates/presets/identity/context/application/commands/remove-member/remove-member.handler.ts.tpl +79 -0
- package/templates/presets/identity/context/application/commands/revoke-delegation/revoke-delegation.command.ts.tpl +15 -0
- package/templates/presets/identity/context/application/commands/revoke-delegation/revoke-delegation.handler.ts.tpl +39 -0
- package/templates/presets/identity/context/application/commands/revoke-invitation/revoke-invitation.command.ts.tpl +15 -0
- package/templates/presets/identity/context/application/commands/revoke-invitation/revoke-invitation.handler.ts.tpl +42 -0
- package/templates/presets/identity/context/application/integration/handlers/account-deactivated.integration-handler.ts.tpl +19 -0
- package/templates/presets/identity/context/application/integration/handlers/member-joined.integration-handler.ts.tpl +33 -0
- package/templates/presets/identity/context/application/integration/handlers/member-removed.integration-handler.ts.tpl +30 -0
- package/templates/presets/identity/context/application/integration/handlers/organisation-opened.integration-handler.ts.tpl +19 -0
- package/templates/presets/identity/context/application/integration/publishers/account-deactivated.publisher.ts.tpl +24 -0
- package/templates/presets/identity/context/application/integration/publishers/delegation-granted.publisher.ts.tpl +36 -0
- package/templates/presets/identity/context/application/integration/publishers/delegation-revoked.publisher.ts.tpl +33 -0
- package/templates/presets/identity/context/application/integration/publishers/member-joined.publisher.ts.tpl +33 -0
- package/templates/presets/identity/context/application/integration/publishers/member-removed.publisher.ts.tpl +33 -0
- package/templates/presets/identity/context/application/integration/publishers/member-role-changed.publisher.ts.tpl +35 -0
- package/templates/presets/identity/context/application/integration/publishers/organisation-opened.publisher.ts.tpl +32 -0
- package/templates/presets/identity/context/application/ports/identity-provider-admin.port.ts.tpl +17 -0
- package/templates/presets/identity/context/application/ports/identity-provider-connection.port.ts.tpl +14 -0
- package/templates/presets/identity/context/application/ports/invitation-tokens.port.ts.tpl +7 -0
- package/templates/presets/identity/context/application/ports/search-members-reader.port.ts.tpl +10 -0
- package/templates/presets/identity/context/application/queries/search-members/search-members.handler.ts.tpl +33 -0
- package/templates/presets/identity/context/application/queries/search-members/search-members.query.ts.tpl +19 -0
- package/templates/presets/identity/context/application/queries/search-members/search-members.result.ts.tpl +17 -0
- package/templates/presets/identity/context/domain/aggregates/account.aggregate.ts.tpl +92 -0
- package/templates/presets/identity/context/domain/aggregates/delegation.aggregate.ts.tpl +117 -0
- package/templates/presets/identity/context/domain/aggregates/invitation.aggregate.ts.tpl +128 -0
- package/templates/presets/identity/context/domain/aggregates/member.aggregate.ts.tpl +149 -0
- package/templates/presets/identity/context/domain/aggregates/organisation.aggregate.ts.tpl +52 -0
- package/templates/presets/identity/context/domain/errors/account.error.ts.tpl +40 -0
- package/templates/presets/identity/context/domain/errors/delegation.error.ts.tpl +45 -0
- package/templates/presets/identity/context/domain/errors/invitation.error.ts.tpl +30 -0
- package/templates/presets/identity/context/domain/errors/member.error.ts.tpl +50 -0
- package/templates/presets/identity/context/domain/errors/organisation.error.ts.tpl +32 -0
- package/templates/presets/identity/context/domain/events/account-deactivated.event.ts.tpl +13 -0
- package/templates/presets/identity/context/domain/events/account-provisioned.event.ts.tpl +17 -0
- package/templates/presets/identity/context/domain/events/delegation-granted.event.ts.tpl +22 -0
- package/templates/presets/identity/context/domain/events/delegation-revoked.event.ts.tpl +18 -0
- package/templates/presets/identity/context/domain/events/invitation-accepted.event.ts.tpl +17 -0
- package/templates/presets/identity/context/domain/events/invitation-declined.event.ts.tpl +16 -0
- package/templates/presets/identity/context/domain/events/invitation-issued.event.ts.tpl +22 -0
- package/templates/presets/identity/context/domain/events/invitation-revoked.event.ts.tpl +16 -0
- package/templates/presets/identity/context/domain/events/member-joined.event.ts.tpl +19 -0
- package/templates/presets/identity/context/domain/events/member-removed.event.ts.tpl +19 -0
- package/templates/presets/identity/context/domain/events/member-role-changed.event.ts.tpl +22 -0
- package/templates/presets/identity/context/domain/events/organisation-opened.event.ts.tpl +17 -0
- package/templates/presets/identity/context/domain/repositories/account.repository.ts.tpl +5 -0
- package/templates/presets/identity/context/domain/repositories/delegation.repository.ts.tpl +10 -0
- package/templates/presets/identity/context/domain/repositories/invitation.repository.ts.tpl +7 -0
- package/templates/presets/identity/context/domain/repositories/member.repository.ts.tpl +13 -0
- package/templates/presets/identity/context/domain/repositories/organisation.repository.ts.tpl +5 -0
- package/templates/presets/identity/context/domain/services/role-rights.service.ts.tpl +59 -0
- package/templates/presets/identity/context/domain/value-objects/delegation-period.vo.ts.tpl +42 -0
- package/templates/presets/identity/context/domain/value-objects/display-name.vo.ts.tpl +29 -0
- package/templates/presets/identity/context/domain/value-objects/email-address.vo.ts.tpl +31 -0
- package/templates/presets/identity/context/domain/value-objects/organisation-name.vo.ts.tpl +29 -0
- package/templates/presets/identity/context/domain/value-objects/organisation-rights.vo.ts.tpl +43 -0
- package/templates/presets/identity/context/domain/value-objects/organisation-slug.vo.ts.tpl +30 -0
- package/templates/presets/identity/context/domain/vocabularies/account-status.vocabulary.ts.tpl +7 -0
- package/templates/presets/identity/context/domain/vocabularies/invitation-status.vocabulary.ts.tpl +9 -0
- package/templates/presets/identity/context/domain/vocabularies/member-right.vocabulary.ts.tpl +9 -0
- package/templates/presets/identity/context/domain/vocabularies/member-role.vocabulary.ts.tpl +8 -0
- package/templates/presets/identity/context/infrastructure/external/crypto-invitation-tokens.adapter.ts.tpl +20 -0
- package/templates/presets/identity/context/infrastructure/external/environment-identity-provider-connection.adapter.ts.tpl +37 -0
- package/templates/presets/identity/context/infrastructure/external/keycloak-identity-provider-admin.adapter.ts.tpl +149 -0
- package/templates/presets/identity/context/infrastructure/persistence/__context__.schema.drizzle.ts.tpl +102 -0
- package/templates/presets/identity/context/infrastructure/persistence/account-row.schema.knex.ts.tpl +15 -0
- package/templates/presets/identity/context/infrastructure/persistence/account.mapper.drizzle.ts.tpl +38 -0
- package/templates/presets/identity/context/infrastructure/persistence/account.mapper.knex.ts.tpl +33 -0
- package/templates/presets/identity/context/infrastructure/persistence/create-__context__-tables.schema.knex.ts.tpl +107 -0
- package/templates/presets/identity/context/infrastructure/persistence/delegation-row.schema.knex.ts.tpl +17 -0
- package/templates/presets/identity/context/infrastructure/persistence/delegation.mapper.drizzle.ts.tpl +50 -0
- package/templates/presets/identity/context/infrastructure/persistence/delegation.mapper.knex.ts.tpl +45 -0
- package/templates/presets/identity/context/infrastructure/persistence/drizzle-account.repository.drizzle.ts.tpl +59 -0
- package/templates/presets/identity/context/infrastructure/persistence/drizzle-delegation.repository.drizzle.ts.tpl +77 -0
- package/templates/presets/identity/context/infrastructure/persistence/drizzle-invitation.repository.drizzle.ts.tpl +65 -0
- package/templates/presets/identity/context/infrastructure/persistence/drizzle-member.repository.drizzle.ts.tpl +84 -0
- package/templates/presets/identity/context/infrastructure/persistence/drizzle-organisation.repository.drizzle.ts.tpl +59 -0
- package/templates/presets/identity/context/infrastructure/persistence/drizzle-search-members-reader.repository.drizzle.ts.tpl +53 -0
- package/templates/presets/identity/context/infrastructure/persistence/invitation-row.schema.knex.ts.tpl +20 -0
- package/templates/presets/identity/context/infrastructure/persistence/invitation.mapper.drizzle.ts.tpl +45 -0
- package/templates/presets/identity/context/infrastructure/persistence/invitation.mapper.knex.ts.tpl +40 -0
- package/templates/presets/identity/context/infrastructure/persistence/knex-account.repository.knex.ts.tpl +61 -0
- package/templates/presets/identity/context/infrastructure/persistence/knex-delegation.repository.knex.ts.tpl +80 -0
- package/templates/presets/identity/context/infrastructure/persistence/knex-invitation.repository.knex.ts.tpl +67 -0
- package/templates/presets/identity/context/infrastructure/persistence/knex-member.repository.knex.ts.tpl +82 -0
- package/templates/presets/identity/context/infrastructure/persistence/knex-organisation.repository.knex.ts.tpl +61 -0
- package/templates/presets/identity/context/infrastructure/persistence/knex-search-members-reader.repository.knex.ts.tpl +77 -0
- package/templates/presets/identity/context/infrastructure/persistence/member-row.schema.knex.ts.tpl +15 -0
- package/templates/presets/identity/context/infrastructure/persistence/member.mapper.drizzle.ts.tpl +36 -0
- package/templates/presets/identity/context/infrastructure/persistence/member.mapper.knex.ts.tpl +31 -0
- package/templates/presets/identity/context/infrastructure/persistence/organisation-row.schema.knex.ts.tpl +12 -0
- package/templates/presets/identity/context/infrastructure/persistence/organisation.mapper.drizzle.ts.tpl +28 -0
- package/templates/presets/identity/context/infrastructure/persistence/organisation.mapper.knex.ts.tpl +26 -0
- package/templates/presets/identity/context/presentation/http/accounts/deactivate-account/deactivate-account.accepted.class-validator.per-route.ts.tpl +6 -0
- package/templates/presets/identity/context/presentation/http/accounts/deactivate-account/deactivate-account.controller.ts.tpl +71 -0
- package/templates/presets/identity/context/presentation/http/accounts/provision-account/provision-account.accepted.class-validator.per-route.ts.tpl +6 -0
- package/templates/presets/identity/context/presentation/http/accounts/provision-account/provision-account.body.class-validator.ts.tpl +9 -0
- package/templates/presets/identity/context/presentation/http/accounts/provision-account/provision-account.controller.ts.tpl +85 -0
- package/templates/presets/identity/context/presentation/http/accounts.routes.ts.tpl +5 -0
- package/templates/presets/identity/context/presentation/http/invitations/accept-invitation/accept-invitation.accepted.class-validator.per-route.ts.tpl +6 -0
- package/templates/presets/identity/context/presentation/http/invitations/accept-invitation/accept-invitation.body.class-validator.per-route.ts.tpl +8 -0
- package/templates/presets/identity/context/presentation/http/invitations/accept-invitation/accept-invitation.controller.ts.tpl +100 -0
- package/templates/presets/identity/context/presentation/http/invitations/decline-invitation/decline-invitation.accepted.class-validator.per-route.ts.tpl +6 -0
- package/templates/presets/identity/context/presentation/http/invitations/decline-invitation/decline-invitation.body.class-validator.per-route.ts.tpl +8 -0
- package/templates/presets/identity/context/presentation/http/invitations/decline-invitation/decline-invitation.controller.ts.tpl +98 -0
- package/templates/presets/identity/context/presentation/http/invitations.routes.ts.tpl +5 -0
- package/templates/presets/identity/context/presentation/http/organisations/change-member-role/change-member-role.accepted.class-validator.per-route.ts.tpl +6 -0
- package/templates/presets/identity/context/presentation/http/organisations/change-member-role/change-member-role.body.class-validator.ts.tpl +7 -0
- package/templates/presets/identity/context/presentation/http/organisations/change-member-role/change-member-role.controller.ts.tpl +83 -0
- package/templates/presets/identity/context/presentation/http/organisations/change-member-role/change-member-role.params.class-validator.ts.tpl +11 -0
- package/templates/presets/identity/context/presentation/http/organisations/grant-delegation/grant-delegation.accepted.class-validator.per-route.ts.tpl +6 -0
- package/templates/presets/identity/context/presentation/http/organisations/grant-delegation/grant-delegation.body.class-validator.ts.tpl +18 -0
- package/templates/presets/identity/context/presentation/http/organisations/grant-delegation/grant-delegation.controller.ts.tpl +103 -0
- package/templates/presets/identity/context/presentation/http/organisations/grant-delegation/grant-delegation.params.class-validator.ts.tpl +7 -0
- package/templates/presets/identity/context/presentation/http/organisations/invite-member/invite-member.accepted.class-validator.ts.tpl +10 -0
- package/templates/presets/identity/context/presentation/http/organisations/invite-member/invite-member.body.class-validator.ts.tpl +10 -0
- package/templates/presets/identity/context/presentation/http/organisations/invite-member/invite-member.controller.ts.tpl +65 -0
- package/templates/presets/identity/context/presentation/http/organisations/invite-member/invite-member.params.class-validator.ts.tpl +7 -0
- package/templates/presets/identity/context/presentation/http/organisations/open-organisation/open-organisation.accepted.class-validator.per-route.ts.tpl +6 -0
- package/templates/presets/identity/context/presentation/http/organisations/open-organisation/open-organisation.body.class-validator.ts.tpl +12 -0
- package/templates/presets/identity/context/presentation/http/organisations/open-organisation/open-organisation.controller.ts.tpl +79 -0
- package/templates/presets/identity/context/presentation/http/organisations/remove-member/remove-member.accepted.class-validator.per-route.ts.tpl +6 -0
- package/templates/presets/identity/context/presentation/http/organisations/remove-member/remove-member.controller.ts.tpl +73 -0
- package/templates/presets/identity/context/presentation/http/organisations/remove-member/remove-member.params.class-validator.ts.tpl +11 -0
- package/templates/presets/identity/context/presentation/http/organisations/revoke-delegation/revoke-delegation.accepted.class-validator.per-route.ts.tpl +6 -0
- package/templates/presets/identity/context/presentation/http/organisations/revoke-delegation/revoke-delegation.controller.ts.tpl +73 -0
- package/templates/presets/identity/context/presentation/http/organisations/revoke-delegation/revoke-delegation.params.class-validator.ts.tpl +11 -0
- package/templates/presets/identity/context/presentation/http/organisations/revoke-invitation/revoke-invitation.accepted.class-validator.per-route.ts.tpl +6 -0
- package/templates/presets/identity/context/presentation/http/organisations/revoke-invitation/revoke-invitation.controller.ts.tpl +73 -0
- package/templates/presets/identity/context/presentation/http/organisations/revoke-invitation/revoke-invitation.params.class-validator.ts.tpl +11 -0
- package/templates/presets/identity/context/presentation/http/organisations/search-members/search-members.controller.ts.tpl +86 -0
- package/templates/presets/identity/context/presentation/http/organisations/search-members/search-members.line.class-validator.ts.tpl +22 -0
- package/templates/presets/identity/context/presentation/http/organisations/search-members/search-members.params.class-validator.ts.tpl +7 -0
- package/templates/presets/identity/context/presentation/http/organisations.routes.ts.tpl +5 -0
- package/templates/presets/identity/context/presentation/http/shared/invitation-token.schema.class-validator.shared.ts.tpl +9 -0
- package/templates/presets/identity/context/presentation/http/shared/invitation-token.schema.zod.shared.ts.tpl +9 -0
- package/templates/presets/identity/context/published-language/account-deactivated.v1.integration-event.ts.tpl +33 -0
- package/templates/presets/identity/context/published-language/account-deactivated.v1.payload.class-validator.ts.tpl +7 -0
- package/templates/presets/identity/context/published-language/delegation-granted.v1.integration-event.ts.tpl +39 -0
- package/templates/presets/identity/context/published-language/delegation-granted.v1.payload.class-validator.ts.tpl +28 -0
- package/templates/presets/identity/context/published-language/delegation-revoked.v1.integration-event.ts.tpl +36 -0
- package/templates/presets/identity/context/published-language/delegation-revoked.v1.payload.class-validator.ts.tpl +16 -0
- package/templates/presets/identity/context/published-language/member-joined.v1.integration-event.ts.tpl +36 -0
- package/templates/presets/identity/context/published-language/member-joined.v1.payload.class-validator.ts.tpl +16 -0
- package/templates/presets/identity/context/published-language/member-removed.v1.integration-event.ts.tpl +36 -0
- package/templates/presets/identity/context/published-language/member-removed.v1.payload.class-validator.ts.tpl +18 -0
- package/templates/presets/identity/context/published-language/member-role-changed.v1.integration-event.ts.tpl +38 -0
- package/templates/presets/identity/context/published-language/member-role-changed.v1.payload.class-validator.ts.tpl +24 -0
- package/templates/presets/identity/context/published-language/organisation-opened.v1.integration-event.ts.tpl +35 -0
- package/templates/presets/identity/context/published-language/organisation-opened.v1.payload.class-validator.ts.tpl +13 -0
- package/templates/presets/identity/context/test/builders/account.builder.ts.tpl +59 -0
- package/templates/presets/identity/context/test/builders/delegation.builder.ts.tpl +67 -0
- package/templates/presets/identity/context/test/builders/invitation.builder.ts.tpl +60 -0
- package/templates/presets/identity/context/test/builders/member.builder.ts.tpl +59 -0
- package/templates/presets/identity/context/test/builders/organisation.builder.ts.tpl +44 -0
- package/templates/presets/identity/context/test/fakes/account.repository.fake.ts.tpl +10 -0
- package/templates/presets/identity/context/test/fakes/delegation.repository.fake.ts.tpl +26 -0
- package/templates/presets/identity/context/test/fakes/identity-provider-admin.fake.ts.tpl +41 -0
- package/templates/presets/identity/context/test/fakes/invitation-tokens.fake.ts.tpl +18 -0
- package/templates/presets/identity/context/test/fakes/invitation.repository.fake.ts.tpl +14 -0
- package/templates/presets/identity/context/test/fakes/member.repository.fake.ts.tpl +29 -0
- package/templates/presets/identity/context/test/fakes/organisation.repository.fake.ts.tpl +10 -0
- package/templates/presets/identity/context/test/fakes/search-members-reader.fake.ts.tpl +25 -0
- package/templates/presets/identity/context/test/integration/account.repository.contract.spec.ts.tpl +48 -0
- package/templates/presets/identity/context/test/integration/delegation.repository.contract.spec.ts.tpl +84 -0
- package/templates/presets/identity/context/test/integration/invitation.repository.contract.spec.ts.tpl +76 -0
- package/templates/presets/identity/context/test/integration/keycloak-identity-provider-admin.adapter.spec.ts.tpl +123 -0
- package/templates/presets/identity/context/test/integration/member.repository.contract.spec.ts.tpl +92 -0
- package/templates/presets/identity/context/test/integration/organisation.repository.contract.spec.ts.tpl +46 -0
- package/templates/presets/identity/context/test/integration/search-members-reader.spec.ts.tpl +86 -0
- package/templates/presets/identity/context/test/unit/accept-invitation.controller.spec.ts.tpl +57 -0
- package/templates/presets/identity/context/test/unit/accept-invitation.handler.spec.ts.tpl +69 -0
- package/templates/presets/identity/context/test/unit/account-deactivated.integration-handler.spec.ts.tpl +24 -0
- package/templates/presets/identity/context/test/unit/account-deactivated.publisher.spec.ts.tpl +25 -0
- package/templates/presets/identity/context/test/unit/account.aggregate.spec.ts.tpl +72 -0
- package/templates/presets/identity/context/test/unit/change-member-role.controller.spec.ts.tpl +57 -0
- package/templates/presets/identity/context/test/unit/change-member-role.handler.spec.ts.tpl +135 -0
- package/templates/presets/identity/context/test/unit/crypto-invitation-tokens.adapter.spec.ts.tpl +15 -0
- package/templates/presets/identity/context/test/unit/deactivate-account.controller.spec.ts.tpl +44 -0
- package/templates/presets/identity/context/test/unit/deactivate-account.handler.spec.ts.tpl +106 -0
- package/templates/presets/identity/context/test/unit/decline-invitation.controller.spec.ts.tpl +57 -0
- package/templates/presets/identity/context/test/unit/decline-invitation.handler.spec.ts.tpl +40 -0
- package/templates/presets/identity/context/test/unit/delegation-granted.publisher.spec.ts.tpl +36 -0
- package/templates/presets/identity/context/test/unit/delegation-period.vo.spec.ts.tpl +32 -0
- package/templates/presets/identity/context/test/unit/delegation-revoked.publisher.spec.ts.tpl +35 -0
- package/templates/presets/identity/context/test/unit/delegation.aggregate.spec.ts.tpl +111 -0
- package/templates/presets/identity/context/test/unit/display-name.vo.spec.ts.tpl +13 -0
- package/templates/presets/identity/context/test/unit/email-address.vo.spec.ts.tpl +24 -0
- package/templates/presets/identity/context/test/unit/environment-identity-provider-connection.adapter.spec.ts.tpl +41 -0
- package/templates/presets/identity/context/test/unit/grant-delegation.controller.spec.ts.tpl +57 -0
- package/templates/presets/identity/context/test/unit/grant-delegation.handler.spec.ts.tpl +66 -0
- package/templates/presets/identity/context/test/unit/identity-application.ts.tpl +79 -0
- package/templates/presets/identity/context/test/unit/invitation.aggregate.spec.ts.tpl +118 -0
- package/templates/presets/identity/context/test/unit/invite-member.controller.spec.ts.tpl +57 -0
- package/templates/presets/identity/context/test/unit/invite-member.handler.spec.ts.tpl +110 -0
- package/templates/presets/identity/context/test/unit/member-joined.integration-handler.spec.ts.tpl +38 -0
- package/templates/presets/identity/context/test/unit/member-joined.publisher.spec.ts.tpl +36 -0
- package/templates/presets/identity/context/test/unit/member-removed.integration-handler.spec.ts.tpl +45 -0
- package/templates/presets/identity/context/test/unit/member-removed.publisher.spec.ts.tpl +35 -0
- package/templates/presets/identity/context/test/unit/member-role-changed.publisher.spec.ts.tpl +36 -0
- package/templates/presets/identity/context/test/unit/member.aggregate.spec.ts.tpl +142 -0
- package/templates/presets/identity/context/test/unit/open-organisation.controller.spec.ts.tpl +57 -0
- package/templates/presets/identity/context/test/unit/open-organisation.handler.spec.ts.tpl +52 -0
- package/templates/presets/identity/context/test/unit/organisation-name.vo.spec.ts.tpl +13 -0
- package/templates/presets/identity/context/test/unit/organisation-opened.integration-handler.spec.ts.tpl +24 -0
- package/templates/presets/identity/context/test/unit/organisation-opened.publisher.spec.ts.tpl +35 -0
- package/templates/presets/identity/context/test/unit/organisation-rights.vo.spec.ts.tpl +23 -0
- package/templates/presets/identity/context/test/unit/organisation-slug.vo.spec.ts.tpl +17 -0
- package/templates/presets/identity/context/test/unit/organisation.aggregate.spec.ts.tpl +33 -0
- package/templates/presets/identity/context/test/unit/provision-account.controller.spec.ts.tpl +73 -0
- package/templates/presets/identity/context/test/unit/provision-account.handler.spec.ts.tpl +65 -0
- package/templates/presets/identity/context/test/unit/remove-member.controller.spec.ts.tpl +44 -0
- package/templates/presets/identity/context/test/unit/remove-member.handler.spec.ts.tpl +125 -0
- package/templates/presets/identity/context/test/unit/revoke-delegation.controller.spec.ts.tpl +44 -0
- package/templates/presets/identity/context/test/unit/revoke-delegation.handler.spec.ts.tpl +51 -0
- package/templates/presets/identity/context/test/unit/revoke-invitation.controller.spec.ts.tpl +44 -0
- package/templates/presets/identity/context/test/unit/revoke-invitation.handler.spec.ts.tpl +44 -0
- package/templates/presets/identity/context/test/unit/role-rights.service.spec.ts.tpl +54 -0
- package/templates/presets/identity/context/test/unit/search-members.controller.spec.ts.tpl +43 -0
- package/templates/presets/identity/context/test/unit/search-members.handler.spec.ts.tpl +38 -0
- package/templates/presets/identity/decision.md.tpl +37 -0
- package/templates/presets/identity/design.md.tpl +71 -0
- package/templates/presets/signed-in-grants.ts.tpl +12 -0
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { describe, expect, it } from '{{tests.module}}';
|
|
2
|
+
import { MemberRight } from '{{alias.context}}/domain/vocabularies/member-right.vocabulary{{alias.ext}}';
|
|
3
|
+
import { MemberRole } from '{{alias.context}}/domain/vocabularies/member-role.vocabulary{{alias.ext}}';
|
|
4
|
+
import { RemoveMemberCommand } from '{{alias.context}}/application/commands/remove-member/remove-member.command{{alias.ext}}';
|
|
5
|
+
import { aDelegation } from '{{alias.context}}/test/builders/delegation.builder{{alias.ext}}';
|
|
6
|
+
import { aMember } from '{{alias.context}}/test/builders/member.builder{{alias.ext}}';
|
|
7
|
+
import { anOrganisation } from '{{alias.context}}/test/builders/organisation.builder{{alias.ext}}';
|
|
8
|
+
import { arrangeIdentity } from '{{alias.context}}/test/unit/identity-application{{alias.ext}}';
|
|
9
|
+
|
|
10
|
+
const DAY = 86_400_000;
|
|
11
|
+
|
|
12
|
+
describe('Remove member', () => {
|
|
13
|
+
it('removes the member, ends every delegation they gave or received, and tells the provider and the other contexts', async () => {
|
|
14
|
+
const { application, organisations, members, delegations, identityProvider, as } = arrangeIdentity();
|
|
15
|
+
const now = application.clock.now();
|
|
16
|
+
|
|
17
|
+
await organisations.save(anOrganisation().identifiedBy('acme').build());
|
|
18
|
+
await members.save(aMember().of('acme', 'subject-ada').as(MemberRole.Admin).build());
|
|
19
|
+
await members.save(aMember().of('acme', 'subject-alan').build());
|
|
20
|
+
|
|
21
|
+
await delegations.save(
|
|
22
|
+
aDelegation()
|
|
23
|
+
.identifiedBy('delegation-1')
|
|
24
|
+
.to('subject-alan')
|
|
25
|
+
.lending(MemberRight.InviteMembers)
|
|
26
|
+
.between(now, new Date(now.getTime() + DAY))
|
|
27
|
+
.build(),
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
// Started once the organisation is arranged: what the provider is asked from now on follows the removal alone.
|
|
31
|
+
await application.start();
|
|
32
|
+
|
|
33
|
+
const before = application.eventBus.published.length;
|
|
34
|
+
|
|
35
|
+
await application.commandBus.execute(new RemoveMemberCommand(as('subject-ada'), 'acme', 'subject-alan'));
|
|
36
|
+
|
|
37
|
+
expect(await members.findOf('acme', 'subject-alan')).toBeUndefined();
|
|
38
|
+
expect((await delegations.findById('delegation-1'))?.isActiveAt(now)).toBe(false);
|
|
39
|
+
expect(identityProvider.asked).toEqual(['remove subject-alan from acme']);
|
|
40
|
+
|
|
41
|
+
expect(application.eventBus.published.slice(before).map((event) => event.type)).toEqual([
|
|
42
|
+
'{{context.kebab}}.member.removed',
|
|
43
|
+
'{{context.kebab}}.delegation.revoked',
|
|
44
|
+
]);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('refuses a person who is not a member, and keeps the last owner', async () => {
|
|
48
|
+
const { application, organisations, members, as } = arrangeIdentity();
|
|
49
|
+
|
|
50
|
+
await organisations.save(anOrganisation().identifiedBy('acme').build());
|
|
51
|
+
await members.save(aMember().of('acme', 'subject-ada').as(MemberRole.Owner).build());
|
|
52
|
+
|
|
53
|
+
await expect(
|
|
54
|
+
application.commandBus.execute(new RemoveMemberCommand(as('subject-ada'), 'acme', 'subject-nobody')),
|
|
55
|
+
).rejects.toMatchObject({ code: 'MEMBER_NOT_FOUND' });
|
|
56
|
+
|
|
57
|
+
await expect(
|
|
58
|
+
application.commandBus.execute(new RemoveMemberCommand(as('subject-ada'), 'acme', 'subject-ada')),
|
|
59
|
+
).rejects.toMatchObject({ code: 'LAST_OWNER' });
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('is refused to a member who does not manage the members', async () => {
|
|
63
|
+
const { application, organisations, members, as } = arrangeIdentity();
|
|
64
|
+
|
|
65
|
+
await organisations.save(anOrganisation().identifiedBy('acme').build());
|
|
66
|
+
await members.save(aMember().of('acme', 'subject-alan').build());
|
|
67
|
+
await members.save(aMember().of('acme', 'subject-grace').build());
|
|
68
|
+
|
|
69
|
+
await expect(
|
|
70
|
+
application.commandBus.execute(new RemoveMemberCommand(as('subject-alan'), 'acme', 'subject-grace')),
|
|
71
|
+
).rejects.toMatchObject({ code: 'FORBIDDEN' });
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
describe('Remove member under a delegation', () => {
|
|
76
|
+
it('removes within the role of the delegate, never above it, and names the delegation used', async () => {
|
|
77
|
+
const { application, organisations, members, delegations, as } = arrangeIdentity();
|
|
78
|
+
const now = application.clock.now();
|
|
79
|
+
|
|
80
|
+
await organisations.save(anOrganisation().identifiedBy('acme').build());
|
|
81
|
+
await members.save(aMember().of('acme', 'subject-ada').as(MemberRole.Owner).build());
|
|
82
|
+
await members.save(aMember().of('acme', 'subject-alan').build());
|
|
83
|
+
await members.save(aMember().of('acme', 'subject-irene').build());
|
|
84
|
+
await members.save(aMember().of('acme', 'subject-linus').as(MemberRole.Admin).build());
|
|
85
|
+
|
|
86
|
+
await delegations.save(
|
|
87
|
+
aDelegation()
|
|
88
|
+
.identifiedBy('delegation-1')
|
|
89
|
+
.from('subject-ada')
|
|
90
|
+
.to('subject-alan')
|
|
91
|
+
.lending(MemberRight.ManageMembers)
|
|
92
|
+
.between(now, new Date(now.getTime() + DAY))
|
|
93
|
+
.build(),
|
|
94
|
+
);
|
|
95
|
+
|
|
96
|
+
await expect(
|
|
97
|
+
application.commandBus.execute(new RemoveMemberCommand(as('subject-alan'), 'acme', 'subject-linus')),
|
|
98
|
+
).rejects.toMatchObject({ code: 'ABOVE_OWN_ROLE' });
|
|
99
|
+
|
|
100
|
+
await application.commandBus.execute(new RemoveMemberCommand(as('subject-alan'), 'acme', 'subject-irene'));
|
|
101
|
+
|
|
102
|
+
expect(application.eventBus.published.at(-1)).toMatchObject({
|
|
103
|
+
type: '{{context.kebab}}.member.removed',
|
|
104
|
+
payload: { accountId: 'subject-irene', delegationId: 'delegation-1' },
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
describe('Two owners who remove each other at once', () => {
|
|
110
|
+
it('cannot both commit: the organisation keeps an owner, and the second change is refused as a conflict', async () => {
|
|
111
|
+
const { application, organisations, members, as } = arrangeIdentity();
|
|
112
|
+
|
|
113
|
+
await organisations.save(anOrganisation().identifiedBy('acme').build());
|
|
114
|
+
await members.save(aMember().of('acme', 'subject-ada').as(MemberRole.Owner).build());
|
|
115
|
+
await members.save(aMember().of('acme', 'subject-grace').as(MemberRole.Owner).build());
|
|
116
|
+
|
|
117
|
+
const outcomes = await Promise.allSettled([
|
|
118
|
+
application.commandBus.execute(new RemoveMemberCommand(as('subject-ada'), 'acme', 'subject-grace')),
|
|
119
|
+
application.commandBus.execute(new RemoveMemberCommand(as('subject-grace'), 'acme', 'subject-ada')),
|
|
120
|
+
]);
|
|
121
|
+
|
|
122
|
+
expect(outcomes.map((outcome) => outcome.status).sort()).toEqual(['fulfilled', 'rejected']);
|
|
123
|
+
expect(await members.countOwners('acme')).toBe(1);
|
|
124
|
+
});
|
|
125
|
+
});
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { describe, expect, it } from '{{tests.module}}';
|
|
2
|
+
import { controllerRoute, executeRoute } from '@ddd-ts-kit/http';
|
|
3
|
+
import { InMemoryCommandBus, type Actor } from '@ddd-ts-kit/kernel';
|
|
4
|
+
import { RevokeDelegationCommand } from '{{alias.context}}/application/commands/revoke-delegation/revoke-delegation.command{{alias.ext}}';
|
|
5
|
+
import { RevokeDelegationController } from '{{alias.context}}/presentation/http/organisations/revoke-delegation/revoke-delegation.controller{{alias.ext}}';
|
|
6
|
+
|
|
7
|
+
/** Who calls, as the authenticator of the platform makes them from the token. */
|
|
8
|
+
const caller: Actor = {
|
|
9
|
+
id: 'subject-ada',
|
|
10
|
+
type: 'user',
|
|
11
|
+
roles: [],
|
|
12
|
+
attributes: { email: 'ada@example.test', username: 'ada' },
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
describe('DELETE /organisations/:organisationId/delegations/:delegationId', () => {
|
|
16
|
+
it('answers 200 with what the use case answered, and sends it what the request says', async () => {
|
|
17
|
+
const { route, sent } = arrange();
|
|
18
|
+
|
|
19
|
+
const response = await executeRoute(
|
|
20
|
+
route,
|
|
21
|
+
{ params: { organisationId: 'acme', delegationId: 'delegation-1' }, query: {}, headers: {}, body: {}, raw: undefined },
|
|
22
|
+
{ validateResponses: true, resolvePrincipal: () => caller },
|
|
23
|
+
);
|
|
24
|
+
|
|
25
|
+
expect(route.path).toBe('/organisations/:organisationId/delegations/:delegationId');
|
|
26
|
+
expect([response.status, response.body]).toEqual([200, { id: 'delegation-1' }]);
|
|
27
|
+
expect(sent).toMatchObject([{ organisationId: 'acme', delegationId: 'delegation-1' }]);
|
|
28
|
+
});
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
/** The route, on a bus that keeps what it was sent and answers as the use case would. */
|
|
32
|
+
function arrange() {
|
|
33
|
+
const sent: RevokeDelegationCommand[] = [];
|
|
34
|
+
|
|
35
|
+
const commands = new InMemoryCommandBus().register(RevokeDelegationCommand, {
|
|
36
|
+
execute: async (command) => {
|
|
37
|
+
sent.push(command);
|
|
38
|
+
|
|
39
|
+
return 'delegation-1';
|
|
40
|
+
},
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
return { route: controllerRoute(new RevokeDelegationController(commands)), sent };
|
|
44
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { describe, expect, it } from '{{tests.module}}';
|
|
2
|
+
import { MemberRole } from '{{alias.context}}/domain/vocabularies/member-role.vocabulary{{alias.ext}}';
|
|
3
|
+
import { RevokeDelegationCommand } from '{{alias.context}}/application/commands/revoke-delegation/revoke-delegation.command{{alias.ext}}';
|
|
4
|
+
import { aDelegation } from '{{alias.context}}/test/builders/delegation.builder{{alias.ext}}';
|
|
5
|
+
import { aMember } from '{{alias.context}}/test/builders/member.builder{{alias.ext}}';
|
|
6
|
+
import { arrangeIdentity } from '{{alias.context}}/test/unit/identity-application{{alias.ext}}';
|
|
7
|
+
|
|
8
|
+
describe('Revoke delegation', () => {
|
|
9
|
+
it('ends a delegation of the organisation while it holds, and tells the other contexts', async () => {
|
|
10
|
+
const { application, members, delegations, as } = arrangeIdentity();
|
|
11
|
+
const now = application.clock.now();
|
|
12
|
+
|
|
13
|
+
await members.save(aMember().of('acme', 'subject-ada').as(MemberRole.Owner).build());
|
|
14
|
+
|
|
15
|
+
await delegations.save(
|
|
16
|
+
aDelegation()
|
|
17
|
+
.identifiedBy('delegation-1')
|
|
18
|
+
.between(now, new Date(now.getTime() + 86_400_000))
|
|
19
|
+
.build(),
|
|
20
|
+
);
|
|
21
|
+
|
|
22
|
+
const before = application.eventBus.published.length;
|
|
23
|
+
|
|
24
|
+
await application.commandBus.execute(new RevokeDelegationCommand(as('subject-ada'), 'acme', 'delegation-1'));
|
|
25
|
+
|
|
26
|
+
expect((await delegations.findById('delegation-1'))?.isActiveAt(now)).toBe(false);
|
|
27
|
+
expect(application.eventBus.published.slice(before)).toMatchObject([{ type: '{{context.kebab}}.delegation.revoked' }]);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('finds no delegation of another organisation', async () => {
|
|
31
|
+
const { application, members, delegations, as } = arrangeIdentity();
|
|
32
|
+
|
|
33
|
+
await members.save(aMember().of('acme', 'subject-ada').as(MemberRole.Owner).build());
|
|
34
|
+
await delegations.save(aDelegation().identifiedBy('delegation-1').in('globex').build());
|
|
35
|
+
|
|
36
|
+
await expect(
|
|
37
|
+
application.commandBus.execute(new RevokeDelegationCommand(as('subject-ada'), 'acme', 'delegation-1')),
|
|
38
|
+
).rejects.toMatchObject({ code: 'DELEGATION_NOT_FOUND' });
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('is refused to a member whose role does not delegate', async () => {
|
|
42
|
+
const { application, members, delegations, as } = arrangeIdentity();
|
|
43
|
+
|
|
44
|
+
await members.save(aMember().of('acme', 'subject-alan').build());
|
|
45
|
+
await delegations.save(aDelegation().identifiedBy('delegation-1').build());
|
|
46
|
+
|
|
47
|
+
await expect(
|
|
48
|
+
application.commandBus.execute(new RevokeDelegationCommand(as('subject-alan'), 'acme', 'delegation-1')),
|
|
49
|
+
).rejects.toMatchObject({ code: 'FORBIDDEN' });
|
|
50
|
+
});
|
|
51
|
+
});
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { describe, expect, it } from '{{tests.module}}';
|
|
2
|
+
import { controllerRoute, executeRoute } from '@ddd-ts-kit/http';
|
|
3
|
+
import { InMemoryCommandBus, type Actor } from '@ddd-ts-kit/kernel';
|
|
4
|
+
import { RevokeInvitationCommand } from '{{alias.context}}/application/commands/revoke-invitation/revoke-invitation.command{{alias.ext}}';
|
|
5
|
+
import { RevokeInvitationController } from '{{alias.context}}/presentation/http/organisations/revoke-invitation/revoke-invitation.controller{{alias.ext}}';
|
|
6
|
+
|
|
7
|
+
/** Who calls, as the authenticator of the platform makes them from the token. */
|
|
8
|
+
const caller: Actor = {
|
|
9
|
+
id: 'subject-ada',
|
|
10
|
+
type: 'user',
|
|
11
|
+
roles: [],
|
|
12
|
+
attributes: { email: 'ada@example.test', username: 'ada' },
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
describe('DELETE /organisations/:organisationId/invitations/:invitationId', () => {
|
|
16
|
+
it('answers 200 with what the use case answered, and sends it what the request says', async () => {
|
|
17
|
+
const { route, sent } = arrange();
|
|
18
|
+
|
|
19
|
+
const response = await executeRoute(
|
|
20
|
+
route,
|
|
21
|
+
{ params: { organisationId: 'acme', invitationId: 'invitation-1' }, query: {}, headers: {}, body: {}, raw: undefined },
|
|
22
|
+
{ validateResponses: true, resolvePrincipal: () => caller },
|
|
23
|
+
);
|
|
24
|
+
|
|
25
|
+
expect(route.path).toBe('/organisations/:organisationId/invitations/:invitationId');
|
|
26
|
+
expect([response.status, response.body]).toEqual([200, { id: 'invitation-1' }]);
|
|
27
|
+
expect(sent).toMatchObject([{ organisationId: 'acme', invitationId: 'invitation-1' }]);
|
|
28
|
+
});
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
/** The route, on a bus that keeps what it was sent and answers as the use case would. */
|
|
32
|
+
function arrange() {
|
|
33
|
+
const sent: RevokeInvitationCommand[] = [];
|
|
34
|
+
|
|
35
|
+
const commands = new InMemoryCommandBus().register(RevokeInvitationCommand, {
|
|
36
|
+
execute: async (command) => {
|
|
37
|
+
sent.push(command);
|
|
38
|
+
|
|
39
|
+
return 'invitation-1';
|
|
40
|
+
},
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
return { route: controllerRoute(new RevokeInvitationController(commands)), sent };
|
|
44
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { describe, expect, it } from '{{tests.module}}';
|
|
2
|
+
import { InvitationStatus } from '{{alias.context}}/domain/vocabularies/invitation-status.vocabulary{{alias.ext}}';
|
|
3
|
+
import { MemberRole } from '{{alias.context}}/domain/vocabularies/member-role.vocabulary{{alias.ext}}';
|
|
4
|
+
import { RevokeInvitationCommand } from '{{alias.context}}/application/commands/revoke-invitation/revoke-invitation.command{{alias.ext}}';
|
|
5
|
+
import { anInvitation } from '{{alias.context}}/test/builders/invitation.builder{{alias.ext}}';
|
|
6
|
+
import { aMember } from '{{alias.context}}/test/builders/member.builder{{alias.ext}}';
|
|
7
|
+
import { arrangeIdentity } from '{{alias.context}}/test/unit/identity-application{{alias.ext}}';
|
|
8
|
+
|
|
9
|
+
describe('Revoke invitation', () => {
|
|
10
|
+
it('takes back an invitation of the organisation', async () => {
|
|
11
|
+
const { application, members, invitations, as } = arrangeIdentity();
|
|
12
|
+
|
|
13
|
+
await members.save(aMember().of('acme', 'subject-ada').as(MemberRole.Admin).build());
|
|
14
|
+
await invitations.save(anInvitation().identifiedBy('invitation-1').to('acme').build());
|
|
15
|
+
|
|
16
|
+
await application.commandBus.execute(new RevokeInvitationCommand(as('subject-ada'), 'acme', 'invitation-1'));
|
|
17
|
+
|
|
18
|
+
expect((await invitations.findById('invitation-1'))?.status).toBe(InvitationStatus.Revoked);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it('finds no invitation of another organisation, and none that does not exist', async () => {
|
|
22
|
+
const { application, members, invitations, as } = arrangeIdentity();
|
|
23
|
+
|
|
24
|
+
await members.save(aMember().of('acme', 'subject-ada').as(MemberRole.Admin).build());
|
|
25
|
+
await invitations.save(anInvitation().identifiedBy('invitation-1').to('globex').build());
|
|
26
|
+
|
|
27
|
+
for (const invitationId of ['invitation-1', 'invitation-9']) {
|
|
28
|
+
await expect(
|
|
29
|
+
application.commandBus.execute(new RevokeInvitationCommand(as('subject-ada'), 'acme', invitationId)),
|
|
30
|
+
).rejects.toMatchObject({ code: 'INVITATION_NOT_FOUND' });
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('is refused to a member whose role does not invite', async () => {
|
|
35
|
+
const { application, members, invitations, as } = arrangeIdentity();
|
|
36
|
+
|
|
37
|
+
await members.save(aMember().of('acme', 'subject-alan').build());
|
|
38
|
+
await invitations.save(anInvitation().identifiedBy('invitation-1').to('acme').build());
|
|
39
|
+
|
|
40
|
+
await expect(
|
|
41
|
+
application.commandBus.execute(new RevokeInvitationCommand(as('subject-alan'), 'acme', 'invitation-1')),
|
|
42
|
+
).rejects.toMatchObject({ code: 'FORBIDDEN' });
|
|
43
|
+
});
|
|
44
|
+
});
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { describe, expect, it } from '{{tests.module}}';
|
|
2
|
+
import { RoleRightsService } from '{{alias.context}}/domain/services/role-rights.service{{alias.ext}}';
|
|
3
|
+
import { MemberRight } from '{{alias.context}}/domain/vocabularies/member-right.vocabulary{{alias.ext}}';
|
|
4
|
+
import { MemberRole } from '{{alias.context}}/domain/vocabularies/member-role.vocabulary{{alias.ext}}';
|
|
5
|
+
import { aDelegation } from '{{alias.context}}/test/builders/delegation.builder{{alias.ext}}';
|
|
6
|
+
import { aMember } from '{{alias.context}}/test/builders/member.builder{{alias.ext}}';
|
|
7
|
+
|
|
8
|
+
const now = new Date('2026-01-10T00:00:00.000Z');
|
|
9
|
+
|
|
10
|
+
describe('RoleRightsService', () => {
|
|
11
|
+
it('grants every right to an owner, all but delegation to an admin, the reading of members to a member', () => {
|
|
12
|
+
expect(new RoleRightsService().rightsOf(MemberRole.Owner)).toEqual(Object.values(MemberRight));
|
|
13
|
+
expect(new RoleRightsService().rightsOf(MemberRole.Admin)).not.toContain(MemberRight.GrantDelegations);
|
|
14
|
+
expect(new RoleRightsService().rightsOf(MemberRole.Member)).toEqual([MemberRight.ReadMembers]);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it('gives an account no right in an organisation it is not, or no longer, a member of', () => {
|
|
18
|
+
const removed = aMember().of('acme', 'account-ada').as(MemberRole.Owner).removedAt(now).build();
|
|
19
|
+
|
|
20
|
+
expect(
|
|
21
|
+
new RoleRightsService().rightsIn('acme', 'account-ada', { member: undefined, lent: [], now }).rights,
|
|
22
|
+
).toEqual([]);
|
|
23
|
+
|
|
24
|
+
expect(new RoleRightsService().rightsIn('acme', 'account-ada', { member: removed, lent: [], now }).rights).toEqual(
|
|
25
|
+
[],
|
|
26
|
+
);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('adds to the rights of the role those an active delegation lends, and nothing of one that ended', () => {
|
|
30
|
+
const member = aMember().of('acme', 'account-deputy').build();
|
|
31
|
+
const active = aDelegation().to('account-deputy').lending(MemberRight.InviteMembers).build();
|
|
32
|
+
|
|
33
|
+
const ended = aDelegation()
|
|
34
|
+
.to('account-deputy')
|
|
35
|
+
.lending(MemberRight.ManageMembers)
|
|
36
|
+
.between(new Date('2025-12-01T00:00:00.000Z'), new Date('2026-01-05T00:00:00.000Z'))
|
|
37
|
+
.build();
|
|
38
|
+
|
|
39
|
+
const rights = new RoleRightsService().rightsIn('acme', 'account-deputy', { member, lent: [active, ended], now });
|
|
40
|
+
|
|
41
|
+
expect(rights.rights).toEqual([MemberRight.InviteMembers, MemberRight.ReadMembers]);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('names the delegation a right is used under when the role does not grant it, and none otherwise', () => {
|
|
45
|
+
const deputy = aMember().of('acme', 'account-deputy').build();
|
|
46
|
+
const admin = aMember().of('acme', 'account-admin').as(MemberRole.Admin).build();
|
|
47
|
+
const lent = [aDelegation().identifiedBy('delegation-1').lending(MemberRight.InviteMembers).build()];
|
|
48
|
+
const rights = new RoleRightsService();
|
|
49
|
+
|
|
50
|
+
expect(rights.delegationUsedFor(MemberRight.InviteMembers, { member: deputy, lent, now })).toBe('delegation-1');
|
|
51
|
+
expect(rights.delegationUsedFor(MemberRight.InviteMembers, { member: admin, lent, now })).toBeUndefined();
|
|
52
|
+
expect(rights.delegationUsedFor(MemberRight.ManageMembers, { member: deputy, lent, now })).toBeUndefined();
|
|
53
|
+
});
|
|
54
|
+
});
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { describe, expect, it } from '{{tests.module}}';
|
|
2
|
+
import { controllerRoute, executeRoute } from '@ddd-ts-kit/http';
|
|
3
|
+
import { InMemoryQueryBus, pageOf, type Actor } from '@ddd-ts-kit/kernel';
|
|
4
|
+
import { SearchMembersQuery } from '{{alias.context}}/application/queries/search-members/search-members.query{{alias.ext}}';
|
|
5
|
+
import { MemberRole } from '{{alias.context}}/domain/vocabularies/member-role.vocabulary{{alias.ext}}';
|
|
6
|
+
import { SearchMembersController } from '{{alias.context}}/presentation/http/organisations/search-members/search-members.controller{{alias.ext}}';
|
|
7
|
+
|
|
8
|
+
const caller: Actor = { id: 'subject-ada', type: 'user', roles: [], attributes: {} };
|
|
9
|
+
|
|
10
|
+
const ada = {
|
|
11
|
+
memberId: 'member-1',
|
|
12
|
+
accountId: 'subject-ada',
|
|
13
|
+
displayName: 'Ada Lovelace',
|
|
14
|
+
email: 'ada@example.test',
|
|
15
|
+
role: MemberRole.Owner,
|
|
16
|
+
joinedAt: new Date('2026-01-01T00:00:00.000Z'),
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
describe('GET /organisations/:organisationId/members', () => {
|
|
20
|
+
it('answers 200 with a page of the members, each instant written as ISO 8601', async () => {
|
|
21
|
+
const asked: SearchMembersQuery[] = [];
|
|
22
|
+
|
|
23
|
+
const queries = new InMemoryQueryBus().register(SearchMembersQuery, {
|
|
24
|
+
execute: async (query) => {
|
|
25
|
+
asked.push(query);
|
|
26
|
+
|
|
27
|
+
return pageOf([ada], { total: 1 });
|
|
28
|
+
},
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
const route = controllerRoute(new SearchMembersController(queries));
|
|
32
|
+
|
|
33
|
+
const response = await executeRoute(
|
|
34
|
+
route,
|
|
35
|
+
{ params: { organisationId: 'acme' }, query: { limit: '10' }, headers: {}, body: {}, raw: undefined },
|
|
36
|
+
{ validateResponses: true, resolvePrincipal: () => caller },
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
expect(response.status).toBe(200);
|
|
40
|
+
expect(response.body).toMatchObject({ items: [{ ...ada, joinedAt: '2026-01-01T00:00:00.000Z' }], total: 1 });
|
|
41
|
+
expect(asked).toMatchObject([{ organisationId: 'acme', page: { kind: 'offset', limit: 10 } }]);
|
|
42
|
+
});
|
|
43
|
+
});
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { describe, expect, it } from '{{tests.module}}';
|
|
2
|
+
import { offsetPage } from '@ddd-ts-kit/kernel';
|
|
3
|
+
import { MemberRole } from '{{alias.context}}/domain/vocabularies/member-role.vocabulary{{alias.ext}}';
|
|
4
|
+
import { SearchMembersQuery } from '{{alias.context}}/application/queries/search-members/search-members.query{{alias.ext}}';
|
|
5
|
+
import { aMember } from '{{alias.context}}/test/builders/member.builder{{alias.ext}}';
|
|
6
|
+
import { arrangeIdentity } from '{{alias.context}}/test/unit/identity-application{{alias.ext}}';
|
|
7
|
+
|
|
8
|
+
const ada = {
|
|
9
|
+
memberId: 'member-1',
|
|
10
|
+
accountId: 'subject-ada',
|
|
11
|
+
displayName: 'Ada Lovelace',
|
|
12
|
+
email: 'ada@example.test',
|
|
13
|
+
role: MemberRole.Owner,
|
|
14
|
+
joinedAt: new Date('2026-01-01T00:00:00.000Z'),
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
describe('Search members', () => {
|
|
18
|
+
it('answers a page of the members of the organisation to one of its members', async () => {
|
|
19
|
+
const { application, members, reader, as } = arrangeIdentity();
|
|
20
|
+
|
|
21
|
+
await members.save(aMember().of('acme', 'subject-ada').as(MemberRole.Owner).build());
|
|
22
|
+
reader.given('acme', ada);
|
|
23
|
+
|
|
24
|
+
const page = await application.queryBus.execute(new SearchMembersQuery(as('subject-ada'), 'acme', offsetPage({ limit: 20 })));
|
|
25
|
+
|
|
26
|
+
expect(page.items).toEqual([ada]);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('answers nothing to a person who is not a member of the organisation', async () => {
|
|
30
|
+
const { application, reader, as } = arrangeIdentity();
|
|
31
|
+
|
|
32
|
+
reader.given('acme', ada);
|
|
33
|
+
|
|
34
|
+
await expect(
|
|
35
|
+
application.queryBus.execute(new SearchMembersQuery(as('subject-mallory'), 'acme', offsetPage({ limit: 20 }))),
|
|
36
|
+
).rejects.toMatchObject({ code: 'FORBIDDEN' });
|
|
37
|
+
});
|
|
38
|
+
});
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# ADR-{{adr.number}}: {{context.kebab}} is the control plane of the tenants
|
|
2
|
+
|
|
3
|
+
- Status: accepted
|
|
4
|
+
- Date: {{adr.date}}
|
|
5
|
+
|
|
6
|
+
## Context
|
|
7
|
+
|
|
8
|
+
The project keeps the data of each organisation apart (`"stack.tenancy"`): every statement of a
|
|
9
|
+
repository is confined to the tenant of the work, and `tenancy/scoped-statement` reports one that
|
|
10
|
+
is not. The organisations of `{{context.kebab}}` are those tenants, and this context decides who
|
|
11
|
+
belongs to which: a person signs in once and lists their organisations, an invitation is answered
|
|
12
|
+
by a person who belongs to none yet, a membership is written for an organisation that is not the
|
|
13
|
+
one the request runs in.
|
|
14
|
+
|
|
15
|
+
## Decision
|
|
16
|
+
|
|
17
|
+
The tables of `{{context.kebab}}` (accounts, organisations, members, invitations, delegations)
|
|
18
|
+
belong to no tenant: they hold no tenant column, and each statement of their adapters carries a
|
|
19
|
+
suppression of `tenancy/scoped-statement` that names this record. What one organisation may read
|
|
20
|
+
of another is decided per use case instead: every use case of an organisation authorizes the
|
|
21
|
+
actor on the rights of their membership in it (the policies of
|
|
22
|
+
`application/authorization/`), and every read names that organisation.
|
|
23
|
+
|
|
24
|
+
## Consequences
|
|
25
|
+
|
|
26
|
+
- Easier: a person, their memberships and their invitations are one model, whatever the tenant
|
|
27
|
+
of the request; the provider is told of every change, so that tokens name the organisations.
|
|
28
|
+
- Harder: isolation between the organisations of this context rests on its policies and on the
|
|
29
|
+
organisation each statement names, not on the storage. A new statement names its organisation.
|
|
30
|
+
- The integration events of an organisation carry it as their tenant, so that the consumers of the
|
|
31
|
+
other contexts run in it.
|
|
32
|
+
|
|
33
|
+
## Alternatives considered
|
|
34
|
+
|
|
35
|
+
- **A tenant column on these tables.** Refused: the membership of a person in an organisation is
|
|
36
|
+
written by work that does not run in that organisation (opening it, accepting an invitation),
|
|
37
|
+
and an elevation widens reads only, never writes.
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
## Design
|
|
2
|
+
|
|
3
|
+
Written by the identity preset of ddd-ts-kit (`--preset identity`); from now on it is the code of
|
|
4
|
+
this project, to change like any other.
|
|
5
|
+
|
|
6
|
+
**The identity provider authenticates, this context authorizes.** An account is a person under
|
|
7
|
+
the subject their provider gives them (`actor.id`): it is provisioned at their first sign-in from
|
|
8
|
+
what their token says (`PUT /accounts/me`) and refreshed at each later one. No password, no secret
|
|
9
|
+
of the provider is stored here. The email address of an account is always the one of the token,
|
|
10
|
+
never one a client sends, and one the provider verified (`email_verified`): an invitation is
|
|
11
|
+
matched on it. A deactivated account leaves every organisation and ends every delegation in the
|
|
12
|
+
same unit of work, so that it holds no right anywhere while its last token lives; the last owner
|
|
13
|
+
of an organisation appoints another owner before deactivating.
|
|
14
|
+
|
|
15
|
+
**Organisations, members and roles.** An organisation is opened under its slug, which is its
|
|
16
|
+
identity for good, and its opener is its first owner. A member holds one role, `owner`, `admin`
|
|
17
|
+
or `member`; what a role grants is one table, `RoleRightsService`. Nobody changes their own role,
|
|
18
|
+
and nobody appoints, invites, changes or removes anyone above their own role (only an owner makes
|
|
19
|
+
an owner), whatever a delegation lends them. An organisation keeps one owner at least: every
|
|
20
|
+
change of a member saves the organisation with it, so that its version orders the changes of its
|
|
21
|
+
owners and two owners who remove each other at once cannot both commit.
|
|
22
|
+
|
|
23
|
+
**Invitations.** An invitation is issued to an email address, in a role, for a week. Its token
|
|
24
|
+
is answered to the inviter once (`POST /organisations/:organisationId/invitations`) and only its
|
|
25
|
+
SHA-256 digest is stored: send it to the person (a notification context, a link), who accepts or
|
|
26
|
+
declines it signed in, with the address it was sent to.
|
|
27
|
+
|
|
28
|
+
**Delegations.** A member lends some of the rights of their role to another member of the
|
|
29
|
+
organisation, for ninety days at most: the delegate acts in their own name, with those rights, and
|
|
30
|
+
the audit trail of the project records every use case either way; the facts a change leads to
|
|
31
|
+
name the delegation it was decided under (`delegationId`). A delegation lends nothing beyond the
|
|
32
|
+
role of its delegator, never what was lent to them, never the power to act above the delegate's
|
|
33
|
+
own role, and it ends when one of its two members leaves or when its delegator changes role.
|
|
34
|
+
|
|
35
|
+
**Who may do what.** Every use case requires a permission of `{{context.camel}}Permissions`, which
|
|
36
|
+
the platform grants to every signed-in person whatever its access model (roles, attribute
|
|
37
|
+
policies, relationships, OpenFGA or an adapter of the project): the grant `'{{context.kebab}}:*'`
|
|
38
|
+
of `signed-in-grants.ts`, which `createAuthorizer` answers before it asks the model
|
|
39
|
+
(`SignedInGrantsAuthorizer` of `@ddd-ts-kit/auth`). Systems, services and anonymous callers get
|
|
40
|
+
nothing from it.
|
|
41
|
+
|
|
42
|
+
Inside an organisation, the policies of `application/authorization/` decide on the rights the
|
|
43
|
+
actor holds there (`OrganisationRights`): their role, and the rights an active delegation lends
|
|
44
|
+
them. The integration events (`member.joined`, `member.role-changed`, `member.removed`,
|
|
45
|
+
`delegation.granted`, `delegation.revoked`) let another context write the relationships of its
|
|
46
|
+
own model, or the tuples of OpenFGA, from these facts.
|
|
47
|
+
{{#if tenancy.any}}
|
|
48
|
+
|
|
49
|
+
**Tenants.** Each organisation is a tenant: its slug is the tenant id, the integration events of
|
|
50
|
+
an organisation carry it as their tenant, and the consumers of the other contexts run in it. The
|
|
51
|
+
tables of this context belong to no tenant, since it decides who belongs to which ({{preset.adr}}).
|
|
52
|
+
The token names the organisations of the person: with Keycloak, the `organization` claim, which
|
|
53
|
+
`createAuthenticator` reads (`tenantClaim: 'organization'`). One organisation is the tenant of
|
|
54
|
+
every request; several are the `tenants` of the actor, and a request names the one it works in
|
|
55
|
+
(`x-tenant-id`), refused if the person is not a member of it.
|
|
56
|
+
{{/if}}
|
|
57
|
+
|
|
58
|
+
**The identity provider follows, at least once.** The consumers of `application/integration/handlers/`
|
|
59
|
+
read the facts the context publishes through its outbox and tell the provider
|
|
60
|
+
(`IdentityProviderAdmin`): it knows the organisation under its slug, adds or removes its members,
|
|
61
|
+
and disables a deactivated account. A provider that is down is asked again by whoever delivers the
|
|
62
|
+
fact (the outbox relay, the broker) until it answers, so a removal or a deactivation is never
|
|
63
|
+
lost; each consumer brings the provider in line with what the context holds now, so that a late
|
|
64
|
+
retry never gives a removed person their organisation back. `KeycloakIdentityProviderAdmin` is a
|
|
65
|
+
recipe for the administration API of Keycloak 26 or later (organizations enabled in the realm), as
|
|
66
|
+
a confidential client holding `manage-users` and `manage-realm` of `realm-management`; it reads
|
|
67
|
+
`KEYCLOAK_URL`, `KEYCLOAK_REALM`, `IDENTITY_PROVIDER_ADMIN_CLIENT_ID` and
|
|
68
|
+
`IDENTITY_PROVIDER_ADMIN_CLIENT_SECRET` when a call is made. Its spec runs it against a local
|
|
69
|
+
server that answers as Keycloak does: run it once against your realm before relying on it. Another
|
|
70
|
+
provider is another adapter of the same port.
|
|
71
|
+
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { Grant } from '@ddd-ts-kit/auth';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* What every signed-in person may do, whatever the access model of the platform decides for the
|
|
5
|
+
* rest: the permissions of a context that serves each person for themselves, such as their
|
|
6
|
+
* account and the organisations they belong to, whose own policies decide on the element.
|
|
7
|
+
* `createAuthorizer` grants them before it asks the model.
|
|
8
|
+
*/
|
|
9
|
+
export const signedInGrants: readonly Grant[] = [
|
|
10
|
+
// ddd-ts-kit:signed-in-grants
|
|
11
|
+
// /ddd-ts-kit:signed-in-grants
|
|
12
|
+
];
|