Add flow types for GitHub graphql query response (#140)

This commit adds flow typing for the JSON result from hitting the GitHub
graphql api. We can't prove that the flow typing is correct, but since
the type definition is colocated with the corresponding fragment
definitions, we can hope that maintainers will maintain both together.

We update the parser to consume the new flow types. There are no flow
errors.

Test plan:
Inspect the flowtypes, verify that they correspond to the data in
example-repo.json, and that there are no flow errors.
This commit is contained in:
Dandelion Mané 2018-04-24 14:05:51 -07:00 committed by GitHub
parent 418b745d7c
commit 6a3e4d754c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 199 additions and 81 deletions

View File

@ -93,6 +93,22 @@ export type Continuation = {|
+destinationPath: $ReadOnlyArray<string | number>, +destinationPath: $ReadOnlyArray<string | number>,
|}; |};
export type ConnectionJSON<+T> = {|
+nodes: $ReadOnlyArray<T>,
+pageInfo: {|
+endCursor: ?string,
+hasNextPage: boolean,
|},
|};
export type RepositoryJSON = {|
+repository: {
+id: string,
+issues: ConnectionJSON<IssueJSON>,
+pullRequests: ConnectionJSON<PullRequestJSON>,
},
|};
/** /**
* The top-level GitHub query to request data about a repository. * The top-level GitHub query to request data about a repository.
* Callers will also be interested in `createVariables`. * Callers will also be interested in `createVariables`.
@ -607,25 +623,49 @@ function mergeDirect<T>(destination: T, source: any): T {
} }
} }
/** export type AuthorJSON = {|
* These fragments are used to construct the root query, and also to +__typename: "User" | "Bot" | "Organization",
* fetch more pages of specific entity types. +id: string,
*/ +login: string,
export function createFragments(): FragmentDefinition[] { +url: string,
|};
function makePageInfo() {
const b = build; const b = build;
const makePageInfo = () => return b.field("pageInfo", {}, [
b.field("pageInfo", {}, [b.field("hasNextPage"), b.field("endCursor")]); b.field("hasNextPage"),
const makeAuthor = () => b.field("author", {}, [b.fragmentSpread("whoami")]); b.field("endCursor"),
return [ ]);
b.fragment("whoami", "Actor", [ }
function makeAuthor() {
const b = build;
return b.field("author", {}, [b.fragmentSpread("whoami")]);
}
function whoamiFragment(): FragmentDefinition {
const b = build;
return b.fragment("whoami", "Actor", [
b.field("__typename"), b.field("__typename"),
b.field("login"), b.field("login"),
b.field("url"), b.field("url"),
b.inlineFragment("User", [b.field("id")]), b.inlineFragment("User", [b.field("id")]),
b.inlineFragment("Organization", [b.field("id")]), b.inlineFragment("Organization", [b.field("id")]),
b.inlineFragment("Bot", [b.field("id")]), b.inlineFragment("Bot", [b.field("id")]),
]), ]);
b.fragment("issues", "IssueConnection", [ }
export type IssueJSON = {|
+id: string,
+url: string,
+title: string,
+body: string,
+number: number,
+author: AuthorJSON,
+comments: ConnectionJSON<CommentJSON>,
|};
function issuesFragment(): FragmentDefinition {
const b = build;
return b.fragment("issues", "IssueConnection", [
makePageInfo(), makePageInfo(),
b.field("nodes", {}, [ b.field("nodes", {}, [
b.field("id"), b.field("id"),
@ -638,8 +678,22 @@ export function createFragments(): FragmentDefinition[] {
b.fragmentSpread("comments"), b.fragmentSpread("comments"),
]), ]),
]), ]),
]), ]);
b.fragment("prs", "PullRequestConnection", [ }
export type PullRequestJSON = {|
+id: string,
+url: string,
+title: string,
+body: string,
+number: number,
+author: AuthorJSON,
+comments: ConnectionJSON<CommentJSON>,
+reviews: ConnectionJSON<PullRequestReviewJSON>,
|};
function pullRequestsFragment(): FragmentDefinition {
const b = build;
return b.fragment("prs", "PullRequestConnection", [
makePageInfo(), makePageInfo(),
b.field("nodes", {}, [ b.field("nodes", {}, [
b.field("id"), b.field("id"),
@ -655,19 +709,47 @@ export function createFragments(): FragmentDefinition[] {
b.fragmentSpread("reviews"), b.fragmentSpread("reviews"),
]), ]),
]), ]),
]), ]);
}
export type CommentJSON = {|
+id: string,
+url: string,
+body: string,
+author: AuthorJSON,
|};
function commentsFragment(): FragmentDefinition {
const b = build;
// (Note: issue comments and PR comments use the same connection type.) // (Note: issue comments and PR comments use the same connection type.)
b.fragment("comments", "IssueCommentConnection", [ return b.fragment("comments", "IssueCommentConnection", [
makePageInfo(), makePageInfo(),
b.field("nodes", {}, [ b.field("nodes", {}, [
b.field("id"), b.field("id"),
b.field("url"), b.field("url"),
makeAuthor(), makeAuthor(),
b.field("body"), b.field("body"),
b.field("url"),
]), ]),
]), ]);
b.fragment("reviews", "PullRequestReviewConnection", [ }
export type PullRequestReviewState =
| "CHANGES_REQUESTED"
| "APPROVED"
| "COMMENTED"
| "DISMISSED"
| "PENDING";
export type PullRequestReviewJSON = {|
+id: string,
+url: string,
+body: string,
+author: AuthorJSON,
+state: PullRequestReviewState,
+comments: ConnectionJSON<PullRequestReviewCommentJSON>,
|};
function reviewsFragment(): FragmentDefinition {
const b = build;
return b.fragment("reviews", "PullRequestReviewConnection", [
makePageInfo(), makePageInfo(),
b.field("nodes", {}, [ b.field("nodes", {}, [
b.field("id"), b.field("id"),
@ -679,8 +761,18 @@ export function createFragments(): FragmentDefinition[] {
b.fragmentSpread("reviewComments"), b.fragmentSpread("reviewComments"),
]), ]),
]), ]),
]), ]);
b.fragment("reviewComments", "PullRequestReviewCommentConnection", [ }
export type PullRequestReviewCommentJSON = {|
+id: string,
+url: string,
+body: string,
+author: AuthorJSON,
|};
function reviewCommentsFragment(): FragmentDefinition {
const b = build;
return b.fragment("reviewComments", "PullRequestReviewCommentConnection", [
makePageInfo(), makePageInfo(),
b.field("nodes", {}, [ b.field("nodes", {}, [
b.field("id"), b.field("id"),
@ -688,7 +780,22 @@ export function createFragments(): FragmentDefinition[] {
b.field("body"), b.field("body"),
makeAuthor(), makeAuthor(),
]), ]),
]), ]);
}
/**
* These fragments are used to construct the root query, and also to
* fetch more pages of specific entity types.
*/
export function createFragments(): FragmentDefinition[] {
const b = build;
return [
whoamiFragment(),
issuesFragment(),
pullRequestsFragment(),
commentsFragment(),
reviewsFragment(),
reviewCommentsFragment(),
]; ];
} }

