@defra/forms-engine-plugin 2.1.10 → 3.0.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 (136) hide show
  1. package/.server/server/index.js +2 -1
  2. package/.server/server/index.js.map +1 -1
  3. package/.server/server/plugins/engine/README.md +2 -2
  4. package/.server/server/plugins/engine/configureEnginePlugin.d.ts +2 -1
  5. package/.server/server/plugins/engine/configureEnginePlugin.js +4 -4
  6. package/.server/server/plugins/engine/configureEnginePlugin.js.map +1 -1
  7. package/.server/server/plugins/engine/helpers.d.ts +7 -11
  8. package/.server/server/plugins/engine/helpers.js +2 -2
  9. package/.server/server/plugins/engine/helpers.js.map +1 -1
  10. package/.server/server/plugins/engine/models/FormModel.d.ts +2 -0
  11. package/.server/server/plugins/engine/models/FormModel.js +5 -2
  12. package/.server/server/plugins/engine/models/FormModel.js.map +1 -1
  13. package/.server/server/plugins/engine/models/SummaryViewModel.d.ts +1 -1
  14. package/.server/server/plugins/engine/models/SummaryViewModel.js +1 -1
  15. package/.server/server/plugins/engine/models/SummaryViewModel.js.map +1 -1
  16. package/.server/server/plugins/engine/options.js +3 -6
  17. package/.server/server/plugins/engine/options.js.map +1 -1
  18. package/.server/server/plugins/engine/options.test.js +2 -8
  19. package/.server/server/plugins/engine/options.test.js.map +1 -1
  20. package/.server/server/plugins/engine/outputFormatters/adapter/v1.d.ts +4 -0
  21. package/.server/server/plugins/engine/outputFormatters/adapter/v1.js +25 -0
  22. package/.server/server/plugins/engine/outputFormatters/adapter/v1.js.map +1 -1
  23. package/.server/server/plugins/engine/outputFormatters/machine/v2.js +7 -6
  24. package/.server/server/plugins/engine/outputFormatters/machine/v2.js.map +1 -1
  25. package/.server/server/plugins/engine/pageControllers/FileUploadPageController.d.ts +5 -6
  26. package/.server/server/plugins/engine/pageControllers/FileUploadPageController.js.map +1 -1
  27. package/.server/server/plugins/engine/pageControllers/PageController.d.ts +6 -6
  28. package/.server/server/plugins/engine/pageControllers/PageController.js +4 -4
  29. package/.server/server/plugins/engine/pageControllers/PageController.js.map +1 -1
  30. package/.server/server/plugins/engine/pageControllers/QuestionPageController.d.ts +13 -13
  31. package/.server/server/plugins/engine/pageControllers/QuestionPageController.js +12 -20
  32. package/.server/server/plugins/engine/pageControllers/QuestionPageController.js.map +1 -1
  33. package/.server/server/plugins/engine/pageControllers/RepeatPageController.d.ts +7 -8
  34. package/.server/server/plugins/engine/pageControllers/RepeatPageController.js.map +1 -1
  35. package/.server/server/plugins/engine/pageControllers/StartPageController.d.ts +2 -2
  36. package/.server/server/plugins/engine/pageControllers/StartPageController.js +1 -1
  37. package/.server/server/plugins/engine/pageControllers/StartPageController.js.map +1 -1
  38. package/.server/server/plugins/engine/pageControllers/StatusPageController.d.ts +3 -4
  39. package/.server/server/plugins/engine/pageControllers/StatusPageController.js +1 -1
  40. package/.server/server/plugins/engine/pageControllers/StatusPageController.js.map +1 -1
  41. package/.server/server/plugins/engine/pageControllers/SummaryPageController.d.ts +5 -4
  42. package/.server/server/plugins/engine/pageControllers/SummaryPageController.js +12 -1
  43. package/.server/server/plugins/engine/pageControllers/SummaryPageController.js.map +1 -1
  44. package/.server/server/plugins/engine/pageControllers/TerminalPageController.d.ts +4 -4
  45. package/.server/server/plugins/engine/pageControllers/TerminalPageController.js +1 -1
  46. package/.server/server/plugins/engine/pageControllers/TerminalPageController.js.map +1 -1
  47. package/.server/server/plugins/engine/pageControllers/__stubs__/server.d.ts +1 -1
  48. package/.server/server/plugins/engine/pageControllers/__stubs__/server.js +2 -6
  49. package/.server/server/plugins/engine/pageControllers/__stubs__/server.js.map +1 -1
  50. package/.server/server/plugins/engine/plugin.js +7 -12
  51. package/.server/server/plugins/engine/plugin.js.map +1 -1
  52. package/.server/server/plugins/engine/routes/index.d.ts +5 -5
  53. package/.server/server/plugins/engine/routes/index.js +3 -1
  54. package/.server/server/plugins/engine/routes/index.js.map +1 -1
  55. package/.server/server/plugins/engine/routes/questions.d.ts +4 -4
  56. package/.server/server/plugins/engine/routes/questions.js.map +1 -1
  57. package/.server/server/plugins/engine/routes/repeaters/item-delete.js.map +1 -1
  58. package/.server/server/plugins/engine/routes/repeaters/summary.js.map +1 -1
  59. package/.server/server/plugins/engine/types/index.d.ts +2 -2
  60. package/.server/server/plugins/engine/types/index.js.map +1 -1
  61. package/.server/server/plugins/engine/types/schema.js +3 -2
  62. package/.server/server/plugins/engine/types/schema.js.map +1 -1
  63. package/.server/server/plugins/engine/types.d.ts +13 -12
  64. package/.server/server/plugins/engine/types.js.map +1 -1
  65. package/.server/server/plugins/engine/views/partials/form.html +3 -3
  66. package/.server/server/plugins/engine/views/summary.html +21 -5
  67. package/.server/server/plugins/nunjucks/context.d.ts +5 -6
  68. package/.server/server/plugins/nunjucks/context.js +3 -3
  69. package/.server/server/plugins/nunjucks/context.js.map +1 -1
  70. package/.server/server/routes/types.d.ts +3 -2
  71. package/.server/server/routes/types.js +1 -1
  72. package/.server/server/routes/types.js.map +1 -1
  73. package/.server/server/schemas/index.js +1 -1
  74. package/.server/server/schemas/index.js.map +1 -1
  75. package/.server/server/services/cacheService.d.ts +11 -19
  76. package/.server/server/services/cacheService.js +9 -30
  77. package/.server/server/services/cacheService.js.map +1 -1
  78. package/.server/server/types.d.ts +4 -1
  79. package/.server/server/types.js.map +1 -1
  80. package/.server/typings/hapi/index.d.js.map +1 -1
  81. package/package.json +4 -2
  82. package/src/server/index.test.ts +0 -39
  83. package/src/server/index.ts +4 -1
  84. package/src/server/plugins/engine/README.md +2 -2
  85. package/src/server/plugins/engine/components/helpers/helpers.test.ts +1 -1
  86. package/src/server/plugins/engine/configureEnginePlugin.ts +15 -11
  87. package/src/server/plugins/engine/helpers.test.ts +3 -2
  88. package/src/server/plugins/engine/helpers.ts +6 -6
  89. package/src/server/plugins/engine/models/FormModel.test.ts +66 -2
  90. package/src/server/plugins/engine/models/FormModel.ts +6 -4
  91. package/src/server/plugins/engine/models/SummaryViewModel.test.ts +7 -7
  92. package/src/server/plugins/engine/models/SummaryViewModel.ts +1 -1
  93. package/src/server/plugins/engine/options.js +6 -6
  94. package/src/server/plugins/engine/options.test.js +2 -6
  95. package/src/server/plugins/engine/outputFormatters/adapter/v1.test.ts +446 -13
  96. package/src/server/plugins/engine/outputFormatters/adapter/v1.ts +37 -0
  97. package/src/server/plugins/engine/outputFormatters/machine/v2.ts +8 -6
  98. package/src/server/plugins/engine/pageControllers/FileUploadPageController.test.ts +8 -10
  99. package/src/server/plugins/engine/pageControllers/FileUploadPageController.ts +9 -8
  100. package/src/server/plugins/engine/pageControllers/PageController.test.ts +11 -8
  101. package/src/server/plugins/engine/pageControllers/PageController.ts +9 -15
  102. package/src/server/plugins/engine/pageControllers/QuestionPageController.test.ts +35 -102
  103. package/src/server/plugins/engine/pageControllers/QuestionPageController.ts +24 -36
  104. package/src/server/plugins/engine/pageControllers/RepeatPageController.test.ts +4 -6
  105. package/src/server/plugins/engine/pageControllers/RepeatPageController.ts +8 -11
  106. package/src/server/plugins/engine/pageControllers/StartPageController.test.ts +4 -4
  107. package/src/server/plugins/engine/pageControllers/StartPageController.ts +1 -1
  108. package/src/server/plugins/engine/pageControllers/StatusPageController.test.ts +4 -4
  109. package/src/server/plugins/engine/pageControllers/StatusPageController.ts +6 -4
  110. package/src/server/plugins/engine/pageControllers/SummaryPageController.ts +15 -5
  111. package/src/server/plugins/engine/pageControllers/TerminalController.test.ts +4 -4
  112. package/src/server/plugins/engine/pageControllers/TerminalPageController.ts +7 -4
  113. package/src/server/plugins/engine/pageControllers/__stubs__/server.ts +5 -6
  114. package/src/server/plugins/engine/plugin.ts +7 -13
  115. package/src/server/plugins/engine/routes/index.ts +9 -12
  116. package/src/server/plugins/engine/routes/questions.test.ts +29 -53
  117. package/src/server/plugins/engine/routes/questions.ts +6 -8
  118. package/src/server/plugins/engine/routes/repeaters/item-delete.ts +5 -14
  119. package/src/server/plugins/engine/routes/repeaters/summary.ts +5 -14
  120. package/src/server/plugins/engine/types/index.ts +4 -1
  121. package/src/server/plugins/engine/types/schema.test.ts +40 -0
  122. package/src/server/plugins/engine/types/schema.ts +3 -1
  123. package/src/server/plugins/engine/types.ts +22 -13
  124. package/src/server/plugins/engine/views/partials/form.html +3 -3
  125. package/src/server/plugins/engine/views/summary.html +21 -5
  126. package/src/server/plugins/nunjucks/context.js +3 -3
  127. package/src/server/routes/types.ts +7 -2
  128. package/src/server/schemas/index.ts +1 -1
  129. package/src/server/services/cacheService.test.ts +1 -117
  130. package/src/server/services/cacheService.ts +22 -73
  131. package/src/server/types.ts +4 -1
  132. package/src/typings/hapi/index.d.ts +6 -7
  133. package/.server/server/plugins/engine/routes/exit.d.ts +0 -46
  134. package/.server/server/plugins/engine/routes/exit.js +0 -36
  135. package/.server/server/plugins/engine/routes/exit.js.map +0 -1
  136. package/src/server/plugins/engine/routes/exit.ts +0 -47
