@sanity/import
Version:
Import documents to a Sanity dataset
140 lines (117 loc) • 4.53 kB
text/typescript
import {generateHelpUrl} from '@sanity/generate-help-url'
import debug from 'debug'
import pMap from 'p-map'
import {
type AssetDocument,
type AssetMetadata,
type ImportOptions,
type SanityDocument,
} from './types.js'
import {getAssetUrlStatus} from './util/urlExists.js'
const logger = debug('sanity:import:asset-validation')
const DEFAULT_VERIFY_CONCURRENCY = 12
const REQUIRED_PROPERTIES = {
_id: 'string',
_type: 'string',
assetId: 'string',
extension: 'string',
mimeType: 'string',
path: 'string',
sha1hash: 'string',
size: 'number',
url: 'string',
} as const
export async function validateAssetDocuments(
docs: SanityDocument[],
options: ImportOptions,
): Promise<void> {
const {targetDataset, targetProjectId} = options
const concurrency = options.assetVerificationConcurrency || DEFAULT_VERIFY_CONCURRENCY
const assetDocs = docs.filter((doc) =>
/^sanity\.[a-zA-Z]+Asset$/.test(doc._type || ''),
) as AssetDocument[]
if (assetDocs.length === 0) {
return
}
options.onProgress({step: 'Validating asset documents'})
for (const doc of assetDocs) validateAssetDocumentProperties(doc)
// Don't allow assets that reference different datasets (unless explicitly allowing it)
if (!options.allowAssetsInDifferentDataset) {
for (const doc of assetDocs) {
const id = doc._id || doc.url
const {dataset, projectId} = getLocationFromDocument(doc)
const resolveText = `See ${generateHelpUrl('import-asset-has-different-target')}`
if (projectId !== targetProjectId) {
throw new Error(
`Asset ${id} references a different project ID than the specified target (asset is in ${projectId}, importing to ${targetProjectId}). ${resolveText}`,
)
}
if (dataset !== targetDataset) {
throw new Error(
`Asset ${id} references a different dataset than the specified target (asset is in ${dataset}, importing to ${targetDataset}). ${resolveText}`,
)
}
}
}
if (!options.allowFailingAssets) {
await pMap(assetDocs, ensureAssetUrlExists, {concurrency})
}
}
function getLocationFromDocument(doc: AssetDocument): {dataset: string; projectId: string} {
const url = doc.path || doc.url || ''
const path = url.replace(/^https:\/\/cdn\.sanity\.[a-z]+\//, '')
const [, projectId, dataset] = path.split('/')
return {dataset: dataset || '', projectId: projectId || ''}
}
async function ensureAssetUrlExists(assetDoc: AssetDocument): Promise<boolean> {
const url = assetDoc.url!
const start = Date.now()
const status = await getAssetUrlStatus(url)
logger(`${url}: %d (%d ms)`, status, Date.now() - start)
if (status === 200) {
return true
}
if (status !== 404) {
throw new Error(
`Document ${assetDoc._id} points to a URL that could not be verified (${url}): ` +
`server returned HTTP ${status}. ` +
`Re-run with --allow-failing-assets to skip URL verification.`,
)
}
const helpUrl = generateHelpUrl('import-asset-file-does-not-exist')
throw new Error(
`Document ${assetDoc._id} points to a URL that does not exist (${url}). See ${helpUrl}.`,
)
}
function validateAssetDocumentProperties(assetDoc: AssetDocument): void {
for (const prop of Object.keys(REQUIRED_PROPERTIES)) {
const expectedType = REQUIRED_PROPERTIES[prop as keyof typeof REQUIRED_PROPERTIES]
const propValue = (assetDoc as Record<string, unknown>)[prop]
if (typeof propValue !== expectedType) {
const errorType = propValue === undefined ? 'is missing' : 'has invalid type for'
throw new Error(`Asset document ${assetDoc._id} ${errorType} required property "${prop}"`)
}
}
if (assetDoc._type === 'sanity.imageAsset') {
validateImageMetadata(assetDoc)
}
}
function validateImageMetadata(assetDoc: AssetDocument): void {
if (!assetDoc.metadata) {
throw new Error(`Asset document ${assetDoc._id} is missing required property "metadata"`)
}
if (!assetDoc.metadata.dimensions) {
throw new Error(
`Asset document ${assetDoc._id} is missing required property "metadata.dimensions"`,
)
}
const dimensionProps = ['width', 'height', 'aspectRatio']
const metadata = assetDoc.metadata as AssetMetadata
for (const prop of dimensionProps) {
if (typeof metadata.dimensions?.[prop as keyof typeof metadata.dimensions] !== 'number') {
throw new TypeError(
`Asset document ${assetDoc._id} is missing required property "metadata.dimensions.${prop}"`,
)
}
}
}