@sanity/import
Version:
Import documents to a Sanity dataset
312 lines (260 loc) • 7.43 kB
JavaScript
/* eslint-disable no-console, no-process-env */
const fs = require('fs')
const path = require('path')
const {createClient} = require('@sanity/client')
const sanityImport = require('./import')
const {getIt} = require('get-it')
const {promise} = require('get-it/middleware')
const meow = require('meow')
const ora = require('ora')
const prettyMs = require('pretty-ms')
const red = (str) => `\u001b[31m${str}\u001b[39m`
const yellow = (str) => `\u001b[33m${str}\u001b[39m`
const printError = (str) => console.error(red(`ERROR: ${str}`))
const cli = meow(
`
Usage
$ sanity-import -p <projectId> -d <dataset> -t <token> sourceFile.ndjson
Options
-p, --project <projectId> Project ID to import to
-d, --dataset <dataset> Dataset to import to
-t, --token <token> Token to authenticate with
--asset-concurrency <concurrency> Number of parallel asset imports
--replace Replace documents with the same IDs
--missing Skip documents that already exist
--allow-failing-assets Skip assets that cannot be fetched/uploaded
--replace-assets Skip reuse of existing assets
--skip-cross-dataset-references Skips references to other datasets
--help Show this help
Rarely used options (should generally not be used)
--allow-assets-in-different-dataset Allow asset documents to reference different project/dataset
--allow-system-documents Imports system documents
Examples
# Import "./my-dataset.ndjson" into dataset "staging"
$ sanity-import -p myPrOj -d staging -t someSecretToken my-dataset.ndjson
# Import into dataset "test" from stdin, read token from env var
$ cat my-dataset.ndjson | sanity-import -p myPrOj -d test -
Environment variables (fallbacks for missing flags)
--token = SANITY_IMPORT_TOKEN
`,
{
flags: {
// Required, but validated below for better error output
project: {
type: 'string',
alias: 'p',
},
dataset: {
type: 'string',
alias: 'd',
},
token: {
type: 'string',
alias: 't',
},
// Optional
replace: {
type: 'boolean',
default: false,
},
missing: {
type: 'boolean',
default: false,
},
allowFailingAssets: {
type: 'boolean',
default: false,
},
allowAssetsInDifferentDataset: {
type: 'boolean',
default: false,
},
replaceAssets: {
type: 'boolean',
default: false,
},
skipCrossDatasetReferences: {
type: 'boolean',
default: false,
},
assetConcurrency: {
type: 'number',
alias: 'c',
},
},
},
)
const {flags, input, showHelp} = cli
const {
dataset,
allowFailingAssets,
replaceAssets,
allowAssetsInDifferentDataset,
skipCrossDatasetReferences,
} = flags
const token = flags.token || process.env.SANITY_IMPORT_TOKEN
const projectId = flags.project
const assetConcurrency = flags.assetConcurrency
const source = input[0]
if (!projectId) {
printError('Flag `--project` is required')
showHelp()
}
if (!dataset) {
printError('Flag `--dataset` is required')
showHelp()
}
if (!token) {
printError('Flag `--token` is required (or set SANITY_IMPORT_TOKEN)')
showHelp()
}
if (!source) {
printError('Source file is required, use `-` to read from stdin')
showHelp()
}
let operation = 'create'
let releasesOperation = 'fail'
if (flags.replace || flags.missing) {
if (flags.replace && flags.missing) {
printError('Cannot use both `--replace` and `--missing`')
showHelp()
}
operation = flags.replace ? 'createOrReplace' : 'createIfNotExists'
releasesOperation = flags.replace ? 'replace' : 'ignore'
}
let currentStep
let currentProgress
let stepStart
let spinInterval
const client = createClient({
apiVersion: '2025-02-19',
projectId,
dataset,
token,
useCdn: false,
})
getStream()
.then((stream) =>
sanityImport(stream, {
client,
operation,
onProgress,
allowFailingAssets,
allowAssetsInDifferentDataset,
skipCrossDatasetReferences,
assetConcurrency,
replaceAssets,
assetsBase: getAssetsBase(),
releasesOperation,
}),
)
.then(({numDocs, warnings}) => {
const timeSpent = prettyMs(Date.now() - stepStart, {secondsDecimalDigits: 2})
currentProgress.text = `[100%] ${currentStep} (${timeSpent})`
currentProgress.succeed()
console.log('Done! Imported %d documents to dataset "%s"\n', numDocs, dataset)
printWarnings(warnings)
})
.catch((err) => {
if (currentProgress) {
currentProgress.fail()
}
printError(err.stack)
})
function printWarnings(warnings) {
const assetFails = warnings.filter((warn) => warn.type === 'asset')
if (!assetFails.length) {
return
}
console.warn(
yellow('⚠ Failed to import the following %s:'),
assetFails.length > 1 ? 'assets' : 'asset',
)
warnings.forEach((warning) => {
console.warn(` ${warning.url}`)
})
}
function getStream() {
if (/^https:\/\//i.test(source)) {
return getUriStream(source)
}
return Promise.resolve(source === '-' ? process.stdin : fs.createReadStream(source))
}
function getAssetsBase() {
if (/^https:\/\//i.test(source) || source === '-') {
return undefined
}
try {
const fileStats = fs.statSync(source)
const sourceIsFolder = fileStats.isDirectory()
return sourceIsFolder ? source : path.dirname(source)
} catch (err) {
return undefined
}
}
function getUriStream(uri) {
const get = getIt([promise()])
return get({url: uri, stream: true}).then(
(res) => {
if (res.statusCode !== 200) {
throw new Error(`Error fetching source: HTTP ${res.statusCode}`)
}
return res.body
},
(err) => {
throw new Error(`Error fetching source:\n${err.message}`)
},
)
}
function onProgress(opts) {
const lengthComputable = opts.total
const sameStep = opts.step == currentStep
const percent = getPercentage(opts)
if (lengthComputable && opts.total === opts.current) {
clearInterval(spinInterval)
spinInterval = null
}
if (sameStep && !lengthComputable) {
return
}
if (sameStep) {
const timeSpent = prettyMs(Date.now() - stepStart, {secDecimalDigits: 2})
currentProgress.text = `${percent}${opts.step} (${timeSpent})`
currentProgress.render()
return
}
// Moved to a new step
const prevStep = currentStep
const prevStepStart = stepStart
stepStart = Date.now()
currentStep = opts.step
if (spinInterval) {
clearInterval(spinInterval)
spinInterval = null
}
if (currentProgress && currentProgress.succeed) {
const timeSpent = prettyMs(Date.now() - prevStepStart, {
secDecimalDigits: 2,
})
currentProgress.text = `[100%] ${prevStep} (${timeSpent})`
currentProgress.succeed()
}
currentProgress = ora(`[0%] ${opts.step} (0.00s)`).start()
if (!lengthComputable) {
spinInterval = setInterval(() => {
const timeSpent = prettyMs(Date.now() - prevStepStart, {
secDecimalDigits: 2,
})
currentProgress.text = `${percent}${opts.step} (${timeSpent})`
currentProgress.render()
}, 60)
}
}
function getPercentage(opts) {
if (!opts.total) {
return ''
}
const percent = Math.floor((opts.current / opts.total) * 100)
return `[${percent}%] `
}