From a81ddb2b5ebdbddb22fcff62adf556e6f59ca1a1 Mon Sep 17 00:00:00 2001 From: George Goodall Date: Thu, 4 Jul 2024 15:06:52 +0100 Subject: [PATCH 1/9] initial commit for alex to review --- package.json | 7 +++--- src/controllers/lpaDetailsController.js | 19 ++++++++++++++++ src/routes/form-wizard/steps.js | 2 ++ src/utils/fetchLocalAuthorities.js | 12 ++++++++++ src/views/lpa-details.html | 29 +++++++++++++++---------- 5 files changed, 54 insertions(+), 15 deletions(-) create mode 100644 src/controllers/lpaDetailsController.js create mode 100644 src/utils/fetchLocalAuthorities.js diff --git a/package.json b/package.json index 25f0ababa..a8908a72d 100644 --- a/package.json +++ b/package.json @@ -20,13 +20,14 @@ "vitest": "^1.6.0" }, "dependencies": { - "@x-govuk/govuk-prototype-components": "^3.0.5", - "@x-govuk/govuk-prototype-filters": "^1.4.0", + "@x-govuk/govuk-prototype-components": "^3.0.4", + "@x-govuk/govuk-prototype-filters": "1.3.1", + "axios": "^1.7.2", "body-parser": "^1.20.2", "cookie-parser": "^1.4.6", "express": "^4.19.2", "express-session": "^1.18.0", - "govuk-frontend": "^5.4.0", + "govuk-frontend": "5.3.0", "hmpo-config": "^3.0.0", "hmpo-form-wizard": "^13.0.0", "hmpo-i18n": "^6.0.1", diff --git a/src/controllers/lpaDetailsController.js b/src/controllers/lpaDetailsController.js new file mode 100644 index 000000000..f9054c6f3 --- /dev/null +++ b/src/controllers/lpaDetailsController.js @@ -0,0 +1,19 @@ +import PageController from './pageController.js' +import fetchLocalAuthorities from '../utils/fetchLocalAuthorities.js' + +class lpaDetailsController extends PageController { + async locals (req, res, next) { + const localAuthorities = await fetchLocalAuthorities() + + const localAuthoritiesNames = localAuthorities.entities.map(lpa => ({ + text: lpa.name, + value: lpa.name + })) + + req.form.options.localAuthorities = localAuthoritiesNames + + super.locals(req, res, next) + } +} + +export default lpaDetailsController diff --git a/src/routes/form-wizard/steps.js b/src/routes/form-wizard/steps.js index b5ec2e101..f98ef8d93 100644 --- a/src/routes/form-wizard/steps.js +++ b/src/routes/form-wizard/steps.js @@ -1,4 +1,5 @@ import chooseDatasetController from '../../controllers/chooseDatasetController.js' +import lpaDetailsController from '../../controllers/lpaDetailsController.js' import PageController from '../../controllers/pageController.js' const defaultParams = { @@ -18,6 +19,7 @@ export default { ...defaultParams, fields: ['lpa', 'name', 'email'], next: 'choose-dataset', + controller: lpaDetailsController, backLink: '/start' }, '/choose-dataset': { diff --git a/src/utils/fetchLocalAuthorities.js b/src/utils/fetchLocalAuthorities.js new file mode 100644 index 000000000..babfeac22 --- /dev/null +++ b/src/utils/fetchLocalAuthorities.js @@ -0,0 +1,12 @@ +import axios from 'axios'; + +export default async () => { + const url = "https://www.planning.data.gov.uk/entity.json?dataset=local-authority&limit=500"; + try { + const response = await axios.get(url); + return response.data; // Return the fetched data + } catch (error) { + console.error('Error fetching local authorities data:', error); + throw error; // Rethrow the error to be handled by the caller + } +} \ No newline at end of file diff --git a/src/views/lpa-details.html b/src/views/lpa-details.html index 11165def0..e9fc0d476 100644 --- a/src/views/lpa-details.html +++ b/src/views/lpa-details.html @@ -2,6 +2,11 @@ {% from "govuk/components/input/macro.njk" import govukInput %} {% from "govuk/components/button/macro.njk" import govukButton %} +{% from "x-govuk/components/autocomplete/macro.njk" import xGovukAutocomplete %} + +{% block headTitle %} + Lpa details – {{ serviceName }} +{% endblock %} {% block pageTitle %} Lpa details – {{ serviceName }} @@ -25,18 +30,18 @@

- {{ - govukInput({ - label: { - text: "Local planning authority", - classes: "govuk-label--m", - isPageHeading: false - }, - id: "lpa", - name: "lpa", - classes: "govuk-!-width-three-quarters" - }) - }} + {{ xGovukAutocomplete({ + id: "lpa", + name: "lpa", + allowEmpty: false, + label: { + classes: "govuk-label--m", + isPageHeading: false, + text: "Choose your local planning authority" + }, + items: options.localAuthorities + }) }} + {{ govukInput({ label: { From 251292be6e967122b5f4aa80441fbca8c165cee3 Mon Sep 17 00:00:00 2001 From: George Goodall Date: Thu, 4 Jul 2024 15:07:03 +0100 Subject: [PATCH 2/9] linting --- src/controllers/lpaDetailsController.js | 4 ++-- src/utils/fetchLocalAuthorities.js | 14 +++++++------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/controllers/lpaDetailsController.js b/src/controllers/lpaDetailsController.js index f9054c6f3..1725b29db 100644 --- a/src/controllers/lpaDetailsController.js +++ b/src/controllers/lpaDetailsController.js @@ -6,8 +6,8 @@ class lpaDetailsController extends PageController { const localAuthorities = await fetchLocalAuthorities() const localAuthoritiesNames = localAuthorities.entities.map(lpa => ({ - text: lpa.name, - value: lpa.name + text: lpa.name, + value: lpa.name })) req.form.options.localAuthorities = localAuthoritiesNames diff --git a/src/utils/fetchLocalAuthorities.js b/src/utils/fetchLocalAuthorities.js index babfeac22..bd98bb868 100644 --- a/src/utils/fetchLocalAuthorities.js +++ b/src/utils/fetchLocalAuthorities.js @@ -1,12 +1,12 @@ -import axios from 'axios'; +import axios from 'axios' export default async () => { - const url = "https://www.planning.data.gov.uk/entity.json?dataset=local-authority&limit=500"; + const url = 'https://www.planning.data.gov.uk/entity.json?dataset=local-authority&limit=500' try { - const response = await axios.get(url); - return response.data; // Return the fetched data + const response = await axios.get(url) + return response.data // Return the fetched data } catch (error) { - console.error('Error fetching local authorities data:', error); - throw error; // Rethrow the error to be handled by the caller + console.error('Error fetching local authorities data:', error) + throw error // Rethrow the error to be handled by the caller } -} \ No newline at end of file +} From f5014403cdda94cde57b5d85c97cd7a5b5b97476 Mon Sep 17 00:00:00 2001 From: George Goodall Date: Thu, 4 Jul 2024 16:15:54 +0100 Subject: [PATCH 3/9] include x-gov javascript file --- src/serverSetup/middlewares.js | 1 + src/views/layouts/main.html | 7 +++++++ 2 files changed, 8 insertions(+) diff --git a/src/serverSetup/middlewares.js b/src/serverSetup/middlewares.js index 1ce70d605..44fd46b2d 100644 --- a/src/serverSetup/middlewares.js +++ b/src/serverSetup/middlewares.js @@ -18,6 +18,7 @@ export function setupMiddlewares (app) { // }) app.use('/assets', express.static('./node_modules/govuk-frontend/dist/govuk/assets')) + app.use('/assets', express.static('./node_modules/@x-govuk/govuk-prototype-components/x-govuk')) app.use('/public', express.static('./public')) app.use(cookieParser()) diff --git a/src/views/layouts/main.html b/src/views/layouts/main.html index 7dbc28811..7301ef0f4 100644 --- a/src/views/layouts/main.html +++ b/src/views/layouts/main.html @@ -69,6 +69,13 @@

Get help

{% block bodyEnd %} {{ super()}} {%block scripts %} + + ... + + + {{ super() }} {% endblock %} {% endblock %} \ No newline at end of file From 9d662670a2c4554e9c04604db9c83703e487a909 Mon Sep 17 00:00:00 2001 From: George Goodall Date: Thu, 4 Jul 2024 16:34:44 +0100 Subject: [PATCH 4/9] change endpoint to large list of orgs instead of just lpas --- src/controllers/lpaDetailsController.js | 10 +++++----- src/utils/fetchLocalAuthorities.js | 6 ++++-- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/src/controllers/lpaDetailsController.js b/src/controllers/lpaDetailsController.js index 1725b29db..9468040e8 100644 --- a/src/controllers/lpaDetailsController.js +++ b/src/controllers/lpaDetailsController.js @@ -3,14 +3,14 @@ import fetchLocalAuthorities from '../utils/fetchLocalAuthorities.js' class lpaDetailsController extends PageController { async locals (req, res, next) { - const localAuthorities = await fetchLocalAuthorities() + const localAuthoritiesNames = await fetchLocalAuthorities() - const localAuthoritiesNames = localAuthorities.entities.map(lpa => ({ - text: lpa.name, - value: lpa.name + const listItems = localAuthoritiesNames.map(name => ({ + text: name, + value: name })) - req.form.options.localAuthorities = localAuthoritiesNames + req.form.options.localAuthorities = listItems super.locals(req, res, next) } diff --git a/src/utils/fetchLocalAuthorities.js b/src/utils/fetchLocalAuthorities.js index bd98bb868..69147ba7e 100644 --- a/src/utils/fetchLocalAuthorities.js +++ b/src/utils/fetchLocalAuthorities.js @@ -1,10 +1,12 @@ import axios from 'axios' export default async () => { - const url = 'https://www.planning.data.gov.uk/entity.json?dataset=local-authority&limit=500' + // const url = 'https://www.planning.data.gov.uk/entity.json?dataset=local-authority&limit=500' + const url = 'https://datasette.planning.data.gov.uk/digital-land.json?sql=select%0D%0A++distinct+provision.organisation%2C%0D%0A++organisation.name%2C%0D%0A++organisation.dataset%0D%0Afrom%0D%0A++provision%2C%0D%0A++organisation%0D%0Awhere%0D%0A++provision.organisation+%3D+organisation.organisation%0D%0Aorder+by%0D%0A++provision.organisation' try { const response = await axios.get(url) - return response.data // Return the fetched data + const names = response.data.rows.map(row => row[1]) + return names // Return the fetched data } catch (error) { console.error('Error fetching local authorities data:', error) throw error // Rethrow the error to be handled by the caller From efbe76cea78c480f24d899b653515b7a715c43c1 Mon Sep 17 00:00:00 2001 From: George Goodall Date: Fri, 5 Jul 2024 13:52:57 +0100 Subject: [PATCH 5/9] add jsdoc to fetch local authorities --- src/utils/fetchLocalAuthorities.js | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/utils/fetchLocalAuthorities.js b/src/utils/fetchLocalAuthorities.js index 69147ba7e..daa6afdb3 100644 --- a/src/utils/fetchLocalAuthorities.js +++ b/src/utils/fetchLocalAuthorities.js @@ -1,5 +1,15 @@ import axios from 'axios' +/** + * Fetches a list of local authority names from a specified dataset. + * + * This function queries a dataset for local authorities, extracting a distinct list of names. + * It performs an HTTP GET request to retrieve the data, then processes the response to return + * only the names of the local authorities. + * + * @returns {Promise} A promise that resolves to an array of local authority names. + * @throws {Error} Throws an error if the HTTP request fails or data processing encounters an issue. + */ export default async () => { // const url = 'https://www.planning.data.gov.uk/entity.json?dataset=local-authority&limit=500' const url = 'https://datasette.planning.data.gov.uk/digital-land.json?sql=select%0D%0A++distinct+provision.organisation%2C%0D%0A++organisation.name%2C%0D%0A++organisation.dataset%0D%0Afrom%0D%0A++provision%2C%0D%0A++organisation%0D%0Awhere%0D%0A++provision.organisation+%3D+organisation.organisation%0D%0Aorder+by%0D%0A++provision.organisation' From bc5cd91c3a2939c9596a7a56587d096525c54e19 Mon Sep 17 00:00:00 2001 From: George Goodall Date: Fri, 5 Jul 2024 13:54:37 +0100 Subject: [PATCH 6/9] pull out sql --- src/utils/fetchLocalAuthorities.js | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/src/utils/fetchLocalAuthorities.js b/src/utils/fetchLocalAuthorities.js index daa6afdb3..1946995bc 100644 --- a/src/utils/fetchLocalAuthorities.js +++ b/src/utils/fetchLocalAuthorities.js @@ -2,17 +2,28 @@ import axios from 'axios' /** * Fetches a list of local authority names from a specified dataset. - * + * * This function queries a dataset for local authorities, extracting a distinct list of names. * It performs an HTTP GET request to retrieve the data, then processes the response to return * only the names of the local authorities. - * + * * @returns {Promise} A promise that resolves to an array of local authority names. * @throws {Error} Throws an error if the HTTP request fails or data processing encounters an issue. */ export default async () => { - // const url = 'https://www.planning.data.gov.uk/entity.json?dataset=local-authority&limit=500' - const url = 'https://datasette.planning.data.gov.uk/digital-land.json?sql=select%0D%0A++distinct+provision.organisation%2C%0D%0A++organisation.name%2C%0D%0A++organisation.dataset%0D%0Afrom%0D%0A++provision%2C%0D%0A++organisation%0D%0Awhere%0D%0A++provision.organisation+%3D+organisation.organisation%0D%0Aorder+by%0D%0A++provision.organisation' + const sql = `select + distinct provision.organisation, + organisation.name, + organisation.dataset + from + provision, + organisation + where + provision.organisation = organisation.organisation + order by + provision.organisation`; + + const url = `https://datasette.planning.data.gov.uk/digital-land.json?sql=${encodeURIComponent(sql)}`; try { const response = await axios.get(url) const names = response.data.rows.map(row => row[1]) From 0cd3bc998c7a9bb756a2748070edbeff492bd0e6 Mon Sep 17 00:00:00 2001 From: George Goodall Date: Fri, 5 Jul 2024 13:54:50 +0100 Subject: [PATCH 7/9] linting --- src/utils/fetchLocalAuthorities.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/utils/fetchLocalAuthorities.js b/src/utils/fetchLocalAuthorities.js index 1946995bc..a62639680 100644 --- a/src/utils/fetchLocalAuthorities.js +++ b/src/utils/fetchLocalAuthorities.js @@ -21,9 +21,9 @@ export default async () => { where provision.organisation = organisation.organisation order by - provision.organisation`; + provision.organisation` - const url = `https://datasette.planning.data.gov.uk/digital-land.json?sql=${encodeURIComponent(sql)}`; + const url = `https://datasette.planning.data.gov.uk/digital-land.json?sql=${encodeURIComponent(sql)}` try { const response = await axios.get(url) const names = response.data.rows.map(row => row[1]) From 5f509d46beb678954abda9cf267a05966da7ff0f Mon Sep 17 00:00:00 2001 From: George Goodall Date: Fri, 5 Jul 2024 16:19:31 +0100 Subject: [PATCH 8/9] get tests working --- src/controllers/lpaDetailsController.js | 6 +-- src/routes/form-wizard/steps.js | 4 +- src/utils/fetchLocalAuthorities.js | 11 ++++- test/unit/fetchLocalAuthorities.test.js | 41 ++++++++++++++++ test/unit/lpaDetailsController.test.js | 63 +++++++++++++++++++++++++ 5 files changed, 118 insertions(+), 7 deletions(-) create mode 100644 test/unit/fetchLocalAuthorities.test.js create mode 100644 test/unit/lpaDetailsController.test.js diff --git a/src/controllers/lpaDetailsController.js b/src/controllers/lpaDetailsController.js index 9468040e8..6301564af 100644 --- a/src/controllers/lpaDetailsController.js +++ b/src/controllers/lpaDetailsController.js @@ -1,7 +1,7 @@ import PageController from './pageController.js' -import fetchLocalAuthorities from '../utils/fetchLocalAuthorities.js' +import { fetchLocalAuthorities } from '../utils/fetchLocalAuthorities.js' -class lpaDetailsController extends PageController { +class LpaDetailsController extends PageController { async locals (req, res, next) { const localAuthoritiesNames = await fetchLocalAuthorities() @@ -16,4 +16,4 @@ class lpaDetailsController extends PageController { } } -export default lpaDetailsController +export default LpaDetailsController diff --git a/src/routes/form-wizard/steps.js b/src/routes/form-wizard/steps.js index 7ae071c98..54047d73b 100644 --- a/src/routes/form-wizard/steps.js +++ b/src/routes/form-wizard/steps.js @@ -1,5 +1,5 @@ import chooseDatasetController from '../../controllers/chooseDatasetController.js' -import lpaDetailsController from '../../controllers/lpaDetailsController.js' +import LpaDetailsController from '../../controllers/lpaDetailsController.js' import PageController from '../../controllers/pageController.js' import CheckAnswersController from '../../controllers/CheckAnswersController.js' @@ -20,7 +20,7 @@ export default { ...defaultParams, fields: ['lpa', 'name', 'email'], next: 'choose-dataset', - controller: lpaDetailsController, + controller: LpaDetailsController, backLink: '/start' }, '/choose-dataset': { diff --git a/src/utils/fetchLocalAuthorities.js b/src/utils/fetchLocalAuthorities.js index a62639680..5fb297905 100644 --- a/src/utils/fetchLocalAuthorities.js +++ b/src/utils/fetchLocalAuthorities.js @@ -10,7 +10,7 @@ import axios from 'axios' * @returns {Promise} A promise that resolves to an array of local authority names. * @throws {Error} Throws an error if the HTTP request fails or data processing encounters an issue. */ -export default async () => { +export const fetchLocalAuthorities = async () => { const sql = `select distinct provision.organisation, organisation.name, @@ -26,7 +26,14 @@ export default async () => { const url = `https://datasette.planning.data.gov.uk/digital-land.json?sql=${encodeURIComponent(sql)}` try { const response = await axios.get(url) - const names = response.data.rows.map(row => row[1]) + const names = response.data.rows.map(row => { + if (row[1] === null) { + console.log('Null value found in response:', row) + return null + } else { + return row[1] + } + }).filter(name => name !== null) // Filter out null values return names // Return the fetched data } catch (error) { console.error('Error fetching local authorities data:', error) diff --git a/test/unit/fetchLocalAuthorities.test.js b/test/unit/fetchLocalAuthorities.test.js new file mode 100644 index 000000000..9b3c73f26 --- /dev/null +++ b/test/unit/fetchLocalAuthorities.test.js @@ -0,0 +1,41 @@ +import axios from 'axios' +import { vi, it, describe, expect } from 'vitest' +import { fetchLocalAuthorities } from '../../src/utils/fetchLocalAuthorities' + +// Mock axios.get to return a fake response +vi.mock('axios') +axios.get.mockResolvedValue({ + data: { + rows: [ + [1, 'Local Authority 1'], + [2, 'Local Authority 2'], + [3, 'Local Authority 3'] + ] + } +}) + +describe('fetchLocalAuthorities', () => { + it('should fetch local authority names', async () => { + const result = await fetchLocalAuthorities() + expect(result).toEqual(['Local Authority 1', 'Local Authority 2', 'Local Authority 3']) + }) + + it('should throw an error if the HTTP request fails', async () => { + axios.get.mockRejectedValue(new Error('Failed to fetch data')) + await expect(fetchLocalAuthorities()).rejects.toThrow('Failed to fetch data') + }) + + it('should throw an error if data processing encounters an issue', async () => { + axios.get.mockResolvedValue({ + data: { + rows: [ + [1, 'Local Authority 1'], + [2, null], // Simulate null value in the response + [3, 'Local Authority 3'] + ] + } + }) + const result = await fetchLocalAuthorities() + expect(result).toEqual(['Local Authority 1', 'Local Authority 3']) + }) +}) diff --git a/test/unit/lpaDetailsController.test.js b/test/unit/lpaDetailsController.test.js new file mode 100644 index 000000000..d3bb2b80d --- /dev/null +++ b/test/unit/lpaDetailsController.test.js @@ -0,0 +1,63 @@ +/* eslint-disable no-import-assign */ + +import LpaDetailsController from '../../src/controllers/lpaDetailsController.js' +import { fetchLocalAuthorities } from '../../src/utils/fetchLocalAuthorities' +import PageController from '../../src/controllers/pageController.js' +import { vi, it, describe, expect, beforeEach, afterEach } from 'vitest' + +vi.mock('../../src/utils/fetchLocalAuthorities.js') + +describe('lpaDetailsController', () => { + let controller + + beforeEach(() => { + controller = new LpaDetailsController({ + route: '/lpa-details' + }) + }) + + afterEach(() => { + vi.restoreAllMocks() + }) + + describe('locals', () => { + it('should set localAuthorities options in the form', async () => { + const req = { + form: { + options: {} + } + } + const res = {} + const next = vi.fn() + + const localAuthoritiesNames = ['Authority 1', 'Authority 2'] + fetchLocalAuthorities = vi.fn().mockResolvedValue(localAuthoritiesNames) + + await controller.locals(req, res, next) + + expect(fetchLocalAuthorities).toHaveBeenCalled() + expect(req.form.options.localAuthorities).toEqual([ + { text: 'Authority 1', value: 'Authority 1' }, + { text: 'Authority 2', value: 'Authority 2' } + ]) + expect(next).toHaveBeenCalled() + }) + + it('should call super.locals', async () => { + const req = { + form: { + options: {} + } + } + const res = {} + const next = vi.fn() + + fetchLocalAuthorities = vi.fn().mockResolvedValue([]) + const superLocalsSpy = vi.spyOn(PageController.prototype, 'locals') + + await controller.locals(req, res, next) + + expect(superLocalsSpy).toHaveBeenCalledWith(req, res, next) + }) + }) +}) From 035be4e6d1684b4bd1a62f3c81a843b8fd7b73ef Mon Sep 17 00:00:00 2001 From: George Goodall Date: Wed, 10 Jul 2024 11:38:19 +0100 Subject: [PATCH 9/9] fix unit tests --- test/unit/lpaDetailsController.test.js | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/test/unit/lpaDetailsController.test.js b/test/unit/lpaDetailsController.test.js index d3bb2b80d..21355a83d 100644 --- a/test/unit/lpaDetailsController.test.js +++ b/test/unit/lpaDetailsController.test.js @@ -1,17 +1,19 @@ /* eslint-disable no-import-assign */ +/* eslint-disable new-cap */ -import LpaDetailsController from '../../src/controllers/lpaDetailsController.js' -import { fetchLocalAuthorities } from '../../src/utils/fetchLocalAuthorities' import PageController from '../../src/controllers/pageController.js' import { vi, it, describe, expect, beforeEach, afterEach } from 'vitest' vi.mock('../../src/utils/fetchLocalAuthorities.js') -describe('lpaDetailsController', () => { +describe('lpaDetailsController', async () => { + let fetchLocalAuthorities let controller - beforeEach(() => { - controller = new LpaDetailsController({ + beforeEach(async () => { + fetchLocalAuthorities = await import('../../src/utils/fetchLocalAuthorities') + const LpaDetailsController = await import('../../src/controllers/lpaDetailsController.js') + controller = new LpaDetailsController.default({ route: '/lpa-details' }) }) @@ -31,11 +33,12 @@ describe('lpaDetailsController', () => { const next = vi.fn() const localAuthoritiesNames = ['Authority 1', 'Authority 2'] - fetchLocalAuthorities = vi.fn().mockResolvedValue(localAuthoritiesNames) + + fetchLocalAuthorities.fetchLocalAuthorities = vi.fn().mockResolvedValue(localAuthoritiesNames) await controller.locals(req, res, next) - expect(fetchLocalAuthorities).toHaveBeenCalled() + expect(fetchLocalAuthorities.fetchLocalAuthorities).toHaveBeenCalled() expect(req.form.options.localAuthorities).toEqual([ { text: 'Authority 1', value: 'Authority 1' }, { text: 'Authority 2', value: 'Authority 2' } @@ -52,7 +55,7 @@ describe('lpaDetailsController', () => { const res = {} const next = vi.fn() - fetchLocalAuthorities = vi.fn().mockResolvedValue([]) + fetchLocalAuthorities.fetchLocalAuthorities = vi.fn().mockResolvedValue([]) const superLocalsSpy = vi.spyOn(PageController.prototype, 'locals') await controller.locals(req, res, next)