Giter Club home page Giter Club logo

githubv4's Introduction

githubv4

Go Reference

Package githubv4 is a client library for accessing GitHub GraphQL API v4 (https://docs.github.com/en/graphql).

If you're looking for a client library for GitHub REST API v3, the recommended package is github (also known as go-github).

Focus

  • Friendly, simple and powerful API.
  • Correctness, high performance and efficiency.
  • Support all of GitHub GraphQL API v4 via code generation from schema.

Installation

go get github.com/shurcooL/githubv4

Usage

Authentication

GitHub GraphQL API v4 requires authentication. The githubv4 package does not directly handle authentication. Instead, when creating a new client, you're expected to pass an http.Client that performs authentication. The easiest and recommended way to do this is to use the golang.org/x/oauth2 package. You'll need an OAuth token from GitHub (for example, a personal API token) with the right scopes. Then:

import "golang.org/x/oauth2"

func main() {
	src := oauth2.StaticTokenSource(
		&oauth2.Token{AccessToken: os.Getenv("GITHUB_TOKEN")},
	)
	httpClient := oauth2.NewClient(context.Background(), src)

	client := githubv4.NewClient(httpClient)
	// Use client...
}

If you are using GitHub Enterprise, use githubv4.NewEnterpriseClient:

client := githubv4.NewEnterpriseClient(os.Getenv("GITHUB_ENDPOINT"), httpClient)
// Use client...

Simple Query

To make a query, you need to define a Go type that corresponds to the GitHub GraphQL schema, and contains the fields you're interested in querying. You can look up the GitHub GraphQL schema at https://docs.github.com/en/graphql/reference/queries.

For example, to make the following GraphQL query:

query {
	viewer {
		login
		createdAt
	}
}

You can define this variable:

var query struct {
	Viewer struct {
		Login     githubv4.String
		CreatedAt githubv4.DateTime
	}
}

Then call client.Query, passing a pointer to it:

err := client.Query(context.Background(), &query, nil)
if err != nil {
	// Handle error.
}
fmt.Println("    Login:", query.Viewer.Login)
fmt.Println("CreatedAt:", query.Viewer.CreatedAt)

// Output:
//     Login: gopher
// CreatedAt: 2017-05-26 21:17:14 +0000 UTC

Scalar Types

For each scalar in the GitHub GraphQL schema listed at https://docs.github.com/en/graphql/reference/scalars, there is a corresponding Go type in package githubv4.

You can use these types when writing queries:

var query struct {
	Viewer struct {
		Login          githubv4.String
		CreatedAt      githubv4.DateTime
		IsBountyHunter githubv4.Boolean
		BioHTML        githubv4.HTML
		WebsiteURL     githubv4.URI
	}
}
// Call client.Query() and use results in query...

However, depending on how you're planning to use the results of your query, it's often more convenient to use other Go types.

The encoding/json rules are used for converting individual JSON-encoded fields from a GraphQL response into Go values. See https://godoc.org/encoding/json#Unmarshal for details. The json.Unmarshaler interface is respected.

That means you can simplify the earlier query by using predeclared Go types:

// import "time"

var query struct {
	Viewer struct {
		Login          string    // E.g., "gopher".
		CreatedAt      time.Time // E.g., time.Date(2017, 5, 26, 21, 17, 14, 0, time.UTC).
		IsBountyHunter bool      // E.g., true.
		BioHTML        string    // E.g., `I am learning <a href="https://graphql.org">GraphQL</a>!`.
		WebsiteURL     string    // E.g., "https://golang.org".
	}
}
// Call client.Query() and use results in query...

The DateTime scalar is described as "an ISO-8601 encoded UTC date string". If you wanted to fetch in that form without parsing it into a time.Time, you can use the string type. For example, this would work:

// import "html/template"

type MyBoolean bool

var query struct {
	Viewer struct {
		Login          string        // E.g., "gopher".
		CreatedAt      string        // E.g., "2017-05-26T21:17:14Z".
		IsBountyHunter MyBoolean     // E.g., MyBoolean(true).
		BioHTML        template.HTML // E.g., template.HTML(`I am learning <a href="https://graphql.org">GraphQL</a>!`).
		WebsiteURL     template.URL  // E.g., template.URL("https://golang.org").
	}
}
// Call client.Query() and use results in query...

Arguments and Variables

Often, you'll want to specify arguments on some fields. You can use the graphql struct field tag for this.

For example, to make the following GraphQL query:

{
	repository(owner: "octocat", name: "Hello-World") {
		description
	}
}

You can define this variable:

var q struct {
	Repository struct {
		Description string
	} `graphql:"repository(owner: \"octocat\", name: \"Hello-World\")"`
}

Then call client.Query:

err := client.Query(context.Background(), &q, nil)
if err != nil {
	// Handle error.
}
fmt.Println(q.Repository.Description)

// Output:
// My first repository on GitHub!

However, that'll only work if the arguments are constant and known in advance. Otherwise, you will need to make use of variables. Replace the constants in the struct field tag with variable names:

// fetchRepoDescription fetches description of repo with owner and name.
func fetchRepoDescription(ctx context.Context, owner, name string) (string, error) {
	var q struct {
		Repository struct {
			Description string
		} `graphql:"repository(owner: $owner, name: $name)"`
	}

When sending variables to GraphQL, you need to use exact types that match GraphQL scalar types, otherwise the GraphQL server will return an error.

So, define a variables map with their values that are converted to GraphQL scalar types:

	variables := map[string]interface{}{
		"owner": githubv4.String(owner),
		"name":  githubv4.String(name),
	}

Finally, call client.Query providing variables:

	err := client.Query(ctx, &q, variables)
	return q.Repository.Description, err
}

Inline Fragments

Some GraphQL queries contain inline fragments. You can use the graphql struct field tag to express them.

For example, to make the following GraphQL query:

{
	repositoryOwner(login: "github") {
		login
		... on Organization {
			description
		}
		... on User {
			bio
		}
	}
}

You can define this variable:

var q struct {
	RepositoryOwner struct {
		Login        string
		Organization struct {
			Description string
		} `graphql:"... on Organization"`
		User struct {
			Bio string
		} `graphql:"... on User"`
	} `graphql:"repositoryOwner(login: \"github\")"`
}

Alternatively, you can define the struct types corresponding to inline fragments, and use them as embedded fields in your query:

type (
	OrganizationFragment struct {
		Description string
	}
	UserFragment struct {
		Bio string
	}
)

var q struct {
	RepositoryOwner struct {
		Login                string
		OrganizationFragment `graphql:"... on Organization"`
		UserFragment         `graphql:"... on User"`
	} `graphql:"repositoryOwner(login: \"github\")"`
}

Then call client.Query:

err := client.Query(context.Background(), &q, nil)
if err != nil {
	// Handle error.
}
fmt.Println(q.RepositoryOwner.Login)
fmt.Println(q.RepositoryOwner.Description)
fmt.Println(q.RepositoryOwner.Bio)

// Output:
// github
// How people build software.
//

Pagination

Imagine you wanted to get a complete list of comments in an issue, and not just the first 10 or so. To do that, you'll need to perform multiple queries and use pagination information. For example:

type comment struct {
	Body   string
	Author struct {
		Login     string
		AvatarURL string `graphql:"avatarUrl(size: 72)"`
	}
	ViewerCanReact bool
}
var q struct {
	Repository struct {
		Issue struct {
			Comments struct {
				Nodes    []comment
				PageInfo struct {
					EndCursor   githubv4.String
					HasNextPage bool
				}
			} `graphql:"comments(first: 100, after: $commentsCursor)"` // 100 per page.
		} `graphql:"issue(number: $issueNumber)"`
	} `graphql:"repository(owner: $repositoryOwner, name: $repositoryName)"`
}
variables := map[string]interface{}{
	"repositoryOwner": githubv4.String(owner),
	"repositoryName":  githubv4.String(name),
	"issueNumber":     githubv4.Int(issue),
	"commentsCursor":  (*githubv4.String)(nil), // Null after argument to get first page.
}

// Get comments from all pages.
var allComments []comment
for {
	err := client.Query(ctx, &q, variables)
	if err != nil {
		return err
	}
	allComments = append(allComments, q.Repository.Issue.Comments.Nodes...)
	if !q.Repository.Issue.Comments.PageInfo.HasNextPage {
		break
	}
	variables["commentsCursor"] = githubv4.NewString(q.Repository.Issue.Comments.PageInfo.EndCursor)
}

There is more than one way to perform pagination. Consider additional fields inside PageInfo object.

Mutations

Mutations often require information that you can only find out by performing a query first. Let's suppose you've already done that.

For example, to make the following GraphQL mutation:

mutation($input: AddReactionInput!) {
	addReaction(input: $input) {
		reaction {
			content
		}
		subject {
			id
		}
	}
}
variables {
	"input": {
		"subjectId": "MDU6SXNzdWUyMTc5NTQ0OTc=",
		"content": "HOORAY"
	}
}

You can define:

var m struct {
	AddReaction struct {
		Reaction struct {
			Content githubv4.ReactionContent
		}
		Subject struct {
			ID githubv4.ID
		}
	} `graphql:"addReaction(input: $input)"`
}
input := githubv4.AddReactionInput{
	SubjectID: targetIssue.ID, // ID of the target issue from a previous query.
	Content:   githubv4.ReactionContentHooray,
}

Then call client.Mutate:

err := client.Mutate(context.Background(), &m, input, nil)
if err != nil {
	// Handle error.
}
fmt.Printf("Added a %v reaction to subject with ID %#v!\n", m.AddReaction.Reaction.Content, m.AddReaction.Subject.ID)

// Output:
// Added a HOORAY reaction to subject with ID "MDU6SXNzdWUyMTc5NTQ0OTc="!

Directories

Path Synopsis
example/githubv4dev githubv4dev is a test program currently being used for developing githubv4 package.

License

githubv4's People

Contributors

bluekeyes avatar charlottestjohn avatar dmitshur avatar frbayart avatar haines avatar jlamb1 avatar mark-rushakoff avatar maxknee avatar n33pm avatar rkoster avatar timetoplatypus avatar wwsean08 avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

githubv4's Issues

Custom JSON tags in query data structure will cause response not to get populated.

If you do a query like this:

type query struct {
	Viewer struct {
		Login githubql.String `json:"username"`
	}
}

(Imagine the user wants to serialize the response later, or uses some struct that happens to have json tags defined for other reasons.)

The JSON-encoded response from GraphQL server will be:

{
    "data": {
        "viewer": {
            "login": "gopher"
        }
    }
}

So query.Viewer.Login will not be populated, since the Go struct has a JSON tag calling it "username", but the field is "login" in the response, which doesn't match.

This happens because encoding/json package, which is currently used to decode the JSON response from GraphQL server into the query data structure, is affected by json struct field tags.

Related to #10, because a solution to that will resolve this too.

Generalize transport using an interface

The current implementation supports using a http.Client. As discussed in #1, it would be nice if there is a transport interface, so that users of the package can implement their own transport.

In my case, I am using graphql over GRPC. GRPC is built on top of http.Client and the http.Client is not used directly. Instead, the a GRPC connection is passed to the generated GRPC client. For a simple example see: https://github.com/grpc/grpc-go/blob/master/examples/helloworld/greeter_client/main.go

I'd imagine that the interface should be quite low level and "close to the wire". In other words, transports should not have to know too much about how graphql works.

Mismatched GraphQL name/alias in query data structure will cause response not to get populated.

If you do a query like this:

var query struct {
	Viewer struct {
		Login     githubql.String
		Biography githubql.String `graphql:"bio"`
	}
}

The JSON-encoded response from GraphQL server will be:

{
    "data": {
        "viewer": {
            "login": "gopher",
            "bio": "The Go gopher."
        }
    }
}

So query.Viewer.Biography will not be populated, since the field is "bio" in the response, and the Go struct has a field named "Biography", which doesn't match.

This happens because encoding/json package, which is currently used to decode the JSON response from GraphQL server into the query data structure, ignores the graphql struct field tags.

Related to #10, because a solution to that will resolve this too.

QQ on generating proper struct

Hello,

I'm trying to convert the following GraphQL expression to Golang struct:

query{
  search(first: 100, type: REPOSITORY, query: "topic:golang org:google") {
    pageInfo {
      hasNextPage
      endCursor
      }
    repos: edges {
      repo: node {
        ... on Repository {
          name
          url
          id
        }
      }
    }
  }
}

I came up with something like this:

package main

import (
	"context"

	"fmt"

	"github.com/shurcooL/githubv4"
	"golang.org/x/oauth2"
)

var q struct {
	Search struct {
		PageInfo struct {
			HasNextPage bool
			EndCursor   githubv4.String
		}
		Repos []struct {
			Repo []struct {
				Repository struct {
					Name githubv4.String
				} `graphql:"... on Repository"`
			} `graphql:"repo: node"`
		} `graphql:"repos: edges"`
	} `graphql:"search(first: 3, type: REPOSITORY, query: \"topic:golang org:google\")"`
}

func main() {
	token := "123"
	ctx := context.Background()
	ts := oauth2.StaticTokenSource(
		&oauth2.Token{AccessToken: token},
	)
	tc := oauth2.NewClient(ctx, ts)

	client := githubv4.NewClient(tc)
	err := client.Query(context.Background(), &q, nil)
	if err != nil {
		fmt.Println(err)
	}
	fmt.Println(q.Search.Repos)
}

However, I'm getting back an empty struct:

$ go run main.go
struct field for "name" doesn't exist in any of 1 places to unmarshal
[{[]}]

Ideas, thoughts?

Thank you for your time & Happy new year!

P.

Enum value names (avoiding collisions).

As discovered in #7, the initial schema I had in mind for the enums will not work, because of name collision between enum values of different types:

// IssueState represents the possible states of an issue.
type IssueState string

// The possible states of an issue.
const (
	Open   IssueState = "OPEN"   // An issue that is still open.
	Closed IssueState = "CLOSED" // An issue that has been closed.
)

// PullRequestState represents the possible states of a pull request.
type PullRequestState string

// The possible states of a pull request.
const (
	Open   PullRequestState = "OPEN"   // A pull request that is still open.
	Closed PullRequestState = "CLOSED" // A pull request that has been closed without being merged.
	Merged PullRequestState = "MERGED" // A pull request that has been closed by being merged.
)

// ProjectState represents state of the project; either 'open' or 'closed'.
type ProjectState string

// State of the project; either 'open' or 'closed'.
const (
	Open   ProjectState = "OPEN"   // The project is open.
	Closed ProjectState = "CLOSED" // The project is closed.
)

...
# github.com/shurcooL/githubql
./enum.go:71: CreatedAt redeclared in this block
	previous declaration at ./enum.go:17
./enum.go:111: Open redeclared in this block
	previous declaration at ./enum.go:8
./enum.go:112: Closed redeclared in this block
	previous declaration at ./enum.go:9
./enum.go:120: CreatedAt redeclared in this block
	previous declaration at ./enum.go:71
./enum.go:121: UpdatedAt redeclared in this block
	previous declaration at ./enum.go:18
./enum.go:149: CreatedAt redeclared in this block
	previous declaration at ./enum.go:120
./enum.go:150: UpdatedAt redeclared in this block
	previous declaration at ./enum.go:121
./enum.go:152: Name redeclared in this block
	previous declaration at ./enum.go:19
./enum.go:170: Open redeclared in this block
	previous declaration at ./enum.go:111
./enum.go:171: Closed redeclared in this block
	previous declaration at ./enum.go:112
./enum.go:171: too many errors

Solutions

These are the solutions that I've got so far and are up for consideration.

Solution 1

Prepend the type name in front of the enum value name, to ensure each identifier is unique and collisions are not possible:

 package githubql
 
 // ReactionContent represents emojis that can be attached to Issues, Pull Requests and Comments.
 type ReactionContent string
 
 // Emojis that can be attached to Issues, Pull Requests and Comments.
 const (
-	ThumbsUp   ReactionContent = "THUMBS_UP"   // Represents the 👍 emoji.
-	ThumbsDown ReactionContent = "THUMBS_DOWN" // Represents the 👎 emoji.
-	Laugh      ReactionContent = "LAUGH"       // Represents the 😄 emoji.
-	Hooray     ReactionContent = "HOORAY"      // Represents the 🎉 emoji.
-	Confused   ReactionContent = "CONFUSED"    // Represents the 😕 emoji.
-	Heart      ReactionContent = "HEART"       // Represents the ❤️ emoji.
+	ReactionContentThumbsUp   ReactionContent = "THUMBS_UP"   // Represents the 👍 emoji.
+	ReactionContentThumbsDown ReactionContent = "THUMBS_DOWN" // Represents the 👎 emoji.
+	ReactionContentLaugh      ReactionContent = "LAUGH"       // Represents the 😄 emoji.
+	ReactionContentHooray     ReactionContent = "HOORAY"      // Represents the 🎉 emoji.
+	ReactionContentConfused   ReactionContent = "CONFUSED"    // Represents the 😕 emoji.
+	ReactionContentHeart      ReactionContent = "HEART"       // Represents the ❤️ emoji.
 )

Usage becomes:

input := githubql.AddReactionInput{
	SubjectID: q.Repository.Issue.ID,
	Content:   githubql.ReactionContentThumbsUp,
}

This is very simple, guaranteed to not have collisions. But the enum value identifiers can become quite verbose, and less readable.

Solution 2

This is a minor variation of solution 1. The idea is to make the enum values slightly more readable by separating the type name and enum value by a middot-like character ۰ (U+06F0).

 package githubql
 
 // ReactionContent represents emojis that can be attached to Issues, Pull Requests and Comments.
 type ReactionContent string
 
 // Emojis that can be attached to Issues, Pull Requests and Comments.
 const (
-	ThumbsUp   ReactionContent = "THUMBS_UP"   // Represents the 👍 emoji.
-	ThumbsDown ReactionContent = "THUMBS_DOWN" // Represents the 👎 emoji.
-	Laugh      ReactionContent = "LAUGH"       // Represents the 😄 emoji.
-	Hooray     ReactionContent = "HOORAY"      // Represents the 🎉 emoji.
-	Confused   ReactionContent = "CONFUSED"    // Represents the 😕 emoji.
-	Heart      ReactionContent = "HEART"       // Represents the ❤️ emoji.
+	ReactionContent۰ThumbsUp   ReactionContent = "THUMBS_UP"   // Represents the 👍 emoji.
+	ReactionContent۰ThumbsDown ReactionContent = "THUMBS_DOWN" // Represents the 👎 emoji.
+	ReactionContent۰Laugh      ReactionContent = "LAUGH"       // Represents the 😄 emoji.
+	ReactionContent۰Hooray     ReactionContent = "HOORAY"      // Represents the 🎉 emoji.
+	ReactionContent۰Confused   ReactionContent = "CONFUSED"    // Represents the 😕 emoji.
+	ReactionContent۰Heart      ReactionContent = "HEART"       // Represents the ❤️ emoji.
 )

Usage becomes:

input := githubql.AddReactionInput{
	SubjectID: q.Repository.Issue.ID,
	Content:   githubql.ReactionContent۰ThumbsUp,
}

The unicode character is not easy to type, but easy to achieve via autocompletion:

image

Solution 3

This is also a variation of solution 1. The idea, suggested to me by @ScottMansfield (thanks!), is to use an initialism of the type name rather than the full name. This makes the identifier names shorter, but still has a risk of there being name collisions if two different types with same initialism have same enum values.

 package githubql
 
 // ReactionContent represents emojis that can be attached to Issues, Pull Requests and Comments.
 type ReactionContent string
 
 // Emojis that can be attached to Issues, Pull Requests and Comments.
 const (
-	ThumbsUp   ReactionContent = "THUMBS_UP"   // Represents the 👍 emoji.
-	ThumbsDown ReactionContent = "THUMBS_DOWN" // Represents the 👎 emoji.
-	Laugh      ReactionContent = "LAUGH"       // Represents the 😄 emoji.
-	Hooray     ReactionContent = "HOORAY"      // Represents the 🎉 emoji.
-	Confused   ReactionContent = "CONFUSED"    // Represents the 😕 emoji.
-	Heart      ReactionContent = "HEART"       // Represents the ❤️ emoji.
+	RCThumbsUp   ReactionContent = "THUMBS_UP"   // Represents the 👍 emoji.
+	RCThumbsDown ReactionContent = "THUMBS_DOWN" // Represents the 👎 emoji.
+	RCLaugh      ReactionContent = "LAUGH"       // Represents the 😄 emoji.
+	RCHooray     ReactionContent = "HOORAY"      // Represents the 🎉 emoji.
+	RCConfused   ReactionContent = "CONFUSED"    // Represents the 😕 emoji.
+	RCHeart      ReactionContent = "HEART"       // Represents the ❤️ emoji.
 )

Usage becomes:

input := githubql.AddReactionInput{
	SubjectID: q.Repository.Issue.ID,
	Content:   githubql.RCThumbsUp,
}

Unfortunately, this approach led to a collision with the existing enums in GitHub GraphQL API:

const ROFCreatedAt ReactionOrderField = "CREATED_AT" // Allows ordering a list of reactions by when they were created.

const ROFCreatedAt RepositoryOrderField = "CREATED_AT" // Order repositories by creation time.
./enum.go:149: ROFCreatedAt redeclared in this block
	previous declaration at ./enum.go:71

It's possible to detect when a collision would occur, and use something more specific that doesn't collide, instead of the initialism. But that introduces complexity, and inconsistency/unpredictability in the naming.

Solution 4

This idea is somewhat related to solution 2, but instead of a hard-to-type character, it becomes a normal dot selector. The idea is to create a separate package for each enum type, and define its values in that package. The enum types are still in main githubql package.

package githubql
 
// ReactionContent represents emojis that can be attached to Issues, Pull Requests and Comments.
type ReactionContent string
// Package reactioncontent contains enum values of githubql.ReactionContent type.
package reactioncontent

import "github.com/shurcooL/githubql"
 
// Emojis that can be attached to Issues, Pull Requests and Comments.
const (
	ThumbsUp   githubql.ReactionContent = "THUMBS_UP"   // Represents the 👍 emoji.
	ThumbsDown githubql.ReactionContent = "THUMBS_DOWN" // Represents the 👎 emoji.
	Laugh      githubql.ReactionContent = "LAUGH"       // Represents the 😄 emoji.
	Hooray     githubql.ReactionContent = "HOORAY"      // Represents the 🎉 emoji.
	Confused   githubql.ReactionContent = "CONFUSED"    // Represents the 😕 emoji.
	Heart      githubql.ReactionContent = "HEART"       // Represents the ❤️ emoji.
)

Usage becomes:

input := githubql.AddReactionInput{
	SubjectID: q.Repository.Issue.ID,
	Content:   reactioncontent.ThumbsUp,
}

The dot character is easy to type, and the code is very readable once written. But this will require importing many new small packages when using enum values. A tool like goimports will make that significantly easier, but it may still be problematic. Also, the documentation will be split up into multiple small packages, which may be harder to read.

Conclusion

All solutions considered so far seem to have certain upsides and downsides. I'm not seeing one solution that is clearly superior to all others. I'm considering going with solution 1 or 2 to begin with, and be open to revisit this decision.

Decode struct within a slice

Hi, I was just using the package to query tags list on my repo.
the query looks like this:

{
  repository(owner: "rails", name: "rails") {
    name
    tags: refs(refPrefix: "refs/tags/", first: 2, orderBy: {field: TAG_COMMIT_DATE, direction: DESC}) {
      edges {
        tag: node {
          name
        }
      }
    }
  }
}

The response looks like this:

{
  "data": {
    "repository": {
      "name": "rails",
      "tags": {
        "edges": [
          {
            "tag": {
              "name": "v5.2.0.rc2"
            }
          },
          {
            "tag": {
              "name": "v5.1.5"
            }
          }
        ]
      }
    }
  }
}

it's working fine on graphQL explorer, but fails to decode here in the package.
from the small Debugging I did it seems like there is no attempt to decode objects within the slice and I'm getting slice doesn't exist in any of 1 places to unmarshal
Any idea or fix ?
Thanks !

Generalize the client?

Going by the examples, it looks like this might be great as a general graphql client that should be able to query any graphql server. Maybe a command line tool can be provided to run an introspection query against a graphql server to generate the appropriate types as well.

"can't decode into non-slice invalid"

Of the two following queries, the first succeeds and the second spits up an error. I spent a while adding debug printfs in graphql/internal/jsonutil but I wasn't able to figure it out. All I know is that the Commits object in the second query causes the problem. Removing that object and replacing it with something else in the PR works fine.

Let me know if you need more info :)

