Some stuff
This commit is contained in:
@@ -0,0 +1,210 @@
|
||||
/**
|
||||
* @typedef MigrationContext
|
||||
* @property {import('sequelize').QueryInterface} queryInterface - a suquelize QueryInterface object.
|
||||
* @property {import('../Logger')} logger - a Logger object.
|
||||
*
|
||||
* @typedef MigrationOptions
|
||||
* @property {MigrationContext} context - an object containing the migration context.
|
||||
*/
|
||||
|
||||
/**
|
||||
* This upward migration script cleans any duplicate series in the `Series` table and
|
||||
* adds a unique index on the `name` and `libraryId` columns.
|
||||
*
|
||||
* @param {MigrationOptions} options - an object containing the migration context.
|
||||
* @returns {Promise<void>} - A promise that resolves when the migration is complete.
|
||||
*/
|
||||
async function up({ context: { queryInterface, logger } }) {
|
||||
// Upwards migration script
|
||||
logger.info('[2.15.0 migration] UPGRADE BEGIN: 2.15.0-series-column-unique ')
|
||||
|
||||
// Run reindex nocase to fix potential corruption issues due to the bad sqlite extension introduced in v2.12.0
|
||||
logger.info('[2.15.0 migration] Reindexing NOCASE indices to fix potential hidden corruption issues')
|
||||
await queryInterface.sequelize.query('REINDEX NOCASE;')
|
||||
|
||||
// Check if the unique index already exists
|
||||
const seriesIndexes = await queryInterface.showIndex('Series')
|
||||
if (seriesIndexes.some((index) => index.name === 'unique_series_name_per_library')) {
|
||||
logger.info('[2.15.0 migration] Unique index on Series.name and Series.libraryId already exists')
|
||||
logger.info('[2.15.0 migration] UPGRADE END: 2.15.0-series-column-unique ')
|
||||
return
|
||||
}
|
||||
|
||||
// The steps taken to deduplicate the series are as follows:
|
||||
// 1. Find all duplicate series in the `Series` table.
|
||||
// 2. Iterate over the duplicate series and find all book IDs that are associated with the duplicate series in `bookSeries` table.
|
||||
// 2.a For each book ID, check if the ID occurs multiple times for the duplicate series.
|
||||
// 2.b If so, keep only one of the rows that has this bookId and seriesId.
|
||||
// 3. Update `bookSeries` table to point to the most recent series.
|
||||
// 4. Delete the older series.
|
||||
|
||||
// Use the queryInterface to get the series table and find duplicates in the `name` and `libraryId` column
|
||||
const [duplicates] = await queryInterface.sequelize.query(`
|
||||
SELECT name, libraryId
|
||||
FROM Series
|
||||
GROUP BY name, libraryId
|
||||
HAVING COUNT(name) > 1
|
||||
`)
|
||||
|
||||
// Print out how many duplicates were found
|
||||
logger.info(`[2.15.0 migration] Found ${duplicates.length} duplicate series`)
|
||||
|
||||
// Iterate over each duplicate series
|
||||
for (const duplicate of duplicates) {
|
||||
// Report the series name that is being deleted
|
||||
logger.info(`[2.15.0 migration] Deduplicating series "${duplicate.name}" in library ${duplicate.libraryId}`)
|
||||
|
||||
// Determine any duplicate book IDs in the `bookSeries` table for the same series
|
||||
const [duplicateBookIds] = await queryInterface.sequelize.query(
|
||||
`
|
||||
SELECT bookId
|
||||
FROM BookSeries
|
||||
WHERE seriesId IN (
|
||||
SELECT id
|
||||
FROM Series
|
||||
WHERE name = :name AND libraryId = :libraryId
|
||||
)
|
||||
GROUP BY bookId
|
||||
HAVING COUNT(bookId) > 1
|
||||
`,
|
||||
{
|
||||
replacements: {
|
||||
name: duplicate.name,
|
||||
libraryId: duplicate.libraryId
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
// Iterate over the duplicate book IDs if there is at least one and only keep the first row that has this bookId and seriesId
|
||||
for (const { bookId } of duplicateBookIds) {
|
||||
logger.info(`[2.15.0 migration] Deduplicating bookId ${bookId} in series "${duplicate.name}" of library ${duplicate.libraryId}`)
|
||||
// Get all rows of `BookSeries` table that have the same `bookId` and `seriesId`. Sort by `sequence` with nulls sorted last
|
||||
const [duplicateBookSeries] = await queryInterface.sequelize.query(
|
||||
`
|
||||
SELECT id
|
||||
FROM BookSeries
|
||||
WHERE bookId = :bookId
|
||||
AND seriesId IN (
|
||||
SELECT id
|
||||
FROM Series
|
||||
WHERE name = :name AND libraryId = :libraryId
|
||||
)
|
||||
ORDER BY sequence NULLS LAST
|
||||
`,
|
||||
{
|
||||
replacements: {
|
||||
bookId,
|
||||
name: duplicate.name,
|
||||
libraryId: duplicate.libraryId
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
// remove the first element from the array
|
||||
duplicateBookSeries.shift()
|
||||
|
||||
// Delete the remaining duplicate rows
|
||||
if (duplicateBookSeries.length > 0) {
|
||||
const [deletedBookSeries] = await queryInterface.sequelize.query(
|
||||
`
|
||||
DELETE FROM BookSeries
|
||||
WHERE id IN (:ids)
|
||||
`,
|
||||
{
|
||||
replacements: {
|
||||
ids: duplicateBookSeries.map((row) => row.id)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
logger.info(`[2.15.0 migration] Finished cleanup of bookId ${bookId} in series "${duplicate.name}" of library ${duplicate.libraryId}`)
|
||||
}
|
||||
|
||||
// Get all the most recent series which matches the `name` and `libraryId`
|
||||
const [mostRecentSeries] = await queryInterface.sequelize.query(
|
||||
`
|
||||
SELECT id
|
||||
FROM Series
|
||||
WHERE name = :name AND libraryId = :libraryId
|
||||
ORDER BY updatedAt DESC
|
||||
LIMIT 1
|
||||
`,
|
||||
{
|
||||
replacements: {
|
||||
name: duplicate.name,
|
||||
libraryId: duplicate.libraryId
|
||||
},
|
||||
type: queryInterface.sequelize.QueryTypes.SELECT
|
||||
}
|
||||
)
|
||||
|
||||
if (mostRecentSeries) {
|
||||
// Update all BookSeries records for this series to point to the most recent series
|
||||
const [seriesUpdated] = await queryInterface.sequelize.query(
|
||||
`
|
||||
UPDATE BookSeries
|
||||
SET seriesId = :mostRecentSeriesId
|
||||
WHERE seriesId IN (
|
||||
SELECT id
|
||||
FROM Series
|
||||
WHERE name = :name AND libraryId = :libraryId
|
||||
AND id != :mostRecentSeriesId
|
||||
)
|
||||
`,
|
||||
{
|
||||
replacements: {
|
||||
name: duplicate.name,
|
||||
libraryId: duplicate.libraryId,
|
||||
mostRecentSeriesId: mostRecentSeries.id
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
// Delete the older series
|
||||
const seriesDeleted = await queryInterface.sequelize.query(
|
||||
`
|
||||
DELETE FROM Series
|
||||
WHERE name = :name AND libraryId = :libraryId
|
||||
AND id != :mostRecentSeriesId
|
||||
`,
|
||||
{
|
||||
replacements: {
|
||||
name: duplicate.name,
|
||||
libraryId: duplicate.libraryId,
|
||||
mostRecentSeriesId: mostRecentSeries.id
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
logger.info(`[2.15.0 migration] Deduplication complete`)
|
||||
|
||||
// Create a unique index based on the name and library ID for the `Series` table
|
||||
await queryInterface.addIndex('Series', ['name', 'libraryId'], {
|
||||
unique: true,
|
||||
name: 'unique_series_name_per_library'
|
||||
})
|
||||
logger.info('[2.15.0 migration] Added unique index on Series.name and Series.libraryId')
|
||||
|
||||
logger.info('[2.15.0 migration] UPGRADE END: 2.15.0-series-column-unique ')
|
||||
}
|
||||
|
||||
/**
|
||||
* This removes the unique index on the `Series` table.
|
||||
*
|
||||
* @param {MigrationOptions} options - an object containing the migration context.
|
||||
* @returns {Promise<void>} - A promise that resolves when the migration is complete.
|
||||
*/
|
||||
async function down({ context: { queryInterface, logger } }) {
|
||||
// Downward migration script
|
||||
logger.info('[2.15.0 migration] DOWNGRADE BEGIN: 2.15.0-series-column-unique ')
|
||||
|
||||
// Remove the unique index
|
||||
await queryInterface.removeIndex('Series', 'unique_series_name_per_library')
|
||||
logger.info('[2.15.0 migration] Removed unique index on Series.name and Series.libraryId')
|
||||
|
||||
logger.info('[2.15.0 migration] DOWNGRADE END: 2.15.0-series-column-unique ')
|
||||
}
|
||||
|
||||
module.exports = { up, down }
|
||||
@@ -0,0 +1,43 @@
|
||||
/**
|
||||
* @typedef MigrationContext
|
||||
* @property {import('sequelize').QueryInterface} queryInterface - a suquelize QueryInterface object.
|
||||
* @property {import('../Logger')} logger - a Logger object.
|
||||
*
|
||||
* @typedef MigrationOptions
|
||||
* @property {MigrationContext} context - an object containing the migration context.
|
||||
*/
|
||||
|
||||
/**
|
||||
* This upward migration script fixes old database corruptions due to the a bad sqlite extension introduced in v2.12.0.
|
||||
*
|
||||
* @param {MigrationOptions} options - an object containing the migration context.
|
||||
* @returns {Promise<void>} - A promise that resolves when the migration is complete.
|
||||
*/
|
||||
async function up({ context: { queryInterface, logger } }) {
|
||||
// Upwards migration script
|
||||
logger.info('[2.15.1 migration] UPGRADE BEGIN: 2.15.1-reindex-nocase ')
|
||||
|
||||
// Run reindex nocase to fix potential corruption issues due to the bad sqlite extension introduced in v2.12.0
|
||||
logger.info('[2.15.1 migration] Reindexing NOCASE indices to fix potential hidden corruption issues')
|
||||
await queryInterface.sequelize.query('REINDEX NOCASE;')
|
||||
|
||||
logger.info('[2.15.1 migration] UPGRADE END: 2.15.1-reindex-nocase ')
|
||||
}
|
||||
|
||||
/**
|
||||
* This downward migration script is a no-op.
|
||||
*
|
||||
* @param {MigrationOptions} options - an object containing the migration context.
|
||||
* @returns {Promise<void>} - A promise that resolves when the migration is complete.
|
||||
*/
|
||||
async function down({ context: { queryInterface, logger } }) {
|
||||
// Downward migration script
|
||||
logger.info('[2.15.1 migration] DOWNGRADE BEGIN: 2.15.1-reindex-nocase ')
|
||||
|
||||
// This migration is a no-op
|
||||
logger.info('[2.15.1 migration] No action required for downgrade')
|
||||
|
||||
logger.info('[2.15.1 migration] DOWNGRADE END: 2.15.1-reindex-nocase ')
|
||||
}
|
||||
|
||||
module.exports = { up, down }
|
||||
@@ -0,0 +1,93 @@
|
||||
/**
|
||||
* @typedef MigrationContext
|
||||
* @property {import('sequelize').QueryInterface} queryInterface - a suquelize QueryInterface object.
|
||||
* @property {import('../Logger')} logger - a Logger object.
|
||||
*
|
||||
* @typedef MigrationOptions
|
||||
* @property {MigrationContext} context - an object containing the migration context.
|
||||
*/
|
||||
|
||||
/**
|
||||
* This upward migration script adds indexes to speed up queries on the `BookAuthor`, `BookSeries`, and `podcastEpisodes` tables.
|
||||
*
|
||||
* @param {MigrationOptions} options - an object containing the migration context.
|
||||
* @returns {Promise<void>} - A promise that resolves when the migration is complete.
|
||||
*/
|
||||
async function up({ context: { queryInterface, logger } }) {
|
||||
// Upwards migration script
|
||||
logger.info('[2.15.2 migration] UPGRADE BEGIN: 2.15.2-index-creation')
|
||||
|
||||
// Create index for bookAuthors
|
||||
logger.info('[2.15.2 migration] Creating index for bookAuthors')
|
||||
const bookAuthorsIndexes = await queryInterface.showIndex('bookAuthors')
|
||||
if (!bookAuthorsIndexes.some((index) => index.name === 'bookAuthor_authorId')) {
|
||||
await queryInterface.addIndex('bookAuthors', ['authorId'], {
|
||||
name: 'bookAuthor_authorId'
|
||||
})
|
||||
} else {
|
||||
logger.info('[2.15.2 migration] Index bookAuthor_authorId already exists')
|
||||
}
|
||||
|
||||
// Create index for bookSeries
|
||||
logger.info('[2.15.2 migration] Creating index for bookSeries')
|
||||
const bookSeriesIndexes = await queryInterface.showIndex('bookSeries')
|
||||
if (!bookSeriesIndexes.some((index) => index.name === 'bookSeries_seriesId')) {
|
||||
await queryInterface.addIndex('bookSeries', ['seriesId'], {
|
||||
name: 'bookSeries_seriesId'
|
||||
})
|
||||
} else {
|
||||
logger.info('[2.15.2 migration] Index bookSeries_seriesId already exists')
|
||||
}
|
||||
|
||||
// Delete existing podcastEpisode index
|
||||
logger.info('[2.15.2 migration] Deleting existing podcastEpisode index')
|
||||
await queryInterface.removeIndex('podcastEpisodes', 'podcast_episodes_created_at')
|
||||
|
||||
// Create index for podcastEpisode and createdAt
|
||||
logger.info('[2.15.2 migration] Creating index for podcastEpisode and createdAt')
|
||||
const podcastEpisodesIndexes = await queryInterface.showIndex('podcastEpisodes')
|
||||
if (!podcastEpisodesIndexes.some((index) => index.name === 'podcastEpisode_createdAt_podcastId')) {
|
||||
await queryInterface.addIndex('podcastEpisodes', ['createdAt', 'podcastId'], {
|
||||
name: 'podcastEpisode_createdAt_podcastId'
|
||||
})
|
||||
} else {
|
||||
logger.info('[2.15.2 migration] Index podcastEpisode_createdAt_podcastId already exists')
|
||||
}
|
||||
|
||||
// Completed migration
|
||||
logger.info('[2.15.2 migration] UPGRADE END: 2.15.2-index-creation')
|
||||
}
|
||||
|
||||
/**
|
||||
* This downward migration script removes the newly created indexes and re-adds the old index on the `podcastEpisodes` table.
|
||||
*
|
||||
* @param {MigrationOptions} options - an object containing the migration context.
|
||||
* @returns {Promise<void>} - A promise that resolves when the migration is complete.
|
||||
*/
|
||||
async function down({ context: { queryInterface, logger } }) {
|
||||
// Downward migration script
|
||||
logger.info('[2.15.2 migration] DOWNGRADE BEGIN: 2.15.2-index-creation')
|
||||
|
||||
// Remove index for bookAuthors
|
||||
logger.info('[2.15.2 migration] Removing index for bookAuthors')
|
||||
await queryInterface.removeIndex('bookAuthors', 'bookAuthor_authorId')
|
||||
|
||||
// Remove index for bookSeries
|
||||
logger.info('[2.15.2 migration] Removing index for bookSeries')
|
||||
await queryInterface.removeIndex('bookSeries', 'bookSeries_seriesId')
|
||||
|
||||
// Delete existing podcastEpisode index
|
||||
logger.info('[2.15.2 migration] Deleting existing podcastEpisode index')
|
||||
await queryInterface.removeIndex('podcastEpisodes', 'podcastEpisode_createdAt_podcastId')
|
||||
|
||||
// Create index for podcastEpisode and createdAt
|
||||
logger.info('[2.15.2 migration] Creating original index for podcastEpisode createdAt')
|
||||
await queryInterface.addIndex('podcastEpisodes', ['createdAt'], {
|
||||
name: 'podcast_episodes_created_at'
|
||||
})
|
||||
|
||||
// Finished migration
|
||||
logger.info('[2.15.2 migration] DOWNGRADE END: 2.15.2-index-creation')
|
||||
}
|
||||
|
||||
module.exports = { up, down }
|
||||
@@ -0,0 +1,102 @@
|
||||
/**
|
||||
* @typedef MigrationContext
|
||||
* @property {import('sequelize').QueryInterface} queryInterface - a suquelize QueryInterface object.
|
||||
* @property {import('../Logger')} logger - a Logger object.
|
||||
*
|
||||
* @typedef MigrationOptions
|
||||
* @property {MigrationContext} context - an object containing the migration context.
|
||||
*/
|
||||
|
||||
/**
|
||||
* This upward migration script changes table columns with data type UUIDv4 to UUID to match associated models.
|
||||
*
|
||||
* @param {MigrationOptions} options - an object containing the migration context.
|
||||
* @returns {Promise<void>} - A promise that resolves when the migration is complete.
|
||||
*/
|
||||
async function up({ context: { queryInterface, logger } }) {
|
||||
// Upwards migration script
|
||||
logger.info('[2.17.0 migration] UPGRADE BEGIN: 2.17.0-uuid-replacement')
|
||||
|
||||
logger.info('[2.17.0 migration] Changing libraryItems.mediaId column to UUID')
|
||||
await queryInterface.changeColumn('libraryItems', 'mediaId', {
|
||||
type: 'UUID'
|
||||
})
|
||||
|
||||
logger.info('[2.17.0 migration] Changing feeds.entityId column to UUID')
|
||||
await queryInterface.changeColumn('feeds', 'entityId', {
|
||||
type: 'UUID'
|
||||
})
|
||||
|
||||
if (await queryInterface.tableExists('mediaItemShares')) {
|
||||
logger.info('[2.17.0 migration] Changing mediaItemShares.mediaItemId column to UUID')
|
||||
await queryInterface.changeColumn('mediaItemShares', 'mediaItemId', {
|
||||
type: 'UUID'
|
||||
})
|
||||
} else {
|
||||
logger.info('[2.17.0 migration] mediaItemShares table does not exist, skipping column change')
|
||||
}
|
||||
|
||||
logger.info('[2.17.0 migration] Changing playbackSessions.mediaItemId column to UUID')
|
||||
await queryInterface.changeColumn('playbackSessions', 'mediaItemId', {
|
||||
type: 'UUID'
|
||||
})
|
||||
|
||||
logger.info('[2.17.0 migration] Changing playlistMediaItems.mediaItemId column to UUID')
|
||||
await queryInterface.changeColumn('playlistMediaItems', 'mediaItemId', {
|
||||
type: 'UUID'
|
||||
})
|
||||
|
||||
logger.info('[2.17.0 migration] Changing mediaProgresses.mediaItemId column to UUID')
|
||||
await queryInterface.changeColumn('mediaProgresses', 'mediaItemId', {
|
||||
type: 'UUID'
|
||||
})
|
||||
|
||||
// Completed migration
|
||||
logger.info('[2.17.0 migration] UPGRADE END: 2.17.0-uuid-replacement')
|
||||
}
|
||||
|
||||
/**
|
||||
* This downward migration script changes table columns data type back to UUIDv4.
|
||||
*
|
||||
* @param {MigrationOptions} options - an object containing the migration context.
|
||||
* @returns {Promise<void>} - A promise that resolves when the migration is complete.
|
||||
*/
|
||||
async function down({ context: { queryInterface, logger } }) {
|
||||
// Downward migration script
|
||||
logger.info('[2.17.0 migration] DOWNGRADE BEGIN: 2.17.0-uuid-replacement')
|
||||
|
||||
logger.info('[2.17.0 migration] Changing libraryItems.mediaId column to UUIDV4')
|
||||
await queryInterface.changeColumn('libraryItems', 'mediaId', {
|
||||
type: 'UUIDV4'
|
||||
})
|
||||
|
||||
logger.info('[2.17.0 migration] Changing feeds.entityId column to UUIDV4')
|
||||
await queryInterface.changeColumn('feeds', 'entityId', {
|
||||
type: 'UUIDV4'
|
||||
})
|
||||
|
||||
logger.info('[2.17.0 migration] Changing mediaItemShares.mediaItemId column to UUIDV4')
|
||||
await queryInterface.changeColumn('mediaItemShares', 'mediaItemId', {
|
||||
type: 'UUIDV4'
|
||||
})
|
||||
|
||||
logger.info('[2.17.0 migration] Changing playbackSessions.mediaItemId column to UUIDV4')
|
||||
await queryInterface.changeColumn('playbackSessions', 'mediaItemId', {
|
||||
type: 'UUIDV4'
|
||||
})
|
||||
|
||||
logger.info('[2.17.0 migration] Changing playlistMediaItems.mediaItemId column to UUIDV4')
|
||||
await queryInterface.changeColumn('playlistMediaItems', 'mediaItemId', {
|
||||
type: 'UUIDV4'
|
||||
})
|
||||
|
||||
logger.info('[2.17.0 migration] Changing mediaProgresses.mediaItemId column to UUIDV4')
|
||||
await queryInterface.changeColumn('mediaProgresses', 'mediaItemId', {
|
||||
type: 'UUIDV4'
|
||||
})
|
||||
|
||||
// Completed migration
|
||||
logger.info('[2.17.0 migration] DOWNGRADE END: 2.17.0-uuid-replacement')
|
||||
}
|
||||
|
||||
module.exports = { up, down }
|
||||
@@ -0,0 +1,259 @@
|
||||
/**
|
||||
* @typedef MigrationContext
|
||||
* @property {import('sequelize').QueryInterface} queryInterface - a suquelize QueryInterface object.
|
||||
* @property {import('../Logger')} logger - a Logger object.
|
||||
*
|
||||
* @typedef MigrationOptions
|
||||
* @property {MigrationContext} context - an object containing the migration context.
|
||||
*/
|
||||
|
||||
/**
|
||||
* This upward migration script changes foreign key constraints for the
|
||||
* libraryItems, feeds, mediaItemShares, playbackSessions, playlistMediaItems, and mediaProgresses tables.
|
||||
*
|
||||
* @param {MigrationOptions} options - an object containing the migration context.
|
||||
* @returns {Promise<void>} - A promise that resolves when the migration is complete.
|
||||
*/
|
||||
async function up({ context: { queryInterface, logger } }) {
|
||||
// Upwards migration script
|
||||
logger.info('[2.17.3 migration] UPGRADE BEGIN: 2.17.3-fk-constraints')
|
||||
|
||||
const execQuery = queryInterface.sequelize.query.bind(queryInterface.sequelize)
|
||||
|
||||
// Disable foreign key constraints for the next sequence of operations
|
||||
await execQuery(`PRAGMA foreign_keys = OFF;`)
|
||||
|
||||
try {
|
||||
await execQuery(`BEGIN TRANSACTION;`)
|
||||
|
||||
logger.info('[2.17.3 migration] Updating libraryItems constraints')
|
||||
const libraryItemsConstraints = [
|
||||
{ field: 'libraryId', onDelete: 'SET NULL', onUpdate: 'CASCADE' },
|
||||
{ field: 'libraryFolderId', onDelete: 'SET NULL', onUpdate: 'CASCADE' }
|
||||
]
|
||||
if (await changeConstraints(queryInterface, 'libraryItems', libraryItemsConstraints)) {
|
||||
logger.info('[2.17.3 migration] Finished updating libraryItems constraints')
|
||||
} else {
|
||||
logger.info('[2.17.3 migration] No changes needed for libraryItems constraints')
|
||||
}
|
||||
|
||||
logger.info('[2.17.3 migration] Updating feeds constraints')
|
||||
const feedsConstraints = [{ field: 'userId', onDelete: 'SET NULL', onUpdate: 'CASCADE' }]
|
||||
if (await changeConstraints(queryInterface, 'feeds', feedsConstraints)) {
|
||||
logger.info('[2.17.3 migration] Finished updating feeds constraints')
|
||||
} else {
|
||||
logger.info('[2.17.3 migration] No changes needed for feeds constraints')
|
||||
}
|
||||
|
||||
if (await queryInterface.tableExists('mediaItemShares')) {
|
||||
logger.info('[2.17.3 migration] Updating mediaItemShares constraints')
|
||||
const mediaItemSharesConstraints = [{ field: 'userId', onDelete: 'SET NULL', onUpdate: 'CASCADE' }]
|
||||
if (await changeConstraints(queryInterface, 'mediaItemShares', mediaItemSharesConstraints)) {
|
||||
logger.info('[2.17.3 migration] Finished updating mediaItemShares constraints')
|
||||
} else {
|
||||
logger.info('[2.17.3 migration] No changes needed for mediaItemShares constraints')
|
||||
}
|
||||
} else {
|
||||
logger.info('[2.17.3 migration] mediaItemShares table does not exist, skipping column change')
|
||||
}
|
||||
|
||||
logger.info('[2.17.3 migration] Updating playbackSessions constraints')
|
||||
const playbackSessionsConstraints = [
|
||||
{ field: 'deviceId', onDelete: 'SET NULL', onUpdate: 'CASCADE' },
|
||||
{ field: 'libraryId', onDelete: 'SET NULL', onUpdate: 'CASCADE' },
|
||||
{ field: 'userId', onDelete: 'SET NULL', onUpdate: 'CASCADE' }
|
||||
]
|
||||
if (await changeConstraints(queryInterface, 'playbackSessions', playbackSessionsConstraints)) {
|
||||
logger.info('[2.17.3 migration] Finished updating playbackSessions constraints')
|
||||
} else {
|
||||
logger.info('[2.17.3 migration] No changes needed for playbackSessions constraints')
|
||||
}
|
||||
|
||||
logger.info('[2.17.3 migration] Updating playlistMediaItems constraints')
|
||||
const playlistMediaItemsConstraints = [{ field: 'playlistId', onDelete: 'CASCADE', onUpdate: 'CASCADE' }]
|
||||
if (await changeConstraints(queryInterface, 'playlistMediaItems', playlistMediaItemsConstraints)) {
|
||||
logger.info('[2.17.3 migration] Finished updating playlistMediaItems constraints')
|
||||
} else {
|
||||
logger.info('[2.17.3 migration] No changes needed for playlistMediaItems constraints')
|
||||
}
|
||||
|
||||
logger.info('[2.17.3 migration] Updating mediaProgresses constraints')
|
||||
const mediaProgressesConstraints = [{ field: 'userId', onDelete: 'CASCADE', onUpdate: 'CASCADE' }]
|
||||
if (await changeConstraints(queryInterface, 'mediaProgresses', mediaProgressesConstraints)) {
|
||||
logger.info('[2.17.3 migration] Finished updating mediaProgresses constraints')
|
||||
} else {
|
||||
logger.info('[2.17.3 migration] No changes needed for mediaProgresses constraints')
|
||||
}
|
||||
|
||||
await execQuery(`COMMIT;`)
|
||||
} catch (error) {
|
||||
logger.error(`[2.17.3 migration] Migration failed - rolling back. Error:`, error)
|
||||
await execQuery(`ROLLBACK;`)
|
||||
}
|
||||
|
||||
await execQuery(`PRAGMA foreign_keys = ON;`)
|
||||
|
||||
// Completed migration
|
||||
logger.info('[2.17.3 migration] UPGRADE END: 2.17.3-fk-constraints')
|
||||
}
|
||||
|
||||
/**
|
||||
* This downward migration script is a no-op.
|
||||
*
|
||||
* @param {MigrationOptions} options - an object containing the migration context.
|
||||
* @returns {Promise<void>} - A promise that resolves when the migration is complete.
|
||||
*/
|
||||
async function down({ context: { queryInterface, logger } }) {
|
||||
// Downward migration script
|
||||
logger.info('[2.17.3 migration] DOWNGRADE BEGIN: 2.17.3-fk-constraints')
|
||||
|
||||
// This migration is a no-op
|
||||
logger.info('[2.17.3 migration] No action required for downgrade')
|
||||
|
||||
// Completed migration
|
||||
logger.info('[2.17.3 migration] DOWNGRADE END: 2.17.3-fk-constraints')
|
||||
}
|
||||
|
||||
/**
|
||||
* @typedef ConstraintUpdateObj
|
||||
* @property {string} field - The field to update
|
||||
* @property {string} onDelete - The onDelete constraint
|
||||
* @property {string} onUpdate - The onUpdate constraint
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef SequelizeFKObj
|
||||
* @property {{ model: string, key: string }} references
|
||||
* @property {string} onDelete
|
||||
* @property {string} onUpdate
|
||||
*/
|
||||
|
||||
/**
|
||||
* @param {Object} fk - The foreign key object from PRAGMA foreign_key_list
|
||||
* @returns {SequelizeFKObj} - The foreign key object formatted for Sequelize
|
||||
*/
|
||||
const formatFKsPragmaToSequelizeFK = (fk) => {
|
||||
return {
|
||||
references: {
|
||||
model: fk.table,
|
||||
key: fk.to
|
||||
},
|
||||
onDelete: fk['on_delete'],
|
||||
onUpdate: fk['on_update']
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {import('sequelize').QueryInterface} queryInterface
|
||||
* @param {string} tableName
|
||||
* @param {ConstraintUpdateObj[]} constraints
|
||||
* @returns {Promise<Record<string, SequelizeFKObj>|null>}
|
||||
*/
|
||||
async function getUpdatedForeignKeys(queryInterface, tableName, constraints) {
|
||||
const execQuery = queryInterface.sequelize.query.bind(queryInterface.sequelize)
|
||||
const quotedTableName = queryInterface.quoteIdentifier(tableName)
|
||||
|
||||
const foreignKeys = await execQuery(`PRAGMA foreign_key_list(${quotedTableName});`)
|
||||
|
||||
let hasUpdates = false
|
||||
const foreignKeysByColName = foreignKeys.reduce((prev, curr) => {
|
||||
const fk = formatFKsPragmaToSequelizeFK(curr)
|
||||
|
||||
const constraint = constraints.find((c) => c.field === curr.from)
|
||||
if (constraint && (constraint.onDelete !== fk.onDelete || constraint.onUpdate !== fk.onUpdate)) {
|
||||
fk.onDelete = constraint.onDelete
|
||||
fk.onUpdate = constraint.onUpdate
|
||||
hasUpdates = true
|
||||
}
|
||||
|
||||
return { ...prev, [curr.from]: fk }
|
||||
}, {})
|
||||
|
||||
return hasUpdates ? foreignKeysByColName : null
|
||||
}
|
||||
|
||||
/**
|
||||
* Extends the Sequelize describeTable function to include the updated foreign key constraints
|
||||
*
|
||||
* @param {import('sequelize').QueryInterface} queryInterface
|
||||
* @param {String} tableName
|
||||
* @param {Record<string, SequelizeFKObj>} updatedForeignKeys
|
||||
*/
|
||||
async function describeTableWithFKs(queryInterface, tableName, updatedForeignKeys) {
|
||||
const tableDescription = await queryInterface.describeTable(tableName)
|
||||
|
||||
const tableDescriptionWithFks = Object.entries(tableDescription).reduce((prev, [col, attributes]) => {
|
||||
let extendedAttributes = attributes
|
||||
|
||||
if (updatedForeignKeys[col]) {
|
||||
extendedAttributes = {
|
||||
...extendedAttributes,
|
||||
...updatedForeignKeys[col]
|
||||
}
|
||||
}
|
||||
return { ...prev, [col]: extendedAttributes }
|
||||
}, {})
|
||||
|
||||
return tableDescriptionWithFks
|
||||
}
|
||||
|
||||
/**
|
||||
* @see https://www.sqlite.org/lang_altertable.html#otheralter
|
||||
* @see https://sequelize.org/docs/v6/other-topics/query-interface/#changing-and-removing-columns-in-sqlite
|
||||
*
|
||||
* @param {import('sequelize').QueryInterface} queryInterface
|
||||
* @param {string} tableName
|
||||
* @param {ConstraintUpdateObj[]} constraints
|
||||
* @returns {Promise<boolean>} - Return false if no changes are needed, true otherwise
|
||||
*/
|
||||
async function changeConstraints(queryInterface, tableName, constraints) {
|
||||
const updatedForeignKeys = await getUpdatedForeignKeys(queryInterface, tableName, constraints)
|
||||
if (!updatedForeignKeys) {
|
||||
return false
|
||||
}
|
||||
|
||||
const execQuery = queryInterface.sequelize.query.bind(queryInterface.sequelize)
|
||||
const quotedTableName = queryInterface.quoteIdentifier(tableName)
|
||||
|
||||
const backupTableName = `${tableName}_${Math.round(Math.random() * 100)}_backup`
|
||||
const quotedBackupTableName = queryInterface.quoteIdentifier(backupTableName)
|
||||
|
||||
try {
|
||||
const tableDescriptionWithFks = await describeTableWithFKs(queryInterface, tableName, updatedForeignKeys)
|
||||
|
||||
const attributes = queryInterface.queryGenerator.attributesToSQL(tableDescriptionWithFks)
|
||||
|
||||
// Create the backup table
|
||||
await queryInterface.createTable(backupTableName, attributes)
|
||||
|
||||
const attributeNames = Object.keys(attributes)
|
||||
.map((attr) => queryInterface.quoteIdentifier(attr))
|
||||
.join(', ')
|
||||
|
||||
// Copy all data from the target table to the backup table
|
||||
await execQuery(`INSERT INTO ${quotedBackupTableName} SELECT ${attributeNames} FROM ${quotedTableName};`)
|
||||
|
||||
// Drop the old (original) table
|
||||
await queryInterface.dropTable(tableName)
|
||||
|
||||
// Rename the backup table to the original table's name
|
||||
await queryInterface.renameTable(backupTableName, tableName)
|
||||
|
||||
// Validate that all foreign key constraints are correct
|
||||
const result = await execQuery(`PRAGMA foreign_key_check(${quotedTableName});`, {
|
||||
type: queryInterface.sequelize.Sequelize.QueryTypes.SELECT
|
||||
})
|
||||
|
||||
// There are foreign key violations, exit
|
||||
if (result.length) {
|
||||
return Promise.reject(`Foreign key violations detected: ${JSON.stringify(result, null, 2)}`)
|
||||
}
|
||||
|
||||
return true
|
||||
} catch (error) {
|
||||
return Promise.reject(error)
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { up, down }
|
||||
@@ -0,0 +1,84 @@
|
||||
/**
|
||||
* @typedef MigrationContext
|
||||
* @property {import('sequelize').QueryInterface} queryInterface - a suquelize QueryInterface object.
|
||||
* @property {import('../Logger')} logger - a Logger object.
|
||||
*
|
||||
* @typedef MigrationOptions
|
||||
* @property {MigrationContext} context - an object containing the migration context.
|
||||
*/
|
||||
|
||||
/**
|
||||
* This upward migration adds an subfolder setting for OIDC redirect URIs.
|
||||
* It updates existing OIDC setups to set this option to None (empty subfolder), so they continue to work as before.
|
||||
* IF OIDC is not enabled, no action is taken (i.e. the subfolder is left undefined),
|
||||
* so that future OIDC setups will use the default subfolder.
|
||||
*
|
||||
* @param {MigrationOptions} options - an object containing the migration context.
|
||||
* @returns {Promise<void>} - A promise that resolves when the migration is complete.
|
||||
*/
|
||||
async function up({ context: { queryInterface, logger } }) {
|
||||
// Upwards migration script
|
||||
logger.info('[2.17.4 migration] UPGRADE BEGIN: 2.17.4-use-subfolder-for-oidc-redirect-uris')
|
||||
|
||||
const serverSettings = await getServerSettings(queryInterface, logger)
|
||||
if (serverSettings.authActiveAuthMethods?.includes('openid')) {
|
||||
logger.info('[2.17.4 migration] OIDC is enabled, adding authOpenIDSubfolderForRedirectURLs to server settings')
|
||||
serverSettings.authOpenIDSubfolderForRedirectURLs = ''
|
||||
await updateServerSettings(queryInterface, logger, serverSettings)
|
||||
} else {
|
||||
logger.info('[2.17.4 migration] OIDC is not enabled, no action required')
|
||||
}
|
||||
|
||||
logger.info('[2.17.4 migration] UPGRADE END: 2.17.4-use-subfolder-for-oidc-redirect-uris')
|
||||
}
|
||||
|
||||
/**
|
||||
* This downward migration script removes the subfolder setting for OIDC redirect URIs.
|
||||
*
|
||||
* @param {MigrationOptions} options - an object containing the migration context.
|
||||
* @returns {Promise<void>} - A promise that resolves when the migration is complete.
|
||||
*/
|
||||
async function down({ context: { queryInterface, logger } }) {
|
||||
// Downward migration script
|
||||
logger.info('[2.17.4 migration] DOWNGRADE BEGIN: 2.17.4-use-subfolder-for-oidc-redirect-uris ')
|
||||
|
||||
// Remove the OIDC subfolder option from the server settings
|
||||
const serverSettings = await getServerSettings(queryInterface, logger)
|
||||
if (serverSettings.authOpenIDSubfolderForRedirectURLs !== undefined) {
|
||||
logger.info('[2.17.4 migration] Removing authOpenIDSubfolderForRedirectURLs from server settings')
|
||||
delete serverSettings.authOpenIDSubfolderForRedirectURLs
|
||||
await updateServerSettings(queryInterface, logger, serverSettings)
|
||||
} else {
|
||||
logger.info('[2.17.4 migration] authOpenIDSubfolderForRedirectURLs not found in server settings, no action required')
|
||||
}
|
||||
|
||||
logger.info('[2.17.4 migration] DOWNGRADE END: 2.17.4-use-subfolder-for-oidc-redirect-uris ')
|
||||
}
|
||||
|
||||
async function getServerSettings(queryInterface, logger) {
|
||||
const result = await queryInterface.sequelize.query('SELECT value FROM settings WHERE key = "server-settings";')
|
||||
if (!result[0].length) {
|
||||
logger.error('[2.17.4 migration] Server settings not found')
|
||||
throw new Error('Server settings not found')
|
||||
}
|
||||
|
||||
let serverSettings = null
|
||||
try {
|
||||
serverSettings = JSON.parse(result[0][0].value)
|
||||
} catch (error) {
|
||||
logger.error('[2.17.4 migration] Error parsing server settings:', error)
|
||||
throw error
|
||||
}
|
||||
|
||||
return serverSettings
|
||||
}
|
||||
|
||||
async function updateServerSettings(queryInterface, logger, serverSettings) {
|
||||
await queryInterface.sequelize.query('UPDATE settings SET value = :value WHERE key = "server-settings";', {
|
||||
replacements: {
|
||||
value: JSON.stringify(serverSettings)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
module.exports = { up, down }
|
||||
@@ -0,0 +1,74 @@
|
||||
/**
|
||||
* @typedef MigrationContext
|
||||
* @property {import('sequelize').QueryInterface} queryInterface - a suquelize QueryInterface object.
|
||||
* @property {import('../Logger')} logger - a Logger object.
|
||||
*
|
||||
* @typedef MigrationOptions
|
||||
* @property {MigrationContext} context - an object containing the migration context.
|
||||
*/
|
||||
|
||||
const migrationVersion = '2.17.5'
|
||||
const migrationName = `${migrationVersion}-remove-host-from-feed-urls`
|
||||
const loggerPrefix = `[${migrationVersion} migration]`
|
||||
|
||||
/**
|
||||
* This upward migration removes the host (serverAddress) from URL columns in the feeds and feedEpisodes tables.
|
||||
*
|
||||
* @param {MigrationOptions} options - an object containing the migration context.
|
||||
* @returns {Promise<void>} - A promise that resolves when the migration is complete.
|
||||
*/
|
||||
async function up({ context: { queryInterface, logger } }) {
|
||||
// Upwards migration script
|
||||
logger.info(`${loggerPrefix} UPGRADE BEGIN: ${migrationName}`)
|
||||
|
||||
logger.info(`${loggerPrefix} Removing serverAddress from Feeds table URLs`)
|
||||
await queryInterface.sequelize.query(`
|
||||
UPDATE Feeds
|
||||
SET feedUrl = REPLACE(feedUrl, COALESCE(serverAddress, ''), ''),
|
||||
imageUrl = REPLACE(imageUrl, COALESCE(serverAddress, ''), ''),
|
||||
siteUrl = REPLACE(siteUrl, COALESCE(serverAddress, ''), '');
|
||||
`)
|
||||
logger.info(`${loggerPrefix} Removed serverAddress from Feeds table URLs`)
|
||||
|
||||
logger.info(`${loggerPrefix} Removing serverAddress from FeedEpisodes table URLs`)
|
||||
await queryInterface.sequelize.query(`
|
||||
UPDATE FeedEpisodes
|
||||
SET siteUrl = REPLACE(siteUrl, (SELECT COALESCE(serverAddress, '') FROM Feeds WHERE Feeds.id = FeedEpisodes.feedId), ''),
|
||||
enclosureUrl = REPLACE(enclosureUrl, (SELECT COALESCE(serverAddress, '') FROM Feeds WHERE Feeds.id = FeedEpisodes.feedId), '');
|
||||
`)
|
||||
logger.info(`${loggerPrefix} Removed serverAddress from FeedEpisodes table URLs`)
|
||||
|
||||
logger.info(`${loggerPrefix} UPGRADE END: ${migrationName}`)
|
||||
}
|
||||
|
||||
/**
|
||||
* This downward migration script adds the host (serverAddress) back to URL columns in the feeds and feedEpisodes tables.
|
||||
*
|
||||
* @param {MigrationOptions} options - an object containing the migration context.
|
||||
* @returns {Promise<void>} - A promise that resolves when the migration is complete.
|
||||
*/
|
||||
async function down({ context: { queryInterface, logger } }) {
|
||||
// Downward migration script
|
||||
logger.info(`${loggerPrefix} DOWNGRADE BEGIN: ${migrationName}`)
|
||||
|
||||
logger.info(`${loggerPrefix} Adding serverAddress back to Feeds table URLs`)
|
||||
await queryInterface.sequelize.query(`
|
||||
UPDATE Feeds
|
||||
SET feedUrl = COALESCE(serverAddress, '') || feedUrl,
|
||||
imageUrl = COALESCE(serverAddress, '') || imageUrl,
|
||||
siteUrl = COALESCE(serverAddress, '') || siteUrl;
|
||||
`)
|
||||
logger.info(`${loggerPrefix} Added serverAddress back to Feeds table URLs`)
|
||||
|
||||
logger.info(`${loggerPrefix} Adding serverAddress back to FeedEpisodes table URLs`)
|
||||
await queryInterface.sequelize.query(`
|
||||
UPDATE FeedEpisodes
|
||||
SET siteUrl = (SELECT COALESCE(serverAddress, '') || FeedEpisodes.siteUrl FROM Feeds WHERE Feeds.id = FeedEpisodes.feedId),
|
||||
enclosureUrl = (SELECT COALESCE(serverAddress, '') || FeedEpisodes.enclosureUrl FROM Feeds WHERE Feeds.id = FeedEpisodes.feedId);
|
||||
`)
|
||||
logger.info(`${loggerPrefix} Added serverAddress back to FeedEpisodes table URLs`)
|
||||
|
||||
logger.info(`${loggerPrefix} DOWNGRADE END: ${migrationName}`)
|
||||
}
|
||||
|
||||
module.exports = { up, down }
|
||||
@@ -0,0 +1,68 @@
|
||||
/**
|
||||
* @typedef MigrationContext
|
||||
* @property {import('sequelize').QueryInterface} queryInterface - a Sequelize QueryInterface object.
|
||||
* @property {import('../Logger')} logger - a Logger object.
|
||||
*
|
||||
* @typedef MigrationOptions
|
||||
* @property {MigrationContext} context - an object containing the migration context.
|
||||
*/
|
||||
|
||||
const migrationVersion = '2.17.6'
|
||||
const migrationName = `${migrationVersion}-share-add-isdownloadable`
|
||||
const loggerPrefix = `[${migrationVersion} migration]`
|
||||
|
||||
/**
|
||||
* This migration script adds the isDownloadable column to the mediaItemShares table.
|
||||
*
|
||||
* @param {MigrationOptions} options - an object containing the migration context.
|
||||
* @returns {Promise<void>} - A promise that resolves when the migration is complete.
|
||||
*/
|
||||
async function up({ context: { queryInterface, logger } }) {
|
||||
logger.info(`${loggerPrefix} UPGRADE BEGIN: ${migrationName}`)
|
||||
|
||||
if (await queryInterface.tableExists('mediaItemShares')) {
|
||||
const tableDescription = await queryInterface.describeTable('mediaItemShares')
|
||||
if (!tableDescription.isDownloadable) {
|
||||
logger.info(`${loggerPrefix} Adding isDownloadable column to mediaItemShares table`)
|
||||
await queryInterface.addColumn('mediaItemShares', 'isDownloadable', {
|
||||
type: queryInterface.sequelize.Sequelize.DataTypes.BOOLEAN,
|
||||
defaultValue: false,
|
||||
allowNull: false
|
||||
})
|
||||
logger.info(`${loggerPrefix} Added isDownloadable column to mediaItemShares table`)
|
||||
} else {
|
||||
logger.info(`${loggerPrefix} isDownloadable column already exists in mediaItemShares table`)
|
||||
}
|
||||
} else {
|
||||
logger.info(`${loggerPrefix} mediaItemShares table does not exist`)
|
||||
}
|
||||
|
||||
logger.info(`${loggerPrefix} UPGRADE END: ${migrationName}`)
|
||||
}
|
||||
|
||||
/**
|
||||
* This migration script removes the isDownloadable column from the mediaItemShares table.
|
||||
*
|
||||
* @param {MigrationOptions} options - an object containing the migration context.
|
||||
* @returns {Promise<void>} - A promise that resolves when the migration is complete.
|
||||
*/
|
||||
async function down({ context: { queryInterface, logger } }) {
|
||||
logger.info(`${loggerPrefix} DOWNGRADE BEGIN: ${migrationName}`)
|
||||
|
||||
if (await queryInterface.tableExists('mediaItemShares')) {
|
||||
const tableDescription = await queryInterface.describeTable('mediaItemShares')
|
||||
if (tableDescription.isDownloadable) {
|
||||
logger.info(`${loggerPrefix} Removing isDownloadable column from mediaItemShares table`)
|
||||
await queryInterface.removeColumn('mediaItemShares', 'isDownloadable')
|
||||
logger.info(`${loggerPrefix} Removed isDownloadable column from mediaItemShares table`)
|
||||
} else {
|
||||
logger.info(`${loggerPrefix} isDownloadable column does not exist in mediaItemShares table`)
|
||||
}
|
||||
} else {
|
||||
logger.info(`${loggerPrefix} mediaItemShares table does not exist`)
|
||||
}
|
||||
|
||||
logger.info(`${loggerPrefix} DOWNGRADE END: ${migrationName}`)
|
||||
}
|
||||
|
||||
module.exports = { up, down }
|
||||
@@ -0,0 +1,83 @@
|
||||
/**
|
||||
* @typedef MigrationContext
|
||||
* @property {import('sequelize').QueryInterface} queryInterface - a suquelize QueryInterface object.
|
||||
* @property {import('../Logger')} logger - a Logger object.
|
||||
*
|
||||
* @typedef MigrationOptions
|
||||
* @property {MigrationContext} context - an object containing the migration context.
|
||||
*/
|
||||
|
||||
const migrationVersion = '2.17.7'
|
||||
const migrationName = `${migrationVersion}-add-indices`
|
||||
const loggerPrefix = `[${migrationVersion} migration]`
|
||||
|
||||
/**
|
||||
* This upward migration adds some indices to the libraryItems and books tables to improve query performance
|
||||
*
|
||||
* @param {MigrationOptions} options - an object containing the migration context.
|
||||
* @returns {Promise<void>} - A promise that resolves when the migration is complete.
|
||||
*/
|
||||
async function up({ context: { queryInterface, logger } }) {
|
||||
// Upwards migration script
|
||||
logger.info(`${loggerPrefix} UPGRADE BEGIN: ${migrationName}`)
|
||||
|
||||
await addIndex(queryInterface, logger, 'libraryItems', ['libraryId', 'mediaType', 'size'])
|
||||
await addIndex(queryInterface, logger, 'books', ['duration'])
|
||||
|
||||
logger.info(`${loggerPrefix} UPGRADE END: ${migrationName}`)
|
||||
}
|
||||
|
||||
/**
|
||||
* This downward migration script removes the indices added in the upward migration script
|
||||
*
|
||||
* @param {MigrationOptions} options - an object containing the migration context.
|
||||
* @returns {Promise<void>} - A promise that resolves when the migration is complete.
|
||||
*/
|
||||
async function down({ context: { queryInterface, logger } }) {
|
||||
// Downward migration script
|
||||
logger.info(`${loggerPrefix} DOWNGRADE BEGIN: ${migrationName}`)
|
||||
|
||||
await removeIndex(queryInterface, logger, 'libraryItems', ['libraryId', 'mediaType', 'size'])
|
||||
await removeIndex(queryInterface, logger, 'books', ['duration'])
|
||||
|
||||
logger.info(`${loggerPrefix} DOWNGRADE END: ${migrationName}`)
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility function to add an index to a table. If the index already exists, it logs a message and continues.
|
||||
*
|
||||
* @param {import('sequelize').QueryInterface} queryInterface
|
||||
* @param {import ('../Logger')} logger
|
||||
* @param {string} tableName
|
||||
* @param {string[]} columns
|
||||
*/
|
||||
async function addIndex(queryInterface, logger, tableName, columns) {
|
||||
try {
|
||||
logger.info(`${loggerPrefix} adding index [${columns.join(', ')}] to table "${tableName}"`)
|
||||
await queryInterface.addIndex(tableName, columns)
|
||||
logger.info(`${loggerPrefix} added index [${columns.join(', ')}] to table "${tableName}"`)
|
||||
} catch (error) {
|
||||
if (error.name === 'SequelizeDatabaseError' && error.message.includes('already exists')) {
|
||||
logger.info(`${loggerPrefix} index [${columns.join(', ')}] for table "${tableName}" already exists`)
|
||||
} else {
|
||||
throw error
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility function to remove an index from a table.
|
||||
* Sequelize implemets it using DROP INDEX IF EXISTS, so it won't throw an error if the index doesn't exist.
|
||||
*
|
||||
* @param {import('sequelize').QueryInterface} queryInterface
|
||||
* @param {import ('../Logger')} logger
|
||||
* @param {string} tableName
|
||||
* @param {string[]} columns
|
||||
*/
|
||||
async function removeIndex(queryInterface, logger, tableName, columns) {
|
||||
logger.info(`${loggerPrefix} removing index [${columns.join(', ')}] from table "${tableName}"`)
|
||||
await queryInterface.removeIndex(tableName, columns)
|
||||
logger.info(`${loggerPrefix} removed index [${columns.join(', ')}] from table "${tableName}"`)
|
||||
}
|
||||
|
||||
module.exports = { up, down }
|
||||
@@ -0,0 +1,164 @@
|
||||
const util = require('util')
|
||||
|
||||
/**
|
||||
* @typedef MigrationContext
|
||||
* @property {import('sequelize').QueryInterface} queryInterface - a suquelize QueryInterface object.
|
||||
* @property {import('../Logger')} logger - a Logger object.
|
||||
*
|
||||
* @typedef MigrationOptions
|
||||
* @property {MigrationContext} context - an object containing the migration context.
|
||||
*/
|
||||
|
||||
const migrationVersion = '2.19.1'
|
||||
const migrationName = `${migrationVersion}-copy-title-to-library-items`
|
||||
const loggerPrefix = `[${migrationVersion} migration]`
|
||||
|
||||
/**
|
||||
* This upward migration adds a title column to the libraryItems table, copies the title from the book to the libraryItem,
|
||||
* and creates a new index on the title column. In addition it sets a trigger on the books table to update the title column
|
||||
* in the libraryItems table when a book is updated.
|
||||
*
|
||||
* @param {MigrationOptions} options - an object containing the migration context.
|
||||
* @returns {Promise<void>} - A promise that resolves when the migration is complete.
|
||||
*/
|
||||
async function up({ context: { queryInterface, logger } }) {
|
||||
// Upwards migration script
|
||||
logger.info(`${loggerPrefix} UPGRADE BEGIN: ${migrationName}`)
|
||||
|
||||
await addColumn(queryInterface, logger, 'libraryItems', 'title', { type: queryInterface.sequelize.Sequelize.STRING, allowNull: true })
|
||||
await copyColumn(queryInterface, logger, 'books', 'title', 'id', 'libraryItems', 'title', 'mediaId')
|
||||
await addTrigger(queryInterface, logger, 'books', 'title', 'id', 'libraryItems', 'title', 'mediaId')
|
||||
await addIndex(queryInterface, logger, 'libraryItems', ['libraryId', 'mediaType', { name: 'title', collate: 'NOCASE' }])
|
||||
|
||||
await addColumn(queryInterface, logger, 'libraryItems', 'titleIgnorePrefix', { type: queryInterface.sequelize.Sequelize.STRING, allowNull: true })
|
||||
await copyColumn(queryInterface, logger, 'books', 'titleIgnorePrefix', 'id', 'libraryItems', 'titleIgnorePrefix', 'mediaId')
|
||||
await addTrigger(queryInterface, logger, 'books', 'titleIgnorePrefix', 'id', 'libraryItems', 'titleIgnorePrefix', 'mediaId')
|
||||
await addIndex(queryInterface, logger, 'libraryItems', ['libraryId', 'mediaType', { name: 'titleIgnorePrefix', collate: 'NOCASE' }])
|
||||
|
||||
await addIndex(queryInterface, logger, 'libraryItems', ['libraryId', 'mediaType', 'createdAt'])
|
||||
|
||||
logger.info(`${loggerPrefix} UPGRADE END: ${migrationName}`)
|
||||
}
|
||||
|
||||
/**
|
||||
* This downward migration script removes the title column from the libraryItems table, removes the trigger on the books table,
|
||||
* and removes the index on the title column.
|
||||
*
|
||||
* @param {MigrationOptions} options - an object containing the migration context.
|
||||
* @returns {Promise<void>} - A promise that resolves when the migration is complete.
|
||||
*/
|
||||
async function down({ context: { queryInterface, logger } }) {
|
||||
// Downward migration script
|
||||
logger.info(`${loggerPrefix} DOWNGRADE BEGIN: ${migrationName}`)
|
||||
|
||||
await removeIndex(queryInterface, logger, 'libraryItems', ['libraryId', 'mediaType', 'title'])
|
||||
await removeTrigger(queryInterface, logger, 'libraryItems', 'title')
|
||||
await removeColumn(queryInterface, logger, 'libraryItems', 'title')
|
||||
|
||||
await removeIndex(queryInterface, logger, 'libraryItems', ['libraryId', 'mediaType', 'titleIgnorePrefix'])
|
||||
await removeTrigger(queryInterface, logger, 'libraryItems', 'titleIgnorePrefix')
|
||||
await removeColumn(queryInterface, logger, 'libraryItems', 'titleIgnorePrefix')
|
||||
|
||||
await removeIndex(queryInterface, logger, 'libraryItems', ['libraryId', 'mediaType', 'createdAt'])
|
||||
|
||||
logger.info(`${loggerPrefix} DOWNGRADE END: ${migrationName}`)
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility function to add an index to a table. If the index already z`exists, it logs a message and continues.
|
||||
*
|
||||
* @param {import('sequelize').QueryInterface} queryInterface
|
||||
* @param {import ('../Logger')} logger
|
||||
* @param {string} tableName
|
||||
* @param {string[]} columns
|
||||
*/
|
||||
async function addIndex(queryInterface, logger, tableName, columns) {
|
||||
const columnString = columns.map((column) => util.inspect(column)).join(', ')
|
||||
const indexName = convertToSnakeCase(`${tableName}_${columns.map((column) => (typeof column === 'string' ? column : column.name)).join('_')}`)
|
||||
try {
|
||||
logger.info(`${loggerPrefix} adding index on [${columnString}] to table ${tableName}. index name: ${indexName}"`)
|
||||
await queryInterface.addIndex(tableName, columns)
|
||||
logger.info(`${loggerPrefix} added index on [${columnString}] to table ${tableName}. index name: ${indexName}"`)
|
||||
} catch (error) {
|
||||
if (error.name === 'SequelizeDatabaseError' && error.message.includes('already exists')) {
|
||||
logger.info(`${loggerPrefix} index [${columnString}] for table "${tableName}" already exists`)
|
||||
} else {
|
||||
throw error
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility function to remove an index from a table.
|
||||
* Sequelize implemets it using DROP INDEX IF EXISTS, so it won't throw an error if the index doesn't exist.
|
||||
*
|
||||
* @param {import('sequelize').QueryInterface} queryInterface
|
||||
* @param {import ('../Logger')} logger
|
||||
* @param {string} tableName
|
||||
* @param {string[]} columns
|
||||
*/
|
||||
async function removeIndex(queryInterface, logger, tableName, columns) {
|
||||
logger.info(`${loggerPrefix} removing index [${columns.join(', ')}] from table "${tableName}"`)
|
||||
await queryInterface.removeIndex(tableName, columns)
|
||||
logger.info(`${loggerPrefix} removed index [${columns.join(', ')}] from table "${tableName}"`)
|
||||
}
|
||||
|
||||
async function addColumn(queryInterface, logger, table, column, options) {
|
||||
logger.info(`${loggerPrefix} adding column "${column}" to table "${table}"`)
|
||||
const tableDescription = await queryInterface.describeTable(table)
|
||||
if (!tableDescription[column]) {
|
||||
await queryInterface.addColumn(table, column, options)
|
||||
logger.info(`${loggerPrefix} added column "${column}" to table "${table}"`)
|
||||
} else {
|
||||
logger.info(`${loggerPrefix} column "${column}" already exists in table "${table}"`)
|
||||
}
|
||||
}
|
||||
|
||||
async function removeColumn(queryInterface, logger, table, column) {
|
||||
logger.info(`${loggerPrefix} removing column "${column}" from table "${table}"`)
|
||||
await queryInterface.removeColumn(table, column)
|
||||
logger.info(`${loggerPrefix} removed column "${column}" from table "${table}"`)
|
||||
}
|
||||
|
||||
async function copyColumn(queryInterface, logger, sourceTable, sourceColumn, sourceIdColumn, targetTable, targetColumn, targetIdColumn) {
|
||||
logger.info(`${loggerPrefix} copying column "${sourceColumn}" from table "${sourceTable}" to table "${targetTable}"`)
|
||||
await queryInterface.sequelize.query(`
|
||||
UPDATE ${targetTable}
|
||||
SET ${targetColumn} = ${sourceTable}.${sourceColumn}
|
||||
FROM ${sourceTable}
|
||||
WHERE ${targetTable}.${targetIdColumn} = ${sourceTable}.${sourceIdColumn}
|
||||
`)
|
||||
logger.info(`${loggerPrefix} copied column "${sourceColumn}" from table "${sourceTable}" to table "${targetTable}"`)
|
||||
}
|
||||
|
||||
async function addTrigger(queryInterface, logger, sourceTable, sourceColumn, sourceIdColumn, targetTable, targetColumn, targetIdColumn) {
|
||||
logger.info(`${loggerPrefix} adding trigger to update ${targetTable}.${targetColumn} when ${sourceTable}.${sourceColumn} is updated`)
|
||||
const triggerName = convertToSnakeCase(`update_${targetTable}_${targetColumn}`)
|
||||
|
||||
await queryInterface.sequelize.query(`DROP TRIGGER IF EXISTS ${triggerName}`)
|
||||
|
||||
await queryInterface.sequelize.query(`
|
||||
CREATE TRIGGER ${triggerName}
|
||||
AFTER UPDATE OF ${sourceColumn} ON ${sourceTable}
|
||||
FOR EACH ROW
|
||||
BEGIN
|
||||
UPDATE ${targetTable}
|
||||
SET ${targetColumn} = NEW.${sourceColumn}
|
||||
WHERE ${targetTable}.${targetIdColumn} = NEW.${sourceIdColumn};
|
||||
END;
|
||||
`)
|
||||
logger.info(`${loggerPrefix} added trigger to update ${targetTable}.${targetColumn} when ${sourceTable}.${sourceColumn} is updated`)
|
||||
}
|
||||
|
||||
async function removeTrigger(queryInterface, logger, targetTable, targetColumn) {
|
||||
logger.info(`${loggerPrefix} removing trigger to update ${targetTable}.${targetColumn}`)
|
||||
const triggerName = convertToSnakeCase(`update_${targetTable}_${targetColumn}`)
|
||||
await queryInterface.sequelize.query(`DROP TRIGGER IF EXISTS ${triggerName}`)
|
||||
logger.info(`${loggerPrefix} removed trigger to update ${targetTable}.${targetColumn}`)
|
||||
}
|
||||
|
||||
function convertToSnakeCase(str) {
|
||||
return str.replace(/([A-Z])/g, '_$1').toLowerCase()
|
||||
}
|
||||
|
||||
module.exports = { up, down }
|
||||
@@ -0,0 +1,219 @@
|
||||
const util = require('util')
|
||||
|
||||
/**
|
||||
* @typedef MigrationContext
|
||||
* @property {import('sequelize').QueryInterface} queryInterface - a suquelize QueryInterface object.
|
||||
* @property {import('../Logger')} logger - a Logger object.
|
||||
*
|
||||
* @typedef MigrationOptions
|
||||
* @property {MigrationContext} context - an object containing the migration context.
|
||||
*/
|
||||
|
||||
const migrationVersion = '2.19.4'
|
||||
const migrationName = `${migrationVersion}-improve-podcast-queries`
|
||||
const loggerPrefix = `[${migrationVersion} migration]`
|
||||
|
||||
/**
|
||||
* This upward migration adds a numEpisodes column to the podcasts table and populates it.
|
||||
* It also adds a podcastId column to the mediaProgresses table and populates it.
|
||||
* It also copies the title and titleIgnorePrefix columns from the podcasts table to the libraryItems table,
|
||||
* and adds triggers to update them when the corresponding columns in the podcasts table are updated.
|
||||
*
|
||||
* @param {MigrationOptions} options - an object containing the migration context.
|
||||
* @returns {Promise<void>} - A promise that resolves when the migration is complete.
|
||||
*/
|
||||
async function up({ context: { queryInterface, logger } }) {
|
||||
// Upwards migration script
|
||||
logger.info(`${loggerPrefix} UPGRADE BEGIN: ${migrationName}`)
|
||||
|
||||
// Add numEpisodes column to podcasts table
|
||||
await addColumn(queryInterface, logger, 'podcasts', 'numEpisodes', { type: queryInterface.sequelize.Sequelize.INTEGER, allowNull: false, defaultValue: 0 })
|
||||
|
||||
// Populate numEpisodes column with the number of episodes for each podcast
|
||||
await populateNumEpisodes(queryInterface, logger)
|
||||
|
||||
// Add podcastId column to mediaProgresses table
|
||||
await addColumn(queryInterface, logger, 'mediaProgresses', 'podcastId', { type: queryInterface.sequelize.Sequelize.UUID, allowNull: true })
|
||||
|
||||
// Populate podcastId column with the podcastId for each mediaProgress
|
||||
await populatePodcastId(queryInterface, logger)
|
||||
|
||||
// Copy title and titleIgnorePrefix columns from podcasts to libraryItems
|
||||
await copyColumn(queryInterface, logger, 'podcasts', 'title', 'id', 'libraryItems', 'title', 'mediaId')
|
||||
await copyColumn(queryInterface, logger, 'podcasts', 'titleIgnorePrefix', 'id', 'libraryItems', 'titleIgnorePrefix', 'mediaId')
|
||||
|
||||
// Add triggers to update title and titleIgnorePrefix in libraryItems
|
||||
await addTrigger(queryInterface, logger, 'podcasts', 'title', 'id', 'libraryItems', 'title', 'mediaId')
|
||||
await addTrigger(queryInterface, logger, 'podcasts', 'titleIgnorePrefix', 'id', 'libraryItems', 'titleIgnorePrefix', 'mediaId')
|
||||
|
||||
logger.info(`${loggerPrefix} UPGRADE END: ${migrationName}`)
|
||||
}
|
||||
|
||||
/**
|
||||
* This downward migration removes the triggers on the podcasts table,
|
||||
* the numEpisodes column from the podcasts table, and the podcastId column from the mediaProgresses table.
|
||||
*
|
||||
* @param {MigrationOptions} options - an object containing the migration context.
|
||||
* @returns {Promise<void>} - A promise that resolves when the migration is complete.
|
||||
*/
|
||||
async function down({ context: { queryInterface, logger } }) {
|
||||
// Downward migration script
|
||||
logger.info(`${loggerPrefix} DOWNGRADE BEGIN: ${migrationName}`)
|
||||
|
||||
// Remove triggers from libraryItems
|
||||
await removeTrigger(queryInterface, logger, 'podcasts', 'title', 'libraryItems', 'title')
|
||||
await removeTrigger(queryInterface, logger, 'podcasts', 'titleIgnorePrefix', 'libraryItems', 'titleIgnorePrefix')
|
||||
|
||||
// Remove numEpisodes column from podcasts table
|
||||
await removeColumn(queryInterface, logger, 'podcasts', 'numEpisodes')
|
||||
|
||||
// Remove podcastId column from mediaProgresses table
|
||||
await removeColumn(queryInterface, logger, 'mediaProgresses', 'podcastId')
|
||||
|
||||
logger.info(`${loggerPrefix} DOWNGRADE END: ${migrationName}`)
|
||||
}
|
||||
|
||||
async function populateNumEpisodes(queryInterface, logger) {
|
||||
logger.info(`${loggerPrefix} populating numEpisodes column in podcasts table`)
|
||||
await queryInterface.sequelize.query(`
|
||||
UPDATE podcasts
|
||||
SET numEpisodes = (SELECT COUNT(*) FROM podcastEpisodes WHERE podcastEpisodes.podcastId = podcasts.id)
|
||||
`)
|
||||
logger.info(`${loggerPrefix} populated numEpisodes column in podcasts table`)
|
||||
}
|
||||
|
||||
async function populatePodcastId(queryInterface, logger) {
|
||||
logger.info(`${loggerPrefix} populating podcastId column in mediaProgresses table`)
|
||||
// bulk update podcastId to the podcastId of the podcastEpisode if the mediaItemType is podcastEpisode
|
||||
await queryInterface.sequelize.query(`
|
||||
UPDATE mediaProgresses
|
||||
SET podcastId = (SELECT podcastId FROM podcastEpisodes WHERE podcastEpisodes.id = mediaProgresses.mediaItemId)
|
||||
WHERE mediaItemType = 'podcastEpisode'
|
||||
`)
|
||||
logger.info(`${loggerPrefix} populated podcastId column in mediaProgresses table`)
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility function to add a column to a table. If the column already exists, it logs a message and continues.
|
||||
*
|
||||
* @param {import('sequelize').QueryInterface} queryInterface - a suquelize QueryInterface object.
|
||||
* @param {import('../Logger')} logger - a Logger object.
|
||||
* @param {string} table - the name of the table to add the column to.
|
||||
* @param {string} column - the name of the column to add.
|
||||
* @param {Object} options - the options for the column.
|
||||
*/
|
||||
async function addColumn(queryInterface, logger, table, column, options) {
|
||||
logger.info(`${loggerPrefix} adding column "${column}" to table "${table}"`)
|
||||
const tableDescription = await queryInterface.describeTable(table)
|
||||
if (!tableDescription[column]) {
|
||||
await queryInterface.addColumn(table, column, options)
|
||||
logger.info(`${loggerPrefix} added column "${column}" to table "${table}"`)
|
||||
} else {
|
||||
logger.info(`${loggerPrefix} column "${column}" already exists in table "${table}"`)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility function to remove a column from a table. If the column does not exist, it logs a message and continues.
|
||||
*
|
||||
* @param {import('sequelize').QueryInterface} queryInterface - a suquelize QueryInterface object.
|
||||
* @param {import('../Logger')} logger - a Logger object.
|
||||
* @param {string} table - the name of the table to remove the column from.
|
||||
* @param {string} column - the name of the column to remove.
|
||||
*/
|
||||
async function removeColumn(queryInterface, logger, table, column) {
|
||||
logger.info(`${loggerPrefix} removing column "${column}" from table "${table}"`)
|
||||
const tableDescription = await queryInterface.describeTable(table)
|
||||
if (tableDescription[column]) {
|
||||
await queryInterface.sequelize.query(`ALTER TABLE ${table} DROP COLUMN ${column}`)
|
||||
logger.info(`${loggerPrefix} removed column "${column}" from table "${table}"`)
|
||||
} else {
|
||||
logger.info(`${loggerPrefix} column "${column}" does not exist in table "${table}"`)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility function to add a trigger to update a column in a target table when a column in a source table is updated.
|
||||
* If the trigger already exists, it drops it and creates a new one.
|
||||
* sourceIdColumn and targetIdColumn are used to match the source and target rows.
|
||||
*
|
||||
* @param {import('sequelize').QueryInterface} queryInterface - a suquelize QueryInterface object.
|
||||
* @param {import('../Logger')} logger - a Logger object.
|
||||
* @param {string} sourceTable - the name of the source table.
|
||||
* @param {string} sourceColumn - the name of the column to update.
|
||||
* @param {string} sourceIdColumn - the name of the id column of the source table.
|
||||
* @param {string} targetTable - the name of the target table.
|
||||
* @param {string} targetColumn - the name of the column to update.
|
||||
* @param {string} targetIdColumn - the name of the id column of the target table.
|
||||
*/
|
||||
async function addTrigger(queryInterface, logger, sourceTable, sourceColumn, sourceIdColumn, targetTable, targetColumn, targetIdColumn) {
|
||||
logger.info(`${loggerPrefix} adding trigger to update ${targetTable}.${targetColumn} when ${sourceTable}.${sourceColumn} is updated`)
|
||||
const triggerName = convertToSnakeCase(`update_${targetTable}_${targetColumn}_from_${sourceTable}_${sourceColumn}`)
|
||||
|
||||
await queryInterface.sequelize.query(`DROP TRIGGER IF EXISTS ${triggerName}`)
|
||||
|
||||
await queryInterface.sequelize.query(`
|
||||
CREATE TRIGGER ${triggerName}
|
||||
AFTER UPDATE OF ${sourceColumn} ON ${sourceTable}
|
||||
FOR EACH ROW
|
||||
BEGIN
|
||||
UPDATE ${targetTable}
|
||||
SET ${targetColumn} = NEW.${sourceColumn}
|
||||
WHERE ${targetTable}.${targetIdColumn} = NEW.${sourceIdColumn};
|
||||
END;
|
||||
`)
|
||||
logger.info(`${loggerPrefix} added trigger to update ${targetTable}.${targetColumn} when ${sourceTable}.${sourceColumn} is updated`)
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility function to remove an update trigger from a table.
|
||||
*
|
||||
* @param {import('sequelize').QueryInterface} queryInterface - a suquelize QueryInterface object.
|
||||
* @param {import('../Logger')} logger - a Logger object.
|
||||
* @param {string} sourceTable - the name of the source table.
|
||||
* @param {string} sourceColumn - the name of the column to update.
|
||||
* @param {string} targetTable - the name of the target table.
|
||||
* @param {string} targetColumn - the name of the column to update.
|
||||
*/
|
||||
async function removeTrigger(queryInterface, logger, sourceTable, sourceColumn, targetTable, targetColumn) {
|
||||
logger.info(`${loggerPrefix} removing trigger to update ${targetTable}.${targetColumn}`)
|
||||
const triggerName = convertToSnakeCase(`update_${targetTable}_${targetColumn}_from_${sourceTable}_${sourceColumn}`)
|
||||
await queryInterface.sequelize.query(`DROP TRIGGER IF EXISTS ${triggerName}`)
|
||||
logger.info(`${loggerPrefix} removed trigger to update ${targetTable}.${targetColumn}`)
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility function to copy a column from a source table to a target table.
|
||||
* sourceIdColumn and targetIdColumn are used to match the source and target rows.
|
||||
*
|
||||
* @param {import('sequelize').QueryInterface} queryInterface - a suquelize QueryInterface object.
|
||||
* @param {import('../Logger')} logger - a Logger object.
|
||||
* @param {string} sourceTable - the name of the source table.
|
||||
* @param {string} sourceColumn - the name of the column to copy.
|
||||
* @param {string} sourceIdColumn - the name of the id column of the source table.
|
||||
* @param {string} targetTable - the name of the target table.
|
||||
* @param {string} targetColumn - the name of the column to copy to.
|
||||
* @param {string} targetIdColumn - the name of the id column of the target table.
|
||||
*/
|
||||
async function copyColumn(queryInterface, logger, sourceTable, sourceColumn, sourceIdColumn, targetTable, targetColumn, targetIdColumn) {
|
||||
logger.info(`${loggerPrefix} copying column "${sourceColumn}" from table "${sourceTable}" to table "${targetTable}"`)
|
||||
await queryInterface.sequelize.query(`
|
||||
UPDATE ${targetTable}
|
||||
SET ${targetColumn} = ${sourceTable}.${sourceColumn}
|
||||
FROM ${sourceTable}
|
||||
WHERE ${targetTable}.${targetIdColumn} = ${sourceTable}.${sourceIdColumn}
|
||||
`)
|
||||
logger.info(`${loggerPrefix} copied column "${sourceColumn}" from table "${sourceTable}" to table "${targetTable}"`)
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility function to convert a string to snake case, e.g. "titleIgnorePrefix" -> "title_ignore_prefix"
|
||||
*
|
||||
* @param {string} str - the string to convert to snake case.
|
||||
* @returns {string} - the string in snake case.
|
||||
*/
|
||||
function convertToSnakeCase(str) {
|
||||
return str.replace(/([A-Z])/g, '_$1').toLowerCase()
|
||||
}
|
||||
|
||||
module.exports = { up, down }
|
||||
@@ -0,0 +1,272 @@
|
||||
const util = require('util')
|
||||
const { Sequelize } = require('sequelize')
|
||||
|
||||
/**
|
||||
* @typedef MigrationContext
|
||||
* @property {import('sequelize').QueryInterface} queryInterface - a suquelize QueryInterface object.
|
||||
* @property {import('../Logger')} logger - a Logger object.
|
||||
*
|
||||
* @typedef MigrationOptions
|
||||
* @property {MigrationContext} context - an object containing the migration context.
|
||||
*/
|
||||
|
||||
const migrationVersion = '2.20.0'
|
||||
const migrationName = `${migrationVersion}-improve-author-sort-queries`
|
||||
const loggerPrefix = `[${migrationVersion} migration]`
|
||||
|
||||
// Migration constants
|
||||
const libraryItems = 'libraryItems'
|
||||
const bookAuthors = 'bookAuthors'
|
||||
const authors = 'authors'
|
||||
const podcastEpisodes = 'podcastEpisodes'
|
||||
const columns = [
|
||||
{ name: 'authorNamesFirstLast', source: `${authors}.name`, spec: { type: Sequelize.STRING, allowNull: true } },
|
||||
{ name: 'authorNamesLastFirst', source: `${authors}.lastFirst`, spec: { type: Sequelize.STRING, allowNull: true } }
|
||||
]
|
||||
const authorsSort = `${bookAuthors}.createdAt ASC`
|
||||
const columnNames = columns.map((column) => column.name).join(', ')
|
||||
const columnSourcesExpression = columns.map((column) => `GROUP_CONCAT(${column.source}, ', ' ORDER BY ${authorsSort})`).join(', ')
|
||||
const authorsJoin = `${authors} JOIN ${bookAuthors} ON ${authors}.id = ${bookAuthors}.authorId`
|
||||
|
||||
/**
|
||||
* This upward migration adds an authorNames column to the libraryItems table and populates it.
|
||||
* It also creates triggers to update the authorNames column when the corresponding bookAuthors and authors records are updated.
|
||||
* It also creates an index on the authorNames column.
|
||||
*
|
||||
* It also adds an index on publishedAt to the podcastEpisodes table.
|
||||
*
|
||||
* @param {MigrationOptions} options - an object containing the migration context.
|
||||
* @returns {Promise<void>} - A promise that resolves when the migration is complete.
|
||||
*/
|
||||
async function up({ context: { queryInterface, logger } }) {
|
||||
const helper = new MigrationHelper(queryInterface, logger)
|
||||
|
||||
// Upwards migration script
|
||||
logger.info(`${loggerPrefix} UPGRADE BEGIN: ${migrationName}`)
|
||||
|
||||
// Add authorNames columns to libraryItems table
|
||||
await helper.addColumns()
|
||||
|
||||
// Populate authorNames columns with the author names for each libraryItem
|
||||
await helper.populateColumnsFromSource()
|
||||
|
||||
// Create triggers to update the authorNames column when the corresponding bookAuthors and authors records are updated
|
||||
await helper.addTriggers()
|
||||
|
||||
// Create indexes on the authorNames columns
|
||||
await helper.addIndexes()
|
||||
|
||||
// Add index on publishedAt to the podcastEpisodes table
|
||||
await helper.addIndex(podcastEpisodes, ['publishedAt'])
|
||||
|
||||
logger.info(`${loggerPrefix} UPGRADE END: ${migrationName}`)
|
||||
}
|
||||
|
||||
/**
|
||||
* This downward migration removes the authorNames column from the libraryItems table,
|
||||
* the triggers on the bookAuthors and authors tables, and the index on the authorNames column.
|
||||
*
|
||||
* It also removes the index on publishedAt from the podcastEpisodes table.
|
||||
*
|
||||
* @param {MigrationOptions} options - an object containing the migration context.
|
||||
* @returns {Promise<void>} - A promise that resolves when the migration is complete.
|
||||
*/
|
||||
async function down({ context: { queryInterface, logger } }) {
|
||||
// Downward migration script
|
||||
logger.info(`${loggerPrefix} DOWNGRADE BEGIN: ${migrationName}`)
|
||||
|
||||
const helper = new MigrationHelper(queryInterface, logger)
|
||||
|
||||
// Remove triggers to update authorNames columns
|
||||
await helper.removeTriggers()
|
||||
|
||||
// Remove index on publishedAt from the podcastEpisodes table
|
||||
await helper.removeIndex(podcastEpisodes, ['publishedAt'])
|
||||
|
||||
// Remove indexes on the authorNames columns
|
||||
await helper.removeIndexes()
|
||||
|
||||
// Remove authorNames columns from libraryItems table
|
||||
await helper.removeColumns()
|
||||
|
||||
logger.info(`${loggerPrefix} DOWNGRADE END: ${migrationName}`)
|
||||
}
|
||||
|
||||
class MigrationHelper {
|
||||
constructor(queryInterface, logger) {
|
||||
this.queryInterface = queryInterface
|
||||
this.logger = logger
|
||||
}
|
||||
|
||||
async addColumn(table, column, options) {
|
||||
this.logger.info(`${loggerPrefix} adding column "${column}" to table "${table}"`)
|
||||
const tableDescription = await this.queryInterface.describeTable(table)
|
||||
if (!tableDescription[column]) {
|
||||
await this.queryInterface.addColumn(table, column, options)
|
||||
this.logger.info(`${loggerPrefix} added column "${column}" to table "${table}"`)
|
||||
} else {
|
||||
this.logger.info(`${loggerPrefix} column "${column}" already exists in table "${table}"`)
|
||||
}
|
||||
}
|
||||
|
||||
async addColumns() {
|
||||
this.logger.info(`${loggerPrefix} adding ${columnNames} columns to ${libraryItems} table`)
|
||||
for (const column of columns) {
|
||||
await this.addColumn(libraryItems, column.name, column.spec)
|
||||
}
|
||||
this.logger.info(`${loggerPrefix} added ${columnNames} columns to ${libraryItems} table`)
|
||||
}
|
||||
|
||||
async removeColumn(table, column) {
|
||||
this.logger.info(`${loggerPrefix} removing column "${column}" from table "${table}"`)
|
||||
const tableDescription = await this.queryInterface.describeTable(table)
|
||||
if (tableDescription[column]) {
|
||||
await this.queryInterface.sequelize.query(`ALTER TABLE ${table} DROP COLUMN ${column}`)
|
||||
this.logger.info(`${loggerPrefix} removed column "${column}" from table "${table}"`)
|
||||
} else {
|
||||
this.logger.info(`${loggerPrefix} column "${column}" does not exist in table "${table}"`)
|
||||
}
|
||||
}
|
||||
|
||||
async removeColumns() {
|
||||
this.logger.info(`${loggerPrefix} removing ${columnNames} columns from ${libraryItems} table`)
|
||||
for (const column of columns) {
|
||||
await this.removeColumn(libraryItems, column.name)
|
||||
}
|
||||
this.logger.info(`${loggerPrefix} removed ${columnNames} columns from ${libraryItems} table`)
|
||||
}
|
||||
|
||||
async populateColumnsFromSource() {
|
||||
this.logger.info(`${loggerPrefix} populating ${columnNames} columns in ${libraryItems} table`)
|
||||
const authorNamesSubQuery = `
|
||||
SELECT ${columnSourcesExpression}
|
||||
FROM ${authorsJoin}
|
||||
WHERE ${bookAuthors}.bookId = ${libraryItems}.mediaId
|
||||
`
|
||||
await this.queryInterface.sequelize.query(`
|
||||
UPDATE ${libraryItems}
|
||||
SET (${columnNames}) = (${authorNamesSubQuery})
|
||||
WHERE mediaType = 'book';
|
||||
`)
|
||||
this.logger.info(`${loggerPrefix} populated ${columnNames} columns in ${libraryItems} table`)
|
||||
}
|
||||
|
||||
async addBookAuthorsTrigger(action) {
|
||||
this.logger.info(`${loggerPrefix} adding trigger to update ${libraryItems} ${columnNames} on ${bookAuthors} ${action}`)
|
||||
const modifiedRecord = action === 'delete' ? 'OLD' : 'NEW'
|
||||
const triggerName = convertToSnakeCase(`update_${libraryItems}_authorNames_on_${bookAuthors}_${action}`)
|
||||
const authorNamesSubQuery = `
|
||||
SELECT ${columnSourcesExpression}
|
||||
FROM ${authorsJoin}
|
||||
WHERE ${bookAuthors}.bookId = ${modifiedRecord}.bookId
|
||||
`
|
||||
await this.queryInterface.sequelize.query(`DROP TRIGGER IF EXISTS ${triggerName}`)
|
||||
|
||||
await this.queryInterface.sequelize.query(`
|
||||
CREATE TRIGGER ${triggerName}
|
||||
AFTER ${action} ON ${bookAuthors}
|
||||
FOR EACH ROW
|
||||
BEGIN
|
||||
UPDATE ${libraryItems}
|
||||
SET (${columnNames}) = (${authorNamesSubQuery})
|
||||
WHERE mediaId = ${modifiedRecord}.bookId;
|
||||
END;
|
||||
`)
|
||||
this.logger.info(`${loggerPrefix} added trigger to update ${libraryItems} ${columnNames} on ${bookAuthors} ${action}`)
|
||||
}
|
||||
|
||||
async addAuthorsUpdateTrigger() {
|
||||
this.logger.info(`${loggerPrefix} adding trigger to update ${libraryItems} ${columnNames} on ${authors} update`)
|
||||
const triggerName = convertToSnakeCase(`update_${libraryItems}_authorNames_on_authors_update`)
|
||||
const authorNamesSubQuery = `
|
||||
SELECT ${columnSourcesExpression}
|
||||
FROM ${authorsJoin}
|
||||
WHERE ${bookAuthors}.bookId = ${libraryItems}.mediaId
|
||||
`
|
||||
|
||||
await this.queryInterface.sequelize.query(`DROP TRIGGER IF EXISTS ${triggerName}`)
|
||||
|
||||
await this.queryInterface.sequelize.query(`
|
||||
CREATE TRIGGER ${triggerName}
|
||||
AFTER UPDATE OF name ON ${authors}
|
||||
FOR EACH ROW
|
||||
BEGIN
|
||||
UPDATE ${libraryItems}
|
||||
SET (${columnNames}) = (${authorNamesSubQuery})
|
||||
WHERE mediaId IN (SELECT bookId FROM ${bookAuthors} WHERE authorId = NEW.id);
|
||||
END;
|
||||
`)
|
||||
this.logger.info(`${loggerPrefix} added trigger to update ${libraryItems} ${columnNames} on ${authors} update`)
|
||||
}
|
||||
|
||||
async addTriggers() {
|
||||
await this.addBookAuthorsTrigger('insert')
|
||||
await this.addBookAuthorsTrigger('delete')
|
||||
await this.addAuthorsUpdateTrigger()
|
||||
}
|
||||
|
||||
async removeBookAuthorsTrigger(action) {
|
||||
this.logger.info(`${loggerPrefix} removing trigger to update ${libraryItems} ${columnNames} on ${bookAuthors} ${action}`)
|
||||
const triggerName = convertToSnakeCase(`update_${libraryItems}_authorNames_on_${bookAuthors}_${action}`)
|
||||
await this.queryInterface.sequelize.query(`DROP TRIGGER IF EXISTS ${triggerName}`)
|
||||
this.logger.info(`${loggerPrefix} removed trigger to update ${libraryItems} ${columnNames} on ${bookAuthors} ${action}`)
|
||||
}
|
||||
|
||||
async removeAuthorsUpdateTrigger() {
|
||||
this.logger.info(`${loggerPrefix} removing trigger to update ${libraryItems} ${columnNames} on ${authors} update`)
|
||||
const triggerName = convertToSnakeCase(`update_${libraryItems}_authorNames_on_authors_update`)
|
||||
await this.queryInterface.sequelize.query(`DROP TRIGGER IF EXISTS ${triggerName}`)
|
||||
this.logger.info(`${loggerPrefix} removed trigger to update ${libraryItems} ${columnNames} on ${authors} update`)
|
||||
}
|
||||
|
||||
async removeTriggers() {
|
||||
await this.removeBookAuthorsTrigger('insert')
|
||||
await this.removeBookAuthorsTrigger('delete')
|
||||
await this.removeAuthorsUpdateTrigger()
|
||||
}
|
||||
|
||||
async addIndex(tableName, columns) {
|
||||
const columnString = columns.map((column) => util.inspect(column)).join(', ')
|
||||
const indexName = convertToSnakeCase(`${tableName}_${columns.map((column) => (typeof column === 'string' ? column : column.name)).join('_')}`)
|
||||
try {
|
||||
this.logger.info(`${loggerPrefix} adding index on [${columnString}] to table ${tableName}. index name: ${indexName}"`)
|
||||
await this.queryInterface.addIndex(tableName, columns)
|
||||
this.logger.info(`${loggerPrefix} added index on [${columnString}] to table ${tableName}. index name: ${indexName}"`)
|
||||
} catch (error) {
|
||||
if (error.name === 'SequelizeDatabaseError' && error.message.includes('already exists')) {
|
||||
this.logger.info(`${loggerPrefix} index [${columnString}] for table "${tableName}" already exists`)
|
||||
} else {
|
||||
throw error
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async addIndexes() {
|
||||
for (const column of columns) {
|
||||
await this.addIndex(libraryItems, ['libraryId', 'mediaType', { name: column.name, collate: 'NOCASE' }])
|
||||
}
|
||||
}
|
||||
|
||||
async removeIndex(tableName, columns) {
|
||||
this.logger.info(`${loggerPrefix} removing index [${columns.join(', ')}] from table "${tableName}"`)
|
||||
await this.queryInterface.removeIndex(tableName, columns)
|
||||
this.logger.info(`${loggerPrefix} removed index [${columns.join(', ')}] from table "${tableName}"`)
|
||||
}
|
||||
|
||||
async removeIndexes() {
|
||||
for (const column of columns) {
|
||||
await this.removeIndex(libraryItems, ['libraryId', 'mediaType', column.name])
|
||||
}
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Utility function to convert a string to snake case, e.g. "titleIgnorePrefix" -> "title_ignore_prefix"
|
||||
*
|
||||
* @param {string} str - the string to convert to snake case.
|
||||
* @returns {string} - the string in snake case.
|
||||
*/
|
||||
function convertToSnakeCase(str) {
|
||||
return str.replace(/([A-Z])/g, '_$1').toLowerCase()
|
||||
}
|
||||
|
||||
module.exports = { up, down }
|
||||
@@ -0,0 +1,163 @@
|
||||
/**
|
||||
* @typedef MigrationContext
|
||||
* @property {import('sequelize').QueryInterface} queryInterface - a suquelize QueryInterface object.
|
||||
* @property {import('../Logger')} logger - a Logger object.
|
||||
*
|
||||
* @typedef MigrationOptions
|
||||
* @property {MigrationContext} context - an object containing the migration context.
|
||||
*/
|
||||
|
||||
const migrationVersion = '2.26.0'
|
||||
const migrationName = `${migrationVersion}-create-auth-tables`
|
||||
const loggerPrefix = `[${migrationVersion} migration]`
|
||||
|
||||
/**
|
||||
* This upward migration creates a sessions table and apiKeys table.
|
||||
*
|
||||
* @param {MigrationOptions} options - an object containing the migration context.
|
||||
* @returns {Promise<void>} - A promise that resolves when the migration is complete.
|
||||
*/
|
||||
async function up({ context: { queryInterface, logger } }) {
|
||||
// Upwards migration script
|
||||
logger.info(`${loggerPrefix} UPGRADE BEGIN: ${migrationName}`)
|
||||
|
||||
// Check if table exists
|
||||
if (await queryInterface.tableExists('sessions')) {
|
||||
logger.info(`${loggerPrefix} table "sessions" already exists`)
|
||||
} else {
|
||||
// Create table
|
||||
logger.info(`${loggerPrefix} creating table "sessions"`)
|
||||
const DataTypes = queryInterface.sequelize.Sequelize.DataTypes
|
||||
await queryInterface.createTable('sessions', {
|
||||
id: {
|
||||
type: DataTypes.UUID,
|
||||
defaultValue: DataTypes.UUIDV4,
|
||||
primaryKey: true
|
||||
},
|
||||
ipAddress: DataTypes.STRING,
|
||||
userAgent: DataTypes.STRING,
|
||||
refreshToken: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false
|
||||
},
|
||||
expiresAt: {
|
||||
type: DataTypes.DATE,
|
||||
allowNull: false
|
||||
},
|
||||
createdAt: {
|
||||
type: DataTypes.DATE,
|
||||
allowNull: false
|
||||
},
|
||||
updatedAt: {
|
||||
type: DataTypes.DATE,
|
||||
allowNull: false
|
||||
},
|
||||
userId: {
|
||||
type: DataTypes.UUID,
|
||||
references: {
|
||||
model: {
|
||||
tableName: 'users'
|
||||
},
|
||||
key: 'id'
|
||||
},
|
||||
allowNull: false,
|
||||
onDelete: 'CASCADE'
|
||||
}
|
||||
})
|
||||
logger.info(`${loggerPrefix} created table "sessions"`)
|
||||
}
|
||||
|
||||
// Check if table exists
|
||||
if (await queryInterface.tableExists('apiKeys')) {
|
||||
logger.info(`${loggerPrefix} table "apiKeys" already exists`)
|
||||
} else {
|
||||
// Create table
|
||||
logger.info(`${loggerPrefix} creating table "apiKeys"`)
|
||||
const DataTypes = queryInterface.sequelize.Sequelize.DataTypes
|
||||
await queryInterface.createTable('apiKeys', {
|
||||
id: {
|
||||
type: DataTypes.UUID,
|
||||
defaultValue: DataTypes.UUIDV4,
|
||||
primaryKey: true
|
||||
},
|
||||
name: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false
|
||||
},
|
||||
description: DataTypes.TEXT,
|
||||
expiresAt: DataTypes.DATE,
|
||||
lastUsedAt: DataTypes.DATE,
|
||||
isActive: {
|
||||
type: DataTypes.BOOLEAN,
|
||||
allowNull: false,
|
||||
defaultValue: false
|
||||
},
|
||||
permissions: DataTypes.JSON,
|
||||
createdAt: {
|
||||
type: DataTypes.DATE,
|
||||
allowNull: false
|
||||
},
|
||||
updatedAt: {
|
||||
type: DataTypes.DATE,
|
||||
allowNull: false
|
||||
},
|
||||
userId: {
|
||||
type: DataTypes.UUID,
|
||||
references: {
|
||||
model: {
|
||||
tableName: 'users'
|
||||
},
|
||||
key: 'id'
|
||||
},
|
||||
onDelete: 'CASCADE'
|
||||
},
|
||||
createdByUserId: {
|
||||
type: DataTypes.UUID,
|
||||
references: {
|
||||
model: {
|
||||
tableName: 'users',
|
||||
as: 'createdByUser'
|
||||
},
|
||||
key: 'id'
|
||||
},
|
||||
onDelete: 'SET NULL'
|
||||
}
|
||||
})
|
||||
logger.info(`${loggerPrefix} created table "apiKeys"`)
|
||||
}
|
||||
|
||||
logger.info(`${loggerPrefix} UPGRADE END: ${migrationName}`)
|
||||
}
|
||||
|
||||
/**
|
||||
* This downward migration script removes the sessions table and apiKeys table.
|
||||
*
|
||||
* @param {MigrationOptions} options - an object containing the migration context.
|
||||
* @returns {Promise<void>} - A promise that resolves when the migration is complete.
|
||||
*/
|
||||
async function down({ context: { queryInterface, logger } }) {
|
||||
// Downward migration script
|
||||
logger.info(`${loggerPrefix} DOWNGRADE BEGIN: ${migrationName}`)
|
||||
|
||||
// Check if table exists
|
||||
if (await queryInterface.tableExists('sessions')) {
|
||||
logger.info(`${loggerPrefix} dropping table "sessions"`)
|
||||
// Drop table
|
||||
await queryInterface.dropTable('sessions')
|
||||
logger.info(`${loggerPrefix} dropped table "sessions"`)
|
||||
} else {
|
||||
logger.info(`${loggerPrefix} table "sessions" does not exist`)
|
||||
}
|
||||
|
||||
if (await queryInterface.tableExists('apiKeys')) {
|
||||
logger.info(`${loggerPrefix} dropping table "apiKeys"`)
|
||||
await queryInterface.dropTable('apiKeys')
|
||||
logger.info(`${loggerPrefix} dropped table "apiKeys"`)
|
||||
} else {
|
||||
logger.info(`${loggerPrefix} table "apiKeys" does not exist`)
|
||||
}
|
||||
|
||||
logger.info(`${loggerPrefix} DOWNGRADE END: ${migrationName}`)
|
||||
}
|
||||
|
||||
module.exports = { up, down }
|
||||
@@ -0,0 +1,74 @@
|
||||
/**
|
||||
* @typedef MigrationContext
|
||||
* @property {import('sequelize').QueryInterface} queryInterface
|
||||
* @property {import('../Logger')} logger
|
||||
*
|
||||
* @typedef MigrationOptions
|
||||
* @property {MigrationContext} context
|
||||
*/
|
||||
|
||||
const migrationVersion = '2.33.0'
|
||||
const migrationName = `${migrationVersion}-add-discover-query-indexes`
|
||||
const loggerPrefix = `[${migrationVersion} migration]`
|
||||
|
||||
const indexes = [
|
||||
{
|
||||
table: 'mediaProgresses',
|
||||
name: 'media_progresses_user_item_finished_time',
|
||||
fields: ['userId', 'mediaItemId', 'isFinished', 'currentTime']
|
||||
},
|
||||
{
|
||||
table: 'bookSeries',
|
||||
name: 'book_series_series_book',
|
||||
fields: ['seriesId', 'bookId']
|
||||
}
|
||||
]
|
||||
|
||||
async function up({ context: { queryInterface, logger } }) {
|
||||
logger.info(`${loggerPrefix} UPGRADE BEGIN: ${migrationName}`)
|
||||
|
||||
for (const index of indexes) {
|
||||
await addIndexIfMissing(queryInterface, logger, index)
|
||||
}
|
||||
|
||||
logger.info(`${loggerPrefix} UPGRADE END: ${migrationName}`)
|
||||
}
|
||||
|
||||
async function down({ context: { queryInterface, logger } }) {
|
||||
logger.info(`${loggerPrefix} DOWNGRADE BEGIN: ${migrationName}`)
|
||||
|
||||
for (const index of indexes) {
|
||||
await removeIndexIfExists(queryInterface, logger, index)
|
||||
}
|
||||
|
||||
logger.info(`${loggerPrefix} DOWNGRADE END: ${migrationName}`)
|
||||
}
|
||||
|
||||
async function addIndexIfMissing(queryInterface, logger, index) {
|
||||
const existing = await queryInterface.showIndex(index.table)
|
||||
if (existing.some((i) => i.name === index.name)) {
|
||||
logger.info(`${loggerPrefix} index ${index.name} already exists on ${index.table}`)
|
||||
return
|
||||
}
|
||||
|
||||
logger.info(`${loggerPrefix} adding index ${index.name} on ${index.table}(${index.fields.join(', ')})`)
|
||||
await queryInterface.addIndex(index.table, {
|
||||
name: index.name,
|
||||
fields: index.fields
|
||||
})
|
||||
logger.info(`${loggerPrefix} added index ${index.name}`)
|
||||
}
|
||||
|
||||
async function removeIndexIfExists(queryInterface, logger, index) {
|
||||
const existing = await queryInterface.showIndex(index.table)
|
||||
if (!existing.some((i) => i.name === index.name)) {
|
||||
logger.info(`${loggerPrefix} index ${index.name} does not exist on ${index.table}`)
|
||||
return
|
||||
}
|
||||
|
||||
logger.info(`${loggerPrefix} removing index ${index.name}`)
|
||||
await queryInterface.removeIndex(index.table, index.name)
|
||||
logger.info(`${loggerPrefix} removed index ${index.name}`)
|
||||
}
|
||||
|
||||
module.exports = { up, down }
|
||||
@@ -0,0 +1,84 @@
|
||||
/**
|
||||
* @typedef MigrationContext
|
||||
* @property {import('sequelize').QueryInterface} queryInterface - a Sequelize QueryInterface object.
|
||||
* @property {import('../Logger')} logger - a Logger object.
|
||||
*
|
||||
* @typedef MigrationOptions
|
||||
* @property {MigrationContext} context - an object containing the migration context.
|
||||
*/
|
||||
|
||||
const migrationVersion = '2.35.0'
|
||||
const migrationName = `${migrationVersion}-add-last-refresh-token`
|
||||
const loggerPrefix = `[${migrationVersion} migration]`
|
||||
|
||||
/**
|
||||
* This migration script adds lastRefreshToken and lastRefreshTokenExpiresAt columns to the sessions table.
|
||||
*
|
||||
* @param {MigrationOptions} options - an object containing the migration context.
|
||||
* @returns {Promise<void>} - A promise that resolves when the migration is complete.
|
||||
*/
|
||||
async function up({ context: { queryInterface, logger } }) {
|
||||
logger.info(`${loggerPrefix} UPGRADE BEGIN: ${migrationName}`)
|
||||
|
||||
if (await queryInterface.tableExists('sessions')) {
|
||||
const tableDescription = await queryInterface.describeTable('sessions')
|
||||
|
||||
if (!tableDescription.lastRefreshToken) {
|
||||
logger.info(`${loggerPrefix} Adding lastRefreshToken column to sessions table`)
|
||||
await queryInterface.addColumn('sessions', 'lastRefreshToken', {
|
||||
type: queryInterface.sequelize.Sequelize.DataTypes.STRING,
|
||||
allowNull: true
|
||||
})
|
||||
} else {
|
||||
logger.info(`${loggerPrefix} lastRefreshToken column already exists in sessions table`)
|
||||
}
|
||||
|
||||
if (!tableDescription.lastRefreshTokenExpiresAt) {
|
||||
logger.info(`${loggerPrefix} Adding lastRefreshTokenExpiresAt column to sessions table`)
|
||||
await queryInterface.addColumn('sessions', 'lastRefreshTokenExpiresAt', {
|
||||
type: queryInterface.sequelize.Sequelize.DataTypes.DATE,
|
||||
allowNull: true
|
||||
})
|
||||
} else {
|
||||
logger.info(`${loggerPrefix} lastRefreshTokenExpiresAt column already exists in sessions table`)
|
||||
}
|
||||
} else {
|
||||
logger.info(`${loggerPrefix} sessions table does not exist`)
|
||||
}
|
||||
|
||||
logger.info(`${loggerPrefix} UPGRADE END: ${migrationName}`)
|
||||
}
|
||||
|
||||
/**
|
||||
* This migration script removes the lastRefreshToken and lastRefreshTokenExpiresAt columns from the sessions table.
|
||||
*
|
||||
* @param {MigrationOptions} options - an object containing the migration context.
|
||||
* @returns {Promise<void>} - A promise that resolves when the migration is complete.
|
||||
*/
|
||||
async function down({ context: { queryInterface, logger } }) {
|
||||
logger.info(`${loggerPrefix} DOWNGRADE BEGIN: ${migrationName}`)
|
||||
|
||||
if (await queryInterface.tableExists('sessions')) {
|
||||
const tableDescription = await queryInterface.describeTable('sessions')
|
||||
|
||||
if (tableDescription.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) {
|
||||
logger.info(`${loggerPrefix} Removing lastRefreshTokenExpiresAt column from sessions table`)
|
||||
await queryInterface.removeColumn('sessions', 'lastRefreshTokenExpiresAt')
|
||||
} else {
|
||||
logger.info(`${loggerPrefix} lastRefreshTokenExpiresAt column does not exist in sessions table`)
|
||||
}
|
||||
} else {
|
||||
logger.info(`${loggerPrefix} sessions table does not exist`)
|
||||
}
|
||||
|
||||
logger.info(`${loggerPrefix} DOWNGRADE END: ${migrationName}`)
|
||||
}
|
||||
|
||||
module.exports = { up, down }
|
||||
Reference in New Issue
Block a user