Refactor: Remove unused structs, redundant services, and streamline repository models

This commit is contained in:
natreex
2026-03-21 23:01:27 -04:00
parent 1478fe10dd
commit 32d2b0fa5a
44 changed files with 2009 additions and 3256 deletions

View File

@@ -317,25 +317,15 @@ pub fn initialize_schema(conn: &Connection) -> Result<(), rusqlite::Error> {
-- Erit Users
CREATE TABLE IF NOT EXISTS erit_users (
user_id TEXT PRIMARY KEY,
first_name TEXT NOT NULL,
last_name TEXT NOT NULL,
username TEXT NOT NULL,
email TEXT NOT NULL,
origin_email TEXT NOT NULL,
origin_username TEXT NOT NULL,
author_name TEXT,
origin_author_name TEXT,
plateform TEXT NOT NULL,
social_id TEXT,
user_group INTEGER NOT NULL DEFAULT 4,
password TEXT,
term_accepted INTEGER NOT NULL DEFAULT 0,
verify_code TEXT,
reg_date INTEGER NOT NULL,
account_verified INTEGER NOT NULL DEFAULT 0,
erite_points INTEGER NOT NULL DEFAULT 100,
stripe_customer_id TEXT,
credits_balance REAL DEFAULT 0
password TEXT
);
-- =========================================================================

View File

@@ -24,15 +24,6 @@ pub struct ActProps {
pub chapters: Option<Vec<ActChapter>>,
}
pub struct ActStory {
pub act_id: i64,
pub summary: String,
pub chapter_summary: String,
pub chapter_goal: String,
pub incidents: Vec<IncidentStory>,
pub plot_points: Vec<PlotPointStory>,
}
#[derive(Serialize, Deserialize, Clone)]
#[serde(rename_all = "camelCase")]
pub struct ActChapter {
@@ -47,13 +38,8 @@ pub struct ActChapter {
pub goal: String,
}
pub struct SyncedActSummary {
pub id: String,
pub last_update: i64,
}
pub use incident_service::{IncidentProps, IncidentStory};
pub use plotpoint_service::{PlotPointProps, PlotPointStory};
pub use incident_service::IncidentProps;
pub use plotpoint_service::PlotPointProps;
#[derive(Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]

View File

@@ -444,7 +444,7 @@ pub fn export_book(data: ExportBookData, db: State<DbManager>, session: State<Se
let conn = db_manager.get_connection(&user_id)?;
let book_data = chapter_service::get_complete_book_data_with_selections(conn, &user_id, &data.book_id, data.selections.as_deref(), lang)?;
match data.format.as_str() {
"epub" => { let result = export_service::transform_to_epub(&book_data)?; Ok(result.buffer) },
"epub" => { let result = export_service::transform_to_epub(&book_data, lang)?; Ok(result.buffer) },
"pdf" => { let result = export_service::transform_to_pdf(&book_data)?; Ok(result.buffer) },
"docx" => { let result = export_service::transform_to_docx(&book_data)?; Ok(result.buffer) },
_ => Err(AppError::Validation(if lang == crate::shared::types::Lang::Fr { "Format non supporté.".to_string() } else { "Unsupported format.".to_string() })),

View File

@@ -9,9 +9,7 @@ pub struct BookQuery {
pub book_type: String,
pub author_id: String,
pub title: String,
pub hashed_title: String,
pub sub_title: Option<String>,
pub hashed_sub_title: Option<String>,
pub summary: Option<String>,
pub serie_id: Option<i64>,
pub desired_release_date: Option<String>,
@@ -86,8 +84,7 @@ pub fn fetch_books(conn: &Connection, user_id: &str, lang: Lang) -> AppResult<Ve
sub_title: query_row.get(4)?, summary: query_row.get(5)?,
serie_id: query_row.get(6)?, desired_release_date: query_row.get(7)?,
desired_word_count: query_row.get(8)?, words_count: query_row.get(9)?,
cover_image: query_row.get(10)?, hashed_title: String::new(),
hashed_sub_title: None,
cover_image: query_row.get(10)?,
})
})
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer la liste des livres.".to_string() } else { "Unable to retrieve book list.".to_string() }))?
@@ -125,8 +122,7 @@ pub fn fetch_book(conn: &Connection, book_id: &str, user_id: &str, lang: Lang) -
sub_title: query_row.get(4)?, cover_image: query_row.get(5)?,
desired_release_date: query_row.get(6)?, desired_word_count: query_row.get(7)?,
words_count: query_row.get(8)?, serie_id: query_row.get(9)?,
book_type: String::new(), hashed_title: String::new(),
hashed_sub_title: None,
book_type: String::new(),
})
})
.map_err(|error| match error {

View File

@@ -67,34 +67,10 @@ pub struct CompleteBookData {
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
pub struct BookUserInfos {
pub first_name: String,
pub last_name: String,
pub username: String,
pub author_name: String,
}
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
pub struct SyncedBook {
pub id: String,
pub book_type: String,
pub title: String,
pub sub_title: Option<String>,
pub last_update: i64,
pub chapters: Vec<SyncedChapter>,
pub characters: Vec<SyncedCharacter>,
pub locations: Vec<SyncedLocation>,
pub worlds: Vec<SyncedWorld>,
pub incidents: Vec<SyncedIncident>,
pub plot_points: Vec<SyncedPlotPoint>,
pub issues: Vec<SyncedIssue>,
pub act_summaries: Vec<SyncedActSummary>,
pub guide_line: Option<SyncedGuideLine>,
pub ai_guide_line: Option<SyncedAIGuideLine>,
pub book_tools: Option<SyncedBookTools>,
pub spells: Vec<SyncedSpell>,
pub spell_tags: Vec<SyncedSpellTag>,
}
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct BookSyncCompare {
@@ -115,43 +91,12 @@ pub struct BookSyncCompare {
pub act_summaries: Vec<String>,
pub guide_line: bool,
pub ai_guide_line: bool,
pub book_tools: bool,
#[serde(rename = "bookTools")]
pub _book_tools: bool,
pub spells: Vec<String>,
pub spell_tags: Vec<String>,
}
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
pub struct SyncedChapter {
pub id: String,
pub title: String,
pub last_update: i64,
}
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
pub struct SyncedCharacter {
pub id: String,
pub name: String,
pub last_update: i64,
}
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
pub struct SyncedLocation {
pub id: String,
pub name: String,
pub last_update: i64,
}
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
pub struct SyncedWorld {
pub id: String,
pub name: String,
pub last_update: i64,
}
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
pub struct SyncedIncident {
@@ -966,8 +911,7 @@ pub fn complete_book_data(conn: &Connection, user_id: &str, book_id: &str, lang:
summary: if let Some(ref summary) = book_data.summary { decrypt_data_with_user_key(summary, &user_key)? } else { String::new() },
cover_image,
user_infos: BookUserInfos {
first_name: if let Some(ref first_name) = user_infos.first_name { decrypt_data_with_user_key(first_name, &user_key)? } else { String::new() },
last_name: if let Some(ref last_name) = user_infos.last_name { decrypt_data_with_user_key(last_name, &user_key)? } else { String::new() },
username: decrypt_data_with_user_key(&user_infos.username, &user_key)?,
author_name: if let Some(ref author_name) = user_infos.author_name { decrypt_data_with_user_key(author_name, &user_key)? } else { String::new() },
},
chapters: decrypted_chapters,

View File

@@ -89,7 +89,6 @@ pub struct SaveChapterContentData {
pub version: i64,
pub content: Value,
pub total_word_count: i64,
pub content_id: String,
}
#[tauri::command]

View File

@@ -23,7 +23,7 @@ pub struct ActChapterQuery {
}
pub struct ChapterStoryQueryResult {
pub chapter_info_id: i64,
pub _chapter_info_id: i64,
pub act_id: i64,
pub summary: String,
pub chapter_summary: String,
@@ -47,18 +47,18 @@ pub struct BookChaptersTable {
pub author_id: String,
pub title: String,
pub hashed_title: String,
pub words_count: Option<i64>,
pub _words_count: Option<i64>,
pub chapter_order: i64,
pub last_update: i64,
}
pub struct BookChapterInfosTable {
pub chapter_info_id: String,
pub _chapter_info_id: String,
pub chapter_id: String,
pub act_id: Option<i64>,
pub incident_id: Option<String>,
pub plot_point_id: Option<String>,
pub book_id: String,
pub _act_id: Option<i64>,
pub _incident_id: Option<String>,
pub _plot_point_id: Option<String>,
pub _book_id: String,
pub author_id: String,
pub summary: Option<String>,
pub goal: Option<String>,
@@ -75,7 +75,7 @@ pub struct SyncedChapterResult {
pub struct SyncedChapterInfoResult {
pub chapter_info_id: String,
pub chapter_id: Option<String>,
pub book_id: String,
pub _book_id: String,
pub last_update: i64,
}
@@ -97,7 +97,7 @@ pub struct SelectedChapterContentResult {
pub title: String,
pub chapter_order: i64,
pub content: String,
pub version: i64,
pub _version: i64,
}
#[derive(serde::Deserialize)]
@@ -307,7 +307,7 @@ pub fn fetch_chapter_story(conn: &Connection, user_id: &str, chapter_id: &str, l
let rows = statement
.query_map(params![chapter_id, user_id], |query_row| {
Ok(ChapterStoryQueryResult {
chapter_info_id: query_row.get(0)?, act_id: query_row.get(1)?,
_chapter_info_id: query_row.get(0)?, act_id: query_row.get(1)?,
summary: query_row.get::<_, Option<String>>(2)?.unwrap_or_default(),
chapter_summary: query_row.get::<_, Option<String>>(3)?.unwrap_or_default(),
chapter_goal: query_row.get::<_, Option<String>>(4)?.unwrap_or_default(),
@@ -393,7 +393,7 @@ pub fn fetch_book_chapters(conn: &Connection, user_id: &str, book_id: &str, lang
Ok(BookChaptersTable {
chapter_id: query_row.get(0)?, book_id: query_row.get(1)?,
author_id: query_row.get(2)?, title: query_row.get(3)?,
hashed_title: query_row.get(4)?, words_count: query_row.get(5)?,
hashed_title: query_row.get(4)?, _words_count: query_row.get(5)?,
chapter_order: query_row.get(6)?, last_update: query_row.get(7)?,
})
})
@@ -413,9 +413,9 @@ pub fn fetch_book_chapter_infos(conn: &Connection, user_id: &str, chapter_id: &s
let rows = statement
.query_map(params![user_id, chapter_id], |query_row| {
Ok(BookChapterInfosTable {
chapter_info_id: query_row.get(0)?, chapter_id: query_row.get(1)?,
act_id: query_row.get(2)?, incident_id: query_row.get(3)?,
plot_point_id: query_row.get(4)?, book_id: query_row.get(5)?,
_chapter_info_id: query_row.get(0)?, chapter_id: query_row.get(1)?,
_act_id: query_row.get(2)?, _incident_id: query_row.get(3)?,
_plot_point_id: query_row.get(4)?, _book_id: query_row.get(5)?,
author_id: query_row.get(6)?, summary: query_row.get(7)?,
goal: query_row.get(8)?, last_update: query_row.get(9)?,
})
@@ -457,7 +457,7 @@ pub fn fetch_synced_chapter_infos(conn: &Connection, user_id: &str, lang: Lang)
.query_map(params![user_id], |query_row| {
Ok(SyncedChapterInfoResult {
chapter_info_id: query_row.get(0)?, chapter_id: query_row.get(1)?,
book_id: query_row.get(2)?, last_update: query_row.get(3)?,
_book_id: query_row.get(2)?, last_update: query_row.get(3)?,
})
})
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les infos des chapitres synchronisés.".to_string() } else { "Unable to retrieve synced chapter infos.".to_string() }))?
@@ -500,7 +500,7 @@ pub fn fetch_complete_chapter_by_id(conn: &Connection, chapter_id: &str, lang: L
Ok(BookChaptersTable {
chapter_id: query_row.get(0)?, book_id: query_row.get(1)?,
author_id: query_row.get(2)?, title: query_row.get(3)?,
hashed_title: query_row.get(4)?, words_count: query_row.get(5)?,
hashed_title: query_row.get(4)?, _words_count: query_row.get(5)?,
chapter_order: query_row.get(6)?, last_update: query_row.get(7)?,
})
})
@@ -520,9 +520,9 @@ pub fn fetch_complete_chapter_info_by_id(conn: &Connection, chapter_info_id: &st
let rows = statement
.query_map(params![chapter_info_id], |query_row| {
Ok(BookChapterInfosTable {
chapter_info_id: query_row.get(0)?, chapter_id: query_row.get(1)?,
act_id: query_row.get(2)?, incident_id: query_row.get(3)?,
plot_point_id: query_row.get(4)?, book_id: query_row.get(5)?,
_chapter_info_id: query_row.get(0)?, chapter_id: query_row.get(1)?,
_act_id: query_row.get(2)?, _incident_id: query_row.get(3)?,
_plot_point_id: query_row.get(4)?, _book_id: query_row.get(5)?,
author_id: query_row.get(6)?, summary: query_row.get(7)?,
goal: query_row.get(8)?, last_update: query_row.get(9)?,
})
@@ -581,7 +581,7 @@ pub fn fetch_selected_chapters_content(conn: &Connection, book_id: &str, selecti
Ok(SelectedChapterContentResult {
chapter_id: query_row.get(0)?, title: query_row.get(1)?,
chapter_order: query_row.get(2)?, content: query_row.get(3)?,
version: query_row.get(4)?,
_version: query_row.get(4)?,
})
})
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer le contenu des chapitres sélectionnés.".to_string() } else { "Unable to retrieve selected chapters content.".to_string() }))?

View File

@@ -24,10 +24,7 @@ pub struct ChapterContent {
pub struct ChapterContentData {
pub title: String,
pub chapter_order: i64,
pub content: String,
pub words_count: i64,
pub version: i64,
}
#[derive(Serialize)]
@@ -47,24 +44,6 @@ pub struct CompanionContent {
pub words_count: i64,
}
pub struct SyncedChapter {
pub id: String,
pub name: String,
pub last_update: i64,
pub contents: Vec<SyncedChapterContent>,
pub info: Option<SyncedChapterInfo>,
}
pub struct SyncedChapterContent {
pub id: String,
pub last_update: i64,
}
pub struct SyncedChapterInfo {
pub id: String,
pub last_update: i64,
}
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
pub struct CompleteChapterContent {
@@ -601,10 +580,7 @@ pub fn get_chapters_or_sheet(book_chapters: &[CompleteChapterContent]) -> Vec<Ch
let parsed_content: Value = serde_json::from_str(&sheet.content).unwrap_or(Value::Null);
processed_chapters.push(ChapterContentData {
title: sheet.title.clone(),
chapter_order: sheet.order,
content: html_to_text(&tip_tap_to_html(&parsed_content)),
words_count: 0,
version: sheet.version.unwrap_or(0),
});
} else if regular_chapter.is_some() {
for chapter_data in book_chapters {
@@ -612,10 +588,7 @@ pub fn get_chapters_or_sheet(book_chapters: &[CompleteChapterContent]) -> Vec<Ch
let parsed_content: Value = serde_json::from_str(&chapter_data.content).unwrap_or(Value::Null);
processed_chapters.push(ChapterContentData {
title: chapter_data.title.clone(),
chapter_order: chapter_data.order,
content: html_to_text(&tip_tap_to_html(&parsed_content)),
words_count: 0,
version: chapter_data.version.unwrap_or(0),
});
}
}

View File

@@ -29,8 +29,8 @@ pub struct BookChapterContentTable {
pub author_id: String,
pub version: i64,
pub content: Option<String>,
pub words_count: i64,
pub time_on_it: i64,
pub _words_count: i64,
pub _time_on_it: i64,
pub last_update: i64,
}
@@ -188,8 +188,8 @@ pub fn fetch_book_chapter_contents(conn: &Connection, user_id: &str, chapter_id:
Ok(BookChapterContentTable {
content_id: query_row.get(0)?, chapter_id: query_row.get(1)?,
author_id: query_row.get(2)?, version: query_row.get(3)?,
content: query_row.get(4)?, words_count: query_row.get(5)?,
time_on_it: query_row.get(6)?, last_update: query_row.get(7)?,
content: query_row.get(4)?, _words_count: query_row.get(5)?,
_time_on_it: query_row.get(6)?, last_update: query_row.get(7)?,
})
})
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer le contenu des chapitres.".to_string() } else { "Unable to retrieve chapter contents.".to_string() }))?
@@ -261,8 +261,8 @@ pub fn fetch_complete_chapter_content_by_id(conn: &Connection, content_id: &str,
Ok(BookChapterContentTable {
content_id: query_row.get(0)?, chapter_id: query_row.get(1)?,
author_id: query_row.get(2)?, version: query_row.get(3)?,
content: query_row.get(4)?, words_count: query_row.get(5)?,
time_on_it: query_row.get(6)?, last_update: query_row.get(7)?,
content: query_row.get(4)?, _words_count: query_row.get(5)?,
_time_on_it: query_row.get(6)?, last_update: query_row.get(7)?,
})
})
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer le contenu de chapitre complet.".to_string() } else { "Unable to retrieve complete chapter content.".to_string() }))?

View File

@@ -18,7 +18,6 @@ fn get_session(session: &State<SessionState>) -> Result<(String, Lang), AppError
#[serde(rename_all = "camelCase")]
pub struct GetCharacterListData {
pub book_id: String,
pub enabled: bool,
}
#[tauri::command]

View File

@@ -106,19 +106,6 @@ pub struct CharacterAttribute {
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

View File

@@ -10,6 +10,7 @@ use serde_json::Value;
use crate::domains::book::service::{CompleteBookData, CompleteChapterContent};
use crate::domains::chapter::service::{get_chapters_or_sheet, tip_tap_to_html, ChapterContentData};
use crate::error::{AppError, AppResult};
use crate::shared::types::Lang;
pub const MAIN_STYLE: &str = r#"h1 {
font-size: 24px !important;
@@ -25,7 +26,6 @@ p {
pub struct ExportResult {
pub buffer: Vec<u8>,
pub file_name: String,
}
/// Transforms book data into a DOCX document.
@@ -33,7 +33,6 @@ pub struct ExportResult {
/// Returns the DOCX buffer and filename.
pub fn transform_to_docx(book_data: &CompleteBookData) -> AppResult<ExportResult> {
let book_title: &str = &book_data.title;
let filename: String = format!("{}.docx", book_title);
let mut docx: Docx = Docx::new();
@@ -92,7 +91,7 @@ pub fn transform_to_docx(book_data: &CompleteBookData) -> AppResult<ExportResult
.pack(&mut Cursor::new(&mut buffer))
.map_err(|error| AppError::Internal(format!("DOCX generation failed: {}", error)))?;
Ok(ExportResult { buffer, file_name: filename })
Ok(ExportResult { buffer })
}
/// Transforms book data into a PDF document.
@@ -100,7 +99,6 @@ pub fn transform_to_docx(book_data: &CompleteBookData) -> AppResult<ExportResult
/// Returns the PDF buffer and filename.
pub fn transform_to_pdf(book_data: &CompleteBookData) -> AppResult<ExportResult> {
let book_title: &str = &book_data.title;
let filename: String = format!("{}.pdf", book_title);
let (pdf_document, page_index, layer_index) = PdfDocument::new(book_title, Mm(210.0), Mm(297.0), "Title Page");
let font = pdf_document
@@ -150,13 +148,13 @@ pub fn transform_to_pdf(book_data: &CompleteBookData) -> AppResult<ExportResult>
.save_to_bytes()
.map_err(|error| AppError::Internal(format!("PDF generation failed: {}", error)))?;
Ok(ExportResult { buffer, file_name: filename })
Ok(ExportResult { buffer })
}
/// Transforms book data into an EPUB document.
/// * `book_data` - The complete book data to export
/// Returns the EPUB buffer and filename.
pub fn transform_to_epub(book_data: &CompleteBookData) -> AppResult<ExportResult> {
pub fn transform_to_epub(book_data: &CompleteBookData, lang: Lang) -> AppResult<ExportResult> {
let book_title: &str = &book_data.title;
let book_id: &str = &book_data.book_id;
@@ -179,8 +177,11 @@ pub fn transform_to_epub(book_data: &CompleteBookData) -> AppResult<ExportResult
epub_builder
.metadata("identifier", &format!("urn:uuid:{}", book_id))
.map_err(|error| AppError::Internal(format!("EPUB metadata error: {}", error)))?;
if book_data.user_infos.author_name.is_empty() {
return Err(AppError::Validation(if lang == Lang::Fr { "Veuillez définir votre nom d'auteur avant d'exporter.".to_string() } else { "Please set your author name before exporting.".to_string() }));
}
epub_builder
.metadata("author", &format!("{} {}", &book_data.user_infos.first_name, &book_data.user_infos.last_name))
.metadata("author", &book_data.user_infos.author_name)
.map_err(|error| AppError::Internal(format!("EPUB metadata error: {}", error)))?;
epub_builder
.metadata("publisher", "ERitors Scribe")
@@ -247,5 +248,5 @@ pub fn transform_to_epub(book_data: &CompleteBookData) -> AppResult<ExportResult
.generate(&mut epub_buffer)
.map_err(|error| AppError::Internal(format!("EPUB generation failed: {}", error)))?;
Ok(ExportResult { buffer: epub_buffer, file_name: format!("{}.epub", book_title) })
Ok(ExportResult { buffer: epub_buffer })
}

View File

@@ -58,18 +58,18 @@ pub struct GuideLineQuery {
}
pub struct GuideLineAIQuery {
pub user_id: String,
pub book_id: String,
pub _user_id: String,
pub _book_id: String,
pub global_resume: Option<String>,
pub themes: Option<String>,
pub verbe_tense: Option<i64>,
pub narrative_type: Option<i64>,
pub langue: Option<i64>,
pub dialogue_type: Option<i64>,
pub tone: Option<String>,
pub _tone: Option<String>,
pub atmosphere: Option<String>,
pub current_resume: Option<String>,
pub meta: String,
pub _meta: String,
}
/// Fetches the guideline for a specific book.
@@ -211,8 +211,8 @@ pub fn fetch_guide_line_ai(conn: &Connection, user_id: &str, book_id: &str, lang
global_resume: query_row.get(2)?, atmosphere: query_row.get(3)?,
verbe_tense: query_row.get(4)?, langue: query_row.get(5)?,
themes: query_row.get(6)?, current_resume: query_row.get(7)?,
user_id: user_id.to_string(), book_id: book_id.to_string(),
tone: None, meta: String::new(),
_user_id: user_id.to_string(), _book_id: book_id.to_string(),
_tone: None, _meta: String::new(),
})
})
.map_err(|error| match error {

View File

@@ -8,16 +8,6 @@ use crate::error::{AppError, AppResult};
use crate::helpers::timestamp_in_seconds;
use crate::shared::types::Lang;
/// Represents the synced guideline data for a book.
pub struct SyncedGuideLine {
pub last_update: i64,
}
/// Represents the synced AI guideline data for a book.
pub struct SyncedAIGuideLine {
pub last_update: i64,
}
/// Represents the decrypted guideline properties for a book.
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]

View File

@@ -10,21 +10,6 @@ use crate::error::AppResult;
use crate::helpers::{create_unique_id, timestamp_in_seconds};
use crate::shared::types::Lang;
/// Represents the story details of an incident within a chapter.
pub struct IncidentStory {
pub incident_title: String,
pub incident_summary: String,
pub chapter_summary: String,
pub chapter_goal: String,
}
/// Represents a synced incident with minimal information for comparison.
pub struct SyncedIncident {
pub id: String,
pub name: String,
pub last_update: i64,
}
#[derive(Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct IncidentProps {

View File

@@ -9,13 +9,6 @@ use crate::error::AppResult;
use crate::helpers::{create_unique_id, timestamp_in_seconds};
use crate::shared::types::Lang;
/// Represents a synced issue with its metadata.
pub struct SyncedIssue {
pub id: String,
pub name: String,
pub last_update: i64,
}
#[derive(Serialize)]
pub struct IssueProps {
pub id: String,

View File

@@ -18,7 +18,6 @@ fn get_session(session: &State<SessionState>) -> Result<(String, Lang), AppError
#[serde(rename_all = "camelCase")]
pub struct GetAllLocationsData {
pub book_id: String,
pub enabled: bool,
}
#[tauri::command]

View File

@@ -24,13 +24,6 @@ pub struct LocationElementQueryResult {
pub element_description: Option<String>,
}
pub struct LocationByTagResult {
pub element_name: String,
pub element_description: Option<String>,
pub sub_elem_name: Option<String>,
pub sub_elem_description: Option<String>,
}
pub struct BookLocationTable {
pub loc_id: String,
pub book_id: String,
@@ -349,51 +342,6 @@ pub fn fetch_location_tags(conn: &Connection, user_id: &str, book_id: &str, lang
/// * `conn` - Database connection
/// * `user_id` - The user's unique identifier
/// * `locations` - An array of location tag IDs to search for
/// * `lang` - The language for error messages ("fr" or "en")
/// Returns an array of locations matching the provided tags.
/// Errors if no tags are provided or no locations are found.
pub fn fetch_locations_by_tags(conn: &Connection, user_id: &str, locations: &[String], lang: Lang) -> AppResult<Vec<LocationByTagResult>> {
if locations.is_empty() {
return Err(AppError::Validation(if lang == Lang::Fr { "Aucun tag fourni.".to_string() } else { "No tags provided.".to_string() }));
}
let location_placeholders: String = locations.iter().map(|_| "?").collect::<Vec<_>>().join(",");
let query = format!("SELECT el.element_name, el.element_description, se.sub_elem_name, se.sub_elem_description FROM location_element AS el LEFT JOIN location_sub_element AS se ON el.element_id = se.element_id WHERE el.user_id = ?1 AND (el.element_id IN ({}) OR se.sub_element_id IN ({}))", location_placeholders, location_placeholders);
let mut statement = conn
.prepare(&query)
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les emplacements par tags.".to_string() } else { "Unable to retrieve locations by tags.".to_string() }))?;
let mut param_values: Vec<Box<dyn rusqlite::types::ToSql>> = Vec::new();
param_values.push(Box::new(user_id.to_string()));
for location in locations {
param_values.push(Box::new(location.clone()));
}
for location in locations {
param_values.push(Box::new(location.clone()));
}
let param_refs: Vec<&dyn rusqlite::types::ToSql> = param_values.iter().map(|parameter| parameter.as_ref() as &dyn rusqlite::types::ToSql).collect();
let locations_by_tags = statement
.query_map(param_refs.as_slice(), |query_row| {
Ok(LocationByTagResult {
element_name: query_row.get(0)?,
element_description: query_row.get(1)?,
sub_elem_name: query_row.get(2)?,
sub_elem_description: query_row.get(3)?,
})
})
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les emplacements par tags.".to_string() } else { "Unable to retrieve locations by tags.".to_string() }))?
.collect::<Result<Vec<_>, _>>()
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les emplacements par tags.".to_string() } else { "Unable to retrieve locations by tags.".to_string() }))?;
if locations_by_tags.is_empty() {
return Err(AppError::NotFound(if lang == Lang::Fr { "Aucun emplacement trouvé avec ces tags.".to_string() } else { "No locations found with these tags.".to_string() }));
}
Ok(locations_by_tags)
}
/// Checks if a location exists in the database.
/// * `conn` - Database connection
/// * `user_id` - The user's unique identifier

View File

@@ -41,29 +41,6 @@ pub struct LocationListResponse {
pub enabled: bool,
}
/// Synced location sub-element (lightweight, for comparison).
pub struct SyncedLocationSubElement {
pub id: String,
pub name: String,
pub last_update: i64,
}
/// Synced location element (lightweight, for comparison).
pub struct SyncedLocationElement {
pub id: String,
pub name: String,
pub last_update: i64,
pub sub_elements: Vec<SyncedLocationSubElement>,
}
/// Synced location (lightweight, for comparison).
pub struct SyncedLocation {
pub id: String,
pub name: String,
pub last_update: i64,
pub elements: Vec<SyncedLocationElement>,
}
/// Retrieves all locations for a given user and book.
/// * `conn` - Database connection
/// * `user_id` - The user's unique identifier
@@ -383,80 +360,3 @@ pub fn get_location_tags(conn: &Connection, user_id: &str, book_id: &str, lang:
Ok(sub_elements)
}
/// Retrieves location elements filtered by specific tag IDs.
/// * `conn` - Database connection
/// * `user_id` - The user's unique identifier
/// * `locations` - Array of location tag IDs to filter by
/// * `lang` - The language for error messages
/// Returns an array of elements with their associated sub-elements.
pub fn get_locations_by_tags(conn: &Connection, user_id: &str, locations: &[String], lang: Lang) -> AppResult<Vec<Element>> {
let location_tag_records: Vec<repo::LocationByTagResult> = repo::fetch_locations_by_tags(conn, user_id, locations, lang)?;
if location_tag_records.is_empty() {
return Ok(vec![]);
}
let user_key: String = get_user_encryption_key(user_id)?;
let mut location_elements: Vec<Element> = Vec::new();
for record in &location_tag_records {
let element_index: Option<usize> = location_elements.iter().position(|elem| elem.name == record.element_name);
let element_idx: usize = match element_index {
Some(idx) => idx,
None => {
let decrypted_name: String = decrypt_data_with_user_key(&record.element_name, &user_key)?;
let decrypted_description: String = if let Some(ref element_description) = record.element_description {
decrypt_data_with_user_key(element_description, &user_key)?
} else {
String::new()
};
location_elements.push(Element {
id: String::new(),
name: decrypted_name,
description: decrypted_description,
sub_elements: vec![],
});
location_elements.len() - 1
}
};
if let Some(ref sub_elem_name) = record.sub_elem_name {
let sub_element_exists: bool = location_elements[element_idx].sub_elements.iter().any(|sub| sub.name == *sub_elem_name);
if !sub_element_exists {
let decrypted_name: String = decrypt_data_with_user_key(sub_elem_name, &user_key)?;
let decrypted_description: String = if let Some(ref sub_elem_description) = record.sub_elem_description {
decrypt_data_with_user_key(sub_elem_description, &user_key)?
} else {
String::new()
};
location_elements[element_idx].sub_elements.push(SubElement {
id: String::new(),
name: decrypted_name,
description: decrypted_description,
});
}
}
}
Ok(location_elements)
}
/// Generates a formatted description string from an array of location elements.
/// * `locations` - Array of location elements to describe
/// Returns a formatted string with location names and descriptions.
pub fn locations_description(locations: &[Element]) -> String {
locations
.iter()
.map(|location| {
let mut description_fields: Vec<String> = Vec::new();
if !location.name.is_empty() {
description_fields.push(format!("Nom : {}", location.name));
}
if !location.description.is_empty() {
description_fields.push(format!("Description : {}", location.description));
}
description_fields.join("\n")
})
.collect::<Vec<String>>()
.join("\n\n")
}

View File

@@ -31,10 +31,7 @@ pub struct OfflineModeStatus {
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct OfflineModeData {
pub enabled: bool,
pub sync_interval_days: Option<i64>,
}
pub struct OfflineModeData {}
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]

View File

@@ -10,7 +10,7 @@ pub struct BookPlotPointsTable {
pub summary: Option<String>,
pub linked_incident_id: Option<String>,
pub author_id: String,
pub book_id: String,
pub _book_id: String,
pub last_update: i64,
}
@@ -152,7 +152,7 @@ pub fn fetch_book_plot_points(conn: &Connection, user_id: &str, book_id: &str, l
summary: query_row.get(3)?,
linked_incident_id: query_row.get(4)?,
author_id: query_row.get(5)?,
book_id: query_row.get(6)?,
_book_id: query_row.get(6)?,
last_update: query_row.get(7)?,
})
})
@@ -235,7 +235,7 @@ pub fn fetch_complete_plot_point_by_id(conn: &Connection, plot_point_id: &str, l
summary: query_row.get(3)?,
linked_incident_id: query_row.get(4)?,
author_id: query_row.get(5)?,
book_id: query_row.get(6)?,
_book_id: query_row.get(6)?,
last_update: query_row.get(7)?,
})
})

View File

@@ -10,14 +10,6 @@ use crate::error::AppResult;
use crate::helpers::{create_unique_id, timestamp_in_seconds};
use crate::shared::types::Lang;
/// Represents the story details associated with a plot point.
pub struct PlotPointStory {
pub plot_title: String,
pub plot_summary: String,
pub chapter_summary: String,
pub chapter_goal: String,
}
#[derive(Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PlotPointProps {
@@ -28,13 +20,6 @@ pub struct PlotPointProps {
pub chapters: Option<Vec<ActChapter>>,
}
/// Represents a synced plot point with minimal information.
pub struct SyncedPlotPoint {
pub id: String,
pub name: String,
pub last_update: i64,
}
/// Retrieves all plot points for a specific book with their associated chapters.
/// Decrypts plot point titles and summaries using the user's encryption key.
/// * `conn` - Database connection

View File

@@ -5,16 +5,16 @@ use crate::shared::types::Lang;
pub struct SeriesResult {
pub series_id: String,
pub user_id: String,
pub _user_id: String,
pub name: String,
pub hashed_name: String,
pub _hashed_name: String,
pub description: Option<String>,
pub cover_image: Option<String>,
pub last_update: i64,
pub _last_update: i64,
}
pub struct SeriesBookResult {
pub series_id: String,
pub _series_id: String,
pub book_id: String,
pub book_order: i64,
pub title: String,
@@ -105,10 +105,10 @@ pub fn fetch_series_by_id(conn: &Connection, user_id: &str, series_id: &str, lan
let series = statement
.query_row(params![series_id, user_id], |query_row| {
Ok(SeriesResult {
series_id: query_row.get(0)?, user_id: query_row.get(1)?,
name: query_row.get(2)?, hashed_name: query_row.get(3)?,
series_id: query_row.get(0)?, _user_id: query_row.get(1)?,
name: query_row.get(2)?, _hashed_name: query_row.get(3)?,
description: query_row.get(4)?, cover_image: query_row.get(5)?,
last_update: query_row.get(6)?,
_last_update: query_row.get(6)?,
})
});
@@ -199,7 +199,7 @@ pub fn fetch_series_books(conn: &Connection, user_id: &str, series_id: &str, lan
let books = statement
.query_map(params![series_id, user_id], |query_row| {
Ok(SeriesBookResult {
series_id: query_row.get(0)?, book_id: query_row.get(1)?,
_series_id: query_row.get(0)?, book_id: query_row.get(1)?,
book_order: query_row.get(2)?, title: query_row.get(3)?,
cover_image: query_row.get(4)?,
})
@@ -403,32 +403,6 @@ pub fn fetch_synced_series_books(conn: &Connection, user_id: &str, lang: Lang) -
Ok(books)
}
/// Fetches a complete series by ID for sync.
/// * `conn` - Database connection
/// * `series_id` - The unique identifier of the series
/// * `lang` - The language for error messages ("fr" or "en")
/// Returns an array containing the series.
pub fn fetch_complete_series_by_id(conn: &Connection, series_id: &str, lang: Lang) -> AppResult<Vec<SeriesTableResult>> {
let mut statement = conn
.prepare("SELECT series_id, user_id, name, hashed_name, description, cover_image, last_update FROM book_series WHERE series_id = ?1")
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer la série complète.".to_string() } else { "Unable to retrieve complete series.".to_string() }))?;
let series = statement
.query_map(params![series_id], |query_row| {
Ok(SeriesTableResult {
series_id: query_row.get(0)?, user_id: query_row.get(1)?,
name: query_row.get(2)?, hashed_name: query_row.get(3)?,
description: query_row.get(4)?, cover_image: query_row.get(5)?,
last_update: query_row.get(6)?,
})
})
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer la série complète.".to_string() } else { "Unable to retrieve complete series.".to_string() }))?
.collect::<Result<Vec<_>, _>>()
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer la série complète.".to_string() } else { "Unable to retrieve complete series.".to_string() }))?;
Ok(series)
}
/// Inserts a series for sync purposes.
/// * `conn` - Database connection
/// * `series_id` - The unique identifier of the series
@@ -498,27 +472,6 @@ pub fn insert_sync_series_book(conn: &Connection, series_id: &str, book_id: &str
Ok(insert_result > 0)
}
/// Checks if a series-book relationship exists.
/// * `conn` - Database connection
/// * `series_id` - The unique identifier of the series
/// * `book_id` - The unique identifier of the book
/// * `lang` - The language for error messages ("fr" or "en")
/// Returns true if the relationship exists.
pub fn is_series_book_exist(conn: &Connection, series_id: &str, book_id: &str, lang: Lang) -> AppResult<bool> {
let mut statement = conn
.prepare("SELECT 1 FROM series_books WHERE series_id = ?1 AND book_id = ?2")
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de vérifier la liaison série-livre.".to_string() } else { "Unable to check series-book.".to_string() }))?;
let exists = statement
.query_row(params![series_id, book_id], |_query_row| Ok(true));
match exists {
Ok(_) => Ok(true),
Err(rusqlite::Error::QueryReturnedNoRows) => Ok(false),
Err(_) => Err(AppError::Internal(if lang == Lang::Fr { "Impossible de vérifier la liaison série-livre.".to_string() } else { "Unable to check series-book.".to_string() })),
}
}
/// Checks if a series exists for a user (alias for is_series_exist).
/// * `conn` - Database connection
/// * `user_id` - The unique identifier of the user

View File

@@ -9,15 +9,6 @@ use crate::error::{AppError, AppResult};
use crate::helpers::{create_unique_id, timestamp_in_seconds};
use crate::shared::types::Lang;
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
pub struct SeriesProps {
pub id: String,
pub name: String,
pub description: String,
pub cover_image: Option<String>,
}
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
pub struct SeriesDetailProps {

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,499 +1,349 @@
use rusqlite::{params, Connection};
use crate::error::{AppError, AppResult};
use crate::shared::types::Lang;
pub struct SeriesWorldResult {
pub world_id: String,
pub world_name: String,
pub history: Option<String>,
pub politics: Option<String>,
pub economy: Option<String>,
pub religion: Option<String>,
pub languages: Option<String>,
pub element_id: Option<String>,
pub element_name: Option<String>,
pub element_description: Option<String>,
pub element_type: Option<i64>,
}
pub struct SeriesWorldsTableResult {
pub world_id: String,
pub series_id: String,
pub user_id: String,
pub name: String,
pub hashed_name: String,
pub history: Option<String>,
pub politics: Option<String>,
pub economy: Option<String>,
pub religion: Option<String>,
pub languages: Option<String>,
pub last_update: i64,
}
pub struct SeriesWorldElementsTableResult {
pub element_id: String,
pub world_id: String,
pub user_id: String,
pub element_type: i64,
pub name: String,
pub original_name: String,
pub description: Option<String>,
pub last_update: i64,
}
pub struct SyncedSeriesWorldResult {
pub world_id: String,
pub series_id: String,
pub name: String,
pub last_update: i64,
}
pub struct SyncedSeriesWorldElementResult {
pub element_id: String,
pub world_id: String,
pub name: String,
pub last_update: i64,
}
/// Checks if a world with the given hashed name already exists for a user and series.
pub fn check_world_exist(conn: &Connection, user_id: &str, series_id: &str, world_name: &str, lang: Lang) -> AppResult<bool> {
let mut statement = conn
.prepare("SELECT world_id FROM series_worlds WHERE user_id=?1 AND series_id=?2 AND hashed_name=?3")
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de vérifier l'existence du monde.".to_string() } else { "Unable to verify world existence.".to_string() }))?;
let exists = statement
.exists(params![user_id, series_id, world_name])
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de vérifier l'existence du monde.".to_string() } else { "Unable to verify world existence.".to_string() }))?;
Ok(exists)
}
/// Inserts a new world into the series.
pub fn insert_new_world(conn: &Connection, world_id: &str, user_id: &str, series_id: &str, encrypted_name: &str, hashed_name: &str, last_update: i64, lang: Lang) -> AppResult<String> {
let insert_result = conn
.execute(
"INSERT INTO series_worlds (world_id, user_id, series_id, name, hashed_name, last_update) VALUES (?1,?2,?3,?4,?5,?6)",
params![world_id, user_id, series_id, encrypted_name, hashed_name, last_update],
)
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible d'ajouter le monde.".to_string() } else { "Unable to add world.".to_string() }))?;
if insert_result > 0 {
Ok(world_id.to_string())
} else {
Err(AppError::Internal(if lang == Lang::Fr { "Erreur lors de l'ajout du monde.".to_string() } else { "Error adding world.".to_string() }))
}
}
/// Fetches all worlds and their elements for a given series.
pub fn fetch_worlds(conn: &Connection, user_id: &str, series_id: &str, lang: Lang) -> AppResult<Vec<SeriesWorldResult>> {
let mut statement = conn
.prepare("SELECT world.world_id AS world_id, world.name AS world_name, world.history, world.politics, world.economy, world.religion, world.languages, element.element_id AS element_id, element.name AS element_name, element.description AS element_description, element.element_type FROM series_worlds AS world LEFT JOIN series_world_elements AS element ON world.world_id = element.world_id WHERE world.user_id = ?1 AND world.series_id = ?2")
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les mondes.".to_string() } else { "Unable to retrieve worlds.".to_string() }))?;
let worlds = statement
.query_map(params![user_id, series_id], |query_row| {
Ok(SeriesWorldResult {
world_id: query_row.get(0)?, world_name: query_row.get(1)?,
history: query_row.get(2)?, politics: query_row.get(3)?,
economy: query_row.get(4)?, religion: query_row.get(5)?,
languages: query_row.get(6)?, element_id: query_row.get(7)?,
element_name: query_row.get(8)?, element_description: query_row.get(9)?,
element_type: query_row.get(10)?,
})
})
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les mondes.".to_string() } else { "Unable to retrieve worlds.".to_string() }))?
.collect::<Result<Vec<_>, _>>()
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les mondes.".to_string() } else { "Unable to retrieve worlds.".to_string() }))?;
Ok(worlds)
}
/// Updates a world's information.
pub fn update_world(
conn: &Connection, user_id: &str, world_id: &str, encrypted_name: &str, hashed_name: &str,
history: Option<&str>, politics: Option<&str>, economy: Option<&str>, religion: Option<&str>,
languages: Option<&str>, last_update: i64, lang: Lang,
) -> AppResult<bool> {
let update_result = conn
.execute(
"UPDATE series_worlds SET name=?1, hashed_name=?2, history=?3, politics=?4, economy=?5, religion=?6, languages=?7, last_update=?8 WHERE world_id=?9 AND user_id=?10",
params![encrypted_name, hashed_name, history, politics, economy, religion, languages, last_update, world_id, user_id],
)
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de mettre à jour le monde.".to_string() } else { "Unable to update world.".to_string() }))?;
Ok(update_result > 0)
}
/// Inserts a new element for a world.
pub fn insert_element(
conn: &Connection, element_id: &str, world_id: &str, user_id: &str, element_type: i64,
encrypted_name: &str, original_name: &str, description: Option<&str>, last_update: i64, lang: Lang,
) -> AppResult<String> {
let insert_result = conn
.execute(
"INSERT INTO series_world_elements (element_id, world_id, user_id, element_type, name, original_name, description, last_update) VALUES (?1,?2,?3,?4,?5,?6,?7,?8)",
params![element_id, world_id, user_id, element_type, encrypted_name, original_name, description, last_update],
)
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible d'ajouter l'élément.".to_string() } else { "Unable to add element.".to_string() }))?;
if insert_result > 0 {
Ok(element_id.to_string())
} else {
Err(AppError::Internal(if lang == Lang::Fr { "Erreur lors de l'ajout de l'élément.".to_string() } else { "Error adding element.".to_string() }))
}
}
/// Deletes an element from a world.
pub fn delete_element(conn: &Connection, user_id: &str, element_id: &str, lang: Lang) -> AppResult<bool> {
let delete_result = conn
.execute(
"DELETE FROM series_world_elements WHERE element_id=?1 AND user_id=?2",
params![element_id, user_id],
)
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de supprimer l'élément.".to_string() } else { "Unable to delete element.".to_string() }))?;
Ok(delete_result > 0)
}
/// Fetches all worlds for a series for sync.
pub fn fetch_series_worlds_table(conn: &Connection, user_id: &str, series_id: &str, lang: Lang) -> AppResult<Vec<SeriesWorldsTableResult>> {
let mut statement = conn
.prepare("SELECT world_id, series_id, user_id, name, hashed_name, history, politics, economy, religion, languages, last_update FROM series_worlds WHERE series_id = ?1 AND user_id = ?2")
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les mondes pour sync.".to_string() } else { "Unable to retrieve worlds for sync.".to_string() }))?;
let worlds = statement
.query_map(params![series_id, user_id], |query_row| {
Ok(SeriesWorldsTableResult {
world_id: query_row.get(0)?, series_id: query_row.get(1)?,
user_id: query_row.get(2)?, name: query_row.get(3)?,
hashed_name: query_row.get(4)?, history: query_row.get(5)?,
politics: query_row.get(6)?, economy: query_row.get(7)?,
religion: query_row.get(8)?, languages: query_row.get(9)?,
last_update: query_row.get(10)?,
})
})
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les mondes pour sync.".to_string() } else { "Unable to retrieve worlds for sync.".to_string() }))?
.collect::<Result<Vec<_>, _>>()
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les mondes pour sync.".to_string() } else { "Unable to retrieve worlds for sync.".to_string() }))?;
Ok(worlds)
}
/// Fetches all elements for a world for sync.
pub fn fetch_series_world_elements_table(conn: &Connection, user_id: &str, world_id: &str, lang: Lang) -> AppResult<Vec<SeriesWorldElementsTableResult>> {
let mut statement = conn
.prepare("SELECT element_id, world_id, user_id, element_type, name, original_name, description, last_update FROM series_world_elements WHERE world_id = ?1 AND user_id = ?2")
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les éléments de monde pour sync.".to_string() } else { "Unable to retrieve world elements for sync.".to_string() }))?;
let elements = statement
.query_map(params![world_id, user_id], |query_row| {
Ok(SeriesWorldElementsTableResult {
element_id: query_row.get(0)?, world_id: query_row.get(1)?,
user_id: query_row.get(2)?, element_type: query_row.get(3)?,
name: query_row.get(4)?, original_name: query_row.get(5)?,
description: query_row.get(6)?, last_update: query_row.get(7)?,
})
})
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les éléments de monde pour sync.".to_string() } else { "Unable to retrieve world elements for sync.".to_string() }))?
.collect::<Result<Vec<_>, _>>()
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les éléments de monde pour sync.".to_string() } else { "Unable to retrieve world elements for sync.".to_string() }))?;
Ok(elements)
}
/// Fetches all series worlds for a user for sync comparison.
pub fn fetch_synced_series_worlds(conn: &Connection, user_id: &str, lang: Lang) -> AppResult<Vec<SyncedSeriesWorldResult>> {
let mut statement = conn
.prepare("SELECT world_id, series_id, name, last_update FROM series_worlds WHERE user_id = ?1")
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les mondes de série pour sync.".to_string() } else { "Unable to retrieve series worlds for sync.".to_string() }))?;
let worlds = statement
.query_map(params![user_id], |query_row| {
Ok(SyncedSeriesWorldResult { world_id: query_row.get(0)?, series_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 mondes de série pour sync.".to_string() } else { "Unable to retrieve series worlds for sync.".to_string() }))?
.collect::<Result<Vec<_>, _>>()
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les mondes de série pour sync.".to_string() } else { "Unable to retrieve series worlds for sync.".to_string() }))?;
Ok(worlds)
}
/// Fetches all series world elements for a user for sync comparison.
pub fn fetch_synced_series_world_elements(conn: &Connection, user_id: &str, lang: Lang) -> AppResult<Vec<SyncedSeriesWorldElementResult>> {
let mut statement = conn
.prepare("SELECT element_id, world_id, name, last_update FROM series_world_elements WHERE user_id = ?1")
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les éléments de monde pour sync.".to_string() } else { "Unable to retrieve world elements for sync.".to_string() }))?;
let elements = statement
.query_map(params![user_id], |query_row| {
Ok(SyncedSeriesWorldElementResult { element_id: query_row.get(0)?, world_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 éléments de monde pour sync.".to_string() } else { "Unable to retrieve world elements for sync.".to_string() }))?
.collect::<Result<Vec<_>, _>>()
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les éléments de monde pour sync.".to_string() } else { "Unable to retrieve world elements for sync.".to_string() }))?;
Ok(elements)
}
/// Fetches a complete world by ID for sync.
pub fn fetch_complete_world_by_id(conn: &Connection, world_id: &str, lang: Lang) -> AppResult<Vec<SeriesWorldsTableResult>> {
let mut statement = conn
.prepare("SELECT world_id, series_id, user_id, name, hashed_name, history, politics, economy, religion, languages, last_update FROM series_worlds WHERE world_id = ?1")
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer le monde complet.".to_string() } else { "Unable to retrieve complete world.".to_string() }))?;
let worlds = statement
.query_map(params![world_id], |query_row| {
Ok(SeriesWorldsTableResult {
world_id: query_row.get(0)?, series_id: query_row.get(1)?,
user_id: query_row.get(2)?, name: query_row.get(3)?,
hashed_name: query_row.get(4)?, history: query_row.get(5)?,
politics: query_row.get(6)?, economy: query_row.get(7)?,
religion: query_row.get(8)?, languages: query_row.get(9)?,
last_update: query_row.get(10)?,
})
})
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer le monde complet.".to_string() } else { "Unable to retrieve complete world.".to_string() }))?
.collect::<Result<Vec<_>, _>>()
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer le monde complet.".to_string() } else { "Unable to retrieve complete world.".to_string() }))?;
Ok(worlds)
}
/// Fetches a complete world element by ID for sync.
pub fn fetch_complete_world_element_by_id(conn: &Connection, element_id: &str, lang: Lang) -> AppResult<Vec<SeriesWorldElementsTableResult>> {
let mut statement = conn
.prepare("SELECT element_id, world_id, user_id, element_type, name, original_name, description, last_update FROM series_world_elements WHERE element_id = ?1")
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer l'élément de monde complet.".to_string() } else { "Unable to retrieve complete world element.".to_string() }))?;
let elements = statement
.query_map(params![element_id], |query_row| {
Ok(SeriesWorldElementsTableResult {
element_id: query_row.get(0)?, world_id: query_row.get(1)?,
user_id: query_row.get(2)?, element_type: query_row.get(3)?,
name: query_row.get(4)?, original_name: query_row.get(5)?,
description: query_row.get(6)?, last_update: query_row.get(7)?,
})
})
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer l'élément de monde complet.".to_string() } else { "Unable to retrieve complete world element.".to_string() }))?
.collect::<Result<Vec<_>, _>>()
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer l'élément de monde complet.".to_string() } else { "Unable to retrieve complete world element.".to_string() }))?;
Ok(elements)
}
/// Checks if a world exists.
pub fn is_world_exist(conn: &Connection, user_id: &str, world_id: &str, lang: Lang) -> AppResult<bool> {
let mut statement = conn
.prepare("SELECT 1 FROM series_worlds WHERE world_id=?1 AND user_id=?2")
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de vérifier l'existence du monde.".to_string() } else { "Unable to check world existence.".to_string() }))?;
let exists = statement
.exists(params![world_id, user_id])
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de vérifier l'existence du monde.".to_string() } else { "Unable to check world existence.".to_string() }))?;
Ok(exists)
}
/// Checks if a world element exists.
pub fn is_world_element_exist(conn: &Connection, user_id: &str, element_id: &str, lang: Lang) -> AppResult<bool> {
let mut statement = conn
.prepare("SELECT 1 FROM series_world_elements WHERE element_id=?1 AND user_id=?2")
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de vérifier l'existence de l'élément.".to_string() } else { "Unable to check element existence.".to_string() }))?;
let exists = statement
.exists(params![element_id, user_id])
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de vérifier l'existence de l'élément.".to_string() } else { "Unable to check element existence.".to_string() }))?;
Ok(exists)
}
/// Inserts a series world for sync.
pub fn insert_sync_world(
conn: &Connection, world_id: &str, series_id: &str, user_id: &str, name: &str, hashed_name: &str,
history: Option<&str>, politics: Option<&str>, economy: Option<&str>, religion: Option<&str>,
languages: Option<&str>, last_update: i64, lang: Lang,
) -> AppResult<bool> {
let insert_result = conn
.execute(
"INSERT INTO series_worlds (world_id, series_id, user_id, name, hashed_name, history, politics, economy, religion, languages, last_update) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11) ON CONFLICT(world_id) DO UPDATE SET name = excluded.name, hashed_name = excluded.hashed_name, history = excluded.history, politics = excluded.politics, economy = excluded.economy, religion = excluded.religion, languages = excluded.languages, last_update = excluded.last_update",
params![world_id, series_id, user_id, name, hashed_name, history, politics, economy, religion, languages, last_update],
)
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible d'insérer le monde pour sync.".to_string() } else { "Unable to insert world for sync.".to_string() }))?;
Ok(insert_result > 0)
}
/// Updates a series world for sync.
pub fn update_sync_world(
conn: &Connection, user_id: &str, world_id: &str, name: &str, hashed_name: &str,
history: Option<&str>, politics: Option<&str>, economy: Option<&str>, religion: Option<&str>,
languages: Option<&str>, last_update: i64, lang: Lang,
) -> AppResult<bool> {
let update_result = conn
.execute(
"UPDATE series_worlds SET name = ?1, hashed_name = ?2, history = ?3, politics = ?4, economy = ?5, religion = ?6, languages = ?7, last_update = ?8 WHERE world_id = ?9 AND user_id = ?10",
params![name, hashed_name, history, politics, economy, religion, languages, last_update, world_id, user_id],
)
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de mettre à jour le monde pour sync.".to_string() } else { "Unable to update world for sync.".to_string() }))?;
Ok(update_result > 0)
}
/// Inserts a series world element for sync.
pub fn insert_sync_world_element(
conn: &Connection, element_id: &str, world_id: &str, user_id: &str, element_type: i64,
name: &str, original_name: &str, description: Option<&str>, last_update: i64, lang: Lang,
) -> AppResult<bool> {
let insert_result = conn
.execute(
"INSERT INTO series_world_elements (element_id, world_id, user_id, element_type, name, original_name, description, last_update) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8) ON CONFLICT(element_id) DO UPDATE SET element_type = excluded.element_type, name = excluded.name, original_name = excluded.original_name, description = excluded.description, last_update = excluded.last_update",
params![element_id, world_id, user_id, element_type, name, original_name, description, last_update],
)
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible d'insérer l'élément de monde pour sync.".to_string() } else { "Unable to insert world element for sync.".to_string() }))?;
Ok(insert_result > 0)
}
/// Updates a series world element for sync.
pub fn update_sync_world_element(
conn: &Connection, user_id: &str, element_id: &str, element_type: i64, name: &str,
original_name: &str, description: Option<&str>, last_update: i64, lang: Lang,
) -> AppResult<bool> {
let update_result = conn
.execute(
"UPDATE series_world_elements SET element_type = ?1, name = ?2, original_name = ?3, description = ?4, last_update = ?5 WHERE element_id = ?6 AND user_id = ?7",
params![element_type, name, original_name, description, last_update, element_id, user_id],
)
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de mettre à jour l'élément de monde pour sync.".to_string() } else { "Unable to update world element for sync.".to_string() }))?;
Ok(update_result > 0)
}
/// Fetches all worlds for a series for sync (without user filter).
pub fn fetch_worlds_table_for_sync(conn: &Connection, series_id: &str, lang: Lang) -> AppResult<Vec<SeriesWorldsTableResult>> {
let mut statement = conn
.prepare("SELECT world_id, series_id, user_id, name, hashed_name, history, politics, economy, religion, languages, last_update FROM series_worlds WHERE series_id = ?1")
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les mondes pour sync.".to_string() } else { "Unable to retrieve worlds for sync.".to_string() }))?;
let worlds = statement
.query_map(params![series_id], |query_row| {
Ok(SeriesWorldsTableResult {
world_id: query_row.get(0)?, series_id: query_row.get(1)?,
user_id: query_row.get(2)?, name: query_row.get(3)?,
hashed_name: query_row.get(4)?, history: query_row.get(5)?,
politics: query_row.get(6)?, economy: query_row.get(7)?,
religion: query_row.get(8)?, languages: query_row.get(9)?,
last_update: query_row.get(10)?,
})
})
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les mondes pour sync.".to_string() } else { "Unable to retrieve worlds for sync.".to_string() }))?
.collect::<Result<Vec<_>, _>>()
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les mondes pour sync.".to_string() } else { "Unable to retrieve worlds for sync.".to_string() }))?;
Ok(worlds)
}
/// Fetches all world elements for a series for sync (via series_worlds join).
pub fn fetch_world_elements_table_for_sync(conn: &Connection, series_id: &str, lang: Lang) -> AppResult<Vec<SeriesWorldElementsTableResult>> {
let mut statement = conn
.prepare("SELECT swe.element_id, swe.world_id, swe.user_id, swe.element_type, swe.name, swe.original_name, swe.description, swe.last_update FROM series_world_elements swe INNER JOIN series_worlds sw ON swe.world_id = sw.world_id WHERE sw.series_id = ?1")
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les éléments de monde pour sync.".to_string() } else { "Unable to retrieve world elements for sync.".to_string() }))?;
let elements = statement
.query_map(params![series_id], |query_row| {
Ok(SeriesWorldElementsTableResult {
element_id: query_row.get(0)?, world_id: query_row.get(1)?,
user_id: query_row.get(2)?, element_type: query_row.get(3)?,
name: query_row.get(4)?, original_name: query_row.get(5)?,
description: query_row.get(6)?, last_update: query_row.get(7)?,
})
})
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les éléments de monde pour sync.".to_string() } else { "Unable to retrieve world elements for sync.".to_string() }))?
.collect::<Result<Vec<_>, _>>()
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les éléments de monde pour sync.".to_string() } else { "Unable to retrieve world elements for sync.".to_string() }))?;
Ok(elements)
}
/// Fetches all worlds for a series (alias for fetch_series_worlds_table).
pub fn fetch_series_worlds(conn: &Connection, user_id: &str, series_id: &str, lang: Lang) -> AppResult<Vec<SeriesWorldsTableResult>> {
fetch_series_worlds_table(conn, user_id, series_id, lang)
}
/// Fetches all world elements for a series by series ID.
pub fn fetch_series_world_elements_by_series_id(conn: &Connection, user_id: &str, series_id: &str, lang: Lang) -> AppResult<Vec<SeriesWorldElementsTableResult>> {
let mut statement = conn
.prepare("SELECT swe.element_id, swe.world_id, swe.user_id, swe.element_type, swe.name, swe.original_name, swe.description, swe.last_update FROM series_world_elements swe INNER JOIN series_worlds sw ON swe.world_id = sw.world_id WHERE sw.series_id = ?1 AND sw.user_id = ?2")
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les éléments de monde par série.".to_string() } else { "Unable to retrieve world elements by series.".to_string() }))?;
let elements = statement
.query_map(params![series_id, user_id], |query_row| {
Ok(SeriesWorldElementsTableResult {
element_id: query_row.get(0)?, world_id: query_row.get(1)?,
user_id: query_row.get(2)?, element_type: query_row.get(3)?,
name: query_row.get(4)?, original_name: query_row.get(5)?,
description: query_row.get(6)?, last_update: query_row.get(7)?,
})
})
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les éléments de monde par série.".to_string() } else { "Unable to retrieve world elements by series.".to_string() }))?
.collect::<Result<Vec<_>, _>>()
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les éléments de monde par série.".to_string() } else { "Unable to retrieve world elements by series.".to_string() }))?;
Ok(elements)
}
/// Checks if a series world exists (alias for is_world_exist).
pub fn series_world_exists(conn: &Connection, user_id: &str, world_id: &str, lang: Lang) -> AppResult<bool> {
is_world_exist(conn, user_id, world_id, lang)
}
/// Checks if a series world element exists (alias for is_world_element_exist).
pub fn series_world_element_exists(conn: &Connection, user_id: &str, element_id: &str, lang: Lang) -> AppResult<bool> {
is_world_element_exist(conn, user_id, element_id, lang)
}
/// Inserts a series world for sync (alias with compatible signature).
pub fn insert_sync_series_world(
conn: &Connection, world_id: &str, series_id: &str, user_id: &str, name: &str, hashed_name: &str,
history: Option<&str>, politics: Option<&str>, economy: Option<&str>, religion: Option<&str>,
languages: Option<&str>, last_update: i64, lang: Lang,
) -> AppResult<bool> {
insert_sync_world(conn, world_id, series_id, user_id, name, hashed_name, history, politics, economy, religion, languages, last_update, lang)
}
/// Updates a series world for sync (without hashed_name).
pub fn update_sync_series_world(
conn: &Connection, world_id: &str, user_id: &str, name: &str, history: Option<&str>,
politics: Option<&str>, economy: Option<&str>, religion: Option<&str>, languages: Option<&str>,
last_update: i64, lang: Lang,
) -> AppResult<bool> {
let update_result = conn
.execute(
"UPDATE series_worlds SET name = ?1, history = ?2, politics = ?3, economy = ?4, religion = ?5, languages = ?6, last_update = ?7 WHERE world_id = ?8 AND user_id = ?9",
params![name, history, politics, economy, religion, languages, last_update, world_id, user_id],
)
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de mettre à jour le monde série pour sync.".to_string() } else { "Unable to update series world for sync.".to_string() }))?;
Ok(update_result > 0)
}
/// Inserts a series world element for sync (alias with compatible signature).
pub fn insert_sync_series_world_element(
conn: &Connection, element_id: &str, world_id: &str, user_id: &str, element_type: i64,
name: &str, original_name: &str, description: Option<&str>, last_update: i64, lang: Lang,
) -> AppResult<bool> {
insert_sync_world_element(conn, element_id, world_id, user_id, element_type, name, original_name, description, last_update, lang)
}
/// Updates a series world element for sync (without element_type and original_name).
pub fn update_sync_series_world_element(conn: &Connection, element_id: &str, user_id: &str, name: &str, description: Option<&str>, last_update: i64, lang: Lang) -> AppResult<bool> {
let update_result = conn
.execute(
"UPDATE series_world_elements SET name = ?1, description = ?2, last_update = ?3 WHERE element_id = ?4 AND user_id = ?5",
params![name, description, last_update, element_id, user_id],
)
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de mettre à jour l'élément de monde série pour sync.".to_string() } else { "Unable to update series world element for sync.".to_string() }))?;
Ok(update_result > 0)
}
use rusqlite::{params, Connection};
use crate::error::{AppError, AppResult};
use crate::shared::types::Lang;
pub struct SeriesWorldResult {
pub world_id: String,
pub world_name: String,
pub history: Option<String>,
pub politics: Option<String>,
pub economy: Option<String>,
pub religion: Option<String>,
pub languages: Option<String>,
pub element_id: Option<String>,
pub element_name: Option<String>,
pub element_description: Option<String>,
pub element_type: Option<i64>,
}
pub struct SeriesWorldsTableResult {
pub world_id: String,
pub series_id: String,
pub user_id: String,
pub name: String,
pub hashed_name: String,
pub history: Option<String>,
pub politics: Option<String>,
pub economy: Option<String>,
pub religion: Option<String>,
pub languages: Option<String>,
pub last_update: i64,
}
pub struct SeriesWorldElementsTableResult {
pub element_id: String,
pub world_id: String,
pub user_id: String,
pub element_type: i64,
pub name: String,
pub original_name: String,
pub description: Option<String>,
pub last_update: i64,
}
pub struct SyncedSeriesWorldResult {
pub world_id: String,
pub series_id: String,
pub name: String,
pub last_update: i64,
}
pub struct SyncedSeriesWorldElementResult {
pub element_id: String,
pub world_id: String,
pub name: String,
pub last_update: i64,
}
/// Checks if a world with the given hashed name already exists for a user and series.
pub fn check_world_exist(conn: &Connection, user_id: &str, series_id: &str, world_name: &str, lang: Lang) -> AppResult<bool> {
let mut statement = conn
.prepare("SELECT world_id FROM series_worlds WHERE user_id=?1 AND series_id=?2 AND hashed_name=?3")
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de vérifier l'existence du monde.".to_string() } else { "Unable to verify world existence.".to_string() }))?;
let exists = statement
.exists(params![user_id, series_id, world_name])
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de vérifier l'existence du monde.".to_string() } else { "Unable to verify world existence.".to_string() }))?;
Ok(exists)
}
/// Inserts a new world into the series.
pub fn insert_new_world(conn: &Connection, world_id: &str, user_id: &str, series_id: &str, encrypted_name: &str, hashed_name: &str, last_update: i64, lang: Lang) -> AppResult<String> {
let insert_result = conn
.execute(
"INSERT INTO series_worlds (world_id, user_id, series_id, name, hashed_name, last_update) VALUES (?1,?2,?3,?4,?5,?6)",
params![world_id, user_id, series_id, encrypted_name, hashed_name, last_update],
)
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible d'ajouter le monde.".to_string() } else { "Unable to add world.".to_string() }))?;
if insert_result > 0 {
Ok(world_id.to_string())
} else {
Err(AppError::Internal(if lang == Lang::Fr { "Erreur lors de l'ajout du monde.".to_string() } else { "Error adding world.".to_string() }))
}
}
/// Fetches all worlds and their elements for a given series.
pub fn fetch_worlds(conn: &Connection, user_id: &str, series_id: &str, lang: Lang) -> AppResult<Vec<SeriesWorldResult>> {
let mut statement = conn
.prepare("SELECT world.world_id AS world_id, world.name AS world_name, world.history, world.politics, world.economy, world.religion, world.languages, element.element_id AS element_id, element.name AS element_name, element.description AS element_description, element.element_type FROM series_worlds AS world LEFT JOIN series_world_elements AS element ON world.world_id = element.world_id WHERE world.user_id = ?1 AND world.series_id = ?2")
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les mondes.".to_string() } else { "Unable to retrieve worlds.".to_string() }))?;
let worlds = statement
.query_map(params![user_id, series_id], |query_row| {
Ok(SeriesWorldResult {
world_id: query_row.get(0)?, world_name: query_row.get(1)?,
history: query_row.get(2)?, politics: query_row.get(3)?,
economy: query_row.get(4)?, religion: query_row.get(5)?,
languages: query_row.get(6)?, element_id: query_row.get(7)?,
element_name: query_row.get(8)?, element_description: query_row.get(9)?,
element_type: query_row.get(10)?,
})
})
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les mondes.".to_string() } else { "Unable to retrieve worlds.".to_string() }))?
.collect::<Result<Vec<_>, _>>()
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les mondes.".to_string() } else { "Unable to retrieve worlds.".to_string() }))?;
Ok(worlds)
}
/// Updates a world's information.
pub fn update_world(
conn: &Connection, user_id: &str, world_id: &str, encrypted_name: &str, hashed_name: &str,
history: Option<&str>, politics: Option<&str>, economy: Option<&str>, religion: Option<&str>,
languages: Option<&str>, last_update: i64, lang: Lang,
) -> AppResult<bool> {
let update_result = conn
.execute(
"UPDATE series_worlds SET name=?1, hashed_name=?2, history=?3, politics=?4, economy=?5, religion=?6, languages=?7, last_update=?8 WHERE world_id=?9 AND user_id=?10",
params![encrypted_name, hashed_name, history, politics, economy, religion, languages, last_update, world_id, user_id],
)
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de mettre à jour le monde.".to_string() } else { "Unable to update world.".to_string() }))?;
Ok(update_result > 0)
}
/// Inserts a new element for a world.
pub fn insert_element(
conn: &Connection, element_id: &str, world_id: &str, user_id: &str, element_type: i64,
encrypted_name: &str, original_name: &str, description: Option<&str>, last_update: i64, lang: Lang,
) -> AppResult<String> {
let insert_result = conn
.execute(
"INSERT INTO series_world_elements (element_id, world_id, user_id, element_type, name, original_name, description, last_update) VALUES (?1,?2,?3,?4,?5,?6,?7,?8)",
params![element_id, world_id, user_id, element_type, encrypted_name, original_name, description, last_update],
)
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible d'ajouter l'élément.".to_string() } else { "Unable to add element.".to_string() }))?;
if insert_result > 0 {
Ok(element_id.to_string())
} else {
Err(AppError::Internal(if lang == Lang::Fr { "Erreur lors de l'ajout de l'élément.".to_string() } else { "Error adding element.".to_string() }))
}
}
/// Deletes an element from a world.
pub fn delete_element(conn: &Connection, user_id: &str, element_id: &str, lang: Lang) -> AppResult<bool> {
let delete_result = conn
.execute(
"DELETE FROM series_world_elements WHERE element_id=?1 AND user_id=?2",
params![element_id, user_id],
)
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de supprimer l'élément.".to_string() } else { "Unable to delete element.".to_string() }))?;
Ok(delete_result > 0)
}
/// Fetches all worlds for a series for sync.
pub fn fetch_series_worlds_table(conn: &Connection, user_id: &str, series_id: &str, lang: Lang) -> AppResult<Vec<SeriesWorldsTableResult>> {
let mut statement = conn
.prepare("SELECT world_id, series_id, user_id, name, hashed_name, history, politics, economy, religion, languages, last_update FROM series_worlds WHERE series_id = ?1 AND user_id = ?2")
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les mondes pour sync.".to_string() } else { "Unable to retrieve worlds for sync.".to_string() }))?;
let worlds = statement
.query_map(params![series_id, user_id], |query_row| {
Ok(SeriesWorldsTableResult {
world_id: query_row.get(0)?, series_id: query_row.get(1)?,
user_id: query_row.get(2)?, name: query_row.get(3)?,
hashed_name: query_row.get(4)?, history: query_row.get(5)?,
politics: query_row.get(6)?, economy: query_row.get(7)?,
religion: query_row.get(8)?, languages: query_row.get(9)?,
last_update: query_row.get(10)?,
})
})
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les mondes pour sync.".to_string() } else { "Unable to retrieve worlds for sync.".to_string() }))?
.collect::<Result<Vec<_>, _>>()
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les mondes pour sync.".to_string() } else { "Unable to retrieve worlds for sync.".to_string() }))?;
Ok(worlds)
}
/// Fetches all series worlds for a user for sync comparison.
pub fn fetch_synced_series_worlds(conn: &Connection, user_id: &str, lang: Lang) -> AppResult<Vec<SyncedSeriesWorldResult>> {
let mut statement = conn
.prepare("SELECT world_id, series_id, name, last_update FROM series_worlds WHERE user_id = ?1")
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les mondes de série pour sync.".to_string() } else { "Unable to retrieve series worlds for sync.".to_string() }))?;
let worlds = statement
.query_map(params![user_id], |query_row| {
Ok(SyncedSeriesWorldResult { world_id: query_row.get(0)?, series_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 mondes de série pour sync.".to_string() } else { "Unable to retrieve series worlds for sync.".to_string() }))?
.collect::<Result<Vec<_>, _>>()
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les mondes de série pour sync.".to_string() } else { "Unable to retrieve series worlds for sync.".to_string() }))?;
Ok(worlds)
}
/// Fetches all series world elements for a user for sync comparison.
pub fn fetch_synced_series_world_elements(conn: &Connection, user_id: &str, lang: Lang) -> AppResult<Vec<SyncedSeriesWorldElementResult>> {
let mut statement = conn
.prepare("SELECT element_id, world_id, name, last_update FROM series_world_elements WHERE user_id = ?1")
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les éléments de monde pour sync.".to_string() } else { "Unable to retrieve world elements for sync.".to_string() }))?;
let elements = statement
.query_map(params![user_id], |query_row| {
Ok(SyncedSeriesWorldElementResult { element_id: query_row.get(0)?, world_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 éléments de monde pour sync.".to_string() } else { "Unable to retrieve world elements for sync.".to_string() }))?
.collect::<Result<Vec<_>, _>>()
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les éléments de monde pour sync.".to_string() } else { "Unable to retrieve world elements for sync.".to_string() }))?;
Ok(elements)
}
/// Checks if a world exists.
pub fn is_world_exist(conn: &Connection, user_id: &str, world_id: &str, lang: Lang) -> AppResult<bool> {
let mut statement = conn
.prepare("SELECT 1 FROM series_worlds WHERE world_id=?1 AND user_id=?2")
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de vérifier l'existence du monde.".to_string() } else { "Unable to check world existence.".to_string() }))?;
let exists = statement
.exists(params![world_id, user_id])
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de vérifier l'existence du monde.".to_string() } else { "Unable to check world existence.".to_string() }))?;
Ok(exists)
}
/// Checks if a world element exists.
pub fn is_world_element_exist(conn: &Connection, user_id: &str, element_id: &str, lang: Lang) -> AppResult<bool> {
let mut statement = conn
.prepare("SELECT 1 FROM series_world_elements WHERE element_id=?1 AND user_id=?2")
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de vérifier l'existence de l'élément.".to_string() } else { "Unable to check element existence.".to_string() }))?;
let exists = statement
.exists(params![element_id, user_id])
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de vérifier l'existence de l'élément.".to_string() } else { "Unable to check element existence.".to_string() }))?;
Ok(exists)
}
/// Inserts a series world for sync.
pub fn insert_sync_world(
conn: &Connection, world_id: &str, series_id: &str, user_id: &str, name: &str, hashed_name: &str,
history: Option<&str>, politics: Option<&str>, economy: Option<&str>, religion: Option<&str>,
languages: Option<&str>, last_update: i64, lang: Lang,
) -> AppResult<bool> {
let insert_result = conn
.execute(
"INSERT INTO series_worlds (world_id, series_id, user_id, name, hashed_name, history, politics, economy, religion, languages, last_update) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11) ON CONFLICT(world_id) DO UPDATE SET name = excluded.name, hashed_name = excluded.hashed_name, history = excluded.history, politics = excluded.politics, economy = excluded.economy, religion = excluded.religion, languages = excluded.languages, last_update = excluded.last_update",
params![world_id, series_id, user_id, name, hashed_name, history, politics, economy, religion, languages, last_update],
)
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible d'insérer le monde pour sync.".to_string() } else { "Unable to insert world for sync.".to_string() }))?;
Ok(insert_result > 0)
}
/// Inserts a series world element for sync.
pub fn insert_sync_world_element(
conn: &Connection, element_id: &str, world_id: &str, user_id: &str, element_type: i64,
name: &str, original_name: &str, description: Option<&str>, last_update: i64, lang: Lang,
) -> AppResult<bool> {
let insert_result = conn
.execute(
"INSERT INTO series_world_elements (element_id, world_id, user_id, element_type, name, original_name, description, last_update) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8) ON CONFLICT(element_id) DO UPDATE SET element_type = excluded.element_type, name = excluded.name, original_name = excluded.original_name, description = excluded.description, last_update = excluded.last_update",
params![element_id, world_id, user_id, element_type, name, original_name, description, last_update],
)
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible d'insérer l'élément de monde pour sync.".to_string() } else { "Unable to insert world element for sync.".to_string() }))?;
Ok(insert_result > 0)
}
/// Fetches all world elements for a series by series ID.
pub fn fetch_series_world_elements_by_series_id(conn: &Connection, user_id: &str, series_id: &str, lang: Lang) -> AppResult<Vec<SeriesWorldElementsTableResult>> {
let mut statement = conn
.prepare("SELECT swe.element_id, swe.world_id, swe.user_id, swe.element_type, swe.name, swe.original_name, swe.description, swe.last_update FROM series_world_elements swe INNER JOIN series_worlds sw ON swe.world_id = sw.world_id WHERE sw.series_id = ?1 AND sw.user_id = ?2")
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les éléments de monde par série.".to_string() } else { "Unable to retrieve world elements by series.".to_string() }))?;
let elements = statement
.query_map(params![series_id, user_id], |query_row| {
Ok(SeriesWorldElementsTableResult {
element_id: query_row.get(0)?, world_id: query_row.get(1)?,
user_id: query_row.get(2)?, element_type: query_row.get(3)?,
name: query_row.get(4)?, original_name: query_row.get(5)?,
description: query_row.get(6)?, last_update: query_row.get(7)?,
})
})
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les éléments de monde par série.".to_string() } else { "Unable to retrieve world elements by series.".to_string() }))?
.collect::<Result<Vec<_>, _>>()
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les éléments de monde par série.".to_string() } else { "Unable to retrieve world elements by series.".to_string() }))?;
Ok(elements)
}
/// Checks if a series world exists (alias for is_world_exist).
pub fn series_world_exists(conn: &Connection, user_id: &str, world_id: &str, lang: Lang) -> AppResult<bool> {
is_world_exist(conn, user_id, world_id, lang)
}
/// Checks if a series world element exists (alias for is_world_element_exist).
pub fn series_world_element_exists(conn: &Connection, user_id: &str, element_id: &str, lang: Lang) -> AppResult<bool> {
is_world_element_exist(conn, user_id, element_id, lang)
}
/// Inserts a series world for sync (alias with compatible signature).
pub fn insert_sync_series_world(
conn: &Connection, world_id: &str, series_id: &str, user_id: &str, name: &str, hashed_name: &str,
history: Option<&str>, politics: Option<&str>, economy: Option<&str>, religion: Option<&str>,
languages: Option<&str>, last_update: i64, lang: Lang,
) -> AppResult<bool> {
insert_sync_world(conn, world_id, series_id, user_id, name, hashed_name, history, politics, economy, religion, languages, last_update, lang)
}
/// Updates a series world for sync (without hashed_name).
pub fn update_sync_series_world(
conn: &Connection, world_id: &str, user_id: &str, name: &str, history: Option<&str>,
politics: Option<&str>, economy: Option<&str>, religion: Option<&str>, languages: Option<&str>,
last_update: i64, lang: Lang,
) -> AppResult<bool> {
let update_result = conn
.execute(
"UPDATE series_worlds SET name = ?1, history = ?2, politics = ?3, economy = ?4, religion = ?5, languages = ?6, last_update = ?7 WHERE world_id = ?8 AND user_id = ?9",
params![name, history, politics, economy, religion, languages, last_update, world_id, user_id],
)
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de mettre à jour le monde série pour sync.".to_string() } else { "Unable to update series world for sync.".to_string() }))?;
Ok(update_result > 0)
}
/// Inserts a series world element for sync (alias with compatible signature).
pub fn insert_sync_series_world_element(
conn: &Connection, element_id: &str, world_id: &str, user_id: &str, element_type: i64,
name: &str, original_name: &str, description: Option<&str>, last_update: i64, lang: Lang,
) -> AppResult<bool> {
insert_sync_world_element(conn, element_id, world_id, user_id, element_type, name, original_name, description, last_update, lang)
}
/// Updates a series world element for sync (without element_type and original_name).
pub fn update_sync_series_world_element(conn: &Connection, element_id: &str, user_id: &str, name: &str, description: Option<&str>, last_update: i64, lang: Lang) -> AppResult<bool> {
let update_result = conn
.execute(
"UPDATE series_world_elements SET name = ?1, description = ?2, last_update = ?3 WHERE element_id = ?4 AND user_id = ?5",
params![name, description, last_update, element_id, user_id],
)
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de mettre à jour l'élément de monde série pour sync.".to_string() } else { "Unable to update series world element for sync.".to_string() }))?;
Ok(update_result > 0)
}

View File

@@ -18,7 +18,6 @@ fn get_session(session: &State<SessionState>) -> Result<(String, Lang), AppError
#[serde(rename_all = "camelCase")]
pub struct GetSpellListData {
pub book_id: String,
pub enabled: bool,
}
#[tauri::command]

View File

@@ -5,7 +5,7 @@ use crate::shared::types::Lang;
pub struct SpellResult {
pub spell_id: String,
pub book_id: String,
pub _book_id: String,
pub name: String,
pub description: Option<String>,
pub appearance: Option<String>,
@@ -54,7 +54,7 @@ pub fn fetch_spells(conn: &Connection, user_id: &str, book_id: &str, lang: Lang)
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)?,
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)?,
@@ -83,7 +83,7 @@ pub fn fetch_spell_by_id(conn: &Connection, user_id: &str, spell_id: &str, lang:
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)?,
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)?,

View File

@@ -52,18 +52,6 @@ pub struct SpellListResponse {
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

View File

@@ -5,7 +5,7 @@ use crate::shared::types::Lang;
pub struct SpellTagResult {
pub tag_id: String,
pub book_id: String,
pub _book_id: String,
pub name: String,
pub color: Option<String>,
}
@@ -36,7 +36,7 @@ pub fn fetch_spell_tags(conn: &Connection, user_id: &str, book_id: &str, lang: L
let rows = statement
.query_map(params![user_id, book_id], |query_row| {
Ok(SpellTagResult {
tag_id: query_row.get(0)?, book_id: query_row.get(1)?,
tag_id: query_row.get(0)?, _book_id: query_row.get(1)?,
name: query_row.get(2)?, color: query_row.get(3)?,
})
})

View File

@@ -143,30 +143,6 @@ pub struct SyncedBookFull {
pub spell_tags: Vec<SyncedSpellTag>,
}
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
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>,
}
/// Retrieves a complete book with all its associated entities for synchronization.
/// Decrypts all encrypted fields using the user's encryption key.
/// * `conn` - Database connection

View File

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

View File

@@ -4,11 +4,11 @@ use crate::error::{AppError, AppResult};
use crate::shared::types::Lang;
pub struct RemovedItemRecord {
pub removal_id: String,
pub _removal_id: String,
pub table_name: String,
pub entity_id: String,
pub book_id: Option<String>,
pub user_id: String,
pub _user_id: String,
pub deleted_at: i64,
}
@@ -51,9 +51,9 @@ pub fn get_deletions_since(conn: &Connection, user_id: &str, since: i64, lang: L
let records = statement
.query_map(params![user_id, since], |query_row| {
Ok(RemovedItemRecord {
removal_id: query_row.get(0)?, table_name: query_row.get(1)?,
_removal_id: query_row.get(0)?, table_name: query_row.get(1)?,
entity_id: query_row.get(2)?, book_id: query_row.get(3)?,
user_id: query_row.get(4)?, deleted_at: query_row.get(5)?,
_user_id: query_row.get(4)?, deleted_at: query_row.get(5)?,
})
})
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les suppressions.".to_string() } else { "Unable to retrieve deletions.".to_string() }))?
@@ -63,63 +63,3 @@ pub fn get_deletions_since(conn: &Connection, user_id: &str, since: i64, lang: L
Ok(records)
}
/// Checks if an entity was previously deleted.
/// * `conn` - Database connection
/// * `table_name` - The table name
/// * `entity_id` - The entity ID
/// * `lang` - The language for error messages ("fr" or "en")
/// Returns true if the entity was deleted locally.
pub fn was_deleted(conn: &Connection, table_name: &str, entity_id: &str, lang: Lang) -> AppResult<bool> {
let mut statement = conn
.prepare("SELECT 1 FROM removed_items WHERE table_name = ?1 AND entity_id = ?2 LIMIT 1")
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de vérifier si l'élément a été supprimé.".to_string() } else { "Unable to check if item was deleted.".to_string() }))?;
let exists = statement
.query_row(params![table_name, entity_id], |_query_row| Ok(true))
.unwrap_or(false);
Ok(exists)
}
/// Retrieves all tracked deletions for a specific book.
/// * `conn` - Database connection
/// * `user_id` - The user ID
/// * `book_id` - The book ID
/// * `lang` - The language for error messages ("fr" or "en")
/// Returns array of removed item records for that book.
pub fn get_deletions_for_book(conn: &Connection, user_id: &str, book_id: &str, lang: Lang) -> AppResult<Vec<RemovedItemRecord>> {
let mut statement = conn
.prepare("SELECT removal_id, table_name, entity_id, book_id, user_id, deleted_at FROM removed_items WHERE user_id = ?1 AND book_id = ?2")
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les suppressions pour ce livre.".to_string() } else { "Unable to retrieve deletions for this book.".to_string() }))?;
let records = statement
.query_map(params![user_id, book_id], |query_row| {
Ok(RemovedItemRecord {
removal_id: query_row.get(0)?, table_name: query_row.get(1)?,
entity_id: query_row.get(2)?, book_id: query_row.get(3)?,
user_id: query_row.get(4)?, deleted_at: query_row.get(5)?,
})
})
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les suppressions pour ce livre.".to_string() } else { "Unable to retrieve deletions for this book.".to_string() }))?
.collect::<Result<Vec<_>, _>>()
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les suppressions pour ce livre.".to_string() } else { "Unable to retrieve deletions for this book.".to_string() }))?;
Ok(records)
}
/// Clears all deletion records for a user.
/// WARNING: Only use this when wiping user data completely.
/// * `conn` - Database connection
/// * `user_id` - The user ID
/// * `lang` - The language for error messages ("fr" or "en")
/// Returns true if cleared successfully.
pub fn clear_all_for_user(conn: &Connection, user_id: &str, lang: Lang) -> AppResult<bool> {
let delete_result = conn
.execute(
"DELETE FROM removed_items WHERE user_id = ?1",
params![user_id],
)
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de supprimer les enregistrements de suppression.".to_string() } else { "Unable to clear deletion records.".to_string() }))?;
Ok(delete_result > 0)
}

View File

@@ -1,25 +0,0 @@
use rusqlite::Connection;
use crate::domains::tombstone::repo;
use crate::error::AppResult;
use crate::helpers::create_unique_id;
use crate::shared::types::Lang;
/// Records a deleted item for sync tracking.
/// Must be called BEFORE the actual deletion from the source table.
/// * `conn` - Database connection
/// * `user_id` - The unique identifier of the user
/// * `book_id` - The book ID (None for series items)
/// * `table_name` - The name of the table from which the item is deleted
/// * `entity_id` - The UUID of the deleted entity
/// * `deleted_at` - The timestamp of deletion (from UI via timestamp_in_seconds())
/// * `lang` - The language for error messages ("fr" or "en")
/// Returns true if the record was inserted successfully.
pub fn delete_tracker(
conn: &Connection, user_id: &str, book_id: Option<&str>, table_name: &str,
entity_id: &str, deleted_at: i64, lang: Lang,
) -> AppResult<bool> {
let removal_id: String = create_unique_id(None);
repo::insert(conn, &removal_id, table_name, entity_id, book_id, user_id, deleted_at, lang)
}

View File

@@ -8,6 +8,14 @@ use crate::domains::user::service;
use crate::error::AppError;
use crate::shared::session::SessionState;
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct UpdateUserData {
pub username: String,
pub email: String,
pub author_name: Option<String>,
}
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct InitUserData {
@@ -25,8 +33,6 @@ pub struct InitUserResult {
#[serde(rename_all = "camelCase")]
pub struct SyncUserData {
pub user_id: String,
pub first_name: String,
pub last_name: String,
pub username: String,
pub email: String,
}
@@ -35,14 +41,10 @@ pub struct SyncUserData {
#[serde(rename_all = "camelCase")]
pub struct UserInfoResponse {
pub id: String,
pub name: String,
pub last_name: String,
pub username: String,
pub email: String,
pub account_verified: bool,
pub author_name: String,
pub group_id: i64,
pub terms_accepted: bool,
}
#[tauri::command]
@@ -62,6 +64,11 @@ pub fn init_user(data: InitUserData, db: State<DbManager>, session: State<Sessio
key_manager::set_last_user_id(&data.user_id)?;
if key_manager::get_user_encryption_key(&data.user_id).is_err() {
let encryption_key = encryption::generate_user_encryption_key(&data.user_id)?;
key_manager::set_user_encryption_key(&data.user_id, &encryption_key)?;
}
Ok(InitUserResult { success: true, error: None })
}
@@ -139,14 +146,10 @@ pub fn get_user_info(db: State<DbManager>, session: State<SessionState>) -> Resu
Ok(UserInfoResponse {
id: user_info.id,
name: user_info.name,
last_name: user_info.last_name,
username: user_info.username,
email: user_info.email,
account_verified: user_info.account_verified,
author_name: user_info.author_name,
group_id: user_info.group_id,
terms_accepted: user_info.terms_accepted,
})
}
@@ -160,6 +163,20 @@ pub fn sync_user(data: SyncUserData, db: State<DbManager>, session: State<Sessio
db_manager.initialize(&data.user_id)?;
let conn = db_manager.get_connection(&data.user_id)?;
service::add_user(conn, &data.user_id, &data.first_name, &data.last_name, &data.username, &data.email, "", lang)?;
service::add_user(conn, &data.user_id, &data.username, &data.email, lang)?;
Ok(true)
}
#[tauri::command]
pub fn update_user_info(data: UpdateUserData, db: State<DbManager>, session: State<SessionState>) -> Result<bool, 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;
drop(session_guard);
let user_key = key_manager::get_user_encryption_key(&user_id)?;
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_user_infos(conn, &user_key, &user_id, &data.username, &data.email, data.author_name.as_deref(), lang)
}

View File

@@ -4,45 +4,18 @@ use crate::error::{AppError, AppResult};
use crate::shared::types::Lang;
pub struct UserInfosQueryResponse {
pub first_name: String,
pub last_name: String,
pub username: String,
pub email: String,
pub plateform: String,
pub term_accepted: i64,
pub account_verified: i64,
pub author_name: Option<String>,
pub rite_points: i64,
pub user_group: i64,
}
pub struct UserAccountQuery {
pub first_name: Option<String>,
pub last_name: Option<String>,
pub username: String,
pub author_name: Option<String>,
pub email: String,
}
pub struct CredentialResponse {
pub valid: bool,
pub message: Option<String>,
pub user: Option<UserResponse>,
}
pub struct UserResponse {
pub id: String,
pub name: String,
pub last_name: String,
pub username: String,
pub email: String,
pub account_verified: bool,
}
pub struct GuideTourResult {
pub step_tour: String,
}
/// Inserts a new user into the database.
/// * `conn` - Database connection
/// * `user_id` - The unique identifier for the user
@@ -56,21 +29,18 @@ pub struct GuideTourResult {
/// Returns the user's UUID if insertion was successful.
/// Errors if the user cannot be registered.
pub fn insert_user(
conn: &Connection, user_id: &str, first_name: &str, last_name: &str, username: &str,
origin_username: &str, email: &str, origin_email: &str, lang: Lang,
conn: &Connection, user_id: &str, username: &str, origin_username: &str,
email: &str, origin_email: &str, lang: Lang,
) -> AppResult<String> {
let reg_date = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).map(|duration| duration.as_millis() as i64).unwrap_or(0);
// Try INSERT first; if user already exists, UPDATE their info instead
let insert_result = conn.execute(
"INSERT OR IGNORE INTO erit_users (user_id, first_name, last_name, username, email, origin_email, origin_username, plateform, term_accepted, account_verified, reg_date) VALUES (?1,?2,?3,?4,?5,?6,?7,?8,?9,?10,?11)",
params![user_id, first_name, last_name, username, email, origin_email, origin_username, "desktop", 0, 1, reg_date],
"INSERT OR IGNORE INTO erit_users (user_id, username, email, origin_email, origin_username) VALUES (?1,?2,?3,?4,?5)",
params![user_id, username, email, origin_email, origin_username],
).map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible d'enregistrer l'utilisateur.".to_string() } else { "Unable to register user.".to_string() }))?;
if insert_result == 0 {
conn.execute(
"UPDATE erit_users SET first_name = ?1, last_name = ?2, username = ?3, email = ?4, origin_email = ?5, origin_username = ?6 WHERE user_id = ?7",
params![first_name, last_name, username, email, origin_email, origin_username, user_id],
"UPDATE erit_users SET username = ?1, email = ?2, origin_email = ?3, origin_username = ?4 WHERE user_id = ?5",
params![username, email, origin_email, origin_username, user_id],
).map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de mettre à jour l'utilisateur.".to_string() } else { "Unable to update user.".to_string() }))?;
}
@@ -85,17 +55,14 @@ pub fn insert_user(
/// Errors if the user is not found or cannot be retrieved.
pub fn fetch_user_infos(conn: &Connection, user_id: &str, lang: Lang) -> AppResult<UserInfosQueryResponse> {
let mut statement = conn
.prepare("SELECT first_name, last_name, username, email, plateform, term_accepted, account_verified, author_name, erite_points AS rite_points, user_group FROM erit_users WHERE user_id = ?1")
.prepare("SELECT username, email, author_name, user_group FROM erit_users WHERE user_id = ?1")
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les informations utilisateur.".to_string() } else { "Unable to retrieve user information.".to_string() }))?;
let user_info = statement
.query_row(params![user_id], |query_row| {
Ok(UserInfosQueryResponse {
first_name: query_row.get(0)?, last_name: query_row.get(1)?,
username: query_row.get(2)?, email: query_row.get(3)?,
plateform: query_row.get(4)?, term_accepted: query_row.get(5)?,
account_verified: query_row.get(6)?, author_name: query_row.get(7)?,
rite_points: query_row.get(8)?, user_group: query_row.get(9)?,
username: query_row.get(0)?, email: query_row.get(1)?,
author_name: query_row.get(2)?, user_group: query_row.get(3)?,
})
})
.map_err(|error| match error {
@@ -121,13 +88,13 @@ pub fn fetch_user_infos(conn: &Connection, user_id: &str, lang: Lang) -> AppResu
/// Returns true if the update was successful, false otherwise.
/// Errors if the update fails.
pub fn update_user_infos(
conn: &Connection, user_id: &str, first_name: &str, last_name: &str, username: &str,
origin_username: &str, email: &str, origin_email: &str, original_author_name: &str, author_name: &str, lang: Lang,
conn: &Connection, user_id: &str, username: &str, origin_username: &str,
email: &str, origin_email: &str, original_author_name: &str, author_name: &str, lang: Lang,
) -> AppResult<bool> {
let update_result = conn
.execute(
"UPDATE erit_users SET first_name = ?1, last_name = ?2, username = ?3, email = ?4, origin_username = ?5, origin_author_name = ?6, author_name = ?7 WHERE user_id = ?8 AND origin_email = ?9",
params![first_name, last_name, username, email, origin_username, original_author_name, author_name, user_id, origin_email],
"UPDATE erit_users SET username = ?1, email = ?2, origin_username = ?3, origin_author_name = ?4, author_name = ?5 WHERE user_id = ?6 AND origin_email = ?7",
params![username, email, origin_username, original_author_name, author_name, user_id, origin_email],
)
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de mettre à jour les informations utilisateur.".to_string() } else { "Unable to update user information.".to_string() }))?;
@@ -142,12 +109,12 @@ pub fn update_user_infos(
/// Errors if the account is not found or cannot be retrieved.
pub fn fetch_account_information(conn: &Connection, user_id: &str, lang: Lang) -> AppResult<UserAccountQuery> {
let mut statement = conn
.prepare("SELECT first_name, last_name, username, author_name, email FROM erit_users WHERE user_id = ?1")
.prepare("SELECT username, author_name, email FROM erit_users WHERE user_id = ?1")
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les informations du compte.".to_string() } else { "Unable to retrieve account information.".to_string() }))?;
let account_info = statement
.query_row(params![user_id], |query_row| {
Ok(UserAccountQuery { first_name: query_row.get(0)?, last_name: query_row.get(1)?, username: query_row.get(2)?, author_name: query_row.get(3)?, email: query_row.get(4)? })
Ok(UserAccountQuery { username: query_row.get(0)?, author_name: query_row.get(1)?, email: query_row.get(2)? })
})
.map_err(|error| match error {
rusqlite::Error::QueryReturnedNoRows => AppError::NotFound(if lang == Lang::Fr { "Compte non trouvé.".to_string() } else { "Account not found.".to_string() }),

View File

@@ -1,156 +1,61 @@
use rusqlite::Connection;
use crate::crypto::encryption::{encrypt_data_with_user_key, decrypt_data_with_user_key, hash_element};
use crate::crypto::key_manager::get_user_encryption_key;
use crate::domains::user::repo;
use crate::error::AppResult;
use crate::shared::types::Lang;
pub struct UserAccount {
pub first_name: String,
pub last_name: String,
pub username: String,
pub author_name: String,
pub email: String,
}
pub struct GuideTour {
pub key: String,
pub value: bool,
}
pub struct BookSummary {
pub book_id: String,
pub title: String,
pub sub_title: Option<String>,
}
pub struct UserInfoResponse {
pub id: String,
pub name: String,
pub last_name: String,
pub username: String,
pub email: String,
pub account_verified: bool,
pub author_name: String,
pub group_id: i64,
pub terms_accepted: bool,
pub guide_tour: Vec<GuideTour>,
}
/// Retrieves complete user information including associated books.
/// * `conn` - Database connection
/// * `user_id` - The unique identifier of the user to fetch
/// * `lang` - The language for error messages
/// Returns the complete user information response.
/// Errors if the user is not found or decryption fails.
pub fn return_user_infos(conn: &Connection, user_id: &str, lang: Lang) -> AppResult<UserInfoResponse> {
let user_infos_data: repo::UserInfosQueryResponse = repo::fetch_user_infos(conn, user_id, lang)?;
let user_encryption_key: String = get_user_encryption_key(user_id)?;
let first_name: String = decrypt_data_with_user_key(&user_infos_data.first_name, &user_encryption_key)?;
let last_name: String = decrypt_data_with_user_key(&user_infos_data.last_name, &user_encryption_key)?;
let username: String = decrypt_data_with_user_key(&user_infos_data.username, &user_encryption_key)?;
let email: String = decrypt_data_with_user_key(&user_infos_data.email, &user_encryption_key)?;
let account_verified: bool = user_infos_data.account_verified == 1;
let author_name: String = if let Some(ref author_name_val) = user_infos_data.author_name { decrypt_data_with_user_key(author_name_val, &user_encryption_key)? } else { String::new() };
let group_id: i64 = user_infos_data.user_group;
let terms_accepted: bool = user_infos_data.term_accepted == 1;
let guide_tour_status: Vec<GuideTour> = vec![];
Ok(UserInfoResponse {
id: user_id.to_string(),
name: first_name,
last_name,
username,
email,
account_verified,
author_name,
group_id,
terms_accepted,
guide_tour: guide_tour_status,
})
}
/// Creates a new user in the database with encrypted personal information.
/// * `conn` - Database connection
/// * `user_id` - The unique identifier for the new user
/// * `first_name` - The user's first name (will be encrypted)
/// * `last_name` - The user's last name (will be encrypted)
/// * `username` - The user's username (will be encrypted and hashed)
/// * `email` - The user's email address (will be encrypted and hashed)
/// * `not_encrypt_password` - The user's password in plain text (unused in current implementation)
/// * `lang` - The preferred language for the user
/// Returns the created user's identifier.
/// Errors if encryption or insertion fails.
pub fn add_user(
conn: &Connection, user_id: &str, first_name: &str, last_name: &str, username: &str,
email: &str, _not_encrypt_password: &str, lang: Lang,
) -> AppResult<String> {
let user_encryption_key: String = get_user_encryption_key(user_id)?;
let encrypted_first_name: String = encrypt_data_with_user_key(first_name, &user_encryption_key)?;
let encrypted_last_name: String = encrypt_data_with_user_key(last_name, &user_encryption_key)?;
let encrypted_username: String = encrypt_data_with_user_key(username, &user_encryption_key)?;
let encrypted_email: String = encrypt_data_with_user_key(email, &user_encryption_key)?;
let hashed_email: String = hash_element(email);
let hashed_username: String = hash_element(username);
repo::insert_user(conn, user_id, &encrypted_first_name, &encrypted_last_name, &encrypted_username, &hashed_username, &encrypted_email, &hashed_email, lang)
}
/// Updates an existing user's profile information in the database.
/// * `conn` - Database connection
/// * `user_key` - The encryption key for the user's data
/// * `user_id` - The unique identifier of the user to update
/// * `first_name` - The updated first name (will be encrypted)
/// * `last_name` - The updated last name (will be encrypted)
/// * `username` - The updated username (will be encrypted and hashed)
/// * `email` - The updated email address (will be encrypted and hashed)
/// * `author_name` - The optional author/pen name (will be encrypted and hashed if provided)
/// * `lang` - The preferred language for the user
/// Returns true if the update was successful.
/// Errors if encryption or update fails.
pub fn update_user_infos(
conn: &Connection, user_key: &str, user_id: &str, first_name: &str, last_name: &str,
username: &str, email: &str, author_name: Option<&str>, lang: Lang,
) -> AppResult<bool> {
let encrypted_first_name: String = encrypt_data_with_user_key(first_name, user_key)?;
let encrypted_last_name: String = encrypt_data_with_user_key(last_name, user_key)?;
let encrypted_username: String = encrypt_data_with_user_key(username, user_key)?;
let encrypted_email: String = encrypt_data_with_user_key(email, user_key)?;
let hashed_email: String = hash_element(email);
let hashed_username: String = hash_element(username);
let mut encrypted_author_name: String = String::new();
let mut hashed_author_name: String = String::new();
if let Some(author_name_val) = author_name {
encrypted_author_name = encrypt_data_with_user_key(author_name_val, user_key)?;
hashed_author_name = hash_element(author_name_val);
}
repo::update_user_infos(conn, user_id, &encrypted_first_name, &encrypted_last_name, &encrypted_username, &hashed_username, &encrypted_email, &hashed_email, &hashed_author_name, &encrypted_author_name, lang)
}
/// Retrieves and decrypts the user's account information from the database.
/// * `conn` - Database connection
/// * `user_id` - The unique identifier of the user
/// * `lang` - The language for error messages
/// Returns the decrypted user account information.
/// Errors if the user is not found or decryption fails.
pub fn get_user_account_information(conn: &Connection, user_id: &str, lang: Lang) -> AppResult<UserAccount> {
let account_data: repo::UserAccountQuery = repo::fetch_account_information(conn, user_id, lang)?;
let user_encryption_key: String = get_user_encryption_key(user_id)?;
let decrypted_first_name: String = if let Some(ref first_name) = account_data.first_name { decrypt_data_with_user_key(first_name, &user_encryption_key)? } else { String::new() };
let decrypted_last_name: String = if let Some(ref last_name) = account_data.last_name { decrypt_data_with_user_key(last_name, &user_encryption_key)? } else { String::new() };
let decrypted_username: String = decrypt_data_with_user_key(&account_data.username, &user_encryption_key)?;
let decrypted_author_name: String = if let Some(ref author_name) = account_data.author_name { decrypt_data_with_user_key(author_name, &user_encryption_key)? } else { String::new() };
let decrypted_email: String = decrypt_data_with_user_key(&account_data.email, &user_encryption_key)?;
Ok(UserAccount {
first_name: decrypted_first_name,
last_name: decrypted_last_name,
username: decrypted_username,
author_name: decrypted_author_name,
email: decrypted_email,
})
}
use rusqlite::Connection;
use crate::crypto::encryption::{encrypt_data_with_user_key, decrypt_data_with_user_key, hash_element};
use crate::crypto::key_manager::get_user_encryption_key;
use crate::domains::user::repo;
use crate::error::AppResult;
use crate::shared::types::Lang;
pub struct UserInfoResponse {
pub id: String,
pub username: String,
pub email: String,
pub author_name: String,
pub group_id: i64,
}
/// Retrieves complete user information including associated books.
pub fn return_user_infos(conn: &Connection, user_id: &str, lang: Lang) -> AppResult<UserInfoResponse> {
let user_infos_data: repo::UserInfosQueryResponse = repo::fetch_user_infos(conn, user_id, lang)?;
let user_encryption_key: String = get_user_encryption_key(user_id)?;
let username: String = decrypt_data_with_user_key(&user_infos_data.username, &user_encryption_key)?;
let email: String = decrypt_data_with_user_key(&user_infos_data.email, &user_encryption_key)?;
let author_name: String = if let Some(ref author_name_val) = user_infos_data.author_name { decrypt_data_with_user_key(author_name_val, &user_encryption_key)? } else { String::new() };
Ok(UserInfoResponse {
id: user_id.to_string(),
username, email, author_name,
group_id: user_infos_data.user_group,
})
}
/// Creates a new user in the database with encrypted personal information.
pub fn add_user(conn: &Connection, user_id: &str, username: &str, email: &str, lang: Lang) -> AppResult<String> {
let user_encryption_key: String = get_user_encryption_key(user_id)?;
let encrypted_username: String = encrypt_data_with_user_key(username, &user_encryption_key)?;
let encrypted_email: String = encrypt_data_with_user_key(email, &user_encryption_key)?;
let hashed_email: String = hash_element(email);
let hashed_username: String = hash_element(username);
repo::insert_user(conn, user_id, &encrypted_username, &hashed_username, &encrypted_email, &hashed_email, lang)
}
/// Updates an existing user's profile information in the database.
pub fn update_user_infos(
conn: &Connection, user_key: &str, user_id: &str,
username: &str, email: &str, author_name: Option<&str>, lang: Lang,
) -> AppResult<bool> {
let encrypted_username: String = encrypt_data_with_user_key(username, user_key)?;
let encrypted_email: String = encrypt_data_with_user_key(email, user_key)?;
let hashed_email: String = hash_element(email);
let hashed_username: String = hash_element(username);
let mut encrypted_author_name: String = String::new();
let mut hashed_author_name: String = String::new();
if let Some(author_name_val) = author_name {
encrypted_author_name = encrypt_data_with_user_key(author_name_val, user_key)?;
hashed_author_name = hash_element(author_name_val);
}
repo::update_user_infos(conn, user_id, &encrypted_username, &hashed_username, &encrypted_email, &hashed_email, &hashed_author_name, &encrypted_author_name, lang)
}

View File

@@ -10,23 +10,6 @@ use crate::error::{AppError, AppResult};
use crate::helpers::{create_unique_id, timestamp_in_seconds};
use crate::shared::types::Lang;
/// Represents a synced world with its elements for synchronization.
#[derive(Debug, Serialize, Deserialize)]
pub struct SyncedWorld {
pub id: String,
pub name: String,
pub last_update: i64,
pub elements: Vec<SyncedWorldElement>,
}
/// Represents a synced world element for synchronization.
#[derive(Debug, Serialize, Deserialize)]
pub struct SyncedWorldElement {
pub id: String,
pub name: String,
pub last_update: i64,
}
/// Represents a single world element with its properties.
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct WorldElement {

View File

@@ -1,4 +1,3 @@
use chrono::Utc;
use regex::Regex;
@@ -20,20 +19,6 @@ pub fn create_unique_id(existing_id: Option<&str>) -> String {
}
}
/// Returns the current date as an ISO 8601 string.
/// Equivalent to TS `System.getCurrentDate()`.
pub fn get_current_date() -> String {
Utc::now().to_rfc3339()
}
/// Converts an ISO date string to a MySQL-compatible date (YYYY-MM-DD).
/// Equivalent to TS `System.dateToMySqlDate()`.
pub fn date_to_mysql_date(iso_date_string: &str) -> String {
match chrono::DateTime::parse_from_rfc3339(iso_date_string) {
Ok(date_object) => date_object.format("%Y-%m-%d").to_string(),
Err(_) => iso_date_string.to_string(),
}
}
/// Converts HTML content to plain text by stripping tags and decoding entities.
/// Equivalent to TS `System.htmlToText()`.

View File

@@ -32,6 +32,7 @@ pub fn run() {
domains::user::commands::get_platform,
domains::user::commands::get_user_info,
domains::user::commands::sync_user,
domains::user::commands::update_user_info,
// ─── Offline ───────────────────────────────────
domains::offline::commands::offline_pin_set,
domains::offline::commands::offline_pin_verify,

View File

@@ -1,3 +1,2 @@
pub mod ai_models;
pub mod session;
pub mod types;