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 super::DBOutPostgresAdapter;
|
||||||
use crate::federation::application::port::out::db::{errors::*, person_followers::*};
|
use crate::federation::application::port::out::db::{errors::*, person_followers::*};
|
||||||
use crate::federation::domain::*;
|
use crate::federation::domain::{followers::*, *};
|
||||||
|
|
||||||
#[async_trait::async_trait]
|
#[async_trait::async_trait]
|
||||||
impl PersonFollowers for DBOutPostgresAdapter {
|
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)
|
self.get_person_from_db_id(res.following_person_id.unwrap() as usize)
|
||||||
.await
|
.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)]
|
#[cfg(test)]
|
||||||
|
@ -120,9 +168,13 @@ mod tests {
|
||||||
assert_eq!(db.count_followers(&person).await.unwrap(), 0);
|
assert_eq!(db.count_followers(&person).await.unwrap(), 0);
|
||||||
|
|
||||||
db.save_person(&person).await.unwrap();
|
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();
|
||||||
db.create(&cmd).await.unwrap();
|
db.create(&cmd).await.unwrap();
|
||||||
assert_eq!(db.count_followers(&person).await.unwrap(), 1);
|
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
db.get_person_from_init_activity_id(cmd.init_activity_id())
|
db.get_person_from_init_activity_id(cmd.init_activity_id())
|
||||||
|
@ -131,12 +183,20 @@ mod tests {
|
||||||
.unwrap(),
|
.unwrap(),
|
||||||
person
|
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())
|
db.delete(cmd.follower(), cmd.init_activity_id())
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
assert_eq!(db.count_followers(&person).await.unwrap(), 0);
|
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;
|
settings.drop_db().await;
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,7 +7,7 @@ use mockall::*;
|
||||||
use url::Url;
|
use url::Url;
|
||||||
|
|
||||||
use super::errors::*;
|
use super::errors::*;
|
||||||
use crate::federation::domain::*;
|
use crate::federation::domain::{followers::*, *};
|
||||||
#[allow(unused_imports)]
|
#[allow(unused_imports)]
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
pub use tests::*;
|
pub use tests::*;
|
||||||
|
@ -22,6 +22,12 @@ pub trait PersonFollowers: Send + Sync {
|
||||||
&self,
|
&self,
|
||||||
init_activity_id: &Url,
|
init_activity_id: &Url,
|
||||||
) -> FederationOutDBPortResult<Option<Person>>;
|
) -> 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>;
|
pub type FederationOutDBPersonFollowersObj = std::sync::Arc<dyn PersonFollowers>;
|
||||||
|
@ -32,6 +38,22 @@ pub mod tests {
|
||||||
|
|
||||||
use std::sync::Arc;
|
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 {
|
pub fn mock_follow_person_create(times: Option<usize>) -> FederationOutDBPersonFollowersObj {
|
||||||
let mut m = MockPersonFollowers::new();
|
let mut m = MockPersonFollowers::new();
|
||||||
if let Some(times) = times {
|
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 errors;
|
||||||
pub mod follow_person_service;
|
pub mod follow_person_service;
|
||||||
pub mod follow_repository_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_person_from_follow_init_activity_id;
|
||||||
pub mod get_remote_actor_service;
|
pub mod get_remote_actor_service;
|
||||||
pub mod get_repository_from_follow_init_activity_id;
|
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 accept_activity;
|
||||||
pub mod follow_activity;
|
pub mod follow_activity;
|
||||||
pub mod follow_repository;
|
pub mod follow_repository;
|
||||||
|
pub mod followers;
|
||||||
pub mod remote_actor;
|
pub mod remote_actor;
|
||||||
|
|
||||||
pub const SEPARATOR: &str = "_";
|
pub const SEPARATOR: &str = "_";
|
||||||
|
|
Loading…
Reference in a new issue