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