Add GitHub `RelationalView` (#411)
The `RelationalView` maps the GitHub GraphQL response data into a View class, which makes it easy to access pieces of GitHub data by their corresponding `StructuredAddress`. This will be a valuable companion to the graph, making it possible to access GitHub node data like the title or body of an issue via the issue's address. This basically is the supplement to the GitHub graph that includes the "payloads" from our v1 Graph. It will also make creating the GitHub graph a lot more convenient, although I've left that for another commit. Designed with feedback from @wchargin. Note: The `RelationalView` objects have a `nominalAuthor` rather than `author`, so as to distinguish between authorship in the GitHub data model (entities have at most one author) and in the SourceCred model (entities may have multiple authors). Test plan: Inspect the included snapshots for reasonability, and run unit tests.
This commit is contained in:
parent
2dec8868db
commit
a470f28204
|
@ -0,0 +1,374 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`plugins/github/relationalView comment matches snapshot 1`] = `
|
||||
Object {
|
||||
"address": Object {
|
||||
"id": "373768703",
|
||||
"parent": Object {
|
||||
"number": "2",
|
||||
"repo": Object {
|
||||
"name": "example-github",
|
||||
"owner": "sourcecred",
|
||||
"type": "REPO",
|
||||
},
|
||||
"type": "ISSUE",
|
||||
},
|
||||
"type": "COMMENT",
|
||||
},
|
||||
"body": "It should also be possible to reference by exact url: https://github.com/sourcecred/example-github/issues/6",
|
||||
"nominalAuthor": Object {
|
||||
"login": "decentralion",
|
||||
"type": "USERLIKE",
|
||||
},
|
||||
"url": "https://github.com/sourcecred/example-github/issues/2#issuecomment-373768703",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`plugins/github/relationalView issue matches snapshot 1`] = `
|
||||
Object {
|
||||
"address": Object {
|
||||
"number": "2",
|
||||
"repo": Object {
|
||||
"name": "example-github",
|
||||
"owner": "sourcecred",
|
||||
"type": "REPO",
|
||||
},
|
||||
"type": "ISSUE",
|
||||
},
|
||||
"body": "This issue references another issue, namely #1",
|
||||
"comments": Array [
|
||||
Object {
|
||||
"id": "373768703",
|
||||
"parent": Object {
|
||||
"number": "2",
|
||||
"repo": Object {
|
||||
"name": "example-github",
|
||||
"owner": "sourcecred",
|
||||
"type": "REPO",
|
||||
},
|
||||
"type": "ISSUE",
|
||||
},
|
||||
"type": "COMMENT",
|
||||
},
|
||||
Object {
|
||||
"id": "373768850",
|
||||
"parent": Object {
|
||||
"number": "2",
|
||||
"repo": Object {
|
||||
"name": "example-github",
|
||||
"owner": "sourcecred",
|
||||
"type": "REPO",
|
||||
},
|
||||
"type": "ISSUE",
|
||||
},
|
||||
"type": "COMMENT",
|
||||
},
|
||||
Object {
|
||||
"id": "385576185",
|
||||
"parent": Object {
|
||||
"number": "2",
|
||||
"repo": Object {
|
||||
"name": "example-github",
|
||||
"owner": "sourcecred",
|
||||
"type": "REPO",
|
||||
},
|
||||
"type": "ISSUE",
|
||||
},
|
||||
"type": "COMMENT",
|
||||
},
|
||||
Object {
|
||||
"id": "385576220",
|
||||
"parent": Object {
|
||||
"number": "2",
|
||||
"repo": Object {
|
||||
"name": "example-github",
|
||||
"owner": "sourcecred",
|
||||
"type": "REPO",
|
||||
},
|
||||
"type": "ISSUE",
|
||||
},
|
||||
"type": "COMMENT",
|
||||
},
|
||||
Object {
|
||||
"id": "385576248",
|
||||
"parent": Object {
|
||||
"number": "2",
|
||||
"repo": Object {
|
||||
"name": "example-github",
|
||||
"owner": "sourcecred",
|
||||
"type": "REPO",
|
||||
},
|
||||
"type": "ISSUE",
|
||||
},
|
||||
"type": "COMMENT",
|
||||
},
|
||||
Object {
|
||||
"id": "385576273",
|
||||
"parent": Object {
|
||||
"number": "2",
|
||||
"repo": Object {
|
||||
"name": "example-github",
|
||||
"owner": "sourcecred",
|
||||
"type": "REPO",
|
||||
},
|
||||
"type": "ISSUE",
|
||||
},
|
||||
"type": "COMMENT",
|
||||
},
|
||||
Object {
|
||||
"id": "385576920",
|
||||
"parent": Object {
|
||||
"number": "2",
|
||||
"repo": Object {
|
||||
"name": "example-github",
|
||||
"owner": "sourcecred",
|
||||
"type": "REPO",
|
||||
},
|
||||
"type": "ISSUE",
|
||||
},
|
||||
"type": "COMMENT",
|
||||
},
|
||||
Object {
|
||||
"id": "385576936",
|
||||
"parent": Object {
|
||||
"number": "2",
|
||||
"repo": Object {
|
||||
"name": "example-github",
|
||||
"owner": "sourcecred",
|
||||
"type": "REPO",
|
||||
},
|
||||
"type": "ISSUE",
|
||||
},
|
||||
"type": "COMMENT",
|
||||
},
|
||||
],
|
||||
"nominalAuthor": Object {
|
||||
"login": "decentralion",
|
||||
"type": "USERLIKE",
|
||||
},
|
||||
"title": "A referencing issue.",
|
||||
"url": "https://github.com/sourcecred/example-github/issues/2",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`plugins/github/relationalView pull matches snapshot 1`] = `
|
||||
Object {
|
||||
"address": Object {
|
||||
"number": "5",
|
||||
"repo": Object {
|
||||
"name": "example-github",
|
||||
"owner": "sourcecred",
|
||||
"type": "REPO",
|
||||
},
|
||||
"type": "PULL",
|
||||
},
|
||||
"body": "@wchargin could you please do the following:
|
||||
- add a commit comment
|
||||
- add a review comment requesting some trivial change
|
||||
- i'll change it
|
||||
- then approve the pr",
|
||||
"comments": Array [
|
||||
Object {
|
||||
"id": "396430464",
|
||||
"parent": Object {
|
||||
"number": "5",
|
||||
"repo": Object {
|
||||
"name": "example-github",
|
||||
"owner": "sourcecred",
|
||||
"type": "REPO",
|
||||
},
|
||||
"type": "PULL",
|
||||
},
|
||||
"type": "COMMENT",
|
||||
},
|
||||
],
|
||||
"mergedAs": Object {
|
||||
"hash": "6d5b3aa31ebb68a06ceb46bbd6cf49b6ccd6f5e6",
|
||||
"type": "COMMIT",
|
||||
},
|
||||
"nominalAuthor": Object {
|
||||
"login": "decentralion",
|
||||
"type": "USERLIKE",
|
||||
},
|
||||
"reviews": Array [
|
||||
Object {
|
||||
"id": "100313899",
|
||||
"pull": Object {
|
||||
"number": "5",
|
||||
"repo": Object {
|
||||
"name": "example-github",
|
||||
"owner": "sourcecred",
|
||||
"type": "REPO",
|
||||
},
|
||||
"type": "PULL",
|
||||
},
|
||||
"type": "REVIEW",
|
||||
},
|
||||
Object {
|
||||
"id": "100314038",
|
||||
"pull": Object {
|
||||
"number": "5",
|
||||
"repo": Object {
|
||||
"name": "example-github",
|
||||
"owner": "sourcecred",
|
||||
"type": "REPO",
|
||||
},
|
||||
"type": "PULL",
|
||||
},
|
||||
"type": "REVIEW",
|
||||
},
|
||||
],
|
||||
"title": "This pull request will be more contentious. I can feel it...",
|
||||
"url": "https://github.com/sourcecred/example-github/pull/5",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`plugins/github/relationalView repo matches snapshot 1`] = `
|
||||
Object {
|
||||
"address": Object {
|
||||
"name": "example-github",
|
||||
"owner": "sourcecred",
|
||||
"type": "REPO",
|
||||
},
|
||||
"issues": Array [
|
||||
Object {
|
||||
"number": "1",
|
||||
"repo": Object {
|
||||
"name": "example-github",
|
||||
"owner": "sourcecred",
|
||||
"type": "REPO",
|
||||
},
|
||||
"type": "ISSUE",
|
||||
},
|
||||
Object {
|
||||
"number": "2",
|
||||
"repo": Object {
|
||||
"name": "example-github",
|
||||
"owner": "sourcecred",
|
||||
"type": "REPO",
|
||||
},
|
||||
"type": "ISSUE",
|
||||
},
|
||||
Object {
|
||||
"number": "4",
|
||||
"repo": Object {
|
||||
"name": "example-github",
|
||||
"owner": "sourcecred",
|
||||
"type": "REPO",
|
||||
},
|
||||
"type": "ISSUE",
|
||||
},
|
||||
Object {
|
||||
"number": "6",
|
||||
"repo": Object {
|
||||
"name": "example-github",
|
||||
"owner": "sourcecred",
|
||||
"type": "REPO",
|
||||
},
|
||||
"type": "ISSUE",
|
||||
},
|
||||
Object {
|
||||
"number": "7",
|
||||
"repo": Object {
|
||||
"name": "example-github",
|
||||
"owner": "sourcecred",
|
||||
"type": "REPO",
|
||||
},
|
||||
"type": "ISSUE",
|
||||
},
|
||||
Object {
|
||||
"number": "8",
|
||||
"repo": Object {
|
||||
"name": "example-github",
|
||||
"owner": "sourcecred",
|
||||
"type": "REPO",
|
||||
},
|
||||
"type": "ISSUE",
|
||||
},
|
||||
],
|
||||
"pulls": Array [
|
||||
Object {
|
||||
"number": "3",
|
||||
"repo": Object {
|
||||
"name": "example-github",
|
||||
"owner": "sourcecred",
|
||||
"type": "REPO",
|
||||
},
|
||||
"type": "PULL",
|
||||
},
|
||||
Object {
|
||||
"number": "5",
|
||||
"repo": Object {
|
||||
"name": "example-github",
|
||||
"owner": "sourcecred",
|
||||
"type": "REPO",
|
||||
},
|
||||
"type": "PULL",
|
||||
},
|
||||
Object {
|
||||
"number": "9",
|
||||
"repo": Object {
|
||||
"name": "example-github",
|
||||
"owner": "sourcecred",
|
||||
"type": "REPO",
|
||||
},
|
||||
"type": "PULL",
|
||||
},
|
||||
],
|
||||
"url": "https://github.com/sourcecred/example-github",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`plugins/github/relationalView review matches snapshot 1`] = `
|
||||
Object {
|
||||
"address": Object {
|
||||
"id": "100313899",
|
||||
"pull": Object {
|
||||
"number": "5",
|
||||
"repo": Object {
|
||||
"name": "example-github",
|
||||
"owner": "sourcecred",
|
||||
"type": "REPO",
|
||||
},
|
||||
"type": "PULL",
|
||||
},
|
||||
"type": "REVIEW",
|
||||
},
|
||||
"body": "hmmm.jpg",
|
||||
"comments": Array [
|
||||
Object {
|
||||
"id": "171460198",
|
||||
"parent": Object {
|
||||
"id": "100313899",
|
||||
"pull": Object {
|
||||
"number": "5",
|
||||
"repo": Object {
|
||||
"name": "example-github",
|
||||
"owner": "sourcecred",
|
||||
"type": "REPO",
|
||||
},
|
||||
"type": "PULL",
|
||||
},
|
||||
"type": "REVIEW",
|
||||
},
|
||||
"type": "COMMENT",
|
||||
},
|
||||
],
|
||||
"nominalAuthor": Object {
|
||||
"login": "wchargin",
|
||||
"type": "USERLIKE",
|
||||
},
|
||||
"state": "CHANGES_REQUESTED",
|
||||
"url": "https://github.com/sourcecred/example-github/pull/5#pullrequestreview-100313899",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`plugins/github/relationalView userlike matches snapshot 1`] = `
|
||||
Object {
|
||||
"address": Object {
|
||||
"login": "decentralion",
|
||||
"type": "USERLIKE",
|
||||
},
|
||||
"url": "https://github.com/decentralion",
|
||||
}
|
||||
`;
|
|
@ -0,0 +1,226 @@
|
|||
// @flow
|
||||
|
||||
import * as N from "./nodes";
|
||||
import * as Q from "./graphql";
|
||||
import * as GitNode from "../git/nodes";
|
||||
|
||||
import {
|
||||
reviewUrlToId,
|
||||
issueCommentUrlToId,
|
||||
pullCommentUrlToId,
|
||||
reviewCommentUrlToId,
|
||||
} from "./urlIdParse";
|
||||
|
||||
export type RepoEntry = {|
|
||||
+address: N.RepoAddress,
|
||||
+url: string,
|
||||
+issues: N.IssueAddress[],
|
||||
+pulls: N.PullAddress[],
|
||||
|};
|
||||
|
||||
export type IssueEntry = {|
|
||||
+address: N.IssueAddress,
|
||||
+title: string,
|
||||
+body: string,
|
||||
+url: string,
|
||||
+comments: N.CommentAddress[],
|
||||
+nominalAuthor: ?N.UserlikeAddress,
|
||||
|};
|
||||
|
||||
export type PullEntry = {|
|
||||
+address: N.PullAddress,
|
||||
+title: string,
|
||||
+body: string,
|
||||
+url: string,
|
||||
+comments: N.CommentAddress[],
|
||||
+reviews: N.ReviewAddress[],
|
||||
+mergedAs: ?GitNode.CommitAddress,
|
||||
+nominalAuthor: ?N.UserlikeAddress,
|
||||
|};
|
||||
|
||||
export type ReviewEntry = {|
|
||||
+address: N.ReviewAddress,
|
||||
+body: string,
|
||||
+url: string,
|
||||
+comments: N.CommentAddress[],
|
||||
+state: Q.ReviewState,
|
||||
+nominalAuthor: ?N.UserlikeAddress,
|
||||
|};
|
||||
|
||||
export type CommentEntry = {|
|
||||
+address: N.CommentAddress,
|
||||
+body: string,
|
||||
+url: string,
|
||||
+nominalAuthor: ?N.UserlikeAddress,
|
||||
|};
|
||||
|
||||
export type UserlikeEntry = {|
|
||||
+address: N.UserlikeAddress,
|
||||
+url: string,
|
||||
|};
|
||||
|
||||
export class RelationalView {
|
||||
_repos: Map<N.RawAddress, RepoEntry>;
|
||||
_issues: Map<N.RawAddress, IssueEntry>;
|
||||
_pulls: Map<N.RawAddress, PullEntry>;
|
||||
_comments: Map<N.RawAddress, CommentEntry>;
|
||||
_reviews: Map<N.RawAddress, ReviewEntry>;
|
||||
_userlikes: Map<N.RawAddress, UserlikeEntry>;
|
||||
|
||||
constructor(data: Q.GithubResponseJSON) {
|
||||
this._repos = new Map();
|
||||
this._issues = new Map();
|
||||
this._pulls = new Map();
|
||||
this._comments = new Map();
|
||||
this._reviews = new Map();
|
||||
this._userlikes = new Map();
|
||||
this._addRepo(data.repository);
|
||||
}
|
||||
|
||||
repos(): Iterator<RepoEntry> {
|
||||
return this._repos.values();
|
||||
}
|
||||
|
||||
repo(address: N.RepoAddress): ?RepoEntry {
|
||||
return this._repos.get(N.toRaw(address));
|
||||
}
|
||||
|
||||
issue(address: N.IssueAddress): ?IssueEntry {
|
||||
return this._issues.get(N.toRaw(address));
|
||||
}
|
||||
pull(address: N.PullAddress): ?PullEntry {
|
||||
return this._pulls.get(N.toRaw(address));
|
||||
}
|
||||
comment(address: N.CommentAddress): ?CommentEntry {
|
||||
return this._comments.get(N.toRaw(address));
|
||||
}
|
||||
review(address: N.ReviewAddress): ?ReviewEntry {
|
||||
return this._reviews.get(N.toRaw(address));
|
||||
}
|
||||
userlike(address: N.UserlikeAddress): ?UserlikeEntry {
|
||||
return this._userlikes.get(N.toRaw(address));
|
||||
}
|
||||
|
||||
_addRepo(json: Q.RepositoryJSON) {
|
||||
const address: N.RepoAddress = {
|
||||
type: N.REPO_TYPE,
|
||||
owner: json.owner.login,
|
||||
name: json.name,
|
||||
};
|
||||
const entry: RepoEntry = {
|
||||
address,
|
||||
url: json.url,
|
||||
issues: json.issues.nodes.map((x) => this._addIssue(address, x)),
|
||||
pulls: json.pulls.nodes.map((x) => this._addPull(address, x)),
|
||||
};
|
||||
const raw = N.toRaw(address);
|
||||
this._repos.set(raw, entry);
|
||||
}
|
||||
|
||||
_addIssue(repo: N.RepoAddress, json: Q.IssueJSON): N.IssueAddress {
|
||||
const address: N.IssueAddress = {
|
||||
type: N.ISSUE_TYPE,
|
||||
number: String(json.number),
|
||||
repo,
|
||||
};
|
||||
const entry: IssueEntry = {
|
||||
address,
|
||||
url: json.url,
|
||||
comments: json.comments.nodes.map((x) => this._addComment(address, x)),
|
||||
nominalAuthor: this._addNullableAuthor(json.author),
|
||||
body: json.body,
|
||||
title: json.title,
|
||||
};
|
||||
this._issues.set(N.toRaw(address), entry);
|
||||
return address;
|
||||
}
|
||||
|
||||
_addPull(repo: N.RepoAddress, json: Q.PullJSON): N.PullAddress {
|
||||
const address: N.PullAddress = {
|
||||
type: N.PULL_TYPE,
|
||||
number: String(json.number),
|
||||
repo,
|
||||
};
|
||||
const mergedAs =
|
||||
json.mergeCommit == null
|
||||
? null
|
||||
: {
|
||||
type: GitNode.COMMIT_TYPE,
|
||||
hash: json.mergeCommit.oid,
|
||||
};
|
||||
|
||||
const entry: PullEntry = {
|
||||
address,
|
||||
url: json.url,
|
||||
comments: json.comments.nodes.map((x) => this._addComment(address, x)),
|
||||
reviews: json.reviews.nodes.map((x) => this._addReview(address, x)),
|
||||
nominalAuthor: this._addNullableAuthor(json.author),
|
||||
body: json.body,
|
||||
title: json.title,
|
||||
mergedAs,
|
||||
};
|
||||
this._pulls.set(N.toRaw(address), entry);
|
||||
return address;
|
||||
}
|
||||
|
||||
_addReview(pull: N.PullAddress, json: Q.ReviewJSON): N.ReviewAddress {
|
||||
const address: N.ReviewAddress = {
|
||||
type: N.REVIEW_TYPE,
|
||||
id: reviewUrlToId(json.url),
|
||||
pull,
|
||||
};
|
||||
const entry: ReviewEntry = {
|
||||
address,
|
||||
url: json.url,
|
||||
state: json.state,
|
||||
comments: json.comments.nodes.map((x) => this._addComment(address, x)),
|
||||
body: json.body,
|
||||
nominalAuthor: this._addNullableAuthor(json.author),
|
||||
};
|
||||
this._reviews.set(N.toRaw(address), entry);
|
||||
return address;
|
||||
}
|
||||
|
||||
_addComment(
|
||||
parent: N.IssueAddress | N.PullAddress | N.ReviewAddress,
|
||||
json: Q.CommentJSON
|
||||
): N.CommentAddress {
|
||||
const id = (function() {
|
||||
switch (parent.type) {
|
||||
case N.ISSUE_TYPE:
|
||||
return issueCommentUrlToId(json.url);
|
||||
case N.PULL_TYPE:
|
||||
return pullCommentUrlToId(json.url);
|
||||
case N.REVIEW_TYPE:
|
||||
return reviewCommentUrlToId(json.url);
|
||||
default:
|
||||
// eslint-disable-next-line no-unused-expressions
|
||||
(parent.type: empty);
|
||||
throw new Error(`Unexpected comment parent type: ${parent.type}`);
|
||||
}
|
||||
})();
|
||||
const address: N.CommentAddress = {type: N.COMMENT_TYPE, id, parent};
|
||||
const entry: CommentEntry = {
|
||||
address,
|
||||
url: json.url,
|
||||
nominalAuthor: this._addNullableAuthor(json.author),
|
||||
body: json.body,
|
||||
};
|
||||
this._comments.set(N.toRaw(address), entry);
|
||||
return address;
|
||||
}
|
||||
|
||||
_addNullableAuthor(json: Q.NullableAuthorJSON): ?N.UserlikeAddress {
|
||||
if (json == null) {
|
||||
return null;
|
||||
} else {
|
||||
const address: N.UserlikeAddress = {
|
||||
type: N.USERLIKE_TYPE,
|
||||
login: json.login,
|
||||
};
|
||||
const entry: UserlikeEntry = {address, url: json.url};
|
||||
this._userlikes.set(N.toRaw(address), entry);
|
||||
return address;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,70 @@
|
|||
// @flow
|
||||
|
||||
import * as R from "./relationalView";
|
||||
|
||||
describe("plugins/github/relationalView", () => {
|
||||
const data = require("./demoData/example-github");
|
||||
// Sharing this state is OK because it's just a view - no mutation allowed!
|
||||
const view = new R.RelationalView(data);
|
||||
|
||||
function assertNotNull<T>(x: ?T): T {
|
||||
if (x == null) {
|
||||
throw new Error(`Assertion fail: ${String(x)} `);
|
||||
}
|
||||
return x;
|
||||
}
|
||||
const repos = () => Array.from(view.repos());
|
||||
it("there is one repository", () => {
|
||||
expect(repos()).toHaveLength(1);
|
||||
});
|
||||
|
||||
const repo = () => assertNotNull(repos()[0]);
|
||||
it("repo matches snapshot", () => {
|
||||
expect(repo()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("repo retrievable by address", () => {
|
||||
expect(view.repo(repo().address)).toEqual(repo());
|
||||
});
|
||||
|
||||
const issue = () => assertNotNull(view.issue(repo().issues[1]));
|
||||
it("issue matches snapshot", () => {
|
||||
expect(issue()).toMatchSnapshot();
|
||||
});
|
||||
it("issue retrievable by address", () => {
|
||||
expect(view.issue(issue().address)).toEqual(issue());
|
||||
});
|
||||
|
||||
const pull = () => assertNotNull(view.pull(repo().pulls[1]));
|
||||
it("pull matches snapshot", () => {
|
||||
expect(pull()).toMatchSnapshot();
|
||||
});
|
||||
it("pull retrievable by address", () => {
|
||||
expect(view.pull(pull().address)).toEqual(pull());
|
||||
});
|
||||
|
||||
const review = () => assertNotNull(view.review(pull().reviews[0]));
|
||||
it("review matches snapshot", () => {
|
||||
expect(review()).toMatchSnapshot();
|
||||
});
|
||||
it("review retrievable by address", () => {
|
||||
expect(view.review(review().address)).toEqual(review());
|
||||
});
|
||||
|
||||
const comment = () => assertNotNull(view.comment(issue().comments[0]));
|
||||
it("comment matches snapshot", () => {
|
||||
expect(comment()).toMatchSnapshot();
|
||||
});
|
||||
it("comment retrievable by address", () => {
|
||||
expect(view.comment(comment().address)).toEqual(comment());
|
||||
});
|
||||
|
||||
const userlike = () =>
|
||||
assertNotNull(view.userlike(assertNotNull(issue().nominalAuthor)));
|
||||
it("userlike matches snapshot", () => {
|
||||
expect(userlike()).toMatchSnapshot();
|
||||
});
|
||||
it("userlike retrievable by address", () => {
|
||||
expect(view.userlike(userlike().address)).toEqual(userlike());
|
||||
});
|
||||
});
|
Loading…
Reference in New Issue