UNPKG

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

203 lines (173 loc) • 5.89 kB
import { type ArraySchemaType, type InitialValueProperty, type InitialValueResolver, type InitialValueResolverContext, isArraySchemaType, isObjectSchemaType, type ObjectSchemaType, type Schema, type SchemaType, } from '@sanity/types' import {isDeepEmpty, randomKey, resolveTypeName} from '@sanity/util/content' import {type Template} from './types' import deepAssign from './util/deepAssign' import {isRecord} from './util/isRecord' import {validateInitialObjectValue} from './validate' /** @internal */ export type Serializeable<T> = { serialize(): T } /** @internal */ export function isBuilder(template: unknown): template is Serializeable<Template> { return isRecord(template) && typeof template.serialize === 'function' } /** @internal */ // returns the "resolved" value from an initial value property (e.g. type.initialValue) // eslint-disable-next-line require-await export async function resolveValue<Params, InitialValue>( initialValueOpt: InitialValueProperty<Params, InitialValue>, params: Params | undefined, context: InitialValueResolverContext, ): Promise<InitialValue | undefined> { return typeof initialValueOpt === 'function' ? (initialValueOpt as InitialValueResolver<Params, InitialValue>)(params, context) : initialValueOpt } /** @internal */ export async function resolveInitialValue( schema: Schema, template: Template, params: {[key: string]: any} = {}, context: InitialValueResolverContext, ): Promise<{[key: string]: any}> { // Template builder? if (isBuilder(template)) { return resolveInitialValue(schema, template.serialize(), params, context) } const {id, schemaType: schemaTypeName, value} = template if (!value) { throw new Error(`Template "${id}" has invalid "value" property`) } let resolvedValue = await resolveValue(value, params, context) if (!isRecord(resolvedValue)) { throw new Error( `Template "${id}" has invalid "value" property - must be a plain object or a resolver function returning a plain object`, ) } // Ensure _type is set on empty objects if (isRecord(resolvedValue) && !Object.keys(resolvedValue).length) { resolvedValue = {_type: schemaTypeName} } // validate default document initial values resolvedValue = validateInitialObjectValue(resolvedValue, template) // Get deep initial values from schema types (note: the initial value from template overrides the types) const schemaType = schema.get(schemaTypeName) if (!schemaType) { throw new Error(`Could not find schema type with name "${schemaTypeName}".`) } const newValue = deepAssign( (await resolveInitialValueForType(schemaType, params, DEFAULT_MAX_RECURSION_DEPTH, context)) || {}, resolvedValue as Record<string, unknown>, ) // revalidate and return new initial values // todo: would be better to do validation as part of type resolution return validateInitialObjectValue(newValue, template) } /** @internal */ export function getItemType(arrayType: ArraySchemaType, item: unknown): SchemaType | undefined { const itemTypeName = resolveTypeName(item) return itemTypeName === 'object' && arrayType.of.length === 1 ? arrayType.of[0] : arrayType.of.find((memberType) => memberType.name === itemTypeName) } /** @internal */ export const DEFAULT_MAX_RECURSION_DEPTH = 10 /** * Resolve initial value for the given schema type (recursively) * * @internal */ export function resolveInitialValueForType<Params extends Record<string, unknown>>( /** * This is the name of the document. */ type: SchemaType, /** * Params is a sanity context object passed to every initial value function. */ params: Params, /** * Maximum recursion depth (default 9). */ maxDepth = DEFAULT_MAX_RECURSION_DEPTH, context: InitialValueResolverContext, ): Promise<any> { if (maxDepth <= 0) { return Promise.resolve(undefined) } if (isObjectSchemaType(type)) { return resolveInitialObjectValue(type, params, maxDepth, context) } if (isArraySchemaType(type)) { return resolveInitialArrayValue(type, params, maxDepth, context) } return resolveValue(type.initialValue, params, context) } async function resolveInitialArrayValue<Params extends Record<string, unknown>>( type: SchemaType, params: Params, maxDepth: number, context: InitialValueResolverContext, ): Promise<any> { const initialArray = await resolveValue(type.initialValue, undefined, context) if (!Array.isArray(initialArray)) { return undefined } return Promise.all( initialArray.map(async (initialItem) => { const itemType = getItemType(type as ArraySchemaType, initialItem)! return isObjectSchemaType(itemType) ? { ...initialItem, ...(await resolveInitialValueForType(itemType, params, maxDepth - 1, context)), _key: randomKey(), } : initialItem }), ) } /** @internal */ export async function resolveInitialObjectValue<Params extends Record<string, unknown>>( type: ObjectSchemaType, params: Params, maxDepth: number, context: InitialValueResolverContext, ): Promise<any> { const initialObject: Record<string, unknown> = { ...((await resolveValue(type.initialValue, params, context)) || {}), } const fieldValues: Record<string, any> = {} await Promise.all( type.fields.map(async (field) => { const initialFieldValue = await resolveInitialValueForType( field.type, params, maxDepth - 1, context, ) if (initialFieldValue !== undefined && initialFieldValue !== null) { fieldValues[field.name] = initialFieldValue } }), ) const merged = deepAssign(fieldValues, initialObject) if (isDeepEmpty(merged)) { return undefined } if (type.name !== 'object') { merged._type = type.name } return merged }