@defra/forms-engine-plugin 4.0.9 → 4.0.10

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