Browse Source

feat(core): Export database migration helpers

Michael Bromley 6 years ago
parent
commit
d509805f58

+ 1 - 5
packages/core/mock-data/clear-all-tables.ts

@@ -1,13 +1,9 @@
 import { createConnection } from 'typeorm';
 
-import { Type } from '../../common/lib/shared-types';
 import { isTestEnvironment } from '../e2e/utils/test-environment';
-import { getAllEntities, preBootstrapConfig } from '../src/bootstrap';
+import { preBootstrapConfig } from '../src/bootstrap';
 import { defaultConfig } from '../src/config/default-config';
 import { VendureConfig } from '../src/config/vendure-config';
-import { coreEntitiesMap } from '../src/entity/entities';
-import { registerCustomEntityFields } from '../src/entity/register-custom-entity-fields';
-import { setEntityIdStrategy } from '../src/entity/set-entity-id-strategy';
 
 // tslint:disable:no-console
 // tslint:disable:no-floating-promises

+ 3 - 3
packages/core/src/config/logger/typeorm-logger.ts

@@ -11,13 +11,13 @@ export class TypeOrmLogger implements TypeOrmLoggerInterface {
     log(level: 'log' | 'info' | 'warn', message: any, queryRunner?: QueryRunner): any {
         switch (level) {
             case 'info':
-                Logger.info(message, context);;
+                Logger.info(message, context);
                 break;
             case 'log':
-                Logger.verbose(message, context);;
+                Logger.verbose(message, context);
                 break;
             case 'warn':
-                Logger.warn(message, context);;
+                Logger.warn(message, context);
                 break;
         }
     }

+ 1 - 0
packages/core/src/index.ts

@@ -1,4 +1,5 @@
 export { bootstrap, bootstrapWorker } from './bootstrap';
+export { generateMigration, revertLastMigration, runMigrations } from './migrate';
 export * from './api/index';
 export * from './common/index';
 export * from './config/index';

+ 172 - 0
packages/core/src/migrate.ts

@@ -0,0 +1,172 @@
+/* tslint:disable:no-console */
+import chalk from 'chalk';
+import fs from 'fs-extra';
+import path from 'path';
+import { Connection, createConnection } from 'typeorm';
+import { MysqlDriver } from 'typeorm/driver/mysql/MysqlDriver';
+import { camelCase } from 'typeorm/util/StringUtils';
+
+import { preBootstrapConfig } from './bootstrap';
+import { VendureConfig } from './config/vendure-config';
+
+export interface MigrationOptions {
+    name: string;
+    outputDir?: string;
+}
+
+/**
+ * Runs any pending database migrations.
+ */
+export async function runMigrations(userConfig: Partial<VendureConfig>) {
+    const config = await preBootstrapConfig(userConfig);
+    Object.assign(config.dbConnectionOptions, {
+        subscribers: [],
+        synchronize: false,
+        migrationsRun: false,
+        dropSchema: false,
+        logger: 'advanced-console',
+        logging: ['query', 'error', 'schema'],
+    });
+    const connection = await createConnection(config.dbConnectionOptions);
+    await disableForeignKeysForSqLite(connection, () => connection.runMigrations({ transaction: true }));
+    await connection.close();
+}
+
+/**
+ * Reverts the last applied database migration.
+ */
+export async function revertLastMigration(userConfig: Partial<VendureConfig>) {
+    const config = await preBootstrapConfig(userConfig);
+    Object.assign(config.dbConnectionOptions, {
+        subscribers: [],
+        synchronize: false,
+        migrationsRun: false,
+        dropSchema: false,
+        logger: 'advanced-console',
+        logging: ['query', 'error', 'schema'],
+    });
+    const connection = await createConnection(config.dbConnectionOptions);
+    await disableForeignKeysForSqLite(connection, () => connection.undoLastMigration({ transaction: true }));
+    await connection.close();
+}
+
+/**
+ * Generates a new migration file based on any schema changes (e.g. adding or removing CustomFields).
+ */
+export async function generateMigration(userConfig: Partial<VendureConfig>, options: MigrationOptions) {
+    const config = await preBootstrapConfig(userConfig);
+    Object.assign(config.dbConnectionOptions, {
+        synchronize: false,
+        migrationsRun: false,
+        dropSchema: false,
+        logging: false,
+    });
+    const connection = await createConnection(config.dbConnectionOptions);
+
+    // TODO: This can hopefully be simplified if/when TypeORM exposes this CLI command directly.
+    // See https://github.com/typeorm/typeorm/issues/4494
+    const sqlInMemory = await connection.driver.createSchemaBuilder().log();
+    const upSqls: string[] = [];
+    const downSqls: string[] = [];
+
+    // 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
+    if (connection.driver instanceof MysqlDriver) {
+        sqlInMemory.upQueries.forEach(upQuery => {
+            upSqls.push(
+                '        await queryRunner.query("' +
+                    upQuery.query.replace(new RegExp(`"`, 'g'), `\\"`) +
+                    '", ' +
+                    JSON.stringify(upQuery.parameters) +
+                    ');',
+            );
+        });
+        sqlInMemory.downQueries.forEach(downQuery => {
+            downSqls.push(
+                '        await queryRunner.query("' +
+                    downQuery.query.replace(new RegExp(`"`, 'g'), `\\"`) +
+                    '", ' +
+                    JSON.stringify(downQuery.parameters) +
+                    ');',
+            );
+        });
+    } else {
+        sqlInMemory.upQueries.forEach(upQuery => {
+            upSqls.push(
+                '        await queryRunner.query(`' +
+                    upQuery.query.replace(new RegExp('`', 'g'), '\\`') +
+                    '`, ' +
+                    JSON.stringify(upQuery.parameters) +
+                    ');',
+            );
+        });
+        sqlInMemory.downQueries.forEach(downQuery => {
+            downSqls.push(
+                '        await queryRunner.query(`' +
+                    downQuery.query.replace(new RegExp('`', 'g'), '\\`') +
+                    '`, ' +
+                    JSON.stringify(downQuery.parameters) +
+                    ');',
+            );
+        });
+    }
+
+    if (upSqls.length) {
+        if (options.name) {
+            const timestamp = new Date().getTime();
+            const filename = timestamp + '-' + options.name + '.ts';
+            const directory = options.outputDir;
+            const fileContent = getTemplate(options.name as any, timestamp, upSqls, downSqls.reverse());
+            const outputPath = path.join(process.cwd(), directory ? directory + '/' : '', filename);
+            await fs.ensureFile(outputPath);
+            await fs.writeFileSync(outputPath, fileContent);
+
+            console.log(chalk.green(`Migration ${chalk.blue(outputPath)} has been generated successfully.`));
+        }
+    } else {
+        console.log(
+            chalk.yellow(
+                `No changes in database schema were found - cannot generate a migration. To create a new empty migration use "typeorm migration:create" command`,
+            ),
+        );
+    }
+    await connection.close();
+}
+
+/**
+ * There is a bug in TypeORM which causes db schema changes to fail with SQLite. This
+ * is a work-around for the issue.
+ * See https://github.com/typeorm/typeorm/issues/2576#issuecomment-499506647
+ */
+async function disableForeignKeysForSqLite(connection: Connection, work: () => Promise<any>) {
+    const isSqLite = connection.options.type === 'sqlite';
+    if (isSqLite) {
+        await connection.query('PRAGMA foreign_keys=OFF');
+    }
+    await work();
+    if (isSqLite) {
+        await connection.query('PRAGMA foreign_keys=ON');
+    }
+}
+
+/**
+ * Gets contents of the migration file.
+ */
+function getTemplate(name: string, timestamp: number, upSqls: string[], downSqls: string[]): string {
+    return `import {MigrationInterface, QueryRunner} from "typeorm";
+
+export class ${camelCase(name, true)}${timestamp} implements MigrationInterface {
+
+   public async up(queryRunner: QueryRunner): Promise<any> {
+${upSqls.join(`
+`)}
+   }
+
+   public async down(queryRunner: QueryRunner): Promise<any> {
+${downSqls.join(`
+`)}
+   }
+
+}
+`;
+}

+ 1 - 0
packages/create/templates/vendure-config.hbs

@@ -50,6 +50,7 @@ const path = require('path');
         username: '{{ dbUserName }}',
         password: '{{ dbPassword }}',
         {{/if}}
+        migrations: [path.join(__dirname, 'migrations/*.ts')],
     },
     paymentOptions: {
         paymentMethodHandlers: [examplePaymentHandler],

+ 4 - 3
packages/dev-server/dev-config.ts

@@ -31,6 +31,7 @@ export const devConfig: VendureConfig = {
     dbConnectionOptions: {
         synchronize: false,
         logging: false,
+        migrations: [path.join(__dirname, 'migrations/*.ts')],
         ...getDbConfig(),
     },
     paymentOptions: {
@@ -79,7 +80,7 @@ function getDbConfig(): ConnectionOptions {
         case 'postgres':
             console.log('Using postgres connection');
             return {
-                synchronize: true,
+                synchronize: false,
                 type: 'postgres',
                 host: '127.0.0.1',
                 port: 5432,
@@ -90,7 +91,7 @@ function getDbConfig(): ConnectionOptions {
         case 'sqlite':
             console.log('Using sqlite connection');
             return {
-                synchronize: true,
+                synchronize: false,
                 type: 'sqlite',
                 database: path.join(__dirname, 'vendure.sqlite'),
             };
@@ -106,7 +107,7 @@ function getDbConfig(): ConnectionOptions {
         default:
             console.log('Using mysql connection');
             return {
-                synchronize: true,
+                synchronize: false,
                 type: 'mysql',
                 host: '192.168.99.100',
                 port: 3306,

+ 27 - 0
packages/dev-server/migration.ts

@@ -0,0 +1,27 @@
+import { generateMigration, revertLastMigration, runMigrations } from '@vendure/core';
+import program from 'commander';
+
+import { devConfig } from './dev-config';
+
+program
+    .command('generate <name>')
+    .description('Generate a new migration file with the given name')
+    .action(name => {
+        return generateMigration(devConfig, { name, outputDir: './migrations' });
+    });
+
+program
+    .command('run')
+    .description('Run all pending migrations')
+    .action(() => {
+        return runMigrations(devConfig);
+    });
+
+program
+    .command('revert')
+    .description('Revert the last applied migration')
+    .action(() => {
+        return revertLastMigration(devConfig);
+    });
+
+program.parse(process.argv);