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