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

239 lines (216 loc) • 8.83 kB
import {type SanityClient} from '@sanity/client' import {DEFAULT_MAX_FIELD_DEPTH} from '@sanity/schema/_internal' import {type ReferenceFilterSearchOptions, type ReferenceSchemaType} from '@sanity/types' import {combineLatest, type Observable, of} from 'rxjs' import {map, mergeMap, startWith, switchMap} from 'rxjs/operators' import {type DocumentPreviewStore, getPreviewPaths, prepareForPreview} from '../../../../preview' import {createSearch} from '../../../../search' import {collate, type CollatedHit, getDraftId, getIdPair, isRecord} from '../../../../util' import {type ReferenceInfo, type ReferenceSearchHit} from '../../../inputs/ReferenceInput/types' const READABLE = { available: true, reason: 'READABLE', } as const const PERMISSION_DENIED = { available: false, reason: 'PERMISSION_DENIED', } as const const NOT_FOUND = { available: false, reason: 'NOT_FOUND', } as const /** * Takes an id and a reference schema type, returns metadata about it */ export function getReferenceInfo( documentPreviewStore: DocumentPreviewStore, id: string, referenceType: ReferenceSchemaType, ): Observable<ReferenceInfo> { const {publishedId, draftId} = getIdPair(id) const pairAvailability$ = documentPreviewStore.unstable_observeDocumentPairAvailability(id) return pairAvailability$.pipe( switchMap((pairAvailability) => { if (!pairAvailability.draft.available && !pairAvailability.published.available) { // combine availability of draft + published const availability = pairAvailability.draft.reason === 'PERMISSION_DENIED' || pairAvailability.published.reason === 'PERMISSION_DENIED' ? PERMISSION_DENIED : NOT_FOUND // short circuit, neither draft nor published is available so no point in trying to get preview return of({ id, type: undefined, availability, preview: { draft: undefined, published: undefined, }, } as const) } const draftRef = {_type: 'reference', _ref: draftId} const publishedRef = {_type: 'reference', _ref: publishedId} const typeName$ = combineLatest([ documentPreviewStore.observeDocumentTypeFromId(draftId), documentPreviewStore.observeDocumentTypeFromId(publishedId), ]).pipe( // assume draft + published are always same type map(([draftTypeName, publishedTypeName]) => draftTypeName || publishedTypeName), ) return typeName$.pipe( switchMap((typeName) => { if (!typeName) { // we have already asserted that either the draft or the published document is readable, so // if we get here we can't read the _type, so we're likely to be in an inconsistent state // waiting for an update to reach the client. Since we're in the context of a reactive stream based on // the _type we'll get it eventually return of({ id, type: undefined, availability: {available: true, reason: 'READABLE'}, preview: { draft: undefined, published: undefined, }, } as const) } // get schema type for the referenced document const refSchemaType = referenceType.to.find((memberType) => memberType.name === typeName)! if (!refSchemaType) { return of({ id, type: typeName, availability: {available: true, reason: 'READABLE'}, preview: { draft: undefined, published: undefined, }, } as const) } const previewPaths = getPreviewPaths(refSchemaType?.preview) || [] const draftPreview$ = documentPreviewStore.observePaths(draftRef, previewPaths).pipe( map((result) => result ? { _id: draftId, ...prepareForPreview(result, refSchemaType), } : undefined, ), startWith(undefined), ) const publishedPreview$ = documentPreviewStore .observePaths(publishedRef, previewPaths) .pipe( map((result) => result ? { _id: publishedId, ...prepareForPreview(result, refSchemaType), } : undefined, ), startWith(undefined), ) const value$ = combineLatest([draftPreview$, publishedPreview$]).pipe( map(([draft, published]) => ({draft, published})), ) return value$.pipe( map((value): ReferenceInfo => { const availability = // eslint-disable-next-line no-nested-ternary pairAvailability.draft.available || pairAvailability.published.available ? READABLE : pairAvailability.draft.reason === 'PERMISSION_DENIED' || pairAvailability.published.reason === 'PERMISSION_DENIED' ? PERMISSION_DENIED : NOT_FOUND return { type: typeName, id: publishedId, availability, preview: { draft: isRecord(value.draft) ? value.draft : undefined, published: isRecord(value.published) ? value.published : undefined, }, } }), ) }), ) }), ) } /** * when we get a search result it may not include all [draft, published] id pairs for documents matching the * query. For example: searching for "potato" may yield a hit in the draft, but not the published (or vice versa) * * This method takes a list of collated search hits and returns an array of the missing "counterpart" ids */ function getCounterpartIds(collatedHits: CollatedHit[]): string[] { return collatedHits .filter( (collatedHit) => // we're interested in hits where either draft or published is missing !collatedHit.draft || !collatedHit.published, ) .map((collatedHit) => // if we have the draft, return the published id or vice versa collatedHit.draft ? collatedHit.id : getDraftId(collatedHit.id), ) } function getExistingCounterparts(client: SanityClient, ids: string[]) { return ids.length === 0 ? of([]) : client.observable.fetch(`*[_id in $ids]._id`, {ids}, {tag: 'get-counterpart-ids'}) } export function referenceSearch( client: SanityClient, textTerm: string, type: ReferenceSchemaType, options: ReferenceFilterSearchOptions, unstable_enableNewSearch: boolean, ): Observable<ReferenceSearchHit[]> { const search = createSearch(type.to, client, { ...options, unstable_enableNewSearch, maxDepth: options.maxFieldDepth || DEFAULT_MAX_FIELD_DEPTH, }) return search(textTerm, {includeDrafts: true}).pipe( map(({hits}) => hits.map(({hit}) => hit)), map(collate), // pick the 100 best matches map((collated) => collated.slice(0, 100)), mergeMap((collated) => { // Note: It might seem like this step is redundant, but it's here for a reason: // The list of search hits returned from here will be passed as options to the reference input's autocomplete. When // one of them gets selected by the user, it will then be passed as the argument to the `onChange` handler in the // Reference Input. This handler will then look at the passed value to determine whether to make a link to a // draft (using _strengthenOnPublish) or a published document. // // Without this step, in a case where both a draft and a published version exist but only the draft matches // the search term, we'd end up making a reference with `_strengthenOnPublish: true`, when we instead should be // making a normal reference to the published id return getExistingCounterparts(client, getCounterpartIds(collated)).pipe( map((existingCounterpartIds) => { return collated.map((entry) => { const draftId = getDraftId(entry.id) return { id: entry.id, type: entry.type, draft: entry.draft || existingCounterpartIds.includes(draftId) ? {_id: draftId, _type: entry.type} : undefined, published: entry.published || existingCounterpartIds.includes(entry.id) ? {_id: entry.id, _type: entry.type} : undefined, } }) }), ) }), ) }