View File

@ -14,6 +14,17 @@ import type {
PullRequestNodePayload, PullRequestNodePayload,
IssueNodePayload, IssueNodePayload,
} from "./types"; } from "./types";
import type {
RepositoryJSON,
PullRequestReviewJSON,
PullRequestJSON,
IssueJSON,
CommentJSON,
AuthorJSON,
PullRequestReviewCommentJSON,
} from "./graphql";
import type {Address} from "../../core/address"; import type {Address} from "../../core/address";
import {PLUGIN_NAME} from "./pluginName"; import {PLUGIN_NAME} from "./pluginName";
import {Graph, edgeID} from "../../core/graph"; import {Graph, edgeID} from "../../core/graph";
@ -54,7 +65,7 @@ export class GithubParser {
| PullRequestReviewCommentNodePayload | PullRequestReviewCommentNodePayload
| PullRequestReviewNodePayload | PullRequestReviewNodePayload
>, >,
authorJson: * authorJson: AuthorJSON
) { ) {
let authorPayload: AuthorNodePayload = { let authorPayload: AuthorNodePayload = {
login: authorJson.login, login: authorJson.login,
@ -102,7 +113,7 @@ export class GithubParser {
parentNode: Node< parentNode: Node<
IssueNodePayload | PullRequestNodePayload | PullRequestReviewNodePayload IssueNodePayload | PullRequestNodePayload | PullRequestReviewNodePayload
>, >,
commentJson: * commentJson: CommentJSON
) { ) {
let commentType: NodeType; let commentType: NodeType;
switch (parentNode.address.type) { switch (parentNode.address.type) {
@ -160,7 +171,7 @@ export class GithubParser {
this.graph.addEdge(containsEdge); this.graph.addEdge(containsEdge);
} }
addIssue(issueJson: *) { addIssue(issueJson: IssueJSON) {
const issuePayload: IssueNodePayload = { const issuePayload: IssueNodePayload = {
url: issueJson.url, url: issueJson.url,
number: issueJson.number, number: issueJson.number,
@ -178,7 +189,7 @@ export class GithubParser {
issueJson.comments.nodes.forEach((c) => this.addComment(issueNode, c)); issueJson.comments.nodes.forEach((c) => this.addComment(issueNode, c));
} }
addPullRequest(prJson: *) { addPullRequest(prJson: PullRequestJSON) {
const pullRequestPayload: PullRequestNodePayload = { const pullRequestPayload: PullRequestNodePayload = {
url: prJson.url, url: prJson.url,
number: prJson.number, number: prJson.number,
@ -201,7 +212,7 @@ export class GithubParser {
addPullRequestReview( addPullRequestReview(
pullRequestNode: Node<PullRequestNodePayload>, pullRequestNode: Node<PullRequestNodePayload>,
reviewJson: * reviewJson: PullRequestReviewJSON
) { ) {
const reviewPayload: PullRequestReviewNodePayload = { const reviewPayload: PullRequestReviewNodePayload = {
url: reviewJson.url, url: reviewJson.url,
@ -218,7 +229,7 @@ export class GithubParser {
reviewJson.comments.nodes.forEach((c) => this.addComment(reviewNode, c)); reviewJson.comments.nodes.forEach((c) => this.addComment(reviewNode, c));
} }
addData(dataJson: *) { addData(dataJson: RepositoryJSON) {
dataJson.repository.issues.nodes.forEach((i) => this.addIssue(i)); dataJson.repository.issues.nodes.forEach((i) => this.addIssue(i));
dataJson.repository.pullRequests.nodes.forEach((pr) => dataJson.repository.pullRequests.nodes.forEach((pr) =>
this.addPullRequest(pr) this.addPullRequest(pr)