Skip to content

Commit 8709ba0

Browse files
authored
feat(DF-922): read form version from definition metadata (#340)
* feat(engine): read form version from definition metadata Use $$__formVersion from form definition metadata as the source of truth for version metadata in form context and adapter v1 payloads, with tests covering fallback precedence. * refactor: remove redundant $$__formVersion preference tests
1 parent 61d0a39 commit 8709ba0

File tree

6 files changed

+108
-19
lines changed

6 files changed

+108
-19
lines changed

src/server/constants.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
export const FORM_VERSION_METADATA_KEY = '$$__formVersion'
12
export const PREVIEW_PATH_PREFIX = '/preview'
23
export const FORM_PREFIX = ''
34
export const EXTERNAL_STATE_PAYLOAD = 'EXTERNAL_STATE_PAYLOAD'

src/server/plugins/engine/beta/form-context.test.ts

Lines changed: 22 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ jest.mock('../pageControllers/index.ts', () => {
4444
})
4545

4646
jest.mock('../helpers.ts', () => ({
47-
__esModule: true,
47+
...jest.requireActual('../helpers.ts'),
4848
getCacheService: (...args: unknown[]) => mockGetCacheService(...args),
4949
checkEmailAddressForLiveFormSubmission: (...args: unknown[]) =>
5050
mockCheckEmailAddressForLiveFormSubmission(...args)
@@ -134,10 +134,17 @@ describe('getFormModel helper', () => {
134134
class CustomController extends PageController {}
135135
const controllers = { CustomController }
136136
const metadata = {
137-
id: 'form-meta-123',
138-
versions: [{ versionNumber: 17 }]
137+
id: 'form-meta-123'
138+
}
139+
const definition = {
140+
pages: [{ path: '/start' }],
141+
metadata: {
142+
$$__formVersion: {
143+
versionNumber: 17,
144+
createdAt: new Date('2024-10-15T10:00:00Z')
145+
}
146+
}
139147
}
140-
const definition = { pages: [{ path: '/start' }] }
141148
let formsService: FormsService
142149
let services: Services
143150
let formModelInstance: { id: string }
@@ -176,7 +183,7 @@ describe('getFormModel helper', () => {
176183
definition,
177184
{
178185
basePath: slug,
179-
versionNumber: metadata.versions[0].versionNumber,
186+
versionNumber: 17,
180187
ordnanceSurveyApiKey: undefined,
181188
formId: metadata.id
182189
},
@@ -210,11 +217,18 @@ describe('getFormModel helper', () => {
210217

211218
describe('resolveFormModel helper', () => {
212219
const slug = 'tb-origin'
213-
const definition = { pages: [] }
220+
const definition = {
221+
pages: [],
222+
metadata: {
223+
$$__formVersion: {
224+
versionNumber: 9,
225+
createdAt: new Date('2024-10-15T10:00:00Z')
226+
}
227+
}
228+
}
214229
const metadata = {
215230
id: 'metadata-123',
216231
live: { updatedAt: new Date('2024-10-15T10:00:00Z') },
217-
versions: [{ versionNumber: 9 }],
218232
notificationEmail: 'enrique.chase@defra.gov.uk'
219233
}
220234
let server: Request['server']
@@ -274,7 +288,7 @@ describe('resolveFormModel helper', () => {
274288
definition,
275289
expect.objectContaining({
276290
basePath: 'forms/preview/live/tb-origin',
277-
versionNumber: metadata.versions[0].versionNumber,
291+
versionNumber: 9,
278292
ordnanceSurveyApiKey: 'os-api-key',
279293
formId: metadata.id
280294
}),

src/server/plugins/engine/beta/form-context.ts

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@ import { isEqual } from 'date-fns'
55
import { PREVIEW_PATH_PREFIX } from '~/src/server/constants.js'
66
import {
77
checkEmailAddressForLiveFormSubmission,
8-
getCacheService
8+
getCacheService,
9+
getFormVersion
910
} from '~/src/server/plugins/engine/helpers.js'
1011
import { FormModel } from '~/src/server/plugins/engine/models/index.js'
1112
import { type PageController } from '~/src/server/plugins/engine/pageControllers/PageController.js'
@@ -27,7 +28,6 @@ export interface FormModelOptions {
2728
services?: Services
2829
controllers?: Record<string, typeof PageController>
2930
basePath?: string
30-
versionNumber?: number
3131
ordnanceSurveyApiKey?: string
3232
formId?: string
3333
routePrefix?: string
@@ -53,8 +53,6 @@ export async function getFormModel(
5353
const formState = resolveState(state)
5454

5555
const metadata = await formsService.getFormMetadata(slug)
56-
const versionNumber =
57-
options.versionNumber ?? metadata.versions?.[0]?.versionNumber
5856

5957
const definition = await formsService.getFormDefinition(
6058
metadata.id,
@@ -67,6 +65,8 @@ export async function getFormModel(
6765
)
6866
}
6967

68+
const versionNumber = getFormVersion(definition)?.versionNumber
69+
7070
return new FormModel(
7171
definition,
7272
{
@@ -182,14 +182,15 @@ export async function resolveFormModel(
182182
const routePrefix =
183183
options.routePrefix ?? server.realm.modifiers.route.prefix
184184

185+
const versionNumber = getFormVersion(definition)?.versionNumber
186+
185187
const model = new FormModel(
186188
definition,
187189
{
188190
basePath:
189191
options.basePath ??
190192
buildBasePath(routePrefix, slug, formState, isPreview),
191-
versionNumber:
192-
options.versionNumber ?? metadata.versions?.[0]?.versionNumber,
193+
versionNumber,
193194
ordnanceSurveyApiKey: options.ordnanceSurveyApiKey,
194195
formId: options.formId ?? metadata.id
195196
},

src/server/plugins/engine/helpers.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import { type Schema, type ValidationErrorItem } from 'joi'
1616
import { Liquid } from 'liquidjs'
1717

1818
import { createLogger } from '~/src/server/common/helpers/logging/logger.js'
19+
import { FORM_VERSION_METADATA_KEY } from '~/src/server/constants.js'
1920
import {
2021
getAnswer,
2122
type Field
@@ -416,6 +417,22 @@ export function handleLegacyRedirect(h: ResponseToolkit, targetUrl: string) {
416417
* If the page doesn't have a title, set it from the title of the first form component
417418
* @param def - the form definition
418419
*/
420+
export interface FormVersionMetadata {
421+
versionNumber: number
422+
createdAt: Date
423+
}
424+
425+
/**
426+
* Extracts form version metadata from a form definition
427+
*/
428+
export function getFormVersion(
429+
definition: Pick<FormDefinition, 'metadata'>
430+
): FormVersionMetadata | undefined {
431+
return definition.metadata?.[FORM_VERSION_METADATA_KEY] as
432+
| FormVersionMetadata
433+
| undefined
434+
}
435+
419436
export function setPageTitles(def: FormDefinition) {
420437
def.pages.forEach((page) => {
421438
if (!page.title) {

src/server/plugins/engine/outputFormatters/adapter/v1.test.ts

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -764,6 +764,60 @@ describe('Adapter v1 formatter', () => {
764764
})
765765

766766
describe('version metadata handling', () => {
767+
it('should prefer $$__formVersion from definition metadata over formMetadata.versions', () => {
768+
const definitionWithFormVersion = {
769+
...definition,
770+
metadata: {
771+
$$__formVersion: {
772+
versionNumber: 42,
773+
createdAt: new Date('2024-06-01T00:00:00.000Z')
774+
}
775+
}
776+
}
777+
778+
const modelWithFormVersion = new FormModel(definitionWithFormVersion, {
779+
basePath: 'test'
780+
})
781+
782+
const contextWithFormVersion = modelWithFormVersion.getFormContext(
783+
request,
784+
state
785+
)
786+
787+
const formMetadata: Partial<FormMetadata> = {
788+
id: 'form-123',
789+
slug: 'test-form',
790+
title: 'Test Form',
791+
notificationEmail: 'test@example.com',
792+
versions: [
793+
{
794+
versionNumber: 1,
795+
createdAt: new Date('2024-01-01T00:00:00.000Z')
796+
}
797+
]
798+
}
799+
800+
const formStatus = {
801+
isPreview: false,
802+
state: FormStatus.Live
803+
}
804+
805+
const body = format(
806+
contextWithFormVersion,
807+
items,
808+
modelWithFormVersion,
809+
submitResponse,
810+
formStatus,
811+
formMetadata as FormMetadata
812+
)
813+
const parsedBody = JSON.parse(body) as FormAdapterSubmissionMessagePayload
814+
815+
expect(parsedBody.meta.versionMetadata).toEqual({
816+
versionNumber: 42,
817+
createdAt: '2024-06-01T00:00:00.000Z'
818+
})
819+
})
820+
767821
it('should include versionMetadata when context has submittedVersionNumber and formMetadata has versions', () => {
768822
const formMetadata: Partial<FormMetadata> = {
769823
id: 'form-123',

src/server/plugins/engine/outputFormatters/adapter/v1.ts

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,10 @@ import {
33
type SubmitResponsePayload
44
} from '@defra/forms-model'
55

6-
import { type checkFormStatus } from '~/src/server/plugins/engine/helpers.js'
6+
import {
7+
getFormVersion,
8+
type checkFormStatus
9+
} from '~/src/server/plugins/engine/helpers.js'
710
import { type FormModel } from '~/src/server/plugins/engine/models/FormModel.js'
811
import { type DetailItem } from '~/src/server/plugins/engine/models/types.js'
912
import { categoriseData } from '~/src/server/plugins/engine/outputFormatters/machine/v2.js'
@@ -28,10 +31,9 @@ export function format(
2831

2932
const { main: v2Main, ...v2Data } = categoriseData(items)
3033

31-
const versionMetadata = getVersionMetadata(
32-
context.submittedVersionNumber,
33-
formMetadata
34-
)
34+
const versionMetadata =
35+
getFormVersion(model.def) ??
36+
getVersionMetadata(context.submittedVersionNumber, formMetadata)
3537

3638
const meta: FormAdapterSubmissionMessageMeta = {
3739
schemaVersion: FormAdapterSubmissionSchemaVersion.V1,

0 commit comments

Comments
 (0)