Add database schema, encryption utilities, and local database service
- Implement `schema.ts` for SQLite schema creation, indexing, and sync metadata initialization. - Develop `encryption.ts` with AES-256-GCM encryption utilities for securing database data. - Add `database.service.ts` to manage CRUD operations with encryption support, user-specific databases, and schema initialization. - Integrate book, chapter, and character operations with encrypted content handling and sync preparation.
This commit is contained in:
@@ -2,226 +2,394 @@ import { BookProps, BookListProps } from '@/lib/models/Book';
|
||||
import { CharacterProps } from '@/lib/models/Character';
|
||||
import { Conversation } from '@/lib/models/QuillSense';
|
||||
import { WorldProps } from '@/lib/models/World';
|
||||
import { ChapterProps } from '@/lib/models/Chapter';
|
||||
|
||||
/**
|
||||
* Service pour gérer les données avec cache local
|
||||
* Sauvegarde automatiquement dans la DB locale (Electron) quand disponible
|
||||
* OfflineDataService - Manages data retrieval with offline/online mode
|
||||
* Automatically caches to local DB when online, serves from local DB when offline
|
||||
*/
|
||||
export class OfflineDataService {
|
||||
private isOffline: boolean = false;
|
||||
|
||||
/**
|
||||
* Get all books - from local DB if offline, from server otherwise
|
||||
*/
|
||||
export async function getBooks(fetchFromServer: () => Promise<BookListProps[]>): Promise<BookListProps[]> {
|
||||
if (!window.electron) {
|
||||
return await fetchFromServer();
|
||||
/**
|
||||
* Set offline status (called by OfflineProvider)
|
||||
*/
|
||||
setOfflineStatus(offline: boolean): void {
|
||||
this.isOffline = offline;
|
||||
}
|
||||
|
||||
// TODO: Check if offline mode is enabled
|
||||
const isOffline = false; // Replace with actual offline check
|
||||
/**
|
||||
* Get current offline status
|
||||
*/
|
||||
getOfflineStatus(): boolean {
|
||||
return this.isOffline;
|
||||
}
|
||||
|
||||
if (isOffline) {
|
||||
// Fetch from local DB
|
||||
const result = await window.electron.dbGetBooks();
|
||||
if (result.success) {
|
||||
return result.data || [];
|
||||
/**
|
||||
* Get all books - from local DB if offline, from server otherwise
|
||||
*/
|
||||
async getBooks(fetchFromServer: () => Promise<BookListProps[]>): Promise<BookListProps[]> {
|
||||
if (!window.electron) {
|
||||
return await fetchFromServer();
|
||||
}
|
||||
throw new Error(result.error || 'Failed to get books from local DB');
|
||||
} else {
|
||||
// Fetch from server and save to local DB
|
||||
const books = await fetchFromServer();
|
||||
|
||||
// Save to local DB in background
|
||||
for (const book of books) {
|
||||
if (this.isOffline) {
|
||||
// Fetch from local DB
|
||||
const result = await window.electron.dbGetBooks();
|
||||
if (result.success && result.data) {
|
||||
const titles = result.data.map(b => b.title).join(', ');
|
||||
console.log(`📚 Books from LOCAL DB: ${titles}`);
|
||||
return result.data;
|
||||
}
|
||||
throw new Error(result.error || 'Failed to get books from local DB');
|
||||
} else {
|
||||
// Fetch from server and save to local DB
|
||||
const books = await fetchFromServer();
|
||||
const titles = books.map(b => b.title).join(', ');
|
||||
console.log(`📚 Books from SERVER: ${titles}`);
|
||||
|
||||
// Save to local DB in background
|
||||
for (const book of books) {
|
||||
try {
|
||||
await window.electron.dbSaveBook(book);
|
||||
} catch (error) {
|
||||
console.error('Failed to save book to local DB:', error);
|
||||
}
|
||||
}
|
||||
|
||||
return books;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get single book - from local DB if offline, from server otherwise
|
||||
*/
|
||||
async getBook(
|
||||
bookId: string,
|
||||
fetchFromServer: () => Promise<BookProps>
|
||||
): Promise<BookProps> {
|
||||
if (!window.electron) {
|
||||
return await fetchFromServer();
|
||||
}
|
||||
|
||||
if (this.isOffline) {
|
||||
const result = await window.electron.dbGetBook(bookId);
|
||||
if (result.success && result.data) {
|
||||
console.log(`📖 "${result.data.title}" from LOCAL DB`);
|
||||
return result.data;
|
||||
}
|
||||
throw new Error(result.error || 'Book not found in local DB');
|
||||
} else {
|
||||
const book = await fetchFromServer();
|
||||
console.log(`📖 "${book.title}" from SERVER`);
|
||||
|
||||
// Save to local DB
|
||||
try {
|
||||
await window.electron.dbSaveBook(book);
|
||||
} catch (error) {
|
||||
console.error('Failed to save book to local DB:', error);
|
||||
}
|
||||
}
|
||||
|
||||
return books;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get single book - from local DB if offline, from server otherwise
|
||||
*/
|
||||
export async function getBook(
|
||||
bookId: string,
|
||||
fetchFromServer: () => Promise<BookProps>
|
||||
): Promise<BookProps> {
|
||||
if (!window.electron) {
|
||||
return await fetchFromServer();
|
||||
}
|
||||
|
||||
const isOffline = false; // Replace with actual offline check
|
||||
|
||||
if (isOffline) {
|
||||
const result = await window.electron.dbGetBook(bookId);
|
||||
if (result.success && result.data) {
|
||||
return result.data;
|
||||
}
|
||||
throw new Error(result.error || 'Book not found in local DB');
|
||||
} else {
|
||||
const book = await fetchFromServer();
|
||||
|
||||
// Save to local DB
|
||||
try {
|
||||
await window.electron.dbSaveBook(book);
|
||||
} catch (error) {
|
||||
console.error('Failed to save book to local DB:', error);
|
||||
}
|
||||
|
||||
return book;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Save book - save to local DB and sync to server later if offline
|
||||
*/
|
||||
export async function saveBook(
|
||||
book: BookProps,
|
||||
authorId: string | undefined,
|
||||
saveToServer: () => Promise<void>
|
||||
): Promise<void> {
|
||||
if (!window.electron) {
|
||||
return await saveToServer();
|
||||
}
|
||||
|
||||
const isOffline = false; // Replace with actual offline check
|
||||
|
||||
// Always save to local DB first
|
||||
await window.electron.dbSaveBook(book, authorId);
|
||||
|
||||
if (!isOffline) {
|
||||
// Also save to server
|
||||
try {
|
||||
await saveToServer();
|
||||
} catch (error) {
|
||||
console.error('Failed to save to server, will sync later:', error);
|
||||
// Data is already in local DB, will be synced later
|
||||
return book;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get characters for a book
|
||||
*/
|
||||
export async function getCharacters(
|
||||
bookId: string,
|
||||
fetchFromServer: () => Promise<CharacterProps[]>
|
||||
): Promise<CharacterProps[]> {
|
||||
if (!window.electron) {
|
||||
return await fetchFromServer();
|
||||
/**
|
||||
* Create new book - creates on server if online, local UUID if offline
|
||||
*/
|
||||
async createBook(
|
||||
bookData: Omit<BookProps, 'bookId'>,
|
||||
authorId: string,
|
||||
createOnServer: () => Promise<string>
|
||||
): Promise<string> {
|
||||
if (!window.electron) {
|
||||
return await createOnServer();
|
||||
}
|
||||
|
||||
if (this.isOffline) {
|
||||
// Generate local UUID and save to local DB
|
||||
const localBookId = crypto.randomUUID();
|
||||
const book: BookProps = { ...bookData, bookId: localBookId };
|
||||
await window.electron.dbSaveBook(book, authorId);
|
||||
console.log(`💾 Book "${book.title}" created locally (offline mode)`);
|
||||
return localBookId;
|
||||
} else {
|
||||
// Create on server and save to local DB
|
||||
const serverBookId = await createOnServer();
|
||||
const book: BookProps = { ...bookData, bookId: serverBookId };
|
||||
await window.electron.dbSaveBook(book, authorId);
|
||||
return serverBookId;
|
||||
}
|
||||
}
|
||||
|
||||
const isOffline = false; // Replace with actual offline check
|
||||
|
||||
if (isOffline) {
|
||||
const result = await window.electron.dbGetCharacters(bookId);
|
||||
if (result.success) {
|
||||
return result.data || [];
|
||||
/**
|
||||
* Save book - save to local DB and sync to server later if offline
|
||||
*/
|
||||
async saveBook(
|
||||
book: BookProps,
|
||||
authorId: string | undefined,
|
||||
saveToServer: () => Promise<void>
|
||||
): Promise<void> {
|
||||
if (!window.electron) {
|
||||
return await saveToServer();
|
||||
}
|
||||
throw new Error(result.error || 'Failed to get characters from local DB');
|
||||
} else {
|
||||
const characters = await fetchFromServer();
|
||||
|
||||
// Save to local DB
|
||||
for (const character of characters) {
|
||||
// Always save to local DB first
|
||||
await window.electron.dbSaveBook(book, authorId);
|
||||
|
||||
if (!this.isOffline) {
|
||||
// Also save to server
|
||||
try {
|
||||
await window.electron.dbSaveCharacter(character, bookId);
|
||||
await saveToServer();
|
||||
} catch (error) {
|
||||
console.error('Failed to save character to local DB:', error);
|
||||
console.error('Failed to save to server, will sync later:', error);
|
||||
// Data is already in local DB, will be synced later
|
||||
}
|
||||
}
|
||||
|
||||
return characters;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Save character
|
||||
*/
|
||||
export async function saveCharacter(
|
||||
character: CharacterProps,
|
||||
bookId: string,
|
||||
saveToServer: () => Promise<void>
|
||||
): Promise<void> {
|
||||
if (!window.electron) {
|
||||
return await saveToServer();
|
||||
}
|
||||
|
||||
const isOffline = false; // Replace with actual offline check
|
||||
|
||||
// Always save to local DB first
|
||||
await window.electron.dbSaveCharacter(character, bookId);
|
||||
|
||||
if (!isOffline) {
|
||||
try {
|
||||
await saveToServer();
|
||||
} catch (error) {
|
||||
console.error('Failed to save to server, will sync later:', error);
|
||||
} else {
|
||||
console.log(`💾 Book queued for sync (offline mode)`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get conversations for a book
|
||||
*/
|
||||
export async function getConversations(
|
||||
bookId: string,
|
||||
fetchFromServer: () => Promise<Conversation[]>
|
||||
): Promise<Conversation[]> {
|
||||
if (!window.electron) {
|
||||
return await fetchFromServer();
|
||||
/**
|
||||
* Get characters for a book
|
||||
*/
|
||||
async getCharacters(
|
||||
bookId: string,
|
||||
fetchFromServer: () => Promise<CharacterProps[]>
|
||||
): Promise<CharacterProps[]> {
|
||||
if (!window.electron) {
|
||||
return await fetchFromServer();
|
||||
}
|
||||
|
||||
if (this.isOffline) {
|
||||
const result = await window.electron.dbGetCharacters(bookId);
|
||||
if (result.success && result.data) {
|
||||
const names = result.data.map(c => c.name).join(', ');
|
||||
console.log(`👤 Characters from LOCAL DB: ${names}`);
|
||||
return result.data;
|
||||
}
|
||||
throw new Error(result.error || 'Failed to get characters from local DB');
|
||||
} else {
|
||||
const characters = await fetchFromServer();
|
||||
const names = characters.map(c => c.name).join(', ');
|
||||
console.log(`👤 Characters from SERVER: ${names}`);
|
||||
|
||||
// Save to local DB
|
||||
for (const character of characters) {
|
||||
try {
|
||||
await window.electron.dbSaveCharacter(character, bookId);
|
||||
} catch (error) {
|
||||
console.error('Failed to save character to local DB:', error);
|
||||
}
|
||||
}
|
||||
|
||||
return characters;
|
||||
}
|
||||
}
|
||||
|
||||
const isOffline = false; // Replace with actual offline check
|
||||
|
||||
if (isOffline) {
|
||||
const result = await window.electron.dbGetConversations(bookId);
|
||||
if (result.success) {
|
||||
return result.data || [];
|
||||
/**
|
||||
* Create character
|
||||
*/
|
||||
async createCharacter(
|
||||
characterData: Omit<CharacterProps, 'id'>,
|
||||
bookId: string,
|
||||
createOnServer: () => Promise<string>
|
||||
): Promise<string> {
|
||||
if (!window.electron) {
|
||||
return await createOnServer();
|
||||
}
|
||||
throw new Error(result.error || 'Failed to get conversations from local DB');
|
||||
} else {
|
||||
const conversations = await fetchFromServer();
|
||||
|
||||
// Save to local DB
|
||||
for (const conversation of conversations) {
|
||||
if (this.isOffline) {
|
||||
const localId = crypto.randomUUID();
|
||||
const character = { ...characterData, id: localId };
|
||||
await window.electron.dbSaveCharacter(character, bookId);
|
||||
console.log(`💾 Character "${character.name}" created locally (offline mode)`);
|
||||
return localId;
|
||||
} else {
|
||||
const serverId = await createOnServer();
|
||||
const character = { ...characterData, id: serverId };
|
||||
await window.electron.dbSaveCharacter(character, bookId);
|
||||
return serverId;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Save character
|
||||
*/
|
||||
async saveCharacter(
|
||||
character: CharacterProps,
|
||||
bookId: string,
|
||||
saveToServer: () => Promise<void>
|
||||
): Promise<void> {
|
||||
if (!window.electron) {
|
||||
return await saveToServer();
|
||||
}
|
||||
|
||||
// Always save to local DB first
|
||||
await window.electron.dbSaveCharacter(character, bookId);
|
||||
|
||||
if (!this.isOffline) {
|
||||
try {
|
||||
await window.electron.dbSaveConversation(conversation, bookId);
|
||||
await saveToServer();
|
||||
} catch (error) {
|
||||
console.error('Failed to save conversation to local DB:', error);
|
||||
console.error('Failed to save to server, will sync later:', error);
|
||||
}
|
||||
} else {
|
||||
console.log(`💾 Character queued for sync (offline mode)`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get conversations for a book
|
||||
*/
|
||||
async getConversations(
|
||||
bookId: string,
|
||||
fetchFromServer: () => Promise<Conversation[]>
|
||||
): Promise<Conversation[]> {
|
||||
if (!window.electron) {
|
||||
return await fetchFromServer();
|
||||
}
|
||||
|
||||
return conversations;
|
||||
if (this.isOffline) {
|
||||
const result = await window.electron.dbGetConversations(bookId);
|
||||
if (result.success) {
|
||||
const titles = result.data?.map((c: any) => c.title).join(', ') || 'none';
|
||||
console.log(`💬 Conversations from LOCAL DB: ${titles}`);
|
||||
return result.data || [];
|
||||
}
|
||||
throw new Error(result.error || 'Failed to get conversations from local DB');
|
||||
} else {
|
||||
const conversations = await fetchFromServer();
|
||||
const titles = conversations.map(c => c.title).join(', ');
|
||||
console.log(`💬 Conversations from SERVER: ${titles}`);
|
||||
|
||||
// Save to local DB
|
||||
for (const conversation of conversations) {
|
||||
try {
|
||||
await window.electron.dbSaveConversation(conversation, bookId);
|
||||
} catch (error) {
|
||||
console.error('Failed to save conversation to local DB:', error);
|
||||
}
|
||||
}
|
||||
|
||||
return conversations;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create conversation
|
||||
*/
|
||||
async createConversation(
|
||||
conversationData: Omit<Conversation, 'id'>,
|
||||
bookId: string,
|
||||
createOnServer: () => Promise<string>
|
||||
): Promise<string> {
|
||||
if (!window.electron) {
|
||||
return await createOnServer();
|
||||
}
|
||||
|
||||
if (this.isOffline) {
|
||||
const localId = crypto.randomUUID();
|
||||
const conversation = { ...conversationData, id: localId };
|
||||
await window.electron.dbSaveConversation(conversation, bookId);
|
||||
console.log(`💾 Conversation "${conversation.title}" created locally (offline mode)`);
|
||||
return localId;
|
||||
} else {
|
||||
const serverId = await createOnServer();
|
||||
const conversation = { ...conversationData, id: serverId };
|
||||
await window.electron.dbSaveConversation(conversation, bookId);
|
||||
return serverId;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Save conversation
|
||||
*/
|
||||
async saveConversation(
|
||||
conversation: Conversation,
|
||||
bookId: string,
|
||||
saveToServer: () => Promise<void>
|
||||
): Promise<void> {
|
||||
if (!window.electron) {
|
||||
return await saveToServer();
|
||||
}
|
||||
|
||||
// Always save to local DB first
|
||||
await window.electron.dbSaveConversation(conversation, bookId);
|
||||
|
||||
if (!this.isOffline) {
|
||||
try {
|
||||
await saveToServer();
|
||||
} catch (error) {
|
||||
console.error('Failed to save to server, will sync later:', error);
|
||||
}
|
||||
} else {
|
||||
console.log(`💾 Conversation queued for sync (offline mode)`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create chapter
|
||||
*/
|
||||
async createChapter(
|
||||
chapterData: Omit<ChapterProps, 'chapterId'>,
|
||||
bookId: string,
|
||||
createOnServer: () => Promise<string>
|
||||
): Promise<string> {
|
||||
if (!window.electron) {
|
||||
return await createOnServer();
|
||||
}
|
||||
|
||||
if (this.isOffline) {
|
||||
const localId = crypto.randomUUID();
|
||||
const chapter = { ...chapterData, chapterId: localId };
|
||||
await window.electron.dbSaveChapter(chapter, bookId);
|
||||
console.log(`💾 Chapter "${chapter.title}" created locally (offline mode)`);
|
||||
return localId;
|
||||
} else {
|
||||
const serverId = await createOnServer();
|
||||
const chapter = { ...chapterData, chapterId: serverId };
|
||||
await window.electron.dbSaveChapter(chapter, bookId);
|
||||
return serverId;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Save chapter content
|
||||
*/
|
||||
async saveChapter(
|
||||
chapter: ChapterProps,
|
||||
bookId: string,
|
||||
saveToServer: () => Promise<void>
|
||||
): Promise<void> {
|
||||
if (!window.electron) {
|
||||
return await saveToServer();
|
||||
}
|
||||
|
||||
// Always save to local DB first
|
||||
await window.electron.dbSaveChapter(chapter, bookId);
|
||||
|
||||
if (!this.isOffline) {
|
||||
try {
|
||||
await saveToServer();
|
||||
} catch (error) {
|
||||
console.error('Failed to save to server, will sync later:', error);
|
||||
}
|
||||
} else {
|
||||
console.log(`💾 Chapter "${chapter.title}" queued for sync (offline mode)`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Singleton instance
|
||||
let offlineDataServiceInstance: OfflineDataService | null = null;
|
||||
|
||||
/**
|
||||
* Save conversation
|
||||
* Get OfflineDataService singleton instance
|
||||
*/
|
||||
export async function saveConversation(
|
||||
conversation: Conversation,
|
||||
bookId: string,
|
||||
saveToServer: () => Promise<void>
|
||||
): Promise<void> {
|
||||
if (!window.electron) {
|
||||
return await saveToServer();
|
||||
}
|
||||
|
||||
const isOffline = false; // Replace with actual offline check
|
||||
|
||||
// Always save to local DB first
|
||||
await window.electron.dbSaveConversation(conversation, bookId);
|
||||
|
||||
if (!isOffline) {
|
||||
try {
|
||||
await saveToServer();
|
||||
} catch (error) {
|
||||
console.error('Failed to save to server, will sync later:', error);
|
||||
}
|
||||
export function getOfflineDataService(): OfflineDataService {
|
||||
if (!offlineDataServiceInstance) {
|
||||
offlineDataServiceInstance = new OfflineDataService();
|
||||
}
|
||||
return offlineDataServiceInstance;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user