Fix migrationMeta database version initial value, and move isDatabaseNew logic inside MigrationManager

This commit is contained in:
mikiher 2024-09-14 08:01:32 +03:00
parent 5b09bd8242
commit 55164803b0
3 changed files with 85 additions and 16 deletions

View File

@ -171,9 +171,9 @@ class Database {
} }
try { try {
const migrationManager = new MigrationManager(this.sequelize, global.ConfigPath) const migrationManager = new MigrationManager(this.sequelize, this.isNew, global.ConfigPath)
await migrationManager.init(packageJson.version) await migrationManager.init(packageJson.version)
if (!this.isNew) await migrationManager.runMigrations() await migrationManager.runMigrations()
} catch (error) { } catch (error) {
Logger.error(`[Database] Failed to run migrations`, error) Logger.error(`[Database] Failed to run migrations`, error)
throw new Error('Database migration failed') throw new Error('Database migration failed')

View File

@ -11,11 +11,13 @@ class MigrationManager {
/** /**
* @param {import('../Database').sequelize} sequelize * @param {import('../Database').sequelize} sequelize
* @param {boolean} isDatabaseNew
* @param {string} [configPath] * @param {string} [configPath]
*/ */
constructor(sequelize, configPath = global.configPath) { constructor(sequelize, isDatabaseNew, configPath = global.configPath) {
if (!sequelize || !(sequelize instanceof Sequelize)) throw new Error('Sequelize instance is required for MigrationManager.') if (!sequelize || !(sequelize instanceof Sequelize)) throw new Error('Sequelize instance is required for MigrationManager.')
this.sequelize = sequelize this.sequelize = sequelize
this.isDatabaseNew = isDatabaseNew
if (!configPath) throw new Error('Config path is required for MigrationManager.') if (!configPath) throw new Error('Config path is required for MigrationManager.')
this.configPath = configPath this.configPath = configPath
this.migrationsSourceDir = path.join(__dirname, '..', 'migrations') this.migrationsSourceDir = path.join(__dirname, '..', 'migrations')
@ -42,6 +44,7 @@ class MigrationManager {
await this.fetchVersionsFromDatabase() await this.fetchVersionsFromDatabase()
if (!this.maxVersion || !this.databaseVersion) throw new Error('Failed to fetch versions from the database.') if (!this.maxVersion || !this.databaseVersion) throw new Error('Failed to fetch versions from the database.')
Logger.debug(`[MigrationManager] Database version: ${this.databaseVersion}, Max version: ${this.maxVersion}, Server version: ${this.serverVersion}`)
if (semver.gt(this.serverVersion, this.maxVersion)) { if (semver.gt(this.serverVersion, this.maxVersion)) {
try { try {
@ -63,6 +66,11 @@ class MigrationManager {
async runMigrations() { async runMigrations() {
if (!this.initialized) throw new Error('MigrationManager is not initialized. Call init() first.') if (!this.initialized) throw new Error('MigrationManager is not initialized. Call init() first.')
if (this.isDatabaseNew) {
Logger.info('[MigrationManager] Database is new. Skipping migrations.')
return
}
const versionCompare = semver.compare(this.serverVersion, this.databaseVersion) const versionCompare = semver.compare(this.serverVersion, this.databaseVersion)
if (versionCompare == 0) { if (versionCompare == 0) {
Logger.info('[MigrationManager] Database is already up to date.') Logger.info('[MigrationManager] Database is already up to date.')
@ -180,7 +188,15 @@ class MigrationManager {
async checkOrCreateMigrationsMetaTable() { async checkOrCreateMigrationsMetaTable() {
const queryInterface = this.sequelize.getQueryInterface() const queryInterface = this.sequelize.getQueryInterface()
if (!(await queryInterface.tableExists(MigrationManager.MIGRATIONS_META_TABLE))) { let migrationsMetaTableExists = await queryInterface.tableExists(MigrationManager.MIGRATIONS_META_TABLE)
if (this.isDatabaseNew && migrationsMetaTableExists) {
// This can happen if database was initialized with force: true
await queryInterface.dropTable(MigrationManager.MIGRATIONS_META_TABLE)
migrationsMetaTableExists = false
}
if (!migrationsMetaTableExists) {
await queryInterface.createTable(MigrationManager.MIGRATIONS_META_TABLE, { await queryInterface.createTable(MigrationManager.MIGRATIONS_META_TABLE, {
key: { key: {
type: DataTypes.STRING, type: DataTypes.STRING,
@ -192,9 +208,10 @@ class MigrationManager {
} }
}) })
await this.sequelize.query("INSERT INTO :migrationsMeta (key, value) VALUES ('version', :version), ('maxVersion', '0.0.0')", { await this.sequelize.query("INSERT INTO :migrationsMeta (key, value) VALUES ('version', :version), ('maxVersion', '0.0.0')", {
replacements: { version: this.serverVersion, migrationsMeta: MigrationManager.MIGRATIONS_META_TABLE }, replacements: { version: this.isDatabaseNew ? this.serverVersion : '0.0.0', migrationsMeta: MigrationManager.MIGRATIONS_META_TABLE },
type: Sequelize.QueryTypes.INSERT type: Sequelize.QueryTypes.INSERT
}) })
Logger.debug(`[MigrationManager] Created migrationsMeta table: "${MigrationManager.MIGRATIONS_META_TABLE}"`)
} }
} }
@ -219,6 +236,7 @@ class MigrationManager {
await fs.copy(sourceFile, targetFile) // Asynchronously copy the files await fs.copy(sourceFile, targetFile) // Asynchronously copy the files
}) })
) )
Logger.debug(`[MigrationManager] Copied migrations to the config directory: "${this.migrationsDir}"`)
} }
/** /**

View File

@ -31,7 +31,7 @@ describe('MigrationManager', () => {
down: sinon.stub() down: sinon.stub()
} }
sequelizeStub.getQueryInterface.returns({}) sequelizeStub.getQueryInterface.returns({})
migrationManager = new MigrationManager(sequelizeStub, configPath) migrationManager = new MigrationManager(sequelizeStub, false, configPath)
migrationManager.fetchVersionsFromDatabase = sinon.stub().resolves() migrationManager.fetchVersionsFromDatabase = sinon.stub().resolves()
migrationManager.copyMigrationsToConfigDir = sinon.stub().resolves() migrationManager.copyMigrationsToConfigDir = sinon.stub().resolves()
migrationManager.updateMaxVersion = sinon.stub().resolves() migrationManager.updateMaxVersion = sinon.stub().resolves()
@ -131,6 +131,21 @@ describe('MigrationManager', () => {
expect(loggerInfoStub.calledWith(sinon.match('Migrations successfully applied'))).to.be.true expect(loggerInfoStub.calledWith(sinon.match('Migrations successfully applied'))).to.be.true
}) })
it('should log that migrations will be skipped if database is new', async () => {
// Arrange
migrationManager.isDatabaseNew = true
migrationManager.initialized = true
// Act
await migrationManager.runMigrations()
// Assert
expect(loggerInfoStub.calledWith(sinon.match('Database is new. Skipping migrations.'))).to.be.true
expect(migrationManager.initUmzug.called).to.be.false
expect(umzugStub.up.called).to.be.false
expect(umzugStub.down.called).to.be.false
})
it('should log that no migrations are needed if serverVersion equals databaseVersion', async () => { it('should log that no migrations are needed if serverVersion equals databaseVersion', async () => {
// Arrange // Arrange
migrationManager.serverVersion = '1.2.0' migrationManager.serverVersion = '1.2.0'
@ -181,7 +196,7 @@ describe('MigrationManager', () => {
// Create a migrationsMeta table and populate it with version and maxVersion // Create a migrationsMeta table and populate it with version and maxVersion
await sequelize.query('CREATE TABLE migrationsMeta (key VARCHAR(255), value VARCHAR(255))') await sequelize.query('CREATE TABLE migrationsMeta (key VARCHAR(255), value VARCHAR(255))')
await sequelize.query("INSERT INTO migrationsMeta (key, value) VALUES ('version', '1.1.0'), ('maxVersion', '1.1.0')") await sequelize.query("INSERT INTO migrationsMeta (key, value) VALUES ('version', '1.1.0'), ('maxVersion', '1.1.0')")
const migrationManager = new MigrationManager(sequelize, configPath) const migrationManager = new MigrationManager(sequelize, false, configPath)
migrationManager.checkOrCreateMigrationsMetaTable = sinon.stub().resolves() migrationManager.checkOrCreateMigrationsMetaTable = sinon.stub().resolves()
// Act // Act
@ -195,7 +210,26 @@ describe('MigrationManager', () => {
it('should create the migrationsMeta table if it does not exist and fetch versions from it', async () => { it('should create the migrationsMeta table if it does not exist and fetch versions from it', async () => {
// Arrange // Arrange
const sequelize = new Sequelize({ dialect: 'sqlite', storage: ':memory:', logging: false }) const sequelize = new Sequelize({ dialect: 'sqlite', storage: ':memory:', logging: false })
const migrationManager = new MigrationManager(sequelize, configPath) const migrationManager = new MigrationManager(sequelize, false, configPath)
migrationManager.serverVersion = serverVersion
// Act
await migrationManager.fetchVersionsFromDatabase()
// Assert
const tableDescription = await sequelize.getQueryInterface().describeTable('migrationsMeta')
expect(tableDescription).to.deep.equal({
key: { type: 'VARCHAR(255)', allowNull: false, defaultValue: undefined, primaryKey: false, unique: false },
value: { type: 'VARCHAR(255)', allowNull: false, defaultValue: undefined, primaryKey: false, unique: false }
})
expect(migrationManager.maxVersion).to.equal('0.0.0')
expect(migrationManager.databaseVersion).to.equal('0.0.0')
})
it('should create the migrationsMeta with databaseVersion=serverVersion if database is new', async () => {
// Arrange
const sequelize = new Sequelize({ dialect: 'sqlite', storage: ':memory:', logging: false })
const migrationManager = new MigrationManager(sequelize, true, configPath)
migrationManager.serverVersion = serverVersion migrationManager.serverVersion = serverVersion
// Act // Act
@ -211,11 +245,28 @@ describe('MigrationManager', () => {
expect(migrationManager.databaseVersion).to.equal(serverVersion) expect(migrationManager.databaseVersion).to.equal(serverVersion)
}) })
it('should re-create the migrationsMeta table if it existed and database is new (Database force=true)', async () => {
// Arrange
const sequelize = new Sequelize({ dialect: 'sqlite', storage: ':memory:', logging: false })
// Create a migrationsMeta table and populate it with version and maxVersion
await sequelize.query('CREATE TABLE migrationsMeta (key VARCHAR(255), value VARCHAR(255))')
await sequelize.query("INSERT INTO migrationsMeta (key, value) VALUES ('version', '1.1.0'), ('maxVersion', '1.1.0')")
const migrationManager = new MigrationManager(sequelize, true, configPath)
migrationManager.serverVersion = serverVersion
// Act
await migrationManager.fetchVersionsFromDatabase()
// Assert
expect(migrationManager.maxVersion).to.equal('0.0.0')
expect(migrationManager.databaseVersion).to.equal(serverVersion)
})
it('should throw an error if the database query fails', async () => { it('should throw an error if the database query fails', async () => {
// Arrange // Arrange
const sequelizeStub = sinon.createStubInstance(Sequelize) const sequelizeStub = sinon.createStubInstance(Sequelize)
sequelizeStub.query.rejects(new Error('Database query failed')) sequelizeStub.query.rejects(new Error('Database query failed'))
const migrationManager = new MigrationManager(sequelizeStub, configPath) const migrationManager = new MigrationManager(sequelizeStub, false, configPath)
migrationManager.checkOrCreateMigrationsMetaTable = sinon.stub().resolves() migrationManager.checkOrCreateMigrationsMetaTable = sinon.stub().resolves()
// Act // Act
@ -236,7 +287,7 @@ describe('MigrationManager', () => {
// Create a migrationsMeta table and populate it with version and maxVersion // Create a migrationsMeta table and populate it with version and maxVersion
await sequelize.query('CREATE TABLE migrationsMeta (key VARCHAR(255), value VARCHAR(255))') await sequelize.query('CREATE TABLE migrationsMeta (key VARCHAR(255), value VARCHAR(255))')
await sequelize.query("INSERT INTO migrationsMeta (key, value) VALUES ('version', '1.1.0'), ('maxVersion', '1.1.0')") await sequelize.query("INSERT INTO migrationsMeta (key, value) VALUES ('version', '1.1.0'), ('maxVersion', '1.1.0')")
const migrationManager = new MigrationManager(sequelize, configPath) const migrationManager = new MigrationManager(sequelize, false, configPath)
migrationManager.serverVersion = '1.2.0' migrationManager.serverVersion = '1.2.0'
// Act // Act
@ -253,7 +304,7 @@ describe('MigrationManager', () => {
describe('extractVersionFromTag', () => { describe('extractVersionFromTag', () => {
it('should return null if tag is not provided', () => { it('should return null if tag is not provided', () => {
// Arrange // Arrange
const migrationManager = new MigrationManager(sequelizeStub, configPath) const migrationManager = new MigrationManager(sequelizeStub, false, configPath)
// Act // Act
const result = migrationManager.extractVersionFromTag() const result = migrationManager.extractVersionFromTag()
@ -264,7 +315,7 @@ describe('MigrationManager', () => {
it('should return null if tag does not match the version format', () => { it('should return null if tag does not match the version format', () => {
// Arrange // Arrange
const migrationManager = new MigrationManager(sequelizeStub, configPath) const migrationManager = new MigrationManager(sequelizeStub, false, configPath)
const tag = 'invalid-tag' const tag = 'invalid-tag'
// Act // Act
@ -276,7 +327,7 @@ describe('MigrationManager', () => {
it('should extract the version from the tag', () => { it('should extract the version from the tag', () => {
// Arrange // Arrange
const migrationManager = new MigrationManager(sequelizeStub, configPath) const migrationManager = new MigrationManager(sequelizeStub, false, configPath)
const tag = 'v1.2.3' const tag = 'v1.2.3'
// Act // Act
@ -290,7 +341,7 @@ describe('MigrationManager', () => {
describe('copyMigrationsToConfigDir', () => { describe('copyMigrationsToConfigDir', () => {
it('should copy migrations to the config directory', async () => { it('should copy migrations to the config directory', async () => {
// Arrange // Arrange
const migrationManager = new MigrationManager(sequelizeStub, configPath) const migrationManager = new MigrationManager(sequelizeStub, false, configPath)
migrationManager.migrationsDir = path.join(configPath, 'migrations') migrationManager.migrationsDir = path.join(configPath, 'migrations')
const migrationsSourceDir = path.join(__dirname, '..', '..', '..', 'server', 'migrations') const migrationsSourceDir = path.join(__dirname, '..', '..', '..', 'server', 'migrations')
const targetDir = migrationManager.migrationsDir const targetDir = migrationManager.migrationsDir
@ -313,7 +364,7 @@ describe('MigrationManager', () => {
it('should throw an error if copying the migrations fails', async () => { it('should throw an error if copying the migrations fails', async () => {
// Arrange // Arrange
const migrationManager = new MigrationManager(sequelizeStub, configPath) const migrationManager = new MigrationManager(sequelizeStub, false, configPath)
migrationManager.migrationsDir = path.join(configPath, 'migrations') migrationManager.migrationsDir = path.join(configPath, 'migrations')
const migrationsSourceDir = path.join(__dirname, '..', '..', '..', 'server', 'migrations') const migrationsSourceDir = path.join(__dirname, '..', '..', '..', 'server', 'migrations')
const targetDir = migrationManager.migrationsDir const targetDir = migrationManager.migrationsDir
@ -484,7 +535,7 @@ describe('MigrationManager', () => {
const readdirStub = sinon.stub(fs, 'readdir').resolves(['v1.0.0-migration.js', 'v1.10.0-migration.js', 'v1.2.0-migration.js', 'v1.1.0-migration.js']) const readdirStub = sinon.stub(fs, 'readdir').resolves(['v1.0.0-migration.js', 'v1.10.0-migration.js', 'v1.2.0-migration.js', 'v1.1.0-migration.js'])
const readFileSyncStub = sinon.stub(fs, 'readFileSync').returns('module.exports = { up: () => {}, down: () => {} }') const readFileSyncStub = sinon.stub(fs, 'readFileSync').returns('module.exports = { up: () => {}, down: () => {} }')
const umzugStorage = memoryStorage() const umzugStorage = memoryStorage()
migrationManager = new MigrationManager(sequelizeStub, configPath) migrationManager = new MigrationManager(sequelizeStub, false, configPath)
migrationManager.migrationsDir = path.join(configPath, 'migrations') migrationManager.migrationsDir = path.join(configPath, 'migrations')
const resolvedMigrationNames = ['v1.0.0-migration.js', 'v1.1.0-migration.js', 'v1.2.0-migration.js', 'v1.10.0-migration.js'] const resolvedMigrationNames = ['v1.0.0-migration.js', 'v1.1.0-migration.js', 'v1.2.0-migration.js', 'v1.10.0-migration.js']
const resolvedMigrationPaths = resolvedMigrationNames.map((name) => path.resolve(path.join(migrationManager.migrationsDir, name))) const resolvedMigrationPaths = resolvedMigrationNames.map((name) => path.resolve(path.join(migrationManager.migrationsDir, name)))