db-migration.class.ts 3.3 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697
  1. import fs from 'fs';
  2. import fsp from 'fs/promises';
  3. import path from 'path';
  4. import { Database } from 'sqlite3';
  5. import { Logger } from '../../../common/util/logger.class';
  6. import { MigrationException } from '../lib/migration-exception.class';
  7. import { MigrationRunner } from '../lib/migration-runner.interface';
  8. import { SQLiteController } from './sqlite-controller.base';
  9. export class DBMigration extends SQLiteController {
  10. constructor(protected db: Database) {
  11. super();
  12. }
  13. public async update() {
  14. const migrationsDir = path.resolve(__dirname, '../migrations');
  15. if (!fs.existsSync(migrationsDir)) return;
  16. const files = await fsp.readdir(migrationsDir);
  17. if (files.length) {
  18. files.sort((a, b) => Number(a.substring(0, 12)) - Number(b.substring(0, 12)));
  19. await this.createMigrationsTable();
  20. const lastID = await this.getLastID();
  21. Logger.debug('[DEBUG] lastid', lastID);
  22. for (const file of files) {
  23. const m = /^(\d{12})_(.*)\.(sql|js)$/.exec(file);
  24. if (!m) {
  25. throw new MigrationException(`File ${file} does not match migration file pattern. Aborted processing migrations.`);
  26. }
  27. const id = Number(m[1]);
  28. if (id <= lastID) continue;
  29. if (m[3] === 'sql') {
  30. const migFilepath = path.join(migrationsDir, file);
  31. const migration = await fsp.readFile(migFilepath, { encoding: 'utf-8' });
  32. Logger.info('[INFO] Applying SQL DB migration', file);
  33. await this.beginTransaction();
  34. try {
  35. await this.exec(migration);
  36. await this.run('INSERT INTO db_migrations(id, title, migrated) VALUES(?, ?, ?);', [id, m[2], new Date().getTime()]);
  37. await this.commit();
  38. Logger.info('[INFO] DB migration', file, 'succeeded.');
  39. } catch (error) {
  40. Logger.error('[ERROR] DB migration failed at', file, '- Rolling back...');
  41. await this.rollback();
  42. throw error;
  43. }
  44. } else {
  45. const migFilepath = path.join(migrationsDir, file.substring(0, file.length - 3));
  46. const imp = require(migFilepath).default;
  47. if (typeof imp !== 'function') {
  48. throw new MigrationException(`File ${file} is not a valid <MigrationRunner> implementation!`);
  49. }
  50. const mig = imp as MigrationRunner;
  51. Logger.info('[INFO] Applying TypeScript DB migration', file);
  52. await this.beginTransaction();
  53. try {
  54. await mig(this);
  55. await this.run('INSERT INTO db_migrations(id, title, migrated) VALUES(?, ?, ?);', [id, m[2], new Date().getTime()]);
  56. await this.commit();
  57. Logger.info('[INFO] DB migration', file, 'succeeded.');
  58. } catch (error) {
  59. Logger.error('[ERROR] DB migration failed at', file, '- Rolling back...');
  60. await this.rollback();
  61. throw error;
  62. }
  63. }
  64. }
  65. }
  66. }
  67. private async createMigrationsTable() {
  68. await this.run(
  69. `CREATE TABLE IF NOT EXISTS db_migrations (
  70. id INTEGER PRIMARY KEY,
  71. title TEXT NOT NULL,
  72. migrated INTEGER NOT NULL
  73. );`,
  74. []
  75. );
  76. }
  77. private async getLastID() {
  78. const results = await this.stmt(`SELECT id FROM db_migrations ORDER BY id DESC LIMIT 0, 1;`, []);
  79. return Number(results.rows[0]?.['id'] ?? '0');
  80. }
  81. }