Skip to content
Open
Show file tree
Hide file tree
Changes from 22 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
c8f5bcd
init
CD-Z Feb 27, 2026
a3c7b57
fixed paged
CD-Z Feb 27, 2026
6da8ca0
init
CD-Z Apr 7, 2026
9b41dc3
feat: add zustand dependency and persistence key contract
CD-Z Apr 7, 2026
4ce7f22
refactor: extract bootstrap data loading into reusable service
CD-Z Mar 25, 2026
0dde694
refactor: move chapter mutations into store-ready action helpers
CD-Z Mar 25, 2026
b2b1a26
feat: add zustand novel store with cache and core actions
CD-Z Mar 25, 2026
4820a2a
refactor: bridge novel persistence contracts for migration safety
CD-Z Mar 25, 2026
9538c06
refactor: migrate NovelScreen domain flows to zustand selectors
CD-Z Mar 25, 2026
05f4ae3
refactor: migrate NovelScreenList to selector-based store access
CD-Z Mar 25, 2026
a3c4d1b
refactor: move reader chapter flows onto store boundaries
CD-Z Mar 25, 2026
e1315de
refactor: decouple useNovelSettings from broad context domain state
CD-Z Mar 25, 2026
55fff01
refactor: align migrateNovel with stable persistence contracts
CD-Z Mar 25, 2026
a6bf396
refactor: cut novel-reader consumers to store-only context boundary
CD-Z Apr 7, 2026
1e0c847
refactor: retire legacy useNovel and route cache cleanup export
CD-Z Apr 7, 2026
3830b61
test: update suites for store-only context boundary cutover
CD-Z Mar 25, 2026
6a34fe9
test: modernize store-era mocks and add contract coverage
CD-Z Apr 7, 2026
626cd54
test: finalize Task-15 sweep—remove dead useNovelData and lint clear …
CD-Z Mar 25, 2026
0548be9
remove imports from NovelScreen
CD-Z Mar 26, 2026
8eb37ec
reworked ai output
CD-Z Mar 27, 2026
354c638
improvements
CD-Z Mar 28, 2026
db25722
implemented synchronus novel and chapter fetch
CD-Z Apr 10, 2026
e9015ad
refactor tests
CD-Z Apr 11, 2026
1ecd3f3
fix db tests
CD-Z Apr 11, 2026
2a6e43b
Update remaining tests.
CD-Z Apr 11, 2026
5af4698
Harden chapter actions and bootstrap flows
CD-Z Apr 11, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions __mocks__/react-native-nitro-modules.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,26 @@ jest.mock('react-native-nitro-modules', () => ({
// Return a mock object that won't be used since MMKV has its own mock
return {};
}),
];

module.exports = {
MMKV,
useMMKVString: createTupleHook(key => strings.get(key)),
useMMKVNumber: createTupleHook(key => numbers.get(key)),
useMMKVBoolean: createTupleHook(key => booleans.get(key)),
useMMKVObject: createTupleHook(key => {
const json = strings.get(key);
if (!json) {
return undefined;
}

try {
return JSON.parse(json);
} catch {
return undefined;
}
}),
NitroModules: {
createHybridObject: jest.fn(() => ({})),
},
}));
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,8 @@
"react-native-worklets": "^0.8.1",
"react-native-zip-archive": "^7.0.2",
"sanitize-html": "^2.17.2",
"urlencode": "^2.0.0"
"urlencode": "^2.0.0",
"zustand": "^5.0.12"
Comment on lines +121 to +122
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

What is the latest version of zustand npm package?

💡 Result:

The latest version of the zustand npm package is 5.0.12.

Citations:


🌐 Web query:

Zustand 5.0.12 React 19 compatibility

💡 Result:

