migrate.ts 8.0 KB

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