diff --git a/crates/radicle-tauri/src/commands/cob/patch.rs b/crates/radicle-tauri/src/commands/cob/patch.rs index 94d8716eb..613d0a0a9 100644 --- a/crates/radicle-tauri/src/commands/cob/patch.rs +++ b/crates/radicle-tauri/src/commands/cob/patch.rs @@ -2,25 +2,55 @@ use radicle::cob; use radicle::git; use radicle::identity; use radicle::patch; - use radicle::patch::{Action, TYPENAME}; + use radicle_types as types; +use radicle_types::cobs; +use radicle_types::domain::patch::models; +use radicle_types::domain::patch::service::Service; +use radicle_types::domain::patch::traits::PatchService; use radicle_types::error::Error; +use radicle_types::outbound::sqlite::Sqlite; use radicle_types::traits::cobs::Cobs; use radicle_types::traits::patch::Patches; use radicle_types::traits::patch::PatchesMut; +use radicle_types::traits::Profile; use crate::AppState; #[tauri::command] pub async fn list_patches( ctx: tauri::State<'_, AppState>, + sqlite_service: tauri::State<'_, Service>, rid: identity::RepoId, status: Option, skip: Option, take: Option, -) -> Result>, Error> { - ctx.list_patches(rid, status, skip, take) +) -> Result>, Error> { + let profile = ctx.profile(); + let cursor = skip.unwrap_or(0); + let take = take.unwrap_or(20); + let aliases = profile.aliases(); + let patches = match status { + None => sqlite_service.list(rid)?.collect::>(), + Some(s) => sqlite_service + .list_by_status(rid, s.into())? + .collect::>(), + }; + let more = cursor + take < patches.len(); + + let patches = patches + .into_iter() + .map(|(id, patch)| models::patch::Patch::new(id, &patch, &aliases)) + .skip(cursor) + .take(take) + .collect::>(); + + Ok::<_, Error>(cobs::PaginatedQuery { + cursor, + more, + content: patches, + }) } #[tauri::command] @@ -28,7 +58,7 @@ pub fn patch_by_id( ctx: tauri::State, rid: identity::RepoId, id: git::Oid, -) -> Result, Error> { +) -> Result, Error> { ctx.get_patch(rid, id) } @@ -37,7 +67,7 @@ pub fn revisions_by_patch( ctx: tauri::State, rid: identity::RepoId, id: git::Oid, -) -> Result>, Error> { +) -> Result>, Error> { ctx.revisions_by_patch(rid, id) } @@ -47,7 +77,7 @@ pub fn revision_by_patch_and_id( rid: identity::RepoId, id: git::Oid, revision_id: git::Oid, -) -> Result, Error> { +) -> Result, Error> { ctx.revision_by_id(rid, id, revision_id) } @@ -86,7 +116,7 @@ pub fn edit_draft_review( ctx: tauri::State, rid: identity::RepoId, cob_id: git::Oid, - edit: types::cobs::patch::ReviewEdit, + edit: models::patch::ReviewEdit, ) -> Result<(), Error> { ctx.edit_draft_review(rid, cob_id, edit) } @@ -97,7 +127,7 @@ pub fn get_draft_review( rid: identity::RepoId, cob_id: git::Oid, revision_id: patch::RevisionId, -) -> Option { +) -> Option { ctx.get_draft_review(rid, cob_id, revision_id) } @@ -106,9 +136,9 @@ pub fn edit_patch( ctx: tauri::State, rid: identity::RepoId, cob_id: git::Oid, - action: types::cobs::patch::Action, - opts: types::cobs::CobOptions, -) -> Result { + action: models::patch::Action, + opts: cobs::CobOptions, +) -> Result { ctx.edit_patch(rid, cob_id, action, opts) } diff --git a/crates/radicle-tauri/src/lib.rs b/crates/radicle-tauri/src/lib.rs index 80bf9a44b..bca736053 100644 --- a/crates/radicle-tauri/src/lib.rs +++ b/crates/radicle-tauri/src/lib.rs @@ -2,6 +2,7 @@ mod commands; use tauri::{Emitter, Manager}; +use radicle::cob::cache::COBS_DB_FILE; use radicle::node::{Handle, NOTIFICATIONS_DB_FILE}; use radicle::Node; @@ -39,6 +40,10 @@ pub fn run() { )?; let inbox_service = domain::inbox::service::Service::new(inbox_db); + let patch_db = + radicle_types::outbound::sqlite::Sqlite::reader(profile.cobs().join(COBS_DB_FILE))?; + let patch_service = domain::patch::service::Service::new(patch_db); + let events_handler = app.handle().clone(); let node_handler = app.handle().clone(); @@ -46,6 +51,7 @@ pub fn run() { let node_status = node.clone(); app.manage(inbox_service); + app.manage(patch_service); app.manage(AppState { profile }); tauri::async_runtime::spawn(async move { diff --git a/crates/radicle-types/src/cobs/patch.rs b/crates/radicle-types/src/cobs/patch.rs index 96486d8c4..8b1378917 100644 --- a/crates/radicle-types/src/cobs/patch.rs +++ b/crates/radicle-types/src/cobs/patch.rs @@ -1,500 +1 @@ -use std::collections::BTreeMap; -use std::collections::BTreeSet; -use radicle::node::AliasStore; -use serde::{Deserialize, Serialize}; -use ts_rs::TS; - -use radicle::cob; -use radicle::git; -use radicle::identity; -use radicle::patch; - -use crate::cobs; - -#[derive(Debug, TS, Serialize)] -#[ts(export)] -#[ts(export_to = "cob/patch/")] -#[serde(rename_all = "camelCase")] -pub struct Patch { - id: String, - author: cobs::Author, - title: String, - #[ts(as = "String")] - base: git::Oid, - #[ts(as = "String")] - head: git::Oid, - state: State, - assignees: Vec, - #[ts(as = "Vec")] - labels: Vec, - #[ts(type = "number")] - timestamp: cob::Timestamp, - revision_count: usize, -} - -impl Patch { - pub fn new(id: patch::PatchId, patch: &patch::Patch, aliases: &impl AliasStore) -> Self { - Self { - id: id.to_string(), - author: cobs::Author::new(patch.author().id(), aliases), - title: patch.title().to_string(), - state: patch.state().clone().into(), - base: *patch.base(), - head: *patch.head(), - assignees: patch - .assignees() - .map(|did| cobs::Author::new(&did, aliases)) - .collect::>(), - labels: patch.labels().cloned().collect::>(), - timestamp: patch.timestamp(), - revision_count: patch.revisions().count(), - } - } - - pub fn timestamp(&self) -> u64 { - self.timestamp.as_millis() - } -} - -#[derive(Debug, Serialize, Deserialize, TS)] -#[serde(rename_all = "camelCase", tag = "status")] -#[ts(export)] -#[ts(export_to = "cob/patch/")] -pub enum State { - Draft, - Open { - #[serde(skip_serializing_if = "Vec::is_empty")] - #[serde(default)] - #[ts(as = "Option>", optional)] - conflicts: Vec<(patch::RevisionId, git::Oid)>, - }, - Archived, - Merged { - #[ts(as = "String")] - revision: patch::RevisionId, - #[ts(as = "String")] - commit: git::Oid, - }, -} - -impl From for patch::State { - fn from(value: State) -> Self { - match value { - State::Archived => Self::Archived, - State::Draft => Self::Draft, - State::Merged { revision, commit } => Self::Merged { revision, commit }, - State::Open { conflicts } => Self::Open { conflicts }, - } - } -} - -impl From for State { - fn from(value: patch::State) -> Self { - match value { - patch::State::Archived => Self::Archived, - patch::State::Draft => Self::Draft, - patch::State::Merged { revision, commit } => Self::Merged { revision, commit }, - patch::State::Open { conflicts } => Self::Open { conflicts }, - } - } -} - -#[derive(Serialize, Deserialize, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export)] -#[ts(export_to = "cob/patch/")] -pub struct ReviewEdit { - #[ts(as = "String")] - pub review_id: cob::patch::ReviewId, - #[serde(default, skip_serializing_if = "Option::is_none")] - #[ts(optional)] - pub verdict: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - #[ts(optional)] - pub summary: Option, - #[ts(as = "Option>", optional)] - pub labels: Vec, -} - -#[derive(Serialize, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export)] -#[ts(export_to = "cob/patch/")] -pub struct Revision { - #[ts(as = "String")] - id: patch::RevisionId, - author: cobs::Author, - description: Vec, - #[ts(as = "String")] - base: git::Oid, - #[ts(as = "String")] - head: git::Oid, - #[ts(as = "Option<_>", optional)] - reviews: Vec, - #[ts(type = "number")] - timestamp: cob::common::Timestamp, - #[ts(as = "Option<_>", optional)] - discussion: Vec>, - #[ts(as = "Option<_>", optional)] - reactions: Vec, -} - -impl Revision { - pub fn new(value: cob::patch::Revision, aliases: &impl AliasStore) -> Self { - Self { - id: value.id(), - author: cobs::Author::new(value.author().id(), aliases), - description: value - .edits() - .map(|e| Edit::new(e, aliases)) - .collect::>(), - base: *value.base(), - head: value.head(), - reviews: value - .reviews() - .map(|(_, r)| Review::new(r.clone(), aliases)) - .collect::>(), - timestamp: value.timestamp(), - discussion: value - .discussion() - .comments() - .map(|(id, c)| { - cobs::thread::Comment::::new( - *id, - c.clone(), - aliases, - ) - }) - .collect::>(), - reactions: value - .reactions() - .iter() - .flat_map(|(location, reactions)| { - let reaction_by_author = reactions.iter().fold( - BTreeMap::new(), - |mut acc: BTreeMap<&cob::Reaction, Vec<_>>, (author, emoji)| { - acc.entry(emoji).or_default().push(author); - acc - }, - ); - reaction_by_author - .into_iter() - .map(|(emoji, authors)| { - cobs::thread::Reaction::new( - *emoji, - authors.into_iter().map(Into::into).collect::>(), - location - .as_ref() - .map(|l| cobs::thread::CodeLocation::new(l.clone())), - aliases, - ) - }) - .collect::>() - }) - .collect::>(), - } - } -} - -#[derive(TS, Serialize)] -#[serde(rename_all = "camelCase")] -#[ts(export)] -#[ts(export_to = "cob/patch/")] -pub struct Edit { - pub author: cobs::Author, - #[ts(type = "number")] - pub timestamp: cob::common::Timestamp, - pub body: String, - #[ts(as = "Option<_>", optional)] - pub embeds: Vec, -} - -impl Edit { - pub fn new(edit: &cob::thread::Edit, aliases: &impl AliasStore) -> Self { - Self { - author: cobs::Author::new(&edit.author.into(), aliases), - timestamp: edit.timestamp, - body: edit.body.clone(), - embeds: edit - .embeds - .iter() - .cloned() - .map(|e| e.into()) - .collect::>(), - } - } -} - -#[derive(Serialize, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export)] -#[ts(export_to = "cob/patch/")] -pub struct Review { - #[ts(as = "String")] - id: cob::patch::ReviewId, - author: cobs::Author, - #[serde(default, skip_serializing_if = "Option::is_none")] - #[ts(optional)] - verdict: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - #[ts(optional)] - summary: Option, - comments: Vec>, - #[ts(type = "number")] - timestamp: cob::common::Timestamp, -} - -impl Review { - pub fn new(review: cob::patch::Review, aliases: &impl AliasStore) -> Self { - Self { - id: review.id(), - author: cobs::Author::new(&review.author().id, aliases), - verdict: review.verdict().map(|v| v.into()), - summary: review.summary().map(|s| s.to_string()), - comments: review - .comments() - .map(|(id, c)| { - cobs::thread::Comment::::new( - *id, - c.clone(), - aliases, - ) - }) - .collect::>(), - timestamp: review.timestamp(), - } - } -} - -#[derive(Debug, Clone, Serialize, Deserialize, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export)] -#[ts(export_to = "cob/patch/")] -pub enum Verdict { - Accept, - Reject, -} - -impl From for Verdict { - fn from(value: cob::patch::Verdict) -> Self { - match value { - cob::patch::Verdict::Accept => Self::Accept, - cob::patch::Verdict::Reject => Self::Reject, - } - } -} - -impl From for cob::patch::Verdict { - fn from(value: Verdict) -> Self { - match value { - Verdict::Accept => Self::Accept, - Verdict::Reject => Self::Reject, - } - } -} - -#[derive(Debug, Serialize, Deserialize, TS)] -#[serde(tag = "type", rename_all = "camelCase")] -#[ts(export)] -#[ts(export_to = "cob/patch/")] -pub enum Action { - #[serde(rename = "edit")] - Edit { - title: String, - #[ts(as = "String")] - target: patch::MergeTarget, - }, - #[serde(rename = "label")] - Label { - #[ts(as = "Vec")] - labels: BTreeSet, - }, - #[serde(rename = "lifecycle")] - Lifecycle { - #[ts(type = "{ status: 'draft' | 'open' | 'archived' }")] - state: patch::Lifecycle, - }, - #[serde(rename = "assign")] - Assign { - #[ts(as = "Vec")] - assignees: BTreeSet, - }, - #[serde(rename = "merge")] - Merge { - #[ts(as = "String")] - revision: patch::RevisionId, - #[ts(as = "String")] - commit: git::Oid, - }, - - #[serde(rename = "review")] - Review { - #[ts(as = "String")] - revision: patch::RevisionId, - #[serde(default, skip_serializing_if = "Option::is_none")] - #[ts(optional)] - summary: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - #[ts(optional)] - verdict: Option, - #[serde(default, skip_serializing_if = "Vec::is_empty")] - #[ts(as = "Option>", optional)] - labels: Vec, - }, - #[serde(rename = "review.edit")] - ReviewEdit { - #[ts(as = "String")] - review: patch::ReviewId, - #[serde(default, skip_serializing_if = "Option::is_none")] - #[ts(optional)] - summary: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - #[ts(optional)] - verdict: Option, - #[serde(default, skip_serializing_if = "Vec::is_empty")] - #[ts(as = "Option>", optional)] - labels: Vec, - }, - #[serde(rename = "review.redact")] - ReviewRedact { - #[ts(as = "String")] - review: patch::ReviewId, - }, - #[serde(rename = "review.comment")] - ReviewComment { - #[ts(as = "String")] - review: patch::ReviewId, - body: String, - #[serde(default, skip_serializing_if = "Option::is_none")] - #[ts(optional)] - location: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - #[ts(as = "Option", optional)] - reply_to: Option, - #[serde(default, skip_serializing_if = "Vec::is_empty")] - #[ts(as = "Option<_>", optional)] - embeds: Vec, - }, - #[serde(rename = "review.comment.edit")] - ReviewCommentEdit { - #[ts(as = "String")] - review: patch::ReviewId, - #[ts(as = "String")] - comment: cob::EntryId, - body: String, - #[ts(as = "Option<_>", optional)] - embeds: Vec, - }, - #[serde(rename = "review.comment.redact")] - ReviewCommentRedact { - #[ts(as = "String")] - review: patch::ReviewId, - #[ts(as = "String")] - comment: cob::EntryId, - }, - #[serde(rename = "review.comment.react")] - ReviewCommentReact { - #[ts(as = "String")] - review: patch::ReviewId, - #[ts(as = "String")] - comment: cob::EntryId, - #[ts(as = "String")] - reaction: cob::Reaction, - active: bool, - }, - #[serde(rename = "review.comment.resolve")] - ReviewCommentResolve { - #[ts(as = "String")] - review: patch::ReviewId, - #[ts(as = "String")] - comment: cob::EntryId, - }, - #[serde(rename = "review.comment.unresolve")] - ReviewCommentUnresolve { - #[ts(as = "String")] - review: patch::ReviewId, - #[ts(as = "String")] - comment: cob::EntryId, - }, - - #[serde(rename = "revision")] - Revision { - description: String, - #[ts(as = "String")] - base: git::Oid, - #[ts(as = "String")] - oid: git::Oid, - #[serde(default, skip_serializing_if = "BTreeSet::is_empty")] - #[ts(as = "Option>", optional)] - resolves: BTreeSet<(cob::EntryId, cob::thread::CommentId)>, - }, - #[serde(rename = "revision.edit")] - RevisionEdit { - #[ts(as = "String")] - revision: patch::RevisionId, - description: String, - #[serde(default, skip_serializing_if = "Vec::is_empty")] - #[ts(as = "Option<_>", optional)] - embeds: Vec, - }, - #[serde(rename = "revision.react")] - RevisionReact { - #[ts(as = "String")] - revision: patch::RevisionId, - #[serde(default, skip_serializing_if = "Option::is_none")] - #[ts(optional)] - location: Option, - #[ts(as = "String")] - reaction: cob::Reaction, - active: bool, - }, - #[serde(rename = "revision.redact")] - RevisionRedact { - #[ts(as = "String")] - revision: patch::RevisionId, - }, - #[serde(rename_all = "camelCase")] - #[serde(rename = "revision.comment")] - RevisionComment { - #[ts(as = "String")] - revision: patch::RevisionId, - #[serde(default, skip_serializing_if = "Option::is_none")] - #[ts(optional)] - location: Option, - body: String, - #[serde(default, skip_serializing_if = "Option::is_none")] - #[ts(as = "Option", optional)] - reply_to: Option, - #[serde(default, skip_serializing_if = "Vec::is_empty")] - #[ts(as = "Option<_>", optional)] - embeds: Vec, - }, - #[serde(rename = "revision.comment.edit")] - RevisionCommentEdit { - #[ts(as = "String")] - revision: patch::RevisionId, - #[ts(as = "String")] - comment: cob::thread::CommentId, - body: String, - #[ts(as = "Option<_>", optional)] - embeds: Vec, - }, - #[serde(rename = "revision.comment.redact")] - RevisionCommentRedact { - #[ts(as = "String")] - revision: patch::RevisionId, - #[ts(as = "String")] - comment: cob::thread::CommentId, - }, - #[serde(rename = "revision.comment.react")] - RevisionCommentReact { - #[ts(as = "String")] - revision: patch::RevisionId, - #[ts(as = "String")] - comment: cob::thread::CommentId, - #[ts(as = "String")] - reaction: cob::Reaction, - active: bool, - }, -} diff --git a/crates/radicle-types/src/cobs/thread.rs b/crates/radicle-types/src/cobs/thread.rs index 5a862edf7..89a1f7eeb 100644 --- a/crates/radicle-types/src/cobs/thread.rs +++ b/crates/radicle-types/src/cobs/thread.rs @@ -5,6 +5,7 @@ use radicle::node::AliasStore; use radicle::{cob, git, identity}; use crate::cobs; +use crate::domain::patch::models; #[derive(TS, Serialize, Deserialize)] #[ts(export)] @@ -39,7 +40,7 @@ pub struct Comment { #[ts(as = "String")] id: cob::thread::CommentId, author: cobs::Author, - edits: Vec, + edits: Vec, reactions: Vec, #[ts(as = "Option", optional)] reply_to: Option, @@ -62,7 +63,7 @@ impl Comment { author: cobs::Author::new(&comment.author().into(), aliases), edits: comment .edits() - .map(|e| cobs::patch::Edit::new(e, aliases)) + .map(|e| models::patch::Edit::new(e, aliases)) .collect::>(), reactions: comment .reactions() @@ -100,7 +101,7 @@ impl Comment { author: cobs::Author::new(&comment.author().into(), aliases), edits: comment .edits() - .map(|e| cobs::patch::Edit::new(e, aliases)) + .map(|e| models::patch::Edit::new(e, aliases)) .collect::>(), reactions: comment .reactions() diff --git a/crates/radicle-types/src/domain.rs b/crates/radicle-types/src/domain.rs index 730098e4c..b91d42240 100644 --- a/crates/radicle-types/src/domain.rs +++ b/crates/radicle-types/src/domain.rs @@ -1 +1,2 @@ pub mod inbox; +pub mod patch; diff --git a/crates/radicle-types/src/domain/inbox/models/notification.rs b/crates/radicle-types/src/domain/inbox/models/notification.rs index 2b11b2847..4062c680f 100644 --- a/crates/radicle-types/src/domain/inbox/models/notification.rs +++ b/crates/radicle-types/src/domain/inbox/models/notification.rs @@ -9,6 +9,7 @@ use ts_rs::TS; use crate::cobs::stream::{self, CobStream}; use crate::cobs::{self, Author}; +use crate::domain::patch::models; #[derive(Serialize, Deserialize, TS)] #[serde(rename_all = "camelCase")] @@ -157,8 +158,8 @@ pub struct Patch { #[ts(type = "number")] pub timestamp: localtime::LocalTime, pub title: String, - pub status: cobs::patch::State, - pub actions: Vec>, + pub status: models::patch::State, + pub actions: Vec>, } /// Type of notification. diff --git a/crates/radicle-types/src/domain/patch.rs b/crates/radicle-types/src/domain/patch.rs new file mode 100644 index 000000000..838215019 --- /dev/null +++ b/crates/radicle-types/src/domain/patch.rs @@ -0,0 +1,3 @@ +pub mod models; +pub mod service; +pub mod traits; diff --git a/crates/radicle-types/src/domain/patch/models.rs b/crates/radicle-types/src/domain/patch/models.rs new file mode 100644 index 000000000..e268c5ac4 --- /dev/null +++ b/crates/radicle-types/src/domain/patch/models.rs @@ -0,0 +1 @@ +pub mod patch; diff --git a/crates/radicle-types/src/domain/patch/models/patch.rs b/crates/radicle-types/src/domain/patch/models/patch.rs new file mode 100644 index 000000000..b931f9cb6 --- /dev/null +++ b/crates/radicle-types/src/domain/patch/models/patch.rs @@ -0,0 +1,510 @@ +use std::collections::BTreeMap; +use std::collections::BTreeSet; + +use radicle::node::AliasStore; +use serde::{Deserialize, Serialize}; +use ts_rs::TS; + +use radicle::cob; +use radicle::git; +use radicle::identity; +use radicle::patch; + +use crate::cobs; + +#[derive(Debug, TS, Serialize)] +#[ts(export)] +#[ts(export_to = "cob/patch/")] +#[serde(rename_all = "camelCase")] +pub struct Patch { + id: String, + author: cobs::Author, + title: String, + #[ts(as = "String")] + base: git::Oid, + #[ts(as = "String")] + head: git::Oid, + state: State, + assignees: Vec, + #[ts(as = "Vec")] + labels: Vec, + #[ts(type = "number")] + timestamp: cob::Timestamp, + revision_count: usize, +} + +#[derive(Debug, thiserror::Error)] +pub enum ListPatchesError { + #[error(transparent)] + Sqlite(#[from] sqlite::Error), + + #[error(transparent)] + Unknown(#[from] anyhow::Error), + // to be extended as new error scenarios are introduced +} + +impl Patch { + pub fn new(id: patch::PatchId, patch: &patch::Patch, aliases: &impl AliasStore) -> Self { + Self { + id: id.to_string(), + author: cobs::Author::new(patch.author().id(), aliases), + title: patch.title().to_string(), + state: patch.state().clone().into(), + base: *patch.base(), + head: *patch.head(), + assignees: patch + .assignees() + .map(|did| cobs::Author::new(&did, aliases)) + .collect::>(), + labels: patch.labels().cloned().collect::>(), + timestamp: patch.timestamp(), + revision_count: patch.revisions().count(), + } + } + + pub fn timestamp(&self) -> u64 { + self.timestamp.as_millis() + } +} + +#[derive(Debug, Serialize, Deserialize, TS)] +#[serde(rename_all = "camelCase", tag = "status")] +#[ts(export)] +#[ts(export_to = "cob/patch/")] +pub enum State { + Draft, + Open { + #[serde(skip_serializing_if = "Vec::is_empty")] + #[serde(default)] + #[ts(as = "Option>", optional)] + conflicts: Vec<(patch::RevisionId, git::Oid)>, + }, + Archived, + Merged { + #[ts(as = "String")] + revision: patch::RevisionId, + #[ts(as = "String")] + commit: git::Oid, + }, +} + +impl From for patch::State { + fn from(value: State) -> Self { + match value { + State::Archived => Self::Archived, + State::Draft => Self::Draft, + State::Merged { revision, commit } => Self::Merged { revision, commit }, + State::Open { conflicts } => Self::Open { conflicts }, + } + } +} + +impl From for State { + fn from(value: patch::State) -> Self { + match value { + patch::State::Archived => Self::Archived, + patch::State::Draft => Self::Draft, + patch::State::Merged { revision, commit } => Self::Merged { revision, commit }, + patch::State::Open { conflicts } => Self::Open { conflicts }, + } + } +} + +#[derive(Serialize, Deserialize, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export)] +#[ts(export_to = "cob/patch/")] +pub struct ReviewEdit { + #[ts(as = "String")] + pub review_id: cob::patch::ReviewId, + #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(optional)] + pub verdict: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(optional)] + pub summary: Option, + #[ts(as = "Option>", optional)] + pub labels: Vec, +} + +#[derive(Serialize, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export)] +#[ts(export_to = "cob/patch/")] +pub struct Revision { + #[ts(as = "String")] + id: patch::RevisionId, + author: cobs::Author, + description: Vec, + #[ts(as = "String")] + base: git::Oid, + #[ts(as = "String")] + head: git::Oid, + #[ts(as = "Option<_>", optional)] + reviews: Vec, + #[ts(type = "number")] + timestamp: cob::common::Timestamp, + #[ts(as = "Option<_>", optional)] + discussion: Vec>, + #[ts(as = "Option<_>", optional)] + reactions: Vec, +} + +impl Revision { + pub fn new(value: cob::patch::Revision, aliases: &impl AliasStore) -> Self { + Self { + id: value.id(), + author: cobs::Author::new(value.author().id(), aliases), + description: value + .edits() + .map(|e| Edit::new(e, aliases)) + .collect::>(), + base: *value.base(), + head: value.head(), + reviews: value + .reviews() + .map(|(_, r)| Review::new(r.clone(), aliases)) + .collect::>(), + timestamp: value.timestamp(), + discussion: value + .discussion() + .comments() + .map(|(id, c)| { + cobs::thread::Comment::::new( + *id, + c.clone(), + aliases, + ) + }) + .collect::>(), + reactions: value + .reactions() + .iter() + .flat_map(|(location, reactions)| { + let reaction_by_author = reactions.iter().fold( + BTreeMap::new(), + |mut acc: BTreeMap<&cob::Reaction, Vec<_>>, (author, emoji)| { + acc.entry(emoji).or_default().push(author); + acc + }, + ); + reaction_by_author + .into_iter() + .map(|(emoji, authors)| { + cobs::thread::Reaction::new( + *emoji, + authors.into_iter().map(Into::into).collect::>(), + location + .as_ref() + .map(|l| cobs::thread::CodeLocation::new(l.clone())), + aliases, + ) + }) + .collect::>() + }) + .collect::>(), + } + } +} + +#[derive(TS, Serialize)] +#[serde(rename_all = "camelCase")] +#[ts(export)] +#[ts(export_to = "cob/patch/")] +pub struct Edit { + pub author: cobs::Author, + #[ts(type = "number")] + pub timestamp: cob::common::Timestamp, + pub body: String, + #[ts(as = "Option<_>", optional)] + pub embeds: Vec, +} + +impl Edit { + pub fn new(edit: &cob::thread::Edit, aliases: &impl AliasStore) -> Self { + Self { + author: cobs::Author::new(&edit.author.into(), aliases), + timestamp: edit.timestamp, + body: edit.body.clone(), + embeds: edit + .embeds + .iter() + .cloned() + .map(|e| e.into()) + .collect::>(), + } + } +} + +#[derive(Serialize, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export)] +#[ts(export_to = "cob/patch/")] +pub struct Review { + #[ts(as = "String")] + id: cob::patch::ReviewId, + author: cobs::Author, + #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(optional)] + verdict: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(optional)] + summary: Option, + comments: Vec>, + #[ts(type = "number")] + timestamp: cob::common::Timestamp, +} + +impl Review { + pub fn new(review: cob::patch::Review, aliases: &impl AliasStore) -> Self { + Self { + id: review.id(), + author: cobs::Author::new(&review.author().id, aliases), + verdict: review.verdict().map(|v| v.into()), + summary: review.summary().map(|s| s.to_string()), + comments: review + .comments() + .map(|(id, c)| { + cobs::thread::Comment::::new( + *id, + c.clone(), + aliases, + ) + }) + .collect::>(), + timestamp: review.timestamp(), + } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export)] +#[ts(export_to = "cob/patch/")] +pub enum Verdict { + Accept, + Reject, +} + +impl From for Verdict { + fn from(value: cob::patch::Verdict) -> Self { + match value { + cob::patch::Verdict::Accept => Self::Accept, + cob::patch::Verdict::Reject => Self::Reject, + } + } +} + +impl From for cob::patch::Verdict { + fn from(value: Verdict) -> Self { + match value { + Verdict::Accept => Self::Accept, + Verdict::Reject => Self::Reject, + } + } +} + +#[derive(Debug, Serialize, Deserialize, TS)] +#[serde(tag = "type", rename_all = "camelCase")] +#[ts(export)] +#[ts(export_to = "cob/patch/")] +pub enum Action { + #[serde(rename = "edit")] + Edit { + title: String, + #[ts(as = "String")] + target: patch::MergeTarget, + }, + #[serde(rename = "label")] + Label { + #[ts(as = "Vec")] + labels: BTreeSet, + }, + #[serde(rename = "lifecycle")] + Lifecycle { + #[ts(type = "{ status: 'draft' | 'open' | 'archived' }")] + state: patch::Lifecycle, + }, + #[serde(rename = "assign")] + Assign { + #[ts(as = "Vec")] + assignees: BTreeSet, + }, + #[serde(rename = "merge")] + Merge { + #[ts(as = "String")] + revision: patch::RevisionId, + #[ts(as = "String")] + commit: git::Oid, + }, + + #[serde(rename = "review")] + Review { + #[ts(as = "String")] + revision: patch::RevisionId, + #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(optional)] + summary: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(optional)] + verdict: Option, + #[serde(default, skip_serializing_if = "Vec::is_empty")] + #[ts(as = "Option>", optional)] + labels: Vec, + }, + #[serde(rename = "review.edit")] + ReviewEdit { + #[ts(as = "String")] + review: patch::ReviewId, + #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(optional)] + summary: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(optional)] + verdict: Option, + #[serde(default, skip_serializing_if = "Vec::is_empty")] + #[ts(as = "Option>", optional)] + labels: Vec, + }, + #[serde(rename = "review.redact")] + ReviewRedact { + #[ts(as = "String")] + review: patch::ReviewId, + }, + #[serde(rename = "review.comment")] + ReviewComment { + #[ts(as = "String")] + review: patch::ReviewId, + body: String, + #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(optional)] + location: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(as = "Option", optional)] + reply_to: Option, + #[serde(default, skip_serializing_if = "Vec::is_empty")] + #[ts(as = "Option<_>", optional)] + embeds: Vec, + }, + #[serde(rename = "review.comment.edit")] + ReviewCommentEdit { + #[ts(as = "String")] + review: patch::ReviewId, + #[ts(as = "String")] + comment: cob::EntryId, + body: String, + #[ts(as = "Option<_>", optional)] + embeds: Vec, + }, + #[serde(rename = "review.comment.redact")] + ReviewCommentRedact { + #[ts(as = "String")] + review: patch::ReviewId, + #[ts(as = "String")] + comment: cob::EntryId, + }, + #[serde(rename = "review.comment.react")] + ReviewCommentReact { + #[ts(as = "String")] + review: patch::ReviewId, + #[ts(as = "String")] + comment: cob::EntryId, + #[ts(as = "String")] + reaction: cob::Reaction, + active: bool, + }, + #[serde(rename = "review.comment.resolve")] + ReviewCommentResolve { + #[ts(as = "String")] + review: patch::ReviewId, + #[ts(as = "String")] + comment: cob::EntryId, + }, + #[serde(rename = "review.comment.unresolve")] + ReviewCommentUnresolve { + #[ts(as = "String")] + review: patch::ReviewId, + #[ts(as = "String")] + comment: cob::EntryId, + }, + + #[serde(rename = "revision")] + Revision { + description: String, + #[ts(as = "String")] + base: git::Oid, + #[ts(as = "String")] + oid: git::Oid, + #[serde(default, skip_serializing_if = "BTreeSet::is_empty")] + #[ts(as = "Option>", optional)] + resolves: BTreeSet<(cob::EntryId, cob::thread::CommentId)>, + }, + #[serde(rename = "revision.edit")] + RevisionEdit { + #[ts(as = "String")] + revision: patch::RevisionId, + description: String, + #[serde(default, skip_serializing_if = "Vec::is_empty")] + #[ts(as = "Option<_>", optional)] + embeds: Vec, + }, + #[serde(rename = "revision.react")] + RevisionReact { + #[ts(as = "String")] + revision: patch::RevisionId, + #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(optional)] + location: Option, + #[ts(as = "String")] + reaction: cob::Reaction, + active: bool, + }, + #[serde(rename = "revision.redact")] + RevisionRedact { + #[ts(as = "String")] + revision: patch::RevisionId, + }, + #[serde(rename_all = "camelCase")] + #[serde(rename = "revision.comment")] + RevisionComment { + #[ts(as = "String")] + revision: patch::RevisionId, + #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(optional)] + location: Option, + body: String, + #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(as = "Option", optional)] + reply_to: Option, + #[serde(default, skip_serializing_if = "Vec::is_empty")] + #[ts(as = "Option<_>", optional)] + embeds: Vec, + }, + #[serde(rename = "revision.comment.edit")] + RevisionCommentEdit { + #[ts(as = "String")] + revision: patch::RevisionId, + #[ts(as = "String")] + comment: cob::thread::CommentId, + body: String, + #[ts(as = "Option<_>", optional)] + embeds: Vec, + }, + #[serde(rename = "revision.comment.redact")] + RevisionCommentRedact { + #[ts(as = "String")] + revision: patch::RevisionId, + #[ts(as = "String")] + comment: cob::thread::CommentId, + }, + #[serde(rename = "revision.comment.react")] + RevisionCommentReact { + #[ts(as = "String")] + revision: patch::RevisionId, + #[ts(as = "String")] + comment: cob::thread::CommentId, + #[ts(as = "String")] + reaction: cob::Reaction, + active: bool, + }, +} diff --git a/crates/radicle-types/src/domain/patch/service.rs b/crates/radicle-types/src/domain/patch/service.rs new file mode 100644 index 000000000..ad1c7832e --- /dev/null +++ b/crates/radicle-types/src/domain/patch/service.rs @@ -0,0 +1,45 @@ +use radicle::identity; +use radicle::patch; +use radicle::patch::Patch; +use radicle::patch::PatchId; + +use crate::domain::patch::traits::{PatchService, PatchStorage}; + +#[derive(Debug, Clone)] +pub struct Service +where + I: PatchStorage, +{ + patches: I, +} + +impl Service +where + I: PatchStorage, +{ + pub fn new(patches: I) -> Self { + Self { patches } + } +} + +impl PatchService for Service +where + I: PatchStorage, +{ + fn list( + &self, + rid: identity::RepoId, + ) -> Result, super::models::patch::ListPatchesError> + { + self.patches.list(rid) + } + + fn list_by_status( + &self, + rid: identity::RepoId, + status: patch::Status, + ) -> Result, super::models::patch::ListPatchesError> + { + self.patches.list_by_status(rid, status) + } +} diff --git a/crates/radicle-types/src/domain/patch/traits.rs b/crates/radicle-types/src/domain/patch/traits.rs new file mode 100644 index 000000000..a608807c6 --- /dev/null +++ b/crates/radicle-types/src/domain/patch/traits.rs @@ -0,0 +1,32 @@ +use radicle::identity; +use radicle::patch; +use radicle::patch::Patch; +use radicle::patch::PatchId; + +use crate::domain::patch::models; + +pub trait PatchStorage { + fn list( + &self, + rid: identity::RepoId, + ) -> Result, models::patch::ListPatchesError>; + + fn list_by_status( + &self, + rid: identity::RepoId, + status: patch::Status, + ) -> Result, models::patch::ListPatchesError>; +} + +pub trait PatchService { + fn list( + &self, + rid: identity::RepoId, + ) -> Result, models::patch::ListPatchesError>; + + fn list_by_status( + &self, + rid: identity::RepoId, + status: patch::Status, + ) -> Result, models::patch::ListPatchesError>; +} diff --git a/crates/radicle-types/src/error.rs b/crates/radicle-types/src/error.rs index 7c70866a6..62053ccd8 100644 --- a/crates/radicle-types/src/error.rs +++ b/crates/radicle-types/src/error.rs @@ -17,6 +17,10 @@ pub enum Error { #[from] crate::domain::inbox::models::notification::ListNotificationsError, ), + /// CobStore error. + #[error(transparent)] + ListPatchesError(#[from] crate::domain::patch::models::patch::ListPatchesError), + /// CobStore error. #[error(transparent)] CobStore(#[from] radicle::cob::store::Error), diff --git a/crates/radicle-types/src/outbound/sqlite.rs b/crates/radicle-types/src/outbound/sqlite.rs index c94e77e79..e3149811c 100644 --- a/crates/radicle-types/src/outbound/sqlite.rs +++ b/crates/radicle-types/src/outbound/sqlite.rs @@ -1,13 +1,17 @@ use std::collections::BTreeMap; use std::path::Path; +use std::str::FromStr; use std::sync::Arc; use std::time; +use radicle::patch::{Patch, PatchId, Status}; use radicle::{git, identity}; use sqlite as sql; use crate::domain::inbox::models::notification; use crate::domain::inbox::traits::InboxStorage; +use crate::domain::patch::models::patch::ListPatchesError; +use crate::domain::patch::traits::PatchStorage; use crate::error::Error; pub struct Sqlite { @@ -29,6 +33,57 @@ impl Sqlite { } } +impl PatchStorage for Sqlite { + fn list( + &self, + rid: identity::RepoId, + ) -> Result, ListPatchesError> { + let mut stmt = self.db.prepare( + "SELECT id, patch, ( + SELECT MIN(JSON_EXTRACT(revision.value, '$.timestamp')) + FROM JSON_EACH(JSON_EXTRACT(p.patch, '$.revisions')) AS revision + ) AS last_revision_timestamp + FROM patches AS p + WHERE repo = ?1 + ORDER BY last_revision_timestamp DESC; + ", + )?; + stmt.bind((1, &rid))?; + Ok(stmt.into_iter().filter_map(|row| { + let row = row.ok()?; + let id = PatchId::from_str(row.read::<&str, _>("id")).ok()?; + let patch = serde_json::from_str::(row.read::<&str, _>("patch")).ok()?; + Some((id, patch)) + })) + } + + fn list_by_status( + &self, + rid: identity::RepoId, + status: Status, + ) -> Result, ListPatchesError> { + let mut stmt = self.db.prepare( + "SELECT id, patch, ( + SELECT MIN(JSON_EXTRACT(revision.value, '$.timestamp')) + FROM JSON_EACH(JSON_EXTRACT(p.patch, '$.revisions')) AS revision + ) AS last_revision_timestamp + FROM patches AS p + WHERE repo = ?1 + AND patch->>'$.state.status' = ?2 + ORDER BY last_revision_timestamp DESC; + ", + )?; + stmt.bind((1, &rid))?; + stmt.bind((2, sql::Value::String(status.to_string())))?; + Ok(stmt.into_iter().filter_map(|row| { + let row = row.ok()?; + let id = PatchId::from_str(row.read::<&str, _>("id")).ok()?; + let patch = serde_json::from_str::(row.read::<&str, _>("patch")).ok()?; + Some((id, patch)) + })) + } +} + impl InboxStorage for Sqlite { fn counts_by_repo( &self, diff --git a/crates/radicle-types/src/traits/patch.rs b/crates/radicle-types/src/traits/patch.rs index b8bd442a4..c3e1ce754 100644 --- a/crates/radicle-types/src/traits/patch.rs +++ b/crates/radicle-types/src/traits/patch.rs @@ -5,59 +5,22 @@ use radicle::storage::ReadStorage; use radicle::{cob, git, identity, patch, Node}; use crate::cobs; +use crate::domain::patch::models; use crate::error::Error; use crate::traits::Profile; pub trait Patches: Profile { - fn list_patches( - &self, - rid: identity::RepoId, - status: Option, - skip: Option, - take: Option, - ) -> Result>, Error> { - let profile = self.profile(); - let cursor = skip.unwrap_or(0); - let take = take.unwrap_or(20); - let repo = profile.storage.repository(rid)?; - let aliases = &profile.aliases(); - let cache = profile.patches(&repo)?; - let patches = match status { - None => cache.list()?.collect::>(), - Some(s) => cache.list_by_status(&s.into())?.collect::>(), - }; - let more = cursor + take < patches.len(); - - let mut patches = patches - .into_iter() - .filter_map(|p| { - p.map(|(id, patch)| cobs::patch::Patch::new(id, &patch, aliases)) - .ok() - }) - .skip(cursor) - .take(take) - .collect::>(); - - patches.sort_by_key(|b| std::cmp::Reverse(b.timestamp())); - - Ok::<_, Error>(cobs::PaginatedQuery { - cursor, - more, - content: patches, - }) - } - fn get_patch( &self, rid: identity::RepoId, id: git::Oid, - ) -> Result, Error> { + ) -> Result, Error> { let profile = self.profile(); let repo = profile.storage.repository(rid)?; let patches = profile.patches(&repo)?; let patch = patches.get(&id.into())?; let aliases = &profile.aliases(); - let patches = patch.map(|patch| cobs::patch::Patch::new(id.into(), &patch, aliases)); + let patches = patch.map(|patch| models::patch::Patch::new(id.into(), &patch, aliases)); Ok::<_, Error>(patches) } @@ -66,7 +29,7 @@ pub trait Patches: Profile { &self, rid: identity::RepoId, id: git::Oid, - ) -> Result>, Error> { + ) -> Result>, Error> { let profile = self.profile(); let repo = profile.storage.repository(rid)?; let patches = profile.patches(&repo)?; @@ -75,7 +38,7 @@ pub trait Patches: Profile { patch .revisions() - .map(|(_, r)| cobs::patch::Revision::new(r.clone(), aliases)) + .map(|(_, r)| models::patch::Revision::new(r.clone(), aliases)) .collect::>() }); @@ -87,7 +50,7 @@ pub trait Patches: Profile { rid: identity::RepoId, id: git::Oid, revision_id: git::Oid, - ) -> Result, Error> { + ) -> Result, Error> { let profile = self.profile(); let repo = profile.storage.repository(rid)?; let patches = profile.patches(&repo)?; @@ -96,7 +59,7 @@ pub trait Patches: Profile { patch .revision(&revision_id.into()) - .map(|r| cobs::patch::Revision::new(r.clone(), aliases)) + .map(|r| models::patch::Revision::new(r.clone(), aliases)) }); Ok::<_, Error>(revision) @@ -108,9 +71,9 @@ pub trait PatchesMut: Profile { &self, rid: identity::RepoId, cob_id: git::Oid, - action: cobs::patch::Action, + action: models::patch::Action, opts: cobs::CobOptions, - ) -> Result { + ) -> Result { let profile = self.profile(); let mut node = Node::new(profile.socket()); let repo = profile.storage.repository(rid)?; @@ -120,7 +83,7 @@ pub trait PatchesMut: Profile { let mut patch = patches.get_mut(&cob_id.into())?; match action { - cobs::patch::Action::RevisionEdit { + models::patch::Action::RevisionEdit { revision, description, embeds, @@ -132,13 +95,13 @@ pub trait PatchesMut: Profile { &signer, )?; } - cobs::patch::Action::RevisionCommentRedact { revision, comment } => { + models::patch::Action::RevisionCommentRedact { revision, comment } => { patch.comment_redact(revision, comment, &signer)?; } - cobs::patch::Action::ReviewCommentRedact { review, comment } => { + models::patch::Action::ReviewCommentRedact { review, comment } => { patch.redact_review_comment(review, comment, &signer)?; } - cobs::patch::Action::ReviewCommentReact { + models::patch::Action::ReviewCommentReact { review, comment, reaction, @@ -146,16 +109,16 @@ pub trait PatchesMut: Profile { } => { patch.react_review_comment(review, comment, reaction, active, &signer)?; } - cobs::patch::Action::ReviewCommentResolve { review, comment } => { + models::patch::Action::ReviewCommentResolve { review, comment } => { patch.resolve_review_comment(review, comment, &signer)?; } - cobs::patch::Action::ReviewCommentUnresolve { review, comment } => { + models::patch::Action::ReviewCommentUnresolve { review, comment } => { patch.unresolve_review_comment(review, comment, &signer)?; } - cobs::patch::Action::Edit { title, target } => { + models::patch::Action::Edit { title, target } => { patch.edit(title, target, &signer)?; } - cobs::patch::Action::ReviewEdit { + models::patch::Action::ReviewEdit { review, summary, verdict, @@ -163,7 +126,7 @@ pub trait PatchesMut: Profile { } => { patch.review_edit(review, verdict.map(|v| v.into()), summary, labels, &signer)?; } - cobs::patch::Action::Review { + models::patch::Action::Review { revision, summary, verdict, @@ -177,10 +140,10 @@ pub trait PatchesMut: Profile { &signer, )?; } - cobs::patch::Action::ReviewRedact { review } => { + models::patch::Action::ReviewRedact { review } => { patch.redact_review(review, &signer)?; } - cobs::patch::Action::ReviewComment { + models::patch::Action::ReviewComment { review, body, location, @@ -196,7 +159,7 @@ pub trait PatchesMut: Profile { &signer, )?; } - cobs::patch::Action::ReviewCommentEdit { + models::patch::Action::ReviewCommentEdit { review, comment, body, @@ -210,16 +173,16 @@ pub trait PatchesMut: Profile { &signer, )?; } - cobs::patch::Action::Lifecycle { state } => { + models::patch::Action::Lifecycle { state } => { patch.lifecycle(state, &signer)?; } - cobs::patch::Action::Assign { assignees } => { + models::patch::Action::Assign { assignees } => { patch.assign(assignees, &signer)?; } - cobs::patch::Action::Label { labels } => { + models::patch::Action::Label { labels } => { patch.label(labels, &signer)?; } - cobs::patch::Action::RevisionReact { + models::patch::Action::RevisionReact { revision, reaction, location, @@ -233,7 +196,7 @@ pub trait PatchesMut: Profile { &signer, )?; } - cobs::patch::Action::RevisionComment { + models::patch::Action::RevisionComment { revision, location, body, @@ -249,7 +212,7 @@ pub trait PatchesMut: Profile { &signer, )?; } - cobs::patch::Action::RevisionCommentEdit { + models::patch::Action::RevisionCommentEdit { revision, comment, body, @@ -263,7 +226,7 @@ pub trait PatchesMut: Profile { &signer, )?; } - cobs::patch::Action::RevisionCommentReact { + models::patch::Action::RevisionCommentReact { revision, comment, reaction, @@ -271,13 +234,13 @@ pub trait PatchesMut: Profile { } => { patch.comment_react(revision, comment, reaction, active, &signer)?; } - cobs::patch::Action::RevisionRedact { revision } => { + models::patch::Action::RevisionRedact { revision } => { patch.redact(revision, &signer)?; } - cobs::patch::Action::Merge { .. } => { + models::patch::Action::Merge { .. } => { unimplemented!("We don't support merging of patches through the desktop") } - cobs::patch::Action::Revision { .. } => { + models::patch::Action::Revision { .. } => { unimplemented!("We don't support creating new revisions through the desktop") } } @@ -288,7 +251,7 @@ pub trait PatchesMut: Profile { } } - Ok::<_, Error>(cobs::patch::Patch::new(*patch.id(), &patch, &aliases)) + Ok::<_, Error>(models::patch::Patch::new(*patch.id(), &patch, &aliases)) } /// Gets the draft review of the local user for a specific patch revision in a repository. @@ -302,7 +265,7 @@ pub trait PatchesMut: Profile { rid: identity::RepoId, cob_id: git::Oid, revision_id: patch::RevisionId, - ) -> Option { + ) -> Option { let profile = self.profile(); let aliases = profile.aliases(); let repo = profile.storage.repository(rid).ok()?; @@ -315,7 +278,7 @@ pub trait PatchesMut: Profile { let review: Option = revision.and_then(|rev| rev.review_by(signer.public_key()).cloned()); - review.map(|r| cobs::patch::Review::new(r, &aliases)) + review.map(|r| models::patch::Review::new(r, &aliases)) } /// Edits a draft review for a specific patch revision in a repository. @@ -326,7 +289,7 @@ pub trait PatchesMut: Profile { &self, rid: identity::RepoId, cob_id: git::Oid, - edit: cobs::patch::ReviewEdit, + edit: models::patch::ReviewEdit, ) -> Result<(), Error> { let profile = self.profile(); let repo = profile.storage.repository(rid)?; diff --git a/crates/test-http-api/src/api.rs b/crates/test-http-api/src/api.rs index db5e33a29..118a1aa89 100644 --- a/crates/test-http-api/src/api.rs +++ b/crates/test-http-api/src/api.rs @@ -8,17 +8,21 @@ use axum::routing::post; use axum::Router; use hyper::header::CONTENT_TYPE; use hyper::Method; -use radicle_types::domain::inbox::models::notification::NotificationCount; use serde::{Deserialize, Serialize}; use tower_http::cors::{self, CorsLayer}; use radicle::{git, identity}; use radicle_types as types; +use radicle_types::cobs; use radicle_types::cobs::issue; use radicle_types::cobs::issue::NewIssue; -use radicle_types::cobs::patch; use radicle_types::cobs::CobOptions; +use radicle_types::domain::inbox::models::notification::NotificationCount; +use radicle_types::domain::patch::models; +use radicle_types::domain::patch::service::Service; +use radicle_types::domain::patch::traits::PatchService; use radicle_types::error::Error; +use radicle_types::outbound::sqlite::Sqlite; use radicle_types::traits::auth::Auth; use radicle_types::traits::cobs::Cobs; use radicle_types::traits::issue::{Issues, IssuesMut}; @@ -30,6 +34,7 @@ use radicle_types::traits::Profile; #[derive(Clone)] pub struct Context { profile: Arc, + patches: Arc>, } impl Auth for Context {} @@ -47,8 +52,8 @@ impl Profile for Context { } impl Context { - pub fn new(profile: Arc) -> Self { - Self { profile } + pub fn new(profile: Arc, patches: Arc>) -> Self { + Self { profile, patches } } } @@ -70,7 +75,7 @@ pub fn router(ctx: Context) -> Router { ) .route( "/activity_by_patch", - post(activity_patch_handler::), + post(activity_patch_handler::), ) .route("/get_diff", post(diff_handler)) .route("/list_issues", post(issues_handler)) @@ -345,9 +350,31 @@ async fn patches_handler( status, }): Json, ) -> impl IntoResponse { - let patches = ctx.list_patches(rid, status, skip, take)?; + let profile = ctx.profile; + let cursor = skip.unwrap_or(0); + let take = take.unwrap_or(20); + let aliases = profile.aliases(); + let patches = match status { + None => ctx.patches.list(rid)?.collect::>(), + Some(s) => ctx + .patches + .list_by_status(rid, s.into())? + .collect::>(), + }; + let more = cursor + take < patches.len(); - Ok::<_, Error>(Json(patches)) + let patches = patches + .into_iter() + .map(|(id, patch)| models::patch::Patch::new(id, &patch, &aliases)) + .skip(cursor) + .take(take) + .collect::>(); + + Ok::<_, Error>(Json(cobs::PaginatedQuery { + cursor, + more, + content: patches, + })) } #[derive(Serialize, Deserialize)] diff --git a/crates/test-http-api/src/lib.rs b/crates/test-http-api/src/lib.rs index 35d046a24..b08189b86 100644 --- a/crates/test-http-api/src/lib.rs +++ b/crates/test-http-api/src/lib.rs @@ -4,8 +4,11 @@ use std::sync::Arc; use axum::Router; use tokio::net::TcpListener; +use radicle::cob::cache::COBS_DB_FILE; use radicle::Profile; +use radicle_types::domain::patch::service::Service as PatchService; + mod api; #[derive(Debug, Clone)] @@ -25,7 +28,12 @@ pub async fn run(options: Options) -> anyhow::Result<()> { fn router(profile: Profile) -> anyhow::Result { let profile = Arc::new(profile); - let ctx = api::Context::new(profile); + + let patch_db = + radicle_types::outbound::sqlite::Sqlite::reader(profile.cobs().join(COBS_DB_FILE))?; + let patch_service = PatchService::new(patch_db); + + let ctx = api::Context::new(profile, Arc::new(patch_service)); Ok(api::router(ctx)) }