use autoincrement for IDs

This commit is contained in:
2026-08-01 15:56:21 -04:00
parent 5da5fee942
commit 2db105bd6f
6 changed files with 154 additions and 287 deletions
+68 -106
View File
@@ -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<String>,
pub category_id: Option<i64>,
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<User> {
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<GroceryList> {
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<Option<GroceryList>> {
pub async fn list_access(&self, list_id: i64) -> DbResult<Option<GroceryList>> {
self.call(move |connection| {
connection
.query_row(
@@ -304,7 +298,7 @@ impl Database {
.await
}
pub async fn items(&self, list_id: String) -> DbResult<Vec<Item>> {
pub async fn items(&self, list_id: i64) -> DbResult<Vec<Item>> {
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<Vec<Category>> {
pub async fn categories(&self, list_id: i64) -> DbResult<Vec<Category>> {
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<i64> {
pub async fn create_category(&self, list_id: i64, name: String) -> DbResult<i64> {
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<String>,
category_id: Option<i64>,
) -> DbResult<i64> {
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<i64> {
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<String>,
category_id: Option<i64>,
) -> DbResult<i64> {
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<i64> {
pub async fn delete_item(&self, list_id: i64, item_id: i64) -> DbResult<i64> {
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<i64> {
pub async fn create_invitation(&self, created_by: i64, token: String) -> DbResult<i64> {
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<bool> {
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<i64>,
) -> 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<i64> {
fn bump_revision(transaction: &rusqlite::Transaction<'_>, list_id: i64) -> DbResult<i64> {
transaction
.execute(
"UPDATE lists SET revision = revision + 1 WHERE id = ?1",
+21 -20
View File
@@ -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<Mutex<HashMap<String, Room>>>,
rooms: Arc<Mutex<HashMap<i64, Room>>>,
}
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<PresenceUser> {
pub async fn presence(&self, list_id: i64) -> Vec<PresenceUser> {
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<PresenceUser> {
let mut users = HashMap::<String, PresenceUser>::new();
let mut users = HashMap::<i64, PresenceUser>::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(),
});
}
+60 -58
View File
@@ -463,12 +463,12 @@ async fn create_list(
async fn list_page(
State(state): State<AppState>,
user: CurrentUser,
Path(list_id): Path<String>,
Path(list_id): Path<i64>,
) -> Result<Response, AppError> {
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<AppState>,
user: CurrentUser,
Path(list_id): Path<String>,
Path(list_id): Path<i64>,
LoggedForm(form): LoggedForm<ItemForm>,
) -> Result<Response, AppError> {
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<AppState>,
user: CurrentUser,
Path((list_id, item_id)): Path<(String, String)>,
Path((list_id, item_id)): Path<(i64, i64)>,
LoggedForm(form): LoggedForm<CheckForm>,
) -> Result<Response, AppError> {
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<AppState>,
user: CurrentUser,
Path((list_id, item_id)): Path<(String, String)>,
Path((list_id, item_id)): Path<(i64, i64)>,
LoggedForm(form): LoggedForm<ItemForm>,
) -> Result<Response, AppError> {
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<AppState>,
user: CurrentUser,
Path((list_id, item_id)): Path<(String, String)>,
Path((list_id, item_id)): Path<(i64, i64)>,
LoggedForm(form): LoggedForm<CsrfForm>,
) -> Result<Response, AppError> {
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<AppState>,
user: CurrentUser,
Path(list_id): Path<String>,
Path(list_id): Path<i64>,
LoggedForm(form): LoggedForm<CategoryForm>,
) -> Result<Response, AppError> {
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<AppState>,
user: CurrentUser,
Path(list_id): Path<String>,
Path(list_id): Path<i64>,
websocket: WebSocketUpgrade,
) -> Result<Response, AppError> {
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<String, AppError> {
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<String, AppError> {
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<Response, AppError> {
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<db::GroceryList, AppError> {
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<String>) -> Option<String> {
category_id.filter(|category_id| !category_id.trim().is_empty())
fn parse_category_id(category_id: Option<String>) -> Option<i64> {
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<bool, AppError> {
+4 -4
View File
@@ -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::<Vec<_>>();
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<i64>) -> 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) }