Refactor schema migrations, optimize queries, and improve data fetching logic
- Simplified `run_dev_queries` by delegating schema updates to `run_migrations` for consistency. - Added universal `fetch_all_*_by_book` methods for streamlined data retrieval across repositories. - Replaced nested query logic with batch fetching to improve performance and maintainability. - Optimized HTML-to-text conversion by consolidating regex patterns with `LazyLock`.
This commit is contained in:
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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<Vec<BookChapterInfosTable>> {
|
||||
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::<Result<Vec<_>, _>>()
|
||||
.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<Vec<SyncedChapterResult>> {
|
||||
let mut statement = conn
|
||||
|
||||
@@ -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<Vec<BookChapterContentTable>> {
|
||||
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::<Result<Vec<_>, _>>()
|
||||
.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
|
||||
|
||||
@@ -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<Vec<BookCharactersAttributesTable>> {
|
||||
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::<Result<Vec<_>, _>>()
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les attributs des personnages.".to_string() } else { "Unable to retrieve character attributes.".to_string() }))?;
|
||||
Ok(attributes)
|
||||
}
|
||||
|
||||
/// Fetches all synced characters for a user.
|
||||
/// * `conn` - Database connection
|
||||
/// * `user_id` - The unique identifier of the user
|
||||
|
||||
@@ -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<Vec<LocationElementTable>> {
|
||||
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::<Result<Vec<_>, _>>()
|
||||
.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<Vec<LocationSubElementTable>> {
|
||||
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::<Result<Vec<_>, _>>()
|
||||
.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
|
||||
|
||||
@@ -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::BookSpellsTable> = spell_repo::fetch_book_spells_table(conn, user_id, book_id, lang)?;
|
||||
let encrypted_spell_tags: Vec<spell_tag_repo::BookSpellTagsTable> = spell_tag_repo::fetch_book_spell_tags_table(conn, user_id, book_id, lang)?;
|
||||
|
||||
let mut nested_chapter_contents: Vec<Vec<chapter_content_repo::BookChapterContentTable>> = Vec::with_capacity(encrypted_chapters.len());
|
||||
let mut nested_chapter_infos: Vec<Vec<chapter_repo::BookChapterInfosTable>> = 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<character_repo::BookCharactersAttributesTable>> = 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<world_repo::BookWorldElementsTable>> = 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<location_repo::LocationElementTable>> = 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<chapter_content_repo::BookChapterContentTable> = nested_chapter_contents.into_iter().flatten().collect();
|
||||
let encrypted_chapter_infos: Vec<chapter_repo::BookChapterInfosTable> = nested_chapter_infos.into_iter().flatten().collect();
|
||||
let encrypted_character_attributes: Vec<character_repo::BookCharactersAttributesTable> = nested_character_attributes.into_iter().flatten().collect();
|
||||
let encrypted_world_elements: Vec<world_repo::BookWorldElementsTable> = nested_world_elements.into_iter().flatten().collect();
|
||||
let encrypted_location_elements: Vec<location_repo::LocationElementTable> = nested_location_elements.into_iter().flatten().collect();
|
||||
|
||||
let mut nested_location_sub_elements: Vec<Vec<location_repo::LocationSubElementTable>> = 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<location_repo::LocationSubElementTable> = nested_location_sub_elements.into_iter().flatten().collect();
|
||||
let encrypted_chapter_contents: Vec<chapter_content_repo::BookChapterContentTable> = chapter_content_repo::fetch_all_chapter_contents_by_book(conn, user_id, book_id, lang)?;
|
||||
let encrypted_chapter_infos: Vec<chapter_repo::BookChapterInfosTable> = chapter_repo::fetch_all_chapter_infos_by_book(conn, user_id, book_id, lang)?;
|
||||
let encrypted_character_attributes: Vec<character_repo::BookCharactersAttributesTable> = character_repo::fetch_all_character_attributes_by_book(conn, user_id, book_id, lang)?;
|
||||
let encrypted_world_elements: Vec<world_repo::BookWorldElementsTable> = world_repo::fetch_all_world_elements_by_book(conn, user_id, book_id, lang)?;
|
||||
let encrypted_location_elements: Vec<location_repo::LocationElementTable> = location_repo::fetch_all_location_elements_by_book(conn, user_id, book_id, lang)?;
|
||||
let encrypted_location_sub_elements: Vec<location_repo::LocationSubElementTable> = location_repo::fetch_all_location_sub_elements_by_book(conn, user_id, book_id, lang)?;
|
||||
|
||||
let mut erit_books: Vec<book_repo::EritBooksTable> = Vec::with_capacity(encrypted_books.len());
|
||||
for book in encrypted_books {
|
||||
|
||||
@@ -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<Vec<BookWorldElementsTable>> {
|
||||
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::<Result<Vec<_>, _>>()
|
||||
.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
|
||||
|
||||
@@ -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)</?p[^>]*>").unwrap();
|
||||
let br_regex: Regex = Regex::new(r"(?i)<br\s*/?>").unwrap();
|
||||
let span_heading_regex: Regex = Regex::new(r"(?i)</?(span|h[1-6])[^>]*>").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<Regex> = LazyLock::new(|| Regex::new(r"(?i)</?p[^>]*>").unwrap());
|
||||
static BR_REGEX: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"(?i)<br\s*/?>").unwrap());
|
||||
static SPAN_HEADING_REGEX: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"(?i)</?(span|h[1-6])[^>]*>").unwrap());
|
||||
static DOUBLE_NEWLINE_REGEX: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"\r?\n\s*\n").unwrap());
|
||||
static SPACES_REGEX: LazyLock<Regex> = 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()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user