feat: db port&adapter read paginated list of followers

This commit is contained in:
Aravinth Manivannan 2024-09-12 19:20:49 +05:30
parent 2094fc17dc
commit 2aa425b833
Signed by: realaravinth
GPG key ID: F8F50389936984FF
7 changed files with 217 additions and 3 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View 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()],
}
}
}

View file

@ -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 = "_";