Add support for syncing tool settings with lastUpdate and improve consistency
- Introduced `lastUpdate` field in `book_tools` for tracking changes. - Refactored tool enablement logic in `CharacterComponent`, `WorldSetting`, and `LocationComponent` for consistency. - Updated database schema and migration scripts for `book_tools` table. - Enhanced synchronization workflows to support new `lastUpdate` logic. - Adjusted related models, repositories, and IPC handlers for streamlined management. - Improved type safety and robustness in tool-related methods with additional checks.
This commit is contained in:
@@ -20,6 +20,8 @@ export default function LoginWrapper({children}: { children: React.ReactNode })
|
|||||||
const [locale, setLocale] = useState<'fr' | 'en'>('fr');
|
const [locale, setLocale] = useState<'fr' | 'en'>('fr');
|
||||||
const [errorMessage, setErrorMessage] = useState('');
|
const [errorMessage, setErrorMessage] = useState('');
|
||||||
const [successMessage, setSuccessMessage] = useState('');
|
const [successMessage, setSuccessMessage] = useState('');
|
||||||
|
const [infoMessage, setInfoMessage] = useState('');
|
||||||
|
const [warningMessage, setWarningMessage] = useState('');
|
||||||
const messages = messagesMap[locale];
|
const messages = messagesMap[locale];
|
||||||
|
|
||||||
const [session, setSession] = useState<SessionProps>({
|
const [session, setSession] = useState<SessionProps>({
|
||||||
|
|||||||
@@ -169,7 +169,8 @@ export default function AddNewBookForm({setCloseForm}: { setCloseForm: Dispatch<
|
|||||||
issues: [],
|
issues: [],
|
||||||
actSummaries: [],
|
actSummaries: [],
|
||||||
guideLine: null,
|
guideLine: null,
|
||||||
aiGuideLine: null
|
aiGuideLine: null,
|
||||||
|
bookTools: null
|
||||||
}]);
|
}]);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
@@ -188,7 +189,8 @@ export default function AddNewBookForm({setCloseForm}: { setCloseForm: Dispatch<
|
|||||||
issues: [],
|
issues: [],
|
||||||
actSummaries: [],
|
actSummaries: [],
|
||||||
guideLine: null,
|
guideLine: null,
|
||||||
aiGuideLine: null
|
aiGuideLine: null,
|
||||||
|
bookTools: null
|
||||||
}]);
|
}]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -96,7 +96,11 @@ export function CharacterComponent({showToggle = true}: {showToggle?: boolean},
|
|||||||
}
|
}
|
||||||
if (response && setBook && book) {
|
if (response && setBook && book) {
|
||||||
setToolEnabled(enabled);
|
setToolEnabled(enabled);
|
||||||
setBook({...book, tools: {...book.tools, characters: enabled}});
|
setBook({...book, tools: {
|
||||||
|
characters: enabled,
|
||||||
|
worlds: book.tools?.worlds ?? false,
|
||||||
|
locations: book.tools?.locations ?? false
|
||||||
|
}});
|
||||||
}
|
}
|
||||||
} catch (e: unknown) {
|
} catch (e: unknown) {
|
||||||
if (e instanceof Error) {
|
if (e instanceof Error) {
|
||||||
@@ -123,7 +127,11 @@ export function CharacterComponent({showToggle = true}: {showToggle?: boolean},
|
|||||||
setCharacters(response.characters);
|
setCharacters(response.characters);
|
||||||
setToolEnabled(response.enabled);
|
setToolEnabled(response.enabled);
|
||||||
if (setBook && book) {
|
if (setBook && book) {
|
||||||
setBook({...book, tools: {...book.tools, characters: response.enabled}});
|
setBook({...book, tools: {
|
||||||
|
characters: response.enabled,
|
||||||
|
worlds: book.tools?.worlds ?? false,
|
||||||
|
locations: book.tools?.locations ?? false
|
||||||
|
}});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e: unknown) {
|
} catch (e: unknown) {
|
||||||
|
|||||||
@@ -95,7 +95,11 @@ export function LocationComponent({showToggle = true}: {showToggle?: boolean}, r
|
|||||||
}
|
}
|
||||||
if (response && setBook && book) {
|
if (response && setBook && book) {
|
||||||
setToolEnabled(enabled);
|
setToolEnabled(enabled);
|
||||||
setBook({...book, tools: {...book.tools, locations: enabled}});
|
setBook({...book, tools: {
|
||||||
|
characters: book.tools?.characters ?? false,
|
||||||
|
worlds: book.tools?.worlds ?? false,
|
||||||
|
locations: enabled
|
||||||
|
}});
|
||||||
}
|
}
|
||||||
} catch (e: unknown) {
|
} catch (e: unknown) {
|
||||||
if (e instanceof Error) {
|
if (e instanceof Error) {
|
||||||
@@ -122,7 +126,11 @@ export function LocationComponent({showToggle = true}: {showToggle?: boolean}, r
|
|||||||
setSections(response.locations);
|
setSections(response.locations);
|
||||||
setToolEnabled(response.enabled);
|
setToolEnabled(response.enabled);
|
||||||
if (setBook && book) {
|
if (setBook && book) {
|
||||||
setBook({...book, tools: {...book.tools, locations: response.enabled}});
|
setBook({...book, tools: {
|
||||||
|
characters: book.tools?.characters ?? false,
|
||||||
|
worlds: book.tools?.worlds ?? false,
|
||||||
|
locations: response.enabled
|
||||||
|
}});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e: unknown) {
|
} catch (e: unknown) {
|
||||||
|
|||||||
@@ -81,7 +81,11 @@ export function WorldSetting({showToggle = true}: {showToggle?: boolean}, ref: a
|
|||||||
}
|
}
|
||||||
if (response && setBook && book) {
|
if (response && setBook && book) {
|
||||||
setToolEnabled(enabled);
|
setToolEnabled(enabled);
|
||||||
setBook({...book, tools: {...book.tools, worlds: enabled}});
|
setBook({...book, tools: {
|
||||||
|
characters: book.tools?.characters ?? false,
|
||||||
|
worlds: enabled,
|
||||||
|
locations: book.tools?.locations ?? false
|
||||||
|
}});
|
||||||
}
|
}
|
||||||
} catch (e: unknown) {
|
} catch (e: unknown) {
|
||||||
if (e instanceof Error) {
|
if (e instanceof Error) {
|
||||||
@@ -108,7 +112,11 @@ export function WorldSetting({showToggle = true}: {showToggle?: boolean}, ref: a
|
|||||||
setWorlds(response.worlds);
|
setWorlds(response.worlds);
|
||||||
setToolEnabled(response.enabled);
|
setToolEnabled(response.enabled);
|
||||||
if (setBook && book) {
|
if (setBook && book) {
|
||||||
setBook({...book, tools: {...book.tools, worlds: response.enabled}});
|
setBook({...book, tools: {
|
||||||
|
characters: book.tools?.characters ?? false,
|
||||||
|
worlds: response.enabled,
|
||||||
|
locations: book.tools?.locations ?? false
|
||||||
|
}});
|
||||||
}
|
}
|
||||||
const formattedWorlds: SelectBoxProps[] = response.worlds.map(
|
const formattedWorlds: SelectBoxProps[] = response.worlds.map(
|
||||||
(world: WorldProps): SelectBoxProps => ({
|
(world: WorldProps): SelectBoxProps => ({
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import System from '../System.js';
|
import System from '../System.js';
|
||||||
import { getUserEncryptionKey } from '../keyManager.js';
|
import { getUserEncryptionKey } from '../keyManager.js';
|
||||||
import BookRepo, { BookQuery, BookToolsTable, BookToolsSettings, EritBooksTable } from "../repositories/book.repository.js";
|
import BookRepo, { BookQuery, BookToolsTable, EritBooksTable } from "../repositories/book.repository.js";
|
||||||
import { BookActSummariesTable } from "../repositories/act.repository.js";
|
import { BookActSummariesTable } from "../repositories/act.repository.js";
|
||||||
import { BookAIGuideLineTable, BookGuideLineTable } from "../repositories/guideline.repository.js";
|
import { BookAIGuideLineTable, BookGuideLineTable } from "../repositories/guideline.repository.js";
|
||||||
import ChapterRepo, {
|
import ChapterRepo, {
|
||||||
@@ -35,9 +35,13 @@ import Cover from "./Cover.js";
|
|||||||
import UserRepo from "../repositories/user.repository.js";
|
import UserRepo from "../repositories/user.repository.js";
|
||||||
|
|
||||||
export interface SyncedBookTools {
|
export interface SyncedBookTools {
|
||||||
charactersEnabled: boolean;
|
lastUpdate: number;
|
||||||
worldsEnabled: boolean;
|
}
|
||||||
locationsEnabled: boolean;
|
|
||||||
|
export interface BookToolsSettings {
|
||||||
|
characters: boolean;
|
||||||
|
worlds: boolean;
|
||||||
|
locations: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface BookProps {
|
export interface BookProps {
|
||||||
@@ -308,7 +312,7 @@ export default class Book {
|
|||||||
|
|
||||||
public static updateBookToolSetting(userId: string, bookId: string, toolName: 'characters' | 'worlds' | 'locations', enabled: boolean, lang: 'fr' | 'en' = 'fr'): boolean {
|
public static updateBookToolSetting(userId: string, bookId: string, toolName: 'characters' | 'worlds' | 'locations', enabled: boolean, lang: 'fr' | 'en' = 'fr'): boolean {
|
||||||
const columnName: 'characters_enabled' | 'worlds_enabled' | 'locations_enabled' = `${toolName}_enabled` as 'characters_enabled' | 'worlds_enabled' | 'locations_enabled';
|
const columnName: 'characters_enabled' | 'worlds_enabled' | 'locations_enabled' = `${toolName}_enabled` as 'characters_enabled' | 'worlds_enabled' | 'locations_enabled';
|
||||||
return BookRepo.updateBookToolSetting(userId, bookId, columnName, enabled, lang);
|
return BookRepo.updateBookToolSetting(userId, bookId, columnName, enabled, System.timeStampInSeconds(), lang);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -199,7 +199,7 @@ export default class Download {
|
|||||||
if (!issuesInserted) return false;
|
if (!issuesInserted) return false;
|
||||||
|
|
||||||
return data.bookTools.every((bookTool: BookToolsTable): boolean => {
|
return data.bookTools.every((bookTool: BookToolsTable): boolean => {
|
||||||
return BookRepo.insertSyncBookTools(bookTool.book_id, userId, bookTool.characters_enabled, bookTool.worlds_enabled, bookTool.locations_enabled, lang);
|
return BookRepo.insertSyncBookTools(bookTool.book_id, userId, bookTool.characters_enabled, bookTool.worlds_enabled, bookTool.locations_enabled, bookTool.last_update, lang);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { getUserEncryptionKey } from "../keyManager.js";
|
import { getUserEncryptionKey } from "../keyManager.js";
|
||||||
import System from "../System.js";
|
import System from "../System.js";
|
||||||
import { BookSyncCompare, CompleteBook, SyncedBook, SyncedBookTools } from "./Book.js";
|
import { BookSyncCompare, CompleteBook, SyncedBook, SyncedBookTools } from "./Book.js";
|
||||||
import BookRepo, { EritBooksTable, SyncedBookResult, BookToolsTable } from "../repositories/book.repository.js";
|
import BookRepo, { EritBooksTable, SyncedBookResult, BookToolsTable, SyncedBookToolsResult } from "../repositories/book.repository.js";
|
||||||
import ChapterRepo, {
|
import ChapterRepo, {
|
||||||
BookChapterInfosTable,
|
BookChapterInfosTable,
|
||||||
BookChaptersTable,
|
BookChaptersTable,
|
||||||
@@ -728,22 +728,14 @@ export default class Sync {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const serverBookTools: BookToolsTable[] = completeBook.bookTools;
|
if (completeBook.bookTools && completeBook.bookTools.length > 0) {
|
||||||
if (serverBookTools && serverBookTools.length > 0) {
|
for (const serverBookTool of completeBook.bookTools) {
|
||||||
for (const serverBookTool of serverBookTools) {
|
const success: boolean = BookRepo.insertSyncBookTools(serverBookTool.book_id, userId, serverBookTool.characters_enabled, serverBookTool.worlds_enabled, serverBookTool.locations_enabled, serverBookTool.last_update, lang);
|
||||||
const bookToolsExists: BookToolsTable | null = BookRepo.fetchBookTools(userId, bookId, lang);
|
if (!success) {
|
||||||
if (bookToolsExists) {
|
|
||||||
BookRepo.updateBookToolSetting(userId, bookId, 'characters_enabled', serverBookTool.characters_enabled === 1, lang);
|
|
||||||
BookRepo.updateBookToolSetting(userId, bookId, 'worlds_enabled', serverBookTool.worlds_enabled === 1, lang);
|
|
||||||
BookRepo.updateBookToolSetting(userId, bookId, 'locations_enabled', serverBookTool.locations_enabled === 1, lang);
|
|
||||||
} else {
|
|
||||||
const insertSuccessful: boolean = BookRepo.insertSyncBookTools(bookId, userId, serverBookTool.characters_enabled, serverBookTool.worlds_enabled, serverBookTool.locations_enabled, lang);
|
|
||||||
if (!insertSuccessful) {
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -961,11 +953,9 @@ export default class Sync {
|
|||||||
lastUpdate: aiGuidelineRecord.last_update
|
lastUpdate: aiGuidelineRecord.last_update
|
||||||
} : null;
|
} : null;
|
||||||
|
|
||||||
const bookToolsRecord: BookToolsTable | null = BookRepo.fetchBookTools(userId, currentBookId, lang);
|
const bookToolsQuery: SyncedBookToolsResult | null = BookRepo.fetchSyncedBookTools(userId, currentBookId, lang);
|
||||||
const bookTools: SyncedBookTools | null = bookToolsRecord ? {
|
const bookTools: SyncedBookTools | null = bookToolsQuery ? {
|
||||||
charactersEnabled: bookToolsRecord.characters_enabled === 1,
|
lastUpdate: bookToolsQuery.last_update
|
||||||
worldsEnabled: bookToolsRecord.worlds_enabled === 1,
|
|
||||||
locationsEnabled: bookToolsRecord.locations_enabled === 1
|
|
||||||
} : null;
|
} : null;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -52,12 +52,11 @@ export interface BookToolsTable extends Record<string, SQLiteValue> {
|
|||||||
characters_enabled: number;
|
characters_enabled: number;
|
||||||
worlds_enabled: number;
|
worlds_enabled: number;
|
||||||
locations_enabled: number;
|
locations_enabled: number;
|
||||||
|
last_update: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface BookToolsSettings {
|
export interface SyncedBookToolsResult extends Record<string, SQLiteValue> {
|
||||||
characters: boolean;
|
last_update: number;
|
||||||
worlds: boolean;
|
|
||||||
locations: boolean;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class BookRepo {
|
export default class BookRepo {
|
||||||
@@ -380,7 +379,7 @@ export default class BookRepo {
|
|||||||
static fetchBookTools(userId: string, bookId: string, lang: 'fr' | 'en'): BookToolsTable | null {
|
static fetchBookTools(userId: string, bookId: string, lang: 'fr' | 'en'): BookToolsTable | null {
|
||||||
try {
|
try {
|
||||||
const db: Database = System.getDb();
|
const db: Database = System.getDb();
|
||||||
const query: string = 'SELECT book_id, user_id, characters_enabled, worlds_enabled, locations_enabled FROM book_tools WHERE user_id=? AND book_id=?';
|
const query: string = 'SELECT book_id, user_id, characters_enabled, worlds_enabled, locations_enabled, last_update FROM book_tools WHERE user_id=? AND book_id=?';
|
||||||
const params: SQLiteValue[] = [userId, bookId];
|
const params: SQLiteValue[] = [userId, bookId];
|
||||||
const result = db.get(query, params) as BookToolsTable | undefined;
|
const result = db.get(query, params) as BookToolsTable | undefined;
|
||||||
return result ?? null;
|
return result ?? null;
|
||||||
@@ -393,20 +392,20 @@ export default class BookRepo {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static updateBookToolSetting(userId: string, bookId: string, toolName: 'characters_enabled' | 'worlds_enabled' | 'locations_enabled', enabled: boolean, lang: 'fr' | 'en'): boolean {
|
static updateBookToolSetting(userId: string, bookId: string, toolName: 'characters_enabled' | 'worlds_enabled' | 'locations_enabled', enabled: boolean, lastUpdate: number, lang: 'fr' | 'en'): boolean {
|
||||||
const enabledValue: number = enabled ? 1 : 0;
|
const enabledValue: number = enabled ? 1 : 0;
|
||||||
try {
|
try {
|
||||||
const db: Database = System.getDb();
|
const db: Database = System.getDb();
|
||||||
const updateQuery: string = `UPDATE book_tools SET ${toolName}=? WHERE user_id=? AND book_id=?`;
|
const updateQuery: string = `UPDATE book_tools SET ${toolName}=?, last_update=? WHERE user_id=? AND book_id=?`;
|
||||||
const updateResult: RunResult = db.run(updateQuery, [enabledValue, userId, bookId]);
|
const updateResult: RunResult = db.run(updateQuery, [enabledValue, lastUpdate, userId, bookId]);
|
||||||
if (updateResult.changes > 0) {
|
if (updateResult.changes > 0) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
const charactersValue: number = toolName === 'characters_enabled' ? enabledValue : 0;
|
const charactersValue: number = toolName === 'characters_enabled' ? enabledValue : 0;
|
||||||
const worldsValue: number = toolName === 'worlds_enabled' ? enabledValue : 0;
|
const worldsValue: number = toolName === 'worlds_enabled' ? enabledValue : 0;
|
||||||
const locationsValue: number = toolName === 'locations_enabled' ? enabledValue : 0;
|
const locationsValue: number = toolName === 'locations_enabled' ? enabledValue : 0;
|
||||||
const insertQuery: string = 'INSERT INTO book_tools (book_id, user_id, characters_enabled, worlds_enabled, locations_enabled) VALUES (?, ?, ?, ?, ?)';
|
const insertQuery: string = 'INSERT INTO book_tools (book_id, user_id, characters_enabled, worlds_enabled, locations_enabled, last_update) VALUES (?, ?, ?, ?, ?, ?)';
|
||||||
const insertResult: RunResult = db.run(insertQuery, [bookId, userId, charactersValue, worldsValue, locationsValue]);
|
const insertResult: RunResult = db.run(insertQuery, [bookId, userId, charactersValue, worldsValue, locationsValue, lastUpdate]);
|
||||||
return insertResult.changes > 0;
|
return insertResult.changes > 0;
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
if (error instanceof Error) {
|
if (error instanceof Error) {
|
||||||
@@ -418,26 +417,35 @@ export default class BookRepo {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Inserts book tools settings during sync.
|
* Upserts book tools settings during sync.
|
||||||
* @param bookId - The book identifier
|
* Inserts if not exists, updates if exists.
|
||||||
* @param userId - The user identifier
|
|
||||||
* @param charactersEnabled - Whether characters tool is enabled
|
|
||||||
* @param worldsEnabled - Whether worlds tool is enabled
|
|
||||||
* @param locationsEnabled - Whether locations tool is enabled
|
|
||||||
* @param lang - The language for error messages
|
|
||||||
* @returns true if the insertion was successful
|
|
||||||
*/
|
*/
|
||||||
static insertSyncBookTools(bookId: string, userId: string, charactersEnabled: number, worldsEnabled: number, locationsEnabled: number, lang: 'fr' | 'en'): boolean {
|
static insertSyncBookTools(bookId: string, userId: string, charactersEnabled: number, worldsEnabled: number, locationsEnabled: number, lastUpdate: number, lang: 'fr' | 'en'): boolean {
|
||||||
try {
|
try {
|
||||||
const db: Database = System.getDb();
|
const db: Database = System.getDb();
|
||||||
const query: string = 'INSERT INTO book_tools (book_id, user_id, characters_enabled, worlds_enabled, locations_enabled) VALUES (?, ?, ?, ?, ?)';
|
const query: string = 'INSERT INTO book_tools (book_id, user_id, characters_enabled, worlds_enabled, locations_enabled, last_update) VALUES (?, ?, ?, ?, ?, ?) ON CONFLICT (book_id, user_id) DO UPDATE SET characters_enabled = excluded.characters_enabled, worlds_enabled = excluded.worlds_enabled, locations_enabled = excluded.locations_enabled, last_update = excluded.last_update';
|
||||||
const params: SQLiteValue[] = [bookId, userId, charactersEnabled, worldsEnabled, locationsEnabled];
|
const params: SQLiteValue[] = [bookId, userId, charactersEnabled, worldsEnabled, locationsEnabled, lastUpdate];
|
||||||
const insertResult: RunResult = db.run(query, params);
|
db.run(query, params);
|
||||||
return insertResult.changes > 0;
|
return true;
|
||||||
|
} catch (error: unknown) {
|
||||||
|
if (error instanceof Error) {
|
||||||
|
console.error(`[BookRepository] DB Error: ${error.message}`);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static fetchSyncedBookTools(userId: string, bookId: string, lang: 'fr' | 'en'): SyncedBookToolsResult | null {
|
||||||
|
try {
|
||||||
|
const db: Database = System.getDb();
|
||||||
|
const query: string = 'SELECT last_update FROM book_tools WHERE user_id = ? AND book_id = ?';
|
||||||
|
const params: SQLiteValue[] = [userId, bookId];
|
||||||
|
const result = db.get(query, params) as SyncedBookToolsResult | undefined;
|
||||||
|
return result ?? null;
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
if (error instanceof Error) {
|
if (error instanceof Error) {
|
||||||
console.error(`DB Error: ${error.message}`);
|
console.error(`DB Error: ${error.message}`);
|
||||||
throw new Error(lang === 'fr' ? "Impossible d'insérer les paramètres des outils." : 'Unable to insert tools settings.');
|
throw new Error(lang === 'fr' ? 'Impossible de récupérer les paramètres des outils.' : 'Unable to fetch tools settings.');
|
||||||
}
|
}
|
||||||
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.");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,128 @@ type Database = sqlite3.Database;
|
|||||||
* Data is encrypted before storage and decrypted on retrieval
|
* Data is encrypted before storage and decrypted on retrieval
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export const SCHEMA_VERSION = 3;
|
// =============================================================================
|
||||||
|
// MIGRATIONS
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
const schemaVersion = 1;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DEV ONLY - S'exécute à chaque refresh, pas besoin de version
|
||||||
|
* Mets ta query, test, efface après
|
||||||
|
*/
|
||||||
|
const devQueries: string[] = [
|
||||||
|
// "ALTER TABLE book_tools ADD COLUMN last_update INTEGER DEFAULT 0",
|
||||||
|
];
|
||||||
|
|
||||||
|
const isDev = process.env.NODE_ENV === 'development';
|
||||||
|
|
||||||
|
function columnExists(db: Database, table: string, column: string): boolean {
|
||||||
|
const result = db.all(`PRAGMA table_info(${table})`) as { name: string }[];
|
||||||
|
return result?.some((row) => row.name === column) ?? false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function addColumn(db: Database, table: string, column: string, type: string): void {
|
||||||
|
if (!columnExists(db, table, column)) {
|
||||||
|
db.exec(`ALTER TABLE ${table} ADD COLUMN ${column} ${type}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getDbVersion(db: Database): number {
|
||||||
|
const result = db.get('PRAGMA user_version') as { user_version: number } | undefined;
|
||||||
|
return result?.user_version ?? 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
function setDbVersion(db: Database, version: number): void {
|
||||||
|
db.exec(`PRAGMA user_version = ${version}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if old _schema_version table exists
|
||||||
|
*/
|
||||||
|
function hasOldSchemaTable(db: Database): boolean {
|
||||||
|
const result = db.get(`
|
||||||
|
SELECT name FROM sqlite_master
|
||||||
|
WHERE type='table' AND name='_schema_version'
|
||||||
|
`) as { name: string } | undefined;
|
||||||
|
return !!result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get version from old _schema_version table
|
||||||
|
*/
|
||||||
|
function getOldSchemaVersion(db: Database): number {
|
||||||
|
try {
|
||||||
|
const result = db.get('SELECT version FROM _schema_version LIMIT 1') as { version: number } | undefined;
|
||||||
|
return result?.version ?? 0;
|
||||||
|
} catch {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Migrate from old _schema_version table to PRAGMA user_version
|
||||||
|
* Old system: v3 = all migrations done (book_tools created, NOT NULL fixes applied)
|
||||||
|
* New system: v1 = equivalent starting point
|
||||||
|
*/
|
||||||
|
function migrateFromOldSystem(db: Database): void {
|
||||||
|
const oldVersion = getOldSchemaVersion(db);
|
||||||
|
|
||||||
|
// Old v3 means all previous migrations were done
|
||||||
|
// Map to new system: old v3 = new v1
|
||||||
|
if (oldVersion >= 3) {
|
||||||
|
setDbVersion(db, schemaVersion);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add last_update column to book_tools if missing (was added after v3)
|
||||||
|
addColumn(db, 'book_tools', 'last_update', 'INTEGER DEFAULT 0');
|
||||||
|
|
||||||
|
// Drop old schema version table
|
||||||
|
db.exec('DROP TABLE IF EXISTS _schema_version');
|
||||||
|
}
|
||||||
|
|
||||||
|
export function runMigrations(db: Database): void {
|
||||||
|
// DEV: run test queries (skip errors silently)
|
||||||
|
if (isDev && devQueries.length > 0) {
|
||||||
|
for (const query of devQueries) {
|
||||||
|
try {
|
||||||
|
db.exec(query);
|
||||||
|
} catch (_) { /* ignore */ }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// PROD only: versioned migrations
|
||||||
|
if (isDev) return;
|
||||||
|
|
||||||
|
// Migrate from old _schema_version system to PRAGMA user_version
|
||||||
|
if (hasOldSchemaTable(db)) {
|
||||||
|
migrateFromOldSystem(db);
|
||||||
|
return; // Migration done, no need to continue
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentVersion = getDbVersion(db);
|
||||||
|
if (currentVersion >= schemaVersion) return;
|
||||||
|
|
||||||
|
// v1 - book_tools table with last_update (for fresh DBs or DBs without old system)
|
||||||
|
if (currentVersion < 1) {
|
||||||
|
db.exec(`
|
||||||
|
CREATE TABLE IF NOT EXISTS book_tools (
|
||||||
|
book_id TEXT NOT NULL,
|
||||||
|
user_id TEXT NOT NULL,
|
||||||
|
characters_enabled INTEGER NOT NULL DEFAULT 0,
|
||||||
|
worlds_enabled INTEGER NOT NULL DEFAULT 0,
|
||||||
|
locations_enabled INTEGER NOT NULL DEFAULT 0,
|
||||||
|
last_update INTEGER DEFAULT 0,
|
||||||
|
UNIQUE (book_id, user_id),
|
||||||
|
FOREIGN KEY (book_id) REFERENCES erit_books(book_id) ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
`);
|
||||||
|
db.exec(`CREATE INDEX IF NOT EXISTS idx_book_tools_book ON book_tools(book_id)`);
|
||||||
|
db.exec(`CREATE INDEX IF NOT EXISTS idx_book_tools_user ON book_tools(user_id)`);
|
||||||
|
}
|
||||||
|
|
||||||
|
setDbVersion(db, schemaVersion);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initialize the local SQLite database with all required tables
|
* Initialize the local SQLite database with all required tables
|
||||||
@@ -420,187 +541,47 @@ export function initializeSchema(db: Database): void {
|
|||||||
characters_enabled INTEGER NOT NULL DEFAULT 0,
|
characters_enabled INTEGER NOT NULL DEFAULT 0,
|
||||||
worlds_enabled INTEGER NOT NULL DEFAULT 0,
|
worlds_enabled INTEGER NOT NULL DEFAULT 0,
|
||||||
locations_enabled INTEGER NOT NULL DEFAULT 0,
|
locations_enabled INTEGER NOT NULL DEFAULT 0,
|
||||||
PRIMARY KEY (book_id, user_id),
|
last_update INTEGER DEFAULT 0,
|
||||||
|
UNIQUE (book_id, user_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
|
||||||
);
|
);
|
||||||
`);
|
`);
|
||||||
|
|
||||||
// Create indexes for better performance
|
// Create indexes for better performance
|
||||||
createIndexes(db);
|
createIndexes(db);
|
||||||
|
|
||||||
// Set schema version for new databases (prevents unnecessary migrations)
|
|
||||||
initializeSchemaVersion(db);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create indexes for frequently queried columns
|
* Create indexes for frequently queried columns
|
||||||
*/
|
*/
|
||||||
function createIndexes(db: Database): void {
|
function createIndexes(db: Database): void {
|
||||||
db.exec(`
|
db.exec(`CREATE INDEX IF NOT EXISTS idx_ai_conversations_book ON ai_conversations(book_id)`);
|
||||||
CREATE INDEX IF NOT EXISTS idx_ai_conversations_book ON ai_conversations(book_id);
|
db.exec(`CREATE INDEX IF NOT EXISTS idx_ai_conversations_user ON ai_conversations(user_id)`);
|
||||||
CREATE INDEX IF NOT EXISTS idx_ai_conversations_user ON ai_conversations(user_id);
|
db.exec(`CREATE INDEX IF NOT EXISTS idx_ai_messages_conversation ON ai_messages_history(conversation_id)`);
|
||||||
CREATE INDEX IF NOT EXISTS idx_ai_messages_conversation ON ai_messages_history(conversation_id);
|
db.exec(`CREATE INDEX IF NOT EXISTS idx_chapters_book ON book_chapters(book_id)`);
|
||||||
CREATE INDEX IF NOT EXISTS idx_chapters_book ON book_chapters(book_id);
|
db.exec(`CREATE INDEX IF NOT EXISTS idx_chapter_content_chapter ON book_chapter_content(chapter_id)`);
|
||||||
CREATE INDEX IF NOT EXISTS idx_chapter_content_chapter ON book_chapter_content(chapter_id);
|
db.exec(`CREATE INDEX IF NOT EXISTS idx_characters_book ON book_characters(book_id)`);
|
||||||
CREATE INDEX IF NOT EXISTS idx_characters_book ON book_characters(book_id);
|
db.exec(`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);
|
db.exec(`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);
|
db.exec(`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);
|
db.exec(`CREATE INDEX IF NOT EXISTS idx_book_tools_book ON book_tools(book_id)`);
|
||||||
`);
|
db.exec(`CREATE INDEX IF NOT EXISTS idx_book_tools_user ON book_tools(user_id)`);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get current schema version from database
|
* Drop all tables (for testing/reset)
|
||||||
*/
|
*/
|
||||||
function getDbSchemaVersion(db: Database): number {
|
export function dropAllTables(db: Database): void {
|
||||||
try {
|
const tables = db.all(`
|
||||||
const result = db.get('SELECT version FROM _schema_version LIMIT 1') as { version: number } | undefined;
|
SELECT name FROM sqlite_master
|
||||||
return result?.version ?? 0;
|
WHERE type='table' AND name NOT LIKE 'sqlite_%'
|
||||||
} catch {
|
`) as { name: string }[];
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set schema version in database
|
|
||||||
*/
|
|
||||||
function setDbSchemaVersion(db: Database, version: number): void {
|
|
||||||
db.exec('CREATE TABLE IF NOT EXISTS _schema_version (version INTEGER PRIMARY KEY)');
|
|
||||||
db.run('DELETE FROM _schema_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
|
|
||||||
*/
|
|
||||||
function columnExists(db: Database, tableName: string, columnName: string): boolean {
|
|
||||||
const columns = db.all(`PRAGMA table_info(${tableName})`) as { name: string }[];
|
|
||||||
return columns.some(col => col.name === columnName);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Safely drop a column if it exists
|
|
||||||
*/
|
|
||||||
function dropColumnIfExists(db: Database, tableName: string, columnName: string): void {
|
|
||||||
if (columnExists(db, tableName, columnName)) {
|
|
||||||
try {
|
|
||||||
db.exec(`ALTER TABLE ${tableName} DROP COLUMN ${columnName}`);
|
|
||||||
} catch (e) {
|
|
||||||
console.error(`[Migration] Failed to drop column ${columnName} from ${tableName}:`, e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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('PRAGMA foreign_keys = OFF');
|
||||||
db.exec(`CREATE TABLE ${tableName}_backup AS SELECT ${columnsToKeep} FROM ${tableName}`);
|
|
||||||
db.exec(`DROP TABLE ${tableName}`);
|
for (const row of tables) {
|
||||||
db.exec(newSchema);
|
db.exec(`DROP TABLE IF EXISTS ${row.name}`);
|
||||||
db.exec(`INSERT INTO ${tableName} (${columnsToKeep}) SELECT ${columnsToKeep} FROM ${tableName}_backup`);
|
}
|
||||||
db.exec(`DROP TABLE ${tableName}_backup`);
|
|
||||||
db.exec('PRAGMA foreign_keys = ON');
|
db.exec('PRAGMA foreign_keys = ON');
|
||||||
} 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
|
|
||||||
*/
|
|
||||||
export function runMigrations(db: Database): void {
|
|
||||||
const currentVersion = getDbSchemaVersion(db);
|
|
||||||
|
|
||||||
if (currentVersion >= SCHEMA_VERSION) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Migration v2: Remove NOT NULL constraints to allow null values from server sync
|
|
||||||
if (currentVersion < 2) {
|
|
||||||
// Recreate erit_books with nullable hashed_sub_title and summary
|
|
||||||
recreateTable(db, 'erit_books', `
|
|
||||||
CREATE TABLE erit_books (
|
|
||||||
book_id TEXT PRIMARY KEY,
|
|
||||||
type TEXT NOT NULL,
|
|
||||||
author_id TEXT NOT NULL,
|
|
||||||
title TEXT NOT NULL,
|
|
||||||
hashed_title TEXT NOT NULL,
|
|
||||||
sub_title TEXT,
|
|
||||||
hashed_sub_title TEXT,
|
|
||||||
summary TEXT,
|
|
||||||
serie_id INTEGER,
|
|
||||||
desired_release_date TEXT,
|
|
||||||
desired_word_count INTEGER,
|
|
||||||
words_count INTEGER,
|
|
||||||
cover_image TEXT,
|
|
||||||
last_update INTEGER DEFAULT 0
|
|
||||||
)
|
|
||||||
`, 'book_id, type, author_id, title, hashed_title, sub_title, hashed_sub_title, summary, serie_id, desired_release_date, desired_word_count, words_count, cover_image, last_update');
|
|
||||||
|
|
||||||
// Recreate book_chapter_content with nullable content
|
|
||||||
recreateTable(db, 'book_chapter_content', `
|
|
||||||
CREATE TABLE book_chapter_content (
|
|
||||||
content_id TEXT PRIMARY KEY,
|
|
||||||
chapter_id TEXT NOT NULL,
|
|
||||||
author_id TEXT NOT NULL,
|
|
||||||
version INTEGER NOT NULL DEFAULT 2,
|
|
||||||
content TEXT,
|
|
||||||
words_count INTEGER NOT NULL,
|
|
||||||
time_on_it INTEGER NOT NULL DEFAULT 0,
|
|
||||||
last_update INTEGER DEFAULT 0,
|
|
||||||
FOREIGN KEY (chapter_id) REFERENCES book_chapters(chapter_id) ON DELETE CASCADE
|
|
||||||
)
|
|
||||||
`, 'content_id, chapter_id, author_id, version, content, words_count, time_on_it, last_update');
|
|
||||||
|
|
||||||
// Recreate book_chapter_infos with nullable summary and goal
|
|
||||||
recreateTable(db, 'book_chapter_infos', `
|
|
||||||
CREATE TABLE book_chapter_infos (
|
|
||||||
chapter_info_id TEXT PRIMARY KEY,
|
|
||||||
chapter_id TEXT,
|
|
||||||
act_id INTEGER,
|
|
||||||
incident_id TEXT,
|
|
||||||
plot_point_id TEXT,
|
|
||||||
book_id TEXT,
|
|
||||||
author_id TEXT,
|
|
||||||
summary TEXT,
|
|
||||||
goal TEXT,
|
|
||||||
last_update INTEGER DEFAULT 0,
|
|
||||||
FOREIGN KEY (chapter_id) REFERENCES book_chapters(chapter_id) ON DELETE CASCADE,
|
|
||||||
FOREIGN KEY (incident_id) REFERENCES book_incidents(incident_id) ON DELETE CASCADE,
|
|
||||||
FOREIGN KEY (plot_point_id) REFERENCES book_plot_points(plot_point_id) ON DELETE CASCADE
|
|
||||||
)
|
|
||||||
`, 'chapter_info_id, chapter_id, act_id, incident_id, plot_point_id, book_id, author_id, summary, goal, last_update');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (currentVersion < 3) {
|
|
||||||
db.exec(`
|
|
||||||
CREATE TABLE IF NOT EXISTS book_tools (
|
|
||||||
book_id TEXT NOT NULL,
|
|
||||||
user_id TEXT NOT NULL,
|
|
||||||
characters_enabled INTEGER NOT NULL DEFAULT 0,
|
|
||||||
worlds_enabled INTEGER NOT NULL DEFAULT 0,
|
|
||||||
locations_enabled INTEGER NOT NULL DEFAULT 0,
|
|
||||||
PRIMARY KEY (book_id, user_id),
|
|
||||||
FOREIGN KEY (book_id) REFERENCES erit_books(book_id) ON DELETE CASCADE
|
|
||||||
);
|
|
||||||
`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update schema version
|
|
||||||
setDbSchemaVersion(db, SCHEMA_VERSION);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -158,7 +158,7 @@ export default class Book {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static booksToSelectBox(books: SyncedBook[]): SelectBoxProps[] {
|
static booksToSelectBox(books: SyncedBook[]): SelectBoxProps[] {
|
||||||
return books.map((book: SyncedBook):SelectBoxProps => {
|
return books.map((book: SyncedBook): SelectBoxProps => {
|
||||||
return {
|
return {
|
||||||
label: book.title,
|
label: book.title,
|
||||||
value: book.id,
|
value: book.id,
|
||||||
|
|||||||
@@ -1,3 +1,7 @@
|
|||||||
|
export interface SyncedBookTools {
|
||||||
|
lastUpdate: number;
|
||||||
|
}
|
||||||
|
|
||||||
export interface SyncedBook {
|
export interface SyncedBook {
|
||||||
id: string;
|
id: string;
|
||||||
type: string;
|
type: string;
|
||||||
@@ -14,6 +18,7 @@ export interface SyncedBook {
|
|||||||
actSummaries: SyncedActSummary[];
|
actSummaries: SyncedActSummary[];
|
||||||
guideLine: SyncedGuideLine | null;
|
guideLine: SyncedGuideLine | null;
|
||||||
aiGuideLine: SyncedAIGuideLine | null;
|
aiGuideLine: SyncedAIGuideLine | null;
|
||||||
|
bookTools: SyncedBookTools | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SyncedChapter {
|
export interface SyncedChapter {
|
||||||
@@ -129,6 +134,7 @@ export interface BookSyncCompare {
|
|||||||
actSummaries: string[];
|
actSummaries: string[];
|
||||||
guideLine: boolean;
|
guideLine: boolean;
|
||||||
aiGuideLine: boolean;
|
aiGuideLine: boolean;
|
||||||
|
bookTools: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function compareBookSyncs(newerBook: SyncedBook, olderBook: SyncedBook): BookSyncCompare | null {
|
export function compareBookSyncs(newerBook: SyncedBook, olderBook: SyncedBook): BookSyncCompare | null {
|
||||||
@@ -148,6 +154,7 @@ export function compareBookSyncs(newerBook: SyncedBook, olderBook: SyncedBook):
|
|||||||
const changedActSummaryIds: string[] = [];
|
const changedActSummaryIds: string[] = [];
|
||||||
let guideLineChanged: boolean = false;
|
let guideLineChanged: boolean = false;
|
||||||
let aiGuideLineChanged: boolean = false;
|
let aiGuideLineChanged: boolean = false;
|
||||||
|
let bookToolsChanged: boolean = false;
|
||||||
|
|
||||||
newerBook.chapters.forEach((newerChapter: SyncedChapter): void => {
|
newerBook.chapters.forEach((newerChapter: SyncedChapter): void => {
|
||||||
const olderChapter: SyncedChapter | undefined = olderBook.chapters.find((chapter: SyncedChapter): boolean => chapter.id === newerChapter.id);
|
const olderChapter: SyncedChapter | undefined = olderBook.chapters.find((chapter: SyncedChapter): boolean => chapter.id === newerChapter.id);
|
||||||
@@ -296,6 +303,12 @@ export function compareBookSyncs(newerBook: SyncedBook, olderBook: SyncedBook):
|
|||||||
aiGuideLineChanged = true;
|
aiGuideLineChanged = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (newerBook.bookTools && olderBook.bookTools) {
|
||||||
|
bookToolsChanged = newerBook.bookTools.lastUpdate > olderBook.bookTools.lastUpdate;
|
||||||
|
} else if (newerBook.bookTools && !olderBook.bookTools) {
|
||||||
|
bookToolsChanged = true;
|
||||||
|
}
|
||||||
|
|
||||||
const hasChanges: boolean =
|
const hasChanges: boolean =
|
||||||
changedChapterIds.length > 0 ||
|
changedChapterIds.length > 0 ||
|
||||||
changedChapterContentIds.length > 0 ||
|
changedChapterContentIds.length > 0 ||
|
||||||
@@ -312,7 +325,8 @@ export function compareBookSyncs(newerBook: SyncedBook, olderBook: SyncedBook):
|
|||||||
changedIssueIds.length > 0 ||
|
changedIssueIds.length > 0 ||
|
||||||
changedActSummaryIds.length > 0 ||
|
changedActSummaryIds.length > 0 ||
|
||||||
guideLineChanged ||
|
guideLineChanged ||
|
||||||
aiGuideLineChanged;
|
aiGuideLineChanged ||
|
||||||
|
bookToolsChanged;
|
||||||
|
|
||||||
if (!hasChanges) {
|
if (!hasChanges) {
|
||||||
return null;
|
return null;
|
||||||
@@ -335,6 +349,7 @@ export function compareBookSyncs(newerBook: SyncedBook, olderBook: SyncedBook):
|
|||||||
issues: changedIssueIds,
|
issues: changedIssueIds,
|
||||||
actSummaries: changedActSummaryIds,
|
actSummaries: changedActSummaryIds,
|
||||||
guideLine: guideLineChanged,
|
guideLine: guideLineChanged,
|
||||||
aiGuideLine: aiGuideLineChanged
|
aiGuideLine: aiGuideLineChanged,
|
||||||
|
bookTools: bookToolsChanged
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user