From 8d92a4c128a576ea64de7c3c9bb4fca852992e70 Mon Sep 17 00:00:00 2001 From: Mohammed Khalid Date: Mon, 1 Sep 2025 14:02:37 +0100 Subject: [PATCH 1/6] feat: add CSV file IDs to adapter v1 for form runner integration --- .../outputFormatters/adapter/v1.test.ts | 196 +++++++++++++++++- .../engine/outputFormatters/adapter/v1.ts | 65 +++++- 2 files changed, 252 insertions(+), 9 deletions(-) diff --git a/src/server/plugins/engine/outputFormatters/adapter/v1.test.ts b/src/server/plugins/engine/outputFormatters/adapter/v1.test.ts index 12b6dc6c8..53f7d4ab3 100644 --- a/src/server/plugins/engine/outputFormatters/adapter/v1.test.ts +++ b/src/server/plugins/engine/outputFormatters/adapter/v1.test.ts @@ -20,13 +20,22 @@ import { import { FormStatus } from '~/src/server/routes/types.js' import definition from '~/test/form/definitions/repeat-mixed.js' +interface AdapterTestPayload extends FormAdapterSubmissionMessagePayload { + result: { + files: { + main?: string + repeaters: Record + } + } +} + const submitResponse = { message: 'Submit completed', result: { files: { main: '00000000-0000-0000-0000-000000000000', repeaters: { - pizza: '11111111-1111-1111-1111-111111111111' + exampleRepeat: '11111111-1111-1111-1111-111111111111' } } } @@ -224,7 +233,7 @@ describe('Adapter v1 formatter', () => { formStatus, formMetadata ) - const parsedBody = JSON.parse(body) as FormAdapterSubmissionMessagePayload + const parsedBody = JSON.parse(body) as AdapterTestPayload expect(parsedBody.meta).toEqual({ schemaVersion: FormAdapterSubmissionSchemaVersion.V1, @@ -246,11 +255,15 @@ describe('Adapter v1 formatter', () => { repeaters: { exampleRepeat: [ { - subItem1_1: 'hello world', - subItem1_2: 'hello world' + state: { + subItem1_1: 'hello world', + subItem1_2: 'hello world' + } }, { - subItem2_1: 'hello world' + state: { + subItem2_1: 'hello world' + } } ] }, @@ -267,6 +280,15 @@ describe('Adapter v1 formatter', () => { ] } }) + + expect(parsedBody.result).toEqual({ + files: { + main: '00000000-0000-0000-0000-000000000000', + repeaters: { + exampleRepeat: '11111111-1111-1111-1111-111111111111' + } + } + }) }) it('should handle preview form status correctly', () => { @@ -380,11 +402,19 @@ describe('Adapter v1 formatter', () => { } const body = format(context, [], model, submitResponse, formStatus) - const parsedBody = JSON.parse(body) as FormAdapterSubmissionMessagePayload + const parsedBody = JSON.parse(body) as AdapterTestPayload expect(parsedBody.data.main).toEqual({}) expect(parsedBody.data.repeaters).toEqual({}) expect(parsedBody.data.files).toEqual({}) + expect(parsedBody.result).toEqual({ + files: { + main: '00000000-0000-0000-0000-000000000000', + repeaters: { + exampleRepeat: '11111111-1111-1111-1111-111111111111' + } + } + }) }) it('should handle different form statuses', () => { @@ -503,4 +533,158 @@ describe('Adapter v1 formatter', () => { expect(parsedBody.meta.formSlug).toBe('') expect(parsedBody.meta.notificationEmail).toBe('only-email@example.com') }) + + it('should include CSV file IDs from submitResponse.result.files', () => { + const formStatus = { + isPreview: false, + state: FormStatus.Live + } + + const body = format(context, items, model, submitResponse, formStatus) + const parsedBody = JSON.parse(body) as AdapterTestPayload + + // Check that main data has no CSV file IDs (they're in result.files) + expect(parsedBody.data.main).toEqual({ + exampleField: 'hello world', + exampleField2: 'hello world' + }) + + // Check that repeater data uses new state structure + expect(parsedBody.data.repeaters.exampleRepeat).toEqual([ + { + state: { + subItem1_1: 'hello world', + subItem1_2: 'hello world' + } + }, + { + state: { + subItem2_1: 'hello world' + } + } + ]) + + // Files section should remain unchanged + expect(parsedBody.data.files.exampleFile1).toEqual([ + { + fileId: '123-456-789', + userDownloadLink: 'https://forms-designer/file-download/123-456-789' + }, + { + fileId: '456-789-123', + userDownloadLink: 'https://forms-designer/file-download/456-789-123' + } + ]) + + // CSV file IDs should be in result.files + expect(parsedBody.result).toEqual({ + files: { + main: '00000000-0000-0000-0000-000000000000', + repeaters: { + exampleRepeat: '11111111-1111-1111-1111-111111111111' + } + } + }) + }) + + it('should handle submitResponse without CSV file IDs gracefully', () => { + const submitResponseWithoutFiles = { + message: 'Submit completed', + result: { + files: { + main: '', + repeaters: {} + } + } + } + + const formStatus = { + isPreview: false, + state: FormStatus.Live + } + + const body = format( + context, + items, + model, + submitResponseWithoutFiles, + formStatus + ) + const parsedBody = JSON.parse(body) as AdapterTestPayload + + // Should work normally without CSV file IDs + expect(parsedBody.data.main).toEqual({ + exampleField: 'hello world', + exampleField2: 'hello world' + }) + + expect(parsedBody.data.repeaters.exampleRepeat).toEqual([ + { + state: { + subItem1_1: 'hello world', + subItem1_2: 'hello world' + } + }, + { + state: { + subItem2_1: 'hello world' + } + } + ]) + }) + + it('should handle submitResponse with only main CSV file ID', () => { + const submitResponseWithMainOnly = { + message: 'Submit completed', + result: { + files: { + main: 'main-only-file-id', + repeaters: {} + } + } + } + + const formStatus = { + isPreview: false, + state: FormStatus.Live + } + + const body = format( + context, + items, + model, + submitResponseWithMainOnly, + formStatus + ) + const parsedBody = JSON.parse(body) as AdapterTestPayload + + // Should work normally + expect(parsedBody.data.main).toEqual({ + exampleField: 'hello world', + exampleField2: 'hello world' + }) + + // Repeaters should use state structure + expect(parsedBody.data.repeaters.exampleRepeat).toEqual([ + { + state: { + subItem1_1: 'hello world', + subItem1_2: 'hello world' + } + }, + { + state: { + subItem2_1: 'hello world' + } + } + ]) + + // CSV file IDs should be in result.files + expect(parsedBody.result).toEqual({ + files: { + main: 'main-only-file-id', + repeaters: {} + } + }) + }) }) diff --git a/src/server/plugins/engine/outputFormatters/adapter/v1.ts b/src/server/plugins/engine/outputFormatters/adapter/v1.ts index 6efacb704..2bac193a7 100644 --- a/src/server/plugins/engine/outputFormatters/adapter/v1.ts +++ b/src/server/plugins/engine/outputFormatters/adapter/v1.ts @@ -6,7 +6,10 @@ import { import { type checkFormStatus } from '~/src/server/plugins/engine/helpers.js' import { type FormModel } from '~/src/server/plugins/engine/models/FormModel.js' import { type DetailItem } from '~/src/server/plugins/engine/models/types.js' -import { format as machineV2 } from '~/src/server/plugins/engine/outputFormatters/machine/v2.js' +import { + format as machineV2, + type RichFormValue +} from '~/src/server/plugins/engine/outputFormatters/machine/v2.js' import { FormAdapterSubmissionSchemaVersion } from '~/src/server/plugins/engine/types/enums.js' import { type FormAdapterSubmissionMessageData, @@ -15,6 +18,24 @@ import { } from '~/src/server/plugins/engine/types.js' import { FormStatus } from '~/src/server/routes/types.js' +interface CsvFiles { + main?: string + repeaters: Record +} + +interface TransformedData + extends Omit { + repeaters: Record }[]> +} + +interface AdapterPayload { + meta: FormAdapterSubmissionMessagePayload['meta'] + data: TransformedData + result: { + files: CsvFiles + } +} + export function format( context: FormContext, items: DetailItem[], @@ -34,7 +55,11 @@ export function format( data: FormAdapterSubmissionMessageData } - const payload: FormAdapterSubmissionMessagePayload = { + const csvFiles = extractCsvFiles(submitResponse) + + const transformedData = transformRepeaters(v2DataParsed.data) + + const payload: AdapterPayload = { meta: { schemaVersion: FormAdapterSubmissionSchemaVersion.V1, timestamp: new Date(), @@ -46,8 +71,42 @@ export function format( isPreview: formStatus.isPreview, notificationEmail: formMetadata?.notificationEmail ?? '' }, - data: v2DataParsed.data + data: transformedData, + result: { + files: csvFiles + } } return JSON.stringify(payload) } + +function extractCsvFiles(submitResponse: SubmitResponsePayload): CsvFiles { + const result = submitResponse.result as { + files?: { + main?: string + repeaters?: Record + } + } + + return { + main: result.files?.main, + repeaters: result.files?.repeaters ?? {} + } +} + +function transformRepeaters( + data: FormAdapterSubmissionMessageData +): TransformedData { + const transformedData: TransformedData = { + ...data, + repeaters: {} + } + + Object.entries(data.repeaters).forEach(([repeaterName, items]) => { + transformedData.repeaters[repeaterName] = items.map((item) => ({ + state: item + })) + }) + + return transformedData +} From 5ec65529a18b24acebfa34b79d7d4e5c67648391 Mon Sep 17 00:00:00 2001 From: Mohammed Khalid Date: Mon, 1 Sep 2025 14:19:08 +0100 Subject: [PATCH 2/6] refactor: keep original repeater data structure unchanged --- .../outputFormatters/adapter/v1.test.ts | 44 ++++++------------- .../engine/outputFormatters/adapter/v1.ts | 31 ++----------- 2 files changed, 17 insertions(+), 58 deletions(-) diff --git a/src/server/plugins/engine/outputFormatters/adapter/v1.test.ts b/src/server/plugins/engine/outputFormatters/adapter/v1.test.ts index 53f7d4ab3..8f2e059ce 100644 --- a/src/server/plugins/engine/outputFormatters/adapter/v1.test.ts +++ b/src/server/plugins/engine/outputFormatters/adapter/v1.test.ts @@ -255,15 +255,11 @@ describe('Adapter v1 formatter', () => { repeaters: { exampleRepeat: [ { - state: { - subItem1_1: 'hello world', - subItem1_2: 'hello world' - } + subItem1_1: 'hello world', + subItem1_2: 'hello world' }, { - state: { - subItem2_1: 'hello world' - } + subItem2_1: 'hello world' } ] }, @@ -549,18 +545,14 @@ describe('Adapter v1 formatter', () => { exampleField2: 'hello world' }) - // Check that repeater data uses new state structure + // Check that repeater data uses direct field structure expect(parsedBody.data.repeaters.exampleRepeat).toEqual([ { - state: { - subItem1_1: 'hello world', - subItem1_2: 'hello world' - } + subItem1_1: 'hello world', + subItem1_2: 'hello world' }, { - state: { - subItem2_1: 'hello world' - } + subItem2_1: 'hello world' } ]) @@ -620,15 +612,11 @@ describe('Adapter v1 formatter', () => { expect(parsedBody.data.repeaters.exampleRepeat).toEqual([ { - state: { - subItem1_1: 'hello world', - subItem1_2: 'hello world' - } + subItem1_1: 'hello world', + subItem1_2: 'hello world' }, { - state: { - subItem2_1: 'hello world' - } + subItem2_1: 'hello world' } ]) }) @@ -664,18 +652,14 @@ describe('Adapter v1 formatter', () => { exampleField2: 'hello world' }) - // Repeaters should use state structure + // Repeaters should use direct field structure expect(parsedBody.data.repeaters.exampleRepeat).toEqual([ { - state: { - subItem1_1: 'hello world', - subItem1_2: 'hello world' - } + subItem1_1: 'hello world', + subItem1_2: 'hello world' }, { - state: { - subItem2_1: 'hello world' - } + subItem2_1: 'hello world' } ]) diff --git a/src/server/plugins/engine/outputFormatters/adapter/v1.ts b/src/server/plugins/engine/outputFormatters/adapter/v1.ts index 2bac193a7..f55d8d765 100644 --- a/src/server/plugins/engine/outputFormatters/adapter/v1.ts +++ b/src/server/plugins/engine/outputFormatters/adapter/v1.ts @@ -6,10 +6,7 @@ import { import { type checkFormStatus } from '~/src/server/plugins/engine/helpers.js' import { type FormModel } from '~/src/server/plugins/engine/models/FormModel.js' import { type DetailItem } from '~/src/server/plugins/engine/models/types.js' -import { - format as machineV2, - type RichFormValue -} from '~/src/server/plugins/engine/outputFormatters/machine/v2.js' +import { format as machineV2 } from '~/src/server/plugins/engine/outputFormatters/machine/v2.js' import { FormAdapterSubmissionSchemaVersion } from '~/src/server/plugins/engine/types/enums.js' import { type FormAdapterSubmissionMessageData, @@ -23,14 +20,9 @@ interface CsvFiles { repeaters: Record } -interface TransformedData - extends Omit { - repeaters: Record }[]> -} - interface AdapterPayload { meta: FormAdapterSubmissionMessagePayload['meta'] - data: TransformedData + data: FormAdapterSubmissionMessageData result: { files: CsvFiles } @@ -57,7 +49,7 @@ export function format( const csvFiles = extractCsvFiles(submitResponse) - const transformedData = transformRepeaters(v2DataParsed.data) + const transformedData = v2DataParsed.data const payload: AdapterPayload = { meta: { @@ -93,20 +85,3 @@ function extractCsvFiles(submitResponse: SubmitResponsePayload): CsvFiles { repeaters: result.files?.repeaters ?? {} } } - -function transformRepeaters( - data: FormAdapterSubmissionMessageData -): TransformedData { - const transformedData: TransformedData = { - ...data, - repeaters: {} - } - - Object.entries(data.repeaters).forEach(([repeaterName, items]) => { - transformedData.repeaters[repeaterName] = items.map((item) => ({ - state: item - })) - }) - - return transformedData -} From 995bd4e73925a782a3d7d06813f47973055e26d4 Mon Sep 17 00:00:00 2001 From: Mohammed Khalid Date: Mon, 1 Sep 2025 14:35:30 +0100 Subject: [PATCH 3/6] test: added test for sonar coverage --- .../outputFormatters/adapter/v1.test.ts | 45 +++++++++++++++---- 1 file changed, 36 insertions(+), 9 deletions(-) diff --git a/src/server/plugins/engine/outputFormatters/adapter/v1.test.ts b/src/server/plugins/engine/outputFormatters/adapter/v1.test.ts index 8f2e059ce..f7386993c 100644 --- a/src/server/plugins/engine/outputFormatters/adapter/v1.test.ts +++ b/src/server/plugins/engine/outputFormatters/adapter/v1.test.ts @@ -1,4 +1,7 @@ -import { type FormMetadata } from '@defra/forms-model' +import { + type FormMetadata, + type SubmitResponsePayload +} from '@defra/forms-model' import { FileUploadField } from '~/src/server/plugins/engine/components/FileUploadField.js' import { type Field } from '~/src/server/plugins/engine/components/helpers/components.js' @@ -539,13 +542,11 @@ describe('Adapter v1 formatter', () => { const body = format(context, items, model, submitResponse, formStatus) const parsedBody = JSON.parse(body) as AdapterTestPayload - // Check that main data has no CSV file IDs (they're in result.files) expect(parsedBody.data.main).toEqual({ exampleField: 'hello world', exampleField2: 'hello world' }) - // Check that repeater data uses direct field structure expect(parsedBody.data.repeaters.exampleRepeat).toEqual([ { subItem1_1: 'hello world', @@ -556,7 +557,6 @@ describe('Adapter v1 formatter', () => { } ]) - // Files section should remain unchanged expect(parsedBody.data.files.exampleFile1).toEqual([ { fileId: '123-456-789', @@ -568,7 +568,6 @@ describe('Adapter v1 formatter', () => { } ]) - // CSV file IDs should be in result.files expect(parsedBody.result).toEqual({ files: { main: '00000000-0000-0000-0000-000000000000', @@ -604,7 +603,6 @@ describe('Adapter v1 formatter', () => { ) const parsedBody = JSON.parse(body) as AdapterTestPayload - // Should work normally without CSV file IDs expect(parsedBody.data.main).toEqual({ exampleField: 'hello world', exampleField2: 'hello world' @@ -646,13 +644,11 @@ describe('Adapter v1 formatter', () => { ) const parsedBody = JSON.parse(body) as AdapterTestPayload - // Should work normally expect(parsedBody.data.main).toEqual({ exampleField: 'hello world', exampleField2: 'hello world' }) - // Repeaters should use direct field structure expect(parsedBody.data.repeaters.exampleRepeat).toEqual([ { subItem1_1: 'hello world', @@ -663,7 +659,38 @@ describe('Adapter v1 formatter', () => { } ]) - // CSV file IDs should be in result.files + expect(parsedBody.result).toEqual({ + files: { + main: 'main-only-file-id', + repeaters: {} + } + }) + }) + + it('should handle submitResponse with missing repeaters property', () => { + const submitResponseWithoutRepeaters = { + message: 'Submit completed', + result: { + files: { + main: 'main-only-file-id' + } + } + } + + const formStatus = { + isPreview: false, + state: FormStatus.Live + } + + const body = format( + context, + items, + model, + submitResponseWithoutRepeaters as unknown as SubmitResponsePayload, + formStatus + ) + const parsedBody = JSON.parse(body) as AdapterTestPayload + expect(parsedBody.result).toEqual({ files: { main: 'main-only-file-id', From 62e564e331783b523478356dc5110e1774bd17ca Mon Sep 17 00:00:00 2001 From: Mohammed Khalid Date: Mon, 1 Sep 2025 17:11:57 +0100 Subject: [PATCH 4/6] refactor: append fileName --- .../engine/outputFormatters/adapter/v1.test.ts | 4 ++++ .../engine/outputFormatters/machine/v2.test.ts | 2 ++ .../engine/outputFormatters/machine/v2.ts | 18 ++++++++++++------ 3 files changed, 18 insertions(+), 6 deletions(-) diff --git a/src/server/plugins/engine/outputFormatters/adapter/v1.test.ts b/src/server/plugins/engine/outputFormatters/adapter/v1.test.ts index f7386993c..ba44c5b60 100644 --- a/src/server/plugins/engine/outputFormatters/adapter/v1.test.ts +++ b/src/server/plugins/engine/outputFormatters/adapter/v1.test.ts @@ -270,10 +270,12 @@ describe('Adapter v1 formatter', () => { exampleFile1: [ { fileId: '123-456-789', + fileName: 'foobar.txt', userDownloadLink: 'https://forms-designer/file-download/123-456-789' }, { fileId: '456-789-123', + fileName: 'bazbuzz.txt', userDownloadLink: 'https://forms-designer/file-download/456-789-123' } ] @@ -560,10 +562,12 @@ describe('Adapter v1 formatter', () => { expect(parsedBody.data.files.exampleFile1).toEqual([ { fileId: '123-456-789', + fileName: 'foobar.txt', userDownloadLink: 'https://forms-designer/file-download/123-456-789' }, { fileId: '456-789-123', + fileName: 'bazbuzz.txt', userDownloadLink: 'https://forms-designer/file-download/456-789-123' } ]) diff --git a/src/server/plugins/engine/outputFormatters/machine/v2.test.ts b/src/server/plugins/engine/outputFormatters/machine/v2.test.ts index cfd85ff89..a29a99ff3 100644 --- a/src/server/plugins/engine/outputFormatters/machine/v2.test.ts +++ b/src/server/plugins/engine/outputFormatters/machine/v2.test.ts @@ -249,10 +249,12 @@ describe('getPersonalisation', () => { exampleFile1: [ { fileId: '123-456-789', + fileName: 'foobar.txt', userDownloadLink: 'https://forms-designer/file-download/123-456-789' }, { fileId: '456-789-123', + fileName: 'bazbuzz.txt', userDownloadLink: 'https://forms-designer/file-download/456-789-123' } ] diff --git a/src/server/plugins/engine/outputFormatters/machine/v2.ts b/src/server/plugins/engine/outputFormatters/machine/v2.ts index 5e2586a8e..eba23b4fc 100644 --- a/src/server/plugins/engine/outputFormatters/machine/v2.ts +++ b/src/server/plugins/engine/outputFormatters/machine/v2.ts @@ -69,7 +69,8 @@ export function format( * fileComponentName: [ * { * fileId: '123-456-789', - * link: 'https://forms-designer/file-download/123-456-789' + * fileName: 'example.pdf', + * userDownloadLink: 'https://forms-designer/file-download/123-456-789' * } * ] * } @@ -79,7 +80,10 @@ function categoriseData(items: DetailItem[]) { const output: { main: Record repeaters: Record[]> - files: Record[]> + files: Record< + string, + { fileId: string; fileName: string; userDownloadLink: string }[] + > } = { main: {}, repeaters: {}, files: {} } items.forEach((item) => { @@ -127,12 +131,14 @@ function extractRepeaters(item: DetailItemRepeat) { * @returns the file upload data */ function extractFileUploads(item: FileUploadFieldDetailitem) { - const fileUploadState = item.field.getContextValueFromState(item.state) ?? [] + const fileUploadState = item.field.getFormValueFromState(item.state) ?? [] - return fileUploadState.map((fileId) => { + return fileUploadState.map((fileState) => { + const { file } = fileState.status.form return { - fileId, - userDownloadLink: `${designerUrl}/file-download/${fileId}` + fileId: file.fileId, + fileName: file.filename, + userDownloadLink: `${designerUrl}/file-download/${file.fileId}` } }) } From dc06e4580642bb1f6f6909852ab561240a045f62 Mon Sep 17 00:00:00 2001 From: whitewater design Date: Mon, 1 Sep 2025 21:20:57 +0100 Subject: [PATCH 5/6] feat: DF-380 - Update adapter v1 schema and types --- .../outputFormatters/adapter/v1.test.ts | 21 +-- .../engine/outputFormatters/adapter/v1.ts | 66 ++++----- .../engine/outputFormatters/machine/v2.ts | 5 +- src/server/plugins/engine/types.ts | 15 ++- .../plugins/engine/types/schema.test.ts | 126 ++++++++++-------- src/server/plugins/engine/types/schema.ts | 25 +++- 6 files changed, 143 insertions(+), 115 deletions(-) diff --git a/src/server/plugins/engine/outputFormatters/adapter/v1.test.ts b/src/server/plugins/engine/outputFormatters/adapter/v1.test.ts index ba44c5b60..e4657c42d 100644 --- a/src/server/plugins/engine/outputFormatters/adapter/v1.test.ts +++ b/src/server/plugins/engine/outputFormatters/adapter/v1.test.ts @@ -23,15 +23,6 @@ import { import { FormStatus } from '~/src/server/routes/types.js' import definition from '~/test/form/definitions/repeat-mixed.js' -interface AdapterTestPayload extends FormAdapterSubmissionMessagePayload { - result: { - files: { - main?: string - repeaters: Record - } - } -} - const submitResponse = { message: 'Submit completed', result: { @@ -236,7 +227,7 @@ describe('Adapter v1 formatter', () => { formStatus, formMetadata ) - const parsedBody = JSON.parse(body) as AdapterTestPayload + const parsedBody = JSON.parse(body) as FormAdapterSubmissionMessagePayload expect(parsedBody.meta).toEqual({ schemaVersion: FormAdapterSubmissionSchemaVersion.V1, @@ -403,7 +394,7 @@ describe('Adapter v1 formatter', () => { } const body = format(context, [], model, submitResponse, formStatus) - const parsedBody = JSON.parse(body) as AdapterTestPayload + const parsedBody = JSON.parse(body) as FormAdapterSubmissionMessagePayload expect(parsedBody.data.main).toEqual({}) expect(parsedBody.data.repeaters).toEqual({}) @@ -542,7 +533,7 @@ describe('Adapter v1 formatter', () => { } const body = format(context, items, model, submitResponse, formStatus) - const parsedBody = JSON.parse(body) as AdapterTestPayload + const parsedBody = JSON.parse(body) as FormAdapterSubmissionMessagePayload expect(parsedBody.data.main).toEqual({ exampleField: 'hello world', @@ -605,7 +596,7 @@ describe('Adapter v1 formatter', () => { submitResponseWithoutFiles, formStatus ) - const parsedBody = JSON.parse(body) as AdapterTestPayload + const parsedBody = JSON.parse(body) as FormAdapterSubmissionMessagePayload expect(parsedBody.data.main).toEqual({ exampleField: 'hello world', @@ -646,7 +637,7 @@ describe('Adapter v1 formatter', () => { submitResponseWithMainOnly, formStatus ) - const parsedBody = JSON.parse(body) as AdapterTestPayload + const parsedBody = JSON.parse(body) as FormAdapterSubmissionMessagePayload expect(parsedBody.data.main).toEqual({ exampleField: 'hello world', @@ -693,7 +684,7 @@ describe('Adapter v1 formatter', () => { submitResponseWithoutRepeaters as unknown as SubmitResponsePayload, formStatus ) - const parsedBody = JSON.parse(body) as AdapterTestPayload + const parsedBody = JSON.parse(body) as FormAdapterSubmissionMessagePayload expect(parsedBody.result).toEqual({ files: { diff --git a/src/server/plugins/engine/outputFormatters/adapter/v1.ts b/src/server/plugins/engine/outputFormatters/adapter/v1.ts index f55d8d765..a2fc5ab47 100644 --- a/src/server/plugins/engine/outputFormatters/adapter/v1.ts +++ b/src/server/plugins/engine/outputFormatters/adapter/v1.ts @@ -10,24 +10,13 @@ import { format as machineV2 } from '~/src/server/plugins/engine/outputFormatter import { FormAdapterSubmissionSchemaVersion } from '~/src/server/plugins/engine/types/enums.js' import { type FormAdapterSubmissionMessageData, + type FormAdapterSubmissionMessageMeta, type FormAdapterSubmissionMessagePayload, + type FormAdapterSubmissionMessageResult, type FormContext } from '~/src/server/plugins/engine/types.js' import { FormStatus } from '~/src/server/routes/types.js' -interface CsvFiles { - main?: string - repeaters: Record -} - -interface AdapterPayload { - meta: FormAdapterSubmissionMessagePayload['meta'] - data: FormAdapterSubmissionMessageData - result: { - files: CsvFiles - } -} - export function format( context: FormContext, items: DetailItem[], @@ -51,37 +40,40 @@ export function format( const transformedData = v2DataParsed.data - const payload: AdapterPayload = { - meta: { - schemaVersion: FormAdapterSubmissionSchemaVersion.V1, - timestamp: new Date(), - referenceNumber: context.referenceNumber, - formName: model.name, - formId: formMetadata?.id ?? '', - formSlug: formMetadata?.slug ?? '', - status: formStatus.isPreview ? FormStatus.Draft : FormStatus.Live, - isPreview: formStatus.isPreview, - notificationEmail: formMetadata?.notificationEmail ?? '' - }, - data: transformedData, - result: { - files: csvFiles - } + const meta: FormAdapterSubmissionMessageMeta = { + schemaVersion: FormAdapterSubmissionSchemaVersion.V1, + timestamp: new Date(), + referenceNumber: context.referenceNumber, + formName: model.name, + formId: formMetadata?.id ?? '', + formSlug: formMetadata?.slug ?? '', + status: formStatus.isPreview ? FormStatus.Draft : FormStatus.Live, + isPreview: formStatus.isPreview, + notificationEmail: formMetadata?.notificationEmail ?? '' + } + const data: FormAdapterSubmissionMessageData = transformedData + + const result: FormAdapterSubmissionMessageResult = { + files: csvFiles + } + + const payload: FormAdapterSubmissionMessagePayload = { + meta, + data, + result } return JSON.stringify(payload) } -function extractCsvFiles(submitResponse: SubmitResponsePayload): CsvFiles { - const result = submitResponse.result as { - files?: { - main?: string - repeaters?: Record - } - } +function extractCsvFiles( + submitResponse: SubmitResponsePayload +): FormAdapterSubmissionMessageResult['files'] { + const result = + submitResponse.result as Partial return { - main: result.files?.main, + main: result.files?.main ?? '', repeaters: result.files?.repeaters ?? {} } } diff --git a/src/server/plugins/engine/outputFormatters/machine/v2.ts b/src/server/plugins/engine/outputFormatters/machine/v2.ts index eba23b4fc..db203e758 100644 --- a/src/server/plugins/engine/outputFormatters/machine/v2.ts +++ b/src/server/plugins/engine/outputFormatters/machine/v2.ts @@ -15,6 +15,7 @@ import { type DetailItemRepeat } from '~/src/server/plugins/engine/models/types.js' import { + type FormAdapterFile, type FormContext, type FormPayload, type FormValue @@ -130,7 +131,9 @@ function extractRepeaters(item: DetailItemRepeat) { * @param item - the file upload item in the form * @returns the file upload data */ -function extractFileUploads(item: FileUploadFieldDetailitem) { +function extractFileUploads( + item: FileUploadFieldDetailitem +): FormAdapterFile[] { const fileUploadState = item.field.getFormValueFromState(item.state) ?? [] return fileUploadState.map((fileState) => { diff --git a/src/server/plugins/engine/types.ts b/src/server/plugins/engine/types.ts index dc381cd86..8e5aaa68f 100644 --- a/src/server/plugins/engine/types.ts +++ b/src/server/plugins/engine/types.ts @@ -405,16 +405,29 @@ export type FormAdapterSubmissionMessageMetaSerialised = Omit< status: string timestamp: string } +export interface FormAdapterFile { + fileName: string + fileId: string + userDownloadLink: string +} + +export interface FormAdapterSubmissionMessageResult { + files: { + main: string + repeaters: Record + } +} export interface FormAdapterSubmissionMessageData { main: Record repeaters: Record[]> - files: Record[]> + files: Record } export interface FormAdapterSubmissionMessagePayload { meta: FormAdapterSubmissionMessageMeta data: FormAdapterSubmissionMessageData + result: FormAdapterSubmissionMessageResult } export interface FormAdapterSubmissionMessage diff --git a/src/server/plugins/engine/types/schema.test.ts b/src/server/plugins/engine/types/schema.test.ts index b5f2e8a73..12a21fbba 100644 --- a/src/server/plugins/engine/types/schema.test.ts +++ b/src/server/plugins/engine/types/schema.test.ts @@ -1,5 +1,6 @@ import { FormStatus } from '@defra/forms-model' +import { type RichFormValue } from '~/src/server/plugins/engine/outputFormatters/machine/v2.js' import { FormAdapterSubmissionSchemaVersion } from '~/src/server/plugins/engine/types/enums.js' import { formAdapterSubmissionMessageDataSchema, @@ -13,6 +14,51 @@ import { } from '~/src/server/plugins/engine/types.js' describe('Schema validation', () => { + const main: Record = { + QMwMir: 'Roman Pizza', + duOEvZ: 'Small', + DzEODf: ['Mozzarella'], + juiCfC: ['Pepperoni', 'Sausage', 'Onions', 'Basil'], + YEpypP: 'None', + JumNVc: 'Joe Bloggs', + ALNehP: '+441234567890', + vAqTmg: { + addressLine1: '1 Anywhere Street', + town: 'Anywhereville', + postcode: 'AN1 2WH' + }, + IbXVGY: { + day: 22, + month: 8, + year: 2025 + }, + HGBWLt: ['Garlic sauce'] + } + + const value1: Record = { + IEKzko: 'dsfsdfsdf' + } + const value2: Record = { + IEKzko: 'dfghfgh' + } + + const validData: FormAdapterSubmissionMessageData = { + main, + repeaters: { + qLVLgb: [value1, value2] + }, + files: { + dLzALM: [ + { + fileId: '489ecc1b-a145-4618-ba5a-b4a0d5ee2dbd', + fileName: 'file-name.json', + userDownloadLink: + 'http://localhost:3005/file-download/489ecc1b-a145-4618-ba5a-b4a0d5ee2dbd' + } + ] + } + } + describe('formAdapterSubmissionMessageMetaSchema', () => { const validMeta: FormAdapterSubmissionMessageMeta = { schemaVersion: FormAdapterSubmissionSchemaVersion.V1, @@ -49,31 +95,6 @@ describe('Schema validation', () => { }) describe('formAdapterSubmissionMessageDataSchema', () => { - const validData: FormAdapterSubmissionMessageData = { - main: { - QMwMir: 'Roman Pizza', - duOEvZ: 'Small', - DzEODf: ['Mozzarella'], - juiCfC: ['Pepperoni', 'Sausage', 'Onions', 'Basil'], - YEpypP: 'None', - JumNVc: 'Joe Bloggs', - ALNehP: '+441234567890', - vAqTmg: { - addressLine1: '1 Anywhere Street', - town: 'Anywhereville', - postcode: 'AN1 2WH' - }, - IbXVGY: { - day: 22, - month: 8, - year: 2025 - }, - HGBWLt: ['Garlic sauce'] - }, - repeaters: {}, - files: {} - } - it('should validate valid data object', () => { const { error } = formAdapterSubmissionMessageDataSchema.validate(validData) @@ -89,41 +110,28 @@ describe('Schema validation', () => { }) describe('formAdapterSubmissionMessagePayloadSchema', () => { + const meta: FormAdapterSubmissionMessageMeta = { + schemaVersion: FormAdapterSubmissionSchemaVersion.V1, + timestamp: new Date('2025-08-22T18:15:10.785Z'), + referenceNumber: '576-225-943', + formName: 'Order a pizza', + formId: '68a8b0449ab460290c28940a', + formSlug: 'order-a-pizza', + status: FormStatus.Live, + isPreview: false, + notificationEmail: 'info@example.com' + } + const validPayload: FormAdapterSubmissionMessagePayload = { - meta: { - schemaVersion: FormAdapterSubmissionSchemaVersion.V1, - timestamp: new Date('2025-08-22T18:15:10.785Z'), - referenceNumber: '576-225-943', - formName: 'Order a pizza', - formId: '68a8b0449ab460290c28940a', - formSlug: 'order-a-pizza', - status: FormStatus.Live, - isPreview: false, - notificationEmail: 'info@example.com' - }, - data: { - main: { - QMwMir: 'Roman Pizza', - duOEvZ: 'Small', - DzEODf: ['Mozzarella'], - juiCfC: ['Pepperoni', 'Sausage', 'Onions', 'Basil'], - YEpypP: 'None', - JumNVc: 'Joe Bloggs', - ALNehP: '+441234567890', - vAqTmg: { - addressLine1: '1 Anywhere Street', - town: 'Anywhereville', - postcode: 'AN1 2WH' - }, - IbXVGY: { - day: 22, - month: 8, - year: 2025 - }, - HGBWLt: ['Garlic sauce'] - }, - repeaters: {}, - files: {} + meta, + data: validData, + result: { + files: { + main: '3d289230-83a3-4852-a68a-cb3569e9b0fe', + repeaters: { + ImxIOP: 'Joe Bloggs' + } + } } } diff --git a/src/server/plugins/engine/types/schema.ts b/src/server/plugins/engine/types/schema.ts index 039ce2082..57331bd1e 100644 --- a/src/server/plugins/engine/types/schema.ts +++ b/src/server/plugins/engine/types/schema.ts @@ -11,7 +11,8 @@ import { FormAdapterSubmissionSchemaVersion } from '~/src/server/plugins/engine/ import { type FormAdapterSubmissionMessageData, type FormAdapterSubmissionMessageMeta, - type FormAdapterSubmissionMessagePayload + type FormAdapterSubmissionMessagePayload, + type FormAdapterSubmissionMessageResult } from '~/src/server/plugins/engine/types.js' export const formAdapterSubmissionMessageMetaSchema = @@ -35,11 +36,31 @@ export const formAdapterSubmissionMessageDataSchema = Joi.object().keys({ main: Joi.object(), repeaters: Joi.object(), + files: Joi.object().pattern( + Joi.string(), + Joi.array().items( + Joi.object().keys({ + fileName: Joi.string().required(), + fileId: Joi.string().required(), + userDownloadLink: Joi.string().required() + }) + ) + ) + }) + +export const formAdapterSubmissionMessageResultSchema = + Joi.object().keys({ files: Joi.object() + .keys({ + main: Joi.string().required(), + repeaters: Joi.object() + }) + .required() }) export const formAdapterSubmissionMessagePayloadSchema = Joi.object().keys({ meta: formAdapterSubmissionMessageMetaSchema.required(), - data: formAdapterSubmissionMessageDataSchema.required() + data: formAdapterSubmissionMessageDataSchema.required(), + result: formAdapterSubmissionMessageResultSchema.required() }) From f62086dfa3a6f9b20b13681e263bcb6bc1241ff7 Mon Sep 17 00:00:00 2001 From: whitewater design Date: Tue, 2 Sep 2025 09:17:12 +0100 Subject: [PATCH 6/6] feat: DF-380 - Resolve circular dependency --- .../engine/outputFormatters/machine/v2.ts | 23 ++----------------- src/server/plugins/engine/types.ts | 21 +++++++++++++++-- src/server/plugins/engine/types/index.ts | 2 -- .../plugins/engine/types/schema.test.ts | 4 ++-- 4 files changed, 23 insertions(+), 27 deletions(-) diff --git a/src/server/plugins/engine/outputFormatters/machine/v2.ts b/src/server/plugins/engine/outputFormatters/machine/v2.ts index db203e758..215fb33d0 100644 --- a/src/server/plugins/engine/outputFormatters/machine/v2.ts +++ b/src/server/plugins/engine/outputFormatters/machine/v2.ts @@ -1,12 +1,7 @@ import { type SubmitResponsePayload } from '@defra/forms-model' import { config } from '~/src/config/index.js' -import { type UkAddressState } from '~/src/server/plugins/engine/components/UkAddressField.js' import { FileUploadField } from '~/src/server/plugins/engine/components/index.js' -import { - type DatePartsState, - type MonthYearState -} from '~/src/server/plugins/engine/components/types.js' import { type checkFormStatus } from '~/src/server/plugins/engine/helpers.js' import { type FormModel } from '~/src/server/plugins/engine/models/index.js' import { @@ -15,10 +10,10 @@ import { type DetailItemRepeat } from '~/src/server/plugins/engine/models/types.js' import { + type FileUploadFieldDetailitem, type FormAdapterFile, type FormContext, - type FormPayload, - type FormValue + type RichFormValue } from '~/src/server/plugins/engine/types.js' const designerUrl = config.get('designerUrl') @@ -151,17 +146,3 @@ function isFileUploadFieldItem( ): item is FileUploadFieldDetailitem { return item.field instanceof FileUploadField } - -/** - * A detail item specifically for files - */ -type FileUploadFieldDetailitem = Omit & { - field: FileUploadField -} - -export type RichFormValue = - | FormValue - | FormPayload - | DatePartsState - | MonthYearState - | UkAddressState diff --git a/src/server/plugins/engine/types.ts b/src/server/plugins/engine/types.ts index 8e5aaa68f..91b98676a 100644 --- a/src/server/plugins/engine/types.ts +++ b/src/server/plugins/engine/types.ts @@ -11,14 +11,18 @@ import { type PluginProperties, type Request } from '@hapi/hapi' import { type JoiExpression, type ValidationErrorItem } from 'joi' import { FormComponent } from '~/src/server/plugins/engine/components/FormComponent.js' +import { type UkAddressState } from '~/src/server/plugins/engine/components/UkAddressField.js' import { type Component } from '~/src/server/plugins/engine/components/helpers/components.js' +import { type FileUploadField } from '~/src/server/plugins/engine/components/index.js' import { type BackLink, type ComponentText, - type ComponentViewModel + type ComponentViewModel, + type DatePartsState, + type MonthYearState } from '~/src/server/plugins/engine/components/types.js' import { type FormModel } from '~/src/server/plugins/engine/models/index.js' -import { type RichFormValue } from '~/src/server/plugins/engine/outputFormatters/machine/v2.js' +import { type DetailItemField } from '~/src/server/plugins/engine/models/types.js' import { type PageController } from '~/src/server/plugins/engine/pageControllers/PageController.js' import { type PageControllerClass } from '~/src/server/plugins/engine/pageControllers/helpers/pages.js' import { @@ -418,6 +422,19 @@ export interface FormAdapterSubmissionMessageResult { } } +/** + * A detail item specifically for files + */ +export type FileUploadFieldDetailitem = Omit & { + field: FileUploadField +} +export type RichFormValue = + | FormValue + | FormPayload + | DatePartsState + | MonthYearState + | UkAddressState + export interface FormAdapterSubmissionMessageData { main: Record repeaters: Record[]> diff --git a/src/server/plugins/engine/types/index.ts b/src/server/plugins/engine/types/index.ts index 917f045db..e7e7e00f1 100644 --- a/src/server/plugins/engine/types/index.ts +++ b/src/server/plugins/engine/types/index.ts @@ -87,7 +87,5 @@ export type { Services } from '~/src/server/types.js' -export type { RichFormValue } from '~/src/server/plugins/engine/outputFormatters/machine/v2.js' - export * from '~/src/server/plugins/engine/types/schema.js' export { FormAdapterSubmissionSchemaVersion } from '~/src/server/plugins/engine/types/enums.js' diff --git a/src/server/plugins/engine/types/schema.test.ts b/src/server/plugins/engine/types/schema.test.ts index 12a21fbba..4bbd324b1 100644 --- a/src/server/plugins/engine/types/schema.test.ts +++ b/src/server/plugins/engine/types/schema.test.ts @@ -1,6 +1,5 @@ import { FormStatus } from '@defra/forms-model' -import { type RichFormValue } from '~/src/server/plugins/engine/outputFormatters/machine/v2.js' import { FormAdapterSubmissionSchemaVersion } from '~/src/server/plugins/engine/types/enums.js' import { formAdapterSubmissionMessageDataSchema, @@ -10,7 +9,8 @@ import { import { type FormAdapterSubmissionMessageData, type FormAdapterSubmissionMessageMeta, - type FormAdapterSubmissionMessagePayload + type FormAdapterSubmissionMessagePayload, + type RichFormValue } from '~/src/server/plugins/engine/types.js' describe('Schema validation', () => {