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:
parent
7845fb3680
commit
a0d2dc4151
37 changed files with 550 additions and 218 deletions
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
|
|
198
src/core/id.rs
Normal file
198
src/core/id.rs
Normal 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)
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
|
|
|
@ -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<Id, Account>,
|
||||
cache: MemCache<AccountId, Account>,
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
|
@ -23,8 +21,8 @@ impl MemAccountDataSource {
|
|||
}
|
||||
}
|
||||
|
||||
impl Indexable<Id> for Account {
|
||||
fn get_id(&self) -> Id {
|
||||
impl Indexable<AccountId> for Account {
|
||||
fn get_id(&self) -> AccountId {
|
||||
self.id
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<Account> {
|
||||
pub async fn by_id(&self, id: AccountId) -> Result<Account> {
|
||||
let account: Account = sqlx::query_as("SELECT * FROM accounts WHERE id = $1")
|
||||
.bind(id)
|
||||
.fetch_one(&self.pool)
|
||||
|
|
|
@ -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<Follow> {
|
||||
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<Follow> {
|
||||
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<Follow> {
|
||||
|
@ -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<Follow> {
|
||||
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<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")
|
||||
.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<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")
|
||||
.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<u32> {
|
||||
pub async fn following_count_of(&self, account_id: AccountId) -> Result<u32> {
|
||||
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<u32> {
|
||||
pub async fn follower_count_of(&self, account_id: AccountId) -> Result<u32> {
|
||||
let follower_count: Count =
|
||||
sqlx::query_as("SELECT COUNT(*) AS count FROM follows WHERE followee_id = $1")
|
||||
.bind(account_id)
|
||||
|
|
72
src/data/like/mem.rs
Normal file
72
src/data/like/mem.rs
Normal 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
|
||||
}
|
||||
}
|
|
@ -1,3 +1,5 @@
|
|||
pub mod pg;
|
||||
mod mem;
|
||||
mod pg;
|
||||
|
||||
pub use mem::MemLikeDataSource;
|
||||
pub use pg::PgLikeDataSource;
|
||||
|
|
|
@ -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<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> {
|
||||
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<u64> {
|
||||
pub async fn count_by_note(&self, note_id: NoteId) -> Result<u64> {
|
||||
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<Like> {
|
||||
pub async fn by_note_and_account(
|
||||
&self,
|
||||
note_id: NoteId,
|
||||
account_id: AccountId,
|
||||
) -> Result<Like> {
|
||||
let like: Like =
|
||||
sqlx::query_as("SELECT * FROM likes WHERE note_id = $1 AND account_id = $2")
|
||||
.bind(note_id)
|
||||
|
|
|
@ -137,14 +137,16 @@ impl Hasher for Djb2 {
|
|||
mod tests {
|
||||
use super::*;
|
||||
|
||||
type TestId = Id<TestEntry>;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
struct TestEntry {
|
||||
id: Id,
|
||||
id: TestId,
|
||||
data: i32,
|
||||
}
|
||||
|
||||
impl Indexable<Id> for TestEntry {
|
||||
fn get_id(&self) -> Id {
|
||||
impl Indexable<TestId> 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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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::*;
|
||||
|
|
|
@ -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<Id, Note>,
|
||||
cache: MemCache<NoteId, Note>,
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
|
@ -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<Id> for Note {
|
||||
fn get_id(&self) -> Id {
|
||||
impl Indexable<NoteId> for Note {
|
||||
fn get_id(&self) -> NoteId {
|
||||
self.id
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<Note> {
|
||||
pub async fn by_id(&self, id: NoteId) -> Result<Note> {
|
||||
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<Vec<Note>> {
|
||||
pub async fn by_account(&self, id: AccountId, since: NaiveDateTime) -> Result<Vec<Note>> {
|
||||
let notes: Vec<Note> = 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<u32> {
|
||||
pub async fn count_by_account(&self, account_id: AccountId) -> Result<u32> {
|
||||
let count: Count =
|
||||
sqlx::query_as("SELECT COUNT(*) AS count FROM notes WHERE account_id = $1")
|
||||
.bind(account_id)
|
||||
|
|
|
@ -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<Uuid, Object>,
|
||||
cache: MemCache<ObjectId, Object>,
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
|
@ -23,8 +21,8 @@ impl MemObjectDataSource {
|
|||
}
|
||||
}
|
||||
|
||||
impl Indexable<Uuid> for Object {
|
||||
fn get_id(&self) -> Uuid {
|
||||
impl Indexable<ObjectId> for Object {
|
||||
fn get_id(&self) -> ObjectId {
|
||||
self.id
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<Object> {
|
||||
pub async fn by_id(&self, id: ObjectId) -> Result<Object> {
|
||||
let object: Object = sqlx::query_as("SELECT * FROM objects WHERE id = $1")
|
||||
.bind(id)
|
||||
.fetch_one(&self.pool)
|
||||
|
|
|
@ -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<Id, User>,
|
||||
cache: MemCache<UserId, User>,
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
|
@ -23,8 +21,8 @@ impl MemUserDataSource {
|
|||
}
|
||||
}
|
||||
|
||||
impl Indexable<Id> for User {
|
||||
fn get_id(&self) -> Id {
|
||||
impl Indexable<UserId> for User {
|
||||
fn get_id(&self) -> UserId {
|
||||
self.id
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<User> {
|
||||
pub async fn by_id(&self, id: UserId) -> Result<User> {
|
||||
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<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")
|
||||
.bind(account_id)
|
||||
.fetch_one(&self.pool)
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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(),
|
||||
|
|
|
@ -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<Account>;
|
||||
|
||||
#[derive(Clone, Deserialize, Serialize, FromRow)]
|
||||
pub struct Account {
|
||||
pub id: Id,
|
||||
pub id: AccountId,
|
||||
pub iri: Option<String>,
|
||||
pub name: String,
|
||||
pub domain: String,
|
||||
|
@ -23,8 +25,8 @@ pub struct NewAccount {
|
|||
pub display_name: Option<String>,
|
||||
}
|
||||
|
||||
impl From<Account> for Id {
|
||||
fn from(account: Account) -> Id {
|
||||
impl From<Account> 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| {
|
||||
|
|
|
@ -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<Follow, (AccountId, AccountId)>;
|
||||
|
||||
#[derive(Deserialize, Serialize, FromRow)]
|
||||
pub struct Follow {
|
||||
pub id: Id,
|
||||
pub iri: Option<String>,
|
||||
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<String>,
|
||||
pub follower_id: Id,
|
||||
pub followee_id: Id,
|
||||
pub follower_id: AccountId,
|
||||
pub followee_id: AccountId,
|
||||
}
|
||||
|
||||
impl From<Follow> for Id {
|
||||
fn from(follow: Follow) -> Id {
|
||||
follow.id
|
||||
impl From<Follow> for FollowId {
|
||||
fn from(follow: Follow) -> FollowId {
|
||||
(follow.follower_id, follow.followee_id).into()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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<Like, (NoteId, AccountId)>;
|
||||
|
||||
#[derive(Clone, Deserialize, Serialize, FromRow)]
|
||||
pub struct Like {
|
||||
pub id: Id,
|
||||
pub iri: Option<String>,
|
||||
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<String>,
|
||||
pub note_id: Id,
|
||||
pub account_id: Id,
|
||||
pub note_id: NoteId,
|
||||
pub account_id: AccountId,
|
||||
}
|
||||
|
||||
impl From<Like> for Id {
|
||||
fn from(like: Like) -> Id {
|
||||
like.id
|
||||
impl From<Like> for LikeId {
|
||||
fn from(val: Like) -> LikeId {
|
||||
(val.note_id, val.account_id).into()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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::*;
|
||||
|
|
|
@ -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<Note>;
|
||||
|
||||
#[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<String>,
|
||||
pub content: String,
|
||||
pub source: Option<String>,
|
||||
|
@ -20,7 +23,7 @@ pub struct Note {
|
|||
}
|
||||
|
||||
pub struct NewNote {
|
||||
pub account_id: Id,
|
||||
pub account_id: AccountId,
|
||||
pub iri: Option<String>,
|
||||
pub content: String,
|
||||
pub source: Option<String>,
|
||||
|
@ -29,9 +32,9 @@ pub struct NewNote {
|
|||
pub sensitive: bool,
|
||||
}
|
||||
|
||||
impl From<Note> for Id {
|
||||
fn from(status: Note) -> Id {
|
||||
status.id
|
||||
impl From<Note> for NoteId {
|
||||
fn from(val: Note) -> NoteId {
|
||||
val.id
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -2,9 +2,13 @@ use chrono::NaiveDateTime;
|
|||
use sqlx::types::Uuid;
|
||||
use sqlx::FromRow;
|
||||
|
||||
use crate::core::*;
|
||||
|
||||
pub type ObjectId = Id<Object, Uuid>;
|
||||
|
||||
#[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<Object> for ObjectId {
|
||||
fn from(val: Object) -> ObjectId {
|
||||
val.id
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<User>;
|
||||
|
||||
#[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<String>,
|
||||
|
@ -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<String>,
|
||||
pub locale: String,
|
||||
}
|
||||
|
||||
impl From<User> for Id {
|
||||
fn from(user: User) -> Id {
|
||||
impl From<User> for UserId {
|
||||
fn from(user: User) -> UserId {
|
||||
user.id
|
||||
}
|
||||
}
|
||||
|
@ -32,7 +35,7 @@ impl From<User> 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())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<I>(&self, id: I) -> Result<Account>
|
||||
where
|
||||
I: Into<Id> + Send,
|
||||
I: Into<AccountId> + Send,
|
||||
{
|
||||
let id = id.into();
|
||||
match self.mem.by_id(id).await {
|
||||
|
|
|
@ -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<I>(&self, id: I) -> Result<Follow>
|
||||
where
|
||||
I: Into<Id> + Send,
|
||||
I: Into<FollowId> + Send,
|
||||
{
|
||||
self.db.by_id(id.into()).await
|
||||
}
|
||||
|
@ -35,8 +35,8 @@ impl FollowRepo {
|
|||
followee_id: J,
|
||||
) -> Result<Follow>
|
||||
where
|
||||
I: Into<Id> + Send,
|
||||
J: Into<Id> + Send,
|
||||
I: Into<AccountId> + Send,
|
||||
J: Into<AccountId> + Send,
|
||||
{
|
||||
let (follower_id, followee_id) = (follower_id.into(), followee_id.into());
|
||||
self.db
|
||||
|
@ -46,8 +46,8 @@ impl FollowRepo {
|
|||
|
||||
pub async fn follows<I, J>(&self, follower_id: I, followee_id: J) -> Result<bool>
|
||||
where
|
||||
I: Into<Id> + Send,
|
||||
J: Into<Id> + Send,
|
||||
I: Into<AccountId> + Send,
|
||||
J: Into<AccountId> + Send,
|
||||
{
|
||||
match self
|
||||
.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>>
|
||||
where
|
||||
I: Into<Id> + Send,
|
||||
I: Into<AccountId> + Send,
|
||||
{
|
||||
self.db.followees_of(account_id.into()).await
|
||||
}
|
||||
|
||||
pub async fn followers_of<I>(&self, account_id: I) -> Result<Vec<Follow>>
|
||||
where
|
||||
I: Into<Id> + Send,
|
||||
I: Into<AccountId> + Send,
|
||||
{
|
||||
self.db.followers_of(account_id.into()).await
|
||||
}
|
||||
|
||||
pub async fn following_count_of<I>(&self, account_id: I) -> Result<u32>
|
||||
where
|
||||
I: Into<Id> + Send,
|
||||
I: Into<AccountId> + Send,
|
||||
{
|
||||
self.db.following_count_of(account_id.into()).await
|
||||
}
|
||||
|
||||
pub async fn follower_count_of<I>(&self, account_id: I) -> Result<u32>
|
||||
where
|
||||
I: Into<Id> + Send,
|
||||
I: Into<AccountId> + Send,
|
||||
{
|
||||
self.db.follower_count_of(account_id.into()).await
|
||||
}
|
||||
|
|
|
@ -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<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>
|
||||
where
|
||||
T: TryInto<Sane<NewLike>, Error = E> + Send,
|
||||
E: Into<Error>,
|
||||
{
|
||||
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<()>
|
||||
where
|
||||
N: Into<Id>,
|
||||
A: Into<Id>,
|
||||
N: Into<NoteId>,
|
||||
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>
|
||||
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>
|
||||
where
|
||||
N: Into<Id>,
|
||||
A: Into<Id>,
|
||||
N: Into<NoteId>,
|
||||
A: Into<AccountId>,
|
||||
{
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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<I>(&self, id: I) -> Result<Note>
|
||||
where
|
||||
I: Into<Id> + Send,
|
||||
I: Into<NoteId> + Send,
|
||||
{
|
||||
let id = id.into();
|
||||
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>>
|
||||
where
|
||||
I: Into<Id> + Send,
|
||||
I: Into<AccountId> + Send,
|
||||
{
|
||||
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<()>
|
||||
where
|
||||
I: Into<Id>,
|
||||
I: Into<NoteId>,
|
||||
{
|
||||
let note_id = note_id.into();
|
||||
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>
|
||||
where
|
||||
I: Into<Id> + Send,
|
||||
I: Into<AccountId> + Send,
|
||||
{
|
||||
self.db.count_by_account(account_id.into()).await
|
||||
}
|
||||
|
|
|
@ -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<U>(&self, id: U) -> Result<Object>
|
||||
where
|
||||
U: Into<Uuid>,
|
||||
U: Into<ObjectId>,
|
||||
{
|
||||
let id = id.into();
|
||||
match self.mem.by_id(id).await {
|
||||
|
|
|
@ -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<I>(&self, id: I) -> Result<User>
|
||||
where
|
||||
I: Into<Id> + Send,
|
||||
I: Into<UserId> + Send,
|
||||
{
|
||||
let id = id.into();
|
||||
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>
|
||||
where
|
||||
I: Into<Id> + Send,
|
||||
I: Into<AccountId> + Send,
|
||||
{
|
||||
self.db.by_account(account_id.into()).await
|
||||
}
|
||||
|
|
|
@ -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<HttpRespo
|
|||
}
|
||||
|
||||
#[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();
|
||||
if let Some(auth) = auth.maybe() {
|
||||
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")]
|
||||
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 account = state.repo.accounts.by_id(id).await?;
|
||||
let notes = state.repo.notes.by_account(account, utc_now()).await?;
|
||||
|
|
|
@ -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<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 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<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 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<Id>, state: AppState, auth: AuthData) -> Resu
|
|||
}
|
||||
|
||||
#[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 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<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 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?;
|
||||
|
|
|
@ -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<Account> {
|
|||
// 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
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue