@sanity/import
Version:
Import documents to a Sanity dataset
157 lines (134 loc) • 4.64 kB
text/typescript
import {type MultipleMutationResult, type SanityClient, type Transaction} from '@sanity/client'
import {extractWithPath} from '@sanity/mutator'
import debug from 'debug'
import pMap from 'p-map'
import {serializePath} from './serializePath.js'
import {
type ImportOptions,
type Reference,
type SanityApiError,
type SanityDocument,
type StreamReference,
} from './types.js'
import {deepGet} from './util/deepGet.js'
import {progressStepper} from './util/progressStepper.js'
import {retryOnFailure} from './util/retryOnFailure.js'
import {suffixTag} from './util/suffixTag.js'
const logger = debug('sanity:import')
const STRENGTHEN_CONCURRENCY = 30
const STRENGTHEN_BATCH_SIZE = 30
export interface StrongRefsTask {
documentId: string
references: string[]
}
interface RefPathItem {
path: (number | string)[]
ref: StreamReference
}
export function getStrongRefs(doc: SanityDocument): StrongRefsTask | null {
const refs = findStrongRefs(doc).map((item) => serializePath(item))
if (refs.length > 0) {
return {
documentId: doc._id,
references: refs,
}
}
return null
}
// Note: mutates in-place
export function weakenStrongRefs(doc: SanityDocument): SanityDocument {
const refs = findStrongRefs(doc)
for (const item of refs) {
item.ref._weak = true
}
return doc
}
// Note: mutates in-place
export function cleanupReferences(doc: SanityDocument, options: ImportOptions): SanityDocument {
const {skipCrossDatasetReferences, targetProjectId} = options
const refPathItems = extractWithPath('..[_ref]', doc)
.map((match) => match.path.slice(0, -1))
.map((path) => ({path, ref: deepGet(doc, path) as StreamReference}))
for (const item of refPathItems) {
// We may want to skip cross-dataset references, eg when importing to other projects
if (skipCrossDatasetReferences && '_dataset' in item.ref) {
const leaf = item.path.at(-1)
const parent =
item.path.length > 1
? (deepGet(doc, item.path.slice(0, -1)) as Record<number | string, unknown>)
: doc
if (typeof leaf === 'string' || typeof leaf === 'number') {
delete parent[leaf]
}
continue
}
// Apply missing _type on references
if ((item.ref as Reference)._type === undefined) {
;(item.ref as Reference)._type = 'reference'
}
// Ensure cross-dataset references point to the same project ID as being imported to
const refWithProjectId = item.ref as StreamReference & {_projectId?: string}
if (refWithProjectId._projectId !== undefined) {
refWithProjectId._projectId = targetProjectId!
}
}
return doc
}
function findStrongRefs(doc: SanityDocument): RefPathItem[] {
return extractWithPath('..[_ref]', doc)
.map((match) => match.path.slice(0, -1))
.map((path) => ({path, ref: deepGet(doc, path) as StreamReference}))
.filter((item) => item.ref._weak !== true)
}
export function strengthenReferences(
strongRefs: StrongRefsTask[],
options: ImportOptions,
): Promise<number[]> {
const {client, tag} = options
const batches: StrongRefsTask[][] = []
for (let i = 0; i < strongRefs.length; i += STRENGTHEN_BATCH_SIZE) {
batches.push(strongRefs.slice(i, i + STRENGTHEN_BATCH_SIZE))
}
if (batches.length === 0) {
return Promise.resolve([0])
}
const progress = progressStepper(options.onProgress, {
step: 'Strengthening references',
total: batches.length,
})
const mapOptions = {concurrency: STRENGTHEN_CONCURRENCY}
return pMap(batches, unsetWeakBatch.bind(null, client, progress, tag), mapOptions)
}
function unsetWeakBatch(
client: SanityClient,
progress: () => void,
tag: string,
batch: StrongRefsTask[],
): Promise<number> {
logger('Strengthening batch of %d documents', batch.length)
return retryOnFailure(
() => {
let trx = client.transaction()
for (const task of batch) {
trx = reducePatch(trx, task)
}
return trx
.commit({tag: suffixTag(tag, 'ref.strengthen'), visibility: 'async'})
.then((res: MultipleMutationResult) => {
progress()
return res.results.length
})
.catch((err: Error) => {
const apiError = err as SanityApiError & {step?: string}
apiError.step = 'strengthen-references'
throw apiError
})
},
{isRetriable: (err: SanityApiError) => !err.statusCode || err.statusCode !== 409},
)
}
function reducePatch(trx: Transaction, task: StrongRefsTask): Transaction {
return trx.patch(task.documentId, (patch) =>
patch.unset(task.references.map((path) => `${path}._weak`)),
)
}