All files / src/utils migrate.ts

84.75% Statements 50/59
83.33% Branches 20/24
100% Functions 13/13
84.48% Lines 49/58

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 1403x 3x                   3x       6x 6x 6x     6x     6x         6x 6x 6x       6x   24x 24x 18x 12x         6x             6x     18x 18x 18x 18x       18x 18x           18x 18x   18x             6x               6x           6x 6x   2x 2x 5x     1x 1x 1x 1x 1x 3x           1x         6x     6x 18x 16x 16x 16x 16x             16x                
import fs from 'fs'
import path from 'path'
import { Sqlite3Database } from '../sqlite3/Sqlite3Database'
import { Migrate } from '../interfaces/migrate.interfaces'
 
import MigrationFile = Migrate.MigrationFile
import MigrationParams = Migrate.MigrationParams
 
/**
 * Migrates database schema to the latest version
 */
export async function migrate (
  db: Sqlite3Database,
  config: MigrationParams = {}
) {
  config.force = config.force || false
  config.table = config.table || 'migrations'
  config.migrationsPath =
    config.migrationsPath || path.join(process.cwd(), 'migrations')
 
  const { force, table, migrationsPath } = config
 
  /* eslint-disable no-await-in-loop */
  const location = path.resolve(migrationsPath)
 
  // Get the list of migration files, for example:
  //   { id: 1, name: 'initial', filename: '001-initial.sql' }
  //   { id: 2, name: 'feature', filename: '002-feature.sql' }
  const migrations = await new Promise<MigrationFile[]>((resolve, reject) => {
    fs.readdir(location, (err, files) => {
      Iif (err) {
        return reject(err)
      }
 
      resolve(
        files
          .map(x => x.match(/^(\d+).(.*?)\.sql$/))
          .filter(x => x !== null)
          .map(x => ({ id: Number(x[1]), name: x[2], filename: x[0] }))
          .sort((a, b) => Math.sign(a.id - b.id))
      )
    })
  })
 
  Iif (!migrations.length) {
    throw new Error(`No migration files found in '${location}'.`)
  }
 
  // Get the list of migrations, for example:
  //   { id: 1, name: 'initial', filename: '001-initial.sql', up: ..., down: ... }
  //   { id: 2, name: 'feature', filename: '002-feature.sql', up: ..., down: ... }
  await Promise.all(
    migrations.map(
      migration =>
        new Promise((resolve, reject) => {
          const filename = path.join(location, migration.filename)
          fs.readFile(filename, 'utf-8', (err, data) => {
            Iif (err) {
              return reject(err)
            }
 
            const [up, down] = data.split(/^--\s+?down\b/im)
            Iif (!down) {
              const message = `The ${migration.filename} file does not contain '-- Down' separator.`
              return reject(new Error(message))
            }
 
            /* eslint-disable no-param-reassign */
            migration.up = up.replace(/^-- .*?$/gm, '').trim() // Remove comments
            migration.down = down.trim() // and trim whitespaces
            /* eslint-enable no-param-reassign */
            resolve()
          })
        })
    )
  )
 
  // Create a database table for migrations meta data if it doesn't exist
  await db.run(`CREATE TABLE IF NOT EXISTS "${table}" (
  id   INTEGER PRIMARY KEY,
  name TEXT    NOT NULL,
  up   TEXT    NOT NULL,
  down TEXT    NOT NULL
)`)
 
  // Get the list of already applied migrations
  let dbMigrations = await db.all(
    `SELECT id, name, up, down FROM "${table}" ORDER BY id ASC`
  )
 
  // Undo migrations that exist only in the database but not in files,
  // also undo the last migration if the `force` option is enabled.
  const lastMigration = migrations[migrations.length - 1]
  for (const migration of dbMigrations
    .slice()
    .sort((a, b) => Math.sign(b.id - a.id))) {
    if (
      !migrations.some(x => x.id === migration.id) ||
      (force && migration.id === lastMigration.id)
    ) {
      await db.run('BEGIN')
      try {
        await db.exec(migration.down)
        await db.run(`DELETE FROM "${table}" WHERE id = ?`, migration.id)
        await db.run('COMMIT')
        dbMigrations = dbMigrations.filter(x => x.id !== migration.id)
      } catch (err) {
        await db.run('ROLLBACK')
        throw err
      }
    } else {
      break
    }
  }
 
  // Apply pending migrations
  const lastMigrationId = dbMigrations.length
    ? dbMigrations[dbMigrations.length - 1].id
    : 0
  for (const migration of migrations) {
    if (migration.id > lastMigrationId) {
      await db.run('BEGIN')
      try {
        await db.exec(migration.up)
        await db.run(
          `INSERT INTO "${table}" (id, name, up, down) VALUES (?, ?, ?, ?)`,
          migration.id,
          migration.name,
          migration.up,
          migration.down
        )
        await db.run('COMMIT')
      } catch (err) {
        await db.run('ROLLBACK')
        throw err
      }
    }
  }
}