ddd-ts-kit 0.16.1 → 0.18.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.fr.md +14 -15
- package/README.md +13 -13
- package/dist/affected/reach-of.js +1 -1
- package/dist/affected/reach-of.js.map +1 -1
- package/dist/check/business-names/closing-parenthesis.d.ts +3 -0
- package/dist/check/business-names/closing-parenthesis.d.ts.map +1 -0
- package/dist/check/business-names/closing-parenthesis.js +16 -0
- package/dist/check/business-names/closing-parenthesis.js.map +1 -0
- package/dist/check/business-names/declared-bindings.d.ts +4 -0
- package/dist/check/business-names/declared-bindings.d.ts.map +1 -0
- package/dist/check/business-names/declared-bindings.js +41 -0
- package/dist/check/business-names/declared-bindings.js.map +1 -0
- package/dist/check/business-names/generic-words-of.d.ts +4 -0
- package/dist/check/business-names/generic-words-of.d.ts.map +1 -0
- package/dist/check/business-names/generic-words-of.js +7 -0
- package/dist/check/business-names/generic-words-of.js.map +1 -0
- package/dist/check/business-names/generic-words.d.ts +7 -0
- package/dist/check/business-names/generic-words.d.ts.map +1 -0
- package/dist/check/business-names/generic-words.js +26 -0
- package/dist/check/business-names/generic-words.js.map +1 -0
- package/dist/check/business-names/identifier-at.d.ts +3 -0
- package/dist/check/business-names/identifier-at.d.ts.map +1 -0
- package/dist/check/business-names/identifier-at.js +6 -0
- package/dist/check/business-names/identifier-at.js.map +1 -0
- package/dist/check/business-names/judges-names.d.ts +7 -0
- package/dist/check/business-names/judges-names.d.ts.map +1 -0
- package/dist/check/business-names/judges-names.js +14 -0
- package/dist/check/business-names/judges-names.js.map +1 -0
- package/dist/check/business-names/named-binding.d.ts +12 -0
- package/dist/check/business-names/named-binding.d.ts.map +1 -0
- package/dist/check/business-names/named-binding.js +2 -0
- package/dist/check/business-names/named-binding.js.map +1 -0
- package/dist/check/business-names/named-bindings.d.ts +4 -0
- package/dist/check/business-names/named-bindings.d.ts.map +1 -0
- package/dist/check/business-names/named-bindings.js +8 -0
- package/dist/check/business-names/named-bindings.js.map +1 -0
- package/dist/check/business-names/opening-bracket.d.ts +3 -0
- package/dist/check/business-names/opening-bracket.d.ts.map +1 -0
- package/dist/check/business-names/opening-bracket.js +16 -0
- package/dist/check/business-names/opening-bracket.js.map +1 -0
- package/dist/check/business-names/parameter-bindings.d.ts +4 -0
- package/dist/check/business-names/parameter-bindings.d.ts.map +1 -0
- package/dist/check/business-names/parameter-bindings.js +28 -0
- package/dist/check/business-names/parameter-bindings.js.map +1 -0
- package/dist/check/business-names/parameter-lists.d.ts +14 -0
- package/dist/check/business-names/parameter-lists.d.ts.map +1 -0
- package/dist/check/business-names/parameter-lists.js +109 -0
- package/dist/check/business-names/parameter-lists.js.map +1 -0
- package/dist/check/business-names/pattern-bindings.d.ts +7 -0
- package/dist/check/business-names/pattern-bindings.d.ts.map +1 -0
- package/dist/check/business-names/pattern-bindings.js +31 -0
- package/dist/check/business-names/pattern-bindings.js.map +1 -0
- package/dist/check/business-names/property-bindings.d.ts +7 -0
- package/dist/check/business-names/property-bindings.d.ts.map +1 -0
- package/dist/check/business-names/property-bindings.js +31 -0
- package/dist/check/business-names/property-bindings.js.map +1 -0
- package/dist/check/business-names/suggested-name.d.ts +8 -0
- package/dist/check/business-names/suggested-name.d.ts.map +1 -0
- package/dist/check/business-names/suggested-name.js +28 -0
- package/dist/check/business-names/suggested-name.js.map +1 -0
- package/dist/check/business-names/top-level-separator.d.ts +3 -0
- package/dist/check/business-names/top-level-separator.d.ts.map +1 -0
- package/dist/check/business-names/top-level-separator.js +23 -0
- package/dist/check/business-names/top-level-separator.js.map +1 -0
- package/dist/check/catalogue/architecture-rules.d.ts.map +1 -1
- package/dist/check/catalogue/architecture-rules.js +4 -0
- package/dist/check/catalogue/architecture-rules.js.map +1 -1
- package/dist/check/declarations/declared-event-handlers.d.ts +4 -9
- package/dist/check/declarations/declared-event-handlers.d.ts.map +1 -1
- package/dist/check/declarations/declared-event-handlers.js +5 -5
- package/dist/check/declarations/declared-event-handlers.js.map +1 -1
- package/dist/check/declarations/option-of.d.ts +0 -2
- package/dist/check/declarations/option-of.d.ts.map +1 -1
- package/dist/check/declarations/option-of.js +1 -7
- package/dist/check/declarations/option-of.js.map +1 -1
- package/dist/check/explain/authz-docs.d.ts.map +1 -1
- package/dist/check/explain/authz-docs.js +14 -1
- package/dist/check/explain/authz-docs.js.map +1 -1
- package/dist/check/explain/events-docs.d.ts.map +1 -1
- package/dist/check/explain/events-docs.js +31 -11
- package/dist/check/explain/events-docs.js.map +1 -1
- package/dist/check/explain/ports-docs.d.ts.map +1 -1
- package/dist/check/explain/ports-docs.js +11 -9
- package/dist/check/explain/ports-docs.js.map +1 -1
- package/dist/check/explain/scheduling-docs.d.ts.map +1 -1
- package/dist/check/explain/scheduling-docs.js +10 -5
- package/dist/check/explain/scheduling-docs.js.map +1 -1
- package/dist/check/explain/tree-docs.d.ts.map +1 -1
- package/dist/check/explain/tree-docs.js +11 -0
- package/dist/check/explain/tree-docs.js.map +1 -1
- package/dist/check/explain/wiring-docs.js +4 -4
- package/dist/check/explain/wiring-docs.js.map +1 -1
- package/dist/check/fixtures/authz-policy-fixtures.d.ts +3 -0
- package/dist/check/fixtures/authz-policy-fixtures.d.ts.map +1 -0
- package/dist/check/fixtures/authz-policy-fixtures.js +97 -0
- package/dist/check/fixtures/authz-policy-fixtures.js.map +1 -0
- package/dist/check/fixtures/business-name-fixtures.d.ts +3 -0
- package/dist/check/fixtures/business-name-fixtures.d.ts.map +1 -0
- package/dist/check/fixtures/business-name-fixtures.js +98 -0
- package/dist/check/fixtures/business-name-fixtures.js.map +1 -0
- package/dist/check/fixtures/declarations-fixtures.d.ts.map +1 -1
- package/dist/check/fixtures/declarations-fixtures.js +2 -42
- package/dist/check/fixtures/declarations-fixtures.js.map +1 -1
- package/dist/check/fixtures/declared.d.ts.map +1 -1
- package/dist/check/fixtures/declared.js +3 -3
- package/dist/check/fixtures/declared.js.map +1 -1
- package/dist/check/fixtures/domain-events-fixtures.js +1 -1
- package/dist/check/fixtures/domain-events-fixtures.js.map +1 -1
- package/dist/check/fixtures/fixtures.d.ts.map +1 -1
- package/dist/check/fixtures/fixtures.js +4 -0
- package/dist/check/fixtures/fixtures.js.map +1 -1
- package/dist/check/rules/authz-policy-asked.d.ts +3 -0
- package/dist/check/rules/authz-policy-asked.d.ts.map +1 -0
- package/dist/check/rules/authz-policy-asked.js +184 -0
- package/dist/check/rules/authz-policy-asked.js.map +1 -0
- package/dist/check/rules/declarations-role-declared.d.ts.map +1 -1
- package/dist/check/rules/declarations-role-declared.js +4 -35
- package/dist/check/rules/declarations-role-declared.js.map +1 -1
- package/dist/check/rules/events-consumer-idempotent.js +3 -3
- package/dist/check/rules/events-consumer-idempotent.js.map +1 -1
- package/dist/check/rules/naming-business-name.d.ts +3 -0
- package/dist/check/rules/naming-business-name.d.ts.map +1 -0
- package/dist/check/rules/naming-business-name.js +41 -0
- package/dist/check/rules/naming-business-name.js.map +1 -0
- package/dist/config/config.d.ts +8 -0
- package/dist/config/config.d.ts.map +1 -1
- package/dist/config/parse-business-names.d.ts +4 -0
- package/dist/config/parse-business-names.d.ts.map +1 -0
- package/dist/config/parse-business-names.js +14 -0
- package/dist/config/parse-business-names.js.map +1 -0
- package/dist/config/parse-config.d.ts.map +1 -1
- package/dist/config/parse-config.js +2 -0
- package/dist/config/parse-config.js.map +1 -1
- package/dist/config/serialize-config.js +9 -1
- package/dist/config/serialize-config.js.map +1 -1
- package/dist/generate/emit.d.ts.map +1 -1
- package/dist/generate/emit.js +2 -14
- package/dist/generate/emit.js.map +1 -1
- package/dist/generate/kinds/ask-policy-in-generated-use-case.d.ts +9 -0
- package/dist/generate/kinds/ask-policy-in-generated-use-case.d.ts.map +1 -0
- package/dist/generate/kinds/ask-policy-in-generated-use-case.js +102 -0
- package/dist/generate/kinds/ask-policy-in-generated-use-case.js.map +1 -0
- package/dist/generate/kinds/assert-declares-its-name.d.ts +9 -0
- package/dist/generate/kinds/assert-declares-its-name.d.ts.map +1 -0
- package/dist/generate/kinds/assert-declares-its-name.js +16 -0
- package/dist/generate/kinds/assert-declares-its-name.js.map +1 -0
- package/dist/generate/kinds/command-context.d.ts +12 -0
- package/dist/generate/kinds/command-context.d.ts.map +1 -0
- package/dist/generate/kinds/command-context.js +18 -0
- package/dist/generate/kinds/command-context.js.map +1 -0
- package/dist/generate/kinds/command-kind.d.ts.map +1 -1
- package/dist/generate/kinds/command-kind.js +7 -24
- package/dist/generate/kinds/command-kind.js.map +1 -1
- package/dist/generate/kinds/command-outputs.d.ts +6 -0
- package/dist/generate/kinds/command-outputs.d.ts.map +1 -0
- package/dist/generate/kinds/command-outputs.js +13 -0
- package/dist/generate/kinds/command-outputs.js.map +1 -0
- package/dist/generate/kinds/declares-its-name.d.ts +5 -0
- package/dist/generate/kinds/declares-its-name.d.ts.map +1 -0
- package/dist/generate/kinds/declares-its-name.js +7 -0
- package/dist/generate/kinds/declares-its-name.js.map +1 -0
- package/dist/generate/kinds/event-file-of.d.ts +5 -0
- package/dist/generate/kinds/event-file-of.d.ts.map +1 -0
- package/dist/generate/kinds/event-file-of.js +5 -0
- package/dist/generate/kinds/event-file-of.js.map +1 -0
- package/dist/generate/kinds/event-handler-kind.d.ts.map +1 -1
- package/dist/generate/kinds/event-handler-kind.js +2 -0
- package/dist/generate/kinds/event-handler-kind.js.map +1 -1
- package/dist/generate/kinds/event-name-reference.d.ts +4 -3
- package/dist/generate/kinds/event-name-reference.d.ts.map +1 -1
- package/dist/generate/kinds/event-name-reference.js +6 -6
- package/dist/generate/kinds/event-name-reference.js.map +1 -1
- package/dist/generate/kinds/integration-event-kind.d.ts.map +1 -1
- package/dist/generate/kinds/integration-event-kind.js +2 -0
- package/dist/generate/kinds/integration-event-kind.js.map +1 -1
- package/dist/generate/kinds/policy-kind.d.ts.map +1 -1
- package/dist/generate/kinds/policy-kind.js +2 -0
- package/dist/generate/kinds/policy-kind.js.map +1 -1
- package/dist/generate/kinds/query-context.d.ts +9 -0
- package/dist/generate/kinds/query-context.d.ts.map +1 -0
- package/dist/generate/kinds/query-context.js +27 -0
- package/dist/generate/kinds/query-context.js.map +1 -0
- package/dist/generate/kinds/query-kind.d.ts.map +1 -1
- package/dist/generate/kinds/query-kind.js +16 -51
- package/dist/generate/kinds/query-kind.js.map +1 -1
- package/dist/generate/kinds/query-outputs.d.ts +8 -0
- package/dist/generate/kinds/query-outputs.d.ts.map +1 -0
- package/dist/generate/kinds/query-outputs.js +20 -0
- package/dist/generate/kinds/query-outputs.js.map +1 -0
- package/dist/generate/kinds/reader-file.d.ts +8 -0
- package/dist/generate/kinds/reader-file.d.ts.map +1 -0
- package/dist/generate/kinds/reader-file.js +9 -0
- package/dist/generate/kinds/reader-file.js.map +1 -0
- package/dist/generate/kinds/route/write-controller.d.ts.map +1 -1
- package/dist/generate/kinds/route/write-controller.js +2 -1
- package/dist/generate/kinds/route/write-controller.js.map +1 -1
- package/dist/generate/kinds/route-kind.d.ts.map +1 -1
- package/dist/generate/kinds/route-kind.js +2 -1
- package/dist/generate/kinds/route-kind.js.map +1 -1
- package/dist/generate/mark-policy-questions.d.ts +10 -0
- package/dist/generate/mark-policy-questions.d.ts.map +1 -0
- package/dist/generate/mark-policy-questions.js +76 -0
- package/dist/generate/mark-policy-questions.js.map +1 -0
- package/dist/generate/permission-pattern.d.ts +4 -0
- package/dist/generate/permission-pattern.d.ts.map +1 -0
- package/dist/generate/permission-pattern.js +7 -0
- package/dist/generate/permission-pattern.js.map +1 -0
- package/dist/generate/refined-by-policy.d.ts +4 -0
- package/dist/generate/refined-by-policy.d.ts.map +1 -0
- package/dist/generate/refined-by-policy.js +9 -0
- package/dist/generate/refined-by-policy.js.map +1 -0
- package/dist/generate/rendered.d.ts +5 -0
- package/dist/generate/rendered.d.ts.map +1 -0
- package/dist/generate/rendered.js +18 -0
- package/dist/generate/rendered.js.map +1 -0
- package/dist/generate/searched-elements.d.ts +13 -0
- package/dist/generate/searched-elements.d.ts.map +1 -0
- package/dist/generate/searched-elements.js +63 -0
- package/dist/generate/searched-elements.js.map +1 -0
- package/dist/generate/wire.d.ts.map +1 -1
- package/dist/generate/wire.js +3 -25
- package/dist/generate/wire.js.map +1 -1
- package/dist/generate/wirings.d.ts +8 -0
- package/dist/generate/wirings.d.ts.map +1 -0
- package/dist/generate/wirings.js +28 -0
- package/dist/generate/wirings.js.map +1 -0
- package/dist/init/copilot-instruction.d.ts +6 -0
- package/dist/init/copilot-instruction.d.ts.map +1 -0
- package/dist/init/copilot-instruction.js +15 -0
- package/dist/init/copilot-instruction.js.map +1 -0
- package/dist/init/files/guidance-files.d.ts +1 -0
- package/dist/init/files/guidance-files.d.ts.map +1 -1
- package/dist/init/files/guidance-files.js +4 -3
- package/dist/init/files/guidance-files.js.map +1 -1
- package/dist/init/files/layer-rules.d.ts +3 -0
- package/dist/init/files/layer-rules.d.ts.map +1 -0
- package/dist/init/files/layer-rules.js +11 -0
- package/dist/init/files/layer-rules.js.map +1 -0
- package/dist/init/files/pipeline-files.d.ts.map +1 -1
- package/dist/init/files/pipeline-files.js +2 -0
- package/dist/init/files/pipeline-files.js.map +1 -1
- package/dist/init/json-object.d.ts +3 -0
- package/dist/init/json-object.d.ts.map +1 -0
- package/dist/init/json-object.js +12 -0
- package/dist/init/json-object.js.map +1 -0
- package/dist/init/manifest/versions.js +24 -24
- package/dist/init/run-init.d.ts.map +1 -1
- package/dist/init/run-init.js +4 -0
- package/dist/init/run-init.js.map +1 -1
- package/dist/init/write-agent-settings.d.ts.map +1 -1
- package/dist/init/write-agent-settings.js +3 -10
- package/dist/init/write-agent-settings.js.map +1 -1
- package/dist/init/write-copilot-instructions.d.ts +7 -0
- package/dist/init/write-copilot-instructions.d.ts.map +1 -0
- package/dist/init/write-copilot-instructions.js +16 -0
- package/dist/init/write-copilot-instructions.js.map +1 -0
- package/dist/init/write-copilot-settings.d.ts +8 -0
- package/dist/init/write-copilot-settings.d.ts.map +1 -0
- package/dist/init/write-copilot-settings.js +36 -0
- package/dist/init/write-copilot-settings.js.map +1 -0
- package/dist/markers/index.d.ts +1 -0
- package/dist/markers/index.d.ts.map +1 -1
- package/dist/markers/index.js +1 -0
- package/dist/markers/index.js.map +1 -1
- package/dist/markers/replace-in-region.d.ts +10 -0
- package/dist/markers/replace-in-region.d.ts.map +1 -0
- package/dist/markers/replace-in-region.js +30 -0
- package/dist/markers/replace-in-region.js.map +1 -0
- package/dist/naming/index.d.ts +2 -0
- package/dist/naming/index.d.ts.map +1 -1
- package/dist/naming/index.js +2 -0
- package/dist/naming/index.js.map +1 -1
- package/dist/naming/plural.d.ts +3 -0
- package/dist/naming/plural.d.ts.map +1 -0
- package/dist/naming/plural.js +8 -0
- package/dist/naming/plural.js.map +1 -0
- package/dist/naming/singular.d.ts +3 -0
- package/dist/naming/singular.d.ts.map +1 -0
- package/dist/naming/singular.js +11 -0
- package/dist/naming/singular.js.map +1 -0
- package/dist/naming/to-name.d.ts.map +1 -1
- package/dist/naming/to-name.js +1 -6
- package/dist/naming/to-name.js.map +1 -1
- package/dist/sdk/agent-instruction.d.ts +1 -1
- package/dist/sdk/agent-instruction.js +1 -1
- package/dist/sdk/instruct-agents.d.ts +1 -1
- package/dist/sdk/instruct-agents.d.ts.map +1 -1
- package/dist/sdk/instruct-agents.js +12 -8
- package/dist/sdk/instruct-agents.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +24 -3
- package/templates/command/handler.declared.ts.tpl +32 -3
- package/templates/command/handler.spec.buses.ts.tpl +24 -1
- package/templates/command/handler.ts.tpl +32 -3
- package/templates/command/wiring.manual.tpl +2 -2
- package/templates/command/wiring.tsyringe.tpl +5 -2
- package/templates/controller/controller.search.ts.tpl +1 -1
- package/templates/event-handler/event-handler.declared.ts.tpl +1 -1
- package/templates/init/AGENTS.md.tpl +46 -0
- package/templates/init/CLAUDE.md.tpl +2 -45
- package/templates/init/README.md.tpl +1 -1
- package/templates/init/rules/application.md.tpl +2 -2
- package/templates/init/rules/domain.md.tpl +1 -1
- package/templates/init/scripts/events.ts.tpl +6 -6
- package/templates/init/scripts/openapi.ts.tpl +2 -2
- package/templates/integration-event/publisher.declared.ts.tpl +1 -1
- package/templates/pipeline/github/workflows/copilot-setup-steps.yml +26 -0
- package/templates/pipeline/package-scripts.json +1 -1
- package/templates/policy/policy.declared.ts.tpl +1 -1
- package/templates/policy/policy.ts.tpl +1 -1
- package/templates/presets/identity/context/application/integration/publishers/account-deactivated.publisher.ts.tpl +1 -1
- package/templates/presets/identity/context/application/integration/publishers/delegation-granted.publisher.ts.tpl +1 -1
- package/templates/presets/identity/context/application/integration/publishers/delegation-revoked.publisher.ts.tpl +1 -1
- package/templates/presets/identity/context/application/integration/publishers/member-joined.publisher.ts.tpl +1 -1
- package/templates/presets/identity/context/application/integration/publishers/member-removed.publisher.ts.tpl +1 -1
- package/templates/presets/identity/context/application/integration/publishers/member-role-changed.publisher.ts.tpl +1 -1
- package/templates/presets/identity/context/application/integration/publishers/organisation-opened.publisher.ts.tpl +1 -1
- package/templates/presets/identity/context/infrastructure/persistence/drizzle-search-members-reader.repository.drizzle.ts.tpl +2 -2
- package/templates/presets/identity/context/presentation/http/organisations/search-members/search-members.controller.ts.tpl +2 -2
- package/templates/presets/identity/context/test/integration/search-members-reader.spec.ts.tpl +1 -1
- package/templates/query/handler.declared.ts.tpl +19 -0
- package/templates/query/handler.spec.buses.ts.tpl +21 -0
- package/templates/query/handler.ts.tpl +19 -0
- package/templates/query/search.reader.fake.ts.tpl +1 -1
- package/templates/query/search.reader.in-memory.ts.tpl +20 -18
- package/templates/query/wiring.manual.tpl +1 -1
- package/templates/query/wiring.tsyringe.tpl +4 -1
- package/templates/route/route.search.ts.tpl +1 -1
|
@@ -1,46 +1,3 @@
|
|
|
1
|
-
|
|
1
|
+
@AGENTS.md
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
## Commands
|
|
6
|
-
|
|
7
|
-
- `npm run verify`: types, lint, format, tests with the coverage thresholds of `ddd-ts-kit.config.json` (per context and layer), architecture rules, import graph, dead code, documentation drift. Once, before a commit that closes a slice. A layer under its threshold gets the missing test, never an exclusion.
|
|
8
|
-
- `npx ddd-ts-kit check --changed-since <ref>`: the architecture rules on what changed; `npx ddd-ts-kit explain <rule-id>` gives the reason and an example.
|
|
9
|
-
- `npx ddd-ts-kit list --json`: the map of the contexts. `npx ddd-ts-kit describe <context> --json`: one context as data (use cases, routes, events, ports, dependencies).
|
|
10
|
-
- `npx ddd-ts-kit status`: the next step of the plan, the slices by status, the markers left per context. `npx ddd-ts-kit todo [<context>]`: each remaining `TODO(domain)` and `it.todo` as `file:line`.
|
|
11
|
-
- `npx ddd-ts-kit docs`: regenerates the context map and the generated sections of each `CONTEXT.md`; `npm run docs:generate` the OpenAPI contract, the forms and the event catalog.
|
|
12
|
-
|
|
13
|
-
## Read less
|
|
14
|
-
|
|
15
|
-
1. `describe <context> --json` and the `CONTEXT.md` of the context before the tree: do not browse `{{config.sourceRoot}}/` to learn what exists.
|
|
16
|
-
2. Open only what the `next` list of a generator (`file:line`) or a violation names.
|
|
17
|
-
3. `check --changed-since <ref>` and the tests of the context while iterating; `npm run verify` once at the end.
|
|
18
|
-
|
|
19
|
-
## Rules by layer
|
|
20
|
-
|
|
21
|
-
Loaded with the files they apply to, from `.claude/rules/`: `domain.md` (`domain/`, the shared kernel), `application.md`, `published-language.md`, `infrastructure.md`, `presentation.md` (and the OpenAPI contract), `platform.md` (`{{config.platformDir}}/`, `main.ts`, `apps/`{{#if style.explicit}}, module files{{/if}}), `tests.md` (specs, fakes, builders, `test/`).
|
|
22
|
-
|
|
23
|
-
## Generated files
|
|
24
|
-
|
|
25
|
-
Create every file of a role with `npx ddd-ts-kit generate <kind> ... --json`, then fill the `TODO(domain)` places it lists; never write one by hand, never restructure what a generator wrote.
|
|
26
|
-
{{#if assembly.barrel}}
|
|
27
|
-
Never edit `<context>.declarations.ts`: `generate`, `docs` and `npm run dev` rewrite it, from the role files and from the constructor signatures (it registers what each constructor is injected). After changing a constructor by hand, `npx ddd-ts-kit docs`.
|
|
28
|
-
{{/if}}
|
|
29
|
-
{{#if assembly.discovery}}
|
|
30
|
-
Never edit `<context>.dependencies.ts`: `generate`, `docs` and `npm run dev` rewrite it from the constructor signatures (it registers what each constructor is injected). After changing a constructor by hand, `npx ddd-ts-kit docs`.
|
|
31
|
-
{{/if}}
|
|
32
|
-
Never edit the contracts under `openapi/` (`openapi/openapi.yaml`, one file per document when `openapi.documents` splits it, the forms of `openapi/forms/`), `docs/integration/asyncapi.yaml`, `docs/integration/event-catalog.md`, `{{config.platformDir}}/http/openapi-documents.ts`, `.dependency-cruiser.cjs` or the generated sections of a `CONTEXT.md`: change the code or the config, then `npm run docs:generate`, `npx ddd-ts-kit docs` and `npx ddd-ts-kit depcruise-config --write`. `stored-events.lock.json` is written by `npx ddd-ts-kit docs --stored-events` once the shape of a version is final (`docs --check` fails while one is unrecorded), never by hand: a stored shape changes only by a new version with its upcaster (`event-sourcing/upcaster-required`).
|
|
33
|
-
{{#if sdk.on}}
|
|
34
|
-
{{sdk.instruction}}
|
|
35
|
-
{{/if}}
|
|
36
|
-
Imports: a file of the project is imported by its alias from the project root, never by a relative path, the same directory included: `{{alias.source}}/...` for `{{config.sourceRoot}}/`, `{{alias.test}}/...` for `test/`, `{{alias.scripts}}/...` for `scripts/`, {{#if alias.extensionless}}never ending in `.js` or `.ts`, and naming a file, never a directory{{/if}}{{#unless alias.extensionless}}always ending in `.js`{{/unless}} (`imports/alias-only`, `imports/extension`; `npm run lint:fix` rewrites a relative import and a wrong ending). The alias is declared once, under `paths` in `tsconfig.json`, from `importAlias` of `ddd-ts-kit.config.json`, and the ending follows `importExtension`: never add a second entry or mapping, never change `module` or `moduleResolution` in `tsconfig.json`, and never compile without `npm run build`, which rewrites the aliases of `dist/` into relative paths ending in `.js` for Node (`imports/alias-declared`).
|
|
37
|
-
Everywhere: one exported class, interface or function per file, named after it in kebab-case; at most 350 lines a file, 30 lines and 3 parameters a function; `private` members, never `#name`; a blank line before a `return` and around `if`, `for`, `try` (`npm run lint:fix`); braces around the body of every `if`, `else`, `for`, `while` and `do`, on its own lines: a branch is visible at a glance, and a statement added to it later cannot silently fall outside it. `npm run format` (Biome, line width 120) lays out every file: do not spend tokens on line breaks.
|
|
38
|
-
|
|
39
|
-
## TypeScript
|
|
40
|
-
|
|
41
|
-
The compiler is the native TypeScript 7, installed as `@typescript/native`; `typescript` 6 is there for the tools that need its API (ESLint, dependency-cruiser). Use `npm run typecheck` and `npm run build`, never `npx tsc`, which would run version 6.
|
|
42
|
-
|
|
43
|
-
## Workflow
|
|
44
|
-
|
|
45
|
-
Follow the recommended steps in order, the supported path (`README.md`, "Recommended steps"): discover the domain, bounded contexts, glossaries, context map, bootstrap, plan the slices, build slice by slice, quality gate, document. `npx ddd-ts-kit status` says where the project stands. When asked to skip a step, say which one and what it costs, and record it in `docs/architecture/plan.md`.
|
|
46
|
-
With the ddd-ts-kit plugin: `/ddd-ts-kit:start` for a new project or to resume one, `/ddd-ts-kit:glossary` before a new name, `/ddd-ts-kit:use-case` for a feature, `/ddd-ts-kit:document` for the contracts, `/ddd-ts-kit:quality-gate` before a pull or merge request.
|
|
3
|
+
The rules by layer of `.claude/rules/` load with the files they apply to.
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# {{project.name}}
|
|
2
2
|
|
|
3
3
|
Domain-Driven Design and Clean Architecture on [ddd-ts-kit](https://www.npmjs.com/package/ddd-ts-kit).
|
|
4
|
-
The rules are in `
|
|
4
|
+
The rules are in `AGENTS.md`, which every coding agent reads, and by layer in `.claude/rules/` and `.github/instructions/`; `npx ddd-ts-kit explain <rule-id>` explains each one.
|
|
5
5
|
|
|
6
6
|
## Run it
|
|
7
7
|
|
|
@@ -6,12 +6,12 @@ paths:
|
|
|
6
6
|
# Application layer
|
|
7
7
|
|
|
8
8
|
- Imports `domain/`, `application/`, published languages, `@ddd-ts-kit/kernel` and `@ddd-ts-kit/auth` only (`layer/application-inward`): never `infrastructure/`, `presentation/`, a framework or a driver.
|
|
9
|
-
- One use case per folder, `commands/<use-case>/` or `queries/<use-case>/`: the message, its handler, a `*.result.ts` when the answer has a shape of its own. The handler has one public method, `execute` (`application/handler-shape`): it loads or builds, calls ONE intention method, `await repository.save(aggregate)`, and returns at most an identifier.
|
|
9
|
+
- One use case per folder, `commands/<use-case>/` or `queries/<use-case>/`: the message, its handler, a `*.result.ts` when the answer has a shape of its own. The handler has one public method, `execute` (`application/handler-shape`): it loads or builds, calls ONE intention method, `await repository.save(aggregate)`, and returns at most an identifier. Each binding is named after the concept it holds, `const invoice = await this.invoiceRepository.findById(command.invoiceId)`, `for (const reminder of dueReminders)`, never `target`, `data` or `item` (`naming/business-name`).
|
|
10
10
|
- Events leave with the save: never `pullEvents()`, never a `DomainEventDispatcher` here (`domain-events/dispatched-on-save`). A reaction is a class of `event-handlers/` or a publisher of `integration/publishers/`. It runs at the end of the unit of work, before the commit, and depends only on what the transaction covers (`domain-events/transactional-before-commit`): repositories, the integration event publisher, the command bus. An effect outside the database is a handler of the `after-commit` phase (`generate event-handler ... --phase after-commit`: at most once, never saves, sends a command) or, when it must not be lost, the handler of an integration event.
|
|
11
11
|
- Nothing here reads the event store: an event-sourced aggregate is loaded and saved through its repository port like any other (`event-sourcing/store-behind-repository`). A projection writes its read model and never a stream (`event-sourcing/projection-never-appends`); one fed by the store is declared `@Projection(name, { feed: 'event-store' })`.
|
|
12
12
|
{{#if style.declared}}
|
|
13
13
|
- Every command and query carries `@Requires(permission)` or `@Public()` (`authz/declared`), and the actor is in the message (`authz/actor-explicit`). Permissions live in `authorization/<context>.permissions.ts`, rules that need loaded state in `authorization/*.policy.ts`.
|
|
14
|
-
- The decorator is the registration (`declarations/role-declared`): `@CommandHandler(Message)`, `@QueryHandler(Message)`, `@DomainEventHandler(Event
|
|
14
|
+
- The decorator is the registration (`declarations/role-declared`): `@CommandHandler(Message)`, `@QueryHandler(Message)`, `@DomainEventHandler(Event)` (the name is the static `eventName` of the event class), `@IntegrationEventHandler(definition, { consumer })`, `@Scheduled('<context>.<job>', schedule)`, `@Background(options)` (the name is the static `jobName`), `@Projection('<context>.<name>')`, `@Policy(permission, { name })`. Names are string literals.
|
|
15
15
|
- The constructor is the one statement of what a class depends on (`declarations/constructor-injection`): each parameter is typed by one port class, with no `?`, default value, rest or destructuring, and no list repeats it in the decorator. To add a dependency, add the parameter: `npx ddd-ts-kit docs` reads the signature into the generated file of the context (`npm run dev` does it on every change), never edit that file.
|
|
16
16
|
{{/if}}
|
|
17
17
|
{{#if style.explicit}}
|
|
@@ -7,7 +7,7 @@ paths:
|
|
|
7
7
|
# Domain layer
|
|
8
8
|
|
|
9
9
|
- Imports only its own `domain/`, the shared kernel and `@ddd-ts-kit/kernel` (`layer/domain-purity`): no framework, no decorator, no validation library, no `node:` module.
|
|
10
|
-
- Names come from the glossary of `CONTEXT.md`, written first (`language/glossary-term`); no `Manager`, `Helper`, `Data`, `Info`, `Model` (`language/no-technical-name`).
|
|
10
|
+
- Names come from the glossary of `CONTEXT.md`, written first (`language/glossary-term`); no `Manager`, `Helper`, `Data`, `Info`, `Model` (`language/no-technical-name`). A variable, a parameter or a property is named after the concept it holds, `appointment`, `calendar`, never `target`, `data`, `item` (`naming/business-name`).
|
|
11
11
|
- Aggregate: private constructor, `create` for birth, `rehydrate` for loading, state changed by methods named after the business intention, no setter, no public mutable property (`domain/aggregate-encapsulation`). One aggregate per transaction; another one is referenced by its id.
|
|
12
12
|
- A status that moves through a lifecycle is declared once as a state machine, `state-machines/<aggregate>.state-machine.ts` (`generate state-machine <context> <Aggregate> --value .. --transition ..`): the aggregate writes its status only from `machine.next(..)` or `machine.start(..)` (`lifecycle/status-through-machine`), and a forbidden transition is refused by a static factory of its error class (`refused`).
|
|
13
13
|
- A value object is a class extending `ValueObject` of the kernel, one per `value-objects/*.vo.ts` (`npx ddd-ts-kit generate value-object <context> <Name>`): a private constructor, static factories that check its rules (`create`, or named after what they build: `of`, `between`), its operations as methods that return a new instance. It is immutable and compared with `equals` (`domain/value-object-immutability`). An email, an amount, a period are types, not strings.{{#if values.classes}} A value is never a function that builds a plain object, nor an exported object literal (`domain/value-object-class`): the class compares by value, checks its invariants once and is recognised with `instanceof` by a mapper. A function of `domain/` or of the shared kernel that answers a record of data, `({ amount, currency })`, is reported; one that answers value objects it builds (`Money.of(..)`), derives a record with a spread, or answers functions (a specification) is not, and a decision is a refusal thrown by a factory or a value object.{{/if}}
|
|
@@ -9,7 +9,7 @@ import { contractFormat, stringifyContract } from 'ddd-ts-kit/dist/contracts.js'
|
|
|
9
9
|
// published language; with --check, fails when a committed file differs. Both files are build
|
|
10
10
|
// products: never edit them by hand.
|
|
11
11
|
const contextsDirectory = '{{config.contextsDir}}';
|
|
12
|
-
const
|
|
12
|
+
const catalog = path.resolve('docs/integration');
|
|
13
13
|
const format = contractFormat();
|
|
14
14
|
const other = format === 'yaml' ? 'asyncapi.json' : 'asyncapi.yaml';
|
|
15
15
|
|
|
@@ -34,17 +34,17 @@ const generated: Readonly<Record<string, string>> = {
|
|
|
34
34
|
if (process.argv.includes('--check')) {
|
|
35
35
|
reportStale();
|
|
36
36
|
} else {
|
|
37
|
-
fs.mkdirSync(
|
|
38
|
-
fs.rmSync(path.join(
|
|
37
|
+
fs.mkdirSync(catalog, { recursive: true });
|
|
38
|
+
fs.rmSync(path.join(catalog, other), { force: true });
|
|
39
39
|
|
|
40
40
|
for (const [file, content] of Object.entries(generated)) {
|
|
41
|
-
fs.writeFileSync(path.join(
|
|
41
|
+
fs.writeFileSync(path.join(catalog, file), content);
|
|
42
42
|
}
|
|
43
43
|
}
|
|
44
44
|
|
|
45
45
|
function reportStale(): void {
|
|
46
46
|
for (const [file, content] of Object.entries(generated)) {
|
|
47
|
-
const committed = fs.existsSync(path.join(
|
|
47
|
+
const committed = fs.existsSync(path.join(catalog, file)) ? fs.readFileSync(path.join(catalog, file), 'utf8') : '';
|
|
48
48
|
|
|
49
49
|
if (committed.replaceAll('\r\n', '\n') !== content) {
|
|
50
50
|
console.error('docs/integration/' + file + ' is out of date: run the script `docs:generate` and commit it.');
|
|
@@ -52,7 +52,7 @@ function reportStale(): void {
|
|
|
52
52
|
}
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
-
if (fs.existsSync(path.join(
|
|
55
|
+
if (fs.existsSync(path.join(catalog, other))) {
|
|
56
56
|
console.error('docs/integration/' + other + ' is in the other format: run the script `docs:generate`.');
|
|
57
57
|
process.exitCode = 1;
|
|
58
58
|
}
|
|
@@ -65,8 +65,8 @@ if (process.argv.includes('--check')) {
|
|
|
65
65
|
|
|
66
66
|
function reportDrift(): void {
|
|
67
67
|
for (const [file, content] of generated) {
|
|
68
|
-
const
|
|
69
|
-
const committed = fs.existsSync(
|
|
68
|
+
const contract = path.join(directory, file);
|
|
69
|
+
const committed = fs.existsSync(contract) ? fs.readFileSync(contract, 'utf8') : '';
|
|
70
70
|
|
|
71
71
|
if (committed.replaceAll('\r\n', '\n') !== content) {
|
|
72
72
|
console.error(`openapi/${file} is out of date: run the script \`docs:generate\` and commit it.`);
|
|
@@ -3,7 +3,7 @@ import { DomainEventHandler } from '@ddd-ts-kit/kernel/declarations';
|
|
|
3
3
|
import { {{event.pascal}} } from '{{alias.context}}/domain/events/{{event.kebab}}.event{{alias.ext}}';
|
|
4
4
|
import { {{event.camel}}V{{event.version}} } from '{{alias.context}}/published-language/{{event.kebab}}.v{{event.version}}.integration-event{{alias.ext}}';
|
|
5
5
|
|
|
6
|
-
@DomainEventHandler({{event.pascal}}
|
|
6
|
+
@DomainEventHandler({{event.pascal}})
|
|
7
7
|
export class {{publisher.pascal}}Publisher implements DomainEventHandler<{{event.pascal}}> {
|
|
8
8
|
constructor(
|
|
9
9
|
private readonly publisher: IntegrationEventPublisher,
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
name: Copilot setup steps
|
|
2
|
+
|
|
3
|
+
# The environment the Copilot cloud agent starts in: Node and the dependencies of the lockfile,
|
|
4
|
+
# so that the checks and the generators of the kit answer at once. Copilot reads this file from
|
|
5
|
+
# the default branch, and only a job of this name; the runs below check it when it changes.
|
|
6
|
+
on:
|
|
7
|
+
workflow_dispatch:
|
|
8
|
+
push:
|
|
9
|
+
paths:
|
|
10
|
+
- .github/workflows/copilot-setup-steps.yml
|
|
11
|
+
pull_request:
|
|
12
|
+
paths:
|
|
13
|
+
- .github/workflows/copilot-setup-steps.yml
|
|
14
|
+
|
|
15
|
+
permissions:
|
|
16
|
+
contents: read
|
|
17
|
+
|
|
18
|
+
jobs:
|
|
19
|
+
copilot-setup-steps:
|
|
20
|
+
runs-on: ubuntu-latest
|
|
21
|
+
timeout-minutes: 15
|
|
22
|
+
permissions:
|
|
23
|
+
contents: read
|
|
24
|
+
steps:
|
|
25
|
+
- uses: actions/checkout@v7
|
|
26
|
+
- uses: ./.github/actions/setup
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
"@biomejs/biome": "2.5.15",
|
|
24
24
|
"@commitlint/cli": "^21.2.3",
|
|
25
25
|
"@commitlint/config-conventional": "^21.2.3",
|
|
26
|
-
"@ddd-ts-kit/eslint-plugin": "^0.
|
|
26
|
+
"@ddd-ts-kit/eslint-plugin": "^0.18.0",
|
|
27
27
|
"@eslint/js": "^10.0.1",
|
|
28
28
|
"@typescript/native": "npm:typescript@~7.0.2",
|
|
29
29
|
"@vitest/coverage-v8": "5.0.3",
|
|
@@ -2,7 +2,7 @@ import { isAuthenticated, type Actor } from '@ddd-ts-kit/kernel';
|
|
|
2
2
|
import { Policy } from '@ddd-ts-kit/kernel/declarations';
|
|
3
3
|
import { {{context.camel}}Permissions } from '{{alias.context}}/application/authorization/{{context.kebab}}.permissions{{alias.ext}}';
|
|
4
4
|
|
|
5
|
-
// TODO(domain): give `allows` the aggregate "{{name.words}}" reads as its second parameter, add `appliesTo(subject: unknown): boolean { return subject instanceof <Aggregate>; }`, and decide from the actor, the loaded element and `now`; the handler asks it through `Access.authorize(command.actor, permission, element)
|
|
5
|
+
// TODO(domain): give `allows` the aggregate "{{name.words}}" reads as its second parameter, add `appliesTo(subject: unknown): boolean { return subject instanceof <Aggregate>; }`, and decide from the actor, the loaded element and `now`; the handler asks it through `Access.authorize(command.actor, permission, <aggregate>)`, the loaded aggregate named after its concept (`invoice`, never `element`).
|
|
6
6
|
@Policy({{context.camel}}Permissions['{{access.permission}}'], { name: '{{context.kebab}}.{{name.kebab}}' })
|
|
7
7
|
export class {{name.pascal}}Policy {
|
|
8
8
|
allows(actor: Actor): boolean {
|
|
@@ -2,7 +2,7 @@ import { definePolicy } from '@ddd-ts-kit/auth';
|
|
|
2
2
|
import { isAuthenticated } from '@ddd-ts-kit/kernel';
|
|
3
3
|
import { {{context.camel}}Permissions } from '{{alias.context}}/application/authorization/{{context.kebab}}.permissions{{alias.ext}}';
|
|
4
4
|
|
|
5
|
-
// TODO(domain): replace `unknown` by the aggregate "{{name.words}}" reads, add `appliesTo: (subject): subject is <Aggregate> => subject instanceof <Aggregate>`, and decide from the actor, the loaded element and `now`; the handler asks it through `Access.authorize(command.actor, permission, element)
|
|
5
|
+
// TODO(domain): replace `unknown` by the aggregate "{{name.words}}" reads, add `appliesTo: (subject): subject is <Aggregate> => subject instanceof <Aggregate>`, and decide from the actor, the loaded element and `now`; the handler asks it through `Access.authorize(command.actor, permission, <aggregate>)`, the loaded aggregate named after its concept (`invoice`, never `element`).
|
|
6
6
|
export const {{name.camel}} = definePolicy<unknown>({
|
|
7
7
|
name: '{{name.kebab}}',
|
|
8
8
|
permission: {{context.camel}}Permissions['{{access.permission}}'],
|
|
@@ -4,7 +4,7 @@ import { AccountDeactivated } from '{{alias.context}}/domain/events/account-deac
|
|
|
4
4
|
import { accountDeactivatedV1 } from '{{alias.context}}/published-language/account-deactivated.v1.integration-event{{alias.ext}}';
|
|
5
5
|
|
|
6
6
|
/** Tells the other contexts, through the outbox, in the transaction of the fact. */
|
|
7
|
-
@DomainEventHandler(AccountDeactivated
|
|
7
|
+
@DomainEventHandler(AccountDeactivated)
|
|
8
8
|
export class AccountDeactivatedPublisher implements DomainEventHandler<AccountDeactivated> {
|
|
9
9
|
constructor(
|
|
10
10
|
private readonly publisher: IntegrationEventPublisher,
|
|
@@ -4,7 +4,7 @@ import { DelegationGranted } from '{{alias.context}}/domain/events/delegation-gr
|
|
|
4
4
|
import { delegationGrantedV1 } from '{{alias.context}}/published-language/delegation-granted.v1.integration-event{{alias.ext}}';
|
|
5
5
|
|
|
6
6
|
/** Tells the other contexts, through the outbox, in the transaction of the fact. */
|
|
7
|
-
@DomainEventHandler(DelegationGranted
|
|
7
|
+
@DomainEventHandler(DelegationGranted)
|
|
8
8
|
export class DelegationGrantedPublisher implements DomainEventHandler<DelegationGranted> {
|
|
9
9
|
constructor(
|
|
10
10
|
private readonly publisher: IntegrationEventPublisher,
|
|
@@ -4,7 +4,7 @@ import { DelegationRevoked } from '{{alias.context}}/domain/events/delegation-re
|
|
|
4
4
|
import { delegationRevokedV1 } from '{{alias.context}}/published-language/delegation-revoked.v1.integration-event{{alias.ext}}';
|
|
5
5
|
|
|
6
6
|
/** Tells the other contexts, through the outbox, in the transaction of the fact. */
|
|
7
|
-
@DomainEventHandler(DelegationRevoked
|
|
7
|
+
@DomainEventHandler(DelegationRevoked)
|
|
8
8
|
export class DelegationRevokedPublisher implements DomainEventHandler<DelegationRevoked> {
|
|
9
9
|
constructor(
|
|
10
10
|
private readonly publisher: IntegrationEventPublisher,
|
|
@@ -4,7 +4,7 @@ import { MemberJoined } from '{{alias.context}}/domain/events/member-joined.even
|
|
|
4
4
|
import { memberJoinedV1 } from '{{alias.context}}/published-language/member-joined.v1.integration-event{{alias.ext}}';
|
|
5
5
|
|
|
6
6
|
/** Tells the other contexts, through the outbox, in the transaction of the fact. */
|
|
7
|
-
@DomainEventHandler(MemberJoined
|
|
7
|
+
@DomainEventHandler(MemberJoined)
|
|
8
8
|
export class MemberJoinedPublisher implements DomainEventHandler<MemberJoined> {
|
|
9
9
|
constructor(
|
|
10
10
|
private readonly publisher: IntegrationEventPublisher,
|
|
@@ -4,7 +4,7 @@ import { MemberRemoved } from '{{alias.context}}/domain/events/member-removed.ev
|
|
|
4
4
|
import { memberRemovedV1 } from '{{alias.context}}/published-language/member-removed.v1.integration-event{{alias.ext}}';
|
|
5
5
|
|
|
6
6
|
/** Tells the other contexts, through the outbox, in the transaction of the fact. */
|
|
7
|
-
@DomainEventHandler(MemberRemoved
|
|
7
|
+
@DomainEventHandler(MemberRemoved)
|
|
8
8
|
export class MemberRemovedPublisher implements DomainEventHandler<MemberRemoved> {
|
|
9
9
|
constructor(
|
|
10
10
|
private readonly publisher: IntegrationEventPublisher,
|
|
@@ -4,7 +4,7 @@ import { MemberRoleChanged } from '{{alias.context}}/domain/events/member-role-c
|
|
|
4
4
|
import { memberRoleChangedV1 } from '{{alias.context}}/published-language/member-role-changed.v1.integration-event{{alias.ext}}';
|
|
5
5
|
|
|
6
6
|
/** Tells the other contexts, through the outbox, in the transaction of the fact. */
|
|
7
|
-
@DomainEventHandler(MemberRoleChanged
|
|
7
|
+
@DomainEventHandler(MemberRoleChanged)
|
|
8
8
|
export class MemberRoleChangedPublisher implements DomainEventHandler<MemberRoleChanged> {
|
|
9
9
|
constructor(
|
|
10
10
|
private readonly publisher: IntegrationEventPublisher,
|
|
@@ -4,7 +4,7 @@ import { OrganisationOpened } from '{{alias.context}}/domain/events/organisation
|
|
|
4
4
|
import { organisationOpenedV1 } from '{{alias.context}}/published-language/organisation-opened.v1.integration-event{{alias.ext}}';
|
|
5
5
|
|
|
6
6
|
/** Tells the other contexts, through the outbox, in the transaction of the fact. */
|
|
7
|
-
@DomainEventHandler(OrganisationOpened
|
|
7
|
+
@DomainEventHandler(OrganisationOpened)
|
|
8
8
|
export class OrganisationOpenedPublisher implements DomainEventHandler<OrganisationOpened> {
|
|
9
9
|
constructor(
|
|
10
10
|
private readonly publisher: IntegrationEventPublisher,
|
|
@@ -22,7 +22,7 @@ export class DrizzleSearchMembersReader implements SearchMembersReader {
|
|
|
22
22
|
const order = page.sort[0]?.direction === 'desc' ? desc : asc;
|
|
23
23
|
const current = and(eq(members.organisationId, organisationId), isNull(members.removedAt));
|
|
24
24
|
|
|
25
|
-
const
|
|
25
|
+
const currentMembers = await executorOf(this.database)
|
|
26
26
|
.select({
|
|
27
27
|
memberId: members.id,
|
|
28
28
|
accountId: members.accountId,
|
|
@@ -40,7 +40,7 @@ export class DrizzleSearchMembersReader implements SearchMembersReader {
|
|
|
40
40
|
|
|
41
41
|
const [counted] = await executorOf(this.database).select({ total: count() }).from(members).where(current);
|
|
42
42
|
|
|
43
|
-
return pageOf(
|
|
43
|
+
return pageOf(currentMembers, { total: counted?.total ?? 0 });
|
|
44
44
|
}
|
|
45
45
|
}
|
|
46
46
|
|
|
@@ -81,6 +81,6 @@ export class SearchMembersController {
|
|
|
81
81
|
}
|
|
82
82
|
}
|
|
83
83
|
|
|
84
|
-
function toLine(
|
|
85
|
-
return { ...
|
|
84
|
+
function toLine(member: SearchMembersItem): SearchMembersLine {
|
|
85
|
+
return { ...member, joinedAt: member.joinedAt.toISOString() };
|
|
86
86
|
}
|
package/templates/presets/identity/context/test/integration/search-members-reader.spec.ts.tpl
CHANGED
|
@@ -76,7 +76,7 @@ describe('{{#if orm.knex}}Knex{{/if}}{{#if orm.drizzle}}Drizzle{{/if}}SearchMemb
|
|
|
76
76
|
|
|
77
77
|
const page = await reader.search('acme', offsetPage({ limit: 1, offset: 1, sort: [{ field: 'joinedAt', direction: 'desc' }] }));
|
|
78
78
|
|
|
79
|
-
expect(page.items.map((
|
|
79
|
+
expect(page.items.map((member) => member.memberId)).toEqual(['member-1']);
|
|
80
80
|
expect(page.total).toBe(2);
|
|
81
81
|
});
|
|
82
82
|
});
|
|
@@ -1,13 +1,32 @@
|
|
|
1
|
+
{{#if access.policy}}
|
|
2
|
+
import type { Access } from '@ddd-ts-kit/auth';
|
|
3
|
+
{{/if}}
|
|
1
4
|
import { QueryHandler } from '@ddd-ts-kit/kernel/declarations';
|
|
5
|
+
{{#if access.policy}}
|
|
6
|
+
{{access.useCasePermissionsImport}}
|
|
7
|
+
{{/if}}
|
|
2
8
|
import type { {{name.pascal}}Reader } from '{{alias.context}}/application/ports/{{name.kebab}}-reader.port{{alias.ext}}';
|
|
3
9
|
import { {{name.pascal}}Query } from '{{alias.context}}/application/queries/{{name.kebab}}/{{name.kebab}}.query{{alias.ext}}';
|
|
4
10
|
import type { {{name.pascal}}Result } from '{{alias.context}}/application/queries/{{name.kebab}}/{{name.kebab}}.result{{alias.ext}}';
|
|
5
11
|
|
|
6
12
|
@QueryHandler({{name.pascal}}Query)
|
|
7
13
|
export class {{name.pascal}}Handler implements QueryHandler<{{name.pascal}}Query> {
|
|
14
|
+
{{#if access.policy}}
|
|
15
|
+
constructor(
|
|
16
|
+
private readonly reader: {{name.pascal}}Reader,
|
|
17
|
+
private readonly access: Access,
|
|
18
|
+
) {}
|
|
19
|
+
{{/if}}
|
|
20
|
+
{{#unless access.policy}}
|
|
8
21
|
constructor(private readonly reader: {{name.pascal}}Reader) {}
|
|
22
|
+
{{/unless}}
|
|
9
23
|
|
|
10
24
|
async execute(query: {{name.pascal}}Query): Promise<{{name.pascal}}Result> {
|
|
25
|
+
{{#if access.policy}}
|
|
26
|
+
// TODO(domain): a policy refines {{access.reference}}: ask it about the element "{{name.words}}" reads once it is read, in place of `query`.
|
|
27
|
+
await this.access.authorize(query.actor, {{access.reference}}, query);
|
|
28
|
+
|
|
29
|
+
{{/if}}
|
|
11
30
|
// TODO(domain): narrow what is read to what the asker may see: ask `Access.scope(query.actor, permission, '<type>')` for the scope and hand it to the reader, never filter in memory after loading; a query never changes state.
|
|
12
31
|
return this.reader.read(query);
|
|
13
32
|
}
|
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
import { describe, expect, it } from '{{tests.module}}';
|
|
2
2
|
{{#if access.protected}}
|
|
3
|
+
{{#if access.policy}}
|
|
4
|
+
import { AuthorizationMap, PolicyAccess, RoleBasedAuthorizer, authorizationMiddleware, requires } from '@ddd-ts-kit/auth';
|
|
5
|
+
{{/if}}
|
|
6
|
+
{{#unless access.policy}}
|
|
3
7
|
import { AuthorizationMap, RoleBasedAuthorizer, authorizationMiddleware, requires } from '@ddd-ts-kit/auth';
|
|
8
|
+
{{/unless}}
|
|
4
9
|
import { anActor } from '@ddd-ts-kit/auth/testing';
|
|
5
10
|
{{/if}}
|
|
6
11
|
import { createTestBuses } from '@ddd-ts-kit/testing';
|
|
@@ -13,12 +18,22 @@ import type { {{name.pascal}}Result } from '{{alias.context}}/application/querie
|
|
|
13
18
|
import { Fake{{name.pascal}}Reader } from '{{alias.context}}/test/fakes/{{name.kebab}}-reader.fake{{alias.ext}}';
|
|
14
19
|
{{#if access.protected}}
|
|
15
20
|
|
|
21
|
+
{{#if access.policy}}
|
|
22
|
+
const authorizer = new RoleBasedAuthorizer({ roles: { holder: [{{context.camel}}Permissions['{{access.permission}}']] } });
|
|
23
|
+
|
|
24
|
+
const holders = authorizationMiddleware({
|
|
25
|
+
authorizer,
|
|
26
|
+
map: AuthorizationMap.of(requires({{name.pascal}}Query, {{context.camel}}Permissions['{{access.permission}}'])),
|
|
27
|
+
});
|
|
28
|
+
{{/if}}
|
|
29
|
+
{{#unless access.policy}}
|
|
16
30
|
const holders = authorizationMiddleware({
|
|
17
31
|
authorizer: new RoleBasedAuthorizer({
|
|
18
32
|
roles: { holder: [{{context.camel}}Permissions['{{access.permission}}']] },
|
|
19
33
|
}),
|
|
20
34
|
map: AuthorizationMap.of(requires({{name.pascal}}Query, {{context.camel}}Permissions['{{access.permission}}'])),
|
|
21
35
|
});
|
|
36
|
+
{{/unless}}
|
|
22
37
|
|
|
23
38
|
const holder = anActor().withRole('holder').build();
|
|
24
39
|
{{/if}}
|
|
@@ -47,8 +62,14 @@ describe('{{name.title}}', () => {
|
|
|
47
62
|
});
|
|
48
63
|
|
|
49
64
|
function arrange(answer: {{name.pascal}}Result) {
|
|
65
|
+
{{#if access.policy}}
|
|
66
|
+
const { queryBus, clock } = createTestBuses();
|
|
67
|
+
const handler = new {{name.pascal}}Handler(new Fake{{name.pascal}}Reader(answer), new PolicyAccess({ authorizer, clock }));
|
|
68
|
+
{{/if}}
|
|
69
|
+
{{#unless access.policy}}
|
|
50
70
|
const { queryBus } = createTestBuses();
|
|
51
71
|
const handler = new {{name.pascal}}Handler(new Fake{{name.pascal}}Reader(answer));
|
|
72
|
+
{{/unless}}
|
|
52
73
|
|
|
53
74
|
return queryBus{{spec.guard}}.register({{name.pascal}}Query, handler);
|
|
54
75
|
}
|
|
@@ -1,12 +1,31 @@
|
|
|
1
|
+
{{#if access.policy}}
|
|
2
|
+
import type { Access } from '@ddd-ts-kit/auth';
|
|
3
|
+
{{/if}}
|
|
1
4
|
import type { QueryHandler } from '@ddd-ts-kit/kernel';
|
|
5
|
+
{{#if access.policy}}
|
|
6
|
+
{{access.useCasePermissionsImport}}
|
|
7
|
+
{{/if}}
|
|
2
8
|
import type { {{name.pascal}}Reader } from '{{alias.context}}/application/ports/{{name.kebab}}-reader.port{{alias.ext}}';
|
|
3
9
|
import type { {{name.pascal}}Query } from '{{alias.context}}/application/queries/{{name.kebab}}/{{name.kebab}}.query{{alias.ext}}';
|
|
4
10
|
import type { {{name.pascal}}Result } from '{{alias.context}}/application/queries/{{name.kebab}}/{{name.kebab}}.result{{alias.ext}}';
|
|
5
11
|
|
|
6
12
|
export class {{name.pascal}}Handler implements QueryHandler<{{name.pascal}}Query> {
|
|
13
|
+
{{#if access.policy}}
|
|
14
|
+
constructor(
|
|
15
|
+
private readonly reader: {{name.pascal}}Reader,
|
|
16
|
+
private readonly access: Access,
|
|
17
|
+
) {}
|
|
18
|
+
{{/if}}
|
|
19
|
+
{{#unless access.policy}}
|
|
7
20
|
constructor(private readonly reader: {{name.pascal}}Reader) {}
|
|
21
|
+
{{/unless}}
|
|
8
22
|
|
|
9
23
|
async execute(query: {{name.pascal}}Query): Promise<{{name.pascal}}Result> {
|
|
24
|
+
{{#if access.policy}}
|
|
25
|
+
// TODO(domain): a policy refines {{access.reference}}: ask it about the element "{{name.words}}" reads once it is read, in place of `query`.
|
|
26
|
+
await this.access.authorize(query.actor, {{access.reference}}, query);
|
|
27
|
+
|
|
28
|
+
{{/if}}
|
|
10
29
|
// TODO(domain): narrow what is read to what the asker may see: ask `Access.scope(query.actor, permission, '<type>')` for the scope and hand it to the reader, never filter in memory after loading; a query never changes state.
|
|
11
30
|
return this.reader.read(query);
|
|
12
31
|
}
|
|
@@ -2,7 +2,7 @@ import type { {{name.pascal}}Item } from '{{alias.context}}/application/queries/
|
|
|
2
2
|
import { InMemory{{name.pascal}}Reader } from '{{alias.context}}/infrastructure/persistence/{{reader.file}}{{alias.ext}}';
|
|
3
3
|
|
|
4
4
|
export class Fake{{name.pascal}}Reader extends InMemory{{name.pascal}}Reader {
|
|
5
|
-
constructor(protected override readonly
|
|
5
|
+
constructor(protected override readonly {{search.elements}}: readonly {{name.pascal}}Item[]) {
|
|
6
6
|
super();
|
|
7
7
|
}
|
|
8
8
|
}
|
|
@@ -17,38 +17,40 @@ import type {
|
|
|
17
17
|
@Repository({{name.pascal}}Reader)
|
|
18
18
|
{{/if}}
|
|
19
19
|
export class InMemory{{name.pascal}}Reader implements {{name.pascal}}Reader {
|
|
20
|
-
protected readonly
|
|
20
|
+
protected readonly {{search.elements}}: readonly {{name.pascal}}Item[] = [];
|
|
21
21
|
|
|
22
22
|
async search(scope: AccessScope, page: PageRequest<{{name.pascal}}Sort>): Promise<{{name.pascal}}Result> {
|
|
23
23
|
// TODO(domain): replace these canned lines by a real read of the projection; a production adapter (SQL view, search index) implements the same port next to this file, turns the scope into a condition of its statement, and orders by the sort field then by `id` in the same direction, so that a cursor resumes on a total order.
|
|
24
|
-
const visible = this.
|
|
24
|
+
const visible = this.{{search.elements}}.filter(isVisibleTo(scope)).sort(inOrder(page));
|
|
25
25
|
const start = page.kind === 'offset' ? page.offset : positionAfter(visible, page.cursor);
|
|
26
|
-
const
|
|
26
|
+
const {{search.elements}} = visible.slice(start, start + page.limit);
|
|
27
27
|
|
|
28
28
|
if (page.kind === 'offset') {
|
|
29
|
-
return pageOf(
|
|
29
|
+
return pageOf({{search.elements}}, { total: visible.length });
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
-
const last =
|
|
32
|
+
const last = {{search.elements}}.at(-1);
|
|
33
33
|
|
|
34
34
|
return pageOf(
|
|
35
|
-
|
|
35
|
+
{{search.elements}},
|
|
36
36
|
last !== undefined && start + page.limit < visible.length ? { nextCursor: encodeCursor({ id: last.id }) } : {},
|
|
37
37
|
);
|
|
38
38
|
}
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
-
function
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
41
|
+
function isVisibleTo(scope: AccessScope): ({{search.element}}: {{name.pascal}}Item) => boolean {
|
|
42
|
+
return ({{search.element}}) => {
|
|
43
|
+
switch (scope.kind) {
|
|
44
|
+
case 'all':
|
|
45
|
+
return true;
|
|
46
|
+
case 'none':
|
|
47
|
+
return false;
|
|
48
|
+
case 'ids':
|
|
49
|
+
return scope.ids.includes({{search.element}}.id);
|
|
50
|
+
case 'filter':
|
|
51
|
+
return Object.entries(scope.filter).every(([field, value]) => Reflect.get({{search.element}}, field) === value);
|
|
52
|
+
}
|
|
53
|
+
};
|
|
52
54
|
}
|
|
53
55
|
|
|
54
56
|
function inOrder(page: PageRequest<{{name.pascal}}Sort>): (left: {{name.pascal}}Item, right: {{name.pascal}}Item) => number {
|
|
@@ -68,5 +70,5 @@ function positionAfter(visible: readonly {{name.pascal}}Item[], cursor: string |
|
|
|
68
70
|
throw new InvalidPageRequestError('The cursor of a page is the nextCursor of a previous page');
|
|
69
71
|
}
|
|
70
72
|
|
|
71
|
-
return visible.findIndex((
|
|
73
|
+
return visible.findIndex(({{search.element}}) => {{search.element}}.id === id) + 1;
|
|
72
74
|
}
|
|
@@ -7,6 +7,6 @@ import { InMemory{{name.pascal}}Reader } from '{{alias.context}}/infrastructure/
|
|
|
7
7
|
@@registrations
|
|
8
8
|
const {{name.camel}}Reader = platform.adapter(new InMemory{{name.pascal}}Reader(), { kind: 'repository', context: '{{context.kebab}}' });
|
|
9
9
|
|
|
10
|
-
platform.queries.register({{name.pascal}}Query, new {{name.pascal}}Handler({{name.camel}}Reader), { context: '{{context.kebab}}' });
|
|
10
|
+
platform.queries.register({{name.pascal}}Query, new {{name.pascal}}Handler({{name.camel}}Reader{{#if access.policy}}, platform.access{{/if}}), { context: '{{context.kebab}}' });
|
|
11
11
|
@@authorization
|
|
12
12
|
{{access.manualDeclaration}}
|
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
@@imports
|
|
2
|
+
{{#if access.policy}}
|
|
3
|
+
import { Access } from '@ddd-ts-kit/auth';
|
|
4
|
+
{{/if}}
|
|
2
5
|
{{access.permissionsImport}}
|
|
3
6
|
import { {{name.pascal}}Reader } from '{{alias.context}}/application/ports/{{name.kebab}}-reader.port{{alias.ext}}';
|
|
4
7
|
import { {{name.pascal}}Handler } from '{{alias.context}}/application/queries/{{name.kebab}}/{{name.kebab}}.handler{{alias.ext}}';
|
|
@@ -6,4 +9,4 @@ import { {{name.pascal}}Query } from '{{alias.context}}/application/queries/{{na
|
|
|
6
9
|
import { InMemory{{name.pascal}}Reader } from '{{alias.context}}/infrastructure/persistence/{{reader.file}}{{alias.ext}}';
|
|
7
10
|
@@registrations
|
|
8
11
|
builder.bind({{name.pascal}}Reader).toClass(InMemory{{name.pascal}}Reader).as('repository');
|
|
9
|
-
builder.query({{name.pascal}}Query){{access.declaration}}.handledBy({{name.pascal}}Handler, [{{name.pascal}}Reader]);
|
|
12
|
+
builder.query({{name.pascal}}Query){{access.declaration}}.handledBy({{name.pascal}}Handler, [{{name.pascal}}Reader{{#if access.policy}}, Access{{/if}}]);
|
|
@@ -84,7 +84,7 @@ export function {{name.camel}}Route(queries: QueryBus): Route {
|
|
|
84
84
|
// TODO(domain): hand the criteria of the query over too, once the query schema declares them next to the page; map each line to its public fields.
|
|
85
85
|
const page = await queries.execute(new {{route.input}}(actorOf(input.context.principal), input.query));
|
|
86
86
|
|
|
87
|
-
return { status: 200, body: pageResponse(page, (
|
|
87
|
+
return { status: 200, body: pageResponse(page, ({{search.element}}) => ({ id: {{search.element}}.id })) };
|
|
88
88
|
},
|
|
89
89
|
);
|
|
90
90
|
}
|