@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.
Files changed (212) hide show
  1. package/README.md +254 -0
  2. package/dist/cli.d.ts +2 -0
  3. package/dist/cli.js +138 -0
  4. package/package.json +49 -0
  5. package/templates/.claude/hooks/QUICKSTART.md +256 -0
  6. package/templates/.claude/hooks/README.md +533 -0
  7. package/templates/.claude/hooks/analyze-commit.sh +22 -0
  8. package/templates/.claude/hooks/analyze-commit.ts +518 -0
  9. package/templates/.claude/hooks/analyzers/README.md +198 -0
  10. package/templates/.claude/hooks/analyzers/code-quality-checker.ts +154 -0
  11. package/templates/.claude/hooks/analyzers/code-quality.md +54 -0
  12. package/templates/.claude/hooks/analyzers/commit-blocker-example.ts.disabled +110 -0
  13. package/templates/.claude/hooks/analyzers/commit-policy.md +49 -0
  14. package/templates/.claude/hooks/analyzers/event-model-validator.md +49 -0
  15. package/templates/.claude/hooks/analyzers/event-model-validator.ts +169 -0
  16. package/templates/.claude/hooks/analyzers/example-logger.ts +70 -0
  17. package/templates/.claude/hooks/analyzers/slice-scope-validator.md +81 -0
  18. package/templates/.claude/hooks/check-review-result.sh +47 -0
  19. package/templates/.claude/hooks/prepare-review.sh +34 -0
  20. package/templates/.claude/hooks/review-agent-prompt.md +42 -0
  21. package/templates/.claude/hooks/run-review-agent.sh +124 -0
  22. package/templates/.claude/settings.local.json +37 -0
  23. package/templates/.claude/skills/help/README.md +84 -0
  24. package/templates/.claude/skills/help/SKILL.md +393 -0
  25. package/templates/.claude/skills/help/templates/demo-config.json +6753 -0
  26. package/templates/.claude/skills/sample-slices/SKILL.md +8 -0
  27. package/templates/.claude/skills/sample-slices/templates/.slices/Library/addbook/code-slice.json +124 -0
  28. package/templates/.claude/skills/sample-slices/templates/.slices/Library/addbook/slice.json +255 -0
  29. package/templates/.claude/skills/sample-slices/templates/.slices/Library/availablebooks/slice.json +107 -0
  30. package/templates/.claude/skills/sample-slices/templates/.slices/index.json +20 -0
  31. package/templates/.claude/skills/sample-slices/templates/Cart/additem/slice.json +979 -0
  32. package/templates/.claude/skills/sample-slices/templates/Cart/archiveitem/slice.json +529 -0
  33. package/templates/.claude/skills/sample-slices/templates/Cart/cartitems/slice.json +1072 -0
  34. package/templates/.claude/skills/sample-slices/templates/Cart/cartwithproducts/slice.json +394 -0
  35. package/templates/.claude/skills/sample-slices/templates/Cart/changedprices/slice.json +88 -0
  36. package/templates/.claude/skills/sample-slices/templates/Cart/changeinventory/slice.json +264 -0
  37. package/templates/.claude/skills/sample-slices/templates/Cart/changeprice/slice.json +308 -0
  38. package/templates/.claude/skills/sample-slices/templates/Cart/clearcart/slice.json +358 -0
  39. package/templates/.claude/skills/sample-slices/templates/Cart/inventories/slice.json +203 -0
  40. package/templates/.claude/skills/sample-slices/templates/Cart/publishcart/slice.json +876 -0
  41. package/templates/.claude/skills/sample-slices/templates/Cart/removeitem/slice.json +560 -0
  42. package/templates/.claude/skills/sample-slices/templates/Cart/submitcart/slice.json +708 -0
  43. package/templates/.claude/skills/sample-slices/templates/Cart/submittedcartdata/slice.json +399 -0
  44. package/templates/.claude/skills/sample-slices/templates/index.json +108 -0
  45. package/templates/.claude/skills/slice-automation/SKILL.md +49 -0
  46. package/templates/.claude/skills/slice-state-change/SKILL.md +369 -0
  47. package/templates/.claude/skills/slice-state-change/templates/AddLocation/AddLocation.test.ts.sample +76 -0
  48. package/templates/.claude/skills/slice-state-change/templates/AddLocation/AddLocationCommand.ts.sample +84 -0
  49. package/templates/.claude/skills/slice-state-change/templates/AddLocation/routes.ts.sample +73 -0
  50. package/templates/.claude/skills/slice-state-change/templates/README.md +46 -0
  51. package/templates/.claude/skills/slice-state-view/SKILL.md +336 -0
  52. package/templates/.claude/skills/slice-state-view/templates/Locations/Locations.test.ts.sample +84 -0
  53. package/templates/.claude/skills/slice-state-view/templates/Locations/LocationsProjection.ts.sample +50 -0
  54. package/templates/.claude/skills/slice-state-view/templates/Locations/routes.ts.sample +46 -0
  55. package/templates/.claude/skills/slice-state-view/templates/README.md +109 -0
  56. package/templates/.claude/skills/slice-state-view/templates/Tables/Tables.test.ts.sample +104 -0
  57. package/templates/.claude/skills/slice-state-view/templates/Tables/TablesProjection.ts.sample +59 -0
  58. package/templates/.claude/skills/slice-state-view/templates/Tables/routes.ts.sample +46 -0
  59. package/templates/.claude/skills/slice-state-view/templates/V2__tables.sql +7 -0
  60. package/templates/.claude/skills/slice-state-view/templates/V8__locations.sql +7 -0
  61. package/templates/.claude/skills/test-analyzer/SKILL.md +373 -0
  62. package/templates/.claude/skills/test-analyzer/examples/specification-format.md +143 -0
  63. package/templates/.claude/skills/test-analyzer/examples/state-change-example.md +111 -0
  64. package/templates/.claude/skills/test-analyzer/examples/state-view-example.md +122 -0
  65. package/templates/AGENTS.md +110 -0
  66. package/templates/Claude.md +58 -0
  67. package/templates/README.md +178 -0
  68. package/templates/backend/.env +9 -0
  69. package/templates/backend/BACKEND_AUTH_SETUP.md +183 -0
  70. package/templates/backend/SWAGGER.md +213 -0
  71. package/templates/backend/eslint.config.mjs +31 -0
  72. package/templates/backend/flyway.conf +17 -0
  73. package/templates/backend/package.json +44 -0
  74. package/templates/backend/prd.json.example +64 -0
  75. package/templates/backend/public/assets/images/banner.png +0 -0
  76. package/templates/backend/public/assets/logo.png +0 -0
  77. package/templates/backend/public/file.svg +4 -0
  78. package/templates/backend/public/globe.svg +12 -0
  79. package/templates/backend/public/next.svg +6 -0
  80. package/templates/backend/public/vercel.svg +3 -0
  81. package/templates/backend/public/window.svg +5 -0
  82. package/templates/backend/server.ts +129 -0
  83. package/templates/backend/setup-env.sh +50 -0
  84. package/templates/backend/src/common/assertions.ts +6 -0
  85. package/templates/backend/src/common/db.ts +1 -0
  86. package/templates/backend/src/common/loadPostgresEventstore.ts +16 -0
  87. package/templates/backend/src/common/parseEndpoint.ts +51 -0
  88. package/templates/backend/src/common/replay.ts +9 -0
  89. package/templates/backend/src/common/routes.ts +19 -0
  90. package/templates/backend/src/common/testHelpers.ts +53 -0
  91. package/templates/backend/src/core/readmodel.ts +28 -0
  92. package/templates/backend/src/core/types.ts +26 -0
  93. package/templates/backend/src/process/process.ts +53 -0
  94. package/templates/backend/src/supabase/LoginHandler.ts +36 -0
  95. package/templates/backend/src/supabase/ProtectedPageProps.ts +21 -0
  96. package/templates/backend/src/supabase/README.md +171 -0
  97. package/templates/backend/src/supabase/api.ts +63 -0
  98. package/templates/backend/src/supabase/authMiddleware.ts +53 -0
  99. package/templates/backend/src/supabase/component.ts +12 -0
  100. package/templates/backend/src/supabase/requireUser.ts +72 -0
  101. package/templates/backend/src/supabase/serverProps.ts +25 -0
  102. package/templates/backend/src/supabase/staticProps.ts +10 -0
  103. package/templates/backend/src/swagger.ts +34 -0
  104. package/templates/backend/src/util/assertions.ts +6 -0
  105. package/templates/backend/supabase/config.toml +295 -0
  106. package/templates/backend/supabase/migrations/20260121155918593_catalogentries.sql.sample +23 -0
  107. package/templates/backend/supabase/seed.sql +1 -0
  108. package/templates/backend/tsconfig.json +31 -0
  109. package/templates/frontend/.env.development +3 -0
  110. package/templates/frontend/AGENTS.md +7 -0
  111. package/templates/frontend/README.md +73 -0
  112. package/templates/frontend/components.json +20 -0
  113. package/templates/frontend/eslint.config.js +26 -0
  114. package/templates/frontend/index.html +18 -0
  115. package/templates/frontend/package-lock.json +8347 -0
  116. package/templates/frontend/package.json +94 -0
  117. package/templates/frontend/postcss.config.js +6 -0
  118. package/templates/frontend/public/favicon.ico +0 -0
  119. package/templates/frontend/public/logo.png +0 -0
  120. package/templates/frontend/public/placeholder.svg +1 -0
  121. package/templates/frontend/public/robots.txt +14 -0
  122. package/templates/frontend/src/App.css +42 -0
  123. package/templates/frontend/src/App.tsx +47 -0
  124. package/templates/frontend/src/components/NavLink.tsx +28 -0
  125. package/templates/frontend/src/components/ProtectedRoute.tsx +24 -0
  126. package/templates/frontend/src/components/calendar/Calendar.tsx +302 -0
  127. package/templates/frontend/src/components/layout/DashboardLayout.tsx +21 -0
  128. package/templates/frontend/src/components/layout/Header.tsx +45 -0
  129. package/templates/frontend/src/components/layout/Sidebar.tsx +82 -0
  130. package/templates/frontend/src/components/tables/ReservationTemplates.tsx +189 -0
  131. package/templates/frontend/src/components/ui/accordion.tsx +52 -0
  132. package/templates/frontend/src/components/ui/alert-dialog.tsx +104 -0
  133. package/templates/frontend/src/components/ui/alert.tsx +43 -0
  134. package/templates/frontend/src/components/ui/aspect-ratio.tsx +5 -0
  135. package/templates/frontend/src/components/ui/avatar.tsx +38 -0
  136. package/templates/frontend/src/components/ui/badge.tsx +29 -0
  137. package/templates/frontend/src/components/ui/breadcrumb.tsx +90 -0
  138. package/templates/frontend/src/components/ui/button.tsx +47 -0
  139. package/templates/frontend/src/components/ui/calendar.tsx +54 -0
  140. package/templates/frontend/src/components/ui/card.tsx +43 -0
  141. package/templates/frontend/src/components/ui/carousel.tsx +224 -0
  142. package/templates/frontend/src/components/ui/chart.tsx +303 -0
  143. package/templates/frontend/src/components/ui/checkbox.tsx +26 -0
  144. package/templates/frontend/src/components/ui/collapsible.tsx +9 -0
  145. package/templates/frontend/src/components/ui/command.tsx +132 -0
  146. package/templates/frontend/src/components/ui/context-menu.tsx +178 -0
  147. package/templates/frontend/src/components/ui/dialog.tsx +95 -0
  148. package/templates/frontend/src/components/ui/drawer.tsx +87 -0
  149. package/templates/frontend/src/components/ui/dropdown-menu.tsx +179 -0
  150. package/templates/frontend/src/components/ui/form.tsx +129 -0
  151. package/templates/frontend/src/components/ui/hover-card.tsx +27 -0
  152. package/templates/frontend/src/components/ui/input-otp.tsx +61 -0
  153. package/templates/frontend/src/components/ui/input.tsx +22 -0
  154. package/templates/frontend/src/components/ui/label.tsx +17 -0
  155. package/templates/frontend/src/components/ui/menubar.tsx +207 -0
  156. package/templates/frontend/src/components/ui/navigation-menu.tsx +120 -0
  157. package/templates/frontend/src/components/ui/pagination.tsx +81 -0
  158. package/templates/frontend/src/components/ui/popover.tsx +29 -0
  159. package/templates/frontend/src/components/ui/progress.tsx +23 -0
  160. package/templates/frontend/src/components/ui/radio-group.tsx +36 -0
  161. package/templates/frontend/src/components/ui/resizable.tsx +37 -0
  162. package/templates/frontend/src/components/ui/scroll-area.tsx +38 -0
  163. package/templates/frontend/src/components/ui/select.tsx +143 -0
  164. package/templates/frontend/src/components/ui/separator.tsx +20 -0
  165. package/templates/frontend/src/components/ui/sheet.tsx +107 -0
  166. package/templates/frontend/src/components/ui/sidebar.tsx +637 -0
  167. package/templates/frontend/src/components/ui/skeleton.tsx +7 -0
  168. package/templates/frontend/src/components/ui/slider.tsx +23 -0
  169. package/templates/frontend/src/components/ui/sonner.tsx +27 -0
  170. package/templates/frontend/src/components/ui/stat-card.tsx +44 -0
  171. package/templates/frontend/src/components/ui/switch.tsx +27 -0
  172. package/templates/frontend/src/components/ui/table.tsx +72 -0
  173. package/templates/frontend/src/components/ui/tabs.tsx +53 -0
  174. package/templates/frontend/src/components/ui/textarea.tsx +21 -0
  175. package/templates/frontend/src/components/ui/toast.tsx +111 -0
  176. package/templates/frontend/src/components/ui/toaster.tsx +24 -0
  177. package/templates/frontend/src/components/ui/toggle-group.tsx +49 -0
  178. package/templates/frontend/src/components/ui/toggle.tsx +37 -0
  179. package/templates/frontend/src/components/ui/tooltip.tsx +28 -0
  180. package/templates/frontend/src/components/ui/use-toast.ts +3 -0
  181. package/templates/frontend/src/contexts/AuthContext.tsx +94 -0
  182. package/templates/frontend/src/contexts/RefreshContext.tsx +236 -0
  183. package/templates/frontend/src/hooks/api/index.ts +2 -0
  184. package/templates/frontend/src/hooks/api/useLocations.ts +15 -0
  185. package/templates/frontend/src/hooks/use-mobile.tsx +19 -0
  186. package/templates/frontend/src/hooks/use-toast.ts +186 -0
  187. package/templates/frontend/src/hooks/useApiContext.ts +11 -0
  188. package/templates/frontend/src/index.css +118 -0
  189. package/templates/frontend/src/integrations/supabase/client.ts +9 -0
  190. package/templates/frontend/src/lib/api-client.ts +136 -0
  191. package/templates/frontend/src/lib/api.ts +1028 -0
  192. package/templates/frontend/src/lib/utils.ts +6 -0
  193. package/templates/frontend/src/main.tsx +5 -0
  194. package/templates/frontend/src/pages/Auth.tsx +408 -0
  195. package/templates/frontend/src/pages/Dashboard.tsx +168 -0
  196. package/templates/frontend/src/pages/Menus.tsx +224 -0
  197. package/templates/frontend/src/pages/NotFound.tsx +24 -0
  198. package/templates/frontend/src/pages/Register.tsx +285 -0
  199. package/templates/frontend/src/test/example.test.ts +0 -0
  200. package/templates/frontend/src/test/setup.ts +15 -0
  201. package/templates/frontend/src/types/index.ts +8 -0
  202. package/templates/frontend/src/vite-env.d.ts +1 -0
  203. package/templates/frontend/tailwind.config.ts +101 -0
  204. package/templates/frontend/tsconfig.app.json +31 -0
  205. package/templates/frontend/tsconfig.json +16 -0
  206. package/templates/frontend/tsconfig.node.json +22 -0
  207. package/templates/frontend/vite.config.ts +21 -0
  208. package/templates/frontend/vitest.config.ts +16 -0
  209. package/templates/init.sh +1 -0
  210. package/templates/prompt.md +139 -0
  211. package/templates/ralph.sh +120 -0
  212. 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
+ ```
@@ -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