Skip to content

Instantly share code, notes, and snippets.

@mikecann
Last active September 11, 2018 09:10
Show Gist options
  • Save mikecann/c5aeabf619312bfb670efc3a3fbaced0 to your computer and use it in GitHub Desktop.
Save mikecann/c5aeabf619312bfb670efc3a3fbaced0 to your computer and use it in GitHub Desktop.
Attempt at Bravent typings, see BraventCounter.ts for what the user-code actually looks like
declare module "bravent" {
import { Validation } from "data.validation";
// ---- Events ------ //
type Event<TEventType extends string, TPayload extends object> = {
[P in keyof TPayload]: TPayload[P]
} & {
type: TEventType;
};
type EventDefinitions<TEventPayload extends object> = {
[eventName: string]: TEventPayload;
};
type EventFromDefinition<
TEventDefinitions extends EventDefinitions<object>,
T extends keyof TEventDefinitions
> = T extends keyof TEventDefinitions
? T extends string ? Event<T, TEventDefinitions[T]> : never
: never;
type EventsInDefinition<TEventDefinitions extends EventDefinitions<object>> = EventFromDefinition<
TEventDefinitions,
keyof TEventDefinitions
>;
type ExtractEventDefinitions<T> = T extends AggregateClass<any, infer X, infer Y> ? X : never;
type EventsFromAggregateClass<T> = EventsInDefinition<ExtractEventDefinitions<T>>;
// ---- Commands ------ //
type Command<TCommandType extends string, TPayload extends object> = {
[P in keyof TPayload]: TPayload[P]
} & {
type: TCommandType;
};
type CommandDefinitions<TCommandPayload extends object> = {
[eventName: string]: TCommandPayload;
};
type CommandFromDefinition<
TCommandDefinitions extends CommandDefinitions<object>,
T extends keyof TCommandDefinitions
> = T extends keyof TCommandDefinitions
? T extends string ? Command<T, TCommandDefinitions[T]> : never
: never;
type CommandsInDefinition<
TCommandDefinitions extends CommandDefinitions<object>
> = CommandFromDefinition<TCommandDefinitions, keyof TCommandDefinitions>;
type ExtractCommandDefinitions<T> = T extends AggregateClass<any, infer X, infer Y> ? Y : never;
type AnyCommandFromAggregateClass<T> = CommandsInDefinition<ExtractCommandDefinitions<T>>;
// ---- Handlers ------ //
type CommandHandlerReturn<TEventDefinitions extends CommandDefinitions<object>> =
| EventsInDefinition<TEventDefinitions>[]
| Validation;
type EventHandlers<TState, TEventDefinitions extends EventDefinitions<object>> = {
[P in keyof TEventDefinitions]: (
state: TState,
event: Event<string, TEventDefinitions[P]>
) => TState
};
type CommandHandlers<
TState,
TCommandDefinitions extends CommandDefinitions<object>,
TEventDefinitions extends EventDefinitions<object>
> = {
[P in keyof TCommandDefinitions]: (
state: TState,
command: Command<string, TCommandDefinitions[P]>
) => CommandHandlerReturn<TEventDefinitions>
};
// ---- Aggregate ------ //
type AnyAggregateClass<T> = AggregateClass<
any,
ExtractEventDefinitions<T>,
ExtractCommandDefinitions<T>
>;
type AnyAggregateInstance<T> = AggregateInstance<
any,
ExtractEventDefinitions<T>,
ExtractCommandDefinitions<T>
>;
type AggregateDefinition<
TState,
TEventDefinitions extends EventDefinitions<object>,
TCommandDefinitions extends CommandDefinitions<object>
> = {
initialState?: TState;
eventHandlers: EventHandlers<TState, TEventDefinitions>;
commandHandlers: CommandHandlers<TState, TCommandDefinitions, TEventDefinitions>;
};
type OnDispatchSuccessHandlerForAggregateClass<T> = OnDispatchSuccessHandler<
ExtractEventDefinitions<T>
>;
type OnDispatchSuccessHandler<TEventDefinitions extends EventDefinitions<object>> = (
newEvents: EventsInDefinition<TEventDefinitions>[]
) => void;
type OnDispatchFailureHandler = (error: any) => {};
type AggregateInstance<
TState,
TEventDefinitions extends EventDefinitions<object>,
TCommandDefinitions extends CommandDefinitions<object>
> = {
dispatch: (
command: CommandsInDefinition<TCommandDefinitions>,
onSuccess?: OnDispatchSuccessHandler<TEventDefinitions>,
onFailure?: OnDispatchFailureHandler
) => AggregateInstance<TState, TEventDefinitions, TCommandDefinitions>;
state: () => TState;
};
type AggregateClass<
TState,
TEventDefinitions extends EventDefinitions<object>,
TCommandDefinitions extends CommandDefinitions<object>
> = {
of: (
events: EventsInDefinition<TEventDefinitions>[]
) => AggregateInstance<TState, TEventDefinitions, TCommandDefinitions>;
};
export function defineAggregate<
TState,
TEventDefinitions extends EventDefinitions<object>,
TCommandDefinitions extends CommandDefinitions<object>
>(
definition: AggregateDefinition<TState, TEventDefinitions, TCommandDefinitions>
): AggregateClass<TState, TEventDefinitions, TCommandDefinitions>;
}
import { BraventCounter, Events } from "./BraventCounter";
import { AnyEvent } from "bravent";
it("updates to the correct state when commands are issued", () => {
let counter = BraventCounter.of([]);
expect(counter.state()).toBe(0);
counter = counter.dispatch({ type: "increment" });
counter = counter.dispatch({ type: "increment" });
expect(counter.state()).toBe(2);
counter = counter.dispatch({ type: "decrement" });
expect(counter.state()).toBe(1);
counter = counter.dispatch({ type: "setCount", count: 123 });
expect(counter.state()).toBe(123);
});
it("returns the corect events", () => {
let counter = BraventCounter.of([]);
let events: AnyEvent<Events>[] = [];
counter.dispatch({ type: "increment" }, e => (events = e));
expect(events.length).toBe(1);
expect(events[0].type).toBe("incremented");
});
it("can fail", () => {
let counter = BraventCounter.of([]);
let errors: any;
counter.dispatch({ type: "error" }, events => {}, e => (errors = e));
expect(errors.length).toBe(1);
expect(errors[0]).toBe("oops");
});
it("can fail if the command doesnt exist", () => {
let counter = BraventCounter.of([]);
let errors: any;
counter.dispatch({ type: "blabla" } as any, events => {}, e => (errors = e));
expect(errors.length).toBe(1);
expect(errors[0]).toBe("Cannot handle command of type 'blabla'.");
});
import { defineAggregate, Event, Command } from "bravent";
import { Failure } from "data.validation";
export type State = number;
export type Events = {
incremented: {};
decremented: {};
countSet: { count: number };
};
export type Commands = {
increment: Events["incremented"];
decrement: Events["decremented"];
setCount: Events["countSet"];
error: {};
};
export const BraventCounter = defineAggregate<State, Events, Commands>({
initialState: 0,
eventHandlers: {
incremented: (state, event) => state + 1,
decremented: (state, event) => state - 1,
countSet: (state, event) => event.count
},
commandHandlers: {
increment: (state, command) => [{ type: "incremented" }],
decrement: (state, command) => [{ type: "decremented" }],
setCount: (state, command) => [{ type: "countSet", count: command.count }],
error: (state, command) => Failure(["oops"])
}
});
declare module "data.validation" {
export class Validation {
value: string[]
}
export const Failure: (errors: string[]) => Validation;
export const Success: () => Validation;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment