UNPKG

payload

Version:

Node, React, Headless CMS and Application Framework built on Next.js

624 lines (623 loc) • 22.9 kB
// @ts-strict-ignore import Ajv from 'ajv'; import ObjectIdImport from 'bson-objectid'; const ObjectId = ObjectIdImport.default || ObjectIdImport; import { isNumber } from '../utilities/isNumber.js'; import { isValidID } from '../utilities/isValidID.js'; export const text = (value, { hasMany, maxLength: fieldMaxLength, maxRows, minLength, minRows, req: { payload: { config }, t }, required })=>{ let maxLength; if (!required) { if (!value) { return true; } } if (hasMany === true) { const lengthValidationResult = validateArrayLength(value, { maxRows, minRows, required, t }); if (typeof lengthValidationResult === 'string') { return lengthValidationResult; } } if (typeof config?.defaultMaxTextLength === 'number') { maxLength = config.defaultMaxTextLength; } if (typeof fieldMaxLength === 'number') { maxLength = fieldMaxLength; } const stringsToValidate = Array.isArray(value) ? value : [ value ]; for (const stringValue of stringsToValidate){ const length = stringValue?.length || 0; if (typeof maxLength === 'number' && length > maxLength) { return t('validation:shorterThanMax', { label: t('general:value'), maxLength, stringValue }); } if (typeof minLength === 'number' && length < minLength) { return t('validation:longerThanMin', { label: t('general:value'), minLength, stringValue }); } } if (required) { if (!(typeof value === 'string' || Array.isArray(value)) || value?.length === 0) { return t('validation:required'); } } return true; }; export const password = (value, { maxLength: fieldMaxLength, minLength = 3, req: { payload: { config }, t }, required })=>{ let maxLength; 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 confirmPassword = (value, { req: { t }, required, siblingData })=>{ if (required && !value) { return t('validation:required'); } if (value && value !== siblingData.password) { return t('fields:passwordsDoNotMatch'); } return true; }; export const email = (value, { collectionSlug, req: { payload: { collections, config }, t }, required, siblingData })=>{ if (collectionSlug) { const collection = collections?.[collectionSlug]?.config ?? config.collections.find(({ slug })=>slug === collectionSlug) // If this is run on the client, `collections` will be undefined, but `config.collections` will be available ; if (collection.auth.loginWithUsername && !collection.auth.loginWithUsername?.requireUsername && !collection.auth.loginWithUsername?.requireEmail) { if (!value && !siblingData?.username) { return t('validation:required'); } } } /** * Disallows emails with double quotes (e.g., "user"@example.com, user@"example.com", "user@example.com") * Rejects spaces anywhere in the email (e.g., user @example.com, user@ example.com, user name@example.com) * Prevents consecutive dots in the local or domain part (e.g., user..name@example.com, user@example..com) * Disallows domains that start or end with a hyphen (e.g., user@-example.com, user@example-.com) * Allows standard email formats (e.g., user@example.com, user.name+alias@example.co.uk, user-name@example.org) * Allows domains with consecutive hyphens as long as they are not leading/trailing (e.g., user@ex--ample.com) * Supports multiple subdomains (e.g., user@sub.domain.example.com) */ const emailRegex = /^(?!.*\.\.)[\w.%+-]+@[a-z0-9](?:[a-z0-9-]*[a-z0-9])?(?:\.[a-z0-9](?:[a-z0-9-]*[a-z0-9])?)*\.[a-z]{2,}$/i; if (value && !emailRegex.test(value) || !value && required) { return t('validation:emailAddress'); } return true; }; export const username = (value, { collectionSlug, req: { payload: { collections, config }, t }, required, siblingData })=>{ let maxLength; if (collectionSlug) { const collection = collections?.[collectionSlug]?.config ?? config.collections.find(({ slug })=>slug === collectionSlug) // If this is run on the client, `collections` will be undefined, but `config.collections` will be available ; if (collection.auth.loginWithUsername && !collection.auth.loginWithUsername?.requireUsername && !collection.auth.loginWithUsername?.requireEmail) { if (!value && !siblingData?.email) { return t('validation:required'); } } } if (typeof config?.defaultMaxTextLength === 'number') { maxLength = config.defaultMaxTextLength; } if (value && maxLength && value.length > maxLength) { return t('validation:shorterThanMax', { maxLength }); } if (!value && required) { return t('validation:required'); } return true; }; export const textarea = (value, { maxLength: fieldMaxLength, minLength, req: { payload: { config }, t }, required })=>{ let maxLength; 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 = (value, { req: { t }, required })=>{ if (required && value === undefined) { return t('validation:required'); } return true; }; export const json = async (value, { jsonError, jsonSchema, req: { t }, required })=>{ const isNotEmpty = (value)=>{ if (value === undefined || value === null) { return false; } if (Array.isArray(value) && value.length === 0) { return false; } if (typeof value === 'object' && Object.keys(value).length === 0) { return false; } return true; }; const fetchSchema = ({ schema, uri })=>{ if (uri && schema) { return schema; } // @ts-expect-error return fetch(uri).then((response)=>{ if (!response.ok) { throw new Error('Network response was not ok'); } return response.json(); }).then((json)=>{ const jsonSchemaSanitizations = { id: undefined, $id: json.id, $schema: 'http://json-schema.org/draft-07/schema#' }; return Object.assign(json, jsonSchemaSanitizations); }); }; if (required && !value) { return t('validation:required'); } if (jsonError !== undefined) { return t('validation:invalidInput'); } if (jsonSchema && isNotEmpty(value)) { try { jsonSchema.schema = await fetchSchema(jsonSchema); const { schema } = jsonSchema; // @ts-expect-error const ajv = new Ajv(); if (!ajv.validate(schema, value)) { return ajv.errorsText(); } } catch (error) { return error.message; } } return true; }; export const checkbox = (value, { req: { t }, required })=>{ if (value && typeof value !== 'boolean' || required && typeof value !== 'boolean') { return t('validation:trueOrFalse'); } return true; }; export const date = (value, { name, req: { t }, required, siblingData, timezone })=>{ const validDate = value && !isNaN(Date.parse(value.toString())); // We need to also check for the timezone data based on this field's config // We cannot do this inside the timezone field validation as it's visually hidden const hasRequiredTimezone = timezone && required; const selectedTimezone = siblingData?.[`${name}_tz`]; // Always resolve to true if the field is not required, as timezone may be optional too then const validTimezone = hasRequiredTimezone ? Boolean(selectedTimezone) : true; if (validDate && validTimezone) { return true; } if (validDate && !validTimezone) { return t('validation:timezoneRequired'); } if (value) { return t('validation:notValidDate', { value }); } if (required) { return t('validation:required'); } return true; }; export const richText = async (value, options)=>{ if (!options?.editor) { throw new Error('richText field has no editor property.'); } if (typeof options?.editor === 'function') { throw new Error('Attempted to access unsanitized rich text editor.'); } const editor = options?.editor; return editor.validate(value, options); }; const validateArrayLength = (value, options)=>{ const { maxRows, minRows, required, t } = options; const arrayLength = Array.isArray(value) ? value.length : value || 0; if (!required && arrayLength === 0) { return true; } if (minRows && arrayLength < minRows) { return t('validation:requiresAtLeast', { count: minRows, label: t('general:rows') }); } if (maxRows && arrayLength > maxRows) { return t('validation:requiresNoMoreThan', { count: maxRows, label: t('general:rows') }); } if (required && !arrayLength) { return t('validation:requiresAtLeast', { count: 1, label: t('general:row') }); } return true; }; export const number = (value, { hasMany, max, maxRows, min, minRows, req: { t }, required })=>{ 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 = Array.isArray(value) ? value : [ value ]; for (const number of numbersToValidate){ if (!isNumber(number)) { return t('validation:enterNumber'); } const numberValue = parseFloat(number); if (typeof max === 'number' && numberValue > max) { return t('validation:greaterThanMax', { label: t('general:value'), max, value }); } if (typeof min === 'number' && numberValue < min) { return t('validation:lessThanMin', { label: t('general:value'), min, value }); } } return true; }; export const array = (value, { maxRows, minRows, req: { t }, required })=>{ return validateArrayLength(value, { maxRows, minRows, required, t }); }; export const blocks = (value, { maxRows, minRows, req: { t }, required })=>{ return validateArrayLength(value, { maxRows, minRows, required, t }); }; const validateFilterOptions = async (value, { id, blockData, data, filterOptions, relationTo, req, req: { payload, t, user }, siblingData })=>{ if (typeof filterOptions !== 'undefined' && value) { const options = {}; const falseCollections = []; const collections = !Array.isArray(relationTo) ? [ relationTo ] : relationTo; const values = Array.isArray(value) ? value : [ value ]; for (const collection of collections){ try { let optionFilter = typeof filterOptions === 'function' ? await filterOptions({ id, blockData, data, relationTo: collection, req, siblingData, user }) : filterOptions; if (optionFilter === true) { optionFilter = null; } const valueIDs = []; values.forEach((val)=>{ if (typeof val === 'object') { if (val?.value) { valueIDs.push(val.value); } else if (ObjectId.isValid(val)) { valueIDs.push(new ObjectId(val).toHexString()); } } if (typeof val === 'string' || typeof val === 'number') { valueIDs.push(val); } }); if (valueIDs.length > 0) { const findWhere = { and: [ { id: { in: valueIDs } } ] }; if (optionFilter && optionFilter !== true) { findWhere.and.push(optionFilter); } if (optionFilter === false) { falseCollections.push(collection); } const result = await req.payloadDataLoader.find({ collection, depth: 0, limit: 0, pagination: false, req, where: findWhere }); options[collection] = result.docs.map((doc)=>doc.id); } else { options[collection] = []; } } catch (err) { req.payload.logger.error({ err, msg: `Error validating filter options for collection ${collection}` }); options[collection] = []; } } const invalidRelationships = values.filter((val)=>{ let collection; let requestedID; if (typeof relationTo === 'string') { collection = relationTo; if (typeof val === 'string' || typeof val === 'number') { requestedID = val; } if (typeof val === 'object' && ObjectId.isValid(val)) { requestedID = new ObjectId(val).toHexString(); } } if (Array.isArray(relationTo) && typeof val === 'object' && val?.relationTo) { collection = val.relationTo; requestedID = val.value; } if (falseCollections.find((slug)=>relationTo === slug)) { return true; } if (!options[collection]) { return true; } 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')); } return true; } return true; }; export const upload = async (value, options)=>{ const { event, maxRows, minRows, relationTo, req: { payload, t }, required } = options; if ((!value && typeof value !== 'number' || 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('general:rows'), min: minRows, value: value.length }); } if (maxRows && value.length > maxRows) { return t('validation:greaterThanMax', { label: t('general:rows'), max: maxRows, value: value.length }); } } if (typeof value !== 'undefined' && value !== null) { const values = Array.isArray(value) ? value : [ value ]; const invalidRelationships = values.filter((val)=>{ let collectionSlug; let requestedID; if (typeof relationTo === 'string') { collectionSlug = relationTo; // custom id if (val || typeof val === 'number') { requestedID = val; } } if (Array.isArray(relationTo) && typeof val === 'object' && val?.relationTo) { collectionSlug = val.relationTo; requestedID = val.value; } if (requestedID === null) { return false; } const idType = payload.collections[collectionSlug]?.customIDType || payload?.db?.defaultIDType || 'text'; return !isValidID(requestedID, idType); }); if (invalidRelationships.length > 0) { return `This relationship field has the following invalid relationships: ${invalidRelationships.map((err, invalid)=>{ return `${err} ${JSON.stringify(invalid)}`; }).join(', ')}`; } } if (event === 'onChange') { return true; } return validateFilterOptions(value, options); }; export const relationship = async (value, options)=>{ const { event, maxRows, minRows, relationTo, req: { payload, t }, required } = options; if ((!value && typeof value !== 'number' || 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('general:rows'), min: minRows, value: value.length }); } if (maxRows && value.length > maxRows) { return t('validation:greaterThanMax', { label: t('general:rows'), max: maxRows, value: value.length }); } } if (typeof value !== 'undefined' && value !== null) { const values = Array.isArray(value) ? value : [ value ]; const invalidRelationships = values.filter((val)=>{ let collectionSlug; let requestedID; if (typeof relationTo === 'string') { collectionSlug = relationTo; // custom id if (val || typeof val === 'number') { requestedID = val; } } if (Array.isArray(relationTo) && typeof val === 'object' && val?.relationTo) { collectionSlug = val.relationTo; requestedID = val.value; } if (requestedID === null) { return false; } const idType = payload.collections[collectionSlug]?.customIDType || payload?.db?.defaultIDType || 'text'; return !isValidID(requestedID, idType); }); if (invalidRelationships.length > 0) { return `This relationship field has the following invalid relationships: ${invalidRelationships.map((err, invalid)=>{ return `${err} ${JSON.stringify(invalid)}`; }).join(', ')}`; } } if (event === 'onChange') { return true; } return validateFilterOptions(value, options); }; export const select = (value, { hasMany, options, req: { t }, required })=>{ 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?.length === 0)) { return t('validation:required'); } return true; }; export const radio = (value, { options, req: { t }, required })=>{ 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 = (value = [ '', '' ], { req: { t }, required })=>{ 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; }; /** * Built-in field validations used by Payload * * These can be re-used in custom validations */ export const validations = { array, blocks, checkbox, code, confirmPassword, date, email, json, number, password, point, radio, relationship, richText, select, text, textarea, upload }; //# sourceMappingURL=validations.js.map