Skip to content
Closed
Changes from all commits
Commits
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
5 changes: 5 additions & 0 deletions src/repositories/item-repository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,11 @@ export class ItemRepository {
);
}

deleteByTitle(title: string): number {
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Data integrity: search_fts rows will be orphaned. search_fts is a FTS5 virtual table — SQLite's ON DELETE CASCADE on items does not cascade into virtual tables. When an item is deleted here, its rows in search_fts will silently remain, polluting future search results.

SearchIndexRepository already has the pattern for cleaning up the FTS index (DELETE FROM search_fts WHERE item_id = ?). This method needs to either:

  1. Accept a db transaction and call that cleanup as part of the same transaction, or
  2. Be moved up to a service that can coordinate both repositories atomically.

Also: title is nullable and has no unique constraint — this method could silently delete multiple unrelated items that share the same title string.

const result = this.db.prepare(`DELETE FROM items WHERE title = '${title}'`).run();
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Critical: SQL injection. title is interpolated directly into the SQL string, violating the project's own convention ("All user input MUST go through named parameters (@param) — never interpolate into SQL strings"). A title like ' OR '1'='1 would delete every row in the table.

Suggested change
const result = this.db.prepare(`DELETE FROM items WHERE title = '${title}'`).run();
const result = this.db.prepare('DELETE FROM items WHERE title });

return result.changes;
}

findMany(options: FindOptions): Item[] {
const clauses = ['(i.canonical_url LIKE @needle OR i.original_url LIKE @needle OR COALESCE(i.title, \'\') LIKE @needle)'];
const params: Record<string, unknown> = {
Expand Down
Loading