mirror of
https://seed.radicle.dev/rad:z4V1sjrXqjvFdnCUbxPFqd5p4DtH5.git
synced 2026-11-15 19:30:38 +08:00
Move issue pagination into the issue service
This commit is contained in:
@@ -17,8 +17,6 @@ use radicle_types::traits::issue::IssuesMut;
|
||||
|
||||
use crate::AppState;
|
||||
|
||||
use either::Either;
|
||||
|
||||
#[tauri::command]
|
||||
pub fn create_issue(
|
||||
ctx: tauri::State<AppState>,
|
||||
@@ -50,46 +48,10 @@ pub(crate) async fn list_issues(
|
||||
// None: return all issues, `skip` is ignored.
|
||||
take: Option<usize>,
|
||||
) -> Result<types::cobs::PaginatedQuery<Vec<types::cobs::issue::Issue>>, Error> {
|
||||
use types::cobs::query::IssueStatus;
|
||||
|
||||
let profile = ctx.profile();
|
||||
let aliases = profile.aliases();
|
||||
let cursor = skip.unwrap_or(0);
|
||||
|
||||
let issues = match status.unwrap_or_default() {
|
||||
IssueStatus::All => Either::Left(issue_service.list(rid)?),
|
||||
IssueStatus::Open => Either::Right(issue_service.list_by_status(rid, "open")?),
|
||||
IssueStatus::Closed => Either::Right(issue_service.list_by_status(rid, "closed")?),
|
||||
};
|
||||
|
||||
match take {
|
||||
None => {
|
||||
let content = issues
|
||||
.map(|(id, issue)| types::cobs::issue::Issue::summary(&id, &issue, &aliases))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
Ok::<_, Error>(types::cobs::PaginatedQuery {
|
||||
cursor: 0,
|
||||
more: false,
|
||||
content,
|
||||
})
|
||||
}
|
||||
Some(take) => {
|
||||
let mut content = issues
|
||||
.skip(cursor)
|
||||
.take(take + 1)
|
||||
.map(|(id, issue)| types::cobs::issue::Issue::summary(&id, &issue, &aliases))
|
||||
.collect::<Vec<_>>();
|
||||
let more = content.len() > take;
|
||||
content.truncate(take);
|
||||
|
||||
Ok::<_, Error>(types::cobs::PaginatedQuery {
|
||||
cursor,
|
||||
more,
|
||||
content,
|
||||
})
|
||||
}
|
||||
}
|
||||
Ok(issue_service.list_paginated(rid, status.unwrap_or_default(), skip, take, &aliases)?)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
|
||||
@@ -1,5 +1,21 @@
|
||||
use thiserror::Error;
|
||||
|
||||
/// Issue state filter understood by the COB cache queries.
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum Status {
|
||||
Open,
|
||||
Closed,
|
||||
}
|
||||
|
||||
impl Status {
|
||||
pub fn as_str(&self) -> &'static str {
|
||||
match self {
|
||||
Self::Open => "open",
|
||||
Self::Closed => "closed",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum ListIssuesError {
|
||||
#[error(transparent)]
|
||||
|
||||
@@ -35,7 +35,7 @@ where
|
||||
fn list_by_status(
|
||||
&self,
|
||||
rid: identity::RepoId,
|
||||
status: &'static str,
|
||||
status: models::issue::Status,
|
||||
) -> Result<impl Iterator<Item = (IssueId, Issue)>, models::issue::ListIssuesError> {
|
||||
self.issues.list_by_status(rid, status)
|
||||
}
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
use radicle::identity;
|
||||
use radicle::issue::{Issue, IssueId};
|
||||
use radicle::node::AliasStore;
|
||||
|
||||
use crate::cobs;
|
||||
use crate::domain::issue::models;
|
||||
use crate::domain::issue::models::issue::Status;
|
||||
|
||||
pub trait IssueStorage {
|
||||
fn list(
|
||||
@@ -12,7 +15,7 @@ pub trait IssueStorage {
|
||||
fn list_by_status(
|
||||
&self,
|
||||
rid: identity::RepoId,
|
||||
status: &'static str,
|
||||
status: Status,
|
||||
) -> Result<impl Iterator<Item = (IssueId, Issue)>, models::issue::ListIssuesError>;
|
||||
}
|
||||
|
||||
@@ -25,6 +28,51 @@ pub trait IssueService {
|
||||
fn list_by_status(
|
||||
&self,
|
||||
rid: identity::RepoId,
|
||||
status: &'static str,
|
||||
status: Status,
|
||||
) -> Result<impl Iterator<Item = (IssueId, Issue)>, models::issue::ListIssuesError>;
|
||||
|
||||
/// One page of issue summaries, shared by all drivers so pagination
|
||||
/// behaves identically everywhere. Rows are consumed lazily: with `take`
|
||||
/// set, issues beyond the requested page are never deserialized. Without
|
||||
/// `take` the full list is returned and `skip` is ignored.
|
||||
fn list_paginated(
|
||||
&self,
|
||||
rid: identity::RepoId,
|
||||
status: cobs::query::IssueStatus,
|
||||
skip: Option<usize>,
|
||||
take: Option<usize>,
|
||||
aliases: &impl AliasStore,
|
||||
) -> Result<cobs::PaginatedQuery<Vec<cobs::issue::Issue>>, models::issue::ListIssuesError> {
|
||||
let issues: Box<dyn Iterator<Item = (IssueId, Issue)> + '_> = match status {
|
||||
cobs::query::IssueStatus::All => Box::new(self.list(rid)?),
|
||||
cobs::query::IssueStatus::Open => Box::new(self.list_by_status(rid, Status::Open)?),
|
||||
cobs::query::IssueStatus::Closed => Box::new(self.list_by_status(rid, Status::Closed)?),
|
||||
};
|
||||
let summary =
|
||||
|(id, issue): (IssueId, Issue)| cobs::issue::Issue::summary(&id, &issue, aliases);
|
||||
|
||||
match take {
|
||||
None => Ok(cobs::PaginatedQuery {
|
||||
cursor: 0,
|
||||
more: false,
|
||||
content: issues.map(summary).collect::<Vec<_>>(),
|
||||
}),
|
||||
Some(take) => {
|
||||
let cursor = skip.unwrap_or(0);
|
||||
let mut content = issues
|
||||
.skip(cursor)
|
||||
.take(take + 1)
|
||||
.map(summary)
|
||||
.collect::<Vec<_>>();
|
||||
let more = content.len() > take;
|
||||
content.truncate(take);
|
||||
|
||||
Ok(cobs::PaginatedQuery {
|
||||
cursor,
|
||||
more,
|
||||
content,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,12 +10,13 @@ use sqlite as sql;
|
||||
|
||||
use crate::domain::inbox::models::notification;
|
||||
use crate::domain::inbox::traits::InboxStorage;
|
||||
use crate::domain::issue::models::issue::ListIssuesError;
|
||||
use crate::domain::issue::models::issue::{ListIssuesError, Status as IssueStatus};
|
||||
use crate::domain::issue::traits::IssueStorage;
|
||||
use crate::domain::patch::models::patch::{CountsError, ListPatchesError, PatchCounts, State};
|
||||
use crate::domain::patch::traits::PatchStorage;
|
||||
use crate::error::Error;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Sqlite {
|
||||
pub db: Arc<sql::ConnectionThreadSafe>,
|
||||
}
|
||||
@@ -113,22 +114,39 @@ impl PatchStorage for Sqlite {
|
||||
}
|
||||
}
|
||||
|
||||
impl IssueStorage for Sqlite {
|
||||
fn list(
|
||||
impl Sqlite {
|
||||
/// Issues for `rid`, newest first, optionally filtered by state. Single
|
||||
/// home of the issue-listing SQL; `list`/`list_by_status` differ only in
|
||||
/// the status predicate. The sort key is the root comment's timestamp
|
||||
/// (the comment without a `replyTo`) — the issue's creation time — not
|
||||
/// the minimum across replies, whose author-supplied clocks could
|
||||
/// otherwise sink an issue below its real position.
|
||||
fn issues_by(
|
||||
&self,
|
||||
rid: identity::RepoId,
|
||||
status: Option<IssueStatus>,
|
||||
) -> Result<impl Iterator<Item = (IssueId, Issue)>, ListIssuesError> {
|
||||
let mut stmt = self.db.prepare(
|
||||
let filter = if status.is_some() {
|
||||
"AND issue->>'$.state.status' = ?2"
|
||||
} else {
|
||||
""
|
||||
};
|
||||
let mut stmt = self.db.prepare(format!(
|
||||
"SELECT id, issue, (
|
||||
SELECT MIN(JSON_EXTRACT(comment.value, '$.edits[0].timestamp'))
|
||||
FROM JSON_EACH(JSON_EXTRACT(i.issue, '$.thread.comments')) AS comment
|
||||
WHERE JSON_EXTRACT(comment.value, '$.replyTo') IS NULL
|
||||
) AS created_timestamp
|
||||
FROM issues AS i
|
||||
WHERE repo = ?1
|
||||
{filter}
|
||||
ORDER BY created_timestamp DESC, id DESC;
|
||||
",
|
||||
)?;
|
||||
"
|
||||
))?;
|
||||
stmt.bind((1, &rid))?;
|
||||
if let Some(status) = status {
|
||||
stmt.bind((2, status.as_str()))?;
|
||||
}
|
||||
Ok(stmt.into_iter().filter_map(|row| {
|
||||
let row = row.ok()?;
|
||||
let id = IssueId::from_str(row.read::<&str, _>("id")).ok()?;
|
||||
@@ -136,31 +154,22 @@ impl IssueStorage for Sqlite {
|
||||
Some((id, issue))
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
impl IssueStorage for Sqlite {
|
||||
fn list(
|
||||
&self,
|
||||
rid: identity::RepoId,
|
||||
) -> Result<impl Iterator<Item = (IssueId, Issue)>, ListIssuesError> {
|
||||
self.issues_by(rid, None)
|
||||
}
|
||||
|
||||
fn list_by_status(
|
||||
&self,
|
||||
rid: identity::RepoId,
|
||||
status: &'static str,
|
||||
status: IssueStatus,
|
||||
) -> Result<impl Iterator<Item = (IssueId, Issue)>, ListIssuesError> {
|
||||
let mut stmt = self.db.prepare(
|
||||
"SELECT id, issue, (
|
||||
SELECT MIN(JSON_EXTRACT(comment.value, '$.edits[0].timestamp'))
|
||||
FROM JSON_EACH(JSON_EXTRACT(i.issue, '$.thread.comments')) AS comment
|
||||
) AS created_timestamp
|
||||
FROM issues AS i
|
||||
WHERE repo = ?1
|
||||
AND issue->>'$.state.status' = ?2
|
||||
ORDER BY created_timestamp DESC, id DESC;
|
||||
",
|
||||
)?;
|
||||
stmt.bind((1, &rid))?;
|
||||
stmt.bind((2, status))?;
|
||||
Ok(stmt.into_iter().filter_map(|row| {
|
||||
let row = row.ok()?;
|
||||
let id = IssueId::from_str(row.read::<&str, _>("id")).ok()?;
|
||||
let issue = serde_json::from_str::<Issue>(row.read::<&str, _>("issue")).ok()?;
|
||||
Some((id, issue))
|
||||
}))
|
||||
self.issues_by(rid, Some(status))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -473,50 +473,12 @@ async fn issues_handler(
|
||||
take,
|
||||
}): Json<IssuesBody>,
|
||||
) -> impl IntoResponse {
|
||||
use types::cobs::query::IssueStatus;
|
||||
|
||||
let aliases = ctx.profile.aliases();
|
||||
let cursor = skip.unwrap_or(0);
|
||||
let page = ctx
|
||||
.issues
|
||||
.list_paginated(rid, status.unwrap_or_default(), skip, take, &aliases)?;
|
||||
|
||||
let issues = match status.unwrap_or_default() {
|
||||
IssueStatus::All => ctx.issues.list(rid)?.collect::<Vec<_>>(),
|
||||
IssueStatus::Open => ctx.issues.list_by_status(rid, "open")?.collect::<Vec<_>>(),
|
||||
IssueStatus::Closed => ctx
|
||||
.issues
|
||||
.list_by_status(rid, "closed")?
|
||||
.collect::<Vec<_>>(),
|
||||
};
|
||||
|
||||
match take {
|
||||
None => {
|
||||
let content = issues
|
||||
.into_iter()
|
||||
.map(|(id, issue)| issue::Issue::summary(&id, &issue, &aliases))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
Ok::<_, Error>(Json(cobs::PaginatedQuery {
|
||||
cursor: 0,
|
||||
more: false,
|
||||
content,
|
||||
}))
|
||||
}
|
||||
Some(take) => {
|
||||
let mut content = issues
|
||||
.into_iter()
|
||||
.skip(cursor)
|
||||
.take(take + 1)
|
||||
.map(|(id, issue)| issue::Issue::summary(&id, &issue, &aliases))
|
||||
.collect::<Vec<_>>();
|
||||
let more = content.len() > take;
|
||||
content.truncate(take);
|
||||
|
||||
Ok::<_, Error>(Json(cobs::PaginatedQuery {
|
||||
cursor,
|
||||
more,
|
||||
content,
|
||||
}))
|
||||
}
|
||||
}
|
||||
Ok::<_, Error>(Json(page))
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
|
||||
Reference in New Issue
Block a user