UNPKG

@sanity/migrate

Version:

Tooling for running data migrations on Sanity.io projects

183 lines (157 loc) 5.66 kB
import {access, mkdir, writeFile} from 'node:fs/promises' import path from 'node:path' import {Args} from '@oclif/core' import {SanityCommand} from '@sanity/cli-core' import {chalk, confirm, input, select} from '@sanity/cli-core/ux' import {deburr} from 'lodash-es' import {getMigrationRootDirectory} from '../../actions/migration/getMigrationRootDirectory.js' import { minimalAdvanced, minimalSimple, renameField, renameType, stringToPTE, } from '../../actions/migration/templates/index.js' import {MIGRATIONS_DIRECTORY} from '../../utils/migration/constants.js' const TEMPLATES = [ {name: 'Minimalistic migration to get you started', template: minimalSimple}, {name: 'Rename an object type', template: renameType}, {name: 'Rename a field', template: renameField}, {name: 'Convert string field to Portable Text', template: stringToPTE}, { name: 'Advanced template using async iterators providing more fine grained control', template: minimalAdvanced, }, ] export class CreateMigrationCommand extends SanityCommand<typeof CreateMigrationCommand> { static override args = { title: Args.string({ description: 'Title of migration', required: false, }), } static override description = 'Create a new migration within your project' static override examples = [ { command: '<%= config.bin %> <%= command.id %>', description: 'Create a new migration, prompting for title and options', }, { command: '<%= config.bin %> <%= command.id %> "Rename field from location to address"', description: 'Create a new migration with the provided title, prompting for options', }, ] public async run(): Promise<void> { const {args} = await this.parse(CreateMigrationCommand) const workDir = await getMigrationRootDirectory(this.output) const title = await this.promptForTitle(args.title) const types = await this.promptForDocumentTypes() const {template} = await this.promptForTemplate() const renderedTemplate = (template || minimalSimple)({ documentTypes: types .split(',') .map((t) => t.trim()) .filter(Boolean), migrationName: title, }) const sluggedName = deburr(title.toLowerCase()) .replaceAll(/\s+/g, '-') .replaceAll(/[^a-z0-9-]/g, '') const destDir = path.join(workDir, MIGRATIONS_DIRECTORY, sluggedName) const definitionFile = path.join(destDir, 'index.ts') const dirCreated = await this.createMigrationFile(destDir, definitionFile, renderedTemplate) if (dirCreated) { this.log() this.log(`${chalk.green('✓')} Migration created!`) this.log() this.log('Next steps:') this.log( `Open ${chalk.bold( definitionFile, )} in your code editor and write the code for your migration.`, ) this.log( `Dry run the migration with:\n\`${chalk.bold( `sanity migration run ${sluggedName} --project=<projectId> --dataset <dataset> `, )}\``, ) this.log( `Run the migration against a dataset with:\n \`${chalk.bold( `sanity migration run ${sluggedName} --project=<projectId> --dataset <dataset> --no-dry-run`, )}\``, ) this.log() this.log( `👉 Learn more about schema and content migrations at ${chalk.bold( 'https://www.sanity.io/docs/schema-and-content-migrations', )}`, ) } } private async createMigrationFile( destDir: string, definitionFile: string, renderedTemplate: string, ): Promise<boolean> { const dirExists = await access(destDir) .then(() => true) .catch(() => false) if (dirExists) { const shouldOverwrite = await this.promptForOverwrite(destDir) if (!shouldOverwrite) return false } try { await mkdir(destDir, {recursive: true}) } catch (error) { const message = error instanceof Error ? error.message : String(error) this.error(`Failed to create migration directory: ${message}`, {exit: 1}) } try { await writeFile(definitionFile, renderedTemplate) } catch (error) { const message = error instanceof Error ? error.message : String(error) this.error(`Failed to create migration file: ${message}`, {exit: 1}) } return true } private async promptForDocumentTypes(): Promise<string> { return input({ message: 'Type of documents to migrate. You can add multiple types separated by comma (optional)', }) } private async promptForOverwrite(destDir: string): Promise<boolean> { return confirm({ default: false, message: `Migration directory ${chalk.cyan(destDir)} already exists. Overwrite?`, }) } private async promptForTemplate(): Promise<{ name: string template: (params: {documentTypes: string[]; migrationName: string}) => string }> { const templatesByName = Object.fromEntries(TEMPLATES.map((t) => [t.name, t])) const templateName = await select({ choices: TEMPLATES.map((definedTemplate) => ({ name: definedTemplate.name, value: definedTemplate.name, })), message: 'Select a template', }) return templatesByName[templateName]! } private async promptForTitle(providedTitle?: string): Promise<string> { if (providedTitle?.trim()) { return providedTitle } return input({ message: 'Title of migration (e.g. "Rename field from location to address")', validate: (value) => { if (!value.trim()) { return 'Title cannot be empty' } return true }, }) } }