Skip to content

Commit

Permalink
Handle mutlipart/form-data
Browse files Browse the repository at this point in the history
  • Loading branch information
David Koblas committed Aug 1, 2018
1 parent 8588110 commit 59d710a
Show file tree
Hide file tree
Showing 4 changed files with 405 additions and 36 deletions.
21 changes: 21 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,24 @@ h := handler.New(&handler.Config{
})
```

### Using Multipart Form Uploads

This handler supports th
[GraphQL multipart request specification](https://github.com/jaydenseric/graphql-multipart-request-spec).
All file uploads will be made available as the following Scalar that you can add to your GraphQL schemas

```go
var UploadScalar = graphql.NewScalar(graphql.ScalarConfig{
Name: "Upload",
ParseValue: func(value interface{}) interface{} {
if v, ok := value.(*handler.MultipartFile); ok {
return v
}
return nil
},
})
```

### Details

The handler will accept requests with
Expand Down Expand Up @@ -70,6 +88,9 @@ depending on the provided `Content-Type` header.
* **`application/graphql`**: The POST body will be parsed as GraphQL
query string, which provides the `query` parameter.

* **`multipart/form-data`**: The POST body will be parsed as GraphQL
query string, which provides the `operations` parameter.
[GraphQL multipart request specification](https://github.com/jaydenseric/graphql-multipart-request-spec)

### Examples
- [golang-graphql-playground](https://github.com/graphql-go/playground)
Expand Down
117 changes: 108 additions & 9 deletions handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,10 @@ package handler
import (
"encoding/json"
"io/ioutil"
"mime/multipart"
"net/http"
"net/url"
"strconv"
"strings"

"github.com/graphql-go/graphql"
Expand All @@ -13,17 +15,24 @@ import (
)

const (
ContentTypeJSON = "application/json"
ContentTypeGraphQL = "application/graphql"
ContentTypeFormURLEncoded = "application/x-www-form-urlencoded"
ContentTypeJSON = "application/json"
ContentTypeGraphQL = "application/graphql"
ContentTypeFormURLEncoded = "application/x-www-form-urlencoded"
ContentTypeMultipartFormData = "multipart/form-data"
)

type MultipartFile struct {
File multipart.File
Header *multipart.FileHeader
}

type Handler struct {
Schema *graphql.Schema
pretty bool
graphiql bool
playground bool
rootObjectFn RootObjectFn
maxMemory int64
}
type RequestOptions struct {
Query string `json:"query" url:"query" schema:"query"`
Expand Down Expand Up @@ -57,7 +66,7 @@ func getFromForm(values url.Values) *RequestOptions {
}

// RequestOptions Parses a http.Request into GraphQL request options struct
func NewRequestOptions(r *http.Request) *RequestOptions {
func NewRequestOptions(r *http.Request, maxMemory int64) *RequestOptions {
if reqOpt := getFromForm(r.URL.Query()); reqOpt != nil {
return reqOpt
}
Expand Down Expand Up @@ -95,6 +104,89 @@ func NewRequestOptions(r *http.Request) *RequestOptions {

return &RequestOptions{}

case ContentTypeMultipartFormData:
if err := r.ParseMultipartForm(maxMemory); err != nil {
// fmt.Printf("Parse Multipart Failed %v", err)
return &RequestOptions{}
}

// @TODO handle array case...

operationsParam := r.FormValue("operations")
var opts RequestOptions
if err := json.Unmarshal([]byte(operationsParam), &opts); err != nil {
// fmt.Printf("Parse Operations Failed %v", err)
return &RequestOptions{}
}

mapParam := r.FormValue("map")
mapValues := make(map[string]([]string))
if len(mapParam) != 0 {
if err := json.Unmarshal([]byte(mapParam), &mapValues); err != nil {
// fmt.Printf("Parse map Failed %v", err)
return &RequestOptions{}
}
}

variables := opts

for key, value := range mapValues {
for _, v := range value {
if file, header, err := r.FormFile(key); err == nil {

// Now set the path in ther variables
var node interface{} = variables

parts := strings.Split(v, ".")
last := parts[len(parts)-1]

for _, vv := range parts[:len(parts)-1] {
// fmt.Printf("Doing vv=%s type=%T parts=%v\n", vv, node, parts)
switch node.(type) {
case RequestOptions:
if vv == "variables" {
node = opts.Variables
} else {
// panic("Invalid top level tag")
return &RequestOptions{}
}
case map[string]interface{}:
node = node.(map[string]interface{})[vv]
case []interface{}:
if idx, err := strconv.ParseInt(vv, 10, 64); err == nil {
node = node.([]interface{})[idx]
} else {
// panic("Unable to lookup index")
return &RequestOptions{}
}
default:
// panic(fmt.Errorf("Unknown type %T", node))
return &RequestOptions{}
}
}

data := &MultipartFile{File: file, Header: header}

switch node.(type) {
case map[string]interface{}:
node.(map[string]interface{})[last] = data
case []interface{}:
if idx, err := strconv.ParseInt(last, 10, 64); err == nil {
node.([]interface{})[idx] = data
} else {
// panic("Unable to lookup index")
return &RequestOptions{}
}
default:
// panic(fmt.Errorf("Unknown last type %T", node))
return &RequestOptions{}
}
}
}
}

return &opts

case ContentTypeJSON:
fallthrough
default:
Expand All @@ -119,7 +211,7 @@ func NewRequestOptions(r *http.Request) *RequestOptions {
// user-provided context.
func (h *Handler) ContextHandler(ctx context.Context, w http.ResponseWriter, r *http.Request) {
// get query
opts := NewRequestOptions(r)
opts := NewRequestOptions(r, h.maxMemory)

// execute graphql query
params := graphql.Params{
Expand Down Expand Up @@ -182,14 +274,15 @@ type Config struct {
GraphiQL bool
Playground bool
RootObjectFn RootObjectFn
MaxMemory int64
}

func NewConfig() *Config {
return &Config{
Schema: nil,
Pretty: true,
GraphiQL: true,
Playground: false,
Schema: nil,
Pretty: true,
GraphiQL: true,
MaxMemory: 0,
}
}

Expand All @@ -201,11 +294,17 @@ func New(p *Config) *Handler {
panic("undefined GraphQL schema")
}

maxMemory := p.MaxMemory
if maxMemory == 0 {
maxMemory = 32 << 20 // 32MB
}

return &Handler{
Schema: p.Schema,
pretty: p.Pretty,
graphiql: p.GraphiQL,
playground: p.Playground,
rootObjectFn: p.RootObjectFn,
maxMemory: maxMemory,
}
}
Loading

0 comments on commit 59d710a

Please sign in to comment.