diff --git a/src/plugins/github/__snapshots__/graphql.test.js.snap b/src/plugins/github/__snapshots__/graphql.test.js.snap deleted file mode 100644 index 2e12a74..0000000 --- a/src/plugins/github/__snapshots__/graphql.test.js.snap +++ /dev/null @@ -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 - } - } -}" -`; diff --git a/src/plugins/github/graphql.js b/src/plugins/github/graphql.js deleted file mode 100644 index ad2272c..0000000 --- a/src/plugins/github/graphql.js +++ /dev/null @@ -1,1187 +0,0 @@ -// @flow - -import type { - Body, - FragmentDefinition, - Selection, - QueryDefinition, -} from "../../graphql/queries"; -import {build} from "../../graphql/queries"; -import type {RepoId} from "../../core/repoId"; - -/** - * This module defines the GraphQL query that we use to access the - * GitHub API, and defines functions to facilitate exhaustively - * requesting all pages of results for this query. - * - * The key type is the `Continuation`, which represents a selection set - * that fetches the next page of results for a particular connection. - * The flow is as follows: - * - * - A Query is executed and fetches some Results in standard form. - * - The Results are analyzed to form Continuations. - * - These continuations are embedded into a new Query. - * - * This process repeats, and each time that Results are fetched, they - * are merged into the previous Results so that the Results get - * progressively more complete. The process terminates when the second - * step does not yield any more Continuations. - * - * Of particular import is the function `continuationsFromContinuation`; - * see more docs on that function. - */ - -/* - * GitHub enforces a hard limit of no more than 100 entities per page, - * in any single connection. GitHub also has a more global restriction - * on the worst-case number of nodes that could be requested by a query, - * which scales as the product of the page limits in any given sequence - * of nested connections. (For more information, see [1].) Therefore, we - * tune the page sizes of various entities to keep them comfortably - * within the global capacity. - * - * For the top-level page size in continuations, we use either - * `PAGE_LIMIT` or the field-specific page size (in the case of - * commit history). - * - * [1]: https://developer.github.com/v4/guides/resource-limitations/#node-limit - */ -export const PAGE_LIMIT = 50; -const PAGE_SIZE_ISSUES = 50; -const PAGE_SIZE_PRS = 50; -const PAGE_SIZE_COMMENTS = 20; -const PAGE_SIZE_REVIEWS = 5; -const PAGE_SIZE_REVIEW_COMMENTS = 10; -const PAGE_SIZE_COMMIT_HISTORY = 100; -const PAGE_SIZE_COMMIT_PARENTS = 5; -const PAGE_SIZE_REACTIONS = 5; - -/** - * What's in a continuation? If we want to fetch more comments for the - * 22nd issue in the results list, we fire off the following query: - * - * _n0: node(id: "") { - * ... on Issue { - * comments(first: PAGE_LIMIT, after: "") { - * ...comments - * } - * } - * - * This would be represented as: - * - * { - * enclosingNodeType: "ISSUE", - * enclosingNodeId: "", - * selections: [b.inlineFragment("Issue", ...)], - * destinationPath: ["repository", "issues", 21], - * } - * - * The `enclosingNodeId` and `selections` are used to construct the - * query. The `destinationPath` is used to merge the continued results - * back into the original results. The `enclosingNodeType` is required - * so that we know how to check for further continuations on the result. - * See function `continuationsFromContinuation` for more details on the - * last one. - * - * The nonce (`_n0`) is deliberately not included in the continuation - * type, because the nonce is a property of a particular embedding of - * the continuation into a query, and not of the continuation itself. - */ -export type Continuation = {| - +enclosingNodeType: - | "REPOSITORY" - | "COMMIT" - | "ISSUE" - | "PULL" - | "REVIEW" - | "ISSUE_COMMENT" - | "REVIEW_COMMENT", - +enclosingNodeId: string, - +selections: $ReadOnlyArray, - +destinationPath: $ReadOnlyArray, -|}; - -export type ConnectionJSON<+T> = {| - +nodes: $ReadOnlyArray, - +pageInfo: {| - +endCursor: ?string, - +hasNextPage: boolean, - |}, -|}; - -export type GithubResponseJSON = {| - +repository: RepositoryJSON, -|}; - -export type RepositoryJSON = {| - +id: string, - +issues: ConnectionJSON, - +pulls: ConnectionJSON, - +url: string, - +name: string, - +owner: UserJSON | OrganizationJSON, - +defaultBranchRef: ?RefJSON, -|}; - -export type RefJSON = {|+id: string, +target: GitObjectJSON|}; -export type GitObjectJSON = - | {| - +__typename: "Commit", - +id: string, - +oid: string, - +history: ConnectionJSON, - |} - | {|+__typename: "Tree", +id: string, +oid: string|} - | {|+__typename: "Blob", +id: string, +oid: string|} - | {|+__typename: "Tag", +id: string, +oid: string|}; - -/** - * The top-level GitHub query to request data about a repository. - * Callers will also be interested in `createVariables`. - */ -export function createQuery(): Body { - const b = build; - const body: Body = [ - b.query( - "FetchData", - [b.param("owner", "String!"), b.param("name", "String!")], - [ - b.field( - "repository", - {owner: b.variable("owner"), name: b.variable("name")}, - [ - b.field("url"), - b.field("name"), - b.field("owner", {}, [b.fragmentSpread("whoami")]), - b.field("id"), - b.field("issues", {first: b.literal(PAGE_SIZE_ISSUES)}, [ - b.fragmentSpread("issues"), - ]), - b.alias( - "pulls", - b.field("pullRequests", {first: b.literal(PAGE_SIZE_PRS)}, [ - b.fragmentSpread("pulls"), - ]) - ), - b.field("defaultBranchRef", {}, [ - b.field("id"), - b.field("target", {}, [ - b.field("__typename"), - b.inlineFragment("Commit", [ - b.field( - "history", - {first: b.literal(PAGE_SIZE_COMMIT_HISTORY)}, - [b.fragmentSpread("commitHistory")] - ), - ]), - b.inlineFragment("Blob", [b.field("id"), b.field("oid")]), - b.inlineFragment("Tag", [b.field("id"), b.field("oid")]), - b.inlineFragment("Tree", [b.field("id"), b.field("oid")]), - ]), - ]), - ] - ), - ] - ), - ...createFragments(), - ]; - return body; -} - -/** - * Find continuations for the top-level result ("data" field) of a - * query. - */ -export function continuationsFromQuery(result: any): Iterator { - return continuationsFromRepository(result.repository, result.repository.id, [ - "repository", - ]); -} - -/** - * Find continuations for a result of a query that was itself generated - * from a continuation. If an original query Q1 returns results R1 that - * yield continuations C1, and the query Q2 is an embedding of - * continuations C1 and returns results R2, then this function, when - * called with (R2, C1), generates the continuations C2 that should be - * used to continue the chain. - * - * Note that these continuations' results should be merged into the - * _original_ data structure, not subsequent results. Continuing with - * the above terminology: results R2 should be merged into R1 to form - * R2', and then continuations C2 should be embedded into a query Q3 - * whose results R3 should be merged into R2' (as opposed to being - * merged into R2, and then this result being merged into R1). This is - * somewhat less efficient in terms of client-side CPU usage, but is - * also somewhat easier to implement. - * - * This function is a critical piece of plumbing: it enables us to - * iterate through pages, using a continuation to fetch a further - * continuation on the same entity. The fact that this function is - * implementable is an indication that the `Continuation` type is - * defined appropriately. This is non-trivial, as there are a lot of - * choices as to where the boundaries should be. (For instance, should - * we include the type of the node that we want to fetch more of, or the - * type of the enclosing node? What sort of path information should we - * retain?) - */ -export function continuationsFromContinuation( - result: any, - source: Continuation -): Iterator { - const continuationsFromEnclosingType = { - REPOSITORY: continuationsFromRepository, - COMMIT: continuationsFromCommit, - ISSUE: continuationsFromIssue, - PULL: continuationsFromPR, - REVIEW: continuationsFromReview, - ISSUE_COMMENT: continuationsFromIssueComment, - REVIEW_COMMENT: continuationsFromReviewComment, - }[source.enclosingNodeType]; - return continuationsFromEnclosingType( - result, - source.enclosingNodeId, - source.destinationPath - ); -} - -function* continuationsFromRepository( - result: any, - nodeId: string, - path: $ReadOnlyArray -): Iterator { - const b = build; - if (result.issues && result.issues.pageInfo.hasNextPage) { - yield { - enclosingNodeType: "REPOSITORY", - enclosingNodeId: nodeId, - selections: [ - b.inlineFragment("Repository", [ - b.field( - "issues", - { - first: b.literal(PAGE_LIMIT), - after: b.literal(result.issues.pageInfo.endCursor), - }, - [b.fragmentSpread("issues")] - ), - ]), - ], - destinationPath: path, - }; - } - if (result.pulls && result.pulls.pageInfo.hasNextPage) { - yield { - enclosingNodeType: "REPOSITORY", - enclosingNodeId: nodeId, - selections: [ - b.inlineFragment("Repository", [ - b.alias( - "pulls", - b.field( - "pullRequests", - { - first: b.literal(PAGE_LIMIT), - after: b.literal(result.pulls.pageInfo.endCursor), - }, - [b.fragmentSpread("pulls")] - ) - ), - ]), - ], - destinationPath: path, - }; - } - if ( - result.defaultBranchRef && - result.defaultBranchRef.target.history.pageInfo.hasNextPage - ) { - yield { - enclosingNodeType: "REPOSITORY", - enclosingNodeId: nodeId, - selections: [ - b.inlineFragment("Repository", [ - b.field("defaultBranchRef", {}, [ - b.field("target", {}, [ - b.field("__typename"), - b.inlineFragment("Commit", [ - b.field( - "history", - { - first: b.literal(PAGE_SIZE_COMMIT_HISTORY), - after: b.literal( - result.defaultBranchRef.target.history.pageInfo.endCursor - ), - }, - [b.fragmentSpread("commitHistory")] - ), - ]), - ]), - ]), - ]), - ], - destinationPath: path, - }; - } - if (result.issues) { - for (let i = 0; i < result.issues.nodes.length; i++) { - const issue = result.issues.nodes[i]; - const subpath = [...path, "issues", "nodes", i]; - yield* continuationsFromIssue(issue, issue.id, subpath); - } - } - if (result.pulls) { - for (let i = 0; i < result.pulls.nodes.length; i++) { - const pull = result.pulls.nodes[i]; - const subpath = [...path, "pulls", "nodes", i]; - yield* continuationsFromPR(pull, pull.id, subpath); - } - } - if (result.defaultBranchRef) { - const topLevelCommit = result.defaultBranchRef.target; - { - const commit = topLevelCommit; - const subpath = [...path, "defaultBranchRef", "target"]; - yield* continuationsFromCommit(commit, commit.id, subpath); - } - const pastCommits = topLevelCommit.history.nodes; - for (let i = 0; i < pastCommits.length; i++) { - const commit = pastCommits[i]; - const subpath = [ - ...path, - "defaultBranchRef", - "target", - "history", - "nodes", - i, - ]; - yield* continuationsFromCommit(commit, commit.id, subpath); - } - } -} - -function* continuationsFromCommit( - result: any, - nodeId: string, - path: $ReadOnlyArray -) { - const b = build; - if (result.parents && result.parents.pageInfo.hasNextPage) { - yield { - enclosingNodeType: "COMMIT", - enclosingNodeId: nodeId, - selections: [ - b.inlineFragment("Commit", [ - b.field( - "parents", - { - first: b.literal(PAGE_LIMIT), - after: b.literal(result.parents.pageInfo.endCursor), - }, - [b.fragmentSpread("commitParents")] - ), - ]), - ], - destinationPath: path, - }; - } -} - -function* continuationsFromIssue( - result: any, - nodeId: string, - path: $ReadOnlyArray -): Iterator { - const b = build; - if (result.comments && result.comments.pageInfo.hasNextPage) { - yield { - enclosingNodeType: "ISSUE", - enclosingNodeId: nodeId, - selections: [ - b.inlineFragment("Issue", [ - b.field( - "comments", - { - first: b.literal(PAGE_LIMIT), - after: b.literal(result.comments.pageInfo.endCursor), - }, - [b.fragmentSpread("comments")] - ), - ]), - ], - destinationPath: path, - }; - } - if (result.reactions && result.reactions.pageInfo.hasNextPage) { - yield { - enclosingNodeType: "ISSUE", - enclosingNodeId: nodeId, - selections: [ - b.inlineFragment("Issue", [ - b.field( - "reactions", - { - first: b.literal(PAGE_LIMIT), - after: b.literal(result.reactions.pageInfo.endCursor), - }, - [b.fragmentSpread("reactions")] - ), - ]), - ], - destinationPath: path, - }; - } - if (result.comments) { - for (let i = 0; i < result.comments.nodes.length; i++) { - const comment = result.comments.nodes[i]; - const subpath = [...path, "comments", "nodes", i]; - yield* continuationsFromIssueComment(comment, comment.id, subpath); - } - } -} - -function* continuationsFromPR( - result: any, - nodeId: string, - path: $ReadOnlyArray -): Iterator { - const b = build; - if (result.comments && result.comments.pageInfo.hasNextPage) { - yield { - enclosingNodeType: "PULL", - enclosingNodeId: nodeId, - selections: [ - b.inlineFragment("PullRequest", [ - b.field( - "comments", - { - first: b.literal(PAGE_LIMIT), - after: b.literal(result.comments.pageInfo.endCursor), - }, - [b.fragmentSpread("comments")] - ), - ]), - ], - destinationPath: path, - }; - } - if (result.reactions && result.reactions.pageInfo.hasNextPage) { - yield { - enclosingNodeType: "PULL", - enclosingNodeId: nodeId, - selections: [ - b.inlineFragment("PullRequest", [ - b.field( - "reactions", - { - first: b.literal(PAGE_LIMIT), - after: b.literal(result.reactions.pageInfo.endCursor), - }, - [b.fragmentSpread("reactions")] - ), - ]), - ], - destinationPath: path, - }; - } - if (result.reviews && result.reviews.pageInfo.hasNextPage) { - yield { - enclosingNodeType: "PULL", - enclosingNodeId: nodeId, - selections: [ - b.inlineFragment("PullRequest", [ - b.field( - "reviews", - { - first: b.literal(PAGE_LIMIT), - after: b.literal(result.reviews.pageInfo.endCursor), - }, - [b.fragmentSpread("reviews")] - ), - ]), - ], - destinationPath: path, - }; - } - if (result.mergeCommit) { - const commit = result.mergeCommit; - const subpath = [...path, "mergeCommit"]; - yield* continuationsFromCommit(commit, commit.id, subpath); - } - if (result.reviews) { - for (let i = 0; i < result.reviews.nodes.length; i++) { - const issue = result.reviews.nodes[i]; - const subpath = [...path, "reviews", "nodes", i]; - yield* continuationsFromReview(issue, issue.id, subpath); - } - } - if (result.comments) { - for (let i = 0; i < result.comments.nodes.length; i++) { - const comment = result.comments.nodes[i]; - const subpath = [...path, "comments", "nodes", i]; - yield* continuationsFromIssueComment(comment, comment.id, subpath); - } - } -} - -function* continuationsFromReview( - result: any, - nodeId: string, - path: $ReadOnlyArray -): Iterator { - const b = build; - if (result.comments.pageInfo.hasNextPage) { - yield { - enclosingNodeType: "REVIEW", - enclosingNodeId: nodeId, - selections: [ - b.inlineFragment("PullRequestReview", [ - b.field( - "comments", - { - first: b.literal(PAGE_LIMIT), - after: b.literal(result.comments.pageInfo.endCursor), - }, - [b.fragmentSpread("reviewComments")] - ), - ]), - ], - destinationPath: path, - }; - } - if (result.comments) { - for (let i = 0; i < result.comments.nodes.length; i++) { - const comment = result.comments.nodes[i]; - const subpath = [...path, "comments", "nodes", i]; - yield* continuationsFromReviewComment(comment, comment.id, subpath); - } - } -} - -// Pull Request comments are also issue comments. -function* continuationsFromIssueComment( - result: any, - nodeId: string, - path: $ReadOnlyArray -): Iterator { - const b = build; - if (result.reactions && result.reactions.pageInfo.hasNextPage) { - yield { - enclosingNodeType: "ISSUE_COMMENT", - enclosingNodeId: nodeId, - selections: [ - b.inlineFragment("IssueComment", [ - b.field( - "reactions", - { - first: b.literal(PAGE_LIMIT), - after: b.literal(result.reactions.pageInfo.endCursor), - }, - [b.fragmentSpread("reactions")] - ), - ]), - ], - destinationPath: path, - }; - } -} - -function* continuationsFromReviewComment( - result: any, - nodeId: string, - path: $ReadOnlyArray -): Iterator { - const b = build; - if (result.reactions && result.reactions.pageInfo.hasNextPage) { - yield { - enclosingNodeType: "REVIEW_COMMENT", - enclosingNodeId: nodeId, - selections: [ - b.inlineFragment("PullRequestReviewComment", [ - b.field( - "reactions", - { - first: b.literal(PAGE_LIMIT), - after: b.literal(result.reactions.pageInfo.endCursor), - }, - [b.fragmentSpread("reactions")] - ), - ]), - ], - destinationPath: path, - }; - } -} - -/** - * Execute the given query, returning all pages of data throughout the - * query. That is: post the query, then find any entities that require - * more pages of data, fetch those additional pages, and merge the - * results. The `postQuery` function may be called multiple times. - */ -export async function postQueryExhaustive( - postQuery: ({body: Body, variables: {+[string]: any}}) => Promise, - payload: {body: Body, variables: {+[string]: any}} -) { - const originalResult = await postQuery(payload); - return resolveContinuations( - postQuery, - originalResult, - Array.from(continuationsFromQuery(originalResult)) - ); -} - -/** - * Given the result of a query and the continuations for that query, - * resolve the continuations and return the merged results. - */ -async function resolveContinuations( - postQuery: ({body: Body, variables: {+[string]: any}}) => Promise, - originalResult: any, - continuations: $ReadOnlyArray -): Promise { - if (!continuations.length) { - return originalResult; - } - - // Assign each continuation a nonce (unique name) so that we can refer - // to it unambiguously, and embed all the continuations into a query. - const embeddings = continuations.map((c, i) => ({ - continuation: c, - nonce: `_n${String(i)}`, - })); - const b = build; - const query = b.query( - "Continuations", - [], - embeddings.map(({continuation, nonce}) => - b.alias( - nonce, - b.field( - "node", - {id: b.literal(continuation.enclosingNodeId)}, - continuation.selections.slice() - ) - ) - ) - ); - const body = [query, ...requiredFragments(query)]; - const payload = {body, variables: {}}; - - // Send the continuation query, then merge these results into the - // original results---then recur, because the new results may - // themselves be incomplete. - const continuationResult = await postQuery(payload); - const mergedResults = embeddings.reduce((acc, {continuation, nonce}) => { - return merge(acc, continuationResult[nonce], continuation.destinationPath); - }, originalResult); - return resolveContinuations( - postQuery, - mergedResults, - Array.from(continuationsFromQuery(mergedResults)) - ); -} - -/** - * A GraphQL query includes a query body and some fragment definitions. - * It is an error to include unneeded fragment definitions. Therefore, - * given a standard set of fragments and an arbitrary query body, we - * need to be able to select just the right set of fragments for our - * particular query. - * - * This function finds all fragments that are transitively used by the - * given query. That is, it finds all fragments used by the query, all - * fragments used by those fragments, and so on. Note that the universe - * of fragments is considered to be the result of `createFragments`; it - * is an error to use a fragment not defined in the result of that - * function. - * - * Equivalently, construct a graph where the nodes are (a) the query and - * (b) all possible fragments, and there is an edge from `a` to `b` if - * `a` references `b`. Then, this function finds the set of vertices - * reachable from the query node. - */ -export function requiredFragments( - query: QueryDefinition -): FragmentDefinition[] { - const fragmentsByName = {}; - createFragments().forEach((fd) => { - fragmentsByName[fd.name] = fd; - }); - - // This function implements a BFS on the graph specified in the - // docstring, with the provisos that the nodes for fragments are the - // fragment names, not the fragments themselves, and that the query - // node is implicit (in the initial value of the `frontier`). - const requiredFragmentNames: Set = new Set(); - let frontier: Set = new Set(); - query.selections.forEach((selection) => { - for (const fragment of usedFragmentNames(selection)) { - frontier.add(fragment); - } - }); - - while (frontier.size > 0) { - frontier.forEach((name) => { - requiredFragmentNames.add(name); - }); - const newFrontier: Set = new Set(); - for (const name of frontier) { - const fragment = fragmentsByName[name]; - if (fragment == null) { - throw new Error(`Unknown fragment: "${fragment}"`); - } - fragment.selections.forEach((selection) => { - for (const fragment of usedFragmentNames(selection)) { - newFrontier.add(fragment); - } - }); - } - frontier = newFrontier; - } - - return createFragments().filter((fd) => requiredFragmentNames.has(fd.name)); -} - -/** - * Find all fragment names directly referenced by the given selection. - * This does not include transitive fragment references. - */ -function* usedFragmentNames(selection: Selection): Iterator { - switch (selection.type) { - case "FRAGMENT_SPREAD": - yield selection.fragmentName; - break; - case "FIELD": - case "INLINE_FRAGMENT": - for (const subselection of selection.selections) { - yield* usedFragmentNames(subselection); - } - break; - default: - throw new Error(`Unknown selection type: ${selection.type}`); - } -} - -/** - * Merge structured data from the given `source` into a given subpath of - * the `destination`. The original inputs are not modified. - * - * Arrays are merged by concatenation. Objects are merged by recursively - * merging each key. Primitives are merged by replacement (the - * destination is simply overwritten with the source). - * - * See test cases for examples. - * - * NOTE: The type of `source` should be the same as the type of - * - * destination[path[0]][path[1]]...[path[path.length - 1]], - * - * but this constraint cannot be expressed in Flow so we just use `any`. - */ -export function merge( - destination: T, - source: any, - path: $ReadOnlyArray -): T { - if (path.length === 0) { - return mergeDirect(destination, source); - } - - function isObject(x) { - return !Array.isArray(x) && typeof x === "object" && x != null; - } - function checkKey(key: string | number, destination: Object | Array) { - if (!(key in destination)) { - const keyText = JSON.stringify(key); - const destinationText = JSON.stringify(destination); - throw new Error( - `Key ${keyText} not found in destination: ${destinationText}` - ); - } - } - - const key = path[0]; - if (typeof key === "number") { - if (!Array.isArray(destination)) { - throw new Error( - "Found index key for non-array destination: " + - JSON.stringify(destination) - ); - } - checkKey(key, destination); - const newValue = merge(destination[key], source, path.slice(1)); - const result = destination.slice(); - result.splice(key, 1, newValue); - return result; - } else if (typeof key === "string") { - if (!isObject(destination)) { - throw new Error( - "Found string key for non-object destination: " + - JSON.stringify(destination) - ); - } - const destinationObject: Object = (destination: any); - checkKey(key, destinationObject); - const newValue = merge(destinationObject[key], source, path.slice(1)); - return { - ...destination, - [key]: newValue, - }; - } else { - throw new Error(`Unexpected key: ${JSON.stringify(key)}`); - } -} - -// Merge, without the path traversal. -function mergeDirect(destination: T, source: any): T { - function isObject(x) { - return !Array.isArray(x) && typeof x === "object" && x != null; - } - if (Array.isArray(source)) { - if (!Array.isArray(destination)) { - const destinationText = JSON.stringify(destination); - const sourceText = JSON.stringify(source); - throw new Error( - "Tried to merge array into non-array: " + - `(source: ${sourceText}; destination: ${destinationText})` - ); - } - return destination.concat(source); - } else if (isObject(source)) { - if (!isObject(destination)) { - const destinationText = JSON.stringify(destination); - const sourceText = JSON.stringify(source); - throw new Error( - "Tried to merge object into non-object: " + - `(source: ${sourceText}; destination: ${destinationText})` - ); - } - const result = {...destination}; - Object.keys(source).forEach((k) => { - result[k] = mergeDirect(result[k], source[k]); - }); - return result; - } else { - if (Array.isArray(destination) || isObject(destination)) { - const destinationText = JSON.stringify(destination); - const sourceText = JSON.stringify(source); - throw new Error( - "Tried to merge primitive into non-primitive: " + - `(source: ${sourceText}; destination: ${destinationText})` - ); - } - return source; - } -} - -// If a user deletes their account, then the author is null -// (the UI will show that the message was written by @ghost). -// Therefore, NullableAuthorJSON is preferred to AuthorJSON -// for most actual usage. -export type NullableAuthorJSON = AuthorJSON | null; -export type AuthorJSON = UserJSON | BotJSON | OrganizationJSON; -export type UserJSON = {| - +__typename: "User", - +id: string, - +login: string, - +url: string, -|}; -export type BotJSON = {| - +__typename: "Bot", - +id: string, - +login: string, - +url: string, -|}; -export type OrganizationJSON = {| - +__typename: "Organization", - +id: string, - +login: string, - +url: string, -|}; - -function makePageInfo() { - const b = build; - return b.field("pageInfo", {}, [ - b.field("hasNextPage"), - b.field("endCursor"), - ]); -} -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("login"), - b.field("url"), - b.inlineFragment("User", [b.field("id")]), - b.inlineFragment("Organization", [b.field("id")]), - b.inlineFragment("Bot", [b.field("id")]), - ]); -} - -export type IssueJSON = {| - +id: string, - +url: string, - +title: string, - +body: string, - +number: number, - +author: NullableAuthorJSON, - +comments: ConnectionJSON, - +reactions: ConnectionJSON, -|}; - -function issuesFragment(): FragmentDefinition { - const b = build; - return b.fragment("issues", "IssueConnection", [ - makePageInfo(), - b.field("nodes", {}, [ - b.field("id"), - b.field("url"), - b.field("title"), - b.field("body"), - b.field("number"), - makeAuthor(), - b.field("comments", {first: b.literal(PAGE_SIZE_COMMENTS)}, [ - b.fragmentSpread("comments"), - ]), - b.field("reactions", {first: b.literal(PAGE_SIZE_REACTIONS)}, [ - b.fragmentSpread("reactions"), - ]), - ]), - ]); -} - -export type PullJSON = {| - +id: string, - +url: string, - +title: string, - +body: string, - +number: number, - +additions: number, - +deletions: number, - +author: NullableAuthorJSON, - +comments: ConnectionJSON, - +reviews: ConnectionJSON, - +mergeCommit: ?CommitJSON, - +reactions: ConnectionJSON, -|}; -function pullsFragment(): FragmentDefinition { - const b = build; - return b.fragment("pulls", "PullRequestConnection", [ - makePageInfo(), - b.field("nodes", {}, [ - b.field("id"), - b.field("url"), - b.field("title"), - b.field("body"), - b.field("number"), - b.field("mergeCommit", {}, [b.fragmentSpread("commit")]), - b.field("additions"), - b.field("deletions"), - makeAuthor(), - b.field("comments", {first: b.literal(PAGE_SIZE_COMMENTS)}, [ - b.fragmentSpread("comments"), - ]), - b.field("reviews", {first: b.literal(PAGE_SIZE_REVIEWS)}, [ - b.fragmentSpread("reviews"), - ]), - b.field("reactions", {first: b.literal(PAGE_SIZE_REACTIONS)}, [ - b.fragmentSpread("reactions"), - ]), - ]), - ]); -} - -export type CommentJSON = {| - +id: string, - +url: string, - +body: string, - +author: NullableAuthorJSON, - +reactions: ConnectionJSON, -|}; -function commentsFragment(): FragmentDefinition { - const b = build; - // (Note: issue comments and PR comments use the same connection type.) - return b.fragment("comments", "IssueCommentConnection", [ - makePageInfo(), - b.field("nodes", {}, [ - b.field("id"), - b.field("url"), - makeAuthor(), - b.field("body"), - b.field("reactions", {first: b.literal(PAGE_SIZE_REACTIONS)}, [ - b.fragmentSpread("reactions"), - ]), - ]), - ]); -} - -export type ReviewState = - | "CHANGES_REQUESTED" - | "APPROVED" - | "COMMENTED" - | "DISMISSED" - | "PENDING"; - -export type ReviewJSON = {| - +id: string, - +url: string, - +body: string, - +author: NullableAuthorJSON, - +state: ReviewState, - +comments: ConnectionJSON, -|}; -function reviewsFragment(): FragmentDefinition { - const b = build; - return b.fragment("reviews", "PullRequestReviewConnection", [ - makePageInfo(), - b.field("nodes", {}, [ - b.field("id"), - b.field("url"), - b.field("body"), - makeAuthor(), - b.field("state"), - b.field("comments", {first: b.literal(PAGE_SIZE_REVIEW_COMMENTS)}, [ - b.fragmentSpread("reviewComments"), - ]), - ]), - ]); -} - -export type ReviewCommentJSON = {| - +id: string, - +url: string, - +body: string, - +author: NullableAuthorJSON, - +reactions: ConnectionJSON, -|}; -function reviewCommentsFragment(): FragmentDefinition { - const b = build; - return b.fragment("reviewComments", "PullRequestReviewCommentConnection", [ - makePageInfo(), - b.field("nodes", {}, [ - b.field("id"), - b.field("url"), - b.field("body"), - makeAuthor(), - b.field("reactions", {first: b.literal(PAGE_SIZE_REACTIONS)}, [ - b.fragmentSpread("reactions"), - ]), - ]), - ]); -} - -export type CommitJSON = {| - +id: string, - +url: string, - +oid: string, // the hash - +author: null | {| - +date: /* ISO 8601 */ string, - +user: null | UserJSON, - |}, - +message: string, - +parents: ConnectionJSON<{|+oid: string|}>, -|}; - -function commitFragment(): FragmentDefinition { - const b = build; - return b.fragment("commit", "Commit", [ - b.field("id"), - b.field("url"), - b.field("oid"), - b.field("message"), - b.field("author", {}, [ - b.field("date"), - b.field("user", {}, [b.fragmentSpread("whoami")]), - ]), - b.field("parents", {first: b.literal(PAGE_SIZE_COMMIT_PARENTS)}, [ - b.fragmentSpread("commitParents"), - ]), - ]); -} - -function commitHistoryFragment(): FragmentDefinition { - const b = build; - return b.fragment("commitHistory", "CommitHistoryConnection", [ - makePageInfo(), - b.field("nodes", {}, [b.fragmentSpread("commit")]), - ]); -} - -function commitParentsFragment(): FragmentDefinition { - const b = build; - return b.fragment("commitParents", "CommitConnection", [ - makePageInfo(), - b.field("nodes", {}, [b.field("oid")]), - ]); -} - -export const Reactions: {| - +THUMBS_UP: "THUMBS_UP", - +THUMBS_DOWN: "THUMBS_DOWN", - +LAUGH: "LAUGH", - +CONFUSED: "CONFUSED", - +HEART: "HEART", - +HOORAY: "HOORAY", -|} = Object.freeze({ - THUMBS_UP: "THUMBS_UP", - THUMBS_DOWN: "THUMBS_DOWN", - LAUGH: "LAUGH", - CONFUSED: "CONFUSED", - HEART: "HEART", - HOORAY: "HOORAY", -}); - -export type ReactionContent = - | typeof Reactions.THUMBS_UP - | typeof Reactions.THUMBS_DOWN - | typeof Reactions.LAUGH - | typeof Reactions.CONFUSED - | typeof Reactions.HEART - | typeof Reactions.HOORAY; - -export type ReactionJSON = {| - +id: string, - +content: ReactionContent, - +user: null | UserJSON, -|}; - -function reactionsFragment(): FragmentDefinition { - const b = build; - return b.fragment("reactions", "ReactionConnection", [ - makePageInfo(), - b.field("nodes", {}, [ - b.field("id"), - b.field("content"), - b.field("user", {}, [b.fragmentSpread("whoami")]), - ]), - ]); -} - -/** - * These fragments are used to construct the root query, and also to - * fetch more pages of specific entity types. - */ -export function createFragments(): FragmentDefinition[] { - return [ - whoamiFragment(), - issuesFragment(), - pullsFragment(), - commentsFragment(), - reviewsFragment(), - reviewCommentsFragment(), - commitHistoryFragment(), - commitParentsFragment(), - commitFragment(), - reactionsFragment(), - ]; -} - -export function createVariables(repoId: RepoId): {+[string]: any} { - return repoId; -} diff --git a/src/plugins/github/graphql.test.js b/src/plugins/github/graphql.test.js deleted file mode 100644 index 2a7523e..0000000 --- a/src/plugins/github/graphql.test.js +++ /dev/null @@ -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: "", - 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(); - }); -}); diff --git a/src/plugins/github/relationalView.js b/src/plugins/github/relationalView.js index 47dbdac..40394e5 100644 --- a/src/plugins/github/relationalView.js +++ b/src/plugins/github/relationalView.js @@ -16,14 +16,11 @@ import type { UserlikeAddress, } from "./nodes"; import * as T from "./graphqlTypes"; -import type {GithubResponseJSON} from "./graphql"; import * as GitNode from "../git/nodes"; import * as MapUtil from "../../util/map"; import * as NullUtil from "../../util/null"; import {botSet} from "./bots"; -import translateContinuations from "./translateContinuations"; - import { reviewUrlToId, issueCommentUrlToId, @@ -59,19 +56,12 @@ export class RelationalView { 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 { + // 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._addReferences(); } diff --git a/src/plugins/github/translateContinuations.js b/src/plugins/github/translateContinuations.js deleted file mode 100644 index 75745f1..0000000 --- a/src/plugins/github/translateContinuations.js +++ /dev/null @@ -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, -|} { - const repositoryJson = json.repository; - const warnings: Array = []; - - // 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, // 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 = 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}; -} diff --git a/src/plugins/github/translateContinuations.test.js b/src/plugins/github/translateContinuations.test.js deleted file mode 100644 index 80eb098..0000000 --- a/src/plugins/github/translateContinuations.test.js +++ /dev/null @@ -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", - }, - ]); - }); - }); -});