@sanity/schema
Version:
224 lines (202 loc) • 7.99 kB
text/typescript
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,
}
}