@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.
Files changed (220) hide show
  1. package/README.md +175 -0
  2. package/cli.js +676 -0
  3. package/package.json +35 -0
  4. package/shared/build-kit/code-export.mjs +560 -0
  5. package/shared/build-kit/lib/ollama-agent.js +147 -0
  6. package/shared/build-kit/package.json +11 -0
  7. package/shared/build-kit/ralph-ollama.js +39 -0
  8. package/shared/build-kit/realtime-agent.js +18 -0
  9. package/stacks/axon/templates/.claude/skills/build-automation/SKILL.md +407 -0
  10. package/stacks/axon/templates/.claude/skills/build-automation/references/feature-flag-patterns.md +114 -0
  11. package/stacks/axon/templates/.claude/skills/build-automation-workflow/SKILL.md +496 -0
  12. package/stacks/axon/templates/.claude/skills/build-automation-workflow/references/axon-workflow-api.md +279 -0
  13. package/stacks/axon/templates/.claude/skills/build-automation-workflow/references/feature-flag-patterns.md +114 -0
  14. package/stacks/axon/templates/.claude/skills/build-state-change/SKILL.md +396 -0
  15. package/stacks/axon/templates/.claude/skills/build-state-change/references/axon-test-fixture-patterns.md +162 -0
  16. package/stacks/axon/templates/.claude/skills/build-state-change/references/feature-flag-patterns.md +56 -0
  17. package/stacks/axon/templates/.claude/skills/build-state-change/references/rest-api-patterns.md +115 -0
  18. package/stacks/axon/templates/.claude/skills/build-state-view/SKILL.md +282 -0
  19. package/stacks/axon/templates/.claude/skills/connect/SKILL.md +178 -0
  20. package/stacks/axon/templates/.claude/skills/learn-eventmodelers-api/SKILL.md +611 -0
  21. package/stacks/axon/templates/.claude/skills/load-slice/SKILL.md +141 -0
  22. package/stacks/axon/templates/.claude/skills/update-slice-status/SKILL.md +105 -0
  23. package/stacks/axon/templates/build-kit/lib/AGENT.md +47 -0
  24. package/stacks/axon/templates/build-kit/lib/agent.sh +20 -0
  25. package/stacks/axon/templates/build-kit/lib/backend-prompt.md +158 -0
  26. package/stacks/axon/templates/build-kit/lib/prompt.md +126 -0
  27. package/stacks/axon/templates/build-kit/lib/ralph.js +369 -0
  28. package/stacks/axon/templates/build-kit/ralph-claude.js +44 -0
  29. package/stacks/axon/templates/build-kit/ralph.sh +98 -0
  30. package/stacks/axon/templates/root/.env.example +5 -0
  31. package/stacks/axon/templates/root/CLAUDE.md +60 -0
  32. package/stacks/axon/templates/root/README.md +44 -0
  33. package/stacks/axon/templates/root/docker-compose.yml +64 -0
  34. package/stacks/axon/templates/root/mvnw +259 -0
  35. package/stacks/axon/templates/root/mvnw.cmd +149 -0
  36. package/stacks/axon/templates/root/pom.xml +125 -0
  37. package/stacks/axon/templates/root/src/main/java/io/axoniq/quickstart/QuickstartApplication.java +128 -0
  38. package/stacks/axon/templates/root/src/main/java/io/axoniq/quickstart/config/WebConfig.java +155 -0
  39. package/stacks/axon/templates/root/src/main/resources/application.properties +2 -0
  40. package/stacks/axon/templates/root/src/main/resources/static/bg-body.png +0 -0
  41. package/stacks/axon/templates/root/src/main/resources/static/bg-header.png +0 -0
  42. package/stacks/axon/templates/root/src/main/resources/static/index.html +596 -0
  43. package/stacks/cratis-csharp/templates/.claude/skills/_shared/cratis-conventions.md +251 -0
  44. package/stacks/cratis-csharp/templates/.claude/skills/build-automation/SKILL.md +99 -0
  45. package/stacks/cratis-csharp/templates/.claude/skills/build-automation/references/patterns.md +115 -0
  46. package/stacks/cratis-csharp/templates/.claude/skills/build-state-change/SKILL.md +163 -0
  47. package/stacks/cratis-csharp/templates/.claude/skills/build-state-change/references/patterns.md +234 -0
  48. package/stacks/cratis-csharp/templates/.claude/skills/build-state-view/SKILL.md +120 -0
  49. package/stacks/cratis-csharp/templates/.claude/skills/build-state-view/references/patterns.md +166 -0
  50. package/stacks/cratis-csharp/templates/.claude/skills/connect/SKILL.md +169 -0
  51. package/stacks/cratis-csharp/templates/.claude/skills/learn-eventmodelers-api/SKILL.md +609 -0
  52. package/stacks/cratis-csharp/templates/.claude/skills/load-slice/SKILL.md +141 -0
  53. package/stacks/cratis-csharp/templates/.claude/skills/update-slice-status/SKILL.md +105 -0
  54. package/stacks/cratis-csharp/templates/build-kit/lib/AGENT.md +58 -0
  55. package/stacks/cratis-csharp/templates/build-kit/lib/agent.sh +20 -0
  56. package/stacks/cratis-csharp/templates/build-kit/lib/backend-prompt.md +129 -0
  57. package/stacks/cratis-csharp/templates/build-kit/lib/prompt.md +124 -0
  58. package/stacks/cratis-csharp/templates/build-kit/lib/ralph.js +302 -0
  59. package/stacks/cratis-csharp/templates/build-kit/ralph-claude.js +37 -0
  60. package/stacks/cratis-csharp/templates/build-kit/ralph.sh +98 -0
  61. package/stacks/cratis-csharp/templates/root/.frontend/index.css +29 -0
  62. package/stacks/cratis-csharp/templates/root/.frontend/index.html +17 -0
  63. package/stacks/cratis-csharp/templates/root/.frontend/main.tsx +18 -0
  64. package/stacks/cratis-csharp/templates/root/.frontend/tsconfig.json +42 -0
  65. package/stacks/cratis-csharp/templates/root/.frontend/tsconfig.node.json +11 -0
  66. package/stacks/cratis-csharp/templates/root/.frontend/vite.config.ts +56 -0
  67. package/stacks/cratis-csharp/templates/root/App.tsx +23 -0
  68. package/stacks/cratis-csharp/templates/root/CLAUDE.md +62 -0
  69. package/stacks/cratis-csharp/templates/root/CratisApp.csproj +25 -0
  70. package/stacks/cratis-csharp/templates/root/CratisApp.sln +18 -0
  71. package/stacks/cratis-csharp/templates/root/GlobalUsings.cs +3 -0
  72. package/stacks/cratis-csharp/templates/root/Home.tsx +102 -0
  73. package/stacks/cratis-csharp/templates/root/Program.cs +26 -0
  74. package/stacks/cratis-csharp/templates/root/README.md +192 -0
  75. package/stacks/cratis-csharp/templates/root/SomeModule/SomeFeature/Listing/AllListings.ts +47 -0
  76. package/stacks/cratis-csharp/templates/root/SomeModule/SomeFeature/Listing/Listing.cs +11 -0
  77. package/stacks/cratis-csharp/templates/root/SomeModule/SomeFeature/Listing/Listing.ts +12 -0
  78. package/stacks/cratis-csharp/templates/root/SomeModule/SomeFeature/Listing/ListingDataTable.tsx +17 -0
  79. package/stacks/cratis-csharp/templates/root/SomeModule/SomeFeature/Listing/index.ts +1 -0
  80. package/stacks/cratis-csharp/templates/root/SomeModule/SomeFeature/Registration/Register.ts +51 -0
  81. package/stacks/cratis-csharp/templates/root/SomeModule/SomeFeature/Registration/RegisterDialog.tsx +18 -0
  82. package/stacks/cratis-csharp/templates/root/SomeModule/SomeFeature/Registration/Registration.cs +27 -0
  83. package/stacks/cratis-csharp/templates/root/SomeModule/SomeFeature/Registration/index.ts +1 -0
  84. package/stacks/cratis-csharp/templates/root/SomeModule/SomeFeature/SomeFeature.tsx +22 -0
  85. package/stacks/cratis-csharp/templates/root/SomeModule/SomeFeature/SomeName.cs +3 -0
  86. package/stacks/cratis-csharp/templates/root/SomeModule/SomeFeature/index.ts +1 -0
  87. package/stacks/cratis-csharp/templates/root/appsettings.Development.json +9 -0
  88. package/stacks/cratis-csharp/templates/root/appsettings.json +26 -0
  89. package/stacks/cratis-csharp/templates/root/docker-compose.yml +23 -0
  90. package/stacks/cratis-csharp/templates/root/package.json +33 -0
  91. package/stacks/cratis-csharp/templates/root/tsconfig.json +3 -0
  92. package/stacks/modeling-kit/templates/.claude/settings.local.json +13 -0
  93. package/stacks/modeling-kit/templates/.claude/skills/analyze-existing-model/SKILL.md +215 -0
  94. package/stacks/modeling-kit/templates/.claude/skills/attributes/SKILL.md +176 -0
  95. package/stacks/modeling-kit/templates/.claude/skills/connect/SKILL.md +182 -0
  96. package/stacks/modeling-kit/templates/.claude/skills/discover-storyboard/SKILL.md +383 -0
  97. package/stacks/modeling-kit/templates/.claude/skills/eventmodeling-applying-conways-law/SKILL.md +465 -0
  98. package/stacks/modeling-kit/templates/.claude/skills/eventmodeling-brainstorming-events/SKILL.md +587 -0
  99. package/stacks/modeling-kit/templates/.claude/skills/eventmodeling-brainstorming-events/references/facilitating-event-modeling-workshops.md +626 -0
  100. package/stacks/modeling-kit/templates/.claude/skills/eventmodeling-checking-completeness/SKILL.md +542 -0
  101. package/stacks/modeling-kit/templates/.claude/skills/eventmodeling-designing-event-models/SKILL.md +497 -0
  102. package/stacks/modeling-kit/templates/.claude/skills/eventmodeling-elaborating-scenarios/SKILL.md +783 -0
  103. package/stacks/modeling-kit/templates/.claude/skills/eventmodeling-identifying-inputs/SKILL.md +604 -0
  104. package/stacks/modeling-kit/templates/.claude/skills/eventmodeling-identifying-outputs/SKILL.md +699 -0
  105. package/stacks/modeling-kit/templates/.claude/skills/eventmodeling-integrating-legacy-systems/SKILL.md +674 -0
  106. package/stacks/modeling-kit/templates/.claude/skills/eventmodeling-optimizing-stream-design/SKILL.md +231 -0
  107. package/stacks/modeling-kit/templates/.claude/skills/eventmodeling-optimizing-stream-design/references/domain-patterns.md +150 -0
  108. package/stacks/modeling-kit/templates/.claude/skills/eventmodeling-optimizing-stream-design/references/patterns.md +329 -0
  109. package/stacks/modeling-kit/templates/.claude/skills/eventmodeling-optimizing-stream-design/references/snapshotting.md +204 -0
  110. package/stacks/modeling-kit/templates/.claude/skills/eventmodeling-orchestrating-event-modeling/SKILL.md +352 -0
  111. package/stacks/modeling-kit/templates/.claude/skills/eventmodeling-orchestrating-event-modeling/references/project-planning-with-event-modeling.md +341 -0
  112. package/stacks/modeling-kit/templates/.claude/skills/eventmodeling-plotting-events/SKILL.md +153 -0
  113. package/stacks/modeling-kit/templates/.claude/skills/eventmodeling-slicing-event-models/SKILL.md +136 -0
  114. package/stacks/modeling-kit/templates/.claude/skills/eventmodeling-slicing-event-models/references/examples.md +78 -0
  115. package/stacks/modeling-kit/templates/.claude/skills/eventmodeling-slicing-event-models/references/patterns.md +85 -0
  116. package/stacks/modeling-kit/templates/.claude/skills/eventmodeling-storyboarding-events/SKILL.md +655 -0
  117. package/stacks/modeling-kit/templates/.claude/skills/eventmodeling-translating-external-events/SKILL.md +479 -0
  118. package/stacks/modeling-kit/templates/.claude/skills/eventmodeling-validating-event-models/SKILL.md +234 -0
  119. package/stacks/modeling-kit/templates/.claude/skills/eventmodeling-validating-event-models-checklist/SKILL.md +353 -0
  120. package/stacks/modeling-kit/templates/.claude/skills/examples/SKILL.md +200 -0
  121. package/stacks/modeling-kit/templates/.claude/skills/handle-comment/SKILL.md +94 -0
  122. package/stacks/modeling-kit/templates/.claude/skills/learn-eventmodelers-api/SKILL.md +441 -0
  123. package/stacks/modeling-kit/templates/.claude/skills/place-element/SKILL.md +290 -0
  124. package/stacks/modeling-kit/templates/.claude/skills/storyboard/SKILL.md +212 -0
  125. package/stacks/modeling-kit/templates/.claude/skills/storyboard-screen/SKILL.md +123 -0
  126. package/stacks/modeling-kit/templates/.claude/skills/timeline/SKILL.md +353 -0
  127. package/stacks/modeling-kit/templates/.claude/skills/update-slice-status/SKILL.md +110 -0
  128. package/stacks/modeling-kit/templates/.claude/skills/wdyt/SKILL.md +134 -0
  129. package/stacks/modeling-kit/templates/kit/lib/agent.sh +20 -0
  130. package/stacks/modeling-kit/templates/kit/lib/ollama-agent.js +147 -0
  131. package/stacks/modeling-kit/templates/kit/lib/ralph.js +286 -0
  132. package/stacks/modeling-kit/templates/kit/package.json +12 -0
  133. package/stacks/modeling-kit/templates/kit/ralph-claude.js +50 -0
  134. package/stacks/modeling-kit/templates/kit/ralph-ollama.js +38 -0
  135. package/stacks/modeling-kit/templates/kit/ralph.sh +61 -0
  136. package/stacks/modeling-kit/templates/kit/realtime-agent.js +18 -0
  137. package/stacks/modeling-kit/templates/root/CLAUDE.md +56 -0
  138. package/stacks/node/templates/.claude/skills/build-automation/SKILL.md +266 -0
  139. package/stacks/node/templates/.claude/skills/build-state-change/SKILL.md +344 -0
  140. package/stacks/node/templates/.claude/skills/build-state-view/SKILL.md +412 -0
  141. package/stacks/node/templates/.claude/skills/connect/SKILL.md +178 -0
  142. package/stacks/node/templates/.claude/skills/learn-eventmodelers-api/SKILL.md +628 -0
  143. package/stacks/node/templates/.claude/skills/load-slice/SKILL.md +143 -0
  144. package/stacks/node/templates/.claude/skills/update-slice-status/SKILL.md +110 -0
  145. package/stacks/node/templates/build-kit/README.md +86 -0
  146. package/stacks/node/templates/build-kit/lib/AGENT.md +47 -0
  147. package/stacks/node/templates/build-kit/lib/agent.sh +20 -0
  148. package/stacks/node/templates/build-kit/lib/backend-prompt.md +152 -0
  149. package/stacks/node/templates/build-kit/lib/prompt.md +122 -0
  150. package/stacks/node/templates/build-kit/lib/ralph.js +369 -0
  151. package/stacks/node/templates/build-kit/ralph-claude.js +44 -0
  152. package/stacks/node/templates/build-kit/ralph.sh +98 -0
  153. package/stacks/node/templates/root/.env.example +15 -0
  154. package/stacks/node/templates/root/CLAUDE.md +60 -0
  155. package/stacks/node/templates/root/docker-compose.yml +15 -0
  156. package/stacks/node/templates/root/flyway.conf +17 -0
  157. package/stacks/node/templates/root/migrations/V1__schema.sql.example +12 -0
  158. package/stacks/node/templates/root/package.json +50 -0
  159. package/stacks/node/templates/root/server.ts +130 -0
  160. package/stacks/node/templates/root/setup-env.sh +53 -0
  161. package/stacks/node/templates/root/src/common/assertions.ts +6 -0
  162. package/stacks/node/templates/root/src/common/db.ts +32 -0
  163. package/stacks/node/templates/root/src/common/loadPostgresEventstore.ts +23 -0
  164. package/stacks/node/templates/root/src/common/parseEndpoint.ts +51 -0
  165. package/stacks/node/templates/root/src/common/processorDlq.ts +28 -0
  166. package/stacks/node/templates/root/src/common/realtimeBroadcast.ts +12 -0
  167. package/stacks/node/templates/root/src/common/replay.ts +16 -0
  168. package/stacks/node/templates/root/src/common/routes.ts +19 -0
  169. package/stacks/node/templates/root/src/common/testHelpers.ts +44 -0
  170. package/stacks/node/templates/root/src/swagger.ts +34 -0
  171. package/stacks/node/templates/root/src/util/assertions.ts +6 -0
  172. package/stacks/node/templates/root/src/util/hash.ts +9 -0
  173. package/stacks/node/templates/root/src/util/sanitize.ts +23 -0
  174. package/stacks/node/templates/root/tsconfig.json +32 -0
  175. package/stacks/node/templates/root/vercel.json +8 -0
  176. package/stacks/supabase/templates/.claude/skills/build-automation/SKILL.md +266 -0
  177. package/stacks/supabase/templates/.claude/skills/build-state-change/SKILL.md +344 -0
  178. package/stacks/supabase/templates/.claude/skills/build-state-view/SKILL.md +390 -0
  179. package/stacks/supabase/templates/.claude/skills/build-webhook/SKILL.md +303 -0
  180. package/stacks/supabase/templates/.claude/skills/connect/SKILL.md +178 -0
  181. package/stacks/supabase/templates/.claude/skills/learn-eventmodelers-api/SKILL.md +628 -0
  182. package/stacks/supabase/templates/.claude/skills/load-slice/SKILL.md +143 -0
  183. package/stacks/supabase/templates/.claude/skills/update-slice-status/SKILL.md +110 -0
  184. package/stacks/supabase/templates/build-kit/README.md +86 -0
  185. package/stacks/supabase/templates/build-kit/lib/AGENT.md +47 -0
  186. package/stacks/supabase/templates/build-kit/lib/agent.sh +20 -0
  187. package/stacks/supabase/templates/build-kit/lib/backend-prompt.md +152 -0
  188. package/stacks/supabase/templates/build-kit/lib/prompt.md +123 -0
  189. package/stacks/supabase/templates/build-kit/lib/ralph.js +369 -0
  190. package/stacks/supabase/templates/build-kit/ralph-claude.js +44 -0
  191. package/stacks/supabase/templates/build-kit/ralph.sh +98 -0
  192. package/stacks/supabase/templates/root/.env.example +22 -0
  193. package/stacks/supabase/templates/root/CLAUDE.md +61 -0
  194. package/stacks/supabase/templates/root/flyway.conf +17 -0
  195. package/stacks/supabase/templates/root/package.json +52 -0
  196. package/stacks/supabase/templates/root/server.ts +177 -0
  197. package/stacks/supabase/templates/root/setup-env.sh +53 -0
  198. package/stacks/supabase/templates/root/src/common/assertions.ts +6 -0
  199. package/stacks/supabase/templates/root/src/common/db.ts +32 -0
  200. package/stacks/supabase/templates/root/src/common/loadPostgresEventstore.ts +23 -0
  201. package/stacks/supabase/templates/root/src/common/parseEndpoint.ts +51 -0
  202. package/stacks/supabase/templates/root/src/common/processorDlq.ts +28 -0
  203. package/stacks/supabase/templates/root/src/common/realtimeBroadcast.ts +19 -0
  204. package/stacks/supabase/templates/root/src/common/replay.ts +16 -0
  205. package/stacks/supabase/templates/root/src/common/routes.ts +23 -0
  206. package/stacks/supabase/templates/root/src/common/testHelpers.ts +54 -0
  207. package/stacks/supabase/templates/root/src/supabase/README.md +141 -0
  208. package/stacks/supabase/templates/root/src/supabase/api.ts +56 -0
  209. package/stacks/supabase/templates/root/src/supabase/requireOrgaAdmin.ts +32 -0
  210. package/stacks/supabase/templates/root/src/supabase/requireSysUser.ts +40 -0
  211. package/stacks/supabase/templates/root/src/supabase/requireUser.ts +72 -0
  212. package/stacks/supabase/templates/root/src/swagger.ts +34 -0
  213. package/stacks/supabase/templates/root/src/util/assertions.ts +6 -0
  214. package/stacks/supabase/templates/root/src/util/hash.ts +9 -0
  215. package/stacks/supabase/templates/root/src/util/sanitize.ts +23 -0
  216. package/stacks/supabase/templates/root/supabase/config.toml +295 -0
  217. package/stacks/supabase/templates/root/supabase/migrations/V1__schema.sql.example +12 -0
  218. package/stacks/supabase/templates/root/supabase/seed.sql +1 -0
  219. package/stacks/supabase/templates/root/tsconfig.json +32 -0
  220. package/stacks/supabase/templates/root/vercel.json +8 -0
