UNPKG

@sanity/schema

Version:
224 lines (202 loc) 7.99 kB
import humanizeList from 'humanize-list' import {flatten, partition} from 'lodash' import {coreTypeNames} from '../../coreTypes' import {error, HELP_IDS, warning} from '../createValidationResult' import {getDupes} from '../utils/getDupes' function isPrimitiveTypeName(typeName: any) { return typeName === 'string' || typeName === 'number' || typeName === 'boolean' } function isAssignable(typeName: any, type: any) { return (typeof type.name === 'string' ? type.name : type.type) === typeName } function quote(n: any) { return `"${n}"` } function pluralize(arr: unknown[], suf = 's') { return arr.length === 1 ? '' : suf } function format(value: unknown) { if (Array.isArray(value)) { return `array with ${value.length} entries` } if (typeof value === 'object' && value !== null) { return `object with keys ${humanizeList(Object.keys(value).map(quote))}` } return quote(value) } export default (typeDef: any, visitorContext: any) => { // name should already have been marked const ofIsArray = Array.isArray(typeDef.of) if (ofIsArray) { const invalid = typeDef.of.reduce((errs: any, def: any, idx: any) => { if (typeof def.name === 'string') { // If an array member has been given a "local" type name, we want to trigger an error if the given member type name // is one of the builtin types // // The following examples should be an error (where book is an existing root level type and reference is a built-in type): // - (…) of: [{type: 'book', name: 'image'}] // - (…) of: [{type: 'book', name: 'object'}] // - (…) of: [{type: 'object', name: 'reference'}] // The following examples are valid (where "address" is not defined as a global object type) // - (…) of: [{type: 'object', name: 'address'}] // The following examples are redundant, but should be allowed (at least for now) // - (…) of: [{type: 'object', name: 'object'}] // - (…) of: [{type: 'image', name: 'image'}] if ( // specifying the same name as the type is redundant, but should not be a hard error at this point // Consider showing a warning for this and deprecate this ability eventually def.name !== def.type && coreTypeNames.includes(def.name) ) { return errs.concat( error( `Found array member declaration with the same type name as a built-in type ("${def.name}"). Array members can not be given the same name as a built-in type.`, HELP_IDS.ARRAY_OF_TYPE_BUILTIN_TYPE_CONFLICT, ), ) } } if (def.type === 'object' && def.name && visitorContext.getType(def.name)) { return errs.concat( warning( `Found array member declaration with the same name as the global schema type "${def.name}". It's recommended to use a unique name to avoid possibly incompatible data types that shares the same name.`, HELP_IDS.ARRAY_OF_TYPE_GLOBAL_TYPE_CONFLICT, ), ) } if (def.type === 'array') { return errs.concat( error( `Found array member declaration of type "array" - multidimensional arrays are not currently supported by Sanity`, HELP_IDS.ARRAY_OF_ARRAY, ), ) } if (def) { return errs } const err = `Found ${def === null ? 'null' : typeof def}, expected member declaration` return errs.concat( error( `Found invalid type member declaration in array at index ${idx}: ${err}`, HELP_IDS.ARRAY_OF_INVALID, ), ) }, []) if (invalid.length > 0) { return { ...typeDef, of: [], _problems: invalid, } } } const problems = flatten([ ofIsArray ? getDupes(typeDef.of, (t) => `${t.name};${t.type}`).map((dupes) => error( `Found ${dupes.length} members with same type, but not unique names "${dupes[0].type}" in array. This makes it impossible to tell their values apart and you should consider naming them`, HELP_IDS.ARRAY_OF_NOT_UNIQUE, ), ) : error( 'The array type is missing or having an invalid value for the required "of" property', HELP_IDS.ARRAY_OF_INVALID, ), ]) const of = ofIsArray ? typeDef.of : [] // Don't allow object types without a name in block arrays const hasObjectTypesWithoutName = of.some( (type: any) => type.type === 'object' && typeof type.name === 'undefined', ) const hasBlockType = of.some((ofType: any) => ofType.type === 'block') if (hasBlockType && hasObjectTypesWithoutName) { problems.push( error( "The array type's 'of' property can't have an object type without a 'name' property as member, when the 'block' type is also a member of that array.", HELP_IDS.ARRAY_OF_INVALID, ), ) } const [primitiveTypes, objectTypes] = partition( of, (ofType) => isPrimitiveTypeName(ofType.type) || isPrimitiveTypeName(visitorContext.getType(ofType.type)?.jsonType), ) const isMixedArray = primitiveTypes.length > 0 && objectTypes.length > 0 if (isMixedArray) { const primitiveTypeNames = primitiveTypes.map((t) => t.type) const objectTypeNames = objectTypes.map((t) => t.type) problems.push( error( `The array type's 'of' property can't have both object types and primitive types (found primitive type ${pluralize( primitiveTypeNames, )} ${humanizeList(primitiveTypeNames.map(quote))} and object type${pluralize( objectTypeNames, )} ${humanizeList(objectTypeNames.map(quote))})`, HELP_IDS.ARRAY_OF_INVALID, ), ) } const list = typeDef?.options?.list if (!isMixedArray && Array.isArray(list)) { const isArrayOfPrimitives = primitiveTypes.length > 0 if (isArrayOfPrimitives) { list.forEach((option) => { const value = option?.value ?? option const isDeclared = primitiveTypes.some((primitiveType) => { return typeof value === visitorContext.getType(primitiveType.type).jsonType }) if (!isDeclared) { const formattedTypeList = humanizeList( primitiveTypes.map((t) => t.name || t.type), {conjunction: 'or'}, ) problems.push( error( `An invalid entry found in options.list: ${format( value, )}. Must be either a value of type ${formattedTypeList}, or an object with {title: string, value: ${formattedTypeList}}`, HELP_IDS.ARRAY_PREDEFINED_CHOICES_INVALID, ), ) } }) } else { list.forEach((option) => { const optionTypeName = option._type || 'object' const isDeclared = objectTypes.some((validObjectType) => isAssignable(optionTypeName, validObjectType), ) if (!isDeclared) { problems.push( error( `An invalid entry found in options.list: ${format( option, )}. Must be an object with "_type" set to ${humanizeList( objectTypes .map((t) => t.name || t.type) .map((t) => (t === 'object' ? 'undefined' : quote(t))), {conjunction: 'or'}, )}`, HELP_IDS.ARRAY_PREDEFINED_CHOICES_INVALID, ), ) } }) } } if (typeDef?.options?.list && typeDef?.options?.layout === 'tags') { problems.push( warning( 'Found array member declaration with both tags layout and a list of predefined values. If you want to display a list of predefined values, remove the tags layout from `options`.', ), ) } return { ...typeDef, of: of.map(visitorContext.visit), _problems: problems, } }