From 2711b989e153669aaf869521ebbc0e9f006834ef Mon Sep 17 00:00:00 2001 From: Nicholas Wallace Date: Fri, 13 Sep 2024 16:55:48 -0700 Subject: [PATCH 01/45] Add: series migration to be unique --- .../v2.13.5-series-column-unique.js | 126 ++++++++++ server/models/Series.js | 6 + .../v2.13.5-series-column-unique.test.js | 226 ++++++++++++++++++ 3 files changed, 358 insertions(+) create mode 100644 server/migrations/v2.13.5-series-column-unique.js create mode 100644 test/server/migrations/v2.13.5-series-column-unique.test.js diff --git a/server/migrations/v2.13.5-series-column-unique.js b/server/migrations/v2.13.5-series-column-unique.js new file mode 100644 index 000000000..e7201bae1 --- /dev/null +++ b/server/migrations/v2.13.5-series-column-unique.js @@ -0,0 +1,126 @@ +const Logger = require('../Logger') + +/** + * @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} - A promise that resolves when the migration is complete. + */ +async function up({ context: { queryInterface, logger } }) { + // Upwards migration script + logger.info('UPGRADE BEGIN: 2.13.5-series-column-unique ') + + // Use the queryInterface to get the series table and find duplicates in the `name` column + const [duplicates] = await queryInterface.sequelize.query(` + SELECT name, libraryId, MAX(updatedAt) AS latestUpdatedAt, COUNT(name) AS count + FROM Series + GROUP BY name, libraryId + HAVING COUNT(name) > 1 + `) + + // Print out how many duplicates were found + logger.info(`[2.13.5 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.13.5 migration] Deduplicating series "${duplicate.name}" in 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.13.5 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('Added unique index on Series.name and Series.libraryId') + + logger.info('UPGRADE END: 2.13.5-series-column-unique ') +} + +/** + * This removes the unique index on the `Series` table. + * + * @param {MigrationOptions} options - an object containing the migration context. + * @returns {Promise} - A promise that resolves when the migration is complete. + */ +async function down({ context: { queryInterface, logger } }) { + // Downward migration script + logger.info('DOWNGRADE BEGIN: 2.13.5-series-column-unique ') + + // Remove the unique index + await queryInterface.removeIndex('Series', 'unique_series_name_per_library') + logger.info('Removed unique index on Series.name and Series.libraryId') + + logger.info('DOWNGRADE END: 2.13.5-series-column-unique ') +} + +module.exports = { up, down } diff --git a/server/models/Series.js b/server/models/Series.js index c57a1a116..731908e9c 100644 --- a/server/models/Series.js +++ b/server/models/Series.js @@ -83,6 +83,12 @@ class Series extends Model { // collate: 'NOCASE' // }] // }, + { + // unique constraint on name and libraryId + fields: ['name', 'libraryId'], + unique: true, + name: 'unique_series_name_per_library' + }, { fields: ['libraryId'] } diff --git a/test/server/migrations/v2.13.5-series-column-unique.test.js b/test/server/migrations/v2.13.5-series-column-unique.test.js new file mode 100644 index 000000000..6d55b6293 --- /dev/null +++ b/test/server/migrations/v2.13.5-series-column-unique.test.js @@ -0,0 +1,226 @@ +const { expect } = require('chai') +const sinon = require('sinon') +const { up, down } = require('../../../server/migrations/v2.13.5-series-column-unique') +const { Sequelize } = require('sequelize') +const Logger = require('../../../server/Logger') +const { query } = require('express') +const { logger } = require('sequelize/lib/utils/logger') +const e = require('express') + +describe('migration_example', () => { + let sequelize + let queryInterface + let loggerInfoStub + let series1Id + let series2Id + let series3Id + let series1Id_dup + let series3Id_dup + let book1Id + let book2Id + let book3Id + let book4Id + let book5Id + let library1Id + let library2Id + let bookSeries1Id + let bookSeries2Id + let bookSeries3Id + let bookSeries1Id_dup + let bookSeries3Id_dup + + 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('Series', { + id: { type: Sequelize.UUID, primaryKey: true }, + name: { type: Sequelize.STRING, allowNull: false }, + libraryId: { type: Sequelize.UUID, allowNull: false }, + createdAt: { type: Sequelize.DATE, allowNull: false }, + updatedAt: { type: Sequelize.DATE, allowNull: false } + }) + await queryInterface.createTable('BookSeries', { + id: { type: Sequelize.UUID, primaryKey: true }, + bookId: { type: Sequelize.UUID, allowNull: false }, + seriesId: { type: Sequelize.UUID, allowNull: false } + }) + // Set UUIDs for the tests + series1Id = 'fc086255-3fd2-4a95-8a28-840d9206501b' + series2Id = '70f46ac2-ee48-4b3c-9822-933cc15c29bd' + series3Id = '01cac008-142b-4e15-b0ff-cf7cc2c5b64e' + series1Id_dup = 'ad0b3b3b-4b3b-4b3b-4b3b-4b3b4b3b4b3b' + series3Id_dup = '4b3b4b3b-4b3b-4b3b-4b3b-4b3b4b3b4b3b' + series1Id_dup2 = '0123456a-4b3b-4b3b-4b3b-4b3b4b3b4b3b' + book1Id = '4a38b6e5-0ae4-4de4-b119-4e33891bd63f' + book2Id = '8bc2e61d-47f6-42ef-a3f4-93cf2f1de82f' + book3Id = 'ec9bbaaf-1e55-457f-b59c-bd2bd955a404' + book4Id = '876f3b3b-4b3b-4b3b-4b3b-4b3b4b3b4b3b' + book5Id = '4e5b4b3b-4b3b-4b3b-4b3b-4b3b4b3b4b3b' + book6Id = 'abcda123-4b3b-4b3b-4b3b-4b3b4b3b4b3b' + library1Id = '3a5a1c7c-a914-472e-88b0-b871ceae63e7' + library2Id = 'fd6c324a-4f3a-4bb0-99d6-7a330e765e7e' + bookSeries1Id = 'eca24687-2241-4ffa-a9b3-02a0ba03c763' + bookSeries2Id = '56f56105-813b-4395-9689-fd04198e7d5d' + bookSeries3Id = '404a1761-c710-4d86-9d78-68d9a9c0fb6b' + bookSeries1Id_dup = '8bea3b3b-4b3b-4b3b-4b3b-4b3b4b3b4b3b' + bookSeries3Id_dup = '89656a3b-4b3b-4b3b-4b3b-4b3b4b3b4b3b' + bookSeries1Id_dup2 = '9bea3b3b-4b3b-4b3b-4b3b-4b3b4b3b4b3b' + }) + afterEach(async () => { + await queryInterface.dropTable('Series') + await queryInterface.dropTable('BookSeries') + }) + it('upgrade with no duplicate series', async () => { + // Add some entries to the Series table using the UUID for the ids + await queryInterface.bulkInsert('Series', [ + { id: series1Id, name: 'Series 1', libraryId: library1Id, createdAt: new Date(), updatedAt: new Date() }, + { id: series2Id, name: 'Series 2', libraryId: library2Id, createdAt: new Date(), updatedAt: new Date() }, + { id: series3Id, name: 'Series 3', libraryId: library1Id, createdAt: new Date(), updatedAt: new Date() } + ]) + // Add some entries to the BookSeries table + await queryInterface.bulkInsert('BookSeries', [ + { id: bookSeries1Id, bookId: book1Id, seriesId: series1Id }, + { id: bookSeries2Id, bookId: book2Id, seriesId: series2Id }, + { id: bookSeries3Id, bookId: book3Id, seriesId: series3Id } + ]) + + await up({ context: { queryInterface, logger: Logger } }) + + expect(loggerInfoStub.callCount).to.equal(5) + expect(loggerInfoStub.getCall(0).calledWith(sinon.match('UPGRADE BEGIN: 2.13.5-series-column-unique '))).to.be.true + expect(loggerInfoStub.getCall(1).calledWith(sinon.match('[2.13.5 migration] Found 0 duplicate series'))).to.be.true + expect(loggerInfoStub.getCall(2).calledWith(sinon.match('[2.13.5 migration] Deduplication complete'))).to.be.true + expect(loggerInfoStub.getCall(3).calledWith(sinon.match('Added unique index on Series.name and Series.libraryId'))).to.be.true + expect(loggerInfoStub.getCall(4).calledWith(sinon.match('UPGRADE END: 2.13.5-series-column-unique '))).to.be.true + // Validate rows in tables + const series = await queryInterface.sequelize.query('SELECT "id", "name", "libraryId" FROM Series', { type: queryInterface.sequelize.QueryTypes.SELECT }) + expect(series).to.have.length(3) + expect(series).to.deep.include({ id: series1Id, name: 'Series 1', libraryId: library1Id }) + expect(series).to.deep.include({ id: series2Id, name: 'Series 2', libraryId: library2Id }) + expect(series).to.deep.include({ id: series3Id, name: 'Series 3', libraryId: library1Id }) + const bookSeries = await queryInterface.sequelize.query('SELECT "id", "bookId", "seriesId" FROM BookSeries', { type: queryInterface.sequelize.QueryTypes.SELECT }) + expect(bookSeries).to.have.length(3) + expect(bookSeries).to.deep.include({ id: bookSeries1Id, bookId: book1Id, seriesId: series1Id }) + expect(bookSeries).to.deep.include({ id: bookSeries2Id, bookId: book2Id, seriesId: series2Id }) + expect(bookSeries).to.deep.include({ id: bookSeries3Id, bookId: book3Id, seriesId: series3Id }) + }) + it('upgrade with duplicate series', async () => { + // Add some entries to the Series table using the UUID for the ids + await queryInterface.bulkInsert('Series', [ + { id: series1Id, name: 'Series 1', libraryId: library1Id, createdAt: new Date(), updatedAt: new Date() }, + { id: series2Id, name: 'Series 2', libraryId: library2Id, createdAt: new Date(), updatedAt: new Date() }, + { id: series3Id, name: 'Series 3', libraryId: library1Id, createdAt: new Date(), updatedAt: new Date() }, + { id: series1Id_dup, name: 'Series 1', libraryId: library1Id, createdAt: new Date(), updatedAt: new Date() }, + { id: series3Id_dup, name: 'Series 3', libraryId: library1Id, createdAt: new Date(), updatedAt: new Date() }, + { id: series1Id_dup2, name: 'Series 1', libraryId: library1Id, createdAt: new Date(), updatedAt: new Date() } + ]) + // Add some entries to the BookSeries table + await queryInterface.bulkInsert('BookSeries', [ + { id: bookSeries1Id, bookId: book1Id, seriesId: series1Id }, + { id: bookSeries2Id, bookId: book2Id, seriesId: series2Id }, + { id: bookSeries3Id, bookId: book3Id, seriesId: series3Id }, + { id: bookSeries1Id_dup, bookId: book4Id, seriesId: series1Id_dup }, + { id: bookSeries3Id_dup, bookId: book5Id, seriesId: series3Id_dup }, + { id: bookSeries1Id_dup2, bookId: book6Id, seriesId: series1Id_dup2 } + ]) + + await up({ context: { queryInterface, logger: Logger } }) + + expect(loggerInfoStub.callCount).to.equal(7) + expect(loggerInfoStub.getCall(0).calledWith(sinon.match('UPGRADE BEGIN: 2.13.5-series-column-unique '))).to.be.true + expect(loggerInfoStub.getCall(1).calledWith(sinon.match('[2.13.5 migration] Found 2 duplicate series'))).to.be.true + expect(loggerInfoStub.getCall(2).calledWith(sinon.match('[2.13.5 migration] Deduplicating series "Series 1" in library 3a5a1c7c-a914-472e-88b0-b871ceae63e7'))).to.be.true + expect(loggerInfoStub.getCall(3).calledWith(sinon.match('[2.13.5 migration] Deduplicating series "Series 3" in library 3a5a1c7c-a914-472e-88b0-b871ceae63e7'))).to.be.true + expect(loggerInfoStub.getCall(4).calledWith(sinon.match('[2.13.5 migration] Deduplication complete'))).to.be.true + expect(loggerInfoStub.getCall(5).calledWith(sinon.match('Added unique index on Series.name and Series.libraryId'))).to.be.true + expect(loggerInfoStub.getCall(6).calledWith(sinon.match('UPGRADE END: 2.13.5-series-column-unique '))).to.be.true + // Validate rows + const series = await queryInterface.sequelize.query('SELECT "id", "name", "libraryId" FROM Series', { type: queryInterface.sequelize.QueryTypes.SELECT }) + expect(series).to.have.length(3) + expect(series).to.deep.include({ id: series1Id, name: 'Series 1', libraryId: library1Id }) + expect(series).to.deep.include({ id: series2Id, name: 'Series 2', libraryId: library2Id }) + expect(series).to.deep.include({ id: series3Id, name: 'Series 3', libraryId: library1Id }) + const bookSeries = await queryInterface.sequelize.query('SELECT "id", "bookId", "seriesId" FROM BookSeries', { type: queryInterface.sequelize.QueryTypes.SELECT }) + expect(bookSeries).to.have.length(6) + expect(bookSeries).to.deep.include({ id: bookSeries1Id, bookId: book1Id, seriesId: series1Id }) + expect(bookSeries).to.deep.include({ id: bookSeries2Id, bookId: book2Id, seriesId: series2Id }) + expect(bookSeries).to.deep.include({ id: bookSeries3Id, bookId: book3Id, seriesId: series3Id }) + expect(bookSeries).to.deep.include({ id: bookSeries1Id_dup, bookId: book4Id, seriesId: series1Id }) + expect(bookSeries).to.deep.include({ id: bookSeries3Id_dup, bookId: book5Id, seriesId: series3Id }) + expect(bookSeries).to.deep.include({ id: bookSeries1Id_dup2, bookId: book6Id, seriesId: series1Id }) + }) + it('update with same series name in different libraries', async () => { + // Add some entries to the Series table using the UUID for the ids + await queryInterface.bulkInsert('Series', [ + { id: series1Id, name: 'Series 1', libraryId: library1Id, createdAt: new Date(), updatedAt: new Date() }, + { id: series2Id, name: 'Series 1', libraryId: library2Id, createdAt: new Date(), updatedAt: new Date() } + ]) + // Add some entries to the BookSeries table + await queryInterface.bulkInsert('BookSeries', [ + { id: bookSeries1Id, bookId: book1Id, seriesId: series1Id }, + { id: bookSeries2Id, bookId: book2Id, seriesId: series2Id } + ]) + + await up({ context: { queryInterface, logger: Logger } }) + + expect(loggerInfoStub.callCount).to.equal(5) + expect(loggerInfoStub.getCall(0).calledWith(sinon.match('UPGRADE BEGIN: 2.13.5-series-column-unique '))).to.be.true + expect(loggerInfoStub.getCall(1).calledWith(sinon.match('[2.13.5 migration] Found 0 duplicate series'))).to.be.true + expect(loggerInfoStub.getCall(2).calledWith(sinon.match('[2.13.5 migration] Deduplication complete'))).to.be.true + expect(loggerInfoStub.getCall(3).calledWith(sinon.match('Added unique index on Series.name and Series.libraryId'))).to.be.true + expect(loggerInfoStub.getCall(4).calledWith(sinon.match('UPGRADE END: 2.13.5-series-column-unique '))).to.be.true + // Validate rows + const series = await queryInterface.sequelize.query('SELECT "id", "name", "libraryId" FROM Series', { type: queryInterface.sequelize.QueryTypes.SELECT }) + expect(series).to.have.length(2) + expect(series).to.deep.include({ id: series1Id, name: 'Series 1', libraryId: library1Id }) + expect(series).to.deep.include({ id: series2Id, name: 'Series 1', libraryId: library2Id }) + const bookSeries = await queryInterface.sequelize.query('SELECT "id", "bookId", "seriesId" FROM BookSeries', { type: queryInterface.sequelize.QueryTypes.SELECT }) + expect(bookSeries).to.have.length(2) + expect(bookSeries).to.deep.include({ id: bookSeries1Id, bookId: book1Id, seriesId: series1Id }) + expect(bookSeries).to.deep.include({ id: bookSeries2Id, bookId: book2Id, seriesId: series2Id }) + }) + }) + + describe('down', () => { + beforeEach(async () => { + await queryInterface.createTable('Series', { + id: { type: Sequelize.UUID, primaryKey: true }, + name: { type: Sequelize.STRING, allowNull: false }, + libraryId: { type: Sequelize.UUID, allowNull: false }, + createdAt: { type: Sequelize.DATE, allowNull: false }, + updatedAt: { type: Sequelize.DATE, allowNull: false } + }) + await queryInterface.createTable('BookSeries', { + id: { type: Sequelize.UUID, primaryKey: true }, + bookId: { type: Sequelize.UUID, allowNull: false }, + seriesId: { type: Sequelize.UUID, allowNull: false } + }) + }) + it('should not have unique constraint on series name and libraryId', async () => { + await up({ context: { queryInterface, logger: Logger } }) + await down({ context: { queryInterface, logger: Logger } }) + + expect(loggerInfoStub.callCount).to.equal(8) + expect(loggerInfoStub.getCall(0).calledWith(sinon.match('UPGRADE BEGIN: 2.13.5-series-column-unique '))).to.be.true + expect(loggerInfoStub.getCall(1).calledWith(sinon.match('[2.13.5 migration] Found 0 duplicate series'))).to.be.true + expect(loggerInfoStub.getCall(2).calledWith(sinon.match('[2.13.5 migration] Deduplication complete'))).to.be.true + expect(loggerInfoStub.getCall(3).calledWith(sinon.match('Added unique index on Series.name and Series.libraryId'))).to.be.true + expect(loggerInfoStub.getCall(4).calledWith(sinon.match('UPGRADE END: 2.13.5-series-column-unique '))).to.be.true + expect(loggerInfoStub.getCall(5).calledWith(sinon.match('DOWNGRADE BEGIN: 2.13.5-series-column-unique '))).to.be.true + expect(loggerInfoStub.getCall(6).calledWith(sinon.match('Removed unique index on Series.name and Series.libraryId'))).to.be.true + expect(loggerInfoStub.getCall(7).calledWith(sinon.match('DOWNGRADE END: 2.13.5-series-column-unique '))).to.be.true + // Ensure index does not exist + const indexes = await queryInterface.showIndex('Series') + expect(indexes).to.not.deep.include({ tableName: 'Series', unique: true, fields: ['name', 'libraryId'], name: 'unique_series_name_per_library' }) + }) + }) +}) From c163f84aec65da8de7d31390e100d83dff86312a Mon Sep 17 00:00:00 2001 From: Nicholas Wallace Date: Fri, 13 Sep 2024 17:01:48 -0700 Subject: [PATCH 02/45] Update migration changelog for series name unique --- server/migrations/changelog.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/server/migrations/changelog.md b/server/migrations/changelog.md index 2e3c295af..bac3ec25e 100644 --- a/server/migrations/changelog.md +++ b/server/migrations/changelog.md @@ -2,6 +2,6 @@ Please add a record of every database migration that you create to this file. This will help us keep track of changes to the database schema over time. -| Server Version | Migration Script Name | Description | -| -------------- | --------------------- | ----------- | -| | | | +| Server Version | Migration Script Name | Description | +| -------------- | ---------------------------- | ------------------------------------------------- | +| v2.13.5 | v2.13.5-series-column-unique | Series must have unique names in the same library | From 8ae62da1389cd4fe87ba4327e9d76a66237b087a Mon Sep 17 00:00:00 2001 From: advplyr Date: Sat, 14 Sep 2024 10:40:01 -0500 Subject: [PATCH 03/45] Update migration unit test name --- test/server/migrations/v2.13.5-series-column-unique.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/server/migrations/v2.13.5-series-column-unique.test.js b/test/server/migrations/v2.13.5-series-column-unique.test.js index 6d55b6293..ed950a01f 100644 --- a/test/server/migrations/v2.13.5-series-column-unique.test.js +++ b/test/server/migrations/v2.13.5-series-column-unique.test.js @@ -7,7 +7,7 @@ const { query } = require('express') const { logger } = require('sequelize/lib/utils/logger') const e = require('express') -describe('migration_example', () => { +describe('migration-v2.13.5-series-column-unique', () => { let sequelize let queryInterface let loggerInfoStub From 868659a2f1260d350694ffeda0d59c58d2aa2769 Mon Sep 17 00:00:00 2001 From: Nicholas Wallace Date: Sat, 14 Sep 2024 11:44:19 -0700 Subject: [PATCH 04/45] Add: unique constraint on bookseries table --- .../v2.13.5-series-column-unique.test.js | 30 ++++++++++++------- 1 file changed, 20 insertions(+), 10 deletions(-) diff --git a/test/server/migrations/v2.13.5-series-column-unique.test.js b/test/server/migrations/v2.13.5-series-column-unique.test.js index ed950a01f..d98fb4f79 100644 --- a/test/server/migrations/v2.13.5-series-column-unique.test.js +++ b/test/server/migrations/v2.13.5-series-column-unique.test.js @@ -48,11 +48,16 @@ describe('migration-v2.13.5-series-column-unique', () => { createdAt: { type: Sequelize.DATE, allowNull: false }, updatedAt: { type: Sequelize.DATE, allowNull: false } }) - await queryInterface.createTable('BookSeries', { - id: { type: Sequelize.UUID, primaryKey: true }, - bookId: { type: Sequelize.UUID, allowNull: false }, - seriesId: { type: Sequelize.UUID, allowNull: false } - }) + // Create a table for BookSeries, with a unique constraint of bookId and seriesId + await queryInterface.createTable( + 'BookSeries', + { + id: { type: Sequelize.UUID, primaryKey: true }, + bookId: { type: Sequelize.UUID, allowNull: false }, + seriesId: { type: Sequelize.UUID, allowNull: false } + }, + { uniqueKeys: { book_series_unique: { fields: ['bookId', 'seriesId'] } } } + ) // Set UUIDs for the tests series1Id = 'fc086255-3fd2-4a95-8a28-840d9206501b' series2Id = '70f46ac2-ee48-4b3c-9822-933cc15c29bd' @@ -199,11 +204,16 @@ describe('migration-v2.13.5-series-column-unique', () => { createdAt: { type: Sequelize.DATE, allowNull: false }, updatedAt: { type: Sequelize.DATE, allowNull: false } }) - await queryInterface.createTable('BookSeries', { - id: { type: Sequelize.UUID, primaryKey: true }, - bookId: { type: Sequelize.UUID, allowNull: false }, - seriesId: { type: Sequelize.UUID, allowNull: false } - }) + // Create a table for BookSeries, with a unique constraint of bookId and seriesId + await queryInterface.createTable( + 'BookSeries', + { + id: { type: Sequelize.UUID, primaryKey: true }, + bookId: { type: Sequelize.UUID, allowNull: false }, + seriesId: { type: Sequelize.UUID, allowNull: false } + }, + { uniqueKeys: { book_series_unique: { fields: ['bookId', 'seriesId'] } } } + ) }) it('should not have unique constraint on series name and libraryId', async () => { await up({ context: { queryInterface, logger: Logger } }) From fa451f362b0a83390bbe9dbc3b30abea0af21d8d Mon Sep 17 00:00:00 2001 From: Nicholas Wallace Date: Sat, 14 Sep 2024 12:11:31 -0700 Subject: [PATCH 05/45] Add: tests for one book in duplicate series --- .../v2.13.5-series-column-unique.test.js | 102 ++++++++++++++++-- 1 file changed, 95 insertions(+), 7 deletions(-) diff --git a/test/server/migrations/v2.13.5-series-column-unique.test.js b/test/server/migrations/v2.13.5-series-column-unique.test.js index d98fb4f79..19dc4a8e4 100644 --- a/test/server/migrations/v2.13.5-series-column-unique.test.js +++ b/test/server/migrations/v2.13.5-series-column-unique.test.js @@ -53,6 +53,7 @@ describe('migration-v2.13.5-series-column-unique', () => { 'BookSeries', { id: { type: Sequelize.UUID, primaryKey: true }, + sequence: { type: Sequelize.STRING, allowNull: true }, bookId: { type: Sequelize.UUID, allowNull: false }, seriesId: { type: Sequelize.UUID, allowNull: false } }, @@ -93,9 +94,9 @@ describe('migration-v2.13.5-series-column-unique', () => { ]) // Add some entries to the BookSeries table await queryInterface.bulkInsert('BookSeries', [ - { id: bookSeries1Id, bookId: book1Id, seriesId: series1Id }, + { id: bookSeries1Id, sequence: '1', bookId: book1Id, seriesId: series1Id }, { id: bookSeries2Id, bookId: book2Id, seriesId: series2Id }, - { id: bookSeries3Id, bookId: book3Id, seriesId: series3Id } + { id: bookSeries3Id, sequence: '1', bookId: book3Id, seriesId: series3Id } ]) await up({ context: { queryInterface, logger: Logger } }) @@ -112,13 +113,13 @@ describe('migration-v2.13.5-series-column-unique', () => { expect(series).to.deep.include({ id: series1Id, name: 'Series 1', libraryId: library1Id }) expect(series).to.deep.include({ id: series2Id, name: 'Series 2', libraryId: library2Id }) expect(series).to.deep.include({ id: series3Id, name: 'Series 3', libraryId: library1Id }) - const bookSeries = await queryInterface.sequelize.query('SELECT "id", "bookId", "seriesId" FROM BookSeries', { type: queryInterface.sequelize.QueryTypes.SELECT }) + const bookSeries = await queryInterface.sequelize.query('SELECT "id", "sequence", "bookId", "seriesId" FROM BookSeries', { type: queryInterface.sequelize.QueryTypes.SELECT }) expect(bookSeries).to.have.length(3) - expect(bookSeries).to.deep.include({ id: bookSeries1Id, bookId: book1Id, seriesId: series1Id }) - expect(bookSeries).to.deep.include({ id: bookSeries2Id, bookId: book2Id, seriesId: series2Id }) - expect(bookSeries).to.deep.include({ id: bookSeries3Id, bookId: book3Id, seriesId: series3Id }) + expect(bookSeries).to.deep.include({ id: bookSeries1Id, sequence: '1', bookId: book1Id, seriesId: series1Id }) + expect(bookSeries).to.deep.include({ id: bookSeries2Id, sequence: null, bookId: book2Id, seriesId: series2Id }) + expect(bookSeries).to.deep.include({ id: bookSeries3Id, sequence: '1', bookId: book3Id, seriesId: series3Id }) }) - it('upgrade with duplicate series', async () => { + it('upgrade with duplicate series and no sequence', async () => { // Add some entries to the Series table using the UUID for the ids await queryInterface.bulkInsert('Series', [ { id: series1Id, name: 'Series 1', libraryId: library1Id, createdAt: new Date(), updatedAt: new Date() }, @@ -193,6 +194,93 @@ describe('migration-v2.13.5-series-column-unique', () => { expect(bookSeries).to.deep.include({ id: bookSeries1Id, bookId: book1Id, seriesId: series1Id }) expect(bookSeries).to.deep.include({ id: bookSeries2Id, bookId: book2Id, seriesId: series2Id }) }) + it('upgrade with one book in two of the same series, both sequence are null', async () => { + // Create two different series with the same name in the same library + await queryInterface.bulkInsert('Series', [ + { id: series1Id, name: 'Series 1', libraryId: library1Id, createdAt: new Date(), updatedAt: new Date() }, + { id: series2Id, name: 'Series 1', libraryId: library1Id, createdAt: new Date(), updatedAt: new Date() } + ]) + // Create a book that is in both series + await queryInterface.bulkInsert('BookSeries', [ + { id: bookSeries1Id, bookId: book1Id, seriesId: series1Id }, + { id: bookSeries2Id, bookId: book1Id, seriesId: series2Id } + ]) + + await up({ context: { queryInterface, logger: Logger } }) + + expect(loggerInfoStub.callCount).to.equal(6) + expect(loggerInfoStub.getCall(0).calledWith(sinon.match('UPGRADE BEGIN: 2.13.5-series-column-unique '))).to.be.true + expect(loggerInfoStub.getCall(1).calledWith(sinon.match('[2.13.5 migration] Found 1 duplicate series'))).to.be.true + expect(loggerInfoStub.getCall(2).calledWith(sinon.match('[2.13.5 migration] Deduplicating series "Series 1" in library 3a5a1c7c-a914-472e-88b0-b871ceae63e7'))).to.be.true + expect(loggerInfoStub.getCall(3).calledWith(sinon.match('[2.13.5 migration] Deduplication complete'))).to.be.true + expect(loggerInfoStub.getCall(4).calledWith(sinon.match('Added unique index on Series.name and Series.libraryId')).to.be.true) + expect(loggerInfoStub.getCall(5).calledWith(sinon.match('UPGRADE END: 2.13.5-series-column-unique '))).to.be.true + // validate rows + const series = await queryInterface.sequelize.query('SELECT "id", "name", "libraryId" FROM Series', { type: queryInterface.sequelize.QueryTypes.SELECT }) + expect(series).to.have.length(1) + expect(series).to.deep.include({ id: series1Id, name: 'Series 1', libraryId: library1Id }) + const bookSeries = await queryInterface.sequelize.query('SELECT "id", "sequence", "bookId", "seriesId" FROM BookSeries', { type: queryInterface.sequelize.QueryTypes.SELECT }) + expect(bookSeries).to.have.length(1) + expect(bookSeries).to.deep.include({ id: bookSeries1Id, sequence: null, bookId: book1Id, seriesId: series1Id }) + }) + it('upgrade with one book in two of the same series, one sequence is null', async () => { + // Create two different series with the same name in the same library + await queryInterface.bulkInsert('Series', [ + { id: series1Id, name: 'Series 1', libraryId: library1Id, createdAt: new Date(), updatedAt: new Date() }, + { id: series2Id, name: 'Series 1', libraryId: library1Id, createdAt: new Date(), updatedAt: new Date() } + ]) + // Create a book that is in both series + await queryInterface.bulkInsert('BookSeries', [ + { id: bookSeries1Id, sequence: '1', bookId: book1Id, seriesId: series1Id }, + { id: bookSeries2Id, bookId: book1Id, seriesId: series2Id } + ]) + + await up({ context: { queryInterface, logger: Logger } }) + + expect(loggerInfoStub.callCount).to.equal(6) + expect(loggerInfoStub.getCall(0).calledWith(sinon.match('UPGRADE BEGIN: 2.13.5-series-column-unique '))).to.be.true + expect(loggerInfoStub.getCall(1).calledWith(sinon.match('[2.13.5 migration] Found 1 duplicate series'))).to.be.true + expect(loggerInfoStub.getCall(2).calledWith(sinon.match('[2.13.5 migration] Deduplicating series "Series 1" in library 3a5a1c7c-a914-472e-88b0-b871ceae63e7'))).to.be.true + expect(loggerInfoStub.getCall(3).calledWith(sinon.match('[2.13.5 migration] Deduplication complete'))).to.be.true + expect(loggerInfoStub.getCall(4).calledWith(sinon.match('Added unique index on Series.name and Series.libraryId')).to.be.true) + expect(loggerInfoStub.getCall(5).calledWith(sinon.match('UPGRADE END: 2.13.5-series-column-unique '))).to.be.true + // validate rows + const series = await queryInterface.sequelize.query('SELECT "id", "name", "libraryId" FROM Series', { type: queryInterface.sequelize.QueryTypes.SELECT }) + expect(series).to.have.length(1) + expect(series).to.deep.include({ id: series1Id, name: 'Series 1', libraryId: library1Id }) + const bookSeries = await queryInterface.sequelize.query('SELECT "id", "sequence", "bookId", "seriesId" FROM BookSeries', { type: queryInterface.sequelize.QueryTypes.SELECT }) + expect(bookSeries).to.have.length(1) + expect(bookSeries).to.deep.include({ id: bookSeries1Id, sequence: '1', bookId: book1Id, seriesId: series1Id }) + }) + it('upgrade with one book in two of the same series, both sequence are not null', async () => { + // Create two different series with the same name in the same library + await queryInterface.bulkInsert('Series', [ + { id: series1Id, name: 'Series 1', libraryId: library1Id, createdAt: new Date(), updatedAt: new Date() }, + { id: series2Id, name: 'Series 1', libraryId: library1Id, createdAt: new Date(), updatedAt: new Date() } + ]) + // Create a book that is in both series + await queryInterface.bulkInsert('BookSeries', [ + { id: bookSeries1Id, sequence: '1', bookId: book1Id, seriesId: series1Id }, + { id: bookSeries2Id, sequence: '2', bookId: book1Id, seriesId: series2Id } + ]) + + await up({ context: { queryInterface, logger: Logger } }) + + expect(loggerInfoStub.callCount).to.equal(6) + expect(loggerInfoStub.getCall(0).calledWith(sinon.match('UPGRADE BEGIN: 2.13.5-series-column-unique '))).to.be.true + expect(loggerInfoStub.getCall(1).calledWith(sinon.match('[2.13.5 migration] Found 1 duplicate series'))).to.be.true + expect(loggerInfoStub.getCall(2).calledWith(sinon.match('[2.13.5 migration] Deduplicating series "Series 1" in library 3a5a1c7c-a914-472e-88b0-b871ceae63e7'))).to.be.true + expect(loggerInfoStub.getCall(3).calledWith(sinon.match('[2.13.5 migration] Deduplication complete'))).to.be.true + expect(loggerInfoStub.getCall(4).calledWith(sinon.match('Added unique index on Series.name and Series.libraryId')).to.be.true) + expect(loggerInfoStub.getCall(5).calledWith(sinon.match('UPGRADE END: 2.13.5-series-column-unique '))).to.be.true + // validate rows + const series = await queryInterface.sequelize.query('SELECT "id", "name", "libraryId" FROM Series', { type: queryInterface.sequelize.QueryTypes.SELECT }) + expect(series).to.have.length(1) + expect(series).to.deep.include({ id: series1Id, name: 'Series 1', libraryId: library1Id }) + const bookSeries = await queryInterface.sequelize.query('SELECT "id", "sequence", "bookId", "seriesId" FROM BookSeries', { type: queryInterface.sequelize.QueryTypes.SELECT }) + expect(bookSeries).to.have.length(1) + expect(bookSeries).to.deep.include({ id: bookSeries1Id, sequence: '1, 2', bookId: book1Id, seriesId: series1Id }) + }) }) describe('down', () => { From 999ada03d16fd5b6faf387c757c454e74dcb50a6 Mon Sep 17 00:00:00 2001 From: Nicholas Wallace Date: Sat, 14 Sep 2024 14:36:47 -0700 Subject: [PATCH 06/45] Fix: missing variables --- test/server/migrations/v2.13.5-series-column-unique.test.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/test/server/migrations/v2.13.5-series-column-unique.test.js b/test/server/migrations/v2.13.5-series-column-unique.test.js index 19dc4a8e4..5ce5a465f 100644 --- a/test/server/migrations/v2.13.5-series-column-unique.test.js +++ b/test/server/migrations/v2.13.5-series-column-unique.test.js @@ -16,11 +16,13 @@ describe('migration-v2.13.5-series-column-unique', () => { let series3Id let series1Id_dup let series3Id_dup + let series1Id_dup2 let book1Id let book2Id let book3Id let book4Id let book5Id + let book6Id let library1Id let library2Id let bookSeries1Id @@ -28,6 +30,7 @@ describe('migration-v2.13.5-series-column-unique', () => { let bookSeries3Id let bookSeries1Id_dup let bookSeries3Id_dup + let bookSeries1Id_dup2 beforeEach(() => { sequelize = new Sequelize({ dialect: 'sqlite', storage: ':memory:', logging: false }) @@ -164,7 +167,7 @@ describe('migration-v2.13.5-series-column-unique', () => { expect(bookSeries).to.deep.include({ id: bookSeries3Id_dup, bookId: book5Id, seriesId: series3Id }) expect(bookSeries).to.deep.include({ id: bookSeries1Id_dup2, bookId: book6Id, seriesId: series1Id }) }) - it('update with same series name in different libraries', async () => { + it('upgrade with same series name in different libraries', async () => { // Add some entries to the Series table using the UUID for the ids await queryInterface.bulkInsert('Series', [ { id: series1Id, name: 'Series 1', libraryId: library1Id, createdAt: new Date(), updatedAt: new Date() }, From 836d772cd4502f109a06d8c78f824a03bad0d249 Mon Sep 17 00:00:00 2001 From: Nicholas Wallace Date: Sat, 14 Sep 2024 15:23:29 -0700 Subject: [PATCH 07/45] Update: remove the same book if occurs multiple times in duplicate series --- .../v2.13.5-series-column-unique.js | 74 ++++++++++++++++++- 1 file changed, 73 insertions(+), 1 deletion(-) diff --git a/server/migrations/v2.13.5-series-column-unique.js b/server/migrations/v2.13.5-series-column-unique.js index e7201bae1..21e4d4d5d 100644 --- a/server/migrations/v2.13.5-series-column-unique.js +++ b/server/migrations/v2.13.5-series-column-unique.js @@ -20,7 +20,15 @@ async function up({ context: { queryInterface, logger } }) { // Upwards migration script logger.info('UPGRADE BEGIN: 2.13.5-series-column-unique ') - // Use the queryInterface to get the series table and find duplicates in the `name` column + // 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, MAX(updatedAt) AS latestUpdatedAt, COUNT(name) AS count FROM Series @@ -36,6 +44,70 @@ async function up({ context: { queryInterface, logger } }) { // Report the series name that is being deleted logger.info(`[2.13.5 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, COUNT(bookId) AS count + 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) { + // 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) + } + } + ) + } + } + // Get all the most recent series which matches the `name` and `libraryId` const [mostRecentSeries] = await queryInterface.sequelize.query( ` From 691ed88096082a42f30e077c0f3c3170c84ab5af Mon Sep 17 00:00:00 2001 From: Nicholas Wallace Date: Sat, 14 Sep 2024 15:34:38 -0700 Subject: [PATCH 08/45] Add more logging, clean up typo --- .../v2.13.5-series-column-unique.js | 2 ++ .../v2.13.5-series-column-unique.test.js | 34 +++++++++++-------- 2 files changed, 22 insertions(+), 14 deletions(-) diff --git a/server/migrations/v2.13.5-series-column-unique.js b/server/migrations/v2.13.5-series-column-unique.js index 21e4d4d5d..8c9775882 100644 --- a/server/migrations/v2.13.5-series-column-unique.js +++ b/server/migrations/v2.13.5-series-column-unique.js @@ -67,6 +67,7 @@ async function up({ context: { queryInterface, logger } }) { // 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.13.5 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( ` @@ -106,6 +107,7 @@ async function up({ context: { queryInterface, logger } }) { } ) } + logger.info(`[2.13.5 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` diff --git a/test/server/migrations/v2.13.5-series-column-unique.test.js b/test/server/migrations/v2.13.5-series-column-unique.test.js index 5ce5a465f..2fd59eeb4 100644 --- a/test/server/migrations/v2.13.5-series-column-unique.test.js +++ b/test/server/migrations/v2.13.5-series-column-unique.test.js @@ -211,13 +211,15 @@ describe('migration-v2.13.5-series-column-unique', () => { await up({ context: { queryInterface, logger: Logger } }) - expect(loggerInfoStub.callCount).to.equal(6) + expect(loggerInfoStub.callCount).to.equal(8) expect(loggerInfoStub.getCall(0).calledWith(sinon.match('UPGRADE BEGIN: 2.13.5-series-column-unique '))).to.be.true expect(loggerInfoStub.getCall(1).calledWith(sinon.match('[2.13.5 migration] Found 1 duplicate series'))).to.be.true expect(loggerInfoStub.getCall(2).calledWith(sinon.match('[2.13.5 migration] Deduplicating series "Series 1" in library 3a5a1c7c-a914-472e-88b0-b871ceae63e7'))).to.be.true - expect(loggerInfoStub.getCall(3).calledWith(sinon.match('[2.13.5 migration] Deduplication complete'))).to.be.true - expect(loggerInfoStub.getCall(4).calledWith(sinon.match('Added unique index on Series.name and Series.libraryId')).to.be.true) - expect(loggerInfoStub.getCall(5).calledWith(sinon.match('UPGRADE END: 2.13.5-series-column-unique '))).to.be.true + expect(loggerInfoStub.getCall(3).calledWith(sinon.match('[2.13.5 migration] Deduplicating bookId 4a38b6e5-0ae4-4de4-b119-4e33891bd63f in series "Series 1" of library 3a5a1c7c-a914-472e-88b0-b871ceae63e7'))).to.be.true + expect(loggerInfoStub.getCall(4).calledWith(sinon.match('[2.13.5 migration] Finished cleanup of bookId 4a38b6e5-0ae4-4de4-b119-4e33891bd63f in series "Series 1" of library 3a5a1c7c-a914-472e-88b0-b871ceae63e7'))).to.be.true + expect(loggerInfoStub.getCall(5).calledWith(sinon.match('[2.13.5 migration] Deduplication complete'))).to.be.true + expect(loggerInfoStub.getCall(6).calledWith(sinon.match('Added unique index on Series.name and Series.libraryId'))).to.be.true + expect(loggerInfoStub.getCall(7).calledWith(sinon.match('UPGRADE END: 2.13.5-series-column-unique '))).to.be.true // validate rows const series = await queryInterface.sequelize.query('SELECT "id", "name", "libraryId" FROM Series', { type: queryInterface.sequelize.QueryTypes.SELECT }) expect(series).to.have.length(1) @@ -240,13 +242,15 @@ describe('migration-v2.13.5-series-column-unique', () => { await up({ context: { queryInterface, logger: Logger } }) - expect(loggerInfoStub.callCount).to.equal(6) + expect(loggerInfoStub.callCount).to.equal(8) expect(loggerInfoStub.getCall(0).calledWith(sinon.match('UPGRADE BEGIN: 2.13.5-series-column-unique '))).to.be.true expect(loggerInfoStub.getCall(1).calledWith(sinon.match('[2.13.5 migration] Found 1 duplicate series'))).to.be.true expect(loggerInfoStub.getCall(2).calledWith(sinon.match('[2.13.5 migration] Deduplicating series "Series 1" in library 3a5a1c7c-a914-472e-88b0-b871ceae63e7'))).to.be.true - expect(loggerInfoStub.getCall(3).calledWith(sinon.match('[2.13.5 migration] Deduplication complete'))).to.be.true - expect(loggerInfoStub.getCall(4).calledWith(sinon.match('Added unique index on Series.name and Series.libraryId')).to.be.true) - expect(loggerInfoStub.getCall(5).calledWith(sinon.match('UPGRADE END: 2.13.5-series-column-unique '))).to.be.true + expect(loggerInfoStub.getCall(3).calledWith(sinon.match('[2.13.5 migration] Deduplicating bookId 4a38b6e5-0ae4-4de4-b119-4e33891bd63f in series "Series 1" of library 3a5a1c7c-a914-472e-88b0-b871ceae63e7'))).to.be.true + expect(loggerInfoStub.getCall(4).calledWith(sinon.match('[2.13.5 migration] Finished cleanup of bookId 4a38b6e5-0ae4-4de4-b119-4e33891bd63f in series "Series 1" of library 3a5a1c7c-a914-472e-88b0-b871ceae63e7'))).to.be.true + expect(loggerInfoStub.getCall(5).calledWith(sinon.match('[2.13.5 migration] Deduplication complete'))).to.be.true + expect(loggerInfoStub.getCall(6).calledWith(sinon.match('Added unique index on Series.name and Series.libraryId'))).to.be.true + expect(loggerInfoStub.getCall(7).calledWith(sinon.match('UPGRADE END: 2.13.5-series-column-unique '))).to.be.true // validate rows const series = await queryInterface.sequelize.query('SELECT "id", "name", "libraryId" FROM Series', { type: queryInterface.sequelize.QueryTypes.SELECT }) expect(series).to.have.length(1) @@ -263,26 +267,28 @@ describe('migration-v2.13.5-series-column-unique', () => { ]) // Create a book that is in both series await queryInterface.bulkInsert('BookSeries', [ - { id: bookSeries1Id, sequence: '1', bookId: book1Id, seriesId: series1Id }, + { id: bookSeries1Id, sequence: '3', bookId: book1Id, seriesId: series1Id }, { id: bookSeries2Id, sequence: '2', bookId: book1Id, seriesId: series2Id } ]) await up({ context: { queryInterface, logger: Logger } }) - expect(loggerInfoStub.callCount).to.equal(6) + expect(loggerInfoStub.callCount).to.equal(8) expect(loggerInfoStub.getCall(0).calledWith(sinon.match('UPGRADE BEGIN: 2.13.5-series-column-unique '))).to.be.true expect(loggerInfoStub.getCall(1).calledWith(sinon.match('[2.13.5 migration] Found 1 duplicate series'))).to.be.true expect(loggerInfoStub.getCall(2).calledWith(sinon.match('[2.13.5 migration] Deduplicating series "Series 1" in library 3a5a1c7c-a914-472e-88b0-b871ceae63e7'))).to.be.true - expect(loggerInfoStub.getCall(3).calledWith(sinon.match('[2.13.5 migration] Deduplication complete'))).to.be.true - expect(loggerInfoStub.getCall(4).calledWith(sinon.match('Added unique index on Series.name and Series.libraryId')).to.be.true) - expect(loggerInfoStub.getCall(5).calledWith(sinon.match('UPGRADE END: 2.13.5-series-column-unique '))).to.be.true + expect(loggerInfoStub.getCall(3).calledWith(sinon.match('[2.13.5 migration] Deduplicating bookId 8bc2e61d-47f6-42ef-a3f4-93cf2f1de82f in series "Series 1" of library 3a5a1c7c-a914-472e-88b0-b871ceae63e7'))).to.be.true + expect(loggerInfoStub.getCall(4).calledWith(sinon.match('[2.13.5 migration] Finished cleanup of bookId 8bc2e61d-47f6-42ef-a3f4-93cf2f1de82f in series "Series 1" of library 3a5a1c7c-a914-472e-88b0-b871ceae63e7'))).to.be.true + expect(loggerInfoStub.getCall(5).calledWith(sinon.match('[2.13.5 migration] Deduplication complete'))).to.be.true + expect(loggerInfoStub.getCall(6).calledWith(sinon.match('Added unique index on Series.name and Series.libraryId'))).to.be.true + expect(loggerInfoStub.getCall(7).calledWith(sinon.match('UPGRADE END: 2.13.5-series-column-unique '))).to.be.true // validate rows const series = await queryInterface.sequelize.query('SELECT "id", "name", "libraryId" FROM Series', { type: queryInterface.sequelize.QueryTypes.SELECT }) expect(series).to.have.length(1) expect(series).to.deep.include({ id: series1Id, name: 'Series 1', libraryId: library1Id }) const bookSeries = await queryInterface.sequelize.query('SELECT "id", "sequence", "bookId", "seriesId" FROM BookSeries', { type: queryInterface.sequelize.QueryTypes.SELECT }) expect(bookSeries).to.have.length(1) - expect(bookSeries).to.deep.include({ id: bookSeries1Id, sequence: '1, 2', bookId: book1Id, seriesId: series1Id }) + expect(bookSeries).to.deep.include({ id: bookSeries1Id, sequence: '2', bookId: book1Id, seriesId: series1Id }) }) }) From 8b95dd65d917fe2c2b200918362b308f2514d6ee Mon Sep 17 00:00:00 2001 From: Nicholas Wallace Date: Sat, 14 Sep 2024 15:43:10 -0700 Subject: [PATCH 09/45] Fix: test cases checking the wrong bookSeriesId --- .../migrations/v2.13.5-series-column-unique.test.js | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/test/server/migrations/v2.13.5-series-column-unique.test.js b/test/server/migrations/v2.13.5-series-column-unique.test.js index 2fd59eeb4..3c5b1b040 100644 --- a/test/server/migrations/v2.13.5-series-column-unique.test.js +++ b/test/server/migrations/v2.13.5-series-column-unique.test.js @@ -226,7 +226,8 @@ describe('migration-v2.13.5-series-column-unique', () => { expect(series).to.deep.include({ id: series1Id, name: 'Series 1', libraryId: library1Id }) const bookSeries = await queryInterface.sequelize.query('SELECT "id", "sequence", "bookId", "seriesId" FROM BookSeries', { type: queryInterface.sequelize.QueryTypes.SELECT }) expect(bookSeries).to.have.length(1) - expect(bookSeries).to.deep.include({ id: bookSeries1Id, sequence: null, bookId: book1Id, seriesId: series1Id }) + // Keep BookSeries 2 because it was edited last from cleaning up duplicate books + expect(bookSeries).to.deep.include({ id: bookSeries2Id, sequence: null, bookId: book1Id, seriesId: series1Id }) }) it('upgrade with one book in two of the same series, one sequence is null', async () => { // Create two different series with the same name in the same library @@ -277,8 +278,8 @@ describe('migration-v2.13.5-series-column-unique', () => { expect(loggerInfoStub.getCall(0).calledWith(sinon.match('UPGRADE BEGIN: 2.13.5-series-column-unique '))).to.be.true expect(loggerInfoStub.getCall(1).calledWith(sinon.match('[2.13.5 migration] Found 1 duplicate series'))).to.be.true expect(loggerInfoStub.getCall(2).calledWith(sinon.match('[2.13.5 migration] Deduplicating series "Series 1" in library 3a5a1c7c-a914-472e-88b0-b871ceae63e7'))).to.be.true - expect(loggerInfoStub.getCall(3).calledWith(sinon.match('[2.13.5 migration] Deduplicating bookId 8bc2e61d-47f6-42ef-a3f4-93cf2f1de82f in series "Series 1" of library 3a5a1c7c-a914-472e-88b0-b871ceae63e7'))).to.be.true - expect(loggerInfoStub.getCall(4).calledWith(sinon.match('[2.13.5 migration] Finished cleanup of bookId 8bc2e61d-47f6-42ef-a3f4-93cf2f1de82f in series "Series 1" of library 3a5a1c7c-a914-472e-88b0-b871ceae63e7'))).to.be.true + expect(loggerInfoStub.getCall(3).calledWith(sinon.match('[2.13.5 migration] Deduplicating bookId 4a38b6e5-0ae4-4de4-b119-4e33891bd63f in series "Series 1" of library 3a5a1c7c-a914-472e-88b0-b871ceae63e7'))).to.be.true + expect(loggerInfoStub.getCall(4).calledWith(sinon.match('[2.13.5 migration] Finished cleanup of bookId 4a38b6e5-0ae4-4de4-b119-4e33891bd63f in series "Series 1" of library 3a5a1c7c-a914-472e-88b0-b871ceae63e7'))).to.be.true expect(loggerInfoStub.getCall(5).calledWith(sinon.match('[2.13.5 migration] Deduplication complete'))).to.be.true expect(loggerInfoStub.getCall(6).calledWith(sinon.match('Added unique index on Series.name and Series.libraryId'))).to.be.true expect(loggerInfoStub.getCall(7).calledWith(sinon.match('UPGRADE END: 2.13.5-series-column-unique '))).to.be.true @@ -288,7 +289,8 @@ describe('migration-v2.13.5-series-column-unique', () => { expect(series).to.deep.include({ id: series1Id, name: 'Series 1', libraryId: library1Id }) const bookSeries = await queryInterface.sequelize.query('SELECT "id", "sequence", "bookId", "seriesId" FROM BookSeries', { type: queryInterface.sequelize.QueryTypes.SELECT }) expect(bookSeries).to.have.length(1) - expect(bookSeries).to.deep.include({ id: bookSeries1Id, sequence: '2', bookId: book1Id, seriesId: series1Id }) + // Keep BookSeries 2 because it is the lower sequence number + expect(bookSeries).to.deep.include({ id: bookSeries2Id, sequence: '2', bookId: book1Id, seriesId: series1Id }) }) }) From 66b290577c3663a62edd2f5aaf8291b18cb72b98 Mon Sep 17 00:00:00 2001 From: Nicholas Wallace Date: Tue, 17 Sep 2024 20:00:06 -0700 Subject: [PATCH 10/45] Clean up unused parts of statement --- server/migrations/v2.13.5-series-column-unique.js | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/server/migrations/v2.13.5-series-column-unique.js b/server/migrations/v2.13.5-series-column-unique.js index 8c9775882..1860772d2 100644 --- a/server/migrations/v2.13.5-series-column-unique.js +++ b/server/migrations/v2.13.5-series-column-unique.js @@ -1,5 +1,3 @@ -const Logger = require('../Logger') - /** * @typedef MigrationContext * @property {import('sequelize').QueryInterface} queryInterface - a suquelize QueryInterface object. @@ -30,7 +28,7 @@ async function up({ context: { queryInterface, logger } }) { // 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, MAX(updatedAt) AS latestUpdatedAt, COUNT(name) AS count + SELECT name, libraryId FROM Series GROUP BY name, libraryId HAVING COUNT(name) > 1 @@ -47,7 +45,7 @@ async function up({ context: { queryInterface, logger } }) { // Determine any duplicate book IDs in the `bookSeries` table for the same series const [duplicateBookIds] = await queryInterface.sequelize.query( ` - SELECT bookId, COUNT(bookId) AS count + SELECT bookId FROM BookSeries WHERE seriesId IN ( SELECT id From 8a7b5cc87d0e8351569fa4e8772da781327d7cd4 Mon Sep 17 00:00:00 2001 From: advplyr Date: Tue, 24 Sep 2024 16:47:09 -0500 Subject: [PATCH 11/45] Ensure series-column-unique migration is idempotent --- server/managers/MigrationManager.js | 3 +-- .../v2.13.5-series-column-unique.js | 20 +++++++++++++------ 2 files changed, 15 insertions(+), 8 deletions(-) diff --git a/server/managers/MigrationManager.js b/server/managers/MigrationManager.js index 706e359cf..beaf8a4d8 100644 --- a/server/managers/MigrationManager.js +++ b/server/managers/MigrationManager.js @@ -38,6 +38,7 @@ class MigrationManager { if (!(await fs.pathExists(this.configPath))) throw new Error(`Config path does not exist: ${this.configPath}`) this.migrationsDir = path.join(this.configPath, 'migrations') + await fs.ensureDir(this.migrationsDir) this.serverVersion = this.extractVersionFromTag(serverVersion) if (!this.serverVersion) throw new Error(`Invalid server version: ${serverVersion}. Expected a version tag like v1.2.3.`) @@ -222,8 +223,6 @@ class MigrationManager { } async copyMigrationsToConfigDir() { - await fs.ensureDir(this.migrationsDir) // Ensure the target directory exists - if (!(await fs.pathExists(this.migrationsSourceDir))) return const files = await fs.readdir(this.migrationsSourceDir) diff --git a/server/migrations/v2.13.5-series-column-unique.js b/server/migrations/v2.13.5-series-column-unique.js index 1860772d2..2724221ab 100644 --- a/server/migrations/v2.13.5-series-column-unique.js +++ b/server/migrations/v2.13.5-series-column-unique.js @@ -16,7 +16,15 @@ */ async function up({ context: { queryInterface, logger } }) { // Upwards migration script - logger.info('UPGRADE BEGIN: 2.13.5-series-column-unique ') + logger.info('[2.13.5 migration] UPGRADE BEGIN: 2.13.5-series-column-unique ') + + // 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.13.5 migration] Unique index on Series.name and Series.libraryId already exists') + logger.info('[2.13.5 migration] UPGRADE END: 2.13.5-series-column-unique ') + return + } // The steps taken to deduplicate the series are as follows: // 1. Find all duplicate series in the `Series` table. @@ -173,9 +181,9 @@ async function up({ context: { queryInterface, logger } }) { unique: true, name: 'unique_series_name_per_library' }) - logger.info('Added unique index on Series.name and Series.libraryId') + logger.info('[2.13.5 migration] Added unique index on Series.name and Series.libraryId') - logger.info('UPGRADE END: 2.13.5-series-column-unique ') + logger.info('[2.13.5 migration] UPGRADE END: 2.13.5-series-column-unique ') } /** @@ -186,13 +194,13 @@ async function up({ context: { queryInterface, logger } }) { */ async function down({ context: { queryInterface, logger } }) { // Downward migration script - logger.info('DOWNGRADE BEGIN: 2.13.5-series-column-unique ') + logger.info('[2.13.5 migration] DOWNGRADE BEGIN: 2.13.5-series-column-unique ') // Remove the unique index await queryInterface.removeIndex('Series', 'unique_series_name_per_library') - logger.info('Removed unique index on Series.name and Series.libraryId') + logger.info('[2.13.5 migration] Removed unique index on Series.name and Series.libraryId') - logger.info('DOWNGRADE END: 2.13.5-series-column-unique ') + logger.info('[2.13.5 migration] DOWNGRADE END: 2.13.5-series-column-unique ') } module.exports = { up, down } From c67b5e950edbd86e74708e92d3c58c700bea5f1b Mon Sep 17 00:00:00 2001 From: advplyr Date: Tue, 24 Sep 2024 16:54:13 -0500 Subject: [PATCH 12/45] Update MigrationManager.test.js - moved migrations ensureDir to init() --- test/server/managers/MigrationManager.test.js | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/test/server/managers/MigrationManager.test.js b/test/server/managers/MigrationManager.test.js index ae94cd75c..af2e9da8f 100644 --- a/test/server/managers/MigrationManager.test.js +++ b/test/server/managers/MigrationManager.test.js @@ -63,6 +63,8 @@ describe('MigrationManager', () => { await migrationManager.init(serverVersion) // Assert + expect(fsEnsureDirStub.calledOnce).to.be.true + expect(fsEnsureDirStub.calledWith(migrationManager.migrationsDir)).to.be.true expect(migrationManager.serverVersion).to.equal(serverVersion) expect(migrationManager.sequelize).to.equal(sequelizeStub) expect(migrationManager.migrationsDir).to.equal(path.join(__dirname, 'migrations')) @@ -353,8 +355,6 @@ describe('MigrationManager', () => { await migrationManager.copyMigrationsToConfigDir() // Assert - expect(fsEnsureDirStub.calledOnce).to.be.true - expect(fsEnsureDirStub.calledWith(targetDir)).to.be.true expect(readdirStub.calledOnce).to.be.true expect(readdirStub.calledWith(migrationsSourceDir)).to.be.true expect(fsCopyStub.calledTwice).to.be.true @@ -382,8 +382,6 @@ describe('MigrationManager', () => { } catch (error) {} // Assert - expect(fsEnsureDirStub.calledOnce).to.be.true - expect(fsEnsureDirStub.calledWith(targetDir)).to.be.true expect(readdirStub.calledOnce).to.be.true expect(readdirStub.calledWith(migrationsSourceDir)).to.be.true expect(fsCopyStub.calledTwice).to.be.true From 5154e31c1cba8bad9a088f5586632fabb3abd656 Mon Sep 17 00:00:00 2001 From: advplyr Date: Tue, 24 Sep 2024 17:06:00 -0500 Subject: [PATCH 13/45] Update migration to v2.14.0 --- server/migrations/changelog.md | 2 +- ...que.js => v2.14.0-series-column-unique.js} | 26 ++--- ...s => v2.14.0-series-column-unique.test.js} | 102 +++++++++--------- 3 files changed, 65 insertions(+), 65 deletions(-) rename server/migrations/{v2.13.5-series-column-unique.js => v2.14.0-series-column-unique.js} (88%) rename test/server/migrations/{v2.13.5-series-column-unique.test.js => v2.14.0-series-column-unique.test.js} (78%) diff --git a/server/migrations/changelog.md b/server/migrations/changelog.md index bac3ec25e..3ab52ac3e 100644 --- a/server/migrations/changelog.md +++ b/server/migrations/changelog.md @@ -4,4 +4,4 @@ Please add a record of every database migration that you create to this file. Th | Server Version | Migration Script Name | Description | | -------------- | ---------------------------- | ------------------------------------------------- | -| v2.13.5 | v2.13.5-series-column-unique | Series must have unique names in the same library | +| v2.14.0 | v2.14.0-series-column-unique | Series must have unique names in the same library | diff --git a/server/migrations/v2.13.5-series-column-unique.js b/server/migrations/v2.14.0-series-column-unique.js similarity index 88% rename from server/migrations/v2.13.5-series-column-unique.js rename to server/migrations/v2.14.0-series-column-unique.js index 2724221ab..489b670bb 100644 --- a/server/migrations/v2.13.5-series-column-unique.js +++ b/server/migrations/v2.14.0-series-column-unique.js @@ -16,13 +16,13 @@ */ async function up({ context: { queryInterface, logger } }) { // Upwards migration script - logger.info('[2.13.5 migration] UPGRADE BEGIN: 2.13.5-series-column-unique ') + logger.info('[2.14.0 migration] UPGRADE BEGIN: 2.14.0-series-column-unique ') // 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.13.5 migration] Unique index on Series.name and Series.libraryId already exists') - logger.info('[2.13.5 migration] UPGRADE END: 2.13.5-series-column-unique ') + logger.info('[2.14.0 migration] Unique index on Series.name and Series.libraryId already exists') + logger.info('[2.14.0 migration] UPGRADE END: 2.14.0-series-column-unique ') return } @@ -43,12 +43,12 @@ async function up({ context: { queryInterface, logger } }) { `) // Print out how many duplicates were found - logger.info(`[2.13.5 migration] Found ${duplicates.length} duplicate series`) + logger.info(`[2.14.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.13.5 migration] Deduplicating series "${duplicate.name}" in library ${duplicate.libraryId}`) + logger.info(`[2.14.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( @@ -73,7 +73,7 @@ async function up({ context: { queryInterface, logger } }) { // 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.13.5 migration] Deduplicating bookId ${bookId} in series "${duplicate.name}" of library ${duplicate.libraryId}`) + logger.info(`[2.14.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( ` @@ -113,7 +113,7 @@ async function up({ context: { queryInterface, logger } }) { } ) } - logger.info(`[2.13.5 migration] Finished cleanup of bookId ${bookId} in series "${duplicate.name}" of library ${duplicate.libraryId}`) + logger.info(`[2.14.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` @@ -174,16 +174,16 @@ async function up({ context: { queryInterface, logger } }) { } } - logger.info(`[2.13.5 migration] Deduplication complete`) + logger.info(`[2.14.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.13.5 migration] Added unique index on Series.name and Series.libraryId') + logger.info('[2.14.0 migration] Added unique index on Series.name and Series.libraryId') - logger.info('[2.13.5 migration] UPGRADE END: 2.13.5-series-column-unique ') + logger.info('[2.14.0 migration] UPGRADE END: 2.14.0-series-column-unique ') } /** @@ -194,13 +194,13 @@ async function up({ context: { queryInterface, logger } }) { */ async function down({ context: { queryInterface, logger } }) { // Downward migration script - logger.info('[2.13.5 migration] DOWNGRADE BEGIN: 2.13.5-series-column-unique ') + logger.info('[2.14.0 migration] DOWNGRADE BEGIN: 2.14.0-series-column-unique ') // Remove the unique index await queryInterface.removeIndex('Series', 'unique_series_name_per_library') - logger.info('[2.13.5 migration] Removed unique index on Series.name and Series.libraryId') + logger.info('[2.14.0 migration] Removed unique index on Series.name and Series.libraryId') - logger.info('[2.13.5 migration] DOWNGRADE END: 2.13.5-series-column-unique ') + logger.info('[2.14.0 migration] DOWNGRADE END: 2.14.0-series-column-unique ') } module.exports = { up, down } diff --git a/test/server/migrations/v2.13.5-series-column-unique.test.js b/test/server/migrations/v2.14.0-series-column-unique.test.js similarity index 78% rename from test/server/migrations/v2.13.5-series-column-unique.test.js rename to test/server/migrations/v2.14.0-series-column-unique.test.js index 3c5b1b040..43acc9278 100644 --- a/test/server/migrations/v2.13.5-series-column-unique.test.js +++ b/test/server/migrations/v2.14.0-series-column-unique.test.js @@ -1,13 +1,13 @@ const { expect } = require('chai') const sinon = require('sinon') -const { up, down } = require('../../../server/migrations/v2.13.5-series-column-unique') +const { up, down } = require('../../../server/migrations/v2.14.0-series-column-unique') const { Sequelize } = require('sequelize') const Logger = require('../../../server/Logger') const { query } = require('express') const { logger } = require('sequelize/lib/utils/logger') const e = require('express') -describe('migration-v2.13.5-series-column-unique', () => { +describe('migration-v2.14.0-series-column-unique', () => { let sequelize let queryInterface let loggerInfoStub @@ -105,11 +105,11 @@ describe('migration-v2.13.5-series-column-unique', () => { await up({ context: { queryInterface, logger: Logger } }) expect(loggerInfoStub.callCount).to.equal(5) - expect(loggerInfoStub.getCall(0).calledWith(sinon.match('UPGRADE BEGIN: 2.13.5-series-column-unique '))).to.be.true - expect(loggerInfoStub.getCall(1).calledWith(sinon.match('[2.13.5 migration] Found 0 duplicate series'))).to.be.true - expect(loggerInfoStub.getCall(2).calledWith(sinon.match('[2.13.5 migration] Deduplication complete'))).to.be.true - expect(loggerInfoStub.getCall(3).calledWith(sinon.match('Added unique index on Series.name and Series.libraryId'))).to.be.true - expect(loggerInfoStub.getCall(4).calledWith(sinon.match('UPGRADE END: 2.13.5-series-column-unique '))).to.be.true + expect(loggerInfoStub.getCall(0).calledWith(sinon.match('[2.14.0 migration] UPGRADE BEGIN: 2.14.0-series-column-unique '))).to.be.true + expect(loggerInfoStub.getCall(1).calledWith(sinon.match('[2.14.0 migration] Found 0 duplicate series'))).to.be.true + expect(loggerInfoStub.getCall(2).calledWith(sinon.match('[2.14.0 migration] Deduplication complete'))).to.be.true + expect(loggerInfoStub.getCall(3).calledWith(sinon.match('[2.14.0 migration] Added unique index on Series.name and Series.libraryId'))).to.be.true + expect(loggerInfoStub.getCall(4).calledWith(sinon.match('[2.14.0 migration] UPGRADE END: 2.14.0-series-column-unique '))).to.be.true // Validate rows in tables const series = await queryInterface.sequelize.query('SELECT "id", "name", "libraryId" FROM Series', { type: queryInterface.sequelize.QueryTypes.SELECT }) expect(series).to.have.length(3) @@ -145,13 +145,13 @@ describe('migration-v2.13.5-series-column-unique', () => { await up({ context: { queryInterface, logger: Logger } }) expect(loggerInfoStub.callCount).to.equal(7) - expect(loggerInfoStub.getCall(0).calledWith(sinon.match('UPGRADE BEGIN: 2.13.5-series-column-unique '))).to.be.true - expect(loggerInfoStub.getCall(1).calledWith(sinon.match('[2.13.5 migration] Found 2 duplicate series'))).to.be.true - expect(loggerInfoStub.getCall(2).calledWith(sinon.match('[2.13.5 migration] Deduplicating series "Series 1" in library 3a5a1c7c-a914-472e-88b0-b871ceae63e7'))).to.be.true - expect(loggerInfoStub.getCall(3).calledWith(sinon.match('[2.13.5 migration] Deduplicating series "Series 3" in library 3a5a1c7c-a914-472e-88b0-b871ceae63e7'))).to.be.true - expect(loggerInfoStub.getCall(4).calledWith(sinon.match('[2.13.5 migration] Deduplication complete'))).to.be.true - expect(loggerInfoStub.getCall(5).calledWith(sinon.match('Added unique index on Series.name and Series.libraryId'))).to.be.true - expect(loggerInfoStub.getCall(6).calledWith(sinon.match('UPGRADE END: 2.13.5-series-column-unique '))).to.be.true + expect(loggerInfoStub.getCall(0).calledWith(sinon.match('[2.14.0 migration] UPGRADE BEGIN: 2.14.0-series-column-unique '))).to.be.true + expect(loggerInfoStub.getCall(1).calledWith(sinon.match('[2.14.0 migration] Found 2 duplicate series'))).to.be.true + expect(loggerInfoStub.getCall(2).calledWith(sinon.match('[2.14.0 migration] Deduplicating series "Series 1" in library 3a5a1c7c-a914-472e-88b0-b871ceae63e7'))).to.be.true + expect(loggerInfoStub.getCall(3).calledWith(sinon.match('[2.14.0 migration] Deduplicating series "Series 3" in library 3a5a1c7c-a914-472e-88b0-b871ceae63e7'))).to.be.true + expect(loggerInfoStub.getCall(4).calledWith(sinon.match('[2.14.0 migration] Deduplication complete'))).to.be.true + expect(loggerInfoStub.getCall(5).calledWith(sinon.match('[2.14.0 migration] Added unique index on Series.name and Series.libraryId'))).to.be.true + expect(loggerInfoStub.getCall(6).calledWith(sinon.match('[2.14.0 migration] UPGRADE END: 2.14.0-series-column-unique '))).to.be.true // Validate rows const series = await queryInterface.sequelize.query('SELECT "id", "name", "libraryId" FROM Series', { type: queryInterface.sequelize.QueryTypes.SELECT }) expect(series).to.have.length(3) @@ -182,11 +182,11 @@ describe('migration-v2.13.5-series-column-unique', () => { await up({ context: { queryInterface, logger: Logger } }) expect(loggerInfoStub.callCount).to.equal(5) - expect(loggerInfoStub.getCall(0).calledWith(sinon.match('UPGRADE BEGIN: 2.13.5-series-column-unique '))).to.be.true - expect(loggerInfoStub.getCall(1).calledWith(sinon.match('[2.13.5 migration] Found 0 duplicate series'))).to.be.true - expect(loggerInfoStub.getCall(2).calledWith(sinon.match('[2.13.5 migration] Deduplication complete'))).to.be.true - expect(loggerInfoStub.getCall(3).calledWith(sinon.match('Added unique index on Series.name and Series.libraryId'))).to.be.true - expect(loggerInfoStub.getCall(4).calledWith(sinon.match('UPGRADE END: 2.13.5-series-column-unique '))).to.be.true + expect(loggerInfoStub.getCall(0).calledWith(sinon.match('[2.14.0 migration] UPGRADE BEGIN: 2.14.0-series-column-unique '))).to.be.true + expect(loggerInfoStub.getCall(1).calledWith(sinon.match('[2.14.0 migration] Found 0 duplicate series'))).to.be.true + expect(loggerInfoStub.getCall(2).calledWith(sinon.match('[2.14.0 migration] Deduplication complete'))).to.be.true + expect(loggerInfoStub.getCall(3).calledWith(sinon.match('[2.14.0 migration] Added unique index on Series.name and Series.libraryId'))).to.be.true + expect(loggerInfoStub.getCall(4).calledWith(sinon.match('[2.14.0 migration] UPGRADE END: 2.14.0-series-column-unique '))).to.be.true // Validate rows const series = await queryInterface.sequelize.query('SELECT "id", "name", "libraryId" FROM Series', { type: queryInterface.sequelize.QueryTypes.SELECT }) expect(series).to.have.length(2) @@ -212,14 +212,14 @@ describe('migration-v2.13.5-series-column-unique', () => { await up({ context: { queryInterface, logger: Logger } }) expect(loggerInfoStub.callCount).to.equal(8) - expect(loggerInfoStub.getCall(0).calledWith(sinon.match('UPGRADE BEGIN: 2.13.5-series-column-unique '))).to.be.true - expect(loggerInfoStub.getCall(1).calledWith(sinon.match('[2.13.5 migration] Found 1 duplicate series'))).to.be.true - expect(loggerInfoStub.getCall(2).calledWith(sinon.match('[2.13.5 migration] Deduplicating series "Series 1" in library 3a5a1c7c-a914-472e-88b0-b871ceae63e7'))).to.be.true - expect(loggerInfoStub.getCall(3).calledWith(sinon.match('[2.13.5 migration] Deduplicating bookId 4a38b6e5-0ae4-4de4-b119-4e33891bd63f in series "Series 1" of library 3a5a1c7c-a914-472e-88b0-b871ceae63e7'))).to.be.true - expect(loggerInfoStub.getCall(4).calledWith(sinon.match('[2.13.5 migration] Finished cleanup of bookId 4a38b6e5-0ae4-4de4-b119-4e33891bd63f in series "Series 1" of library 3a5a1c7c-a914-472e-88b0-b871ceae63e7'))).to.be.true - expect(loggerInfoStub.getCall(5).calledWith(sinon.match('[2.13.5 migration] Deduplication complete'))).to.be.true - expect(loggerInfoStub.getCall(6).calledWith(sinon.match('Added unique index on Series.name and Series.libraryId'))).to.be.true - expect(loggerInfoStub.getCall(7).calledWith(sinon.match('UPGRADE END: 2.13.5-series-column-unique '))).to.be.true + expect(loggerInfoStub.getCall(0).calledWith(sinon.match('[2.14.0 migration] UPGRADE BEGIN: 2.14.0-series-column-unique '))).to.be.true + expect(loggerInfoStub.getCall(1).calledWith(sinon.match('[2.14.0 migration] Found 1 duplicate series'))).to.be.true + expect(loggerInfoStub.getCall(2).calledWith(sinon.match('[2.14.0 migration] Deduplicating series "Series 1" in library 3a5a1c7c-a914-472e-88b0-b871ceae63e7'))).to.be.true + expect(loggerInfoStub.getCall(3).calledWith(sinon.match('[2.14.0 migration] Deduplicating bookId 4a38b6e5-0ae4-4de4-b119-4e33891bd63f in series "Series 1" of library 3a5a1c7c-a914-472e-88b0-b871ceae63e7'))).to.be.true + expect(loggerInfoStub.getCall(4).calledWith(sinon.match('[2.14.0 migration] Finished cleanup of bookId 4a38b6e5-0ae4-4de4-b119-4e33891bd63f in series "Series 1" of library 3a5a1c7c-a914-472e-88b0-b871ceae63e7'))).to.be.true + expect(loggerInfoStub.getCall(5).calledWith(sinon.match('[2.14.0 migration] Deduplication complete'))).to.be.true + expect(loggerInfoStub.getCall(6).calledWith(sinon.match('[2.14.0 migration] Added unique index on Series.name and Series.libraryId'))).to.be.true + expect(loggerInfoStub.getCall(7).calledWith(sinon.match('[2.14.0 migration] UPGRADE END: 2.14.0-series-column-unique '))).to.be.true // validate rows const series = await queryInterface.sequelize.query('SELECT "id", "name", "libraryId" FROM Series', { type: queryInterface.sequelize.QueryTypes.SELECT }) expect(series).to.have.length(1) @@ -244,14 +244,14 @@ describe('migration-v2.13.5-series-column-unique', () => { await up({ context: { queryInterface, logger: Logger } }) expect(loggerInfoStub.callCount).to.equal(8) - expect(loggerInfoStub.getCall(0).calledWith(sinon.match('UPGRADE BEGIN: 2.13.5-series-column-unique '))).to.be.true - expect(loggerInfoStub.getCall(1).calledWith(sinon.match('[2.13.5 migration] Found 1 duplicate series'))).to.be.true - expect(loggerInfoStub.getCall(2).calledWith(sinon.match('[2.13.5 migration] Deduplicating series "Series 1" in library 3a5a1c7c-a914-472e-88b0-b871ceae63e7'))).to.be.true - expect(loggerInfoStub.getCall(3).calledWith(sinon.match('[2.13.5 migration] Deduplicating bookId 4a38b6e5-0ae4-4de4-b119-4e33891bd63f in series "Series 1" of library 3a5a1c7c-a914-472e-88b0-b871ceae63e7'))).to.be.true - expect(loggerInfoStub.getCall(4).calledWith(sinon.match('[2.13.5 migration] Finished cleanup of bookId 4a38b6e5-0ae4-4de4-b119-4e33891bd63f in series "Series 1" of library 3a5a1c7c-a914-472e-88b0-b871ceae63e7'))).to.be.true - expect(loggerInfoStub.getCall(5).calledWith(sinon.match('[2.13.5 migration] Deduplication complete'))).to.be.true - expect(loggerInfoStub.getCall(6).calledWith(sinon.match('Added unique index on Series.name and Series.libraryId'))).to.be.true - expect(loggerInfoStub.getCall(7).calledWith(sinon.match('UPGRADE END: 2.13.5-series-column-unique '))).to.be.true + expect(loggerInfoStub.getCall(0).calledWith(sinon.match('[2.14.0 migration] UPGRADE BEGIN: 2.14.0-series-column-unique '))).to.be.true + expect(loggerInfoStub.getCall(1).calledWith(sinon.match('[2.14.0 migration] Found 1 duplicate series'))).to.be.true + expect(loggerInfoStub.getCall(2).calledWith(sinon.match('[2.14.0 migration] Deduplicating series "Series 1" in library 3a5a1c7c-a914-472e-88b0-b871ceae63e7'))).to.be.true + expect(loggerInfoStub.getCall(3).calledWith(sinon.match('[2.14.0 migration] Deduplicating bookId 4a38b6e5-0ae4-4de4-b119-4e33891bd63f in series "Series 1" of library 3a5a1c7c-a914-472e-88b0-b871ceae63e7'))).to.be.true + expect(loggerInfoStub.getCall(4).calledWith(sinon.match('[2.14.0 migration] Finished cleanup of bookId 4a38b6e5-0ae4-4de4-b119-4e33891bd63f in series "Series 1" of library 3a5a1c7c-a914-472e-88b0-b871ceae63e7'))).to.be.true + expect(loggerInfoStub.getCall(5).calledWith(sinon.match('[2.14.0 migration] Deduplication complete'))).to.be.true + expect(loggerInfoStub.getCall(6).calledWith(sinon.match('[2.14.0 migration] Added unique index on Series.name and Series.libraryId'))).to.be.true + expect(loggerInfoStub.getCall(7).calledWith(sinon.match('[2.14.0 migration] UPGRADE END: 2.14.0-series-column-unique '))).to.be.true // validate rows const series = await queryInterface.sequelize.query('SELECT "id", "name", "libraryId" FROM Series', { type: queryInterface.sequelize.QueryTypes.SELECT }) expect(series).to.have.length(1) @@ -275,14 +275,14 @@ describe('migration-v2.13.5-series-column-unique', () => { await up({ context: { queryInterface, logger: Logger } }) expect(loggerInfoStub.callCount).to.equal(8) - expect(loggerInfoStub.getCall(0).calledWith(sinon.match('UPGRADE BEGIN: 2.13.5-series-column-unique '))).to.be.true - expect(loggerInfoStub.getCall(1).calledWith(sinon.match('[2.13.5 migration] Found 1 duplicate series'))).to.be.true - expect(loggerInfoStub.getCall(2).calledWith(sinon.match('[2.13.5 migration] Deduplicating series "Series 1" in library 3a5a1c7c-a914-472e-88b0-b871ceae63e7'))).to.be.true - expect(loggerInfoStub.getCall(3).calledWith(sinon.match('[2.13.5 migration] Deduplicating bookId 4a38b6e5-0ae4-4de4-b119-4e33891bd63f in series "Series 1" of library 3a5a1c7c-a914-472e-88b0-b871ceae63e7'))).to.be.true - expect(loggerInfoStub.getCall(4).calledWith(sinon.match('[2.13.5 migration] Finished cleanup of bookId 4a38b6e5-0ae4-4de4-b119-4e33891bd63f in series "Series 1" of library 3a5a1c7c-a914-472e-88b0-b871ceae63e7'))).to.be.true - expect(loggerInfoStub.getCall(5).calledWith(sinon.match('[2.13.5 migration] Deduplication complete'))).to.be.true - expect(loggerInfoStub.getCall(6).calledWith(sinon.match('Added unique index on Series.name and Series.libraryId'))).to.be.true - expect(loggerInfoStub.getCall(7).calledWith(sinon.match('UPGRADE END: 2.13.5-series-column-unique '))).to.be.true + expect(loggerInfoStub.getCall(0).calledWith(sinon.match('[2.14.0 migration] UPGRADE BEGIN: 2.14.0-series-column-unique '))).to.be.true + expect(loggerInfoStub.getCall(1).calledWith(sinon.match('[2.14.0 migration] Found 1 duplicate series'))).to.be.true + expect(loggerInfoStub.getCall(2).calledWith(sinon.match('[2.14.0 migration] Deduplicating series "Series 1" in library 3a5a1c7c-a914-472e-88b0-b871ceae63e7'))).to.be.true + expect(loggerInfoStub.getCall(3).calledWith(sinon.match('[2.14.0 migration] Deduplicating bookId 4a38b6e5-0ae4-4de4-b119-4e33891bd63f in series "Series 1" of library 3a5a1c7c-a914-472e-88b0-b871ceae63e7'))).to.be.true + expect(loggerInfoStub.getCall(4).calledWith(sinon.match('[2.14.0 migration] Finished cleanup of bookId 4a38b6e5-0ae4-4de4-b119-4e33891bd63f in series "Series 1" of library 3a5a1c7c-a914-472e-88b0-b871ceae63e7'))).to.be.true + expect(loggerInfoStub.getCall(5).calledWith(sinon.match('[2.14.0 migration] Deduplication complete'))).to.be.true + expect(loggerInfoStub.getCall(6).calledWith(sinon.match('[2.14.0 migration] Added unique index on Series.name and Series.libraryId'))).to.be.true + expect(loggerInfoStub.getCall(7).calledWith(sinon.match('[2.14.0 migration] UPGRADE END: 2.14.0-series-column-unique '))).to.be.true // validate rows const series = await queryInterface.sequelize.query('SELECT "id", "name", "libraryId" FROM Series', { type: queryInterface.sequelize.QueryTypes.SELECT }) expect(series).to.have.length(1) @@ -319,14 +319,14 @@ describe('migration-v2.13.5-series-column-unique', () => { await down({ context: { queryInterface, logger: Logger } }) expect(loggerInfoStub.callCount).to.equal(8) - expect(loggerInfoStub.getCall(0).calledWith(sinon.match('UPGRADE BEGIN: 2.13.5-series-column-unique '))).to.be.true - expect(loggerInfoStub.getCall(1).calledWith(sinon.match('[2.13.5 migration] Found 0 duplicate series'))).to.be.true - expect(loggerInfoStub.getCall(2).calledWith(sinon.match('[2.13.5 migration] Deduplication complete'))).to.be.true - expect(loggerInfoStub.getCall(3).calledWith(sinon.match('Added unique index on Series.name and Series.libraryId'))).to.be.true - expect(loggerInfoStub.getCall(4).calledWith(sinon.match('UPGRADE END: 2.13.5-series-column-unique '))).to.be.true - expect(loggerInfoStub.getCall(5).calledWith(sinon.match('DOWNGRADE BEGIN: 2.13.5-series-column-unique '))).to.be.true - expect(loggerInfoStub.getCall(6).calledWith(sinon.match('Removed unique index on Series.name and Series.libraryId'))).to.be.true - expect(loggerInfoStub.getCall(7).calledWith(sinon.match('DOWNGRADE END: 2.13.5-series-column-unique '))).to.be.true + expect(loggerInfoStub.getCall(0).calledWith(sinon.match('[2.14.0 migration] UPGRADE BEGIN: 2.14.0-series-column-unique '))).to.be.true + expect(loggerInfoStub.getCall(1).calledWith(sinon.match('[2.14.0 migration] Found 0 duplicate series'))).to.be.true + expect(loggerInfoStub.getCall(2).calledWith(sinon.match('[2.14.0 migration] Deduplication complete'))).to.be.true + expect(loggerInfoStub.getCall(3).calledWith(sinon.match('[2.14.0 migration] Added unique index on Series.name and Series.libraryId'))).to.be.true + expect(loggerInfoStub.getCall(4).calledWith(sinon.match('[2.14.0 migration] UPGRADE END: 2.14.0-series-column-unique '))).to.be.true + expect(loggerInfoStub.getCall(5).calledWith(sinon.match('[2.14.0 migration] DOWNGRADE BEGIN: 2.14.0-series-column-unique '))).to.be.true + expect(loggerInfoStub.getCall(6).calledWith(sinon.match('[2.14.0 migration] Removed unique index on Series.name and Series.libraryId'))).to.be.true + expect(loggerInfoStub.getCall(7).calledWith(sinon.match('[2.14.0 migration] DOWNGRADE END: 2.14.0-series-column-unique '))).to.be.true // Ensure index does not exist const indexes = await queryInterface.showIndex('Series') expect(indexes).to.not.deep.include({ tableName: 'Series', unique: true, fields: ['name', 'libraryId'], name: 'unique_series_name_per_library' }) From e6e494a92cbf5d72e40fd9268c5a65c07a6630ee Mon Sep 17 00:00:00 2001 From: Nicholas Wallace Date: Mon, 7 Oct 2024 18:52:14 -0700 Subject: [PATCH 14/45] Rename for next minor release --- server/migrations/changelog.md | 2 +- ...que.js => v2.15.0-series-column-unique.js} | 26 ++--- ...s => v2.15.0-series-column-unique.test.js} | 102 +++++++++--------- 3 files changed, 65 insertions(+), 65 deletions(-) rename server/migrations/{v2.14.0-series-column-unique.js => v2.15.0-series-column-unique.js} (88%) rename test/server/migrations/{v2.14.0-series-column-unique.test.js => v2.15.0-series-column-unique.test.js} (86%) diff --git a/server/migrations/changelog.md b/server/migrations/changelog.md index 3ab52ac3e..b5dde7492 100644 --- a/server/migrations/changelog.md +++ b/server/migrations/changelog.md @@ -4,4 +4,4 @@ Please add a record of every database migration that you create to this file. Th | Server Version | Migration Script Name | Description | | -------------- | ---------------------------- | ------------------------------------------------- | -| v2.14.0 | v2.14.0-series-column-unique | Series must have unique names in the same library | +| v2.15.0 | v2.15.0-series-column-unique | Series must have unique names in the same library | diff --git a/server/migrations/v2.14.0-series-column-unique.js b/server/migrations/v2.15.0-series-column-unique.js similarity index 88% rename from server/migrations/v2.14.0-series-column-unique.js rename to server/migrations/v2.15.0-series-column-unique.js index 489b670bb..96b0ea604 100644 --- a/server/migrations/v2.14.0-series-column-unique.js +++ b/server/migrations/v2.15.0-series-column-unique.js @@ -16,13 +16,13 @@ */ async function up({ context: { queryInterface, logger } }) { // Upwards migration script - logger.info('[2.14.0 migration] UPGRADE BEGIN: 2.14.0-series-column-unique ') + logger.info('[2.15.0 migration] UPGRADE BEGIN: 2.15.0-series-column-unique ') // 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.14.0 migration] Unique index on Series.name and Series.libraryId already exists') - logger.info('[2.14.0 migration] UPGRADE END: 2.14.0-series-column-unique ') + 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 } @@ -43,12 +43,12 @@ async function up({ context: { queryInterface, logger } }) { `) // Print out how many duplicates were found - logger.info(`[2.14.0 migration] Found ${duplicates.length} duplicate series`) + 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.14.0 migration] Deduplicating series "${duplicate.name}" in library ${duplicate.libraryId}`) + 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( @@ -73,7 +73,7 @@ async function up({ context: { queryInterface, logger } }) { // 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.14.0 migration] Deduplicating bookId ${bookId} in series "${duplicate.name}" of library ${duplicate.libraryId}`) + 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( ` @@ -113,7 +113,7 @@ async function up({ context: { queryInterface, logger } }) { } ) } - logger.info(`[2.14.0 migration] Finished cleanup of bookId ${bookId} in series "${duplicate.name}" of library ${duplicate.libraryId}`) + 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` @@ -174,16 +174,16 @@ async function up({ context: { queryInterface, logger } }) { } } - logger.info(`[2.14.0 migration] Deduplication complete`) + 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.14.0 migration] Added unique index on Series.name and Series.libraryId') + logger.info('[2.15.0 migration] Added unique index on Series.name and Series.libraryId') - logger.info('[2.14.0 migration] UPGRADE END: 2.14.0-series-column-unique ') + logger.info('[2.15.0 migration] UPGRADE END: 2.15.0-series-column-unique ') } /** @@ -194,13 +194,13 @@ async function up({ context: { queryInterface, logger } }) { */ async function down({ context: { queryInterface, logger } }) { // Downward migration script - logger.info('[2.14.0 migration] DOWNGRADE BEGIN: 2.14.0-series-column-unique ') + 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.14.0 migration] Removed unique index on Series.name and Series.libraryId') + logger.info('[2.15.0 migration] Removed unique index on Series.name and Series.libraryId') - logger.info('[2.14.0 migration] DOWNGRADE END: 2.14.0-series-column-unique ') + logger.info('[2.15.0 migration] DOWNGRADE END: 2.15.0-series-column-unique ') } module.exports = { up, down } diff --git a/test/server/migrations/v2.14.0-series-column-unique.test.js b/test/server/migrations/v2.15.0-series-column-unique.test.js similarity index 86% rename from test/server/migrations/v2.14.0-series-column-unique.test.js rename to test/server/migrations/v2.15.0-series-column-unique.test.js index 43acc9278..4ae07e63e 100644 --- a/test/server/migrations/v2.14.0-series-column-unique.test.js +++ b/test/server/migrations/v2.15.0-series-column-unique.test.js @@ -1,13 +1,13 @@ const { expect } = require('chai') const sinon = require('sinon') -const { up, down } = require('../../../server/migrations/v2.14.0-series-column-unique') +const { up, down } = require('../../../server/migrations/v2.15.0-series-column-unique') const { Sequelize } = require('sequelize') const Logger = require('../../../server/Logger') const { query } = require('express') const { logger } = require('sequelize/lib/utils/logger') const e = require('express') -describe('migration-v2.14.0-series-column-unique', () => { +describe('migration-v2.15.0-series-column-unique', () => { let sequelize let queryInterface let loggerInfoStub @@ -105,11 +105,11 @@ describe('migration-v2.14.0-series-column-unique', () => { await up({ context: { queryInterface, logger: Logger } }) expect(loggerInfoStub.callCount).to.equal(5) - expect(loggerInfoStub.getCall(0).calledWith(sinon.match('[2.14.0 migration] UPGRADE BEGIN: 2.14.0-series-column-unique '))).to.be.true - expect(loggerInfoStub.getCall(1).calledWith(sinon.match('[2.14.0 migration] Found 0 duplicate series'))).to.be.true - expect(loggerInfoStub.getCall(2).calledWith(sinon.match('[2.14.0 migration] Deduplication complete'))).to.be.true - expect(loggerInfoStub.getCall(3).calledWith(sinon.match('[2.14.0 migration] Added unique index on Series.name and Series.libraryId'))).to.be.true - expect(loggerInfoStub.getCall(4).calledWith(sinon.match('[2.14.0 migration] UPGRADE END: 2.14.0-series-column-unique '))).to.be.true + expect(loggerInfoStub.getCall(0).calledWith(sinon.match('[2.15.0 migration] UPGRADE BEGIN: 2.15.0-series-column-unique '))).to.be.true + expect(loggerInfoStub.getCall(1).calledWith(sinon.match('[2.15.0 migration] Found 0 duplicate series'))).to.be.true + expect(loggerInfoStub.getCall(2).calledWith(sinon.match('[2.15.0 migration] Deduplication complete'))).to.be.true + expect(loggerInfoStub.getCall(3).calledWith(sinon.match('[2.15.0 migration] Added unique index on Series.name and Series.libraryId'))).to.be.true + expect(loggerInfoStub.getCall(4).calledWith(sinon.match('[2.15.0 migration] UPGRADE END: 2.15.0-series-column-unique '))).to.be.true // Validate rows in tables const series = await queryInterface.sequelize.query('SELECT "id", "name", "libraryId" FROM Series', { type: queryInterface.sequelize.QueryTypes.SELECT }) expect(series).to.have.length(3) @@ -145,13 +145,13 @@ describe('migration-v2.14.0-series-column-unique', () => { await up({ context: { queryInterface, logger: Logger } }) expect(loggerInfoStub.callCount).to.equal(7) - expect(loggerInfoStub.getCall(0).calledWith(sinon.match('[2.14.0 migration] UPGRADE BEGIN: 2.14.0-series-column-unique '))).to.be.true - expect(loggerInfoStub.getCall(1).calledWith(sinon.match('[2.14.0 migration] Found 2 duplicate series'))).to.be.true - expect(loggerInfoStub.getCall(2).calledWith(sinon.match('[2.14.0 migration] Deduplicating series "Series 1" in library 3a5a1c7c-a914-472e-88b0-b871ceae63e7'))).to.be.true - expect(loggerInfoStub.getCall(3).calledWith(sinon.match('[2.14.0 migration] Deduplicating series "Series 3" in library 3a5a1c7c-a914-472e-88b0-b871ceae63e7'))).to.be.true - expect(loggerInfoStub.getCall(4).calledWith(sinon.match('[2.14.0 migration] Deduplication complete'))).to.be.true - expect(loggerInfoStub.getCall(5).calledWith(sinon.match('[2.14.0 migration] Added unique index on Series.name and Series.libraryId'))).to.be.true - expect(loggerInfoStub.getCall(6).calledWith(sinon.match('[2.14.0 migration] UPGRADE END: 2.14.0-series-column-unique '))).to.be.true + expect(loggerInfoStub.getCall(0).calledWith(sinon.match('[2.15.0 migration] UPGRADE BEGIN: 2.15.0-series-column-unique '))).to.be.true + expect(loggerInfoStub.getCall(1).calledWith(sinon.match('[2.15.0 migration] Found 2 duplicate series'))).to.be.true + expect(loggerInfoStub.getCall(2).calledWith(sinon.match('[2.15.0 migration] Deduplicating series "Series 1" in library 3a5a1c7c-a914-472e-88b0-b871ceae63e7'))).to.be.true + expect(loggerInfoStub.getCall(3).calledWith(sinon.match('[2.15.0 migration] Deduplicating series "Series 3" in library 3a5a1c7c-a914-472e-88b0-b871ceae63e7'))).to.be.true + expect(loggerInfoStub.getCall(4).calledWith(sinon.match('[2.15.0 migration] Deduplication complete'))).to.be.true + expect(loggerInfoStub.getCall(5).calledWith(sinon.match('[2.15.0 migration] Added unique index on Series.name and Series.libraryId'))).to.be.true + expect(loggerInfoStub.getCall(6).calledWith(sinon.match('[2.15.0 migration] UPGRADE END: 2.15.0-series-column-unique '))).to.be.true // Validate rows const series = await queryInterface.sequelize.query('SELECT "id", "name", "libraryId" FROM Series', { type: queryInterface.sequelize.QueryTypes.SELECT }) expect(series).to.have.length(3) @@ -182,11 +182,11 @@ describe('migration-v2.14.0-series-column-unique', () => { await up({ context: { queryInterface, logger: Logger } }) expect(loggerInfoStub.callCount).to.equal(5) - expect(loggerInfoStub.getCall(0).calledWith(sinon.match('[2.14.0 migration] UPGRADE BEGIN: 2.14.0-series-column-unique '))).to.be.true - expect(loggerInfoStub.getCall(1).calledWith(sinon.match('[2.14.0 migration] Found 0 duplicate series'))).to.be.true - expect(loggerInfoStub.getCall(2).calledWith(sinon.match('[2.14.0 migration] Deduplication complete'))).to.be.true - expect(loggerInfoStub.getCall(3).calledWith(sinon.match('[2.14.0 migration] Added unique index on Series.name and Series.libraryId'))).to.be.true - expect(loggerInfoStub.getCall(4).calledWith(sinon.match('[2.14.0 migration] UPGRADE END: 2.14.0-series-column-unique '))).to.be.true + expect(loggerInfoStub.getCall(0).calledWith(sinon.match('[2.15.0 migration] UPGRADE BEGIN: 2.15.0-series-column-unique '))).to.be.true + expect(loggerInfoStub.getCall(1).calledWith(sinon.match('[2.15.0 migration] Found 0 duplicate series'))).to.be.true + expect(loggerInfoStub.getCall(2).calledWith(sinon.match('[2.15.0 migration] Deduplication complete'))).to.be.true + expect(loggerInfoStub.getCall(3).calledWith(sinon.match('[2.15.0 migration] Added unique index on Series.name and Series.libraryId'))).to.be.true + expect(loggerInfoStub.getCall(4).calledWith(sinon.match('[2.15.0 migration] UPGRADE END: 2.15.0-series-column-unique '))).to.be.true // Validate rows const series = await queryInterface.sequelize.query('SELECT "id", "name", "libraryId" FROM Series', { type: queryInterface.sequelize.QueryTypes.SELECT }) expect(series).to.have.length(2) @@ -212,14 +212,14 @@ describe('migration-v2.14.0-series-column-unique', () => { await up({ context: { queryInterface, logger: Logger } }) expect(loggerInfoStub.callCount).to.equal(8) - expect(loggerInfoStub.getCall(0).calledWith(sinon.match('[2.14.0 migration] UPGRADE BEGIN: 2.14.0-series-column-unique '))).to.be.true - expect(loggerInfoStub.getCall(1).calledWith(sinon.match('[2.14.0 migration] Found 1 duplicate series'))).to.be.true - expect(loggerInfoStub.getCall(2).calledWith(sinon.match('[2.14.0 migration] Deduplicating series "Series 1" in library 3a5a1c7c-a914-472e-88b0-b871ceae63e7'))).to.be.true - expect(loggerInfoStub.getCall(3).calledWith(sinon.match('[2.14.0 migration] Deduplicating bookId 4a38b6e5-0ae4-4de4-b119-4e33891bd63f in series "Series 1" of library 3a5a1c7c-a914-472e-88b0-b871ceae63e7'))).to.be.true - expect(loggerInfoStub.getCall(4).calledWith(sinon.match('[2.14.0 migration] Finished cleanup of bookId 4a38b6e5-0ae4-4de4-b119-4e33891bd63f in series "Series 1" of library 3a5a1c7c-a914-472e-88b0-b871ceae63e7'))).to.be.true - expect(loggerInfoStub.getCall(5).calledWith(sinon.match('[2.14.0 migration] Deduplication complete'))).to.be.true - expect(loggerInfoStub.getCall(6).calledWith(sinon.match('[2.14.0 migration] Added unique index on Series.name and Series.libraryId'))).to.be.true - expect(loggerInfoStub.getCall(7).calledWith(sinon.match('[2.14.0 migration] UPGRADE END: 2.14.0-series-column-unique '))).to.be.true + expect(loggerInfoStub.getCall(0).calledWith(sinon.match('[2.15.0 migration] UPGRADE BEGIN: 2.15.0-series-column-unique '))).to.be.true + expect(loggerInfoStub.getCall(1).calledWith(sinon.match('[2.15.0 migration] Found 1 duplicate series'))).to.be.true + expect(loggerInfoStub.getCall(2).calledWith(sinon.match('[2.15.0 migration] Deduplicating series "Series 1" in library 3a5a1c7c-a914-472e-88b0-b871ceae63e7'))).to.be.true + expect(loggerInfoStub.getCall(3).calledWith(sinon.match('[2.15.0 migration] Deduplicating bookId 4a38b6e5-0ae4-4de4-b119-4e33891bd63f in series "Series 1" of library 3a5a1c7c-a914-472e-88b0-b871ceae63e7'))).to.be.true + expect(loggerInfoStub.getCall(4).calledWith(sinon.match('[2.15.0 migration] Finished cleanup of bookId 4a38b6e5-0ae4-4de4-b119-4e33891bd63f in series "Series 1" of library 3a5a1c7c-a914-472e-88b0-b871ceae63e7'))).to.be.true + expect(loggerInfoStub.getCall(5).calledWith(sinon.match('[2.15.0 migration] Deduplication complete'))).to.be.true + expect(loggerInfoStub.getCall(6).calledWith(sinon.match('[2.15.0 migration] Added unique index on Series.name and Series.libraryId'))).to.be.true + expect(loggerInfoStub.getCall(7).calledWith(sinon.match('[2.15.0 migration] UPGRADE END: 2.15.0-series-column-unique '))).to.be.true // validate rows const series = await queryInterface.sequelize.query('SELECT "id", "name", "libraryId" FROM Series', { type: queryInterface.sequelize.QueryTypes.SELECT }) expect(series).to.have.length(1) @@ -244,14 +244,14 @@ describe('migration-v2.14.0-series-column-unique', () => { await up({ context: { queryInterface, logger: Logger } }) expect(loggerInfoStub.callCount).to.equal(8) - expect(loggerInfoStub.getCall(0).calledWith(sinon.match('[2.14.0 migration] UPGRADE BEGIN: 2.14.0-series-column-unique '))).to.be.true - expect(loggerInfoStub.getCall(1).calledWith(sinon.match('[2.14.0 migration] Found 1 duplicate series'))).to.be.true - expect(loggerInfoStub.getCall(2).calledWith(sinon.match('[2.14.0 migration] Deduplicating series "Series 1" in library 3a5a1c7c-a914-472e-88b0-b871ceae63e7'))).to.be.true - expect(loggerInfoStub.getCall(3).calledWith(sinon.match('[2.14.0 migration] Deduplicating bookId 4a38b6e5-0ae4-4de4-b119-4e33891bd63f in series "Series 1" of library 3a5a1c7c-a914-472e-88b0-b871ceae63e7'))).to.be.true - expect(loggerInfoStub.getCall(4).calledWith(sinon.match('[2.14.0 migration] Finished cleanup of bookId 4a38b6e5-0ae4-4de4-b119-4e33891bd63f in series "Series 1" of library 3a5a1c7c-a914-472e-88b0-b871ceae63e7'))).to.be.true - expect(loggerInfoStub.getCall(5).calledWith(sinon.match('[2.14.0 migration] Deduplication complete'))).to.be.true - expect(loggerInfoStub.getCall(6).calledWith(sinon.match('[2.14.0 migration] Added unique index on Series.name and Series.libraryId'))).to.be.true - expect(loggerInfoStub.getCall(7).calledWith(sinon.match('[2.14.0 migration] UPGRADE END: 2.14.0-series-column-unique '))).to.be.true + expect(loggerInfoStub.getCall(0).calledWith(sinon.match('[2.15.0 migration] UPGRADE BEGIN: 2.15.0-series-column-unique '))).to.be.true + expect(loggerInfoStub.getCall(1).calledWith(sinon.match('[2.15.0 migration] Found 1 duplicate series'))).to.be.true + expect(loggerInfoStub.getCall(2).calledWith(sinon.match('[2.15.0 migration] Deduplicating series "Series 1" in library 3a5a1c7c-a914-472e-88b0-b871ceae63e7'))).to.be.true + expect(loggerInfoStub.getCall(3).calledWith(sinon.match('[2.15.0 migration] Deduplicating bookId 4a38b6e5-0ae4-4de4-b119-4e33891bd63f in series "Series 1" of library 3a5a1c7c-a914-472e-88b0-b871ceae63e7'))).to.be.true + expect(loggerInfoStub.getCall(4).calledWith(sinon.match('[2.15.0 migration] Finished cleanup of bookId 4a38b6e5-0ae4-4de4-b119-4e33891bd63f in series "Series 1" of library 3a5a1c7c-a914-472e-88b0-b871ceae63e7'))).to.be.true + expect(loggerInfoStub.getCall(5).calledWith(sinon.match('[2.15.0 migration] Deduplication complete'))).to.be.true + expect(loggerInfoStub.getCall(6).calledWith(sinon.match('[2.15.0 migration] Added unique index on Series.name and Series.libraryId'))).to.be.true + expect(loggerInfoStub.getCall(7).calledWith(sinon.match('[2.15.0 migration] UPGRADE END: 2.15.0-series-column-unique '))).to.be.true // validate rows const series = await queryInterface.sequelize.query('SELECT "id", "name", "libraryId" FROM Series', { type: queryInterface.sequelize.QueryTypes.SELECT }) expect(series).to.have.length(1) @@ -275,14 +275,14 @@ describe('migration-v2.14.0-series-column-unique', () => { await up({ context: { queryInterface, logger: Logger } }) expect(loggerInfoStub.callCount).to.equal(8) - expect(loggerInfoStub.getCall(0).calledWith(sinon.match('[2.14.0 migration] UPGRADE BEGIN: 2.14.0-series-column-unique '))).to.be.true - expect(loggerInfoStub.getCall(1).calledWith(sinon.match('[2.14.0 migration] Found 1 duplicate series'))).to.be.true - expect(loggerInfoStub.getCall(2).calledWith(sinon.match('[2.14.0 migration] Deduplicating series "Series 1" in library 3a5a1c7c-a914-472e-88b0-b871ceae63e7'))).to.be.true - expect(loggerInfoStub.getCall(3).calledWith(sinon.match('[2.14.0 migration] Deduplicating bookId 4a38b6e5-0ae4-4de4-b119-4e33891bd63f in series "Series 1" of library 3a5a1c7c-a914-472e-88b0-b871ceae63e7'))).to.be.true - expect(loggerInfoStub.getCall(4).calledWith(sinon.match('[2.14.0 migration] Finished cleanup of bookId 4a38b6e5-0ae4-4de4-b119-4e33891bd63f in series "Series 1" of library 3a5a1c7c-a914-472e-88b0-b871ceae63e7'))).to.be.true - expect(loggerInfoStub.getCall(5).calledWith(sinon.match('[2.14.0 migration] Deduplication complete'))).to.be.true - expect(loggerInfoStub.getCall(6).calledWith(sinon.match('[2.14.0 migration] Added unique index on Series.name and Series.libraryId'))).to.be.true - expect(loggerInfoStub.getCall(7).calledWith(sinon.match('[2.14.0 migration] UPGRADE END: 2.14.0-series-column-unique '))).to.be.true + expect(loggerInfoStub.getCall(0).calledWith(sinon.match('[2.15.0 migration] UPGRADE BEGIN: 2.15.0-series-column-unique '))).to.be.true + expect(loggerInfoStub.getCall(1).calledWith(sinon.match('[2.15.0 migration] Found 1 duplicate series'))).to.be.true + expect(loggerInfoStub.getCall(2).calledWith(sinon.match('[2.15.0 migration] Deduplicating series "Series 1" in library 3a5a1c7c-a914-472e-88b0-b871ceae63e7'))).to.be.true + expect(loggerInfoStub.getCall(3).calledWith(sinon.match('[2.15.0 migration] Deduplicating bookId 4a38b6e5-0ae4-4de4-b119-4e33891bd63f in series "Series 1" of library 3a5a1c7c-a914-472e-88b0-b871ceae63e7'))).to.be.true + expect(loggerInfoStub.getCall(4).calledWith(sinon.match('[2.15.0 migration] Finished cleanup of bookId 4a38b6e5-0ae4-4de4-b119-4e33891bd63f in series "Series 1" of library 3a5a1c7c-a914-472e-88b0-b871ceae63e7'))).to.be.true + expect(loggerInfoStub.getCall(5).calledWith(sinon.match('[2.15.0 migration] Deduplication complete'))).to.be.true + expect(loggerInfoStub.getCall(6).calledWith(sinon.match('[2.15.0 migration] Added unique index on Series.name and Series.libraryId'))).to.be.true + expect(loggerInfoStub.getCall(7).calledWith(sinon.match('[2.15.0 migration] UPGRADE END: 2.15.0-series-column-unique '))).to.be.true // validate rows const series = await queryInterface.sequelize.query('SELECT "id", "name", "libraryId" FROM Series', { type: queryInterface.sequelize.QueryTypes.SELECT }) expect(series).to.have.length(1) @@ -319,14 +319,14 @@ describe('migration-v2.14.0-series-column-unique', () => { await down({ context: { queryInterface, logger: Logger } }) expect(loggerInfoStub.callCount).to.equal(8) - expect(loggerInfoStub.getCall(0).calledWith(sinon.match('[2.14.0 migration] UPGRADE BEGIN: 2.14.0-series-column-unique '))).to.be.true - expect(loggerInfoStub.getCall(1).calledWith(sinon.match('[2.14.0 migration] Found 0 duplicate series'))).to.be.true - expect(loggerInfoStub.getCall(2).calledWith(sinon.match('[2.14.0 migration] Deduplication complete'))).to.be.true - expect(loggerInfoStub.getCall(3).calledWith(sinon.match('[2.14.0 migration] Added unique index on Series.name and Series.libraryId'))).to.be.true - expect(loggerInfoStub.getCall(4).calledWith(sinon.match('[2.14.0 migration] UPGRADE END: 2.14.0-series-column-unique '))).to.be.true - expect(loggerInfoStub.getCall(5).calledWith(sinon.match('[2.14.0 migration] DOWNGRADE BEGIN: 2.14.0-series-column-unique '))).to.be.true - expect(loggerInfoStub.getCall(6).calledWith(sinon.match('[2.14.0 migration] Removed unique index on Series.name and Series.libraryId'))).to.be.true - expect(loggerInfoStub.getCall(7).calledWith(sinon.match('[2.14.0 migration] DOWNGRADE END: 2.14.0-series-column-unique '))).to.be.true + expect(loggerInfoStub.getCall(0).calledWith(sinon.match('[2.15.0 migration] UPGRADE BEGIN: 2.15.0-series-column-unique '))).to.be.true + expect(loggerInfoStub.getCall(1).calledWith(sinon.match('[2.15.0 migration] Found 0 duplicate series'))).to.be.true + expect(loggerInfoStub.getCall(2).calledWith(sinon.match('[2.15.0 migration] Deduplication complete'))).to.be.true + expect(loggerInfoStub.getCall(3).calledWith(sinon.match('[2.15.0 migration] Added unique index on Series.name and Series.libraryId'))).to.be.true + expect(loggerInfoStub.getCall(4).calledWith(sinon.match('[2.15.0 migration] UPGRADE END: 2.15.0-series-column-unique '))).to.be.true + expect(loggerInfoStub.getCall(5).calledWith(sinon.match('[2.15.0 migration] DOWNGRADE BEGIN: 2.15.0-series-column-unique '))).to.be.true + expect(loggerInfoStub.getCall(6).calledWith(sinon.match('[2.15.0 migration] Removed unique index on Series.name and Series.libraryId'))).to.be.true + expect(loggerInfoStub.getCall(7).calledWith(sinon.match('[2.15.0 migration] DOWNGRADE END: 2.15.0-series-column-unique '))).to.be.true // Ensure index does not exist const indexes = await queryInterface.showIndex('Series') expect(indexes).to.not.deep.include({ tableName: 'Series', unique: true, fields: ['name', 'libraryId'], name: 'unique_series_name_per_library' }) From f98f78a5bd2673f1cc62e798f18b09fc16f35491 Mon Sep 17 00:00:00 2001 From: Nicholas Wallace Date: Thu, 10 Oct 2024 21:14:51 -0700 Subject: [PATCH 15/45] Podcast search strings --- client/pages/library/_library/podcast/search.vue | 10 +++++----- client/strings/en-us.json | 5 +++++ 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/client/pages/library/_library/podcast/search.vue b/client/pages/library/_library/podcast/search.vue index b80ca2f8f..983b9025c 100644 --- a/client/pages/library/_library/podcast/search.vue +++ b/client/pages/library/_library/podcast/search.vue @@ -5,7 +5,7 @@
- + {{ $strings.ButtonSubmit }} @@ -108,7 +108,7 @@ export default { if (!txt || !txt.includes(' tag not found OR an tag was not found') + this.$toast.error(this.$strings.MessageTaskOpmlParseFastFail) this.processing = false return } @@ -117,7 +117,7 @@ export default { .$post(`/api/podcasts/opml/parse`, { opmlText: txt }) .then((data) => { if (!data.feeds?.length) { - this.$toast.error('No feeds found in OPML file') + this.$toast.error(this.$strings.MessageTaskOpmlParseNoneFound) } else { this.opmlFeeds = data.feeds || [] this.showOPMLFeedsModal = true @@ -125,7 +125,7 @@ export default { }) .catch((error) => { console.error('Failed', error) - this.$toast.error('Failed to parse OPML file') + this.$toast.error(this.$strings.MessageTaskOpmlParseFailed) }) .finally(() => { this.processing = false @@ -191,7 +191,7 @@ export default { return } if (!podcast.feedUrl) { - this.$toast.error('Invalid podcast - no feed') + this.$toast.error(this.$strings.MessageNoPodcastFeed) return } this.processing = true diff --git a/client/strings/en-us.json b/client/strings/en-us.json index 34b014dcf..bbc29146b 100644 --- a/client/strings/en-us.json +++ b/client/strings/en-us.json @@ -746,6 +746,7 @@ "MessageNoLogs": "No Logs", "MessageNoMediaProgress": "No Media Progress", "MessageNoNotifications": "No Notifications", + "MessageNoPodcastFeed": "Invalid podcast: No Feed", "MessageNoPodcastsFound": "No podcasts found", "MessageNoResults": "No Results", "MessageNoSearchResultsFor": "No search results for \"{0}\"", @@ -762,6 +763,7 @@ "MessagePlaylistCreateFromCollection": "Create playlist from collection", "MessagePleaseWait": "Please wait...", "MessagePodcastHasNoRSSFeedForMatching": "Podcast has no RSS feed url to use for matching", + "MessagePodcastSearchField": "Enter search term or RSS feed URL", "MessageQuickMatchDescription": "Populate empty item details & cover with first match result from '{0}'. Does not overwrite details unless 'Prefer matched metadata' server setting is enabled.", "MessageRemoveChapter": "Remove chapter", "MessageRemoveEpisodes": "Remove {0} episode(s)", @@ -804,6 +806,9 @@ "MessageTaskOpmlImportFeedPodcastExists": "Podcast already exists at path", "MessageTaskOpmlImportFeedPodcastFailed": "Failed to create podcast", "MessageTaskOpmlImportFinished": "Added {0} podcasts", + "MessageTaskOpmlParseFailed": "Failed to parse OPML file", + "MessageTaskOpmlParseFastFail": "Invalid OPML file tag not found OR an tag was not found", + "MessageTaskOpmlParseNoneFound": "No feeds found in OPML file", "MessageTaskScanItemsAdded": "{0} added", "MessageTaskScanItemsMissing": "{0} missing", "MessageTaskScanItemsUpdated": "{0} updated", From 29db5f199095693522cf2e715ebd9c5eab944ae4 Mon Sep 17 00:00:00 2001 From: Nicholas Wallace Date: Thu, 10 Oct 2024 21:21:15 -0700 Subject: [PATCH 16/45] Update: tools strings --- client/components/modals/item/tabs/Tools.vue | 8 ++++---- client/strings/en-us.json | 3 +++ 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/client/components/modals/item/tabs/Tools.vue b/client/components/modals/item/tabs/Tools.vue index ad9435c43..5f148c3d3 100644 --- a/client/components/modals/item/tabs/Tools.vue +++ b/client/components/modals/item/tabs/Tools.vue @@ -33,18 +33,18 @@ launch - Quick Embed + {{ $strings.ButtonQuickEmbed }}
-

Queued for metadata embed ({{ queuedEmbedLIds.length }} in queue)

+

{{ $getString('MessageQuickEmbedQueue', [queuedEmbedLIds.length]) }}

-

Currently embedding metadata

+

{{ $strings.MessageQuickEmbedInProgress }}

@@ -113,7 +113,7 @@ export default { methods: { quickEmbed() { const payload = { - message: 'Warning! Quick embed will not backup your audio files. Make sure that you have a backup of your audio files.

Would you like to continue?', + message: this.$strings.MessageConfirmQuickEmbed, callback: (confirmed) => { if (confirmed) { this.$axios diff --git a/client/strings/en-us.json b/client/strings/en-us.json index bbc29146b..cd8f76849 100644 --- a/client/strings/en-us.json +++ b/client/strings/en-us.json @@ -66,6 +66,7 @@ "ButtonPurgeItemsCache": "Purge Items Cache", "ButtonQueueAddItem": "Add to queue", "ButtonQueueRemoveItem": "Remove from queue", + "ButtonQuickEmbed": "Quick Embed", "ButtonQuickEmbedMetadata": "Quick Embed Metadata", "ButtonQuickMatch": "Quick Match", "ButtonReScan": "Re-Scan", @@ -764,6 +765,8 @@ "MessagePleaseWait": "Please wait...", "MessagePodcastHasNoRSSFeedForMatching": "Podcast has no RSS feed url to use for matching", "MessagePodcastSearchField": "Enter search term or RSS feed URL", + "MessageQuickEmbedInProgress": "Quick embed in progress", + "MessageQuickEmbedQueue": "Queued for quick embed ({0} in queue)", "MessageQuickMatchDescription": "Populate empty item details & cover with first match result from '{0}'. Does not overwrite details unless 'Prefer matched metadata' server setting is enabled.", "MessageRemoveChapter": "Remove chapter", "MessageRemoveEpisodes": "Remove {0} episode(s)", From c33b470fcaf660eb7a59be57dc55587230b00e57 Mon Sep 17 00:00:00 2001 From: Nicholas Wallace Date: Thu, 10 Oct 2024 21:58:17 -0700 Subject: [PATCH 17/45] Tools Manager strings --- client/pages/audiobook/_id/manage.vue | 34 +++++++++++++-------------- client/strings/en-us.json | 16 +++++++++++++ 2 files changed, 33 insertions(+), 17 deletions(-) diff --git a/client/pages/audiobook/_id/manage.vue b/client/pages/audiobook/_id/manage.vue index 7de82b510..fb5ccc294 100644 --- a/client/pages/audiobook/_id/manage.vue +++ b/client/pages/audiobook/_id/manage.vue @@ -63,11 +63,11 @@
-

Audiobook is queued for metadata embed ({{ queuedEmbedLIds.length }} in queue)

+

{{ $getString('MessageEmbedQueue', [queuedEmbedLIds.length]) }}

- +
@@ -78,7 +78,7 @@
@@ -94,11 +94,11 @@
- - - + + +
-

Warning: Do not update these settings unless you are familiar with ffmpeg encoding options.

+

{{ $strings.LabelEncodingWarningAdvancedSettings }}

@@ -106,36 +106,36 @@
star -

Metadata will be embedded in the audio tracks inside your audiobook folder.

+

{{ $strings.LabelEncodingInfoEmbedded }}

star

- Finished M4B will be put into your audiobook folder at .../{{ libraryItemRelPath }}/. + {{ $strings.LabelEncodingFinishedM4B }} .../{{ libraryItemRelPath }}/.

star

- A backup of your original audio files will be stored in /metadata/cache/items/{{ libraryItemId }}/. Make sure to periodically purge items cache. + {{ $strings.LabelEncodingBackupLocation }} /metadata/cache/items/{{ libraryItemId }}/. {{ $strings.LabelEncodingClearItemCache }}

star -

Chapters are not embedded in multi-track audiobooks.

+

{{ $strings.LabelEncodingChaptersNotEmbedded }}

star -

Encoding can take up to 30 minutes.

+

{{ $strings.LabelEncodingTimeWarning }}

star -

If you have the watcher disabled you will need to re-scan this audiobook afterwards.

+

{{ $strings.LabelEncodingWatcherDisabled }}

star -

Once the task is started you can navigate away from this page.

+

{{ $strings.LabelEncodingStartedNavigation }}

@@ -269,11 +269,11 @@ export default { }, availableTools() { if (this.isSingleM4b) { - return [{ value: 'embed', text: 'Embed Metadata' }] + return [{ value: 'embed', text: this.$strings.LabelToolsEmbedMetadata }] } else { return [ - { value: 'embed', text: 'Embed Metadata' }, - { value: 'm4b', text: 'M4B Encoder' } + { value: 'embed', text: this.$strings.LabelToolsEmbedMetadata }, + { value: 'm4b', text: this.$strings.LabelToolsM4bEncoder } ] } }, diff --git a/client/strings/en-us.json b/client/strings/en-us.json index cd8f76849..4370851d4 100644 --- a/client/strings/en-us.json +++ b/client/strings/en-us.json @@ -226,6 +226,9 @@ "LabelAllUsersIncludingGuests": "All users including guests", "LabelAlreadyInYourLibrary": "Already in your library", "LabelAppend": "Append", + "LabelAudioBitrate": "Audio Bitrate (e.g. 128k)", + "LabelAudioChannels": "Audio Channels (1 or 2)", + "LabelAudioCodec": "Audio Codec", "LabelAuthor": "Author", "LabelAuthorFirstLast": "Author (First Last)", "LabelAuthorLastFirst": "Author (Last, First)", @@ -238,6 +241,7 @@ "LabelAutoRegister": "Auto Register", "LabelAutoRegisterDescription": "Automatically create new users after logging in", "LabelBackToUser": "Back to User", + "LabelBackupAudioFiles": "Backup Audio Files", "LabelBackupLocation": "Backup Location", "LabelBackupsEnableAutomaticBackups": "Enable automatic backups", "LabelBackupsEnableAutomaticBackupsHelp": "Backups saved in /metadata/backups", @@ -304,6 +308,15 @@ "LabelEmailSettingsTestAddress": "Test Address", "LabelEmbeddedCover": "Embedded Cover", "LabelEnable": "Enable", + "LabelEncodingBackupLocation": "A backup of your original audio files will be stored in:", + "LabelEncodingChaptersNotEmbedded": "Chapters are not embedded in multi-track audiobooks.", + "LabelEncodingClearItemCache": "Make sure to periodically purge items cache.", + "LabelEncodingFinishedM4B": "Finished M4B will be put into your audiobook folder at:", + "LabelEncodingInfoEmbedded": "Metadata will be embedded in the audio tracks inside your audiobook folder.", + "LabelEncodingStartedNavigation": "Once the task is started you can navigate away from this page.", + "LabelEncodingTimeWarning": "Encoding can take up to 30 minutes.", + "LabelEncodingWarningAdvancedSettings": "Warning: Do not update these settings unless you are familiar with ffmpeg encoding options.", + "LabelEncodingWatcherDisabled": "If you have the watcher disabled you will need to re-scan this audiobook afterwards.", "LabelEnd": "End", "LabelEndOfChapter": "End of Chapter", "LabelEpisode": "Episode", @@ -597,6 +610,7 @@ "LabelTitle": "Title", "LabelToolsEmbedMetadata": "Embed Metadata", "LabelToolsEmbedMetadataDescription": "Embed metadata into audio files including cover image and chapters.", + "LabelToolsM4bEncoder": "M4B Encoder", "LabelToolsMakeM4b": "Make M4B Audiobook File", "LabelToolsMakeM4bDescription": "Generate a .M4B audiobook file with embedded metadata, cover image, and chapters.", "LabelToolsSplitM4b": "Split M4B to MP3's", @@ -622,6 +636,7 @@ "LabelUploaderDragAndDrop": "Drag & drop files or folders", "LabelUploaderDropFiles": "Drop files", "LabelUploaderItemFetchMetadataHelp": "Automatically fetch title, author, and series", + "LabelUseAdvancedOptions": "Use Advanced Options", "LabelUseChapterTrack": "Use chapter track", "LabelUseFullTrack": "Use full track", "LabelUser": "User", @@ -703,6 +718,7 @@ "MessageDragFilesIntoTrackOrder": "Drag files into correct track order", "MessageEmbedFailed": "Embed Failed!", "MessageEmbedFinished": "Embed Finished!", + "MessageEmbedQueue": "Queued for metadata embed ({0} in queue)", "MessageEpisodesQueuedForDownload": "{0} Episode(s) queued for download", "MessageEreaderDevices": "To ensure delivery of ebooks, you may need to add the above email address as a valid sender for each device listed below.", "MessageFeedURLWillBe": "Feed URL will be {0}", From 8a20510cde0286eca1790210c471d5bdb59ec2c5 Mon Sep 17 00:00:00 2001 From: Nicholas Wallace Date: Thu, 10 Oct 2024 22:12:31 -0700 Subject: [PATCH 18/45] Localize: subtitle `books` --- client/components/cards/LazyBookCard.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/components/cards/LazyBookCard.vue b/client/components/cards/LazyBookCard.vue index ae2cdd5b7..09424b3c6 100644 --- a/client/components/cards/LazyBookCard.vue +++ b/client/components/cards/LazyBookCard.vue @@ -325,7 +325,7 @@ export default { }, displaySubtitle() { if (!this.libraryItem) return '\u00A0' - if (this.collapsedSeries) return this.collapsedSeries.numBooks === 1 ? '1 book' : `${this.collapsedSeries.numBooks} books` + if (this.collapsedSeries) return `${this.collapsedSeries.numBooks} ${this.$strings.LabelBooks}` if (this.mediaMetadata.subtitle) return this.mediaMetadata.subtitle if (this.mediaMetadata.seriesName) return this.mediaMetadata.seriesName return '' From 33eae1e03ac5c0f0033d0b1614e11f39f6a34ea2 Mon Sep 17 00:00:00 2001 From: advplyr Date: Fri, 11 Oct 2024 16:55:09 -0500 Subject: [PATCH 19/45] Fix:Server crash on podcast add page, adds API endpoint to get podcast titles #3499 - Instead of loading all podcast library items this page now loads only the needed data --- .../widgets/AlreadyInLibraryIndicator.vue | 7 +--- .../pages/library/_library/podcast/search.vue | 12 +++--- server/controllers/LibraryController.js | 38 +++++++++++++++++++ server/routers/ApiRouter.js | 1 + 4 files changed, 47 insertions(+), 11 deletions(-) diff --git a/client/components/widgets/AlreadyInLibraryIndicator.vue b/client/components/widgets/AlreadyInLibraryIndicator.vue index 14f2143d4..cb6bfb30c 100644 --- a/client/components/widgets/AlreadyInLibraryIndicator.vue +++ b/client/components/widgets/AlreadyInLibraryIndicator.vue @@ -1,14 +1,11 @@