fix: Include All Chapters Beyond the 1000-Row UI Limit (#1938)
* chapter backup and restore fix * Update .gitignore Co-authored-by: Rajarshee Chatterjee <rajarshee.adm@gmail.com> * test: update backup mocks for getAllNovelChaptersForBackup --------- Co-authored-by: Rajarshee Chatterjee <rajarshee.adm@gmail.com>
This commit is contained in:
@@ -427,6 +427,23 @@ export const getNovelChapters = async (
|
||||
.all();
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns every chapter row for a novel.
|
||||
*
|
||||
* This is intentionally separate from getNovelChapters, whose default limit is
|
||||
* used to keep interactive screens responsive. Backup creation must never use
|
||||
* a paginated UI query because doing so silently produces incomplete backups.
|
||||
*/
|
||||
export const getAllNovelChaptersForBackup = async (
|
||||
novelId: number,
|
||||
): Promise<ChapterInfo[]> =>
|
||||
dbManager
|
||||
.select()
|
||||
.from(chapterSchema)
|
||||
.where(eq(chapterSchema.novelId, novelId))
|
||||
.orderBy(asc(chapterSchema.id))
|
||||
.all();
|
||||
|
||||
export const getNovelChaptersSync = (
|
||||
novelId: number,
|
||||
sort?: ChapterOrderKey,
|
||||
|
||||
@@ -18,6 +18,7 @@ import {
|
||||
markAllChaptersRead,
|
||||
markAllChaptersUnread,
|
||||
getNovelChapters,
|
||||
getAllNovelChaptersForBackup,
|
||||
getUnreadNovelChapters,
|
||||
getAllUndownloadedChapters,
|
||||
getAllUndownloadedAndUnreadChapters,
|
||||
@@ -128,6 +129,33 @@ describe('ChapterQueries', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('getAllNovelChaptersForBackup', () => {
|
||||
it('returns chapters beyond the 1000-row UI query limit', async () => {
|
||||
const testDb = getTestDb();
|
||||
// No Novel row is needed for this selector test. Keeping the id orphaned
|
||||
// also avoids running aggregate-stat triggers 1001 times during setup.
|
||||
const novelId = 123456;
|
||||
|
||||
const values = Array.from(
|
||||
{ length: 1001 },
|
||||
(_, position) =>
|
||||
`(${novelId}, '/chapter/${position + 1}', ` +
|
||||
`'Chapter ${position + 1}', ${position + 1}, '1', ${position})`,
|
||||
).join(',');
|
||||
testDb.sqlite.executeSync(
|
||||
`INSERT INTO Chapter
|
||||
(novelId, path, name, chapterNumber, page, position)
|
||||
VALUES ${values}`,
|
||||
);
|
||||
|
||||
expect(await getNovelChapters(novelId)).toHaveLength(1000);
|
||||
|
||||
const backupChapters = await getAllNovelChaptersForBackup(novelId);
|
||||
expect(backupChapters).toHaveLength(1001);
|
||||
expect(backupChapters.at(-1)?.name).toBe('Chapter 1001');
|
||||
});
|
||||
});
|
||||
|
||||
describe('markChaptersRead', () => {
|
||||
it('should mark multiple chapters as read', async () => {
|
||||
const testDb = getTestDb();
|
||||
|
||||
@@ -81,6 +81,7 @@ export const testModules = {
|
||||
'clearUpdates',
|
||||
'getCustomPages',
|
||||
'getNovelChapters',
|
||||
'getAllNovelChaptersForBackup',
|
||||
'getUnreadNovelChapters',
|
||||
'getAllUndownloadedChapters',
|
||||
'getAllUndownloadedAndUnreadChapters',
|
||||
|
||||
@@ -2,7 +2,7 @@ import {
|
||||
_restoreNovelAndChapters,
|
||||
getAllNovels,
|
||||
} from '@database/queries/NovelQueries';
|
||||
import { getNovelChapters } from '@database/queries/ChapterQueries';
|
||||
import { getAllNovelChaptersForBackup } from '@database/queries/ChapterQueries';
|
||||
import {
|
||||
_restoreCategory,
|
||||
getAllNovelCategories,
|
||||
@@ -19,7 +19,7 @@ jest.mock('@database/queries/NovelQueries', () => ({
|
||||
}));
|
||||
|
||||
jest.mock('@database/queries/ChapterQueries', () => ({
|
||||
getNovelChapters: jest.fn(),
|
||||
getAllNovelChaptersForBackup: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('@database/queries/CategoryQueries', () => ({
|
||||
@@ -74,7 +74,7 @@ describe('selective backup data', () => {
|
||||
jest.mocked(NativeFile.mkdir).mockResolvedValue(undefined);
|
||||
jest.mocked(NativeFile.writeFile).mockResolvedValue(undefined);
|
||||
jest.mocked(getAllNovels).mockResolvedValue([]);
|
||||
jest.mocked(getNovelChapters).mockResolvedValue([]);
|
||||
jest.mocked(getAllNovelChaptersForBackup).mockResolvedValue([]);
|
||||
jest.mocked(getCategoriesFromDb).mockResolvedValue([]);
|
||||
jest.mocked(getAllNovelCategories).mockResolvedValue([]);
|
||||
});
|
||||
@@ -139,7 +139,7 @@ describe('selective backup data', () => {
|
||||
cover: 'file:///storage/Novels/source/1/cover.png',
|
||||
},
|
||||
]);
|
||||
jest.mocked(getNovelChapters).mockResolvedValueOnce([
|
||||
jest.mocked(getAllNovelChaptersForBackup).mockResolvedValueOnce([
|
||||
{
|
||||
id: 10,
|
||||
novelId: 1,
|
||||
@@ -147,7 +147,7 @@ describe('selective backup data', () => {
|
||||
name: 'Chapter 1',
|
||||
isDownloaded: true,
|
||||
},
|
||||
] as Awaited<ReturnType<typeof getNovelChapters>>);
|
||||
] as Awaited<ReturnType<typeof getAllNovelChaptersForBackup>>);
|
||||
|
||||
await prepareBackupData('/cache', {
|
||||
library: true,
|
||||
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
_restoreNovelAndChapters,
|
||||
getAllNovels,
|
||||
} from '@database/queries/NovelQueries';
|
||||
import { getNovelChapters } from '@database/queries/ChapterQueries';
|
||||
import { getAllNovelChaptersForBackup } from '@database/queries/ChapterQueries';
|
||||
import {
|
||||
_restoreCategory,
|
||||
getAllNovelCategories,
|
||||
@@ -104,7 +104,7 @@ export const prepareBackupData = async (
|
||||
await getAllNovels().then(async novels => {
|
||||
for (const novel of novels) {
|
||||
try {
|
||||
const chapters = await getNovelChapters(novel.id);
|
||||
const chapters = await getAllNovelChaptersForBackup(novel.id);
|
||||
const backedUpChapters = options.downloadedFiles
|
||||
? chapters
|
||||
: chapters.map(chapter => ({
|
||||
|
||||
Reference in New Issue
Block a user