Skip to content

Commit

Permalink
DT-2902 graphql demo (#16)
Browse files Browse the repository at this point in the history
* DT-2902 Basic plumbing for a demo page for GraphQL
  • Loading branch information
andymarke committed Dec 9, 2021
1 parent 2182278 commit 6fea21e
Show file tree
Hide file tree
Showing 22 changed files with 617 additions and 15 deletions.
1 change: 1 addition & 0 deletions assets/sass/application.sass
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,5 @@ $path: "/assets/images/"
@import 'moj/all'

@import './components/header-bar'
@import './components/card'
@import './local'
100 changes: 100 additions & 0 deletions assets/sass/components/_card.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
/* ==========================================================================
COMPONENTS / #CARD
========================================================================== */

$card-border-width: 1px;
$card-border-bottom-width: govuk-spacing(1);
$card-border-hover-color: $govuk-border-colour;
$card-border-color: lighten($card-border-hover-color, 15%);

.card {
margin-bottom: govuk-spacing(7);
background: $govuk-body-background-colour;
border: $card-border-width solid $card-border-color;
position: relative;
width: 100%;
padding: govuk-spacing(5);

&__heading {
margin-top: 0;
margin-bottom: govuk-spacing(3);
}

&__description {
margin-bottom: 0;
}

/* Clickable card
========================================================================== */
&--clickable {
border-bottom-width: $card-border-bottom-width;

&:hover,
&:active {
cursor: pointer;

.card__heading a,
.card__link {
color: $govuk-link-hover-colour;
text-decoration: none;

&:focus {
@include govuk-focused-text;
}
}
}

&:hover {
border-color: $card-border-hover-color;
}

&:active {
border-color: $card-border-hover-color;
bottom: -$card-border-width;
}
}
}

/* Card group
========================================================================== */

/**
* Card group allows you to have a row of cards.
*
* Flexbox is used to make each card in a row the same height.
*/

.card-group {
display: flex;
flex-wrap: wrap;
margin-bottom: govuk-spacing(3);
padding: 0;

@include govuk-media-query($until: desktop) {
margin-bottom: govuk-spacing(6);
}

&__item {
display: flex;
list-style-type: none;
margin-bottom: 0;

@include govuk-media-query($until: desktop) {
flex: 0 0 100%;
}

.card {
margin-bottom: govuk-spacing(5);
}

@include govuk-media-query($until: desktop) {
.card {
margin-bottom: govuk-spacing(3);
}

&:last-child .card {
margin-bottom: 0;
}
}
}
}
1 change: 1 addition & 0 deletions helm_deploy/values-dev.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ generic-service:
INGRESS_URL: "https://hmpps-audit-poc-ui-dev.hmpps.service.justice.gov.uk"
HMPPS_AUTH_URL: "https://sign-in-dev.hmpps.service.justice.gov.uk/auth"
TOKEN_VERIFICATION_API_URL: "https://token-verification-api-dev.prison.service.justice.gov.uk"
GRAPHQL_API_URL: "https://gql-api-dev.hmpps.service.justice.gov.uk"

generic-prometheus-alerts:
alertSeverity: digital-prison-service-dev
Expand Down
4 changes: 1 addition & 3 deletions integration_tests/pages/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,8 @@ import Page, { PageElement } from './page'

export default class IndexPage extends Page {
constructor() {
super('This site is under construction...')
super('Sample Application')
}

headerUserName = (): PageElement => cy.get('[data-qa=header-user-name]')

courtRegisterLink = (): PageElement => cy.get('[href="/court-register"]')
}
9 changes: 9 additions & 0 deletions server/@types/express/index.d.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,17 @@
export default {}

export type RequestData = 'basicDetails' | 'sentences' | 'offences' | 'offendermanagers'
export interface PrisonerSearchForm {
lastName?: string
prisonerNumber?: string
data?: Array<RequestData>
}
declare module 'express-session' {
// Declare that the session will potentially contain these additional fields
interface SessionData {
returnTo: string
nowInMinutes: number
prisonerSearchForm: PrisonerSearchForm
}
}

Expand All @@ -18,6 +25,8 @@ export declare global {

interface Request {
verified?: boolean
flash(type: string, message: Array<Record<string, string>>): number
flash(message: 'errors'): Array<Record<string, string>>
}
}
}
8 changes: 6 additions & 2 deletions server/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,12 @@ import setUpAuthentication from './middleware/setUpAuthentication'
import setUpHealthChecks from './middleware/setUpHealthChecks'
import setUpWebRequestParsing from './middleware/setupRequestParsing'
import authorisationMiddleware from './middleware/authorisationMiddleware'
import GraphQLDemoService from './services/graphQLDemoService'

