UNPKG

@codeforbreakfast/eventsourcing-commands

Version:

Wire command validation and dispatch for event sourcing systems - External boundary layer with schema validation

123 lines 5.17 kB
import { Schema, Data, Effect, pipe } from 'effect'; import { EventStreamPosition } from '@codeforbreakfast/eventsourcing-store'; // ============================================================================ // Wire Commands - External Transport Layer // ============================================================================ /** * Wire command for transport/serialization * Used by APIs, message queues, and other external interfaces */ export const WireCommand = Schema.Struct({ id: Schema.String, target: Schema.String, name: Schema.String, payload: Schema.Unknown, }); // ============================================================================ // Command Errors // ============================================================================ export class CommandValidationError extends Data.TaggedError('CommandValidationError') { } export class CommandHandlerNotFoundError extends Data.TaggedError('CommandHandlerNotFoundError') { } export class CommandExecutionError extends Data.TaggedError('CommandExecutionError') { } export class AggregateNotFoundError extends Data.TaggedError('AggregateNotFoundError') { } export class ConcurrencyConflictError extends Data.TaggedError('ConcurrencyConflictError') { } // ============================================================================ // Command Results // ============================================================================ export const CommandSuccess = Schema.Struct({ _tag: Schema.Literal('Success'), position: EventStreamPosition, }); export const CommandFailure = Schema.Struct({ _tag: Schema.Literal('Failure'), error: Schema.Union(Schema.Struct({ _tag: Schema.Literal('ValidationError'), commandId: Schema.String, commandName: Schema.String, validationErrors: Schema.Array(Schema.String), }), Schema.Struct({ _tag: Schema.Literal('HandlerNotFound'), commandId: Schema.String, commandName: Schema.String, availableHandlers: Schema.Array(Schema.String), }), Schema.Struct({ _tag: Schema.Literal('ExecutionError'), commandId: Schema.String, commandName: Schema.String, message: Schema.String, }), Schema.Struct({ _tag: Schema.Literal('AggregateNotFound'), commandId: Schema.String, aggregateId: Schema.String, aggregateType: Schema.String, }), Schema.Struct({ _tag: Schema.Literal('ConcurrencyConflict'), commandId: Schema.String, expectedVersion: Schema.Number, actualVersion: Schema.Number, }), Schema.Struct({ _tag: Schema.Literal('UnknownError'), commandId: Schema.String, message: Schema.String, })), }); export const CommandResult = Schema.Union(CommandSuccess, CommandFailure); // ============================================================================ // Command Result Type Guards // ============================================================================ export const isCommandSuccess = Schema.is(CommandSuccess); export const isCommandFailure = Schema.is(CommandFailure); /** * Creates a command definition */ export const defineCommand = (name, payloadSchema) => ({ name, payloadSchema, }); const buildSchemaFromCommandList = (commands) => { const schemas = commands.map((cmd) => Schema.Struct({ id: Schema.String, target: Schema.String, name: Schema.Literal(cmd.name), payload: cmd.payloadSchema, })); return schemas.length === 1 ? // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Type assertion required to match return type when single schema schemas[0] : schemas.length >= 2 ? // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Type assertion required to match return type for union of variable schemas Schema.Union(schemas[0], schemas[1], ...schemas.slice(2)) : (() => { throw new Error('Unexpected state: should have at least 1 schema'); })(); }; /** * Builds a discriminated union schema from command definitions * This creates an exhaustive schema that can parse any registered command */ export const buildCommandSchema = (commands) => // eslint-disable-next-line effect/prefer-match-over-ternary -- Synchronous input validation guard, not pattern matching commands.length === 0 ? (() => { throw new Error('At least one command definition is required'); })() : buildSchemaFromCommandList(commands); /** * Validates and transforms a wire command into a domain command */ export const validateCommand = (payloadSchema) => (wireCommand) => pipe(wireCommand.payload, Schema.decodeUnknown(payloadSchema), Effect.mapError((parseError) => new CommandValidationError({ commandId: wireCommand.id, commandName: wireCommand.name, validationErrors: [parseError.message || 'Payload validation failed'], })), Effect.map((validatedPayload) => ({ id: wireCommand.id, target: wireCommand.target, name: wireCommand.name, payload: validatedPayload, }))); //# sourceMappingURL=commands.js.map