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

Commit 56e0895

Browse files
committed
Update Issues GraphQL query and related codes
- Added new fields (specified in #169) to the GraphQL query. - Restored previously commented-out project-related fields. - Explicitly mapped GraphQL custom scalar `URI` to Rust. - Made `Issue` struct support serialization/deserialization to enable storing/retrieving from the database. - Updated `GitHubIssue` struct to align with the `Issue` struct, and adjusted related functions accordingly. - Made alias for enums related with `Issue` struct. - Changed the numeric type in `parse_key`'s return value from `i64` to `i32` to align with the GraphQL API. Close: #169
1 parent 4b2509f commit 56e0895

File tree

8 files changed

+489
-76
lines changed

8 files changed

+489
-76
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
1515
`assignee`, `author`, `repo`(repository name), `begin` and `end` (creation
1616
date range). The query returns the `openIssueCount` field, indicating the
1717
number of open issues.
18+
- Added new fields in `issues` GraphQL query, and related structs (`Issue` and
19+
`GitHubIssue`).
1820

1921
### Changed
2022

src/database.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -151,7 +151,7 @@ impl<T: TryFromKeyValue> DoubleEndedIterator for Iter<T> {
151151
}
152152
}
153153

154-
pub(crate) fn parse_key(key: &[u8]) -> Result<(String, String, i64)> {
154+
pub(crate) fn parse_key(key: &[u8]) -> Result<(String, String, i32)> {
155155
let re = Regex::new(r"(?P<owner>\S+)/(?P<name>\S+)#(?P<number>[0-9]+)").expect("valid regex");
156156
if let Some(caps) = re.captures(
157157
String::from_utf8(key.to_vec())
@@ -172,7 +172,7 @@ pub(crate) fn parse_key(key: &[u8]) -> Result<(String, String, i64)> {
172172
.name("number")
173173
.ok_or_else(|| anyhow!("invalid key"))?
174174
.as_str()
175-
.parse::<i64>()
175+
.parse::<i32>()
176176
.context("invalid key")?;
177177
Ok((owner, name, number))
178178
} else {

src/github.rs

Lines changed: 223 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,31 @@ use serde::{Deserialize, Serialize};
88
use tokio::time;
99
use tracing::error;
1010

11-
use crate::{database::Database, github::{
12-
issues::{IssueState, IssuesRepositoryIssuesNodesAuthor::User as userName},
13-
pull_requests::PullRequestsRepositoryPullRequestsNodesReviewRequestsNodesRequestedReviewer::User,
14-
}, settings::Repository as RepoInfo};
11+
use crate::{
12+
database::Database,
13+
github::{
14+
issues::{
15+
IssueState, IssuesRepositoryIssuesNodesAuthor::User as IssueAuthor,
16+
IssuesRepositoryIssuesNodesClosedByPullRequestsReferencesEdgesNodeAuthor::User as PullRequestRefAuthor,
17+
IssuesRepositoryIssuesNodesCommentsNodesAuthor::User as IssueCommentsAuthor,
18+
IssuesRepositoryIssuesNodesProjectItemsNodesTodoInitiationOption as TodoInitOption,
19+
IssuesRepositoryIssuesNodesProjectItemsNodesTodoPendingDays as TodoPendingDays,
20+
IssuesRepositoryIssuesNodesProjectItemsNodesTodoPriority as TodoPriority,
21+
IssuesRepositoryIssuesNodesProjectItemsNodesTodoSize as TodoSize,
22+
IssuesRepositoryIssuesNodesProjectItemsNodesTodoStatus as TodoStatus,
23+
IssuesRepositoryIssuesNodesSubIssuesNodesAuthor::User as SubIssueAuthor,
24+
},
25+
pull_requests::PullRequestsRepositoryPullRequestsNodesReviewRequestsNodesRequestedReviewer::User,
26+
},
27+
graphql::{
28+
issue::{
29+
Comment, CommentConnection, ParentIssue, ProjectV2Item, ProjectV2ItemConnection,
30+
PullRequestRef, SubIssue, SubIssueConnection,
31+
},
32+
DateTimeUtc,
33+
},
34+
settings::Repository as RepoInfo,
35+
};
1536

1637
const GITHUB_FETCH_SIZE: i64 = 10;
1738
const GITHUB_URL: &str = "https://api.github.com/graphql";
@@ -20,6 +41,9 @@ const INIT_TIME: &str = "1992-06-05T00:00:00Z";
2041

2142
type DateTime = Timestamp;
2243

44+
#[allow(clippy::upper_case_acronyms)]
45+
type URI = String;
46+
2347
#[derive(GraphQLQuery)]
2448
#[graphql(
2549
schema_path = "src/github/schema.graphql",
@@ -44,18 +68,28 @@ impl Default for IssueState {
4468

4569
#[derive(Debug, Default, Serialize, Deserialize)]
4670
pub(super) struct GitHubIssue {
47-
pub(super) number: i64,
71+
pub(super) id: String,
72+
pub(super) number: i32,
4873
pub(super) title: String,
4974
pub(super) author: String,
50-
pub(super) closed_at: Option<DateTime>,
51-
pub(super) created_at: DateTime,
75+
pub(super) body: String,
5276
pub(super) state: IssueState,
5377
pub(super) assignees: Vec<String>,
78+
pub(super) labels: Vec<String>,
79+
pub(super) comments: CommentConnection,
80+
pub(super) project_items: ProjectV2ItemConnection,
81+
pub(super) sub_issues: SubIssueConnection,
82+
pub(super) parent: Option<ParentIssue>,
83+
pub(super) url: String,
84+
pub(super) closed_by_pull_requests: Vec<PullRequestRef>,
85+
pub(super) created_at: DateTimeUtc,
86+
pub(super) updated_at: DateTimeUtc,
87+
pub(super) closed_at: Option<DateTimeUtc>,
5488
}
5589

5690
#[derive(Debug)]
5791
pub(super) struct GitHubPullRequests {
58-
pub(super) number: i64,
92+
pub(super) number: i32,
5993
pub(super) title: String,
6094
pub(super) assignees: Vec<String>,
6195
pub(super) reviewers: Vec<String>,
@@ -126,6 +160,7 @@ pub(super) async fn fetch_periodically(
126160
}
127161
}
128162

163+
#[allow(clippy::too_many_lines)]
129164
async fn send_github_issue_query(
130165
owner: &str,
131166
name: &str,
@@ -142,45 +177,194 @@ async fn send_github_issue_query(
142177
first: Some(GITHUB_FETCH_SIZE),
143178
last: None,
144179
before: None,
145-
after: end_cur,
180+
after: end_cur.take(),
146181
since: Some(since.parse::<DateTime>()?),
147182
};
148183
let resp_body: GraphQlResponse<issues::ResponseData> =
149184
send_query::<Issues>(token, var).await?.json().await?;
150185
if let Some(data) = resp_body.data {
151186
if let Some(repository) = data.repository {
152-
if let Some(nodes) = repository.issues.nodes.as_ref() {
153-
for issue in nodes.iter().flatten() {
154-
let mut author: String = String::new();
155-
156-
let author_ref = issue.author.as_ref().context("Missing issue author")?;
157-
if let userName(on_user) = author_ref {
158-
author.clone_from(&on_user.login.clone());
159-
}
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-
});
164-
issues.push(GitHubIssue {
165-
number: issue.number,
166-
title: issue.title.to_string(),
167-
author,
168-
closed_at: issue.closed_at,
169-
created_at: issue.created_at,
170-
state: issue.state.clone(),
171-
assignees,
172-
});
173-
}
174-
if !repository.issues.page_info.has_next_page {
175-
total_issue.push(issues);
176-
break;
177-
}
178-
end_cur = repository.issues.page_info.end_cursor;
179-
continue;
187+
let nodes = repository.issues.nodes.unwrap_or_default();
188+
for issue in nodes.into_iter().flatten() {
189+
let author = match issue.author.context("Missing issue author")? {
190+
IssueAuthor(u) => u.login,
191+
_ => String::new(),
192+
};
193+
issues.push(GitHubIssue {
194+
id: issue.id,
195+
number: issue.number.try_into().unwrap_or_default(),
196+
title: issue.title,
197+
author,
198+
body: issue.body,
199+
state: issue.state,
200+
assignees: issue
201+
.assignees
202+
.nodes
203+
.unwrap_or_default()
204+
.into_iter()
205+
.flatten()
206+
.map(|n| n.login)
207+
.collect(),
208+
labels: issue
209+
.labels
210+
.and_then(|l| l.nodes)
211+
.unwrap_or_default()
212+
.into_iter()
213+
.flatten()
214+
.map(|node| node.name)
215+
.collect(),
216+
comments: CommentConnection {
217+
total_count: issue.comments.total_count.try_into().unwrap_or_default(),
218+
nodes: issue
219+
.comments
220+
.nodes
221+
.unwrap_or_default()
222+
.into_iter()
223+
.flatten()
224+
.map(|comment| Comment {
225+
author: match comment.author {
226+
Some(IssueCommentsAuthor(u)) => u.login,
227+
_ => String::new(),
228+
},
229+
body: comment.body,
230+
created_at: DateTimeUtc(comment.created_at),
231+
id: comment.id,
232+
repository_name: comment.repository.name,
233+
updated_at: DateTimeUtc(comment.updated_at),
234+
url: comment.url,
235+
})
236+
.collect(),
237+
},
238+
project_items: ProjectV2ItemConnection {
239+
total_count: issue
240+
.project_items
241+
.total_count
242+
.try_into()
243+
.unwrap_or_default(),
244+
nodes: issue
245+
.project_items
246+
.nodes
247+
.unwrap_or_default()
248+
.into_iter()
249+
.flatten()
250+
.map(|node| ProjectV2Item {
251+
id: node.id,
252+
todo_status: node.todo_status.and_then(|status| match status {
253+
TodoStatus::ProjectV2ItemFieldSingleSelectValue(inner) => {
254+
inner.name
255+
}
256+
_ => None,
257+
}),
258+
todo_priority: node.todo_priority.and_then(|priority| {
259+
match priority {
260+
TodoPriority::ProjectV2ItemFieldSingleSelectValue(
261+
inner,
262+
) => inner.name,
263+
_ => None,
264+
}
265+
}),
266+
todo_size: node.todo_size.and_then(|size| match size {
267+
TodoSize::ProjectV2ItemFieldSingleSelectValue(inner) => {
268+
inner.name
269+
}
270+
_ => None,
271+
}),
272+
todo_initiation_option: node.todo_initiation_option.and_then(
273+
|init| match init {
274+
TodoInitOption::ProjectV2ItemFieldSingleSelectValue(
275+
inner,
276+
) => inner.name,
277+
_ => None,
278+
},
279+
),
280+
todo_pending_days: node.todo_pending_days.and_then(|days| {
281+
match days {
282+
TodoPendingDays::ProjectV2ItemFieldNumberValue(
283+
inner,
284+
) => inner.number,
285+
_ => None,
286+
}
287+
}),
288+
})
289+
.collect(),
290+
},
291+
sub_issues: SubIssueConnection {
292+
total_count: issue
293+
.sub_issues
294+
.total_count
295+
.try_into()
296+
.unwrap_or_default(),
297+
nodes: issue
298+
.sub_issues
299+
.nodes
300+
.unwrap_or_default()
301+
.into_iter()
302+
.flatten()
303+
.map(|si| SubIssue {
304+
id: si.id,
305+
number: si.number.try_into().unwrap_or_default(),
306+
title: si.title,
307+
state: si.state,
308+
created_at: DateTimeUtc(si.created_at),
309+
updated_at: DateTimeUtc(si.updated_at),
310+
closed_at: si.closed_at.map(DateTimeUtc),
311+
author: match si.author {
312+
Some(SubIssueAuthor(u)) => u.login,
313+
_ => String::new(),
314+
},
315+
assignees: si
316+
.assignees
317+
.nodes
318+
.unwrap_or_default()
319+
.into_iter()
320+
.flatten()
321+
.map(|n| n.login)
322+
.collect(),
323+
})
324+
.collect(),
325+
},
326+
parent: issue.parent.map(|parent| ParentIssue {
327+
id: parent.id,
328+
number: parent.number.try_into().unwrap_or_default(),
329+
title: parent.title,
330+
}),
331+
url: issue.url,
332+
closed_by_pull_requests: issue
333+
.closed_by_pull_requests_references
334+
.map(|r| r.edges)
335+
.unwrap_or_default()
336+
.into_iter()
337+
.flatten()
338+
.flatten()
339+
.filter_map(|edge| {
340+
edge.node.map(|node| PullRequestRef {
341+
number: node.number.try_into().unwrap_or_default(),
342+
state: node.state,
343+
created_at: DateTimeUtc(node.created_at),
344+
updated_at: DateTimeUtc(node.updated_at),
345+
closed_at: node.closed_at.map(DateTimeUtc),
346+
author: match node.author {
347+
Some(PullRequestRefAuthor(u)) => u.login,
348+
_ => String::new(),
349+
},
350+
url: node.url,
351+
})
352+
})
353+
.collect(),
354+
created_at: DateTimeUtc(issue.created_at),
355+
updated_at: DateTimeUtc(issue.updated_at),
356+
closed_at: issue.closed_at.map(DateTimeUtc),
357+
});
180358
}
359+
if !repository.issues.page_info.has_next_page {
360+
total_issue.push(issues);
361+
break;
362+
}
363+
end_cur = repository.issues.page_info.end_cursor;
364+
continue;
181365
}
366+
bail!("Failed to parse response data");
182367
}
183-
bail!("Failed to parse response data");
184368
}
185369
Ok(total_issue)
186370
}
@@ -230,7 +414,7 @@ async fn send_github_pr_query(
230414
}
231415

232416
prs.push(GitHubPullRequests {
233-
number: pr.number,
417+
number: pr.number.try_into().unwrap_or_default(),
234418
title: pr.title.to_string(),
235419
assignees,
236420
reviewers,

0 commit comments

Comments
 (0)