Fix required field validation for missing keys

This commit is contained in:
karilint
2026-05-27 13:34:02 +03:00
parent 549ba5090a
commit 8c386e150b
5 changed files with 79 additions and 32 deletions
+3 -3
View File
@@ -13,6 +13,8 @@ Validators are created as objects with the following fields. Only `name` is requ
- `condition` used to set some condition for when a validator is run. Happens before the check for if a value is required. Used for example in references to only run validators for for certain fields since they are only mandatory on some reference types, not all. Although using 'required' instead may now be possible after changes to it.
- `useEditData` A boolean for if you want to pass editData to the different checks of a validator instead of just the data of the field being validated. Used in references since some validators need to pass editData to a common function. Again, using 'required' instead may be possible after its changes
Use `validator(...)` when validating one edited field. Use `validateFields(...)` for full-object validation, such as backend create/update checks, so validators run for every configured field even when a required key is missing from the submitted data.
Example:
```
@@ -30,6 +32,4 @@ To know if user is creating a new entry or editing existing one, check if the id
### Note!
At the moment you can only create validators for existing and defined fields of editData. This is limiting in some cases and creating validators that check multiple fields (as is necessary in references) requires some trickery. Similarly you cannot create multiple validators for the same field.
Validators are only run if the data is defined! If the value of a key of editData == undefined, the data is not checked even if it is set as required in the validator. Similarly if a key does not exist in editData, it will not be checked even if the data would be required. This is also the case in backend. This may lead to mistakes getting into the db if some data must be set but it is not defined at all.
For full-object validation, do not iterate over `Object.keys(editData)`. That only checks fields included in the payload. Instead, call `validateFields(...)`, which iterates over the configured validator keys and catches required fields that are `undefined` or missing entirely.
+19 -18
View File
@@ -1,24 +1,25 @@
import { EditDataType, TimeBoundDetailsType } from '../types'
import { Validators, validator } from './validator'
import { Validators, validateFields, validator } from './validator'
export const timeBoundValidators: Validators<EditDataType<Partial<TimeBoundDetailsType>>> = {
b_name: {
name: 'Name',
required: true,
},
age: {
name: 'Age (Ma)',
required: true,
asNumber: (num: number) => {
if (num < 0) return 'Age must be a positive number'
return
},
},
}
export const validateTimeBound = (
editData: EditDataType<TimeBoundDetailsType>,
fieldName: keyof TimeBoundDetailsType
) => {
const validators: Validators<EditDataType<Partial<TimeBoundDetailsType>>> = {
b_name: {
name: 'Name',
required: true,
},
age: {
name: 'Age (Ma)',
required: true,
asNumber: (num: number) => {
if (num < 0) return 'Age must be a positive number'
return
},
},
}
) => validator<EditDataType<TimeBoundDetailsType>>(timeBoundValidators, editData, fieldName)
return validator<EditDataType<TimeBoundDetailsType>>(validators, editData, fieldName)
}
export const validateTimeBoundFields = (editData: Partial<EditDataType<TimeBoundDetailsType>>) =>
validateFields<EditDataType<TimeBoundDetailsType>>(timeBoundValidators, editData)
+10 -1
View File
@@ -3,7 +3,7 @@ import { Editable, Reference } from '../types'
export type ValidationError = string | null | undefined
export type ValidationObject = { name: string; error: ValidationError }
type Validator = {
export type Validator = {
name: string
required?: (() => ValidationError) | boolean
minLength?: number
@@ -87,6 +87,15 @@ export const validator = <T>(
return { name: fieldValidator.name, error: validationError }
}
export const validateFields = <T>(
validators: Validators<Partial<T>>,
editData: Partial<T>,
fieldNames: Array<keyof T> = Object.keys(validators) as Array<keyof T>
): ValidationObject[] =>
fieldNames
.map(fieldName => validator<T>(validators, editData, fieldName))
.filter((validationObject): validationObject is ValidationObject => Boolean(validationObject.error))
export const referenceValidator: (references: Editable<Reference>[]) => ValidationError = (
references: Editable<Reference>[]
) => {
@@ -0,0 +1,45 @@
import { describe, expect, it } from '@jest/globals'
import { validateTimeBoundFields } from '@/shared/validators/timeBound'
import { validateFields, type Validators } from '@/shared/validators/validator'
type TestData = {
id?: number
requiredName?: string
optionalComment?: string | null
}
describe('validateFields', () => {
const validators: Validators<Partial<TestData>> = {
requiredName: {
name: 'Required name',
required: true,
},
optionalComment: {
name: 'Optional comment',
},
}
it('runs validators for configured fields missing from editData', () => {
const errors = validateFields<TestData>(validators, {})
expect(errors).toEqual([{ name: 'Required name', error: 'This field is required' }])
})
it('allows callers to validate a focused field list', () => {
const errors = validateFields<TestData>(validators, {}, ['optionalComment'])
expect(errors).toEqual([])
})
})
describe('validateTimeBoundFields', () => {
it('reports missing required fields', () => {
const errors = validateTimeBoundFields({})
expect(errors).toEqual([
{ name: 'Name', error: 'This field is required' },
{ name: 'Age (Ma)', error: 'This field is required' },
])
})
})