@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/cratis-csharp/templates/.claude/skills/build-state-change/references/patterns.md
ADDED
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
# Write Slice Patterns — Cratis Arc + Chronicle
|
|
2
|
+
|
|
3
|
+
Complete code for state-change (write) slices. All artifacts go in a single
|
|
4
|
+
`<Module>/<Feature>/<Slice>/<Slice>.cs` file (the shipped starter uses `SomeModule/SomeFeature/...` —
|
|
5
|
+
match whatever the existing slices use). The example slices use no file header (match the project).
|
|
6
|
+
Namespace mirrors the folders and drops any `.Features.`. The examples below use short illustrative
|
|
7
|
+
names (`MyApp.Authors.Registration`); mirror your project's actual module/feature layout instead.
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## 1. Concept (one per file, in the feature folder)
|
|
12
|
+
|
|
13
|
+
```csharp
|
|
14
|
+
// Features/Authors/AuthorId.cs
|
|
15
|
+
namespace MyApp.Authors;
|
|
16
|
+
|
|
17
|
+
public record AuthorId(Guid Value) : ConceptAs<Guid>(Value)
|
|
18
|
+
{
|
|
19
|
+
public static readonly AuthorId NotSet = new(Guid.Empty);
|
|
20
|
+
public static implicit operator Guid(AuthorId id) => id.Value;
|
|
21
|
+
public static implicit operator AuthorId(Guid value) => new(value);
|
|
22
|
+
public static implicit operator EventSourceId(AuthorId id) => new(id.Value.ToString());
|
|
23
|
+
public static AuthorId New() => new(Guid.NewGuid());
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Features/Authors/AuthorName.cs
|
|
27
|
+
public record AuthorName(string Value) : ConceptAs<string>(Value)
|
|
28
|
+
{
|
|
29
|
+
public static readonly AuthorName NotSet = new(string.Empty);
|
|
30
|
+
public static implicit operator string(AuthorName name) => name.Value;
|
|
31
|
+
public static implicit operator AuthorName(string value) => new(value);
|
|
32
|
+
}
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
---
|
|
36
|
+
|
|
37
|
+
## 2. Simplest write slice — single event
|
|
38
|
+
|
|
39
|
+
```csharp
|
|
40
|
+
// Features/Authors/Registration/Registration.cs
|
|
41
|
+
namespace MyApp.Authors.Registration;
|
|
42
|
+
|
|
43
|
+
using Cratis.Arc.Commands.ModelBound;
|
|
44
|
+
using Cratis.Chronicle.Events;
|
|
45
|
+
|
|
46
|
+
[Command]
|
|
47
|
+
public record RegisterAuthor([Key] AuthorId Id, AuthorName Name)
|
|
48
|
+
{
|
|
49
|
+
public AuthorRegistered Handle() => new(Name);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
[EventType]
|
|
53
|
+
public record AuthorRegistered(AuthorName Name);
|
|
54
|
+
|
|
55
|
+
public class RegisterAuthorValidator : CommandValidator<RegisterAuthor>
|
|
56
|
+
{
|
|
57
|
+
public RegisterAuthorValidator() =>
|
|
58
|
+
RuleFor(c => c.Name).NotEmpty().WithMessage("Name is required").MaximumLength(100);
|
|
59
|
+
}
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
The `[Key]` parameter is the event source id. With a concept that converts to `EventSourceId`, you may
|
|
63
|
+
omit `[Key]` and let Arc resolve it.
|
|
64
|
+
|
|
65
|
+
---
|
|
66
|
+
|
|
67
|
+
## 3. Generate-and-return a new id
|
|
68
|
+
|
|
69
|
+
```csharp
|
|
70
|
+
[Command]
|
|
71
|
+
public record RegisterAuthor(AuthorName Name)
|
|
72
|
+
{
|
|
73
|
+
public (AuthorId, AuthorRegistered) Handle()
|
|
74
|
+
{
|
|
75
|
+
var id = AuthorId.New();
|
|
76
|
+
return (id, new AuthorRegistered(Name)); // id flows back as CommandResult<AuthorId>.response
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
---
|
|
82
|
+
|
|
83
|
+
## 4. Multiple events
|
|
84
|
+
|
|
85
|
+
```csharp
|
|
86
|
+
[Command]
|
|
87
|
+
public record TransferFunds(AccountId FromId, AccountId ToId, decimal Amount)
|
|
88
|
+
{
|
|
89
|
+
public IEnumerable<object> Handle() =>
|
|
90
|
+
[
|
|
91
|
+
new FundsWithdrawn(FromId, Amount),
|
|
92
|
+
new FundsDeposited(ToId, Amount),
|
|
93
|
+
];
|
|
94
|
+
}
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
---
|
|
98
|
+
|
|
99
|
+
## 5. Injected dependency / constraint
|
|
100
|
+
|
|
101
|
+
`Handle()` parameters are DI-resolved. Use for external services or constraints (e.g. uniqueness):
|
|
102
|
+
|
|
103
|
+
```csharp
|
|
104
|
+
[Command]
|
|
105
|
+
public record OpenDebitAccount([Key] AccountId Id, string Name, OwnerId OwnerId)
|
|
106
|
+
{
|
|
107
|
+
public async Task<DebitAccountOpened> Handle(IUniqueAccountConstraint constraint)
|
|
108
|
+
{
|
|
109
|
+
await constraint.Validate(Name); // throws on violation
|
|
110
|
+
return new DebitAccountOpened(Name, OwnerId);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
---
|
|
116
|
+
|
|
117
|
+
## 6. Business rule via DCB (inject a read model)
|
|
118
|
+
|
|
119
|
+
When a rule depends on event-sourced state, accept the relevant read model as a `Handle()` parameter.
|
|
120
|
+
Arc materializes current state from the event log and injects it. Return a `Result<,>` for a clean
|
|
121
|
+
success/error contract:
|
|
122
|
+
|
|
123
|
+
```csharp
|
|
124
|
+
[Command]
|
|
125
|
+
public record ReserveBook([Key] BookId Id, MemberId Member)
|
|
126
|
+
{
|
|
127
|
+
public Result<BookReserved, ReservationError> Handle(BookAvailability availability)
|
|
128
|
+
{
|
|
129
|
+
if (availability is null || !availability.IsAvailable)
|
|
130
|
+
return ReservationError.NotAvailable;
|
|
131
|
+
return new BookReserved(Member);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
public enum ReservationError { NotAvailable }
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
Only encode rules that appear in the slice `description` / `comments`.
|
|
139
|
+
|
|
140
|
+
---
|
|
141
|
+
|
|
142
|
+
## 7. Integration spec (Chronicle, full stack)
|
|
143
|
+
|
|
144
|
+
`Features/Authors/Registration/when_registering/and_there_are_no_authors.cs`
|
|
145
|
+
|
|
146
|
+
```csharp
|
|
147
|
+
using context = MyApp.Authors.Registration.when_registering.and_there_are_no_authors.context;
|
|
148
|
+
|
|
149
|
+
namespace MyApp.Authors.Registration.when_registering;
|
|
150
|
+
|
|
151
|
+
[Collection(ChronicleCollection.Name)]
|
|
152
|
+
public class and_there_are_no_authors(context context) : Given<context>(context)
|
|
153
|
+
{
|
|
154
|
+
public class context(ChronicleOutOfProcessFixture fixture) : given.an_http_client(fixture)
|
|
155
|
+
{
|
|
156
|
+
public CommandResult<AuthorId>? Result;
|
|
157
|
+
|
|
158
|
+
async Task Because() =>
|
|
159
|
+
Result = await Client.ExecuteCommand<RegisterAuthor, AuthorId>(
|
|
160
|
+
"/api/authors/register",
|
|
161
|
+
new RegisterAuthor(AuthorId.New(), new AuthorName("John Doe")));
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
[Fact] void should_be_successful() => Context.Result!.IsSuccess.ShouldBeTrue();
|
|
165
|
+
[Fact] void should_have_appended_one_event() =>
|
|
166
|
+
Context.ShouldHaveTailSequenceNumber(EventSequenceNumber.First);
|
|
167
|
+
[Fact] void should_append_author_registered_event() =>
|
|
168
|
+
Context.ShouldHaveAppendedEvent<AuthorRegistered>(
|
|
169
|
+
EventSequenceNumber.First, Context.Result!.Response,
|
|
170
|
+
evt => evt.Name.Value.ShouldEqual("John Doe"));
|
|
171
|
+
}
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
### Pure unit spec on `Handle()` (no I/O)
|
|
175
|
+
|
|
176
|
+
```csharp
|
|
177
|
+
namespace MyApp.Authors.Registration.for_RegisterAuthor;
|
|
178
|
+
|
|
179
|
+
public class when_registering : Specification
|
|
180
|
+
{
|
|
181
|
+
RegisterAuthor _command;
|
|
182
|
+
AuthorRegistered _event;
|
|
183
|
+
|
|
184
|
+
void Establish() => _command = new(AuthorId.New(), new AuthorName("John"));
|
|
185
|
+
void Because() => _event = _command.Handle();
|
|
186
|
+
|
|
187
|
+
[Fact] void should_carry_the_name() => _event.Name.Value.ShouldEqual("John");
|
|
188
|
+
}
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
---
|
|
192
|
+
|
|
193
|
+
## 8. React command UI (after `dotnet build` generated the proxy)
|
|
194
|
+
|
|
195
|
+
The proxy (`RegisterAuthor.ts`) is generated **next to** `Registration.cs` (the starter's `.csproj`
|
|
196
|
+
sets `CratisProxiesUseSourceFileAsOutputFile=true`), so import it from the **same folder**. Add a barrel
|
|
197
|
+
`index.ts` (`export * from './AddAuthor'`) like the shipped example slice.
|
|
198
|
+
|
|
199
|
+
```tsx
|
|
200
|
+
// Authors/Registration/AddAuthor.tsx
|
|
201
|
+
import { CommandDialog } from '@cratis/components/CommandDialog';
|
|
202
|
+
import { InputTextField } from '@cratis/components/CommandForm';
|
|
203
|
+
import { RegisterAuthor } from './RegisterAuthor'; // co-located generated proxy
|
|
204
|
+
|
|
205
|
+
export const AddAuthor = () => (
|
|
206
|
+
<CommandDialog command={RegisterAuthor} title="Add author" okLabel="Add" cancelLabel="Cancel">
|
|
207
|
+
<InputTextField<RegisterAuthor> value={c => c.name} title="Name" />
|
|
208
|
+
</CommandDialog>
|
|
209
|
+
);
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
Inline form alternative:
|
|
213
|
+
|
|
214
|
+
```tsx
|
|
215
|
+
const [command] = RegisterAuthor.use();
|
|
216
|
+
const submit = async () => {
|
|
217
|
+
const result = await command.execute();
|
|
218
|
+
if (result.isSuccess) onSuccess(result.response);
|
|
219
|
+
};
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
Never import `Dialog` from `primereact/dialog` — use the Cratis wrappers. PrimeReact CSS variables for
|
|
223
|
+
colors. No `any`. Full descriptive variable names.
|
|
224
|
+
|
|
225
|
+
---
|
|
226
|
+
|
|
227
|
+
## Checklist
|
|
228
|
+
|
|
229
|
+
- One `.cs` file, namespace without `.Features.` (no file header unless the project uses one).
|
|
230
|
+
- `[Command]` with `Handle()` on the record — no handler class.
|
|
231
|
+
- `[EventType]` with **no** arguments; past-tense names; no nullable fields.
|
|
232
|
+
- `ConceptAs<T>` for every identity / value — no raw `Guid` / `string` in the domain.
|
|
233
|
+
- `CommandValidator<T>` for validations; read-model parameter for DCB rules.
|
|
234
|
+
- One spec per scenario in `slice.json`; `dotnet build` clean; specs pass.
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: build-state-view
|
|
3
|
+
description: >
|
|
4
|
+
Implement read slices the Cratis way — a [ReadModel] record with static query methods, fed by a
|
|
5
|
+
Chronicle projection or reducer, plus the React component that renders it. Use when: (1) implementing
|
|
6
|
+
a new read slice / projection in a Cratis project, (2) a slice.json has a non-empty readModel /
|
|
7
|
+
projections / queries section, (3) the user provides a read-slice Event Modeling artifact or
|
|
8
|
+
specification and asks to implement it, (4) the user says "implement", "create", "add" a read slice,
|
|
9
|
+
read model, projection, reducer, or query in a Cratis Arc / Chronicle project.
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
# Cratis — Read Slice (State View)
|
|
13
|
+
|
|
14
|
+
A read slice projects events into a queryable read model. In Cratis the path lives in **one `.cs`
|
|
15
|
+
file**:
|
|
16
|
+
|
|
17
|
+
```
|
|
18
|
+
[ReadModel] record + static queries → projection or reducer → dotnet build → React
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
> **Read first:** [../_shared/cratis-conventions.md](../_shared/cratis-conventions.md). Everything
|
|
22
|
+
> below assumes those rules.
|
|
23
|
+
|
|
24
|
+
## Step 0 — Discover conventions
|
|
25
|
+
|
|
26
|
+
Read `CLAUDE.md` and one existing read slice. Confirm the namespace root, how existing read models
|
|
27
|
+
declare queries (snapshot vs observable), whether projections use model-bound attributes or
|
|
28
|
+
`IProjectionFor<T>`, and the MongoDB collection wiring. Resolve slice `comments` when done (see the
|
|
29
|
+
state-change skill's Step 0 for the resolve endpoint).
|
|
30
|
+
|
|
31
|
+
## Step 1 — Understand the input (`slice.json` is the source of truth)
|
|
32
|
+
|
|
33
|
+
| Element | What to extract |
|
|
34
|
+
|---|---|
|
|
35
|
+
| **Read model** | Name, fields, the key field |
|
|
36
|
+
| **Source events** | Which events feed each field (projections **join events, never read models**) |
|
|
37
|
+
| **Queries** | The queries to expose; whether each is snapshot or real-time (observable) |
|
|
38
|
+
| **Specifications** | Each scenario → an executable spec proving the projection result |
|
|
39
|
+
|
|
40
|
+
If a field has no source event in `slice.json`, do not invent one. One read model per use case —
|
|
41
|
+
never reuse a model across slices.
|
|
42
|
+
|
|
43
|
+
## Step 2 — Write the slice `.cs` file
|
|
44
|
+
|
|
45
|
+
`<Module>/<Feature>/<Slice>/<Slice>.cs` — namespace `<Root>.<Module>.<Feature>.<Slice>` (mirror the folders; drop any `.Features.`).
|
|
46
|
+
|
|
47
|
+
### Read model + static query methods
|
|
48
|
+
```csharp
|
|
49
|
+
[ReadModel]
|
|
50
|
+
public record AuthorListItem(AuthorId Id, AuthorName Name, int BookCount)
|
|
51
|
+
{
|
|
52
|
+
public static async Task<IEnumerable<AuthorListItem>> AllAuthors(
|
|
53
|
+
IMongoCollection<AuthorListItem> collection)
|
|
54
|
+
=> await collection.Find(_ => true).ToListAsync();
|
|
55
|
+
|
|
56
|
+
public static async Task<AuthorListItem?> GetAuthor(
|
|
57
|
+
AuthorId id, IMongoCollection<AuthorListItem> collection)
|
|
58
|
+
=> await collection.Find(a => a.Id == id).FirstOrDefaultAsync();
|
|
59
|
+
|
|
60
|
+
// Real-time push — return ISubject<T> directly (never Task<ISubject<T>>)
|
|
61
|
+
public static ISubject<IEnumerable<AuthorListItem>> ObserveAllAuthors(
|
|
62
|
+
IMongoCollection<AuthorListItem> collection)
|
|
63
|
+
=> collection.Observe();
|
|
64
|
+
}
|
|
65
|
+
```
|
|
66
|
+
- `[ReadModel]` required. Query methods are **`public static`** on the record; the method name becomes
|
|
67
|
+
the TS query proxy class name. No controller, no `IReadModels`.
|
|
68
|
+
- Favor reactive (`ISubject<T>`) queries when the slice wants live updates.
|
|
69
|
+
|
|
70
|
+
### Choose projection vs reducer
|
|
71
|
+
- **Projection** — shaped read models, field mapping, joins, children (most read slices).
|
|
72
|
+
- **Reducer** — running aggregates (balances, counts, sums) where state accumulates across events.
|
|
73
|
+
|
|
74
|
+
### Projection (declarative; AutoMap on by default)
|
|
75
|
+
```csharp
|
|
76
|
+
public class AuthorListItemProjection : IProjectionFor<AuthorListItem>
|
|
77
|
+
{
|
|
78
|
+
public void Define(IProjectionBuilderFor<AuthorListItem> builder) => builder
|
|
79
|
+
.From<AuthorRegistered>(from => from.Set(m => m.Name).To(e => e.Name))
|
|
80
|
+
.From<BookAdded>(from => from.Add(m => m.BookCount).With(_ => 1));
|
|
81
|
+
}
|
|
82
|
+
```
|
|
83
|
+
Or model-bound shorthand for simple cases:
|
|
84
|
+
```csharp
|
|
85
|
+
[ReadModel]
|
|
86
|
+
public record AuthorInfo([Key] Guid Id, [FromEvent<AuthorRegistered>] string Name);
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
Projections/reducers are discovered automatically — no registration. See
|
|
90
|
+
[references/patterns.md](references/patterns.md) for joins, children, composite keys, and reducers.
|
|
91
|
+
|
|
92
|
+
## Step 3 — Build
|
|
93
|
+
|
|
94
|
+
`dotnet build` from the project root — zero warnings / errors. This generates the query proxy the
|
|
95
|
+
frontend imports.
|
|
96
|
+
|
|
97
|
+
## Step 4 — Specs
|
|
98
|
+
|
|
99
|
+
Write specs proving the projection/reducer produces the expected read model from a sequence of events —
|
|
100
|
+
one spec per scenario in `slice.json`. Run `dotnet test --filter "FullyQualifiedName~<SliceName>"`.
|
|
101
|
+
See [references/patterns.md](references/patterns.md).
|
|
102
|
+
|
|
103
|
+
## Step 5 — Frontend
|
|
104
|
+
|
|
105
|
+
Add `<Module>/<Feature>/<Slice>/<Component>.tsx` importing the co-located generated query proxy from
|
|
106
|
+
`./` (`MyQuery.use()` snapshot, `MyQuery.useWithPaging(pageSize)` for paging, observable proxy for live
|
|
107
|
+
data; or `DataTableForObservableQuery` for a table). Add a barrel `index.ts`. Register it in the
|
|
108
|
+
feature's composition page; add routing in `App.tsx` if it's a new page.
|
|
109
|
+
|
|
110
|
+
## Final verification — does the implementation match `slice.json`?
|
|
111
|
+
|
|
112
|
+
- [ ] Every read-model field → a record property fed by a projection/reducer mapping.
|
|
113
|
+
- [ ] Every source event used exists; projections map from **events**, never read models.
|
|
114
|
+
- [ ] Every query in the slice is exposed as a static method; observable where the slice wants live data.
|
|
115
|
+
- [ ] Every scenario → an executable spec.
|
|
116
|
+
- [ ] `dotnet build` clean; specs pass.
|
|
117
|
+
|
|
118
|
+
## References
|
|
119
|
+
- [references/patterns.md](references/patterns.md) — full projection/reducer/query/spec code + React.
|
|
120
|
+
- [../_shared/cratis-conventions.md](../_shared/cratis-conventions.md) — the Cratis conventions.
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
# Read Slice Patterns — Cratis Arc + Chronicle
|
|
2
|
+
|
|
3
|
+
Complete code for state-view (read) slices. All artifacts go in a single
|
|
4
|
+
`<Module>/<Feature>/<Slice>/<Slice>.cs` (the shipped starter uses `SomeModule/SomeFeature/...` — match
|
|
5
|
+
whatever the existing slices use). The example slices use no file header (match the project); namespace
|
|
6
|
+
mirrors the folders and drops any `.Features.`. Examples below use short illustrative names
|
|
7
|
+
(`MyApp.Authors.Listing`); mirror your project's actual module/feature layout instead.
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## 1. Read model + queries (snapshot + observable)
|
|
12
|
+
|
|
13
|
+
```csharp
|
|
14
|
+
// Features/Authors/Listing/Listing.cs
|
|
15
|
+
namespace MyApp.Authors.Listing;
|
|
16
|
+
|
|
17
|
+
using System.Reactive.Subjects;
|
|
18
|
+
using Cratis.Arc.Queries.ModelBound;
|
|
19
|
+
using MongoDB.Driver;
|
|
20
|
+
|
|
21
|
+
[ReadModel]
|
|
22
|
+
public record AuthorListItem(AuthorId Id, AuthorName Name, int BookCount)
|
|
23
|
+
{
|
|
24
|
+
public static async Task<IEnumerable<AuthorListItem>> AllAuthors(
|
|
25
|
+
IMongoCollection<AuthorListItem> collection)
|
|
26
|
+
=> await collection.Find(Builders<AuthorListItem>.Filter.Empty).ToListAsync();
|
|
27
|
+
|
|
28
|
+
public static async Task<AuthorListItem?> GetAuthor(
|
|
29
|
+
AuthorId id, IMongoCollection<AuthorListItem> collection)
|
|
30
|
+
=> await collection.Find(a => a.Id == id).FirstOrDefaultAsync();
|
|
31
|
+
|
|
32
|
+
public static ISubject<IEnumerable<AuthorListItem>> ObserveAllAuthors(
|
|
33
|
+
IMongoCollection<AuthorListItem> collection)
|
|
34
|
+
=> collection.Observe();
|
|
35
|
+
|
|
36
|
+
public static ISubject<AuthorListItem> ObserveAuthor(
|
|
37
|
+
AuthorId id, IMongoCollection<AuthorListItem> collection)
|
|
38
|
+
=> collection.Observe(a => a.Id == id);
|
|
39
|
+
}
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Rules: `[ReadModel]` required; methods `public static`; observable methods return `ISubject<T>`
|
|
43
|
+
directly (never `Task<ISubject<T>>`); `ConceptAs<T>` for identity fields; one read model per use case.
|
|
44
|
+
|
|
45
|
+
---
|
|
46
|
+
|
|
47
|
+
## 2A. Projection (declarative mapping)
|
|
48
|
+
|
|
49
|
+
```csharp
|
|
50
|
+
public class AuthorListItemProjection : IProjectionFor<AuthorListItem>
|
|
51
|
+
{
|
|
52
|
+
public void Define(IProjectionBuilderFor<AuthorListItem> builder) => builder
|
|
53
|
+
.From<AuthorRegistered>(from => from
|
|
54
|
+
.Set(m => m.Name).To(e => e.Name)
|
|
55
|
+
.Set(m => m.BookCount).WithValue(0))
|
|
56
|
+
.From<BookAdded>(from => from
|
|
57
|
+
.Add(m => m.BookCount).With(_ => 1))
|
|
58
|
+
.From<BookRemoved>(from => from
|
|
59
|
+
.Subtract(m => m.BookCount).With(_ => 1));
|
|
60
|
+
}
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
- Keyed by **event source id** by default (the id passed to `IEventLog.Append`).
|
|
64
|
+
- AutoMap is on — `.From<E>()` directly; matching property names map automatically.
|
|
65
|
+
- Builder verbs: `Set(...).To(...)` / `.WithValue(...)`, `Add(...).With(...)`, `Subtract(...).With(...)`,
|
|
66
|
+
`.Join<T>()`, `.Children(...)`. Projections **join events, never read models**.
|
|
67
|
+
|
|
68
|
+
### Model-bound shorthand (simple cases)
|
|
69
|
+
```csharp
|
|
70
|
+
[ReadModel]
|
|
71
|
+
public record AuthorInfo(
|
|
72
|
+
[Key] Guid Id,
|
|
73
|
+
[FromEvent<AuthorRegistered>] string Name,
|
|
74
|
+
[SetFrom<AuthorRegistered>(nameof(AuthorRegistered.Country))] string Country);
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
---
|
|
78
|
+
|
|
79
|
+
## 2B. Reducer (running aggregate)
|
|
80
|
+
|
|
81
|
+
```csharp
|
|
82
|
+
public class AccountBalanceReducer : IReducerFor<AccountBalance>
|
|
83
|
+
{
|
|
84
|
+
public AccountBalance Opened(DebitAccountOpened @event, AccountBalance? current, EventContext context)
|
|
85
|
+
=> new(0m, context.Occurred);
|
|
86
|
+
|
|
87
|
+
public AccountBalance Deposited(FundsDeposited @event, AccountBalance? current, EventContext context)
|
|
88
|
+
=> (current ?? new(0m, context.Occurred)) with { Balance = (current?.Balance ?? 0m) + @event.Amount };
|
|
89
|
+
|
|
90
|
+
public AccountBalance Withdrawn(FundsWithdrawn @event, AccountBalance? current, EventContext context)
|
|
91
|
+
=> (current ?? new(0m, context.Occurred)) with { Balance = (current?.Balance ?? 0m) - @event.Amount };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
public record AccountBalance(decimal Balance, DateTimeOffset LastUpdated);
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
Return the complete new state; never mutate `current` (`null` on the first event). Add
|
|
98
|
+
`[FilterEventsByTag]` / `[EventSourceType]` / `[EventStreamType]` when filtering by metadata.
|
|
99
|
+
|
|
100
|
+
---
|
|
101
|
+
|
|
102
|
+
## 3. Spec — projection result from a sequence of events
|
|
103
|
+
|
|
104
|
+
```csharp
|
|
105
|
+
namespace MyApp.Authors.Listing.for_AuthorListItem.when_an_author_registered_and_added_two_books;
|
|
106
|
+
|
|
107
|
+
// Use the project's projection-spec harness (e.g. a ProjectionSpecificationContext / Given<context>).
|
|
108
|
+
// The shape: append events, observe the resulting read model, assert fields.
|
|
109
|
+
|
|
110
|
+
public class and_projecting : Specification
|
|
111
|
+
{
|
|
112
|
+
AuthorListItem _result;
|
|
113
|
+
|
|
114
|
+
void Establish() { /* append AuthorRegistered, BookAdded, BookAdded to the test event store */ }
|
|
115
|
+
void Because() { /* run the projection / read the resulting model */ }
|
|
116
|
+
|
|
117
|
+
[Fact] void should_have_the_name() => _result.Name.Value.ShouldEqual("John");
|
|
118
|
+
[Fact] void should_count_two_books() => _result.BookCount.ShouldEqual(2);
|
|
119
|
+
}
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
Match the project's existing projection-spec harness — discover it from an existing read slice's specs.
|
|
123
|
+
|
|
124
|
+
---
|
|
125
|
+
|
|
126
|
+
## 4. React — render the query proxy
|
|
127
|
+
|
|
128
|
+
The query proxy (`AllAuthors.ts`) is generated **next to** `Listing.cs` — import it from the same
|
|
129
|
+
folder. The simplest list UI uses the Cratis `DataTableForObservableQuery` component (see the shipped
|
|
130
|
+
example slice's `ListingDataTable.tsx`):
|
|
131
|
+
|
|
132
|
+
```tsx
|
|
133
|
+
// Authors/Listing/ListingDataTable.tsx
|
|
134
|
+
import { DataTableForObservableQuery } from '@cratis/components/DataTables';
|
|
135
|
+
import { Column } from 'primereact/column';
|
|
136
|
+
import { AllAuthors } from './AllAuthors'; // co-located generated proxy
|
|
137
|
+
|
|
138
|
+
export const ListingDataTable = () => (
|
|
139
|
+
<DataTableForObservableQuery query={AllAuthors} dataKey='id' emptyMessage='No authors yet.'>
|
|
140
|
+
<Column field='name' header='Name' />
|
|
141
|
+
<Column field='bookCount' header='Books' />
|
|
142
|
+
</DataTableForObservableQuery>
|
|
143
|
+
);
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
Hook form (manual rendering):
|
|
147
|
+
|
|
148
|
+
```tsx
|
|
149
|
+
const [authors] = AllAuthors.use(); // snapshot or observable, depending on the proxy
|
|
150
|
+
const [result, , setPage] = AllAuthors.useWithPaging(10); // paging
|
|
151
|
+
// result.data, result.paging.totalItems, result.paging.page
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
Observable queries generate an `ObservableQueryFor` proxy (from `collection.Observe()`); snapshot
|
|
155
|
+
queries generate `QueryFor`. PrimeReact CSS variables / Tailwind for styling; no `any`; full
|
|
156
|
+
descriptive names. Add a barrel `index.ts` for the slice.
|
|
157
|
+
|
|
158
|
+
---
|
|
159
|
+
|
|
160
|
+
## Checklist
|
|
161
|
+
|
|
162
|
+
- One `.cs` file, namespace without `.Features.` (no file header unless the project uses one).
|
|
163
|
+
- `[ReadModel]` record; `public static` query methods; observable returns `ISubject<T>` directly.
|
|
164
|
+
- Projection (mapping) or reducer (aggregate) — discovered automatically, no registration.
|
|
165
|
+
- Projections map from events only; one read model per use case.
|
|
166
|
+
- `ConceptAs<T>` for identity fields; one spec per scenario; `dotnet build` clean; specs pass.
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: connect
|
|
3
|
+
description: Resolve eventmodelers connection config (token, boardId, baseUrl) from inline params or .eventmodelers/config.json — ask the user for missing values, persist them, and add the file to .gitignore. All other skills invoke this first.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Connect — Resolve Eventmodelers Config
|
|
7
|
+
|
|
8
|
+
**Every other skill invokes this skill first** before making any API calls. Do not proceed past this skill until all four values (`TOKEN`, `BOARD_ID`, `ORG_ID`, `BASE_URL`) are resolved.
|
|
9
|
+
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
## What this skill produces
|
|
13
|
+
|
|
14
|
+
After running, the following variables are available for the rest of the session:
|
|
15
|
+
|
|
16
|
+
| Variable | Header sent to API | Description |
|
|
17
|
+
|----------|--------------------|-------------|
|
|
18
|
+
| `TOKEN` | `x-token` | API token UUID |
|
|
19
|
+
| `BOARD_ID` | `x-board-id` | Target board UUID |
|
|
20
|
+
| `ORG_ID` | — | Organization UUID (used in all board-scoped URLs) |
|
|
21
|
+
| `BASE_URL` | — | Base URL, e.g. `http://localhost:3000` |
|
|
22
|
+
|
|
23
|
+
Every API call in every skill must include these headers:
|
|
24
|
+
```
|
|
25
|
+
x-token: <TOKEN>
|
|
26
|
+
x-board-id: <BOARD_ID>
|
|
27
|
+
x-user-id: <skill-name> ← set by each skill individually
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
All board-scoped URLs follow the pattern: `<BASE_URL>/api/org/<ORG_ID>/boards/<BOARD_ID>/...`
|
|
31
|
+
|
|
32
|
+
---
|
|
33
|
+
|
|
34
|
+
## Step 0 — Check for inline parameters
|
|
35
|
+
|
|
36
|
+
Before reading the config file, scan the prompt/arguments that invoked this skill for inline overrides. Supported formats:
|
|
37
|
+
|
|
38
|
+
| Pattern | Example |
|
|
39
|
+
|---------|---------|
|
|
40
|
+
| `board=<uuid>` | `board=05cda19d-d5b8-4b51-ae88-c72f2611548a` |
|
|
41
|
+
| `token=<uuid>` | `token=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx` |
|
|
42
|
+
| `org=<uuid>` | `org=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx` |
|
|
43
|
+
| `baseUrl=<url>` | `baseUrl=http://localhost:3000` |
|
|
44
|
+
|
|
45
|
+
If an inline `board=<uuid>` is found, use it as `BOARD_ID` — **it takes priority over the config file**. Same for `token`, `org`, and `baseUrl`. Record which values came from inline params so they are not overwritten in Step 3.
|
|
46
|
+
|
|
47
|
+
---
|
|
48
|
+
|
|
49
|
+
## Step 1 — Read config file
|
|
50
|
+
|
|
51
|
+
Check whether `.eventmodelers/config.json` exists in the current working directory:
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
cat .eventmodelers/config.json 2>/dev/null
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
If the file exists and is valid JSON, extract any values **not already set by Step 0**:
|
|
58
|
+
- `token` → `TOKEN`
|
|
59
|
+
- `boardId` → `BOARD_ID`
|
|
60
|
+
- `organizationId` → `ORG_ID`
|
|
61
|
+
- `baseUrl` → `BASE_URL` (default: `https://api.eventmodelers.ai` if missing)
|
|
62
|
+
|
|
63
|
+
Resolution priority: **inline param > config file > ask user**
|
|
64
|
+
|
|
65
|
+
If all four are present (from any source), skip to **Step 4 — Verify**.
|
|
66
|
+
|
|
67
|
+
---
|
|
68
|
+
|
|
69
|
+
## Step 2 — Ask for missing values
|
|
70
|
+
|
|
71
|
+
If after Steps 0 and 1 any required field is still missing, **ask the user one question first**:
|
|
72
|
+
|
|
73
|
+
> "Do you have a config from the eventmodelers accounts page? (yes / no)"
|
|
74
|
+
|
|
75
|
+
**If the user answers yes:**
|
|
76
|
+
Stop asking questions. Show this hint and wait for them to paste:
|
|
77
|
+
|
|
78
|
+
> "Great — please paste your config from https://app.eventmodelers.ai/account here."
|
|
79
|
+
|
|
80
|
+
When they paste a JSON object, parse it immediately — accept both `orgId` and `organizationId` as the organization field — apply all values, and proceed directly to Step 3.
|
|
81
|
+
|
|
82
|
+
**If the user answers no** (or pastes only a partial config), ask for each still-missing field one at a time, in this order: `token`, then `boardId`, then `orgId`. Wait for the answer before asking the next.
|
|
83
|
+
|
|
84
|
+
| Field | What to ask |
|
|
85
|
+
|-------|-------------|
|
|
86
|
+
| `token` | "Please provide your eventmodelers API token (a UUID from your workspace settings)." |
|
|
87
|
+
| `boardId` | "Please provide the board ID you want to work with (the UUID from the board URL)." |
|
|
88
|
+
| `orgId` | "Please provide your organization ID (the UUID from your organization settings)." |
|
|
89
|
+
| `baseUrl` | Do **not** ask — default to `https://api.eventmodelers.ai` silently. |
|
|
90
|
+
|
|
91
|
+
Where to find the token: users generate API tokens in their workspace settings at the eventmodelers platform. The token is shown only once at creation time. It is a UUID and must belong to the same organization as the board.
|
|
92
|
+
|
|
93
|
+
---
|
|
94
|
+
|
|
95
|
+
## Step 3 — Persist config
|
|
96
|
+
|
|
97
|
+
Once all values are collected, write the config file. When writing, merge with any existing config — do **not** overwrite fields that were provided as inline params with values from a previous config (the inline param is the user's explicit intent for this session, but the persisted value should reflect the most recently user-supplied value):
|
|
98
|
+
|
|
99
|
+
```bash
|
|
100
|
+
mkdir -p .eventmodelers
|
|
101
|
+
cat > .eventmodelers/config.json << 'EOF'
|
|
102
|
+
{
|
|
103
|
+
"token": "<TOKEN>",
|
|
104
|
+
"boardId": "<BOARD_ID>",
|
|
105
|
+
"orgId": "<ORG_ID>",
|
|
106
|
+
"baseUrl": "<BASE_URL>"
|
|
107
|
+
}
|
|
108
|
+
EOF
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
Then ensure `.eventmodelers/config.json` is in `.gitignore`. Check whether it is already present:
|
|
112
|
+
|
|
113
|
+
```bash
|
|
114
|
+
grep -q ".eventmodelers/config.json" .gitignore 2>/dev/null || echo "MISSING"
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
If `MISSING`, append it:
|
|
118
|
+
|
|
119
|
+
```bash
|
|
120
|
+
echo ".eventmodelers/config.json" >> .gitignore
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
Tell the user: `"Config saved to .eventmodelers/config.json and added to .gitignore."`
|
|
124
|
+
|
|
125
|
+
---
|
|
126
|
+
|
|
127
|
+
## Step 4 — Verify
|
|
128
|
+
|
|
129
|
+
Confirm the token and board are valid with a lightweight call:
|
|
130
|
+
|
|
131
|
+
```bash
|
|
132
|
+
curl -s -o /dev/null -w "%{http_code}" \
|
|
133
|
+
-H "x-token: <TOKEN>" \
|
|
134
|
+
-H "x-board-id: <BOARD_ID>" \
|
|
135
|
+
-H "x-user-id: connect-skill" \
|
|
136
|
+
"<BASE_URL>/api/org/<ORG_ID>/boards/<BOARD_ID>/nodes?type=CHAPTER"
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
| Response | Action |
|
|
140
|
+
|----------|--------|
|
|
141
|
+
| `200` | Config is valid. Print one line: `"Connected — board <BOARD_ID>"` and return. |
|
|
142
|
+
| `401` | Token is invalid or missing. Tell the user and re-run from Step 2, clearing `token`. |
|
|
143
|
+
| `403` | Token organization does not match board. Tell the user to check that the token was issued for the correct workspace. Re-run from Step 2 for both fields. |
|
|
144
|
+
| `404` | Board not found. Tell the user and re-run from Step 2, clearing `boardId`. |
|
|
145
|
+
| Any other | Print the status code and raw response. Ask the user how to proceed. |
|
|
146
|
+
|
|
147
|
+
---
|
|
148
|
+
|
|
149
|
+
## Config file format
|
|
150
|
+
|
|
151
|
+
`.eventmodelers/config.json`:
|
|
152
|
+
```json
|
|
153
|
+
{
|
|
154
|
+
"token": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
|
|
155
|
+
"boardId": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
|
|
156
|
+
"orgId": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
|
|
157
|
+
"baseUrl": "http://localhost:3000"
|
|
158
|
+
}
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
The `token` field is a secret. It is never logged or shown after initial confirmation.
|
|
162
|
+
|
|
163
|
+
---
|
|
164
|
+
|
|
165
|
+
## Security notes
|
|
166
|
+
|
|
167
|
+
- The config file is workspace-local and gitignored — never commit it.
|
|
168
|
+
- The token grants write access to all boards in its organization — treat it like a password.
|
|
169
|
+
- If a skill receives a `401` or `403` mid-session, re-invoke this skill to refresh the config before retrying.
|