import { Database, QueryResult, RunResult, SQLiteValue } from 'node-sqlite3-wasm'; import System from "../System.js"; export interface SeriesResult extends Record { series_id: string; user_id: string; name: string; hashed_name: string; description: string | null; cover_image: string | null; last_update: number; } export interface SeriesBookResult extends Record { series_id: string; book_id: string; book_order: number; title: string; cover_image: string | null; } export interface SeriesListItem extends Record { series_id: string; name: string; description: string | null; cover_image: string | null; book_count: number; book_ids: string | null; } export interface SeriesTableResult extends Record { series_id: string; user_id: string; name: string; hashed_name: string; description: string | null; cover_image: string | null; last_update: number; } export interface SeriesBooksTableResult extends Record { series_id: string; book_id: string; book_order: number; last_update: number; } export interface SyncedSeriesResult extends Record { series_id: string; name: string; description: string | null; last_update: number; } export interface SyncedSeriesBookResult extends Record { series_id: string; book_id: string; book_order: number; last_update: number; } export default class SeriesRepo { /** * Fetches all series for a user. * @param userId - The unique identifier of the user * @param lang - The language for error messages ('fr' or 'en') * @returns An array of series with book counts */ public static fetchUserSeries(userId: string, lang: 'fr' | 'en' = 'fr'): SeriesListItem[] { try { const db: Database = System.getDb(); const query: string = 'SELECT series.series_id, series.name, series.description, series.cover_image, COUNT(series_books.book_id) AS book_count, GROUP_CONCAT(series_books.book_id) AS book_ids FROM book_series series LEFT JOIN series_books ON series.series_id = series_books.series_id WHERE series.user_id = ? GROUP BY series.series_id, series.last_update ORDER BY series.last_update DESC'; const series: SeriesListItem[] = db.all(query, [userId]) as SeriesListItem[]; return series; } catch (error: unknown) { if (error instanceof Error) { console.error(`DB Error: ${error.message}`); throw new Error(lang === 'fr' ? `Impossible de récupérer les séries.` : `Unable to retrieve series.`); } else { console.error("An unknown error occurred."); throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); } } } /** * Fetches a single series by its ID. * @param userId - The unique identifier of the user * @param seriesId - The unique identifier of the series * @param lang - The language for error messages ('fr' or 'en') * @returns The series result or null if not found */ public static fetchSeriesById(userId: string, seriesId: string, lang: 'fr' | 'en' = 'fr'): SeriesResult | null { try { const db: Database = System.getDb(); const query: string = 'SELECT series_id, user_id, name, hashed_name, description, cover_image, last_update FROM book_series WHERE series_id = ? AND user_id = ?'; const series: SeriesResult | undefined = db.get(query, [seriesId, userId]) as SeriesResult | undefined; return series || null; } catch (error: unknown) { if (error instanceof Error) { console.error(`DB Error: ${error.message}`); throw new Error(lang === 'fr' ? `Impossible de récupérer la série.` : `Unable to retrieve series.`); } else { console.error("An unknown error occurred."); throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); } } } /** * Inserts a new series. * @param seriesId - The unique identifier for the new series * @param userId - The unique identifier of the user * @param name - The encrypted name * @param hashedName - The hashed name for duplicate detection * @param description - The encrypted description (nullable) * @param lang - The language for error messages ('fr' or 'en') * @returns The series ID if successful */ public static insertSeries(seriesId: string, userId: string, name: string, hashedName: string, description: string | null, lang: 'fr' | 'en' = 'fr'): string { let insertResult: RunResult; try { const db: Database = System.getDb(); const query: string = 'INSERT INTO book_series (series_id, user_id, name, hashed_name, description, last_update) VALUES (?, ?, ?, ?, ?, ?)'; const params: SQLiteValue[] = [seriesId, userId, name, hashedName, description, System.timeStampInSeconds()]; insertResult = db.run(query, params); } catch (error: unknown) { if (error instanceof Error) { console.error(`DB Error: ${error.message}`); throw new Error(lang === 'fr' ? `Impossible de créer la série.` : `Unable to create series.`); } else { console.error("An unknown error occurred."); throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); } } if (!insertResult || insertResult.changes === 0) { throw new Error(lang === 'fr' ? `Une erreur s'est produite lors de la création de la série.` : `Error creating series.`); } return seriesId; } /** * Updates an existing series. * @param userId - The unique identifier of the user * @param seriesId - The unique identifier of the series * @param name - The encrypted name * @param hashedName - The hashed name * @param description - The encrypted description (nullable) * @param lang - The language for error messages ('fr' or 'en') * @returns True if the update was successful */ public static updateSeries(userId: string, seriesId: string, name: string, hashedName: string, description: string | null, lang: 'fr' | 'en' = 'fr'): boolean { try { const db: Database = System.getDb(); const query: string = 'UPDATE book_series SET name = ?, hashed_name = ?, description = ?, last_update = ? WHERE series_id = ? AND user_id = ?'; const params: SQLiteValue[] = [name, hashedName, description, System.timeStampInSeconds(), seriesId, userId]; const updateResult: RunResult = db.run(query, params); return updateResult.changes > 0; } catch (error: unknown) { if (error instanceof Error) { console.error(`DB Error: ${error.message}`); throw new Error(lang === 'fr' ? `Impossible de mettre à jour la série.` : `Unable to update series.`); } else { console.error("An unknown error occurred."); throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); } } } /** * Deletes a series. * @param userId - The unique identifier of the user * @param seriesId - The unique identifier of the series * @param lang - The language for error messages ('fr' or 'en') * @returns True if the deletion was successful */ public static deleteSeries(userId: string, seriesId: string, lang: 'fr' | 'en' = 'fr'): boolean { try { const db: Database = System.getDb(); const query: string = 'DELETE FROM book_series WHERE series_id = ? AND user_id = ?'; const params: SQLiteValue[] = [seriesId, userId]; const deleteResult: RunResult = db.run(query, params); return deleteResult.changes > 0; } catch (error: unknown) { if (error instanceof Error) { console.error(`DB Error: ${error.message}`); throw new Error(lang === 'fr' ? `Impossible de supprimer la série.` : `Unable to delete series.`); } else { console.error("An unknown error occurred."); throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); } } } /** * Fetches all books in a series with their order. * @param userId - The unique identifier of the user * @param seriesId - The unique identifier of the series * @param lang - The language for error messages ('fr' or 'en') * @returns An array of books in the series */ public static fetchSeriesBooks(userId: string, seriesId: string, lang: 'fr' | 'en' = 'fr'): SeriesBookResult[] { try { const db: Database = System.getDb(); const query: string = 'SELECT sb.series_id, sb.book_id, sb.book_order, b.title, b.cover_image FROM series_books sb INNER JOIN erit_books b ON sb.book_id = b.book_id WHERE sb.series_id = ? AND b.author_id = ? ORDER BY sb.book_order'; const books: SeriesBookResult[] = db.all(query, [seriesId, userId]) as SeriesBookResult[]; return books; } catch (error: unknown) { if (error instanceof Error) { console.error(`DB Error: ${error.message}`); throw new Error(lang === 'fr' ? `Impossible de récupérer les livres de la série.` : `Unable to retrieve series books.`); } else { console.error("An unknown error occurred."); throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); } } } /** * Adds a book to a series. * @param seriesId - The unique identifier of the series * @param bookId - The unique identifier of the book * @param bookOrder - The order of the book in the series * @param lang - The language for error messages ('fr' or 'en') * @returns True if the addition was successful */ public static addBookToSeries(seriesId: string, bookId: string, bookOrder: number, lang: 'fr' | 'en' = 'fr'): boolean { try { const db: Database = System.getDb(); const query: string = 'INSERT INTO series_books (series_id, book_id, book_order, last_update) VALUES (?, ?, ?, ?) ON CONFLICT(series_id, book_id) DO UPDATE SET book_order = excluded.book_order, last_update = excluded.last_update'; const params: SQLiteValue[] = [seriesId, bookId, bookOrder, System.timeStampInSeconds()]; const insertResult: RunResult = db.run(query, params); return insertResult.changes > 0; } catch (error: unknown) { if (error instanceof Error) { console.error(`DB Error: ${error.message}`); throw new Error(lang === 'fr' ? `Impossible d'ajouter le livre à la série.` : `Unable to add book to series.`); } else { console.error("An unknown error occurred."); throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); } } } /** * Removes a book from a series. * @param seriesId - The unique identifier of the series * @param bookId - The unique identifier of the book * @param lang - The language for error messages ('fr' or 'en') * @returns True if the removal was successful */ public static removeBookFromSeries(seriesId: string, bookId: string, lang: 'fr' | 'en' = 'fr'): boolean { try { const db: Database = System.getDb(); const query: string = 'DELETE FROM series_books WHERE series_id = ? AND book_id = ?'; const params: SQLiteValue[] = [seriesId, bookId]; const deleteResult: RunResult = db.run(query, params); return deleteResult.changes > 0; } catch (error: unknown) { if (error instanceof Error) { console.error(`DB Error: ${error.message}`); throw new Error(lang === 'fr' ? `Impossible de retirer le livre de la série.` : `Unable to remove book from series.`); } else { console.error("An unknown error occurred."); throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); } } } /** * Updates the order of books in a series. * @param seriesId - The unique identifier of the series * @param booksOrder - An array of {bookId, order} objects * @param lang - The language for error messages ('fr' or 'en') * @returns True if the update was successful */ public static updateBooksOrder(seriesId: string, booksOrder: {bookId: string, order: number}[], lang: 'fr' | 'en' = 'fr'): boolean { try { const db: Database = System.getDb(); const timestamp: number = System.timeStampInSeconds(); for (const bookOrder of booksOrder) { const query: string = 'UPDATE series_books SET book_order = ?, last_update = ? WHERE series_id = ? AND book_id = ?'; db.run(query, [bookOrder.order, timestamp, seriesId, bookOrder.bookId]); } return true; } catch (error: unknown) { if (error instanceof Error) { console.error(`DB Error: ${error.message}`); throw new Error(lang === 'fr' ? `Impossible de réordonner les livres.` : `Unable to reorder books.`); } else { console.error("An unknown error occurred."); throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); } } } /** * Checks if a series exists for a user. * @param userId - The unique identifier of the user * @param seriesId - The unique identifier of the series * @param lang - The language for error messages ('fr' or 'en') * @returns True if the series exists */ public static isSeriesExist(userId: string, seriesId: string, lang: 'fr' | 'en' = 'fr'): boolean { try { const db: Database = System.getDb(); const query: string = 'SELECT 1 FROM book_series WHERE series_id = ? AND user_id = ?'; const params: SQLiteValue[] = [seriesId, userId]; const result: QueryResult | null = db.get(query, params); return result !== null; } catch (error: unknown) { if (error instanceof Error) { console.error(`DB Error: ${error.message}`); throw new Error(lang === 'fr' ? `Impossible de vérifier l'existence de la série.` : `Unable to check series existence.`); } else { console.error("An unknown error occurred."); throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); } } } /** * Gets the series ID for a book if it belongs to one. * @param bookId - The unique identifier of the book * @param lang - The language for error messages ('fr' or 'en') * @returns The series ID or null */ public static getSeriesIdForBook(bookId: string, lang: 'fr' | 'en' = 'fr'): string | null { try { const db: Database = System.getDb(); const query: string = 'SELECT series_id FROM series_books WHERE book_id = ?'; const result = db.get(query, [bookId]) as { series_id: string } | undefined; return result ? result.series_id : null; } catch (error: unknown) { if (error instanceof Error) { console.error(`DB Error: ${error.message}`); throw new Error(lang === 'fr' ? `Impossible de vérifier la série du livre.` : `Unable to check book series.`); } else { console.error("An unknown error occurred."); throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); } } } /** * Fetches a series table row for sync purposes. * @param userId - The unique identifier of the user * @param seriesId - The unique identifier of the series * @param lang - The language for error messages ('fr' or 'en') * @returns An array containing the series table row */ public static fetchSeriesTableForSync(userId: string, seriesId: string, lang: 'fr' | 'en' = 'fr'): SeriesTableResult[] { try { const db: Database = System.getDb(); const query: string = 'SELECT series_id, user_id, name, hashed_name, description, cover_image, last_update FROM book_series WHERE series_id = ? AND user_id = ?'; const series: SeriesTableResult[] = db.all(query, [seriesId, userId]) as SeriesTableResult[]; return series; } catch (error: unknown) { if (error instanceof Error) { console.error(`DB Error: ${error.message}`); throw new Error(lang === 'fr' ? `Impossible de récupérer la série pour sync.` : `Unable to retrieve series for sync.`); } else { throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); } } } /** * Fetches all series-books relationships for sync. * @param seriesId - The unique identifier of the series * @param lang - The language for error messages ('fr' or 'en') * @returns An array of series-books table rows */ public static fetchSeriesBooksTable(seriesId: string, lang: 'fr' | 'en' = 'fr'): SeriesBooksTableResult[] { try { const db: Database = System.getDb(); const query: string = 'SELECT series_id, book_id, book_order, last_update FROM series_books WHERE series_id = ? ORDER BY book_order'; const books: SeriesBooksTableResult[] = db.all(query, [seriesId]) as SeriesBooksTableResult[]; return books; } catch (error: unknown) { if (error instanceof Error) { console.error(`DB Error: ${error.message}`); throw new Error(lang === 'fr' ? `Impossible de récupérer les livres de la série pour sync.` : `Unable to retrieve series books for sync.`); } else { throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); } } } /** * Fetches all series for a user for sync comparison. * @param userId - The unique identifier of the user * @param lang - The language for error messages ('fr' or 'en') * @returns An array of synced series results */ public static fetchSyncedSeries(userId: string, lang: 'fr' | 'en' = 'fr'): SyncedSeriesResult[] { try { const db: Database = System.getDb(); const query: string = 'SELECT series_id, name, description, last_update FROM book_series WHERE user_id = ? ORDER BY last_update DESC'; const series: SyncedSeriesResult[] = db.all(query, [userId]) as SyncedSeriesResult[]; return series; } catch (error: unknown) { if (error instanceof Error) { console.error(`DB Error: ${error.message}`); throw new Error(lang === 'fr' ? `Impossible de récupérer les séries pour sync.` : `Unable to retrieve series for sync.`); } else { throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); } } } /** * Fetches all series-books relationships for a user for sync comparison. * @param userId - The unique identifier of the user * @param lang - The language for error messages ('fr' or 'en') * @returns An array of synced series book results */ public static fetchSyncedSeriesBooks(userId: string, lang: 'fr' | 'en' = 'fr'): SyncedSeriesBookResult[] { try { const db: Database = System.getDb(); const query: string = 'SELECT sb.series_id, sb.book_id, sb.book_order, sb.last_update FROM series_books sb INNER JOIN book_series bs ON sb.series_id = bs.series_id WHERE bs.user_id = ? ORDER BY sb.book_order'; const books: SyncedSeriesBookResult[] = db.all(query, [userId]) as SyncedSeriesBookResult[]; return books; } catch (error: unknown) { if (error instanceof Error) { console.error(`DB Error: ${error.message}`); throw new Error(lang === 'fr' ? `Impossible de récupérer les livres de séries pour sync.` : `Unable to retrieve series books for sync.`); } else { throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); } } } /** * Fetches a complete series by ID for sync. * @param seriesId - The unique identifier of the series * @param lang - The language for error messages ('fr' or 'en') * @returns An array containing the series */ public static fetchCompleteSeriesById(seriesId: string, lang: 'fr' | 'en' = 'fr'): SeriesTableResult[] { try { const db: Database = System.getDb(); const query: string = 'SELECT series_id, user_id, name, hashed_name, description, cover_image, last_update FROM book_series WHERE series_id = ?'; const series: SeriesTableResult[] = db.all(query, [seriesId]) as SeriesTableResult[]; return series; } catch (error: unknown) { if (error instanceof Error) { console.error(`DB Error: ${error.message}`); throw new Error(lang === 'fr' ? `Impossible de récupérer la série complète.` : `Unable to retrieve complete series.`); } else { throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); } } } /** * Inserts a series for sync purposes. * @param lang - The language for error messages ('fr' or 'en') * @returns True if the insertion was successful */ public static insertSyncSeries(seriesId: string, userId: string, name: string, hashedName: string, description: string | null, coverImage: string | null, lastUpdate: number, lang: 'fr' | 'en' = 'fr'): boolean { try { const db: Database = System.getDb(); const query: string = 'INSERT INTO book_series (series_id, user_id, name, hashed_name, description, cover_image, last_update) VALUES (?, ?, ?, ?, ?, ?, ?) ON CONFLICT(series_id) DO UPDATE SET name = excluded.name, hashed_name = excluded.hashed_name, description = excluded.description, cover_image = excluded.cover_image, last_update = excluded.last_update'; const params: SQLiteValue[] = [seriesId, userId, name, hashedName, description, coverImage, lastUpdate]; const insertResult: RunResult = db.run(query, params); return insertResult.changes > 0; } catch (error: unknown) { if (error instanceof Error) { console.error(`DB Error: ${error.message}`); throw new Error(lang === 'fr' ? `Impossible d'insérer la série pour sync.` : `Unable to insert series for sync.`); } else { throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); } } } /** * Updates a series for sync purposes. * @param lang - The language for error messages ('fr' or 'en') * @returns True if the update was successful */ public static updateSyncSeries(userId: string, seriesId: string, name: string, hashedName: string, description: string | null, coverImage: string | null, lastUpdate: number, lang: 'fr' | 'en' = 'fr'): boolean { try { const db: Database = System.getDb(); const query: string = 'UPDATE book_series SET name = ?, hashed_name = ?, description = ?, cover_image = ?, last_update = ? WHERE series_id = ? AND user_id = ?'; const params: SQLiteValue[] = [name, hashedName, description, coverImage, lastUpdate, seriesId, userId]; const updateResult: RunResult = db.run(query, params); return updateResult.changes > 0; } catch (error: unknown) { if (error instanceof Error) { console.error(`DB Error: ${error.message}`); throw new Error(lang === 'fr' ? `Impossible de mettre à jour la série pour sync.` : `Unable to update series for sync.`); } else { throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); } } } /** * Inserts a series-book relationship for sync. * @param lang - The language for error messages ('fr' or 'en') * @returns True if the insertion was successful */ public static insertSyncSeriesBook(seriesId: string, bookId: string, bookOrder: number, lastUpdate: number, lang: 'fr' | 'en' = 'fr'): boolean { try { const db: Database = System.getDb(); const query: string = 'INSERT INTO series_books (series_id, book_id, book_order, last_update) VALUES (?, ?, ?, ?) ON CONFLICT(series_id, book_id) DO UPDATE SET book_order = excluded.book_order, last_update = excluded.last_update'; const params: SQLiteValue[] = [seriesId, bookId, bookOrder, lastUpdate]; const insertResult: RunResult = db.run(query, params); return insertResult.changes > 0; } catch (error: unknown) { if (error instanceof Error) { console.error(`DB Error: ${error.message}`); throw new Error(lang === 'fr' ? `Impossible d'insérer la liaison série-livre pour sync.` : `Unable to insert series-book for sync.`); } else { throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); } } } /** * Checks if a series-book relationship exists. * @param lang - The language for error messages ('fr' or 'en') * @returns True if the relationship exists */ public static isSeriesBookExist(seriesId: string, bookId: string, lang: 'fr' | 'en' = 'fr'): boolean { try { const db: Database = System.getDb(); const query: string = 'SELECT 1 FROM series_books WHERE series_id = ? AND book_id = ?'; const result: QueryResult | null = db.get(query, [seriesId, bookId]); return result !== null; } catch (error: unknown) { if (error instanceof Error) { console.error(`DB Error: ${error.message}`); throw new Error(lang === 'fr' ? `Impossible de vérifier la liaison série-livre.` : `Unable to check series-book.`); } else { throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); } } } /** * Checks if a series exists for a user (alias for isSeriesExist). * @param userId - The unique identifier of the user * @param seriesId - The unique identifier of the series * @param lang - The language for error messages ('fr' or 'en') * @returns True if the series exists */ public static seriesExists(userId: string, seriesId: string, lang: 'fr' | 'en' = 'fr'): boolean { return this.isSeriesExist(userId, seriesId, lang); } }