diff --git a/app/page.tsx b/app/page.tsx index fba8ee5..8d35966 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -33,6 +33,8 @@ import OfflineProvider from "@/context/OfflineProvider"; import OfflineContext from "@/context/OfflineContext"; import OfflinePinSetup from "@/components/offline/OfflinePinSetup"; import OfflinePinVerify from "@/components/offline/OfflinePinVerify"; +import {SyncedBook} from "@/lib/models/SyncedBook"; +import {BooksSyncContext} from "@/context/BooksSyncContext"; const messagesMap = { fr: frMessages, @@ -59,6 +61,13 @@ function ScribeContent() { const [session, setSession] = useState({user: null, accessToken: '', isConnected: false}); const [currentChapter, setCurrentChapter] = useState(undefined); const [currentBook, setCurrentBook] = useState(null); + + const [serverSyncedBooks, setServerSyncedBooks] = useState([]); + const [localSyncedBooks, setLocalSyncedBooks] = useState([]); + const [booksToSyncFromServer, setBooksToSyncFromServer] = useState([]); + const [booksToSyncToServer, setBooksToSyncToServer] = useState([]); + const [serverOnlyBooks, setServerOnlyBooks] = useState([]); + const [localOnlyBooks, setLocalOnlyBooks] = useState([]); const [currentCredits, setCurrentCredits] = useState(160); const [amountSpent, setAmountSpent] = useState(session.user?.aiUsage || 0); @@ -143,6 +152,7 @@ function ScribeContent() { useEffect((): void => { if (session.isConnected) { + getBooks().then() setIsTermsAccepted(session.user?.termsAccepted ?? false); setHomeStepsGuide(User.guideTourDone(session.user?.guideTour ?? [], 'home-basic')); setIsLoading(false); @@ -154,8 +164,46 @@ function ScribeContent() { getLastChapter().then(); } }, [currentBook]); - - // Check for PIN setup after successful connection + + useEffect(():void => { + setBooksToSyncFromServer(serverSyncedBooks.filter((serverBook: SyncedBook):boolean => { + const localBook: SyncedBook | undefined = localSyncedBooks.find((localBook: SyncedBook):boolean => localBook.id === serverBook.id); + console.log('localBook from setBookToSyncFromServer',localBook); + console.log('serverBook from setBookToSyncFromServer',serverBook); + return !localBook || localBook.lastUpdate < serverBook.lastUpdate; + })) + setBooksToSyncToServer(localSyncedBooks.filter((localBook: SyncedBook):boolean => { + const serverBook: SyncedBook | undefined = serverSyncedBooks.find((serverBook: SyncedBook):boolean => serverBook.id === localBook.id); + return !serverBook || serverBook.lastUpdate < localBook.lastUpdate; + })) + setServerOnlyBooks(serverSyncedBooks.filter((serverBook: SyncedBook):boolean => !localSyncedBooks.find((localBook: SyncedBook):boolean => localBook.id === serverBook.id))) + setLocalOnlyBooks(localSyncedBooks.filter((localBook: SyncedBook):boolean => !serverSyncedBooks.find((serverBook: SyncedBook):boolean => serverBook.id === localBook.id))) + }, [localSyncedBooks, serverSyncedBooks]); + + async function getBooks(): Promise { + try { + let localBooksResponse: SyncedBook[] + if (!isCurrentlyOffline()){ + localBooksResponse = await window.electron.invoke('db:books:synced'); + } else { + localBooksResponse = []; + } + const serverBooksResponse: SyncedBook[] = await System.authGetQueryToServer('books/synced', session.accessToken, locale); + if (serverBooksResponse) { + setServerSyncedBooks(serverBooksResponse); + } + if (localBooksResponse) { + setLocalSyncedBooks(localBooksResponse); + } + } catch (e: unknown) { + if (e instanceof Error) { + errorMessage(e.message); + } else { + errorMessage(t("homePage.errors.fetchBooksError")); + } + } + } + useEffect(():void => { async function checkPinSetup() { if (session.isConnected && window.electron) { @@ -179,6 +227,7 @@ function ScribeContent() { } checkPinSetup().then(); + }, [session.isConnected]); async function handlePinVerifySuccess(userId: string): Promise { @@ -186,16 +235,18 @@ function ScribeContent() { try { if (window.electron) { + const storedToken: string | null = await window.electron.getToken(); + const encryptionKey:string|null = await window.electron.getUserEncryptionKey(userId); if (encryptionKey) { await window.electron.dbInitialize(userId, encryptionKey); - + const localUser:UserProps = await window.electron.invoke('db:user:info'); if (localUser && localUser.id) { setSession({ isConnected: true, user: localUser, - accessToken: 'offline', // Special offline token + accessToken: storedToken || '', }); setShowPinVerify(false); setCurrentCredits(localUser.creditsBalance || 0); @@ -225,7 +276,7 @@ function ScribeContent() { async function handleHomeTour(): Promise { try { const response: boolean = await System.authPostToServer('logs/tour', { - plateforme: 'web', + plateforme: 'desktop', tour: 'home-basic' }, session.accessToken, @@ -260,7 +311,6 @@ function ScribeContent() { const user: UserProps = await System.authGetQueryToServer('user/infos', token, locale); if (!user) { errorMessage(t("homePage.errors.userNotFound")); - // Token invalide, supprimer et logout if (window.electron) { await window.electron.removeToken(); window.electron.logout(); @@ -272,18 +322,18 @@ function ScribeContent() { try { const initResult = await window.electron.initUser(user.id); if (!initResult.success) { - console.error('[Page] Failed to initialize user:', initResult.error); - } else { - try { - const offlineStatus = await window.electron.offlineModeGet(); - if (!offlineStatus.hasPin) { - setTimeout(():void => { - setShowPinSetup(true); - }, 2000); - } - } catch (error) { - console.error('[Page] Error checking offline mode:', error); + errorMessage(initResult.error || t("homePage.errors.offlineInitError")); + return; + } + try { + const offlineStatus = await window.electron.offlineModeGet(); + if (!offlineStatus.hasPin) { + setTimeout(():void => { + setShowPinSetup(true); + }, 2000); } + } catch (error) { + console.error('[Page] Error checking offline mode:', error); } } catch (error) { console.error('[Page] Error initializing user:', error); @@ -358,7 +408,6 @@ function ScribeContent() { } catch (error) { console.error('[Auth] Error checking offline mode:', error); } - window.electron.logout(); } } @@ -439,60 +488,62 @@ function ScribeContent() { return ( - - - -
- - - -
- - - -
- -
-
- { - homeStepsGuide && !isCurrentlyOffline() && - setHomeStepsGuide(false)}/> - } - { - !isTermsAccepted && !isCurrentlyOffline() && - } - { - showPinSetup && window.electron && ( - setShowPinSetup(false)} - onSuccess={() => { - setShowPinSetup(false); - console.log('[Page] PIN configured successfully'); - }} - /> - ) - } - { - showPinVerify && window.electron && ( - { - //window.electron.logout(); - }} - /> - ) - } -
-
-
+ + + + +
+ + + +
+ + + +
+ +
+
+ { + homeStepsGuide && !isCurrentlyOffline() && + setHomeStepsGuide(false)}/> + } + { + !isTermsAccepted && !isCurrentlyOffline() && + } + { + showPinSetup && window.electron && ( + setShowPinSetup(false)} + onSuccess={() => { + setShowPinSetup(false); + console.log('[Page] PIN configured successfully'); + }} + /> + ) + } + { + showPinVerify && window.electron && ( + { + //window.electron.logout(); + }} + /> + ) + } +
+
+
+
); } diff --git a/components/SyncBook.tsx b/components/SyncBook.tsx new file mode 100644 index 0000000..8aba075 --- /dev/null +++ b/components/SyncBook.tsx @@ -0,0 +1,138 @@ +import {FontAwesomeIcon} from "@fortawesome/react-fontawesome"; +import {faCloud, faCloudArrowDown, faCloudArrowUp, faSpinner} from "@fortawesome/free-solid-svg-icons"; +import {useTranslations} from "next-intl"; +import {useState, useContext} from "react"; +import OfflineContext, {OfflineContextType} from "@/context/OfflineContext"; +import System from "@/lib/models/System"; +import {SessionContext, SessionContextProps} from "@/context/SessionContext"; +import {LangContext} from "@/context/LangContext"; +import {CompleteBook} from "@/lib/models/Book"; +import {SyncType} from "@/context/BooksSyncContext"; +import {AlertContext, AlertContextProps} from "@/context/AlertContext"; + +interface SyncBookProps { + bookId: string; + status: SyncType; +} + +export default function SyncBook({bookId, status}: SyncBookProps) { + const t = useTranslations(); + const {session} = useContext(SessionContext); + const {lang} = useContext(LangContext); + const {errorMessage} = useContext(AlertContext); + const {isCurrentlyOffline} = useContext(OfflineContext); + const [isLoading, setIsLoading] = useState(false); + const [currentStatus, setCurrentStatus] = useState(status); + + const isOffline: boolean = isCurrentlyOffline(); + + async function upload(): Promise { + // TODO: Implement upload local-only book to server + } + + async function download(): Promise { + try { + const response: CompleteBook = await System.authGetQueryToServer('book/sync/download', session.accessToken, lang, {bookId}); + if (!response) { + errorMessage(t("bookCard.downloadError")); + return; + } + const syncStatus:boolean = await window.electron.invoke('db:book:syncSave', response); + if (!syncStatus) { + errorMessage(t("bookCard.downloadError")); + return; + } + setCurrentStatus('synced'); + } catch (e:unknown) { + if (e instanceof Error) { + errorMessage(e.message); + } else { + errorMessage(t("bookCard.downloadError")); + } + } + } + + async function syncFromServer(): Promise { + // TODO: Implement sync from server (server has newer version) + } + + async function syncToServer(): Promise { + // TODO: Implement sync to server (local has newer version) + } + + if (isLoading) { + return ( +
+ + + +
+ ); + } + + return ( +
+ {/* Fully synced - no action needed */} + {currentStatus === 'synced' && ( + + + + )} + + {/* Local only - can upload to server */} + {currentStatus === 'local-only' && ( + + )} + + {/* Server only - can download to local */} + {currentStatus === 'server-only' && ( + + )} + + {/* Needs to sync from server (server has newer version) */} + {currentStatus === 'to-sync-from-server' && ( + + )} + + {/* Needs to sync to server (local has newer version) */} + {currentStatus === 'to-sync-to-server' && ( + + )} +
+ ); +} diff --git a/components/book/BookCard.tsx b/components/book/BookCard.tsx index df49ea8..e7b51b6 100644 --- a/components/book/BookCard.tsx +++ b/components/book/BookCard.tsx @@ -3,19 +3,22 @@ import {BookProps} from "@/lib/models/Book"; import DeleteBook from "@/components/book/settings/DeleteBook"; import ExportBook from "@/components/ExportBook"; import {useTranslations} from "next-intl"; +import SyncBook from "@/components/SyncBook"; +import {SyncType} from "@/context/BooksSyncContext"; +import {useEffect} from "react"; -export default function BookCard( - { - book, - onClickCallback, - index - }: { - book: BookProps, - onClickCallback: Function; - index: number; - }) { +interface BookCardProps { + book: BookProps; + onClickCallback: (bookId: string) => void; + index: number; + syncStatus: SyncType; +} + +export default function BookCard({book, onClickCallback, index, syncStatus}: BookCardProps) { const t = useTranslations(); - + useEffect(() => { + console.log(syncStatus) + }, [syncStatus]); return (
@@ -66,8 +69,7 @@ export default function BookCard(
- +
diff --git a/components/book/BookList.tsx b/components/book/BookList.tsx index 68ccd0c..d554747 100644 --- a/components/book/BookList.tsx +++ b/components/book/BookList.tsx @@ -14,6 +14,8 @@ import User from "@/lib/models/User"; import {useTranslations} from "next-intl"; import {LangContext, LangContextProps} from "@/context/LangContext"; import OfflineContext, {OfflineContextType} from "@/context/OfflineContext"; +import {BooksSyncContext, BooksSyncContextProps, SyncType} from "@/context/BooksSyncContext"; +import {SyncedBook} from "@/lib/models/SyncedBook"; export default function BookList() { const {session, setSession} = useContext(SessionContext); @@ -23,6 +25,7 @@ export default function BookList() { const t = useTranslations(); const {lang} = useContext(LangContext) const {isCurrentlyOffline} = useContext(OfflineContext) + const {booksToSyncFromServer, booksToSyncToServer, serverOnlyBooks, localOnlyBooks} = useContext(BooksSyncContext) const [searchQuery, setSearchQuery] = useState(''); const [groupedBooks, setGroupedBooks] = useState>({}); @@ -86,7 +89,7 @@ export default function BookList() { useEffect((): void => { getBooks().then() - }, [session.user?.books]); + }, [booksToSyncFromServer, booksToSyncToServer, serverOnlyBooks, localOnlyBooks]); useEffect((): void => { if (accessToken) getBooks().then(); @@ -115,11 +118,21 @@ export default function BookList() { async function getBooks(): Promise { setIsLoadingBooks(true); try { - let bookResponse: BookListProps[] = []; + let bookResponse: (BookListProps & { itIsLocal: boolean })[] = []; if (!isCurrentlyOffline()) { - bookResponse = await System.authGetQueryToServer('books', accessToken, lang); + const [onlineBooks, localBooks]: [BookListProps[], BookListProps[]] = await Promise.all([ + System.authGetQueryToServer('books', accessToken, lang), + window.electron.invoke('db:book:books') + ]); + const onlineBookIds: Set = new Set(onlineBooks.map((book: BookListProps): string => book.id)); + const uniqueLocalBooks: BookListProps[] = localBooks.filter((book: BookListProps): boolean => !onlineBookIds.has(book.id)); + bookResponse = [ + ...onlineBooks.map((book: BookListProps): BookListProps & { itIsLocal: boolean } => ({ ...book, itIsLocal: false })), + ...uniqueLocalBooks.map((book: BookListProps): BookListProps & { itIsLocal: boolean } => ({ ...book, itIsLocal: true })) + ]; } else { - bookResponse = await window.electron.invoke('db:book:books'); + const localBooks: BookListProps[] = await window.electron.invoke('db:book:books'); + bookResponse = localBooks.map((book: BookListProps): BookListProps & { itIsLocal: boolean } => ({ ...book, itIsLocal: true })); } if (bookResponse) { const booksByType: Record = bookResponse.reduce((groups: Record, book: BookListProps): Record => { @@ -170,6 +183,22 @@ export default function BookList() { {} ); + function detectBookSyncStatus(bookId: string):SyncType { + if (serverOnlyBooks.find((book: SyncedBook):boolean => book.id === bookId)) { + return 'server-only'; + } + if (localOnlyBooks.find((book: SyncedBook):boolean => book.id === bookId)) { + return 'local-only'; + } + if (booksToSyncFromServer.find((book: SyncedBook):boolean => book.id === bookId)) { + return 'to-sync-from-server'; + } + if (booksToSyncToServer.find((book: SyncedBook):boolean => book.id === bookId)) { + return 'to-sync-to-server'; + } + return 'synced'; + } + async function getBook(bookId: string): Promise { try { let bookResponse: BookListProps|null = null; @@ -267,8 +296,10 @@ export default function BookList() { {...(idx === 0 && {'data-guide': 'book-card'})} className={`w-full sm:w-1/3 md:w-1/4 lg:w-1/5 xl:w-1/6 p-2 box-border ${User.guideTourDone(session.user?.guideTour || [], 'new-first-book') && 'mb-[200px]'}`}> + index={idx} + />
)) } diff --git a/components/editor/TextEditor.tsx b/components/editor/TextEditor.tsx index 1cb8662..c0094f1 100644 --- a/components/editor/TextEditor.tsx +++ b/components/editor/TextEditor.tsx @@ -300,7 +300,7 @@ export default function TextEditor() { content, totalWordCount: editor.getText().length, currentTime: mainTimer - }, session?.accessToken ?? ''); + }, session?.accessToken, lang); } if (!response) { errorMessage(t('editor.error.savedFailed')); diff --git a/context/BooksSyncContext.ts b/context/BooksSyncContext.ts new file mode 100644 index 0000000..6908ec8 --- /dev/null +++ b/context/BooksSyncContext.ts @@ -0,0 +1,22 @@ +import {SyncedBook} from "@/lib/models/SyncedBook"; +import {Context, createContext} from "react"; + +export type SyncType = 'server-only' | 'local-only' | 'to-sync-from-server' | 'to-sync-to-server' | 'synced' + +export interface BooksSyncContextProps { + serverSyncedBooks:SyncedBook[]; + localSyncedBooks:SyncedBook[]; + booksToSyncFromServer:SyncedBook[]; + booksToSyncToServer:SyncedBook[]; + serverOnlyBooks:SyncedBook[]; + localOnlyBooks:SyncedBook[]; +} + +export const BooksSyncContext:Context = createContext({ + serverSyncedBooks:[], + localSyncedBooks:[], + booksToSyncFromServer:[], + booksToSyncToServer:[], + serverOnlyBooks:[], + localOnlyBooks:[] +}) \ No newline at end of file diff --git a/context/UserContext.ts b/context/UserContext.ts deleted file mode 100755 index 3b8284b..0000000 --- a/context/UserContext.ts +++ /dev/null @@ -1,25 +0,0 @@ -import {UserProps} from "@/lib/models/User"; -import {Context, createContext} from "react"; - -export interface UserContextProps { - user: UserProps -} - -export const UserContext: Context = createContext({ - user: { - id: '', - name: '', - lastName: '', - username: '', - writingLang: 0, - writingLevel: 0, - ritePoints: 0, - groupId: 0, - aiUsage: 0, - apiKeys: { - openai: false, - anthropic: false, - gemini: false, - } - } -}) diff --git a/electron/database/System.ts b/electron/database/System.ts index b5bb0bb..5b689d6 100644 --- a/electron/database/System.ts +++ b/electron/database/System.ts @@ -16,6 +16,11 @@ export default class System { return encryptDataWithUserKey(data, userKey); } + public static timeStampInSeconds(): number { + const date:number = new Date().getTime(); + return Math.floor(date / 1000); + } + public static decryptDataWithUserKey(encryptedData: string, userKey: string): string { return decryptDataWithUserKey(encryptedData, userKey); } diff --git a/electron/database/database.service.ts b/electron/database/database.service.ts index 2194a72..cebd3ce 100644 --- a/electron/database/database.service.ts +++ b/electron/database/database.service.ts @@ -27,10 +27,9 @@ export class DatabaseService { if (this.db) { this.close(); } - - // Get user data directory - const userDataPath = app.getPath('userData'); - const dbPath = path.join(userDataPath, `eritors-local-${userId}.db`); + + const userDataPath:string = app.getPath('userData'); + const dbPath:string = path.join(userDataPath, `eritors-local.db`); this.db = new sqlite3.Database(dbPath); this.userEncryptionKey = encryptionKey; diff --git a/electron/database/models/Book.ts b/electron/database/models/Book.ts index 570f25c..3ef3ac7 100644 --- a/electron/database/models/Book.ts +++ b/electron/database/models/Book.ts @@ -1,21 +1,45 @@ -import BookRepository, { - BookCoverQuery, - BookQuery, - ChapterBookResult, GuideLineAIQuery, - GuideLineQuery, WorldElementValue -} from '../repositories/book.repository.js'; import type { - IssueQuery, ActQuery, - PlotPointQuery, + BookActSummariesTable, BookAIGuideLineTable, BookChapterContentTable, BookChapterInfosTable, BookChaptersTable, + BookCharactersAttributesTable, + BookCharactersTable, BookGuideLineTable, BookIncidentsTable, BookIssuesTable, BookLocationTable, + BookPlotPointsTable, BookWorldElementsTable, BookWorldTable, + EritBooksTable, IncidentQuery, + IssueQuery, LocationElementTable, LocationSubElementTable, + PlotPointQuery, + SyncedActSummaryResult, + SyncedAIGuideLineResult, + SyncedBookResult, + SyncedChapterContentResult, + SyncedChapterInfoResult, + SyncedChapterResult, + SyncedCharacterAttributeResult, + SyncedCharacterResult, + SyncedGuideLineResult, + SyncedIncidentResult, + SyncedIssueResult, + SyncedLocationElementResult, + SyncedLocationResult, + SyncedLocationSubElementResult, + SyncedPlotPointResult, + SyncedWorldElementResult, + SyncedWorldResult, WorldQuery } from '../repositories/book.repository.js'; +import BookRepository from '../repositories/book.repository.js'; +import BookRepo, { + BookCoverQuery, + BookQuery, + ChapterBookResult, + GuideLineAIQuery, + GuideLineQuery, + WorldElementValue +} from '../repositories/book.repository.js'; import System from '../System.js'; -import { getUserEncryptionKey } from '../keyManager.js'; +import {getUserEncryptionKey} from '../keyManager.js'; import path from "path"; import fs from "fs"; -import BookRepo from "../repositories/book.repository.js"; import Chapter, {ActChapter, ChapterContentData, ChapterProps} from "./Chapter.js"; import UserRepo from "../repositories/user.repository.js"; import ChapterRepo from "../repositories/chapter.repository.js"; @@ -35,6 +59,139 @@ export interface BookProps{ bookMeta?:string; } +export interface CompleteBook { + eritBooks: EritBooksTable[]; + actSummaries: BookActSummariesTable[]; + aiGuideLine: BookAIGuideLineTable[]; + chapters: BookChaptersTable[]; + chapterContents: BookChapterContentTable[]; + chapterInfos: BookChapterInfosTable[]; + characters: BookCharactersTable[]; + characterAttributes: BookCharactersAttributesTable[]; + guideLine: BookGuideLineTable[]; + incidents: BookIncidentsTable[]; + issues: BookIssuesTable[]; + locations: BookLocationTable[]; + plotPoints: BookPlotPointsTable[]; + worlds: BookWorldTable[]; + worldElements: BookWorldElementsTable[]; + locationElements: LocationElementTable[]; + locationSubElements: LocationSubElementTable[]; +} + +export interface SyncedBook { + id: string; + type: string; + title: string; + subTitle: string | null; + lastUpdate: number; + chapters: SyncedChapter[]; + characters: SyncedCharacter[]; + locations: SyncedLocation[]; + worlds: SyncedWorld[]; + incidents: SyncedIncident[]; + plotPoints: SyncedPlotPoint[]; + issues: SyncedIssue[]; + actSummaries: SyncedActSummary[]; + guideLine: SyncedGuideLine | null; + aiGuideLine: SyncedAIGuideLine | null; +} + +export interface SyncedChapter { + id: string; + name: string; + lastUpdate: number; + contents: SyncedChapterContent[]; + info: SyncedChapterInfo | null; +} + +export interface SyncedChapterContent { + id: string; + lastUpdate: number; +} + +export interface SyncedChapterInfo { + id: string; + lastUpdate: number; +} + +export interface SyncedCharacter { + id: string; + name: string; + lastUpdate: number; + attributes: SyncedCharacterAttribute[]; +} + +export interface SyncedCharacterAttribute { + id: string; + name: string; + lastUpdate: number; +} + +export interface SyncedLocation { + id: string; + name: string; + lastUpdate: number; + elements: SyncedLocationElement[]; +} + +export interface SyncedLocationElement { + id: string; + name: string; + lastUpdate: number; + subElements: SyncedLocationSubElement[]; +} + +export interface SyncedLocationSubElement { + id: string; + name: string; + lastUpdate: number; +} + +export interface SyncedWorld { + id: string; + name: string; + lastUpdate: number; + elements: SyncedWorldElement[]; +} + +export interface SyncedWorldElement { + id: string; + name: string; + lastUpdate: number; +} + +export interface SyncedIncident { + id: string; + name: string; + lastUpdate: number; +} + +export interface SyncedPlotPoint { + id: string; + name: string; + lastUpdate: number; +} + +export interface SyncedIssue { + id: string; + name: string; + lastUpdate: number; +} + +export interface SyncedActSummary { + id: string; + lastUpdate: number; +} + +export interface SyncedGuideLine { + lastUpdate: number; +} + +export interface SyncedAIGuideLine { + lastUpdate: number; +} + export interface GuideLine{ tone:string; atmosphere:string; @@ -928,4 +1085,385 @@ export default class Book { this.cover = ''; } } + + static async getSyncedBooks(userId: string, lang: 'fr' | 'en'):Promise { + const userKey: string = getUserEncryptionKey(userId); + + const [ + allBooks, + allChapters, + allChapterContents, + allChapterInfos, + allCharacters, + allCharacterAttributes, + allLocations, + allLocationElements, + allLocationSubElements, + allWorlds, + allWorldElements, + allIncidents, + allPlotPoints, + allIssues, + allActSummaries, + allGuidelines, + allAIGuidelines + ]: [ + SyncedBookResult[], + SyncedChapterResult[], + SyncedChapterContentResult[], + SyncedChapterInfoResult[], + SyncedCharacterResult[], + SyncedCharacterAttributeResult[], + SyncedLocationResult[], + SyncedLocationElementResult[], + SyncedLocationSubElementResult[], + SyncedWorldResult[], + SyncedWorldElementResult[], + SyncedIncidentResult[], + SyncedPlotPointResult[], + SyncedIssueResult[], + SyncedActSummaryResult[], + SyncedGuideLineResult[], + SyncedAIGuideLineResult[] + ] = await Promise.all([ + BookRepo.fetchSyncedBooks(userId,lang), + BookRepo.fetchSyncedChapters(userId,lang), + BookRepo.fetchSyncedChapterContents(userId,lang), + BookRepo.fetchSyncedChapterInfos(userId,lang), + BookRepo.fetchSyncedCharacters(userId,lang), + BookRepo.fetchSyncedCharacterAttributes(userId,lang), + BookRepo.fetchSyncedLocations(userId,lang), + BookRepo.fetchSyncedLocationElements(userId,lang), + BookRepo.fetchSyncedLocationSubElements(userId,lang), + BookRepo.fetchSyncedWorlds(userId,lang), + BookRepo.fetchSyncedWorldElements(userId,lang), + BookRepo.fetchSyncedIncidents(userId,lang), + BookRepo.fetchSyncedPlotPoints(userId,lang), + BookRepo.fetchSyncedIssues(userId,lang), + BookRepo.fetchSyncedActSummaries(userId,lang), + BookRepo.fetchSyncedGuideLine(userId,lang), + BookRepo.fetchSyncedAIGuideLine(userId,lang) + ]); + + return allBooks.map((book: SyncedBookResult): SyncedBook => { + const bookId: string = book.book_id; + + const chapters: SyncedChapter[] = allChapters + .filter((chapter: SyncedChapterResult): boolean => chapter.book_id === bookId) + .map((chapter: SyncedChapterResult): SyncedChapter => { + const chapterId: string = chapter.chapter_id; + + const contents: SyncedChapterContent[] = allChapterContents + .filter((content: SyncedChapterContentResult): boolean => content.chapter_id === chapterId) + .map((content: SyncedChapterContentResult): SyncedChapterContent => ({ + id: content.content_id, + lastUpdate: content.last_update + })); + + const infoData: SyncedChapterInfoResult | undefined = allChapterInfos.find((info: SyncedChapterInfoResult): boolean => info.chapter_id === chapterId); + const info: SyncedChapterInfo | null = infoData ? { + id: infoData.chapter_info_id, + lastUpdate: infoData.last_update + } : null; + + return { + id: chapterId, + name: System.decryptDataWithUserKey(chapter.title, userKey), + lastUpdate: chapter.last_update, + contents, + info + }; + }); + + const characters: SyncedCharacter[] = allCharacters + .filter((character: SyncedCharacterResult): boolean => character.book_id === bookId) + .map((character: SyncedCharacterResult): SyncedCharacter => { + const characterId: string = character.character_id; + + const attributes: SyncedCharacterAttribute[] = allCharacterAttributes + .filter((attribute: SyncedCharacterAttributeResult): boolean => attribute.character_id === characterId) + .map((attribute: SyncedCharacterAttributeResult): SyncedCharacterAttribute => ({ + id: attribute.attr_id, + name: System.decryptDataWithUserKey(attribute.attribute_name, userKey), + lastUpdate: attribute.last_update + })); + + return { + id: characterId, + name: System.decryptDataWithUserKey(character.first_name, userKey), + lastUpdate: character.last_update, + attributes + }; + }); + + const locations: SyncedLocation[] = allLocations + .filter((location: SyncedLocationResult): boolean => location.book_id === bookId) + .map((location: SyncedLocationResult): SyncedLocation => { + const locationId: string = location.loc_id; + + const elements: SyncedLocationElement[] = allLocationElements + .filter((element: SyncedLocationElementResult): boolean => element.location === locationId) + .map((element: SyncedLocationElementResult): SyncedLocationElement => { + const elementId: string = element.element_id; + + const subElements: SyncedLocationSubElement[] = allLocationSubElements + .filter((subElement: SyncedLocationSubElementResult): boolean => subElement.element_id === elementId) + .map((subElement: SyncedLocationSubElementResult): SyncedLocationSubElement => ({ + id: subElement.sub_element_id, + name: System.decryptDataWithUserKey(subElement.sub_elem_name, userKey), + lastUpdate: subElement.last_update + })); + + return { + id: elementId, + name: System.decryptDataWithUserKey(element.element_name, userKey), + lastUpdate: element.last_update, + subElements + }; + }); + + return { + id: locationId, + name: System.decryptDataWithUserKey(location.loc_name, userKey), + lastUpdate: location.last_update, + elements + }; + }); + + const worlds: SyncedWorld[] = allWorlds + .filter((world: SyncedWorldResult): boolean => world.book_id === bookId) + .map((world: SyncedWorldResult): SyncedWorld => { + const worldId: string = world.world_id; + + const elements: SyncedWorldElement[] = allWorldElements + .filter((worldElement: SyncedWorldElementResult): boolean => worldElement.world_id === worldId) + .map((worldElement: SyncedWorldElementResult): SyncedWorldElement => ({ + id: worldElement.element_id, + name: System.decryptDataWithUserKey(worldElement.name, userKey), + lastUpdate: worldElement.last_update + })); + + return { + id: worldId, + name: System.decryptDataWithUserKey(world.name, userKey), + lastUpdate: world.last_update, + elements + }; + }); + + const incidents: SyncedIncident[] = allIncidents + .filter((incident: SyncedIncidentResult): boolean => incident.book_id === bookId) + .map((incident: SyncedIncidentResult): SyncedIncident => ({ + id: incident.incident_id, + name: System.decryptDataWithUserKey(incident.title, userKey), + lastUpdate: incident.last_update + })); + + const plotPoints: SyncedPlotPoint[] = allPlotPoints + .filter((plotPoint: SyncedPlotPointResult): boolean => plotPoint.book_id === bookId) + .map((plotPoint: SyncedPlotPointResult): SyncedPlotPoint => ({ + id: plotPoint.plot_point_id, + name: System.decryptDataWithUserKey(plotPoint.title, userKey), + lastUpdate: plotPoint.last_update + })); + + const issues: SyncedIssue[] = allIssues + .filter((issue: SyncedIssueResult): boolean => issue.book_id === bookId) + .map((issue: SyncedIssueResult): SyncedIssue => ({ + id: issue.issue_id, + name: System.decryptDataWithUserKey(issue.name, userKey), + lastUpdate: issue.last_update + })); + + const actSummaries: SyncedActSummary[] = allActSummaries + .filter((actSummary: SyncedActSummaryResult): boolean => actSummary.book_id === bookId) + .map((actSummary: SyncedActSummaryResult): SyncedActSummary => ({ + id: actSummary.act_sum_id, + lastUpdate: actSummary.last_update + })); + + const guidelineData: SyncedGuideLineResult | undefined = allGuidelines.find((guideline: SyncedGuideLineResult): boolean => guideline.book_id === bookId); + const guideLine: SyncedGuideLine | null = guidelineData ? { + lastUpdate: guidelineData.last_update + } : null; + + const aiGuidelineData: SyncedAIGuideLineResult | undefined = allAIGuidelines.find((aiGuideline: SyncedAIGuideLineResult): boolean => aiGuideline.book_id === bookId); + const aiGuideLine: SyncedAIGuideLine | null = aiGuidelineData ? { + lastUpdate: aiGuidelineData.last_update + } : null; + + return { + id: bookId, + type: book.type, + title: System.decryptDataWithUserKey(book.title, userKey), + subTitle: book.sub_title ? System.decryptDataWithUserKey(book.sub_title, userKey) : null, + lastUpdate: book.last_update, + chapters, + characters, + locations, + worlds, + incidents, + plotPoints, + issues, + actSummaries, + guideLine, + aiGuideLine + }; + }); + } + + static async saveCompleteBook(userId: string, data: CompleteBook, lang: "fr" | "en"):Promise { + const userKey: string = getUserEncryptionKey(userId); + + const book: EritBooksTable = data.eritBooks[0]; + const encryptedBookTitle: string = System.encryptDataWithUserKey(book.title, userKey); + const encryptedBookSubTitle: string | null = book.sub_title ? System.encryptDataWithUserKey(book.sub_title, userKey) : null; + const encryptedBookSummary: string | null = book.summary ? System.encryptDataWithUserKey(book.summary, userKey) : null; + const encryptedBookCoverImage: string | null = book.cover_image ? System.encryptDataWithUserKey(book.cover_image, userKey) : null; + + const bookInserted: boolean = BookRepo.insertSyncBook( + book.book_id, + userId, + book.type, + encryptedBookTitle, + book.hashed_title, + encryptedBookSubTitle, + book.hashed_sub_title, + encryptedBookSummary, + book.serie_id, + book.desired_release_date, + book.desired_word_count, + book.words_count, + encryptedBookCoverImage, + book.last_update, + lang + ); + if (!bookInserted) return false; + + const chaptersInserted: boolean = data.chapters.every((chapter: BookChaptersTable): boolean => { + const encryptedTitle: string = System.encryptDataWithUserKey(chapter.title, userKey); + return BookRepo.insertSyncChapter(chapter.chapter_id, chapter.book_id, userId, encryptedTitle, chapter.hashed_title, chapter.words_count, chapter.chapter_order, chapter.last_update, lang); + }); + if (!chaptersInserted) return false; + + const incidentsInserted: boolean = data.incidents.every((incident: BookIncidentsTable): boolean => { + const encryptedIncidentTitle: string = System.encryptDataWithUserKey(incident.title, userKey); + const encryptedIncidentSummary: string | null = incident.summary ? System.encryptDataWithUserKey(incident.summary, userKey) : null; + return BookRepo.insertSyncIncident(incident.incident_id, userId, incident.book_id, encryptedIncidentTitle, incident.hashed_title, encryptedIncidentSummary, incident.last_update, lang); + }); + if (!incidentsInserted) return false; + + const plotPointsInserted: boolean = data.plotPoints.every((plotPoint: BookPlotPointsTable): boolean => { + const encryptedPlotTitle: string = System.encryptDataWithUserKey(plotPoint.title, userKey); + const encryptedPlotSummary: string | null = plotPoint.summary ? System.encryptDataWithUserKey(plotPoint.summary, userKey) : null; + return BookRepo.insertSyncPlotPoint(plotPoint.plot_point_id, encryptedPlotTitle, plotPoint.hashed_title, encryptedPlotSummary, plotPoint.linked_incident_id, userId, plotPoint.book_id, plotPoint.last_update, lang); + }); + if (!plotPointsInserted) return false; + + const chapterContentsInserted: boolean = data.chapterContents.every((content: BookChapterContentTable): boolean => { + const encryptedContent: string | null = content.content ? System.encryptDataWithUserKey(JSON.stringify(content.content), userKey) : null; + return BookRepo.insertSyncChapterContent(content.content_id, content.chapter_id, userId, content.version, encryptedContent, content.words_count, content.time_on_it, content.last_update, lang); + }); + if (!chapterContentsInserted) return false; + + const chapterInfosInserted: boolean = data.chapterInfos.every((info: BookChapterInfosTable): boolean => { + const encryptedSummary: string | null = info.summary ? System.encryptDataWithUserKey(info.summary, userKey) : null; + const encryptedGoal: string | null = info.goal ? System.encryptDataWithUserKey(info.goal, userKey) : null; + return BookRepo.insertSyncChapterInfo(info.chapter_info_id, info.chapter_id, info.act_id, info.incident_id, info.plot_point_id, info.book_id, userId, encryptedSummary, encryptedGoal, info.last_update, lang); + }); + if (!chapterInfosInserted) return false; + + const charactersInserted: boolean = data.characters.every((character: BookCharactersTable): boolean => { + const encryptedFirstName: string = System.encryptDataWithUserKey(character.first_name, userKey); + const encryptedLastName: string | null = character.last_name ? System.encryptDataWithUserKey(character.last_name, userKey) : null; + const encryptedCategory: string = System.encryptDataWithUserKey(character.category, userKey); + const encryptedCharTitle: string | null = character.title ? System.encryptDataWithUserKey(character.title, userKey) : null; + const encryptedRole: string | null = character.role ? System.encryptDataWithUserKey(character.role, userKey) : null; + const encryptedBiography: string | null = character.biography ? System.encryptDataWithUserKey(character.biography, userKey) : null; + const encryptedCharHistory: string | null = character.history ? System.encryptDataWithUserKey(character.history, userKey) : null; + return BookRepo.insertSyncCharacter(character.character_id, character.book_id, userId, encryptedFirstName, encryptedLastName, encryptedCategory, encryptedCharTitle, character.image, encryptedRole, encryptedBiography, encryptedCharHistory, character.last_update, lang); + }); + if (!charactersInserted) return false; + + const characterAttributesInserted: boolean = data.characterAttributes.every((attr: BookCharactersAttributesTable): boolean => { + const encryptedAttrName: string = System.encryptDataWithUserKey(attr.attribute_name, userKey); + const encryptedAttrValue: string = System.encryptDataWithUserKey(attr.attribute_value, userKey); + return BookRepo.insertSyncCharacterAttribute(attr.attr_id, attr.character_id, userId, encryptedAttrName, encryptedAttrValue, attr.last_update, lang); + }); + if (!characterAttributesInserted) return false; + + const locationsInserted: boolean = data.locations.every((location: BookLocationTable): boolean => { + const encryptedLocName: string = System.encryptDataWithUserKey(location.loc_name, userKey); + return BookRepo.insertSyncLocation(location.loc_id, location.book_id, userId, encryptedLocName, location.loc_original_name, location.last_update, lang); + }); + if (!locationsInserted) return false; + + const locationElementsInserted: boolean = data.locationElements.every((element: LocationElementTable): boolean => { + const encryptedLocElemName: string = System.encryptDataWithUserKey(element.element_name, userKey); + const encryptedLocElemDesc: string | null = element.element_description ? System.encryptDataWithUserKey(element.element_description, userKey) : null; + return BookRepo.insertSyncLocationElement(element.element_id, element.location, userId, encryptedLocElemName, element.original_name, encryptedLocElemDesc, element.last_update, lang); + }); + if (!locationElementsInserted) return false; + + const locationSubElementsInserted: boolean = data.locationSubElements.every((subElement: LocationSubElementTable): boolean => { + const encryptedSubElemName: string = System.encryptDataWithUserKey(subElement.sub_elem_name, userKey); + const encryptedSubElemDesc: string | null = subElement.sub_elem_description ? System.encryptDataWithUserKey(subElement.sub_elem_description, userKey) : null; + return BookRepo.insertSyncLocationSubElement(subElement.sub_element_id, subElement.element_id, userId, encryptedSubElemName, subElement.original_name, encryptedSubElemDesc, subElement.last_update, lang); + }); + if (!locationSubElementsInserted) return false; + + const worldsInserted: boolean = data.worlds.every((world: BookWorldTable): boolean => { + const encryptedWorldName: string = System.encryptDataWithUserKey(world.name, userKey); + const encryptedWorldHistory: string | null = world.history ? System.encryptDataWithUserKey(world.history, userKey) : null; + const encryptedPolitics: string | null = world.politics ? System.encryptDataWithUserKey(world.politics, userKey) : null; + const encryptedEconomy: string | null = world.economy ? System.encryptDataWithUserKey(world.economy, userKey) : null; + const encryptedReligion: string | null = world.religion ? System.encryptDataWithUserKey(world.religion, userKey) : null; + const encryptedLanguages: string | null = world.languages ? System.encryptDataWithUserKey(world.languages, userKey) : null; + return BookRepo.insertSyncWorld(world.world_id, encryptedWorldName, world.hashed_name, userId, world.book_id, encryptedWorldHistory, encryptedPolitics, encryptedEconomy, encryptedReligion, encryptedLanguages, world.last_update, lang); + }); + if (!worldsInserted) return false; + + const worldElementsInserted: boolean = data.worldElements.every((element: BookWorldElementsTable): boolean => { + const encryptedElemName: string = System.encryptDataWithUserKey(element.name, userKey); + const encryptedElemDesc: string | null = element.description ? System.encryptDataWithUserKey(element.description, userKey) : null; + return BookRepo.insertSyncWorldElement(element.element_id, element.world_id, userId, element.element_type, encryptedElemName, element.original_name, encryptedElemDesc, element.last_update, lang); + }); + if (!worldElementsInserted) return false; + + const actSummariesInserted: boolean = data.actSummaries.every((actSummary: BookActSummariesTable): boolean => { + const encryptedSummary: string | null = actSummary.summary ? System.encryptDataWithUserKey(actSummary.summary, userKey) : null; + return BookRepo.insertSyncActSummary(actSummary.act_sum_id, actSummary.book_id, userId, actSummary.act_index, encryptedSummary, actSummary.last_update, lang); + }); + if (!actSummariesInserted) return false; + + const aiGuidelinesInserted: boolean = data.aiGuideLine.every((aiGuide: BookAIGuideLineTable): boolean => { + const encryptedGlobalResume: string | null = aiGuide.global_resume ? System.encryptDataWithUserKey(aiGuide.global_resume, userKey) : null; + const encryptedAIThemes: string | null = aiGuide.themes ? System.encryptDataWithUserKey(aiGuide.themes, userKey) : null; + const encryptedAITone: string | null = aiGuide.tone ? System.encryptDataWithUserKey(aiGuide.tone, userKey) : null; + const encryptedAIAtmosphere: string | null = aiGuide.atmosphere ? System.encryptDataWithUserKey(aiGuide.atmosphere, userKey) : null; + const encryptedCurrentResume: string | null = aiGuide.current_resume ? System.encryptDataWithUserKey(aiGuide.current_resume, userKey) : null; + return BookRepo.insertSyncAIGuideLine(userId, aiGuide.book_id, encryptedGlobalResume, encryptedAIThemes, aiGuide.verbe_tense, aiGuide.narrative_type, aiGuide.langue, aiGuide.dialogue_type, encryptedAITone, encryptedAIAtmosphere, encryptedCurrentResume, aiGuide.last_update, lang); + }); + if (!aiGuidelinesInserted) return false; + + const guidelinesInserted: boolean = data.guideLine.every((guide: BookGuideLineTable): boolean => { + const encryptedGuideTone: string | null = guide.tone ? System.encryptDataWithUserKey(guide.tone, userKey) : null; + const encryptedGuideAtmosphere: string | null = guide.atmosphere ? System.encryptDataWithUserKey(guide.atmosphere, userKey) : null; + const encryptedWritingStyle: string | null = guide.writing_style ? System.encryptDataWithUserKey(guide.writing_style, userKey) : null; + const encryptedGuideThemes: string | null = guide.themes ? System.encryptDataWithUserKey(guide.themes, userKey) : null; + const encryptedSymbolism: string | null = guide.symbolism ? System.encryptDataWithUserKey(guide.symbolism, userKey) : null; + const encryptedMotifs: string | null = guide.motifs ? System.encryptDataWithUserKey(guide.motifs, userKey) : null; + const encryptedNarrativeVoice: string | null = guide.narrative_voice ? System.encryptDataWithUserKey(guide.narrative_voice, userKey) : null; + const encryptedPacing: string | null = guide.pacing ? System.encryptDataWithUserKey(guide.pacing, userKey) : null; + const encryptedIntendedAudience: string | null = guide.intended_audience ? System.encryptDataWithUserKey(guide.intended_audience, userKey) : null; + const encryptedKeyMessages: string | null = guide.key_messages ? System.encryptDataWithUserKey(guide.key_messages, userKey) : null; + return BookRepo.insertSyncGuideLine(userId, guide.book_id, encryptedGuideTone, encryptedGuideAtmosphere, encryptedWritingStyle, encryptedGuideThemes, encryptedSymbolism, encryptedMotifs, encryptedNarrativeVoice, encryptedPacing, encryptedIntendedAudience, encryptedKeyMessages, guide.last_update, lang); + }); + if (!guidelinesInserted) return false; + + return data.issues.every((issue: BookIssuesTable): boolean => { + const encryptedIssueName: string = System.encryptDataWithUserKey(issue.name, userKey); + return BookRepo.insertSyncIssue(issue.issue_id, userId, issue.book_id, encryptedIssueName, issue.hashed_issue_name, issue.last_update, lang); + }); + } } \ No newline at end of file diff --git a/electron/database/repositories/book.repository.ts b/electron/database/repositories/book.repository.ts index 6b33ba2..031ef87 100644 --- a/electron/database/repositories/book.repository.ts +++ b/electron/database/repositories/book.repository.ts @@ -17,6 +17,320 @@ export interface BookQuery extends Record { cover_image: string | null; } +export interface EritBooksTable extends Record { + book_id: string; + type: string; + author_id: string; + title: string; + hashed_title: string; + sub_title: string | null; + hashed_sub_title: string | null; + summary: string | null; + serie_id: number | null; + desired_release_date: string | null; + desired_word_count: number | null; + words_count: number | null; + last_update: number; + cover_image: string | null; +} + +export interface BookActSummariesTable extends Record { + act_sum_id: string; + book_id: string; + user_id: string; + act_index: number; + last_update: number; + summary: string | null; +} + +export interface BookAIGuideLineTable extends Record { + user_id: string; + book_id: string; + global_resume: string | null; + themes: string | null; + verbe_tense: number | null; + narrative_type: number | null; + langue: number | null; + dialogue_type: number | null; + tone: string | null; + atmosphere: string | null; + current_resume: string | null; + last_update: number; +} + +export interface BookChaptersTable extends Record { + chapter_id: string; + book_id: string; + author_id: string; + title: string; + hashed_title: string | null; + words_count: number | null; + chapter_order: number | null; + last_update: number; +} + +export interface BookChapterContentTable extends Record { + content_id: string; + chapter_id: string; + author_id: string; + version: number; + content: string | null; + words_count: number; + time_on_it: number; + last_update: number; +} + +export interface BookChapterInfosTable extends Record { + chapter_info_id: string; + chapter_id: string; + act_id: number | null; + incident_id: string | null; + plot_point_id: string | null; + book_id: string; + author_id: string; + summary: string | null; + goal: string | null; + last_update: number; +} + +export interface BookCharactersTable extends Record { + character_id: string; + book_id: string; + user_id: string; + first_name: string; + last_name: string | null; + category: string; + title: string | null; + image: string | null; + role: string | null; + biography: string | null; + history: string | null; + last_update: number; +} + +export interface BookCharactersAttributesTable extends Record { + attr_id: string; + character_id: string; + user_id: string; + attribute_name: string; + attribute_value: string; + last_update: number; +} + +export interface BookGuideLineTable extends Record { + user_id: string; + book_id: string; + tone: string | null; + atmosphere: string | null; + writing_style: string | null; + themes: string | null; + symbolism: string | null; + motifs: string | null; + narrative_voice: string | null; + pacing: string | null; + intended_audience: string | null; + key_messages: string | null; + last_update: number; +} + +export interface BookIncidentsTable extends Record { + incident_id: string; + author_id: string; + book_id: string; + title: string; + hashed_title: string; + summary: string | null; + last_update: number; +} + +export interface BookIssuesTable extends Record { + issue_id: string; + author_id: string; + book_id: string; + name: string; + hashed_issue_name: string; + last_update: number; +} + +export interface BookLocationTable extends Record { + loc_id: string; + book_id: string; + user_id: string; + loc_name: string; + loc_original_name: string; + last_update: number; +} + +export interface BookPlotPointsTable extends Record { + plot_point_id: string; + title: string; + hashed_title: string; + summary: string | null; + linked_incident_id: string | null; + author_id: string; + book_id: string; + last_update: number; +} + +export interface BookWorldTable extends Record { + world_id: string; + name: string; + hashed_name: string; + author_id: string; + book_id: string; + history: string | null; + politics: string | null; + economy: string | null; + religion: string | null; + languages: string | null; + last_update: number; +} + +export interface BookWorldElementsTable extends Record { + element_id: string; + world_id: string; + user_id: string; + element_type: number; + name: string; + original_name: string; + description: string | null; + last_update: number; +} + +export interface LocationElementTable extends Record { + element_id: string; + location: string; + user_id: string; + element_name: string; + original_name: string; + element_description: string | null; + last_update: number; +} + +export interface LocationSubElementTable extends Record { + sub_element_id: string; + element_id: string; + user_id: string; + sub_elem_name: string; + original_name: string; + sub_elem_description: string | null; + last_update: number; +} + +export interface SyncedBookResult extends Record { + book_id: string; + type: string; + title: string; + sub_title: string | null; + last_update: number; +} + +export interface SyncedChapterResult extends Record { + chapter_id: string; + book_id: string; + title: string; + last_update: number; +} + +export interface SyncedChapterContentResult extends Record { + content_id: string; + chapter_id: string; + last_update: number; +} + +export interface SyncedChapterInfoResult extends Record { + chapter_info_id: string; + chapter_id: string | null; + book_id: string; + last_update: number; +} + +export interface SyncedCharacterResult extends Record { + character_id: string; + book_id: string; + first_name: string; + last_update: number; +} + +export interface SyncedCharacterAttributeResult extends Record { + attr_id: string; + character_id: string; + attribute_name: string; + last_update: number; +} + +export interface SyncedLocationResult extends Record { + loc_id: string; + book_id: string; + loc_name: string; + last_update: number; +} + +export interface SyncedLocationElementResult extends Record { + element_id: string; + location: string; + element_name: string; + last_update: number; +} + +export interface SyncedLocationSubElementResult extends Record { + sub_element_id: string; + element_id: string; + sub_elem_name: string; + last_update: number; +} + +export interface SyncedWorldResult extends Record { + world_id: string; + book_id: string; + name: string; + last_update: number; +} + +export interface SyncedWorldElementResult extends Record { + element_id: string; + world_id: string; + name: string; + last_update: number; +} + +export interface SyncedIncidentResult extends Record { + incident_id: string; + book_id: string; + title: string; + last_update: number; +} + +export interface SyncedPlotPointResult extends Record { + plot_point_id: string; + book_id: string; + title: string; + last_update: number; +} + +export interface SyncedIssueResult extends Record { + issue_id: string; + book_id: string; + name: string; + last_update: number; +} + +export interface SyncedActSummaryResult extends Record { + act_sum_id: string; + book_id: string; + last_update: number; +} + +export interface SyncedGuideLineResult extends Record { + book_id: string; + last_update: number; +} + +export interface SyncedAIGuideLineResult extends Record { + book_id: string; + last_update: number; +} + export interface GuideLineQuery extends Record { tone: string; atmosphere: string; @@ -118,7 +432,7 @@ export default class BookRepo { public static updateBookCover(bookId:string,coverImageName:string,userId:string, lang: 'fr' | 'en'):boolean{ try { const db: Database = System.getDb(); - const result:RunResult = db.run('UPDATE `erit_books` SET cover_image=? WHERE `book_id`=? AND author_id=?', [coverImageName, bookId, userId]); + const result:RunResult = db.run('UPDATE `erit_books` SET cover_image=?, last_update=? WHERE `book_id`=? AND author_id=?', [coverImageName, System.timeStampInSeconds(), bookId, userId]); return result.changes>0; } catch (e: unknown) { if (e instanceof Error) { @@ -241,10 +555,10 @@ export default class BookRepo { } public static insertBook(bookId: string, userId: string, encryptedTitle: string, hashedTitle: string, encryptedSubTitle: string, hashedSubTitle: string, encryptedSummary: string, type: string, serie: number, publicationDate: string, desiredWordCount: number, lang: 'fr' | 'en'): string { - let result:RunResult + let result: RunResult; try { const db: Database = System.getDb(); - result = db.run('INSERT INTO erit_books (book_id, type, author_id, title, hashed_title, sub_title, hashed_sub_title, summary, serie_id, desired_release_date, desired_word_count) VALUES (?,?,?,?,?,?,?,?,?,?,?)', [bookId, type, userId, encryptedTitle, hashedTitle, encryptedSubTitle, hashedSubTitle, encryptedSummary, serie, publicationDate ? publicationDate : null, desiredWordCount]); + result = db.run('INSERT INTO erit_books (book_id, type, author_id, title, hashed_title, sub_title, hashed_sub_title, summary, serie_id, desired_release_date, desired_word_count, last_update) VALUES (?,?,?,?,?,?,?,?,?,?,?,?)', [bookId, type, userId, encryptedTitle, hashedTitle, encryptedSubTitle, hashedSubTitle, encryptedSummary, serie, publicationDate ? publicationDate : null, desiredWordCount, System.timeStampInSeconds()]); } catch (err: unknown) { if (err instanceof Error) { console.error(`DB Error: ${err.message}`); @@ -254,11 +568,10 @@ export default class BookRepo { throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); } } - if (result.changes > 0) { - return bookId; - } else { + if (!result || result.changes === 0) { throw new Error(lang === 'fr' ? `Erreur lors de l'ajout du livre.` : `Error adding book.`); } + return bookId; } public static fetchBookCover(userId:string,bookId:string, lang: 'fr' | 'en'):BookCoverQuery{ try { @@ -278,8 +591,8 @@ export default class BookRepo { static updateBookBasicInformation(userId: string, title: string, hashedTitle: string, subTitle: string, hashedSubTitle: string, summary: string, publicationDate: string, wordCount: number, bookId: string, lang: 'fr' | 'en'): boolean { try { const db: Database = System.getDb(); - const result: RunResult = db.run('UPDATE erit_books SET title=?, hashed_title=?, sub_title=?, hashed_sub_title=?, summary=?, serie_id=?, desired_release_date=?, desired_word_count=? WHERE author_id=? AND book_id=?', - [title, hashedTitle, subTitle, hashedSubTitle, summary, 0, publicationDate ? System.dateToMySqlDate(publicationDate) : null, wordCount, userId, bookId]); + const result: RunResult = db.run('UPDATE erit_books SET title=?, hashed_title=?, sub_title=?, hashed_sub_title=?, summary=?, serie_id=?, desired_release_date=?, desired_word_count=?, last_update=? WHERE author_id=? AND book_id=?', + [title, hashedTitle, subTitle, hashedSubTitle, summary, 0, publicationDate ? System.dateToMySqlDate(publicationDate) : null, wordCount, System.timeStampInSeconds(), userId, bookId]); return result.changes > 0; } catch (e: unknown) { if (e instanceof Error) { @@ -295,11 +608,11 @@ export default class BookRepo { static updateGuideLine(userId: string, bookId: string, encryptedTone: string, encryptedAtmosphere: string, encryptedWritingStyle: string, encryptedThemes: string, encryptedSymbolism: string, encryptedMotifs: string, encryptedNarrativeVoice: string, encryptedPacing: string, encryptedKeyMessages: string, encryptedIntendedAudience: string, lang: 'fr' | 'en'): boolean { try { const db: Database = System.getDb(); - const result: RunResult = db.run('UPDATE book_guide_line SET tone=?, atmosphere=?, writing_style=?, themes=?, symbolism=?, motifs=?, narrative_voice=?, pacing=?, key_messages=? WHERE user_id=? AND book_id=?', [encryptedTone, encryptedAtmosphere, encryptedWritingStyle, encryptedThemes, encryptedSymbolism, encryptedMotifs, encryptedNarrativeVoice, encryptedPacing, encryptedKeyMessages, userId, bookId]); + const result: RunResult = db.run('UPDATE book_guide_line SET tone=?, atmosphere=?, writing_style=?, themes=?, symbolism=?, motifs=?, narrative_voice=?, pacing=?, key_messages=?, last_update=? WHERE user_id=? AND book_id=?', [encryptedTone, encryptedAtmosphere, encryptedWritingStyle, encryptedThemes, encryptedSymbolism, encryptedMotifs, encryptedNarrativeVoice, encryptedPacing, encryptedKeyMessages, System.timeStampInSeconds(), userId, bookId]); if (result.changes > 0) { return true; } else { - const insert:RunResult = db.run('INSERT INTO book_guide_line (user_id, book_id, tone, atmosphere, writing_style, themes, symbolism, motifs, narrative_voice, pacing, intended_audience, key_messages) VALUES (?,?,?,?,?,?,?,?,?,?,?,?)', [userId, bookId, encryptedTone, encryptedAtmosphere, encryptedWritingStyle, encryptedThemes, encryptedSymbolism, encryptedMotifs, encryptedNarrativeVoice, encryptedPacing, encryptedIntendedAudience, encryptedKeyMessages]); + const insert:RunResult = db.run('INSERT INTO book_guide_line (user_id, book_id, tone, atmosphere, writing_style, themes, symbolism, motifs, narrative_voice, pacing, intended_audience, key_messages, last_update) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?)', [userId, bookId, encryptedTone, encryptedAtmosphere, encryptedWritingStyle, encryptedThemes, encryptedSymbolism, encryptedMotifs, encryptedNarrativeVoice, encryptedPacing, encryptedIntendedAudience, encryptedKeyMessages, System.timeStampInSeconds()]); return insert.changes > 0; } } catch (e: unknown) { @@ -316,7 +629,7 @@ export default class BookRepo { public static updateActSummary(userId: string, bookId: string, actId: number, summary: string, lang: 'fr' | 'en'): boolean { try { const db: Database = System.getDb(); - const result: RunResult = db.run('UPDATE book_act_summaries SET summary=? WHERE user_id=? AND book_id=? AND act_sum_id=?', [summary, userId, bookId, actId]); + const result: RunResult = db.run('UPDATE book_act_summaries SET summary=?, last_update=? WHERE user_id=? AND book_id=? AND act_sum_id=?', [summary, System.timeStampInSeconds(), userId, bookId, actId]); return result.changes > 0; } catch (e: unknown) { if (e instanceof Error) { @@ -330,14 +643,10 @@ export default class BookRepo { } public static insertNewIncident(incidentId: string, userId: string, bookId: string, encryptedName: string, hashedName: string, lang: 'fr' | 'en'): string { + let result: RunResult; try { const db: Database = System.getDb(); - const result: RunResult = db.run('INSERT INTO book_incidents (incident_id,author_id, book_id, title, hashed_title) VALUES (?,?,?,?,?)', [incidentId, userId, bookId, encryptedName, hashedName]); - if (result.changes > 0) { - return incidentId; - } else { - throw new Error(lang === 'fr' ? `Une erreur s'est produite lors de l'ajout de l'élément déclencheur.` : `Error adding incident.`); - } + result = db.run('INSERT INTO book_incidents (incident_id,author_id, book_id, title, hashed_title, last_update) VALUES (?,?,?,?,?,?)', [incidentId, userId, bookId, encryptedName, hashedName, System.timeStampInSeconds()]); } catch (e: unknown) { if (e instanceof Error) { console.error(`DB Error: ${e.message}`); @@ -347,6 +656,10 @@ export default class BookRepo { throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); } } + if (!result || result.changes === 0) { + throw new Error(lang === 'fr' ? `Une erreur s'est produite lors de l'ajout de l'élément déclencheur.` : `Error adding incident.`); + } + return incidentId; } public static deleteIncident(userId: string, bookId: string, incidentId: string, lang: 'fr' | 'en'): boolean { @@ -366,18 +679,26 @@ export default class BookRepo { } static insertNewPlotPoint(plotPointId: string, userId: string, bookId: string, encryptedName: string, hashedName: string, incidentId: string, lang: 'fr' | 'en'): string { + let existingResult: QueryResult | null; + let insertResult: RunResult; try { const db: Database = System.getDb(); - const existingResult = db.get('SELECT plot_point_id FROM book_plot_points WHERE author_id=? AND book_id=? AND hashed_title=?', [userId, bookId, hashedName]); - if (existingResult !== null) { - throw new Error(lang === 'fr' ? `Ce point de l'intrigue existe déjà.` : `This plot point already exists.`); - } - const insertResult: RunResult = db.run('INSERT INTO book_plot_points (plot_point_id,title,hashed_title,author_id,book_id,linked_incident_id) VALUES (?,?,?,?,?,?)', [plotPointId, encryptedName, hashedName, userId, bookId, incidentId]); - if (insertResult.changes > 0) { - return plotPointId; + existingResult = db.get('SELECT plot_point_id FROM book_plot_points WHERE author_id=? AND book_id=? AND hashed_title=?', [userId, bookId, hashedName]); + } catch (e: unknown) { + if (e instanceof Error) { + console.error(`DB Error: ${e.message}`); + throw new Error(lang === 'fr' ? `Impossible de vérifier l'existence du point d'intrigue.` : `Unable to verify plot point existence.`); } else { - throw new Error(lang === 'fr' ? `Une erreur s'est produite lors de l'ajout du point d'intrigue.` : `Error adding plot point.`); + console.error("An unknown error occurred."); + throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); } + } + if (existingResult !== null) { + throw new Error(lang === 'fr' ? `Ce point de l'intrigue existe déjà.` : `This plot point already exists.`); + } + try { + const db: Database = System.getDb(); + insertResult = db.run('INSERT INTO book_plot_points (plot_point_id,title,hashed_title,author_id,book_id,linked_incident_id,last_update) VALUES (?,?,?,?,?,?,?)', [plotPointId, encryptedName, hashedName, userId, bookId, incidentId, System.timeStampInSeconds()]); } catch (e: unknown) { if (e instanceof Error) { console.error(`DB Error: ${e.message}`); @@ -387,6 +708,10 @@ export default class BookRepo { 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 l'ajout du point d'intrigue.` : `Error adding plot point.`); + } + return plotPointId; } static deletePlotPoint(userId: string, plotNumId: string, lang: 'fr' | 'en'): boolean { @@ -406,18 +731,26 @@ export default class BookRepo { } public static insertNewIssue(issueId: string, userId: string, bookId: string, encryptedName: string, hashedName: string, lang: 'fr' | 'en'): string { + let existingResult: QueryResult | null; + let insertResult: RunResult; try { const db: Database = System.getDb(); - const result = db.get('SELECT issue_id FROM book_issues WHERE hashed_issue_name=? AND book_id=? AND author_id=?', [hashedName, bookId, userId]); - if (result !== null) { - throw new Error(lang === 'fr' ? `La problématique existe déjà.` : `This issue already exists.`); - } - const insertResult: RunResult = db.run('INSERT INTO book_issues (issue_id,author_id, book_id, name, hashed_issue_name) VALUES (?,?,?,?,?)', [issueId, userId, bookId, encryptedName, hashedName]); - if (insertResult.changes > 0) { - return issueId; + existingResult = db.get('SELECT issue_id FROM book_issues WHERE hashed_issue_name=? AND book_id=? AND author_id=?', [hashedName, bookId, userId]); + } catch (e: unknown) { + if (e instanceof Error) { + console.error(`DB Error: ${e.message}`); + throw new Error(lang === 'fr' ? `Impossible de vérifier l'existence de la problématique.` : `Unable to verify issue existence.`); } else { - throw new Error(lang === 'fr' ? `Erreur pendant l'ajout de la problématique.` : `Error adding issue.`); + console.error("An unknown error occurred."); + throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); } + } + if (existingResult !== null) { + throw new Error(lang === 'fr' ? `La problématique existe déjà.` : `This issue already exists.`); + } + try { + const db: Database = System.getDb(); + insertResult = db.run('INSERT INTO book_issues (issue_id,author_id, book_id, name, hashed_issue_name, last_update) VALUES (?,?,?,?,?,?)', [issueId, userId, bookId, encryptedName, hashedName, System.timeStampInSeconds()]); } catch (e: unknown) { if (e instanceof Error) { console.error(`DB Error: ${e.message}`); @@ -427,6 +760,10 @@ export default class BookRepo { 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' ? `Erreur pendant l'ajout de la problématique.` : `Error adding issue.`); + } + return issueId; } public static deleteIssue(userId: string, issueId: string, lang: 'fr' | 'en'): boolean { @@ -448,7 +785,7 @@ export default class BookRepo { public static updateIncident(userId: string, bookId: string, incidentId: string, encryptedIncidentName: string, incidentHashedName: string, incidentSummary: string, lang: 'fr' | 'en'): boolean { try { const db: Database = System.getDb(); - const result: RunResult = db.run('UPDATE book_incidents SET title=?, hashed_title=?, summary=? WHERE author_id=? AND book_id=? AND incident_id=?', [encryptedIncidentName, incidentHashedName, incidentSummary, userId, bookId, incidentId]); + const result: RunResult = db.run('UPDATE book_incidents SET title=?, hashed_title=?, summary=?, last_update=? WHERE author_id=? AND book_id=? AND incident_id=?', [encryptedIncidentName, incidentHashedName, incidentSummary, System.timeStampInSeconds(), userId, bookId, incidentId]); return result.changes > 0; } catch (e: unknown) { if (e instanceof Error) { @@ -464,7 +801,7 @@ export default class BookRepo { public static updatePlotPoint(userId: string, bookId: string, plotPointId: string, encryptedPlotPointName: string, plotPointHashedName: string, plotPointSummary: string, lang: 'fr' | 'en'): boolean { try { const db: Database = System.getDb(); - const result: RunResult = db.run('UPDATE book_plot_points SET title=?, hashed_title=?, summary=? WHERE author_id=? AND book_id=? AND plot_point_id=?', [encryptedPlotPointName, plotPointHashedName, plotPointSummary, userId, bookId, plotPointId]); + const result: RunResult = db.run('UPDATE book_plot_points SET title=?, hashed_title=?, summary=?, last_update=? WHERE author_id=? AND book_id=? AND plot_point_id=?', [encryptedPlotPointName, plotPointHashedName, plotPointSummary, System.timeStampInSeconds(), userId, bookId, plotPointId]); return result.changes > 0; } catch (e: unknown) { if (e instanceof Error) { @@ -493,14 +830,10 @@ export default class BookRepo { } public static insertNewWorld(worldId: string, userId: string, bookId: string, encryptedName: string, hashedName: string, lang: 'fr' | 'en'): string { + let result: RunResult; try { const db: Database = System.getDb(); - const result: RunResult = db.run('INSERT INTO book_world (world_id,author_id, book_id, name, hashed_name) VALUES (?,?,?,?,?)', [worldId, userId, bookId, encryptedName, hashedName]); - if (result.changes > 0) { - return worldId; - } else { - throw new Error(lang === 'fr' ? `Erreur lors de l'ajout du monde.` : `Error adding world.`); - } + result = db.run('INSERT INTO book_world (world_id,author_id, book_id, name, hashed_name, last_update) VALUES (?,?,?,?,?,?)', [worldId, userId, bookId, encryptedName, hashedName, System.timeStampInSeconds()]); } catch (e: unknown) { if (e instanceof Error) { console.error(`DB Error: ${e.message}`); @@ -510,6 +843,10 @@ export default class BookRepo { throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); } } + if (!result || result.changes === 0) { + throw new Error(lang === 'fr' ? `Erreur lors de l'ajout du monde.` : `Error adding world.`); + } + return worldId; } public static fetchWorlds(userId: string, bookId: string, lang: 'fr' | 'en'):WorldQuery[] { try { @@ -529,7 +866,7 @@ export default class BookRepo { public static updateWorld(userId: string, worldId: string, encryptName: string, hashedName: string, encryptHistory: string, encryptPolitics: string, encryptEconomy: string, encryptReligion: string, encryptLanguages: string, lang: 'fr' | 'en'): boolean { try { const db: Database = System.getDb(); - const result: RunResult = db.run('UPDATE book_world SET name=?, hashed_name=?, history=?, politics=?, economy=?, religion=?, languages=? WHERE author_id=? AND world_id=?', [encryptName, hashedName, encryptHistory, encryptPolitics, encryptEconomy, encryptReligion, encryptLanguages, userId, worldId]); + const result: RunResult = db.run('UPDATE book_world SET name=?, hashed_name=?, history=?, politics=?, economy=?, religion=?, languages=?, last_update=? WHERE author_id=? AND world_id=?', [encryptName, hashedName, encryptHistory, encryptPolitics, encryptEconomy, encryptReligion, encryptLanguages, System.timeStampInSeconds(), userId, worldId]); return result.changes > 0; } catch (e: unknown) { if (e instanceof Error) { @@ -546,7 +883,7 @@ export default class BookRepo { try { const db: Database = System.getDb(); for (const element of elements) { - const result: RunResult = db.run('UPDATE book_world_elements SET name=?, description=?, element_type=? WHERE user_id=? AND element_id=?', [element.name, element.description, element.type, userId, element.id]); + const result: RunResult = db.run('UPDATE book_world_elements SET name=?, description=?, element_type=?, last_update=? WHERE user_id=? AND element_id=?', [element.name, element.description, element.type, System.timeStampInSeconds(), userId, element.id]); if (result.changes <= 0) { return false; } @@ -580,14 +917,10 @@ export default class BookRepo { } public static insertNewElement(userId: string, elementId: string, elementType: number, worldId: string, encryptedName: string, hashedName: string, lang: 'fr' | 'en'): string { + let result: RunResult; try { const db: Database = System.getDb(); - const result: RunResult = db.run('INSERT INTO book_world_elements (element_id,world_id,user_id, name, original_name, element_type) VALUES (?,?,?,?,?,?)', [elementId, worldId, userId, encryptedName, hashedName, elementType]); - if (result.changes > 0) { - return elementId; - } else { - throw new Error(lang === 'fr' ? `Erreur lors de l'ajout de l'élément.` : `Error adding element.`); - } + result = db.run('INSERT INTO book_world_elements (element_id,world_id,user_id, name, original_name, element_type, last_update) VALUES (?,?,?,?,?,?,?)', [elementId, worldId, userId, encryptedName, hashedName, elementType, System.timeStampInSeconds()]); } catch (e: unknown) { if (e instanceof Error) { console.error(`DB Error: ${e.message}`); @@ -597,6 +930,10 @@ export default class BookRepo { throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); } } + if (!result || result.changes === 0) { + throw new Error(lang === 'fr' ? `Erreur lors de l'ajout de l'élément.` : `Error adding element.`); + } + return elementId; } public static deleteElement(userId: string, elementId: string, lang: 'fr' | 'en'): boolean { @@ -634,11 +971,11 @@ export default class BookRepo { static insertAIGuideLine(userId: string, bookId: string, narrativeType: number, dialogueType: number, encryptedPlotSummary: string, encryptedToneAtmosphere: string, verbTense: number, language: number, encryptedThemes: string, lang: 'fr' | 'en'): boolean { try { const db: Database = System.getDb(); - let result: RunResult = db.run('UPDATE book_ai_guide_line SET narrative_type=?, dialogue_type=?, global_resume=?, atmosphere=?, verbe_tense=?, langue=?, themes=? WHERE user_id=? AND book_id=?', [narrativeType ? narrativeType : null, dialogueType ? dialogueType : null, encryptedPlotSummary, encryptedToneAtmosphere, verbTense ? verbTense : null, language ? language : null, encryptedThemes, userId, bookId]); + let result: RunResult = db.run('UPDATE book_ai_guide_line SET narrative_type=?, dialogue_type=?, global_resume=?, atmosphere=?, verbe_tense=?, langue=?, themes=?, last_update=? WHERE user_id=? AND book_id=?', [narrativeType ? narrativeType : null, dialogueType ? dialogueType : null, encryptedPlotSummary, encryptedToneAtmosphere, verbTense ? verbTense : null, language ? language : null, encryptedThemes, System.timeStampInSeconds(), userId, bookId]); if (result.changes > 0) { return true; } else { - result = db.run('INSERT INTO book_ai_guide_line (user_id, book_id, global_resume, themes, verbe_tense, narrative_type, langue, dialogue_type, tone, atmosphere, current_resume) VALUES (?,?,?,?,?,?,?,?,?,?,?)', [userId, bookId, encryptedPlotSummary, encryptedThemes, verbTense ? verbTense : null, narrativeType ? narrativeType : null, language ? language : null, dialogueType ? dialogueType : null, encryptedToneAtmosphere, encryptedToneAtmosphere, encryptedPlotSummary]); + result = db.run('INSERT INTO book_ai_guide_line (user_id, book_id, global_resume, themes, verbe_tense, narrative_type, langue, dialogue_type, tone, atmosphere, current_resume, last_update) VALUES (?,?,?,?,?,?,?,?,?,?,?,?)', [userId, bookId, encryptedPlotSummary, encryptedThemes, verbTense ? verbTense : null, narrativeType ? narrativeType : null, language ? language : null, dialogueType ? dialogueType : null, encryptedToneAtmosphere, encryptedToneAtmosphere, encryptedPlotSummary, System.timeStampInSeconds()]); return result.changes > 0; } } catch (e: unknown) { @@ -653,13 +990,10 @@ export default class BookRepo { } static fetchGuideLineAI(userId: string, bookId: string, lang: 'fr' | 'en'): GuideLineAIQuery { + let result: GuideLineAIQuery | null; try { const db: Database = System.getDb(); - const result = db.get('SELECT narrative_type, dialogue_type, global_resume, atmosphere, verbe_tense, langue, themes, current_resume, meta FROM book_ai_guide_line WHERE user_id=? AND book_id=?', [userId, bookId]) as GuideLineAIQuery | null; - if (!result) { - throw new Error(lang === 'fr' ? `Ligne directrice IA non trouvée.` : `AI guideline not found.`); - } - return result; + result = db.get('SELECT narrative_type, dialogue_type, global_resume, atmosphere, verbe_tense, langue, themes, current_resume FROM book_ai_guide_line WHERE user_id=? AND book_id=?', [userId, bookId]) as GuideLineAIQuery | null; } catch (e: unknown) { if (e instanceof Error) { console.error(`DB Error: ${e.message}`); @@ -669,17 +1003,17 @@ export default class BookRepo { throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); } } + if (!result) { + throw new Error(lang === 'fr' ? `Ligne directrice IA non trouvée.` : `AI guideline not found.`); + } + return result; } static insertActSummary(actSummaryId: string, userId: string, bookId: string, actId: number, actSummary: string, lang: 'fr' | 'en'): string { + let result:RunResult try { const db: Database = System.getDb(); - const result: RunResult = db.run('INSERT INTO book_act_summaries (act_sum_id, book_id, user_id, act_index, summary) VALUES (?,?,?,?,?)', [actSummaryId, bookId, userId, actId, actSummary]); - if (result.changes > 0) { - return actSummaryId; - } else { - throw new Error(lang === 'fr' ? `Erreur lors de l'ajout du résumé de l'acte.` : `Error adding act summary.`); - } + result = db.run('INSERT INTO book_act_summaries (act_sum_id, book_id, user_id, act_index, summary, last_update) VALUES (?,?,?,?,?,?)', [actSummaryId, bookId, userId, actId, actSummary, System.timeStampInSeconds()]); } catch (e: unknown) { if (e instanceof Error) { console.error(`DB Error: ${e.message}`); @@ -689,16 +1023,17 @@ export default class BookRepo { throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); } } + if (!result) { + throw new Error(lang === 'fr' ? `Erreur lors de l'ajout du résumé de l'acte.` : `Error adding act summary.`); + } + return actSummaryId; } static fetchCompleteBookChapters(id: string, lang: 'fr' | 'en'): ChapterBookResult[] { + let result: ChapterBookResult[]; try { const db: Database = System.getDb(); - const result = db.all('SELECT title, chapter_order, content.content FROM book_chapters AS chapter LEFT JOIN book_chapter_content AS content ON chapter.chapter_id = content.chapter_id AND content.version = (SELECT MAX(version) FROM book_chapter_content WHERE chapter_id = chapter.chapter_id AND version > 1) WHERE chapter.book_id = ? ORDER BY chapter.chapter_order', [id]) as ChapterBookResult[]; - if (result.length === 0) { - throw new Error(lang === 'fr' ? `Aucun chapitre trouvé.` : `No chapters found.`); - } - return result; + result = db.all('SELECT title, chapter_order, content.content FROM book_chapters AS chapter LEFT JOIN book_chapter_content AS content ON chapter.chapter_id = content.chapter_id AND content.version = (SELECT MAX(version) FROM book_chapter_content WHERE chapter_id = chapter.chapter_id AND version > 1) WHERE chapter.book_id = ? ORDER BY chapter.chapter_order', [id]) as ChapterBookResult[]; } catch (e: unknown) { if (e instanceof Error) { console.error(`DB Error: ${e.message}`); @@ -708,5 +1043,770 @@ export default class BookRepo { throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); } } + if (result.length === 0) { + throw new Error(lang === 'fr' ? `Aucun chapitre trouvé.` : `No chapters found.`); + } + return result; + } + static fetchSyncedBooks(userId: string, lang: 'fr' | 'en'): SyncedBookResult[] { + try { + const db: Database = System.getDb(); + return db.all('SELECT book_id, type, title, sub_title, last_update FROM erit_books WHERE author_id = ?', [userId]) as SyncedBookResult[]; + } catch (e: unknown) { + if (e instanceof Error) { + console.error(`DB Error: ${e.message}`); + throw new Error(lang === 'fr' ? `Impossible de récupérer les livres synchronisés.` : `Unable to retrieve synced books.`); + } else { + console.error("An unknown error occurred."); + throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); + } + } + } + + static fetchSyncedChapters(userId: string, lang: 'fr' | 'en'): SyncedChapterResult[] { + try { + const db: Database = System.getDb(); + return db.all('SELECT chapter_id, book_id, title, last_update FROM book_chapters WHERE author_id = ?', [userId]) as SyncedChapterResult[]; + } catch (e: unknown) { + if (e instanceof Error) { + console.error(`DB Error: ${e.message}`); + throw new Error(lang === 'fr' ? `Impossible de récupérer les chapitres synchronisés.` : `Unable to retrieve synced chapters.`); + } else { + console.error("An unknown error occurred."); + throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); + } + } + } + + static fetchSyncedChapterContents(userId: string, lang: 'fr' | 'en'): SyncedChapterContentResult[] { + try { + const db: Database = System.getDb(); + return db.all('SELECT content_id, chapter_id, last_update FROM book_chapter_content WHERE author_id = ?', [userId]) as SyncedChapterContentResult[]; + } catch (e: unknown) { + if (e instanceof Error) { + console.error(`DB Error: ${e.message}`); + throw new Error(lang === 'fr' ? `Impossible de récupérer le contenu des chapitres synchronisés.` : `Unable to retrieve synced chapter contents.`); + } else { + console.error("An unknown error occurred."); + throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); + } + } + } + + static fetchSyncedChapterInfos(userId: string, lang: 'fr' | 'en'): SyncedChapterInfoResult[] { + try { + const db: Database = System.getDb(); + return db.all('SELECT chapter_info_id, chapter_id, book_id, last_update FROM book_chapter_infos WHERE author_id = ?', [userId]) as SyncedChapterInfoResult[]; + } catch (e: unknown) { + if (e instanceof Error) { + console.error(`DB Error: ${e.message}`); + throw new Error(lang === 'fr' ? `Impossible de récupérer les infos des chapitres synchronisés.` : `Unable to retrieve synced chapter infos.`); + } else { + console.error("An unknown error occurred."); + throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); + } + } + } + + static fetchSyncedCharacters(userId: string, lang: 'fr' | 'en'): SyncedCharacterResult[] { + try { + const db: Database = System.getDb(); + return db.all('SELECT character_id, book_id, first_name, last_update FROM book_characters WHERE user_id = ?', [userId]) as SyncedCharacterResult[]; + } catch (e: unknown) { + if (e instanceof Error) { + console.error(`DB Error: ${e.message}`); + throw new Error(lang === 'fr' ? `Impossible de récupérer les personnages synchronisés.` : `Unable to retrieve synced characters.`); + } else { + console.error("An unknown error occurred."); + throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); + } + } + } + + static fetchSyncedCharacterAttributes(userId: string, lang: 'fr' | 'en'): SyncedCharacterAttributeResult[] { + try { + const db: Database = System.getDb(); + return db.all('SELECT attr_id, character_id, attribute_name, last_update FROM book_characters_attributes WHERE user_id = ?', [userId]) as SyncedCharacterAttributeResult[]; + } catch (e: unknown) { + if (e instanceof Error) { + console.error(`DB Error: ${e.message}`); + throw new Error(lang === 'fr' ? `Impossible de récupérer les attributs des personnages synchronisés.` : `Unable to retrieve synced character attributes.`); + } else { + console.error("An unknown error occurred."); + throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); + } + } + } + + static fetchSyncedLocations(userId: string, lang: 'fr' | 'en'): SyncedLocationResult[] { + try { + const db: Database = System.getDb(); + return db.all('SELECT loc_id, book_id, loc_name, last_update FROM book_location WHERE user_id = ?', [userId]) as SyncedLocationResult[]; + } catch (e: unknown) { + if (e instanceof Error) { + console.error(`DB Error: ${e.message}`); + throw new Error(lang === 'fr' ? `Impossible de récupérer les lieux synchronisés.` : `Unable to retrieve synced locations.`); + } else { + console.error("An unknown error occurred."); + throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); + } + } + } + + static fetchSyncedLocationElements(userId: string, lang: 'fr' | 'en'): SyncedLocationElementResult[] { + try { + const db: Database = System.getDb(); + return db.all('SELECT element_id, location, element_name, last_update FROM location_element WHERE user_id = ?', [userId]) as SyncedLocationElementResult[]; + } catch (e: unknown) { + if (e instanceof Error) { + console.error(`DB Error: ${e.message}`); + throw new Error(lang === 'fr' ? `Impossible de récupérer les éléments de lieu synchronisés.` : `Unable to retrieve synced location elements.`); + } else { + console.error("An unknown error occurred."); + throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); + } + } + } + + static fetchSyncedLocationSubElements(userId: string, lang: 'fr' | 'en'): SyncedLocationSubElementResult[] { + try { + const db: Database = System.getDb(); + return db.all('SELECT sub_element_id, element_id, sub_elem_name, last_update FROM location_sub_element WHERE user_id = ?', [userId]) as SyncedLocationSubElementResult[]; + } catch (e: unknown) { + if (e instanceof Error) { + console.error(`DB Error: ${e.message}`); + throw new Error(lang === 'fr' ? `Impossible de récupérer les sous-éléments de lieu synchronisés.` : `Unable to retrieve synced location sub-elements.`); + } else { + console.error("An unknown error occurred."); + throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); + } + } + } + + static fetchSyncedWorlds(userId: string, lang: 'fr' | 'en'): SyncedWorldResult[] { + try { + const db: Database = System.getDb(); + return db.all('SELECT world_id, book_id, name, last_update FROM book_world WHERE author_id = ?', [userId]) as SyncedWorldResult[]; + } catch (e: unknown) { + if (e instanceof Error) { + console.error(`DB Error: ${e.message}`); + throw new Error(lang === 'fr' ? `Impossible de récupérer les mondes synchronisés.` : `Unable to retrieve synced worlds.`); + } else { + console.error("An unknown error occurred."); + throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); + } + } + } + + static fetchSyncedWorldElements(userId: string, lang: 'fr' | 'en'): SyncedWorldElementResult[] { + try { + const db: Database = System.getDb(); + return db.all('SELECT element_id, world_id, name, last_update FROM book_world_elements WHERE user_id = ?', [userId]) as SyncedWorldElementResult[]; + } catch (e: unknown) { + if (e instanceof Error) { + console.error(`DB Error: ${e.message}`); + throw new Error(lang === 'fr' ? `Impossible de récupérer les éléments de monde synchronisés.` : `Unable to retrieve synced world elements.`); + } else { + console.error("An unknown error occurred."); + throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); + } + } + } + + static fetchSyncedIncidents(userId: string, lang: 'fr' | 'en'): SyncedIncidentResult[] { + try { + const db: Database = System.getDb(); + return db.all('SELECT incident_id, book_id, title, last_update FROM book_incidents WHERE author_id = ?', [userId]) as SyncedIncidentResult[]; + } catch (e: unknown) { + if (e instanceof Error) { + console.error(`DB Error: ${e.message}`); + throw new Error(lang === 'fr' ? `Impossible de récupérer les incidents synchronisés.` : `Unable to retrieve synced incidents.`); + } else { + console.error("An unknown error occurred."); + throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); + } + } + } + + static fetchSyncedPlotPoints(userId: string, lang: 'fr' | 'en'): SyncedPlotPointResult[] { + try { + const db: Database = System.getDb(); + return db.all('SELECT plot_point_id, book_id, title, last_update FROM book_plot_points WHERE author_id = ?', [userId]) as SyncedPlotPointResult[]; + } catch (e: unknown) { + if (e instanceof Error) { + console.error(`DB Error: ${e.message}`); + throw new Error(lang === 'fr' ? `Impossible de récupérer les points d'intrigue synchronisés.` : `Unable to retrieve synced plot points.`); + } else { + console.error("An unknown error occurred."); + throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); + } + } + } + + static fetchSyncedIssues(userId: string, lang: 'fr' | 'en'): SyncedIssueResult[] { + try { + const db: Database = System.getDb(); + return db.all('SELECT issue_id, book_id, name, last_update FROM book_issues WHERE author_id = ?', [userId]) as SyncedIssueResult[]; + } catch (e: unknown) { + if (e instanceof Error) { + console.error(`DB Error: ${e.message}`); + throw new Error(lang === 'fr' ? `Impossible de récupérer les problématiques synchronisées.` : `Unable to retrieve synced issues.`); + } else { + console.error("An unknown error occurred."); + throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); + } + } + } + + static fetchSyncedActSummaries(userId: string, lang: 'fr' | 'en'): SyncedActSummaryResult[] { + try { + const db: Database = System.getDb(); + return db.all('SELECT act_sum_id, book_id, last_update FROM book_act_summaries WHERE user_id = ?', [userId]) as SyncedActSummaryResult[]; + } catch (e: unknown) { + if (e instanceof Error) { + console.error(`DB Error: ${e.message}`); + throw new Error(lang === 'fr' ? `Impossible de récupérer les résumés d'actes synchronisés.` : `Unable to retrieve synced act summaries.`); + } else { + console.error("An unknown error occurred."); + throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); + } + } + } + + static fetchSyncedGuideLine(userId: string, lang: 'fr' | 'en'): SyncedGuideLineResult[] { + try { + const db: Database = System.getDb(); + return db.all('SELECT book_id, last_update FROM book_guide_line WHERE user_id = ?', [userId]) as SyncedGuideLineResult[]; + } catch (e: unknown) { + if (e instanceof Error) { + console.error(`DB Error: ${e.message}`); + throw new Error(lang === 'fr' ? `Impossible de récupérer les lignes directrices synchronisées.` : `Unable to retrieve synced guidelines.`); + } else { + console.error("An unknown error occurred."); + throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); + } + } + } + + static fetchSyncedAIGuideLine(userId: string, lang: 'fr' | 'en'): SyncedAIGuideLineResult[] { + try { + const db: Database = System.getDb(); + return db.all('SELECT book_id, last_update FROM book_ai_guide_line WHERE user_id = ?', [userId]) as SyncedAIGuideLineResult[]; + } catch (e: unknown) { + if (e instanceof Error) { + console.error(`DB Error: ${e.message}`); + throw new Error(lang === 'fr' ? `Impossible de récupérer les lignes directrices IA synchronisées.` : `Unable to retrieve synced AI guidelines.`); + } else { + console.error("An unknown error occurred."); + throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); + } + } + } + + // ===================== SYNC INSERT METHODS ===================== + + static insertSyncBook( + bookId: string, + userId: string, + type: string, + title: string, + hashedTitle: string, + subTitle: string | null, + hashedSubTitle: string | null, + summary: string | null, + serieId: number | null, + desiredReleaseDate: string | null, + desiredWordCount: number | null, + wordsCount: number | null, + coverImage: string | null, + lastUpdate: number, + lang: 'fr' | 'en' + ): boolean { + try { + const db: Database = System.getDb(); + const result: RunResult = db.run( + `INSERT INTO erit_books (book_id, author_id, type, title, hashed_title, sub_title, hashed_sub_title, summary, serie_id, desired_release_date, desired_word_count, words_count, cover_image, last_update) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, + [bookId, userId, type, title, hashedTitle, subTitle, hashedSubTitle, summary, serieId, desiredReleaseDate, desiredWordCount, wordsCount, coverImage, lastUpdate] + ); + return result.changes > 0; + } catch (e: unknown) { + if (e instanceof Error) { + console.error(`DB Error: ${e.message}`); + throw new Error(lang === 'fr' ? `Impossible d'insérer le livre synchronisé.` : `Unable to insert synced book.`); + } else { + throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); + } + } + } + + static insertSyncActSummary( + actSumId: string, + bookId: string, + userId: string, + actIndex: number, + summary: string | null, + lastUpdate: number, + lang: 'fr' | 'en' + ): boolean { + try { + const db: Database = System.getDb(); + const result: RunResult = db.run( + `INSERT INTO book_act_summaries (act_sum_id, book_id, user_id, act_index, summary, last_update) VALUES (?, ?, ?, ?, ?, ?)`, + [actSumId, bookId, userId, actIndex, summary, lastUpdate] + ); + return result.changes > 0; + } catch (e: unknown) { + if (e instanceof Error) { + console.error(`DB Error: ${e.message}`); + throw new Error(lang === 'fr' ? `Impossible d'insérer le résumé d'acte.` : `Unable to insert act summary.`); + } else { + throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); + } + } + } + + static insertSyncAIGuideLine( + userId: string, + bookId: string, + globalResume: string | null, + themes: string | null, + verbeTense: number | null, + narrativeType: number | null, + langue: number | null, + dialogueType: number | null, + tone: string | null, + atmosphere: string | null, + currentResume: string | null, + lastUpdate: number, + lang: 'fr' | 'en' + ): boolean { + try { + const db: Database = System.getDb(); + const result: RunResult = db.run( + `INSERT INTO book_ai_guide_line (user_id, book_id, global_resume, themes, verbe_tense, narrative_type, langue, dialogue_type, tone, atmosphere, current_resume, last_update) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, + [userId, bookId, globalResume, themes, verbeTense, narrativeType, langue, dialogueType, tone, atmosphere, currentResume, lastUpdate] + ); + return result.changes > 0; + } catch (e: unknown) { + if (e instanceof Error) { + console.error(`DB Error: ${e.message}`); + throw new Error(lang === 'fr' ? `Impossible d'insérer la ligne directrice IA.` : `Unable to insert AI guideline.`); + } else { + throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); + } + } + } + + static insertSyncChapter( + chapterId: string, + bookId: string, + authorId: string, + title: string, + hashedTitle: string | null, + wordsCount: number | null, + chapterOrder: number | null, + lastUpdate: number, + lang: 'fr' | 'en' + ): boolean { + try { + const db: Database = System.getDb(); + const result: RunResult = db.run( + `INSERT INTO book_chapters (chapter_id, book_id, author_id, title, hashed_title, words_count, chapter_order, last_update) + VALUES (?, ?, ?, ?, ?, ?, ?, ?)`, + [chapterId, bookId, authorId, title, hashedTitle, wordsCount, chapterOrder, lastUpdate] + ); + return result.changes > 0; + } catch (e: unknown) { + if (e instanceof Error) { + console.error(`DB Error: ${e.message}`); + throw new Error(lang === 'fr' ? `Impossible d'insérer le chapitre.` : `Unable to insert chapter.`); + } else { + throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); + } + } + } + + static insertSyncChapterContent( + contentId: string, + chapterId: string, + authorId: string, + version: number, + content: string | null, + wordsCount: number, + timeOnIt: number, + lastUpdate: number, + lang: 'fr' | 'en' + ): boolean { + try { + const db: Database = System.getDb(); + const result: RunResult = db.run( + `INSERT INTO book_chapter_content (content_id, chapter_id, author_id, version, content, words_count, time_on_it, last_update) + VALUES (?, ?, ?, ?, ?, ?, ?, ?)`, + [contentId, chapterId, authorId, version, content, wordsCount, timeOnIt, lastUpdate] + ); + return result.changes > 0; + } catch (e: unknown) { + if (e instanceof Error) { + console.error(`DB Error: ${e.message}`); + throw new Error(lang === 'fr' ? `Impossible d'insérer le contenu du chapitre.` : `Unable to insert chapter content.`); + } else { + throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); + } + } + } + + static insertSyncChapterInfo( + chapterInfoId: string, + chapterId: string, + actId: number | null, + incidentId: string | null, + plotPointId: string | null, + bookId: string, + authorId: string, + summary: string | null, + goal: string | null, + lastUpdate: number, + lang: 'fr' | 'en' + ): boolean { + try { + const db: Database = System.getDb(); + const result: RunResult = db.run( + `INSERT INTO book_chapter_infos (chapter_info_id, chapter_id, act_id, incident_id, plot_point_id, book_id, author_id, summary, goal, last_update) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, + [chapterInfoId, chapterId, actId, incidentId, plotPointId, bookId, authorId, summary, goal, lastUpdate] + ); + return result.changes > 0; + } catch (e: unknown) { + if (e instanceof Error) { + console.error(`DB Error: ${e.message}`); + throw new Error(lang === 'fr' ? `Impossible d'insérer les infos du chapitre.` : `Unable to insert chapter info.`); + } else { + throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); + } + } + } + + static insertSyncCharacter( + characterId: string, + bookId: string, + userId: string, + firstName: string, + lastName: string | null, + category: string, + title: string | null, + image: string | null, + role: string | null, + biography: string | null, + history: string | null, + lastUpdate: number, + lang: 'fr' | 'en' + ): boolean { + try { + const db: Database = System.getDb(); + const result: RunResult = db.run( + `INSERT INTO book_characters (character_id, book_id, user_id, first_name, last_name, category, title, image, role, biography, history, last_update) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, + [characterId, bookId, userId, firstName, lastName, category, title, image, role, biography, history, lastUpdate] + ); + return result.changes > 0; + } catch (e: unknown) { + if (e instanceof Error) { + console.error(`DB Error: ${e.message}`); + throw new Error(lang === 'fr' ? `Impossible d'insérer le personnage.` : `Unable to insert character.`); + } else { + throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); + } + } + } + + static insertSyncCharacterAttribute( + attrId: string, + characterId: string, + userId: string, + attributeName: string, + attributeValue: string, + lastUpdate: number, + lang: 'fr' | 'en' + ): boolean { + try { + const db: Database = System.getDb(); + const result: RunResult = db.run( + `INSERT INTO book_characters_attributes (attr_id, character_id, user_id, attribute_name, attribute_value, last_update) + VALUES (?, ?, ?, ?, ?, ?)`, + [attrId, characterId, userId, attributeName, attributeValue, lastUpdate] + ); + return result.changes > 0; + } catch (e: unknown) { + if (e instanceof Error) { + console.error(`DB Error: ${e.message}`); + throw new Error(lang === 'fr' ? `Impossible d'insérer l'attribut du personnage.` : `Unable to insert character attribute.`); + } else { + throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); + } + } + } + + static insertSyncGuideLine( + userId: string, + bookId: string, + tone: string | null, + atmosphere: string | null, + writingStyle: string | null, + themes: string | null, + symbolism: string | null, + motifs: string | null, + narrativeVoice: string | null, + pacing: string | null, + intendedAudience: string | null, + keyMessages: string | null, + lastUpdate: number, + lang: 'fr' | 'en' + ): boolean { + try { + const db: Database = System.getDb(); + const result: RunResult = db.run( + `INSERT INTO book_guide_line (user_id, book_id, tone, atmosphere, writing_style, themes, symbolism, motifs, narrative_voice, pacing, intended_audience, key_messages, last_update) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, + [userId, bookId, tone, atmosphere, writingStyle, themes, symbolism, motifs, narrativeVoice, pacing, intendedAudience, keyMessages, lastUpdate] + ); + return result.changes > 0; + } catch (e: unknown) { + if (e instanceof Error) { + console.error(`DB Error: ${e.message}`); + throw new Error(lang === 'fr' ? `Impossible d'insérer la ligne directrice.` : `Unable to insert guideline.`); + } else { + throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); + } + } + } + + static insertSyncIncident( + incidentId: string, + authorId: string, + bookId: string, + title: string, + hashedTitle: string, + summary: string | null, + lastUpdate: number, + lang: 'fr' | 'en' + ): boolean { + try { + const db: Database = System.getDb(); + const result: RunResult = db.run( + `INSERT INTO book_incidents (incident_id, author_id, book_id, title, hashed_title, summary, last_update) + VALUES (?, ?, ?, ?, ?, ?, ?)`, + [incidentId, authorId, bookId, title, hashedTitle, summary, lastUpdate] + ); + return result.changes > 0; + } catch (e: unknown) { + if (e instanceof Error) { + console.error(`DB Error: ${e.message}`); + throw new Error(lang === 'fr' ? `Impossible d'insérer l'incident.` : `Unable to insert incident.`); + } else { + throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); + } + } + } + + static insertSyncIssue( + issueId: string, + authorId: string, + bookId: string, + name: string, + hashedIssueName: string, + lastUpdate: number, + lang: 'fr' | 'en' + ): boolean { + try { + const db: Database = System.getDb(); + const result: RunResult = db.run( + `INSERT INTO book_issues (issue_id, author_id, book_id, name, hashed_issue_name, last_update) + VALUES (?, ?, ?, ?, ?, ?)`, + [issueId, authorId, bookId, name, hashedIssueName, lastUpdate] + ); + return result.changes > 0; + } catch (e: unknown) { + if (e instanceof Error) { + console.error(`DB Error: ${e.message}`); + throw new Error(lang === 'fr' ? `Impossible d'insérer la problématique.` : `Unable to insert issue.`); + } else { + throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); + } + } + } + + static insertSyncLocation( + locId: string, + bookId: string, + userId: string, + locName: string, + locOriginalName: string, + lastUpdate: number, + lang: 'fr' | 'en' + ): boolean { + try { + const db: Database = System.getDb(); + const result: RunResult = db.run( + `INSERT INTO book_location (loc_id, book_id, user_id, loc_name, loc_original_name, last_update) + VALUES (?, ?, ?, ?, ?, ?)`, + [locId, bookId, userId, locName, locOriginalName, lastUpdate] + ); + return result.changes > 0; + } catch (e: unknown) { + if (e instanceof Error) { + console.error(`DB Error: ${e.message}`); + throw new Error(lang === 'fr' ? `Impossible d'insérer le lieu.` : `Unable to insert location.`); + } else { + throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); + } + } + } + + static insertSyncPlotPoint( + plotPointId: string, + title: string, + hashedTitle: string, + summary: string | null, + linkedIncidentId: string | null, + authorId: string, + bookId: string, + lastUpdate: number, + lang: 'fr' | 'en' + ): boolean { + try { + const db: Database = System.getDb(); + const result: RunResult = db.run( + `INSERT INTO book_plot_points (plot_point_id, title, hashed_title, summary, linked_incident_id, author_id, book_id, last_update) + VALUES (?, ?, ?, ?, ?, ?, ?, ?)`, + [plotPointId, title, hashedTitle, summary, linkedIncidentId, authorId, bookId, lastUpdate] + ); + return result.changes > 0; + } catch (e: unknown) { + if (e instanceof Error) { + console.error(`DB Error: ${e.message}`); + throw new Error(lang === 'fr' ? `Impossible d'insérer le point d'intrigue.` : `Unable to insert plot point.`); + } else { + throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); + } + } + } + + static insertSyncWorld( + worldId: string, + name: string, + hashedName: string, + authorId: string, + bookId: string, + history: string | null, + politics: string | null, + economy: string | null, + religion: string | null, + languages: string | null, + lastUpdate: number, + lang: 'fr' | 'en' + ): boolean { + try { + const db: Database = System.getDb(); + const result: RunResult = db.run( + `INSERT INTO book_world (world_id, name, hashed_name, author_id, book_id, history, politics, economy, religion, languages, last_update) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, + [worldId, name, hashedName, authorId, bookId, history, politics, economy, religion, languages, lastUpdate] + ); + return result.changes > 0; + } catch (e: unknown) { + if (e instanceof Error) { + console.error(`DB Error: ${e.message}`); + throw new Error(lang === 'fr' ? `Impossible d'insérer le monde.` : `Unable to insert world.`); + } else { + throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); + } + } + } + + static insertSyncWorldElement( + elementId: string, + worldId: string, + userId: string, + elementType: number, + name: string, + originalName: string, + description: string | null, + lastUpdate: number, + lang: 'fr' | 'en' + ): boolean { + try { + const db: Database = System.getDb(); + const result: RunResult = db.run( + `INSERT INTO book_world_elements (element_id, world_id, user_id, element_type, name, original_name, description, last_update) + VALUES (?, ?, ?, ?, ?, ?, ?, ?)`, + [elementId, worldId, userId, elementType, name, originalName, description, lastUpdate] + ); + return result.changes > 0; + } catch (e: unknown) { + if (e instanceof Error) { + console.error(`DB Error: ${e.message}`); + throw new Error(lang === 'fr' ? `Impossible d'insérer l'élément du monde.` : `Unable to insert world element.`); + } else { + throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); + } + } + } + + static insertSyncLocationElement( + elementId: string, + location: string, + userId: string, + elementName: string, + originalName: string, + elementDescription: string | null, + lastUpdate: number, + lang: 'fr' | 'en' + ): boolean { + try { + const db: Database = System.getDb(); + const result: RunResult = db.run( + `INSERT INTO location_element (element_id, location, user_id, element_name, original_name, element_description, last_update) + VALUES (?, ?, ?, ?, ?, ?, ?)`, + [elementId, location, userId, elementName, originalName, elementDescription, lastUpdate] + ); + return result.changes > 0; + } catch (e: unknown) { + if (e instanceof Error) { + console.error(`DB Error: ${e.message}`); + throw new Error(lang === 'fr' ? `Impossible d'insérer l'élément du lieu.` : `Unable to insert location element.`); + } else { + throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); + } + } + } + + static insertSyncLocationSubElement( + subElementId: string, + elementId: string, + userId: string, + subElemName: string, + originalName: string, + subElemDescription: string | null, + lastUpdate: number, + lang: 'fr' | 'en' + ): boolean { + try { + const db: Database = System.getDb(); + const result: RunResult = db.run( + `INSERT INTO location_sub_element (sub_element_id, element_id, user_id, sub_elem_name, original_name, sub_elem_description, last_update) + VALUES (?, ?, ?, ?, ?, ?, ?)`, + [subElementId, elementId, userId, subElemName, originalName, subElemDescription, lastUpdate] + ); + return result.changes > 0; + } catch (e: unknown) { + if (e instanceof Error) { + console.error(`DB Error: ${e.message}`); + throw new Error(lang === 'fr' ? `Impossible d'insérer le sous-élément du lieu.` : `Unable to insert location sub-element.`); + } else { + throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); + } + } } } diff --git a/electron/database/repositories/chapter.repository.ts b/electron/database/repositories/chapter.repository.ts index f7df5fe..0e9762a 100644 --- a/electron/database/repositories/chapter.repository.ts +++ b/electron/database/repositories/chapter.repository.ts @@ -78,7 +78,7 @@ export default class ChapterRepo{ let result: RunResult; try { const db: Database = System.getDb(); - result = db.run('INSERT INTO book_chapters (chapter_id, author_id, book_id, title, hashed_title, words_count, chapter_order) VALUES (?,?,?,?,?,?,?)', [chapterId, userId, bookId, title, hashedTitle, wordsCount, chapterOrder]); + result = db.run('INSERT INTO book_chapters (chapter_id, author_id, book_id, title, hashed_title, words_count, chapter_order, last_update) VALUES (?,?,?,?,?,?,?,?)', [chapterId, userId, bookId, title, hashedTitle, wordsCount, chapterOrder, System.timeStampInSeconds()]); } catch (e: unknown) { if (e instanceof Error) { console.error(`DB Error: ${e.message}`); @@ -88,11 +88,10 @@ export default class ChapterRepo{ throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); } } - if (result.changes > 0) { - return chapterId; - } else { + if (!result || result.changes === 0) { throw new Error(lang === 'fr' ? `Une erreur s'est passé lors de l'ajout du chapitre.` : `Error adding chapter.`); } + return chapterId; } public static fetchWholeChapter(userId: string, chapterId: string, version: number, lang: 'fr' | 'en' = 'fr'): ChapterContentQueryResult { @@ -197,7 +196,7 @@ export default class ChapterRepo{ } try { const db: Database = System.getDb(); - result = db.run('INSERT INTO book_chapter_infos (chapter_info_id, chapter_id, act_id, book_id, author_id, incident_id, plot_point_id, summary, goal) VALUES (?,?,?,?,?,?,?,?,?)', [chapterInfoId, chapterId, actId, bookId, userId, incidentId, plotId, '', '']); + result = db.run('INSERT INTO book_chapter_infos (chapter_info_id, chapter_id, act_id, book_id, author_id, incident_id, plot_point_id, summary, goal, last_update) VALUES (?,?,?,?,?,?,?,?,?,?)', [chapterInfoId, chapterId, actId, bookId, userId, incidentId, plotId, '', '', System.timeStampInSeconds()]); } catch (e: unknown) { if (e instanceof Error) { console.error(`DB Error: ${e.message}`); @@ -207,17 +206,16 @@ export default class ChapterRepo{ throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); } } - if (result.changes > 0) { - return chapterInfoId; - } else { + if (!result || result.changes === 0) { throw new Error(lang === 'fr' ? `Une erreur s'est produite pendant la liaison du chapitre.` : `Error linking chapter.`); } + return chapterInfoId; } public static updateChapter(userId: string, chapterId: string, encryptedTitle: string, hashTitle: string, chapterOrder: number, lang: 'fr' | 'en' = 'fr'): boolean { try { const db: Database = System.getDb(); - const result: RunResult = db.run('UPDATE book_chapters SET title=?, hashed_title=?, chapter_order=? WHERE author_id=? AND chapter_id=?', [encryptedTitle, hashTitle, chapterOrder, userId, chapterId]); + const result: RunResult = db.run('UPDATE book_chapters SET title=?, hashed_title=?, chapter_order=?, last_update=? WHERE author_id=? AND chapter_id=?', [encryptedTitle, hashTitle, chapterOrder, System.timeStampInSeconds(), userId, chapterId]); return result.changes > 0; } catch (e: unknown) { if (e instanceof Error) { @@ -270,12 +268,12 @@ export default class ChapterRepo{ public static updateChapterContent(userId: string, chapterId: string, version: number, encryptContent: string, wordsCount: number, lang: 'fr' | 'en' = 'fr'): boolean { try { const db: Database = System.getDb(); - const result: RunResult = db.run('UPDATE book_chapter_content SET content=?, words_count=? WHERE chapter_id=? AND author_id=? AND version=?', [encryptContent, wordsCount, chapterId, userId, version]); + const result: RunResult = db.run('UPDATE book_chapter_content SET content=?, words_count=?, last_update=? WHERE chapter_id=? AND author_id=? AND version=?', [encryptContent, wordsCount, System.timeStampInSeconds(), chapterId, userId, version]); if (result.changes > 0) { return true; } else { const contentId:string = System.createUniqueId(); - const insertResult: RunResult = db.run('INSERT INTO book_chapter_content (content_id,chapter_id, author_id, version, content, words_count) VALUES (?,?,?,?,?,?)', [contentId, chapterId, userId, version, encryptContent, wordsCount]); + const insertResult: RunResult = db.run('INSERT INTO book_chapter_content (content_id,chapter_id, author_id, version, content, words_count, last_update) VALUES (?,?,?,?,?,?,?)', [contentId, chapterId, userId, version, encryptContent, wordsCount, System.timeStampInSeconds()]); return insertResult.changes > 0; } } catch (e: unknown) { diff --git a/electron/database/repositories/character.repository.ts b/electron/database/repositories/character.repository.ts index 9ce550d..adeb851 100644 --- a/electron/database/repositories/character.repository.ts +++ b/electron/database/repositories/character.repository.ts @@ -54,7 +54,7 @@ export default class CharacterRepo { let result: RunResult; try { const db: Database = System.getDb(); - result = db.run('INSERT INTO `book_characters` (character_id, book_id, user_id, first_name, last_name, category, title, image, role, biography, history) VALUES (?,?,?,?,?,?,?,?,?,?,?)', [characterId, bookId, userId, encryptedName, encryptedLastName, encryptedCategory, encryptedTitle, encryptedImage, encryptedRole, encryptedBiography, encryptedHistory]); + result = db.run('INSERT INTO `book_characters` (character_id, book_id, user_id, first_name, last_name, category, title, image, role, biography, history, last_update) VALUES (?,?,?,?,?,?,?,?,?,?,?,?)', [characterId, bookId, userId, encryptedName, encryptedLastName, encryptedCategory, encryptedTitle, encryptedImage, encryptedRole, encryptedBiography, encryptedHistory, System.timeStampInSeconds()]); } catch (e: unknown) { if (e instanceof Error) { console.error(`DB Error: ${e.message}`); @@ -64,18 +64,17 @@ export default class CharacterRepo { throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); } } - if (result.changes > 0) { - return characterId; - } else { + if (!result || result.changes === 0) { throw new Error(lang === 'fr' ? `Une erreur s'est produite lors de l'ajout du personnage.` : `Error adding character.`); } + return characterId; } static insertAttribute(attributeId: string, characterId: string, userId: string, type: string, name: string, lang: 'fr' | 'en' = 'fr'): string { let result: RunResult; try { const db: Database = System.getDb(); - result = db.run('INSERT INTO `book_characters_attributes` (attr_id, character_id, user_id, attribute_name, attribute_value) VALUES (?,?,?,?,?)', [attributeId, characterId, userId, type, name]); + result = db.run('INSERT INTO `book_characters_attributes` (attr_id, character_id, user_id, attribute_name, attribute_value, last_update) VALUES (?,?,?,?,?,?)', [attributeId, characterId, userId, type, name, System.timeStampInSeconds()]); } catch (e: unknown) { if (e instanceof Error) { console.error(`DB Error: ${e.message}`); @@ -85,17 +84,16 @@ export default class CharacterRepo { throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); } } - if (result.changes > 0) { - return attributeId; - } else { + if (!result || result.changes === 0) { throw new Error(lang === 'fr' ? `Une erreur s'est produite lors de l'ajout de l'attribut.` : `Error adding attribute.`); } + return attributeId; } static updateCharacter(userId: string, id: number | null, encryptedName: string, encryptedLastName: string, encryptedTitle: string, encryptedCategory: string, encryptedImage: string, encryptedRole: string, encryptedBiography: string, encryptedHistory: string, lang: 'fr' | 'en' = 'fr'): boolean { try { const db: Database = System.getDb(); - const result: RunResult = db.run('UPDATE `book_characters` SET `first_name`=?,`last_name`=?,`title`=?,`category`=?,`image`=?,`role`=?,`biography`=?,`history`=? WHERE `character_id`=? AND `user_id`=?', [encryptedName, encryptedLastName, encryptedTitle, encryptedCategory, encryptedImage, encryptedRole, encryptedBiography, encryptedHistory, id, userId]); + const result: RunResult = db.run('UPDATE `book_characters` SET `first_name`=?,`last_name`=?,`title`=?,`category`=?,`image`=?,`role`=?,`biography`=?,`history`=?,`last_update`=? WHERE `character_id`=? AND `user_id`=?', [encryptedName, encryptedLastName, encryptedTitle, encryptedCategory, encryptedImage, encryptedRole, encryptedBiography, encryptedHistory, System.timeStampInSeconds(), id, userId]); return result.changes > 0; } catch (e: unknown) { if (e instanceof Error) { diff --git a/electron/database/repositories/location.repository.ts b/electron/database/repositories/location.repository.ts index edc1401..d27be7f 100644 --- a/electron/database/repositories/location.repository.ts +++ b/electron/database/repositories/location.repository.ts @@ -51,7 +51,7 @@ export default class LocationRepo { let result: RunResult; try { const db: Database = System.getDb(); - result = db.run('INSERT INTO book_location (loc_id, book_id, user_id, loc_name, loc_original_name) VALUES (?, ?, ?, ?, ?)', [locationId, bookId, userId, encryptedName, originalName]); + result = db.run('INSERT INTO book_location (loc_id, book_id, user_id, loc_name, loc_original_name, last_update) VALUES (?, ?, ?, ?, ?, ?)', [locationId, bookId, userId, encryptedName, originalName, System.timeStampInSeconds()]); } catch (e: unknown) { if (e instanceof Error) { console.error(`DB Error: ${e.message}`); @@ -61,18 +61,17 @@ export default class LocationRepo { throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); } } - if (result.changes > 0) { - return locationId; - } else { + if (!result || result.changes === 0) { throw new Error(lang === 'fr' ? `Une erreur s'est produite lors de l'ajout de la section d'emplacement.` : `Error adding location section.`); } + return locationId; } static insertLocationElement(userId: string, elementId: string, locationId: string, encryptedName: string, originalName: string, lang: 'fr' | 'en' = 'fr'): string { let result: RunResult; try { const db: Database = System.getDb(); - result = db.run('INSERT INTO location_element (element_id, location, user_id, element_name, original_name, element_description) VALUES (?,?,?,?,?,?)', [elementId, locationId, userId, encryptedName, originalName, '']); + result = db.run('INSERT INTO location_element (element_id, location, user_id, element_name, original_name, element_description, last_update) VALUES (?,?,?,?,?,?,?)', [elementId, locationId, userId, encryptedName, originalName, '', System.timeStampInSeconds()]); } catch (e: unknown) { if (e instanceof Error) { console.error(`DB Error: ${e.message}`); @@ -82,18 +81,17 @@ export default class LocationRepo { throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); } } - if (result.changes > 0) { - return elementId; - } else { + if (!result || result.changes === 0) { throw new Error(lang === 'fr' ? `Une erreur s'est produite lors de l'ajout de l'élément d'emplacement.` : `Error adding location element.`); } + return elementId; } static insertLocationSubElement(userId: string, subElementId: string, elementId: string, encryptedName: string, originalName: string, lang: 'fr' | 'en' = 'fr'): string { let result: RunResult; try { const db: Database = System.getDb(); - result = db.run('INSERT INTO location_sub_element (sub_element_id, element_id, user_id, sub_elem_name, original_name, sub_elem_description) VALUES (?,?,?,?,?,?)', [subElementId, elementId, userId, encryptedName, originalName, '']); + result = db.run('INSERT INTO location_sub_element (sub_element_id, element_id, user_id, sub_elem_name, original_name, sub_elem_description, last_update) VALUES (?,?,?,?,?,?,?)', [subElementId, elementId, userId, encryptedName, originalName, '', System.timeStampInSeconds()]); } catch (e: unknown) { if (e instanceof Error) { console.error(`DB Error: ${e.message}`); @@ -103,17 +101,16 @@ export default class LocationRepo { throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); } } - if (result.changes > 0) { - return subElementId; - } else { + if (!result || result.changes === 0) { throw new Error(lang === 'fr' ? `Une erreur s'est produite lors de l'ajout du sous-élément d'emplacement.` : `Error adding location sub-element.`); } + return subElementId; } static updateLocationSubElement(userId: string, id: string, encryptedName: string, originalName: string, encryptDescription: string, lang: 'fr' | 'en' = 'fr'): boolean { try { const db: Database = System.getDb(); - const result: RunResult = db.run('UPDATE location_sub_element SET sub_elem_name=?, original_name=?, sub_elem_description=? WHERE sub_element_id=? AND user_id=?', [encryptedName, originalName, encryptDescription, id, userId]); + const result: RunResult = db.run('UPDATE location_sub_element SET sub_elem_name=?, original_name=?, sub_elem_description=?, last_update=? WHERE sub_element_id=? AND user_id=?', [encryptedName, originalName, encryptDescription, System.timeStampInSeconds(), id, userId]); return result.changes > 0; } catch (e: unknown) { if (e instanceof Error) { @@ -129,7 +126,7 @@ export default class LocationRepo { static updateLocationElement(userId: string, id: string, encryptedName: string, originalName: string, encryptedDescription: string, lang: 'fr' | 'en' = 'fr'): boolean { try { const db: Database = System.getDb(); - const result: RunResult = db.run('UPDATE location_element SET element_name=?, original_name=?, element_description=? WHERE element_id=? AND user_id=?', [encryptedName, originalName, encryptedDescription, id, userId]); + const result: RunResult = db.run('UPDATE location_element SET element_name=?, original_name=?, element_description=?, last_update=? WHERE element_id=? AND user_id=?', [encryptedName, originalName, encryptedDescription, System.timeStampInSeconds(), id, userId]); return result.changes > 0; } catch (e: unknown) { if (e instanceof Error) { @@ -145,7 +142,7 @@ export default class LocationRepo { static updateLocationSection(userId: string, id: string, encryptedName: string, originalName: string, lang: 'fr' | 'en' = 'fr'): boolean { try { const db: Database = System.getDb(); - const result: RunResult = db.run('UPDATE book_location SET loc_name=?, loc_original_name=? WHERE loc_id=? AND user_id=?', [encryptedName, originalName, id, userId]); + const result: RunResult = db.run('UPDATE book_location SET loc_name=?, loc_original_name=?, last_update=? WHERE loc_id=? AND user_id=?', [encryptedName, originalName, System.timeStampInSeconds(), id, userId]); return result.changes > 0; } catch (e: unknown) { if (e instanceof Error) { diff --git a/electron/database/schema.ts b/electron/database/schema.ts index e5cd2c7..22fa471 100644 --- a/electron/database/schema.ts +++ b/electron/database/schema.ts @@ -18,29 +18,6 @@ export function initializeSchema(db: Database): void { // Enable foreign keys db.exec('PRAGMA foreign_keys = ON'); - // Create sync metadata table (tracks last sync times) - db.exec(` - CREATE TABLE IF NOT EXISTS _sync_metadata ( - table_name TEXT PRIMARY KEY, - last_sync_at INTEGER NOT NULL, - last_push_at INTEGER, - pending_changes INTEGER DEFAULT 0 - ); - `); - - // Create pending changes queue (for offline operations) - db.exec(` - CREATE TABLE IF NOT EXISTS _pending_changes ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - table_name TEXT NOT NULL, - operation TEXT NOT NULL, -- 'INSERT', 'UPDATE', 'DELETE' - record_id TEXT NOT NULL, - data TEXT, -- JSON data for INSERT/UPDATE - created_at INTEGER NOT NULL, - retry_count INTEGER DEFAULT 0 - ); - `); - // AI Conversations db.exec(` CREATE TABLE IF NOT EXISTS ai_conversations ( @@ -53,7 +30,6 @@ export function initializeSchema(db: Database): void { user_id TEXT NOT NULL, summary TEXT, convo_meta TEXT NOT NULL, - synced INTEGER DEFAULT 0, FOREIGN KEY (book_id) REFERENCES erit_books(book_id) ON DELETE CASCADE ); `); @@ -66,7 +42,6 @@ export function initializeSchema(db: Database): void { role TEXT NOT NULL, message TEXT NOT NULL, message_date INTEGER NOT NULL, - synced INTEGER DEFAULT 0, FOREIGN KEY (conversation_id) REFERENCES ai_conversations(conversation_id) ON DELETE CASCADE ); `); @@ -75,7 +50,8 @@ export function initializeSchema(db: Database): void { db.exec(` CREATE TABLE IF NOT EXISTS book_acts ( act_id INTEGER PRIMARY KEY, - title TEXT NOT NULL + title TEXT NOT NULL, + last_update INTEGER DEFAULT 0 ); `); @@ -87,7 +63,7 @@ export function initializeSchema(db: Database): void { user_id TEXT NOT NULL, act_index INTEGER NOT NULL, summary TEXT, - synced INTEGER DEFAULT 0, + last_update INTEGER DEFAULT 0, FOREIGN KEY (book_id) REFERENCES erit_books(book_id) ON DELETE CASCADE ); `); @@ -106,7 +82,7 @@ export function initializeSchema(db: Database): void { tone TEXT, atmosphere TEXT, current_resume TEXT, - synced INTEGER DEFAULT 0, + last_update INTEGER DEFAULT 0, PRIMARY KEY (user_id, book_id), FOREIGN KEY (book_id) REFERENCES erit_books(book_id) ON DELETE CASCADE ); @@ -122,7 +98,7 @@ export function initializeSchema(db: Database): void { hashed_title TEXT, words_count INTEGER, chapter_order INTEGER, - synced INTEGER DEFAULT 0, + last_update INTEGER DEFAULT 0, FOREIGN KEY (book_id) REFERENCES erit_books(book_id) ON DELETE CASCADE ); `); @@ -134,10 +110,10 @@ export function initializeSchema(db: Database): void { chapter_id TEXT NOT NULL, author_id TEXT NOT NULL, version INTEGER NOT NULL DEFAULT 2, - content TEXT NOT NULL, + content TEXT, words_count INTEGER NOT NULL, time_on_it INTEGER NOT NULL DEFAULT 0, - synced INTEGER DEFAULT 0, + last_update INTEGER DEFAULT 0, FOREIGN KEY (chapter_id) REFERENCES book_chapters(chapter_id) ON DELETE CASCADE ); `); @@ -152,9 +128,9 @@ export function initializeSchema(db: Database): void { plot_point_id TEXT, book_id TEXT, author_id TEXT, - summary TEXT NOT NULL, - goal TEXT NOT NULL, - synced INTEGER DEFAULT 0, + summary TEXT, + goal TEXT, + last_update INTEGER DEFAULT 0, FOREIGN KEY (chapter_id) REFERENCES book_chapters(chapter_id) ON DELETE CASCADE, FOREIGN KEY (incident_id) REFERENCES book_incidents(incident_id) ON DELETE CASCADE, FOREIGN KEY (plot_point_id) REFERENCES book_plot_points(plot_point_id) ON DELETE CASCADE @@ -175,7 +151,7 @@ export function initializeSchema(db: Database): void { role TEXT, biography TEXT, history TEXT, - synced INTEGER DEFAULT 0, + last_update INTEGER DEFAULT 0, FOREIGN KEY (book_id) REFERENCES erit_books(book_id) ON DELETE CASCADE ); `); @@ -188,7 +164,7 @@ export function initializeSchema(db: Database): void { user_id TEXT NOT NULL, attribute_name TEXT NOT NULL, attribute_value TEXT NOT NULL, - synced INTEGER DEFAULT 0, + last_update INTEGER DEFAULT 0, FOREIGN KEY (character_id) REFERENCES book_characters(character_id) ON DELETE CASCADE ); `); @@ -202,7 +178,7 @@ export function initializeSchema(db: Database): void { type TEXT NOT NULL, description TEXT NOT NULL, history TEXT NOT NULL, - synced INTEGER DEFAULT 0 + last_update INTEGER DEFAULT 0 ); `); @@ -211,17 +187,17 @@ export function initializeSchema(db: Database): void { CREATE TABLE IF NOT EXISTS book_guide_line ( user_id TEXT NOT NULL, book_id TEXT NOT NULL, - tone TEXT NOT NULL, - atmosphere TEXT NOT NULL, - writing_style TEXT NOT NULL, - themes TEXT NOT NULL, - symbolism TEXT NOT NULL, - motifs TEXT NOT NULL, - narrative_voice TEXT NOT NULL, - pacing TEXT NOT NULL, - intended_audience TEXT NOT NULL, - key_messages TEXT NOT NULL, - synced INTEGER DEFAULT 0, + tone TEXT, + atmosphere TEXT, + writing_style TEXT, + themes TEXT, + symbolism TEXT, + motifs TEXT, + narrative_voice TEXT, + pacing TEXT, + intended_audience TEXT, + key_messages TEXT, + last_update INTEGER DEFAULT 0, PRIMARY KEY (user_id, book_id), FOREIGN KEY (book_id) REFERENCES erit_books(book_id) ON DELETE CASCADE ); @@ -236,7 +212,7 @@ export function initializeSchema(db: Database): void { title TEXT NOT NULL, hashed_title TEXT NOT NULL, summary TEXT, - synced INTEGER DEFAULT 0, + last_update INTEGER DEFAULT 0, FOREIGN KEY (book_id) REFERENCES erit_books(book_id) ON DELETE CASCADE ); `); @@ -249,7 +225,7 @@ export function initializeSchema(db: Database): void { book_id TEXT NOT NULL, name TEXT NOT NULL, hashed_issue_name TEXT NOT NULL, - synced INTEGER DEFAULT 0, + last_update INTEGER DEFAULT 0, FOREIGN KEY (book_id) REFERENCES erit_books(book_id) ON DELETE CASCADE ); `); @@ -262,7 +238,7 @@ export function initializeSchema(db: Database): void { user_id TEXT NOT NULL, loc_name TEXT NOT NULL, loc_original_name TEXT NOT NULL, - synced INTEGER DEFAULT 0, + last_update INTEGER DEFAULT 0, FOREIGN KEY (book_id) REFERENCES erit_books(book_id) ON DELETE CASCADE ); `); @@ -277,7 +253,7 @@ export function initializeSchema(db: Database): void { linked_incident_id TEXT, author_id TEXT NOT NULL, book_id TEXT NOT NULL, - synced INTEGER DEFAULT 0, + last_update INTEGER DEFAULT 0, FOREIGN KEY (book_id) REFERENCES erit_books(book_id) ON DELETE CASCADE ); `); @@ -295,7 +271,7 @@ export function initializeSchema(db: Database): void { economy TEXT, religion TEXT, languages TEXT, - synced INTEGER DEFAULT 0, + last_update INTEGER DEFAULT 0, FOREIGN KEY (book_id) REFERENCES erit_books(book_id) ON DELETE CASCADE ); `); @@ -310,7 +286,7 @@ export function initializeSchema(db: Database): void { name TEXT NOT NULL, original_name TEXT NOT NULL, description TEXT, - synced INTEGER DEFAULT 0, + last_update INTEGER DEFAULT 0, FOREIGN KEY (world_id) REFERENCES book_world(world_id) ON DELETE CASCADE ); `); @@ -324,14 +300,14 @@ export function initializeSchema(db: Database): void { title TEXT NOT NULL, hashed_title TEXT NOT NULL, sub_title TEXT, - hashed_sub_title TEXT NOT NULL, - summary TEXT NOT NULL, + hashed_sub_title TEXT, + summary TEXT, serie_id INTEGER, desired_release_date TEXT, desired_word_count INTEGER, words_count INTEGER, cover_image TEXT, - synced INTEGER DEFAULT 0 + last_update INTEGER DEFAULT 0 ); `); @@ -354,8 +330,7 @@ export function initializeSchema(db: Database): void { interline TEXT NOT NULL, paper_width INTEGER NOT NULL, theme TEXT NOT NULL, - focus INTEGER NOT NULL, - synced INTEGER DEFAULT 0 + focus INTEGER NOT NULL ); `); @@ -381,8 +356,7 @@ export function initializeSchema(db: Database): void { account_verified INTEGER NOT NULL DEFAULT 0, erite_points INTEGER NOT NULL DEFAULT 100, stripe_customer_id TEXT, - credits_balance REAL DEFAULT 0, - synced INTEGER DEFAULT 0 + credits_balance REAL DEFAULT 0 ); `); @@ -395,7 +369,7 @@ export function initializeSchema(db: Database): void { element_name TEXT NOT NULL, original_name TEXT NOT NULL, element_description TEXT, - synced INTEGER DEFAULT 0, + last_update INTEGER DEFAULT 0, FOREIGN KEY (location) REFERENCES book_location(loc_id) ON DELETE CASCADE ); `); @@ -409,7 +383,7 @@ export function initializeSchema(db: Database): void { sub_elem_name TEXT NOT NULL, original_name TEXT NOT NULL, sub_elem_description TEXT, - synced INTEGER DEFAULT 0, + last_update INTEGER DEFAULT 0, FOREIGN KEY (element_id) REFERENCES location_element(element_id) ON DELETE CASCADE ); `); @@ -421,7 +395,6 @@ export function initializeSchema(db: Database): void { brand TEXT NOT NULL, key TEXT NOT NULL, actif INTEGER NOT NULL DEFAULT 1, - synced INTEGER DEFAULT 0, FOREIGN KEY (user_id) REFERENCES erit_users(user_id) ON DELETE CASCADE ); `); @@ -433,7 +406,6 @@ export function initializeSchema(db: Database): void { book_id TEXT NOT NULL, chapter_id TEXT NOT NULL, version INTEGER NOT NULL, - synced INTEGER DEFAULT 0, PRIMARY KEY (user_id, book_id), FOREIGN KEY (book_id) REFERENCES erit_books(book_id) ON DELETE CASCADE, FOREIGN KEY (chapter_id) REFERENCES book_chapters(chapter_id) ON DELETE CASCADE @@ -443,8 +415,8 @@ export function initializeSchema(db: Database): void { // Create indexes for better performance createIndexes(db); - // Initialize sync metadata for all tables - initializeSyncMetadata(db); + // Set schema version for new databases (prevents unnecessary migrations) + initializeSchemaVersion(db); } /** @@ -461,50 +433,9 @@ function createIndexes(db: Database): void { CREATE INDEX IF NOT EXISTS idx_character_attrs_character ON book_characters_attributes(character_id); CREATE INDEX IF NOT EXISTS idx_world_book ON book_world(book_id); CREATE INDEX IF NOT EXISTS idx_world_elements_world ON book_world_elements(world_id); - CREATE INDEX IF NOT EXISTS idx_pending_changes_table ON _pending_changes(table_name); - CREATE INDEX IF NOT EXISTS idx_pending_changes_created ON _pending_changes(created_at); `); } -/** - * Initialize sync metadata for all tables - */ -function initializeSyncMetadata(db: Database): void { - const tables = [ - 'ai_conversations', 'ai_messages_history', 'book_acts', 'book_act_summaries', - 'book_ai_guide_line', 'book_chapters', 'book_chapter_content', 'book_chapter_infos', - 'book_characters', 'book_characters_attributes', 'book_guide_line', 'book_incidents', - 'book_issues', 'book_location', 'book_plot_points', 'book_world', 'book_world_elements', - 'erit_books', 'erit_editor', 'erit_users', 'location_element', 'location_sub_element', - 'user_keys', 'user_last_chapter' - ]; - - for (const table of tables) { - db.run(` - INSERT OR IGNORE INTO _sync_metadata (table_name, last_sync_at, pending_changes) - VALUES (?, 0, 0) - `, [table]); - } -} - -/** - * Drop all tables (for testing/reset) - */ -export function dropAllTables(db: Database): void { - const tables = db.all(` - SELECT name FROM sqlite_master - WHERE type='table' AND name NOT LIKE 'sqlite_%' - `, []) as unknown as { name: string }[]; - - db.exec('PRAGMA foreign_keys = OFF'); - - for (const { name } of tables) { - db.exec(`DROP TABLE IF EXISTS ${name}`); - } - - db.exec('PRAGMA foreign_keys = ON'); -} - /** * Get current schema version from database */ @@ -526,6 +457,17 @@ function setDbSchemaVersion(db: Database, version: number): void { db.run('INSERT INTO _schema_version (version) VALUES (?)', [version]); } +/** + * Initialize schema version for new databases + * Only sets version if table doesn't exist yet (new DB) + */ +function initializeSchemaVersion(db: Database): void { + const currentVersion = getDbSchemaVersion(db); + if (currentVersion === 0) { + setDbSchemaVersion(db, SCHEMA_VERSION); + } +} + /** * Check if a column exists in a table */ @@ -548,6 +490,25 @@ function dropColumnIfExists(db: Database, tableName: string, columnName: string) } } +/** + * Recreate a table with a new schema while preserving data + */ +function recreateTable(db: Database, tableName: string, newSchema: string, columnsToKeep: string): void { + try { + db.exec('PRAGMA foreign_keys = OFF'); + db.exec(`CREATE TABLE ${tableName}_backup AS SELECT ${columnsToKeep} FROM ${tableName}`); + db.exec(`DROP TABLE ${tableName}`); + db.exec(newSchema); + db.exec(`INSERT INTO ${tableName} (${columnsToKeep}) SELECT ${columnsToKeep} FROM ${tableName}_backup`); + db.exec(`DROP TABLE ${tableName}_backup`); + db.exec('PRAGMA foreign_keys = ON'); + console.log(`[Migration] Recreated table ${tableName}`); + } catch (e) { + console.error(`[Migration] Failed to recreate table ${tableName}:`, e); + db.exec('PRAGMA foreign_keys = ON'); + } +} + /** * Run migrations to update schema from one version to another */ @@ -560,29 +521,63 @@ export function runMigrations(db: Database): void { console.log(`[Migration] Upgrading schema from version ${currentVersion} to ${SCHEMA_VERSION}`); - // Migration 1 -> 2: Remove all meta_* columns + // Migration v2: Remove NOT NULL constraints to allow null values from server sync if (currentVersion < 2) { - console.log('[Migration] Running migration v2: Removing meta columns...'); + console.log('[Migration] Running migration v2: Allowing NULL in certain columns...'); - dropColumnIfExists(db, 'ai_messages_history', 'meta_message'); - dropColumnIfExists(db, 'book_act_summaries', 'meta_acts'); - dropColumnIfExists(db, 'book_ai_guide_line', 'meta'); - dropColumnIfExists(db, 'book_chapters', 'meta_chapter'); - dropColumnIfExists(db, 'book_chapter_content', 'meta_chapter_content'); - dropColumnIfExists(db, 'book_chapter_infos', 'meta_chapter_info'); - dropColumnIfExists(db, 'book_characters', 'char_meta'); - dropColumnIfExists(db, 'book_characters_attributes', 'attr_meta'); - dropColumnIfExists(db, 'book_guide_line', 'meta_guide_line'); - dropColumnIfExists(db, 'book_incidents', 'meta_incident'); - dropColumnIfExists(db, 'book_issues', 'meta_issue'); - dropColumnIfExists(db, 'book_location', 'loc_meta'); - dropColumnIfExists(db, 'book_plot_points', 'meta_plot'); - dropColumnIfExists(db, 'book_world', 'meta_world'); - dropColumnIfExists(db, 'book_world_elements', 'meta_element'); - dropColumnIfExists(db, 'erit_books', 'book_meta'); - dropColumnIfExists(db, 'erit_users', 'user_meta'); - dropColumnIfExists(db, 'location_element', 'element_meta'); - dropColumnIfExists(db, 'location_sub_element', 'sub_elem_meta'); + // Recreate erit_books with nullable hashed_sub_title and summary + recreateTable(db, 'erit_books', ` + CREATE TABLE erit_books ( + book_id TEXT PRIMARY KEY, + type TEXT NOT NULL, + author_id TEXT NOT NULL, + title TEXT NOT NULL, + hashed_title TEXT NOT NULL, + sub_title TEXT, + hashed_sub_title TEXT, + summary TEXT, + serie_id INTEGER, + desired_release_date TEXT, + desired_word_count INTEGER, + words_count INTEGER, + cover_image TEXT, + last_update INTEGER DEFAULT 0 + ) + `, 'book_id, type, author_id, title, hashed_title, sub_title, hashed_sub_title, summary, serie_id, desired_release_date, desired_word_count, words_count, cover_image, last_update'); + + // Recreate book_chapter_content with nullable content + recreateTable(db, 'book_chapter_content', ` + CREATE TABLE book_chapter_content ( + content_id TEXT PRIMARY KEY, + chapter_id TEXT NOT NULL, + author_id TEXT NOT NULL, + version INTEGER NOT NULL DEFAULT 2, + content TEXT, + words_count INTEGER NOT NULL, + time_on_it INTEGER NOT NULL DEFAULT 0, + last_update INTEGER DEFAULT 0, + FOREIGN KEY (chapter_id) REFERENCES book_chapters(chapter_id) ON DELETE CASCADE + ) + `, 'content_id, chapter_id, author_id, version, content, words_count, time_on_it, last_update'); + + // Recreate book_chapter_infos with nullable summary and goal + recreateTable(db, 'book_chapter_infos', ` + CREATE TABLE book_chapter_infos ( + chapter_info_id TEXT PRIMARY KEY, + chapter_id TEXT, + act_id INTEGER, + incident_id TEXT, + plot_point_id TEXT, + book_id TEXT, + author_id TEXT, + summary TEXT, + goal TEXT, + last_update INTEGER DEFAULT 0, + FOREIGN KEY (chapter_id) REFERENCES book_chapters(chapter_id) ON DELETE CASCADE, + FOREIGN KEY (incident_id) REFERENCES book_incidents(incident_id) ON DELETE CASCADE, + FOREIGN KEY (plot_point_id) REFERENCES book_plot_points(plot_point_id) ON DELETE CASCADE + ) + `, 'chapter_info_id, chapter_id, act_id, incident_id, plot_point_id, book_id, author_id, summary, goal, last_update'); console.log('[Migration] Migration v2 completed'); } diff --git a/electron/ipc/book.ipc.ts b/electron/ipc/book.ipc.ts index baa984d..186c7a2 100644 --- a/electron/ipc/book.ipc.ts +++ b/electron/ipc/book.ipc.ts @@ -1,6 +1,6 @@ import { ipcMain } from 'electron'; import { createHandler } from '../database/LocalSystem.js'; -import Book from '../database/models/Book.js'; +import Book, {CompleteBook, SyncedBook} from '../database/models/Book.js'; import type { BookProps, GuideLine, GuideLineAI, Act, Issue, WorldProps } from '../database/models/Book.js'; import Chapter from '../database/models/Chapter.js'; import type { ChapterProps } from '../database/models/Chapter.js'; @@ -88,6 +88,10 @@ interface SetAIGuideLineData { themes: string; } +interface GetGuidelineData { + id: string; +} + // GET /books - Get all books ipcMain.handle('db:book:books', createHandler( async function(userId: string, _body: void, lang: 'fr' | 'en'):Promise { @@ -96,6 +100,20 @@ ipcMain.handle('db:book:books', createHandler( ) ); +// GET /books/synced - Get all synced books +ipcMain.handle('db:books:synced', createHandler( + async function(userId: string, _body: void, lang: 'fr' | 'en'):Promise { + return await Book.getSyncedBooks(userId, lang); + }) +); + +// POST /book/sync/save - Save complete book +ipcMain.handle('db:book:syncSave', createHandler( + async function(userId: string, data: CompleteBook, lang: 'fr' | 'en'):Promise { + return await Book.saveCompleteBook(userId, data, lang); + }) +); + // GET /book/:id - Get single book ipcMain.handle('db:book:bookBasicInformation', createHandler( async function(userId: string, bookId: string, lang: 'fr' | 'en'):Promise { @@ -113,11 +131,7 @@ ipcMain.handle('db:book:updateBasicInformation', createHandler(async function(userId: string, data: GetGuidelineData, lang: 'fr' | 'en') { return await Book.getGuideLine(userId, data.id, lang); } diff --git a/lib/locales/en.json b/lib/locales/en.json index 2ffb4b6..4b4f152 100644 --- a/lib/locales/en.json +++ b/lib/locales/en.json @@ -115,7 +115,13 @@ "bookCard": { "noCoverAlt": "No cover", "initialsSeparator": ".", - "subtitlePlaceholder": "No subtitle" + "subtitlePlaceholder": "No subtitle", + "synced": "Synced", + "localOnly": "Local only", + "serverOnly": "Server only", + "toSyncFromServer": "Download from server", + "toSyncToServer": "Upload to server", + "sync": "Sync" }, "scribeTopBar": { "logoAlt": "Logo", diff --git a/lib/locales/fr.json b/lib/locales/fr.json index fe33797..7a6df84 100644 --- a/lib/locales/fr.json +++ b/lib/locales/fr.json @@ -115,7 +115,13 @@ "bookCard": { "noCoverAlt": "Pas de couverture", "initialsSeparator": ".", - "subtitlePlaceholder": "Aucun sous-titre" + "subtitlePlaceholder": "Aucun sous-titre", + "synced": "Synchronisé", + "localOnly": "Local uniquement", + "serverOnly": "Sur le serveur uniquement", + "toSyncFromServer": "Télécharger depuis le serveur", + "toSyncToServer": "Envoyer vers le serveur", + "sync": "Synchroniser" }, "scribeTopBar": { "logoAlt": "Logo", diff --git a/lib/models/Book.ts b/lib/models/Book.ts index 8441675..66bd300 100644 --- a/lib/models/Book.ts +++ b/lib/models/Book.ts @@ -1,6 +1,61 @@ import {Author} from './User'; import {ActChapter, ChapterProps} from "@/lib/models/Chapter"; import {SelectBoxProps} from "@/shared/interface"; +import { + BookActSummariesTable, + BookAIGuideLineTable, + BookChapterContentTable, BookChapterInfosTable, + BookChaptersTable, BookCharactersAttributesTable, BookCharactersTable, BookGuideLineTable, BookIncidentsTable, + BookIssuesTable, BookLocationTable, BookPlotPointsTable, BookWorldElementsTable, BookWorldTable, + EritBooksTable, LocationElementTable, LocationSubElementTable +} from "@/lib/models/BookTables"; +import { + SyncedActSummary, SyncedAIGuideLine, + SyncedChapter, + SyncedCharacter, SyncedGuideLine, + SyncedIncident, SyncedIssue, + SyncedLocation, + SyncedPlotPoint, + SyncedWorld +} from "@/lib/models/SyncedBook"; + +export interface CompleteBook { + eritBooks: EritBooksTable[]; + actSummaries: BookActSummariesTable[]; + aiGuideLine: BookAIGuideLineTable[]; + chapters: BookChaptersTable[]; + chapterContents: BookChapterContentTable[]; + chapterInfos: BookChapterInfosTable[]; + characters: BookCharactersTable[]; + characterAttributes: BookCharactersAttributesTable[]; + guideLine: BookGuideLineTable[]; + incidents: BookIncidentsTable[]; + issues: BookIssuesTable[]; + locations: BookLocationTable[]; + plotPoints: BookPlotPointsTable[]; + worlds: BookWorldTable[]; + worldElements: BookWorldElementsTable[]; + locationElements: LocationElementTable[]; + locationSubElements: LocationSubElementTable[]; +} + +export interface SyncedBook { + id: string; + type: string; + title: string; + subTitle: string | null; + lastUpdate: number; + chapters: SyncedChapter[]; + characters: SyncedCharacter[]; + locations: SyncedLocation[]; + worlds: SyncedWorld[]; + incidents: SyncedIncident[]; + plotPoints: SyncedPlotPoint[]; + issues: SyncedIssue[]; + actSummaries: SyncedActSummary[]; + guideLine: SyncedGuideLine | null; + aiGuideLine: SyncedAIGuideLine | null; +} export interface BookProps { bookId: string; @@ -30,6 +85,7 @@ export interface BookListProps { wordCount?: number; coverImage?: string; bookMeta?: string; + itIsLocal?: boolean; } export interface GuideLine { diff --git a/lib/models/BookTables.ts b/lib/models/BookTables.ts new file mode 100644 index 0000000..868fdde --- /dev/null +++ b/lib/models/BookTables.ts @@ -0,0 +1,182 @@ +export interface EritBooksTable { + book_id:string; + type:string; + author_id:string; + title:string; + hashed_title:string; + sub_title:string|null; + hashed_sub_title:string|null; + summary:string|null; + serie_id:number|null; + desired_release_date:string|null; + desired_word_count:number|null; + words_count:number|null; + cover_image:string|null; +} + +export interface BookActSummariesTable { + act_sum_id: string; + book_id: string; + user_id: string; + act_index: number; + summary: string | null; +} + +export interface BookAIGuideLineTable { + user_id: string; + book_id: string; + global_resume: string | null; + themes: string | null; + verbe_tense: number | null; + narrative_type: number | null; + langue: number | null; + dialogue_type: number | null; + tone: string | null; + atmosphere: string | null; + current_resume: string | null; +} + +export interface BookChaptersTable { + chapter_id: string; + book_id: string; + author_id: string; + title: string; + hashed_title: string | null; + words_count: number | null; + chapter_order: number | null; +} + +export interface BookChapterContentTable { + content_id: string; + chapter_id: string; + author_id: string; + version: number; + content: string | null; + words_count: number; + time_on_it: number; +} + +export interface BookChapterInfosTable { + chapter_info_id: string; + chapter_id: string; + act_id: number | null; + incident_id: string | null; + plot_point_id: string | null; + book_id: string; + author_id: string; + summary: string | null; + goal: string | null; +} + +export interface BookCharactersTable { + character_id: string; + book_id: string; + user_id: string; + first_name: string; + last_name: string | null; + category: string; + title: string | null; + image: string | null; + role: string | null; + biography: string | null; + history: string | null; +} + +export interface BookCharactersAttributesTable { + attr_id: string; + character_id: string; + user_id: string; + attribute_name: string; + attribute_value: string; +} + +export interface BookGuideLineTable { + user_id: string; + book_id: string; + tone: string | null; + atmosphere: string | null; + writing_style: string | null; + themes: string | null; + symbolism: string | null; + motifs: string | null; + narrative_voice: string | null; + pacing: string | null; + intended_audience: string | null; + key_messages: string | null; +} + +export interface BookIncidentsTable { + incident_id: string; + author_id: string; + book_id: string; + title: string; + hashed_title: string; + summary: string | null; +} + +export interface BookIssuesTable { + issue_id: string; + author_id: string; + book_id: string; + name: string; + hashed_issue_name: string; +} + +export interface BookLocationTable { + loc_id: string; + book_id: string; + user_id: string; + loc_name: string; + loc_original_name: string; +} + +export interface BookPlotPointsTable { + plot_point_id: string; + title: string; + hashed_title: string; + summary: string | null; + linked_incident_id: string | null; + author_id: string; + book_id: string; +} + +export interface BookWorldTable { + world_id: string; + name: string; + hashed_name: string; + author_id: string; + book_id: string; + history: string | null; + politics: string | null; + economy: string | null; + religion: string | null; + languages: string | null; +} + +export interface BookWorldElementsTable { + element_id: string; + world_id: string; + user_id: string; + element_type: number; + name: string; + original_name: string; + description: string | null; +} + +export interface LocationElementTable { + element_id: string; + location: string; + user_id: string; + element_name: string; + original_name: string; + element_description: string | null; +} + +export interface LocationSubElementTable { + sub_element_id: string; + element_id: string; + user_id: string; + sub_elem_name: string; + original_name: string; + sub_elem_description: string | null; +} \ No newline at end of file diff --git a/lib/models/SyncedBook.ts b/lib/models/SyncedBook.ts new file mode 100644 index 0000000..098b5c3 --- /dev/null +++ b/lib/models/SyncedBook.ts @@ -0,0 +1,112 @@ +export interface SyncedBook { + id: string; + type: string; + title: string; + subTitle: string | null; + lastUpdate: number; + chapters: SyncedChapter[]; + characters: SyncedCharacter[]; + locations: SyncedLocation[]; + worlds: SyncedWorld[]; + incidents: SyncedIncident[]; + plotPoints: SyncedPlotPoint[]; + issues: SyncedIssue[]; + actSummaries: SyncedActSummary[]; + guideLine: SyncedGuideLine | null; + aiGuideLine: SyncedAIGuideLine | null; +} + +export interface SyncedChapter { + id: string; + name: string; + lastUpdate: number; + contents: SyncedChapterContent[]; + info: SyncedChapterInfo | null; +} + +export interface SyncedChapterContent { + id: string; + lastUpdate: number; +} + +export interface SyncedChapterInfo { + id: string; + lastUpdate: number; +} + +export interface SyncedCharacter { + id: string; + name: string; + lastUpdate: number; + attributes: SyncedCharacterAttribute[]; +} + +export interface SyncedCharacterAttribute { + id: string; + name: string; + lastUpdate: number; +} + +export interface SyncedLocation { + id: string; + name: string; + lastUpdate: number; + elements: SyncedLocationElement[]; +} + +export interface SyncedLocationElement { + id: string; + name: string; + lastUpdate: number; + subElements: SyncedLocationSubElement[]; +} + +export interface SyncedLocationSubElement { + id: string; + name: string; + lastUpdate: number; +} + +export interface SyncedWorld { + id: string; + name: string; + lastUpdate: number; + elements: SyncedWorldElement[]; +} + +export interface SyncedWorldElement { + id: string; + name: string; + lastUpdate: number; +} + +export interface SyncedIncident { + id: string; + name: string; + lastUpdate: number; +} + +export interface SyncedPlotPoint { + id: string; + name: string; + lastUpdate: number; +} + +export interface SyncedIssue { + id: string; + name: string; + lastUpdate: number; +} + +export interface SyncedActSummary { + id: string; + lastUpdate: number; +} + +export interface SyncedGuideLine { + lastUpdate: number; +} + +export interface SyncedAIGuideLine { + lastUpdate: number; +} \ No newline at end of file diff --git a/lib/models/User.ts b/lib/models/User.ts index fc7ddd5..7edaa14 100644 --- a/lib/models/User.ts +++ b/lib/models/User.ts @@ -22,7 +22,6 @@ export interface UserProps { openai: boolean, anthropic: boolean, }, - books?: BookProps[]; guideTour?: GuideTour[]; subscription?: Subscription[]; writingLang: number;