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:
Sebastian Martinez
2025-01-23 22:23:44 +01:00
parent 62bdbc37d9
commit dc87d3fa84
12 changed files with 218 additions and 0 deletions
Generated
+1
View File
@@ -4144,6 +4144,7 @@ dependencies = [
"radicle-surf",
"serde",
"serde_json",
"sqlite",
"tempfile",
"thiserror 1.0.69",
"tree-sitter-bash",
+1
View File
@@ -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" }
+1
View File
@@ -0,0 +1 @@
pub mod inbox;
+3
View File
@@ -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>;
}
+4
View File
@@ -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),
+2
View File
@@ -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;
+1
View File
@@ -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, &params.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,
>>()
}
}