@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,412 @@
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
+ // Knex here is used purely as a SQL builder/executor — it is never given its
82
+ // own connection string or pool. Building a query with `.toQuery()` never
83
+ // opens a connection at all. Any query that needs to actually *execute*
84
+ // inside evolve() (see "Async DB lookup" below) must be run via
85
+ // `.connection(context.connection.client)`, which pins it to the same raw pg
86
+ // client Emmett already holds open for this event's transaction — instead of
87
+ // opening a brand-new Postgres connection per event. That matters for two
88
+ // reasons: it avoids extra connection churn against the pool (a real
89
+ // contributor to connection exhaustion under load), and it means the query
90
+ // commits or rolls back atomically with the event append — a lookup or write
91
+ // done on a separate connection would not see the current transaction's
92
+ // uncommitted state and would not be undone if the append later fails
93
+ // (e.g. an optimistic-concurrency conflict).
94
+ export const getKnexInstance = (): Knex => knex({client: 'pg'});
95
+
96
+ type {SliceName}Events = {EventA} | {EventB};
97
+
98
+ export const {SliceName}Projection = postgreSQLRawSQLProjection<{SliceName}Events>({
99
+ name: '{SliceName}Projection',
100
+ canHandle: ['{EventA}', '{EventB}'],
101
+ evolve: async (event, context): Promise<SQL[]> => {
102
+ const db = getKnexInstance();
103
+
104
+ switch (event.type) {
105
+ case '{EventA}':
106
+ // Insert with upsert — use for create/update events
107
+ return [sql(db(tableName)
108
+ .withSchema('public')
109
+ .insert({
110
+ id: event.data.id,
111
+ field1: event.data.field1,
112
+ field2: event.data.field2,
113
+ })
114
+ .onConflict('id')
115
+ .merge(['field1', 'field2'])
116
+ .toQuery())];
117
+
118
+ case '{EventB}':
119
+ // Delete — use for cancellation/removal events
120
+ return [sql(db(tableName)
121
+ .withSchema('public')
122
+ .where({id: event.data.id})
123
+ .delete()
124
+ .toQuery())];
125
+
126
+ default:
127
+ return [];
128
+ }
129
+ },
130
+ });
131
+ ```
132
+
133
+ Note: nothing here calls `db.destroy()` — this Knex instance never opens its own
134
+ connection or pool, so there is nothing to tear down. `evolve()`'s returned SQL
135
+ is executed later by Emmett itself (`context.execute.batchCommand(...)`), on
136
+ its own transaction — that part is already atomic with the event append
137
+ without any extra work.
138
+
139
+ ### SQL operation patterns
140
+
141
+ **Insert with upsert (create or update):**
142
+ ```typescript
143
+ return [sql(db(tableName)
144
+ .withSchema('public')
145
+ .insert({ id: event.data.id, field: event.data.field })
146
+ .onConflict('id')
147
+ .merge(['field']) // list only columns to update on conflict
148
+ .toQuery())];
149
+ ```
150
+
151
+ **Update only (record already exists):**
152
+ ```typescript
153
+ return [sql(db(tableName)
154
+ .withSchema('public')
155
+ .where({id: event.data.id})
156
+ .update({field: event.data.field})
157
+ .toQuery())];
158
+ ```
159
+
160
+ **Delete:**
161
+ ```typescript
162
+ return [sql(db(tableName)
163
+ .withSchema('public')
164
+ .where({id: event.data.id})
165
+ .delete()
166
+ .toQuery())];
167
+ ```
168
+
169
+ **Async DB lookup before update** (when you need to read current state first):
170
+ ```typescript
171
+ // This query actually executes (unlike the .toQuery()-only patterns above),
172
+ // so it MUST be pinned to Emmett's own client via .connection(...) — otherwise
173
+ // it opens a brand-new, untransacted Postgres connection just to do a read.
174
+ const row = await db(tableName)
175
+ .withSchema('public')
176
+ .where({id: event.data.id})
177
+ .select('field')
178
+ .connection(context.connection.client)
179
+ .first();
180
+
181
+ if (!row) return [];
182
+
183
+ const newValue = row.field + delta;
184
+ return [sql(db(tableName)
185
+ .withSchema('public')
186
+ .where({id: event.data.id})
187
+ .update({field: newValue})
188
+ .toQuery())];
189
+ ```
190
+
191
+ Do not call `db.destroy()` anywhere in `evolve()` — this Knex instance never owns
192
+ a connection or pool (see Step 3), so there's nothing to destroy, and destroying
193
+ it would tear down `context.connection.client` before Emmett is done with it.
194
+
195
+ ---
196
+
197
+ ## Step 4 — Register the projection in the event store
198
+
199
+ File: `src/common/loadPostgresEventstore.ts`
200
+
201
+ Add the new projection to the `projections.inline([...])` array:
202
+
203
+ ```typescript
204
+ import {{SliceName}Projection} from '../slices/{context}/{SliceName}/{SliceName}Projection';
205
+
206
+ // inside getPostgreSQLEventStore options:
207
+ projections: projections.inline([
208
+ // ... existing projections ...
209
+ {SliceName}Projection,
210
+ ]),
211
+ ```
212
+
213
+ ---
214
+
215
+ ## Step 5 — Create `{SliceName}.test.ts`
216
+
217
+ File: `src/slices/{context}/{SliceName}/{SliceName}.test.ts`
218
+
219
+ Uses `PostgreSQLProjectionSpec` with a real PostgreSQL container (Testcontainers). Flyway runs actual migrations so the schema matches production exactly.
220
+
221
+ ```typescript
222
+ import {before, after, describe, it} from 'node:test';
223
+ import {PostgreSQLProjectionAssert, PostgreSQLProjectionSpec} from '@event-driven-io/emmett-postgresql';
224
+ import {{SliceName}Projection} from './{SliceName}Projection';
225
+ import {PostgreSqlContainer, StartedPostgreSqlContainer} from '@testcontainers/postgresql';
226
+ import knex, {Knex} from 'knex';
227
+ import assert from 'assert';
228
+ import {runFlywayMigrations} from '../../../common/testHelpers';
229
+
230
+ const TEST_ID = 'test-id-001';
231
+
232
+ describe('{SliceName} Specification', () => {
233
+ let postgres: StartedPostgreSqlContainer;
234
+ let connectionString: string;
235
+ let db: Knex;
236
+ let given: PostgreSQLProjectionSpec<any>;
237
+
238
+ before(async () => {
239
+ postgres = await new PostgreSqlContainer('postgres').start();
240
+ connectionString = postgres.getConnectionUri();
241
+
242
+ db = knex({client: 'pg', connection: connectionString});
243
+
244
+ await runFlywayMigrations(connectionString);
245
+
246
+ // Insert any prerequisite rows required by foreign keys:
247
+ // await db('parent_table').withSchema('public').insert({...});
248
+
249
+ given = PostgreSQLProjectionSpec.for({
250
+ projection: {SliceName}Projection,
251
+ connectionString,
252
+ });
253
+ });
254
+
255
+ after(async () => {
256
+ await db?.destroy();
257
+ await postgres?.stop();
258
+ });
259
+
260
+ it('spec: {SliceName} - inserts row on {EventA}', async () => {
261
+ const assertReadModel: PostgreSQLProjectionAssert = async ({connectionString: connStr}) => {
262
+ const queryDb = knex({client: 'pg', connection: connStr});
263
+ try {
264
+ const result = await queryDb('{tablename}')
265
+ .withSchema('public')
266
+ .where({id: TEST_ID})
267
+ .first();
268
+
269
+ assert.ok(result, 'row should exist');
270
+ assert.strictEqual(result.id, TEST_ID);
271
+ assert.strictEqual(result.field1, 'expected-value');
272
+ } finally {
273
+ await queryDb.destroy();
274
+ }
275
+ };
276
+
277
+ await given([{
278
+ type: '{EventA}',
279
+ data: {id: TEST_ID, field1: 'expected-value'},
280
+ metadata: {stream_name: `{context}-${TEST_ID}`},
281
+ }])
282
+ .when([])
283
+ .then(assertReadModel);
284
+ });
285
+
286
+ it('spec: {SliceName} - removes row on {EventB}', async () => {
287
+ const assertReadModel: PostgreSQLProjectionAssert = async ({connectionString: connStr}) => {
288
+ const queryDb = knex({client: 'pg', connection: connStr});
289
+ try {
290
+ const result = await queryDb('{tablename}')
291
+ .withSchema('public')
292
+ .where({id: TEST_ID})
293
+ .first();
294
+
295
+ assert.strictEqual(result, undefined, 'row should be deleted');
296
+ } finally {
297
+ await queryDb.destroy();
298
+ }
299
+ };
300
+
301
+ await given([
302
+ {
303
+ type: '{EventA}',
304
+ data: {id: TEST_ID, field1: 'value'},
305
+ metadata: {stream_name: `{context}-${TEST_ID}`},
306
+ },
307
+ {
308
+ type: '{EventB}',
309
+ data: {id: TEST_ID},
310
+ metadata: {stream_name: `{context}-${TEST_ID}`},
311
+ },
312
+ ])
313
+ .when([])
314
+ .then(assertReadModel);
315
+ });
316
+ });
317
+ ```
318
+
319
+ Write one `it` block per specification in the slice.json. Use `given([events]).when([]).then(assertReadModel)`.
320
+
321
+ ---
322
+
323
+ ## Step 6 — Create `routes.ts`
324
+
325
+ File: `src/slices/{context}/{SliceName}/routes.ts`
326
+
327
+ > **Concrete example**: `src/slices/example/routes.ts` — shows the full pattern with `requireUser`, `assertNotEmpty`, error mapping, and OpenAPI annotations. Read it before implementing.
328
+
329
+ ```typescript
330
+ import {Request, Response, Router} from 'express';
331
+ import {WebApiSetup} from '@event-driven-io/emmett-expressjs';
332
+ import {requireUser} from '../../../supabase/requireUser';
333
+ import {{SliceName}ReadModel, tableName} from './{SliceName}Projection';
334
+ import {readmodel} from '../../../core/readmodel';
335
+ import createClient from '../../../supabase/api';
336
+
337
+ export const api = (): WebApiSetup => (router: Router): void => {
338
+
339
+ router.get('/api/query/{slicename}-collection', async (req: Request, res: Response) => {
340
+ try {
341
+ const principal = await requireUser(req, res, true);
342
+ if (principal.error) return;
343
+
344
+ const id = req.query._id?.toString();
345
+ const supabase = createClient();
346
+
347
+ const data: {SliceName}ReadModel | {SliceName}ReadModel[] | null =
348
+ id
349
+ ? await readmodel(tableName, supabase).findById<{SliceName}ReadModel>('id', id)
350
+ : await readmodel(tableName, supabase).findAll<{SliceName}ReadModel>({});
351
+
352
+ const sanitized = JSON.parse(
353
+ JSON.stringify(data ?? [], (_, value) =>
354
+ typeof value === 'bigint' ? value.toString() : value,
355
+ ),
356
+ );
357
+
358
+ return res.status(200).json(sanitized);
359
+ } catch (err) {
360
+ console.error(err);
361
+ return res.status(500).json({ok: false, error: 'Server error'});
362
+ }
363
+ });
364
+ };
365
+ ```
366
+
367
+ ---
368
+
369
+ ## Step 7 — Wire up the route
370
+
371
+ Find the application's router registration (usually `src/index.ts` or `src/app.ts`) and add:
372
+
373
+ ```typescript
374
+ import {api as {SliceName}Api} from './slices/{context}/{SliceName}/routes';
375
+
376
+ {SliceName}Api()(router);
377
+ ```
378
+
379
+ ---
380
+
381
+ ## Files to create / modify
382
+
383
+ ```
384
+ src/slices/{context}/{SliceName}/
385
+ ├── {SliceName}Projection.ts ← projection logic
386
+ ├── {SliceName}.test.ts ← PostgreSQLProjectionSpec tests
387
+ └── routes.ts ← GET query endpoint
388
+
389
+ supabase/migrations/
390
+ └── V{N}__{tablename}.sql ← table DDL
391
+
392
+ src/common/
393
+ └── loadPostgresEventstore.ts ← add projection to inline([...]) list
394
+ ```
395
+
396
+ ---
397
+
398
+ ## Checklist
399
+
400
+ - [ ] Migration file created with correct version number and all columns
401
+ - [ ] `tableName` constant matches the migration table name exactly
402
+ - [ ] Projection registered in `loadPostgresEventstore.ts`
403
+ - [ ] `canHandle` lists every event type the projection reacts to
404
+ - [ ] `getKnexInstance()` takes no connection string/pool — it's a pure SQL builder
405
+ - [ ] Any query in `evolve()` that actually executes (not just `.toQuery()`) is run via `.connection(context.connection.client)`
406
+ - [ ] No `db.destroy()` calls in `evolve()` — the projection never owns its own connection
407
+ - [ ] Tests use `runFlywayMigrations()` to apply the real schema
408
+ - [ ] One test scenario per specification in slice.json
409
+ o- [ ] 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
410
+ - [ ] Every event type in `events[]` is listed in the projection's `canHandle` — no assumed events
411
+ - [ ] No extra columns or fields were added beyond what slice.json defines
412
+ - [ ] No field names were assumed or guessed — if a field is not in slice.json, it is not in the code
@@ -0,0 +1,178 @@
1
+ ---
2
+ name: connect
3
+ description: Resolve eventmodelers connection config (token, boardId, baseUrl) from inline params or .eventmodelers/config.json — ask the user for missing values, persist them, and add the file to .gitignore. All other skills invoke this first.
4
+ ---
5
+
6
+ # Connect — Resolve Eventmodelers Config
7
+
8
+ **Every other skill invokes this skill first** before making any API calls. Do not proceed past this skill until all four values (`TOKEN`, `BOARD_ID`, `ORG_ID`, `BASE_URL`) are resolved.
9
+
10
+ ---
11
+
12
+ ## What this skill produces
13
+
14
+ After running, the following variables are available for the rest of the session:
15
+
16
+ | Variable | Header sent to API | Description |
17
+ |----------|--------------------|-------------|
18
+ | `TOKEN` | `x-token` | API token UUID |
19
+ | `BOARD_ID` | `x-board-id` | Target board UUID |
20
+ | `ORG_ID` | — | Organization UUID (used in all board-scoped URLs) |
21
+ | `BASE_URL` | — | Base URL, e.g. `http://localhost:3000` |
22
+
23
+ Every API call in every skill must include these headers:
24
+ ```
25
+ x-token: <TOKEN>
26
+ x-board-id: <BOARD_ID>
27
+ x-user-id: <skill-name> ← set by each skill individually
28
+ ```
29
+
30
+ All board-scoped URLs follow the pattern: `<BASE_URL>/api/org/<ORG_ID>/boards/<BOARD_ID>/...`
31
+
32
+ ---
33
+
34
+ ## Step 0 — Check for inline parameters
35
+
36
+ Before reading the config file, scan the prompt/arguments that invoked this skill for inline overrides. Supported formats:
37
+
38
+ | Pattern | Example |
39
+ |---------|---------|
40
+ | `board=<uuid>` | `board=05cda19d-d5b8-4b51-ae88-c72f2611548a` |
41
+ | `token=<uuid>` | `token=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx` |
42
+ | `org=<uuid>` | `org=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx` |
43
+ | `baseUrl=<url>` | `baseUrl=http://localhost:3000` |
44
+
45
+ If an inline `board=<uuid>` is found, use it as `BOARD_ID` — **it takes priority over the config file**. Same for `token`, `org`, and `baseUrl`. Record which values came from inline params so they are not overwritten in Step 3.
46
+
47
+ ---
48
+
49
+ ## Step 1 — Read config file
50
+
51
+ Search for `.eventmodelers/config.json` starting from the current working directory and walking up through all parent directories:
52
+
53
+ ```bash
54
+ dir="$PWD"
55
+ config_file=""
56
+ while [ "$dir" != "/" ]; do
57
+ if [ -f "$dir/.eventmodelers/config.json" ]; then
58
+ config_file="$dir/.eventmodelers/config.json"
59
+ break
60
+ fi
61
+ dir="$(dirname "$dir")"
62
+ done
63
+ [ -n "$config_file" ] && cat "$config_file"
64
+ ```
65
+
66
+ If a file is found (at any level), note its path and extract any values **not already set by Step 0**:
67
+ - `token` → `TOKEN`
68
+ - `boardId` → `BOARD_ID`
69
+ - `organizationId` → `ORG_ID`
70
+ - `baseUrl` → `BASE_URL` (default: `https://api.eventmodelers.ai` if missing)
71
+
72
+ Resolution priority: **inline param > config file > ask user**
73
+
74
+ If all four are present (from any source), skip to **Step 4 — Verify**.
75
+
76
+ ---
77
+
78
+ ## Step 2 — Ask for missing values
79
+
80
+ If after Steps 0 and 1 any required field is still missing, **ask the user one question first**:
81
+
82
+ > "Do you have a config from the eventmodelers accounts page? (yes / no)"
83
+
84
+ **If the user answers yes:**
85
+ Stop asking questions. Show this hint and wait for them to paste:
86
+
87
+ > "Great — please paste your config from https://app.eventmodelers.ai/account here."
88
+
89
+ When they paste a JSON object, parse it immediately — accept both `orgId` and `organizationId` as the organization field — apply all values, and proceed directly to Step 3.
90
+
91
+ **If the user answers no** (or pastes only a partial config), ask for each still-missing field one at a time, in this order: `token`, then `boardId`, then `orgId`. Wait for the answer before asking the next.
92
+
93
+ | Field | What to ask |
94
+ |-------|--------------------------------------------------------------------------------------|
95
+ | `token` | "Please provide your eventmodelers API token (a UUID from your workspace settings)." |
96
+ | `boardId` | "Please provide the board ID you want to work with (the UUID from the board URL)." |
97
+ | `orgId` | "Please provide your organization ID (the UUID from your organization settings)." |
98
+ | `baseUrl` | Do **not** ask — default to `https://api.eventmodelers.ai` silently. |
99
+
100
+ Where to find the token: users generate API tokens in their workspace settings at the eventmodelers platform. The token is shown only once at creation time. It is a UUID and must belong to the same organization as the board.
101
+
102
+ ---
103
+
104
+ ## Step 3 — Persist config
105
+
106
+ Once all values are collected, write the config file. When writing, merge with any existing config — do **not** overwrite fields that were provided as inline params with values from a previous config (the inline param is the user's explicit intent for this session, but the persisted value should reflect the most recently user-supplied value):
107
+
108
+ ```bash
109
+ mkdir -p .eventmodelers
110
+ cat > .eventmodelers/config.json << 'EOF'
111
+ {
112
+ "token": "<TOKEN>",
113
+ "boardId": "<BOARD_ID>",
114
+ "orgId": "<ORG_ID>",
115
+ "baseUrl": "<BASE_URL>"
116
+ }
117
+ EOF
118
+ ```
119
+
120
+ Then ensure `.eventmodelers/config.json` is in `.gitignore`. Check whether it is already present:
121
+
122
+ ```bash
123
+ grep -q ".eventmodelers/config.json" .gitignore 2>/dev/null || echo "MISSING"
124
+ ```
125
+
126
+ If `MISSING`, append it:
127
+
128
+ ```bash
129
+ echo ".eventmodelers/config.json" >> .gitignore
130
+ ```
131
+
132
+ Tell the user: `"Config saved to .eventmodelers/config.json and added to .gitignore."`
133
+
134
+ ---
135
+
136
+ ## Step 4 — Verify
137
+
138
+ Confirm the token and board are valid with a lightweight call:
139
+
140
+ ```bash
141
+ curl -s -o /dev/null -w "%{http_code}" \
142
+ -H "x-token: <TOKEN>" \
143
+ -H "x-board-id: <BOARD_ID>" \
144
+ -H "x-user-id: connect-skill" \
145
+ "<BASE_URL>/api/org/<ORG_ID>/boards/<BOARD_ID>/nodes?type=CHAPTER"
146
+ ```
147
+
148
+ | Response | Action |
149
+ |----------|--------|
150
+ | `200` | Config is valid. Print one line: `"Connected — board <BOARD_ID>"` and return. |
151
+ | `401` | Token is invalid or missing. Tell the user and re-run from Step 2, clearing `token`. |
152
+ | `403` | Token organization does not match board. Tell the user to check that the token was issued for the correct workspace. Re-run from Step 2 for both fields. |
153
+ | `404` | Board not found. Tell the user and re-run from Step 2, clearing `boardId`. |
154
+ | Any other | Print the status code and raw response. Ask the user how to proceed. |
155
+
156
+ ---
157
+
158
+ ## Config file format
159
+
160
+ `.eventmodelers/config.json`:
161
+ ```json
162
+ {
163
+ "token": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
164
+ "boardId": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
165
+ "orgId": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
166
+ "baseUrl": "http://localhost:3000"
167
+ }
168
+ ```
169
+
170
+ The `token` field is a secret. It is never logged or shown after initial confirmation.
171
+
172
+ ---
173
+
174
+ ## Security notes
175
+
176
+ - The config file is workspace-local and gitignored — never commit it.
177
+ - The token grants write access to all boards in its organization — treat it like a password.
178
+ - If a skill receives a `401` or `403` mid-session, re-invoke this skill to refresh the config before retrying.