@defra/forms-engine-plugin 0.1.9 → 0.1.10
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/.server/server/forms/test.yaml +363 -0
- package/.server/server/utils/file-form-service.js +134 -0
- package/.server/server/utils/file-form-service.js.map +1 -0
- package/.server/server/utils/file-form-service.test.js +52 -0
- package/.server/server/utils/file-form-service.test.js.map +1 -0
- package/README.md +3 -1
- package/package.json +6 -3
- package/src/server/forms/test.yaml +363 -0
- package/src/server/plugins/engine/components/AutocompleteField.test.ts +5 -5
- package/src/server/plugins/engine/components/CheckboxesField.test.ts +7 -7
- package/src/server/plugins/engine/components/List.test.ts +3 -0
- package/src/server/plugins/engine/components/RadiosField.test.ts +5 -5
- package/src/server/plugins/engine/components/SelectField.test.ts +5 -5
- package/src/server/plugins/engine/pageControllers/QuestionPageController.test.ts +4 -0
- package/src/server/utils/file-form-service.js +144 -0
- package/src/server/utils/file-form-service.test.js +79 -0
|
@@ -0,0 +1,363 @@
|
|
|
1
|
+
---
|
|
2
|
+
startPage: "/start"
|
|
3
|
+
pages:
|
|
4
|
+
- title: Start
|
|
5
|
+
path: "/start"
|
|
6
|
+
components: []
|
|
7
|
+
next:
|
|
8
|
+
- path: "/uk-passport"
|
|
9
|
+
controller: StartPageController
|
|
10
|
+
- path: "/uk-passport"
|
|
11
|
+
components:
|
|
12
|
+
- type: YesNoField
|
|
13
|
+
name: ukPassport
|
|
14
|
+
title: Do you have a UK passport?
|
|
15
|
+
options:
|
|
16
|
+
required: true
|
|
17
|
+
schema: {}
|
|
18
|
+
section: checkBeforeYouStart
|
|
19
|
+
next:
|
|
20
|
+
- path: "/how-many-people"
|
|
21
|
+
- path: "/no-uk-passport"
|
|
22
|
+
condition: doesntHaveUKPassport
|
|
23
|
+
title: Do you have a UK passport?
|
|
24
|
+
- path: "/no-uk-passport"
|
|
25
|
+
title: You're not eligible for this service
|
|
26
|
+
components:
|
|
27
|
+
- type: Html
|
|
28
|
+
name: html
|
|
29
|
+
title: Html
|
|
30
|
+
content: >-
|
|
31
|
+
<p class="govuk-body">
|
|
32
|
+
If you still think you're eligible please contact the Foreign and Commonwealth Office.
|
|
33
|
+
</p>
|
|
34
|
+
options: {}
|
|
35
|
+
schema: {}
|
|
36
|
+
next: []
|
|
37
|
+
- path: "/how-many-people"
|
|
38
|
+
section: applicantDetails
|
|
39
|
+
components:
|
|
40
|
+
- options:
|
|
41
|
+
classes: govuk-input--width-10
|
|
42
|
+
required: true
|
|
43
|
+
type: SelectField
|
|
44
|
+
name: numberOfApplicants
|
|
45
|
+
title: How many applicants are there?
|
|
46
|
+
list: numberOfApplicants
|
|
47
|
+
next:
|
|
48
|
+
- path: "/applicant-one"
|
|
49
|
+
title: How many applicants are there?
|
|
50
|
+
- path: "/applicant-one"
|
|
51
|
+
title: Applicant 1
|
|
52
|
+
section: applicantOneDetails
|
|
53
|
+
components:
|
|
54
|
+
- type: Html
|
|
55
|
+
name: html
|
|
56
|
+
title: Html
|
|
57
|
+
content: <p class="govuk-body">Provide the details as they appear on your passport.</p>
|
|
58
|
+
options: {}
|
|
59
|
+
schema: {}
|
|
60
|
+
- type: TextField
|
|
61
|
+
name: firstName
|
|
62
|
+
title: First name
|
|
63
|
+
options:
|
|
64
|
+
required: true
|
|
65
|
+
schema: {}
|
|
66
|
+
- options:
|
|
67
|
+
required: false
|
|
68
|
+
optionalText: false
|
|
69
|
+
type: TextField
|
|
70
|
+
name: middleName
|
|
71
|
+
title: Middle name
|
|
72
|
+
hint: If you have a middle name on your passport you must include it here
|
|
73
|
+
schema: {}
|
|
74
|
+
- type: TextField
|
|
75
|
+
name: lastName
|
|
76
|
+
title: Surname
|
|
77
|
+
options:
|
|
78
|
+
required: true
|
|
79
|
+
schema: {}
|
|
80
|
+
next:
|
|
81
|
+
- path: "/applicant-one-address"
|
|
82
|
+
- path: "/applicant-one-address"
|
|
83
|
+
section: applicantOneDetails
|
|
84
|
+
components:
|
|
85
|
+
- type: UkAddressField
|
|
86
|
+
name: address
|
|
87
|
+
title: Address
|
|
88
|
+
options:
|
|
89
|
+
required: true
|
|
90
|
+
schema: {}
|
|
91
|
+
next:
|
|
92
|
+
- path: "/applicant-two"
|
|
93
|
+
condition: moreThanOneApplicant
|
|
94
|
+
- path: "/contact-details"
|
|
95
|
+
title: Address
|
|
96
|
+
- path: "/applicant-two"
|
|
97
|
+
title: Applicant 2
|
|
98
|
+
section: applicantTwoDetails
|
|
99
|
+
components:
|
|
100
|
+
- type: Html
|
|
101
|
+
name: html
|
|
102
|
+
title: Html
|
|
103
|
+
content: <p class="govuk-body">Provide the details as they appear on your passport.</p>
|
|
104
|
+
options: {}
|
|
105
|
+
schema: {}
|
|
106
|
+
- type: TextField
|
|
107
|
+
name: firstName
|
|
108
|
+
title: First name
|
|
109
|
+
options:
|
|
110
|
+
required: true
|
|
111
|
+
schema: {}
|
|
112
|
+
- options:
|
|
113
|
+
required: false
|
|
114
|
+
optionalText: false
|
|
115
|
+
type: TextField
|
|
116
|
+
name: middleName
|
|
117
|
+
title: Middle name
|
|
118
|
+
hint: If you have a middle name on your passport you must include it here
|
|
119
|
+
schema: {}
|
|
120
|
+
- type: TextField
|
|
121
|
+
name: lastName
|
|
122
|
+
title: Surname
|
|
123
|
+
options:
|
|
124
|
+
required: true
|
|
125
|
+
schema: {}
|
|
126
|
+
next:
|
|
127
|
+
- path: "/applicant-two-address"
|
|
128
|
+
- path: "/applicant-two-address"
|
|
129
|
+
section: applicantTwoDetails
|
|
130
|
+
components:
|
|
131
|
+
- type: UkAddressField
|
|
132
|
+
name: address
|
|
133
|
+
title: Address
|
|
134
|
+
options:
|
|
135
|
+
required: true
|
|
136
|
+
schema: {}
|
|
137
|
+
next:
|
|
138
|
+
- path: "/applicant-three"
|
|
139
|
+
condition: moreThanTwoApplicants
|
|
140
|
+
- path: "/contact-details"
|
|
141
|
+
title: Address
|
|
142
|
+
- path: "/applicant-three"
|
|
143
|
+
title: Applicant 3
|
|
144
|
+
section: applicantThreeDetails
|
|
145
|
+
components:
|
|
146
|
+
- type: Html
|
|
147
|
+
name: html
|
|
148
|
+
title: Html
|
|
149
|
+
content: <p class="govuk-body">Provide the details as they appear on your passport.</p>
|
|
150
|
+
options: {}
|
|
151
|
+
schema: {}
|
|
152
|
+
- type: TextField
|
|
153
|
+
name: firstName
|
|
154
|
+
title: First name
|
|
155
|
+
options:
|
|
156
|
+
required: true
|
|
157
|
+
schema: {}
|
|
158
|
+
- options:
|
|
159
|
+
required: false
|
|
160
|
+
optionalText: false
|
|
161
|
+
type: TextField
|
|
162
|
+
name: middleName
|
|
163
|
+
title: Middle name
|
|
164
|
+
hint: If you have a middle name on your passport you must include it here
|
|
165
|
+
schema: {}
|
|
166
|
+
- type: TextField
|
|
167
|
+
name: lastName
|
|
168
|
+
title: Surname
|
|
169
|
+
options:
|
|
170
|
+
required: true
|
|
171
|
+
schema: {}
|
|
172
|
+
next:
|
|
173
|
+
- path: "/applicant-three-address"
|
|
174
|
+
- path: "/applicant-three-address"
|
|
175
|
+
section: applicantThreeDetails
|
|
176
|
+
components:
|
|
177
|
+
- type: UkAddressField
|
|
178
|
+
name: address
|
|
179
|
+
title: Address
|
|
180
|
+
options:
|
|
181
|
+
required: true
|
|
182
|
+
schema: {}
|
|
183
|
+
next:
|
|
184
|
+
- path: "/applicant-four"
|
|
185
|
+
condition: moreThanThreeApplicants
|
|
186
|
+
- path: "/contact-details"
|
|
187
|
+
title: Address
|
|
188
|
+
- path: "/applicant-four"
|
|
189
|
+
title: Applicant 4
|
|
190
|
+
section: applicantFourDetails
|
|
191
|
+
components:
|
|
192
|
+
- type: Html
|
|
193
|
+
name: html
|
|
194
|
+
title: Html
|
|
195
|
+
content: <p class="govuk-body">Provide the details as they appear on your passport.</p>
|
|
196
|
+
options: {}
|
|
197
|
+
schema: {}
|
|
198
|
+
- type: TextField
|
|
199
|
+
name: firstName
|
|
200
|
+
title: First name
|
|
201
|
+
options:
|
|
202
|
+
required: true
|
|
203
|
+
schema: {}
|
|
204
|
+
- options:
|
|
205
|
+
required: false
|
|
206
|
+
optionalText: false
|
|
207
|
+
type: TextField
|
|
208
|
+
name: middleName
|
|
209
|
+
title: Middle name
|
|
210
|
+
hint: If you have a middle name on your passport you must include it here
|
|
211
|
+
schema: {}
|
|
212
|
+
- type: TextField
|
|
213
|
+
name: lastName
|
|
214
|
+
title: Surname
|
|
215
|
+
options:
|
|
216
|
+
required: true
|
|
217
|
+
schema: {}
|
|
218
|
+
next:
|
|
219
|
+
- path: "/applicant-four-address"
|
|
220
|
+
- path: "/applicant-four-address"
|
|
221
|
+
section: applicantFourDetails
|
|
222
|
+
components:
|
|
223
|
+
- type: UkAddressField
|
|
224
|
+
name: address
|
|
225
|
+
title: Address
|
|
226
|
+
options:
|
|
227
|
+
required: true
|
|
228
|
+
schema: {}
|
|
229
|
+
next:
|
|
230
|
+
- path: "/contact-details"
|
|
231
|
+
title: Address
|
|
232
|
+
- path: "/contact-details"
|
|
233
|
+
section: applicantDetails
|
|
234
|
+
components:
|
|
235
|
+
- type: TelephoneNumberField
|
|
236
|
+
name: phoneNumber
|
|
237
|
+
title: Phone number
|
|
238
|
+
hint: If you haven't got a UK phone number, include country code
|
|
239
|
+
options:
|
|
240
|
+
required: true
|
|
241
|
+
schema: {}
|
|
242
|
+
- type: EmailAddressField
|
|
243
|
+
name: emailAddress
|
|
244
|
+
title: Your email address
|
|
245
|
+
options:
|
|
246
|
+
required: true
|
|
247
|
+
schema: {}
|
|
248
|
+
next:
|
|
249
|
+
- path: "/summary"
|
|
250
|
+
title: Applicant contact details
|
|
251
|
+
- path: "/summary"
|
|
252
|
+
controller: SummaryPageController
|
|
253
|
+
title: Summary
|
|
254
|
+
components: []
|
|
255
|
+
next: []
|
|
256
|
+
lists:
|
|
257
|
+
- name: numberOfApplicants
|
|
258
|
+
title: Number of people
|
|
259
|
+
type: number
|
|
260
|
+
items:
|
|
261
|
+
- text: '1'
|
|
262
|
+
value: 1
|
|
263
|
+
description: ''
|
|
264
|
+
condition: ''
|
|
265
|
+
- text: '2'
|
|
266
|
+
value: 2
|
|
267
|
+
description: ''
|
|
268
|
+
condition: ''
|
|
269
|
+
- text: '3'
|
|
270
|
+
value: 3
|
|
271
|
+
description: ''
|
|
272
|
+
condition: ''
|
|
273
|
+
- text: '4'
|
|
274
|
+
value: 4
|
|
275
|
+
description: ''
|
|
276
|
+
condition: ''
|
|
277
|
+
sections:
|
|
278
|
+
- name: checkBeforeYouStart
|
|
279
|
+
title: Check before you start
|
|
280
|
+
- name: applicantDetails
|
|
281
|
+
title: Applicant details
|
|
282
|
+
- name: applicantOneDetails
|
|
283
|
+
title: Applicant 1
|
|
284
|
+
- name: applicantTwoDetails
|
|
285
|
+
title: Applicant 2
|
|
286
|
+
- name: applicantThreeDetails
|
|
287
|
+
title: Applicant 3
|
|
288
|
+
- name: applicantFourDetails
|
|
289
|
+
title: Applicant 4
|
|
290
|
+
phaseBanner: {}
|
|
291
|
+
declaration: <p class="govuk-body">All the answers you have provided are true to the
|
|
292
|
+
best of your knowledge.</p>
|
|
293
|
+
conditions:
|
|
294
|
+
- name: hasUKPassport
|
|
295
|
+
displayName: hasUKPassport
|
|
296
|
+
value:
|
|
297
|
+
name: hasUKPassport
|
|
298
|
+
conditions:
|
|
299
|
+
- field:
|
|
300
|
+
name: checkBeforeYouStart.ukPassport
|
|
301
|
+
type: YesNoField
|
|
302
|
+
display: Do you have a UK passport?
|
|
303
|
+
operator: is
|
|
304
|
+
value:
|
|
305
|
+
type: Value
|
|
306
|
+
value: 'true'
|
|
307
|
+
display: 'true'
|
|
308
|
+
- name: doesntHaveUKPassport
|
|
309
|
+
displayName: doesntHaveUKPassport
|
|
310
|
+
value:
|
|
311
|
+
name: doesntHaveUKPassport
|
|
312
|
+
conditions:
|
|
313
|
+
- field:
|
|
314
|
+
name: checkBeforeYouStart.ukPassport
|
|
315
|
+
type: YesNoField
|
|
316
|
+
display: Do you have a UK passport?
|
|
317
|
+
operator: is
|
|
318
|
+
value:
|
|
319
|
+
type: Value
|
|
320
|
+
value: 'false'
|
|
321
|
+
display: 'false'
|
|
322
|
+
- name: moreThanOneApplicant
|
|
323
|
+
displayName: moreThanOneApplicant
|
|
324
|
+
value:
|
|
325
|
+
name: moreThanOneApplicant
|
|
326
|
+
conditions:
|
|
327
|
+
- field:
|
|
328
|
+
name: applicantDetails.numberOfApplicants
|
|
329
|
+
type: SelectField
|
|
330
|
+
display: How many applicants are there?
|
|
331
|
+
operator: is more than
|
|
332
|
+
value:
|
|
333
|
+
type: Value
|
|
334
|
+
value: '1'
|
|
335
|
+
display: '1'
|
|
336
|
+
- name: moreThanTwoApplicants
|
|
337
|
+
displayName: moreThanTwoApplicants
|
|
338
|
+
value:
|
|
339
|
+
name: moreThanTwoApplicants
|
|
340
|
+
conditions:
|
|
341
|
+
- field:
|
|
342
|
+
name: applicantDetails.numberOfApplicants
|
|
343
|
+
type: SelectField
|
|
344
|
+
display: How many applicants are there?
|
|
345
|
+
operator: is more than
|
|
346
|
+
value:
|
|
347
|
+
type: Value
|
|
348
|
+
value: '2'
|
|
349
|
+
display: '2'
|
|
350
|
+
- name: moreThanThreeApplicants
|
|
351
|
+
displayName: moreThanThreeApplicants
|
|
352
|
+
value:
|
|
353
|
+
name: moreThanThreeApplicants
|
|
354
|
+
conditions:
|
|
355
|
+
- field:
|
|
356
|
+
name: applicantDetails.numberOfApplicants
|
|
357
|
+
type: SelectField
|
|
358
|
+
display: How many applicants are there?
|
|
359
|
+
operator: is more than
|
|
360
|
+
value:
|
|
361
|
+
type: Value
|
|
362
|
+
value: '3'
|
|
363
|
+
display: '3'
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import fs from 'fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import YAML from 'yaml';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* FileFormService class
|
|
7
|
+
*/
|
|
8
|
+
export class FileFormService {
|
|
9
|
+
/**
|
|
10
|
+
* The map of form metadatas by slug
|
|
11
|
+
* @type {Map<string, FormMetadata>}
|
|
12
|
+
*/
|
|
13
|
+
#metadata = new Map();
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* The map of form definitions by id
|
|
17
|
+
* @type {Map<string, FormDefinition>}
|
|
18
|
+
*/
|
|
19
|
+
#definition = new Map();
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Add form from a file
|
|
23
|
+
* @param {string} filepath - the file path
|
|
24
|
+
* @param {FormMetadata} metadata - the metadata to use for this form
|
|
25
|
+
* @returns {Promise<FormDefinition>}
|
|
26
|
+
*/
|
|
27
|
+
async addForm(filepath, metadata) {
|
|
28
|
+
const definition = await this.readForm(filepath);
|
|
29
|
+
this.#metadata.set(metadata.slug, metadata);
|
|
30
|
+
this.#definition.set(metadata.id, definition);
|
|
31
|
+
return definition;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Read the form definition from file
|
|
36
|
+
* @param {string} filepath - the file path
|
|
37
|
+
* @returns {Promise<FormDefinition>}
|
|
38
|
+
*/
|
|
39
|
+
async readForm(filepath) {
|
|
40
|
+
const ext = path.extname(filepath).toLowerCase();
|
|
41
|
+
switch (ext) {
|
|
42
|
+
case '.json':
|
|
43
|
+
return this.readJsonForm(filepath);
|
|
44
|
+
case '.yaml':
|
|
45
|
+
return this.readYamlForm(filepath);
|
|
46
|
+
default:
|
|
47
|
+
throw new Error(`Invalid file extension '${ext}'`);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Read the form definition from a json file
|
|
53
|
+
* @param {string} filepath - the file path
|
|
54
|
+
* @returns {Promise<FormDefinition>}
|
|
55
|
+
*/
|
|
56
|
+
async readJsonForm(filepath) {
|
|
57
|
+
/**
|
|
58
|
+
* @type {FormDefinition}
|
|
59
|
+
*/
|
|
60
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
|
61
|
+
const definition = JSON.parse(await fs.readFile(filepath, 'utf8'));
|
|
62
|
+
return definition;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Read the form definition from a yaml file
|
|
67
|
+
* @param {string} filepath - the file path
|
|
68
|
+
* @returns {Promise<FormDefinition>}
|
|
69
|
+
*/
|
|
70
|
+
async readYamlForm(filepath) {
|
|
71
|
+
/**
|
|
72
|
+
* @type {FormDefinition}
|
|
73
|
+
*/
|
|
74
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
|
75
|
+
const definition = YAML.parse(await fs.readFile(filepath, 'utf8'));
|
|
76
|
+
return definition;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Get the form metadata by slug
|
|
81
|
+
* @param {string} slug - the form slug
|
|
82
|
+
* @returns {FormMetadata}
|
|
83
|
+
*/
|
|
84
|
+
getFormMetadata(slug) {
|
|
85
|
+
const metadata = this.#metadata.get(slug);
|
|
86
|
+
if (!metadata) {
|
|
87
|
+
throw new Error(`Form metadata '${slug}' not found`);
|
|
88
|
+
}
|
|
89
|
+
return metadata;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Get the form defintion by id
|
|
94
|
+
* @param {string} id - the form id
|
|
95
|
+
* @returns {FormDefinition}
|
|
96
|
+
*/
|
|
97
|
+
getFormDefinition(id) {
|
|
98
|
+
const definition = this.#definition.get(id);
|
|
99
|
+
if (!definition) {
|
|
100
|
+
throw new Error(`Form definition '${id}' not found`);
|
|
101
|
+
}
|
|
102
|
+
return definition;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Returns a FormsService compliant interface
|
|
107
|
+
* @returns {import('~/src/server/types.js').FormsService}
|
|
108
|
+
*/
|
|
109
|
+
toFormsService() {
|
|
110
|
+
return {
|
|
111
|
+
/**
|
|
112
|
+
* Get the form metadata by slug
|
|
113
|
+
* @param {string} slug
|
|
114
|
+
* @returns {Promise<FormMetadata>}
|
|
115
|
+
*/
|
|
116
|
+
getFormMetadata: slug => {
|
|
117
|
+
return Promise.resolve(this.getFormMetadata(slug));
|
|
118
|
+
},
|
|
119
|
+
/**
|
|
120
|
+
* Get the form defintion by id
|
|
121
|
+
* @param {string} id
|
|
122
|
+
* @returns {Promise<FormDefinition>}
|
|
123
|
+
*/
|
|
124
|
+
getFormDefinition: id => {
|
|
125
|
+
return Promise.resolve(this.getFormDefinition(id));
|
|
126
|
+
}
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* @import { FormMetadata, FormDefinition } from '@defra/forms-model'
|
|
133
|
+
*/
|
|
134
|
+
//# sourceMappingURL=file-form-service.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"file-form-service.js","names":["fs","path","YAML","FileFormService","metadata","Map","definition","addForm","filepath","readForm","set","slug","id","ext","extname","toLowerCase","readJsonForm","readYamlForm","Error","JSON","parse","readFile","getFormMetadata","get","getFormDefinition","toFormsService","Promise","resolve"],"sources":["../../../src/server/utils/file-form-service.js"],"sourcesContent":["import fs from 'fs/promises'\nimport path from 'node:path'\n\nimport YAML from 'yaml'\n\n/**\n * FileFormService class\n */\nexport class FileFormService {\n /**\n * The map of form metadatas by slug\n * @type {Map<string, FormMetadata>}\n */\n #metadata = new Map()\n\n /**\n * The map of form definitions by id\n * @type {Map<string, FormDefinition>}\n */\n #definition = new Map()\n\n /**\n * Add form from a file\n * @param {string} filepath - the file path\n * @param {FormMetadata} metadata - the metadata to use for this form\n * @returns {Promise<FormDefinition>}\n */\n async addForm(filepath, metadata) {\n const definition = await this.readForm(filepath)\n\n this.#metadata.set(metadata.slug, metadata)\n this.#definition.set(metadata.id, definition)\n\n return definition\n }\n\n /**\n * Read the form definition from file\n * @param {string} filepath - the file path\n * @returns {Promise<FormDefinition>}\n */\n async readForm(filepath) {\n const ext = path.extname(filepath).toLowerCase()\n\n switch (ext) {\n case '.json':\n return this.readJsonForm(filepath)\n case '.yaml':\n return this.readYamlForm(filepath)\n default:\n throw new Error(`Invalid file extension '${ext}'`)\n }\n }\n\n /**\n * Read the form definition from a json file\n * @param {string} filepath - the file path\n * @returns {Promise<FormDefinition>}\n */\n async readJsonForm(filepath) {\n /**\n * @type {FormDefinition}\n */\n // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment\n const definition = JSON.parse(await fs.readFile(filepath, 'utf8'))\n\n return definition\n }\n\n /**\n * Read the form definition from a yaml file\n * @param {string} filepath - the file path\n * @returns {Promise<FormDefinition>}\n */\n async readYamlForm(filepath) {\n /**\n * @type {FormDefinition}\n */\n // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment\n const definition = YAML.parse(await fs.readFile(filepath, 'utf8'))\n\n return definition\n }\n\n /**\n * Get the form metadata by slug\n * @param {string} slug - the form slug\n * @returns {FormMetadata}\n */\n getFormMetadata(slug) {\n const metadata = this.#metadata.get(slug)\n\n if (!metadata) {\n throw new Error(`Form metadata '${slug}' not found`)\n }\n\n return metadata\n }\n\n /**\n * Get the form defintion by id\n * @param {string} id - the form id\n * @returns {FormDefinition}\n */\n getFormDefinition(id) {\n const definition = this.#definition.get(id)\n\n if (!definition) {\n throw new Error(`Form definition '${id}' not found`)\n }\n\n return definition\n }\n\n /**\n * Returns a FormsService compliant interface\n * @returns {import('~/src/server/types.js').FormsService}\n */\n toFormsService() {\n return {\n /**\n * Get the form metadata by slug\n * @param {string} slug\n * @returns {Promise<FormMetadata>}\n */\n getFormMetadata: (slug) => {\n return Promise.resolve(this.getFormMetadata(slug))\n },\n\n /**\n * Get the form defintion by id\n * @param {string} id\n * @returns {Promise<FormDefinition>}\n */\n getFormDefinition: (id) => {\n return Promise.resolve(this.getFormDefinition(id))\n }\n }\n }\n}\n\n/**\n * @import { FormMetadata, FormDefinition } from '@defra/forms-model'\n */\n"],"mappings":"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","ignoreList":[]}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { FormStatus } from "../routes/types.js";
|
|
2
|
+
import { FileFormService } from "./file-form-service.js";
|
|
3
|
+
|
|
4
|
+
// Create the metadata which is shared for all forms
|
|
5
|
+
const now = new Date();
|
|
6
|
+
const user = {
|
|
7
|
+
id: 'user',
|
|
8
|
+
displayName: 'Username'
|
|
9
|
+
};
|
|
10
|
+
const author = {
|
|
11
|
+
createdAt: now,
|
|
12
|
+
createdBy: user,
|
|
13
|
+
updatedAt: now,
|
|
14
|
+
updatedBy: user
|
|
15
|
+
};
|
|
16
|
+
const metadata = {
|
|
17
|
+
id: '95e92559-968d-44ae-8666-2b1ad3dffd31',
|
|
18
|
+
slug: 'example-form',
|
|
19
|
+
title: 'Example form',
|
|
20
|
+
organisation: 'Defra',
|
|
21
|
+
teamName: 'Team name',
|
|
22
|
+
teamEmail: 'team@defra.gov.uk',
|
|
23
|
+
submissionGuidance: "Thanks for your submission, we'll be in touch",
|
|
24
|
+
notificationEmail: 'email@domain.com',
|
|
25
|
+
...author,
|
|
26
|
+
live: author
|
|
27
|
+
};
|
|
28
|
+
describe('File Form Service', () => {
|
|
29
|
+
it('should load JSON files from disk', async () => {
|
|
30
|
+
const loader = new FileFormService();
|
|
31
|
+
const definition = await loader.addForm('src/server/forms/test.json', metadata);
|
|
32
|
+
const formsService = loader.toFormsService();
|
|
33
|
+
expect(await formsService.getFormMetadata(metadata.slug)).toBe(metadata);
|
|
34
|
+
expect(await formsService.getFormDefinition(metadata.id, FormStatus.Draft)).toBe(definition);
|
|
35
|
+
expect(() => loader.getFormMetadata('invalid-slug')).toThrow("Form metadata 'invalid-slug' not found");
|
|
36
|
+
expect(() => loader.getFormDefinition('invalid-id')).toThrow("Form definition 'invalid-id' not found");
|
|
37
|
+
});
|
|
38
|
+
it('should load YAML files from disk', async () => {
|
|
39
|
+
const loader = new FileFormService();
|
|
40
|
+
const definition = await loader.addForm('src/server/forms/test.yaml', metadata);
|
|
41
|
+
const formsService = loader.toFormsService();
|
|
42
|
+
expect(await formsService.getFormMetadata(metadata.slug)).toBe(metadata);
|
|
43
|
+
expect(await formsService.getFormDefinition(metadata.id, FormStatus.Draft)).toBe(definition);
|
|
44
|
+
expect(() => loader.getFormMetadata('invalid-slug')).toThrow("Form metadata 'invalid-slug' not found");
|
|
45
|
+
expect(() => loader.getFormDefinition('invalid-id')).toThrow("Form definition 'invalid-id' not found");
|
|
46
|
+
});
|
|
47
|
+
it("should throw if the file isn't JSON or YAML", async () => {
|
|
48
|
+
const loader = new FileFormService();
|
|
49
|
+
await expect(loader.addForm('src/server/forms/test.txt', metadata)).rejects.toThrow("Invalid file extension '.txt'");
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
//# sourceMappingURL=file-form-service.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"file-form-service.test.js","names":["FormStatus","FileFormService","now","Date","user","id","displayName","author","createdAt","createdBy","updatedAt","updatedBy","metadata","slug","title","organisation","teamName","teamEmail","submissionGuidance","notificationEmail","live","describe","it","loader","definition","addForm","formsService","toFormsService","expect","getFormMetadata","toBe","getFormDefinition","Draft","toThrow","rejects"],"sources":["../../../src/server/utils/file-form-service.test.js"],"sourcesContent":["import { FormStatus } from '~/src/server/routes/types.js'\nimport { FileFormService } from '~/src/server/utils/file-form-service.js'\n\n// Create the metadata which is shared for all forms\nconst now = new Date()\nconst user = { id: 'user', displayName: 'Username' }\nconst author = {\n createdAt: now,\n createdBy: user,\n updatedAt: now,\n updatedBy: user\n}\n\nconst metadata = {\n id: '95e92559-968d-44ae-8666-2b1ad3dffd31',\n slug: 'example-form',\n title: 'Example form',\n organisation: 'Defra',\n teamName: 'Team name',\n teamEmail: 'team@defra.gov.uk',\n submissionGuidance: \"Thanks for your submission, we'll be in touch\",\n notificationEmail: 'email@domain.com',\n ...author,\n live: author\n}\n\ndescribe('File Form Service', () => {\n it('should load JSON files from disk', async () => {\n const loader = new FileFormService()\n\n const definition = await loader.addForm(\n 'src/server/forms/test.json',\n metadata\n )\n\n const formsService = loader.toFormsService()\n expect(await formsService.getFormMetadata(metadata.slug)).toBe(metadata)\n expect(\n await formsService.getFormDefinition(metadata.id, FormStatus.Draft)\n ).toBe(definition)\n\n expect(() => loader.getFormMetadata('invalid-slug')).toThrow(\n \"Form metadata 'invalid-slug' not found\"\n )\n expect(() => loader.getFormDefinition('invalid-id')).toThrow(\n \"Form definition 'invalid-id' not found\"\n )\n })\n\n it('should load YAML files from disk', async () => {\n const loader = new FileFormService()\n\n const definition = await loader.addForm(\n 'src/server/forms/test.yaml',\n metadata\n )\n\n const formsService = loader.toFormsService()\n expect(await formsService.getFormMetadata(metadata.slug)).toBe(metadata)\n expect(\n await formsService.getFormDefinition(metadata.id, FormStatus.Draft)\n ).toBe(definition)\n\n expect(() => loader.getFormMetadata('invalid-slug')).toThrow(\n \"Form metadata 'invalid-slug' not found\"\n )\n expect(() => loader.getFormDefinition('invalid-id')).toThrow(\n \"Form definition 'invalid-id' not found\"\n )\n })\n\n it(\"should throw if the file isn't JSON or YAML\", async () => {\n const loader = new FileFormService()\n\n await expect(\n loader.addForm('src/server/forms/test.txt', metadata)\n ).rejects.toThrow(\"Invalid file extension '.txt'\")\n })\n})\n"],"mappings":"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","ignoreList":[]}
|
package/README.md
CHANGED
|
@@ -18,7 +18,9 @@ It is designed to be embedded in the frontend of a digital service and provide a
|
|
|
18
18
|
|
|
19
19
|
## Demo of DXT
|
|
20
20
|
|
|
21
|
-
|
|
21
|
+
If you are within the Defra network, [see a live demo](https://forms-engine-plugin-example-ui.dev.cdp-int.defra.cloud/example-form).
|
|
22
|
+
|
|
23
|
+
If you aren't within the Defra network, [see our example UI and run it locally](https://github.com/DEFRA/forms-engine-plugin-example-ui).
|
|
22
24
|
|
|
23
25
|
## Installation
|
|
24
26
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@defra/forms-engine-plugin",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.10",
|
|
4
4
|
"description": "Defra forms engine",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"files": [
|
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
"./file-upload.min.js": "./.public/javascripts/file-upload.min.js",
|
|
22
22
|
"./file-upload.min.js.map": "./.public/javascripts/file-upload.min.js.map",
|
|
23
23
|
"./application.min.css": "./.public/stylesheets/application.min.css",
|
|
24
|
+
"./file-form-service.js": "./.server/server/utils/file-form-service.js",
|
|
24
25
|
"./controllers/*": "./.server/server/plugins/engine/pageControllers/*",
|
|
25
26
|
"./services/*": "./.server/server/plugins/engine/services/*",
|
|
26
27
|
"./package.json": "./package.json"
|
|
@@ -33,6 +34,7 @@
|
|
|
33
34
|
"dev:debug": "concurrently \"npm run client:watch\" \"npm run server:watch:debug\" --kill-others --names \"client,server\" --prefix-colors \"red.dim,blue.dim\"",
|
|
34
35
|
"format": "npm run format:check -- --write",
|
|
35
36
|
"format:check": "prettier --cache --cache-location .cache/prettier --cache-strategy content --check \"**/*.{cjs,js,json,md,mjs,scss,ts}\"",
|
|
37
|
+
"generate-schema-docs": "node scripts/generate-schema-docs.js",
|
|
36
38
|
"postinstall": "npm run setup:husky",
|
|
37
39
|
"lint": "npm run lint:editorconfig && npm run lint:js && npm run lint:types",
|
|
38
40
|
"lint:editorconfig": "editorconfig-checker",
|
|
@@ -60,7 +62,7 @@
|
|
|
60
62
|
},
|
|
61
63
|
"license": "SEE LICENSE IN LICENSE",
|
|
62
64
|
"dependencies": {
|
|
63
|
-
"@defra/forms-model": "^3.0.
|
|
65
|
+
"@defra/forms-model": "^3.0.438",
|
|
64
66
|
"@defra/hapi-tracing": "^1.0.0",
|
|
65
67
|
"@elastic/ecs-pino-format": "^1.5.0",
|
|
66
68
|
"@hapi/boom": "^10.0.1",
|
|
@@ -102,7 +104,8 @@
|
|
|
102
104
|
"pino": "^9.6.0",
|
|
103
105
|
"pino-pretty": "^13.0.0",
|
|
104
106
|
"proxy-agent": "^6.5.0",
|
|
105
|
-
"resolve": "^1.22.10"
|
|
107
|
+
"resolve": "^1.22.10",
|
|
108
|
+
"yaml": "^2.7.1"
|
|
106
109
|
},
|
|
107
110
|
"devDependencies": {
|
|
108
111
|
"@babel/cli": "^7.26.4",
|