@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
package/stacks/axon/templates/.claude/skills/build-automation/references/feature-flag-patterns.md
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
# Feature Flag Patterns
|
|
2
|
+
|
|
3
|
+
How to conditionally enable/disable individual slice components. Examples use a generic `Ordering` bounded context.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Option 1: Spring Boot `@ConditionalOnProperty` (default)
|
|
8
|
+
|
|
9
|
+
Add `@ConditionalOnProperty` to every conditionally-loaded component in the slice. Update config files
|
|
10
|
+
and Spring metadata so the IDE auto-completes property keys.
|
|
11
|
+
|
|
12
|
+
### Annotation on slice components
|
|
13
|
+
|
|
14
|
+
```java
|
|
15
|
+
// Entity
|
|
16
|
+
@ConditionalOnProperty(prefix = "slices.ordering", name = "write.placeorder.enabled")
|
|
17
|
+
@EventSourced(tagKey = EventTags.ORDER_ID)
|
|
18
|
+
class PlaceOrderEntity { ... }
|
|
19
|
+
|
|
20
|
+
// Handler
|
|
21
|
+
@ConditionalOnProperty(prefix = "slices.ordering", name = "write.placeorder.enabled")
|
|
22
|
+
@Component
|
|
23
|
+
public class PlaceOrderHandler { ... }
|
|
24
|
+
|
|
25
|
+
// REST controller (if applicable)
|
|
26
|
+
@ConditionalOnProperty(prefix = "slices.ordering", name = "write.placeorder.enabled")
|
|
27
|
+
@RestController
|
|
28
|
+
public class PlaceOrderRestController { ... }
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
For the **Explicit Registration pattern**, put `@ConditionalOnProperty` on the `@Configuration` class only:
|
|
32
|
+
|
|
33
|
+
```java
|
|
34
|
+
@ConditionalOnProperty(prefix = "slices.ordering", name = "write.placeorder.enabled")
|
|
35
|
+
@Configuration
|
|
36
|
+
class PlaceOrderWriteSliceConfig { ... }
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
### `application.properties` (main — enable by default)
|
|
40
|
+
|
|
41
|
+
```properties
|
|
42
|
+
slices.ordering.write.placeorder.enabled=true
|
|
43
|
+
slices.ordering.read.getorders.enabled=true
|
|
44
|
+
slices.ordering.automation.notifycustomeronorder.enabled=true
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
### `application.properties` (test — disable by default, opt-in per test)
|
|
48
|
+
|
|
49
|
+
```properties
|
|
50
|
+
slices.ordering.write.placeorder.enabled=false
|
|
51
|
+
slices.ordering.read.getorders.enabled=false
|
|
52
|
+
slices.ordering.automation.notifycustomeronorder.enabled=false
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
### Opt-in in a specific test class
|
|
56
|
+
|
|
57
|
+
```java
|
|
58
|
+
@TestPropertySource(properties = {"slices.ordering.write.placeorder.enabled=true"})
|
|
59
|
+
@SpringBootTest
|
|
60
|
+
class PlaceOrderTest { ... }
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
### `META-INF/additional-spring-configuration-metadata.json`
|
|
64
|
+
|
|
65
|
+
Register each property so IDEs auto-complete and validate it:
|
|
66
|
+
|
|
67
|
+
```json
|
|
68
|
+
{
|
|
69
|
+
"properties": [
|
|
70
|
+
{
|
|
71
|
+
"name": "slices.ordering.write.placeorder.enabled",
|
|
72
|
+
"type": "java.lang.Boolean",
|
|
73
|
+
"description": "Enable/disable the PlaceOrder write slice in the Ordering bounded context."
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
"name": "slices.ordering.read.getorders.enabled",
|
|
77
|
+
"type": "java.lang.Boolean",
|
|
78
|
+
"description": "Enable/disable the GetOrders read slice in the Ordering bounded context."
|
|
79
|
+
}
|
|
80
|
+
]
|
|
81
|
+
}
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
---
|
|
85
|
+
|
|
86
|
+
## Option 2: Spring Profile (`@Profile`)
|
|
87
|
+
|
|
88
|
+
Enable/disable components by activating a named Spring profile. Simpler for "all or nothing" rollouts,
|
|
89
|
+
less granular than `@ConditionalOnProperty`.
|
|
90
|
+
|
|
91
|
+
```java
|
|
92
|
+
@Profile("ordering-write")
|
|
93
|
+
@Component
|
|
94
|
+
public class PlaceOrderHandler { ... }
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
Activate in tests via `@ActiveProfiles("ordering-write")` or `spring.profiles.active=ordering-write`.
|
|
98
|
+
|
|
99
|
+
---
|
|
100
|
+
|
|
101
|
+
## Option 3: No feature flags
|
|
102
|
+
|
|
103
|
+
If the project ships all slices unconditionally, omit `@ConditionalOnProperty` entirely. Applicable when:
|
|
104
|
+
- The project is small and startup time is not a concern
|
|
105
|
+
- There is no partial-rollout requirement
|
|
106
|
+
- Feature branches are the rollout mechanism
|
|
107
|
+
|
|
108
|
+
---
|
|
109
|
+
|
|
110
|
+
## Convention recommendation
|
|
111
|
+
|
|
112
|
+
- Use `@ConditionalOnProperty` for new projects: zero extra dependencies, IDE-friendly, test-friendly.
|
|
113
|
+
- Disable all slices by default in the test properties; opt-in per test class via `@TestPropertySource`.
|
|
114
|
+
- Always enable BOTH the slice under test AND its dependencies (e.g., the automation AND the target write slice).
|
|
@@ -0,0 +1,496 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: build-automation-workflow
|
|
3
|
+
authors:
|
|
4
|
+
- Martin Dilger
|
|
5
|
+
description: >
|
|
6
|
+
Analyze a list of automation slices and translate appropriate ones into Axon Framework 5 Workflows
|
|
7
|
+
(long-running, durable, multi-step processes) instead of plain @EventHandler automations.
|
|
8
|
+
Determines WHICH slices need a Workflow, then implements them using the AF5 Workflow engine.
|
|
9
|
+
Use when: (1) given a set of automation slice.json definitions, decide which ones should be
|
|
10
|
+
Workflows; (2) user asks to "convert automation to workf
|
|
11
|
+
low", "implement as workflow",
|
|
12
|
+
or "use Axon Workflow for this slice"; (3) a slice has multi-step logic, needs to wait for
|
|
13
|
+
external input/approval, requires compensation/rollback, or spans significant time.
|
|
14
|
+
NOTE: AF5 Workflows are in Preview — APIs may change; not intended for production use yet.
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
# Build Automation Workflow (Java / Axon Framework 5)
|
|
18
|
+
|
|
19
|
+
> **Preview feature** — AF5 Workflow APIs may change without notice and are not yet for production.
|
|
20
|
+
|
|
21
|
+
New workflow slices live under
|
|
22
|
+
`src/main/java/de/eventmodelers/slices/{context}/automation/{slicename}/`.
|
|
23
|
+
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
## Step 0: Discover Project Conventions
|
|
27
|
+
|
|
28
|
+
> **Comments & description**: Each element in the slice carries a `comments: string[]` array and a `description` field. Use these as implementation hints. When done, resolve each used comment: `POST <BASE_URL>/api/org/<ORG_ID>/boards/<BOARD_ID>/nodes/<nodeId>/comments/<commentId>/resolve` (get IDs first via GET on same path).
|
|
29
|
+
|
|
30
|
+
Read `CLAUDE.md` and check whether the project already uses AF5 Workflows. Look for:
|
|
31
|
+
|
|
32
|
+
- A `WorkflowModule` bean in any `@Configuration` class
|
|
33
|
+
- Classes annotated with `@Workflow`
|
|
34
|
+
- Existing `*Workflow.java` files under any slice package
|
|
35
|
+
- The `WorkflowModule` dependency in `pom.xml`:
|
|
36
|
+
|
|
37
|
+
```xml
|
|
38
|
+
<dependency>
|
|
39
|
+
<groupId>io.axoniq.framework</groupId>
|
|
40
|
+
<artifactId>axoniq-workflows</artifactId>
|
|
41
|
+
<!-- version managed by the BOM -->
|
|
42
|
+
</dependency>
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
If the module is missing from the pom, add it before implementing any workflow.
|
|
46
|
+
|
|
47
|
+
---
|
|
48
|
+
|
|
49
|
+
## Step 1: Analyze Automation Slices — Which Ones Need a Workflow?
|
|
50
|
+
|
|
51
|
+
Read every slice.json under `.build-kit/.slices/{context}/` where `processors` is not empty,
|
|
52
|
+
or where the user specifies a list of slices to analyze.
|
|
53
|
+
|
|
54
|
+
For each automation slice, apply this decision table:
|
|
55
|
+
|
|
56
|
+
| Signal in the slice definition | Decision |
|
|
57
|
+
|---|---|
|
|
58
|
+
| Single trigger event → single command, no waiting | **Plain automation** (`build-automation`) |
|
|
59
|
+
| Multi-step: trigger → command → wait for event → another command | **Workflow** |
|
|
60
|
+
| Process must wait for human approval or external confirmation | **Workflow** |
|
|
61
|
+
| Compensation/rollback: if step N fails, undo steps 1..N-1 | **Workflow** |
|
|
62
|
+
| Steps involve a timer / scheduled delay | **Workflow** |
|
|
63
|
+
| Fan-out: one trigger dispatches to many entities then waits for all | **Workflow** |
|
|
64
|
+
| Parent-child: one process spawns another and waits for its completion | **Workflow** (sub-workflow) |
|
|
65
|
+
| Scatter-gather: send to many, process replies as they arrive | **Workflow** |
|
|
66
|
+
|
|
67
|
+
For each slice, output a classification:
|
|
68
|
+
|
|
69
|
+
```
|
|
70
|
+
Slice: "ProcessOrderFulfillment" → WORKFLOW (reason: multi-step with compensation)
|
|
71
|
+
Slice: "SendWelcomeEmail" → AUTOMATION (reason: single-step fire-and-forget)
|
|
72
|
+
Slice: "ApproveExpenseReport" → WORKFLOW (reason: human approval with timeout/escalation)
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
Ask the user to confirm before proceeding if any classification is ambiguous.
|
|
76
|
+
|
|
77
|
+
---
|
|
78
|
+
|
|
79
|
+
## Step 2: Ensure Events and Commands Exist
|
|
80
|
+
|
|
81
|
+
For each workflow slice, verify all trigger events and dispatched commands exist. If they don't,
|
|
82
|
+
create them **first** following `build-state-change` Step 4a (sealed event interface + concrete
|
|
83
|
+
event records).
|
|
84
|
+
|
|
85
|
+
---
|
|
86
|
+
|
|
87
|
+
## Step 3: Implement the Workflow
|
|
88
|
+
|
|
89
|
+
### 3a — Annotated workflow class (default)
|
|
90
|
+
|
|
91
|
+
The simplest way to define a workflow using `@Workflow`:
|
|
92
|
+
|
|
93
|
+
```java
|
|
94
|
+
// File: {slicename}/{SliceName}Workflow.java
|
|
95
|
+
package de.eventmodelers.slices.{context}.automation.{slicename};
|
|
96
|
+
|
|
97
|
+
import org.axonframework.workflow.annotation.Workflow;
|
|
98
|
+
import org.axonframework.workflow.SimpleWorkflowContext;
|
|
99
|
+
|
|
100
|
+
@Workflow(
|
|
101
|
+
idProperty = "{triggerEventIdField}", // field name on the trigger event that becomes the workflow ID
|
|
102
|
+
startOnEventClass = {TriggerEvent}.class, // event that starts a new workflow instance
|
|
103
|
+
workflowNamespace = "de.eventmodelers.slices.{context}"
|
|
104
|
+
)
|
|
105
|
+
public class {SliceName}Workflow {
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* The main workflow method. Runs as a durable, resumable coroutine.
|
|
109
|
+
* ctx.awaitExecute() blocks until the step completes or fails.
|
|
110
|
+
* ctx.waitForEvent() suspends until an external event arrives.
|
|
111
|
+
*/
|
|
112
|
+
public void execute(SimpleWorkflowContext ctx) {
|
|
113
|
+
var orderId = (String) ctx.workflowPayload().get("{triggerEventIdField}");
|
|
114
|
+
|
|
115
|
+
// Step 1: dispatch the first command and wait for it to complete
|
|
116
|
+
ctx.awaitExecute("step1-{commandName}",
|
|
117
|
+
payload("{field}", ctx.workflowPayload().get("{field}")).getValues(),
|
|
118
|
+
{ServiceInterface}::{methodName});
|
|
119
|
+
|
|
120
|
+
// Step 2: wait for a confirmation event before continuing
|
|
121
|
+
var confirmation = ctx.waitForEvent("step2-await{EventName}",
|
|
122
|
+
{ConfirmationEvent}.class,
|
|
123
|
+
Duration.ofMinutes(30));
|
|
124
|
+
|
|
125
|
+
if (confirmation.timeout()) {
|
|
126
|
+
ctx.fail(new RuntimeException("No {ConfirmationEvent} received within 30 minutes"));
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
confirmation.await(); // blocks until event arrives (already arrived since !timeout())
|
|
131
|
+
|
|
132
|
+
// Step 3: dispatch a second command based on the confirmation
|
|
133
|
+
ctx.awaitExecute("step3-{secondCommandName}",
|
|
134
|
+
payload("{field}", confirmation.result()).getValues(),
|
|
135
|
+
{ServiceInterface}::{secondMethodName});
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
### 3b — Key context API
|
|
141
|
+
|
|
142
|
+
| Method | What it does |
|
|
143
|
+
|---|---|
|
|
144
|
+
| `ctx.awaitExecute(stepName, payload, handler)` | Dispatches a command/action and **blocks** until completion |
|
|
145
|
+
| `ctx.execute(stepName, payload, handler, timeout, options)` | Dispatches **without blocking** — returns `WorkflowStepResult` immediately |
|
|
146
|
+
| `ctx.waitForEvent(stepName, EventClass.class, timeout)` | **Suspends** the workflow until the event arrives or the timeout fires |
|
|
147
|
+
| `ctx.awaitEvent(stepName, EventClass.class, timeout)` | Same as `waitForEvent` but also matches using an association predicate |
|
|
148
|
+
| `ctx.sleep(stepName, duration)` | Durably suspends for a duration (survives restarts) |
|
|
149
|
+
| `ctx.allMatch(predicate, steps...)` | Fan-in barrier: waits until all steps satisfy the predicate |
|
|
150
|
+
| `ctx.anyMatch(predicate, steps...)` | Scatter-gather: waits until any step satisfies the predicate |
|
|
151
|
+
| `ctx.setPayload(stepName, newPayload)` | Persists state to the durable workflow payload |
|
|
152
|
+
| `ctx.workflowId()` | The workflow instance ID (derived from the trigger event) |
|
|
153
|
+
| `ctx.workflowPayload()` | Current durable payload as `Map<String, Object>` |
|
|
154
|
+
| `ctx.fail(exception)` | Terminates the workflow with FAILED status |
|
|
155
|
+
| `ctx.cancel()` | Terminates the workflow with CANCELLED status |
|
|
156
|
+
|
|
157
|
+
The `handler` argument in `awaitExecute`/`execute` is a `BiFunction<ProcessingContext, Map<String, Object>, Map<String, Object>>` —
|
|
158
|
+
typically a Spring bean method reference: `MyService::doWork`.
|
|
159
|
+
|
|
160
|
+
### 3c — Service handler beans
|
|
161
|
+
|
|
162
|
+
Workflow step handlers are Spring beans registered in the `ProcessingContext`. For each step,
|
|
163
|
+
create a Spring service bean:
|
|
164
|
+
|
|
165
|
+
```java
|
|
166
|
+
// File: {slicename}/{SliceName}WorkflowServices.java
|
|
167
|
+
@Component
|
|
168
|
+
@ConditionalOnProperty(prefix = "slices.{context}.automation", name = "{slicename}.enabled")
|
|
169
|
+
public class {SliceName}WorkflowServices {
|
|
170
|
+
|
|
171
|
+
private final CommandGateway commandGateway;
|
|
172
|
+
|
|
173
|
+
public {SliceName}WorkflowServices(CommandGateway commandGateway) {
|
|
174
|
+
this.commandGateway = commandGateway;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/** Step 1: reserve/validate. Returns result map for next steps. */
|
|
178
|
+
public Map<String, Object> step1(ProcessingContext ctx, Map<String, Object> payload) {
|
|
179
|
+
var command = new {SliceName}Command(
|
|
180
|
+
(String) payload.get("{field}")
|
|
181
|
+
// map other fields
|
|
182
|
+
);
|
|
183
|
+
commandGateway.sendAndWait(command);
|
|
184
|
+
return Map.of(); // return data the next step needs, or empty map
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/** Step 2: compensate step 1 on failure. */
|
|
188
|
+
public Map<String, Object> compensateStep1(ProcessingContext ctx, Map<String, Object> payload) {
|
|
189
|
+
var command = new Compensate{SliceName}Command((String) payload.get("{field}"));
|
|
190
|
+
commandGateway.sendAndWait(command);
|
|
191
|
+
return Map.of();
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
### 3d — Register the workflow module
|
|
197
|
+
|
|
198
|
+
Add the `WorkflowModule` to the Spring configuration. **Create once per bounded context**
|
|
199
|
+
(not once per slice).
|
|
200
|
+
|
|
201
|
+
```java
|
|
202
|
+
// File: {context}/WorkflowConfiguration.java
|
|
203
|
+
@Configuration
|
|
204
|
+
@ConditionalOnProperty(prefix = "slices.{context}", name = "workflows.enabled")
|
|
205
|
+
public class {Context}WorkflowConfiguration {
|
|
206
|
+
|
|
207
|
+
@Bean
|
|
208
|
+
public EntityModule<?, ?> {context}WorkflowModule() {
|
|
209
|
+
return WorkflowModule
|
|
210
|
+
.usingContext(SimpleWorkflowContext.class)
|
|
211
|
+
.workflowContextFactory(c -> new SimpleWorkflowContextFactory())
|
|
212
|
+
.workflowExecutionFactory(c ->
|
|
213
|
+
new DSLAdoptingExecutionFactory<>(SimpleWorkflowContext.class))
|
|
214
|
+
.definitions(d -> d
|
|
215
|
+
// autodetected: reads @Workflow annotations from registered beans
|
|
216
|
+
.autodetected(c -> new {SliceName}Workflow(), SimpleWorkflowContext.class)
|
|
217
|
+
// add more workflows in the same chain:
|
|
218
|
+
// .autodetected(c -> new {OtherSlice}Workflow(), SimpleWorkflowContext.class)
|
|
219
|
+
);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
---
|
|
225
|
+
|
|
226
|
+
## Step 4: Common Patterns
|
|
227
|
+
|
|
228
|
+
### Compensating transaction (saga)
|
|
229
|
+
|
|
230
|
+
```java
|
|
231
|
+
public void execute(SimpleWorkflowContext ctx) {
|
|
232
|
+
var id = (String) ctx.workflowPayload().get("id");
|
|
233
|
+
|
|
234
|
+
// Step 1
|
|
235
|
+
ctx.awaitExecute("reserveStock",
|
|
236
|
+
payload("id", id).getValues(),
|
|
237
|
+
services::reserveStock);
|
|
238
|
+
|
|
239
|
+
// Step 2 — compensate step 1 on failure
|
|
240
|
+
try {
|
|
241
|
+
ctx.awaitExecute("chargePayment",
|
|
242
|
+
payload("id", id, "amount", ctx.workflowPayload().get("amount")).getValues(),
|
|
243
|
+
services::charge);
|
|
244
|
+
} catch (StepFailedException e) {
|
|
245
|
+
ctx.awaitExecute("releaseStock",
|
|
246
|
+
payload("id", id).getValues(),
|
|
247
|
+
services::releaseStock);
|
|
248
|
+
ctx.fail(new RuntimeException("Payment failed: " + e.getMessage()));
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// Step 3 — compensate steps 2 and 1 on failure
|
|
253
|
+
try {
|
|
254
|
+
ctx.awaitExecute("shipOrder",
|
|
255
|
+
payload("id", id).getValues(),
|
|
256
|
+
services::shipOrder);
|
|
257
|
+
} catch (StepFailedException e) {
|
|
258
|
+
ctx.awaitExecute("refundPayment",
|
|
259
|
+
payload("id", id).getValues(),
|
|
260
|
+
services::refundPayment);
|
|
261
|
+
ctx.awaitExecute("releaseStock",
|
|
262
|
+
payload("id", id).getValues(),
|
|
263
|
+
services::releaseStock);
|
|
264
|
+
ctx.fail(new RuntimeException("Shipping failed: " + e.getMessage()));
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
### Human-in-the-loop (approval with escalation)
|
|
270
|
+
|
|
271
|
+
```java
|
|
272
|
+
public void execute(SimpleWorkflowContext ctx) {
|
|
273
|
+
// Request approval
|
|
274
|
+
ctx.awaitExecute("requestApproval",
|
|
275
|
+
payload("approver", "team-lead", "requestId", ctx.workflowId()).getValues(),
|
|
276
|
+
services::sendApprovalRequest);
|
|
277
|
+
|
|
278
|
+
// Wait up to 3 days for a response
|
|
279
|
+
var decision = ctx.waitForEvent("awaitApproval",
|
|
280
|
+
ApprovalDecisionEvent.class,
|
|
281
|
+
Duration.ofDays(3));
|
|
282
|
+
|
|
283
|
+
if (decision.timeout()) {
|
|
284
|
+
// Escalate to department head
|
|
285
|
+
ctx.awaitExecute("escalate",
|
|
286
|
+
payload("approver", "department-head", "requestId", ctx.workflowId()).getValues(),
|
|
287
|
+
services::escalate);
|
|
288
|
+
|
|
289
|
+
decision = ctx.waitForEvent("awaitEscalatedApproval",
|
|
290
|
+
ApprovalDecisionEvent.class,
|
|
291
|
+
Duration.ofDays(1));
|
|
292
|
+
|
|
293
|
+
if (decision.timeout()) {
|
|
294
|
+
ctx.fail(new RuntimeException("No approval received after escalation"));
|
|
295
|
+
return;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
decision.await();
|
|
300
|
+
// decision.result() contains the ApprovalDecisionEvent payload
|
|
301
|
+
}
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
### Fan-out / Fan-in
|
|
305
|
+
|
|
306
|
+
```java
|
|
307
|
+
public void execute(SimpleWorkflowContext ctx) {
|
|
308
|
+
var items = (List<Map<String, Object>>) ctx.workflowPayload().get("lineItems");
|
|
309
|
+
|
|
310
|
+
// Fan-out: launch reservations concurrently
|
|
311
|
+
var reservations = items.stream()
|
|
312
|
+
.map(item -> ctx.execute(
|
|
313
|
+
"reserve-" + item.get("sku"),
|
|
314
|
+
payload("sku", item.get("sku"), "qty", item.get("quantity")).getValues(),
|
|
315
|
+
services::reserveStock,
|
|
316
|
+
Duration.ofSeconds(30),
|
|
317
|
+
defaults()))
|
|
318
|
+
.toArray(WorkflowStepResult[]::new);
|
|
319
|
+
|
|
320
|
+
// Fan-in: wait for all
|
|
321
|
+
var guard = ctx.allMatch(WorkflowStepResult::isCompleted, reservations);
|
|
322
|
+
|
|
323
|
+
if (!guard.success()) {
|
|
324
|
+
for (var step : guard.unmatched()) {
|
|
325
|
+
if (!step.isCompleted()) step.cancel("Partial failure");
|
|
326
|
+
}
|
|
327
|
+
ctx.fail(new RuntimeException("Not all items could be reserved"));
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
```
|
|
331
|
+
|
|
332
|
+
### Lifecycle listeners
|
|
333
|
+
|
|
334
|
+
```java
|
|
335
|
+
@Workflow(idProperty = "id", startOnEventClass = OrderPlaced.class,
|
|
336
|
+
workflowNamespace = "de.eventmodelers.slices.{context}")
|
|
337
|
+
public class {SliceName}Workflow {
|
|
338
|
+
|
|
339
|
+
public void execute(SimpleWorkflowContext ctx) { /* ... */ }
|
|
340
|
+
|
|
341
|
+
@OnSuccess
|
|
342
|
+
public void onSuccess(SimpleWorkflowContext ctx) {
|
|
343
|
+
log.info("Workflow {} completed for id={}", ctx.workflowId(),
|
|
344
|
+
ctx.workflowPayload().get("id"));
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
@OnFailure
|
|
348
|
+
public void onFailure(SimpleWorkflowContext ctx) {
|
|
349
|
+
log.error("Workflow {} failed for id={}", ctx.workflowId(),
|
|
350
|
+
ctx.workflowPayload().get("id"));
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
@OnTimeout
|
|
354
|
+
public void onTimeout(SimpleWorkflowContext ctx) {
|
|
355
|
+
log.warn("Workflow {} timed out", ctx.workflowId());
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
```
|
|
359
|
+
|
|
360
|
+
---
|
|
361
|
+
|
|
362
|
+
## Step 5: Feature Flag Configuration
|
|
363
|
+
|
|
364
|
+
Follow the same pattern as other slices:
|
|
365
|
+
|
|
366
|
+
`src/main/resources/application.properties`:
|
|
367
|
+
```properties
|
|
368
|
+
slices.{context}.automation.{slicename}.enabled=true
|
|
369
|
+
slices.{context}.workflows.enabled=true
|
|
370
|
+
```
|
|
371
|
+
|
|
372
|
+
`src/test/resources/application.properties`:
|
|
373
|
+
```properties
|
|
374
|
+
slices.{context}.automation.{slicename}.enabled=false
|
|
375
|
+
slices.{context}.workflows.enabled=false
|
|
376
|
+
```
|
|
377
|
+
|
|
378
|
+
`META-INF/additional-spring-configuration-metadata.json` — add entries for the new properties.
|
|
379
|
+
|
|
380
|
+
See [references/feature-flag-patterns.md](references/feature-flag-patterns.md) for the full pattern.
|
|
381
|
+
|
|
382
|
+
---
|
|
383
|
+
|
|
384
|
+
## Step 6: Tests
|
|
385
|
+
|
|
386
|
+
### Unit tests — test `execute()` logic with mock services
|
|
387
|
+
|
|
388
|
+
For simple workflows, stub the service methods and verify step sequencing:
|
|
389
|
+
|
|
390
|
+
```java
|
|
391
|
+
class {SliceName}WorkflowTest {
|
|
392
|
+
|
|
393
|
+
private {SliceName}Workflow workflow;
|
|
394
|
+
private {SliceName}WorkflowServices services;
|
|
395
|
+
private SimpleWorkflowContext ctx;
|
|
396
|
+
|
|
397
|
+
@BeforeEach
|
|
398
|
+
void setUp() {
|
|
399
|
+
services = mock({SliceName}WorkflowServices.class);
|
|
400
|
+
workflow = new {SliceName}Workflow(services);
|
|
401
|
+
ctx = mock(SimpleWorkflowContext.class);
|
|
402
|
+
// stub workflowId() and workflowPayload() as needed
|
|
403
|
+
when(ctx.workflowId()).thenReturn("wf-1");
|
|
404
|
+
when(ctx.workflowPayload()).thenReturn(Map.of("id", "entity-1"));
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
@Test
|
|
408
|
+
@DisplayName("given trigger event, workflow executes all steps in order")
|
|
409
|
+
void happyPath() {
|
|
410
|
+
// stub ctx.awaitExecute to return normally
|
|
411
|
+
doNothing().when(ctx).awaitExecute(anyString(), anyMap(), any());
|
|
412
|
+
var mockEvent = mock(WorkflowStepResult.class);
|
|
413
|
+
when(ctx.waitForEvent(anyString(), any(), any())).thenReturn(mockEvent);
|
|
414
|
+
when(mockEvent.timeout()).thenReturn(false);
|
|
415
|
+
|
|
416
|
+
workflow.execute(ctx);
|
|
417
|
+
|
|
418
|
+
// verify steps were called in order
|
|
419
|
+
var inOrder = inOrder(ctx);
|
|
420
|
+
inOrder.verify(ctx).awaitExecute(eq("step1-{commandName}"), anyMap(), any());
|
|
421
|
+
inOrder.verify(ctx).waitForEvent(eq("step2-await{EventName}"), any(), any());
|
|
422
|
+
inOrder.verify(ctx).awaitExecute(eq("step3-{secondCommandName}"), anyMap(), any());
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
@Test
|
|
426
|
+
@DisplayName("given step2 times out, workflow fails")
|
|
427
|
+
void timeoutFails() {
|
|
428
|
+
doNothing().when(ctx).awaitExecute(anyString(), anyMap(), any());
|
|
429
|
+
var timedOut = mock(WorkflowStepResult.class);
|
|
430
|
+
when(ctx.waitForEvent(anyString(), any(), any())).thenReturn(timedOut);
|
|
431
|
+
when(timedOut.timeout()).thenReturn(true);
|
|
432
|
+
|
|
433
|
+
workflow.execute(ctx);
|
|
434
|
+
|
|
435
|
+
verify(ctx).fail(any(RuntimeException.class));
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
@Test
|
|
439
|
+
@DisplayName("given step2 payment fails, step1 stock is released")
|
|
440
|
+
void compensatesOnPaymentFailure() {
|
|
441
|
+
doNothing().when(ctx).awaitExecute(eq("reserveStock"), anyMap(), any());
|
|
442
|
+
doThrow(new StepFailedException("insufficient funds"))
|
|
443
|
+
.when(ctx).awaitExecute(eq("chargePayment"), anyMap(), any());
|
|
444
|
+
|
|
445
|
+
workflow.execute(ctx);
|
|
446
|
+
|
|
447
|
+
var inOrder = inOrder(ctx);
|
|
448
|
+
inOrder.verify(ctx).awaitExecute(eq("chargePayment"), anyMap(), any());
|
|
449
|
+
inOrder.verify(ctx).awaitExecute(eq("releaseStock"), anyMap(), any());
|
|
450
|
+
inOrder.verify(ctx).fail(any());
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
```
|
|
454
|
+
|
|
455
|
+
### Integration tests — `AbstractDeclarativeTestBase`
|
|
456
|
+
|
|
457
|
+
For full integration tests, use AF5's `AbstractDeclarativeTestBase` (requires the workflow module
|
|
458
|
+
on the test classpath):
|
|
459
|
+
|
|
460
|
+
```java
|
|
461
|
+
@SpringBootTest
|
|
462
|
+
@TestPropertySource(properties = {
|
|
463
|
+
"slices.{context}.workflows.enabled=true",
|
|
464
|
+
"slices.{context}.automation.{slicename}.enabled=true"
|
|
465
|
+
})
|
|
466
|
+
class {SliceName}WorkflowIntegrationTest extends AbstractDeclarativeTestBase {
|
|
467
|
+
|
|
468
|
+
// AbstractDeclarativeTestBase provides fixture methods for
|
|
469
|
+
// publishing events and asserting workflow state/steps.
|
|
470
|
+
// Refer to the Axon Framework test documentation for the full API.
|
|
471
|
+
}
|
|
472
|
+
```
|
|
473
|
+
|
|
474
|
+
---
|
|
475
|
+
|
|
476
|
+
## Files to Create / Modify
|
|
477
|
+
|
|
478
|
+
```
|
|
479
|
+
src/main/java/de/eventmodelers/slices/{context}/automation/{slicename}/
|
|
480
|
+
├── {SliceName}Workflow.java ← @Workflow class (execute method + lifecycle listeners)
|
|
481
|
+
└── {SliceName}WorkflowServices.java ← @Component step handlers (Spring beans)
|
|
482
|
+
|
|
483
|
+
src/main/java/de/eventmodelers/slices/{context}/
|
|
484
|
+
└── WorkflowConfiguration.java ← WorkflowModule bean (one per context, create if missing)
|
|
485
|
+
|
|
486
|
+
src/test/java/de/eventmodelers/slices/{context}/automation/{slicename}/
|
|
487
|
+
└── {SliceName}WorkflowTest.java ← unit tests with mocked context + services
|
|
488
|
+
```
|
|
489
|
+
|
|
490
|
+
---
|
|
491
|
+
|
|
492
|
+
## References
|
|
493
|
+
|
|
494
|
+
- [Axon Workflow API](references/axon-workflow-api.md) — Annotations, context API, configuration, patterns
|
|
495
|
+
- [Feature Flag Patterns](references/feature-flag-patterns.md) — `@ConditionalOnProperty` and alternatives
|
|
496
|
+
- [Axon Workflow documentation](https://docs.axoniq.io/axon-framework-reference/development/workflows/) — Official reference (Preview)
|