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

235 lines (216 loc) • 9.01 kB
import {type SanityClient, type StackablePerspective} 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, switchMap} from 'rxjs/operators' import {type DocumentPreviewStore, getPreviewStateObservable} from '../../../../preview' import {createSearch} from '../../../../search' import {collate, type CollatedHit, getDraftId, getIdPair} 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, {version, perspective}: {version?: string; perspective?: StackablePerspective[]} = {}, ): Observable<ReferenceInfo> { const {publishedId, draftId, versionId} = getIdPair(id, {version}) const pairAvailability$ = documentPreviewStore.unstable_observeDocumentPairAvailability(id, { version, }) return pairAvailability$.pipe( switchMap((pairAvailability) => { if ( !pairAvailability.draft.available && !pairAvailability.published.available && !pairAvailability.version?.available ) { // combine availability of draft + published const availability = pairAvailability.version?.reason === 'PERMISSION_DENIED' || pairAvailability.draft.reason === 'PERMISSION_DENIED' || pairAvailability.published.reason === 'PERMISSION_DENIED' ? PERMISSION_DENIED : NOT_FOUND // short circuit, neither draft nor published nor version is available so no point in trying to get preview return of({ id, type: undefined, availability, isPublished: null, preview: { snapshot: null, original: null, }, } as const) } const typeName$ = combineLatest([ documentPreviewStore.observeDocumentTypeFromId(draftId), documentPreviewStore.observeDocumentTypeFromId(publishedId), ...(versionId ? [documentPreviewStore.observeDocumentTypeFromId(versionId)] : []), ]).pipe( // assume draft + published + version are always same type map( ([draftTypeName, publishedTypeName, versionTypeName]) => versionTypeName || 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'}, isPublished: null, preview: { snapshot: null, original: null, }, } 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'}, isPublished: null, preview: { snapshot: null, original: null, }, } as const) } const publishedDocumentExists$ = documentPreviewStore .observePaths({_id: publishedId}, ['_rev']) .pipe(map((res) => Boolean((res as {_id: string; _rev: string} | undefined)?._rev))) const previewState$ = getPreviewStateObservable( documentPreviewStore, refSchemaType, publishedId, perspective, ) return combineLatest([previewState$, publishedDocumentExists$]).pipe( map(([previewState, publishedDocumentExists]): ReferenceInfo => { const availability = // eslint-disable-next-line no-nested-ternary pairAvailability.version?.available || pairAvailability.draft.available || pairAvailability.published.available ? READABLE : pairAvailability.version?.reason === 'PERMISSION_DENIED' || pairAvailability.draft.reason === 'PERMISSION_DENIED' || pairAvailability.published.reason === 'PERMISSION_DENIED' ? PERMISSION_DENIED : NOT_FOUND return { type: typeName, id: publishedId, availability, isPublished: publishedDocumentExists, preview: {snapshot: previewState.snapshot, original: previewState.original}, } }), ) }), ) }), ) } /** * 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, ): Observable<ReferenceSearchHit[]> { const search = createSearch(type.to, client, { ...options, maxDepth: options.maxFieldDepth || DEFAULT_MAX_FIELD_DEPTH, }) return search(textTerm, {includeDrafts: true}).pipe( map(({hits}) => hits.map(({hit}) => hit)), map((docs) => docs.map((doc) => ({ ...doc, // Pass the original id if available, it could be a `draftId` or a `versionId` , the _id will be the published one when using perspectives to query the data. _id: (doc._originalId as string) || doc._id, })), ), map((docs) => collate(docs)), // 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, } }) }), ) }), ) }