172 lines
5.5 KiB
Rust
172 lines
5.5 KiB
Rust
use crate::schema::items;
|
|
use crate::schema::items::dsl::*;
|
|
use crate::{SqliteLike, lock_db_write, lock_db_read};
|
|
use crate::user;
|
|
use diesel::dsl::max;
|
|
use diesel::prelude::*;
|
|
use serde::{Serialize, Deserialize};
|
|
use std::vec::Vec;
|
|
|
|
#[derive(Debug)]
|
|
pub struct ItemOpError(pub String);
|
|
|
|
impl ItemOpError {
|
|
fn new(s: impl Into<String>) -> ItemOpError {
|
|
ItemOpError(s.into())
|
|
}
|
|
}
|
|
|
|
impl Into<ItemOpError> for &str {
|
|
fn into(self) -> ItemOpError {
|
|
ItemOpError::new(self)
|
|
}
|
|
}
|
|
|
|
#[derive(Queryable)]
|
|
pub struct Item {
|
|
// This "id", though primary key, is not how the client actually
|
|
// identifies an item, and it is not sent to the client.
|
|
// Instead, this "id" is more like a "timestamp", in the sense
|
|
// that each time an item is modified, it increments.
|
|
// (this incrementing is achieved by deleting and re-inserting
|
|
// the item and relying on AUTOINCREMENT)
|
|
// This is used in place of the role of timestamp in the Ruby
|
|
// and Go implementation.
|
|
pub id: i64,
|
|
pub owner: i32,
|
|
pub uuid: String,
|
|
pub content: Option<String>,
|
|
pub content_type: String,
|
|
pub enc_item_key: Option<String>,
|
|
pub deleted: bool,
|
|
pub created_at: String,
|
|
pub updated_at: Option<String>
|
|
}
|
|
|
|
#[derive(Insertable)]
|
|
#[table_name = "items"]
|
|
struct InsertItem {
|
|
owner: i32,
|
|
uuid: String,
|
|
content: Option<String>,
|
|
content_type: String,
|
|
enc_item_key: Option<String>,
|
|
deleted: bool,
|
|
created_at: String,
|
|
updated_at: Option<String>
|
|
}
|
|
|
|
#[derive(Serialize, Deserialize, Clone)]
|
|
pub struct SyncItem {
|
|
pub uuid: String,
|
|
pub content: Option<String>,
|
|
pub content_type: String,
|
|
pub enc_item_key: Option<String>,
|
|
#[serde(default)]
|
|
pub deleted: bool,
|
|
pub created_at: String,
|
|
pub updated_at: Option<String>
|
|
}
|
|
|
|
impl Into<SyncItem> for Item {
|
|
fn into(self) -> SyncItem {
|
|
SyncItem {
|
|
uuid: self.uuid,
|
|
content: self.content,
|
|
content_type: self.content_type,
|
|
enc_item_key: self.enc_item_key,
|
|
deleted: self.deleted,
|
|
created_at: self.created_at,
|
|
updated_at: self.updated_at
|
|
}
|
|
}
|
|
}
|
|
|
|
impl SyncItem {
|
|
pub fn items_of_user(
|
|
db: &impl SqliteLike, u: &user::User,
|
|
since_id: Option<i64>, max_id: Option<i64>,
|
|
limit: Option<i64>
|
|
) -> Result<Vec<Item>, ItemOpError> {
|
|
lock_db_read!()
|
|
.and_then(|_| {
|
|
let mut stmt = items.filter(owner.eq(u.id)).into_boxed();
|
|
if let Some(limit) = limit {
|
|
stmt = stmt.limit(limit);
|
|
}
|
|
|
|
if let Some(since_id) = since_id {
|
|
stmt = stmt.filter(id.gt(since_id));
|
|
}
|
|
|
|
if let Some(max_id) = max_id {
|
|
stmt = stmt.filter(id.le(max_id));
|
|
}
|
|
|
|
stmt.order(id.asc())
|
|
.load::<Item>(db)
|
|
.map_err(|_| "Database error".into())
|
|
})
|
|
}
|
|
|
|
pub fn find_item_by_uuid(db: &impl SqliteLike, u: &user::User, i: &str) -> Result<Item, ItemOpError> {
|
|
lock_db_read!()
|
|
.and_then(|_| {
|
|
items.filter(owner.eq(u.id).and(uuid.eq(i)))
|
|
.first::<Item>(db)
|
|
.map_err(|_| "Database error".into())
|
|
})
|
|
}
|
|
|
|
// Get the current maximum item ID for a user.
|
|
// Remember that IDs do not identify item; instead, they are incremented to the largest value
|
|
// every time an item is updated (see Self::items_insert).
|
|
// The ID returned by this function is more like a "timestamp" of the latest "state"
|
|
pub fn get_current_max_id(db: &impl SqliteLike, u: &user::User) -> Result<Option<i64>, ItemOpError> {
|
|
lock_db_read!()
|
|
.and_then(|_| {
|
|
items.filter(owner.eq(u.id))
|
|
.select(max(id))
|
|
.first::<Option<i64>>(db)
|
|
.map_err(|_| "Database error".into())
|
|
})
|
|
}
|
|
|
|
pub fn items_insert(db: &impl SqliteLike, u: &user::User, it: &SyncItem) -> Result<i64, ItemOpError> {
|
|
// First, try to find the original item, if any, delete it, and insert a new one with the same UUID
|
|
// This way, the ID is updated each time an item is updated
|
|
// This method acts both as insertion and update
|
|
let orig = lock_db_read!()
|
|
.and_then(|_| {
|
|
items.filter(uuid.eq(&it.uuid).and(owner.eq(u.id)))
|
|
.load::<Item>(db)
|
|
.map_err(|_| "Database error".into())
|
|
})?;
|
|
|
|
let _lock = lock_db_write!()?;
|
|
if !orig.is_empty() {
|
|
diesel::delete(items.filter(uuid.eq(&it.uuid).and(owner.eq(u.id))))
|
|
.execute(db)
|
|
.map(|_| ())
|
|
.map_err(|_| "Database error".into())?;
|
|
}
|
|
|
|
diesel::insert_into(items::table)
|
|
.values(InsertItem {
|
|
owner: u.id,
|
|
uuid: it.uuid.clone(),
|
|
content: if it.deleted { None } else { it.content.clone() },
|
|
content_type: it.content_type.clone(),
|
|
enc_item_key: if it.deleted { None } else { it.enc_item_key.clone() },
|
|
deleted: it.deleted,
|
|
created_at: it.created_at.clone(),
|
|
updated_at: it.updated_at.clone()
|
|
})
|
|
.execute(db)
|
|
.map_err(|_| "Database error".into())?;
|
|
std::mem::drop(_lock);
|
|
|
|
Self::find_item_by_uuid(db, u, &it.uuid)
|
|
.map(|i| i.id)
|
|
}
|
|
} |