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
186 lines (155 loc) • 5.5 kB
text/typescript
import {type Schema} from '@sanity/types'
import {randomKey} from '@sanity/util/content'
import {toString as pathToString} from '@sanity/util/paths'
import {type Template, type TemplateParameter} from './types'
import {isRecord} from './util/isRecord'
const ALLOWED_REF_PROPS = [
'_dataset',
'_projectId',
'_strengthenOnPublish',
'_key',
'_ref',
'_type',
'_weak',
]
const REQUIRED_TEMPLATE_PROPS: (keyof Template)[] = ['id', 'title', 'schemaType', 'value']
function templateId(template: Template, i: number) {
return quote(template.id || template.title) || (typeof i === 'number' && `at index ${i}`) || ''
}
function quote(str: string) {
return str && str.length > 0 ? `"${str}"` : str
}
export function validateTemplates(schema: Schema, templates: Template[]): Template[] {
const idMap = new Map()
templates.forEach((template, i) => {
const id = templateId(template, i)
if (typeof (template as {[key: string]: any}).values !== 'undefined' && !template.value) {
throw new Error(`Template ${id} is missing "value" property, but contained "values". Typo?`)
}
const missing = REQUIRED_TEMPLATE_PROPS.filter((prop) => !template[prop])
if (missing.length > 0) {
throw new Error(`Template ${id} is missing required properties: ${missing.join(', ')}`)
}
if (typeof template.value !== 'function' && !isRecord(template.value)) {
throw new Error(
`Template ${id} has an invalid "value" property; should be a function or an object`,
)
}
if (typeof template.parameters !== 'undefined') {
if (Array.isArray(template.parameters)) {
template.parameters.forEach((param, j) => validateParameter(schema, param, template, j))
} else {
throw new Error(`Template ${id} has an invalid "parameters" property; must be an array`)
}
}
if (idMap.has(template.id)) {
const dupeIndex = idMap.get(template.id)
const dupe = `${quote(templates[dupeIndex].title)} at index ${dupeIndex}`
throw new Error(
`Template "${template.title}" at index ${i} has the same ID ("${template.id}") as template ${dupe}`,
)
}
idMap.set(template.id, i)
})
return templates
}
export function validateInitialObjectValue<T extends Record<string, unknown>>(
value: T,
template: Template,
): T {
const contextError = (msg: string) => `Template "${template.id}" initial value: ${msg}`
if (!isRecord(value)) {
throw new Error(contextError(`resolved to a non-object`))
}
if (value._type && template.schemaType !== value._type) {
throw new Error(
contextError(
`includes "_type"-property (${value._type}) that does not match template (${template.schemaType})`,
),
)
}
try {
return validateValue(value)
} catch (err) {
err.message = contextError(err.message)
throw err
}
}
function validateValue(value: unknown, path: (string | number)[] = [], parentIsArray = false): any {
if (Array.isArray(value)) {
return value.map((item, i) => {
if (Array.isArray(item)) {
throw new Error(
`multidimensional arrays are not supported (at path "${pathToString(path)}")`,
)
}
return validateValue(item, path.concat(i), true)
})
}
if (!isRecord(value)) {
return value
}
// Apply missing keys is the parent is an array
const initial: {[key: string]: any} = parentIsArray && !value._key ? {_key: randomKey()} : {}
// Ensure non-root objects have _type
if (path.length > 0 && !value._type) {
if (value._ref) {
// In the case of references, we know what the type should be, so apply it
initial._type = 'reference'
} else {
// todo: consider if we need to re-instantiate this. It currently makes the valid case of having an initial object value for a field fail
// throw new Error(`missing "_type" property at path "${pathToString(path)}"`)
}
}
if (value._ref) {
validateReference(value, path)
}
// Validate deeply
return Object.keys(value).reduce((acc, key) => {
acc[key] = validateValue(value[key], path.concat([key]))
return acc
}, initial)
}
function validateParameter(
schema: Schema,
parameter: TemplateParameter,
template: Template,
index: number,
) {
// const schema = getDefaultSchema()
if (!parameter.name) {
throw new Error(
`Template ${template.id} has a parameter at index ${index} that is missing its "name"-property`,
)
}
// I know, this is a weird one
if (parameter.name === 'template') {
throw new Error(
`Template parameters cannot be named "template", see parameter #${index} for template ${template.id}`,
)
}
if (!schema.get(parameter.type)) {
throw new Error(
`Template parameter "${parameter.name}" has an invalid/unknown type: "${parameter.type}"`,
)
}
}
function validateReference(
value: {_type?: unknown; type?: unknown},
path: (string | number)[] = [],
) {
if (!value._type && value.type) {
throw new Error(
`Reference is missing "_type", but has a "type" property at path "${pathToString(path)}"`,
)
}
const disallowed = Object.keys(value).filter((key) => !ALLOWED_REF_PROPS.includes(key))
if (disallowed.length > 0) {
const plural = disallowed.length > 1 ? 'properties' : 'property'
throw new Error(
`Disallowed ${plural} found in reference: ${disallowed
.map(quote)
.join(', ')} at path "${pathToString(path)}"`,
)
}
}