diff --git a/migrations/20221207230140_create_follows.sql b/migrations/20221207230140_create_follows.sql index c23bbc7..98b205c 100644 --- a/migrations/20221207230140_create_follows.sql +++ b/migrations/20221207230140_create_follows.sql @@ -1,10 +1,9 @@ CREATE TABLE follows ( - id BIGSERIAL PRIMARY KEY, iri VARCHAR, follower_id BIGINT REFERENCES accounts (id) ON DELETE CASCADE, followee_id BIGINT REFERENCES accounts (id) ON DELETE CASCADE, - created_at TIMESTAMP NOT NULL DEFAULT now() + created_at TIMESTAMP NOT NULL DEFAULT now(), + CONSTRAINT follows_pkey PRIMARY KEY (follower_id, followee_id) ); CREATE UNIQUE INDEX index_follows_on_iri ON follows(iri); -CREATE UNIQUE INDEX index_follows_on_follower_id_and_followee_id ON follows (follower_id, followee_id); diff --git a/migrations/20221211175753_create_likes.sql b/migrations/20221211175753_create_likes.sql index 03b31e0..251c67f 100644 --- a/migrations/20221211175753_create_likes.sql +++ b/migrations/20221211175753_create_likes.sql @@ -1,10 +1,9 @@ CREATE TABLE likes ( - id BIGSERIAL PRIMARY KEY, iri VARCHAR, note_id BIGINT REFERENCES notes (id) ON DELETE CASCADE, account_id BIGINT REFERENCES accounts (id) ON DELETE CASCADE, - created_at TIMESTAMP NOT NULL DEFAULT now() + created_at TIMESTAMP NOT NULL DEFAULT now(), + CONSTRAINT likes_pkey PRIMARY KEY (note_id, account_id) ); CREATE UNIQUE INDEX index_likes_on_iri ON likes (iri); -CREATE UNIQUE INDEX index_likes_on_note_id_and_account_id ON likes (note_id, account_id); diff --git a/src/core/id.rs b/src/core/id.rs new file mode 100644 index 0000000..f7fdc25 --- /dev/null +++ b/src/core/id.rs @@ -0,0 +1,198 @@ +use sqlx::{ + database::{HasArguments, HasValueRef}, + encode::IsNull, + error::BoxDynError, + Database, +}; +use std::{ + any::type_name, + fmt, + hash::{Hash, Hasher}, + marker::PhantomData, +}; + +/// Primitive type for regular database primary keys. +pub type IntId = i64; + +/// Safe ID type for database models. +/// `T` is the database model that this ID references, +/// `I` is the primitive type representing the raw database type. +/// +/// Database models should define their own ID types, like so: +/// +/// ``` +/// pub type MyId = Id; +/// +/// pub struct MyModel { +/// pub id: MyId, +/// // ... +/// } +/// ``` +pub struct Id { + id: I, + _phantom: PhantomData, +} + +impl Id { + pub fn inner(&self) -> &I { + &self.id + } + + pub fn into_inner(self) -> I { + self.id + } + + pub const fn from(id: I) -> Self { + Id { + id, + _phantom: PhantomData, + } + } +} + +impl Clone for Id +where + I: Clone, +{ + fn clone(&self) -> Self { + Id { + id: self.id.clone(), + _phantom: PhantomData, + } + } +} +impl Copy for Id where I: Copy {} + +impl From for Id { + fn from(id: I) -> Self { + Id { + id, + _phantom: PhantomData, + } + } +} + +impl PartialEq for Id +where + I: PartialEq, +{ + fn eq(&self, other: &Self) -> bool { + self.id.eq(&other.id) + } +} +impl Eq for Id where I: Eq {} + +impl Hash for Id +where + I: Hash, +{ + fn hash(&self, state: &mut H) + where + H: Hasher, + { + self.id.hash(state); + } +} + +unsafe impl Send for Id where I: Send {} +unsafe impl Sync for Id where I: Sync {} + +impl<'r, DB: Database, T, I> sqlx::Decode<'r, DB> for Id +where + I: sqlx::Decode<'r, DB>, +{ + fn decode(value: >::ValueRef) -> Result { + let id = I::decode(value)?; + Ok(Id { + id, + _phantom: PhantomData, + }) + } +} + +impl<'q, DB: Database, T, I> sqlx::Encode<'q, DB> for Id +where + I: sqlx::Encode<'q, DB>, +{ + fn encode(self, buf: &mut >::ArgumentBuffer) -> IsNull { + self.id.encode(buf) + } + + fn encode_by_ref(&self, buf: &mut >::ArgumentBuffer) -> IsNull { + self.id.encode_by_ref(buf) + } + + fn produces(&self) -> Option { + self.id.produces() + } + + fn size_hint(&self) -> usize { + self.id.size_hint() + } +} + +impl sqlx::Type for Id +where + I: sqlx::Type, +{ + fn type_info() -> DB::TypeInfo { + I::type_info() + } + + fn compatible(ty: &DB::TypeInfo) -> bool { + I::compatible(ty) + } +} + +impl serde::Serialize for Id +where + I: serde::Serialize, +{ + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + self.id.serialize(serializer) + } +} + +impl<'de, T, I> serde::Deserialize<'de> for Id +where + I: serde::Deserialize<'de>, +{ + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + let id = I::deserialize(deserializer)?; + Ok(Id { + id, + _phantom: PhantomData, + }) + } + + fn deserialize_in_place(deserializer: D, place: &mut Self) -> Result<(), D::Error> + where + D: serde::Deserializer<'de>, + { + I::deserialize_in_place(deserializer, &mut place.id) + } +} + +impl fmt::Debug for Id +where + I: fmt::Debug, +{ + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{}#{:?}", type_name::(), &self.id) + } +} + +impl fmt::Display for Id +where + I: fmt::Display, +{ + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + fmt::Display::fmt(&self.id, f) + } +} diff --git a/src/core/mod.rs b/src/core/mod.rs index 6c614dc..efdbd75 100644 --- a/src/core/mod.rs +++ b/src/core/mod.rs @@ -7,8 +7,8 @@ pub use crate::util::validate::{Insane, Sane}; mod error; pub use error::*; - -pub type Id = i64; +mod id; +pub use id::*; pub fn utc_now() -> NaiveDateTime { Utc::now().naive_utc() diff --git a/src/data/account/mem.rs b/src/data/account/mem.rs index 76bf86d..fa94845 100644 --- a/src/data/account/mem.rs +++ b/src/data/account/mem.rs @@ -1,10 +1,8 @@ -use crate::core::*; - use crate::data::memcache::{Indexable, MemCache}; -use crate::model::Account; +use crate::model::{Account, AccountId}; pub struct MemAccountDataSource { - cache: MemCache, + cache: MemCache, } impl MemAccountDataSource { @@ -14,7 +12,7 @@ impl MemAccountDataSource { } } - pub async fn by_id(&self, id: Id) -> Option { + pub async fn by_id(&self, id: AccountId) -> Option { self.cache.get(id).await } @@ -23,8 +21,8 @@ impl MemAccountDataSource { } } -impl Indexable for Account { - fn get_id(&self) -> Id { +impl Indexable for Account { + fn get_id(&self) -> AccountId { self.id } } diff --git a/src/data/account/pg.rs b/src/data/account/pg.rs index 9087e76..139ed7b 100644 --- a/src/data/account/pg.rs +++ b/src/data/account/pg.rs @@ -1,7 +1,7 @@ use sqlx::PgPool; use crate::core::*; -use crate::model::account::{Account, NewAccount}; +use crate::model::{Account, AccountId, NewAccount}; pub struct PgAccountDataSource { pool: PgPool, @@ -12,7 +12,7 @@ impl PgAccountDataSource { PgAccountDataSource { pool } } - pub async fn by_id(&self, id: Id) -> Result { + pub async fn by_id(&self, id: AccountId) -> Result { let account: Account = sqlx::query_as("SELECT * FROM accounts WHERE id = $1") .bind(id) .fetch_one(&self.pool) diff --git a/src/data/follow/pg.rs b/src/data/follow/pg.rs index 7bfb10d..4ee686f 100644 --- a/src/data/follow/pg.rs +++ b/src/data/follow/pg.rs @@ -2,7 +2,8 @@ use sqlx::PgPool; use crate::core::*; use crate::data::Count; -use crate::model::follow::{Follow, NewFollow}; +use crate::model::AccountId; +use crate::model::{Follow, FollowId, NewFollow}; pub struct PgFollowDataSource { pool: PgPool, @@ -13,12 +14,10 @@ impl PgFollowDataSource { PgFollowDataSource { pool } } - pub async fn by_id(&self, id: Id) -> Result { - let follow: Follow = sqlx::query_as("SELECT * FROM follows WHERE id = $1") - .bind(id) - .fetch_one(&self.pool) - .await?; - Ok(follow) + pub async fn by_id(&self, id: FollowId) -> Result { + let (follower_id, followee_id) = id.into_inner(); + self.by_follower_and_followee(follower_id, followee_id) + .await } pub async fn create(&self, new: NewFollow) -> Result { @@ -37,8 +36,8 @@ impl PgFollowDataSource { pub async fn by_follower_and_followee( &self, - follower_id: Id, - followee_id: Id, + follower_id: AccountId, + followee_id: AccountId, ) -> Result { let follow: Follow = sqlx::query_as("SELECT * FROM follows WHERE follower_id = $1 AND followee_id = $2") @@ -49,7 +48,7 @@ impl PgFollowDataSource { Ok(follow) } - pub async fn followees_of(&self, account_id: Id) -> Result> { + pub async fn followees_of(&self, account_id: AccountId) -> Result> { let followees: Vec = sqlx::query_as("SELECT * FROM follows WHERE follower_id = $1") .bind(account_id) .fetch_all(&self.pool) @@ -57,7 +56,7 @@ impl PgFollowDataSource { Ok(followees) } - pub async fn followers_of(&self, account_id: Id) -> Result> { + pub async fn followers_of(&self, account_id: AccountId) -> Result> { let followers: Vec = sqlx::query_as("SELECT * FROM follows WHERE followee_id = $1") .bind(account_id) .fetch_all(&self.pool) @@ -65,7 +64,7 @@ impl PgFollowDataSource { Ok(followers) } - pub async fn following_count_of(&self, account_id: Id) -> Result { + pub async fn following_count_of(&self, account_id: AccountId) -> Result { let followee_count: Count = sqlx::query_as("SELECT COUNT(*) AS count FROM follows WHERE follower_id = $1") .bind(account_id) @@ -74,7 +73,7 @@ impl PgFollowDataSource { Ok(followee_count.count as u32) } - pub async fn follower_count_of(&self, account_id: Id) -> Result { + pub async fn follower_count_of(&self, account_id: AccountId) -> Result { let follower_count: Count = sqlx::query_as("SELECT COUNT(*) AS count FROM follows WHERE followee_id = $1") .bind(account_id) diff --git a/src/data/like/mem.rs b/src/data/like/mem.rs new file mode 100644 index 0000000..78a9f21 --- /dev/null +++ b/src/data/like/mem.rs @@ -0,0 +1,72 @@ +use crate::data::memcache::{Indexable, MemCache}; +use crate::model::{AccountId, Like, LikeId, NoteId}; + +pub struct MemLikeDataSource { + cache: MemCache, + counts: MemCache, +} + +#[derive(Clone)] +struct LikeCount { + note_id: NoteId, + likes: u64, +} + +impl MemLikeDataSource { + pub fn new() -> MemLikeDataSource { + MemLikeDataSource { + cache: MemCache::new(), + counts: MemCache::new(), + } + } + + pub async fn by_note_and_account( + &self, + note_id: NoteId, + account_id: AccountId, + ) -> Option { + self.cache.get((note_id, account_id).into()).await + } + + pub async fn store(&self, like: Like) { + self.cache.put(like).await; + } + + /// Delete a like entry from the cache **without decrementing the cached count**. + pub async fn delete(&self, note_id: NoteId, account_id: AccountId) { + self.cache.del((note_id, account_id).into()).await; + } + + pub async fn get_count_by_note(&self, note_id: NoteId) -> Option { + self.counts.get(note_id).await.map(|count| count.likes) + } + + pub async fn store_count_by_note(&self, note_id: NoteId, count: u64) { + self.counts + .put(LikeCount { + note_id, + likes: count, + }) + .await; + } + + pub async fn increment_count(&self, note_id: NoteId) { + self.counts.update(note_id, |entry| entry.likes += 1).await; + } + + pub async fn decrement_count(&self, note_id: NoteId) { + self.counts.update(note_id, |entry| entry.likes -= 1).await; + } +} + +impl Indexable for Like { + fn get_id(&self) -> LikeId { + (self.note_id, self.account_id).into() + } +} + +impl Indexable for LikeCount { + fn get_id(&self) -> NoteId { + self.note_id + } +} diff --git a/src/data/like/mod.rs b/src/data/like/mod.rs index 8085007..403c37c 100644 --- a/src/data/like/mod.rs +++ b/src/data/like/mod.rs @@ -1,3 +1,5 @@ -pub mod pg; +mod mem; +mod pg; +pub use mem::MemLikeDataSource; pub use pg::PgLikeDataSource; diff --git a/src/data/like/pg.rs b/src/data/like/pg.rs index 624e8ad..9319541 100644 --- a/src/data/like/pg.rs +++ b/src/data/like/pg.rs @@ -2,7 +2,7 @@ use sqlx::PgPool; use crate::core::*; use crate::data::Count; -use crate::model::{Like, NewLike}; +use crate::model::{AccountId, Like, NewLike, NoteId}; pub struct PgLikeDataSource { pool: PgPool, @@ -13,14 +13,6 @@ impl PgLikeDataSource { PgLikeDataSource { pool } } - pub async fn by_id(&self, id: Id) -> Result { - let like: Like = sqlx::query_as("SELECT * FROM likes WHERE id = $1") - .bind(id) - .fetch_one(&self.pool) - .await?; - Ok(like) - } - pub async fn create(&self, new: NewLike) -> Result { let like: Like = sqlx::query_as( "INSERT INTO likes (iri, note_id, account_id) @@ -35,7 +27,7 @@ impl PgLikeDataSource { Ok(like) } - pub async fn delete(&self, note_id: Id, account_id: Id) -> Result<()> { + pub async fn delete(&self, note_id: NoteId, account_id: AccountId) -> Result<()> { sqlx::query("DELETE FROM likes WHERE note_id = $1 AND account_id = $2") .bind(note_id) .bind(account_id) @@ -44,7 +36,7 @@ impl PgLikeDataSource { Ok(()) } - pub async fn count_by_note(&self, note_id: Id) -> Result { + pub async fn count_by_note(&self, note_id: NoteId) -> Result { let result: Count = sqlx::query_as("SELECT COUNT(*) as count FROM likes WHERE note_id = $1") .bind(note_id) @@ -53,7 +45,11 @@ impl PgLikeDataSource { Ok(result.count as u64) } - pub async fn by_note_and_account(&self, note_id: Id, account_id: Id) -> Result { + pub async fn by_note_and_account( + &self, + note_id: NoteId, + account_id: AccountId, + ) -> Result { let like: Like = sqlx::query_as("SELECT * FROM likes WHERE note_id = $1 AND account_id = $2") .bind(note_id) diff --git a/src/data/memcache.rs b/src/data/memcache.rs index 9716609..9a87225 100644 --- a/src/data/memcache.rs +++ b/src/data/memcache.rs @@ -137,14 +137,16 @@ impl Hasher for Djb2 { mod tests { use super::*; + type TestId = Id; + #[derive(Clone, Debug, PartialEq)] struct TestEntry { - id: Id, + id: TestId, data: i32, } - impl Indexable for TestEntry { - fn get_id(&self) -> Id { + impl Indexable for TestEntry { + fn get_id(&self) -> TestId { self.id } } @@ -152,14 +154,16 @@ mod tests { #[actix_web::test] async fn store_stuff() { let cache = MemCache::with_capacity(256); - for id in 0..1024 { + for i in 0i64..1024i64 { + let id = i.into(); let entry = TestEntry { id, data: 0 }; cache.put(entry.clone()).await; assert_eq!(cache.get(id).await, Some(entry)); } let mut had_entries = false; - for id in 0..1024 { + for i in 0..1024 { + let id = i.into(); let entry = cache.del(id).await; assert_eq!(cache.get(id).await, None); if let Some(entry) = entry { @@ -172,19 +176,19 @@ mod tests { #[actix_web::test] async fn update_stuff() { - const ID: Id = 420; + let id = 420.into(); let cache = MemCache::with_capacity(256); - cache.put(TestEntry { id: ID, data: 69 }).await; - cache.update(ID, |entry| entry.data = 1312).await; - assert_eq!(cache.get(ID).await, Some(TestEntry { id: ID, data: 1312 })); + cache.put(TestEntry { id, data: 69 }).await; + cache.update(id, |entry| entry.data = 1312).await; + assert_eq!(cache.get(id).await, Some(TestEntry { id, data: 1312 })); } #[actix_web::test] #[should_panic] async fn mutate_id() { - const ID: Id = 420; + let id = 420.into(); let cache = MemCache::with_capacity(256); - cache.put(TestEntry { id: ID, data: 69 }).await; - cache.update(ID, |entry| entry.id = 1312).await; + cache.put(TestEntry { id, data: 69 }).await; + cache.update(id, |entry| entry.id = 1312.into()).await; } } diff --git a/src/data/mod.rs b/src/data/mod.rs index a2e1603..acebd9d 100644 --- a/src/data/mod.rs +++ b/src/data/mod.rs @@ -1,12 +1,12 @@ /// Small support utility for in-memory cache. pub mod memcache; -pub mod account; -pub mod follow; -pub mod like; -pub mod note; -pub mod object; -pub mod user; +mod account; +mod follow; +mod like; +mod note; +mod object; +mod user; pub use account::*; pub use follow::*; diff --git a/src/data/note/mem.rs b/src/data/note/mem.rs index 47a575d..664252b 100644 --- a/src/data/note/mem.rs +++ b/src/data/note/mem.rs @@ -1,10 +1,8 @@ -use crate::core::*; - use crate::data::memcache::{Indexable, MemCache}; -use crate::model::Note; +use crate::model::{Note, NoteId}; pub struct MemNoteDataSource { - cache: MemCache, + cache: MemCache, } impl MemNoteDataSource { @@ -14,7 +12,7 @@ impl MemNoteDataSource { } } - pub async fn by_id(&self, id: Id) -> Option { + pub async fn by_id(&self, id: NoteId) -> Option { self.cache.get(id).await } @@ -22,13 +20,13 @@ impl MemNoteDataSource { self.cache.put(note).await } - pub async fn delete(&self, id: Id) { + pub async fn delete(&self, id: NoteId) { self.cache.del(id).await; } } -impl Indexable for Note { - fn get_id(&self) -> Id { +impl Indexable for Note { + fn get_id(&self) -> NoteId { self.id } } diff --git a/src/data/note/pg.rs b/src/data/note/pg.rs index f1edd42..9d636e9 100644 --- a/src/data/note/pg.rs +++ b/src/data/note/pg.rs @@ -3,7 +3,7 @@ use sqlx::PgPool; use crate::core::*; use crate::data::Count; -use crate::model::note::{NewNote, Note}; +use crate::model::{AccountId, NewNote, Note, NoteId}; pub struct PgNoteDataSource { pool: PgPool, @@ -14,7 +14,7 @@ impl PgNoteDataSource { PgNoteDataSource { pool } } - pub async fn by_id(&self, id: Id) -> Result { + pub async fn by_id(&self, id: NoteId) -> Result { let note: Note = sqlx::query_as("SELECT * FROM notes WHERE id = $1") .bind(id) .fetch_one(&self.pool) @@ -22,7 +22,7 @@ impl PgNoteDataSource { Ok(note) } - pub async fn by_account(&self, id: Id, since: NaiveDateTime) -> Result> { + pub async fn by_account(&self, id: AccountId, since: NaiveDateTime) -> Result> { let notes: Vec = sqlx::query_as( "SELECT * FROM notes WHERE account_id = $1 AND created_at < $1 @@ -61,7 +61,7 @@ impl PgNoteDataSource { Ok(note) } - pub async fn delete(&self, id: Id) -> Result<()> { + pub async fn delete(&self, id: NoteId) -> Result<()> { sqlx::query("DELETE FROM notes WHERE id = $1") .bind(id) .execute(&self.pool) @@ -69,7 +69,7 @@ impl PgNoteDataSource { Ok(()) } - pub async fn count_by_account(&self, account_id: Id) -> Result { + pub async fn count_by_account(&self, account_id: AccountId) -> Result { let count: Count = sqlx::query_as("SELECT COUNT(*) AS count FROM notes WHERE account_id = $1") .bind(account_id) diff --git a/src/data/object/mem.rs b/src/data/object/mem.rs index 846d099..6230251 100644 --- a/src/data/object/mem.rs +++ b/src/data/object/mem.rs @@ -1,10 +1,8 @@ -use uuid::Uuid; - use crate::data::memcache::{Indexable, MemCache}; -use crate::model::Object; +use crate::model::{Object, ObjectId}; pub struct MemObjectDataSource { - cache: MemCache, + cache: MemCache, } impl MemObjectDataSource { @@ -14,7 +12,7 @@ impl MemObjectDataSource { } } - pub async fn by_id(&self, id: Uuid) -> Option { + pub async fn by_id(&self, id: ObjectId) -> Option { self.cache.get(id).await } @@ -23,8 +21,8 @@ impl MemObjectDataSource { } } -impl Indexable for Object { - fn get_id(&self) -> Uuid { +impl Indexable for Object { + fn get_id(&self) -> ObjectId { self.id } } diff --git a/src/data/object/pg.rs b/src/data/object/pg.rs index 937212f..e7681c2 100644 --- a/src/data/object/pg.rs +++ b/src/data/object/pg.rs @@ -1,8 +1,7 @@ -use sqlx::types::Uuid; use sqlx::PgPool; use crate::core::*; -use crate::model::object::*; +use crate::model::{NewObject, Object, ObjectId}; pub struct PgObjectDataSource { pool: PgPool, @@ -37,7 +36,7 @@ impl PgObjectDataSource { Ok(()) } - pub async fn by_id(&self, id: Uuid) -> Result { + pub async fn by_id(&self, id: ObjectId) -> Result { let object: Object = sqlx::query_as("SELECT * FROM objects WHERE id = $1") .bind(id) .fetch_one(&self.pool) diff --git a/src/data/user/mem.rs b/src/data/user/mem.rs index 3a2c21c..8c5630b 100644 --- a/src/data/user/mem.rs +++ b/src/data/user/mem.rs @@ -1,10 +1,8 @@ -use crate::core::*; - use crate::data::memcache::{Indexable, MemCache}; -use crate::model::User; +use crate::model::{User, UserId}; pub struct MemUserDataSource { - cache: MemCache, + cache: MemCache, } impl MemUserDataSource { @@ -14,7 +12,7 @@ impl MemUserDataSource { } } - pub async fn by_id(&self, id: Id) -> Option { + pub async fn by_id(&self, id: UserId) -> Option { self.cache.get(id).await } @@ -23,8 +21,8 @@ impl MemUserDataSource { } } -impl Indexable for User { - fn get_id(&self) -> Id { +impl Indexable for User { + fn get_id(&self) -> UserId { self.id } } diff --git a/src/data/user/pg.rs b/src/data/user/pg.rs index 4e792bc..4e8b689 100644 --- a/src/data/user/pg.rs +++ b/src/data/user/pg.rs @@ -2,7 +2,7 @@ use serde; use sqlx::{Executor, PgPool}; use crate::core::*; -use crate::model::user::{NewUser, User}; +use crate::model::{AccountId, NewUser, User, UserId}; pub struct PgUserDataSource { pool: PgPool, @@ -13,7 +13,7 @@ impl PgUserDataSource { PgUserDataSource { pool } } - pub async fn by_id(&self, id: Id) -> Result { + pub async fn by_id(&self, id: UserId) -> Result { let user: User = sqlx::query_as("SELECT * FROM users WHERE id = $1") .bind(id) .fetch_one(&self.pool) @@ -21,7 +21,7 @@ impl PgUserDataSource { Ok(user) } - pub async fn by_account(&self, account_id: Id) -> Result { + pub async fn by_account(&self, account_id: AccountId) -> Result { let user: User = sqlx::query_as("SELECT * FROM users WHERE account_id = $1") .bind(account_id) .fetch_one(&self.pool) diff --git a/src/ent/account.rs b/src/ent/account.rs index 901554a..bc3823e 100644 --- a/src/ent/account.rs +++ b/src/ent/account.rs @@ -29,7 +29,7 @@ impl Account { let following_count = state.repo.follows.following_count_of(model.id).await?; let statuses_count = state.repo.notes.count_by_account(model.id).await?; Ok(Account { - id: format!("{}", model.id), + id: model.id.to_string(), username: model.name.clone(), display_name: model.display_name.as_ref().unwrap_or(&model.name).clone(), created_at: DateTime::from_utc(model.created_at, Utc), diff --git a/src/ent/status.rs b/src/ent/status.rs index 0a5e6b6..14165cf 100644 --- a/src/ent/status.rs +++ b/src/ent/status.rs @@ -39,7 +39,7 @@ impl Status { }; let author = state.repo.accounts.by_id(model.account_id).await?; Ok(Status { - id: format!("{}", model.id), + id: model.id.to_string(), created_at: DateTime::from_utc(model.created_at, Utc), sensitive: model.sensitive, spoiler_text: model.summary.clone().unwrap_or_default(), diff --git a/src/model/account.rs b/src/model/account.rs index c615f56..56319c0 100644 --- a/src/model/account.rs +++ b/src/model/account.rs @@ -2,12 +2,14 @@ use chrono::prelude::*; use serde::{Deserialize, Serialize}; use sqlx::FromRow; -use crate::core::Id; +use crate::core::*; use crate::util::validate::{ResultBuilder, Validate}; +pub type AccountId = Id; + #[derive(Clone, Deserialize, Serialize, FromRow)] pub struct Account { - pub id: Id, + pub id: AccountId, pub iri: Option, pub name: String, pub domain: String, @@ -23,8 +25,8 @@ pub struct NewAccount { pub display_name: Option, } -impl From for Id { - fn from(account: Account) -> Id { +impl From for AccountId { + fn from(account: Account) -> AccountId { account.id } } @@ -33,7 +35,7 @@ impl Validate for NewAccount { fn validate(&self, builder: ResultBuilder) -> ResultBuilder { builder .field(&self.name, "name", |f| { - f.check("Must not be empty", |v| v.len() > 0) + f.check("Must not be empty", |v| !v.is_empty()) .check("Must be at most 16 characters long", |v| v.len() <= 16) }) .field(&self.display_name, "display_name", |f| { diff --git a/src/model/follow.rs b/src/model/follow.rs index 1b3024c..cc11dbb 100644 --- a/src/model/follow.rs +++ b/src/model/follow.rs @@ -3,26 +3,28 @@ use serde::{Deserialize, Serialize}; use sqlx::FromRow; use crate::core::*; +use crate::model::account::AccountId; use crate::util::validate::{ResultBuilder, Validate}; +pub type FollowId = Id; + #[derive(Deserialize, Serialize, FromRow)] pub struct Follow { - pub id: Id, pub iri: Option, - pub follower_id: Id, - pub followee_id: Id, + pub follower_id: AccountId, + pub followee_id: AccountId, pub created_at: NaiveDateTime, } pub struct NewFollow { pub iri: Option, - pub follower_id: Id, - pub followee_id: Id, + pub follower_id: AccountId, + pub followee_id: AccountId, } -impl From for Id { - fn from(follow: Follow) -> Id { - follow.id +impl From for FollowId { + fn from(follow: Follow) -> FollowId { + (follow.follower_id, follow.followee_id).into() } } diff --git a/src/model/like.rs b/src/model/like.rs index 192ddac..efafbe4 100644 --- a/src/model/like.rs +++ b/src/model/like.rs @@ -3,26 +3,28 @@ use serde::{Deserialize, Serialize}; use sqlx::FromRow; use crate::core::*; +use crate::model::{AccountId, NoteId}; use crate::util::validate::{ResultBuilder, Validate}; -#[derive(Deserialize, Serialize, FromRow)] +pub type LikeId = Id; + +#[derive(Clone, Deserialize, Serialize, FromRow)] pub struct Like { - pub id: Id, pub iri: Option, - pub note_id: Id, - pub account_id: Id, + pub note_id: NoteId, + pub account_id: AccountId, pub created_at: NaiveDateTime, } pub struct NewLike { pub iri: Option, - pub note_id: Id, - pub account_id: Id, + pub note_id: NoteId, + pub account_id: AccountId, } -impl From for Id { - fn from(like: Like) -> Id { - like.id +impl From for LikeId { + fn from(val: Like) -> LikeId { + (val.note_id, val.account_id).into() } } diff --git a/src/model/mod.rs b/src/model/mod.rs index daf1104..2ef1334 100644 --- a/src/model/mod.rs +++ b/src/model/mod.rs @@ -1,13 +1,13 @@ -pub mod account; -pub mod follow; -pub mod like; -pub mod note; -pub mod object; -pub mod user; +mod account; +mod follow; +mod like; +mod note; +mod object; +mod user; -pub use account::{Account, NewAccount}; -pub use follow::{Follow, NewFollow}; -pub use like::{Like, NewLike}; -pub use note::{NewNote, Note}; -pub use object::{NewObject, Object}; -pub use user::{NewUser, User}; +pub use account::*; +pub use follow::*; +pub use like::*; +pub use note::*; +pub use object::*; +pub use user::*; diff --git a/src/model/note.rs b/src/model/note.rs index fcb1453..946f30d 100644 --- a/src/model/note.rs +++ b/src/model/note.rs @@ -3,12 +3,15 @@ use serde::{Deserialize, Serialize}; use sqlx::FromRow; use crate::core::*; +use crate::model::AccountId; use crate::util::validate::{ResultBuilder, Validate}; +pub type NoteId = Id; + #[derive(Clone, Deserialize, Serialize, FromRow)] pub struct Note { - pub id: Id, - pub account_id: Id, + pub id: NoteId, + pub account_id: AccountId, pub iri: Option, pub content: String, pub source: Option, @@ -20,7 +23,7 @@ pub struct Note { } pub struct NewNote { - pub account_id: Id, + pub account_id: AccountId, pub iri: Option, pub content: String, pub source: Option, @@ -29,9 +32,9 @@ pub struct NewNote { pub sensitive: bool, } -impl From for Id { - fn from(status: Note) -> Id { - status.id +impl From for NoteId { + fn from(val: Note) -> NoteId { + val.id } } diff --git a/src/model/object.rs b/src/model/object.rs index 4d1a872..792ae74 100644 --- a/src/model/object.rs +++ b/src/model/object.rs @@ -2,9 +2,13 @@ use chrono::NaiveDateTime; use sqlx::types::Uuid; use sqlx::FromRow; +use crate::core::*; + +pub type ObjectId = Id; + #[derive(Clone, FromRow)] pub struct Object { - pub id: Uuid, + pub id: ObjectId, pub iri: String, pub data: String, pub created_at: NaiveDateTime, @@ -12,7 +16,13 @@ pub struct Object { } pub struct NewObject { - pub id: Uuid, + pub id: ObjectId, pub iri: String, pub data: String, } + +impl From for ObjectId { + fn from(val: Object) -> ObjectId { + val.id + } +} diff --git a/src/model/user.rs b/src/model/user.rs index f782fa2..35014e1 100644 --- a/src/model/user.rs +++ b/src/model/user.rs @@ -2,12 +2,15 @@ use serde::{Deserialize, Serialize}; use sqlx::FromRow; use crate::core::*; +use crate::model::AccountId; use crate::util::validate::{ResultBuilder, Validate}; +pub type UserId = Id; + #[derive(Clone, Deserialize, Serialize, FromRow)] pub struct User { - pub id: Id, - pub account_id: Id, + pub id: UserId, + pub account_id: AccountId, pub email: String, pub password: String, pub reason: Option, @@ -16,15 +19,15 @@ pub struct User { } pub struct NewUser { - pub account_id: Id, + pub account_id: AccountId, pub email: String, pub password: String, pub reason: Option, pub locale: String, } -impl From for Id { - fn from(user: User) -> Id { +impl From for UserId { + fn from(user: User) -> UserId { user.id } } @@ -32,7 +35,7 @@ impl From for Id { impl Validate for NewUser { fn validate(&self, builder: ResultBuilder) -> ResultBuilder { builder.field(&self.email, "email", |f| { - f.check("Email must not be empty", |v| v.len() > 0) + f.check("Email must not be empty", |v| !v.is_empty()) }) } } diff --git a/src/repo/account.rs b/src/repo/account.rs index aa451be..d395711 100644 --- a/src/repo/account.rs +++ b/src/repo/account.rs @@ -1,8 +1,8 @@ use sqlx::PgPool; use crate::core::*; -use crate::data::account::{MemAccountDataSource, PgAccountDataSource}; -use crate::model::account::{Account, NewAccount}; +use crate::data::{MemAccountDataSource, PgAccountDataSource}; +use crate::model::{Account, AccountId, NewAccount}; pub struct AccountRepo { db: PgAccountDataSource, @@ -19,7 +19,7 @@ impl AccountRepo { pub async fn by_id(&self, id: I) -> Result where - I: Into + Send, + I: Into + Send, { let id = id.into(); match self.mem.by_id(id).await { diff --git a/src/repo/follow.rs b/src/repo/follow.rs index 6fea631..95a0872 100644 --- a/src/repo/follow.rs +++ b/src/repo/follow.rs @@ -1,8 +1,8 @@ use sqlx::PgPool; use crate::core::*; -use crate::data::follow::PgFollowDataSource; -use crate::model::follow::{Follow, NewFollow}; +use crate::data::PgFollowDataSource; +use crate::model::{AccountId, Follow, FollowId, NewFollow}; pub struct FollowRepo { db: PgFollowDataSource, @@ -17,7 +17,7 @@ impl FollowRepo { pub async fn by_id(&self, id: I) -> Result where - I: Into + Send, + I: Into + Send, { self.db.by_id(id.into()).await } @@ -35,8 +35,8 @@ impl FollowRepo { followee_id: J, ) -> Result where - I: Into + Send, - J: Into + Send, + I: Into + Send, + J: Into + Send, { let (follower_id, followee_id) = (follower_id.into(), followee_id.into()); self.db @@ -46,8 +46,8 @@ impl FollowRepo { pub async fn follows(&self, follower_id: I, followee_id: J) -> Result where - I: Into + Send, - J: Into + Send, + I: Into + Send, + J: Into + Send, { match self .by_follower_and_followee(follower_id, followee_id) @@ -61,28 +61,28 @@ impl FollowRepo { pub async fn followees_of(&self, account_id: I) -> Result> where - I: Into + Send, + I: Into + Send, { self.db.followees_of(account_id.into()).await } pub async fn followers_of(&self, account_id: I) -> Result> where - I: Into + Send, + I: Into + Send, { self.db.followers_of(account_id.into()).await } pub async fn following_count_of(&self, account_id: I) -> Result where - I: Into + Send, + I: Into + Send, { self.db.following_count_of(account_id.into()).await } pub async fn follower_count_of(&self, account_id: I) -> Result where - I: Into + Send, + I: Into + Send, { self.db.follower_count_of(account_id.into()).await } diff --git a/src/repo/like.rs b/src/repo/like.rs index d79fba1..4ed04e1 100644 --- a/src/repo/like.rs +++ b/src/repo/like.rs @@ -1,57 +1,72 @@ use sqlx::PgPool; use crate::core::*; -use crate::data::PgLikeDataSource; -use crate::model::{Like, NewLike}; +use crate::data::{MemLikeDataSource, PgLikeDataSource}; +use crate::model::{AccountId, Like, NewLike, NoteId}; pub struct LikeRepo { db: PgLikeDataSource, + mem: MemLikeDataSource, } impl LikeRepo { pub fn new(db_pool: PgPool) -> LikeRepo { LikeRepo { db: PgLikeDataSource::new(db_pool), + mem: MemLikeDataSource::new(), } } - pub async fn by_id(&self, id: I) -> Result - where - I: Into + Send, - { - self.db.by_id(id.into()).await - } - pub async fn create(&self, new: T) -> Result where T: TryInto, Error = E> + Send, E: Into, { let new = new.try_into().map_err(|e| e.into())?.inner(); - self.db.create(new).await + let like = self.db.create(new).await?; + self.mem.store(like.clone()).await; + self.mem.increment_count(like.note_id).await; + Ok(like) } pub async fn delete(&self, note_id: N, account_id: A) -> Result<()> where - N: Into, - A: Into, + N: Into, + A: Into, { - self.db.delete(note_id.into(), account_id.into()).await + let (note_id, account_id) = (note_id.into(), account_id.into()); + self.db.delete(note_id, account_id).await?; + self.mem.delete(note_id, account_id).await; + self.mem.decrement_count(note_id).await; + Ok(()) } pub async fn count_by_note(&self, note_id: I) -> Result where - I: Into + Send, + I: Into + Send, { - self.db.count_by_note(note_id.into()).await + let note_id = note_id.into(); + if let Some(count) = self.mem.get_count_by_note(note_id).await { + return Ok(count); + } + + let count = self.db.count_by_note(note_id).await?; + self.mem.store_count_by_note(note_id, count).await; + Ok(count) } pub async fn by_note_and_account(&self, note_id: N, account_id: A) -> Result where - N: Into, - A: Into, + N: Into, + A: Into, { let (note_id, account_id) = (note_id.into(), account_id.into()); - self.db.by_note_and_account(note_id, account_id).await + if let Some(like) = self.mem.by_note_and_account(note_id, account_id).await { + return Ok(like); + } + + let like = self.db.by_note_and_account(note_id, account_id).await?; + self.mem.store(like.clone()).await; + Ok(like) } } diff --git a/src/repo/mod.rs b/src/repo/mod.rs index 2f362ac..3e4e44f 100644 --- a/src/repo/mod.rs +++ b/src/repo/mod.rs @@ -1,18 +1,18 @@ use sqlx::PgPool; -pub mod account; -pub mod follow; -pub mod like; -pub mod note; -pub mod object; -pub mod user; +mod account; +mod follow; +mod like; +mod note; +mod object; +mod user; -use account::AccountRepo; -use follow::FollowRepo; -use like::LikeRepo; -use note::NoteRepo; -use object::ObjectRepo; -use user::UserRepo; +pub use account::AccountRepo; +pub use follow::FollowRepo; +pub use like::LikeRepo; +pub use note::NoteRepo; +pub use object::ObjectRepo; +pub use user::UserRepo; /// The central collection of all data accessible to the app. /// This is included in `AppState` so it is accessible everywhere. diff --git a/src/repo/note.rs b/src/repo/note.rs index dbcd087..da33c2f 100644 --- a/src/repo/note.rs +++ b/src/repo/note.rs @@ -3,7 +3,7 @@ use sqlx::PgPool; use crate::core::*; use crate::data::{MemNoteDataSource, PgNoteDataSource}; -use crate::model::{NewNote, Note}; +use crate::model::{AccountId, NewNote, Note, NoteId}; pub struct NoteRepo { db: PgNoteDataSource, @@ -20,7 +20,7 @@ impl NoteRepo { pub async fn by_id(&self, id: I) -> Result where - I: Into + Send, + I: Into + Send, { let id = id.into(); match self.mem.by_id(id).await { @@ -31,7 +31,7 @@ impl NoteRepo { pub async fn by_account(&self, account_id: I, since: NaiveDateTime) -> Result> where - I: Into + Send, + I: Into + Send, { self.db.by_account(account_id.into(), since).await } @@ -49,7 +49,7 @@ impl NoteRepo { pub async fn delete(&self, note_id: I) -> Result<()> where - I: Into, + I: Into, { let note_id = note_id.into(); self.db.delete(note_id).await?; @@ -59,7 +59,7 @@ impl NoteRepo { pub async fn count_by_account(&self, account_id: I) -> Result where - I: Into + Send, + I: Into + Send, { self.db.count_by_account(account_id.into()).await } diff --git a/src/repo/object.rs b/src/repo/object.rs index a445730..7fe9a60 100644 --- a/src/repo/object.rs +++ b/src/repo/object.rs @@ -1,9 +1,8 @@ -use sqlx::types::Uuid; use sqlx::PgPool; use crate::core::*; -use crate::data::object::{MemObjectDataSource, PgObjectDataSource}; -use crate::model::{NewObject, Object}; +use crate::data::{MemObjectDataSource, PgObjectDataSource}; +use crate::model::{NewObject, Object, ObjectId}; pub struct ObjectRepo { db: PgObjectDataSource, @@ -37,7 +36,7 @@ impl ObjectRepo { pub async fn by_id(&self, id: U) -> Result where - U: Into, + U: Into, { let id = id.into(); match self.mem.by_id(id).await { diff --git a/src/repo/user.rs b/src/repo/user.rs index 495e4fb..86ab965 100644 --- a/src/repo/user.rs +++ b/src/repo/user.rs @@ -1,8 +1,8 @@ use sqlx::PgPool; use crate::core::*; -use crate::data::user::{MemUserDataSource, PgUserDataSource}; -use crate::model::user::{NewUser, User}; +use crate::data::{MemUserDataSource, PgUserDataSource}; +use crate::model::{AccountId, NewUser, User, UserId}; pub struct UserRepo { db: PgUserDataSource, @@ -19,7 +19,7 @@ impl UserRepo { pub async fn by_id(&self, id: I) -> Result where - I: Into + Send, + I: Into + Send, { let id = id.into(); match self.mem.by_id(id).await { @@ -30,7 +30,7 @@ impl UserRepo { pub async fn by_account(&self, account_id: I) -> Result where - I: Into + Send, + I: Into + Send, { self.db.by_account(account_id.into()).await } diff --git a/src/route/api/v1/accounts.rs b/src/route/api/v1/accounts.rs index 098ab39..2cfe0a8 100644 --- a/src/route/api/v1/accounts.rs +++ b/src/route/api/v1/accounts.rs @@ -4,7 +4,7 @@ use serde::Deserialize; use crate::core::*; use crate::ent; use crate::middle::AuthData; -use crate::model::{NewAccount, NewUser}; +use crate::model::{AccountId, NewAccount, NewUser}; use crate::state::AppState; use crate::util::password; use crate::util::validate::{ResultBuilder, Validate}; @@ -70,7 +70,11 @@ async fn verify_credentials(auth: AuthData, state: AppState) -> Result, state: AppState) -> Result { +async fn get_by_id( + auth: AuthData, + path: web::Path, + state: AppState, +) -> Result { let id = path.into_inner(); if let Some(auth) = auth.maybe() { if auth.id == id { @@ -85,7 +89,7 @@ async fn get_by_id(auth: AuthData, path: web::Path, state: AppState) -> Resu } #[get("/{id}/statuses")] -async fn get_notes(path: web::Path, state: AppState) -> Result { +async fn get_notes(path: web::Path, state: AppState) -> Result { let id = path.into_inner(); let account = state.repo.accounts.by_id(id).await?; let notes = state.repo.notes.by_account(account, utc_now()).await?; diff --git a/src/route/api/v1/statuses.rs b/src/route/api/v1/statuses.rs index bdaf265..7ae8ef3 100644 --- a/src/route/api/v1/statuses.rs +++ b/src/route/api/v1/statuses.rs @@ -4,7 +4,7 @@ use serde::Deserialize; use crate::core::*; use crate::ent; use crate::middle::AuthData; -use crate::model::{NewLike, NewNote}; +use crate::model::{NewLike, NewNote, NoteId}; use crate::state::AppState; #[derive(Deserialize)] @@ -37,14 +37,22 @@ async fn create_note( } #[get("/{id}")] -async fn get_by_id(path: web::Path, state: AppState, auth: AuthData) -> Result { +async fn get_by_id( + path: web::Path, + state: AppState, + auth: AuthData, +) -> Result { let id = path.into_inner(); let note = state.repo.notes.by_id(id).await?; Ok(HttpResponse::Ok().json(ent::Status::from_model(&state, ¬e, &auth).await?)) } #[delete("/{id}")] -async fn del_by_id(path: web::Path, state: AppState, auth: AuthData) -> Result { +async fn del_by_id( + path: web::Path, + state: AppState, + auth: AuthData, +) -> Result { let id = path.into_inner(); let auth_account = auth.require()?; let note = state.repo.notes.by_id(id).await?; @@ -57,27 +65,51 @@ async fn del_by_id(path: web::Path, state: AppState, auth: AuthData) -> Resu } #[post("/{id}/favourite")] -async fn add_favourite(path: web::Path, state: AppState, auth: AuthData) -> Result { +async fn add_favourite( + path: web::Path, + state: AppState, + auth: AuthData, +) -> Result { let note_id = path.into_inner(); let auth_account = auth.require()?; let note = state.repo.notes.by_id(note_id).await?; - if state.repo.likes.by_note_and_account(note_id, auth_account.id).await.is_err() { - state.repo.likes.create(Insane::from(NewLike { - iri: None, - note_id: note.id, - account_id: auth_account.id, - })).await?; + if state + .repo + .likes + .by_note_and_account(note_id, auth_account.id) + .await + .is_err() + { + state + .repo + .likes + .create(Insane::from(NewLike { + iri: None, + note_id: note.id, + account_id: auth_account.id, + })) + .await?; } let response = ent::Status::from_model(&state, ¬e, &auth).await?; Ok(HttpResponse::Ok().json(response)) } #[post("/{id}/unfavourite")] -async fn del_favourite(path: web::Path, state: AppState, auth: AuthData) -> Result { +async fn del_favourite( + path: web::Path, + state: AppState, + auth: AuthData, +) -> Result { let note_id = path.into_inner(); let auth_account = auth.require()?; let note = state.repo.notes.by_id(note_id).await?; - if state.repo.likes.by_note_and_account(note_id, auth_account.id).await.is_ok() { + if state + .repo + .likes + .by_note_and_account(note_id, auth_account.id) + .await + .is_ok() + { state.repo.likes.delete(note.id, auth_account.id).await?; } let response = ent::Status::from_model(&state, ¬e, &auth).await?; diff --git a/src/util/token.rs b/src/util/token.rs index e9099cf..d20bdbd 100644 --- a/src/util/token.rs +++ b/src/util/token.rs @@ -1,4 +1,4 @@ -use jsonwebtoken::{decode, encode, Algorithm, DecodingKey, EncodingKey, Header, Validation}; +use jsonwebtoken::{decode, encode, Algorithm, Header, Validation}; use serde::{Deserialize, Serialize}; use crate::core::*; @@ -36,6 +36,6 @@ pub async fn validate(state: &AppState, token: &str) -> Result { // tokens that are valid for longer than VALID_SECS are sus return Err(Error::BadCredentials); } - let account_id: Id = claims.sub.parse().expect("We issued an invalid token??"); + let account_id: IntId = claims.sub.parse().expect("We issued an invalid token??"); state.repo.accounts.by_id(account_id).await }