Skip to content
This repository was archived by the owner on Nov 6, 2025. It is now read-only.

Commit bad7e98

Browse files
committed
Add issueStat query and openIssueCount field
Closes #145
1 parent 803a0a6 commit bad7e98

File tree

9 files changed

+317
-64
lines changed

9 files changed

+317
-64
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,10 @@ this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
1111
- Tracing with a filter set by `RUST_LOG` environment variable.
1212
- Added support for passing the SSH passphrase through the `SSH_PASSPHRASE`
1313
environment variable.
14+
- Added new GraphQL API: `issueStat` query. Users can filter issues by
15+
`assignee`, `author`, `repo`(repository name), `begin` and `end` (creation
16+
date range). The query returns the `openIssueCount` field, indicating the
17+
number of open issues.
1418

1519
### Changed
1620

Cargo.lock

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,11 @@ edition = "2021"
55

66
[dependencies]
77
anyhow = "1"
8-
async-graphql = "7"
8+
async-graphql = { version = "7", features = ["chrono"] }
99
async-graphql-warp = "7"
1010
base64 = "0.22"
1111
bincode = "1"
12-
chrono = "0.4"
12+
chrono = { version = "0.4", features = ["serde"] }
1313
clap = { version = "4", features = ["derive"] }
1414
config = { version = "0.15", features = ["toml"], default-features = false }
1515
directories = "6"

src/database.rs

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ use sled::{Db, Tree};
77

88
use crate::{
99
github::{GitHubIssue, GitHubPullRequests},
10-
graphql::{Issue, PullRequest},
10+
graphql::{issue::Issue, pull_request::PullRequest},
1111
};
1212
const ISSUE_TREE_NAME: &str = "issues";
1313
const PULL_REQUEST_TREE_NAME: &str = "pull_requests";
@@ -66,11 +66,7 @@ impl Database {
6666
) -> Result<()> {
6767
for item in resp {
6868
let keystr: String = format!("{owner}/{name}#{}", item.number);
69-
Database::insert(
70-
&keystr,
71-
(&item.title, &item.author, &item.closed_at),
72-
&self.issue_tree,
73-
)?;
69+
Database::insert(&keystr, item, &self.issue_tree)?;
7470
}
7571
Ok(())
7672
}

src/github.rs

Lines changed: 25 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,12 @@ use anyhow::{bail, Context, Result};
44
use chrono::Utc;
55
use graphql_client::{GraphQLQuery, QueryBody, Response as GraphQlResponse};
66
use reqwest::{Client, RequestBuilder, Response};
7-
use serde::Serialize;
7+
use serde::{Deserialize, Serialize};
88
use tokio::time;
99
use tracing::error;
1010

1111
use crate::{database::Database, github::{
12-
issues::IssuesRepositoryIssuesNodesAuthor::User as userName,
12+
issues::{IssueState, IssuesRepositoryIssuesNodesAuthor::User as userName},
1313
pull_requests::PullRequestsRepositoryPullRequestsNodesReviewRequestsNodesRequestedReviewer::User,
1414
}, settings::Repository as RepoInfo};
1515

@@ -18,15 +18,15 @@ const GITHUB_URL: &str = "https://api.github.com/graphql";
1818
const APP_USER_AGENT: &str = concat!(env!("CARGO_PKG_NAME"), "/", env!("CARGO_PKG_VERSION"),);
1919
const INIT_TIME: &str = "1992-06-05T00:00:00Z";
2020

21-
type DateTime = String;
21+
type DateTime = chrono::DateTime<Utc>;
2222

2323
#[derive(GraphQLQuery)]
2424
#[graphql(
2525
schema_path = "src/github/schema.graphql",
2626
query_path = "src/github/issues.graphql",
27-
response_derives = "Debug"
27+
response_derives = "Debug, Clone"
2828
)]
29-
struct Issues;
29+
pub(crate) struct Issues;
3030

3131
#[derive(GraphQLQuery)]
3232
#[graphql(
@@ -35,12 +35,22 @@ struct Issues;
3535
)]
3636
struct PullRequests;
3737

38-
#[derive(Debug)]
38+
#[allow(clippy::derivable_impls)]
39+
impl Default for IssueState {
40+
fn default() -> Self {
41+
IssueState::OPEN
42+
}
43+
}
44+
45+
#[derive(Debug, Default, Serialize, Deserialize)]
3946
pub(super) struct GitHubIssue {
4047
pub(super) number: i64,
4148
pub(super) title: String,
4249
pub(super) author: String,
4350
pub(super) closed_at: Option<DateTime>,
51+
pub(super) created_at: DateTime,
52+
pub(super) state: IssueState,
53+
pub(super) assignees: Vec<String>,
4454
}
4555

4656
#[derive(Debug)]
@@ -133,7 +143,7 @@ async fn send_github_issue_query(
133143
last: None,
134144
before: None,
135145
after: end_cur,
136-
since: Some(since.to_string()),
146+
since: Some(since.parse::<DateTime>()?),
137147
};
138148
let resp_body: GraphQlResponse<issues::ResponseData> =
139149
send_query::<Issues>(token, var).await?.json().await?;
@@ -147,11 +157,18 @@ async fn send_github_issue_query(
147157
if let userName(on_user) = author_ref {
148158
author.clone_from(&on_user.login.clone());
149159
}
160+
let assignees =
161+
issue.assignees.nodes.as_ref().map_or(Vec::new(), |nodes| {
162+
nodes.iter().flatten().map(|a| a.login.clone()).collect()
163+
});
150164
issues.push(GitHubIssue {
151165
number: issue.number,
152166
title: issue.title.to_string(),
153167
author,
154-
closed_at: issue.closed_at.clone(),
168+
closed_at: issue.closed_at,
169+
created_at: issue.created_at,
170+
state: issue.state.clone(),
171+
assignees,
155172
});
156173
}
157174
if !repository.issues.page_info.has_next_page {

src/github/issues.graphql

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,11 @@ query Issues(
3535
login
3636
}
3737
}
38+
assignees(first: 10) {
39+
nodes {
40+
login
41+
}
42+
}
3843
}
3944
}
4045
}

src/graphql.rs

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1-
mod issue;
2-
mod pull_request;
1+
pub(crate) mod issue;
2+
pub(crate) mod issue_stat;
3+
pub(crate) mod pull_request;
34

45
use std::fmt::Display;
56

@@ -9,8 +10,6 @@ use async_graphql::{
910
};
1011
use base64::{engine::general_purpose, Engine as _};
1112

12-
pub(crate) use self::issue::Issue;
13-
pub(crate) use self::pull_request::PullRequest;
1413
use crate::database::Database;
1514

1615
/// The default page size for connections when neither `first` nor `last` is
@@ -21,7 +20,11 @@ const DEFAULT_PAGE_SIZE: usize = 100;
2120
///
2221
/// This is exposed only for [`Schema`], and not used directly.
2322
#[derive(Default, MergedObject)]
24-
pub(crate) struct Query(issue::IssueQuery, pull_request::PullRequestQuery);
23+
pub(crate) struct Query(
24+
issue::IssueQuery,
25+
pull_request::PullRequestQuery,
26+
issue_stat::IssueStatQuery,
27+
);
2528

2629
pub(crate) type Schema = async_graphql::Schema<Query, EmptyMutation, EmptySubscription>;
2730

src/graphql/issue.rs

Lines changed: 33 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,16 @@ use std::fmt;
33
use anyhow::Context as AnyhowContext;
44
use async_graphql::{
55
connection::{query, Connection, EmptyFields},
6-
Context, Object, Result, SimpleObject,
6+
scalar, Context, Object, Result, SimpleObject,
77
};
8+
use chrono::{DateTime, Utc};
89

