migrate.ts 8.7 KB

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