diff --git a/radicle-httpd/src/api.rs b/radicle-httpd/src/api.rs index 750a2984..a4a71919 100644 --- a/radicle-httpd/src/api.rs +++ b/radicle-httpd/src/api.rs @@ -17,6 +17,7 @@ use tower_http::cors::{self, CorsLayer}; use radicle::cob::issue; use radicle::cob::patch; use radicle::identity::Id; +use radicle::node::routing::Store; use radicle::storage::{ReadRepository, ReadStorage}; use radicle::Profile; @@ -52,6 +53,8 @@ impl Context { let delegates = doc.delegates; let issues = issue::Issues::open(&repo)?.counts()?; let patches = patch::Patches::open(&repo)?.counts()?; + let routing = &self.profile.routing()?; + let trackings = routing.count(&id).unwrap_or_default(); Ok(project::Info { payload, @@ -60,6 +63,7 @@ impl Context { issues, patches, id, + trackings, }) } @@ -183,5 +187,6 @@ mod project { pub patches: cob::patch::PatchCounts, pub issues: cob::issue::IssueCounts, pub id: Id, + pub trackings: usize, } } diff --git a/radicle-httpd/src/api/error.rs b/radicle-httpd/src/api/error.rs index e20c6e84..f1763e26 100644 --- a/radicle-httpd/src/api/error.rs +++ b/radicle-httpd/src/api/error.rs @@ -73,6 +73,10 @@ pub enum Error { /// Tracking store error. #[error(transparent)] TrackingStore(#[from] radicle::node::tracking::store::Error), + + /// Routing store error. + #[error(transparent)] + RoutingStore(#[from] radicle::node::routing::Error), } impl IntoResponse for Error { diff --git a/radicle-httpd/src/api/v1/delegates.rs b/radicle-httpd/src/api/v1/delegates.rs index 603eb287..82541651 100644 --- a/radicle-httpd/src/api/v1/delegates.rs +++ b/radicle-httpd/src/api/v1/delegates.rs @@ -6,6 +6,7 @@ use axum::{Json, Router}; use radicle::cob::issue::Issues; use radicle::cob::patch::Patches; use radicle::identity::Did; +use radicle::node::routing::Store; use radicle::storage::{ReadRepository, ReadStorage}; use crate::api::error::Error; @@ -34,6 +35,7 @@ async fn delegates_projects_handler( let page = page.unwrap_or(0); let per_page = per_page.unwrap_or(10); let storage = &ctx.profile.storage; + let routing = &ctx.profile.routing()?; let projects = storage .repositories()? .into_iter() @@ -54,6 +56,8 @@ async fn delegates_projects_handler( let Ok(patches) = Patches::open(&repo) else { return None }; let Ok(patches) = patches.counts() else { return None }; + let trackings = routing.count(&id).unwrap_or_default(); + Some(Info { payload, delegates, @@ -61,6 +65,7 @@ async fn delegates_projects_handler( issues, patches, id, + trackings, }) }) .skip(page * per_page) @@ -107,7 +112,8 @@ mod routes { "open": 1, "closed": 0, }, - "id": RID + "id": RID, + "trackings": 0, } ]) ); diff --git a/radicle-httpd/src/api/v1/projects.rs b/radicle-httpd/src/api/v1/projects.rs index 55fccb3a..38653962 100644 --- a/radicle-httpd/src/api/v1/projects.rs +++ b/radicle-httpd/src/api/v1/projects.rs @@ -14,6 +14,7 @@ use tower_http::set_header::SetResponseHeaderLayer; use radicle::cob::{issue, patch, thread, ActorId, Tag}; use radicle::identity::Id; +use radicle::node::routing::Store; use radicle::node::NodeId; use radicle::storage::git::paths; use radicle::storage::{ReadRepository, ReadStorage, WriteRepository}; @@ -77,6 +78,7 @@ async fn project_root_handler( let page = page.unwrap_or(0); let per_page = per_page.unwrap_or(10); let storage = &ctx.profile.storage; + let routing = &ctx.profile.routing()?; let projects = storage .repositories()? .into_iter() @@ -91,6 +93,7 @@ async fn project_root_handler( let Ok(patches) = patch::Patches::open(&repo) else { return None }; let Ok(patches) = patches.counts() else { return None }; let delegates = doc.delegates; + let trackings = routing.count(&id).unwrap_or_default(); Some(Info { payload, @@ -99,6 +102,7 @@ async fn project_root_handler( issues, patches, id, + trackings, }) }) .skip(page * per_page) @@ -800,7 +804,8 @@ mod routes { "open": 1, "closed": 0, }, - "id": RID + "id": RID, + "trackings": 0, } ]) ); @@ -831,7 +836,8 @@ mod routes { "open": 1, "closed": 0, }, - "id": RID + "id": RID, + "trackings": 0, }) ); } diff --git a/radicle-httpd/src/test.rs b/radicle-httpd/src/test.rs index bbb0319e..14a922bc 100644 --- a/radicle-httpd/src/test.rs +++ b/radicle-httpd/src/test.rs @@ -16,6 +16,7 @@ use radicle::crypto::ssh::keystore::MemorySigner; use radicle::crypto::ssh::Keystore; use radicle::crypto::{KeyPair, Seed, Signer}; use radicle::git::{raw as git2, RefString}; +use radicle::node::routing as RoutingStore; use radicle::node::tracking::store as TrackingStore; use radicle::profile::Home; use radicle::storage::ReadStorage; @@ -85,7 +86,9 @@ pub fn contributor(dir: &Path) -> Context { fn seed_with_signer(dir: &Path, profile: radicle::Profile, signer: &G) -> Context { let tracking_db = dir.join("radicle").join("node").join("tracking.db"); + let routing_db = dir.join("radicle").join("node").join("routing.db"); TrackingStore::Config::open(tracking_db).unwrap(); + RoutingStore::Table::open(routing_db).unwrap(); let workdir = dir.join("hello-world"); diff --git a/radicle/src/node/routing.rs b/radicle/src/node/routing.rs index 3ab3363e..1ce191d3 100644 --- a/radicle/src/node/routing.rs +++ b/radicle/src/node/routing.rs @@ -104,6 +104,8 @@ pub trait Store { fn len(&self) -> Result; /// Prune entries older than the given timestamp. fn prune(&mut self, oldest: Timestamp, limit: Option) -> Result; + /// Count the number of routes for a specific repo RID. + fn count(&self, id: &Id) -> Result; } impl Store for Table { @@ -236,6 +238,24 @@ impl Store for Table { Ok(self.db.change_count()) } + + fn count(&self, id: &Id) -> Result { + let mut stmt = self + .db + .prepare("SELECT COUNT(*) FROM routing WHERE resource = ?")?; + + stmt.bind((1, id))?; + + let count: i64 = stmt + .into_iter() + .next() + .expect("COUNT will always return a single row")? + .read(0); + + let count: usize = count.try_into().map_err(|_| Error::UnitOverflow)?; + + Ok(count) + } } #[cfg(test)] @@ -408,4 +428,17 @@ mod test { } } } + + #[test] + fn test_count() { + let id = arbitrary::gen::(1); + let nodes = arbitrary::set::(5..10); + let mut db = Table::open(":memory:").unwrap(); + + for node in &nodes { + db.insert(id, *node, 0).unwrap(); + } + + assert_eq!(db.count(&id).unwrap(), nodes.len()); + } } diff --git a/radicle/src/profile.rs b/radicle/src/profile.rs index f86990e6..f5ec4600 100644 --- a/radicle/src/profile.rs +++ b/radicle/src/profile.rs @@ -18,7 +18,7 @@ use thiserror::Error; use crate::crypto::ssh::agent::Agent; use crate::crypto::ssh::{keystore, Keystore, Passphrase}; use crate::crypto::{PublicKey, Signer}; -use crate::node::{self, tracking}; +use crate::node::{self, routing, tracking}; use crate::prelude::Did; use crate::storage::git::transport; use crate::storage::git::Storage; @@ -135,6 +135,14 @@ impl Profile { Ok(config) } + /// Return a read-only handle to the routing database of the node. + pub fn routing(&self) -> Result { + let path = self.home.node().join(node::ROUTING_DB_FILE); + let router = routing::Table::reader(path)?; + + Ok(router) + } + /// Return the path to the keys folder. pub fn keys(&self) -> PathBuf { self.home.keys()