export default function createApp(userService: UserService): express.Application {
export default function createApp(
userService: UserService,
graphQLDemoService: GraphQLDemoService
): express.Application {
const app = express()

app.set('json spaces', 2)
Expand All @@ -33,7 +37,7 @@ export default function createApp(userService: UserService): express.Application
app.use(setUpAuthentication())
app.use(authorisationMiddleware())

app.use('/', indexRoutes(standardRouter(userService)))
app.use('/', indexRoutes(standardRouter(userService), { graphQLDemoService }))

app.use((req, res, next) => next(createError(404, 'Not found')))
app.use(errorHandler(process.env.NODE_ENV === 'production'))
Expand Down
8 changes: 8 additions & 0 deletions server/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,14 @@ export default {
agent: new AgentConfig(),
enabled: get('TOKEN_VERIFICATION_ENABLED', 'false') === 'true',
},
graphQLEndpoint: {
url: get('GRAPHQL_API_URL', 'http://localhost:8080', requiredInProduction) as string,
timeout: {
response: Number(get('HMPPS_AUTH_TIMEOUT_RESPONSE', 10000)),
deadline: Number(get('HMPPS_AUTH_TIMEOUT_DEADLINE', 10000)),
},
agent: new AgentConfig(),
},
},
domain: get('INGRESS_URL', 'http://localhost:3000', requiredInProduction),
}
8 changes: 4 additions & 4 deletions server/data/restClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ interface PostRequest {
path?: string
headers?: Record<string, string>
responseType?: string
data?: Record<string, unknown>
data?: Record<string, unknown> | string
raw?: boolean
}

Expand Down Expand Up @@ -68,14 +68,14 @@ export default class RestClient {
}
}

