Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 25 additions & 14 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
3 changes: 2 additions & 1 deletion src/service/events.test.js
Original file line number Diff line number Diff line change
@@ -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'
Expand Down Expand Up @@ -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'
}
Expand Down
118 changes: 66 additions & 52 deletions src/service/mappers/formatters/human/v1.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Copy link
Copy Markdown
Contributor

@alexluckett alexluckett Apr 29, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why do we need to cast this to any?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Without it, we end up with an error:

Argument of type 'RichFormValue | FormAdapterFile[] | null' is not assignable to parameter of type '(((((((((((string | number | boolean | (string | number | boolean)[]) & (string | number | boolean)[]) & DatePartsState) & (string | number | boolean | (string | ... 1 more ... | boolean)[] | UploadState | RepeatListState | FeatureCollection | FormPayload)) & (string | ... 6 more ... | FormPayload)) & FileState[]) &...'.
  Type 'null' is not assignable to type '(((((((((((string | number | boolean | (string | number | boolean)[]) & (string | number | boolean)[]) & DatePartsState) & (string | number | boolean | (string | ... 1 more ... | boolean)[] | UploadState | RepeatListState | FeatureCollection | FormPayload)) & (string | ... 6 more ... | FormPayload)) & FileState[]) &...'.

It doesn't match any of the types in the FormComponent.d.ts types, so the FEP would require a further update to make this without it.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've created a new ticket to address further issues here: https://eaflood.atlassian.net/browse/DF-1028

We should prevent any type when linting after we fix the remaining issues.

)

const label = escapeContent(field.title)
questionLines.push(`## ${label}\n`)
Expand All @@ -100,7 +108,7 @@ function processMainEntries(formSubmissionMessage, formModel, componentMap) {
const answerLine = generateFieldLine(
answer,
field,
richFormValue,
/** @type {RichFormValue} */ (/** @type {unknown} */ (richFormValue)),
Copy link
Copy Markdown
Contributor

@alexluckett alexluckett Apr 29, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

type any on L101, as unkown as RichFormValue here?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's because it can be any of these: RichFormValue | FormAdapterFile[] | null, and the function only accepts the first one.

formSubmissionMessage
)
questionLines.push(answerLine)
Expand Down Expand Up @@ -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} */ ({})
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can this type be removed or a proper type used? the previous argument doesn't have an explicit type.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, because it doesn't meet the contract of the required Services type and omitting it could change the behaviour because there's a default parameter value. So to properly fix it may require a change in behaviour.

)

const formName = escapeContent(meta.formName)
/**
Expand Down Expand Up @@ -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)
)
Comment on lines +279 to +281
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

better to use a type guard here rather than as unknown as FormAdapterFile[]?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The ideal solution would be to have the function accept the correct type in the first place. But, because of the way it's used in this file, it shouldn't be necessary (ie it's called when some other piece of data indicates that this is the type). It's a question of how many changes do we want to make for this?


// Skip empty files
if (!formAdapterFiles.length) {
Expand All @@ -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}
*/
Expand Down Expand Up @@ -368,27 +382,15 @@ function formatGeospatialField(
/**
* Map of component types to their formatting handlers
* Using Map to preserve class constructor references
* @type {Map<new (...args: any[]) => 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)

/**
*
Expand All @@ -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
Expand Down Expand Up @@ -446,7 +447,7 @@ function calculateOrder(formDefinition, formSubmissionMessage) {
/**
*
* @param {Record<string, FormStateValue>} subfieldObject
* @param {[string, FormValue|null]} entry
* @param {[string, RichFormValue|null]} entry
* @returns {Record<string, FormStateValue>}
*/
function handleSubfields(subfieldObject, [key, value]) {
Expand Down Expand Up @@ -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(
(acc, entry) => handleSubfields(acc, entry),
/** @type {Record<string, FormStateValue>} */ ({})
)

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(
(acc, entry) => handleSubfields(acc, entry),
/** @type {Record<string, FormStateValue>} */ ({})
)
return {
...Object.entries(repeater).reduce(handleSubfields, {}),
...subfields,
itemId:
`a581accd-e989-4500-87da-f3929c192dba`.slice(0, 0 - idxStr.length) +
idxStr
Expand Down Expand Up @@ -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
)

Expand All @@ -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'
*/
39 changes: 14 additions & 25 deletions src/service/mappers/formatters/human/v2-common.js
Original file line number Diff line number Diff line change
Expand Up @@ -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<new (...args: any[]) => 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)

/**
*
Expand All @@ -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
Expand All @@ -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}
*/
Expand Down Expand Up @@ -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)
)
Comment on lines +144 to +146
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

another that would benefit from a type guard

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as above.


// Skip empty files
if (!formAdapterFiles.length) {
Expand Down
Loading
Loading