Skip to content
81 changes: 55 additions & 26 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -736,6 +736,7 @@ client.Query(ctx context.Context, q interface{}, variables map[string]interface{
```

Currently, there are 3 option types:

- `operation_name`
- `operation_directive`
- `bind_extensions`
Expand Down Expand Up @@ -887,42 +888,70 @@ func (c *Client) NamedQueryRaw(ctx context.Context, name string, q interface{},
func (c *Client) NamedMutateRaw(ctx context.Context, name string, q interface{}, variables map[string]interface{}) ([]byte, error)
```

### Multiple mutations with ordered map
### Dynamic query builder

You might need to make multiple mutations in a single query. It's not very convenient with structs
so you can use ordered map `[][2]interface{}` instead.
You might need to dynamically multiple queries or mutations in a request. It isn't convenient with static structures.
`Builder` helps us construct many queries flexibly.

For example, to make the following GraphQL mutation:

```GraphQL
mutation($login1: String!, $login2: String!, $login3: String!) {
createUser(login: $login1) { login }
createUser(login: $login2) { login }
createUser(login: $login3) { login }
}
variables {
"login1": "grihabor",
"login2": "diman",
"login3": "indigo"
query($userId: String!, $disabled: Boolean!, $limit: Int!) {
userByPk(userId: $userId) { id name }
groups(disabled: $disabled) { id user_permissions }
topUsers: users(limit: $limit) { id name }
}

# variables {
# "userId": "1",
# "disabled": false,
# "limit": 5
# }
```

You can define:

```Go
type CreateUser struct {
Login string
type User struct {
ID string
Name string
}
m := [][2]interface{}{
{"createUser(login: $login1)", &CreateUser{}},
{"createUser(login: $login2)", &CreateUser{}},
{"createUser(login: $login3)", &CreateUser{}},

var groups []struct {
ID string
Permissions []string `graphql:"user_permissions"`
}
variables := map[string]interface{}{
"login1": "grihabor",
"login2": "diman",
"login3": "indigo",

var userByPk User
var topUsers []User

builder := graphql.NewBuilder().
Query("userByPk(userId: $userId)", &userByPk).
Query("groups(disabled: $disabled)", &groups).
Query("topUsers: users(limit: $limit)", &topUsers).
Variables(map[string]interface{}{
"userId": 1,
"disabled": false,
"limit": 5,
})

query, variables, err := builder.Build()
if err != nil {
return err
}

err = client.Query(context.Background(), query, variables)
if err != nil {
return err
}

// or use Query / Mutate shortcut methods
err = builder.Query(client)
if err != nil {
return err
}


```

### Debugging and Unit test
Expand Down Expand Up @@ -982,11 +1011,11 @@ Because the GraphQL query string is generated in runtime using reflection, it is

## Directories

| Path | Synopsis |
| -------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------- |
| [example/graphqldev](https://godoc.org/github.com/shurcooL/graphql/example/graphqldev) | graphqldev is a test program currently being used for developing graphql package. |
| Path | Synopsis |
| -------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------- |
| [example/graphqldev](https://godoc.org/github.com/shurcooL/graphql/example/graphqldev) | graphqldev is a test program currently being used for developing graphql package. |
| [ident](https://godoc.org/github.com/shurcooL/graphql/ident) | Package ident provides functions for parsing and converting identifier names between various naming conventions. |
| [internal/jsonutil](https://godoc.org/github.com/shurcooL/graphql/internal/jsonutil) | Package jsonutil provides a function for decoding JSON into a GraphQL query data structure. |
| [internal/jsonutil](https://godoc.org/github.com/shurcooL/graphql/internal/jsonutil) | Package jsonutil provides a function for decoding JSON into a GraphQL query data structure. |

## References

Expand Down
240 changes: 240 additions & 0 deletions builder.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,240 @@
package graphql

import (
"context"
"errors"
"fmt"
"regexp"
)

var (
// regular expression for the graphql variable name
// reference: https://spec.graphql.org/June2018/#sec-Names
regexVariableName = regexp.MustCompile(`\$([_A-Za-z][_0-9A-Za-z]*)`)

errBuildQueryRequired = errors.New("no graphql query to be built")
)

type queryBuilderItem struct {
query string
binding interface{}
requiredVars []string
}

// Builder is used to efficiently build dynamic queries and variables
// It helps construct multiple queries to a single request that needs to be conditionally added
type Builder struct {
context context.Context
queries []queryBuilderItem
variables map[string]interface{}
}

// QueryBinding the type alias of interface tuple
// that includes the query string without fields and the binding type
type QueryBinding [2]interface{}

// NewBuilder creates an empty Builder instance
func NewBuilder() Builder {
return Builder{
variables: make(map[string]interface{}),
}
}

// Bind returns the new Builder with the inputted query
func (b Builder) Context(ctx context.Context) Builder {
return Builder{
context: ctx,
queries: b.queries,
variables: b.variables,
}
}

// Bind returns the new Builder with a new query and target data binding
func (b Builder) Bind(query string, binding interface{}) Builder {
return Builder{
context: b.context,
queries: append(b.queries, queryBuilderItem{
query,
binding,
findAllVariableNames(query),
}),
variables: b.variables,
}
}

// Variables returns the new Builder with the inputted variables
func (b Builder) Variable(key string, value interface{}) Builder {
return Builder{
context: b.context,
queries: b.queries,
variables: setMapValue(b.variables, key, value),
}
}

// Variables returns the new Builder with the inputted variables
func (b Builder) Variables(variables map[string]interface{}) Builder {
return Builder{
context: b.context,
queries: b.queries,
variables: mergeMap(b.variables, variables),
}
}

// Unbind returns the new Builder with query items and related variables removed
func (b Builder) Unbind(query string, extra ...string) Builder {
var newQueries []queryBuilderItem
newVars := make(map[string]interface{})

for _, q := range b.queries {
if q.query == query || sliceStringContains(extra, q.query) {
continue
}
newQueries = append(newQueries, q)
if len(b.variables) > 0 {
for _, k := range q.requiredVars {
if v, ok := b.variables[k]; ok {
newVars[k] = v
}
}
}
}

return Builder{
context: b.context,
queries: newQueries,
variables: newVars,
}
}

// RemoveQuery returns the new Builder with query items removed
// this method only remove query items only,
// to remove both query and variables, use Remove instead
func (b Builder) RemoveQuery(query string, extra ...string) Builder {
var newQueries []queryBuilderItem

for _, q := range b.queries {
if q.query != query && !sliceStringContains(extra, q.query) {
newQueries = append(newQueries, q)
}
}

return Builder{
context: b.context,
queries: newQueries,
variables: b.variables,
}
}

// RemoveQuery returns the new Builder with variable fields removed
func (b Builder) RemoveVariable(key string, extra ...string) Builder {
newVars := make(map[string]interface{})
for k, v := range b.variables {
if k != key && !sliceStringContains(extra, k) {
newVars[k] = v
}
}

return Builder{
context: b.context,
queries: b.queries,
variables: newVars,
}
}

// Build query and variable interfaces
func (b Builder) Build() ([]QueryBinding, map[string]interface{}, error) {
if len(b.queries) == 0 {
return nil, nil, errBuildQueryRequired
}

var requiredVars []string
for _, q := range b.queries {
requiredVars = append(requiredVars, q.requiredVars...)
}
variableLength := len(b.variables)
requiredVariableLength := len(requiredVars)
isMismatchedVariables := variableLength != requiredVariableLength
if !isMismatchedVariables && requiredVariableLength > 0 {
for _, varName := range requiredVars {
if _, ok := b.variables[varName]; !ok {
isMismatchedVariables = true
break
}
}
}
if isMismatchedVariables {
varNames := make([]string, 0, variableLength)
for k := range b.variables {
varNames = append(varNames, k)
}
return nil, nil, fmt.Errorf("mismatched variables; want: %+v; got: %+v", requiredVars, varNames)
}

query := make([]QueryBinding, 0, len(b.queries))
for _, q := range b.queries {
query = append(query, [2]interface{}{q.query, q.binding})
}
return query, b.variables, nil
}

// Query builds parameters and executes the GraphQL query request
func (b Builder) Query(c *Client, options ...Option) error {
q, v, err := b.Build()
if err != nil {
return err
}
ctx := b.context
if ctx == nil {
ctx = context.TODO()
}
return c.Query(ctx, &q, v, options...)
}

// Mutate builds parameters and executes the GraphQL query request
func (b Builder) Mutate(c *Client, options ...Option) error {
q, v, err := b.Build()
if err != nil {
return err
}
ctx := b.context
if ctx == nil {
ctx = context.TODO()
}
return c.Mutate(ctx, &q, v, options...)
}

func setMapValue(src map[string]interface{}, key string, value interface{}) map[string]interface{} {
if src == nil {
src = make(map[string]interface{})
}
src[key] = value
return src
}

func mergeMap(src map[string]interface{}, dest map[string]interface{}) map[string]interface{} {
result := make(map[string]interface{})
for k, v := range src {
setMapValue(result, k, v)
}
for k, v := range dest {
setMapValue(result, k, v)
}
return result
}

func sliceStringContains(slice []string, val string) bool {
for _, s := range slice {
if s == val {
return true
}
}
return false
}

func findAllVariableNames(query string) []string {
var results []string
for _, names := range regexVariableName.FindAllStringSubmatch(query, -1) {
results = append(results, names[1])
}
return results
}
Loading