@ddd-ts-kit/di 0.0.0-stage → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1075 -2
- package/dist/application/application-options.d.ts +19 -0
- package/dist/application/application-options.d.ts.map +1 -0
- package/dist/application/application-options.js +2 -0
- package/dist/application/application-options.js.map +1 -0
- package/dist/application/application.d.ts +22 -0
- package/dist/application/application.d.ts.map +1 -0
- package/dist/application/application.js +2 -0
- package/dist/application/application.js.map +1 -0
- package/dist/application/assemble.d.ts +6 -0
- package/dist/application/assemble.d.ts.map +1 -0
- package/dist/application/assemble.js +16 -0
- package/dist/application/assemble.js.map +1 -0
- package/dist/application/binding-info.d.ts +10 -0
- package/dist/application/binding-info.d.ts.map +1 -0
- package/dist/application/binding-info.js +2 -0
- package/dist/application/binding-info.js.map +1 -0
- package/dist/application/bound-ports.d.ts +8 -0
- package/dist/application/bound-ports.d.ts.map +1 -0
- package/dist/application/bound-ports.js +2 -0
- package/dist/application/bound-ports.js.map +1 -0
- package/dist/application/consumer-subscriptions.d.ts +21 -0
- package/dist/application/consumer-subscriptions.d.ts.map +1 -0
- package/dist/application/consumer-subscriptions.js +59 -0
- package/dist/application/consumer-subscriptions.js.map +1 -0
- package/dist/application/container-application.d.ts +44 -0
- package/dist/application/container-application.d.ts.map +1 -0
- package/dist/application/container-application.js +224 -0
- package/dist/application/container-application.js.map +1 -0
- package/dist/application/create-application.d.ts +10 -0
- package/dist/application/create-application.d.ts.map +1 -0
- package/dist/application/create-application.js +12 -0
- package/dist/application/create-application.js.map +1 -0
- package/dist/application/decorate.d.ts +8 -0
- package/dist/application/decorate.d.ts.map +1 -0
- package/dist/application/decorate.js +2 -0
- package/dist/application/decorate.js.map +1 -0
- package/dist/application/delegating-applications.d.ts +7 -0
- package/dist/application/delegating-applications.d.ts.map +1 -0
- package/dist/application/delegating-applications.js +6 -0
- package/dist/application/delegating-applications.js.map +1 -0
- package/dist/application/failures-of.d.ts +3 -0
- package/dist/application/failures-of.d.ts.map +1 -0
- package/dist/application/failures-of.js +13 -0
- package/dist/application/failures-of.js.map +1 -0
- package/dist/application/is-handler-registry.d.ts +9 -0
- package/dist/application/is-handler-registry.d.ts.map +1 -0
- package/dist/application/is-handler-registry.js +5 -0
- package/dist/application/is-handler-registry.js.map +1 -0
- package/dist/application/job-lifecycle.d.ts +16 -0
- package/dist/application/job-lifecycle.d.ts.map +1 -0
- package/dist/application/job-lifecycle.js +48 -0
- package/dist/application/job-lifecycle.js.map +1 -0
- package/dist/application/override-for-tests.d.ts +8 -0
- package/dist/application/override-for-tests.d.ts.map +1 -0
- package/dist/application/override-for-tests.js +14 -0
- package/dist/application/override-for-tests.js.map +1 -0
- package/dist/application/override.d.ts +5 -0
- package/dist/application/override.d.ts.map +1 -0
- package/dist/application/override.js +5 -0
- package/dist/application/override.js.map +1 -0
- package/dist/application/with-binding-aspects.d.ts +8 -0
- package/dist/application/with-binding-aspects.d.ts.map +1 -0
- package/dist/application/with-binding-aspects.js +26 -0
- package/dist/application/with-binding-aspects.js.map +1 -0
- package/dist/authorization/assert-authorization-declared.d.ts +5 -0
- package/dist/authorization/assert-authorization-declared.d.ts.map +1 -0
- package/dist/authorization/assert-authorization-declared.js +28 -0
- package/dist/authorization/assert-authorization-declared.js.map +1 -0
- package/dist/authorization/authorization-mode.d.ts +3 -0
- package/dist/authorization/authorization-mode.d.ts.map +1 -0
- package/dist/authorization/authorization-mode.js +2 -0
- package/dist/authorization/authorization-mode.js.map +1 -0
- package/dist/authorization/declarations-of.d.ts +4 -0
- package/dist/authorization/declarations-of.d.ts.map +1 -0
- package/dist/authorization/declarations-of.js +4 -0
- package/dist/authorization/declarations-of.js.map +1 -0
- package/dist/authorization/declared-policies.d.ts +10 -0
- package/dist/authorization/declared-policies.d.ts.map +1 -0
- package/dist/authorization/declared-policies.js +8 -0
- package/dist/authorization/declared-policies.js.map +1 -0
- package/dist/authorization/declared-requirement.d.ts +5 -0
- package/dist/authorization/declared-requirement.d.ts.map +1 -0
- package/dist/authorization/declared-requirement.js +16 -0
- package/dist/authorization/declared-requirement.js.map +1 -0
- package/dist/authorization/pass-every-message.d.ts +3 -0
- package/dist/authorization/pass-every-message.d.ts.map +1 -0
- package/dist/authorization/pass-every-message.js +2 -0
- package/dist/authorization/pass-every-message.js.map +1 -0
- package/dist/authorization/protected-requirement.d.ts +6 -0
- package/dist/authorization/protected-requirement.d.ts.map +1 -0
- package/dist/authorization/protected-requirement.js +9 -0
- package/dist/authorization/protected-requirement.js.map +1 -0
- package/dist/authorization/requirement-options.d.ts +8 -0
- package/dist/authorization/requirement-options.d.ts.map +1 -0
- package/dist/authorization/requirement-options.js +2 -0
- package/dist/authorization/requirement-options.js.map +1 -0
- package/dist/authorization/requirement.d.ts +5 -0
- package/dist/authorization/requirement.d.ts.map +1 -0
- package/dist/authorization/requirement.js +2 -0
- package/dist/authorization/requirement.js.map +1 -0
- package/dist/authorization/use-case-access.d.ts +2 -0
- package/dist/authorization/use-case-access.d.ts.map +1 -0
- package/dist/authorization/use-case-access.js +2 -0
- package/dist/authorization/use-case-access.js.map +1 -0
- package/dist/authorization/use-case-declaration.d.ts +14 -0
- package/dist/authorization/use-case-declaration.d.ts.map +1 -0
- package/dist/authorization/use-case-declaration.js +2 -0
- package/dist/authorization/use-case-declaration.js.map +1 -0
- package/dist/authorization/use-case-declarations.d.ts +7 -0
- package/dist/authorization/use-case-declarations.d.ts.map +1 -0
- package/dist/authorization/use-case-declarations.js +5 -0
- package/dist/authorization/use-case-declarations.js.map +1 -0
- package/dist/authorization/use-case-guard-middleware.d.ts +5 -0
- package/dist/authorization/use-case-guard-middleware.d.ts.map +1 -0
- package/dist/authorization/use-case-guard-middleware.js +6 -0
- package/dist/authorization/use-case-guard-middleware.js.map +1 -0
- package/dist/authorization/use-case-guard.d.ts +10 -0
- package/dist/authorization/use-case-guard.d.ts.map +1 -0
- package/dist/authorization/use-case-guard.js +8 -0
- package/dist/authorization/use-case-guard.js.map +1 -0
- package/dist/checks/assert-sound.d.ts +5 -0
- package/dist/checks/assert-sound.d.ts.map +1 -0
- package/dist/checks/assert-sound.js +74 -0
- package/dist/checks/assert-sound.js.map +1 -0
- package/dist/checks/cycle-error.d.ts +4 -0
- package/dist/checks/cycle-error.d.ts.map +1 -0
- package/dist/checks/cycle-error.js +6 -0
- package/dist/checks/cycle-error.js.map +1 -0
- package/dist/consumer/consumer-options.d.ts +14 -0
- package/dist/consumer/consumer-options.d.ts.map +1 -0
- package/dist/consumer/consumer-options.js +2 -0
- package/dist/consumer/consumer-options.js.map +1 -0
- package/dist/consumer/consumer-runtime.d.ts +9 -0
- package/dist/consumer/consumer-runtime.d.ts.map +1 -0
- package/dist/consumer/consumer-runtime.js +2 -0
- package/dist/consumer/consumer-runtime.js.map +1 -0
- package/dist/consumer/observed-projection.d.ts +19 -0
- package/dist/consumer/observed-projection.d.ts.map +1 -0
- package/dist/consumer/observed-projection.js +44 -0
- package/dist/consumer/observed-projection.js.map +1 -0
- package/dist/consumer/receive.d.ts +21 -0
- package/dist/consumer/receive.d.ts.map +1 -0
- package/dist/consumer/receive.js +40 -0
- package/dist/consumer/receive.js.map +1 -0
- package/dist/consumer/unreadable-event-error.d.ts +9 -0
- package/dist/consumer/unreadable-event-error.d.ts.map +1 -0
- package/dist/consumer/unreadable-event-error.js +15 -0
- package/dist/consumer/unreadable-event-error.js.map +1 -0
- package/dist/declared/apply-through.d.ts +3 -0
- package/dist/declared/apply-through.d.ts.map +1 -0
- package/dist/declared/apply-through.js +11 -0
- package/dist/declared/apply-through.js.map +1 -0
- package/dist/declared/consumption-of.d.ts +15 -0
- package/dist/declared/consumption-of.d.ts.map +1 -0
- package/dist/declared/consumption-of.js +15 -0
- package/dist/declared/consumption-of.js.map +1 -0
- package/dist/declared/context-module.d.ts +10 -0
- package/dist/declared/context-module.d.ts.map +1 -0
- package/dist/declared/context-module.js +16 -0
- package/dist/declared/context-module.js.map +1 -0
- package/dist/declared/declared-source.d.ts +7 -0
- package/dist/declared/declared-source.d.ts.map +1 -0
- package/dist/declared/declared-source.js +2 -0
- package/dist/declared/declared-source.js.map +1 -0
- package/dist/declared/discovered-files.d.ts +6 -0
- package/dist/declared/discovered-files.d.ts.map +1 -0
- package/dist/declared/discovered-files.js +10 -0
- package/dist/declared/discovered-files.js.map +1 -0
- package/dist/declared/include-declared.d.ts +10 -0
- package/dist/declared/include-declared.d.ts.map +1 -0
- package/dist/declared/include-declared.js +173 -0
- package/dist/declared/include-declared.js.map +1 -0
- package/dist/declared/projection-of.d.ts +24 -0
- package/dist/declared/projection-of.d.ts.map +1 -0
- package/dist/declared/projection-of.js +73 -0
- package/dist/declared/projection-of.js.map +1 -0
- package/dist/description/describe-wiring.d.ts +6 -0
- package/dist/description/describe-wiring.d.ts.map +1 -0
- package/dist/description/describe-wiring.js +77 -0
- package/dist/description/describe-wiring.js.map +1 -0
- package/dist/description/wiring-description.d.ts +98 -0
- package/dist/description/wiring-description.d.ts.map +1 -0
- package/dist/description/wiring-description.js +2 -0
- package/dist/description/wiring-description.js.map +1 -0
- package/dist/discovery/discover-context.d.ts +27 -0
- package/dist/discovery/discover-context.d.ts.map +1 -0
- package/dist/discovery/discover-context.js +69 -0
- package/dist/discovery/discover-context.js.map +1 -0
- package/dist/discovery/role-files-of.d.ts +16 -0
- package/dist/discovery/role-files-of.d.ts.map +1 -0
- package/dist/discovery/role-files-of.js +70 -0
- package/dist/discovery/role-files-of.js.map +1 -0
- package/dist/discovery/role-rule.d.ts +10 -0
- package/dist/discovery/role-rule.d.ts.map +1 -0
- package/dist/discovery/role-rule.js +23 -0
- package/dist/discovery/role-rule.js.map +1 -0
- package/dist/discovery.d.ts +2 -0
- package/dist/discovery.d.ts.map +1 -0
- package/dist/discovery.js +2 -0
- package/dist/discovery.js.map +1 -0
- package/dist/errors/wiring-error.d.ts +7 -0
- package/dist/errors/wiring-error.d.ts.map +1 -0
- package/dist/errors/wiring-error.js +10 -0
- package/dist/errors/wiring-error.js.map +1 -0
- package/dist/index.d.ts +52 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +20 -0
- package/dist/index.js.map +1 -0
- package/dist/jobs/definition-of.d.ts +7 -0
- package/dist/jobs/definition-of.d.ts.map +1 -0
- package/dist/jobs/definition-of.js +11 -0
- package/dist/jobs/definition-of.js.map +1 -0
- package/dist/jobs/effective-job.d.ts +7 -0
- package/dist/jobs/effective-job.d.ts.map +1 -0
- package/dist/jobs/effective-job.js +2 -0
- package/dist/jobs/effective-job.js.map +1 -0
- package/dist/jobs/effective-jobs.d.ts +6 -0
- package/dist/jobs/effective-jobs.d.ts.map +1 -0
- package/dist/jobs/effective-jobs.js +33 -0
- package/dist/jobs/effective-jobs.js.map +1 -0
- package/dist/jobs/jobs-options.d.ts +16 -0
- package/dist/jobs/jobs-options.d.ts.map +1 -0
- package/dist/jobs/jobs-options.js +2 -0
- package/dist/jobs/jobs-options.js.map +1 -0
- package/dist/jobs/readable.d.ts +3 -0
- package/dist/jobs/readable.d.ts.map +1 -0
- package/dist/jobs/readable.js +15 -0
- package/dist/jobs/readable.js.map +1 -0
- package/dist/jobs/written-job.d.ts +11 -0
- package/dist/jobs/written-job.d.ts.map +1 -0
- package/dist/jobs/written-job.js +8 -0
- package/dist/jobs/written-job.js.map +1 -0
- package/dist/kernel/guarded-query-bus.d.ts +5 -0
- package/dist/kernel/guarded-query-bus.d.ts.map +1 -0
- package/dist/kernel/guarded-query-bus.js +9 -0
- package/dist/kernel/guarded-query-bus.js.map +1 -0
- package/dist/kernel/instrumentation.d.ts +37 -0
- package/dist/kernel/instrumentation.d.ts.map +1 -0
- package/dist/kernel/instrumentation.js +9 -0
- package/dist/kernel/instrumentation.js.map +1 -0
- package/dist/kernel/kernel-module-options.d.ts +57 -0
- package/dist/kernel/kernel-module-options.d.ts.map +1 -0
- package/dist/kernel/kernel-module-options.js +2 -0
- package/dist/kernel/kernel-module-options.js.map +1 -0
- package/dist/kernel/kernel-module.d.ts +14 -0
- package/dist/kernel/kernel-module.d.ts.map +1 -0
- package/dist/kernel/kernel-module.js +133 -0
- package/dist/kernel/kernel-module.js.map +1 -0
- package/dist/kernel/kernel-ports.d.ts +4 -0
- package/dist/kernel/kernel-ports.d.ts.map +1 -0
- package/dist/kernel/kernel-ports.js +25 -0
- package/dist/kernel/kernel-ports.js.map +1 -0
- package/dist/kernel/lazy-error-reporter.d.ts +8 -0
- package/dist/kernel/lazy-error-reporter.d.ts.map +1 -0
- package/dist/kernel/lazy-error-reporter.js +14 -0
- package/dist/kernel/lazy-error-reporter.js.map +1 -0
- package/dist/kernel/provided.d.ts +4 -0
- package/dist/kernel/provided.d.ts.map +1 -0
- package/dist/kernel/provided.js +2 -0
- package/dist/kernel/provided.js.map +1 -0
- package/dist/kernel/reported-failures.d.ts +5 -0
- package/dist/kernel/reported-failures.d.ts.map +1 -0
- package/dist/kernel/reported-failures.js +9 -0
- package/dist/kernel/reported-failures.js.map +1 -0
- package/dist/kernel/transactional-command-bus.d.ts +10 -0
- package/dist/kernel/transactional-command-bus.d.ts.map +1 -0
- package/dist/kernel/transactional-command-bus.js +17 -0
- package/dist/kernel/transactional-command-bus.js.map +1 -0
- package/dist/module/binding-options-of.d.ts +5 -0
- package/dist/module/binding-options-of.d.ts.map +1 -0
- package/dist/module/binding-options-of.js +23 -0
- package/dist/module/binding-options-of.js.map +1 -0
- package/dist/module/binding-options.d.ts +8 -0
- package/dist/module/binding-options.d.ts.map +1 -0
- package/dist/module/binding-options.js +2 -0
- package/dist/module/binding-options.js.map +1 -0
- package/dist/module/binding-target.d.ts +15 -0
- package/dist/module/binding-target.d.ts.map +1 -0
- package/dist/module/binding-target.js +2 -0
- package/dist/module/binding-target.js.map +1 -0
- package/dist/module/collect-wiring.d.ts +5 -0
- package/dist/module/collect-wiring.d.ts.map +1 -0
- package/dist/module/collect-wiring.js +13 -0
- package/dist/module/collect-wiring.js.map +1 -0
- package/dist/module/context-module.d.ts +10 -0
- package/dist/module/context-module.d.ts.map +1 -0
- package/dist/module/context-module.js +2 -0
- package/dist/module/context-module.js.map +1 -0
- package/dist/module/define-context-module.d.ts +4 -0
- package/dist/module/define-context-module.d.ts.map +1 -0
- package/dist/module/define-context-module.js +5 -0
- package/dist/module/define-context-module.js.map +1 -0
- package/dist/module/factory-made-handlers.d.ts +6 -0
- package/dist/module/factory-made-handlers.d.ts.map +1 -0
- package/dist/module/factory-made-handlers.js +24 -0
- package/dist/module/factory-made-handlers.js.map +1 -0
- package/dist/module/handler-target.d.ts +11 -0
- package/dist/module/handler-target.d.ts.map +1 -0
- package/dist/module/handler-target.js +2 -0
- package/dist/module/handler-target.js.map +1 -0
- package/dist/module/module-builder.d.ts +50 -0
- package/dist/module/module-builder.d.ts.map +1 -0
- package/dist/module/module-builder.js +2 -0
- package/dist/module/module-builder.js.map +1 -0
- package/dist/module/module-context.d.ts +5 -0
- package/dist/module/module-context.d.ts.map +1 -0
- package/dist/module/module-context.js +2 -0
- package/dist/module/module-context.js.map +1 -0
- package/dist/module/module-recorder.d.ts +31 -0
- package/dist/module/module-recorder.d.ts.map +1 -0
- package/dist/module/module-recorder.js +133 -0
- package/dist/module/module-recorder.js.map +1 -0
- package/dist/module/protected-use-case-target.d.ts +14 -0
- package/dist/module/protected-use-case-target.d.ts.map +1 -0
- package/dist/module/protected-use-case-target.js +2 -0
- package/dist/module/protected-use-case-target.js.map +1 -0
- package/dist/module/use-case-target.d.ts +8 -0
- package/dist/module/use-case-target.d.ts.map +1 -0
- package/dist/module/use-case-target.js +2 -0
- package/dist/module/use-case-target.js.map +1 -0
- package/dist/module/value-binding-options.d.ts +13 -0
- package/dist/module/value-binding-options.d.ts.map +1 -0
- package/dist/module/value-binding-options.js +2 -0
- package/dist/module/value-binding-options.js.map +1 -0
- package/dist/testing/create-test-application.d.ts +12 -0
- package/dist/testing/create-test-application.d.ts.map +1 -0
- package/dist/testing/create-test-application.js +89 -0
- package/dist/testing/create-test-application.js.map +1 -0
- package/dist/testing/create-test-kernel.d.ts +6 -0
- package/dist/testing/create-test-kernel.d.ts.map +1 -0
- package/dist/testing/create-test-kernel.js +65 -0
- package/dist/testing/create-test-kernel.js.map +1 -0
- package/dist/testing/fake.d.ts +5 -0
- package/dist/testing/fake.d.ts.map +1 -0
- package/dist/testing/fake.js +6 -0
- package/dist/testing/fake.js.map +1 -0
- package/dist/testing/test-application-of.d.ts +10 -0
- package/dist/testing/test-application-of.d.ts.map +1 -0
- package/dist/testing/test-application-of.js +49 -0
- package/dist/testing/test-application-of.js.map +1 -0
- package/dist/testing/test-application-options.d.ts +35 -0
- package/dist/testing/test-application-options.d.ts.map +1 -0
- package/dist/testing/test-application-options.js +2 -0
- package/dist/testing/test-application-options.js.map +1 -0
- package/dist/testing/test-application.d.ts +15 -0
- package/dist/testing/test-application.d.ts.map +1 -0
- package/dist/testing/test-application.js +2 -0
- package/dist/testing/test-application.js.map +1 -0
- package/dist/testing/test-jobs.d.ts +16 -0
- package/dist/testing/test-jobs.d.ts.map +1 -0
- package/dist/testing/test-jobs.js +2 -0
- package/dist/testing/test-jobs.js.map +1 -0
- package/dist/testing/test-kernel.d.ts +39 -0
- package/dist/testing/test-kernel.d.ts.map +1 -0
- package/dist/testing/test-kernel.js +2 -0
- package/dist/testing/test-kernel.js.map +1 -0
- package/dist/token/same-token.d.ts +3 -0
- package/dist/token/same-token.d.ts.map +1 -0
- package/dist/token/same-token.js +5 -0
- package/dist/token/same-token.js.map +1 -0
- package/dist/token/token-map.d.ts +14 -0
- package/dist/token/token-map.d.ts.map +1 -0
- package/dist/token/token-map.js +24 -0
- package/dist/token/token-map.js.map +1 -0
- package/dist/token/token-set.d.ts +9 -0
- package/dist/token/token-set.d.ts.map +1 -0
- package/dist/token/token-set.js +19 -0
- package/dist/token/token-set.js.map +1 -0
- package/dist/wiring/any-message-handler.d.ts +5 -0
- package/dist/wiring/any-message-handler.d.ts.map +1 -0
- package/dist/wiring/any-message-handler.js +2 -0
- package/dist/wiring/any-message-handler.js.map +1 -0
- package/dist/wiring/assert-port.d.ts +4 -0
- package/dist/wiring/assert-port.d.ts.map +1 -0
- package/dist/wiring/assert-port.js +10 -0
- package/dist/wiring/assert-port.js.map +1 -0
- package/dist/wiring/binding-source.d.ts +2 -0
- package/dist/wiring/binding-source.d.ts.map +1 -0
- package/dist/wiring/binding-source.js +2 -0
- package/dist/wiring/binding-source.js.map +1 -0
- package/dist/wiring/binding.d.ts +22 -0
- package/dist/wiring/binding.d.ts.map +1 -0
- package/dist/wiring/binding.js +2 -0
- package/dist/wiring/binding.js.map +1 -0
- package/dist/wiring/declarer.d.ts +6 -0
- package/dist/wiring/declarer.d.ts.map +1 -0
- package/dist/wiring/declarer.js +2 -0
- package/dist/wiring/declarer.js.map +1 -0
- package/dist/wiring/domain-event-handling.d.ts +20 -0
- package/dist/wiring/domain-event-handling.d.ts.map +1 -0
- package/dist/wiring/domain-event-handling.js +2 -0
- package/dist/wiring/domain-event-handling.js.map +1 -0
- package/dist/wiring/event-consumption.d.ts +17 -0
- package/dist/wiring/event-consumption.d.ts.map +1 -0
- package/dist/wiring/event-consumption.js +2 -0
- package/dist/wiring/event-consumption.js.map +1 -0
- package/dist/wiring/job-registration.d.ts +14 -0
- package/dist/wiring/job-registration.d.ts.map +1 -0
- package/dist/wiring/job-registration.js +2 -0
- package/dist/wiring/job-registration.js.map +1 -0
- package/dist/wiring/kind-aspect.d.ts +11 -0
- package/dist/wiring/kind-aspect.d.ts.map +1 -0
- package/dist/wiring/kind-aspect.js +2 -0
- package/dist/wiring/kind-aspect.js.map +1 -0
- package/dist/wiring/lifetime.d.ts +2 -0
- package/dist/wiring/lifetime.d.ts.map +1 -0
- package/dist/wiring/lifetime.js +2 -0
- package/dist/wiring/lifetime.js.map +1 -0
- package/dist/wiring/message-handling.d.ts +15 -0
- package/dist/wiring/message-handling.d.ts.map +1 -0
- package/dist/wiring/message-handling.js +2 -0
- package/dist/wiring/message-handling.js.map +1 -0
- package/dist/wiring/natural-kind.d.ts +9 -0
- package/dist/wiring/natural-kind.d.ts.map +1 -0
- package/dist/wiring/natural-kind.js +11 -0
- package/dist/wiring/natural-kind.js.map +1 -0
- package/dist/wiring/policy-binding.d.ts +11 -0
- package/dist/wiring/policy-binding.d.ts.map +1 -0
- package/dist/wiring/policy-binding.js +2 -0
- package/dist/wiring/policy-binding.js.map +1 -0
- package/dist/wiring/projection-registration.d.ts +22 -0
- package/dist/wiring/projection-registration.d.ts.map +1 -0
- package/dist/wiring/projection-registration.js +2 -0
- package/dist/wiring/projection-registration.js.map +1 -0
- package/dist/wiring/recipe.d.ts +9 -0
- package/dist/wiring/recipe.d.ts.map +1 -0
- package/dist/wiring/recipe.js +15 -0
- package/dist/wiring/recipe.js.map +1 -0
- package/dist/wiring/resolve.d.ts +3 -0
- package/dist/wiring/resolve.d.ts.map +1 -0
- package/dist/wiring/resolve.js +2 -0
- package/dist/wiring/resolve.js.map +1 -0
- package/dist/wiring/where-of.d.ts +9 -0
- package/dist/wiring/where-of.d.ts.map +1 -0
- package/dist/wiring/where-of.js +9 -0
- package/dist/wiring/where-of.js.map +1 -0
- package/dist/wiring/wiring-draft.d.ts +31 -0
- package/dist/wiring/wiring-draft.d.ts.map +1 -0
- package/dist/wiring/wiring-draft.js +83 -0
- package/dist/wiring/wiring-draft.js.map +1 -0
- package/dist/wiring/wiring.d.ts +23 -0
- package/dist/wiring/wiring.d.ts.map +1 -0
- package/dist/wiring/wiring.js +2 -0
- package/dist/wiring/wiring.js.map +1 -0
- package/package.json +42 -4
package/README.md
CHANGED
|
@@ -1,3 +1,1076 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @ddd-ts-kit/di
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Context modules on top of [tsyringe](https://github.com/microsoft/tsyringe), for projects that
|
|
4
|
+
want dependency injection without a heavy framework.
|
|
5
|
+
|
|
6
|
+
A port is an abstract class, and the class is what you inject by. A class says what it is, and
|
|
7
|
+
the context is assembled from what its classes say: there is no file listing bindings to keep in
|
|
8
|
+
step with the code, and no constant standing for a port. The explicit module, where every
|
|
9
|
+
binding is written by hand, stays supported and mixes with the declared style in one
|
|
10
|
+
application.
|
|
11
|
+
|
|
12
|
+
```
|
|
13
|
+
npm i @ddd-ts-kit/di tsyringe reflect-metadata
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
`reflect-metadata` is imported by this package before tsyringe loads (tsyringe refuses to load
|
|
17
|
+
without it), so you do not import it yourself. No decorator of the kit needs it: dependencies
|
|
18
|
+
are named, never reflected.
|
|
19
|
+
|
|
20
|
+
## A context that declares itself
|
|
21
|
+
|
|
22
|
+
The decorators come from `@ddd-ts-kit/kernel/declarations`, which holds no container: the
|
|
23
|
+
application layer imports the kernel and nothing else. This package turns the declarations
|
|
24
|
+
into bindings; nothing else does. The domain carries no decorator.
|
|
25
|
+
|
|
26
|
+
A port lives where the layer that owns it lives: a repository port in
|
|
27
|
+
`domain/repositories/`, any other port in `application/ports/`, one abstract class per file.
|
|
28
|
+
|
|
29
|
+
```ts
|
|
30
|
+
// contexts/billing/domain/repositories/invoice.repository.ts
|
|
31
|
+
import { Repository } from '@ddd-ts-kit/kernel';
|
|
32
|
+
|
|
33
|
+
export abstract class InvoiceRepository extends Repository<Invoice> {
|
|
34
|
+
abstract findUnpaidOf(customerId: string): Promise<readonly Invoice[]>;
|
|
35
|
+
}
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
```ts
|
|
39
|
+
// contexts/billing/application/ports/payment-gateway.port.ts
|
|
40
|
+
export abstract class PaymentGateway {
|
|
41
|
+
abstract capture(invoiceId: string, amount: Money): Promise<PaymentReceipt>;
|
|
42
|
+
}
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
```ts
|
|
46
|
+
// contexts/billing/application/commands/issue-invoice/issue-invoice.command.ts
|
|
47
|
+
import { AuthorizedCommand, type Actor } from '@ddd-ts-kit/kernel';
|
|
48
|
+
import { Requires } from '@ddd-ts-kit/kernel/declarations';
|
|
49
|
+
|
|
50
|
+
@Requires(permissions['invoice.issue'], {
|
|
51
|
+
resource: (command: IssueInvoice) => ({ type: 'customer', id: command.customerId }),
|
|
52
|
+
})
|
|
53
|
+
export class IssueInvoice extends AuthorizedCommand<string> {
|
|
54
|
+
constructor(actor: Actor, readonly customerId: string) {
|
|
55
|
+
super(actor);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
```ts
|
|
61
|
+
// contexts/billing/application/commands/issue-invoice/issue-invoice.handler.ts
|
|
62
|
+
import { Clock, IdGenerator } from '@ddd-ts-kit/kernel';
|
|
63
|
+
import { CommandHandler } from '@ddd-ts-kit/kernel/declarations';
|
|
64
|
+
|
|
65
|
+
@CommandHandler(IssueInvoice, [InvoiceRepository, PaymentGateway, IdGenerator, Clock])
|
|
66
|
+
export class IssueInvoiceHandler implements CommandHandler<IssueInvoice> {
|
|
67
|
+
constructor(
|
|
68
|
+
private readonly invoices: InvoiceRepository,
|
|
69
|
+
private readonly payments: PaymentGateway,
|
|
70
|
+
private readonly ids: IdGenerator,
|
|
71
|
+
private readonly clock: Clock,
|
|
72
|
+
) {}
|
|
73
|
+
|
|
74
|
+
async execute(command: IssueInvoice): Promise<string> {
|
|
75
|
+
const invoice = Invoice.issue({ id: this.ids.next(), customerId: command.customerId, issuedAt: this.clock.now() });
|
|
76
|
+
|
|
77
|
+
await this.invoices.save(invoice);
|
|
78
|
+
|
|
79
|
+
return invoice.id;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
The handler only saves. The save hands over the domain events the aggregate recorded, and the
|
|
85
|
+
unit of work of the command dispatches them once the handler has returned, before the commit:
|
|
86
|
+
see "Domain events and the unit of work".
|
|
87
|
+
|
|
88
|
+
```ts
|
|
89
|
+
// contexts/billing/application/queries/get-invoice/get-invoice.query.ts and .handler.ts
|
|
90
|
+
@Public()
|
|
91
|
+
export class GetInvoice extends Query<InvoiceView | undefined> {
|
|
92
|
+
constructor(readonly invoiceId: string) {
|
|
93
|
+
super();
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
@QueryHandler(GetInvoice, [InvoiceRepository])
|
|
98
|
+
export class GetInvoiceHandler implements QueryHandler<GetInvoice> { /* ... */ }
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
```ts
|
|
102
|
+
// contexts/billing/application/integration/handlers/invoice-on-order-placed.integration-handler.ts
|
|
103
|
+
import { CommandBus, systemActor, type IntegrationEvent } from '@ddd-ts-kit/kernel';
|
|
104
|
+
import { IntegrationEventHandler } from '@ddd-ts-kit/kernel/declarations';
|
|
105
|
+
import { OrderPlaced, type OrderPlacedPayload } from '@/contexts/ordering/published-language/index.js';
|
|
106
|
+
|
|
107
|
+
@IntegrationEventHandler(OrderPlaced, { consumer: 'billing.invoice-on-order-placed' }, [CommandBus])
|
|
108
|
+
export class InvoiceOnOrderPlaced implements IntegrationEventHandler<OrderPlacedPayload> {
|
|
109
|
+
constructor(private readonly commands: CommandBus) {}
|
|
110
|
+
|
|
111
|
+
async handle(event: IntegrationEvent<OrderPlacedPayload>): Promise<void> {
|
|
112
|
+
await this.commands.execute(new IssueInvoice(systemActor('billing.invoice-on-order-placed'), event.payload.customerId));
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
```ts
|
|
118
|
+
// contexts/billing/application/integration/publishers/invoice-issued.publisher.ts
|
|
119
|
+
import { IdGenerator, IntegrationEventPublisher } from '@ddd-ts-kit/kernel';
|
|
120
|
+
import { DomainEventHandler } from '@ddd-ts-kit/kernel/declarations';
|
|
121
|
+
|
|
122
|
+
@DomainEventHandler(InvoiceIssued, { name: 'billing.invoice.issued' }, [IntegrationEventPublisher, IdGenerator])
|
|
123
|
+
export class InvoiceIssuedPublisher implements DomainEventHandler<InvoiceIssued> {
|
|
124
|
+
constructor(
|
|
125
|
+
private readonly publisher: IntegrationEventPublisher,
|
|
126
|
+
private readonly ids: IdGenerator,
|
|
127
|
+
) {}
|
|
128
|
+
|
|
129
|
+
async handle(event: InvoiceIssued): Promise<void> { /* publishes billing.invoice.issued */ }
|
|
130
|
+
}
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
```ts
|
|
134
|
+
// contexts/billing/infrastructure/persistence/knex-invoice.repository.ts
|
|
135
|
+
import { Clock, ExecutionScope } from '@ddd-ts-kit/kernel';
|
|
136
|
+
import { Repository } from '@ddd-ts-kit/kernel/declarations';
|
|
137
|
+
|
|
138
|
+
@Repository(InvoiceRepository, [Database, ExecutionScope, Clock])
|
|
139
|
+
export class KnexInvoiceRepository implements InvoiceRepository { /* ... */ }
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
```ts
|
|
143
|
+
// contexts/billing/infrastructure/external/http-payment.gateway.ts
|
|
144
|
+
import { Gateway } from '@ddd-ts-kit/kernel/declarations';
|
|
145
|
+
import { retry, timeout } from '@ddd-ts-kit/aspects';
|
|
146
|
+
|
|
147
|
+
@Gateway(PaymentGateway, { aspects: [retry({ attempts: 3 }), timeout(2000)] }, [PaymentSettings])
|
|
148
|
+
export class HttpPaymentGateway implements PaymentGateway { /* ... */ }
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
That is the whole context. It is assembled in one line, in one of two ways.
|
|
152
|
+
|
|
153
|
+
```ts
|
|
154
|
+
// main.ts, discovered: no file of the context names its classes
|
|
155
|
+
import { CommandBus } from '@ddd-ts-kit/kernel';
|
|
156
|
+
import { createApplication, kernelModule } from '@ddd-ts-kit/di';
|
|
157
|
+
import { discoverContext } from '@ddd-ts-kit/di/discovery';
|
|
158
|
+
|
|
159
|
+
const billing = await discoverContext('billing', new URL('./contexts/billing/', import.meta.url));
|
|
160
|
+
const app = createApplication({ modules: [kernelModule(), platformModule, billing], authorization: 'required' });
|
|
161
|
+
await app.start();
|
|
162
|
+
const invoiceId = await app.resolve(CommandBus).execute(new IssueInvoice(actor, 'customer-1')); // typed string
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
```ts
|
|
166
|
+
// main.ts, listed: one generated barrel per context and nothing else
|
|
167
|
+
import { contextModule } from '@ddd-ts-kit/di';
|
|
168
|
+
import * as billing from '@/contexts/billing/billing.declarations.js';
|
|
169
|
+
|
|
170
|
+
const app = createApplication({ modules: [kernelModule(), platformModule, contextModule('billing', billing)] });
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
## Ports and values
|
|
174
|
+
|
|
175
|
+
### A port is an abstract class
|
|
176
|
+
|
|
177
|
+
The class names the dependency and carries its type: a dependency list is checked against a
|
|
178
|
+
constructor, `app.resolve(Clock)` answers a `Clock`, and nothing else stands for the port. The
|
|
179
|
+
ports of the kernel (`Clock`, `IdGenerator`, `UnitOfWork`, `CommandBus`, `QueryBus`,
|
|
180
|
+
`IntegrationEventPublisher`, `ExecutionScope`...) are abstract classes imported from
|
|
181
|
+
`@ddd-ts-kit/kernel`; yours are written the same way.
|
|
182
|
+
|
|
183
|
+
The rule, checked when a binding is registered (`INVALID_PORT` for `toClass`, a
|
|
184
|
+
`DeclarationError` for a decorator):
|
|
185
|
+
|
|
186
|
+
- a port has abstract members only: no body, no constructor parameter, no private or protected
|
|
187
|
+
member. One exception, `static readonly tokenKey` (see "Identity");
|
|
188
|
+
- a port may extend another port, and is then another port;
|
|
189
|
+
- an adapter `implements` its port and never `extends` it;
|
|
190
|
+
- a class bound to itself (`bind(Settings).toClass(Settings, [...])`, a handler, a job, a
|
|
191
|
+
policy) is no port and keeps its methods.
|
|
192
|
+
|
|
193
|
+
Because a port holds nothing but a shape, a fake and a plain object stand for it:
|
|
194
|
+
`fake(Clock, { now: () => new Date(0) })`.
|
|
195
|
+
|
|
196
|
+
### A value is typed by an abstract class too
|
|
197
|
+
|
|
198
|
+
A configuration object, a database handle, a list, a primitive: an abstract class with
|
|
199
|
+
`abstract readonly` properties, bound with `toValue` to a plain object. That is the one way.
|
|
200
|
+
|
|
201
|
+
```ts
|
|
202
|
+
// platform/database.ts
|
|
203
|
+
export abstract class Database {
|
|
204
|
+
abstract readonly client: Knex;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// contexts/billing/application/ports/payment-settings.port.ts
|
|
208
|
+
export abstract class PaymentSettings {
|
|
209
|
+
abstract readonly apiUrl: string;
|
|
210
|
+
abstract readonly apiKey: string;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// platform/platform.module.ts
|
|
214
|
+
builder.bind(Database).toValue({ client: knex(config.database) });
|
|
215
|
+
builder.bind(PaymentSettings).toValue({ apiUrl: config.payments.url, apiKey: config.payments.key });
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
A primitive is wrapped: `abstract class TaxRate { abstract readonly value: number }`, then
|
|
219
|
+
`builder.bind(TaxRate).toValue({ value: 0.2 })`.
|
|
220
|
+
|
|
221
|
+
### What the compiler checks
|
|
222
|
+
|
|
223
|
+
Where the declaration or the statement is written, with no run:
|
|
224
|
+
|
|
225
|
+
| Mistake | Example |
|
|
226
|
+
|---|---|
|
|
227
|
+
| the class does not implement the port | `bind(Mailer).toClass(SmsSender)`, `@Repository(InvoiceRepository)` on a class missing `findById` |
|
|
228
|
+
| the value does not have the shape of the port | `bind(Clock).toValue('noon')`, `fake(OrderRepository, { save })`, `override(Clock, 12)` |
|
|
229
|
+
| the list names a port of another type | `handledBy(GetOrderHandler, [Clock])` for a constructor taking an `OrderRepository` |
|
|
230
|
+
| the list is in another order than the constructor | `[IdGenerator, Clock]` for `(clock: Clock, ids: IdGenerator)` |
|
|
231
|
+
| the list is missing, one short or one too many | `toClass(ReserveStock)` for a constructor with parameters |
|
|
232
|
+
| the list names a concrete class where its port is expected | `[InMemoryOrderRepository]` for a parameter typed `OrderRepository` |
|
|
233
|
+
| the handler does not handle that message or answers another result | `command(PlaceOrder).handledBy(CancelOrderHandler, ...)` |
|
|
234
|
+
| the event handler handles another payload | `on(ParcelShipped, ...).handledBy(InvoiceOnOrderPlaced, ...)` |
|
|
235
|
+
| `.requires(...)` on a message that carries no actor | `query(GetCatalog).requires(permission)` |
|
|
236
|
+
|
|
237
|
+
Two limits of a structural type system. A concrete class is told apart from its port by what
|
|
238
|
+
it adds: a class with no member of its own, private ones included, is the port for the
|
|
239
|
+
compiler. And a constructor whose parameters are all optional still has parameters: it takes a
|
|
240
|
+
dependency list.
|
|
241
|
+
|
|
242
|
+
### Identity
|
|
243
|
+
|
|
244
|
+
`tokenIdentity(port)` is the symbol a container keys a port by; `describeToken(port)` is its
|
|
245
|
+
class name, which is what error messages and `describe()` print.
|
|
246
|
+
|
|
247
|
+
- A port of your project is identified by the class itself. Two classes of the same name are
|
|
248
|
+
two ports, and a class that extends a port is another port.
|
|
249
|
+
- A port that declares `static readonly tokenKey = '<package>.<Port>'` is identified by that
|
|
250
|
+
key, the same in every copy of its package a process loads. The ports of the kernel all
|
|
251
|
+
declare one (`kernel.Clock`...), and so do `UseCaseGuard`, `UseCaseDeclarations` and
|
|
252
|
+
`DeclaredPolicies` of this package: a handler compiled against one copy of the kernel is
|
|
253
|
+
wired by a container built on another.
|
|
254
|
+
- The key is not inherited: `abstract class BusinessClock extends Clock` is another port, bound
|
|
255
|
+
separately.
|
|
256
|
+
|
|
257
|
+
The limit: a port of your own without a `tokenKey`, loaded twice (the same file imported as
|
|
258
|
+
source and as compiled output, or a package installed in two versions), is two ports. The
|
|
259
|
+
binding made for one does not answer the other, which the application reports as unbound.
|
|
260
|
+
Load each port once, or give a port that a published package exposes a `tokenKey`.
|
|
261
|
+
|
|
262
|
+
## What a class may declare
|
|
263
|
+
|
|
264
|
+
The full list, with the options of each, is in the README of `@ddd-ts-kit/kernel`
|
|
265
|
+
("Declarations"). What this package does with each:
|
|
266
|
+
|
|
267
|
+
| Declaration | Becomes |
|
|
268
|
+
|---|---|
|
|
269
|
+
| `@Requires(permission, options?)` / `@Public()` on a command or a query | the access of the use case: what `.requires(...)` and `.public()` say in an explicit module, read by `authorization: 'required'` and by the authorization module |
|
|
270
|
+
| `@CommandHandler(Message, [ports])`, `@QueryHandler(Message, [ports])` | a singleton of kind `use-case`, registered on `CommandBus` or `QueryBus` |
|
|
271
|
+
| `@IntegrationEventHandler(contract, { consumer }, [ports])` | a singleton of kind `consumer`, subscribed by `start()` on the subscriber of its context, idempotent per consumer name, run in the execution context of the event, its mark and its work in one unit of work |
|
|
272
|
+
| `@DomainEventHandler(Event, { name, phase? }, [ports])` | a singleton of kind `domain-event-handler`, attached to `DomainEventDispatcher` when the application is created, in its phase: `before-commit` (default), at the end of the unit of work and inside its transaction, or `after-commit`, once it has committed; an event dispatched under the name and built from another class is left alone |
|
|
273
|
+
| `@Projection(name, [ports])`, with `@On(definition)` or `@On(Event, { name })` on its methods | a singleton of kind `consumer`. Each integration event it applies is subscribed by `start()` under the name of the projection, exactly as a handler of integration events is: its own execution context, idempotent per name, its mark and its write in one unit of work. Each domain event of its own context is attached to `DomainEventDispatcher` and applied inside the save. See "Projections" |
|
|
274
|
+
| `@Scheduled(name, schedule and options, [ports])` | a singleton of kind `job`, registered on `JobScheduler` when the application is created, started by `start()` after the consumers and stopped first by `stop()`; see "Scheduled jobs" |
|
|
275
|
+
| `@Repository(Port, options?, [ports])` | the binding of the port, kind `repository`: its save hands the domain events of the aggregate to the unit of work, which dispatches them at its end |
|
|
276
|
+
| `@Gateway(Port, options?, [ports])` | the binding of the port, kind `gateway` |
|
|
277
|
+
| `@Adapter(Port, { kind? }, [ports])` | the binding of the port, kind `external` (default) or `publisher` |
|
|
278
|
+
| `@Policy(permission, { name }, [ports])` | a singleton listed in `DeclaredPolicies`, which the authorization module evaluates |
|
|
279
|
+
|
|
280
|
+
A handler, a job, a policy and an event handler are bound under themselves: the class is what
|
|
281
|
+
builds them, and `app.resolve(IssueInvoiceHandler)` answers the instance the bus runs. One
|
|
282
|
+
class is therefore one binding: listing a declared class in two modules is refused.
|
|
283
|
+
|
|
284
|
+
The options of a repository, a gateway and an adapter are the ones of an explicit binding:
|
|
285
|
+
`transient: true` for `.transient()`, `aspects: [...]` for `.with(...)`, the first outermost,
|
|
286
|
+
and `replaces: true` for `rebind`, which a platform adapter uses to take the place of a default
|
|
287
|
+
of the kernel module (`@Adapter(UnitOfWork, { replaces: true }, [Database])`). The kind
|
|
288
|
+
reaches the `decorate` hook, so tracing and global aspects see declared adapters as they see
|
|
289
|
+
explicit ones. A handler is built on the first message it handles; nothing is built when a
|
|
290
|
+
class is declared or when a module is created.
|
|
291
|
+
|
|
292
|
+
### `contextModule(name, declared)`
|
|
293
|
+
|
|
294
|
+
`declared` is a class, a list (lists may nest) or the namespace import of a barrel. Of a
|
|
295
|
+
namespace only the declared classes are kept, so the barrel may also export ports and types.
|
|
296
|
+
A class named directly in a list must carry a declaration (`UNDECLARED_CLASS`): a forgotten
|
|
297
|
+
decorator is not skipped in silence. A class met twice is taken once. The module is static:
|
|
298
|
+
a bundler follows every class, and so does a dead-code analysis.
|
|
299
|
+
|
|
300
|
+
### `discoverContext(name, directory, options?)`
|
|
301
|
+
|
|
302
|
+
From `@ddd-ts-kit/di/discovery`, the one entry of this package that reads the disk. It loads,
|
|
303
|
+
in the order of their paths, every file under `application/` and `infrastructure/` of the
|
|
304
|
+
context whose name ends with a role suffix, and keeps the exported classes that declare
|
|
305
|
+
themselves:
|
|
306
|
+
|
|
307
|
+
| Suffix | Folder | Declares |
|
|
308
|
+
|---|---|---|
|
|
309
|
+
| `*.command`, `*.query` | `application/commands/`, `application/queries/` | `@Requires` or `@Public` |
|
|
310
|
+
| `*.handler` | `application/commands/`, `application/queries/` | `@CommandHandler` or `@QueryHandler` |
|
|
311
|
+
| `*.integration-handler` | `application/integration/handlers/` | `@IntegrationEventHandler` |
|
|
312
|
+
| `*.publisher` | `application/integration/publishers/` | `@DomainEventHandler` |
|
|
313
|
+
| `*.event-handler` | `application/event-handlers/` | `@DomainEventHandler` |
|
|
314
|
+
| `*.policy` | `application/authorization/` | `@Policy` |
|
|
315
|
+
| `*.job` | `application/scheduling/` | `@Scheduled` |
|
|
316
|
+
| `*.projection` | `application/projections/` | `@Projection` |
|
|
317
|
+
| `*.repository` | `infrastructure/persistence/` | `@Repository` |
|
|
318
|
+
| `*.gateway`, `*.adapter` | `infrastructure/` | `@Gateway`, `@Adapter` |
|
|
319
|
+
|
|
320
|
+
A publisher is a domain event handler whose work is to announce the event to the other
|
|
321
|
+
contexts: the same declaration as an event handler, under a suffix and a folder that say what
|
|
322
|
+
it is for.
|
|
323
|
+
|
|
324
|
+
`domain/`, `presentation/`, `published-language/` and `test/` are never read, and neither are
|
|
325
|
+
the `*.port` files of `application/ports/`: ports and fakes are left alone. It fails with a
|
|
326
|
+
`WiringError` naming the file:
|
|
327
|
+
|
|
328
|
+
- `DISCOVERY_FAILED`: a file that throws while loading (the failure is kept as `cause`), or a
|
|
329
|
+
directory with neither `application/` nor `infrastructure/`;
|
|
330
|
+
- `NOTHING_DECLARED`: a file of a role suffix that exports no declared class;
|
|
331
|
+
- `MISPLACED_DECLARATION`: a file of a role suffix outside its folder (refused before it is
|
|
332
|
+
loaded), or a class that declares another role than the one of its file.
|
|
333
|
+
|
|
334
|
+
Options: `ignore: (file) => boolean` leaves files out (a policy written with `definePolicy`, an
|
|
335
|
+
adapter bound explicitly); `extensions` (default `.js`, `.mjs`, `.ts`, `.mts`, the first that
|
|
336
|
+
exists wins, `.d.ts` never) says what is loaded; `load: (url) => Promise<module>` replaces
|
|
337
|
+
`import()`.
|
|
338
|
+
|
|
339
|
+
What it costs, to weigh against one generated barrel:
|
|
340
|
+
|
|
341
|
+
- it reads directories and imports at startup, sequentially, before the application exists;
|
|
342
|
+
- a bundler cannot follow it: a bundled application lists its classes with `contextModule`;
|
|
343
|
+
- the files it loads are imported by nothing, so knip and dependency-cruiser must be told that
|
|
344
|
+
they are entries (see "Limits");
|
|
345
|
+
- it runs what runs the process: compiled `.js` in production, `.ts` only under a loader or a
|
|
346
|
+
test runner that transforms them;
|
|
347
|
+
- under Vitest this package must be inlined, so that the `import()` of discovery goes through
|
|
348
|
+
the runner, which compiles the `.ts` role files and lowers their decorators:
|
|
349
|
+
`test: { server: { deps: { inline: ['@ddd-ts-kit/di'] } } }`. A project initialised with
|
|
350
|
+
`--assembly discovery` has that line in its `vitest.config.ts`.
|
|
351
|
+
|
|
352
|
+
Prefer `contextModule` with a barrel for anything bundled, for a serverless cold start, and
|
|
353
|
+
whenever the tooling should see the classes; prefer discovery when adding a file must be all
|
|
354
|
+
it takes.
|
|
355
|
+
|
|
356
|
+
## Decorator modes
|
|
357
|
+
|
|
358
|
+
The declarations work under both decorator modes of TypeScript, with the same source line:
|
|
359
|
+
|
|
360
|
+
| Stack | `tsconfig.json` | Why |
|
|
361
|
+
|---|---|---|
|
|
362
|
+
| class-validator, NestJS, TypeORM entities, any library with legacy decorators | `"experimentalDecorators": true` | those libraries only have legacy decorators, and one program has one mode |
|
|
363
|
+
| anything else | nothing: leave `experimentalDecorators` unset | standard (TC39) decorators need no flag |
|
|
364
|
+
|
|
365
|
+
A declaration decorator is a function of the class alone. The legacy call hands it the class,
|
|
366
|
+
the standard call hands it the class and a context that it does not read, so the same
|
|
367
|
+
function serves both and stores the declaration in a registry of the kernel, never in
|
|
368
|
+
`Symbol.metadata`. Neither mode needs `emitDecoratorMetadata`, and no parameter decorator
|
|
369
|
+
exists in either: the dependency list is the one mechanism. Vitest lowers legacy decorators by
|
|
370
|
+
itself; for standard ones add `decorators()` of `@ddd-ts-kit/testing/vitest` to the Vitest
|
|
371
|
+
config.
|
|
372
|
+
|
|
373
|
+
## An explicit module
|
|
374
|
+
|
|
375
|
+
The alternative: every binding written by hand in `<context>.module.ts`. It needs no
|
|
376
|
+
decorator at all, and suits a context whose wiring is unusual (factories, values, conditional
|
|
377
|
+
bindings) or a team that wants the wiring in one file. The ports are the same abstract classes,
|
|
378
|
+
in the same files.
|
|
379
|
+
|
|
380
|
+
```ts
|
|
381
|
+
// contexts/ordering/ordering.module.ts
|
|
382
|
+
import { Clock, CommandBus, IdGenerator, IntegrationEventPublisher } from '@ddd-ts-kit/kernel';
|
|
383
|
+
import { defineContextModule } from '@ddd-ts-kit/di';
|
|
384
|
+
import { PaymentCaptured } from '@/contexts/billing/published-language/index.js';
|
|
385
|
+
|
|
386
|
+
export const orderingModule = defineContextModule({
|
|
387
|
+
name: 'ordering',
|
|
388
|
+
register(builder) {
|
|
389
|
+
builder.bind(OrderRepository).toClass(DrizzleOrderRepository, [Database]).as('repository');
|
|
390
|
+
builder.bind(OrderReads).toClass(DrizzleOrderReads, [Database]).as('repository');
|
|
391
|
+
builder.bind(StockGateway).toClass(HttpStockGateway, [StockSettings]).as('gateway');
|
|
392
|
+
|
|
393
|
+
builder
|
|
394
|
+
.command(PlaceOrder)
|
|
395
|
+
.handledBy(PlaceOrderHandler, [OrderRepository, StockGateway, IntegrationEventPublisher, IdGenerator, Clock]);
|
|
396
|
+
builder.command(MarkOrderPaid).handledBy(MarkOrderPaidHandler, [OrderRepository, Clock]);
|
|
397
|
+
builder.query(GetOrder).handledBy(GetOrderHandler, [OrderReads]);
|
|
398
|
+
|
|
399
|
+
builder
|
|
400
|
+
.on(PaymentCaptured, { consumer: 'ordering.mark-order-paid' })
|
|
401
|
+
.handledBy(MarkOrderPaidOnPaymentCaptured, [CommandBus]);
|
|
402
|
+
},
|
|
403
|
+
});
|
|
404
|
+
```
|
|
405
|
+
|
|
406
|
+
Nothing else is needed: no manual registration on a bus, no subscription code.
|
|
407
|
+
|
|
408
|
+
```ts
|
|
409
|
+
// main.ts
|
|
410
|
+
import { CommandBus } from '@ddd-ts-kit/kernel';
|
|
411
|
+
import { createApplication, kernelModule } from '@ddd-ts-kit/di';
|
|
412
|
+
|
|
413
|
+
const app = createApplication({ modules: [kernelModule(), platformModule, orderingModule, billingModule] });
|
|
414
|
+
await app.start();
|
|
415
|
+
const orderId = await app.resolve(CommandBus).execute(new PlaceOrder('sku-1')); // typed string
|
|
416
|
+
process.on('SIGTERM', () => void app.stop());
|
|
417
|
+
```
|
|
418
|
+
|
|
419
|
+
```ts
|
|
420
|
+
// contexts/ordering/test/unit/place-order.handler.spec.ts
|
|
421
|
+
import { createTestApplication, fake } from '@ddd-ts-kit/di';
|
|
422
|
+
|
|
423
|
+
it('places an order and announces it', async () => {
|
|
424
|
+
const orders = new InMemoryOrderRepository();
|
|
425
|
+
const app = createTestApplication({
|
|
426
|
+
modules: [orderingModule],
|
|
427
|
+
fakes: [fake(OrderRepository, orders), fake(StockGateway, new InMemoryStock({ 'sku-1': 3 }))],
|
|
428
|
+
});
|
|
429
|
+
|
|
430
|
+
const orderId = await app.commandBus.execute(new PlaceOrder('sku-1'));
|
|
431
|
+
|
|
432
|
+
expect(orders.count()).toBe(1);
|
|
433
|
+
expect(app.eventBus.published).toMatchObject([{ type: 'ordering.order.placed', payload: { orderId } }]);
|
|
434
|
+
});
|
|
435
|
+
```
|
|
436
|
+
|
|
437
|
+
## Mixing the two styles
|
|
438
|
+
|
|
439
|
+
- **In one application**: declared and explicit modules are listed together; every check
|
|
440
|
+
below applies across them.
|
|
441
|
+
- **In one module**: `builder.include(declared)` adds declared classes next to explicit
|
|
442
|
+
statements, for the value or the factory a decorator cannot express.
|
|
443
|
+
|
|
444
|
+
```ts
|
|
445
|
+
export const billingModule = defineContextModule({
|
|
446
|
+
name: 'billing',
|
|
447
|
+
register(builder) {
|
|
448
|
+
builder.include(billing);
|
|
449
|
+
builder.bind(TaxRate).toValue({ value: config.taxRate });
|
|
450
|
+
},
|
|
451
|
+
});
|
|
452
|
+
```
|
|
453
|
+
|
|
454
|
+
- **A decorated message in an explicit module**: `builder.command(IssueInvoice).handledBy(...)`
|
|
455
|
+
reads the `@Requires` or `@Public` of the message; `.requires(...)` and `.public()` written
|
|
456
|
+
on the builder win.
|
|
457
|
+
- **Replacing a declared adapter**: a later module calls `builder.rebind(Port)`, and a test
|
|
458
|
+
gives `fake(Port, ...)`, exactly as for an explicit binding.
|
|
459
|
+
|
|
460
|
+
## Statements of an explicit module
|
|
461
|
+
|
|
462
|
+
| Call | Meaning |
|
|
463
|
+
|---|---|
|
|
464
|
+
| `bind(Port).toClass(Class, [ports])` | `new Class(...)` with the ports resolved in order; the list is omitted for a constructor without parameters. The class implements the port (`INVALID_PORT` when the port has a body or the class extends it) |
|
|
465
|
+
| `bind(Port).toFactory((resolve) => instance)` | anything a constructor cannot express |
|
|
466
|
+
| `bind(Port).toValue(value)` | an existing object, a fake, or the plain object a value class types |
|
|
467
|
+
| `.singleton()` / `.transient()` | one instance per application (default) or one per resolution |
|
|
468
|
+
| `.as(kind)` | role of the binding, handed to the `decorate` hook: `repository`, `gateway`, `external`, `use-case`, `publisher`, `consumer`, `domain-event-handler`, `job`. A port extending the kernel `Repository` is of kind `repository` without it |
|
|
469
|
+
| `rebind(Port).to...` | replaces the binding of a module listed earlier, such as a default of `kernelModule()` |
|
|
470
|
+
| `command(Type).handledBy(Handler, [ports])` | binds the handler under itself (singleton, kind `use-case`) and registers it on `CommandBus` |
|
|
471
|
+
| `query(Type).handledBy(Handler, [ports])` | the same on `QueryBus` |
|
|
472
|
+
| `on(contract, { consumer }).handledBy(Handler, [ports])` | binds the integration event handler under itself (kind `consumer`) and subscribes its `handle(event)` on the subscriber of the module |
|
|
473
|
+
| `job(name, schedule and options).handledBy(Job, [ports])` | binds the job under itself (singleton, kind `job`) and registers its `run(execution)` on `JobScheduler` |
|
|
474
|
+
| `.handledByFactory((resolve) => handler)` | the factory form of the four lines above, bound under a class made for the statement, which reads `command(PlaceOrder) handler` in a description |
|
|
475
|
+
| `include(declared)` | adds classes that declare themselves: what `contextModule` does |
|
|
476
|
+
|
|
477
|
+
**One class is one binding.** `handledBy(Handler)` binds the class under itself, so the same
|
|
478
|
+
handler class cannot be given to two statements: the second is refused with
|
|
479
|
+
`DUPLICATE_BINDING`. One handler per message is the rule the kit follows everywhere; where one
|
|
480
|
+
object really serves two statements, give each `handledByFactory(() => shared)`.
|
|
481
|
+
|
|
482
|
+
Handlers are registered when the application is created, which resolves the two buses; a
|
|
483
|
+
handler itself is built on the first message it handles, then reused. Consumers are subscribed
|
|
484
|
+
by `start()` and unsubscribed by `stop()`, since a subscription is a side effect.
|
|
485
|
+
|
|
486
|
+
## Integration event handlers
|
|
487
|
+
|
|
488
|
+
`contract` is anything with `type`, `version` and `parse(event)`: a declaration made by
|
|
489
|
+
`defineIntegrationEvent` of `@ddd-ts-kit/events` fits as it is. An event of another version is
|
|
490
|
+
skipped, an event the contract cannot read throws `UnreadableEventError` so that the provider
|
|
491
|
+
retries or dead-letters it, and a redelivered event is handled once per `consumer` name when
|
|
492
|
+
`ProcessedEventStore` is bound. The name is required: it is the idempotency key, and it must
|
|
493
|
+
survive the renaming of a class.
|
|
494
|
+
|
|
495
|
+
### The execution context of the event
|
|
496
|
+
|
|
497
|
+
Every consumer handles its events inside `ExecutionScope`, in a context of its own: origin
|
|
498
|
+
`event`, the correlation of the event (its id when it names none), the event as `causationId`,
|
|
499
|
+
and `systemActor(consumer)` as actor, which is therefore what `ActorContext` answers to the
|
|
500
|
+
repositories of the consuming context. Nothing to wire. A project whose envelopes name an
|
|
501
|
+
actor adds `on(contract, { consumer, actorOf: (event) => actor })`. For the correlation to
|
|
502
|
+
start at the request, publish through a scoped publisher:
|
|
503
|
+
|
|
504
|
+
```ts
|
|
505
|
+
kernelModule({
|
|
506
|
+
eventPublisher: (resolve) => scopedPublisher(new OutboxPublisher(store), resolve(ExecutionScope)),
|
|
507
|
+
});
|
|
508
|
+
```
|
|
509
|
+
|
|
510
|
+
### One unit of work per event
|
|
511
|
+
|
|
512
|
+
When `UnitOfWork` and `ProcessedEventStore` are both bound, which `kernelModule()` does, the
|
|
513
|
+
idempotency mark and the work of the handler run inside one `unitOfWork.run`, opened inside
|
|
514
|
+
the execution context of the event:
|
|
515
|
+
|
|
516
|
+
- a handler that fails leaves no mark: the transaction that held it is rolled back, and the
|
|
517
|
+
redelivery is handled;
|
|
518
|
+
- a handler that succeeds commits its mark with its writes: the redelivery is skipped, and
|
|
519
|
+
there is no window where one is kept without the other;
|
|
520
|
+
- a command the handler sends runs its own `unitOfWork.run` inside that one, so the
|
|
521
|
+
`UnitOfWork` adapter must join the transaction that is open, as the adapters of the kit do;
|
|
522
|
+
- the unit of work is resolved at each event, as the command bus resolves it at each command.
|
|
523
|
+
|
|
524
|
+
This holds for a store whose mark is written in the database of the unit of work. Without a
|
|
525
|
+
`UnitOfWork` bound, the mark is taken first and given back with `release` when the handler
|
|
526
|
+
fails, for a store that has one; a store without `release` keeps the mark of a failed handler.
|
|
527
|
+
|
|
528
|
+
### The subscriber of each context
|
|
529
|
+
|
|
530
|
+
A handler of a context is subscribed on `EventSubscribers.forContext(<module name>)`. By
|
|
531
|
+
default every context gets the single `IntegrationEventSubscriber`, which is the in-memory
|
|
532
|
+
event bus: one process, one bus, nothing to say.
|
|
533
|
+
|
|
534
|
+
A broker gives each consuming context its own subscriber, so that the context keeps its queues
|
|
535
|
+
when it is extracted. Say it once, in the kernel module or from a later module:
|
|
536
|
+
|
|
537
|
+
```ts
|
|
538
|
+
kernelModule({ eventBus: bus, eventSubscribers: { forContext: (name) => bus.forConsumer(name) } });
|
|
539
|
+
|
|
540
|
+
builder.rebind(EventSubscribers).toValue({ forContext: (name) => bus.forConsumer(name) });
|
|
541
|
+
```
|
|
542
|
+
|
|
543
|
+
The name handed to `forContext` is the name of the module: the first argument of
|
|
544
|
+
`contextModule` and `discoverContext`, the `name` of `defineContextModule`. An application
|
|
545
|
+
without the kernel module needs one of the two bound, `IntegrationEventSubscriber` or
|
|
546
|
+
`EventSubscribers`; `EventSubscribers` wins when both are.
|
|
547
|
+
|
|
548
|
+
## Projections
|
|
549
|
+
|
|
550
|
+
A projection is a class of `application/projections/`, in a `*.projection.ts` file, declared
|
|
551
|
+
with `@Projection('<context>.<name>', [ports])`; the README of `@ddd-ts-kit/kernel`
|
|
552
|
+
("Projections") says what it may do and why its writes are idempotent and ordered. Here it is
|
|
553
|
+
wired: `contextModule` and `discoverContext` find it like any declared class, and an explicit
|
|
554
|
+
module takes it with `builder.include(AppointmentListProjection)`.
|
|
555
|
+
|
|
556
|
+
The CLI does not know projections yet: `ddd-ts-kit check` refuses `application/projections/`
|
|
557
|
+
(`tree/closed`) unless the directory is declared in `extraDirectories`, and refuses a line for
|
|
558
|
+
a projection in the generated barrel (`declarations/barrel-current`). Until it does, a
|
|
559
|
+
project held by the CLI registers a projection by discovery or with `builder.include(...)`.
|
|
560
|
+
|
|
561
|
+
```ts
|
|
562
|
+
const app = createTestApplication({ modules: [contextModule('reporting', reporting)] });
|
|
563
|
+
|
|
564
|
+
await app.start();
|
|
565
|
+
await app.eventBus.publish([orderPlaced('evt-1'), orderPlaced('evt-1'), orderPlaced('evt-2')]);
|
|
566
|
+
|
|
567
|
+
expect(await app.get(CustomerOrders).get('customer-1')).toMatchObject({ orders: 2 });
|
|
568
|
+
```
|
|
569
|
+
|
|
570
|
+
A spec feeds a projection through the in-memory bus and reads the model through its port:
|
|
571
|
+
there is nothing else to learn. The fake of a read model port is `InMemoryReadModelStore` of
|
|
572
|
+
the kernel, bound by its adapter or given with `fake(Port, store)`.
|
|
573
|
+
|
|
574
|
+
- **One consumer.** The name of the projection is the consumer name of every event it applies:
|
|
575
|
+
the idempotency scope in `ProcessedEventStore`, the system actor it runs as, the
|
|
576
|
+
`process <projection>` span and, behind a broker, its queue or its group. Two projections of
|
|
577
|
+
the same name are refused (`DUPLICATE_PROJECTION`), and so are two methods of one projection
|
|
578
|
+
applying the same event (`DUPLICATE_CONSUMER`).
|
|
579
|
+
- **One unit of work per event.** With `UnitOfWork` and `ProcessedEventStore` bound to the same
|
|
580
|
+
database as the read model, the mark and the row commit or roll back together. Without
|
|
581
|
+
them, the stamp each row carries still makes a redelivery a no-op.
|
|
582
|
+
- **Measured.** With `Metrics` bound, `projection_events_total` counts each event by `context`,
|
|
583
|
+
`projection`, `event` and `result` (`applied`, `stale`, `ignored`, `failed`), and
|
|
584
|
+
`projection_last_event_age_ms` holds the age of the last event applied when it was.
|
|
585
|
+
- **Described.** `describe().projections` lists each projection with the events it applies and
|
|
586
|
+
the method of each; they do not appear among `integrationEventHandlers` and
|
|
587
|
+
`domainEventHandlers`.
|
|
588
|
+
|
|
589
|
+
## Behind a broker
|
|
590
|
+
|
|
591
|
+
Discovery, a broker and a database, with no helper of your own. The complete composition root:
|
|
592
|
+
|
|
593
|
+
```ts
|
|
594
|
+
// main.ts
|
|
595
|
+
import { EventSubscribers, ProcessedEventStore, UnitOfWork } from '@ddd-ts-kit/kernel';
|
|
596
|
+
import { createApplication, defineContextModule, kernelModule } from '@ddd-ts-kit/di';
|
|
597
|
+
import { discoverContext } from '@ddd-ts-kit/di/discovery';
|
|
598
|
+
import { RabbitMqEventBus } from '@ddd-ts-kit/rabbitmq';
|
|
599
|
+
|
|
600
|
+
const bus = new RabbitMqEventBus({ connection: config.amqpUrl });
|
|
601
|
+
|
|
602
|
+
const platformModule = defineContextModule({
|
|
603
|
+
name: 'platform',
|
|
604
|
+
register(builder) {
|
|
605
|
+
builder.bind(Database).toValue({ client: knex(config.database) });
|
|
606
|
+
builder.rebind(UnitOfWork).toClass(DatabaseUnitOfWork, [Database]);
|
|
607
|
+
builder.rebind(ProcessedEventStore).toClass(DatabaseProcessedEvents, [Database]);
|
|
608
|
+
builder.rebind(EventSubscribers).toValue({ forContext: (name) => bus.forConsumer(name) });
|
|
609
|
+
},
|
|
610
|
+
onStop: () => bus.close(),
|
|
611
|
+
});
|
|
612
|
+
|
|
613
|
+
const app = createApplication({
|
|
614
|
+
modules: [
|
|
615
|
+
kernelModule({ eventBus: bus }),
|
|
616
|
+
platformModule,
|
|
617
|
+
await discoverContext('ordering', new URL('./contexts/ordering/', import.meta.url)),
|
|
618
|
+
await discoverContext('billing', new URL('./contexts/billing/', import.meta.url)),
|
|
619
|
+
],
|
|
620
|
+
authorization: 'required',
|
|
621
|
+
});
|
|
622
|
+
|
|
623
|
+
await app.start();
|
|
624
|
+
process.on('SIGTERM', () => void app.stop());
|
|
625
|
+
```
|
|
626
|
+
|
|
627
|
+
That is all: `discoverContext('billing', ...)` names the module `billing`, so its integration
|
|
628
|
+
event handlers are subscribed on `bus.forConsumer('billing')`, each event handled in the
|
|
629
|
+
execution context of the event, its mark and its work in one database transaction. With a
|
|
630
|
+
barrel instead of discovery the two lines become `contextModule('ordering', ordering)` and
|
|
631
|
+
`contextModule('billing', billing)`, and nothing else changes. `DatabaseUnitOfWork` and
|
|
632
|
+
`DatabaseProcessedEvents` stand for the adapters of your persistence package; any bus with a
|
|
633
|
+
`forConsumer(name)` fits where `RabbitMqEventBus` is.
|
|
634
|
+
|
|
635
|
+
## The kernel module
|
|
636
|
+
|
|
637
|
+
`kernelModule(options?)` binds the ports every context depends on.
|
|
638
|
+
|
|
639
|
+
| Port | Default | Option |
|
|
640
|
+
|---|---|---|
|
|
641
|
+
| `Clock` | `SystemClock` | `clock` |
|
|
642
|
+
| `IdGenerator` | `UuidGenerator` | `ids` |
|
|
643
|
+
| `UnitOfWork` | `InMemoryUnitOfWork` | `unitOfWork` |
|
|
644
|
+
| `CommandBus` | `InMemoryCommandBus` running each command in the unit of work: the domain events its saves announce are dispatched once the handler has returned, before the commit, then the `after-commit` handlers run | `commandBus` |
|
|
645
|
+
| `QueryBus` | `InMemoryQueryBus` | `queryBus` |
|
|
646
|
+
| `EventBus` | `InMemoryEventBus` | `eventBus` |
|
|
647
|
+
| `IntegrationEventPublisher` | the event bus | `eventPublisher` |
|
|
648
|
+
| `IntegrationEventSubscriber` | the event bus | `eventSubscriber` |
|
|
649
|
+
| `EventSubscribers` | the single `IntegrationEventSubscriber`, for every context | `eventSubscribers` |
|
|
650
|
+
| `ProcessedEventStore` | `InMemoryProcessedEventStore` | `processedEvents` |
|
|
651
|
+
| `ExecutionScope` | `AsyncLocalStorageExecutionScope` | `executionScope` |
|
|
652
|
+
| `ActorContext` | `ScopedActorContext`: the actor of the execution scope | `actorContext` |
|
|
653
|
+
| `DomainEventDispatcher` | `InMemoryDomainEventDispatcher`, which hears what the save of every repository announced; a failure of an `after-commit` handler goes to the `Logger` and the `ErrorReporter` (`reportAfterCommitFailures`) | `domainEvents` |
|
|
654
|
+
| `Logger` | `NoopLogger`. `app.stop()` flushes it last, after the error reporter, and stops all the same when the flush fails | `logger` |
|
|
655
|
+
| `Metrics` | `NoopMetrics` | `metrics` |
|
|
656
|
+
| `ErrorReporter` | `NoopErrorReporter`. The default buses and the default scheduler hand it their unexpected failures (never a `DomainError`), and `app.stop()` gives it two seconds to flush | `errorReporter` |
|
|
657
|
+
| `Cache` | `InMemoryCache` on `Clock` | `cache` |
|
|
658
|
+
| `Lock` | `InMemoryLock` on `Clock`: this process only | `lock` |
|
|
659
|
+
| `RateLimiter` | `InMemoryRateLimiter` on `Clock`: this process only | `rateLimiter` |
|
|
660
|
+
| `JobScheduler` | `InProcessJobScheduler` of `@ddd-ts-kit/scheduling` on `Clock`, `Lock`, `ExecutionScope`, `IdGenerator`, `Logger`, `Metrics` and `ErrorReporter` | `jobScheduler` |
|
|
661
|
+
|
|
662
|
+
The ports are the abstract classes of `@ddd-ts-kit/kernel`: a handler names `Clock` in its
|
|
663
|
+
dependency list without importing this package.
|
|
664
|
+
|
|
665
|
+
An option is an instance or a `(resolve) => instance` factory. An adapter with dependencies of
|
|
666
|
+
its own reads better in the module that owns them:
|
|
667
|
+
|
|
668
|
+
```ts
|
|
669
|
+
export const persistenceModule = defineContextModule({
|
|
670
|
+
name: 'persistence',
|
|
671
|
+
register(builder) {
|
|
672
|
+
builder.bind(Database).toFactory(() => ({ client: connect(config.databaseUrl) }));
|
|
673
|
+
builder.rebind(UnitOfWork).toClass(DrizzleUnitOfWork, [Database]);
|
|
674
|
+
builder.rebind(IntegrationEventPublisher).toClass(OutboxPublisher, [OutboxStore]).as('publisher');
|
|
675
|
+
},
|
|
676
|
+
});
|
|
677
|
+
```
|
|
678
|
+
|
|
679
|
+
Use cases publish through `IntegrationEventPublisher` and never through `EventBus`, so moving
|
|
680
|
+
to an outbox touches no context. A bus given for `CommandBus` or `QueryBus` needs a
|
|
681
|
+
`register(type, handler)` method for the declared handlers to be attached to it.
|
|
682
|
+
|
|
683
|
+
## Scheduled jobs
|
|
684
|
+
|
|
685
|
+
A class of `application/scheduling/` that carries `@Scheduled` is a job of its context: nothing
|
|
686
|
+
else registers it.
|
|
687
|
+
|
|
688
|
+
```ts
|
|
689
|
+
// contexts/billing/application/scheduling/send-reminders.job.ts
|
|
690
|
+
import { CommandBus, systemActor } from '@ddd-ts-kit/kernel';
|
|
691
|
+
import { Scheduled, type JobExecution, type ScheduledJob } from '@ddd-ts-kit/kernel/declarations';
|
|
692
|
+
|
|
693
|
+
@Scheduled('billing.send-reminders', { cron: '0 7 * * 1-5', timeZone: 'Europe/Paris' }, [CommandBus])
|
|
694
|
+
export class SendRemindersJob implements ScheduledJob {
|
|
695
|
+
constructor(private readonly commands: CommandBus) {}
|
|
696
|
+
|
|
697
|
+
async run(execution: JobExecution): Promise<void> {
|
|
698
|
+
await this.commands.execute(new SendReminders(systemActor('billing.send-reminders'), execution.scheduledAt));
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
```
|
|
702
|
+
|
|
703
|
+
- **Registered when the application is created**, on `JobScheduler`, with the defaults of
|
|
704
|
+
its options filled in. The class is built on its first run, so a job that cannot be built
|
|
705
|
+
fails that run and nothing else.
|
|
706
|
+
- **Started by `start()`**, after the modules and once the consumers are subscribed: a job
|
|
707
|
+
sends commands whose events must find their handlers. **Stopped first by `stop()`**, which
|
|
708
|
+
waits for the runs in progress, then aborts them, while everything they use is still up.
|
|
709
|
+
- **Each run happens in `ExecutionScope`**, in a context of origin `schedule`, as
|
|
710
|
+
`systemActor('<job name>')`, with a correlation of its own. The scheduler does it, not the
|
|
711
|
+
job.
|
|
712
|
+
- **The default scheduler** is the in-process one of `@ddd-ts-kit/scheduling`, on the bound
|
|
713
|
+
`Clock` and `Lock`. With the default lock, which excludes inside one process only, several
|
|
714
|
+
instances each run every job: bind a lock between processes (`RedisLock`) before scaling
|
|
715
|
+
out. What one instance among several guarantees, and what it does not, is in that README.
|
|
716
|
+
- **An application without job** never resolves the scheduler.
|
|
717
|
+
|
|
718
|
+
`createApplication({ jobs })` says whether this process runs the jobs and what configuration
|
|
719
|
+
changes of each:
|
|
720
|
+
|
|
721
|
+
```ts
|
|
722
|
+
createApplication({
|
|
723
|
+
modules,
|
|
724
|
+
jobs: {
|
|
725
|
+
start: config.runJobs, // default true
|
|
726
|
+
settings: {
|
|
727
|
+
'billing.send-reminders': { enabled: config.reminders.enabled },
|
|
728
|
+
'billing.export-ledger': { schedule: { every: '6h' }, timeoutMs: 300_000 },
|
|
729
|
+
},
|
|
730
|
+
},
|
|
731
|
+
});
|
|
732
|
+
```
|
|
733
|
+
|
|
734
|
+
| Option | |
|
|
735
|
+
|---|---|
|
|
736
|
+
| `start: false` | the jobs are registered and described, `runNow` runs one, and `start()` never starts the scheduler: the process that serves requests next to a worker process that runs the jobs |
|
|
737
|
+
| `settings['<job name>']` | `enabled`, `schedule` and any option of the declaration (`overlap`, `missed`, `timeoutMs`, `jitterMs`, `singleInstance`), which win over what the class wrote. A key left undefined changes nothing |
|
|
738
|
+
|
|
739
|
+
`app.resolve(JobScheduler)` is the port: `runNow(name)` for an operations endpoint,
|
|
740
|
+
`describe()` for the next run and the last outcome of each job. A context extracted as its own
|
|
741
|
+
service takes its jobs with it: they belong to its module, and nothing is global.
|
|
742
|
+
|
|
743
|
+
## Tests
|
|
744
|
+
|
|
745
|
+
A declared context is tested like an explicit one: the module is the unit, the fakes replace
|
|
746
|
+
its adapters.
|
|
747
|
+
|
|
748
|
+
```ts
|
|
749
|
+
const app = createTestApplication({
|
|
750
|
+
modules: [contextModule('billing', billing)],
|
|
751
|
+
fakes: [fake(PaymentGateway, new InMemoryPayments())],
|
|
752
|
+
});
|
|
753
|
+
```
|
|
754
|
+
|
|
755
|
+
`fake(Port, value)` is checked: the value implements the port. An in-memory class, an object
|
|
756
|
+
literal and a class written in the spec all fit, since a port is a shape.
|
|
757
|
+
|
|
758
|
+
`createTestApplication({ modules, fakes, now?, idPrefix?, actor?, session?, executionContext?, jobs? })` puts the modules on a deterministic
|
|
759
|
+
kernel, whether or not they list `kernelModule()`, and returns the application plus what a
|
|
760
|
+
spec touches:
|
|
761
|
+
|
|
762
|
+
| Member | Is |
|
|
763
|
+
|---|---|
|
|
764
|
+
| `commandBus`, `queryBus` | the in-memory buses, with every declared handler |
|
|
765
|
+
| `eventBus` | `InMemoryEventBus`; `eventBus.published` lists what the use cases announced. Every context listens on it, whatever `EventSubscribers` the modules bind |
|
|
766
|
+
| `domainEvents` | `InMemoryDomainEventDispatcher`: the save of every repository, adapter or fake, announces to it, with the timing of the application (at the end of the command, then after it for `after-commit` handlers); a spec may dispatch a domain event itself to see the declared handlers react, and `domainEvents.afterCommitFailures` holds what `after-commit` handlers threw |
|
|
767
|
+
| `clock` | `FixedClock` at `now` (default `2026-01-01T00:00:00.000Z`); `clock.advance(ms)` |
|
|
768
|
+
| `ids` | `SequentialIdGenerator`: `id-1`, `id-2`... |
|
|
769
|
+
| `actorContext` | `FixedActorContext`; `actAs(actor)` changes who infrastructure sees |
|
|
770
|
+
| `executionScope` | `FixedExecutionScope` (correlation `test-correlation`, origin `test`), following `actorContext` |
|
|
771
|
+
| `logger`, `metrics` | `InMemoryLogger` and `InMemoryMetrics`: assert `logger.entries`, `metrics.samples` |
|
|
772
|
+
| `errors` | `InMemoryErrorReporter`: `errors.reports` lists the unexpected failures of the commands, queries and jobs, as data |
|
|
773
|
+
| `cache` | `InMemoryCache` on `clock` |
|
|
774
|
+
| `lock`, `rateLimiter` | `InMemoryLock` and `InMemoryRateLimiter` on `clock` |
|
|
775
|
+
| `jobs` | `runNow(name)` runs a declared job as the scheduler would and throws what it threw; `scheduler` is a `ManualJobScheduler` on `clock`, not started |
|
|
776
|
+
| `get(Port)` | `resolve`, typed by the port |
|
|
777
|
+
| `as(actorOrSession, work)` | runs a block as someone else, then comes back |
|
|
778
|
+
|
|
779
|
+
`actor`, `session` and `executionContext` say who infrastructure sees by default: an actor, a
|
|
780
|
+
whole session (its actor becomes the current actor), or the complete execution context for a
|
|
781
|
+
spec about a tenant, a correlation or a deadline. `app.as` switches for one block and hands the
|
|
782
|
+
actor to it, which is what a spec about authorization wants to read:
|
|
783
|
+
|
|
784
|
+
```ts
|
|
785
|
+
const app = createTestApplication({ modules: [billingModule, authorization], actor: accountant });
|
|
786
|
+
|
|
787
|
+
await app.as(accountant, (actor) => app.commandBus.execute(new IssueInvoice(actor, 'customer-1')));
|
|
788
|
+
await expect(
|
|
789
|
+
app.as(clerk, (actor) => app.commandBus.execute(new VoidInvoice(actor, 'inv-1'))),
|
|
790
|
+
).rejects.toMatchObject({ code: 'FORBIDDEN' });
|
|
791
|
+
|
|
792
|
+
app.as(aSession().of(accountant).impersonatedBy('support-7').build(), () => {
|
|
793
|
+
// infrastructure sees the session: audit records the impersonation
|
|
794
|
+
});
|
|
795
|
+
```
|
|
796
|
+
|
|
797
|
+
Use cases still receive the actor in the command or query; `as` only decides what the
|
|
798
|
+
repositories, the audit columns and the tenant scoping see while the block runs.
|
|
799
|
+
|
|
800
|
+
**Jobs are never started by time** in a test application: `start()` subscribes the consumers
|
|
801
|
+
and leaves the scheduler alone, whatever the clock does. A spec runs a job by name and asserts
|
|
802
|
+
the fakes, as it would for a command:
|
|
803
|
+
|
|
804
|
+
```ts
|
|
805
|
+
await app.jobs.runNow('billing.send-reminders');
|
|
806
|
+
|
|
807
|
+
expect(mailer.sent).toHaveLength(2);
|
|
808
|
+
```
|
|
809
|
+
|
|
810
|
+
For a spec about the schedule itself, `await app.jobs.scheduler.start()` then
|
|
811
|
+
`await app.jobs.scheduler.advance(ms)` move `app.clock` and run what becomes due, and
|
|
812
|
+
`app.jobs.scheduler.runs` lists what happened. The `jobs` option takes per-job settings, as
|
|
813
|
+
`createApplication({ jobs: { settings } })` does. Runs take their correlation ids from a
|
|
814
|
+
generator of their own (`job-run-1`...), so a job leaves the identifiers a spec predicts alone.
|
|
815
|
+
|
|
816
|
+
A fake must stand for a port that a module binds or that a class depends on, otherwise
|
|
817
|
+
`UNKNOWN_FAKE` is raised: a fake for the wrong port would silently leave the real adapter in
|
|
818
|
+
place. Consumers receive events once `await app.start()` ran.
|
|
819
|
+
|
|
820
|
+
`createApplication({ overrides: [override(Port, value)] })` is the same pair without that
|
|
821
|
+
check, and `overrideForTests(app, Port, value)` swaps a binding on an existing application;
|
|
822
|
+
call it before the first `resolve`, since a singleton already built keeps what it was given. An
|
|
823
|
+
override replaces the binding, may stand in for a module that is not loaded, and is not
|
|
824
|
+
decorated. Every application owns a tsyringe child container (`app.container`), so two tests
|
|
825
|
+
never share instances.
|
|
826
|
+
|
|
827
|
+
## Checks
|
|
828
|
+
|
|
829
|
+
`createApplication` throws a `WiringError` (with a `code`) before anything is instantiated:
|
|
830
|
+
|
|
831
|
+
- `DUPLICATE_BINDING`: a port bound twice, or a class given to two statements or listed in two
|
|
832
|
+
modules, both modules named;
|
|
833
|
+
- `NOTHING_TO_REBIND`: a `rebind` of a port that no earlier module binds;
|
|
834
|
+
- `INVALID_PORT`: a class bound to a port that carries a body, or that it extends;
|
|
835
|
+
- `UNBOUND_DEPENDENCY`: a class dependency that no module binds, or handlers, consumers and
|
|
836
|
+
jobs declared without the bus, the subscriber or the scheduler they go on;
|
|
837
|
+
- `DEPENDENCY_CYCLE`: a cycle between class bindings, the path printed;
|
|
838
|
+
- `INCOMPLETE_BINDING`: a `bind`, `command`, `query`, `on` or `job` left without its target;
|
|
839
|
+
- `DUPLICATE_HANDLER`: a command or a query with two handlers, both modules named;
|
|
840
|
+
- `DUPLICATE_CONSUMER`: two consumers of the same event sharing a name;
|
|
841
|
+
- `DUPLICATE_PROJECTION`: two projections sharing a name;
|
|
842
|
+
- `BUS_WITHOUT_REGISTER`: handlers declared for a bus that cannot receive them;
|
|
843
|
+
- `UNKNOWN_FAKE`: see above;
|
|
844
|
+
- `UNDECLARED_CLASS`: a class listed in `contextModule` or `include` without a declaration;
|
|
845
|
+
- `DUPLICATE_JOB`: two jobs sharing a name, both modules named: the name is the lock, the
|
|
846
|
+
actor and the label of a job;
|
|
847
|
+
- `UNKNOWN_JOB`: settings given for a job that no module declares;
|
|
848
|
+
- `INVALID_JOB_SETTINGS`: a schedule or an option of the settings that cannot be read (a cron,
|
|
849
|
+
a time zone, a zero interval), or a job of an explicit module whose name or schedule cannot.
|
|
850
|
+
|
|
851
|
+
`DUPLICATE_HANDLER`, `DUPLICATE_CONSUMER` and `DUPLICATE_JOB` are reported before the
|
|
852
|
+
`DUPLICATE_BINDING` of the class they share, so the message says what the mistake is.
|
|
853
|
+
|
|
854
|
+
The checks do not tell the two styles apart. For a declared class the message names the class
|
|
855
|
+
and, when discovery loaded it, the file:
|
|
856
|
+
|
|
857
|
+
```
|
|
858
|
+
Token "InvoiceRepository" is bound twice: by module "billing" (declared by class
|
|
859
|
+
KnexInvoiceRepository in /app/dist/contexts/billing/infrastructure/persistence/knex-invoice.repository.js)
|
|
860
|
+
and by module "invoicing" (declared by class InMemoryInvoiceRepository).
|
|
861
|
+
```
|
|
862
|
+
|
|
863
|
+
Factories do not declare what they resolve, so a cycle or a missing port going through a
|
|
864
|
+
factory is reported when it is resolved (`DEPENDENCY_CYCLE`, `UNBOUND_TOKEN`).
|
|
865
|
+
|
|
866
|
+
## Diagnostics
|
|
867
|
+
|
|
868
|
+
`app.describe()` returns the wiring as plain, serializable data: ports and classes by name.
|
|
869
|
+
|
|
870
|
+
```ts
|
|
871
|
+
{
|
|
872
|
+
modules: ['kernel', 'ordering'],
|
|
873
|
+
bindings: [{ token: 'PlaceOrderHandler', module: 'ordering', kind: 'use-case', lifetime: 'singleton',
|
|
874
|
+
source: 'class', implementation: 'PlaceOrderHandler',
|
|
875
|
+
dependencies: ['OrderRepository', 'Clock'], overridden: false }],
|
|
876
|
+
commands: [{ message: 'PlaceOrder', handler: 'PlaceOrderHandler', module: 'ordering' }],
|
|
877
|
+
queries: [{ message: 'GetOrder', handler: 'GetOrderHandler', module: 'ordering' }],
|
|
878
|
+
integrationEventHandlers: [{ event: 'billing.payment.captured', version: 1, consumer: 'ordering.mark-order-paid',
|
|
879
|
+
handler: 'MarkOrderPaidOnPaymentCaptured', module: 'ordering' }],
|
|
880
|
+
domainEventHandlers: [],
|
|
881
|
+
policies: [],
|
|
882
|
+
jobs: [{ name: 'ordering.expire-carts', schedule: 'every 15m', overlap: 'skip', missed: 'skip', jitterMs: 0,
|
|
883
|
+
enabled: true, singleInstance: true, job: 'ExpireCartsJob', module: 'ordering' }],
|
|
884
|
+
projections: [{ name: 'ordering.order-list', projection: 'OrderListProjection', module: 'ordering',
|
|
885
|
+
applies: [{ kind: 'integration', event: 'billing.payment.captured', version: 1, method: 'paid' }] }],
|
|
886
|
+
overrides: [],
|
|
887
|
+
}
|
|
888
|
+
```
|
|
889
|
+
|
|
890
|
+
An entry that comes from a declaration also carries `origin: 'declared'`, `declaredBy` (the
|
|
891
|
+
class) and, when discovery loaded it, `file`. An entry written in a module is `explicit` and
|
|
892
|
+
carries none of the three:
|
|
893
|
+
|
|
894
|
+
```ts
|
|
895
|
+
{ token: 'InvoiceRepository', module: 'billing', kind: 'repository', lifetime: 'singleton',
|
|
896
|
+
source: 'class', implementation: 'KnexInvoiceRepository', dependencies: ['Database'],
|
|
897
|
+
overridden: false, origin: 'declared', declaredBy: 'KnexInvoiceRepository',
|
|
898
|
+
file: '/app/dist/contexts/billing/infrastructure/persistence/knex-invoice.repository.js' }
|
|
899
|
+
```
|
|
900
|
+
|
|
901
|
+
`domainEventHandlers` lists the declared handlers of domain events, publishers included, each
|
|
902
|
+
with its `phase` (`before-commit` or `after-commit`),
|
|
903
|
+
`policies` the declared policy classes, `jobs` the jobs with the settings they run with, those
|
|
904
|
+
of the application applied. What a job did last and when it runs next is not wiring: ask
|
|
905
|
+
`app.resolve(JobScheduler).describe()`.
|
|
906
|
+
|
|
907
|
+
## Domain events and the unit of work
|
|
908
|
+
|
|
909
|
+
An effect of a domain event exists only if the state change that recorded it is committed.
|
|
910
|
+
|
|
911
|
+
A handler writes `await this.orders.save(order); return order.id;` and nothing else: no
|
|
912
|
+
`pullEvents()`, no `DomainEventDispatcher` in a use case. `kernelModule()` puts
|
|
913
|
+
`dispatchingOnSave` of the kernel around every binding of kind `repository`, in every module:
|
|
914
|
+
declared with `@Repository`, bound `.as('repository')`, or bound under a port that extends the
|
|
915
|
+
kernel `Repository` (which is a repository whether the module says so or not). A repository
|
|
916
|
+
adapter contains no dispatch code. The save only hands the events over; the unit of work of the
|
|
917
|
+
command dispatches them at its end.
|
|
918
|
+
|
|
919
|
+
| Question | Answer |
|
|
920
|
+
|---|---|
|
|
921
|
+
| Which methods | Every method of the repository that is handed an aggregate, or an array of them, and resolves: `save(order)`, `saveAll([a, b])`, `remove(order)`. A method that is handed no aggregate (`findById`, `remove(id)`) announces nothing: a removal that announces an event takes the aggregate |
|
|
922
|
+
| Which events, in which order | Those the aggregates of the call recorded, pulled once the call resolved: in the order of the arguments, then of recording, and across the saves of the command in the order of the saves |
|
|
923
|
+
| When | Once the command handler has returned, inside the transaction, before the commit: the default command bus runs the handler in `UnitOfWork.run`, and a save only queues. Every handler sees every save of the command. What the domain event handlers write, the outbox included, commits or rolls back with the state |
|
|
924
|
+
| After the commit | Handlers declared `phase: 'after-commit'` run once the transaction has committed, never when it rolled back, in the order the events were dispatched, awaited before the command answers. At most once: a crash between the commit and the run loses them |
|
|
925
|
+
| A `before-commit` handler fails | The command fails with what it threw and the unit of work rolls back. The events still waiting are dropped, no `after-commit` handler runs, and the aggregate instances have given their events: do not save them again, a retry reloads them |
|
|
926
|
+
| An `after-commit` handler fails | The command still succeeds. The failure is logged (`Logger.error`, "A domain event handler failed after the commit", with `domainEvent`, `errorType`, `errorCode`) and handed to the `ErrorReporter` under the operation `after-commit <event name>`; the other handlers still run |
|
|
927
|
+
| The save fails | Nothing is pulled: the events stay on the aggregate. The command fails, and the events queued by earlier saves are dropped |
|
|
928
|
+
| The command handler throws after a save, or the commit fails | The queued events are dropped: no handler of either phase for a handler that threw, no `after-commit` handler for a commit that failed; nothing stored, no outbox row |
|
|
929
|
+
| A handler saves another aggregate | Before the commit: its events wait behind the ones already queued and are dispatched in recording order, without recursion. After the commit: refused before the write with `SaveAfterCommitError`; send a command through the `CommandBus` instead |
|
|
930
|
+
| A handler sends a command | Before the commit, that command joins the unit of work in progress: one dispatch, at the end of the outermost, one commit. After the commit, it is a unit of work of its own |
|
|
931
|
+
| The same aggregate saved twice | The second save announces what was recorded since the first, which may be nothing |
|
|
932
|
+
| A save outside of any unit of work | Dispatches before it resolves, `before-commit` handlers then `after-commit` ones. Nothing is atomic then: the state is written, the handlers run, and a handler that fails rejects the save without undoing it |
|
|
933
|
+
| An adapter that pulls the events itself | Leaves nothing to dispatch: no double dispatch, and no dispatch at all unless the adapter does it. An adapter never calls `pullEvents()` |
|
|
934
|
+
| Order of the wrappers | Outermost first: the `decorate` hook, the instrumentation (the span of the save), the hand-over of the events, the aspects other modules put around the kind (`builder.around`), the aspects of the binding (`.with(retry(...))`). A retried save announces once |
|
|
935
|
+
|
|
936
|
+
A `before-commit` handler may only have effects the transaction covers, because the commit can
|
|
937
|
+
still fail after it ran: `domain-events/transactional-before-commit` holds it. An effect outside
|
|
938
|
+
the database is an `after-commit` handler when it may be lost, an integration event through the
|
|
939
|
+
outbox when it must not:
|
|
940
|
+
|
|
941
|
+
```ts
|
|
942
|
+
@DomainEventHandler(InvoiceIssued, { name: 'billing.invoice.issued', phase: 'after-commit' }, [Cache])
|
|
943
|
+
export class InvoiceIssuedEventHandler implements DomainEventHandler<InvoiceIssued> {
|
|
944
|
+
constructor(private readonly cache: Cache) {}
|
|
945
|
+
|
|
946
|
+
async handle(event: InvoiceIssued): Promise<void> {
|
|
947
|
+
await this.cache.delete(cacheKey<InvoiceSummary>('invoice-summary', event.aggregateId));
|
|
948
|
+
}
|
|
949
|
+
}
|
|
950
|
+
```
|
|
951
|
+
|
|
952
|
+
A `UnitOfWork` given through `kernelModule({ unitOfWork })` gets this timing for commands
|
|
953
|
+
whatever it is made of: the default command bus puts `withinUnitOfWork` of the kernel around
|
|
954
|
+
it. Built on `withinUnitOfWork` itself, as the ORM packages are, it gives the same timing to a
|
|
955
|
+
`run` called directly, the one of a consumer included.
|
|
956
|
+
|
|
957
|
+
In a test application, the adapter of a module and the fake given for a repository port both
|
|
958
|
+
announce to `app.domainEvents`, whether `kernelModule()` is listed or not. Without a container,
|
|
959
|
+
`dispatchingOnSave(repository, () => dispatcher)` of the kernel is the same wrapper, and
|
|
960
|
+
`createTestBuses().dispatching(fake)` of `@ddd-ts-kit/testing` applies it to a fake.
|
|
961
|
+
|
|
962
|
+
A module puts an aspect of its own around every binding of some kinds with
|
|
963
|
+
`builder.around(kinds, (resolve) => aspect)`: outside the aspects of each binding, inside the
|
|
964
|
+
`decorate` hook, the first declared outermost.
|
|
965
|
+
|
|
966
|
+
## Observing the application
|
|
967
|
+
|
|
968
|
+
`kernelModule({ instrumentation })` takes what observes the application from outside: an object
|
|
969
|
+
whose members are wrappers, each optional, applied where the thing is built.
|
|
970
|
+
|
|
971
|
+
| Member | Wraps |
|
|
972
|
+
|---|---|
|
|
973
|
+
| `binding` | every instance of a binding declared with a kind, outside the dispatch of domain events |
|
|
974
|
+
| `useCase` | a bus middleware, on both default buses, after the use case guard and outside the unit of work; it reads `info.context`, the module of the handler |
|
|
975
|
+
| `publisher` | the `IntegrationEventPublisher` |
|
|
976
|
+
| `consumer` | what each declared consumer is subscribed with, told its consumer name, its module and its event type |
|
|
977
|
+
| `domainEvents` | the `DomainEventDispatcher` |
|
|
978
|
+
| `job` | each run of a declared job, inside its execution scope, told the job name and its module |
|
|
979
|
+
| `errorReporter` | the `ErrorReporter` |
|
|
980
|
+
| `executionScope` | told the `ExecutionScope` once it is built |
|
|
981
|
+
|
|
982
|
+
`traceApplication()` of `@ddd-ts-kit/tracing` is such an object, and makes one readable trace
|
|
983
|
+
per action; this package depends on no tracing library. A port given through an option of
|
|
984
|
+
`kernelModule` is wrapped too; one rebound by a later module is not.
|
|
985
|
+
|
|
986
|
+
## The `decorate` hook
|
|
987
|
+
|
|
988
|
+
`createApplication({ decorate })` receives every instance a binding creates, handlers and
|
|
989
|
+
consumers included, with `{ token, context, kind? }` where `token` is the port class, and
|
|
990
|
+
returns the instance to hand out. It is the place for cross-cutting wrappers of a project.
|
|
991
|
+
Tracing comes through `kernelModule({ instrumentation })` instead, see "Observing the
|
|
992
|
+
application".
|
|
993
|
+
|
|
994
|
+
One binding takes its own wrappers with `.with(...)`, the first outermost:
|
|
995
|
+
|
|
996
|
+
```ts
|
|
997
|
+
builder.bind(PaymentGateway).toClass(HttpPaymentGateway, [PaymentSettings]).as('external').with(retry({ attempts: 3 }), timeout(2000));
|
|
998
|
+
```
|
|
999
|
+
|
|
1000
|
+
A `BindingAspect` is `(instance, { kind?, className, context, token }) => instance`, where
|
|
1001
|
+
`token` is the name of the port; the aspects of `@ddd-ts-kit/aspects` fit as they are, and its
|
|
1002
|
+
`aspects(...)` builds a `decorate` hook. The `decorate` hook comes around the aspects of a
|
|
1003
|
+
binding.
|
|
1004
|
+
|
|
1005
|
+
## The tsyringe container
|
|
1006
|
+
|
|
1007
|
+
`app.container` is the tsyringe child container of the application, for interoperability. A
|
|
1008
|
+
port is registered there under `tokenIdentity(port)`, a symbol, never under the class: tsyringe
|
|
1009
|
+
would try to build a class it is asked for, and an abstract class is not one of its tokens.
|
|
1010
|
+
|
|
1011
|
+
```ts
|
|
1012
|
+
import { tokenIdentity } from '@ddd-ts-kit/di';
|
|
1013
|
+
|
|
1014
|
+
app.container.isRegistered(tokenIdentity(Clock)); // true
|
|
1015
|
+
app.container.resolve<Clock>(tokenIdentity(Clock)); // what app.resolve(Clock) answers
|
|
1016
|
+
app.container.isRegistered(PlaceOrderHandler); // false: bound under its identity too
|
|
1017
|
+
```
|
|
1018
|
+
|
|
1019
|
+
`app.resolve(Port)` is the typed way in, and the one that reports a cycle or an unbound port
|
|
1020
|
+
with the names of who asked.
|
|
1021
|
+
|
|
1022
|
+
## Access control
|
|
1023
|
+
|
|
1024
|
+
A use case says what it requires, or says it is public: on the message itself with
|
|
1025
|
+
`@Requires(permission)` or `@Public()`, or where it is registered:
|
|
1026
|
+
|
|
1027
|
+
```ts
|
|
1028
|
+
builder.command(IssueInvoice).requires(permissions['invoice.issue']).handledBy(IssueInvoiceHandler, [InvoiceRepository, Clock]);
|
|
1029
|
+
builder.query(GetCatalog).public().handledBy(GetCatalogHandler, [Catalog]);
|
|
1030
|
+
```
|
|
1031
|
+
|
|
1032
|
+
`.requires` exists only for messages that carry an actor (`AuthorizedCommand`,
|
|
1033
|
+
`AuthorizedQuery` of the kernel). With `createApplication({ modules, authorization: 'required' })`
|
|
1034
|
+
an application whose command or query declares neither, in either way, does not start
|
|
1035
|
+
(`UNDECLARED_AUTHORIZATION`), and one that declares them without a module enforcing them does
|
|
1036
|
+
not either (`AUTHORIZATION_NOT_ENFORCED`).
|
|
1037
|
+
|
|
1038
|
+
Three classes of this package carry it:
|
|
1039
|
+
|
|
1040
|
+
| Class | Is |
|
|
1041
|
+
|---|---|
|
|
1042
|
+
| `UseCaseGuard` | the port both buses ask before anything else, the unit of work included: `check(message, next, info)`. Unbound, every message passes; an authorization module binds it |
|
|
1043
|
+
| `UseCaseDeclarations` | a value, `{ list }`: what every command and query declared, for whoever enforces it |
|
|
1044
|
+
| `DeclaredPolicies` | a value, `{ list }`: the `@Policy` classes of the contexts, each resolved by its class |
|
|
1045
|
+
|
|
1046
|
+
The enforcement itself and the authorizers live in `@ddd-ts-kit/auth` (`authorizationModule`
|
|
1047
|
+
from `@ddd-ts-kit/auth/di`). `ActorContext` is bound by `kernelModule()`; the test application
|
|
1048
|
+
takes an `actor` option, and lets every message through unless a listed module binds
|
|
1049
|
+
`UseCaseGuard`.
|
|
1050
|
+
|
|
1051
|
+
## Limits
|
|
1052
|
+
|
|
1053
|
+
- A class plays one role: one declaration per class, and an adapter that implements two ports
|
|
1054
|
+
is bound explicitly, once per port.
|
|
1055
|
+
- A class is one binding: a handler class serves one statement, a declared class one module.
|
|
1056
|
+
- A class decorator of another library that replaces the class (answers a new one) must be
|
|
1057
|
+
written below the declaration, so that the declaration lands on the class that is exported.
|
|
1058
|
+
A decorator that only annotates, as `@Injectable()` does, composes in either order.
|
|
1059
|
+
- A declaration is not inherited: a subclass declares itself or is bound explicitly.
|
|
1060
|
+
- A consumer name, a policy name, a job name and the name of a domain event are written in the
|
|
1061
|
+
declaration; they are not derived from the class, which a refactoring renames.
|
|
1062
|
+
- A port of your own is identified by its class: see "Identity" for a port loaded twice.
|
|
1063
|
+
- Discovered files are imported by nothing. In a project using discovery, tell knip that they
|
|
1064
|
+
are entries and dependency-cruiser that they are not orphans:
|
|
1065
|
+
|
|
1066
|
+
```jsonc
|
|
1067
|
+
// knip.json
|
|
1068
|
+
{ "entry": ["src/main.ts", "src/contexts/*/{application,infrastructure}/**/*.{command,query,handler,integration-handler,publisher,event-handler,policy,job,projection,repository,gateway,adapter}.ts"] }
|
|
1069
|
+
```
|
|
1070
|
+
|
|
1071
|
+
```js
|
|
1072
|
+
// .dependency-cruiser.cjs, in the pathNot of the no-orphans rule
|
|
1073
|
+
'\\.(command|query|handler|integration-handler|publisher|event-handler|policy|job|projection|repository|gateway|adapter)\\.ts$'
|
|
1074
|
+
```
|
|
1075
|
+
|
|
1076
|
+
With a barrel and `contextModule` neither setting is needed.
|