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,123 @@
use serde::Deserialize;
use tauri::State;
use crate::db::connection::DbManager;
use crate::domains::character::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 GetCharacterListData {
pub book_id: String,
pub enabled: bool,
}
#[tauri::command]
pub fn get_character_list(data: GetCharacterListData, db: State<DbManager>, session: State<SessionState>) -> Result<service::CharacterListResponse, 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_character_list(conn, &user_id, &data.book_id, lang)
}
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct GetCharacterAttributesData {
pub character_id: String,
}
#[tauri::command]
pub fn get_character_attributes(data: GetCharacterAttributesData, db: State<DbManager>, session: State<SessionState>) -> Result<Vec<service::CharacterAttribute>, 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_attributes(conn, &data.character_id, &user_id, lang)
}
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CreateCharacterData {
pub character: service::CharacterPropsPost,
pub book_id: String,
pub id: Option<String>,
}
#[tauri::command]
pub fn create_character(data: CreateCharacterData, db: State<DbManager>, session: State<SessionState>) -> Result<String, 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_new_character(conn, &user_id, &data.character, &data.book_id, lang, data.id.as_deref())
}
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct AddCharacterAttributeData {
pub character_id: String,
pub r#type: String,
pub name: String,
pub id: Option<String>,
}
#[tauri::command]
pub fn add_character_attribute(data: AddCharacterAttributeData, db: State<DbManager>, session: State<SessionState>) -> Result<String, 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_new_attribute(conn, &data.character_id, &user_id, &data.r#type, &data.name, lang, data.id.as_deref())
}
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct DeleteCharacterAttributeData {
pub attribute_id: String,
pub book_id: String,
pub deleted_at: i64,
}
#[tauri::command]
pub fn delete_character_attribute(data: DeleteCharacterAttributeData, 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_attribute(conn, &user_id, &data.book_id, &data.attribute_id, data.deleted_at, lang)
}
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct UpdateCharacterData {
pub character: service::CharacterPropsPost,
}
#[tauri::command]
pub fn update_character(data: UpdateCharacterData, 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_character(conn, &user_id, &data.character, lang)
}
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct DeleteCharacterData {
pub character_id: String,
pub book_id: String,
pub deleted_at: i64,
}
#[tauri::command]
pub fn delete_character(data: DeleteCharacterData, 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_character(conn, &user_id, &data.book_id, &data.character_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,663 @@
use rusqlite::{params, Connection};
use crate::error::{AppError, AppResult};
use crate::shared::types::Lang;
pub struct BookCharactersTable {
pub character_id: String,
pub book_id: String,
pub user_id: String,
pub first_name: String,
pub last_name: Option<String>,
pub nickname: Option<String>,
pub age: Option<String>,
pub gender: Option<String>,
pub species: Option<String>,
pub nationality: Option<String>,
pub status: Option<String>,
pub category: String,
pub title: Option<String>,
pub image: Option<String>,
pub role: Option<String>,
pub biography: Option<String>,
pub history: Option<String>,
pub speech_pattern: Option<String>,
pub catchphrase: Option<String>,
pub residence: Option<String>,
pub notes: Option<String>,
pub color: Option<String>,
pub last_update: i64,
}
pub struct SyncedCharacterResult {
pub character_id: String,
pub book_id: String,
pub first_name: String,
pub last_update: i64,
}
pub struct SyncedCharacterAttributeResult {
pub attr_id: String,
pub character_id: String,
pub attribute_name: String,
pub last_update: i64,
}
pub struct BookCharactersAttributesTable {
pub attr_id: String,
pub character_id: String,
pub user_id: String,
pub attribute_name: String,
pub attribute_value: String,
pub last_update: i64,
}
pub struct CharacterResult {
pub character_id: String,
pub first_name: String,
pub last_name: String,
pub nickname: Option<String>,
pub age: Option<String>,
pub gender: Option<String>,
pub species: Option<String>,
pub nationality: Option<String>,
pub status: Option<String>,
pub title: String,
pub category: String,
pub image: String,
pub role: String,
pub biography: String,
pub history: String,
pub speech_pattern: Option<String>,
pub catchphrase: Option<String>,
pub residence: Option<String>,
pub notes: Option<String>,
pub color: Option<String>,
pub series_character_id: Option<String>,
}
pub struct AttributeResult {
pub attr_id: String,
pub attribute_name: String,
pub attribute_value: String,
}
pub struct CompleteCharacterResult {
pub character_id: String,
pub first_name: String,
pub last_name: String,
pub nickname: Option<String>,
pub age: Option<String>,
pub gender: Option<String>,
pub species: Option<String>,
pub nationality: Option<String>,
pub status: Option<String>,
pub category: String,
pub title: String,
pub role: String,
pub biography: String,
pub history: String,
pub speech_pattern: Option<String>,
pub catchphrase: Option<String>,
pub residence: Option<String>,
pub notes: Option<String>,
pub color: Option<String>,
pub attribute_name: String,
pub attribute_value: String,
}
pub struct CharacterData {
pub first_name: String,
pub last_name: Option<String>,
pub nickname: Option<String>,
pub age: Option<String>,
pub gender: Option<String>,
pub species: Option<String>,
pub nationality: Option<String>,
pub status: Option<String>,
pub title: Option<String>,
pub category: Option<String>,
pub image: Option<String>,
pub role: Option<String>,
pub biography: Option<String>,
pub history: Option<String>,
pub speech_pattern: Option<String>,
pub catchphrase: Option<String>,
pub residence: Option<String>,
pub notes: Option<String>,
pub color: Option<String>,
}
pub struct SyncCharacterData {
pub first_name: String,
pub last_name: Option<String>,
pub nickname: Option<String>,
pub age: Option<String>,
pub gender: Option<String>,
pub species: Option<String>,
pub nationality: Option<String>,
pub status: Option<String>,
pub category: String,
pub title: Option<String>,
pub image: Option<String>,
pub role: Option<String>,
pub biography: Option<String>,
pub history: Option<String>,
pub speech_pattern: Option<String>,
pub catchphrase: Option<String>,
pub residence: Option<String>,
pub notes: Option<String>,
pub color: Option<String>,
}
/// Fetches all characters for a specific book and 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 ("fr" or "en")
/// Returns an array of character results.
pub fn fetch_characters(conn: &Connection, user_id: &str, book_id: &str, lang: Lang) -> AppResult<Vec<CharacterResult>> {
let mut statement = conn
.prepare("SELECT character_id, first_name, last_name, nickname, age, gender, species, nationality, status, title, category, image, role, biography, history, speech_pattern, catchphrase, residence, notes, color, series_character_id FROM book_characters WHERE book_id=?1 AND user_id=?2")
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les personnages.".to_string() } else { "Unable to retrieve characters.".to_string() }))?;
let characters = statement
.query_map(params![book_id, user_id], |query_row| {
Ok(CharacterResult {
character_id: query_row.get(0)?, first_name: query_row.get(1)?,
last_name: query_row.get(2)?, nickname: query_row.get(3)?,
age: query_row.get(4)?, gender: query_row.get(5)?,
species: query_row.get(6)?, nationality: query_row.get(7)?,
status: query_row.get(8)?, title: query_row.get(9)?,
category: query_row.get(10)?, image: query_row.get(11)?,
role: query_row.get(12)?, biography: query_row.get(13)?,
history: query_row.get(14)?, speech_pattern: query_row.get(15)?,
catchphrase: query_row.get(16)?, residence: query_row.get(17)?,
notes: query_row.get(18)?, color: query_row.get(19)?,
series_character_id: query_row.get(20)?,
})
})
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les personnages.".to_string() } else { "Unable to retrieve characters.".to_string() }))?
.collect::<Result<Vec<_>, _>>()
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les personnages.".to_string() } else { "Unable to retrieve characters.".to_string() }))?;
Ok(characters)
}
/// Adds a new character to the database.
/// * `conn` - Database connection
/// * `user_id` - The unique identifier of the user
/// * `character_id` - The unique identifier for the new character
/// * `character_data` - Object containing all encrypted character fields
/// * `book_id` - The unique identifier of the book
/// * `lang` - The language for error messages ("fr" or "en")
/// * `series_character_id` - Optional series character identifier
/// Returns the character ID if successful.
pub fn add_new_character(
conn: &Connection, user_id: &str, character_id: &str, character_data: &CharacterData,
book_id: &str, lang: Lang, series_character_id: Option<&str>, last_update: i64,
) -> AppResult<String> {
let insert_result = if let Some(series_id) = series_character_id {
conn.execute(
"INSERT INTO book_characters (character_id, book_id, user_id, first_name, last_name, nickname, age, gender, species, nationality, status, category, title, image, role, biography, history, speech_pattern, catchphrase, residence, notes, color, series_character_id, last_update) VALUES (?1,?2,?3,?4,?5,?6,?7,?8,?9,?10,?11,?12,?13,?14,?15,?16,?17,?18,?19,?20,?21,?22,?23,?24)",
params![character_id, book_id, user_id, character_data.first_name, character_data.last_name, character_data.nickname, character_data.age, character_data.gender, character_data.species, character_data.nationality, character_data.status, character_data.category, character_data.title, character_data.image, character_data.role, character_data.biography, character_data.history, character_data.speech_pattern, character_data.catchphrase, character_data.residence, character_data.notes, character_data.color, series_id, last_update],
)
} else {
conn.execute(
"INSERT INTO book_characters (character_id, book_id, user_id, first_name, last_name, nickname, age, gender, species, nationality, status, category, title, image, role, biography, history, speech_pattern, catchphrase, residence, notes, color, last_update) VALUES (?1,?2,?3,?4,?5,?6,?7,?8,?9,?10,?11,?12,?13,?14,?15,?16,?17,?18,?19,?20,?21,?22,?23)",
params![character_id, book_id, user_id, character_data.first_name, character_data.last_name, character_data.nickname, character_data.age, character_data.gender, character_data.species, character_data.nationality, character_data.status, character_data.category, character_data.title, character_data.image, character_data.role, character_data.biography, character_data.history, character_data.speech_pattern, character_data.catchphrase, character_data.residence, character_data.notes, character_data.color, last_update],
)
};
let insert_result = insert_result
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible d'ajouter le personnage.".to_string() } else { "Unable to add character.".to_string() }))?;
if insert_result > 0 {
Ok(character_id.to_string())
} else {
Err(AppError::Internal(if lang == Lang::Fr { "Une erreur s'est produite lors de l'ajout du personnage.".to_string() } else { "Error adding character.".to_string() }))
}
}
/// Inserts a new attribute for a character.
/// * `conn` - Database connection
/// * `attribute_id` - The unique identifier for the new attribute
/// * `character_id` - The unique identifier of the character
/// * `user_id` - The unique identifier of the user
/// * `attribute_type` - The attribute name/type
/// * `name` - The attribute value
/// * `last_update` - The timestamp of the last update
/// * `lang` - The language for error messages ("fr" or "en")
/// Returns the attribute ID if successful.
pub fn insert_attribute(
conn: &Connection, attribute_id: &str, character_id: &str, user_id: &str,
attribute_type: &str, name: &str, last_update: i64, lang: Lang,
) -> AppResult<String> {
let insert_result = conn
.execute(
"INSERT INTO book_characters_attributes (attr_id, character_id, user_id, attribute_name, attribute_value, last_update) VALUES (?1,?2,?3,?4,?5,?6)",
params![attribute_id, character_id, user_id, attribute_type, name, last_update],
)
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible d'ajouter l'attribut.".to_string() } else { "Unable to add attribute.".to_string() }))?;
if insert_result > 0 {
Ok(attribute_id.to_string())
} else {
Err(AppError::Internal(if lang == Lang::Fr { "Une erreur s'est produite lors de l'ajout de l'attribut.".to_string() } else { "Error adding attribute.".to_string() }))
}
}
/// Updates an existing character's information.
/// * `conn` - Database connection
/// * `user_id` - The unique identifier of the user
/// * `id` - The unique identifier of the character to update
/// * `character_data` - Object containing all encrypted character fields
/// * `last_update` - The timestamp of the last update
/// * `lang` - The language for error messages ("fr" or "en")
/// * `series_character_id` - Optional series character identifier
/// Returns true if the update was successful, false otherwise.
pub fn update_character(
conn: &Connection, user_id: &str, id: &str, character_data: &CharacterData,
last_update: i64, lang: Lang, series_character_id: Option<&str>,
) -> AppResult<bool> {
let update_result = if let Some(series_id) = series_character_id {
conn.execute(
"UPDATE book_characters SET first_name=?1, last_name=?2, nickname=?3, age=?4, gender=?5, species=?6, nationality=?7, status=?8, title=?9, category=?10, image=?11, role=?12, biography=?13, history=?14, speech_pattern=?15, catchphrase=?16, residence=?17, notes=?18, color=?19, series_character_id=?20, last_update=?21 WHERE character_id=?22 AND user_id=?23",
params![character_data.first_name, character_data.last_name, character_data.nickname, character_data.age, character_data.gender, character_data.species, character_data.nationality, character_data.status, character_data.title, character_data.category, character_data.image, character_data.role, character_data.biography, character_data.history, character_data.speech_pattern, character_data.catchphrase, character_data.residence, character_data.notes, character_data.color, series_id, last_update, id, user_id],
)
} else {
conn.execute(
"UPDATE book_characters SET first_name=?1, last_name=?2, nickname=?3, age=?4, gender=?5, species=?6, nationality=?7, status=?8, title=?9, category=?10, image=?11, role=?12, biography=?13, history=?14, speech_pattern=?15, catchphrase=?16, residence=?17, notes=?18, color=?19, last_update=?20 WHERE character_id=?21 AND user_id=?22",
params![character_data.first_name, character_data.last_name, character_data.nickname, character_data.age, character_data.gender, character_data.species, character_data.nationality, character_data.status, character_data.title, character_data.category, character_data.image, character_data.role, character_data.biography, character_data.history, character_data.speech_pattern, character_data.catchphrase, character_data.residence, character_data.notes, character_data.color, last_update, id, user_id],
)
};
let update_result = update_result
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de mettre à jour le personnage.".to_string() } else { "Unable to update character.".to_string() }))?;
Ok(update_result > 0)
}
/// Deletes a character and all its related data (attributes) from the database.
/// * `conn` - Database connection
/// * `user_id` - The unique identifier of the user
/// * `character_id` - The unique identifier of the character to delete
/// * `lang` - The language for error messages ("fr" or "en")
/// Returns true if the deletion was successful, false otherwise.
pub fn delete_character(conn: &Connection, user_id: &str, character_id: &str, lang: Lang) -> AppResult<bool> {
conn.execute(
"DELETE FROM book_characters_attributes WHERE character_id=?1 AND user_id=?2",
params![character_id, user_id],
)
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de supprimer le personnage.".to_string() } else { "Unable to delete character.".to_string() }))?;
let delete_result = conn
.execute(
"DELETE FROM book_characters WHERE character_id=?1 AND user_id=?2",
params![character_id, user_id],
)
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de supprimer le personnage.".to_string() } else { "Unable to delete character.".to_string() }))?;
Ok(delete_result > 0)
}
/// Deletes a character attribute from the database.
/// * `conn` - Database connection
/// * `user_id` - The unique identifier of the user
/// * `attribute_id` - The unique identifier of the attribute to delete
/// * `lang` - The language for error messages ("fr" or "en")
/// Returns true if the deletion was successful, false otherwise.
pub fn delete_attribute(conn: &Connection, user_id: &str, attribute_id: &str, lang: Lang) -> AppResult<bool> {
let delete_result = conn
.execute(
"DELETE FROM book_characters_attributes WHERE attr_id=?1 AND user_id=?2",
params![attribute_id, user_id],
)
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de supprimer l'attribut.".to_string() } else { "Unable to delete attribute.".to_string() }))?;
Ok(delete_result > 0)
}
/// Fetches all attributes for a specific character.
/// * `conn` - Database connection
/// * `character_id` - The unique identifier of the character
/// * `user_id` - The unique identifier of the user
/// * `lang` - The language for error messages ("fr" or "en")
/// Returns an array of attribute results.
pub fn fetch_attributes(conn: &Connection, character_id: &str, user_id: &str, lang: Lang) -> AppResult<Vec<AttributeResult>> {
let mut statement = conn
.prepare("SELECT attr_id, attribute_name, attribute_value FROM book_characters_attributes WHERE character_id=?1 AND user_id=?2")
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les attributs.".to_string() } else { "Unable to retrieve attributes.".to_string() }))?;
let attributes = statement
.query_map(params![character_id, user_id], |query_row| {
Ok(AttributeResult { attr_id: query_row.get(0)?, attribute_name: query_row.get(1)?, attribute_value: query_row.get(2)? })
})
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les attributs.".to_string() } else { "Unable to retrieve attributes.".to_string() }))?
.collect::<Result<Vec<_>, _>>()
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les attributs.".to_string() } else { "Unable to retrieve attributes.".to_string() }))?;
Ok(attributes)
}
/// Fetches complete character information including attributes, optionally filtered by character IDs.
/// * `conn` - Database connection
/// * `user_id` - The unique identifier of the user
/// * `book_id` - The unique identifier of the book
/// * `tags` - An optional array of character IDs to filter by
/// * `lang` - The language for error messages ("fr" or "en")
/// Returns an array of complete character results with attributes.
pub fn fetch_complete_characters(conn: &Connection, user_id: &str, book_id: &str, tags: &[String], lang: Lang) -> AppResult<Vec<CompleteCharacterResult>> {
let mut query = "SELECT charac.character_id, first_name, last_name, nickname, age, gender, species, nationality, status, category, title, role, biography, history, speech_pattern, catchphrase, residence, notes, color, attribute_name, attribute_value FROM book_characters AS charac LEFT JOIN book_characters_attributes AS attr ON charac.character_id=attr.character_id WHERE charac.user_id=?1 AND charac.book_id=?2".to_string();
let mut param_values: Vec<Box<dyn rusqlite::types::ToSql>> = Vec::new();
param_values.push(Box::new(user_id.to_string()));
param_values.push(Box::new(book_id.to_string()));
if !tags.is_empty() {
let placeholders: String = tags.iter().enumerate().map(|(index, _)| format!("?{}", index + 3)).collect::<Vec<_>>().join(",");
query += &format!(" AND charac.character_id IN ({})", placeholders);
for tag in tags {
param_values.push(Box::new(tag.clone()));
}
}
let param_refs: Vec<&dyn rusqlite::types::ToSql> = param_values.iter().map(|param| param.as_ref()).collect();
let mut statement = conn
.prepare(&query)
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les personnages complets.".to_string() } else { "Unable to retrieve complete characters.".to_string() }))?;
let characters = statement
.query_map(param_refs.as_slice(), |query_row| {
Ok(CompleteCharacterResult {
character_id: query_row.get(0)?, first_name: query_row.get(1)?,
last_name: query_row.get(2)?, nickname: query_row.get(3)?,
age: query_row.get(4)?, gender: query_row.get(5)?,
species: query_row.get(6)?, nationality: query_row.get(7)?,
status: query_row.get(8)?, category: query_row.get(9)?,
title: query_row.get(10)?, role: query_row.get(11)?,
biography: query_row.get(12)?, history: query_row.get(13)?,
speech_pattern: query_row.get(14)?, catchphrase: query_row.get(15)?,
residence: query_row.get(16)?, notes: query_row.get(17)?,
color: query_row.get(18)?, attribute_name: query_row.get(19)?,
attribute_value: query_row.get(20)?,
})
})
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les personnages complets.".to_string() } else { "Unable to retrieve complete characters.".to_string() }))?
.collect::<Result<Vec<_>, _>>()
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les personnages complets.".to_string() } else { "Unable to retrieve complete characters.".to_string() }))?;
if characters.is_empty() {
return Err(AppError::NotFound(if lang == Lang::Fr { "Aucun personnage complet trouvé.".to_string() } else { "No complete characters found.".to_string() }));
}
Ok(characters)
}
/// Updates an existing character attribute.
/// * `conn` - Database connection
/// * `user_id` - The unique identifier of the user
/// * `character_attribute_id` - The unique identifier of the attribute to update
/// * `attribute_name` - The new attribute name
/// * `attribute_value` - The new attribute value
/// * `last_update` - The timestamp of the last update
/// * `lang` - The language for error messages ("fr" or "en")
/// Returns true if the update was successful, false otherwise.
pub fn update_character_attribute(
conn: &Connection, user_id: &str, character_attribute_id: &str,
attribute_name: &str, attribute_value: &str, last_update: i64, lang: Lang,
) -> AppResult<bool> {
let update_result = conn
.execute(
"UPDATE book_characters_attributes SET attribute_name=?1, attribute_value=?2, last_update=?3 WHERE attr_id=?4 AND user_id=?5",
params![attribute_name, attribute_value, last_update, character_attribute_id, user_id],
)
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de mettre à jour l'attribut du personnage.".to_string() } else { "Unable to update character attribute.".to_string() }))?;
Ok(update_result > 0)
}
/// Checks if a character exists in the database.
/// * `conn` - Database connection
/// * `user_id` - The unique identifier of the user
/// * `character_id` - The unique identifier of the character to check
/// * `lang` - The language for error messages ("fr" or "en")
/// Returns true if the character exists, false otherwise.
pub fn is_character_exist(conn: &Connection, user_id: &str, character_id: &str, lang: Lang) -> AppResult<bool> {
let mut statement = conn
.prepare("SELECT 1 FROM book_characters WHERE character_id=?1 AND user_id=?2")
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de vérifier l'existence du personnage.".to_string() } else { "Unable to check character existence.".to_string() }))?;
let exists = statement
.query_row(params![character_id, user_id], |_query_row| Ok(true))
.unwrap_or(false);
Ok(exists)
}
/// Checks if a character attribute exists in the database.
/// * `conn` - Database connection
/// * `user_id` - The unique identifier of the user
/// * `character_attribute_id` - The unique identifier of the attribute to check
/// * `lang` - The language for error messages ("fr" or "en")
/// Returns true if the attribute exists, false otherwise.
pub fn is_character_attribute_exist(conn: &Connection, user_id: &str, character_attribute_id: &str, lang: Lang) -> AppResult<bool> {
let mut statement = conn
.prepare("SELECT 1 FROM book_characters_attributes WHERE attr_id=?1 AND user_id=?2")
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de vérifier l'existence de l'attribut du personnage.".to_string() } else { "Unable to check character attribute existence.".to_string() }))?;
let exists = statement
.query_row(params![character_attribute_id, user_id], |_query_row| Ok(true))
.unwrap_or(false);
Ok(exists)
}
/// Fetches all characters 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 book characters.
pub fn fetch_book_characters(conn: &Connection, user_id: &str, book_id: &str, lang: Lang) -> AppResult<Vec<BookCharactersTable>> {
let mut statement = conn
.prepare("SELECT character_id, book_id, user_id, first_name, last_name, nickname, age, gender, species, nationality, status, category, title, image, role, biography, history, speech_pattern, catchphrase, residence, notes, color, last_update FROM book_characters WHERE user_id=?1 AND book_id=?2")
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les personnages.".to_string() } else { "Unable to retrieve characters.".to_string() }))?;
let characters = statement
.query_map(params![user_id, book_id], |query_row| {
Ok(BookCharactersTable {
character_id: query_row.get(0)?, book_id: query_row.get(1)?,
user_id: query_row.get(2)?, first_name: query_row.get(3)?,
last_name: query_row.get(4)?, nickname: query_row.get(5)?,
age: query_row.get(6)?, gender: query_row.get(7)?,
species: query_row.get(8)?, nationality: query_row.get(9)?,
status: query_row.get(10)?, category: query_row.get(11)?,
title: query_row.get(12)?, image: query_row.get(13)?,
role: query_row.get(14)?, biography: query_row.get(15)?,
history: query_row.get(16)?, speech_pattern: query_row.get(17)?,
catchphrase: query_row.get(18)?, residence: query_row.get(19)?,
notes: query_row.get(20)?, color: query_row.get(21)?,
last_update: query_row.get(22)?,
})
})
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les personnages.".to_string() } else { "Unable to retrieve characters.".to_string() }))?
.collect::<Result<Vec<_>, _>>()
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les personnages.".to_string() } else { "Unable to retrieve characters.".to_string() }))?;
Ok(characters)
}
/// Fetches all attributes for a specific character.
/// * `conn` - Database connection
/// * `user_id` - The unique identifier of the user
/// * `character_id` - The unique identifier of the character
/// * `lang` - The language for error messages ("fr" or "en")
/// Returns an array of character attributes.
pub fn fetch_book_characters_attributes(conn: &Connection, user_id: &str, character_id: &str, lang: Lang) -> AppResult<Vec<BookCharactersAttributesTable>> {
let mut statement = conn
.prepare("SELECT attr_id, character_id, user_id, attribute_name, attribute_value, last_update FROM book_characters_attributes WHERE user_id=?1 AND character_id=?2")
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les attributs des personnages.".to_string() } else { "Unable to retrieve character attributes.".to_string() }))?;
let attributes = statement
.query_map(params![user_id, character_id], |query_row| {
Ok(BookCharactersAttributesTable {
attr_id: query_row.get(0)?, character_id: query_row.get(1)?,
user_id: query_row.get(2)?, attribute_name: query_row.get(3)?,
attribute_value: query_row.get(4)?, last_update: query_row.get(5)?,
})
})
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les attributs des personnages.".to_string() } else { "Unable to retrieve character attributes.".to_string() }))?
.collect::<Result<Vec<_>, _>>()
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les attributs des personnages.".to_string() } else { "Unable to retrieve character attributes.".to_string() }))?;
Ok(attributes)
}
/// Fetches all synced characters for a user.
/// * `conn` - Database connection
/// * `user_id` - The unique identifier of the user
/// * `lang` - The language for error messages ("fr" or "en")
/// Returns an array of synced character results.
pub fn fetch_synced_characters(conn: &Connection, user_id: &str, lang: Lang) -> AppResult<Vec<SyncedCharacterResult>> {
let mut statement = conn
.prepare("SELECT character_id, book_id, first_name, last_update FROM book_characters WHERE user_id = ?1")
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les personnages synchronisés.".to_string() } else { "Unable to retrieve synced characters.".to_string() }))?;
let synced_characters = statement
.query_map(params![user_id], |query_row| {
Ok(SyncedCharacterResult { character_id: query_row.get(0)?, book_id: query_row.get(1)?, first_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 personnages synchronisés.".to_string() } else { "Unable to retrieve synced characters.".to_string() }))?
.collect::<Result<Vec<_>, _>>()
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les personnages synchronisés.".to_string() } else { "Unable to retrieve synced characters.".to_string() }))?;
Ok(synced_characters)
}
/// Fetches all synced character attributes for a user.
/// * `conn` - Database connection
/// * `user_id` - The unique identifier of the user
/// * `lang` - The language for error messages ("fr" or "en")
/// Returns an array of synced character attribute results.
pub fn fetch_synced_character_attributes(conn: &Connection, user_id: &str, lang: Lang) -> AppResult<Vec<SyncedCharacterAttributeResult>> {
let mut statement = conn
.prepare("SELECT attr_id, character_id, attribute_name, last_update FROM book_characters_attributes WHERE user_id = ?1")
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les attributs des personnages synchronisés.".to_string() } else { "Unable to retrieve synced character attributes.".to_string() }))?;
let synced_attributes = statement
.query_map(params![user_id], |query_row| {
Ok(SyncedCharacterAttributeResult { attr_id: query_row.get(0)?, character_id: query_row.get(1)?, attribute_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 attributs des personnages synchronisés.".to_string() } else { "Unable to retrieve synced character attributes.".to_string() }))?
.collect::<Result<Vec<_>, _>>()
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les attributs des personnages synchronisés.".to_string() } else { "Unable to retrieve synced character attributes.".to_string() }))?;
Ok(synced_attributes)
}
/// Inserts a synced character into the database.
/// * `conn` - Database connection
/// * `character_id` - The unique identifier of the character
/// * `book_id` - The unique identifier of the book
/// * `user_id` - The unique identifier of the user
/// * `character_data` - Object containing all character fields
/// * `last_update` - The timestamp of the last update
/// * `lang` - The language for error messages ("fr" or "en")
/// Returns true if the insertion was successful, false otherwise.
pub fn insert_sync_character(
conn: &Connection, character_id: &str, book_id: &str, user_id: &str,
character_data: &SyncCharacterData, last_update: i64, lang: Lang,
) -> AppResult<bool> {
let insert_result = conn
.execute(
"INSERT INTO book_characters (character_id, book_id, user_id, first_name, last_name, nickname, age, gender, species, nationality, status, category, title, image, role, biography, history, speech_pattern, catchphrase, residence, notes, color, last_update) VALUES (?1,?2,?3,?4,?5,?6,?7,?8,?9,?10,?11,?12,?13,?14,?15,?16,?17,?18,?19,?20,?21,?22,?23)",
params![character_id, book_id, user_id, character_data.first_name, character_data.last_name, character_data.nickname, character_data.age, character_data.gender, character_data.species, character_data.nationality, character_data.status, character_data.category, character_data.title, character_data.image, character_data.role, character_data.biography, character_data.history, character_data.speech_pattern, character_data.catchphrase, character_data.residence, character_data.notes, character_data.color, last_update],
)
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible d'insérer le personnage.".to_string() } else { "Unable to insert character.".to_string() }))?;
Ok(insert_result > 0)
}
/// Inserts a synced character attribute into the database.
/// * `conn` - Database connection
/// * `attr_id` - The unique identifier of the attribute
/// * `character_id` - The unique identifier of the character
/// * `user_id` - The unique identifier of the user
/// * `attribute_name` - The name of the attribute
/// * `attribute_value` - The value of the attribute
/// * `last_update` - The timestamp of the last update
/// * `lang` - The language for error messages ("fr" or "en")
/// Returns true if the insertion was successful, false otherwise.
pub fn insert_sync_character_attribute(
conn: &Connection, attr_id: &str, character_id: &str, user_id: &str,
attribute_name: &str, attribute_value: &str, last_update: i64, lang: Lang,
) -> AppResult<bool> {
let insert_result = conn
.execute(
"INSERT INTO book_characters_attributes (attr_id, character_id, user_id, attribute_name, attribute_value, last_update) VALUES (?1,?2,?3,?4,?5,?6)",
params![attr_id, character_id, user_id, attribute_name, attribute_value, last_update],
)
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible d'insérer l'attribut du personnage.".to_string() } else { "Unable to insert character attribute.".to_string() }))?;
Ok(insert_result > 0)
}
/// Fetches a complete character by its ID.
/// * `conn` - Database connection
/// * `id` - The unique identifier of the character
/// * `lang` - The language for error messages ("fr" or "en")
/// Returns an array of book characters (typically one).
pub fn fetch_complete_character_by_id(conn: &Connection, id: &str, lang: Lang) -> AppResult<Vec<BookCharactersTable>> {
let mut statement = conn
.prepare("SELECT character_id, book_id, user_id, first_name, last_name, nickname, age, gender, species, nationality, status, category, title, image, role, biography, history, speech_pattern, catchphrase, residence, notes, color, last_update FROM book_characters WHERE character_id = ?1")
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer le personnage complet.".to_string() } else { "Unable to retrieve complete character.".to_string() }))?;
let character = statement
.query_map(params![id], |query_row| {
Ok(BookCharactersTable {
character_id: query_row.get(0)?, book_id: query_row.get(1)?,
user_id: query_row.get(2)?, first_name: query_row.get(3)?,
last_name: query_row.get(4)?, nickname: query_row.get(5)?,
age: query_row.get(6)?, gender: query_row.get(7)?,
species: query_row.get(8)?, nationality: query_row.get(9)?,
status: query_row.get(10)?, category: query_row.get(11)?,
title: query_row.get(12)?, image: query_row.get(13)?,
role: query_row.get(14)?, biography: query_row.get(15)?,
history: query_row.get(16)?, speech_pattern: query_row.get(17)?,
catchphrase: query_row.get(18)?, residence: query_row.get(19)?,
notes: query_row.get(20)?, color: query_row.get(21)?,
last_update: query_row.get(22)?,
})
})
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer le personnage complet.".to_string() } else { "Unable to retrieve complete character.".to_string() }))?
.collect::<Result<Vec<_>, _>>()
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer le personnage complet.".to_string() } else { "Unable to retrieve complete character.".to_string() }))?;
Ok(character)
}
/// Fetches a complete character attribute by its ID.
/// * `conn` - Database connection
/// * `id` - The unique identifier of the attribute
/// * `lang` - The language for error messages ("fr" or "en")
/// Returns an array of character attributes (typically one).
pub fn fetch_complete_character_attribute_by_id(conn: &Connection, id: &str, lang: Lang) -> AppResult<Vec<BookCharactersAttributesTable>> {
let mut statement = conn
.prepare("SELECT attr_id, character_id, user_id, attribute_name, attribute_value, last_update FROM book_characters_attributes WHERE attr_id = ?1")
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer l'attribut de personnage complet.".to_string() } else { "Unable to retrieve complete character attribute.".to_string() }))?;
let attribute = statement
.query_map(params![id], |query_row| {
Ok(BookCharactersAttributesTable {
attr_id: query_row.get(0)?, character_id: query_row.get(1)?,
user_id: query_row.get(2)?, attribute_name: query_row.get(3)?,
attribute_value: query_row.get(4)?, last_update: query_row.get(5)?,
})
})
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer l'attribut de personnage complet.".to_string() } else { "Unable to retrieve complete character attribute.".to_string() }))?
.collect::<Result<Vec<_>, _>>()
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer l'attribut de personnage complet.".to_string() } else { "Unable to retrieve complete character attribute.".to_string() }))?;
Ok(attribute)
}

View File

@@ -0,0 +1,656 @@
use std::collections::HashMap;
use rusqlite::Connection;
use serde::{Deserialize, Serialize};
use crate::crypto::encryption::{encrypt_data_with_user_key, decrypt_data_with_user_key};
use crate::crypto::key_manager::get_user_encryption_key;
use crate::domains::book::repo as book_repo;
use crate::domains::character::repo;
use crate::domains::tombstone::repo as tombstone_repo;
use crate::error::AppResult;
use crate::helpers::{create_unique_id, timestamp_in_seconds};
use crate::shared::types::Lang;
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CharacterPropsPost {
pub id: Option<String>,
pub name: String,
pub last_name: String,
pub nickname: String,
pub age: Option<i64>,
pub gender: String,
pub species: String,
pub nationality: String,
pub status: String,
pub category: String,
pub title: String,
pub image: String,
pub physical: Vec<AttributeName>,
pub psychological: Vec<AttributeName>,
pub relations: Vec<AttributeName>,
pub skills: Vec<AttributeName>,
pub weaknesses: Vec<AttributeName>,
pub strengths: Vec<AttributeName>,
pub goals: Vec<AttributeName>,
pub motivations: Vec<AttributeName>,
pub arc: Vec<AttributeName>,
pub secrets: Vec<AttributeName>,
pub fears: Vec<AttributeName>,
pub flaws: Vec<AttributeName>,
pub beliefs: Vec<AttributeName>,
pub conflicts: Vec<AttributeName>,
pub quotes: Vec<AttributeName>,
pub distinguishing_marks: Vec<AttributeName>,
pub items: Vec<AttributeName>,
pub affiliations: Vec<AttributeName>,
pub role: String,
pub biography: Option<String>,
pub history: Option<String>,
pub speech_pattern: Option<String>,
pub catchphrase: Option<String>,
pub residence: Option<String>,
pub notes: Option<String>,
pub color: Option<String>,
pub series_character_id: Option<String>,
}
#[derive(Deserialize)]
pub struct AttributeName {
pub name: String,
}
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
pub struct CharacterProps {
pub id: String,
pub name: String,
pub last_name: String,
pub nickname: String,
pub age: Option<i64>,
pub gender: String,
pub species: String,
pub nationality: String,
pub status: String,
pub title: String,
pub category: String,
pub image: String,
pub role: String,
pub biography: String,
pub history: String,
pub speech_pattern: String,
pub catchphrase: String,
pub residence: String,
pub notes: String,
pub color: String,
pub series_character_id: Option<String>,
}
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
pub struct CharacterListResponse {
pub characters: Vec<CharacterProps>,
pub enabled: bool,
}
pub struct CompleteCharacterProps {
pub id: Option<String>,
pub name: String,
pub last_name: String,
pub nickname: String,
pub age: Option<i64>,
pub gender: String,
pub species: String,
pub nationality: String,
pub status: String,
pub title: String,
pub category: String,
pub image: Option<String>,
pub role: String,
pub biography: String,
pub history: String,
pub speech_pattern: String,
pub catchphrase: String,
pub residence: String,
pub notes: String,
pub color: String,
pub physical: Vec<Attribute>,
pub psychological: Vec<Attribute>,
pub relations: Vec<Attribute>,
pub skills: Vec<Attribute>,
pub weaknesses: Vec<Attribute>,
pub strengths: Vec<Attribute>,
pub goals: Vec<Attribute>,
pub motivations: Vec<Attribute>,
pub arc: Vec<Attribute>,
pub secrets: Vec<Attribute>,
pub fears: Vec<Attribute>,
pub flaws: Vec<Attribute>,
pub beliefs: Vec<Attribute>,
pub conflicts: Vec<Attribute>,
pub quotes: Vec<Attribute>,
pub distinguishing_marks: Vec<Attribute>,
pub items: Vec<Attribute>,
pub affiliations: Vec<Attribute>,
}
#[derive(Serialize)]
pub struct Attribute {
pub id: String,
pub name: String,
}
#[derive(Serialize)]
pub struct CharacterAttribute {
pub r#type: String,
pub values: Vec<Attribute>,
}
pub struct SyncedCharacter {
pub id: String,
pub name: String,
pub last_update: i64,
pub attributes: Vec<SyncedCharacterAttribute>,
}
pub struct SyncedCharacterAttribute {
pub id: String,
pub name: String,
pub last_update: i64,
}
/// Retrieves a list of all characters for a specific book.
/// Decrypts character data using the user's encryption key.
/// * `conn` - Database connection
/// * `user_id` - The unique identifier of the user
/// * `book_id` - The unique identifier of the book
/// * `lang` - The language code for localization
/// Returns a CharacterListResponse containing decrypted characters and enabled status.
pub fn get_character_list(conn: &Connection, user_id: &str, book_id: &str, lang: Lang) -> AppResult<CharacterListResponse> {
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, |book_tools_row| book_tools_row.characters_enabled == 1);
let user_key: String = get_user_encryption_key(user_id)?;
let encrypted_characters: Vec<repo::CharacterResult> = repo::fetch_characters(conn, user_id, book_id, lang)?;
if encrypted_characters.is_empty() {
return Ok(CharacterListResponse { characters: vec![], enabled });
}
let mut decrypted_character_list: Vec<CharacterProps> = Vec::with_capacity(encrypted_characters.len());
for encrypted_character in encrypted_characters {
decrypted_character_list.push(CharacterProps {
id: encrypted_character.character_id,
name: if encrypted_character.first_name.is_empty() { String::new() } else { decrypt_data_with_user_key(&encrypted_character.first_name, &user_key)? },
last_name: if encrypted_character.last_name.is_empty() { String::new() } else { decrypt_data_with_user_key(&encrypted_character.last_name, &user_key)? },
nickname: if let Some(ref value) = encrypted_character.nickname { decrypt_data_with_user_key(value, &user_key)? } else { String::new() },
age: if let Some(ref value) = encrypted_character.age { Some(decrypt_data_with_user_key(value, &user_key)?.parse::<i64>().unwrap_or(0)) } else { None },
gender: if let Some(ref value) = encrypted_character.gender { decrypt_data_with_user_key(value, &user_key)? } else { String::new() },
species: if let Some(ref value) = encrypted_character.species { decrypt_data_with_user_key(value, &user_key)? } else { String::new() },
nationality: if let Some(ref value) = encrypted_character.nationality { decrypt_data_with_user_key(value, &user_key)? } else { String::new() },
status: if let Some(ref value) = encrypted_character.status { decrypt_data_with_user_key(value, &user_key)? } else { "alive".to_string() },
title: if encrypted_character.title.is_empty() { String::new() } else { decrypt_data_with_user_key(&encrypted_character.title, &user_key)? },
category: if encrypted_character.category.is_empty() { String::new() } else { decrypt_data_with_user_key(&encrypted_character.category, &user_key)? },
image: if encrypted_character.image.is_empty() { String::new() } else { decrypt_data_with_user_key(&encrypted_character.image, &user_key)? },
role: if encrypted_character.role.is_empty() { String::new() } else { decrypt_data_with_user_key(&encrypted_character.role, &user_key)? },
biography: if encrypted_character.biography.is_empty() { String::new() } else { decrypt_data_with_user_key(&encrypted_character.biography, &user_key)? },
history: if encrypted_character.history.is_empty() { String::new() } else { decrypt_data_with_user_key(&encrypted_character.history, &user_key)? },
speech_pattern: if let Some(ref value) = encrypted_character.speech_pattern { decrypt_data_with_user_key(value, &user_key)? } else { String::new() },
catchphrase: if let Some(ref value) = encrypted_character.catchphrase { decrypt_data_with_user_key(value, &user_key)? } else { String::new() },
residence: if let Some(ref value) = encrypted_character.residence { decrypt_data_with_user_key(value, &user_key)? } else { String::new() },
notes: if let Some(ref value) = encrypted_character.notes { decrypt_data_with_user_key(value, &user_key)? } else { String::new() },
color: if let Some(ref value) = encrypted_character.color { decrypt_data_with_user_key(value, &user_key)? } else { String::new() },
series_character_id: encrypted_character.series_character_id,
});
}
Ok(CharacterListResponse { characters: decrypted_character_list, enabled })
}
/// Creates a new character with all its attributes for a specific book.
/// Encrypts all character data before storing in the database.
/// * `conn` - Database connection
/// * `user_id` - The unique identifier of the user
/// * `character` - The character data to be created
/// * `book_id` - The unique identifier of the book
/// * `lang` - The language code for localization
/// * `existing_character_id` - Optional existing character ID for updates or imports
/// Returns the unique identifier of the newly created character.
pub fn add_new_character(conn: &Connection, user_id: &str, character: &CharacterPropsPost, book_id: &str, lang: Lang, existing_character_id: Option<&str>) -> AppResult<String> {
let user_key: String = get_user_encryption_key(user_id)?;
let character_id: String = create_unique_id(existing_character_id);
let last_update: i64 = timestamp_in_seconds();
let character_data = repo::CharacterData {
first_name: encrypt_data_with_user_key(&character.name, &user_key)?,
last_name: Some(encrypt_data_with_user_key(&character.last_name, &user_key)?),
nickname: Some(encrypt_data_with_user_key(if character.nickname.is_empty() { "" } else { &character.nickname }, &user_key)?),
age: if let Some(age_value) = character.age { Some(encrypt_data_with_user_key(&age_value.to_string(), &user_key)?) } else { None },
gender: Some(encrypt_data_with_user_key(if character.gender.is_empty() { "" } else { &character.gender }, &user_key)?),
species: Some(encrypt_data_with_user_key(if character.species.is_empty() { "" } else { &character.species }, &user_key)?),
nationality: Some(encrypt_data_with_user_key(if character.nationality.is_empty() { "" } else { &character.nationality }, &user_key)?),
status: Some(encrypt_data_with_user_key(if character.status.is_empty() { "alive" } else { &character.status }, &user_key)?),
title: Some(encrypt_data_with_user_key(&character.title, &user_key)?),
category: Some(encrypt_data_with_user_key(&character.category, &user_key)?),
image: Some(encrypt_data_with_user_key(&character.image, &user_key)?),
role: Some(encrypt_data_with_user_key(&character.role, &user_key)?),
biography: Some(encrypt_data_with_user_key(character.biography.as_deref().unwrap_or(""), &user_key)?),
history: Some(encrypt_data_with_user_key(character.history.as_deref().unwrap_or(""), &user_key)?),
speech_pattern: Some(encrypt_data_with_user_key(character.speech_pattern.as_deref().unwrap_or(""), &user_key)?),
catchphrase: Some(encrypt_data_with_user_key(character.catchphrase.as_deref().unwrap_or(""), &user_key)?),
residence: Some(encrypt_data_with_user_key(character.residence.as_deref().unwrap_or(""), &user_key)?),
notes: Some(encrypt_data_with_user_key(character.notes.as_deref().unwrap_or(""), &user_key)?),
color: Some(encrypt_data_with_user_key(character.color.as_deref().unwrap_or(""), &user_key)?),
};
let series_character_id: Option<&str> = character.series_character_id.as_deref();
repo::add_new_character(conn, user_id, &character_id, &character_data, book_id, lang, series_character_id, last_update)?;
let attribute_arrays: Vec<(&str, &Vec<AttributeName>)> = vec![
("physical", &character.physical),
("psychological", &character.psychological),
("relations", &character.relations),
("skills", &character.skills),
("weaknesses", &character.weaknesses),
("strengths", &character.strengths),
("goals", &character.goals),
("motivations", &character.motivations),
("arc", &character.arc),
("secrets", &character.secrets),
("fears", &character.fears),
("flaws", &character.flaws),
("beliefs", &character.beliefs),
("conflicts", &character.conflicts),
("quotes", &character.quotes),
("distinguishingMarks", &character.distinguishing_marks),
("items", &character.items),
("affiliations", &character.affiliations),
];
for (attribute_type, attribute_items) in attribute_arrays {
if !attribute_items.is_empty() {
for attribute_item in attribute_items {
add_new_attribute(conn, &character_id, user_id, attribute_type, &attribute_item.name, lang, None)?;
}
}
}
Ok(character_id)
}
/// Updates an existing character's core properties.
/// Encrypts all updated data before storing in the database.
/// * `conn` - Database connection
/// * `user_id` - The unique identifier of the user
/// * `character` - The character data with updated values
/// * `lang` - The language code for localization
/// Returns true if the update was successful, false otherwise.
pub fn update_character(conn: &Connection, user_id: &str, character: &CharacterPropsPost, lang: Lang) -> AppResult<bool> {
let user_key: String = get_user_encryption_key(user_id)?;
let character_id: &str = match character.id.as_deref() {
Some(id) => id,
None => return Ok(false),
};
let last_update: i64 = timestamp_in_seconds();
let character_data = repo::CharacterData {
first_name: encrypt_data_with_user_key(&character.name, &user_key)?,
last_name: Some(encrypt_data_with_user_key(&character.last_name, &user_key)?),
nickname: Some(encrypt_data_with_user_key(if character.nickname.is_empty() { "" } else { &character.nickname }, &user_key)?),
age: if let Some(age_value) = character.age { Some(encrypt_data_with_user_key(&age_value.to_string(), &user_key)?) } else { None },
gender: Some(encrypt_data_with_user_key(if character.gender.is_empty() { "" } else { &character.gender }, &user_key)?),
species: Some(encrypt_data_with_user_key(if character.species.is_empty() { "" } else { &character.species }, &user_key)?),
nationality: Some(encrypt_data_with_user_key(if character.nationality.is_empty() { "" } else { &character.nationality }, &user_key)?),
status: Some(encrypt_data_with_user_key(if character.status.is_empty() { "alive" } else { &character.status }, &user_key)?),
title: Some(encrypt_data_with_user_key(&character.title, &user_key)?),
category: Some(encrypt_data_with_user_key(&character.category, &user_key)?),
image: Some(encrypt_data_with_user_key(&character.image, &user_key)?),
role: Some(encrypt_data_with_user_key(&character.role, &user_key)?),
biography: Some(encrypt_data_with_user_key(character.biography.as_deref().unwrap_or(""), &user_key)?),
history: Some(encrypt_data_with_user_key(character.history.as_deref().unwrap_or(""), &user_key)?),
speech_pattern: Some(encrypt_data_with_user_key(character.speech_pattern.as_deref().unwrap_or(""), &user_key)?),
catchphrase: Some(encrypt_data_with_user_key(character.catchphrase.as_deref().unwrap_or(""), &user_key)?),
residence: Some(encrypt_data_with_user_key(character.residence.as_deref().unwrap_or(""), &user_key)?),
notes: Some(encrypt_data_with_user_key(character.notes.as_deref().unwrap_or(""), &user_key)?),
color: Some(encrypt_data_with_user_key(character.color.as_deref().unwrap_or(""), &user_key)?),
};
let series_character_id: Option<&str> = character.series_character_id.as_deref();
repo::update_character(conn, user_id, character_id, &character_data, last_update, lang, series_character_id)
}
/// Adds a new attribute to a character.
/// Attributes are categorized properties like physical traits, skills, or goals.
/// * `conn` - Database connection
/// * `character_id` - The unique identifier of the character
/// * `user_id` - The unique identifier of the user
/// * `r#type` - The type/category of the attribute (e.g., 'physical', 'skills')
/// * `name` - The value/name of the attribute
/// * `lang` - The language code for localization
/// * `existing_attribute_id` - Optional existing attribute ID for updates or imports
/// Returns the unique identifier of the newly created attribute.
pub fn add_new_attribute(conn: &Connection, character_id: &str, user_id: &str, r#type: &str, name: &str, lang: Lang, existing_attribute_id: Option<&str>) -> AppResult<String> {
let user_key: String = get_user_encryption_key(user_id)?;
let attribute_id: String = create_unique_id(existing_attribute_id);
let encrypted_type: String = encrypt_data_with_user_key(r#type, &user_key)?;
let encrypted_name: String = encrypt_data_with_user_key(name, &user_key)?;
let last_update: i64 = timestamp_in_seconds();
repo::insert_attribute(conn, &attribute_id, character_id, user_id, &encrypted_type, &encrypted_name, last_update, lang)
}
/// Deletes an attribute from a character.
/// * `conn` - Database connection
/// * `user_id` - The unique identifier of the user
/// * `book_id` - The unique identifier of the book
/// * `attribute_id` - The unique identifier of the attribute to delete
/// * `deleted_at` - The timestamp of deletion
/// * `lang` - The language code for localization
/// Returns true if the deletion was successful, false otherwise.
pub fn delete_attribute(conn: &Connection, user_id: &str, book_id: &str, attribute_id: &str, deleted_at: i64, lang: Lang) -> AppResult<bool> {
let deleted: bool = repo::delete_attribute(conn, user_id, attribute_id, lang)?;
if deleted {
let removal_id: String = create_unique_id(None);
tombstone_repo::insert(conn, &removal_id, "book_characters_attributes", attribute_id, Some(book_id), user_id, deleted_at, lang)?;
}
Ok(deleted)
}
/// Deletes a character and all its related data.
/// * `conn` - Database connection
/// * `user_id` - The unique identifier of the user
/// * `book_id` - The unique identifier of the book
/// * `character_id` - The unique identifier of the character to delete
/// * `deleted_at` - The timestamp of deletion
/// * `lang` - The language code for localization
/// Returns true if the deletion was successful.
pub fn delete_character(conn: &Connection, user_id: &str, book_id: &str, character_id: &str, deleted_at: i64, lang: Lang) -> AppResult<bool> {
let deleted: bool = repo::delete_character(conn, user_id, character_id, lang)?;
if deleted {
let removal_id: String = create_unique_id(None);
tombstone_repo::insert(conn, &removal_id, "book_characters", character_id, Some(book_id), user_id, deleted_at, lang)?;
}
Ok(deleted)
}
/// Retrieves all attributes for a specific character, grouped by type.
/// Decrypts attribute data using the user's encryption key.
/// * `conn` - Database connection
/// * `character_id` - The unique identifier of the character
/// * `user_id` - The unique identifier of the user
/// * `lang` - The language code for localization
/// Returns an array of character attributes grouped by type.
pub fn get_attributes(conn: &Connection, character_id: &str, user_id: &str, lang: Lang) -> AppResult<Vec<CharacterAttribute>> {
let user_key: String = get_user_encryption_key(user_id)?;
let encrypted_attributes: Vec<repo::AttributeResult> = repo::fetch_attributes(conn, character_id, user_id, lang)?;
if encrypted_attributes.is_empty() {
return Ok(vec![]);
}
let mut attributes_by_type: HashMap<String, Vec<Attribute>> = HashMap::new();
for encrypted_attribute in encrypted_attributes {
let decrypted_type: String = decrypt_data_with_user_key(&encrypted_attribute.attribute_name, &user_key)?;
let decrypted_value: String = if encrypted_attribute.attribute_value.is_empty() { String::new() } else { decrypt_data_with_user_key(&encrypted_attribute.attribute_value, &user_key)? };
attributes_by_type.entry(decrypted_type).or_insert_with(Vec::new).push(Attribute {
id: encrypted_attribute.attr_id,
name: decrypted_value,
});
}
let character_attributes: Vec<CharacterAttribute> = attributes_by_type.into_iter().map(|(attribute_type, values)| CharacterAttribute {
r#type: attribute_type,
values,
}).collect();
Ok(character_attributes)
}
/// Retrieves complete character data including all attributes for multiple characters.
/// Used for exporting or displaying full character profiles.
/// * `conn` - Database connection
/// * `user_id` - The unique identifier of the user
/// * `book_id` - The unique identifier of the book
/// * `characters` - An array of character IDs to retrieve
/// * `lang` - The language code for localization
/// Returns an array of complete character objects with all their attributes.
pub fn get_complete_character_list(conn: &Connection, user_id: &str, book_id: &str, characters: &[String], lang: Lang) -> AppResult<Vec<CompleteCharacterProps>> {
let encrypted_character_list: Vec<repo::CompleteCharacterResult> = match repo::fetch_complete_characters(conn, user_id, book_id, characters, lang) {
Ok(result) => result,
Err(_) => return Ok(vec![]),
};
if encrypted_character_list.is_empty() {
return Ok(vec![]);
}
let user_key: String = get_user_encryption_key(user_id)?;
let mut complete_characters_map: HashMap<String, CompleteCharacterProps> = HashMap::new();
for encrypted_character in &encrypted_character_list {
if encrypted_character.character_id.is_empty() {
continue;
}
if !complete_characters_map.contains_key(&encrypted_character.character_id) {
let decrypted_character = CompleteCharacterProps {
id: None,
name: if encrypted_character.first_name.is_empty() { String::new() } else { decrypt_data_with_user_key(&encrypted_character.first_name, &user_key)? },
last_name: if encrypted_character.last_name.is_empty() { String::new() } else { decrypt_data_with_user_key(&encrypted_character.last_name, &user_key)? },
nickname: if let Some(ref value) = encrypted_character.nickname { decrypt_data_with_user_key(value, &user_key)? } else { String::new() },
age: if let Some(ref value) = encrypted_character.age { Some(decrypt_data_with_user_key(value, &user_key)?.parse::<i64>().unwrap_or(0)) } else { None },
gender: if let Some(ref value) = encrypted_character.gender { decrypt_data_with_user_key(value, &user_key)? } else { String::new() },
species: if let Some(ref value) = encrypted_character.species { decrypt_data_with_user_key(value, &user_key)? } else { String::new() },
nationality: if let Some(ref value) = encrypted_character.nationality { decrypt_data_with_user_key(value, &user_key)? } else { String::new() },
status: if let Some(ref value) = encrypted_character.status { decrypt_data_with_user_key(value, &user_key)? } else { "alive".to_string() },
title: if encrypted_character.title.is_empty() { String::new() } else { decrypt_data_with_user_key(&encrypted_character.title, &user_key)? },
category: if encrypted_character.category.is_empty() { String::new() } else { decrypt_data_with_user_key(&encrypted_character.category, &user_key)? },
image: None,
role: if encrypted_character.role.is_empty() { String::new() } else { decrypt_data_with_user_key(&encrypted_character.role, &user_key)? },
biography: if encrypted_character.biography.is_empty() { String::new() } else { decrypt_data_with_user_key(&encrypted_character.biography, &user_key)? },
history: if encrypted_character.history.is_empty() { String::new() } else { decrypt_data_with_user_key(&encrypted_character.history, &user_key)? },
speech_pattern: if let Some(ref value) = encrypted_character.speech_pattern { decrypt_data_with_user_key(value, &user_key)? } else { String::new() },
catchphrase: if let Some(ref value) = encrypted_character.catchphrase { decrypt_data_with_user_key(value, &user_key)? } else { String::new() },
residence: if let Some(ref value) = encrypted_character.residence { decrypt_data_with_user_key(value, &user_key)? } else { String::new() },
notes: if let Some(ref value) = encrypted_character.notes { decrypt_data_with_user_key(value, &user_key)? } else { String::new() },
color: if let Some(ref value) = encrypted_character.color { decrypt_data_with_user_key(value, &user_key)? } else { String::new() },
physical: vec![],
psychological: vec![],
relations: vec![],
skills: vec![],
weaknesses: vec![],
strengths: vec![],
goals: vec![],
motivations: vec![],
arc: vec![],
secrets: vec![],
fears: vec![],
flaws: vec![],
beliefs: vec![],
conflicts: vec![],
quotes: vec![],
distinguishing_marks: vec![],
items: vec![],
affiliations: vec![],
};
complete_characters_map.insert(encrypted_character.character_id.clone(), decrypted_character);
}
let character_entry: &mut CompleteCharacterProps = match complete_characters_map.get_mut(&encrypted_character.character_id) {
Some(entry) => entry,
None => continue,
};
if encrypted_character.attribute_name.is_empty() {
continue;
}
let decrypted_attribute_name: String = decrypt_data_with_user_key(&encrypted_character.attribute_name, &user_key)?;
let decrypted_attribute_value: String = if encrypted_character.attribute_value.is_empty() { String::new() } else { decrypt_data_with_user_key(&encrypted_character.attribute_value, &user_key)? };
let attribute = Attribute { id: String::new(), name: decrypted_attribute_value };
match decrypted_attribute_name.as_str() {
"physical" => character_entry.physical.push(attribute),
"psychological" => character_entry.psychological.push(attribute),
"relations" => character_entry.relations.push(attribute),
"skills" => character_entry.skills.push(attribute),
"weaknesses" => character_entry.weaknesses.push(attribute),
"strengths" => character_entry.strengths.push(attribute),
"goals" => character_entry.goals.push(attribute),
"motivations" => character_entry.motivations.push(attribute),
"arc" => character_entry.arc.push(attribute),
"secrets" => character_entry.secrets.push(attribute),
"fears" => character_entry.fears.push(attribute),
"flaws" => character_entry.flaws.push(attribute),
"beliefs" => character_entry.beliefs.push(attribute),
"conflicts" => character_entry.conflicts.push(attribute),
"quotes" => character_entry.quotes.push(attribute),
"distinguishingMarks" => character_entry.distinguishing_marks.push(attribute),
"items" => character_entry.items.push(attribute),
"affiliations" => character_entry.affiliations.push(attribute),
_ => {}
}
}
Ok(complete_characters_map.into_values().collect())
}
/// Generates a formatted vCard-style string representation of characters.
/// Useful for AI context or text-based exports.
/// * `characters` - An array of complete character objects to format
/// Returns a formatted string containing all character information.
pub fn character_v_card(characters: &[CompleteCharacterProps]) -> String {
let mut unique_characters_map: HashMap<String, CompleteCharacterProps> = HashMap::new();
for character in characters {
let character_identifier: String = if !character.name.is_empty() {
character.name.clone()
} else if let Some(ref id) = character.id {
id.clone()
} else {
"unknown".to_string()
};
if !unique_characters_map.contains_key(&character_identifier) {
unique_characters_map.insert(character_identifier.clone(), CompleteCharacterProps {
id: None,
name: character.name.clone(),
last_name: character.last_name.clone(),
nickname: character.nickname.clone(),
age: character.age,
gender: character.gender.clone(),
species: character.species.clone(),
nationality: character.nationality.clone(),
status: character.status.clone(),
title: character.title.clone(),
category: character.category.clone(),
image: None,
role: character.role.clone(),
biography: character.biography.clone(),
history: character.history.clone(),
speech_pattern: character.speech_pattern.clone(),
catchphrase: character.catchphrase.clone(),
residence: character.residence.clone(),
notes: character.notes.clone(),
color: character.color.clone(),
physical: vec![],
psychological: vec![],
relations: vec![],
skills: vec![],
weaknesses: vec![],
strengths: vec![],
goals: vec![],
motivations: vec![],
arc: vec![],
secrets: vec![],
fears: vec![],
flaws: vec![],
beliefs: vec![],
conflicts: vec![],
quotes: vec![],
distinguishing_marks: vec![],
items: vec![],
affiliations: vec![],
});
}
let aggregated_character_data: &mut CompleteCharacterProps = unique_characters_map.get_mut(&character_identifier).unwrap();
for attribute in &character.physical { aggregated_character_data.physical.push(Attribute { id: attribute.id.clone(), name: attribute.name.clone() }); }
for attribute in &character.psychological { aggregated_character_data.psychological.push(Attribute { id: attribute.id.clone(), name: attribute.name.clone() }); }
for attribute in &character.relations { aggregated_character_data.relations.push(Attribute { id: attribute.id.clone(), name: attribute.name.clone() }); }
for attribute in &character.skills { aggregated_character_data.skills.push(Attribute { id: attribute.id.clone(), name: attribute.name.clone() }); }
for attribute in &character.weaknesses { aggregated_character_data.weaknesses.push(Attribute { id: attribute.id.clone(), name: attribute.name.clone() }); }
for attribute in &character.strengths { aggregated_character_data.strengths.push(Attribute { id: attribute.id.clone(), name: attribute.name.clone() }); }
for attribute in &character.goals { aggregated_character_data.goals.push(Attribute { id: attribute.id.clone(), name: attribute.name.clone() }); }
for attribute in &character.motivations { aggregated_character_data.motivations.push(Attribute { id: attribute.id.clone(), name: attribute.name.clone() }); }
for attribute in &character.arc { aggregated_character_data.arc.push(Attribute { id: attribute.id.clone(), name: attribute.name.clone() }); }
for attribute in &character.secrets { aggregated_character_data.secrets.push(Attribute { id: attribute.id.clone(), name: attribute.name.clone() }); }
for attribute in &character.fears { aggregated_character_data.fears.push(Attribute { id: attribute.id.clone(), name: attribute.name.clone() }); }
for attribute in &character.flaws { aggregated_character_data.flaws.push(Attribute { id: attribute.id.clone(), name: attribute.name.clone() }); }
for attribute in &character.beliefs { aggregated_character_data.beliefs.push(Attribute { id: attribute.id.clone(), name: attribute.name.clone() }); }
for attribute in &character.conflicts { aggregated_character_data.conflicts.push(Attribute { id: attribute.id.clone(), name: attribute.name.clone() }); }
for attribute in &character.quotes { aggregated_character_data.quotes.push(Attribute { id: attribute.id.clone(), name: attribute.name.clone() }); }
for attribute in &character.distinguishing_marks { aggregated_character_data.distinguishing_marks.push(Attribute { id: attribute.id.clone(), name: attribute.name.clone() }); }
for attribute in &character.items { aggregated_character_data.items.push(Attribute { id: attribute.id.clone(), name: attribute.name.clone() }); }
for attribute in &character.affiliations { aggregated_character_data.affiliations.push(Attribute { id: attribute.id.clone(), name: attribute.name.clone() }); }
}
let formatted_characters_description: String = unique_characters_map.values().map(|character| {
let mut character_description_lines: Vec<String> = Vec::new();
let full_name: String = [&character.name, &character.last_name].iter().filter(|name| !name.is_empty()).map(|name| name.as_str()).collect::<Vec<&str>>().join(" ");
if !full_name.is_empty() {
character_description_lines.push(format!("Nom : {}", full_name));
}
let simple_properties: Vec<(&str, &str)> = vec![
("Category", &character.category),
("Title", &character.title),
("Role", &character.role),
("Biography", &character.biography),
("History", &character.history),
];
for (property_label, property_value) in simple_properties {
if !property_value.is_empty() {
character_description_lines.push(format!("{} : {}", property_label, property_value));
}
}
let array_properties: Vec<(&str, &Vec<Attribute>)> = vec![
("Physical", &character.physical),
("Psychological", &character.psychological),
("Relations", &character.relations),
("Skills", &character.skills),
("Weaknesses", &character.weaknesses),
("Strengths", &character.strengths),
("Goals", &character.goals),
("Motivations", &character.motivations),
("Arc", &character.arc),
("Secrets", &character.secrets),
("Fears", &character.fears),
("Flaws", &character.flaws),
("Beliefs", &character.beliefs),
("Conflicts", &character.conflicts),
("Quotes", &character.quotes),
("DistinguishingMarks", &character.distinguishing_marks),
("Items", &character.items),
("Affiliations", &character.affiliations),
];
for (capitalized_property_key, attribute_values) in array_properties {
if !attribute_values.is_empty() {
let formatted_attribute_values: String = attribute_values.iter().map(|attribute_item| attribute_item.name.as_str()).collect::<Vec<&str>>().join(", ");
character_description_lines.push(format!("{} : {}", capitalized_property_key, formatted_attribute_values));
}
}
character_description_lines.join("\n")
}).collect::<Vec<String>>().join("\n\n");
formatted_characters_description
}