@defra/forms-model 3.0.702 → 3.0.704
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/dist/module/form/form-audit/index.js +12 -0
- package/dist/module/form/form-audit/index.js.map +1 -1
- package/dist/module/form/form-definition/helpers.js +20 -0
- package/dist/module/form/form-definition/helpers.js.map +1 -1
- package/dist/module/form/form-definition/index.js +29 -17
- package/dist/module/form/form-definition/index.js.map +1 -1
- package/dist/module/form/form-definition/types.js.map +1 -1
- package/dist/module/form/form-manager/errors.js +6 -1
- package/dist/module/form/form-manager/errors.js.map +1 -1
- package/dist/module/form/form-manager/types.js +13 -0
- package/dist/module/form/form-manager/types.js.map +1 -1
- package/dist/module/form/form-submission/enums.js +25 -0
- package/dist/module/form/form-submission/enums.js.map +1 -1
- package/dist/module/form/form-submission/index.js +21 -1
- package/dist/module/form/form-submission/index.js.map +1 -1
- package/dist/module/form/form-submission/types.js.map +1 -1
- package/dist/module/manage/dead-letter-queues.js +1 -0
- package/dist/module/manage/dead-letter-queues.js.map +1 -1
- package/dist/types/form/form-audit/index.d.ts +7 -0
- package/dist/types/form/form-audit/index.d.ts.map +1 -1
- package/dist/types/form/form-definition/helpers.d.ts +15 -1
- package/dist/types/form/form-definition/helpers.d.ts.map +1 -1
- package/dist/types/form/form-definition/index.d.ts.map +1 -1
- package/dist/types/form/form-definition/types.d.ts +5 -0
- package/dist/types/form/form-definition/types.d.ts.map +1 -1
- package/dist/types/form/form-manager/errors.d.ts.map +1 -1
- package/dist/types/form/form-manager/types.d.ts +2 -0
- package/dist/types/form/form-manager/types.d.ts.map +1 -1
- package/dist/types/form/form-submission/enums.d.ts +23 -0
- package/dist/types/form/form-submission/enums.d.ts.map +1 -1
- package/dist/types/form/form-submission/index.d.ts +11 -1
- package/dist/types/form/form-submission/index.d.ts.map +1 -1
- package/dist/types/form/form-submission/types.d.ts +46 -1
- package/dist/types/form/form-submission/types.d.ts.map +1 -1
- package/dist/types/manage/dead-letter-queues.d.ts +1 -0
- package/dist/types/manage/dead-letter-queues.d.ts.map +1 -1
- package/package.json +2 -2
- package/schemas/form-definition-schema.json +2 -0
- package/schemas/form-definition-v2-schema.json +63 -36
- package/src/form/form-audit/index.ts +32 -0
- package/src/form/form-definition/helpers.ts +32 -1
- package/src/form/form-definition/index.ts +66 -21
- package/src/form/form-definition/types.ts +5 -0
- package/src/form/form-manager/errors.ts +7 -1
- package/src/form/form-manager/types.ts +13 -0
- package/src/form/form-submission/enums.ts +26 -0
- package/src/form/form-submission/index.ts +49 -0
- package/src/form/form-submission/types.ts +54 -0
- package/src/manage/dead-letter-queues.ts +1 -0
|
@@ -143,42 +143,6 @@
|
|
|
143
143
|
"additionalProperties": false,
|
|
144
144
|
"title": "Output"
|
|
145
145
|
},
|
|
146
|
-
"outputs": {
|
|
147
|
-
"type": "array",
|
|
148
|
-
"description": "One or more email targets/types for submission emails",
|
|
149
|
-
"items": {
|
|
150
|
-
"type": "object",
|
|
151
|
-
"properties": {
|
|
152
|
-
"emailAddress": {
|
|
153
|
-
"type": "string",
|
|
154
|
-
"description": "Email address where form submissions are sent",
|
|
155
|
-
"format": "email",
|
|
156
|
-
"title": "Email Address"
|
|
157
|
-
},
|
|
158
|
-
"audience": {
|
|
159
|
-
"type": "string",
|
|
160
|
-
"description": "Target audience for the output (human readable or machine processable)",
|
|
161
|
-
"enum": [
|
|
162
|
-
"human",
|
|
163
|
-
"machine"
|
|
164
|
-
],
|
|
165
|
-
"title": "Audience"
|
|
166
|
-
},
|
|
167
|
-
"version": {
|
|
168
|
-
"type": "string",
|
|
169
|
-
"description": "Version identifier for the output format",
|
|
170
|
-
"title": "Version"
|
|
171
|
-
}
|
|
172
|
-
},
|
|
173
|
-
"required": [
|
|
174
|
-
"audience",
|
|
175
|
-
"version"
|
|
176
|
-
],
|
|
177
|
-
"additionalProperties": false,
|
|
178
|
-
"title": "Outputs Item"
|
|
179
|
-
},
|
|
180
|
-
"title": "Outputs"
|
|
181
|
-
},
|
|
182
146
|
"schema": {
|
|
183
147
|
"description": "Form schema version to use (2)",
|
|
184
148
|
"const": 2,
|
|
@@ -2441,9 +2405,72 @@
|
|
|
2441
2405
|
"title": "Sections Item"
|
|
2442
2406
|
},
|
|
2443
2407
|
"title": "Sections"
|
|
2408
|
+
},
|
|
2409
|
+
"outputs": {
|
|
2410
|
+
"type": "array",
|
|
2411
|
+
"description": "Outputs schema for V2 forms",
|
|
2412
|
+
"uniqueItems": true,
|
|
2413
|
+
"items": {
|
|
2414
|
+
"type": "object",
|
|
2415
|
+
"description": "Configuration for a single submission email target",
|
|
2416
|
+
"properties": {
|
|
2417
|
+
"emailAddress": {
|
|
2418
|
+
"type": "string",
|
|
2419
|
+
"description": "Email address where form submissions are sent",
|
|
2420
|
+
"format": "email",
|
|
2421
|
+
"title": "Email Address"
|
|
2422
|
+
},
|
|
2423
|
+
"audience": {
|
|
2424
|
+
"type": "string",
|
|
2425
|
+
"description": "Target audience for the output (human readable or machine processable)",
|
|
2426
|
+
"enum": [
|
|
2427
|
+
"human",
|
|
2428
|
+
"machine"
|
|
2429
|
+
],
|
|
2430
|
+
"title": "Audience"
|
|
2431
|
+
},
|
|
2432
|
+
"version": {
|
|
2433
|
+
"type": "string",
|
|
2434
|
+
"description": "Version identifier for the output format",
|
|
2435
|
+
"title": "Version"
|
|
2436
|
+
},
|
|
2437
|
+
"condition": {
|
|
2438
|
+
"type": [
|
|
2439
|
+
"array",
|
|
2440
|
+
"boolean",
|
|
2441
|
+
"number",
|
|
2442
|
+
"object",
|
|
2443
|
+
"string",
|
|
2444
|
+
"null"
|
|
2445
|
+
],
|
|
2446
|
+
"description": "Optional condition that determines if submissions are sent to this output",
|
|
2447
|
+
"enum": [
|
|
2448
|
+
"",
|
|
2449
|
+
{
|
|
2450
|
+
"ref": {
|
|
2451
|
+
"path": [
|
|
2452
|
+
"conditions"
|
|
2453
|
+
],
|
|
2454
|
+
"ancestor": "root",
|
|
2455
|
+
"in": true
|
|
2456
|
+
}
|
|
2457
|
+
}
|
|
2458
|
+
],
|
|
2459
|
+
"title": "Condition"
|
|
2460
|
+
}
|
|
2461
|
+
},
|
|
2462
|
+
"required": [
|
|
2463
|
+
"audience",
|
|
2464
|
+
"version"
|
|
2465
|
+
],
|
|
2466
|
+
"additionalProperties": false,
|
|
2467
|
+
"title": "Outputs Item"
|
|
2468
|
+
},
|
|
2469
|
+
"title": "Outputs"
|
|
2444
2470
|
}
|
|
2445
2471
|
},
|
|
2446
2472
|
"required": [
|
|
2473
|
+
"schema",
|
|
2447
2474
|
"pages",
|
|
2448
2475
|
"sections"
|
|
2449
2476
|
],
|
|
@@ -54,6 +54,7 @@ import {
|
|
|
54
54
|
type FormUploadedMessageData,
|
|
55
55
|
type FormsBackupRequestedMessageData
|
|
56
56
|
} from '~/src/form/form-audit/types.js'
|
|
57
|
+
import { type Output } from '~/src/form/form-definition/types.js'
|
|
57
58
|
import { emailAddressNoUnicodeSchema } from '~/src/form/form-editor/index.js'
|
|
58
59
|
import {
|
|
59
60
|
contactSchema,
|
|
@@ -102,6 +103,37 @@ export const formDefinitionS3Meta = Joi.object<FormDefinitionS3Meta>()
|
|
|
102
103
|
})
|
|
103
104
|
.description('Schema for form data S3 object in message')
|
|
104
105
|
|
|
106
|
+
/**
|
|
107
|
+
* Stands apart from the definition's own output schema, which resolves a
|
|
108
|
+
* condition against the sibling `conditions` array - a lookup that has no
|
|
109
|
+
* meaning on an audit message carrying the output on its own.
|
|
110
|
+
*/
|
|
111
|
+
export const formOutputMessageData = Joi.object<Output>()
|
|
112
|
+
.keys({
|
|
113
|
+
emailAddress: emailAddressNoUnicodeSchema
|
|
114
|
+
.required()
|
|
115
|
+
.description('Email address the submission is sent to'),
|
|
116
|
+
audience: Joi.string()
|
|
117
|
+
.trim()
|
|
118
|
+
.valid('human', 'machine')
|
|
119
|
+
.required()
|
|
120
|
+
.description(
|
|
121
|
+
'Whether the submission is sent human readable or machine processable'
|
|
122
|
+
),
|
|
123
|
+
version: Joi.string()
|
|
124
|
+
.trim()
|
|
125
|
+
.required()
|
|
126
|
+
.description('Version identifier of the output format'),
|
|
127
|
+
condition: Joi.string()
|
|
128
|
+
.trim()
|
|
129
|
+
.allow('')
|
|
130
|
+
.optional()
|
|
131
|
+
.description(
|
|
132
|
+
'Id of the condition determining whether the submission is sent to this address'
|
|
133
|
+
)
|
|
134
|
+
})
|
|
135
|
+
.description('A submission email target as recorded on an audit event')
|
|
136
|
+
|
|
105
137
|
export const formUpdatedMessageData = formMessageDataBase
|
|
106
138
|
.append<FormUpdatedMessageData>({
|
|
107
139
|
requestType: Joi.string()
|
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import { ComponentType } from '~/src/components/enums.js'
|
|
2
2
|
import { type ComponentDef } from '~/src/components/types.js'
|
|
3
3
|
import { type ConditionListItemRefValueDataV2 } from '~/src/conditions/types.js'
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
type FormDefinition,
|
|
6
|
+
type Output
|
|
7
|
+
} from '~/src/form/form-definition/types.js'
|
|
5
8
|
import { type ControllerType } from '~/src/pages/enums.js'
|
|
6
9
|
import { hasComponents } from '~/src/pages/helpers.js'
|
|
7
10
|
|
|
@@ -81,6 +84,34 @@ export function getHiddenFields(definition: FormDefinition) {
|
|
|
81
84
|
return totalHiddenFields
|
|
82
85
|
}
|
|
83
86
|
|
|
87
|
+
/**
|
|
88
|
+
* Builds the key identifying a submission email target. Two outputs sharing a
|
|
89
|
+
* key would send the same format of the same submission to the same inbox in
|
|
90
|
+
* the same circumstances, so one of them is redundant. Addresses are compared
|
|
91
|
+
* case-insensitively and a missing condition is treated as an empty one.
|
|
92
|
+
* @param output - submission email target
|
|
93
|
+
*/
|
|
94
|
+
export function getOutputKey(output: Partial<Output>): string {
|
|
95
|
+
return [
|
|
96
|
+
output.emailAddress?.trim().toLowerCase() ?? '',
|
|
97
|
+
output.condition?.trim() ?? '',
|
|
98
|
+
output.audience ?? '',
|
|
99
|
+
output.version?.trim() ?? ''
|
|
100
|
+
].join('|')
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Whether two submission email targets are duplicates of one another
|
|
105
|
+
* @param output - submission email target
|
|
106
|
+
* @param otherOutput - submission email target to compare against
|
|
107
|
+
*/
|
|
108
|
+
export function isDuplicateOutput(
|
|
109
|
+
output: Partial<Output>,
|
|
110
|
+
otherOutput: Partial<Output>
|
|
111
|
+
): boolean {
|
|
112
|
+
return getOutputKey(output) === getOutputKey(otherOutput)
|
|
113
|
+
}
|
|
114
|
+
|
|
84
115
|
/**
|
|
85
116
|
* @param definition - form definition
|
|
86
117
|
* @returns {boolean}
|
|
@@ -40,6 +40,7 @@ import {
|
|
|
40
40
|
} from '~/src/form/form-definition/constants.js'
|
|
41
41
|
import {
|
|
42
42
|
isConditionListItemRefValueData,
|
|
43
|
+
isDuplicateOutput,
|
|
43
44
|
isFormDefinition
|
|
44
45
|
} from '~/src/form/form-definition/helpers.js'
|
|
45
46
|
import {
|
|
@@ -53,6 +54,7 @@ import {
|
|
|
53
54
|
type Item,
|
|
54
55
|
type Link,
|
|
55
56
|
type List,
|
|
57
|
+
type Output,
|
|
56
58
|
type Page,
|
|
57
59
|
type PhaseBanner,
|
|
58
60
|
type Repeat,
|
|
@@ -1197,6 +1199,66 @@ const outputSchema = Joi.object<FormDefinition['output']>()
|
|
|
1197
1199
|
.description('Version identifier for the output format')
|
|
1198
1200
|
})
|
|
1199
1201
|
|
|
1202
|
+
const outputsItemSchema = Joi.object<Output>()
|
|
1203
|
+
.description('Configuration for a single submission email target')
|
|
1204
|
+
.keys({
|
|
1205
|
+
emailAddress: emailAddressNoUnicodeSchema.description(
|
|
1206
|
+
'Email address where form submissions are sent'
|
|
1207
|
+
),
|
|
1208
|
+
audience: Joi.string()
|
|
1209
|
+
.trim()
|
|
1210
|
+
.valid('human', 'machine')
|
|
1211
|
+
.required()
|
|
1212
|
+
.description(
|
|
1213
|
+
'Target audience for the output (human readable or machine processable)'
|
|
1214
|
+
),
|
|
1215
|
+
version: Joi.string()
|
|
1216
|
+
.trim()
|
|
1217
|
+
.required()
|
|
1218
|
+
.description('Version identifier for the output format')
|
|
1219
|
+
})
|
|
1220
|
+
|
|
1221
|
+
const outputsItemSchemaV2 = outputsItemSchema.keys({
|
|
1222
|
+
condition: Joi.string()
|
|
1223
|
+
.trim()
|
|
1224
|
+
.allow('')
|
|
1225
|
+
.optional()
|
|
1226
|
+
.when('/conditions', {
|
|
1227
|
+
is: Joi.exist(),
|
|
1228
|
+
then: Joi.valid('', conditionIdRef)
|
|
1229
|
+
})
|
|
1230
|
+
.description(
|
|
1231
|
+
'Optional condition that determines if submissions are sent to this output'
|
|
1232
|
+
)
|
|
1233
|
+
.error(checkErrors(FormDefinitionError.RefOutputCondition))
|
|
1234
|
+
})
|
|
1235
|
+
|
|
1236
|
+
const outputsSchema = Joi.array<Output>()
|
|
1237
|
+
.items(outputsItemSchema)
|
|
1238
|
+
.unique(isDuplicateOutput)
|
|
1239
|
+
// Without this the raw Joi message names the array index, eg `"outputs[4]"
|
|
1240
|
+
// contains a duplicate value`, which means nothing to a form author
|
|
1241
|
+
.messages({
|
|
1242
|
+
'array.unique':
|
|
1243
|
+
'Email address {{#value.emailAddress}} is already receiving the same submissions'
|
|
1244
|
+
})
|
|
1245
|
+
.optional()
|
|
1246
|
+
.description('One or more email targets/types for submission emails')
|
|
1247
|
+
.error(checkErrors(FormDefinitionError.UniqueOutput))
|
|
1248
|
+
|
|
1249
|
+
const outputsSchemaV2 = Joi.array<Output>()
|
|
1250
|
+
.items(outputsItemSchemaV2)
|
|
1251
|
+
.unique(isDuplicateOutput)
|
|
1252
|
+
// Without this the raw Joi message names the array index, eg `"outputs[4]"
|
|
1253
|
+
// contains a duplicate value`, which means nothing to a form author
|
|
1254
|
+
.messages({
|
|
1255
|
+
'array.unique':
|
|
1256
|
+
'Email address {{#value.emailAddress}} is already receiving the same submissions'
|
|
1257
|
+
})
|
|
1258
|
+
.optional()
|
|
1259
|
+
.description('Outputs schema for V2 forms')
|
|
1260
|
+
.error(checkErrors(FormDefinitionError.UniqueOutput))
|
|
1261
|
+
|
|
1200
1262
|
/**
|
|
1201
1263
|
* Joi schema for `FormDefinition` interface
|
|
1202
1264
|
* @see {@link FormDefinition}
|
|
@@ -1271,31 +1333,12 @@ export const formDefinitionSchema = Joi.object<FormDefinition>()
|
|
|
1271
1333
|
.description('Phase banner configuration'),
|
|
1272
1334
|
options: optionsSchema.optional().description('Options for the form'),
|
|
1273
1335
|
outputEmail: emailAddressNoUnicodeSchema
|
|
1274
|
-
.email({ tlds: { allow: ['uk'] } })
|
|
1275
1336
|
.optional()
|
|
1276
1337
|
.description('Email address where form submissions are sent'),
|
|
1277
1338
|
output: outputSchema
|
|
1278
1339
|
.optional()
|
|
1279
1340
|
.description('Configuration for submission output format'),
|
|
1280
|
-
outputs:
|
|
1281
|
-
.items({
|
|
1282
|
-
emailAddress: emailAddressNoUnicodeSchema
|
|
1283
|
-
.email({ tlds: { allow: ['uk'] } })
|
|
1284
|
-
.description('Email address where form submissions are sent'),
|
|
1285
|
-
audience: Joi.string()
|
|
1286
|
-
.trim()
|
|
1287
|
-
.valid('human', 'machine')
|
|
1288
|
-
.required()
|
|
1289
|
-
.description(
|
|
1290
|
-
'Target audience for the output (human readable or machine processable)'
|
|
1291
|
-
),
|
|
1292
|
-
version: Joi.string()
|
|
1293
|
-
.trim()
|
|
1294
|
-
.required()
|
|
1295
|
-
.description('Version identifier for the output format')
|
|
1296
|
-
})
|
|
1297
|
-
.optional()
|
|
1298
|
-
.description('One or more email targets/types for submission emails')
|
|
1341
|
+
outputs: outputsSchema
|
|
1299
1342
|
})
|
|
1300
1343
|
|
|
1301
1344
|
export const formDefinitionV2Schema = formDefinitionSchema
|
|
@@ -1303,6 +1346,7 @@ export const formDefinitionV2Schema = formDefinitionSchema
|
|
|
1303
1346
|
schema: Joi.number()
|
|
1304
1347
|
.integer()
|
|
1305
1348
|
.valid(SchemaVersion.V2)
|
|
1349
|
+
.required()
|
|
1306
1350
|
.description('Form schema version to use (2)'),
|
|
1307
1351
|
pages: Joi.array<Page>()
|
|
1308
1352
|
.items(pageSchemaV2)
|
|
@@ -1353,7 +1397,8 @@ export const formDefinitionV2Schema = formDefinitionSchema
|
|
|
1353
1397
|
FormDefinitionError.UniqueSectionName,
|
|
1354
1398
|
FormDefinitionError.UniqueSectionTitle
|
|
1355
1399
|
])
|
|
1356
|
-
)
|
|
1400
|
+
),
|
|
1401
|
+
outputs: outputsSchemaV2
|
|
1357
1402
|
})
|
|
1358
1403
|
.description('Form definition schema for V2')
|
|
1359
1404
|
|
|
@@ -188,6 +188,11 @@ export interface Output {
|
|
|
188
188
|
audience: OutputAudience
|
|
189
189
|
version: string
|
|
190
190
|
emailAddress: string
|
|
191
|
+
/**
|
|
192
|
+
* Id of the condition determining whether submissions are sent to this
|
|
193
|
+
* output. V2 only - rejected by the V1 schema.
|
|
194
|
+
*/
|
|
195
|
+
condition?: string
|
|
191
196
|
}
|
|
192
197
|
|
|
193
198
|
export interface FormOptions {
|
|
@@ -30,10 +30,12 @@ export const checkErrors = (
|
|
|
30
30
|
const keyMatch =
|
|
31
31
|
typeof err.local.key === 'number' ? err.local.path : err.local.key
|
|
32
32
|
|
|
33
|
+
// An empty key matches any unique constraint for schemas whose
|
|
34
|
+
// uniqueness is a composite of several keys rather than a single one
|
|
33
35
|
if (
|
|
34
36
|
errorDetails.type === FormDefinitionErrorType.Unique &&
|
|
35
37
|
err.code === 'array.unique' &&
|
|
36
|
-
keyMatch === errorDetails.key
|
|
38
|
+
(errorDetails.key === '' || keyMatch === errorDetails.key)
|
|
37
39
|
) {
|
|
38
40
|
err.local.errorCode = formError
|
|
39
41
|
err.local.errorType = FormDefinitionErrorType.Unique
|
|
@@ -172,6 +174,8 @@ export const formErrorsToMessages: Record<FormDefinitionError, string> = {
|
|
|
172
174
|
'Each item in a list must have unique text. Change the item text to one that is not already used.',
|
|
173
175
|
[FormDefinitionError.UniqueListItemValue]:
|
|
174
176
|
'Each item in a list must have a unique value. Change the item value to one that is not already used.',
|
|
177
|
+
[FormDefinitionError.UniqueOutput]:
|
|
178
|
+
'This email address is already receiving the same submissions. Change the address, condition or format, or remove the duplicate.',
|
|
175
179
|
[FormDefinitionError.RefPageCondition]:
|
|
176
180
|
'This page is referenced by a condition. Remove the condition before making changes to this page.',
|
|
177
181
|
[FormDefinitionError.RefConditionComponentId]:
|
|
@@ -184,6 +188,8 @@ export const formErrorsToMessages: Record<FormDefinitionError, string> = {
|
|
|
184
188
|
'A condition is using another condition. Remove the reference before making changes.',
|
|
185
189
|
[FormDefinitionError.RefPageComponentList]:
|
|
186
190
|
'A question on this page is using a list. Remove the reference before making changes to the list.',
|
|
191
|
+
[FormDefinitionError.RefOutputCondition]:
|
|
192
|
+
'A submission email is using a condition that does not exist in this form. Select an existing condition or remove it.',
|
|
187
193
|
[FormDefinitionError.IncompatibleConditionComponentType]:
|
|
188
194
|
'You cannot change to this question type because this question is used in a condition. Remove the condition or select a different question type.',
|
|
189
195
|
[FormDefinitionError.IncompatibleQuestionRegex]:
|
|
@@ -45,12 +45,14 @@ export enum FormDefinitionError {
|
|
|
45
45
|
UniqueListItemId = 'unique_list_item_id',
|
|
46
46
|
UniqueListItemText = 'unique_list_item_text',
|
|
47
47
|
UniqueListItemValue = 'unique_list_item_value',
|
|
48
|
+
UniqueOutput = 'unique_output',
|
|
48
49
|
RefPageCondition = 'ref_page_condition',
|
|
49
50
|
RefConditionComponentId = 'ref_condition_component_id',
|
|
50
51
|
RefConditionListId = 'ref_condition_list_id',
|
|
51
52
|
RefConditionItemId = 'ref_condition_item_id',
|
|
52
53
|
RefConditionConditionId = 'ref_condition_condition_id',
|
|
53
54
|
RefPageComponentList = 'ref_page_component_list',
|
|
55
|
+
RefOutputCondition = 'ref_output_condition',
|
|
54
56
|
IncompatibleConditionComponentType = 'incompatible_condition_component_type',
|
|
55
57
|
IncompatibleQuestionRegex = 'incompatible_question_regex',
|
|
56
58
|
Other = 'other'
|
|
@@ -125,6 +127,13 @@ export const formDefinitionErrors: FormDefinitionErrors = {
|
|
|
125
127
|
key: 'value',
|
|
126
128
|
type: FormDefinitionErrorType.Unique
|
|
127
129
|
},
|
|
130
|
+
[FormDefinitionError.UniqueOutput]: {
|
|
131
|
+
// Outputs are compared on a composite of their address, condition,
|
|
132
|
+
// audience and version, so there is no single key to match on. An empty
|
|
133
|
+
// key matches any unique constraint on the schema it is attached to.
|
|
134
|
+
key: '',
|
|
135
|
+
type: FormDefinitionErrorType.Unique
|
|
136
|
+
},
|
|
128
137
|
[FormDefinitionError.RefPageCondition]: {
|
|
129
138
|
key: 'condition',
|
|
130
139
|
type: FormDefinitionErrorType.Ref
|
|
@@ -149,6 +158,10 @@ export const formDefinitionErrors: FormDefinitionErrors = {
|
|
|
149
158
|
key: 'list',
|
|
150
159
|
type: FormDefinitionErrorType.Ref
|
|
151
160
|
},
|
|
161
|
+
[FormDefinitionError.RefOutputCondition]: {
|
|
162
|
+
key: 'condition',
|
|
163
|
+
type: FormDefinitionErrorType.Ref
|
|
164
|
+
},
|
|
152
165
|
[FormDefinitionError.IncompatibleConditionComponentType]: {
|
|
153
166
|
key: 'componentId',
|
|
154
167
|
type: FormDefinitionErrorType.Incompatible
|
|
@@ -19,3 +19,29 @@ export enum SubmissionEventMessageSource {
|
|
|
19
19
|
export enum SubmissionEventMessageType {
|
|
20
20
|
RUNNER_SAVE_AND_EXIT = 'RUNNER_SAVE_AND_EXIT'
|
|
21
21
|
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* The outcome of evaluating a single form condition.
|
|
25
|
+
*
|
|
26
|
+
* `Error` is deliberately distinct from `False`. The runner defaults a failed
|
|
27
|
+
* evaluation to `false` when routing, but for an audit record the two are not
|
|
28
|
+
* the same thing and a consumer must be able to tell them apart.
|
|
29
|
+
*/
|
|
30
|
+
export enum ConditionEvaluationOutcome {
|
|
31
|
+
/**
|
|
32
|
+
* The condition evaluated to `true`
|
|
33
|
+
*/
|
|
34
|
+
True = 'true',
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* The condition evaluated to `false`
|
|
38
|
+
*/
|
|
39
|
+
False = 'false',
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Evaluation threw - most commonly because the condition references a
|
|
43
|
+
* component that is absent from the evaluation context, such as a component
|
|
44
|
+
* on a repeater page. The runner treats this as `false`.
|
|
45
|
+
*/
|
|
46
|
+
Error = 'error'
|
|
47
|
+
}
|
|
@@ -2,6 +2,7 @@ import Joi from 'joi'
|
|
|
2
2
|
|
|
3
3
|
import { FormStatus } from '~/src/common/enums.js'
|
|
4
4
|
import {
|
|
5
|
+
ConditionEvaluationOutcome,
|
|
5
6
|
SecurityQuestionsEnum,
|
|
6
7
|
SubmissionEventMessageCategory,
|
|
7
8
|
SubmissionEventMessageSchemaVersion,
|
|
@@ -11,6 +12,8 @@ import {
|
|
|
11
12
|
import {
|
|
12
13
|
type SaveAndExitMessage,
|
|
13
14
|
type SaveAndExitMessageData,
|
|
15
|
+
type SubmitConditionEvaluation,
|
|
16
|
+
type SubmitConditionReference,
|
|
14
17
|
type SubmitPayload,
|
|
15
18
|
type SubmitRecord,
|
|
16
19
|
type SubmitRecordset
|
|
@@ -61,6 +64,52 @@ export const formSubmitRecordsetSchema = Joi.object<SubmitRecordset>({
|
|
|
61
64
|
.label('FormSubmitRecordset')
|
|
62
65
|
.description('Collection of repeated field values from a repeatable section')
|
|
63
66
|
|
|
67
|
+
/**
|
|
68
|
+
* Joi schema for `SubmitConditionReference` interface
|
|
69
|
+
* @see {@link SubmitConditionReference}
|
|
70
|
+
*/
|
|
71
|
+
export const formSubmitConditionReferenceSchema =
|
|
72
|
+
Joi.object<SubmitConditionReference>({
|
|
73
|
+
componentId: Joi.string()
|
|
74
|
+
.required()
|
|
75
|
+
.description('Identifier of the component the condition depends on'),
|
|
76
|
+
componentName: Joi.string()
|
|
77
|
+
.required()
|
|
78
|
+
.description(
|
|
79
|
+
'Name of the component the condition depends on, matching the submitted record name'
|
|
80
|
+
),
|
|
81
|
+
answered: Joi.boolean()
|
|
82
|
+
.required()
|
|
83
|
+
.description(
|
|
84
|
+
'Whether the component held an answer when the condition was evaluated'
|
|
85
|
+
)
|
|
86
|
+
})
|
|
87
|
+
.label('FormSubmitConditionReference')
|
|
88
|
+
.description('A component a condition depends on, and its answered state')
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Joi schema for `SubmitConditionEvaluation` interface
|
|
92
|
+
* @see {@link SubmitConditionEvaluation}
|
|
93
|
+
*/
|
|
94
|
+
export const formSubmitConditionEvaluationSchema =
|
|
95
|
+
Joi.object<SubmitConditionEvaluation>({
|
|
96
|
+
conditionId: Joi.string()
|
|
97
|
+
.required()
|
|
98
|
+
.description('Identifier of the condition in the V2 form definition'),
|
|
99
|
+
outcome: Joi.string()
|
|
100
|
+
.valid(...Object.values(ConditionEvaluationOutcome))
|
|
101
|
+
.required()
|
|
102
|
+
.description('Result of evaluating the condition'),
|
|
103
|
+
references: Joi.array<SubmitConditionReference>()
|
|
104
|
+
.items(formSubmitConditionReferenceSchema)
|
|
105
|
+
.required()
|
|
106
|
+
.description(
|
|
107
|
+
'Components the condition depends on, including those reached through nested condition references'
|
|
108
|
+
)
|
|
109
|
+
})
|
|
110
|
+
.label('FormSubmitConditionEvaluation')
|
|
111
|
+
.description('Recorded outcome of a single condition at submission')
|
|
112
|
+
|
|
64
113
|
/**
|
|
65
114
|
* Joi schema for `SubmitPayload` interface
|
|
66
115
|
* @see {@link SubmitPayload}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { type FormStatus } from '~/src/common/enums.js'
|
|
2
2
|
import {
|
|
3
|
+
type ConditionEvaluationOutcome,
|
|
3
4
|
type SecurityQuestionsEnum,
|
|
4
5
|
type SubmissionEventMessageCategory,
|
|
5
6
|
type SubmissionEventMessageSchemaVersion,
|
|
@@ -7,6 +8,8 @@ import {
|
|
|
7
8
|
type SubmissionEventMessageType
|
|
8
9
|
} from '~/src/form/form-submission/enums.js'
|
|
9
10
|
import {
|
|
11
|
+
formSubmitConditionEvaluationSchema,
|
|
12
|
+
formSubmitConditionReferenceSchema,
|
|
10
13
|
formSubmitPayloadSchema,
|
|
11
14
|
formSubmitRecordSchema,
|
|
12
15
|
formSubmitRecordsetSchema
|
|
@@ -54,6 +57,57 @@ export interface SubmitRecordset {
|
|
|
54
57
|
value: SubmitRecord[][]
|
|
55
58
|
}
|
|
56
59
|
|
|
60
|
+
/**
|
|
61
|
+
* A component referenced by a condition, and whether the user had answered it
|
|
62
|
+
* when the condition was evaluated.
|
|
63
|
+
*
|
|
64
|
+
* An unanswered question is not absent from the runner's evaluation context -
|
|
65
|
+
* it is present as `null` - so conditions still return a boolean for questions
|
|
66
|
+
* that were skipped or never reached. Negative operators ("is not", "is shorter
|
|
67
|
+
* than") return `true` in that case. Without knowing which referenced questions
|
|
68
|
+
* were answered, a consumer cannot tell a real match from a vacuous one.
|
|
69
|
+
* @see {@link formSubmitConditionReferenceSchema}
|
|
70
|
+
*/
|
|
71
|
+
export interface SubmitConditionReference {
|
|
72
|
+
/**
|
|
73
|
+
* The id of the referenced component
|
|
74
|
+
*/
|
|
75
|
+
componentId: string
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* The name of the referenced component, as used in the submitted records
|
|
79
|
+
*/
|
|
80
|
+
componentName: string
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Whether the referenced component held an answer at the point of evaluation
|
|
84
|
+
*/
|
|
85
|
+
answered: boolean
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* The recorded outcome of a single condition at the point of submission.
|
|
90
|
+
* V2 forms only - V1 conditions are not captured.
|
|
91
|
+
* @see {@link formSubmitConditionEvaluationSchema}
|
|
92
|
+
*/
|
|
93
|
+
export interface SubmitConditionEvaluation {
|
|
94
|
+
/**
|
|
95
|
+
* The id of the condition, as authored in the V2 form definition
|
|
96
|
+
*/
|
|
97
|
+
conditionId: string
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* The result of evaluating the condition
|
|
101
|
+
*/
|
|
102
|
+
outcome: ConditionEvaluationOutcome
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Every component the condition depends on, including those reached through
|
|
106
|
+
* nested condition references, and whether each was answered
|
|
107
|
+
*/
|
|
108
|
+
references: SubmitConditionReference[]
|
|
109
|
+
}
|
|
110
|
+
|
|
57
111
|
/**
|
|
58
112
|
* Interface for the submission-api `/submit` payload
|
|
59
113
|
* @see {@link formSubmitPayloadSchema}
|