Files
payload/packages/payload/src/fields/validations.ts
2023-11-21 10:12:26 -05:00

515 lines
13 KiB
TypeScript

import type { RichTextAdapter } from '../exports/types'
import type {
ArrayField,
BlockField,
CheckboxField,
CodeField,
DateField,
EmailField,
JSONField,
NumberField,
PointField,
RadioField,
RelationshipField,
RelationshipValue,
RichTextField,
SelectField,
TextField,
TextareaField,
UploadField,
Validate,
} from './config/types'
import canUseDOM from '../utilities/canUseDOM'
import { getIDType } from '../utilities/getIDType'
import { isNumber } from '../utilities/isNumber'
import { isValidID } from '../utilities/isValidID'
import { fieldAffectsData } from './config/types'
export const text: Validate<unknown, unknown, TextField> = (
value: string,
{ config, maxLength: fieldMaxLength, minLength, required, t },
) => {
let maxLength: number
if (typeof config?.defaultMaxTextLength === 'number') maxLength = config.defaultMaxTextLength
if (typeof fieldMaxLength === 'number') maxLength = fieldMaxLength
if (value && maxLength && value.length > maxLength) {
return t('validation:shorterThanMax', { maxLength })
}
if (value && minLength && value?.length < minLength) {
return t('validation:longerThanMin', { minLength })
}
if (required) {
if (typeof value !== 'string' || value?.length === 0) {
return t('validation:required')
}
}
return true
}
export const password: Validate<unknown, unknown, TextField> = (
value: string,
{ config, maxLength: fieldMaxLength, minLength, payload, required, t },
) => {
let maxLength: number
if (typeof config?.defaultMaxTextLength === 'number') maxLength = config.defaultMaxTextLength
if (typeof fieldMaxLength === 'number') maxLength = fieldMaxLength
if (value && maxLength && value.length > maxLength) {
return t('validation:shorterThanMax', { maxLength })
}
if (value && minLength && value.length < minLength) {
return t('validation:longerThanMin', { minLength })
}
if (required && !value) {
return t('validation:required')
}
return true
}
export const email: Validate<unknown, unknown, EmailField> = (value: string, { required, t }) => {
if ((value && !/\S[^\s@]*@\S+\.\S+/.test(value)) || (!value && required)) {
return t('validation:emailAddress')
}
return true
}
export const textarea: Validate<unknown, unknown, TextareaField> = (
value: string,
{ config, maxLength: fieldMaxLength, minLength, payload, required, t },
) => {
let maxLength: number
if (typeof config?.defaultMaxTextLength === 'number') maxLength = config.defaultMaxTextLength
if (typeof fieldMaxLength === 'number') maxLength = fieldMaxLength
if (value && maxLength && value.length > maxLength) {
return t('validation:shorterThanMax', { maxLength })
}
if (value && minLength && value.length < minLength) {
return t('validation:longerThanMin', { minLength })
}
if (required && !value) {
return t('validation:required')
}
return true
}
export const code: Validate<unknown, unknown, CodeField> = (value: string, { required, t }) => {
if (required && value === undefined) {
return t('validation:required')
}
return true
}
export const json: Validate<unknown, unknown, JSONField & { jsonError?: string }> = (
value: string,
{ jsonError, required, t },
) => {
if (required && !value) {
return t('validation:required')
}
if (jsonError !== undefined) {
return t('validation:invalidInput')
}
return true
}
export const checkbox: Validate<unknown, unknown, CheckboxField> = (
value: boolean,
{ required, t },
) => {
if ((value && typeof value !== 'boolean') || (required && typeof value !== 'boolean')) {
return t('validation:trueOrFalse')
}
return true
}
export const date: Validate<unknown, unknown, DateField> = (value, { required, t }) => {
if (value && !isNaN(Date.parse(value.toString()))) {
/* eslint-disable-line */
return true
}
if (value) {
return t('validation:notValidDate', { value })
}
if (required) {
return t('validation:required')
}
return true
}
export const richText: Validate<object, unknown, RichTextField, RichTextField> = async (
value,
options,
) => {
const editor: RichTextAdapter = options?.editor
return await editor.validate(value, options)
}
const validateArrayLength: any = (
value,
options: {
maxRows?: number
minRows?: number
required?: boolean
t: (key: string, options?: { [key: string]: number | string }) => string
},
) => {
const { maxRows, minRows, required, t } = options
const arrayLength = Array.isArray(value) ? value.length : 0
if (!required && arrayLength === 0) return true
if (minRows && arrayLength < minRows) {
return t('validation:requiresAtLeast', { count: minRows, label: t('rows') })
}
if (maxRows && arrayLength > maxRows) {
return t('validation:requiresNoMoreThan', { count: maxRows, label: t('rows') })
}
if (required && !arrayLength) {
return t('validation:requiresAtLeast', { count: 1, label: t('row') })
}
return true
}
export const number: Validate<unknown, unknown, NumberField> = (
value: number | number[],
{ hasMany, max, maxRows, min, minRows, required, t },
) => {
if (hasMany === true) {
const lengthValidationResult = validateArrayLength(value, { maxRows, minRows, required, t })
if (typeof lengthValidationResult === 'string') return lengthValidationResult
}
if (!value && !isNumber(value)) {
// if no value is present, validate based on required
if (required) return t('validation:required')
if (!required) return true
}
const numbersToValidate: number[] = Array.isArray(value) ? value : [value]
for (const number of numbersToValidate) {
if (!isNumber(number)) return t('validation:enterNumber')
const numberValue = parseFloat(number as unknown as string)
if (typeof max === 'number' && numberValue > max) {
return t('validation:greaterThanMax', { label: t('value'), max, value })
}
if (typeof min === 'number' && numberValue < min) {
return t('validation:lessThanMin', { label: t('value'), min, value })
}
}
return true
}
export const array: Validate<unknown, unknown, ArrayField> = (
value,
{ maxRows, minRows, required, t },
) => {
return validateArrayLength(value, { maxRows, minRows, required, t })
}
export const blocks: Validate<unknown, unknown, BlockField> = (
value,
{ maxRows, minRows, required, t },
) => {
return validateArrayLength(value, { maxRows, minRows, required, t })
}
const validateFilterOptions: Validate = async (
value,
{ id, data, filterOptions, payload, relationTo, req, siblingData, t, user },
) => {
if (!canUseDOM && typeof filterOptions !== 'undefined' && value) {
const options: {
[collection: string]: (number | string)[]
} = {}
const collections = typeof relationTo === 'string' ? [relationTo] : relationTo
const values = Array.isArray(value) ? value : [value]
await Promise.all(
collections.map(async (collection) => {
const optionFilter =
typeof filterOptions === 'function'
? await filterOptions({
id,
data,
relationTo: collection,
siblingData,
user,
})
: filterOptions
const valueIDs: (number | string)[] = []
values.forEach((val) => {
if (typeof val === 'object' && val?.value) {
valueIDs.push(val.value)
}
if (typeof val === 'string' || typeof val === 'number') {
valueIDs.push(val)
}
})
if (valueIDs.length > 0) {
const findWhere = {
and: [{ id: { in: valueIDs } }],
}
if (optionFilter) findWhere.and.push(optionFilter)
const result = await payload.find({
collection,
depth: 0,
limit: 0,
pagination: false,
req,
where: findWhere,
})
options[collection] = result.docs.map((doc) => doc.id)
} else {
options[collection] = []
}
}),
)
const invalidRelationships = values.filter((val) => {
let collection: string
let requestedID: number | string
if (typeof relationTo === 'string') {
collection = relationTo
if (typeof val === 'string' || typeof val === 'number') {
requestedID = val
}
}
if (Array.isArray(relationTo) && typeof val === 'object' && val?.relationTo) {
collection = val.relationTo
requestedID = val.value
}
return options[collection].indexOf(requestedID) === -1
})
if (invalidRelationships.length > 0) {
return invalidRelationships.reduce((err, invalid, i) => {
return `${err} ${JSON.stringify(invalid)}${
invalidRelationships.length === i + 1 ? ',' : ''
} `
}, t('validation:invalidSelections')) as string
}
return true
}
return true
}
export const upload: Validate<unknown, unknown, UploadField> = (value: string, options) => {
if (!value && options.required) {
return options.t('validation:required')
}
if (!canUseDOM && typeof value !== 'undefined' && value !== null) {
const idField = options?.config?.collections
?.find((collection) => collection.slug === options.relationTo)
?.fields?.find((field) => fieldAffectsData(field) && field.name === 'id')
const type = getIDType(idField, options?.payload?.db?.defaultIDType)
if (!isValidID(value, type)) {
return options.t('validation:validUploadID')
}
}
return validateFilterOptions(value, options)
}
export const relationship: Validate<unknown, unknown, RelationshipField> = async (
value: RelationshipValue,
options,
) => {
const { config, maxRows, minRows, payload, relationTo, required, t } = options
if ((!value || (Array.isArray(value) && value.length === 0)) && required) {
return t('validation:required')
}
if (Array.isArray(value) && value.length > 0) {
if (minRows && value.length < minRows) {
return t('validation:lessThanMin', { label: t('rows'), min: minRows, value: value.length })
}
if (maxRows && value.length > maxRows) {
return t('validation:greaterThanMax', { label: t('rows'), max: maxRows, value: value.length })
}
}
if (!canUseDOM && typeof value !== 'undefined' && value !== null) {
const values = Array.isArray(value) ? value : [value]
const invalidRelationships = values.filter((val) => {
let collectionSlug: string
let requestedID
if (typeof relationTo === 'string') {
collectionSlug = relationTo
// custom id
if (val) {
requestedID = val
}
}
if (Array.isArray(relationTo) && typeof val === 'object' && val?.relationTo) {
collectionSlug = val.relationTo
requestedID = val.value
}
if (requestedID === null) return false
const idField = config?.collections
?.find((collection) => collection.slug === collectionSlug)
?.fields?.find((field) => fieldAffectsData(field) && field.name === 'id')
const type = getIDType(idField, payload?.db?.defaultIDType)
return !isValidID(requestedID, type)
})
if (invalidRelationships.length > 0) {
return `This relationship field has the following invalid relationships: ${invalidRelationships
.map((err, invalid) => {
return `${err} ${JSON.stringify(invalid)}`
})
.join(', ')}`
}
}
return validateFilterOptions(value, options)
}
export const select: Validate<unknown, unknown, SelectField> = (
value,
{ hasMany, options, required, t },
) => {
if (
Array.isArray(value) &&
value.some(
(input) =>
!options.some(
(option) => option === input || (typeof option !== 'string' && option?.value === input),
),
)
) {
return t('validation:invalidSelection')
}
if (
typeof value === 'string' &&
!options.some(
(option) => option === value || (typeof option !== 'string' && option.value === value),
)
) {
return t('validation:invalidSelection')
}
if (
required &&
(typeof value === 'undefined' ||
value === null ||
(hasMany && Array.isArray(value) && (value as [])?.length === 0))
) {
return t('validation:required')
}
return true
}
export const radio: Validate<unknown, unknown, RadioField> = (value, { options, required, t }) => {
if (value) {
const valueMatchesOption = options.some(
(option) => option === value || (typeof option !== 'string' && option.value === value),
)
return valueMatchesOption || t('validation:invalidSelection')
}
return required ? t('validation:required') : true
}
export const point: Validate<unknown, unknown, PointField> = (
value: [number | string, number | string] = ['', ''],
{ required, t },
) => {
const lng = parseFloat(String(value[0]))
const lat = parseFloat(String(value[1]))
if (
required &&
((value[0] && value[1] && typeof lng !== 'number' && typeof lat !== 'number') ||
Number.isNaN(lng) ||
Number.isNaN(lat) ||
(Array.isArray(value) && value.length !== 2))
) {
return t('validation:requiresTwoNumbers')
}
if ((value[1] && Number.isNaN(lng)) || (value[0] && Number.isNaN(lat))) {
return t('validation:invalidInput')
}
return true
}
export default {
array,
blocks,
checkbox,
code,
date,
email,
json,
number,
password,
point,
radio,
relationship,
richText,
select,
text,
textarea,
upload,
}