From 9314127c56679d480458b397431666871cec3c5d Mon Sep 17 00:00:00 2001 From: Kevin Gatera Date: Tue, 3 Mar 2026 21:37:52 -0500 Subject: [PATCH] prevent discover query crashes from restarting server --- server/utils/queries/libraryFilters.js | 31 +++++++------ .../utils/queries/libraryItemsBookFilters.js | 2 +- .../utils/queries/libraryFilters.test.js | 45 +++++++++++++++++++ .../queries/libraryItemsBookFilters.test.js | 15 +++++++ 4 files changed, 79 insertions(+), 14 deletions(-) create mode 100644 test/server/utils/queries/libraryFilters.test.js diff --git a/server/utils/queries/libraryFilters.js b/server/utils/queries/libraryFilters.js index 49f5381e1..ada2965e5 100644 --- a/server/utils/queries/libraryFilters.js +++ b/server/utils/queries/libraryFilters.js @@ -374,19 +374,24 @@ module.exports = { async getLibraryItemsToDiscover(library, user, include, limit) { if (library.mediaType !== 'book') return { libraryItems: [], count: 0 } - const { libraryItems, count } = await libraryItemsBookFilters.getDiscoverLibraryItems(library.id, user, include, limit) - return { - libraryItems: libraryItems.map((li) => { - const oldLibraryItem = li.toOldJSONMinified() - if (li.rssFeed) { - oldLibraryItem.rssFeed = li.rssFeed.toOldJSONMinified() - } - if (li.mediaItemShare) { - oldLibraryItem.mediaItemShare = li.mediaItemShare - } - return oldLibraryItem - }), - count + try { + const { libraryItems, count } = await libraryItemsBookFilters.getDiscoverLibraryItems(library.id, user, include, limit) + return { + libraryItems: libraryItems.map((li) => { + const oldLibraryItem = li.toOldJSONMinified() + if (li.rssFeed) { + oldLibraryItem.rssFeed = li.rssFeed.toOldJSONMinified() + } + if (li.mediaItemShare) { + oldLibraryItem.mediaItemShare = li.mediaItemShare + } + return oldLibraryItem + }), + count + } + } catch (error) { + Logger.error(`[LibraryFilters] Failed to load discover shelf for library "${library.id}"`, error) + return { libraryItems: [], count: 0 } } }, diff --git a/server/utils/queries/libraryItemsBookFilters.js b/server/utils/queries/libraryItemsBookFilters.js index 0a5a6a4ed..18f0918ad 100644 --- a/server/utils/queries/libraryItemsBookFilters.js +++ b/server/utils/queries/libraryItemsBookFilters.js @@ -942,7 +942,7 @@ module.exports = { const discoverWhere = [ { '$mediaProgresses.isFinished$': { - [Sequelize.Op.or]: [null, 0] + [Sequelize.Op.or]: [null, false] }, '$mediaProgresses.currentTime$': { [Sequelize.Op.or]: [null, 0] diff --git a/test/server/utils/queries/libraryFilters.test.js b/test/server/utils/queries/libraryFilters.test.js new file mode 100644 index 000000000..c88aa63ed --- /dev/null +++ b/test/server/utils/queries/libraryFilters.test.js @@ -0,0 +1,45 @@ +const { expect } = require('chai') +const sinon = require('sinon') + +const Logger = require('../../../../server/Logger') +const libraryFilters = require('../../../../server/utils/queries/libraryFilters') +const libraryItemsBookFilters = require('../../../../server/utils/queries/libraryItemsBookFilters') + +describe('libraryFilters discover shelf resilience', () => { + afterEach(() => { + sinon.restore() + }) + + it('should return empty discover shelf when discover query fails', async () => { + sinon.stub(libraryItemsBookFilters, 'getDiscoverLibraryItems').rejects(new Error('discover failed')) + const errorStub = sinon.stub(Logger, 'error') + + const result = await libraryFilters.getLibraryItemsToDiscover({ mediaType: 'book', id: 'library-1' }, { id: 'user-1' }, [], 10) + + expect(result).to.deep.equal({ libraryItems: [], count: 0 }) + expect(errorStub.calledWithMatch('[LibraryFilters] Failed to load discover shelf for library "library-1"')).to.equal(true) + }) + + it('should keep discover shelf mapping behavior when query succeeds', async () => { + const libraryItem = { + toOldJSONMinified: () => ({ id: 'item-1' }), + rssFeed: { toOldJSONMinified: () => ({ id: 'rss-1' }) }, + mediaItemShare: { id: 'share-1' } + } + sinon.stub(libraryItemsBookFilters, 'getDiscoverLibraryItems').resolves({ + libraryItems: [libraryItem], + count: 1 + }) + + const result = await libraryFilters.getLibraryItemsToDiscover({ mediaType: 'book', id: 'library-1' }, { id: 'user-1' }, [], 10) + + expect(result.count).to.equal(1) + expect(result.libraryItems).to.deep.equal([ + { + id: 'item-1', + rssFeed: { id: 'rss-1' }, + mediaItemShare: { id: 'share-1' } + } + ]) + }) +}) diff --git a/test/server/utils/queries/libraryItemsBookFilters.test.js b/test/server/utils/queries/libraryItemsBookFilters.test.js index 19b6a58bc..9be7baff4 100644 --- a/test/server/utils/queries/libraryItemsBookFilters.test.js +++ b/test/server/utils/queries/libraryItemsBookFilters.test.js @@ -1,5 +1,6 @@ const { expect } = require('chai') const sinon = require('sinon') +const Sequelize = require('sequelize') const Database = require('../../../../server/Database') const Logger = require('../../../../server/Logger') @@ -130,4 +131,18 @@ describe('libraryItemsBookFilters postgres query safety', () => { expect(orderExpression).to.include('"books->bookSeries"."sequence"') }) + + it('should use boolean false for discover not-started media progress on postgres', async () => { + Database.sequelize = createSequelizeStub('postgres', modelStubs) + modelStubs.series.findAll.resolves([]) + modelStubs.book.count.resolves(0) + modelStubs.book.findAll.resolves([]) + + await libraryItemsBookFilters.getDiscoverLibraryItems('library-1', { id: 'user-1', canAccessExplicitContent: true }, [], 10) + + const countOptions = modelStubs.book.count.firstCall.args[0] + const progressFilter = countOptions.where[0]['$mediaProgresses.isFinished$'] + + expect(progressFilter[Sequelize.Op.or]).to.deep.equal([null, false]) + }) })