diff --git a/commands/api.go b/commands/api.go index 85552ba7de72486b2d59dd30eb47966fdfee76a7..0e31a9d1b83aacd891082e163fe31605e602e7c8 100644 --- a/commands/api.go +++ b/commands/api.go @@ -1,6 +1,7 @@ package commands import ( + "bytes" "fmt" "io" "io/ioutil" @@ -189,7 +190,8 @@ func apiCommand(cmd *Command, args *Args) { host = defHost.Host } - if path == "graphql" && params["query"] != nil { + isGraphQL := path == "graphql" + if isGraphQL && params["query"] != nil { query := params["query"].(string) query = strings.Replace(query, quote("{owner}"), quote(owner), 1) query = strings.Replace(query, quote("{repo}"), quote(repo), 1) @@ -253,8 +255,15 @@ func apiCommand(cmd *Command, args *Args) { fmt.Fprintf(out, "\r\n") } + endCursor := "" + hasNextPage := false + if parseJSON && jsonType { - utils.JSONPath(out, response.Body, colorize) + hasNextPage, endCursor = utils.JSONPath(out, response.Body, colorize) + } else if paginate && isGraphQL { + bodyCopy := &bytes.Buffer{} + io.Copy(out, io.TeeReader(response.Body, bodyCopy)) + hasNextPage, endCursor = utils.JSONPath(ioutil.Discard, bodyCopy, false) } else { io.Copy(out, response.Body) } @@ -266,7 +275,16 @@ func apiCommand(cmd *Command, args *Args) { requestLoop = false if paginate { - if nextLink := response.Link("next"); nextLink != "" { + if isGraphQL && hasNextPage && endCursor != "" { + if v, ok := params["variables"]; ok { + variables := v.(map[string]interface{}) + variables["endCursor"] = endCursor + } else { + variables := map[string]interface{}{"endCursor": endCursor} + params["variables"] = variables + } + requestLoop = true + } else if nextLink := response.Link("next"); nextLink != "" { path = nextLink requestLoop = true } diff --git a/features/api.feature b/features/api.feature index 5be1f541e5764072271341b600ebfa792b82e741..1c2987265b5ee74cdf5b873be2b516ec92b0603b 100644 --- a/features/api.feature +++ b/features/api.feature @@ -127,6 +127,28 @@ Feature: hub api [{"page":3}] """ + Scenario: Paginate GraphQL + Given the GitHub API server: + """ + post('/graphql') { + variables = params[:variables] || {} + page = (variables["endCursor"] || 1).to_i + json :data => { + :pageInfo => { + :hasNextPage => page < 3, + :endCursor => (page+1).to_s + } + } + } + """ + When I successfully run `hub api --paginate graphql -f query=QUERY` + Then the output should contain exactly: + """ + {"data":{"pageInfo":{"hasNextPage":true,"endCursor":"2"}}} + {"data":{"pageInfo":{"hasNextPage":true,"endCursor":"3"}}} + {"data":{"pageInfo":{"hasNextPage":false,"endCursor":"4"}}} + """ + Scenario: Avoid leaking token to a 3rd party Given the GitHub API server: """ diff --git a/utils/json.go b/utils/json.go index 046d1bc748d518bca987ba6c7ee4a68999ef790a..d612543ae94d4324ed322b9de230847c59249eaa 100644 --- a/utils/json.go +++ b/utils/json.go @@ -29,10 +29,7 @@ func stateKey(s *state) string { } } -func printValue(token json.Token) { -} - -func JSONPath(out io.Writer, src io.Reader, colorize bool) { +func JSONPath(out io.Writer, src io.Reader, colorize bool) (hasNextPage bool, endCursor string) { dec := json.NewDecoder(src) dec.UseNumber() @@ -84,12 +81,18 @@ func JSONPath(out io.Writer, src io.Reader, colorize bool) { switch tt := token.(type) { case string: fmt.Fprintf(out, "%s\n", strings.Replace(tt, "\n", "\\n", -1)) + if strings.HasSuffix(k, ".pageInfo.endCursor") { + endCursor = tt + } case json.Number: fmt.Fprintf(out, "%s\n", color("0;35", tt)) case nil: fmt.Fprintf(out, "\n") case bool: fmt.Fprintf(out, "%s\n", color("1;33", fmt.Sprintf("%v", tt))) + if strings.HasSuffix(k, ".pageInfo.hasNextPage") { + hasNextPage = tt + } default: panic("unknown type") } @@ -97,4 +100,5 @@ func JSONPath(out io.Writer, src io.Reader, colorize bool) { } } } + return }