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
155 lines (136 loc) • 4.96 kB
text/typescript
import {type CliCommandArguments, type CliCommandContext, type CliOutputter} from '@sanity/cli'
import {type ClientConfig} from '@sanity/client'
import chalk from 'chalk'
import fs from 'fs'
import logSymbols from 'log-symbols'
import path from 'path'
import {type ValidationWorkerChannel} from '../../threads/validateDocuments'
import {type WorkerChannelReceiver} from '../../util/workerChannels'
import {reporters} from './reporters'
import {validateDocuments} from './validateDocuments'
interface ValidateFlags {
workspace?: string
format?: string
dataset?: string
file?: string
level?: 'error' | 'warning' | 'info'
'max-custom-validation-concurrency'?: number
yes?: boolean
y?: boolean
}
export type BuiltInValidationReporter = (options: {
output: CliOutputter
worker: WorkerChannelReceiver<ValidationWorkerChannel>
flags: ValidateFlags
}) => Promise<'error' | 'warning' | 'info'>
export default async function validateAction(
args: CliCommandArguments<ValidateFlags>,
{apiClient, workDir, output, prompt}: CliCommandContext,
): Promise<void> {
const flags = args.extOptions
const unattendedMode = Boolean(flags.yes || flags.y)
if (!unattendedMode) {
output.print(
`${chalk.yellow(`${logSymbols.warning} Warning:`)} This command ${
flags.file
? 'reads all documents from your input file'
: 'downloads all documents from your dataset'
} and processes them through your local schema within a ` +
`simulated browser environment.\n`,
)
output.print(`Potential pitfalls:\n`)
output.print(
`- Processes all documents locally (excluding assets). Large datasets may require more resources.`,
)
output.print(
`- Executes all custom validation functions. Some functions may need to be refactored for compatibility.`,
)
output.print(
`- Not all standard browser features are available and may cause issues while loading your Studio.`,
)
output.print(
`- Adheres to document permissions. Ensure this account can see all desired documents.`,
)
if (flags.file) {
output.print(
`- Checks for missing document references against the live dataset if not found in your file.`,
)
}
const confirmed = await prompt.single<boolean>({
type: 'confirm',
message: `Are you sure you want to continue?`,
default: true,
})
if (!confirmed) {
output.print('User aborted')
process.exitCode = 1
return
}
}
if (flags.format && !(flags.format in reporters)) {
const formatter = new Intl.ListFormat('en-US', {
style: 'long',
type: 'conjunction',
})
throw new Error(
`Did not recognize format '${flags.format}'. Available formats are ${formatter.format(
Object.keys(reporters).map((key) => `'${key}'`),
)}`,
)
}
const level = flags.level || 'warning'
if (level !== 'error' && level !== 'warning' && level !== 'info') {
throw new Error(`Invalid level. Available levels are 'error', 'warning', and 'info'.`)
}
const maxCustomValidationConcurrency = flags['max-custom-validation-concurrency']
if (
maxCustomValidationConcurrency &&
typeof maxCustomValidationConcurrency !== 'number' &&
!Number.isInteger(maxCustomValidationConcurrency)
) {
throw new Error(`'--max-custom-validation-concurrency' must be an integer.`)
}
const clientConfig: Partial<ClientConfig> = {
...apiClient({
requireUser: true,
requireProject: false, // we'll get this from the workspace
}).config(),
// we set this explictly to true because the default client configuration
// from the CLI comes configured with `useProjectHostname: false` when
// `requireProject` is set to false
useProjectHostname: true,
// we set this explictly to true because we pass in a token via the
// `clientConfiguration` object and also mock a browser environment in
// this worker which triggers the browser warning
ignoreBrowserTokenWarning: true,
}
let ndjsonFilePath
if (flags.file) {
if (typeof flags.file !== 'string') {
throw new Error(`'--file' must be a string`)
}
const filePath = path.resolve(workDir, flags.file)
const stat = await fs.promises.stat(filePath)
if (!stat.isFile()) {
throw new Error(`'--file' must point to a valid ndjson file or tarball`)
}
ndjsonFilePath = filePath
}
const overallLevel = await validateDocuments({
workspace: flags.workspace,
dataset: flags.dataset,
clientConfig,
workDir,
level,
maxCustomValidationConcurrency,
ndjsonFilePath,
reporter: (worker) => {
const reporter =
flags.format && flags.format in reporters
? reporters[flags.format as keyof typeof reporters]
: reporters.pretty
return reporter({output, worker, flags})
},
})
process.exitCode = overallLevel === 'error' ? 1 : 0
}