sanity
Version:
Sanity is a real-time content infrastructure with a scalable, hosted backend featuring a Graph Oriented Query Language (GROQ), asset pipelines and fast edge caches
127 lines (103 loc) • 3.67 kB
text/typescript
import {type ValidationMarker, type Validators} from '@sanity/types'
import {type LocaleSource} from '../../i18n'
import {deepEquals} from '../util/deepEquals'
import {isLocalizedMessages, localizeMessage} from '../util/localizeMessage'
import {pathToString} from '../util/pathToString'
import {typeString} from '../util/typeString'
const SLOW_VALIDATOR_TIMEOUT = 5000
const formatValidationErrors = (options: {
message: string | undefined
results: ValidationMarker[]
operation: 'conjunction' | 'disjunction'
i18n: LocaleSource
}) => {
if (options.message) return options.message
if (options.results.length === 1) return options.results[0]?.message
// Intentionally hard-coded to use locale conjunction/disjunctions
return options.i18n.t('{{messages, list}}', {
messages: options.results.map((err) => err.message || err.item?.message),
formatParams: {messages: {style: 'long', type: options.operation}},
})
}
export const genericValidators: Validators = {
type: (expectedType, value, message, {i18n}) => {
const actualType = typeString(value)
if (actualType !== expectedType && actualType !== 'undefined') {
return message || i18n.t('validation:generic.incorrect-type', {actualType, expectedType})
}
return true
},
presence: (expected, value, message, {i18n}) => {
if (value === undefined && expected === 'required') {
return message || i18n.t('validation:generic.required')
}
return true
},
all: async (children, value, message, context) => {
const resolved = await Promise.all(children.map((child) => child.validate(value, context)))
const results = resolved.flat()
if (results.length === 0) {
return true
}
return formatValidationErrors({
message,
results,
operation: 'conjunction',
i18n: context.i18n,
})
},
either: async (children, value, message, context) => {
const resolved = await Promise.all(children.map((child) => child.validate(value, context)))
const results = resolved.flat()
// if one of the results is an empty array then at least one rule match
if (resolved.find((result) => !result.length)) {
return true
}
return formatValidationErrors({
message,
results,
operation: 'disjunction',
i18n: context.i18n,
})
},
valid: (allowedValues, actual, message, {i18n}) => {
const valueType = typeof actual
if (valueType === 'undefined') {
return true
}
const value = (valueType === 'number' || valueType === 'string') && `${actual}`
const strValue = value && value.length > 30 ? `${value.slice(0, 30)}…` : value
return allowedValues.some((expected) => deepEquals(expected, actual))
? true
: message ||
i18n.t(
'validation:generic.not-allowed',
value ? {context: 'hint', replace: {hint: strValue}} : {},
)
},
custom: async (fn, value, message, context) => {
const slowTimer = setTimeout(() => {
// only show this warning in the studio
if (context.environment !== 'studio') return
// eslint-disable-next-line no-console
console.warn(
`Custom validator at ${pathToString(
context.path,
)} has taken more than ${SLOW_VALIDATOR_TIMEOUT}ms to respond`,
)
}, SLOW_VALIDATOR_TIMEOUT)
let result
try {
result = await fn(value, context)
} finally {
clearTimeout(slowTimer)
}
if (isLocalizedMessages(result)) {
return localizeMessage(result, context.i18n)
}
if (typeof result === 'string') {
return message || result
}
return result
},
}