@@ -0,0 +1,390 @@
1
+ ---
2
+ name: build-state-view
3
+ description: Implements an emmett state-view slice (projection, tests, route, migration) from a slice.json definition
4
+ ---
5
+
6
+ # Build State View Slice
7
+
8
+ > Before doing anything else, read the slice definition from `.slices/{Context}/{slicename}/slice.json`. This file is the **source of truth** for all fields, events, and read model shape. Never invent fields not defined there.
9
+
10
+ ---
11
+
12
+ ## What a State View Slice is
13
+
14
+ A state-view slice is a **read model projection**. It listens to events from the event store and materializes them into a queryable PostgreSQL table. It does not emit events or process commands.
15
+
16
+ ---
17
+
18
+ ## Step 1 — Read the slice.json
19
+
20
+ From the slice definition, extract:
21
+ - **sliceName** — the projection name
22
+ - **context** — bounded context
23
+ - **events[]** — events this projection handles (its `canHandle` list)
24
+ - **readModel / fields** — the columns of the output table
25
+ > **Comments & description**: Each element (commands, events, readmodels, processors, screens, tables) carries a `comments: string[]` array (board comments on that node) and a `description` field. The slice itself also has `comments: string[]`. Use these as implementation hints — pass them as code comments, documentation, or validation logic where they add value. When done, resolve each used comment: `POST <BASE_URL>/api/org/<ORG_ID>/boards/<BOARD_ID>/nodes/<nodeId>/comments/<commentId>/resolve` (get comment IDs first via GET on the same path without the last two segments).
26
+
27
+
28
+ ---
29
+
30
+ ## Step 2 — Create the migration
31
+
32
+ File: `supabase/migrations/V{N}__{tablename}.sql`
33
+
34
+ Choose the next available version number by checking existing migration files.
35
+
36
+ ```sql
37
+ CREATE TABLE IF NOT EXISTS "public"."{tablename}"
38
+ (
39
+ id TEXT PRIMARY KEY,
40
+ -- other columns from read model fields in slice.json
41
+ -- use snake_case for all column names
42
+ created_at TIMESTAMP DEFAULT NOW()
43
+ );
44
+ ```
45
+
46
+ **Column type guide:**
47
+
48
+ | Field type | SQL type |
49
+ |-----------|---------|
50
+ | string / UUID | `TEXT` |
51
+ | number (integer) | `INTEGER` |
52
+ | number (float) | `NUMERIC` |
53
+ | boolean | `BOOLEAN` |
54
+ | date | `TIMESTAMP` |
55
+ | nullable number | `INTEGER` (allow NULL) |
56
+
57
+ The PRIMARY KEY column is the one used in `.onConflict(...)` in the projection.
58
+
59
+ ---
60
+
61
+ ## Step 3 — Create `{SliceName}Projection.ts`
62
+
63
+ File: `src/slices/{context}/{SliceName}/{SliceName}Projection.ts`
64
+
65
+ ### Full structure
66
+
67
+ ```typescript
68
+ import {postgreSQLRawSQLProjection} from '@event-driven-io/emmett-postgresql';
69
+ import {sql, SQL} from '@event-driven-io/dumbo';
70
+ import knex, {Knex} from 'knex';
71
+ import {type {EventA}, type {EventB}} from '../{Context}Events';
72
+
73
+ export const tableName = '{tablename}';
74
+
75
+ // TypeScript shape of one row in the read model
76
+ export type {SliceName}ReadModel = {
77
+ id: string;
78
+ // ... fields from slice.json readModel
79
+ };
80
+
81
+ export const getKnexInstance = (connectionString: string): Knex =>
82
+ knex({client: 'pg', connection: connectionString, pool: {min: 0, max: 1}});
83
+
84
+ type {SliceName}Events = {EventA} | {EventB};
85
+
86
+ export const {SliceName}Projection = postgreSQLRawSQLProjection<{SliceName}Events>({
87
+ name: '{SliceName}Projection',
88
+ canHandle: ['{EventA}', '{EventB}'],
89
+ evolve: async (event, context): Promise<SQL[]> => {
90
+ const db = getKnexInstance(context.connection.connectionString);
91
+
92
+ try {
93
+ switch (event.type) {
94
+ case '{EventA}':
95
+ // Insert with upsert — use for create/update events
96
+ return [sql(db(tableName)
97
+ .withSchema('public')
98
+ .insert({
99
+ id: event.data.id,
100
+ field1: event.data.field1,
101
+ field2: event.data.field2,
102
+ })
103
+ .onConflict('id')
104
+ .merge(['field1', 'field2'])
105
+ .toQuery())];
106
+
107
+ case '{EventB}':
108
+ // Delete — use for cancellation/removal events
109
+ return [sql(db(tableName)
110
+ .withSchema('public')
111
+ .where({id: event.data.id})
112
+ .delete()
113
+ .toQuery())];
114
+
115
+ default:
116
+ return [];
117
+ }
118
+ } finally {
119
+ await db.destroy();
120
+ }
121
+ },
122
+ });
123
+ ```
124
+
125
+ ### SQL operation patterns
126
+
127
+ **Insert with upsert (create or update):**
128
+ ```typescript
129
+ return [sql(db(tableName)
130
+ .withSchema('public')
131
+ .insert({ id: event.data.id, field: event.data.field })
132
+ .onConflict('id')
133
+ .merge(['field']) // list only columns to update on conflict
134
+ .toQuery())];
135
+ ```
136
+
137
+ **Update only (record already exists):**
138
+ ```typescript
139
+ return [sql(db(tableName)
140
+ .withSchema('public')
141
+ .where({id: event.data.id})
142
+ .update({field: event.data.field})
143
+ .toQuery())];
144
+ ```
145
+
146
+ **Delete:**
147
+ ```typescript
148
+ return [sql(db(tableName)
149
+ .withSchema('public')
150
+ .where({id: event.data.id})
151
+ .delete()
152
+ .toQuery())];
153
+ ```
154
+
155
+ **Async DB lookup before update** (when you need to read current state first):
156
+ ```typescript
157
+ const row = await db(tableName)
158
+ .withSchema('public')
159
+ .where({id: event.data.id})
160
+ .select('field')
161
+ .first();
162
+
163
+ if (!row) return [];
164
+
165
+ const newValue = row.field + delta;
166
+ return [sql(db(tableName)
167
+ .withSchema('public')
168
+ .where({id: event.data.id})
169
+ .update({field: newValue})
170
+ .toQuery())];
171
+ ```
172
+
173
+ Always wrap in `try/finally` and call `db.destroy()` in the `finally` block.
174
+
175
+ ---
176
+
177
+ ## Step 4 — Register the projection in the event store
178
+
179
+ File: `src/common/loadPostgresEventstore.ts`
180
+
181
+ Add the new projection to the `projections.inline([...])` array:
182
+
183
+ ```typescript
184
+ import {{SliceName}Projection} from '../slices/{context}/{SliceName}/{SliceName}Projection';
185
+
186
+ // inside getPostgreSQLEventStore options:
187
+ projections: projections.inline([
188
+ // ... existing projections ...
189
+ {SliceName}Projection,
190
+ ]),
191
+ ```
192
+
193
+ ---
194
+
195
+ ## Step 5 — Create `{SliceName}.test.ts`
196
+
197
+ File: `src/slices/{context}/{SliceName}/{SliceName}.test.ts`
198
+
199
+ Uses `PostgreSQLProjectionSpec` with a real PostgreSQL container (Testcontainers). Flyway runs actual migrations so the schema matches production exactly.
200
+
201
+ ```typescript
202
+ import {before, after, describe, it} from 'node:test';
203
+ import {PostgreSQLProjectionAssert, PostgreSQLProjectionSpec} from '@event-driven-io/emmett-postgresql';
204
+ import {{SliceName}Projection} from './{SliceName}Projection';
205
+ import {PostgreSqlContainer, StartedPostgreSqlContainer} from '@testcontainers/postgresql';
206
+ import knex, {Knex} from 'knex';
207
+ import assert from 'assert';
208
+ import {runFlywayMigrations} from '../../../common/testHelpers';
209
+
210
+ const TEST_ID = 'test-id-001';
211
+
212
+ describe('{SliceName} Specification', () => {
213
+ let postgres: StartedPostgreSqlContainer;
214
+ let connectionString: string;
215
+ let db: Knex;
216
+ let given: PostgreSQLProjectionSpec<any>;
217
+
218
+ before(async () => {
219
+ postgres = await new PostgreSqlContainer('postgres').start();
220
+ connectionString = postgres.getConnectionUri();
221
+
222
+ db = knex({client: 'pg', connection: connectionString});
223
+
224
+ await runFlywayMigrations(connectionString);
225
+
226
+ // Insert any prerequisite rows required by foreign keys:
227
+ // await db('parent_table').withSchema('public').insert({...});
228
+
229
+ given = PostgreSQLProjectionSpec.for({
230
+ projection: {SliceName}Projection,
231
+ connectionString,
232
+ });
233
+ });
234
+
235
+ after(async () => {
236
+ await db?.destroy();
237
+ await postgres?.stop();
238
+ });
239
+
240
+ it('spec: {SliceName} - inserts row on {EventA}', async () => {
241
+ const assertReadModel: PostgreSQLProjectionAssert = async ({connectionString: connStr}) => {
242
+ const queryDb = knex({client: 'pg', connection: connStr});
243
+ try {
244
+ const result = await queryDb('{tablename}')
245
+ .withSchema('public')
246
+ .where({id: TEST_ID})
247
+ .first();
248
+
249
+ assert.ok(result, 'row should exist');
250
+ assert.strictEqual(result.id, TEST_ID);
251
+ assert.strictEqual(result.field1, 'expected-value');
252
+ } finally {
253
+ await queryDb.destroy();
254
+ }
255
+ };
256
+
257
+ await given([{
258
+ type: '{EventA}',
259
+ data: {id: TEST_ID, field1: 'expected-value'},
260
+ metadata: {stream_name: `{context}-${TEST_ID}`},
261
+ }])
262
+ .when([])
263
+ .then(assertReadModel);
264
+ });
265
+
266
+ it('spec: {SliceName} - removes row on {EventB}', async () => {
267
+ const assertReadModel: PostgreSQLProjectionAssert = async ({connectionString: connStr}) => {
268
+ const queryDb = knex({client: 'pg', connection: connStr});
269
+ try {
270
+ const result = await queryDb('{tablename}')
271
+ .withSchema('public')
272
+ .where({id: TEST_ID})
273
+ .first();
274
+
275
+ assert.strictEqual(result, undefined, 'row should be deleted');
276
+ } finally {
277
+ await queryDb.destroy();
278
+ }
279
+ };
280
+
281
+ await given([
282
+ {
283
+ type: '{EventA}',
284
+ data: {id: TEST_ID, field1: 'value'},
285
+ metadata: {stream_name: `{context}-${TEST_ID}`},
286
+ },
287
+ {
288
+ type: '{EventB}',
289
+ data: {id: TEST_ID},
290
+ metadata: {stream_name: `{context}-${TEST_ID}`},
291
+ },
292
+ ])
293
+ .when([])
294
+ .then(assertReadModel);
295
+ });
296
+ });
297
+ ```
298
+
299
+ Write one `it` block per specification in the slice.json. Use `given([events]).when([]).then(assertReadModel)`.
300
+
301
+ ---
302
+
303
+ ## Step 6 — Create `routes.ts`
304
+
305
+ File: `src/slices/{context}/{SliceName}/routes.ts`
306
+
307
+ > **Concrete example**: `src/slices/example/routes.ts` — shows the full pattern with `requireUser`, `assertNotEmpty`, error mapping, and OpenAPI annotations. Read it before implementing.
308
+
309
+ ```typescript
310
+ import {Request, Response, Router} from 'express';
311
+ import {WebApiSetup} from '@event-driven-io/emmett-expressjs';
312
+ import {requireUser} from '../../../supabase/requireUser';
313
+ import {{SliceName}ReadModel, tableName} from './{SliceName}Projection';
314
+ import {readmodel} from '../../../core/readmodel';
315
+ import createClient from '../../../supabase/api';
316
+
317
+ export const api = (): WebApiSetup => (router: Router): void => {
318
+
319
+ router.get('/api/query/{slicename}-collection', async (req: Request, res: Response) => {
320
+ try {
321
+ const principal = await requireUser(req, res, true);
322
+ if (principal.error) return;
323
+
324
+ const id = req.query._id?.toString();
325
+ const supabase = createClient();
326
+
327
+ const data: {SliceName}ReadModel | {SliceName}ReadModel[] | null =
328
+ id
329
+ ? await readmodel(tableName, supabase).findById<{SliceName}ReadModel>('id', id)
330
+ : await readmodel(tableName, supabase).findAll<{SliceName}ReadModel>({});
331
+
332
+ const sanitized = JSON.parse(
333
+ JSON.stringify(data ?? [], (_, value) =>
334
+ typeof value === 'bigint' ? value.toString() : value,
335
+ ),
336
+ );
337
+
338
+ return res.status(200).json(sanitized);
339
+ } catch (err) {
340
+ console.error(err);
341
+ return res.status(500).json({ok: false, error: 'Server error'});
342
+ }
343
+ });
344
+ };
345
+ ```
346
+
347
+ ---
348
+
349
+ ## Step 7 — Wire up the route
350
+
351
+ Find the application's router registration (usually `src/index.ts` or `src/app.ts`) and add:
352
+
353
+ ```typescript
354
+ import {api as {SliceName}Api} from './slices/{context}/{SliceName}/routes';
355
+
356
+ {SliceName}Api()(router);
357
+ ```
358
+
359
+ ---
360
+
361
+ ## Files to create / modify
362
+
363
+ ```
364
+ src/slices/{context}/{SliceName}/
365
+ ├── {SliceName}Projection.ts ← projection logic
366
+ ├── {SliceName}.test.ts ← PostgreSQLProjectionSpec tests
367
+ └── routes.ts ← GET query endpoint
368
+
369
+ supabase/migrations/
370
+ └── V{N}__{tablename}.sql ← table DDL
371
+
372
+ src/common/
373
+ └── loadPostgresEventstore.ts ← add projection to inline([...]) list
374
+ ```
375
+
376
+ ---
377
+
378
+ ## Checklist
379
+
380
+ - [ ] Migration file created with correct version number and all columns
381
+ - [ ] `tableName` constant matches the migration table name exactly
382
+ - [ ] Projection registered in `loadPostgresEventstore.ts`
383
+ - [ ] `canHandle` lists every event type the projection reacts to
384
+ - [ ] `finally { await db.destroy() }` present in every `evolve` handler
385
+ - [ ] Tests use `runFlywayMigrations()` to apply the real schema
386
+ - [ ] One test scenario per specification in slice.json
387
+ - [ ] Every field in the read model definition in slice.json has a column in the migration and a field in the TypeScript type — no invented columns
388
+ - [ ] Every event type in `events[]` is listed in the projection's `canHandle` — no assumed events
389
+ - [ ] No extra columns or fields were added beyond what slice.json defines
390
+ - [ ] No field names were assumed or guessed — if a field is not in slice.json, it is not in the code
@@ -0,0 +1,303 @@
1
+ ---
2
+ name: build-webhook
3
+ description: Implements a webhook slice as a self-contained Supabase Edge Function using the Emmett framework — exposes an HTTP endpoint, fires a command, emits events
4
+ ---
5
+
6
+ # Build Webhook Slice (Supabase Edge Function)
7
+
8
+ > Before doing anything else, read the slice definition from `.build-kit/.slices/{Context}/{slicename}/slice.json`. This file is the **source of truth** for all commands, events, and endpoint behaviour.
9
+
10
+ ---
11
+
12
+
13
+ ## What a Webhook Slice is
14
+
15
+ A webhook slice exposes an HTTP endpoint (Supabase Edge Function) that accepts an external request (e.g. payment provider callback), translates it into an Emmett command, and appends events to the PostgreSQL event store.
16
+
17
+ Architecture:
18
+
19
+ ```
20
+ HTTP Request (external webhook / API call)
21
+
22
+
23
+ supabase/functions/{name}/index.ts ← edge function — parse, build command, handle
24
+
25
+
26
+ CommandHandler (Emmett) ← evolve state, decide events
27
+
28
+
29
+ PostgreSQL Event Store ← new events appended
30
+ ```
31
+
32
+ The edge function is **self-contained** — it does not import from the Node.js backend. Emmett imports use Deno's `npm:` specifier; the runtime scaffold uses `jsr:@supabase/server`.
33
+
34
+ ---
35
+
36
+ ## Step 1 — Read the slice.json
37
+
38
+ Extract:
39
+ - **sliceName** — becomes the Command name and function folder name (PascalCase / kebab-case)
40
+ - **context** — bounded context
41
+ - **commands[]** — command data fields (build the request body shape from these)
42
+ - **events[]** — events emitted when the command succeeds
43
+ - **endpoint** (if present) — preferred URL path; default to kebab-case slice name
44
+ > **Comments & description**: Each element carries a `comments: string[]` array and a `description` field. The slice also has `comments: string[]`. Use as implementation hints. When done, resolve each used comment: `POST <BASE_URL>/api/org/<ORG_ID>/boards/<BOARD_ID>/nodes/<nodeId>/comments/<commentId>/resolve` (get IDs first via GET on same path).
45
+
46
+ ---
47
+
48
+ ## Step 2 — Ensure the shared events union exists
49
+
50
+ Check whether `src/slices/{context}/{Context}Events.ts` already has the event types. If not, add them using the same pattern as `build-state-change`:
51
+
52
+ ```typescript
53
+ // src/slices/{context}/{Context}Events.ts
54
+ import type {Event} from '@event-driven-io/emmett';
55
+
56
+ type CommonMeta = {
57
+ stream_name?: string;
58
+ userId?: string;
59
+ correlation_id?: string;
60
+ causation_id?: string;
61
+ };
62
+
63
+ export type {EventName} = Event<'{EventName}', {
64
+ id: string;
65
+ // fields from slice.json events[]
66
+ }, CommonMeta>;
67
+
68
+ export type {Context}Events = /* existing */ | {EventName};
69
+ ```
70
+
71
+ This keeps backend event types in sync even though the edge function is self-contained.
72
+
73
+ ---
74
+
75
+ ## Step 3 — Create the Supabase Edge Function
76
+
77
+ File: `supabase/functions/{function-name}/index.ts`
78
+
79
+ Function name is the slice name in kebab-case (e.g. `activate-license`).
80
+
81
+ ```typescript
82
+ // Setup type definitions for built-in Supabase Runtime APIs
83
+ import "jsr:@supabase/functions-js/edge-runtime.d.ts";
84
+ import {withSupabase} from "jsr:@supabase/server@^1";
85
+ import {CommandHandler, type Command} from 'npm:@event-driven-io/emmett';
86
+ import {getPostgresEventStore} from 'npm:@event-driven-io/emmett-postgresql';
87
+
88
+ // ── Types ─────────────────────────────────────────────────────────────────────
89
+
90
+ type CommonMeta = {
91
+ correlation_id?: string;
92
+ causation_id?: string;
93
+ userId?: string;
94
+ };
95
+
96
+ // Event type(s) — mirror src/slices/{context}/{Context}Events.ts
97
+ type {EventName} = {
98
+ type: '{EventName}';
99
+ data: {
100
+ id: string;
101
+ // fields from slice.json events[]
102
+ };
103
+ metadata: CommonMeta;
104
+ };
105
+
106
+ type {Context}Events = {EventName}; // extend union if multiple events
107
+
108
+ // Command type — fields from slice.json commands[]
109
+ type {SliceName}Command = Command<'{SliceName}', {
110
+ id: string;
111
+ // other fields from slice.json commands[].fields
112
+ }, CommonMeta>;
113
+
114
+ // Request body shape — derived from commands[].fields
115
+ interface {SliceName}Payload {
116
+ id?: string;
117
+ // other fields from slice.json commands[].fields
118
+ }
119
+
120
+ // ── State (idempotency) ───────────────────────────────────────────────────────
121
+
122
+ type {SliceName}State = {
123
+ processed: boolean;
124
+ };
125
+
126
+ const initialState = (): {SliceName}State => ({processed: false});
127
+
128
+ const evolve = (state: {SliceName}State, event: {Context}Events): {SliceName}State => {
129
+ switch (event.type) {
130
+ case '{EventName}':
131
+ return {...state, processed: true};
132
+ default:
133
+ return state;
134
+ }
135
+ };
136
+
137
+ const decide = (
138
+ command: {SliceName}Command,
139
+ state: {SliceName}State,
140
+ ): {Context}Events[] => {
141
+ if (state.processed) {
142
+ throw {code: 'already_processed', message: 'Already processed'};
143
+ }
144
+
145
+ return [{
146
+ type: '{EventName}',
147
+ data: {
148
+ id: command.data.id,
149
+ // map all fields from command.data per slice.json
150
+ },
151
+ metadata: {
152
+ correlation_id: command.metadata?.correlation_id,
153
+ causation_id: command.metadata?.causation_id,
154
+ },
155
+ }];
156
+ };
157
+
158
+ // ── Handler ───────────────────────────────────────────────────────────────────
159
+
160
+ const {SliceName}Handler = CommandHandler<{SliceName}State, {Context}Events>({
161
+ evolve,
162
+ initialState,
163
+ });
164
+
165
+ async function handle{SliceName}(id: string, command: {SliceName}Command) {
166
+ const connectionString = Deno.env.get('SUPABASE_DB_URL');
167
+ if (!connectionString) throw new Error('SUPABASE_DB_URL not set');
168
+
169
+ const eventStore = getPostgresEventStore(connectionString);
170
+ const result = await {SliceName}Handler(
171
+ eventStore,
172
+ id,
173
+ (state: {SliceName}State) => decide(command, state),
174
+ );
175
+ return {
176
+ nextExpectedStreamVersion: result.nextExpectedStreamVersion?.toString(),
177
+ lastEventGlobalPosition: result.lastEventGlobalPosition?.toString(),
178
+ };
179
+ }
180
+
181
+ // ── HTTP Handler ──────────────────────────────────────────────────────────────
182
+
183
+ console.info('{function-name} started');
184
+
185
+ export default {
186
+ fetch: withSupabase({auth: ['publishable', 'secret']}, async (req, _ctx) => {
187
+ try {
188
+ const body: {SliceName}Payload = await req.json();
189
+ const id = body.id ?? crypto.randomUUID();
190
+
191
+ const command: {SliceName}Command = {
192
+ type: '{SliceName}',
193
+ data: {
194
+ id,
195
+ // map fields from body per slice.json commands[].fields
196
+ },
197
+ metadata: {
198
+ correlation_id: req.headers.get('x-correlation-id') ?? id,
199
+ causation_id: id,
200
+ },
201
+ };
202
+
203
+ const result = await handle{SliceName}(id, command);
204
+
205
+ return Response.json({ok: true, id, ...result}, {status: 201});
206
+ } catch (err: unknown) {
207
+ const error = err as {code?: string; message?: string};
208
+ if (error.code) {
209
+ return Response.json({error: error.message}, {status: 409});
210
+ }
211
+ console.error('[{function-name}] Unhandled error:', err);
212
+ return Response.json({error: 'Internal server error'}, {status: 500});
213
+ }
214
+ }),
215
+ };
216
+ ```
217
+
218
+ ### Key decisions when filling in the template
219
+
220
+ **Function name** — use kebab-case of the slice name as the directory (e.g. `activate-license`). This becomes the function URL: `/functions/v1/activate-license`.
221
+
222
+ **`withSupabase` auth modes** — `"publishable"` allows calls with the anon key; `"secret"` allows calls with the service role key. Use `ctx.authMode` to branch on privileged vs. unprivileged behaviour if needed. For payment webhooks where the provider cannot supply a Supabase key, you may need to verify a provider-specific signature header instead (e.g. Stripe's `x-stripe-signature`).
223
+
224
+ **`id` field** — always use the aggregate identifier as both the stream key and response `id`. Generate with `crypto.randomUUID()` if not provided in the request body.
225
+
226
+ **Idempotency** — `state.processed` guards against replaying the same command. Match the guard to the emitted event: the `evolve` case for `{EventName}` sets `processed: true`.
227
+
228
+ **Error codes** — throw `{code: 'snake_case_code', message: '...'}` from `decide`. The handler maps those to `409 Conflict`.
229
+
230
+ **`SUPABASE_DB_URL`** — PostgreSQL connection string. Available automatically as a built-in secret in Supabase Edge Functions — no manual configuration needed.
231
+
232
+ **CORS** — `withSupabase` handles preflight automatically. No manual `OPTIONS` handler needed.
233
+
234
+ ---
235
+
236
+ ## Step 4 — Verify event store schema migration
237
+
238
+ The Emmett schema must be migrated before the edge function can write events. This happens once in the **backend startup**. Confirm that `src/common/loadPostgresEventstore.ts` calls `schema.migrate()`:
239
+
240
+ ```typescript
241
+ await eventStoreInstance.schema.migrate(); // must be present
242
+ ```
243
+
244
+ If the project has no Node.js backend yet, run the migration manually via a one-off script before deploying the edge function.
245
+
246
+ ---
247
+
248
+ ## Step 5 — Register the function in supabase config
249
+
250
+ Add to `supabase/config.toml`:
251
+
252
+ ```toml
253
+ [functions.{function-name}]
254
+ verify_jwt = false # false for public webhooks; true for internal API calls
255
+ ```
256
+
257
+ Payment providers (Stripe, Paddle, etc.) cannot supply a Supabase JWT, so `verify_jwt = false` is the correct choice for webhook endpoints. Verify the provider's own signature header in the handler body instead.
258
+
259
+ ---
260
+
261
+ ## Step 6 — Test the edge function locally
262
+
263
+ ```bash
264
+ supabase functions serve {function-name} --env-file .env.local
265
+ ```
266
+
267
+ Then call it:
268
+
269
+ ```bash
270
+ curl -i -X POST http://localhost:54321/functions/v1/{function-name} \
271
+ -H "Content-Type: application/json" \
272
+ -d '{ "id": "test-123", ... }'
273
+ ```
274
+
275
+ ---
276
+
277
+ ## Checklist
278
+
279
+ - [ ] `supabase/functions/{function-name}/index.ts` created
280
+ - [ ] All `{SliceName}`, `{EventName}`, `{Context}`, `{function-name}` placeholders replaced
281
+ - [ ] Fields in `command.data` match `commands[].fields` from slice.json exactly
282
+ - [ ] Fields in emitted event `data` match `events[].fields` from slice.json exactly
283
+ - [ ] Idempotency guard in `decide` (throws `already_processed` on duplicate)
284
+ - [ ] Error `code` used in `throw` so the 409 mapping works
285
+ - [ ] Event type added to `src/slices/{context}/{Context}Events.ts`
286
+ - [ ] `supabase/config.toml` entry added with correct `verify_jwt` setting
287
+ - [ ] `schema.migrate()` confirmed in backend startup (or run manually)
288
+ - [ ] Local test with `supabase functions serve` passes
289
+
290
+ ---
291
+
292
+ ## Files to create / modify
293
+
294
+ ```
295
+ supabase/functions/{function-name}/
296
+ └── index.ts ← self-contained Deno edge function
297
+
298
+ src/slices/{context}/
299
+ └── {Context}Events.ts ← add new event type and update union
300
+
301
+ supabase/
302
+ └── config.toml ← register the new function
303
+ ```