Radish alpha
r
Radicle desktop app
Radicle
Git (anonymous pull)
Log in to clone via SSH
Adding `Service` and `Storage` traits for the inbox logic
Sebastian Martinez committed 1 year ago
commit dc87d3fa847f82be1dfbf43d3e2aee7e1f8f5d7a
parent 62bdbc37d909294fd3d1e04b3af645b754398856
12 files changed +218 -0
modified Cargo.lock
@@ -4144,6 +4144,7 @@ dependencies = [
 "radicle-surf",
 "serde",
 "serde_json",
+
 "sqlite",
 "tempfile",
 "thiserror 1.0.69",
 "tree-sitter-bash",
modified crates/radicle-types/Cargo.toml
@@ -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" }
added crates/radicle-types/src/domain.rs
@@ -0,0 +1 @@
+
pub mod inbox;
added crates/radicle-types/src/domain/inbox.rs
@@ -0,0 +1,3 @@
+
pub mod models;
+
pub mod service;
+
pub mod traits;
added crates/radicle-types/src/domain/inbox/models.rs
@@ -0,0 +1 @@
+
pub mod notification;
added crates/radicle-types/src/domain/inbox/models/notification.rs
@@ -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
+
}
added crates/radicle-types/src/domain/inbox/service.rs
@@ -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)
+
    }
+
}
added crates/radicle-types/src/domain/inbox/traits.rs
@@ -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>;
+
}
modified crates/radicle-types/src/error.rs
@@ -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),
modified crates/radicle-types/src/lib.rs
@@ -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;
added crates/radicle-types/src/outbound.rs
@@ -0,0 +1 @@
+
pub mod sqlite;
added crates/radicle-types/src/outbound/sqlite.rs
@@ -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,
+
            >>()
+
    }
+
}