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

388 lines (339 loc) • 11.3 kB
import {type SchemaType} from '@sanity/types' import {uniq} from 'lodash' import {type I18nTextRecord} from 'sanity' import {type ChildResolver} from './ChildResolver' import {HELP_URL, SerializeError} from './SerializeError' import { type Child, type DocumentNode, type EditorNode, type Serializable, type SerializeOptions, } from './StructureNodes' import {type StructureContext, type View} from './types' import {getStructureNodeId} from './util/getStructureNodeId' import {resolveTypeForDocument} from './util/resolveTypeForDocument' import {validateId} from './util/validateId' import {form} from './views' import {maybeSerializeView, type ViewBuilder} from './views/View' const createDocumentChildResolver = ({resolveDocumentNode, getClient}: StructureContext): ChildResolver => async (itemId, {params, path}) => { let type = params.type const parentPath = path.slice(0, path.length - 1) const currentSegment = path[path.length - 1] if (!type) { type = await resolveTypeForDocument(getClient, itemId) } if (!type) { throw new SerializeError( `Failed to resolve document, and no type provided in parameters.`, parentPath, currentSegment, ) } return resolveDocumentNode({documentId: itemId, schemaType: type}) } /** * Interface for options of Partial Documents. See {@link PartialDocumentNode} * * @public */ export interface DocumentOptions { /** Document Id */ id: string /** Document Type */ type: string /** Document Template */ template?: string /** Template parameters */ templateParameters?: Record<string, unknown> } /** * Interface for partial document (focused on the document pane) * * @public */ export interface PartialDocumentNode { /** Document Id */ id?: string /** Document title */ title?: string /** I18n key and namespace used to populate the localized title */ i18n?: I18nTextRecord<'title'> /** Document children of type {@link Child} */ child?: Child /** * Views for the document pane. See {@link ViewBuilder} and {@link View} */ views?: (View | ViewBuilder)[] /** * Document options. See {@link DocumentOptions} */ options?: Partial<DocumentOptions> } /** * A `DocumentBuilder` is used to build a document node. * * @public */ export class DocumentBuilder implements Serializable<DocumentNode> { /** Component builder option object See {@link PartialDocumentNode} */ protected spec: PartialDocumentNode constructor( /** * Structure context. See {@link StructureContext} */ protected _context: StructureContext, spec?: PartialDocumentNode, ) { this.spec = spec ? spec : {} } /** Set Document Builder ID * @param id - document builder ID * @returns document builder based on ID provided. See {@link DocumentBuilder} */ id(id: string): DocumentBuilder { return this.clone({id}) } /** Get Document Builder ID * @returns document ID. See {@link PartialDocumentNode} */ getId(): PartialDocumentNode['id'] { return this.spec.id } /** Set Document title * @param title - document title * @returns document builder based on title provided (and ID). See {@link DocumentBuilder} */ title(title: string): DocumentBuilder { return this.clone({title, id: getStructureNodeId(title, this.spec.id)}) } /** Get Document title * @returns document title. See {@link PartialDocumentNode} */ getTitle(): PartialDocumentNode['title'] { return this.spec.title } /** Set the i18n key and namespace used to populate the localized title. * @param i18n - the key and namespaced used to populate the localized title. * @returns component builder based on i18n key and ns provided */ i18n(i18n: I18nTextRecord<'title'>): DocumentBuilder { return this.clone({i18n}) } /** Get i18n key and namespace used to populate the localized title * @returns the i18n key and namespace used to populate the localized title */ getI18n(): I18nTextRecord<'title'> | undefined { return this.spec.i18n } /** Set Document child * @param child - document child * @returns document builder based on child provided. See {@link DocumentBuilder} */ child(child: Child): DocumentBuilder { return this.clone({child}) } /** Get Document child * @returns document child. See {@link PartialDocumentNode} */ getChild(): PartialDocumentNode['child'] { return this.spec.child } /** Set Document ID * @param documentId - document ID * @returns document builder with document based on ID provided. See {@link DocumentBuilder} */ documentId(documentId: string): DocumentBuilder { // Let's try to be a bit helpful and assign an ID from document ID if none is specified const paneId = this.spec.id || documentId return this.clone({ id: paneId, options: { ...(this.spec.options || {}), id: documentId, }, }) } /** Get Document ID * @returns document ID. See {@link DocumentOptions} */ getDocumentId(): Partial<DocumentOptions>['id'] { return this.spec.options?.id } /** Set Document Type * @param documentType - document type * @returns document builder with document based on type provided. See {@link DocumentBuilder} */ schemaType(documentType: SchemaType | string): DocumentBuilder { return this.clone({ options: { ...(this.spec.options || {}), type: typeof documentType === 'string' ? documentType : documentType.name, }, }) } /** Get Document Type * @returns document type. See {@link DocumentOptions} */ getSchemaType(): Partial<DocumentOptions>['type'] { return this.spec.options?.type } /** Set Document Template * @param templateId - document template ID * @param parameters - document template parameters * @returns document builder with document based on template provided. See {@link DocumentBuilder} */ initialValueTemplate(templateId: string, parameters?: Record<string, unknown>): DocumentBuilder { return this.clone({ options: { ...(this.spec.options || {}), template: templateId, templateParameters: parameters, }, }) } /** Get Document Template * @returns document template. See {@link DocumentOptions} */ getInitialValueTemplate(): Partial<DocumentOptions>['template'] { return this.spec.options?.template } /** Get Document's initial value Template parameters * @returns document template parameters. See {@link DocumentOptions} */ getInitialValueTemplateParameters(): Partial<DocumentOptions>['templateParameters'] { return this.spec.options?.templateParameters } /** Set Document views * @param views - document views. See {@link ViewBuilder} and {@link View} * @returns document builder with document based on views provided. See {@link DocumentBuilder} */ views(views: (View | ViewBuilder)[]): DocumentBuilder { return this.clone({views}) } /** Get Document views * @returns document views. See {@link ViewBuilder} and {@link View} */ getViews(): (View | ViewBuilder)[] { return this.spec.views || [] } /** Serialize Document builder * @param options - serialization options. See {@link SerializeOptions} * @returns document node based on path, index and hint provided in options. See {@link DocumentNode} */ serialize({path = [], index, hint}: SerializeOptions = {path: []}): DocumentNode { const urlId = path[index || path.length - 1] // Try to grab document ID / editor ID from URL if not defined const id = this.spec.id || (urlId && `${urlId}`) || '' const options: Partial<DocumentOptions> = { id, type: undefined, template: undefined, templateParameters: undefined, ...this.spec.options, } if (typeof id !== 'string' || !id) { throw new SerializeError( '`id` is required for document nodes', path, index, hint, ).withHelpUrl(HELP_URL.ID_REQUIRED) } if (!options || !options.id) { throw new SerializeError( 'document id (`id`) is required for document nodes', path, id, hint, ).withHelpUrl(HELP_URL.DOCUMENT_ID_REQUIRED) } if (!options || !options.type) { throw new SerializeError( 'document type (`schemaType`) is required for document nodes', path, id, hint, ) } const views = (this.spec.views && this.spec.views.length > 0 ? this.spec.views : [form()]).map( (item, i) => maybeSerializeView(item, i, path), ) const viewIds = views.map((view) => view.id) const dupes = uniq(viewIds.filter((viewId, i) => viewIds.includes(viewId, i + 1))) if (dupes.length > 0) { throw new SerializeError( `document node has views with duplicate IDs: ${dupes.join(', ')}`, path, id, hint, ) } return { ...this.spec, child: this.spec.child || createDocumentChildResolver(this._context), id: validateId(id, path, index), type: 'document', options: getDocumentOptions(options), views, } } /** Clone Document builder * @param withSpec - partial document node specification used to extend the cloned builder. See {@link PartialDocumentNode} * @returns document builder based on context and spec provided. See {@link DocumentBuilder} */ clone(withSpec: PartialDocumentNode = {}): DocumentBuilder { const builder = new DocumentBuilder(this._context) const options = {...(this.spec.options || {}), ...(withSpec.options || {})} builder.spec = {...this.spec, ...withSpec, options} return builder } } function getDocumentOptions(spec: Partial<DocumentOptions>): DocumentOptions { const opts: DocumentOptions = { id: spec.id || '', type: spec.type || '*', } if (spec.template) { opts.template = spec.template } if (spec.templateParameters) { opts.templateParameters = spec.templateParameters } return opts } /** @internal */ export function documentFromEditor(context: StructureContext, spec?: EditorNode): DocumentBuilder { let doc = spec?.type ? // Use user-defined document fragment as base if possible context.resolveDocumentNode({schemaType: spec.type}) : // Fall back to plain old document builder new DocumentBuilder(context) if (!spec) return doc const {id, type, template, templateParameters} = spec.options doc = doc.id(spec.id).documentId(id) if (type) { doc = doc.schemaType(type) } if (template) { doc = doc.initialValueTemplate(template, templateParameters) } if (spec.child) { doc = doc.child(spec.child) } return doc } /** @internal */ export function documentFromEditorWithInitialValue( {resolveDocumentNode, templates}: StructureContext, templateId: string, parameters?: Record<string, unknown>, ): DocumentBuilder { const template = templates.find((t) => t.id === templateId) if (!template) { throw new Error(`Template with ID "${templateId}" not defined`) } return resolveDocumentNode({schemaType: template.schemaType}).initialValueTemplate( templateId, parameters, ) }