@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,64 @@
1
+ {
2
+ "project": "MyApp",
3
+ "branchName": "ralph/task-priority",
4
+ "description": "Task Priority System - Add priority levels to tasks",
5
+ "userStories": [
6
+ {
7
+ "id": "US-001",
8
+ "title": "Add priority field to database",
9
+ "description": "As a developer, I need to store task priority so it persists across sessions.",
10
+ "acceptanceCriteria": [
11
+ "Add priority column to tasks table: 'high' | 'medium' | 'low' (default 'medium')",
12
+ "Generate and run migration successfully",
13
+ "Typecheck passes"
14
+ ],
15
+ "priority": 1,
16
+ "passes": false,
17
+ "notes": ""
18
+ },
19
+ {
20
+ "id": "US-002",
21
+ "title": "Display priority indicator on task cards",
22
+ "description": "As a user, I want to see task priority at a glance.",
23
+ "acceptanceCriteria": [
24
+ "Each task card shows colored priority badge (red=high, yellow=medium, gray=low)",
25
+ "Priority visible without hovering or clicking",
26
+ "Typecheck passes",
27
+ "Verify in browser using dev-browser skill"
28
+ ],
29
+ "priority": 2,
30
+ "passes": false,
31
+ "notes": ""
32
+ },
33
+ {
34
+ "id": "US-003",
35
+ "title": "Add priority selector to task edit",
36
+ "description": "As a user, I want to change a task's priority when editing it.",
37
+ "acceptanceCriteria": [
38
+ "Priority dropdown in task edit modal",
39
+ "Shows current priority as selected",
40
+ "Saves immediately on selection change",
41
+ "Typecheck passes",
42
+ "Verify in browser using dev-browser skill"
43
+ ],
44
+ "priority": 3,
45
+ "passes": false,
46
+ "notes": ""
47
+ },
48
+ {
49
+ "id": "US-004",
50
+ "title": "Filter tasks by priority",
51
+ "description": "As a user, I want to filter the task list to see only high-priority items.",
52
+ "acceptanceCriteria": [
53
+ "Filter dropdown with options: All | High | Medium | Low",
54
+ "Filter persists in URL params",
55
+ "Empty state message when no tasks match filter",
56
+ "Typecheck passes",
57
+ "Verify in browser using dev-browser skill"
58
+ ],
59
+ "priority": 4,
60
+ "passes": false,
61
+ "notes": ""
62
+ }
63
+ ]
64
+ }
@@ -0,0 +1,4 @@
1
+ <svg fill="none" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg">
2
+ <path d="M14.5 13.5V5.41a1 1 0 0 0-.3-.7L9.8.29A1 1 0 0 0 9.08 0H1.5v13.5A2.5 2.5 0 0 0 4 16h8a2.5 2.5 0 0 0 2.5-2.5m-1.5 0v-7H8v-5H3v12a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1M9.5 5V2.12L12.38 5zM5.13 5h-.62v1.25h2.12V5zm-.62 3h7.12v1.25H4.5zm.62 3h-.62v1.25h7.12V11z"
3
+ clip-rule="evenodd" fill="#666" fill-rule="evenodd"/>
4
+ </svg>
@@ -0,0 +1,12 @@
1
+ <svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16">
2
+ <g clip-path="url(#a)">
3
+ <path fill-rule="evenodd" clip-rule="evenodd"
4
+ d="M10.27 14.1a6.5 6.5 0 0 0 3.67-3.45q-1.24.21-2.7.34-.31 1.83-.97 3.1M8 16A8 8 0 1 0 8 0a8 8 0 0 0 0 16m.48-1.52a7 7 0 0 1-.96 0H7.5a4 4 0 0 1-.84-1.32q-.38-.89-.63-2.08a40 40 0 0 0 3.92 0q-.25 1.2-.63 2.08a4 4 0 0 1-.84 1.31zm2.94-4.76q1.66-.15 2.95-.43a7 7 0 0 0 0-2.58q-1.3-.27-2.95-.43a18 18 0 0 1 0 3.44m-1.27-3.54a17 17 0 0 1 0 3.64 39 39 0 0 1-4.3 0 17 17 0 0 1 0-3.64 39 39 0 0 1 4.3 0m1.1-1.17q1.45.13 2.69.34a6.5 6.5 0 0 0-3.67-3.44q.65 1.26.98 3.1M8.48 1.5l.01.02q.41.37.84 1.31.38.89.63 2.08a40 40 0 0 0-3.92 0q.25-1.2.63-2.08a4 4 0 0 1 .85-1.32 7 7 0 0 1 .96 0m-2.75.4a6.5 6.5 0 0 0-3.67 3.44 29 29 0 0 1 2.7-.34q.31-1.83.97-3.1M4.58 6.28q-1.66.16-2.95.43a7 7 0 0 0 0 2.58q1.3.27 2.95.43a18 18 0 0 1 0-3.44m.17 4.71q-1.45-.12-2.69-.34a6.5 6.5 0 0 0 3.67 3.44q-.65-1.27-.98-3.1"
5
+ fill="#666"/>
6
+ </g>
7
+ <defs>
8
+ <clipPath id="a">
9
+ <path fill="#fff" d="M0 0h16v16H0z"/>
10
+ </clipPath>
11
+ </defs>
12
+ </svg>
@@ -0,0 +1,6 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 394 80">
2
+ <path fill="#000"
3
+ d="M262 0h68.5v12.7h-27.2v66.6h-13.6V12.7H262V0ZM149 0v12.7H94v20.4h44.3v12.6H94v21h55v12.6H80.5V0h68.7zm34.3 0h-17.8l63.8 79.4h17.9l-32-39.7 32-39.6h-17.9l-23 28.6-23-28.6zm18.3 56.7-9-11-27.1 33.7h17.8l18.3-22.7z"/>
4
+ <path fill="#000"
5
+ d="M81 79.3 17 0H0v79.3h13.6V17l50.2 62.3H81Zm252.6-.4c-1 0-1.8-.4-2.5-1s-1.1-1.6-1.1-2.6.3-1.8 1-2.5 1.6-1 2.6-1 1.8.3 2.5 1a3.4 3.4 0 0 1 .6 4.3 3.7 3.7 0 0 1-3 1.8zm23.2-33.5h6v23.3c0 2.1-.4 4-1.3 5.5a9.1 9.1 0 0 1-3.8 3.5c-1.6.8-3.5 1.3-5.7 1.3-2 0-3.7-.4-5.3-1s-2.8-1.8-3.7-3.2c-.9-1.3-1.4-3-1.4-5h6c.1.8.3 1.6.7 2.2s1 1.2 1.6 1.5c.7.4 1.5.5 2.4.5 1 0 1.8-.2 2.4-.6a4 4 0 0 0 1.6-1.8c.3-.8.5-1.8.5-3V45.5zm30.9 9.1a4.4 4.4 0 0 0-2-3.3 7.5 7.5 0 0 0-4.3-1.1c-1.3 0-2.4.2-3.3.5-.9.4-1.6 1-2 1.6a3.5 3.5 0 0 0-.3 4c.3.5.7.9 1.3 1.2l1.8 1 2 .5 3.2.8c1.3.3 2.5.7 3.7 1.2a13 13 0 0 1 3.2 1.8 8.1 8.1 0 0 1 3 6.5c0 2-.5 3.7-1.5 5.1a10 10 0 0 1-4.4 3.5c-1.8.8-4.1 1.2-6.8 1.2-2.6 0-4.9-.4-6.8-1.2-2-.8-3.4-2-4.5-3.5a10 10 0 0 1-1.7-5.6h6a5 5 0 0 0 3.5 4.6c1 .4 2.2.6 3.4.6 1.3 0 2.5-.2 3.5-.6 1-.4 1.8-1 2.4-1.7a4 4 0 0 0 .8-2.4c0-.9-.2-1.6-.7-2.2a11 11 0 0 0-2.1-1.4l-3.2-1-3.8-1c-2.8-.7-5-1.7-6.6-3.2a7.2 7.2 0 0 1-2.4-5.7 8 8 0 0 1 1.7-5 10 10 0 0 1 4.3-3.5c2-.8 4-1.2 6.4-1.2 2.3 0 4.4.4 6.2 1.2 1.8.8 3.2 2 4.3 3.4 1 1.4 1.5 3 1.5 5h-5.8z"/>
6
+ </svg>
@@ -0,0 +1,3 @@
1
+ <svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1155 1000">
2
+ <path d="m577.3 0 577.4 1000H0z" fill="#fff"/>
3
+ </svg>
@@ -0,0 +1,5 @@
1
+ <svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16">
2
+ <path fill-rule="evenodd" clip-rule="evenodd"
3
+ d="M1.5 2.5h13v10a1 1 0 0 1-1 1h-11a1 1 0 0 1-1-1zM0 1h16v11.5a2.5 2.5 0 0 1-2.5 2.5h-11A2.5 2.5 0 0 1 0 12.5zm3.75 4.5a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5M7 4.75a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0m1.75.75a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5"
4
+ fill="#666"/>
5
+ </svg>
@@ -0,0 +1,129 @@
1
+ import {join} from 'path';
2
+ import {getApplication, startAPI, WebApiSetup} from '@event-driven-io/emmett-expressjs';
3
+ import {glob} from "glob";
4
+ import {replayProjection} from "./src/common/replay";
5
+ import express, {Application, Request, Response} from 'express';
6
+ import {requireUser} from "./src/supabase/requireUser";
7
+ import swaggerUi from 'swagger-ui-express'
8
+ import {specs} from './src/swagger';
9
+ import cors from 'cors';
10
+ import {api as replayApi} from "./src/common/routes"
11
+
12
+ async function startServer() {
13
+ const routesPattern = join(__dirname, 'src/slices/**/routes{,-*}.@(ts|js)');
14
+ const routeFiles = await glob(routesPattern, {nodir: true});
15
+ console.log('Found route files:', routeFiles);
16
+
17
+ const processorPattern = join(__dirname, 'src/slices/**/processor{,-*}.@(ts|js)');
18
+ const processorFiles = await glob(processorPattern, {nodir: true});
19
+ console.log('Found processor files:', processorFiles);
20
+
21
+ const rootApp: Application = express();
22
+
23
+ // Configure CORS to allow requests from localhost:8080 and localhost:8081
24
+ rootApp.use(cors({
25
+ origin: ['http://localhost:8080', 'http://localhost:8081', '*'],
26
+ credentials: true,
27
+ methods: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'OPTIONS'],
28
+ allowedHeaders: ['Content-Type', 'Authorization','x-user-id']
29
+ }));
30
+
31
+ const webApis: WebApiSetup[] = [];
32
+
33
+ for (const file of routeFiles) {
34
+ const webApiModule: { api: () => WebApiSetup } = await import(file);
35
+ if (typeof webApiModule.api == 'function') {
36
+ var module = webApiModule.api()
37
+ webApis.push(module);
38
+ } else {
39
+ console.error(`Expected api function to be defined in ${file}`);
40
+ }
41
+ }
42
+
43
+ webApis.push(replayApi)
44
+
45
+ for (const processorFile of processorFiles) {
46
+ const processor: { processor: { start: () => {} } } = await import(processorFile);
47
+ if (typeof processor.processor.start == "function") {
48
+ console.log(`starting processor ${processorFile}`)
49
+ processor.processor.start()
50
+ }
51
+ }
52
+
53
+ // Get the main application from emmett
54
+ const childApp: Application = getApplication({
55
+ apis: webApis,
56
+ disableJsonMiddleware: false,
57
+ enableDefaultExpressEtag: true,
58
+ });
59
+
60
+ // Add your custom routes to the main application (BEFORE the catch-all)
61
+ childApp.post("/internal/replay/:slice/:projectionName", async (req: Request, resp: Response) => {
62
+ const {slice, projectionName} = req.params
63
+ await replayProjection(slice, projectionName);
64
+ return resp.status(200).json({status: 'ok'});
65
+ });
66
+
67
+ // Protected user info endpoint - requires JWT token in Authorization header
68
+ childApp.get('/api/user', async (req: Request, res: Response) => {
69
+ console.log('API user route hit'); // Debug log
70
+ try {
71
+ const result = await requireUser(req, res, false)
72
+ if (result.error) {
73
+ // Response already sent by requireUser if sendUnauthorized=true
74
+ if (!res.headersSent) {
75
+ res.status(401).json({error: result.error})
76
+ }
77
+ } else {
78
+ res.status(200).json({
79
+ userId: result.user.id,
80
+ email: result.user.email,
81
+ metadata: result.user.user_metadata
82
+ })
83
+ }
84
+ } catch (error) {
85
+ console.error('Error in /api/user:', error);
86
+ if (!res.headersSent) {
87
+ res.status(500).json({error: 'Internal server error'});
88
+ }
89
+ }
90
+ });
91
+
92
+ // Swagger UI endpoints
93
+ childApp.use('/api-docs', swaggerUi.serve);
94
+ childApp.get('/api-docs', swaggerUi.setup(specs, {
95
+ swaggerOptions: {
96
+ urls: [
97
+ {
98
+ url: '/swagger.json',
99
+ name: 'JSON',
100
+ },
101
+ ],
102
+ },
103
+ }));
104
+
105
+ // OpenAPI spec endpoint
106
+ childApp.get('/swagger.json', (req: Request, res: Response) => {
107
+ res.setHeader('Content-Type', 'application/json');
108
+ res.send(specs);
109
+ });
110
+
111
+ const port = parseInt(process.env.PORT || '3000', 10);
112
+ console.log(`> Ready on port ${port}`);
113
+
114
+ rootApp.use(childApp)
115
+ // Start the main application
116
+ startAPI(rootApp, {port: port});
117
+
118
+ process.on('unhandledRejection', (reason, promise) => {
119
+ console.error('⛔ Unhandled Rejection:', reason);
120
+ if (reason instanceof Error && reason.stack) {
121
+ console.error('Stack trace:\n', reason.stack);
122
+ }
123
+ });
124
+ }
125
+
126
+ startServer().catch(error => {
127
+ console.error('Failed to start server:', error);
128
+ process.exit(1);
129
+ });
@@ -0,0 +1,50 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ prompt() {
5
+ local var_name="$1"
6
+ local prompt_text="$2"
7
+ local value
8
+ read -rp "$prompt_text: " value
9
+ if [[ -z "$value" ]]; then
10
+ echo "Error: $var_name cannot be empty." >&2
11
+ exit 1
12
+ fi
13
+ echo "$value"
14
+ }
15
+
16
+ prompt_secret() {
17
+ local var_name="$1"
18
+ local prompt_text="$2"
19
+ local value
20
+ read -rsp "$prompt_text: " value
21
+ echo "" >&2
22
+ if [[ -z "$value" ]]; then
23
+ echo "Error: $var_name cannot be empty." >&2
24
+ exit 1
25
+ fi
26
+ echo "$value"
27
+ }
28
+
29
+ echo "=== Supabase .env setup ==="
30
+ echo ""
31
+
32
+ PROJECT_ID=$(prompt SUPABASE_PROJECT_ID "Supabase Project ID")
33
+ DB_PASSWORD=$(prompt_secret SUPABASE_DB_PASSWORD "Database Password")
34
+ PUBLISHABLE_KEY=$(prompt_secret SUPABASE_PUBLISHABLE_KEY "Supabase Publishable Key")
35
+ SECRET_KEY=$(prompt_secret SUPABASE_SECRET_KEY "Supabase Secret Key")
36
+
37
+ cat > .env <<EOF
38
+ SUPABASE_URL=https://${PROJECT_ID}.supabase.co
39
+ SUPABASE_PUBLISHABLE_KEY=${PUBLISHABLE_KEY}
40
+ SUPABASE_DB_URL=postgresql://postgres.${PROJECT_ID}:${DB_PASSWORD}@aws-1-eu-central-1.pooler.supabase.com:5432/postgres?prepareThreshold=0
41
+ SUPABASE_SECRET_KEY=${SECRET_KEY}
42
+
43
+ # Flyway configuration
44
+ FLYWAY_URL=jdbc:postgresql://aws-1-eu-west-1.pooler.supabase.com:6543/postgres?user=postgres.${PROJECT_ID}&password=${DB_PASSWORD}
45
+ FLYWAY_USER=postgres.${PROJECT_ID}
46
+ FLYWAY_PASSWORD=${DB_PASSWORD}
47
+ EOF
48
+
49
+ echo ""
50
+ echo ".env created successfully."
@@ -0,0 +1,6 @@
1
+ export function assertNotEmpty<T>(value: T): NonNullable<T> {
2
+ if (value === null || value === undefined) {
3
+ throw new Error("Expected non-empty value");
4
+ }
5
+ return value!!;
6
+ }
@@ -0,0 +1 @@
1
+ export const postgresUrl = process.env.SUPABASE_DB_URL ?? "missing-url"
@@ -0,0 +1,16 @@
1
+ import {getPostgreSQLEventStore} from "@event-driven-io/emmett-postgresql";
2
+ import {projections} from "@event-driven-io/emmett";
3
+ import {postgresUrl} from "./db";
4
+
5
+ export const findEventstore = async () => {
6
+
7
+ return getPostgreSQLEventStore(postgresUrl, {
8
+ schema: {
9
+ autoMigration: "CreateOrUpdate"
10
+ },
11
+ projections: projections.inline([
12
+ ]),
13
+
14
+ });
15
+
16
+ }
@@ -0,0 +1,51 @@
1
+ /*
2
+ * Copyright (c) 2025 Nebulit GmbH
3
+ * Licensed under the MIT License.
4
+ */
5
+
6
+ const serviceURI = "http://localhost:3000"
7
+
8
+ export function parseEndpoint(endpoint: string, data?: any) {
9
+ var parsedEndpoint = endpoint?.startsWith("/") ? endpoint.substring(1) : endpoint
10
+ return serviceURI + "/" + lowercaseFirstCharacter(parsedEndpoint).replace(/{(\w+)}/g, (match, param) => {
11
+ return param && data && data[param] !== undefined ? data[param] : match;
12
+ })
13
+ }
14
+
15
+
16
+ export function parseQueryEndpoint(
17
+ endpoint: string,
18
+ queries?: Record<string, string>
19
+ ) {
20
+ const parsedEndpoint = endpoint.startsWith("/")
21
+ ? endpoint.substring(1)
22
+ : endpoint;
23
+
24
+ const basePath =
25
+ serviceURI + "/api/query/" + parsedEndpoint;
26
+
27
+ const queryString = queries
28
+ ? "?" + new URLSearchParams(filterEmptyEntries(queries)).toString()
29
+ : "";
30
+
31
+ return basePath + queryString;
32
+ }
33
+
34
+ function filterEmptyEntries(queries?: Record<string, string>): Record<string, string> {
35
+ if (!queries) return {};
36
+ return Object.fromEntries(
37
+ Object.entries(queries).filter(([key, value]) => value !== "")
38
+ );
39
+ }
40
+
41
+
42
+ function lowercaseFirstCharacter(inputString: string) {
43
+ // Check if the string is not empty
44
+ if (inputString?.length > 0) {
45
+ // Capitalize the first character and concatenate the rest of the string
46
+ return inputString.charAt(0).toLowerCase() + inputString.substring(1);
47
+ } else {
48
+ // Return an empty string if the input is empty
49
+ return "";
50
+ }
51
+ }
@@ -0,0 +1,9 @@
1
+ import {PostgreSQLProjectionDefinition, rebuildPostgreSQLProjections} from "@event-driven-io/emmett-postgresql";
2
+ import {postgresUrl} from "./db";
3
+
4
+ export const replayProjection = async (slice: string, projectionName: string): Promise<void> => {
5
+ const projectionImport = await import((`../slices/${slice}/${projectionName}.ts`))
6
+ const projection: PostgreSQLProjectionDefinition = projectionImport[projectionName]
7
+
8
+ return rebuildPostgreSQLProjections({projection: projection, connectionString: postgresUrl}).start();
9
+ }
@@ -0,0 +1,19 @@
1
+ import {Request, Response, Router} from 'express';
2
+ import {WebApiSetup} from "@event-driven-io/emmett-expressjs";
3
+ import {assertNotEmpty} from "../util/assertions";
4
+ import {replayProjection} from "./replay";
5
+
6
+
7
+ export const api =
8
+ (
9
+ // external dependencies
10
+ ): WebApiSetup =>
11
+ (router: Router): void => {
12
+
13
+ router.post('/api/replay/:slice/:projection', async (req: Request, res: Response) => {
14
+ const slice= assertNotEmpty(req.params.slice)
15
+ const projection = assertNotEmpty(req.params.projection)
16
+ replayProjection(slice, projection)
17
+ });
18
+ };
19
+
@@ -0,0 +1,53 @@
1
+ import {execSync} from 'child_process';
2
+ import {writeFileSync, unlinkSync} from 'fs';
3
+ import {tmpdir} from 'os';
4
+ import {join} from 'path';
5
+
6
+ /**
7
+ * Runs Flyway migrations against a test database
8
+ * @param connectionString PostgreSQL connection string
9
+ */
10
+ export async function runFlywayMigrations(connectionString: string): Promise<void> {
11
+ // Parse connection string to extract components
12
+ const url = new URL(connectionString);
13
+ const jdbcUrl = `jdbc:postgresql://${url.hostname}:${url.port || 5432}${url.pathname}`;
14
+ const user = url.username;
15
+ const password = url.password;
16
+
17
+ // Create temporary Flyway config file
18
+ const tempConfigPath = join(tmpdir(), `flyway-test-${Date.now()}.conf`);
19
+ const migrationsPath = join(process.cwd(), 'supabase', 'migrations');
20
+
21
+ const config = `
22
+ flyway.url=${jdbcUrl}
23
+ flyway.user=${user}
24
+ flyway.password=${password}
25
+ flyway.locations=filesystem:${migrationsPath}
26
+ flyway.schemas=public
27
+ flyway.placeholderReplacement=false
28
+ flyway.validateOnMigrate=true
29
+ flyway.cleanDisabled=false
30
+ `;
31
+
32
+ try {
33
+ writeFileSync(tempConfigPath, config, 'utf8');
34
+
35
+ // Run Flyway migrate
36
+ execSync(`flyway -configFiles=${tempConfigPath} migrate`, {
37
+ stdio: 'pipe',
38
+ encoding: 'utf8'
39
+ });
40
+ } catch (error: any) {
41
+ console.error('Flyway migration failed:', error.message);
42
+ if (error.stdout) console.error('STDOUT:', error.stdout);
43
+ if (error.stderr) console.error('STDERR:', error.stderr);
44
+ throw new Error(`Flyway migration failed: ${error.message}`);
45
+ } finally {
46
+ // Cleanup temp config file
47
+ try {
48
+ unlinkSync(tempConfigPath);
49
+ } catch {
50
+ // Ignore cleanup errors
51
+ }
52
+ }
53
+ }
@@ -0,0 +1,28 @@
1
+ import {SupabaseClient} from "@supabase/supabase-js";
2
+ import {PostgrestFilterBuilder} from "@supabase/postgrest-js";
3
+
4
+
5
+ export const readmodel = (collection: string, supabase: SupabaseClient) => ({
6
+ findAll: async <T>(query?: Record<string, any>,
7
+ queryWrapper: (query: PostgrestFilterBuilder<any, any, any, any, any>) => PostgrestFilterBuilder<any, any, any, any, any> = (query) => query): Promise<T[]> => {
8
+ var query1 = supabase.from(collection).select("*");
9
+ Object.entries(query??{}).forEach((it,value) => {
10
+ query1 = query1.eq(it[0], it[1])
11
+ })
12
+ let qb = queryWrapper(query1);
13
+ const response = await qb;
14
+ if (response.error) throw response.error;
15
+ return response.data as T[]
16
+ },
17
+
18
+ findById: async <T>(idcolumn: string, id: string): Promise<T | null> => {
19
+ const response = await supabase
20
+ .from(collection)
21
+ .select("*")
22
+ .eq(idcolumn, id)
23
+ .maybeSingle();
24
+ if (response.error) throw response.error;
25
+ return response as T | null;
26
+ },
27
+
28
+ });
@@ -0,0 +1,26 @@
1
+ /*
2
+ * Copyright (c) 2025 Nebulit GmbH
3
+ * Licensed under the MIT License.
4
+ */
5
+
6
+ import React from 'react';
7
+
8
+ export type CommandConfig = {
9
+ command: string
10
+ endpoint: string,
11
+ schema: any // json schema
12
+ }
13
+
14
+ export type ReadModelConfig = {
15
+ readModel: string
16
+ endpoint: string,
17
+ readModelView: React.FC<any>,
18
+ }
19
+
20
+ export type ViewSelection = {
21
+ "slice": string,
22
+ "viewType": string,
23
+ "commandView": React.FC<any>,
24
+ "viewName": string
25
+ }
26
+
@@ -0,0 +1,53 @@
1
+ import cron from "node-cron";
2
+ import {parseQueryEndpoint} from "../common/parseEndpoint";
3
+
4
+ export type ProcessorTodoItem = {
5
+ id: string,
6
+ status: string
7
+ createdAt: Date
8
+ }
9
+
10
+ export type ProcessorConfig = {
11
+ schedule: string,
12
+ endpoint: string,
13
+ query?: Record<string, string>,
14
+ }
15
+
16
+ export const startProcessor = <T>(config: ProcessorConfig, handler: (item: T & ProcessorTodoItem) => void) => {
17
+ cron.schedule(config.schedule, async () => {
18
+ const data = await fetchData(parseQueryEndpoint(config.endpoint, config.query ? config.query : {
19
+ "status": "OPEN",
20
+ "_limit": "1"
21
+ }))
22
+ if (Array.isArray(data) && data?.length > 0) {
23
+ handler(data[0]);
24
+ } else {
25
+ //console.log("No item to process")
26
+ }
27
+ })
28
+ }
29
+
30
+
31
+ async function fetchData(endpoint: string) {
32
+ try {
33
+ const response = await fetch(endpoint, {
34
+ method: 'GET',
35
+ headers: {
36
+ 'Content-Type': 'application/json'
37
+ }
38
+ });
39
+
40
+ if (!response.ok) {
41
+ return Promise.reject(`HTTP error! status: ${response.status}`);
42
+ }
43
+
44
+ return await response.json(); // This will be a list of objects
45
+ } catch (error) {
46
+ console.error('Error fetching data:', error);
47
+ return Promise.reject(`Error fetching data ${error}`);
48
+ }
49
+ }
50
+
51
+
52
+
53
+
@@ -0,0 +1,36 @@
1
+ import {type EmailOtpType} from '@supabase/supabase-js'
2
+ import {Request, Response} from "express";
3
+
4
+ import createClient from './api'
5
+
6
+ function stringOrFirstString(item: string | string[] | undefined) {
7
+ return Array.isArray(item) ? item[0] : item
8
+ }
9
+
10
+ export default async function LoginHandler(req: Request, res: Response) {
11
+ if (req.method !== 'GET') {
12
+ res.status(405).appendHeader('Allow', 'GET').end()
13
+ return
14
+ }
15
+
16
+ const queryParams = req.query
17
+ const token_hash = stringOrFirstString(queryParams["token_hash"]?.toString())
18
+ const type = stringOrFirstString(queryParams["type"]?.toString())
19
+
20
+ let next = '/error'
21
+
22
+ if (token_hash && type) {
23
+ const supabase = createClient()
24
+ const {error} = await supabase.auth.verifyOtp({
25
+ type: type as EmailOtpType,
26
+ token_hash,
27
+ })
28
+ if (error) {
29
+ console.error(error)
30
+ } else {
31
+ next = stringOrFirstString(queryParams["next"]?.toString()) || '/'
32
+ }
33
+ }
34
+
35
+ res.redirect(next)
36
+ }
@@ -0,0 +1,21 @@
1
+ import {type Request, type Response} from "express";
2
+ import {createClient} from "./serverProps";
3
+
4
+ export async function getAuthenticatedUser(req: Request, res: Response) {
5
+ const supabase = createClient(req, res)
6
+ const {data, error} = await supabase.auth.getUser()
7
+ if (error || !data) {
8
+ return null
9
+ }
10
+ return data.user
11
+ }
12
+
13
+ export async function requireAuthMiddleware(req: Request, res: Response, next: () => void) {
14
+ const user = await getAuthenticatedUser(req, res)
15
+ if (!user) {
16
+ res.redirect('/auth/login')
17
+ return
18
+ }
19
+ (req as any).user = user
20
+ next()
21
+ }