refactor model IDs to use safe types

This is a major refactor that introduces a new
generic type Id<T, I> for tagging any ID with the
model they reference without any runtime overhead.
From now on, it should be pretty much impossible
to pass e.g. a note ID to a function that expects,
say, an account ID.

Furthermore, the follow and like tables have been
simplified to use the two IDs that define the
relationship as a composite primary key.

Finally, likes now have a memory cache.
This commit is contained in:
anna 2023-01-20 16:45:12 +01:00
parent 7845fb3680
commit a0d2dc4151
Signed by: fef
GPG key ID: EC22E476DC2D3D84
37 changed files with 550 additions and 218 deletions

View file

@ -1,10 +1,9 @@
CREATE TABLE follows ( CREATE TABLE follows (
id BIGSERIAL PRIMARY KEY,
iri VARCHAR, iri VARCHAR,
follower_id BIGINT REFERENCES accounts (id) ON DELETE CASCADE, follower_id BIGINT REFERENCES accounts (id) ON DELETE CASCADE,
followee_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_iri ON follows(iri);
CREATE UNIQUE INDEX index_follows_on_follower_id_and_followee_id ON follows (follower_id, followee_id);

View file

@ -1,10 +1,9 @@
CREATE TABLE likes ( CREATE TABLE likes (
id BIGSERIAL PRIMARY KEY,
iri VARCHAR, iri VARCHAR,
note_id BIGINT REFERENCES notes (id) ON DELETE CASCADE, note_id BIGINT REFERENCES notes (id) ON DELETE CASCADE,
account_id BIGINT REFERENCES accounts (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_iri ON likes (iri);
CREATE UNIQUE INDEX index_likes_on_note_id_and_account_id ON likes (note_id, account_id);

198
src/core/id.rs Normal file
View file

@ -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<MyModel>;
///
/// pub struct MyModel {
/// pub id: MyId,
/// // ...
/// }
/// ```
pub struct Id<T, I = IntId> {
id: I,
_phantom: PhantomData<T>,
}
impl<T, I> Id<T, I> {
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<T, I> Clone for Id<T, I>
where
I: Clone,
{
fn clone(&self) -> Self {
Id {
id: self.id.clone(),
_phantom: PhantomData,
}
}
}
impl<T, I> Copy for Id<T, I> where I: Copy {}
impl<T, I> From<I> for Id<T, I> {
fn from(id: I) -> Self {
Id {
id,
_phantom: PhantomData,
}
}
}
impl<T, I> PartialEq for Id<T, I>
where
I: PartialEq,
{
fn eq(&self, other: &Self) -> bool {
self.id.eq(&other.id)
}
}
impl<T, I> Eq for Id<T, I> where I: Eq {}
impl<T, I> Hash for Id<T, I>
where
I: Hash,
{
fn hash<H>(&self, state: &mut H)
where
H: Hasher,
{
self.id.hash(state);
}
}
unsafe impl<T, I> Send for Id<T, I> where I: Send {}
unsafe impl<T, I> Sync for Id<T, I> where I: Sync {}
impl<'r, DB: Database, T, I> sqlx::Decode<'r, DB> for Id<T, I>
where
I: sqlx::Decode<'r, DB>,
{
fn decode(value: <DB as HasValueRef<'r>>::ValueRef) -> Result<Self, BoxDynError> {
let id = I::decode(value)?;
Ok(Id {
id,
_phantom: PhantomData,
})
}
}
impl<'q, DB: Database, T, I> sqlx::Encode<'q, DB> for Id<T, I>
where
I: sqlx::Encode<'q, DB>,
{
fn encode(self, buf: &mut <DB as HasArguments<'q>>::ArgumentBuffer) -> IsNull {
self.id.encode(buf)
}
fn encode_by_ref(&self, buf: &mut <DB as HasArguments<'q>>::ArgumentBuffer) -> IsNull {
self.id.encode_by_ref(buf)
}
fn produces(&self) -> Option<DB::TypeInfo> {
self.id.produces()
}
fn size_hint(&self) -> usize {
self.id.size_hint()
}
}
impl<DB: Database, T, I> sqlx::Type<DB> for Id<T, I>
where
I: sqlx::Type<DB>,
{
fn type_info() -> DB::TypeInfo {
I::type_info()
}
fn compatible(ty: &DB::TypeInfo) -> bool {
I::compatible(ty)
}
}
impl<T, I> serde::Serialize for Id<T, I>
where
I: serde::Serialize,
{
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
self.id.serialize(serializer)
}
}
impl<'de, T, I> serde::Deserialize<'de> for Id<T, I>
where
I: serde::Deserialize<'de>,
{
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let id = I::deserialize(deserializer)?;
Ok(Id {
id,
_phantom: PhantomData,
})
}
fn deserialize_in_place<D>(deserializer: D, place: &mut Self) -> Result<(), D::Error>
where
D: serde::Deserializer<'de>,
{
I::deserialize_in_place(deserializer, &mut place.id)
}
}
impl<T, I> fmt::Debug for Id<T, I>
where
I: fmt::Debug,
{
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{}#{:?}", type_name::<T>(), &self.id)
}
}
impl<T, I> fmt::Display for Id<T, I>
where
I: fmt::Display,
{
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
fmt::Display::fmt(&self.id, f)
}
}

View file

@ -7,8 +7,8 @@ pub use crate::util::validate::{Insane, Sane};
mod error; mod error;
pub use error::*; pub use error::*;
mod id;
pub type Id = i64; pub use id::*;
pub fn utc_now() -> NaiveDateTime { pub fn utc_now() -> NaiveDateTime {
Utc::now().naive_utc() Utc::now().naive_utc()

View file

@ -1,10 +1,8 @@
use crate::core::*;
use crate::data::memcache::{Indexable, MemCache}; use crate::data::memcache::{Indexable, MemCache};
use crate::model::Account; use crate::model::{Account, AccountId};
pub struct MemAccountDataSource { pub struct MemAccountDataSource {
cache: MemCache<Id, Account>, cache: MemCache<AccountId, Account>,
} }
impl MemAccountDataSource { impl MemAccountDataSource {
@ -14,7 +12,7 @@ impl MemAccountDataSource {
} }
} }
pub async fn by_id(&self, id: Id) -> Option<Account> { pub async fn by_id(&self, id: AccountId) -> Option<Account> {
self.cache.get(id).await self.cache.get(id).await
} }
@ -23,8 +21,8 @@ impl MemAccountDataSource {
} }
} }
impl Indexable<Id> for Account { impl Indexable<AccountId> for Account {
fn get_id(&self) -> Id { fn get_id(&self) -> AccountId {
self.id self.id
} }
} }

