Skip to content

Commit

Permalink
feat(cli): Implement migrations in CLI
Browse files Browse the repository at this point in the history
  • Loading branch information
michaelbromley committed Apr 4, 2024
1 parent 1c44113 commit 9860abd
Show file tree
Hide file tree
Showing 7 changed files with 173 additions and 47 deletions.
19 changes: 18 additions & 1 deletion packages/cli/src/cli.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,33 @@
#! /usr/bin/env node

import { Command } from 'commander';
import pc from 'picocolors';

import { registerAddCommand } from './commands/add/add';
import { registerMigrateCommand } from './commands/migrate/migrate';

const program = new Command();

// eslint-disable-next-line @typescript-eslint/no-var-requires
const version = require('../package.json').version;

program.version(version).description('The Vendure CLI');
program
.version(version)
.usage(`vendure <command>`)
.description(
pc.blue(`
888
888
888
888 888 .d88b. 88888b. .d88888 888 888 888d888 .d88b.
888 888 d8P Y8b 888 "88b d88" 888 888 888 888P" d8P Y8b
Y88 88P 88888888 888 888 888 888 888 888 888 88888888
Y8bd8P Y8b. 888 888 Y88b 888 Y88b 888 888 Y8b.
Y88P "Y8888 888 888 "Y88888 "Y88888 888 "Y8888
`),
);

registerAddCommand(program);
registerMigrateCommand(program);

void program.parseAsync(process.argv);
Original file line number Diff line number Diff line change
@@ -1,30 +1,21 @@
import { cancel, isCancel, log, text } from '@clack/prompts';
import { cancel, isCancel, log, spinner, text } from '@clack/prompts';
import { generateMigration } from '@vendure/core';
import path from 'node:path';
import { register } from 'ts-node';

import { CliCommand, CliCommandReturnVal } from '../../../shared/cli-command';
import { analyzeProject } from '../../../shared/shared-prompts';
import { VendureConfigRef } from '../../../shared/vendure-config-ref';
import { VendurePluginRef } from '../../../shared/vendure-plugin-ref';
import { isRunningInTsNode } from '../../../utilities/utils';
import { loadVendureConfigFile } from '../load-vendure-config-file';

const cancelledMessage = 'Add entity cancelled';

export interface GenerateMigrationOptions {
plugin?: VendurePluginRef;
}
const cancelledMessage = 'Generate migration cancelled';

export const generateMigrationCommand = new CliCommand({
id: 'generate-migration',
category: 'Other',
description: 'Generate a new database migration',
run: options => runGenerateMigration(options),
run: () => runGenerateMigration(),
});

