UNPKG

@sanity/migrate

Version:

Tooling for running data migrations on Sanity.io projects

163 lines (145 loc) 5.03 kB
import {type MultipleMutationResult} from '@sanity/client' import {type SanityDocument} from '@sanity/types' import arrify from 'arrify' import {endpoints} from '../fetch-utils/endpoints' import {fetchAsyncIterator, type FetchOptions} from '../fetch-utils/fetchStream' import {toFetchOptions} from '../fetch-utils/sanityRequestOptions' import {bufferThroughFile} from '../fs-webstream/bufferThroughFile' import {decodeText, parseJSON} from '../it-utils' import {concatStr} from '../it-utils/concatStr' import {lastValueFrom} from '../it-utils/lastValueFrom' import {mapAsync} from '../it-utils/mapAsync' import {parse, stringify} from '../it-utils/ndjson' import {tap} from '../it-utils/tap' import {fromExportEndpoint, safeJsonParser} from '../sources/fromExportEndpoint' import { type APIConfig, type Migration, type MigrationContext, type MigrationProgress, } from '../types' import {asyncIterableToStream} from '../utils/asyncIterableToStream' import {streamToAsyncIterator} from '../utils/streamToAsyncIterator' import {collectMigrationMutations} from './collectMigrationMutations' import { DEFAULT_MUTATION_CONCURRENCY, MAX_MUTATION_CONCURRENCY, MUTATION_ENDPOINT_MAX_BODY_SIZE, } from './constants' import {applyFilters} from './utils/applyFilters' import {batchMutations} from './utils/batchMutations' import {createContextClient} from './utils/createContextClient' import {createFilteredDocumentsClient} from './utils/createFilteredDocumentsClient' import {createBufferFile} from './utils/getBufferFile' import {toSanityMutations, type TransactionPayload} from './utils/toSanityMutations' export interface MigrationRunnerConfig { api: APIConfig concurrency?: number onProgress?: (event: MigrationProgress) => void } export async function* toFetchOptionsIterable( apiConfig: APIConfig, mutations: AsyncIterableIterator<TransactionPayload>, ) { for await (const transaction of mutations) { yield toFetchOptions({ projectId: apiConfig.projectId, apiVersion: apiConfig.apiVersion, token: apiConfig.token, tag: 'sanity.migration.mutate', apiHost: apiConfig.apiHost ?? 'api.sanity.io', endpoint: endpoints.data.mutate(apiConfig.dataset, { returnIds: true, visibility: 'async', autoGenerateArrayKeys: true, }), body: JSON.stringify(transaction), }) } } export async function run(config: MigrationRunnerConfig, migration: Migration) { const stats: MigrationProgress = { documents: 0, mutations: 0, pending: 0, queuedBatches: 0, completedTransactions: [], currentTransactions: [], } const filteredDocuments = applyFilters( migration, parse<SanityDocument>( decodeText( streamToAsyncIterator( await fromExportEndpoint({...config.api, documentTypes: migration.documentTypes}), ), ), {parse: safeJsonParser}, ), ) const abortController = new AbortController() const createReader = bufferThroughFile( asyncIterableToStream(stringify(filteredDocuments)), await createBufferFile(), {signal: abortController.signal}, ) const client = createContextClient({ ...config.api, useCdn: false, requestTagPrefix: 'sanity.migration', }) const filteredDocumentsClient = createFilteredDocumentsClient(createReader) const context: MigrationContext = { client, filtered: filteredDocumentsClient, dryRun: false, } const documents = () => tap( parse<SanityDocument>(decodeText(streamToAsyncIterator(createReader())), { parse: safeJsonParser, }), () => { config.onProgress?.({...stats, documents: ++stats.documents}) }, ) const mutations = tap(collectMigrationMutations(migration, documents, context), (muts) => { stats.currentTransactions = arrify(muts) config.onProgress?.({ ...stats, mutations: ++stats.mutations, }) }) const concurrency = config?.concurrency ?? DEFAULT_MUTATION_CONCURRENCY if (concurrency > MAX_MUTATION_CONCURRENCY) { throw new Error(`Concurrency exceeds maximum allowed value (${MAX_MUTATION_CONCURRENCY})`) } const batches = tap( batchMutations(toSanityMutations(mutations), MUTATION_ENDPOINT_MAX_BODY_SIZE), () => { config.onProgress?.({...stats, queuedBatches: ++stats.queuedBatches}) }, ) const submit = async (opts: FetchOptions): Promise<MultipleMutationResult> => lastValueFrom(parseJSON(concatStr(decodeText(await fetchAsyncIterator(opts))))) const commits = await mapAsync( toFetchOptionsIterable(config.api, batches), (opts) => { config.onProgress?.({...stats, pending: ++stats.pending}) return submit(opts) }, concurrency, ) for await (const result of commits) { stats.completedTransactions.push(result) config.onProgress?.({ ...stats, }) } config.onProgress?.({ ...stats, done: true, }) // Cancel export/buffer stream, it's not needed anymore abortController.abort() }