From 7eaaadc782095b6c853b00958055d9987c16dfb9 Mon Sep 17 00:00:00 2001 From: Aaron Scully Date: Wed, 22 Apr 2026 14:33:31 +0100 Subject: [PATCH 1/2] Linting fixes --- package-lock.json | 39 +++--- package.json | 2 +- src/service/events.test.js | 3 +- src/service/mappers/formatters/human/v1.js | 118 ++++++++++-------- .../mappers/formatters/human/v2-common.js | 39 +++--- .../mappers/formatters/human/v2-repeater.js | 14 +-- src/service/mappers/formatters/human/v2.js | 80 ++++++++---- src/service/mappers/formatters/machine/v1.js | 17 ++- src/service/mappers/formatters/shared.js | 13 +- src/service/mappers/formatters/user/v1.js | 95 +++++++------- .../mappers/formatters/user/v1.test.js | 1 + src/service/mappers/submission.js | 4 +- src/service/mappers/submission.test.js | 3 +- 13 files changed, 244 insertions(+), 184 deletions(-) diff --git a/package-lock.json b/package-lock.json index 45f3933..0806cdf 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,7 +11,7 @@ "license": "OGL-UK-3.0", "dependencies": { "@aws-sdk/client-sqs": "3.982.0", - "@defra/forms-engine-plugin": "4.5.5", + "@defra/forms-engine-plugin": "4.7.3", "@defra/forms-model": "3.0.638", "@defra/hapi-tracing": "^1.28.0", "@elastic/ecs-pino-format": "^1.5.0", @@ -3744,13 +3744,13 @@ "license": "MIT" }, "node_modules/@defra/forms-engine-plugin": { - "version": "4.5.5", - "resolved": "https://registry.npmjs.org/@defra/forms-engine-plugin/-/forms-engine-plugin-4.5.5.tgz", - "integrity": "sha512-+FpU2C654NUz1uVgGKB9iLSeOLt3bXsMKM678csQysO2EUB7j655qYh1J5q/bwdhiA4TeEZpxJ3dJ+rDKdk7vg==", + "version": "4.7.3", + "resolved": "https://registry.npmjs.org/@defra/forms-engine-plugin/-/forms-engine-plugin-4.7.3.tgz", + "integrity": "sha512-n77/Y5iLEqUEgGlRgcBitGB4c5cQeiuSG0Q+ulULsLCtYv9QO58eHwVxxM8waiPPkFHa2xffUvWCoxv4cOB/9Q==", "hasInstallScript": true, "license": "SEE LICENSE IN LICENSE", "dependencies": { - "@defra/forms-model": "^3.0.637", + "@defra/forms-model": "^3.0.647", "@defra/hapi-tracing": "^1.29.0", "@defra/interactive-map": "^0.0.17-alpha", "@elastic/ecs-pino-format": "^1.5.0", @@ -3806,6 +3806,26 @@ "npm": ">=10.9.0 <11.6.4" } }, + "node_modules/@defra/forms-engine-plugin/node_modules/@defra/forms-model": { + "version": "3.0.648", + "resolved": "https://registry.npmjs.org/@defra/forms-model/-/forms-model-3.0.648.tgz", + "integrity": "sha512-hbQGF09vFI8Izpal2LiWIHNQbUJ4eqkQYcIcnEWDf8UpkB7qxB3LW2BQKGStjIYw4eiUz5Qus3uSG7R2skyZqA==", + "license": "OGL-UK-3.0", + "dependencies": { + "@joi/date": "^2.1.1", + "marked": "^15.0.12", + "nanoid": "^5.0.7", + "slug": "^11.0.0", + "uuid": "^11.1.0" + }, + "engines": { + "node": "^22.12.0", + "npm": ">=10.9.0 <11.6.4" + }, + "peerDependencies": { + "joi": "^17.0.0" + } + }, "node_modules/@defra/forms-model": { "version": "3.0.638", "resolved": "https://registry.npmjs.org/@defra/forms-model/-/forms-model-3.0.638.tgz", @@ -27557,15 +27577,6 @@ "integrity": "sha512-pcaShQc1Shq0y+E7GqJqvZj8DTthWV1KeHGdi0Z6IAin2Oi3JnLCOfwnCo84qc+HAp52wT9nK9H7FAJp5a44GQ==", "license": "ISC" }, - "node_modules/preact": { - "version": "8.5.3", - "resolved": "https://registry.npmjs.org/preact/-/preact-8.5.3.tgz", - "integrity": "sha512-O3kKP+1YdgqHOFsZF2a9JVdtqD+RPzCQc3rP+Ualf7V6rmRDchZ9MJbiGTT7LuyqFKZqlHSOyO/oMFmI2lVTsw==", - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "peer": true - }, "node_modules/prelude-ls": { "version": "1.2.1", "dev": true, diff --git a/package.json b/package.json index 36416b8..1e936ef 100644 --- a/package.json +++ b/package.json @@ -31,7 +31,7 @@ }, "dependencies": { "@aws-sdk/client-sqs": "3.982.0", - "@defra/forms-engine-plugin": "4.5.5", + "@defra/forms-engine-plugin": "4.7.3", "@defra/forms-model": "3.0.638", "@defra/hapi-tracing": "^1.28.0", "@elastic/ecs-pino-format": "^1.5.0", diff --git a/src/service/events.test.js b/src/service/events.test.js index 29c238b..8dea7a4 100644 --- a/src/service/events.test.js +++ b/src/service/events.test.js @@ -1,4 +1,5 @@ import { FormAdapterSubmissionSchemaVersion } from '@defra/forms-engine-plugin/engine/types/enums.js' +import { FormStatus } from '@defra/forms-model' import { ValidationError } from 'joi' import { deleteEventMessage } from '~/src/messaging/event.js' @@ -40,7 +41,7 @@ describe('events', () => { formName: 'Order a pizza', formId: '68a8b0449ab460290c28940a', formSlug: 'order-a-pizza', - status: 'live', + status: FormStatus.Live, isPreview: false, notificationEmail: 'info@example.com' } diff --git a/src/service/mappers/formatters/human/v1.js b/src/service/mappers/formatters/human/v1.js index 8fa4655..68124c2 100644 --- a/src/service/mappers/formatters/human/v1.js +++ b/src/service/mappers/formatters/human/v1.js @@ -85,13 +85,21 @@ function processMainEntries(formSubmissionMessage, formModel, componentMap) { const questionLines = /** @type {string[]} */ ([]) const field = formModel.componentMap.get(key) + if (!(field instanceof FormComponent)) { + continue + } + let mappedRichFormValue = richFormValue - if (field instanceof FileUploadField) { - mappedRichFormValue = richFormValue.map(mapFormAdapterFileToFileState) + if (field instanceof FileUploadField && richFormValue !== null) { + mappedRichFormValue = /** @type {FormAdapterFile[]} */ ( + /** @type {unknown} */ (richFormValue) + ).map(mapFormAdapterFileToFileState) } - const answer = field.getDisplayStringFromFormValue(mappedRichFormValue) + const answer = field.getDisplayStringFromFormValue( + /** @type {any} */ (mappedRichFormValue) + ) const label = escapeContent(field.title) questionLines.push(`## ${label}\n`) @@ -100,7 +108,7 @@ function processMainEntries(formSubmissionMessage, formModel, componentMap) { const answerLine = generateFieldLine( answer, field, - richFormValue, + /** @type {RichFormValue} */ (/** @type {unknown} */ (richFormValue)), formSubmissionMessage ) questionLines.push(answerLine) @@ -203,7 +211,11 @@ export function formatter( const { isPreview, status } = meta const files = result.files - const formModel = new FormModel(formDefinition, { basePath: '' }, {}) + const formModel = new FormModel( + formDefinition, + { basePath: '' }, + /** @type {any} */ ({}) + ) const formName = escapeContent(meta.formName) /** @@ -264,7 +276,9 @@ export function formatter( * @returns {string} */ function formatFileUploadField(answer, _field, richFormValue) { - const formAdapterFiles = /** @type {FormAdapterFile[]} */ (richFormValue) + const formAdapterFiles = /** @type {FormAdapterFile[]} */ ( + /** @type {unknown} */ (richFormValue) + ) // Skip empty files if (!formAdapterFiles.length) { @@ -287,7 +301,7 @@ function formatFileUploadField(answer, _field, richFormValue) { /** * Format list form component field * @param {string} answer - * @param {Component} field + * @param {ListFormComponent} field * @param {RichFormValue} richFormValue * @returns {string} */ @@ -368,27 +382,15 @@ function formatGeospatialField( /** * Map of component types to their formatting handlers * Using Map to preserve class constructor references + * @type {Map Component, (answer: string, field: Component, richFormValue: RichFormValue, formSubmissionMessage: FormAdapterSubmissionMessage) => string>} */ -const fieldHandlers = new Map([ - [Components.FileUploadField, formatFileUploadField], - [Components.MultilineTextField, formatMultilineTextField], - [Components.UkAddressField, formatUkAddressField], - [Components.EastingNorthingField, formatLocationField], - [Components.LatLongField, formatLocationField], - [Components.GeospatialField, formatGeospatialField] -]) - -/** - * Check if field is a list component and return appropriate handler - * @param {Component} field - * @returns {((answer: string, field: Component, richFormValue: RichFormValue) => string) | null} - */ -function getListComponentHandler(field) { - if (field instanceof ListFormComponent && field instanceof FormComponent) { - return formatListFormComponent - } - return null -} +const fieldHandlers = new Map() +fieldHandlers.set(Components.FileUploadField, formatFileUploadField) +fieldHandlers.set(Components.MultilineTextField, formatMultilineTextField) +fieldHandlers.set(Components.UkAddressField, formatUkAddressField) +fieldHandlers.set(Components.EastingNorthingField, formatLocationField) +fieldHandlers.set(Components.LatLongField, formatLocationField) +fieldHandlers.set(Components.GeospatialField, formatGeospatialField) /** * @@ -405,9 +407,8 @@ function generateFieldLine( formSubmissionMessage ) { // Check list component first (special case with multiple inheriance) - const listHandler = getListComponentHandler(field) - if (listHandler) { - return listHandler(answer, field, richFormValue) + if (field instanceof ListFormComponent && field instanceof FormComponent) { + return formatListFormComponent(answer, field, richFormValue) } // Iterate through registered handlers @@ -446,7 +447,7 @@ function calculateOrder(formDefinition, formSubmissionMessage) { /** * * @param {Record} subfieldObject - * @param {[string, FormValue|null]} entry + * @param {[string, RichFormValue|null]} entry * @returns {Record} */ function handleSubfields(subfieldObject, [key, value]) { @@ -509,14 +510,21 @@ function mapFormAdapterFileToFileState(file) { */ export function mapValueToState(formSubmissionMessage) { const mainEntries = Object.entries(formSubmissionMessage.data.main) - const main = mainEntries.reduce(handleSubfields, {}) + const main = mainEntries.reduce( + handleSubfields, + /** @type {Record} */ ({}) + ) const repeaterEntries = Object.entries(formSubmissionMessage.data.repeaters) const repeaters = repeaterEntries.reduce((repeaterObject, [key, value]) => { const values = value.map((repeater, idx) => { const idxStr = `${idx}` + const subfields = Object.entries(repeater).reduce( + handleSubfields, + /** @type {Record} */ ({}) + ) return { - ...Object.entries(repeater).reduce(handleSubfields, {}), + ...subfields, itemId: `a581accd-e989-4500-87da-f3929c192dba`.slice(0, 0 - idxStr.length) + idxStr @@ -559,14 +567,15 @@ export function getRelevantPagesForLegacy( const state = mapValueToState(formSubmissionMessage) const context = model.getFormContext( - { + /** @type {FormContextRequest} */ ({ query: { - force: true + force: 'true' }, params: { - path: 'summary' + path: 'summary', + slug: '' } - }, + }), state ) @@ -575,25 +584,30 @@ export function getRelevantPagesForLegacy( relevantPages ) - return typedRelevantPages.reduce((order, page) => { - const { collection } = page - - if (page instanceof RepeatPageController) { - return [...order, page.repeat.options.name] - } else { - return [ - ...order, - ...collection.fields.map( - /** @type {(f: Component) => string} */ ((f) => f.name) - ) - ] - } - }, []) + return typedRelevantPages.reduce( + /** @type {(order: string[], page: PageControllerClass) => string[]} */ ( + (order, page) => { + const { collection } = page + + if (page instanceof RepeatPageController) { + return [...order, page.repeat.options.name] + } else { + return [ + ...order, + ...collection.fields.map( + /** @type {(f: Component) => string} */ ((f) => f.name) + ) + ] + } + } + ), + /** @type {string[]} */ ([]) + ) } /** * @import { Component } from '@defra/forms-engine-plugin/engine/components/helpers/components.js' * @import { PageControllerClass } from '@defra/forms-engine-plugin/engine/pageControllers/helpers/pages.js' - * @import { FormAdapterSubmissionMessage, FormAdapterFile, RichFormValue, FormValue, FormStateValue, FileState, UploadStatusFileResponse } from '@defra/forms-engine-plugin/engine/types.js' + * @import { FormAdapterSubmissionMessage, FormAdapterFile, RichFormValue, FormStateValue, FileState, FormContextRequest, UploadStatusFileResponse } from '@defra/forms-engine-plugin/engine/types.js' * @import { FormDefinition } from '@defra/forms-model' */ diff --git a/src/service/mappers/formatters/human/v2-common.js b/src/service/mappers/formatters/human/v2-common.js index 4b60116..8437884 100644 --- a/src/service/mappers/formatters/human/v2-common.js +++ b/src/service/mappers/formatters/human/v2-common.js @@ -17,15 +17,15 @@ const designerUrl = config.get('designerUrl') /** * Map of component types to their formatting handlers * Using Map to preserve class constructor references + * @type {Map Component, (answer: string, field: Component, richFormValue: RichFormValue, formSubmissionMessage: FormAdapterSubmissionMessage) => string>} */ -const fieldHandlers = new Map([ - [Components.FileUploadField, formatFileUploadField], - [Components.MultilineTextField, formatMultilineTextField], - [Components.UkAddressField, formatUkAddressField], - [Components.EastingNorthingField, formatLocationField], - [Components.LatLongField, formatLocationField], - [Components.GeospatialField, formatGeospatialField] -]) +const fieldHandlers = new Map() +fieldHandlers.set(Components.FileUploadField, formatFileUploadField) +fieldHandlers.set(Components.MultilineTextField, formatMultilineTextField) +fieldHandlers.set(Components.UkAddressField, formatUkAddressField) +fieldHandlers.set(Components.EastingNorthingField, formatLocationField) +fieldHandlers.set(Components.LatLongField, formatLocationField) +fieldHandlers.set(Components.GeospatialField, formatGeospatialField) /** * @@ -42,9 +42,8 @@ export function generateFieldLine( formSubmissionMessage ) { // Check list component first (special case with multiple inheriance) - const listHandler = getListComponentHandler(field) - if (listHandler) { - return listHandler(answer, field, richFormValue) + if (field instanceof ListFormComponent && field instanceof FormComponent) { + return formatListFormComponent(answer, field, richFormValue) } // Iterate through registered handlers @@ -58,22 +57,10 @@ export function generateFieldLine( return `${escapeContent(answer)}\n` } -/** - * Check if field is a list component and return appropriate handler - * @param {Component} field - * @returns {((answer: string, field: Component, richFormValue: RichFormValue) => string) | null} - */ -function getListComponentHandler(field) { - if (field instanceof ListFormComponent && field instanceof FormComponent) { - return formatListFormComponent - } - return null -} - /** * Format list form component field * @param {string} answer - * @param {Component} field + * @param {ListFormComponent} field * @param {RichFormValue} richFormValue * @returns {string} */ @@ -154,7 +141,9 @@ function formatGeospatialField( * @returns {string} */ function formatFileUploadField(answer, _field, richFormValue) { - const formAdapterFiles = /** @type {FormAdapterFile[]} */ (richFormValue) + const formAdapterFiles = /** @type {FormAdapterFile[]} */ ( + /** @type {unknown} */ (richFormValue) + ) // Skip empty files if (!formAdapterFiles.length) { diff --git a/src/service/mappers/formatters/human/v2-repeater.js b/src/service/mappers/formatters/human/v2-repeater.js index 0b9e366..55ea719 100644 --- a/src/service/mappers/formatters/human/v2-repeater.js +++ b/src/service/mappers/formatters/human/v2-repeater.js @@ -99,17 +99,14 @@ function processRepeaterComponent( const componentValue = itemData[componentName] // Skip if no value - if ( - componentValue === null || - componentValue === undefined || - componentValue === '' - ) { + if (componentValue === undefined || componentValue === '') { continue } const itemLabel = `${repeaterTitle} ${i + 1}` + const formField = /** @type {FormComponent} */ (componentField) const componentAnswer = - componentField.getDisplayStringFromFormValue(componentValue) + formField.getDisplayStringFromFormValue(componentValue) // Repeater item label uses heading level 2 (##) questionLines.push( @@ -159,9 +156,7 @@ export function processRepeaterEntries( (cd) => 'title' in cd )) { const componentName = componentDef.name - const componentField = /** @type {Component} */ ( - formModel.componentMap.get(componentName) - ) + const componentField = formModel.componentMap.get(componentName) if (!componentField) { continue @@ -183,6 +178,7 @@ export function processRepeaterEntries( /** * @import { Component } from '@defra/forms-engine-plugin/engine/components/helpers/components.js' + * @import { FormComponent } from '@defra/forms-engine-plugin/engine/components/FormComponent.js' * @import { FormAdapterSubmissionMessage, RichFormValue } from '@defra/forms-engine-plugin/engine/types.js' * @import { FormModel } from '@defra/forms-engine-plugin/engine/models/FormModel.js' * @import { FormDefinition, PageRepeat } from '@defra/forms-model' diff --git a/src/service/mappers/formatters/human/v2.js b/src/service/mappers/formatters/human/v2.js index f2b7a8d..409ebdd 100644 --- a/src/service/mappers/formatters/human/v2.js +++ b/src/service/mappers/formatters/human/v2.js @@ -1,5 +1,6 @@ import { RepeatPageController } from '@defra/forms-engine-plugin/controllers/RepeatPageController.js' import { FileUploadField } from '@defra/forms-engine-plugin/engine/components/FileUploadField.js' +import { FormComponent } from '@defra/forms-engine-plugin/engine/components/FormComponent.js' import { FormModel } from '@defra/forms-engine-plugin/engine/models/FormModel.js' import { FileStatus, @@ -74,13 +75,21 @@ function processMainEntries(formSubmissionMessage, formModel, componentMap) { const questionLines = /** @type {string[]} */ ([]) const field = formModel.componentMap.get(key) + if (!(field instanceof FormComponent)) { + continue + } + let mappedRichFormValue = richFormValue - if (field instanceof FileUploadField) { - mappedRichFormValue = richFormValue.map(mapFormAdapterFileToFileState) + if (field instanceof FileUploadField && richFormValue !== null) { + mappedRichFormValue = /** @type {FormAdapterFile[]} */ ( + /** @type {unknown} */ (richFormValue) + ).map(mapFormAdapterFileToFileState) } - const answer = field.getDisplayStringFromFormValue(mappedRichFormValue) + const answer = field.getDisplayStringFromFormValue( + /** @type {any} */ (mappedRichFormValue) + ) const label = escapeContent(field.title) questionLines.push(`## ${label}\n`) @@ -89,7 +98,7 @@ function processMainEntries(formSubmissionMessage, formModel, componentMap) { const answerLine = generateFieldLine( answer, field, - richFormValue, + /** @type {RichFormValue} */ (/** @type {unknown} */ (richFormValue)), formSubmissionMessage ) questionLines.push(answerLine) @@ -131,7 +140,11 @@ export function formatter( const { isPreview, status } = meta const files = result.files - const formModel = new FormModel(formDefinition, { basePath: '' }, {}) + const formModel = new FormModel( + formDefinition, + { basePath: '' }, + /** @type {any} */ ({}) + ) const formName = escapeContent(meta.formName) const now = new Date() @@ -216,7 +229,7 @@ function calculateOrder(formDefinition, formSubmissionMessage) { /** * * @param {Record} subfieldObject - * @param {[string, FormValue|null]} entry + * @param {[string, RichFormValue|null]} entry * @returns {Record} */ function handleSubfields(subfieldObject, [key, value]) { @@ -279,14 +292,21 @@ function mapFormAdapterFileToFileState(file) { */ export function mapValueToState(formSubmissionMessage) { const mainEntries = Object.entries(formSubmissionMessage.data.main) - const main = mainEntries.reduce(handleSubfields, {}) + const main = mainEntries.reduce( + handleSubfields, + /** @type {Record} */ ({}) + ) const repeaterEntries = Object.entries(formSubmissionMessage.data.repeaters) const repeaters = repeaterEntries.reduce((repeaterObject, [key, value]) => { const values = value.map((repeater, idx) => { const idxStr = `${idx}` + const reduced = Object.entries(repeater).reduce( + handleSubfields, + /** @type {Record} */ ({}) + ) return { - ...Object.entries(repeater).reduce(handleSubfields, {}), + ...reduced, itemId: `a581accd-e989-4500-87da-f3929c192dba`.slice(0, 0 - idxStr.length) + idxStr @@ -329,14 +349,15 @@ export function getRelevantPagesForLegacy( const state = mapValueToState(formSubmissionMessage) const context = model.getFormContext( - { + /** @type {FormContextRequest} */ ({ query: { - force: true + force: 'true' }, params: { - path: 'summary' + path: 'summary', + slug: '' } - }, + }), state ) @@ -345,25 +366,30 @@ export function getRelevantPagesForLegacy( relevantPages ) - return typedRelevantPages.reduce((order, page) => { - const { collection } = page - - if (page instanceof RepeatPageController) { - return [...order, page.repeat.options.name] - } else { - return [ - ...order, - ...collection.fields.map( - /** @type {(f: Component) => string} */ ((f) => f.name) - ) - ] - } - }, []) + return typedRelevantPages.reduce( + /** @type {(order: string[], page: PageControllerClass) => string[]} */ ( + (order, page) => { + const { collection } = page + + if (page instanceof RepeatPageController) { + return [...order, page.repeat.options.name] + } else { + return [ + ...order, + ...collection.fields.map( + /** @type {(f: Component) => string} */ ((f) => f.name) + ) + ] + } + } + ), + /** @type {string[]} */ ([]) + ) } /** * @import { Component } from '@defra/forms-engine-plugin/engine/components/helpers/components.js' * @import { PageControllerClass } from '@defra/forms-engine-plugin/engine/pageControllers/helpers/pages.js' - * @import { FormAdapterSubmissionMessage, FormAdapterFile, FormValue, FormStateValue, FileState, UploadStatusFileResponse } from '@defra/forms-engine-plugin/engine/types.js' + * @import { FormAdapterSubmissionMessage, FormAdapterFile, RichFormValue, FormStateValue, FileState, FormContextRequest, UploadStatusFileResponse } from '@defra/forms-engine-plugin/engine/types.js' * @import { FormDefinition } from '@defra/forms-model' */ diff --git a/src/service/mappers/formatters/machine/v1.js b/src/service/mappers/formatters/machine/v1.js index 2d22a7d..aee8057 100644 --- a/src/service/mappers/formatters/machine/v1.js +++ b/src/service/mappers/formatters/machine/v1.js @@ -30,7 +30,7 @@ export function formatter( * @returns {*} */ function formatData(formSubmissionMessage, formDefinition) { - const formModel = new FormModel(formDefinition, { basePath: '' }, {}) + const formModel = new FormModel(formDefinition, { basePath: '' }) const { main: mainInput, repeaters: repeatersInput, @@ -38,17 +38,25 @@ function formatData(formSubmissionMessage, formDefinition) { } = formSubmissionMessage.data /** - * @param {[string,RichFormValue]} entry + * @param {[string,RichFormValue|null]} entry */ function mapField([key, value]) { const component = formModel.componentMap.get(key) - const mappedValue = component.getContextValueFromFormValue(value) + + if (!component) { + return [key, ''] + } + + const formField = /** @type {FormComponent} */ (component) + const mappedValue = formField.getContextValueFromFormValue( + /** @type {RichFormValue} */ (value ?? undefined) + ) return [key, mappedValue?.toString() ?? ''] } /** - * @param {Record} richFormRecord + * @param {Record} richFormRecord */ function mapRecord(richFormRecord) { return Object.fromEntries(Object.entries(richFormRecord).map(mapField)) @@ -71,6 +79,7 @@ function formatData(formSubmissionMessage, formDefinition) { } /** + * @import { FormComponent } from '@defra/forms-engine-plugin/engine/components/FormComponent.js' * @import { FormAdapterSubmissionMessage, RichFormValue } from '@defra/forms-engine-plugin/engine/types.js' * @import { FormDefinition } from '@defra/forms-model' */ diff --git a/src/service/mappers/formatters/shared.js b/src/service/mappers/formatters/shared.js index 8765cdc..338fbd4 100644 --- a/src/service/mappers/formatters/shared.js +++ b/src/service/mappers/formatters/shared.js @@ -39,8 +39,11 @@ export function formatMultilineTextField(answer, _field, _richFormValue) { */ export function formatUkAddressField(_answer, field, richFormValue) { // Format UK addresses into new lines - return (field.getContextValueFromFormValue(richFormValue) ?? []) - .map(escapeContent) + const formField = /** @type {FormComponent} */ (field) + const contextValue = formField.getContextValueFromFormValue(richFormValue) + return [contextValue ?? []] + .flat() + .map((v) => escapeContent(String(v))) .join('\n') .concat('\n') } @@ -53,8 +56,9 @@ export function formatUkAddressField(_answer, field, richFormValue) { * @returns {string} */ export function formatLocationField(_answer, field, richFormValue) { - const contextValue = field.getContextValueFromFormValue(richFormValue) - return contextValue ? `${contextValue}\n` : '' + const formField = /** @type {FormComponent} */ (field) + const contextValue = formField.getContextValueFromFormValue(richFormValue) + return contextValue ? `${String(contextValue)}\n` : '' } /** @@ -149,6 +153,7 @@ export function generateGeospatialMapLink( /** * @import { Component } from '@defra/forms-engine-plugin/engine/components/helpers/components.js' + * @import { FormComponent } from '@defra/forms-engine-plugin/engine/components/FormComponent.js' * @import { FormAdapterSubmissionMessage, GeospatialState, RichFormValue } from '@defra/forms-engine-plugin/engine/types.js' * @import { FormDefinition } from '@defra/forms-model' */ diff --git a/src/service/mappers/formatters/user/v1.js b/src/service/mappers/formatters/user/v1.js index 4c247bd..2c616fa 100644 --- a/src/service/mappers/formatters/user/v1.js +++ b/src/service/mappers/formatters/user/v1.js @@ -18,7 +18,7 @@ import { /** * Check if an optional field should be skipped (no value provided) * @param {Component} field - * @param {RichFormValue} richFormValue + * @param {RichFormValue | null} richFormValue * @returns {boolean} */ function shouldSkipOptionalField(field, richFormValue) { @@ -56,18 +56,25 @@ function processMainEntries(formSubmissionMessage, formModel) { for (const [key, richFormValue] of mainEntries) { const field = formModel.componentMap.get(key) - if (!field) { + if (!(field instanceof FormComponent)) { continue } - if (shouldSkipOptionalField(field, richFormValue)) { + if ( + shouldSkipOptionalField( + field, + /** @type {RichFormValue | null} */ (richFormValue) + ) + ) { continue } - const answer = field.getDisplayStringFromFormValue(richFormValue) + const answer = field.getDisplayStringFromFormValue( + /** @type {any} */ (richFormValue) + ) // Also skip if optional and the display string is empty - if (!field.options?.required && answer === '') { + if (!field.options.required && answer === '') { continue } @@ -78,7 +85,11 @@ function processMainEntries(formSubmissionMessage, formModel) { questionLines.push(`# ${label}\n`) // Generate the answer line(s) - const answerLine = generateFieldLine(answer, field, richFormValue) + const answerLine = generateFieldLine( + answer, + field, + /** @type {RichFormValue} */ (/** @type {unknown} */ (richFormValue)) + ) questionLines.push(answerLine) componentMap.set(key, questionLines) @@ -90,9 +101,9 @@ function processMainEntries(formSubmissionMessage, formModel) { /** * Process a single repeater component across all items * @param {string} repeaterTitle - * @param {Component} componentField + * @param {FormComponent} componentField * @param {string} componentName - * @param {Record[]} repeaterItems + * @param {Record[]} repeaterItems * @returns {string[]} */ function processRepeaterComponent( @@ -122,15 +133,20 @@ function processRepeaterComponent( } const itemLabel = `${repeaterTitle} ${i + 1}` - const componentAnswer = - componentField.getDisplayStringFromFormValue(componentValue) + const componentAnswer = componentField.getDisplayStringFromFormValue( + /** @type {any} */ (componentValue) + ) // Repeater item label uses heading level 2 (##) questionLines.push(`## ${escapeContent(itemLabel)}\n`) // Answer beneath with blank line separation questionLines.push( - generateFieldLine(componentAnswer, componentField, componentValue) + generateFieldLine( + componentAnswer, + /** @type {Component} */ (/** @type {unknown} */ (componentField)), + componentValue + ) ) } @@ -163,9 +179,8 @@ function processRepeaterEntries( } const repeaterTitle = escapeContent(repeaterPage.repeat.options.title) - const repeaterItems = /** @type {Record[]} */ ( - repeaterData - ) + const repeaterItems = + /** @type {Record[]} */ (repeaterData) if (!hasComponents(repeaterPage)) { continue @@ -176,11 +191,9 @@ function processRepeaterEntries( (cd) => 'title' in cd )) { const componentName = componentDef.name - const componentField = /** @type {Component} */ ( - formModel.componentMap.get(componentName) - ) + const componentField = formModel.componentMap.get(componentName) - if (!componentField) { + if (!(componentField instanceof FormComponent)) { continue } @@ -228,7 +241,11 @@ function assembleOutput(order, componentMap) { * @returns {string} */ export function formatter(formSubmissionMessage, formDefinition) { - const formModel = new FormModel(formDefinition, { basePath: '' }, {}) + const formModel = new FormModel( + formDefinition, + { basePath: '' }, + /** @type {any} */ ({}) + ) const order = calculateOrder(formDefinition, formSubmissionMessage) // Process main entries and repeater entries @@ -253,7 +270,9 @@ export function formatter(formSubmissionMessage, formDefinition) { * @returns {string} */ function formatFileUploadField(answer, _field, richFormValue) { - const formAdapterFiles = /** @type {FormAdapterFile[]} */ (richFormValue) + const formAdapterFiles = /** @type {FormAdapterFile[]} */ ( + /** @type {unknown} */ (richFormValue) + ) // Skip empty files if (!formAdapterFiles.length) { @@ -280,7 +299,7 @@ function formatFileUploadField(answer, _field, richFormValue) { * Format list form component field (radio, checkbox, select) * Uses bullet points only for multiple answers, plain text for single answers * @param {string} _answer - * @param {Component} field + * @param {ListFormComponent} field * @param {RichFormValue} richFormValue * @returns {string} */ @@ -315,27 +334,16 @@ function formatListFormComponent(_answer, field, richFormValue) { /** * Map of component types to their formatting handlers + * Using Map to preserve class constructor references + * @type {Map Component, (answer: string, field: Component, richFormValue: RichFormValue) => string>} */ -const fieldHandlers = new Map([ - [Components.FileUploadField, formatFileUploadField], - [Components.MultilineTextField, formatMultilineTextField], - [Components.UkAddressField, formatUkAddressField], - [Components.EastingNorthingField, formatLocationField], - [Components.LatLongField, formatLocationField], - [Components.GeospatialField, formatGeospatialField] -]) - -/** - * Check if field is a list component and return appropriate handler - * @param {Component} field - * @returns {((answer: string, field: Component, richFormValue: RichFormValue) => string) | null} - */ -function getListComponentHandler(field) { - if (field instanceof ListFormComponent && field instanceof FormComponent) { - return formatListFormComponent - } - return null -} +const fieldHandlers = new Map() +fieldHandlers.set(Components.FileUploadField, formatFileUploadField) +fieldHandlers.set(Components.MultilineTextField, formatMultilineTextField) +fieldHandlers.set(Components.UkAddressField, formatUkAddressField) +fieldHandlers.set(Components.EastingNorthingField, formatLocationField) +fieldHandlers.set(Components.LatLongField, formatLocationField) +fieldHandlers.set(Components.GeospatialField, formatGeospatialField) /** * Generate formatted line for a field value @@ -346,9 +354,8 @@ function getListComponentHandler(field) { */ function generateFieldLine(answer, field, richFormValue) { // Check list component first (special case with multiple inheritance) - const listHandler = getListComponentHandler(field) - if (listHandler) { - return listHandler(answer, field, richFormValue) + if (field instanceof ListFormComponent && field instanceof FormComponent) { + return formatListFormComponent(answer, field, richFormValue) } // Iterate through registered handlers diff --git a/src/service/mappers/formatters/user/v1.test.js b/src/service/mappers/formatters/user/v1.test.js index c12056d..2db4ada 100644 --- a/src/service/mappers/formatters/user/v1.test.js +++ b/src/service/mappers/formatters/user/v1.test.js @@ -320,6 +320,7 @@ describe('User answers formatter v1', () => { repeaterComponentDate: { day: 1, month: 1, year: 2000 } }, { + // @ts-expect-error - intentionally testing null handling repeaterComponentName: null, repeaterComponentDate: { day: 1, month: 1, year: 2020 } }, diff --git a/src/service/mappers/submission.js b/src/service/mappers/submission.js index 0c53f37..89e551c 100644 --- a/src/service/mappers/submission.js +++ b/src/service/mappers/submission.js @@ -3,7 +3,7 @@ import Joi from 'joi' /** * @param {Message} message - * @returns {FormAdapterSubmissionMessagePayload} + * @returns {FormAdapterSubmissionMessage} */ export function mapSubmissionEvent(message) { if (!message.MessageId) { @@ -37,5 +37,5 @@ export function mapSubmissionEvent(message) { /** * @import { Message } from '@aws-sdk/client-sqs' - * @import { FormAdapterSubmissionMessagePayload } from '@defra/forms-engine-plugin/engine/types.js' + * @import { FormAdapterSubmissionMessage, FormAdapterSubmissionMessagePayload } from '@defra/forms-engine-plugin/engine/types.js' */ diff --git a/src/service/mappers/submission.test.js b/src/service/mappers/submission.test.js index 8cfb18d..0ec2bdd 100644 --- a/src/service/mappers/submission.test.js +++ b/src/service/mappers/submission.test.js @@ -1,4 +1,5 @@ import { FormAdapterSubmissionSchemaVersion } from '@defra/forms-engine-plugin/engine/types/enums.js' +import { FormStatus } from '@defra/forms-model' import { ValidationError } from 'joi' import { @@ -16,7 +17,7 @@ describe('events', () => { formName: 'Order a pizza', formId: '68a8b0449ab460290c28940a', formSlug: 'order-a-pizza', - status: 'live', + status: FormStatus.Live, isPreview: false, notificationEmail: 'info@example.com' } From 591f9d6419912f41d7a1da76beba7ac5a35191cf Mon Sep 17 00:00:00 2001 From: Aaron Scully Date: Wed, 22 Apr 2026 16:07:14 +0100 Subject: [PATCH 2/2] Appease Sonar --- src/service/mappers/formatters/human/v1.js | 4 ++-- src/service/mappers/formatters/human/v2.js | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/service/mappers/formatters/human/v1.js b/src/service/mappers/formatters/human/v1.js index 68124c2..35e1546 100644 --- a/src/service/mappers/formatters/human/v1.js +++ b/src/service/mappers/formatters/human/v1.js @@ -511,7 +511,7 @@ function mapFormAdapterFileToFileState(file) { export function mapValueToState(formSubmissionMessage) { const mainEntries = Object.entries(formSubmissionMessage.data.main) const main = mainEntries.reduce( - handleSubfields, + (acc, entry) => handleSubfields(acc, entry), /** @type {Record} */ ({}) ) @@ -520,7 +520,7 @@ export function mapValueToState(formSubmissionMessage) { const values = value.map((repeater, idx) => { const idxStr = `${idx}` const subfields = Object.entries(repeater).reduce( - handleSubfields, + (acc, entry) => handleSubfields(acc, entry), /** @type {Record} */ ({}) ) return { diff --git a/src/service/mappers/formatters/human/v2.js b/src/service/mappers/formatters/human/v2.js index 409ebdd..a7a0b3a 100644 --- a/src/service/mappers/formatters/human/v2.js +++ b/src/service/mappers/formatters/human/v2.js @@ -293,7 +293,7 @@ function mapFormAdapterFileToFileState(file) { export function mapValueToState(formSubmissionMessage) { const mainEntries = Object.entries(formSubmissionMessage.data.main) const main = mainEntries.reduce( - handleSubfields, + (acc, entry) => handleSubfields(acc, entry), /** @type {Record} */ ({}) ) @@ -302,7 +302,7 @@ export function mapValueToState(formSubmissionMessage) { const values = value.map((repeater, idx) => { const idxStr = `${idx}` const reduced = Object.entries(repeater).reduce( - handleSubfields, + (acc, entry) => handleSubfields(acc, entry), /** @type {Record} */ ({}) ) return {