@eventmodelers/cli 0.0.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 +175 -0
- package/cli.js +676 -0
- package/package.json +35 -0
- package/shared/build-kit/code-export.mjs +560 -0
- package/shared/build-kit/lib/ollama-agent.js +147 -0
- package/shared/build-kit/package.json +11 -0
- package/shared/build-kit/ralph-ollama.js +39 -0
- package/shared/build-kit/realtime-agent.js +18 -0
- package/stacks/axon/templates/.claude/skills/build-automation/SKILL.md +407 -0
- package/stacks/axon/templates/.claude/skills/build-automation/references/feature-flag-patterns.md +114 -0
- package/stacks/axon/templates/.claude/skills/build-automation-workflow/SKILL.md +496 -0
- package/stacks/axon/templates/.claude/skills/build-automation-workflow/references/axon-workflow-api.md +279 -0
- package/stacks/axon/templates/.claude/skills/build-automation-workflow/references/feature-flag-patterns.md +114 -0
- package/stacks/axon/templates/.claude/skills/build-state-change/SKILL.md +396 -0
- package/stacks/axon/templates/.claude/skills/build-state-change/references/axon-test-fixture-patterns.md +162 -0
- package/stacks/axon/templates/.claude/skills/build-state-change/references/feature-flag-patterns.md +56 -0
- package/stacks/axon/templates/.claude/skills/build-state-change/references/rest-api-patterns.md +115 -0
- package/stacks/axon/templates/.claude/skills/build-state-view/SKILL.md +282 -0
- package/stacks/axon/templates/.claude/skills/connect/SKILL.md +178 -0
- package/stacks/axon/templates/.claude/skills/learn-eventmodelers-api/SKILL.md +611 -0
- package/stacks/axon/templates/.claude/skills/load-slice/SKILL.md +141 -0
- package/stacks/axon/templates/.claude/skills/update-slice-status/SKILL.md +105 -0
- package/stacks/axon/templates/build-kit/lib/AGENT.md +47 -0
- package/stacks/axon/templates/build-kit/lib/agent.sh +20 -0
- package/stacks/axon/templates/build-kit/lib/backend-prompt.md +158 -0
- package/stacks/axon/templates/build-kit/lib/prompt.md +126 -0
- package/stacks/axon/templates/build-kit/lib/ralph.js +369 -0
- package/stacks/axon/templates/build-kit/ralph-claude.js +44 -0
- package/stacks/axon/templates/build-kit/ralph.sh +98 -0
- package/stacks/axon/templates/root/.env.example +5 -0
- package/stacks/axon/templates/root/CLAUDE.md +60 -0
- package/stacks/axon/templates/root/README.md +44 -0
- package/stacks/axon/templates/root/docker-compose.yml +64 -0
- package/stacks/axon/templates/root/mvnw +259 -0
- package/stacks/axon/templates/root/mvnw.cmd +149 -0
- package/stacks/axon/templates/root/pom.xml +125 -0
- package/stacks/axon/templates/root/src/main/java/io/axoniq/quickstart/QuickstartApplication.java +128 -0
- package/stacks/axon/templates/root/src/main/java/io/axoniq/quickstart/config/WebConfig.java +155 -0
- package/stacks/axon/templates/root/src/main/resources/application.properties +2 -0
- package/stacks/axon/templates/root/src/main/resources/static/bg-body.png +0 -0
- package/stacks/axon/templates/root/src/main/resources/static/bg-header.png +0 -0
- package/stacks/axon/templates/root/src/main/resources/static/index.html +596 -0
- package/stacks/cratis-csharp/templates/.claude/skills/_shared/cratis-conventions.md +251 -0
- package/stacks/cratis-csharp/templates/.claude/skills/build-automation/SKILL.md +99 -0
- package/stacks/cratis-csharp/templates/.claude/skills/build-automation/references/patterns.md +115 -0
- package/stacks/cratis-csharp/templates/.claude/skills/build-state-change/SKILL.md +163 -0
- package/stacks/cratis-csharp/templates/.claude/skills/build-state-change/references/patterns.md +234 -0
- package/stacks/cratis-csharp/templates/.claude/skills/build-state-view/SKILL.md +120 -0
- package/stacks/cratis-csharp/templates/.claude/skills/build-state-view/references/patterns.md +166 -0
- package/stacks/cratis-csharp/templates/.claude/skills/connect/SKILL.md +169 -0
- package/stacks/cratis-csharp/templates/.claude/skills/learn-eventmodelers-api/SKILL.md +609 -0
- package/stacks/cratis-csharp/templates/.claude/skills/load-slice/SKILL.md +141 -0
- package/stacks/cratis-csharp/templates/.claude/skills/update-slice-status/SKILL.md +105 -0
- package/stacks/cratis-csharp/templates/build-kit/lib/AGENT.md +58 -0
- package/stacks/cratis-csharp/templates/build-kit/lib/agent.sh +20 -0
- package/stacks/cratis-csharp/templates/build-kit/lib/backend-prompt.md +129 -0
- package/stacks/cratis-csharp/templates/build-kit/lib/prompt.md +124 -0
- package/stacks/cratis-csharp/templates/build-kit/lib/ralph.js +302 -0
- package/stacks/cratis-csharp/templates/build-kit/ralph-claude.js +37 -0
- package/stacks/cratis-csharp/templates/build-kit/ralph.sh +98 -0
- package/stacks/cratis-csharp/templates/root/.frontend/index.css +29 -0
- package/stacks/cratis-csharp/templates/root/.frontend/index.html +17 -0
- package/stacks/cratis-csharp/templates/root/.frontend/main.tsx +18 -0
- package/stacks/cratis-csharp/templates/root/.frontend/tsconfig.json +42 -0
- package/stacks/cratis-csharp/templates/root/.frontend/tsconfig.node.json +11 -0
- package/stacks/cratis-csharp/templates/root/.frontend/vite.config.ts +56 -0
- package/stacks/cratis-csharp/templates/root/App.tsx +23 -0
- package/stacks/cratis-csharp/templates/root/CLAUDE.md +62 -0
- package/stacks/cratis-csharp/templates/root/CratisApp.csproj +25 -0
- package/stacks/cratis-csharp/templates/root/CratisApp.sln +18 -0
- package/stacks/cratis-csharp/templates/root/GlobalUsings.cs +3 -0
- package/stacks/cratis-csharp/templates/root/Home.tsx +102 -0
- package/stacks/cratis-csharp/templates/root/Program.cs +26 -0
- package/stacks/cratis-csharp/templates/root/README.md +192 -0
- package/stacks/cratis-csharp/templates/root/SomeModule/SomeFeature/Listing/AllListings.ts +47 -0
- package/stacks/cratis-csharp/templates/root/SomeModule/SomeFeature/Listing/Listing.cs +11 -0
- package/stacks/cratis-csharp/templates/root/SomeModule/SomeFeature/Listing/Listing.ts +12 -0
- package/stacks/cratis-csharp/templates/root/SomeModule/SomeFeature/Listing/ListingDataTable.tsx +17 -0
- package/stacks/cratis-csharp/templates/root/SomeModule/SomeFeature/Listing/index.ts +1 -0
- package/stacks/cratis-csharp/templates/root/SomeModule/SomeFeature/Registration/Register.ts +51 -0
- package/stacks/cratis-csharp/templates/root/SomeModule/SomeFeature/Registration/RegisterDialog.tsx +18 -0
- package/stacks/cratis-csharp/templates/root/SomeModule/SomeFeature/Registration/Registration.cs +27 -0
- package/stacks/cratis-csharp/templates/root/SomeModule/SomeFeature/Registration/index.ts +1 -0
- package/stacks/cratis-csharp/templates/root/SomeModule/SomeFeature/SomeFeature.tsx +22 -0
- package/stacks/cratis-csharp/templates/root/SomeModule/SomeFeature/SomeName.cs +3 -0
- package/stacks/cratis-csharp/templates/root/SomeModule/SomeFeature/index.ts +1 -0
- package/stacks/cratis-csharp/templates/root/appsettings.Development.json +9 -0
- package/stacks/cratis-csharp/templates/root/appsettings.json +26 -0
- package/stacks/cratis-csharp/templates/root/docker-compose.yml +23 -0
- package/stacks/cratis-csharp/templates/root/package.json +33 -0
- package/stacks/cratis-csharp/templates/root/tsconfig.json +3 -0
- package/stacks/modeling-kit/templates/.claude/settings.local.json +13 -0
- package/stacks/modeling-kit/templates/.claude/skills/analyze-existing-model/SKILL.md +215 -0
- package/stacks/modeling-kit/templates/.claude/skills/attributes/SKILL.md +176 -0
- package/stacks/modeling-kit/templates/.claude/skills/connect/SKILL.md +182 -0
- package/stacks/modeling-kit/templates/.claude/skills/discover-storyboard/SKILL.md +383 -0
- package/stacks/modeling-kit/templates/.claude/skills/eventmodeling-applying-conways-law/SKILL.md +465 -0
- package/stacks/modeling-kit/templates/.claude/skills/eventmodeling-brainstorming-events/SKILL.md +587 -0
- package/stacks/modeling-kit/templates/.claude/skills/eventmodeling-brainstorming-events/references/facilitating-event-modeling-workshops.md +626 -0
- package/stacks/modeling-kit/templates/.claude/skills/eventmodeling-checking-completeness/SKILL.md +542 -0
- package/stacks/modeling-kit/templates/.claude/skills/eventmodeling-designing-event-models/SKILL.md +497 -0
- package/stacks/modeling-kit/templates/.claude/skills/eventmodeling-elaborating-scenarios/SKILL.md +783 -0
- package/stacks/modeling-kit/templates/.claude/skills/eventmodeling-identifying-inputs/SKILL.md +604 -0
- package/stacks/modeling-kit/templates/.claude/skills/eventmodeling-identifying-outputs/SKILL.md +699 -0
- package/stacks/modeling-kit/templates/.claude/skills/eventmodeling-integrating-legacy-systems/SKILL.md +674 -0
- package/stacks/modeling-kit/templates/.claude/skills/eventmodeling-optimizing-stream-design/SKILL.md +231 -0
- package/stacks/modeling-kit/templates/.claude/skills/eventmodeling-optimizing-stream-design/references/domain-patterns.md +150 -0
- package/stacks/modeling-kit/templates/.claude/skills/eventmodeling-optimizing-stream-design/references/patterns.md +329 -0
- package/stacks/modeling-kit/templates/.claude/skills/eventmodeling-optimizing-stream-design/references/snapshotting.md +204 -0
- package/stacks/modeling-kit/templates/.claude/skills/eventmodeling-orchestrating-event-modeling/SKILL.md +352 -0
- package/stacks/modeling-kit/templates/.claude/skills/eventmodeling-orchestrating-event-modeling/references/project-planning-with-event-modeling.md +341 -0
- package/stacks/modeling-kit/templates/.claude/skills/eventmodeling-plotting-events/SKILL.md +153 -0
- package/stacks/modeling-kit/templates/.claude/skills/eventmodeling-slicing-event-models/SKILL.md +136 -0
- package/stacks/modeling-kit/templates/.claude/skills/eventmodeling-slicing-event-models/references/examples.md +78 -0
- package/stacks/modeling-kit/templates/.claude/skills/eventmodeling-slicing-event-models/references/patterns.md +85 -0
- package/stacks/modeling-kit/templates/.claude/skills/eventmodeling-storyboarding-events/SKILL.md +655 -0
- package/stacks/modeling-kit/templates/.claude/skills/eventmodeling-translating-external-events/SKILL.md +479 -0
- package/stacks/modeling-kit/templates/.claude/skills/eventmodeling-validating-event-models/SKILL.md +234 -0
- package/stacks/modeling-kit/templates/.claude/skills/eventmodeling-validating-event-models-checklist/SKILL.md +353 -0
- package/stacks/modeling-kit/templates/.claude/skills/examples/SKILL.md +200 -0
- package/stacks/modeling-kit/templates/.claude/skills/handle-comment/SKILL.md +94 -0
- package/stacks/modeling-kit/templates/.claude/skills/learn-eventmodelers-api/SKILL.md +441 -0
- package/stacks/modeling-kit/templates/.claude/skills/place-element/SKILL.md +290 -0
- package/stacks/modeling-kit/templates/.claude/skills/storyboard/SKILL.md +212 -0
- package/stacks/modeling-kit/templates/.claude/skills/storyboard-screen/SKILL.md +123 -0
- package/stacks/modeling-kit/templates/.claude/skills/timeline/SKILL.md +353 -0
- package/stacks/modeling-kit/templates/.claude/skills/update-slice-status/SKILL.md +110 -0
- package/stacks/modeling-kit/templates/.claude/skills/wdyt/SKILL.md +134 -0
- package/stacks/modeling-kit/templates/kit/lib/agent.sh +20 -0
- package/stacks/modeling-kit/templates/kit/lib/ollama-agent.js +147 -0
- package/stacks/modeling-kit/templates/kit/lib/ralph.js +286 -0
- package/stacks/modeling-kit/templates/kit/package.json +12 -0
- package/stacks/modeling-kit/templates/kit/ralph-claude.js +50 -0
- package/stacks/modeling-kit/templates/kit/ralph-ollama.js +38 -0
- package/stacks/modeling-kit/templates/kit/ralph.sh +61 -0
- package/stacks/modeling-kit/templates/kit/realtime-agent.js +18 -0
- package/stacks/modeling-kit/templates/root/CLAUDE.md +56 -0
- package/stacks/node/templates/.claude/skills/build-automation/SKILL.md +266 -0
- package/stacks/node/templates/.claude/skills/build-state-change/SKILL.md +344 -0
- package/stacks/node/templates/.claude/skills/build-state-view/SKILL.md +412 -0
- package/stacks/node/templates/.claude/skills/connect/SKILL.md +178 -0
- package/stacks/node/templates/.claude/skills/learn-eventmodelers-api/SKILL.md +628 -0
- package/stacks/node/templates/.claude/skills/load-slice/SKILL.md +143 -0
- package/stacks/node/templates/.claude/skills/update-slice-status/SKILL.md +110 -0
- package/stacks/node/templates/build-kit/README.md +86 -0
- package/stacks/node/templates/build-kit/lib/AGENT.md +47 -0
- package/stacks/node/templates/build-kit/lib/agent.sh +20 -0
- package/stacks/node/templates/build-kit/lib/backend-prompt.md +152 -0
- package/stacks/node/templates/build-kit/lib/prompt.md +122 -0
- package/stacks/node/templates/build-kit/lib/ralph.js +369 -0
- package/stacks/node/templates/build-kit/ralph-claude.js +44 -0
- package/stacks/node/templates/build-kit/ralph.sh +98 -0
- package/stacks/node/templates/root/.env.example +15 -0
- package/stacks/node/templates/root/CLAUDE.md +60 -0
- package/stacks/node/templates/root/docker-compose.yml +15 -0
- package/stacks/node/templates/root/flyway.conf +17 -0
- package/stacks/node/templates/root/migrations/V1__schema.sql.example +12 -0
- package/stacks/node/templates/root/package.json +50 -0
- package/stacks/node/templates/root/server.ts +130 -0
- package/stacks/node/templates/root/setup-env.sh +53 -0
- package/stacks/node/templates/root/src/common/assertions.ts +6 -0
- package/stacks/node/templates/root/src/common/db.ts +32 -0
- package/stacks/node/templates/root/src/common/loadPostgresEventstore.ts +23 -0
- package/stacks/node/templates/root/src/common/parseEndpoint.ts +51 -0
- package/stacks/node/templates/root/src/common/processorDlq.ts +28 -0
- package/stacks/node/templates/root/src/common/realtimeBroadcast.ts +12 -0
- package/stacks/node/templates/root/src/common/replay.ts +16 -0
- package/stacks/node/templates/root/src/common/routes.ts +19 -0
- package/stacks/node/templates/root/src/common/testHelpers.ts +44 -0
- package/stacks/node/templates/root/src/swagger.ts +34 -0
- package/stacks/node/templates/root/src/util/assertions.ts +6 -0
- package/stacks/node/templates/root/src/util/hash.ts +9 -0
- package/stacks/node/templates/root/src/util/sanitize.ts +23 -0
- package/stacks/node/templates/root/tsconfig.json +32 -0
- package/stacks/node/templates/root/vercel.json +8 -0
- package/stacks/supabase/templates/.claude/skills/build-automation/SKILL.md +266 -0
- package/stacks/supabase/templates/.claude/skills/build-state-change/SKILL.md +344 -0
- package/stacks/supabase/templates/.claude/skills/build-state-view/SKILL.md +390 -0
- package/stacks/supabase/templates/.claude/skills/build-webhook/SKILL.md +303 -0
- package/stacks/supabase/templates/.claude/skills/connect/SKILL.md +178 -0
- package/stacks/supabase/templates/.claude/skills/learn-eventmodelers-api/SKILL.md +628 -0
- package/stacks/supabase/templates/.claude/skills/load-slice/SKILL.md +143 -0
- package/stacks/supabase/templates/.claude/skills/update-slice-status/SKILL.md +110 -0
- package/stacks/supabase/templates/build-kit/README.md +86 -0
- package/stacks/supabase/templates/build-kit/lib/AGENT.md +47 -0
- package/stacks/supabase/templates/build-kit/lib/agent.sh +20 -0
- package/stacks/supabase/templates/build-kit/lib/backend-prompt.md +152 -0
- package/stacks/supabase/templates/build-kit/lib/prompt.md +123 -0
- package/stacks/supabase/templates/build-kit/lib/ralph.js +369 -0
- package/stacks/supabase/templates/build-kit/ralph-claude.js +44 -0
- package/stacks/supabase/templates/build-kit/ralph.sh +98 -0
- package/stacks/supabase/templates/root/.env.example +22 -0
- package/stacks/supabase/templates/root/CLAUDE.md +61 -0
- package/stacks/supabase/templates/root/flyway.conf +17 -0
- package/stacks/supabase/templates/root/package.json +52 -0
- package/stacks/supabase/templates/root/server.ts +177 -0
- package/stacks/supabase/templates/root/setup-env.sh +53 -0
- package/stacks/supabase/templates/root/src/common/assertions.ts +6 -0
- package/stacks/supabase/templates/root/src/common/db.ts +32 -0
- package/stacks/supabase/templates/root/src/common/loadPostgresEventstore.ts +23 -0
- package/stacks/supabase/templates/root/src/common/parseEndpoint.ts +51 -0
- package/stacks/supabase/templates/root/src/common/processorDlq.ts +28 -0
- package/stacks/supabase/templates/root/src/common/realtimeBroadcast.ts +19 -0
- package/stacks/supabase/templates/root/src/common/replay.ts +16 -0
- package/stacks/supabase/templates/root/src/common/routes.ts +23 -0
- package/stacks/supabase/templates/root/src/common/testHelpers.ts +54 -0
- package/stacks/supabase/templates/root/src/supabase/README.md +141 -0
- package/stacks/supabase/templates/root/src/supabase/api.ts +56 -0
- package/stacks/supabase/templates/root/src/supabase/requireOrgaAdmin.ts +32 -0
- package/stacks/supabase/templates/root/src/supabase/requireSysUser.ts +40 -0
- package/stacks/supabase/templates/root/src/supabase/requireUser.ts +72 -0
- package/stacks/supabase/templates/root/src/swagger.ts +34 -0
- package/stacks/supabase/templates/root/src/util/assertions.ts +6 -0
- package/stacks/supabase/templates/root/src/util/hash.ts +9 -0
- package/stacks/supabase/templates/root/src/util/sanitize.ts +23 -0
- package/stacks/supabase/templates/root/supabase/config.toml +295 -0
- package/stacks/supabase/templates/root/supabase/migrations/V1__schema.sql.example +12 -0
- package/stacks/supabase/templates/root/supabase/seed.sql +1 -0
- package/stacks/supabase/templates/root/tsconfig.json +32 -0
- package/stacks/supabase/templates/root/vercel.json +8 -0
|
@@ -0,0 +1,396 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: build-state-change
|
|
3
|
+
authors:
|
|
4
|
+
- Mateusz Nowak
|
|
5
|
+
- Martin Dilger
|
|
6
|
+
description: >
|
|
7
|
+
Implement Event Sourcing write slices using Axon Framework 5.1.1 in this project's one established
|
|
8
|
+
pattern: Command record → mutable decision-model entity (@EventSourced + @EventCriteriaBuilder) →
|
|
9
|
+
@Component CommandHandler that checks entity state inline → AxonTestFixture unit test (no Spring
|
|
10
|
+
context). Use when implementing a new write slice / command handler from a slice.json event model in
|
|
11
|
+
this project. There is exactly one supported style — do not offer alternatives.
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
# Axon Framework 5 — Write Slice
|
|
15
|
+
|
|
16
|
+
One pattern only. Directory layout is flat — `src/main/java/.../slices/{context}/{slicename}/`, no
|
|
17
|
+
`write`/`read`/`automation` folder layer in between (only the shared `slices/{context}/events/` folder
|
|
18
|
+
sits alongside slice folders). Every step below is grounded in the `RegisterCustomer` slice
|
|
19
|
+
(`src/main/java/.../foo/register/`, test in
|
|
20
|
+
`src/test/java/.../foo/register/RegisterCustomerDecisionModelTest.java`) and, for the compound-identifier
|
|
21
|
+
case in Step 1, the `SubscribeToCourse` slice (`src/main/java/.../foo/subscribetocourse/`) — both
|
|
22
|
+
verified, compiled and passing under `mvn test` against `io.axoniq.framework:axoniq-framework-bom:5.1.1`.
|
|
23
|
+
|
|
24
|
+
## Step 0: Read the slice definition
|
|
25
|
+
|
|
26
|
+
Read `.build-kit/.slices/{context}/{slicename}/slice.json`. Extract, and use **only** what's there:
|
|
27
|
+
|
|
28
|
+
- `commands[].fields[]` → Command record fields, in order
|
|
29
|
+
- `events[].fields[]` → Event record fields, in order
|
|
30
|
+
- `specifications[]` (GWT scenarios) → one test method per scenario
|
|
31
|
+
- Which command field(s) have `idAttribute: true` — these carry `@TargetEntityId` (see Step 1) and the
|
|
32
|
+
matching event field(s) carry `@EventTag`
|
|
33
|
+
|
|
34
|
+
Never invent a field, business rule, or event that isn't in slice.json.
|
|
35
|
+
|
|
36
|
+
## Step 1: Command
|
|
37
|
+
|
|
38
|
+
**Exactly one field has `idAttribute: true` in slice.json** — annotate it directly:
|
|
39
|
+
|
|
40
|
+
```java
|
|
41
|
+
package io.axoniq.quickstart.slices.{context}.{slicename};
|
|
42
|
+
|
|
43
|
+
import org.axonframework.messaging.commandhandling.annotation.Command;
|
|
44
|
+
import org.axonframework.modelling.annotation.TargetEntityId;
|
|
45
|
+
|
|
46
|
+
@Command(namespace = "{Context}", name = "{SliceName}", version = "1.0.0")
|
|
47
|
+
public record {SliceName}Command(
|
|
48
|
+
String field1,
|
|
49
|
+
@TargetEntityId
|
|
50
|
+
String idField
|
|
51
|
+
) {}
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
**Two or more fields have `idAttribute: true`** — Axon only allows the resolved identifier to collapse
|
|
55
|
+
to exactly one distinct non-null value (`AnnotationBasedEntityIdResolver`, package
|
|
56
|
+
`org.axonframework.modelling.annotation`); annotating two fields with different values throws
|
|
57
|
+
`EntityIdResolutionException` at command-dispatch time, not at compile time. **Never annotate more than
|
|
58
|
+
one field directly.** Instead build a compound id record and put `@TargetEntityId` on a derived method —
|
|
59
|
+
verified against the `SubscribeToCourse` slice (`email` + `courseId` both `idAttribute: true`):
|
|
60
|
+
|
|
61
|
+
```java
|
|
62
|
+
package io.axoniq.quickstart.slices.{context}.{slicename};
|
|
63
|
+
|
|
64
|
+
public record {SliceName}Id(String field1, String field2) {}
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
```java
|
|
68
|
+
package io.axoniq.quickstart.slices.{context}.{slicename};
|
|
69
|
+
|
|
70
|
+
import org.axonframework.messaging.commandhandling.annotation.Command;
|
|
71
|
+
import org.axonframework.modelling.annotation.TargetEntityId;
|
|
72
|
+
|
|
73
|
+
@Command(namespace = "{Context}", name = "{SliceName}", version = "1.0.0")
|
|
74
|
+
public record {SliceName}Command(String field1, String field2) {
|
|
75
|
+
|
|
76
|
+
@TargetEntityId
|
|
77
|
+
public {SliceName}Id identifier() {
|
|
78
|
+
return new {SliceName}Id(field1, field2);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
The entity's generic id type becomes `{SliceName}Id` everywhere it's referenced (Step 3's
|
|
84
|
+
`@EventCriteriaBuilder` first parameter, Step 7's `EventSourcedEntityModule.autodetected({SliceName}Id.class, ...)`)
|
|
85
|
+
instead of `String`. `@TargetEntityId` scans both fields and methods on the payload
|
|
86
|
+
(`AnnotationBasedEntityIdResolver.findMembers`), so a method works exactly like a field for this purpose.
|
|
87
|
+
|
|
88
|
+
## Step 2: Event — only if it doesn't already exist
|
|
89
|
+
|
|
90
|
+
Check `src/main/java/.../{context}/events/` first; add to the existing sealed/marker interface rather
|
|
91
|
+
than creating a duplicate.
|
|
92
|
+
|
|
93
|
+
```java
|
|
94
|
+
package io.axoniq.quickstart.slices.{context}.events;
|
|
95
|
+
|
|
96
|
+
import org.axonframework.eventsourcing.annotation.EventTag;
|
|
97
|
+
import org.axonframework.messaging.eventhandling.annotation.Event;
|
|
98
|
+
|
|
99
|
+
@Event(namespace = "{Context}", name = "{EventName}", version = "1.0.0")
|
|
100
|
+
public record {EventName}(
|
|
101
|
+
String field1,
|
|
102
|
+
@EventTag(key = EventTags.{TAG_CONSTANT})
|
|
103
|
+
String idField
|
|
104
|
+
) implements {Context}Event {}
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
Add the tag constant to the context's `EventTags` class if it isn't already there:
|
|
108
|
+
|
|
109
|
+
```java
|
|
110
|
+
public static final String {TAG_CONSTANT} = "idField";
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
## Step 3: Decision-model entity
|
|
114
|
+
|
|
115
|
+
Package-private, mutable field(s) — **not** an immutable `State` record with free-standing
|
|
116
|
+
`decide()`/`evolve()` static methods. One boolean/value field per fact the command handler's rule
|
|
117
|
+
check needs, nothing else.
|
|
118
|
+
|
|
119
|
+
```java
|
|
120
|
+
package io.axoniq.quickstart.slices.{context}.{slicename};
|
|
121
|
+
|
|
122
|
+
import io.axoniq.quickstart.slices.{context}.events.{EventName};
|
|
123
|
+
import io.axoniq.quickstart.slices.{context}.events.EventTags;
|
|
124
|
+
import org.axonframework.eventsourcing.annotation.EventCriteriaBuilder;
|
|
125
|
+
import org.axonframework.eventsourcing.annotation.EventSourcingHandler;
|
|
126
|
+
import org.axonframework.eventsourcing.annotation.reflection.EntityCreator;
|
|
127
|
+
import org.axonframework.extension.spring.stereotype.EventSourced;
|
|
128
|
+
import org.axonframework.messaging.core.MessageTypeResolver;
|
|
129
|
+
import org.axonframework.messaging.eventstreaming.EventCriteria;
|
|
130
|
+
import org.axonframework.messaging.eventstreaming.Tag;
|
|
131
|
+
|
|
132
|
+
@EventSourced
|
|
133
|
+
class {SliceName}DecisionModel {
|
|
134
|
+
|
|
135
|
+
public boolean <ruleFlag>;
|
|
136
|
+
|
|
137
|
+
@EntityCreator
|
|
138
|
+
private {SliceName}DecisionModel() {
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
@EventCriteriaBuilder
|
|
142
|
+
private static EventCriteria resolveCriteria(String idField, MessageTypeResolver messageTypeResolver) {
|
|
143
|
+
return EventCriteria.havingTags(Tag.of(EventTags.{TAG_CONSTANT}, idField))
|
|
144
|
+
.andBeingOneOfTypes(messageTypeResolver, {EventName}.class);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
@EventSourcingHandler
|
|
148
|
+
public void on({EventName} event) {
|
|
149
|
+
this.<ruleFlag> = true;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
**Always write `@EventCriteriaBuilder`. Never rely on bare `@EventSourced(tagKey = "...")`.** A tag-key
|
|
155
|
+
mismatch produces no compiler error and no runtime exception — the entity just silently never loads any
|
|
156
|
+
prior events, so it always looks "fresh" and the business rule never fires. `@EventCriteriaBuilder`'s
|
|
157
|
+
first parameter is always the `@TargetEntityId` type; a second `MessageTypeResolver` parameter is
|
|
158
|
+
resolved automatically from `Configuration` — pass it plus the event class straight into
|
|
159
|
+
`andBeingOneOfTypes(messageTypeResolver, {EventName}.class)`.
|
|
160
|
+
|
|
161
|
+
**The `MessageTypeResolver` parameter is not optional — don't drop it to save an injection.** It was
|
|
162
|
+
tempting to think `andBeingOneOfTypes(new QualifiedName({EventName}.class))` should work without it,
|
|
163
|
+
since `QualifiedName` has a `Class<?>` constructor. Verified experimentally that it does NOT:
|
|
164
|
+
`QualifiedName(Class<?>)`'s only job is `clazz.getName()` — the raw Java class name
|
|
165
|
+
(`io.axoniq.quickstart.slices.foo.events.CustomerRegistered`) — not the `@Event(namespace, name)` value
|
|
166
|
+
the event was actually appended under (`Foo.CustomerRegistered`). Swapping to it in
|
|
167
|
+
`SubscribeToCourseDecisionModel` as a test made 2 of 3 passing tests fail immediately, silently, with
|
|
168
|
+
no exception pointing at the real cause — the criteria just stopped matching anything, identical in
|
|
169
|
+
effect to a `tagKey` mismatch. `MessageTypeResolver` is what correctly bridges the `@Event` annotation
|
|
170
|
+
to the type Axon actually matches on; there is no annotation-free shortcut. Never hand-type a
|
|
171
|
+
`"Namespace.Name"` string either, and never pass `{EventName}.class.getName()` — both drift silently
|
|
172
|
+
out of sync with the event's `@Event` annotation with no compiler check.
|
|
173
|
+
|
|
174
|
+
`EventCriteria`/`Tag` live in `org.axonframework.messaging.eventstreaming` in 5.1.1 — not
|
|
175
|
+
`org.axonframework.eventsourcing.eventstore` (that package has no such classes in this version).
|
|
176
|
+
|
|
177
|
+
**If Step 1 used a compound `{SliceName}Id`** (two or more `idAttribute: true` fields), the first
|
|
178
|
+
parameter here is `{SliceName}Id id` instead of `String idField` — pull the individual values back out
|
|
179
|
+
with `id.field1()`/`id.field2()` for tagging.
|
|
180
|
+
|
|
181
|
+
**Tag each event type by what THIS decision actually needs checked for it — not uniformly.** When
|
|
182
|
+
multiple event types feed one entity (via `EventCriteria.either(...)`), each branch gets its own tag
|
|
183
|
+
set, chosen per the specific invariant that event type is being loaded to verify. This is context-
|
|
184
|
+
dependent: the same event type can legitimately need a wider or narrower tag set in a different slice
|
|
185
|
+
that reads it for a different rule. Verified worked example — `SubscribeToCourseDecisionModel`, id is
|
|
186
|
+
`SubscriptionId(email, courseId)`, two rules, two different tag scopes on two different event types:
|
|
187
|
+
|
|
188
|
+
```java
|
|
189
|
+
@EventCriteriaBuilder
|
|
190
|
+
private static EventCriteria resolveCriteria(SubscriptionId id, MessageTypeResolver messageTypeResolver) {
|
|
191
|
+
return EventCriteria.either(
|
|
192
|
+
// "is this customer registered at all" — scoped to email only
|
|
193
|
+
EventCriteria.havingTags(Tag.of(EventTags.EMAIL, id.email()))
|
|
194
|
+
.andBeingOneOfTypes(messageTypeResolver, CustomerRegistered.class),
|
|
195
|
+
// "did this customer already subscribe to THIS course" — scoped to email + courseId
|
|
196
|
+
EventCriteria.havingTags(Tag.of(EventTags.EMAIL, id.email()), Tag.of(EventTags.COURSE_ID, id.courseId()))
|
|
197
|
+
.andBeingOneOfTypes(messageTypeResolver, SubscribedToCourse.class)
|
|
198
|
+
);
|
|
199
|
+
}
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
`CustomerRegistered` only needs the `email` tag — "is this customer registered" doesn't involve a
|
|
203
|
+
course. `SubscribedToCourse` needs **both** `email` and `courseId` tags together — the rule is "already
|
|
204
|
+
subscribed to *this* course", not "subscribed to any course", so loading a customer's subscription to a
|
|
205
|
+
*different* course must not satisfy it. This also directly simplifies the entity: narrowing the second
|
|
206
|
+
branch to `email` + `courseId` means the entity only ever sees `SubscribedToCourse` events for this
|
|
207
|
+
exact course, so a `Set<String> subscribedCourseIds` collecting every course is unnecessary — a single
|
|
208
|
+
`boolean alreadySubscribedToThisCourse` set by `@EventSourcingHandler` is enough. Getting the tag scope
|
|
209
|
+
wrong doesn't fail loudly: too-wide a scope silently pulls in unrelated events (correct here only by
|
|
210
|
+
coincidence, e.g. it still worked when courses were checked in Java after the fact) or double-counts;
|
|
211
|
+
too-narrow a scope silently drops events the rule actually needed.
|
|
212
|
+
|
|
213
|
+
A read-side use case for the *same* `SubscribedToCourse` event (e.g. "how many courses is this customer
|
|
214
|
+
subscribed to") would legitimately query it with `email` only, no `courseId` — the correct tag scope is
|
|
215
|
+
a property of the specific decision/query consuming the event, not a fixed property of the event type
|
|
216
|
+
itself.
|
|
217
|
+
|
|
218
|
+
## Step 4: Command handler
|
|
219
|
+
|
|
220
|
+
```java
|
|
221
|
+
package io.axoniq.quickstart.slices.{context}.{slicename};
|
|
222
|
+
|
|
223
|
+
import io.axoniq.quickstart.slices.{context}.events.{EventName};
|
|
224
|
+
import org.axonframework.messaging.commandhandling.annotation.CommandHandler;
|
|
225
|
+
import org.axonframework.messaging.core.Metadata;
|
|
226
|
+
import org.axonframework.messaging.eventhandling.gateway.EventAppender;
|
|
227
|
+
import org.axonframework.modelling.annotation.InjectEntity;
|
|
228
|
+
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
|
229
|
+
import org.springframework.stereotype.Component;
|
|
230
|
+
|
|
231
|
+
@Component
|
|
232
|
+
@ConditionalOnProperty(prefix = "slices.{context}.write", name = "{slicename}.enabled")
|
|
233
|
+
public class {SliceName}CommandHandler {
|
|
234
|
+
|
|
235
|
+
@CommandHandler
|
|
236
|
+
public void handle(
|
|
237
|
+
{SliceName}Command command,
|
|
238
|
+
Metadata metadata,
|
|
239
|
+
@InjectEntity {SliceName}DecisionModel entity,
|
|
240
|
+
EventAppender eventAppender
|
|
241
|
+
) {
|
|
242
|
+
if (entity.<ruleFlag>) {
|
|
243
|
+
throw new IllegalStateException("...");
|
|
244
|
+
}
|
|
245
|
+
eventAppender.append(new {EventName}(command.field1(), command.idField()));
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
`Metadata` is `org.axonframework.messaging.core.Metadata` — **not** `AxonMetadata`, which does not
|
|
251
|
+
exist as a type in this version.
|
|
252
|
+
|
|
253
|
+
## Step 5: REST endpoint — only if slice.json shows an inbound `SCREEN` dependency on the command
|
|
254
|
+
|
|
255
|
+
```java
|
|
256
|
+
package io.axoniq.quickstart.slices.{context}.{slicename};
|
|
257
|
+
|
|
258
|
+
import org.axonframework.messaging.commandhandling.gateway.CommandGateway;
|
|
259
|
+
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
|
260
|
+
import org.springframework.http.ResponseEntity;
|
|
261
|
+
import org.springframework.web.bind.annotation.PostMapping;
|
|
262
|
+
import org.springframework.web.bind.annotation.RequestBody;
|
|
263
|
+
import org.springframework.web.bind.annotation.RestController;
|
|
264
|
+
import reactor.core.publisher.Mono;
|
|
265
|
+
|
|
266
|
+
@RestController
|
|
267
|
+
@ConditionalOnProperty(prefix = "slices.{context}.write", name = "{slicename}.enabled")
|
|
268
|
+
public class {SliceName}RestController {
|
|
269
|
+
|
|
270
|
+
private final CommandGateway commandGateway;
|
|
271
|
+
|
|
272
|
+
public {SliceName}RestController(CommandGateway commandGateway) {
|
|
273
|
+
this.commandGateway = commandGateway;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
@PostMapping("/api/{context}/{resource}")
|
|
277
|
+
public Mono<ResponseEntity<Void>> handle(@RequestBody {SliceName}RequestBody body) {
|
|
278
|
+
var command = new {SliceName}Command(body.field1(), body.idField());
|
|
279
|
+
return Mono.fromFuture(commandGateway.send(command).getResultMessage())
|
|
280
|
+
.map(ignored -> ResponseEntity.ok().<Void>build())
|
|
281
|
+
.onErrorResume(e -> Mono.just(ResponseEntity.badRequest().<Void>build()));
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
public record {SliceName}RequestBody(String field1, String idField) {}
|
|
285
|
+
}
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
If the only inbound dependency is another slice's `AUTOMATION`, skip this step — it calls the
|
|
289
|
+
`CommandGateway` in-process, it doesn't need HTTP.
|
|
290
|
+
|
|
291
|
+
This project uses WebFlux (`Mono<ResponseEntity<...>>`, not plain `ResponseEntity`) — see
|
|
292
|
+
[references/rest-api-patterns.md](references/rest-api-patterns.md) for the full annotated breakdown and
|
|
293
|
+
a `WebTestClient`-based test shape (this repo has no REST test yet — that file's test is the
|
|
294
|
+
recommended pattern for the first one, not a verified-passing example like the other references).
|
|
295
|
+
|
|
296
|
+
## Step 6: Feature flag
|
|
297
|
+
|
|
298
|
+
Every slice component (handler, REST controller) gets `@ConditionalOnProperty(prefix =
|
|
299
|
+
"slices.{context}.write", name = "{slicename}.enabled")` — the entity does not need it. Wire the flag
|
|
300
|
+
in all three places:
|
|
301
|
+
|
|
302
|
+
- `src/main/resources/application.properties` — `slices.{context}.write.{slicename}.enabled=true`
|
|
303
|
+
- `src/test/resources/application.properties` — `slices.{context}.write.{slicename}.enabled=false`
|
|
304
|
+
- `META-INF/additional-spring-configuration-metadata.json` — add a `{"name": "...", "type":
|
|
305
|
+
"java.lang.Boolean", "description": "..."}` entry
|
|
306
|
+
|
|
307
|
+
This flag is irrelevant to the Step 7 test below — that test never boots Spring, so
|
|
308
|
+
`@ConditionalOnProperty` never runs.
|
|
309
|
+
|
|
310
|
+
## Step 7: Test — `AxonTestFixture`, no Spring context
|
|
311
|
+
|
|
312
|
+
One-time `pom.xml` addition (no version needed — resolved via the project's existing
|
|
313
|
+
`axoniq-framework-bom` → `axon-framework-bom` import):
|
|
314
|
+
|
|
315
|
+
```xml
|
|
316
|
+
<dependency>
|
|
317
|
+
<groupId>org.axonframework</groupId>
|
|
318
|
+
<artifactId>axon-test</artifactId>
|
|
319
|
+
<scope>test</scope>
|
|
320
|
+
</dependency>
|
|
321
|
+
```
|
|
322
|
+
|
|
323
|
+
```java
|
|
324
|
+
package io.axoniq.quickstart.slices.{context}.{slicename};
|
|
325
|
+
|
|
326
|
+
import io.axoniq.quickstart.slices.{context}.events.{EventName};
|
|
327
|
+
import org.axonframework.eventsourcing.configuration.EventSourcedEntityModule;
|
|
328
|
+
import org.axonframework.eventsourcing.configuration.EventSourcingConfigurer;
|
|
329
|
+
import org.axonframework.messaging.commandhandling.configuration.CommandHandlingModule;
|
|
330
|
+
import org.axonframework.test.fixture.AxonTestFixture;
|
|
331
|
+
import org.junit.jupiter.api.AfterEach;
|
|
332
|
+
import org.junit.jupiter.api.BeforeEach;
|
|
333
|
+
import org.junit.jupiter.api.DisplayName;
|
|
334
|
+
import org.junit.jupiter.api.Test;
|
|
335
|
+
|
|
336
|
+
class {SliceName}DecisionModelTest {
|
|
337
|
+
|
|
338
|
+
private AxonTestFixture fixture;
|
|
339
|
+
|
|
340
|
+
@BeforeEach
|
|
341
|
+
void setUp() {
|
|
342
|
+
var entityModule = EventSourcedEntityModule
|
|
343
|
+
.autodetected(String.class, {SliceName}DecisionModel.class);
|
|
344
|
+
var commandHandlerModule = CommandHandlingModule.named("{SliceName}")
|
|
345
|
+
.commandHandlers()
|
|
346
|
+
.autodetectedCommandHandlingComponent(c -> new {SliceName}CommandHandler());
|
|
347
|
+
var configurer = EventSourcingConfigurer.create()
|
|
348
|
+
.registerEntity(entityModule)
|
|
349
|
+
.registerCommandHandlingModule(commandHandlerModule);
|
|
350
|
+
fixture = AxonTestFixture.with(configurer);
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
@AfterEach
|
|
354
|
+
void tearDown() {
|
|
355
|
+
fixture.stop();
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
@Test
|
|
359
|
+
@DisplayName("given no prior activity, when {sliceName}, then {eventName} emitted")
|
|
360
|
+
void happyPath() {
|
|
361
|
+
fixture.given().noPriorActivity()
|
|
362
|
+
.when().command(new {SliceName}Command("value1", "id-1"))
|
|
363
|
+
.then().success()
|
|
364
|
+
.events(new {EventName}("value1", "id-1"));
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
@Test
|
|
368
|
+
@DisplayName("given <rule already true>, when {sliceName}, then rejected")
|
|
369
|
+
void ruleViolation() {
|
|
370
|
+
fixture.given().event(new {EventName}("value1", "id-1"))
|
|
371
|
+
.when().command(new {SliceName}Command("value2", "id-1"))
|
|
372
|
+
.then().exception(IllegalStateException.class, "...");
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
```
|
|
376
|
+
|
|
377
|
+
One test method per GWT scenario in slice.json's `specifications[]`. This works because
|
|
378
|
+
`EventSourcedEntityModule.autodetected(...)` and `...autodetectedCommandHandlingComponent(...)` operate
|
|
379
|
+
directly on the `@EventSourced`/`@Component` classes via reflection — the same classes Spring
|
|
380
|
+
auto-discovers in production also work standalone, with zero `ApplicationContext`, zero
|
|
381
|
+
`@SpringBootTest`.
|
|
382
|
+
|
|
383
|
+
Fluent API cheat sheet and known gotchas (wrong `EventCriteria`/`Tag` package, a builder method that
|
|
384
|
+
was renamed between AF5 patch versions, `Customization.disableAxonServer()` not existing in 5.1.1):
|
|
385
|
+
see [references/axon-test-fixture-patterns.md](references/axon-test-fixture-patterns.md).
|
|
386
|
+
|
|
387
|
+
## Final Verification
|
|
388
|
+
|
|
389
|
+
Before considering the slice done:
|
|
390
|
+
|
|
391
|
+
- [ ] Every field in slice.json's `commands[]` is in the Command record — no invented fields, none missing
|
|
392
|
+
- [ ] Every field in slice.json's `events[]` is in the Event record — no invented fields, none missing
|
|
393
|
+
- [ ] Every `specifications[]` scenario has a corresponding test method
|
|
394
|
+
- [ ] No business rule exists in the handler that isn't traceable to slice.json's `description`/`comments`
|
|
395
|
+
- [ ] `mvn compile -q`, then run the slice's own tests only
|
|
396
|
+
- [ ] If checks pass, commit with `feat: {Slice Name}` and set slice status to `Done`
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
# AxonTestFixture Patterns (Verified against Axon Framework 5.1.1)
|
|
2
|
+
|
|
3
|
+
Everything on this page was compiled and run with `mvn test` against
|
|
4
|
+
`io.axoniq.framework:axoniq-framework-bom:5.1.1` (which pulls in
|
|
5
|
+
`org.axonframework:axon-framework-bom:5.1.1`). It supersedes any conflicting claim elsewhere in this
|
|
6
|
+
skill about `@SpringBootTest` / `AxonTestFixture.configSlice(...)` — that method does not exist in
|
|
7
|
+
5.1.1. Ground-truth worked example: `RegisterCustomer` slice in this repo
|
|
8
|
+
s(`src/main/java/.../foo/register/`, test in `src/test/java/.../foo/register/RegisterCustomerDecisionModelTest.java`).
|
|
9
|
+
Slice folders sit directly under their context — `slices/{context}/{slicename}/`, no `write`/`read`/
|
|
10
|
+
`automation` layer in between.
|
|
11
|
+
|
|
12
|
+
## 0. Add the dependency
|
|
13
|
+
|
|
14
|
+
`axon-test` is **not** pulled in by `axoniq-spring-boot-starter`. Add it explicitly (no version needed —
|
|
15
|
+
it's managed by the `axoniq-framework-bom` → `axon-framework-bom` import already in the project's `pom.xml`):
|
|
16
|
+
|
|
17
|
+
```xml
|
|
18
|
+
<dependency>
|
|
19
|
+
<groupId>org.axonframework</groupId>
|
|
20
|
+
<artifactId>axon-test</artifactId>
|
|
21
|
+
<scope>test</scope>
|
|
22
|
+
</dependency>
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## 0b. `@EventSourced` (Spring stereotype) works directly with `EventSourcedEntityModule.autodetected(...)`
|
|
26
|
+
|
|
27
|
+
`org.axonframework.extension.spring.stereotype.EventSourced` is not a plain marker — it aliases its
|
|
28
|
+
attributes onto `org.axonframework.eventsourcing.annotation.EventSourcedEntity` via Spring's
|
|
29
|
+
`@AliasFor` mechanism. `AnnotationBasedEventCriteriaResolver`/`EventSourcedEntityModule.autodetected(...)`
|
|
30
|
+
resolve that alias correctly at runtime even with zero Spring context — verified empirically:
|
|
31
|
+
`EventSourcedEntityModule.autodetected(String.class, RegisterCustomerDecisionModel.class)` succeeded
|
|
32
|
+
against an entity annotated only with the bare `@EventSourced` (no explicit `@EventSourcedEntity`).
|
|
33
|
+
You do **not** need to dual-annotate or swap to `@EventSourcedEntity` just to make the entity
|
|
34
|
+
fixture-testable.
|
|
35
|
+
|
|
36
|
+
## 1. Key finding: you do NOT need a Spring context to test the entity/handler pair
|
|
37
|
+
|
|
38
|
+
Production code stays `@EventSourced` (Spring stereotype) entity + `@Component` handler, auto-discovered
|
|
39
|
+
by `SpringEventSourcedEntityLookup` at runtime. The test does **not** have to boot Spring —
|
|
40
|
+
`EventSourcedEntityModule.autodetected(...)` and `CommandHandlingModule...autodetectedCommandHandlingComponent(...)`
|
|
41
|
+
work directly off the annotated classes via reflection, regardless of them also being
|
|
42
|
+
`@Component`/`@EventSourced`-Spring-stereotyped for production wiring. This is dramatically faster than
|
|
43
|
+
`@SpringBootTest` (no `ApplicationContext` startup) and needs zero Spring test slices.
|
|
44
|
+
|
|
45
|
+
```java
|
|
46
|
+
class RegisterCustomerDecisionModelTest {
|
|
47
|
+
|
|
48
|
+
private AxonTestFixture fixture;
|
|
49
|
+
|
|
50
|
+
@BeforeEach
|
|
51
|
+
void setUp() {
|
|
52
|
+
var entityModule = EventSourcedEntityModule
|
|
53
|
+
.autodetected(String.class, RegisterCustomerDecisionModel.class);
|
|
54
|
+
var commandHandlerModule = CommandHandlingModule.named("Register")
|
|
55
|
+
.commandHandlers()
|
|
56
|
+
.autodetectedCommandHandlingComponent(c -> new RegisterCustomerCommandHandler());
|
|
57
|
+
var configurer = EventSourcingConfigurer.create()
|
|
58
|
+
.registerEntity(entityModule)
|
|
59
|
+
.registerCommandHandlingModule(commandHandlerModule);
|
|
60
|
+
fixture = AxonTestFixture.with(configurer);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
@AfterEach
|
|
64
|
+
void tearDown() {
|
|
65
|
+
fixture.stop();
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
@Test
|
|
69
|
+
@DisplayName("given no prior registration, when register customer, then customer registered emitted")
|
|
70
|
+
void happyPath() {
|
|
71
|
+
fixture.given().noPriorActivity()
|
|
72
|
+
.when().command(new RegisterCustomerCommand("Martin", "test@test.de"))
|
|
73
|
+
.then().success()
|
|
74
|
+
.events(new CustomerRegistered("Martin", "test@test.de"));
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
@Test
|
|
78
|
+
@DisplayName("given customer registered, when register customer with same email, then already registered")
|
|
79
|
+
void alreadyRegistered() {
|
|
80
|
+
fixture.given().event(new CustomerRegistered("Martin", "test@test.de"))
|
|
81
|
+
.when().command(new RegisterCustomerCommand("Max", "test@test.de"))
|
|
82
|
+
.then().exception(IllegalStateException.class, "Email already registered");
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
**Method name note**: in 5.0.1 the fluent builder method was `annotatedCommandHandlingComponent`; it was
|
|
88
|
+
**renamed** to `autodetectedCommandHandlingComponent` in 5.1.x. Use `javap` against the project's actual
|
|
89
|
+
resolved jar before trusting either name in a new project — don't assume the older name still compiles.
|
|
90
|
+
|
|
91
|
+
`AxonTestFixture.Customization::disableAxonServer` (seen in older AF4-adjacent examples) does **not**
|
|
92
|
+
exist as a method in 5.1.1's `AxonTestFixture.Customization` record — the plain `AxonTestFixture.with(configurer)`
|
|
93
|
+
overload (no customization argument) already runs fully in-memory with no Axon Server connection. Only
|
|
94
|
+
reach for the `UnaryOperator<Customization>` overload (`.asIntegrationTest()`, `.registerFieldFilter(...)`,
|
|
95
|
+
`.registerIgnoredField(...)`) when you actually need one of those specific behaviors.
|
|
96
|
+
|
|
97
|
+
## 2. Fluent API cheat sheet
|
|
98
|
+
|
|
99
|
+
```java
|
|
100
|
+
fixture.given().noPriorActivity() // no prior events
|
|
101
|
+
fixture.given().event(someEvent) // one prior event
|
|
102
|
+
fixture.given().events(e1, e2, ...) // several prior events
|
|
103
|
+
|
|
104
|
+
fixture.when().command(someCommand)
|
|
105
|
+
fixture.when().event(someEvent) // dispatch via when() also supported
|
|
106
|
+
|
|
107
|
+
.then().success() // command handler returned normally
|
|
108
|
+
.then().events(expectedEvent1, expectedEvent2) // exact events emitted (equality via all-fields match)
|
|
109
|
+
.then().exception(IllegalStateException.class) // exception type only
|
|
110
|
+
.then().exception(IllegalStateException.class, "Email already registered") // type + exact message
|
|
111
|
+
.then().noEvents() // handler ran, nothing emitted
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
`given()`/`when()`/`then()` all live on `AxonTestFixture` (`fixture.given()`, `fixture.when()`), chained
|
|
115
|
+
onto the previous phase's return value (`.given()....when()....then()...`), not called repeatedly on
|
|
116
|
+
`fixture` mid-chain.
|
|
117
|
+
|
|
118
|
+
## 3. `@EventCriteriaBuilder` over bare `tagKey`
|
|
119
|
+
|
|
120
|
+
**Do not** rely on `@EventSourced(tagKey = "...")` (or the plain `@EventSourcedEntity(tagKey = "...")`)
|
|
121
|
+
for anything beyond the most trivial single-tag case — it silently produces an entity that never loads
|
|
122
|
+
any prior events if the tag key doesn't happen to match by coincidence, and there is no compiler or
|
|
123
|
+
runtime error to catch the mismatch; you only notice when a "should already exist" test unexpectedly
|
|
124
|
+
passes as if the entity were fresh. Prefer an explicit `@EventCriteriaBuilder` static method:
|
|
125
|
+
|
|
126
|
+
```java
|
|
127
|
+
@EventCriteriaBuilder
|
|
128
|
+
private static EventCriteria resolveCriteria(String email, MessageTypeResolver messageTypeResolver) {
|
|
129
|
+
return EventCriteria.havingTags(Tag.of(EventTags.EMAIL, email))
|
|
130
|
+
.andBeingOneOfTypes(messageTypeResolver, CustomerRegistered.class);
|
|
131
|
+
}
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
Rules (verified from `AnnotationBasedEventCriteriaResolver`, package `org.axonframework.eventsourcing.annotation`):
|
|
135
|
+
- Must be `static`, must return `EventCriteria` (package `org.axonframework.messaging.eventstreaming`, **not**
|
|
136
|
+
`org.axonframework.eventsourcing.eventstore` — that package does not contain `EventCriteria`/`Tag` in 5.1.1).
|
|
137
|
+
- First parameter is always the entity's `@InjectEntity`/`@TargetEntityId` identifier type (here `String`).
|
|
138
|
+
- Any additional parameters are resolved as components from the `Configuration` — `MessageTypeResolver` is
|
|
139
|
+
a valid injectable component and is the type-safe way to restrict the criteria to specific event classes:
|
|
140
|
+
`andBeingOneOfTypes(MessageTypeResolver, Class<?>...)`. This avoids hand-typing `"Namespace.Name"` strings
|
|
141
|
+
that can silently drift out of sync with the `@Event(namespace=..., name=...)` annotation on the event
|
|
142
|
+
record — prefer it over the string-literal `andBeingOneOfTypes(String...)` overload wherever a
|
|
143
|
+
`MessageTypeResolver` is easily injectable (which it always is, inside an `@EventCriteriaBuilder` method).
|
|
144
|
+
- Only one `@EventCriteriaBuilder` method may exist per distinct identifier parameter type on a given entity.
|
|
145
|
+
|
|
146
|
+
## 4. Minimal working `pom.xml` diff
|
|
147
|
+
|
|
148
|
+
```xml
|
|
149
|
+
<dependency>
|
|
150
|
+
<groupId>org.junit.jupiter</groupId>
|
|
151
|
+
<artifactId>junit-jupiter</artifactId>
|
|
152
|
+
<scope>test</scope>
|
|
153
|
+
</dependency>
|
|
154
|
+
<dependency> <!-- add this -->
|
|
155
|
+
<groupId>org.axonframework</groupId>
|
|
156
|
+
<artifactId>axon-test</artifactId>
|
|
157
|
+
<scope>test</scope>
|
|
158
|
+
</dependency>
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
No other pom changes are required — version resolution comes from the BOM already imported for the
|
|
162
|
+
main `org.axonframework`/`io.axoniq.framework` dependencies.
|
package/stacks/axon/templates/.claude/skills/build-state-change/references/feature-flag-patterns.md
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# Feature Flag Patterns
|
|
2
|
+
|
|
3
|
+
This project's one supported approach: `@ConditionalOnProperty` on the handler and REST controller
|
|
4
|
+
(**not** on the entity — verified against the `RegisterCustomer`, `AllCustomers`, and `NotifyCustomer`
|
|
5
|
+
slices, none of which put the annotation on their entity/projector-internals class). Examples use a
|
|
6
|
+
generic `Ordering` bounded context.
|
|
7
|
+
|
|
8
|
+
## Annotation on slice components
|
|
9
|
+
|
|
10
|
+
```java
|
|
11
|
+
// Handler
|
|
12
|
+
@ConditionalOnProperty(prefix = "slices.ordering.write", name = "placeorder.enabled")
|
|
13
|
+
@Component
|
|
14
|
+
public class PlaceOrderCommandHandler { ... }
|
|
15
|
+
|
|
16
|
+
// REST controller (if applicable)
|
|
17
|
+
@ConditionalOnProperty(prefix = "slices.ordering.write", name = "placeorder.enabled")
|
|
18
|
+
@RestController
|
|
19
|
+
public class PlaceOrderRestController { ... }
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## `application.properties` (main — enable by default)
|
|
23
|
+
|
|
24
|
+
```properties
|
|
25
|
+
slices.ordering.write.placeorder.enabled=true
|
|
26
|
+
slices.ordering.read.getorders.enabled=true
|
|
27
|
+
slices.ordering.automation.notifycustomeronorder.enabled=true
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## `application.properties` (test — disable by default)
|
|
31
|
+
|
|
32
|
+
```properties
|
|
33
|
+
slices.ordering.write.placeorder.enabled=false
|
|
34
|
+
slices.ordering.read.getorders.enabled=false
|
|
35
|
+
slices.ordering.automation.notifycustomeronorder.enabled=false
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
This only matters for tests that boot a Spring context (`@SpringBootTest`). The `AxonTestFixture`
|
|
39
|
+
unit-test pattern in [axon-test-fixture-patterns.md](axon-test-fixture-patterns.md) never boots Spring,
|
|
40
|
+
so these properties don't affect it either way.
|
|
41
|
+
|
|
42
|
+
## `META-INF/additional-spring-configuration-metadata.json`
|
|
43
|
+
|
|
44
|
+
Register each property so IDEs auto-complete and validate it:
|
|
45
|
+
|
|
46
|
+
```json
|
|
47
|
+
{
|
|
48
|
+
"properties": [
|
|
49
|
+
{
|
|
50
|
+
"name": "slices.ordering.write.placeorder.enabled",
|
|
51
|
+
"type": "java.lang.Boolean",
|
|
52
|
+
"description": "Enable/disable the PlaceOrder write slice in the Ordering bounded context."
|
|
53
|
+
}
|
|
54
|
+
]
|
|
55
|
+
}
|
|
56
|
+
```
|