UNPKG

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
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), }) }