migrate.ts 7.4 KB

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