UNPKG

@sanity/schema

Version:
210 lines (185 loc) • 5.94 kB
import {isPlainObject} from 'lodash' import inspect from '../../inspect' import {error, HELP_IDS, warning} from '../createValidationResult' import {validateComponent} from '../utils/validateComponent' const VALID_FIELD_RE = /^[A-Za-z]+[0-9A-Za-z_]*$/ const CONVENTIONAL_FIELD_RE = /^[A-Za-z_]+[0-9A-Za-z_]*$/ interface Field { name: string } interface PreviewConfig { select?: { [key: string]: string } // eslint-disable-next-line @typescript-eslint/ban-types prepare?: Function } function validateFieldName(name: any): Array<any> { if (typeof name !== 'string') { return [ error( `Field names must be strings. Saw "${inspect(name)}"`, HELP_IDS.OBJECT_FIELD_NAME_INVALID, ), ] } if (name.startsWith('_')) { return [ error( `Invalid field name "${name}". Field names cannot start with underscores "_" as it's reserved for system fields.`, HELP_IDS.OBJECT_FIELD_NAME_INVALID, ), ] } if (!VALID_FIELD_RE.test(name)) { return [ error( `Invalid field name: "${name}". Fields can only contain characters from A-Z, numbers and underscores and should not start with a number (must pass the regular expression ${String( VALID_FIELD_RE, )}).`, HELP_IDS.OBJECT_FIELD_NAME_INVALID, ), ] } if (!CONVENTIONAL_FIELD_RE.test(name)) { return [ warning( 'Thats an interesting field name for sure! But it is... how to put it... a bit... unconventional?' + ' It may be wise to keep special characters out of field names for easier access later on.', ), HELP_IDS.OBJECT_FIELD_NAME_INVALID, ] } return [] } export function validateField(field: any, _visitorContext: any) { if (!isPlainObject(field)) { return [ error( `Incorrect type for field definition - should be an object, saw ${inspect(field)}`, HELP_IDS.OBJECT_FIELD_DEFINITION_INVALID_TYPE, ), ] } const problems = [] problems.push( ...('name' in field ? validateFieldName(field.name) : [error('Missing field name', HELP_IDS.OBJECT_FIELD_NAME_INVALID)]), ) problems.push(...validateComponent(field)) return problems } function getDuplicateFields(array: Array<Field>): Array<Array<Field> | null> { const dupes: {[name: string]: Array<Field>} = {} array.forEach((field) => { if (!dupes[field.name]) { dupes[field.name] = [] } dupes[field.name].push(field) }) return Object.keys(dupes) .map((fieldName) => (dupes[fieldName].length > 1 ? dupes[fieldName] : null)) .filter(Boolean) } export function validateFields(fields: any, options = {allowEmpty: false}) { const problems = [] const fieldsIsArray = Array.isArray(fields) if (!fieldsIsArray) { return [ error( `The "fields" property must be an array of fields. Instead saw "${typeof fields}"`, HELP_IDS.OBJECT_FIELDS_INVALID, ), ] } const fieldsWithNames = fields.filter((field) => typeof field.name === 'string') getDuplicateFields(fieldsWithNames).forEach((dupes) => { problems.push( error( `Found ${dupes!.length} fields with name "${dupes![0].name}" in object`, HELP_IDS.OBJECT_FIELD_NOT_UNIQUE, ), ) }) if (fields.length === 0 && !options.allowEmpty) { problems.push(error('Object should have at least one field', HELP_IDS.OBJECT_FIELDS_INVALID)) } const standaloneBlockFields = fields .filter((field) => field.type === 'block') .map((field) => `"${field.name}"`) if (standaloneBlockFields.length > 0) { const fmtFields = standaloneBlockFields.join(', ') problems.push( error( `Invalid standalone block field(s) ${fmtFields}. Block content must be defined as an array of blocks`, HELP_IDS.STANDALONE_BLOCK_TYPE, ), ) } return problems } export function validatePreview(preview: PreviewConfig) { if (!isPlainObject(preview)) { return [error(`The "preview" property must be an object, instead saw "${typeof preview}"`)] } if (typeof preview.prepare !== 'undefined' && typeof preview.prepare !== 'function') { return [ error( `The "preview.prepare" property must be a function, instead saw "${typeof preview.prepare}"`, ), ] } if (!preview.select) { return [] } if (!isPlainObject(preview.select)) { return [ error( `The "preview.select" property must be an object, instead saw "${typeof preview.prepare}"`, ), ] } return Object.keys(preview.select).reduce((errs: any, key) => { return typeof preview.select![key] === 'string' ? errs : errs.concat( error( `The key "${key}" of "preview.select" must be a string, instead saw "${typeof preview .select![key]}"`, ), ) }, []) } export default (typeDef: any, visitorContext: any) => { let problems = validateFields(typeDef.fields) let preview = typeDef.preview if (preview) { const previewErrors = validatePreview(typeDef.preview) problems = problems.concat(previewErrors) preview = previewErrors.some((err: any) => err.severity === 'error') ? {} : preview } if ( typeDef.type !== 'document' && typeDef.type !== 'object' && typeof typeDef.initialValue !== 'undefined' ) { problems.push( error(`The "initialValue" property is currently only supported for document & object types.`), ) } return { ...typeDef, preview, fields: (Array.isArray(typeDef.fields) ? typeDef.fields : []).map((field: any, index: any) => { const {name, ...fieldTypeDef} = field const {_problems, ...fieldType} = visitorContext.visit(fieldTypeDef, index) return { name, ...fieldType, _problems: validateField(field, visitorContext).concat(_problems || []), } }), _problems: problems, } }