mirror of
https://seed.radicle.dev/rad:z4V1sjrXqjvFdnCUbxPFqd5p4DtH5.git
synced 2026-10-25 14:50:30 +08:00
Adding Service and Storage traits for the inbox logic
To start making the backend codebase more future proof, I started introducing the hexagonal architecture concept, of having `Service` traits that is the public API and inner traits like `Storage`, `Metrics`, etc. that can be implemented by different adapters like `Sqlite`. This is a first development in this direction.
This commit is contained in:
Generated
+1
@@ -4144,6 +4144,7 @@ dependencies = [
|
||||
"radicle-surf",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"sqlite",
|
||||
"tempfile",
|
||||
"thiserror 1.0.69",
|
||||
"tree-sitter-bash",
|
||||
|
||||
@@ -15,6 +15,7 @@ radicle = { version = "0.14.0", features = ["test"] }
|
||||
radicle-surf = { version = "0.22.1", features = ["serde"] }
|
||||
serde = { version = "1.0.210", features = ["derive"] }
|
||||
serde_json = { version = "1.0.132" }
|
||||
sqlite = { version = "0.32.0", features = ["bundled"] }
|
||||
tempfile = { version = "3.14.0" }
|
||||
thiserror = { version = "1.0.65" }
|
||||
tree-sitter-bash = { version = "0.23.3" }
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
pub mod inbox;
|
||||
@@ -0,0 +1,3 @@
|
||||
pub mod models;
|
||||
pub mod service;
|
||||
pub mod traits;
|
||||
@@ -0,0 +1 @@
|
||||
pub mod notification;
|
||||
@@ -0,0 +1,43 @@
|
||||
use radicle::{git, identity, node, storage};
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Clone, serde::Serialize, serde::Deserialize)]
|
||||
#[serde(rename = "camelCase")]
|
||||
pub struct NotificationRow {
|
||||
pub row_id: node::notifications::NotificationId,
|
||||
pub timestamp: localtime::LocalTime,
|
||||
pub remote: Option<storage::RemoteId>,
|
||||
pub old: Option<git::Oid>,
|
||||
pub new: Option<git::Oid>,
|
||||
}
|
||||
|
||||
pub type RepoGroup = std::collections::BTreeMap<git::Qualified<'static>, Vec<NotificationRow>>;
|
||||
pub type CountByRepo = (identity::RepoId, usize);
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct CountsByRepoParams {
|
||||
pub repo: identity::RepoId,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct RepoGroupParams {
|
||||
pub repo: identity::RepoId,
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum ListNotificationsError {
|
||||
#[error(transparent)]
|
||||
RefError(#[from] git::RefError),
|
||||
|
||||
#[error(transparent)]
|
||||
SerdeJSON(#[from] serde_json::Error),
|
||||
|
||||
#[error(transparent)]
|
||||
Sqlite(#[from] sqlite::Error),
|
||||
|
||||
#[error(transparent)]
|
||||
NotificationKindError(#[from] node::notifications::NotificationKindError),
|
||||
|
||||
#[error(transparent)]
|
||||
Unknown(#[from] anyhow::Error),
|
||||
// to be extended as new error scenarios are introduced
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
use radicle::git;
|
||||
|
||||
use crate::domain::inbox::models::notification::{
|
||||
CountByRepo, ListNotificationsError, NotificationRow, RepoGroupParams,
|
||||
};
|
||||
use crate::domain::inbox::traits::{InboxService, InboxStorage};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Service<I>
|
||||
where
|
||||
I: InboxStorage,
|
||||
{
|
||||
inbox: I,
|
||||
}
|
||||
|
||||
impl<I> Service<I>
|
||||
where
|
||||
I: InboxStorage,
|
||||
{
|
||||
pub fn new(inbox: I) -> Self {
|
||||
Self { inbox }
|
||||
}
|
||||
}
|
||||
|
||||
impl<I> InboxService for Service<I>
|
||||
where
|
||||
I: InboxStorage,
|
||||
{
|
||||
fn counts_by_repo(
|
||||
&self,
|
||||
) -> Result<
|
||||
impl Iterator<Item = Result<CountByRepo, ListNotificationsError>>,
|
||||
ListNotificationsError,
|
||||
> {
|
||||
self.inbox.counts_by_repo()
|
||||
}
|
||||
|
||||
fn repo_group(
|
||||
&self,
|
||||
params: RepoGroupParams,
|
||||
) -> Result<
|
||||
std::collections::BTreeMap<git::Qualified<'static>, Vec<NotificationRow>>,
|
||||
ListNotificationsError,
|
||||
> {
|
||||
self.inbox.repo_group(params)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
use crate::domain::inbox::models::notification::{
|
||||
CountByRepo, ListNotificationsError, RepoGroupParams,
|
||||
};
|
||||
|
||||
use super::models::notification::RepoGroup;
|
||||
|
||||
pub trait InboxStorage {
|
||||
fn counts_by_repo(
|
||||
&self,
|
||||
) -> Result<
|
||||
impl Iterator<Item = Result<CountByRepo, ListNotificationsError>>,
|
||||
ListNotificationsError,
|
||||
>;
|
||||
|
||||
fn repo_group(&self, params: RepoGroupParams) -> Result<RepoGroup, ListNotificationsError>;
|
||||
}
|
||||
|
||||
pub trait InboxService {
|
||||
/// Get the total notification count by repos.
|
||||
fn counts_by_repo(
|
||||
&self,
|
||||
) -> Result<
|
||||
impl Iterator<Item = Result<CountByRepo, ListNotificationsError>>,
|
||||
ListNotificationsError,
|
||||
>;
|
||||
|
||||
fn repo_group(&self, params: RepoGroupParams) -> Result<RepoGroup, ListNotificationsError>;
|
||||
}
|
||||
@@ -21,6 +21,10 @@ pub enum Error {
|
||||
#[error(transparent)]
|
||||
Io(#[from] std::io::Error),
|
||||
|
||||
/// Io error.
|
||||
#[error(transparent)]
|
||||
Sqlite(#[from] sqlite::Error),
|
||||
|
||||
/// Crypto error.
|
||||
#[error(transparent)]
|
||||
Crypto(#[from] radicle::crypto::ssh::keystore::Error),
|
||||
|
||||
@@ -9,7 +9,9 @@ use traits::Profile;
|
||||
pub mod cobs;
|
||||
pub mod config;
|
||||
pub mod diff;
|
||||
pub mod domain;
|
||||
pub mod error;
|
||||
pub mod outbound;
|
||||
pub mod repo;
|
||||
pub mod syntax;
|
||||
pub mod test;
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
pub mod sqlite;
|
||||
@@ -0,0 +1,86 @@
|
||||
use std::collections::BTreeMap;
|
||||
use std::path::Path;
|
||||
use std::sync::Arc;
|
||||
use std::time;
|
||||
|
||||
use radicle::git::Qualified;
|
||||
use radicle::{git, identity};
|
||||
use sqlite as sql;
|
||||
|
||||
use crate::domain::inbox::models::notification;
|
||||
use crate::domain::inbox::traits::InboxStorage;
|
||||
use crate::error::Error;
|
||||
|
||||
pub struct Sqlite {
|
||||
pub db: Arc<sql::ConnectionThreadSafe>,
|
||||
}
|
||||
|
||||
impl Sqlite {
|
||||
/// How long to wait for the database lock to be released before failing a read.
|
||||
const DB_READ_TIMEOUT: time::Duration = time::Duration::from_secs(3);
|
||||
|
||||
pub fn reader<P: AsRef<Path>>(path: P) -> Result<Self, Error> {
|
||||
let mut db = sql::Connection::open_thread_safe_with_flags(
|
||||
path,
|
||||
sqlite::OpenFlags::new().with_read_only(),
|
||||
)?;
|
||||
db.set_busy_timeout(Self::DB_READ_TIMEOUT.as_millis() as usize)?;
|
||||
|
||||
Ok(Self { db: Arc::new(db) })
|
||||
}
|
||||
}
|
||||
|
||||
impl InboxStorage for Sqlite {
|
||||
fn counts_by_repo(
|
||||
&self,
|
||||
) -> Result<
|
||||
impl Iterator<Item = Result<notification::CountByRepo, notification::ListNotificationsError>>,
|
||||
notification::ListNotificationsError,
|
||||
> {
|
||||
let stmt = self.db.prepare(
|
||||
"SELECT repo, COUNT(*) as count
|
||||
FROM `repository-notifications`
|
||||
GROUP BY repo",
|
||||
)?;
|
||||
|
||||
Ok(stmt.into_iter().map(|row| {
|
||||
let row = row?;
|
||||
let count = row.try_read::<i64, _>("count")? as usize;
|
||||
let repo = row.try_read::<identity::RepoId, _>("repo")?;
|
||||
|
||||
Ok((repo, count))
|
||||
}))
|
||||
}
|
||||
|
||||
fn repo_group(
|
||||
&self,
|
||||
params: notification::RepoGroupParams,
|
||||
) -> Result<
|
||||
std::collections::BTreeMap<git::Qualified<'static>, Vec<notification::NotificationRow>>,
|
||||
notification::ListNotificationsError,
|
||||
> {
|
||||
let mut stmt = self.db.prepare(
|
||||
"SELECT ref, substr(ref, 66) ref_without_namespace, json_group_array(json_object('row_id', rowid, 'timestamp', timestamp, 'remote', substr(ref, 17, 48), 'old', old, 'new', new)) as value
|
||||
FROM 'repository-notifications'
|
||||
WHERE repo = ?
|
||||
GROUP BY ref_without_namespace
|
||||
ORDER BY timestamp DESC"
|
||||
)?;
|
||||
stmt.bind((1, ¶ms.repo))?;
|
||||
|
||||
stmt.into_iter()
|
||||
.map(|row| {
|
||||
let row = row?;
|
||||
let refstr = row.try_read::<&str, _>("ref")?;
|
||||
let value = row.try_read::<&str, _>("value")?;
|
||||
let items = serde_json::from_str::<Vec<notification::NotificationRow>>(value)?;
|
||||
let (_, reference) = git::parse_ref::<String>(refstr)?;
|
||||
|
||||
Ok((reference.to_owned(), items))
|
||||
})
|
||||
.collect::<Result<
|
||||
BTreeMap<Qualified<'static>, Vec<notification::NotificationRow>>,
|
||||
notification::ListNotificationsError,
|
||||
>>()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user