Document and improve support for binding non-scalars to a specific type (#69)
## Summary: We had this setting called "scalars", which said: bind this GraphQL type to this Go type, rather than the one you would normally use. It's called that because it's most useful for custom scalars, where "the one you would normally use" is "error: unknown scalar". But nothing ever stopped you from using it for a non-scalar type. I was planning on removing this functionality, because it's sort of a rough edge, but a discussion with Craig found some good use cases, so instead, in this commit, I document it better and add some slightly nicer ways to specify it. Specifically, here are a few potential non-scalar use cases: - bind a GraphQL enum to a nonstandard type (or even `string`) - bind an input type to some type that has exactly the fields you want; this acts as a sort of workaround for issues #14 and #44 - bind an object type to your own struct, so as to add methods to it (this is the use case Craig raised) - bind an object type to your own struct, so as to share it between multiple queries (I believe named fragments will address this case better, but it doesn't hurt to have options) - bind a GraphQL list type to a non-slice type in Go (presumably one with an UnmarshalJSON method), or any other different structure The latter three cases still have the sharp edge I was originally worried about, which is that nothing guarantees that the fields you request in the query are the ones the type expects to get. But I think it's worth having the option, with appropriate disclaimers. The main change to help support that better is that you can now specify the type inline in the query, as an alternative to specifying it in the config file; this means you might map a given object to a given struct, but only in some cases, and when you do you have a chance to look at the list of fields you're requesting. Additionally, I renamed the config field from "scalars" to "bindings" (but mentioned it in a few places where you might go looking for how to map scalars, most importantly the error message you get for an unknown (custom) scalar). While I was making a breaking change, I also changed it to be a `map[string]<struct>` instead of a `map[string]string`, because I expect to add more fields soon, e.g. to handle issue #38. Finally, since the feature is now intended/documented, I added some tests, although it's honestly quite simple on the genqlient side. ## Test plan: make tesc Author: benjaminjkraft Reviewers: csilvers, aberkan, dnerdy, MiguelCastillo Required Reviewers: Approved by: csilvers Checks: ⌛ Test (1.17), ⌛ Test (1.16), ⌛ Test (1.15), ⌛ Test (1.14), ⌛ Test (1.13), ⌛ Lint, ⌛ Test (1.17), ⌛ Test (1.16), ⌛ Test (1.15), ⌛ Test (1.14), ⌛ Test (1.13), ⌛ Lint Pull request URL: https://github.com/Khan/genqlient/pull/69
This commit is contained in:
+16
@@ -0,0 +1,16 @@
|
||||
query GetPokemonSiblings($input: PokemonInput!) {
|
||||
user(query: {hasPokemon: $input}) {
|
||||
# this will override the default mapping to internal/testutil.ID:
|
||||
# @genqlient(bind: "string")
|
||||
id
|
||||
# this is normally an enum, but here we make it a (list of) string:
|
||||
# @genqlient(bind: "[]string")
|
||||
roles
|
||||
name
|
||||
# this is mapped globally to internal/testutil.Pokemon:
|
||||
pokemon { species level }
|
||||
# this overrides said mapping:
|
||||
# @genqlient(bind: "-")
|
||||
genqlientPokemon: pokemon { species level }
|
||||
}
|
||||
}
|
||||
+12
@@ -20,6 +20,16 @@ enum Role {
|
||||
TEACHER
|
||||
}
|
||||
|
||||
input PokemonInput {
|
||||
species: String!
|
||||
level: Int!
|
||||
}
|
||||
|
||||
type Pokemon {
|
||||
species: String!
|
||||
level: Int!
|
||||
}
|
||||
|
||||
"""UserQueryInput is the argument to Query.users.
|
||||
|
||||
Ideally this would support anything and everything!
|
||||
@@ -33,6 +43,7 @@ input UserQueryInput {
|
||||
id: ID
|
||||
role: Role
|
||||
names: [String]
|
||||
hasPokemon: PokemonInput
|
||||
}
|
||||
|
||||
type AuthMethod {
|
||||
@@ -53,6 +64,7 @@ type User {
|
||||
emailsWithNulls: [String]!
|
||||
emailsWithNullsOrNull: [String]
|
||||
authMethods: [AuthMethod!]!
|
||||
pokemon: [Pokemon!]
|
||||
}
|
||||
|
||||
"""Content is implemented by various types like Article, Video, and Topic."""
|
||||
|
||||
+4
-3
@@ -50,9 +50,10 @@ type UserQueryInput struct {
|
||||
Email string `json:"email"`
|
||||
Name string `json:"name"`
|
||||
// id looks the user up by ID. It's a great way to look up users.
|
||||
Id testutil.ID `json:"id"`
|
||||
Role Role `json:"role"`
|
||||
Names []string `json:"names"`
|
||||
Id testutil.ID `json:"id"`
|
||||
Role Role `json:"role"`
|
||||
Names []string `json:"names"`
|
||||
HasPokemon testutil.Pokemon `json:"hasPokemon"`
|
||||
}
|
||||
|
||||
func InputObjectQuery(
|
||||
|
||||
+4
-3
@@ -66,9 +66,10 @@ type UserQueryInput struct {
|
||||
Email string `json:"email"`
|
||||
Name string `json:"name"`
|
||||
// id looks the user up by ID. It's a great way to look up users.
|
||||
Id testutil.ID `json:"id"`
|
||||
Role Role `json:"role"`
|
||||
Names []string `json:"names"`
|
||||
Id testutil.ID `json:"id"`
|
||||
Role Role `json:"role"`
|
||||
Names []string `json:"names"`
|
||||
HasPokemon testutil.Pokemon `json:"hasPokemon"`
|
||||
}
|
||||
|
||||
func OmitEmptyQuery(
|
||||
|
||||
+4
-3
@@ -73,9 +73,10 @@ type UserQueryInput struct {
|
||||
Email *string `json:"email"`
|
||||
Name *string `json:"name"`
|
||||
// id looks the user up by ID. It's a great way to look up users.
|
||||
Id *testutil.ID `json:"id"`
|
||||
Role *Role `json:"role"`
|
||||
Names []*string `json:"names"`
|
||||
Id *testutil.ID `json:"id"`
|
||||
Role *Role `json:"role"`
|
||||
Names []*string `json:"names"`
|
||||
HasPokemon *testutil.Pokemon `json:"hasPokemon"`
|
||||
}
|
||||
|
||||
func PointersQuery(
|
||||
|
||||
+4
-3
@@ -73,9 +73,10 @@ type UserQueryInput struct {
|
||||
Email string `json:"email"`
|
||||
Name string `json:"name"`
|
||||
// id looks the user up by ID. It's a great way to look up users.
|
||||
Id testutil.ID `json:"id"`
|
||||
Role Role `json:"role"`
|
||||
Names []string `json:"names"`
|
||||
Id testutil.ID `json:"id"`
|
||||
Role Role `json:"role"`
|
||||
Names []string `json:"names"`
|
||||
HasPokemon testutil.Pokemon `json:"hasPokemon"`
|
||||
}
|
||||
|
||||
func PointersQuery(
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
package test
|
||||
|
||||
// Code generated by github.com/Khan/genqlient, DO NOT EDIT.
|
||||
|
||||
import (
|
||||
"github.com/Khan/genqlient/graphql"
|
||||
"github.com/Khan/genqlient/internal/testutil"
|
||||
)
|
||||
|
||||
// GetPokemonSiblingsResponse is returned by GetPokemonSiblings on success.
|
||||
type GetPokemonSiblingsResponse struct {
|
||||
// user looks up a user by some stuff.
|
||||
//
|
||||
// See UserQueryInput for what stuff is supported.
|
||||
// If query is null, returns the current user.
|
||||
User GetPokemonSiblingsUser `json:"user"`
|
||||
}
|
||||
|
||||
// GetPokemonSiblingsUser includes the requested fields of the GraphQL type User.
|
||||
// The GraphQL type's documentation follows.
|
||||
//
|
||||
// A User is a user!
|
||||
type GetPokemonSiblingsUser struct {
|
||||
// id is the user's ID.
|
||||
//
|
||||
// It is stable, unique, and opaque, like all good IDs.
|
||||
Id string `json:"id"`
|
||||
Roles []string `json:"roles"`
|
||||
Name string `json:"name"`
|
||||
Pokemon []testutil.Pokemon `json:"pokemon"`
|
||||
GenqlientPokemon []GetPokemonSiblingsUserGenqlientPokemon `json:"genqlientPokemon"`
|
||||
}
|
||||
|
||||
// GetPokemonSiblingsUserGenqlientPokemon includes the requested fields of the GraphQL type Pokemon.
|
||||
type GetPokemonSiblingsUserGenqlientPokemon struct {
|
||||
Species string `json:"species"`
|
||||
Level int `json:"level"`
|
||||
}
|
||||
|
||||
func GetPokemonSiblings(
|
||||
client graphql.Client,
|
||||
input testutil.Pokemon,
|
||||
) (*GetPokemonSiblingsResponse, error) {
|
||||
variables := map[string]interface{}{
|
||||
"input": input,
|
||||
}
|
||||
|
||||
var retval GetPokemonSiblingsResponse
|
||||
err := client.MakeRequest(
|
||||
nil,
|
||||
"GetPokemonSiblings",
|
||||
`
|
||||
query GetPokemonSiblings ($input: PokemonInput!) {
|
||||
user(query: {hasPokemon:$input}) {
|
||||
id
|
||||
roles
|
||||
name
|
||||
pokemon {
|
||||
species
|
||||
level
|
||||
}
|
||||
genqlientPokemon: pokemon {
|
||||
species
|
||||
level
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
&retval,
|
||||
variables,
|
||||
)
|
||||
return &retval, err
|
||||
}
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"operations": [
|
||||
{
|
||||
"operationName": "GetPokemonSiblings",
|
||||
"query": "\nquery GetPokemonSiblings ($input: PokemonInput!) {\n\tuser(query: {hasPokemon:$input}) {\n\t\tid\n\t\troles\n\t\tname\n\t\tpokemon {\n\t\t\tspecies\n\t\t\tlevel\n\t\t}\n\t\tgenqlientPokemon: pokemon {\n\t\t\tspecies\n\t\t\tlevel\n\t\t}\n\t}\n}\n",
|
||||
"sourceLocation": "testdata/queries/Pokemon.graphql"
|
||||
}
|
||||
]
|
||||
}
|
||||
+4
-3
@@ -50,9 +50,10 @@ type userQueryInput struct {
|
||||
Email string `json:"email"`
|
||||
Name string `json:"name"`
|
||||
// id looks the user up by ID. It's a great way to look up users.
|
||||
Id testutil.ID `json:"id"`
|
||||
Role Role `json:"role"`
|
||||
Names []string `json:"names"`
|
||||
Id testutil.ID `json:"id"`
|
||||
Role Role `json:"role"`
|
||||
Names []string `json:"names"`
|
||||
HasPokemon testutil.Pokemon `json:"hasPokemon"`
|
||||
}
|
||||
|
||||
func unexported(
|
||||
|
||||
@@ -1 +1 @@
|
||||
testdata/errors/UnknownScalar.schema.graphql:3: unknown scalar UnknownScalar: please add it to genqlient.yaml
|
||||
testdata/errors/UnknownScalar.schema.graphql:3: unknown scalar UnknownScalar: please add it to "bindings" in genqlient.yaml
|
||||
|
||||
@@ -1 +1 @@
|
||||
testdata/errors/UnknownScalar.schema.graphql:3: unknown scalar UnknownScalar: please add it to genqlient.yaml
|
||||
testdata/errors/UnknownScalar.schema.graphql:3: unknown scalar UnknownScalar: please add it to "bindings" in genqlient.yaml
|
||||
|
||||
Reference in New Issue
Block a user