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
131 lines (112 loc) • 4 kB
text/typescript
import {type InitialValueResolverContext, type Schema} from '@sanity/types'
import {from, merge, type Observable, of} from 'rxjs'
import {
catchError,
debounceTime,
distinctUntilChanged,
filter,
map,
scan,
startWith,
switchMap,
} from 'rxjs/operators'
import {type DocumentPreviewStore} from '../../../../preview'
import {resolveInitialValue, type Template} from '../../../../templates'
import {getDraftId, getPublishedId} from '../../../../util'
import {
type InitialValueErrorMsg,
type InitialValueLoadingMsg,
type InitialValueMsg,
type InitialValueSuccessMsg,
} from './types'
/**
* @hidden
* @beta */
export interface InitialValueOptions {
documentId: string
documentType: string
templateName?: string
templateParams?: Record<string, any>
}
const LOADING_MSG: InitialValueLoadingMsg = {type: 'loading'}
/**
* @internal
*/
export function getInitialValueStream(
schema: Schema,
initialValueTemplates: Template[],
documentPreviewStore: DocumentPreviewStore,
opts: InitialValueOptions,
context: InitialValueResolverContext,
): Observable<InitialValueMsg> {
const draft$ = documentPreviewStore.observePaths(
{_type: 'reference', _ref: getDraftId(opts.documentId)},
['_type'],
)
const published$ = documentPreviewStore.observePaths(
{_type: 'reference', _ref: getPublishedId(opts.documentId)},
['_type'],
)
const value$ = merge(
draft$.pipe(map((draft) => ({draft}))),
published$.pipe(map((published) => ({published}))),
).pipe(
scan((prev, res) => ({...prev, ...res}), {}),
// Wait until we know the state of both draft and published
filter((res) => 'draft' in res && 'published' in res),
map((res: any) => res.draft || res.published),
// Only update if we didn't previously have a document but we now do
distinctUntilChanged((prev, next) => Boolean(prev) !== Boolean(next)),
// Prevent rapid re-resolving when transitioning between different templates
debounceTime(25),
)
return value$.pipe(
switchMap((document) => {
// Already exists, so no initial value is needed
if (document) {
return of({type: 'success', value: null})
}
if (!opts.templateName) {
// @todo: Make sure this is the correct behavior
return of({isResolving: false, initialValue: undefined})
}
const template = initialValueTemplates.find((t) => t.id === opts.templateName)
if (!template) {
// eslint-disable-next-line no-console
console.warn('Template "%s" not defined, using empty initial value', opts.templateName)
return of({isResolving: false, initialValue: undefined})
}
const initialValueWithParams$ = from(
resolveInitialValue(schema, template, opts.templateParams, context),
)
.pipe(map((initialValue) => ({isResolving: false, initialValue})))
.pipe(
catchError((resolveError) => {
/* eslint-disable no-console */
console.group('Failed to resolve initial value')
console.error(resolveError)
console.error('Template ID: %s', opts.templateName)
console.error('Parameters: %o', opts.templateParams)
console.groupEnd()
/* eslint-enable no-console */
const msg: InitialValueErrorMsg = {type: 'error', error: resolveError}
return of(msg)
}),
)
return merge(of({isResolving: true}), initialValueWithParams$).pipe(
switchMap(({isResolving, initialValue, resolveError}: any) => {
if (resolveError) {
return of({type: 'error', message: 'Failed to resolve initial value'})
}
if (isResolving) {
return of(LOADING_MSG)
}
const msg: InitialValueSuccessMsg = {type: 'success', value: initialValue}
return of(msg)
}),
)
}),
startWith(LOADING_MSG),
distinctUntilChanged(),
) as Observable<InitialValueMsg>
}