async post({
async post<T>({
path = null,
headers = {},
responseType = '',
data = {},
raw = false,
}: PostRequest = {}): Promise<unknown> {
logger.info(`Post using user credentials: calling ${this.name}: ${path}`)
}: PostRequest = {}): Promise<T> {
logger.info(`Post using user credentials: calling ${this.name}: ${this.apiUrl()}${path}`)
try {
const result = await superagent
.post(`${this.apiUrl()}${path}`)
Expand Down
4 changes: 3 additions & 1 deletion server/index.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import createApp from './app'
import HmppsAuthClient from './data/hmppsAuthClient'
import TokenStore from './data/tokenStore'
import GraphQLDemoService from './services/graphQLDemoService'
import UserService from './services/userService'

const hmppsAuthClient = new HmppsAuthClient(new TokenStore())
const userService = new UserService(hmppsAuthClient)
const graphQLDemoService = new GraphQLDemoService(hmppsAuthClient)

const app = createApp(userService)
const app = createApp(userService, graphQLDemoService)

export default app
35 changes: 35 additions & 0 deletions server/routes/graphql/graphQLDemoController.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { Request, Response } from 'express'

import GraphQLDemoService from '../../services/graphQLDemoService'

export default class GraphqlDemoController {
constructor(private readonly graphQLDemoService: GraphQLDemoService) {}

async demo(req: Request, res: Response): Promise<void> {
const form = req.session.prisonerSearchForm || {}
const errors = req.flash('errors') || []
res.render('pages/graphql/demo', { form, errors })
}

async details(req: Request, res: Response): Promise<void> {
const { id } = req.query as { id: string }
const form = req.session.prisonerSearchForm || {}
req.session.prisonerSearchForm = { ...form, prisonerNumber: id }
const prisoners = await this.graphQLDemoService.search({}, req.session.prisonerSearchForm)
res.render('pages/graphql/prisoner-details', { data: JSON.stringify(prisoners), prisoner: prisoners[0] })
}

async search(req: Request, res: Response): Promise<void> {
req.session.prisonerSearchForm = { ...req.body }
const prisoners = await this.graphQLDemoService.search({}, req.session.prisonerSearchForm)

if (prisoners.length > 1) {
res.render('pages/graphql/search-results', { data: JSON.stringify(prisoners), prisoners })
} else if (prisoners.length === 1) {
res.render('pages/graphql/prisoner-details', { data: JSON.stringify(prisoners), prisoner: prisoners[0] })
} else {
req.flash('errors', [{ text: 'No prisoners found' }])
res.redirect('/graphql/demo')
}
}
}
21 changes: 21 additions & 0 deletions server/routes/graphql/graphQLRouter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import type { RequestHandler, Router } from 'express'

import asyncMiddleware from '../../middleware/asyncMiddleware'
import GraphQLDemoService from '../../services/graphQLDemoService'
import GraphQLDemoController from './graphQLDemoController'

export interface Services {
graphQLDemoService: GraphQLDemoService
}
export default function routes(router: Router, services: Services): Router {
const get = (path: string, handler: RequestHandler) => router.get(path, asyncMiddleware(handler))
const post = (path: string, handler: RequestHandler) => router.post(path, asyncMiddleware(handler))

const graphqlDemoController = new GraphQLDemoController(services.graphQLDemoService)

get('/graphql/demo', (req, res) => graphqlDemoController.demo(req, res))
get('/graphql/details', (req, res) => graphqlDemoController.details(req, res))
post('/graphql/search', (req, res) => graphqlDemoController.search(req, res))

return router
}
2 changes: 1 addition & 1 deletion server/routes/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ describe('GET /', () => {
.get('/')
.expect('Content-Type', /html/)
.expect(res => {
expect(res.text).toContain('This site is under construction...')
expect(res.text).toContain('Sample Application')
})
})
})
10 changes: 9 additions & 1 deletion server/routes/index.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,21 @@
import type { RequestHandler, Router } from 'express'

import graphQLRoutes from './graphql/graphQLRouter'

import asyncMiddleware from '../middleware/asyncMiddleware'
import GraphQLDemoService from '../services/graphQLDemoService'

export interface Services {
graphQLDemoService: GraphQLDemoService
}

export default function routes(router: Router): Router {
export default function routes(router: Router, services: Services): Router {
const get = (path: string, handler: RequestHandler) => router.get(path, asyncMiddleware(handler))

get('/', (req, res, next) => {
res.render('pages/index')
})
graphQLRoutes(router, services)

return router
}
3 changes: 2 additions & 1 deletion server/routes/testutils/appSetup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import errorHandler from '../../errorHandler'
import standardRouter from '../standardRouter'
import UserService from '../../services/userService'
import * as auth from '../../authentication/auth'
import graphQLDemoService from './mockGraphQLDemoService'

const user = {
name: 'john smith',
Expand Down Expand Up @@ -56,5 +57,5 @@ function appSetup(route: Router, production: boolean): Express {

export default function appWithAllRoutes({ production = false }: { production?: boolean }): Express {
auth.default.authenticationMiddleware = () => (req, res, next) => next()
return appSetup(allRoutes(standardRouter(new MockUserService())), production)
return appSetup(allRoutes(standardRouter(new MockUserService()), { graphQLDemoService }), production)
}
8 changes: 8 additions & 0 deletions server/routes/testutils/mockGraphQLDemoService.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import HmppsAuthClient from '../../data/hmppsAuthClient'
import GraphQLDemoService from '../../services/graphQLDemoService'

jest.mock('../../services/graphQLDemoService')

const graphQLDemoService = new GraphQLDemoService({} as HmppsAuthClient) as jest.Mocked<GraphQLDemoService>

export default graphQLDemoService
Loading

0 comments on commit 6fea21e

Please sign in to comment.