fix: strongly types field validation args (#8263)

Continuation of #8243. Strongly types the `value` argument within
`field.validate` functions:

- Uses existing internal validation types for field `validate` property
- Exports additional validation types to cover `hasMany` fields
- Includes `null` and `undefined` values
This commit is contained in:
Jacob Fletcher
2024-09-17 15:14:10 -04:00
committed by GitHub
parent 110fda7533
commit c0aad3cccb
4 changed files with 110 additions and 28 deletions

View File

@@ -104,9 +104,33 @@ import type {
} from '../../config/types.js'
import type { DBIdentifierName } from '../../database/types.js'
import type { SanitizedGlobalConfig } from '../../globals/config/types.js'
import type { CollectionSlug } from '../../index.js'
import type {
ArrayFieldValidation,
BlocksFieldValidation,
CheckboxFieldValidation,
CodeFieldValidation,
CollectionSlug,
DateFieldValidation,
EmailFieldValidation,
JSONFieldValidation,
PointFieldValidation,
RadioFieldValidation,
TextareaFieldValidation,
} from '../../index.js'
import type { DocumentPreferences } from '../../preferences/types.js'
import type { Operation, PayloadRequest, RequestContext, Where } from '../../types/index.js'
import type {
NumberFieldManyValidation,
NumberFieldSingleValidation,
RelationshipFieldManyValidation,
RelationshipFieldSingleValidation,
SelectFieldManyValidation,
SelectFieldSingleValidation,
TextFieldManyValidation,
TextFieldSingleValidation,
UploadFieldManyValidation,
UploadFieldSingleValidation,
} from '../validations.js'
export type FieldHookArgs<TData extends TypeWithID = any, TValue = any, TSiblingData = any> = {
/** The collection which the field belongs to. If the field belongs to a global, this will be null. */
@@ -335,7 +359,7 @@ export type Validate<
TSiblingData = any,
TFieldConfig extends object = object,
> = (
value: TValue,
value: null | TValue | undefined,
options: ValidateOptions<TData, TSiblingData, TFieldConfig, TValue>,
) => Promise<string | true> | string | true
@@ -452,7 +476,7 @@ export type NumberField = {
maxRows?: number
/** Minimum number of numbers in the numbers array, if `hasMany` is set to true. */
minRows?: number
validate?: Validate<number[], unknown, unknown, NumberField>
validate?: NumberFieldManyValidation
}
| {
/** Makes this field an ordered array of numbers instead of just a single number. */
@@ -461,7 +485,7 @@ export type NumberField = {
maxRows?: undefined
/** Minimum number of numbers in the numbers array, if `hasMany` is set to true. */
minRows?: undefined
validate?: Validate<number, unknown, unknown, NumberField>
validate?: NumberFieldSingleValidation
}
) &
Omit<FieldBase, 'validate'>
@@ -502,7 +526,7 @@ export type TextField = {
maxRows?: number
/** Minimum number of strings in the strings array, if `hasMany` is set to true. */
minRows?: number
validate?: Validate<string[], unknown, unknown, TextField>
validate?: TextFieldManyValidation
}
| {
/** Makes this field an ordered array of strings instead of just a single string. */
@@ -511,7 +535,7 @@ export type TextField = {
maxRows?: undefined
/** Minimum number of strings in the strings array, if `hasMany` is set to true. */
minRows?: undefined
validate?: Validate<string, unknown, unknown, TextField>
validate?: TextFieldSingleValidation
}
) &
Omit<FieldBase, 'validate'>
@@ -541,7 +565,7 @@ export type EmailField = {
placeholder?: Record<string, string> | string
} & Admin
type: 'email'
validate?: Validate<string, unknown, unknown, EmailField>
validate?: EmailFieldValidation
} & Omit<FieldBase, 'validate'>
export type EmailFieldClient = {
@@ -572,7 +596,7 @@ export type TextareaField = {
maxLength?: number
minLength?: number
type: 'textarea'
validate?: Validate<string, unknown, unknown, TextareaField>
validate?: TextareaFieldValidation
} & Omit<FieldBase, 'validate'>
export type TextareaFieldClient = {
@@ -598,7 +622,7 @@ export type CheckboxField = {
} & Admin['components']
} & Admin
type: 'checkbox'
validate?: Validate<boolean, unknown, unknown, CheckboxField>
validate?: CheckboxFieldValidation
} & Omit<FieldBase, 'validate'>
export type CheckboxFieldClient = {
@@ -625,7 +649,7 @@ export type DateField = {
placeholder?: Record<string, string> | string
} & Admin
type: 'date'
validate?: Validate<unknown, unknown, unknown, DateField>
validate?: DateFieldValidation
} & Omit<FieldBase, 'validate'>
export type DateFieldClient = {
@@ -861,7 +885,7 @@ type SharedUploadProperties = {
*/
min?: number
minRows?: number
validate?: Validate<unknown[], unknown, unknown, SharedUploadProperties>
validate?: UploadFieldManyValidation
}
| {
hasMany?: false | undefined
@@ -875,7 +899,7 @@ type SharedUploadProperties = {
*/
min?: undefined
minRows?: undefined
validate?: Validate<unknown, unknown, unknown, SharedUploadProperties>
validate?: UploadFieldSingleValidation
}
) &
Omit<FieldBase, 'validate'>
@@ -950,7 +974,7 @@ export type CodeField = {
maxLength?: number
minLength?: number
type: 'code'
validate?: Validate<string, unknown, unknown, CodeField>
validate?: CodeFieldValidation
} & Omit<FieldBase, 'admin' | 'validate'>
export type CodeFieldClient = {
@@ -983,7 +1007,7 @@ export type JSONField = {
uri: string
}
type: 'json'
validate?: Validate<Record<string, unknown>, unknown, unknown, JSONField>
validate?: JSONFieldValidation
} & Omit<FieldBase, 'admin' | 'validate'>
export type JSONFieldClient = {
@@ -1024,11 +1048,11 @@ export type SelectField = {
} & (
| {
hasMany: true
validate?: Validate<string[], unknown, unknown, SelectField>
validate?: SelectFieldManyValidation
}
| {
hasMany?: false | undefined
validate?: Validate<string, unknown, unknown, SelectField>
validate?: SelectFieldSingleValidation
}
) &
Omit<FieldBase, 'validate'>
@@ -1068,7 +1092,7 @@ type SharedRelationshipProperties = {
*/
min?: number
minRows?: number
validate?: Validate<any[], unknown, unknown, SharedRelationshipProperties>
validate?: RelationshipFieldManyValidation
}
| {
hasMany?: false | undefined
@@ -1082,7 +1106,7 @@ type SharedRelationshipProperties = {
*/
min?: undefined
minRows?: undefined
validate?: Validate<any, unknown, unknown, SharedRelationshipProperties>
validate?: RelationshipFieldSingleValidation
}
) &
Omit<FieldBase, 'validate'>
@@ -1155,11 +1179,11 @@ export function valueIsValueWithRelation(value: unknown): value is ValueWithRela
return value !== null && typeof value === 'object' && 'relationTo' in value && 'value' in value
}
export type RelationshipValue =
| (number | string)[]
| (number | string)
| ValueWithRelation
| ValueWithRelation[]
export type RelationshipValue = RelationshipValueMany | RelationshipValueSingle
export type RelationshipValueMany = (number | string)[] | ValueWithRelation[]
export type RelationshipValueSingle = number | string | ValueWithRelation
export type RichTextField<
TValue extends object = any,
@@ -1231,7 +1255,7 @@ export type ArrayField = {
maxRows?: number
minRows?: number
type: 'array'
validate?: Validate<unknown[], unknown, unknown, ArrayField>
validate?: ArrayFieldValidation
} & Omit<FieldBase, 'validate'>
export type ArrayFieldClient = {
@@ -1266,7 +1290,7 @@ export type RadioField = {
enumName?: DBIdentifierName
options: Option[]
type: 'radio'
validate?: Validate<string, unknown, unknown, RadioField>
validate?: RadioFieldValidation
} & Omit<FieldBase, 'validate'>
export type RadioFieldClient = {
@@ -1352,7 +1376,7 @@ export type BlocksField = {
maxRows?: number
minRows?: number
type: 'blocks'
validate?: Validate<string, unknown, unknown, BlocksField>
validate?: BlocksFieldValidation
} & Omit<FieldBase, 'validate'>
export type BlocksFieldClient = {
@@ -1379,7 +1403,7 @@ export type PointField = {
step?: number
} & Admin
type: 'point'
validate?: Validate<[number, number], unknown, unknown, PointField>
validate?: PointFieldValidation
} & Omit<FieldBase, 'validate'>
export type PointFieldClient = {

View File

@@ -20,6 +20,8 @@ import type {
RadioField,
RelationshipField,
RelationshipValue,
RelationshipValueMany,
RelationshipValueSingle,
RichTextField,
SelectField,
TextareaField,
@@ -32,6 +34,11 @@ import { isNumber } from '../utilities/isNumber.js'
import { isValidID } from '../utilities/isValidID.js'
export type TextFieldValidation = Validate<string, unknown, unknown, TextField>
export type TextFieldManyValidation = Validate<string[], unknown, unknown, TextField>
export type TextFieldSingleValidation = Validate<string, unknown, unknown, TextField>
export const text: TextFieldValidation = (
value,
{
@@ -93,6 +100,7 @@ export const text: TextFieldValidation = (
}
export type PasswordFieldValidation = Validate<string, unknown, unknown, TextField>
export const password: PasswordFieldValidation = (
value,
{
@@ -135,6 +143,7 @@ export type ConfirmPasswordFieldValidation = Validate<
{ password: string },
TextField
>
export const confirmPassword: ConfirmPasswordFieldValidation = (
value,
{ req: { t }, required, siblingData },
@@ -151,6 +160,7 @@ export const confirmPassword: ConfirmPasswordFieldValidation = (
}
export type EmailFieldValidation = Validate<string, unknown, { username?: string }, EmailField>
export const email: EmailFieldValidation = (
value,
{
@@ -185,6 +195,7 @@ export const email: EmailFieldValidation = (
}
export type UsernameFieldValidation = Validate<string, unknown, { email?: string }, TextField>
export const username: UsernameFieldValidation = (
value,
{
@@ -229,6 +240,7 @@ export const username: UsernameFieldValidation = (
}
export type TextareaFieldValidation = Validate<string, unknown, unknown, TextareaField>
export const textarea: TextareaFieldValidation = (
value,
{
@@ -265,6 +277,7 @@ export const textarea: TextareaFieldValidation = (
}
export type CodeFieldValidation = Validate<string, unknown, unknown, CodeField>
export const code: CodeFieldValidation = (value, { req: { t }, required }) => {
if (required && value === undefined) {
return t('validation:required')
@@ -279,6 +292,7 @@ export type JSONFieldValidation = Validate<
unknown,
{ jsonError?: string } & JSONField
>
export const json: JSONFieldValidation = async (
value,
{ jsonError, jsonSchema, req: { t }, required },
@@ -348,6 +362,7 @@ export const json: JSONFieldValidation = async (
}
export type CheckboxFieldValidation = Validate<boolean, unknown, unknown, CheckboxField>
export const checkbox: CheckboxFieldValidation = (value, { req: { t }, required }) => {
if ((value && typeof value !== 'boolean') || (required && typeof value !== 'boolean')) {
return t('validation:trueOrFalse')
@@ -357,6 +372,7 @@ export const checkbox: CheckboxFieldValidation = (value, { req: { t }, required
}
export type DateFieldValidation = Validate<Date, unknown, unknown, DateField>
export const date: DateFieldValidation = (value, { req: { t }, required }) => {
if (value && !isNaN(Date.parse(value.toString()))) {
return true
@@ -374,6 +390,7 @@ export const date: DateFieldValidation = (value, { req: { t }, required }) => {
}
export type RichTextFieldValidation = Validate<object, unknown, unknown, RichTextField>
export const richText: RichTextFieldValidation = async (value, options) => {
if (!options?.editor) {
throw new Error('richText field has no editor property.')
@@ -420,6 +437,11 @@ const validateArrayLength = (
}
export type NumberFieldValidation = Validate<number | number[], unknown, unknown, NumberField>
export type NumberFieldManyValidation = Validate<number[], unknown, unknown, NumberField>
export type NumberFieldSingleValidation = Validate<number, unknown, unknown, NumberField>
export const number: NumberFieldValidation = (
value,
{ hasMany, max, maxRows, min, minRows, req: { t }, required },
@@ -611,6 +633,10 @@ const validateFilterOptions: Validate<
export type UploadFieldValidation = Validate<unknown, unknown, unknown, UploadField>
export type UploadFieldManyValidation = Validate<unknown[], unknown, unknown, UploadField>
export type UploadFieldSingleValidation = Validate<unknown, unknown, unknown, UploadField>
export const upload: UploadFieldValidation = async (value, options) => {
const {
maxRows,
@@ -694,6 +720,21 @@ export type RelationshipFieldValidation = Validate<
unknown,
RelationshipField
>
export type RelationshipFieldManyValidation = Validate<
RelationshipValueMany,
unknown,
unknown,
RelationshipField
>
export type RelationshipFieldSingleValidation = Validate<
RelationshipValueSingle,
unknown,
unknown,
RelationshipField
>
export const relationship: RelationshipFieldValidation = async (value, options) => {
const {
maxRows,
@@ -772,6 +813,11 @@ export const relationship: RelationshipFieldValidation = async (value, options)
}
export type SelectFieldValidation = Validate<string | string[], unknown, unknown, SelectField>
export type SelectFieldManyValidation = Validate<string[], unknown, unknown, SelectField>
export type SelectFieldSingleValidation = Validate<string, unknown, unknown, SelectField>
export const select: SelectFieldValidation = (
value,
{ hasMany, options, req: { t }, required },
@@ -810,6 +856,7 @@ export const select: SelectFieldValidation = (
}
export type RadioFieldValidation = Validate<unknown, unknown, unknown, RadioField>
export const radio: RadioFieldValidation = (value, { options, req: { t }, required }) => {
if (value) {
const valueMatchesOption = options.some(
@@ -827,6 +874,7 @@ export type PointFieldValidation = Validate<
unknown,
PointField
>
export const point: PointFieldValidation = (value = ['', ''], { req: { t }, required }) => {
const lng = parseFloat(String(value[0]))
const lat = parseFloat(String(value[1]))

View File

@@ -937,15 +937,25 @@ export type {
DateFieldValidation,
EmailFieldValidation,
JSONFieldValidation,
NumberFieldManyValidation,
NumberFieldSingleValidation,
NumberFieldValidation,
PasswordFieldValidation,
PointFieldValidation,
RadioFieldValidation,
RelationshipFieldManyValidation,
RelationshipFieldSingleValidation,
RelationshipFieldValidation,
RichTextFieldValidation,
SelectFieldManyValidation,
SelectFieldSingleValidation,
SelectFieldValidation,
TextareaFieldValidation,
TextFieldManyValidation,
TextFieldSingleValidation,
TextFieldValidation,
UploadFieldManyValidation,
UploadFieldSingleValidation,
UploadFieldValidation,
UsernameFieldValidation,
} from './fields/validations.js'

View File

@@ -60,7 +60,7 @@ const NumberFields: CollectionConfig = {
name: 'validatesHasMany',
type: 'number',
hasMany: true,
validate: (value: number[]) => {
validate: (value) => {
if (value && !Array.isArray(value)) {
return 'value should be an array'
}