README.md
1githubv4
2========
3
4[![Build Status](https://travis-ci.org/shurcooL/githubv4.svg?branch=master)](https://travis-ci.org/shurcooL/githubv4) [![GoDoc](https://godoc.org/github.com/shurcooL/githubv4?status.svg)](https://godoc.org/github.com/shurcooL/githubv4)
5
6Package `githubv4` is a client library for accessing GitHub GraphQL API v4 (https://docs.github.com/en/graphql).
7
8If you're looking for a client library for GitHub REST API v3, the recommended package is [`github.com/google/go-github/github`](https://godoc.org/github.com/google/go-github/github).
9
10Focus
11-----
12
13- Friendly, simple and powerful API.
14- Correctness, high performance and efficiency.
15- Support all of GitHub GraphQL API v4 via code generation from schema.
16
17Installation
18------------
19
20`githubv4` requires Go version 1.8 or later.
21
22```bash
23go get -u github.com/shurcooL/githubv4
24```
25
26Usage
27-----
28
29### Authentication
30
31GitHub GraphQL API v4 [requires authentication](https://docs.github.com/en/graphql/guides/forming-calls-with-graphql#authenticating-with-graphql). 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`](https://golang.org/x/oauth2) package. You'll need an OAuth token from GitHub (for example, a [personal API token](https://help.github.com/articles/creating-a-personal-access-token-for-the-command-line/)) with the right scopes. Then:
32
33```Go
34import "golang.org/x/oauth2"
35
36func main() {
37 src := oauth2.StaticTokenSource(
38 &oauth2.Token{AccessToken: os.Getenv("GITHUB_TOKEN")},
39 )
40 httpClient := oauth2.NewClient(context.Background(), src)
41
42 client := githubv4.NewClient(httpClient)
43 // Use client...
44}
45```
46
47If you are using GitHub Enterprise, use [`githubv4.NewEnterpriseClient`](https://godoc.org/github.com/shurcooL/githubv4#NewEnterpriseClient):
48
49```Go
50client := githubv4.NewEnterpriseClient(os.Getenv("GITHUB_ENDPOINT"), httpClient)
51// Use client...
52```
53
54### Simple Query
55
56To 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.
57
58For example, to make the following GraphQL query:
59
60```GraphQL
61query {
62 viewer {
63 login
64 createdAt
65 }
66}
67```
68
69You can define this variable:
70
71```Go
72var query struct {
73 Viewer struct {
74 Login githubv4.String
75 CreatedAt githubv4.DateTime
76 }
77}
78```
79
80Then call `client.Query`, passing a pointer to it:
81
82```Go
83err := client.Query(context.Background(), &query, nil)
84if err != nil {
85 // Handle error.
86}
87fmt.Println(" Login:", query.Viewer.Login)
88fmt.Println("CreatedAt:", query.Viewer.CreatedAt)
89
90// Output:
91// Login: gopher
92// CreatedAt: 2017-05-26 21:17:14 +0000 UTC
93```
94
95### Scalar Types
96
97For 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`.
98
99You can use these types when writing queries:
100
101```Go
102var query struct {
103 Viewer struct {
104 Login githubv4.String
105 CreatedAt githubv4.DateTime
106 IsBountyHunter githubv4.Boolean
107 BioHTML githubv4.HTML
108 WebsiteURL githubv4.URI
109 }
110}
111// Call client.Query() and use results in query...
112```
113
114However, depending on how you're planning to use the results of your query, it's often more convenient to use other Go types.
115
116The `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`](https://godoc.org/encoding/json#Unmarshaler) interface is respected.
117
118That means you can simplify the earlier query by using predeclared Go types:
119
120```Go
121// import "time"
122
123var query struct {
124 Viewer struct {
125 Login string // E.g., "gopher".
126 CreatedAt time.Time // E.g., time.Date(2017, 5, 26, 21, 17, 14, 0, time.UTC).
127 IsBountyHunter bool // E.g., true.
128 BioHTML string // E.g., `I am learning <a href="https://graphql.org">GraphQL</a>!`.
129 WebsiteURL string // E.g., "https://golang.org".
130 }
131}
132// Call client.Query() and use results in query...
133```
134
135The [`DateTime`](https://docs.github.com/en/graphql/reference/scalars#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:
136
137```Go
138// import "html/template"
139
140type MyBoolean bool
141
142var query struct {
143 Viewer struct {
144 Login string // E.g., "gopher".
145 CreatedAt string // E.g., "2017-05-26T21:17:14Z".
146 IsBountyHunter MyBoolean // E.g., MyBoolean(true).
147 BioHTML template.HTML // E.g., template.HTML(`I am learning <a href="https://graphql.org">GraphQL</a>!`).
148 WebsiteURL template.URL // E.g., template.URL("https://golang.org").
149 }
150}
151// Call client.Query() and use results in query...
152```
153
154### Arguments and Variables
155
156Often, you'll want to specify arguments on some fields. You can use the `graphql` struct field tag for this.
157
158For example, to make the following GraphQL query:
159
160```GraphQL
161{
162 repository(owner: "octocat", name: "Hello-World") {
163 description
164 }
165}
166```
167
168You can define this variable:
169
170```Go
171var q struct {
172 Repository struct {
173 Description string
174 } `graphql:"repository(owner: \"octocat\", name: \"Hello-World\")"`
175}
176```
177
178Then call `client.Query`:
179
180```Go
181err := client.Query(context.Background(), &q, nil)
182if err != nil {
183 // Handle error.
184}
185fmt.Println(q.Repository.Description)
186
187// Output:
188// My first repository on GitHub!
189```
190
191However, 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:
192
193```Go
194// fetchRepoDescription fetches description of repo with owner and name.
195func fetchRepoDescription(ctx context.Context, owner, name string) (string, error) {
196 var q struct {
197 Repository struct {
198 Description string
199 } `graphql:"repository(owner: $owner, name: $name)"`
200 }
201```
202
203When sending variables to GraphQL, you need to use exact types that match GraphQL scalar types, otherwise the GraphQL server will return an error.
204
205So, define a `variables` map with their values that are converted to GraphQL scalar types:
206
207```Go
208 variables := map[string]interface{}{
209 "owner": githubv4.String(owner),
210 "name": githubv4.String(name),
211 }
212```
213
214Finally, call `client.Query` providing `variables`:
215
216```Go
217 err := client.Query(ctx, &q, variables)
218 return q.Repository.Description, err
219}
220```
221
222### Inline Fragments
223
224Some GraphQL queries contain inline fragments. You can use the `graphql` struct field tag to express them.
225
226For example, to make the following GraphQL query:
227
228```GraphQL
229{
230 repositoryOwner(login: "github") {
231 login
232 ... on Organization {
233 description
234 }
235 ... on User {
236 bio
237 }
238 }
239}
240```
241
242You can define this variable:
243
244```Go
245var q struct {
246 RepositoryOwner struct {
247 Login string
248 Organization struct {
249 Description string
250 } `graphql:"... on Organization"`
251 User struct {
252 Bio string
253 } `graphql:"... on User"`
254 } `graphql:"repositoryOwner(login: \"github\")"`
255}
256```
257
258Alternatively, you can define the struct types corresponding to inline fragments, and use them as embedded fields in your query:
259
260```Go
261type (
262 OrganizationFragment struct {
263 Description string
264 }
265 UserFragment struct {
266 Bio string
267 }
268)
269
270var q struct {
271 RepositoryOwner struct {
272 Login string
273 OrganizationFragment `graphql:"... on Organization"`
274 UserFragment `graphql:"... on User"`
275 } `graphql:"repositoryOwner(login: \"github\")"`
276}
277```
278
279Then call `client.Query`:
280
281```Go
282err := client.Query(context.Background(), &q, nil)
283if err != nil {
284 // Handle error.
285}
286fmt.Println(q.RepositoryOwner.Login)
287fmt.Println(q.RepositoryOwner.Description)
288fmt.Println(q.RepositoryOwner.Bio)
289
290// Output:
291// github
292// How people build software.
293//
294```
295
296### Pagination
297
298Imagine 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:
299
300```Go
301type comment struct {
302 Body string
303 Author struct {
304 Login string
305 AvatarURL string `graphql:"avatarUrl(size: 72)"`
306 }
307 ViewerCanReact bool
308}
309var q struct {
310 Repository struct {
311 Issue struct {
312 Comments struct {
313 Nodes []comment
314 PageInfo struct {
315 EndCursor githubv4.String
316 HasNextPage bool
317 }
318 } `graphql:"comments(first: 100, after: $commentsCursor)"` // 100 per page.
319 } `graphql:"issue(number: $issueNumber)"`
320 } `graphql:"repository(owner: $repositoryOwner, name: $repositoryName)"`
321}
322variables := map[string]interface{}{
323 "repositoryOwner": githubv4.String(owner),
324 "repositoryName": githubv4.String(name),
325 "issueNumber": githubv4.Int(issue),
326 "commentsCursor": (*githubv4.String)(nil), // Null after argument to get first page.
327}
328
329// Get comments from all pages.
330var allComments []comment
331for {
332 err := s.clQL.Query(ctx, &q, variables)
333 if err != nil {
334 return err
335 }
336 allComments = append(allComments, q.Repository.Issue.Comments.Nodes...)
337 if !q.Repository.Issue.Comments.PageInfo.HasNextPage {
338 break
339 }
340 variables["commentsCursor"] = githubv4.NewString(q.Repository.Issue.Comments.PageInfo.EndCursor)
341}
342```
343
344There is more than one way to perform pagination. Consider additional fields inside [`PageInfo`](https://docs.github.com/en/graphql/reference/objects#pageinfo) object.
345
346### Mutations
347
348Mutations often require information that you can only find out by performing a query first. Let's suppose you've already done that.
349
350For example, to make the following GraphQL mutation:
351
352```GraphQL
353mutation($input: AddReactionInput!) {
354 addReaction(input: $input) {
355 reaction {
356 content
357 }
358 subject {
359 id
360 }
361 }
362}
363variables {
364 "input": {
365 "subjectId": "MDU6SXNzdWUyMTc5NTQ0OTc=",
366 "content": "HOORAY"
367 }
368}
369```
370
371You can define:
372
373```Go
374var m struct {
375 AddReaction struct {
376 Reaction struct {
377 Content githubv4.ReactionContent
378 }
379 Subject struct {
380 ID githubv4.ID
381 }
382 } `graphql:"addReaction(input: $input)"`
383}
384input := githubv4.AddReactionInput{
385 SubjectID: targetIssue.ID, // ID of the target issue from a previous query.
386 Content: githubv4.ReactionContentHooray,
387}
388```
389
390Then call `client.Mutate`:
391
392```Go
393err := client.Mutate(context.Background(), &m, input, nil)
394if err != nil {
395 // Handle error.
396}
397fmt.Printf("Added a %v reaction to subject with ID %#v!\n", m.AddReaction.Reaction.Content, m.AddReaction.Subject.ID)
398
399// Output:
400// Added a HOORAY reaction to subject with ID "MDU6SXNzdWUyMTc5NTQ0OTc="!
401```
402
403Directories
404-----------
405
406| Path | Synopsis |
407|-------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------|
408| [example/githubv4dev](https://godoc.org/github.com/shurcooL/githubv4/example/githubv4dev) | githubv4dev is a test program currently being used for developing githubv4 package. |
409
410License
411-------
412
413- [MIT License](LICENSE)
414