add option to export all operations
This commit is contained in:
@@ -49,7 +49,7 @@ fmt.Println("you are", viewerResp.Viewer.MyName)
|
|||||||
//go:generate go run github.com/Khan/genqlient
|
//go:generate go run github.com/Khan/genqlient
|
||||||
```
|
```
|
||||||
|
|
||||||
For a complete working example, see `example/`.
|
For a complete working example, see `example/`. For configuration options, see `go doc github.com/Khan/genqlient/generate.Config`.
|
||||||
|
|
||||||
TODO: document this a bit more, including different ways to specify queries, options once we have those, etc.
|
TODO: document this a bit more, including different ways to specify queries, options once we have those, etc.
|
||||||
|
|
||||||
|
|||||||
+24
-3
@@ -18,27 +18,48 @@ var defaultConfig = &Config{
|
|||||||
}
|
}
|
||||||
|
|
||||||
type Config struct {
|
type Config struct {
|
||||||
// The package name for the output code; defaults to the directory name of
|
|
||||||
// Generated
|
|
||||||
Package string `yaml:"package"`
|
|
||||||
// The filename with the GraphQL schema (in SDL format); defaults to
|
// The filename with the GraphQL schema (in SDL format); defaults to
|
||||||
// schema.graphql
|
// schema.graphql
|
||||||
// TODO: Allow fetching a schema via introspection (will need to figure out
|
// TODO: Allow fetching a schema via introspection (will need to figure out
|
||||||
// how to convert that to SDL).
|
// how to convert that to SDL).
|
||||||
Schema string `yaml:"schema"`
|
Schema string `yaml:"schema"`
|
||||||
|
|
||||||
// Filenames or globs with the queries; defaults to queries.graphql.
|
// Filenames or globs with the queries; defaults to queries.graphql.
|
||||||
//
|
//
|
||||||
// These may be .graphql files, containing the queries in SDL format, or
|
// These may be .graphql files, containing the queries in SDL format, or
|
||||||
// Go files, in which case any string-literal starting with (optional
|
// Go files, in which case any string-literal starting with (optional
|
||||||
// whitespace and) the string "# @genqlient" will be extracted as a query.
|
// whitespace and) the string "# @genqlient" will be extracted as a query.
|
||||||
Queries []string `yaml:"queries"`
|
Queries []string `yaml:"queries"`
|
||||||
|
|
||||||
|
// If set, a file at this path will be generated containing the exact
|
||||||
|
// operations that genqlient will send to the server.
|
||||||
|
//
|
||||||
|
// This is useful for systems which require queries to be explicitly
|
||||||
|
// safelisted, especially for cases like queries involving fragments where
|
||||||
|
// it may not exactly match the input queries. The JSON is an object of
|
||||||
|
// the form
|
||||||
|
// {"operations": [{
|
||||||
|
// "operationName": "operationname",
|
||||||
|
// "query": "query operationName { ... }",
|
||||||
|
// }]}
|
||||||
|
// Keys may be added in the future.
|
||||||
|
//
|
||||||
|
// By default, no such file is written.
|
||||||
|
ExportOperations string `yaml:"export_operations"`
|
||||||
|
|
||||||
// The filename to which to write the generated code; defaults to
|
// The filename to which to write the generated code; defaults to
|
||||||
// generated.go
|
// generated.go
|
||||||
Generated string `yaml:"generated"`
|
Generated string `yaml:"generated"`
|
||||||
|
|
||||||
|
// The package name for the output code; defaults to the directory name of
|
||||||
|
// Generated
|
||||||
|
Package string `yaml:"package"`
|
||||||
|
|
||||||
// Set to the fully-qualified name of a type which generated helpers should
|
// Set to the fully-qualified name of a type which generated helpers should
|
||||||
// accept and use as the context.Context for HTTP requests. Defaults to
|
// accept and use as the context.Context for HTTP requests. Defaults to
|
||||||
// context.Context; set to the empty string to omit context entirely.
|
// context.Context; set to the empty string to omit context entirely.
|
||||||
ContextType string `yaml:"context_type"`
|
ContextType string `yaml:"context_type"`
|
||||||
|
|
||||||
// If set, a snippet of Go code to get a *graphql.Client from the context
|
// If set, a snippet of Go code to get a *graphql.Client from the context
|
||||||
// (which will be named ctx). For example, this might do
|
// (which will be named ctx). For example, this might do
|
||||||
// ctx.Value(myKey).(*graphql.Client). If omitted, client must be
|
// ctx.Value(myKey).(*graphql.Client). If omitted, client must be
|
||||||
|
|||||||
@@ -21,19 +21,21 @@ func TestGenerateExample(t *testing.T) {
|
|||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
code, err := Generate(config)
|
generated, err := Generate(config)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
expectedCode, err := ioutil.ReadFile(config.Generated)
|
for filename, content := range generated {
|
||||||
if err != nil {
|
expectedContent, err := ioutil.ReadFile(filename)
|
||||||
t.Fatal(err)
|
if err != nil {
|
||||||
}
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
if !bytes.Equal(code, expectedCode) {
|
if !bytes.Equal(content, expectedContent) {
|
||||||
t.Errorf(
|
t.Errorf(
|
||||||
"diffs to generated code:\n---actual---\n%v\n---expected---\n%v",
|
"diffs to %v:\n---actual---\n%v\n---expected---\n%v",
|
||||||
string(code), string(expectedCode))
|
filename, string(content), string(expectedContent))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+27
-8
@@ -2,6 +2,7 @@ package generate
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"go/format"
|
"go/format"
|
||||||
"sort"
|
"sort"
|
||||||
@@ -26,19 +27,24 @@ type generator struct {
|
|||||||
schema *ast.Schema
|
schema *ast.Schema
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// JSON tags in operation are for ExportOperations (see Config for details).
|
||||||
type operation struct {
|
type operation struct {
|
||||||
// The type of the operation (query, mutation, or subscription).
|
// The type of the operation (query, mutation, or subscription).
|
||||||
Type ast.Operation
|
Type ast.Operation `json:"-"`
|
||||||
// The name of the operation, from GraphQL.
|
// The name of the operation, from GraphQL.
|
||||||
Name string
|
Name string `json:"operationName"`
|
||||||
// The documentation for the operation, from GraphQL.
|
// The documentation for the operation, from GraphQL.
|
||||||
Doc string
|
Doc string `json:"-"`
|
||||||
// The body of the operation to send.
|
// The body of the operation to send.
|
||||||
Body string
|
Body string `json:"query"`
|
||||||
// The arguments to the operation.
|
// The arguments to the operation.
|
||||||
Args []argument
|
Args []argument `json:"-"`
|
||||||
// The type-name for the operation's response type.
|
// The type-name for the operation's response type.
|
||||||
ResponseName string
|
ResponseName string `json:"-"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type exportedOperations struct {
|
||||||
|
Operations []operation `json:"operations"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type argument struct {
|
type argument struct {
|
||||||
@@ -143,7 +149,8 @@ func (g *generator) addOperation(op *ast.OperationDefinition) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func Generate(config *Config) ([]byte, error) {
|
// Generate returns a map from absolute-path filename to generated content.
|
||||||
|
func Generate(config *Config) (map[string][]byte, error) {
|
||||||
schema, err := getSchema(config.Schema)
|
schema, err := getSchema(config.Schema)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -181,5 +188,17 @@ func Generate(config *Config) ([]byte, error) {
|
|||||||
err, string(unformatted))
|
err, string(unformatted))
|
||||||
}
|
}
|
||||||
|
|
||||||
return formatted, nil
|
retval := map[string][]byte{
|
||||||
|
config.Generated: formatted,
|
||||||
|
}
|
||||||
|
|
||||||
|
if config.ExportOperations != "" {
|
||||||
|
retval[config.ExportOperations], err = json.Marshal(
|
||||||
|
exportedOperations{Operations: g.Operations})
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("unable to export queries: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return retval, nil
|
||||||
}
|
}
|
||||||
|
|||||||
+33
-19
@@ -25,11 +25,11 @@ func readFile(t *testing.T, filename string, allowNotExist bool) string {
|
|||||||
return string(data)
|
return string(data)
|
||||||
}
|
}
|
||||||
|
|
||||||
func gofmt(src string) (string, error) {
|
func gofmt(filename, src string) (string, error) {
|
||||||
src = strings.TrimSpace(src)
|
src = strings.TrimSpace(src)
|
||||||
formatted, err := format.Source([]byte(src))
|
formatted, err := format.Source([]byte(src))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return src, fmt.Errorf("go parse error: %w", err)
|
return src, fmt.Errorf("go parse error in %v: %w", filename, err)
|
||||||
}
|
}
|
||||||
return string(formatted), nil
|
return string(formatted), nil
|
||||||
}
|
}
|
||||||
@@ -60,34 +60,48 @@ func TestGenerate(t *testing.T) {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
goFilename := graphqlFilename + ".go"
|
goFilename := graphqlFilename + ".go"
|
||||||
|
queriesFilename := graphqlFilename + ".json"
|
||||||
|
|
||||||
t.Run(graphqlFilename, func(t *testing.T) {
|
t.Run(graphqlFilename, func(t *testing.T) {
|
||||||
expectedGoCode, err := gofmt(readFile(t, goFilename, update))
|
generated, err := Generate(&Config{
|
||||||
if err != nil {
|
Schema: filepath.Join(dataDir, "schema.graphql"),
|
||||||
t.Fatal(err)
|
Queries: []string{filepath.Join(dataDir, graphqlFilename)},
|
||||||
}
|
Package: "test",
|
||||||
|
Generated: goFilename,
|
||||||
goCode, err := Generate(&Config{
|
ExportOperations: queriesFilename,
|
||||||
Schema: filepath.Join(dataDir, "schema.graphql"),
|
|
||||||
Queries: []string{filepath.Join(dataDir, graphqlFilename)},
|
|
||||||
Package: "test",
|
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if string(goCode) != expectedGoCode {
|
for filename, content := range generated {
|
||||||
t.Errorf("got:\n%v\nwant:\n%v\n", string(goCode), expectedGoCode)
|
expectedContent := readFile(t, filename, update)
|
||||||
if update {
|
if strings.HasSuffix(filename, ".go") {
|
||||||
t.Log("Updating testdata dir to match")
|
fmted, err := gofmt(filename, expectedContent)
|
||||||
err = ioutil.WriteFile(filepath.Join(dataDir, goFilename), goCode, 0o644)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("Unable to update testdata dir: %v", err)
|
// Ignore gofmt errors if we are updating
|
||||||
|
if !update {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
expectedContent = fmted
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// TODO(benkraft): Also check that the code at least builds!
|
if string(content) != expectedContent {
|
||||||
|
t.Errorf("mismatch in %v\ngot:\n%v\nwant:\n%v\n",
|
||||||
|
filename, string(content), expectedContent)
|
||||||
|
if update {
|
||||||
|
t.Log("Updating testdata dir to match")
|
||||||
|
err = ioutil.WriteFile(filepath.Join(dataDir, filename), content, 0o644)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Unable to update testdata dir: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO(benkraft): Also check that the code at least builds!
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+13
-11
@@ -13,22 +13,24 @@ func readConfigGenerateAndWrite(configFilename string) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
code, err := Generate(config)
|
generated, err := Generate(config)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
err = os.MkdirAll(filepath.Dir(config.Generated), 0o755)
|
for filename, content := range generated {
|
||||||
if err != nil {
|
err = os.MkdirAll(filepath.Dir(filename), 0o755)
|
||||||
return fmt.Errorf(
|
if err != nil {
|
||||||
"could not create parent directory for generated file %v: %v",
|
return fmt.Errorf(
|
||||||
config.Generated, err)
|
"could not create parent directory for generated file %v: %v",
|
||||||
}
|
filename, err)
|
||||||
|
}
|
||||||
|
|
||||||
err = ioutil.WriteFile(config.Generated, code, 0o644)
|
err = ioutil.WriteFile(filename, content, 0o644)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("could not write generated file %v: %v",
|
return fmt.Errorf("could not write generated file %v: %v",
|
||||||
config.Generated, err)
|
filename, err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
{"operations":[{"operationName":"InputObjectQuery","query":"\nquery InputObjectQuery ($query: UserQueryInput) {\n\tuser(query: $query) {\n\t\tid\n\t}\n}\n"}]}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{"operations":[{"operationName":"InterfaceNoFragmentsQuery","query":"\nquery InterfaceNoFragmentsQuery {\n\troot {\n\t\tid\n\t\tname\n\t\tchildren {\n\t\t\tid\n\t\t\tname\n\t\t}\n\t}\n}\n"}]}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{"operations":[{"operationName":"ListInputQuery","query":"\nquery ListInputQuery ($names: [String]) {\n\tuser(query: {names:$names}) {\n\t\tid\n\t}\n}\n"}]}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{"operations":[{"operationName":"QueryWithAlias","query":"\nquery QueryWithAlias {\n\tUser: user {\n\t\tID: id\n\t}\n}\n"}]}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{"operations":[{"operationName":"QueryWithDoubleAlias","query":"\nquery QueryWithDoubleAlias {\n\tuser {\n\t\tID: id\n\t\tAlsoID: id\n\t}\n}\n"}]}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{"operations":[{"operationName":"QueryWithEnums","query":"\nquery QueryWithEnums {\n\tuser {\n\t\troles\n\t}\n\totherUser: user {\n\t\troles\n\t}\n}\n"}]}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{"operations":[{"operationName":"QueryWithSlices","query":"\nquery QueryWithSlices {\n\tuser {\n\t\temails\n\t\temailsOrNull\n\t\temailsWithNulls\n\t\temailsWithNullsOrNull\n\t}\n}\n"}]}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{"operations":[{"operationName":"QueryWithStructs","query":"\nquery QueryWithStructs {\n\tuser {\n\t\tauthMethods {\n\t\t\tprovider\n\t\t\temail\n\t\t}\n\t}\n}\n"}]}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{"operations":[{"operationName":"SimpleInputQuery","query":"\nquery SimpleInputQuery ($name: String!) {\n\tuser(query: {name:$name}) {\n\t\tid\n\t}\n}\n"}]}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{"operations":[{"operationName":"SimpleQuery","query":"\nquery SimpleQuery {\n\tuser {\n\t\tid\n\t}\n}\n"}]}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{"operations":[{"operationName":"TypeNameQuery","query":"\nquery TypeNameQuery {\n\tuser {\n\t\t__typename\n\t\tid\n\t}\n}\n"}]}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{"operations":[{"operationName":"UnionNoFragmentsQuery","query":"\nquery UnionNoFragmentsQuery {\n\trandomLeaf {\n\t\t__typename\n\t}\n}\n"}]}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{"operations":[{"operationName":"UsesEnumTwiceQuery","query":"\nquery UsesEnumTwiceQuery {\n\tMe: user {\n\t\troles\n\t}\n\tOtherUser: user {\n\t\troles\n\t}\n}\n"}]}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{"operations":[{"operationName":"unexported","query":"\nquery unexported ($query: UserQueryInput) {\n\tuser(query: $query) {\n\t\tid\n\t}\n}\n"}]}
|
||||||
+1
-1
@@ -71,7 +71,7 @@ func NewClient(endpoint string, httpClient *http.Client) Client {
|
|||||||
|
|
||||||
type payload struct {
|
type payload struct {
|
||||||
Query string `json:"query"`
|
Query string `json:"query"`
|
||||||
Variables map[string]interface{} `json:"variables"`
|
Variables map[string]interface{} `json:"variables,omitempty"`
|
||||||
// OpName is only required if there are multiple queries in the document,
|
// OpName is only required if there are multiple queries in the document,
|
||||||
// but we set it unconditionally, because that's easier.
|
// but we set it unconditionally, because that's easier.
|
||||||
OpName string `json:"operationName"`
|
OpName string `json:"operationName"`
|
||||||
|
|||||||
Reference in New Issue
Block a user