fix(metadata): omit unknown page counts from file writes

Normalize stored zero values at the shared write payload boundary so every format omits an unknown page count. Align collection repository tests with the query shape left after podcast removal.

Closes #2040
This commit is contained in:
neon
2026-11-01 20:13:34 -07:00
parent 305b6087b0
commit 60a303ac1f
3 changed files with 19 additions and 17 deletions
@@ -104,8 +104,7 @@ describe('CollectionRepository', () => {
const orderBy = vi.fn().mockResolvedValue([{ id: 1, memberCount: 2 }]); const orderBy = vi.fn().mockResolvedValue([{ id: 1, memberCount: 2 }]);
const groupBy = vi.fn().mockReturnValue({ orderBy }); const groupBy = vi.fn().mockReturnValue({ orderBy });
const where = vi.fn().mockReturnValue({ groupBy }); const where = vi.fn().mockReturnValue({ groupBy });
const thirdLeftJoin = vi.fn().mockReturnValue({ where }); const secondLeftJoin = vi.fn().mockReturnValue({ where });
const secondLeftJoin = vi.fn().mockReturnValue({ leftJoin: thirdLeftJoin });
const firstLeftJoin = vi.fn().mockReturnValue({ leftJoin: secondLeftJoin }); const firstLeftJoin = vi.fn().mockReturnValue({ leftJoin: secondLeftJoin });
const from = vi.fn().mockReturnValue({ leftJoin: firstLeftJoin }); const from = vi.fn().mockReturnValue({ leftJoin: firstLeftJoin });
db.select.mockReturnValueOnce({ from } as never); db.select.mockReturnValueOnce({ from } as never);
@@ -116,15 +115,13 @@ describe('CollectionRepository', () => {
expect(rows).toEqual([{ id: 1, memberCount: 2 }]); expect(rows).toEqual([{ id: 1, memberCount: 2 }]);
expect(db.select).toHaveBeenCalledWith(expect.objectContaining({ memberCount: expect.anything() })); expect(db.select).toHaveBeenCalledWith(expect.objectContaining({ memberCount: expect.anything() }));
expect(secondLeftJoin).toHaveBeenCalledWith(expect.anything(), expect.objectContaining({ op: 'and' })); expect(secondLeftJoin).toHaveBeenCalledWith(expect.anything(), expect.objectContaining({ op: 'and' }));
expect(thirdLeftJoin).toHaveBeenCalledWith(expect.anything(), expect.objectContaining({ op: 'eq' }));
}); });
it('findAllVisibleForUser keeps public metadata visible while counting only viewer-visible books', async () => { it('findAllVisibleForUser keeps public metadata visible while counting only viewer-visible books', async () => {
const orderBy = vi.fn().mockResolvedValue([{ id: 1, bookCount: 1 }]); const orderBy = vi.fn().mockResolvedValue([{ id: 1, bookCount: 1 }]);
const groupBy = vi.fn().mockReturnValue({ orderBy }); const groupBy = vi.fn().mockReturnValue({ orderBy });
const where = vi.fn().mockReturnValue({ groupBy }); const where = vi.fn().mockReturnValue({ groupBy });
const thirdLeftJoin = vi.fn().mockReturnValue({ where }); const secondLeftJoin = vi.fn().mockReturnValue({ where });
const secondLeftJoin = vi.fn().mockReturnValue({ leftJoin: thirdLeftJoin });
const firstLeftJoin = vi.fn().mockReturnValue({ leftJoin: secondLeftJoin }); const firstLeftJoin = vi.fn().mockReturnValue({ leftJoin: secondLeftJoin });
const from = vi.fn().mockReturnValue({ leftJoin: firstLeftJoin }); const from = vi.fn().mockReturnValue({ leftJoin: firstLeftJoin });
db.select.mockReturnValueOnce({ from } as never); db.select.mockReturnValueOnce({ from } as never);
@@ -134,8 +131,8 @@ describe('CollectionRepository', () => {
expect(rows).toEqual([{ id: 1, bookCount: 1 }]); expect(rows).toEqual([{ id: 1, bookCount: 1 }]);
expect(where).toHaveBeenCalledWith( expect(where).toHaveBeenCalledWith(
expect.objectContaining({ expect.objectContaining({
op: 'and', op: 'or',
clauses: expect.arrayContaining([expect.objectContaining({ op: 'or' }), expect.objectContaining({ op: 'eq', right: 'books' })]), clauses: expect.arrayContaining([expect.objectContaining({ op: 'eq', right: 5 }), expect.objectContaining({ op: 'eq', right: true })]),
}), }),
); );
expect(secondLeftJoin).toHaveBeenCalledWith(expect.anything(), expect.objectContaining({ op: 'and' })); expect(secondLeftJoin).toHaveBeenCalledWith(expect.anything(), expect.objectContaining({ op: 'and' }));
@@ -145,8 +142,7 @@ describe('CollectionRepository', () => {
const limit = vi.fn().mockResolvedValue([{ id: 10, bookCount: 1 }]); const limit = vi.fn().mockResolvedValue([{ id: 10, bookCount: 1 }]);
const groupBy = vi.fn().mockReturnValue({ limit }); const groupBy = vi.fn().mockReturnValue({ limit });
const where = vi.fn().mockReturnValue({ groupBy }); const where = vi.fn().mockReturnValue({ groupBy });
const thirdLeftJoin = vi.fn().mockReturnValue({ where }); const secondLeftJoin = vi.fn().mockReturnValue({ where });
const secondLeftJoin = vi.fn().mockReturnValue({ leftJoin: thirdLeftJoin });
const firstLeftJoin = vi.fn().mockReturnValue({ leftJoin: secondLeftJoin }); const firstLeftJoin = vi.fn().mockReturnValue({ leftJoin: secondLeftJoin });
const from = vi.fn().mockReturnValue({ leftJoin: firstLeftJoin }); const from = vi.fn().mockReturnValue({ leftJoin: firstLeftJoin });
db.select.mockReturnValueOnce({ from } as never); db.select.mockReturnValueOnce({ from } as never);
@@ -156,13 +152,7 @@ describe('CollectionRepository', () => {
expect(where).toHaveBeenCalledWith( expect(where).toHaveBeenCalledWith(
expect.objectContaining({ expect.objectContaining({
op: 'and', op: 'and',
clauses: expect.arrayContaining([ clauses: expect.arrayContaining([expect.objectContaining({ op: 'eq', right: 10 }), expect.objectContaining({ op: 'or' })]),
expect.objectContaining({
op: 'and',
clauses: expect.arrayContaining([expect.objectContaining({ op: 'or' })]),
}),
expect.objectContaining({ op: 'eq', right: 'books' }),
]),
}), }),
); );
}); });
@@ -136,6 +136,18 @@ describe('FileWriteRepository', () => {
await expect(repo.loadPayload(11)).resolves.toBeNull(); await expect(repo.loadPayload(11)).resolves.toBeNull();
}); });
it('loadPayload normalizes an unknown zero page count to null', async () => {
const metadataChain = chain([{ title: 'Unknown length', pageCount: 0 }]);
const emptyRelatedRows = chain([]);
const db = {
select: vi.fn().mockReturnValue(emptyRelatedRows).mockReturnValueOnce(metadataChain),
};
const repo = new FileWriteRepository(db as never);
await expect(repo.loadPayload(12)).resolves.toMatchObject({ pageCount: null });
});
it('loadPayload maps metadata, authors, genres, and tags', async () => { it('loadPayload maps metadata, authors, genres, and tags', async () => {
const meta = { const meta = {
title: 'Dune', title: 'Dune',
@@ -276,7 +276,7 @@ export class FileWriteRepository {
publishedDate: meta.publishedDate, publishedDate: meta.publishedDate,
publishedYear: meta.publishedYear, publishedYear: meta.publishedYear,
language: meta.language, language: meta.language,
pageCount: meta.pageCount, pageCount: typeof meta.pageCount === 'number' && meta.pageCount > 0 ? meta.pageCount : null,
seriesName: meta.seriesName, seriesName: meta.seriesName,
seriesIndex: meta.seriesIndex, seriesIndex: meta.seriesIndex,
isbn10: meta.isbn10, isbn10: meta.isbn10,