package main

import (
    "context"
    "fmt"
    "github.com/shurcooL/githubql"
    "golang.org/x/oauth2"
    "os"
)

type repoQuery struct {
    Repository struct {
        PullRequests struct {
            Nodes []struct {
                Commits struct {
                    Nodes []struct {
                        URL githubql.URI `graphql:"url"`
                    }
                } `graphql:"commits(last: 1)"`
            }
        } `graphql:"pullRequests(first: 1)"`
    } `graphql:"repository(owner: \"kubernetes\", name: \"test-infra\")"`
}

type searchQuery struct {
    Search struct {
        Nodes []struct {
            PullRequest struct {
                Commits struct {
                    Nodes []struct {
                        URL githubql.URI `graphql:"url"`
                    }
                } `graphql:"commits(last: 1)"`
            } `graphql:"... on PullRequest"`
        }
    } `graphql:"search(type: ISSUE, first: 1, query: \"type:pr repo:kubernetes/test-infra\")"`
}   

func main() {
    src := oauth2.StaticTokenSource(
        &oauth2.Token{AccessToken: os.Getenv("GITHUB_TOKEN")},
    )   
    httpClient := oauth2.NewClient(context.Background(), src)
    
    client := githubql.NewClient(httpClient)
    sq := searchQuery{}
    if err := client.Query(context.Background(), &sq, nil); err != nil {
        fmt.Printf("Search error: %v\n", err)
    }
    rq := repoQuery{}
    if err := client.Query(context.Background(), &rq, nil); err != nil {
        fmt.Printf("Repo error: %v\n", err)
    }
} 
$ GITHUB_TOKEN=<redacted> go run test.go
Search error: can't decode into non-slice invalid