Zustand 5.0.12 is not officially compatible with React 19. The peer dependency for React is specified as ">=18.0.0", which does not include React 19. Official documentation and changelogs for v5.0.12 do not mention React 19 support or updates to peer dependencies. Early discussions (e.g., Nov 2024) show peer dependency conflicts with React 19 RCs when using npm strict mode, requiring --force or --legacy-peer-deps. While some tools like DepFixer claim compatibility for earlier 5.x versions and Zustand works in practice due to minimal React API usage, strict compatibility requires peer deps alignment, which is absent. For React 19, follow best practices like using selectors with 'use' prefix and useShallow for destructuring to work with React Compiler.

Citations:


Zustand 5.0.12 is not officially compatible with React 19.

Zustand 5.0.12 is the latest stable version, but its peer dependency specifies >=18.0.0, explicitly excluding React 19. Using it with React 19 will require --force or --legacy-peer-deps flags during installation. If this project targets React 19, either upgrade Zustand once React 19 support is released or retain React 18 compatibility.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@package.json` around lines 117 - 118, The package.json currently lists
"zustand": "^5.0.12" which declares a peer dependency excluding React 19; update
package configuration to avoid broken installs by either (A) pinning the project
React peer/dependency to a 18.x range (so keep "zustand": "^5.0.12") or (B) if
you must target React 19, remove or delay the dependency upgrade and add a TODO
to upgrade "zustand" to a version that advertises React 19 support once released
(or use an explicit override/resolution only as a temporary measure); adjust the
package.json "react"/"react-dom" entries or add a comment/issue referencing
"zustand": "^5.0.12" to document the chosen approach.

},
"devDependencies": {
"@babel/core": "^7.29.0",
Expand Down
27 changes: 27 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

45 changes: 42 additions & 3 deletions src/database/manager/manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,9 +71,9 @@ class DbManager implements IDbManager {
>;
}

public async allSync<T extends ExecutableSelect>(
public allSync<T extends ExecutableSelect>(
query: T,
): Promise<Awaited<ReturnType<T['all']>>> {
): Awaited<ReturnType<T['all']>> {
const { sql: sqlString, params } = query.toSQL();
return db.executeSync(sqlString, params as any[]).rows as Awaited<
ReturnType<T['all']>
Expand Down Expand Up @@ -135,6 +135,40 @@ type FireOn = Array<{ table: TableNames; ids?: number[] }>;
export function useLiveQuery<T extends ExecutableSelect>(
query: T,
fireOn: FireOn,
callback?: (data: Awaited<ReturnType<T['all']>>) => void,
) {
type ReturnValue = Awaited<ReturnType<T['all']>>;

const { sql: sqlString, params } = query.toSQL();
const paramsKey = JSON.stringify(params);
const fireOnKey = JSON.stringify(fireOn);

const [data, setData] = useState<ReturnValue>(() => {
const r = db.executeSync(sqlString, params as any[]).rows as ReturnValue;
if (callback) callback(r);
return r;
Comment on lines +146 to +149
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, find and read the file
wc -l src/database/manager/manager.ts

Repository: CD-Z/lnreader

Length of output: 92


🏁 Script executed:

# Read the file around the mentioned line ranges
cat -n src/database/manager/manager.ts | sed -n '140,200p'

Repository: CD-Z/lnreader

Length of output: 2251


🏁 Script executed:

cat -n src/database/manager/manager.ts | sed -n '135,170p'

Repository: CD-Z/lnreader

Length of output: 1346


🏁 Script executed:

cat -n src/database/manager/manager.ts | sed -n '168,202p'

Repository: CD-Z/lnreader

Length of output: 1293


Make callback delivery lifecycle-safe.

The callback paths have two React-specific correctness issues: the initial calls are triggered from render (useState initializers on lines 146-149 and 179-185), and later reactive updates keep stale callback closures because callback is excluded from both dependency arrays (lines 152-164 and 187-199) with exhaustive-deps suppressed. This means callbacks can run before the component commits, still fire after an abandoned/unmounted async render, or invoke stale logic on later updates. Keep the latest callback in a useRef and invoke it only from committed effects/subscriptions.

Minimal pattern
-import { useEffect, useState } from 'react';
+import { useEffect, useRef, useState } from 'react';

 export function useLiveQuery<T extends ExecutableSelect>(
   query: T,
   fireOn: FireOn,
   callback?: (data: Awaited<ReturnType<T['all']>>) => void,
 ) {
   type ReturnValue = Awaited<ReturnType<T['all']>>;
+  const callbackRef = useRef(callback);
+
+  useEffect(() => {
+    callbackRef.current = callback;
+  }, [callback]);

-  const [data, setData] = useState<ReturnValue>(() => {
-    const r = db.executeSync(sqlString, params as any[]).rows as ReturnValue;
-    if (callback) callback(r);
-    return r;
-  });
+  const [data, setData] = useState<ReturnValue>(
+    () => db.executeSync(sqlString, params as any[]).rows as ReturnValue,
+  );
+
+  useEffect(() => {
+    callbackRef.current?.(data);
+  }, []);

   useEffect(() => {
     const unsub = db.reactiveExecute({
       query: sqlString,
       arguments: params as any[],
       fireOn,
       callback: (result: { rows: ReturnValue }) => {
         setData(result.rows);
-        if (callback) callback(result.rows);
+        callbackRef.current?.(result.rows);
       },
     });

Apply the same callbackRef.current pattern to useLiveQueryAsync, and move the initial async callback into an effect with cancellation so it can't fire after unmount.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/database/manager/manager.ts` around lines 146 - 149, The current code
calls the provided callback directly from the useState initializer and from
effects with callback excluded from deps, causing render-time invocation and
stale/unsafe closures; fix this by storing the latest callback in a ref (e.g.
callbackRef.current) and updating it inside an effect, remove direct calls to
callback from the useState initializer (do not call callback from the
db.executeSync initialiser), and invoke callbackRef.current only from committed
effects/subscriptions (and from the live query subscription handlers) so calls
are cancelled on unmount; apply the same pattern to useLiveQueryAsync by moving
the initial async callback into an effect with cancellation and using
callbackRef.current for all subsequent invocations to avoid stale closures.

});

