@defra/forms-engine-plugin 4.0.9 → 4.0.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,5 +1,9 @@
1
1
  import { type NumberFieldComponent } from '@defra/forms-model'
2
- import joi, { type CustomValidator, type NumberSchema } from 'joi'
2
+ import joi, {
3
+ type CustomHelpers,
4
+ type CustomValidator,
5
+ type NumberSchema
6
+ } from 'joi'
3
7
 
4
8
  import {
5
9
  FormComponent,
@@ -161,29 +165,154 @@ export class NumberField extends FormComponent {
161
165
  }
162
166
  }
163
167
 
168
+ /**
169
+ * Validates string length of a numeric value
170
+ * @param value - The numeric value to validate
171
+ * @param minLength - Minimum required string length
172
+ * @param maxLength - Maximum allowed string length
173
+ * @returns Object with validation result
174
+ */
175
+ export function validateStringLength(
176
+ value: number,
177
+ minLength?: number,
178
+ maxLength?: number
179
+ ): { isValid: boolean; error?: 'minLength' | 'maxLength' } {
180
+ if (typeof minLength !== 'number' && typeof maxLength !== 'number') {
181
+ return { isValid: true }
182
+ }
183
+
184
+ const valueStr = String(value)
185
+
186
+ if (typeof minLength === 'number' && valueStr.length < minLength) {
187
+ return { isValid: false, error: 'minLength' }
188
+ }
189
+
190
+ if (typeof maxLength === 'number' && valueStr.length > maxLength) {
191
+ return { isValid: false, error: 'maxLength' }
192
+ }
193
+
194
+ return { isValid: true }
195
+ }
196
+
197
+ /**
198
+ * Validates minimum decimal precision
199
+ * @param value - The numeric value to validate
200
+ * @param minPrecision - Minimum required decimal places
201
+ * @returns true if valid, false if invalid
202
+ */
203
+ export function validateMinimumPrecision(
204
+ value: number,
205
+ minPrecision: number
206
+ ): boolean {
207
+ if (Number.isInteger(value)) {
208
+ return false
209
+ }
210
+
211
+ const valueStr = String(value)
212
+ const decimalIndex = valueStr.indexOf('.')
213
+
214
+ if (decimalIndex !== -1) {
215
+ const decimalPlaces = valueStr.length - decimalIndex - 1
216
+ return decimalPlaces >= minPrecision
217
+ }
218
+
219
+ return false
220
+ }
221
+
222
+ /**
223
+ * Helper function to handle length validation errors
224
+ * Returns the appropriate error response based on the validation result
225
+ */
226
+ function handleLengthValidationError(
227
+ lengthCheck: ReturnType<typeof validateStringLength>,
228
+ helpers: CustomHelpers,
229
+ custom: string | undefined,
230
+ minLength: number | undefined,
231
+ maxLength: number | undefined
232
+ ) {
233
+ if (!lengthCheck.isValid && lengthCheck.error) {
234
+ const errorType = `number.${lengthCheck.error}`
235
+
236
+ if (custom) {
237
+ // Only pass the relevant length value in context
238
+ const contextData =
239
+ lengthCheck.error === 'minLength'
240
+ ? { minLength: minLength ?? 0 }
241
+ : { maxLength: maxLength ?? 0 }
242
+ return helpers.message({ custom }, contextData)
243
+ }
244
+
245
+ const context =
246
+ lengthCheck.error === 'minLength'
247
+ ? { minLength: minLength ?? 0 }
248
+ : { maxLength: maxLength ?? 0 }
249
+ return helpers.error(errorType, context)
250
+ }
251
+ return null
252
+ }
253
+
164
254
  export function getValidatorPrecision(component: NumberField) {
165
255
  const validator: CustomValidator = (value: number, helpers) => {
166
256
  const { options, schema } = component
167
-
168
257
  const { customValidationMessage: custom } = options
169
- const { precision: limit } = schema
258
+ const {
259
+ precision: limit,
260
+ minPrecision,
261
+ minLength,
262
+ maxLength
263
+ } = schema as {
264
+ precision?: number
265
+ minPrecision?: number
266
+ minLength?: number
267
+ maxLength?: number
268
+ }
170
269
 
171
270
  if (!limit || limit <= 0) {
271
+ const lengthCheck = validateStringLength(value, minLength, maxLength)
272
+ const error = handleLengthValidationError(
273
+ lengthCheck,
274
+ helpers,
275
+ custom,
276
+ minLength,
277
+ maxLength
278
+ )
279
+ if (error) return error
172
280
  return value
173
281
  }
174
282
 
283
+ // Validate precision (max decimal places)
175
284
  const validationSchema = joi
176
285
  .number()
177
286
  .precision(limit)
178
287
  .prefs({ convert: false })
179
288
 
180
289
  try {
181
- return joi.attempt(value, validationSchema)
290
+ joi.attempt(value, validationSchema)
182
291
  } catch {
183
292
  return custom
184
293
  ? helpers.message({ custom }, { limit })
185
294
  : helpers.error('number.precision', { limit })
186
295
  }
296
+
297
+ // Validate minimum precision (min decimal places)
298
+ if (typeof minPrecision === 'number' && minPrecision > 0) {
299
+ if (!validateMinimumPrecision(value, minPrecision)) {
300
+ return helpers.error('number.minPrecision', { minPrecision })
301
+ }
302
+ }
303
+
304
+ // Check string length validation after precision checks
305
+ const lengthCheck = validateStringLength(value, minLength, maxLength)
306
+ const error = handleLengthValidationError(
307
+ lengthCheck,
308
+ helpers,
309
+ custom,
310
+ minLength,
311
+ maxLength
312
+ )
313
+ if (error) return error
314
+
315
+ return value
187
316
  }
188
317
 
189
318
  return validator
@@ -11,7 +11,6 @@ import { PageController } from '~/src/server/plugins/engine/pageControllers/Page
11
11
  import { getProxyUrlForLocalDevelopment } from '~/src/server/plugins/engine/pageControllers/helpers/index.js'
12
12
  import {
13
13
  createPage,
14
- isPageController,
15
14
  type PageControllerType
16
15
  } from '~/src/server/plugins/engine/pageControllers/helpers/pages.js'
17
16
  import {
@@ -26,7 +25,10 @@ import {
26
25
  import definition from '~/test/form/definitions/blank.js'
27
26
 
28
27
  describe('Page controller helpers', () => {
29
- const examples = PageTypes.map((pageType) => {
28
+ const examples = PageTypes.filter(
29
+ (pageType) =>
30
+ pageType.controller !== ControllerType.SummaryWithConfirmationEmail
31
+ ).map((pageType) => {
30
32
  const pageDef = structuredClone(pageType)
31
33
 
32
34
  let controller: PageControllerType | undefined
@@ -67,10 +69,6 @@ describe('Page controller helpers', () => {
67
69
  controller = SummaryPageController
68
70
  break
69
71
 
70
- case ControllerType.SummaryWithConfirmationEmail:
71
- controller = SummaryPageController
72
- break
73
-
74
72
  case ControllerType.Status:
75
73
  controller = StatusPageController
76
74
  break
@@ -155,24 +153,6 @@ describe('Page controller helpers', () => {
155
153
  })
156
154
  })
157
155
 
158
- describe('Helper: isPageController', () => {
159
- it.each([...examples])(
160
- "allows valid page controller '$pageDef.controller'",
161
- ({ pageDef }) => {
162
- expect(isPageController(pageDef.controller)).toBe(true)
163
- }
164
- )
165
-
166
- it.each([
167
- { name: './pages/unknown.js' },
168
- { name: 'UnknownPageController' },
169
- { name: undefined },
170
- { name: '' }
171
- ])("rejects invalid page controller '$name'", ({ name }) => {
172
- expect(isPageController(name)).toBe(false)
173
- })
174
- })
175
-
176
156
  describe('Helper: getProxyUrlForLocalDevelopment', () => {
177
157
  it('returns null if uploadUrl is undefined', () => {
178
158
  expect(getProxyUrlForLocalDevelopment(undefined)).toBeNull()
@@ -1,23 +1,12 @@
1
1
  import {
2
2
  ControllerType,
3
3
  controllerNameFromPath,
4
- isControllerName,
5
4
  type Page
6
5
  } from '@defra/forms-model'
7
6
 
8
7
  import { type FormModel } from '~/src/server/plugins/engine/models/index.js'
9
8
  import * as PageControllers from '~/src/server/plugins/engine/pageControllers/index.js'
10
9
 
11
- export function isPageController(
12
- controllerName?: string | ControllerType
13
- ): controllerName is keyof typeof PageControllers {
14
- // Handle SummaryWithConfirmationEmail as it uses SummaryPageController
15
- if (controllerName === ControllerType.SummaryWithConfirmationEmail) {
16
- return true
17
- }
18
- return isControllerName(controllerName) && controllerName in PageControllers
19
- }
20
-
21
10
  export type PageControllerClass = InstanceType<PageControllerType>
22
11
  export type PageControllerType =
23
12
  (typeof PageControllers)[keyof typeof PageControllers]
@@ -56,10 +45,6 @@ export function createPage(model: FormModel, pageDef: Page) {
56
45
  controller = new PageControllers.SummaryPageController(model, pageDef)
57
46
  break
58
47
 
59
- case ControllerType.SummaryWithConfirmationEmail:
60
- controller = new PageControllers.SummaryPageController(model, pageDef)
61
- break
62
-
63
48
  case ControllerType.Status:
64
49
  controller = new PageControllers.StatusPageController(model, pageDef)
65
50
  break