View file

@ -1,7 +1,7 @@
use sqlx::PgPool; use sqlx::PgPool;
use crate::core::*; use crate::core::*;
use crate::model::account::{Account, NewAccount}; use crate::model::{Account, AccountId, NewAccount};
pub struct PgAccountDataSource { pub struct PgAccountDataSource {
pool: PgPool, pool: PgPool,
@ -12,7 +12,7 @@ impl PgAccountDataSource {
PgAccountDataSource { pool } PgAccountDataSource { pool }
} }
pub async fn by_id(&self, id: Id) -> Result<Account> { pub async fn by_id(&self, id: AccountId) -> Result<Account> {
let account: Account = sqlx::query_as("SELECT * FROM accounts WHERE id = $1") let account: Account = sqlx::query_as("SELECT * FROM accounts WHERE id = $1")
.bind(id) .bind(id)
.fetch_one(&self.pool) .fetch_one(&self.pool)

View file

@ -2,7 +2,8 @@ use sqlx::PgPool;
use crate::core::*; use crate::core::*;
use crate::data::Count; use crate::data::Count;
use crate::model::follow::{Follow, NewFollow}; use crate::model::AccountId;
use crate::model::{Follow, FollowId, NewFollow};
pub struct PgFollowDataSource { pub struct PgFollowDataSource {
pool: PgPool, pool: PgPool,
@ -13,12 +14,10 @@ impl PgFollowDataSource {
PgFollowDataSource { pool } PgFollowDataSource { pool }
} }
pub async fn by_id(&self, id: Id) -> Result<Follow> { pub async fn by_id(&self, id: FollowId) -> Result<Follow> {
let follow: Follow = sqlx::query_as("SELECT * FROM follows WHERE id = $1") let (follower_id, followee_id) = id.into_inner();
.bind(id) self.by_follower_and_followee(follower_id, followee_id)
.fetch_one(&self.pool) .await
.await?;
Ok(follow)
} }
pub async fn create(&self, new: NewFollow) -> Result<Follow> { pub async fn create(&self, new: NewFollow) -> Result<Follow> {
@ -37,8 +36,8 @@ impl PgFollowDataSource {
pub async fn by_follower_and_followee( pub async fn by_follower_and_followee(
&self, &self,
follower_id: Id, follower_id: AccountId,
followee_id: Id, followee_id: AccountId,
) -> Result<Follow> { ) -> Result<Follow> {
let follow: Follow = let follow: Follow =
sqlx::query_as("SELECT * FROM follows WHERE follower_id = $1 AND followee_id = $2") sqlx::query_as("SELECT * FROM follows WHERE follower_id = $1 AND followee_id = $2")
@ -49,7 +48,7 @@ impl PgFollowDataSource {
Ok(follow) Ok(follow)
} }
pub async fn followees_of(&self, account_id: Id) -> Result<Vec<Follow>> { pub async fn followees_of(&self, account_id: AccountId) -> Result<Vec<Follow>> {
let followees: Vec<Follow> = sqlx::query_as("SELECT * FROM follows WHERE follower_id = $1") let followees: Vec<Follow> = sqlx::query_as("SELECT * FROM follows WHERE follower_id = $1")
.bind(account_id) .bind(account_id)
.fetch_all(&self.pool) .fetch_all(&self.pool)
@ -57,7 +56,7 @@ impl PgFollowDataSource {
Ok(followees) Ok(followees)
} }
pub async fn followers_of(&self, account_id: Id) -> Result<Vec<Follow>> { pub async fn followers_of(&self, account_id: AccountId) -> Result<Vec<Follow>> {
let followers: Vec<Follow> = sqlx::query_as("SELECT * FROM follows WHERE followee_id = $1") let followers: Vec<Follow> = sqlx::query_as("SELECT * FROM follows WHERE followee_id = $1")
.bind(account_id) .bind(account_id)
.fetch_all(&self.pool) .fetch_all(&self.pool)
@ -65,7 +64,7 @@ impl PgFollowDataSource {
Ok(followers) Ok(followers)
} }
pub async fn following_count_of(&self, account_id: Id) -> Result<u32> { pub async fn following_count_of(&self, account_id: AccountId) -> Result<u32> {
let followee_count: Count = let followee_count: Count =
sqlx::query_as("SELECT COUNT(*) AS count FROM follows WHERE follower_id = $1") sqlx::query_as("SELECT COUNT(*) AS count FROM follows WHERE follower_id = $1")
.bind(account_id) .bind(account_id)
@ -74,7 +73,7 @@ impl PgFollowDataSource {
Ok(followee_count.count as u32) Ok(followee_count.count as u32)
} }
pub async fn follower_count_of(&self, account_id: Id) -> Result<u32> { pub async fn follower_count_of(&self, account_id: AccountId) -> Result<u32> {
let follower_count: Count = let follower_count: Count =
sqlx::query_as("SELECT COUNT(*) AS count FROM follows WHERE followee_id = $1") sqlx::query_as("SELECT COUNT(*) AS count FROM follows WHERE followee_id = $1")
.bind(account_id) .bind(account_id)

72
src/data/like/mem.rs Normal file
View file

@ -0,0 +1,72 @@
use crate::data::memcache::{Indexable, MemCache};
use crate::model::{AccountId, Like, LikeId, NoteId};
pub struct MemLikeDataSource {
cache: MemCache<LikeId, Like>,
counts: MemCache<NoteId, LikeCount>,
}
#[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<Like> {
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<u64> {
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<LikeId> for Like {
fn get_id(&self) -> LikeId {
(self.note_id, self.account_id).into()
}
}
impl Indexable<NoteId> for LikeCount {
fn get_id(&self) -> NoteId {
self.note_id
}
}

View file

@ -1,3 +1,5 @@
pub mod pg; mod mem;
mod pg;
pub use mem::MemLikeDataSource;
pub use pg::PgLikeDataSource; pub use pg::PgLikeDataSource;

View file

@ -2,7 +2,7 @@ use sqlx::PgPool;
use crate::core::*; use crate::core::*;
use crate::data::Count; use crate::data::Count;
use crate::model::{Like, NewLike}; use crate::model::{AccountId, Like, NewLike, NoteId};
pub struct PgLikeDataSource { pub struct PgLikeDataSource {
pool: PgPool, pool: PgPool,
@ -13,14 +13,6 @@ impl PgLikeDataSource {
PgLikeDataSource { pool } PgLikeDataSource { pool }
} }
pub async fn by_id(&self, id: Id) -> Result<Like> {
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<Like> { pub async fn create(&self, new: NewLike) -> Result<Like> {
let like: Like = sqlx::query_as( let like: Like = sqlx::query_as(
"INSERT INTO likes (iri, note_id, account_id) "INSERT INTO likes (iri, note_id, account_id)
@ -35,7 +27,7 @@ impl PgLikeDataSource {
Ok(like) 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") sqlx::query("DELETE FROM likes WHERE note_id = $1 AND account_id = $2")
.bind(note_id) .bind(note_id)
.bind(account_id) .bind(account_id)
@ -44,7 +36,7 @@ impl PgLikeDataSource {
Ok(()) Ok(())
} }
pub async fn count_by_note(&self, note_id: Id) -> Result<u64> { pub async fn count_by_note(&self, note_id: NoteId) -> Result<u64> {
let result: Count = let result: Count =
sqlx::query_as("SELECT COUNT(*) as count FROM likes WHERE note_id = $1") sqlx::query_as("SELECT COUNT(*) as count FROM likes WHERE note_id = $1")
.bind(note_id) .bind(note_id)
@ -53,7 +45,11 @@ impl PgLikeDataSource {
Ok(result.count as u64) Ok(result.count as u64)
} }
pub async fn by_note_and_account(&self, note_id: Id, account_id: Id) -> Result<Like> { pub async fn by_note_and_account(
&self,
note_id: NoteId,
account_id: AccountId,
) -> Result<Like> {
let like: Like = let like: Like =
sqlx::query_as("SELECT * FROM likes WHERE note_id = $1 AND account_id = $2") sqlx::query_as("SELECT * FROM likes WHERE note_id = $1 AND account_id = $2")
.bind(note_id) .bind(note_id)

View file

@ -137,14 +137,16 @@ impl Hasher for Djb2 {
mod tests { mod tests {
use super::*; use super::*;
type TestId = Id<TestEntry>;
#[derive(Clone, Debug, PartialEq)] #[derive(Clone, Debug, PartialEq)]
struct TestEntry { struct TestEntry {
id: Id, id: TestId,
data: i32, data: i32,
} }
impl Indexable<Id> for TestEntry { impl Indexable<TestId> for TestEntry {
fn get_id(&self) -> Id { fn get_id(&self) -> TestId {
self.id self.id
} }
} }
@ -152,14 +154,16 @@ mod tests {
#[actix_web::test] #[actix_web::test]
async fn store_stuff() { async fn store_stuff() {
let cache = MemCache::with_capacity(256); 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 }; let entry = TestEntry { id, data: 0 };
cache.put(entry.clone()).await; cache.put(entry.clone()).await;
assert_eq!(cache.get(id).await, Some(entry)); assert_eq!(cache.get(id).await, Some(entry));
} }
let mut had_entries = false; 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; let entry = cache.del(id).await;
assert_eq!(cache.get(id).await, None); assert_eq!(cache.get(id).await, None);
if let Some(entry) = entry { if let Some(entry) = entry {
@ -172,19 +176,19 @@ mod tests {
#[actix_web::test] #[actix_web::test]
async fn update_stuff() { async fn update_stuff() {
const ID: Id = 420; let id = 420.into();
let cache = MemCache::with_capacity(256); let cache = MemCache::with_capacity(256);
cache.put(TestEntry { id: ID, data: 69 }).await; cache.put(TestEntry { id, data: 69 }).await;
cache.update(ID, |entry| entry.data = 1312).await; cache.update(id, |entry| entry.data = 1312).await;
assert_eq!(cache.get(ID).await, Some(TestEntry { id: ID, data: 1312 })); assert_eq!(cache.get(id).await, Some(TestEntry { id, data: 1312 }));
} }
#[actix_web::test] #[actix_web::test]
#[should_panic] #[should_panic]
async fn mutate_id() { async fn mutate_id() {
const ID: Id = 420; let id = 420.into();
let cache = MemCache::with_capacity(256); let cache = MemCache::with_capacity(256);
cache.put(TestEntry { id: ID, data: 69 }).await; cache.put(TestEntry { id, data: 69 }).await;
cache.update(ID, |entry| entry.id = 1312).await; cache.update(id, |entry| entry.id = 1312.into()).await;
} }
} }

View file

@ -1,12 +1,12 @@
/// Small support utility for in-memory cache. /// Small support utility for in-memory cache.
pub mod memcache; pub mod memcache;
pub mod account; mod account;
pub mod follow; mod follow;
pub mod like; mod like;
pub mod note; mod note;
pub mod object; mod object;
pub mod user; mod user;
pub use account::*; pub use account::*;
pub use follow::*; pub use follow::*;

View file

@ -1,10 +1,8 @@
use crate::core::*;
use crate::data::memcache::{Indexable, MemCache}; use crate::data::memcache::{Indexable, MemCache};
use crate::model::Note; use crate::model::{Note, NoteId};
pub struct MemNoteDataSource { pub struct MemNoteDataSource {
cache: MemCache<Id, Note>, cache: MemCache<NoteId, Note>,
} }
impl MemNoteDataSource { impl MemNoteDataSource {
@ -14,7 +12,7 @@ impl MemNoteDataSource {
} }
} }
pub async fn by_id(&self, id: Id) -> Option<Note> { pub async fn by_id(&self, id: NoteId) -> Option<Note> {
self.cache.get(id).await self.cache.get(id).await
} }
@ -22,13 +20,13 @@ impl MemNoteDataSource {
self.cache.put(note).await self.cache.put(note).await
} }
pub async fn delete(&self, id: Id) { pub async fn delete(&self, id: NoteId) {
self.cache.del(id).await; self.cache.del(id).await;
} }
} }
impl Indexable<Id> for Note { impl Indexable<NoteId> for Note {
fn get_id(&self) -> Id { fn get_id(&self) -> NoteId {
self.id self.id
} }
} }

View file

@ -3,7 +3,7 @@ use sqlx::PgPool;
use crate::core::*; use crate::core::*;
use crate::data::Count; use crate::data::Count;
use crate::model::note::{NewNote, Note}; use crate::model::{AccountId, NewNote, Note, NoteId};
pub struct PgNoteDataSource { pub struct PgNoteDataSource {
pool: PgPool, pool: PgPool,
@ -14,7 +14,7 @@ impl PgNoteDataSource {
PgNoteDataSource { pool } PgNoteDataSource { pool }
} }
pub async fn by_id(&self, id: Id) -> Result<Note> { pub async fn by_id(&self, id: NoteId) -> Result<Note> {
let note: Note = sqlx::query_as("SELECT * FROM notes WHERE id = $1") let note: Note = sqlx::query_as("SELECT * FROM notes WHERE id = $1")
.bind(id) .bind(id)
.fetch_one(&self.pool) .fetch_one(&self.pool)
@ -22,7 +22,7 @@ impl PgNoteDataSource {
Ok(note) Ok(note)
} }
pub async fn by_account(&self, id: Id, since: NaiveDateTime) -> Result<Vec<Note>> { pub async fn by_account(&self, id: AccountId, since: NaiveDateTime) -> Result<Vec<Note>> {
let notes: Vec<Note> = sqlx::query_as( let notes: Vec<Note> = sqlx::query_as(
"SELECT * FROM notes "SELECT * FROM notes
WHERE account_id = $1 AND created_at < $1 WHERE account_id = $1 AND created_at < $1
@ -61,7 +61,7 @@ impl PgNoteDataSource {
Ok(note) 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") sqlx::query("DELETE FROM notes WHERE id = $1")
.bind(id) .bind(id)
.execute(&self.pool) .execute(&self.pool)
@ -69,7 +69,7 @@ impl PgNoteDataSource {
Ok(()) Ok(())
} }
pub async fn count_by_account(&self, account_id: Id) -> Result<u32> { pub async fn count_by_account(&self, account_id: AccountId) -> Result<u32> {
let count: Count = let count: Count =
sqlx::query_as("SELECT COUNT(*) AS count FROM notes WHERE account_id = $1") sqlx::query_as("SELECT COUNT(*) AS count FROM notes WHERE account_id = $1")
.bind(account_id) .bind(account_id)

View file

@ -1,10 +1,8 @@
use uuid::Uuid;
use crate::data::memcache::{Indexable, MemCache}; use crate::data::memcache::{Indexable, MemCache};
use crate::model::Object; use crate::model::{Object, ObjectId};
pub struct MemObjectDataSource { pub struct MemObjectDataSource {
cache: MemCache<Uuid, Object>, cache: MemCache<ObjectId, Object>,
} }
impl MemObjectDataSource { impl MemObjectDataSource {
@ -14,7 +12,7 @@ impl MemObjectDataSource {
} }
} }
pub async fn by_id(&self, id: Uuid) -> Option<Object> { pub async fn by_id(&self, id: ObjectId) -> Option<Object> {
self.cache.get(id).await self.cache.get(id).await
} }
@ -23,8 +21,8 @@ impl MemObjectDataSource {
} }
} }
impl Indexable<Uuid> for Object { impl Indexable<ObjectId> for Object {
fn get_id(&self) -> Uuid { fn get_id(&self) -> ObjectId {
self.id self.id
} }
} }

View file

@ -1,8 +1,7 @@
use sqlx::types::Uuid;
use sqlx::PgPool; use sqlx::PgPool;
use crate::core::*; use crate::core::*;
use crate::model::object::*; use crate::model::{NewObject, Object, ObjectId};
pub struct PgObjectDataSource { pub struct PgObjectDataSource {
pool: PgPool, pool: PgPool,
@ -37,7 +36,7 @@ impl PgObjectDataSource {
Ok(()) Ok(())
} }
pub async fn by_id(&self, id: Uuid) -> Result<Object> { pub async fn by_id(&self, id: ObjectId) -> Result<Object> {
let object: Object = sqlx::query_as("SELECT * FROM objects WHERE id = $1") let object: Object = sqlx::query_as("SELECT * FROM objects WHERE id = $1")
.bind(id) .bind(id)
.fetch_one(&self.pool) .fetch_one(&self.pool)

View file

@ -1,10 +1,8 @@
use crate::core::*;
use crate::data::memcache::{Indexable, MemCache}; use crate::data::memcache::{Indexable, MemCache};
use crate::model::User; use crate::model::{User, UserId};
pub struct MemUserDataSource { pub struct MemUserDataSource {
cache: MemCache<Id, User>, cache: MemCache<UserId, User>,
} }
impl MemUserDataSource { impl MemUserDataSource {
@ -14,7 +12,7 @@ impl MemUserDataSource {
} }
} }
pub async fn by_id(&self, id: Id) -> Option<User> { pub async fn by_id(&self, id: UserId) -> Option<User> {
self.cache.get(id).await self.cache.get(id).await
} }
@ -23,8 +21,8 @@ impl MemUserDataSource {
} }
} }
impl Indexable<Id> for User { impl Indexable<UserId> for User {
fn get_id(&self) -> Id { fn get_id(&self) -> UserId {
self.id self.id
} }
} }

View file

@ -2,7 +2,7 @@ use serde;
use sqlx::{Executor, PgPool}; use sqlx::{Executor, PgPool};
use crate::core::*; use crate::core::*;
use crate::model::user::{NewUser, User}; use crate::model::{AccountId, NewUser, User, UserId};
pub struct PgUserDataSource { pub struct PgUserDataSource {
pool: PgPool, pool: PgPool,
@ -13,7 +13,7 @@ impl PgUserDataSource {
PgUserDataSource { pool } PgUserDataSource { pool }
} }
pub async fn by_id(&self, id: Id) -> Result<User> { pub async fn by_id(&self, id: UserId) -> Result<User> {
let user: User = sqlx::query_as("SELECT * FROM users WHERE id = $1") let user: User = sqlx::query_as("SELECT * FROM users WHERE id = $1")
.bind(id) .bind(id)
.fetch_one(&self.pool) .fetch_one(&self.pool)
@ -21,7 +21,7 @@ impl PgUserDataSource {
Ok(user) Ok(user)
} }
pub async fn by_account(&self, account_id: Id) -> Result<User> { pub async fn by_account(&self, account_id: AccountId) -> Result<User> {
let user: User = sqlx::query_as("SELECT * FROM users WHERE account_id = $1") let user: User = sqlx::query_as("SELECT * FROM users WHERE account_id = $1")
.bind(account_id) .bind(account_id)
.fetch_one(&self.pool) .fetch_one(&self.pool)

View file

@ -29,7 +29,7 @@ impl Account {
let following_count = state.repo.follows.following_count_of(model.id).await?; let following_count = state.repo.follows.following_count_of(model.id).await?;
let statuses_count = state.repo.notes.count_by_account(model.id).await?; let statuses_count = state.repo.notes.count_by_account(model.id).await?;
Ok(Account { Ok(Account {
id: format!("{}", model.id), id: model.id.to_string(),
username: model.name.clone(), username: model.name.clone(),
display_name: model.display_name.as_ref().unwrap_or(&model.name).clone(), display_name: model.display_name.as_ref().unwrap_or(&model.name).clone(),
created_at: DateTime::from_utc(model.created_at, Utc), created_at: DateTime::from_utc(model.created_at, Utc),

View file

@ -39,7 +39,7 @@ impl Status {
}; };
let author = state.repo.accounts.by_id(model.account_id).await?; let author = state.repo.accounts.by_id(model.account_id).await?;
Ok(Status { Ok(Status {
id: format!("{}", model.id), id: model.id.to_string(),
created_at: DateTime::from_utc(model.created_at, Utc), created_at: DateTime::from_utc(model.created_at, Utc),
sensitive: model.sensitive, sensitive: model.sensitive,
spoiler_text: model.summary.clone().unwrap_or_default(), spoiler_text: model.summary.clone().unwrap_or_default(),

View file

@ -2,12 +2,14 @@ use chrono::prelude::*;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use sqlx::FromRow; use sqlx::FromRow;
use crate::core::Id; use crate::core::*;
use crate::util::validate::{ResultBuilder, Validate}; use crate::util::validate::{ResultBuilder, Validate};
pub type AccountId = Id<Account>;
#[derive(Clone, Deserialize, Serialize, FromRow)] #[derive(Clone, Deserialize, Serialize, FromRow)]
pub struct Account { pub struct Account {
pub id: Id, pub id: AccountId,
pub iri: Option<String>, pub iri: Option<String>,
pub name: String, pub name: String,
pub domain: String, pub domain: String,
@ -23,8 +25,8 @@ pub struct NewAccount {
pub display_name: Option<String>, pub display_name: Option<String>,
} }
impl From<Account> for Id { impl From<Account> for AccountId {
fn from(account: Account) -> Id { fn from(account: Account) -> AccountId {
account.id account.id
} }
} }
@ -33,7 +35,7 @@ impl Validate for NewAccount {
fn validate(&self, builder: ResultBuilder) -> ResultBuilder { fn validate(&self, builder: ResultBuilder) -> ResultBuilder {
builder builder
.field(&self.name, "name", |f| { .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) .check("Must be at most 16 characters long", |v| v.len() <= 16)
}) })
.field(&self.display_name, "display_name", |f| { .field(&self.display_name, "display_name", |f| {

View file

@ -3,26 +3,28 @@ use serde::{Deserialize, Serialize};
use sqlx::FromRow; use sqlx::FromRow;
use crate::core::*; use crate::core::*;
use crate::model::account::AccountId;
use crate::util::validate::{ResultBuilder, Validate}; use crate::util::validate::{ResultBuilder, Validate};
pub type FollowId = Id<Follow, (AccountId, AccountId)>;
#[derive(Deserialize, Serialize, FromRow)] #[derive(Deserialize, Serialize, FromRow)]
pub struct Follow { pub struct Follow {
pub id: Id,
pub iri: Option<String>, pub iri: Option<String>,
pub follower_id: Id, pub follower_id: AccountId,
pub followee_id: Id, pub followee_id: AccountId,
pub created_at: NaiveDateTime, pub created_at: NaiveDateTime,
} }
pub struct NewFollow { pub struct NewFollow {
pub iri: Option<String>, pub iri: Option<String>,
pub follower_id: Id, pub follower_id: AccountId,
pub followee_id: Id, pub followee_id: AccountId,
} }
impl From<Follow> for Id { impl From<Follow> for FollowId {
fn from(follow: Follow) -> Id { fn from(follow: Follow) -> FollowId {
follow.id (follow.follower_id, follow.followee_id).into()
} }
} }

View file

@ -3,26 +3,28 @@ use serde::{Deserialize, Serialize};
use sqlx::FromRow; use sqlx::FromRow;
use crate::core::*; use crate::core::*;
use crate::model::{AccountId, NoteId};
use crate::util::validate::{ResultBuilder, Validate}; use crate::util::validate::{ResultBuilder, Validate};
#[derive(Deserialize, Serialize, FromRow)] pub type LikeId = Id<Like, (NoteId, AccountId)>;
#[derive(Clone, Deserialize, Serialize, FromRow)]
pub struct Like { pub struct Like {
pub id: Id,
pub iri: Option<String>, pub iri: Option<String>,
pub note_id: Id, pub note_id: NoteId,
pub account_id: Id, pub account_id: AccountId,
pub created_at: NaiveDateTime, pub created_at: NaiveDateTime,
} }
pub struct NewLike { pub struct NewLike {
pub iri: Option<String>, pub iri: Option<String>,
pub note_id: Id, pub note_id: NoteId,
pub account_id: Id, pub account_id: AccountId,
} }
impl From<Like> for Id { impl From<Like> for LikeId {
fn from(like: Like) -> Id { fn from(val: Like) -> LikeId {
like.id (val.note_id, val.account_id).into()
} }
} }

View file

@ -1,13 +1,13 @@
pub mod account; mod account;
pub mod follow; mod follow;
pub mod like; mod like;
pub mod note; mod note;
pub mod object; mod object;
pub mod user; mod user;
pub use account::{Account, NewAccount}; pub use account::*;
pub use follow::{Follow, NewFollow}; pub use follow::*;
pub use like::{Like, NewLike}; pub use like::*;
pub use note::{NewNote, Note}; pub use note::*;
pub use object::{NewObject, Object}; pub use object::*;
pub use user::{NewUser, User}; pub use user::*;

View file

@ -3,12 +3,15 @@ use serde::{Deserialize, Serialize};
use sqlx::FromRow; use sqlx::FromRow;
use crate::core::*; use crate::core::*;
use crate::model::AccountId;
use crate::util::validate::{ResultBuilder, Validate}; use crate::util::validate::{ResultBuilder, Validate};
pub type NoteId = Id<Note>;
#[derive(Clone, Deserialize, Serialize, FromRow)] #[derive(Clone, Deserialize, Serialize, FromRow)]
pub struct Note { pub struct Note {
pub id: Id, pub id: NoteId,
pub account_id: Id, pub account_id: AccountId,
pub iri: Option<String>, pub iri: Option<String>,
pub content: String, pub content: String,
pub source: Option<String>, pub source: Option<String>,
@ -20,7 +23,7 @@ pub struct Note {
} }
pub struct NewNote { pub struct NewNote {
pub account_id: Id, pub account_id: AccountId,
pub iri: Option<String>, pub iri: Option<String>,
pub content: String, pub content: String,
pub source: Option<String>, pub source: Option<String>,
@ -29,9 +32,9 @@ pub struct NewNote {
pub sensitive: bool, pub sensitive: bool,
} }
impl From<Note> for Id { impl From<Note> for NoteId {
fn from(status: Note) -> Id { fn from(val: Note) -> NoteId {
status.id val.id
} }
} }

View file

@ -2,9 +2,13 @@ use chrono::NaiveDateTime;
use sqlx::types::Uuid; use sqlx::types::Uuid;
use sqlx::FromRow; use sqlx::FromRow;
use crate::core::*;
pub type ObjectId = Id<Object, Uuid>;
#[derive(Clone, FromRow)] #[derive(Clone, FromRow)]
pub struct Object { pub struct Object {
pub id: Uuid, pub id: ObjectId,
pub iri: String, pub iri: String,
pub data: String, pub data: String,
pub created_at: NaiveDateTime, pub created_at: NaiveDateTime,
@ -12,7 +16,13 @@ pub struct Object {
} }
pub struct NewObject { pub struct NewObject {
pub id: Uuid, pub id: ObjectId,
pub iri: String, pub iri: String,
pub data: String, pub data: String,
} }
impl From<Object> for ObjectId {
fn from(val: Object) -> ObjectId {
val.id
}
}

View file

@ -2,12 +2,15 @@ use serde::{Deserialize, Serialize};
use sqlx::FromRow; use sqlx::FromRow;
use crate::core::*; use crate::core::*;
use crate::model::AccountId;
use crate::util::validate::{ResultBuilder, Validate}; use crate::util::validate::{ResultBuilder, Validate};
pub type UserId = Id<User>;
#[derive(Clone, Deserialize, Serialize, FromRow)] #[derive(Clone, Deserialize, Serialize, FromRow)]
pub struct User { pub struct User {
pub id: Id, pub id: UserId,
pub account_id: Id, pub account_id: AccountId,
pub email: String, pub email: String,
pub password: String, pub password: String,
pub reason: Option<String>, pub reason: Option<String>,
@ -16,15 +19,15 @@ pub struct User {
} }
pub struct NewUser { pub struct NewUser {
pub account_id: Id, pub account_id: AccountId,
pub email: String, pub email: String,
pub password: String, pub password: String,
pub reason: Option<String>, pub reason: Option<String>,
pub locale: String, pub locale: String,
} }
impl From<User> for Id { impl From<User> for UserId {
fn from(user: User) -> Id { fn from(user: User) -> UserId {
user.id user.id
} }
} }
@ -32,7 +35,7 @@ impl From<User> for Id {
impl Validate for NewUser { impl Validate for NewUser {
fn validate(&self, builder: ResultBuilder) -> ResultBuilder { fn validate(&self, builder: ResultBuilder) -> ResultBuilder {
builder.field(&self.email, "email", |f| { 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())
}) })
} }
} }

View file

@ -1,8 +1,8 @@
use sqlx::PgPool; use sqlx::PgPool;
use crate::core::*; use crate::core::*;
use crate::data::account::{MemAccountDataSource, PgAccountDataSource}; use crate::data::{MemAccountDataSource, PgAccountDataSource};
use crate::model::account::{Account, NewAccount}; use crate::model::{Account, AccountId, NewAccount};
pub struct AccountRepo { pub struct AccountRepo {
db: PgAccountDataSource, db: PgAccountDataSource,
@ -19,7 +19,7 @@ impl AccountRepo {
pub async fn by_id<I>(&self, id: I) -> Result<Account> pub async fn by_id<I>(&self, id: I) -> Result<Account>
where where
I: Into<Id> + Send, I: Into<AccountId> + Send,
{ {
let id = id.into(); let id = id.into();
match self.mem.by_id(id).await { match self.mem.by_id(id).await {

View file

@ -1,8 +1,8 @@
use sqlx::PgPool; use sqlx::PgPool;
use crate::core::*; use crate::core::*;
use crate::data::follow::PgFollowDataSource; use crate::data::PgFollowDataSource;
use crate::model::follow::{Follow, NewFollow}; use crate::model::{AccountId, Follow, FollowId, NewFollow};
pub struct FollowRepo { pub struct FollowRepo {
db: PgFollowDataSource, db: PgFollowDataSource,
@ -17,7 +17,7 @@ impl FollowRepo {
pub async fn by_id<I>(&self, id: I) -> Result<Follow> pub async fn by_id<I>(&self, id: I) -> Result<Follow>
where where
I: Into<Id> + Send, I: Into<FollowId> + Send,
{ {
self.db.by_id(id.into()).await self.db.by_id(id.into()).await
} }
@ -35,8 +35,8 @@ impl FollowRepo {
followee_id: J, followee_id: J,
) -> Result<Follow> ) -> Result<Follow>
where where
I: Into<Id> + Send, I: Into<AccountId> + Send,
J: Into<Id> + Send, J: Into<AccountId> + Send,
{ {
let (follower_id, followee_id) = (follower_id.into(), followee_id.into()); let (follower_id, followee_id) = (follower_id.into(), followee_id.into());
self.db self.db
@ -46,8 +46,8 @@ impl FollowRepo {
pub async fn follows<I, J>(&self, follower_id: I, followee_id: J) -> Result<bool> pub async fn follows<I, J>(&self, follower_id: I, followee_id: J) -> Result<bool>
where where
I: Into<Id> + Send, I: Into<AccountId> + Send,
J: Into<Id> + Send, J: Into<AccountId> + Send,
{ {
match self match self
.by_follower_and_followee(follower_id, followee_id) .by_follower_and_followee(follower_id, followee_id)
@ -61,28 +61,28 @@ impl FollowRepo {
pub async fn followees_of<I>(&self, account_id: I) -> Result<Vec<Follow>> pub async fn followees_of<I>(&self, account_id: I) -> Result<Vec<Follow>>
where where
I: Into<Id> + Send, I: Into<AccountId> + Send,
{ {
self.db.followees_of(account_id.into()).await self.db.followees_of(account_id.into()).await
} }
pub async fn followers_of<I>(&self, account_id: I) -> Result<Vec<Follow>> pub async fn followers_of<I>(&self, account_id: I) -> Result<Vec<Follow>>
where where
I: Into<Id> + Send, I: Into<AccountId> + Send,
{ {
self.db.followers_of(account_id.into()).await self.db.followers_of(account_id.into()).await
} }
pub async fn following_count_of<I>(&self, account_id: I) -> Result<u32> pub async fn following_count_of<I>(&self, account_id: I) -> Result<u32>
where where
I: Into<Id> + Send, I: Into<AccountId> + Send,
{ {
self.db.following_count_of(account_id.into()).await self.db.following_count_of(account_id.into()).await
} }
pub async fn follower_count_of<I>(&self, account_id: I) -> Result<u32> pub async fn follower_count_of<I>(&self, account_id: I) -> Result<u32>
where where
I: Into<Id> + Send, I: Into<AccountId> + Send,
{ {
self.db.follower_count_of(account_id.into()).await self.db.follower_count_of(account_id.into()).await
} }

View file

@ -1,57 +1,72 @@
use sqlx::PgPool; use sqlx::PgPool;
use crate::core::*; use crate::core::*;
use crate::data::PgLikeDataSource; use crate::data::{MemLikeDataSource, PgLikeDataSource};
use crate::model::{Like, NewLike}; use crate::model::{AccountId, Like, NewLike, NoteId};
pub struct LikeRepo { pub struct LikeRepo {
db: PgLikeDataSource, db: PgLikeDataSource,
mem: MemLikeDataSource,
} }
impl LikeRepo { impl LikeRepo {
pub fn new(db_pool: PgPool) -> LikeRepo { pub fn new(db_pool: PgPool) -> LikeRepo {
LikeRepo { LikeRepo {
db: PgLikeDataSource::new(db_pool), db: PgLikeDataSource::new(db_pool),
mem: MemLikeDataSource::new(),
} }
} }
pub async fn by_id<I>(&self, id: I) -> Result<Like>
where
I: Into<Id> + Send,
{
self.db.by_id(id.into()).await
}
pub async fn create<T, E>(&self, new: T) -> Result<Like> pub async fn create<T, E>(&self, new: T) -> Result<Like>
where where
T: TryInto<Sane<NewLike>, Error = E> + Send, T: TryInto<Sane<NewLike>, Error = E> + Send,
E: Into<Error>, E: Into<Error>,
{ {
let new = new.try_into().map_err(|e| e.into())?.inner(); 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<N, A>(&self, note_id: N, account_id: A) -> Result<()> pub async fn delete<N, A>(&self, note_id: N, account_id: A) -> Result<()>
where where
N: Into<Id>, N: Into<NoteId>,
A: Into<Id>, A: Into<AccountId>,
{ {
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<I>(&self, note_id: I) -> Result<u64> pub async fn count_by_note<I>(&self, note_id: I) -> Result<u64>
where where
I: Into<Id> + Send, I: Into<NoteId> + 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<N, A>(&self, note_id: N, account_id: A) -> Result<Like> pub async fn by_note_and_account<N, A>(&self, note_id: N, account_id: A) -> Result<Like>
where where
N: Into<Id>, N: Into<NoteId>,
A: Into<Id>, A: Into<AccountId>,
{ {
let (note_id, account_id) = (note_id.into(), account_id.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)
} }
} }

View file

@ -1,18 +1,18 @@
use sqlx::PgPool; use sqlx::PgPool;
pub mod account; mod account;
pub mod follow; mod follow;
pub mod like; mod like;
pub mod note; mod note;
pub mod object; mod object;
pub mod user; mod user;
use account::AccountRepo; pub use account::AccountRepo;
use follow::FollowRepo; pub use follow::FollowRepo;
use like::LikeRepo; pub use like::LikeRepo;
use note::NoteRepo; pub use note::NoteRepo;
use object::ObjectRepo; pub use object::ObjectRepo;
use user::UserRepo; pub use user::UserRepo;
/// The central collection of all data accessible to the app. /// The central collection of all data accessible to the app.
/// This is included in `AppState` so it is accessible everywhere. /// This is included in `AppState` so it is accessible everywhere.

View file

@ -3,7 +3,7 @@ use sqlx::PgPool;
use crate::core::*; use crate::core::*;
use crate::data::{MemNoteDataSource, PgNoteDataSource}; use crate::data::{MemNoteDataSource, PgNoteDataSource};
use crate::model::{NewNote, Note}; use crate::model::{AccountId, NewNote, Note, NoteId};
pub struct NoteRepo { pub struct NoteRepo {
db: PgNoteDataSource, db: PgNoteDataSource,
@ -20,7 +20,7 @@ impl NoteRepo {
pub async fn by_id<I>(&self, id: I) -> Result<Note> pub async fn by_id<I>(&self, id: I) -> Result<Note>
where where
I: Into<Id> + Send, I: Into<NoteId> + Send,
{ {
let id = id.into(); let id = id.into();
match self.mem.by_id(id).await { match self.mem.by_id(id).await {
@ -31,7 +31,7 @@ impl NoteRepo {
pub async fn by_account<I>(&self, account_id: I, since: NaiveDateTime) -> Result<Vec<Note>> pub async fn by_account<I>(&self, account_id: I, since: NaiveDateTime) -> Result<Vec<Note>>
where where
I: Into<Id> + Send, I: Into<AccountId> + Send,
{ {
self.db.by_account(account_id.into(), since).await self.db.by_account(account_id.into(), since).await
} }
@ -49,7 +49,7 @@ impl NoteRepo {
pub async fn delete<I>(&self, note_id: I) -> Result<()> pub async fn delete<I>(&self, note_id: I) -> Result<()>
where where
I: Into<Id>, I: Into<NoteId>,
{ {
let note_id = note_id.into(); let note_id = note_id.into();
self.db.delete(note_id).await?; self.db.delete(note_id).await?;
@ -59,7 +59,7 @@ impl NoteRepo {
pub async fn count_by_account<I>(&self, account_id: I) -> Result<u32> pub async fn count_by_account<I>(&self, account_id: I) -> Result<u32>
where where
I: Into<Id> + Send, I: Into<AccountId> + Send,
{ {
self.db.count_by_account(account_id.into()).await self.db.count_by_account(account_id.into()).await
} }

View file

@ -1,9 +1,8 @@
use sqlx::types::Uuid;
use sqlx::PgPool; use sqlx::PgPool;
use crate::core::*; use crate::core::*;
use crate::data::object::{MemObjectDataSource, PgObjectDataSource}; use crate::data::{MemObjectDataSource, PgObjectDataSource};
use crate::model::{NewObject, Object}; use crate::model::{NewObject, Object, ObjectId};
pub struct ObjectRepo { pub struct ObjectRepo {
db: PgObjectDataSource, db: PgObjectDataSource,
@ -37,7 +36,7 @@ impl ObjectRepo {
pub async fn by_id<U>(&self, id: U) -> Result<Object> pub async fn by_id<U>(&self, id: U) -> Result<Object>
where where
U: Into<Uuid>, U: Into<ObjectId>,
{ {
let id = id.into(); let id = id.into();
match self.mem.by_id(id).await { match self.mem.by_id(id).await {

View file

@ -1,8 +1,8 @@
use sqlx::PgPool; use sqlx::PgPool;
use crate::core::*; use crate::core::*;
use crate::data::user::{MemUserDataSource, PgUserDataSource}; use crate::data::{MemUserDataSource, PgUserDataSource};
use crate::model::user::{NewUser, User}; use crate::model::{AccountId, NewUser, User, UserId};
pub struct UserRepo { pub struct UserRepo {
db: PgUserDataSource, db: PgUserDataSource,
@ -19,7 +19,7 @@ impl UserRepo {
pub async fn by_id<I>(&self, id: I) -> Result<User> pub async fn by_id<I>(&self, id: I) -> Result<User>
where where
I: Into<Id> + Send, I: Into<UserId> + Send,
{ {
let id = id.into(); let id = id.into();
match self.mem.by_id(id).await { match self.mem.by_id(id).await {
@ -30,7 +30,7 @@ impl UserRepo {
pub async fn by_account<I>(&self, account_id: I) -> Result<User> pub async fn by_account<I>(&self, account_id: I) -> Result<User>
where where
I: Into<Id> + Send, I: Into<AccountId> + Send,
{ {
self.db.by_account(account_id.into()).await self.db.by_account(account_id.into()).await
} }

View file

@ -4,7 +4,7 @@ use serde::Deserialize;
use crate::core::*; use crate::core::*;
use crate::ent; use crate::ent;
use crate::middle::AuthData; use crate::middle::AuthData;
use crate::model::{NewAccount, NewUser}; use crate::model::{AccountId, NewAccount, NewUser};
use crate::state::AppState; use crate::state::AppState;
use crate::util::password; use crate::util::password;
use crate::util::validate::{ResultBuilder, Validate}; use crate::util::validate::{ResultBuilder, Validate};
@ -70,7 +70,11 @@ async fn verify_credentials(auth: AuthData, state: AppState) -> Result<HttpRespo
} }
#[get("/{id}")] #[get("/{id}")]
async fn get_by_id(auth: AuthData, path: web::Path<Id>, state: AppState) -> Result<HttpResponse> { async fn get_by_id(
auth: AuthData,
path: web::Path<AccountId>,
state: AppState,
) -> Result<HttpResponse> {
let id = path.into_inner(); let id = path.into_inner();
if let Some(auth) = auth.maybe() { if let Some(auth) = auth.maybe() {
if auth.id == id { if auth.id == id {
@ -85,7 +89,7 @@ async fn get_by_id(auth: AuthData, path: web::Path<Id>, state: AppState) -> Resu
} }
#[get("/{id}/statuses")] #[get("/{id}/statuses")]
async fn get_notes(path: web::Path<Id>, state: AppState) -> Result<HttpResponse> { async fn get_notes(path: web::Path<AccountId>, state: AppState) -> Result<HttpResponse> {
let id = path.into_inner(); let id = path.into_inner();
let account = state.repo.accounts.by_id(id).await?; let account = state.repo.accounts.by_id(id).await?;
let notes = state.repo.notes.by_account(account, utc_now()).await?; let notes = state.repo.notes.by_account(account, utc_now()).await?;

View file

@ -4,7 +4,7 @@ use serde::Deserialize;
use crate::core::*; use crate::core::*;
use crate::ent; use crate::ent;
use crate::middle::AuthData; use crate::middle::AuthData;
use crate::model::{NewLike, NewNote}; use crate::model::{NewLike, NewNote, NoteId};
use crate::state::AppState; use crate::state::AppState;
#[derive(Deserialize)] #[derive(Deserialize)]
@ -37,14 +37,22 @@ async fn create_note(
} }
#[get("/{id}")] #[get("/{id}")]
async fn get_by_id(path: web::Path<Id>, state: AppState, auth: AuthData) -> Result<HttpResponse> { async fn get_by_id(
path: web::Path<NoteId>,
state: AppState,
auth: AuthData,
) -> Result<HttpResponse> {
let id = path.into_inner(); let id = path.into_inner();
let note = state.repo.notes.by_id(id).await?; let note = state.repo.notes.by_id(id).await?;
Ok(HttpResponse::Ok().json(ent::Status::from_model(&state, &note, &auth).await?)) Ok(HttpResponse::Ok().json(ent::Status::from_model(&state, &note, &auth).await?))
} }
#[delete("/{id}")] #[delete("/{id}")]
async fn del_by_id(path: web::Path<Id>, state: AppState, auth: AuthData) -> Result<HttpResponse> { async fn del_by_id(
path: web::Path<NoteId>,
state: AppState,
auth: AuthData,
) -> Result<HttpResponse> {
let id = path.into_inner(); let id = path.into_inner();
let auth_account = auth.require()?; let auth_account = auth.require()?;
let note = state.repo.notes.by_id(id).await?; let note = state.repo.notes.by_id(id).await?;
@ -57,27 +65,51 @@ async fn del_by_id(path: web::Path<Id>, state: AppState, auth: AuthData) -> Resu
} }
#[post("/{id}/favourite")] #[post("/{id}/favourite")]
async fn add_favourite(path: web::Path<Id>, state: AppState, auth: AuthData) -> Result<HttpResponse> { async fn add_favourite(
path: web::Path<NoteId>,
state: AppState,
auth: AuthData,
) -> Result<HttpResponse> {
let note_id = path.into_inner(); let note_id = path.into_inner();
let auth_account = auth.require()?; let auth_account = auth.require()?;
let note = state.repo.notes.by_id(note_id).await?; 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() { if state
state.repo.likes.create(Insane::from(NewLike { .repo
iri: None, .likes
note_id: note.id, .by_note_and_account(note_id, auth_account.id)
account_id: auth_account.id, .await
})).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, &note, &auth).await?; let response = ent::Status::from_model(&state, &note, &auth).await?;
Ok(HttpResponse::Ok().json(response)) Ok(HttpResponse::Ok().json(response))
} }
#[post("/{id}/unfavourite")] #[post("/{id}/unfavourite")]
async fn del_favourite(path: web::Path<Id>, state: AppState, auth: AuthData) -> Result<HttpResponse> { async fn del_favourite(
path: web::Path<NoteId>,
state: AppState,
auth: AuthData,
) -> Result<HttpResponse> {
let note_id = path.into_inner(); let note_id = path.into_inner();
let auth_account = auth.require()?; let auth_account = auth.require()?;
let note = state.repo.notes.by_id(note_id).await?; 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?; state.repo.likes.delete(note.id, auth_account.id).await?;
} }
let response = ent::Status::from_model(&state, &note, &auth).await?; let response = ent::Status::from_model(&state, &note, &auth).await?;

View file

@ -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 serde::{Deserialize, Serialize};
use crate::core::*; use crate::core::*;
@ -36,6 +36,6 @@ pub async fn validate(state: &AppState, token: &str) -> Result<Account> {
// tokens that are valid for longer than VALID_SECS are sus // tokens that are valid for longer than VALID_SECS are sus
return Err(Error::BadCredentials); 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 state.repo.accounts.by_id(account_id).await
} }