소스 정보
- 저장소
- dilgerma/embuilder-node
- 최근 소스 활동
- 2026년 2월 28일 06:49
- 감지된 SKILL.md 언어
- 영어
- 스타
- 5
- 포크
- 1
설치 방법
기본적으로 소스를 먼저 확인하는 Prompt가 선택됩니다. 직접 명령으로 전환하거나 로컬 사본을 다운로드할 수도 있습니다.
소스 파일 검토
설치 여부를 결정하기 전에 SKILL.md와 SkillsMP에 표시된 보조 파일을 읽어 보세요.
파일 탐색기
5 개 파일SKILL.md 표시 중
SKILL.md
소스 지침 · 읽기 전용 미리보기- name
- slice-state-change
- description
- builds a state-change slice from an event model
## Overview
State Change Slices are slices that change the system by processing a command. Each slice implements the Command-Event pattern using event sourcing.
## Critical Requirements
### Restaurant ID Requirement
- **CRITICAL**: ALL events MUST have `restaurantId` in their metadata (camelCase)
- **NEVER** use `locationId` or `location_id` - these are outdated and forbidden
- **CRITICAL**: ALL database tables MUST have a `restaurant_id` column (snake_case)
- This ensures proper multi-tenancy and data isolation
## Implementation Steps
When creating a state-change slice, you MUST create the following files in `src/slices/{SliceName}/`:
1. **{SliceName}Command.ts** - Command handler implementation
2. **{SliceName}.test.ts** - Test specifications
3. **routes.ts** - API endpoint (unless explicitly told not to)
4. **src/slices/{SliceName}/ui-prompt.md - prompt to build the UI
## Command Handler Structure
Every Command handler file follows this pattern:
### 1. Imports
```typescript
import type {Command} from '@event-driven-io/emmett'
import {CommandHandler} from '@event-driven-io/emmett';
import {ContextEvents} from "../../events/ContextEvents";
import {findEventstore} from "../../common/loadPostgresEventstore";
```
### 2. Command Type Definition
Define the command with:
- Command name (matches slice name)
- Data fields from specification
- Standard metadata fields
```typescript
export type {CommandName}Command = Command<'{CommandName}', {
// data fields from spec
},
{
correlation_id?: string,
causation_id?: string,
now?: Date,
streamName?: string,
}>;
```
### 3. State Type
Define state needed for business logic validation:
```typescript
export type {CommandName}State = {
// fields needed for validation
// often empty {} if no validation needed
}
export const {CommandName}InitialState = (): {CommandName}State => ({
// initial state
});
```
### 4. Evolve Function
Updates state based on events (event sourcing projection):
```typescript
export const evolve = (
state: {CommandName}State,
event: ContextEvents,
): {CommandName}State => {
const {type, data} = event;
switch (type) {
case "{EventName}":
// update state based on event
return { ...state, /* updates */ };
default:
return state;
}
};
```
### 5. Decide Function
Business logic that validates command and returns events:
```typescript
export const decide = (
command: {CommandName}Command,
state: {CommandName}State,
): ContextEvents[] => {
// validation logic
// throw errors if validation fails
return [{
type: '{EventName}',
data: {
// event data fields
},
metadata: {
correlation_id: command.metadata?.correlation_id,
causation_id: command.metadata?.causation_id,
// these 2 are mandatory
restaurantId: command.metadata?.restaurantId,
userId: command.metadata?.userId
}
}];
};
```
### 6. Command Handler & Export
```typescript
const {CommandName}CommandHandler = CommandHandler<{CommandName}State, ContextEvents>({
evolve,
initialState: {CommandName}InitialState
});
export const handle{CommandName} = async (id: string, command: {CommandName}Command) => {
const eventStore = await findEventstore()
const result = await {CommandName}CommandHandler(eventStore, id, (state: {CommandName}State) => decide(command, state))
return {
nextExpectedStreamVersion: result.nextExpectedStreamVersion,
lastEventGlobalPosition: result.lastEventGlobalPosition
}
}
```
## Key Patterns
### Metadata Handling
- **Optional chaining**: Use `command.metadata?.correlation_id` (some examples use this)
- **Direct access**: Use `command.metadata.correlation_id` (other examples use this)
- **CRITICAL**: ALWAYS use `restaurantId` (camelCase) - NEVER use `locationId` or `location_id` (outdated)
- restaurantId: command.metadata?.restaurantId,
- userId: command.metadata?.userId
- Be consistent within your implementation
### Destructuring
Two patterns observed:
1. Direct access: `command.data.fieldName`
2. Destructure: `const {field1, field2} = command.data;`
Both are acceptable - choose based on readability.
### State Validation
- Only include fields in state that are needed for validation
- Many simple commands have empty state `{}`
- Complex validations (like AddLocation) track sets or maps in state
## Testing
Every command MUST have tests using DeciderSpecification:
```typescript
import {DeciderSpecification} from '@event-driven-io/emmett';
import {{CommandName}Command, {CommandName}State, decide, evolve} from "./{CommandName}Command";
import {describe, it} from "node:test";
describe('{CommandName} Specification', () => {
const given = DeciderSpecification.for({
decide,
evolve,
initialState: () => ({})
});
it('spec: {test description}', () => {
const command: {CommandName}Command = {
type: '{CommandName}',
data: {
// test data
},
metadata: {now: new Date()},
}
given([/* precondition events */])
.when(command)
.then([{
type: '{EventName}',
data: {
// expected event data
},
metadata: {}
}])
});
});
```
## Routes (API Endpoint)
Unless explicitly told not to, create a routes.ts file:
```typescript
import {Router, Request, Response} from 'express';
import {{CommandName}Command, handle{CommandName}} from './{CommandName}Command';
import {requireUser} from "../../supabase/requireUser";
import {WebApiSetup} from "@event-driven-io/emmett-expressjs";
import {assertNotEmpty} from "../../common/assertions";
export type {CommandName}RequestPayload = {
// fields matching command data (all optional with ?)
}
export type {CommandName}Request = Request<
Partial<{ id: string }>,
unknown,
Partial<{CommandName}RequestPayload>
>;
export const api =
(
// external dependencies
): WebApiSetup =>
(router: Router): void => {
router.post('/api/{commandname}/:id', requireRestaurantAccess, async (req: {CommandName}Request, res: Response) => {
const principal = await requireUser(req, res, false);
if (principal.error) {
return res.status(401).json(principal);
}
const correlation_id = req.header("correlation_id") ?? req.params.id
const causation_id = req.params.id
const restaurantId = (req as any).restaurant_id
const userId = (req as any).user_id
try {
const command: {CommandName}Command = {
data: {
// map from req.body with assertNotEmpty
},
metadata: {
correlation_id: correlation_id,
causation_id: causation_id
},
type: "{CommandName}"
}
if (!req.params.id) throw "no id provided"
const result = await handle{CommandName}(assertNotEmpty(req.params.id), command);
res.set("correlation_id", correlation_id)
res.set("causation_id", causation_id)
return res.status(200).json({
ok: true,
next_expected_stream_version: result.nextExpectedStreamVersion?.toString(),
last_event_global_position: result.lastEventGlobalPosition?.toString()
});
} catch (err) {
console.error(err);
return res.status(500).json({ok: false, error: 'Server error'});
}
});
};
```
## Business Logic
In *Command.ts
Evolve-Function provides the state we can use to validate:
```
export const evolve = (
state: PlanVacationState,
event: ContextEvents,
): PlanVacationState => {
const {type, data} = event;
switch (type) {
// case "..Event":
case 'VacationPlanned':
state.plannedVacations.push({id: event.data.vacation_id, from: event.data.from, to: event.data.to})
return state;
case 'VacationCancelled':
state.plannedVacations = state.plannedVacations.filter(it => it.id !== event.data.vacation_id)
return state;
default:
return state;
}
};
```
The Decide-Function then makes the decision ( success or error )
```
export const decide = (
command: PlanVacationCommand,
state: PlanVacationState,
): ContextEvents[] => {
state.plannedVacations.forEach(vacation => {
if (
command.data.from <= vacation.to &&
command.data.to >= vacation.from
) {
throw {error: "conflicting_vacations"}
}
})
return [{
type: "VacationPlanned",
data: {
...
},
metadata: {
...
}
}]
};
```
## Error Handling
In routes.ts, define an error mapper:
```
const errorMapping = (error:string): string => {
switch(error) {
case "conflicting_vacations" : return "Achtung, Betriebsurlaub überschneidet sich."
default: return "Leider ist ein Fehler aufgetreten"
}
}
```
use this directly in the route.
```
router.post('/api/planvacation/:id', requireRestaurantAccess, async (req: PlanVacationRequest, res: Response) => {
...
} catch (err:any) {
console.error(err);
return res.status(500).json({ok: false, error: errorMapping(err.error)});
}
});
};
```
## slice json
in each slice folder, generate a file .slice.json
```
{
"id" : "<slice id>",
"slice": "<slice title>",
"context": "<contextx>",
"link": "https://miro.com/app/board/<board-id>=/?moveToWidget=<slice id>"
}
```
## References
- See `templates/AddLocation` for samples
## UI Prompt
```
to build the UI - use this endpont "endpoint URL"
Payload example:
<payload example as JSON>
make sure to put endpoints into the api.ts and follow the rules:
- provide all headers
```
GitHub에서 보기