migrate.ts 7.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208
  1. /* tslint:disable:no-console */
  2. import chalk from 'chalk';
  3. import fs from 'fs-extra';
  4. import path from 'path';
  5. import { Connection, ConnectionOptions, createConnection } from 'typeorm';
  6. import { MysqlDriver } from 'typeorm/driver/mysql/MysqlDriver';
  7. import { camelCase } from 'typeorm/util/StringUtils';
  8. import { preBootstrapConfig } from './bootstrap';
  9. import { VendureConfig } from './config/vendure-config';
  10. /**
  11. * @description
  12. * Configuration for generating a new migration script via {@link generateMigration}.
  13. *
  14. * @docsCategory migration
  15. */
  16. export interface MigrationOptions {
  17. /**
  18. * @description
  19. * The name of the migration. The resulting migration script will be named
  20. * `{TIMESTAMP}-{name}.ts`.
  21. */
  22. name: string;
  23. /**
  24. * @description
  25. * The output directory of the generated migration scripts.
  26. */
  27. outputDir?: string;
  28. }
  29. /**
  30. * @description
  31. * Runs any pending database migrations. See [TypeORM migration docs](https://typeorm.io/#/migrations)
  32. * for more information about the underlying migration mechanism.
  33. *
  34. * @docsCategory migration
  35. */
  36. export async function runMigrations(userConfig: Partial<VendureConfig>) {
  37. const config = await preBootstrapConfig(userConfig);
  38. const connection = await createConnection(createConnectionOptions(config));
  39. try {
  40. const migrations = await disableForeignKeysForSqLite(connection, () =>
  41. connection.runMigrations({ transaction: 'each' }),
  42. );
  43. for (const migration of migrations) {
  44. console.log(chalk.green(`Successfully ran migration: ${migration.name}`));
  45. }
  46. } catch (e) {
  47. console.log(chalk.red(`An error occurred when running migrations:`));
  48. console.log(e.message);
  49. process.exitCode = 1;
  50. } finally {
  51. await connection.close();
  52. }
  53. }
  54. /**
  55. * @description
  56. * Reverts the last applied database migration. See [TypeORM migration docs](https://typeorm.io/#/migrations)
  57. * for more information about the underlying migration mechanism.
  58. *
  59. * @docsCategory migration
  60. */
  61. export async function revertLastMigration(userConfig: Partial<VendureConfig>) {
  62. const config = await preBootstrapConfig(userConfig);
  63. const connection = await createConnection(createConnectionOptions(config));
  64. try {
  65. await disableForeignKeysForSqLite(connection, () =>
  66. connection.undoLastMigration({ transaction: 'each' }),
  67. );
  68. } catch (e) {
  69. console.log(chalk.red(`An error occurred when reverting migration:`));
  70. console.log(e.message);
  71. process.exitCode = 1;
  72. } finally {
  73. await connection.close();
  74. }
  75. }
  76. /**
  77. * @description
  78. * Generates a new migration file based on any schema changes (e.g. adding or removing CustomFields).
  79. * See [TypeORM migration docs](https://typeorm.io/#/migrations) for more information about the
  80. * underlying migration mechanism.
  81. *
  82. * @docsCategory migration
  83. */
  84. export async function generateMigration(userConfig: Partial<VendureConfig>, options: MigrationOptions) {
  85. const config = await preBootstrapConfig(userConfig);
  86. const connection = await createConnection(createConnectionOptions(config));
  87. // TODO: This can hopefully be simplified if/when TypeORM exposes this CLI command directly.
  88. // See https://github.com/typeorm/typeorm/issues/4494
  89. const sqlInMemory = await connection.driver.createSchemaBuilder().log();
  90. const upSqls: string[] = [];
  91. const downSqls: string[] = [];
  92. // mysql is exceptional here because it uses ` character in to escape names in queries, that's why for mysql
  93. // we are using simple quoted string instead of template string syntax
  94. if (connection.driver instanceof MysqlDriver) {
  95. sqlInMemory.upQueries.forEach(upQuery => {
  96. upSqls.push(
  97. ' await queryRunner.query("' +
  98. upQuery.query.replace(new RegExp(`"`, 'g'), `\\"`) +
  99. '", ' +
  100. JSON.stringify(upQuery.parameters) +
  101. ');',
  102. );
  103. });
  104. sqlInMemory.downQueries.forEach(downQuery => {
  105. downSqls.push(
  106. ' await queryRunner.query("' +
  107. downQuery.query.replace(new RegExp(`"`, 'g'), `\\"`) +
  108. '", ' +
  109. JSON.stringify(downQuery.parameters) +
  110. ');',
  111. );
  112. });
  113. } else {
  114. sqlInMemory.upQueries.forEach(upQuery => {
  115. upSqls.push(
  116. ' await queryRunner.query(`' +
  117. upQuery.query.replace(new RegExp('`', 'g'), '\\`') +
  118. '`, ' +
  119. JSON.stringify(upQuery.parameters) +
  120. ');',
  121. );
  122. });
  123. sqlInMemory.downQueries.forEach(downQuery => {
  124. downSqls.push(
  125. ' await queryRunner.query(`' +
  126. downQuery.query.replace(new RegExp('`', 'g'), '\\`') +
  127. '`, ' +
  128. JSON.stringify(downQuery.parameters) +
  129. ');',
  130. );
  131. });
  132. }
  133. if (upSqls.length) {
  134. if (options.name) {
  135. const timestamp = new Date().getTime();
  136. const filename = timestamp + '-' + options.name + '.ts';
  137. const directory = options.outputDir;
  138. const fileContent = getTemplate(options.name as any, timestamp, upSqls, downSqls.reverse());
  139. const outputPath = directory
  140. ? path.join(directory, filename)
  141. : path.join(process.cwd(), filename);
  142. await fs.ensureFile(outputPath);
  143. await fs.writeFileSync(outputPath, fileContent);
  144. console.log(chalk.green(`Migration ${chalk.blue(outputPath)} has been generated successfully.`));
  145. }
  146. } else {
  147. console.log(chalk.yellow(`No changes in database schema were found - cannot generate a migration.`));
  148. }
  149. await connection.close();
  150. }
  151. function createConnectionOptions(userConfig: Partial<VendureConfig>): ConnectionOptions {
  152. return Object.assign({ logging: ['query', 'error', 'schema'] }, userConfig.dbConnectionOptions, {
  153. subscribers: [],
  154. synchronize: false,
  155. migrationsRun: false,
  156. dropSchema: false,
  157. logger: 'advanced-console',
  158. });
  159. }
  160. /**
  161. * There is a bug in TypeORM which causes db schema changes to fail with SQLite. This
  162. * is a work-around for the issue.
  163. * See https://github.com/typeorm/typeorm/issues/2576#issuecomment-499506647
  164. */
  165. async function disableForeignKeysForSqLite<T>(connection: Connection, work: () => Promise<T>): Promise<T> {
  166. const isSqLite = connection.options.type === 'sqlite';
  167. if (isSqLite) {
  168. await connection.query('PRAGMA foreign_keys=OFF');
  169. }
  170. const result = await work();
  171. if (isSqLite) {
  172. await connection.query('PRAGMA foreign_keys=ON');
  173. }
  174. return result;
  175. }
  176. /**
  177. * Gets contents of the migration file.
  178. */
  179. function getTemplate(name: string, timestamp: number, upSqls: string[], downSqls: string[]): string {
  180. return `import {MigrationInterface, QueryRunner} from "typeorm";
  181. export class ${camelCase(name, true)}${timestamp} implements MigrationInterface {
  182. public async up(queryRunner: QueryRunner): Promise<any> {
  183. ${upSqls.join(`
  184. `)}
  185. }
  186. public async down(queryRunner: QueryRunner): Promise<any> {
  187. ${downSqls.join(`
  188. `)}
  189. }
  190. }
  191. `;
  192. }