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

163 lines (147 loc) 5.43 kB
/* eslint-disable max-nested-callbacks */ import {type SanityClient} from '@sanity/client' import {flatten, keyBy} from 'lodash' import {combineLatest, defer, from, type Observable, of} from 'rxjs' import {distinctUntilChanged, map, mergeMap, reduce, switchMap} from 'rxjs/operators' import shallowEquals from 'shallow-equals' import {getDraftId, getPublishedId, isRecord} from '../util' import { AVAILABILITY_NOT_FOUND, AVAILABILITY_PERMISSION_DENIED, AVAILABILITY_READABLE, } from './constants' import { type AvailabilityResponse, type DocumentAvailability, type DraftsModelDocumentAvailability, type ObservePathsFn, } from './types' import {debounceCollect} from './utils/debounceCollect' const MAX_DOCUMENT_ID_CHUNK_SIZE = 11164 /** * Takes an array of document IDs and puts them into individual chunks. * Because document IDs can vary greatly in size, we want to chunk by the length of the * combined comma-separated ID set. We try to stay within 11164 bytes - this is about the * same length the Sanity client uses for max query size, and accounts for rather large * headers to be present - so this _should_ be safe. * * @param documentIds - Unique document IDs to chunk * @returns Array of document ID chunks */ function chunkDocumentIds(documentIds: string[]): string[][] { let chunk: string[] = [] let chunkSize = 0 const chunks: string[][] = [] for (const documentId of documentIds) { // Reached the max length? start a new chunk if (chunkSize + documentId.length + 1 >= MAX_DOCUMENT_ID_CHUNK_SIZE) { chunks.push(chunk) chunk = [] chunkSize = 0 } chunkSize += documentId.length + 1 // +1 is to account for a comma between IDs chunk.push(documentId) } if (!chunks.includes(chunk)) { chunks.push(chunk) } return chunks } /** * Mutative concat * @param array - the array to concat to * @param chunks - the items to concat to the array */ function mutConcat<T>(array: T[], chunks: T[]) { array.push(...chunks) return array } export function create_preview_availability( versionedClient: SanityClient, observePaths: ObservePathsFn, ): { observeDocumentPairAvailability(id: string): Observable<DraftsModelDocumentAvailability> } { /** * Returns an observable of metadata for a given drafts model document */ function observeDocumentPairAvailability( id: string, ): Observable<DraftsModelDocumentAvailability> { const draftId = getDraftId(id) const publishedId = getPublishedId(id) return combineLatest([ observeDocumentAvailability(draftId), observeDocumentAvailability(publishedId), ]).pipe( distinctUntilChanged(shallowEquals), map(([draftReadability, publishedReadability]) => { return { draft: draftReadability, published: publishedReadability, } }), ) } /** * Observable of metadata for the document with the given id * If we can't read a document it is either because it's not readable or because it doesn't exist * * @internal */ function observeDocumentAvailability(id: string): Observable<DocumentAvailability> { // check for existence return observePaths({_ref: id}, [['_rev']]).pipe( map((res) => isRecord(res) && Boolean('_rev' in res && res?._rev)), distinctUntilChanged(), switchMap((hasRev) => { return hasRev ? // short circuit: if we can read the _rev field we know it both exists and is readable of(AVAILABILITY_READABLE) : // we can't read the _rev field for two possible reasons: 1) the document isn't readable or 2) the document doesn't exist fetchDocumentReadability(id) }), ) } const fetchDocumentReadability = debounceCollect(function fetchDocumentReadability( args: string[][], ): Observable<DocumentAvailability[]> { const uniqueIds = [...new Set(flatten(args))] return from(chunkDocumentIds(uniqueIds)).pipe( mergeMap(fetchDocumentReadabilityChunked, 10), reduce<DocumentAvailability[], DocumentAvailability[]>(mutConcat, []), map((res) => args.map(([id]) => res[uniqueIds.indexOf(id)])), ) }, 1) function fetchDocumentReadabilityChunked(ids: string[]): Observable<DocumentAvailability[]> { return defer(() => { const requestOptions = { uri: versionedClient.getDataUrl('doc', ids.join(',')), json: true, query: {excludeContent: 'true'}, tag: 'preview.documents-availability', } return versionedClient.observable.request<AvailabilityResponse>(requestOptions).pipe( map((response) => { const omitted = keyBy(response.omitted || [], (entry) => entry.id) return ids.map((id) => { const omittedEntry = omitted[id] if (!omittedEntry) { // it's not omitted, so it exists and is readable return AVAILABILITY_READABLE } if (omittedEntry.reason === 'existence') { return AVAILABILITY_NOT_FOUND } if (omittedEntry.reason === 'permission') { // it's not omitted, so it exists and is readable return AVAILABILITY_PERMISSION_DENIED } throw new Error(`Unexpected reason for omission: "${omittedEntry.reason}"`) }) }), ) }) } return {observeDocumentPairAvailability} }