Remove CharacterComponent and CharacterDetail components

- Deleted `CharacterComponent` and `CharacterDetail` files from the project.
- Refactored related logic to improve code maintainability and reduce redundancy.
This commit is contained in:
natreex
2026-02-05 14:12:08 -05:00
parent cec5830360
commit 209dc6f85a
133 changed files with 17673 additions and 3110 deletions

View File

@@ -36,6 +36,7 @@ import { SyncedActSummary } from "./Act.js";
import { SyncedAIGuideLine, SyncedGuideLine } from "./GuideLine.js";
import Cover from "./Cover.js";
import UserRepo from "../repositories/user.repository.js";
import RemovedItem from "./RemovedItem.js";
export interface SyncedBookTools {
lastUpdate: number;
@@ -369,6 +370,7 @@ export interface SyncedSeriesSpellTag {
export interface SyncedSeries {
id: string;
name: string;
description: string | null;
lastUpdate: number;
books: SyncedSeriesBook[];
characters: SyncedSeriesCharacter[];
@@ -500,6 +502,8 @@ export default class Book {
const book: Book = new Book(bookId);
book.getBookInfos(userId);
const bookTools: BookToolsTable | null = BookRepo.fetchBookTools(userId, bookId, lang);
// Récupérer le seriesId depuis series_books
const seriesId: string | null = BookRepo.fetchBookSeriesId(bookId, lang);
return {
bookId: book.getId(),
type: book.getType(),
@@ -508,6 +512,7 @@ export default class Book {
subTitle: book.getSubTitle(),
summary: book.getSummary(),
serieId: book.getSerieId(),
seriesId: seriesId,
desiredReleaseDate: book.getDesiredReleaseDate(),
desiredWordCount: book.getDesiredWordCount(),
wordCount: book.getWordCount(),
@@ -547,11 +552,16 @@ export default class Book {
* Removes a book from the database.
* @param userId - The unique identifier of the user
* @param bookId - The unique identifier of the book to remove
* @param deletedAt - The timestamp of deletion (from UI via System.timeStampInSeconds())
* @param lang - The language for error messages ('fr' or 'en')
* @returns True if the book was removed, false otherwise
*/
public static removeBook(userId: string, bookId: string, lang: 'fr' | 'en' = 'fr'): boolean {
return BookRepo.deleteBook(userId, bookId, lang);
public static removeBook(userId: string, bookId: string, deletedAt: number, lang: 'fr' | 'en' = 'fr'): boolean {
const deleted: boolean = BookRepo.deleteBook(userId, bookId, lang);
if (deleted) {
RemovedItem.deleteTracker(userId, bookId, 'erit_books', bookId, deletedAt, lang);
}
return deleted;
}
public static updateBookToolSetting(userId: string, bookId: string, toolName: 'characters' | 'worlds' | 'locations' | 'spells', enabled: boolean, lang: 'fr' | 'en' = 'fr'): boolean {

View File

@@ -13,6 +13,7 @@ import ChapterContentRepository, {
CompanionContentQueryResult,
ContentQueryResult
} from "../repositories/chaptercontent.repository.js";
import RemovedItem from "./RemovedItem.js";
export interface ChapterContent {
version: number;
@@ -262,12 +263,18 @@ export default class Chapter {
/**
* Removes a chapter from the database.
* @param userId - The unique identifier of the user
* @param bookId - The unique identifier of the book
* @param chapterId - The unique identifier of the chapter to remove
* @param deletedAt - The timestamp of deletion (from UI via System.timeStampInSeconds())
* @param lang - The language for error messages ('fr' or 'en')
* @returns True if the chapter was removed successfully, false otherwise
*/
public static removeChapter(userId: string, chapterId: string, lang: 'fr' | 'en' = 'fr'): boolean {
return ChapterRepo.deleteChapter(userId, chapterId, lang);
public static removeChapter(userId: string, bookId: string, chapterId: string, deletedAt: number, lang: 'fr' | 'en' = 'fr'): boolean {
const deleted: boolean = ChapterRepo.deleteChapter(userId, chapterId, lang);
if (deleted) {
RemovedItem.deleteTracker(userId, bookId, 'book_chapters', chapterId, deletedAt, lang);
}
return deleted;
}
/**
@@ -437,12 +444,18 @@ export default class Chapter {
/**
* Removes chapter information by its identifier.
* @param userId - The unique identifier of the user
* @param bookId - The unique identifier of the book
* @param chapterInfoId - The unique identifier of the chapter information to remove
* @param deletedAt - The timestamp of deletion (from UI via System.timeStampInSeconds())
* @param lang - The language for error messages ('fr' or 'en')
* @returns True if the chapter information was removed successfully, false otherwise
*/
static removeChapterInformation(userId: string, chapterInfoId: string, lang: 'fr' | 'en' = 'fr'): boolean {
return ChapterRepo.deleteChapterInformation(userId, chapterInfoId, lang);
static removeChapterInformation(userId: string, bookId: string, chapterInfoId: string, deletedAt: number, lang: 'fr' | 'en' = 'fr'): boolean {
const deleted: boolean = ChapterRepo.deleteChapterInformation(userId, chapterInfoId, lang);
if (deleted) {
RemovedItem.deleteTracker(userId, bookId, 'book_chapter_infos', chapterInfoId, deletedAt, lang);
}
return deleted;
}
/**

View File

@@ -6,6 +6,7 @@ import CharacterRepo, {
import BookRepo, {BookToolsTable} from "../repositories/book.repository.js";
import System from "../System.js";
import {getUserEncryptionKey} from "../keyManager.js";
import RemovedItem from "./RemovedItem.js";
export type CharacterCategory = 'Main' | 'Secondary' | 'Recurring';
@@ -290,23 +291,35 @@ export default class Character {
/**
* Deletes an attribute from a character.
* @param userId - The unique identifier of the user
* @param bookId - The unique identifier of the book
* @param attributeId - The unique identifier of the attribute to delete
* @param deletedAt - The timestamp of deletion
* @param lang - The language code for localization (defaults to 'fr')
* @returns True if the deletion was successful, false otherwise
*/
static deleteAttribute(userId: string, attributeId: string, lang: 'fr' | 'en' = 'fr'): boolean {
return CharacterRepo.deleteAttribute(userId, attributeId, lang);
static deleteAttribute(userId: string, bookId: string, attributeId: string, deletedAt: number = System.timeStampInSeconds(), lang: 'fr' | 'en' = 'fr'): boolean {
const deleted: boolean = CharacterRepo.deleteAttribute(userId, attributeId, lang);
if (deleted) {
RemovedItem.deleteTracker(userId, bookId, 'book_characters_attributes', attributeId, deletedAt, lang);
}
return deleted;
}
/**
* Deletes a character and all its related data.
* @param userId - The unique identifier of the user
* @param bookId - The unique identifier of the book
* @param characterId - The unique identifier of the character to delete
* @param deletedAt - The timestamp of deletion
* @param lang - The language code for localization (defaults to 'fr')
* @returns True if the deletion was successful
*/
static deleteCharacter(userId: string, characterId: string, lang: 'fr' | 'en' = 'fr'): boolean {
return CharacterRepo.deleteCharacter(userId, characterId, lang);
static deleteCharacter(userId: string, bookId: string, characterId: string, deletedAt: number = System.timeStampInSeconds(), lang: 'fr' | 'en' = 'fr'): boolean {
const deleted: boolean = CharacterRepo.deleteCharacter(userId, characterId, lang);
if (deleted) {
RemovedItem.deleteTracker(userId, bookId, 'book_characters', characterId, deletedAt, lang);
}
return deleted;
}
/**

View File

@@ -235,9 +235,9 @@ export default class Download {
const spellsInserted: boolean = data.spells.every((spell: BookSpellsTable): boolean => {
const encryptedName: string = System.encryptDataWithUserKey(spell.name, userEncryptionKey);
const encryptedDescription: string = System.encryptDataWithUserKey(spell.description, userEncryptionKey);
const encryptedAppearance: string = System.encryptDataWithUserKey(spell.appearance, userEncryptionKey);
const encryptedTags: string = System.encryptDataWithUserKey(spell.tags, userEncryptionKey);
const encryptedDescription: string | null = spell.description ? System.encryptDataWithUserKey(spell.description, userEncryptionKey) : null;
const encryptedAppearance: string | null = spell.appearance ? System.encryptDataWithUserKey(spell.appearance, userEncryptionKey) : null;
const encryptedTags: string | null = spell.tags ? System.encryptDataWithUserKey(spell.tags, userEncryptionKey) : null;
const encryptedPowerLevel: string | null = spell.power_level ? System.encryptDataWithUserKey(spell.power_level, userEncryptionKey) : null;
const encryptedComponents: string | null = spell.components ? System.encryptDataWithUserKey(spell.components, userEncryptionKey) : null;
const encryptedLimitations: string | null = spell.limitations ? System.encryptDataWithUserKey(spell.limitations, userEncryptionKey) : null;

View File

@@ -2,6 +2,7 @@ import { getUserEncryptionKey } from "../keyManager.js";
import System from "../System.js";
import { ActChapter } from "./Act.js";
import IncidentRepository, { IncidentQuery } from "../repositories/incident.repository.js";
import RemovedItem from "./RemovedItem.js";
export interface IncidentStory {
incidentTitle: string;
@@ -91,6 +92,7 @@ export default class Incident {
* @param userId - The unique identifier of the user
* @param bookId - The unique identifier of the book
* @param incidentId - The unique identifier of the incident to remove
* @param deletedAt - The timestamp of deletion
* @param lang - The language for error messages (defaults to 'fr')
* @returns True if the incident was successfully deleted, false otherwise
*/
@@ -98,8 +100,13 @@ export default class Incident {
userId: string,
bookId: string,
incidentId: string,
deletedAt: number = System.timeStampInSeconds(),
lang: 'fr' | 'en' = 'fr'
): boolean {
return IncidentRepository.deleteIncident(userId, bookId, incidentId, lang);
const deleted: boolean = IncidentRepository.deleteIncident(userId, bookId, incidentId, lang);
if (deleted) {
RemovedItem.deleteTracker(userId, bookId, 'book_incidents', incidentId, deletedAt, lang);
}
return deleted;
}
}

View File

@@ -1,6 +1,7 @@
import { getUserEncryptionKey } from "../keyManager.js";
import System from "../System.js";
import IssueRepository, { IssueQuery } from "../repositories/issue.repository.js";
import RemovedItem from "./RemovedItem.js";
/**
* Represents a synced issue with its metadata.
@@ -84,15 +85,23 @@ export default class Issue {
* Removes an issue from the database.
*
* @param userId - The unique identifier of the user.
* @param bookId - The unique identifier of the book.
* @param issueId - The unique identifier of the issue to remove.
* @param deletedAt - The timestamp of deletion.
* @param lang - The language for error messages ('fr' or 'en'). Defaults to 'fr'.
* @returns True if the issue was successfully removed, false otherwise.
*/
public static removeIssue(
userId: string,
bookId: string,
issueId: string,
deletedAt: number = System.timeStampInSeconds(),
lang: 'fr' | 'en' = 'fr'
): boolean {
return IssueRepository.deleteIssue(userId, issueId, lang);
const deleted: boolean = IssueRepository.deleteIssue(userId, issueId, lang);
if (deleted) {
RemovedItem.deleteTracker(userId, bookId, 'book_issues', issueId, deletedAt, lang);
}
return deleted;
}
}

View File

@@ -6,6 +6,7 @@ import LocationRepo, {
import System from "../System.js";
import {getUserEncryptionKey} from "../keyManager.js";
import BookRepo, {BookToolsTable} from "../repositories/book.repository.js";
import RemovedItem from "./RemovedItem.js";
export interface SubElement {
id: string;
@@ -229,34 +230,52 @@ export default class Location {
/**
* Deletes a location section and all its associated elements and sub-elements.
* @param userId - The user's unique identifier.
* @param bookId - The book's unique identifier.
* @param locationId - The location's unique identifier to delete.
* @param deletedAt - The timestamp of deletion.
* @param lang - The language for error messages ('fr' or 'en'). Defaults to 'fr'.
* @returns The result of the delete operation.
*/
static deleteLocationSection(userId: string, locationId: string, lang: 'fr' | 'en' = 'fr'): boolean {
return LocationRepo.deleteLocationSection(userId, locationId, lang);
static deleteLocationSection(userId: string, bookId: string, locationId: string, deletedAt: number = System.timeStampInSeconds(), lang: 'fr' | 'en' = 'fr'): boolean {
const deleted: boolean = LocationRepo.deleteLocationSection(userId, locationId, lang);
if (deleted) {
RemovedItem.deleteTracker(userId, bookId, 'book_location', locationId, deletedAt, lang);
}
return deleted;
}
/**
* Deletes a location element and all its associated sub-elements.
* @param userId - The user's unique identifier.
* @param bookId - The book's unique identifier.
* @param elementId - The element's unique identifier to delete.
* @param deletedAt - The timestamp of deletion.
* @param lang - The language for error messages ('fr' or 'en'). Defaults to 'fr'.
* @returns The result of the delete operation.
*/
static deleteLocationElement(userId: string, elementId: string, lang: 'fr' | 'en' = 'fr'): boolean {
return LocationRepo.deleteLocationElement(userId, elementId, lang);
static deleteLocationElement(userId: string, bookId: string, elementId: string, deletedAt: number = System.timeStampInSeconds(), lang: 'fr' | 'en' = 'fr'): boolean {
const deleted: boolean = LocationRepo.deleteLocationElement(userId, elementId, lang);
if (deleted) {
RemovedItem.deleteTracker(userId, bookId, 'location_element', elementId, deletedAt, lang);
}
return deleted;
}
/**
* Deletes a location sub-element.
* @param userId - The user's unique identifier.
* @param bookId - The book's unique identifier.
* @param subElementId - The sub-element's unique identifier to delete.
* @param deletedAt - The timestamp of deletion.
* @param lang - The language for error messages ('fr' or 'en'). Defaults to 'fr'.
* @returns The result of the delete operation.
*/
static deleteLocationSubElement(userId: string, subElementId: string, lang: 'fr' | 'en' = 'fr'): boolean {
return LocationRepo.deleteLocationSubElement(userId, subElementId, lang);
static deleteLocationSubElement(userId: string, bookId: string, subElementId: string, deletedAt: number = System.timeStampInSeconds(), lang: 'fr' | 'en' = 'fr'): boolean {
const deleted: boolean = LocationRepo.deleteLocationSubElement(userId, subElementId, lang);
if (deleted) {
RemovedItem.deleteTracker(userId, bookId, 'location_sub_element', subElementId, deletedAt, lang);
}
return deleted;
}
/**

View File

@@ -2,6 +2,7 @@ import { getUserEncryptionKey } from "../keyManager.js";
import System from "../System.js";
import { ActChapter } from "./Act.js";
import PlotPointRepository, { PlotPointQuery } from "../repositories/plotpoint.repository.js";
import RemovedItem from "./RemovedItem.js";
export interface PlotPointStory {
plotTitle: string;
@@ -96,11 +97,17 @@ export default class PlotPoint {
/**
* Removes a plot point from the database.
* @param userId - The unique identifier of the user
* @param bookId - The unique identifier of the book
* @param plotId - The unique identifier of the plot point to remove
* @param deletedAt - The timestamp of deletion
* @param lang - The language for error messages ('fr' or 'en'), defaults to 'fr'
* @returns True if the plot point was successfully deleted, false otherwise
*/
static removePlotPoint(userId: string, plotId: string, lang: 'fr' | 'en' = 'fr'): boolean {
return PlotPointRepository.deletePlotPoint(userId, plotId, lang);
static removePlotPoint(userId: string, bookId: string, plotId: string, deletedAt: number = System.timeStampInSeconds(), lang: 'fr' | 'en' = 'fr'): boolean {
const deleted: boolean = PlotPointRepository.deletePlotPoint(userId, plotId, lang);
if (deleted) {
RemovedItem.deleteTracker(userId, bookId, 'book_plot_points', plotId, deletedAt, lang);
}
return deleted;
}
}

View File

@@ -0,0 +1,41 @@
import System from '../System.js';
import RemovedItemsRepository from '../repositories/removed-items.repository.js';
/**
* Model class for tracking deleted items for sync purposes.
* Provides the main entry point for recording deletions.
*/
export default class RemovedItem {
/**
* Records a deleted item for sync tracking.
* Must be called BEFORE the actual deletion from the source table.
*
* @param userId - The unique identifier of the user.
* @param bookId - The book ID (null for series items).
* @param tableName - The name of the table from which the item is deleted.
* @param entityId - The UUID of the deleted entity.
* @param deletedAt - The timestamp of deletion (from UI via System.timeStampInSeconds()).
* @param lang - The language for error messages ('fr' or 'en'). Defaults to 'fr'.
* @returns True if the record was inserted successfully.
*/
public static deleteTracker(
userId: string,
bookId: string | null,
tableName: string,
entityId: string,
deletedAt: number,
lang: 'fr' | 'en' = 'fr'
): boolean {
const removalId: string = System.createUniqueId();
return RemovedItemsRepository.insert(
removalId,
tableName,
entityId,
bookId,
userId,
deletedAt,
lang
);
}
}

View File

@@ -1,6 +1,7 @@
import { getUserEncryptionKey } from "../keyManager.js";
import System from "../System.js";
import SeriesRepo, { SeriesBookResult, SeriesListItem, SeriesResult } from "../repositories/series.repo.js";
import RemovedItem from "./RemovedItem.js";
export interface SeriesProps {
id: string;
@@ -148,16 +149,21 @@ export default class Series {
* Deletes a series.
* @param userId - The unique identifier of the user
* @param seriesId - The unique identifier of the series
* @param deletedAt - The timestamp of deletion
* @param lang - The language for error messages ('fr' or 'en')
* @returns True if the deletion was successful
*/
public static deleteSeries(userId: string, seriesId: string, lang: 'fr' | 'en' = 'fr'): boolean {
public static deleteSeries(userId: string, seriesId: string, deletedAt: number = System.timeStampInSeconds(), lang: 'fr' | 'en' = 'fr'): boolean {
const exists: boolean = SeriesRepo.isSeriesExist(userId, seriesId, lang);
if (!exists) {
throw new Error(lang === 'fr' ? 'Série non trouvée.' : 'Series not found.');
}
return SeriesRepo.deleteSeries(userId, seriesId, lang);
const deleted: boolean = SeriesRepo.deleteSeries(userId, seriesId, lang);
if (deleted) {
RemovedItem.deleteTracker(userId, null, 'book_series', seriesId, deletedAt, lang);
}
return deleted;
}
/**
@@ -183,16 +189,21 @@ export default class Series {
* @param userId - The unique identifier of the user
* @param seriesId - The unique identifier of the series
* @param bookId - The unique identifier of the book
* @param deletedAt - The timestamp of deletion
* @param lang - The language for error messages ('fr' or 'en')
* @returns True if the removal was successful
*/
public static removeBookFromSeries(userId: string, seriesId: string, bookId: string, lang: 'fr' | 'en' = 'fr'): boolean {
public static removeBookFromSeries(userId: string, seriesId: string, bookId: string, deletedAt: number = System.timeStampInSeconds(), lang: 'fr' | 'en' = 'fr'): boolean {
const exists: boolean = SeriesRepo.isSeriesExist(userId, seriesId, lang);
if (!exists) {
throw new Error(lang === 'fr' ? 'Série non trouvée.' : 'Series not found.');
}
return SeriesRepo.removeBookFromSeries(seriesId, bookId, lang);
const deleted: boolean = SeriesRepo.removeBookFromSeries(seriesId, bookId, lang);
if (deleted) {
RemovedItem.deleteTracker(userId, null, 'series_books', `${seriesId}_${bookId}`, deletedAt, lang);
}
return deleted;
}
/**

View File

@@ -1,6 +1,7 @@
import { getUserEncryptionKey } from "../keyManager.js";
import System from "../System.js";
import SeriesCharacterRepo, { SeriesCharacterAttributeResult, SeriesCharacterResult } from "../repositories/series-character.repo.js";
import RemovedItem from "./RemovedItem.js";
export type CharacterCategory = 'Main' | 'Secondary' | 'Recurring';
@@ -213,15 +214,20 @@ export default class SeriesCharacter {
* Deletes a character from a series.
* @param userId - The unique identifier of the user
* @param characterId - The unique identifier of the character
* @param deletedAt - The timestamp of deletion
* @param lang - The language for error messages ('fr' or 'en')
* @returns True if the deletion was successful
*/
public static deleteCharacter(userId: string, characterId: string, lang: 'fr' | 'en' = 'fr'): boolean {
public static deleteCharacter(userId: string, characterId: string, deletedAt: number = System.timeStampInSeconds(), lang: 'fr' | 'en' = 'fr'): boolean {
const exists: boolean = SeriesCharacterRepo.isCharacterExist(userId, characterId, lang);
if (!exists) {
throw new Error(lang === 'fr' ? 'Personnage non trouvé.' : 'Character not found.');
}
return SeriesCharacterRepo.deleteCharacter(userId, characterId, lang);
const deleted: boolean = SeriesCharacterRepo.deleteCharacter(userId, characterId, lang);
if (deleted) {
RemovedItem.deleteTracker(userId, null, 'series_characters', characterId, deletedAt, lang);
}
return deleted;
}
/**
@@ -248,11 +254,16 @@ export default class SeriesCharacter {
* Deletes an attribute from a character.
* @param userId - The unique identifier of the user
* @param attributeId - The unique identifier of the attribute
* @param deletedAt - The timestamp of deletion
* @param lang - The language for error messages ('fr' or 'en')
* @returns True if the deletion was successful
*/
public static deleteAttribute(userId: string, attributeId: string, lang: 'fr' | 'en' = 'fr'): boolean {
return SeriesCharacterRepo.deleteAttribute(userId, attributeId, lang);
public static deleteAttribute(userId: string, attributeId: string, deletedAt: number = System.timeStampInSeconds(), lang: 'fr' | 'en' = 'fr'): boolean {
const deleted: boolean = SeriesCharacterRepo.deleteAttribute(userId, attributeId, lang);
if (deleted) {
RemovedItem.deleteTracker(userId, null, 'series_characters_attributes', attributeId, deletedAt, lang);
}
return deleted;
}
/**

View File

@@ -1,6 +1,7 @@
import { getUserEncryptionKey } from "../keyManager.js";
import System from "../System.js";
import SeriesLocationRepo, { SeriesLocationResult, SeriesLocationElementResult, SeriesLocationSubElementResult } from "../repositories/series-location.repo.js";
import RemovedItem from "./RemovedItem.js";
export interface SeriesLocationSubElementProps {
id: string;
@@ -123,32 +124,47 @@ export default class SeriesLocation {
* Deletes a location section.
* @param userId - The unique identifier of the user
* @param locationId - The unique identifier of the location
* @param deletedAt - The timestamp of deletion
* @param lang - The language for error messages ('fr' or 'en')
* @returns True if successful
*/
public static deleteLocation(userId: string, locationId: string, lang: 'fr' | 'en' = 'fr'): boolean {
return SeriesLocationRepo.deleteLocation(userId, locationId, lang);
public static deleteLocation(userId: string, locationId: string, deletedAt: number = System.timeStampInSeconds(), lang: 'fr' | 'en' = 'fr'): boolean {
const deleted: boolean = SeriesLocationRepo.deleteLocation(userId, locationId, lang);
if (deleted) {
RemovedItem.deleteTracker(userId, null, 'series_locations', locationId, deletedAt, lang);
}
return deleted;
}
/**
* Deletes an element.
* @param userId - The unique identifier of the user
* @param elementId - The unique identifier of the element
* @param deletedAt - The timestamp of deletion
* @param lang - The language for error messages ('fr' or 'en')
* @returns True if successful
*/
public static deleteElement(userId: string, elementId: string, lang: 'fr' | 'en' = 'fr'): boolean {
return SeriesLocationRepo.deleteElement(userId, elementId, lang);
public static deleteElement(userId: string, elementId: string, deletedAt: number = System.timeStampInSeconds(), lang: 'fr' | 'en' = 'fr'): boolean {
const deleted: boolean = SeriesLocationRepo.deleteElement(userId, elementId, lang);
if (deleted) {
RemovedItem.deleteTracker(userId, null, 'series_location_elements', elementId, deletedAt, lang);
}
return deleted;
}
/**
* Deletes a sub-element.
* @param userId - The unique identifier of the user
* @param subElementId - The unique identifier of the sub-element
* @param deletedAt - The timestamp of deletion
* @param lang - The language for error messages ('fr' or 'en')
* @returns True if successful
*/
public static deleteSubElement(userId: string, subElementId: string, lang: 'fr' | 'en' = 'fr'): boolean {
return SeriesLocationRepo.deleteSubElement(userId, subElementId, lang);
public static deleteSubElement(userId: string, subElementId: string, deletedAt: number = System.timeStampInSeconds(), lang: 'fr' | 'en' = 'fr'): boolean {
const deleted: boolean = SeriesLocationRepo.deleteSubElement(userId, subElementId, lang);
if (deleted) {
RemovedItem.deleteTracker(userId, null, 'series_location_sub_elements', subElementId, deletedAt, lang);
}
return deleted;
}
}

View File

@@ -1,6 +1,7 @@
import { getUserEncryptionKey } from "../keyManager.js";
import System from "../System.js";
import SeriesSpellRepo, { SeriesSpellResult, SeriesSpellTagResult } from "../repositories/series-spell.repo.js";
import RemovedItem from "./RemovedItem.js";
export interface SeriesSpellTagProps {
id: string;
@@ -155,11 +156,16 @@ export default class SeriesSpell {
* Deletes a spell.
* @param userId - The unique identifier of the user
* @param spellId - The unique identifier of the spell
* @param deletedAt - The timestamp of deletion
* @param lang - The language for error messages ('fr' or 'en')
* @returns True if successful
*/
public static deleteSpell(userId: string, spellId: string, lang: 'fr' | 'en' = 'fr'): boolean {
return SeriesSpellRepo.deleteSpell(userId, spellId, lang);
public static deleteSpell(userId: string, spellId: string, deletedAt: number = System.timeStampInSeconds(), lang: 'fr' | 'en' = 'fr'): boolean {
const deleted: boolean = SeriesSpellRepo.deleteSpell(userId, spellId, lang);
if (deleted) {
RemovedItem.deleteTracker(userId, null, 'series_spells', spellId, deletedAt, lang);
}
return deleted;
}
/**
@@ -202,10 +208,15 @@ export default class SeriesSpell {
* Deletes a tag.
* @param userId - The unique identifier of the user
* @param tagId - The unique identifier of the tag
* @param deletedAt - The timestamp of deletion
* @param lang - The language for error messages ('fr' or 'en')
* @returns True if successful
*/
public static deleteTag(userId: string, tagId: string, lang: 'fr' | 'en' = 'fr'): boolean {
return SeriesSpellRepo.deleteTag(userId, tagId, lang);
public static deleteTag(userId: string, tagId: string, deletedAt: number = System.timeStampInSeconds(), lang: 'fr' | 'en' = 'fr'): boolean {
const deleted: boolean = SeriesSpellRepo.deleteTag(userId, tagId, lang);
if (deleted) {
RemovedItem.deleteTracker(userId, null, 'series_spell_tags', tagId, deletedAt, lang);
}
return deleted;
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,7 @@
import { getUserEncryptionKey } from "../keyManager.js";
import System from "../System.js";
import SeriesWorldRepo, { SeriesWorldResult } from "../repositories/series-world.repo.js";
import RemovedItem from "./RemovedItem.js";
export interface SeriesWorldElementProps {
id: string;
@@ -181,10 +182,15 @@ export default class SeriesWorld {
* Deletes an element from a world.
* @param userId - The unique identifier of the user
* @param elementId - The unique identifier of the element
* @param deletedAt - The timestamp of deletion
* @param lang - The language for error messages ('fr' or 'en')
* @returns True if successful
*/
public static deleteElement(userId: string, elementId: string, lang: 'fr' | 'en' = 'fr'): boolean {
return SeriesWorldRepo.deleteElement(userId, elementId, lang);
public static deleteElement(userId: string, elementId: string, deletedAt: number = System.timeStampInSeconds(), lang: 'fr' | 'en' = 'fr'): boolean {
const deleted: boolean = SeriesWorldRepo.deleteElement(userId, elementId, lang);
if (deleted) {
RemovedItem.deleteTracker(userId, null, 'series_world_elements', elementId, deletedAt, lang);
}
return deleted;
}
}

View File

@@ -3,6 +3,7 @@ import SpellTagRepo, { SpellTagResult } from '../repositories/spelltag.repo.js';
import BookRepo, { BookToolsTable } from '../repositories/book.repository.js';
import System from '../System.js';
import { getUserEncryptionKey } from '../keyManager.js';
import RemovedItem from './RemovedItem.js';
export interface SpellTagProps {
id: string;
@@ -118,16 +119,16 @@ export default class Spell {
* @param lang - The language for error messages ('fr' or 'en')
* @returns True if the deletion was successful
*/
static deleteSpellTag(userId: string, tagId: string, bookId: string, lang: 'fr' | 'en' = 'fr'): boolean {
static deleteSpellTag(userId: string, bookId: string, tagId: string, deletedAt: number = System.timeStampInSeconds(), lang: 'fr' | 'en' = 'fr'): boolean {
const userKey: string = getUserEncryptionKey(userId);
const spells: SpellResult[] = SpellRepo.fetchSpells(userId, bookId, lang);
for (const spell of spells) {
const decryptedTags: string = System.decryptDataWithUserKey(spell.tags, userKey);
const decryptedTags: string | null = spell.tags ? System.decryptDataWithUserKey(spell.tags, userKey) : null;
let tagsArray: string[] = [];
try {
tagsArray = JSON.parse(decryptedTags) as string[];
tagsArray = decryptedTags ? JSON.parse(decryptedTags) as string[] : [];
} catch {
tagsArray = [];
}
@@ -140,7 +141,11 @@ export default class Spell {
}
// Then delete the tag
return SpellTagRepo.deleteSpellTag(userId, tagId, lang);
const deleted: boolean = SpellTagRepo.deleteSpellTag(userId, tagId, lang);
if (deleted) {
RemovedItem.deleteTracker(userId, bookId, 'book_spell_tags', tagId, deletedAt, lang);
}
return deleted;
}
/**
@@ -172,12 +177,12 @@ export default class Spell {
const spells: SpellListItem[] = spellResults.map((spell: SpellResult): SpellListItem => {
const decryptedName: string = System.decryptDataWithUserKey(spell.name, userKey);
const decryptedDescription: string = System.decryptDataWithUserKey(spell.description, userKey);
const decryptedTags: string = System.decryptDataWithUserKey(spell.tags, userKey);
const decryptedDescription: string | null = spell.description ? System.decryptDataWithUserKey(spell.description, userKey) : null;
const decryptedTags: string | null = spell.tags ? System.decryptDataWithUserKey(spell.tags, userKey) : null;
let tagIds: string[];
try {
tagIds = JSON.parse(decryptedTags) as string[];
tagIds = decryptedTags ? JSON.parse(decryptedTags) as string[] : [];
} catch {
tagIds = [];
}
@@ -186,9 +191,9 @@ export default class Spell {
.map((tagId: string): SpellTagProps | undefined => tagMap.get(tagId))
.filter((tag: SpellTagProps | undefined): tag is SpellTagProps => tag !== undefined);
const truncatedDescription: string = decryptedDescription.length > 150
? decryptedDescription.substring(0, 150) + '...'
: decryptedDescription;
const truncatedDescription: string = decryptedDescription
? (decryptedDescription.length > 150 ? decryptedDescription.substring(0, 150) + '...' : decryptedDescription)
: '';
return {
id: spell.spell_id,
@@ -222,13 +227,13 @@ export default class Spell {
}
const decryptedName: string = System.decryptDataWithUserKey(spell.name, userKey);
const decryptedDescription: string = System.decryptDataWithUserKey(spell.description, userKey);
const decryptedAppearance: string = System.decryptDataWithUserKey(spell.appearance, userKey);
const decryptedTags: string = System.decryptDataWithUserKey(spell.tags, userKey);
const decryptedDescription: string | null = spell.description ? System.decryptDataWithUserKey(spell.description, userKey) : null;
const decryptedAppearance: string | null = spell.appearance ? System.decryptDataWithUserKey(spell.appearance, userKey) : null;
const decryptedTags: string | null = spell.tags ? System.decryptDataWithUserKey(spell.tags, userKey) : null;
let tagIds: string[];
try {
tagIds = JSON.parse(decryptedTags) as string[];
tagIds = decryptedTags ? JSON.parse(decryptedTags) as string[] : [];
} catch {
tagIds = [];
}
@@ -236,8 +241,8 @@ export default class Spell {
return {
id: spell.spell_id,
name: decryptedName,
description: decryptedDescription,
appearance: decryptedAppearance,
description: decryptedDescription || '',
appearance: decryptedAppearance || '',
tags: tagIds,
powerLevel: spell.power_level ? System.decryptDataWithUserKey(spell.power_level, userKey) : null,
components: spell.components ? System.decryptDataWithUserKey(spell.components, userKey) : null,
@@ -356,11 +361,17 @@ export default class Spell {
/**
* Deletes a spell.
* @param userId - The unique identifier of the user
* @param bookId - The unique identifier of the book
* @param spellId - The unique identifier of the spell
* @param deletedAt - The timestamp of deletion
* @param lang - The language for error messages ('fr' or 'en')
* @returns True if the deletion was successful
*/
static deleteSpell(userId: string, spellId: string, lang: 'fr' | 'en' = 'fr'): boolean {
return SpellRepo.deleteSpell(userId, spellId, lang);
static deleteSpell(userId: string, bookId: string, spellId: string, deletedAt: number = System.timeStampInSeconds(), lang: 'fr' | 'en' = 'fr'): boolean {
const deleted: boolean = SpellRepo.deleteSpell(userId, spellId, lang);
if (deleted) {
RemovedItem.deleteTracker(userId, bookId, 'book_spells', spellId, deletedAt, lang);
}
return deleted;
}
}

View File

@@ -57,6 +57,40 @@ import { SyncedPlotPoint } from "./PlotPoint.js";
import { SyncedIssue } from "./Issue.js";
import { SyncedActSummary } from "./Act.js";
import { SyncedAIGuideLine, SyncedGuideLine } from "./GuideLine.js";
import {
SyncedSeries,
SyncedSeriesBook,
SyncedSeriesCharacter,
SyncedSeriesCharacterAttribute,
SyncedSeriesWorld,
SyncedSeriesWorldElement,
SyncedSeriesLocation,
SyncedSeriesLocationElement,
SyncedSeriesLocationSubElement,
SyncedSeriesSpell,
SyncedSeriesSpellTag
} from "./Book.js";
import SeriesRepo, {
SyncedSeriesResult,
SyncedSeriesBookResult
} from "../repositories/series.repo.js";
import SeriesCharacterRepo, {
SyncedSeriesCharacterResult,
SyncedSeriesCharacterAttributeResult
} from "../repositories/series-character.repo.js";
import SeriesWorldRepo, {
SyncedSeriesWorldResult,
SyncedSeriesWorldElementResult
} from "../repositories/series-world.repo.js";
import SeriesLocationRepo, {
SyncedSeriesLocationResult,
SyncedSeriesLocationElementResult,
SyncedSeriesLocationSubElementResult
} from "../repositories/series-location.repo.js";
import SeriesSpellRepo, {
SyncedSeriesSpellResult,
SyncedSeriesSpellTagResult
} from "../repositories/series-spell.repo.js";
/**
* Handles synchronization operations between local database and remote server.
@@ -375,9 +409,9 @@ export default class Sync {
decryptedSpells.push({
...spellRecord,
name: System.decryptDataWithUserKey(spellRecord.name, userEncryptionKey),
description: System.decryptDataWithUserKey(spellRecord.description, userEncryptionKey),
appearance: System.decryptDataWithUserKey(spellRecord.appearance, userEncryptionKey),
tags: System.decryptDataWithUserKey(spellRecord.tags, userEncryptionKey),
description: spellRecord.description ? System.decryptDataWithUserKey(spellRecord.description, userEncryptionKey) : null,
appearance: spellRecord.appearance ? System.decryptDataWithUserKey(spellRecord.appearance, userEncryptionKey) : null,
tags: spellRecord.tags ? System.decryptDataWithUserKey(spellRecord.tags, userEncryptionKey) : null,
power_level: spellRecord.power_level ? System.decryptDataWithUserKey(spellRecord.power_level, userEncryptionKey) : null,
components: spellRecord.components ? System.decryptDataWithUserKey(spellRecord.components, userEncryptionKey) : null,
limitations: spellRecord.limitations ? System.decryptDataWithUserKey(spellRecord.limitations, userEncryptionKey) : null,
@@ -823,9 +857,9 @@ export default class Sync {
for (const serverSpell of completeBook.spells) {
const spellExists: boolean = SpellRepo.isSpellExist(userId, serverSpell.spell_id, lang);
const encryptedName: string = System.encryptDataWithUserKey(serverSpell.name, userEncryptionKey);
const encryptedDescription: string = System.encryptDataWithUserKey(serverSpell.description, userEncryptionKey);
const encryptedAppearance: string = System.encryptDataWithUserKey(serverSpell.appearance, userEncryptionKey);
const encryptedTags: string = System.encryptDataWithUserKey(serverSpell.tags, userEncryptionKey);
const encryptedDescription: string | null = serverSpell.description ? System.encryptDataWithUserKey(serverSpell.description, userEncryptionKey) : null;
const encryptedAppearance: string | null = serverSpell.appearance ? System.encryptDataWithUserKey(serverSpell.appearance, userEncryptionKey) : null;
const encryptedTags: string | null = serverSpell.tags ? System.encryptDataWithUserKey(serverSpell.tags, userEncryptionKey) : null;
const encryptedPowerLevel: string | null = serverSpell.power_level ? System.encryptDataWithUserKey(serverSpell.power_level, userEncryptionKey) : null;
const encryptedComponents: string | null = serverSpell.components ? System.encryptDataWithUserKey(serverSpell.components, userEncryptionKey) : null;
const encryptedLimitations: string | null = serverSpell.limitations ? System.encryptDataWithUserKey(serverSpell.limitations, userEncryptionKey) : null;
@@ -1113,4 +1147,131 @@ export default class Sync {
};
});
}
// ===== SERIES SYNC METHODS =====
/**
* Retrieves all series for the current user with lightweight structure for sync comparison.
* Returns synced series with nested characters, worlds, locations, spells, and spell tags.
* All encrypted fields are decrypted before return.
* @param userId - The unique identifier of the user
* @param lang - The language for error messages ('fr' or 'en')
* @returns An array of synced series with all nested entities
*/
static getSyncedSeries(userId: string, lang: 'fr' | 'en'): SyncedSeries[] {
const userEncryptionKey: string = getUserEncryptionKey(userId);
const allSeries: SyncedSeriesResult[] = SeriesRepo.fetchSyncedSeries(userId, lang);
const allSeriesBooks: SyncedSeriesBookResult[] = SeriesRepo.fetchSyncedSeriesBooks(userId, lang);
const allCharacters: SyncedSeriesCharacterResult[] = SeriesCharacterRepo.fetchSyncedSeriesCharacters(userId, lang);
const allCharacterAttributes: SyncedSeriesCharacterAttributeResult[] = SeriesCharacterRepo.fetchSyncedSeriesCharacterAttributes(userId, lang);
const allWorlds: SyncedSeriesWorldResult[] = SeriesWorldRepo.fetchSyncedSeriesWorlds(userId, lang);
const allWorldElements: SyncedSeriesWorldElementResult[] = SeriesWorldRepo.fetchSyncedSeriesWorldElements(userId, lang);
const allLocations: SyncedSeriesLocationResult[] = SeriesLocationRepo.fetchSyncedSeriesLocations(userId, lang);
const allLocationElements: SyncedSeriesLocationElementResult[] = SeriesLocationRepo.fetchSyncedSeriesLocationElements(userId, lang);
const allLocationSubElements: SyncedSeriesLocationSubElementResult[] = SeriesLocationRepo.fetchSyncedSeriesLocationSubElements(userId, lang);
const allSpells: SyncedSeriesSpellResult[] = SeriesSpellRepo.fetchSyncedSeriesSpells(userId, lang);
const allSpellTags: SyncedSeriesSpellTagResult[] = SeriesSpellRepo.fetchSyncedSeriesSpellTags(userId, lang);
return allSeries.map((series: SyncedSeriesResult): SyncedSeries => {
const seriesId: string = series.series_id;
// Map series books
const books: SyncedSeriesBook[] = allSeriesBooks
.filter((sb: SyncedSeriesBookResult): boolean => sb.series_id === seriesId)
.map((sb: SyncedSeriesBookResult): SyncedSeriesBook => ({
bookId: sb.book_id,
order: sb.book_order,
lastUpdate: sb.last_update
}));
// Map characters with attributes
const characters: SyncedSeriesCharacter[] = allCharacters
.filter((c: SyncedSeriesCharacterResult): boolean => c.series_id === seriesId)
.map((c: SyncedSeriesCharacterResult): SyncedSeriesCharacter => ({
id: c.character_id,
name: System.decryptDataWithUserKey(c.first_name, userEncryptionKey),
lastUpdate: c.last_update,
attributes: allCharacterAttributes
.filter((a: SyncedSeriesCharacterAttributeResult): boolean => a.character_id === c.character_id)
.map((a: SyncedSeriesCharacterAttributeResult): SyncedSeriesCharacterAttribute => ({
id: a.attr_id,
name: System.decryptDataWithUserKey(a.attribute_name, userEncryptionKey),
lastUpdate: a.last_update
}))
}));
// Map worlds with elements
const worlds: SyncedSeriesWorld[] = allWorlds
.filter((w: SyncedSeriesWorldResult): boolean => w.series_id === seriesId)
.map((w: SyncedSeriesWorldResult): SyncedSeriesWorld => ({
id: w.world_id,
name: System.decryptDataWithUserKey(w.name, userEncryptionKey),
lastUpdate: w.last_update,
elements: allWorldElements
.filter((e: SyncedSeriesWorldElementResult): boolean => e.world_id === w.world_id)
.map((e: SyncedSeriesWorldElementResult): SyncedSeriesWorldElement => ({
id: e.element_id,
name: System.decryptDataWithUserKey(e.name, userEncryptionKey),
lastUpdate: e.last_update
}))
}));
// Map locations with elements and sub-elements
const locations: SyncedSeriesLocation[] = allLocations
.filter((l: SyncedSeriesLocationResult): boolean => l.series_id === seriesId)
.map((l: SyncedSeriesLocationResult): SyncedSeriesLocation => ({
id: l.loc_id,
name: System.decryptDataWithUserKey(l.loc_name, userEncryptionKey),
lastUpdate: l.last_update,
elements: allLocationElements
.filter((e: SyncedSeriesLocationElementResult): boolean => e.location_id === l.loc_id)
.map((e: SyncedSeriesLocationElementResult): SyncedSeriesLocationElement => ({
id: e.element_id,
name: System.decryptDataWithUserKey(e.element_name, userEncryptionKey),
lastUpdate: e.last_update,
subElements: allLocationSubElements
.filter((se: SyncedSeriesLocationSubElementResult): boolean => se.element_id === e.element_id)
.map((se: SyncedSeriesLocationSubElementResult): SyncedSeriesLocationSubElement => ({
id: se.sub_element_id,
name: System.decryptDataWithUserKey(se.sub_elem_name, userEncryptionKey),
lastUpdate: se.last_update
}))
}))
}));
// Map spells
const spells: SyncedSeriesSpell[] = allSpells
.filter((s: SyncedSeriesSpellResult): boolean => s.series_id === seriesId)
.map((s: SyncedSeriesSpellResult): SyncedSeriesSpell => ({
id: s.spell_id,
name: System.decryptDataWithUserKey(s.name, userEncryptionKey),
lastUpdate: s.last_update
}));
// Map spell tags
const spellTags: SyncedSeriesSpellTag[] = allSpellTags
.filter((t: SyncedSeriesSpellTagResult): boolean => t.series_id === seriesId)
.map((t: SyncedSeriesSpellTagResult): SyncedSeriesSpellTag => ({
id: t.tag_id,
name: System.decryptDataWithUserKey(t.name, userEncryptionKey),
lastUpdate: t.last_update
}));
return {
id: seriesId,
name: System.decryptDataWithUserKey(series.name, userEncryptionKey),
description: series.description
? System.decryptDataWithUserKey(series.description, userEncryptionKey)
: null,
lastUpdate: series.last_update,
books,
characters,
worlds,
locations,
spells,
spellTags
};
});
}
}

View File

@@ -261,9 +261,9 @@ export default class Upload {
const spells: BookSpellsTable[] = encryptedSpells.map((spell: BookSpellsTable): BookSpellsTable => ({
...spell,
name: System.decryptDataWithUserKey(spell.name, userEncryptionKey),
description: System.decryptDataWithUserKey(spell.description, userEncryptionKey),
appearance: System.decryptDataWithUserKey(spell.appearance, userEncryptionKey),
tags: System.decryptDataWithUserKey(spell.tags, userEncryptionKey),
description: spell.description ? System.decryptDataWithUserKey(spell.description, userEncryptionKey) : null,
appearance: spell.appearance ? System.decryptDataWithUserKey(spell.appearance, userEncryptionKey) : null,
tags: spell.tags ? System.decryptDataWithUserKey(spell.tags, userEncryptionKey) : null,
power_level: spell.power_level ? System.decryptDataWithUserKey(spell.power_level, userEncryptionKey) : null,
components: spell.components ? System.decryptDataWithUserKey(spell.components, userEncryptionKey) : null,
limitations: spell.limitations ? System.decryptDataWithUserKey(spell.limitations, userEncryptionKey) : null,

View File

@@ -2,6 +2,7 @@ import { getUserEncryptionKey } from "../keyManager.js";
import System from "../System.js";
import WorldRepository, { WorldElementValue, WorldQuery } from "../repositories/world.repository.js";
import BookRepo, {BookToolsTable} from "../repositories/book.repository.js";
import RemovedItem from "./RemovedItem.js";
export interface SyncedWorld {
id: string;
@@ -269,12 +270,18 @@ export default class World {
/**
* Removes an element from a world.
* @param userId - The unique identifier of the user
* @param bookId - The unique identifier of the book
* @param elementId - The unique identifier of the element to remove
* @param deletedAt - The timestamp of deletion
* @param lang - The language for error messages ('fr' or 'en'), defaults to 'fr'
* @returns True if the deletion was successful, false otherwise
*/
public static removeElementFromWorld(userId: string, elementId: string, lang: 'fr' | 'en' = 'fr'): boolean {
return WorldRepository.deleteElement(userId, elementId, lang);
public static removeElementFromWorld(userId: string, bookId: string, elementId: string, deletedAt: number = System.timeStampInSeconds(), lang: 'fr' | 'en' = 'fr'): boolean {
const deleted: boolean = WorldRepository.deleteElement(userId, elementId, lang);
if (deleted) {
RemovedItem.deleteTracker(userId, bookId, 'book_world_elements', elementId, deletedAt, lang);
}
return deleted;
}
}

View File

@@ -123,7 +123,7 @@ export default class BookRepo {
let book: BookQuery;
try {
const db: Database = System.getDb();
const query: string = 'SELECT book_id, author_id, title, summary, sub_title, cover_image, desired_release_date, desired_word_count, words_count FROM erit_books WHERE book_id=? AND author_id=?';
const query: string = 'SELECT book_id, author_id, title, summary, sub_title, cover_image, desired_release_date, desired_word_count, words_count, serie_id FROM erit_books WHERE book_id=? AND author_id=?';
const params: SQLiteValue[] = [bookId, userId];
book = db.get(query, params) as BookQuery;
} catch (error: unknown) {
@@ -456,4 +456,40 @@ export default class BookRepo {
throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred.");
}
}
static isBookExist(userId: string, bookId: string, lang: 'fr' | 'en'): boolean {
try {
const db: Database = System.getDb();
const query: string = 'SELECT 1 FROM erit_books WHERE author_id = ? AND book_id = ? LIMIT 1';
const params: SQLiteValue[] = [userId, bookId];
const result = db.get(query, params);
return result !== undefined && result !== null;
} catch (error: unknown) {
if (error instanceof Error) {
console.error(`DB Error: ${error.message}`);
}
return false;
}
}
/**
* Retrieves the series_id for a book from series_books table.
* @param bookId - The book identifier
* @param lang - The language for error messages
* @returns The series_id or null if book is not in a series
*/
static fetchBookSeriesId(bookId: string, lang: 'fr' | 'en'): string | null {
try {
const db: Database = System.getDb();
const query: string = 'SELECT series_id FROM series_books WHERE book_id = ? LIMIT 1';
const params: SQLiteValue[] = [bookId];
const result = db.get(query, params) as { series_id: string } | undefined;
return result?.series_id || null;
} catch (error: unknown) {
if (error instanceof Error) {
console.error(`DB Error: ${error.message}`);
}
return null;
}
}
}

View File

@@ -0,0 +1,158 @@
import { Database, RunResult, SQLiteValue } from 'node-sqlite3-wasm';
import System from '../System.js';
export interface RemovedItemRecord extends Record<string, SQLiteValue> {
removal_id: string;
table_name: string;
entity_id: string;
book_id: string | null;
user_id: string;
deleted_at: number;
}
/**
* Repository for tracking deleted items for sync purposes.
*/
export default class RemovedItemsRepository {
/**
* Inserts a removal record into the database.
* @param removalId - The unique ID for this removal record.
* @param tableName - The name of the table from which the item is deleted.
* @param entityId - The UUID of the deleted entity.
* @param bookId - Book ID (null for series items).
* @param userId - The user ID who owns the item.
* @param deletedAt - Timestamp of deletion.
* @param lang - The language for error messages ('fr' or 'en').
* @returns True if inserted successfully.
*/
public static insert(
removalId: string,
tableName: string,
entityId: string,
bookId: string | null,
userId: string,
deletedAt: number,
lang: 'fr' | 'en'
): boolean {
try {
const db: Database = System.getDb();
const query: string = `
INSERT INTO removed_items (removal_id, table_name, entity_id, book_id, user_id, deleted_at)
VALUES (?, ?, ?, ?, ?, ?)
ON CONFLICT(table_name, entity_id) DO UPDATE SET deleted_at = excluded.deleted_at
`;
const params: SQLiteValue[] = [removalId, tableName, entityId, bookId, userId, deletedAt];
const result: RunResult = db.run(query, params);
return result.changes > 0;
} catch (error: unknown) {
if (error instanceof Error) {
console.error(`DB Error: ${error.message}`);
throw new Error(lang === 'fr' ? `Impossible d'enregistrer la suppression.` : `Unable to record deletion.`);
} else {
console.error("An unknown error occurred.");
throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred.");
}
}
}
/**
* Retrieves deletions since a specific timestamp.
* Used to get deletions that occurred since last sync.
* @param userId - The user ID.
* @param since - Timestamp to get deletions after.
* @param lang - The language for error messages ('fr' or 'en').
* @returns Array of removed item records.
*/
public static getDeletionsSince(userId: string, since: number, lang: 'fr' | 'en'): RemovedItemRecord[] {
try {
const db: Database = System.getDb();
const query: string = 'SELECT * FROM removed_items WHERE user_id = ? AND deleted_at > ?';
const params: SQLiteValue[] = [userId, since];
const records: RemovedItemRecord[] = db.all(query, params) as RemovedItemRecord[];
return records;
} catch (error: unknown) {
if (error instanceof Error) {
console.error(`DB Error: ${error.message}`);
throw new Error(lang === 'fr' ? `Impossible de récupérer les suppressions.` : `Unable to retrieve deletions.`);
} else {
console.error("An unknown error occurred.");
throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred.");
}
}
}
/**
* Checks if an entity was previously deleted.
* @param tableName - The table name.
* @param entityId - The entity ID.
* @param lang - The language for error messages ('fr' or 'en').
* @returns True if the entity was deleted locally.
*/
public static wasDeleted(tableName: string, entityId: string, lang: 'fr' | 'en'): boolean {
try {
const db: Database = System.getDb();
const query: string = 'SELECT 1 FROM removed_items WHERE table_name = ? AND entity_id = ? LIMIT 1';
const params: SQLiteValue[] = [tableName, entityId];
const result = db.get(query, params);
return result !== null;
} catch (error: unknown) {
if (error instanceof Error) {
console.error(`DB Error: ${error.message}`);
throw new Error(lang === 'fr' ? `Impossible de vérifier si l'élément a été supprimé.` : `Unable to check if item was deleted.`);
} else {
console.error("An unknown error occurred.");
throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred.");
}
}
}
/**
* Retrieves all tracked deletions for a specific book.
* @param userId - The user ID.
* @param bookId - The book ID.
* @param lang - The language for error messages ('fr' or 'en').
* @returns Array of removed item records for that book.
*/
public static getDeletionsForBook(userId: string, bookId: string, lang: 'fr' | 'en'): RemovedItemRecord[] {
try {
const db: Database = System.getDb();
const query: string = 'SELECT * FROM removed_items WHERE user_id = ? AND book_id = ?';
const params: SQLiteValue[] = [userId, bookId];
const records: RemovedItemRecord[] = db.all(query, params) as RemovedItemRecord[];
return records;
} catch (error: unknown) {
if (error instanceof Error) {
console.error(`DB Error: ${error.message}`);
throw new Error(lang === 'fr' ? `Impossible de récupérer les suppressions pour ce livre.` : `Unable to retrieve deletions for this book.`);
} else {
console.error("An unknown error occurred.");
throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred.");
}
}
}
/**
* Clears all deletion records for a user.
* WARNING: Only use this when wiping user data completely.
* @param userId - The user ID.
* @param lang - The language for error messages ('fr' or 'en').
* @returns True if cleared successfully.
*/
public static clearAllForUser(userId: string, lang: 'fr' | 'en'): boolean {
try {
const db: Database = System.getDb();
const query: string = 'DELETE FROM removed_items WHERE user_id = ?';
const params: SQLiteValue[] = [userId];
const result: RunResult = db.run(query, params);
return result.changes > 0;
} catch (error: unknown) {
if (error instanceof Error) {
console.error(`DB Error: ${error.message}`);
throw new Error(lang === 'fr' ? `Impossible de supprimer les enregistrements de suppression.` : `Unable to clear deletion records.`);
} else {
console.error("An unknown error occurred.");
throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred.");
}
}
}
}

View File

@@ -469,4 +469,73 @@ export default class SeriesCharacterRepo {
}
}
}
static fetchCharactersTableForSync(seriesId: string, lang: 'fr' | 'en' = 'fr'): SeriesCharactersTableResult[] {
try {
const db: Database = System.getDb();
const query: string = 'SELECT character_id, series_id, user_id, first_name, last_name, nickname, age, gender, species, nationality, status, title, category, image, role, biography, history, speech_pattern, catchphrase, residence, notes, color, last_update FROM series_characters WHERE series_id = ?';
return db.all(query, [seriesId]) as SeriesCharactersTableResult[];
} catch (error: unknown) {
if (error instanceof Error) {
console.error(`DB Error: ${error.message}`);
throw new Error(lang === 'fr' ? `Impossible de récupérer les personnages pour sync.` : `Unable to retrieve characters for sync.`);
} else {
throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred.");
}
}
}
static fetchCharacterAttributesTableForSync(seriesId: string, lang: 'fr' | 'en' = 'fr'): SeriesCharacterAttributesTableResult[] {
try {
const db: Database = System.getDb();
const query: string = 'SELECT sca.attr_id, sca.character_id, sca.user_id, sca.attribute_name, sca.attribute_value, sca.last_update FROM series_characters_attributes sca INNER JOIN series_characters sc ON sca.character_id = sc.character_id WHERE sc.series_id = ?';
return db.all(query, [seriesId]) as SeriesCharacterAttributesTableResult[];
} catch (error: unknown) {
if (error instanceof Error) {
console.error(`DB Error: ${error.message}`);
throw new Error(lang === 'fr' ? `Impossible de récupérer les attributs pour sync.` : `Unable to retrieve attributes for sync.`);
} else {
throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred.");
}
}
}
/**
* Fetches all characters for a series (alias for fetchCharacters that returns full table result).
*/
static fetchSeriesCharacters(userId: string, seriesId: string, lang: 'fr' | 'en' = 'fr'): SeriesCharactersTableResult[] {
return this.fetchSeriesCharactersTable(userId, seriesId, lang);
}
/**
* Fetches all character attributes for a series by series ID.
*/
static fetchSeriesCharacterAttributesBySeriesId(userId: string, seriesId: string, lang: 'fr' | 'en' = 'fr'): SeriesCharacterAttributesTableResult[] {
try {
const db: Database = System.getDb();
const query: string = 'SELECT sca.attr_id, sca.character_id, sca.user_id, sca.attribute_name, sca.attribute_value, sca.last_update FROM series_characters_attributes sca INNER JOIN series_characters sc ON sca.character_id = sc.character_id WHERE sc.series_id = ? AND sc.user_id = ?';
return db.all(query, [seriesId, userId]) as SeriesCharacterAttributesTableResult[];
} catch (error: unknown) {
if (error instanceof Error) {
console.error(`DB Error: ${error.message}`);
throw new Error(lang === 'fr' ? `Impossible de récupérer les attributs par série.` : `Unable to retrieve attributes by series.`);
} else {
throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred.");
}
}
}
/**
* Checks if a series character exists (alias for isCharacterExist).
*/
static seriesCharacterExists(userId: string, characterId: string, lang: 'fr' | 'en' = 'fr'): boolean {
return this.isCharacterExist(userId, characterId, lang);
}
/**
* Checks if a series character attribute exists (alias for isAttributeExist).
*/
static seriesCharacterAttributeExists(userId: string, attrId: string, lang: 'fr' | 'en' = 'fr'): boolean {
return this.isAttributeExist(userId, attrId, lang);
}
}

View File

@@ -620,4 +620,194 @@ export default class SeriesLocationRepo {
}
}
}
public static fetchLocationsTableForSync(seriesId: string, lang: 'fr' | 'en' = 'fr'): SeriesLocationsTableResult[] {
try {
const db: Database = System.getDb();
const query: string = 'SELECT loc_id, series_id, user_id, loc_name, loc_original_name, last_update FROM series_locations WHERE series_id = ?';
return db.all(query, [seriesId]) as SeriesLocationsTableResult[];
} catch (error: unknown) {
if (error instanceof Error) {
console.error(`DB Error: ${error.message}`);
throw new Error(lang === 'fr' ? `Impossible de récupérer les lieux pour sync.` : `Unable to retrieve locations for sync.`);
} else {
throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred.");
}
}
}
public static fetchLocationElementsTableForSync(seriesId: string, lang: 'fr' | 'en' = 'fr'): SeriesLocationElementsTableResult[] {
try {
const db: Database = System.getDb();
const query: string = 'SELECT sle.element_id, sle.location_id, sle.user_id, sle.element_name, sle.original_name, sle.element_description, sle.last_update FROM series_location_elements sle INNER JOIN series_locations sl ON sle.location_id = sl.loc_id WHERE sl.series_id = ?';
return db.all(query, [seriesId]) as SeriesLocationElementsTableResult[];
} catch (error: unknown) {
if (error instanceof Error) {
console.error(`DB Error: ${error.message}`);
throw new Error(lang === 'fr' ? `Impossible de récupérer les éléments de lieu pour sync.` : `Unable to retrieve location elements for sync.`);
} else {
throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred.");
}
}
}
public static fetchLocationSubElementsTableForSync(seriesId: string, lang: 'fr' | 'en' = 'fr'): SeriesLocationSubElementsTableResult[] {
try {
const db: Database = System.getDb();
const query: string = 'SELECT slse.sub_element_id, slse.element_id, slse.user_id, slse.sub_elem_name, slse.original_name, slse.sub_elem_description, slse.last_update FROM series_location_sub_elements slse INNER JOIN series_location_elements sle ON slse.element_id = sle.element_id INNER JOIN series_locations sl ON sle.location_id = sl.loc_id WHERE sl.series_id = ?';
return db.all(query, [seriesId]) as SeriesLocationSubElementsTableResult[];
} catch (error: unknown) {
if (error instanceof Error) {
console.error(`DB Error: ${error.message}`);
throw new Error(lang === 'fr' ? `Impossible de récupérer les sous-éléments de lieu pour sync.` : `Unable to retrieve location sub-elements for sync.`);
} else {
throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred.");
}
}
}
/**
* Fetches all locations for a series (alias for fetchSeriesLocationsTable).
*/
public static fetchSeriesLocations(userId: string, seriesId: string, lang: 'fr' | 'en' = 'fr'): SeriesLocationsTableResult[] {
return this.fetchSeriesLocationsTable(userId, seriesId, lang);
}
/**
* Fetches all location elements for a series by series ID.
*/
public static fetchSeriesLocationElementsBySeriesId(userId: string, seriesId: string, lang: 'fr' | 'en' = 'fr'): SeriesLocationElementsTableResult[] {
try {
const db: Database = System.getDb();
const query: string = 'SELECT sle.element_id, sle.location_id, sle.user_id, sle.element_name, sle.original_name, sle.element_description, sle.last_update FROM series_location_elements sle INNER JOIN series_locations sl ON sle.location_id = sl.loc_id WHERE sl.series_id = ? AND sl.user_id = ?';
return db.all(query, [seriesId, userId]) as SeriesLocationElementsTableResult[];
} catch (error: unknown) {
if (error instanceof Error) {
console.error(`DB Error: ${error.message}`);
throw new Error(lang === 'fr' ? `Impossible de récupérer les éléments de lieu par série.` : `Unable to retrieve location elements by series.`);
} else {
throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred.");
}
}
}
/**
* Fetches all location sub-elements for a series by series ID.
*/
public static fetchSeriesLocationSubElementsBySeriesId(userId: string, seriesId: string, lang: 'fr' | 'en' = 'fr'): SeriesLocationSubElementsTableResult[] {
try {
const db: Database = System.getDb();
const query: string = 'SELECT slse.sub_element_id, slse.element_id, slse.user_id, slse.sub_elem_name, slse.original_name, slse.sub_elem_description, slse.last_update FROM series_location_sub_elements slse INNER JOIN series_location_elements sle ON slse.element_id = sle.element_id INNER JOIN series_locations sl ON sle.location_id = sl.loc_id WHERE sl.series_id = ? AND sl.user_id = ?';
return db.all(query, [seriesId, userId]) as SeriesLocationSubElementsTableResult[];
} catch (error: unknown) {
if (error instanceof Error) {
console.error(`DB Error: ${error.message}`);
throw new Error(lang === 'fr' ? `Impossible de récupérer les sous-éléments de lieu par série.` : `Unable to retrieve location sub-elements by series.`);
} else {
throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred.");
}
}
}
/**
* Checks if a series location exists (alias for isLocationExist).
*/
public static seriesLocationExists(userId: string, locationId: string, lang: 'fr' | 'en' = 'fr'): boolean {
return this.isLocationExist(userId, locationId, lang);
}
/**
* Checks if a series location element exists (alias for isLocationElementExist).
*/
public static seriesLocationElementExists(userId: string, elementId: string, lang: 'fr' | 'en' = 'fr'): boolean {
return this.isLocationElementExist(userId, elementId, lang);
}
/**
* Checks if a series location sub-element exists (alias for isLocationSubElementExist).
*/
public static seriesLocationSubElementExists(userId: string, subElementId: string, lang: 'fr' | 'en' = 'fr'): boolean {
return this.isLocationSubElementExist(userId, subElementId, lang);
}
/**
* Inserts a series location for sync (alias with compatible signature).
*/
public static insertSyncSeriesLocation(locationId: string, seriesId: string, userId: string, locName: string, locOriginalName: string, lastUpdate: number, lang: 'fr' | 'en' = 'fr'): boolean {
return this.insertSyncLocation(locationId, seriesId, userId, locName, locOriginalName, lastUpdate, lang);
}
/**
* Updates a series location for sync (without originalName).
*/
public static updateSyncSeriesLocation(locationId: string, userId: string, locName: string, lastUpdate: number, lang: 'fr' | 'en' = 'fr'): boolean {
try {
const db: Database = System.getDb();
const query: string = 'UPDATE series_locations SET loc_name = ?, last_update = ? WHERE loc_id = ? AND user_id = ?';
const params: SQLiteValue[] = [locName, lastUpdate, locationId, userId];
const updateResult: RunResult = db.run(query, params);
return updateResult.changes > 0;
} catch (error: unknown) {
if (error instanceof Error) {
console.error(`DB Error: ${error.message}`);
throw new Error(lang === 'fr' ? `Impossible de mettre à jour le lieu série pour sync.` : `Unable to update series location for sync.`);
} else {
throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred.");
}
}
}
/**
* Inserts a series location element for sync (alias with compatible signature).
*/
public static insertSyncSeriesLocationElement(elementId: string, locationId: string, userId: string, elementName: string, originalName: string, elementDescription: string | null, lastUpdate: number, lang: 'fr' | 'en' = 'fr'): boolean {
return this.insertSyncLocationElement(elementId, locationId, userId, elementName, originalName, elementDescription, lastUpdate, lang);
}
/**
* Updates a series location element for sync (without originalName).
*/
public static updateSyncSeriesLocationElement(elementId: string, userId: string, elementName: string, elementDescription: string | null, lastUpdate: number, lang: 'fr' | 'en' = 'fr'): boolean {
try {
const db: Database = System.getDb();
const query: string = 'UPDATE series_location_elements SET element_name = ?, element_description = ?, last_update = ? WHERE element_id = ? AND user_id = ?';
const params: SQLiteValue[] = [elementName, elementDescription, lastUpdate, elementId, userId];
const updateResult: RunResult = db.run(query, params);
return updateResult.changes > 0;
} catch (error: unknown) {
if (error instanceof Error) {
console.error(`DB Error: ${error.message}`);
throw new Error(lang === 'fr' ? `Impossible de mettre à jour l'élément de lieu série pour sync.` : `Unable to update series location element for sync.`);
} else {
throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred.");
}
}
}
/**
* Inserts a series location sub-element for sync (alias with compatible signature).
*/
public static insertSyncSeriesLocationSubElement(subElementId: string, elementId: string, userId: string, subElemName: string, originalName: string, subElemDescription: string | null, lastUpdate: number, lang: 'fr' | 'en' = 'fr'): boolean {
return this.insertSyncLocationSubElement(subElementId, elementId, userId, subElemName, originalName, subElemDescription, lastUpdate, lang);
}
/**
* Updates a series location sub-element for sync (without originalName).
*/
public static updateSyncSeriesLocationSubElement(subElementId: string, userId: string, subElemName: string, subElemDescription: string | null, lastUpdate: number, lang: 'fr' | 'en' = 'fr'): boolean {
try {
const db: Database = System.getDb();
const query: string = 'UPDATE series_location_sub_elements SET sub_elem_name = ?, sub_elem_description = ?, last_update = ? WHERE sub_element_id = ? AND user_id = ?';
const params: SQLiteValue[] = [subElemName, subElemDescription, lastUpdate, subElementId, userId];
const updateResult: RunResult = db.run(query, params);
return updateResult.changes > 0;
} catch (error: unknown) {
if (error instanceof Error) {
console.error(`DB Error: ${error.message}`);
throw new Error(lang === 'fr' ? `Impossible de mettre à jour le sous-élément série pour sync.` : `Unable to update series sub-element for sync.`);
} else {
throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred.");
}
}
}
}

View File

@@ -26,9 +26,9 @@ export interface SeriesSpellsTableResult extends Record<string, SQLiteValue> {
user_id: string;
name: string;
name_hash: string;
description: string;
appearance: string;
tags: string;
description: string | null;
appearance: string | null;
tags: string | null;
power_level: string | null;
components: string | null;
limitations: string | null;
@@ -496,4 +496,152 @@ export default class SeriesSpellRepo {
}
}
}
static fetchSpellsTableForSync(seriesId: string, lang: 'fr' | 'en' = 'fr'): SeriesSpellsTableResult[] {
try {
const db: Database = System.getDb();
const query: string = 'SELECT spell_id, series_id, user_id, name, name_hash, description, appearance, tags, power_level, components, limitations, notes, last_update FROM series_spells WHERE series_id = ?';
return db.all(query, [seriesId]) as SeriesSpellsTableResult[];
} catch (error: unknown) {
if (error instanceof Error) {
console.error(`DB Error: ${error.message}`);
throw new Error(lang === 'fr' ? `Impossible de récupérer les sorts pour sync.` : `Unable to retrieve spells for sync.`);
} else {
throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred.");
}
}
}
static fetchSpellTagsTableForSync(seriesId: string, lang: 'fr' | 'en' = 'fr'): SeriesSpellTagsTableResult[] {
try {
const db: Database = System.getDb();
const query: string = 'SELECT tag_id, series_id, user_id, name, hashed_name, color, last_update FROM series_spell_tags WHERE series_id = ?';
return db.all(query, [seriesId]) as SeriesSpellTagsTableResult[];
} catch (error: unknown) {
if (error instanceof Error) {
console.error(`DB Error: ${error.message}`);
throw new Error(lang === 'fr' ? `Impossible de récupérer les tags de sort pour sync.` : `Unable to retrieve spell tags for sync.`);
} else {
throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred.");
}
}
}
/**
* Fetches all spells for a series (alias for fetchSeriesSpellsTable).
*/
static fetchSeriesSpells(userId: string, seriesId: string, lang: 'fr' | 'en' = 'fr'): SeriesSpellsTableResult[] {
return this.fetchSeriesSpellsTable(userId, seriesId, lang);
}
/**
* Fetches all spell tags for a series (alias for fetchSeriesSpellTagsTable).
*/
static fetchSeriesSpellTags(userId: string, seriesId: string, lang: 'fr' | 'en' = 'fr'): SeriesSpellTagsTableResult[] {
return this.fetchSeriesSpellTagsTable(userId, seriesId, lang);
}
/**
* Checks if a series spell exists (alias for isSpellExist).
*/
static seriesSpellExists(userId: string, spellId: string, lang: 'fr' | 'en' = 'fr'): boolean {
return this.isSpellExist(userId, spellId, lang);
}
/**
* Checks if a series spell tag exists (alias for isSpellTagExist).
*/
static seriesSpellTagExists(userId: string, tagId: string, lang: 'fr' | 'en' = 'fr'): boolean {
return this.isSpellTagExist(userId, tagId, lang);
}
/**
* Fetches a complete spell by ID for sync (array format).
*/
static fetchCompleteSpellById(spellId: string, lang: 'fr' | 'en' = 'fr'): SeriesSpellsTableResult[] {
try {
const db: Database = System.getDb();
const query: string = 'SELECT spell_id, series_id, user_id, name, name_hash, description, appearance, tags, power_level, components, limitations, notes, last_update FROM series_spells WHERE spell_id = ?';
return db.all(query, [spellId]) as SeriesSpellsTableResult[];
} catch (error: unknown) {
if (error instanceof Error) {
console.error(`DB Error: ${error.message}`);
throw new Error(lang === 'fr' ? `Impossible de récupérer le sort complet.` : `Unable to retrieve complete spell.`);
} else {
throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred.");
}
}
}
/**
* Fetches a complete spell tag by ID for sync (array format).
*/
static fetchCompleteSpellTagById(tagId: string, lang: 'fr' | 'en' = 'fr'): SeriesSpellTagsTableResult[] {
try {
const db: Database = System.getDb();
const query: string = 'SELECT tag_id, series_id, user_id, name, hashed_name, color, last_update FROM series_spell_tags WHERE tag_id = ?';
return db.all(query, [tagId]) as SeriesSpellTagsTableResult[];
} catch (error: unknown) {
if (error instanceof Error) {
console.error(`DB Error: ${error.message}`);
throw new Error(lang === 'fr' ? `Impossible de récupérer le tag complet.` : `Unable to retrieve complete tag.`);
} else {
throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred.");
}
}
}
/**
* Inserts a series spell for sync (alias with compatible signature).
*/
static insertSyncSeriesSpell(spellId: string, seriesId: string, userId: string, name: string, nameHash: string, description: string, appearance: string, tags: string, powerLevel: string | null, components: string | null, limitations: string | null, notes: string | null, lastUpdate: number, lang: 'fr' | 'en' = 'fr'): boolean {
return this.insertSyncSpell(spellId, seriesId, userId, name, nameHash, description, appearance, tags, powerLevel, components, limitations, notes, lastUpdate, lang);
}
/**
* Updates a series spell for sync (simplified signature).
*/
static updateSyncSeriesSpell(spellId: string, userId: string, name: string, description: string, appearance: string, tags: string, powerLevel: string | null, components: string | null, limitations: string | null, notes: string | null, lastUpdate: number, lang: 'fr' | 'en' = 'fr'): boolean {
try {
const db: Database = System.getDb();
const query: string = 'UPDATE series_spells SET name = ?, description = ?, appearance = ?, tags = ?, power_level = ?, components = ?, limitations = ?, notes = ?, last_update = ? WHERE spell_id = ? AND user_id = ?';
const params: SQLiteValue[] = [name, description, appearance, tags, powerLevel, components, limitations, notes, lastUpdate, spellId, userId];
const updateResult: RunResult = db.run(query, params);
return updateResult.changes > 0;
} catch (error: unknown) {
if (error instanceof Error) {
console.error(`DB Error: ${error.message}`);
throw new Error(lang === 'fr' ? `Impossible de mettre à jour le sort série pour sync.` : `Unable to update series spell for sync.`);
} else {
throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred.");
}
}
}
/**
* Inserts a series spell tag for sync (alias with compatible signature).
*/
static insertSyncSeriesSpellTag(tagId: string, seriesId: string, userId: string, name: string, hashedName: string, color: string | null, lastUpdate: number, lang: 'fr' | 'en' = 'fr'): boolean {
return this.insertSyncSpellTag(tagId, seriesId, userId, name, hashedName, color, lastUpdate, lang);
}
/**
* Updates a series spell tag for sync (simplified signature).
*/
static updateSyncSeriesSpellTag(tagId: string, userId: string, name: string, color: string | null, lastUpdate: number, lang: 'fr' | 'en' = 'fr'): boolean {
try {
const db: Database = System.getDb();
const query: string = 'UPDATE series_spell_tags SET name = ?, color = ?, last_update = ? WHERE tag_id = ? AND user_id = ?';
const params: SQLiteValue[] = [name, color, lastUpdate, tagId, userId];
const updateResult: RunResult = db.run(query, params);
return updateResult.changes > 0;
} catch (error: unknown) {
if (error instanceof Error) {
console.error(`DB Error: ${error.message}`);
throw new Error(lang === 'fr' ? `Impossible de mettre à jour le tag série pour sync.` : `Unable to update series tag for sync.`);
} else {
throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred.");
}
}
}
}

View File

@@ -429,4 +429,127 @@ export default class SeriesWorldRepo {
}
}
}
public static fetchWorldsTableForSync(seriesId: string, lang: 'fr' | 'en' = 'fr'): SeriesWorldsTableResult[] {
try {
const db: Database = System.getDb();
const query: string = 'SELECT world_id, series_id, user_id, name, hashed_name, history, politics, economy, religion, languages, last_update FROM series_worlds WHERE series_id = ?';
return db.all(query, [seriesId]) as SeriesWorldsTableResult[];
} catch (error: unknown) {
if (error instanceof Error) {
console.error(`DB Error: ${error.message}`);
throw new Error(lang === 'fr' ? `Impossible de récupérer les mondes pour sync.` : `Unable to retrieve worlds for sync.`);
} else {
throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred.");
}
}
}
public static fetchWorldElementsTableForSync(seriesId: string, lang: 'fr' | 'en' = 'fr'): SeriesWorldElementsTableResult[] {
try {
const db: Database = System.getDb();
const query: string = 'SELECT swe.element_id, swe.world_id, swe.user_id, swe.element_type, swe.name, swe.original_name, swe.description, swe.last_update FROM series_world_elements swe INNER JOIN series_worlds sw ON swe.world_id = sw.world_id WHERE sw.series_id = ?';
return db.all(query, [seriesId]) as SeriesWorldElementsTableResult[];
} catch (error: unknown) {
if (error instanceof Error) {
console.error(`DB Error: ${error.message}`);
throw new Error(lang === 'fr' ? `Impossible de récupérer les éléments de monde pour sync.` : `Unable to retrieve world elements for sync.`);
} else {
throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred.");
}
}
}
/**
* Fetches all worlds for a series (alias for fetchSeriesWorldsTable).
*/
public static fetchSeriesWorlds(userId: string, seriesId: string, lang: 'fr' | 'en' = 'fr'): SeriesWorldsTableResult[] {
return this.fetchSeriesWorldsTable(userId, seriesId, lang);
}
/**
* Fetches all world elements for a series by series ID.
*/
public static fetchSeriesWorldElementsBySeriesId(userId: string, seriesId: string, lang: 'fr' | 'en' = 'fr'): SeriesWorldElementsTableResult[] {
try {
const db: Database = System.getDb();
const query: string = 'SELECT swe.element_id, swe.world_id, swe.user_id, swe.element_type, swe.name, swe.original_name, swe.description, swe.last_update FROM series_world_elements swe INNER JOIN series_worlds sw ON swe.world_id = sw.world_id WHERE sw.series_id = ? AND sw.user_id = ?';
return db.all(query, [seriesId, userId]) as SeriesWorldElementsTableResult[];
} catch (error: unknown) {
if (error instanceof Error) {
console.error(`DB Error: ${error.message}`);
throw new Error(lang === 'fr' ? `Impossible de récupérer les éléments de monde par série.` : `Unable to retrieve world elements by series.`);
} else {
throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred.");
}
}
}
/**
* Checks if a series world exists (alias for isWorldExist).
*/
public static seriesWorldExists(userId: string, worldId: string, lang: 'fr' | 'en' = 'fr'): boolean {
return this.isWorldExist(userId, worldId, lang);
}
/**
* Checks if a series world element exists (alias for isWorldElementExist).
*/
public static seriesWorldElementExists(userId: string, elementId: string, lang: 'fr' | 'en' = 'fr'): boolean {
return this.isWorldElementExist(userId, elementId, lang);
}
/**
* Inserts a series world for sync (alias with compatible signature).
*/
public static insertSyncSeriesWorld(worldId: string, seriesId: string, userId: string, name: string, hashedName: string, history: string | null, politics: string | null, economy: string | null, religion: string | null, languages: string | null, lastUpdate: number, lang: 'fr' | 'en' = 'fr'): boolean {
return this.insertSyncWorld(worldId, seriesId, userId, name, hashedName, history, politics, economy, religion, languages, lastUpdate, lang);
}
/**
* Updates a series world for sync (without hashedName).
*/
public static updateSyncSeriesWorld(worldId: string, userId: string, name: string, history: string | null, politics: string | null, economy: string | null, religion: string | null, languages: string | null, lastUpdate: number, lang: 'fr' | 'en' = 'fr'): boolean {
try {
const db: Database = System.getDb();
const query: string = 'UPDATE series_worlds SET name = ?, history = ?, politics = ?, economy = ?, religion = ?, languages = ?, last_update = ? WHERE world_id = ? AND user_id = ?';
const params: SQLiteValue[] = [name, history, politics, economy, religion, languages, lastUpdate, worldId, userId];
const updateResult: RunResult = db.run(query, params);
return updateResult.changes > 0;
} catch (error: unknown) {
if (error instanceof Error) {
console.error(`DB Error: ${error.message}`);
throw new Error(lang === 'fr' ? `Impossible de mettre à jour le monde série pour sync.` : `Unable to update series world for sync.`);
} else {
throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred.");
}
}
}
/**
* Inserts a series world element for sync (alias with compatible signature).
*/
public static insertSyncSeriesWorldElement(elementId: string, worldId: string, userId: string, elementType: number, name: string, originalName: string, description: string | null, lastUpdate: number, lang: 'fr' | 'en' = 'fr'): boolean {
return this.insertSyncWorldElement(elementId, worldId, userId, elementType, name, originalName, description, lastUpdate, lang);
}
/**
* Updates a series world element for sync (without elementType and originalName).
*/
public static updateSyncSeriesWorldElement(elementId: string, userId: string, name: string, description: string | null, lastUpdate: number, lang: 'fr' | 'en' = 'fr'): boolean {
try {
const db: Database = System.getDb();
const query: string = 'UPDATE series_world_elements SET name = ?, description = ?, last_update = ? WHERE element_id = ? AND user_id = ?';
const params: SQLiteValue[] = [name, description, lastUpdate, elementId, userId];
const updateResult: RunResult = db.run(query, params);
return updateResult.changes > 0;
} catch (error: unknown) {
if (error instanceof Error) {
console.error(`DB Error: ${error.message}`);
throw new Error(lang === 'fr' ? `Impossible de mettre à jour l'élément de monde série pour sync.` : `Unable to update series world element for sync.`);
} else {
throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred.");
}
}
}
}

View File

@@ -539,4 +539,15 @@ export default class SeriesRepo {
}
}
}
/**
* Checks if a series exists for a user (alias for isSeriesExist).
* @param userId - The unique identifier of the user
* @param seriesId - The unique identifier of the series
* @param lang - The language for error messages ('fr' or 'en')
* @returns True if the series exists
*/
public static seriesExists(userId: string, seriesId: string, lang: 'fr' | 'en' = 'fr'): boolean {
return this.isSeriesExist(userId, seriesId, lang);
}
}

View File

@@ -5,9 +5,9 @@ export interface SpellResult extends Record<string, SQLiteValue> {
spell_id: string;
book_id: string;
name: string;
description: string;
appearance: string;
tags: string;
description: string | null;
appearance: string | null;
tags: string | null;
power_level: string | null;
components: string | null;
limitations: string | null;
@@ -21,9 +21,9 @@ export interface BookSpellsTable extends Record<string, SQLiteValue> {
user_id: string;
name: string;
name_hash: string;
description: string;
appearance: string;
tags: string;
description: string | null;
appearance: string | null;
tags: string | null;
power_level: string | null;
components: string | null;
limitations: string | null;
@@ -301,7 +301,7 @@ export default class SpellRepo {
* @param lang - The language for error messages ('fr' or 'en')
* @returns True if the insertion was successful
*/
static insertSyncSpell(spellId: string, bookId: string, userId: string, name: string, nameHash: string, description: string, appearance: string, tags: string, powerLevel: string | null, components: string | null, limitations: string | null, notes: string | null, lastUpdate: number, lang: 'fr' | 'en' = 'fr'): boolean {
static insertSyncSpell(spellId: string, bookId: string, userId: string, name: string, nameHash: string, description: string | null, appearance: string | null, tags: string | null, powerLevel: string | null, components: string | null, limitations: string | null, notes: string | null, lastUpdate: number, lang: 'fr' | 'en' = 'fr'): boolean {
try {
const db: Database = System.getDb();
const query: string = 'INSERT OR REPLACE INTO book_spells (spell_id, book_id, user_id, name, name_hash, description, appearance, tags, power_level, components, limitations, notes, last_update) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?)';
@@ -359,7 +359,7 @@ export default class SpellRepo {
* @param lang - The language for error messages ('fr' or 'en')
* @returns True if the update was successful
*/
static updateSyncSpell(userId: string, spellId: string, name: string, nameHash: string, description: string, appearance: string, tags: string, powerLevel: string | null, components: string | null, limitations: string | null, notes: string | null, lastUpdate: number, lang: 'fr' | 'en' = 'fr'): boolean {
static updateSyncSpell(userId: string, spellId: string, name: string, nameHash: string, description: string | null, appearance: string | null, tags: string | null, powerLevel: string | null, components: string | null, limitations: string | null, notes: string | null, lastUpdate: number, lang: 'fr' | 'en' = 'fr'): boolean {
try {
const db: Database = System.getDb();
const query: string = 'UPDATE book_spells SET name=?, name_hash=?, description=?, appearance=?, tags=?, power_level=?, components=?, limitations=?, notes=?, last_update=? WHERE spell_id=? AND user_id=?';

View File

@@ -19,7 +19,205 @@ const schemaVersion = 3;
* DEV ONLY - S'exécute à chaque refresh, pas besoin de version
* Mets ta query, test, efface après
*/
const devQueries: string[] = [];
const devQueries: string[] = [
// V3 Migration: Series tables and series_*_id columns
// Book Series
`CREATE TABLE IF NOT EXISTS book_series (
series_id TEXT PRIMARY KEY,
user_id TEXT NOT NULL,
name TEXT NOT NULL,
hashed_name TEXT NOT NULL,
description TEXT,
cover_image TEXT,
last_update INTEGER DEFAULT 0,
FOREIGN KEY (user_id) REFERENCES erit_users(user_id) ON DELETE CASCADE
)`,
`CREATE INDEX IF NOT EXISTS idx_book_series_user ON book_series(user_id)`,
// Series Books
`CREATE TABLE IF NOT EXISTS series_books (
series_id TEXT NOT NULL,
book_id TEXT NOT NULL,
book_order INTEGER NOT NULL DEFAULT 1,
last_update INTEGER DEFAULT 0,
PRIMARY KEY (series_id, book_id),
FOREIGN KEY (series_id) REFERENCES book_series(series_id) ON DELETE CASCADE,
FOREIGN KEY (book_id) REFERENCES erit_books(book_id) ON DELETE CASCADE
)`,
`CREATE INDEX IF NOT EXISTS idx_series_books_book ON series_books(book_id)`,
// Series Characters
`CREATE TABLE IF NOT EXISTS series_characters (
character_id TEXT PRIMARY KEY,
series_id TEXT NOT NULL,
user_id TEXT NOT NULL,
first_name TEXT NOT NULL,
last_name TEXT,
nickname TEXT,
age TEXT,
gender TEXT,
species TEXT,
nationality TEXT,
status TEXT,
category TEXT NOT NULL,
title TEXT,
image TEXT,
role TEXT,
biography TEXT,
history TEXT,
speech_pattern TEXT,
catchphrase TEXT,
residence TEXT,
notes TEXT,
color TEXT,
last_update INTEGER DEFAULT 0,
FOREIGN KEY (series_id) REFERENCES book_series(series_id) ON DELETE CASCADE
)`,
`CREATE INDEX IF NOT EXISTS idx_series_characters_series ON series_characters(series_id)`,
`CREATE INDEX IF NOT EXISTS idx_series_characters_user ON series_characters(user_id)`,
// Series Characters Attributes
`CREATE TABLE IF NOT EXISTS series_characters_attributes (
attr_id TEXT PRIMARY KEY,
character_id TEXT NOT NULL,
user_id TEXT NOT NULL,
attribute_name TEXT NOT NULL,
attribute_value TEXT NOT NULL,
last_update INTEGER DEFAULT 0,
FOREIGN KEY (character_id) REFERENCES series_characters(character_id) ON DELETE CASCADE
)`,
`CREATE INDEX IF NOT EXISTS idx_series_char_attrs_character ON series_characters_attributes(character_id)`,
`CREATE INDEX IF NOT EXISTS idx_series_char_attrs_user ON series_characters_attributes(user_id)`,
// Series Worlds
`CREATE TABLE IF NOT EXISTS series_worlds (
world_id TEXT PRIMARY KEY,
series_id TEXT NOT NULL,
user_id TEXT NOT NULL,
name TEXT NOT NULL,
hashed_name TEXT NOT NULL,
history TEXT,
politics TEXT,
economy TEXT,
religion TEXT,
languages TEXT,
last_update INTEGER DEFAULT 0,
FOREIGN KEY (series_id) REFERENCES book_series(series_id) ON DELETE CASCADE
)`,
`CREATE INDEX IF NOT EXISTS idx_series_worlds_series ON series_worlds(series_id)`,
`CREATE INDEX IF NOT EXISTS idx_series_worlds_user ON series_worlds(user_id)`,
// Series World Elements
`CREATE TABLE IF NOT EXISTS series_world_elements (
element_id TEXT PRIMARY KEY,
world_id TEXT NOT NULL,
user_id TEXT NOT NULL,
element_type INTEGER NOT NULL,
name TEXT NOT NULL,
original_name TEXT NOT NULL,
description TEXT,
last_update INTEGER DEFAULT 0,
FOREIGN KEY (world_id) REFERENCES series_worlds(world_id) ON DELETE CASCADE
)`,
`CREATE INDEX IF NOT EXISTS idx_series_world_elements_world ON series_world_elements(world_id)`,
`CREATE INDEX IF NOT EXISTS idx_series_world_elements_user ON series_world_elements(user_id)`,
// Series Locations
`CREATE TABLE IF NOT EXISTS series_locations (
loc_id TEXT PRIMARY KEY,
series_id TEXT NOT NULL,
user_id TEXT NOT NULL,
loc_name TEXT NOT NULL,
loc_original_name TEXT NOT NULL,
last_update INTEGER DEFAULT 0,
FOREIGN KEY (series_id) REFERENCES book_series(series_id) ON DELETE CASCADE
)`,
`CREATE INDEX IF NOT EXISTS idx_series_locations_series ON series_locations(series_id)`,
`CREATE INDEX IF NOT EXISTS idx_series_locations_user ON series_locations(user_id)`,
// Series Location Elements
`CREATE TABLE IF NOT EXISTS series_location_elements (
element_id TEXT PRIMARY KEY,
location_id TEXT NOT NULL,
user_id TEXT NOT NULL,
element_name TEXT NOT NULL,
original_name TEXT NOT NULL,
element_description TEXT,
last_update INTEGER DEFAULT 0,
FOREIGN KEY (location_id) REFERENCES series_locations(loc_id) ON DELETE CASCADE
)`,
`CREATE INDEX IF NOT EXISTS idx_series_loc_elements_location ON series_location_elements(location_id)`,
`CREATE INDEX IF NOT EXISTS idx_series_loc_elements_user ON series_location_elements(user_id)`,
// Series Location Sub Elements
`CREATE TABLE IF NOT EXISTS series_location_sub_elements (
sub_element_id TEXT PRIMARY KEY,
element_id TEXT NOT NULL,
user_id TEXT NOT NULL,
sub_elem_name TEXT NOT NULL,
original_name TEXT NOT NULL,
sub_elem_description TEXT,
last_update INTEGER DEFAULT 0,
FOREIGN KEY (element_id) REFERENCES series_location_elements(element_id) ON DELETE CASCADE
)`,
`CREATE INDEX IF NOT EXISTS idx_series_loc_sub_elements_element ON series_location_sub_elements(element_id)`,
`CREATE INDEX IF NOT EXISTS idx_series_loc_sub_elements_user ON series_location_sub_elements(user_id)`,
// Series Spells
`CREATE TABLE IF NOT EXISTS series_spells (
spell_id TEXT PRIMARY KEY,
series_id TEXT NOT NULL,
user_id TEXT NOT NULL,
name TEXT NOT NULL,
name_hash TEXT NOT NULL,
description TEXT,
appearance TEXT,
tags TEXT,
power_level TEXT,
components TEXT,
limitations TEXT,
notes TEXT,
last_update INTEGER DEFAULT 0,
FOREIGN KEY (series_id) REFERENCES book_series(series_id) ON DELETE CASCADE
)`,
`CREATE INDEX IF NOT EXISTS idx_series_spells_series ON series_spells(series_id)`,
`CREATE INDEX IF NOT EXISTS idx_series_spells_user ON series_spells(user_id)`,
// Series Spell Tags
`CREATE TABLE IF NOT EXISTS series_spell_tags (
tag_id TEXT PRIMARY KEY,
series_id TEXT NOT NULL,
user_id TEXT NOT NULL,
name TEXT NOT NULL,
hashed_name TEXT NOT NULL,
color TEXT,
last_update INTEGER DEFAULT 0,
FOREIGN KEY (series_id) REFERENCES book_series(series_id) ON DELETE CASCADE
)`,
`CREATE INDEX IF NOT EXISTS idx_series_spell_tags_series ON series_spell_tags(series_id)`,
`CREATE INDEX IF NOT EXISTS idx_series_spell_tags_user ON series_spell_tags(user_id)`,
// Add series_*_id columns to existing book tables (will fail silently if already exists)
`ALTER TABLE book_characters ADD COLUMN series_character_id TEXT DEFAULT NULL`,
`ALTER TABLE book_world ADD COLUMN series_world_id TEXT DEFAULT NULL`,
`ALTER TABLE book_location ADD COLUMN series_location_id TEXT DEFAULT NULL`,
`ALTER TABLE book_spells ADD COLUMN series_spell_id TEXT DEFAULT NULL`,
// Removed Items (sync deletion tracking)
`CREATE TABLE IF NOT EXISTS removed_items (
removal_id TEXT PRIMARY KEY,
table_name TEXT NOT NULL,
entity_id TEXT NOT NULL,
book_id TEXT,
user_id TEXT NOT NULL,
deleted_at INTEGER NOT NULL
)`,
`CREATE INDEX IF NOT EXISTS idx_removed_items_user ON removed_items(user_id)`,
`CREATE INDEX IF NOT EXISTS idx_removed_items_book ON removed_items(book_id)`,
`CREATE INDEX IF NOT EXISTS idx_removed_items_deleted_at ON removed_items(deleted_at)`,
`CREATE UNIQUE INDEX IF NOT EXISTS idx_removed_items_entity ON removed_items(table_name, entity_id)`,
];
const isDev:boolean = !app.isPackaged;
@@ -123,9 +321,9 @@ function migrateFromOldSystem(db: Database): void {
user_id TEXT NOT NULL,
name TEXT NOT NULL,
name_hash TEXT NOT NULL,
description TEXT NOT NULL,
appearance TEXT NOT NULL,
tags TEXT NOT NULL,
description TEXT,
appearance TEXT,
tags TEXT,
power_level TEXT,
components TEXT,
limitations TEXT,
@@ -172,7 +370,7 @@ function migrateFromOldSystem(db: Database): void {
db.exec(`CREATE INDEX IF NOT EXISTS idx_series_loc_sub_elements_element ON series_location_sub_elements(element_id)`);
db.exec(`CREATE INDEX IF NOT EXISTS idx_series_loc_sub_elements_user ON series_location_sub_elements(user_id)`);
db.exec(`CREATE TABLE IF NOT EXISTS series_spells (spell_id TEXT PRIMARY KEY, series_id TEXT NOT NULL, user_id TEXT NOT NULL, name TEXT NOT NULL, name_hash TEXT NOT NULL, description TEXT NOT NULL, appearance TEXT NOT NULL, tags TEXT, power_level TEXT, components TEXT, limitations TEXT, notes TEXT, last_update INTEGER DEFAULT 0, FOREIGN KEY (series_id) REFERENCES book_series(series_id) ON DELETE CASCADE);`);
db.exec(`CREATE TABLE IF NOT EXISTS series_spells (spell_id TEXT PRIMARY KEY, series_id TEXT NOT NULL, user_id TEXT NOT NULL, name TEXT NOT NULL, name_hash TEXT NOT NULL, description TEXT, appearance TEXT, tags TEXT, power_level TEXT, components TEXT, limitations TEXT, notes TEXT, last_update INTEGER DEFAULT 0, FOREIGN KEY (series_id) REFERENCES book_series(series_id) ON DELETE CASCADE);`);
db.exec(`CREATE INDEX IF NOT EXISTS idx_series_spells_series ON series_spells(series_id)`);
db.exec(`CREATE INDEX IF NOT EXISTS idx_series_spells_user ON series_spells(user_id)`);
@@ -186,6 +384,23 @@ function migrateFromOldSystem(db: Database): void {
addColumn(db, 'book_location', 'series_location_id', 'TEXT DEFAULT NULL');
addColumn(db, 'book_spells', 'series_spell_id', 'TEXT DEFAULT NULL');
// Removed Items (sync deletion tracking)
db.exec(`
CREATE TABLE IF NOT EXISTS removed_items (
removal_id TEXT PRIMARY KEY,
table_name TEXT NOT NULL,
entity_id TEXT NOT NULL,
book_id TEXT,
user_id TEXT NOT NULL,
deleted_at INTEGER NOT NULL,
removed_time INTEGER NOT NULL DEFAULT 0
)
`);
db.exec(`CREATE INDEX IF NOT EXISTS idx_removed_items_user ON removed_items(user_id)`);
db.exec(`CREATE INDEX IF NOT EXISTS idx_removed_items_book ON removed_items(book_id)`);
db.exec(`CREATE INDEX IF NOT EXISTS idx_removed_items_deleted_at ON removed_items(deleted_at)`);
db.exec(`CREATE UNIQUE INDEX IF NOT EXISTS idx_removed_items_entity ON removed_items(table_name, entity_id)`);
// Drop old schema version table
db.exec('DROP TABLE IF EXISTS _schema_version');
}
@@ -255,9 +470,9 @@ export function runMigrations(db: Database): void {
user_id TEXT NOT NULL,
name TEXT NOT NULL,
name_hash TEXT NOT NULL,
description TEXT NOT NULL,
appearance TEXT NOT NULL,
tags TEXT NOT NULL,
description TEXT,
appearance TEXT,
tags TEXT,
power_level TEXT,
components TEXT,
limitations TEXT,
@@ -333,7 +548,7 @@ export function runMigrations(db: Database): void {
db.exec(`CREATE INDEX IF NOT EXISTS idx_series_loc_sub_elements_user ON series_location_sub_elements(user_id)`);
// Series Spells
db.exec(`CREATE TABLE IF NOT EXISTS series_spells (spell_id TEXT PRIMARY KEY, series_id TEXT NOT NULL, user_id TEXT NOT NULL, name TEXT NOT NULL, name_hash TEXT NOT NULL, description TEXT NOT NULL, appearance TEXT NOT NULL, tags TEXT, power_level TEXT, components TEXT, limitations TEXT, notes TEXT, last_update INTEGER DEFAULT 0, FOREIGN KEY (series_id) REFERENCES book_series(series_id) ON DELETE CASCADE);`);
db.exec(`CREATE TABLE IF NOT EXISTS series_spells (spell_id TEXT PRIMARY KEY, series_id TEXT NOT NULL, user_id TEXT NOT NULL, name TEXT NOT NULL, name_hash TEXT NOT NULL, description TEXT, appearance TEXT, tags TEXT, power_level TEXT, components TEXT, limitations TEXT, notes TEXT, last_update INTEGER DEFAULT 0, FOREIGN KEY (series_id) REFERENCES book_series(series_id) ON DELETE CASCADE);`);
db.exec(`CREATE INDEX IF NOT EXISTS idx_series_spells_series ON series_spells(series_id)`);
db.exec(`CREATE INDEX IF NOT EXISTS idx_series_spells_user ON series_spells(user_id)`);
@@ -347,6 +562,23 @@ export function runMigrations(db: Database): void {
addColumn(db, 'book_world', 'series_world_id', 'TEXT DEFAULT NULL');
addColumn(db, 'book_location', 'series_location_id', 'TEXT DEFAULT NULL');
addColumn(db, 'book_spells', 'series_spell_id', 'TEXT DEFAULT NULL');
// Removed Items (sync deletion tracking)
db.exec(`
CREATE TABLE IF NOT EXISTS removed_items (
removal_id TEXT PRIMARY KEY,
table_name TEXT NOT NULL,
entity_id TEXT NOT NULL,
book_id TEXT,
user_id TEXT NOT NULL,
deleted_at INTEGER NOT NULL,
removed_time INTEGER NOT NULL DEFAULT 0
)
`);
db.exec(`CREATE INDEX IF NOT EXISTS idx_removed_items_user ON removed_items(user_id)`);
db.exec(`CREATE INDEX IF NOT EXISTS idx_removed_items_book ON removed_items(book_id)`);
db.exec(`CREATE INDEX IF NOT EXISTS idx_removed_items_deleted_at ON removed_items(deleted_at)`);
db.exec(`CREATE UNIQUE INDEX IF NOT EXISTS idx_removed_items_entity ON removed_items(table_name, entity_id)`);
}
setDbVersion(db, schemaVersion);
@@ -808,9 +1040,9 @@ export function initializeSchema(db: Database): void {
user_id TEXT NOT NULL,
name TEXT NOT NULL,
name_hash TEXT NOT NULL,
description TEXT NOT NULL,
appearance TEXT NOT NULL,
tags TEXT NOT NULL,
description TEXT,
appearance TEXT,
tags TEXT,
power_level TEXT,
components TEXT,
limitations TEXT,
@@ -978,8 +1210,8 @@ export function initializeSchema(db: Database): void {
user_id TEXT NOT NULL,
name TEXT NOT NULL,
name_hash TEXT NOT NULL,
description TEXT NOT NULL,
appearance TEXT NOT NULL,
description TEXT,
appearance TEXT,
tags TEXT,
power_level TEXT,
components TEXT,
@@ -1004,6 +1236,19 @@ export function initializeSchema(db: Database): void {
);
`);
// Removed Items (sync deletion tracking)
db.exec(`
CREATE TABLE IF NOT EXISTS removed_items (
removal_id TEXT PRIMARY KEY,
table_name TEXT NOT NULL,
entity_id TEXT NOT NULL,
book_id TEXT,
user_id TEXT NOT NULL,
deleted_at INTEGER NOT NULL,
removed_time INTEGER NOT NULL DEFAULT 0
)
`);
// Create indexes for better performance
createIndexes(db);
}
@@ -1049,6 +1294,11 @@ function createIndexes(db: Database): void {
db.exec(`CREATE INDEX IF NOT EXISTS idx_series_spells_user ON series_spells(user_id)`);
db.exec(`CREATE INDEX IF NOT EXISTS idx_series_spell_tags_series ON series_spell_tags(series_id)`);
db.exec(`CREATE INDEX IF NOT EXISTS idx_series_spell_tags_user ON series_spell_tags(user_id)`);
// Removed items indexes
db.exec(`CREATE INDEX IF NOT EXISTS idx_removed_items_user ON removed_items(user_id)`);
db.exec(`CREATE INDEX IF NOT EXISTS idx_removed_items_book ON removed_items(book_id)`);
db.exec(`CREATE INDEX IF NOT EXISTS idx_removed_items_deleted_at ON removed_items(deleted_at)`);
db.exec(`CREATE UNIQUE INDEX IF NOT EXISTS idx_removed_items_entity ON removed_items(table_name, entity_id)`);
}
/**

View File

@@ -270,11 +270,12 @@ ipcMain.handle(
interface RemoveIncidentData {
bookId: string;
incidentId: string;
deletedAt: number;
}
ipcMain.handle('db:book:incident:remove', createHandler<RemoveIncidentData, boolean>(
function(userId: string, data: RemoveIncidentData, lang: 'fr' | 'en') {
return Incident.removeIncident(userId, data.bookId, data.incidentId, lang);
return Incident.removeIncident(userId, data.bookId, data.incidentId, data.deletedAt, lang);
}
)
);
@@ -297,12 +298,14 @@ ipcMain.handle('db:book:plot:add', createHandler<AddPlotPointData, string>(
// DELETE /book/plot/remove - Remove plot point
interface RemovePlotData {
plotId: string;
bookId: string;
deletedAt: number;
}
ipcMain.handle(
'db:book:plot:remove',
createHandler<RemovePlotData, boolean>(
function(userId: string, data: RemovePlotData, lang: 'fr' | 'en') {
return PlotPoint.removePlotPoint(userId, data.plotId, lang);
return PlotPoint.removePlotPoint(userId, data.bookId, data.plotId, data.deletedAt, lang);
}
)
);
@@ -319,10 +322,11 @@ ipcMain.handle('db:book:issue:add', createHandler<AddIssueData, string>(
interface RemoveIssueData {
bookId: string;
issueId: string;
deletedAt: number;
}
ipcMain.handle('db:book:issue:remove', createHandler<RemoveIssueData, boolean>(
function(userId: string, data: RemoveIssueData, lang: 'fr' | 'en') {
return Issue.removeIssue(userId, data.issueId, lang);
return Issue.removeIssue(userId, data.bookId, data.issueId, data.deletedAt, lang);
}
)
);
@@ -363,10 +367,12 @@ ipcMain.handle('db:book:world:element:add', createHandler<AddWorldElementData, s
// DELETE /book/world/element/delete - Remove element from world
interface RemoveWorldElementData {
elementId: string;
bookId: string;
deletedAt: number;
}
ipcMain.handle('db:book:world:element:remove', createHandler<RemoveWorldElementData, boolean>(
function(userId: string, data: RemoveWorldElementData, lang: 'fr' | 'en') {
return World.removeElementFromWorld(userId, data.elementId, lang);
return World.removeElementFromWorld(userId, data.bookId, data.elementId, data.deletedAt, lang);
}
)
);
@@ -374,10 +380,11 @@ ipcMain.handle('db:book:world:element:remove', createHandler<RemoveWorldElementD
// DELETE /book/delete - Delete book
interface DeleteBookData {
id: string;
deletedAt: number;
}
ipcMain.handle('db:book:delete', createHandler<DeleteBookData, boolean>(
function(userId: string, data: DeleteBookData, lang: 'fr' | 'en') {
return Book.removeBook(userId, data.id, lang);
return Book.removeBook(userId, data.id, data.deletedAt, lang);
}
)
);

View File

@@ -120,11 +120,12 @@ ipcMain.handle('db:chapter:add', createHandler<AddChapterData, string>(
// DELETE /chapter/remove - Remove chapter
interface RemoveChapterData {
chapterId: string;
bookId?: string;
bookId: string;
deletedAt: number;
}
ipcMain.handle('db:chapter:remove', createHandler<RemoveChapterData, boolean>(
function(userId: string, data: RemoveChapterData, lang: 'fr' | 'en'): boolean {
return Chapter.removeChapter(userId, data.chapterId, lang);
return Chapter.removeChapter(userId, data.bookId, data.chapterId, data.deletedAt, lang);
}
)
);
@@ -157,10 +158,12 @@ ipcMain.handle('db:chapter:information:add', createHandler<AddChapterInformation
// DELETE /chapter/resume/remove - Remove chapter information
interface RemoveChapterInfoData {
chapterInfoId: string;
bookId: string;
deletedAt: number;
}
ipcMain.handle('db:chapter:information:remove', createHandler<RemoveChapterInfoData, boolean>(
function(userId: string, data: RemoveChapterInfoData, lang: 'fr' | 'en'): boolean {
return Chapter.removeChapterInformation(userId, data.chapterInfoId, lang);
return Chapter.removeChapterInformation(userId, data.bookId, data.chapterInfoId, data.deletedAt, lang);
}
)
);

View File

@@ -57,10 +57,12 @@ ipcMain.handle('db:character:attribute:add', createHandler<AddAttributeData, str
// DELETE /character/attribute/delete - Delete character attribute
interface DeleteAttributeData {
attributeId: string;
bookId: string;
deletedAt: number;
}
ipcMain.handle('db:character:attribute:delete', createHandler<DeleteAttributeData, boolean>(
function(userId: string, data: DeleteAttributeData, lang: 'fr' | 'en'): boolean {
return Character.deleteAttribute(userId, data.attributeId, lang);
return Character.deleteAttribute(userId, data.bookId, data.attributeId, data.deletedAt, lang);
}
)
);
@@ -79,10 +81,12 @@ ipcMain.handle('db:character:update', createHandler<UpdateCharacterData, boolean
// DELETE /character/delete - Delete character
interface DeleteCharacterData {
characterId: string;
bookId: string;
deletedAt: number;
}
ipcMain.handle('db:character:delete', createHandler<DeleteCharacterData, boolean>(
function(userId: string, data: DeleteCharacterData, lang: 'fr' | 'en'): boolean {
return Character.deleteCharacter(userId, data.characterId, lang);
return Character.deleteCharacter(userId, data.bookId, data.characterId, data.deletedAt, lang);
}
)
);

View File

@@ -90,10 +90,12 @@ ipcMain.handle('db:location:section:update', createHandler<UpdateSectionWithSeri
// DELETE /location/delete - Delete location section
interface DeleteLocationData {
locationId: string;
bookId: string;
deletedAt: number;
}
ipcMain.handle('db:location:delete', createHandler<DeleteLocationData, boolean>(
function(userId: string, data: DeleteLocationData, lang: 'fr' | 'en'): boolean {
return Location.deleteLocationSection(userId, data.locationId, lang);
return Location.deleteLocationSection(userId, data.bookId, data.locationId, data.deletedAt, lang);
}
)
);
@@ -101,10 +103,12 @@ ipcMain.handle('db:location:delete', createHandler<DeleteLocationData, boolean>(
// DELETE /location/element/delete - Delete location element
interface DeleteLocationElementData {
elementId: string;
bookId: string;
deletedAt: number;
}
ipcMain.handle('db:location:element:delete', createHandler<DeleteLocationElementData, boolean>(
function(userId: string, data: DeleteLocationElementData, lang: 'fr' | 'en'): boolean {
return Location.deleteLocationElement(userId, data.elementId, lang);
return Location.deleteLocationElement(userId, data.bookId, data.elementId, data.deletedAt, lang);
}
)
);
@@ -112,10 +116,12 @@ ipcMain.handle('db:location:element:delete', createHandler<DeleteLocationElement
// DELETE /location/sub-element/delete - Delete location sub-element
interface DeleteLocationSubElementData {
subElementId: string;
bookId: string;
deletedAt: number;
}
ipcMain.handle('db:location:subelement:delete', createHandler<DeleteLocationSubElementData, boolean>(
function(userId: string, data: DeleteLocationSubElementData, lang: 'fr' | 'en'): boolean {
return Location.deleteLocationSubElement(userId, data.subElementId, lang);
return Location.deleteLocationSubElement(userId, data.bookId, data.subElementId, data.deletedAt, lang);
}
)
);

View File

@@ -21,6 +21,7 @@ interface UpdateCharacterData {
interface DeleteCharacterData {
characterId: string;
deletedAt: number;
}
interface AddAttributeData {
@@ -31,6 +32,7 @@ interface AddAttributeData {
interface DeleteAttributeData {
attributeId: string;
deletedAt: number;
}
// GET /series/character/list - Get character list
@@ -64,7 +66,7 @@ ipcMain.handle('db:series:character:update', createHandler<UpdateCharacterData,
// DELETE /series/character/delete - Delete character
ipcMain.handle('db:series:character:delete', createHandler<DeleteCharacterData, boolean>(
function(userId: string, data: DeleteCharacterData, lang: 'fr' | 'en'): boolean {
return SeriesCharacter.deleteCharacter(userId, data.characterId, lang);
return SeriesCharacter.deleteCharacter(userId, data.characterId, data.deletedAt, lang);
}
));
@@ -78,6 +80,6 @@ ipcMain.handle('db:series:character:attribute:add', createHandler<AddAttributeDa
// DELETE /series/character/attribute/delete - Delete attribute
ipcMain.handle('db:series:character:attribute:delete', createHandler<DeleteAttributeData, boolean>(
function(userId: string, data: DeleteAttributeData, lang: 'fr' | 'en'): boolean {
return SeriesCharacter.deleteAttribute(userId, data.attributeId, lang);
return SeriesCharacter.deleteAttribute(userId, data.attributeId, data.deletedAt, lang);
}
));

View File

@@ -25,14 +25,17 @@ interface AddSubElementData {
interface DeleteLocationData {
locationId: string;
deletedAt: number;
}
interface DeleteElementData {
elementId: string;
deletedAt: number;
}
interface DeleteSubElementData {
subElementId: string;
deletedAt: number;
}
// GET /series/location/list - Get location list
@@ -66,20 +69,20 @@ ipcMain.handle('db:series:location:subelement:add', createHandler<AddSubElementD
// DELETE /series/location/delete - Delete location
ipcMain.handle('db:series:location:delete', createHandler<DeleteLocationData, boolean>(
function(userId: string, data: DeleteLocationData, lang: 'fr' | 'en'): boolean {
return SeriesLocation.deleteLocation(userId, data.locationId, lang);
return SeriesLocation.deleteLocation(userId, data.locationId, data.deletedAt, lang);
}
));
// DELETE /series/location/element/delete - Delete element
ipcMain.handle('db:series:location:element:delete', createHandler<DeleteElementData, boolean>(
function(userId: string, data: DeleteElementData, lang: 'fr' | 'en'): boolean {
return SeriesLocation.deleteElement(userId, data.elementId, lang);
return SeriesLocation.deleteElement(userId, data.elementId, data.deletedAt, lang);
}
));
// DELETE /series/location/sub-element/delete - Delete sub-element
ipcMain.handle('db:series:location:subelement:delete', createHandler<DeleteSubElementData, boolean>(
function(userId: string, data: DeleteSubElementData, lang: 'fr' | 'en'): boolean {
return SeriesLocation.deleteSubElement(userId, data.subElementId, lang);
return SeriesLocation.deleteSubElement(userId, data.subElementId, data.deletedAt, lang);
}
));

View File

@@ -36,6 +36,7 @@ interface UpdateSpellData {
interface DeleteSpellData {
spellId: string;
deletedAt: number;
}
interface AddTagData {
@@ -52,6 +53,7 @@ interface UpdateTagData {
interface DeleteTagData {
tagId: string;
deletedAt: number;
}
// GET /series/spell/list - Get spell list
@@ -85,7 +87,7 @@ ipcMain.handle('db:series:spell:update', createHandler<UpdateSpellData, boolean>
// DELETE /series/spell/delete - Delete spell
ipcMain.handle('db:series:spell:delete', createHandler<DeleteSpellData, boolean>(
function(userId: string, data: DeleteSpellData, lang: 'fr' | 'en'): boolean {
return SeriesSpell.deleteSpell(userId, data.spellId, lang);
return SeriesSpell.deleteSpell(userId, data.spellId, data.deletedAt, lang);
}
));
@@ -106,6 +108,6 @@ ipcMain.handle('db:series:spell:tag:update', createHandler<UpdateTagData, boolea
// DELETE /series/spell/tag/delete - Delete tag
ipcMain.handle('db:series:spell:tag:delete', createHandler<DeleteTagData, boolean>(
function(userId: string, data: DeleteTagData, lang: 'fr' | 'en'): boolean {
return SeriesSpell.deleteTag(userId, data.tagId, lang);
return SeriesSpell.deleteTag(userId, data.tagId, data.deletedAt, lang);
}
));

View File

@@ -1,6 +1,6 @@
import { ipcMain } from 'electron';
import { createHandler } from '../database/LocalSystem.js';
import SeriesSync, { SeriesSyncUploadPayload, SeriesSyncResult } from '../database/models/SeriesSync.js';
import SeriesSync, { SeriesSyncUploadPayload, SeriesSyncResult, CompleteSeries, SyncedSeries } from '../database/models/SeriesSync.js';
import { SyncElementType } from '../database/repositories/series-sync.repo.js';
interface UploadToSeriesData {
@@ -10,7 +10,6 @@ interface UploadToSeriesData {
value: string;
}
// POST /series/sync/upload - Upload field to series
ipcMain.handle('db:series:sync:upload', createHandler<UploadToSeriesData, SeriesSyncResult>(
function(userId: string, data: UploadToSeriesData, lang: 'fr' | 'en'): SeriesSyncResult {
const payload: SeriesSyncUploadPayload = {
@@ -22,3 +21,34 @@ ipcMain.handle('db:series:sync:upload', createHandler<UploadToSeriesData, Series
return SeriesSync.uploadFieldToSeries(userId, payload, lang);
}
));
ipcMain.handle('db:series:synced', createHandler<void, SyncedSeries[]>(
function(userId: string, _data: void, lang: 'fr' | 'en'): SyncedSeries[] {
return SeriesSync.getSyncedSeries(userId, lang);
}
));
ipcMain.handle('db:series:uploadToServer', createHandler<string, CompleteSeries>(
async function(userId: string, seriesId: string, lang: 'fr' | 'en'): Promise<CompleteSeries> {
return SeriesSync.getCompleteSeriesForUpload(userId, seriesId, lang);
}
));
ipcMain.handle('db:series:syncSave', createHandler<CompleteSeries, boolean>(
async function(userId: string, completeSeries: CompleteSeries, lang: 'fr' | 'en'): Promise<boolean> {
return SeriesSync.saveCompleteSeries(userId, completeSeries, lang);
}
));
ipcMain.handle('db:series:sync:toClient', createHandler<CompleteSeries, boolean>(
async function(userId: string, completeSeries: CompleteSeries, lang: 'fr' | 'en'): Promise<boolean> {
return SeriesSync.syncSeriesFromServerToClient(userId, completeSeries, lang);
}
));
ipcMain.handle('db:series:sync:toServer', createHandler<object, CompleteSeries>(
async function(userId: string, syncCompare: object, lang: 'fr' | 'en'): Promise<CompleteSeries> {
const seriesId = (syncCompare as { id: string }).id;
return SeriesSync.getCompleteSeriesForUpload(userId, seriesId, lang);
}
));

View File

@@ -30,6 +30,7 @@ interface AddElementData {
interface DeleteElementData {
elementId: string;
deletedAt: number;
}
// GET /series/world/list - Get world list
@@ -71,6 +72,6 @@ ipcMain.handle('db:series:world:element:add', createHandler<AddElementData, stri
// DELETE /series/world/element/delete - Delete element
ipcMain.handle('db:series:world:element:delete', createHandler<DeleteElementData, boolean>(
function(userId: string, data: DeleteElementData, lang: 'fr' | 'en'): boolean {
return SeriesWorld.deleteElement(userId, data.elementId, lang);
return SeriesWorld.deleteElement(userId, data.elementId, data.deletedAt, lang);
}
));

View File

@@ -16,6 +16,7 @@ interface UpdateSeriesData {
interface DeleteSeriesData {
seriesId: string;
deletedAt: number;
}
interface GetSeriesDetailData {
@@ -31,6 +32,7 @@ interface AddBookToSeriesData {
interface RemoveBookFromSeriesData {
seriesId: string;
bookId: string;
deletedAt: number;
}
interface UpdateBooksOrderData {
@@ -77,7 +79,7 @@ ipcMain.handle('db:series:update', createHandler<UpdateSeriesData, boolean>(
// DELETE /series/delete - Delete series
ipcMain.handle('db:series:delete', createHandler<DeleteSeriesData, boolean>(
async function(userId: string, data: DeleteSeriesData, lang: 'fr' | 'en'): Promise<boolean> {
return await Series.deleteSeries(userId, data.seriesId, lang);
return await Series.deleteSeries(userId, data.seriesId, data.deletedAt, lang);
}
));
@@ -98,7 +100,7 @@ ipcMain.handle('db:series:book:add', createHandler<AddBookToSeriesData, boolean>
// DELETE /series/book/remove - Remove book from series
ipcMain.handle('db:series:book:remove', createHandler<RemoveBookFromSeriesData, boolean>(
async function(userId: string, data: RemoveBookFromSeriesData, lang: 'fr' | 'en'): Promise<boolean> {
return await Series.removeBookFromSeries(userId, data.seriesId, data.bookId, lang);
return await Series.removeBookFromSeries(userId, data.seriesId, data.bookId, data.deletedAt, lang);
}
));

View File

@@ -46,6 +46,8 @@ interface UpdateSpellData {
interface DeleteSpellData {
spellId: string;
bookId: string;
deletedAt: number;
}
interface CreateTagData {
@@ -63,6 +65,7 @@ interface UpdateTagData {
interface DeleteTagData {
tagId: string;
bookId: string;
deletedAt: number;
}
// ==================== SPELL HANDLERS ====================
@@ -152,7 +155,7 @@ ipcMain.handle(
'db:spell:delete',
createHandler<DeleteSpellData, boolean>(
function (userId: string, data: DeleteSpellData, lang: 'fr' | 'en'): boolean {
return Spell.deleteSpell(userId, data.spellId, lang);
return Spell.deleteSpell(userId, data.bookId, data.spellId, data.deletedAt, lang);
},
),
);
@@ -198,7 +201,7 @@ ipcMain.handle(
'db:spell:tag:delete',
createHandler<DeleteTagData, boolean>(
function (userId: string, data: DeleteTagData, lang: 'fr' | 'en'): boolean {
return Spell.deleteSpellTag(userId, data.tagId, data.bookId, lang);
return Spell.deleteSpellTag(userId, data.bookId, data.tagId, data.deletedAt, lang);
},
),
);

View File

@@ -0,0 +1,122 @@
import { ipcMain } from 'electron';
import { createHandler } from '../database/LocalSystem.js';
import RemovedItemsRepository, { RemovedItemRecord } from '../database/repositories/removed-items.repository.js';
import Book from '../database/models/Book.js';
import Chapter from '../database/models/Chapter.js';
import Character from '../database/models/Character.js';
import Location from '../database/models/Location.js';
import World from '../database/models/World.js';
import Incident from '../database/models/Incident.js';
import PlotPoint from '../database/models/PlotPoint.js';
import Issue from '../database/models/Issue.js';
import Spell from '../database/models/Spell.js';
import Series from '../database/models/Series.js';
import SeriesCharacter from '../database/models/SeriesCharacter.js';
import SeriesLocation from '../database/models/SeriesLocation.js';
import SeriesWorld from '../database/models/SeriesWorld.js';
import SeriesSpell from '../database/models/SeriesSpell.js';
/**
* Get tombstones since a specific timestamp.
*/
ipcMain.handle('db:tombstones:since', createHandler<number, RemovedItemRecord[]>(
function(userId: string, since: number, lang: 'fr' | 'en'): RemovedItemRecord[] {
return RemovedItemsRepository.getDeletionsSince(userId, since, lang);
})
);
/**
* Apply server tombstones for book entities locally.
*/
ipcMain.handle('db:tombstones:apply:books', createHandler<RemovedItemRecord[], void>(
function(userId: string, tombstones: RemovedItemRecord[], lang: 'fr' | 'en'): void {
for (const tombstone of tombstones) {
switch (tombstone.table_name) {
case 'erit_books':
Book.removeBook(userId, tombstone.entity_id, tombstone.deleted_at, lang);
break;
case 'book_chapters':
Chapter.removeChapter(userId, tombstone.book_id!, tombstone.entity_id, tombstone.deleted_at, lang);
break;
case 'book_chapter_infos':
Chapter.removeChapterInformation(userId, tombstone.book_id!, tombstone.entity_id, tombstone.deleted_at, lang);
break;
case 'book_characters':
Character.deleteCharacter(userId, tombstone.book_id!, tombstone.entity_id, tombstone.deleted_at, lang);
break;
case 'book_characters_attributes':
Character.deleteAttribute(userId, tombstone.book_id!, tombstone.entity_id, tombstone.deleted_at, lang);
break;
case 'book_location':
Location.deleteLocationSection(userId, tombstone.book_id!, tombstone.entity_id, tombstone.deleted_at, lang);
break;
case 'location_element':
Location.deleteLocationElement(userId, tombstone.book_id!, tombstone.entity_id, tombstone.deleted_at, lang);
break;
case 'location_sub_element':
Location.deleteLocationSubElement(userId, tombstone.book_id!, tombstone.entity_id, tombstone.deleted_at, lang);
break;
case 'book_world_elements':
World.removeElementFromWorld(userId, tombstone.book_id!, tombstone.entity_id, tombstone.deleted_at, lang);
break;
case 'book_incidents':
Incident.removeIncident(userId, tombstone.book_id!, tombstone.entity_id, tombstone.deleted_at, lang);
break;
case 'book_plot_points':
PlotPoint.removePlotPoint(userId, tombstone.book_id!, tombstone.entity_id, tombstone.deleted_at, lang);
break;
case 'book_issues':
Issue.removeIssue(userId, tombstone.book_id!, tombstone.entity_id, tombstone.deleted_at, lang);
break;
case 'book_spells':
Spell.deleteSpell(userId, tombstone.book_id!, tombstone.entity_id, tombstone.deleted_at, lang);
break;
case 'book_spell_tags':
Spell.deleteSpellTag(userId, tombstone.book_id!, tombstone.entity_id, tombstone.deleted_at, lang);
break;
}
}
})
);
/**
* Apply server tombstones for series entities locally.
*/
ipcMain.handle('db:tombstones:apply:series', createHandler<RemovedItemRecord[], void>(
function(userId: string, tombstones: RemovedItemRecord[], lang: 'fr' | 'en'): void {
for (const tombstone of tombstones) {
switch (tombstone.table_name) {
case 'erit_series':
Series.deleteSeries(userId, tombstone.entity_id, tombstone.deleted_at, lang);
break;
case 'series_books':
Series.removeBookFromSeries(userId, tombstone.book_id!, tombstone.entity_id, tombstone.deleted_at, lang);
break;
case 'series_characters':
SeriesCharacter.deleteCharacter(userId, tombstone.entity_id, tombstone.deleted_at, lang);
break;
case 'series_characters_attributes':
SeriesCharacter.deleteAttribute(userId, tombstone.entity_id, tombstone.deleted_at, lang);
break;
case 'series_locations':
SeriesLocation.deleteLocation(userId, tombstone.entity_id, tombstone.deleted_at, lang);
break;
case 'series_location_elements':
SeriesLocation.deleteElement(userId, tombstone.entity_id, tombstone.deleted_at, lang);
break;
case 'series_location_sub_elements':
SeriesLocation.deleteSubElement(userId, tombstone.entity_id, tombstone.deleted_at, lang);
break;
case 'series_world_elements':
SeriesWorld.deleteElement(userId, tombstone.entity_id, tombstone.deleted_at, lang);
break;
case 'series_spells':
SeriesSpell.deleteSpell(userId, tombstone.entity_id, tombstone.deleted_at, lang);
break;
case 'series_spell_tags':
SeriesSpell.deleteTag(userId, tombstone.entity_id, tombstone.deleted_at, lang);
break;
}
}
})
);

View File

@@ -16,6 +16,13 @@ import './ipc/character.ipc.js';
import './ipc/location.ipc.js';
import './ipc/offline.ipc.js';
import './ipc/spell.ipc.js';
import './ipc/series.ipc.js';
import './ipc/series-sync.ipc.js';
import './ipc/series-character.ipc.js';
import './ipc/series-location.ipc.js';
import './ipc/series-world.ipc.js';
import './ipc/series-spell.ipc.js';
import './ipc/tombstone.ipc.js';
// Fix pour __dirname en ES modules
const __filename = fileURLToPath(import.meta.url);