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

135 lines (113 loc) 4.07 kB
import {isCrossDatasetReference, isReference} from '@sanity/types' import {uniq} from 'lodash' import {type Observable, of as observableOf} from 'rxjs' import {switchMap} from 'rxjs/operators' import {isRecord} from '../util' import {type ApiConfig, type FieldName, type Previewable, type PreviewPath} from './types' import {props} from './utils/props' function createEmpty(fields: FieldName[]) { return fields.reduce((result: Record<string, undefined>, field) => { result[field] = undefined return result }, {}) } function resolveMissingHeads(value: Record<string, unknown>, paths: string[][]) { return paths.filter((path) => !(path[0] in value)) } function getDocumentId(value: Previewable) { if (isReference(value)) { return value._ref } return '_id' in value ? value._id : undefined } type ObserveFieldsFn = ( id: string, fields: FieldName[], apiConfig?: ApiConfig, ) => Observable<Record<string, unknown> | null> function observePaths( value: Previewable, paths: PreviewPath[], observeFields: ObserveFieldsFn, apiConfig?: ApiConfig, ): Observable<Record<string, unknown> | null> { if (!value || typeof value !== 'object') { // Reached a leaf. Return as is return observableOf(value as null) // @todo } const id = getDocumentId(value) const currentValue: Record<string, unknown> = id ? {...value, _id: id} : {...value} if (currentValue._type === 'reference') { delete currentValue._type delete currentValue._ref delete currentValue._weak delete currentValue._dataset delete currentValue._projectId delete currentValue._strengthenOnPublish } const pathsWithMissingHeads = resolveMissingHeads(currentValue, paths) if (id && pathsWithMissingHeads.length > 0) { // Reached a node that is either a document (with _id), or a reference (with _ref) that // needs to be "materialized" const nextHeads: string[] = uniq(pathsWithMissingHeads.map((path: string[]) => path[0])) const refApiConfig = isCrossDatasetReference(value) ? {projectId: value._projectId, dataset: value._dataset} : apiConfig return observeFields(id, nextHeads, refApiConfig).pipe( switchMap((snapshot) => { if (snapshot === null) { return observableOf(null) } return observePaths( { ...createEmpty(nextHeads), ...(isReference(value) ? {...value, ...refApiConfig} : value), ...snapshot, } as Previewable, paths, observeFields, refApiConfig, ) }), ) } // We have all the fields needed already present on value const leads: Record<string, string[][]> = {} paths.forEach((path) => { const [head, ...tail] = path if (!leads[head]) { leads[head] = [] } leads[head].push(tail) }) const next = Object.keys(leads).reduce((res: Record<string, unknown>, head) => { const tails = leads[head].filter((tail) => tail.length > 0) if (tails.length === 0) { res[head] = isRecord(value) ? (value as Record<string, unknown>)[head] : undefined } else { res[head] = observePaths((value as any)[head], tails, observeFields, apiConfig) } return res }, currentValue) return observableOf(next).pipe(props({wait: true})) } // Normalizes path arguments so it supports both dot-paths and array paths, e.g. // - ['propA.propB', 'propA.propC'] // - [['propA', 'propB'], ['propA', 'propC']] function normalizePaths(path: (FieldName | PreviewPath)[]): PreviewPath[] { return path.map((segment: FieldName | PreviewPath) => typeof segment === 'string' ? segment.split('.') : segment, ) } export function createPathObserver(context: {observeFields: ObserveFieldsFn}) { const {observeFields} = context return { observePaths( value: Previewable, paths: (FieldName | PreviewPath)[], apiConfig?: ApiConfig, ): Observable<Record<string, unknown> | null> { return observePaths(value, normalizePaths(paths), observeFields, apiConfig) }, } }