import type { OrchestrationCommand, OrchestrationProject, OrchestrationReadModel, OrchestrationThread, ProjectId, ThreadId, } from "@t3tools/contracts"; import { Effect } from "effect"; import { OrchestrationCommandInvariantError } from "./Errors.ts"; function invariantError(commandType: string, detail: string): OrchestrationCommandInvariantError { return new OrchestrationCommandInvariantError({ commandType, detail, }); } export function findThreadById( readModel: OrchestrationReadModel, threadId: ThreadId, ): OrchestrationThread | undefined { return readModel.threads.find((thread) => thread.id === threadId); } export function findProjectById( readModel: OrchestrationReadModel, projectId: ProjectId, ): OrchestrationProject | undefined { return readModel.projects.find((project) => project.id === projectId); } export function listThreadsByProjectId( readModel: OrchestrationReadModel, projectId: ProjectId, ): ReadonlyArray { return readModel.threads.filter((thread) => thread.projectId === projectId); } export function requireProject(input: { readonly readModel: OrchestrationReadModel; readonly command: OrchestrationCommand; readonly projectId: ProjectId; }): Effect.Effect { const project = findProjectById(input.readModel, input.projectId); if (project) { return Effect.succeed(project); } return Effect.fail( invariantError( input.command.type, `Project '${input.projectId}' does not exist for command '${input.command.type}'.`, ), ); } export function requireProjectAbsent(input: { readonly readModel: OrchestrationReadModel; readonly command: OrchestrationCommand; readonly projectId: ProjectId; }): Effect.Effect { if (!findProjectById(input.readModel, input.projectId)) { return Effect.void; } return Effect.fail( invariantError( input.command.type, `Project '${input.projectId}' already exists and cannot be created twice.`, ), ); } export function requireThread(input: { readonly readModel: OrchestrationReadModel; readonly command: OrchestrationCommand; readonly threadId: ThreadId; }): Effect.Effect { const thread = findThreadById(input.readModel, input.threadId); if (thread) { return Effect.succeed(thread); } return Effect.fail( invariantError( input.command.type, `Thread '${input.threadId}' does not exist for command '${input.command.type}'.`, ), ); } export function requireThreadArchived(input: { readonly readModel: OrchestrationReadModel; readonly command: OrchestrationCommand; readonly threadId: ThreadId; }): Effect.Effect { return requireThread(input).pipe( Effect.flatMap((thread) => thread.archivedAt !== null ? Effect.succeed(thread) : Effect.fail( invariantError( input.command.type, `Thread '${input.threadId}' is not archived for command '${input.command.type}'.`, ), ), ), ); } export function requireThreadNotArchived(input: { readonly readModel: OrchestrationReadModel; readonly command: OrchestrationCommand; readonly threadId: ThreadId; }): Effect.Effect { return requireThread(input).pipe( Effect.flatMap((thread) => thread.archivedAt === null ? Effect.succeed(thread) : Effect.fail( invariantError( input.command.type, `Thread '${input.threadId}' is already archived and cannot handle command '${input.command.type}'.`, ), ), ), ); } export function requireThreadAbsent(input: { readonly readModel: OrchestrationReadModel; readonly command: OrchestrationCommand; readonly threadId: ThreadId; }): Effect.Effect { if (!findThreadById(input.readModel, input.threadId)) { return Effect.void; } return Effect.fail( invariantError( input.command.type, `Thread '${input.threadId}' already exists and cannot be created twice.`, ), ); } export function requireNonNegativeInteger(input: { readonly commandType: OrchestrationCommand["type"]; readonly field: string; readonly value: number; }): Effect.Effect { if (Number.isInteger(input.value) && input.value >= 0) { return Effect.void; } return Effect.fail( invariantError( input.commandType, `${input.field} must be an integer greater than or equal to 0.`, ), ); }