Unmarshaling on unions with same parameter name

@dmitshur Thanks for the nice library on graphql and GitHub.

What is the problem?

I am trying to query for the audit log actor and that part of the API uses unions. The relevant part of the query is the actor one so I will omit the rest. Actors can be bots, organizations or users, so it uses 3 types of unions.

type EntryActor struct {
	Typename     string `graphql:"__typename"`
	Bot          `graphql:"...on Bot" json:",omitempty"`
	Organization `graphql:"... on Organization" json:",omitempty"`
	User         `graphql:"... on User" json:",omitempty"`
}

The problem comes when each of this elements have fields that are common (like login, createdAt or updatedAt), as the parser don't know where to do the parsing and if a specific field is repeated in multiple embedded structs it gets ommited (see documentation below).

The Go visibility rules for struct fields are amended for JSON when deciding which field to marshal or unmarshal. If there are multiple fields at the same level, and that level is the least nested (and would therefore be the nesting level selected by the usual Go rules), the following extra rules apply:

  1. Of those fields, if any are JSON-tagged, only tagged fields are considered, even if there are multiple untagged fields that would otherwise conflict.

  2. If there is exactly one field (tagged or not according to the first rule), that is selected.

  3. Otherwise there are multiple fields, and all are ignored; no error occurs.

