diff --git a/src/queries/equipment/construct-view-model.ts b/src/queries/equipment/construct-view-model.ts new file mode 100644 index 00000000..dd1d1885 --- /dev/null +++ b/src/queries/equipment/construct-view-model.ts @@ -0,0 +1,34 @@ +import {pipe} from 'fp-ts/lib/function'; +import * as E from 'fp-ts/Either'; +import {Dependencies} from '../../dependencies'; +import * as TE from 'fp-ts/TaskEither'; +import {readModels} from '../../read-models'; +import { + FailureWithStatus, + failureWithStatus, +} from '../../types/failureWithStatus'; +import {ViewModel} from './view-model'; +import {User} from '../../types'; +import {StatusCodes} from 'http-status-codes'; +import {sequenceS} from 'fp-ts/lib/Apply'; + +export type Params = {equipmentId: string; user: User}; + +export const constructViewModel = + (deps: Dependencies) => + (params: Params): TE.TaskEither => + pipe( + deps.getAllEvents(), + TE.map(events => ({ + user: E.right(params.user), + name: pipe( + params.equipmentId, + readModels.equipment.get(events), + E.fromOption(() => + failureWithStatus('No such equipment', StatusCodes.NOT_FOUND)() + ), + E.map(equipment => equipment.name) + ), + })), + TE.chainEitherK(sequenceS(E.Apply)) + ); diff --git a/src/queries/equipment/index.ts b/src/queries/equipment/index.ts new file mode 100644 index 00000000..450224e8 --- /dev/null +++ b/src/queries/equipment/index.ts @@ -0,0 +1,61 @@ +import {Request, Response} from 'express'; +import * as t from 'io-ts'; +import {flow, pipe} from 'fp-ts/lib/function'; +import * as TE from 'fp-ts/TaskEither'; +import {Dependencies} from '../../dependencies'; +import {getUserFromSession} from '../../authentication'; +import {logInRoute} from '../../authentication/configure-auth-routes'; +import { + FailureWithStatus, + failureWithStatus, +} from '../../types/failureWithStatus'; +import {StatusCodes} from 'http-status-codes'; +import {constructViewModel} from './construct-view-model'; +import {Params} from './construct-view-model'; +import {render} from './render'; +import * as E from 'fp-ts/Either'; +import {sequenceS} from 'fp-ts/lib/Apply'; +import {formatValidationErrors} from 'io-ts-reporters'; + +const notLoggedIn = () => + failureWithStatus('You are not logged in.', StatusCodes.UNAUTHORIZED)(); + +const invalidParams = flow( + formatValidationErrors, + failureWithStatus('Invalid request parameters', StatusCodes.BAD_REQUEST) +); + +const getParams = + (deps: Dependencies) => + (req: Request): E.Either => + pipe( + { + user: pipe( + req.session, + getUserFromSession(deps), + E.fromOption(notLoggedIn) + ), + equipmentId: pipe( + req.params, + t.strict({equipment: t.string}).decode, + E.mapLeft(invalidParams), + E.map(params => params.equipment) + ), + }, + sequenceS(E.Apply) + ); + +export const equipment = + (deps: Dependencies) => async (req: Request, res: Response) => { + await pipe( + req, + getParams(deps), + TE.fromEither, + TE.chain(constructViewModel(deps)), + TE.map(render), + TE.matchW( + () => res.redirect(logInRoute), + page => res.status(200).send(page) + ) + )(); + }; diff --git a/src/queries/equipment/render.ts b/src/queries/equipment/render.ts new file mode 100644 index 00000000..8322a09a --- /dev/null +++ b/src/queries/equipment/render.ts @@ -0,0 +1,11 @@ +import {pipe} from 'fp-ts/lib/function'; +import {pageTemplate} from '../../templates'; +import {html} from '../../types/html'; +import * as O from 'fp-ts/Option'; +import {ViewModel} from './view-model'; + +export const render = (viewModel: ViewModel) => + pipe( + html`

${viewModel.name}

`, + pageTemplate(viewModel.name, O.some(viewModel.user)) + ); diff --git a/src/queries/equipment/view-model.ts b/src/queries/equipment/view-model.ts new file mode 100644 index 00000000..6e52bc4b --- /dev/null +++ b/src/queries/equipment/view-model.ts @@ -0,0 +1,6 @@ +import {User} from '../../types'; + +export type ViewModel = { + user: User; + name: string; +}; diff --git a/src/queries/index.ts b/src/queries/index.ts index 3a4653ec..49b6357e 100644 --- a/src/queries/index.ts +++ b/src/queries/index.ts @@ -5,11 +5,13 @@ import {superUsers} from './super-users'; import asyncHandler from 'express-async-handler'; import {area} from './area'; import {allEquipment} from './all-equipment'; +import {equipment} from './equipment'; export const queries = { allEquipment: flow(allEquipment, asyncHandler), areas: flow(areas, asyncHandler), area: flow(area, asyncHandler), + equipment: flow(equipment, asyncHandler), superUsers: flow(superUsers, asyncHandler), landing: flow(landing, asyncHandler), }; diff --git a/src/read-models/equipment/get.ts b/src/read-models/equipment/get.ts new file mode 100644 index 00000000..0317894b --- /dev/null +++ b/src/read-models/equipment/get.ts @@ -0,0 +1,18 @@ +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'; + +type Equipment = { + name: string; +}; + +export const get = + (events: ReadonlyArray) => + (equipmentId: string): O.Option => + pipe( + events, + RA.filter(isEventOfType('EquipmentAdded')), + RA.filter(event => event.id === equipmentId), + RA.head + ); diff --git a/src/read-models/equipment/index.ts b/src/read-models/equipment/index.ts index 71577643..812e8700 100644 --- a/src/read-models/equipment/index.ts +++ b/src/read-models/equipment/index.ts @@ -1,7 +1,9 @@ +import {get} from './get'; import {getAll} from './get-all'; import {getForArea} from './get-for-area'; export const equipment = { + get, getAll, getForArea, }; diff --git a/src/router.ts b/src/router.ts index bb4d41d3..e9210ca0 100644 --- a/src/router.ts +++ b/src/router.ts @@ -40,6 +40,7 @@ export const createRouter = (deps: Dependencies, conf: Config): Router => { '/equipment/add', http.formPost(deps, commands.equipment.add, '/equipment') ); + router.get('/equipment/:equipment', queries.equipment(deps)); router.get('/super-users', queries.superUsers(deps)); router.get(