From 2db105bd6f47cd1614b4deb377e03025afd19c02 Mon Sep 17 00:00:00 2001 From: Simon Bernier St-Pierre Date: Sat, 1 Aug 2026 15:56:21 -0400 Subject: [PATCH] use autoincrement for IDs --- Cargo.lock | 99 +---------------------------- Cargo.toml | 1 - src/db.rs | 174 ++++++++++++++++++++------------------------------- src/hub.rs | 41 ++++++------ src/main.rs | 118 +++++++++++++++++----------------- src/views.rs | 8 +-- 6 files changed, 154 insertions(+), 287 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index ed92449..0113575 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -132,12 +132,6 @@ dependencies = [ "generic-array", ] -[[package]] -name = "bumpalo" -version = "3.20.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" - [[package]] name = "bytes" version = "1.12.1" @@ -314,21 +308,10 @@ checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" dependencies = [ "cfg-if", "libc", - "r-efi 5.3.0", + "r-efi", "wasip2", ] -[[package]] -name = "getrandom" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" -dependencies = [ - "cfg-if", - "libc", - "r-efi 6.0.0", -] - [[package]] name = "hashbrown" version = "0.14.5" @@ -445,17 +428,6 @@ version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" -[[package]] -name = "js-sys" -version = "0.3.103" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53b44bfcdb3f8d5837a46dae1ca9660a837176eee74a28b229bc626816589102" -dependencies = [ - "cfg-if", - "futures-util", - "wasm-bindgen", -] - [[package]] name = "lazy_static" version = "1.5.0" @@ -676,12 +648,6 @@ version = "5.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" -[[package]] -name = "r-efi" -version = "6.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" - [[package]] name = "rand" version = "0.8.7" @@ -781,12 +747,6 @@ dependencies = [ "smallvec", ] -[[package]] -name = "rustversion" -version = "1.0.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" - [[package]] name = "ryu" version = "1.0.23" @@ -958,7 +918,6 @@ dependencies = [ "tower-http", "tracing", "tracing-subscriber", - "uuid", ] [[package]] @@ -1221,17 +1180,6 @@ version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" -[[package]] -name = "uuid" -version = "1.24.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf3923a6f5c4c6382e0b653c4117f48d631ea17f38ed86e2a828e6f7412f5239" -dependencies = [ - "getrandom 0.4.3", - "js-sys", - "wasm-bindgen", -] - [[package]] name = "valuable" version = "0.1.1" @@ -1265,51 +1213,6 @@ dependencies = [ "wit-bindgen", ] -[[package]] -name = "wasm-bindgen" -version = "0.2.126" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b067c0c11094aef6b7a801c1e34a26affafdf3d051dba08456b868789aaf9a4" -dependencies = [ - "cfg-if", - "once_cell", - "rustversion", - "wasm-bindgen-macro", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-macro" -version = "0.2.126" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "167ce5e579f6bcf889c4f7175a8a5a585de84e8ff93976ce393efa5f2837aab1" -dependencies = [ - "quote", - "wasm-bindgen-macro-support", -] - -[[package]] -name = "wasm-bindgen-macro-support" -version = "0.2.126" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3997c7839262f4ef12cf90b818d6340c18e80f263f1a94bf157d0ec4420380e" -dependencies = [ - "bumpalo", - "proc-macro2", - "quote", - "syn 2.0.119", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-shared" -version = "0.2.126" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc1b4cb0cc549fcf58d7dfc081778139b3d283a081644e833e84682ad71cea24" -dependencies = [ - "unicode-ident", -] - [[package]] name = "windows-link" version = "0.2.1" diff --git a/Cargo.toml b/Cargo.toml index b559911..9763e04 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,4 +18,3 @@ tokio = { version = "1", features = ["full"] } tower-http = { version = "0.6", features = ["fs", "trace"] } tracing = "0.1" tracing-subscriber = { version = "0.3", features = ["env-filter"] } -uuid = { version = "1", features = ["v4"] } diff --git a/src/db.rs b/src/db.rs index 9403574..6b4425d 100644 --- a/src/db.rs +++ b/src/db.rs @@ -8,7 +8,6 @@ use rand::{RngCore, rngs::OsRng}; use rusqlite::{Connection, OptionalExtension, params}; use sha2::{Digest, Sha256}; use thiserror::Error; -use uuid::Uuid; #[derive(Debug, Error)] pub enum DbError { @@ -31,7 +30,7 @@ pub struct Database { #[derive(Clone, Debug)] pub struct User { - pub id: String, + pub id: i64, pub email: String, pub display_name: String, } @@ -44,26 +43,26 @@ pub struct SessionUser { #[derive(Clone, Debug)] pub struct GroceryList { - pub id: String, + pub id: i64, pub name: String, pub revision: i64, } #[derive(Clone, Debug)] pub struct Item { - pub id: String, - pub list_id: String, + pub id: i64, + pub list_id: i64, pub name: String, pub quantity: String, pub note: String, - pub category_id: Option, + pub category_id: Option, pub checked: bool, pub version: i64, } #[derive(Clone, Debug)] pub struct Category { - pub id: String, + pub id: i64, pub name: String, } @@ -106,20 +105,21 @@ impl Database { password_hash: String, ) -> DbResult { self.call(move |connection| { - let user = User { - id: Uuid::new_v4().to_string(), - email, - display_name, - }; - let result = connection.execute( - "INSERT INTO users (id, email, display_name, password_hash, created_at) - VALUES (?1, ?2, ?3, ?4, ?5)", - params![user.id, user.email, user.display_name, password_hash, now()], + "INSERT INTO users (email, display_name, password_hash, created_at) + VALUES (?1, ?2, ?3, ?4)", + params![email, display_name, password_hash, now()], ); match result { - Ok(_) => Ok(user), + Ok(_) => { + let id = connection.last_insert_rowid(); + Ok(User { + id, + email, + display_name, + }) + } Err(error) if error.to_string().contains("UNIQUE") => Err(DbError::Conflict), Err(error) => Err(sql_error(error)), } @@ -162,7 +162,7 @@ impl Database { .await } - pub async fn create_session(&self, user_id: String) -> DbResult<(String, String)> { + pub async fn create_session(&self, user_id: i64) -> DbResult<(String, String)> { self.call(move |connection| { let session_token = new_secret(); let csrf_token = new_secret(); @@ -248,41 +248,35 @@ impl Database { pub async fn create_list(&self, name: String) -> DbResult { self.call(move |connection| { - let list = GroceryList { - id: Uuid::new_v4().to_string(), - name, - revision: 0, - }; let transaction = connection.transaction().map_err(sql_error)?; transaction .execute( - "INSERT INTO lists (id, name, revision, created_at) - VALUES (?1, ?2, 0, ?3)", - params![list.id, list.name, now()], + "INSERT INTO lists (name, revision, created_at) + VALUES (?1, 0, ?2)", + params![name, now()], ) .map_err(sql_error)?; + let list_id = transaction.last_insert_rowid(); for (position, category_name) in DEFAULT_CATEGORIES.iter().enumerate() { transaction .execute( - "INSERT INTO categories (id, list_id, name, position, created_at) - VALUES (?1, ?2, ?3, ?4, ?5)", - params![ - Uuid::new_v4().to_string(), - list.id, - category_name, - position as i64, - now() - ], + "INSERT INTO categories (list_id, name, position, created_at) + VALUES (?1, ?2, ?3, ?4)", + params![list_id, category_name, position as i64, now()], ) .map_err(sql_error)?; } transaction.commit().map_err(sql_error)?; - Ok(list) + Ok(GroceryList { + id: list_id, + name, + revision: 0, + }) }) .await } - pub async fn list_access(&self, list_id: String) -> DbResult> { + pub async fn list_access(&self, list_id: i64) -> DbResult> { self.call(move |connection| { connection .query_row( @@ -304,7 +298,7 @@ impl Database { .await } - pub async fn items(&self, list_id: String) -> DbResult> { + pub async fn items(&self, list_id: i64) -> DbResult> { self.call(move |connection| { let mut statement = connection .prepare( @@ -333,7 +327,7 @@ impl Database { .await } - pub async fn categories(&self, list_id: String) -> DbResult> { + pub async fn categories(&self, list_id: i64) -> DbResult> { self.call(move |connection| { let mut statement = connection .prepare( @@ -356,7 +350,7 @@ impl Database { .await } - pub async fn create_category(&self, list_id: String, name: String) -> DbResult { + pub async fn create_category(&self, list_id: i64, name: String) -> DbResult { self.call(move |connection| { let transaction = connection.transaction().map_err(sql_error)?; let position: i64 = transaction @@ -368,9 +362,9 @@ impl Database { ) .map_err(sql_error)?; let result = transaction.execute( - "INSERT INTO categories (id, list_id, name, position, created_at) - VALUES (?1, ?2, ?3, ?4, ?5)", - params![Uuid::new_v4().to_string(), list_id, name, position, now()], + "INSERT INTO categories (list_id, name, position, created_at) + VALUES (?1, ?2, ?3, ?4)", + params![list_id, name, position, now()], ); match result { Ok(_) => {} @@ -379,7 +373,7 @@ impl Database { } Err(error) => return Err(sql_error(error)), } - let revision = bump_revision(&transaction, &list_id)?; + let revision = bump_revision(&transaction, list_id)?; transaction.commit().map_err(sql_error)?; Ok(revision) }) @@ -388,16 +382,15 @@ impl Database { pub async fn add_item( &self, - list_id: String, + list_id: i64, name: String, quantity: String, note: String, - category_id: Option, + category_id: Option, ) -> DbResult { self.call(move |connection| { let transaction = connection.transaction().map_err(sql_error)?; - let category_id = category_id.filter(|category_id| !category_id.is_empty()); - ensure_category(&transaction, &list_id, category_id.as_deref())?; + ensure_category(&transaction, list_id, category_id)?; let position: i64 = transaction .query_row( "SELECT COALESCE(MAX(position), -1) + 1 FROM items WHERE list_id = ?1", @@ -408,21 +401,12 @@ impl Database { transaction .execute( "INSERT INTO items - (id, list_id, name, quantity, note, category_id, checked, version, position, created_at, updated_at) - VALUES (?1, ?2, ?3, ?4, ?5, ?6, 0, 1, ?7, ?8, ?8)", - params![ - Uuid::new_v4().to_string(), - list_id, - name, - quantity, - note, - category_id, - position, - now() - ], + (list_id, name, quantity, note, category_id, checked, version, position, created_at, updated_at) + VALUES (?1, ?2, ?3, ?4, ?5, 0, 1, ?6, ?7, ?7)", + params![list_id, name, quantity, note, category_id, position, now()], ) .map_err(sql_error)?; - let revision = bump_revision(&transaction, &list_id)?; + let revision = bump_revision(&transaction, list_id)?; transaction.commit().map_err(sql_error)?; Ok(revision) }) @@ -431,8 +415,8 @@ impl Database { pub async fn set_item_checked( &self, - list_id: String, - item_id: String, + list_id: i64, + item_id: i64, checked: bool, ) -> DbResult { self.call(move |connection| { @@ -448,7 +432,7 @@ impl Database { if changed == 0 { return Err(DbError::NotFound); } - let revision = bump_revision(&transaction, &list_id)?; + let revision = bump_revision(&transaction, list_id)?; transaction.commit().map_err(sql_error)?; Ok(revision) }) @@ -457,17 +441,16 @@ impl Database { pub async fn update_item( &self, - list_id: String, - item_id: String, + list_id: i64, + item_id: i64, name: String, quantity: String, note: String, - category_id: Option, + category_id: Option, ) -> DbResult { self.call(move |connection| { let transaction = connection.transaction().map_err(sql_error)?; - let category_id = category_id.filter(|category_id| !category_id.is_empty()); - ensure_category(&transaction, &list_id, category_id.as_deref())?; + ensure_category(&transaction, list_id, category_id)?; let changed = transaction .execute( "UPDATE items @@ -480,14 +463,14 @@ impl Database { if changed == 0 { return Err(DbError::NotFound); } - let revision = bump_revision(&transaction, &list_id)?; + let revision = bump_revision(&transaction, list_id)?; transaction.commit().map_err(sql_error)?; Ok(revision) }) .await } - pub async fn delete_item(&self, list_id: String, item_id: String) -> DbResult { + pub async fn delete_item(&self, list_id: i64, item_id: i64) -> DbResult { self.call(move |connection| { let transaction = connection.transaction().map_err(sql_error)?; let changed = transaction @@ -499,14 +482,14 @@ impl Database { if changed == 0 { return Err(DbError::NotFound); } - let revision = bump_revision(&transaction, &list_id)?; + let revision = bump_revision(&transaction, list_id)?; transaction.commit().map_err(sql_error)?; Ok(revision) }) .await } - pub async fn create_invitation(&self, created_by: String, token: String) -> DbResult { + pub async fn create_invitation(&self, created_by: i64, token: String) -> DbResult { self.call(move |connection| { let expires_at = now() + 60 * 60 * 24 * 7; connection @@ -584,7 +567,7 @@ fn migrate(connection: &Connection) -> DbResult<()> { connection .execute_batch( "CREATE TABLE IF NOT EXISTS users ( - id TEXT PRIMARY KEY, + id INTEGER PRIMARY KEY AUTOINCREMENT, email TEXT NOT NULL UNIQUE COLLATE NOCASE, display_name TEXT NOT NULL, password_hash TEXT NOT NULL, @@ -592,19 +575,19 @@ fn migrate(connection: &Connection) -> DbResult<()> { ); CREATE TABLE IF NOT EXISTS sessions ( token_hash TEXT PRIMARY KEY, - user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE, + user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, csrf_token TEXT NOT NULL, expires_at INTEGER NOT NULL ); CREATE TABLE IF NOT EXISTS lists ( - id TEXT PRIMARY KEY, + id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, revision INTEGER NOT NULL DEFAULT 0, created_at INTEGER NOT NULL ); CREATE TABLE IF NOT EXISTS categories ( - id TEXT PRIMARY KEY, - list_id TEXT NOT NULL REFERENCES lists(id) ON DELETE CASCADE, + id INTEGER PRIMARY KEY AUTOINCREMENT, + list_id INTEGER NOT NULL REFERENCES lists(id) ON DELETE CASCADE, name TEXT NOT NULL COLLATE NOCASE, position INTEGER NOT NULL DEFAULT 0, created_at INTEGER NOT NULL, @@ -612,16 +595,16 @@ fn migrate(connection: &Connection) -> DbResult<()> { ); CREATE TABLE IF NOT EXISTS invitations ( token_hash TEXT PRIMARY KEY, - created_by TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE, + created_by INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, expires_at INTEGER NOT NULL ); CREATE TABLE IF NOT EXISTS items ( - id TEXT PRIMARY KEY, - list_id TEXT NOT NULL REFERENCES lists(id) ON DELETE CASCADE, + id INTEGER PRIMARY KEY AUTOINCREMENT, + list_id INTEGER NOT NULL REFERENCES lists(id) ON DELETE CASCADE, name TEXT NOT NULL, quantity TEXT NOT NULL DEFAULT '', note TEXT NOT NULL DEFAULT '', - category_id TEXT REFERENCES categories(id) ON DELETE SET NULL, + category_id INTEGER REFERENCES categories(id) ON DELETE SET NULL, checked INTEGER NOT NULL DEFAULT 0, version INTEGER NOT NULL DEFAULT 1, position INTEGER NOT NULL DEFAULT 0, @@ -634,34 +617,13 @@ fn migrate(connection: &Connection) -> DbResult<()> { ) .map_err(sql_error)?; - if !has_column(connection, "items", "category_id")? { - connection - .execute( - "ALTER TABLE items ADD COLUMN category_id TEXT REFERENCES categories(id) ON DELETE SET NULL", - [], - ) - .map_err(sql_error)?; - } Ok(()) } -fn has_column(connection: &Connection, table: &str, wanted: &str) -> DbResult { - let mut statement = connection - .prepare(&format!("PRAGMA table_info({table})")) - .map_err(sql_error)?; - let mut rows = statement.query([]).map_err(sql_error)?; - while let Some(row) = rows.next().map_err(sql_error)? { - if row.get::<_, String>(1).map_err(sql_error)? == wanted { - return Ok(true); - } - } - Ok(false) -} - fn ensure_category( transaction: &rusqlite::Transaction<'_>, - list_id: &str, - category_id: Option<&str>, + list_id: i64, + category_id: Option, ) -> DbResult<()> { let Some(category_id) = category_id else { return Ok(()); @@ -689,7 +651,7 @@ const DEFAULT_CATEGORIES: &[&str] = &[ "Household", ]; -fn bump_revision(transaction: &rusqlite::Transaction<'_>, list_id: &str) -> DbResult { +fn bump_revision(transaction: &rusqlite::Transaction<'_>, list_id: i64) -> DbResult { transaction .execute( "UPDATE lists SET revision = revision + 1 WHERE id = ?1", diff --git a/src/hub.rs b/src/hub.rs index ef0cc11..2874d5b 100644 --- a/src/hub.rs +++ b/src/hub.rs @@ -5,19 +5,19 @@ use tokio::sync::{Mutex, broadcast}; #[derive(Clone, Debug)] pub struct PresenceUser { - pub user_id: String, + pub user_id: i64, pub display_name: String, } #[derive(Clone, Debug)] pub enum HubEvent { - ListChanged { list_id: String, revision: i64 }, - PresenceChanged { list_id: String }, + ListChanged { list_id: i64, revision: i64 }, + PresenceChanged { list_id: i64 }, } #[derive(Debug)] struct ConnectionInfo { - user_id: String, + user_id: i64, display_name: String, } @@ -34,18 +34,18 @@ pub struct Subscription { #[derive(Clone, Default)] pub struct Hub { - rooms: Arc>>, + rooms: Arc>>, } impl Hub { pub async fn join( &self, - list_id: String, - user_id: String, + list_id: i64, + user_id: i64, display_name: String, ) -> Subscription { let mut rooms = self.rooms.lock().await; - let room = rooms.entry(list_id.clone()).or_insert_with(|| { + let room = rooms.entry(list_id).or_insert_with(|| { let (sender, _) = broadcast::channel(64); Room { sender, @@ -79,10 +79,10 @@ impl Hub { } } - pub async fn leave(&self, list_id: &str, connection_id: &str) { + pub async fn leave(&self, list_id: i64, connection_id: &str) { let mut rooms = self.rooms.lock().await; let mut remove_room = false; - if let Some(room) = rooms.get_mut(list_id) { + if let Some(room) = rooms.get_mut(&list_id) { let removed = room.connections.remove(connection_id); if let Some(removed) = removed { let still_present = room @@ -90,19 +90,17 @@ impl Hub { .values() .any(|connection| connection.user_id == removed.user_id); if !still_present { - let _ = room.sender.send(HubEvent::PresenceChanged { - list_id: list_id.to_owned(), - }); + let _ = room.sender.send(HubEvent::PresenceChanged { list_id }); } } remove_room = room.connections.is_empty(); } if remove_room { - rooms.remove(list_id); + rooms.remove(&list_id); } } - pub async fn publish_list_changed(&self, list_id: String, revision: i64) { + pub async fn publish_list_changed(&self, list_id: i64, revision: i64) { let rooms = self.rooms.lock().await; if let Some(room) = rooms.get(&list_id) { let _ = room @@ -111,19 +109,22 @@ impl Hub { } } - pub async fn presence(&self, list_id: &str) -> Vec { + pub async fn presence(&self, list_id: i64) -> Vec { let rooms = self.rooms.lock().await; - rooms.get(list_id).map(current_presence).unwrap_or_default() + rooms + .get(&list_id) + .map(current_presence) + .unwrap_or_default() } } fn current_presence(room: &Room) -> Vec { - let mut users = HashMap::::new(); + let mut users = HashMap::::new(); for connection in room.connections.values() { users - .entry(connection.user_id.clone()) + .entry(connection.user_id) .or_insert_with(|| PresenceUser { - user_id: connection.user_id.clone(), + user_id: connection.user_id, display_name: connection.display_name.clone(), }); } diff --git a/src/main.rs b/src/main.rs index 276999f..2982ebb 100644 --- a/src/main.rs +++ b/src/main.rs @@ -463,12 +463,12 @@ async fn create_list( async fn list_page( State(state): State, user: CurrentUser, - Path(list_id): Path, + Path(list_id): Path, ) -> Result { - let access = require_access(&state, &list_id).await?; - let items = state.db.items(list_id.clone()).await?; - let categories = state.db.categories(list_id.clone()).await?; - let presence = state.hub.presence(&list_id).await; + let access = require_access(&state, list_id).await?; + let items = state.db.items(list_id).await?; + let categories = state.db.categories(list_id).await?; + let presence = state.hub.presence(list_id).await; Ok(html_response(views::list_page( &user.session.user, &access, @@ -482,15 +482,15 @@ async fn list_page( async fn add_item( State(state): State, user: CurrentUser, - Path(list_id): Path, + Path(list_id): Path, LoggedForm(form): LoggedForm, ) -> Result { verify_csrf(&user, &form.csrf)?; - require_access(&state, &list_id).await?; + require_access(&state, list_id).await?; let name = form.name.trim().to_owned(); let quantity = form.quantity.trim().to_owned(); let note = form.note.trim().to_owned(); - let category_id = normalize_category_id(form.category_id); + let category_id = parse_category_id(form.category_id); if name.is_empty() || name.chars().count() > 120 { return Err(AppError::BadRequest( "Item names must be between 1 and 120 characters.".into(), @@ -498,23 +498,23 @@ async fn add_item( } let revision = state .db - .add_item(list_id.clone(), name, quantity, note, category_id) + .add_item(list_id, name, quantity, note, category_id) .await?; state .hub - .publish_list_changed(list_id.clone(), revision) + .publish_list_changed(list_id, revision) .await; - list_fragment_response(&state, &user, &list_id).await + list_fragment_response(&state, &user, list_id).await } async fn check_item( State(state): State, user: CurrentUser, - Path((list_id, item_id)): Path<(String, String)>, + Path((list_id, item_id)): Path<(i64, i64)>, LoggedForm(form): LoggedForm, ) -> Result { verify_csrf(&user, &form.csrf)?; - require_access(&state, &list_id).await?; + require_access(&state, list_id).await?; let checked = match form.checked.as_str() { "1" | "true" => true, "0" | "false" => false, @@ -522,23 +522,23 @@ async fn check_item( }; let revision = state .db - .set_item_checked(list_id.clone(), item_id, checked) + .set_item_checked(list_id, item_id, checked) .await?; state .hub - .publish_list_changed(list_id.clone(), revision) + .publish_list_changed(list_id, revision) .await; - list_fragment_response(&state, &user, &list_id).await + list_fragment_response(&state, &user, list_id).await } async fn edit_item( State(state): State, user: CurrentUser, - Path((list_id, item_id)): Path<(String, String)>, + Path((list_id, item_id)): Path<(i64, i64)>, LoggedForm(form): LoggedForm, ) -> Result { verify_csrf(&user, &form.csrf)?; - require_access(&state, &list_id).await?; + require_access(&state, list_id).await?; let name = form.name.trim().to_owned(); if name.is_empty() || name.chars().count() > 120 { return Err(AppError::BadRequest( @@ -548,59 +548,59 @@ async fn edit_item( let revision = state .db .update_item( - list_id.clone(), + list_id, item_id, name, form.quantity.trim().to_owned(), form.note.trim().to_owned(), - normalize_category_id(form.category_id), + parse_category_id(form.category_id), ) .await?; state .hub - .publish_list_changed(list_id.clone(), revision) + .publish_list_changed(list_id, revision) .await; - list_fragment_response(&state, &user, &list_id).await + list_fragment_response(&state, &user, list_id).await } async fn delete_item( State(state): State, user: CurrentUser, - Path((list_id, item_id)): Path<(String, String)>, + Path((list_id, item_id)): Path<(i64, i64)>, LoggedForm(form): LoggedForm, ) -> Result { verify_csrf(&user, &form.csrf)?; - require_access(&state, &list_id).await?; - let revision = state.db.delete_item(list_id.clone(), item_id).await?; + require_access(&state, list_id).await?; + let revision = state.db.delete_item(list_id, item_id).await?; state .hub - .publish_list_changed(list_id.clone(), revision) + .publish_list_changed(list_id, revision) .await; - list_fragment_response(&state, &user, &list_id).await + list_fragment_response(&state, &user, list_id).await } async fn create_category( State(state): State, user: CurrentUser, - Path(list_id): Path, + Path(list_id): Path, LoggedForm(form): LoggedForm, ) -> Result { verify_csrf(&user, &form.csrf)?; - require_access(&state, &list_id).await?; + require_access(&state, list_id).await?; let name = form.name.trim().to_owned(); if name.is_empty() || name.chars().count() > 60 { return Err(AppError::BadRequest( "Category names must be between 1 and 60 characters.".into(), )); } - let revision = state.db.create_category(list_id.clone(), name).await?; + let revision = state.db.create_category(list_id, name).await?; state .hub - .publish_list_changed(list_id.clone(), revision) + .publish_list_changed(list_id, revision) .await; - let access = require_access(&state, &list_id).await?; - let items = state.db.items(list_id.clone()).await?; + let access = require_access(&state, list_id).await?; + let items = state.db.items(list_id).await?; let categories = state.db.categories(list_id).await?; Ok(html_response(views::category_created( &access, @@ -667,10 +667,10 @@ async fn accept_invitation( async fn list_stream( State(state): State, user: CurrentUser, - Path(list_id): Path, + Path(list_id): Path, websocket: WebSocketUpgrade, ) -> Result { - require_access(&state, &list_id).await?; + require_access(&state, list_id).await?; let state_for_socket = state.clone(); let user_for_socket = user.clone(); Ok(websocket @@ -678,12 +678,12 @@ async fn list_stream( .into_response()) } -async fn handle_socket(state: AppState, user: CurrentUser, list_id: String, socket: WebSocket) { +async fn handle_socket(state: AppState, user: CurrentUser, list_id: i64, socket: WebSocket) { let subscription = state .hub .join( - list_id.clone(), - user.session.user.id.clone(), + list_id, + user.session.user.id, user.session.user.display_name.clone(), ) .await; @@ -692,16 +692,16 @@ async fn handle_socket(state: AppState, user: CurrentUser, list_id: String, sock let mut heartbeat = tokio::time::interval(Duration::from_secs(30)); heartbeat.tick().await; - match websocket_snapshot(&state, &user, &list_id, &subscription.presence).await { + match websocket_snapshot(&state, &user, list_id, &subscription.presence).await { Ok(snapshot) => { if sender.send(Message::Text(snapshot.into())).await.is_err() { - state.hub.leave(&list_id, &connection_id).await; + state.hub.leave(list_id, &connection_id).await; return; } } Err(error) => { error!(%error, "could not render websocket snapshot"); - state.hub.leave(&list_id, &connection_id).await; + state.hub.leave(list_id, &connection_id).await; return; } } @@ -713,7 +713,7 @@ async fn handle_socket(state: AppState, user: CurrentUser, list_id: String, sock match event { Ok(HubEvent::ListChanged { list_id: event_list_id, revision }) if event_list_id == list_id => { tracing::debug!(%list_id, revision, "list changed on websocket"); - match websocket_list_update(&state, &user, &list_id).await { + match websocket_list_update(&state, &user, list_id).await { Ok(update) => { if sender.send(Message::Text(update.into())).await.is_err() { break; @@ -726,14 +726,14 @@ async fn handle_socket(state: AppState, user: CurrentUser, list_id: String, sock } } Ok(HubEvent::PresenceChanged { list_id: event_list_id }) if event_list_id == list_id => { - let presence = state.hub.presence(&list_id).await; + let presence = state.hub.presence(list_id).await; let update = views::presence_panel(&presence, true).into_string(); if sender.send(Message::Text(update.into())).await.is_err() { break; } } Err(tokio::sync::broadcast::error::RecvError::Lagged(_)) => { - match websocket_snapshot(&state, &user, &list_id, &state.hub.presence(&list_id).await).await { + match websocket_snapshot(&state, &user, list_id, &state.hub.presence(list_id).await).await { Ok(snapshot) => { if sender.send(Message::Text(snapshot.into())).await.is_err() { break; @@ -769,18 +769,18 @@ async fn handle_socket(state: AppState, user: CurrentUser, list_id: String, sock } } - state.hub.leave(&list_id, &connection_id).await; + state.hub.leave(list_id, &connection_id).await; } async fn websocket_snapshot( state: &AppState, user: &CurrentUser, - list_id: &str, + list_id: i64, presence: &[hub::PresenceUser], ) -> Result { let access = require_access(state, list_id).await?; - let items = state.db.items(list_id.to_owned()).await?; - let categories = state.db.categories(list_id.to_owned()).await?; + let items = state.db.items(list_id).await?; + let categories = state.db.categories(list_id).await?; Ok( views::live_list_fragments(&access, &items, &categories, &user.session.csrf_token) .into_string() @@ -791,11 +791,11 @@ async fn websocket_snapshot( async fn websocket_list_update( state: &AppState, user: &CurrentUser, - list_id: &str, + list_id: i64, ) -> Result { let access = require_access(state, list_id).await?; - let items = state.db.items(list_id.to_owned()).await?; - let categories = state.db.categories(list_id.to_owned()).await?; + let items = state.db.items(list_id).await?; + let categories = state.db.categories(list_id).await?; Ok( views::live_list_fragments(&access, &items, &categories, &user.session.csrf_token) .into_string(), @@ -805,11 +805,11 @@ async fn websocket_list_update( async fn list_fragment_response( state: &AppState, user: &CurrentUser, - list_id: &str, + list_id: i64, ) -> Result { let access = require_access(state, list_id).await?; - let items = state.db.items(list_id.to_owned()).await?; - let categories = state.db.categories(list_id.to_owned()).await?; + let items = state.db.items(list_id).await?; + let categories = state.db.categories(list_id).await?; Ok(html_response(views::list_items_fragment( &access, &items, @@ -821,11 +821,11 @@ async fn list_fragment_response( async fn require_access( state: &AppState, - list_id: &str, + list_id: i64, ) -> Result { state .db - .list_access(list_id.to_owned()) + .list_access(list_id) .await? .ok_or(AppError::NotFound) } @@ -856,8 +856,10 @@ fn verify_csrf(user: &CurrentUser, token: &str) -> Result<(), AppError> { Ok(()) } -fn normalize_category_id(category_id: Option) -> Option { - category_id.filter(|category_id| !category_id.trim().is_empty()) +fn parse_category_id(category_id: Option) -> Option { + category_id + .filter(|category_id| !category_id.trim().is_empty()) + .and_then(|category_id| category_id.trim().parse().ok()) } async fn can_register(state: &AppState, invite: Option<&str>) -> Result { diff --git a/src/views.rs b/src/views.rs index 09baa55..0496c8d 100644 --- a/src/views.rs +++ b/src/views.rs @@ -282,7 +282,7 @@ fn item_groups<'a>(items: &'a [Item], categories: &[Category]) -> Vec<(String, V for category in categories { let items_in_category = items .iter() - .filter(|item| item.category_id.as_deref() == Some(category.id.as_str())) + .filter(|item| item.category_id == Some(category.id)) .collect::>(); if !items_in_category.is_empty() { groups.push((category.name.clone(), items_in_category)); @@ -359,7 +359,7 @@ fn item_row(item: &Item, categories: &[Category], csrf_token: &str) -> Markup { input name="note" value=(item.note) maxlength="120"; label { "Category" } select name="category_id" { - (category_options(categories, item.category_id.as_deref())) + (category_options(categories, item.category_id)) } button class="button button-small button-secondary" type="submit" { "Save" } } @@ -377,7 +377,7 @@ fn item_row(item: &Item, categories: &[Category], csrf_token: &str) -> Markup { } } -fn category_options(categories: &[Category], selected: Option<&str>) -> Markup { +fn category_options(categories: &[Category], selected: Option) -> Markup { html! { @if selected.is_none() { option value="" selected { "No category" } @@ -385,7 +385,7 @@ fn category_options(categories: &[Category], selected: Option<&str>) -> Markup { option value="" { "No category" } } @for category in categories { - @if selected == Some(category.id.as_str()) { + @if selected == Some(category.id) { option value=(category.id) selected { (category.name) } } @else { option value=(category.id) { (category.name) }