Migrate from window.electron to tauri IPC functions across components

- Replaced `window.electron.invoke` calls with equivalent `tauri` function calls for all IPC interactions.
- Removed `electron.d.ts` TypeScript definitions as they are no longer needed.
- Updated related logic for offline/online state synchronization.
- Added `types.rs` and `shared/mod.rs` modules to support Tauri IPC integration with Rust enums and shared logic.
- Refactored IPC request queues to use updated handler names for consistency with Tauri.
This commit is contained in:
natreex
2026-03-21 09:34:13 -04:00
parent 1a15692e40
commit ee4438834c
144 changed files with 21258 additions and 876 deletions

View File

@@ -0,0 +1,175 @@
use serde::Deserialize;
use tauri::State;
use crate::db::connection::DbManager;
use crate::domains::spell::service;
use crate::error::AppError;
use crate::shared::session::SessionState;
use crate::shared::types::Lang;
fn get_session(session: &State<SessionState>) -> Result<(String, Lang), AppError> {
let session_guard = session.lock().map_err(|e| AppError::Internal(format!("Session lock failed: {}", e)))?;
let user_id = session_guard.get_user_id().map_err(|e| AppError::Auth(e))?.to_string();
let lang = session_guard.lang;
Ok((user_id, lang))
}
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct GetSpellListData {
pub book_id: String,
pub enabled: bool,
}
#[tauri::command]
pub fn get_spell_list(data: GetSpellListData, db: State<DbManager>, session: State<SessionState>) -> Result<service::SpellListResponse, AppError> {
let (user_id, lang) = get_session(&session)?;
let db_manager = db.lock().map_err(|e| AppError::Internal(format!("DB lock failed: {}", e)))?;
let conn = db_manager.get_connection(&user_id)?;
service::get_spell_list(conn, &user_id, &data.book_id, lang)
}
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct GetSpellTagsData {
pub book_id: String,
}
#[tauri::command]
pub fn get_spell_tags(data: GetSpellTagsData, db: State<DbManager>, session: State<SessionState>) -> Result<Vec<service::SpellTagProps>, AppError> {
let (user_id, lang) = get_session(&session)?;
let db_manager = db.lock().map_err(|e| AppError::Internal(format!("DB lock failed: {}", e)))?;
let conn = db_manager.get_connection(&user_id)?;
service::get_spell_tags(conn, &user_id, &data.book_id, lang)
}
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct GetSpellDetailData {
pub spell_id: String,
}
#[tauri::command]
pub fn get_spell_detail(data: GetSpellDetailData, db: State<DbManager>, session: State<SessionState>) -> Result<service::SpellProps, AppError> {
let (user_id, lang) = get_session(&session)?;
let db_manager = db.lock().map_err(|e| AppError::Internal(format!("DB lock failed: {}", e)))?;
let conn = db_manager.get_connection(&user_id)?;
service::get_spell_detail(conn, &user_id, &data.spell_id, lang)
}
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SpellData {
pub name: String,
pub description: String,
pub appearance: String,
pub tags: Vec<String>,
pub power_level: Option<String>,
pub components: Option<String>,
pub limitations: Option<String>,
pub notes: Option<String>,
pub series_spell_id: Option<String>,
}
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CreateSpellData {
pub book_id: String,
pub spell: SpellData,
}
#[tauri::command]
pub fn create_spell(data: CreateSpellData, db: State<DbManager>, session: State<SessionState>) -> Result<service::SpellProps, AppError> {
let (user_id, lang) = get_session(&session)?;
let db_manager = db.lock().map_err(|e| AppError::Internal(format!("DB lock failed: {}", e)))?;
let conn = db_manager.get_connection(&user_id)?;
service::add_spell(
conn, &user_id, &data.book_id, &data.spell.name, &data.spell.description, &data.spell.appearance,
data.spell.tags, data.spell.power_level.as_deref(), data.spell.components.as_deref(),
data.spell.limitations.as_deref(), data.spell.notes.as_deref(), None, lang, data.spell.series_spell_id.as_deref(),
)
}
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct UpdateSpellData {
pub spell_id: String,
pub spell: SpellData,
}
#[tauri::command]
pub fn update_spell(data: UpdateSpellData, db: State<DbManager>, session: State<SessionState>) -> Result<bool, AppError> {
let (user_id, lang) = get_session(&session)?;
let db_manager = db.lock().map_err(|e| AppError::Internal(format!("DB lock failed: {}", e)))?;
let conn = db_manager.get_connection(&user_id)?;
service::update_spell(
conn, &user_id, &data.spell_id, &data.spell.name, &data.spell.description, &data.spell.appearance,
data.spell.tags, data.spell.power_level.as_deref(), data.spell.components.as_deref(),
data.spell.limitations.as_deref(), data.spell.notes.as_deref(), lang, data.spell.series_spell_id.as_deref(),
)
}
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct DeleteSpellData {
pub spell_id: String,
pub book_id: String,
pub deleted_at: i64,
}
#[tauri::command]
pub fn delete_spell(data: DeleteSpellData, db: State<DbManager>, session: State<SessionState>) -> Result<bool, AppError> {
let (user_id, lang) = get_session(&session)?;
let db_manager = db.lock().map_err(|e| AppError::Internal(format!("DB lock failed: {}", e)))?;
let conn = db_manager.get_connection(&user_id)?;
service::delete_spell(conn, &user_id, &data.book_id, &data.spell_id, data.deleted_at, lang)
}
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CreateSpellTagData {
pub book_id: String,
pub name: String,
pub color: Option<String>,
pub id: Option<String>,
}
#[tauri::command]
pub fn create_spell_tag(data: CreateSpellTagData, db: State<DbManager>, session: State<SessionState>) -> Result<service::SpellTagProps, AppError> {
let (user_id, lang) = get_session(&session)?;
let db_manager = db.lock().map_err(|e| AppError::Internal(format!("DB lock failed: {}", e)))?;
let conn = db_manager.get_connection(&user_id)?;
service::add_spell_tag(conn, &user_id, &data.book_id, &data.name, data.color.as_deref(), data.id.as_deref(), lang)
}
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct UpdateSpellTagData {
pub tag_id: String,
pub name: String,
pub color: Option<String>,
}
#[tauri::command]
pub fn update_spell_tag(data: UpdateSpellTagData, db: State<DbManager>, session: State<SessionState>) -> Result<bool, AppError> {
let (user_id, lang) = get_session(&session)?;
let db_manager = db.lock().map_err(|e| AppError::Internal(format!("DB lock failed: {}", e)))?;
let conn = db_manager.get_connection(&user_id)?;
service::update_spell_tag(conn, &user_id, &data.tag_id, &data.name, data.color.as_deref(), lang)
}
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct DeleteSpellTagData {
pub tag_id: String,
pub book_id: String,
pub deleted_at: i64,
}
#[tauri::command]
pub fn delete_spell_tag(data: DeleteSpellTagData, db: State<DbManager>, session: State<SessionState>) -> Result<bool, AppError> {
let (user_id, lang) = get_session(&session)?;
let db_manager = db.lock().map_err(|e| AppError::Internal(format!("DB lock failed: {}", e)))?;
let conn = db_manager.get_connection(&user_id)?;
service::delete_spell_tag(conn, &user_id, &data.book_id, &data.tag_id, data.deleted_at, lang)
}

