Skip to main content

slice-state-change

builds a state-change slice from an event model

معلومات المصدر

المستودع
dilgerma/embuilder-node
آخر نشاط في المصدر
٢٨ فبراير ٢٠٢٦ في ٠٦:٤٩
لغة SKILL.md المكتشفة
الإنجليزية
النجوم
٥
التفرعات
١

خيارات التثبيت

يُحدَّد 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