Skip to content

Commit c7b9fc6

Browse files
committed
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.
1 parent 4bf1407 commit c7b9fc6

6 files changed

Lines changed: 154 additions & 19 deletions

File tree

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: 68 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
},
@@ -195,6 +202,29 @@ describe('getFormModel helper', () => {
195202
)
196203
})
197204

205+
test('prefers $$__formVersion from definition metadata over metadata.versions', async () => {
206+
const definitionWithFormVersion = {
207+
...definition,
208+
metadata: {
209+
$$__formVersion: { versionNumber: 42 }
210+
}
211+
}
212+
formsService.getFormDefinition = jest
213+
.fn()
214+
.mockResolvedValue(definitionWithFormVersion)
215+
216+
await getFormModel(slug, state, { services, controllers })
217+
218+
expect(FormModel).toHaveBeenCalledWith(
219+
definitionWithFormVersion,
220+
expect.objectContaining({
221+
versionNumber: 42
222+
}),
223+
services,
224+
controllers
225+
)
226+
})
227+
198228
test('throws when no form definition is available', async () => {
199229
jest.mocked(formsService.getFormDefinition).mockResolvedValue(undefined)
200230

@@ -210,11 +240,18 @@ describe('getFormModel helper', () => {
210240

211241
describe('resolveFormModel helper', () => {
212242
const slug = 'tb-origin'
213-
const definition = { pages: [] }
243+
const definition = {
244+
pages: [],
245+
metadata: {
246+
$$__formVersion: {
247+
versionNumber: 9,
248+
createdAt: new Date('2024-10-15T10:00:00Z')
249+
}
250+
}
251+
}
214252
const metadata = {
215253
id: 'metadata-123',
216254
live: { updatedAt: new Date('2024-10-15T10:00:00Z') },
217-
versions: [{ versionNumber: 9 }],
218255
notificationEmail: 'enrique.chase@defra.gov.uk'
219256
}
220257
let server: Request['server']
@@ -274,7 +311,7 @@ describe('resolveFormModel helper', () => {
274311
definition,
275312
expect.objectContaining({
276313
basePath: 'forms/preview/live/tb-origin',
277-
versionNumber: metadata.versions[0].versionNumber,
314+
versionNumber: 9,
278315
ordnanceSurveyApiKey: 'os-api-key',
279316
formId: metadata.id
280317
}),
@@ -283,6 +320,29 @@ describe('resolveFormModel helper', () => {
283320
)
284321
})
285322

323+
test('prefers $$__formVersion from definition metadata over metadata.versions', async () => {
324+
const definitionWithFormVersion = {
325+
...definition,
326+
metadata: {
327+
$$__formVersion: { versionNumber: 55 }
328+
}
329+
}
330+
mockFormsService.getFormDefinition = jest
331+
.fn()
332+
.mockResolvedValue(definitionWithFormVersion)
333+
334+
await resolveFormModel(server, slug, FormStatus.Live)
335+
336+
expect(FormModel).toHaveBeenCalledWith(
337+
definitionWithFormVersion,
338+
expect.objectContaining({
339+
versionNumber: 55
340+
}),
341+
mockServices,
342+
undefined
343+
)
344+
})
345+
286346
test('throws when requested form state does not exist on metadata', async () => {
287347
mockFormsService.getFormMetadata.mockResolvedValue({
288348
id: 'metadata-123',

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)