feat: db port&adapter read paginated list of followers
This commit is contained in:
parent
2094fc17dc
commit
2aa425b833
7 changed files with 217 additions and 3 deletions
|
@ -0,0 +1,24 @@
|
|||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT\n follower\n FROM\n person_followers\n WHERE\n following_person_id = (SELECT ID FROM persons WHERE html_url = $1)\n OFFSET $2 LIMIT $3;",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "follower",
|
||||
"type_info": "Text"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Int8",
|
||||
"Int8"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "a57266fa15a40dc8eefcb9c12421d7303af2c7fa9b9075fcd4180c5205b61c49"
|
||||
}
|
|
@ -5,7 +5,7 @@ use url::Url;
|
|||
|
||||
use super::DBOutPostgresAdapter;
|
||||
use crate::federation::application::port::out::db::{errors::*, person_followers::*};
|
||||
use crate::federation::domain::*;
|
||||
use crate::federation::domain::{followers::*, *};
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl PersonFollowers for DBOutPostgresAdapter {
|
||||
|
@ -97,6 +97,54 @@ impl PersonFollowers for DBOutPostgresAdapter {
|
|||
self.get_person_from_db_id(res.following_person_id.unwrap() as usize)
|
||||
.await
|
||||
}
|
||||
/// page starts from 0
|
||||
async fn get_person_followers(
|
||||
&self,
|
||||
person: &Person,
|
||||
page: usize,
|
||||
) -> FederationOutDBPortResult<Followers> {
|
||||
const LIMIT: usize = 30;
|
||||
let offset = page * LIMIT;
|
||||
|
||||
struct Followers {
|
||||
follower: String,
|
||||
}
|
||||
let res = sqlx::query_as!(
|
||||
Followers,
|
||||
"SELECT
|
||||
follower
|
||||
FROM
|
||||
person_followers
|
||||
WHERE
|
||||
following_person_id = (SELECT ID FROM persons WHERE html_url = $1)
|
||||
OFFSET $2 LIMIT $3;",
|
||||
person.html_url().as_str(),
|
||||
offset as i32,
|
||||
LIMIT as i32
|
||||
)
|
||||
.fetch_all(&self.pool)
|
||||
.await?;
|
||||
|
||||
let mut followers = Vec::with_capacity(res.len());
|
||||
res.iter()
|
||||
.for_each(|s| followers.push(Url::parse(&s.follower).unwrap()));
|
||||
|
||||
let count = self.count_followers(person).await?;
|
||||
|
||||
let next_page = if followers.len() < LIMIT {
|
||||
None
|
||||
} else {
|
||||
Some(page + 1)
|
||||
};
|
||||
|
||||
Ok(FollowersBuilder::default()
|
||||
.current_page(page)
|
||||
.total_followers(count)
|
||||
.followers(followers)
|
||||
.next_page(next_page)
|
||||
.build()
|
||||
.unwrap())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
@ -120,9 +168,13 @@ mod tests {
|
|||
assert_eq!(db.count_followers(&person).await.unwrap(), 0);
|
||||
|
||||
db.save_person(&person).await.unwrap();
|
||||
assert_eq!(db.count_followers(&person).await.unwrap(), 0);
|
||||
let followers = db.get_person_followers(&person, 0).await.unwrap();
|
||||
assert_eq!(*followers.total_followers(), 0);
|
||||
assert!(followers.followers().is_empty());
|
||||
|
||||
db.create(&cmd).await.unwrap();
|
||||
db.create(&cmd).await.unwrap();
|
||||
assert_eq!(db.count_followers(&person).await.unwrap(), 1);
|
||||
|
||||
assert_eq!(
|
||||
db.get_person_from_init_activity_id(cmd.init_activity_id())
|
||||
|
@ -131,12 +183,20 @@ mod tests {
|
|||
.unwrap(),
|
||||
person
|
||||
);
|
||||
let followers = db.get_person_followers(&person, 0).await.unwrap();
|
||||
assert_eq!(*followers.total_followers(), 1);
|
||||
assert_eq!(*followers.next_page(), None);
|
||||
assert_eq!(followers.followers().as_ref(), [cmd.follower().clone()]);
|
||||
|
||||
db.delete(cmd.follower(), cmd.init_activity_id())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(db.count_followers(&person).await.unwrap(), 0);
|
||||
let followers = db.get_person_followers(&person, 0).await.unwrap();
|
||||
assert_eq!(*followers.total_followers(), 0);
|
||||
assert_eq!(*followers.next_page(), None);
|
||||
assert!(followers.followers().is_empty());
|
||||
|
||||
settings.drop_db().await;
|
||||
}
|
||||
|
|
|
@ -7,7 +7,7 @@ use mockall::*;
|
|||
use url::Url;
|
||||
|
||||
use super::errors::*;
|
||||
use crate::federation::domain::*;
|
||||
use crate::federation::domain::{followers::*, *};
|
||||
#[allow(unused_imports)]
|
||||
#[cfg(test)]
|
||||
pub use tests::*;
|
||||
|
@ -22,6 +22,12 @@ pub trait PersonFollowers: Send + Sync {
|
|||
&self,
|
||||
init_activity_id: &Url,
|
||||
) -> FederationOutDBPortResult<Option<Person>>;
|
||||
/// page starts from 0
|
||||
async fn get_person_followers(
|
||||
&self,
|
||||
person: &Person,
|
||||
page: usize,
|
||||
) -> FederationOutDBPortResult<Followers>;
|
||||
}
|
||||
|
||||
pub type FederationOutDBPersonFollowersObj = std::sync::Arc<dyn PersonFollowers>;
|
||||
|
@ -32,6 +38,22 @@ pub mod tests {
|
|||
|
||||
use std::sync::Arc;
|
||||
|
||||
pub fn mock_get_person_followers(times: Option<usize>) -> FederationOutDBPersonFollowersObj {
|
||||
let mut m = MockPersonFollowers::new();
|
||||
if let Some(times) = times {
|
||||
m.expect_get_person_followers()
|
||||
.times(times)
|
||||
.returning(|_, _| Ok(Followers::default()));
|
||||
m.expect_count_followers().returning(|_| Ok(0));
|
||||
} else {
|
||||
m.expect_get_person_followers()
|
||||
.returning(|_, _| Ok(Followers::default()));
|
||||
m.expect_count_followers().returning(|_| Ok(0));
|
||||
}
|
||||
|
||||
Arc::new(m)
|
||||
}
|
||||
|
||||
pub fn mock_follow_person_create(times: Option<usize>) -> FederationOutDBPersonFollowersObj {
|
||||
let mut m = MockPersonFollowers::new();
|
||||
if let Some(times) = times {
|
||||
|
|
|
@ -0,0 +1,77 @@
|
|||
// SPDX-FileCopyrightText: 2024 Aravinth Manivannan <realaravinth@batsense.net>
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
use derive_builder::Builder;
|
||||
use derive_getters::Getters;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use super::errors::*;
|
||||
use crate::federation::application::port::out::db::person_followers::*;
|
||||
use crate::federation::domain::{followers::*, *};
|
||||
|
||||
#[allow(unused_imports)]
|
||||
pub use command::*;
|
||||
|
||||
pub mod command {
|
||||
use super::*;
|
||||
|
||||
#[derive(Debug, Getters, Clone, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
|
||||
pub struct GetPersonFollowersCommand {
|
||||
person: Person,
|
||||
page: usize,
|
||||
}
|
||||
|
||||
impl GetPersonFollowersCommand {
|
||||
pub fn new_command(person: Person, page: usize) -> Self {
|
||||
Self { person, page }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
pub trait GetPersonFollowersUseCase: Send + Sync {
|
||||
async fn get_person_followers(
|
||||
&self,
|
||||
cmd: command::GetPersonFollowersCommand,
|
||||
) -> FederationServiceResult<Followers>;
|
||||
}
|
||||
|
||||
#[derive(Builder, Clone)]
|
||||
pub struct GetPersonFollowersService {
|
||||
out_db_person_followers_adapter: FederationOutDBPersonFollowersObj,
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl GetPersonFollowersUseCase for GetPersonFollowersService {
|
||||
async fn get_person_followers(
|
||||
&self,
|
||||
cmd: command::GetPersonFollowersCommand,
|
||||
) -> FederationServiceResult<Followers> {
|
||||
Ok(self
|
||||
.out_db_person_followers_adapter
|
||||
.get_person_followers(cmd.person(), *(cmd.page()))
|
||||
.await?)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
use crate::tests::bdd::*;
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_service() {
|
||||
let person = Person::default();
|
||||
let cmd = GetPersonFollowersCommand::new_command(person, 0);
|
||||
// retrieved from DB
|
||||
let s = GetPersonFollowersServiceBuilder::default()
|
||||
.out_db_person_followers_adapter(mock_get_person_followers(IS_CALLED_ONLY_ONCE))
|
||||
.build()
|
||||
.unwrap();
|
||||
|
||||
let resp = s.get_person_followers(cmd.clone()).await.unwrap();
|
||||
assert_eq!(resp, Followers::default());
|
||||
}
|
||||
}
|
|
@ -17,6 +17,7 @@ pub mod delete_remote_actor_service;
|
|||
pub mod errors;
|
||||
pub mod follow_person_service;
|
||||
pub mod follow_repository_service;
|
||||
pub mod get_person_follower_list_service;
|
||||
pub mod get_person_from_follow_init_activity_id;
|
||||
pub mod get_remote_actor_service;
|
||||
pub mod get_repository_from_follow_init_activity_id;
|
||||
|
|
29
src/federation/domain/followers.rs
Normal file
29
src/federation/domain/followers.rs
Normal file
|
@ -0,0 +1,29 @@
|
|||
// SPDX-FileCopyrightText: 2024 Aravinth Manivannan <realaravinth@batsense.net>
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
use derive_builder::Builder;
|
||||
use derive_getters::Getters;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use url::Url;
|
||||
|
||||
#[derive(
|
||||
Debug, Builder, Clone, Getters, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord,
|
||||
)]
|
||||
pub struct Followers {
|
||||
total_followers: usize,
|
||||
current_page: usize,
|
||||
next_page: Option<usize>,
|
||||
followers: Vec<Url>,
|
||||
}
|
||||
|
||||
impl Default for Followers {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
total_followers: 1,
|
||||
current_page: 0,
|
||||
next_page: None,
|
||||
followers: vec![Url::parse("https://forgeflux.example.com/foooo").unwrap()],
|
||||
}
|
||||
}
|
||||
}
|
|
@ -14,6 +14,7 @@ use url::Url;
|
|||
pub mod accept_activity;
|
||||
pub mod follow_activity;
|
||||
pub mod follow_repository;
|
||||
pub mod followers;
|
||||
pub mod remote_actor;
|
||||
|
||||
pub const SEPARATOR: &str = "_";
|
||||
|
|
Loading…
Reference in a new issue