├── .gitattributes ├── go.mod ├── go.sum ├── .gitignore ├── arguments.go ├── examples ├── simple │ └── main.go ├── github-batch-request │ └── main.go ├── playground │ └── main.go └── starwars │ └── main.go ├── .github └── workflows │ └── ci.yaml ├── LICENSE ├── values.go ├── README.md ├── fluentgraphql_test.go └── fluentgraphql.go /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/mergestat/fluentgraphql 2 | 3 | go 1.17 4 | 5 | require github.com/graphql-go/graphql v0.8.0 6 | 7 | require github.com/google/go-cmp v0.5.8 8 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg= 2 | github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 3 | github.com/graphql-go/graphql v0.8.0 h1:JHRQMeQjofwqVvGwYnr8JnPTY0AxgVy1HpHSGPLdH0I= 4 | github.com/graphql-go/graphql v0.8.0/go.mod h1:nKiHzRM0qopJEwCITUuIsxk9PlVlwIiiI8pnJEhordQ= 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # If you prefer the allow list template instead of the deny list, see community template: 2 | # https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore 3 | # 4 | # Binaries for programs and plugins 5 | *.exe 6 | *.exe~ 7 | *.dll 8 | *.so 9 | *.dylib 10 | 11 | # Test binary, built with `go test -c` 12 | *.test 13 | 14 | # Output of the go coverage tool, specifically when used with LiteIDE 15 | *.out 16 | 17 | # Dependency directories (remove the comment below to include it) 18 | # vendor/ 19 | 20 | # Go workspace file 21 | go.work 22 | -------------------------------------------------------------------------------- /arguments.go: -------------------------------------------------------------------------------- 1 | package fluentgraphql 2 | 3 | import ( 4 | "github.com/graphql-go/graphql/language/ast" 5 | ) 6 | 7 | // argument represents an argument to a GraphQL selection 8 | type argument struct { 9 | astArg *ast.Argument 10 | } 11 | 12 | // NewArgument constructs a new argument with a value 13 | func NewArgument(name string, val *Value) *argument { 14 | return &argument{ 15 | astArg: ast.NewArgument(&ast.Argument{ 16 | Name: ast.NewName(&ast.Name{Value: name}), 17 | Value: val.astValue, 18 | }), 19 | } 20 | } 21 | 22 | // WithArguments is a selection option for specifying arguments 23 | func WithArguments(args ...*argument) selectionOption { 24 | return func(s *Selection) { 25 | for _, arg := range args { 26 | switch n := s.node.(type) { 27 | case *ast.Field: 28 | n.Arguments = append(n.Arguments, arg.astArg) 29 | } 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /examples/simple/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | 6 | fgql "github.com/mergestat/fluentgraphql" 7 | ) 8 | 9 | func main() { 10 | a := fgql.NewQuery().Scalar("hello").Root().String() 11 | 12 | /* 13 | { 14 | hello 15 | } 16 | */ 17 | fmt.Println(a) 18 | 19 | b := fgql.NewQuery().Scalar("hello").Scalar("world").Root().String() 20 | 21 | /* 22 | { 23 | hello 24 | world 25 | } 26 | */ 27 | fmt.Println(b) 28 | 29 | c := fgql.NewQuery( 30 | fgql.WithName("MyQuery"), fgql.WithVariableDefinitions(fgql.NewVariableDefinition("myVar", "string", true, nil))). 31 | Scalar("hello", fgql.WithAlias("myAlias"), fgql.WithArguments( 32 | fgql.NewArgument("anArg", fgql.NewVariableValue("myVar")), 33 | )). 34 | Scalar("world"). 35 | Root().String() 36 | 37 | /* 38 | query MyQuery($myVar: string!) { 39 | myAlias: hello(anArg: $myVar) 40 | world 41 | } 42 | */ 43 | fmt.Println(c) 44 | } 45 | -------------------------------------------------------------------------------- /.github/workflows/ci.yaml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: [push, pull_request] 3 | jobs: 4 | build: 5 | name: Build for ${{ matrix.os }} 6 | runs-on: ${{ matrix.os }} 7 | strategy: 8 | matrix: 9 | include: 10 | - os: ubuntu-latest 11 | asset_name: mergestat-linux-amd64 12 | - os: macos-latest 13 | asset_name: mergestat-macos-amd64 14 | 15 | steps: 16 | - name: Set up Go 1.17 17 | uses: actions/setup-go@v1 18 | with: 19 | go-version: 1.17.5 20 | id: go 21 | 22 | - name: Check out source 23 | uses: actions/checkout@v1 24 | 25 | - name: vet 26 | run: go vet -v ./... 27 | 28 | - name: golangci-lint 29 | uses: golangci/golangci-lint-action@v2 30 | 31 | - name: test 32 | run: go test -v ./... -cover -covermode=count -coverprofile=coverage.out 33 | 34 | - name: upload coverage 35 | uses: codecov/codecov-action@v1 36 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Patrick DeVivo 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /examples/github-batch-request/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "fmt" 7 | "io/ioutil" 8 | "log" 9 | "net/http" 10 | "os" 11 | "strings" 12 | 13 | fgql "github.com/mergestat/fluentgraphql" 14 | ) 15 | 16 | var ( 17 | githubToken = os.Getenv("GITHUB_TOKEN") 18 | // repoList taken from here: https://github.com/github/explore/blob/main/collections/front-end-javascript-frameworks/index.md 19 | repoList = []string{ 20 | "marko-js/marko", 21 | "mithriljs/mithril.js", 22 | "angular/angular", 23 | "emberjs/ember.js", 24 | "knockout/knockout", 25 | "tastejs/todomvc", 26 | "spine/spine", 27 | "vuejs/vue", 28 | "Polymer/polymer", 29 | "facebook/react", 30 | "finom/seemple", 31 | "aurelia/framework", 32 | "optimizely/nuclear-js", 33 | "jashkenas/backbone", 34 | "dojo/dojo", 35 | "jorgebucaran/hyperapp", 36 | "riot/riot", 37 | "daemonite/material", 38 | "polymer/lit-element", 39 | "aurelia/aurelia", 40 | "sveltejs/svelte", 41 | "neomjs/neo", 42 | "preactjs/preact", 43 | } 44 | ) 45 | 46 | func main() { 47 | q := fgql.NewQuery() 48 | 49 | // iterate over the list of repos and add a selection to the query for each one 50 | for i, repo := range repoList { 51 | split := strings.Split(repo, "/") 52 | owner := split[0] 53 | name := split[1] 54 | 55 | q.Selection("repository", fgql.WithAlias(fmt.Sprintf("repo_%d", i)), fgql.WithArguments( 56 | fgql.NewArgument("owner", fgql.NewStringValue(owner)), 57 | fgql.NewArgument("name", fgql.NewStringValue(name)), 58 | )). 59 | Selection("owner").Scalar("login").Parent(). 60 | Scalar("name").Scalar("stargazerCount") 61 | } 62 | 63 | fmt.Println(q.Root().String()) 64 | 65 | body := map[string]interface{}{ 66 | "query": q.Root().String(), 67 | } 68 | 69 | b, err := json.Marshal(body) 70 | if err != nil { 71 | log.Fatal(err) 72 | } 73 | 74 | req, err := http.NewRequest(http.MethodPost, "https://api.github.com/graphql", bytes.NewReader(b)) 75 | if err != nil { 76 | log.Fatal(err) 77 | } 78 | req.Header.Set("Authorization", fmt.Sprintf("bearer %s", githubToken)) 79 | 80 | res, err := http.DefaultClient.Do(req) 81 | if err != nil { 82 | log.Fatal(err) 83 | } 84 | 85 | resBody, err := ioutil.ReadAll(res.Body) 86 | if err != nil { 87 | log.Fatal(err) 88 | } 89 | 90 | fmt.Println(string(resBody)) 91 | } 92 | -------------------------------------------------------------------------------- /values.go: -------------------------------------------------------------------------------- 1 | package fluentgraphql 2 | 3 | import ( 4 | "fmt" 5 | "strconv" 6 | 7 | "github.com/graphql-go/graphql/language/ast" 8 | ) 9 | 10 | // Value represents a GraphQL value 11 | type Value struct { 12 | astValue ast.Value 13 | } 14 | 15 | // NewIntValue returns an integer value 16 | func NewIntValue(val int) *Value { 17 | return &Value{ 18 | astValue: ast.NewIntValue(&ast.IntValue{ 19 | Value: strconv.Itoa(val), 20 | }), 21 | } 22 | } 23 | 24 | // NewFloatValue returns a float value 25 | func NewFloatValue(val float64) *Value { 26 | return &Value{ 27 | astValue: ast.NewFloatValue(&ast.FloatValue{ 28 | Value: fmt.Sprintf("%v", val), 29 | }), 30 | } 31 | } 32 | 33 | // NewStringValue returns a string value 34 | func NewStringValue(val string) *Value { 35 | return &Value{ 36 | astValue: ast.NewStringValue(&ast.StringValue{ 37 | Value: val, 38 | }), 39 | } 40 | } 41 | 42 | // NewBooleanValue returns a boolean value 43 | func NewBooleanValue(val bool) *Value { 44 | return &Value{ 45 | astValue: ast.NewBooleanValue(&ast.BooleanValue{ 46 | Value: val, 47 | }), 48 | } 49 | } 50 | 51 | // NewEnumValue returns an enum value 52 | func NewEnumValue(val string) *Value { 53 | return &Value{ 54 | astValue: ast.NewEnumValue(&ast.EnumValue{ 55 | Value: val, 56 | }), 57 | } 58 | } 59 | 60 | // NewListValue returns a list value 61 | func NewListValue(values ...*Value) *Value { 62 | vals := make([]ast.Value, 0, len(values)) 63 | for _, v := range values { 64 | vals = append(vals, v.astValue) 65 | } 66 | 67 | return &Value{ 68 | astValue: ast.NewListValue(&ast.ListValue{ 69 | Values: vals, 70 | }), 71 | } 72 | } 73 | 74 | type objectValueField struct { 75 | fieldName string 76 | value *Value 77 | } 78 | 79 | // NewObjectValueField returns a field for an object value 80 | func NewObjectValueField(fieldName string, value *Value) *objectValueField { 81 | return &objectValueField{ 82 | fieldName: fieldName, 83 | value: value, 84 | } 85 | } 86 | 87 | // NewObjectValue returns an object value 88 | func NewObjectValue(values ...*objectValueField) *Value { 89 | fields := make([]*ast.ObjectField, 0, len(values)) 90 | 91 | for _, f := range values { 92 | fields = append(fields, ast.NewObjectField(&ast.ObjectField{ 93 | Name: ast.NewName(&ast.Name{Value: f.fieldName}), 94 | Value: f.value.astValue, 95 | })) 96 | } 97 | 98 | return &Value{ 99 | astValue: ast.NewObjectValue(&ast.ObjectValue{ 100 | Fields: fields, 101 | }), 102 | } 103 | } 104 | 105 | // NewVariableValue returns a variable value 106 | func NewVariableValue(name string) *Value { 107 | return &Value{ 108 | astValue: ast.NewVariable(&ast.Variable{ 109 | Name: ast.NewName(&ast.Name{Value: name}), 110 | }), 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /examples/playground/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/graphql-go/graphql/language/ast" 7 | "github.com/graphql-go/graphql/language/printer" 8 | fgql "github.com/mergestat/fluentgraphql" 9 | ) 10 | 11 | func main() { 12 | s := fgql.NewQuery(fgql.WithName("MyQuery"), fgql.WithVariableDefinitions( 13 | fgql.NewVariableDefinition("var1", "string", false, fgql.NewBooleanValue(true)), 14 | )). 15 | Scalar("hello", fgql.WithAlias("alias")). 16 | Scalar("world"). 17 | Selection("object", fgql.WithArguments( 18 | fgql.NewArgument("int", fgql.NewIntValue(123)), 19 | fgql.NewArgument("float", fgql.NewFloatValue(123.102434)), 20 | fgql.NewArgument("string", fgql.NewStringValue("string-arg")), 21 | fgql.NewArgument("boolean", fgql.NewBooleanValue(false)), 22 | fgql.NewArgument("enum", fgql.NewEnumValue("SOME_ENUM")), 23 | fgql.NewArgument("list", fgql.NewListValue( 24 | fgql.NewStringValue("str1"), 25 | fgql.NewStringValue("str2"), 26 | fgql.NewStringValue("str3"), 27 | )), 28 | fgql.NewArgument("object", fgql.NewObjectValue( 29 | fgql.NewObjectValueField("field1", fgql.NewStringValue("str1")), 30 | fgql.NewObjectValueField("field1", fgql.NewIntValue(123)), 31 | )), 32 | fgql.NewArgument("var", fgql.NewVariableValue("someVar")), 33 | )). 34 | Scalar("patrick").Parent(). 35 | Selection("world").Scalar("hello").InlineFragment("User").Scalar("fragmentField").Selection("patrick").Scalar("devivo"). 36 | Root().Fragment("comparisonFields", "Character").Scalar("name").Selection("friendsConnection", fgql.WithArguments( 37 | fgql.NewArgument("first", fgql.NewVariableValue("first")), 38 | )).Scalar("totalCount").Selection("edges").Selection("node").Scalar("name"). 39 | Root().String() 40 | 41 | fmt.Println(s) 42 | 43 | a := ast.NewOperationDefinition(&ast.OperationDefinition{ 44 | Operation: ast.OperationTypeMutation, 45 | Name: ast.NewName(&ast.Name{Value: ""}), 46 | SelectionSet: ast.NewSelectionSet(&ast.SelectionSet{ 47 | Selections: []ast.Selection{ 48 | ast.NewField(&ast.Field{ 49 | Name: ast.NewName(&ast.Name{Value: "hello"}), 50 | }), 51 | ast.NewField(&ast.Field{ 52 | Name: ast.NewName(&ast.Name{Value: "world"}), 53 | Arguments: []*ast.Argument{ 54 | ast.NewArgument(&ast.Argument{ 55 | Name: ast.NewName(&ast.Name{Value: "arg1"}), 56 | Value: ast.NewIntValue(&ast.IntValue{Value: "123"}), 57 | }), 58 | }, 59 | }), 60 | ast.NewField(&ast.Field{ 61 | Name: ast.NewName(&ast.Name{Value: "author"}), 62 | SelectionSet: ast.NewSelectionSet(&ast.SelectionSet{ 63 | Selections: []ast.Selection{ 64 | ast.NewInlineFragment((&ast.InlineFragment{ 65 | TypeCondition: ast.NewNamed(&ast.Named{Name: ast.NewName(&ast.Name{Value: "User"})}), 66 | SelectionSet: ast.NewSelectionSet(&ast.SelectionSet{ 67 | Selections: []ast.Selection{ 68 | ast.NewField(&ast.Field{ 69 | Name: ast.NewName(&ast.Name{Value: "userField"}), 70 | }), 71 | }, 72 | }), 73 | })), 74 | }, 75 | }), 76 | }), 77 | }, 78 | }), 79 | }) 80 | 81 | // printer.Print(a) 82 | fmt.Println(printer.Print(a)) 83 | 84 | } 85 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Go Reference](https://pkg.go.dev/badge/github.com/mergestat/fluentgraphql.svg)](https://pkg.go.dev/github.com/mergestat/fluentgraphql) 2 | [![Go Report Card](https://goreportcard.com/badge/github.com/mergestat/fluentgraphql)](https://goreportcard.com/report/github.com/mergestat/fluentgraphql) 3 | [![codecov](https://codecov.io/gh/mergestat/fluentgraphql/branch/main/graph/badge.svg?token=1HTURZGWIL)](https://codecov.io/gh/mergestat/fluentgraphql) 4 | 5 | # fluentgraphql 6 | 7 | This package wraps the [graphql-go/graphql](https://github.com/graphql-go/graphql) implementation to provide a "fluent" pattern for constructing GraphQL queries in Go. 8 | This can be valuable in situations where *dynamic* queries are desired: when the fields of a GraphQL query (or mutation) are not known until runtime. 9 | For most other use cases, plain query strings or a helper library such as [this](https://github.com/shurcooL/graphql) should be sufficient. 10 | 11 | ```golang 12 | package main 13 | 14 | import ( 15 | fgql "github.com/mergestat/fluentgraphql" 16 | ) 17 | 18 | func main() { 19 | fgql.NewQuery().Scalar("hello").Root().String() // { hello } 20 | } 21 | ``` 22 | 23 | ## Basic Usage 24 | 25 | `go get github.com/mergestat/fluentgraphql` 26 | 27 | ```golang 28 | import ( 29 | fgql "github.com/mergestat/fluentgraphql" 30 | ) 31 | ``` 32 | 33 | The package name is `fluentgraphql`, but we alias it here to `fgql` which is more concise. 34 | A query or mutation is started like so: 35 | 36 | ```golang 37 | q := fgql.NewQuery() // a new query builder 38 | m := fgql.NewMutation() // a new mutation builder 39 | ``` 40 | 41 | A query can be constructed with calls to builder methods, such as in the following example. 42 | See [this file](https://github.com/mergestat/fluentgraphql/blob/main/examples/starwars/main.go) for more thorough examples. 43 | 44 | ```golang 45 | /* 46 | query HeroComparison($first: Int = 3) { 47 | leftComparison: hero(episode: EMPIRE) { 48 | ...comparisonFields 49 | } 50 | rightComparison: hero(episode: JEDI) { 51 | ...comparisonFields 52 | } 53 | } 54 | 55 | fragment comparisonFields on Character { 56 | name 57 | friendsConnection(first: $first) { 58 | totalCount 59 | edges { 60 | node { 61 | name 62 | } 63 | } 64 | } 65 | } 66 | */ 67 | q = fgql.NewQuery( 68 | fgql.WithName("HeroComparison"), 69 | fgql.WithVariableDefinitions( 70 | fgql.NewVariableDefinition("first", "Int", false, fgql.NewIntValue(3)), 71 | ), 72 | ). 73 | Selection("hero", 74 | fgql.WithAlias("leftComparison"), 75 | fgql.WithArguments(fgql.NewArgument("episode", fgql.NewEnumValue("EMPIRE"))), 76 | ).FragmentSpread("comparisonFields"). 77 | Parent(). 78 | Selection("hero", 79 | fgql.WithAlias("rightComparison"), 80 | fgql.WithArguments(fgql.NewArgument("episode", fgql.NewEnumValue("JEDI"))), 81 | ).FragmentSpread("comparisonFields"). 82 | Root(). 83 | Fragment("comparisonFields", "Character"). 84 | Scalar("name"). 85 | Selection("friendsConnection", fgql.WithArguments(fgql.NewArgument("first", fgql.NewVariableValue("first")))). 86 | Scalar("totalCount"). 87 | Selection("edges").Selection("node").Scalar("name"). 88 | Root().String() 89 | fmt.Println(q) 90 | ``` 91 | Note the call to `.Root().String()`. 92 | `Root()` traverses the builder tree back to the root, so that when `String()` is called, the *entire* query is printed as a string. 93 | 94 | ### Batching Requests 95 | A use case where a fluent interface is valuable is when dynamically generating a "batch" of queries to make to a GraphQL API. 96 | For instance, in the [`github-batch-request` example](https://github.com/mergestat/fluentgraphql/blob/main/examples/github-batch-request/main.go), we can build a query that retrieves the `stargazerCount` field of multiple, arbitrary repositories at once. 97 | This allows us to batch multiple lookups into a single HTTP request, avoiding multiple round-trip requests. 98 | 99 | ```golang 100 | q := fgql.NewQuery() 101 | 102 | // iterate over the list of repos and add a selection to the query for each one 103 | for i, repo := range repoList { 104 | split := strings.Split(repo, "/") 105 | owner := split[0] 106 | name := split[1] 107 | 108 | q.Selection("repository", fgql.WithAlias(fmt.Sprintf("repo_%d", i)), fgql.WithArguments( 109 | fgql.NewArgument("owner", fgql.NewStringValue(owner)), 110 | fgql.NewArgument("name", fgql.NewStringValue(name)), 111 | )). 112 | Selection("owner").Scalar("login").Parent(). 113 | Scalar("name").Scalar("stargazerCount") 114 | } 115 | ``` 116 | 117 | Produces a query that looks something like: 118 | 119 | ```graphql 120 | { 121 | repo_0: repository(owner: "marko-js", name: "marko") { 122 | owner { 123 | login 124 | } 125 | name 126 | stargazerCount 127 | } 128 | repo_1: repository(owner: "mithriljs", name: "mithril.js") { 129 | owner { 130 | login 131 | } 132 | name 133 | stargazerCount 134 | } 135 | repo_2: repository(owner: "angular", name: "angular") { 136 | owner { 137 | login 138 | } 139 | name 140 | stargazerCount 141 | } 142 | ... 143 | } 144 | ``` 145 | -------------------------------------------------------------------------------- /fluentgraphql_test.go: -------------------------------------------------------------------------------- 1 | package fluentgraphql 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/google/go-cmp/cmp" 7 | "github.com/graphql-go/graphql/language/ast" 8 | "github.com/graphql-go/graphql/language/parser" 9 | ) 10 | 11 | func queryMatchesTree(t *testing.T, query string, root ast.Node) string { 12 | t.Helper() 13 | opts := parser.ParseOptions{ 14 | NoSource: true, 15 | NoLocation: true, 16 | } 17 | params := parser.ParseParams{ 18 | Source: query, 19 | Options: opts, 20 | } 21 | expectedDocument, err := parser.Parse(params) 22 | if err != nil { 23 | t.Fatalf("unexpected error: %v", err) 24 | } 25 | 26 | document := ast.NewDocument(&ast.Document{ 27 | Definitions: []ast.Node{root}, 28 | }) 29 | 30 | return cmp.Diff(expectedDocument, document) 31 | } 32 | 33 | func TestSelections(t *testing.T) { 34 | for name, testCase := range map[string]struct { 35 | wanted string 36 | selection *Selection 37 | }{ 38 | "SingleScalar": { 39 | wanted: `{ hello }`, 40 | selection: NewQuery().Scalar("hello"), 41 | }, 42 | "SingleScalarWithIntArgument": { 43 | wanted: `{ hello(arg1: 123) }`, 44 | selection: NewQuery().Scalar("hello", WithArguments(NewArgument("arg1", NewIntValue(123)))), 45 | }, 46 | "SingleScalarWithFloatArgument": { 47 | wanted: `{ hello(arg1: 123.12) }`, 48 | selection: NewQuery().Scalar("hello", WithArguments(NewArgument("arg1", NewFloatValue(123.12)))), 49 | }, 50 | "SingleScalarWithStringArgument": { 51 | wanted: `{ hello(arg1: "string") }`, 52 | selection: NewQuery().Scalar("hello", WithArguments(NewArgument("arg1", NewStringValue("string")))), 53 | }, 54 | "SingleScalarWithBooleanArgument": { 55 | wanted: `{ hello(arg1: true) }`, 56 | selection: NewQuery().Scalar("hello", WithArguments(NewArgument("arg1", NewBooleanValue(true)))), 57 | }, 58 | "SingleScalarWithEnumArgument": { 59 | wanted: `{ hello(arg1: SOME_ENUM) }`, 60 | selection: NewQuery().Scalar("hello", WithArguments(NewArgument("arg1", NewEnumValue("SOME_ENUM")))), 61 | }, 62 | "SingleScalarWithListArgument": { 63 | wanted: `{ hello(arg1: ["some-string", "some-other-string"]) }`, 64 | selection: NewQuery().Scalar("hello", WithArguments(NewArgument("arg1", NewListValue(NewStringValue("some-string"), NewStringValue("some-other-string"))))), 65 | }, 66 | "SingleScalarWithObjectArgument": { 67 | wanted: `{ hello(arg1: { val1: "a", val2: "b"}) }`, 68 | selection: NewQuery().Scalar("hello", WithArguments(NewArgument("arg1", NewObjectValue( 69 | NewObjectValueField("val1", NewStringValue("a")), 70 | NewObjectValueField("val2", NewStringValue("b")), 71 | )))), 72 | }, 73 | "SingleScalarWithVariableArgument": { 74 | wanted: `{ hello(arg1: $var1) }`, 75 | selection: NewQuery().Scalar("hello", WithArguments(NewArgument("arg1", NewVariableValue("var1")))), 76 | }, 77 | "SingleScalarWithQueryName": { 78 | wanted: `query SomeName { hello }`, 79 | selection: NewQuery(WithName("SomeName")).Scalar("hello"), 80 | }, 81 | "SingleScalarWithAlias": { 82 | wanted: `{ someAlias: hello }`, 83 | selection: NewQuery().Scalar("hello", WithAlias("someAlias")), 84 | }, 85 | "SingleScalarWithVariable": { 86 | wanted: `query($a: string) { hello }`, 87 | selection: NewQuery(WithVariableDefinitions(NewVariableDefinition("a", "string", false, nil))).Scalar("hello"), 88 | }, 89 | "SingleScalarWithRequiredVariable": { 90 | wanted: `query($a: string!) { hello }`, 91 | selection: NewQuery(WithVariableDefinitions(NewVariableDefinition("a", "string", true, nil))).Scalar("hello"), 92 | }, 93 | "SingleScalarWithVariableAndName": { 94 | wanted: `query NamedQuery($a: string) { hello }`, 95 | selection: NewQuery(WithName("NamedQuery"), WithVariableDefinitions(NewVariableDefinition("a", "string", false, nil))).Scalar("hello"), 96 | }, 97 | "SubSelection": { 98 | wanted: `{ hello { world } }`, 99 | selection: NewQuery().Selection("hello").Scalar("world"), 100 | }, 101 | } { 102 | t.Run(name, func(t *testing.T) { 103 | root := testCase.selection.Root() 104 | if diff := queryMatchesTree(t, testCase.wanted, root.node); diff != "" { 105 | t.Log("produced GraphQL query does not match what's wanted", diff) 106 | t.Fatal() 107 | } 108 | }) 109 | } 110 | } 111 | 112 | func TestMutations(t *testing.T) { 113 | for name, testCase := range map[string]struct { 114 | wanted string 115 | selection *Selection 116 | }{ 117 | "SingleScalar": { 118 | wanted: `mutation { hello }`, 119 | selection: NewMutation().Scalar("hello"), 120 | }, 121 | "SingleScalarWithIntArgument": { 122 | wanted: `mutation { hello(arg1: 123) }`, 123 | selection: NewMutation().Scalar("hello", WithArguments(NewArgument("arg1", NewIntValue(123)))), 124 | }, 125 | "SingleScalarWithFloatArgument": { 126 | wanted: `mutation { hello(arg1: 123.12) }`, 127 | selection: NewMutation().Scalar("hello", WithArguments(NewArgument("arg1", NewFloatValue(123.12)))), 128 | }, 129 | "SingleScalarWithStringArgument": { 130 | wanted: `mutation { hello(arg1: "string") }`, 131 | selection: NewMutation().Scalar("hello", WithArguments(NewArgument("arg1", NewStringValue("string")))), 132 | }, 133 | "SingleScalarWithBooleanArgument": { 134 | wanted: `mutation { hello(arg1: true) }`, 135 | selection: NewMutation().Scalar("hello", WithArguments(NewArgument("arg1", NewBooleanValue(true)))), 136 | }, 137 | "SingleScalarWithEnumArgument": { 138 | wanted: `mutation { hello(arg1: SOME_ENUM) }`, 139 | selection: NewMutation().Scalar("hello", WithArguments(NewArgument("arg1", NewEnumValue("SOME_ENUM")))), 140 | }, 141 | "SingleScalarWithListArgument": { 142 | wanted: `mutation { hello(arg1: ["some-string", "some-other-string"]) }`, 143 | selection: NewMutation().Scalar("hello", WithArguments(NewArgument("arg1", NewListValue(NewStringValue("some-string"), NewStringValue("some-other-string"))))), 144 | }, 145 | "SingleScalarWithObjectArgument": { 146 | wanted: `mutation { hello(arg1: { val1: "a", val2: "b"}) }`, 147 | selection: NewMutation().Scalar("hello", WithArguments(NewArgument("arg1", NewObjectValue( 148 | NewObjectValueField("val1", NewStringValue("a")), 149 | NewObjectValueField("val2", NewStringValue("b")), 150 | )))), 151 | }, 152 | "SingleScalarWithVariableArgument": { 153 | wanted: `mutation { hello(arg1: $var1) }`, 154 | selection: NewMutation().Scalar("hello", WithArguments(NewArgument("arg1", NewVariableValue("var1")))), 155 | }, 156 | "SingleScalarWithQueryName": { 157 | wanted: `mutation SomeName { hello }`, 158 | selection: NewMutation(WithName("SomeName")).Scalar("hello"), 159 | }, 160 | } { 161 | t.Run(name, func(t *testing.T) { 162 | root := testCase.selection.Root() 163 | if diff := queryMatchesTree(t, testCase.wanted, root.node); diff != "" { 164 | t.Log("produced GraphQL query does not match what's wanted", diff) 165 | t.Fatal() 166 | } 167 | }) 168 | } 169 | } 170 | -------------------------------------------------------------------------------- /examples/starwars/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | 6 | fgql "github.com/mergestat/fluentgraphql" 7 | ) 8 | 9 | // implements the queries list on this page: https://graphql.org/learn/queries/ 10 | func main() { 11 | var q string 12 | /* 13 | { 14 | hero { 15 | name 16 | } 17 | } 18 | */ 19 | q = fgql.NewQuery().Selection("hero").Scalar("name"). 20 | Root().String() 21 | fmt.Println(q) 22 | 23 | /* 24 | { 25 | hero { 26 | name 27 | # Queries can have comments! 28 | friends { 29 | name 30 | } 31 | } 32 | } 33 | */ 34 | q = fgql.NewQuery().Selection("hero").Scalar("name").Selection("friends").Scalar("name"). 35 | Root().String() 36 | fmt.Println(q) 37 | 38 | /* 39 | { 40 | human(id: "1000") { 41 | name 42 | height 43 | } 44 | } 45 | */ 46 | q = fgql.NewQuery().Selection("human", fgql.WithArguments(fgql.NewArgument("id", fgql.NewStringValue("1000")))).Scalar("name").Scalar("height"). 47 | Root().String() 48 | fmt.Println(q) 49 | 50 | /* 51 | { 52 | human(id: "1000") { 53 | name 54 | height(unit: FOOT) 55 | } 56 | } 57 | */ 58 | q = fgql.NewQuery(). 59 | Selection("human", 60 | fgql.WithArguments(fgql.NewArgument("id", fgql.NewStringValue("1000"))), 61 | ). 62 | Scalar("name"). 63 | Scalar("height", 64 | fgql.WithArguments(fgql.NewArgument("unit", fgql.NewEnumValue("FOOT"))), 65 | ). 66 | Root().String() 67 | fmt.Println(q) 68 | 69 | /* 70 | { 71 | empireHero: hero(episode: EMPIRE) { 72 | name 73 | } 74 | jediHero: hero(episode: JEDI) { 75 | name 76 | } 77 | } 78 | */ 79 | q = fgql.NewQuery(). 80 | Selection("hero", 81 | fgql.WithAlias("empireHero"), 82 | fgql.WithArguments(fgql.NewArgument("episode", fgql.NewEnumValue("EMPIRE"))), 83 | ).Scalar("name"). 84 | Parent(). // .Parent() moves us "up" one step in the builder tree 85 | Selection("hero", 86 | fgql.WithAlias("jediHero"), 87 | fgql.WithArguments(fgql.NewArgument("episode", fgql.NewEnumValue("JEDI"))), 88 | ).Scalar("name"). 89 | Root().String() 90 | fmt.Println(q) 91 | 92 | /* 93 | { 94 | leftComparison: hero(episode: EMPIRE) { 95 | ...comparisonFields 96 | } 97 | rightComparison: hero(episode: JEDI) { 98 | ...comparisonFields 99 | } 100 | } 101 | 102 | fragment comparisonFields on Character { 103 | name 104 | appearsIn 105 | friends { 106 | name 107 | } 108 | } 109 | */ 110 | q = fgql.NewQuery(). 111 | Selection("hero", 112 | fgql.WithAlias("leftComparison"), 113 | fgql.WithArguments(fgql.NewArgument("episode", fgql.NewEnumValue("EMPIRE"))), 114 | ).FragmentSpread("comparisonFields"). 115 | Parent(). 116 | Selection("hero", 117 | fgql.WithAlias("rightComparison"), 118 | fgql.WithArguments(fgql.NewArgument("episode", fgql.NewEnumValue("JEDI"))), 119 | ).FragmentSpread("comparisonFields"). 120 | Root(). 121 | Fragment("comparisonFields", "Character"). 122 | Scalar("name"). 123 | Scalar("appearsIn"). 124 | Selection("friends").Scalar("name"). 125 | Root().String() 126 | fmt.Println(q) 127 | 128 | /* 129 | query HeroComparison($first: Int = 3) { 130 | leftComparison: hero(episode: EMPIRE) { 131 | ...comparisonFields 132 | } 133 | rightComparison: hero(episode: JEDI) { 134 | ...comparisonFields 135 | } 136 | } 137 | 138 | fragment comparisonFields on Character { 139 | name 140 | friendsConnection(first: $first) { 141 | totalCount 142 | edges { 143 | node { 144 | name 145 | } 146 | } 147 | } 148 | } 149 | */ 150 | q = fgql.NewQuery( 151 | fgql.WithName("HeroComparison"), 152 | fgql.WithVariableDefinitions( 153 | fgql.NewVariableDefinition("first", "Int", false, fgql.NewIntValue(3)), 154 | ), 155 | ). 156 | Selection("hero", 157 | fgql.WithAlias("leftComparison"), 158 | fgql.WithArguments(fgql.NewArgument("episode", fgql.NewEnumValue("EMPIRE"))), 159 | ).FragmentSpread("comparisonFields"). 160 | Parent(). 161 | Selection("hero", 162 | fgql.WithAlias("rightComparison"), 163 | fgql.WithArguments(fgql.NewArgument("episode", fgql.NewEnumValue("JEDI"))), 164 | ).FragmentSpread("comparisonFields"). 165 | Root(). 166 | Fragment("comparisonFields", "Character"). 167 | Scalar("name"). 168 | Selection("friendsConnection", fgql.WithArguments(fgql.NewArgument("first", fgql.NewVariableValue("first")))). 169 | Scalar("totalCount"). 170 | Selection("edges").Selection("node").Scalar("name"). 171 | Root().String() 172 | fmt.Println(q) 173 | 174 | /* 175 | query HeroNameAndFriends { 176 | hero { 177 | name 178 | friends { 179 | name 180 | } 181 | } 182 | } 183 | */ 184 | q = fgql.NewQuery(fgql.WithName("HeroNameAndFriends")). 185 | Selection("hero").Scalar("name"). 186 | Selection("friends").Scalar("name"). 187 | Root().String() 188 | fmt.Println(q) 189 | 190 | /* 191 | query HeroNameAndFriends($episode: Episode) { 192 | hero(episode: $episode) { 193 | name 194 | friends { 195 | name 196 | } 197 | } 198 | } 199 | */ 200 | q = fgql.NewQuery( 201 | fgql.WithName("HeroNameAndFriends"), 202 | fgql.WithVariableDefinitions(fgql.NewVariableDefinition("episode", "Episode", false, nil)), 203 | ). 204 | Selection("hero", fgql.WithArguments(fgql.NewArgument("episode", fgql.NewVariableValue("episode")))). 205 | Scalar("name"). 206 | Selection("friends").Scalar("name"). 207 | Root().String() 208 | fmt.Println(q) 209 | 210 | /* 211 | query HeroNameAndFriends($episode: Episode = JEDI) { 212 | hero(episode: $episode) { 213 | name 214 | friends { 215 | name 216 | } 217 | } 218 | } 219 | */ 220 | q = fgql.NewQuery( 221 | fgql.WithName("HeroNameAndFriends"), 222 | fgql.WithVariableDefinitions(fgql.NewVariableDefinition("episode", "Episode", false, fgql.NewEnumValue("JEDI"))), 223 | ). 224 | Selection("hero", fgql.WithArguments(fgql.NewArgument("episode", fgql.NewVariableValue("episode")))). 225 | Scalar("name"). 226 | Selection("friends").Scalar("name"). 227 | Root().String() 228 | fmt.Println(q) 229 | 230 | /* 231 | query Hero($episode: Episode, $withFriends: Boolean!) { 232 | hero(episode: $episode) { 233 | name 234 | friends @include(if: $withFriends) { 235 | name 236 | } 237 | } 238 | } 239 | */ 240 | // TODO(patrickdevivo) implement when directives are supported 241 | 242 | /* 243 | mutation CreateReviewForEpisode($ep: Episode!, $review: ReviewInput!) { 244 | createReview(episode: $ep, review: $review) { 245 | stars 246 | commentary 247 | } 248 | } 249 | */ 250 | q = fgql.NewMutation( 251 | fgql.WithName("CreateReviewForEpisode"), 252 | fgql.WithVariableDefinitions( 253 | fgql.NewVariableDefinition("ep", "Episode", true, nil), 254 | fgql.NewVariableDefinition("review", "ReviewInput", true, nil), 255 | ), 256 | ). 257 | Selection("createReview", fgql.WithArguments( 258 | fgql.NewArgument("episode", fgql.NewVariableValue("ep")), 259 | fgql.NewArgument("review", fgql.NewVariableValue("review")), 260 | )). 261 | Scalar("stars").Scalar("commentary"). 262 | Root().String() 263 | fmt.Println(q) 264 | 265 | /* 266 | query HeroForEpisode($ep: Episode!) { 267 | hero(episode: $ep) { 268 | name 269 | ... on Droid { 270 | primaryFunction 271 | } 272 | ... on Human { 273 | height 274 | } 275 | } 276 | } 277 | */ 278 | q = fgql.NewQuery( 279 | fgql.WithName("HeroForEpisode"), 280 | fgql.WithVariableDefinitions( 281 | fgql.NewVariableDefinition("ep", "Episode", true, nil), 282 | ), 283 | ). 284 | Selection("hero", fgql.WithArguments( 285 | fgql.NewArgument("episode", fgql.NewVariableValue("ep")), 286 | )). 287 | Scalar("name"). 288 | InlineFragment("Droid").Scalar("primaryFunction"). 289 | Parent(). 290 | InlineFragment("Human").Scalar("height"). 291 | Root().String() 292 | fmt.Println(q) 293 | 294 | /* 295 | { 296 | search(text: "an") { 297 | __typename 298 | ... on Human { 299 | name 300 | } 301 | ... on Droid { 302 | name 303 | } 304 | ... on Starship { 305 | name 306 | } 307 | } 308 | } 309 | */ 310 | q = fgql.NewQuery(). 311 | Selection("search", fgql.WithArguments( 312 | fgql.NewArgument("text", fgql.NewStringValue("an")), 313 | )). 314 | Scalar("__typename"). 315 | InlineFragment("Human").Scalar("name"). 316 | Parent(). 317 | InlineFragment("Droid").Scalar("name"). 318 | Parent(). 319 | InlineFragment("Starship").Scalar("name"). 320 | Root().String() 321 | fmt.Println(q) 322 | } 323 | -------------------------------------------------------------------------------- /fluentgraphql.go: -------------------------------------------------------------------------------- 1 | package fluentgraphql 2 | 3 | import ( 4 | "github.com/graphql-go/graphql/language/ast" 5 | "github.com/graphql-go/graphql/language/printer" 6 | ) 7 | 8 | type Selection struct { 9 | parent *Selection 10 | node ast.Node 11 | } 12 | 13 | // NewQuery returns a selection builder for a new GraphQL query. 14 | // query { ... } 15 | func NewQuery(options ...operationOption) *Selection { 16 | s := &Selection{ 17 | parent: nil, 18 | node: ast.NewOperationDefinition(&ast.OperationDefinition{ 19 | Operation: ast.OperationTypeQuery, 20 | // VariableDefinitions: make([]*ast.VariableDefinition, 0), 21 | Directives: make([]*ast.Directive, 0), 22 | SelectionSet: ast.NewSelectionSet(&ast.SelectionSet{}), 23 | }), 24 | } 25 | for _, option := range options { 26 | option(s) 27 | } 28 | return s 29 | } 30 | 31 | // NewMutation returns a selection builder for a new GraphQL mutation. 32 | // mutation { ... } 33 | func NewMutation(options ...operationOption) *Selection { 34 | s := &Selection{ 35 | parent: nil, 36 | node: ast.NewOperationDefinition(&ast.OperationDefinition{ 37 | Operation: ast.OperationTypeMutation, 38 | VariableDefinitions: make([]*ast.VariableDefinition, 0), 39 | Directives: make([]*ast.Directive, 0), 40 | SelectionSet: ast.NewSelectionSet(&ast.SelectionSet{}), 41 | }), 42 | } 43 | for _, option := range options { 44 | option(s) 45 | } 46 | return s 47 | } 48 | 49 | type operationOption selectionOption 50 | 51 | // WithName specifies a name for the operation 52 | func WithName(name string) operationOption { 53 | return func(s *Selection) { 54 | switch n := s.node.(type) { 55 | case *ast.OperationDefinition: 56 | n.Name = ast.NewName(&ast.Name{ 57 | Value: name, 58 | }) 59 | n.VariableDefinitions = make([]*ast.VariableDefinition, 0) 60 | } 61 | } 62 | } 63 | 64 | type variableDefinition struct { 65 | astVarDef *ast.VariableDefinition 66 | } 67 | 68 | // NewVariableDefinition defines a new variable definition 69 | func NewVariableDefinition(name string, varType string, required bool, defaultVal *Value) *variableDefinition { 70 | varDef := &variableDefinition{ 71 | astVarDef: ast.NewVariableDefinition(&ast.VariableDefinition{ 72 | Variable: NewVariableValue(name).astValue.(*ast.Variable), 73 | }), 74 | } 75 | 76 | if required { 77 | varDef.astVarDef.Type = ast.NewNonNull(&ast.NonNull{ 78 | Type: ast.NewNamed(&ast.Named{ 79 | Name: ast.NewName(&ast.Name{ 80 | Value: varType, 81 | }), 82 | }), 83 | }) 84 | } else { 85 | varDef.astVarDef.Type = ast.NewNamed(&ast.Named{ 86 | Name: ast.NewName(&ast.Name{ 87 | Value: varType, 88 | }), 89 | }) 90 | } 91 | 92 | if defaultVal != nil { 93 | varDef.astVarDef.DefaultValue = defaultVal.astValue 94 | } 95 | 96 | return varDef 97 | } 98 | 99 | // WithVariableDefinitions is an operation option for declaring variable definitions 100 | func WithVariableDefinitions(vars ...*variableDefinition) operationOption { 101 | return func(s *Selection) { 102 | switch n := s.node.(type) { 103 | case *ast.OperationDefinition: 104 | varDefs := make([]*ast.VariableDefinition, 0, len(vars)) 105 | for _, v := range vars { 106 | varDefs = append(varDefs, v.astVarDef) 107 | } 108 | n.VariableDefinitions = varDefs 109 | } 110 | } 111 | } 112 | 113 | // Scalar adds a scalar field to the current selection 114 | func (s *Selection) Scalar(fieldName string, options ...selectionOption) *Selection { 115 | newS := &Selection{ 116 | node: ast.NewField(&ast.Field{ 117 | Name: ast.NewName(&ast.Name{Value: fieldName}), 118 | Arguments: make([]*ast.Argument, 0), 119 | Directives: make([]*ast.Directive, 0), 120 | }), 121 | } 122 | switch n := s.node.(type) { 123 | case *ast.OperationDefinition: 124 | n.SelectionSet.Selections = append(n.SelectionSet.Selections, newS.node.(*ast.Field)) 125 | case *ast.Field: 126 | n.SelectionSet.Selections = append(n.SelectionSet.Selections, newS.node.(*ast.Field)) 127 | case *ast.InlineFragment: 128 | n.SelectionSet.Selections = append(n.SelectionSet.Selections, newS.node.(*ast.Field)) 129 | case *ast.FragmentDefinition: 130 | n.SelectionSet.Selections = append(n.SelectionSet.Selections, newS.node.(*ast.Field)) 131 | } 132 | 133 | for _, option := range options { 134 | option(newS) 135 | } 136 | 137 | return s 138 | } 139 | 140 | // Selection adds a subselection to the current selection 141 | func (s *Selection) Selection(fieldName string, options ...selectionOption) *Selection { 142 | newS := &Selection{ 143 | parent: s, 144 | node: ast.NewField(&ast.Field{ 145 | Name: ast.NewName(&ast.Name{Value: fieldName}), 146 | SelectionSet: ast.NewSelectionSet(&ast.SelectionSet{}), 147 | Arguments: make([]*ast.Argument, 0), 148 | Directives: make([]*ast.Directive, 0), 149 | }), 150 | } 151 | switch n := s.node.(type) { 152 | case *ast.OperationDefinition: 153 | n.SelectionSet.Selections = append(n.SelectionSet.Selections, newS.node.(*ast.Field)) 154 | case *ast.Field: 155 | n.SelectionSet.Selections = append(n.SelectionSet.Selections, newS.node.(*ast.Field)) 156 | case *ast.InlineFragment: 157 | n.SelectionSet.Selections = append(n.SelectionSet.Selections, newS.node.(*ast.Field)) 158 | case *ast.FragmentDefinition: 159 | n.SelectionSet.Selections = append(n.SelectionSet.Selections, newS.node.(*ast.Field)) 160 | } 161 | 162 | for _, option := range options { 163 | option(newS) 164 | } 165 | 166 | return newS 167 | } 168 | 169 | // InlineFragment adds an inline fragment to the current selection 170 | func (s *Selection) InlineFragment(typeCondition string) *Selection { 171 | newFrag := ast.NewInlineFragment((&ast.InlineFragment{ 172 | TypeCondition: ast.NewNamed(&ast.Named{Name: ast.NewName(&ast.Name{Value: typeCondition})}), 173 | SelectionSet: ast.NewSelectionSet(&ast.SelectionSet{}), 174 | })) 175 | newS := &Selection{ 176 | parent: s, 177 | node: newFrag, 178 | } 179 | switch n := s.node.(type) { 180 | case *ast.Field: 181 | n.SelectionSet.Selections = append(n.SelectionSet.Selections, newS.node.(*ast.InlineFragment)) 182 | } 183 | 184 | return newS 185 | } 186 | 187 | // Fragment adds a fragement definition 188 | func (s *Selection) Fragment(name, typeCondition string) *Selection { 189 | newFrag := ast.NewFragmentDefinition((&ast.FragmentDefinition{ 190 | Name: ast.NewName(&ast.Name{Value: name}), 191 | TypeCondition: ast.NewNamed(&ast.Named{Name: ast.NewName(&ast.Name{Value: typeCondition})}), 192 | SelectionSet: ast.NewSelectionSet(&ast.SelectionSet{}), 193 | })) 194 | newS := &Selection{ 195 | parent: s, 196 | node: newFrag, 197 | } 198 | switch n := s.node.(type) { 199 | case *ast.Field: 200 | n.SelectionSet.Selections = append(n.SelectionSet.Selections, newS.node.(*ast.FragmentDefinition)) 201 | case *ast.OperationDefinition: 202 | n.SelectionSet.Selections = append(n.SelectionSet.Selections, newS.node.(*ast.FragmentDefinition)) 203 | } 204 | 205 | return newS 206 | } 207 | 208 | // FragmentSpread adds a fragement spread 209 | func (s *Selection) FragmentSpread(name string) *Selection { 210 | newFrag := ast.NewFragmentSpread((&ast.FragmentSpread{ 211 | Name: ast.NewName(&ast.Name{Value: name}), 212 | })) 213 | newS := &Selection{ 214 | parent: s, 215 | node: newFrag, 216 | } 217 | switch n := s.node.(type) { 218 | case *ast.Field: 219 | n.SelectionSet.Selections = append(n.SelectionSet.Selections, newS.node.(*ast.FragmentSpread)) 220 | case *ast.OperationDefinition: 221 | n.SelectionSet.Selections = append(n.SelectionSet.Selections, newS.node.(*ast.FragmentSpread)) 222 | } 223 | 224 | return s 225 | } 226 | 227 | // Parent returns the parent of this selection. If it's the root, will return nil. 228 | func (s *Selection) Parent() *Selection { 229 | return s.parent 230 | } 231 | 232 | // Root traverses all parents of the current selection until the root 233 | func (s *Selection) Root() *Selection { 234 | if s.parent == nil { 235 | return s 236 | } 237 | current := s.parent 238 | for { 239 | if current.parent == nil { 240 | return current 241 | } else { 242 | current = current.parent 243 | } 244 | } 245 | } 246 | 247 | // String returns the selection as a GraphQL query string 248 | func (s *Selection) String() string { 249 | return printer.Print(s.node).(string) 250 | } 251 | 252 | // selectionOption enables options for a selection 253 | type selectionOption func(*Selection) 254 | 255 | // WithAlias is an option for specifying a selection alias 256 | func WithAlias(alias string) selectionOption { 257 | return func(s *Selection) { 258 | switch n := s.node.(type) { 259 | case *ast.Field: 260 | n.Alias = ast.NewName(&ast.Name{ 261 | Value: alias, 262 | }) 263 | } 264 | } 265 | } 266 | --------------------------------------------------------------------------------