@@ -11,7 +11,10 @@ import {
11
11
  type DetailItemField,
12
12
  type DetailItemRepeat
13
13
  } from '~/src/server/plugins/engine/models/types.js'
14
- import { format } from '~/src/server/plugins/engine/outputFormatters/adapter/v1.js'
14
+ import {
15
+ format,
16
+ getVersionMetadata
17
+ } from '~/src/server/plugins/engine/outputFormatters/adapter/v1.js'
15
18
  import { buildFormContextRequest } from '~/src/server/plugins/engine/pageControllers/__stubs__/request.js'
16
19
  import { FormAdapterSubmissionSchemaVersion } from '~/src/server/plugins/engine/types/index.js'
17
20
  import {
@@ -207,7 +210,7 @@ describe('Adapter v1 formatter', () => {
207
210
  })
208
211
 
209
212
  it('should return the adapter v1 output with complete formMetadata', () => {
210
- const formMetadata: FormMetadata = {
213
+ const formMetadata: Partial<FormMetadata> = {
211
214
  id: 'form-123',
212
215
  slug: 'test-form',
213
216
  title: 'Test Form',
@@ -225,7 +228,7 @@ describe('Adapter v1 formatter', () => {
225
228
  model,
226
229
  submitResponse,
227
230
  formStatus,
228
- formMetadata
231
+ formMetadata as FormMetadata
229
232
  )
230
233
  const parsedBody = JSON.parse(body) as FormAdapterSubmissionMessagePayload
231
234
 
@@ -284,7 +287,7 @@ describe('Adapter v1 formatter', () => {
284
287
  })
285
288
 
286
289
  it('should handle preview form status correctly', () => {
287
- const formMetadata: FormMetadata = {
290
+ const formMetadata: Partial<FormMetadata> = {
288
291
  id: 'form-123',
289
292
  slug: 'test-form',
290
293
  title: 'Test Form',
@@ -302,7 +305,7 @@ describe('Adapter v1 formatter', () => {
302
305
  model,
303
306
  submitResponse,
304
307
  formStatus,
305
- formMetadata
308
+ formMetadata as FormMetadata
306
309
  )
307
310
  const parsedBody = JSON.parse(body) as FormAdapterSubmissionMessagePayload
308
311
 
@@ -327,7 +330,7 @@ describe('Adapter v1 formatter', () => {
327
330
  })
328
331
 
329
332
  it('should handle partial formMetadata', () => {
330
- const formMetadata: FormMetadata = {
333
+ const formMetadata: Partial<FormMetadata> = {
331
334
  id: 'form-456',
332
335
  slug: 'partial-form',
333
336
  title: 'Partial Form'
@@ -344,7 +347,7 @@ describe('Adapter v1 formatter', () => {
344
347
  model,
345
348
  submitResponse,
346
349
  formStatus,
347
- formMetadata
350
+ formMetadata as FormMetadata
348
351
  )
349
352
  const parsedBody = JSON.parse(body) as FormAdapterSubmissionMessagePayload
350
353
 
@@ -452,7 +455,7 @@ describe('Adapter v1 formatter', () => {
452
455
  })
453
456
 
454
457
  it('should handle formMetadata with only id', () => {
455
- const formMetadata: FormMetadata = {
458
+ const formMetadata: Partial<FormMetadata> = {
456
459
  id: 'only-id-form'
457
460
  } as FormMetadata
458
461
 
@@ -467,7 +470,7 @@ describe('Adapter v1 formatter', () => {
467
470
  model,
468
471
  submitResponse,
469
472
  formStatus,
470
- formMetadata
473
+ formMetadata as FormMetadata
471
474
  )
472
475
  const parsedBody = JSON.parse(body) as FormAdapterSubmissionMessagePayload
473
476
 
@@ -477,7 +480,7 @@ describe('Adapter v1 formatter', () => {
477
480
  })
478
481
 
479
482
  it('should handle formMetadata with only slug', () => {
480
- const formMetadata: FormMetadata = {
483
+ const formMetadata: Partial<FormMetadata> = {
481
484
  slug: 'only-slug-form'
482
485
  } as FormMetadata
483
486
 
@@ -492,7 +495,7 @@ describe('Adapter v1 formatter', () => {
492
495
  model,
493
496
  submitResponse,
494
497
  formStatus,
495
- formMetadata
498
+ formMetadata as FormMetadata
496
499
  )
497
500
  const parsedBody = JSON.parse(body) as FormAdapterSubmissionMessagePayload
498
501
 
@@ -502,7 +505,7 @@ describe('Adapter v1 formatter', () => {
502
505
  })
503
506
 
504
507
  it('should handle formMetadata with only notificationEmail', () => {
505
- const formMetadata: FormMetadata = {
508
+ const formMetadata: Partial<FormMetadata> = {
506
509
  notificationEmail: 'only-email@example.com'
507
510
  } as FormMetadata
508
511
 
@@ -517,7 +520,7 @@ describe('Adapter v1 formatter', () => {
517
520
  model,
518
521
  submitResponse,
519
522
  formStatus,
520
- formMetadata
523
+ formMetadata as FormMetadata
521
524
  )
522
525
  const parsedBody = JSON.parse(body) as FormAdapterSubmissionMessagePayload
523
526
 
@@ -693,4 +696,434 @@ describe('Adapter v1 formatter', () => {
693
696
  }
694
697
  })
695
698
  })
699
+
700
+ it('should handle missing versionMetadata gracefully', () => {
701
+ const formMetadata: Partial<FormMetadata> = {
702
+ id: 'form-123',
703
+ slug: 'test-form',
704
+ title: 'Test Form',
705
+ notificationEmail: 'test@example.com'
706
+ } as FormMetadata
707
+
708
+ const formStatus = {
709
+ isPreview: false,
710
+ state: FormStatus.Live
711
+ }
712
+
713
+ const body = format(
714
+ context,
715
+ items,
716
+ model,
717
+ submitResponse,
718
+ formStatus,
719
+ formMetadata as FormMetadata
720
+ )
721
+ const parsedBody = JSON.parse(body) as FormAdapterSubmissionMessagePayload
722
+
723
+ expect(parsedBody.meta.versionMetadata).toBeUndefined()
724
+ })
725
+
726
+ describe('version metadata handling', () => {
727
+ it('should include versionMetadata when context has submittedVersionNumber and formMetadata has versions', () => {
728
+ const formMetadata: Partial<FormMetadata> = {
729
+ id: 'form-123',
730
+ slug: 'test-form',
731
+ title: 'Test Form',
732
+ notificationEmail: 'test@example.com',
733
+ versions: [
734
+ {
735
+ versionNumber: 1,
736
+ createdAt: new Date('2024-01-01T00:00:00.000Z')
737
+ },
738
+ {
739
+ versionNumber: 2,
740
+ createdAt: new Date('2024-01-15T00:00:00.000Z')
741
+ }
742
+ ]
743
+ }
744
+
745
+ const modelWithVersion = new FormModel(definition, {
746
+ basePath: 'test',
747
+ versionNumber: 2
748
+ })
749
+
750
+ const contextWithVersion = modelWithVersion.getFormContext(request, state)
751
+
752
+ const formStatus = {
753
+ isPreview: false,
754
+ state: FormStatus.Live
755
+ }
756
+
757
+ const body = format(
758
+ contextWithVersion,
759
+ items,
760
+ modelWithVersion,
761
+ submitResponse,
762
+ formStatus,
763
+ formMetadata as FormMetadata
764
+ )
765
+ const parsedBody = JSON.parse(body) as FormAdapterSubmissionMessagePayload
766
+
767
+ expect(parsedBody.meta.versionMetadata).toEqual({
768
+ versionNumber: 2,
769
+ createdAt: '2024-01-15T00:00:00.000Z'
770
+ })
771
+ })
772
+
773
+ it('should use first version as fallback when submittedVersionNumber is undefined', () => {
774
+ const formMetadata: Partial<FormMetadata> = {
775
+ id: 'form-123',
776
+ slug: 'test-form',
777
+ title: 'Test Form',
778
+ notificationEmail: 'test@example.com',
779
+ versions: [
780
+ {
781
+ versionNumber: 1,
782
+ createdAt: new Date('2024-01-01T00:00:00.000Z')
783
+ },
784
+ {
785
+ versionNumber: 2,
786
+ createdAt: new Date('2024-01-15T00:00:00.000Z')
787
+ }
788
+ ]
789
+ }
790
+
791
+ const formStatus = {
792
+ isPreview: false,
793
+ state: FormStatus.Live
794
+ }
795
+
796
+ const body = format(
797
+ context,
798
+ items,
799
+ model,
800
+ submitResponse,
801
+ formStatus,
802
+ formMetadata as FormMetadata
803
+ )
804
+ const parsedBody = JSON.parse(body) as FormAdapterSubmissionMessagePayload
805
+
806
+ expect(parsedBody.meta.versionMetadata).toEqual({
807
+ versionNumber: 1,
808
+ createdAt: '2024-01-01T00:00:00.000Z'
809
+ })
810
+ })
811
+
812
+ it('should not include versionMetadata when submittedVersionNumber is undefined and no versions exist', () => {
813
+ const formMetadata: Partial<FormMetadata> = {
814
+ id: 'form-123',
815
+ slug: 'test-form',
816
+ title: 'Test Form',
817
+ notificationEmail: 'test@example.com'
818
+ }
819
+
820
+ const formStatus = {
821
+ isPreview: false,
822
+ state: FormStatus.Live
823
+ }
824
+
825
+ const body = format(
826
+ context,
827
+ items,
828
+ model,
829
+ submitResponse,
830
+ formStatus,
831
+ formMetadata as FormMetadata
832
+ )
833
+ const parsedBody = JSON.parse(body) as FormAdapterSubmissionMessagePayload
834
+
835
+ expect(parsedBody.meta.versionMetadata).toBeUndefined()
836
+ })
837
+
838
+ it('should not include versionMetadata when submittedVersionNumber is undefined and versions array is empty', () => {
839
+ const formMetadata: Partial<FormMetadata> = {
840
+ id: 'form-123',
841
+ slug: 'test-form',
842
+ title: 'Test Form',
843
+ notificationEmail: 'test@example.com',
844
+ versions: []
845
+ }
846
+
847
+ const formStatus = {
848
+ isPreview: false,
849
+ state: FormStatus.Live
850
+ }
851
+
852
+ const body = format(
853
+ context,
854
+ items,
855
+ model,
856
+ submitResponse,
857
+ formStatus,
858
+ formMetadata as FormMetadata
859
+ )
860
+ const parsedBody = JSON.parse(body) as FormAdapterSubmissionMessagePayload
861
+
862
+ expect(parsedBody.meta.versionMetadata).toBeUndefined()
863
+ })
864
+
865
+ it('should not include versionMetadata when submittedVersionNumber does not match any version', () => {
866
+ const formMetadata: Partial<FormMetadata> = {
867
+ id: 'form-123',
868
+ slug: 'test-form',
869
+ title: 'Test Form',
870
+ notificationEmail: 'test@example.com',
871
+ versions: [
872
+ {
873
+ versionNumber: 1,
874
+ createdAt: new Date('2024-01-01T00:00:00.000Z')
875
+ },
876
+ {
877
+ versionNumber: 2,
878
+ createdAt: new Date('2024-01-15T00:00:00.000Z')
879
+ }
880
+ ]
881
+ }
882
+
883
+ const modelWithVersion = new FormModel(definition, {
884
+ basePath: 'test',
885
+ versionNumber: 99 // Non-existent version
886
+ })
887
+
888
+ const contextWithVersion = modelWithVersion.getFormContext(request, state)
889
+
890
+ const formStatus = {
891
+ isPreview: false,
892
+ state: FormStatus.Live
893
+ }
894
+
895
+ const body = format(
896
+ contextWithVersion,
897
+ items,
898
+ modelWithVersion,
899
+ submitResponse,
900
+ formStatus,
901
+ formMetadata as FormMetadata
902
+ )
903
+ const parsedBody = JSON.parse(body) as FormAdapterSubmissionMessagePayload
904
+
905
+ // Should fall back to first version since submittedVersionNumber doesn't match
906
+ expect(parsedBody.meta.versionMetadata).toEqual({
907
+ versionNumber: 1,
908
+ createdAt: '2024-01-01T00:00:00.000Z'
909
+ })
910
+ })
911
+
912
+ it('should use first version as fallback when submittedVersionNumber does not match any version', () => {
913
+ const formMetadata: Partial<FormMetadata> = {
914
+ id: 'form-123',
915
+ slug: 'test-form',
916
+ title: 'Test Form',
917
+ notificationEmail: 'test@example.com',
918
+ versions: [
919
+ {
920
+ versionNumber: 1,
921
+ createdAt: new Date('2024-01-01T00:00:00.000Z')
922
+ },
923
+ {
924
+ versionNumber: 2,
925
+ createdAt: new Date('2024-01-15T00:00:00.000Z')
926
+ }
927
+ ]
928
+ }
929
+
930
+ const modelWithVersion = new FormModel(definition, {
931
+ basePath: 'test',
932
+ versionNumber: 99 // Non-existent version
933
+ })
934
+
935
+ const contextWithVersion = modelWithVersion.getFormContext(request, state)
936
+
937
+ const formStatus = {
938
+ isPreview: false,
939
+ state: FormStatus.Live
940
+ }
941
+
942
+ const body = format(
943
+ contextWithVersion,
944
+ items,
945
+ modelWithVersion,
946
+ submitResponse,
947
+ formStatus,
948
+ formMetadata as FormMetadata
949
+ )
950
+ const parsedBody = JSON.parse(body) as FormAdapterSubmissionMessagePayload
951
+
952
+ // Should fall back to first version since submittedVersionNumber doesn't match
953
+ expect(parsedBody.meta.versionMetadata).toEqual({
954
+ versionNumber: 1,
955
+ createdAt: '2024-01-01T00:00:00.000Z'
956
+ })
957
+ })
958
+
959
+ it('should handle single version in versions array', () => {
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: 5,
968
+ createdAt: new Date('2024-02-01T00:00:00.000Z')
969
+ }
970
+ ]
971
+ }
972
+
973
+ const formStatus = {
974
+ isPreview: false,
975
+ state: FormStatus.Live
976
+ }
977
+
978
+ const body = format(
979
+ context,
980
+ items,
981
+ model,
982
+ submitResponse,
983
+ formStatus,
984
+ formMetadata as FormMetadata
985
+ )
986
+ const parsedBody = JSON.parse(body) as FormAdapterSubmissionMessagePayload
987
+
988
+ expect(parsedBody.meta.versionMetadata).toEqual({
989
+ versionNumber: 5,
990
+ createdAt: '2024-02-01T00:00:00.000Z'
991
+ })
992
+ })
993
+ })
994
+
995
+ describe('getVersionMetadata', () => {
996
+ const mockFormMetadata: Partial<FormMetadata> = {
997
+ id: 'form-123',
998
+ slug: 'test-form',
999
+ title: 'Test Form',
1000
+ notificationEmail: 'test@example.com',
1001
+ versions: [
1002
+ {
1003
+ versionNumber: 1,
1004
+ createdAt: new Date('2024-01-01T00:00:00.000Z')
1005
+ },
1006
+ {
1007
+ versionNumber: 2,
1008
+ createdAt: new Date('2024-01-02T00:00:00.000Z')
1009
+ },
1010
+ {
1011
+ versionNumber: 3,
1012
+ createdAt: new Date('2024-01-03T00:00:00.000Z')
1013
+ }
1014
+ ]
1015
+ }
1016
+
1017
+ it('should return undefined when no form metadata provided', () => {
1018
+ const result = getVersionMetadata(1, undefined)
1019
+ expect(result).toBeUndefined()
1020
+ })
1021
+
1022
+ it('should return undefined when form metadata has no versions', () => {
1023
+ const formMetadataWithoutVersions: Partial<FormMetadata> = {
1024
+ ...mockFormMetadata,
1025
+ versions: undefined
1026
+ }
1027
+
1028
+ const result = getVersionMetadata(
1029
+ 1,
1030
+ formMetadataWithoutVersions as FormMetadata
1031
+ )
1032
+ expect(result).toBeUndefined()
1033
+ })
1034
+
1035
+ it('should return undefined when versions array is empty', () => {
1036
+ const formMetadataWithEmptyVersions: Partial<FormMetadata> = {
1037
+ ...mockFormMetadata,
1038
+ versions: []
1039
+ }
1040
+
1041
+ const result = getVersionMetadata(
1042
+ 1,
1043
+ formMetadataWithEmptyVersions as FormMetadata
1044
+ )
1045
+ expect(result).toBeUndefined()
1046
+ })
1047
+
1048
+ it('should return specific version when submittedVersionNumber matches', () => {
1049
+ const result = getVersionMetadata(2, mockFormMetadata as FormMetadata)
1050
+ expect(result).toEqual({
1051
+ versionNumber: 2,
1052
+ createdAt: new Date('2024-01-02T00:00:00.000Z')
1053
+ })
1054
+ })
1055
+
1056
+ it('should return first version when submittedVersionNumber not found', () => {
1057
+ const result = getVersionMetadata(999, mockFormMetadata as FormMetadata)
1058
+ expect(result).toEqual({
1059
+ versionNumber: 1,
1060
+ createdAt: new Date('2024-01-01T00:00:00.000Z')
1061
+ })
1062
+ })
1063
+
1064
+ it('should return first version when no submittedVersionNumber provided', () => {
1065
+ const result = getVersionMetadata(
1066
+ undefined,
1067
+ mockFormMetadata as FormMetadata
1068
+ )
1069
+ expect(result).toEqual({
1070
+ versionNumber: 1,
1071
+ createdAt: new Date('2024-01-01T00:00:00.000Z')
1072
+ })
1073
+ })
1074
+
1075
+ it('should handle single version in versions array', () => {
1076
+ const singleVersionMetadata: Partial<FormMetadata> = {
1077
+ ...mockFormMetadata,
1078
+ versions: [
1079
+ {
1080
+ versionNumber: 5,
1081
+ createdAt: new Date('2024-02-01T00:00:00.000Z')
1082
+ }
1083
+ ]
1084
+ }
1085
+
1086
+ const result = getVersionMetadata(
1087
+ undefined,
1088
+ singleVersionMetadata as FormMetadata
1089
+ )
1090
+ expect(result).toEqual({
1091
+ versionNumber: 5,
1092
+ createdAt: new Date('2024-02-01T00:00:00.000Z')
1093
+ })
1094
+ })
1095
+
1096
+ it('should return correct version when submittedVersionNumber is 0', () => {
1097
+ const metadataWithVersionZero: Partial<FormMetadata> = {
1098
+ ...mockFormMetadata,
1099
+ versions: [
1100
+ {
1101
+ versionNumber: 0,
1102
+ createdAt: new Date('2024-01-01T00:00:00.000Z')
1103
+ },
1104
+ {
1105
+ versionNumber: 1,
1106
+ createdAt: new Date('2024-01-02T00:00:00.000Z')
1107
+ }
1108
+ ]
1109
+ }
1110
+
1111
+ const result = getVersionMetadata(
1112
+ 0,
1113
+ metadataWithVersionZero as FormMetadata
1114
+ )
1115
+ expect(result).toEqual({
1116
+ versionNumber: 0,
1117
+ createdAt: new Date('2024-01-01T00:00:00.000Z')
1118
+ })
1119
+ })
1120
+
1121
+ it('should handle negative submittedVersionNumber by falling back to first version', () => {
1122
+ const result = getVersionMetadata(-1, mockFormMetadata as FormMetadata)
1123
+ expect(result).toEqual({
1124
+ versionNumber: 1,
1125
+ createdAt: new Date('2024-01-01T00:00:00.000Z')
1126
+ })
1127
+ })
1128
+ })
696
1129
  })
@@ -40,6 +40,11 @@ export function format(
40
40
 
41
41
  const transformedData = v2DataParsed.data
42
42
 
43
+ const versionMetadata = getVersionMetadata(
44
+ context.submittedVersionNumber,
45
+ formMetadata
46
+ )
47
+
43
48
  const meta: FormAdapterSubmissionMessageMeta = {
44
49
  schemaVersion: FormAdapterSubmissionSchemaVersion.V1,
45
50
  timestamp: new Date(),
@@ -51,6 +56,10 @@ export function format(
51
56
  isPreview: formStatus.isPreview,
52
57
  notificationEmail: formMetadata?.notificationEmail ?? ''
53
58
  }
59
+
60
+ if (versionMetadata) {
61
+ meta.versionMetadata = versionMetadata
62
+ }
54
63
  const data: FormAdapterSubmissionMessageData = transformedData
55
64
 
56
65
  const result: FormAdapterSubmissionMessageResult = {
@@ -66,6 +75,34 @@ export function format(
66
75
  return JSON.stringify(payload)
67
76
  }
68
77
 
78
+ export function getVersionMetadata(
79
+ submittedVersionNumber: number | undefined,
80
+ formMetadata?: FormMetadata
81
+ ): { versionNumber: number; createdAt: Date } | undefined {
82
+ if (!formMetadata?.versions?.length) {
83
+ return undefined
84
+ }
85
+
86
+ if (submittedVersionNumber !== undefined) {
87
+ const submittedVersion = formMetadata.versions.find(
88
+ (v) => v.versionNumber === submittedVersionNumber
89
+ )
90
+ if (submittedVersion) {
91
+ return {
92
+ versionNumber: submittedVersion.versionNumber,
93
+ createdAt: submittedVersion.createdAt
94
+ }
95
+ }
96
+ }
97
+
98
+ // fallback to first available version
99
+ const firstVersion = formMetadata.versions[0]
100
+ return {
101
+ versionNumber: firstVersion.versionNumber,
102
+ createdAt: firstVersion.createdAt
103
+ }
104
+ }
105
+
69
106
  function extractCsvFiles(
70
107
  submitResponse: SubmitResponsePayload
71
108
  ): FormAdapterSubmissionMessageResult['files'] {
@@ -29,13 +29,15 @@ export function format(
29
29
 
30
30
  const categorisedData = categoriseData(items)
31
31
 
32
+ const meta: Record<string, unknown> = {
33
+ schemaVersion: '2',
34
+ timestamp: now.toISOString(),
35
+ definition: model.def,
36
+ referenceNumber: context.referenceNumber
37
+ }
38
+
32
39
  const data = {
33
- meta: {
34
- schemaVersion: '2',
35
- timestamp: now.toISOString(),
36
- definition: model.def,
37
- referenceNumber: context.referenceNumber
38
- },
40
+ meta,
39
41
  data: categorisedData
40
42
  }
41
43
 
@@ -1,6 +1,5 @@
1
1
  /* eslint-disable @typescript-eslint/dot-notation */
2
2
  import { ComponentType, type ComponentDef } from '@defra/forms-model'
3
- import { type ResponseToolkit } from '@hapi/hapi'
4
3
  import { type ValidationErrorItem, type ValidationResult } from 'joi'
5
4
 
6
5
  import { tempItemSchema } from '~/src/server/plugins/engine/components/FileUploadField.js'
@@ -14,7 +13,7 @@ import {
14
13
  prepareStatus
15
14
  } from '~/src/server/plugins/engine/pageControllers/FileUploadPageController.js'
16
15
  import { QuestionPageController } from '~/src/server/plugins/engine/pageControllers/QuestionPageController.js'
17
- import { serverWithSaveAndReturn } from '~/src/server/plugins/engine/pageControllers/__stubs__/server.js'
16
+ import { serverWithSaveAndExit } from '~/src/server/plugins/engine/pageControllers/__stubs__/server.js'
18
17
  import * as pageHelpers from '~/src/server/plugins/engine/pageControllers/helpers/index.js'
19
18
  import * as uploadService from '~/src/server/plugins/engine/services/uploadService.js'
20
19
  import {
@@ -30,7 +29,8 @@ import {
30
29
  } from '~/src/server/plugins/engine/types.js'
31
30
  import {
32
31
  type FormRequest,
33
- type FormRequestPayload
32
+ type FormRequestPayload,
33
+ type FormResponseToolkit
34
34
  } from '~/src/server/routes/types.js'
35
35
  import { type CacheService } from '~/src/server/services/index.js'
36
36
  import definition from '~/test/form/definitions/file-upload-basic.js'
@@ -1040,7 +1040,7 @@ describe('FileUploadPageController', () => {
1040
1040
  } as unknown as FormRequest
1041
1041
 
1042
1042
  const context = { state } as unknown as FormContext
1043
- const h = {} as unknown as Pick<ResponseToolkit, 'redirect' | 'view'>
1043
+ const h = {} as unknown as FormResponseToolkit
1044
1044
 
1045
1045
  const handler = controller.makeGetItemDeleteRouteHandler()
1046
1046
 
@@ -1058,7 +1058,7 @@ describe('FileUploadPageController', () => {
1058
1058
 
1059
1059
  const h = {
1060
1060
  redirect: jest.fn()
1061
- } as unknown as Pick<ResponseToolkit, 'redirect' | 'view'>
1061
+ } as unknown as FormResponseToolkit
1062
1062
 
1063
1063
  const context = {
1064
1064
  state: {}
@@ -1119,11 +1119,9 @@ describe('FileUploadPageController', () => {
1119
1119
  })
1120
1120
  })
1121
1121
 
1122
- describe('shouldShowSaveAndReturn', () => {
1123
- it('should return true when save and return is enabled', () => {
1124
- expect(controller.shouldShowSaveAndReturn(serverWithSaveAndReturn)).toBe(
1125
- true
1126
- )
1122
+ describe('shouldShowSaveAndExit', () => {
1123
+ it('should return true when save and exit is enabled', () => {
1124
+ expect(controller.shouldShowSaveAndExit(serverWithSaveAndExit)).toBe(true)
1127
1125
  })
1128
1126
  })
1129
1127
  })