@codeforbreakfast/eventsourcing-commands
Version:
Wire command validation and dispatch for event sourcing systems - External boundary layer with schema validation
123 lines • 5.17 kB
JavaScript
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