UNPKG

@codeforbreakfast/eventsourcing-commands

Version:

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

158 lines 8.16 kB
import { describe, it, expect } from '@codeforbreakfast/buntest'; import { Schema, Effect, pipe, Match } from 'effect'; import { defineCommand } from './commands'; import { makeCommandRegistry } from './command-registry'; describe('Command Registry', () => { const UserPayload = Schema.Struct({ email: pipe(Schema.String, Schema.pattern(/^[^\s@]+@[^\s@]+\.[^\s@]+$/)), name: pipe(Schema.String, Schema.minLength(1)), }); it.effect('should register and dispatch commands successfully', () => { const createUserCommand = defineCommand('CreateUser', UserPayload); const commands = [createUserCommand]; const commandMatcher = (command) => pipe(command, Match.value, Match.when({ name: 'CreateUser' }, () => Effect.succeed({ _tag: 'Success', position: { streamId: 'user-123', eventNumber: 1 }, })), Match.exhaustive); const registry = makeCommandRegistry(commands, commandMatcher); // Create a valid wire command const wireCommand = { id: 'cmd-123', target: 'user-456', name: 'CreateUser', payload: { email: 'test@example.com', name: 'John Doe', }, }; const assertSuccess = (result) => pipe(result, Match.value, Match.tag('Success', (success) => { expect(success.position.eventNumber).toBe(1); }), Match.tag('Failure', () => { expect(true).toBe(false); }), Match.exhaustive); // Dispatch the command return pipe(wireCommand, registry.dispatch, Effect.tap((result) => Effect.sync(() => assertSuccess(result)))); }); it.effect('should handle validation errors', () => { const createUserCommand = defineCommand('CreateUser', UserPayload); const commands = [createUserCommand]; const commandMatcher = (command) => pipe(command, Match.value, Match.when({ name: 'CreateUser' }, () => Effect.succeed({ _tag: 'Success', position: { streamId: 'user-123', eventNumber: 1 }, })), Match.exhaustive); const registry = makeCommandRegistry(commands, commandMatcher); const invalidCommand = { id: 'cmd-123', target: 'user-456', name: 'CreateUser', payload: { email: 'invalid-email', // Invalid email name: '', // Empty name }, }; const assertValidationErrorDetails = (error) => pipe(error, Match.value, Match.tag('ValidationError', (validationError) => { expect(validationError.commandId).toBe('cmd-123'); expect(validationError.commandName).toBe('CreateUser'); expect(validationError.validationErrors.length).toBeGreaterThan(0); }), Match.orElse(() => { expect(true).toBe(false); })); const assertValidationError = (result) => pipe(result, Match.value, Match.tag('Success', () => { expect(true).toBe(false); }), Match.tag('Failure', (failure) => assertValidationErrorDetails(failure.error)), Match.exhaustive); return pipe(invalidCommand, registry.dispatch, Effect.tap((result) => Effect.sync(() => assertValidationError(result)))); }); it.effect('should handle unknown commands', () => { const createUserCommand = defineCommand('CreateUser', UserPayload); const commands = [createUserCommand]; const commandMatcher = (command) => pipe(command, Match.value, Match.when({ name: 'CreateUser' }, () => Effect.succeed({ _tag: 'Success', position: { streamId: 'user-123', eventNumber: 1 }, })), Match.exhaustive); const registry = makeCommandRegistry(commands, commandMatcher); const unknownCommand = { id: 'cmd-123', target: 'user-456', name: 'UnknownCommand', payload: {}, }; const assertUnknownCommandErrorDetails = (error) => pipe(error, Match.value, Match.tag('ValidationError', (validationError) => { expect(validationError.commandName).toBe('UnknownCommand'); expect(validationError.validationErrors.length).toBeGreaterThan(0); }), Match.orElse(() => { expect(true).toBe(false); })); const assertUnknownCommand = (result) => pipe(result, Match.value, Match.tag('Success', () => { expect(true).toBe(false); }), Match.tag('Failure', (failure) => assertUnknownCommandErrorDetails(failure.error)), Match.exhaustive); return pipe(unknownCommand, registry.dispatch, Effect.tap((result) => Effect.sync(() => assertUnknownCommand(result)))); }); it.effect('should handle command execution errors', () => { const createUserCommand = defineCommand('CreateUser', UserPayload); const commands = [createUserCommand]; const commandMatcher = (command) => pipe(command, Match.value, Match.when({ name: 'CreateUser' }, () => Effect.die(new Error('Something went wrong'))), Match.exhaustive); const registry = makeCommandRegistry(commands, commandMatcher); const wireCommand = { id: 'cmd-123', target: 'user-456', name: 'CreateUser', payload: { email: 'test@example.com', name: 'John Doe', }, }; const assertUnknownErrorDetails = (error) => pipe(error, Match.value, Match.tag('UnknownError', () => { expect(true).toBe(true); }), Match.orElse(() => { expect(true).toBe(false); })); const assertUnknownError = (result) => pipe(result, Match.value, Match.tag('Success', () => { expect(true).toBe(false); }), Match.tag('Failure', (failure) => assertUnknownErrorDetails(failure.error)), Match.exhaustive); return pipe(wireCommand, registry.dispatch, Effect.tap((result) => Effect.sync(() => assertUnknownError(result)))); }); it.effect('should support multiple command types', () => { const UpdateEmailPayload = Schema.Struct({ newEmail: pipe(Schema.String, Schema.pattern(/^[^\s@]+@[^\s@]+\.[^\s@]+$/)), }); const createUserCommand = defineCommand('CreateUser', UserPayload); const updateEmailCommand = defineCommand('UpdateEmail', UpdateEmailPayload); const commands = [createUserCommand, updateEmailCommand]; const commandMatcher = (command) => pipe(command, Match.value, Match.when({ name: 'CreateUser' }, () => Effect.succeed({ _tag: 'Success', position: { streamId: 'user-123', eventNumber: 1 }, })), Match.when({ name: 'UpdateEmail' }, () => Effect.succeed({ _tag: 'Success', position: { streamId: 'user-123', eventNumber: 2 }, })), Match.exhaustive); const registry = makeCommandRegistry(commands, commandMatcher); // Test both commands work const createCommand = { id: 'cmd-1', target: 'user-456', name: 'CreateUser', payload: { email: 'test@example.com', name: 'John Doe' }, }; const updateCommand = { id: 'cmd-2', target: 'user-456', name: 'UpdateEmail', payload: { newEmail: 'new@example.com' }, }; const assertSuccess0 = (result) => pipe(result, Match.value, Match.tag('Success', () => { expect(true).toBe(true); }), Match.tag('Failure', () => { expect(true).toBe(false); }), Match.exhaustive); const assertSuccess1 = (result) => pipe(result, Match.value, Match.tag('Success', () => { expect(true).toBe(true); }), Match.tag('Failure', () => { expect(true).toBe(false); }), Match.exhaustive); return pipe([registry.dispatch(createCommand), registry.dispatch(updateCommand)], Effect.all, Effect.tap((results) => Effect.sync(() => { assertSuccess0(results[0]); assertSuccess1(results[1]); }))); }); }); //# sourceMappingURL=command-registry.test.js.map