@defra/forms-engine-plugin 4.5.6 → 4.6.1

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.
Files changed (35) hide show
  1. package/.server/server/plugins/engine/beta/form-context.js +1 -5
  2. package/.server/server/plugins/engine/beta/form-context.js.map +1 -1
  3. package/.server/server/plugins/engine/components/EmailAddressField.d.ts +2 -4
  4. package/.server/server/plugins/engine/components/EmailAddressField.js +5 -1
  5. package/.server/server/plugins/engine/components/EmailAddressField.js.map +1 -1
  6. package/.server/server/plugins/engine/models/FormModel.d.ts +0 -2
  7. package/.server/server/plugins/engine/models/FormModel.js +1 -4
  8. package/.server/server/plugins/engine/models/FormModel.js.map +1 -1
  9. package/.server/server/plugins/engine/outputFormatters/adapter/v1.d.ts +0 -4
  10. package/.server/server/plugins/engine/outputFormatters/adapter/v1.js +1 -22
  11. package/.server/server/plugins/engine/outputFormatters/adapter/v1.js.map +1 -1
  12. package/.server/server/plugins/engine/pageControllers/helpers/state.js +1 -1
  13. package/.server/server/plugins/engine/pageControllers/helpers/state.js.map +1 -1
  14. package/.server/server/plugins/engine/pageControllers/validationOptions.js +2 -0
  15. package/.server/server/plugins/engine/pageControllers/validationOptions.js.map +1 -1
  16. package/.server/server/plugins/engine/types.d.ts +0 -1
  17. package/.server/server/plugins/engine/types.js.map +1 -1
  18. package/.server/server/utils/utils.d.ts +6 -0
  19. package/.server/server/utils/utils.js +13 -0
  20. package/.server/server/utils/utils.js.map +1 -1
  21. package/.server/server/utils/utils.test.js +37 -1
  22. package/.server/server/utils/utils.test.js.map +1 -1
  23. package/package.json +2 -2
  24. package/src/server/plugins/engine/beta/form-context.test.ts +0 -2
  25. package/src/server/plugins/engine/beta/form-context.ts +1 -8
  26. package/src/server/plugins/engine/components/EmailAddressField.ts +16 -4
  27. package/src/server/plugins/engine/models/FormModel.test.ts +0 -64
  28. package/src/server/plugins/engine/models/FormModel.ts +1 -5
  29. package/src/server/plugins/engine/outputFormatters/adapter/v1.test.ts +4 -356
  30. package/src/server/plugins/engine/outputFormatters/adapter/v1.ts +1 -31
  31. package/src/server/plugins/engine/pageControllers/helpers/state.ts +1 -1
  32. package/src/server/plugins/engine/pageControllers/validationOptions.ts +2 -0
  33. package/src/server/plugins/engine/types.ts +0 -1
  34. package/src/server/utils/utils.js +11 -0
  35. package/src/server/utils/utils.test.js +16 -1
@@ -11,10 +11,7 @@ import {
11
11
  type DetailItemField,
12
12
  type DetailItemRepeat
13
13
  } from '~/src/server/plugins/engine/models/types.js'
14
- import {
15
- format,
16
- getVersionMetadata
17
- } from '~/src/server/plugins/engine/outputFormatters/adapter/v1.js'
14
+ import { format } from '~/src/server/plugins/engine/outputFormatters/adapter/v1.js'
18
15
  import { buildFormContextRequest } from '~/src/server/plugins/engine/pageControllers/__stubs__/request.js'
19
16
  import { FormAdapterSubmissionSchemaVersion } from '~/src/server/plugins/engine/types/index.js'
