migrate.ts 6.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188
  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. await disableForeignKeysForSqLite(connection, () => connection.runMigrations({ transaction: 'each' }));
  40. await connection.close();
  41. }
  42. /**
  43. * @description
  44. * Reverts the last applied database migration. See [TypeORM migration docs](https://typeorm.io/#/migrations)
  45. * for more information about the underlying migration mechanism.
  46. *
  47. * @docsCategory migration
  48. */
  49. export async function revertLastMigration(userConfig: Partial<VendureConfig>) {
  50. const config = await preBootstrapConfig(userConfig);
  51. const connection = await createConnection(createConnectionOptions(config));
  52. await disableForeignKeysForSqLite(connection, () =>
  53. connection.undoLastMigration({ transaction: 'each' }),
  54. );
  55. await connection.close();
  56. }
  57. /**
  58. * @description
  59. * Generates a new migration file based on any schema changes (e.g. adding or removing CustomFields).
  60. * See [TypeORM migration docs](https://typeorm.io/#/migrations) for more information about the
  61. * underlying migration mechanism.
  62. *
  63. * @docsCategory migration
  64. */
  65. export async function generateMigration(userConfig: Partial<VendureConfig>, options: MigrationOptions) {
  66. const config = await preBootstrapConfig(userConfig);
  67. const connection = await createConnection(createConnectionOptions(config));
  68. // TODO: This can hopefully be simplified if/when TypeORM exposes this CLI command directly.
  69. // See https://github.com/typeorm/typeorm/issues/4494
  70. const sqlInMemory = await connection.driver.createSchemaBuilder().log();
  71. const upSqls: string[] = [];
  72. const downSqls: string[] = [];
  73. // mysql is exceptional here because it uses ` character in to escape names in queries, that's why for mysql
  74. // we are using simple quoted string instead of template string syntax
  75. if (connection.driver instanceof MysqlDriver) {
  76. sqlInMemory.upQueries.forEach(upQuery => {
  77. upSqls.push(
  78. ' await queryRunner.query("' +
  79. upQuery.query.replace(new RegExp(`"`, 'g'), `\\"`) +
  80. '", ' +
  81. JSON.stringify(upQuery.parameters) +
  82. ');',
  83. );
  84. });
  85. sqlInMemory.downQueries.forEach(downQuery => {
  86. downSqls.push(
  87. ' await queryRunner.query("' +
  88. downQuery.query.replace(new RegExp(`"`, 'g'), `\\"`) +
  89. '", ' +
  90. JSON.stringify(downQuery.parameters) +
  91. ');',
  92. );
  93. });
  94. } else {
  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. }
  114. if (upSqls.length) {
  115. if (options.name) {
  116. const timestamp = new Date().getTime();
  117. const filename = timestamp + '-' + options.name + '.ts';
  118. const directory = options.outputDir;
  119. const fileContent = getTemplate(options.name as any, timestamp, upSqls, downSqls.reverse());
  120. const outputPath = directory
  121. ? path.join(directory, filename)
  122. : path.join(process.cwd(), filename);
  123. await fs.ensureFile(outputPath);
  124. await fs.writeFileSync(outputPath, fileContent);
  125. console.log(chalk.green(`Migration ${chalk.blue(outputPath)} has been generated successfully.`));
  126. }
  127. } else {
  128. console.log(chalk.yellow(`No changes in database schema were found - cannot generate a migration.`));
  129. }
  130. await connection.close();
  131. }
  132. function createConnectionOptions(userConfig: Partial<VendureConfig>): ConnectionOptions {
  133. return Object.assign({ logging: ['query', 'error', 'schema'] }, userConfig.dbConnectionOptions, {
  134. subscribers: [],
  135. synchronize: false,
  136. migrationsRun: false,
  137. dropSchema: false,
  138. logger: 'advanced-console',
  139. });
  140. }
  141. /**
  142. * There is a bug in TypeORM which causes db schema changes to fail with SQLite. This
  143. * is a work-around for the issue.
  144. * See https://github.com/typeorm/typeorm/issues/2576#issuecomment-499506647
  145. */
  146. async function disableForeignKeysForSqLite(connection: Connection, work: () => Promise<any>) {
  147. const isSqLite = connection.options.type === 'sqlite';
  148. if (isSqLite) {
  149. await connection.query('PRAGMA foreign_keys=OFF');
  150. }
  151. await work();
  152. if (isSqLite) {
  153. await connection.query('PRAGMA foreign_keys=ON');
  154. }
  155. }
  156. /**
  157. * Gets contents of the migration file.
  158. */
  159. function getTemplate(name: string, timestamp: number, upSqls: string[], downSqls: string[]): string {
  160. return `import {MigrationInterface, QueryRunner} from "typeorm";
  161. export class ${camelCase(name, true)}${timestamp} implements MigrationInterface {
  162. public async up(queryRunner: QueryRunner): Promise<any> {
  163. ${upSqls.join(`
  164. `)}
  165. }
  166. public async down(queryRunner: QueryRunner): Promise<any> {
  167. ${downSqls.join(`
  168. `)}
  169. }
  170. }
  171. `;
  172. }