This site has been updated.
Reload to display the latest version?
(click here)

Configuring graphql-go/graphql

May 17, 2020

It’s not that hard to use graphql with go, but it could take some effort to find all the gotchas. Therefore let’s save some time and talk about some configuration of graphql in Golang.

In this tutorial I’ll use graphql-go/graphql library, version v0.7.9

Somehow for this library there are only a few examples of schema type definitions. And almost no example of somehow sophisticated scenarios. So you’ll need to spend some time diggin the internet in order to find a solution to your case. Here I want to share some examples.

First of all you’ll need to handle routing in order to receive graphql requests:

import "net/http"

http.Handle("/graphql", services.CorsMiddleware(controllers.GraphqlHandler()))

CorsMiddleware will do 2 things

  1. Allow CORS (or not, you can change it in your implementation)
  2. Handle OPTIONS request of the browser
func EnableCors(w http.ResponseWriter, r *http.Request) {
    Logger.Printf("Enabling CORS (%s)", r.Method)
    w.Header().Set("Access-Control-Allow-Origin", "*")
    w.Header().Set("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept")
    w.Header().Set("Access-Control-Allow-Methods", "GET, POST,OPTIONS")
    if r.Method == "OPTIONS" {
        w.WriteHeader(http.StatusOK)
        return
    }
}

func CorsMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        EnableCors(w, r)

        // If the request method is OPTIONS,
        // then I don't want to process the body or send back a response.
        // In this case I will not continue to the next handler.
        if r.Method != "OPTIONS" {
            next.ServeHTTP(w,r)
        }
    })
}

In the same service I also have handy function to response with json:

func ResponseJson(jsonStructData interface{}, w http.ResponseWriter) error {
    jsonResponse, err := json.Marshal(jsonStructData)
    if err != nil {
        return err
    }
    w.Header().Set("Content-Type", "application/json")
    if _, err := w.Write(jsonResponse); err != nil {
        return err
    }
    return nil
}

Now, the graphqlCtrl controller. Here I need to process the body from the request and deal with the query:

func GraphqlHandler() http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        if r.Body == nil {
            http.Error(w, "No query data", 400)
            return
        }

        var rBody models.GraphqlReqBody
        var err error
        err = json.NewDecoder(r.Body).Decode(&rBody)
        if err == nil {
            graphqlData := models.ProcessGqlQuery(rBody.Query, rBody.Variables)
            if graphqlData.Errors != nil {
                w.WriteHeader(http.StatusBadRequest)
            }
            err = services.ResponseJson(
                graphqlData,
                w,
            )
        }

        if err != nil {
            services.LoggerE.Print(err.Error())
            http.Error(w, "Something went wrong", http.StatusInternalServerError)
        }
    })
}

Now I need to deal with the query. For this specific purpose I have the ProcessGqlQuery function, but first I need to define a general struct for the graphql request body:

type GraphqlReqBody struct {
    Query string `json:"query"`
    OperationName string `json:"operationName"`
    Variables map[string]interface{} `json:"variables"`
}

Now defining the start of the query processing:

func ProcessGqlQuery(query string, variables map[string]interface{}) *graphql.Result {
    retrieveTransactions := RetrieveTransactionsFromFile()

    result := graphql.Do(
        graphql.Params{
            Schema: gqlSchema(retrieveTransactions),
            RequestString: query,
            VariableValues: variables,
        },
    )
    if len(result.Errors) > 0 {
        services.LoggerE.Printf(
            "failed to execute graphql operation, errors: %+v",
            result.Errors,
        )
    }

    return result
}

I will not explain here how RetrieveTransactionsFromFile() works. It’s not important for the overall example. Far more interesting to define the schema.

func gqlSchema(queryTransactions func() []services.Transaction) graphql.Schema {
    queryFields := graphql.Fields{
        "transactions": &graphql.Field{
            Type:        graphql.NewList(hqlTransactionType),
            Description: "Transactions list",
            Args: graphql.FieldConfigArgument{
                "accountFrom": &graphql.ArgumentConfig{
                    Type: graphql.String,
                },
                "accountTo": &graphql.ArgumentConfig{
                    Type: graphql.String,
                },
                "dateRange": &graphql.ArgumentConfig{
                    Type: graphql.NewInputObject(graphql.InputObjectConfig{
                        Name: "DateRangeInput",
                        Fields: graphql.InputObjectConfigFieldMap{
                            "gt": &graphql.InputObjectFieldConfig{
                                Type: graphql.String,
                            },
                            "lt": &graphql.InputObjectFieldConfig{
                                Type: graphql.String,
                            },
                        },
                    }),
                },
            },
            Resolve: func(params graphql.ResolveParams) (interface{}, error) {
                transactions := queryTransactions()
                transactions = services.FilterByDateRange(params.Args["dateRange"], transactions)
                transactions = services.FilterByFromAccount(params.Args["accountFrom"], transactions)
                transactions = services.FilterByToAccount(params.Args["accountTo"], transactions)
                services.Logger.Printf("After filter left %d transactions", len(transactions))
                return transactions, nil
            },
        },
    }

    schemaConfig := graphql.SchemaConfig{
        Query: graphql.NewObject(
            graphql.ObjectConfig{
                Name: "Query",
                Fields: queryFields,
            },
        ),
    }
    schema, err := graphql.NewSchema(schemaConfig)
    if err != nil {
        services.LoggerE.Printf("failed to create new schema, error: %v", err)
    }
    return schema
}

Again, filtering methods FilterByDateRange, FilterByFromAccount and FilterByToAccount are not important here. What is left to do is to define transaction type:

var hqlTransactionType = graphql.NewObject(
    graphql.ObjectConfig{
        Name: "Transaction",
        Fields: graphql.Fields{
            "id": &graphql.Field{
                Type: graphql.String,
            },
            "date": &graphql.Field{
                Type: graphql.String,
            },
            "transactionType": &graphql.Field{
                Type: graphql.String,
            },
            "accountFrom": &graphql.Field{
                Type: graphql.String,
            },
            "amountInAccountFromCoin": &graphql.Field{
                Type: graphql.Float,
            },
            "accountFromCoin": &graphql.Field{
                Type: graphql.String,
            },
            "accountTo": &graphql.Field{
                Type: graphql.String,
            },
            "amountInAccountToCoin": &graphql.Field{
                Type: graphql.Float,
            },
            "accountToCoin": &graphql.Field{
                Type: graphql.String,
            },
            "amountInDefaultCoin": &graphql.Field{
                Type: graphql.Float,
            },
            "exchangeRate": &graphql.Field{
                Type: graphql.Float,
            },
            "defaultCoin": &graphql.Field{
                Type: graphql.String,
            },
            "tags": &graphql.Field{
                Type: graphql.NewList(graphql.String),
            },
            "comments": &graphql.Field{
                Type: graphql.String,
            },
            "category": &graphql.Field{
                Type: graphql.String,
            },
            "rootCategory": &graphql.Field{
                Type: graphql.String,
            },
        },
    },
)