9-
use crate::database::{self, Database, TryFromKeyValue};
10+
use crate::{
11+
database::{self, Database, TryFromKeyValue},
12+
github::{issues::IssueState, GitHubIssue},
13+
};
14+
15+
scalar!(IssueState);
1016

1117
#[derive(SimpleObject)]
1218
pub(crate) struct Issue {
@@ -15,19 +21,32 @@ pub(crate) struct Issue {
1521
pub(crate) number: i32,
1622
pub(crate) title: String,
1723
pub(crate) author: String,
24+
pub(crate) created_at: DateTime<Utc>,
25+
pub(crate) state: IssueState,
26+
pub(crate) assignees: Vec<String>,
1827
}
1928

2029
impl TryFromKeyValue for Issue {
2130
fn try_from_key_value(key: &[u8], value: &[u8]) -> anyhow::Result<Self> {
2231
let (owner, repo, number) = database::parse_key(key)
2332
.with_context(|| format!("invalid key in database: {key:02x?}"))?;
24-
let (title, author, _) = bincode::deserialize::<(String, String, Option<String>)>(value)?;
33+
let GitHubIssue {
34+
title,
35+
author,
36+
created_at,
37+
state,
38+
assignees,
39+
..
40+
} = bincode::deserialize::<GitHubIssue>(value)?;
2541
let issue = Issue {
2642
title,
2743
author,
2844
owner,
2945
repo,
3046
number: i32::try_from(number).unwrap_or(i32::MAX),
47+
created_at,
48+
state,
49+
assignees,
3150
};
3251
Ok(issue)
3352
}
@@ -69,6 +88,15 @@ impl IssueQuery {
6988
mod tests {
7089
use crate::{github::GitHubIssue, graphql::TestSchema};
7190

91+
fn create_issues(n: usize) -> Vec<GitHubIssue> {
92+
(1..=n)
93+
.map(|i| GitHubIssue {
94+
number: i64::try_from(i).unwrap(),
95+
..Default::default()
96+
})
97+
.collect()
98+
}
99+
72100
#[tokio::test]
73101
async fn issues_empty() {
74102
let schema = TestSchema::new();
@@ -89,26 +117,7 @@ mod tests {
89117
#[tokio::test]
90118
async fn issues_first() {
91119
let schema = TestSchema::new();
92-
let issues = vec![
93-
GitHubIssue {
94-
number: 1,
95-
title: "issue 1".to_string(),
96-
author: "author 1".to_string(),
97-
closed_at: None,
98-
},
99-
GitHubIssue {
100-
number: 2,
101-
title: "issue 2".to_string(),
102-
author: "author 2".to_string(),
103-
closed_at: None,
104-
},
105-
GitHubIssue {
106-
number: 3,
107-
title: "issue 3".to_string(),
108-
author: "author 3".to_string(),
109-
closed_at: None,
110-
},
111-
];
120+
let issues = create_issues(3);
112121
schema.db.insert_issues(issues, "owner", "name").unwrap();
113122

114123
let query = r"
@@ -148,26 +157,7 @@ mod tests {
148157
#[tokio::test]
149158
async fn issues_last() {
150159
let schema = TestSchema::new();
151-
let issues = vec![
152-
GitHubIssue {
153-
number: 1,
154-
title: "issue 1".to_string(),
155-
author: "author 1".to_string(),
156-
closed_at: None,
157-
},
158-
GitHubIssue {
159-
number: 2,
160-
title: "issue 2".to_string(),
161-
author: "author 2".to_string(),
162-
closed_at: None,
163-
},
164-
GitHubIssue {
165-
number: 3,
166-
title: "issue 3".to_string(),
167-
author: "author 3".to_string(),
168-
closed_at: None,
169-
},
170-
];
160+
let issues = create_issues(3);
171161
schema.db.insert_issues(issues, "owner", "name").unwrap();
172162

173163
let query = r"

0 commit comments

Comments
 (0)