I also thought of creating a custom unmarshal function, but it seems not to work due to the way jsonutil.unmarshalGraphQL behaves as it used the custom decoder. See the function below:

func (actor *EntryActor) UnmarshalJSON(data []byte) error {
	// Ignore null, like in the main JSON package.
	if string(data) == "null" {
		return nil
	}
	var top struct {
		Typename string
	}
	if err := json.Unmarshal(data, &top); err != nil {
		return err
	}
	actor.Typename = top.Typename
	var v interface{}
	switch top.Typename {
	case "User":
		v = &actor.User
	case "Org":
		v = &actor.Organization
	case "Bot":
		v = &actor.Bot
	}
	if err := json.Unmarshal(data, &v); err != nil {
		return err
	}
	return nil
}

Is there a way I could workaround this so I an tell the parser how to parse each of those by __typename? With the current implementation seems hard to do properly queries against the auditLog which makes an intensive use of unions.

Code and query

The graphql query I am testing would look like:

query($org: String!) {
  organization(login: $org) {
    auditLog(first: 20){
      nodes {
        ... on AuditEntry {
          actor {
            __typename
            ... on Actor {
	      createdAt
              login
              resourcePath
              url
            }
            ... on User {
	      createdAt
              login
              resourcePath
              url
            }
            ... on Bot {
	      createdAt
              login
              resourcePath
              url
            }
          }
        }
      }
    }
  }
}

And all the go structs:

allExistingEntriesQuery struct {
	Organization struct {
		AuditLog struct {
			PageInfo struct {
				EndCursor   graphql.String
				HasNextPage bool
			}
			Nodes LogEntries
		} `graphql:"auditLog(first: 1, after: $page)"`
	} `graphql:"organization(login: $login)"`
}


type LogEntries []LogEntry

type LogEntry struct {
	Entry `graphql:"... on AuditEntry"`
}

type Entry struct {
	Actor             *EntryActor    `json:",omitempty"`
}

type EntryActor struct {
	Typename     string       `graphql:"__typename"`
	Bot          Bot          `graphql:"...on Bot" json:",omitempty"`
	Organization Organization `graphql:"... on Organization" json:",omitempty"`
	User         User         `graphql:"... on User" json:",omitempty"`
}

type User struct {
	CreatedAt                       *time.Time                    `json:",omitempty"`
	Login                      *string          `json:",omitempty"`
	ResourcePath               *string          `json:",omitempty"`
	Url                        *string          `json:",omitempty"`
}

type Organization struct {
	CreatedAt                       *time.Time                    `json:",omitempty"`
	Login                           *string                       `json:",omitempty"`
	ResourcePath                    *string                       `json:",omitempty"`
	Url                             *string                       `json:",omitempty"`
}

type Bot struct {
	CreatedAt                       *time.Time                    `json:",omitempty"`
	Login                           *string                       `json:",omitempty"`
	ResourcePath                    *string                       `json:",omitempty"`
	Url                             *string                       `json:",omitempty"`
}

Result: using named properties

If I use Bot, User and Organization named the result duplicated the values on each of them:

[
    {
        "Actor": {
            "Typename": "User",
            "Bot": {
                "CreatedAt": "2012-08-13T10:57:11Z",
                "Login": "droidpl",
                "ResourcePath": "/droidpl",
                "Url": "https://github.com/droidpl"
            },
            "Organization": {
                "CreatedAt": "2012-08-13T10:57:11Z",
                "Login": "droidpl",
                "ResourcePath": "/droidpl",
                "Url": "https://github.com/droidpl"
            },
            "User": {
                "CreatedAt": "2012-08-13T10:57:11Z",
                "Login": "droidpl",
                "ResourcePath": "/droidpl",
                "Url": "https://github.com/droidpl"
            }
        }
    }
]

While this should be only parsing the User part as __typename references, not each of the repeated elements

Result: with unnamed properties

If I use as EntryActor the properties unnamed:

type EntryActor struct {
	Typename     string       `graphql:"__typename"`
	Bot          `graphql:"...on Bot" json:",omitempty"`
	Organization `graphql:"... on Organization" json:",omitempty"`
	User         `graphql:"... on User" json:",omitempty"`
}

It creates a collision and given the rule in the documentation, the field gets ignored and the result looks like:

[
    {
        "Actor": {
            "Typename": "User"
        }
    }
]

Union support.

It looks like supporting unions will be an interesting challenge.

From https://developer.github.com/v4/reference/union/:

A union is a type of object representing many objects.

For example, imagine you want to fetch a timeline of an issue. The type of timeline field of Issue object is IssueTimelineConnection, which is a connection type for IssueTimelineItem, a union:

https://developer.github.com/v4/reference/union/issuetimelineitem/

Suppose you want to find all closed/reopened events within an issue. For each event, you want to know who was the actor, and when it happened. You might write a query like this:

query {
  repository(owner: "shurcooL-test", name: "test-repo") {
    issue(number: 3) {
      timeline(first:10) {
        nodes {
          typename: __typename
          ... on ClosedEvent {
            createdAt
            actor {login}
          }
          ... on ReopenedEvent {
            createdAt
            actor {login}
          }
        }
      }
    }
  }
}

Which might give you a response like this:

{
  "data": {
    "repository": {
      "issue": {
        "timeline": {
          "nodes": [
            {
              "typename": "ClosedEvent",
              "createdAt": "2017-06-29T04:12:01Z",
              "actor": {
                "login": "shurcooL-test"
              }
            },
            {
              "typename": "ReopenedEvent",
              "createdAt": "2017-06-29T04:12:06Z",
              "actor": {
                "login": "shurcooL-test"
              }
            }
          ]
        }
      }
    }
  }
}

A Go type that can result in that query is something like this:

type ClosedEvent struct {
	Actor     struct{ Login githubql.String }
	CreatedAt githubql.DateTime
}
type ReopenedEvent struct {
	Actor     struct{ Login githubql.String }
	CreatedAt githubql.DateTime
}
type IssueTimelineItem struct {
	Typename      string `graphql:"typename :__typename"`
	ClosedEvent   `graphql:"... on ClosedEvent"`
	ReopenedEvent `graphql:"... on ReopenedEvent"`
}
var q struct {
	Repository struct {
		Issue struct {
			Timeline struct {
				Nodes []IssueTimelineItem
			} `graphql:"timeline(first: 10)"`
		} `graphql:"issue(number: $issueNumber)"`
	} `graphql:"repository(owner: $repositoryOwner, name: $repositoryName)"`
}

Unfortunately, because IssueTimelineItem embeds both ClosedEvent and ReopenedEvent, according to the current encoding/json unmarshaling rules:

If there are multiple fields at the same level, and that level is the least nested (and would therefore be the nesting level selected by the usual Go rules), the following extra rules apply:

  1. Of those fields, if any are JSON-tagged, only tagged fields are considered, even if there are multiple untagged fields that would otherwise conflict.

  2. If there is exactly one field (tagged or not according to the first rule), that is selected.

  3. Otherwise there are multiple fields, and all are ignored; no error occurs.

The multiple fields are ignored. So, the following JSON value:

{
  "typename": "ClosedEvent",
  "createdAt": "2017-06-29T04:12:01Z",
  "actor": {
    "login": "shurcooL-test"
  }
}

When unmarshaled into IssueTimelineItem , becomes:

IssueTimelineItem{
	Typename: "ClosedEvent",
	ClosedEvent: ClosedEvent{
		Actor: struct{ Login githubql.String }{
			Login: "",
		},
		CreatedAt: githubql.DateTime{},
	},
	ReopenedEvent: ReopenedEvent{
		Actor: struct{ Login githubql.String }{
			Login: "",
		},
		CreatedAt: githubql.DateTime{},
	},
}

Only the value of Typename has correct value. Accessing item.ClosedEvent.CreatedAt and item.ClosedEvent.Actor.Login gives zero values.