View File

@@ -0,0 +1,3 @@
pub mod commands;
pub mod repo;
pub mod service;

View File

@@ -0,0 +1,362 @@
use rusqlite::{params, Connection};
use crate::error::{AppError, AppResult};
use crate::shared::types::Lang;
pub struct SpellResult {
pub spell_id: String,
pub book_id: String,
pub name: String,
pub description: Option<String>,
pub appearance: Option<String>,
pub tags: Option<String>,
pub power_level: Option<String>,
pub components: Option<String>,
pub limitations: Option<String>,
pub notes: Option<String>,
pub series_spell_id: Option<String>,
}
pub struct BookSpellsTable {
pub spell_id: String,
pub book_id: String,
pub user_id: String,
pub name: String,
pub name_hash: String,
pub description: Option<String>,
pub appearance: Option<String>,
pub tags: Option<String>,
pub power_level: Option<String>,
pub components: Option<String>,
pub limitations: Option<String>,
pub notes: Option<String>,
pub last_update: i64,
}
pub struct SyncedSpellResult {
pub spell_id: String,
pub book_id: String,
pub name: String,
pub last_update: i64,
}
/// Fetches all spells for a specific book owned by the user.
/// * `conn` - Database connection
/// * `user_id` - The unique identifier of the user
/// * `book_id` - The unique identifier of the book
/// * `lang` - The language for error messages
/// Returns an array of spell results.
pub fn fetch_spells(conn: &Connection, user_id: &str, book_id: &str, lang: Lang) -> AppResult<Vec<SpellResult>> {
let mut statement = conn
.prepare("SELECT spell_id, book_id, name, description, appearance, tags, power_level, components, limitations, notes, series_spell_id FROM book_spells WHERE user_id=?1 AND book_id=?2")
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les sorts.".to_string() } else { "Unable to retrieve spells.".to_string() }))?;
let spells = statement
.query_map(params![user_id, book_id], |query_row| {
Ok(SpellResult {
spell_id: query_row.get(0)?, book_id: query_row.get(1)?,
name: query_row.get(2)?, description: query_row.get(3)?,
appearance: query_row.get(4)?, tags: query_row.get(5)?,
power_level: query_row.get(6)?, components: query_row.get(7)?,
limitations: query_row.get(8)?, notes: query_row.get(9)?,
series_spell_id: query_row.get(10)?,
})
})
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les sorts.".to_string() } else { "Unable to retrieve spells.".to_string() }))?
.collect::<Result<Vec<_>, _>>()
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les sorts.".to_string() } else { "Unable to retrieve spells.".to_string() }))?;
Ok(spells)
}
/// Fetches a single spell by its ID.
/// * `conn` - Database connection
/// * `user_id` - The unique identifier of the user
/// * `spell_id` - The unique identifier of the spell
/// * `lang` - The language for error messages
/// Returns the spell result or None if not found.
pub fn fetch_spell_by_id(conn: &Connection, user_id: &str, spell_id: &str, lang: Lang) -> AppResult<Option<SpellResult>> {
let mut statement = conn
.prepare("SELECT spell_id, book_id, name, description, appearance, tags, power_level, components, limitations, notes, series_spell_id FROM book_spells WHERE user_id=?1 AND spell_id=?2")
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer le sort.".to_string() } else { "Unable to retrieve spell.".to_string() }))?;
let spells = statement
.query_map(params![user_id, spell_id], |query_row| {
Ok(SpellResult {
spell_id: query_row.get(0)?, book_id: query_row.get(1)?,
name: query_row.get(2)?, description: query_row.get(3)?,
appearance: query_row.get(4)?, tags: query_row.get(5)?,
power_level: query_row.get(6)?, components: query_row.get(7)?,
limitations: query_row.get(8)?, notes: query_row.get(9)?,
series_spell_id: query_row.get(10)?,
})
})
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer le sort.".to_string() } else { "Unable to retrieve spell.".to_string() }))?
.collect::<Result<Vec<_>, _>>()
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer le sort.".to_string() } else { "Unable to retrieve spell.".to_string() }))?;
Ok(spells.into_iter().next())
}
/// Inserts a new spell.
/// * `conn` - Database connection
/// * `spell_id` - The unique identifier for the new spell
/// * `book_id` - The unique identifier of the book
/// * `user_id` - The unique identifier of the user
/// * `name` - The encrypted name
/// * `name_hash` - The hashed name for duplicate detection
/// * `description` - The encrypted description
/// * `appearance` - The encrypted appearance
/// * `tags` - The encrypted JSON tags array
/// * `power_level` - The encrypted power level (nullable)
/// * `components` - The encrypted components (nullable)
/// * `limitations` - The encrypted limitations (nullable)
/// * `notes` - The encrypted notes (nullable)
/// * `lang` - The language for error messages
/// * `series_spell_id` - The optional series spell identifier
/// Returns the spell ID if successful.
pub fn insert_spell(
conn: &Connection, spell_id: &str, book_id: &str, user_id: &str, name: &str,
name_hash: &str, description: Option<&str>, appearance: Option<&str>, tags: &str,
power_level: Option<&str>, components: Option<&str>, limitations: Option<&str>,
notes: Option<&str>, last_update: i64, lang: Lang, series_spell_id: Option<&str>,
) -> AppResult<String> {
let insert_result = match series_spell_id {
Some(series_id) => conn
.execute("INSERT INTO book_spells (spell_id, book_id, user_id, name, name_hash, description, appearance, tags, power_level, components, limitations, notes, series_spell_id, last_update) VALUES (?1,?2,?3,?4,?5,?6,?7,?8,?9,?10,?11,?12,?13,?14)", params![spell_id, book_id, user_id, name, name_hash, description, appearance, tags, power_level, components, limitations, notes, series_id, last_update])
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible d'ajouter le sort.".to_string() } else { "Unable to add spell.".to_string() }))?,
None => conn
.execute("INSERT INTO book_spells (spell_id, book_id, user_id, name, name_hash, description, appearance, tags, power_level, components, limitations, notes, last_update) VALUES (?1,?2,?3,?4,?5,?6,?7,?8,?9,?10,?11,?12,?13)", params![spell_id, book_id, user_id, name, name_hash, description, appearance, tags, power_level, components, limitations, notes, last_update])
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible d'ajouter le sort.".to_string() } else { "Unable to add spell.".to_string() }))?,
};
if insert_result > 0 {
Ok(spell_id.to_string())
} else {
Err(AppError::Internal(if lang == Lang::Fr { "Une erreur s'est produite lors de l'ajout du sort.".to_string() } else { "Error adding spell.".to_string() }))
}
}
/// Updates an existing spell.
/// * `conn` - Database connection
/// * `user_id` - The unique identifier of the user
/// * `spell_id` - The unique identifier of the spell
/// * `name` - The encrypted name
/// * `name_hash` - The hashed name
/// * `description` - The encrypted description
/// * `appearance` - The encrypted appearance
/// * `tags` - The encrypted JSON tags array
/// * `power_level` - The encrypted power level (nullable)
/// * `components` - The encrypted components (nullable)
/// * `limitations` - The encrypted limitations (nullable)
/// * `notes` - The encrypted notes (nullable)
/// * `lang` - The language for error messages
/// * `series_spell_id` - The optional series spell identifier
/// Returns true if the update was successful.
pub fn update_spell(
conn: &Connection, user_id: &str, spell_id: &str, name: &str, name_hash: &str,
description: Option<&str>, appearance: Option<&str>, tags: &str, power_level: Option<&str>,
components: Option<&str>, limitations: Option<&str>, notes: Option<&str>, last_update: i64, lang: Lang,
series_spell_id: Option<&str>,
) -> AppResult<bool> {
let update_result = match series_spell_id {
Some(series_id) => conn
.execute("UPDATE book_spells SET name=?1, name_hash=?2, description=?3, appearance=?4, tags=?5, power_level=?6, components=?7, limitations=?8, notes=?9, series_spell_id=?10, last_update=?11 WHERE spell_id=?12 AND user_id=?13", params![name, name_hash, description, appearance, tags, power_level, components, limitations, notes, series_id, last_update, spell_id, user_id])
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de mettre à jour le sort.".to_string() } else { "Unable to update spell.".to_string() }))?,
None => conn
.execute("UPDATE book_spells SET name=?1, name_hash=?2, description=?3, appearance=?4, tags=?5, power_level=?6, components=?7, limitations=?8, notes=?9, last_update=?10 WHERE spell_id=?11 AND user_id=?12", params![name, name_hash, description, appearance, tags, power_level, components, limitations, notes, last_update, spell_id, user_id])
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de mettre à jour le sort.".to_string() } else { "Unable to update spell.".to_string() }))?,
};
Ok(update_result > 0)
}
/// Deletes a spell.
/// * `conn` - Database connection
/// * `user_id` - The unique identifier of the user
/// * `spell_id` - The unique identifier of the spell
/// * `lang` - The language for error messages
/// Returns true if the deletion was successful.
pub fn delete_spell(conn: &Connection, user_id: &str, spell_id: &str, lang: Lang) -> AppResult<bool> {
let delete_result = conn
.execute("DELETE FROM book_spells WHERE spell_id=?1 AND user_id=?2", params![spell_id, user_id])
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de supprimer le sort.".to_string() } else { "Unable to delete spell.".to_string() }))?;
Ok(delete_result > 0)
}
/// Updates the tags field of a spell.
/// * `conn` - Database connection
/// * `user_id` - The unique identifier of the user
/// * `spell_id` - The unique identifier of the spell
/// * `tags` - The new encrypted JSON tags array
/// * `lang` - The language for error messages
/// Returns true if the update was successful.
pub fn update_spell_tags(conn: &Connection, user_id: &str, spell_id: &str, tags: &str, last_update: i64, lang: Lang) -> AppResult<bool> {
let update_result = conn
.execute("UPDATE book_spells SET tags=?1, last_update=?2 WHERE spell_id=?3 AND user_id=?4", params![tags, last_update, spell_id, user_id])
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de mettre à jour les tags du sort.".to_string() } else { "Unable to update spell tags.".to_string() }))?;
Ok(update_result > 0)
}
/// Fetches all spells for a book with full table data for sync.
/// * `conn` - Database connection
/// * `user_id` - The unique identifier of the user
/// * `book_id` - The unique identifier of the book
/// * `lang` - The language for error messages
/// Returns an array of book spells table records.
pub fn fetch_book_spells_table(conn: &Connection, user_id: &str, book_id: &str, lang: Lang) -> AppResult<Vec<BookSpellsTable>> {
let mut statement = conn
.prepare("SELECT spell_id, book_id, user_id, name, name_hash, description, appearance, tags, power_level, components, limitations, notes, last_update FROM book_spells WHERE user_id=?1 AND book_id=?2")
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les sorts.".to_string() } else { "Unable to retrieve spells.".to_string() }))?;
let spells = statement
.query_map(params![user_id, book_id], |query_row| {
Ok(BookSpellsTable {
spell_id: query_row.get(0)?, book_id: query_row.get(1)?,
user_id: query_row.get(2)?, name: query_row.get(3)?,
name_hash: query_row.get(4)?, description: query_row.get(5)?,
appearance: query_row.get(6)?, tags: query_row.get(7)?,
power_level: query_row.get(8)?, components: query_row.get(9)?,
limitations: query_row.get(10)?, notes: query_row.get(11)?,
last_update: query_row.get(12)?,
})
})
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les sorts.".to_string() } else { "Unable to retrieve spells.".to_string() }))?
.collect::<Result<Vec<_>, _>>()
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les sorts.".to_string() } else { "Unable to retrieve spells.".to_string() }))?;
Ok(spells)
}
/// Fetches a complete spell record by its ID.
/// * `conn` - Database connection
/// * `user_id` - The unique identifier of the user
/// * `spell_id` - The unique identifier of the spell
/// * `lang` - The language for error messages
/// Returns the spell table record or None.
pub fn fetch_spell_table_by_id(conn: &Connection, user_id: &str, spell_id: &str, lang: Lang) -> AppResult<Option<BookSpellsTable>> {
let mut statement = conn
.prepare("SELECT spell_id, book_id, user_id, name, name_hash, description, appearance, tags, power_level, components, limitations, notes, last_update FROM book_spells WHERE user_id=?1 AND spell_id=?2")
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer le sort.".to_string() } else { "Unable to retrieve spell.".to_string() }))?;
let spells = statement
.query_map(params![user_id, spell_id], |query_row| {
Ok(BookSpellsTable {
spell_id: query_row.get(0)?, book_id: query_row.get(1)?,
user_id: query_row.get(2)?, name: query_row.get(3)?,
name_hash: query_row.get(4)?, description: query_row.get(5)?,
appearance: query_row.get(6)?, tags: query_row.get(7)?,
power_level: query_row.get(8)?, components: query_row.get(9)?,
limitations: query_row.get(10)?, notes: query_row.get(11)?,
last_update: query_row.get(12)?,
})
})
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer le sort.".to_string() } else { "Unable to retrieve spell.".to_string() }))?
.collect::<Result<Vec<_>, _>>()
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer le sort.".to_string() } else { "Unable to retrieve spell.".to_string() }))?;
Ok(spells.into_iter().next())
}
/// Fetches all synced spells for a user.
/// * `conn` - Database connection
/// * `user_id` - The unique identifier of the user
/// * `lang` - The language for error messages
/// Returns an array of synced spell results.
pub fn fetch_synced_spells(conn: &Connection, user_id: &str, lang: Lang) -> AppResult<Vec<SyncedSpellResult>> {
let mut statement = conn
.prepare("SELECT spell_id, book_id, name, last_update FROM book_spells WHERE user_id=?1")
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les sorts synchronisés.".to_string() } else { "Unable to retrieve synced spells.".to_string() }))?;
let spells = statement
.query_map(params![user_id], |query_row| {
Ok(SyncedSpellResult {
spell_id: query_row.get(0)?, book_id: query_row.get(1)?,
name: query_row.get(2)?, last_update: query_row.get(3)?,
})
})
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les sorts synchronisés.".to_string() } else { "Unable to retrieve synced spells.".to_string() }))?
.collect::<Result<Vec<_>, _>>()
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les sorts synchronisés.".to_string() } else { "Unable to retrieve synced spells.".to_string() }))?;
Ok(spells)
}
/// Inserts or updates a spell from synchronization data.
/// * `conn` - Database connection
/// * `spell_id` - The unique identifier for the spell
/// * `book_id` - The unique identifier of the book
/// * `user_id` - The unique identifier of the user
/// * `name` - The encrypted name
/// * `name_hash` - The hashed name
/// * `description` - The encrypted description
/// * `appearance` - The encrypted appearance
/// * `tags` - The encrypted JSON tags array
/// * `power_level` - The encrypted power level (nullable)
/// * `components` - The encrypted components (nullable)
/// * `limitations` - The encrypted limitations (nullable)
/// * `notes` - The encrypted notes (nullable)
/// * `last_update` - The timestamp of the last update
/// * `lang` - The language for error messages
/// Returns true if the insertion was successful.
pub fn insert_sync_spell(
conn: &Connection, spell_id: &str, book_id: &str, user_id: &str, name: &str,
name_hash: &str, description: Option<&str>, appearance: Option<&str>, tags: Option<&str>,
power_level: Option<&str>, components: Option<&str>, limitations: Option<&str>,
notes: Option<&str>, last_update: i64, lang: Lang,
) -> AppResult<bool> {
let insert_result = conn
.execute("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 (?1,?2,?3,?4,?5,?6,?7,?8,?9,?10,?11,?12,?13)", params![spell_id, book_id, user_id, name, name_hash, description, appearance, tags, power_level, components, limitations, notes, last_update])
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible d'insérer le sort.".to_string() } else { "Unable to insert spell.".to_string() }))?;
Ok(insert_result > 0)
}
/// Checks if a spell exists.
/// * `conn` - Database connection
/// * `user_id` - The unique identifier of the user
/// * `spell_id` - The unique identifier of the spell
/// * `lang` - The language for error messages
/// Returns true if the spell exists.
pub fn is_spell_exist(conn: &Connection, user_id: &str, spell_id: &str, lang: Lang) -> AppResult<bool> {
let mut statement = conn
.prepare("SELECT 1 FROM book_spells WHERE spell_id=?1 AND user_id=?2")
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de vérifier l'existence du sort.".to_string() } else { "Unable to check spell existence.".to_string() }))?;
let exists = statement
.exists(params![spell_id, user_id])
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de vérifier l'existence du sort.".to_string() } else { "Unable to check spell existence.".to_string() }))?;
Ok(exists)
}
/// Updates a spell with timestamp for sync.
/// * `conn` - Database connection
/// * `user_id` - The unique identifier of the user
/// * `spell_id` - The unique identifier of the spell
/// * `name` - The encrypted name
/// * `name_hash` - The hashed name
/// * `description` - The encrypted description
/// * `appearance` - The encrypted appearance
/// * `tags` - The encrypted JSON tags array
/// * `power_level` - The encrypted power level (nullable)
/// * `components` - The encrypted components (nullable)
/// * `limitations` - The encrypted limitations (nullable)
/// * `notes` - The encrypted notes (nullable)
/// * `last_update` - The timestamp of the last update
/// * `lang` - The language for error messages
/// Returns true if the update was successful.
pub fn update_sync_spell(
conn: &Connection, user_id: &str, spell_id: &str, name: &str, name_hash: &str,
description: Option<&str>, appearance: Option<&str>, tags: Option<&str>,
power_level: Option<&str>, components: Option<&str>, limitations: Option<&str>,
notes: Option<&str>, last_update: i64, lang: Lang,
) -> AppResult<bool> {
let update_result = conn
.execute("UPDATE book_spells SET name=?1, name_hash=?2, description=?3, appearance=?4, tags=?5, power_level=?6, components=?7, limitations=?8, notes=?9, last_update=?10 WHERE spell_id=?11 AND user_id=?12", params![name, name_hash, description, appearance, tags, power_level, components, limitations, notes, last_update, spell_id, user_id])
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de mettre à jour le sort.".to_string() } else { "Unable to update spell.".to_string() }))?;
Ok(update_result > 0)
}

