@defra/forms-engine-plugin 0.1.26 → 0.1.28
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.public/assets/images/govuk-crest.svg +1 -1
- package/.public/assets/rebrand/images/favicon.ico +0 -0
- package/.public/assets/rebrand/images/favicon.svg +1 -0
- package/.public/assets/rebrand/images/govuk-crest.svg +1 -0
- package/.public/assets/rebrand/images/govuk-icon-180.png +0 -0
- package/.public/assets/rebrand/images/govuk-icon-192.png +0 -0
- package/.public/assets/rebrand/images/govuk-icon-512.png +0 -0
- package/.public/assets/rebrand/images/govuk-icon-mask.svg +1 -0
- package/.public/assets/rebrand/images/govuk-opengraph-image.png +0 -0
- package/.public/assets/rebrand/manifest.json +39 -0
- package/.public/assets-manifest.json +9 -0
- package/.public/javascripts/application.min.js +1 -1
- package/.public/javascripts/application.min.js.LICENSE.txt +5 -1
- package/.public/javascripts/application.min.js.map +1 -1
- package/.public/javascripts/shared.min.js +1 -1
- package/.public/javascripts/shared.min.js.LICENSE.txt +5 -1
- package/.public/javascripts/shared.min.js.map +1 -1
- package/.public/stylesheets/application.min.css +2 -2
- package/.public/stylesheets/application.min.css.map +1 -1
- package/.server/index.js +6 -2
- package/.server/index.js.map +1 -1
- package/.server/server/common/helpers/logging/request-tracing.js +1 -1
- package/.server/server/common/helpers/logging/request-tracing.js.map +1 -1
- package/.server/server/common/helpers/redis-client.js +5 -3
- package/.server/server/common/helpers/redis-client.js.map +1 -1
- package/.server/server/constants.d.ts +0 -1
- package/.server/server/constants.js +0 -1
- package/.server/server/constants.js.map +1 -1
- package/.server/server/index.js +3 -1
- package/.server/server/index.js.map +1 -1
- package/.server/server/plugins/engine/components/DatePartsField.d.ts +1 -6
- package/.server/server/plugins/engine/components/DatePartsField.js +2 -1
- package/.server/server/plugins/engine/components/DatePartsField.js.map +1 -1
- package/.server/server/plugins/engine/components/MonthYearField.d.ts +1 -5
- package/.server/server/plugins/engine/components/MonthYearField.js +3 -2
- package/.server/server/plugins/engine/components/MonthYearField.js.map +1 -1
- package/.server/server/plugins/engine/components/YesNoField.js +2 -1
- package/.server/server/plugins/engine/components/YesNoField.js.map +1 -1
- package/.server/server/plugins/engine/components/types.d.ts +9 -0
- package/.server/server/plugins/engine/components/types.js.map +1 -1
- package/.server/server/plugins/engine/date-helper.d.ts +12 -0
- package/.server/server/plugins/engine/date-helper.js +21 -0
- package/.server/server/plugins/engine/date-helper.js.map +1 -0
- package/.server/server/plugins/engine/helpers.js +4 -3
- package/.server/server/plugins/engine/helpers.js.map +1 -1
- package/.server/server/plugins/engine/index.js +3 -2
- package/.server/server/plugins/engine/index.js.map +1 -1
- package/.server/server/plugins/engine/models/FormModel.d.ts +13 -6
- package/.server/server/plugins/engine/models/FormModel.js +51 -18
- package/.server/server/plugins/engine/models/FormModel.js.map +1 -1
- package/.server/server/plugins/engine/outputFormatters/machine/v2.js.map +1 -1
- package/.server/server/plugins/engine/pageControllers/FileUploadPageController.js +3 -2
- package/.server/server/plugins/engine/pageControllers/FileUploadPageController.js.map +1 -1
- package/.server/server/plugins/engine/plugin.js +6 -5
- package/.server/server/plugins/engine/plugin.js.map +1 -1
- package/.server/server/plugins/engine/services/notifyService.js +3 -1
- package/.server/server/plugins/engine/services/notifyService.js.map +1 -1
- package/.server/server/plugins/engine/views/components/tag-env/template.njk +5 -24
- package/.server/server/plugins/engine/views/components/tag-env/template.test.js +19 -53
- package/.server/server/plugins/engine/views/components/tag-env/template.test.js.map +1 -1
- package/.server/server/plugins/errorPages.js +1 -1
- package/.server/server/plugins/errorPages.js.map +1 -1
- package/.server/server/plugins/nunjucks/context.js +1 -1
- package/.server/server/plugins/nunjucks/context.js.map +1 -1
- package/.server/server/plugins/nunjucks/environment.d.ts +1 -0
- package/.server/server/plugins/nunjucks/environment.js +4 -0
- package/.server/server/plugins/nunjucks/environment.js.map +1 -1
- package/package.json +18 -18
- package/src/index.ts +7 -2
- package/src/server/common/helpers/logging/request-tracing.js +1 -1
- package/src/server/common/helpers/redis-client.js +5 -3
- package/src/server/constants.js +0 -1
- package/src/server/index.ts +5 -2
- package/src/server/plugins/engine/components/DatePartsField.test.ts +17 -0
- package/src/server/plugins/engine/components/DatePartsField.ts +7 -8
- package/src/server/plugins/engine/components/MonthYearField.test.ts +15 -0
- package/src/server/plugins/engine/components/MonthYearField.ts +12 -8
- package/src/server/plugins/engine/components/YesNoField.ts +16 -2
- package/src/server/plugins/engine/components/types.ts +11 -0
- package/src/server/plugins/engine/date-helper.test.ts +47 -0
- package/src/server/plugins/engine/date-helper.ts +32 -0
- package/src/server/plugins/engine/helpers.ts +9 -2
- package/src/server/plugins/engine/index.ts +4 -2
- package/src/server/plugins/engine/models/FormModel.test.ts +163 -1
- package/src/server/plugins/engine/models/FormModel.ts +90 -23
- package/src/server/plugins/engine/outputFormatters/machine/v2.ts +4 -2
- package/src/server/plugins/engine/pageControllers/FileUploadPageController.test.ts +2 -1
- package/src/server/plugins/engine/pageControllers/FileUploadPageController.ts +6 -2
- package/src/server/plugins/engine/plugin.ts +11 -7
- package/src/server/plugins/engine/services/notifyService.ts +6 -2
- package/src/server/plugins/engine/views/components/tag-env/template.njk +5 -24
- package/src/server/plugins/engine/views/components/tag-env/template.test.js +18 -56
- package/src/server/plugins/errorPages.ts +5 -1
- package/src/server/plugins/nunjucks/context.js +3 -1
- package/src/server/plugins/nunjucks/environment.js +6 -0
|
@@ -1,13 +1,27 @@
|
|
|
1
|
+
import {
|
|
2
|
+
SchemaVersion,
|
|
3
|
+
formDefinitionV2Schema,
|
|
4
|
+
type FormDefinition
|
|
5
|
+
} from '@defra/forms-model'
|
|
6
|
+
|
|
7
|
+
import { todayAsDateOnly } from '~/src/server/plugins/engine/date-helper.js'
|
|
1
8
|
import { FormModel } from '~/src/server/plugins/engine/models/FormModel.js'
|
|
2
9
|
import { type FormContextRequest } from '~/src/server/plugins/engine/types.js'
|
|
3
10
|
import { V2 as definitionV2 } from '~/test/form/definitions/conditions-basic.js'
|
|
4
11
|
import definition from '~/test/form/definitions/conditions-escaping.js'
|
|
5
12
|
import conditionsListDefinition from '~/test/form/definitions/conditions-list.js'
|
|
13
|
+
import relativeDatesDefinition from '~/test/form/definitions/conditions-relative-dates-v2.js'
|
|
6
14
|
import fieldsRequiredDefinition from '~/test/form/definitions/fields-required.js'
|
|
7
15
|
|
|
16
|
+
jest.mock('~/src/server/plugins/engine/date-helper.ts')
|
|
17
|
+
|
|
8
18
|
describe('FormModel', () => {
|
|
19
|
+
beforeEach(() => {
|
|
20
|
+
jest.resetAllMocks()
|
|
21
|
+
})
|
|
22
|
+
|
|
9
23
|
describe('Constructor', () => {
|
|
10
|
-
it(
|
|
24
|
+
it('loads a valid form definition', () => {
|
|
11
25
|
expect(
|
|
12
26
|
() => new FormModel(definition, { basePath: 'test' })
|
|
13
27
|
).not.toThrow()
|
|
@@ -26,6 +40,109 @@ describe('FormModel', () => {
|
|
|
26
40
|
)
|
|
27
41
|
expect(model.def.pages.at(1)?.title).toBe('Date of marriage')
|
|
28
42
|
})
|
|
43
|
+
|
|
44
|
+
it('Gets a list by ID', () => {
|
|
45
|
+
jest.mock('@defra/forms-model')
|
|
46
|
+
|
|
47
|
+
const definitionWithLists: FormDefinition = {
|
|
48
|
+
...definitionV2,
|
|
49
|
+
lists: [
|
|
50
|
+
{
|
|
51
|
+
id: 'c5eba145-b04d-4d41-a50c-e5e2f9b6357f',
|
|
52
|
+
type: 'string',
|
|
53
|
+
title: 'foo',
|
|
54
|
+
name: 'foo',
|
|
55
|
+
items: [
|
|
56
|
+
{ text: 'a', value: 'a' },
|
|
57
|
+
{ text: 'b', value: 'b' }
|
|
58
|
+
]
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
type: 'string',
|
|
62
|
+
title: 'bar',
|
|
63
|
+
name: 'bar',
|
|
64
|
+
items: [
|
|
65
|
+
{
|
|
66
|
+
id: 'a85a42a8-3e08-4c2a-b263-a0dc0b8c49f6',
|
|
67
|
+
text: 'a',
|
|
68
|
+
value: 'a'
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
id: 'c31664ac-887b-434b-b9f4-e5bc30d24439',
|
|
72
|
+
text: 'b',
|
|
73
|
+
value: 'b'
|
|
74
|
+
}
|
|
75
|
+
]
|
|
76
|
+
}
|
|
77
|
+
]
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
formDefinitionV2Schema.validate = jest
|
|
81
|
+
.fn()
|
|
82
|
+
.mockReturnValue({ value: definitionWithLists })
|
|
83
|
+
|
|
84
|
+
const model = new FormModel(definitionWithLists, { basePath: 'test' })
|
|
85
|
+
|
|
86
|
+
expect(
|
|
87
|
+
model.getListById('c5eba145-b04d-4d41-a50c-e5e2f9b6357f')
|
|
88
|
+
).toBeDefined()
|
|
89
|
+
expect(model.listDefIdMap.size).toBe(2) // 1 + the yes/no list. list 'bar' isn't present as there's no ID
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
it('Gets a component by ID', () => {
|
|
93
|
+
jest.mock('@defra/forms-model')
|
|
94
|
+
|
|
95
|
+
formDefinitionV2Schema.validate = jest
|
|
96
|
+
.fn()
|
|
97
|
+
.mockReturnValue({ value: definitionV2 })
|
|
98
|
+
|
|
99
|
+
const model = new FormModel(definitionV2, { basePath: 'test' })
|
|
100
|
+
|
|
101
|
+
expect(
|
|
102
|
+
model.getComponentById('717eb213-4e4b-4a2d-9cfd-2780f5e1e3e5')
|
|
103
|
+
).toBeDefined()
|
|
104
|
+
expect(model.listDefIdMap.size).toBe(1)
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
it('gets a condition by its ID', () => {
|
|
108
|
+
jest.mock('@defra/forms-model')
|
|
109
|
+
formDefinitionV2Schema.validate = jest
|
|
110
|
+
.fn()
|
|
111
|
+
.mockReturnValue({ value: definitionV2 })
|
|
112
|
+
const model = new FormModel(definitionV2, { basePath: 'test' })
|
|
113
|
+
|
|
114
|
+
expect(
|
|
115
|
+
model.getConditionById('6c9e2f4a-1d7b-5e8c-3f6a-9e2d5b7c4f1a')
|
|
116
|
+
).toBeDefined()
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
it('throws an error if schema validation fails', () => {
|
|
120
|
+
jest.mock('@defra/forms-model')
|
|
121
|
+
|
|
122
|
+
formDefinitionV2Schema.validate = jest.fn().mockReturnValueOnce({
|
|
123
|
+
error: 'Validation error'
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
expect(() => new FormModel(definitionV2, { basePath: 'test' })).toThrow(
|
|
127
|
+
'Validation error'
|
|
128
|
+
)
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
it('assigns v1 to the schema if not defined', () => {
|
|
132
|
+
const definitionWithoutSchema: FormDefinition = {
|
|
133
|
+
...definition,
|
|
134
|
+
schema: undefined
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Mock validation to just return the definition
|
|
138
|
+
formDefinitionV2Schema.validate = jest
|
|
139
|
+
.fn()
|
|
140
|
+
.mockReturnValue({ value: definitionWithoutSchema })
|
|
141
|
+
|
|
142
|
+
const model = new FormModel(definitionWithoutSchema, { basePath: 'test' })
|
|
143
|
+
|
|
144
|
+
expect(model.schemaVersion).toBe(SchemaVersion.V1)
|
|
145
|
+
})
|
|
29
146
|
})
|
|
30
147
|
|
|
31
148
|
describe('getFormContext', () => {
|
|
@@ -184,4 +301,49 @@ describe('FormModel', () => {
|
|
|
184
301
|
})
|
|
185
302
|
})
|
|
186
303
|
})
|
|
304
|
+
|
|
305
|
+
describe('makeCondition', () => {
|
|
306
|
+
test('relative date condition', () => {
|
|
307
|
+
jest.mock('@defra/forms-model')
|
|
308
|
+
formDefinitionV2Schema.validate = jest
|
|
309
|
+
.fn()
|
|
310
|
+
.mockReturnValue({ value: relativeDatesDefinition })
|
|
311
|
+
const model = new FormModel(relativeDatesDefinition, { basePath: 'test' })
|
|
312
|
+
|
|
313
|
+
const allConditionsKeys = Object.keys(model.conditions)
|
|
314
|
+
expect(allConditionsKeys).toHaveLength(8)
|
|
315
|
+
|
|
316
|
+
// Only test releative date conditions
|
|
317
|
+
const relativeConditionsKeys = allConditionsKeys.slice(4)
|
|
318
|
+
expect(relativeConditionsKeys).toHaveLength(4)
|
|
319
|
+
|
|
320
|
+
const formState = {
|
|
321
|
+
ybMHIv: '2023-06-18'
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
const expectedResultsDayBefore = [true, false, false, true]
|
|
325
|
+
|
|
326
|
+
const expectedResultsDayOf = [true, true, false, false]
|
|
327
|
+
|
|
328
|
+
const expectedResultsDayAfter = [false, true, true, false]
|
|
329
|
+
|
|
330
|
+
// Only relative date conditions
|
|
331
|
+
for (let i = 0; i < relativeConditionsKeys.length; i++) {
|
|
332
|
+
const condition = model.conditions[relativeConditionsKeys[i]]
|
|
333
|
+
jest.mocked(todayAsDateOnly).mockReturnValue(new Date(2025, 5, 19))
|
|
334
|
+
const conditionExec = model.makeCondition(
|
|
335
|
+
// @ts-expect-error - type doesnt need to match for this test
|
|
336
|
+
condition
|
|
337
|
+
)
|
|
338
|
+
formState.ybMHIv = '2023-06-18'
|
|
339
|
+
expect(conditionExec.fn(formState)).toBe(expectedResultsDayBefore[i])
|
|
340
|
+
|
|
341
|
+
formState.ybMHIv = '2023-06-19'
|
|
342
|
+
expect(conditionExec.fn(formState)).toBe(expectedResultsDayOf[i])
|
|
343
|
+
|
|
344
|
+
formState.ybMHIv = '2023-06-20'
|
|
345
|
+
expect(conditionExec.fn(formState)).toBe(expectedResultsDayAfter[i])
|
|
346
|
+
}
|
|
347
|
+
})
|
|
348
|
+
})
|
|
187
349
|
})
|
|
@@ -4,26 +4,35 @@ import {
|
|
|
4
4
|
ControllerPath,
|
|
5
5
|
ControllerType,
|
|
6
6
|
Engine,
|
|
7
|
+
SchemaVersion,
|
|
8
|
+
convertConditionWrapperFromV2,
|
|
7
9
|
formDefinitionSchema,
|
|
10
|
+
formDefinitionV2Schema,
|
|
8
11
|
hasComponents,
|
|
9
12
|
hasRepeater,
|
|
13
|
+
isConditionWrapperV2,
|
|
14
|
+
yesNoListId,
|
|
15
|
+
yesNoListName,
|
|
10
16
|
type ComponentDef,
|
|
11
17
|
type ConditionWrapper,
|
|
18
|
+
type ConditionWrapperV2,
|
|
12
19
|
type ConditionsModelData,
|
|
13
20
|
type DateUnits,
|
|
14
21
|
type FormDefinition,
|
|
15
22
|
type List,
|
|
16
23
|
type Page
|
|
17
24
|
} from '@defra/forms-model'
|
|
18
|
-
import { add } from 'date-fns'
|
|
25
|
+
import { add, format } from 'date-fns'
|
|
19
26
|
import { Parser, type Value } from 'expr-eval'
|
|
20
27
|
import joi from 'joi'
|
|
21
28
|
|
|
22
29
|
import { type ListFormComponent } from '~/src/server/plugins/engine/components/ListFormComponent.js'
|
|
30
|
+
import {} from '~/src/server/plugins/engine/components/YesNoField.js'
|
|
23
31
|
import {
|
|
24
32
|
hasListFormField,
|
|
25
33
|
type Component
|
|
26
34
|
} from '~/src/server/plugins/engine/components/helpers.js'
|
|
35
|
+
import { todayAsDateOnly } from '~/src/server/plugins/engine/date-helper.js'
|
|
27
36
|
import {
|
|
28
37
|
findPage,
|
|
29
38
|
getError,
|
|
@@ -53,6 +62,8 @@ export class FormModel {
|
|
|
53
62
|
/** The runtime engine that should be used */
|
|
54
63
|
engine?: Engine
|
|
55
64
|
|
|
65
|
+
schemaVersion: SchemaVersion
|
|
66
|
+
|
|
56
67
|
/** the entire form JSON as an object */
|
|
57
68
|
def: FormDefinition
|
|
58
69
|
|
|
@@ -67,8 +78,13 @@ export class FormModel {
|
|
|
67
78
|
|
|
68
79
|
controllers?: Record<string, typeof PageController>
|
|
69
80
|
pageDefMap: Map<string, Page>
|
|
81
|
+
|
|
70
82
|
listDefMap: Map<string, List>
|
|
83
|
+
listDefIdMap: Map<string, List>
|
|
84
|
+
|
|
71
85
|
componentDefMap: Map<string, ComponentDef>
|
|
86
|
+
componentDefIdMap: Map<string, ComponentDef>
|
|
87
|
+
|
|
72
88
|
pageMap: Map<string, PageControllerClass>
|
|
73
89
|
componentMap: Map<string, Component>
|
|
74
90
|
|
|
@@ -78,7 +94,13 @@ export class FormModel {
|
|
|
78
94
|
services: Services = defaultServices,
|
|
79
95
|
controllers?: Record<string, typeof PageController>
|
|
80
96
|
) {
|
|
81
|
-
|
|
97
|
+
let schema = formDefinitionSchema
|
|
98
|
+
|
|
99
|
+
if (def.schema === SchemaVersion.V2) {
|
|
100
|
+
schema = formDefinitionV2Schema
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const result = schema.validate(def, { abortEarly: false })
|
|
82
104
|
|
|
83
105
|
if (result.error) {
|
|
84
106
|
throw result.error
|
|
@@ -90,15 +112,18 @@ export class FormModel {
|
|
|
90
112
|
|
|
91
113
|
// Add default lists
|
|
92
114
|
def.lists.push({
|
|
115
|
+
id: def.schema === SchemaVersion.V1 ? yesNoListName : yesNoListId,
|
|
93
116
|
name: '__yesNo',
|
|
94
117
|
title: 'Yes/No',
|
|
95
118
|
type: 'boolean',
|
|
96
119
|
items: [
|
|
97
120
|
{
|
|
121
|
+
id: '02900d42-83d1-4c72-a719-c4e8228952fa',
|
|
98
122
|
text: 'Yes',
|
|
99
123
|
value: true
|
|
100
124
|
},
|
|
101
125
|
{
|
|
126
|
+
id: 'f39000eb-c51b-4019-8f82-bbda0423f04d',
|
|
102
127
|
text: 'No',
|
|
103
128
|
value: false
|
|
104
129
|
}
|
|
@@ -109,6 +134,7 @@ export class FormModel {
|
|
|
109
134
|
setPageTitles(def)
|
|
110
135
|
|
|
111
136
|
this.engine = def.engine
|
|
137
|
+
this.schemaVersion = def.schema ?? SchemaVersion.V1
|
|
112
138
|
this.def = def
|
|
113
139
|
this.lists = def.lists
|
|
114
140
|
this.sections = def.sections
|
|
@@ -119,8 +145,38 @@ export class FormModel {
|
|
|
119
145
|
this.services = services
|
|
120
146
|
this.controllers = controllers
|
|
121
147
|
|
|
148
|
+
this.pageDefMap = new Map(def.pages.map((page) => [page.path, page]))
|
|
149
|
+
this.listDefMap = new Map(def.lists.map((list) => [list.name, list]))
|
|
150
|
+
this.listDefIdMap = new Map(
|
|
151
|
+
def.lists
|
|
152
|
+
.filter((list) => list.id) // Skip lists without an ID
|
|
153
|
+
// eslint-disable-next-line @typescript-eslint/non-nullable-type-assertion-style
|
|
154
|
+
.map((list) => [list.id as string, list])
|
|
155
|
+
)
|
|
156
|
+
this.componentDefMap = new Map(
|
|
157
|
+
def.pages
|
|
158
|
+
.filter(hasComponents)
|
|
159
|
+
.flatMap((page) =>
|
|
160
|
+
page.components.map((component) => [component.name, component])
|
|
161
|
+
)
|
|
162
|
+
)
|
|
163
|
+
this.componentDefIdMap = new Map(
|
|
164
|
+
def.pages.filter(hasComponents).flatMap((page) =>
|
|
165
|
+
page.components
|
|
166
|
+
.filter((component) => component.id) // Skip components without an ID
|
|
167
|
+
.map((component) => {
|
|
168
|
+
// eslint-disable-next-line @typescript-eslint/non-nullable-type-assertion-style
|
|
169
|
+
return [component.id as string, component]
|
|
170
|
+
})
|
|
171
|
+
)
|
|
172
|
+
)
|
|
173
|
+
|
|
122
174
|
def.conditions.forEach((conditionDef) => {
|
|
123
|
-
const condition = this.makeCondition(
|
|
175
|
+
const condition = this.makeCondition(
|
|
176
|
+
isConditionWrapperV2(conditionDef)
|
|
177
|
+
? convertConditionWrapperFromV2(conditionDef, this)
|
|
178
|
+
: conditionDef
|
|
179
|
+
)
|
|
124
180
|
this.conditions[condition.name] = condition
|
|
125
181
|
})
|
|
126
182
|
|
|
@@ -142,16 +198,6 @@ export class FormModel {
|
|
|
142
198
|
)
|
|
143
199
|
}
|
|
144
200
|
|
|
145
|
-
this.pageDefMap = new Map(def.pages.map((page) => [page.path, page]))
|
|
146
|
-
this.listDefMap = new Map(def.lists.map((list) => [list.name, list]))
|
|
147
|
-
this.componentDefMap = new Map(
|
|
148
|
-
def.pages
|
|
149
|
-
.filter(hasComponents)
|
|
150
|
-
.flatMap((page) =>
|
|
151
|
-
page.components.map((component) => [component.name, component])
|
|
152
|
-
)
|
|
153
|
-
)
|
|
154
|
-
|
|
155
201
|
this.pageMap = new Map(this.pages.map((page) => [page.path, page]))
|
|
156
202
|
this.componentMap = new Map(
|
|
157
203
|
this.pages.flatMap((page) =>
|
|
@@ -163,13 +209,6 @@ export class FormModel {
|
|
|
163
209
|
)
|
|
164
210
|
}
|
|
165
211
|
|
|
166
|
-
/**
|
|
167
|
-
* build the entire model schema from individual pages/sections
|
|
168
|
-
*/
|
|
169
|
-
makeSchema() {
|
|
170
|
-
return this.makeFilteredSchema(this.pages)
|
|
171
|
-
}
|
|
172
|
-
|
|
173
212
|
/**
|
|
174
213
|
* build the entire model schema from individual pages/sections and filter out answers
|
|
175
214
|
* for pages which are no longer accessible due to an answer that has been changed
|
|
@@ -199,7 +238,14 @@ export class FormModel {
|
|
|
199
238
|
|
|
200
239
|
Object.assign(parser.functions, {
|
|
201
240
|
dateForComparison(timePeriod: number, timeUnit: DateUnits) {
|
|
202
|
-
|
|
241
|
+
// The time element must be stripped (hence using startOfDay() which has no time element),
|
|
242
|
+
// then formatted as YYYY-MM-DD otherwise we can hit time element and BST issues giving the
|
|
243
|
+
// wrong date to compare against.
|
|
244
|
+
// Do not use .toISOString() to format the date as that introduces BST errors.
|
|
245
|
+
return format(
|
|
246
|
+
add(todayAsDateOnly(), { [timeUnit]: timePeriod }),
|
|
247
|
+
'yyyy-MM-dd'
|
|
248
|
+
)
|
|
203
249
|
}
|
|
204
250
|
})
|
|
205
251
|
|
|
@@ -246,8 +292,10 @@ export class FormModel {
|
|
|
246
292
|
return parser.parse(conditions.toExpression())
|
|
247
293
|
}
|
|
248
294
|
|
|
249
|
-
getList(
|
|
250
|
-
return this.
|
|
295
|
+
getList(nameOrId: string): List | undefined {
|
|
296
|
+
return this.schemaVersion === SchemaVersion.V1
|
|
297
|
+
? this.lists.find((list) => list.name === nameOrId)
|
|
298
|
+
: this.lists.find((list) => list.id === nameOrId)
|
|
251
299
|
}
|
|
252
300
|
|
|
253
301
|
/**
|
|
@@ -448,6 +496,25 @@ export class FormModel {
|
|
|
448
496
|
}
|
|
449
497
|
}
|
|
450
498
|
}
|
|
499
|
+
|
|
500
|
+
getComponentById(componentId: string): ComponentDef | undefined {
|
|
501
|
+
return this.componentDefIdMap.get(componentId)
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
getListById(listId: string): List | undefined {
|
|
505
|
+
return this.listDefIdMap.get(listId)
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
/**
|
|
509
|
+
* Returns a condition by its ID. O(n) lookup time.
|
|
510
|
+
* @param conditionId
|
|
511
|
+
* @returns
|
|
512
|
+
*/
|
|
513
|
+
getConditionById(conditionId: string): ConditionWrapperV2 | undefined {
|
|
514
|
+
return this.def.conditions
|
|
515
|
+
.filter(isConditionWrapperV2)
|
|
516
|
+
.find((condition) => condition.id === conditionId)
|
|
517
|
+
}
|
|
451
518
|
}
|
|
452
519
|
|
|
453
520
|
/**
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { type SubmitResponsePayload } from '@defra/forms-model'
|
|
2
2
|
|
|
3
3
|
import { config } from '~/src/config/index.js'
|
|
4
|
-
import { type DatePartsState } from '~/src/server/plugins/engine/components/DatePartsField.js'
|
|
5
|
-
import { type MonthYearState } from '~/src/server/plugins/engine/components/MonthYearField.js'
|
|
6
4
|
import { type UkAddressState } from '~/src/server/plugins/engine/components/UkAddressField.js'
|
|
7
5
|
import { FileUploadField } from '~/src/server/plugins/engine/components/index.js'
|
|
6
|
+
import {
|
|
7
|
+
type DatePartsState,
|
|
8
|
+
type MonthYearState
|
|
9
|
+
} from '~/src/server/plugins/engine/components/types.js'
|
|
8
10
|
import { type checkFormStatus } from '~/src/server/plugins/engine/helpers.js'
|
|
9
11
|
import { type FormModel } from '~/src/server/plugins/engine/models/index.js'
|
|
10
12
|
import {
|
|
@@ -253,8 +253,9 @@ describe('FileUploadPageController', () => {
|
|
|
253
253
|
)
|
|
254
254
|
|
|
255
255
|
expect(request.logger.error).toHaveBeenCalledWith(
|
|
256
|
+
expect.any(Error),
|
|
256
257
|
expect.stringContaining(
|
|
257
|
-
'Exceeded cumulative retry delay for some-id
|
|
258
|
+
'[uploadTimeout] Exceeded cumulative retry delay for uploadId: some-id at depth: 7 - re-initiating new upload'
|
|
258
259
|
)
|
|
259
260
|
)
|
|
260
261
|
|
|
@@ -325,9 +325,13 @@ export class FileUploadPageController extends QuestionPageController {
|
|
|
325
325
|
// Depth 1: 2000ms, Depth 2: 4000ms, Depth 3: 8000ms, Depth 4: 16000ms, Depth 5+: 30000ms (capped)
|
|
326
326
|
// A depth of 5 (or more) implies cumulative delays roughly reaching 55 seconds.
|
|
327
327
|
if (depth >= 5) {
|
|
328
|
-
|
|
328
|
+
const error = new Error(
|
|
329
329
|
`Exceeded cumulative retry delay for ${uploadId} (depth: ${depth}). Re-initiating a new upload.`
|
|
330
330
|
)
|
|
331
|
+
request.logger.error(
|
|
332
|
+
error,
|
|
333
|
+
`[uploadTimeout] Exceeded cumulative retry delay for uploadId: ${uploadId} at depth: ${depth} - re-initiating new upload`
|
|
334
|
+
)
|
|
331
335
|
await this.initiateAndStoreNewUpload(request, state)
|
|
332
336
|
throw Boom.gatewayTimeout(
|
|
333
337
|
`Timed out waiting for ${uploadId} after cumulative retries exceeding ${((CDP_UPLOAD_TIMEOUT_MS - 5000) / 1000).toFixed(0)} seconds`
|
|
@@ -335,7 +339,7 @@ export class FileUploadPageController extends QuestionPageController {
|
|
|
335
339
|
}
|
|
336
340
|
const delay = getExponentialBackoffDelay(depth)
|
|
337
341
|
request.logger.info(
|
|
338
|
-
`Waiting ${delay / 1000} seconds for ${uploadId} to complete (depth: ${depth})`
|
|
342
|
+
`[uploadRetry] Waiting ${delay / 1000} seconds for uploadId: ${uploadId} to complete (retry depth: ${depth})`
|
|
339
343
|
)
|
|
340
344
|
await wait(delay)
|
|
341
345
|
return this.checkUploadStatus(request, state, depth + 1)
|
|
@@ -2,7 +2,11 @@ import { existsSync } from 'fs'
|
|
|
2
2
|
import { dirname, join } from 'path'
|
|
3
3
|
import { fileURLToPath } from 'url'
|
|
4
4
|
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
getErrorMessage,
|
|
7
|
+
hasFormComponents,
|
|
8
|
+
slugSchema
|
|
9
|
+
} from '@defra/forms-model'
|
|
6
10
|
import Boom from '@hapi/boom'
|
|
7
11
|
import {
|
|
8
12
|
type Plugin,
|
|
@@ -773,10 +777,10 @@ export const plugin = {
|
|
|
773
777
|
request: FormRequest,
|
|
774
778
|
h: Pick<ResponseToolkit, 'response'>
|
|
775
779
|
) => {
|
|
780
|
+
const { uploadId } = request.params as unknown as {
|
|
781
|
+
uploadId: string
|
|
782
|
+
}
|
|
776
783
|
try {
|
|
777
|
-
const { uploadId } = request.params as unknown as {
|
|
778
|
-
uploadId: string
|
|
779
|
-
}
|
|
780
784
|
const status = await getUploadStatus(uploadId)
|
|
781
785
|
|
|
782
786
|
if (!status) {
|
|
@@ -785,10 +789,10 @@ export const plugin = {
|
|
|
785
789
|
|
|
786
790
|
return h.response(status)
|
|
787
791
|
} catch (error) {
|
|
792
|
+
const errMsg = getErrorMessage(error)
|
|
788
793
|
request.logger.error(
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
error
|
|
794
|
+
errMsg,
|
|
795
|
+
`[uploadStatusFailed] Upload status check failed for uploadId: ${uploadId} - ${errMsg}`
|
|
792
796
|
)
|
|
793
797
|
return h.response({ error: 'Status check error' }).code(500)
|
|
794
798
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type SubmitResponsePayload } from '@defra/forms-model'
|
|
1
|
+
import { getErrorMessage, type SubmitResponsePayload } from '@defra/forms-model'
|
|
2
2
|
|
|
3
3
|
import { config } from '~/src/config/index.js'
|
|
4
4
|
import { escapeMarkdown } from '~/src/server/plugins/engine/components/helpers.js'
|
|
@@ -56,7 +56,11 @@ export async function submit(
|
|
|
56
56
|
|
|
57
57
|
request.logger.info(logTags, 'Email sent successfully')
|
|
58
58
|
} catch (err) {
|
|
59
|
-
|
|
59
|
+
const errMsg = getErrorMessage(err)
|
|
60
|
+
request.logger.error(
|
|
61
|
+
errMsg,
|
|
62
|
+
`[emailSendFailed] Error sending notification email - templateId: ${templateId} - recipient: ${emailAddress} - ${errMsg}`
|
|
63
|
+
)
|
|
60
64
|
|
|
61
65
|
throw err
|
|
62
66
|
}
|
|
@@ -1,30 +1,11 @@
|
|
|
1
1
|
{%- from "govuk/components/tag/macro.njk" import govukTag -%}
|
|
2
2
|
|
|
3
|
-
{
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
{% case "dev" %}
|
|
8
|
-
{% set text = "Development" %}
|
|
9
|
-
{% set classes = "govuk-tag--grey" %}
|
|
10
|
-
{% case "test" %}
|
|
11
|
-
{% set text = "Test" %}
|
|
12
|
-
{% set classes = "govuk-tag--yellow" %}
|
|
13
|
-
{% case "ext-test" %}
|
|
14
|
-
{% set text = "External test" %}
|
|
15
|
-
{% set classes = "govuk-tag--yellow" %}
|
|
16
|
-
{% case "perf-test" %}
|
|
17
|
-
{% set text = "Performance test" %}
|
|
18
|
-
{% set classes = "govuk-tag--yellow" %}
|
|
19
|
-
{% case "prod" %}
|
|
20
|
-
{% set text = "Production" %}
|
|
21
|
-
{% set classes = "govuk-tag--red" %}
|
|
22
|
-
{% default %}
|
|
23
|
-
{% set text = params.env | replace("-", " ") | capitalize %}
|
|
24
|
-
{% set classes = "govuk-tag--grey" %}
|
|
25
|
-
{% endswitch -%}
|
|
3
|
+
{% set tagEnv = " app-tag--env" %}
|
|
4
|
+
|
|
5
|
+
{% set text = params.env | replace("-", " ") | capitalize %}
|
|
6
|
+
{% set classes = "govuk-tag--grey" %}
|
|
26
7
|
|
|
27
8
|
{{ govukTag({
|
|
28
9
|
text: text,
|
|
29
|
-
classes: classes +
|
|
10
|
+
classes: classes + tagEnv
|
|
30
11
|
}) }}
|
|
@@ -1,66 +1,28 @@
|
|
|
1
1
|
import { renderMacro } from '~/test/helpers/component-helpers.js'
|
|
2
2
|
|
|
3
3
|
describe('Tag environment component', () => {
|
|
4
|
-
|
|
5
|
-
{
|
|
6
|
-
text: 'Local',
|
|
7
|
-
env: 'local',
|
|
8
|
-
colour: 'green'
|
|
9
|
-
},
|
|
10
|
-
{
|
|
11
|
-
text: 'Development',
|
|
12
|
-
env: 'dev',
|
|
13
|
-
colour: 'grey'
|
|
14
|
-
},
|
|
15
|
-
{
|
|
16
|
-
text: 'Test',
|
|
17
|
-
env: 'test',
|
|
18
|
-
colour: 'yellow'
|
|
19
|
-
},
|
|
20
|
-
{
|
|
21
|
-
text: 'External test',
|
|
22
|
-
env: 'ext-test',
|
|
23
|
-
colour: 'yellow'
|
|
24
|
-
},
|
|
25
|
-
{
|
|
26
|
-
text: 'Performance test',
|
|
27
|
-
env: 'perf-test',
|
|
28
|
-
colour: 'yellow'
|
|
29
|
-
},
|
|
30
|
-
{
|
|
31
|
-
text: 'Production',
|
|
32
|
-
env: 'prod',
|
|
33
|
-
colour: 'red'
|
|
34
|
-
},
|
|
35
|
-
{
|
|
36
|
-
text: 'Unknown environment',
|
|
37
|
-
env: 'unknown-environment',
|
|
38
|
-
colour: 'grey'
|
|
39
|
-
}
|
|
40
|
-
])('Environment: $text', ({ text, env, colour }) => {
|
|
41
|
-
let $component = /** @type {HTMLElement | null} */ (null)
|
|
4
|
+
let $component = /** @type {HTMLElement | null} */ (null)
|
|
42
5
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
6
|
+
beforeEach(() => {
|
|
7
|
+
const { container } = renderMacro(
|
|
8
|
+
'appTagEnv',
|
|
9
|
+
'components/tag-env/macro.njk',
|
|
10
|
+
{ params: { env: 'Devtool' } }
|
|
11
|
+
)
|
|
49
12
|
|
|
50
|
-
|
|
51
|
-
|
|
13
|
+
$component = container.getByRole('strong')
|
|
14
|
+
})
|
|
52
15
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
16
|
+
it('should render contents', () => {
|
|
17
|
+
expect($component).toBeInTheDocument()
|
|
18
|
+
expect($component).toHaveClass('govuk-tag')
|
|
19
|
+
})
|
|
57
20
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
21
|
+
it('should have text content', () => {
|
|
22
|
+
expect($component).toHaveTextContent('Devtool')
|
|
23
|
+
})
|
|
61
24
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
})
|
|
25
|
+
it('should use environment colour', () => {
|
|
26
|
+
expect($component).toHaveClass(`govuk-tag--grey`)
|
|
65
27
|
})
|
|
66
28
|
})
|
|
@@ -25,10 +25,14 @@ export default {
|
|
|
25
25
|
stack: response.stack
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
-
request.
|
|
28
|
+
request.logger.error(
|
|
29
|
+
error,
|
|
30
|
+
`[httpError] HTTP ${statusCode} error occurred - ${response.message} - path: ${request.path} - method: ${request.method}`
|
|
31
|
+
)
|
|
29
32
|
|
|
30
33
|
return h.response(error).code(statusCode)
|
|
31
34
|
}
|
|
35
|
+
|
|
32
36
|
return h.continue
|
|
33
37
|
})
|
|
34
38
|
}
|
|
@@ -73,7 +73,9 @@ export function devtoolContext(_request) {
|
|
|
73
73
|
// eslint-disable-next-line -- Allow JSON type 'any'
|
|
74
74
|
webpackManifest = JSON.parse(readFileSync(manifestPath, 'utf-8'))
|
|
75
75
|
} catch {
|
|
76
|
-
logger.
|
|
76
|
+
logger.info(
|
|
77
|
+
`[webpackManifestMissing] Webpack ${basename(manifestPath)} not found - running without asset manifest`
|
|
78
|
+
)
|
|
77
79
|
}
|
|
78
80
|
}
|
|
79
81
|
|
|
@@ -109,6 +109,12 @@ export function evaluate(template) {
|
|
|
109
109
|
|
|
110
110
|
environment.addGlobal('evaluate', evaluate)
|
|
111
111
|
|
|
112
|
+
export function govukRebrand() {
|
|
113
|
+
return true
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
environment.addGlobal('govukRebrand', govukRebrand())
|
|
117
|
+
|
|
112
118
|
/**
|
|
113
119
|
* @import { NunjucksContext } from '~/src/server/plugins/nunjucks/types.js'
|
|
114
120
|
* @import { FormSubmissionError } from '~/src/server/plugins/engine/types.js'
|