UNPKG

@sanity/sdk

Version:
203 lines (177 loc) 6.39 kB
import {type SanityClient} from '@sanity/client' import {type SanityDocument} from 'groq' import {distinctUntilChanged, filter, first, firstValueFrom, map, race} from 'rxjs' import {bindActionByDataset} from '../store/createActionBinder' import {type SanityInstance} from '../store/createSanityInstance' import {type StoreContext} from '../store/defineStore' import {type DocumentAction} from './actions' import {documentStore, type DocumentStoreState} from './documentStore' import {type DocumentSet} from './processMutations' import {type AppliedTransaction, type QueuedTransaction, queueTransaction} from './reducers' /** @beta */ export interface ActionsResult<TDocument extends SanityDocument = SanityDocument> { transactionId: string documents: DocumentSet<TDocument> previous: DocumentSet<TDocument> previousRevs: {[documentId: string]: string | undefined} appeared: string[] updated: string[] disappeared: string[] submitted: () => ReturnType<SanityClient['action']> } /** @beta */ export interface ApplyDocumentActionsOptions { /** * Optionally provide an ID to be used as this transaction ID */ transactionId?: string /** * Set this to true to prevent this action from being batched with others. */ disableBatching?: boolean } /** @beta */ export function applyDocumentActions< TDocumentType extends string = string, TDataset extends string = string, TProjectId extends string = string, >( instance: SanityInstance, action: | DocumentAction<TDocumentType, TDataset, TProjectId> | DocumentAction<TDocumentType, TDataset, TProjectId>[], options?: ApplyDocumentActionsOptions, ): Promise<ActionsResult<SanityDocument<TDocumentType, `${TProjectId}.${TDataset}`>>> /** @beta */ export function applyDocumentActions( instance: SanityInstance, action: DocumentAction | DocumentAction[], options?: ApplyDocumentActionsOptions, ): Promise<ActionsResult> /** @beta */ export function applyDocumentActions( ...args: Parameters<typeof boundApplyDocumentActions> ): ReturnType<typeof boundApplyDocumentActions> { return boundApplyDocumentActions(...args) } const boundApplyDocumentActions = bindActionByDataset(documentStore, _applyDocumentActions) /** @internal */ async function _applyDocumentActions( {instance, state}: StoreContext<DocumentStoreState>, actionOrActions: DocumentAction | DocumentAction[], {transactionId = crypto.randomUUID(), disableBatching}: ApplyDocumentActionsOptions = {}, ): Promise<ActionsResult> { const actions = Array.isArray(actionOrActions) ? actionOrActions : [actionOrActions] let projectId let dataset for (const action of actions) { if (action.projectId) { if (!projectId) projectId = action.projectId if (action.projectId !== projectId) { throw new Error( `Mismatched project IDs found in actions. All actions must belong to the same project. Found "${action.projectId}" but expected "${projectId}".`, ) } if (action.dataset) { if (!dataset) dataset = action.dataset if (action.dataset !== dataset) { throw new Error( `Mismatched datasets found in actions. All actions must belong to the same dataset. Found "${action.dataset}" but expected "${dataset}".`, ) } } } } if ( (projectId && projectId !== instance.config.projectId) || (dataset && dataset !== instance.config.dataset) ) { const matchedInstance = instance.match({projectId, dataset}) if (!matchedInstance) { throw new Error( `Could not find a matching instance for projectId: "${projectId}" and dataset: "${dataset}"`, ) } return boundApplyDocumentActions(matchedInstance, actionOrActions, { disableBatching, transactionId, }) } const {events} = state.get() const transaction: QueuedTransaction = { transactionId, actions, ...(disableBatching && {disableBatching}), } const fatalError$ = state.observable.pipe( map((s) => s.error), first(Boolean), map((error) => ({type: 'error', error}) as const), ) const transactionError$ = events.pipe( filter((e) => e.type === 'error'), first((e) => e.transactionId === transactionId), ) const appliedTransaction$ = state.observable.pipe( map((s) => s.applied), distinctUntilChanged(), map((applied) => applied.find((t) => t.transactionId === transactionId)), first(Boolean), ) const successfulTransaction$ = events.pipe( filter((e) => e.type === 'accepted'), first((e) => e.outgoing.batchedTransactionIds.includes(transactionId)), ) const rejectedTransaction$ = events.pipe( filter((e) => e.type === 'reverted'), first((e) => e.outgoing.batchedTransactionIds.includes(transactionId)), ) const appliedTransactionOrError = firstValueFrom( race([fatalError$, transactionError$, appliedTransaction$]), ) const acceptedOrRejectedTransaction = firstValueFrom( race([successfulTransaction$, rejectedTransaction$, transactionError$]), ) state.set('queueTransaction', (prev) => queueTransaction(prev, transaction)) const result = await appliedTransactionOrError if ('type' in result && result.type === 'error') throw result.error const {working: documents, previous, previousRevs} = result as AppliedTransaction const existingIds = new Set( Object.entries(previous) .filter(([, value]) => !!value) .map(([key]) => key), ) const resultingIds = new Set( Object.entries(documents) .filter(([, value]) => !!value) .map(([key]) => key), ) const allIds = new Set([...existingIds, ...resultingIds]) const updated: string[] = [] const appeared: string[] = [] const disappeared: string[] = [] for (const id of allIds) { if (existingIds.has(id) && resultingIds.has(id)) { updated.push(id) } else if (!existingIds.has(id) && resultingIds.has(id)) { appeared.push(id) } else if (!resultingIds.has(id) && existingIds.has(id)) { disappeared.push(id) } } async function submitted() { const raceResult = await acceptedOrRejectedTransaction if (raceResult.type !== 'accepted') throw raceResult.error return raceResult.result } return { transactionId, documents, previous, previousRevs, appeared, updated, disappeared, submitted, } }