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
123 lines (101 loc) • 3.7 kB
text/typescript
import {type Rule, type RuleTypeConstraint, type SchemaType} from '@sanity/types'
import {Rule as RuleClass} from '../Rule'
import {slugValidator} from '../validators/slugValidator'
const ruleConstraintTypes: {[P in Lowercase<RuleTypeConstraint>]: true} = {
array: true,
boolean: true,
date: true,
number: true,
object: true,
string: true,
}
const isRuleConstraint = (typeString: string): typeString is Lowercase<RuleTypeConstraint> =>
typeString in ruleConstraintTypes
export function getTypeChain(
type: SchemaType | undefined,
visited: Set<SchemaType> = new Set(),
): SchemaType[] {
if (!type) return []
if (visited.has(type)) return []
visited.add(type)
const next = type.type ? getTypeChain(type.type, visited) : []
return [...next, type]
}
function baseRuleReducer(inputRule: Rule, type: SchemaType) {
let baseRule = inputRule
if (isRuleConstraint(type.jsonType)) {
baseRule = baseRule.type(type.jsonType)
}
const typeOptionsList =
// if type.options is truthy
type?.options &&
// and type.options is an object (non-null from the previous)
typeof type.options === 'object' &&
// and if `list` is in options
'list' in type.options &&
// then finally access the list
type.options.list
if (Array.isArray(typeOptionsList)) {
baseRule = baseRule.valid(
typeOptionsList.map((option) => extractValueFromListOption(option, type)),
)
}
if (type.name === 'datetime') return baseRule.type('Date')
if (type.name === 'date') return baseRule.type('Date')
if (type.name === 'url') return baseRule.uri()
if (type.name === 'slug') return baseRule.custom(slugValidator, {bypassConcurrencyLimit: true})
if (type.name === 'reference') return baseRule.reference()
if (type.name === 'email') return baseRule.email()
return baseRule
}
function hasValueField(typeDef: SchemaType | undefined): boolean {
if (!typeDef) return false
if (!('fields' in typeDef) && typeDef.type) return hasValueField(typeDef.type)
if (!('fields' in typeDef)) return false
if (!Array.isArray(typeDef.fields)) return false
return typeDef.fields.some((field) => field.name === 'value')
}
function extractValueFromListOption(option: unknown, typeDef: SchemaType): unknown {
// If you define a `list` option with object items, where the item has a `value` field,
// we don't want to treat that as the value but rather the surrounding object
// This differs from the case where you have a title/value pair setup for a string/number, for instance
if (typeDef.jsonType === 'object' && hasValueField(typeDef)) return option
return (option as Record<string, unknown>).value === undefined
? option
: (option as Record<string, unknown>).value
}
/**
* Takes in `SchemaValidationValue` and returns an array of `Rule` instances.
*/
export function normalizeValidationRules(typeDef: SchemaType | undefined): Rule[] {
if (!typeDef) {
return []
}
const validation = typeDef.validation
if (Array.isArray(validation)) {
return validation.flatMap((i) =>
normalizeValidationRules({
...typeDef,
validation: i,
}),
)
}
if (validation && typeof validation === 'object') {
return [validation]
}
const baseRule =
// using an object + Object.values to de-dupe the type chain by type name
Object.values(
getTypeChain(typeDef).reduce<Record<string, SchemaType>>((acc, type) => {
acc[type.name] = type
return acc
}, {}),
).reduce(baseRuleReducer, new RuleClass(typeDef))
if (!validation) {
return [baseRule]
}
return normalizeValidationRules({
...typeDef,
validation: validation(baseRule),
})
}