From 178c1d33765e06889107408180a9d0e8717e11a0 Mon Sep 17 00:00:00 2001 From: Ben Kraft Date: Mon, 19 Feb 2024 12:17:28 -0800 Subject: [PATCH 1/2] Reorganize and improve documentation We've been in need of a documentation revamp for a while; a giant FAQ was never the greatest structure and got worse as it grew. In this commit I reorganize the documentation. Most of it is just moving around existing text, but I added some new documentation here and there. The changes: - Many of the FAQ questions have moved to several new docs, on the client/runtime, handling your schema, and various operation types; the FAQ has answers to a few of the actually most frequent questions, as well as a few things that didn't fit elsewhere. - We now have a `docs/README.md` which acts as an index, so we can just link to `/docs`. - I lowercased the files that don't need match a GitHub convention, so it's a bit less yell-y. - I added documentation on: - how we version genqlient (fixes #63) - newer options for optional types - a bit more on custom scalars and integer types (fixes #128) --- README.md | 4 +- docs/CHANGELOG.md | 6 +- docs/CONTRIBUTING.md | 8 +- docs/FAQ.md | 564 ------------------ docs/README.md | 34 ++ docs/client_config.md | 157 +++++ docs/{DESIGN.md => design.md} | 0 docs/faq.md | 121 ++++ docs/genqlient_directive.graphql | 2 +- docs/{INTRODUCTION.md => introduction.md} | 6 +- docs/operations.md | 408 +++++++++++++ docs/schema.md | 130 ++++ docs/versioning.md | 30 + generate/convert.go | 4 +- generate/names.go | 4 +- .../queries/ComplexInlineFragments.graphql | 2 +- ....graphql-ComplexInlineFragments.graphql.go | 3 +- 17 files changed, 898 insertions(+), 585 deletions(-) delete mode 100644 docs/FAQ.md create mode 100644 docs/README.md create mode 100644 docs/client_config.md rename docs/{DESIGN.md => design.md} (100%) create mode 100644 docs/faq.md rename docs/{INTRODUCTION.md => introduction.md} (85%) create mode 100644 docs/operations.md create mode 100644 docs/schema.md create mode 100644 docs/versioning.md diff --git a/README.md b/README.md index e1e9fa34..d26d0c50 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,7 @@ genqlient provides: ## How do I use genqlient? -You can download and run genqlient the usual way: `go run github.com/Khan/genqlient`. To set your project up to use genqlient, see the [getting started guide](docs/INTRODUCTION.md), or the [example](example). For more complete documentation, see the [docs](docs). +You can download and run genqlient the usual way: `go run github.com/Khan/genqlient`. To set your project up to use genqlient, see the [getting started guide](docs/introduction.md), or the [example](example). For more complete documentation, see the [docs](docs). ## How can I help? @@ -47,6 +47,6 @@ This code works, but it has a few problems: - The GraphQL variables aren't type-safe at all; you could have passed `{"id": true}` and again you won't know until runtime! - You have to write everything twice, or hide the query in complicated struct tags, or give up what type safety you do have and resort to `interface{}`. -These problems aren't a big deal in a small application, but for serious production-grade tools they're not ideal. And they should be entirely avoidable: GraphQL and Go are both typed languages; and GraphQL servers expose their schema in a standard, machine-readable format. We should be able to simply write a query and have that automatically validated against the schema and turned into a Go struct which we can use in our code. In fact, there's already good prior art to do this sort of thing: [99designs/gqlgen](https://github.com/99designs/gqlgen) is a popular server library that generates types, and Apollo has a [codegen tool](https://www.apollographql.com/docs/devtools/cli/#supported-commands) to generate similar client-types for several other languages. (See [docs/DESIGN.md](docs/DESIGN.md) for more prior art.) +These problems aren't a big deal in a small application, but for serious production-grade tools they're not ideal. And they should be entirely avoidable: GraphQL and Go are both typed languages; and GraphQL servers expose their schema in a standard, machine-readable format. We should be able to simply write a query and have that automatically validated against the schema and turned into a Go struct which we can use in our code. In fact, there's already good prior art to do this sort of thing: [99designs/gqlgen](https://github.com/99designs/gqlgen) is a popular server library that generates types, and Apollo has a [codegen tool](https://www.apollographql.com/docs/devtools/cli/#supported-commands) to generate similar client-types for several other languages. (See the [design note](docs/design.md) for more prior art.) genqlient fills that gap: you just specify the query, and it generates type-safe helpers, validated against the schema, that make the query. diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 1405bef1..1958aa8f 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -109,9 +109,9 @@ Version 0.3.0 adds several new configuration options, allowing simplification of ### New features: -- genqlient's types are now safe to JSON-marshal, which can be useful for putting them in a cache, for example. See the [docs](FAQ.md#-let-me-json-marshal-my-response-objects) for details. -- The new `flatten` option in the `# @genqlient` directive allows for a simpler form of type-sharing using fragment spreads. See the [docs](FAQ.md#-shared-types-between-different-parts-of-the-query) for details. -- The new `for` option in the `# @genqlient` directive allows applying options to a particular field anywhere it appears in the query. This is especially useful for fields of input types, for which there is otherwise no way to specify options; see the [documentation on handling nullable fields](FAQ.md#-nullable-fields) for an example, and the [`# @genqlient` directive reference](genqlient_directive.graphql) for the full details. +- genqlient's types are now safe to JSON-marshal, which can be useful for putting them in a cache, for example. See the [docs](client_config.md#marshaling) for details. +- The new `flatten` option in the `# @genqlient` directive allows for a simpler form of type-sharing using fragment spreads. See the [docs](operations.md#sharing-types) for details. +- The new `for` option in the `# @genqlient` directive allows applying options to a particular field anywhere it appears in the query. This is especially useful for fields of input types, for which there is otherwise no way to specify options; see the [documentation on handling nullable fields](operations.md#nullable-fields) for an example, and the [`# @genqlient` directive reference](genqlient_directive.graphql) for the full details. ### Bug fixes: diff --git a/docs/CONTRIBUTING.md b/docs/CONTRIBUTING.md index c4a845d6..0094a415 100644 --- a/docs/CONTRIBUTING.md +++ b/docs/CONTRIBUTING.md @@ -22,9 +22,9 @@ Pull requests should have: - documentation, for new features - changelog entries -Pull requests will be squash-merged, so subsequent commit messages may be brief (e.g. "review comments"). +The PR description template will remind you of these. Pull requests will be squash-merged, so subsequent commit messages may be brief (e.g. "review comments"). -Large changes should typically be discussed on the issue tracker first, and should ideally be broken up into separate PRs, or failing that, several commits, for ease of reviewing. +Large changes should typically be discussed on the issue tracker first, and should ideally be broken up into separate PRs, or failing that, several commits, for ease of reviewing. This is especially true of breaking changes; see the [versioning policy](versioning.md) for what we consider breaking. ## Style @@ -44,11 +44,11 @@ If you update any code-generation logic or templates, even if no new tests are n ## Finding your way around -If you're new to genqlient, start out by reading the source of `generate.Generate`, whose comments describe most of the high-level operation of genqlient. In general, the code is documented inline, often with an introductory comment at the top of the file. See [DESIGN.md](DESIGN.md) for documentation of major design decisions, which is a good way to get a sense of why genqlient is structured the way it is. +If you're new to genqlient, start out by reading the source of `generate.Generate`, whose comments describe most of the high-level operation of genqlient. In general, the code is documented inline, often with an introductory comment at the top of the file. See the [design note](design.md) for documentation of major design decisions, which is a good way to get a sense of why genqlient is structured the way it is. ## Making a release -We try to cut releases periodically. To make a release: +See the [versioning strategy](versioning.md) for when to make a release. To make a release: - Scan PRs since the last release to check we didn't miss anything in the changelog. - Check if there are any regressions or major problems with new features we want to fix before cutting the release. diff --git a/docs/FAQ.md b/docs/FAQ.md deleted file mode 100644 index 35b084c1..00000000 --- a/docs/FAQ.md +++ /dev/null @@ -1,564 +0,0 @@ -# Frequently Asked Questions - -This document describes common questions about genqlient, and provides an index to how to represent common query structures. For a full list of configuration options, see [genqlient.yaml](genqlient.yaml) and [genqlient_directive.graphql](genqlient_directive.graphql). - -## How do I set up genqlient to … - -### … get started? - -There's a [doc for that](INTRODUCTION.md)! - -### … use GET requests instead of POST requests? - -You can use `graphql.NewClientUsingGet` to create a client that will use query parameters to create the request. For example: -```go -ctx := context.Background() -client := graphql.NewClientUsingGet("https://api.github.com/graphql", http.DefaultClient) -resp, err := getUser(ctx, client, "benjaminjkraft") -fmt.Println(resp.User.Name, err) -``` - -This request will be sent via an HTTP GET request, with the query, operation name and variables encoded in the URL. - -For example, if the query is defined as: - -```graphql -query getUser($login: String!) { - user(login: $login) { - name - } -} -``` - -The URL requested will be: - -`https://api.github.com/graphql?operationName%3DgetUser%26query%3D%0Aquery%20getUser(%24login%3A%20String!)%20%7B%0A%20%20user(login%3A%20%24login)%20%7B%0A%20%20%20%20name%0A%20%20%7D%0A%7D%0A%26variables%3D%7B%22login%22%3A%22benjaminjkraft%22%7D` - -The client does not support mutations, and will return an error if passed a request that attempts one. - -### … use an API that requires authentication? - -When you call `graphql.NewClient`, pass in an HTTP client that adds whatever authentication headers you need (typically by wrapping the client's `Transport`). For example: - -```go -type authedTransport struct { - wrapped http.RoundTripper -} - -func (t *authedTransport) RoundTrip(req *http.Request) (*http.Response, error) { - key := ... - req.Header.Set("Authorization", "bearer "+key) - return t.wrapped.RoundTrip(req) -} - -func MakeQuery(...) { - client := graphql.NewClient("https://api.github.com/graphql", - &http.Client{Transport: &authedTransport{wrapped: http.DefaultTransport}}) - - resp, err := MyQuery(ctx, client, ...) -} -``` - -For more on wrapping HTTP clients, see [this post](https://dev.to/stevenacoffman/tripperwares-http-client-middleware-chaining-roundtrippers-3o00). - -### … make requests against a mock server, for tests? - -Testing code that uses genqlient typically involves passing in a special HTTP client that does what you want, similar to authentication. For example, you might write a client whose `RoundTrip` returns a fixed response, constructed with [`httptest`](https://pkg.go.dev/net/http/httptest). Or, you can use `httptest` to start up a temporary server, and point genqlient at that. Many third-party packages provide support for this sort of thing; genqlient should work with any HTTP-level mocking that can expose a regular `http.Client`. - -### … test my GraphQL APIs? - -If you want, you can use genqlient to test your GraphQL APIs; as with mocking you can point genqlient at anything that exposes an ordinary HTTP endpoint or a custom `http.Client`. However, at Khan Academy we've found that genqlient usually isn't the best client for testing; we prefer to use a lightweight (and weakly-typed) client for that, and may separately open-source ours in the future. - -### … handle GraphQL errors? - -Each genqlient-generated helper function returns two results, a pointer to a response-struct, and an error. The response-struct will always be initialized (never nil), even on error. If the request returns a valid GraphQL response containing errors, the returned error will be [`As`-able](https://pkg.go.dev/errors#As) as [`gqlerror.List`](https://pkg.go.dev/github.com/vektah/gqlparser/v2/gqlerror#List), and the struct may be partly-populated (if one field failed but another was computed successfully). If the request fails entirely, the error will be another error (e.g. a [`*url.Error`](https://pkg.go.dev/net/url#Error)), and the response will be blank (but still non-nil). - -For example, you might do one of the following: -```go -// return both error and field: -resp, err := GetUser(...) -return resp.User.Name, err - -// handle different errors differently: -resp, err := GetUser(...) -var errList *gqlerror.List -if errors.As(err, &errList) { - for _, err := range errList { - fmt.Printf("%v at %v\n", err.Message, err.Path) - } - fmt.Printf("partial response: %v\n", resp) -} else if err != nil { - fmt.Printf("http/network error: %v\n", err) -} else { - fmt.Printf("successful response: %v\n", resp) -} -``` - -### … use custom scalars? - -Just tell genqlient via the `bindings` option in `genqlient.yaml`: - -```yaml -bindings: - DateTime: - type: time.Time -``` - -Make sure the given type has whatever logic is needed to convert to/from JSON (e.g. `MarshalJSON`/`UnmarshalJSON` or JSON tags). See the [`genqlient.yaml` documentation](genqlient.yaml) for the full syntax. - -### … require 32-bit integers? - -The GraphQL spec officially defines the `Int` type to be a [signed 32-bit integer](https://spec.graphql.org/draft/#sec-Int). GraphQL clients and servers vary wildly in their enforcement of this; for example: -- [Apollo Server](https://github.com/apollographql/apollo-server/) explicitly checks that integers are at most 32 bits -- [gqlgen](https://github.com/99designs/gqlgen) by default allows any integer that fits in `int` (i.e. 64 bits on most platforms) -- [Apollo Client](https://github.com/apollographql/apollo-client) doesn't check (but implicitly is limited to 53 bits by JavaScript) -- [shurcooL/graphql](https://github.com/shurcooL/graphql) requires integers be passed as a `graphql.Int`, defined to be an `int32` - -By default, genqlient maps GraphQL `Int`s to Go's `int`, meaning that on 64 bit systems there's no client-side restriction. If you prefer to limit integers to `int32`, you can set a binding in your `genqlient.yaml`: - -```yaml -bindings: - Int: - type: int32 -``` - -Or, you can bind it to any other type, perhaps one with size-checked constructors; see the [`genqlient.yaml` documentation](genqlient.yaml) for more details. - -### … let me json-marshal my response objects? - -This is supported by default! All genqlient-generated types support both JSON-marshaling and unmarshaling, which can be useful for putting them in a cache, inspecting them by hand, using them in mocks (although this is [not recommended](#-test-my-graphql-apis)), or anything else you can do with JSON. It's not guaranteed that marshaling a genqlient type will produce the exact GraphQL input -- we try to get as close as we can but there are some limitations around Go zero values -- but unmarshaling again should produce the value genqlient returned. That is: - -```go -resp, err := MyQuery(...) -// not guaranteed to match what the server sent (but close): -b, err := json.Marshal(resp) -// guaranteed to match resp: -var respAgain MyQueryResponse -err := json.Unmarshal(b, &resp) -``` - -### … let me use introspection to fetch my client schema? - -This is currently not supported by default. You can however use a tool such as [gqlfetch](https://github.com/suessflorian/gqlfetch) to build your client schema using introspection and then let `genqlient` continue from there. Moreover, you can define yourself what happens when `go:generate` is run via managing your own _go runnable_ progam. - -For example - suppose the file `generate/main.go`; - -```go -package main - -import ( - "context" - "fmt" - "os" - - "github.com/Khan/genqlient/generate" - "github.com/suessflorian/gqlfetch" -) - -func main() { - schema, err := gqlfetch.BuildClientSchema(context.Background(), "http://localhost:8080/query") - if err != nil { - fmt.Println(err) - os.Exit(1) - } - - if err = os.WriteFile("schema.graphql", []byte(schema), 0644); err != nil { - fmt.Println(err) - os.Exit(1) - } - - generate.Main() -} -``` - -This can now be invoked upon `go generate` via `//go:generate yourpkg/generate`. - -## How do I make a query with … - -### … a specific name for a field? - -genqlient supports GraphQL field-aliases, and uses them to determine the Go struct field name. For example, if you do -```graphql -query MyQuery { - myGreatName: myString -} -``` -and genqlient will generate a Go field `MyGreatName string`. Note that the alias will always be uppercased, to ensure the field is visible to the Go JSON library. - -### … nullable fields? - -There are two ways to handle nullable fields in genqlient. One way is to use the Go idiom, where null gets mapped to the zero value; this is the default in genqlient. So if you have a GraphQL field of type `String`, and you do: - -```graphql -query MyQuery(arg: String) { - myString -} -``` - -then genqlient will generate a Go field `MyString string`, and set it to the empty string if the server returns null. This works even for structs: if an object type in GraphQL is null, genqlient will set the corresponding struct to its zero value. It can be helpful to request `id` in such cases, since that’s a field that should always be set, or `__typename` which is guaranteed to be set, so you can use its presence to decide whether to look at the other fields. - -For input fields, you often want to tell genqlient to send null to the server if the argument is set to the zero value, similar to the JSON `omitempty` tag. In this case, you can do: - -```graphql -query MyQuery( - # @genqlient(omitempty: true) - arg: String, -) { - myString -} -``` - -You can also put the `# @genqlient(omitempty: true)` on the first line, which will apply it to all arguments in the query, or `# @genqlient(for: "MyInput.myField", omitempty: true)` on the first line to apply it to a particular field of a particular input type used by the query (for which there would otherwise be no place to put the directive, as the field never appears explicitly in the query, but only in the schema). - -If you need to distinguish null from the empty string (or generally from the Go zero value of your type), you can tell genqlient to use a pointer for the field or argument like this: -```graphql -query MyQuery( - # @genqlient(pointer: true) - arg: String, -) { - # @genqlient(pointer: true) - myString -} -``` - -This will generate a Go field `MyString *string`, and set it to `nil` if the server returns null (and in reverse for arguments). Such fields can be harder to work with in Go, but allow a clear distinction between null and the Go zero value. Again, you can put the directive on the first line to apply it to everything in the query, although this usually gets cumbersome, or use `for` to apply it to a specific input-type field. - -As an example of using all these options together: -```graphql -# @genqlient(omitempty: true) -# @genqlient(for: "MyInputType.id", omitempty: false, pointer: true) -# @genqlient(for: "MyInputType.name", omitempty: false, pointer: true) -query MyQuery( - arg1: MyInputType!, - # @genqlient(pointer: true) - arg2: String!, - # @genqlient(omitempty: false) - arg3: String!, -) { - myString(arg1: $arg1, arg2: $arg2, arg3: $arg3) -} -``` -This will generate: -```go -func MyQuery( - ctx context.Context, - client graphql.Client, - arg1 MyInputType, - arg2 *string, // omitempty - arg3 string, -) (*MyQueryResponse, error) - -type MyInputType struct { - Id *string `json:"id"` - Name *string `json:"name"` - Title string `json:"title,omitempty"` - Age int `json:"age,omitempty"` -} -``` - -See [genqlient_directive.graphql](genqlient_directive.graphql) for complete documentation on these options. - -### … GraphQL interfaces? - -If you request an interface field, genqlient generates an interface type corresponding to the GraphQL interface, and several struct types corresponding to its implementations. For example, given a query: - -```graphql -query GetBooks { - favorite { - title - ... on Novel { - protagonist - } - ... on Dictionary { - language - } - } -} -``` - -genqlient will generate the following types (see [below](#-genqlient-generate-such-complicated-type-names) for more on the names): - -```go -type GetBooksFavoriteBook interface { - GetTitle() string -} -type GetBooksFavoriteNovel struct { - Title string - Protagonist string -} -type GetBooksFavoriteDictionary struct { - Title string - Language string -} -// (similarly for any other types that implement Book) -``` - -These can be used in the ordinary Go ways: to access shared fields, use the interface methods; to access type-specific fields, use a type switch: - -```go -resp, err := GetBooks(...) -fmt.Println("Favorite book:", resp.Favorite.GetTitle()) -if novel, ok := resp.Favorite.(*GetBooksFavoriteNovel); ok { - fmt.Println("Protagonist:", novel.Protagonist) -} -``` - -The interface-type's GoDoc will include a list of its implementations, for your convenience. - -If you only want to request shared fields of the interface (i.e. no fragments), this may seem like a lot of ceremony. If you prefer, you can instead add `# @genqlient(struct: true)` to the field, and genqlient will just generate a struct, like it does for GraphQL object types. For example, given: - -```graphql -query GetBooks { - # @genqlient(struct: true) - favorite { - title - } -} -``` - -genqlient will generate just: - -```go -type GetBooksFavoriteBook struct { - Title string -} -``` - -Keep in mind that if you later want to add fragments to your selection, you won't be able to use `struct` anymore; when you remove it you may need to update your code to replace `.Title` with `.GetTitle()` and so on. - - -### … shared types between different parts of the query? - -Suppose you have a query which requests several different fields each of the same GraphQL type, e.g. `User` (or `[User]`): - -```graphql -query GetMonopolyPlayers { - game { - winner { id name } - banker { id name } - spectators { id name } - } -} -``` - -This will produce a Go type like: -```go -type GetMonopolyPlayersGame struct { - Winner GetMonopolyPlayersGameWinnerUser - Banker GetMonopolyPlayersGameBankerUser - Spectators []GetMonopolyPlayersGameSpectatorsUser -} - -type GetMonopolyPlayersGameWinnerUser struct { - Id string - Name string -} - -// (others similarly) -``` - -But maybe you wanted to be able to pass all those users to a shared function (defined in your code), say `FormatUser(user ???) string`. That's no good; you need to put three different types as the `???`. genqlient has several ways to deal with this. - -**Fragments:** One option -- the GraphQL Way, perhaps -- is to use fragments. You'd write your query like: - -```graphql -fragment MonopolyUser on User { - id - name -} - -query GetMonopolyPlayers { - game { - winner { ...MonopolyUser } - banker { ...MonopolyUser } - spectators { ...MonopolyUser } - } -} -``` - -genqlient will notice this, and generate a type corresponding to the fragment; `GetMonopolyPlayersGame` will look as before, but each of the field types will have a shared embed: - -```go -type MonopolyUser struct { - Id string - Name string -} - -type GetMonopolyPlayersGameWinnerUser struct { - MonopolyUser -} - -// (others similarly) -``` - -Thus you can have `FormatUser` accept a `MonopolyUser`, and pass it `game.Winner.MonopolyUser`, `game.Spectators[i].MonopolyUser`, etc. This is convenient if you may later want to add other fields to some of the queries, because you can still do - -```graphql -fragment MonopolyUser on User { - id - name -} - -query GetMonopolyPlayers { - game { - winner { - winCount - ...MonopolyUser - } - banker { - bankerRating - ...MonopolyUser - } - spectators { ...MonopolyUser } - } -} -``` - -and you can even spread the fragment into interface types. It also avoids having to list the fields several times. - -**Fragments, flattened:** The Go field for `winner`, in the first query above, has type `GetMonopolyPlayersGameWinnerUser` which just wraps `MonopolyUser`. If we don't want to add any other fields, that's unnecessary! Instead, we could do -``` -query GetMonopolyPlayers { - game { - # @genqlient(flatten: true) - winner { - ...MonopolyUser - } - # (etc.) - } -} -``` -and genqlient will skip the indirection and give the field `Winner` type `MonopolyUser` directly. This is often much more convenient if you put all the fields in the fragment, like the first query did. See the [options documentation](genqlient_directive.graphql) for more details. - -**Interfaces:** For each struct field it generates, genqlient also generates an interface method. If you want to share code between two types which to GraphQL are unrelated, you can define an interface containing that getter method, and genqlient's struct types will implement it. (Depending on your exact query, you may need to do a type-assertion from a genqlient-generated interface to yours.) For example, in the above query you could simply do: -```go -type MonopolyUser interface { - GetId() string - GetName() string -} - -func FormatUser(user MonopolyUser) { ... } - -FormatUser(resp.Game.Winner) -``` - -In general in such cases it's better to change the GraphQL schema to show how the types are related, and use one of the other mechanisms, but this option is useful for schemas where you can't do that, or in the meantime. - -**Type names:** Finally, if you always want exactly the same fields on exactly the same types, and don't want to deal with interfaces at all, you can use the simpler but more restrictive genqlient option `typename`: - -```graphql -query GetMonopolyPlayers { - game { - # @genqlient(typename: "User") - winner { id name } - # @genqlient(typename: "User") - banker { id name } - # @genqlient(typename: "User") - spectators { id name } - } -} -``` - -This will tell genqlient to use the same types for each field: - -```go -type GetMonopolyPlayersGame struct { - Winner User - Banker User - Spectators []User -} - -type User struct { - Id string - Name string -} -``` - -In this case, genqlient will validate that each type given the name `User` has the exact same fields; see the [full documentation](genqlient_directive.graphql) for details. - -**Bindings:** It's also possible to use the `bindings` option (see [`genqlient.yaml` documentation](genqlient.yaml)) for a similar purpose, but this is not recommended as it typically requires more work for less gain. - -### … documentation on the output types? - -For any GraphQL types or fields with documentation in the GraphQL schema, genqlient automatically includes that documentation in the generated code's GoDoc. To add additional information to genqlient entrypoints, you can put comments in the GraphQL source: - -```graphql -# This query gets the current user. -# -# If you also need to specify options on the query, you can put -# the @genqlient directive after the docuentation, like this: -# -# @genqlient(omitempty: true) -query GetUser { ... } -``` - -## Why does… - -### … genqlient generate such complicated type-names? - -The short answer is that GraphQL forces our hand. For example, consider a query -```graphql -query GetFamilyNames { - user { - name - children { - name - } - } -} -``` -which returns the following JSON: -```graphql -{ - "user": { - "name": "Ellis Marsalis Jr.", - "children": [ - {"name": "Branford Marsalis"}, - {"name": "Delfeayo Marsalis"}, - {"name": "Jason Marsalis"}, - {"name": "Wynton Marsalis"} - ] - } -} -``` -We need two different `User` types to represent this: one with a `Children` field, and one without. (And there may be more in other queries!) Of course, we could name them `User1` and `User2`, but that's both less descriptive and less stable as the query changes (perhaps to add `parent`), so we call them `GetFamilyNamesUser` and `GetFamilyNamesUserChildrenUser`. - -For the long answer, see [DESIGN.md](DESIGN.md#named-vs-unnamed-types). - -If you find yourself needing to reference long generated names, you can always add type aliases for them, e.g.: -```go -type User = GetFamilyNamesUser -type ChildUser = GetFamilyNamesUserChildrenUser -``` - -Alternately, you can use the `typename` option: if you query -```graphql -query GetFamilyNames { - # @genqlient(typename: "User") - user { - name - # @genqlient(typename: "ChildUser") - children { - name - } - } -} -``` -genqlient will instead generate types with the given names. (You'll need to avoid conflicts; see the [full documentation](genqlient_directive.graphql) for details.) - -### … my editor/IDE plugin not know about the code genqlient just generated? - -If your tools are backed by [gopls](https://github.com/golang/tools/blob/master/gopls/README.md) (which is most of them), they simply don't know it was updated. In most cases, keeping the generated file (typically `generated.go`) open in the background, and reloading it after each run of `genqlient`, will do the trick. - -### … genqlient fail after `go mod tidy`? - -If genqlient fails with an error `missing go.sum entry for module providing package`, this is typically because `go mod tidy` removed its dependencies because they weren't imported by your Go module. You can read more about this in golang/go#45552; see in particular [this comment](https://github.com/golang/go/issues/45552#issuecomment-819545037). In short, if you want to be able to `go run` on newer Go you'll need to have a (blank) import of genqlient's entrypoint in a special `tools.go` file somewhere in your module so `go mod tidy` doesn't prune it: - -```go -//go:build tools -// +build tools - -package client - -import _ "github.com/Khan/genqlient" -``` diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 00000000..9d4a7334 --- /dev/null +++ b/docs/README.md @@ -0,0 +1,34 @@ +# genqlient documentation + +generated graphql client ⇒ genqlient + +Welcome to the genqlient documentation! This documentation is made possible by viewers like you; if you see something unclear, file an [issue] or make a [pull request] to improve it! + +[issue]: https://github.com/Khan/genqlient/issues/new/choose +[pull request]: https://github.com/Khan/genqlient/compare + +## Usage/recipes + +- [Getting started guide](introduction.md) +- [Runnable usage example](../example) +- [Handling your GraphQL schema](schema.md) +- [Client configuration and usage](client_config.md) +- [Writing your GraphQL operations](operations.md) + +# Reference + +- [Go package reference](https://pkg.go.dev/github.com/Khan/genqlient) +- [`genqlient.yaml` configuration reference](genqlient.yaml) +- [`@genqlient` directive reference](genqlient_directive.graphql) +- [changelog](CHANGELOG.md) + +## Background + +- [Why genqlient](../README.md#why-another-graphql-client) +- [Notes on the design of genqlient](design.md) +- Blog posts on the [usage][blog1] and [design][blog2] of genqlient +- [Contributing to genqlient](CONTRIBUTING.md) +- [Security policy](SECURITY.md) + +[blog1]: https://blog.khanacademy.org/genqlient-a-truly-type-safe-go-graphql-client/ +[blog2]: https://blog.khanacademy.org/where-go-and-graphql-collide-behind-the-curtain-with-genqlient/ diff --git a/docs/client_config.md b/docs/client_config.md new file mode 100644 index 00000000..b980e53d --- /dev/null +++ b/docs/client_config.md @@ -0,0 +1,157 @@ +# Configuring and using the genqlient client + +This document describes common patterns for using the genqlient client at runtime. For full client reference documentation, see the [godoc]. + +[godoc]: https://pkg.go.dev/github.com/Khan/genqlient/graphql + +## Creating a client + +For most users, just call [`graphql.NewClient`][godoc#NewClient] to get a `graphql.Client`, which you can then pass to genqlient's generated functions. For example, `graphql.NewClient("https://your.api.example/path", http.DefaultClient)` will call an API at the given URL in a fashion compatible with most GraphQL servers. + +For example (see the [getting started docs](INTRODUCTION.md) for the full setup): + +```go +ctx := context.Background() +client := graphql.NewClient("https://api.github.com/graphql", http.DefaultClient) +resp, err := getUser(ctx, client, "benjaminjkraft") +fmt.Println(resp.User.Name, err) +``` + +You can pass the client around however you like to inject dependencies, such as via a global variable, context value, or [fancy typed context][kacontext]. + +[godoc#NewClient]: https://pkg.go.dev/github.com/Khan/genqlient/graphql#NewClient +[kacontext]: https://blog.khanacademy.org/statically-typed-context-in-go/ + +### Authentication and other headers + +To use an API requiring authentication, you can customize the HTTP client passed to [`graphql.NewClient`][godoc#NewClient] to add whatever headers you need. The usual way to do this is to wrap the client's `Transport`: + +```go +type authedTransport struct { + wrapped http.RoundTripper +} + +func (t *authedTransport) RoundTrip(req *http.Request) (*http.Response, error) { + key := ... + req.Header.Set("Authorization", "bearer "+key) + return t.wrapped.RoundTrip(req) +} + +func MakeQuery(...) { + client := graphql.NewClient("https://api.github.com/graphql", + &http.Client{Transport: &authedTransport{wrapped: http.DefaultTransport}}) + + resp, err := MyQuery(ctx, client, ...) +} +``` + +The same method works for passing other HTTP headers, like [`traceparent`](https://www.w3.org/TR/trace-context/). To set a request-dependent header, the `RoundTrip` method has access to the full request, including the context from `req.Context()`. For more on wrapping HTTP clients, see [this post](https://dev.to/stevenacoffman/tripperwares-http-client-middleware-chaining-roundtrippers-3o00). + +### GET requests + +To use GET instead of POST requests, use [`graphql.NewClientUsingGet`][godoc#NewClientUsingGet) to create a client that puts the request in GET query parameters, compatible with many GraphQL servers. For example: +```go +ctx := context.Background() +client := graphql.NewClientUsingGet("https://api.github.com/graphql", http.DefaultClient) +resp, err := getUser(ctx, client, "benjaminjkraft") +fmt.Println(resp.User.Name, err) +``` + +The request will be sent via an HTTP GET request, with the query, operation name and variables encoded in the URL, like so: +``` +https://api.github.com/graphql?operationName%3DgetUser%26query%3D%0Aquery%20getUser(%24login%3A%20String!)%20%7B%0A%20%20user(login%3A%20%24login)%20%7B%0A%20%20%20%20name%0A%20%20%7D%0A%7D%0A%26variables%3D%7B%22login%22%3A%22benjaminjkraft%22%7D +``` + +This is useful for caching requests in a CDN or browser cache. It's not recommended for requests containing sensitive data. This client does not support mutations, and will return an error if used for a mutation. + +[godoc#NewClientUsingGet]: https://pkg.go.dev/github.com/Khan/genqlient/graphql#NewClientUsingGet + +### Custom clients + +The genqlient client is an interface; you may define your own implementation. This could wrap the ordinary client to handle GraphQL extensions or set query-specific headers; or start from scratch to use a custom transport. For details, see the [documentation][godoc#Client]. + +[godoc#Client]: https://pkg.go.dev/github.com/Khan/genqlient/graphql#Client + +## Testing + +### Testing code that uses genqlient + +Testing code that uses genqlient typically involves passing in a special HTTP client that does what you want, similar to authentication. For example, you might write a client whose `RoundTrip` returns a fixed response, constructed with [`httptest`]. Or, you can use `httptest` to start up a temporary server, and point genqlient at that. Many third-party packages provide support for this sort of thing; genqlient should work with any HTTP-level mocking that can expose a regular `http.Client`. + +For an example, genqlient's own integration tests use both approaches: +- we [set up a simple GraphQL server](../internal/integration/server/server.go) using [`gqlgen`][gqlgen] and [`httptest`][httptest], and run requests against that +- we also [wrap the HTTP client](../internal/integration/roundtrip.go) to do extra assertions about each request and response (to check the marshaling and unmarshaling logic). + +[gqlgen]: https://gqlgen.com/ +[httptest]: https://pkg.go.dev/net/http/httptest + +### Testing servers + +If you want, you can use genqlient to test your GraphQL APIs; as with mocking you can point genqlient at anything that exposes an ordinary HTTP endpoint or a custom `http.Client`. However, at Khan Academy we've found that genqlient usually isn't the best client for testing: for example, manually constructing values of genqlient's response types gets cumbersome when interfaces or fragments are involved. Instead, we prefer to use a lightweight (and weakly-typed) client for that, and may separately open-source ours in the future. + +## Response objects + +Each genqlient-generated helper function returns a struct whose type corresponds to the query result. For example, given a simple query: + +```graphql +query getUser($login: String!) { + user(login: $login) { + name + } +} +``` + +genqlient will generate something like the following: + +```go +func getUser(...) (*getUserResponse, error) { ... } + +type getUserResponse struct { + User getUserUser +} + +type getUserUser struct { + Name string +} +``` + +For more on accessing response objects for interfaces and fragments, see the [operations documentation](operations.md#interfaces). + +### Handling errors + +In addition to the response-struct, each genqlient-generated helper function returns an error. The response-struct will always be initialized (never nil), even on error. If the request returns a valid GraphQL response containing errors, the returned error will be [`As`-able](https://pkg.go.dev/errors#As) as [`gqlerror.List`](https://pkg.go.dev/github.com/vektah/gqlparser/v2/gqlerror#List), and the struct may be partly-populated (if one field failed but another was computed successfully). If the request fails entirely, the error will be another error (e.g. a [`*url.Error`](https://pkg.go.dev/net/url#Error)), and the response will be blank (but still non-nil). + +For example, you might do one of the following: +```go +// return both error and field: +resp, err := getUser(...) +return resp.User.Name, err + +// handle different errors differently: +resp, err := getUser(...) +var errList *gqlerror.List +if errors.As(err, &errList) { + for _, err := range errList { + fmt.Printf("%v at %v\n", err.Message, err.Path) + } + fmt.Printf("partial response: %v\n", resp) +} else if err != nil { + fmt.Printf("http/network error: %v\n", err) +} else { + fmt.Printf("successful response: %v\n", resp) +} +``` + +### Marshaling + +All genqlient-generated types support both JSON-marshaling and unmarshaling, which can be useful for putting them in a cache, inspecting them by hand, using them in mocks (although this is [not recommended](#testing-servers)), or anything else you can do with JSON. It's not guaranteed that marshaling a genqlient type will produce the exact GraphQL input -- we try to get as close as we can but there are some limitations around Go zero values -- but unmarshaling again should produce the value genqlient returned. That is: + +```go +resp, err := MyQuery(...) +// not guaranteed to match what the server sent (but close): +b, err := json.Marshal(resp) +// guaranteed to match resp: +var respAgain MyQueryResponse +err := json.Unmarshal(b, &resp) +``` + diff --git a/docs/DESIGN.md b/docs/design.md similarity index 100% rename from docs/DESIGN.md rename to docs/design.md diff --git a/docs/faq.md b/docs/faq.md new file mode 100644 index 00000000..85178c2f --- /dev/null +++ b/docs/faq.md @@ -0,0 +1,121 @@ +# Frequently Asked Questions + +This document describes common questions about genqlient, and provides an index to how to represent common query structures. For a full list of configuration options, see [genqlient.yaml](genqlient.yaml) and [genqlient_directive.graphql](genqlient_directive.graphql). + +## Configuring genqlient + +### How do I set up genqlient to use an API that requires authentication? + +Customize the `http.Client` of your [`graphql.Client`](client_config.md#authentication-and-other-headers). + +### How do I make requests against a mock server, for tests? + +Inject a test HTTP response or server [into the `graphql.Client`](client_config.md#testing). + +### Does genqlient support custom scalars? + +Tell genqlient how to handle your custom scalars with the [`bindings` option](schema.md#custom-scalars). + +### Can I use introspection to fetch my client schema? + +Yes, but you'll need to use a separate tool ([example](schema.md#fetching-your-schema)). + +## Why? + +### Why use genqlient? + +See the [README.md](../README.md#why-another-graphql-client). + +### Why does genqlient generate such complicated type-names? + +The short answer is that GraphQL forces our hand. For example, consider a query +```graphql +query GetFamilyNames { + user { + name + children { + name + } + } +} +``` +which returns the following JSON: +```graphql +{ + "user": { + "name": "Ellis Marsalis Jr.", + "children": [ + {"name": "Branford Marsalis"}, + {"name": "Delfeayo Marsalis"}, + {"name": "Jason Marsalis"}, + {"name": "Wynton Marsalis"} + ] + } +} +``` +We need two different `User` types to represent this: one with a `Children` field, and one without. (And there may be more in other queries!) Of course, we could name them `User1` and `User2`, but that's both less descriptive and less stable as the query changes (perhaps to add `parent`), so we call them `GetFamilyNamesUser` and `GetFamilyNamesUserChildrenUser`. + +This applies even in cases where the types are exactly the same, so that the type names will be stable as the query changes. For example, in the following query there are three different "User" types: + +```graphql +query GetMonopolyPlayers { + game { + winner { id name } + banker { id name } + spectators { id name } + } +} +``` + +This will produce a Go type like: +```go +type GetMonopolyPlayersGame struct { + Winner GetMonopolyPlayersGameWinnerUser + Banker GetMonopolyPlayersGameBankerUser + Spectators []GetMonopolyPlayersGameSpectatorsUser +} + +type GetMonopolyPlayersGameWinnerUser struct { + Id string + Name string +} + +// (other structs identical) +``` + +This may seem unnecessary, but imagine if we generated a single type `GetMonopolyPlayersGameUser`, and then later you changed the query to do `spectators { id name favoritePlayer }`; we'd now have to change all three type names, potentially forcing you to update all the code that uses the query result. + +For more on customizing the type names -- including combining several types into one -- see the [operations documentation](operations.md#type-names). + +For the long answer, see the [design note](design.md#named-vs-unnamed-types). + +## Known issues + +### My editor/IDE plugin doesn't know about the code genqlient just generated + +If your tools are backed by [gopls](https://github.com/golang/tools/blob/master/gopls/README.md) (which is most of them), they simply don't know it was updated. In most cases, keeping the generated file (typically `generated.go`) open in the background, and reloading it after each run of `genqlient`, will do the trick. + +### genqlient fails after `go mod tidy` + +If genqlient fails with an error `missing go.sum entry for module providing package`, this is typically because `go mod tidy` removed its dependencies because they weren't imported by your Go module. You can read more about this in golang/go#45552; see in particular [this comment](https://github.com/golang/go/issues/45552#issuecomment-819545037). In short, if you want to be able to `go run` on newer Go you'll need to have a (blank) import of genqlient's entrypoint in a special `tools.go` file somewhere in your module so `go mod tidy` doesn't prune it: + +```go +//go:build tools +// +build tools + +package client + +import _ "github.com/Khan/genqlient" +``` + +### I'm getting confusing errors from `@genqlient` directives + +Currently, `@genqlient` directives apply to all relevant nodes on the following line, see [#151](https://github.com/Khan/genqlient/issues/151) or the [`@genqlient` documentation](genqlient_directive.graphql). If in doubt, spread things out onto more lines and they'll probably work! + +Common examples of this error: +- `for is only applicable to operations and arguments` +- `omitempty may only be used on optional arguments` + +### My issue is fixed in `main` but not in the latest release + +genqlient does not publish a release for every bugfix; read more about our [versioning strategy](versioning.md) or use `go get -u github.com/Khan/genqlient@main` to install from latest `main`. diff --git a/docs/genqlient_directive.graphql b/docs/genqlient_directive.graphql index 7ef93797..2ba0eb6c 100644 --- a/docs/genqlient_directive.graphql +++ b/docs/genqlient_directive.graphql @@ -222,7 +222,7 @@ directive genqlient( # request the exact same fields). They must even have the fields in the # same order. They should also have matching @genqlient directives, although # this is not currently validated (see issue #123). Fragments are often - # easier to use (see the discussion of code-sharing in FAQ.md, and the + # easier to use (see the discussion of code-sharing in operations.md, and the # "flatten" option above). # # Note that unlike most directives, if applied to the entire operation, diff --git a/docs/INTRODUCTION.md b/docs/introduction.md similarity index 85% rename from docs/INTRODUCTION.md rename to docs/introduction.md index e1fe33df..f5915d50 100644 --- a/docs/INTRODUCTION.md +++ b/docs/introduction.md @@ -1,6 +1,6 @@ # Getting started with genqlient -This document describes how to set up genqlient and use it for simple queries. See also the full worked [example](../example), the [FAQ](FAQ.md), and the reference for [project-wide](genqlient.yaml) and [query-specific](genqlient_directive.graphql) configuration options. +This document describes how to set up genqlient and use it for simple queries. See also the full worked [example](../example), the [FAQ](faq.md), and the rest of the [documentation](docs). ## Step 1: Download your schema @@ -61,6 +61,4 @@ resp, err := getUser(...) ``` (You don't need to do anything with the constant, just keep it somewhere in the source as documentation and for the next time you run genqlient.) In this case you'll need to update `genqlient.yaml` to tell it to look at your Go code. -All the filenames above, and many other aspects of genqlient, are configurable; see [genqlient.yaml](genqlient.yaml) for the full range of options. You can also configure how genqlient converts specific parts of your query with the [`@genqlient` directive](genqlient_directive.graphql). See the [FAQ](FAQ.md) for common options. - -If you want to know even more, and help contribute to genqlient, see [DESIGN.md](DESIGN.md) and [CONTRIBUTING.md](CONTRIBUTING.md). Happy querying! +All the filenames above, and many other aspects of genqlient, are configurable; see the [full documentation](.) for usage guides, reference information, and documentation on how to contribute to genqlient. diff --git a/docs/operations.md b/docs/operations.md new file mode 100644 index 00000000..0610a310 --- /dev/null +++ b/docs/operations.md @@ -0,0 +1,408 @@ +# Writing your GraphQL operations for genqlient + +While simple query structures map naturally from GraphQL to Go, more complex queries require different handling. This document describes how genqlient maps various GraphQL structures to Go, and the best ways to structure your queries and configure genqlient to handle them. For a complete list of options, see the [`genqlient.yaml`](genqlient.yaml) and [`@genqlient`](genqlient_directive.graphql) references. + +## Nullable fields + +There are several ways to handle nullable fields in genqlient: using [zero values](#zero-values), [pointers](#pointers), or [generics](#generics). In some cases you'll also need [`omitempty`](#omitempty). + +### Zero values + +One way is to use the Go idiom, where null gets mapped to the zero value; this is the default in genqlient. So if you have a GraphQL field of type `String`, and you do: + +```graphql +query MyQuery(arg: String) { + myString +} +``` + +then genqlient will generate a Go field `MyString string`, and set it to the empty string if the server returns null. This works even for structs: if an object type in GraphQL is null, genqlient will set the corresponding struct to its zero value. It can be helpful to request `id` in such cases, since that’s a field that should always be set, or `__typename` which is guaranteed to be set, so you can use its presence to decide whether to look at the other fields. + +### omitempty + +For input fields, you often want to tell genqlient to send null to the server if the argument is set to the zero value, similar to the JSON `omitempty` tag. In this case, you can do: + +```graphql +query MyQuery( + # @genqlient(omitempty: true) + arg: String, +) { + myString +} +``` + +You can also put the `# @genqlient(omitempty: true)` on the first line, which will apply it to all arguments in the query, or `# @genqlient(for: "MyInput.myField", omitempty: true)` on the first line to apply it to a particular field of a particular input type used by the query (for which there would otherwise be no place to put the directive, as the field never appears explicitly in the query, but only in the schema). + +Note that omitempty doesn't apply to structs, just like `encoding/json`. For structs that may be entirely unset, you can use `# @genqlient(omitempty: true, pointer: true)`, since `nil` pointers are omitted. + +### Generics + +If you need to distinguish null from the empty string (or generally from the Go zero value of your type), you can tell genqlient to use a generic type for optional fields/arguments, similar to Rust's `Option`. + +You can configure this by defining the type to use in your code (or using one from a library), for example: +```go +type Option[T any] struct { + Value T + HasValue bool +} + +// MarshalJSON, UnmarshalJSON, constructors, etc. +``` + +Then tell genqlient to use it like so: +```yaml +optional: generic +optional_generic_type: github.com/path/to/your/package.Option +``` + +This will generate a Go field `MyString Option[string]`, which you can handle as desired. + +### Pointers + +Similar to generics, you can tell genqlient to use a pointer for a field or argument: +```graphql +query MyQuery( + # @genqlient(pointer: true) + arg: String, +) { + # @genqlient(pointer: true) + myString +} +``` + +This will generate a Go field `MyString *string`, and set it to `nil` if the server returns null (and in reverse for arguments). Such fields can be harder to work with in Go, but allow a clear distinction between null and the Go zero value. You can put `optional: pointer` to apply this to all optional fields, or put the directive on the first line to apply it to everything in the query, although both often get cumbersome. To apply it to a specific input-type field, use `for`: + +As an example of using all these options together: +```graphql +# @genqlient(omitempty: true) +# @genqlient(for: "MyInputType.id", omitempty: false, pointer: true) +# @genqlient(for: "MyInputType.name", omitempty: false, pointer: true) +query MyQuery( + arg1: MyInputType!, + # @genqlient(pointer: true) + arg2: String!, + # @genqlient(omitempty: false) + arg3: String!, +) { + myString(arg1: $arg1, arg2: $arg2, arg3: $arg3) +} +``` +This will generate: +```go +func MyQuery( + ctx context.Context, + client graphql.Client, + arg1 MyInputType, + arg2 *string, // omitempty + arg3 string, +) (*MyQueryResponse, error) + +type MyInputType struct { + Id *string `json:"id"` + Name *string `json:"name"` + Title string `json:"title,omitempty"` + Age int `json:"age,omitempty"` +} +``` + +## GraphQL Interfaces + +If you request an interface field, genqlient generates an interface type corresponding to the GraphQL interface, and several struct types corresponding to its implementations. For example, given a query: + +```graphql +query GetBooks { + favorite { + title + ... on Novel { + protagonist + } + ... on Dictionary { + language + } + } +} +``` + +genqlient will generate the following types (see [below](#-genqlient-generate-such-complicated-type-names) for more on the names): + +```go +type GetBooksFavoriteBook interface { + GetTitle() string +} +type GetBooksFavoriteNovel struct { + Title string + Protagonist string +} +type GetBooksFavoriteDictionary struct { + Title string + Language string +} +// (similarly for any other types that implement Book) +``` + +These can be used in the ordinary Go ways: to access shared fields, use the interface methods; to access type-specific fields, use a type switch: + +```go +resp, err := GetBooks(...) +fmt.Println("Favorite book:", resp.Favorite.GetTitle()) +if novel, ok := resp.Favorite.(*GetBooksFavoriteNovel); ok { + fmt.Println("Protagonist:", novel.Protagonist) +} +``` + +The interface-type's GoDoc will include a list of its implementations, for your convenience. + +If you only want to request shared fields of the interface (i.e. no fragments), this may seem like a lot of ceremony. If you prefer, you can instead add `# @genqlient(struct: true)` to the field, and genqlient will just generate a struct, like it does for GraphQL object types. For example, given: + +```graphql +query GetBooks { + # @genqlient(struct: true) + favorite { + title + } +} +``` + +genqlient will generate just: + +```go +type GetBooksFavoriteBook struct { + Title string +} +``` + +Keep in mind that if you later want to add fragments to your selection, you won't be able to use `struct` anymore; when you remove it you may need to update your code to replace `.Title` with `.GetTitle()` and so on. + +## Sharing types + +By default, genqlient generates a different type for each part of each query, [even those which are structurally the same](faq.md#why-does-genqlient-generate-such-complicated-type-names-). Sometimes, however, you want to have some common code that can accept data from several queries or parts of queries. + +For example, suppose you have a query which requests several different fields each of the same GraphQL type, e.g. `User` (or `[User]`): + +```graphql +query GetMonopolyPlayers { + game { + winner { id name } + banker { id name } + spectators { id name } + } +} +``` + +This will produce a Go type like: +```go +type GetMonopolyPlayersGame struct { + Winner GetMonopolyPlayersGameWinnerUser + Banker GetMonopolyPlayersGameBankerUser + Spectators []GetMonopolyPlayersGameSpectatorsUser +} + +type GetMonopolyPlayersGameWinnerUser struct { + Id string + Name string +} + +// (other structs identical) +``` + +But maybe you wanted to be able to pass all those users to a shared function (defined in your code), say `FormatUser(user ???) string`. That's no good; you need to put three different types as the `???`. + +genqlient has several ways to deal with this. The two best methods for most uses are [fragments](#fragments), useful for reuse that exactly matches the query; and [Go interfaces](#go-interfaces), useful for more flexible access to types with common fields. For some use cases, the [`typename`](#shared-type-names) and [`bindings`](#bindings) options can be useful. + +### Fragments + +One option -- the GraphQL Way, perhaps -- is to use fragments. You'd write your query like: + +```graphql +fragment MonopolyUser on User { + id + name +} + +query GetMonopolyPlayers { + game { + winner { ...MonopolyUser } + banker { ...MonopolyUser } + spectators { ...MonopolyUser } + } +} +``` + +genqlient will notice this, and generate a type corresponding to the fragment; `GetMonopolyPlayersGame` will look as before, but each of the field types will have a shared embed: + +```go +type MonopolyUser struct { + Id string + Name string +} + +type GetMonopolyPlayersGameWinnerUser struct { + MonopolyUser +} + +// (others similarly) +``` + +Thus you can have `FormatUser` accept a `MonopolyUser`, and pass it `game.Winner.MonopolyUser`, `game.Spectators[i].MonopolyUser`, etc. This is convenient if you may later want to add other fields to some of the queries, because you can still do + +```graphql +fragment MonopolyUser on User { + id + name +} + +query GetMonopolyPlayers { + game { + winner { + winCount + ...MonopolyUser + } + banker { + bankerRating + ...MonopolyUser + } + spectators { ...MonopolyUser } + } +} +``` + +and you can even spread the fragment into interface types. It also avoids having to list the fields several times. + +### Flattening fragments +The Go field for `winner`, in the first query above, has type `GetMonopolyPlayersGameWinnerUser` which just wraps `MonopolyUser`. If we don't want to add any other fields, that's unnecessary! Instead, we could do +``` +query GetMonopolyPlayers { + game { + # @genqlient(flatten: true) + winner { + ...MonopolyUser + } + # (etc.) + } +} +``` +and genqlient will skip the indirection and give the field `Winner` type `MonopolyUser` directly. This is often much more convenient if you put all the fields in the fragment, like the first query did. + +### Go interfaces + +For each struct field it generates, genqlient also generates an interface method. If you want to share code between two types which to GraphQL are unrelated, you can define an interface containing that getter method, and genqlient's struct types will implement it. (Depending on your exact query, you may need to do a type-assertion from a genqlient-generated interface to yours.) For example, in the above query you could simply do: +```go +type MonopolyUser interface { + GetId() string + GetName() string +} + +func FormatUser(user MonopolyUser) { ... } + +FormatUser(resp.Game.Winner) +``` + +In general in such cases it's better to change the GraphQL schema to show how the types are related, and use one of the other mechanisms, but this option is useful for schemas where you can't do that, or in the meantime. + +### Shared type names + +Finally, if you always want exactly the same fields on exactly the same types, and don't want to deal with interfaces at all, you can assign the same type name to multiple fields + +```graphql +query GetMonopolyPlayers { + game { + # @genqlient(typename: "User") + winner { id name } + # @genqlient(typename: "User") + banker { id name } + # @genqlient(typename: "User") + spectators { id name } + } +} +``` + +This will tell genqlient to use the same types for each field: + +```go +type GetMonopolyPlayersGame struct { + Winner User + Banker User + Spectators []User +} + +type User struct { + Id string + Name string +} +``` + +See [below](#type-names) for more on this option. + +### Bindings + +It's also possible to use the `bindings` option (see [`genqlient.yaml` documentation](genqlient.yaml)) for a similar purpose, but this is not recommended as it typically requires more work for less gain. + +## Names + +### Operation names + +genqlient will use the exact name of your query as the generated function name. For example, if your query looks like `query myQuery { ... }`, then genqlient will generate `func myQuery(...) (*myQueryResponse, error)`. This means your queries should follow the usual Go conventions, especially starting with an uppercase letter if the query should be exported. + +### Field names + +By default, genqlient chooses field names based on the schema's field names. To customize the name, genqlient supports GraphQL field-aliases. For example, if you do +```graphql +query MyQuery { + myGreatName: myString +} +``` +and genqlient will generate a Go field `MyGreatName string`. Note that the alias will always be uppercased, to ensure the field is visible to the Go JSON library. + +### Type names + +genqlient generates quite verbose type names in many cases. (In short, this is because the same `User` GraphQL type must map to different Go types depending which fields are requested; see the FAQ for [more](faq.md#why-does-genqlient-generate-such-complicated-type-names-). + +For example, in the following query there are two different user structs. +```graphql +query GetFamilyNames { + user { # GetFamilyNamesUser + name + children { # GetFamilyNamesUserChildrenUser + name + } + } +} +``` + +In many cases, you won't need to refer to these directly (only the field names, which are short). But when you do, you can add type aliases for them in your code: +```go +type User = GetFamilyNamesUser +type ChildUser = GetFamilyNamesUserChildrenUser +``` + +Alternately, you can use the `typename` option: if you query +```graphql +query GetFamilyNames { + # @genqlient(typename: "User") + user { + name + # @genqlient(typename: "ChildUser") + children { + name + } + } +} +``` +genqlient will instead generate types with the given names. You can even [map multiple identical types to one](#shared-type-names). + +This approach can be quite convenient, but you'll need to take care to avoid conflicts: each name must be unique, unless the fields are exactly the same (see the [full documentation](genqlient_directive.graphql) for details). + +## Documentation + +For any GraphQL types or fields with documentation in the GraphQL schema, genqlient automatically includes that documentation in the generated code's GoDoc. To add additional information to genqlient entrypoints, you can put comments in the GraphQL source: + +```graphql +# This query gets the current user. +# +# If you also need to specify options on the query, you can put +# the @genqlient directive after the docuentation, like this: +# +# @genqlient(omitempty: true) +query GetUser { ... } +``` + diff --git a/docs/schema.md b/docs/schema.md new file mode 100644 index 00000000..e28be829 --- /dev/null +++ b/docs/schema.md @@ -0,0 +1,130 @@ +# Configuring genqlient to use your GraphQL schema + +This document describes common configuration options to get genqlient to work well with your GraphQL schema. For a complete list of options, see the [`genqlient.yaml` reference](genqlient.yaml). + +## Fetching your schema + +At present, genqlient expects your schema to exist on-disk. To fetch the schema from the server using introspection, you can use a tool such as [gqlfetch] and then let `genqlient` continue from there. Similarly, for [federated] servers you might fetch the supergraph (federated) schema from a registry, or construct it locally from the subgraph schemas. + +[gqlfetch]: https://github.com/suessflorian/gqlfetch +[federated]: https://www.apollographql.com/docs/federation/ + +If desired, you can wrap this process up in a tool that you call via `go generate`, for example: + +```go +package main + +import ( + "context" + "fmt" + "os" + + "github.com/Khan/genqlient/generate" + "github.com/suessflorian/gqlfetch" +) + +func main() { + schema, err := gqlfetch.BuildClientSchema(context.Background(), "http://localhost:8080/query") + if err != nil { + fmt.Println(err) + os.Exit(1) + } + + if err = os.WriteFile("schema.graphql", []byte(schema), 0644); err != nil { + fmt.Println(err) + os.Exit(1) + } + + generate.Main() +} +``` + +This can now be invoked upon `go generate` via `//go:generate yourpkg/generate`. + +## Scalars + +GraphQL [defines][spec#scalar] five standard scalar types, which genqlient automatically maps to the following Go types: + +| GraphQL type | Go type | +---------------------------- +| `Int` | `int` | +| `Float` | `float64` | +| `String` | `string` | +| `Boolean` | `bool` | +| `ID` | `string` | + +For custom scalars, or to map to different types, use the `bindings` option in [`genqlient.yaml`](genqlient.yaml). + +[spec#scalar]: https://spec.graphql.org/draft/#sec-Scalars + +### Custom scalars + +Some schemas define custom scalars. You'll need to tell genqlient what types to use for those via the `bindings` option in `genqlient.yaml`, for example: + +```yaml +bindings: + DateTime: + type: time.Time +``` + +The schema should define how custom scalars are encoded in JSON; you'll need to make sure the given type has the appropriate `MarshalJSON`/`UnmarshalJSON` or `json` tags. When using a third-party type, like `time.Time`, you can alternately define separate functions: + +```yaml +bindings: + DateTime: + type: time.Time + marshaler: "github.com/your/package.MarshalDateTime" + unmarshaler: "github.com/your/package.UnmarshalDateTime" +``` + +See genqlient's integration tests for a full example: [types](../internal/testutil/types.go), [config](../internal/integration/genqlient.yaml). + +To leave a custom scalar as raw JSON, map it to `encoding/json.RawMessage`: + +```yaml +bindings: + JSON: + type: encoding/json.RawMessage +``` + +### Integer sizing + + +The GraphQL spec officially defines the `Int` type to be a [signed 32-bit integer](https://spec.graphql.org/draft/#sec-Int). GraphQL clients and servers vary wildly in their enforcement of this; for example: +- [Apollo Server](https://github.com/apollographql/apollo-server/) explicitly checks that integers are at most 32 bits +- [gqlgen](https://github.com/99designs/gqlgen) by default allows any integer that fits in `int` (i.e. 64 bits on most platforms) +- [Apollo Client](https://github.com/apollographql/apollo-client) doesn't check (but implicitly is limited to 53 bits by JavaScript) +- [shurcooL/graphql](https://github.com/shurcooL/graphql) requires integers be passed as a `graphql.Int`, defined to be an `int32` + +By default, genqlient maps GraphQL `Int`s to Go's `int`, meaning that on 64 bit systems there's no client-side restriction. This is convenient for most use cases, but means the client won't prevent you from passing a 64-bit integer to a server that will reject or truncate it. + +If you prefer to limit integers to `int32`, you can set a binding in your `genqlient.yaml`: + +```yaml +bindings: + Int: + type: int32 +``` + +Or, you can bind it to any other type, perhaps one with size-checked constructors, similar to a custom scalar. + +If your schema has a big-integer type, you can bind that similarly to other custom scalars: +```yaml +bindings: + BigInt: + type: math/big # or int64, or string, etc. + # if you need custom marshaling + marshaler: "github.com/path/to/package.MarshalBigInt" + unmarshaler: "github.com/path/to/package.UnmarshalBigInt" +``` + +## Extensions + +Some schemas/servers make use of GraphQL extensions, for example to add rate-limit information to responses. There are two ways to handle these in genqlient: + +1. If you want to handle extensions in a general way for all queries (for example, to automatically retry after the rate-limit resets, you can do this in your [`graphql.Client` implementation](client_config.md#custom-clients). +2. To return response extensions directly in the generated helper functions (so that callers can decide what to do), set `use_extensions: true` in your [`genqlient.yaml`](genqlient.yaml). + +## Hasura, Dgraph, and other generated schemas + +Some GraphQL tools, like Hasura and Dgraph, generate large schemas automatically from non-GraphQL data (like database schemas). These schemas tend to be quite large and complex, and often run into trouble with GraphQL. See [#272](https://github.com/Khan/genqlient/issues/272) for discussion of how to use these tools with genqlient. diff --git a/docs/versioning.md b/docs/versioning.md new file mode 100644 index 00000000..ea4410da --- /dev/null +++ b/docs/versioning.md @@ -0,0 +1,30 @@ +# Versioning policy + +This document describes how we manage genqlient versions. See all published versions on [pkg.go.dev](https://pkg.go.dev/github.com/Khan/genqlient?tab=versions) or [GitHub](https://github.com/Khan/genqlient/releases). + +## When do we make a release? + +In general, we do not cut a release for every bugfix; instead we try to cut a release after major changes have had some time to bake, and in some cases after large codebases using genqlient have tried them. This ensures releases are somewhat more likely to work. + +If that stability is desirable to you, use tagged releases of genqlient only (e.g. `go get github.com/Khan/genqlient@latest`), and be aware that new features may take somewhat longer to make it to a release. (Feel free to make an issue to request a release if it's been a while.) + +If you always want the latest and greatest changes quickly, Go has excellent support for installing packages at any commit. We do have extensive tests and try to keep the main branch safe for production use, but we're never perfect, so take care appropriate to your use case. You can install the main branch with `go get github.com/Khan/genqlient@main`, or replace `main` with any commit SHA. Please report any bugs you see so they can be fixed before the next release! + +For the details of actually making a release, see the [contributor docs](CONTRIBUTING.md#making-a-release). + +## What is a breaking change? + +We consider the following changes to be breaking: +- breaking changes to the runtime `graphql` package (obviously) +- changes which cause genqlient to, given the same valid query, make a breaking change to the API or behavior of the generating code (i.e. it should be safe to re-run a newer version of genqlient on existing queries) +- changes to the `graphql` runtime package which require corresponding changes to the code-generator (i.e. there's no obligation to run genqlient every time you upgrade) + +We don't consider the following changes to be breaking: +- syntactic changes to the generated output for existing queries; if you check that your generated code is up to date in CI you should expect to need to update it when you update genqlient +- changes, including breaking API changes, to any double-underscore-prefixed names in the generated code (i.e. don't refer to these in your code); the same applies to any names from the `graphql` runtime package documented as "intended for the use of genqlient's generated code only" +- changes to the code-generator which require corresponding changes to the `graphql` runtime package (these are safe because your runtime should be the same or newer). +- dropping support for Go versions which are no longer supported by the Go project (all but the [two newest](https://go.dev/doc/devel/release)) + +Additionally, your version of the `graphql` runtime package must be the same or newer (and the same major version) as your version of the code-generator. (It's recommended to use the same version of both, but it's not required to regenerate all your queries after upgrading.) + +Note that while genqlient is on version 0.x we may make breaking changes at any time, although we still aim to do so only in minor version bumps (0.6.0, not 0.5.1), and we aim to minimize breaking changes, especially to core functionality. diff --git a/generate/convert.go b/generate/convert.go index d3a9f8d3..b7e6a6d2 100644 --- a/generate/convert.go +++ b/generate/convert.go @@ -678,7 +678,7 @@ func (g *generator) convertSelectionSet( // the fragment's type. This is distinct from the rules for when a fragment // spread is legal, which is true when the fragment would be active for *any* // of the concrete types the spread-context could have (see the [GraphQL spec] -// or docs/DESIGN.md). +// or docs/design.md). // // containingTypedef is as described in convertInlineFragment, below. // fragmentTypedef is the definition of the fragment's type-condition, i.e. the @@ -726,7 +726,7 @@ func fragmentMatches(containingTypedef, fragmentTypedef *ast.Definition) bool { // // In general, we treat such fragments' fields as if they were fields of the // parent selection-set (except of course they are only included in types the -// fragment matches); see docs/DESIGN.md for more. +// fragment matches); see docs/design.md for more. func (g *generator) convertInlineFragment( namePrefix *prefixList, fragment *ast.InlineFragment, diff --git a/generate/names.go b/generate/names.go index 29074e0e..0aecacc2 100644 --- a/generate/names.go +++ b/generate/names.go @@ -2,7 +2,7 @@ package generate // This file generates the names for genqlient's generated types. This is // somewhat tricky because the names need to be unique, stable, and, to the -// extent possible, human-readable and -writable. See docs/DESIGN.md for an +// extent possible, human-readable and -writable. See docs/design.md for an // overview of the considerations; in short, we need long names. // // Specifically, the names we generate are of the form: @@ -33,7 +33,7 @@ package generate // One subtlety in the above description is: is the "MyType" the interface or // the implementation? When it's a suffix, the answer is both: we generate // both MyFieldMyInterface and MyFieldMyImplementation, and the latter, in Go, -// implements the former. (See docs/DESIGN.md for more.) But as an infix, we +// implements the former. (See docs/design.md for more.) But as an infix, we // use the type on which the field is requested. Concretely, the following // schema and query: // type Query { f: I } diff --git a/generate/testdata/queries/ComplexInlineFragments.graphql b/generate/testdata/queries/ComplexInlineFragments.graphql index 0e4dbe4c..456973d5 100644 --- a/generate/testdata/queries/ComplexInlineFragments.graphql +++ b/generate/testdata/queries/ComplexInlineFragments.graphql @@ -1,4 +1,4 @@ -# We test all the spread cases from docs/DESIGN.md, see there for more context +# We test all the spread cases from docs/design.md, see there for more context # on each, as well as various other nonsense. But for abstract-in-abstract # spreads, we can't test cases (4b) and (4c), where I implements J or vice # versa, because gqlparser doesn't support interfaces that implement other diff --git a/generate/testdata/snapshots/TestGenerate-ComplexInlineFragments.graphql-ComplexInlineFragments.graphql.go b/generate/testdata/snapshots/TestGenerate-ComplexInlineFragments.graphql-ComplexInlineFragments.graphql.go index 5cd21cd9..a4e123de 100644 --- a/generate/testdata/snapshots/TestGenerate-ComplexInlineFragments.graphql-ComplexInlineFragments.graphql.go +++ b/generate/testdata/snapshots/TestGenerate-ComplexInlineFragments.graphql-ComplexInlineFragments.graphql.go @@ -1429,7 +1429,7 @@ query ComplexInlineFragments { } ` -// We test all the spread cases from docs/DESIGN.md, see there for more context +// We test all the spread cases from docs/design.md, see there for more context // on each, as well as various other nonsense. But for abstract-in-abstract // spreads, we can't test cases (4b) and (4c), where I implements J or vice // versa, because gqlparser doesn't support interfaces that implement other @@ -1454,4 +1454,3 @@ func ComplexInlineFragments( return &data_, err_ } - From f37c25b33e52d5fe17f0b3a5a451de309f53a301 Mon Sep 17 00:00:00 2001 From: Ben Kraft Date: Mon, 19 Feb 2024 12:30:08 -0800 Subject: [PATCH 2/2] fix snapshot --- ...plexInlineFragments.graphql-ComplexInlineFragments.graphql.go | 1 + 1 file changed, 1 insertion(+) diff --git a/generate/testdata/snapshots/TestGenerate-ComplexInlineFragments.graphql-ComplexInlineFragments.graphql.go b/generate/testdata/snapshots/TestGenerate-ComplexInlineFragments.graphql-ComplexInlineFragments.graphql.go index a4e123de..d5591c0c 100644 --- a/generate/testdata/snapshots/TestGenerate-ComplexInlineFragments.graphql-ComplexInlineFragments.graphql.go +++ b/generate/testdata/snapshots/TestGenerate-ComplexInlineFragments.graphql-ComplexInlineFragments.graphql.go @@ -1454,3 +1454,4 @@ func ComplexInlineFragments( return &data_, err_ } +