GithubHelp home page GithubHelp logo

genqlient's Introduction

generated graphql client โ‡’ genqlient

Go Reference Test Status Contributor Covenant GoReportcard

genqlient: a truly type-safe Go GraphQL client

What is genqlient?

genqlient is a Go library to easily generate type-safe code to query a GraphQL API. It takes advantage of the fact that both GraphQL and Go are typed languages to ensure at compile-time that your code is making a valid GraphQL query and using the result correctly, all with a minimum of boilerplate.

genqlient provides:

  • Compile-time validation of GraphQL queries: never ship an invalid GraphQL query again!
  • Type-safe response objects: genqlient generates the right type for each query, so you know the response will unmarshal correctly and never need to use interface{}.
  • Production-readiness: genqlient is used in production at Khan Academy, where it supports millions of learners and teachers around the world.

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, or the example. For more complete documentation, see the docs.

How can I help?

genqlient welcomes contributions! Check out the (Contribution Guidelines), or file an issue on GitHub.

Why another GraphQL client?

Most common Go GraphQL clients have you write code something like this:

query := `query GetUser($id: ID!) { user(id: $id) { name } }`
variables := map[string]interface{}{"id": "123"}
var resp struct {
	Me struct {
		Name graphql.String
	}
}
client.Query(ctx, query, &resp, variables)
fmt.Println(resp.Me.Name)
// Output: Luke Skywalker

