diff --git a/src-tauri/src/db/schema.rs b/src-tauri/src/db/schema.rs index d9e3e65..543ad9f 100644 --- a/src-tauri/src/db/schema.rs +++ b/src-tauri/src/db/schema.rs @@ -1370,213 +1370,11 @@ pub fn run_migrations(conn: &Connection) -> Result<(), rusqlite::Error> { // DEV QUERIES // ============================================================================= -/// DEV ONLY - Runs dev queries silently (errors are ignored). -/// In the TypeScript version these run on every refresh in dev mode. -/// Call this before `run_migrations` when in dev mode. +/// DEV ONLY - Ensures schema is up-to-date in dev mode. +/// Delegates to run_migrations which is already idempotent +/// (uses IF NOT EXISTS and column_exists checks). pub fn run_dev_queries(conn: &Connection) { - let dev_queries: &[&str] = &[ - // V3 Migration: Series tables and series_*_id columns - - // Book Series - "CREATE TABLE IF NOT EXISTS book_series ( - series_id TEXT PRIMARY KEY, - user_id TEXT NOT NULL, - name TEXT NOT NULL, - hashed_name TEXT NOT NULL, - description TEXT, - cover_image TEXT, - last_update INTEGER DEFAULT 0, - FOREIGN KEY (user_id) REFERENCES erit_users(user_id) ON DELETE CASCADE - )", - "CREATE INDEX IF NOT EXISTS idx_book_series_user ON book_series(user_id)", - - // Series Books - "CREATE TABLE IF NOT EXISTS series_books ( - series_id TEXT NOT NULL, - book_id TEXT NOT NULL, - book_order INTEGER NOT NULL DEFAULT 1, - last_update INTEGER DEFAULT 0, - PRIMARY KEY (series_id, book_id), - FOREIGN KEY (series_id) REFERENCES book_series(series_id) ON DELETE CASCADE, - FOREIGN KEY (book_id) REFERENCES erit_books(book_id) ON DELETE CASCADE - )", - "CREATE INDEX IF NOT EXISTS idx_series_books_book ON series_books(book_id)", - - // Series Characters - "CREATE TABLE IF NOT EXISTS series_characters ( - character_id TEXT PRIMARY KEY, - series_id TEXT NOT NULL, - user_id TEXT NOT NULL, - first_name TEXT NOT NULL, - last_name TEXT, - nickname TEXT, - age TEXT, - gender TEXT, - species TEXT, - nationality TEXT, - status TEXT, - category TEXT NOT NULL, - title TEXT, - image TEXT, - role TEXT, - biography TEXT, - history TEXT, - speech_pattern TEXT, - catchphrase TEXT, - residence TEXT, - notes TEXT, - color TEXT, - last_update INTEGER DEFAULT 0, - FOREIGN KEY (series_id) REFERENCES book_series(series_id) ON DELETE CASCADE - )", - "CREATE INDEX IF NOT EXISTS idx_series_characters_series ON series_characters(series_id)", - "CREATE INDEX IF NOT EXISTS idx_series_characters_user ON series_characters(user_id)", - - // Series Characters Attributes - "CREATE TABLE IF NOT EXISTS series_characters_attributes ( - attr_id TEXT PRIMARY KEY, - character_id TEXT NOT NULL, - user_id TEXT NOT NULL, - attribute_name TEXT NOT NULL, - attribute_value TEXT NOT NULL, - last_update INTEGER DEFAULT 0, - FOREIGN KEY (character_id) REFERENCES series_characters(character_id) ON DELETE CASCADE - )", - "CREATE INDEX IF NOT EXISTS idx_series_char_attrs_character ON series_characters_attributes(character_id)", - "CREATE INDEX IF NOT EXISTS idx_series_char_attrs_user ON series_characters_attributes(user_id)", - - // Series Worlds - "CREATE TABLE IF NOT EXISTS series_worlds ( - world_id TEXT PRIMARY KEY, - series_id TEXT NOT NULL, - user_id TEXT NOT NULL, - name TEXT NOT NULL, - hashed_name TEXT NOT NULL, - history TEXT, - politics TEXT, - economy TEXT, - religion TEXT, - languages TEXT, - last_update INTEGER DEFAULT 0, - FOREIGN KEY (series_id) REFERENCES book_series(series_id) ON DELETE CASCADE - )", - "CREATE INDEX IF NOT EXISTS idx_series_worlds_series ON series_worlds(series_id)", - "CREATE INDEX IF NOT EXISTS idx_series_worlds_user ON series_worlds(user_id)", - - // Series World Elements - "CREATE TABLE IF NOT EXISTS series_world_elements ( - element_id TEXT PRIMARY KEY, - world_id TEXT NOT NULL, - user_id TEXT NOT NULL, - element_type INTEGER NOT NULL, - name TEXT NOT NULL, - original_name TEXT NOT NULL, - description TEXT, - last_update INTEGER DEFAULT 0, - FOREIGN KEY (world_id) REFERENCES series_worlds(world_id) ON DELETE CASCADE - )", - "CREATE INDEX IF NOT EXISTS idx_series_world_elements_world ON series_world_elements(world_id)", - "CREATE INDEX IF NOT EXISTS idx_series_world_elements_user ON series_world_elements(user_id)", - - // Series Locations - "CREATE TABLE IF NOT EXISTS series_locations ( - loc_id TEXT PRIMARY KEY, - series_id TEXT NOT NULL, - user_id TEXT NOT NULL, - loc_name TEXT NOT NULL, - loc_original_name TEXT NOT NULL, - last_update INTEGER DEFAULT 0, - FOREIGN KEY (series_id) REFERENCES book_series(series_id) ON DELETE CASCADE - )", - "CREATE INDEX IF NOT EXISTS idx_series_locations_series ON series_locations(series_id)", - "CREATE INDEX IF NOT EXISTS idx_series_locations_user ON series_locations(user_id)", - - // Series Location Elements - "CREATE TABLE IF NOT EXISTS series_location_elements ( - element_id TEXT PRIMARY KEY, - location_id TEXT NOT NULL, - user_id TEXT NOT NULL, - element_name TEXT NOT NULL, - original_name TEXT NOT NULL, - element_description TEXT, - last_update INTEGER DEFAULT 0, - FOREIGN KEY (location_id) REFERENCES series_locations(loc_id) ON DELETE CASCADE - )", - "CREATE INDEX IF NOT EXISTS idx_series_loc_elements_location ON series_location_elements(location_id)", - "CREATE INDEX IF NOT EXISTS idx_series_loc_elements_user ON series_location_elements(user_id)", - - // Series Location Sub Elements - "CREATE TABLE IF NOT EXISTS series_location_sub_elements ( - sub_element_id TEXT PRIMARY KEY, - element_id TEXT NOT NULL, - user_id TEXT NOT NULL, - sub_elem_name TEXT NOT NULL, - original_name TEXT NOT NULL, - sub_elem_description TEXT, - last_update INTEGER DEFAULT 0, - FOREIGN KEY (element_id) REFERENCES series_location_elements(element_id) ON DELETE CASCADE - )", - "CREATE INDEX IF NOT EXISTS idx_series_loc_sub_elements_element ON series_location_sub_elements(element_id)", - "CREATE INDEX IF NOT EXISTS idx_series_loc_sub_elements_user ON series_location_sub_elements(user_id)", - - // Series Spells - "CREATE TABLE IF NOT EXISTS series_spells ( - spell_id TEXT PRIMARY KEY, - series_id TEXT NOT NULL, - user_id TEXT NOT NULL, - name TEXT NOT NULL, - name_hash TEXT NOT NULL, - description TEXT, - appearance TEXT, - tags TEXT, - power_level TEXT, - components TEXT, - limitations TEXT, - notes TEXT, - last_update INTEGER DEFAULT 0, - FOREIGN KEY (series_id) REFERENCES book_series(series_id) ON DELETE CASCADE - )", - "CREATE INDEX IF NOT EXISTS idx_series_spells_series ON series_spells(series_id)", - "CREATE INDEX IF NOT EXISTS idx_series_spells_user ON series_spells(user_id)", - - // Series Spell Tags - "CREATE TABLE IF NOT EXISTS series_spell_tags ( - tag_id TEXT PRIMARY KEY, - series_id TEXT NOT NULL, - user_id TEXT NOT NULL, - name TEXT NOT NULL, - hashed_name TEXT NOT NULL, - color TEXT, - last_update INTEGER DEFAULT 0, - FOREIGN KEY (series_id) REFERENCES book_series(series_id) ON DELETE CASCADE - )", - "CREATE INDEX IF NOT EXISTS idx_series_spell_tags_series ON series_spell_tags(series_id)", - "CREATE INDEX IF NOT EXISTS idx_series_spell_tags_user ON series_spell_tags(user_id)", - - // Add series_*_id columns to existing book tables (will fail silently if already exists) - "ALTER TABLE book_characters ADD COLUMN series_character_id TEXT DEFAULT NULL", - "ALTER TABLE book_world ADD COLUMN series_world_id TEXT DEFAULT NULL", - "ALTER TABLE book_location ADD COLUMN series_location_id TEXT DEFAULT NULL", - "ALTER TABLE book_spells ADD COLUMN series_spell_id TEXT DEFAULT NULL", - - // Removed Items (sync deletion tracking) - "CREATE TABLE IF NOT EXISTS removed_items ( - removal_id TEXT PRIMARY KEY, - table_name TEXT NOT NULL, - entity_id TEXT NOT NULL, - book_id TEXT, - user_id TEXT NOT NULL, - deleted_at INTEGER NOT NULL - )", - "CREATE INDEX IF NOT EXISTS idx_removed_items_user ON removed_items(user_id)", - "CREATE INDEX IF NOT EXISTS idx_removed_items_book ON removed_items(book_id)", - "CREATE INDEX IF NOT EXISTS idx_removed_items_deleted_at ON removed_items(deleted_at)", - "CREATE UNIQUE INDEX IF NOT EXISTS idx_removed_items_entity ON removed_items(table_name, entity_id)", - ]; - - for query in dev_queries { - let _ = conn.execute_batch(query); - } + let _ = run_migrations(conn); } diff --git a/src-tauri/src/domains/chapter/repo.rs b/src-tauri/src/domains/chapter/repo.rs index 377c6ee..7383817 100644 --- a/src-tauri/src/domains/chapter/repo.rs +++ b/src-tauri/src/domains/chapter/repo.rs @@ -427,6 +427,26 @@ pub fn fetch_book_chapter_infos(conn: &Connection, user_id: &str, chapter_id: &s Ok(rows) } +pub fn fetch_all_chapter_infos_by_book(conn: &Connection, user_id: &str, book_id: &str, lang: Lang) -> AppResult> { + let mut statement = conn + .prepare("SELECT chapter_info_id, chapter_id, act_id, incident_id, plot_point_id, book_id, author_id, summary, goal, last_update FROM book_chapter_infos WHERE author_id=?1 AND book_id=?2") + .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les infos des chapitres.".to_string() } else { "Unable to retrieve chapter infos.".to_string() }))?; + let rows = statement + .query_map(params![user_id, book_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)?, + author_id: query_row.get(6)?, summary: query_row.get(7)?, + goal: query_row.get(8)?, last_update: query_row.get(9)?, + }) + }) + .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les infos des chapitres.".to_string() } else { "Unable to retrieve chapter infos.".to_string() }))? + .collect::, _>>() + .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les infos des chapitres.".to_string() } else { "Unable to retrieve chapter infos.".to_string() }))?; + Ok(rows) +} + /// Retrieves synced chapters for a user. pub fn fetch_synced_chapters(conn: &Connection, user_id: &str, lang: Lang) -> AppResult> { let mut statement = conn diff --git a/src-tauri/src/domains/chapter_content/repo.rs b/src-tauri/src/domains/chapter_content/repo.rs index 41f7179..e141cbc 100644 --- a/src-tauri/src/domains/chapter_content/repo.rs +++ b/src-tauri/src/domains/chapter_content/repo.rs @@ -199,6 +199,25 @@ pub fn fetch_book_chapter_contents(conn: &Connection, user_id: &str, chapter_id: Ok(rows) } +pub fn fetch_all_chapter_contents_by_book(conn: &Connection, user_id: &str, book_id: &str, lang: Lang) -> AppResult> { + let mut statement = conn + .prepare("SELECT bcc.content_id, bcc.chapter_id, bcc.author_id, bcc.version, bcc.content, bcc.words_count, bcc.time_on_it, bcc.last_update FROM book_chapter_content bcc INNER JOIN book_chapters bc ON bcc.chapter_id = bc.chapter_id WHERE bcc.author_id=?1 AND bc.book_id=?2") + .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() }))?; + let rows = statement + .query_map(params![user_id, book_id], |query_row| { + 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)?, + }) + }) + .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() }))? + .collect::, _>>() + .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() }))?; + Ok(rows) +} + /// Fetches all synced chapter contents for a user (content ID, chapter ID, and last update timestamp). /// * `conn` - Database connection /// * `user_id` - The ID of the user/author diff --git a/src-tauri/src/domains/character/repo.rs b/src-tauri/src/domains/character/repo.rs index ebc20ce..39e6a1c 100644 --- a/src-tauri/src/domains/character/repo.rs +++ b/src-tauri/src/domains/character/repo.rs @@ -442,6 +442,24 @@ pub fn fetch_book_characters_attributes(conn: &Connection, user_id: &str, charac Ok(attributes) } +pub fn fetch_all_character_attributes_by_book(conn: &Connection, user_id: &str, book_id: &str, lang: Lang) -> AppResult> { + let mut statement = conn + .prepare("SELECT bca.attr_id, bca.character_id, bca.user_id, bca.attribute_name, bca.attribute_value, bca.last_update FROM book_characters_attributes bca INNER JOIN book_characters bc ON bca.character_id = bc.character_id WHERE bca.user_id=?1 AND bc.book_id=?2") + .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les attributs des personnages.".to_string() } else { "Unable to retrieve character attributes.".to_string() }))?; + let attributes = statement + .query_map(params![user_id, book_id], |query_row| { + Ok(BookCharactersAttributesTable { + attr_id: query_row.get(0)?, character_id: query_row.get(1)?, + user_id: query_row.get(2)?, attribute_name: query_row.get(3)?, + attribute_value: query_row.get(4)?, last_update: query_row.get(5)?, + }) + }) + .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les attributs des personnages.".to_string() } else { "Unable to retrieve character attributes.".to_string() }))? + .collect::, _>>() + .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les attributs des personnages.".to_string() } else { "Unable to retrieve character attributes.".to_string() }))?; + Ok(attributes) +} + /// Fetches all synced characters for a user. /// * `conn` - Database connection /// * `user_id` - The unique identifier of the user diff --git a/src-tauri/src/domains/location/repo.rs b/src-tauri/src/domains/location/repo.rs index 076eec5..8d80d3f 100644 --- a/src-tauri/src/domains/location/repo.rs +++ b/src-tauri/src/domains/location/repo.rs @@ -452,6 +452,48 @@ pub fn fetch_location_elements(conn: &Connection, user_id: &str, location_id: &s Ok(location_elements) } +pub fn fetch_all_location_elements_by_book(conn: &Connection, user_id: &str, book_id: &str, lang: Lang) -> AppResult> { + let mut statement = conn + .prepare("SELECT e.element_id, e.location, e.user_id, e.element_name, e.original_name, e.element_description, e.last_update FROM location_element e INNER JOIN book_location l ON e.location = l.loc_id AND e.user_id = l.user_id WHERE e.user_id = ?1 AND l.book_id = ?2") + .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les éléments de lieu.".to_string() } else { "Unable to retrieve location elements.".to_string() }))?; + + let location_elements = statement + .query_map(params![user_id, book_id], |query_row| { + Ok(LocationElementTable { + element_id: query_row.get(0)?, location: query_row.get(1)?, + user_id: query_row.get(2)?, element_name: query_row.get(3)?, + original_name: query_row.get(4)?, element_description: query_row.get(5)?, + last_update: query_row.get(6)?, + }) + }) + .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les éléments de lieu.".to_string() } else { "Unable to retrieve location elements.".to_string() }))? + .collect::, _>>() + .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les éléments de lieu.".to_string() } else { "Unable to retrieve location elements.".to_string() }))?; + + Ok(location_elements) +} + +pub fn fetch_all_location_sub_elements_by_book(conn: &Connection, user_id: &str, book_id: &str, lang: Lang) -> AppResult> { + let mut statement = conn + .prepare("SELECT se.sub_element_id, se.element_id, se.user_id, se.sub_elem_name, se.original_name, se.sub_elem_description, se.last_update FROM location_sub_element se INNER JOIN location_element e ON se.element_id = e.element_id AND se.user_id = e.user_id INNER JOIN book_location l ON e.location = l.loc_id AND e.user_id = l.user_id WHERE se.user_id = ?1 AND l.book_id = ?2") + .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les sous-éléments de lieu.".to_string() } else { "Unable to retrieve location sub-elements.".to_string() }))?; + + let location_sub_elements = statement + .query_map(params![user_id, book_id], |query_row| { + Ok(LocationSubElementTable { + sub_element_id: query_row.get(0)?, element_id: query_row.get(1)?, + user_id: query_row.get(2)?, sub_elem_name: query_row.get(3)?, + original_name: query_row.get(4)?, sub_elem_description: query_row.get(5)?, + last_update: query_row.get(6)?, + }) + }) + .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les sous-éléments de lieu.".to_string() } else { "Unable to retrieve location sub-elements.".to_string() }))? + .collect::, _>>() + .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les sous-éléments de lieu.".to_string() } else { "Unable to retrieve location sub-elements.".to_string() }))?; + + Ok(location_sub_elements) +} + /// Fetches all sub-elements for a specific location element. /// * `conn` - Database connection /// * `user_id` - The user's unique identifier diff --git a/src-tauri/src/domains/upload/service.rs b/src-tauri/src/domains/upload/service.rs index b2ec7f7..ed58035 100644 --- a/src-tauri/src/domains/upload/service.rs +++ b/src-tauri/src/domains/upload/service.rs @@ -51,39 +51,12 @@ pub fn upload_book_for_sync(conn: &Connection, user_id: &str, book_id: &str, lan let encrypted_spells: Vec = spell_repo::fetch_book_spells_table(conn, user_id, book_id, lang)?; let encrypted_spell_tags: Vec = spell_tag_repo::fetch_book_spell_tags_table(conn, user_id, book_id, lang)?; - let mut nested_chapter_contents: Vec> = Vec::with_capacity(encrypted_chapters.len()); - let mut nested_chapter_infos: Vec> = Vec::with_capacity(encrypted_chapters.len()); - for chapter in &encrypted_chapters { - nested_chapter_contents.push(chapter_content_repo::fetch_book_chapter_contents(conn, user_id, &chapter.chapter_id, lang)?); - nested_chapter_infos.push(chapter_repo::fetch_book_chapter_infos(conn, user_id, &chapter.chapter_id, lang)?); - } - - let mut nested_character_attributes: Vec> = Vec::with_capacity(encrypted_characters.len()); - for character in &encrypted_characters { - nested_character_attributes.push(character_repo::fetch_book_characters_attributes(conn, user_id, &character.character_id, lang)?); - } - - let mut nested_world_elements: Vec> = Vec::with_capacity(encrypted_worlds.len()); - for world in &encrypted_worlds { - nested_world_elements.push(world_repo::fetch_book_world_elements(conn, user_id, &world.world_id, lang)?); - } - - let mut nested_location_elements: Vec> = Vec::with_capacity(encrypted_locations.len()); - for location in &encrypted_locations { - nested_location_elements.push(location_repo::fetch_location_elements(conn, user_id, &location.loc_id, lang)?); - } - - let encrypted_chapter_contents: Vec = nested_chapter_contents.into_iter().flatten().collect(); - let encrypted_chapter_infos: Vec = nested_chapter_infos.into_iter().flatten().collect(); - let encrypted_character_attributes: Vec = nested_character_attributes.into_iter().flatten().collect(); - let encrypted_world_elements: Vec = nested_world_elements.into_iter().flatten().collect(); - let encrypted_location_elements: Vec = nested_location_elements.into_iter().flatten().collect(); - - let mut nested_location_sub_elements: Vec> = Vec::with_capacity(encrypted_location_elements.len()); - for element in &encrypted_location_elements { - nested_location_sub_elements.push(location_repo::fetch_location_sub_elements(conn, user_id, &element.element_id, lang)?); - } - let encrypted_location_sub_elements: Vec = nested_location_sub_elements.into_iter().flatten().collect(); + let encrypted_chapter_contents: Vec = chapter_content_repo::fetch_all_chapter_contents_by_book(conn, user_id, book_id, lang)?; + let encrypted_chapter_infos: Vec = chapter_repo::fetch_all_chapter_infos_by_book(conn, user_id, book_id, lang)?; + let encrypted_character_attributes: Vec = character_repo::fetch_all_character_attributes_by_book(conn, user_id, book_id, lang)?; + let encrypted_world_elements: Vec = world_repo::fetch_all_world_elements_by_book(conn, user_id, book_id, lang)?; + let encrypted_location_elements: Vec = location_repo::fetch_all_location_elements_by_book(conn, user_id, book_id, lang)?; + let encrypted_location_sub_elements: Vec = location_repo::fetch_all_location_sub_elements_by_book(conn, user_id, book_id, lang)?; let mut erit_books: Vec = Vec::with_capacity(encrypted_books.len()); for book in encrypted_books { diff --git a/src-tauri/src/domains/world/repo.rs b/src-tauri/src/domains/world/repo.rs index 5eda9a4..9072595 100644 --- a/src-tauri/src/domains/world/repo.rs +++ b/src-tauri/src/domains/world/repo.rs @@ -308,6 +308,27 @@ pub fn fetch_book_world_elements(conn: &Connection, user_id: &str, world_id: &st Ok(elements) } +pub fn fetch_all_world_elements_by_book(conn: &Connection, user_id: &str, book_id: &str, lang: Lang) -> AppResult> { + let mut statement = conn + .prepare("SELECT e.element_id, e.world_id, e.user_id, e.element_type, e.name, e.original_name, e.description, e.last_update FROM book_world_elements e INNER JOIN book_world w ON e.world_id = w.world_id AND e.user_id = w.author_id WHERE e.user_id=?1 AND w.book_id=?2") + .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les éléments du monde.".to_string() } else { "Unable to retrieve world elements.".to_string() }))?; + + let elements = statement + .query_map(params![user_id, book_id], |query_row| { + Ok(BookWorldElementsTable { + 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 du monde.".to_string() } else { "Unable to retrieve world elements.".to_string() }))? + .collect::, _>>() + .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les éléments du monde.".to_string() } else { "Unable to retrieve world elements.".to_string() }))?; + + Ok(elements) +} + /// Fetches all synced worlds for a specific user. /// * `conn` - Database connection /// * `user_id` - The unique identifier of the user diff --git a/src-tauri/src/helpers.rs b/src-tauri/src/helpers.rs index 1a5f2df..9b7a4ce 100644 --- a/src-tauri/src/helpers.rs +++ b/src-tauri/src/helpers.rs @@ -1,44 +1,45 @@ -use regex::Regex; - - -/// Returns the current UNIX timestamp in seconds. -/// Equivalent to TS `System.timeStampInSeconds()`. -pub fn timestamp_in_seconds() -> i64 { - std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .map(|duration| duration.as_secs() as i64) - .unwrap_or(0) -} - -/// Creates a new UUID v4 string, or reuses an existing ID if provided. -/// Equivalent to TS `System.createUniqueId()`. -pub fn create_unique_id(existing_id: Option<&str>) -> String { - match existing_id { - Some(id) => id.to_string(), - None => uuid::Uuid::new_v4().to_string(), - } -} - - -/// Converts HTML content to plain text by stripping tags and decoding entities. -/// Equivalent to TS `System.htmlToText()`. -pub fn html_to_text(html_node: &str) -> String { - let mut text: String = html_node.to_string(); - let p_regex: Regex = Regex::new(r"(?i)]*>").unwrap(); - let br_regex: Regex = Regex::new(r"(?i)").unwrap(); - let span_heading_regex: Regex = Regex::new(r"(?i)]*>").unwrap(); - text = p_regex.replace_all(&text, "\n").to_string(); - text = br_regex.replace_all(&text, "\n").to_string(); - text = span_heading_regex.replace_all(&text, "").to_string(); - text = text.replace("'", "'"); - text = text.replace(""", "\""); - text = text.replace("&", "&"); - text = text.replace("<", "<"); - text = text.replace(">", ">"); - text = text.replace("'", "'"); - let double_newline_regex: Regex = Regex::new(r"\r?\n\s*\n").unwrap(); - text = double_newline_regex.replace_all(&text, "\n").to_string(); - let spaces_regex: Regex = Regex::new(r"[ \t]+").unwrap(); - text = spaces_regex.replace_all(&text, " ").to_string(); - text.trim().to_string() -} +use std::sync::LazyLock; +use regex::Regex; + +static P_REGEX: LazyLock = LazyLock::new(|| Regex::new(r"(?i)]*>").unwrap()); +static BR_REGEX: LazyLock = LazyLock::new(|| Regex::new(r"(?i)").unwrap()); +static SPAN_HEADING_REGEX: LazyLock = LazyLock::new(|| Regex::new(r"(?i)]*>").unwrap()); +static DOUBLE_NEWLINE_REGEX: LazyLock = LazyLock::new(|| Regex::new(r"\r?\n\s*\n").unwrap()); +static SPACES_REGEX: LazyLock = LazyLock::new(|| Regex::new(r"[ \t]+").unwrap()); + + +/// Returns the current UNIX timestamp in seconds. +/// Equivalent to TS `System.timeStampInSeconds()`. +pub fn timestamp_in_seconds() -> i64 { + std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .map(|duration| duration.as_secs() as i64) + .unwrap_or(0) +} + +/// Creates a new UUID v4 string, or reuses an existing ID if provided. +/// Equivalent to TS `System.createUniqueId()`. +pub fn create_unique_id(existing_id: Option<&str>) -> String { + match existing_id { + Some(id) => id.to_string(), + None => uuid::Uuid::new_v4().to_string(), + } +} + + +/// Converts HTML content to plain text by stripping tags and decoding entities. +/// Equivalent to TS `System.htmlToText()`. +pub fn html_to_text(html_node: &str) -> String { + let mut text: String = P_REGEX.replace_all(html_node, "\n").to_string(); + text = BR_REGEX.replace_all(&text, "\n").to_string(); + text = SPAN_HEADING_REGEX.replace_all(&text, "").to_string(); + text = text.replace("'", "'"); + text = text.replace(""", "\""); + text = text.replace("&", "&"); + text = text.replace("<", "<"); + text = text.replace(">", ">"); + text = text.replace("'", "'"); + text = DOUBLE_NEWLINE_REGEX.replace_all(&text, "\n").to_string(); + text = SPACES_REGEX.replace_all(&text, " ").to_string(); + text.trim().to_string() +}