20
17
  import {
@@ -764,7 +761,7 @@ describe('Adapter v1 formatter', () => {
764
761
  })
765
762
 
766
763
  describe('version metadata handling', () => {
767
- it('should prefer $$__formVersion from definition metadata over formMetadata.versions', () => {
764
+ it('should include versionMetadata from $$__formVersion in definition metadata', () => {
768
765
  const definitionWithFormVersion = {
769
766
  ...definition,
770
767
  metadata: {
@@ -818,92 +815,7 @@ describe('Adapter v1 formatter', () => {
818
815
  })
819
816
  })
820
817
 
821
- it('should include versionMetadata when context has submittedVersionNumber and formMetadata has versions', () => {
822
- const formMetadata: Partial<FormMetadata> = {
823
- id: 'form-123',
824
- slug: 'test-form',
825
- title: 'Test Form',
826
- notificationEmail: 'test@example.com',
827
- versions: [
828
- {
829
- versionNumber: 1,
830
- createdAt: new Date('2024-01-01T00:00:00.000Z')
831
- },
832
- {
833
- versionNumber: 2,
834
- createdAt: new Date('2024-01-15T00:00:00.000Z')
835
- }
836
- ]
837
- }
838
-
839
- const modelWithVersion = new FormModel(definition, {
840
- basePath: 'test',
841
- versionNumber: 2
842
- })
843
-
844
- const contextWithVersion = modelWithVersion.getFormContext(request, state)
845
-
846
- const formStatus = {
847
- isPreview: false,
848
- state: FormStatus.Live
849
- }
850
-
851
- const body = format(
852
- contextWithVersion,
853
- items,
854
- modelWithVersion,
855
- submitResponse,
856
- formStatus,
857
- formMetadata as FormMetadata
858
- )
859
- const parsedBody = JSON.parse(body) as FormAdapterSubmissionMessagePayload
860
-
861
- expect(parsedBody.meta.versionMetadata).toEqual({
862
- versionNumber: 2,
863
- createdAt: '2024-01-15T00:00:00.000Z'
864
- })
865
- })
866
-
867
- it('should use first version as fallback when submittedVersionNumber is undefined', () => {
868
- const formMetadata: Partial<FormMetadata> = {
869
- id: 'form-123',
870
- slug: 'test-form',
871
- title: 'Test Form',
872
- notificationEmail: 'test@example.com',
873
- versions: [
874
- {
875
- versionNumber: 1,
876
- createdAt: new Date('2024-01-01T00:00:00.000Z')
877
- },
878
- {
879
- versionNumber: 2,
880
- createdAt: new Date('2024-01-15T00:00:00.000Z')
881
- }
882
- ]
883
- }
884
-
885
- const formStatus = {
886
- isPreview: false,
887
- state: FormStatus.Live
888
- }
889
-
890
- const body = format(
891
- context,
892
- items,
893
- model,
894
- submitResponse,
895
- formStatus,
896
- formMetadata as FormMetadata
897
- )
898
- const parsedBody = JSON.parse(body) as FormAdapterSubmissionMessagePayload
899
-
900
- expect(parsedBody.meta.versionMetadata).toEqual({
901
- versionNumber: 1,
902
- createdAt: '2024-01-01T00:00:00.000Z'
903
- })
904
- })
905
-
906
- it('should not include versionMetadata when submittedVersionNumber is undefined and no versions exist', () => {
818
+ it('should not include versionMetadata when no versions exist', () => {
907
819
  const formMetadata: Partial<FormMetadata> = {
908
820
  id: 'form-123',
909
821
  slug: 'test-form',
@@ -929,7 +841,7 @@ describe('Adapter v1 formatter', () => {
929
841
  expect(parsedBody.meta.versionMetadata).toBeUndefined()
930
842
  })
931
843
 
932
- it('should not include versionMetadata when submittedVersionNumber is undefined and versions array is empty', () => {
844
+ it('should not include versionMetadata when versions array is empty', () => {
933
845
  const formMetadata: Partial<FormMetadata> = {
934
846
  id: 'form-123',
935
847
  slug: 'test-form',
@@ -955,269 +867,5 @@ describe('Adapter v1 formatter', () => {
955
867
 
956
868
  expect(parsedBody.meta.versionMetadata).toBeUndefined()
957
869
  })
958
-
959
- it('should not include versionMetadata when submittedVersionNumber does not match any version', () => {
960
- const formMetadata: Partial<FormMetadata> = {
961
- id: 'form-123',
962
- slug: 'test-form',
963
- title: 'Test Form',
964
- notificationEmail: 'test@example.com',
965
- versions: [
966
- {
967
- versionNumber: 1,
968
- createdAt: new Date('2024-01-01T00:00:00.000Z')
969
- },
970
- {
971
- versionNumber: 2,
972
- createdAt: new Date('2024-01-15T00:00:00.000Z')
973
- }
974
- ]
975
- }
976
-
977
- const modelWithVersion = new FormModel(definition, {
978
- basePath: 'test',
979
- versionNumber: 99 // Non-existent version
980
- })
981
-
982
- const contextWithVersion = modelWithVersion.getFormContext(request, state)
983
-
984
- const formStatus = {
985
- isPreview: false,
986
- state: FormStatus.Live
987
- }
988
-
989
- const body = format(
990
- contextWithVersion,
991
- items,
992
- modelWithVersion,
993
- submitResponse,
994
- formStatus,
995
- formMetadata as FormMetadata
996
- )
997
- const parsedBody = JSON.parse(body) as FormAdapterSubmissionMessagePayload
998
-
999
- // Should fall back to first version since submittedVersionNumber doesn't match
1000
- expect(parsedBody.meta.versionMetadata).toEqual({
1001
- versionNumber: 1,
1002
- createdAt: '2024-01-01T00:00:00.000Z'
1003
- })
1004
- })
1005
-
1006
- it('should use first version as fallback when submittedVersionNumber does not match any version', () => {
1007
- const formMetadata: Partial<FormMetadata> = {
1008
- id: 'form-123',
1009
- slug: 'test-form',
1010
- title: 'Test Form',
1011
- notificationEmail: 'test@example.com',
1012
- versions: [
1013
- {
1014
- versionNumber: 1,
1015
- createdAt: new Date('2024-01-01T00:00:00.000Z')
1016
- },
1017
- {
1018
- versionNumber: 2,
1019
- createdAt: new Date('2024-01-15T00:00:00.000Z')
1020
- }
1021
- ]
1022
- }
1023
-
1024
- const modelWithVersion = new FormModel(definition, {
1025
- basePath: 'test',
1026
- versionNumber: 99 // Non-existent version
1027
- })
1028
-
1029
- const contextWithVersion = modelWithVersion.getFormContext(request, state)
1030
-
1031
- const formStatus = {
1032
- isPreview: false,
1033
- state: FormStatus.Live
1034
- }
1035
-
1036
- const body = format(
1037
- contextWithVersion,
1038
- items,
1039
- modelWithVersion,
1040
- submitResponse,
1041
- formStatus,
1042
- formMetadata as FormMetadata
1043
- )
1044
- const parsedBody = JSON.parse(body) as FormAdapterSubmissionMessagePayload
1045
-
1046
- // Should fall back to first version since submittedVersionNumber doesn't match
1047
- expect(parsedBody.meta.versionMetadata).toEqual({
1048
- versionNumber: 1,
1049
- createdAt: '2024-01-01T00:00:00.000Z'
1050
- })
1051
- })
1052
-
1053
- it('should handle single version in versions array', () => {
1054
- const formMetadata: Partial<FormMetadata> = {
1055
- id: 'form-123',
1056
- slug: 'test-form',
1057
- title: 'Test Form',
1058
- notificationEmail: 'test@example.com',
1059
- versions: [
1060
- {
1061
- versionNumber: 5,
1062
- createdAt: new Date('2024-02-01T00:00:00.000Z')
1063
- }
1064
- ]
1065
- }
1066
-
1067
- const formStatus = {
1068
- isPreview: false,
1069
- state: FormStatus.Live
1070
- }
1071
-
1072
- const body = format(
1073
- context,
1074
- items,
1075
- model,
1076
- submitResponse,
1077
- formStatus,
1078
- formMetadata as FormMetadata
1079
- )
1080
- const parsedBody = JSON.parse(body) as FormAdapterSubmissionMessagePayload
1081
-
1082
- expect(parsedBody.meta.versionMetadata).toEqual({
1083
- versionNumber: 5,
1084
- createdAt: '2024-02-01T00:00:00.000Z'
1085
- })
1086
- })
1087
- })
1088
-
1089
- describe('getVersionMetadata', () => {
1090
- const mockFormMetadata: Partial<FormMetadata> = {
1091
- id: 'form-123',
1092
- slug: 'test-form',
1093
- title: 'Test Form',
1094
- notificationEmail: 'test@example.com',
1095
- versions: [
1096
- {
1097
- versionNumber: 1,
1098
- createdAt: new Date('2024-01-01T00:00:00.000Z')
1099
- },
1100
- {
1101
- versionNumber: 2,
1102
- createdAt: new Date('2024-01-02T00:00:00.000Z')
1103
- },
1104
- {
1105
- versionNumber: 3,
1106
- createdAt: new Date('2024-01-03T00:00:00.000Z')
1107
- }
1108
- ]
1109
- }
1110
-
1111
- it('should return undefined when no form metadata provided', () => {
1112
- const result = getVersionMetadata(1, undefined)
1113
- expect(result).toBeUndefined()
1114
- })
1115
-
1116
- it('should return undefined when form metadata has no versions', () => {
1117
- const formMetadataWithoutVersions: Partial<FormMetadata> = {
1118
- ...mockFormMetadata,
1119
- versions: undefined
1120
- }
1121
-
1122
- const result = getVersionMetadata(
1123
- 1,
1124
- formMetadataWithoutVersions as FormMetadata
1125
- )
1126
- expect(result).toBeUndefined()
1127
- })
1128
-
1129
- it('should return undefined when versions array is empty', () => {
1130
- const formMetadataWithEmptyVersions: Partial<FormMetadata> = {
1131
- ...mockFormMetadata,
1132
- versions: []
1133
- }
1134
-
1135
- const result = getVersionMetadata(
1136
- 1,
1137
- formMetadataWithEmptyVersions as FormMetadata
1138
- )
1139
- expect(result).toBeUndefined()
1140
- })
1141
-
1142
- it('should return specific version when submittedVersionNumber matches', () => {
1143
- const result = getVersionMetadata(2, mockFormMetadata as FormMetadata)
1144
- expect(result).toEqual({
1145
- versionNumber: 2,
1146
- createdAt: new Date('2024-01-02T00:00:00.000Z')
1147
- })
1148
- })
1149
-
1150
- it('should return first version when submittedVersionNumber not found', () => {
1151
- const result = getVersionMetadata(999, mockFormMetadata as FormMetadata)
1152
- expect(result).toEqual({
1153
- versionNumber: 1,
1154
- createdAt: new Date('2024-01-01T00:00:00.000Z')
1155
- })
1156
- })
1157
-
1158
- it('should return first version when no submittedVersionNumber provided', () => {
1159
- const result = getVersionMetadata(
1160
- undefined,
1161
- mockFormMetadata as FormMetadata
1162
- )
1163
- expect(result).toEqual({
1164
- versionNumber: 1,
1165
- createdAt: new Date('2024-01-01T00:00:00.000Z')
1166
- })
1167
- })
1168
-
1169
- it('should handle single version in versions array', () => {
1170
- const singleVersionMetadata: Partial<FormMetadata> = {
1171
- ...mockFormMetadata,
1172
- versions: [
1173
- {
1174
- versionNumber: 5,
1175
- createdAt: new Date('2024-02-01T00:00:00.000Z')
1176
- }
1177
- ]
1178
- }
1179
-
1180
- const result = getVersionMetadata(
1181
- undefined,
1182
- singleVersionMetadata as FormMetadata
1183
- )
1184
- expect(result).toEqual({
1185
- versionNumber: 5,
1186
- createdAt: new Date('2024-02-01T00:00:00.000Z')
1187
- })
1188
- })
1189
-
1190
- it('should return correct version when submittedVersionNumber is 0', () => {
1191
- const metadataWithVersionZero: Partial<FormMetadata> = {
1192
- ...mockFormMetadata,
1193
- versions: [
1194
- {
1195
- versionNumber: 0,
1196
- createdAt: new Date('2024-01-01T00:00:00.000Z')
1197
- },
1198
- {
1199
- versionNumber: 1,
1200
- createdAt: new Date('2024-01-02T00:00:00.000Z')
1201
- }
1202
- ]
1203
- }
1204
-
1205
- const result = getVersionMetadata(
1206
- 0,
1207
- metadataWithVersionZero as FormMetadata
1208
- )
1209
- expect(result).toEqual({
1210
- versionNumber: 0,
1211
- createdAt: new Date('2024-01-01T00:00:00.000Z')
1212
- })
1213
- })
1214
-
1215
- it('should handle negative submittedVersionNumber by falling back to first version', () => {
1216
- const result = getVersionMetadata(-1, mockFormMetadata as FormMetadata)
1217
- expect(result).toEqual({
1218
- versionNumber: 1,
1219
- createdAt: new Date('2024-01-01T00:00:00.000Z')
1220
- })
1221
- })
1222
870
  })
1223
871
  })
@@ -31,9 +31,7 @@ export function format(
31
31
 
32
32
  const { main: v2Main, ...v2Data } = categoriseData(items)
33
33
 
34
- const versionMetadata =
35
- getFormVersion(model.def) ??
36
- getVersionMetadata(context.submittedVersionNumber, formMetadata)
34
+ const versionMetadata = getFormVersion(model.def)
37
35
 
38
36
  const meta: FormAdapterSubmissionMessageMeta = {
39
37
  schemaVersion: FormAdapterSubmissionSchemaVersion.V1,
@@ -79,34 +77,6 @@ export function format(
79
77
  return JSON.stringify(payload)
80
78
  }
81
79
 
82
- export function getVersionMetadata(
83
- submittedVersionNumber: number | undefined,
84
- formMetadata?: FormMetadata
85
- ): { versionNumber: number; createdAt: Date } | undefined {
86
- if (!formMetadata?.versions?.length) {
87
- return undefined
88
- }
89
-
90
- if (submittedVersionNumber !== undefined) {
91
- const submittedVersion = formMetadata.versions.find(
92
- (v) => v.versionNumber === submittedVersionNumber
93
- )
94
- if (submittedVersion) {
95
- return {
96
- versionNumber: submittedVersion.versionNumber,
97
- createdAt: submittedVersion.createdAt
98
- }
99
- }
100
- }
101
-
102
- // fallback to first available version
103
- const firstVersion = formMetadata.versions[0]
104
- return {
105
- versionNumber: firstVersion.versionNumber,
106
- createdAt: firstVersion.createdAt
107
- }
108
- }
109
-
110
80
  function extractCsvFiles(
111
81
  submitResponse: SubmitResponsePayload
112
82
  ): FormAdapterSubmissionMessageResult['files'] {
@@ -1,5 +1,4 @@
1
1
  import { ControllerType, getHiddenFields } from '@defra/forms-model'
2
- import { validate as isValidUUID } from 'uuid'
3
2
 
4
3
  import { getCacheService } from '~/src/server/plugins/engine/helpers.js'
5
4
  import {
@@ -16,6 +15,7 @@ import {
16
15
  } from '~/src/server/plugins/engine/types.js'
17
16
  import { type FormQuery } from '~/src/server/routes/types.js'
18
17
  import { type Services } from '~/src/server/types.js'
18
+ import { isValidUUID } from '~/src/server/utils/utils.js'
19
19
 
20
20
  const GUID_LENGTH = 36
21
21
 
@@ -44,6 +44,7 @@ export const messageTemplate: Record<string, JoiExpression> = {
44
44
  'Enter {{lowerFirst(#label)}} in the correct format',
45
45
  opts
46
46
  ) as JoiExpression,
47
+ unicode: '{{#label}} includes invalid characters, for example, long dashes',
47
48
  number: '{{#label}} must be a number',
48
49
  numberPrecision: '{{#label}} must have {{#limit}} or fewer decimal places',
49
50
  numberInteger: '{{#label}} must be a whole number',
@@ -69,6 +70,7 @@ export const messages: LanguageMessagesExt = {
69
70
  'string.empty': messageTemplate.required,
70
71
  'string.max': messageTemplate.max,
71
72
  'string.email': messageTemplate.format,
73
+ 'string.unicode': messageTemplate.unicode,
72
74
  'string.pattern.base': messageTemplate.pattern,
73
75
  'string.maxWords': messageTemplate.maxWords,
74
76
 
@@ -194,7 +194,6 @@ export interface FormContext {
194
194
  pageMap: Map<string, PageControllerClass>
195
195
  componentMap: Map<string, Component>
196
196
  referenceNumber: string
197
- submittedVersionNumber?: number
198
197
  }
199
198
 
200
199
  export type FormContextRequest = (
@@ -1,4 +1,5 @@
1
1
  import { getTraceId } from '@defra/hapi-tracing'
2
+ import Joi from 'joi'
2
3
 
3
4
  import { config } from '~/src/config/index.js'
4
5
 
@@ -22,3 +23,13 @@ export function applyTraceHeaders(
22
23
 
23
24
  return existingHeaders ? Object.assign(existingHeaders, headers) : headers
24
25
  }
26
+
27
+ /**
28
+ * Validates if a string conforms to the uuid structure
29
+ * @param {string} str
30
+ * @returns
31
+ */
32
+ export function isValidUUID(str) {
33
+ const { error } = Joi.string().uuid().validate(str)
34
+ return error === undefined
35
+ }
@@ -1,7 +1,7 @@
1
1
  import { getTraceId } from '@defra/hapi-tracing'
2
2
 
3
3
  import { config } from '~/src/config/index.js'
4
- import { applyTraceHeaders } from '~/src/server/utils/utils.js'
4
+ import { applyTraceHeaders, isValidUUID } from '~/src/server/utils/utils.js'
5
5
 
6
6
  jest.mock('@defra/hapi-tracing')
7
7
 
@@ -51,4 +51,19 @@ describe('Header helper functions', () => {
51
51
 
52
52
  expect(result).toBe(existingHeaders)
53
53
  })
54
+
55
+ it.each([
56
+ { uuid: '1f457a37-7b99-452e-8324-df9e041abff2', valid: true },
57
+ { uuid: '0c9a2690-9a0c-4a2c-98d7-e9ef95615ac9', valid: true },
58
+ { uuid: 'f223de3b-5ae5-44b2-8cee-ea8439adc335', valid: true },
59
+ { uuid: '82ecc90c-bc47-4ec5-80af-1a9fc1c4c08c', valid: true },
60
+ { uuid: 'd99ff582-ecce-474f-a44b-bc5961d977c5', valid: true },
61
+ { uuid: '7afffc8a-81ab-4aa6-a8f5-ecf6a600a781', valid: true },
62
+ { uuid: '7afffc8a81ab4aa6a8f5ecf6a600a781', valid: true },
63
+ { uuid: '', valid: false },
64
+ { uuid: 'uuid', valid: false },
65
+ { uuid: 'h4f84ef8-b5e1-4544-94aa-1b671d50d8cb', valid: false }
66
+ ])('should validate uuid appropriately %s', ({ uuid, valid }) => {
67
+ expect(isValidUUID(uuid)).toBe(valid)
68
+ })
54
69
  })