This is not a problem if the multiple potential event types don't have common fields.

I came up with the following solution on the user-side, to implement a custom UnmarshalJSON method on the IssueTimelineItem type:

// UnmarshalJSON implements the json.Unmarshaler interface.
func (i *IssueTimelineItem) UnmarshalJSON(data []byte) error {
	// Ignore null, like in the main JSON package.
	if string(data) == "null" {
		return nil
	}
	var top struct {
		Typename string
	}
	err := json.Unmarshal(data, &top)
	if err != nil {
		return err
	}
	*i = IssueTimelineItem{Typename: top.Typename}
	var v interface{}
	switch i.Typename {
	case "ClosedEvent":
		v = &i.ClosedEvent
	case "ReopenedEvent":
		v = &i.ReopenedEvent
	default:
		return nil
	}
	err = json.Unmarshal(data, v)
	return err
}

But this is very verbose, inconvenient, error prone. It also means the custom IssueTimelineItem type must be declared at package level rather than inside a function. I want to find a better solution.

One of the big guns here is making a copy of encoding/json internal to githubql and changing its behavior, but that's something I'd really like to avoid.

Enable starfox preview

I would like to enable the starfox-preview. In order to do that, a header must be set like this: eq.Header.Add("Accept", "application/vnd.github.starfox-preview")

I tested this with setting it in ctxhttp.go, which is of course only good for testing. Where would be the right place to accomplish this? The Post() func in ctxhttp.go immediately returns the response with the execution of Do(). Thx!

How to check if the result is null?

query:

{
  repositoryOwner(login: "_dnxv__") {
    ... on ProfileOwner {
      pinnedItemsRemaining
      itemShowcase {
        hasPinnedItems
      }
    }
  }
}

result:

{
  "data": {
    "repositoryOwner": null
  }
}

but the variable query in golang is a struct, so how to check if it's null? any workaround or tricks?

How to query for commits in a given Repository

Hello,

I'm trying to convert this query into a struct:

{
  repository(owner: "google", name: "gson") {
    name
    refs(first: 100, refPrefix: "refs/heads/") {
      edges {
        node {
          name
          target {
            ... on Commit {
              id
              history(first: 2) {
                totalCount
                edges {
                  node {
                    ... on Commit {
                      committer {
                        date
                        email
                        name
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    }
  }
}

I got this from https://stackoverflow.com/questions/15919539/github-api-how-to-efficiently-find-the-number-of-commits-for-a-repository

I tested it on the Github explorer just to be safe and its working as it should.
What I'm trying to do is to get the "TotalCount" of commits per contributor to a specific Repository.
I'm not really sure how I'm supposed to do it exactly.
I already got this:

type repositoryInfo struct {
	Repository struct {
		Refs struct {
			TotalCount githubv4.Int //number of branches
			Nodes []ref
			PageInfo pageInfo
		}`graphql:"refs(refPrefix:$prefix,first:$refFirst,after:$refAfter,orderBy:$orderBy)"`
	} `graphql:"repository(owner:$login,name:$repositoryName)"`
}

type ref struct {
	Name githubv4.String
	Prefix githubv4.String

	Target struct {
		AbbreviatedOid githubv4.String
		ID githubv4.GitObjectID
		//History struct {
		//	TotalCount githubv4.Int
		//}`graphql:"history(first:0)"`
	}`graphql:"... on Commit"`
}

But I get this error message:

Fragment on Commit can't be spread inside Ref

However I can't seem to find any useful documentation.
Can anybody tell me what I'm doing wrong please?

Inline fragments with strings

ran into issues running variables with inline fragments within a github query string
graphql:"search(query: \"language:$language\", type: REPOSITORY, first:$pageSize)"

however, this version works correctly with variables:
graphql:"search(query: \"language:PowerShell\", type: REPOSITORY, first:$pageSize)"

variables := map[string]interface{}{
	"language": githubql.String("PowerShell"),
	"pageSize": githubql.Int(100),
}
err := client.Query(context.Background(), &query, variables)

PullRequest lacks IsDraft field

The IsDraft field appears to exist for PullRequests in the graphql v4 docs:

Screen Shot 2020-05-28 at 9 47 22 AM

However, when querying for it I am given this response: GHE GraphQL query error: Field 'isDraft' doesn't exist on type 'PullRequest' I'm running GHE 2.20.7 and the April 13th, 2020 commit/release of this repo. This information is available via the v3 API.

Googling around a little showed that some folks were having to use a preview header to get this data via v4. But that information was a little dated.

Any suggestions on how we could add support for this here?

rate limiting: how to ?

As far as I can tell, this package doesn't have any specific code path to handle the Github rate limiting.

It'd be nice to at least return a specific error on HTTP 429 so that retry/back-of could be implemented outside of this package. Ideally this would be implemented within githubv4.

Unable to get query commit in repository.

Trying to perform this query

query {
  repository(owner: "octocat", name: "Hello-world") {
    object(oid:"e212ffe458a5f1df7647ee461aff22c7b3a52580") {
      ... on Commit {
        pushedDate
      }
    }
  }
}

For this i use next go code:

	var q struct {
		Repository struct {
			Object struct {
				Commit struct {
					pushedDate github.DateTime
				} `graphql:"... on Commit"`
			} `graphql:"object(oid: $sha)"`
		} `graphql:"repository(owner: \"octocat\", name: $name)"`
	}

	variables := map[string]interface{}{
		"name":  github.String("Hello-World"),
		"sha": github.GitObjectID("e212ffe458a5f1df7647ee461aff22c7b3a52580"),
	}

	err := client.Query(ctx, &q, variables)

In the end i got error:
struct field for "pushedDate" doesn't exist in any of 2 places to unmarshal

Idiomatic/suggested variable naming style.

As part of the API decision #4, using structs forced me to use exported field names (since encoding/json skips unexported fields when doing JSON marshaling), which means all variables were capitalized. When I made the switch to use maps instead of structs, I kept the names unmodified (to make fewer changes all at once).

variables := map[string]interface{}{
	"RepositoryOwner": githubql.String(owner),
	"RepositoryName":  githubql.String(name),
	"IssueNumber":     githubql.Int(issue),
}

// `graphql:"repository(owner: $RepositoryOwner, name: $RepositoryName)"`
// `graphql:"issue(number: $IssueNumber)"`

However, now that maps are used, it's possible to use lower case variables, which seems to be the idiomatic GraphQL style:

variables := map[string]interface{}{
	"repositoryOwner": githubql.String(owner),
	"repositoryName":  githubql.String(name),
	"issueNumber":     githubql.Int(issue),
}

// `graphql:"repository(owner: $repositoryOwner, name: $repositoryName)"`
// `graphql:"issue(number: $issueNumber)"`

It seems like a good idea to use lower case variable names and stay closer to idiomatic GraphQL variable naming.

That's what I'll try to go with next, and see how it goes.

BodyHTML doesn't exist on type Issue

var q struct {
  Repository struct {
    Issue struct {
      Number github.Int `json:"id"`
      Title      github.String `json:"title"`
      URL github.URI `json:"url"`
      CreatedAt  github.DateTime `json:"createdAt"`
      BodyHTML github.String `json:"bodyHTML"`
    } `graphql:"issue(number:31)"`
  } `graphql:"repository(owner: \"\", name: \"\")"`
}

error message: [{%!d(string=Field 'bodyHtml' doesn't exist on type 'Issue') [{1 107}]}]data {{{0 %!d(githubv4.String=) {0} {{0 0 0}} %!d(githubv4.String=)}}}%

but I use https://developer.github.com/v4/explorer/ can output the result:

image

search doesn't work

Seems obvious given knowledge of the type-based (rather than value-based) reflection code in hacky.go, but nevertheless it is impossible to express a search query such as:

{
  search(first: 100, query: "repo:cockroachdb/cockroach state:open teamcity: failed tests on master", type: ISSUE) {
    nodes {
      ... on Issue {
        number
        repository {
          nameWithOwner
        }
      }
    }
  }
}

because the search's values aren't serialized. My code:

package main

import (
	"context"
	"log"

	"github.com/shurcooL/githubql"
	"golang.org/x/oauth2"
)

func main() {
	ctx := context.Background()

	client := githubql.NewClient(oauth2.NewClient(ctx, oauth2.StaticTokenSource(
		&oauth2.Token{AccessToken: "deadbeef"},
	)))

	var query struct {
		Search struct {
			First githubql.Int
			Query githubql.String
			Type  githubql.SearchType
		}
		Nodes []struct {
			Issue struct {
				Number githubql.Int
			}
		}
	}
	query.Search.First = 100
	query.Search.Query = "repo:cockroachdb/cockroach state:open teamcity: failed tests on master"
	query.Search.Type = githubql.Issue

	log.Printf("err=%v", client.Query(ctx, &query, nil))
}

produces the query:

{"query":"{search{first,query,type},nodes{issue{number}}}"}

and the error:

err=Field 'search' is missing required arguments: query, type

Ability to print query string before POSTing

Currently the whole process happens in the do function of the Client and there is no easy way of printing out the query string.

I would like to inspect this query string during the development of my app, but it would be useful for other things, such as logging.

Support for "expression:" on "object"?

I have the following query (only the Repository section is included) that I tested against the GitHub GraphQL API to get the content of a file as text:

...
nodes {
        name
        object(expression: "master:LICENSE") {
          ... on Blob {
            text
          }
        }
      }
...

I was wondering how to translate the object(expression: "master:LICENSE") for githubql ?
I tried the following (without success):

Object struct {
     Blob struct {
       Text githubql.String
     }`graphql:"... on Blob"`
 }`graphql:"expression:\"master:LICENSE\""`   

->Error: Parse error on "master:LICENSE" (STRING)
Please note that I'm a total newbie to Go and I have very limited experience with GraphQL so do not hesitate to point me to the doc if I missed something :)

Query Interfaces and unions

Thanks for taking a crack at the client library...

How would you query a specific type when the schema specifies an interface or union as the return type?

For instance, say I try the following query to github:

query {
  search(first: 100, type: ISSUE, query: \"user:ereyes01 repo:firebase label:enhancement state:open\") {
    nodes {
      ... on Issue {
        number
      }
    }
  }
}

The search query returns a SearchResultItem, and in this case, I just want the issues. Is there a way using your library to specify the .. on Issue part of the query above?

Query batching and/or dynamic queries.

I want to query multiple repositories at the same time. But I don't want to write the query for a specific number of repositories, but rather create the query at runtime.
I currently don't see a way to do that.

{
  justwatchcom_gopass: repository(owner: "justwatchcom", name: "gopass") {
    id
    name
  }
  prometheus_prometheus: repository(owner: "prometheus", name: "prometheus") {
    id
    name
  }
   ... a lot of other dynamic repositories added at runtime
}

As a work around I'm doing these queries one after another, but as it's one of the benefits of doing multiple resource queries at ones we should try to support this.

Go type of variables object.

The variables object, which is optionally present as part of queries and mutations, must be valid JSON object.

By encoding/json rules, we can use a Go struct or a Go map type, which will then get JSON encoded into a JSON object.

During initial development, I started with the following signature for Query:

func (c *Client) Query(ctx context.Context, q interface{}, variables interface{}) error

And usage looked like this:

variables := struct {
	RepositoryOwner githubql.String
	RepositoryName  githubql.String
	IssueNumber     githubql.Int
}{githubql.String(repo.Owner), githubql.String(repo.Repo), githubql.Int(id)}
err = client.Query(ctx, &v, variables)

However, having to define a new struct for each variables object seemed less optimal than using a map. Mostly because the Go syntax has you repeating the fields and the field values twice, as well as doing type conversions.

Compare to the syntax when using a map:

func (c *Client) Query(ctx context.Context, q interface{}, variables map[string]interface{}) error
variables := map[string]interface{}{
	"RepositoryOwner": githubql.String(repo.Owner),
	"RepositoryName":  githubql.String(repo.Repo),
	"IssueNumber":     githubql.Int(id),
}
err = client.Query(ctx, &v, variables)

That seems easier to write and read, so that was the API decision I went with for the initial commit.

Consider support for Schema Previews.

Similar to GitHub REST API v3, GitHub GraphQL API v4 also has preview APIs that let users try out new features and changes before they become part of the official GitHub API:

https://developer.github.com/v4/previews/

One of the core focuses of this library is:

  • Support all of GitHub GraphQL API v4 via code generation from schema.

There are two relevant parts. One, we might want to support the preview APIs, since they're in a way a part of the API. Two, we want to try to do this in an automated way, rather than manually.

Finding a way to support these without in an automated way is very important, because otherwise, this could create a very significant amount of manual work (compared to the work required to develop and maintain the rest of this library).

How to encode GraphQL Aliases

Hi, I'm trying to figure out how to encode GraphQL aliases (https://graphql.github.io/learn/queries/#aliases) using this library.

I'm trying to encode something like this query:

{
  repository(owner: "davidoram", name: "gittest") {
    nameWithOwner
    tags: refs(refPrefix: "refs/tags/", last: 30, orderBy: {field: TAG_COMMIT_DATE, direction: DESC}) {
      edges {
        tag: node {
          name
          target {
            sha: oid
          }
        }
      }
    }
  }
}

.. in particular I haven't figured out how to encode the tags: refs alias as a go struct with the appropriate graphql struct tag?

Parse 502 status code response as a GraphQL response.

As reported by @cjwagner in shurcooL/graphql#29 (comment) and recently reproduced by me, GitHub GraphQL API v4 can return a response with status code 502 that includes a valid GraphQL response:

{
    "data":"null",
    "errors":[
        {
            "message":"Something went wrong while executing your query. This may be the result of a timeout, or it could be a GitHub bug. Please include `8FF2:721B:31D84D5:6EF58BD:5BCF3079` when reporting this issue."
        }
    ]
}

(I'm a little surprised and concerned that data field is a string "null" rather than a JSON null value. It's still valid JSON, but... According to GraphQL spec, "If an error was encountered during the execution that prevented a valid response, the data entry in the response should be null.")

We can parse it as such, instead of returning a generic "non-200 OK status code" error:

image

This may depend on shurcooL/graphql#5 being resolved, or maybe there's a shorter path to getting this done.

Is there a way to use a null type in the after graphql tag?

I am trying to fetch all the PRs in a repo, I start with this struct to make my first request

type firstBatchRequest struct {
		Repository struct {
			PullRequests struct {
				Nodes []struct {
					Author  githubV4Actor
					Participants struct {
						Nodes []struct {
							Login githubv4.String
						}
					} `graphql:"participants(first:$nodes)"`
				}
				PageInfo struct {
					EndCursor   githubv4.String
					HasNextPage githubv4.Boolean
				}
			} `graphql:"pullRequests(baseRefName:$branch,first:$prNumber)"`
		} `graphql:"repository(owner:$repositoryOwner,name:$repositoryName)"`
	}

with these options

opts := map[string]interface{}{
			"repositoryOwner": githubv4.String("someone"),
			"repositoryName":  githubv4.String("something"),
			"prNumber":        githubv4.Int(100),
			"branch":          githubv4.String("master"),
			"nodes":           githubv4.Int(100),
		}

and once I make the first request I make subsequent requests using the endCursor obtained from the first one to get the PRs after that like this

type subsequentBatchRequest struct {
		Repository struct {
			PullRequests struct {
				Nodes []struct {
					Number  githubv4.Int
					Author  githubV4Actor
					Participants struct {
						Nodes []struct {
							Login githubv4.String
						}
					} `graphql:"participants(first:$nodes)"`
				}
				PageInfo struct {
					EndCursor   githubv4.String
					HasNextPage githubv4.Boolean
				}
			} `graphql:"pullRequests(baseRefName:$branch,first:$prNumber,after:$endCursor)"`
		} `graphql:"repository(owner:$repositoryOwner,name:$repositoryName)"`
	}

opts := map[string]interface{}{
				"repositoryOwner": githubv4.String("someone"),
				"repositoryName":  githubv4.String("something"),
				"prNumber":        githubv4.Int(100),
				"branch":          githubv4.String("master"),
				"endCursor":       githubv4.String(cursor),
				"nodes":           githubv4.Int(100),
			}

The only difference between these two is that one uses an endcursor and one doesn't, if there was a way to pass a null for the after tag I would be able to reduce the two structs to just one and reuse it, is there anyway to do that?

Provide tag to ignore field

Other golang serializers tend to use "-" as an ignore tag. Eg, json : json:"-"

I've found an ugly workaround for githubql:

var query struct {
  ...
  IgnoreThisField int `graphql:"# ignore\n"`
}

Question: How to add a querystring as a variable?

Hi,

Hopefully this is simple miss on my part.

	var query struct {
		Search struct {
			RepositoryCount int
			PageInfo        struct {
				EndCursor   githubv4.String
				HasNextPage bool
			}
			Repos []struct {
				Repository respository `graphql:"... on Repository"`
			} `graphql:"nodes"`
		} `graphql:"search(first: 100, after: $repocursor, type: REPOSITORY, query: $querystring)"`
		RateLimit struct {
			Cost      githubv4.Int
			Limit     githubv4.Int
			Remaining githubv4.Int
			ResetAt   githubv4.DateTime
		}
	}

	variables := map[string]interface{}{
		"repocursor":  (*githubv4.String)(nil),
		"querystring": githubv4.String(`\"archived: false pushed:>2020-04-01 created:2020-01-01..2020-02-01\"`),
	}

What is the correct format/method to substitute in a querystring?

Thanks in advance

Provide errors response for error handling

GitHub GraphQL API returns errors response on error cases such as not found, for example:

{
  "data": {
    "repository": null
  },
  "errors": [
    {
      "type": "NOT_FOUND",
      "path": [
        "repository"
      ],
      "locations": [
        {
          "line": 2,
          "column": 3
        }
      ],
      "message": "Could not resolve to a Repository with the name 'non-existent-repository'."
    }
  ]
}

Currently Query() does not provide any errors but it should do for error handling, for example:

err := client.Query(ctx, &q, vars)
if errors, ok := githubv4.Errors(err); ok {
  if len(errors) > 0 && errors[0].Type() == "NOT_FOUND" {
    // not found case
  }
}

Thank you for the great work!

Query list of template repos owned by team?

Hello,

I'm unable to find documentation as to how I could nest this query into a struct, does anyone have any ideas how I could go about doing this?

organization(login: "myorg") {
    name
    team(slug: "myteam") {
      repositories(orderBy: {field: NAME, direction: ASC}, first: 100) {
        edges {
          node {
            name
            sshUrl
            isTemplate
          }
        }
      }
    }
  }
}

Variable input of type MergePullRequestInput! was provided invalid value

var m struct {
	MergePullRequest struct {
		PullRequest struct {
			ID githubv4.ID
		}
	} `graphql:"mergePullRequest(input:$input)"`
}
squash := githubv4.PullRequestMergeMethodSquash
input := githubv4.MergePullRequestInput{
	PullRequestID: githubv4.ID(prID),
	MergeMethod:   &squash, // <<<<<<<<<<<<<<<
}

...

err := c.client.Mutate(ctx, &m, input, nil)
fmt.Println(err)
Variable input of type MergePullRequestInput! was provided invalid value

MergeMethod is a *PullRequestMergeMethod. The values for PullRequestMergeMethod are all constants. I passed githubv4.PullRequestMergeMethodSquash the only way I know how, by copying the constant to a variable and then passing a pointer to the variable, but I got the error you see above. Am I doing something wrong?

I double-checked the GitHub settings for this repo and they do allow squash merging.

Question: Would a single pagination cursor solve GitHub v4 API's woes?

While playing with the GitHub v4 GraphQL API, we quickly ran up against what appears to be a significant limitation: multiple pagination cursors can not be followed in a single query.

At this point, it is not clear to me if this is a fundamental GraphQL API issue or an issue with the GitHub implementation of the GraphQL API, so I would like to open a discussion here where people with more experience can help clear up any inaccuracies and ideally help propose a solution to GitHub.

Let's paint a hypothetical picture for discussion (but first note that the GitHub v4 GraphQL API limits each entity response to 100 items).

Let's say a large company has ~200 orgs each with an average of ~250 repositories and each of those repos has ~300 contributors (and each contributor has "owner", "write" or "read" privileges).

Let's say I would like to build up a githubql query that answers the question:

"Give me all contributors (and their privileges) of all repositories of all organizations in my account."

Obviously, pagination is needed... but the way it is currently implemented, a pagination cursor is provided for each list of contributors, each list of repositories, and each list of organizations. As a result, it is not possible to complete the query by following a single pagination cursor. Furthermore, it is not clear to me that the query can be completed at all due to the ambiguity of specifying a pagination cursor for one list of contributors for one org/repo combo versus the next org/repo combo.

(I will add an example later, but wanted to keep this as small as possible to highlight the issue with the GitHub v4 GraphQL API.)

Ideally, since a GraphQL query is naturally a depth-first search (since the full depth of the query is specified up-front), there should be a single pagination cursor that can return the paginated results in depth-first order. (As it currently stands, each list is expanded breadth-first with pagination cursors provided for each list that contains over 100 items.)

I will work on putting together an example, but in the meantime, please let me know which portions of this need more explanation or if you would prefer that I move this discussion to the GitHub forums instead.

Nodes missing from results

I'm trying to query issues together with their labels and columns. In order to do that, I've created this query structure:

	var query struct {
		Repository struct {
			Issues struct {
				TotalCount int
				PageInfo   struct {
					StartCursor githubv4.String
					EndCursor   githubv4.String
					HasNextPage bool
				}
				Nodes []struct {
					CreatedAt    githubv4.DateTime
					ClosedAt     githubv4.DateTime
					Title        githubv4.String
					Url          githubv4.URI
					ProjectCards struct {
						Nodes []struct {
							id     githubv4.ID
							Column struct {
								name githubv4.String
							} `graphql:"column"`
						} `graphql:"nodes"`
					} `graphql:"projectCards"`
					Labels struct {
						Nodes []struct {
							Name githubv4.String
						}
					} `graphql:"labels(first: 100)"`
				}
			} `graphql:"issues(first: 100)"`
		} `graphql:"repository(owner: \"brejoc\", name: \"test\")"`
	}

This repository should return four issues, but githubv4 only returns two of them. While digging through the code I noticed the do() function and printed the query from in.Query. The generated query (when executed in GraphiQL) actually returns the expected four issues. So something seems to happen afterwards.

Here is the query that gets generated:

{repository(owner: "brejoc", name: "test"){issues(first: 100){totalCount,pageInfo{startCursor,endCursor,hasNextPage},nodes{createdAt,closedAt,title,url,projectCards{nodes{id,column{name}}},labels(first: 100){nodes{name}}}}}}

Any hints on where to continue to debug this are very welcome! Thx!

null cursor support, not "null"

Thanks for this work.

I want to get the first connection page with the after cursor is null, and then get rest of connection pages by the cursor in previous page info.

I don't know how to set the argument after as null. The type of cursor in graphql is string. I can set string with value "null", but not null.

type query struct {
	Repository struct {
		Name        githubql.String
		Description githubql.String
		CreatedAt   githubql.DateTime
		Issues struct {
			TotalCount githubql.Int
			Nodes    []Issue
			PageInfo PageInfo
		} `graphql:"issues(first: 100, after: $after)"`
	} `graphql:"repository(owner: $owner, name: $name)"`
}
variables := map[string]interface{}{
	"owner":  githubql.String("someone"),
	"name":   githubql.String("myreponame"),
	"labels": []githubql.String{"alabel"},
	"after":  githubql.String("null"),
}

help wanted.

GitHub App Installation OAuth Token Errors

Currently I have an application that leverages both the Github v3 API (using https://github.com/google/go-github) and the v4 API for a number of Pull Request checks. Until recently I was using a personal access token in both libraries for authorization, but I recently switched to a GitHub App.

Here's a snippet of how I instantiate the clients:

// NewClient will generate a configured GitHub client for use.
func NewClient(ctx context.Context) *github.Client {
	return github.NewClient(newGitHubAuth(&ctx))
}

// NewGraphClient will generate a configured GitHub client for use against the GraphQL API.
func NewGraphClient(ctx context.Context) *githubv4.Client {
	return githubv4.NewClient(newGitHubAuth(&ctx))
}

func newGitHubAuth(ctx *context.Context) *http.Client {
	// GitHub App installation
	appPem := os.Getenv("GITHUB_APP_PEM")
	if appPem != "" {
		log.Debug("Pulling credentials for GitHub App")
		itr, err := ghinstallation.NewKeyFromFile(http.DefaultTransport, appID, appInstallationID, appPem)
		if err != nil {
			log.Warning(err)
		}

		return &http.Client{Transport: itr}
	}

	// Interactive run by a user
	token := os.Getenv("GITHUB_TOKEN")
	if token != "" {
		log.Debug("Pulling credentials for GitHub Personal Access Token")
		ts := oauth2.StaticTokenSource(&oauth2.Token{AccessToken: token})
		return oauth2.NewClient(*ctx, ts)
	}

	log.Error("Unable to find GitHub credentials from the GITHUB_APP_PEM or GITHUB_TOKEN environment variables")
	return &http.Client{}
}

Note that this is using the same https://github.com/bradleyfalzon/ghinstallation library for transport-based authentication as the v3 library is using.

Shortly after deploying, the v4 client began throwing a number of could not refresh installation id ... received non 2xx response status errors from the underlying ghinstallation library...but only for the v4 library. The v3 library appears to have continued functioning.

I haven't read anything in this repository around recommended implementations of GitHub Apps. Is using ghinstallation supported? If so, any idea what may be happening?

Do not include unhelpful scalar types.

This is an issue with the current API design I've come to feel by using the API more, by thinking about it more, and through some user feedback (/cc @slimsag).

Right now, githubql defines a custom type for each GitHub GraphQL API scalar:

https://developer.github.com/v4/reference/scalar/

https://github.com/shurcooL/githubql/blob/bd2ca14c68143d6449401ed5c65ddefe7ef9058f/scalar.go#L19-L61

Some of them are very primitive types that the Go type system already offers as built in types:

// Boolean represents true or false values.
type Boolean bool

// Float represents signed double-precision fractional values as
// specified by IEEE 754.
type Float float64

// Int represents non-fractional signed whole numeric values.
// Int can represent values between -(2^31) and 2^31 - 1.
type Int int32

// String represents textual data as UTF-8 character sequences.
// This type is most often used by GraphQL to represent free-form
// human-readable text.
type String string

I created scalar.go completely by hand (it's so small, and unlikely to change often, that auto-generating it from schema wasn't worth it at this time). The thinking was that githubql should expose all types in the GitHub GraphQL schema, and scalars were a small and easy place to start. So I added all of them. Which lets you write code like this:

var query struct {
	Viewer struct {
		Login     githubql.String
		CreatedAt githubql.DateTime
	}
}

Some early user feedback immediately pointed it out as a source of uncertainty/unexpected complexity:

I'm sure I could figure out the reason why from looking at the code, but I wonder if the githubql.String type is really needed? I wonder why string can't be directly used, especially since variables := map[string]interface{} has a value interface{} type meaning forgetting to wrap it in a githubql.String can introduce subtle bugs not caught by the compiler

And the answer is those custom types are absolutely not needed. In fact, I had a note at the top of schalar.go:

// Note: These custom types are meant to be used in queries, but it's not required.
// They're here for convenience and documentation. If you use the base Go types,
// things will still work.
//
// TODO: In Go 1.9, consider using type aliases instead (for extra simplicity
//       and convenience).

After writing more and more code that uses githubql, I've come to realize these types are simply counter-productive. Using them adds no value to the code, only makes it more verbose by forcing you to do type conversions. Not using them feels weird because they exist, and README demonstrates them being used.

Compare, using them:

variables := map[string]interface{}{
	"repositoryOwner": githubql.String(repo.Owner),
	"repositoryName":  githubql.String(repo.Repo),
	"issueNumber":     githubql.Int(id),
	"timelineCursor":  (*githubql.String)(nil),
	"timelineLength":  githubql.Int(100),
}
if opt != nil { // Paginate. Use opt.Start and opt.Length.
	if opt.Start != 0 {
		variables["timelineCursor"] = githubql.NewString(githubql.String(cursor))
	}
	variables["timelineLength"] = githubql.Int(opt.Length)
}

Vs using Go's equivalent built in types:

variables := map[string]interface{}{
	"repositoryOwner": repo.Owner,
	"repositoryName":  repo.Repo,
	"issueNumber":     id,
	"timelineCursor":  (*string)(nil),
	"timelineLength":  100,
}
if opt != nil { // Paginate. Use opt.Start and opt.Length.
	if opt.Start != 0 {
		variables["timelineCursor"] = githubql.NewString(cursor)
	}
	variables["timelineLength"] = opt.Length
}

Additionally, sometimes you query for an int:

Nodes []struct {
	Number githubql.Int

And immediately try to convert it to uint64, for example:

Number: uint64(issue.Number),

But instead, you might as well ask for a uint64 to be decoded into in the query:

Nodes []struct {
	Number uint64
Number: issue.Number,

This should not be more unsafe. As long as a query is executed successfully at least once, it'll always work. Besides, someone could've accidentally used githubql.String where githubql.Int should've been used, the compiler wouldn't catch that either.

The only useful purpose they serve is documentation of GitHub GraphQL scalars, but I think we need to provide such documentation without actually having the unneeded types.

How can i make omit empty in grpahql query struct

I have the following struct in which the Topics in the field tag can be optional so I made it as a pointer. Now while try calling the graphql it is resulting in error

type Topic struct {
	Name githubv4.String
}
type TopicsNode struct {
	Topic Topic
}

type RepositoryTopics struct {
	Nodes []TopicsNode
}

type Repository struct {
	Name        githubv4.String
	Description githubv4.String
	Topics      *RepositoryTopics `graphql:"repositoryTopics(first: 20)"`
}

type Execute struct {
	Repository *Repository `graphql:"repository(owner:$orgName,name: $repositoryName)"`
}

I tried calling the graphql


	repos := Repository{
		Name: "MY_REPO",
	}

	query := Execute{
		Repository: &repos,
	}

	variables := map[string]interface{}{
		"repositoryName": githubv4.String(RepoName),
		"orgName":        githubv4.String(GithubOrg),
	}

	err := GraphQL.Query(context.Background(), &query, variables)

I am getting the topics field always. How can i make it as optional

I tried two ways

Topics      *RepositoryTopics `graphql:"repositoryTopics(first: 20)" json:",omitempty"`
Topics      *RepositoryTopics `graphql:"repositoryTopics(first: 20) omitempty"`

Proposal: Rename package to githubv4.

This is something I've been thinking about for the last few months, and I am increasingly convinced this would be an improvement. If accepted, it's better to get this done sooner, before the library has many more users.

The proposal is:

-// Package githubql is a client library for accessing GitHub
+// Package githubv4 is a client library for accessing GitHub
 // GraphQL API v4 (https://developer.github.com/v4/).
 //
 // If you're looking for a client library for GitHub REST API v3,
 // the recommended package is github.com/google/go-github/github.
 //
 // Status: In active early research and development. The API will change when
 // opportunities for improvement are discovered; it is not yet frozen.
 //
 // For now, see README for more details.
-package githubql // import "github.com/shurcooL/githubql"
+package githubv4 // import "github.com/shurcooL/githubv4"

First, I think there are some issues with the current githubql name (at least in my mind). It sounds cool, like "GitHub Query Language", but that's not very accurate. If anything, it should've been githubgql for "GitHub GraphQL [API]".

It might be just me, but having githubql and graphql, I constantly keep mixing up their names, even though I'm well aware of their differences. I guess it's just that both start with "G" and end with "QL", which makes the names harder to differentiate.

Next, I think that githubv4 is a more practical name for the following reason. Currently, GitHub GraphQL API v4 is far from complete (and seemingly, it will not have feature parity with GitHub REST API v3 for many years). So during this transitional time, it's going to be very common to import both:

import (
	...
	"github.com/google/go-github/github"
	"github.com/shurcooL/githubql"
)

github and githubql don't make for great package names in code that uses both. They're hard to tell apart, have different length names, etc.

So doing this seems favorable:

import (
	...
	githubv3 "github.com/google/go-github/github"
	githubv4 "github.com/shurcooL/githubql"
)

Also, in theory, if GitHub were to release GitHub API v5, and it happened to also use GraphQL, that would be another data point showing that githubv4 is a better name than githubql.

Updating the code and moving/re-fetching the package is a bit annoying for users, but not too difficult or risky.

According to https://godoc.org/github.com/shurcooL/githubql?importers, there are not very many (public) importers at this time, so this seems viable.

I'm happy to hear thoughts or convincing arguments on this, if anyone has any. Thanks.

/cc @willnorris @gmlewis For your awareness and thoughts.

ID type intersects poorly with map[string]interface{}.

When you create a variable of type githubql.ID and place it into a map[string]interface{} as part of variables, the fact that its type was githubql.ID gets "lost" because the type is interface{} which satisfies interfaces{}.

This means queryArguments has a hard time telling the original type name:

var nodeID = githubql.ID("someid")

variables := map[string]interface{}{
    "id": nodeID,
}

Results in query($nodeID: string!) { ... } instead of query($nodeID: ID!) { ... }.

Roadmap.

This issue provides a rough roadmap to give you an idea of the development status, and the remaining planned work.

Roadmap

Currently Implemented

  • Basic and intermediate queries.
  • All mutations.
  • Query minification before network transfer.
  • Scalars.
    • Improved support (#9).
  • Specifying arguments and passing variables.
  • Thorough test coverage.
    • Initial basic tests (hacky but functional).
    • Better organized, medium sized tests.
  • Aliases.
    • Documentation.
    • Improved support.
  • Inline fragments.
    • Documentation.
  • Generate all of objects, enums, input objects, etc.
    • Clean up GitHub documentation to pass golint.
  • Unions.
    • Functional.
    • Improved support (#10).
  • Directives (haven't tested yet, but expect it to be supported).
  • Research and complete, document the rest of GraphQL features.
  • Fully document (and add tests for edge cases) the graphql struct field tag.
  • Extremely clean, beautiful, idiomatic Go code (100% coverage, 0 lines of hacky code).
    • Document all public identifiers.
    • Clean up implementations of some private helpers (currently functional, but hacky).

Future

  • GitHub Enterprise support (when it's available; GitHub themselves haven't released support yet; GitHub has released it now). Using issue #23 to track this feature.
  • Frontend support (e.g., using githubql in frontend Go code via WebAssembly or GopherJS).
  • Local error detection (maybe).
  • Calculating a rate limit score before running the call.
  • Avoiding making network calls when rate limit quota exceeded and not yet reset.
  • Support for OpenTracing.

Known Unknowns

  • Whether or not the current API design will scale to support all of advanced GraphQL specification features, and future changes. So far, things are looking great, no major blockers found. I am constantly evaluating it against alternative API designs that I've considered and prototyped myself, and new ones that I become aware of.
  • I have only explored roughly 80% of the GraphQL specification (Working Draft – October 2016).
  • Performance, allocations, memory usage under heavy workloads in long-running processes.
  • Optimal long-term package/code layout (i.e., whether to split off some of the parts into smaller sub-packages).

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.