ワンクリックで
build-state-view
Implements an emmett state-view slice (projection, tests, route, migration) from a slice.json definition
Codex または Claude でインストール この Prompt をコピーして Codex、Claude、または他のアシスタントに貼り付けると、Skill ページを確認してインストールできます。
メニュー
Implements an emmett state-view slice (projection, tests, route, migration) from a slice.json definition
Codex または Claude でインストール この Prompt をコピーして Codex、Claude、または他のアシスタントに貼り付けると、Skill ページを確認してインストールできます。
SOC 職業分類に基づく
Teaches an agent everything about the eventmodelers platform API — all endpoints, their purpose, request payloads, response shapes, authentication, and element types.
Implements an emmett state-view slice (projection, tests, route, migration) from a slice.json definition
Teaches an agent everything about the eventmodelers platform API — all endpoints, their purpose, request payloads, response shapes, authentication, and element types.
Teaches an agent everything about the eventmodelers platform API — all endpoints, their purpose, request payloads, response shapes, authentication, and element types.
Implement automation and translation slices the Cratis way — an IReactor that observes events and produces side effects, triggering further writes via ICommandPipeline. Use when: (1) implementing a new automation / reactor in a Cratis project, (2) a slice.json has a non-empty processors[] section or sliceType === "TRANSLATION", (3) the user provides an Event Modeling artifact or description of an event-driven reaction and asks to implement it, (4) the user says "implement", "create", "add" an automation, reactor, translation, or event-to-command flow in a Cratis Arc / Chronicle project.
Implement Event Sourcing write slices the Cratis way — using Cratis Arc (CQRS) + Cratis Chronicle (event sourcing) in a .NET / C# project. A write slice is: Command → Handle() → Event(s), with optional validators, constraints, and DCB business rules. Use when: (1) implementing a new write slice / command in a Cratis project, (2) a slice.json has a non-empty commands[] / events[] section, (3) the user provides an Event Modeling artifact, specification, or natural-language description of a command and asks to implement it, (4) the user says "implement", "create", "add" a write slice, command, or state change in a Cratis Arc / Chronicle project.
| name | build-state-view |
| description | Implements an emmett state-view slice (projection, tests, route, migration) from a slice.json definition |
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.
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.
From the slice definition, extract:
canHandle list)Comments & description: Each element (commands, events, readmodels, processors, screens, tables) carries a
comments: string[]array (board comments on that node) and adescriptionfield. The slice itself also hascomments: 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).
File: supabase/migrations/V{N}__{tablename}.sql
Choose the next available version number by checking existing migration files.
CREATE TABLE IF NOT EXISTS "public"."{tablename}"
(
id TEXT PRIMARY KEY,
-- other columns from read model fields in slice.json
-- use snake_case for all column names
created_at TIMESTAMP DEFAULT NOW()
);
Column type guide:
| Field type | SQL type |
|---|---|
| string / UUID | TEXT |
| number (integer) | INTEGER |
| number (float) | NUMERIC |
| boolean | BOOLEAN |
| date | TIMESTAMP |
| nullable number | INTEGER (allow NULL) |
The PRIMARY KEY column is the one used in .onConflict(...) in the projection.
{SliceName}Projection.tsFile: src/slices/{context}/{SliceName}/{SliceName}Projection.ts
import {postgreSQLRawSQLProjection} from '@event-driven-io/emmett-postgresql';
import {sql, SQL} from '@event-driven-io/dumbo';
import knex, {Knex} from 'knex';
import {type {EventA}, type {EventB}} from '../{Context}Events';
export const tableName = '{tablename}';
// TypeScript shape of one row in the read model
export type {SliceName}ReadModel = {
id: string;
// ... fields from slice.json readModel
};
export const getKnexInstance = (connectionString: string): Knex =>
knex({client: 'pg', connection: connectionString, pool: {min: 0, max: 1}});
type {SliceName}Events = {EventA} | {EventB};
export const {SliceName}Projection = postgreSQLRawSQLProjection<{SliceName}Events>({
name: '{SliceName}Projection',
canHandle: ['{EventA}', '{EventB}'],
evolve: async (event, context): Promise<SQL[]> => {
const db = getKnexInstance(context.connection.connectionString);
try {
switch (event.type) {
case '{EventA}':
// Insert with upsert — use for create/update events
return [sql(db(tableName)
.withSchema('public')
.insert({
id: event.data.id,
field1: event.data.field1,
field2: event.data.field2,
})
.onConflict('id')
.merge(['field1', 'field2'])
.toQuery())];
case '{EventB}':
// Delete — use for cancellation/removal events
return [sql(db(tableName)
.withSchema('public')
.where({id: event.data.id})
.delete()
.toQuery())];
default:
return [];
}
} finally {
await db.destroy();
}
},
});
Insert with upsert (create or update):
return [sql(db(tableName)
.withSchema('public')
.insert({ id: event.data.id, field: event.data.field })
.onConflict('id')
.merge(['field']) // list only columns to update on conflict
.toQuery())];
Update only (record already exists):
return [sql(db(tableName)
.withSchema('public')
.where({id: event.data.id})
.update({field: event.data.field})
.toQuery())];
Delete:
return [sql(db(tableName)
.withSchema('public')
.where({id: event.data.id})
.delete()
.toQuery())];
Async DB lookup before update (when you need to read current state first):
const row = await db(tableName)
.withSchema('public')
.where({id: event.data.id})
.select('field')
.first();
if (!row) return [];
const newValue = row.field + delta;
return [sql(db(tableName)
.withSchema('public')
.where({id: event.data.id})
.update({field: newValue})
.toQuery())];
Always wrap in try/finally and call db.destroy() in the finally block.
File: src/common/loadPostgresEventstore.ts
Add the new projection to the projections.inline([...]) array:
import {{SliceName}Projection} from '../slices/{context}/{SliceName}/{SliceName}Projection';
// inside getPostgreSQLEventStore options:
projections: projections.inline([
// ... existing projections ...
{SliceName}Projection,
]),
{SliceName}.test.tsFile: src/slices/{context}/{SliceName}/{SliceName}.test.ts
Uses PostgreSQLProjectionSpec with a real PostgreSQL container (Testcontainers). Flyway runs actual migrations so the schema matches production exactly.
import {before, after, describe, it} from 'node:test';
import {PostgreSQLProjectionAssert, PostgreSQLProjectionSpec} from '@event-driven-io/emmett-postgresql';
import {{SliceName}Projection} from './{SliceName}Projection';
import {PostgreSqlContainer, StartedPostgreSqlContainer} from '@testcontainers/postgresql';
import knex, {Knex} from 'knex';
import assert from 'assert';
import {runFlywayMigrations} from '../../../common/testHelpers';
const TEST_ID = 'test-id-001';
describe('{SliceName} Specification', () => {
let postgres: StartedPostgreSqlContainer;
let connectionString: string;
let db: Knex;
let given: PostgreSQLProjectionSpec<any>;
before(async () => {
postgres = await new PostgreSqlContainer('postgres').start();
connectionString = postgres.getConnectionUri();
db = knex({client: 'pg', connection: connectionString});
await runFlywayMigrations(connectionString);
// Insert any prerequisite rows required by foreign keys:
// await db('parent_table').withSchema('public').insert({...});
given = PostgreSQLProjectionSpec.for({
projection: {SliceName}Projection,
connectionString,
});
});
after(async () => {
await db?.destroy();
await postgres?.stop();
});
it('spec: {SliceName} - inserts row on {EventA}', async () => {
const assertReadModel: PostgreSQLProjectionAssert = async ({connectionString: connStr}) => {
const queryDb = knex({client: 'pg', connection: connStr});
try {
const result = await queryDb('{tablename}')
.withSchema('public')
.where({id: TEST_ID})
.first();
assert.ok(result, 'row should exist');
assert.strictEqual(result.id, TEST_ID);
assert.strictEqual(result.field1, 'expected-value');
} finally {
await queryDb.destroy();
}
};
await given([{
type: '{EventA}',
data: {id: TEST_ID, field1: 'expected-value'},
metadata: {stream_name: `{context}-${TEST_ID}`},
}])
.when([])
.then(assertReadModel);
});
it('spec: {SliceName} - removes row on {EventB}', async () => {
const assertReadModel: PostgreSQLProjectionAssert = async ({connectionString: connStr}) => {
const queryDb = knex({client: 'pg', connection: connStr});
try {
const result = await queryDb('{tablename}')
.withSchema('public')
.where({id: TEST_ID})
.first();
assert.strictEqual(result, undefined, 'row should be deleted');
} finally {
await queryDb.destroy();
}
};
await given([
{
type: '{EventA}',
data: {id: TEST_ID, field1: 'value'},
metadata: {stream_name: `{context}-${TEST_ID}`},
},
{
type: '{EventB}',
data: {id: TEST_ID},
metadata: {stream_name: `{context}-${TEST_ID}`},
},
])
.when([])
.then(assertReadModel);
});
});
Write one it block per specification in the slice.json. Use given([events]).when([]).then(assertReadModel).
routes.tsFile: src/slices/{context}/{SliceName}/routes.ts
Concrete example:
src/slices/example/routes.ts— shows the full pattern withrequireUser,assertNotEmpty, error mapping, and OpenAPI annotations. Read it before implementing.
import {Request, Response, Router} from 'express';
import {WebApiSetup} from '@event-driven-io/emmett-expressjs';
import {requireUser} from '../../../supabase/requireUser';
import {{SliceName}ReadModel, tableName} from './{SliceName}Projection';
import {readmodel} from '../../../core/readmodel';
import createClient from '../../../supabase/api';
export const api = (): WebApiSetup => (router: Router): void => {
router.get('/api/query/{slicename}-collection', async (req: Request, res: Response) => {
try {
const principal = await requireUser(req, res, true);
if (principal.error) return;
const id = req.query._id?.toString();
const supabase = createClient();
const data: {SliceName}ReadModel | {SliceName}ReadModel[] | null =
id
? await readmodel(tableName, supabase).findById<{SliceName}ReadModel>('id', id)
: await readmodel(tableName, supabase).findAll<{SliceName}ReadModel>({});
const sanitized = JSON.parse(
JSON.stringify(data ?? [], (_, value) =>
typeof value === 'bigint' ? value.toString() : value,
),
);
return res.status(200).json(sanitized);
} catch (err) {
console.error(err);
return res.status(500).json({ok: false, error: 'Server error'});
}
});
};
Find the application's router registration (usually src/index.ts or src/app.ts) and add:
import {api as {SliceName}Api} from './slices/{context}/{SliceName}/routes';
{SliceName}Api()(router);
src/slices/{context}/{SliceName}/
├── {SliceName}Projection.ts ← projection logic
├── {SliceName}.test.ts ← PostgreSQLProjectionSpec tests
└── routes.ts ← GET query endpoint
supabase/migrations/
└── V{N}__{tablename}.sql ← table DDL
src/common/
└── loadPostgresEventstore.ts ← add projection to inline([...]) list
tableName constant matches the migration table name exactlyloadPostgresEventstore.tscanHandle lists every event type the projection reacts tofinally { await db.destroy() } present in every evolve handlerrunFlywayMigrations() to apply the real schemaevents[] is listed in the projection's canHandle — no assumed events