This code works, but it has a few problems:

  • While the response struct is type-safe at the Go level; there's nothing to check that the schema looks like you expect. Maybe the field is called fullName, not name; or maybe you capitalized it wrong (since Go and GraphQL have different conventions); you won't know until runtime.
  • 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 is a popular server library that generates types, and Apollo has a codegen tool to generate similar client-types for several other languages. (See the design note 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.

genqlient's People

Contributors

adambabik avatar bafko avatar benjaminjkraft avatar breml avatar connec avatar csilvers avatar dependabot[bot] avatar dnerdy avatar dylanrjohnston avatar edigaryev avatar hsblhsn avatar janboll avatar johnmaguire avatar kevinmichaelchen avatar nathanstitt avatar nuvivo314 avatar omarkohl avatar salman-rb avatar spencermurray avatar stevenacoffman avatar suessflorian avatar tarrencev avatar vikstrous2 avatar zholti avatar zzh8829 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  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

genqlient's Issues

Minify queries

We could minify queries, optionally, to save space.

If anyone really needs this, they could do it in graphql.Client, so that might be a better way to prototype.

Allow sharing of type-names between queries.

We have two different gql queries:

	_ = `# @genqlient
	query TestPrep_GetAssessmentItemsByShas($exerciseID: String!, $shas: [String!]!) {
	  assessmentItemsByShas(exerciseId: $exerciseID, shas: $shas) {
	    contentId
	    itemData
	    itemShapeType
	    name
	    sha
	    tagIds
	  }
	}`

	_ = `# @genqlient
	query TestPrep_GetAssessmentItemsForExercise($exerciseID: String!) {
	  exerciseById(id: $exerciseID) {
		assessmentItems {
	      contentId
	      itemData
	      itemShapeType
	      name
	      sha
	      tagIds
	    }
	  }
	}`

While the graphql queries differ, the "return" type is the same. It would be convenient if they could both be the same type in genqlient, rather than two different types with identical shapes.

This is probably related to #12.

Switch to a snapshot testing library

Cupaloy looks good, although some part of me still wants to be able to put the files exactly where they are, for easy perusal.

May need to do an internal ADR for this. (Edit: done.)

Add integration tests

We could add some integration tests against common GraphQL servers, to make sure we actually match what they expect. Unclear how much of an issue this is in practice; the HTTP parts of GraphQL are a bit underspecified, but also fairly simple.

Allow __all to request all fields

A fun thing we can do, because we generate code, is to allow you to put in a pseudo-field like __all, which we expand to all the fields. That's probably a terrible idea, both in that it diverges from the spec, and you shouldn't want to do that, but we could do it anyway! And it would be useful especially for tests.

Document behavior/options for the 32-bit int issue

  • note that GraphQL ints are 32-bit, servers may reject or misbehave:
    • apollo-server explicitly checks ints are 32 bits
    • gqlgen checks (via ParseInt) that ints fit in whatever type your code uses (default int)
    • graphene documents 53 bits but actually requires 32 (unless you use its BigInt type)
  • default behavior is to not check
    • apollo-client doesn't check (although it's JS so it's at most 53 bits)
    • shurcooL effectively makes you pass its graphql.Int type, which is int32
  • instead can set an int32 in scalars, or a custom type that say warns on conversion

Do the initialisms thing

If you have a field id, we should, for better or worse, generate ID rather than Id, and XMLHTTPRequest rather than XmlHttpRequest, per the Go style. Or at this point, probably, have an option to do so.

Allow mapping directly to a type but not using its standard JSON serialization

You might want to map your GraphQL DateTime to time.Time, but you might want to only include microseconds (because Python doesn't like them). You can do that by defining your own type, but then you have to cast at every call, which is the kind of boilerplate genqlient is designed to reduce! We could instead let you just define marshal/unmarshal functions, like gqlgen does.

Remove common prefix from all query-names

At Khan we prefix all of our queries with the service-name, e.g. Coaches_GetAssignments, so that the names are globally unique (useful for logs). But in the generated types, this prefix is just noise. It would be nice to find a way to remove it; except it's a bit hard to do so in a generalizable way.

Another option would be to munge the query to add to the "real" operation name -- that is you would just write query GetAssignments and genqlient would know based on your config to send Coaches_GetAssignments.

Use a different JSON library

There are a few potential ideas here, which may coordinate or conflict.

First, any code that's using genqlient should be a great match for one of the libraries that does JSON via codegen rather than reflection, which are theoretically way better for perf. @StevenACoffman and @csilvers pointed me to easyjson, ffjson, go-codec, jsonparser, and gojay, and Steve posted some more details about them below; I'd also want to run our own benchmarks on actual genqlient-generated code.

On the other hand, right now we do a lot of work to try to do what we want within the encoding/json API, so it might instead be better to use a library that has a different set of extension points more suitable to our needs. Steve pointed me to mapstructure which is definitely aimed at what we are trying to do in the case of abstract types and custom unmarshalers, which are the most complex case on the decoding side. Sadly it looks like it has somewhat limited support for encoding (see mitchellh/mapstructure#166), and while it has more flexible support for embedding than the standard library, it will only simplify our unmarshalers (by making it easier to merge embedded structs), not eliminate them.

Switching to mapstructure would also preclude switching to one of the other code-gen libraries, which tend to be compatible with encoding/json. Some do add additional options, so we could explore that. (Of those linked above, go-codec seems to have the most extension points, and would likely have similar benefits to mapstructure, although like mapstructure its support for embedding is likely not flexible enough to solve all our problems. It also has support for other encodings, which could be useful for e.g. putting your responses in a cache, or if you use some strange transport for your GraphQL (not likely). Note that it's possible golang/go#5901 will add some of the features we'd like to the standard library, although the timeline on that is very unclear to me.)

One potential future need comes from #30; which for fields will require special handling in (un)marshaling to (un)marshal several layers of JSON structure into/from one layer of Go structure. I didn't see support from any of the libraries for that, although mapstructure might at least provide better tools for us to do it ourselves, or one could imagine any of the libraries might add it (essentially what we want is support for a tag json:"a.b.c"). Another potential future need is more options for how to handle omitempty; right now we match JSON because I think that's the least-bad default (see #103) but it's got some problems for sure. (And now we probably do want to try to keep that behavior, at least as the default, for better or worse, which could be tricky for libraries that have different behavior.)

Finally, as mentioned in #120, another option is to roll our own, making use of what we know about our types to simplify things. I suspect it's not worth it yet, but if our (un)marshalers start having to get even more complicated, it may be; we don't need to implement nearly all of the behavior of the general-purpose libraries since we know what we want our types to look like.

For any of the third-party codegen options, we do need to make sure this won't cause trouble with custom scalars: it might be a little surprising if you have to rerun genqlient because you changed a json tag in a custom scalar, although it's not a huge problem in practice I expect (especially since probably most custom scalars will have UnmarshalJSON methods). In any case we could also put this behind a flag. A first-party implementation could call out to encoding/json for custom scalars, of course.

Test generated JSON-(un)marshalers

Especially as we introduce support for interfaces and fragments, a lot of the surface area of genqlient's generated code is going to be in the (un)marshalers. We should have some tests for those, to make sure that they actually work, rather than just that they're plausible enough to pass code review.

Simplify graphql.Client interface (MakeRequest)

The graphql.Client interface is scarily complex, for something we have to support forever. We may want to try to make it a bit more constrained, with specific hooks for specific things, so that callers can't do weird things. But we'll need to see how people actually use it.

At Khan, we currently:

  • munge the URL to add the opname
  • munge the error (if any), wrapping, and potentially adding some of the variables

Other potential use cases:

  • caching (so #29 can be done in third-party code)
  • send a hash (likewise #20) or minify (#7)
  • non-HTTP transports
  • non-JSON transports (this will be fairly hard since we'll have to generate unmarshalers for them; I'm not sure if anyone even does this though)

Line numbers of errors are not accurate

go run github.com/Khan/genqlient /home/csilvers/khan/webapp/services/users/genqlient.yaml
invalid query-spec file cross_service/admin.go: cross_service/admin.go:2: Expected $, found Name

Is there any way you can give a more accurate line number? The error is in line 2 of some graphql raw-string, but I don't know which one!

Revamp documentation

We need some proper documentation, not just random bits and bobs in the README and godoc. This can be based in part on what people hit in use at Khan.

Incomplete list:

  • adapt internal Khan doc "Using genqlient in webapp"
  • FAQ on "how do I represent/do/handle ..." to guide people to which options they need

Clean up client_getter (and document it)

One option is to just say it's a function whose argument is context. (That way we can handle imports right.) But ideally it could also be a global var; and what if you want to be able to return an error?

Or we could just flag it out for now, since Khan didn't end up needing it.

Plugin for _entities queries in Apollo federation

You might want to be able to make an _entities query against another federation server (perhaps one of your peers). The _entities field is a bit arcane for humans, but we could autogenerate the weird bits to let you make use of it; you would just write a fragment and we would put that in the right place.

Expand the flatten option to support fields

If you do mutation { MyMutation { error { code } } }, or something, we should be able to just return (code string, err error), at least if you ask us too. This would be a nice convenience -- the helper's callers no longer have to know as much about the query structure -- but the rules and logic to do it are surprisingly subtle.

Send a hash instead of the query

It's unclear to me if there's a standard spec for this, but certainly it's something some people do, to save bytes or whatnot. Apollo has some fancy way of doing it.

This would make #7 less important. It's also doable in graphql.Client, although perhaps better done at compile-time.

Make type-naming scheme a bit smarter

For example if you have a field doThing: DoThingMutation or error: MyError we should be able to be a bit smarter than DoThingDoThingMutation/ErrorMyError, in the same way we currently shorten error: Error to just Error. Although of course get too fancy here, and it gets hard to think about, and too magical.

Caching

Lots of clients do caching. We could too! Perhaps via a special client, or a query-option, depending.

Add documentation to fields (and types)

We already add comments on the query as documentation on the helper, but even more useful would be adding any documentation from the schema to the generated fields/types.

Allow configuring name-format

You might want to change the "Response" suffix, say, or make the names exported or unexported automatically. Actually there are actually two name-formats in question: the response-type name, and the prefix used for the other types.

Improve options-handling for input types

Right now you can't apply an option, like pointer: true, to a field of an input type, because there's nowhere to put it. So we need to figure out a syntax; maybe just pointerfields: ["field1", "field2.field3"] but that will be a bit of an explosion of options.

We also need to decide how to resolve conflicts: this would mean that two references to the same GraphQL input-type are no longer guaranteed to generate the same Go type, which means we have to prefix the names or something. (This is unlikely to come up within a single query, but more likely across queries.)

As a sort of workaround, you can construct the type inline:

query MyQuery(inputField: String!, ...) {
  myField(input: {
    field: $inputField,
    ....
  })
}

Possibly useful report

I didn't investigate too closely, but I didn't want to forget it in case this is useful. Over the weekend I thought I'd make a githubstatus cli tool and got this reproducible error:

$ git clone [email protected]:Khan/genql.git
$ cd genql/
$ go install
$ cd ..
$ mkdir githubstatus
$ cd githubstatus
$ git init
$ go mod init github.com/StevenACoffman/githubstatus
$ curl https://developer.github.com/v4/public_schema/schema.public.graphql -o schema.public.graphql
genql schema.public.graphql

invalid config file schema.public.graphql: yaml: unmarshal errors:
  line 1: cannot unmarshal !!str `` into generate.Config

Another weekend I might be able to spend more time on figuring it out, but thought I'd pass it on in case I forgot and it might be helpful in the meantime.

Support Apollo-style batching

Unclear exactly what the API for this would be -- would you specify the batches in advance and we generate one function, or is there some quasi-generic genqlient.All(<genqlient-functions>) <responses>, or what? But probably it would be useful. See also #45.

omitempty generates broken code for structs

You get

	var zero_myVar []string
	if myVar != zero_myVar {
		variables["myVar"] = myVar
	}

which is semantically correct but doesn't compile, because arrays aren't comparable. We should instead compare to nil (or check len, depending what we want to do with []string{}) for slices and maps.

As a workaround, since []string(nil) encodes to null, there's usually no need for omitempty on such fields, unless the backend actually makes a distinction between the argument being null and unset.

Remove line numbers from safelist file

They're useful for error message, but in the safelist file they just get out of date (or have to constantly be updated). We can just special-case it.

Make it possible to ask for a *[]T

If you put pointer: true on a list-field, you get []*T (or [][]*T, or...) which is probably what you wanted (so you can represent an required list of optional items, for example, but not the only option. If anyone finds a reason to want *[]T, or for that matter *[][]*[]T, we can figure out a syntax for it; it's easy enough to do technically. (Although it won't be totally trivial if T is an interface.)

Allow specifying type-names

The autogenerated names can be annoying, so you might want to explicitly specify the names. (With that comes some responsibility: you have to avoid conflicts. But in practice it's not super hard.) This would also make #11 less necessary.

Figure out defaults for pointers/omitempty

I have this unreasonable belief that we don't want pointer: true to be the default for optional args. Maybe omitempty: true should be though? (Although it could be surprising.) Or maybe pointer: true should be the default at least for input-types (not counting scalars/enums)? Or maybe that's all too magical and/or inconsistent.

See also #33.

Clarify type documentation

We do something like

// MyType's GraphQL description
type MyQueryMyFieldMyType struct { ... }

but for output types this may be only a partial representation of the type! For example, the description might suggest some useful fields, which don't exist on this struct. We could remove it, or just prefix it with a message like for the toplevel response type, e.g. "<goname> includes the fields of the GraphQL type <graphqlname> requested in <queryname>. The GraphQL type documentation follows.".

Potential field-name conflicts

The function from GraphQL field-name to Go struct field-name is not injective, so there's the potential that we generate two fields with the same name (which will fail to compile). The most likely way this would happen is if you request a field typename in a selection of interface type, where genqlient will add __typename; both translate to Typename in Go. But it's also possible that you request two fields MyField and myField, or myField and _myField; by convention field names and aliases are lowerCamelCase but it's not required and only double-underscore is officially reserved. It's always possible to work around this by aliasing, of course.

Hide __typename field if not user requested

If you request some field of interface type, we automatically add __typename to your selection, so we can use the right type. But you probably don't need that field in your structs; you can always switch on the Go type! So ideally we should omit it from the generated structs. This isn't hard in principle, it's just some awkward plumbing since the place where we need to add it and the place where we need to ignore it are not very close together.

[Feature Request] A way to combine several identical queries instead of looping

I understand that this isn't production ready yet nor is it complete but I'd like to request a feature if you guys are taking those requests.

I have a small app that hits a graphql endpoint and returns ~50-60 product handles. I then need to individually query data about those products through their handle. The query for each individual product looks like this:

productByHandle(handle:$handle) { 
    variants(first: 35) { 
        edges { 
            node { 
                id 
                price 
            } 
        } 
    } 
}

If I were to loop through each one and run the query individually I would hit a rate limit unless I specified something like time.Sleep(500 * time.Millisecond) after each query. But that adds up and takes time.

So what I ended up doing was writing a function that combines all the queries into named/aliased queries like this:

query {
  product1: productByHandle(handle:"ABC") { 
      variants(first: 35) { 
          edges { 
              node { 
                  id 
                  price 
              } 
          } 
      } 
  }
  product2: productByHandle(handle:"ZYX") { 
      variants(first: 35) { 
          edges { 
              node { 
                  id 
                  price 
              } 
          } 
      } 
  }
  product3: productByHandle(handle:"XYZ") { 
      variants(first: 35) { 
          edges { 
              node { 
                  id 
                  price 
              } 
          } 
      } 
  }
  product4: productByHandle(handle:"CBA") { 
      variants(first: 35) { 
          edges { 
              node { 
                  id 
                  price 
              } 
          } 
      } 
  }
  ...
  ...
  ...
}

which allows me to send it as one query instead of 50-60 individual ones.

So essentially what I am proposing is a method for doing essentially the same with genqlient; to be able to combine several identical queries into one large one. Thoughts?

Generate mocks

It's not totally clear how we would do this usefully. But it's something to think about!

Omitempty-style handling for array elements

Suppose you have an argument that's a [String]! and you want to pass ["a", null, "c"]. Right now you pretty much have to do pointer: true, then pass []*string{&"a", null, &"c"} (except of course that's not valid syntax, the real syntax is uglier until golang/go#45624). But we should be able to just let you apply the omitempty to the elements, instead of or in addition to to the whole array, so that you can pass []string{"a", "", "c"} and it does the thing you mean. It may need a name since it's really "translate empty elements to null", rather than omitting them.

See also #16 and #14.

Add an option for HasX fields

Just as another way to handle optionality. Although mostly it's turning out to be an issue in inputs, not results, where this is less useful!

Decide whether "scalars" is just for scalars

In principle you can actually use this for any type! But, like, gosh, is that a good idea? What if you have the wrong fields? We should either validate that the type is a scalar, or document that it's supported generally (and any caveats).

Clean up API

This is just to make sure there's nothing in godoc that we don't want to commit to. Although of course in v0 we don't totally have to.

At some point we will also need to decide if we consider a change to generated code to be breaking; it is only if you re-run your generator but obviously you want to be able to do that. (What if it's a non-functional change? That's only breaking if you expect your generator to be deterministic, which you might also reasonably want.)

See also #19.

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.