package handler
import (
"encoding/json"
"io/ioutil"
"net/http"
"net/url"
"strings"
"github.com/graphql-go/graphql"
"context"
)
const (
ContentTypeJSON = "application/json"
ContentTypeGraphQL = "application/graphql"
ContentTypeFormURLEncoded = "application/x-www-form-urlencoded"
)
type Handler struct {
Schema *graphql.Schema
pretty bool
graphiql bool
}
type RequestOptions struct {
Query string `json:"query" url:"query" schema:"query"`
Variables map[string]interface{} `json:"variables" url:"variables" schema:"variables"`
OperationName string `json:"operationName" url:"operationName" schema:"operationName"`
}
// a workaround for getting`variables` as a JSON string
type requestOptionsCompatibility struct {
Query string `json:"query" url:"query" schema:"query"`
Variables string `json:"variables" url:"variables" schema:"variables"`
OperationName string `json:"operationName" url:"operationName" schema:"operationName"`
}
func getFromForm(values url.Values) *RequestOptions {
query := values.Get("query")
if query != "" {
// get variables map
variables := make(map[string]interface{}, len(values))
variablesStr := values.Get("variables")
json.Unmarshal([]byte(variablesStr), &variables)
return &RequestOptions{
Query: query,
Variables: variables,
OperationName: values.Get("operationName"),
}
}
return nil
}
// RequestOptions Parses a http.Request into GraphQL request options struct
func NewRequestOptions(r *http.Request) *RequestOptions {
if reqOpt := getFromForm(r.URL.Query()); reqOpt != nil {
return reqOpt
}
if r.Method != "POST" {
return &RequestOptions{}
}
if r.Body == nil {
return &RequestOptions{}
}
// TODO: improve Content-Type handling
contentTypeStr := r.Header.Get("Content-Type")
contentTypeTokens := strings.Split(contentTypeStr, ";")
contentType := contentTypeTokens[0]
switch contentType {
case ContentTypeGraphQL:
body, err := ioutil.ReadAll(r.Body)
if err != nil {
return &RequestOptions{}
}
return &RequestOptions{
Query: string(body),
}
case ContentTypeFormURLEncoded:
if err := r.ParseForm(); err != nil {
return &RequestOptions{}
}
if reqOpt := getFromForm(r.PostForm); reqOpt != nil {
return reqOpt
}
return &RequestOptions{}
case ContentTypeJSON:
fallthrough
default:
var opts RequestOptions
body, err := ioutil.ReadAll(r.Body)
if err != nil {
return &opts
}
err = json.Unmarshal(body, &opts)
if err != nil {
// Probably `variables` was sent as a string instead of an object.
// So, we try to be polite and try to parse that as a JSON string
var optsCompatible requestOptionsCompatibility
json.Unmarshal(body, &optsCompatible)
json.Unmarshal([]byte(optsCompatible.Variables), &opts.Variables)
}
return &opts
}
}
// ContextHandler provides an entrypoint into executing graphQL queries with a
// user-provided context.
func (h *Handler) ContextHandler(ctx context.Context, w http.ResponseWriter, r *http.Request) {
// get query
opts := NewRequestOptions(r)
// execute graphql query
params := graphql.Params{
Schema: *h.Schema,
RequestString: opts.Query,
VariableValues: opts.Variables,
OperationName: opts.OperationName,
Context: ctx,
}
result := graphql.Do(params)
if h.graphiql {
acceptHeader := r.Header.Get("Accept")
_, raw := r.URL.Query()["raw"]
if !raw && !strings.Contains(acceptHeader, "application/json") && strings.Contains(acceptHeader, "text/html") {
renderGraphiQL(w, params)
return
}
}
// use proper JSON Header
w.Header().Add("Content-Type", "application/json; charset=utf-8")
if h.pretty {
w.WriteHeader(http.StatusOK)
buff, _ := json.MarshalIndent(result, "", "\t")
w.Write(buff)
} else {
w.WriteHeader(http.StatusOK)
buff, _ := json.Marshal(result)
w.Write(buff)
}
}
// ServeHTTP provides an entrypoint into executing graphQL queries.
func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
h.ContextHandler(r.Context(), w, r)
}
type Config struct {
Schema *graphql.Schema
Pretty bool
GraphiQL bool
}
func NewConfig() *Config {
return &Config{
Schema: nil,
Pretty: true,
GraphiQL: true,
}
}
func New(p *Config) *Handler {
if p == nil {
p = NewConfig()
}
if p.Schema == nil {
panic("undefined GraphQL schema")
}
return &Handler{
Schema: p.Schema,
pretty: p.Pretty,
graphiql: p.GraphiQL,
}
}