mirror of
https://github.com/advplyr/audiobookshelf.git
synced 2026-08-03 15:11:44 +00:00
make v2.35.0 migration column checks case-insensitive
describeTable on a migrated postgres database returns lowercase column names, so the camelCase existence checks never matched and a replay attempted addColumn twice, failing the migration. Match column names case-insensitively like the other postgres-hardened migrations.
This commit is contained in:
parent
9abf86a5b9
commit
a9c89e4828
2 changed files with 106 additions and 4 deletions
|
|
@ -22,8 +22,10 @@ async function up({ context: { queryInterface, logger } }) {
|
|||
|
||||
if (await queryInterface.tableExists('sessions')) {
|
||||
const tableDescription = await queryInterface.describeTable('sessions')
|
||||
// Postgres folds unquoted identifiers to lowercase, so match column names case-insensitively
|
||||
const hasColumn = (name) => Object.keys(tableDescription).some((column) => column.toLowerCase() === name.toLowerCase())
|
||||
|
||||
if (!tableDescription.lastRefreshToken) {
|
||||
if (!hasColumn('lastRefreshToken')) {
|
||||
logger.info(`${loggerPrefix} Adding lastRefreshToken column to sessions table`)
|
||||
await queryInterface.addColumn('sessions', 'lastRefreshToken', {
|
||||
type: queryInterface.sequelize.Sequelize.DataTypes.STRING,
|
||||
|
|
@ -33,7 +35,7 @@ async function up({ context: { queryInterface, logger } }) {
|
|||
logger.info(`${loggerPrefix} lastRefreshToken column already exists in sessions table`)
|
||||
}
|
||||
|
||||
if (!tableDescription.lastRefreshTokenExpiresAt) {
|
||||
if (!hasColumn('lastRefreshTokenExpiresAt')) {
|
||||
logger.info(`${loggerPrefix} Adding lastRefreshTokenExpiresAt column to sessions table`)
|
||||
await queryInterface.addColumn('sessions', 'lastRefreshTokenExpiresAt', {
|
||||
type: queryInterface.sequelize.Sequelize.DataTypes.DATE,
|
||||
|
|
@ -60,15 +62,17 @@ async function down({ context: { queryInterface, logger } }) {
|
|||
|
||||
if (await queryInterface.tableExists('sessions')) {
|
||||
const tableDescription = await queryInterface.describeTable('sessions')
|
||||
// Postgres folds unquoted identifiers to lowercase, so match column names case-insensitively
|
||||
const hasColumn = (name) => Object.keys(tableDescription).some((column) => column.toLowerCase() === name.toLowerCase())
|
||||
|
||||
if (tableDescription.lastRefreshToken) {
|
||||
if (hasColumn('lastRefreshToken')) {
|
||||
logger.info(`${loggerPrefix} Removing lastRefreshToken column from sessions table`)
|
||||
await queryInterface.removeColumn('sessions', 'lastRefreshToken')
|
||||
} else {
|
||||
logger.info(`${loggerPrefix} lastRefreshToken column does not exist in sessions table`)
|
||||
}
|
||||
|
||||
if (tableDescription.lastRefreshTokenExpiresAt) {
|
||||
if (hasColumn('lastRefreshTokenExpiresAt')) {
|
||||
logger.info(`${loggerPrefix} Removing lastRefreshTokenExpiresAt column from sessions table`)
|
||||
await queryInterface.removeColumn('sessions', 'lastRefreshTokenExpiresAt')
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,98 @@
|
|||
const { expect } = require('chai')
|
||||
const sinon = require('sinon')
|
||||
const { up, down } = require('../../../server/migrations/v2.35.0-add-last-refresh-token')
|
||||
const { Sequelize } = require('sequelize')
|
||||
const Logger = require('../../../server/Logger')
|
||||
|
||||
describe('migration-v2.35.0-add-last-refresh-token', () => {
|
||||
let sequelize
|
||||
let queryInterface
|
||||
let loggerInfoStub
|
||||
|
||||
beforeEach(() => {
|
||||
sequelize = new Sequelize({ dialect: 'sqlite', storage: ':memory:', logging: false })
|
||||
queryInterface = sequelize.getQueryInterface()
|
||||
loggerInfoStub = sinon.stub(Logger, 'info')
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
sinon.restore()
|
||||
})
|
||||
|
||||
describe('up', () => {
|
||||
beforeEach(async () => {
|
||||
await queryInterface.createTable('sessions', {
|
||||
id: { type: Sequelize.UUID, primaryKey: true },
|
||||
createdAt: { type: Sequelize.DATE, allowNull: false },
|
||||
updatedAt: { type: Sequelize.DATE, allowNull: false }
|
||||
})
|
||||
})
|
||||
|
||||
it('should add lastRefreshToken columns when they do not exist', async () => {
|
||||
await up({ context: { queryInterface, logger: Logger } })
|
||||
|
||||
const tableDescription = await queryInterface.describeTable('sessions')
|
||||
expect(tableDescription.lastRefreshToken).to.exist
|
||||
expect(tableDescription.lastRefreshTokenExpiresAt).to.exist
|
||||
})
|
||||
|
||||
it('should not fail when the columns already exist', async () => {
|
||||
await up({ context: { queryInterface, logger: Logger } })
|
||||
await up({ context: { queryInterface, logger: Logger } })
|
||||
|
||||
const tableDescription = await queryInterface.describeTable('sessions')
|
||||
expect(tableDescription.lastRefreshToken).to.exist
|
||||
expect(tableDescription.lastRefreshTokenExpiresAt).to.exist
|
||||
expect(loggerInfoStub.calledWithMatch('lastRefreshToken column already exists')).to.equal(true)
|
||||
})
|
||||
|
||||
it('should detect existing columns case-insensitively for postgres identifier folding', async () => {
|
||||
// Postgres folds unquoted identifiers to lowercase, so describeTable on a
|
||||
// migrated postgres database returns lowercase column names
|
||||
const fakeQueryInterface = {
|
||||
sequelize,
|
||||
tableExists: async () => true,
|
||||
describeTable: async () => ({
|
||||
id: {},
|
||||
lastrefreshtoken: {},
|
||||
lastrefreshtokenexpiresat: {}
|
||||
}),
|
||||
addColumn: async () => {
|
||||
throw new Error('addColumn must not be called for existing lowercase columns')
|
||||
}
|
||||
}
|
||||
|
||||
await up({ context: { queryInterface: fakeQueryInterface, logger: Logger } })
|
||||
|
||||
expect(loggerInfoStub.calledWithMatch('lastRefreshToken column already exists')).to.equal(true)
|
||||
expect(loggerInfoStub.calledWithMatch('lastRefreshTokenExpiresAt column already exists')).to.equal(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('down', () => {
|
||||
beforeEach(async () => {
|
||||
await queryInterface.createTable('sessions', {
|
||||
id: { type: Sequelize.UUID, primaryKey: true },
|
||||
lastRefreshToken: { type: Sequelize.STRING, allowNull: true },
|
||||
lastRefreshTokenExpiresAt: { type: Sequelize.DATE, allowNull: true },
|
||||
createdAt: { type: Sequelize.DATE, allowNull: false },
|
||||
updatedAt: { type: Sequelize.DATE, allowNull: false }
|
||||
})
|
||||
})
|
||||
|
||||
it('should remove lastRefreshToken columns when they exist', async () => {
|
||||
await down({ context: { queryInterface, logger: Logger } })
|
||||
|
||||
const tableDescription = await queryInterface.describeTable('sessions')
|
||||
expect(tableDescription.lastRefreshToken).to.not.exist
|
||||
expect(tableDescription.lastRefreshTokenExpiresAt).to.not.exist
|
||||
})
|
||||
|
||||
it('should not fail when the columns do not exist', async () => {
|
||||
await down({ context: { queryInterface, logger: Logger } })
|
||||
await down({ context: { queryInterface, logger: Logger } })
|
||||
|
||||
expect(loggerInfoStub.calledWithMatch('lastRefreshToken column does not exist')).to.equal(true)
|
||||
})
|
||||
})
|
||||
})
|
||||
Loading…
Add table
Add a link
Reference in a new issue