async function runGenerateMigration(
options?: Partial<GenerateMigrationOptions>,
): Promise<CliCommandReturnVal> {
async function runGenerateMigration(): Promise<CliCommandReturnVal> {
const project = await analyzeProject({ cancelledMessage });
const vendureConfig = new VendureConfigRef(project);
log.info('Using VendureConfig from ' + vendureConfig.getPathRelativeToProjectRoot());
Expand All @@ -44,26 +35,16 @@ async function runGenerateMigration(
process.exit(0);
}
const config = loadVendureConfigFile(vendureConfig);
await generateMigration(config, { name, outputDir: './src/migrations' });

const migrationSpinner = spinner();
migrationSpinner.start('Generating migration...');
const migrationName = await generateMigration(config, { name, outputDir: './src/migrations' });
const report =
typeof migrationName === 'string'
? `New migration generated: ${migrationName}`
: 'No changes in database schema were found, so no migration was generated';
migrationSpinner.stop(report);
return {
project,
modifiedSourceFiles: [],
};
}

function loadVendureConfigFile(vendureConfig: VendureConfigRef) {
if (!isRunningInTsNode()) {
const tsConfigPath = path.join(process.cwd(), 'tsconfig.json');
// eslint-disable-next-line @typescript-eslint/no-var-requires
const compilerOptions = require(tsConfigPath).compilerOptions;
register({ compilerOptions });
}
const exportedVarName = vendureConfig.getConfigObjectVariableName();
if (!exportedVarName) {
throw new Error('Could not find the exported variable name in the VendureConfig file');
}
// eslint-disable-next-line @typescript-eslint/no-var-requires
const config = require(vendureConfig.sourceFile.getFilePath())[exportedVarName];
return config;
}
21 changes: 21 additions & 0 deletions packages/cli/src/commands/migrate/load-vendure-config-file.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import path from 'node:path';
import { register } from 'ts-node';

import { VendureConfigRef } from '../../shared/vendure-config-ref';
import { isRunningInTsNode } from '../../utilities/utils';

export function loadVendureConfigFile(vendureConfig: VendureConfigRef) {
if (!isRunningInTsNode()) {
const tsConfigPath = path.join(process.cwd(), 'tsconfig.json');
// eslint-disable-next-line @typescript-eslint/no-var-requires
const compilerOptions = require(tsConfigPath).compilerOptions;
register({ compilerOptions, transpileOnly: true });
}
const exportedVarName = vendureConfig.getConfigObjectVariableName();
if (!exportedVarName) {
throw new Error('Could not find the exported variable name in the VendureConfig file');
}
// eslint-disable-next-line @typescript-eslint/no-var-requires
const config = require(vendureConfig.sourceFile.getFilePath())[exportedVarName];
return config;
}
12 changes: 11 additions & 1 deletion packages/cli/src/commands/migrate/migrate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import { Command } from 'commander';
import pc from 'picocolors';

import { generateMigrationCommand } from './generate-migration/generate-migration';
import { revertMigrationCommand } from './revert-migration/revert-migration';
import { runMigrationCommand } from './run-migration/run-migration';

const cancelledMessage = 'Migrate cancelled.';

Expand All @@ -18,7 +20,7 @@ export function registerMigrateCommand(program: Command) {
.action(async () => {
// eslint-disable-next-line no-console
console.log(`\n`);
intro(pc.blue('️ Vendure migrations'));
intro(pc.blue('🏗️ Vendure migrations'));
const action = await select({
message: 'What would you like to do?',
options: [
Expand All @@ -32,10 +34,18 @@ export function registerMigrateCommand(program: Command) {
process.exit(0);
}
try {
process.env.VENDURE_RUNNING_IN_CLI = 'true';
if (action === 'generate') {
await generateMigrationCommand.run();
}
if (action === 'run') {
await runMigrationCommand.run();
}
if (action === 'revert') {
await revertMigrationCommand.run();
}
outro('✅ Done!');
process.env.VENDURE_RUNNING_IN_CLI = undefined;
} catch (e: any) {
log.error(e.message as string);
if (e.stack) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { log, spinner } from '@clack/prompts';
import { revertLastMigration } from '@vendure/core';

import { CliCommand, CliCommandReturnVal } from '../../../shared/cli-command';
import { analyzeProject } from '../../../shared/shared-prompts';
import { VendureConfigRef } from '../../../shared/vendure-config-ref';
import { loadVendureConfigFile } from '../load-vendure-config-file';

const cancelledMessage = 'Revert migrations cancelled';

export const revertMigrationCommand = new CliCommand({
id: 'run-migration',
category: 'Other',
description: 'Run any pending database migrations',
run: () => runRevertMigration(),
});

async function runRevertMigration(): Promise<CliCommandReturnVal> {
const project = await analyzeProject({ cancelledMessage });
const vendureConfig = new VendureConfigRef(project);
log.info('Using VendureConfig from ' + vendureConfig.getPathRelativeToProjectRoot());
const config = loadVendureConfigFile(vendureConfig);

const runSpinner = spinner();
runSpinner.start('Reverting last migration...');
await revertLastMigration(config);
runSpinner.stop(`Successfully reverted last migration`);
return {
project,
modifiedSourceFiles: [],
};
}
35 changes: 35 additions & 0 deletions packages/cli/src/commands/migrate/run-migration/run-migration.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { log, spinner } from '@clack/prompts';
import { runMigrations } from '@vendure/core';

import { CliCommand, CliCommandReturnVal } from '../../../shared/cli-command';
import { analyzeProject } from '../../../shared/shared-prompts';
import { VendureConfigRef } from '../../../shared/vendure-config-ref';
import { loadVendureConfigFile } from '../load-vendure-config-file';

const cancelledMessage = 'Run migrations cancelled';

export const runMigrationCommand = new CliCommand({
id: 'run-migration',
category: 'Other',
description: 'Run any pending database migrations',
run: () => runRunMigration(),
});

async function runRunMigration(): Promise<CliCommandReturnVal> {
const project = await analyzeProject({ cancelledMessage });
const vendureConfig = new VendureConfigRef(project);
log.info('Using VendureConfig from ' + vendureConfig.getPathRelativeToProjectRoot());
const config = loadVendureConfigFile(vendureConfig);

const runSpinner = spinner();
runSpinner.start('Running migrations...');
const migrationsRan = await runMigrations(config);
const report = migrationsRan.length
? `Successfully ran ${migrationsRan.length} migrations`
: 'No pending migrations found';
runSpinner.stop(report);
return {
project,
modifiedSourceFiles: [],
};
}
56 changes: 43 additions & 13 deletions packages/core/src/migrate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,37 +37,44 @@ export interface MigrationOptions {
*
* @docsCategory migration
*/
export async function runMigrations(userConfig: Partial<VendureConfig>) {
export async function runMigrations(userConfig: Partial<VendureConfig>): Promise<string[]> {
const config = await preBootstrapConfig(userConfig);
const connection = await createConnection(createConnectionOptions(config));
const migrationsRan: string[] = [];
try {
const migrations = await disableForeignKeysForSqLite(connection, () =>
connection.runMigrations({ transaction: 'each' }),
);
for (const migration of migrations) {
console.log(chalk.green(`Successfully ran migration: ${migration.name}`));
log(chalk.green(`Successfully ran migration: ${migration.name}`));
migrationsRan.push(migration.name);
}
} catch (e: any) {
console.log(chalk.red('An error occurred when running migrations:'));
console.log(e.message);
process.exitCode = 1;
log(chalk.red('An error occurred when running migrations:'));
log(e.message);
if (isRunningFromVendureCli()) {
throw e;
} else {
process.exitCode = 1;
}
} finally {
await checkMigrationStatus(connection);
await connection.close();
resetConfig();
}
return migrationsRan;
}

async function checkMigrationStatus(connection: Connection) {
const builderLog = await connection.driver.createSchemaBuilder().log();
if (builderLog.upQueries.length) {
console.log(
log(
chalk.yellow(
'Your database schema does not match your current configuration. Generate a new migration for the following changes:',
),
);
for (const query of builderLog.upQueries) {
console.log(' - ' + chalk.yellow(query.query));
log(' - ' + chalk.yellow(query.query));
}
}
}
Expand All @@ -87,9 +94,13 @@ export async function revertLastMigration(userConfig: Partial<VendureConfig>) {
connection.undoLastMigration({ transaction: 'each' }),
);
} catch (e: any) {
console.log(chalk.red('An error occurred when reverting migration:'));
console.log(e.message);
process.exitCode = 1;
log(chalk.red('An error occurred when reverting migration:'));
log(e.message);
if (isRunningFromVendureCli()) {
throw e;
} else {
process.exitCode = 1;
}
} finally {
await connection.close();
resetConfig();
Expand All @@ -104,7 +115,10 @@ export async function revertLastMigration(userConfig: Partial<VendureConfig>) {
*
* @docsCategory migration
*/
export async function generateMigration(userConfig: Partial<VendureConfig>, options: MigrationOptions) {
export async function generateMigration(
userConfig: Partial<VendureConfig>,
options: MigrationOptions,
): Promise<string | undefined> {
const config = await preBootstrapConfig(userConfig);
const connection = await createConnection(createConnectionOptions(config));

Expand All @@ -113,6 +127,7 @@ export async function generateMigration(userConfig: Partial<VendureConfig>, opti
const sqlInMemory = await connection.driver.createSchemaBuilder().log();
const upSqls: string[] = [];
const downSqls: string[] = [];
let migrationName: string | undefined;

// mysql is exceptional here because it uses ` character in to escape names in queries, that's why for mysql
// we are using simple quoted string instead of template string syntax
Expand Down Expand Up @@ -168,13 +183,15 @@ export async function generateMigration(userConfig: Partial<VendureConfig>, opti
await fs.ensureFile(outputPath);
fs.writeFileSync(outputPath, fileContent);

console.log(chalk.green(`Migration ${chalk.blue(outputPath)} has been generated successfully.`));
log(chalk.green(`Migration ${chalk.blue(outputPath)} has been generated successfully.`));
migrationName = outputPath;
}
} else {
console.log(chalk.yellow('No changes in database schema were found - cannot generate a migration.'));
log(chalk.yellow('No changes in database schema were found - cannot generate a migration.'));
}
await connection.close();
resetConfig();
return migrationName;
}

function createConnectionOptions(userConfig: Partial<VendureConfig>): DataSourceOptions {
Expand Down Expand Up @@ -225,3 +242,16 @@ ${downSqls.join(`
}
`;
}

function log(message: string) {
// If running from within the Vendure CLI, we allow the CLI app
// to handle the logging.
if (isRunningFromVendureCli()) {
return;
}
console.log(message);
}

function isRunningFromVendureCli(): boolean {
return process.env.VENDURE_RUNNING_IN_CLI != null;
}

0 comments on commit 9860abd

Please sign in to comment.