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

374 lines (335 loc) • 10.7 kB
import { type Action, type EditAction, type IdentifiedSanityDocumentStub, type SanityClient, } from '@sanity/client' import {getPublishedId, getVersionId} from '../../util' import {type ReleasesUpsellContextValue} from '../contexts/upsell/types' import {getReleaseIdFromReleaseDocumentId, type ReleaseDocument} from '../index' import {type RevertDocument} from '../tool/components/releaseCTAButtons/ReleaseRevertButton/useDocumentRevertStates' import {prepareVersionReferences} from '../util/prepareVersionReferences' import {isReleaseLimitError} from './isReleaseLimitError' import {type EditableReleaseDocument} from './types' interface operationsOptions { dryRun?: boolean skipCrossDatasetValidation?: boolean } export interface ReleaseOperationsStore { publishRelease: (releaseId: string, opts?: operationsOptions) => Promise<void> schedule: (releaseId: string, date: Date, opts?: operationsOptions) => Promise<void> //todo: reschedule: (releaseId: string, newDate: Date) => Promise<void> unschedule: (releaseId: string, opts?: operationsOptions) => Promise<void> archive: (releaseId: string, opts?: operationsOptions) => Promise<void> unarchive: (releaseId: string, opts?: operationsOptions) => Promise<void> updateRelease: (release: EditableReleaseDocument, opts?: operationsOptions) => Promise<void> createRelease: (release: EditableReleaseDocument, opts?: operationsOptions) => Promise<void> deleteRelease: (releaseId: string, opts?: operationsOptions) => Promise<void> revertRelease: ( revertReleaseId: string, documents: RevertDocument[], releaseMetadata: ReleaseDocument['metadata'], revertType: 'staged' | 'immediate', opts?: operationsOptions, ) => Promise<void> createVersion: ( releaseId: string, documentId: string, initialvalue?: Record<string, unknown>, opts?: operationsOptions, ) => Promise<void> discardVersion: (releaseId: string, documentId: string, opts?: operationsOptions) => Promise<void> unpublishVersion: (documentId: string, opts?: operationsOptions) => Promise<void> } const METADATA_PROPERTY_NAME = 'metadata' export function createReleaseOperationsStore(options: { client: SanityClient onReleaseLimitReached: ReleasesUpsellContextValue['onReleaseLimitReached'] }): ReleaseOperationsStore { const {client} = options const requestAction = createRequestAction(options.onReleaseLimitReached) const handleCreateRelease = (release: EditableReleaseDocument, opts?: operationsOptions) => requestAction( client, { actionType: 'sanity.action.release.create', releaseId: getReleaseIdFromReleaseDocumentId(release._id), [METADATA_PROPERTY_NAME]: release.metadata, }, opts, ) const handleUpdateRelease = async ( release: EditableReleaseDocument, opts?: operationsOptions, ) => { const bundleId = getReleaseIdFromReleaseDocumentId(release._id) const unsetKeys = Object.entries(release) .filter(([_, value]) => value === undefined) .map(([key]) => `${METADATA_PROPERTY_NAME}.${key}`) await requestAction( client, { actionType: 'sanity.action.release.edit', releaseId: bundleId, patch: { // todo: consider more granular updates here set: {[METADATA_PROPERTY_NAME]: release.metadata}, unset: unsetKeys, }, }, opts, ) } const handlePublishRelease = (releaseId: string, opts?: operationsOptions) => requestAction( client, [ { actionType: 'sanity.action.release.publish', releaseId: getReleaseIdFromReleaseDocumentId(releaseId), }, ], opts, ) const handleScheduleRelease = (releaseId: string, publishAt: Date, opts?: operationsOptions) => requestAction( client, [ { actionType: 'sanity.action.release.schedule', releaseId: getReleaseIdFromReleaseDocumentId(releaseId), publishAt: publishAt.toISOString(), }, ], opts, ) const handleUnscheduleRelease = (releaseId: string, opts?: operationsOptions) => requestAction( client, [ { actionType: 'sanity.action.release.unschedule', releaseId: getReleaseIdFromReleaseDocumentId(releaseId), }, ], opts, ) const handleArchiveRelease = (releaseId: string, opts?: operationsOptions) => requestAction( client, [ { actionType: 'sanity.action.release.archive', releaseId: getReleaseIdFromReleaseDocumentId(releaseId), }, ], opts, ) const handleUnarchiveRelease = (releaseId: string, opts?: operationsOptions) => requestAction( client, [ { actionType: 'sanity.action.release.unarchive', releaseId: getReleaseIdFromReleaseDocumentId(releaseId), }, ], opts, ) const handleDeleteRelease = (releaseId: string, opts?: operationsOptions) => requestAction( client, [ { actionType: 'sanity.action.release.delete', releaseId: getReleaseIdFromReleaseDocumentId(releaseId), }, ], opts, ) const handleCreateVersion = async ( releaseId: string, documentId: string, initialValue?: Record<string, unknown>, opts?: operationsOptions, ) => { // the documentId will show you where the document is coming from and which // document should it copy from // fetch original document const document = await client.getDocument(documentId) if (!document && !initialValue) { throw new Error(`Document with id ${documentId} not found and no initial value provided`) } const versionDocument = prepareVersionReferences({ ...(document || {}), ...(initialValue || {}), _id: getVersionId(documentId, releaseId), }) as IdentifiedSanityDocumentStub await requestAction( client, [ { actionType: 'sanity.action.document.version.create', publishedId: getPublishedId(documentId), document: versionDocument, }, ], opts, ) } const handleDiscardVersion = (releaseId: string, documentId: string, opts?: operationsOptions) => requestAction( client, [ { actionType: 'sanity.action.document.version.discard', versionId: getVersionId(documentId, releaseId), purge: false, // keep document history }, ], opts, ) const handleUnpublishVersion = (documentId: string) => requestAction(client, [ { actionType: 'sanity.action.document.version.unpublish', versionId: documentId, publishedId: getPublishedId(documentId), }, ]) const handleRevertRelease = async ( revertReleaseId: string, releaseDocuments: RevertDocument[], releaseMetadata: ReleaseDocument['metadata'], revertType: 'staged' | 'immediate', ) => { await handleCreateRelease({ _id: revertReleaseId, metadata: { title: releaseMetadata.title, description: releaseMetadata.description, releaseType: 'asap', }, }) await Promise.allSettled( releaseDocuments.map((document) => handleCreateVersion( getReleaseIdFromReleaseDocumentId(revertReleaseId), document._id, document, ), ), ) if (revertType === 'immediate') { await handlePublishRelease(revertReleaseId) } } return { archive: handleArchiveRelease, unarchive: handleUnarchiveRelease, schedule: handleScheduleRelease, unschedule: handleUnscheduleRelease, createRelease: handleCreateRelease, updateRelease: handleUpdateRelease, publishRelease: handlePublishRelease, deleteRelease: handleDeleteRelease, revertRelease: handleRevertRelease, createVersion: handleCreateVersion, discardVersion: handleDiscardVersion, unpublishVersion: handleUnpublishVersion, } } interface ScheduleApiAction { actionType: 'sanity.action.release.schedule' releaseId: string publishAt: string dryRun?: boolean skipCrossDatasetValidation?: boolean } interface PublishApiAction { actionType: 'sanity.action.release.publish' releaseId: string } interface ArchiveApiAction { actionType: 'sanity.action.release.archive' releaseId: string } interface UnarchiveApiAction { actionType: 'sanity.action.release.unarchive' releaseId: string } interface UnscheduleApiAction { actionType: 'sanity.action.release.unschedule' releaseId: string } interface CreateReleaseApiAction { actionType: 'sanity.action.release.create' releaseId: string [METADATA_PROPERTY_NAME]?: Partial<ReleaseDocument['metadata']> } interface CreateVersionReleaseApiAction { actionType: 'sanity.action.document.version.create' publishedId: string document: IdentifiedSanityDocumentStub } interface UnpublishVersionReleaseApiAction { actionType: 'sanity.action.document.version.unpublish' versionId: string publishedId: string } interface DiscardVersionReleaseApiAction { actionType: 'sanity.action.document.version.discard' versionId: string purge?: boolean } interface EditReleaseApiAction { actionType: 'sanity.action.release.edit' releaseId: string patch: EditAction['patch'] } interface DeleteApiAction { actionType: 'sanity.action.release.delete' releaseId: string } type ReleaseAction = | Action | ScheduleApiAction | PublishApiAction | CreateReleaseApiAction | EditReleaseApiAction | UnscheduleApiAction | ArchiveApiAction | UnarchiveApiAction | DeleteApiAction | CreateVersionReleaseApiAction | UnpublishVersionReleaseApiAction | DiscardVersionReleaseApiAction export function createRequestAction( onReleaseLimitReached: ReleasesUpsellContextValue['onReleaseLimitReached'], ) { return async function requestAction( client: SanityClient, actions: ReleaseAction | ReleaseAction[], options?: operationsOptions, ): Promise<void> { const {dataset} = client.config() try { return await client.request({ uri: `/data/actions/${dataset}`, method: 'POST', body: { ...options, actions: Array.isArray(actions) ? actions : [actions], }, }) } catch (e) { // if dryRunning then essentially this is a silent request // so don't want to create disruptive upsell because of limit if (!options?.dryRun && isReleaseLimitError(e)) { // free accounts do not return limit, 0 is implied onReleaseLimitReached(e.details.limit || 0) } throw e } } }