Created
October 14, 2020 07:48
-
-
Save fvilante/fe1341e0ae21093011961643dc6e250d to your computer and use it in GitHub Desktop.
Aggregate Full-Typed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
type Message<N extends string = string, D = unknown> = { name: N, data: D } | |
type Context<K0 extends string = string, K1 extends string = string, > = { | |
contextName: K0 | |
aggregateName: K1 | |
aggregateId: string | |
} | |
interface Command<N extends string = string, D = unknown> extends Message<N,D> { } | |
export interface Event<N extends string = string, D = unknown> extends Message<N,D> { } | |
type Id = { id: string } | |
// An Action describes a successful Aggregation State transtion. | |
// Note: One command can produce zero or more Events | |
type Action<TCommand extends Command = Command, TEvents extends Event[] = Event[]> = { | |
command: TCommand, | |
events: TEvents | |
} | |
type InferActions<As extends Action[]> = { | |
AllCmdNames: As[number]['command']['name'] | |
AllEventNames: As[number]['events'][number]['name'] | |
GetCmdByCmdName: { | |
[N in InferActions<As>['AllCmdNames']]: Extract<As[number]['command'], Command<N,unknown>> | |
} | |
GetEventByEventName: { | |
[N in InferActions<As>['AllEventNames']]: Extract<As[number]['events'][number], Event<N,unknown>> | |
} | |
GetActionByCmdName: { | |
[N in InferActions<As>['AllCmdNames']]: Extract<As[number], Action<Command<N,unknown>, Event[]>> | |
} | |
} | |
//just type aliases | |
type GetCmdNames<As extends Action[]> = InferActions<As>['AllCmdNames'] | |
type GetEventNames<As extends Action[]> = InferActions<As>['AllEventNames'] | |
type GetActionByCmdName<As extends Action[],N extends string> = InferActions<As>['GetActionByCmdName'][N] | |
type GetEventsByEventName<As extends Action[],N extends string> = InferActions<As>['GetEventByEventName'][N] | |
// produces an event E from a command C using state S, or throw an Error | |
type CommandDefinition<Ctx extends Context, S, A extends Action> = { | |
isAuthorized?: (command: A['command']) => boolean | Promise<boolean> | |
handler: (command: A['command'], stateAndContext: Ctx & S) => A['events'], | |
} | |
// reduce internal state S using an Event E | |
type EventDefinition<Ctx extends Context, S, E extends Event,> = (state: S, event: Ctx & E & Id ) => S | |
// every aggragate should define an InitialAction responsible to initialize it. | |
type InitialAction<S> = Action<Command<'Initialize',S>,[Event<'Initialized',S>]> | |
type Aggregate<Ctx extends Context, S, As extends [InitialAction<S>, ...Action[]]> = { | |
commands: { | |
[N in GetCmdNames<As>]: CommandDefinition< Ctx, S, GetActionByCmdName<As,N> > | |
} | |
events: { | |
[K in GetEventNames<As>]: EventDefinition< Ctx, S, GetEventsByEventName<As,K> > | |
} | |
} | |
// ============ | |
// just a helper to construct an Aggregate type | |
type AggregateConstructorArgument<K0 extends string = string, K1 extends string = string, S = unknown, As extends [InitialAction<S>, ...Action[]] = [InitialAction<S>, ...Action[]]> = { | |
contextName: K0 | |
aggregateContext: K1, | |
state: S | |
actions: As | |
} | |
type MakeAggregate<A extends AggregateConstructorArgument> = | |
Aggregate<Context<A['contextName'],A['aggregateContext']>,A['state'], A['actions']> | |
// ==================== | |
// EXAMPLE: Concrete of Aggregate construction | |
// example of Aggregate type construction | |
type Item = { | |
itemId: string | |
title: string | |
} | |
type ShoppingCartState = { | |
isPlaced: boolean | |
items: Item[] | |
} | |
const initialState: ShoppingCartState = { | |
isPlaced: false, | |
items: [] | |
} | |
type Cart = MakeAggregate<{ | |
contextName: 'shopping', | |
aggregateContext: 'cart', | |
state: ShoppingCartState, | |
actions: [ | |
{ | |
command: { name: 'Initialize', data: ShoppingCartState }, | |
events: [ {name: 'Initialized', data: ShoppingCartState } ] | |
}, | |
{ | |
command: { name: 'AddItem', data: Item }, | |
events: [ {name: 'ItemAdded', data: Item} ] | |
}, | |
{ | |
command: { name: 'RemoveItem', data: Item['itemId'] }, | |
events: [ {name: 'ItemRemoved', data: Item['itemId'] } ] | |
}, | |
{ | |
command: { name: 'PlaceOrder', data: { isPlaced: true } }, | |
events: [ {name: 'OrderPlaced', data: { isPlaced: true }} ] | |
}, | |
]}> | |
const Cart: Cart = { | |
commands: { | |
Initialize: { handler: (command, state) => [{ name: 'Initialized', data: initialState }] }, | |
AddItem: { | |
handler: (command, state) => [{ name: 'ItemAdded', data: command.data }], | |
isAuthorized: command => true | |
}, | |
RemoveItem: { handler: (command, state) => [{ name: 'ItemRemoved', data: command.data }] }, | |
PlaceOrder: { handler: (command, state) => [{ name: 'OrderPlaced', data: command.data }] }, | |
}, | |
events: { | |
Initialized: (state, event) => event.data, | |
ItemAdded: (state, event) => ({...state, items: [...state.items, event.data]}), | |
ItemRemoved: (state, event) => ({...state, items: [...state.items.filter( it => it.itemId !== event.data)]}), | |
OrderPlaced: (state, event) => ({...state, isPlaced: event.data.isPlaced}), | |
} | |
} | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment