Răsfoiți Sursa

feat(cli): Implement migrations in CLI

Michael Bromley 1 an în urmă
părinte
comite
9860abdbe9

+ 18 - 1
packages/cli/src/cli.ts

@@ -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);

+ 13 - 32
packages/cli/src/commands/migrate/generate-migration/generate-migration.ts

@@ -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());
@@ -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 - 0
packages/cli/src/commands/migrate/load-vendure-config-file.ts

@@ -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;
+}

+ 11 - 1
packages/cli/src/commands/migrate/migrate.ts

@@ -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.';
 
@@ -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: [
@@ -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) {

+ 32 - 0
packages/cli/src/commands/migrate/revert-migration/revert-migration.ts

@@ -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 - 0
packages/cli/src/commands/migrate/run-migration/run-migration.ts

@@ -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: [],
+    };
+}

+ 43 - 13
packages/core/src/migrate.ts

@@ -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));
         }
     }
 }
@@ -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();
@@ -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));
 
@@ -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
@@ -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 {
@@ -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;
+}