Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

DT-2902 graphql demo #16

Merged
merged 7 commits into from
Dec 9, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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