@nebulit/embuilder 0.1.39
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +254 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +138 -0
- package/package.json +49 -0
- package/templates/.claude/hooks/QUICKSTART.md +256 -0
- package/templates/.claude/hooks/README.md +533 -0
- package/templates/.claude/hooks/analyze-commit.sh +22 -0
- package/templates/.claude/hooks/analyze-commit.ts +518 -0
- package/templates/.claude/hooks/analyzers/README.md +198 -0
- package/templates/.claude/hooks/analyzers/code-quality-checker.ts +154 -0
- package/templates/.claude/hooks/analyzers/code-quality.md +54 -0
- package/templates/.claude/hooks/analyzers/commit-blocker-example.ts.disabled +110 -0
- package/templates/.claude/hooks/analyzers/commit-policy.md +49 -0
- package/templates/.claude/hooks/analyzers/event-model-validator.md +49 -0
- package/templates/.claude/hooks/analyzers/event-model-validator.ts +169 -0
- package/templates/.claude/hooks/analyzers/example-logger.ts +70 -0
- package/templates/.claude/hooks/analyzers/slice-scope-validator.md +81 -0
- package/templates/.claude/hooks/check-review-result.sh +47 -0
- package/templates/.claude/hooks/prepare-review.sh +34 -0
- package/templates/.claude/hooks/review-agent-prompt.md +42 -0
- package/templates/.claude/hooks/run-review-agent.sh +124 -0
- package/templates/.claude/settings.local.json +37 -0
- package/templates/.claude/skills/help/README.md +84 -0
- package/templates/.claude/skills/help/SKILL.md +393 -0
- package/templates/.claude/skills/help/templates/demo-config.json +6753 -0
- package/templates/.claude/skills/sample-slices/SKILL.md +8 -0
- package/templates/.claude/skills/sample-slices/templates/.slices/Library/addbook/code-slice.json +124 -0
- package/templates/.claude/skills/sample-slices/templates/.slices/Library/addbook/slice.json +255 -0
- package/templates/.claude/skills/sample-slices/templates/.slices/Library/availablebooks/slice.json +107 -0
- package/templates/.claude/skills/sample-slices/templates/.slices/index.json +20 -0
- package/templates/.claude/skills/sample-slices/templates/Cart/additem/slice.json +979 -0
- package/templates/.claude/skills/sample-slices/templates/Cart/archiveitem/slice.json +529 -0
- package/templates/.claude/skills/sample-slices/templates/Cart/cartitems/slice.json +1072 -0
- package/templates/.claude/skills/sample-slices/templates/Cart/cartwithproducts/slice.json +394 -0
- package/templates/.claude/skills/sample-slices/templates/Cart/changedprices/slice.json +88 -0
- package/templates/.claude/skills/sample-slices/templates/Cart/changeinventory/slice.json +264 -0
- package/templates/.claude/skills/sample-slices/templates/Cart/changeprice/slice.json +308 -0
- package/templates/.claude/skills/sample-slices/templates/Cart/clearcart/slice.json +358 -0
- package/templates/.claude/skills/sample-slices/templates/Cart/inventories/slice.json +203 -0
- package/templates/.claude/skills/sample-slices/templates/Cart/publishcart/slice.json +876 -0
- package/templates/.claude/skills/sample-slices/templates/Cart/removeitem/slice.json +560 -0
- package/templates/.claude/skills/sample-slices/templates/Cart/submitcart/slice.json +708 -0
- package/templates/.claude/skills/sample-slices/templates/Cart/submittedcartdata/slice.json +399 -0
- package/templates/.claude/skills/sample-slices/templates/index.json +108 -0
- package/templates/.claude/skills/slice-automation/SKILL.md +49 -0
- package/templates/.claude/skills/slice-state-change/SKILL.md +369 -0
- package/templates/.claude/skills/slice-state-change/templates/AddLocation/AddLocation.test.ts.sample +76 -0
- package/templates/.claude/skills/slice-state-change/templates/AddLocation/AddLocationCommand.ts.sample +84 -0
- package/templates/.claude/skills/slice-state-change/templates/AddLocation/routes.ts.sample +73 -0
- package/templates/.claude/skills/slice-state-change/templates/README.md +46 -0
- package/templates/.claude/skills/slice-state-view/SKILL.md +336 -0
- package/templates/.claude/skills/slice-state-view/templates/Locations/Locations.test.ts.sample +84 -0
- package/templates/.claude/skills/slice-state-view/templates/Locations/LocationsProjection.ts.sample +50 -0
- package/templates/.claude/skills/slice-state-view/templates/Locations/routes.ts.sample +46 -0
- package/templates/.claude/skills/slice-state-view/templates/README.md +109 -0
- package/templates/.claude/skills/slice-state-view/templates/Tables/Tables.test.ts.sample +104 -0
- package/templates/.claude/skills/slice-state-view/templates/Tables/TablesProjection.ts.sample +59 -0
- package/templates/.claude/skills/slice-state-view/templates/Tables/routes.ts.sample +46 -0
- package/templates/.claude/skills/slice-state-view/templates/V2__tables.sql +7 -0
- package/templates/.claude/skills/slice-state-view/templates/V8__locations.sql +7 -0
- package/templates/.claude/skills/test-analyzer/SKILL.md +373 -0
- package/templates/.claude/skills/test-analyzer/examples/specification-format.md +143 -0
- package/templates/.claude/skills/test-analyzer/examples/state-change-example.md +111 -0
- package/templates/.claude/skills/test-analyzer/examples/state-view-example.md +122 -0
- package/templates/AGENTS.md +110 -0
- package/templates/Claude.md +58 -0
- package/templates/README.md +178 -0
- package/templates/backend/.env +9 -0
- package/templates/backend/BACKEND_AUTH_SETUP.md +183 -0
- package/templates/backend/SWAGGER.md +213 -0
- package/templates/backend/eslint.config.mjs +31 -0
- package/templates/backend/flyway.conf +17 -0
- package/templates/backend/package.json +44 -0
- package/templates/backend/prd.json.example +64 -0
- package/templates/backend/public/assets/images/banner.png +0 -0
- package/templates/backend/public/assets/logo.png +0 -0
- package/templates/backend/public/file.svg +4 -0
- package/templates/backend/public/globe.svg +12 -0
- package/templates/backend/public/next.svg +6 -0
- package/templates/backend/public/vercel.svg +3 -0
- package/templates/backend/public/window.svg +5 -0
- package/templates/backend/server.ts +129 -0
- package/templates/backend/setup-env.sh +50 -0
- package/templates/backend/src/common/assertions.ts +6 -0
- package/templates/backend/src/common/db.ts +1 -0
- package/templates/backend/src/common/loadPostgresEventstore.ts +16 -0
- package/templates/backend/src/common/parseEndpoint.ts +51 -0
- package/templates/backend/src/common/replay.ts +9 -0
- package/templates/backend/src/common/routes.ts +19 -0
- package/templates/backend/src/common/testHelpers.ts +53 -0
- package/templates/backend/src/core/readmodel.ts +28 -0
- package/templates/backend/src/core/types.ts +26 -0
- package/templates/backend/src/process/process.ts +53 -0
- package/templates/backend/src/supabase/LoginHandler.ts +36 -0
- package/templates/backend/src/supabase/ProtectedPageProps.ts +21 -0
- package/templates/backend/src/supabase/README.md +171 -0
- package/templates/backend/src/supabase/api.ts +63 -0
- package/templates/backend/src/supabase/authMiddleware.ts +53 -0
- package/templates/backend/src/supabase/component.ts +12 -0
- package/templates/backend/src/supabase/requireUser.ts +72 -0
- package/templates/backend/src/supabase/serverProps.ts +25 -0
- package/templates/backend/src/supabase/staticProps.ts +10 -0
- package/templates/backend/src/swagger.ts +34 -0
- package/templates/backend/src/util/assertions.ts +6 -0
- package/templates/backend/supabase/config.toml +295 -0
- package/templates/backend/supabase/migrations/20260121155918593_catalogentries.sql.sample +23 -0
- package/templates/backend/supabase/seed.sql +1 -0
- package/templates/backend/tsconfig.json +31 -0
- package/templates/frontend/.env.development +3 -0
- package/templates/frontend/AGENTS.md +7 -0
- package/templates/frontend/README.md +73 -0
- package/templates/frontend/components.json +20 -0
- package/templates/frontend/eslint.config.js +26 -0
- package/templates/frontend/index.html +18 -0
- package/templates/frontend/package-lock.json +8347 -0
- package/templates/frontend/package.json +94 -0
- package/templates/frontend/postcss.config.js +6 -0
- package/templates/frontend/public/favicon.ico +0 -0
- package/templates/frontend/public/logo.png +0 -0
- package/templates/frontend/public/placeholder.svg +1 -0
- package/templates/frontend/public/robots.txt +14 -0
- package/templates/frontend/src/App.css +42 -0
- package/templates/frontend/src/App.tsx +47 -0
- package/templates/frontend/src/components/NavLink.tsx +28 -0
- package/templates/frontend/src/components/ProtectedRoute.tsx +24 -0
- package/templates/frontend/src/components/calendar/Calendar.tsx +302 -0
- package/templates/frontend/src/components/layout/DashboardLayout.tsx +21 -0
- package/templates/frontend/src/components/layout/Header.tsx +45 -0
- package/templates/frontend/src/components/layout/Sidebar.tsx +82 -0
- package/templates/frontend/src/components/tables/ReservationTemplates.tsx +189 -0
- package/templates/frontend/src/components/ui/accordion.tsx +52 -0
- package/templates/frontend/src/components/ui/alert-dialog.tsx +104 -0
- package/templates/frontend/src/components/ui/alert.tsx +43 -0
- package/templates/frontend/src/components/ui/aspect-ratio.tsx +5 -0
- package/templates/frontend/src/components/ui/avatar.tsx +38 -0
- package/templates/frontend/src/components/ui/badge.tsx +29 -0
- package/templates/frontend/src/components/ui/breadcrumb.tsx +90 -0
- package/templates/frontend/src/components/ui/button.tsx +47 -0
- package/templates/frontend/src/components/ui/calendar.tsx +54 -0
- package/templates/frontend/src/components/ui/card.tsx +43 -0
- package/templates/frontend/src/components/ui/carousel.tsx +224 -0
- package/templates/frontend/src/components/ui/chart.tsx +303 -0
- package/templates/frontend/src/components/ui/checkbox.tsx +26 -0
- package/templates/frontend/src/components/ui/collapsible.tsx +9 -0
- package/templates/frontend/src/components/ui/command.tsx +132 -0
- package/templates/frontend/src/components/ui/context-menu.tsx +178 -0
- package/templates/frontend/src/components/ui/dialog.tsx +95 -0
- package/templates/frontend/src/components/ui/drawer.tsx +87 -0
- package/templates/frontend/src/components/ui/dropdown-menu.tsx +179 -0
- package/templates/frontend/src/components/ui/form.tsx +129 -0
- package/templates/frontend/src/components/ui/hover-card.tsx +27 -0
- package/templates/frontend/src/components/ui/input-otp.tsx +61 -0
- package/templates/frontend/src/components/ui/input.tsx +22 -0
- package/templates/frontend/src/components/ui/label.tsx +17 -0
- package/templates/frontend/src/components/ui/menubar.tsx +207 -0
- package/templates/frontend/src/components/ui/navigation-menu.tsx +120 -0
- package/templates/frontend/src/components/ui/pagination.tsx +81 -0
- package/templates/frontend/src/components/ui/popover.tsx +29 -0
- package/templates/frontend/src/components/ui/progress.tsx +23 -0
- package/templates/frontend/src/components/ui/radio-group.tsx +36 -0
- package/templates/frontend/src/components/ui/resizable.tsx +37 -0
- package/templates/frontend/src/components/ui/scroll-area.tsx +38 -0
- package/templates/frontend/src/components/ui/select.tsx +143 -0
- package/templates/frontend/src/components/ui/separator.tsx +20 -0
- package/templates/frontend/src/components/ui/sheet.tsx +107 -0
- package/templates/frontend/src/components/ui/sidebar.tsx +637 -0
- package/templates/frontend/src/components/ui/skeleton.tsx +7 -0
- package/templates/frontend/src/components/ui/slider.tsx +23 -0
- package/templates/frontend/src/components/ui/sonner.tsx +27 -0
- package/templates/frontend/src/components/ui/stat-card.tsx +44 -0
- package/templates/frontend/src/components/ui/switch.tsx +27 -0
- package/templates/frontend/src/components/ui/table.tsx +72 -0
- package/templates/frontend/src/components/ui/tabs.tsx +53 -0
- package/templates/frontend/src/components/ui/textarea.tsx +21 -0
- package/templates/frontend/src/components/ui/toast.tsx +111 -0
- package/templates/frontend/src/components/ui/toaster.tsx +24 -0
- package/templates/frontend/src/components/ui/toggle-group.tsx +49 -0
- package/templates/frontend/src/components/ui/toggle.tsx +37 -0
- package/templates/frontend/src/components/ui/tooltip.tsx +28 -0
- package/templates/frontend/src/components/ui/use-toast.ts +3 -0
- package/templates/frontend/src/contexts/AuthContext.tsx +94 -0
- package/templates/frontend/src/contexts/RefreshContext.tsx +236 -0
- package/templates/frontend/src/hooks/api/index.ts +2 -0
- package/templates/frontend/src/hooks/api/useLocations.ts +15 -0
- package/templates/frontend/src/hooks/use-mobile.tsx +19 -0
- package/templates/frontend/src/hooks/use-toast.ts +186 -0
- package/templates/frontend/src/hooks/useApiContext.ts +11 -0
- package/templates/frontend/src/index.css +118 -0
- package/templates/frontend/src/integrations/supabase/client.ts +9 -0
- package/templates/frontend/src/lib/api-client.ts +136 -0
- package/templates/frontend/src/lib/api.ts +1028 -0
- package/templates/frontend/src/lib/utils.ts +6 -0
- package/templates/frontend/src/main.tsx +5 -0
- package/templates/frontend/src/pages/Auth.tsx +408 -0
- package/templates/frontend/src/pages/Dashboard.tsx +168 -0
- package/templates/frontend/src/pages/Menus.tsx +224 -0
- package/templates/frontend/src/pages/NotFound.tsx +24 -0
- package/templates/frontend/src/pages/Register.tsx +285 -0
- package/templates/frontend/src/test/example.test.ts +0 -0
- package/templates/frontend/src/test/setup.ts +15 -0
- package/templates/frontend/src/types/index.ts +8 -0
- package/templates/frontend/src/vite-env.d.ts +1 -0
- package/templates/frontend/tailwind.config.ts +101 -0
- package/templates/frontend/tsconfig.app.json +31 -0
- package/templates/frontend/tsconfig.json +16 -0
- package/templates/frontend/tsconfig.node.json +22 -0
- package/templates/frontend/vite.config.ts +21 -0
- package/templates/frontend/vitest.config.ts +16 -0
- package/templates/init.sh +1 -0
- package/templates/prompt.md +139 -0
- package/templates/ralph.sh +120 -0
- package/templates/server.mjs +505 -0
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import {before, after, describe, it} from "node:test";
|
|
2
|
+
import {PostgreSQLProjectionAssert, PostgreSQLProjectionSpec} from "@event-driven-io/emmett-postgresql";
|
|
3
|
+
import {TablesProjection} from "./TablesProjection";
|
|
4
|
+
import {PostgreSqlContainer, StartedPostgreSqlContainer} from "@testcontainers/postgresql";
|
|
5
|
+
import {TableAdded} from "../../events/TableAdded"
|
|
6
|
+
import {TableUpdated} from "../../events/TableUpdated"
|
|
7
|
+
import knex from 'knex';
|
|
8
|
+
import assert from 'assert';
|
|
9
|
+
import {runFlywayMigrations} from "../../common/testHelpers";
|
|
10
|
+
|
|
11
|
+
describe('Tables Specification', () => {
|
|
12
|
+
let postgres: StartedPostgreSqlContainer;
|
|
13
|
+
let connectionString: string
|
|
14
|
+
|
|
15
|
+
let given: PostgreSQLProjectionSpec<TableAdded | TableUpdated>
|
|
16
|
+
|
|
17
|
+
before(async () => {
|
|
18
|
+
postgres = await new PostgreSqlContainer("postgres").start();
|
|
19
|
+
connectionString = postgres.getConnectionUri();
|
|
20
|
+
|
|
21
|
+
await runFlywayMigrations(connectionString)
|
|
22
|
+
|
|
23
|
+
given = PostgreSQLProjectionSpec.for({
|
|
24
|
+
projection: TablesProjection,
|
|
25
|
+
connectionString,
|
|
26
|
+
});
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
after(async () => {
|
|
30
|
+
await postgres?.stop();
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('spec: Tables - add and update scenario', async () => {
|
|
34
|
+
const tableId1 = "fbf165d3-5fe2-4a34-af24-f0ad64ca8412"
|
|
35
|
+
const tableId2 = "3ccaf75c-7120-4e4d-9de9-c5ad822a62ec"
|
|
36
|
+
|
|
37
|
+
const assertTables: PostgreSQLProjectionAssert = async ({connectionString: connStr}) => {
|
|
38
|
+
const queryDb = knex({
|
|
39
|
+
client: 'pg',
|
|
40
|
+
connection: connStr,
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
try {
|
|
44
|
+
const result = await queryDb('tables')
|
|
45
|
+
.withSchema('public')
|
|
46
|
+
.select('table_id', 'name', 'seats')
|
|
47
|
+
.orderBy('table_id');
|
|
48
|
+
|
|
49
|
+
assert.strictEqual(result.length, 2, 'Should have 2 tables');
|
|
50
|
+
|
|
51
|
+
const table1 = result.find(t => t.table_id === tableId1);
|
|
52
|
+
const table2 = result.find(t => t.table_id === tableId2);
|
|
53
|
+
|
|
54
|
+
assert(table1, 'Table 1 should exist');
|
|
55
|
+
assert.strictEqual(table1.name, "Table 1 Updated");
|
|
56
|
+
assert.strictEqual(table1.seats, 8);
|
|
57
|
+
|
|
58
|
+
assert(table2, 'Table 2 should exist');
|
|
59
|
+
assert.strictEqual(table2.name, "Table 2");
|
|
60
|
+
assert.strictEqual(table2.seats, 6);
|
|
61
|
+
} finally {
|
|
62
|
+
await queryDb.destroy();
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
await given([{
|
|
67
|
+
type: 'TableAdded',
|
|
68
|
+
data: {
|
|
69
|
+
minPersons: 2,
|
|
70
|
+
name: "Table 1",
|
|
71
|
+
reservable: true,
|
|
72
|
+
seats: 4,
|
|
73
|
+
tableid: tableId1
|
|
74
|
+
},
|
|
75
|
+
metadata: {streamName: 'fbf165d3-5fe2-4a34-af24-f0ad64ca8412'}
|
|
76
|
+
}])
|
|
77
|
+
.when([
|
|
78
|
+
{
|
|
79
|
+
type: 'TableAdded',
|
|
80
|
+
data: {
|
|
81
|
+
minPersons: 2,
|
|
82
|
+
name: "Table 2",
|
|
83
|
+
reservable: true,
|
|
84
|
+
seats: 6,
|
|
85
|
+
tableid: tableId2
|
|
86
|
+
},
|
|
87
|
+
metadata: {streamName: 'fbf165d3-5fe2-4a34-af24-f0ad64ca8412'}
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
type: 'TableUpdated',
|
|
91
|
+
data: {
|
|
92
|
+
minPersons: 2,
|
|
93
|
+
name: "Table 1 Updated",
|
|
94
|
+
reservable: true,
|
|
95
|
+
seats: 8,
|
|
96
|
+
tableid: tableId1
|
|
97
|
+
},
|
|
98
|
+
metadata: {streamName: 'fbf165d3-5fe2-4a34-af24-f0ad64ca8412'}
|
|
99
|
+
}
|
|
100
|
+
])
|
|
101
|
+
.then(assertTables);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
});
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import {postgreSQLRawSQLProjection} from '@event-driven-io/emmett-postgresql';
|
|
2
|
+
import {sql} from '@event-driven-io/dumbo';
|
|
3
|
+
import knex, {Knex} from 'knex';
|
|
4
|
+
import {ContextEvents} from '../../events/ContextEvents';
|
|
5
|
+
|
|
6
|
+
export type TablesReadModelItem = {
|
|
7
|
+
seats: number,
|
|
8
|
+
name: string,
|
|
9
|
+
tableId: string,
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export type TablesReadModel = {
|
|
13
|
+
data: TablesReadModelItem[],
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export const tableName = 'tables';
|
|
17
|
+
|
|
18
|
+
export const getKnexInstance = (connectionString: string): Knex => {
|
|
19
|
+
return knex({
|
|
20
|
+
client: 'pg',
|
|
21
|
+
connection: connectionString,
|
|
22
|
+
});
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
export const TablesProjection = postgreSQLRawSQLProjection<ContextEvents>({
|
|
27
|
+
canHandle: ['TableAdded', 'TableUpdated'],
|
|
28
|
+
evolve: (event, context) => {
|
|
29
|
+
const {type, data} = event;
|
|
30
|
+
const db = getKnexInstance(context.connection.connectionString);
|
|
31
|
+
|
|
32
|
+
switch (type) {
|
|
33
|
+
case 'TableAdded':
|
|
34
|
+
return sql(db(tableName)
|
|
35
|
+
.withSchema('public')
|
|
36
|
+
.insert({
|
|
37
|
+
table_id: data.tableid,
|
|
38
|
+
name: data.name,
|
|
39
|
+
seats: data.seats,
|
|
40
|
+
})
|
|
41
|
+
.onConflict('table_id')
|
|
42
|
+
.merge({name: data.name, seats: data.seats})
|
|
43
|
+
.toQuery());
|
|
44
|
+
|
|
45
|
+
case 'TableUpdated':
|
|
46
|
+
return sql(db(tableName)
|
|
47
|
+
.withSchema('public')
|
|
48
|
+
.where('table_id', data.tableid)
|
|
49
|
+
.update({
|
|
50
|
+
name: data.name,
|
|
51
|
+
seats: data.seats,
|
|
52
|
+
})
|
|
53
|
+
.toQuery());
|
|
54
|
+
|
|
55
|
+
default:
|
|
56
|
+
return [];
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
});
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import {Request, Response, Router} from 'express';
|
|
2
|
+
import {TablesReadModel, tableName} from "./TablesProjection";
|
|
3
|
+
import {WebApiSetup} from "@event-driven-io/emmett-expressjs";
|
|
4
|
+
import createClient from "../../supabase/api";
|
|
5
|
+
import {readmodel} from "../../core/readmodel";
|
|
6
|
+
import {requireUser} from "../../supabase/requireUser";
|
|
7
|
+
|
|
8
|
+
export const api =
|
|
9
|
+
(
|
|
10
|
+
// external dependencies
|
|
11
|
+
): WebApiSetup =>
|
|
12
|
+
(router: Router): void => {
|
|
13
|
+
router.get('/api/query/tables-collection', async (req: Request, res: Response) => {
|
|
14
|
+
try {
|
|
15
|
+
const principal = await requireUser(req, res, true);
|
|
16
|
+
if (principal.error) {
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const userId = principal.user.id;
|
|
21
|
+
const id = req.query._id?.toString();
|
|
22
|
+
|
|
23
|
+
const supabase = createClient()
|
|
24
|
+
|
|
25
|
+
const query: any = {...req.query, user_id: userId};
|
|
26
|
+
delete query._id;
|
|
27
|
+
|
|
28
|
+
const data: TablesReadModel | TablesReadModel[] | null =
|
|
29
|
+
id ? await readmodel(tableName, supabase).findById<TablesReadModel>("table_id", id) :
|
|
30
|
+
await readmodel(tableName, supabase).findAll<TablesReadModel>(query)
|
|
31
|
+
|
|
32
|
+
// Serialize, handling bigint properly
|
|
33
|
+
const sanitized = JSON.parse(
|
|
34
|
+
JSON.stringify(data || [], (key, value) =>
|
|
35
|
+
typeof value === 'bigint' ? value.toString() : value
|
|
36
|
+
)
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
return res.status(200).json(sanitized);
|
|
40
|
+
} catch (err) {
|
|
41
|
+
console.error(err);
|
|
42
|
+
return res.status(500).json({ok: false, error: 'Server error'});
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
};
|
|
@@ -0,0 +1,373 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: test-analyzer
|
|
3
|
+
description: analyzes test files and generates slice.json specifications for drift detection
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
## Overview
|
|
7
|
+
|
|
8
|
+
Analyzes test files (`*.test.ts`) to generate `code-slice.json` specification files. These capture behavioral contracts in given/when/then form, enabling drift detection between tests and `slice.json` design documentation.
|
|
9
|
+
|
|
10
|
+
**CRITICAL:** This skill NEVER touches existing `slice.json` files. It only creates/deletes `code-slice.json` files.
|
|
11
|
+
|
|
12
|
+
## Purpose
|
|
13
|
+
|
|
14
|
+
- Extract test specifications from existing test code
|
|
15
|
+
- Generate `code-slice.json` files matching the format of `slice.json` in `.slices/` directory
|
|
16
|
+
- Enable drift detection by comparing `code-slice.json` (what tests say) with `slice.json` (design documentation)
|
|
17
|
+
- **NEVER modify or delete existing `slice.json` files** - only work with `code-slice.json`
|
|
18
|
+
|
|
19
|
+
## Input
|
|
20
|
+
|
|
21
|
+
- Path to a test file (e.g., `ActivateShift.test.ts` or `OnlineReservationStatus.test.ts`)
|
|
22
|
+
- Test file must follow given/when/then pattern using:
|
|
23
|
+
- `DeciderSpecification.for()` for state-change slices
|
|
24
|
+
- `PostgreSQLProjectionSpec.for()` for state-view slices
|
|
25
|
+
|
|
26
|
+
## Output Format
|
|
27
|
+
|
|
28
|
+
**File:** `.slices/<Context>/<folder>/code-slice.json` (same directory as `slice.json`)
|
|
29
|
+
|
|
30
|
+
Contains:
|
|
31
|
+
```json
|
|
32
|
+
{
|
|
33
|
+
"id": "<slice-id-from-slice.json>",
|
|
34
|
+
"title": "<slice-title-from-slice.json>",
|
|
35
|
+
"specifications": [...]
|
|
36
|
+
}
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
- MUST include `id` and `title` fields copied from the corresponding `slice.json` file
|
|
40
|
+
- NO commands, events, readmodels, or other slice metadata (only id, title, and specifications)
|
|
41
|
+
- Each specification has complete nested objects with fields, NOT simple strings
|
|
42
|
+
- See [specification-format.md](examples/specification-format.md) for detailed structure
|
|
43
|
+
|
|
44
|
+
### Specifications Array Content
|
|
45
|
+
|
|
46
|
+
Include ALL test cases that are:
|
|
47
|
+
- NOT already in the `specifications` array of existing `slice.json`
|
|
48
|
+
- All given/when/then scenarios from tests (happy path, edge cases, error scenarios)
|
|
49
|
+
- Behavioral contracts being tested
|
|
50
|
+
- NO implementation details, only specifications
|
|
51
|
+
|
|
52
|
+
**IMPORTANT:** Even if a command/event is defined in slice.json's commands/events arrays, if the test specification is NOT in the specifications array, it MUST be included in code-slice.json
|
|
53
|
+
|
|
54
|
+
### Specification Structure
|
|
55
|
+
|
|
56
|
+
Each specification includes:
|
|
57
|
+
- `vertical`: false
|
|
58
|
+
- `id`: unique generated ID
|
|
59
|
+
- `sliceName`: slice title
|
|
60
|
+
- `type`: COMMAND, EVENT, READMODEL or SPEC_ERROR'
|
|
61
|
+
- `title`: spec description from test
|
|
62
|
+
- `given`: array of event objects (preconditions)
|
|
63
|
+
- STATE_CHANGE: events that happened before
|
|
64
|
+
- STATE_VIEW: events to be projected
|
|
65
|
+
- id is the same as the original elements id
|
|
66
|
+
- `when`: command object OR empty array
|
|
67
|
+
- STATE_CHANGE: single command object (the action being tested)
|
|
68
|
+
- STATE_VIEW: empty array `[]` (events are in `given` for projections)
|
|
69
|
+
- id is the same as the original elements id
|
|
70
|
+
- `then`: array of event/error/readmodel objects
|
|
71
|
+
- Can be events (what happened as a result, type SPEC_EVENT)
|
|
72
|
+
- Can be errors (SPEC_ERROR type)
|
|
73
|
+
- Can be readmodel assertions (SPEC_READMODEL type for projections)
|
|
74
|
+
- id is the same as the original elements id
|
|
75
|
+
- `comments`: empty array
|
|
76
|
+
- `linkedId`: MUST be the same as the specification's `id` field (self-reference)
|
|
77
|
+
|
|
78
|
+
## Supported Slice Types
|
|
79
|
+
|
|
80
|
+
### 1. STATE_CHANGE Slices
|
|
81
|
+
|
|
82
|
+
Uses `DeciderSpecification.for()` with decide/evolve pattern.
|
|
83
|
+
|
|
84
|
+
```typescript
|
|
85
|
+
const given = DeciderSpecification.for({
|
|
86
|
+
decide,
|
|
87
|
+
evolve,
|
|
88
|
+
initialState: ...
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it('spec: description', () => {
|
|
92
|
+
given([...events])
|
|
93
|
+
.when(command)
|
|
94
|
+
.then([...events])
|
|
95
|
+
});
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
**See:** [state-change-example.md](examples/state-change-example.md)
|
|
99
|
+
|
|
100
|
+
### 2. STATE_VIEW Slices
|
|
101
|
+
|
|
102
|
+
Uses `PostgreSQLProjectionSpec.for()` for projections/read models.
|
|
103
|
+
|
|
104
|
+
```typescript
|
|
105
|
+
given = PostgreSQLProjectionSpec.for({
|
|
106
|
+
projection: SomeProjection,
|
|
107
|
+
connectionString,
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it('spec: description', () => {
|
|
111
|
+
await given([...events]) // Events are in given() for projections
|
|
112
|
+
.when([]) // when() is always empty for projections
|
|
113
|
+
.then(assertReadModel);
|
|
114
|
+
});
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
**IMPORTANT for STATE_VIEW:**
|
|
118
|
+
- Events are placed in `given` array (the events to project)
|
|
119
|
+
- `when` is always empty array (no command in projections)
|
|
120
|
+
- `then` contains readmodel assertions
|
|
121
|
+
|
|
122
|
+
**See:** [state-view-example.md](examples/state-view-example.md)
|
|
123
|
+
|
|
124
|
+
## Analysis Process
|
|
125
|
+
|
|
126
|
+
### Step 1: Identify Slice Type
|
|
127
|
+
|
|
128
|
+
1. Read the test file
|
|
129
|
+
2. Look for `DeciderSpecification.for()` → STATE_CHANGE
|
|
130
|
+
3. Look for `PostgreSQLProjectionSpec.for()` → STATE_VIEW
|
|
131
|
+
4. Extract slice name from describe block or file name
|
|
132
|
+
|
|
133
|
+
### Step 2: Extract Test Specifications
|
|
134
|
+
|
|
135
|
+
For each `it('spec: ...', ...)` test:
|
|
136
|
+
|
|
137
|
+
1. Extract specification description from test name
|
|
138
|
+
2. Identify given events (preconditions)
|
|
139
|
+
3. Identify when command/events (action)
|
|
140
|
+
4. Identify then events/assertions (postconditions)
|
|
141
|
+
5. Map to given/when/then structure
|
|
142
|
+
6. **Compare with existing slice.json:**
|
|
143
|
+
- Check if specification already exists in `slice.json` specifications array
|
|
144
|
+
- If exact match found in specifications array → SKIP
|
|
145
|
+
- If NOT in specifications array → INCLUDE (even if command/event element exists)
|
|
146
|
+
|
|
147
|
+
### Step 3: Extract Element Definitions
|
|
148
|
+
|
|
149
|
+
#### For STATE_CHANGE:
|
|
150
|
+
|
|
151
|
+
- **Commands:** Extract from `when()` calls with fields and types
|
|
152
|
+
- **Events:** Extract from `given()` and `then()` calls with fields and types
|
|
153
|
+
- **Dependencies:** Commands → Events (OUTBOUND), Events → Commands (INBOUND)
|
|
154
|
+
|
|
155
|
+
#### For STATE_VIEW:
|
|
156
|
+
|
|
157
|
+
- **Events:** Extract from `given()` calls (events to be projected)
|
|
158
|
+
- **IMPORTANT:** `when()` is always empty for STATE_VIEW slices
|
|
159
|
+
- **Read Model:** Extract from projection name and assertion logic in `then()`
|
|
160
|
+
- **Dependencies:** Events → Read Model (OUTBOUND)
|
|
161
|
+
|
|
162
|
+
### Step 4: Build Specifications Array
|
|
163
|
+
|
|
164
|
+
- Include ALL test cases NOT in `slice.json` specifications array
|
|
165
|
+
- This includes happy path scenarios, error scenarios, edge cases, and alternative flows
|
|
166
|
+
- Format as given/when/then specifications with full field definitions
|
|
167
|
+
|
|
168
|
+
See [specification-format.md](examples/specification-format.md) for complete format details.
|
|
169
|
+
|
|
170
|
+
## Slice Name and Context Detection
|
|
171
|
+
|
|
172
|
+
**Slice Name:**
|
|
173
|
+
- Extract from test file name: `<SliceName>.test.ts` → `<SliceName>`
|
|
174
|
+
- Example: `ActivateShift.test.ts` → "ActivateShift"
|
|
175
|
+
|
|
176
|
+
**Context:**
|
|
177
|
+
- If metadata contains `restaurantId` or `locationId` → "Restaurant Management"
|
|
178
|
+
- Otherwise → extract from aggregate name or default to "General"
|
|
179
|
+
|
|
180
|
+
**Using `.slices/index.json` for lookup:**
|
|
181
|
+
1. Read `.slices/index.json` to find correct folder
|
|
182
|
+
2. Match on slice title (case-insensitive)
|
|
183
|
+
3. Extract `folder` and `context` from matching entry
|
|
184
|
+
4. Construct path: `.slices/<context>/<folder>/slice.json`
|
|
185
|
+
|
|
186
|
+
## Implementation Steps
|
|
187
|
+
|
|
188
|
+
When invoked with a test file path:
|
|
189
|
+
|
|
190
|
+
1. **Read and parse the test file**
|
|
191
|
+
- Use Read tool to load file
|
|
192
|
+
- Identify testing framework pattern
|
|
193
|
+
|
|
194
|
+
2. **Extract slice metadata**
|
|
195
|
+
- Parse describe block for slice name
|
|
196
|
+
- Determine slice type (DeciderSpecification = STATE_CHANGE, PostgreSQLProjectionSpec = STATE_VIEW)
|
|
197
|
+
- Extract context from metadata fields
|
|
198
|
+
- Infer slice name from file path
|
|
199
|
+
|
|
200
|
+
3. **Check for existing files**
|
|
201
|
+
- Lookup `slice.json` location via `.slices/index.json`
|
|
202
|
+
- Read `slice.json` if found for comparison
|
|
203
|
+
- Extract `id` and `title` fields from `slice.json` (required for code-slice.json)
|
|
204
|
+
- Track which elements already exist in design documentation
|
|
205
|
+
|
|
206
|
+
4. **Analyze test cases**
|
|
207
|
+
- Find all `it('spec: ...')` test cases
|
|
208
|
+
- Extract given/when/then structures
|
|
209
|
+
- Compare with existing `slice.json`
|
|
210
|
+
|
|
211
|
+
5. **Build specifications array**
|
|
212
|
+
- Include ALL test specifications NOT already in `slice.json` specifications array
|
|
213
|
+
- Format with complete field structures (see [specification-format.md](examples/specification-format.md))
|
|
214
|
+
- **CRITICAL: Look up real IDs from slice.json for linkedId fields:**
|
|
215
|
+
- For each command in `when`: Find matching command in `slice.json` → `commands` array by title
|
|
216
|
+
- For each event in `given`/`then`:
|
|
217
|
+
- First try `slice.json` → `events` array by title
|
|
218
|
+
- For STATE_VIEW: If not found, search in `readmodels` → `dependencies` array (INBOUND events)
|
|
219
|
+
- Use the `id` field from the matched element/dependency as the `linkedId`
|
|
220
|
+
- For each readmodel assertion in `then`: Find matching readmodel in `slice.json` → `readmodels` array by title
|
|
221
|
+
- If no match found, use `null` (indicates new element not in design documentation)
|
|
222
|
+
- For errors (SPEC_ERROR): Do NOT include `linkedId` field (errors don't exist in slice.json)
|
|
223
|
+
|
|
224
|
+
6. **Decision: Generate, Update, or Remove code-slice.json**
|
|
225
|
+
- **Compare test specifications with `slice.json` specifications array**
|
|
226
|
+
- **All test specs in `slice.json` specifications:** Remove/don't create `code-slice.json` (no drift)
|
|
227
|
+
- **Test specs NOT in `slice.json` specifications:** Generate/update `code-slice.json` with:
|
|
228
|
+
- Write to `.slices/<Context>/<folder>/code-slice.json` (same directory as `slice.json`)
|
|
229
|
+
- `id` field copied from `slice.json`
|
|
230
|
+
- `title` field copied from `slice.json`
|
|
231
|
+
- `specifications` array with missing specifications
|
|
232
|
+
- **NEVER touch `slice.json`** - it's read-only design documentation
|
|
233
|
+
|
|
234
|
+
7. **Validate output**
|
|
235
|
+
- Ensure JSON is valid
|
|
236
|
+
- Verify all required fields present
|
|
237
|
+
- Check structure matches reference format
|
|
238
|
+
|
|
239
|
+
## Field Type Mapping
|
|
240
|
+
|
|
241
|
+
Infer TypeScript/JSON types from test data:
|
|
242
|
+
- String values → `"String"`
|
|
243
|
+
- UUID format → `"UUID"`
|
|
244
|
+
- Date strings (ISO format) → `"Date"` or `"DateTime"`
|
|
245
|
+
- Numbers with decimals → `"Decimal"`
|
|
246
|
+
- Whole numbers → `"Integer"`
|
|
247
|
+
- Boolean values → `"Boolean"`
|
|
248
|
+
- Arrays → `"List"` with element type
|
|
249
|
+
|
|
250
|
+
## ID Generation
|
|
251
|
+
|
|
252
|
+
**CRITICAL: LinkedId Fields Must Use Real IDs from slice.json**
|
|
253
|
+
|
|
254
|
+
- **linkedId in given/when/then elements**: MUST use the actual element IDs from slice.json
|
|
255
|
+
- Look up command IDs in `slice.json` → `commands` array → `id` field
|
|
256
|
+
- Look up event IDs in `slice.json` → `events` array → `id` field
|
|
257
|
+
- **For STATE_VIEW slices**: Events may not be in the `events` array if they're from other slices
|
|
258
|
+
- Instead, look in `readmodels` → `dependencies` array for entries with `type: "INBOUND"` and `elementType: "EVENT"`
|
|
259
|
+
- Use the `id` field from the dependency entry
|
|
260
|
+
- Look up readmodel IDs in `slice.json` → `readmodels` array → `id` field
|
|
261
|
+
- If element not found in slice.json, use `null` (will be identified as drift)
|
|
262
|
+
|
|
263
|
+
- **Other ID fields**: Generate unique IDs for specification containers
|
|
264
|
+
- Specification `id`: Generate unique ID (e.g., `"GENERATED-SPEC-<counter>"`)
|
|
265
|
+
- Element wrapper `id`: Generate unique ID for each given/when/then element
|
|
266
|
+
- These are just structural IDs, not the element's identity
|
|
267
|
+
|
|
268
|
+
**Example from slice.json:**
|
|
269
|
+
```json
|
|
270
|
+
{
|
|
271
|
+
"commands": [
|
|
272
|
+
{
|
|
273
|
+
"id": "3458764657124632223", // ← Use THIS for linkedId
|
|
274
|
+
"title": "Activate Shift",
|
|
275
|
+
...
|
|
276
|
+
}
|
|
277
|
+
],
|
|
278
|
+
"events": [
|
|
279
|
+
{
|
|
280
|
+
"id": "3458764657005669318", // ← Use THIS for linkedId
|
|
281
|
+
"title": "Shift activated",
|
|
282
|
+
...
|
|
283
|
+
}
|
|
284
|
+
]
|
|
285
|
+
}
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
**Example in code-slice.json:**
|
|
289
|
+
```json
|
|
290
|
+
{
|
|
291
|
+
"id": "GENERATED-SPEC-001", // ← Specification ID
|
|
292
|
+
"linkedId": "GENERATED-SPEC-001", // ← MUST match specification id
|
|
293
|
+
"when": [
|
|
294
|
+
{
|
|
295
|
+
"title": "Activate Shift",
|
|
296
|
+
"id": "GENERATED-SPEC-CMD-1", // ← Generated wrapper ID
|
|
297
|
+
"linkedId": "3458764657124632223" // ← MUST be real command ID from slice.json
|
|
298
|
+
}
|
|
299
|
+
]
|
|
300
|
+
}
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
**Summary of linkedId usage:**
|
|
304
|
+
1. **Specification level**: `specification.linkedId` = `specification.id` (self-reference)
|
|
305
|
+
2. **Element level (given/when/then)**:
|
|
306
|
+
- Commands: `element.linkedId` = real ID from `slice.json` → `commands` array
|
|
307
|
+
- Events: `element.linkedId` = real ID from `slice.json` → `events` array OR `readmodels` → `dependencies` (for STATE_VIEW)
|
|
308
|
+
- Readmodels: `element.linkedId` = real ID from `slice.json` → `readmodels` array
|
|
309
|
+
3. **Error elements (SPEC_ERROR)**: No `linkedId` field (errors don't exist in slice.json)
|
|
310
|
+
|
|
311
|
+
## Usage
|
|
312
|
+
|
|
313
|
+
Invoke with any test file path:
|
|
314
|
+
|
|
315
|
+
**State-change slices:**
|
|
316
|
+
```
|
|
317
|
+
/test-analyzer src/slices/ActivateShift/ActivateShift.test.ts
|
|
318
|
+
/test-analyzer src/slices/PlanVacation/PlanVacation.test.ts
|
|
319
|
+
```
|
|
320
|
+
|
|
321
|
+
**State-view slices (projections):**
|
|
322
|
+
```
|
|
323
|
+
/test-analyzer src/slices/ActivatedOnlineReservations/OnlineReservationStatus.test.ts
|
|
324
|
+
/test-analyzer src/slices/ActiveShifts/ActiveShifts.test.ts
|
|
325
|
+
```
|
|
326
|
+
|
|
327
|
+
The skill automatically:
|
|
328
|
+
- Detects slice name from file path
|
|
329
|
+
- Determines if STATE_CHANGE or STATE_VIEW
|
|
330
|
+
- Finds corresponding `.slices/` documentation
|
|
331
|
+
- Generates `code-slice.json` in same directory as test file
|
|
332
|
+
|
|
333
|
+
## Drift Detection Strategy
|
|
334
|
+
|
|
335
|
+
1. **Compare test specifications with `slice.json` specifications array**
|
|
336
|
+
2. **Extract specifications NOT in `slice.json` specifications array:**
|
|
337
|
+
- Happy path scenarios not in specifications
|
|
338
|
+
- Error scenarios (thenThrows) not in specifications
|
|
339
|
+
- Edge cases not in specifications
|
|
340
|
+
- Alternative flows not in specifications
|
|
341
|
+
- **Note:** Even if command/event elements exist in slice.json, if the specification (given/when/then) is not in the specifications array, include it
|
|
342
|
+
3. **Actions on `code-slice.json`:**
|
|
343
|
+
- All test specs in `slice.json` specifications → Remove/don't create `code-slice.json` (no drift)
|
|
344
|
+
- Test specs NOT in `slice.json` specifications → Generate/update `code-slice.json`
|
|
345
|
+
- No `slice.json` found → Generate `code-slice.json` with all test specifications
|
|
346
|
+
|
|
347
|
+
**Key Principle:**
|
|
348
|
+
- `slice.json` = design documentation (source of truth, READ ONLY)
|
|
349
|
+
- `code-slice.json` = test-derived specifications (generated/deleted by this skill)
|
|
350
|
+
- Drift = test specifications that are NOT in slice.json specifications array
|
|
351
|
+
|
|
352
|
+
## File Locations
|
|
353
|
+
|
|
354
|
+
- **Test file:** `src/slices/<SliceName>/<SliceName>.test.ts`
|
|
355
|
+
- **Output:** `.slices/<Context>/<folder>/code-slice.json` (same directory as slice.json)
|
|
356
|
+
- **Index lookup:** `.slices/index.json` (find matching slice by title)
|
|
357
|
+
- **Reference:** `.slices/<Context>/<folder>/slice.json` (READ ONLY, from index.json)
|
|
358
|
+
|
|
359
|
+
## Examples
|
|
360
|
+
|
|
361
|
+
- [STATE_CHANGE Example](examples/state-change-example.md) - Complete example with ActivateShift slice
|
|
362
|
+
- [STATE_VIEW Example](examples/state-view-example.md) - Complete example with OnlineReservationStatus projection
|
|
363
|
+
- [Specification Format](examples/specification-format.md) - Detailed JSON structure and field definitions
|
|
364
|
+
|
|
365
|
+
## Key Notes
|
|
366
|
+
|
|
367
|
+
- Focus on extracting specifications, not implementation
|
|
368
|
+
- Specifications array contains ALL tests NOT already in `slice.json` specifications array
|
|
369
|
+
- **Remove `code-slice.json` if no drift detected** (all test specs are in slice.json specifications) - keeps codebase clean
|
|
370
|
+
- **Include ALL test specifications** not in slice.json specifications array, including happy path scenarios
|
|
371
|
+
- Drift occurs when test specifications are missing from slice.json specifications array, even if the command/event elements are defined
|
|
372
|
+
- **NEVER modify or delete `slice.json` files** - they are read-only design documentation
|
|
373
|
+
- Generic approach works with any slice - automatically detects name, type, and context
|