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

269 lines (250 loc) • 7.98 kB
import {type SanityClient} from '@sanity/client' import {diffInput, wrap} from '@sanity/diff' import {type SanityDocument, type TransactionLogEventWithEffects} from '@sanity/types' import {applyPatch, incremental} from 'mendoza' import { catchError, combineLatest, from, map, type Observable, of, shareReplay, startWith, switchMap, tap, } from 'rxjs' import {type Annotation, type ObjectDiff} from '../../field' import {wrapValue} from '../_legacy/history/history/diffValue' import {getDocumentTransactions} from './getDocumentTransactions' import { type DocumentGroupEvent, type EventsStoreRevision, isCreateDocumentVersionEvent, isEditDocumentVersionEvent, } from './types' import {type EventsObservableValue} from './useEventsStore' const buildDocumentForDiffInput = (document?: Partial<SanityDocument> | null) => { if (!document) return {} // Remove internal fields and undefined values const {_id, _rev, _createdAt, _updatedAt, _type, ...rest} = JSON.parse(JSON.stringify(document)) return rest } type EventMeta = { transactionIndex: number event?: DocumentGroupEvent } | null function omitRev(document: SanityDocument): Omit<SanityDocument, '_rev'> { const {_rev, ...doc} = document return doc } function annotationForTransactionIndex( transactions: TransactionLogEventWithEffects[], idx: number, event?: DocumentGroupEvent, ) { const tx = transactions[idx] if (!tx) return null return { timestamp: tx.timestamp, author: tx.author, event: event, } } function extractAnnotationForFromInput( transactions: TransactionLogEventWithEffects[], meta: EventMeta, ): Annotation { if (meta) { return annotationForTransactionIndex(transactions, meta.transactionIndex + 1, meta.event) } return null } function extractAnnotationForToInput( transactions: TransactionLogEventWithEffects[], meta: EventMeta, ): Annotation { if (meta) { return annotationForTransactionIndex(transactions, meta.transactionIndex, meta.event) } return null } function diffValue({ transactions, fromValue, fromRaw, toValue, toRaw, }: { transactions: TransactionLogEventWithEffects[] fromValue: incremental.Value<EventMeta> fromRaw: Omit<SanityDocument, '_rev'> toValue: incremental.Value<EventMeta> toRaw: Omit<SanityDocument, '_rev'> }) { const fromInput = wrapValue<EventMeta>(fromValue, fromRaw, { fromValue(value) { return extractAnnotationForFromInput(transactions, value.endMeta) }, fromMeta(meta) { return extractAnnotationForFromInput(transactions, meta) }, }) const toInput = wrapValue<EventMeta>(toValue, toRaw, { fromValue(value) { return extractAnnotationForToInput(transactions, value.startMeta) }, fromMeta(meta) { return extractAnnotationForToInput(transactions, meta) }, }) return diffInput(fromInput, toInput) } function calculateDiff({ initialDoc, documentId, transactions, events = [], }: { initialDoc: SanityDocument finalDoc?: SanityDocument transactions: TransactionLogEventWithEffects[] events: DocumentGroupEvent[] documentId: string }) { const initialValue = incremental.wrap<EventMeta>(omitRev(initialDoc), null) let document = incremental.wrap<EventMeta>(omitRev(initialDoc), null) let finalDocument = omitRev(initialDoc) transactions.forEach((transaction, index) => { const meta: EventMeta = { transactionIndex: index, event: events.find( (event) => !isEditDocumentVersionEvent(event) && 'revisionId' in event && event.revisionId === transaction.id, ), } const effect = transaction.effects[documentId] if (effect) { document = incremental.applyPatch(document, effect.apply, meta) finalDocument = applyPatch(finalDocument, effect.apply) } }) const diff = diffValue({ transactions, fromValue: initialValue, fromRaw: initialDoc, toValue: document, toRaw: finalDocument, }) as ObjectDiff return diff } function removeDuplicatedTransactions(transactions: TransactionLogEventWithEffects[]) { const seen = new Set() return transactions.filter((tx) => { if (seen.has(tx.id)) return false seen.add(tx.id) return true }) } export function getDocumentChanges({ eventsObservable$, documentId, client, to$, since$, remoteTransactions$, }: { eventsObservable$: Observable<EventsObservableValue> documentId: string client: SanityClient to$: Observable<EventsStoreRevision | null> remoteTransactions$: Observable<TransactionLogEventWithEffects[]> since$: Observable<EventsStoreRevision | null> }): Observable<{loading: boolean; diff: ObjectDiff | null; error: Error | null}> { let lastResolvedSince: string | null = null let lastResolvedTo: string | null = null let lastTransactions: TransactionLogEventWithEffects[] = [] return combineLatest(to$, since$, eventsObservable$).pipe( switchMap(([toObs, since, {events}]) => { const to = toObs?.document let sinceDoc: SanityDocument | undefined = undefined if (since?.document) { sinceDoc = since?.document } else { const selectedToEvent = events.find((event) => event.id === to?._rev) const isShowingCreationEvent = selectedToEvent && isCreateDocumentVersionEvent(selectedToEvent) if (isShowingCreationEvent && to) { sinceDoc = {_type: to._type, _id: to._id, _rev: to._rev} as SanityDocument } } if (!sinceDoc) { return of({loading: false, diff: null, error: null}) } return remoteTransactions$.pipe( switchMap((remoteTx) => { // When the user doesn't have a revision selected, so he is viewing the latest version of the document in the form. // For this case, we can use the remote transactions to calculate the diff. const viewingLatest = !to?._rev const getTransactions = (): Observable<TransactionLogEventWithEffects[]> => { if (viewingLatest && lastResolvedSince === sinceDoc._rev) { // The document has been previously resolved and it's on latest, we can use the remote transactions, we don't need to fetch them again return of(removeDuplicatedTransactions(lastTransactions.concat(remoteTx))) } if ( lastResolvedSince && lastResolvedSince === sinceDoc._rev && lastResolvedTo && lastResolvedTo === to?._rev ) { // The since and to haven't changed, use the same transactions. return of(lastTransactions) } return from( getDocumentTransactions({ documentId, client, toTransaction: to?._rev, fromTransaction: sinceDoc._rev, }), ) } return getTransactions().pipe( tap((transactions) => { lastResolvedSince = sinceDoc._rev lastTransactions = transactions if (to?._rev) { lastResolvedTo = to._rev } }), map((transactions) => { return { loading: false, diff: calculateDiff({documentId, initialDoc: sinceDoc, transactions, events}), error: null, } }), ) }), catchError((error) => { console.error(error) return of({loading: false, diff: null, error}) }), startWith({ loading: true, error: null, diff: sinceDoc && to ? (diffInput( wrap(buildDocumentForDiffInput(sinceDoc), null), wrap(buildDocumentForDiffInput(to), null), ) as ObjectDiff) : null, }), shareReplay(1), ) }), ) }