Skip to content

Instantly share code, notes, and snippets.

@fvilante
Created October 14, 2020 07:48
Show Gist options
  • Save fvilante/fe1341e0ae21093011961643dc6e250d to your computer and use it in GitHub Desktop.
Save fvilante/fe1341e0ae21093011961643dc6e250d to your computer and use it in GitHub Desktop.
Aggregate Full-Typed
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