useEffect(() => {
const unsub = db.reactiveExecute({
query: sqlString,
arguments: params as any[],
fireOn,
callback: (result: { rows: ReturnValue }) => {
setData(result.rows);
if (callback) callback(result.rows);
},
});
return unsub;
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [sqlString, paramsKey, fireOnKey]);

return data;
}
export function useLiveQueryAsync<T extends ExecutableSelect>(
query: T,
fireOn: FireOn,
callback?: (data: Awaited<ReturnType<T['all']>>) => void,
) {
type ReturnValue = Awaited<ReturnType<T['all']>>;

Expand All @@ -143,7 +177,11 @@ export function useLiveQuery<T extends ExecutableSelect>(
const fireOnKey = JSON.stringify(fireOn);

const [data, setData] = useState<ReturnValue>(
() => db.executeSync(sqlString, params as any[]).rows as ReturnValue,
() =>
db.execute(sqlString, params as any[]).then(result => {
callback?.(result.rows as ReturnValue);
return result.rows;
}) as ReturnValue,
);
Comment on lines 179 to 185
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Critical: useState initializer returns a Promise, not the resolved value.

The useState initializer returns a Promise (db.execute(...).then(...)), which means React stores the Promise object itself as state, not the resolved data. This will cause type errors and rendering issues since data will be a Promise, not ReturnValue.

🐛 Suggested fix: Use null/undefined initial state and load asynchronously
-  const [data, setData] = useState<ReturnValue>(
-    () =>
-      db.execute(sqlString, params as any[]).then(result => {
-        callback?.(result.rows as ReturnValue);
-        return result.rows;
-      }) as ReturnValue,
-  );
+  const [data, setData] = useState<ReturnValue | null>(null);
+
+  useEffect(() => {
+    let cancelled = false;
+    db.execute(sqlString, params as any[]).then(result => {
+      if (!cancelled) {
+        setData(result.rows as ReturnValue);
+        callback?.(result.rows as ReturnValue);
+      }
+    });
+    return () => { cancelled = true; };
+    // eslint-disable-next-line react-hooks/exhaustive-deps
+  }, [sqlString, paramsKey]);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/database/manager/manager.ts` around lines 179 - 185, The state
initializer currently returns a Promise (db.execute(...).then(...)) so React
stores a Promise instead of the resolved ReturnValue; change to initialize data
to a safe empty value (null/undefined or empty array matching ReturnValue) and
move the db.execute call into an async effect: in a useEffect tied to
sqlString/params call db.execute(sqlString, params), await the result, then call
setData(result.rows as ReturnValue) and callback?.(result.rows), and handle
component unmount (cancellation flag) to avoid setting state after unmount;
update references to useState, setData, db.execute, callback, sqlString and
params accordingly.


useEffect(() => {
Expand All @@ -153,6 +191,7 @@ export function useLiveQuery<T extends ExecutableSelect>(
fireOn,
callback: (result: { rows: ReturnValue }) => {
setData(result.rows);
if (callback) callback(result.rows);
},
});
return unsub;
Expand Down
95 changes: 69 additions & 26 deletions src/database/queries/ChapterQueries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -300,23 +300,61 @@ export const clearUpdates = async (): Promise<void> => {
// #endregion
// #region Selectors

export const getCustomPages = async (novelId: number) => {
return await dbManager
.selectDistinct({ page: chapterSchema.page })
.from(chapterSchema)
.where(eq(chapterSchema.novelId, novelId))
.orderBy(asc(castInt(chapterSchema.page)))
.all();
export const getCustomPages = (novelId: number) => {
return dbManager.allSync(
dbManager
.selectDistinct({ page: chapterSchema.page })
.from(chapterSchema)
.where(eq(chapterSchema.novelId, novelId))
.orderBy(asc(castInt(chapterSchema.page))),
);
};
Comment on lines +303 to 311
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Sync change may cause test inconsistency.

The function now returns synchronously, but the test file (per context snippet at ChapterQueries.test.ts:566-577) uses await getCustomPages(novelId). While awaiting a non-Promise value works in JavaScript, this inconsistency should be addressed in tests for clarity.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/database/queries/ChapterQueries.ts` around lines 303 - 311, The
getCustomPages function was changed to return synchronously causing mismatch
with tests that await its result; restore an async contract by making
getCustomPages return a Promise (e.g., wrap the dbManager.allSync result in
Promise.resolve or switch back to the async dbManager.all call) so callers using
await (tests referencing getCustomPages) remain correct—update the
implementation in getCustomPages to return a Promise while keeping the same
result shape.


export const getNovelChapters = async (
novelId: number,
sort?: ChapterOrderKey,
filter?: ChapterFilterKey[],
page?: string,
limit: number = 1000,
): Promise<ChapterInfo[]> =>
dbManager
.select()
.from(chapterSchema)
.where(eq(chapterSchema.novelId, novelId));
.where(
and(
eq(chapterSchema.novelId, novelId),
!page ? sql.raw('true') : eq(chapterSchema.page, page),
chapterFilterToSQL(filter),
),
)
.orderBy(chapterOrderToSQL(sort))
.limit(limit)
.all();

export const getNovelChaptersSync = (
novelId: number,
sort?: ChapterOrderKey,
filter?: ChapterFilterKey[],
page?: string,
limit: number = 1000,
): ChapterInfo[] =>
dbManager.allSync(
dbManager
.select()
.from(chapterSchema)
.where(
and(
eq(chapterSchema.novelId, novelId),
!page ? sql.raw('true') : eq(chapterSchema.page, page),
chapterFilterToSQL(filter),
),
)
.orderBy(chapterOrderToSQL(sort))
.limit(limit), // Adding a limit to prevent potential performance issues with large datasets
);
/**
* @deprecated, use getNovelChapters with whereConditions instead
*/
export const getUnreadNovelChapters = async (
novelId: number,
): Promise<ChapterInfo[]> =>
Expand All @@ -326,7 +364,9 @@ export const getUnreadNovelChapters = async (
.where(
and(eq(chapterSchema.novelId, novelId), eq(chapterSchema.unread, true)),
);

/**
* @deprecated, use getNovelChapters with whereConditions instead
*/
export const getAllUndownloadedChapters = async (
novelId: number,
): Promise<ChapterInfo[]> =>
Expand All @@ -339,7 +379,9 @@ export const getAllUndownloadedChapters = async (
eq(chapterSchema.isDownloaded, false),
),
);

/**
* @deprecated, use getNovelChapters with whereConditions instead
*/
export const getAllUndownloadedAndUnreadChapters = async (
novelId: number,
): Promise<ChapterInfo[]> =>
Expand Down Expand Up @@ -408,8 +450,8 @@ export const getPageChaptersBatched = async (
page?: string,
batch: number = 0,
) => {
const limit = 300;
const offset = 300 * batch;
const limit = 1000;
const offset = 1000 * batch;
const query = dbManager
.select()
.from(chapterSchema)
Expand Down Expand Up @@ -451,20 +493,21 @@ export const getFirstUnreadChapter = (
filter?: ChapterFilterKey[],
page?: string,
) =>
dbManager
.select()
.from(chapterSchema)
.where(
and(
eq(chapterSchema.novelId, novelId),
eq(chapterSchema.page, page || '1'),
eq(chapterSchema.unread, true),
chapterFilterToSQL(filter),
),
)
.orderBy(asc(chapterSchema.position))
.limit(1)
.get();
dbManager.getSync(
dbManager
.select()
.from(chapterSchema)
.where(
and(
eq(chapterSchema.novelId, novelId),
eq(chapterSchema.page, page || '1'),
eq(chapterSchema.unread, true),
chapterFilterToSQL(filter),
),
)
.orderBy(asc(chapterSchema.position))
.limit(1),
);

export const getNovelChaptersByName = async (
novelId: number,
Expand Down
17 changes: 6 additions & 11 deletions src/database/queries/NovelQueries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { insertChapters } from './ChapterQueries';

import { showToast } from '@utils/showToast';
import { getString } from '@strings/translations';
import { BackupNovel, NovelInfo } from '../types';
import { BackupNovel, DBNovelInfo, NovelInfo } from '../types';
import { SourceNovel } from '@plugins/types';
import { NOVEL_STORAGE } from '@utils/Storages';
import { downloadFile } from '@plugins/helpers/fetch';
Expand Down Expand Up @@ -82,21 +82,16 @@ export const getAllNovels = async (): Promise<NovelInfo[]> => {
return dbManager.select().from(novelSchema).all();
};

export const getNovelById = async (
novelId: number,
): Promise<NovelInfo | undefined> => {
const res = dbManager
.select()
.from(novelSchema)
.where(eq(novelSchema.id, novelId))
.get();
return res;
export const getNovelById = (novelId: number): DBNovelInfo | undefined => {
return dbManager.getSync(
dbManager.select().from(novelSchema).where(eq(novelSchema.id, novelId)),
);
};

export const getNovelByPath = (
novelPath: string,
pluginId: string,
): NovelInfo | undefined => {
): DBNovelInfo | undefined => {
const res = dbManager.getSync(
dbManager
.select()
Expand Down
26 changes: 26 additions & 0 deletions src/database/utils/filter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,12 @@ const FILTER_STATES = {
} as const;
export type FilterStates = typeof FILTER_STATES;

export type FilterObject = {
unread?: boolean;
isDownloaded?: boolean;
bookmark?: boolean;
};

export class ChapterFilterObject {
private filter: Map<
ChapterFilterPositiveKey,
Expand Down Expand Up @@ -51,6 +57,26 @@ export class ChapterFilterObject {
return res as ChapterFilterKey[];
}

toFilterObject(): FilterObject {
const result: FilterObject = {};
for (const [key, value] of this.filter.entries()) {
if (value === FILTER_STATES.OFF) continue;

switch (key) {
case 'read':
result.unread = value !== FILTER_STATES.ON;
break;
case 'downloaded':
result.isDownloaded = value === FILTER_STATES.ON;
break;
case 'bookmarked':
result.bookmark = value === FILTER_STATES.ON;
break;
}
}
return result;
}
Comment on lines +60 to +78
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Find usages of toFilterObject to understand expected behavior
rg -n "toFilterObject" --type ts

Repository: CD-Z/lnreader

Length of output: 123


toFilterObject() method is never called and has incomplete logic for INDETERMINATE states.

The method is defined but unused in the codebase. When present, it has an asymmetry: the read filter always sets a value (inverting INDETERMINATE to unread=true), but downloaded and bookmarked filters only set values when state is ON, leaving them undefined for INDETERMINATE.

If this method is meant to be used, either:

  1. Remove the method if it's unused dead code, or
  2. Fix the logic to consistently handle INDETERMINATE:
case 'downloaded':
-  result.isDownloaded = value === FILTER_STATES.ON;
+  if (value !== FILTER_STATES.OFF) {
+    result.isDownloaded = value === FILTER_STATES.ON;
+  }
  break;
case 'bookmarked':
-  result.bookmark = value === FILTER_STATES.ON;
+  if (value !== FILTER_STATES.OFF) {
+    result.bookmark = value === FILTER_STATES.ON;
+  }
  break;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
toFilterObject(): FilterObject {
const result: FilterObject = {};
for (const [key, value] of this.filter.entries()) {
if (value === FILTER_STATES.OFF) continue;
switch (key) {
case 'read':
result.unread = value !== FILTER_STATES.ON;
break;
case 'downloaded':
result.isDownloaded = value === FILTER_STATES.ON;
break;
case 'bookmarked':
result.bookmark = value === FILTER_STATES.ON;
break;
}
}
return result;
}
toFilterObject(): FilterObject {
const result: FilterObject = {};
for (const [key, value] of this.filter.entries()) {
if (value === FILTER_STATES.OFF) continue;
switch (key) {
case 'read':
result.unread = value !== FILTER_STATES.ON;
break;
case 'downloaded':
if (value !== FILTER_STATES.OFF) {
result.isDownloaded = value === FILTER_STATES.ON;
}
break;
case 'bookmarked':
if (value !== FILTER_STATES.OFF) {
result.bookmark = value === FILTER_STATES.ON;
}
break;
}
}
return result;
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/database/utils/filter.ts` around lines 60 - 78, toFilterObject currently
contains asymmetrical handling of FILTER_STATES (it inverts 'read' for
INDETERMINATE but only sets 'downloaded'/'bookmarked' when ON) and is unused;
either remove it or make its logic consistent: if you keep toFilterObject,
change the switch for keys 'read','downloaded','bookmarked' to explicitly handle
FILTER_STATES.ON, FILTER_STATES.OFF and FILTER_STATES.INDETERMINATE (set result
fields to true/false for ON/OFF and leave them undefined or set to undefined for
INDETERMINATE), using the FILTER_STATES enum and returning a proper
FilterObject; alternatively delete toFilterObject to remove dead code.


set(key: ChapterFilterPositiveKey, value: keyof typeof FILTER_STATES) {
this.filter.set(key, FILTER_STATES[value]);
this.setState([...this.toArray()]);
Expand Down
3 changes: 2 additions & 1 deletion src/database/utils/parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ import {
} from '@database/constants';
import { SQL, sql } from 'drizzle-orm';

export function chapterOrderToSQL(order: ChapterOrderKey) {
export function chapterOrderToSQL(order?: ChapterOrderKey) {
if (!order) return sql.raw(CHAPTER_ORDER.positionAsc);
const o = CHAPTER_ORDER[order] ?? CHAPTER_ORDER.positionAsc;
return sql.raw(o);
}
Expand Down
Loading
Loading