Add BooksSyncContext, refine database schema, and enhance synchronization support

- Introduce `BooksSyncContext` for managing book synchronization states (server-only, local-only, to-sync, etc.).
- Remove `UserContext` and related dependencies.
- Refine localization strings (`en.json`) with sync-related updates (e.g., "toSyncFromServer", "toSyncToServer").
- Extend database schema with additional tables and fields for syncing books, chapters, and related entities.
- Add `last_update` fields and update corresponding repository methods to support synchronization logic.
- Enhance IPC handlers with stricter typing, data validation, and sync-aware operations.
This commit is contained in:
natreex
2025-12-07 14:36:03 -05:00
parent db2c88a42d
commit bb331b5c22
22 changed files with 2594 additions and 370 deletions

View File

@@ -33,6 +33,8 @@ import OfflineProvider from "@/context/OfflineProvider";
import OfflineContext from "@/context/OfflineContext"; import OfflineContext from "@/context/OfflineContext";
import OfflinePinSetup from "@/components/offline/OfflinePinSetup"; import OfflinePinSetup from "@/components/offline/OfflinePinSetup";
import OfflinePinVerify from "@/components/offline/OfflinePinVerify"; import OfflinePinVerify from "@/components/offline/OfflinePinVerify";
import {SyncedBook} from "@/lib/models/SyncedBook";
import {BooksSyncContext} from "@/context/BooksSyncContext";
const messagesMap = { const messagesMap = {
fr: frMessages, fr: frMessages,
@@ -60,6 +62,13 @@ function ScribeContent() {
const [currentChapter, setCurrentChapter] = useState<ChapterProps | undefined>(undefined); const [currentChapter, setCurrentChapter] = useState<ChapterProps | undefined>(undefined);
const [currentBook, setCurrentBook] = useState<BookProps | null>(null); const [currentBook, setCurrentBook] = useState<BookProps | null>(null);
const [serverSyncedBooks, setServerSyncedBooks] = useState<SyncedBook[]>([]);
const [localSyncedBooks, setLocalSyncedBooks] = useState<SyncedBook[]>([]);
const [booksToSyncFromServer, setBooksToSyncFromServer] = useState<SyncedBook[]>([]);
const [booksToSyncToServer, setBooksToSyncToServer] = useState<SyncedBook[]>([]);
const [serverOnlyBooks, setServerOnlyBooks] = useState<SyncedBook[]>([]);
const [localOnlyBooks, setLocalOnlyBooks] = useState<SyncedBook[]>([]);
const [currentCredits, setCurrentCredits] = useState<number>(160); const [currentCredits, setCurrentCredits] = useState<number>(160);
const [amountSpent, setAmountSpent] = useState<number>(session.user?.aiUsage || 0); const [amountSpent, setAmountSpent] = useState<number>(session.user?.aiUsage || 0);
@@ -143,6 +152,7 @@ function ScribeContent() {
useEffect((): void => { useEffect((): void => {
if (session.isConnected) { if (session.isConnected) {
getBooks().then()
setIsTermsAccepted(session.user?.termsAccepted ?? false); setIsTermsAccepted(session.user?.termsAccepted ?? false);
setHomeStepsGuide(User.guideTourDone(session.user?.guideTour ?? [], 'home-basic')); setHomeStepsGuide(User.guideTourDone(session.user?.guideTour ?? [], 'home-basic'));
setIsLoading(false); setIsLoading(false);
@@ -155,7 +165,45 @@ function ScribeContent() {
} }
}, [currentBook]); }, [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<void> {
try {
let localBooksResponse: SyncedBook[]
if (!isCurrentlyOffline()){
localBooksResponse = await window.electron.invoke<SyncedBook[]>('db:books:synced');
} else {
localBooksResponse = [];
}
const serverBooksResponse: SyncedBook[] = await System.authGetQueryToServer<SyncedBook[]>('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 => { useEffect(():void => {
async function checkPinSetup() { async function checkPinSetup() {
if (session.isConnected && window.electron) { if (session.isConnected && window.electron) {
@@ -179,6 +227,7 @@ function ScribeContent() {
} }
checkPinSetup().then(); checkPinSetup().then();
}, [session.isConnected]); }, [session.isConnected]);
async function handlePinVerifySuccess(userId: string): Promise<void> { async function handlePinVerifySuccess(userId: string): Promise<void> {
@@ -186,6 +235,8 @@ function ScribeContent() {
try { try {
if (window.electron) { if (window.electron) {
const storedToken: string | null = await window.electron.getToken();
const encryptionKey:string|null = await window.electron.getUserEncryptionKey(userId); const encryptionKey:string|null = await window.electron.getUserEncryptionKey(userId);
if (encryptionKey) { if (encryptionKey) {
await window.electron.dbInitialize(userId, encryptionKey); await window.electron.dbInitialize(userId, encryptionKey);
@@ -195,7 +246,7 @@ function ScribeContent() {
setSession({ setSession({
isConnected: true, isConnected: true,
user: localUser, user: localUser,
accessToken: 'offline', // Special offline token accessToken: storedToken || '',
}); });
setShowPinVerify(false); setShowPinVerify(false);
setCurrentCredits(localUser.creditsBalance || 0); setCurrentCredits(localUser.creditsBalance || 0);
@@ -225,7 +276,7 @@ function ScribeContent() {
async function handleHomeTour(): Promise<void> { async function handleHomeTour(): Promise<void> {
try { try {
const response: boolean = await System.authPostToServer<boolean>('logs/tour', { const response: boolean = await System.authPostToServer<boolean>('logs/tour', {
plateforme: 'web', plateforme: 'desktop',
tour: 'home-basic' tour: 'home-basic'
}, },
session.accessToken, session.accessToken,
@@ -260,7 +311,6 @@ function ScribeContent() {
const user: UserProps = await System.authGetQueryToServer<UserProps>('user/infos', token, locale); const user: UserProps = await System.authGetQueryToServer<UserProps>('user/infos', token, locale);
if (!user) { if (!user) {
errorMessage(t("homePage.errors.userNotFound")); errorMessage(t("homePage.errors.userNotFound"));
// Token invalide, supprimer et logout
if (window.electron) { if (window.electron) {
await window.electron.removeToken(); await window.electron.removeToken();
window.electron.logout(); window.electron.logout();
@@ -272,8 +322,9 @@ function ScribeContent() {
try { try {
const initResult = await window.electron.initUser(user.id); const initResult = await window.electron.initUser(user.id);
if (!initResult.success) { if (!initResult.success) {
console.error('[Page] Failed to initialize user:', initResult.error); errorMessage(initResult.error || t("homePage.errors.offlineInitError"));
} else { return;
}
try { try {
const offlineStatus = await window.electron.offlineModeGet(); const offlineStatus = await window.electron.offlineModeGet();
if (!offlineStatus.hasPin) { if (!offlineStatus.hasPin) {
@@ -284,7 +335,6 @@ function ScribeContent() {
} catch (error) { } catch (error) {
console.error('[Page] Error checking offline mode:', error); console.error('[Page] Error checking offline mode:', error);
} }
}
} catch (error) { } catch (error) {
console.error('[Page] Error initializing user:', error); console.error('[Page] Error initializing user:', error);
} }
@@ -358,7 +408,6 @@ function ScribeContent() {
} catch (error) { } catch (error) {
console.error('[Auth] Error checking offline mode:', error); console.error('[Auth] Error checking offline mode:', error);
} }
window.electron.logout(); window.electron.logout();
} }
} }
@@ -439,6 +488,7 @@ function ScribeContent() {
return ( return (
<SessionContext.Provider value={{session: session, setSession: setSession}}> <SessionContext.Provider value={{session: session, setSession: setSession}}>
<BooksSyncContext.Provider value={{serverSyncedBooks, localSyncedBooks, booksToSyncFromServer, booksToSyncToServer, serverOnlyBooks, localOnlyBooks}}>
<BookContext.Provider value={{book: currentBook, setBook: setCurrentBook}}> <BookContext.Provider value={{book: currentBook, setBook: setCurrentBook}}>
<ChapterContext.Provider value={{chapter: currentChapter, setChapter: setCurrentChapter}}> <ChapterContext.Provider value={{chapter: currentChapter, setChapter: setCurrentChapter}}>
<AIUsageContext.Provider value={{ <AIUsageContext.Provider value={{
@@ -493,6 +543,7 @@ function ScribeContent() {
</AIUsageContext.Provider> </AIUsageContext.Provider>
</ChapterContext.Provider> </ChapterContext.Provider>
</BookContext.Provider> </BookContext.Provider>
</BooksSyncContext.Provider>
</SessionContext.Provider> </SessionContext.Provider>
); );
} }

138
components/SyncBook.tsx Normal file
View File

@@ -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<SessionContextProps>(SessionContext);
const {lang} = useContext(LangContext);
const {errorMessage} = useContext<AlertContextProps>(AlertContext);
const {isCurrentlyOffline} = useContext<OfflineContextType>(OfflineContext);
const [isLoading, setIsLoading] = useState<boolean>(false);
const [currentStatus, setCurrentStatus] = useState<SyncType>(status);
const isOffline: boolean = isCurrentlyOffline();
async function upload(): Promise<void> {
// TODO: Implement upload local-only book to server
}
async function download(): Promise<void> {
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<boolean>('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<void> {
// TODO: Implement sync from server (server has newer version)
}
async function syncToServer(): Promise<void> {
// TODO: Implement sync to server (local has newer version)
}
if (isLoading) {
return (
<div className="flex items-center gap-2">
<span className="text-primary">
<FontAwesomeIcon icon={faSpinner} className="w-4 h-4 animate-spin"/>
</span>
</div>
);
}
return (
<div className="flex items-center gap-2">
{/* Fully synced - no action needed */}
{currentStatus === 'synced' && (
<span
className="text-gray-light"
title={t("bookCard.synced")}
>
<FontAwesomeIcon icon={faCloud} className="w-4 h-4"/>
</span>
)}
{/* Local only - can upload to server */}
{currentStatus === 'local-only' && (
<button
onClick={upload}
className={`transition-colors ${isOffline ? 'text-gray-dark cursor-not-allowed' : 'text-gray hover:text-primary cursor-pointer'}`}
title={t("bookCard.localOnly")}
type="button"
disabled={isOffline}
>
<FontAwesomeIcon icon={faCloudArrowUp} className="w-4 h-4"/>
</button>
)}
{/* Server only - can download to local */}
{currentStatus === 'server-only' && (
<button
onClick={download}
className={`transition-colors ${isOffline ? 'text-gray-dark cursor-not-allowed' : 'text-gray hover:text-primary cursor-pointer'}`}
title={t("bookCard.serverOnly")}
type="button"
disabled={isOffline}
>
<FontAwesomeIcon icon={faCloudArrowDown} className="w-4 h-4"/>
</button>
)}
{/* Needs to sync from server (server has newer version) */}
{currentStatus === 'to-sync-from-server' && (
<button
onClick={syncFromServer}
className={`transition-colors ${isOffline ? 'text-gray-dark cursor-not-allowed' : 'text-warning hover:text-primary cursor-pointer'}`}
title={t("bookCard.toSyncFromServer")}
type="button"
disabled={isOffline}
>
<FontAwesomeIcon icon={faCloudArrowDown} className="w-4 h-4"/>
</button>
)}
{/* Needs to sync to server (local has newer version) */}
{currentStatus === 'to-sync-to-server' && (
<button
onClick={syncToServer}
className={`transition-colors ${isOffline ? 'text-gray-dark cursor-not-allowed' : 'text-warning hover:text-primary cursor-pointer'}`}
title={t("bookCard.toSyncToServer")}
type="button"
disabled={isOffline}
>
<FontAwesomeIcon icon={faCloudArrowUp} className="w-4 h-4"/>
</button>
)}
</div>
);
}

View File

@@ -3,19 +3,22 @@ import {BookProps} from "@/lib/models/Book";
import DeleteBook from "@/components/book/settings/DeleteBook"; import DeleteBook from "@/components/book/settings/DeleteBook";
import ExportBook from "@/components/ExportBook"; import ExportBook from "@/components/ExportBook";
import {useTranslations} from "next-intl"; import {useTranslations} from "next-intl";
import SyncBook from "@/components/SyncBook";
import {SyncType} from "@/context/BooksSyncContext";
import {useEffect} from "react";
export default function BookCard( interface BookCardProps {
{ book: BookProps;
book, onClickCallback: (bookId: string) => void;
onClickCallback,
index
}: {
book: BookProps,
onClickCallback: Function;
index: number; index: number;
}) { syncStatus: SyncType;
const t = useTranslations(); }
export default function BookCard({book, onClickCallback, index, syncStatus}: BookCardProps) {
const t = useTranslations();
useEffect(() => {
console.log(syncStatus)
}, [syncStatus]);
return ( return (
<div <div
className="group bg-tertiary/90 backdrop-blur-sm rounded-2xl shadow-lg hover:shadow-2xl transition-all duration-300 h-full border border-secondary/50 hover:border-primary/50 flex flex-col hover:scale-105"> className="group bg-tertiary/90 backdrop-blur-sm rounded-2xl shadow-lg hover:shadow-2xl transition-all duration-300 h-full border border-secondary/50 hover:border-primary/50 flex flex-col hover:scale-105">
@@ -66,8 +69,7 @@ export default function BookCard(
</div> </div>
</div> </div>
<div className="flex justify-between items-center pt-3 border-t border-secondary/30"> <div className="flex justify-between items-center pt-3 border-t border-secondary/30">
<span <SyncBook status={syncStatus} bookId={book.bookId}/>
className="bg-primary/10 text-primary text-xs px-3 py-1 rounded-full font-medium border border-primary/30"></span>
<div className="flex items-center gap-1" {...index === 0 && {'data-guide': 'bottom-book-card'}}> <div className="flex items-center gap-1" {...index === 0 && {'data-guide': 'bottom-book-card'}}>
<ExportBook bookTitle={book.title} bookId={book.bookId}/> <ExportBook bookTitle={book.title} bookId={book.bookId}/>
<DeleteBook bookId={book.bookId}/> <DeleteBook bookId={book.bookId}/>

View File

@@ -14,6 +14,8 @@ import User from "@/lib/models/User";
import {useTranslations} from "next-intl"; import {useTranslations} from "next-intl";
import {LangContext, LangContextProps} from "@/context/LangContext"; import {LangContext, LangContextProps} from "@/context/LangContext";
import OfflineContext, {OfflineContextType} from "@/context/OfflineContext"; import OfflineContext, {OfflineContextType} from "@/context/OfflineContext";
import {BooksSyncContext, BooksSyncContextProps, SyncType} from "@/context/BooksSyncContext";
import {SyncedBook} from "@/lib/models/SyncedBook";
export default function BookList() { export default function BookList() {
const {session, setSession} = useContext(SessionContext); const {session, setSession} = useContext(SessionContext);
@@ -23,6 +25,7 @@ export default function BookList() {
const t = useTranslations(); const t = useTranslations();
const {lang} = useContext<LangContextProps>(LangContext) const {lang} = useContext<LangContextProps>(LangContext)
const {isCurrentlyOffline} = useContext<OfflineContextType>(OfflineContext) const {isCurrentlyOffline} = useContext<OfflineContextType>(OfflineContext)
const {booksToSyncFromServer, booksToSyncToServer, serverOnlyBooks, localOnlyBooks} = useContext<BooksSyncContextProps>(BooksSyncContext)
const [searchQuery, setSearchQuery] = useState<string>(''); const [searchQuery, setSearchQuery] = useState<string>('');
const [groupedBooks, setGroupedBooks] = useState<Record<string, BookProps[]>>({}); const [groupedBooks, setGroupedBooks] = useState<Record<string, BookProps[]>>({});
@@ -86,7 +89,7 @@ export default function BookList() {
useEffect((): void => { useEffect((): void => {
getBooks().then() getBooks().then()
}, [session.user?.books]); }, [booksToSyncFromServer, booksToSyncToServer, serverOnlyBooks, localOnlyBooks]);
useEffect((): void => { useEffect((): void => {
if (accessToken) getBooks().then(); if (accessToken) getBooks().then();
@@ -115,11 +118,21 @@ export default function BookList() {
async function getBooks(): Promise<void> { async function getBooks(): Promise<void> {
setIsLoadingBooks(true); setIsLoadingBooks(true);
try { try {
let bookResponse: BookListProps[] = []; let bookResponse: (BookListProps & { itIsLocal: boolean })[] = [];
if (!isCurrentlyOffline()) { if (!isCurrentlyOffline()) {
bookResponse = await System.authGetQueryToServer<BookListProps[]>('books', accessToken, lang); const [onlineBooks, localBooks]: [BookListProps[], BookListProps[]] = await Promise.all([
System.authGetQueryToServer<BookListProps[]>('books', accessToken, lang),
window.electron.invoke<BookListProps[]>('db:book:books')
]);
const onlineBookIds: Set<string> = 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 { } else {
bookResponse = await window.electron.invoke<BookListProps[]>('db:book:books'); const localBooks: BookListProps[] = await window.electron.invoke<BookListProps[]>('db:book:books');
bookResponse = localBooks.map((book: BookListProps): BookListProps & { itIsLocal: boolean } => ({ ...book, itIsLocal: true }));
} }
if (bookResponse) { if (bookResponse) {
const booksByType: Record<string, BookProps[]> = bookResponse.reduce((groups: Record<string, BookProps[]>, book: BookListProps): Record<string, BookProps[]> => { const booksByType: Record<string, BookProps[]> = bookResponse.reduce((groups: Record<string, BookProps[]>, book: BookListProps): Record<string, BookProps[]> => {
@@ -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<void> { async function getBook(bookId: string): Promise<void> {
try { try {
let bookResponse: BookListProps|null = null; let bookResponse: BookListProps|null = null;
@@ -267,8 +296,10 @@ export default function BookList() {
{...(idx === 0 && {'data-guide': 'book-card'})} {...(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]'}`}> 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]'}`}>
<BookCard book={book} <BookCard book={book}
syncStatus={detectBookSyncStatus(book.bookId)}
onClickCallback={getBook} onClickCallback={getBook}
index={idx}/> index={idx}
/>
</div> </div>
)) ))
} }

View File

@@ -300,7 +300,7 @@ export default function TextEditor() {
content, content,
totalWordCount: editor.getText().length, totalWordCount: editor.getText().length,
currentTime: mainTimer currentTime: mainTimer
}, session?.accessToken ?? ''); }, session?.accessToken, lang);
} }
if (!response) { if (!response) {
errorMessage(t('editor.error.savedFailed')); errorMessage(t('editor.error.savedFailed'));

View File

@@ -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<BooksSyncContextProps> = createContext<BooksSyncContextProps>({
serverSyncedBooks:[],
localSyncedBooks:[],
booksToSyncFromServer:[],
booksToSyncToServer:[],
serverOnlyBooks:[],
localOnlyBooks:[]
})

View File

@@ -1,25 +0,0 @@
import {UserProps} from "@/lib/models/User";
import {Context, createContext} from "react";
export interface UserContextProps {
user: UserProps
}
export const UserContext: Context<UserContextProps> = createContext<UserContextProps>({
user: {
id: '',
name: '',
lastName: '',
username: '',
writingLang: 0,
writingLevel: 0,
ritePoints: 0,
groupId: 0,
aiUsage: 0,
apiKeys: {
openai: false,
anthropic: false,
gemini: false,
}
}
})

View File

@@ -16,6 +16,11 @@ export default class System {
return encryptDataWithUserKey(data, userKey); 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 { public static decryptDataWithUserKey(encryptedData: string, userKey: string): string {
return decryptDataWithUserKey(encryptedData, userKey); return decryptDataWithUserKey(encryptedData, userKey);
} }

View File

@@ -28,9 +28,8 @@ export class DatabaseService {
this.close(); this.close();
} }
// Get user data directory const userDataPath:string = app.getPath('userData');
const userDataPath = app.getPath('userData'); const dbPath:string = path.join(userDataPath, `eritors-local.db`);
const dbPath = path.join(userDataPath, `eritors-local-${userId}.db`);
this.db = new sqlite3.Database(dbPath); this.db = new sqlite3.Database(dbPath);
this.userEncryptionKey = encryptionKey; this.userEncryptionKey = encryptionKey;

View File

@@ -1,21 +1,45 @@
import BookRepository, {
BookCoverQuery,
BookQuery,
ChapterBookResult, GuideLineAIQuery,
GuideLineQuery, WorldElementValue
} from '../repositories/book.repository.js';
import type { import type {
IssueQuery,
ActQuery, ActQuery,
PlotPointQuery, BookActSummariesTable, BookAIGuideLineTable, BookChapterContentTable, BookChapterInfosTable, BookChaptersTable,
BookCharactersAttributesTable,
BookCharactersTable, BookGuideLineTable, BookIncidentsTable, BookIssuesTable, BookLocationTable,
BookPlotPointsTable, BookWorldElementsTable, BookWorldTable,
EritBooksTable,
IncidentQuery, IncidentQuery,
IssueQuery, LocationElementTable, LocationSubElementTable,
PlotPointQuery,
SyncedActSummaryResult,
SyncedAIGuideLineResult,
SyncedBookResult,
SyncedChapterContentResult,
SyncedChapterInfoResult,
SyncedChapterResult,
SyncedCharacterAttributeResult,
SyncedCharacterResult,
SyncedGuideLineResult,
SyncedIncidentResult,
SyncedIssueResult,
SyncedLocationElementResult,
SyncedLocationResult,
SyncedLocationSubElementResult,
SyncedPlotPointResult,
SyncedWorldElementResult,
SyncedWorldResult,
WorldQuery WorldQuery
} from '../repositories/book.repository.js'; } 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 System from '../System.js';
import { getUserEncryptionKey } from '../keyManager.js'; import {getUserEncryptionKey} from '../keyManager.js';
import path from "path"; import path from "path";
import fs from "fs"; import fs from "fs";
import BookRepo from "../repositories/book.repository.js";
import Chapter, {ActChapter, ChapterContentData, ChapterProps} from "./Chapter.js"; import Chapter, {ActChapter, ChapterContentData, ChapterProps} from "./Chapter.js";
import UserRepo from "../repositories/user.repository.js"; import UserRepo from "../repositories/user.repository.js";
import ChapterRepo from "../repositories/chapter.repository.js"; import ChapterRepo from "../repositories/chapter.repository.js";
@@ -35,6 +59,139 @@ export interface BookProps{
bookMeta?:string; 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{ export interface GuideLine{
tone:string; tone:string;
atmosphere:string; atmosphere:string;
@@ -928,4 +1085,385 @@ export default class Book {
this.cover = ''; this.cover = '';
} }
} }
static async getSyncedBooks(userId: string, lang: 'fr' | 'en'):Promise<SyncedBook[]> {
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<boolean> {
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);
});
}
} }

File diff suppressed because it is too large Load Diff

View File

@@ -78,7 +78,7 @@ export default class ChapterRepo{
let result: RunResult; let result: RunResult;
try { try {
const db: Database = System.getDb(); 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) { } catch (e: unknown) {
if (e instanceof Error) { if (e instanceof Error) {
console.error(`DB Error: ${e.message}`); 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."); throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred.");
} }
} }
if (result.changes > 0) { if (!result || result.changes === 0) {
return chapterId;
} else {
throw new Error(lang === 'fr' ? `Une erreur s'est passé lors de l'ajout du chapitre.` : `Error adding chapter.`); 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 { public static fetchWholeChapter(userId: string, chapterId: string, version: number, lang: 'fr' | 'en' = 'fr'): ChapterContentQueryResult {
@@ -197,7 +196,7 @@ export default class ChapterRepo{
} }
try { try {
const db: Database = System.getDb(); 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) { } catch (e: unknown) {
if (e instanceof Error) { if (e instanceof Error) {
console.error(`DB Error: ${e.message}`); 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."); throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred.");
} }
} }
if (result.changes > 0) { if (!result || result.changes === 0) {
return chapterInfoId;
} else {
throw new Error(lang === 'fr' ? `Une erreur s'est produite pendant la liaison du chapitre.` : `Error linking chapter.`); 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 { public static updateChapter(userId: string, chapterId: string, encryptedTitle: string, hashTitle: string, chapterOrder: number, lang: 'fr' | 'en' = 'fr'): boolean {
try { try {
const db: Database = System.getDb(); 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; return result.changes > 0;
} catch (e: unknown) { } catch (e: unknown) {
if (e instanceof Error) { 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 { public static updateChapterContent(userId: string, chapterId: string, version: number, encryptContent: string, wordsCount: number, lang: 'fr' | 'en' = 'fr'): boolean {
try { try {
const db: Database = System.getDb(); 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) { if (result.changes > 0) {
return true; return true;
} else { } else {
const contentId:string = System.createUniqueId(); 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; return insertResult.changes > 0;
} }
} catch (e: unknown) { } catch (e: unknown) {

View File

@@ -54,7 +54,7 @@ export default class CharacterRepo {
let result: RunResult; let result: RunResult;
try { try {
const db: Database = System.getDb(); 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) { } catch (e: unknown) {
if (e instanceof Error) { if (e instanceof Error) {
console.error(`DB Error: ${e.message}`); 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."); throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred.");
} }
} }
if (result.changes > 0) { if (!result || result.changes === 0) {
return characterId;
} else {
throw new Error(lang === 'fr' ? `Une erreur s'est produite lors de l'ajout du personnage.` : `Error adding character.`); 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 { static insertAttribute(attributeId: string, characterId: string, userId: string, type: string, name: string, lang: 'fr' | 'en' = 'fr'): string {
let result: RunResult; let result: RunResult;
try { try {
const db: Database = System.getDb(); 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) { } catch (e: unknown) {
if (e instanceof Error) { if (e instanceof Error) {
console.error(`DB Error: ${e.message}`); 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."); throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred.");
} }
} }
if (result.changes > 0) { if (!result || result.changes === 0) {
return attributeId;
} else {
throw new Error(lang === 'fr' ? `Une erreur s'est produite lors de l'ajout de l'attribut.` : `Error adding attribute.`); 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 { 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 { try {
const db: Database = System.getDb(); 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; return result.changes > 0;
} catch (e: unknown) { } catch (e: unknown) {
if (e instanceof Error) { if (e instanceof Error) {

View File

@@ -51,7 +51,7 @@ export default class LocationRepo {
let result: RunResult; let result: RunResult;
try { try {
const db: Database = System.getDb(); 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) { } catch (e: unknown) {
if (e instanceof Error) { if (e instanceof Error) {
console.error(`DB Error: ${e.message}`); 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."); throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred.");
} }
} }
if (result.changes > 0) { if (!result || result.changes === 0) {
return locationId;
} else {
throw new Error(lang === 'fr' ? `Une erreur s'est produite lors de l'ajout de la section d'emplacement.` : `Error adding location section.`); 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 { static insertLocationElement(userId: string, elementId: string, locationId: string, encryptedName: string, originalName: string, lang: 'fr' | 'en' = 'fr'): string {
let result: RunResult; let result: RunResult;
try { try {
const db: Database = System.getDb(); 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) { } catch (e: unknown) {
if (e instanceof Error) { if (e instanceof Error) {
console.error(`DB Error: ${e.message}`); 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."); throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred.");
} }
} }
if (result.changes > 0) { if (!result || result.changes === 0) {
return elementId;
} else {
throw new Error(lang === 'fr' ? `Une erreur s'est produite lors de l'ajout de l'élément d'emplacement.` : `Error adding location element.`); 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 { static insertLocationSubElement(userId: string, subElementId: string, elementId: string, encryptedName: string, originalName: string, lang: 'fr' | 'en' = 'fr'): string {
let result: RunResult; let result: RunResult;
try { try {
const db: Database = System.getDb(); 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) { } catch (e: unknown) {
if (e instanceof Error) { if (e instanceof Error) {
console.error(`DB Error: ${e.message}`); 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."); throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred.");
} }
} }
if (result.changes > 0) { if (!result || result.changes === 0) {
return subElementId;
} else {
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.`); 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 { static updateLocationSubElement(userId: string, id: string, encryptedName: string, originalName: string, encryptDescription: string, lang: 'fr' | 'en' = 'fr'): boolean {
try { try {
const db: Database = System.getDb(); 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; return result.changes > 0;
} catch (e: unknown) { } catch (e: unknown) {
if (e instanceof Error) { 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 { static updateLocationElement(userId: string, id: string, encryptedName: string, originalName: string, encryptedDescription: string, lang: 'fr' | 'en' = 'fr'): boolean {
try { try {
const db: Database = System.getDb(); 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; return result.changes > 0;
} catch (e: unknown) { } catch (e: unknown) {
if (e instanceof Error) { 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 { static updateLocationSection(userId: string, id: string, encryptedName: string, originalName: string, lang: 'fr' | 'en' = 'fr'): boolean {
try { try {
const db: Database = System.getDb(); 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; return result.changes > 0;
} catch (e: unknown) { } catch (e: unknown) {
if (e instanceof Error) { if (e instanceof Error) {

View File

@@ -18,29 +18,6 @@ export function initializeSchema(db: Database): void {
// Enable foreign keys // Enable foreign keys
db.exec('PRAGMA foreign_keys = ON'); 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 // AI Conversations
db.exec(` db.exec(`
CREATE TABLE IF NOT EXISTS ai_conversations ( CREATE TABLE IF NOT EXISTS ai_conversations (
@@ -53,7 +30,6 @@ export function initializeSchema(db: Database): void {
user_id TEXT NOT NULL, user_id TEXT NOT NULL,
summary TEXT, summary TEXT,
convo_meta TEXT NOT NULL, convo_meta TEXT NOT NULL,
synced INTEGER DEFAULT 0,
FOREIGN KEY (book_id) REFERENCES erit_books(book_id) ON DELETE CASCADE 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, role TEXT NOT NULL,
message TEXT NOT NULL, message TEXT NOT NULL,
message_date INTEGER NOT NULL, message_date INTEGER NOT NULL,
synced INTEGER DEFAULT 0,
FOREIGN KEY (conversation_id) REFERENCES ai_conversations(conversation_id) ON DELETE CASCADE FOREIGN KEY (conversation_id) REFERENCES ai_conversations(conversation_id) ON DELETE CASCADE
); );
`); `);
@@ -75,7 +50,8 @@ export function initializeSchema(db: Database): void {
db.exec(` db.exec(`
CREATE TABLE IF NOT EXISTS book_acts ( CREATE TABLE IF NOT EXISTS book_acts (
act_id INTEGER PRIMARY KEY, 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, user_id TEXT NOT NULL,
act_index INTEGER NOT NULL, act_index INTEGER NOT NULL,
summary TEXT, summary TEXT,
synced INTEGER DEFAULT 0, last_update INTEGER DEFAULT 0,
FOREIGN KEY (book_id) REFERENCES erit_books(book_id) ON DELETE CASCADE FOREIGN KEY (book_id) REFERENCES erit_books(book_id) ON DELETE CASCADE
); );
`); `);
@@ -106,7 +82,7 @@ export function initializeSchema(db: Database): void {
tone TEXT, tone TEXT,
atmosphere TEXT, atmosphere TEXT,
current_resume TEXT, current_resume TEXT,
synced INTEGER DEFAULT 0, last_update INTEGER DEFAULT 0,
PRIMARY KEY (user_id, book_id), PRIMARY KEY (user_id, book_id),
FOREIGN KEY (book_id) REFERENCES erit_books(book_id) ON DELETE CASCADE 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, hashed_title TEXT,
words_count INTEGER, words_count INTEGER,
chapter_order 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 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, chapter_id TEXT NOT NULL,
author_id TEXT NOT NULL, author_id TEXT NOT NULL,
version INTEGER NOT NULL DEFAULT 2, version INTEGER NOT NULL DEFAULT 2,
content TEXT NOT NULL, content TEXT,
words_count INTEGER NOT NULL, words_count INTEGER NOT NULL,
time_on_it INTEGER NOT NULL DEFAULT 0, 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 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, plot_point_id TEXT,
book_id TEXT, book_id TEXT,
author_id TEXT, author_id TEXT,
summary TEXT NOT NULL, summary TEXT,
goal TEXT NOT NULL, goal TEXT,
synced INTEGER DEFAULT 0, last_update INTEGER DEFAULT 0,
FOREIGN KEY (chapter_id) REFERENCES book_chapters(chapter_id) ON DELETE CASCADE, 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 (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 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, role TEXT,
biography TEXT, biography TEXT,
history TEXT, history TEXT,
synced INTEGER DEFAULT 0, last_update INTEGER DEFAULT 0,
FOREIGN KEY (book_id) REFERENCES erit_books(book_id) ON DELETE CASCADE 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, user_id TEXT NOT NULL,
attribute_name TEXT NOT NULL, attribute_name TEXT NOT NULL,
attribute_value 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 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, type TEXT NOT NULL,
description TEXT NOT NULL, description TEXT NOT NULL,
history 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 ( CREATE TABLE IF NOT EXISTS book_guide_line (
user_id TEXT NOT NULL, user_id TEXT NOT NULL,
book_id TEXT NOT NULL, book_id TEXT NOT NULL,
tone TEXT NOT NULL, tone TEXT,
atmosphere TEXT NOT NULL, atmosphere TEXT,
writing_style TEXT NOT NULL, writing_style TEXT,
themes TEXT NOT NULL, themes TEXT,
symbolism TEXT NOT NULL, symbolism TEXT,
motifs TEXT NOT NULL, motifs TEXT,
narrative_voice TEXT NOT NULL, narrative_voice TEXT,
pacing TEXT NOT NULL, pacing TEXT,
intended_audience TEXT NOT NULL, intended_audience TEXT,
key_messages TEXT NOT NULL, key_messages TEXT,
synced INTEGER DEFAULT 0, last_update INTEGER DEFAULT 0,
PRIMARY KEY (user_id, book_id), PRIMARY KEY (user_id, book_id),
FOREIGN KEY (book_id) REFERENCES erit_books(book_id) ON DELETE CASCADE 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, title TEXT NOT NULL,
hashed_title TEXT NOT NULL, hashed_title TEXT NOT NULL,
summary TEXT, summary TEXT,
synced INTEGER DEFAULT 0, last_update INTEGER DEFAULT 0,
FOREIGN KEY (book_id) REFERENCES erit_books(book_id) ON DELETE CASCADE 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, book_id TEXT NOT NULL,
name TEXT NOT NULL, name TEXT NOT NULL,
hashed_issue_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 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, user_id TEXT NOT NULL,
loc_name TEXT NOT NULL, loc_name TEXT NOT NULL,
loc_original_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 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, linked_incident_id TEXT,
author_id TEXT NOT NULL, author_id TEXT NOT NULL,
book_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 FOREIGN KEY (book_id) REFERENCES erit_books(book_id) ON DELETE CASCADE
); );
`); `);
@@ -295,7 +271,7 @@ export function initializeSchema(db: Database): void {
economy TEXT, economy TEXT,
religion TEXT, religion TEXT,
languages TEXT, languages TEXT,
synced INTEGER DEFAULT 0, last_update INTEGER DEFAULT 0,
FOREIGN KEY (book_id) REFERENCES erit_books(book_id) ON DELETE CASCADE 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, name TEXT NOT NULL,
original_name TEXT NOT NULL, original_name TEXT NOT NULL,
description TEXT, description TEXT,
synced INTEGER DEFAULT 0, last_update INTEGER DEFAULT 0,
FOREIGN KEY (world_id) REFERENCES book_world(world_id) ON DELETE CASCADE 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, title TEXT NOT NULL,
hashed_title TEXT NOT NULL, hashed_title TEXT NOT NULL,
sub_title TEXT, sub_title TEXT,
hashed_sub_title TEXT NOT NULL, hashed_sub_title TEXT,
summary TEXT NOT NULL, summary TEXT,
serie_id INTEGER, serie_id INTEGER,
desired_release_date TEXT, desired_release_date TEXT,
desired_word_count INTEGER, desired_word_count INTEGER,
words_count INTEGER, words_count INTEGER,
cover_image TEXT, 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, interline TEXT NOT NULL,
paper_width INTEGER NOT NULL, paper_width INTEGER NOT NULL,
theme TEXT NOT NULL, theme TEXT NOT NULL,
focus INTEGER NOT NULL, focus INTEGER NOT NULL
synced INTEGER DEFAULT 0
); );
`); `);
@@ -381,8 +356,7 @@ export function initializeSchema(db: Database): void {
account_verified INTEGER NOT NULL DEFAULT 0, account_verified INTEGER NOT NULL DEFAULT 0,
erite_points INTEGER NOT NULL DEFAULT 100, erite_points INTEGER NOT NULL DEFAULT 100,
stripe_customer_id TEXT, stripe_customer_id TEXT,
credits_balance REAL DEFAULT 0, credits_balance REAL DEFAULT 0
synced INTEGER DEFAULT 0
); );
`); `);
@@ -395,7 +369,7 @@ export function initializeSchema(db: Database): void {
element_name TEXT NOT NULL, element_name TEXT NOT NULL,
original_name TEXT NOT NULL, original_name TEXT NOT NULL,
element_description TEXT, element_description TEXT,
synced INTEGER DEFAULT 0, last_update INTEGER DEFAULT 0,
FOREIGN KEY (location) REFERENCES book_location(loc_id) ON DELETE CASCADE 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, sub_elem_name TEXT NOT NULL,
original_name TEXT NOT NULL, original_name TEXT NOT NULL,
sub_elem_description TEXT, 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 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, brand TEXT NOT NULL,
key TEXT NOT NULL, key TEXT NOT NULL,
actif INTEGER NOT NULL DEFAULT 1, actif INTEGER NOT NULL DEFAULT 1,
synced INTEGER DEFAULT 0,
FOREIGN KEY (user_id) REFERENCES erit_users(user_id) ON DELETE CASCADE 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, book_id TEXT NOT NULL,
chapter_id TEXT NOT NULL, chapter_id TEXT NOT NULL,
version INTEGER NOT NULL, version INTEGER NOT NULL,
synced INTEGER DEFAULT 0,
PRIMARY KEY (user_id, book_id), PRIMARY KEY (user_id, book_id),
FOREIGN KEY (book_id) REFERENCES erit_books(book_id) ON DELETE CASCADE, FOREIGN KEY (book_id) REFERENCES erit_books(book_id) ON DELETE CASCADE,
FOREIGN KEY (chapter_id) REFERENCES book_chapters(chapter_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 // Create indexes for better performance
createIndexes(db); createIndexes(db);
// Initialize sync metadata for all tables // Set schema version for new databases (prevents unnecessary migrations)
initializeSyncMetadata(db); 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_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_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_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 * 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]); 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 * 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 * 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}`); 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) { 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'); // Recreate erit_books with nullable hashed_sub_title and summary
dropColumnIfExists(db, 'book_act_summaries', 'meta_acts'); recreateTable(db, 'erit_books', `
dropColumnIfExists(db, 'book_ai_guide_line', 'meta'); CREATE TABLE erit_books (
dropColumnIfExists(db, 'book_chapters', 'meta_chapter'); book_id TEXT PRIMARY KEY,
dropColumnIfExists(db, 'book_chapter_content', 'meta_chapter_content'); type TEXT NOT NULL,
dropColumnIfExists(db, 'book_chapter_infos', 'meta_chapter_info'); author_id TEXT NOT NULL,
dropColumnIfExists(db, 'book_characters', 'char_meta'); title TEXT NOT NULL,
dropColumnIfExists(db, 'book_characters_attributes', 'attr_meta'); hashed_title TEXT NOT NULL,
dropColumnIfExists(db, 'book_guide_line', 'meta_guide_line'); sub_title TEXT,
dropColumnIfExists(db, 'book_incidents', 'meta_incident'); hashed_sub_title TEXT,
dropColumnIfExists(db, 'book_issues', 'meta_issue'); summary TEXT,
dropColumnIfExists(db, 'book_location', 'loc_meta'); serie_id INTEGER,
dropColumnIfExists(db, 'book_plot_points', 'meta_plot'); desired_release_date TEXT,
dropColumnIfExists(db, 'book_world', 'meta_world'); desired_word_count INTEGER,
dropColumnIfExists(db, 'book_world_elements', 'meta_element'); words_count INTEGER,
dropColumnIfExists(db, 'erit_books', 'book_meta'); cover_image TEXT,
dropColumnIfExists(db, 'erit_users', 'user_meta'); last_update INTEGER DEFAULT 0
dropColumnIfExists(db, 'location_element', 'element_meta'); )
dropColumnIfExists(db, 'location_sub_element', 'sub_elem_meta'); `, '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'); console.log('[Migration] Migration v2 completed');
} }

View File

@@ -1,6 +1,6 @@
import { ipcMain } from 'electron'; import { ipcMain } from 'electron';
import { createHandler } from '../database/LocalSystem.js'; 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 type { BookProps, GuideLine, GuideLineAI, Act, Issue, WorldProps } from '../database/models/Book.js';
import Chapter from '../database/models/Chapter.js'; import Chapter from '../database/models/Chapter.js';
import type { ChapterProps } from '../database/models/Chapter.js'; import type { ChapterProps } from '../database/models/Chapter.js';
@@ -88,6 +88,10 @@ interface SetAIGuideLineData {
themes: string; themes: string;
} }
interface GetGuidelineData {
id: string;
}
// GET /books - Get all books // GET /books - Get all books
ipcMain.handle('db:book:books', createHandler<void, BookProps[]>( ipcMain.handle('db:book:books', createHandler<void, BookProps[]>(
async function(userId: string, _body: void, lang: 'fr' | 'en'):Promise<BookProps[]> { async function(userId: string, _body: void, lang: 'fr' | 'en'):Promise<BookProps[]> {
@@ -96,6 +100,20 @@ ipcMain.handle('db:book:books', createHandler<void, BookProps[]>(
) )
); );
// GET /books/synced - Get all synced books
ipcMain.handle('db:books:synced', createHandler<void, SyncedBook[]>(
async function(userId: string, _body: void, lang: 'fr' | 'en'):Promise<SyncedBook[]> {
return await Book.getSyncedBooks(userId, lang);
})
);
// POST /book/sync/save - Save complete book
ipcMain.handle('db:book:syncSave', createHandler<CompleteBook, boolean>(
async function(userId: string, data: CompleteBook, lang: 'fr' | 'en'):Promise<boolean> {
return await Book.saveCompleteBook(userId, data, lang);
})
);
// GET /book/:id - Get single book // GET /book/:id - Get single book
ipcMain.handle('db:book:bookBasicInformation', createHandler<string, BookProps>( ipcMain.handle('db:book:bookBasicInformation', createHandler<string, BookProps>(
async function(userId: string, bookId: string, lang: 'fr' | 'en'):Promise<BookProps> { async function(userId: string, bookId: string, lang: 'fr' | 'en'):Promise<BookProps> {
@@ -113,11 +131,7 @@ ipcMain.handle('db:book:updateBasicInformation', createHandler<UpdateBookBasicDa
); );
// GET /book/guide-line - Get guideline // GET /book/guide-line - Get guideline
interface GetGuidelineData { ipcMain.handle('db:book:guideline:get',
id: string;
}
ipcMain.handle(
'db:book:guideline:get',
createHandler<GetGuidelineData, GuideLine | null>(async function(userId: string, data: GetGuidelineData, lang: 'fr' | 'en') { createHandler<GetGuidelineData, GuideLine | null>(async function(userId: string, data: GetGuidelineData, lang: 'fr' | 'en') {
return await Book.getGuideLine(userId, data.id, lang); return await Book.getGuideLine(userId, data.id, lang);
} }

View File

@@ -115,7 +115,13 @@
"bookCard": { "bookCard": {
"noCoverAlt": "No cover", "noCoverAlt": "No cover",
"initialsSeparator": ".", "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": { "scribeTopBar": {
"logoAlt": "Logo", "logoAlt": "Logo",

View File

@@ -115,7 +115,13 @@
"bookCard": { "bookCard": {
"noCoverAlt": "Pas de couverture", "noCoverAlt": "Pas de couverture",
"initialsSeparator": ".", "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": { "scribeTopBar": {
"logoAlt": "Logo", "logoAlt": "Logo",

View File

@@ -1,6 +1,61 @@
import {Author} from './User'; import {Author} from './User';
import {ActChapter, ChapterProps} from "@/lib/models/Chapter"; import {ActChapter, ChapterProps} from "@/lib/models/Chapter";
import {SelectBoxProps} from "@/shared/interface"; 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 { export interface BookProps {
bookId: string; bookId: string;
@@ -30,6 +85,7 @@ export interface BookListProps {
wordCount?: number; wordCount?: number;
coverImage?: string; coverImage?: string;
bookMeta?: string; bookMeta?: string;
itIsLocal?: boolean;
} }
export interface GuideLine { export interface GuideLine {

182
lib/models/BookTables.ts Normal file
View File

@@ -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;
}

112
lib/models/SyncedBook.ts Normal file
View File

@@ -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;
}

View File

@@ -22,7 +22,6 @@ export interface UserProps {
openai: boolean, openai: boolean,
anthropic: boolean, anthropic: boolean,
}, },
books?: BookProps[];
guideTour?: GuideTour[]; guideTour?: GuideTour[];
subscription?: Subscription[]; subscription?: Subscription[];
writingLang: number; writingLang: number;