diff --git a/server/src/modules/dashboard/dashboard.repository.test.ts b/server/src/modules/dashboard/dashboard.repository.test.ts index fc05b3a13..560d18202 100644 --- a/server/src/modules/dashboard/dashboard.repository.test.ts +++ b/server/src/modules/dashboard/dashboard.repository.test.ts @@ -64,18 +64,22 @@ describe('DashboardRepository', () => { expect(listChain.limit).toHaveBeenCalledWith(2); }); - it('maps continue-reading rows to id list without joining audiobook progress', async () => { + it('maps continue-reading rows to id list and excludes terminal read statuses', async () => { const listChain = makeLimitChain([{ id: 40 }, { id: 9 }]); const db = { select: vi.fn().mockReturnValue(listChain) }; const repo = new DashboardRepository(db as never); const result = await repo.findContinueReadingBookIds([8], 55, 10); + const whereArg = listChain.where.mock.calls[0]?.[0]; + const whereValues = collectValues(whereArg); expect(result).toEqual([40, 9]); - expect(listChain.leftJoin).toHaveBeenCalledTimes(2); + expect(listChain.leftJoin).toHaveBeenCalledTimes(3); expect(listChain.leftJoin.mock.calls[0]?.[0]).toBe(bookFiles); expect(listChain.leftJoin.mock.calls[1]?.[0]).toBe(readingProgress); + expect(listChain.leftJoin.mock.calls[2]?.[0]).toBe(userBookStatus); expect(listChain.leftJoin.mock.calls.some((call) => call[0] === audiobookProgress)).toBe(false); + expect(whereValues).toEqual(expect.arrayContaining(['read', 'skimmed', 'abandoned'])); expect(listChain.innerJoin).not.toHaveBeenCalled(); expect(listChain.orderBy).toHaveBeenCalledTimes(1); expect(listChain.limit).toHaveBeenCalledWith(10); diff --git a/server/src/modules/dashboard/dashboard.repository.ts b/server/src/modules/dashboard/dashboard.repository.ts index b611f4fa1..c1f449fef 100644 --- a/server/src/modules/dashboard/dashboard.repository.ts +++ b/server/src/modules/dashboard/dashboard.repository.ts @@ -11,6 +11,7 @@ import { buildContentFilterClauses } from '../../common/utils/content-filter-sql type Db = NodePgDatabase; type UpNextInSeriesRow = { id: number }; const AUDIO_FORMATS = BOOK_FORMATS.filter(isAudioFormat); +const CONTINUE_READING_EXCLUDED_READ_STATUSES = ['read', 'skimmed', 'abandoned'] as const satisfies readonly ReadStatus[]; const DISCOVERY_EXCLUDED_READ_STATUSES = ['reading', 'rereading', 'on_hold', 'read', 'skimmed', 'abandoned'] as const satisfies readonly ReadStatus[]; @Injectable() @@ -44,12 +45,14 @@ export class DashboardRepository { .from(books) .leftJoin(bookFiles, eq(bookFiles.id, books.primaryFileId)) .leftJoin(readingProgress, and(eq(readingProgress.bookFileId, bookFiles.id), eq(readingProgress.userId, userId))) + .leftJoin(userBookStatus, and(eq(userBookStatus.bookId, books.id), eq(userBookStatus.userId, userId))) .where( and( inArray(books.libraryId, accessibleLibraryIds), eq(books.status, 'present'), or(isNull(bookFiles.format), notInArray(bookFiles.format, AUDIO_FORMATS)), sql`${readingProgress.percentage} > 0 and ${readingProgress.percentage} < 100`, + or(isNull(userBookStatus.bookId), notInArray(userBookStatus.status, [...CONTINUE_READING_EXCLUDED_READ_STATUSES])), ...cfClauses, ), )