UNPKG

create-sekiban-app

Version:

Create a new Sekiban application with Dapr

185 lines (167 loc) 5.67 kB
import { defineProjector, EmptyAggregatePayload, AggregateProjector, PartitionKeys, Aggregate, IEvent, Result, ok, SekibanError } from '@sekiban/core'; import { z } from 'zod'; import { TaskCreated, TaskAssigned, TaskCompleted, TaskUpdated, TaskDeleted, TaskCompletionReverted } from '../events/task-events.js'; // Active task state export interface TaskState { aggregateType: 'Task'; taskId: string; title: string; description?: string; assignedTo?: string; dueDate?: string; priority: 'low' | 'medium' | 'high'; status: 'active' | 'deleted'; createdAt: string; updatedAt: string; deletedAt?: string; } // Completed task state - separate type to enforce state machine export interface CompletedTaskState { aggregateType: 'CompletedTask'; taskId: string; title: string; description?: string; assignedTo?: string; dueDate?: string; priority: 'low' | 'medium' | 'high'; status: 'completed'; createdAt: string; updatedAt: string; completedAt: string; completedBy: string; completionNotes?: string; } // Union type for all possible states export type TaskPayloadUnion = TaskState | CompletedTaskState; // Task Projector using defineProjector - now supports multiple states export const taskProjectorDefinition = defineProjector<TaskPayloadUnion>({ aggregateType: 'Task', initialState: () => new EmptyAggregatePayload(), projections: { TaskCreated: (state: any, event: z.infer<typeof TaskCreated.schema>) => ({ aggregateType: 'Task' as const, taskId: event.taskId, title: event.title, description: event.description, assignedTo: event.assignedTo, dueDate: event.dueDate, priority: event.priority, status: 'active' as const, createdAt: event.createdAt, updatedAt: event.createdAt } as TaskState), TaskAssigned: (state: any, event: z.infer<typeof TaskAssigned.schema>) => { if (!state || state.aggregateType !== 'Task') return state; return { ...state, assignedTo: event.assignedTo, updatedAt: event.assignedAt } as TaskState; }, TaskCompleted: (state: any, event: z.infer<typeof TaskCompleted.schema>) => { // Can only complete active tasks if (!state || state.aggregateType !== 'Task' || state.status === 'deleted') return state; // Transform to CompletedTaskState const taskState = state as TaskState; return { aggregateType: 'CompletedTask' as const, taskId: taskState.taskId, title: taskState.title, description: taskState.description, assignedTo: taskState.assignedTo, dueDate: taskState.dueDate, priority: taskState.priority, status: 'completed' as const, createdAt: taskState.createdAt, updatedAt: event.completedAt, completedAt: event.completedAt, completedBy: event.completedBy, completionNotes: event.notes } as CompletedTaskState; }, TaskUpdated: (state: any, event: z.infer<typeof TaskUpdated.schema>) => { // Can only update active tasks if (!state || state.aggregateType !== 'Task') return state; return { ...state, title: event.title || state.title, description: event.description !== undefined ? event.description : state.description, dueDate: event.dueDate !== undefined ? event.dueDate : state.dueDate, priority: event.priority || state.priority, updatedAt: event.updatedAt } as TaskState; }, TaskDeleted: (state: any, event: z.infer<typeof TaskDeleted.schema>) => { // Can only delete active tasks if (!state || state.aggregateType !== 'Task') return state; return { ...state, status: 'deleted' as const, deletedAt: event.deletedAt, updatedAt: event.deletedAt } as TaskState; }, TaskCompletionReverted: (state: any, event: z.infer<typeof TaskCompletionReverted.schema>) => { // Can only revert completed tasks if (!state || state.aggregateType !== 'CompletedTask') return state; // Transform back to TaskState const completedState = state as CompletedTaskState; return { aggregateType: 'Task' as const, taskId: completedState.taskId, title: completedState.title, description: completedState.description, assignedTo: completedState.assignedTo, dueDate: completedState.dueDate, priority: completedState.priority, status: 'active' as const, createdAt: completedState.createdAt, updatedAt: event.revertedAt } as TaskState; } } }); // TaskProjector class for command API compatibility export class TaskProjector extends AggregateProjector<TaskPayloadUnion> { readonly aggregateTypeName = 'Task'; readonly multiProjectorName = 'TaskAggregateListProjector'; getInitialState(partitionKeys: PartitionKeys): Aggregate<EmptyAggregatePayload> { return taskProjectorDefinition.getInitialState(partitionKeys); } project( aggregate: Aggregate<TaskPayloadUnion | EmptyAggregatePayload>, event: IEvent ): Result<Aggregate<TaskPayloadUnion | EmptyAggregatePayload>, SekibanError> { return taskProjectorDefinition.project(aggregate, event); } canHandle(eventType: string): boolean { return [ 'TaskCreated', 'TaskAssigned', 'TaskCompleted', 'TaskUpdated', 'TaskDeleted', 'TaskCompletionReverted' ].includes(eventType); } getSupportedPayloadTypes(): string[] { return ['Task', 'CompletedTask']; } }