diff --git a/src/read-models/equipment/get-all.ts b/src/read-models/equipment/get-all.ts new file mode 100644 index 0000000..43fa837 --- /dev/null +++ b/src/read-models/equipment/get-all.ts @@ -0,0 +1,38 @@ +import {pipe} from 'fp-ts/lib/function'; +import * as O from 'fp-ts/Option'; +import {DomainEvent, isEventOfType} from '../../types'; +import * as RA from 'fp-ts/ReadonlyArray'; +import {readModels} from '..'; +import {UUID} from 'io-ts-types'; + +type Equipment = { + name: string; + id: UUID; + areaId: UUID; + areaName: string; + trainingSheetId: O.Option; +}; + +export const getAll = ( + events: ReadonlyArray +): ReadonlyArray => + pipe( + events, + RA.filter(isEventOfType('EquipmentAdded')), + RA.map(equipment => + pipe( + readModels.areas.getArea(events)(equipment.areaId), + O.map(area => ({ + ...equipment, + areaName: area.name, + })), + O.map(equipmentData => ({ + ...equipmentData, + trainingSheetId: readModels.equipment.getTrainingSheetId(events)( + equipmentData.id + ), + })) + ) + ), + RA.compact + ); diff --git a/src/read-models/equipment/get-for-area.ts b/src/read-models/equipment/get-for-area.ts new file mode 100644 index 0000000..2360973 --- /dev/null +++ b/src/read-models/equipment/get-for-area.ts @@ -0,0 +1,19 @@ +import {pipe} from 'fp-ts/lib/function'; +import {DomainEvent, isEventOfType} from '../../types'; +import * as RA from 'fp-ts/ReadonlyArray'; +import {UUID} from 'io-ts-types'; + +type Equipment = { + name: string; + id: UUID; + areaId: UUID; +}; + +export const getForArea = + (events: ReadonlyArray) => + (areaId: string): ReadonlyArray => + pipe( + events, + RA.filter(isEventOfType('EquipmentAdded')), + RA.filter(event => event.areaId === areaId) + ); diff --git a/src/read-models/equipment/get-trained-on.ts b/src/read-models/equipment/get-trained-on.ts new file mode 100644 index 0000000..cc6744a --- /dev/null +++ b/src/read-models/equipment/get-trained-on.ts @@ -0,0 +1,45 @@ +import {DomainEvent} from '../../types'; + +type TrainedInfo = { + when: Date; + by: number | null; + prev: ReadonlyArray<{ + when: Date; + by: number | null; + }>; +}; + +export const getMembersTrainedOn = + (equipmentId: string) => + (events: ReadonlyArray): ReadonlyMap => { + // Note that currently you cannot revoke training. + + const trained: Map = new Map(); + for (const event of events) { + if ( + event.type !== 'MemberTrainedOnEquipment' || + event.equipmentId !== equipmentId + ) { + continue; + } + const current = trained.get(event.memberNumber); + if (current) { + trained.set(event.memberNumber, { + when: event.recordedAt, + by: event.trainedByMemberNumber, + prev: current.prev.concat([ + { + when: current.when, + by: current.by, + }, + ]), + }); + } + trained.set(event.memberNumber, { + when: event.recordedAt, + by: event.trainedByMemberNumber, + prev: [], + }); + } + return trained; + }; diff --git a/src/read-models/equipment/get-training-quiz-results.ts b/src/read-models/equipment/get-training-quiz-results.ts new file mode 100644 index 0000000..89aaaf2 --- /dev/null +++ b/src/read-models/equipment/get-training-quiz-results.ts @@ -0,0 +1,17 @@ +import {pipe} from 'fp-ts/lib/function'; +import * as RA from 'fp-ts/ReadonlyArray'; +import {DomainEvent, isEventOfType} from '../../types'; +import {EventOfType} from '../../types/domain-event'; + +export const getTrainingQuizResults = + (events: ReadonlyArray) => + ( + equipmentId: string + ): ReadonlyArray> => + pipe( + events, + RA.filter(isEventOfType('EquipmentTrainingQuizResult')), + RA.filter(event => { + return event.equipmentId === equipmentId; + }) + ); diff --git a/src/read-models/equipment/get-training-sheet-id.ts b/src/read-models/equipment/get-training-sheet-id.ts new file mode 100644 index 0000000..c8e8fb4 --- /dev/null +++ b/src/read-models/equipment/get-training-sheet-id.ts @@ -0,0 +1,18 @@ +import {pipe} from 'fp-ts/lib/function'; +import * as RA from 'fp-ts/ReadonlyArray'; +import * as O from 'fp-ts/Option'; +import {DomainEvent, isEventOfType} from '../../types'; + +export const getTrainingSheetId = + (events: ReadonlyArray) => + (equipmentId: string): O.Option => + pipe( + events, + RA.filter(isEventOfType('EquipmentTrainingSheetRegistered')), + RA.filter(event => event.equipmentId === equipmentId), + RA.last, + O.match( + () => O.none, + mostRecentSheet => O.some(mostRecentSheet.trainingSheetId) + ) + ); diff --git a/src/read-models/equipment/get.ts b/src/read-models/equipment/get.ts new file mode 100644 index 0000000..e19bba9 --- /dev/null +++ b/src/read-models/equipment/get.ts @@ -0,0 +1,90 @@ +import {pipe} from 'fp-ts/lib/function'; +import * as RM from 'fp-ts/ReadonlyMap'; +import * as O from 'fp-ts/Option'; +import {DomainEvent, SubsetOfDomainEvent, filterByName} from '../../types'; +import * as RA from 'fp-ts/ReadonlyArray'; +import {EventName, isEventOfType} from '../../types/domain-event'; +import {Eq as stringEq} from 'fp-ts/string'; +import {UUID} from 'io-ts-types'; + +export type Equipment = { + name: string; + id: UUID; + trainers: ReadonlyArray; + areaId: UUID; + trainedMembers: ReadonlyArray; + trainingSheetId: O.Option; +}; + +type EquipmentState = { + name: string; + id: UUID; + areaId: UUID; + trainers: Set; + trainedMembers: Set; + trainingSheetId?: string; +}; + +const pertinentEvents: Array = [ + 'EquipmentAdded', + 'TrainerAdded', + 'MemberTrainedOnEquipment', + 'EquipmentTrainingSheetRegistered', +]; + +const updateState = ( + state: Map, + event: SubsetOfDomainEvent +) => { + if (isEventOfType('EquipmentAdded')(event)) { + state.set(event.id, { + ...event, + trainers: new Set(), + trainedMembers: new Set(), + }); + } + if (isEventOfType('TrainerAdded')(event)) { + const equipment = state.get(event.equipmentId); + if (equipment) { + state.set(event.equipmentId, { + ...equipment, + trainers: equipment.trainers.add(event.memberNumber), + }); + } + } + if (isEventOfType('MemberTrainedOnEquipment')(event)) { + const equipment = state.get(event.equipmentId); + if (equipment) { + state.set(event.equipmentId, { + ...equipment, + trainedMembers: equipment.trainedMembers.add(event.memberNumber), + }); + } + } + if (isEventOfType('EquipmentTrainingSheetRegistered')(event)) { + const equipment = state.get(event.equipmentId); + if (equipment) { + state.set(event.equipmentId, { + ...equipment, + trainingSheetId: event.trainingSheetId, + }); + } + } + return state; +}; + +export const get = + (events: ReadonlyArray) => + (equipmentId: string): O.Option => + pipe( + events, + filterByName(pertinentEvents), + RA.reduce(new Map(), updateState), + RM.lookup(stringEq)(equipmentId), // TODO - Do updateState lazily based on what is looked up. + O.map(state => ({ + ...state, + trainingSheetId: O.fromNullable(state.trainingSheetId), + trainers: Array.from(state.trainers.values()), + trainedMembers: Array.from(state.trainedMembers.values()), + })) + ); diff --git a/src/read-models/equipment/index.ts b/src/read-models/equipment/index.ts new file mode 100644 index 0000000..d0207ec --- /dev/null +++ b/src/read-models/equipment/index.ts @@ -0,0 +1,13 @@ +import {get} from './get'; +import {getAll} from './get-all'; +import {getForArea} from './get-for-area'; +import {getTrainingQuizResults} from './get-training-quiz-results'; +import {getTrainingSheetId} from './get-training-sheet-id'; + +export const equipment = { + get, + getAll, + getForArea, + getTrainingSheetId, + getTrainingQuizResults, +};