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 (
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);

View file

@ -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
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;
pub use error::*;
pub type Id = i64;
mod id;
pub use id::*;
pub fn utc_now() -> NaiveDateTime {
Utc::now().naive_utc()

View file

@ -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
}
}

View file

@ -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)

View file

@ -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
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;

View file

@ -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)

View file

@ -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;
}
}

View file

@ -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::*;

View file

@ -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
}
}

View file

@ -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)

View file

@ -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
}
}

View file

@ -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)

View file

@ -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
}
}

View file

@ -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)

View file

@ -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),

View file

@ -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(),

View file

@ -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| {

View file

@ -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()
}
}

View file

@ -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()
}
}

View file

@ -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::*;

View file

@ -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
}
}

View file

@ -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
}
}

View file

@ -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())
})
}
}

View file

@ -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 {

View file

@ -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
}

View file

@ -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)
}
}

View file

@ -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.

View file

@ -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
}

View file

@ -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 {

View file

@ -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
}

View file

@ -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?;

View file

@ -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, &note, &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, &note, &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, &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 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
}