github: remove legacy continuations code (#964)

Summary:
It is time. (Replaced with #622.)

Test Plan:
Running `yarn flow` suffices. Running `yarn test --full` also passes.

wchargin-branch: remove-legacy-graphql
This commit is contained in:
William Chargin 2018-10-31 12:45:59 -07:00 committed by GitHub
parent 2e0b17cef7
commit 6b789d61d6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 5 additions and 3049 deletions

View File

@ -1,408 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`plugins/github/graphql #postQueryExhaustive resolves a representative query 1`] = `
Object {
"repository": Object {
"id": "opaque-repo",
"issues": Object {
"nodes": Array [
Object {
"author": Object {
"__typename": "User",
"id": "opaque-user-decentralion",
"login": "decentralion",
},
"body": "Like it says, please comment!",
"comments": Object {
"nodes": Array [
Object {
"author": Object {
"__typename": "User",
"id": "opaque-user-decentralion",
"login": "decentralion",
},
"body": "Here: I'll start.",
"id": "opaque-issue1comment1",
"url": "opaque://issue/1/comment/1",
},
Object {
"author": Object {
"__typename": "User",
"id": "opaque-user-wchargin",
"login": "wchargin",
},
"body": "Closing due to no fun allowed.",
"id": "opaque-issue1comment2",
"url": "opaque://issue/1/comment/2",
},
Object {
"author": Object {
"__typename": "User",
"id": "opaque-user-decentralion",
"login": "decentralion",
},
"body": "That is not very nice.",
"id": "opaque-issue1comment3",
"url": "opaque://issue/1/comment/3",
},
],
"pageInfo": Object {
"endCursor": "opaque-cursor-issue1comments-v2",
"hasNextPage": false,
},
},
"id": "opaque-issue1",
"number": 1,
"title": "Request for comments",
},
Object {
"author": Object {
"__typename": "User",
"id": "opaque-user-wchargin",
"login": "wchargin",
},
"body": "You can comment here, too.",
"comments": Object {
"nodes": Array [
Object {
"author": Object {
"__typename": "User",
"id": "opaque-user-decentralion",
"login": "decentralion",
},
"body": "What fun!",
"id": "opaque-issue3comment1",
"url": "opaque://issue/3/comment/1",
},
Object {
"author": Object {
"__typename": "User",
"id": "opaque-user-decentralion",
"login": "decentralion",
},
"body": "I will comment on this issue for a second time.",
"id": "opaque-issue3comment2",
"url": "opaque://issue/1/comment/3",
},
],
"pageInfo": Object {
"endCursor": "opaque-cursor-issue3comments-v2",
"hasNextPage": false,
},
},
"id": "opaque-issue3",
"number": 2,
"title": "Another",
},
Object {
"author": Object {
"__typename": "User",
"id": "opaque-user-wchargin",
"login": "wchargin",
},
"body": "My mailbox is out of space",
"comments": Object {
"nodes": Array [
Object {
"author": Object {
"__typename": "User",
"id": "opaque-user-decentralion",
"login": "decentralion",
},
"body": "But you posted the last issue",
"id": "opaque-issue4comment1",
"url": "opaque://issue/4/comment/1",
},
],
"pageInfo": Object {
"endCursor": "opaque-cursor-issue4comments-v2",
"hasNextPage": false,
},
},
"id": "opaque-issue4",
"number": 4,
"title": "Please stop making issues",
},
],
"pageInfo": Object {
"endCursor": "opaque-cursor-issues-v2",
"hasNextPage": false,
},
},
"pulls": Object {
"nodes": Array [
Object {
"author": Object {
"__typename": "User",
"id": "opaque-user-wchargin",
"login": "wchargin",
},
"body": "Surely this deserves much cred.",
"comments": Object {
"nodes": Array [],
"pageInfo": Object {
"endCursor": null,
"hasNextPage": false,
},
},
"id": "opaque-pull2",
"number": 2,
"reviews": Object {
"nodes": Array [
Object {
"author": Object {
"__typename": "User",
"id": "opaque-user-decentralion",
"login": "decentralion",
},
"body": "You actually introduced a new typo instead.",
"comments": Object {
"nodes": Array [],
"pageInfo": Object {
"endCursor": null,
"hasNextPage": false,
},
},
"id": "opaque-pull2review1",
"state": "CHANGES_REQUESTED",
},
Object {
"author": Object {
"__typename": "User",
"id": "opaque-user-decentralion",
"login": "decentralion",
},
"body": "Looks godo to me.",
"comments": Object {
"nodes": Array [],
"pageInfo": Object {
"endCursor": null,
"hasNextPage": false,
},
},
"id": "opaque-pull2review2",
"state": "APPROVED",
},
],
"pageInfo": Object {
"endCursor": "opaque-cursor-pull2reviews-v1",
"hasNextPage": false,
},
},
"title": "Fix typo in README",
},
],
"pageInfo": Object {
"endCursor": "opaque-cursor-pulls-v0",
"hasNextPage": false,
},
},
},
}
`;
exports[`plugins/github/graphql creates a query 1`] = `
"query FetchData($owner: String! $name: String!) {
repository(owner: $owner name: $name) {
url
name
owner {
...whoami
}
id
issues(first: 50) {
...issues
}
pulls: pullRequests(first: 50) {
...pulls
}
defaultBranchRef {
id
target {
__typename
... on Commit {
history(first: 100) {
...commitHistory
}
}
... on Blob {
id
oid
}
... on Tag {
id
oid
}
... on Tree {
id
oid
}
}
}
}
}
fragment whoami on Actor {
__typename
login
url
... on User {
id
}
... on Organization {
id
}
... on Bot {
id
}
}
fragment issues on IssueConnection {
pageInfo {
hasNextPage
endCursor
}
nodes {
id
url
title
body
number
author {
...whoami
}
comments(first: 20) {
...comments
}
reactions(first: 5) {
...reactions
}
}
}
fragment pulls on PullRequestConnection {
pageInfo {
hasNextPage
endCursor
}
nodes {
id
url
title
body
number
mergeCommit {
...commit
}
additions
deletions
author {
...whoami
}
comments(first: 20) {
...comments
}
reviews(first: 5) {
...reviews
}
reactions(first: 5) {
...reactions
}
}
}
fragment comments on IssueCommentConnection {
pageInfo {
hasNextPage
endCursor
}
nodes {
id
url
author {
...whoami
}
body
reactions(first: 5) {
...reactions
}
}
}
fragment reviews on PullRequestReviewConnection {
pageInfo {
hasNextPage
endCursor
}
nodes {
id
url
body
author {
...whoami
}
state
comments(first: 10) {
...reviewComments
}
}
}
fragment reviewComments on PullRequestReviewCommentConnection {
pageInfo {
hasNextPage
endCursor
}
nodes {
id
url
body
author {
...whoami
}
reactions(first: 5) {
...reactions
}
}
}
fragment commitHistory on CommitHistoryConnection {
pageInfo {
hasNextPage
endCursor
}
nodes {
...commit
}
}
fragment commitParents on CommitConnection {
pageInfo {
hasNextPage
endCursor
}
nodes {
oid
}
}
fragment commit on Commit {
id
url
oid
message
author {
date
user {
...whoami
}
}
parents(first: 5) {
...commitParents
}
}
fragment reactions on ReactionConnection {
pageInfo {
hasNextPage
endCursor
}
nodes {
id
content
user {
...whoami
}
}
}"
`;

File diff suppressed because it is too large Load Diff

View File

@ -1,964 +0,0 @@
// @flow
import type {Continuation} from "./graphql";
import {build, stringify, multilineLayout} from "../../graphql/queries";
import {
PAGE_LIMIT,
createQuery,
createVariables,
continuationsFromQuery,
continuationsFromContinuation,
createFragments,
merge,
postQueryExhaustive,
requiredFragments,
} from "./graphql";
import {makeRepoId} from "../../core/repoId";
describe("plugins/github/graphql", () => {
describe("creates continuations", () => {
const makeAuthor = (name) => ({
__typename: "User",
login: name,
id: `opaque-user-${name}`,
});
function makeData(hasNextPageFor: {
issues: boolean,
pulls: boolean,
issueComments: boolean,
pullComments: boolean,
reviews: boolean,
reviewComments: boolean,
}) {
return {
repository: {
id: "opaque-repo",
issues: {
pageInfo: {
hasNextPage: hasNextPageFor.issues,
endCursor: "opaque-cursor-issues",
},
nodes: [
{
id: "opaque-issue1",
title: "A pressing issue",
body: "<button>A</button>",
number: 1,
author: makeAuthor("decentralion"),
comments: {
pageInfo: {
hasNextPage: hasNextPageFor.issueComments,
endCursor: "opaque-cursor-issue1comments",
},
nodes: [
{
id: "opaque-issue1comment1",
author: makeAuthor("wchargin"),
body: "I wish pancakes were still in vogue.",
url: "opaque://issue/1/comment/1",
},
],
},
},
],
},
pulls: {
pageInfo: {
hasNextPage: hasNextPageFor.pulls,
endCursor: "opaque-cursor-pulls",
},
nodes: [
{
id: "opaque-pull2",
title: "texdoc exam",
body: "What is air?",
number: 2,
author: makeAuthor("wchargin"),
comments: {
pageInfo: {
hasNextPage: hasNextPageFor.pullComments,
endCursor: "opaque-cursor-pull2comments",
},
nodes: [
{
id: "opaque-pull2comment1",
author: makeAuthor("decentralion"),
body: "Why is there air?",
url: "opaque://pull/2/comment/1",
},
],
},
reviews: {
pageInfo: {
hasNextPage: hasNextPageFor.reviews,
endCursor: "opaque-cursor-pull2reviews",
},
nodes: [
{
id: "opaque-pull2review1",
body: "Hmmm...",
author: makeAuthor("decentralion"),
state: "CHANGES_REQUESTED",
comments: {
pageInfo: {
hasNextPage: hasNextPageFor.reviewComments,
endCursor: "opaque-cursor-pull2review1comments",
},
nodes: [
{
id: "opaque-pull2review1comment1",
body: "What if there were no air?",
url: "opaque://pull/2/review/1/comment/1",
author: makeAuthor("decentralion"),
},
],
},
},
],
},
},
],
},
},
};
}
function makeContinuations(): {[string]: Continuation} {
const b = build;
return {
issues: {
enclosingNodeType: "REPOSITORY",
enclosingNodeId: "opaque-repo",
selections: [
b.inlineFragment("Repository", [
b.field(
"issues",
{
first: b.literal(PAGE_LIMIT),
after: b.literal("opaque-cursor-issues"),
},
[b.fragmentSpread("issues")]
),
]),
],
destinationPath: ["repository"],
},
pulls: {
enclosingNodeType: "REPOSITORY",
enclosingNodeId: "opaque-repo",
selections: [
b.inlineFragment("Repository", [
b.alias(
"pulls",
b.field(
"pullRequests",
{
first: b.literal(PAGE_LIMIT),
after: b.literal("opaque-cursor-pulls"),
},
[b.fragmentSpread("pulls")]
)
),
]),
],
destinationPath: ["repository"],
},
issueComments: {
enclosingNodeType: "ISSUE",
enclosingNodeId: "opaque-issue1",
selections: [
b.inlineFragment("Issue", [
b.field(
"comments",
{
first: b.literal(PAGE_LIMIT),
after: b.literal("opaque-cursor-issue1comments"),
},
[b.fragmentSpread("comments")]
),
]),
],
destinationPath: ["repository", "issues", "nodes", 0],
},
pullComments: {
enclosingNodeType: "PULL",
enclosingNodeId: "opaque-pull2",
selections: [
b.inlineFragment("PullRequest", [
b.field(
"comments",
{
first: b.literal(PAGE_LIMIT),
after: b.literal("opaque-cursor-pull2comments"),
},
[b.fragmentSpread("comments")]
),
]),
],
destinationPath: ["repository", "pulls", "nodes", 0],
},
reviews: {
enclosingNodeType: "PULL",
enclosingNodeId: "opaque-pull2",
selections: [
b.inlineFragment("PullRequest", [
b.field(
"reviews",
{
first: b.literal(PAGE_LIMIT),
after: b.literal("opaque-cursor-pull2reviews"),
},
[b.fragmentSpread("reviews")]
),
]),
],
destinationPath: ["repository", "pulls", "nodes", 0],
},
reviewComments: {
enclosingNodeType: "REVIEW",
enclosingNodeId: "opaque-pull2review1",
selections: [
b.inlineFragment("PullRequestReview", [
b.field(
"comments",
{
first: b.literal(PAGE_LIMIT),
after: b.literal("opaque-cursor-pull2review1comments"),
},
[b.fragmentSpread("reviewComments")]
),
]),
],
destinationPath: [
"repository",
"pulls",
"nodes",
0,
"reviews",
"nodes",
0,
],
},
};
}
test("from a top-level result with lots of continuations", () => {
const data = makeData({
issues: true,
pulls: true,
issueComments: true,
pullComments: true,
reviews: true,
reviewComments: true,
});
const result = Array.from(continuationsFromQuery(data));
const expectedContinuations: Continuation[] = (() => {
const continuations = makeContinuations();
return [
continuations.issues,
continuations.pulls,
continuations.issueComments,
continuations.pullComments,
continuations.reviews,
continuations.reviewComments,
];
})();
expectedContinuations.forEach((x) => {
expect(result).toContainEqual(x);
});
expect(result).toHaveLength(expectedContinuations.length);
});
test("from a top-level result with sparse continuations", () => {
// Here, some elements have continuations, but are children of
// elements without continuations. This tests that we always recur
// through the whole structure.
const data = makeData({
issues: true,
pulls: false,
issueComments: false,
pullComments: true,
reviews: false,
reviewComments: true,
});
const result = Array.from(continuationsFromQuery(data));
const expectedContinuations: Continuation[] = (() => {
const continuations = makeContinuations();
return [
continuations.issues,
continuations.pullComments,
continuations.reviewComments,
];
})();
expectedContinuations.forEach((x) => {
expect(result).toContainEqual(x);
});
expect(result).toHaveLength(expectedContinuations.length);
});
describe("from another continuation", () => {
function makeContinuationResult(hasNextPages: boolean) {
return {
issues: {
pageInfo: {
hasNextPage: hasNextPages,
endCursor: "opaque-cursor-moreissues",
},
nodes: [
{
id: "opaque-issue3",
title: "todo",
body: "it means everything",
number: 3,
author: makeAuthor("wchargin"),
comments: {
pageInfo: {
hasNextPage: hasNextPages,
endCursor: "opaque-cursor-issue3comments",
},
nodes: [
{
id: "opaque-issue3comment1",
author: makeAuthor("decentralion"),
body:
"if it means everything, does it really mean anything?",
url: "opaque://issue/3/comment/1",
},
],
},
},
],
},
};
}
test("when there are more pages at multiple levels of nesting", () => {
const continuation = makeContinuations().issues;
const continuationResult = makeContinuationResult(true);
const result = Array.from(
continuationsFromContinuation(continuationResult, continuation)
);
const b = build;
const expectedContinuations = [
{
enclosingNodeType: "REPOSITORY",
enclosingNodeId: "opaque-repo",
selections: [
b.inlineFragment("Repository", [
b.field(
"issues",
{
first: b.literal(PAGE_LIMIT),
after: b.literal("opaque-cursor-moreissues"),
},
[b.fragmentSpread("issues")]
),
]),
],
destinationPath: ["repository"],
},
{
enclosingNodeType: "ISSUE",
enclosingNodeId: "opaque-issue3",
selections: [
b.inlineFragment("Issue", [
b.field(
"comments",
{
first: b.literal(PAGE_LIMIT),
after: b.literal("opaque-cursor-issue3comments"),
},
[b.fragmentSpread("comments")]
),
]),
],
destinationPath: ["repository", "issues", "nodes", 0],
},
];
expectedContinuations.forEach((x) => {
expect(result).toContainEqual(x);
});
expect(result).toHaveLength(expectedContinuations.length);
});
test("when there are no more pages", () => {
const continuation = makeContinuations().issues;
const continuationResult = makeContinuationResult(false);
const result = Array.from(
continuationsFromContinuation(continuationResult, continuation)
);
expect(result).toHaveLength(0);
});
});
});
describe("#merge", () => {
describe("merges at the root", () => {
it("replacing primitive numbers", () => {
expect(merge(3, 5, [])).toEqual(5);
});
it("replacing primitive strings", () => {
expect(merge("three", "five", [])).toEqual("five");
});
it("replacing a primitive string with null", () => {
expect(merge("three", null, [])).toEqual(null);
});
it("replacing null with a number", () => {
expect(merge(null, 3, [])).toEqual(3);
});
it("concatenating arrays", () => {
expect(merge([1, 2], [3, 4], [])).toEqual([1, 2, 3, 4]);
});
it("merging objects", () => {
const destination = {a: 1, b: 2};
const source = {c: 3, d: 4};
const expected = {a: 1, b: 2, c: 3, d: 4};
expect(merge(destination, source, [])).toEqual(expected);
});
it("overwriting primitives in an object", () => {
const destination = {hasNextPage: true, endCursor: "cursor-aaa"};
const source = {hasNextPage: false, endCursor: "cursor-bbb"};
expect(merge(destination, source, [])).toEqual(source);
});
it("merging complex structures recursively", () => {
const destination = {
fst: {a: 1, b: 2},
snd: {e: 5, f: 6},
fruits: ["apple", "banana"],
letters: ["whiskey", "x-ray"],
};
const source = {
fst: {c: 3, d: 4},
snd: {g: 7, h: 8},
fruits: ["cherry", "durian"],
letters: ["yankee", "zulu"],
};
const expected = {
fst: {a: 1, b: 2, c: 3, d: 4},
snd: {e: 5, f: 6, g: 7, h: 8},
fruits: ["apple", "banana", "cherry", "durian"],
letters: ["whiskey", "x-ray", "yankee", "zulu"],
};
expect(merge(destination, source, [])).toEqual(expected);
});
});
describe("traverses", () => {
it("down an object path", () => {
const destination = {
child: {
grandchild: {
one: 1,
two: 2,
},
otherGrandchild: "world",
},
otherChild: "hello",
};
const source = {
three: 3,
four: 4,
};
const expected = {
child: {
grandchild: {
one: 1,
two: 2,
three: 3,
four: 4,
},
otherGrandchild: "world",
},
otherChild: "hello",
};
expect(merge(destination, source, ["child", "grandchild"])).toEqual(
expected
);
});
it("down an array path", () => {
const destination = [["change me", [1, 2]], ["ignore me", [5, 6]]];
const source = [3, 4];
const expected = [["change me", [1, 2, 3, 4]], ["ignore me", [5, 6]]];
expect(merge(destination, source, [0, 1])).toEqual(expected);
});
it("down a path of mixed objects and arrays", () => {
const destination = {
families: [
{
childCount: 3,
children: [
{name: "Alice", hobbies: ["acupuncture"]},
{name: "Bob", hobbies: ["billiards"]},
{name: "Cheryl", hobbies: ["chess"]},
],
},
{
childCount: 0,
children: [],
},
],
};
const path = ["families", 0, "children", 2, "hobbies"];
const source = ["charades", "cheese-rolling"];
const expected = {
families: [
{
childCount: 3,
children: [
{name: "Alice", hobbies: ["acupuncture"]},
{name: "Bob", hobbies: ["billiards"]},
{
name: "Cheryl",
hobbies: ["chess", "charades", "cheese-rolling"],
},
],
},
{childCount: 0, children: []},
],
};
expect(merge(destination, source, path)).toEqual(expected);
});
});
describe("doesn't mutate its inputs", () => {
it("when merging arrays", () => {
const destination = [1, 2];
const source = [3, 4];
merge(destination, source, []);
expect(destination).toEqual([1, 2]);
expect(source).toEqual([3, 4]);
});
it("when merging objects", () => {
const destination = {a: 1, b: 2};
const source = {c: 3, d: 4};
merge(destination, source, []);
expect(destination).toEqual({a: 1, b: 2});
expect(source).toEqual({c: 3, d: 4});
});
test("along an object path", () => {
const makeDestination = () => ({
child: {
grandchild: {
one: 1,
two: 2,
},
otherGrandchild: "world",
},
otherChild: "hello",
});
const makeSource = () => ({
three: 3,
four: 4,
});
const destination = makeDestination();
const source = makeSource();
merge(destination, source, ["child", "grandchild"]);
expect(destination).toEqual(makeDestination());
expect(source).toEqual(makeSource());
});
test("along an array path", () => {
const makeDestination = () => [
["change me", [1, 2]],
["ignore me", [5, 6]],
];
const makeSource = () => [3, 4];
const destination = makeDestination();
const source = makeSource();
merge(destination, source, [0, 1]);
expect(destination).toEqual(makeDestination());
expect(source).toEqual(makeSource());
});
});
describe("complains", () => {
describe("about bad keys", () => {
it("when given a numeric key into a primitive", () => {
expect(() => merge(123, 234, [0])).toThrow(/non-array/);
});
it("when given a numeric key into null", () => {
expect(() => merge(null, null, [0])).toThrow(/non-array/);
});
describe("when given a numeric key into an object", () => {
test("for the usual case of an object with string keys", () => {
expect(() => merge({a: 1}, {b: 2}, [0])).toThrow(/non-array/);
});
test("even when the object has the stringifed version of the key", () => {
expect(() =>
merge({"0": "zero", "1": "one"}, {"2": "two"}, [0])
).toThrow(/non-array/);
});
});
it("when given a string key into a primitive", () => {
expect(() => merge(123, 234, ["k"])).toThrow(/non-object/);
});
it("when given a string key into null", () => {
expect(() => merge(null, null, ["k"])).toThrow(/non-object/);
});
it("when given a string key into an array", () => {
expect(() => merge([1, 2], [1, 2], ["k"])).toThrow(/non-object/);
});
it("when given a non-string, non-numeric key", () => {
const badKey: any = false;
expect(() => merge({a: 1}, {b: 2}, [badKey])).toThrow(/key.*false/);
});
it("when given a non-existent string key", () => {
expect(() => merge({a: 1}, {b: 2}, ["c"])).toThrow(/"c" not found/);
});
it("when given a non-existent numeric key", () => {
expect(() => merge([1], [2], [3])).toThrow(/3 not found/);
});
});
describe("about source/destination mismatch", () => {
it("when merging an array into a non-array", () => {
const re = () => /array into non-array/;
expect(() => merge({a: 1}, [2], [])).toThrow(re());
expect(() => merge(true, [2], [])).toThrow(re());
});
it("when merging an object into a non-object", () => {
const re = () => /object into non-object/;
expect(() => merge([1], {b: 2}, [])).toThrow(re());
expect(() => merge(true, {b: 2}, [])).toThrow(re());
});
it("when merging a primitive into a non-primitive", () => {
const re = () => /primitive into non-primitive/;
expect(() => merge([], true, [])).toThrow(re());
expect(() => merge({a: 1}, true, [])).toThrow(re());
});
});
});
});
describe("#postQueryExhaustive", () => {
it("finds no fragments in an empty query", () => {
const b = build;
const query = b.query("Noop", [], []);
expect(requiredFragments(query)).toEqual([]);
});
it("finds a fragment with no dependencies", () => {
const b = build;
const query = b.query(
"FindReviewComments",
[],
[
b.field("node", {id: b.literal("some-user")}, [
b.inlineFragment("Actor", [b.fragmentSpread("whoami")]),
]),
]
);
const result = requiredFragments(query);
expect(result.map((fd) => fd.name).sort()).toEqual(["whoami"]);
result.forEach((fd) => expect(createFragments()).toContainEqual(fd));
});
it("transitively finds dependent fragments", () => {
const b = build;
const query = b.query(
"FindReviewComments",
[],
[
b.field("node", {id: b.literal("some-pull-request")}, [
b.inlineFragment("PullRequest", [
b.field(
"reviews",
{
first: b.literal(1),
},
[b.fragmentSpread("reviews")]
),
]),
]),
]
);
const result = requiredFragments(query);
expect(result.map((fd) => fd.name).sort()).toEqual([
"reactions",
"reviewComments",
"reviews",
"whoami",
]);
result.forEach((fd) => expect(createFragments()).toContainEqual(fd));
});
});
describe("#postQueryExhaustive", () => {
it("resolves a representative query", async () => {
const makeAuthor = (name) => ({
__typename: "User",
login: name,
id: `opaque-user-${name}`,
});
// We'll have three stages:
// - The original result will need more issues, and more
// comments for issue 1, and more reviews for PR 2.
// - The next result will need more issues, and comments for
// issues 1 (original issue) and 3 (new issue).
// - The final result will need no more data.
// We obey the contract pretty much exactly, except that we return
// far fewer results than are asked for by the query.
//
// Here is the response to the initial query.
const response0 = {
repository: {
id: "opaque-repo",
issues: {
pageInfo: {
hasNextPage: true,
endCursor: "opaque-cursor-issues-v0",
},
nodes: [
{
id: "opaque-issue1",
title: "Request for comments",
body: "Like it says, please comment!",
number: 1,
author: makeAuthor("decentralion"),
comments: {
pageInfo: {
hasNextPage: true,
endCursor: "opaque-cursor-issue1comments-v0",
},
nodes: [
{
id: "opaque-issue1comment1",
body: "Here: I'll start.",
url: "opaque://issue/1/comment/1",
author: makeAuthor("decentralion"),
},
],
},
},
],
},
pulls: {
pageInfo: {
hasNextPage: false,
endCursor: "opaque-cursor-pulls-v0",
},
nodes: [
{
id: "opaque-pull2",
title: "Fix typo in README",
body: "Surely this deserves much cred.",
number: 2,
author: makeAuthor("wchargin"),
comments: {
pageInfo: {
hasNextPage: false,
endCursor: null,
},
nodes: [],
},
reviews: {
pageInfo: {
hasNextPage: true,
endCursor: "opaque-cursor-pull2reviews-v0",
},
nodes: [
{
id: "opaque-pull2review1",
body: "You actually introduced a new typo instead.",
author: makeAuthor("decentralion"),
state: "CHANGES_REQUESTED",
comments: {
pageInfo: {
hasNextPage: false,
endCursor: null,
},
nodes: [],
},
},
],
},
},
],
},
},
};
// Here is the response to the continuations generated from the
// first query.
const response1 = {
_n0: {
// Requested more issues.
issues: {
pageInfo: {
hasNextPage: true,
endCursor: "opaque-cursor-issues-v1",
},
nodes: [
{
id: "opaque-issue3",
title: "Another",
body: "You can comment here, too.",
number: 2,
author: makeAuthor("wchargin"),
comments: {
pageInfo: {
hasNextPage: true,
endCursor: "opaque-cursor-issue3comments-v1",
},
nodes: [
{
id: "opaque-issue3comment1",
body: "What fun!",
url: "opaque://issue/3/comment/1",
author: makeAuthor("decentralion"),
},
],
},
},
],
},
},
_n1: {
// Requested more comments for issue 1.
comments: {
pageInfo: {
hasNextPage: true,
endCursor: "opaque-cursor-issue1comments-v1",
},
nodes: [
{
id: "opaque-issue1comment2",
body: "Closing due to no fun allowed.",
url: "opaque://issue/1/comment/2",
author: makeAuthor("wchargin"),
},
],
},
},
_n2: {
// Requested more reviews for issue 2.
reviews: {
pageInfo: {
hasNextPage: false,
endCursor: "opaque-cursor-pull2reviews-v1",
},
nodes: [
{
id: "opaque-pull2review2",
body: "Looks godo to me.",
author: makeAuthor("decentralion"),
state: "APPROVED",
comments: {
pageInfo: {
hasNextPage: false,
endCursor: null,
},
nodes: [],
},
},
],
},
},
};
// Here is the response to the continuations generated from the
// second query.
const response2 = {
_n0: {
// Requested more issues.
issues: {
pageInfo: {
hasNextPage: false,
endCursor: "opaque-cursor-issues-v2",
},
nodes: [
{
id: "opaque-issue4",
title: "Please stop making issues",
body: "My mailbox is out of space",
number: 4,
author: makeAuthor("wchargin"),
comments: {
pageInfo: {
hasNextPage: false,
endCursor: "opaque-cursor-issue4comments-v2",
},
nodes: [
{
id: "opaque-issue4comment1",
body: "But you posted the last issue",
url: "opaque://issue/4/comment/1",
author: makeAuthor("decentralion"),
},
],
},
},
],
},
},
_n1: {
// Requested more comments for issue 1.
comments: {
pageInfo: {
hasNextPage: false,
endCursor: "opaque-cursor-issue1comments-v2",
},
nodes: [
{
id: "opaque-issue1comment3",
body: "That is not very nice.",
url: "opaque://issue/1/comment/3",
author: makeAuthor("decentralion"),
},
],
},
},
_n2: {
// Requested more comments for issue 3.
comments: {
pageInfo: {
hasNextPage: false,
endCursor: "opaque-cursor-issue3comments-v2",
},
nodes: [
{
id: "opaque-issue3comment2",
body: "I will comment on this issue for a second time.",
url: "opaque://issue/1/comment/3",
author: makeAuthor("decentralion"),
},
],
},
},
};
const postQuery = jest
.fn()
.mockReturnValueOnce(Promise.resolve(response0))
.mockReturnValueOnce(Promise.resolve(response1))
.mockReturnValueOnce(Promise.resolve(response2));
const result = await postQueryExhaustive(postQuery, {
body: createQuery(),
variables: createVariables(makeRepoId("sourcecred", "discussion")),
});
expect(postQuery).toHaveBeenCalledTimes(3);
// Save the result snapshot for inspection. In particular, there
// shouldn't be any nodes in the snapshot that have more pages.
expect(result).toMatchSnapshot();
});
});
it("creates a query", () => {
expect(
stringify.body(createQuery(), multilineLayout(" "))
).toMatchSnapshot();
});
});

View File

@ -16,14 +16,11 @@ import type {
UserlikeAddress, UserlikeAddress,
} from "./nodes"; } from "./nodes";
import * as T from "./graphqlTypes"; import * as T from "./graphqlTypes";
import type {GithubResponseJSON} from "./graphql";
import * as GitNode from "../git/nodes"; import * as GitNode from "../git/nodes";
import * as MapUtil from "../../util/map"; import * as MapUtil from "../../util/map";
import * as NullUtil from "../../util/null"; import * as NullUtil from "../../util/null";
import {botSet} from "./bots"; import {botSet} from "./bots";
import translateContinuations from "./translateContinuations";
import { import {
reviewUrlToId, reviewUrlToId,
issueCommentUrlToId, issueCommentUrlToId,
@ -59,19 +56,12 @@ export class RelationalView {
this._mapReferencedBy = new Map(); this._mapReferencedBy = new Map();
} }
addData(data: GithubResponseJSON) {
// Warning: calling `addData` can put the RelationalView in an inconistent
// state. for example, if called with {repo: {issues: [1,2,3]}} and then with
// {repo: {issues: [4, 5]}}, then calls to repo.issues() will only give back
// issues 4 and 5 (although issues 1, 2, and 3 will still be in the view)
const {result: repository, warnings} = translateContinuations(data);
for (const warning of warnings) {
console.warn(stringify(warning));
}
this.addRepository(repository);
}
addRepository(repository: T.Repository): void { addRepository(repository: T.Repository): void {
// Warning: calling `addRepository` can put the RelationalView in an
// inconsistent state. for example, if called with a repo with
// issues [#1, #2, #3] and then with a repo with issues [#4, #5],
// then calls to `repo.issues()` will only give back issues 4 and 5
// (although issues 1, 2, and 3 will still be in the view).
this._addRepo(repository); this._addRepo(repository);
this._addReferences(); this._addReferences();
} }

View File

@ -1,331 +0,0 @@
// @flow
// Temporary module to translate GraphQL results from the old format
// with manually resolved continuations to the format emitted by the
// Mirror module. See issue #923 for context.
import type {
AuthorJSON,
BotJSON,
CommentJSON,
CommitJSON,
GitObjectJSON,
GithubResponseJSON,
IssueJSON,
OrganizationJSON,
PullJSON,
ReactionJSON,
RefJSON,
RepositoryJSON,
ReviewCommentJSON,
ReviewJSON,
UserJSON,
} from "./graphql";
import type {
Actor,
Blob,
Bot,
Commit,
GitObject,
GitObjectID,
Issue,
IssueComment,
Organization,
PullRequest,
PullRequestReview,
PullRequestReviewComment,
Reaction,
Ref,
Repository,
RepositoryOwner,
Tag,
Tree,
User,
} from "./graphqlTypes";
export type Warning =
// We've never seen it happen, and don't know how it could. But the
// GitHub schema says that it can. This warning is more of a
// diagnostic to the SourceCred maintainers (if it comes up on a real
// repository, we can learn something!) than an indication that
// something has gone wrong.
| {|+type: "NON_COMMIT_REF_TARGET", +target: GitObjectJSON|}
// This can happen if a commit has a parent that we did not fetch. We
// only fetch commits that are Git-reachable from HEAD or are the direct
// merge commit of a pull request. We may therefore omit commits that
// disappeared from master after a force-push, or were an ancestor of a
// pull request that was merged into a branch other than master. See
// issue #923 for more context. If this is omitted, we will simply
// omit the offending parent commit.
| {|+type: "UNKNOWN_PARENT_OID", +child: GitObjectID, +parent: GitObjectID|};
export default function translate(
json: GithubResponseJSON
): {|
+result: Repository,
+warnings: $ReadOnlyArray<Warning>,
|} {
const repositoryJson = json.repository;
const warnings: Array<Warning> = [];
// Most of the work that this function does is exploding connections
// into lists of nodes. But commits require some special attention,
// because we have to resolve parent OIDs to actual parent commits.
// This means that it is most convenient to start by discovering all
// commits in the data.
const commits: Map<
GitObjectID,
{|
...Commit,
parents: Array<null | Commit>, // mutable: we build this incrementally
|}
> = new Map();
// First, create all the commit objects, initializing them with empty
// parent arrays. We put these temporarily into a map keyed by OID for
// deduplication: a commit may appear both in the linearized history
// from HEAD and also as the merge commit of a pull request, and we
// want to process it just once.
const commitJsons: $ReadOnlyArray<CommitJSON> = Array.from(
new Map(
Array.from(
(function*() {
if (repositoryJson.defaultBranchRef) {
const target = repositoryJson.defaultBranchRef.target;
switch (target.__typename) {
case "Commit":
yield* target.history.nodes;
break;
case "Tree":
case "Blob":
case "Tag":
warnings.push({type: "NON_COMMIT_REF_TARGET", target});
break;
// istanbul ignore next: unreachable per Flow
default:
throw new Error((target.type: empty));
}
}
for (const pull of repositoryJson.pulls.nodes) {
if (pull.mergeCommit) {
yield pull.mergeCommit;
}
}
})()
).map((json) => [json.oid, json])
).values()
);
for (const commitJson of commitJsons) {
const commit = {
__typename: "Commit",
author: {...commitJson.author},
id: commitJson.id,
message: commitJson.message,
oid: commitJson.oid,
parents: [],
url: commitJson.url,
};
commits.set(commit.oid, commit);
}
// Then, once all the objects have been created, we can set up the
// parents.
for (const commitJson of commitJsons) {
const commit = commits.get(commitJson.oid);
// istanbul ignore next: should not be possible
if (commit == null) {
throw new Error(
"invariant violation: commit came out of nowhere: " + commitJson.oid
);
}
for (const {oid: parentOid} of commitJson.parents.nodes) {
const parentCommit = commits.get(parentOid);
if (parentCommit == null) {
warnings.push({
type: "UNKNOWN_PARENT_OID",
child: commitJson.oid,
parent: parentOid,
});
} else {
commit.parents.push(parentCommit);
}
}
}
// The rest is mostly mechanical. The pattern is: we pull off and
// recursively translate the non-primitive fields of each object, and
// then add a typename and put back the primitives. For union types,
// we switch on the __typename and dispatch to the appropriate object
// translators.
function translateRepository(json: RepositoryJSON): Repository {
const {defaultBranchRef, issues, owner, pulls, ...rest} = json;
return {
__typename: "Repository",
defaultBranchRef:
defaultBranchRef == null
? null
: translateDefaultBranchRef(defaultBranchRef),
issues: issues.nodes.map(translateIssue),
owner: translateRepositoryOwner(owner),
pullRequests: pulls.nodes.map(translatePullRequest),
...rest,
};
}
function translateDefaultBranchRef(json: RefJSON): Ref {
const {target, ...rest} = json;
return {
__typename: "Ref",
target: translateDefaultBranchRefTarget(target),
...rest,
};
}
// This one is a bit wonky, because our `GitObjectJSON` type is not a
// good representation of the GitHub schema. In particular, a
// `GitObjectJSON` can represent a commit, but in a different form
// than our `CommitJSON`! This function _only_ applies to
// `GitObjectJSON`s that we fetched as the `target` of the
// `defaultBranchRef` of a repository. But these are the only
// `GitObjectJSON`s that we fetch, so it's okay.
function translateDefaultBranchRefTarget(json: GitObjectJSON): GitObject {
switch (json.__typename) {
case "Commit":
// The default branch ref is `null` if there are no commits, so
// the history must include at least one commit (the HEAD
// commit).
return lookUpCommit(json.history.nodes[0].oid);
case "Blob":
return ({...json}: Blob);
case "Tag":
return ({...json}: Tag);
case "Tree":
return ({...json}: Tree);
// istanbul ignore next: unreachable per Flow
default:
throw new Error((json.__typename: empty));
}
}
function lookUpCommit(oid: GitObjectID): Commit {
const commit = commits.get(oid);
// istanbul ignore if: unreachable: we explored all commits in
// the response, including this one.
if (commit == null) {
throw new Error("invariant violation: unknown commit: " + oid);
}
return commit;
}
function translateCommit(json: CommitJSON): Commit {
return lookUpCommit(json.oid);
}
function translateIssue(json: IssueJSON): Issue {
const {author, comments, reactions, ...rest} = json;
return {
__typename: "Issue",
author: author == null ? null : translateActor(author),
comments: comments.nodes.map(translateIssueComment),
reactions: reactions.nodes.map(translateReaction),
...rest,
};
}
function translateIssueComment(json: CommentJSON): IssueComment {
const {author, reactions, ...rest} = json;
return {
__typename: "IssueComment",
author: author == null ? null : translateActor(author),
reactions: reactions.nodes.map(translateReaction),
...rest,
};
}
function translateReaction(json: ReactionJSON): Reaction {
const {user, ...rest} = json;
return {
__typename: "Reaction",
user: user == null ? null : translateUser(user),
...rest,
};
}
function translateRepositoryOwner(
json: UserJSON | OrganizationJSON
): RepositoryOwner {
switch (json.__typename) {
case "User":
return translateUser(json);
case "Organization":
return translateOrganization(json);
// istanbul ignore next: unreachable per Flow
default:
throw new Error((json.__typename: empty));
}
}
function translateActor(json: AuthorJSON): Actor {
switch (json.__typename) {
case "User":
return translateUser(json);
case "Organization":
return translateOrganization(json);
case "Bot":
return translateBot(json);
// istanbul ignore next: unreachable per Flow
default:
throw new Error((json.__typename: empty));
}
}
function translateUser(json: UserJSON): User {
return {...json};
}
function translateOrganization(json: OrganizationJSON): Organization {
return {...json};
}
function translateBot(json: BotJSON): Bot {
return {...json};
}
function translatePullRequest(json: PullJSON): PullRequest {
const {author, comments, mergeCommit, reactions, reviews, ...rest} = json;
return {
__typename: "PullRequest",
author: author == null ? null : translateActor(author),
comments: comments.nodes.map(translateIssueComment),
mergeCommit: mergeCommit == null ? null : translateCommit(mergeCommit),
reactions: reactions.nodes.map(translateReaction),
reviews: reviews.nodes.map(translatePullRequestReview),
...rest,
};
}
function translatePullRequestReview(json: ReviewJSON): PullRequestReview {
const {author, comments, ...rest} = json;
return {
__typename: "PullRequestReview",
author: author == null ? null : translateActor(author),
comments: comments.nodes.map(translatePullRequestReviewComment),
...rest,
};
}
function translatePullRequestReviewComment(
json: ReviewCommentJSON
): PullRequestReviewComment {
const {author, reactions, ...rest} = json;
return {
__typename: "PullRequestReviewComment",
author: author == null ? null : translateActor(author),
reactions: reactions.nodes.map(translateReaction),
...rest,
};
}
const result = translateRepository(repositoryJson);
return {result, warnings};
}

View File

@ -1,144 +0,0 @@
// @flow
import translateContinuations from "./translateContinuations";
describe("plugins/github/translateContinuations", () => {
describe("translateContinuations", () => {
it("raises a warning if the defaultBranchRef is not a commit", () => {
const exampleData = {
repository: {
defaultBranchRef: {
id: "ref-id",
target: {
__typename: "Tree",
id: "tree-id",
oid: "123",
},
},
id: "repo-id",
issues: {
nodes: [],
pageInfo: {hasNextPage: false, endCursor: null},
},
name: "bar",
owner: {
__typename: "User",
id: "user-id",
login: "foo",
url: "https://github.com/foo",
},
pulls: {
nodes: [],
pageInfo: {hasNextPage: false, endCursor: null},
},
url: "https://github.com/foo/bar",
},
};
const {result, warnings} = translateContinuations(exampleData);
expect(result.defaultBranchRef).toEqual({
__typename: "Ref",
id: "ref-id",
target: {__typename: "Tree", id: "tree-id", oid: "123"},
});
expect(warnings).toEqual([
{
type: "NON_COMMIT_REF_TARGET",
target: {__typename: "Tree", id: "tree-id", oid: "123"},
},
]);
});
it("raises a warning if there is an unknown commit", () => {
const exampleData = {
repository: {
defaultBranchRef: null,
id: "repo-id",
issues: {
nodes: [],
pageInfo: {hasNextPage: false, endCursor: null},
},
name: "bar",
owner: {
__typename: "User",
id: "user-id",
login: "foo",
url: "https://github.com/foo",
},
pulls: {
nodes: [
{
id: "pr-id",
number: 1,
author: {
__typename: "Bot",
id: "bot-id",
login: "baz",
url: "https://github.com/baz",
},
additions: 7,
deletions: 9,
comments: {
nodes: [],
pageInfo: {hasNextPage: false, endCursor: null},
},
reviews: {
nodes: [],
pageInfo: {hasNextPage: false, endCursor: null},
},
reactions: {
nodes: [],
pageInfo: {hasNextPage: false, endCursor: null},
},
mergeCommit: {
id: "commit-id",
author: {
date: "2001-02-03T04:05:06",
user: null,
},
message: "where are my parents?",
oid: "456",
parents: {
nodes: [{oid: "789"}],
pageInfo: {hasNextPage: false, endCursor: "cursor-parents"},
},
url: "https://github.com/foo/bar/commit/456",
},
title: "something",
body: "whatever",
url: "https://github.com/foo/bar/pull/1",
},
],
pageInfo: {hasNextPage: false, endCursor: "cursor-pulls"},
},
url: "https://github.com/foo/bar",
},
};
const {result, warnings} = translateContinuations(exampleData);
const pr = result.pullRequests[0];
if (pr == null) {
throw new Error(String(pr));
}
expect(pr.mergeCommit).toEqual({
__typename: "Commit",
id: "commit-id",
author: {
date: "2001-02-03T04:05:06",
user: null,
},
message: "where are my parents?",
oid: "456",
parents: [
/* empty! */
],
url: "https://github.com/foo/bar/commit/456",
});
expect(warnings).toEqual([
{
type: "UNKNOWN_PARENT_OID",
child: "456",
parent: "789",
},
]);
});
});
});