@nebulit/embuilder 0.1.39
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 +254 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +138 -0
- package/package.json +49 -0
- package/templates/.claude/hooks/QUICKSTART.md +256 -0
- package/templates/.claude/hooks/README.md +533 -0
- package/templates/.claude/hooks/analyze-commit.sh +22 -0
- package/templates/.claude/hooks/analyze-commit.ts +518 -0
- package/templates/.claude/hooks/analyzers/README.md +198 -0
- package/templates/.claude/hooks/analyzers/code-quality-checker.ts +154 -0
- package/templates/.claude/hooks/analyzers/code-quality.md +54 -0
- package/templates/.claude/hooks/analyzers/commit-blocker-example.ts.disabled +110 -0
- package/templates/.claude/hooks/analyzers/commit-policy.md +49 -0
- package/templates/.claude/hooks/analyzers/event-model-validator.md +49 -0
- package/templates/.claude/hooks/analyzers/event-model-validator.ts +169 -0
- package/templates/.claude/hooks/analyzers/example-logger.ts +70 -0
- package/templates/.claude/hooks/analyzers/slice-scope-validator.md +81 -0
- package/templates/.claude/hooks/check-review-result.sh +47 -0
- package/templates/.claude/hooks/prepare-review.sh +34 -0
- package/templates/.claude/hooks/review-agent-prompt.md +42 -0
- package/templates/.claude/hooks/run-review-agent.sh +124 -0
- package/templates/.claude/settings.local.json +37 -0
- package/templates/.claude/skills/help/README.md +84 -0
- package/templates/.claude/skills/help/SKILL.md +393 -0
- package/templates/.claude/skills/help/templates/demo-config.json +6753 -0
- package/templates/.claude/skills/sample-slices/SKILL.md +8 -0
- package/templates/.claude/skills/sample-slices/templates/.slices/Library/addbook/code-slice.json +124 -0
- package/templates/.claude/skills/sample-slices/templates/.slices/Library/addbook/slice.json +255 -0
- package/templates/.claude/skills/sample-slices/templates/.slices/Library/availablebooks/slice.json +107 -0
- package/templates/.claude/skills/sample-slices/templates/.slices/index.json +20 -0
- package/templates/.claude/skills/sample-slices/templates/Cart/additem/slice.json +979 -0
- package/templates/.claude/skills/sample-slices/templates/Cart/archiveitem/slice.json +529 -0
- package/templates/.claude/skills/sample-slices/templates/Cart/cartitems/slice.json +1072 -0
- package/templates/.claude/skills/sample-slices/templates/Cart/cartwithproducts/slice.json +394 -0
- package/templates/.claude/skills/sample-slices/templates/Cart/changedprices/slice.json +88 -0
- package/templates/.claude/skills/sample-slices/templates/Cart/changeinventory/slice.json +264 -0
- package/templates/.claude/skills/sample-slices/templates/Cart/changeprice/slice.json +308 -0
- package/templates/.claude/skills/sample-slices/templates/Cart/clearcart/slice.json +358 -0
- package/templates/.claude/skills/sample-slices/templates/Cart/inventories/slice.json +203 -0
- package/templates/.claude/skills/sample-slices/templates/Cart/publishcart/slice.json +876 -0
- package/templates/.claude/skills/sample-slices/templates/Cart/removeitem/slice.json +560 -0
- package/templates/.claude/skills/sample-slices/templates/Cart/submitcart/slice.json +708 -0
- package/templates/.claude/skills/sample-slices/templates/Cart/submittedcartdata/slice.json +399 -0
- package/templates/.claude/skills/sample-slices/templates/index.json +108 -0
- package/templates/.claude/skills/slice-automation/SKILL.md +49 -0
- package/templates/.claude/skills/slice-state-change/SKILL.md +369 -0
- package/templates/.claude/skills/slice-state-change/templates/AddLocation/AddLocation.test.ts.sample +76 -0
- package/templates/.claude/skills/slice-state-change/templates/AddLocation/AddLocationCommand.ts.sample +84 -0
- package/templates/.claude/skills/slice-state-change/templates/AddLocation/routes.ts.sample +73 -0
- package/templates/.claude/skills/slice-state-change/templates/README.md +46 -0
- package/templates/.claude/skills/slice-state-view/SKILL.md +336 -0
- package/templates/.claude/skills/slice-state-view/templates/Locations/Locations.test.ts.sample +84 -0
- package/templates/.claude/skills/slice-state-view/templates/Locations/LocationsProjection.ts.sample +50 -0
- package/templates/.claude/skills/slice-state-view/templates/Locations/routes.ts.sample +46 -0
- package/templates/.claude/skills/slice-state-view/templates/README.md +109 -0
- package/templates/.claude/skills/slice-state-view/templates/Tables/Tables.test.ts.sample +104 -0
- package/templates/.claude/skills/slice-state-view/templates/Tables/TablesProjection.ts.sample +59 -0
- package/templates/.claude/skills/slice-state-view/templates/Tables/routes.ts.sample +46 -0
- package/templates/.claude/skills/slice-state-view/templates/V2__tables.sql +7 -0
- package/templates/.claude/skills/slice-state-view/templates/V8__locations.sql +7 -0
- package/templates/.claude/skills/test-analyzer/SKILL.md +373 -0
- package/templates/.claude/skills/test-analyzer/examples/specification-format.md +143 -0
- package/templates/.claude/skills/test-analyzer/examples/state-change-example.md +111 -0
- package/templates/.claude/skills/test-analyzer/examples/state-view-example.md +122 -0
- package/templates/AGENTS.md +110 -0
- package/templates/Claude.md +58 -0
- package/templates/README.md +178 -0
- package/templates/backend/.env +9 -0
- package/templates/backend/BACKEND_AUTH_SETUP.md +183 -0
- package/templates/backend/SWAGGER.md +213 -0
- package/templates/backend/eslint.config.mjs +31 -0
- package/templates/backend/flyway.conf +17 -0
- package/templates/backend/package.json +44 -0
- package/templates/backend/prd.json.example +64 -0
- package/templates/backend/public/assets/images/banner.png +0 -0
- package/templates/backend/public/assets/logo.png +0 -0
- package/templates/backend/public/file.svg +4 -0
- package/templates/backend/public/globe.svg +12 -0
- package/templates/backend/public/next.svg +6 -0
- package/templates/backend/public/vercel.svg +3 -0
- package/templates/backend/public/window.svg +5 -0
- package/templates/backend/server.ts +129 -0
- package/templates/backend/setup-env.sh +50 -0
- package/templates/backend/src/common/assertions.ts +6 -0
- package/templates/backend/src/common/db.ts +1 -0
- package/templates/backend/src/common/loadPostgresEventstore.ts +16 -0
- package/templates/backend/src/common/parseEndpoint.ts +51 -0
- package/templates/backend/src/common/replay.ts +9 -0
- package/templates/backend/src/common/routes.ts +19 -0
- package/templates/backend/src/common/testHelpers.ts +53 -0
- package/templates/backend/src/core/readmodel.ts +28 -0
- package/templates/backend/src/core/types.ts +26 -0
- package/templates/backend/src/process/process.ts +53 -0
- package/templates/backend/src/supabase/LoginHandler.ts +36 -0
- package/templates/backend/src/supabase/ProtectedPageProps.ts +21 -0
- package/templates/backend/src/supabase/README.md +171 -0
- package/templates/backend/src/supabase/api.ts +63 -0
- package/templates/backend/src/supabase/authMiddleware.ts +53 -0
- package/templates/backend/src/supabase/component.ts +12 -0
- package/templates/backend/src/supabase/requireUser.ts +72 -0
- package/templates/backend/src/supabase/serverProps.ts +25 -0
- package/templates/backend/src/supabase/staticProps.ts +10 -0
- package/templates/backend/src/swagger.ts +34 -0
- package/templates/backend/src/util/assertions.ts +6 -0
- package/templates/backend/supabase/config.toml +295 -0
- package/templates/backend/supabase/migrations/20260121155918593_catalogentries.sql.sample +23 -0
- package/templates/backend/supabase/seed.sql +1 -0
- package/templates/backend/tsconfig.json +31 -0
- package/templates/frontend/.env.development +3 -0
- package/templates/frontend/AGENTS.md +7 -0
- package/templates/frontend/README.md +73 -0
- package/templates/frontend/components.json +20 -0
- package/templates/frontend/eslint.config.js +26 -0
- package/templates/frontend/index.html +18 -0
- package/templates/frontend/package-lock.json +8347 -0
- package/templates/frontend/package.json +94 -0
- package/templates/frontend/postcss.config.js +6 -0
- package/templates/frontend/public/favicon.ico +0 -0
- package/templates/frontend/public/logo.png +0 -0
- package/templates/frontend/public/placeholder.svg +1 -0
- package/templates/frontend/public/robots.txt +14 -0
- package/templates/frontend/src/App.css +42 -0
- package/templates/frontend/src/App.tsx +47 -0
- package/templates/frontend/src/components/NavLink.tsx +28 -0
- package/templates/frontend/src/components/ProtectedRoute.tsx +24 -0
- package/templates/frontend/src/components/calendar/Calendar.tsx +302 -0
- package/templates/frontend/src/components/layout/DashboardLayout.tsx +21 -0
- package/templates/frontend/src/components/layout/Header.tsx +45 -0
- package/templates/frontend/src/components/layout/Sidebar.tsx +82 -0
- package/templates/frontend/src/components/tables/ReservationTemplates.tsx +189 -0
- package/templates/frontend/src/components/ui/accordion.tsx +52 -0
- package/templates/frontend/src/components/ui/alert-dialog.tsx +104 -0
- package/templates/frontend/src/components/ui/alert.tsx +43 -0
- package/templates/frontend/src/components/ui/aspect-ratio.tsx +5 -0
- package/templates/frontend/src/components/ui/avatar.tsx +38 -0
- package/templates/frontend/src/components/ui/badge.tsx +29 -0
- package/templates/frontend/src/components/ui/breadcrumb.tsx +90 -0
- package/templates/frontend/src/components/ui/button.tsx +47 -0
- package/templates/frontend/src/components/ui/calendar.tsx +54 -0
- package/templates/frontend/src/components/ui/card.tsx +43 -0
- package/templates/frontend/src/components/ui/carousel.tsx +224 -0
- package/templates/frontend/src/components/ui/chart.tsx +303 -0
- package/templates/frontend/src/components/ui/checkbox.tsx +26 -0
- package/templates/frontend/src/components/ui/collapsible.tsx +9 -0
- package/templates/frontend/src/components/ui/command.tsx +132 -0
- package/templates/frontend/src/components/ui/context-menu.tsx +178 -0
- package/templates/frontend/src/components/ui/dialog.tsx +95 -0
- package/templates/frontend/src/components/ui/drawer.tsx +87 -0
- package/templates/frontend/src/components/ui/dropdown-menu.tsx +179 -0
- package/templates/frontend/src/components/ui/form.tsx +129 -0
- package/templates/frontend/src/components/ui/hover-card.tsx +27 -0
- package/templates/frontend/src/components/ui/input-otp.tsx +61 -0
- package/templates/frontend/src/components/ui/input.tsx +22 -0
- package/templates/frontend/src/components/ui/label.tsx +17 -0
- package/templates/frontend/src/components/ui/menubar.tsx +207 -0
- package/templates/frontend/src/components/ui/navigation-menu.tsx +120 -0
- package/templates/frontend/src/components/ui/pagination.tsx +81 -0
- package/templates/frontend/src/components/ui/popover.tsx +29 -0
- package/templates/frontend/src/components/ui/progress.tsx +23 -0
- package/templates/frontend/src/components/ui/radio-group.tsx +36 -0
- package/templates/frontend/src/components/ui/resizable.tsx +37 -0
- package/templates/frontend/src/components/ui/scroll-area.tsx +38 -0
- package/templates/frontend/src/components/ui/select.tsx +143 -0
- package/templates/frontend/src/components/ui/separator.tsx +20 -0
- package/templates/frontend/src/components/ui/sheet.tsx +107 -0
- package/templates/frontend/src/components/ui/sidebar.tsx +637 -0
- package/templates/frontend/src/components/ui/skeleton.tsx +7 -0
- package/templates/frontend/src/components/ui/slider.tsx +23 -0
- package/templates/frontend/src/components/ui/sonner.tsx +27 -0
- package/templates/frontend/src/components/ui/stat-card.tsx +44 -0
- package/templates/frontend/src/components/ui/switch.tsx +27 -0
- package/templates/frontend/src/components/ui/table.tsx +72 -0
- package/templates/frontend/src/components/ui/tabs.tsx +53 -0
- package/templates/frontend/src/components/ui/textarea.tsx +21 -0
- package/templates/frontend/src/components/ui/toast.tsx +111 -0
- package/templates/frontend/src/components/ui/toaster.tsx +24 -0
- package/templates/frontend/src/components/ui/toggle-group.tsx +49 -0
- package/templates/frontend/src/components/ui/toggle.tsx +37 -0
- package/templates/frontend/src/components/ui/tooltip.tsx +28 -0
- package/templates/frontend/src/components/ui/use-toast.ts +3 -0
- package/templates/frontend/src/contexts/AuthContext.tsx +94 -0
- package/templates/frontend/src/contexts/RefreshContext.tsx +236 -0
- package/templates/frontend/src/hooks/api/index.ts +2 -0
- package/templates/frontend/src/hooks/api/useLocations.ts +15 -0
- package/templates/frontend/src/hooks/use-mobile.tsx +19 -0
- package/templates/frontend/src/hooks/use-toast.ts +186 -0
- package/templates/frontend/src/hooks/useApiContext.ts +11 -0
- package/templates/frontend/src/index.css +118 -0
- package/templates/frontend/src/integrations/supabase/client.ts +9 -0
- package/templates/frontend/src/lib/api-client.ts +136 -0
- package/templates/frontend/src/lib/api.ts +1028 -0
- package/templates/frontend/src/lib/utils.ts +6 -0
- package/templates/frontend/src/main.tsx +5 -0
- package/templates/frontend/src/pages/Auth.tsx +408 -0
- package/templates/frontend/src/pages/Dashboard.tsx +168 -0
- package/templates/frontend/src/pages/Menus.tsx +224 -0
- package/templates/frontend/src/pages/NotFound.tsx +24 -0
- package/templates/frontend/src/pages/Register.tsx +285 -0
- package/templates/frontend/src/test/example.test.ts +0 -0
- package/templates/frontend/src/test/setup.ts +15 -0
- package/templates/frontend/src/types/index.ts +8 -0
- package/templates/frontend/src/vite-env.d.ts +1 -0
- package/templates/frontend/tailwind.config.ts +101 -0
- package/templates/frontend/tsconfig.app.json +31 -0
- package/templates/frontend/tsconfig.json +16 -0
- package/templates/frontend/tsconfig.node.json +22 -0
- package/templates/frontend/vite.config.ts +21 -0
- package/templates/frontend/vitest.config.ts +16 -0
- package/templates/init.sh +1 -0
- package/templates/prompt.md +139 -0
- package/templates/ralph.sh +120 -0
- package/templates/server.mjs +505 -0
|
@@ -0,0 +1,369 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: slice-state-change
|
|
3
|
+
description: builds a state-change slice from an event model
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
## Overview
|
|
7
|
+
|
|
8
|
+
State Change Slices are slices that change the system by processing a command. Each slice implements the Command-Event pattern using event sourcing.
|
|
9
|
+
|
|
10
|
+
## Critical Requirements
|
|
11
|
+
|
|
12
|
+
### Restaurant ID Requirement
|
|
13
|
+
- **CRITICAL**: ALL events MUST have `restaurantId` in their metadata (camelCase)
|
|
14
|
+
- **NEVER** use `locationId` or `location_id` - these are outdated and forbidden
|
|
15
|
+
- **CRITICAL**: ALL database tables MUST have a `restaurant_id` column (snake_case)
|
|
16
|
+
- This ensures proper multi-tenancy and data isolation
|
|
17
|
+
|
|
18
|
+
## Implementation Steps
|
|
19
|
+
|
|
20
|
+
When creating a state-change slice, you MUST create the following files in `src/slices/{SliceName}/`:
|
|
21
|
+
|
|
22
|
+
1. **{SliceName}Command.ts** - Command handler implementation
|
|
23
|
+
2. **{SliceName}.test.ts** - Test specifications
|
|
24
|
+
3. **routes.ts** - API endpoint (unless explicitly told not to)
|
|
25
|
+
4. **src/slices/{SliceName}/ui-prompt.md - prompt to build the UI
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
## Command Handler Structure
|
|
29
|
+
|
|
30
|
+
Every Command handler file follows this pattern:
|
|
31
|
+
|
|
32
|
+
### 1. Imports
|
|
33
|
+
```typescript
|
|
34
|
+
import type {Command} from '@event-driven-io/emmett'
|
|
35
|
+
import {CommandHandler} from '@event-driven-io/emmett';
|
|
36
|
+
import {ContextEvents} from "../../events/ContextEvents";
|
|
37
|
+
import {findEventstore} from "../../common/loadPostgresEventstore";
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
### 2. Command Type Definition
|
|
41
|
+
Define the command with:
|
|
42
|
+
- Command name (matches slice name)
|
|
43
|
+
- Data fields from specification
|
|
44
|
+
- Standard metadata fields
|
|
45
|
+
|
|
46
|
+
```typescript
|
|
47
|
+
export type {CommandName}Command = Command<'{CommandName}', {
|
|
48
|
+
// data fields from spec
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
correlation_id?: string,
|
|
52
|
+
causation_id?: string,
|
|
53
|
+
now?: Date,
|
|
54
|
+
streamName?: string,
|
|
55
|
+
}>;
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
### 3. State Type
|
|
59
|
+
Define state needed for business logic validation:
|
|
60
|
+
```typescript
|
|
61
|
+
export type {CommandName}State = {
|
|
62
|
+
// fields needed for validation
|
|
63
|
+
// often empty {} if no validation needed
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export const {CommandName}InitialState = (): {CommandName}State => ({
|
|
67
|
+
// initial state
|
|
68
|
+
});
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
### 4. Evolve Function
|
|
72
|
+
Updates state based on events (event sourcing projection):
|
|
73
|
+
```typescript
|
|
74
|
+
export const evolve = (
|
|
75
|
+
state: {CommandName}State,
|
|
76
|
+
event: ContextEvents,
|
|
77
|
+
): {CommandName}State => {
|
|
78
|
+
const {type, data} = event;
|
|
79
|
+
|
|
80
|
+
switch (type) {
|
|
81
|
+
case "{EventName}":
|
|
82
|
+
// update state based on event
|
|
83
|
+
return { ...state, /* updates */ };
|
|
84
|
+
default:
|
|
85
|
+
return state;
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
### 5. Decide Function
|
|
91
|
+
Business logic that validates command and returns events:
|
|
92
|
+
```typescript
|
|
93
|
+
export const decide = (
|
|
94
|
+
command: {CommandName}Command,
|
|
95
|
+
state: {CommandName}State,
|
|
96
|
+
): ContextEvents[] => {
|
|
97
|
+
// validation logic
|
|
98
|
+
// throw errors if validation fails
|
|
99
|
+
|
|
100
|
+
return [{
|
|
101
|
+
type: '{EventName}',
|
|
102
|
+
data: {
|
|
103
|
+
// event data fields
|
|
104
|
+
},
|
|
105
|
+
metadata: {
|
|
106
|
+
correlation_id: command.metadata?.correlation_id,
|
|
107
|
+
causation_id: command.metadata?.causation_id,
|
|
108
|
+
// these 2 are mandatory
|
|
109
|
+
restaurantId: command.metadata?.restaurantId,
|
|
110
|
+
userId: command.metadata?.userId
|
|
111
|
+
}
|
|
112
|
+
}];
|
|
113
|
+
};
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
### 6. Command Handler & Export
|
|
117
|
+
```typescript
|
|
118
|
+
const {CommandName}CommandHandler = CommandHandler<{CommandName}State, ContextEvents>({
|
|
119
|
+
evolve,
|
|
120
|
+
initialState: {CommandName}InitialState
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
export const handle{CommandName} = async (id: string, command: {CommandName}Command) => {
|
|
124
|
+
const eventStore = await findEventstore()
|
|
125
|
+
const result = await {CommandName}CommandHandler(eventStore, id, (state: {CommandName}State) => decide(command, state))
|
|
126
|
+
return {
|
|
127
|
+
nextExpectedStreamVersion: result.nextExpectedStreamVersion,
|
|
128
|
+
lastEventGlobalPosition: result.lastEventGlobalPosition
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
## Key Patterns
|
|
134
|
+
|
|
135
|
+
### Metadata Handling
|
|
136
|
+
- **Optional chaining**: Use `command.metadata?.correlation_id` (some examples use this)
|
|
137
|
+
- **Direct access**: Use `command.metadata.correlation_id` (other examples use this)
|
|
138
|
+
- **CRITICAL**: ALWAYS use `restaurantId` (camelCase) - NEVER use `locationId` or `location_id` (outdated)
|
|
139
|
+
- restaurantId: command.metadata?.restaurantId,
|
|
140
|
+
- userId: command.metadata?.userId
|
|
141
|
+
- Be consistent within your implementation
|
|
142
|
+
|
|
143
|
+
### Destructuring
|
|
144
|
+
Two patterns observed:
|
|
145
|
+
1. Direct access: `command.data.fieldName`
|
|
146
|
+
2. Destructure: `const {field1, field2} = command.data;`
|
|
147
|
+
|
|
148
|
+
Both are acceptable - choose based on readability.
|
|
149
|
+
|
|
150
|
+
### State Validation
|
|
151
|
+
- Only include fields in state that are needed for validation
|
|
152
|
+
- Many simple commands have empty state `{}`
|
|
153
|
+
- Complex validations (like AddLocation) track sets or maps in state
|
|
154
|
+
|
|
155
|
+
## Testing
|
|
156
|
+
|
|
157
|
+
Every command MUST have tests using DeciderSpecification:
|
|
158
|
+
|
|
159
|
+
```typescript
|
|
160
|
+
import {DeciderSpecification} from '@event-driven-io/emmett';
|
|
161
|
+
import {{CommandName}Command, {CommandName}State, decide, evolve} from "./{CommandName}Command";
|
|
162
|
+
import {describe, it} from "node:test";
|
|
163
|
+
|
|
164
|
+
describe('{CommandName} Specification', () => {
|
|
165
|
+
const given = DeciderSpecification.for({
|
|
166
|
+
decide,
|
|
167
|
+
evolve,
|
|
168
|
+
initialState: () => ({})
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
it('spec: {test description}', () => {
|
|
172
|
+
const command: {CommandName}Command = {
|
|
173
|
+
type: '{CommandName}',
|
|
174
|
+
data: {
|
|
175
|
+
// test data
|
|
176
|
+
},
|
|
177
|
+
metadata: {now: new Date()},
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
given([/* precondition events */])
|
|
181
|
+
.when(command)
|
|
182
|
+
.then([{
|
|
183
|
+
type: '{EventName}',
|
|
184
|
+
data: {
|
|
185
|
+
// expected event data
|
|
186
|
+
},
|
|
187
|
+
metadata: {}
|
|
188
|
+
}])
|
|
189
|
+
});
|
|
190
|
+
});
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
## Routes (API Endpoint)
|
|
194
|
+
|
|
195
|
+
Unless explicitly told not to, create a routes.ts file:
|
|
196
|
+
|
|
197
|
+
```typescript
|
|
198
|
+
import {Router, Request, Response} from 'express';
|
|
199
|
+
import {{CommandName}Command, handle{CommandName}} from './{CommandName}Command';
|
|
200
|
+
import {requireUser} from "../../supabase/requireUser";
|
|
201
|
+
import {WebApiSetup} from "@event-driven-io/emmett-expressjs";
|
|
202
|
+
import {assertNotEmpty} from "../../common/assertions";
|
|
203
|
+
|
|
204
|
+
export type {CommandName}RequestPayload = {
|
|
205
|
+
// fields matching command data (all optional with ?)
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
export type {CommandName}Request = Request<
|
|
209
|
+
Partial<{ id: string }>,
|
|
210
|
+
unknown,
|
|
211
|
+
Partial<{CommandName}RequestPayload>
|
|
212
|
+
>;
|
|
213
|
+
|
|
214
|
+
export const api =
|
|
215
|
+
(
|
|
216
|
+
// external dependencies
|
|
217
|
+
): WebApiSetup =>
|
|
218
|
+
(router: Router): void => {
|
|
219
|
+
router.post('/api/{commandname}/:id', requireRestaurantAccess, async (req: {CommandName}Request, res: Response) => {
|
|
220
|
+
const principal = await requireUser(req, res, false);
|
|
221
|
+
if (principal.error) {
|
|
222
|
+
return res.status(401).json(principal);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const correlation_id = req.header("correlation_id") ?? req.params.id
|
|
226
|
+
const causation_id = req.params.id
|
|
227
|
+
const restaurantId = (req as any).restaurant_id
|
|
228
|
+
const userId = (req as any).user_id
|
|
229
|
+
|
|
230
|
+
try {
|
|
231
|
+
const command: {CommandName}Command = {
|
|
232
|
+
data: {
|
|
233
|
+
// map from req.body with assertNotEmpty
|
|
234
|
+
},
|
|
235
|
+
metadata: {
|
|
236
|
+
correlation_id: correlation_id,
|
|
237
|
+
causation_id: causation_id
|
|
238
|
+
},
|
|
239
|
+
type: "{CommandName}"
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
if (!req.params.id) throw "no id provided"
|
|
243
|
+
|
|
244
|
+
const result = await handle{CommandName}(assertNotEmpty(req.params.id), command);
|
|
245
|
+
|
|
246
|
+
res.set("correlation_id", correlation_id)
|
|
247
|
+
res.set("causation_id", causation_id)
|
|
248
|
+
|
|
249
|
+
return res.status(200).json({
|
|
250
|
+
ok: true,
|
|
251
|
+
next_expected_stream_version: result.nextExpectedStreamVersion?.toString(),
|
|
252
|
+
last_event_global_position: result.lastEventGlobalPosition?.toString()
|
|
253
|
+
});
|
|
254
|
+
} catch (err) {
|
|
255
|
+
console.error(err);
|
|
256
|
+
return res.status(500).json({ok: false, error: 'Server error'});
|
|
257
|
+
}
|
|
258
|
+
});
|
|
259
|
+
};
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
## Business Logic
|
|
263
|
+
|
|
264
|
+
In *Command.ts
|
|
265
|
+
Evolve-Function provides the state we can use to validate:
|
|
266
|
+
|
|
267
|
+
```
|
|
268
|
+
export const evolve = (
|
|
269
|
+
state: PlanVacationState,
|
|
270
|
+
event: ContextEvents,
|
|
271
|
+
): PlanVacationState => {
|
|
272
|
+
const {type, data} = event;
|
|
273
|
+
|
|
274
|
+
switch (type) {
|
|
275
|
+
// case "..Event":
|
|
276
|
+
case 'VacationPlanned':
|
|
277
|
+
state.plannedVacations.push({id: event.data.vacation_id, from: event.data.from, to: event.data.to})
|
|
278
|
+
return state;
|
|
279
|
+
case 'VacationCancelled':
|
|
280
|
+
state.plannedVacations = state.plannedVacations.filter(it => it.id !== event.data.vacation_id)
|
|
281
|
+
return state;
|
|
282
|
+
default:
|
|
283
|
+
return state;
|
|
284
|
+
}
|
|
285
|
+
};
|
|
286
|
+
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
The Decide-Function then makes the decision ( success or error )
|
|
290
|
+
```
|
|
291
|
+
export const decide = (
|
|
292
|
+
command: PlanVacationCommand,
|
|
293
|
+
state: PlanVacationState,
|
|
294
|
+
): ContextEvents[] => {
|
|
295
|
+
|
|
296
|
+
state.plannedVacations.forEach(vacation => {
|
|
297
|
+
if (
|
|
298
|
+
command.data.from <= vacation.to &&
|
|
299
|
+
command.data.to >= vacation.from
|
|
300
|
+
) {
|
|
301
|
+
throw {error: "conflicting_vacations"}
|
|
302
|
+
}
|
|
303
|
+
})
|
|
304
|
+
|
|
305
|
+
return [{
|
|
306
|
+
type: "VacationPlanned",
|
|
307
|
+
data: {
|
|
308
|
+
...
|
|
309
|
+
},
|
|
310
|
+
metadata: {
|
|
311
|
+
...
|
|
312
|
+
}
|
|
313
|
+
}]
|
|
314
|
+
};
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
## Error Handling
|
|
318
|
+
|
|
319
|
+
In routes.ts, define an error mapper:
|
|
320
|
+
```
|
|
321
|
+
const errorMapping = (error:string): string => {
|
|
322
|
+
|
|
323
|
+
switch(error) {
|
|
324
|
+
case "conflicting_vacations" : return "Achtung, Betriebsurlaub überschneidet sich."
|
|
325
|
+
default: return "Leider ist ein Fehler aufgetreten"
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
use this directly in the route.
|
|
331
|
+
|
|
332
|
+
```
|
|
333
|
+
router.post('/api/planvacation/:id', requireRestaurantAccess, async (req: PlanVacationRequest, res: Response) => {
|
|
334
|
+
...
|
|
335
|
+
} catch (err:any) {
|
|
336
|
+
console.error(err);
|
|
337
|
+
return res.status(500).json({ok: false, error: errorMapping(err.error)});
|
|
338
|
+
}
|
|
339
|
+
});
|
|
340
|
+
};
|
|
341
|
+
```
|
|
342
|
+
|
|
343
|
+
## slice json
|
|
344
|
+
in each slice folder, generate a file .slice.json
|
|
345
|
+
```
|
|
346
|
+
{
|
|
347
|
+
"id" : "<slice id>",
|
|
348
|
+
"slice": "<slice title>",
|
|
349
|
+
"context": "<contextx>",
|
|
350
|
+
"link": "https://miro.com/app/board/<board-id>=/?moveToWidget=<slice id>"
|
|
351
|
+
}
|
|
352
|
+
```
|
|
353
|
+
|
|
354
|
+
## References
|
|
355
|
+
|
|
356
|
+
- See `templates/AddLocation` for samples
|
|
357
|
+
|
|
358
|
+
## UI Prompt
|
|
359
|
+
|
|
360
|
+
```
|
|
361
|
+
to build the UI - use this endpont "endpoint URL"
|
|
362
|
+
|
|
363
|
+
Payload example:
|
|
364
|
+
<payload example as JSON>
|
|
365
|
+
|
|
366
|
+
make sure to put endpoints into the api.ts and follow the rules:
|
|
367
|
+
- provide all headers
|
|
368
|
+
|
|
369
|
+
```
|
package/templates/.claude/skills/slice-state-change/templates/AddLocation/AddLocation.test.ts.sample
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import {DeciderSpecification} from '@event-driven-io/emmett';
|
|
2
|
+
import {AddLocationCommand, AddLocationInitialState, decide, evolve} from "./AddLocationCommand";
|
|
3
|
+
import {describe, it} from "node:test";
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
describe('AddLocation Specification', () => {
|
|
7
|
+
|
|
8
|
+
const given = DeciderSpecification.for({
|
|
9
|
+
decide,
|
|
10
|
+
evolve,
|
|
11
|
+
initialState: AddLocationInitialState
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it('spec: Add Location - scenario', () => {
|
|
15
|
+
|
|
16
|
+
const command: AddLocationCommand = {
|
|
17
|
+
type: 'AddLocation',
|
|
18
|
+
data: {
|
|
19
|
+
city: "New York",
|
|
20
|
+
housenumber: "123",
|
|
21
|
+
name: "Main Office",
|
|
22
|
+
street: "Broadway",
|
|
23
|
+
zipCode: "10001",
|
|
24
|
+
location_id: "7771b53c-b378-4f10-a6b7-16f837316960"
|
|
25
|
+
},
|
|
26
|
+
metadata: {now: new Date()},
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
given([])
|
|
30
|
+
.when(command)
|
|
31
|
+
.then([{
|
|
32
|
+
type: 'LocationAdded',
|
|
33
|
+
data: {
|
|
34
|
+
city: command.data.city,
|
|
35
|
+
housenumber: command.data.housenumber,
|
|
36
|
+
name: command.data.name,
|
|
37
|
+
street: command.data.street,
|
|
38
|
+
zipCode: command.data.zipCode,
|
|
39
|
+
location_id: command.data.location_id
|
|
40
|
+
},
|
|
41
|
+
metadata: {}
|
|
42
|
+
}])
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('spec: Add duplicate Location - scenario', () => {
|
|
46
|
+
|
|
47
|
+
const command: AddLocationCommand = {
|
|
48
|
+
type: 'AddLocation',
|
|
49
|
+
data: {
|
|
50
|
+
city: "Boston",
|
|
51
|
+
housenumber: "456",
|
|
52
|
+
name: "Branch Office",
|
|
53
|
+
street: "Main St",
|
|
54
|
+
zipCode: "02101",
|
|
55
|
+
location_id: "7771b53c-b378-4f10-a6b7-16f837316960"
|
|
56
|
+
},
|
|
57
|
+
metadata: {now: new Date()},
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
given([{
|
|
61
|
+
type: 'LocationAdded',
|
|
62
|
+
data: {
|
|
63
|
+
city: "New York",
|
|
64
|
+
housenumber: "123",
|
|
65
|
+
name: "Main Office",
|
|
66
|
+
street: "Broadway",
|
|
67
|
+
zipCode: "10001",
|
|
68
|
+
location_id: "7771b53c-b378-4f10-a6b7-16f837316960"
|
|
69
|
+
},
|
|
70
|
+
metadata: {}
|
|
71
|
+
}])
|
|
72
|
+
.when(command)
|
|
73
|
+
.thenThrows()
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
});
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import type {Command} from '@event-driven-io/emmett'
|
|
2
|
+
import {CommandHandler} from '@event-driven-io/emmett';
|
|
3
|
+
import {ContextEvents} from "../../events/ContextEvents";
|
|
4
|
+
import {findEventstore} from "../../common/loadPostgresEventstore";
|
|
5
|
+
|
|
6
|
+
export type AddLocationCommand = Command<'AddLocation', {
|
|
7
|
+
city: string,
|
|
8
|
+
housenumber: string,
|
|
9
|
+
name: string,
|
|
10
|
+
street: string,
|
|
11
|
+
zipCode: string,
|
|
12
|
+
location_id: string
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
correlation_id?: string,
|
|
16
|
+
causation_id?: string,
|
|
17
|
+
now?: Date,
|
|
18
|
+
streamName?: string,
|
|
19
|
+
}>;
|
|
20
|
+
|
|
21
|
+
export type AddLocationState = {
|
|
22
|
+
locationIds: Set<string>;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export const AddLocationInitialState = (): AddLocationState => ({
|
|
26
|
+
locationIds: new Set(),
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
export const evolve = (
|
|
30
|
+
state: AddLocationState,
|
|
31
|
+
event: ContextEvents,
|
|
32
|
+
): AddLocationState => {
|
|
33
|
+
const {type, data} = event;
|
|
34
|
+
|
|
35
|
+
switch (type) {
|
|
36
|
+
case "LocationAdded":
|
|
37
|
+
return {
|
|
38
|
+
...state,
|
|
39
|
+
locationIds: new Set([...state.locationIds, data.location_id])
|
|
40
|
+
};
|
|
41
|
+
default:
|
|
42
|
+
return state;
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
export const decide = (
|
|
47
|
+
command: AddLocationCommand,
|
|
48
|
+
state: AddLocationState,
|
|
49
|
+
): ContextEvents[] => {
|
|
50
|
+
// Check if location with same location_id already exists
|
|
51
|
+
if (state.locationIds.has(command.data.location_id)) {
|
|
52
|
+
throw new Error('location must be unique');
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return [{
|
|
56
|
+
type: "LocationAdded",
|
|
57
|
+
data: {
|
|
58
|
+
city: command.data.city,
|
|
59
|
+
housenumber: command.data.housenumber,
|
|
60
|
+
name: command.data.name,
|
|
61
|
+
street: command.data.street,
|
|
62
|
+
zipCode: command.data.zipCode,
|
|
63
|
+
location_id: command.data.location_id
|
|
64
|
+
}, metadata: {
|
|
65
|
+
correlation_id: command.metadata?.correlation_id,
|
|
66
|
+
causation_id: command.metadata?.causation_id
|
|
67
|
+
}
|
|
68
|
+
}]
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
const AddLocationCommandHandler = CommandHandler<AddLocationState, ContextEvents>({
|
|
73
|
+
evolve,
|
|
74
|
+
initialState: AddLocationInitialState
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
export const handleAddLocation = async (id: string, command: AddLocationCommand) => {
|
|
78
|
+
const eventStore = await findEventstore()
|
|
79
|
+
const result = await AddLocationCommandHandler(eventStore, id, (state: AddLocationState) => decide(command, state))
|
|
80
|
+
return {
|
|
81
|
+
nextExpectedStreamVersion: result.nextExpectedStreamVersion,
|
|
82
|
+
lastEventGlobalPosition: result.lastEventGlobalPosition
|
|
83
|
+
}
|
|
84
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import {Request, Response, Router} from 'express';
|
|
2
|
+
import {AddLocationCommand, handleAddLocation} from './AddLocationCommand';
|
|
3
|
+
import {requireUser} from "../../supabase/requireUser";
|
|
4
|
+
import {WebApiSetup} from "@event-driven-io/emmett-expressjs";
|
|
5
|
+
import {assertNotEmpty} from "../../util/assertions";
|
|
6
|
+
|
|
7
|
+
export type AddLocationRequestPayload = {
|
|
8
|
+
city?: string,
|
|
9
|
+
housenumber?: string,
|
|
10
|
+
name?: string,
|
|
11
|
+
street?: string,
|
|
12
|
+
zipCode?: string,
|
|
13
|
+
location_id?: string
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export type AddLocationRequest = Request<
|
|
17
|
+
Partial<{ id: string }>,
|
|
18
|
+
unknown,
|
|
19
|
+
Partial<AddLocationRequestPayload>
|
|
20
|
+
>;
|
|
21
|
+
|
|
22
|
+
export const api =
|
|
23
|
+
(
|
|
24
|
+
// external dependencies
|
|
25
|
+
): WebApiSetup =>
|
|
26
|
+
(router: Router): void => {
|
|
27
|
+
router.post('/api/addlocation/:id', requireRestaurantAccess, async (req: AddLocationRequest, res: Response) => {
|
|
28
|
+
const principal = await requireUser(req, res, false);
|
|
29
|
+
if (principal.error) {
|
|
30
|
+
return res.status(401).json(principal);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const correlation_id = req.header("correlation_id") ?? req.params.id
|
|
34
|
+
const causation_id = req.params.id
|
|
35
|
+
|
|
36
|
+
try {
|
|
37
|
+
const command: AddLocationCommand = {
|
|
38
|
+
data: {
|
|
39
|
+
city: assertNotEmpty(req.body.city),
|
|
40
|
+
housenumber: assertNotEmpty(req.body.housenumber),
|
|
41
|
+
name: assertNotEmpty(req.body.name),
|
|
42
|
+
street: assertNotEmpty(req.body.street),
|
|
43
|
+
zipCode: assertNotEmpty(req.body.zipCode),
|
|
44
|
+
location_id: assertNotEmpty(req.body.location_id)
|
|
45
|
+
},
|
|
46
|
+
metadata: {
|
|
47
|
+
correlation_id: correlation_id,
|
|
48
|
+
causation_id: causation_id
|
|
49
|
+
},
|
|
50
|
+
type: "AddLocation"
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (!req.params.id) throw "no id provided"
|
|
54
|
+
|
|
55
|
+
const result = await handleAddLocation(assertNotEmpty(req.params.id), command);
|
|
56
|
+
|
|
57
|
+
res.set("correlation_id", correlation_id)
|
|
58
|
+
res.set("causation_id", causation_id)
|
|
59
|
+
|
|
60
|
+
return res.status(200).json({
|
|
61
|
+
ok: true,
|
|
62
|
+
next_expected_stream_version: result.nextExpectedStreamVersion?.toString(),
|
|
63
|
+
last_event_global_position: result.lastEventGlobalPosition?.toString()
|
|
64
|
+
});
|
|
65
|
+
} catch (err) {
|
|
66
|
+
console.error(err);
|
|
67
|
+
if (err instanceof Error && err.message === 'location must be unique') {
|
|
68
|
+
return res.status(409).json({ok: false, error: 'location must be unique'});
|
|
69
|
+
}
|
|
70
|
+
return res.status(500).json({ok: false, error: 'Server error'});
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
};
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# State-Change Slice Templates
|
|
2
|
+
|
|
3
|
+
This folder contains real working examples from the codebase to use as templates.
|
|
4
|
+
|
|
5
|
+
## Simple Example: AddTable
|
|
6
|
+
|
|
7
|
+
**Files:**
|
|
8
|
+
- `sample.ts.sample` - Command handler
|
|
9
|
+
- `sample-test.ts.sample` - Tests
|
|
10
|
+
- `routes.ts.sample` - API endpoint
|
|
11
|
+
|
|
12
|
+
**Use this when:**
|
|
13
|
+
- No business logic validation needed
|
|
14
|
+
- Simple event emission
|
|
15
|
+
- Empty state `{}`
|
|
16
|
+
- Direct command-to-event mapping
|
|
17
|
+
|
|
18
|
+
**Key features:**
|
|
19
|
+
- Destructuring pattern for command data
|
|
20
|
+
- Direct metadata access (no optional chaining)
|
|
21
|
+
- Simple decide function with no validation
|
|
22
|
+
|
|
23
|
+
## Complex Example with Validation: AddLocation
|
|
24
|
+
|
|
25
|
+
**Files:**
|
|
26
|
+
- `AddLocation/AddLocationCommand.ts.sample` - Command handler
|
|
27
|
+
- `AddLocation/AddLocation.test.ts.sample` - Tests with validation scenarios
|
|
28
|
+
- `AddLocation/routes.ts.sample` - API endpoint with error handling
|
|
29
|
+
|
|
30
|
+
**Use this when:**
|
|
31
|
+
- Business logic validation required
|
|
32
|
+
- State tracking needed (e.g., tracking unique IDs)
|
|
33
|
+
- Error conditions must be tested
|
|
34
|
+
- Custom error handling in API
|
|
35
|
+
|
|
36
|
+
**Key features:**
|
|
37
|
+
- State management with Set for uniqueness checking
|
|
38
|
+
- Evolve function that updates state
|
|
39
|
+
- Validation in decide function with error throwing
|
|
40
|
+
- Multiple test scenarios (success and error cases)
|
|
41
|
+
- Custom HTTP status codes (409 for conflict)
|
|
42
|
+
- Specific error message handling in routes
|
|
43
|
+
|
|
44
|
+
## Other Files
|
|
45
|
+
|
|
46
|
+
- `sample-input.json` - Example JSON input structure for event modeling
|