View File

@@ -0,0 +1,406 @@
use std::collections::HashMap;
use rusqlite::Connection;
use serde::Serialize;
use crate::crypto::encryption::{decrypt_data_with_user_key, encrypt_data_with_user_key, hash_element};
use crate::crypto::key_manager::get_user_encryption_key;
use crate::domains::book::repo as book_repo;
use crate::domains::spell::repo;
use crate::domains::spell_tag::repo as spell_tag_repo;
use crate::domains::tombstone::repo as tombstone_repo;
use crate::error::{AppError, AppResult};
use crate::helpers::{create_unique_id, timestamp_in_seconds};
use crate::shared::types::Lang;
#[derive(Serialize)]
pub struct SpellTagProps {
pub id: String,
pub name: String,
pub color: Option<String>,
}
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
pub struct SpellProps {
pub id: String,
pub name: String,
pub description: String,
pub appearance: String,
pub tags: Vec<String>,
pub power_level: Option<String>,
pub components: Option<String>,
pub limitations: Option<String>,
pub notes: Option<String>,
pub series_spell_id: Option<String>,
}
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
pub struct SpellListItem {
pub id: String,
pub name: String,
pub description: String,
pub tags: Vec<SpellTagProps>,
pub series_spell_id: Option<String>,
}
#[derive(Serialize)]
pub struct SpellListResponse {
pub enabled: bool,
pub spells: Vec<SpellListItem>,
pub tags: Vec<SpellTagProps>,
}
pub struct SyncedSpell {
pub id: String,
pub name: String,
pub last_update: i64,
}
pub struct SyncedSpellTag {
pub id: String,
pub name: String,
pub last_update: i64,
}
/// Retrieves all spell tags for a specific book.
/// * `conn` - Database connection
/// * `user_id` - The unique identifier of the user
/// * `book_id` - The unique identifier of the book
/// * `lang` - The language for error messages ("fr" or "en")
/// Returns an array of spell tag props.
pub fn get_spell_tags(conn: &Connection, user_id: &str, book_id: &str, lang: Lang) -> AppResult<Vec<SpellTagProps>> {
let user_key: String = get_user_encryption_key(user_id)?;
let spell_tags: Vec<spell_tag_repo::SpellTagResult> = spell_tag_repo::fetch_spell_tags(conn, user_id, book_id, lang)?;
let mut result: Vec<SpellTagProps> = Vec::with_capacity(spell_tags.len());
for tag in spell_tags {
result.push(SpellTagProps {
id: tag.tag_id,
name: decrypt_data_with_user_key(&tag.name, &user_key)?,
color: tag.color,
});
}
Ok(result)
}
/// Adds a new spell tag to a book.
/// * `conn` - Database connection
/// * `user_id` - The unique identifier of the user
/// * `book_id` - The unique identifier of the book
/// * `name` - The name of the tag
/// * `color` - The optional color hex code
/// * `existing_tag_id` - Optional existing tag ID for sync
/// * `lang` - The language for error messages ("fr" or "en")
/// Returns the created spell tag props.
pub fn add_spell_tag(
conn: &Connection, user_id: &str, book_id: &str, name: &str, color: Option<&str>,
existing_tag_id: Option<&str>, lang: Lang,
) -> AppResult<SpellTagProps> {
let user_key: String = get_user_encryption_key(user_id)?;
let tag_id: String = create_unique_id(existing_tag_id);
let encrypted_name: String = encrypt_data_with_user_key(name, &user_key)?;
let name_hash: String = hash_element(name);
let last_update: i64 = timestamp_in_seconds();
spell_tag_repo::insert_spell_tag(conn, &tag_id, book_id, user_id, &encrypted_name, &name_hash, color, last_update, lang)?;
Ok(SpellTagProps {
id: tag_id,
name: name.to_string(),
color: color.map(|c| c.to_string()),
})
}
/// Updates an existing spell tag.
/// * `conn` - Database connection
/// * `user_id` - The unique identifier of the user
/// * `tag_id` - The unique identifier of the tag
/// * `name` - The new name of the tag
/// * `color` - The new optional color hex code
/// * `lang` - The language for error messages ("fr" or "en")
/// Returns true if the update was successful.
pub fn update_spell_tag(
conn: &Connection, user_id: &str, tag_id: &str, name: &str, color: Option<&str>, lang: Lang,
) -> AppResult<bool> {
let user_key: String = get_user_encryption_key(user_id)?;
let encrypted_name: String = encrypt_data_with_user_key(name, &user_key)?;
let name_hash: String = hash_element(name);
let last_update: i64 = timestamp_in_seconds();
spell_tag_repo::update_spell_tag(conn, user_id, tag_id, &encrypted_name, &name_hash, color, last_update, lang)
}
/// Deletes a spell tag and removes its references from all spells in the book.
/// * `conn` - Database connection
/// * `user_id` - The unique identifier of the user
/// * `book_id` - The unique identifier of the book
/// * `tag_id` - The unique identifier of the tag to delete
/// * `deleted_at` - The timestamp of deletion
/// * `lang` - The language for error messages ("fr" or "en")
/// Returns true if the deletion was successful.
pub fn delete_spell_tag(
conn: &Connection, user_id: &str, book_id: &str, tag_id: &str, deleted_at: i64, lang: Lang,
) -> AppResult<bool> {
let user_key: String = get_user_encryption_key(user_id)?;
let spells: Vec<repo::SpellResult> = repo::fetch_spells(conn, user_id, book_id, lang)?;
let last_update: i64 = timestamp_in_seconds();
for spell in &spells {
let decrypted_tags: Option<String> = if let Some(ref tags) = spell.tags {
Some(decrypt_data_with_user_key(tags, &user_key)?)
} else {
None
};
let tags_array: Vec<String> = match decrypted_tags {
Some(ref tags_str) => serde_json::from_str(tags_str).unwrap_or_default(),
None => Vec::new(),
};
if tags_array.contains(&tag_id.to_string()) {
let updated_tags: Vec<&String> = tags_array.iter().filter(|t| t.as_str() != tag_id).collect();
let serialized_tags: String = serde_json::to_string(&updated_tags).unwrap_or_else(|_| "[]".to_string());
let encrypted_tags: String = encrypt_data_with_user_key(&serialized_tags, &user_key)?;
repo::update_spell_tags(conn, user_id, &spell.spell_id, &encrypted_tags, last_update, lang)?;
}
}
let deleted: bool = spell_tag_repo::delete_spell_tag(conn, user_id, tag_id, lang)?;
if deleted {
let removal_id: String = create_unique_id(None);
tombstone_repo::insert(conn, &removal_id, "book_spell_tags", tag_id, Some(book_id), user_id, deleted_at, lang)?;
}
Ok(deleted)
}
/// Retrieves the spell list with tags for a specific book.
/// * `conn` - Database connection
/// * `user_id` - The unique identifier of the user
/// * `book_id` - The unique identifier of the book
/// * `lang` - The language for error messages ("fr" or "en")
/// Returns the spell list response with enabled status, spells, and tags.
pub fn get_spell_list(conn: &Connection, user_id: &str, book_id: &str, lang: Lang) -> AppResult<SpellListResponse> {
let user_key: String = get_user_encryption_key(user_id)?;
let book_tools: Option<book_repo::BookToolsTable> = book_repo::fetch_book_tools(conn, user_id, book_id, lang)?;
let enabled: bool = book_tools.map_or(false, |bt| bt.spells_enabled == 1);
let spell_tags: Vec<spell_tag_repo::SpellTagResult> = spell_tag_repo::fetch_spell_tags(conn, user_id, book_id, lang)?;
let mut tags: Vec<SpellTagProps> = Vec::with_capacity(spell_tags.len());
let mut tag_map: HashMap<String, (String, Option<String>)> = HashMap::new();
for tag in spell_tags {
let decrypted_name: String = decrypt_data_with_user_key(&tag.name, &user_key)?;
tag_map.insert(tag.tag_id.clone(), (decrypted_name.clone(), tag.color.clone()));
tags.push(SpellTagProps {
id: tag.tag_id,
name: decrypted_name,
color: tag.color,
});
}
let spell_results: Vec<repo::SpellResult> = repo::fetch_spells(conn, user_id, book_id, lang)?;
let mut spells: Vec<SpellListItem> = Vec::with_capacity(spell_results.len());
for spell in spell_results {
let decrypted_name: String = decrypt_data_with_user_key(&spell.name, &user_key)?;
let decrypted_description: Option<String> = if let Some(ref description) = spell.description {
Some(decrypt_data_with_user_key(description, &user_key)?)
} else {
None
};
let decrypted_tags: Option<String> = if let Some(ref tags_str) = spell.tags {
Some(decrypt_data_with_user_key(tags_str, &user_key)?)
} else {
None
};
let tag_ids: Vec<String> = match decrypted_tags {
Some(ref tags_str) => serde_json::from_str(tags_str).unwrap_or_default(),
None => Vec::new(),
};
let resolved_tags: Vec<SpellTagProps> = tag_ids
.iter()
.filter_map(|tag_id| {
tag_map.get(tag_id).map(|(name, color)| SpellTagProps {
id: tag_id.clone(),
name: name.clone(),
color: color.clone(),
})
})
.collect();
let truncated_description: String = match decrypted_description {
Some(ref desc) if desc.len() > 150 => format!("{}...", &desc[..150]),
Some(ref desc) => desc.clone(),
None => String::new(),
};
spells.push(SpellListItem {
id: spell.spell_id,
name: decrypted_name,
description: truncated_description,
tags: resolved_tags,
series_spell_id: spell.series_spell_id,
});
}
Ok(SpellListResponse { enabled, spells, tags })
}
/// Retrieves the full details of a specific spell.
/// * `conn` - Database connection
/// * `user_id` - The unique identifier of the user
/// * `spell_id` - The unique identifier of the spell
/// * `lang` - The language for error messages ("fr" or "en")
/// Returns the spell props with all details.
pub fn get_spell_detail(conn: &Connection, user_id: &str, spell_id: &str, lang: Lang) -> AppResult<SpellProps> {
let user_key: String = get_user_encryption_key(user_id)?;
let spell: repo::SpellResult = repo::fetch_spell_by_id(conn, user_id, spell_id, lang)?
.ok_or_else(|| AppError::Internal(if lang == Lang::Fr { "Sort non trouvé.".to_string() } else { "Spell not found.".to_string() }))?;
let decrypted_name: String = decrypt_data_with_user_key(&spell.name, &user_key)?;
let decrypted_description: String = if let Some(ref description) = spell.description { decrypt_data_with_user_key(description, &user_key)? } else { String::new() };
let decrypted_appearance: String = if let Some(ref appearance) = spell.appearance { decrypt_data_with_user_key(appearance, &user_key)? } else { String::new() };
let decrypted_tags: Option<String> = if let Some(ref tags) = spell.tags { Some(decrypt_data_with_user_key(tags, &user_key)?) } else { None };
let tag_ids: Vec<String> = match decrypted_tags {
Some(ref tags_str) => serde_json::from_str(tags_str).unwrap_or_default(),
None => Vec::new(),
};
Ok(SpellProps {
id: spell.spell_id,
name: decrypted_name,
description: decrypted_description,
appearance: decrypted_appearance,
tags: tag_ids,
power_level: if let Some(ref power_level) = spell.power_level { Some(decrypt_data_with_user_key(power_level, &user_key)?) } else { None },
components: if let Some(ref components) = spell.components { Some(decrypt_data_with_user_key(components, &user_key)?) } else { None },
limitations: if let Some(ref limitations) = spell.limitations { Some(decrypt_data_with_user_key(limitations, &user_key)?) } else { None },
notes: if let Some(ref notes) = spell.notes { Some(decrypt_data_with_user_key(notes, &user_key)?) } else { None },
series_spell_id: spell.series_spell_id,
})
}
/// Adds a new spell to a book.
/// * `conn` - Database connection
/// * `user_id` - The unique identifier of the user
/// * `book_id` - The unique identifier of the book
/// * `name` - The name of the spell
/// * `description` - The description of the spell
/// * `appearance` - The appearance of the spell
/// * `tags` - The tag IDs array
/// * `power_level` - The optional power level
/// * `components` - The optional components
/// * `limitations` - The optional limitations
/// * `notes` - The optional notes
/// * `existing_spell_id` - Optional existing spell ID for sync
/// * `lang` - The language for error messages ("fr" or "en")
/// * `series_spell_id` - The optional series spell identifier
/// Returns the created spell props.
pub fn add_spell(
conn: &Connection, user_id: &str, book_id: &str, name: &str, description: &str, appearance: &str,
tags: Vec<String>, power_level: Option<&str>, components: Option<&str>, limitations: Option<&str>,
notes: Option<&str>, existing_spell_id: Option<&str>, lang: Lang, series_spell_id: Option<&str>,
) -> AppResult<SpellProps> {
let user_key: String = get_user_encryption_key(user_id)?;
let spell_id: String = create_unique_id(existing_spell_id);
let encrypted_name: String = encrypt_data_with_user_key(name, &user_key)?;
let name_hash: String = hash_element(name);
let encrypted_description: String = encrypt_data_with_user_key(description, &user_key)?;
let encrypted_appearance: String = encrypt_data_with_user_key(appearance, &user_key)?;
let serialized_tags: String = serde_json::to_string(&tags).unwrap_or_else(|_| "[]".to_string());
let encrypted_tags: String = encrypt_data_with_user_key(&serialized_tags, &user_key)?;
let encrypted_power_level: Option<String> = if let Some(power_level_val) = power_level { Some(encrypt_data_with_user_key(power_level_val, &user_key)?) } else { None };
let encrypted_components: Option<String> = if let Some(components_val) = components { Some(encrypt_data_with_user_key(components_val, &user_key)?) } else { None };
let encrypted_limitations: Option<String> = if let Some(limitations_val) = limitations { Some(encrypt_data_with_user_key(limitations_val, &user_key)?) } else { None };
let encrypted_notes: Option<String> = if let Some(notes_val) = notes { Some(encrypt_data_with_user_key(notes_val, &user_key)?) } else { None };
let last_update: i64 = timestamp_in_seconds();
repo::insert_spell(
conn, &spell_id, book_id, user_id, &encrypted_name, &name_hash,
Some(&encrypted_description), Some(&encrypted_appearance), &encrypted_tags,
encrypted_power_level.as_deref(), encrypted_components.as_deref(),
encrypted_limitations.as_deref(), encrypted_notes.as_deref(),
last_update, lang, series_spell_id,
)?;
Ok(SpellProps {
id: spell_id,
name: name.to_string(),
description: description.to_string(),
appearance: appearance.to_string(),
tags,
power_level: power_level.map(|s| s.to_string()),
components: components.map(|s| s.to_string()),
limitations: limitations.map(|s| s.to_string()),
notes: notes.map(|s| s.to_string()),
series_spell_id: series_spell_id.map(|s| s.to_string()),
})
}
/// Updates an existing spell.
/// * `conn` - Database connection
/// * `user_id` - The unique identifier of the user
/// * `spell_id` - The unique identifier of the spell
/// * `name` - The name of the spell
/// * `description` - The description of the spell
/// * `appearance` - The appearance of the spell
/// * `tags` - The tag IDs array
/// * `power_level` - The optional power level
/// * `components` - The optional components
/// * `limitations` - The optional limitations
/// * `notes` - The optional notes
/// * `lang` - The language for error messages ("fr" or "en")
/// * `series_spell_id` - The optional series spell identifier
/// Returns true if the update was successful.
pub fn update_spell(
conn: &Connection, user_id: &str, spell_id: &str, name: &str, description: &str, appearance: &str,
tags: Vec<String>, power_level: Option<&str>, components: Option<&str>, limitations: Option<&str>,
notes: Option<&str>, lang: Lang, series_spell_id: Option<&str>,
) -> AppResult<bool> {
let user_key: String = get_user_encryption_key(user_id)?;
let encrypted_name: String = encrypt_data_with_user_key(name, &user_key)?;
let name_hash: String = hash_element(name);
let encrypted_description: String = encrypt_data_with_user_key(description, &user_key)?;
let encrypted_appearance: String = encrypt_data_with_user_key(appearance, &user_key)?;
let serialized_tags: String = serde_json::to_string(&tags).unwrap_or_else(|_| "[]".to_string());
let encrypted_tags: String = encrypt_data_with_user_key(&serialized_tags, &user_key)?;
let encrypted_power_level: Option<String> = if let Some(power_level_val) = power_level { Some(encrypt_data_with_user_key(power_level_val, &user_key)?) } else { None };
let encrypted_components: Option<String> = if let Some(components_val) = components { Some(encrypt_data_with_user_key(components_val, &user_key)?) } else { None };
let encrypted_limitations: Option<String> = if let Some(limitations_val) = limitations { Some(encrypt_data_with_user_key(limitations_val, &user_key)?) } else { None };
let encrypted_notes: Option<String> = if let Some(notes_val) = notes { Some(encrypt_data_with_user_key(notes_val, &user_key)?) } else { None };
let last_update: i64 = timestamp_in_seconds();
repo::update_spell(
conn, user_id, spell_id, &encrypted_name, &name_hash,
Some(&encrypted_description), Some(&encrypted_appearance), &encrypted_tags,
encrypted_power_level.as_deref(), encrypted_components.as_deref(),
encrypted_limitations.as_deref(), encrypted_notes.as_deref(),
last_update, lang, series_spell_id,
)
}
/// Deletes a spell.
/// * `conn` - Database connection
/// * `user_id` - The unique identifier of the user
/// * `book_id` - The unique identifier of the book
/// * `spell_id` - The unique identifier of the spell
/// * `deleted_at` - The timestamp of deletion
/// * `lang` - The language for error messages ("fr" or "en")
/// Returns true if the deletion was successful.
pub fn delete_spell(conn: &Connection, user_id: &str, book_id: &str, spell_id: &str, deleted_at: i64, lang: Lang) -> AppResult<bool> {
let deleted: bool = repo::delete_spell(conn, user_id, spell_id, lang)?;
if deleted {
let removal_id: String = create_unique_id(None);
tombstone_repo::insert(conn, &removal_id, "book_spells", spell_id, Some(book_id), user_id, deleted_at, lang)?;
}
Ok(deleted)
}