From b454621d436be44ef082bb82c841fe404a91639f Mon Sep 17 00:00:00 2001 From: Antonelli Horacio Date: Tue, 26 Feb 2019 19:23:29 +0000 Subject: [PATCH 01/39] Add support to upload files. --- handler/graphql.go | 119 +++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 116 insertions(+), 3 deletions(-) diff --git a/handler/graphql.go b/handler/graphql.go index 7c5f70cfdf..8a36db3ad3 100644 --- a/handler/graphql.go +++ b/handler/graphql.go @@ -1,11 +1,16 @@ package handler import ( + "bytes" "context" "encoding/json" + "errors" "fmt" "io" + "mime/multipart" "net/http" + "regexp" + "strconv" "strings" "time" @@ -322,9 +327,17 @@ func (gh *graphqlHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { } } case http.MethodPost: - if err := jsonDecode(r.Body, &reqParams); err != nil { - sendErrorf(w, http.StatusBadRequest, "json body could not be decoded: "+err.Error()) - return + contentType := strings.SplitN(r.Header.Get("Content-Type"), ";", 2)[0] + if contentType == "multipart/form-data" { + if err := processMultipart(r, &reqParams); err != nil { + sendErrorf(w, http.StatusBadRequest, "multipart body could not be decoded: "+err.Error()) + return + } + } else { + if err := jsonDecode(r.Body, &reqParams); err != nil { + sendErrorf(w, http.StatusBadRequest, "json body could not be decoded: "+err.Error()) + return + } } default: w.WriteHeader(http.StatusMethodNotAllowed) @@ -477,3 +490,103 @@ func sendError(w http.ResponseWriter, code int, errors ...*gqlerror.Error) { func sendErrorf(w http.ResponseWriter, code int, format string, args ...interface{}) { sendError(w, code, &gqlerror.Error{Message: fmt.Sprintf(format, args...)}) } + +//----------------------------------------------------------------- + +type Upload struct { + File multipart.File + Filename string + Size int64 +} + +func processMultipart(r *http.Request, request *params) error { + // Parse multipart form + if err := r.ParseMultipartForm(1024); err != nil { + return err + } + + // Unmarshal uploads + var uploads = map[Upload][]string{} + var uploadsMap = map[string][]string{} + if err := json.Unmarshal([]byte(r.Form.Get("map")), &uploadsMap); err != nil { + return err + } else { + for key, path := range uploadsMap { + if file, header, err := r.FormFile(key); err != nil { + panic(err) + //w.WriteHeader(http.StatusInternalServerError) + //return + } else { + uploads[Upload{ + File: file, + Size: header.Size, + Filename: header.Filename, + }] = path + } + } + } + + var operations interface{} + + // Unmarshal operations + if err := jsonDecode(bytes.NewBuffer([]byte(r.Form.Get("operations"))), &operations); err != nil { + return err + } + + // set uploads to operations + for file, paths := range uploads { + for _, path := range paths { + set(file, operations, path) + } + } + + switch data := operations.(type) { + case map[string]interface{}: + if value, ok := data["operationName"]; ok && value != nil { + request.OperationName = value.(string) + } + if value, ok := data["query"]; ok && value != nil { + request.Query = value.(string) + } + if value, ok := data["variables"]; ok && value != nil { + request.Variables = value.(map[string]interface{}) + } + return nil + default: + return errors.New("bad request") + } + + return errors.New("invalid operation") +} + +func set(v interface{}, m interface{}, path string) error { + var parts []interface{} + for _, p := range strings.Split(path, ".") { + if isNumber, err := regexp.MatchString(`\d+`, p); err != nil { + return err + } else if isNumber { + index, _ := strconv.Atoi(p) + parts = append(parts, index) + } else { + parts = append(parts, p) + } + } + for i, p := range parts { + last := i == len(parts)-1 + switch idx := p.(type) { + case string: + if last { + m.(map[string]interface{})[idx] = v + } else { + m = m.(map[string]interface{})[idx] + } + case int: + if last { + m.([]interface{})[idx] = v + } else { + m = m.([]interface{})[idx] + } + } + } + return nil +} From 9c17ce33f74900fcb2db4285f30b1d57143759e6 Mon Sep 17 00:00:00 2001 From: hantonelli Date: Sat, 2 Mar 2019 18:01:02 +0000 Subject: [PATCH 02/39] Add file upload --- example/fileupload/.gqlgen.yml | 6 + example/fileupload/fileupload_test.go | 157 ++ example/fileupload/generated.go | 2303 +++++++++++++++++++++++++ example/fileupload/model/generated.go | 12 + example/fileupload/model/model.go | 33 + example/fileupload/readme.md | 126 ++ example/fileupload/resolvers.go | 50 + example/fileupload/schema.graphql | 27 + example/fileupload/server/server.go | 15 + graphql/upload.go | 11 + handler/graphql.go | 110 +- handler/graphql_test.go | 113 ++ handler/stub.go | 7 +- handler/websocket_test.go | 4 +- 14 files changed, 2968 insertions(+), 6 deletions(-) create mode 100644 example/fileupload/.gqlgen.yml create mode 100644 example/fileupload/fileupload_test.go create mode 100644 example/fileupload/generated.go create mode 100644 example/fileupload/model/generated.go create mode 100644 example/fileupload/model/model.go create mode 100644 example/fileupload/readme.md create mode 100644 example/fileupload/resolvers.go create mode 100644 example/fileupload/schema.graphql create mode 100644 example/fileupload/server/server.go create mode 100644 graphql/upload.go diff --git a/example/fileupload/.gqlgen.yml b/example/fileupload/.gqlgen.yml new file mode 100644 index 0000000000..38f5f7c81d --- /dev/null +++ b/example/fileupload/.gqlgen.yml @@ -0,0 +1,6 @@ +model: + filename: model/generated.go + +models: + Upload: + model: github.com/99designs/gqlgen/example/fileupload/model.Upload diff --git a/example/fileupload/fileupload_test.go b/example/fileupload/fileupload_test.go new file mode 100644 index 0000000000..b33dcce8a1 --- /dev/null +++ b/example/fileupload/fileupload_test.go @@ -0,0 +1,157 @@ +package fileupload + +import ( + "bytes" + "context" + "fmt" + "io/ioutil" + "mime/multipart" + "net/http" + "net/http/httptest" + "testing" + + "github.com/99designs/gqlgen/handler" + "github.com/stretchr/testify/require" + + "github.com/99designs/gqlgen/example/fileupload/model" +) + +func TestFileUpload(t *testing.T) { + + t.Run("valid singleUpload", func(t *testing.T) { + resolver := &Resolver{ + SingleUploadFunc: func(ctx context.Context, file model.Upload) (*model.File, error) { + require.NotNil(t, file) + require.NotNil(t, file.File) + content, err := ioutil.ReadAll(file.File) + require.Nil(t, err) + require.Equal(t, string(content), "test") + + return &model.File{ + ID: 1, + }, nil + }, + } + srv := httptest.NewServer(handler.GraphQL(NewExecutableSchema(Config{Resolvers: resolver}))) + defer srv.Close() + + bodyBuf := &bytes.Buffer{} + bodyWriter := multipart.NewWriter(bodyBuf) + err := bodyWriter.WriteField("operations", `{ "query": "mutation ($file: Upload!) { singleUpload(file: $file) { id } }", "variables": { "file": null } }`) + require.NoError(t, err) + err = bodyWriter.WriteField("map", `{ "0": ["variables.file"] }`) + require.NoError(t, err) + w, err := bodyWriter.CreateFormFile("0", "a.txt") + require.NoError(t, err) + _, err = w.Write([]byte("test")) + require.NoError(t, err) + err = bodyWriter.Close() + require.NoError(t, err) + + contentType := bodyWriter.FormDataContentType() + + resp, err := http.Post(fmt.Sprintf("%s/graphql", srv.URL), contentType, bodyBuf) + require.Nil(t, err) + defer func() { + _ = resp.Body.Close() + }() + require.Equal(t, http.StatusOK, resp.StatusCode) + responseBody, err := ioutil.ReadAll(resp.Body) + require.Nil(t, err) + require.Equal(t, `{"data":{"singleUpload":{"id":1}}}`, string(responseBody)) + }) + + t.Run("valid single file upload with payload", func(t *testing.T) { + resolver := &Resolver{ + SingleUploadWithPayloadFunc: func(ctx context.Context, req model.UploadFile) (*model.File, error) { + require.Equal(t, req.ID, 1) + require.NotNil(t, req.File) + require.NotNil(t, req.File.File) + content, err := ioutil.ReadAll(req.File.File) + require.Nil(t, err) + require.Equal(t, string(content), "test") + + return &model.File{ + ID: 1, + }, nil + }, + } + srv := httptest.NewServer(handler.GraphQL(NewExecutableSchema(Config{Resolvers: resolver}))) + defer srv.Close() + + bodyBuf := &bytes.Buffer{} + bodyWriter := multipart.NewWriter(bodyBuf) + err := bodyWriter.WriteField("operations", `{ "query": "mutation ($req: UploadFile!) { singleUploadWithPayload(req: $req) { id } }", "variables": { "req": {"file": null, "id": 1 } } }`) + require.NoError(t, err) + err = bodyWriter.WriteField("map", `{ "0": ["variables.req.file"] }`) + require.NoError(t, err) + w, err := bodyWriter.CreateFormFile("0", "a.txt") + require.NoError(t, err) + _, err = w.Write([]byte("test")) + require.NoError(t, err) + err = bodyWriter.Close() + require.NoError(t, err) + + contentType := bodyWriter.FormDataContentType() + + resp, err := http.Post(fmt.Sprintf("%s/graphql", srv.URL), contentType, bodyBuf) + require.Nil(t, err) + defer func() { + _ = resp.Body.Close() + }() + require.Equal(t, http.StatusOK, resp.StatusCode) + responseBody, err := ioutil.ReadAll(resp.Body) + require.Nil(t, err) + require.Equal(t, `{"data":{"singleUploadWithPayload":{"id":1}}}`, string(responseBody)) + }) + + t.Run("valid file list upload", func(t *testing.T) { + resolver := &Resolver{ + MultipleUploadFunc: func(ctx context.Context, files []model.Upload) ([]model.File, error) { + require.Len(t, files, 2) + for i := range files { + require.NotNil(t, files[i].File) + content, err := ioutil.ReadAll(files[i].File) + require.Nil(t, err) + require.Equal(t, string(content), "test") + } + return []model.File{ + {ID: 1}, + {ID: 2}, + }, nil + }, + } + srv := httptest.NewServer(handler.GraphQL(NewExecutableSchema(Config{Resolvers: resolver}))) + defer srv.Close() + + bodyBuf := &bytes.Buffer{} + bodyWriter := multipart.NewWriter(bodyBuf) + err := bodyWriter.WriteField("operations", `{ "query": "mutation($files: [Upload!]!) { multipleUpload(files: $files) { id } }", "variables": { "files": [null, null] } }`) + require.NoError(t, err) + err = bodyWriter.WriteField("map", `{ "0": ["variables.files.0"], "1": ["variables.files.1"] }`) + require.NoError(t, err) + w0, err := bodyWriter.CreateFormFile("0", "a.txt") + require.NoError(t, err) + _, err = w0.Write([]byte("test")) + require.NoError(t, err) + w1, err := bodyWriter.CreateFormFile("1", "b.txt") + require.NoError(t, err) + _, err = w1.Write([]byte("test")) + require.NoError(t, err) + err = bodyWriter.Close() + require.NoError(t, err) + + contentType := bodyWriter.FormDataContentType() + + resp, err := http.Post(fmt.Sprintf("%s/graphql", srv.URL), contentType, bodyBuf) + require.Nil(t, err) + defer func() { + _ = resp.Body.Close() + }() + require.Equal(t, http.StatusOK, resp.StatusCode) + responseBody, err := ioutil.ReadAll(resp.Body) + require.Nil(t, err) + require.Equal(t, `{"data":{"multipleUpload":[{"id":1},{"id":2}]}}`, string(responseBody)) + }) + +} diff --git a/example/fileupload/generated.go b/example/fileupload/generated.go new file mode 100644 index 0000000000..aae66eeebb --- /dev/null +++ b/example/fileupload/generated.go @@ -0,0 +1,2303 @@ +// Code generated by github.com/99designs/gqlgen, DO NOT EDIT. + +package fileupload + +import ( + "bytes" + "context" + "errors" + "strconv" + "sync" + + "github.com/99designs/gqlgen/example/fileupload/model" + "github.com/99designs/gqlgen/graphql" + "github.com/99designs/gqlgen/graphql/introspection" + "github.com/vektah/gqlparser" + "github.com/vektah/gqlparser/ast" +) + +// region ************************** generated!.gotpl ************************** + +// NewExecutableSchema creates an ExecutableSchema from the ResolverRoot interface. +func NewExecutableSchema(cfg Config) graphql.ExecutableSchema { + return &executableSchema{ + resolvers: cfg.Resolvers, + directives: cfg.Directives, + complexity: cfg.Complexity, + } +} + +type Config struct { + Resolvers ResolverRoot + Directives DirectiveRoot + Complexity ComplexityRoot +} + +type ResolverRoot interface { + Mutation() MutationResolver + Query() QueryResolver +} + +type DirectiveRoot struct { +} + +type ComplexityRoot struct { + File struct { + ID func(childComplexity int) int + } + + Mutation struct { + SingleUpload func(childComplexity int, file model.Upload) int + SingleUploadWithPayload func(childComplexity int, req model.UploadFile) int + MultipleUpload func(childComplexity int, files []model.Upload) int + } + + Query struct { + File func(childComplexity int, fileName string) int + } +} + +type MutationResolver interface { + SingleUpload(ctx context.Context, file model.Upload) (*model.File, error) + SingleUploadWithPayload(ctx context.Context, req model.UploadFile) (*model.File, error) + MultipleUpload(ctx context.Context, files []model.Upload) ([]model.File, error) +} +type QueryResolver interface { + File(ctx context.Context, fileName string) (*model.File, error) +} + +type executableSchema struct { + resolvers ResolverRoot + directives DirectiveRoot + complexity ComplexityRoot +} + +func (e *executableSchema) Schema() *ast.Schema { + return parsedSchema +} + +func (e *executableSchema) Complexity(typeName, field string, childComplexity int, rawArgs map[string]interface{}) (int, bool) { + ec := executionContext{nil, e} + _ = ec + switch typeName + "." + field { + + case "File.ID": + if e.complexity.File.ID == nil { + break + } + + return e.complexity.File.ID(childComplexity), true + + case "Mutation.SingleUpload": + if e.complexity.Mutation.SingleUpload == nil { + break + } + + args, err := ec.field_Mutation_singleUpload_args(context.TODO(), rawArgs) + if err != nil { + return 0, false + } + + return e.complexity.Mutation.SingleUpload(childComplexity, args["file"].(model.Upload)), true + + case "Mutation.SingleUploadWithPayload": + if e.complexity.Mutation.SingleUploadWithPayload == nil { + break + } + + args, err := ec.field_Mutation_singleUploadWithPayload_args(context.TODO(), rawArgs) + if err != nil { + return 0, false + } + + return e.complexity.Mutation.SingleUploadWithPayload(childComplexity, args["req"].(model.UploadFile)), true + + case "Mutation.MultipleUpload": + if e.complexity.Mutation.MultipleUpload == nil { + break + } + + args, err := ec.field_Mutation_multipleUpload_args(context.TODO(), rawArgs) + if err != nil { + return 0, false + } + + return e.complexity.Mutation.MultipleUpload(childComplexity, args["files"].([]model.Upload)), true + + case "Query.File": + if e.complexity.Query.File == nil { + break + } + + args, err := ec.field_Query_file_args(context.TODO(), rawArgs) + if err != nil { + return 0, false + } + + return e.complexity.Query.File(childComplexity, args["fileName"].(string)), true + + } + return 0, false +} + +func (e *executableSchema) Query(ctx context.Context, op *ast.OperationDefinition) *graphql.Response { + ec := executionContext{graphql.GetRequestContext(ctx), e} + + buf := ec.RequestMiddleware(ctx, func(ctx context.Context) []byte { + data := ec._Query(ctx, op.SelectionSet) + var buf bytes.Buffer + data.MarshalGQL(&buf) + return buf.Bytes() + }) + + return &graphql.Response{ + Data: buf, + Errors: ec.Errors, + Extensions: ec.Extensions, + } +} + +func (e *executableSchema) Mutation(ctx context.Context, op *ast.OperationDefinition) *graphql.Response { + ec := executionContext{graphql.GetRequestContext(ctx), e} + + buf := ec.RequestMiddleware(ctx, func(ctx context.Context) []byte { + data := ec._Mutation(ctx, op.SelectionSet) + var buf bytes.Buffer + data.MarshalGQL(&buf) + return buf.Bytes() + }) + + return &graphql.Response{ + Data: buf, + Errors: ec.Errors, + Extensions: ec.Extensions, + } +} + +func (e *executableSchema) Subscription(ctx context.Context, op *ast.OperationDefinition) func() *graphql.Response { + return graphql.OneShot(graphql.ErrorResponse(ctx, "subscriptions are not supported")) +} + +type executionContext struct { + *graphql.RequestContext + *executableSchema +} + +func (ec *executionContext) FieldMiddleware(ctx context.Context, obj interface{}, next graphql.Resolver) (ret interface{}) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = nil + } + }() + res, err := ec.ResolverMiddleware(ctx, next) + if err != nil { + ec.Error(ctx, err) + return nil + } + return res +} + +func (ec *executionContext) introspectSchema() (*introspection.Schema, error) { + if ec.DisableIntrospection { + return nil, errors.New("introspection disabled") + } + return introspection.WrapSchema(parsedSchema), nil +} + +func (ec *executionContext) introspectType(name string) (*introspection.Type, error) { + if ec.DisableIntrospection { + return nil, errors.New("introspection disabled") + } + return introspection.WrapTypeFromDef(parsedSchema, parsedSchema.Types[name]), nil +} + +var parsedSchema = gqlparser.MustLoadSchema( + &ast.Source{Name: "schema.graphql", Input: `# The query type, represents all of the entry points into our object graph + +# The Upload type, represents the response of uploading a file +scalar Upload + +# The File type, represents the response of uploading a file +type File { + id: Int! +} + +# The UploadFile type, represents the request for uploading a file with certain payload +input UploadFile { + id: Int! + file: Upload! +} + +type Query { + file(fileName: String!): File +} + +# The mutation type, represents all updates we can make to our data +type Mutation { + singleUpload(file: Upload!): File! + singleUploadWithPayload(req: UploadFile!): File! + multipleUpload(files: [Upload!]!): [File!]! +} + +`}, +) + +// endregion ************************** generated!.gotpl ************************** + +// region ***************************** args.gotpl ***************************** + +func (ec *executionContext) field_Mutation_multipleUpload_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) { + var err error + args := map[string]interface{}{} + var arg0 []model.Upload + if tmp, ok := rawArgs["files"]; ok { + arg0, err = ec.unmarshalNUpload2ᚕgithubᚗcomᚋ99designsᚋgqlgenᚋexampleᚋfileuploadᚋmodelᚐUpload(ctx, tmp) + if err != nil { + return nil, err + } + } + args["files"] = arg0 + return args, nil +} + +func (ec *executionContext) field_Mutation_singleUploadWithPayload_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) { + var err error + args := map[string]interface{}{} + var arg0 model.UploadFile + if tmp, ok := rawArgs["req"]; ok { + arg0, err = ec.unmarshalNUploadFile2githubᚗcomᚋ99designsᚋgqlgenᚋexampleᚋfileuploadᚋmodelᚐUploadFile(ctx, tmp) + if err != nil { + return nil, err + } + } + args["req"] = arg0 + return args, nil +} + +func (ec *executionContext) field_Mutation_singleUpload_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) { + var err error + args := map[string]interface{}{} + var arg0 model.Upload + if tmp, ok := rawArgs["file"]; ok { + arg0, err = ec.unmarshalNUpload2githubᚗcomᚋ99designsᚋgqlgenᚋexampleᚋfileuploadᚋmodelᚐUpload(ctx, tmp) + if err != nil { + return nil, err + } + } + args["file"] = arg0 + return args, nil +} + +func (ec *executionContext) field_Query___type_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) { + var err error + args := map[string]interface{}{} + var arg0 string + if tmp, ok := rawArgs["name"]; ok { + arg0, err = ec.unmarshalNString2string(ctx, tmp) + if err != nil { + return nil, err + } + } + args["name"] = arg0 + return args, nil +} + +func (ec *executionContext) field_Query_file_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) { + var err error + args := map[string]interface{}{} + var arg0 string + if tmp, ok := rawArgs["fileName"]; ok { + arg0, err = ec.unmarshalNString2string(ctx, tmp) + if err != nil { + return nil, err + } + } + args["fileName"] = arg0 + return args, nil +} + +func (ec *executionContext) field___Type_enumValues_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) { + var err error + args := map[string]interface{}{} + var arg0 bool + if tmp, ok := rawArgs["includeDeprecated"]; ok { + arg0, err = ec.unmarshalOBoolean2bool(ctx, tmp) + if err != nil { + return nil, err + } + } + args["includeDeprecated"] = arg0 + return args, nil +} + +func (ec *executionContext) field___Type_fields_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) { + var err error + args := map[string]interface{}{} + var arg0 bool + if tmp, ok := rawArgs["includeDeprecated"]; ok { + arg0, err = ec.unmarshalOBoolean2bool(ctx, tmp) + if err != nil { + return nil, err + } + } + args["includeDeprecated"] = arg0 + return args, nil +} + +// endregion ***************************** args.gotpl ***************************** + +// region **************************** field.gotpl ***************************** + +func (ec *executionContext) _File_id(ctx context.Context, field graphql.CollectedField, obj *model.File) graphql.Marshaler { + ctx = ec.Tracer.StartFieldExecution(ctx, field) + defer func() { ec.Tracer.EndFieldExecution(ctx) }() + rctx := &graphql.ResolverContext{ + Object: "File", + Field: field, + Args: nil, + } + ctx = graphql.WithResolverContext(ctx, rctx) + ctx = ec.Tracer.StartFieldResolverExecution(ctx, rctx) + resTmp := ec.FieldMiddleware(ctx, obj, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return obj.ID, nil + }) + if resTmp == nil { + if !ec.HasError(rctx) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.(int) + rctx.Result = res + ctx = ec.Tracer.StartFieldChildExecution(ctx) + return ec.marshalNInt2int(ctx, field.Selections, res) +} + +func (ec *executionContext) _Mutation_singleUpload(ctx context.Context, field graphql.CollectedField) graphql.Marshaler { + ctx = ec.Tracer.StartFieldExecution(ctx, field) + defer func() { ec.Tracer.EndFieldExecution(ctx) }() + rctx := &graphql.ResolverContext{ + Object: "Mutation", + Field: field, + Args: nil, + } + ctx = graphql.WithResolverContext(ctx, rctx) + rawArgs := field.ArgumentMap(ec.Variables) + args, err := ec.field_Mutation_singleUpload_args(ctx, rawArgs) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + rctx.Args = args + ctx = ec.Tracer.StartFieldResolverExecution(ctx, rctx) + resTmp := ec.FieldMiddleware(ctx, nil, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return ec.resolvers.Mutation().SingleUpload(rctx, args["file"].(model.Upload)) + }) + if resTmp == nil { + if !ec.HasError(rctx) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.(*model.File) + rctx.Result = res + ctx = ec.Tracer.StartFieldChildExecution(ctx) + return ec.marshalNFile2ᚖgithubᚗcomᚋ99designsᚋgqlgenᚋexampleᚋfileuploadᚋmodelᚐFile(ctx, field.Selections, res) +} + +func (ec *executionContext) _Mutation_singleUploadWithPayload(ctx context.Context, field graphql.CollectedField) graphql.Marshaler { + ctx = ec.Tracer.StartFieldExecution(ctx, field) + defer func() { ec.Tracer.EndFieldExecution(ctx) }() + rctx := &graphql.ResolverContext{ + Object: "Mutation", + Field: field, + Args: nil, + } + ctx = graphql.WithResolverContext(ctx, rctx) + rawArgs := field.ArgumentMap(ec.Variables) + args, err := ec.field_Mutation_singleUploadWithPayload_args(ctx, rawArgs) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + rctx.Args = args + ctx = ec.Tracer.StartFieldResolverExecution(ctx, rctx) + resTmp := ec.FieldMiddleware(ctx, nil, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return ec.resolvers.Mutation().SingleUploadWithPayload(rctx, args["req"].(model.UploadFile)) + }) + if resTmp == nil { + if !ec.HasError(rctx) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.(*model.File) + rctx.Result = res + ctx = ec.Tracer.StartFieldChildExecution(ctx) + return ec.marshalNFile2ᚖgithubᚗcomᚋ99designsᚋgqlgenᚋexampleᚋfileuploadᚋmodelᚐFile(ctx, field.Selections, res) +} + +func (ec *executionContext) _Mutation_multipleUpload(ctx context.Context, field graphql.CollectedField) graphql.Marshaler { + ctx = ec.Tracer.StartFieldExecution(ctx, field) + defer func() { ec.Tracer.EndFieldExecution(ctx) }() + rctx := &graphql.ResolverContext{ + Object: "Mutation", + Field: field, + Args: nil, + } + ctx = graphql.WithResolverContext(ctx, rctx) + rawArgs := field.ArgumentMap(ec.Variables) + args, err := ec.field_Mutation_multipleUpload_args(ctx, rawArgs) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + rctx.Args = args + ctx = ec.Tracer.StartFieldResolverExecution(ctx, rctx) + resTmp := ec.FieldMiddleware(ctx, nil, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return ec.resolvers.Mutation().MultipleUpload(rctx, args["files"].([]model.Upload)) + }) + if resTmp == nil { + if !ec.HasError(rctx) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.([]model.File) + rctx.Result = res + ctx = ec.Tracer.StartFieldChildExecution(ctx) + return ec.marshalNFile2ᚕgithubᚗcomᚋ99designsᚋgqlgenᚋexampleᚋfileuploadᚋmodelᚐFile(ctx, field.Selections, res) +} + +func (ec *executionContext) _Query_file(ctx context.Context, field graphql.CollectedField) graphql.Marshaler { + ctx = ec.Tracer.StartFieldExecution(ctx, field) + defer func() { ec.Tracer.EndFieldExecution(ctx) }() + rctx := &graphql.ResolverContext{ + Object: "Query", + Field: field, + Args: nil, + } + ctx = graphql.WithResolverContext(ctx, rctx) + rawArgs := field.ArgumentMap(ec.Variables) + args, err := ec.field_Query_file_args(ctx, rawArgs) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + rctx.Args = args + ctx = ec.Tracer.StartFieldResolverExecution(ctx, rctx) + resTmp := ec.FieldMiddleware(ctx, nil, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return ec.resolvers.Query().File(rctx, args["fileName"].(string)) + }) + if resTmp == nil { + return graphql.Null + } + res := resTmp.(*model.File) + rctx.Result = res + ctx = ec.Tracer.StartFieldChildExecution(ctx) + return ec.marshalOFile2ᚖgithubᚗcomᚋ99designsᚋgqlgenᚋexampleᚋfileuploadᚋmodelᚐFile(ctx, field.Selections, res) +} + +func (ec *executionContext) _Query___type(ctx context.Context, field graphql.CollectedField) graphql.Marshaler { + ctx = ec.Tracer.StartFieldExecution(ctx, field) + defer func() { ec.Tracer.EndFieldExecution(ctx) }() + rctx := &graphql.ResolverContext{ + Object: "Query", + Field: field, + Args: nil, + } + ctx = graphql.WithResolverContext(ctx, rctx) + rawArgs := field.ArgumentMap(ec.Variables) + args, err := ec.field_Query___type_args(ctx, rawArgs) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + rctx.Args = args + ctx = ec.Tracer.StartFieldResolverExecution(ctx, rctx) + resTmp := ec.FieldMiddleware(ctx, nil, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return ec.introspectType(args["name"].(string)) + }) + if resTmp == nil { + return graphql.Null + } + res := resTmp.(*introspection.Type) + rctx.Result = res + ctx = ec.Tracer.StartFieldChildExecution(ctx) + return ec.marshalO__Type2ᚖgithubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚋintrospectionᚐType(ctx, field.Selections, res) +} + +func (ec *executionContext) _Query___schema(ctx context.Context, field graphql.CollectedField) graphql.Marshaler { + ctx = ec.Tracer.StartFieldExecution(ctx, field) + defer func() { ec.Tracer.EndFieldExecution(ctx) }() + rctx := &graphql.ResolverContext{ + Object: "Query", + Field: field, + Args: nil, + } + ctx = graphql.WithResolverContext(ctx, rctx) + ctx = ec.Tracer.StartFieldResolverExecution(ctx, rctx) + resTmp := ec.FieldMiddleware(ctx, nil, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return ec.introspectSchema() + }) + if resTmp == nil { + return graphql.Null + } + res := resTmp.(*introspection.Schema) + rctx.Result = res + ctx = ec.Tracer.StartFieldChildExecution(ctx) + return ec.marshalO__Schema2ᚖgithubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚋintrospectionᚐSchema(ctx, field.Selections, res) +} + +func (ec *executionContext) ___Directive_name(ctx context.Context, field graphql.CollectedField, obj *introspection.Directive) graphql.Marshaler { + ctx = ec.Tracer.StartFieldExecution(ctx, field) + defer func() { ec.Tracer.EndFieldExecution(ctx) }() + rctx := &graphql.ResolverContext{ + Object: "__Directive", + Field: field, + Args: nil, + } + ctx = graphql.WithResolverContext(ctx, rctx) + ctx = ec.Tracer.StartFieldResolverExecution(ctx, rctx) + resTmp := ec.FieldMiddleware(ctx, obj, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return obj.Name, nil + }) + if resTmp == nil { + if !ec.HasError(rctx) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.(string) + rctx.Result = res + ctx = ec.Tracer.StartFieldChildExecution(ctx) + return ec.marshalNString2string(ctx, field.Selections, res) +} + +func (ec *executionContext) ___Directive_description(ctx context.Context, field graphql.CollectedField, obj *introspection.Directive) graphql.Marshaler { + ctx = ec.Tracer.StartFieldExecution(ctx, field) + defer func() { ec.Tracer.EndFieldExecution(ctx) }() + rctx := &graphql.ResolverContext{ + Object: "__Directive", + Field: field, + Args: nil, + } + ctx = graphql.WithResolverContext(ctx, rctx) + ctx = ec.Tracer.StartFieldResolverExecution(ctx, rctx) + resTmp := ec.FieldMiddleware(ctx, obj, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return obj.Description, nil + }) + if resTmp == nil { + return graphql.Null + } + res := resTmp.(string) + rctx.Result = res + ctx = ec.Tracer.StartFieldChildExecution(ctx) + return ec.marshalOString2string(ctx, field.Selections, res) +} + +func (ec *executionContext) ___Directive_locations(ctx context.Context, field graphql.CollectedField, obj *introspection.Directive) graphql.Marshaler { + ctx = ec.Tracer.StartFieldExecution(ctx, field) + defer func() { ec.Tracer.EndFieldExecution(ctx) }() + rctx := &graphql.ResolverContext{ + Object: "__Directive", + Field: field, + Args: nil, + } + ctx = graphql.WithResolverContext(ctx, rctx) + ctx = ec.Tracer.StartFieldResolverExecution(ctx, rctx) + resTmp := ec.FieldMiddleware(ctx, obj, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return obj.Locations, nil + }) + if resTmp == nil { + if !ec.HasError(rctx) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.([]string) + rctx.Result = res + ctx = ec.Tracer.StartFieldChildExecution(ctx) + return ec.marshalN__DirectiveLocation2ᚕstring(ctx, field.Selections, res) +} + +func (ec *executionContext) ___Directive_args(ctx context.Context, field graphql.CollectedField, obj *introspection.Directive) graphql.Marshaler { + ctx = ec.Tracer.StartFieldExecution(ctx, field) + defer func() { ec.Tracer.EndFieldExecution(ctx) }() + rctx := &graphql.ResolverContext{ + Object: "__Directive", + Field: field, + Args: nil, + } + ctx = graphql.WithResolverContext(ctx, rctx) + ctx = ec.Tracer.StartFieldResolverExecution(ctx, rctx) + resTmp := ec.FieldMiddleware(ctx, obj, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return obj.Args, nil + }) + if resTmp == nil { + if !ec.HasError(rctx) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.([]introspection.InputValue) + rctx.Result = res + ctx = ec.Tracer.StartFieldChildExecution(ctx) + return ec.marshalN__InputValue2ᚕgithubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚋintrospectionᚐInputValue(ctx, field.Selections, res) +} + +func (ec *executionContext) ___EnumValue_name(ctx context.Context, field graphql.CollectedField, obj *introspection.EnumValue) graphql.Marshaler { + ctx = ec.Tracer.StartFieldExecution(ctx, field) + defer func() { ec.Tracer.EndFieldExecution(ctx) }() + rctx := &graphql.ResolverContext{ + Object: "__EnumValue", + Field: field, + Args: nil, + } + ctx = graphql.WithResolverContext(ctx, rctx) + ctx = ec.Tracer.StartFieldResolverExecution(ctx, rctx) + resTmp := ec.FieldMiddleware(ctx, obj, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return obj.Name, nil + }) + if resTmp == nil { + if !ec.HasError(rctx) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.(string) + rctx.Result = res + ctx = ec.Tracer.StartFieldChildExecution(ctx) + return ec.marshalNString2string(ctx, field.Selections, res) +} + +func (ec *executionContext) ___EnumValue_description(ctx context.Context, field graphql.CollectedField, obj *introspection.EnumValue) graphql.Marshaler { + ctx = ec.Tracer.StartFieldExecution(ctx, field) + defer func() { ec.Tracer.EndFieldExecution(ctx) }() + rctx := &graphql.ResolverContext{ + Object: "__EnumValue", + Field: field, + Args: nil, + } + ctx = graphql.WithResolverContext(ctx, rctx) + ctx = ec.Tracer.StartFieldResolverExecution(ctx, rctx) + resTmp := ec.FieldMiddleware(ctx, obj, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return obj.Description, nil + }) + if resTmp == nil { + return graphql.Null + } + res := resTmp.(string) + rctx.Result = res + ctx = ec.Tracer.StartFieldChildExecution(ctx) + return ec.marshalOString2string(ctx, field.Selections, res) +} + +func (ec *executionContext) ___EnumValue_isDeprecated(ctx context.Context, field graphql.CollectedField, obj *introspection.EnumValue) graphql.Marshaler { + ctx = ec.Tracer.StartFieldExecution(ctx, field) + defer func() { ec.Tracer.EndFieldExecution(ctx) }() + rctx := &graphql.ResolverContext{ + Object: "__EnumValue", + Field: field, + Args: nil, + } + ctx = graphql.WithResolverContext(ctx, rctx) + ctx = ec.Tracer.StartFieldResolverExecution(ctx, rctx) + resTmp := ec.FieldMiddleware(ctx, obj, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return obj.IsDeprecated(), nil + }) + if resTmp == nil { + if !ec.HasError(rctx) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.(bool) + rctx.Result = res + ctx = ec.Tracer.StartFieldChildExecution(ctx) + return ec.marshalNBoolean2bool(ctx, field.Selections, res) +} + +func (ec *executionContext) ___EnumValue_deprecationReason(ctx context.Context, field graphql.CollectedField, obj *introspection.EnumValue) graphql.Marshaler { + ctx = ec.Tracer.StartFieldExecution(ctx, field) + defer func() { ec.Tracer.EndFieldExecution(ctx) }() + rctx := &graphql.ResolverContext{ + Object: "__EnumValue", + Field: field, + Args: nil, + } + ctx = graphql.WithResolverContext(ctx, rctx) + ctx = ec.Tracer.StartFieldResolverExecution(ctx, rctx) + resTmp := ec.FieldMiddleware(ctx, obj, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return obj.DeprecationReason(), nil + }) + if resTmp == nil { + return graphql.Null + } + res := resTmp.(*string) + rctx.Result = res + ctx = ec.Tracer.StartFieldChildExecution(ctx) + return ec.marshalOString2ᚖstring(ctx, field.Selections, res) +} + +func (ec *executionContext) ___Field_name(ctx context.Context, field graphql.CollectedField, obj *introspection.Field) graphql.Marshaler { + ctx = ec.Tracer.StartFieldExecution(ctx, field) + defer func() { ec.Tracer.EndFieldExecution(ctx) }() + rctx := &graphql.ResolverContext{ + Object: "__Field", + Field: field, + Args: nil, + } + ctx = graphql.WithResolverContext(ctx, rctx) + ctx = ec.Tracer.StartFieldResolverExecution(ctx, rctx) + resTmp := ec.FieldMiddleware(ctx, obj, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return obj.Name, nil + }) + if resTmp == nil { + if !ec.HasError(rctx) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.(string) + rctx.Result = res + ctx = ec.Tracer.StartFieldChildExecution(ctx) + return ec.marshalNString2string(ctx, field.Selections, res) +} + +func (ec *executionContext) ___Field_description(ctx context.Context, field graphql.CollectedField, obj *introspection.Field) graphql.Marshaler { + ctx = ec.Tracer.StartFieldExecution(ctx, field) + defer func() { ec.Tracer.EndFieldExecution(ctx) }() + rctx := &graphql.ResolverContext{ + Object: "__Field", + Field: field, + Args: nil, + } + ctx = graphql.WithResolverContext(ctx, rctx) + ctx = ec.Tracer.StartFieldResolverExecution(ctx, rctx) + resTmp := ec.FieldMiddleware(ctx, obj, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return obj.Description, nil + }) + if resTmp == nil { + return graphql.Null + } + res := resTmp.(string) + rctx.Result = res + ctx = ec.Tracer.StartFieldChildExecution(ctx) + return ec.marshalOString2string(ctx, field.Selections, res) +} + +func (ec *executionContext) ___Field_args(ctx context.Context, field graphql.CollectedField, obj *introspection.Field) graphql.Marshaler { + ctx = ec.Tracer.StartFieldExecution(ctx, field) + defer func() { ec.Tracer.EndFieldExecution(ctx) }() + rctx := &graphql.ResolverContext{ + Object: "__Field", + Field: field, + Args: nil, + } + ctx = graphql.WithResolverContext(ctx, rctx) + ctx = ec.Tracer.StartFieldResolverExecution(ctx, rctx) + resTmp := ec.FieldMiddleware(ctx, obj, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return obj.Args, nil + }) + if resTmp == nil { + if !ec.HasError(rctx) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.([]introspection.InputValue) + rctx.Result = res + ctx = ec.Tracer.StartFieldChildExecution(ctx) + return ec.marshalN__InputValue2ᚕgithubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚋintrospectionᚐInputValue(ctx, field.Selections, res) +} + +func (ec *executionContext) ___Field_type(ctx context.Context, field graphql.CollectedField, obj *introspection.Field) graphql.Marshaler { + ctx = ec.Tracer.StartFieldExecution(ctx, field) + defer func() { ec.Tracer.EndFieldExecution(ctx) }() + rctx := &graphql.ResolverContext{ + Object: "__Field", + Field: field, + Args: nil, + } + ctx = graphql.WithResolverContext(ctx, rctx) + ctx = ec.Tracer.StartFieldResolverExecution(ctx, rctx) + resTmp := ec.FieldMiddleware(ctx, obj, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return obj.Type, nil + }) + if resTmp == nil { + if !ec.HasError(rctx) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.(*introspection.Type) + rctx.Result = res + ctx = ec.Tracer.StartFieldChildExecution(ctx) + return ec.marshalN__Type2ᚖgithubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚋintrospectionᚐType(ctx, field.Selections, res) +} + +func (ec *executionContext) ___Field_isDeprecated(ctx context.Context, field graphql.CollectedField, obj *introspection.Field) graphql.Marshaler { + ctx = ec.Tracer.StartFieldExecution(ctx, field) + defer func() { ec.Tracer.EndFieldExecution(ctx) }() + rctx := &graphql.ResolverContext{ + Object: "__Field", + Field: field, + Args: nil, + } + ctx = graphql.WithResolverContext(ctx, rctx) + ctx = ec.Tracer.StartFieldResolverExecution(ctx, rctx) + resTmp := ec.FieldMiddleware(ctx, obj, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return obj.IsDeprecated(), nil + }) + if resTmp == nil { + if !ec.HasError(rctx) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.(bool) + rctx.Result = res + ctx = ec.Tracer.StartFieldChildExecution(ctx) + return ec.marshalNBoolean2bool(ctx, field.Selections, res) +} + +func (ec *executionContext) ___Field_deprecationReason(ctx context.Context, field graphql.CollectedField, obj *introspection.Field) graphql.Marshaler { + ctx = ec.Tracer.StartFieldExecution(ctx, field) + defer func() { ec.Tracer.EndFieldExecution(ctx) }() + rctx := &graphql.ResolverContext{ + Object: "__Field", + Field: field, + Args: nil, + } + ctx = graphql.WithResolverContext(ctx, rctx) + ctx = ec.Tracer.StartFieldResolverExecution(ctx, rctx) + resTmp := ec.FieldMiddleware(ctx, obj, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return obj.DeprecationReason(), nil + }) + if resTmp == nil { + return graphql.Null + } + res := resTmp.(*string) + rctx.Result = res + ctx = ec.Tracer.StartFieldChildExecution(ctx) + return ec.marshalOString2ᚖstring(ctx, field.Selections, res) +} + +func (ec *executionContext) ___InputValue_name(ctx context.Context, field graphql.CollectedField, obj *introspection.InputValue) graphql.Marshaler { + ctx = ec.Tracer.StartFieldExecution(ctx, field) + defer func() { ec.Tracer.EndFieldExecution(ctx) }() + rctx := &graphql.ResolverContext{ + Object: "__InputValue", + Field: field, + Args: nil, + } + ctx = graphql.WithResolverContext(ctx, rctx) + ctx = ec.Tracer.StartFieldResolverExecution(ctx, rctx) + resTmp := ec.FieldMiddleware(ctx, obj, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return obj.Name, nil + }) + if resTmp == nil { + if !ec.HasError(rctx) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.(string) + rctx.Result = res + ctx = ec.Tracer.StartFieldChildExecution(ctx) + return ec.marshalNString2string(ctx, field.Selections, res) +} + +func (ec *executionContext) ___InputValue_description(ctx context.Context, field graphql.CollectedField, obj *introspection.InputValue) graphql.Marshaler { + ctx = ec.Tracer.StartFieldExecution(ctx, field) + defer func() { ec.Tracer.EndFieldExecution(ctx) }() + rctx := &graphql.ResolverContext{ + Object: "__InputValue", + Field: field, + Args: nil, + } + ctx = graphql.WithResolverContext(ctx, rctx) + ctx = ec.Tracer.StartFieldResolverExecution(ctx, rctx) + resTmp := ec.FieldMiddleware(ctx, obj, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return obj.Description, nil + }) + if resTmp == nil { + return graphql.Null + } + res := resTmp.(string) + rctx.Result = res + ctx = ec.Tracer.StartFieldChildExecution(ctx) + return ec.marshalOString2string(ctx, field.Selections, res) +} + +func (ec *executionContext) ___InputValue_type(ctx context.Context, field graphql.CollectedField, obj *introspection.InputValue) graphql.Marshaler { + ctx = ec.Tracer.StartFieldExecution(ctx, field) + defer func() { ec.Tracer.EndFieldExecution(ctx) }() + rctx := &graphql.ResolverContext{ + Object: "__InputValue", + Field: field, + Args: nil, + } + ctx = graphql.WithResolverContext(ctx, rctx) + ctx = ec.Tracer.StartFieldResolverExecution(ctx, rctx) + resTmp := ec.FieldMiddleware(ctx, obj, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return obj.Type, nil + }) + if resTmp == nil { + if !ec.HasError(rctx) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.(*introspection.Type) + rctx.Result = res + ctx = ec.Tracer.StartFieldChildExecution(ctx) + return ec.marshalN__Type2ᚖgithubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚋintrospectionᚐType(ctx, field.Selections, res) +} + +func (ec *executionContext) ___InputValue_defaultValue(ctx context.Context, field graphql.CollectedField, obj *introspection.InputValue) graphql.Marshaler { + ctx = ec.Tracer.StartFieldExecution(ctx, field) + defer func() { ec.Tracer.EndFieldExecution(ctx) }() + rctx := &graphql.ResolverContext{ + Object: "__InputValue", + Field: field, + Args: nil, + } + ctx = graphql.WithResolverContext(ctx, rctx) + ctx = ec.Tracer.StartFieldResolverExecution(ctx, rctx) + resTmp := ec.FieldMiddleware(ctx, obj, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return obj.DefaultValue, nil + }) + if resTmp == nil { + return graphql.Null + } + res := resTmp.(*string) + rctx.Result = res + ctx = ec.Tracer.StartFieldChildExecution(ctx) + return ec.marshalOString2ᚖstring(ctx, field.Selections, res) +} + +func (ec *executionContext) ___Schema_types(ctx context.Context, field graphql.CollectedField, obj *introspection.Schema) graphql.Marshaler { + ctx = ec.Tracer.StartFieldExecution(ctx, field) + defer func() { ec.Tracer.EndFieldExecution(ctx) }() + rctx := &graphql.ResolverContext{ + Object: "__Schema", + Field: field, + Args: nil, + } + ctx = graphql.WithResolverContext(ctx, rctx) + ctx = ec.Tracer.StartFieldResolverExecution(ctx, rctx) + resTmp := ec.FieldMiddleware(ctx, obj, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return obj.Types(), nil + }) + if resTmp == nil { + if !ec.HasError(rctx) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.([]introspection.Type) + rctx.Result = res + ctx = ec.Tracer.StartFieldChildExecution(ctx) + return ec.marshalN__Type2ᚕgithubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚋintrospectionᚐType(ctx, field.Selections, res) +} + +func (ec *executionContext) ___Schema_queryType(ctx context.Context, field graphql.CollectedField, obj *introspection.Schema) graphql.Marshaler { + ctx = ec.Tracer.StartFieldExecution(ctx, field) + defer func() { ec.Tracer.EndFieldExecution(ctx) }() + rctx := &graphql.ResolverContext{ + Object: "__Schema", + Field: field, + Args: nil, + } + ctx = graphql.WithResolverContext(ctx, rctx) + ctx = ec.Tracer.StartFieldResolverExecution(ctx, rctx) + resTmp := ec.FieldMiddleware(ctx, obj, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return obj.QueryType(), nil + }) + if resTmp == nil { + if !ec.HasError(rctx) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.(*introspection.Type) + rctx.Result = res + ctx = ec.Tracer.StartFieldChildExecution(ctx) + return ec.marshalN__Type2ᚖgithubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚋintrospectionᚐType(ctx, field.Selections, res) +} + +func (ec *executionContext) ___Schema_mutationType(ctx context.Context, field graphql.CollectedField, obj *introspection.Schema) graphql.Marshaler { + ctx = ec.Tracer.StartFieldExecution(ctx, field) + defer func() { ec.Tracer.EndFieldExecution(ctx) }() + rctx := &graphql.ResolverContext{ + Object: "__Schema", + Field: field, + Args: nil, + } + ctx = graphql.WithResolverContext(ctx, rctx) + ctx = ec.Tracer.StartFieldResolverExecution(ctx, rctx) + resTmp := ec.FieldMiddleware(ctx, obj, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return obj.MutationType(), nil + }) + if resTmp == nil { + return graphql.Null + } + res := resTmp.(*introspection.Type) + rctx.Result = res + ctx = ec.Tracer.StartFieldChildExecution(ctx) + return ec.marshalO__Type2ᚖgithubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚋintrospectionᚐType(ctx, field.Selections, res) +} + +func (ec *executionContext) ___Schema_subscriptionType(ctx context.Context, field graphql.CollectedField, obj *introspection.Schema) graphql.Marshaler { + ctx = ec.Tracer.StartFieldExecution(ctx, field) + defer func() { ec.Tracer.EndFieldExecution(ctx) }() + rctx := &graphql.ResolverContext{ + Object: "__Schema", + Field: field, + Args: nil, + } + ctx = graphql.WithResolverContext(ctx, rctx) + ctx = ec.Tracer.StartFieldResolverExecution(ctx, rctx) + resTmp := ec.FieldMiddleware(ctx, obj, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return obj.SubscriptionType(), nil + }) + if resTmp == nil { + return graphql.Null + } + res := resTmp.(*introspection.Type) + rctx.Result = res + ctx = ec.Tracer.StartFieldChildExecution(ctx) + return ec.marshalO__Type2ᚖgithubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚋintrospectionᚐType(ctx, field.Selections, res) +} + +func (ec *executionContext) ___Schema_directives(ctx context.Context, field graphql.CollectedField, obj *introspection.Schema) graphql.Marshaler { + ctx = ec.Tracer.StartFieldExecution(ctx, field) + defer func() { ec.Tracer.EndFieldExecution(ctx) }() + rctx := &graphql.ResolverContext{ + Object: "__Schema", + Field: field, + Args: nil, + } + ctx = graphql.WithResolverContext(ctx, rctx) + ctx = ec.Tracer.StartFieldResolverExecution(ctx, rctx) + resTmp := ec.FieldMiddleware(ctx, obj, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return obj.Directives(), nil + }) + if resTmp == nil { + if !ec.HasError(rctx) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.([]introspection.Directive) + rctx.Result = res + ctx = ec.Tracer.StartFieldChildExecution(ctx) + return ec.marshalN__Directive2ᚕgithubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚋintrospectionᚐDirective(ctx, field.Selections, res) +} + +func (ec *executionContext) ___Type_kind(ctx context.Context, field graphql.CollectedField, obj *introspection.Type) graphql.Marshaler { + ctx = ec.Tracer.StartFieldExecution(ctx, field) + defer func() { ec.Tracer.EndFieldExecution(ctx) }() + rctx := &graphql.ResolverContext{ + Object: "__Type", + Field: field, + Args: nil, + } + ctx = graphql.WithResolverContext(ctx, rctx) + ctx = ec.Tracer.StartFieldResolverExecution(ctx, rctx) + resTmp := ec.FieldMiddleware(ctx, obj, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return obj.Kind(), nil + }) + if resTmp == nil { + if !ec.HasError(rctx) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.(string) + rctx.Result = res + ctx = ec.Tracer.StartFieldChildExecution(ctx) + return ec.marshalN__TypeKind2string(ctx, field.Selections, res) +} + +func (ec *executionContext) ___Type_name(ctx context.Context, field graphql.CollectedField, obj *introspection.Type) graphql.Marshaler { + ctx = ec.Tracer.StartFieldExecution(ctx, field) + defer func() { ec.Tracer.EndFieldExecution(ctx) }() + rctx := &graphql.ResolverContext{ + Object: "__Type", + Field: field, + Args: nil, + } + ctx = graphql.WithResolverContext(ctx, rctx) + ctx = ec.Tracer.StartFieldResolverExecution(ctx, rctx) + resTmp := ec.FieldMiddleware(ctx, obj, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return obj.Name(), nil + }) + if resTmp == nil { + return graphql.Null + } + res := resTmp.(*string) + rctx.Result = res + ctx = ec.Tracer.StartFieldChildExecution(ctx) + return ec.marshalOString2ᚖstring(ctx, field.Selections, res) +} + +func (ec *executionContext) ___Type_description(ctx context.Context, field graphql.CollectedField, obj *introspection.Type) graphql.Marshaler { + ctx = ec.Tracer.StartFieldExecution(ctx, field) + defer func() { ec.Tracer.EndFieldExecution(ctx) }() + rctx := &graphql.ResolverContext{ + Object: "__Type", + Field: field, + Args: nil, + } + ctx = graphql.WithResolverContext(ctx, rctx) + ctx = ec.Tracer.StartFieldResolverExecution(ctx, rctx) + resTmp := ec.FieldMiddleware(ctx, obj, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return obj.Description(), nil + }) + if resTmp == nil { + return graphql.Null + } + res := resTmp.(string) + rctx.Result = res + ctx = ec.Tracer.StartFieldChildExecution(ctx) + return ec.marshalOString2string(ctx, field.Selections, res) +} + +func (ec *executionContext) ___Type_fields(ctx context.Context, field graphql.CollectedField, obj *introspection.Type) graphql.Marshaler { + ctx = ec.Tracer.StartFieldExecution(ctx, field) + defer func() { ec.Tracer.EndFieldExecution(ctx) }() + rctx := &graphql.ResolverContext{ + Object: "__Type", + Field: field, + Args: nil, + } + ctx = graphql.WithResolverContext(ctx, rctx) + rawArgs := field.ArgumentMap(ec.Variables) + args, err := ec.field___Type_fields_args(ctx, rawArgs) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + rctx.Args = args + ctx = ec.Tracer.StartFieldResolverExecution(ctx, rctx) + resTmp := ec.FieldMiddleware(ctx, obj, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return obj.Fields(args["includeDeprecated"].(bool)), nil + }) + if resTmp == nil { + return graphql.Null + } + res := resTmp.([]introspection.Field) + rctx.Result = res + ctx = ec.Tracer.StartFieldChildExecution(ctx) + return ec.marshalO__Field2ᚕgithubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚋintrospectionᚐField(ctx, field.Selections, res) +} + +func (ec *executionContext) ___Type_interfaces(ctx context.Context, field graphql.CollectedField, obj *introspection.Type) graphql.Marshaler { + ctx = ec.Tracer.StartFieldExecution(ctx, field) + defer func() { ec.Tracer.EndFieldExecution(ctx) }() + rctx := &graphql.ResolverContext{ + Object: "__Type", + Field: field, + Args: nil, + } + ctx = graphql.WithResolverContext(ctx, rctx) + ctx = ec.Tracer.StartFieldResolverExecution(ctx, rctx) + resTmp := ec.FieldMiddleware(ctx, obj, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return obj.Interfaces(), nil + }) + if resTmp == nil { + return graphql.Null + } + res := resTmp.([]introspection.Type) + rctx.Result = res + ctx = ec.Tracer.StartFieldChildExecution(ctx) + return ec.marshalO__Type2ᚕgithubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚋintrospectionᚐType(ctx, field.Selections, res) +} + +func (ec *executionContext) ___Type_possibleTypes(ctx context.Context, field graphql.CollectedField, obj *introspection.Type) graphql.Marshaler { + ctx = ec.Tracer.StartFieldExecution(ctx, field) + defer func() { ec.Tracer.EndFieldExecution(ctx) }() + rctx := &graphql.ResolverContext{ + Object: "__Type", + Field: field, + Args: nil, + } + ctx = graphql.WithResolverContext(ctx, rctx) + ctx = ec.Tracer.StartFieldResolverExecution(ctx, rctx) + resTmp := ec.FieldMiddleware(ctx, obj, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return obj.PossibleTypes(), nil + }) + if resTmp == nil { + return graphql.Null + } + res := resTmp.([]introspection.Type) + rctx.Result = res + ctx = ec.Tracer.StartFieldChildExecution(ctx) + return ec.marshalO__Type2ᚕgithubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚋintrospectionᚐType(ctx, field.Selections, res) +} + +func (ec *executionContext) ___Type_enumValues(ctx context.Context, field graphql.CollectedField, obj *introspection.Type) graphql.Marshaler { + ctx = ec.Tracer.StartFieldExecution(ctx, field) + defer func() { ec.Tracer.EndFieldExecution(ctx) }() + rctx := &graphql.ResolverContext{ + Object: "__Type", + Field: field, + Args: nil, + } + ctx = graphql.WithResolverContext(ctx, rctx) + rawArgs := field.ArgumentMap(ec.Variables) + args, err := ec.field___Type_enumValues_args(ctx, rawArgs) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + rctx.Args = args + ctx = ec.Tracer.StartFieldResolverExecution(ctx, rctx) + resTmp := ec.FieldMiddleware(ctx, obj, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return obj.EnumValues(args["includeDeprecated"].(bool)), nil + }) + if resTmp == nil { + return graphql.Null + } + res := resTmp.([]introspection.EnumValue) + rctx.Result = res + ctx = ec.Tracer.StartFieldChildExecution(ctx) + return ec.marshalO__EnumValue2ᚕgithubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚋintrospectionᚐEnumValue(ctx, field.Selections, res) +} + +func (ec *executionContext) ___Type_inputFields(ctx context.Context, field graphql.CollectedField, obj *introspection.Type) graphql.Marshaler { + ctx = ec.Tracer.StartFieldExecution(ctx, field) + defer func() { ec.Tracer.EndFieldExecution(ctx) }() + rctx := &graphql.ResolverContext{ + Object: "__Type", + Field: field, + Args: nil, + } + ctx = graphql.WithResolverContext(ctx, rctx) + ctx = ec.Tracer.StartFieldResolverExecution(ctx, rctx) + resTmp := ec.FieldMiddleware(ctx, obj, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return obj.InputFields(), nil + }) + if resTmp == nil { + return graphql.Null + } + res := resTmp.([]introspection.InputValue) + rctx.Result = res + ctx = ec.Tracer.StartFieldChildExecution(ctx) + return ec.marshalO__InputValue2ᚕgithubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚋintrospectionᚐInputValue(ctx, field.Selections, res) +} + +func (ec *executionContext) ___Type_ofType(ctx context.Context, field graphql.CollectedField, obj *introspection.Type) graphql.Marshaler { + ctx = ec.Tracer.StartFieldExecution(ctx, field) + defer func() { ec.Tracer.EndFieldExecution(ctx) }() + rctx := &graphql.ResolverContext{ + Object: "__Type", + Field: field, + Args: nil, + } + ctx = graphql.WithResolverContext(ctx, rctx) + ctx = ec.Tracer.StartFieldResolverExecution(ctx, rctx) + resTmp := ec.FieldMiddleware(ctx, obj, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return obj.OfType(), nil + }) + if resTmp == nil { + return graphql.Null + } + res := resTmp.(*introspection.Type) + rctx.Result = res + ctx = ec.Tracer.StartFieldChildExecution(ctx) + return ec.marshalO__Type2ᚖgithubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚋintrospectionᚐType(ctx, field.Selections, res) +} + +// endregion **************************** field.gotpl ***************************** + +// region **************************** input.gotpl ***************************** + +func (ec *executionContext) unmarshalInputUploadFile(ctx context.Context, v interface{}) (model.UploadFile, error) { + var it model.UploadFile + var asMap = v.(map[string]interface{}) + + for k, v := range asMap { + switch k { + case "id": + var err error + it.ID, err = ec.unmarshalNInt2int(ctx, v) + if err != nil { + return it, err + } + case "file": + var err error + it.File, err = ec.unmarshalNUpload2githubᚗcomᚋ99designsᚋgqlgenᚋexampleᚋfileuploadᚋmodelᚐUpload(ctx, v) + if err != nil { + return it, err + } + } + } + + return it, nil +} + +// endregion **************************** input.gotpl ***************************** + +// region ************************** interface.gotpl *************************** + +// endregion ************************** interface.gotpl *************************** + +// region **************************** object.gotpl **************************** + +var fileImplementors = []string{"File"} + +func (ec *executionContext) _File(ctx context.Context, sel ast.SelectionSet, obj *model.File) graphql.Marshaler { + fields := graphql.CollectFields(ctx, sel, fileImplementors) + + out := graphql.NewFieldSet(fields) + invalid := false + for i, field := range fields { + switch field.Name { + case "__typename": + out.Values[i] = graphql.MarshalString("File") + case "id": + out.Values[i] = ec._File_id(ctx, field, obj) + if out.Values[i] == graphql.Null { + invalid = true + } + default: + panic("unknown field " + strconv.Quote(field.Name)) + } + } + out.Dispatch() + if invalid { + return graphql.Null + } + return out +} + +var mutationImplementors = []string{"Mutation"} + +func (ec *executionContext) _Mutation(ctx context.Context, sel ast.SelectionSet) graphql.Marshaler { + fields := graphql.CollectFields(ctx, sel, mutationImplementors) + + ctx = graphql.WithResolverContext(ctx, &graphql.ResolverContext{ + Object: "Mutation", + }) + + out := graphql.NewFieldSet(fields) + invalid := false + for i, field := range fields { + switch field.Name { + case "__typename": + out.Values[i] = graphql.MarshalString("Mutation") + case "singleUpload": + out.Values[i] = ec._Mutation_singleUpload(ctx, field) + if out.Values[i] == graphql.Null { + invalid = true + } + case "singleUploadWithPayload": + out.Values[i] = ec._Mutation_singleUploadWithPayload(ctx, field) + if out.Values[i] == graphql.Null { + invalid = true + } + case "multipleUpload": + out.Values[i] = ec._Mutation_multipleUpload(ctx, field) + if out.Values[i] == graphql.Null { + invalid = true + } + default: + panic("unknown field " + strconv.Quote(field.Name)) + } + } + out.Dispatch() + if invalid { + return graphql.Null + } + return out +} + +var queryImplementors = []string{"Query"} + +func (ec *executionContext) _Query(ctx context.Context, sel ast.SelectionSet) graphql.Marshaler { + fields := graphql.CollectFields(ctx, sel, queryImplementors) + + ctx = graphql.WithResolverContext(ctx, &graphql.ResolverContext{ + Object: "Query", + }) + + out := graphql.NewFieldSet(fields) + invalid := false + for i, field := range fields { + switch field.Name { + case "__typename": + out.Values[i] = graphql.MarshalString("Query") + case "file": + field := field + out.Concurrently(i, func() (res graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + } + }() + res = ec._Query_file(ctx, field) + return res + }) + case "__type": + out.Values[i] = ec._Query___type(ctx, field) + case "__schema": + out.Values[i] = ec._Query___schema(ctx, field) + default: + panic("unknown field " + strconv.Quote(field.Name)) + } + } + out.Dispatch() + if invalid { + return graphql.Null + } + return out +} + +var __DirectiveImplementors = []string{"__Directive"} + +func (ec *executionContext) ___Directive(ctx context.Context, sel ast.SelectionSet, obj *introspection.Directive) graphql.Marshaler { + fields := graphql.CollectFields(ctx, sel, __DirectiveImplementors) + + out := graphql.NewFieldSet(fields) + invalid := false + for i, field := range fields { + switch field.Name { + case "__typename": + out.Values[i] = graphql.MarshalString("__Directive") + case "name": + out.Values[i] = ec.___Directive_name(ctx, field, obj) + if out.Values[i] == graphql.Null { + invalid = true + } + case "description": + out.Values[i] = ec.___Directive_description(ctx, field, obj) + case "locations": + out.Values[i] = ec.___Directive_locations(ctx, field, obj) + if out.Values[i] == graphql.Null { + invalid = true + } + case "args": + out.Values[i] = ec.___Directive_args(ctx, field, obj) + if out.Values[i] == graphql.Null { + invalid = true + } + default: + panic("unknown field " + strconv.Quote(field.Name)) + } + } + out.Dispatch() + if invalid { + return graphql.Null + } + return out +} + +var __EnumValueImplementors = []string{"__EnumValue"} + +func (ec *executionContext) ___EnumValue(ctx context.Context, sel ast.SelectionSet, obj *introspection.EnumValue) graphql.Marshaler { + fields := graphql.CollectFields(ctx, sel, __EnumValueImplementors) + + out := graphql.NewFieldSet(fields) + invalid := false + for i, field := range fields { + switch field.Name { + case "__typename": + out.Values[i] = graphql.MarshalString("__EnumValue") + case "name": + out.Values[i] = ec.___EnumValue_name(ctx, field, obj) + if out.Values[i] == graphql.Null { + invalid = true + } + case "description": + out.Values[i] = ec.___EnumValue_description(ctx, field, obj) + case "isDeprecated": + out.Values[i] = ec.___EnumValue_isDeprecated(ctx, field, obj) + if out.Values[i] == graphql.Null { + invalid = true + } + case "deprecationReason": + out.Values[i] = ec.___EnumValue_deprecationReason(ctx, field, obj) + default: + panic("unknown field " + strconv.Quote(field.Name)) + } + } + out.Dispatch() + if invalid { + return graphql.Null + } + return out +} + +var __FieldImplementors = []string{"__Field"} + +func (ec *executionContext) ___Field(ctx context.Context, sel ast.SelectionSet, obj *introspection.Field) graphql.Marshaler { + fields := graphql.CollectFields(ctx, sel, __FieldImplementors) + + out := graphql.NewFieldSet(fields) + invalid := false + for i, field := range fields { + switch field.Name { + case "__typename": + out.Values[i] = graphql.MarshalString("__Field") + case "name": + out.Values[i] = ec.___Field_name(ctx, field, obj) + if out.Values[i] == graphql.Null { + invalid = true + } + case "description": + out.Values[i] = ec.___Field_description(ctx, field, obj) + case "args": + out.Values[i] = ec.___Field_args(ctx, field, obj) + if out.Values[i] == graphql.Null { + invalid = true + } + case "type": + out.Values[i] = ec.___Field_type(ctx, field, obj) + if out.Values[i] == graphql.Null { + invalid = true + } + case "isDeprecated": + out.Values[i] = ec.___Field_isDeprecated(ctx, field, obj) + if out.Values[i] == graphql.Null { + invalid = true + } + case "deprecationReason": + out.Values[i] = ec.___Field_deprecationReason(ctx, field, obj) + default: + panic("unknown field " + strconv.Quote(field.Name)) + } + } + out.Dispatch() + if invalid { + return graphql.Null + } + return out +} + +var __InputValueImplementors = []string{"__InputValue"} + +func (ec *executionContext) ___InputValue(ctx context.Context, sel ast.SelectionSet, obj *introspection.InputValue) graphql.Marshaler { + fields := graphql.CollectFields(ctx, sel, __InputValueImplementors) + + out := graphql.NewFieldSet(fields) + invalid := false + for i, field := range fields { + switch field.Name { + case "__typename": + out.Values[i] = graphql.MarshalString("__InputValue") + case "name": + out.Values[i] = ec.___InputValue_name(ctx, field, obj) + if out.Values[i] == graphql.Null { + invalid = true + } + case "description": + out.Values[i] = ec.___InputValue_description(ctx, field, obj) + case "type": + out.Values[i] = ec.___InputValue_type(ctx, field, obj) + if out.Values[i] == graphql.Null { + invalid = true + } + case "defaultValue": + out.Values[i] = ec.___InputValue_defaultValue(ctx, field, obj) + default: + panic("unknown field " + strconv.Quote(field.Name)) + } + } + out.Dispatch() + if invalid { + return graphql.Null + } + return out +} + +var __SchemaImplementors = []string{"__Schema"} + +func (ec *executionContext) ___Schema(ctx context.Context, sel ast.SelectionSet, obj *introspection.Schema) graphql.Marshaler { + fields := graphql.CollectFields(ctx, sel, __SchemaImplementors) + + out := graphql.NewFieldSet(fields) + invalid := false + for i, field := range fields { + switch field.Name { + case "__typename": + out.Values[i] = graphql.MarshalString("__Schema") + case "types": + out.Values[i] = ec.___Schema_types(ctx, field, obj) + if out.Values[i] == graphql.Null { + invalid = true + } + case "queryType": + out.Values[i] = ec.___Schema_queryType(ctx, field, obj) + if out.Values[i] == graphql.Null { + invalid = true + } + case "mutationType": + out.Values[i] = ec.___Schema_mutationType(ctx, field, obj) + case "subscriptionType": + out.Values[i] = ec.___Schema_subscriptionType(ctx, field, obj) + case "directives": + out.Values[i] = ec.___Schema_directives(ctx, field, obj) + if out.Values[i] == graphql.Null { + invalid = true + } + default: + panic("unknown field " + strconv.Quote(field.Name)) + } + } + out.Dispatch() + if invalid { + return graphql.Null + } + return out +} + +var __TypeImplementors = []string{"__Type"} + +func (ec *executionContext) ___Type(ctx context.Context, sel ast.SelectionSet, obj *introspection.Type) graphql.Marshaler { + fields := graphql.CollectFields(ctx, sel, __TypeImplementors) + + out := graphql.NewFieldSet(fields) + invalid := false + for i, field := range fields { + switch field.Name { + case "__typename": + out.Values[i] = graphql.MarshalString("__Type") + case "kind": + out.Values[i] = ec.___Type_kind(ctx, field, obj) + if out.Values[i] == graphql.Null { + invalid = true + } + case "name": + out.Values[i] = ec.___Type_name(ctx, field, obj) + case "description": + out.Values[i] = ec.___Type_description(ctx, field, obj) + case "fields": + out.Values[i] = ec.___Type_fields(ctx, field, obj) + case "interfaces": + out.Values[i] = ec.___Type_interfaces(ctx, field, obj) + case "possibleTypes": + out.Values[i] = ec.___Type_possibleTypes(ctx, field, obj) + case "enumValues": + out.Values[i] = ec.___Type_enumValues(ctx, field, obj) + case "inputFields": + out.Values[i] = ec.___Type_inputFields(ctx, field, obj) + case "ofType": + out.Values[i] = ec.___Type_ofType(ctx, field, obj) + default: + panic("unknown field " + strconv.Quote(field.Name)) + } + } + out.Dispatch() + if invalid { + return graphql.Null + } + return out +} + +// endregion **************************** object.gotpl **************************** + +// region ***************************** type.gotpl ***************************** + +func (ec *executionContext) unmarshalNBoolean2bool(ctx context.Context, v interface{}) (bool, error) { + return graphql.UnmarshalBoolean(v) +} + +func (ec *executionContext) marshalNBoolean2bool(ctx context.Context, sel ast.SelectionSet, v bool) graphql.Marshaler { + return graphql.MarshalBoolean(v) +} + +func (ec *executionContext) marshalNFile2githubᚗcomᚋ99designsᚋgqlgenᚋexampleᚋfileuploadᚋmodelᚐFile(ctx context.Context, sel ast.SelectionSet, v model.File) graphql.Marshaler { + return ec._File(ctx, sel, &v) +} + +func (ec *executionContext) marshalNFile2ᚕgithubᚗcomᚋ99designsᚋgqlgenᚋexampleᚋfileuploadᚋmodelᚐFile(ctx context.Context, sel ast.SelectionSet, v []model.File) graphql.Marshaler { + ret := make(graphql.Array, len(v)) + var wg sync.WaitGroup + isLen1 := len(v) == 1 + if !isLen1 { + wg.Add(len(v)) + } + for i := range v { + i := i + rctx := &graphql.ResolverContext{ + Index: &i, + Result: &v[i], + } + ctx := graphql.WithResolverContext(ctx, rctx) + f := func(i int) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = nil + } + }() + if !isLen1 { + defer wg.Done() + } + ret[i] = ec.marshalNFile2githubᚗcomᚋ99designsᚋgqlgenᚋexampleᚋfileuploadᚋmodelᚐFile(ctx, sel, v[i]) + } + if isLen1 { + f(i) + } else { + go f(i) + } + + } + wg.Wait() + return ret +} + +func (ec *executionContext) marshalNFile2ᚖgithubᚗcomᚋ99designsᚋgqlgenᚋexampleᚋfileuploadᚋmodelᚐFile(ctx context.Context, sel ast.SelectionSet, v *model.File) graphql.Marshaler { + if v == nil { + if !ec.HasError(graphql.GetResolverContext(ctx)) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + return ec._File(ctx, sel, v) +} + +func (ec *executionContext) unmarshalNInt2int(ctx context.Context, v interface{}) (int, error) { + return graphql.UnmarshalInt(v) +} + +func (ec *executionContext) marshalNInt2int(ctx context.Context, sel ast.SelectionSet, v int) graphql.Marshaler { + return graphql.MarshalInt(v) +} + +func (ec *executionContext) unmarshalNString2string(ctx context.Context, v interface{}) (string, error) { + return graphql.UnmarshalString(v) +} + +func (ec *executionContext) marshalNString2string(ctx context.Context, sel ast.SelectionSet, v string) graphql.Marshaler { + return graphql.MarshalString(v) +} + +func (ec *executionContext) unmarshalNUpload2githubᚗcomᚋ99designsᚋgqlgenᚋexampleᚋfileuploadᚋmodelᚐUpload(ctx context.Context, v interface{}) (model.Upload, error) { + var res model.Upload + return res, res.UnmarshalGQL(v) +} + +func (ec *executionContext) marshalNUpload2githubᚗcomᚋ99designsᚋgqlgenᚋexampleᚋfileuploadᚋmodelᚐUpload(ctx context.Context, sel ast.SelectionSet, v model.Upload) graphql.Marshaler { + return v +} + +func (ec *executionContext) unmarshalNUpload2ᚕgithubᚗcomᚋ99designsᚋgqlgenᚋexampleᚋfileuploadᚋmodelᚐUpload(ctx context.Context, v interface{}) ([]model.Upload, error) { + var vSlice []interface{} + if v != nil { + if tmp1, ok := v.([]interface{}); ok { + vSlice = tmp1 + } else { + vSlice = []interface{}{v} + } + } + var err error + res := make([]model.Upload, len(vSlice)) + for i := range vSlice { + res[i], err = ec.unmarshalNUpload2githubᚗcomᚋ99designsᚋgqlgenᚋexampleᚋfileuploadᚋmodelᚐUpload(ctx, vSlice[i]) + if err != nil { + return nil, err + } + } + return res, nil +} + +func (ec *executionContext) marshalNUpload2ᚕgithubᚗcomᚋ99designsᚋgqlgenᚋexampleᚋfileuploadᚋmodelᚐUpload(ctx context.Context, sel ast.SelectionSet, v []model.Upload) graphql.Marshaler { + ret := make(graphql.Array, len(v)) + for i := range v { + ret[i] = ec.marshalNUpload2githubᚗcomᚋ99designsᚋgqlgenᚋexampleᚋfileuploadᚋmodelᚐUpload(ctx, sel, v[i]) + } + + return ret +} + +func (ec *executionContext) unmarshalNUploadFile2githubᚗcomᚋ99designsᚋgqlgenᚋexampleᚋfileuploadᚋmodelᚐUploadFile(ctx context.Context, v interface{}) (model.UploadFile, error) { + return ec.unmarshalInputUploadFile(ctx, v) +} + +func (ec *executionContext) marshalN__Directive2githubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚋintrospectionᚐDirective(ctx context.Context, sel ast.SelectionSet, v introspection.Directive) graphql.Marshaler { + return ec.___Directive(ctx, sel, &v) +} + +func (ec *executionContext) marshalN__Directive2ᚕgithubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚋintrospectionᚐDirective(ctx context.Context, sel ast.SelectionSet, v []introspection.Directive) graphql.Marshaler { + ret := make(graphql.Array, len(v)) + var wg sync.WaitGroup + isLen1 := len(v) == 1 + if !isLen1 { + wg.Add(len(v)) + } + for i := range v { + i := i + rctx := &graphql.ResolverContext{ + Index: &i, + Result: &v[i], + } + ctx := graphql.WithResolverContext(ctx, rctx) + f := func(i int) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = nil + } + }() + if !isLen1 { + defer wg.Done() + } + ret[i] = ec.marshalN__Directive2githubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚋintrospectionᚐDirective(ctx, sel, v[i]) + } + if isLen1 { + f(i) + } else { + go f(i) + } + + } + wg.Wait() + return ret +} + +func (ec *executionContext) unmarshalN__DirectiveLocation2string(ctx context.Context, v interface{}) (string, error) { + return graphql.UnmarshalString(v) +} + +func (ec *executionContext) marshalN__DirectiveLocation2string(ctx context.Context, sel ast.SelectionSet, v string) graphql.Marshaler { + return graphql.MarshalString(v) +} + +func (ec *executionContext) unmarshalN__DirectiveLocation2ᚕstring(ctx context.Context, v interface{}) ([]string, error) { + var vSlice []interface{} + if v != nil { + if tmp1, ok := v.([]interface{}); ok { + vSlice = tmp1 + } else { + vSlice = []interface{}{v} + } + } + var err error + res := make([]string, len(vSlice)) + for i := range vSlice { + res[i], err = ec.unmarshalN__DirectiveLocation2string(ctx, vSlice[i]) + if err != nil { + return nil, err + } + } + return res, nil +} + +func (ec *executionContext) marshalN__DirectiveLocation2ᚕstring(ctx context.Context, sel ast.SelectionSet, v []string) graphql.Marshaler { + ret := make(graphql.Array, len(v)) + var wg sync.WaitGroup + isLen1 := len(v) == 1 + if !isLen1 { + wg.Add(len(v)) + } + for i := range v { + i := i + rctx := &graphql.ResolverContext{ + Index: &i, + Result: &v[i], + } + ctx := graphql.WithResolverContext(ctx, rctx) + f := func(i int) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = nil + } + }() + if !isLen1 { + defer wg.Done() + } + ret[i] = ec.marshalN__DirectiveLocation2string(ctx, sel, v[i]) + } + if isLen1 { + f(i) + } else { + go f(i) + } + + } + wg.Wait() + return ret +} + +func (ec *executionContext) marshalN__EnumValue2githubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚋintrospectionᚐEnumValue(ctx context.Context, sel ast.SelectionSet, v introspection.EnumValue) graphql.Marshaler { + return ec.___EnumValue(ctx, sel, &v) +} + +func (ec *executionContext) marshalN__Field2githubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚋintrospectionᚐField(ctx context.Context, sel ast.SelectionSet, v introspection.Field) graphql.Marshaler { + return ec.___Field(ctx, sel, &v) +} + +func (ec *executionContext) marshalN__InputValue2githubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚋintrospectionᚐInputValue(ctx context.Context, sel ast.SelectionSet, v introspection.InputValue) graphql.Marshaler { + return ec.___InputValue(ctx, sel, &v) +} + +func (ec *executionContext) marshalN__InputValue2ᚕgithubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚋintrospectionᚐInputValue(ctx context.Context, sel ast.SelectionSet, v []introspection.InputValue) graphql.Marshaler { + ret := make(graphql.Array, len(v)) + var wg sync.WaitGroup + isLen1 := len(v) == 1 + if !isLen1 { + wg.Add(len(v)) + } + for i := range v { + i := i + rctx := &graphql.ResolverContext{ + Index: &i, + Result: &v[i], + } + ctx := graphql.WithResolverContext(ctx, rctx) + f := func(i int) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = nil + } + }() + if !isLen1 { + defer wg.Done() + } + ret[i] = ec.marshalN__InputValue2githubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚋintrospectionᚐInputValue(ctx, sel, v[i]) + } + if isLen1 { + f(i) + } else { + go f(i) + } + + } + wg.Wait() + return ret +} + +func (ec *executionContext) marshalN__Type2githubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚋintrospectionᚐType(ctx context.Context, sel ast.SelectionSet, v introspection.Type) graphql.Marshaler { + return ec.___Type(ctx, sel, &v) +} + +func (ec *executionContext) marshalN__Type2ᚕgithubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚋintrospectionᚐType(ctx context.Context, sel ast.SelectionSet, v []introspection.Type) graphql.Marshaler { + ret := make(graphql.Array, len(v)) + var wg sync.WaitGroup + isLen1 := len(v) == 1 + if !isLen1 { + wg.Add(len(v)) + } + for i := range v { + i := i + rctx := &graphql.ResolverContext{ + Index: &i, + Result: &v[i], + } + ctx := graphql.WithResolverContext(ctx, rctx) + f := func(i int) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = nil + } + }() + if !isLen1 { + defer wg.Done() + } + ret[i] = ec.marshalN__Type2githubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚋintrospectionᚐType(ctx, sel, v[i]) + } + if isLen1 { + f(i) + } else { + go f(i) + } + + } + wg.Wait() + return ret +} + +func (ec *executionContext) marshalN__Type2ᚖgithubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚋintrospectionᚐType(ctx context.Context, sel ast.SelectionSet, v *introspection.Type) graphql.Marshaler { + if v == nil { + if !ec.HasError(graphql.GetResolverContext(ctx)) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + return ec.___Type(ctx, sel, v) +} + +func (ec *executionContext) unmarshalN__TypeKind2string(ctx context.Context, v interface{}) (string, error) { + return graphql.UnmarshalString(v) +} + +func (ec *executionContext) marshalN__TypeKind2string(ctx context.Context, sel ast.SelectionSet, v string) graphql.Marshaler { + return graphql.MarshalString(v) +} + +func (ec *executionContext) unmarshalOBoolean2bool(ctx context.Context, v interface{}) (bool, error) { + return graphql.UnmarshalBoolean(v) +} + +func (ec *executionContext) marshalOBoolean2bool(ctx context.Context, sel ast.SelectionSet, v bool) graphql.Marshaler { + return graphql.MarshalBoolean(v) +} + +func (ec *executionContext) unmarshalOBoolean2ᚖbool(ctx context.Context, v interface{}) (*bool, error) { + if v == nil { + return nil, nil + } + res, err := ec.unmarshalOBoolean2bool(ctx, v) + return &res, err +} + +func (ec *executionContext) marshalOBoolean2ᚖbool(ctx context.Context, sel ast.SelectionSet, v *bool) graphql.Marshaler { + if v == nil { + return graphql.Null + } + return ec.marshalOBoolean2bool(ctx, sel, *v) +} + +func (ec *executionContext) marshalOFile2githubᚗcomᚋ99designsᚋgqlgenᚋexampleᚋfileuploadᚋmodelᚐFile(ctx context.Context, sel ast.SelectionSet, v model.File) graphql.Marshaler { + return ec._File(ctx, sel, &v) +} + +func (ec *executionContext) marshalOFile2ᚖgithubᚗcomᚋ99designsᚋgqlgenᚋexampleᚋfileuploadᚋmodelᚐFile(ctx context.Context, sel ast.SelectionSet, v *model.File) graphql.Marshaler { + if v == nil { + return graphql.Null + } + return ec._File(ctx, sel, v) +} + +func (ec *executionContext) unmarshalOString2string(ctx context.Context, v interface{}) (string, error) { + return graphql.UnmarshalString(v) +} + +func (ec *executionContext) marshalOString2string(ctx context.Context, sel ast.SelectionSet, v string) graphql.Marshaler { + return graphql.MarshalString(v) +} + +func (ec *executionContext) unmarshalOString2ᚖstring(ctx context.Context, v interface{}) (*string, error) { + if v == nil { + return nil, nil + } + res, err := ec.unmarshalOString2string(ctx, v) + return &res, err +} + +func (ec *executionContext) marshalOString2ᚖstring(ctx context.Context, sel ast.SelectionSet, v *string) graphql.Marshaler { + if v == nil { + return graphql.Null + } + return ec.marshalOString2string(ctx, sel, *v) +} + +func (ec *executionContext) marshalO__EnumValue2ᚕgithubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚋintrospectionᚐEnumValue(ctx context.Context, sel ast.SelectionSet, v []introspection.EnumValue) graphql.Marshaler { + ret := make(graphql.Array, len(v)) + var wg sync.WaitGroup + isLen1 := len(v) == 1 + if !isLen1 { + wg.Add(len(v)) + } + for i := range v { + i := i + rctx := &graphql.ResolverContext{ + Index: &i, + Result: &v[i], + } + ctx := graphql.WithResolverContext(ctx, rctx) + f := func(i int) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = nil + } + }() + if !isLen1 { + defer wg.Done() + } + ret[i] = ec.marshalN__EnumValue2githubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚋintrospectionᚐEnumValue(ctx, sel, v[i]) + } + if isLen1 { + f(i) + } else { + go f(i) + } + + } + wg.Wait() + return ret +} + +func (ec *executionContext) marshalO__Field2ᚕgithubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚋintrospectionᚐField(ctx context.Context, sel ast.SelectionSet, v []introspection.Field) graphql.Marshaler { + ret := make(graphql.Array, len(v)) + var wg sync.WaitGroup + isLen1 := len(v) == 1 + if !isLen1 { + wg.Add(len(v)) + } + for i := range v { + i := i + rctx := &graphql.ResolverContext{ + Index: &i, + Result: &v[i], + } + ctx := graphql.WithResolverContext(ctx, rctx) + f := func(i int) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = nil + } + }() + if !isLen1 { + defer wg.Done() + } + ret[i] = ec.marshalN__Field2githubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚋintrospectionᚐField(ctx, sel, v[i]) + } + if isLen1 { + f(i) + } else { + go f(i) + } + + } + wg.Wait() + return ret +} + +func (ec *executionContext) marshalO__InputValue2ᚕgithubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚋintrospectionᚐInputValue(ctx context.Context, sel ast.SelectionSet, v []introspection.InputValue) graphql.Marshaler { + ret := make(graphql.Array, len(v)) + var wg sync.WaitGroup + isLen1 := len(v) == 1 + if !isLen1 { + wg.Add(len(v)) + } + for i := range v { + i := i + rctx := &graphql.ResolverContext{ + Index: &i, + Result: &v[i], + } + ctx := graphql.WithResolverContext(ctx, rctx) + f := func(i int) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = nil + } + }() + if !isLen1 { + defer wg.Done() + } + ret[i] = ec.marshalN__InputValue2githubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚋintrospectionᚐInputValue(ctx, sel, v[i]) + } + if isLen1 { + f(i) + } else { + go f(i) + } + + } + wg.Wait() + return ret +} + +func (ec *executionContext) marshalO__Schema2githubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚋintrospectionᚐSchema(ctx context.Context, sel ast.SelectionSet, v introspection.Schema) graphql.Marshaler { + return ec.___Schema(ctx, sel, &v) +} + +func (ec *executionContext) marshalO__Schema2ᚖgithubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚋintrospectionᚐSchema(ctx context.Context, sel ast.SelectionSet, v *introspection.Schema) graphql.Marshaler { + if v == nil { + return graphql.Null + } + return ec.___Schema(ctx, sel, v) +} + +func (ec *executionContext) marshalO__Type2githubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚋintrospectionᚐType(ctx context.Context, sel ast.SelectionSet, v introspection.Type) graphql.Marshaler { + return ec.___Type(ctx, sel, &v) +} + +func (ec *executionContext) marshalO__Type2ᚕgithubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚋintrospectionᚐType(ctx context.Context, sel ast.SelectionSet, v []introspection.Type) graphql.Marshaler { + ret := make(graphql.Array, len(v)) + var wg sync.WaitGroup + isLen1 := len(v) == 1 + if !isLen1 { + wg.Add(len(v)) + } + for i := range v { + i := i + rctx := &graphql.ResolverContext{ + Index: &i, + Result: &v[i], + } + ctx := graphql.WithResolverContext(ctx, rctx) + f := func(i int) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = nil + } + }() + if !isLen1 { + defer wg.Done() + } + ret[i] = ec.marshalN__Type2githubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚋintrospectionᚐType(ctx, sel, v[i]) + } + if isLen1 { + f(i) + } else { + go f(i) + } + + } + wg.Wait() + return ret +} + +func (ec *executionContext) marshalO__Type2ᚖgithubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚋintrospectionᚐType(ctx context.Context, sel ast.SelectionSet, v *introspection.Type) graphql.Marshaler { + if v == nil { + return graphql.Null + } + return ec.___Type(ctx, sel, v) +} + +// endregion ***************************** type.gotpl ***************************** diff --git a/example/fileupload/model/generated.go b/example/fileupload/model/generated.go new file mode 100644 index 0000000000..36c0f4ef2e --- /dev/null +++ b/example/fileupload/model/generated.go @@ -0,0 +1,12 @@ +// Code generated by github.com/99designs/gqlgen, DO NOT EDIT. + +package model + +type File struct { + ID int `json:"id"` +} + +type UploadFile struct { + ID int `json:"id"` + File Upload `json:"file"` +} diff --git a/example/fileupload/model/model.go b/example/fileupload/model/model.go new file mode 100644 index 0000000000..09e28765cc --- /dev/null +++ b/example/fileupload/model/model.go @@ -0,0 +1,33 @@ +package model + +import ( + "github.com/99designs/gqlgen/graphql" + "github.com/pkg/errors" + "io" + "mime/multipart" +) + +type Upload struct { + File multipart.File + FileName string + Size int64 +} + +// if the type referenced in .gqlgen.yml is a function that returns a marshaller we can use it to encode and decode +// onto any existing go type. +func (u *Upload) UnmarshalGQL(v interface{}) error { + data, ok := v.(graphql.Upload) + if !ok { + return errors.New("upload should be a graphql Upload") + } + *u = Upload{ + File: data.File, + FileName: data.Filename, + Size: data.Size, + } + return nil +} + +func (u Upload) MarshalGQL(w io.Writer) { + io.Copy(w, u.File) +} diff --git a/example/fileupload/readme.md b/example/fileupload/readme.md new file mode 100644 index 0000000000..b8eb9cc2ed --- /dev/null +++ b/example/fileupload/readme.md @@ -0,0 +1,126 @@ +### fileupload example + +This server demonstrates how to handle file upload + +to run this server +```bash +go run ./example/fileupload/server/server.go +``` + +and open http://localhost:8080 in your browser + + +## Examples + +curl localhost:3001/graphql \ + -F operations='{ "query": "mutation ($file: Upload!) { singleUpload(file: $file) { id } }", "variables": { "file": null } }' \ + -F map='{ "0": ["variables.file"] }' \ + -F 0=@a.txt + + +### Single file + +#### Operations + +```js +{ + query: ` + mutation($file: Upload!) { + uploadFile(file: $file) { + id + } + } + `, + variables: { + file: File // a.txt + } +} +``` + +#### cURL request + +```shell +curl localhost:3001/graphql \ + -F operations='{ "query": "mutation ($file: Upload!) { singleUpload(file: $file) { id } }", "variables": { "file": null } }' \ + -F map='{ "0": ["variables.file"] }' \ + -F 0=@a.txt +``` + +#### Request payload + +``` +--------------------------cec8e8123c05ba25 +Content-Disposition: form-data; name="operations" + +{ "query": "mutation ($file: Upload!) { singleUpload(file: $file) { id } }", "variables": { "file": null } } +--------------------------cec8e8123c05ba25 +Content-Disposition: form-data; name="map" + +{ "0": ["variables.file"] } +--------------------------cec8e8123c05ba25 +Content-Disposition: form-data; name="0"; filename="a.txt" +Content-Type: text/plain + +Alpha file content. + +--------------------------cec8e8123c05ba25-- +``` + +### File list + +#### Operations + +```js +{ + query: ` + mutation($files: [Upload!]!) { + multipleUpload(files: $files) { + id + } + } + `, + variables: { + files: [ + File, // b.txt + File // c.txt + ] + } +} +``` + +#### cURL request + +```shell +curl localhost:3001/graphql \ + -F operations='{ "query": "mutation($files: [Upload!]!) { multipleUpload(files: $files) { id } }", "variables": { "files": [null, null] } }' \ + -F map='{ "0": ["variables.files.0"], "1": ["variables.files.1"] }' \ + -F 0=@b.txt \ + -F 1=@c.txt +``` + +#### Request payload + +``` +--------------------------ec62457de6331cad +Content-Disposition: form-data; name="operations" + +{ "query": "mutation($files: [Upload!]!) { multipleUpload(files: $files) { id } }", "variables": { "files": [null, null] } } +--------------------------ec62457de6331cad +Content-Disposition: form-data; name="map" + +{ "0": ["variables.files.0"], "1": ["variables.files.1"] } +--------------------------ec62457de6331cad +Content-Disposition: form-data; name="0"; filename="b.txt" +Content-Type: text/plain + +Bravo file content. + +--------------------------ec62457de6331cad +Content-Disposition: form-data; name="1"; filename="c.txt" +Content-Type: text/plain + +Charlie file content. + +--------------------------ec62457de6331cad-- +``` + diff --git a/example/fileupload/resolvers.go b/example/fileupload/resolvers.go new file mode 100644 index 0000000000..2b0f5df210 --- /dev/null +++ b/example/fileupload/resolvers.go @@ -0,0 +1,50 @@ +//go:generate go run ../../testdata/gqlgen.go + +package fileupload + +import ( + "context" + "fmt" + + "github.com/99designs/gqlgen/example/fileupload/model" +) + +type Resolver struct { + SingleUploadFunc func(ctx context.Context, file model.Upload) (*model.File, error) + SingleUploadWithPayloadFunc func(ctx context.Context, req model.UploadFile) (*model.File, error) + MultipleUploadFunc func(ctx context.Context, files []model.Upload) ([]model.File, error) +} + +func (r *Resolver) Mutation() MutationResolver { + return &mutationResolver{r} +} +func (r *Resolver) Query() QueryResolver { + return &queryResolver{r} +} + +type mutationResolver struct{ *Resolver } + +func (r *mutationResolver) SingleUpload(ctx context.Context, file model.Upload) (*model.File, error) { + if r.SingleUploadFunc != nil { + return r.SingleUploadFunc(ctx, file) + } + return nil, fmt.Errorf("not implemented") +} +func (r *mutationResolver) SingleUploadWithPayload(ctx context.Context, req model.UploadFile) (*model.File, error) { + if r.SingleUploadWithPayloadFunc != nil { + return r.SingleUploadWithPayloadFunc(ctx, req) + } + return nil, fmt.Errorf("not implemented") +} +func (r *mutationResolver) MultipleUpload(ctx context.Context, files []model.Upload) ([]model.File, error) { + if r.MultipleUploadFunc != nil { + return r.MultipleUploadFunc(ctx, files) + } + return nil, fmt.Errorf("not implemented") +} + +type queryResolver struct{ *Resolver } + +func (r *queryResolver) File(ctx context.Context, fileName string) (*model.File, error) { + return nil, fmt.Errorf("not implemented") +} diff --git a/example/fileupload/schema.graphql b/example/fileupload/schema.graphql new file mode 100644 index 0000000000..a63df78b90 --- /dev/null +++ b/example/fileupload/schema.graphql @@ -0,0 +1,27 @@ +# The query type, represents all of the entry points into our object graph + +# The Upload type, represents the response of uploading a file +scalar Upload + +# The File type, represents the response of uploading a file +type File { + id: Int! +} + +# The UploadFile type, represents the request for uploading a file with certain payload +input UploadFile { + id: Int! + file: Upload! +} + +type Query { + file(fileName: String!): File +} + +# The mutation type, represents all updates we can make to our data +type Mutation { + singleUpload(file: Upload!): File! + singleUploadWithPayload(req: UploadFile!): File! + multipleUpload(files: [Upload!]!): [File!]! +} + diff --git a/example/fileupload/server/server.go b/example/fileupload/server/server.go new file mode 100644 index 0000000000..d47009295f --- /dev/null +++ b/example/fileupload/server/server.go @@ -0,0 +1,15 @@ +package main + +import ( + "log" + "net/http" + + "github.com/99designs/gqlgen/example/fileupload" + "github.com/99designs/gqlgen/handler" +) + +func main() { + http.Handle("/", handler.Playground("File Upload Demo", "/query")) + http.Handle("/query", handler.GraphQL(fileupload.NewExecutableSchema(fileupload.Config{Resolvers: &fileupload.Resolver{}}))) + log.Fatal(http.ListenAndServe(":8086", nil)) +} diff --git a/graphql/upload.go b/graphql/upload.go new file mode 100644 index 0000000000..98f875814e --- /dev/null +++ b/graphql/upload.go @@ -0,0 +1,11 @@ +package graphql + +import ( + "mime/multipart" +) + +type Upload struct { + File multipart.File + Filename string + Size int64 +} diff --git a/handler/graphql.go b/handler/graphql.go index 585897a924..aaa4a9ae88 100644 --- a/handler/graphql.go +++ b/handler/graphql.go @@ -1,11 +1,15 @@ package handler import ( + "bytes" "context" "encoding/json" + "errors" "fmt" "io" "net/http" + "regexp" + "strconv" "strings" "time" @@ -325,9 +329,17 @@ func (gh *graphqlHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { } } case http.MethodPost: - if err := jsonDecode(r.Body, &reqParams); err != nil { - sendErrorf(w, http.StatusBadRequest, "json body could not be decoded: "+err.Error()) - return + contentType := strings.SplitN(r.Header.Get("Content-Type"), ";", 2)[0] + if contentType == "multipart/form-data" { + if err := processMultipart(r, &reqParams); err != nil { + sendErrorf(w, http.StatusBadRequest, "multipart body could not be decoded: "+err.Error()) + return + } + } else { + if err := jsonDecode(r.Body, &reqParams); err != nil { + sendErrorf(w, http.StatusBadRequest, "json body could not be decoded: "+err.Error()) + return + } } default: w.WriteHeader(http.StatusMethodNotAllowed) @@ -479,3 +491,95 @@ func sendError(w http.ResponseWriter, code int, errors ...*gqlerror.Error) { func sendErrorf(w http.ResponseWriter, code int, format string, args ...interface{}) { sendError(w, code, &gqlerror.Error{Message: fmt.Sprintf(format, args...)}) } + +func processMultipart(r *http.Request, request *params) error { + // Parse multipart form + if err := r.ParseMultipartForm(1024); err != nil { + return err + } + + // Unmarshal uploads + var uploads = map[graphql.Upload][]string{} + var uploadsMap = map[string][]string{} + if err := json.Unmarshal([]byte(r.Form.Get("map")), &uploadsMap); err != nil { + return err + } else { + for key, path := range uploadsMap { + if file, header, err := r.FormFile(key); err != nil { + panic(err) + //w.WriteHeader(http.StatusInternalServerError) + //return + } else { + uploads[graphql.Upload{ + File: file, + Size: header.Size, + Filename: header.Filename, + }] = path + } + } + } + + var operations interface{} + + // Unmarshal operations + if err := jsonDecode(bytes.NewBuffer([]byte(r.Form.Get("operations"))), &operations); err != nil { + return err + } + + // addUploadToOperations uploads to operations + for file, paths := range uploads { + for _, path := range paths { + addUploadToOperations(operations, file, path) + } + } + + switch data := operations.(type) { + case map[string]interface{}: + if value, ok := data["operationName"]; ok && value != nil { + request.OperationName = value.(string) + } + if value, ok := data["query"]; ok && value != nil { + request.Query = value.(string) + } + if value, ok := data["variables"]; ok && value != nil { + request.Variables = value.(map[string]interface{}) + } + return nil + default: + return errors.New("bad request") + } + + return errors.New("invalid operation") +} + +func addUploadToOperations(operations interface{}, upload graphql.Upload, path string) error { + var parts []interface{} + for _, p := range strings.Split(path, ".") { + if isNumber, err := regexp.MatchString(`\d+`, p); err != nil { + return err + } else if isNumber { + index, _ := strconv.Atoi(p) + parts = append(parts, index) + } else { + parts = append(parts, p) + } + } + for i, p := range parts { + last := i == len(parts)-1 + switch idx := p.(type) { + case string: + if last { + operations.(map[string]interface{})[idx] = upload + } else { + operations = operations.(map[string]interface{})[idx] + } + case int: + if last { + operations.([]interface{})[idx] = upload + } else { + operations = operations.([]interface{})[idx] + } + } + } + return nil +} diff --git a/handler/graphql_test.go b/handler/graphql_test.go index e6c2d9063e..12d61ca24a 100644 --- a/handler/graphql_test.go +++ b/handler/graphql_test.go @@ -1,12 +1,19 @@ package handler import ( + "bytes" + "context" + "mime/multipart" "net/http" "net/http/httptest" "strings" "testing" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/99designs/gqlgen/graphql" + "github.com/vektah/gqlparser/ast" ) func TestHandlerPOST(t *testing.T) { @@ -112,6 +119,112 @@ func TestHandlerGET(t *testing.T) { }) } +func TestFileUpload(t *testing.T) { + t.Run("valid single file upload", func(t *testing.T) { + stub := &executableSchemaStub{ + MutationFunc: func(ctx context.Context, op *ast.OperationDefinition) *graphql.Response { + require.Equal(t, len(op.VariableDefinitions), 1) + require.Equal(t, op.VariableDefinitions[0].Variable, "file") + return &graphql.Response{Data: []byte(`{"name":"test"}`)} + }, + } + handler := GraphQL(stub) + + bodyBuf := &bytes.Buffer{} + bodyWriter := multipart.NewWriter(bodyBuf) + err := bodyWriter.WriteField("operations", `{ "query": "mutation ($file: Upload!) { singleUpload(file: $file) { fileName, size } }", "variables": { "file": null } }`) + require.NoError(t, err) + err = bodyWriter.WriteField("map", `{ "0": ["variables.file"] }`) + require.NoError(t, err) + w, err := bodyWriter.CreateFormFile("0", "a.txt") + require.NoError(t, err) + _, err = w.Write([]byte("test")) + require.NoError(t, err) + err = bodyWriter.Close() + require.NoError(t, err) + + req, err := http.NewRequest("POST", "/graphql", bodyBuf) + require.NoError(t, err) + + contentType := bodyWriter.FormDataContentType() + req.Header.Set("Content-Type", contentType) + resp := httptest.NewRecorder() + handler.ServeHTTP(resp, req) + require.Equal(t, http.StatusOK, resp.Code) + require.Equal(t, `{"data":{"name":"test"}}`, resp.Body.String()) + }) + + t.Run("valid single file upload with payload", func(t *testing.T) { + stub := &executableSchemaStub{} + stub.MutationFunc = func(ctx context.Context, op *ast.OperationDefinition) *graphql.Response { + require.Equal(t, len(op.VariableDefinitions), 1) + require.Equal(t, op.VariableDefinitions[0].Variable, "file") + return &graphql.Response{Data: []byte(`{"name":"test"}`)} + } + handler := GraphQL(stub) + + bodyBuf := &bytes.Buffer{} + bodyWriter := multipart.NewWriter(bodyBuf) + err := bodyWriter.WriteField("operations", `{ "query": "mutation ($file: Upload!) { singleUpload(file: $file) { fileName, size } }", "variables": { "file": null } }`) + require.NoError(t, err) + err = bodyWriter.WriteField("map", `{ "0": ["variables.file"] }`) + require.NoError(t, err) + w, err := bodyWriter.CreateFormFile("0", "a.txt") + require.NoError(t, err) + _, err = w.Write([]byte("test")) + require.NoError(t, err) + err = bodyWriter.Close() + require.NoError(t, err) + + req, err := http.NewRequest("POST", "/graphql", bodyBuf) + require.NoError(t, err) + + contentType := bodyWriter.FormDataContentType() + req.Header.Set("Content-Type", contentType) + resp := httptest.NewRecorder() + handler.ServeHTTP(resp, req) + require.Equal(t, http.StatusOK, resp.Code) + require.Equal(t, `{"data":{"name":"test"}}`, resp.Body.String()) + }) + + t.Run("valid file list upload", func(t *testing.T) { + stub := &executableSchemaStub{} + stub.MutationFunc = func(ctx context.Context, op *ast.OperationDefinition) *graphql.Response { + require.Equal(t, len(op.VariableDefinitions), 1) + require.Equal(t, op.VariableDefinitions[0].Variable, "files") + return &graphql.Response{Data: []byte(`{"name":"test"}`)} + } + handler := GraphQL(stub) + + bodyBuf := &bytes.Buffer{} + bodyWriter := multipart.NewWriter(bodyBuf) + err := bodyWriter.WriteField("operations", `{ "query": "mutation($files: [Upload!]!) { multipleUpload(files: $files) { fileName } }", "variables": { "files": [null, null] } }`) + require.NoError(t, err) + err = bodyWriter.WriteField("map", `{ "0": ["variables.files.0"], "1": ["variables.files.1"] }`) + require.NoError(t, err) + w0, err := bodyWriter.CreateFormFile("0", "a.txt") + require.NoError(t, err) + _, err = w0.Write([]byte("test")) + require.NoError(t, err) + w1, err := bodyWriter.CreateFormFile("1", "b.txt") + require.NoError(t, err) + _, err = w1.Write([]byte("test")) + require.NoError(t, err) + err = bodyWriter.Close() + require.NoError(t, err) + + req, err := http.NewRequest("POST", "/graphql", bodyBuf) + require.NoError(t, err) + + req.Header.Set("Content-Type", bodyWriter.FormDataContentType()) + resp := httptest.NewRecorder() + handler.ServeHTTP(resp, req) + require.Equal(t, http.StatusOK, resp.Code) + require.Equal(t, `{"data":{"name":"test"}}`, resp.Body.String()) + }) + +} + func TestHandlerOptions(t *testing.T) { h := GraphQL(&executableSchemaStub{}) diff --git a/handler/stub.go b/handler/stub.go index d237e18892..3a53060e8b 100644 --- a/handler/stub.go +++ b/handler/stub.go @@ -9,7 +9,8 @@ import ( ) type executableSchemaStub struct { - NextResp chan struct{} + NextResp chan struct{} + MutationFunc func(ctx context.Context, op *ast.OperationDefinition) *graphql.Response } var _ graphql.ExecutableSchema = &executableSchemaStub{} @@ -22,6 +23,7 @@ func (e *executableSchemaStub) Schema() *ast.Schema { user(id: Int): User! } type User { name: String! } + scalar Upload `}) } @@ -34,6 +36,9 @@ func (e *executableSchemaStub) Query(ctx context.Context, op *ast.OperationDefin } func (e *executableSchemaStub) Mutation(ctx context.Context, op *ast.OperationDefinition) *graphql.Response { + if e.MutationFunc != nil { + return e.MutationFunc(ctx, op) + } return graphql.ErrorResponse(ctx, "mutations are not supported") } diff --git a/handler/websocket_test.go b/handler/websocket_test.go index f8675475c9..a49ef257d1 100644 --- a/handler/websocket_test.go +++ b/handler/websocket_test.go @@ -13,7 +13,7 @@ import ( func TestWebsocket(t *testing.T) { next := make(chan struct{}) - h := GraphQL(&executableSchemaStub{next}) + h := GraphQL(&executableSchemaStub{NextResp: next}) srv := httptest.NewServer(h) defer srv.Close() @@ -125,7 +125,7 @@ func TestWebsocket(t *testing.T) { func TestWebsocketWithKeepAlive(t *testing.T) { next := make(chan struct{}) - h := GraphQL(&executableSchemaStub{next}, WebsocketKeepAliveDuration(10*time.Millisecond)) + h := GraphQL(&executableSchemaStub{NextResp: next}, WebsocketKeepAliveDuration(10*time.Millisecond)) srv := httptest.NewServer(h) defer srv.Close() From 5afb6b4059b8d13206d19ad5ad4277698c118370 Mon Sep 17 00:00:00 2001 From: hantonelli Date: Sun, 3 Mar 2019 00:50:42 +0000 Subject: [PATCH 03/39] Add file upload to default schema --- codegen/config/config.go | 8 +++++ example/fileupload/.gqlgen.yml | 4 --- example/fileupload/generated.go | 48 ++++++++++++--------------- example/fileupload/model/generated.go | 8 +++-- example/fileupload/resolvers.go | 9 ++--- example/fileupload/schema.graphql | 3 -- graphql/upload.go | 16 +++++++++ 7 files changed, 57 insertions(+), 39 deletions(-) diff --git a/codegen/config/config.go b/codegen/config/config.go index 1638239d00..08fafb43ee 100644 --- a/codegen/config/config.go +++ b/codegen/config/config.go @@ -345,6 +345,7 @@ func (c *Config) InjectBuiltins(s *ast.Schema) { "Boolean": {Model: StringList{"github.com/99designs/gqlgen/graphql.Boolean"}}, "Time": {Model: StringList{"github.com/99designs/gqlgen/graphql.Time"}}, "Map": {Model: StringList{"github.com/99designs/gqlgen/graphql.Map"}}, + "Upload": {Model: StringList{"github.com/99designs/gqlgen/graphql.Upload"}}, "ID": { Model: StringList{ "github.com/99designs/gqlgen/graphql.ID", @@ -365,6 +366,13 @@ func (c *Config) LoadSchema() (*ast.Schema, map[string]string, error) { var sources []*ast.Source + // Add upload scalar + sources = append(sources, &ast.Source{ + Name: "uploadPrelude.graphql", + Input: "\"The `Upload` scalar type represents a multipart file upload.\"\nscalar Upload", + BuiltIn: true, + }) + for _, filename := range c.SchemaFilename { filename = filepath.ToSlash(filename) var err error diff --git a/example/fileupload/.gqlgen.yml b/example/fileupload/.gqlgen.yml index 38f5f7c81d..b71c035a38 100644 --- a/example/fileupload/.gqlgen.yml +++ b/example/fileupload/.gqlgen.yml @@ -1,6 +1,2 @@ model: filename: model/generated.go - -models: - Upload: - model: github.com/99designs/gqlgen/example/fileupload/model.Upload diff --git a/example/fileupload/generated.go b/example/fileupload/generated.go index aae66eeebb..8ef6879a06 100644 --- a/example/fileupload/generated.go +++ b/example/fileupload/generated.go @@ -47,9 +47,9 @@ type ComplexityRoot struct { } Mutation struct { - SingleUpload func(childComplexity int, file model.Upload) int + SingleUpload func(childComplexity int, file graphql.Upload) int SingleUploadWithPayload func(childComplexity int, req model.UploadFile) int - MultipleUpload func(childComplexity int, files []model.Upload) int + MultipleUpload func(childComplexity int, files []graphql.Upload) int } Query struct { @@ -58,9 +58,9 @@ type ComplexityRoot struct { } type MutationResolver interface { - SingleUpload(ctx context.Context, file model.Upload) (*model.File, error) + SingleUpload(ctx context.Context, file graphql.Upload) (*model.File, error) SingleUploadWithPayload(ctx context.Context, req model.UploadFile) (*model.File, error) - MultipleUpload(ctx context.Context, files []model.Upload) ([]model.File, error) + MultipleUpload(ctx context.Context, files []graphql.Upload) ([]model.File, error) } type QueryResolver interface { File(ctx context.Context, fileName string) (*model.File, error) @@ -98,7 +98,7 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return 0, false } - return e.complexity.Mutation.SingleUpload(childComplexity, args["file"].(model.Upload)), true + return e.complexity.Mutation.SingleUpload(childComplexity, args["file"].(graphql.Upload)), true case "Mutation.SingleUploadWithPayload": if e.complexity.Mutation.SingleUploadWithPayload == nil { @@ -122,7 +122,7 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return 0, false } - return e.complexity.Mutation.MultipleUpload(childComplexity, args["files"].([]model.Upload)), true + return e.complexity.Mutation.MultipleUpload(childComplexity, args["files"].([]graphql.Upload)), true case "Query.File": if e.complexity.Query.File == nil { @@ -215,9 +215,6 @@ func (ec *executionContext) introspectType(name string) (*introspection.Type, er var parsedSchema = gqlparser.MustLoadSchema( &ast.Source{Name: "schema.graphql", Input: `# The query type, represents all of the entry points into our object graph -# The Upload type, represents the response of uploading a file -scalar Upload - # The File type, represents the response of uploading a file type File { id: Int! @@ -250,9 +247,9 @@ type Mutation { func (ec *executionContext) field_Mutation_multipleUpload_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) { var err error args := map[string]interface{}{} - var arg0 []model.Upload + var arg0 []graphql.Upload if tmp, ok := rawArgs["files"]; ok { - arg0, err = ec.unmarshalNUpload2ᚕgithubᚗcomᚋ99designsᚋgqlgenᚋexampleᚋfileuploadᚋmodelᚐUpload(ctx, tmp) + arg0, err = ec.unmarshalNUpload2ᚕgithubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚐUpload(ctx, tmp) if err != nil { return nil, err } @@ -278,9 +275,9 @@ func (ec *executionContext) field_Mutation_singleUploadWithPayload_args(ctx cont func (ec *executionContext) field_Mutation_singleUpload_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) { var err error args := map[string]interface{}{} - var arg0 model.Upload + var arg0 graphql.Upload if tmp, ok := rawArgs["file"]; ok { - arg0, err = ec.unmarshalNUpload2githubᚗcomᚋ99designsᚋgqlgenᚋexampleᚋfileuploadᚋmodelᚐUpload(ctx, tmp) + arg0, err = ec.unmarshalNUpload2githubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚐUpload(ctx, tmp) if err != nil { return nil, err } @@ -394,7 +391,7 @@ func (ec *executionContext) _Mutation_singleUpload(ctx context.Context, field gr ctx = ec.Tracer.StartFieldResolverExecution(ctx, rctx) resTmp := ec.FieldMiddleware(ctx, nil, func(rctx context.Context) (interface{}, error) { ctx = rctx // use context from middleware stack in children - return ec.resolvers.Mutation().SingleUpload(rctx, args["file"].(model.Upload)) + return ec.resolvers.Mutation().SingleUpload(rctx, args["file"].(graphql.Upload)) }) if resTmp == nil { if !ec.HasError(rctx) { @@ -460,7 +457,7 @@ func (ec *executionContext) _Mutation_multipleUpload(ctx context.Context, field ctx = ec.Tracer.StartFieldResolverExecution(ctx, rctx) resTmp := ec.FieldMiddleware(ctx, nil, func(rctx context.Context) (interface{}, error) { ctx = rctx // use context from middleware stack in children - return ec.resolvers.Mutation().MultipleUpload(rctx, args["files"].([]model.Upload)) + return ec.resolvers.Mutation().MultipleUpload(rctx, args["files"].([]graphql.Upload)) }) if resTmp == nil { if !ec.HasError(rctx) { @@ -1370,7 +1367,7 @@ func (ec *executionContext) unmarshalInputUploadFile(ctx context.Context, v inte } case "file": var err error - it.File, err = ec.unmarshalNUpload2githubᚗcomᚋ99designsᚋgqlgenᚋexampleᚋfileuploadᚋmodelᚐUpload(ctx, v) + it.File, err = ec.unmarshalNUpload2githubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚐUpload(ctx, v) if err != nil { return it, err } @@ -1817,16 +1814,15 @@ func (ec *executionContext) marshalNString2string(ctx context.Context, sel ast.S return graphql.MarshalString(v) } -func (ec *executionContext) unmarshalNUpload2githubᚗcomᚋ99designsᚋgqlgenᚋexampleᚋfileuploadᚋmodelᚐUpload(ctx context.Context, v interface{}) (model.Upload, error) { - var res model.Upload - return res, res.UnmarshalGQL(v) +func (ec *executionContext) unmarshalNUpload2githubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚐUpload(ctx context.Context, v interface{}) (graphql.Upload, error) { + return graphql.UnmarshalUpload(v) } -func (ec *executionContext) marshalNUpload2githubᚗcomᚋ99designsᚋgqlgenᚋexampleᚋfileuploadᚋmodelᚐUpload(ctx context.Context, sel ast.SelectionSet, v model.Upload) graphql.Marshaler { - return v +func (ec *executionContext) marshalNUpload2githubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚐUpload(ctx context.Context, sel ast.SelectionSet, v graphql.Upload) graphql.Marshaler { + return graphql.MarshalUpload(v) } -func (ec *executionContext) unmarshalNUpload2ᚕgithubᚗcomᚋ99designsᚋgqlgenᚋexampleᚋfileuploadᚋmodelᚐUpload(ctx context.Context, v interface{}) ([]model.Upload, error) { +func (ec *executionContext) unmarshalNUpload2ᚕgithubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚐUpload(ctx context.Context, v interface{}) ([]graphql.Upload, error) { var vSlice []interface{} if v != nil { if tmp1, ok := v.([]interface{}); ok { @@ -1836,9 +1832,9 @@ func (ec *executionContext) unmarshalNUpload2ᚕgithubᚗcomᚋ99designsᚋgqlge } } var err error - res := make([]model.Upload, len(vSlice)) + res := make([]graphql.Upload, len(vSlice)) for i := range vSlice { - res[i], err = ec.unmarshalNUpload2githubᚗcomᚋ99designsᚋgqlgenᚋexampleᚋfileuploadᚋmodelᚐUpload(ctx, vSlice[i]) + res[i], err = ec.unmarshalNUpload2githubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚐUpload(ctx, vSlice[i]) if err != nil { return nil, err } @@ -1846,10 +1842,10 @@ func (ec *executionContext) unmarshalNUpload2ᚕgithubᚗcomᚋ99designsᚋgqlge return res, nil } -func (ec *executionContext) marshalNUpload2ᚕgithubᚗcomᚋ99designsᚋgqlgenᚋexampleᚋfileuploadᚋmodelᚐUpload(ctx context.Context, sel ast.SelectionSet, v []model.Upload) graphql.Marshaler { +func (ec *executionContext) marshalNUpload2ᚕgithubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚐUpload(ctx context.Context, sel ast.SelectionSet, v []graphql.Upload) graphql.Marshaler { ret := make(graphql.Array, len(v)) for i := range v { - ret[i] = ec.marshalNUpload2githubᚗcomᚋ99designsᚋgqlgenᚋexampleᚋfileuploadᚋmodelᚐUpload(ctx, sel, v[i]) + ret[i] = ec.marshalNUpload2githubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚐUpload(ctx, sel, v[i]) } return ret diff --git a/example/fileupload/model/generated.go b/example/fileupload/model/generated.go index 36c0f4ef2e..3b19ef846c 100644 --- a/example/fileupload/model/generated.go +++ b/example/fileupload/model/generated.go @@ -2,11 +2,15 @@ package model +import ( + "github.com/99designs/gqlgen/graphql" +) + type File struct { ID int `json:"id"` } type UploadFile struct { - ID int `json:"id"` - File Upload `json:"file"` + ID int `json:"id"` + File graphql.Upload `json:"file"` } diff --git a/example/fileupload/resolvers.go b/example/fileupload/resolvers.go index 2b0f5df210..62b5962d21 100644 --- a/example/fileupload/resolvers.go +++ b/example/fileupload/resolvers.go @@ -7,12 +7,13 @@ import ( "fmt" "github.com/99designs/gqlgen/example/fileupload/model" + "github.com/99designs/gqlgen/graphql" ) type Resolver struct { - SingleUploadFunc func(ctx context.Context, file model.Upload) (*model.File, error) + SingleUploadFunc func(ctx context.Context, file graphql.Upload) (*model.File, error) SingleUploadWithPayloadFunc func(ctx context.Context, req model.UploadFile) (*model.File, error) - MultipleUploadFunc func(ctx context.Context, files []model.Upload) ([]model.File, error) + MultipleUploadFunc func(ctx context.Context, files []graphql.Upload) ([]model.File, error) } func (r *Resolver) Mutation() MutationResolver { @@ -24,7 +25,7 @@ func (r *Resolver) Query() QueryResolver { type mutationResolver struct{ *Resolver } -func (r *mutationResolver) SingleUpload(ctx context.Context, file model.Upload) (*model.File, error) { +func (r *mutationResolver) SingleUpload(ctx context.Context, file graphql.Upload) (*model.File, error) { if r.SingleUploadFunc != nil { return r.SingleUploadFunc(ctx, file) } @@ -36,7 +37,7 @@ func (r *mutationResolver) SingleUploadWithPayload(ctx context.Context, req mode } return nil, fmt.Errorf("not implemented") } -func (r *mutationResolver) MultipleUpload(ctx context.Context, files []model.Upload) ([]model.File, error) { +func (r *mutationResolver) MultipleUpload(ctx context.Context, files []graphql.Upload) ([]model.File, error) { if r.MultipleUploadFunc != nil { return r.MultipleUploadFunc(ctx, files) } diff --git a/example/fileupload/schema.graphql b/example/fileupload/schema.graphql index a63df78b90..b55f05678b 100644 --- a/example/fileupload/schema.graphql +++ b/example/fileupload/schema.graphql @@ -1,8 +1,5 @@ # The query type, represents all of the entry points into our object graph -# The Upload type, represents the response of uploading a file -scalar Upload - # The File type, represents the response of uploading a file type File { id: Int! diff --git a/graphql/upload.go b/graphql/upload.go index 98f875814e..e7919f1b41 100644 --- a/graphql/upload.go +++ b/graphql/upload.go @@ -1,6 +1,8 @@ package graphql import ( + "fmt" + "io" "mime/multipart" ) @@ -9,3 +11,17 @@ type Upload struct { Filename string Size int64 } + +func MarshalUpload(f Upload) Marshaler { + return WriterFunc(func(w io.Writer) { + io.Copy(w, f.File) + }) +} + +func UnmarshalUpload(v interface{}) (Upload, error) { + upload, ok := v.(Upload) + if !ok { + return Upload{}, fmt.Errorf("%T is not an Upload", v) + } + return upload, nil +} From 10beedb30fe05e94891cbffdc314498b4cc005c7 Mon Sep 17 00:00:00 2001 From: hantonelli Date: Sun, 3 Mar 2019 00:53:14 +0000 Subject: [PATCH 04/39] Remove not required file --- example/fileupload/model/model.go | 33 ------------------------------- 1 file changed, 33 deletions(-) delete mode 100644 example/fileupload/model/model.go diff --git a/example/fileupload/model/model.go b/example/fileupload/model/model.go deleted file mode 100644 index 09e28765cc..0000000000 --- a/example/fileupload/model/model.go +++ /dev/null @@ -1,33 +0,0 @@ -package model - -import ( - "github.com/99designs/gqlgen/graphql" - "github.com/pkg/errors" - "io" - "mime/multipart" -) - -type Upload struct { - File multipart.File - FileName string - Size int64 -} - -// if the type referenced in .gqlgen.yml is a function that returns a marshaller we can use it to encode and decode -// onto any existing go type. -func (u *Upload) UnmarshalGQL(v interface{}) error { - data, ok := v.(graphql.Upload) - if !ok { - return errors.New("upload should be a graphql Upload") - } - *u = Upload{ - File: data.File, - FileName: data.Filename, - Size: data.Size, - } - return nil -} - -func (u Upload) MarshalGQL(w io.Writer) { - io.Copy(w, u.File) -} From a7e95c597673673cb71a9f3e8f64a33f3dc1a197 Mon Sep 17 00:00:00 2001 From: hantonelli Date: Sun, 3 Mar 2019 21:33:37 +0000 Subject: [PATCH 05/39] Fix tests. Improve file generation --- codegen/config/config.go | 10 +- example/fileupload/fileupload_test.go | 63 ++++++++++- example/fileupload/generated.go | 144 ++++++++++++++++++-------- example/fileupload/readme.md | 1 + example/fileupload/resolvers.go | 18 +++- example/fileupload/schema.graphql | 3 +- handler/graphql.go | 2 + handler/graphql_test.go | 84 +++++++++++---- handler/mock.go | 59 +++++++++++ handler/stub.go | 5 - 10 files changed, 304 insertions(+), 85 deletions(-) create mode 100644 handler/mock.go diff --git a/codegen/config/config.go b/codegen/config/config.go index 08fafb43ee..49f7ad90c5 100644 --- a/codegen/config/config.go +++ b/codegen/config/config.go @@ -367,11 +367,13 @@ func (c *Config) LoadSchema() (*ast.Schema, map[string]string, error) { var sources []*ast.Source // Add upload scalar - sources = append(sources, &ast.Source{ - Name: "uploadPrelude.graphql", - Input: "\"The `Upload` scalar type represents a multipart file upload.\"\nscalar Upload", + uploadSource := &ast.Source{ + Name: "uploadScalar.graphql", + Input: "\"The `Upload` scalar type represents a multipart file upload.\"\nscalar Upload", BuiltIn: true, - }) + } + schemaStrings[uploadSource.Name] = uploadSource.Input + sources = append(sources, uploadSource) for _, filename := range c.SchemaFilename { filename = filepath.ToSlash(filename) diff --git a/example/fileupload/fileupload_test.go b/example/fileupload/fileupload_test.go index b33dcce8a1..a028d14a52 100644 --- a/example/fileupload/fileupload_test.go +++ b/example/fileupload/fileupload_test.go @@ -10,17 +10,17 @@ import ( "net/http/httptest" "testing" + "github.com/99designs/gqlgen/example/fileupload/model" + "github.com/99designs/gqlgen/graphql" "github.com/99designs/gqlgen/handler" "github.com/stretchr/testify/require" - - "github.com/99designs/gqlgen/example/fileupload/model" ) func TestFileUpload(t *testing.T) { t.Run("valid singleUpload", func(t *testing.T) { resolver := &Resolver{ - SingleUploadFunc: func(ctx context.Context, file model.Upload) (*model.File, error) { + SingleUploadFunc: func(ctx context.Context, file graphql.Upload) (*model.File, error) { require.NotNil(t, file) require.NotNil(t, file.File) content, err := ioutil.ReadAll(file.File) @@ -107,7 +107,7 @@ func TestFileUpload(t *testing.T) { t.Run("valid file list upload", func(t *testing.T) { resolver := &Resolver{ - MultipleUploadFunc: func(ctx context.Context, files []model.Upload) ([]model.File, error) { + MultipleUploadFunc: func(ctx context.Context, files []graphql.Upload) ([]model.File, error) { require.Len(t, files, 2) for i := range files { require.NotNil(t, files[i].File) @@ -154,4 +154,59 @@ func TestFileUpload(t *testing.T) { require.Equal(t, `{"data":{"multipleUpload":[{"id":1},{"id":2}]}}`, string(responseBody)) }) + t.Run("valid file list upload with payload", func(t *testing.T) { + resolver := &Resolver{ + MultipleUploadWithPayloadFunc: func(ctx context.Context, req []model.UploadFile) ([]model.File, error) { + require.Len(t, req, 2) + var ids []int + var contents []string + for i := range req { + require.NotNil(t, req[i].File) + require.NotNil(t, req[i].File.File) + content, err := ioutil.ReadAll(req[i].File.File) + require.Nil(t, err) + ids = append(ids, req[i].ID) + contents = append(contents, string(content)) + } + require.ElementsMatch(t, []int{1, 2}, ids) + require.ElementsMatch(t, []string{"test1", "test2"}, contents) + return []model.File{ + {ID: 1}, + {ID: 2}, + }, nil + }, + } + srv := httptest.NewServer(handler.GraphQL(NewExecutableSchema(Config{Resolvers: resolver}))) + defer srv.Close() + + bodyBuf := &bytes.Buffer{} + bodyWriter := multipart.NewWriter(bodyBuf) + err := bodyWriter.WriteField("operations", `{ "query": "mutation($req: [UploadFile!]!) { multipleUploadWithPayload(req: $req) { id } }", "variables": { "req": [ { "id": 1, "file": null }, { "id": 2, "file": null } ] } }`) + require.NoError(t, err) + err = bodyWriter.WriteField("map", `{ "0": ["variables.req.0.file"], "1": ["variables.req.1.file"] }`) + require.NoError(t, err) + w0, err := bodyWriter.CreateFormFile("0", "a.txt") + require.NoError(t, err) + _, err = w0.Write([]byte("test1")) + require.NoError(t, err) + w1, err := bodyWriter.CreateFormFile("1", "b.txt") + require.NoError(t, err) + _, err = w1.Write([]byte("test2")) + require.NoError(t, err) + err = bodyWriter.Close() + require.NoError(t, err) + + contentType := bodyWriter.FormDataContentType() + + resp, err := http.Post(fmt.Sprintf("%s/graphql", srv.URL), contentType, bodyBuf) + require.Nil(t, err) + defer func() { + _ = resp.Body.Close() + }() + require.Equal(t, http.StatusOK, resp.StatusCode) + responseBody, err := ioutil.ReadAll(resp.Body) + require.Nil(t, err) + require.Equal(t, `{"data":{"multipleUploadWithPayload":[{"id":1},{"id":2}]}}`, string(responseBody)) + }) + } diff --git a/example/fileupload/generated.go b/example/fileupload/generated.go index 8ef6879a06..fcb91dc2b6 100644 --- a/example/fileupload/generated.go +++ b/example/fileupload/generated.go @@ -47,13 +47,14 @@ type ComplexityRoot struct { } Mutation struct { - SingleUpload func(childComplexity int, file graphql.Upload) int - SingleUploadWithPayload func(childComplexity int, req model.UploadFile) int - MultipleUpload func(childComplexity int, files []graphql.Upload) int + SingleUpload func(childComplexity int, file graphql.Upload) int + SingleUploadWithPayload func(childComplexity int, req model.UploadFile) int + MultipleUpload func(childComplexity int, files []graphql.Upload) int + MultipleUploadWithPayload func(childComplexity int, req []model.UploadFile) int } Query struct { - File func(childComplexity int, fileName string) int + Empty func(childComplexity int) int } } @@ -61,9 +62,10 @@ type MutationResolver interface { SingleUpload(ctx context.Context, file graphql.Upload) (*model.File, error) SingleUploadWithPayload(ctx context.Context, req model.UploadFile) (*model.File, error) MultipleUpload(ctx context.Context, files []graphql.Upload) ([]model.File, error) + MultipleUploadWithPayload(ctx context.Context, req []model.UploadFile) ([]model.File, error) } type QueryResolver interface { - File(ctx context.Context, fileName string) (*model.File, error) + Empty(ctx context.Context) (string, error) } type executableSchema struct { @@ -124,17 +126,24 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.Mutation.MultipleUpload(childComplexity, args["files"].([]graphql.Upload)), true - case "Query.File": - if e.complexity.Query.File == nil { + case "Mutation.MultipleUploadWithPayload": + if e.complexity.Mutation.MultipleUploadWithPayload == nil { break } - args, err := ec.field_Query_file_args(context.TODO(), rawArgs) + args, err := ec.field_Mutation_multipleUploadWithPayload_args(context.TODO(), rawArgs) if err != nil { return 0, false } - return e.complexity.Query.File(childComplexity, args["fileName"].(string)), true + return e.complexity.Mutation.MultipleUploadWithPayload(childComplexity, args["req"].([]model.UploadFile)), true + + case "Query.Empty": + if e.complexity.Query.Empty == nil { + break + } + + return e.complexity.Query.Empty(childComplexity), true } return 0, false @@ -227,7 +236,7 @@ input UploadFile { } type Query { - file(fileName: String!): File + empty: String! } # The mutation type, represents all updates we can make to our data @@ -235,15 +244,32 @@ type Mutation { singleUpload(file: Upload!): File! singleUploadWithPayload(req: UploadFile!): File! multipleUpload(files: [Upload!]!): [File!]! + multipleUploadWithPayload(req: [UploadFile!]!): [File!]! } `}, + &ast.Source{Name: "uploadScalar.graphql", Input: `"The ` + "`" + `Upload` + "`" + ` scalar type represents a multipart file upload." +scalar Upload`}, ) // endregion ************************** generated!.gotpl ************************** // region ***************************** args.gotpl ***************************** +func (ec *executionContext) field_Mutation_multipleUploadWithPayload_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) { + var err error + args := map[string]interface{}{} + var arg0 []model.UploadFile + if tmp, ok := rawArgs["req"]; ok { + arg0, err = ec.unmarshalNUploadFile2ᚕgithubᚗcomᚋ99designsᚋgqlgenᚋexampleᚋfileuploadᚋmodelᚐUploadFile(ctx, tmp) + if err != nil { + return nil, err + } + } + args["req"] = arg0 + return args, nil +} + func (ec *executionContext) field_Mutation_multipleUpload_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) { var err error args := map[string]interface{}{} @@ -300,20 +326,6 @@ func (ec *executionContext) field_Query___type_args(ctx context.Context, rawArgs return args, nil } -func (ec *executionContext) field_Query_file_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) { - var err error - args := map[string]interface{}{} - var arg0 string - if tmp, ok := rawArgs["fileName"]; ok { - arg0, err = ec.unmarshalNString2string(ctx, tmp) - if err != nil { - return nil, err - } - } - args["fileName"] = arg0 - return args, nil -} - func (ec *executionContext) field___Type_enumValues_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) { var err error args := map[string]interface{}{} @@ -471,17 +483,17 @@ func (ec *executionContext) _Mutation_multipleUpload(ctx context.Context, field return ec.marshalNFile2ᚕgithubᚗcomᚋ99designsᚋgqlgenᚋexampleᚋfileuploadᚋmodelᚐFile(ctx, field.Selections, res) } -func (ec *executionContext) _Query_file(ctx context.Context, field graphql.CollectedField) graphql.Marshaler { +func (ec *executionContext) _Mutation_multipleUploadWithPayload(ctx context.Context, field graphql.CollectedField) graphql.Marshaler { ctx = ec.Tracer.StartFieldExecution(ctx, field) defer func() { ec.Tracer.EndFieldExecution(ctx) }() rctx := &graphql.ResolverContext{ - Object: "Query", + Object: "Mutation", Field: field, Args: nil, } ctx = graphql.WithResolverContext(ctx, rctx) rawArgs := field.ArgumentMap(ec.Variables) - args, err := ec.field_Query_file_args(ctx, rawArgs) + args, err := ec.field_Mutation_multipleUploadWithPayload_args(ctx, rawArgs) if err != nil { ec.Error(ctx, err) return graphql.Null @@ -490,15 +502,44 @@ func (ec *executionContext) _Query_file(ctx context.Context, field graphql.Colle ctx = ec.Tracer.StartFieldResolverExecution(ctx, rctx) resTmp := ec.FieldMiddleware(ctx, nil, func(rctx context.Context) (interface{}, error) { ctx = rctx // use context from middleware stack in children - return ec.resolvers.Query().File(rctx, args["fileName"].(string)) + return ec.resolvers.Mutation().MultipleUploadWithPayload(rctx, args["req"].([]model.UploadFile)) }) if resTmp == nil { + if !ec.HasError(rctx) { + ec.Errorf(ctx, "must not be null") + } return graphql.Null } - res := resTmp.(*model.File) + res := resTmp.([]model.File) rctx.Result = res ctx = ec.Tracer.StartFieldChildExecution(ctx) - return ec.marshalOFile2ᚖgithubᚗcomᚋ99designsᚋgqlgenᚋexampleᚋfileuploadᚋmodelᚐFile(ctx, field.Selections, res) + return ec.marshalNFile2ᚕgithubᚗcomᚋ99designsᚋgqlgenᚋexampleᚋfileuploadᚋmodelᚐFile(ctx, field.Selections, res) +} + +func (ec *executionContext) _Query_empty(ctx context.Context, field graphql.CollectedField) graphql.Marshaler { + ctx = ec.Tracer.StartFieldExecution(ctx, field) + defer func() { ec.Tracer.EndFieldExecution(ctx) }() + rctx := &graphql.ResolverContext{ + Object: "Query", + Field: field, + Args: nil, + } + ctx = graphql.WithResolverContext(ctx, rctx) + ctx = ec.Tracer.StartFieldResolverExecution(ctx, rctx) + resTmp := ec.FieldMiddleware(ctx, nil, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return ec.resolvers.Query().Empty(rctx) + }) + if resTmp == nil { + if !ec.HasError(rctx) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.(string) + rctx.Result = res + ctx = ec.Tracer.StartFieldChildExecution(ctx) + return ec.marshalNString2string(ctx, field.Selections, res) } func (ec *executionContext) _Query___type(ctx context.Context, field graphql.CollectedField) graphql.Marshaler { @@ -1442,6 +1483,11 @@ func (ec *executionContext) _Mutation(ctx context.Context, sel ast.SelectionSet) if out.Values[i] == graphql.Null { invalid = true } + case "multipleUploadWithPayload": + out.Values[i] = ec._Mutation_multipleUploadWithPayload(ctx, field) + if out.Values[i] == graphql.Null { + invalid = true + } default: panic("unknown field " + strconv.Quote(field.Name)) } @@ -1468,7 +1514,7 @@ func (ec *executionContext) _Query(ctx context.Context, sel ast.SelectionSet) gr switch field.Name { case "__typename": out.Values[i] = graphql.MarshalString("Query") - case "file": + case "empty": field := field out.Concurrently(i, func() (res graphql.Marshaler) { defer func() { @@ -1476,7 +1522,10 @@ func (ec *executionContext) _Query(ctx context.Context, sel ast.SelectionSet) gr ec.Error(ctx, ec.Recover(ctx, r)) } }() - res = ec._Query_file(ctx, field) + res = ec._Query_empty(ctx, field) + if res == graphql.Null { + invalid = true + } return res }) case "__type": @@ -1855,6 +1904,26 @@ func (ec *executionContext) unmarshalNUploadFile2githubᚗcomᚋ99designsᚋgqlg return ec.unmarshalInputUploadFile(ctx, v) } +func (ec *executionContext) unmarshalNUploadFile2ᚕgithubᚗcomᚋ99designsᚋgqlgenᚋexampleᚋfileuploadᚋmodelᚐUploadFile(ctx context.Context, v interface{}) ([]model.UploadFile, error) { + var vSlice []interface{} + if v != nil { + if tmp1, ok := v.([]interface{}); ok { + vSlice = tmp1 + } else { + vSlice = []interface{}{v} + } + } + var err error + res := make([]model.UploadFile, len(vSlice)) + for i := range vSlice { + res[i], err = ec.unmarshalNUploadFile2githubᚗcomᚋ99designsᚋgqlgenᚋexampleᚋfileuploadᚋmodelᚐUploadFile(ctx, vSlice[i]) + if err != nil { + return nil, err + } + } + return res, nil +} + func (ec *executionContext) marshalN__Directive2githubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚋintrospectionᚐDirective(ctx context.Context, sel ast.SelectionSet, v introspection.Directive) graphql.Marshaler { return ec.___Directive(ctx, sel, &v) } @@ -2092,17 +2161,6 @@ func (ec *executionContext) marshalOBoolean2ᚖbool(ctx context.Context, sel ast return ec.marshalOBoolean2bool(ctx, sel, *v) } -func (ec *executionContext) marshalOFile2githubᚗcomᚋ99designsᚋgqlgenᚋexampleᚋfileuploadᚋmodelᚐFile(ctx context.Context, sel ast.SelectionSet, v model.File) graphql.Marshaler { - return ec._File(ctx, sel, &v) -} - -func (ec *executionContext) marshalOFile2ᚖgithubᚗcomᚋ99designsᚋgqlgenᚋexampleᚋfileuploadᚋmodelᚐFile(ctx context.Context, sel ast.SelectionSet, v *model.File) graphql.Marshaler { - if v == nil { - return graphql.Null - } - return ec._File(ctx, sel, v) -} - func (ec *executionContext) unmarshalOString2string(ctx context.Context, v interface{}) (string, error) { return graphql.UnmarshalString(v) } diff --git a/example/fileupload/readme.md b/example/fileupload/readme.md index b8eb9cc2ed..fb8b444645 100644 --- a/example/fileupload/readme.md +++ b/example/fileupload/readme.md @@ -9,6 +9,7 @@ go run ./example/fileupload/server/server.go and open http://localhost:8080 in your browser +//TODO Test examples!! ## Examples diff --git a/example/fileupload/resolvers.go b/example/fileupload/resolvers.go index 62b5962d21..ad11c38352 100644 --- a/example/fileupload/resolvers.go +++ b/example/fileupload/resolvers.go @@ -11,9 +11,10 @@ import ( ) type Resolver struct { - SingleUploadFunc func(ctx context.Context, file graphql.Upload) (*model.File, error) - SingleUploadWithPayloadFunc func(ctx context.Context, req model.UploadFile) (*model.File, error) - MultipleUploadFunc func(ctx context.Context, files []graphql.Upload) ([]model.File, error) + SingleUploadFunc func(ctx context.Context, file graphql.Upload) (*model.File, error) + SingleUploadWithPayloadFunc func(ctx context.Context, req model.UploadFile) (*model.File, error) + MultipleUploadFunc func(ctx context.Context, files []graphql.Upload) ([]model.File, error) + MultipleUploadWithPayloadFunc func(ctx context.Context, req []model.UploadFile) ([]model.File, error) } func (r *Resolver) Mutation() MutationResolver { @@ -44,8 +45,15 @@ func (r *mutationResolver) MultipleUpload(ctx context.Context, files []graphql.U return nil, fmt.Errorf("not implemented") } +func (r *mutationResolver) MultipleUploadWithPayload(ctx context.Context, req []model.UploadFile) ([]model.File, error) { + if r.MultipleUploadWithPayloadFunc != nil { + return r.MultipleUploadWithPayloadFunc(ctx, req) + } + return nil, fmt.Errorf("not implemented") +} + type queryResolver struct{ *Resolver } -func (r *queryResolver) File(ctx context.Context, fileName string) (*model.File, error) { - return nil, fmt.Errorf("not implemented") +func (r *queryResolver) Empty(ctx context.Context) (string, error) { + return "", fmt.Errorf("not implemented") } diff --git a/example/fileupload/schema.graphql b/example/fileupload/schema.graphql index b55f05678b..937aa3f34c 100644 --- a/example/fileupload/schema.graphql +++ b/example/fileupload/schema.graphql @@ -12,7 +12,7 @@ input UploadFile { } type Query { - file(fileName: String!): File + empty: String! } # The mutation type, represents all updates we can make to our data @@ -20,5 +20,6 @@ type Mutation { singleUpload(file: Upload!): File! singleUploadWithPayload(req: UploadFile!): File! multipleUpload(files: [Upload!]!): [File!]! + multipleUploadWithPayload(req: [UploadFile!]!): [File!]! } diff --git a/handler/graphql.go b/handler/graphql.go index aaa4a9ae88..e38ef0d17c 100644 --- a/handler/graphql.go +++ b/handler/graphql.go @@ -492,6 +492,7 @@ func sendErrorf(w http.ResponseWriter, code int, format string, args ...interfac sendError(w, code, &gqlerror.Error{Message: fmt.Sprintf(format, args...)}) } +//TODO Add test for this func processMultipart(r *http.Request, request *params) error { // Parse multipart form if err := r.ParseMultipartForm(1024); err != nil { @@ -552,6 +553,7 @@ func processMultipart(r *http.Request, request *params) error { return errors.New("invalid operation") } +//TODO Add test for this func addUploadToOperations(operations interface{}, upload graphql.Upload, path string) error { var parts []interface{} for _, p := range strings.Split(path, ".") { diff --git a/handler/graphql_test.go b/handler/graphql_test.go index 12d61ca24a..ee70916273 100644 --- a/handler/graphql_test.go +++ b/handler/graphql_test.go @@ -9,10 +9,9 @@ import ( "strings" "testing" + "github.com/99designs/gqlgen/graphql" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - - "github.com/99designs/gqlgen/graphql" "github.com/vektah/gqlparser/ast" ) @@ -120,19 +119,20 @@ func TestHandlerGET(t *testing.T) { } func TestFileUpload(t *testing.T) { + t.Run("valid single file upload", func(t *testing.T) { - stub := &executableSchemaStub{ + mock := &executableSchemaMock{ MutationFunc: func(ctx context.Context, op *ast.OperationDefinition) *graphql.Response { require.Equal(t, len(op.VariableDefinitions), 1) require.Equal(t, op.VariableDefinitions[0].Variable, "file") - return &graphql.Response{Data: []byte(`{"name":"test"}`)} + return &graphql.Response{Data: []byte(`{"singleUpload":{"id":1}}`)} }, } - handler := GraphQL(stub) + handler := GraphQL(mock) bodyBuf := &bytes.Buffer{} bodyWriter := multipart.NewWriter(bodyBuf) - err := bodyWriter.WriteField("operations", `{ "query": "mutation ($file: Upload!) { singleUpload(file: $file) { fileName, size } }", "variables": { "file": null } }`) + err := bodyWriter.WriteField("operations", `{ "query": "mutation ($file: Upload!) { singleUpload(file: $file) { id } }", "variables": { "file": null } }`) require.NoError(t, err) err = bodyWriter.WriteField("map", `{ "0": ["variables.file"] }`) require.NoError(t, err) @@ -151,23 +151,24 @@ func TestFileUpload(t *testing.T) { resp := httptest.NewRecorder() handler.ServeHTTP(resp, req) require.Equal(t, http.StatusOK, resp.Code) - require.Equal(t, `{"data":{"name":"test"}}`, resp.Body.String()) + require.Equal(t, `{"data":{"singleUpload":{"id":1}}}`, resp.Body.String()) }) t.Run("valid single file upload with payload", func(t *testing.T) { - stub := &executableSchemaStub{} - stub.MutationFunc = func(ctx context.Context, op *ast.OperationDefinition) *graphql.Response { - require.Equal(t, len(op.VariableDefinitions), 1) - require.Equal(t, op.VariableDefinitions[0].Variable, "file") - return &graphql.Response{Data: []byte(`{"name":"test"}`)} + stub := &executableSchemaMock{ + MutationFunc: func(ctx context.Context, op *ast.OperationDefinition) *graphql.Response { + require.Equal(t, len(op.VariableDefinitions), 1) + require.Equal(t, op.VariableDefinitions[0].Variable, "req") + return &graphql.Response{Data: []byte(`{"singleUploadWithPayload":{"id":1}}`)} + }, } handler := GraphQL(stub) bodyBuf := &bytes.Buffer{} bodyWriter := multipart.NewWriter(bodyBuf) - err := bodyWriter.WriteField("operations", `{ "query": "mutation ($file: Upload!) { singleUpload(file: $file) { fileName, size } }", "variables": { "file": null } }`) + err := bodyWriter.WriteField("operations", `{ "query": "mutation ($req: UploadFile!) { singleUploadWithPayload(req: $req) { id } }", "variables": { "req": {"file": null, "id": 1 } } }`) require.NoError(t, err) - err = bodyWriter.WriteField("map", `{ "0": ["variables.file"] }`) + err = bodyWriter.WriteField("map", `{ "0": ["variables.req.file"] }`) require.NoError(t, err) w, err := bodyWriter.CreateFormFile("0", "a.txt") require.NoError(t, err) @@ -184,21 +185,22 @@ func TestFileUpload(t *testing.T) { resp := httptest.NewRecorder() handler.ServeHTTP(resp, req) require.Equal(t, http.StatusOK, resp.Code) - require.Equal(t, `{"data":{"name":"test"}}`, resp.Body.String()) + require.Equal(t, `{"data":{"singleUploadWithPayload":{"id":1}}}`, resp.Body.String()) }) t.Run("valid file list upload", func(t *testing.T) { - stub := &executableSchemaStub{} - stub.MutationFunc = func(ctx context.Context, op *ast.OperationDefinition) *graphql.Response { - require.Equal(t, len(op.VariableDefinitions), 1) - require.Equal(t, op.VariableDefinitions[0].Variable, "files") - return &graphql.Response{Data: []byte(`{"name":"test"}`)} + mock := &executableSchemaMock{ + MutationFunc: func(ctx context.Context, op *ast.OperationDefinition) *graphql.Response { + require.Equal(t, len(op.VariableDefinitions), 1) + require.Equal(t, op.VariableDefinitions[0].Variable, "files") + return &graphql.Response{Data: []byte(`{"multipleUpload":[{"id":1},{"id":2}]}`)} + }, } - handler := GraphQL(stub) + handler := GraphQL(mock) bodyBuf := &bytes.Buffer{} bodyWriter := multipart.NewWriter(bodyBuf) - err := bodyWriter.WriteField("operations", `{ "query": "mutation($files: [Upload!]!) { multipleUpload(files: $files) { fileName } }", "variables": { "files": [null, null] } }`) + err := bodyWriter.WriteField("operations", `{ "query": "mutation($files: [Upload!]!) { multipleUpload(files: $files) { id } }", "variables": { "files": [null, null] } }`) require.NoError(t, err) err = bodyWriter.WriteField("map", `{ "0": ["variables.files.0"], "1": ["variables.files.1"] }`) require.NoError(t, err) @@ -220,9 +222,45 @@ func TestFileUpload(t *testing.T) { resp := httptest.NewRecorder() handler.ServeHTTP(resp, req) require.Equal(t, http.StatusOK, resp.Code) - require.Equal(t, `{"data":{"name":"test"}}`, resp.Body.String()) + require.Equal(t, `{"data":{"multipleUpload":[{"id":1},{"id":2}]}}`, resp.Body.String()) }) + t.Run("valid file list upload with payload", func(t *testing.T) { + mock := &executableSchemaMock{ + MutationFunc: func(ctx context.Context, op *ast.OperationDefinition) *graphql.Response { + require.Equal(t, len(op.VariableDefinitions), 1) + require.Equal(t, op.VariableDefinitions[0].Variable, "req") + return &graphql.Response{Data: []byte(`{"multipleUploadWithPayload":[{"id":1},{"id":2}]}`)} + }, + } + handler := GraphQL(mock) + + bodyBuf := &bytes.Buffer{} + bodyWriter := multipart.NewWriter(bodyBuf) + err := bodyWriter.WriteField("operations", `{ "query": "mutation($req: [UploadFile!]!) { multipleUploadWithPayload(req: $req) { id } }", "variables": { "req": [ { "id": 1, "file": null }, { "id": 2, "file": null } ] } }`) + require.NoError(t, err) + err = bodyWriter.WriteField("map", `{ "0": ["variables.req.0.file"], "1": ["variables.req.1.file"] }`) + require.NoError(t, err) + w0, err := bodyWriter.CreateFormFile("0", "a.txt") + require.NoError(t, err) + _, err = w0.Write([]byte("test")) + require.NoError(t, err) + w1, err := bodyWriter.CreateFormFile("1", "b.txt") + require.NoError(t, err) + _, err = w1.Write([]byte("test")) + require.NoError(t, err) + err = bodyWriter.Close() + require.NoError(t, err) + + req, err := http.NewRequest("POST", "/graphql", bodyBuf) + require.NoError(t, err) + + req.Header.Set("Content-Type", bodyWriter.FormDataContentType()) + resp := httptest.NewRecorder() + handler.ServeHTTP(resp, req) + require.Equal(t, http.StatusOK, resp.Code) + require.Equal(t, `{"data":{"multipleUploadWithPayload":[{"id":1},{"id":2}]}}`, resp.Body.String()) + }) } func TestHandlerOptions(t *testing.T) { diff --git a/handler/mock.go b/handler/mock.go new file mode 100644 index 0000000000..bc139d850a --- /dev/null +++ b/handler/mock.go @@ -0,0 +1,59 @@ +package handler + +import ( + "context" + + "github.com/99designs/gqlgen/graphql" + "github.com/vektah/gqlparser" + "github.com/vektah/gqlparser/ast" +) + +type executableSchemaMock struct { + MutationFunc func(ctx context.Context, op *ast.OperationDefinition) *graphql.Response +} + +var _ graphql.ExecutableSchema = &executableSchemaMock{} + +func (e *executableSchemaMock) Schema() *ast.Schema { + return gqlparser.MustLoadSchema(&ast.Source{Input: ` + schema { query: Query, mutation: Mutation } + type Query { + empty: String! + } + scalar Upload + type File { + id: Int! + } + input UploadFile { + id: Int! + file: Upload! + } + type Mutation { + singleUpload(file: Upload!): File! + singleUploadWithPayload(req: UploadFile!): File! + multipleUpload(files: [Upload!]!): [File!]! + multipleUploadWithPayload(req: [UploadFile!]!): [File!]! + } + `}) +} + +func (e *executableSchemaMock) Complexity(typeName, field string, childComplexity int, args map[string]interface{}) (int, bool) { + return 0, false +} + +func (e *executableSchemaMock) Query(ctx context.Context, op *ast.OperationDefinition) *graphql.Response { + return graphql.ErrorResponse(ctx, "queries are not supported") +} + +func (e *executableSchemaMock) Mutation(ctx context.Context, op *ast.OperationDefinition) *graphql.Response { + return e.MutationFunc(ctx, op) +} + +func (e *executableSchemaMock) Subscription(ctx context.Context, op *ast.OperationDefinition) func() *graphql.Response { + return func() *graphql.Response { + select { + case <-ctx.Done(): + return nil + } + } +} diff --git a/handler/stub.go b/handler/stub.go index 3a53060e8b..09fa8fa01d 100644 --- a/handler/stub.go +++ b/handler/stub.go @@ -10,7 +10,6 @@ import ( type executableSchemaStub struct { NextResp chan struct{} - MutationFunc func(ctx context.Context, op *ast.OperationDefinition) *graphql.Response } var _ graphql.ExecutableSchema = &executableSchemaStub{} @@ -23,7 +22,6 @@ func (e *executableSchemaStub) Schema() *ast.Schema { user(id: Int): User! } type User { name: String! } - scalar Upload `}) } @@ -36,9 +34,6 @@ func (e *executableSchemaStub) Query(ctx context.Context, op *ast.OperationDefin } func (e *executableSchemaStub) Mutation(ctx context.Context, op *ast.OperationDefinition) *graphql.Response { - if e.MutationFunc != nil { - return e.MutationFunc(ctx, op) - } return graphql.ErrorResponse(ctx, "mutations are not supported") } From 998f7674cae96d0cf7eca9eaffffc4cab3e55b0a Mon Sep 17 00:00:00 2001 From: hantonelli Date: Sun, 3 Mar 2019 21:38:23 +0000 Subject: [PATCH 06/39] Revert changing the stub file --- handler/stub.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/handler/stub.go b/handler/stub.go index 09fa8fa01d..d237e18892 100644 --- a/handler/stub.go +++ b/handler/stub.go @@ -9,7 +9,7 @@ import ( ) type executableSchemaStub struct { - NextResp chan struct{} + NextResp chan struct{} } var _ graphql.ExecutableSchema = &executableSchemaStub{} From c493d1b96c33ab73ab27332cd9ba41a9f073170b Mon Sep 17 00:00:00 2001 From: hantonelli Date: Sun, 3 Mar 2019 21:42:15 +0000 Subject: [PATCH 07/39] Revert changing to websocket_test --- handler/websocket_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/handler/websocket_test.go b/handler/websocket_test.go index a49ef257d1..f80a3270ad 100644 --- a/handler/websocket_test.go +++ b/handler/websocket_test.go @@ -13,7 +13,7 @@ import ( func TestWebsocket(t *testing.T) { next := make(chan struct{}) - h := GraphQL(&executableSchemaStub{NextResp: next}) + h := GraphQL(&executableSchemaStub{next}) srv := httptest.NewServer(h) defer srv.Close() From db7a03b117ba2fd74346f669df9b926e46b891c7 Mon Sep 17 00:00:00 2001 From: hantonelli Date: Sun, 3 Mar 2019 21:49:21 +0000 Subject: [PATCH 08/39] Improve tests and names --- example/fileupload/fileupload_test.go | 8 +++++--- handler/graphql_test.go | 12 ++++++------ 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/example/fileupload/fileupload_test.go b/example/fileupload/fileupload_test.go index a028d14a52..2cda68f436 100644 --- a/example/fileupload/fileupload_test.go +++ b/example/fileupload/fileupload_test.go @@ -109,12 +109,14 @@ func TestFileUpload(t *testing.T) { resolver := &Resolver{ MultipleUploadFunc: func(ctx context.Context, files []graphql.Upload) ([]model.File, error) { require.Len(t, files, 2) + var contents []string for i := range files { require.NotNil(t, files[i].File) content, err := ioutil.ReadAll(files[i].File) require.Nil(t, err) - require.Equal(t, string(content), "test") + contents = append(contents, string(content)) } + require.ElementsMatch(t, []string{"test1", "test2"}, contents) return []model.File{ {ID: 1}, {ID: 2}, @@ -132,11 +134,11 @@ func TestFileUpload(t *testing.T) { require.NoError(t, err) w0, err := bodyWriter.CreateFormFile("0", "a.txt") require.NoError(t, err) - _, err = w0.Write([]byte("test")) + _, err = w0.Write([]byte("test1")) require.NoError(t, err) w1, err := bodyWriter.CreateFormFile("1", "b.txt") require.NoError(t, err) - _, err = w1.Write([]byte("test")) + _, err = w1.Write([]byte("test2")) require.NoError(t, err) err = bodyWriter.Close() require.NoError(t, err) diff --git a/handler/graphql_test.go b/handler/graphql_test.go index ee70916273..0e8ca29804 100644 --- a/handler/graphql_test.go +++ b/handler/graphql_test.go @@ -155,14 +155,14 @@ func TestFileUpload(t *testing.T) { }) t.Run("valid single file upload with payload", func(t *testing.T) { - stub := &executableSchemaMock{ + mock := &executableSchemaMock{ MutationFunc: func(ctx context.Context, op *ast.OperationDefinition) *graphql.Response { require.Equal(t, len(op.VariableDefinitions), 1) require.Equal(t, op.VariableDefinitions[0].Variable, "req") return &graphql.Response{Data: []byte(`{"singleUploadWithPayload":{"id":1}}`)} }, } - handler := GraphQL(stub) + handler := GraphQL(mock) bodyBuf := &bytes.Buffer{} bodyWriter := multipart.NewWriter(bodyBuf) @@ -206,11 +206,11 @@ func TestFileUpload(t *testing.T) { require.NoError(t, err) w0, err := bodyWriter.CreateFormFile("0", "a.txt") require.NoError(t, err) - _, err = w0.Write([]byte("test")) + _, err = w0.Write([]byte("test1")) require.NoError(t, err) w1, err := bodyWriter.CreateFormFile("1", "b.txt") require.NoError(t, err) - _, err = w1.Write([]byte("test")) + _, err = w1.Write([]byte("test2")) require.NoError(t, err) err = bodyWriter.Close() require.NoError(t, err) @@ -243,11 +243,11 @@ func TestFileUpload(t *testing.T) { require.NoError(t, err) w0, err := bodyWriter.CreateFormFile("0", "a.txt") require.NoError(t, err) - _, err = w0.Write([]byte("test")) + _, err = w0.Write([]byte("test1")) require.NoError(t, err) w1, err := bodyWriter.CreateFormFile("1", "b.txt") require.NoError(t, err) - _, err = w1.Write([]byte("test")) + _, err = w1.Write([]byte("test2")) require.NoError(t, err) err = bodyWriter.Close() require.NoError(t, err) From 3c5f8bb9f7e47ee3e39cc2cd254191d274e1c59f Mon Sep 17 00:00:00 2001 From: hantonelli Date: Sun, 10 Mar 2019 13:16:02 +0000 Subject: [PATCH 09/39] Improve some examples --- example/fileupload/readme.md | 61 ++++++++++++++++++----------- example/fileupload/server/server.go | 16 +++++++- example/fileupload/testfiles/a.txt | 1 + example/fileupload/testfiles/b.txt | 1 + example/fileupload/testfiles/c.txt | 1 + 5 files changed, 55 insertions(+), 25 deletions(-) create mode 100644 example/fileupload/testfiles/a.txt create mode 100644 example/fileupload/testfiles/b.txt create mode 100644 example/fileupload/testfiles/c.txt diff --git a/example/fileupload/readme.md b/example/fileupload/readme.md index fb8b444645..a3d3b6b682 100644 --- a/example/fileupload/readme.md +++ b/example/fileupload/readme.md @@ -7,16 +7,7 @@ to run this server go run ./example/fileupload/server/server.go ``` -and open http://localhost:8080 in your browser - -//TODO Test examples!! - -## Examples - -curl localhost:3001/graphql \ - -F operations='{ "query": "mutation ($file: Upload!) { singleUpload(file: $file) { id } }", "variables": { "file": null } }' \ - -F map='{ "0": ["variables.file"] }' \ - -F 0=@a.txt +and open http://localhost:8087 in your browser ### Single file @@ -27,7 +18,7 @@ curl localhost:3001/graphql \ { query: ` mutation($file: Upload!) { - uploadFile(file: $file) { + singleUpload(file: $file) { id } } @@ -41,30 +32,46 @@ curl localhost:3001/graphql \ #### cURL request ```shell -curl localhost:3001/graphql \ +curl localhost:8087/query \ -F operations='{ "query": "mutation ($file: Upload!) { singleUpload(file: $file) { id } }", "variables": { "file": null } }' \ -F map='{ "0": ["variables.file"] }' \ - -F 0=@a.txt + -F 0=@./example/fileupload./testfiles/a.txt +``` + + +```shell +curl localhost:8087/query \ + -F operations='{ "query": "mutation ($req: UploadFile!) { singleUploadWithPayload(req: $req) { id } }", "variables": { "req": {"file": null, "id": 1 } } }' \ + -F map='{ "0": ["variables.req.file"] }' \ + -F 0=@./example/fileupload/testfiles/a.txt ``` #### Request payload ``` ---------------------------cec8e8123c05ba25 +POST /query HTTP/1.1 +Host: localhost:8087 +User-Agent: curl/7.60.0 +Accept: */* +Content-Length: 525 +Content-Type: multipart/form-data; boundary=-------------------- +----c259ddf1cd194033 +=> Send data, 525 bytes (0x20d) +--------------------------c259ddf1cd194033 Content-Disposition: form-data; name="operations" -{ "query": "mutation ($file: Upload!) { singleUpload(file: $file) { id } }", "variables": { "file": null } } ---------------------------cec8e8123c05ba25 +{ "query": "mutation ($file: Upload!) { singleUpload(file: $file +) { id } }", "variables": { "file": null } } +--------------------------c259ddf1cd194033 Content-Disposition: form-data; name="map" { "0": ["variables.file"] } ---------------------------cec8e8123c05ba25 +--------------------------c259ddf1cd194033 Content-Disposition: form-data; name="0"; filename="a.txt" Content-Type: text/plain Alpha file content. - ---------------------------cec8e8123c05ba25-- +--------------------------c259ddf1cd194033-- ``` ### File list @@ -91,12 +98,20 @@ Alpha file content. #### cURL request -```shell -curl localhost:3001/graphql \ +``` +curl localhost:8087/query \ -F operations='{ "query": "mutation($files: [Upload!]!) { multipleUpload(files: $files) { id } }", "variables": { "files": [null, null] } }' \ -F map='{ "0": ["variables.files.0"], "1": ["variables.files.1"] }' \ - -F 0=@b.txt \ - -F 1=@c.txt + -F 0=@./example/fileupload/testfiles/b.txt \ + -F 1=@./example/fileupload/testfiles/c.txt +``` + +``` +curl localhost:8087/query \ + -F operations='{ "query": "mutation($req: [UploadFile!]!) { multipleUploadWithPayload(req: $req) { id } }", "variables": { "req": [ { "id": 1, "file": null }, { "id": 2, "file": null } ] } }' \ + -F map='{ "0": ["variables.req.0.file"], "1": ["variables.req.1.file"] }' \ + -F 0=@./example/fileupload/testfiles/b.txt \ + -F 1=@./example/fileupload/testfiles/c.txt ``` #### Request payload diff --git a/example/fileupload/server/server.go b/example/fileupload/server/server.go index d47009295f..c87196b78e 100644 --- a/example/fileupload/server/server.go +++ b/example/fileupload/server/server.go @@ -1,15 +1,27 @@ package main import ( + "context" "log" "net/http" "github.com/99designs/gqlgen/example/fileupload" + "github.com/99designs/gqlgen/example/fileupload/model" + "github.com/99designs/gqlgen/graphql" "github.com/99designs/gqlgen/handler" ) func main() { http.Handle("/", handler.Playground("File Upload Demo", "/query")) - http.Handle("/query", handler.GraphQL(fileupload.NewExecutableSchema(fileupload.Config{Resolvers: &fileupload.Resolver{}}))) - log.Fatal(http.ListenAndServe(":8086", nil)) + resolver := &fileupload.Resolver{ + SingleUploadFunc: func(ctx context.Context, file graphql.Upload) (*model.File, error) { + return &model.File{ + ID: 1, + },nil + }, + } + http.Handle("/query", handler.GraphQL(fileupload.NewExecutableSchema(fileupload.Config{Resolvers: resolver}))) + + log.Print("connect to http://localhost:8087/ for GraphQL playground") + log.Fatal(http.ListenAndServe(":8087", nil)) } diff --git a/example/fileupload/testfiles/a.txt b/example/fileupload/testfiles/a.txt new file mode 100644 index 0000000000..96ff01944e --- /dev/null +++ b/example/fileupload/testfiles/a.txt @@ -0,0 +1 @@ +Alpha file content diff --git a/example/fileupload/testfiles/b.txt b/example/fileupload/testfiles/b.txt new file mode 100644 index 0000000000..32b94fd894 --- /dev/null +++ b/example/fileupload/testfiles/b.txt @@ -0,0 +1 @@ +Bravo file content diff --git a/example/fileupload/testfiles/c.txt b/example/fileupload/testfiles/c.txt new file mode 100644 index 0000000000..cfec6a422f --- /dev/null +++ b/example/fileupload/testfiles/c.txt @@ -0,0 +1 @@ +Charlie file content From 493d9375b767f85333e4f7d70d1bab9c0e47e475 Mon Sep 17 00:00:00 2001 From: hantonelli Date: Sun, 10 Mar 2019 14:00:23 +0000 Subject: [PATCH 10/39] Improve examples --- example/fileupload/fileupload_test.go | 49 +++++--- example/fileupload/generated.go | 82 ++++++++++++- example/fileupload/model/generated.go | 4 +- example/fileupload/readme.md | 169 ++++++++++++++++++++++---- example/fileupload/schema.graphql | 2 + example/fileupload/server/server.go | 71 ++++++++++- 6 files changed, 327 insertions(+), 50 deletions(-) diff --git a/example/fileupload/fileupload_test.go b/example/fileupload/fileupload_test.go index 2cda68f436..948a5ce38f 100644 --- a/example/fileupload/fileupload_test.go +++ b/example/fileupload/fileupload_test.go @@ -18,7 +18,7 @@ import ( func TestFileUpload(t *testing.T) { - t.Run("valid singleUpload", func(t *testing.T) { + t.Run("valid single file upload", func(t *testing.T) { resolver := &Resolver{ SingleUploadFunc: func(ctx context.Context, file graphql.Upload) (*model.File, error) { require.NotNil(t, file) @@ -28,7 +28,9 @@ func TestFileUpload(t *testing.T) { require.Equal(t, string(content), "test") return &model.File{ - ID: 1, + ID: 1, + Name: file.Filename, + Content: string(content), }, nil }, } @@ -37,7 +39,7 @@ func TestFileUpload(t *testing.T) { bodyBuf := &bytes.Buffer{} bodyWriter := multipart.NewWriter(bodyBuf) - err := bodyWriter.WriteField("operations", `{ "query": "mutation ($file: Upload!) { singleUpload(file: $file) { id } }", "variables": { "file": null } }`) + err := bodyWriter.WriteField("operations", `{ "query": "mutation ($file: Upload!) { singleUpload(file: $file) { id, name, content } }", "variables": { "file": null } }`) require.NoError(t, err) err = bodyWriter.WriteField("map", `{ "0": ["variables.file"] }`) require.NoError(t, err) @@ -58,7 +60,8 @@ func TestFileUpload(t *testing.T) { require.Equal(t, http.StatusOK, resp.StatusCode) responseBody, err := ioutil.ReadAll(resp.Body) require.Nil(t, err) - require.Equal(t, `{"data":{"singleUpload":{"id":1}}}`, string(responseBody)) + responseString := string(responseBody) + require.Equal(t, `{"data":{"singleUpload":{"id":1,"name":"a.txt","content":"test"}}}`, responseString) }) t.Run("valid single file upload with payload", func(t *testing.T) { @@ -72,7 +75,9 @@ func TestFileUpload(t *testing.T) { require.Equal(t, string(content), "test") return &model.File{ - ID: 1, + ID: 1, + Name: req.File.Filename, + Content: string(content), }, nil }, } @@ -81,7 +86,7 @@ func TestFileUpload(t *testing.T) { bodyBuf := &bytes.Buffer{} bodyWriter := multipart.NewWriter(bodyBuf) - err := bodyWriter.WriteField("operations", `{ "query": "mutation ($req: UploadFile!) { singleUploadWithPayload(req: $req) { id } }", "variables": { "req": {"file": null, "id": 1 } } }`) + err := bodyWriter.WriteField("operations", `{ "query": "mutation ($req: UploadFile!) { singleUploadWithPayload(req: $req) { id, name, content } }", "variables": { "req": {"file": null, "id": 1 } } }`) require.NoError(t, err) err = bodyWriter.WriteField("map", `{ "0": ["variables.req.file"] }`) require.NoError(t, err) @@ -102,7 +107,7 @@ func TestFileUpload(t *testing.T) { require.Equal(t, http.StatusOK, resp.StatusCode) responseBody, err := ioutil.ReadAll(resp.Body) require.Nil(t, err) - require.Equal(t, `{"data":{"singleUploadWithPayload":{"id":1}}}`, string(responseBody)) + require.Equal(t, `{"data":{"singleUploadWithPayload":{"id":1,"name":"a.txt","content":"test"}}}`, string(responseBody)) }) t.Run("valid file list upload", func(t *testing.T) { @@ -110,17 +115,20 @@ func TestFileUpload(t *testing.T) { MultipleUploadFunc: func(ctx context.Context, files []graphql.Upload) ([]model.File, error) { require.Len(t, files, 2) var contents []string + var resp []model.File for i := range files { require.NotNil(t, files[i].File) content, err := ioutil.ReadAll(files[i].File) require.Nil(t, err) contents = append(contents, string(content)) + resp = append(resp, model.File{ + ID: i + 1, + Name: files[i].Filename, + Content: string(content), + }) } require.ElementsMatch(t, []string{"test1", "test2"}, contents) - return []model.File{ - {ID: 1}, - {ID: 2}, - }, nil + return resp, nil }, } srv := httptest.NewServer(handler.GraphQL(NewExecutableSchema(Config{Resolvers: resolver}))) @@ -128,7 +136,7 @@ func TestFileUpload(t *testing.T) { bodyBuf := &bytes.Buffer{} bodyWriter := multipart.NewWriter(bodyBuf) - err := bodyWriter.WriteField("operations", `{ "query": "mutation($files: [Upload!]!) { multipleUpload(files: $files) { id } }", "variables": { "files": [null, null] } }`) + err := bodyWriter.WriteField("operations", `{ "query": "mutation($files: [Upload!]!) { multipleUpload(files: $files) { id, name, content } }", "variables": { "files": [null, null] } }`) require.NoError(t, err) err = bodyWriter.WriteField("map", `{ "0": ["variables.files.0"], "1": ["variables.files.1"] }`) require.NoError(t, err) @@ -153,7 +161,7 @@ func TestFileUpload(t *testing.T) { require.Equal(t, http.StatusOK, resp.StatusCode) responseBody, err := ioutil.ReadAll(resp.Body) require.Nil(t, err) - require.Equal(t, `{"data":{"multipleUpload":[{"id":1},{"id":2}]}}`, string(responseBody)) + require.Equal(t, `{"data":{"multipleUpload":[{"id":1,"name":"a.txt","content":"test1"},{"id":2,"name":"b.txt","content":"test2"}]}}`, string(responseBody)) }) t.Run("valid file list upload with payload", func(t *testing.T) { @@ -162,6 +170,7 @@ func TestFileUpload(t *testing.T) { require.Len(t, req, 2) var ids []int var contents []string + var resp []model.File for i := range req { require.NotNil(t, req[i].File) require.NotNil(t, req[i].File.File) @@ -169,13 +178,15 @@ func TestFileUpload(t *testing.T) { require.Nil(t, err) ids = append(ids, req[i].ID) contents = append(contents, string(content)) + resp = append(resp, model.File{ + ID: i + 1, + Name: req[i].File.Filename, + Content: string(content), + }) } require.ElementsMatch(t, []int{1, 2}, ids) require.ElementsMatch(t, []string{"test1", "test2"}, contents) - return []model.File{ - {ID: 1}, - {ID: 2}, - }, nil + return resp, nil }, } srv := httptest.NewServer(handler.GraphQL(NewExecutableSchema(Config{Resolvers: resolver}))) @@ -183,7 +194,7 @@ func TestFileUpload(t *testing.T) { bodyBuf := &bytes.Buffer{} bodyWriter := multipart.NewWriter(bodyBuf) - err := bodyWriter.WriteField("operations", `{ "query": "mutation($req: [UploadFile!]!) { multipleUploadWithPayload(req: $req) { id } }", "variables": { "req": [ { "id": 1, "file": null }, { "id": 2, "file": null } ] } }`) + err := bodyWriter.WriteField("operations", `{ "query": "mutation($req: [UploadFile!]!) { multipleUploadWithPayload(req: $req) { id, name, content } }", "variables": { "req": [ { "id": 1, "file": null }, { "id": 2, "file": null } ] } }`) require.NoError(t, err) err = bodyWriter.WriteField("map", `{ "0": ["variables.req.0.file"], "1": ["variables.req.1.file"] }`) require.NoError(t, err) @@ -208,7 +219,7 @@ func TestFileUpload(t *testing.T) { require.Equal(t, http.StatusOK, resp.StatusCode) responseBody, err := ioutil.ReadAll(resp.Body) require.Nil(t, err) - require.Equal(t, `{"data":{"multipleUploadWithPayload":[{"id":1},{"id":2}]}}`, string(responseBody)) + require.Equal(t, `{"data":{"multipleUploadWithPayload":[{"id":1,"name":"a.txt","content":"test1"},{"id":2,"name":"b.txt","content":"test2"}]}}`, string(responseBody)) }) } diff --git a/example/fileupload/generated.go b/example/fileupload/generated.go index fcb91dc2b6..0fb3ab47f8 100644 --- a/example/fileupload/generated.go +++ b/example/fileupload/generated.go @@ -43,7 +43,9 @@ type DirectiveRoot struct { type ComplexityRoot struct { File struct { - ID func(childComplexity int) int + ID func(childComplexity int) int + Name func(childComplexity int) int + Content func(childComplexity int) int } Mutation struct { @@ -90,6 +92,20 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.File.ID(childComplexity), true + case "File.Name": + if e.complexity.File.Name == nil { + break + } + + return e.complexity.File.Name(childComplexity), true + + case "File.Content": + if e.complexity.File.Content == nil { + break + } + + return e.complexity.File.Content(childComplexity), true + case "Mutation.SingleUpload": if e.complexity.Mutation.SingleUpload == nil { break @@ -227,6 +243,8 @@ var parsedSchema = gqlparser.MustLoadSchema( # The File type, represents the response of uploading a file type File { id: Int! + name: String! + content: String! } # The UploadFile type, represents the request for uploading a file with certain payload @@ -384,6 +402,58 @@ func (ec *executionContext) _File_id(ctx context.Context, field graphql.Collecte return ec.marshalNInt2int(ctx, field.Selections, res) } +func (ec *executionContext) _File_name(ctx context.Context, field graphql.CollectedField, obj *model.File) graphql.Marshaler { + ctx = ec.Tracer.StartFieldExecution(ctx, field) + defer func() { ec.Tracer.EndFieldExecution(ctx) }() + rctx := &graphql.ResolverContext{ + Object: "File", + Field: field, + Args: nil, + } + ctx = graphql.WithResolverContext(ctx, rctx) + ctx = ec.Tracer.StartFieldResolverExecution(ctx, rctx) + resTmp := ec.FieldMiddleware(ctx, obj, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return obj.Name, nil + }) + if resTmp == nil { + if !ec.HasError(rctx) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.(string) + rctx.Result = res + ctx = ec.Tracer.StartFieldChildExecution(ctx) + return ec.marshalNString2string(ctx, field.Selections, res) +} + +func (ec *executionContext) _File_content(ctx context.Context, field graphql.CollectedField, obj *model.File) graphql.Marshaler { + ctx = ec.Tracer.StartFieldExecution(ctx, field) + defer func() { ec.Tracer.EndFieldExecution(ctx) }() + rctx := &graphql.ResolverContext{ + Object: "File", + Field: field, + Args: nil, + } + ctx = graphql.WithResolverContext(ctx, rctx) + ctx = ec.Tracer.StartFieldResolverExecution(ctx, rctx) + resTmp := ec.FieldMiddleware(ctx, obj, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return obj.Content, nil + }) + if resTmp == nil { + if !ec.HasError(rctx) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.(string) + rctx.Result = res + ctx = ec.Tracer.StartFieldChildExecution(ctx) + return ec.marshalNString2string(ctx, field.Selections, res) +} + func (ec *executionContext) _Mutation_singleUpload(ctx context.Context, field graphql.CollectedField) graphql.Marshaler { ctx = ec.Tracer.StartFieldExecution(ctx, field) defer func() { ec.Tracer.EndFieldExecution(ctx) }() @@ -1442,6 +1512,16 @@ func (ec *executionContext) _File(ctx context.Context, sel ast.SelectionSet, obj if out.Values[i] == graphql.Null { invalid = true } + case "name": + out.Values[i] = ec._File_name(ctx, field, obj) + if out.Values[i] == graphql.Null { + invalid = true + } + case "content": + out.Values[i] = ec._File_content(ctx, field, obj) + if out.Values[i] == graphql.Null { + invalid = true + } default: panic("unknown field " + strconv.Quote(field.Name)) } diff --git a/example/fileupload/model/generated.go b/example/fileupload/model/generated.go index 3b19ef846c..88136be5e2 100644 --- a/example/fileupload/model/generated.go +++ b/example/fileupload/model/generated.go @@ -7,7 +7,9 @@ import ( ) type File struct { - ID int `json:"id"` + ID int `json:"id"` + Name string `json:"name"` + Content string `json:"content"` } type UploadFile struct { diff --git a/example/fileupload/readme.md b/example/fileupload/readme.md index a3d3b6b682..638f99a110 100644 --- a/example/fileupload/readme.md +++ b/example/fileupload/readme.md @@ -33,15 +33,64 @@ and open http://localhost:8087 in your browser ```shell curl localhost:8087/query \ - -F operations='{ "query": "mutation ($file: Upload!) { singleUpload(file: $file) { id } }", "variables": { "file": null } }' \ + -F operations='{ "query": "mutation ($file: Upload!) { singleUpload(file: $file) { id, name, content } }", "variables": { "file": null } }' \ -F map='{ "0": ["variables.file"] }' \ -F 0=@./example/fileupload./testfiles/a.txt ``` +#### Request payload + +``` +POST /query HTTP/1.1 +Host: localhost:8087 +User-Agent: curl/7.60.0 +Accept: */* +Content-Length: 540 +Content-Type: multipart/form-data; boundary=-------------------- +----e6b2b29561e71173 + +=> Send data, 540 bytes (0x21c) +--------------------------e6b2b29561e71173 +Content-Disposition: form-data; name="operations" + +{ "query": "mutation ($file: Upload!) { singleUpload(file: $file +) { id, name, content } }", "variables": { "file": null } } +--------------------------e6b2b29561e71173 +Content-Disposition: form-data; name="map" + +{ "0": ["variables.file"] } +--------------------------e6b2b29561e71173 +Content-Disposition: form-data; name="0"; filename="a.txt" +Content-Type: text/plain + +Alpha file content. +--------------------------e6b2b29561e71173-- +``` + +### Single file with payload + +#### Operations + +```js +{ + query: ` + mutation($file: Upload!) { + singleUpload(file: $file) { + id + } + } + `, + variables: { + file: File // a.txt + } +} +``` + +#### cURL request ```shell curl localhost:8087/query \ - -F operations='{ "query": "mutation ($req: UploadFile!) { singleUploadWithPayload(req: $req) { id } }", "variables": { "req": {"file": null, "id": 1 } } }' \ + -F operations='{ "query": "mutation ($req: UploadFile!) { singleUploadWithPayload(req: $req) { id, name, content } }", "variables": { "req": {"file": null, "id": 1 } } }' \ -F map='{ "0": ["variables.req.file"] }' \ -F 0=@./example/fileupload/testfiles/a.txt ``` @@ -53,27 +102,30 @@ POST /query HTTP/1.1 Host: localhost:8087 User-Agent: curl/7.60.0 Accept: */* -Content-Length: 525 +Content-Length: 575 Content-Type: multipart/form-data; boundary=-------------------- -----c259ddf1cd194033 -=> Send data, 525 bytes (0x20d) ---------------------------c259ddf1cd194033 +----38752760889d14aa + +=> Send data, 575 bytes (0x23f) +--------------------------38752760889d14aa Content-Disposition: form-data; name="operations" -{ "query": "mutation ($file: Upload!) { singleUpload(file: $file -) { id } }", "variables": { "file": null } } ---------------------------c259ddf1cd194033 +{ "query": "mutation ($req: UploadFile!) { singleUploadWithPaylo +ad(req: $req) { id, name, content } }", "variables": { "req": {" +file": null, "id": 1 } } } +--------------------------38752760889d14aa Content-Disposition: form-data; name="map" -{ "0": ["variables.file"] } ---------------------------c259ddf1cd194033 +{ "0": ["variables.req.file"] } +--------------------------38752760889d14aa Content-Disposition: form-data; name="0"; filename="a.txt" Content-Type: text/plain Alpha file content. ---------------------------c259ddf1cd194033-- +--------------------------38752760889d14aa-- ``` + ### File list #### Operations @@ -100,15 +152,76 @@ Alpha file content. ``` curl localhost:8087/query \ - -F operations='{ "query": "mutation($files: [Upload!]!) { multipleUpload(files: $files) { id } }", "variables": { "files": [null, null] } }' \ + -F operations='{ "query": "mutation($files: [Upload!]!) { multipleUpload(files: $files) { id, name, content } }", "variables": { "files": [null, null] } }' \ -F map='{ "0": ["variables.files.0"], "1": ["variables.files.1"] }' \ -F 0=@./example/fileupload/testfiles/b.txt \ -F 1=@./example/fileupload/testfiles/c.txt ``` +#### Request payload + +``` +POST /query HTTP/1.1 +Host: localhost:8087 +User-Agent: curl/7.60.0 +Accept: */* +Content-Length: 742 +Content-Type: multipart/form-data; boundary=-------------------- +----d7aca2a93c3655e0 + +=> Send data, 742 bytes (0x2e6) +--------------------------d7aca2a93c3655e0 +Content-Disposition: form-data; name="operations" + +{ "query": "mutation($files: [Upload!]!) { multipleUpload(files: + $files) { id, name, content } }", "variables": { "files": [null +, null] } } +--------------------------d7aca2a93c3655e0 +Content-Disposition: form-data; name="map" + +{ "0": ["variables.files.0"], "1": ["variables.files.1"] } +--------------------------d7aca2a93c3655e0 +Content-Disposition: form-data; name="0"; filename="b.txt" +Content-Type: text/plain + +Bravo file content. +--------------------------d7aca2a93c3655e0 +Content-Disposition: form-data; name="1"; filename="c.txt" +Content-Type: text/plain + +Charlie file content. +--------------------------d7aca2a93c3655e0-- +``` + + + +### File list with payload + +#### Operations + +```js +{ + query: ` + mutation($files: [Upload!]!) { + multipleUpload(files: $files) { + id + } + } + `, + variables: { + files: [ + File, // b.txt + File // c.txt + ] + } +} +``` + +#### cURL request + ``` curl localhost:8087/query \ - -F operations='{ "query": "mutation($req: [UploadFile!]!) { multipleUploadWithPayload(req: $req) { id } }", "variables": { "req": [ { "id": 1, "file": null }, { "id": 2, "file": null } ] } }' \ + -F operations='{ "query": "mutation($req: [UploadFile!]!) { multipleUploadWithPayload(req: $req) { id, name, content } }", "variables": { "req": [ { "id": 1, "file": null }, { "id": 2, "file": null } ] } }' \ -F map='{ "0": ["variables.req.0.file"], "1": ["variables.req.1.file"] }' \ -F 0=@./example/fileupload/testfiles/b.txt \ -F 1=@./example/fileupload/testfiles/c.txt @@ -117,26 +230,36 @@ curl localhost:8087/query \ #### Request payload ``` ---------------------------ec62457de6331cad +=> Send header, 191 bytes (0xbf) +POST /query HTTP/1.1 +Host: localhost:8087 +User-Agent: curl/7.60.0 +Accept: */* +Content-Length: 799 +Content-Type: multipart/form-data; boundary=-------------------- +----65aab09fb49ee66f + +=> Send data, 799 bytes (0x31f) +--------------------------65aab09fb49ee66f Content-Disposition: form-data; name="operations" -{ "query": "mutation($files: [Upload!]!) { multipleUpload(files: $files) { id } }", "variables": { "files": [null, null] } } ---------------------------ec62457de6331cad +{ "query": "mutation($req: [UploadFile!]!) { multipleUploadWithP +ayload(req: $req) { id, name, content } }", "variables": { "req" +: [ { "id": 1, "file": null }, { "id": 2, "file": null } ] } } +--------------------------65aab09fb49ee66f Content-Disposition: form-data; name="map" -{ "0": ["variables.files.0"], "1": ["variables.files.1"] } ---------------------------ec62457de6331cad +{ "0": ["variables.req.0.file"], "1": ["variables.req.1.file"] } +--------------------------65aab09fb49ee66f Content-Disposition: form-data; name="0"; filename="b.txt" Content-Type: text/plain Bravo file content. - ---------------------------ec62457de6331cad +--------------------------65aab09fb49ee66f Content-Disposition: form-data; name="1"; filename="c.txt" Content-Type: text/plain Charlie file content. - ---------------------------ec62457de6331cad-- +--------------------------65aab09fb49ee66f-- ``` diff --git a/example/fileupload/schema.graphql b/example/fileupload/schema.graphql index 937aa3f34c..d5f3067b36 100644 --- a/example/fileupload/schema.graphql +++ b/example/fileupload/schema.graphql @@ -3,6 +3,8 @@ # The File type, represents the response of uploading a file type File { id: Int! + name: String! + content: String! } # The UploadFile type, represents the request for uploading a file with certain payload diff --git a/example/fileupload/server/server.go b/example/fileupload/server/server.go index c87196b78e..c7cdf900c7 100644 --- a/example/fileupload/server/server.go +++ b/example/fileupload/server/server.go @@ -2,6 +2,8 @@ package main import ( "context" + "errors" + "io/ioutil" "log" "net/http" @@ -13,15 +15,72 @@ import ( func main() { http.Handle("/", handler.Playground("File Upload Demo", "/query")) + resolver := getResolver() + http.Handle("/query", handler.GraphQL(fileupload.NewExecutableSchema(fileupload.Config{Resolvers: resolver}))) + log.Print("connect to http://localhost:8087/ for GraphQL playground") + log.Fatal(http.ListenAndServe(":8087", nil)) +} + +func getResolver() *fileupload.Resolver { resolver := &fileupload.Resolver{ SingleUploadFunc: func(ctx context.Context, file graphql.Upload) (*model.File, error) { + content, err := ioutil.ReadAll(file.File) + if err != nil { + return nil, err + } + return &model.File{ + ID: 1, + Name: file.Filename, + Content: string(content), + }, nil + }, + SingleUploadWithPayloadFunc: func(ctx context.Context, req model.UploadFile) (*model.File, error) { + content, err := ioutil.ReadAll(req.File.File) + if err != nil { + return nil, err + } return &model.File{ - ID: 1, - },nil + ID: 1, + Name: req.File.Filename, + Content: string(content), + }, nil + }, + MultipleUploadFunc: func(ctx context.Context, files []graphql.Upload) ([]model.File, error) { + if len(files) == 0 { + return nil, errors.New("empty list") + } + var resp []model.File + for i := range files { + content, err := ioutil.ReadAll(files[i].File) + if err != nil { + return []model.File{}, err + } + resp = append(resp, model.File{ + ID: i + 1, + Name: files[i].Filename, + Content: string(content), + }) + } + return resp, nil + }, + MultipleUploadWithPayloadFunc: func(ctx context.Context, req []model.UploadFile) ([]model.File, error) { + if len(req) == 0 { + return nil, errors.New("empty list") + } + var resp []model.File + for i := range req { + content, err := ioutil.ReadAll(req[i].File.File) + if err != nil { + return []model.File{}, err + } + resp = append(resp, model.File{ + ID: i + 1, + Name: req[i].File.Filename, + Content: string(content), + }) + } + return resp, nil }, } - http.Handle("/query", handler.GraphQL(fileupload.NewExecutableSchema(fileupload.Config{Resolvers: resolver}))) - - log.Print("connect to http://localhost:8087/ for GraphQL playground") - log.Fatal(http.ListenAndServe(":8087", nil)) + return resolver } From 61c1cb9cbb5b1cb154901526beec94041c8b35b2 Mon Sep 17 00:00:00 2001 From: hantonelli Date: Sun, 10 Mar 2019 14:03:05 +0000 Subject: [PATCH 11/39] Improve examples --- example/fileupload/readme.md | 56 ++++-------------------------------- 1 file changed, 6 insertions(+), 50 deletions(-) diff --git a/example/fileupload/readme.md b/example/fileupload/readme.md index 638f99a110..3a884f2817 100644 --- a/example/fileupload/readme.md +++ b/example/fileupload/readme.md @@ -41,20 +41,10 @@ curl localhost:8087/query \ #### Request payload ``` -POST /query HTTP/1.1 -Host: localhost:8087 -User-Agent: curl/7.60.0 -Accept: */* -Content-Length: 540 -Content-Type: multipart/form-data; boundary=-------------------- -----e6b2b29561e71173 - -=> Send data, 540 bytes (0x21c) --------------------------e6b2b29561e71173 Content-Disposition: form-data; name="operations" -{ "query": "mutation ($file: Upload!) { singleUpload(file: $file -) { id, name, content } }", "variables": { "file": null } } +{ "query": "mutation ($file: Upload!) { singleUpload(file: $file) { id, name, content } }", "variables": { "file": null } } --------------------------e6b2b29561e71173 Content-Disposition: form-data; name="map" @@ -98,21 +88,10 @@ curl localhost:8087/query \ #### Request payload ``` -POST /query HTTP/1.1 -Host: localhost:8087 -User-Agent: curl/7.60.0 -Accept: */* -Content-Length: 575 -Content-Type: multipart/form-data; boundary=-------------------- -----38752760889d14aa - -=> Send data, 575 bytes (0x23f) --------------------------38752760889d14aa Content-Disposition: form-data; name="operations" -{ "query": "mutation ($req: UploadFile!) { singleUploadWithPaylo -ad(req: $req) { id, name, content } }", "variables": { "req": {" -file": null, "id": 1 } } } +{ "query": "mutation ($req: UploadFile!) { singleUploadWithPayload(req: $req) { id, name, content } }", "variables": { "req": {"file": null, "id": 1 } } } --------------------------38752760889d14aa Content-Disposition: form-data; name="map" @@ -142,7 +121,7 @@ Alpha file content. variables: { files: [ File, // b.txt - File // c.txt + File // c.txt ] } } @@ -161,21 +140,10 @@ curl localhost:8087/query \ #### Request payload ``` -POST /query HTTP/1.1 -Host: localhost:8087 -User-Agent: curl/7.60.0 -Accept: */* -Content-Length: 742 -Content-Type: multipart/form-data; boundary=-------------------- -----d7aca2a93c3655e0 - -=> Send data, 742 bytes (0x2e6) --------------------------d7aca2a93c3655e0 Content-Disposition: form-data; name="operations" -{ "query": "mutation($files: [Upload!]!) { multipleUpload(files: - $files) { id, name, content } }", "variables": { "files": [null -, null] } } +{ "query": "mutation($files: [Upload!]!) { multipleUpload(files: $files) { id, name, content } }", "variables": { "files": [null, null] } } --------------------------d7aca2a93c3655e0 Content-Disposition: form-data; name="map" @@ -211,7 +179,7 @@ Charlie file content. variables: { files: [ File, // b.txt - File // c.txt + File // c.txt ] } } @@ -230,22 +198,10 @@ curl localhost:8087/query \ #### Request payload ``` -=> Send header, 191 bytes (0xbf) -POST /query HTTP/1.1 -Host: localhost:8087 -User-Agent: curl/7.60.0 -Accept: */* -Content-Length: 799 -Content-Type: multipart/form-data; boundary=-------------------- -----65aab09fb49ee66f - -=> Send data, 799 bytes (0x31f) --------------------------65aab09fb49ee66f Content-Disposition: form-data; name="operations" -{ "query": "mutation($req: [UploadFile!]!) { multipleUploadWithP -ayload(req: $req) { id, name, content } }", "variables": { "req" -: [ { "id": 1, "file": null }, { "id": 2, "file": null } ] } } +{ "query": "mutation($req: [UploadFile!]!) { multipleUploadWithPayload(req: $req) { id, name, content } }", "variables": { "req": [ { "id": 1, "file": null }, { "id": 2, "file": null } ] } } --------------------------65aab09fb49ee66f Content-Disposition: form-data; name="map" From 68446e17d6157036cd59d897a35724dac43647bc Mon Sep 17 00:00:00 2001 From: hantonelli Date: Sun, 10 Mar 2019 14:06:18 +0000 Subject: [PATCH 12/39] Revert change to websocket_test --- handler/websocket_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/handler/websocket_test.go b/handler/websocket_test.go index f80a3270ad..f8675475c9 100644 --- a/handler/websocket_test.go +++ b/handler/websocket_test.go @@ -125,7 +125,7 @@ func TestWebsocket(t *testing.T) { func TestWebsocketWithKeepAlive(t *testing.T) { next := make(chan struct{}) - h := GraphQL(&executableSchemaStub{NextResp: next}, WebsocketKeepAliveDuration(10*time.Millisecond)) + h := GraphQL(&executableSchemaStub{next}, WebsocketKeepAliveDuration(10*time.Millisecond)) srv := httptest.NewServer(h) defer srv.Close() From 2c414edcbb3f672afea9cb7d1cfa822129674b84 Mon Sep 17 00:00:00 2001 From: hantonelli Date: Sun, 10 Mar 2019 16:01:12 +0000 Subject: [PATCH 13/39] Improve and add tests --- handler/graphql.go | 29 +++--- handler/graphql_test.go | 221 ++++++++++++++++++++++++++-------------- 2 files changed, 162 insertions(+), 88 deletions(-) diff --git a/handler/graphql.go b/handler/graphql.go index e38ef0d17c..5f8b8486c7 100644 --- a/handler/graphql.go +++ b/handler/graphql.go @@ -492,24 +492,27 @@ func sendErrorf(w http.ResponseWriter, code int, format string, args ...interfac sendError(w, code, &gqlerror.Error{Message: fmt.Sprintf(format, args...)}) } -//TODO Add test for this func processMultipart(r *http.Request, request *params) error { // Parse multipart form if err := r.ParseMultipartForm(1024); err != nil { - return err + return errors.New("failed to parse multipart form") + } + + // Unmarshal operations + var operations interface{} + if err := jsonDecode(bytes.NewBuffer([]byte(r.Form.Get("operations"))), &operations); err != nil { + return errors.New("operations form field could not be decoded") } // Unmarshal uploads var uploads = map[graphql.Upload][]string{} var uploadsMap = map[string][]string{} if err := json.Unmarshal([]byte(r.Form.Get("map")), &uploadsMap); err != nil { - return err + return errors.New("map form field could not be decoded") } else { for key, path := range uploadsMap { if file, header, err := r.FormFile(key); err != nil { - panic(err) - //w.WriteHeader(http.StatusInternalServerError) - //return + return errors.New(fmt.Sprintf("failed to get key %s from form", key)) } else { uploads[graphql.Upload{ File: file, @@ -520,13 +523,6 @@ func processMultipart(r *http.Request, request *params) error { } } - var operations interface{} - - // Unmarshal operations - if err := jsonDecode(bytes.NewBuffer([]byte(r.Form.Get("operations"))), &operations); err != nil { - return err - } - // addUploadToOperations uploads to operations for file, paths := range uploads { for _, path := range paths { @@ -553,7 +549,6 @@ func processMultipart(r *http.Request, request *params) error { return errors.New("invalid operation") } -//TODO Add test for this func addUploadToOperations(operations interface{}, upload graphql.Upload, path string) error { var parts []interface{} for _, p := range strings.Split(path, ".") { @@ -570,12 +565,18 @@ func addUploadToOperations(operations interface{}, upload graphql.Upload, path s last := i == len(parts)-1 switch idx := p.(type) { case string: + if operations == nil { + operations = map[string]interface{}{} + } if last { operations.(map[string]interface{})[idx] = upload } else { operations = operations.(map[string]interface{})[idx] } case int: + if operations == nil { + operations = map[string]interface{}{} + } if last { operations.([]interface{})[idx] = upload } else { diff --git a/handler/graphql_test.go b/handler/graphql_test.go index 0e8ca29804..bd9cfab08e 100644 --- a/handler/graphql_test.go +++ b/handler/graphql_test.go @@ -3,6 +3,7 @@ package handler import ( "bytes" "context" + "io/ioutil" "mime/multipart" "net/http" "net/http/httptest" @@ -130,24 +131,17 @@ func TestFileUpload(t *testing.T) { } handler := GraphQL(mock) - bodyBuf := &bytes.Buffer{} - bodyWriter := multipart.NewWriter(bodyBuf) - err := bodyWriter.WriteField("operations", `{ "query": "mutation ($file: Upload!) { singleUpload(file: $file) { id } }", "variables": { "file": null } }`) - require.NoError(t, err) - err = bodyWriter.WriteField("map", `{ "0": ["variables.file"] }`) - require.NoError(t, err) - w, err := bodyWriter.CreateFormFile("0", "a.txt") - require.NoError(t, err) - _, err = w.Write([]byte("test")) - require.NoError(t, err) - err = bodyWriter.Close() - require.NoError(t, err) - - req, err := http.NewRequest("POST", "/graphql", bodyBuf) - require.NoError(t, err) + operations := `{ "query": "mutation ($file: Upload!) { singleUpload(file: $file) { id } }", "variables": { "file": null } }` + mapData := `{ "0": ["variables.file"] }` + files := []file{ + { + mapKey: "0", + name: "a.txt", + content: "test1", + }, + } + req := createUploadRequest(t, operations, mapData, files) - contentType := bodyWriter.FormDataContentType() - req.Header.Set("Content-Type", contentType) resp := httptest.NewRecorder() handler.ServeHTTP(resp, req) require.Equal(t, http.StatusOK, resp.Code) @@ -164,24 +158,17 @@ func TestFileUpload(t *testing.T) { } handler := GraphQL(mock) - bodyBuf := &bytes.Buffer{} - bodyWriter := multipart.NewWriter(bodyBuf) - err := bodyWriter.WriteField("operations", `{ "query": "mutation ($req: UploadFile!) { singleUploadWithPayload(req: $req) { id } }", "variables": { "req": {"file": null, "id": 1 } } }`) - require.NoError(t, err) - err = bodyWriter.WriteField("map", `{ "0": ["variables.req.file"] }`) - require.NoError(t, err) - w, err := bodyWriter.CreateFormFile("0", "a.txt") - require.NoError(t, err) - _, err = w.Write([]byte("test")) - require.NoError(t, err) - err = bodyWriter.Close() - require.NoError(t, err) - - req, err := http.NewRequest("POST", "/graphql", bodyBuf) - require.NoError(t, err) + operations := `{ "query": "mutation ($req: UploadFile!) { singleUploadWithPayload(req: $req) { id } }", "variables": { "req": {"file": null, "id": 1 } } }` + mapData := `{ "0": ["variables.req.file"] }` + files := []file{ + { + mapKey: "0", + name: "a.txt", + content: "test1", + }, + } + req := createUploadRequest(t, operations, mapData, files) - contentType := bodyWriter.FormDataContentType() - req.Header.Set("Content-Type", contentType) resp := httptest.NewRecorder() handler.ServeHTTP(resp, req) require.Equal(t, http.StatusOK, resp.Code) @@ -198,27 +185,22 @@ func TestFileUpload(t *testing.T) { } handler := GraphQL(mock) - bodyBuf := &bytes.Buffer{} - bodyWriter := multipart.NewWriter(bodyBuf) - err := bodyWriter.WriteField("operations", `{ "query": "mutation($files: [Upload!]!) { multipleUpload(files: $files) { id } }", "variables": { "files": [null, null] } }`) - require.NoError(t, err) - err = bodyWriter.WriteField("map", `{ "0": ["variables.files.0"], "1": ["variables.files.1"] }`) - require.NoError(t, err) - w0, err := bodyWriter.CreateFormFile("0", "a.txt") - require.NoError(t, err) - _, err = w0.Write([]byte("test1")) - require.NoError(t, err) - w1, err := bodyWriter.CreateFormFile("1", "b.txt") - require.NoError(t, err) - _, err = w1.Write([]byte("test2")) - require.NoError(t, err) - err = bodyWriter.Close() - require.NoError(t, err) - - req, err := http.NewRequest("POST", "/graphql", bodyBuf) - require.NoError(t, err) + operations := `{ "query": "mutation($files: [Upload!]!) { multipleUpload(files: $files) { id } }", "variables": { "files": [null, null] } }` + mapData := `{ "0": ["variables.files.0"], "1": ["variables.files.1"] }` + files := []file{ + { + mapKey: "0", + name: "a.txt", + content: "test1", + }, + { + mapKey: "1", + name: "b.txt", + content: "test2", + }, + } + req := createUploadRequest(t, operations, mapData, files) - req.Header.Set("Content-Type", bodyWriter.FormDataContentType()) resp := httptest.NewRecorder() handler.ServeHTTP(resp, req) require.Equal(t, http.StatusOK, resp.Code) @@ -235,27 +217,22 @@ func TestFileUpload(t *testing.T) { } handler := GraphQL(mock) - bodyBuf := &bytes.Buffer{} - bodyWriter := multipart.NewWriter(bodyBuf) - err := bodyWriter.WriteField("operations", `{ "query": "mutation($req: [UploadFile!]!) { multipleUploadWithPayload(req: $req) { id } }", "variables": { "req": [ { "id": 1, "file": null }, { "id": 2, "file": null } ] } }`) - require.NoError(t, err) - err = bodyWriter.WriteField("map", `{ "0": ["variables.req.0.file"], "1": ["variables.req.1.file"] }`) - require.NoError(t, err) - w0, err := bodyWriter.CreateFormFile("0", "a.txt") - require.NoError(t, err) - _, err = w0.Write([]byte("test1")) - require.NoError(t, err) - w1, err := bodyWriter.CreateFormFile("1", "b.txt") - require.NoError(t, err) - _, err = w1.Write([]byte("test2")) - require.NoError(t, err) - err = bodyWriter.Close() - require.NoError(t, err) - - req, err := http.NewRequest("POST", "/graphql", bodyBuf) - require.NoError(t, err) + operations := `{ "query": "mutation($req: [UploadFile!]!) { multipleUploadWithPayload(req: $req) { id } }", "variables": { "req": [ { "id": 1, "file": null }, { "id": 2, "file": null } ] } }` + mapData := `{ "0": ["variables.req.0.file"], "1": ["variables.req.1.file"] }` + files := []file{ + { + mapKey: "0", + name: "a.txt", + content: "test1", + }, + { + mapKey: "1", + name: "b.txt", + content: "test2", + }, + } + req := createUploadRequest(t, operations, mapData, files) - req.Header.Set("Content-Type", bodyWriter.FormDataContentType()) resp := httptest.NewRecorder() handler.ServeHTTP(resp, req) require.Equal(t, http.StatusOK, resp.Code) @@ -263,6 +240,70 @@ func TestFileUpload(t *testing.T) { }) } +func TestProcessMultipart(t *testing.T) { + t.Run("parse multipart form failure", func(t *testing.T) { + req := &http.Request{ + Method: "POST", + Header: http.Header{"Content-Type": {`multipart/form-data; boundary="foo123"`}}, + Body: ioutil.NopCloser(new(bytes.Buffer)), + } + var reqParams params + err := processMultipart(req, &reqParams) + require.NotNil(t, err) + errMsg := err.Error() + require.Equal(t, errMsg, "failed to parse multipart form") + }) + + t.Run("fail parse operation", func(t *testing.T) { + operations := `invalid operation` + mapData := `{ "0": ["variables.file"] }` + files := []file{ + { + mapKey: "0", + name: "a.txt", + content: "test1", + }, + } + req := createUploadRequest(t, operations, mapData, files) + + var reqParams params + err := processMultipart(req, &reqParams) + require.NotNil(t, err) + require.Equal(t, err.Error(), "operations form field could not be decoded") + }) + + t.Run("fail parse map", func(t *testing.T) { + operations := `{ "query": "mutation ($file: Upload!) { singleUpload(file: $file) { id } }", "variables": { "file": null } }` + mapData := `invalid map` + files := []file{ + { + mapKey: "0", + name: "a.txt", + content: "test1", + }, + } + req := createUploadRequest(t, operations, mapData, files) + + var reqParams params + err := processMultipart(req, &reqParams) + require.NotNil(t, err) + require.Equal(t, err.Error(), "map form field could not be decoded") + }) + + t.Run("fail missing file", func(t *testing.T) { + operations := `{ "query": "mutation ($file: Upload!) { singleUpload(file: $file) { id } }", "variables": { "file": null } }` + mapData := `{ "0": ["variables.file"] }` + var files []file + req := createUploadRequest(t, operations, mapData, files) + + var reqParams params + err := processMultipart(req, &reqParams) + require.NotNil(t, err) + require.Equal(t, err.Error(), "failed to get key 0 from form") + }) + +} + func TestHandlerOptions(t *testing.T) { h := GraphQL(&executableSchemaStub{}) @@ -278,6 +319,38 @@ func TestHandlerHead(t *testing.T) { assert.Equal(t, http.StatusMethodNotAllowed, resp.Code) } +type file struct { + mapKey string + name string + content string +} + +func createUploadRequest(t *testing.T, operations, mapData string, files []file) *http.Request { + bodyBuf := &bytes.Buffer{} + bodyWriter := multipart.NewWriter(bodyBuf) + + err := bodyWriter.WriteField("operations", operations) + require.NoError(t, err) + + err = bodyWriter.WriteField("map", mapData) + require.NoError(t, err) + + for i := range files { + ff, err := bodyWriter.CreateFormFile(files[i].mapKey, files[i].name) + require.NoError(t, err) + _, err = ff.Write([]byte(files[i].content)) + require.NoError(t, err) + } + err = bodyWriter.Close() + require.NoError(t, err) + + req, err := http.NewRequest("POST", "/graphql", bodyBuf) + require.NoError(t, err) + + req.Header.Set("Content-Type", bodyWriter.FormDataContentType()) + return req +} + func doRequest(handler http.Handler, method string, target string, body string) *httptest.ResponseRecorder { r := httptest.NewRequest(method, target, strings.NewReader(body)) w := httptest.NewRecorder() From 4d92696bf6e87404f85aaea0a5bfd8c15b7744b7 Mon Sep 17 00:00:00 2001 From: hantonelli Date: Sun, 10 Mar 2019 17:53:35 +0000 Subject: [PATCH 14/39] Clean up code and add tests --- handler/graphql.go | 72 ++++++++++++------------- handler/graphql_test.go | 114 ++++++++++++++++++++++++++++++---------- 2 files changed, 122 insertions(+), 64 deletions(-) diff --git a/handler/graphql.go b/handler/graphql.go index 5f8b8486c7..810ffa3dea 100644 --- a/handler/graphql.go +++ b/handler/graphql.go @@ -1,7 +1,6 @@ package handler import ( - "bytes" "context" "encoding/json" "errors" @@ -23,6 +22,11 @@ import ( "github.com/vektah/gqlparser/validator" ) +const ( + defaultMaxMemory = 32 << 20 // 32 MB + variablePrefix = "variables." +) + type params struct { Query string `json:"query"` OperationName string `json:"operationName"` @@ -494,59 +498,53 @@ func sendErrorf(w http.ResponseWriter, code int, format string, args ...interfac func processMultipart(r *http.Request, request *params) error { // Parse multipart form - if err := r.ParseMultipartForm(1024); err != nil { + if err := r.ParseMultipartForm(defaultMaxMemory); err != nil { return errors.New("failed to parse multipart form") } // Unmarshal operations - var operations interface{} - if err := jsonDecode(bytes.NewBuffer([]byte(r.Form.Get("operations"))), &operations); err != nil { + var operations map[string]interface{} + if err := jsonDecode(strings.NewReader(r.Form.Get("operations")), &operations); err != nil { return errors.New("operations form field could not be decoded") } - // Unmarshal uploads - var uploads = map[graphql.Upload][]string{} + // Unmarshal map var uploadsMap = map[string][]string{} if err := json.Unmarshal([]byte(r.Form.Get("map")), &uploadsMap); err != nil { return errors.New("map form field could not be decoded") - } else { - for key, path := range uploadsMap { - if file, header, err := r.FormFile(key); err != nil { - return errors.New(fmt.Sprintf("failed to get key %s from form", key)) - } else { - uploads[graphql.Upload{ - File: file, - Size: header.Size, - Filename: header.Filename, - }] = path - } - } } - // addUploadToOperations uploads to operations - for file, paths := range uploads { - for _, path := range paths { - addUploadToOperations(operations, file, path) - } - } - - switch data := operations.(type) { - case map[string]interface{}: - if value, ok := data["operationName"]; ok && value != nil { - request.OperationName = value.(string) + var upload graphql.Upload + for key, path := range uploadsMap { + file, header, err := r.FormFile(key) + if err != nil { + return errors.New(fmt.Sprintf("failed to get key %s from form", key)) } - if value, ok := data["query"]; ok && value != nil { - request.Query = value.(string) + upload = graphql.Upload{ + File: file, + Size: header.Size, + Filename: header.Filename, } - if value, ok := data["variables"]; ok && value != nil { - request.Variables = value.(map[string]interface{}) + + if len(path) != 1 || !strings.HasPrefix(path[0], variablePrefix) { + return errors.New(fmt.Sprintf("invalid value for key %s", key)) } - return nil - default: - return errors.New("bad request") + + addUploadToOperations(operations, upload, path[0]) + } + + // set request variables + if value, ok := operations["operationName"]; ok && value != nil { + request.OperationName = value.(string) + } + if value, ok := operations["query"]; ok && value != nil { + request.Query = value.(string) + } + if value, ok := operations["variables"]; ok && value != nil { + request.Variables = value.(map[string]interface{}) } - return errors.New("invalid operation") + return nil } func addUploadToOperations(operations interface{}, upload graphql.Upload, path string) error { diff --git a/handler/graphql_test.go b/handler/graphql_test.go index bd9cfab08e..73f5f66c75 100644 --- a/handler/graphql_test.go +++ b/handler/graphql_test.go @@ -135,7 +135,7 @@ func TestFileUpload(t *testing.T) { mapData := `{ "0": ["variables.file"] }` files := []file{ { - mapKey: "0", + mapKey: "0", name: "a.txt", content: "test1", }, @@ -162,7 +162,7 @@ func TestFileUpload(t *testing.T) { mapData := `{ "0": ["variables.req.file"] }` files := []file{ { - mapKey: "0", + mapKey: "0", name: "a.txt", content: "test1", }, @@ -189,12 +189,12 @@ func TestFileUpload(t *testing.T) { mapData := `{ "0": ["variables.files.0"], "1": ["variables.files.1"] }` files := []file{ { - mapKey: "0", + mapKey: "0", name: "a.txt", content: "test1", }, { - mapKey: "1", + mapKey: "1", name: "b.txt", content: "test2", }, @@ -221,12 +221,12 @@ func TestFileUpload(t *testing.T) { mapData := `{ "0": ["variables.req.0.file"], "1": ["variables.req.1.file"] }` files := []file{ { - mapKey: "0", + mapKey: "0", name: "a.txt", content: "test1", }, { - mapKey: "1", + mapKey: "1", name: "b.txt", content: "test2", }, @@ -241,6 +241,16 @@ func TestFileUpload(t *testing.T) { } func TestProcessMultipart(t *testing.T) { + validOperations := `{ "query": "mutation ($file: Upload!) { singleUpload(file: $file) { id } }", "variables": { "file": null } }` + validMap := `{ "0": ["variables.file"] }` + validFiles := []file{ + { + mapKey: "0", + name: "a.txt", + content: "test1", + }, + } + t.Run("parse multipart form failure", func(t *testing.T) { req := &http.Request{ Method: "POST", @@ -256,15 +266,7 @@ func TestProcessMultipart(t *testing.T) { t.Run("fail parse operation", func(t *testing.T) { operations := `invalid operation` - mapData := `{ "0": ["variables.file"] }` - files := []file{ - { - mapKey: "0", - name: "a.txt", - content: "test1", - }, - } - req := createUploadRequest(t, operations, mapData, files) + req := createUploadRequest(t, operations, validMap, validFiles) var reqParams params err := processMultipart(req, &reqParams) @@ -273,16 +275,8 @@ func TestProcessMultipart(t *testing.T) { }) t.Run("fail parse map", func(t *testing.T) { - operations := `{ "query": "mutation ($file: Upload!) { singleUpload(file: $file) { id } }", "variables": { "file": null } }` mapData := `invalid map` - files := []file{ - { - mapKey: "0", - name: "a.txt", - content: "test1", - }, - } - req := createUploadRequest(t, operations, mapData, files) + req := createUploadRequest(t, validOperations, mapData, validFiles) var reqParams params err := processMultipart(req, &reqParams) @@ -291,10 +285,8 @@ func TestProcessMultipart(t *testing.T) { }) t.Run("fail missing file", func(t *testing.T) { - operations := `{ "query": "mutation ($file: Upload!) { singleUpload(file: $file) { id } }", "variables": { "file": null } }` - mapData := `{ "0": ["variables.file"] }` var files []file - req := createUploadRequest(t, operations, mapData, files) + req := createUploadRequest(t, validOperations, validMap, files) var reqParams params err := processMultipart(req, &reqParams) @@ -302,6 +294,45 @@ func TestProcessMultipart(t *testing.T) { require.Equal(t, err.Error(), "failed to get key 0 from form") }) + t.Run("fail if map entry with two values", func(t *testing.T) { + mapData := `{ "0": ["variables.file", "variables.file"] }` + req := createUploadRequest(t, validOperations, mapData, validFiles) + + var reqParams params + err := processMultipart(req, &reqParams) + require.NotNil(t, err) + require.Equal(t, err.Error(), "invalid value for key 0") + }) + + t.Run("fail if map entry with invalid prefix", func(t *testing.T) { + mapData := `{ "0": ["var.file"] }` + req := createUploadRequest(t, validOperations, mapData, validFiles) + + var reqParams params + err := processMultipart(req, &reqParams) + require.NotNil(t, err) + require.Equal(t, err.Error(), "invalid value for key 0") + }) + + t.Run("valid request", func(t *testing.T) { + req := createUploadRequest(t, validOperations, validMap, validFiles) + + var reqParams params + err := processMultipart(req, &reqParams) + require.Nil(t, err) + require.Equal(t, "mutation ($file: Upload!) { singleUpload(file: $file) { id } }", reqParams.Query) + require.Equal(t, "", reqParams.OperationName) + require.Equal(t, 1, len(reqParams.Variables)) + require.NotNil(t, reqParams.Variables["file"]) + reqParamsFile, ok := reqParams.Variables["file"].(graphql.Upload) + require.True(t, ok) + require.Equal(t, "a.txt", reqParamsFile.Filename) + require.Equal(t, int64(len("test1")), reqParamsFile.Size) + var content []byte + content, err = ioutil.ReadAll(reqParamsFile.File) + require.Nil(t, err) + require.Equal(t, "test1", string(content)) + }) } func TestHandlerOptions(t *testing.T) { @@ -351,6 +382,35 @@ func createUploadRequest(t *testing.T, operations, mapData string, files []file) return req } +func createInvalidUploadRequest(t *testing.T, operations, mapData string, files []file) *http.Request { + bodyBuf := &bytes.Buffer{} + bodyWriter := multipart.NewWriter(bodyBuf) + + err := bodyWriter.WriteField("invalidFormParameter", "") + require.NoError(t, err) + + err = bodyWriter.WriteField("operations", operations) + require.NoError(t, err) + + err = bodyWriter.WriteField("map", mapData) + require.NoError(t, err) + + for i := range files { + ff, err := bodyWriter.CreateFormFile(files[i].mapKey, files[i].name) + require.NoError(t, err) + _, err = ff.Write([]byte(files[i].content)) + require.NoError(t, err) + } + err = bodyWriter.Close() + require.NoError(t, err) + + req, err := http.NewRequest("POST", "/graphql", bodyBuf) + require.NoError(t, err) + + req.Header.Set("Content-Type", bodyWriter.FormDataContentType()) + return req +} + func doRequest(handler http.Handler, method string, target string, body string) *httptest.ResponseRecorder { r := httptest.NewRequest(method, target, strings.NewReader(body)) w := httptest.NewRecorder() From be8d6d1249bca523133950a631686267467c0d96 Mon Sep 17 00:00:00 2001 From: hantonelli Date: Sun, 10 Mar 2019 21:49:20 +0000 Subject: [PATCH 15/39] Improve test --- handler/graphql.go | 8 +-- handler/graphql_test.go | 115 +++++++++++++++++++++++++++++----------- 2 files changed, 87 insertions(+), 36 deletions(-) diff --git a/handler/graphql.go b/handler/graphql.go index 810ffa3dea..d027adf28c 100644 --- a/handler/graphql.go +++ b/handler/graphql.go @@ -24,7 +24,7 @@ import ( const ( defaultMaxMemory = 32 << 20 // 32 MB - variablePrefix = "variables." + variablePrefix = "variables." ) type params struct { @@ -551,7 +551,7 @@ func addUploadToOperations(operations interface{}, upload graphql.Upload, path s var parts []interface{} for _, p := range strings.Split(path, ".") { if isNumber, err := regexp.MatchString(`\d+`, p); err != nil { - return err + return errors.New(fmt.Sprintf("failed to parse path, path: %s, subpath: %s", path, p)) } else if isNumber { index, _ := strconv.Atoi(p) parts = append(parts, index) @@ -564,7 +564,7 @@ func addUploadToOperations(operations interface{}, upload graphql.Upload, path s switch idx := p.(type) { case string: if operations == nil { - operations = map[string]interface{}{} + return errors.New(fmt.Sprintf("variables is missing, path: %s", path)) } if last { operations.(map[string]interface{})[idx] = upload @@ -573,7 +573,7 @@ func addUploadToOperations(operations interface{}, upload graphql.Upload, path s } case int: if operations == nil { - operations = map[string]interface{}{} + return errors.New(fmt.Sprintf("variables is missing, path: %s", path)) } if last { operations.([]interface{})[idx] = upload diff --git a/handler/graphql_test.go b/handler/graphql_test.go index 73f5f66c75..7fd3a30e00 100644 --- a/handler/graphql_test.go +++ b/handler/graphql_test.go @@ -7,6 +7,7 @@ import ( "mime/multipart" "net/http" "net/http/httptest" + "os" "strings" "testing" @@ -251,7 +252,7 @@ func TestProcessMultipart(t *testing.T) { }, } - t.Run("parse multipart form failure", func(t *testing.T) { + t.Run("fail to parse multipart", func(t *testing.T) { req := &http.Request{ Method: "POST", Header: http.Header{"Content-Type": {`multipart/form-data; boundary="foo123"`}}, @@ -294,7 +295,7 @@ func TestProcessMultipart(t *testing.T) { require.Equal(t, err.Error(), "failed to get key 0 from form") }) - t.Run("fail if map entry with two values", func(t *testing.T) { + t.Run("fail map entry with two values", func(t *testing.T) { mapData := `{ "0": ["variables.file", "variables.file"] }` req := createUploadRequest(t, validOperations, mapData, validFiles) @@ -304,7 +305,7 @@ func TestProcessMultipart(t *testing.T) { require.Equal(t, err.Error(), "invalid value for key 0") }) - t.Run("fail if map entry with invalid prefix", func(t *testing.T) { + t.Run("fail map entry with invalid prefix", func(t *testing.T) { mapData := `{ "0": ["var.file"] }` req := createUploadRequest(t, validOperations, mapData, validFiles) @@ -335,6 +336,85 @@ func TestProcessMultipart(t *testing.T) { }) } +func TestAddUploadToOperations(t *testing.T) { + + t.Run("fail missing all variables", func(t *testing.T) { + file, err := os.Open("path/to/file") + var operations map[string]interface{} + upload := graphql.Upload{ + File: file, + Filename: "a.txt", + Size: int64(5), + } + path := "variables.req.0.file" + err = addUploadToOperations(operations, upload, path) + require.NotNil(t, err) + require.Equal(t, "variables is missing, path: variables.req.0.file", err.Error()) + }) + + t.Run("valid variable", func(t *testing.T) { + file, err := os.Open("path/to/file") + operations := map[string]interface{}{ + "variables": map[string]interface{}{ + "file": nil, + }, + } + + upload := graphql.Upload{ + File: file, + Filename: "a.txt", + Size: int64(5), + } + + expected := map[string]interface{}{ + "variables": map[string]interface{}{ + "file": upload, + }, + } + + path := "variables.file" + err = addUploadToOperations(operations, upload, path) + require.Nil(t, err) + + require.Equal(t, operations, expected) + }) + + t.Run("valid nested variable", func(t *testing.T) { + file, err := os.Open("path/to/file") + operations := map[string]interface{}{ + "variables": map[string]interface{}{ + "req": []interface{}{ + map[string]interface{}{ + "file": nil, + }, + }, + }, + } + + upload := graphql.Upload{ + File: file, + Filename: "a.txt", + Size: int64(5), + } + + expected := map[string]interface{}{ + "variables": map[string]interface{}{ + "req": []interface{}{ + map[string]interface{}{ + "file": upload, + }, + }, + }, + } + + path := "variables.req.0.file" + err = addUploadToOperations(operations, upload, path) + require.Nil(t, err) + + require.Equal(t, operations, expected) + }) +} + func TestHandlerOptions(t *testing.T) { h := GraphQL(&executableSchemaStub{}) @@ -382,35 +462,6 @@ func createUploadRequest(t *testing.T, operations, mapData string, files []file) return req } -func createInvalidUploadRequest(t *testing.T, operations, mapData string, files []file) *http.Request { - bodyBuf := &bytes.Buffer{} - bodyWriter := multipart.NewWriter(bodyBuf) - - err := bodyWriter.WriteField("invalidFormParameter", "") - require.NoError(t, err) - - err = bodyWriter.WriteField("operations", operations) - require.NoError(t, err) - - err = bodyWriter.WriteField("map", mapData) - require.NoError(t, err) - - for i := range files { - ff, err := bodyWriter.CreateFormFile(files[i].mapKey, files[i].name) - require.NoError(t, err) - _, err = ff.Write([]byte(files[i].content)) - require.NoError(t, err) - } - err = bodyWriter.Close() - require.NoError(t, err) - - req, err := http.NewRequest("POST", "/graphql", bodyBuf) - require.NoError(t, err) - - req.Header.Set("Content-Type", bodyWriter.FormDataContentType()) - return req -} - func doRequest(handler http.Handler, method string, target string, body string) *httptest.ResponseRecorder { r := httptest.NewRequest(method, target, strings.NewReader(body)) w := httptest.NewRecorder() From 239bc46f6f54f7d7f28f6f182b93a91dbc75d50d Mon Sep 17 00:00:00 2001 From: hantonelli Date: Sun, 10 Mar 2019 21:57:55 +0000 Subject: [PATCH 16/39] Add comments --- handler/graphql.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/handler/graphql.go b/handler/graphql.go index d027adf28c..28eb0e22a3 100644 --- a/handler/graphql.go +++ b/handler/graphql.go @@ -514,6 +514,7 @@ func processMultipart(r *http.Request, request *params) error { return errors.New("map form field could not be decoded") } + // Read form files and add them to operations variables var upload graphql.Upload for key, path := range uploadsMap { file, header, err := r.FormFile(key) @@ -525,11 +526,9 @@ func processMultipart(r *http.Request, request *params) error { Size: header.Size, Filename: header.Filename, } - if len(path) != 1 || !strings.HasPrefix(path[0], variablePrefix) { return errors.New(fmt.Sprintf("invalid value for key %s", key)) } - addUploadToOperations(operations, upload, path[0]) } From 3a6f2fb7e7e83d7de00fc6198795257f81c40d59 Mon Sep 17 00:00:00 2001 From: hantonelli Date: Sun, 10 Mar 2019 22:28:12 +0000 Subject: [PATCH 17/39] Improve test code --- example/fileupload/fileupload_test.go | 154 ++++++++++++++------------ 1 file changed, 86 insertions(+), 68 deletions(-) diff --git a/example/fileupload/fileupload_test.go b/example/fileupload/fileupload_test.go index 948a5ce38f..6ada462a15 100644 --- a/example/fileupload/fileupload_test.go +++ b/example/fileupload/fileupload_test.go @@ -17,6 +17,7 @@ import ( ) func TestFileUpload(t *testing.T) { + client := http.Client{} t.Run("valid single file upload", func(t *testing.T) { resolver := &Resolver{ @@ -37,22 +38,18 @@ func TestFileUpload(t *testing.T) { srv := httptest.NewServer(handler.GraphQL(NewExecutableSchema(Config{Resolvers: resolver}))) defer srv.Close() - bodyBuf := &bytes.Buffer{} - bodyWriter := multipart.NewWriter(bodyBuf) - err := bodyWriter.WriteField("operations", `{ "query": "mutation ($file: Upload!) { singleUpload(file: $file) { id, name, content } }", "variables": { "file": null } }`) - require.NoError(t, err) - err = bodyWriter.WriteField("map", `{ "0": ["variables.file"] }`) - require.NoError(t, err) - w, err := bodyWriter.CreateFormFile("0", "a.txt") - require.NoError(t, err) - _, err = w.Write([]byte("test")) - require.NoError(t, err) - err = bodyWriter.Close() - require.NoError(t, err) - - contentType := bodyWriter.FormDataContentType() + operations := `{ "query": "mutation ($file: Upload!) { singleUpload(file: $file) { id, name, content } }", "variables": { "file": null } }` + mapData := `{ "0": ["variables.file"] }` + files := []file{ + { + mapKey: "0", + name: "a.txt", + content: "test", + }, + } + req := createUploadRequest(t, srv.URL, operations, mapData, files) - resp, err := http.Post(fmt.Sprintf("%s/graphql", srv.URL), contentType, bodyBuf) + resp, err := client.Do(req) require.Nil(t, err) defer func() { _ = resp.Body.Close() @@ -84,22 +81,18 @@ func TestFileUpload(t *testing.T) { srv := httptest.NewServer(handler.GraphQL(NewExecutableSchema(Config{Resolvers: resolver}))) defer srv.Close() - bodyBuf := &bytes.Buffer{} - bodyWriter := multipart.NewWriter(bodyBuf) - err := bodyWriter.WriteField("operations", `{ "query": "mutation ($req: UploadFile!) { singleUploadWithPayload(req: $req) { id, name, content } }", "variables": { "req": {"file": null, "id": 1 } } }`) - require.NoError(t, err) - err = bodyWriter.WriteField("map", `{ "0": ["variables.req.file"] }`) - require.NoError(t, err) - w, err := bodyWriter.CreateFormFile("0", "a.txt") - require.NoError(t, err) - _, err = w.Write([]byte("test")) - require.NoError(t, err) - err = bodyWriter.Close() - require.NoError(t, err) - - contentType := bodyWriter.FormDataContentType() + operations := `{ "query": "mutation ($req: UploadFile!) { singleUploadWithPayload(req: $req) { id, name, content } }", "variables": { "req": {"file": null, "id": 1 } } }` + mapData := `{ "0": ["variables.req.file"] }` + files := []file{ + { + mapKey: "0", + name: "a.txt", + content: "test", + }, + } + req := createUploadRequest(t, srv.URL, operations, mapData, files) - resp, err := http.Post(fmt.Sprintf("%s/graphql", srv.URL), contentType, bodyBuf) + resp, err := client.Do(req) require.Nil(t, err) defer func() { _ = resp.Body.Close() @@ -134,26 +127,23 @@ func TestFileUpload(t *testing.T) { srv := httptest.NewServer(handler.GraphQL(NewExecutableSchema(Config{Resolvers: resolver}))) defer srv.Close() - bodyBuf := &bytes.Buffer{} - bodyWriter := multipart.NewWriter(bodyBuf) - err := bodyWriter.WriteField("operations", `{ "query": "mutation($files: [Upload!]!) { multipleUpload(files: $files) { id, name, content } }", "variables": { "files": [null, null] } }`) - require.NoError(t, err) - err = bodyWriter.WriteField("map", `{ "0": ["variables.files.0"], "1": ["variables.files.1"] }`) - require.NoError(t, err) - w0, err := bodyWriter.CreateFormFile("0", "a.txt") - require.NoError(t, err) - _, err = w0.Write([]byte("test1")) - require.NoError(t, err) - w1, err := bodyWriter.CreateFormFile("1", "b.txt") - require.NoError(t, err) - _, err = w1.Write([]byte("test2")) - require.NoError(t, err) - err = bodyWriter.Close() - require.NoError(t, err) - - contentType := bodyWriter.FormDataContentType() + operations := `{ "query": "mutation($files: [Upload!]!) { multipleUpload(files: $files) { id, name, content } }", "variables": { "files": [null, null] } }` + mapData := `{ "0": ["variables.files.0"], "1": ["variables.files.1"] }` + files := []file{ + { + mapKey: "0", + name: "a.txt", + content: "test1", + }, + { + mapKey: "1", + name: "b.txt", + content: "test2", + }, + } + req := createUploadRequest(t, srv.URL, operations, mapData, files) - resp, err := http.Post(fmt.Sprintf("%s/graphql", srv.URL), contentType, bodyBuf) + resp, err := client.Do(req) require.Nil(t, err) defer func() { _ = resp.Body.Close() @@ -192,26 +182,23 @@ func TestFileUpload(t *testing.T) { srv := httptest.NewServer(handler.GraphQL(NewExecutableSchema(Config{Resolvers: resolver}))) defer srv.Close() - bodyBuf := &bytes.Buffer{} - bodyWriter := multipart.NewWriter(bodyBuf) - err := bodyWriter.WriteField("operations", `{ "query": "mutation($req: [UploadFile!]!) { multipleUploadWithPayload(req: $req) { id, name, content } }", "variables": { "req": [ { "id": 1, "file": null }, { "id": 2, "file": null } ] } }`) - require.NoError(t, err) - err = bodyWriter.WriteField("map", `{ "0": ["variables.req.0.file"], "1": ["variables.req.1.file"] }`) - require.NoError(t, err) - w0, err := bodyWriter.CreateFormFile("0", "a.txt") - require.NoError(t, err) - _, err = w0.Write([]byte("test1")) - require.NoError(t, err) - w1, err := bodyWriter.CreateFormFile("1", "b.txt") - require.NoError(t, err) - _, err = w1.Write([]byte("test2")) - require.NoError(t, err) - err = bodyWriter.Close() - require.NoError(t, err) - - contentType := bodyWriter.FormDataContentType() + operations := `{ "query": "mutation($req: [UploadFile!]!) { multipleUploadWithPayload(req: $req) { id, name, content } }", "variables": { "req": [ { "id": 1, "file": null }, { "id": 2, "file": null } ] } }` + mapData := `{ "0": ["variables.req.0.file"], "1": ["variables.req.1.file"] }` + files := []file{ + { + mapKey: "0", + name: "a.txt", + content: "test1", + }, + { + mapKey: "1", + name: "b.txt", + content: "test2", + }, + } + req := createUploadRequest(t, srv.URL, operations, mapData, files) - resp, err := http.Post(fmt.Sprintf("%s/graphql", srv.URL), contentType, bodyBuf) + resp, err := client.Do(req) require.Nil(t, err) defer func() { _ = resp.Body.Close() @@ -221,5 +208,36 @@ func TestFileUpload(t *testing.T) { require.Nil(t, err) require.Equal(t, `{"data":{"multipleUploadWithPayload":[{"id":1,"name":"a.txt","content":"test1"},{"id":2,"name":"b.txt","content":"test2"}]}}`, string(responseBody)) }) +} + +type file struct { + mapKey string + name string + content string +} + +func createUploadRequest(t *testing.T, url, operations, mapData string, files []file) *http.Request { + bodyBuf := &bytes.Buffer{} + bodyWriter := multipart.NewWriter(bodyBuf) + + err := bodyWriter.WriteField("operations", operations) + require.NoError(t, err) + + err = bodyWriter.WriteField("map", mapData) + require.NoError(t, err) + + for i := range files { + ff, err := bodyWriter.CreateFormFile(files[i].mapKey, files[i].name) + require.NoError(t, err) + _, err = ff.Write([]byte(files[i].content)) + require.NoError(t, err) + } + err = bodyWriter.Close() + require.NoError(t, err) + + req, err := http.NewRequest("POST", fmt.Sprintf("%s/graphql", url), bodyBuf) + require.NoError(t, err) + req.Header.Set("Content-Type", bodyWriter.FormDataContentType()) + return req } From f244442e86a96c99333610972134cf90a081fddc Mon Sep 17 00:00:00 2001 From: hantonelli Date: Sun, 31 Mar 2019 14:00:17 +0100 Subject: [PATCH 18/39] Fix merge. Remove regexp check. --- handler/graphql.go | 9 +-- handler/graphql_test.go | 123 ++++++++++++++++++++-------------------- 2 files changed, 63 insertions(+), 69 deletions(-) diff --git a/handler/graphql.go b/handler/graphql.go index a54307e9c5..f9a59b1ff8 100644 --- a/handler/graphql.go +++ b/handler/graphql.go @@ -1,15 +1,12 @@ package handler import ( - "bytes" "context" "encoding/json" "errors" "fmt" "io" - "mime/multipart" "net/http" - "regexp" "strconv" "strings" "time" @@ -565,10 +562,8 @@ func processMultipart(r *http.Request, request *params) error { func addUploadToOperations(operations interface{}, upload graphql.Upload, path string) error { var parts []interface{} for _, p := range strings.Split(path, ".") { - if isNumber, err := regexp.MatchString(`\d+`, p); err != nil { - return errors.New(fmt.Sprintf("failed to parse path, path: %s, subpath: %s", path, p)) - } else if isNumber { - index, _ := strconv.Atoi(p) + index, parseNbrErr := strconv.Atoi(p) + if parseNbrErr == nil { parts = append(parts, index) } else { parts = append(parts, p) diff --git a/handler/graphql_test.go b/handler/graphql_test.go index 526ca4c4cc..254a2ede59 100644 --- a/handler/graphql_test.go +++ b/handler/graphql_test.go @@ -1,7 +1,6 @@ package handler import ( - "context" "bytes" "context" "io/ioutil" @@ -122,6 +121,67 @@ func TestHandlerGET(t *testing.T) { }) } +func TestHandlerOptions(t *testing.T) { + h := GraphQL(&executableSchemaStub{}) + + resp := doRequest(h, "OPTIONS", "/graphql?query={me{name}}", ``) + assert.Equal(t, http.StatusOK, resp.Code) + assert.Equal(t, "OPTIONS, GET, POST", resp.Header().Get("Allow")) +} + +func TestHandlerHead(t *testing.T) { + h := GraphQL(&executableSchemaStub{}) + + resp := doRequest(h, "HEAD", "/graphql?query={me{name}}", ``) + assert.Equal(t, http.StatusMethodNotAllowed, resp.Code) +} + +func TestHandlerComplexity(t *testing.T) { + t.Run("static complexity", func(t *testing.T) { + h := GraphQL(&executableSchemaStub{}, ComplexityLimit(2)) + + t.Run("below complexity limit", func(t *testing.T) { + resp := doRequest(h, "POST", "/graphql", `{"query":"{ me { name } }"}`) + assert.Equal(t, http.StatusOK, resp.Code) + assert.Equal(t, `{"data":{"name":"test"}}`, resp.Body.String()) + }) + + t.Run("above complexity limit", func(t *testing.T) { + resp := doRequest(h, "POST", "/graphql", `{"query":"{ a: me { name } b: me { name } }"}`) + assert.Equal(t, http.StatusUnprocessableEntity, resp.Code) + assert.Equal(t, `{"errors":[{"message":"operation has complexity 4, which exceeds the limit of 2"}],"data":null}`, resp.Body.String()) + }) + }) + + t.Run("dynamic complexity", func(t *testing.T) { + h := GraphQL(&executableSchemaStub{}, ComplexityLimitFunc(func(ctx context.Context) int { + reqCtx := graphql.GetRequestContext(ctx) + if strings.Contains(reqCtx.RawQuery, "dummy") { + return 4 + } + return 2 + })) + + t.Run("below complexity limit", func(t *testing.T) { + resp := doRequest(h, "POST", "/graphql", `{"query":"{ me { name } }"}`) + assert.Equal(t, http.StatusOK, resp.Code) + assert.Equal(t, `{"data":{"name":"test"}}`, resp.Body.String()) + }) + + t.Run("above complexity limit", func(t *testing.T) { + resp := doRequest(h, "POST", "/graphql", `{"query":"{ a: me { name } b: me { name } }"}`) + assert.Equal(t, http.StatusUnprocessableEntity, resp.Code) + assert.Equal(t, `{"errors":[{"message":"operation has complexity 4, which exceeds the limit of 2"}],"data":null}`, resp.Body.String()) + }) + + t.Run("within dynamic complexity limit", func(t *testing.T) { + resp := doRequest(h, "POST", "/graphql", `{"query":"{ a: me { name } dummy: me { name } }"}`) + assert.Equal(t, http.StatusOK, resp.Code) + assert.Equal(t, `{"data":{"name":"test"}}`, resp.Body.String()) + }) + }) +} + func TestFileUpload(t *testing.T) { t.Run("valid single file upload", func(t *testing.T) { @@ -417,67 +477,6 @@ func TestAddUploadToOperations(t *testing.T) { }) } -func TestHandlerOptions(t *testing.T) { - h := GraphQL(&executableSchemaStub{}) - - resp := doRequest(h, "OPTIONS", "/graphql?query={me{name}}", ``) - assert.Equal(t, http.StatusOK, resp.Code) - assert.Equal(t, "OPTIONS, GET, POST", resp.Header().Get("Allow")) -} - -func TestHandlerHead(t *testing.T) { - h := GraphQL(&executableSchemaStub{}) - - resp := doRequest(h, "HEAD", "/graphql?query={me{name}}", ``) - assert.Equal(t, http.StatusMethodNotAllowed, resp.Code) -} - -func TestHandlerComplexity(t *testing.T) { - t.Run("static complexity", func(t *testing.T) { - h := GraphQL(&executableSchemaStub{}, ComplexityLimit(2)) - - t.Run("below complexity limit", func(t *testing.T) { - resp := doRequest(h, "POST", "/graphql", `{"query":"{ me { name } }"}`) - assert.Equal(t, http.StatusOK, resp.Code) - assert.Equal(t, `{"data":{"name":"test"}}`, resp.Body.String()) - }) - - t.Run("above complexity limit", func(t *testing.T) { - resp := doRequest(h, "POST", "/graphql", `{"query":"{ a: me { name } b: me { name } }"}`) - assert.Equal(t, http.StatusUnprocessableEntity, resp.Code) - assert.Equal(t, `{"errors":[{"message":"operation has complexity 4, which exceeds the limit of 2"}],"data":null}`, resp.Body.String()) - }) - }) - - t.Run("dynamic complexity", func(t *testing.T) { - h := GraphQL(&executableSchemaStub{}, ComplexityLimitFunc(func(ctx context.Context) int { - reqCtx := graphql.GetRequestContext(ctx) - if strings.Contains(reqCtx.RawQuery, "dummy") { - return 4 - } - return 2 - })) - - t.Run("below complexity limit", func(t *testing.T) { - resp := doRequest(h, "POST", "/graphql", `{"query":"{ me { name } }"}`) - assert.Equal(t, http.StatusOK, resp.Code) - assert.Equal(t, `{"data":{"name":"test"}}`, resp.Body.String()) - }) - - t.Run("above complexity limit", func(t *testing.T) { - resp := doRequest(h, "POST", "/graphql", `{"query":"{ a: me { name } b: me { name } }"}`) - assert.Equal(t, http.StatusUnprocessableEntity, resp.Code) - assert.Equal(t, `{"errors":[{"message":"operation has complexity 4, which exceeds the limit of 2"}],"data":null}`, resp.Body.String()) - }) - - t.Run("within dynamic complexity limit", func(t *testing.T) { - resp := doRequest(h, "POST", "/graphql", `{"query":"{ a: me { name } dummy: me { name } }"}`) - assert.Equal(t, http.StatusOK, resp.Code) - assert.Equal(t, `{"data":{"name":"test"}}`, resp.Body.String()) - }) - }) -} - type file struct { mapKey string name string From 662dc3372caa4aca09474eb67ff0c35b838af3f3 Mon Sep 17 00:00:00 2001 From: hantonelli Date: Sun, 31 Mar 2019 14:50:01 +0100 Subject: [PATCH 19/39] Move Upload to injected if defined in the schema as scalars --- codegen/config/config.go | 16 +- example/fileupload/generated.go | 370 +++++++++++++++----------- example/fileupload/model/generated.go | 2 + example/fileupload/schema.graphql | 10 +- 4 files changed, 223 insertions(+), 175 deletions(-) diff --git a/codegen/config/config.go b/codegen/config/config.go index e23d7e9c68..37c5bbc17a 100644 --- a/codegen/config/config.go +++ b/codegen/config/config.go @@ -347,7 +347,6 @@ func (c *Config) InjectBuiltins(s *ast.Schema) { "github.com/99designs/gqlgen/graphql.Int32", "github.com/99designs/gqlgen/graphql.Int64", }}, - "Upload": {Model: StringList{"github.com/99designs/gqlgen/graphql.Upload"}}, "ID": { Model: StringList{ "github.com/99designs/gqlgen/graphql.ID", @@ -364,8 +363,9 @@ func (c *Config) InjectBuiltins(s *ast.Schema) { // These are additional types that are injected if defined in the schema as scalars. extraBuiltins := TypeMap{ - "Time": {Model: StringList{"github.com/99designs/gqlgen/graphql.Time"}}, - "Map": {Model: StringList{"github.com/99designs/gqlgen/graphql.Map"}}, + "Time": {Model: StringList{"github.com/99designs/gqlgen/graphql.Time"}}, + "Map": {Model: StringList{"github.com/99designs/gqlgen/graphql.Map"}}, + "Upload": {Model: StringList{"github.com/99designs/gqlgen/graphql.Upload"}}, } for typeName, entry := range extraBuiltins { @@ -379,16 +379,6 @@ func (c *Config) LoadSchema() (*ast.Schema, map[string]string, error) { schemaStrings := map[string]string{} var sources []*ast.Source - - // Add upload scalar - uploadSource := &ast.Source{ - Name: "uploadScalar.graphql", - Input: "\"The `Upload` scalar type represents a multipart file upload.\"\nscalar Upload", - BuiltIn: true, - } - schemaStrings[uploadSource.Name] = uploadSource.Input - sources = append(sources, uploadSource) - for _, filename := range c.SchemaFilename { filename = filepath.ToSlash(filename) var err error diff --git a/example/fileupload/generated.go b/example/fileupload/generated.go index 0fb3ab47f8..05c1a2afc2 100644 --- a/example/fileupload/generated.go +++ b/example/fileupload/generated.go @@ -43,16 +43,16 @@ type DirectiveRoot struct { type ComplexityRoot struct { File struct { + Content func(childComplexity int) int ID func(childComplexity int) int Name func(childComplexity int) int - Content func(childComplexity int) int } Mutation struct { - SingleUpload func(childComplexity int, file graphql.Upload) int - SingleUploadWithPayload func(childComplexity int, req model.UploadFile) int MultipleUpload func(childComplexity int, files []graphql.Upload) int MultipleUploadWithPayload func(childComplexity int, req []model.UploadFile) int + SingleUpload func(childComplexity int, file graphql.Upload) int + SingleUploadWithPayload func(childComplexity int, req model.UploadFile) int } Query struct { @@ -85,6 +85,13 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in _ = ec switch typeName + "." + field { + case "File.Content": + if e.complexity.File.Content == nil { + break + } + + return e.complexity.File.Content(childComplexity), true + case "File.ID": if e.complexity.File.ID == nil { break @@ -99,60 +106,53 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.File.Name(childComplexity), true - case "File.Content": - if e.complexity.File.Content == nil { - break - } - - return e.complexity.File.Content(childComplexity), true - - case "Mutation.SingleUpload": - if e.complexity.Mutation.SingleUpload == nil { + case "Mutation.MultipleUpload": + if e.complexity.Mutation.MultipleUpload == nil { break } - args, err := ec.field_Mutation_singleUpload_args(context.TODO(), rawArgs) + args, err := ec.field_Mutation_multipleUpload_args(context.TODO(), rawArgs) if err != nil { return 0, false } - return e.complexity.Mutation.SingleUpload(childComplexity, args["file"].(graphql.Upload)), true + return e.complexity.Mutation.MultipleUpload(childComplexity, args["files"].([]graphql.Upload)), true - case "Mutation.SingleUploadWithPayload": - if e.complexity.Mutation.SingleUploadWithPayload == nil { + case "Mutation.MultipleUploadWithPayload": + if e.complexity.Mutation.MultipleUploadWithPayload == nil { break } - args, err := ec.field_Mutation_singleUploadWithPayload_args(context.TODO(), rawArgs) + args, err := ec.field_Mutation_multipleUploadWithPayload_args(context.TODO(), rawArgs) if err != nil { return 0, false } - return e.complexity.Mutation.SingleUploadWithPayload(childComplexity, args["req"].(model.UploadFile)), true + return e.complexity.Mutation.MultipleUploadWithPayload(childComplexity, args["req"].([]model.UploadFile)), true - case "Mutation.MultipleUpload": - if e.complexity.Mutation.MultipleUpload == nil { + case "Mutation.SingleUpload": + if e.complexity.Mutation.SingleUpload == nil { break } - args, err := ec.field_Mutation_multipleUpload_args(context.TODO(), rawArgs) + args, err := ec.field_Mutation_singleUpload_args(context.TODO(), rawArgs) if err != nil { return 0, false } - return e.complexity.Mutation.MultipleUpload(childComplexity, args["files"].([]graphql.Upload)), true + return e.complexity.Mutation.SingleUpload(childComplexity, args["file"].(graphql.Upload)), true - case "Mutation.MultipleUploadWithPayload": - if e.complexity.Mutation.MultipleUploadWithPayload == nil { + case "Mutation.SingleUploadWithPayload": + if e.complexity.Mutation.SingleUploadWithPayload == nil { break } - args, err := ec.field_Mutation_multipleUploadWithPayload_args(context.TODO(), rawArgs) + args, err := ec.field_Mutation_singleUploadWithPayload_args(context.TODO(), rawArgs) if err != nil { return 0, false } - return e.complexity.Mutation.MultipleUploadWithPayload(childComplexity, args["req"].([]model.UploadFile)), true + return e.complexity.Mutation.SingleUploadWithPayload(childComplexity, args["req"].(model.UploadFile)), true case "Query.Empty": if e.complexity.Query.Empty == nil { @@ -238,26 +238,28 @@ func (ec *executionContext) introspectType(name string) (*introspection.Type, er } var parsedSchema = gqlparser.MustLoadSchema( - &ast.Source{Name: "schema.graphql", Input: `# The query type, represents all of the entry points into our object graph + &ast.Source{Name: "schema.graphql", Input: `"The ` + "`" + `Upload` + "`" + ` scalar type represents a multipart file upload." +scalar Upload -# The File type, represents the response of uploading a file +"The ` + "`" + `File` + "`" + ` type, represents the response of uploading a file." type File { id: Int! name: String! content: String! } -# The UploadFile type, represents the request for uploading a file with certain payload +"The ` + "`" + `UploadFile` + "`" + ` type, represents the request for uploading a file with certain payload." input UploadFile { id: Int! file: Upload! } +"The ` + "`" + `Query` + "`" + ` type, represents all of the entry points into our object graph." type Query { empty: String! } -# The mutation type, represents all updates we can make to our data +"The ` + "`" + `Mutation` + "`" + ` type, represents all updates we can make to our data." type Mutation { singleUpload(file: Upload!): File! singleUploadWithPayload(req: UploadFile!): File! @@ -266,8 +268,6 @@ type Mutation { } `}, - &ast.Source{Name: "uploadScalar.graphql", Input: `"The ` + "`" + `Upload` + "`" + ` scalar type represents a multipart file upload." -scalar Upload`}, ) // endregion ************************** generated!.gotpl ************************** @@ -380,9 +380,10 @@ func (ec *executionContext) _File_id(ctx context.Context, field graphql.Collecte ctx = ec.Tracer.StartFieldExecution(ctx, field) defer func() { ec.Tracer.EndFieldExecution(ctx) }() rctx := &graphql.ResolverContext{ - Object: "File", - Field: field, - Args: nil, + Object: "File", + Field: field, + Args: nil, + IsMethod: false, } ctx = graphql.WithResolverContext(ctx, rctx) ctx = ec.Tracer.StartFieldResolverExecution(ctx, rctx) @@ -406,9 +407,10 @@ func (ec *executionContext) _File_name(ctx context.Context, field graphql.Collec ctx = ec.Tracer.StartFieldExecution(ctx, field) defer func() { ec.Tracer.EndFieldExecution(ctx) }() rctx := &graphql.ResolverContext{ - Object: "File", - Field: field, - Args: nil, + Object: "File", + Field: field, + Args: nil, + IsMethod: false, } ctx = graphql.WithResolverContext(ctx, rctx) ctx = ec.Tracer.StartFieldResolverExecution(ctx, rctx) @@ -432,9 +434,10 @@ func (ec *executionContext) _File_content(ctx context.Context, field graphql.Col ctx = ec.Tracer.StartFieldExecution(ctx, field) defer func() { ec.Tracer.EndFieldExecution(ctx) }() rctx := &graphql.ResolverContext{ - Object: "File", - Field: field, - Args: nil, + Object: "File", + Field: field, + Args: nil, + IsMethod: false, } ctx = graphql.WithResolverContext(ctx, rctx) ctx = ec.Tracer.StartFieldResolverExecution(ctx, rctx) @@ -458,9 +461,10 @@ func (ec *executionContext) _Mutation_singleUpload(ctx context.Context, field gr ctx = ec.Tracer.StartFieldExecution(ctx, field) defer func() { ec.Tracer.EndFieldExecution(ctx) }() rctx := &graphql.ResolverContext{ - Object: "Mutation", - Field: field, - Args: nil, + Object: "Mutation", + Field: field, + Args: nil, + IsMethod: true, } ctx = graphql.WithResolverContext(ctx, rctx) rawArgs := field.ArgumentMap(ec.Variables) @@ -491,9 +495,10 @@ func (ec *executionContext) _Mutation_singleUploadWithPayload(ctx context.Contex ctx = ec.Tracer.StartFieldExecution(ctx, field) defer func() { ec.Tracer.EndFieldExecution(ctx) }() rctx := &graphql.ResolverContext{ - Object: "Mutation", - Field: field, - Args: nil, + Object: "Mutation", + Field: field, + Args: nil, + IsMethod: true, } ctx = graphql.WithResolverContext(ctx, rctx) rawArgs := field.ArgumentMap(ec.Variables) @@ -524,9 +529,10 @@ func (ec *executionContext) _Mutation_multipleUpload(ctx context.Context, field ctx = ec.Tracer.StartFieldExecution(ctx, field) defer func() { ec.Tracer.EndFieldExecution(ctx) }() rctx := &graphql.ResolverContext{ - Object: "Mutation", - Field: field, - Args: nil, + Object: "Mutation", + Field: field, + Args: nil, + IsMethod: true, } ctx = graphql.WithResolverContext(ctx, rctx) rawArgs := field.ArgumentMap(ec.Variables) @@ -557,9 +563,10 @@ func (ec *executionContext) _Mutation_multipleUploadWithPayload(ctx context.Cont ctx = ec.Tracer.StartFieldExecution(ctx, field) defer func() { ec.Tracer.EndFieldExecution(ctx) }() rctx := &graphql.ResolverContext{ - Object: "Mutation", - Field: field, - Args: nil, + Object: "Mutation", + Field: field, + Args: nil, + IsMethod: true, } ctx = graphql.WithResolverContext(ctx, rctx) rawArgs := field.ArgumentMap(ec.Variables) @@ -590,9 +597,10 @@ func (ec *executionContext) _Query_empty(ctx context.Context, field graphql.Coll ctx = ec.Tracer.StartFieldExecution(ctx, field) defer func() { ec.Tracer.EndFieldExecution(ctx) }() rctx := &graphql.ResolverContext{ - Object: "Query", - Field: field, - Args: nil, + Object: "Query", + Field: field, + Args: nil, + IsMethod: true, } ctx = graphql.WithResolverContext(ctx, rctx) ctx = ec.Tracer.StartFieldResolverExecution(ctx, rctx) @@ -616,9 +624,10 @@ func (ec *executionContext) _Query___type(ctx context.Context, field graphql.Col ctx = ec.Tracer.StartFieldExecution(ctx, field) defer func() { ec.Tracer.EndFieldExecution(ctx) }() rctx := &graphql.ResolverContext{ - Object: "Query", - Field: field, - Args: nil, + Object: "Query", + Field: field, + Args: nil, + IsMethod: true, } ctx = graphql.WithResolverContext(ctx, rctx) rawArgs := field.ArgumentMap(ec.Variables) @@ -646,9 +655,10 @@ func (ec *executionContext) _Query___schema(ctx context.Context, field graphql.C ctx = ec.Tracer.StartFieldExecution(ctx, field) defer func() { ec.Tracer.EndFieldExecution(ctx) }() rctx := &graphql.ResolverContext{ - Object: "Query", - Field: field, - Args: nil, + Object: "Query", + Field: field, + Args: nil, + IsMethod: true, } ctx = graphql.WithResolverContext(ctx, rctx) ctx = ec.Tracer.StartFieldResolverExecution(ctx, rctx) @@ -669,9 +679,10 @@ func (ec *executionContext) ___Directive_name(ctx context.Context, field graphql ctx = ec.Tracer.StartFieldExecution(ctx, field) defer func() { ec.Tracer.EndFieldExecution(ctx) }() rctx := &graphql.ResolverContext{ - Object: "__Directive", - Field: field, - Args: nil, + Object: "__Directive", + Field: field, + Args: nil, + IsMethod: false, } ctx = graphql.WithResolverContext(ctx, rctx) ctx = ec.Tracer.StartFieldResolverExecution(ctx, rctx) @@ -695,9 +706,10 @@ func (ec *executionContext) ___Directive_description(ctx context.Context, field ctx = ec.Tracer.StartFieldExecution(ctx, field) defer func() { ec.Tracer.EndFieldExecution(ctx) }() rctx := &graphql.ResolverContext{ - Object: "__Directive", - Field: field, - Args: nil, + Object: "__Directive", + Field: field, + Args: nil, + IsMethod: false, } ctx = graphql.WithResolverContext(ctx, rctx) ctx = ec.Tracer.StartFieldResolverExecution(ctx, rctx) @@ -718,9 +730,10 @@ func (ec *executionContext) ___Directive_locations(ctx context.Context, field gr ctx = ec.Tracer.StartFieldExecution(ctx, field) defer func() { ec.Tracer.EndFieldExecution(ctx) }() rctx := &graphql.ResolverContext{ - Object: "__Directive", - Field: field, - Args: nil, + Object: "__Directive", + Field: field, + Args: nil, + IsMethod: false, } ctx = graphql.WithResolverContext(ctx, rctx) ctx = ec.Tracer.StartFieldResolverExecution(ctx, rctx) @@ -744,9 +757,10 @@ func (ec *executionContext) ___Directive_args(ctx context.Context, field graphql ctx = ec.Tracer.StartFieldExecution(ctx, field) defer func() { ec.Tracer.EndFieldExecution(ctx) }() rctx := &graphql.ResolverContext{ - Object: "__Directive", - Field: field, - Args: nil, + Object: "__Directive", + Field: field, + Args: nil, + IsMethod: false, } ctx = graphql.WithResolverContext(ctx, rctx) ctx = ec.Tracer.StartFieldResolverExecution(ctx, rctx) @@ -770,9 +784,10 @@ func (ec *executionContext) ___EnumValue_name(ctx context.Context, field graphql ctx = ec.Tracer.StartFieldExecution(ctx, field) defer func() { ec.Tracer.EndFieldExecution(ctx) }() rctx := &graphql.ResolverContext{ - Object: "__EnumValue", - Field: field, - Args: nil, + Object: "__EnumValue", + Field: field, + Args: nil, + IsMethod: false, } ctx = graphql.WithResolverContext(ctx, rctx) ctx = ec.Tracer.StartFieldResolverExecution(ctx, rctx) @@ -796,9 +811,10 @@ func (ec *executionContext) ___EnumValue_description(ctx context.Context, field ctx = ec.Tracer.StartFieldExecution(ctx, field) defer func() { ec.Tracer.EndFieldExecution(ctx) }() rctx := &graphql.ResolverContext{ - Object: "__EnumValue", - Field: field, - Args: nil, + Object: "__EnumValue", + Field: field, + Args: nil, + IsMethod: false, } ctx = graphql.WithResolverContext(ctx, rctx) ctx = ec.Tracer.StartFieldResolverExecution(ctx, rctx) @@ -819,9 +835,10 @@ func (ec *executionContext) ___EnumValue_isDeprecated(ctx context.Context, field ctx = ec.Tracer.StartFieldExecution(ctx, field) defer func() { ec.Tracer.EndFieldExecution(ctx) }() rctx := &graphql.ResolverContext{ - Object: "__EnumValue", - Field: field, - Args: nil, + Object: "__EnumValue", + Field: field, + Args: nil, + IsMethod: true, } ctx = graphql.WithResolverContext(ctx, rctx) ctx = ec.Tracer.StartFieldResolverExecution(ctx, rctx) @@ -845,9 +862,10 @@ func (ec *executionContext) ___EnumValue_deprecationReason(ctx context.Context, ctx = ec.Tracer.StartFieldExecution(ctx, field) defer func() { ec.Tracer.EndFieldExecution(ctx) }() rctx := &graphql.ResolverContext{ - Object: "__EnumValue", - Field: field, - Args: nil, + Object: "__EnumValue", + Field: field, + Args: nil, + IsMethod: true, } ctx = graphql.WithResolverContext(ctx, rctx) ctx = ec.Tracer.StartFieldResolverExecution(ctx, rctx) @@ -868,9 +886,10 @@ func (ec *executionContext) ___Field_name(ctx context.Context, field graphql.Col ctx = ec.Tracer.StartFieldExecution(ctx, field) defer func() { ec.Tracer.EndFieldExecution(ctx) }() rctx := &graphql.ResolverContext{ - Object: "__Field", - Field: field, - Args: nil, + Object: "__Field", + Field: field, + Args: nil, + IsMethod: false, } ctx = graphql.WithResolverContext(ctx, rctx) ctx = ec.Tracer.StartFieldResolverExecution(ctx, rctx) @@ -894,9 +913,10 @@ func (ec *executionContext) ___Field_description(ctx context.Context, field grap ctx = ec.Tracer.StartFieldExecution(ctx, field) defer func() { ec.Tracer.EndFieldExecution(ctx) }() rctx := &graphql.ResolverContext{ - Object: "__Field", - Field: field, - Args: nil, + Object: "__Field", + Field: field, + Args: nil, + IsMethod: false, } ctx = graphql.WithResolverContext(ctx, rctx) ctx = ec.Tracer.StartFieldResolverExecution(ctx, rctx) @@ -917,9 +937,10 @@ func (ec *executionContext) ___Field_args(ctx context.Context, field graphql.Col ctx = ec.Tracer.StartFieldExecution(ctx, field) defer func() { ec.Tracer.EndFieldExecution(ctx) }() rctx := &graphql.ResolverContext{ - Object: "__Field", - Field: field, - Args: nil, + Object: "__Field", + Field: field, + Args: nil, + IsMethod: false, } ctx = graphql.WithResolverContext(ctx, rctx) ctx = ec.Tracer.StartFieldResolverExecution(ctx, rctx) @@ -943,9 +964,10 @@ func (ec *executionContext) ___Field_type(ctx context.Context, field graphql.Col ctx = ec.Tracer.StartFieldExecution(ctx, field) defer func() { ec.Tracer.EndFieldExecution(ctx) }() rctx := &graphql.ResolverContext{ - Object: "__Field", - Field: field, - Args: nil, + Object: "__Field", + Field: field, + Args: nil, + IsMethod: false, } ctx = graphql.WithResolverContext(ctx, rctx) ctx = ec.Tracer.StartFieldResolverExecution(ctx, rctx) @@ -969,9 +991,10 @@ func (ec *executionContext) ___Field_isDeprecated(ctx context.Context, field gra ctx = ec.Tracer.StartFieldExecution(ctx, field) defer func() { ec.Tracer.EndFieldExecution(ctx) }() rctx := &graphql.ResolverContext{ - Object: "__Field", - Field: field, - Args: nil, + Object: "__Field", + Field: field, + Args: nil, + IsMethod: true, } ctx = graphql.WithResolverContext(ctx, rctx) ctx = ec.Tracer.StartFieldResolverExecution(ctx, rctx) @@ -995,9 +1018,10 @@ func (ec *executionContext) ___Field_deprecationReason(ctx context.Context, fiel ctx = ec.Tracer.StartFieldExecution(ctx, field) defer func() { ec.Tracer.EndFieldExecution(ctx) }() rctx := &graphql.ResolverContext{ - Object: "__Field", - Field: field, - Args: nil, + Object: "__Field", + Field: field, + Args: nil, + IsMethod: true, } ctx = graphql.WithResolverContext(ctx, rctx) ctx = ec.Tracer.StartFieldResolverExecution(ctx, rctx) @@ -1018,9 +1042,10 @@ func (ec *executionContext) ___InputValue_name(ctx context.Context, field graphq ctx = ec.Tracer.StartFieldExecution(ctx, field) defer func() { ec.Tracer.EndFieldExecution(ctx) }() rctx := &graphql.ResolverContext{ - Object: "__InputValue", - Field: field, - Args: nil, + Object: "__InputValue", + Field: field, + Args: nil, + IsMethod: false, } ctx = graphql.WithResolverContext(ctx, rctx) ctx = ec.Tracer.StartFieldResolverExecution(ctx, rctx) @@ -1044,9 +1069,10 @@ func (ec *executionContext) ___InputValue_description(ctx context.Context, field ctx = ec.Tracer.StartFieldExecution(ctx, field) defer func() { ec.Tracer.EndFieldExecution(ctx) }() rctx := &graphql.ResolverContext{ - Object: "__InputValue", - Field: field, - Args: nil, + Object: "__InputValue", + Field: field, + Args: nil, + IsMethod: false, } ctx = graphql.WithResolverContext(ctx, rctx) ctx = ec.Tracer.StartFieldResolverExecution(ctx, rctx) @@ -1067,9 +1093,10 @@ func (ec *executionContext) ___InputValue_type(ctx context.Context, field graphq ctx = ec.Tracer.StartFieldExecution(ctx, field) defer func() { ec.Tracer.EndFieldExecution(ctx) }() rctx := &graphql.ResolverContext{ - Object: "__InputValue", - Field: field, - Args: nil, + Object: "__InputValue", + Field: field, + Args: nil, + IsMethod: false, } ctx = graphql.WithResolverContext(ctx, rctx) ctx = ec.Tracer.StartFieldResolverExecution(ctx, rctx) @@ -1093,9 +1120,10 @@ func (ec *executionContext) ___InputValue_defaultValue(ctx context.Context, fiel ctx = ec.Tracer.StartFieldExecution(ctx, field) defer func() { ec.Tracer.EndFieldExecution(ctx) }() rctx := &graphql.ResolverContext{ - Object: "__InputValue", - Field: field, - Args: nil, + Object: "__InputValue", + Field: field, + Args: nil, + IsMethod: false, } ctx = graphql.WithResolverContext(ctx, rctx) ctx = ec.Tracer.StartFieldResolverExecution(ctx, rctx) @@ -1116,9 +1144,10 @@ func (ec *executionContext) ___Schema_types(ctx context.Context, field graphql.C ctx = ec.Tracer.StartFieldExecution(ctx, field) defer func() { ec.Tracer.EndFieldExecution(ctx) }() rctx := &graphql.ResolverContext{ - Object: "__Schema", - Field: field, - Args: nil, + Object: "__Schema", + Field: field, + Args: nil, + IsMethod: true, } ctx = graphql.WithResolverContext(ctx, rctx) ctx = ec.Tracer.StartFieldResolverExecution(ctx, rctx) @@ -1142,9 +1171,10 @@ func (ec *executionContext) ___Schema_queryType(ctx context.Context, field graph ctx = ec.Tracer.StartFieldExecution(ctx, field) defer func() { ec.Tracer.EndFieldExecution(ctx) }() rctx := &graphql.ResolverContext{ - Object: "__Schema", - Field: field, - Args: nil, + Object: "__Schema", + Field: field, + Args: nil, + IsMethod: true, } ctx = graphql.WithResolverContext(ctx, rctx) ctx = ec.Tracer.StartFieldResolverExecution(ctx, rctx) @@ -1168,9 +1198,10 @@ func (ec *executionContext) ___Schema_mutationType(ctx context.Context, field gr ctx = ec.Tracer.StartFieldExecution(ctx, field) defer func() { ec.Tracer.EndFieldExecution(ctx) }() rctx := &graphql.ResolverContext{ - Object: "__Schema", - Field: field, - Args: nil, + Object: "__Schema", + Field: field, + Args: nil, + IsMethod: true, } ctx = graphql.WithResolverContext(ctx, rctx) ctx = ec.Tracer.StartFieldResolverExecution(ctx, rctx) @@ -1191,9 +1222,10 @@ func (ec *executionContext) ___Schema_subscriptionType(ctx context.Context, fiel ctx = ec.Tracer.StartFieldExecution(ctx, field) defer func() { ec.Tracer.EndFieldExecution(ctx) }() rctx := &graphql.ResolverContext{ - Object: "__Schema", - Field: field, - Args: nil, + Object: "__Schema", + Field: field, + Args: nil, + IsMethod: true, } ctx = graphql.WithResolverContext(ctx, rctx) ctx = ec.Tracer.StartFieldResolverExecution(ctx, rctx) @@ -1214,9 +1246,10 @@ func (ec *executionContext) ___Schema_directives(ctx context.Context, field grap ctx = ec.Tracer.StartFieldExecution(ctx, field) defer func() { ec.Tracer.EndFieldExecution(ctx) }() rctx := &graphql.ResolverContext{ - Object: "__Schema", - Field: field, - Args: nil, + Object: "__Schema", + Field: field, + Args: nil, + IsMethod: true, } ctx = graphql.WithResolverContext(ctx, rctx) ctx = ec.Tracer.StartFieldResolverExecution(ctx, rctx) @@ -1240,9 +1273,10 @@ func (ec *executionContext) ___Type_kind(ctx context.Context, field graphql.Coll ctx = ec.Tracer.StartFieldExecution(ctx, field) defer func() { ec.Tracer.EndFieldExecution(ctx) }() rctx := &graphql.ResolverContext{ - Object: "__Type", - Field: field, - Args: nil, + Object: "__Type", + Field: field, + Args: nil, + IsMethod: true, } ctx = graphql.WithResolverContext(ctx, rctx) ctx = ec.Tracer.StartFieldResolverExecution(ctx, rctx) @@ -1266,9 +1300,10 @@ func (ec *executionContext) ___Type_name(ctx context.Context, field graphql.Coll ctx = ec.Tracer.StartFieldExecution(ctx, field) defer func() { ec.Tracer.EndFieldExecution(ctx) }() rctx := &graphql.ResolverContext{ - Object: "__Type", - Field: field, - Args: nil, + Object: "__Type", + Field: field, + Args: nil, + IsMethod: true, } ctx = graphql.WithResolverContext(ctx, rctx) ctx = ec.Tracer.StartFieldResolverExecution(ctx, rctx) @@ -1289,9 +1324,10 @@ func (ec *executionContext) ___Type_description(ctx context.Context, field graph ctx = ec.Tracer.StartFieldExecution(ctx, field) defer func() { ec.Tracer.EndFieldExecution(ctx) }() rctx := &graphql.ResolverContext{ - Object: "__Type", - Field: field, - Args: nil, + Object: "__Type", + Field: field, + Args: nil, + IsMethod: true, } ctx = graphql.WithResolverContext(ctx, rctx) ctx = ec.Tracer.StartFieldResolverExecution(ctx, rctx) @@ -1312,9 +1348,10 @@ func (ec *executionContext) ___Type_fields(ctx context.Context, field graphql.Co ctx = ec.Tracer.StartFieldExecution(ctx, field) defer func() { ec.Tracer.EndFieldExecution(ctx) }() rctx := &graphql.ResolverContext{ - Object: "__Type", - Field: field, - Args: nil, + Object: "__Type", + Field: field, + Args: nil, + IsMethod: true, } ctx = graphql.WithResolverContext(ctx, rctx) rawArgs := field.ArgumentMap(ec.Variables) @@ -1342,9 +1379,10 @@ func (ec *executionContext) ___Type_interfaces(ctx context.Context, field graphq ctx = ec.Tracer.StartFieldExecution(ctx, field) defer func() { ec.Tracer.EndFieldExecution(ctx) }() rctx := &graphql.ResolverContext{ - Object: "__Type", - Field: field, - Args: nil, + Object: "__Type", + Field: field, + Args: nil, + IsMethod: true, } ctx = graphql.WithResolverContext(ctx, rctx) ctx = ec.Tracer.StartFieldResolverExecution(ctx, rctx) @@ -1365,9 +1403,10 @@ func (ec *executionContext) ___Type_possibleTypes(ctx context.Context, field gra ctx = ec.Tracer.StartFieldExecution(ctx, field) defer func() { ec.Tracer.EndFieldExecution(ctx) }() rctx := &graphql.ResolverContext{ - Object: "__Type", - Field: field, - Args: nil, + Object: "__Type", + Field: field, + Args: nil, + IsMethod: true, } ctx = graphql.WithResolverContext(ctx, rctx) ctx = ec.Tracer.StartFieldResolverExecution(ctx, rctx) @@ -1388,9 +1427,10 @@ func (ec *executionContext) ___Type_enumValues(ctx context.Context, field graphq ctx = ec.Tracer.StartFieldExecution(ctx, field) defer func() { ec.Tracer.EndFieldExecution(ctx) }() rctx := &graphql.ResolverContext{ - Object: "__Type", - Field: field, - Args: nil, + Object: "__Type", + Field: field, + Args: nil, + IsMethod: true, } ctx = graphql.WithResolverContext(ctx, rctx) rawArgs := field.ArgumentMap(ec.Variables) @@ -1418,9 +1458,10 @@ func (ec *executionContext) ___Type_inputFields(ctx context.Context, field graph ctx = ec.Tracer.StartFieldExecution(ctx, field) defer func() { ec.Tracer.EndFieldExecution(ctx) }() rctx := &graphql.ResolverContext{ - Object: "__Type", - Field: field, - Args: nil, + Object: "__Type", + Field: field, + Args: nil, + IsMethod: true, } ctx = graphql.WithResolverContext(ctx, rctx) ctx = ec.Tracer.StartFieldResolverExecution(ctx, rctx) @@ -1441,9 +1482,10 @@ func (ec *executionContext) ___Type_ofType(ctx context.Context, field graphql.Co ctx = ec.Tracer.StartFieldExecution(ctx, field) defer func() { ec.Tracer.EndFieldExecution(ctx) }() rctx := &graphql.ResolverContext{ - Object: "__Type", - Field: field, - Args: nil, + Object: "__Type", + Field: field, + Args: nil, + IsMethod: true, } ctx = graphql.WithResolverContext(ctx, rctx) ctx = ec.Tracer.StartFieldResolverExecution(ctx, rctx) @@ -2265,6 +2307,9 @@ func (ec *executionContext) marshalOString2ᚖstring(ctx context.Context, sel as } func (ec *executionContext) marshalO__EnumValue2ᚕgithubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚋintrospectionᚐEnumValue(ctx context.Context, sel ast.SelectionSet, v []introspection.EnumValue) graphql.Marshaler { + if v == nil { + return graphql.Null + } ret := make(graphql.Array, len(v)) var wg sync.WaitGroup isLen1 := len(v) == 1 @@ -2302,6 +2347,9 @@ func (ec *executionContext) marshalO__EnumValue2ᚕgithubᚗcomᚋ99designsᚋgq } func (ec *executionContext) marshalO__Field2ᚕgithubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚋintrospectionᚐField(ctx context.Context, sel ast.SelectionSet, v []introspection.Field) graphql.Marshaler { + if v == nil { + return graphql.Null + } ret := make(graphql.Array, len(v)) var wg sync.WaitGroup isLen1 := len(v) == 1 @@ -2339,6 +2387,9 @@ func (ec *executionContext) marshalO__Field2ᚕgithubᚗcomᚋ99designsᚋgqlgen } func (ec *executionContext) marshalO__InputValue2ᚕgithubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚋintrospectionᚐInputValue(ctx context.Context, sel ast.SelectionSet, v []introspection.InputValue) graphql.Marshaler { + if v == nil { + return graphql.Null + } ret := make(graphql.Array, len(v)) var wg sync.WaitGroup isLen1 := len(v) == 1 @@ -2391,6 +2442,9 @@ func (ec *executionContext) marshalO__Type2githubᚗcomᚋ99designsᚋgqlgenᚋg } func (ec *executionContext) marshalO__Type2ᚕgithubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚋintrospectionᚐType(ctx context.Context, sel ast.SelectionSet, v []introspection.Type) graphql.Marshaler { + if v == nil { + return graphql.Null + } ret := make(graphql.Array, len(v)) var wg sync.WaitGroup isLen1 := len(v) == 1 diff --git a/example/fileupload/model/generated.go b/example/fileupload/model/generated.go index 88136be5e2..c311e9adfe 100644 --- a/example/fileupload/model/generated.go +++ b/example/fileupload/model/generated.go @@ -6,12 +6,14 @@ import ( "github.com/99designs/gqlgen/graphql" ) +// The `File` type, represents the response of uploading a file. type File struct { ID int `json:"id"` Name string `json:"name"` Content string `json:"content"` } +// The `UploadFile` type, represents the request for uploading a file with certain payload. type UploadFile struct { ID int `json:"id"` File graphql.Upload `json:"file"` diff --git a/example/fileupload/schema.graphql b/example/fileupload/schema.graphql index d5f3067b36..891c61ec93 100644 --- a/example/fileupload/schema.graphql +++ b/example/fileupload/schema.graphql @@ -1,23 +1,25 @@ -# The query type, represents all of the entry points into our object graph +"The `Upload` scalar type represents a multipart file upload." +scalar Upload -# The File type, represents the response of uploading a file +"The `File` type, represents the response of uploading a file." type File { id: Int! name: String! content: String! } -# The UploadFile type, represents the request for uploading a file with certain payload +"The `UploadFile` type, represents the request for uploading a file with certain payload." input UploadFile { id: Int! file: Upload! } +"The `Query` type, represents all of the entry points into our object graph." type Query { empty: String! } -# The mutation type, represents all updates we can make to our data +"The `Mutation` type, represents all updates we can make to our data." type Mutation { singleUpload(file: Upload!): File! singleUploadWithPayload(req: UploadFile!): File! From fc318364521228b1cf208fb6157e77a4d2af0ff0 Mon Sep 17 00:00:00 2001 From: hantonelli Date: Sun, 31 Mar 2019 15:07:58 +0100 Subject: [PATCH 20/39] Improve format, inline const. --- codegen/config/config.go | 3 ++- handler/graphql.go | 3 +-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/codegen/config/config.go b/codegen/config/config.go index 37c5bbc17a..1e735a6243 100644 --- a/codegen/config/config.go +++ b/codegen/config/config.go @@ -13,7 +13,7 @@ import ( "github.com/pkg/errors" "github.com/vektah/gqlparser" "github.com/vektah/gqlparser/ast" - yaml "gopkg.in/yaml.v2" + "gopkg.in/yaml.v2" ) type Config struct { @@ -379,6 +379,7 @@ func (c *Config) LoadSchema() (*ast.Schema, map[string]string, error) { schemaStrings := map[string]string{} var sources []*ast.Source + for _, filename := range c.SchemaFilename { filename = filepath.ToSlash(filename) var err error diff --git a/handler/graphql.go b/handler/graphql.go index f9a59b1ff8..5ead497c12 100644 --- a/handler/graphql.go +++ b/handler/graphql.go @@ -23,7 +23,6 @@ import ( const ( defaultMaxMemory = 32 << 20 // 32 MB - variablePrefix = "variables." ) type params struct { @@ -539,7 +538,7 @@ func processMultipart(r *http.Request, request *params) error { Size: header.Size, Filename: header.Filename, } - if len(path) != 1 || !strings.HasPrefix(path[0], variablePrefix) { + if len(path) != 1 || !strings.HasPrefix(path[0], "variables.") { return errors.New(fmt.Sprintf("invalid value for key %s", key)) } addUploadToOperations(operations, upload, path[0]) From 849d4b1eaa5bf2141ef30e9a0d850f851770dcbe Mon Sep 17 00:00:00 2001 From: hantonelli Date: Sun, 31 Mar 2019 16:03:22 +0100 Subject: [PATCH 21/39] Make uploadMaxMemory configurable --- handler/graphql.go | 26 +++++++++++++++++++------- handler/graphql_test.go | 14 +++++++------- 2 files changed, 26 insertions(+), 14 deletions(-) diff --git a/handler/graphql.go b/handler/graphql.go index 5ead497c12..bbb680a655 100644 --- a/handler/graphql.go +++ b/handler/graphql.go @@ -21,10 +21,6 @@ import ( "github.com/vektah/gqlparser/validator" ) -const ( - defaultMaxMemory = 32 << 20 // 32 MB -) - type params struct { Query string `json:"query"` OperationName string `json:"operationName"` @@ -43,6 +39,7 @@ type Config struct { complexityLimitFunc graphql.ComplexityLimitFunc disableIntrospection bool connectionKeepAlivePingInterval time.Duration + uploadMaxMemory int64 } func (c *Config) newRequestContext(es graphql.ExecutableSchema, doc *ast.QueryDocument, op *ast.OperationDefinition, query string, variables map[string]interface{}) *graphql.RequestContext { @@ -249,6 +246,15 @@ func (tw *tracerWrapper) EndOperationExecution(ctx context.Context) { tw.tracer1.EndOperationExecution(ctx) } +// UploadMaxMemory sets the total of maxMemory bytes used to parse a request body +// as multipart/form-data in memory, with the remainder stored on disk in +// temporary files. +func UploadMaxMemory(maxMemory int64) Option { + return func(cfg *Config) { + cfg.uploadMaxMemory = maxMemory + } +} + // CacheSize sets the maximum size of the query cache. // If size is less than or equal to 0, the cache is disabled. func CacheSize(size int) Option { @@ -270,9 +276,15 @@ func WebsocketKeepAliveDuration(duration time.Duration) Option { const DefaultCacheSize = 1000 const DefaultConnectionKeepAlivePingInterval = 25 * time.Second +// DefaultUploadMaxMemory sets the total of maxMemory bytes used to parse a request body +// as multipart/form-data in memory, with the remainder stored on disk in +// temporary files. The default value is 32 MB. +const DefaultUploadMaxMemory = 32 << 20 + func GraphQL(exec graphql.ExecutableSchema, options ...Option) http.HandlerFunc { cfg := &Config{ cacheSize: DefaultCacheSize, + uploadMaxMemory: DefaultUploadMaxMemory, connectionKeepAlivePingInterval: DefaultConnectionKeepAlivePingInterval, upgrader: websocket.Upgrader{ ReadBufferSize: 1024, @@ -343,7 +355,7 @@ func (gh *graphqlHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { case http.MethodPost: contentType := strings.SplitN(r.Header.Get("Content-Type"), ";", 2)[0] if contentType == "multipart/form-data" { - if err := processMultipart(r, &reqParams); err != nil { + if err := processMultipart(r, &reqParams, gh.cfg.uploadMaxMemory); err != nil { sendErrorf(w, http.StatusBadRequest, "multipart body could not be decoded: "+err.Error()) return } @@ -508,9 +520,9 @@ func sendErrorf(w http.ResponseWriter, code int, format string, args ...interfac sendError(w, code, &gqlerror.Error{Message: fmt.Sprintf(format, args...)}) } -func processMultipart(r *http.Request, request *params) error { +func processMultipart(r *http.Request, request *params, uploadMaxMemory int64) error { // Parse multipart form - if err := r.ParseMultipartForm(defaultMaxMemory); err != nil { + if err := r.ParseMultipartForm(uploadMaxMemory); err != nil { return errors.New("failed to parse multipart form") } diff --git a/handler/graphql_test.go b/handler/graphql_test.go index 254a2ede59..dfac2d90bf 100644 --- a/handler/graphql_test.go +++ b/handler/graphql_test.go @@ -321,7 +321,7 @@ func TestProcessMultipart(t *testing.T) { Body: ioutil.NopCloser(new(bytes.Buffer)), } var reqParams params - err := processMultipart(req, &reqParams) + err := processMultipart(req, &reqParams, DefaultFileMaxMemory) require.NotNil(t, err) errMsg := err.Error() require.Equal(t, errMsg, "failed to parse multipart form") @@ -332,7 +332,7 @@ func TestProcessMultipart(t *testing.T) { req := createUploadRequest(t, operations, validMap, validFiles) var reqParams params - err := processMultipart(req, &reqParams) + err := processMultipart(req, &reqParams, DefaultFileMaxMemory) require.NotNil(t, err) require.Equal(t, err.Error(), "operations form field could not be decoded") }) @@ -342,7 +342,7 @@ func TestProcessMultipart(t *testing.T) { req := createUploadRequest(t, validOperations, mapData, validFiles) var reqParams params - err := processMultipart(req, &reqParams) + err := processMultipart(req, &reqParams, DefaultFileMaxMemory) require.NotNil(t, err) require.Equal(t, err.Error(), "map form field could not be decoded") }) @@ -352,7 +352,7 @@ func TestProcessMultipart(t *testing.T) { req := createUploadRequest(t, validOperations, validMap, files) var reqParams params - err := processMultipart(req, &reqParams) + err := processMultipart(req, &reqParams, DefaultFileMaxMemory) require.NotNil(t, err) require.Equal(t, err.Error(), "failed to get key 0 from form") }) @@ -362,7 +362,7 @@ func TestProcessMultipart(t *testing.T) { req := createUploadRequest(t, validOperations, mapData, validFiles) var reqParams params - err := processMultipart(req, &reqParams) + err := processMultipart(req, &reqParams, DefaultFileMaxMemory) require.NotNil(t, err) require.Equal(t, err.Error(), "invalid value for key 0") }) @@ -372,7 +372,7 @@ func TestProcessMultipart(t *testing.T) { req := createUploadRequest(t, validOperations, mapData, validFiles) var reqParams params - err := processMultipart(req, &reqParams) + err := processMultipart(req, &reqParams, DefaultFileMaxMemory) require.NotNil(t, err) require.Equal(t, err.Error(), "invalid value for key 0") }) @@ -381,7 +381,7 @@ func TestProcessMultipart(t *testing.T) { req := createUploadRequest(t, validOperations, validMap, validFiles) var reqParams params - err := processMultipart(req, &reqParams) + err := processMultipart(req, &reqParams, DefaultFileMaxMemory) require.Nil(t, err) require.Equal(t, "mutation ($file: Upload!) { singleUpload(file: $file) { id } }", reqParams.Query) require.Equal(t, "", reqParams.OperationName) From 425849a697d475c70ba496f079d9cc5f46f457b2 Mon Sep 17 00:00:00 2001 From: hantonelli Date: Sun, 31 Mar 2019 18:02:35 +0100 Subject: [PATCH 22/39] Improve fileupload example readme. Update scalars.md. Add file-upload.md --- docs/content/reference/file-upload.md | 129 ++++++++++++++++++++++++++ docs/content/reference/scalars.md | 18 +++- example/fileupload/readme.md | 30 +++--- 3 files changed, 165 insertions(+), 12 deletions(-) create mode 100644 docs/content/reference/file-upload.md diff --git a/docs/content/reference/file-upload.md b/docs/content/reference/file-upload.md new file mode 100644 index 0000000000..93f9015394 --- /dev/null +++ b/docs/content/reference/file-upload.md @@ -0,0 +1,129 @@ +--- +title: 'File Upload' +description: How to upload files. +linkTitle: File Upload +menu: { main: { parent: 'reference' } } +--- + +Graphql server has an already build it Upload scalar to upload files using a multipart request. +If follow the following spec https://github.com/jaydenseric/graphql-multipart-request-spec, +that defines a interoperable multipart form field structure for GraphQL requests, used by +various file upload client implementations. + +To use it you add the Upload scalar in you schema, and it will automatically add the marshalling +behaviour to Go types. + +# Examples + +## Single file upload +For this use case, the schema could look like this. + +```graphql +"The `UploadFile, // b.txt` scalar type represents a multipart file upload." +scalar Upload + +"The `Query` type, represents all of the entry points into our object graph." +type Query { + ... +} + +"The `Mutation` type, represents all updates we can make to our data." +type Mutation { + singleUpload(file: Upload!): Bool! +} +``` + +cURL can be use the make a query as follows: +``` +curl localhost:4000/graphql \ + -F operations='{ "query": "mutation ($file: Upload!) { singleUpload(file: $file) { id } }", "variables": { "file": null } }' \ + -F map='{ "0": ["variables.file"] }' \ + -F 0=@a.txt +``` + +That invokes the following operation: +``` +{ + query: ` + mutation($file: Upload!) { + singleUpload(file: $file) { + id + } + } + `, + variables: { + file: File // a.txt + } +} +``` + +## Multiple file upload +For this use case, the schema could look like this. + +```graphql +"The `Upload` scalar type represents a multipart file upload." +scalar Upload + +"The `File` type, represents the response of uploading a file." +type File { + id: Int! + name: String! + content: String! +} + +"The `UploadFile` type, represents the request for uploading a file with certain payload." +input UploadFile { + id: Int! + file: Upload! +} + +"The `Query` type, represents all of the entry points into our object graph." +type Query { + ... +} + +"The `Mutation` type, represents all updates we can make to our data." +type Mutation { + multipleUpload(req: [UploadFile!]!): [File!]! +} + +``` + +cURL can be use the make a query as follows: + +``` +curl localhost:4000/query \ + -F operations='{ "query": "mutation($req: [UploadFile!]!) { multipleUpload(req: $req) { id, name, content } }", "variables": { "req": [ { "id": 1, "file": null }, { "id": 2, "file": null } ] } }' \ + -F map='{ "0": ["variables.req.0.file"], "1": ["variables.req.1.file"] }' \ + -F 0=@b.txt \ + -F 1=@c.txt +``` + +That invokes the following operation: +``` +{ + query: ` + mutation($req: [UploadFile!]!) + multipleUpload(req: $req) { + id, + name, + content + } + } + `, + variables: { + req: [ + { + id: 1, + File, // b.txt + }, + { + id: 2, + File, // c.txt + } + ] + } +} +``` + +see the [example/fileupload](https://github.com/99designs/gqlgen/tree/master/example/fileupload) package for more examples. diff --git a/docs/content/reference/scalars.md b/docs/content/reference/scalars.md index ec9a9fcf0b..7b098b0689 100644 --- a/docs/content/reference/scalars.md +++ b/docs/content/reference/scalars.md @@ -7,7 +7,7 @@ menu: { main: { parent: 'reference' } } ## Built-in helpers -gqlgen ships with two built-in helpers for common custom scalar use-cases, `Time` and `Map`. Adding either of these to a schema will automatically add the marshalling behaviour to Go types. +gqlgen ships with three built-in helpers for common custom scalar use-cases, `Time`, `Map` and `Upload`. Adding any of these to a schema will automatically add the marshalling behaviour to Go types. ### Time @@ -25,6 +25,22 @@ scalar Map Maps an arbitrary GraphQL value to a `map[string]{interface}` Go type. + +### Upload + +```graphql +scalar Upload +``` + +Maps a `Upload` GraphQL scalar to a `graphql.Upload` struct, defined as follows: +``` +type Upload struct { + File multipart.File + Filename string + Size int64 +} +``` + ## Custom scalars with user defined types For user defined types you can implement the graphql.Marshal and graphql.Unmarshal interfaces and they will be called. diff --git a/example/fileupload/readme.md b/example/fileupload/readme.md index 3a884f2817..ae0eb4d095 100644 --- a/example/fileupload/readme.md +++ b/example/fileupload/readme.md @@ -14,7 +14,7 @@ and open http://localhost:8087 in your browser #### Operations -```js +``` { query: ` mutation($file: Upload!) { @@ -61,7 +61,7 @@ Alpha file content. #### Operations -```js +``` { query: ` mutation($file: Upload!) { @@ -109,7 +109,7 @@ Alpha file content. #### Operations -```js +``` { query: ` mutation($files: [Upload!]!) { @@ -167,20 +167,28 @@ Charlie file content. #### Operations -```js +``` { query: ` - mutation($files: [Upload!]!) { - multipleUpload(files: $files) { - id + mutation($req: [UploadFile!]!) + multipleUploadWithPayload(req: $req) { + id, + name, + content } } `, variables: { - files: [ - File, // b.txt - File // c.txt - ] + req: [ + { + id: 1, + File, // b.txt + }, + { + id: 2, + File, // c.txt + } + ] } } ``` From 83cde4b69e95758e8a819cc5ab02ab9f1a3301b3 Mon Sep 17 00:00:00 2001 From: hantonelli Date: Sun, 31 Mar 2019 20:10:13 +0100 Subject: [PATCH 23/39] Fix tests. Improve code format. --- codegen/config/config.go | 2 +- handler/graphql.go | 1 - handler/graphql_test.go | 14 +++++++------- 3 files changed, 8 insertions(+), 9 deletions(-) diff --git a/codegen/config/config.go b/codegen/config/config.go index 1e735a6243..1fc8ab46d4 100644 --- a/codegen/config/config.go +++ b/codegen/config/config.go @@ -13,7 +13,7 @@ import ( "github.com/pkg/errors" "github.com/vektah/gqlparser" "github.com/vektah/gqlparser/ast" - "gopkg.in/yaml.v2" + yaml "gopkg.in/yaml.v2" ) type Config struct { diff --git a/handler/graphql.go b/handler/graphql.go index bbb680a655..bf8fa4aed3 100644 --- a/handler/graphql.go +++ b/handler/graphql.go @@ -275,7 +275,6 @@ func WebsocketKeepAliveDuration(duration time.Duration) Option { const DefaultCacheSize = 1000 const DefaultConnectionKeepAlivePingInterval = 25 * time.Second - // DefaultUploadMaxMemory sets the total of maxMemory bytes used to parse a request body // as multipart/form-data in memory, with the remainder stored on disk in // temporary files. The default value is 32 MB. diff --git a/handler/graphql_test.go b/handler/graphql_test.go index dfac2d90bf..b7c7875633 100644 --- a/handler/graphql_test.go +++ b/handler/graphql_test.go @@ -321,7 +321,7 @@ func TestProcessMultipart(t *testing.T) { Body: ioutil.NopCloser(new(bytes.Buffer)), } var reqParams params - err := processMultipart(req, &reqParams, DefaultFileMaxMemory) + err := processMultipart(req, &reqParams, DefaultUploadMaxMemory) require.NotNil(t, err) errMsg := err.Error() require.Equal(t, errMsg, "failed to parse multipart form") @@ -332,7 +332,7 @@ func TestProcessMultipart(t *testing.T) { req := createUploadRequest(t, operations, validMap, validFiles) var reqParams params - err := processMultipart(req, &reqParams, DefaultFileMaxMemory) + err := processMultipart(req, &reqParams, DefaultUploadMaxMemory) require.NotNil(t, err) require.Equal(t, err.Error(), "operations form field could not be decoded") }) @@ -342,7 +342,7 @@ func TestProcessMultipart(t *testing.T) { req := createUploadRequest(t, validOperations, mapData, validFiles) var reqParams params - err := processMultipart(req, &reqParams, DefaultFileMaxMemory) + err := processMultipart(req, &reqParams, DefaultUploadMaxMemory) require.NotNil(t, err) require.Equal(t, err.Error(), "map form field could not be decoded") }) @@ -352,7 +352,7 @@ func TestProcessMultipart(t *testing.T) { req := createUploadRequest(t, validOperations, validMap, files) var reqParams params - err := processMultipart(req, &reqParams, DefaultFileMaxMemory) + err := processMultipart(req, &reqParams, DefaultUploadMaxMemory) require.NotNil(t, err) require.Equal(t, err.Error(), "failed to get key 0 from form") }) @@ -362,7 +362,7 @@ func TestProcessMultipart(t *testing.T) { req := createUploadRequest(t, validOperations, mapData, validFiles) var reqParams params - err := processMultipart(req, &reqParams, DefaultFileMaxMemory) + err := processMultipart(req, &reqParams, DefaultUploadMaxMemory) require.NotNil(t, err) require.Equal(t, err.Error(), "invalid value for key 0") }) @@ -372,7 +372,7 @@ func TestProcessMultipart(t *testing.T) { req := createUploadRequest(t, validOperations, mapData, validFiles) var reqParams params - err := processMultipart(req, &reqParams, DefaultFileMaxMemory) + err := processMultipart(req, &reqParams, DefaultUploadMaxMemory) require.NotNil(t, err) require.Equal(t, err.Error(), "invalid value for key 0") }) @@ -381,7 +381,7 @@ func TestProcessMultipart(t *testing.T) { req := createUploadRequest(t, validOperations, validMap, validFiles) var reqParams params - err := processMultipart(req, &reqParams, DefaultFileMaxMemory) + err := processMultipart(req, &reqParams, DefaultUploadMaxMemory) require.Nil(t, err) require.Equal(t, "mutation ($file: Upload!) { singleUpload(file: $file) { id } }", reqParams.Query) require.Equal(t, "", reqParams.OperationName) From 73b3a5366cd145534014215d9c7597619d64b506 Mon Sep 17 00:00:00 2001 From: hantonelli Date: Sun, 31 Mar 2019 20:22:32 +0100 Subject: [PATCH 24/39] Fmt graphql.go --- handler/graphql.go | 1 + 1 file changed, 1 insertion(+) diff --git a/handler/graphql.go b/handler/graphql.go index bf8fa4aed3..bbb680a655 100644 --- a/handler/graphql.go +++ b/handler/graphql.go @@ -275,6 +275,7 @@ func WebsocketKeepAliveDuration(duration time.Duration) Option { const DefaultCacheSize = 1000 const DefaultConnectionKeepAlivePingInterval = 25 * time.Second + // DefaultUploadMaxMemory sets the total of maxMemory bytes used to parse a request body // as multipart/form-data in memory, with the remainder stored on disk in // temporary files. The default value is 32 MB. From 2c1f8573321521e6ca70c73f6a78192a9ca38066 Mon Sep 17 00:00:00 2001 From: hantonelli Date: Sun, 31 Mar 2019 20:42:07 +0100 Subject: [PATCH 25/39] Fix lint errors. --- handler/graphql.go | 13 ++++++++----- handler/graphql_test.go | 3 +++ handler/mock.go | 6 ++---- 3 files changed, 13 insertions(+), 9 deletions(-) diff --git a/handler/graphql.go b/handler/graphql.go index bbb680a655..faa2fdb493 100644 --- a/handler/graphql.go +++ b/handler/graphql.go @@ -543,7 +543,7 @@ func processMultipart(r *http.Request, request *params, uploadMaxMemory int64) e for key, path := range uploadsMap { file, header, err := r.FormFile(key) if err != nil { - return errors.New(fmt.Sprintf("failed to get key %s from form", key)) + return fmt.Errorf("failed to get key %s from form", key) } upload = graphql.Upload{ File: file, @@ -551,9 +551,12 @@ func processMultipart(r *http.Request, request *params, uploadMaxMemory int64) e Filename: header.Filename, } if len(path) != 1 || !strings.HasPrefix(path[0], "variables.") { - return errors.New(fmt.Sprintf("invalid value for key %s", key)) + return fmt.Errorf("invalid value for key %s", key) + } + err = addUploadToOperations(operations, upload, path[0]) + if err != nil { + return err } - addUploadToOperations(operations, upload, path[0]) } // set request variables @@ -585,7 +588,7 @@ func addUploadToOperations(operations interface{}, upload graphql.Upload, path s switch idx := p.(type) { case string: if operations == nil { - return errors.New(fmt.Sprintf("variables is missing, path: %s", path)) + return fmt.Errorf("variables is missing, path: %s", path) } if last { operations.(map[string]interface{})[idx] = upload @@ -594,7 +597,7 @@ func addUploadToOperations(operations interface{}, upload graphql.Upload, path s } case int: if operations == nil { - return errors.New(fmt.Sprintf("variables is missing, path: %s", path)) + return fmt.Errorf("variables is missing, path: %s", path) } if last { operations.([]interface{})[idx] = upload diff --git a/handler/graphql_test.go b/handler/graphql_test.go index b7c7875633..7156980219 100644 --- a/handler/graphql_test.go +++ b/handler/graphql_test.go @@ -402,6 +402,7 @@ func TestAddUploadToOperations(t *testing.T) { t.Run("fail missing all variables", func(t *testing.T) { file, err := os.Open("path/to/file") + require.Nil(t, err) var operations map[string]interface{} upload := graphql.Upload{ File: file, @@ -421,6 +422,7 @@ func TestAddUploadToOperations(t *testing.T) { "file": nil, }, } + require.Nil(t, err) upload := graphql.Upload{ File: file, @@ -443,6 +445,7 @@ func TestAddUploadToOperations(t *testing.T) { t.Run("valid nested variable", func(t *testing.T) { file, err := os.Open("path/to/file") + require.Nil(t, err) operations := map[string]interface{}{ "variables": map[string]interface{}{ "req": []interface{}{ diff --git a/handler/mock.go b/handler/mock.go index bc139d850a..3e70cf036b 100644 --- a/handler/mock.go +++ b/handler/mock.go @@ -51,9 +51,7 @@ func (e *executableSchemaMock) Mutation(ctx context.Context, op *ast.OperationDe func (e *executableSchemaMock) Subscription(ctx context.Context, op *ast.OperationDefinition) func() *graphql.Response { return func() *graphql.Response { - select { - case <-ctx.Done(): - return nil - } + <-ctx.Done() + return nil } } From d3770395a23e6ec92e06886036f1832f4ddf9af2 Mon Sep 17 00:00:00 2001 From: hantonelli Date: Sun, 31 Mar 2019 20:49:38 +0100 Subject: [PATCH 26/39] Fix tests. --- handler/graphql_test.go | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/handler/graphql_test.go b/handler/graphql_test.go index 7156980219..e87694bd0c 100644 --- a/handler/graphql_test.go +++ b/handler/graphql_test.go @@ -401,8 +401,7 @@ func TestProcessMultipart(t *testing.T) { func TestAddUploadToOperations(t *testing.T) { t.Run("fail missing all variables", func(t *testing.T) { - file, err := os.Open("path/to/file") - require.Nil(t, err) + file, _ := os.Open("path/to/file") var operations map[string]interface{} upload := graphql.Upload{ File: file, @@ -410,19 +409,18 @@ func TestAddUploadToOperations(t *testing.T) { Size: int64(5), } path := "variables.req.0.file" - err = addUploadToOperations(operations, upload, path) + err := addUploadToOperations(operations, upload, path) require.NotNil(t, err) require.Equal(t, "variables is missing, path: variables.req.0.file", err.Error()) }) t.Run("valid variable", func(t *testing.T) { - file, err := os.Open("path/to/file") + file, _ := os.Open("path/to/file") operations := map[string]interface{}{ "variables": map[string]interface{}{ "file": nil, }, } - require.Nil(t, err) upload := graphql.Upload{ File: file, @@ -437,15 +435,14 @@ func TestAddUploadToOperations(t *testing.T) { } path := "variables.file" - err = addUploadToOperations(operations, upload, path) + err := addUploadToOperations(operations, upload, path) require.Nil(t, err) require.Equal(t, operations, expected) }) t.Run("valid nested variable", func(t *testing.T) { - file, err := os.Open("path/to/file") - require.Nil(t, err) + file, _ := os.Open("path/to/file") operations := map[string]interface{}{ "variables": map[string]interface{}{ "req": []interface{}{ @@ -473,7 +470,7 @@ func TestAddUploadToOperations(t *testing.T) { } path := "variables.req.0.file" - err = addUploadToOperations(operations, upload, path) + err := addUploadToOperations(operations, upload, path) require.Nil(t, err) require.Equal(t, operations, expected) From 2cf7f452dd9ca5b4baa1399c501926a51debf59d Mon Sep 17 00:00:00 2001 From: hantonelli Date: Sat, 13 Apr 2019 19:38:29 +0100 Subject: [PATCH 27/39] Fix comments (add request size limit, remove useless comments, improve decoding and function signature, improve documentation) --- docs/content/reference/file-upload.md | 24 ++-- example/fileupload/server/server.go | 8 +- handler/graphql.go | 151 ++++++++++++++------------ handler/graphql_test.go | 70 ++++++------ 4 files changed, 141 insertions(+), 112 deletions(-) diff --git a/docs/content/reference/file-upload.md b/docs/content/reference/file-upload.md index 93f9015394..116bc4f7a4 100644 --- a/docs/content/reference/file-upload.md +++ b/docs/content/reference/file-upload.md @@ -5,13 +5,21 @@ linkTitle: File Upload menu: { main: { parent: 'reference' } } --- -Graphql server has an already build it Upload scalar to upload files using a multipart request. -If follow the following spec https://github.com/jaydenseric/graphql-multipart-request-spec, -that defines a interoperable multipart form field structure for GraphQL requests, used by +Graphql server has an already built-in Upload scalar to upload files using a multipart request. \ +It implements the following spec https://github.com/jaydenseric/graphql-multipart-request-spec, +that defines an interoperable multipart form field structure for GraphQL requests, used by various file upload client implementations. -To use it you add the Upload scalar in you schema, and it will automatically add the marshalling -behaviour to Go types. +To use it you need to add the Upload scalar in your schema, and it will automatically add the +marshalling behaviour to Go types. + +# Configuration +There are two specific options that can be configured for uploading files: +- uploadMaxSize \ +This option specifies the maximum number of bytes used to parse a request body as multipart/form-data. +- uploadMaxMemory \ +This option specifies the maximum number of bytes used to parse a request body as +multipart/form-data in memory, with the remainder stored on disk in temporary files. # Examples @@ -33,7 +41,7 @@ type Mutation { } ``` -cURL can be use the make a query as follows: +cURL can be used the make a query as follows: ``` curl localhost:4000/graphql \ -F operations='{ "query": "mutation ($file: Upload!) { singleUpload(file: $file) { id } }", "variables": { "file": null } }' \ @@ -71,7 +79,7 @@ type File { content: String! } -"The `UploadFile` type, represents the request for uploading a file with certain payload." +"The `UploadFile` type, represents the request for uploading a file with a certain payload." input UploadFile { id: Int! file: Upload! @@ -89,7 +97,7 @@ type Mutation { ``` -cURL can be use the make a query as follows: +cURL can be used the make a query as follows: ``` curl localhost:4000/query \ diff --git a/example/fileupload/server/server.go b/example/fileupload/server/server.go index c7cdf900c7..fdc72aab81 100644 --- a/example/fileupload/server/server.go +++ b/example/fileupload/server/server.go @@ -16,7 +16,13 @@ import ( func main() { http.Handle("/", handler.Playground("File Upload Demo", "/query")) resolver := getResolver() - http.Handle("/query", handler.GraphQL(fileupload.NewExecutableSchema(fileupload.Config{Resolvers: resolver}))) + exec := fileupload.NewExecutableSchema(fileupload.Config{Resolvers: resolver}) + + var mb int64 = 1 << 20 + uploadMaxMemory := handler.UploadMaxMemory(32 * mb) + uploadMaxSize := handler.UploadMaxSize(50 * mb) + + http.Handle("/query", handler.GraphQL(exec, uploadMaxMemory, uploadMaxSize)) log.Print("connect to http://localhost:8087/ for GraphQL playground") log.Fatal(http.ListenAndServe(":8087", nil)) } diff --git a/handler/graphql.go b/handler/graphql.go index faa2fdb493..2e52f1d9be 100644 --- a/handler/graphql.go +++ b/handler/graphql.go @@ -40,6 +40,7 @@ type Config struct { disableIntrospection bool connectionKeepAlivePingInterval time.Duration uploadMaxMemory int64 + uploadMaxSize int64 } func (c *Config) newRequestContext(es graphql.ExecutableSchema, doc *ast.QueryDocument, op *ast.OperationDefinition, query string, variables map[string]interface{}) *graphql.RequestContext { @@ -246,15 +247,6 @@ func (tw *tracerWrapper) EndOperationExecution(ctx context.Context) { tw.tracer1.EndOperationExecution(ctx) } -// UploadMaxMemory sets the total of maxMemory bytes used to parse a request body -// as multipart/form-data in memory, with the remainder stored on disk in -// temporary files. -func UploadMaxMemory(maxMemory int64) Option { - return func(cfg *Config) { - cfg.uploadMaxMemory = maxMemory - } -} - // CacheSize sets the maximum size of the query cache. // If size is less than or equal to 0, the cache is disabled. func CacheSize(size int) Option { @@ -263,6 +255,23 @@ func CacheSize(size int) Option { } } +// UploadMaxSize sets the maximum number of bytes used to parse a request body +// as multipart/form-data. +func UploadMaxSize(size int64) Option { + return func(cfg *Config) { + cfg.uploadMaxSize = size + } +} + +// UploadMaxMemory sets the maximum number of bytes used to parse a request body +// as multipart/form-data in memory, with the remainder stored on disk in +// temporary files. +func UploadMaxMemory(size int64) Option { + return func(cfg *Config) { + cfg.uploadMaxMemory = size + } +} + // WebsocketKeepAliveDuration allows you to reconfigure the keepalive behavior. // By default, keepalive is enabled with a DefaultConnectionKeepAlivePingInterval // duration. Set handler.connectionKeepAlivePingInterval = 0 to disable keepalive @@ -276,15 +285,20 @@ func WebsocketKeepAliveDuration(duration time.Duration) Option { const DefaultCacheSize = 1000 const DefaultConnectionKeepAlivePingInterval = 25 * time.Second -// DefaultUploadMaxMemory sets the total of maxMemory bytes used to parse a request body +// DefaultUploadMaxMemory is the maximum number of bytes used to parse a request body // as multipart/form-data in memory, with the remainder stored on disk in -// temporary files. The default value is 32 MB. +// temporary files. const DefaultUploadMaxMemory = 32 << 20 +// DefaultUploadMaxSize is maximum number of bytes used to parse a request body +// as multipart/form-data. +const DefaultUploadMaxSize = 32 << 20 + func GraphQL(exec graphql.ExecutableSchema, options ...Option) http.HandlerFunc { cfg := &Config{ cacheSize: DefaultCacheSize, uploadMaxMemory: DefaultUploadMaxMemory, + uploadMaxSize: DefaultUploadMaxSize, connectionKeepAlivePingInterval: DefaultConnectionKeepAlivePingInterval, upgrader: websocket.Upgrader{ ReadBufferSize: 1024, @@ -355,7 +369,7 @@ func (gh *graphqlHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { case http.MethodPost: contentType := strings.SplitN(r.Header.Get("Content-Type"), ";", 2)[0] if contentType == "multipart/form-data" { - if err := processMultipart(r, &reqParams, gh.cfg.uploadMaxMemory); err != nil { + if err := processMultipart(w, r, &reqParams, gh.cfg.uploadMaxSize, gh.cfg.uploadMaxMemory); err != nil { sendErrorf(w, http.StatusBadRequest, "multipart body could not be decoded: "+err.Error()) return } @@ -520,91 +534,86 @@ func sendErrorf(w http.ResponseWriter, code int, format string, args ...interfac sendError(w, code, &gqlerror.Error{Message: fmt.Sprintf(format, args...)}) } -func processMultipart(r *http.Request, request *params, uploadMaxMemory int64) error { - // Parse multipart form - if err := r.ParseMultipartForm(uploadMaxMemory); err != nil { +func processMultipart(w http.ResponseWriter, r *http.Request, request *params, uploadMaxSize, uploadMaxMemory int64) error { + var err error + if r.ContentLength > uploadMaxSize { + return errors.New("failed to parse multipart form, request body too large") + } + r.Body = http.MaxBytesReader(w, r.Body, uploadMaxSize) + if err = r.ParseMultipartForm(uploadMaxMemory); err != nil { + if strings.Contains(err.Error(), "request body too large") { + return errors.New("failed to parse multipart form, request body too large") + } return errors.New("failed to parse multipart form") } - // Unmarshal operations - var operations map[string]interface{} - if err := jsonDecode(strings.NewReader(r.Form.Get("operations")), &operations); err != nil { + if err = jsonDecode(strings.NewReader(r.Form.Get("operations")), &request); err != nil { return errors.New("operations form field could not be decoded") } - // Unmarshal map var uploadsMap = map[string][]string{} - if err := json.Unmarshal([]byte(r.Form.Get("map")), &uploadsMap); err != nil { + if err = json.Unmarshal([]byte(r.Form.Get("map")), &uploadsMap); err != nil { return errors.New("map form field could not be decoded") } - // Read form files and add them to operations variables var upload graphql.Upload - for key, path := range uploadsMap { - file, header, err := r.FormFile(key) - if err != nil { - return fmt.Errorf("failed to get key %s from form", key) - } - upload = graphql.Upload{ - File: file, - Size: header.Size, - Filename: header.Filename, - } - if len(path) != 1 || !strings.HasPrefix(path[0], "variables.") { - return fmt.Errorf("invalid value for key %s", key) - } - err = addUploadToOperations(operations, upload, path[0]) + for key, paths := range uploadsMap { + err = func() error { + file, header, err := r.FormFile(key) + if err != nil { + return fmt.Errorf("failed to get key %s from form", key) + } + defer file.Close() + if len(paths) == 0 { + return fmt.Errorf("invalid empty operations paths list for key %s", key) + } + upload = graphql.Upload{ + File: file, + Size: header.Size, + Filename: header.Filename, + } + for _, path := range paths { + if !strings.HasPrefix(path, "variables.") { + return fmt.Errorf("invalid operations paths for key %s", key) + } + err = addUploadToOperations(request, upload, path) + if err != nil { + return err + } + } + return nil + }() if err != nil { return err } } - - // set request variables - if value, ok := operations["operationName"]; ok && value != nil { - request.OperationName = value.(string) - } - if value, ok := operations["query"]; ok && value != nil { - request.Query = value.(string) - } - if value, ok := operations["variables"]; ok && value != nil { - request.Variables = value.(map[string]interface{}) - } - return nil } -func addUploadToOperations(operations interface{}, upload graphql.Upload, path string) error { - var parts []interface{} - for _, p := range strings.Split(path, ".") { - index, parseNbrErr := strconv.Atoi(p) - if parseNbrErr == nil { - parts = append(parts, index) - } else { - parts = append(parts, p) +func addUploadToOperations(request *params, upload graphql.Upload, path string) error { + var ptr interface{} = request.Variables + parts := strings.Split(path, ".") + + // skip the first part (variables) because we started there + for i, p := range parts[1:] { + last := i == len(parts)-2 + if ptr == nil { + return fmt.Errorf("variables is missing, path: %s", path) } - } - for i, p := range parts { - last := i == len(parts)-1 - switch idx := p.(type) { - case string: - if operations == nil { - return fmt.Errorf("variables is missing, path: %s", path) - } + if index, parseNbrErr := strconv.Atoi(p); parseNbrErr == nil { if last { - operations.(map[string]interface{})[idx] = upload + ptr.([]interface{})[index] = upload } else { - operations = operations.(map[string]interface{})[idx] - } - case int: - if operations == nil { - return fmt.Errorf("variables is missing, path: %s", path) + ptr = ptr.([]interface{})[index] } + } else { if last { - operations.([]interface{})[idx] = upload + ptr.(map[string]interface{})[p] = upload } else { - operations = operations.([]interface{})[idx] + ptr = ptr.(map[string]interface{})[p] } } } + return nil } diff --git a/handler/graphql_test.go b/handler/graphql_test.go index e87694bd0c..cb9b1788e3 100644 --- a/handler/graphql_test.go +++ b/handler/graphql_test.go @@ -321,10 +321,10 @@ func TestProcessMultipart(t *testing.T) { Body: ioutil.NopCloser(new(bytes.Buffer)), } var reqParams params - err := processMultipart(req, &reqParams, DefaultUploadMaxMemory) + w := httptest.NewRecorder() + err := processMultipart(w, req, &reqParams, DefaultUploadMaxSize, DefaultUploadMaxMemory) require.NotNil(t, err) - errMsg := err.Error() - require.Equal(t, errMsg, "failed to parse multipart form") + require.Equal(t, err.Error(), "failed to parse multipart form") }) t.Run("fail parse operation", func(t *testing.T) { @@ -332,7 +332,8 @@ func TestProcessMultipart(t *testing.T) { req := createUploadRequest(t, operations, validMap, validFiles) var reqParams params - err := processMultipart(req, &reqParams, DefaultUploadMaxMemory) + w := httptest.NewRecorder() + err := processMultipart(w, req, &reqParams, DefaultUploadMaxSize, DefaultUploadMaxMemory) require.NotNil(t, err) require.Equal(t, err.Error(), "operations form field could not be decoded") }) @@ -342,7 +343,8 @@ func TestProcessMultipart(t *testing.T) { req := createUploadRequest(t, validOperations, mapData, validFiles) var reqParams params - err := processMultipart(req, &reqParams, DefaultUploadMaxMemory) + w := httptest.NewRecorder() + err := processMultipart(w, req, &reqParams, DefaultUploadMaxSize, DefaultUploadMaxMemory) require.NotNil(t, err) require.Equal(t, err.Error(), "map form field could not be decoded") }) @@ -352,36 +354,40 @@ func TestProcessMultipart(t *testing.T) { req := createUploadRequest(t, validOperations, validMap, files) var reqParams params - err := processMultipart(req, &reqParams, DefaultUploadMaxMemory) + w := httptest.NewRecorder() + err := processMultipart(w, req, &reqParams, DefaultUploadMaxSize, DefaultUploadMaxMemory) require.NotNil(t, err) require.Equal(t, err.Error(), "failed to get key 0 from form") }) - t.Run("fail map entry with two values", func(t *testing.T) { - mapData := `{ "0": ["variables.file", "variables.file"] }` + t.Run("fail map entry with invalid operations paths prefix", func(t *testing.T) { + mapData := `{ "0": ["var.file"] }` req := createUploadRequest(t, validOperations, mapData, validFiles) var reqParams params - err := processMultipart(req, &reqParams, DefaultUploadMaxMemory) + w := httptest.NewRecorder() + err := processMultipart(w, req, &reqParams, DefaultUploadMaxSize, DefaultUploadMaxMemory) require.NotNil(t, err) - require.Equal(t, err.Error(), "invalid value for key 0") + require.Equal(t, err.Error(), "invalid operations paths for key 0") }) - t.Run("fail map entry with invalid prefix", func(t *testing.T) { - mapData := `{ "0": ["var.file"] }` - req := createUploadRequest(t, validOperations, mapData, validFiles) + t.Run("fail parse request big body", func(t *testing.T) { + req := createUploadRequest(t, validOperations, validMap, validFiles) var reqParams params - err := processMultipart(req, &reqParams, DefaultUploadMaxMemory) + w := httptest.NewRecorder() + var smallMaxSize int64 = 2 + err := processMultipart(w, req, &reqParams, smallMaxSize, DefaultUploadMaxMemory) require.NotNil(t, err) - require.Equal(t, err.Error(), "invalid value for key 0") + require.Equal(t, err.Error(), "failed to parse multipart form, request body too large") }) t.Run("valid request", func(t *testing.T) { req := createUploadRequest(t, validOperations, validMap, validFiles) var reqParams params - err := processMultipart(req, &reqParams, DefaultUploadMaxMemory) + w := httptest.NewRecorder() + err := processMultipart(w, req, &reqParams, DefaultUploadMaxSize, DefaultUploadMaxMemory) require.Nil(t, err) require.Equal(t, "mutation ($file: Upload!) { singleUpload(file: $file) { id } }", reqParams.Query) require.Equal(t, "", reqParams.OperationName) @@ -391,8 +397,7 @@ func TestProcessMultipart(t *testing.T) { require.True(t, ok) require.Equal(t, "a.txt", reqParamsFile.Filename) require.Equal(t, int64(len("test1")), reqParamsFile.Size) - var content []byte - content, err = ioutil.ReadAll(reqParamsFile.File) + content, err := ioutil.ReadAll(reqParamsFile.File) require.Nil(t, err) require.Equal(t, "test1", string(content)) }) @@ -402,22 +407,23 @@ func TestAddUploadToOperations(t *testing.T) { t.Run("fail missing all variables", func(t *testing.T) { file, _ := os.Open("path/to/file") - var operations map[string]interface{} + request := ¶ms{} + upload := graphql.Upload{ File: file, Filename: "a.txt", Size: int64(5), } path := "variables.req.0.file" - err := addUploadToOperations(operations, upload, path) + err := addUploadToOperations(request, upload, path) require.NotNil(t, err) require.Equal(t, "variables is missing, path: variables.req.0.file", err.Error()) }) t.Run("valid variable", func(t *testing.T) { file, _ := os.Open("path/to/file") - operations := map[string]interface{}{ - "variables": map[string]interface{}{ + request := ¶ms{ + Variables: map[string]interface{}{ "file": nil, }, } @@ -428,23 +434,23 @@ func TestAddUploadToOperations(t *testing.T) { Size: int64(5), } - expected := map[string]interface{}{ - "variables": map[string]interface{}{ + expected := ¶ms{ + Variables: map[string]interface{}{ "file": upload, }, } path := "variables.file" - err := addUploadToOperations(operations, upload, path) + err := addUploadToOperations(request, upload, path) require.Nil(t, err) - require.Equal(t, operations, expected) + require.Equal(t, request, expected) }) t.Run("valid nested variable", func(t *testing.T) { file, _ := os.Open("path/to/file") - operations := map[string]interface{}{ - "variables": map[string]interface{}{ + request := ¶ms{ + Variables: map[string]interface{}{ "req": []interface{}{ map[string]interface{}{ "file": nil, @@ -459,8 +465,8 @@ func TestAddUploadToOperations(t *testing.T) { Size: int64(5), } - expected := map[string]interface{}{ - "variables": map[string]interface{}{ + expected := ¶ms{ + Variables: map[string]interface{}{ "req": []interface{}{ map[string]interface{}{ "file": upload, @@ -470,10 +476,10 @@ func TestAddUploadToOperations(t *testing.T) { } path := "variables.req.0.file" - err := addUploadToOperations(operations, upload, path) + err := addUploadToOperations(request, upload, path) require.Nil(t, err) - require.Equal(t, operations, expected) + require.Equal(t, request, expected) }) } From da52e810cf98ffe9a57cb0a8ecf68df97f6ac6f7 Mon Sep 17 00:00:00 2001 From: hantonelli Date: Mon, 15 Apr 2019 22:26:35 +0100 Subject: [PATCH 28/39] Extend test and don't close form file. --- example/fileupload/fileupload_test.go | 51 +++++++++++++++++++++ handler/graphql.go | 1 - handler/graphql_test.go | 65 +++++++++++++++++++++++++++ 3 files changed, 116 insertions(+), 1 deletion(-) diff --git a/example/fileupload/fileupload_test.go b/example/fileupload/fileupload_test.go index 6ada462a15..e91029375f 100644 --- a/example/fileupload/fileupload_test.go +++ b/example/fileupload/fileupload_test.go @@ -208,6 +208,57 @@ func TestFileUpload(t *testing.T) { require.Nil(t, err) require.Equal(t, `{"data":{"multipleUploadWithPayload":[{"id":1,"name":"a.txt","content":"test1"},{"id":2,"name":"b.txt","content":"test2"}]}}`, string(responseBody)) }) + + t.Run("valid file list upload with payload and file reuse", func(t *testing.T) { + resolver := &Resolver{ + MultipleUploadWithPayloadFunc: func(ctx context.Context, req []model.UploadFile) ([]model.File, error) { + require.Len(t, req, 2) + var ids []int + var contents []string + var resp []model.File + for i := range req { + require.NotNil(t, req[i].File) + require.NotNil(t, req[i].File.File) + ids = append(ids, req[i].ID) + req[i].File.File.Seek(0, 0) + content, err := ioutil.ReadAll(req[i].File.File) + require.Nil(t, err) + contents = append(contents, string(content)) + resp = append(resp, model.File{ + ID: i + 1, + Name: req[i].File.Filename, + Content: string(content), + }) + } + require.ElementsMatch(t, []int{1, 2}, ids) + require.ElementsMatch(t, []string{"test1", "test1"}, contents) + return resp, nil + }, + } + srv := httptest.NewServer(handler.GraphQL(NewExecutableSchema(Config{Resolvers: resolver}), handler.UploadMaxMemory(2))) + defer srv.Close() + + operations := `{ "query": "mutation($req: [UploadFile!]!) { multipleUploadWithPayload(req: $req) { id, name, content } }", "variables": { "req": [ { "id": 1, "file": null }, { "id": 2, "file": null } ] } }` + mapData := `{ "0": ["variables.req.0.file", "variables.req.1.file"] }` + files := []file{ + { + mapKey: "0", + name: "a.txt", + content: "test1", + }, + } + req := createUploadRequest(t, srv.URL, operations, mapData, files) + + resp, err := client.Do(req) + require.Nil(t, err) + defer func() { + _ = resp.Body.Close() + }() + require.Equal(t, http.StatusOK, resp.StatusCode) + responseBody, err := ioutil.ReadAll(resp.Body) + require.Nil(t, err) + require.Equal(t, `{"data":{"multipleUploadWithPayload":[{"id":1,"name":"a.txt","content":"test1"},{"id":2,"name":"a.txt","content":"test1"}]}}`, string(responseBody)) + }) } type file struct { diff --git a/handler/graphql.go b/handler/graphql.go index 2e52f1d9be..e34329a8f0 100644 --- a/handler/graphql.go +++ b/handler/graphql.go @@ -563,7 +563,6 @@ func processMultipart(w http.ResponseWriter, r *http.Request, request *params, u if err != nil { return fmt.Errorf("failed to get key %s from form", key) } - defer file.Close() if len(paths) == 0 { return fmt.Errorf("invalid empty operations paths list for key %s", key) } diff --git a/handler/graphql_test.go b/handler/graphql_test.go index cb9b1788e3..fd353cff1b 100644 --- a/handler/graphql_test.go +++ b/handler/graphql_test.go @@ -3,6 +3,7 @@ package handler import ( "bytes" "context" + "fmt" "io/ioutil" "mime/multipart" "net/http" @@ -301,6 +302,33 @@ func TestFileUpload(t *testing.T) { require.Equal(t, http.StatusOK, resp.Code) require.Equal(t, `{"data":{"multipleUploadWithPayload":[{"id":1},{"id":2}]}}`, resp.Body.String()) }) + + t.Run("valid file list upload with payload and file reuse", func(t *testing.T) { + mock := &executableSchemaMock{ + MutationFunc: func(ctx context.Context, op *ast.OperationDefinition) *graphql.Response { + require.Equal(t, len(op.VariableDefinitions), 1) + require.Equal(t, op.VariableDefinitions[0].Variable, "req") + return &graphql.Response{Data: []byte(`{"multipleUploadWithPayload":[{"id":1},{"id":2}]}`)} + }, + } + handler := GraphQL(mock) + + operations := `{ "query": "mutation($req: [UploadFile!]!) { multipleUploadWithPayload(req: $req) { id } }", "variables": { "req": [ { "id": 1, "file": null }, { "id": 2, "file": null } ] } }` + mapData := `{ "0": ["variables.req.0.file", "variables.req.1.file"] }` + files := []file{ + { + mapKey: "0", + name: "a.txt", + content: "test1", + }, + } + req := createUploadRequest(t, operations, mapData, files) + + resp := httptest.NewRecorder() + handler.ServeHTTP(resp, req) + require.Equal(t, http.StatusOK, resp.Code) + require.Equal(t, `{"data":{"multipleUploadWithPayload":[{"id":1},{"id":2}]}}`, resp.Body.String()) + }) } func TestProcessMultipart(t *testing.T) { @@ -401,6 +429,43 @@ func TestProcessMultipart(t *testing.T) { require.Nil(t, err) require.Equal(t, "test1", string(content)) }) + + t.Run("valid request with two values", func(t *testing.T) { + operations := `{ "query": "mutation($req: [UploadFile!]!) { multipleUploadWithPayload(req: $req) { id } }", "variables": { "req": [ { "id": 1, "file": null }, { "id": 2, "file": null } ] } }` + mapData := `{ "0": ["variables.req.0.file", "variables.req.1.file"] }` + files := []file{ + { + mapKey: "0", + name: "a.txt", + content: "test1", + }, + } + req := createUploadRequest(t, operations, mapData, files) + + var reqParams params + w := httptest.NewRecorder() + err := processMultipart(w, req, &reqParams, DefaultUploadMaxSize, 2) + require.Nil(t, err) + require.Equal(t, "mutation($req: [UploadFile!]!) { multipleUploadWithPayload(req: $req) { id } }", reqParams.Query) + require.Equal(t, "", reqParams.OperationName) + require.Equal(t, 1, len(reqParams.Variables)) + require.NotNil(t, reqParams.Variables["req"]) + reqParamsFile, ok := reqParams.Variables["req"].([]interface{}) + require.True(t, ok) + require.Equal(t, 2, len(reqParamsFile)) + for i, item := range reqParamsFile { + itemMap := item.(map[string]interface{}) + require.Equal(t, fmt.Sprint(itemMap["id"]), fmt.Sprint(i+1)) + file := itemMap["file"].(graphql.Upload) + require.Equal(t, "a.txt", file.Filename) + require.Equal(t, int64(len("test1")), file.Size) + _, err = file.File.Seek(0, 0) + require.Nil(t, err) + content, err := ioutil.ReadAll(file.File) + require.Nil(t, err) + require.Equal(t, "test1", string(content)) + } + }) } func TestAddUploadToOperations(t *testing.T) { From 7ade7c21d83d37397bd0fa42dc7bfadcdd492fa9 Mon Sep 17 00:00:00 2001 From: hantonelli Date: Mon, 15 Apr 2019 22:47:51 +0100 Subject: [PATCH 29/39] Change graphql.Upload File field to FileData. --- docs/content/reference/scalars.md | 2 +- example/fileupload/fileupload_test.go | 41 ++++++++++----------------- example/fileupload/server/server.go | 25 +++------------- graphql/upload.go | 5 ++-- handler/graphql.go | 8 +++++- handler/graphql_test.go | 18 ++++-------- 6 files changed, 34 insertions(+), 65 deletions(-) diff --git a/docs/content/reference/scalars.md b/docs/content/reference/scalars.md index 7b098b0689..2cf127f295 100644 --- a/docs/content/reference/scalars.md +++ b/docs/content/reference/scalars.md @@ -35,7 +35,7 @@ scalar Upload Maps a `Upload` GraphQL scalar to a `graphql.Upload` struct, defined as follows: ``` type Upload struct { - File multipart.File + FileData []byte Filename string Size int64 } diff --git a/example/fileupload/fileupload_test.go b/example/fileupload/fileupload_test.go index e91029375f..3ad3444a5f 100644 --- a/example/fileupload/fileupload_test.go +++ b/example/fileupload/fileupload_test.go @@ -23,15 +23,13 @@ func TestFileUpload(t *testing.T) { resolver := &Resolver{ SingleUploadFunc: func(ctx context.Context, file graphql.Upload) (*model.File, error) { require.NotNil(t, file) - require.NotNil(t, file.File) - content, err := ioutil.ReadAll(file.File) - require.Nil(t, err) - require.Equal(t, string(content), "test") + require.NotNil(t, file.FileData) + require.Equal(t, string(file.FileData), "test") return &model.File{ ID: 1, Name: file.Filename, - Content: string(content), + Content: string(file.FileData), }, nil }, } @@ -66,15 +64,13 @@ func TestFileUpload(t *testing.T) { SingleUploadWithPayloadFunc: func(ctx context.Context, req model.UploadFile) (*model.File, error) { require.Equal(t, req.ID, 1) require.NotNil(t, req.File) - require.NotNil(t, req.File.File) - content, err := ioutil.ReadAll(req.File.File) - require.Nil(t, err) - require.Equal(t, string(content), "test") + require.NotNil(t, req.File.FileData) + require.Equal(t, string(req.File.FileData), "test") return &model.File{ ID: 1, Name: req.File.Filename, - Content: string(content), + Content: string(req.File.FileData), }, nil }, } @@ -110,14 +106,12 @@ func TestFileUpload(t *testing.T) { var contents []string var resp []model.File for i := range files { - require.NotNil(t, files[i].File) - content, err := ioutil.ReadAll(files[i].File) - require.Nil(t, err) - contents = append(contents, string(content)) + require.NotNil(t, files[i].FileData) + contents = append(contents, string(files[i].FileData)) resp = append(resp, model.File{ ID: i + 1, Name: files[i].Filename, - Content: string(content), + Content: string(files[i].FileData), }) } require.ElementsMatch(t, []string{"test1", "test2"}, contents) @@ -163,15 +157,13 @@ func TestFileUpload(t *testing.T) { var resp []model.File for i := range req { require.NotNil(t, req[i].File) - require.NotNil(t, req[i].File.File) - content, err := ioutil.ReadAll(req[i].File.File) - require.Nil(t, err) + require.NotNil(t, req[i].File.FileData) ids = append(ids, req[i].ID) - contents = append(contents, string(content)) + contents = append(contents, string(req[i].File.FileData)) resp = append(resp, model.File{ ID: i + 1, Name: req[i].File.Filename, - Content: string(content), + Content: string(req[i].File.FileData), }) } require.ElementsMatch(t, []int{1, 2}, ids) @@ -218,16 +210,13 @@ func TestFileUpload(t *testing.T) { var resp []model.File for i := range req { require.NotNil(t, req[i].File) - require.NotNil(t, req[i].File.File) + require.NotNil(t, req[i].File.FileData) ids = append(ids, req[i].ID) - req[i].File.File.Seek(0, 0) - content, err := ioutil.ReadAll(req[i].File.File) - require.Nil(t, err) - contents = append(contents, string(content)) + contents = append(contents, string(req[i].File.FileData)) resp = append(resp, model.File{ ID: i + 1, Name: req[i].File.Filename, - Content: string(content), + Content: string(req[i].File.FileData), }) } require.ElementsMatch(t, []int{1, 2}, ids) diff --git a/example/fileupload/server/server.go b/example/fileupload/server/server.go index fdc72aab81..9f7888f474 100644 --- a/example/fileupload/server/server.go +++ b/example/fileupload/server/server.go @@ -3,7 +3,6 @@ package main import ( "context" "errors" - "io/ioutil" "log" "net/http" @@ -30,25 +29,17 @@ func main() { func getResolver() *fileupload.Resolver { resolver := &fileupload.Resolver{ SingleUploadFunc: func(ctx context.Context, file graphql.Upload) (*model.File, error) { - content, err := ioutil.ReadAll(file.File) - if err != nil { - return nil, err - } return &model.File{ ID: 1, Name: file.Filename, - Content: string(content), + Content: string(file.FileData), }, nil }, SingleUploadWithPayloadFunc: func(ctx context.Context, req model.UploadFile) (*model.File, error) { - content, err := ioutil.ReadAll(req.File.File) - if err != nil { - return nil, err - } return &model.File{ ID: 1, Name: req.File.Filename, - Content: string(content), + Content: string(req.File.FileData), }, nil }, MultipleUploadFunc: func(ctx context.Context, files []graphql.Upload) ([]model.File, error) { @@ -57,14 +48,10 @@ func getResolver() *fileupload.Resolver { } var resp []model.File for i := range files { - content, err := ioutil.ReadAll(files[i].File) - if err != nil { - return []model.File{}, err - } resp = append(resp, model.File{ ID: i + 1, Name: files[i].Filename, - Content: string(content), + Content: string(files[i].FileData), }) } return resp, nil @@ -75,14 +62,10 @@ func getResolver() *fileupload.Resolver { } var resp []model.File for i := range req { - content, err := ioutil.ReadAll(req[i].File.File) - if err != nil { - return []model.File{}, err - } resp = append(resp, model.File{ ID: i + 1, Name: req[i].File.Filename, - Content: string(content), + Content: string(req[i].File.FileData), }) } return resp, nil diff --git a/graphql/upload.go b/graphql/upload.go index e7919f1b41..1b01bc6891 100644 --- a/graphql/upload.go +++ b/graphql/upload.go @@ -3,18 +3,17 @@ package graphql import ( "fmt" "io" - "mime/multipart" ) type Upload struct { - File multipart.File + FileData []byte Filename string Size int64 } func MarshalUpload(f Upload) Marshaler { return WriterFunc(func(w io.Writer) { - io.Copy(w, f.File) + w.Write(f.FileData) }) } diff --git a/handler/graphql.go b/handler/graphql.go index e34329a8f0..db128f11d3 100644 --- a/handler/graphql.go +++ b/handler/graphql.go @@ -6,6 +6,7 @@ import ( "errors" "fmt" "io" + "io/ioutil" "net/http" "strconv" "strings" @@ -563,11 +564,16 @@ func processMultipart(w http.ResponseWriter, r *http.Request, request *params, u if err != nil { return fmt.Errorf("failed to get key %s from form", key) } + defer file.Close() if len(paths) == 0 { return fmt.Errorf("invalid empty operations paths list for key %s", key) } + fileData, err := ioutil.ReadAll(file) + if err != nil { + return fmt.Errorf("failed to read file for key %s", key) + } upload = graphql.Upload{ - File: file, + FileData: fileData, Size: header.Size, Filename: header.Filename, } diff --git a/handler/graphql_test.go b/handler/graphql_test.go index fd353cff1b..50c9bd74b8 100644 --- a/handler/graphql_test.go +++ b/handler/graphql_test.go @@ -8,7 +8,6 @@ import ( "mime/multipart" "net/http" "net/http/httptest" - "os" "strings" "testing" @@ -425,9 +424,8 @@ func TestProcessMultipart(t *testing.T) { require.True(t, ok) require.Equal(t, "a.txt", reqParamsFile.Filename) require.Equal(t, int64(len("test1")), reqParamsFile.Size) - content, err := ioutil.ReadAll(reqParamsFile.File) require.Nil(t, err) - require.Equal(t, "test1", string(content)) + require.Equal(t, "test1", string(reqParamsFile.FileData)) }) t.Run("valid request with two values", func(t *testing.T) { @@ -459,11 +457,8 @@ func TestProcessMultipart(t *testing.T) { file := itemMap["file"].(graphql.Upload) require.Equal(t, "a.txt", file.Filename) require.Equal(t, int64(len("test1")), file.Size) - _, err = file.File.Seek(0, 0) require.Nil(t, err) - content, err := ioutil.ReadAll(file.File) - require.Nil(t, err) - require.Equal(t, "test1", string(content)) + require.Equal(t, "test1", string(file.FileData)) } }) } @@ -471,11 +466,10 @@ func TestProcessMultipart(t *testing.T) { func TestAddUploadToOperations(t *testing.T) { t.Run("fail missing all variables", func(t *testing.T) { - file, _ := os.Open("path/to/file") request := ¶ms{} upload := graphql.Upload{ - File: file, + FileData: []byte{}, Filename: "a.txt", Size: int64(5), } @@ -486,7 +480,6 @@ func TestAddUploadToOperations(t *testing.T) { }) t.Run("valid variable", func(t *testing.T) { - file, _ := os.Open("path/to/file") request := ¶ms{ Variables: map[string]interface{}{ "file": nil, @@ -494,7 +487,7 @@ func TestAddUploadToOperations(t *testing.T) { } upload := graphql.Upload{ - File: file, + FileData: []byte{}, Filename: "a.txt", Size: int64(5), } @@ -513,7 +506,6 @@ func TestAddUploadToOperations(t *testing.T) { }) t.Run("valid nested variable", func(t *testing.T) { - file, _ := os.Open("path/to/file") request := ¶ms{ Variables: map[string]interface{}{ "req": []interface{}{ @@ -525,7 +517,7 @@ func TestAddUploadToOperations(t *testing.T) { } upload := graphql.Upload{ - File: file, + FileData: []byte{}, Filename: "a.txt", Size: int64(5), } From 0306783ed6c9eeb371f5775a33cfc505a9961351 Mon Sep 17 00:00:00 2001 From: hantonelli Date: Thu, 18 Apr 2019 19:22:36 +0100 Subject: [PATCH 30/39] Revert "Change graphql.Upload File field to FileData." This reverts commit 7ade7c2 --- docs/content/reference/scalars.md | 2 +- example/fileupload/fileupload_test.go | 41 +++++++++++++++++---------- example/fileupload/server/server.go | 25 +++++++++++++--- graphql/upload.go | 5 ++-- handler/graphql.go | 8 +----- handler/graphql_test.go | 18 ++++++++---- 6 files changed, 65 insertions(+), 34 deletions(-) diff --git a/docs/content/reference/scalars.md b/docs/content/reference/scalars.md index 2cf127f295..7b098b0689 100644 --- a/docs/content/reference/scalars.md +++ b/docs/content/reference/scalars.md @@ -35,7 +35,7 @@ scalar Upload Maps a `Upload` GraphQL scalar to a `graphql.Upload` struct, defined as follows: ``` type Upload struct { - FileData []byte + File multipart.File Filename string Size int64 } diff --git a/example/fileupload/fileupload_test.go b/example/fileupload/fileupload_test.go index 3ad3444a5f..e91029375f 100644 --- a/example/fileupload/fileupload_test.go +++ b/example/fileupload/fileupload_test.go @@ -23,13 +23,15 @@ func TestFileUpload(t *testing.T) { resolver := &Resolver{ SingleUploadFunc: func(ctx context.Context, file graphql.Upload) (*model.File, error) { require.NotNil(t, file) - require.NotNil(t, file.FileData) - require.Equal(t, string(file.FileData), "test") + require.NotNil(t, file.File) + content, err := ioutil.ReadAll(file.File) + require.Nil(t, err) + require.Equal(t, string(content), "test") return &model.File{ ID: 1, Name: file.Filename, - Content: string(file.FileData), + Content: string(content), }, nil }, } @@ -64,13 +66,15 @@ func TestFileUpload(t *testing.T) { SingleUploadWithPayloadFunc: func(ctx context.Context, req model.UploadFile) (*model.File, error) { require.Equal(t, req.ID, 1) require.NotNil(t, req.File) - require.NotNil(t, req.File.FileData) - require.Equal(t, string(req.File.FileData), "test") + require.NotNil(t, req.File.File) + content, err := ioutil.ReadAll(req.File.File) + require.Nil(t, err) + require.Equal(t, string(content), "test") return &model.File{ ID: 1, Name: req.File.Filename, - Content: string(req.File.FileData), + Content: string(content), }, nil }, } @@ -106,12 +110,14 @@ func TestFileUpload(t *testing.T) { var contents []string var resp []model.File for i := range files { - require.NotNil(t, files[i].FileData) - contents = append(contents, string(files[i].FileData)) + require.NotNil(t, files[i].File) + content, err := ioutil.ReadAll(files[i].File) + require.Nil(t, err) + contents = append(contents, string(content)) resp = append(resp, model.File{ ID: i + 1, Name: files[i].Filename, - Content: string(files[i].FileData), + Content: string(content), }) } require.ElementsMatch(t, []string{"test1", "test2"}, contents) @@ -157,13 +163,15 @@ func TestFileUpload(t *testing.T) { var resp []model.File for i := range req { require.NotNil(t, req[i].File) - require.NotNil(t, req[i].File.FileData) + require.NotNil(t, req[i].File.File) + content, err := ioutil.ReadAll(req[i].File.File) + require.Nil(t, err) ids = append(ids, req[i].ID) - contents = append(contents, string(req[i].File.FileData)) + contents = append(contents, string(content)) resp = append(resp, model.File{ ID: i + 1, Name: req[i].File.Filename, - Content: string(req[i].File.FileData), + Content: string(content), }) } require.ElementsMatch(t, []int{1, 2}, ids) @@ -210,13 +218,16 @@ func TestFileUpload(t *testing.T) { var resp []model.File for i := range req { require.NotNil(t, req[i].File) - require.NotNil(t, req[i].File.FileData) + require.NotNil(t, req[i].File.File) ids = append(ids, req[i].ID) - contents = append(contents, string(req[i].File.FileData)) + req[i].File.File.Seek(0, 0) + content, err := ioutil.ReadAll(req[i].File.File) + require.Nil(t, err) + contents = append(contents, string(content)) resp = append(resp, model.File{ ID: i + 1, Name: req[i].File.Filename, - Content: string(req[i].File.FileData), + Content: string(content), }) } require.ElementsMatch(t, []int{1, 2}, ids) diff --git a/example/fileupload/server/server.go b/example/fileupload/server/server.go index 9f7888f474..fdc72aab81 100644 --- a/example/fileupload/server/server.go +++ b/example/fileupload/server/server.go @@ -3,6 +3,7 @@ package main import ( "context" "errors" + "io/ioutil" "log" "net/http" @@ -29,17 +30,25 @@ func main() { func getResolver() *fileupload.Resolver { resolver := &fileupload.Resolver{ SingleUploadFunc: func(ctx context.Context, file graphql.Upload) (*model.File, error) { + content, err := ioutil.ReadAll(file.File) + if err != nil { + return nil, err + } return &model.File{ ID: 1, Name: file.Filename, - Content: string(file.FileData), + Content: string(content), }, nil }, SingleUploadWithPayloadFunc: func(ctx context.Context, req model.UploadFile) (*model.File, error) { + content, err := ioutil.ReadAll(req.File.File) + if err != nil { + return nil, err + } return &model.File{ ID: 1, Name: req.File.Filename, - Content: string(req.File.FileData), + Content: string(content), }, nil }, MultipleUploadFunc: func(ctx context.Context, files []graphql.Upload) ([]model.File, error) { @@ -48,10 +57,14 @@ func getResolver() *fileupload.Resolver { } var resp []model.File for i := range files { + content, err := ioutil.ReadAll(files[i].File) + if err != nil { + return []model.File{}, err + } resp = append(resp, model.File{ ID: i + 1, Name: files[i].Filename, - Content: string(files[i].FileData), + Content: string(content), }) } return resp, nil @@ -62,10 +75,14 @@ func getResolver() *fileupload.Resolver { } var resp []model.File for i := range req { + content, err := ioutil.ReadAll(req[i].File.File) + if err != nil { + return []model.File{}, err + } resp = append(resp, model.File{ ID: i + 1, Name: req[i].File.Filename, - Content: string(req[i].File.FileData), + Content: string(content), }) } return resp, nil diff --git a/graphql/upload.go b/graphql/upload.go index 1b01bc6891..e7919f1b41 100644 --- a/graphql/upload.go +++ b/graphql/upload.go @@ -3,17 +3,18 @@ package graphql import ( "fmt" "io" + "mime/multipart" ) type Upload struct { - FileData []byte + File multipart.File Filename string Size int64 } func MarshalUpload(f Upload) Marshaler { return WriterFunc(func(w io.Writer) { - w.Write(f.FileData) + io.Copy(w, f.File) }) } diff --git a/handler/graphql.go b/handler/graphql.go index db128f11d3..e34329a8f0 100644 --- a/handler/graphql.go +++ b/handler/graphql.go @@ -6,7 +6,6 @@ import ( "errors" "fmt" "io" - "io/ioutil" "net/http" "strconv" "strings" @@ -564,16 +563,11 @@ func processMultipart(w http.ResponseWriter, r *http.Request, request *params, u if err != nil { return fmt.Errorf("failed to get key %s from form", key) } - defer file.Close() if len(paths) == 0 { return fmt.Errorf("invalid empty operations paths list for key %s", key) } - fileData, err := ioutil.ReadAll(file) - if err != nil { - return fmt.Errorf("failed to read file for key %s", key) - } upload = graphql.Upload{ - FileData: fileData, + File: file, Size: header.Size, Filename: header.Filename, } diff --git a/handler/graphql_test.go b/handler/graphql_test.go index 50c9bd74b8..fd353cff1b 100644 --- a/handler/graphql_test.go +++ b/handler/graphql_test.go @@ -8,6 +8,7 @@ import ( "mime/multipart" "net/http" "net/http/httptest" + "os" "strings" "testing" @@ -424,8 +425,9 @@ func TestProcessMultipart(t *testing.T) { require.True(t, ok) require.Equal(t, "a.txt", reqParamsFile.Filename) require.Equal(t, int64(len("test1")), reqParamsFile.Size) + content, err := ioutil.ReadAll(reqParamsFile.File) require.Nil(t, err) - require.Equal(t, "test1", string(reqParamsFile.FileData)) + require.Equal(t, "test1", string(content)) }) t.Run("valid request with two values", func(t *testing.T) { @@ -457,8 +459,11 @@ func TestProcessMultipart(t *testing.T) { file := itemMap["file"].(graphql.Upload) require.Equal(t, "a.txt", file.Filename) require.Equal(t, int64(len("test1")), file.Size) + _, err = file.File.Seek(0, 0) require.Nil(t, err) - require.Equal(t, "test1", string(file.FileData)) + content, err := ioutil.ReadAll(file.File) + require.Nil(t, err) + require.Equal(t, "test1", string(content)) } }) } @@ -466,10 +471,11 @@ func TestProcessMultipart(t *testing.T) { func TestAddUploadToOperations(t *testing.T) { t.Run("fail missing all variables", func(t *testing.T) { + file, _ := os.Open("path/to/file") request := ¶ms{} upload := graphql.Upload{ - FileData: []byte{}, + File: file, Filename: "a.txt", Size: int64(5), } @@ -480,6 +486,7 @@ func TestAddUploadToOperations(t *testing.T) { }) t.Run("valid variable", func(t *testing.T) { + file, _ := os.Open("path/to/file") request := ¶ms{ Variables: map[string]interface{}{ "file": nil, @@ -487,7 +494,7 @@ func TestAddUploadToOperations(t *testing.T) { } upload := graphql.Upload{ - FileData: []byte{}, + File: file, Filename: "a.txt", Size: int64(5), } @@ -506,6 +513,7 @@ func TestAddUploadToOperations(t *testing.T) { }) t.Run("valid nested variable", func(t *testing.T) { + file, _ := os.Open("path/to/file") request := ¶ms{ Variables: map[string]interface{}{ "req": []interface{}{ @@ -517,7 +525,7 @@ func TestAddUploadToOperations(t *testing.T) { } upload := graphql.Upload{ - FileData: []byte{}, + File: file, Filename: "a.txt", Size: int64(5), } From f8484159adde6204e727c3edc09bb7ca4e264029 Mon Sep 17 00:00:00 2001 From: hantonelli Date: Fri, 19 Apr 2019 11:46:44 +0100 Subject: [PATCH 31/39] Modify graphql.Upload to use io.ReadCloser. Change the way upload files are managed. --- docs/content/reference/scalars.md | 2 +- example/fileupload/fileupload_test.go | 55 +++++---- graphql/upload.go | 3 +- handler/graphql.go | 104 ++++++++++++---- handler/graphql_test.go | 165 +++++++++++++++++--------- 5 files changed, 226 insertions(+), 103 deletions(-) diff --git a/docs/content/reference/scalars.md b/docs/content/reference/scalars.md index 7b098b0689..36f035b9f3 100644 --- a/docs/content/reference/scalars.md +++ b/docs/content/reference/scalars.md @@ -35,7 +35,7 @@ scalar Upload Maps a `Upload` GraphQL scalar to a `graphql.Upload` struct, defined as follows: ``` type Upload struct { - File multipart.File + File io.ReadCloser Filename string Size int64 } diff --git a/example/fileupload/fileupload_test.go b/example/fileupload/fileupload_test.go index e91029375f..c0d735aaa1 100644 --- a/example/fileupload/fileupload_test.go +++ b/example/fileupload/fileupload_test.go @@ -51,14 +51,13 @@ func TestFileUpload(t *testing.T) { resp, err := client.Do(req) require.Nil(t, err) - defer func() { - _ = resp.Body.Close() - }() require.Equal(t, http.StatusOK, resp.StatusCode) responseBody, err := ioutil.ReadAll(resp.Body) require.Nil(t, err) responseString := string(responseBody) require.Equal(t, `{"data":{"singleUpload":{"id":1,"name":"a.txt","content":"test"}}}`, responseString) + err = resp.Body.Close() + require.Nil(t, err) }) t.Run("valid single file upload with payload", func(t *testing.T) { @@ -94,13 +93,12 @@ func TestFileUpload(t *testing.T) { resp, err := client.Do(req) require.Nil(t, err) - defer func() { - _ = resp.Body.Close() - }() require.Equal(t, http.StatusOK, resp.StatusCode) responseBody, err := ioutil.ReadAll(resp.Body) require.Nil(t, err) require.Equal(t, `{"data":{"singleUploadWithPayload":{"id":1,"name":"a.txt","content":"test"}}}`, string(responseBody)) + err = resp.Body.Close() + require.Nil(t, err) }) t.Run("valid file list upload", func(t *testing.T) { @@ -145,13 +143,12 @@ func TestFileUpload(t *testing.T) { resp, err := client.Do(req) require.Nil(t, err) - defer func() { - _ = resp.Body.Close() - }() require.Equal(t, http.StatusOK, resp.StatusCode) responseBody, err := ioutil.ReadAll(resp.Body) require.Nil(t, err) require.Equal(t, `{"data":{"multipleUpload":[{"id":1,"name":"a.txt","content":"test1"},{"id":2,"name":"b.txt","content":"test2"}]}}`, string(responseBody)) + err = resp.Body.Close() + require.Nil(t, err) }) t.Run("valid file list upload with payload", func(t *testing.T) { @@ -200,13 +197,12 @@ func TestFileUpload(t *testing.T) { resp, err := client.Do(req) require.Nil(t, err) - defer func() { - _ = resp.Body.Close() - }() require.Equal(t, http.StatusOK, resp.StatusCode) responseBody, err := ioutil.ReadAll(resp.Body) require.Nil(t, err) require.Equal(t, `{"data":{"multipleUploadWithPayload":[{"id":1,"name":"a.txt","content":"test1"},{"id":2,"name":"b.txt","content":"test2"}]}}`, string(responseBody)) + err = resp.Body.Close() + require.Nil(t, err) }) t.Run("valid file list upload with payload and file reuse", func(t *testing.T) { @@ -220,7 +216,6 @@ func TestFileUpload(t *testing.T) { require.NotNil(t, req[i].File) require.NotNil(t, req[i].File.File) ids = append(ids, req[i].ID) - req[i].File.File.Seek(0, 0) content, err := ioutil.ReadAll(req[i].File.File) require.Nil(t, err) contents = append(contents, string(content)) @@ -235,8 +230,6 @@ func TestFileUpload(t *testing.T) { return resp, nil }, } - srv := httptest.NewServer(handler.GraphQL(NewExecutableSchema(Config{Resolvers: resolver}), handler.UploadMaxMemory(2))) - defer srv.Close() operations := `{ "query": "mutation($req: [UploadFile!]!) { multipleUploadWithPayload(req: $req) { id, name, content } }", "variables": { "req": [ { "id": 1, "file": null }, { "id": 2, "file": null } ] } }` mapData := `{ "0": ["variables.req.0.file", "variables.req.1.file"] }` @@ -247,17 +240,29 @@ func TestFileUpload(t *testing.T) { content: "test1", }, } - req := createUploadRequest(t, srv.URL, operations, mapData, files) - resp, err := client.Do(req) - require.Nil(t, err) - defer func() { - _ = resp.Body.Close() - }() - require.Equal(t, http.StatusOK, resp.StatusCode) - responseBody, err := ioutil.ReadAll(resp.Body) - require.Nil(t, err) - require.Equal(t, `{"data":{"multipleUploadWithPayload":[{"id":1,"name":"a.txt","content":"test1"},{"id":2,"name":"a.txt","content":"test1"}]}}`, string(responseBody)) + test := func(uploadMaxMemory int64) { + memory := handler.UploadMaxMemory(uploadMaxMemory) + srv := httptest.NewServer(handler.GraphQL(NewExecutableSchema(Config{Resolvers: resolver}), memory)) + defer srv.Close() + req := createUploadRequest(t, srv.URL, operations, mapData, files) + resp, err := client.Do(req) + require.Nil(t, err) + require.Equal(t, http.StatusOK, resp.StatusCode) + responseBody, err := ioutil.ReadAll(resp.Body) + require.Nil(t, err) + require.Equal(t, `{"data":{"multipleUploadWithPayload":[{"id":1,"name":"a.txt","content":"test1"},{"id":2,"name":"a.txt","content":"test1"}]}}`, string(responseBody)) + err = resp.Body.Close() + require.Nil(t, err) + } + + t.Run("payload smaller than UploadMaxMemory, stored in memory", func(t *testing.T){ + test(5000) + }) + + t.Run("payload bigger than UploadMaxMemory, persisted to disk", func(t *testing.T){ + test(2) + }) }) } diff --git a/graphql/upload.go b/graphql/upload.go index e7919f1b41..22d6103149 100644 --- a/graphql/upload.go +++ b/graphql/upload.go @@ -3,11 +3,10 @@ package graphql import ( "fmt" "io" - "mime/multipart" ) type Upload struct { - File multipart.File + File io.Reader Filename string Size int64 } diff --git a/handler/graphql.go b/handler/graphql.go index e34329a8f0..df99efc2cf 100644 --- a/handler/graphql.go +++ b/handler/graphql.go @@ -1,12 +1,15 @@ package handler import ( + "bytes" "context" "encoding/json" "errors" "fmt" "io" + "io/ioutil" "net/http" + "os" "strconv" "strings" "time" @@ -369,7 +372,17 @@ func (gh *graphqlHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { case http.MethodPost: contentType := strings.SplitN(r.Header.Get("Content-Type"), ";", 2)[0] if contentType == "multipart/form-data" { - if err := processMultipart(w, r, &reqParams, gh.cfg.uploadMaxSize, gh.cfg.uploadMaxMemory); err != nil { + var closers []io.Closer + var tmpFiles []string + defer func() { + for i := len(closers) - 1; 0 <= i; i-- { + _ = closers[i].Close() + } + for _, tmpFile := range tmpFiles { + _ = os.Remove(tmpFile) + } + }() + if err := processMultipart(w, r, &reqParams, &closers, &tmpFiles, gh.cfg.uploadMaxSize, gh.cfg.uploadMaxMemory); err != nil { sendErrorf(w, http.StatusBadRequest, "multipart body could not be decoded: "+err.Error()) return } @@ -534,7 +547,7 @@ func sendErrorf(w http.ResponseWriter, code int, format string, args ...interfac sendError(w, code, &gqlerror.Error{Message: fmt.Sprintf(format, args...)}) } -func processMultipart(w http.ResponseWriter, r *http.Request, request *params, uploadMaxSize, uploadMaxMemory int64) error { +func processMultipart(w http.ResponseWriter, r *http.Request, request *params, closers *[]io.Closer, tmpFiles *[]string, uploadMaxSize, uploadMaxMemory int64) error { var err error if r.ContentLength > uploadMaxSize { return errors.New("failed to parse multipart form, request body too large") @@ -546,6 +559,7 @@ func processMultipart(w http.ResponseWriter, r *http.Request, request *params, u } return errors.New("failed to parse multipart form") } + *closers = append(*closers, r.Body) if err = jsonDecode(strings.NewReader(r.Form.Get("operations")), &request); err != nil { return errors.New("operations form field could not be decoded") @@ -558,38 +572,86 @@ func processMultipart(w http.ResponseWriter, r *http.Request, request *params, u var upload graphql.Upload for key, paths := range uploadsMap { - err = func() error { - file, header, err := r.FormFile(key) - if err != nil { - return fmt.Errorf("failed to get key %s from form", key) - } - if len(paths) == 0 { - return fmt.Errorf("invalid empty operations paths list for key %s", key) - } + if len(paths) == 0 { + return fmt.Errorf("invalid empty operations paths list for key %s", key) + } + file, header, err := r.FormFile(key) + if err != nil { + return fmt.Errorf("failed to get key %s from form", key) + } + *closers = append(*closers, file) + + if len(paths) == 1 { upload = graphql.Upload{ File: file, Size: header.Size, Filename: header.Filename, } - for _, path := range paths { - if !strings.HasPrefix(path, "variables.") { - return fmt.Errorf("invalid operations paths for key %s", key) + err = addUploadToOperations(request, upload, key, paths[0]) + if err != nil { + return err + } + } else { + if r.ContentLength < uploadMaxMemory { + fileContent, err := ioutil.ReadAll(file) + if err != nil { + return fmt.Errorf("failed to read file for key %s", key) + } + for _, path := range paths { + upload = graphql.Upload{ + File: ioutil.NopCloser(bytes.NewReader(fileContent)), + Size: header.Size, + Filename: header.Filename, + } + err = addUploadToOperations(request, upload, key, path) + if err != nil { + return err + } + } + } else { + tmpFile, err := ioutil.TempFile(os.TempDir(), "gqlgen-") + if err != nil { + return fmt.Errorf("failed to create temp file for key %s", key) } - err = addUploadToOperations(request, upload, path) + tmpName := tmpFile.Name() + *tmpFiles = append(*tmpFiles, tmpName) + _, err = io.Copy(tmpFile, file) if err != nil { - return err + if err := tmpFile.Close(); err != nil { + return fmt.Errorf("failed to copy to temp file and close temp file for key %s", key) + } + return fmt.Errorf("failed to copy to temp file for key %s", key) + } + if err := tmpFile.Close(); err != nil { + return fmt.Errorf("failed to close temp file for key %s", key) + } + for _, path := range paths { + pathTmpFile, err := os.Open(tmpName) + if err != nil { + return fmt.Errorf("failed to open temp file for key %s", key) + } + *closers = append(*closers, pathTmpFile) + upload = graphql.Upload{ + File: pathTmpFile, + Size: header.Size, + Filename: header.Filename, + } + err = addUploadToOperations(request, upload, key, path) + if err != nil { + return err + } } } - return nil - }() - if err != nil { - return err } } return nil } -func addUploadToOperations(request *params, upload graphql.Upload, path string) error { +func addUploadToOperations(request *params, upload graphql.Upload, key, path string) error { + if !strings.HasPrefix(path, "variables.") { + return fmt.Errorf("invalid operations paths for key %s", key) + } + var ptr interface{} = request.Variables parts := strings.Split(path, ".") @@ -597,7 +659,7 @@ func addUploadToOperations(request *params, upload graphql.Upload, path string) for i, p := range parts[1:] { last := i == len(parts)-2 if ptr == nil { - return fmt.Errorf("variables is missing, path: %s", path) + return fmt.Errorf("path is missing \"variables.\" prefix, key: %s, path: %s", key, path) } if index, parseNbrErr := strconv.Atoi(p); parseNbrErr == nil { if last { diff --git a/handler/graphql_test.go b/handler/graphql_test.go index fd353cff1b..82fd1606e2 100644 --- a/handler/graphql_test.go +++ b/handler/graphql_test.go @@ -4,6 +4,7 @@ import ( "bytes" "context" "fmt" + "io" "io/ioutil" "mime/multipart" "net/http" @@ -304,30 +305,41 @@ func TestFileUpload(t *testing.T) { }) t.Run("valid file list upload with payload and file reuse", func(t *testing.T) { - mock := &executableSchemaMock{ - MutationFunc: func(ctx context.Context, op *ast.OperationDefinition) *graphql.Response { - require.Equal(t, len(op.VariableDefinitions), 1) - require.Equal(t, op.VariableDefinitions[0].Variable, "req") - return &graphql.Response{Data: []byte(`{"multipleUploadWithPayload":[{"id":1},{"id":2}]}`)} - }, - } - handler := GraphQL(mock) + test := func (uploadMaxMemory int64) { + mock := &executableSchemaMock{ + MutationFunc: func(ctx context.Context, op *ast.OperationDefinition) *graphql.Response { + require.Equal(t, len(op.VariableDefinitions), 1) + require.Equal(t, op.VariableDefinitions[0].Variable, "req") + return &graphql.Response{Data: []byte(`{"multipleUploadWithPayload":[{"id":1},{"id":2}]}`)} + }, + } + maxMemory := UploadMaxMemory(5000) + handler := GraphQL(mock, maxMemory) + + operations := `{ "query": "mutation($req: [UploadFile!]!) { multipleUploadWithPayload(req: $req) { id } }", "variables": { "req": [ { "id": 1, "file": null }, { "id": 2, "file": null } ] } }` + mapData := `{ "0": ["variables.req.0.file", "variables.req.1.file"] }` + files := []file{ + { + mapKey: "0", + name: "a.txt", + content: "test1", + }, + } + req := createUploadRequest(t, operations, mapData, files) - operations := `{ "query": "mutation($req: [UploadFile!]!) { multipleUploadWithPayload(req: $req) { id } }", "variables": { "req": [ { "id": 1, "file": null }, { "id": 2, "file": null } ] } }` - mapData := `{ "0": ["variables.req.0.file", "variables.req.1.file"] }` - files := []file{ - { - mapKey: "0", - name: "a.txt", - content: "test1", - }, + resp := httptest.NewRecorder() + handler.ServeHTTP(resp, req) + require.Equal(t, http.StatusOK, resp.Code) + require.Equal(t, `{"data":{"multipleUploadWithPayload":[{"id":1},{"id":2}]}}`, resp.Body.String()) } - req := createUploadRequest(t, operations, mapData, files) - resp := httptest.NewRecorder() - handler.ServeHTTP(resp, req) - require.Equal(t, http.StatusOK, resp.Code) - require.Equal(t, `{"data":{"multipleUploadWithPayload":[{"id":1},{"id":2}]}}`, resp.Body.String()) + t.Run("payload smaller than UploadMaxMemory, stored in memory", func(t *testing.T){ + test(5000) + }) + + t.Run("payload bigger than UploadMaxMemory, persisted to disk", func(t *testing.T){ + test(2) + }) }) } @@ -342,6 +354,17 @@ func TestProcessMultipart(t *testing.T) { }, } + cleanUp := func(t *testing.T, closers []io.Closer, tmpFiles []string) { + for i := len(closers) - 1; 0 <= i; i-- { + err := closers[i].Close() + require.Nil(t, err) + } + for _, tmpFiles := range tmpFiles { + err := os.Remove(tmpFiles) + require.Nil(t, err) + } + } + t.Run("fail to parse multipart", func(t *testing.T) { req := &http.Request{ Method: "POST", @@ -349,10 +372,13 @@ func TestProcessMultipart(t *testing.T) { Body: ioutil.NopCloser(new(bytes.Buffer)), } var reqParams params + var closers []io.Closer + var tmpFiles []string w := httptest.NewRecorder() - err := processMultipart(w, req, &reqParams, DefaultUploadMaxSize, DefaultUploadMaxMemory) + err := processMultipart(w, req, &reqParams, &closers, &tmpFiles, DefaultUploadMaxSize, DefaultUploadMaxMemory) require.NotNil(t, err) require.Equal(t, err.Error(), "failed to parse multipart form") + cleanUp(t, closers, tmpFiles) }) t.Run("fail parse operation", func(t *testing.T) { @@ -360,10 +386,13 @@ func TestProcessMultipart(t *testing.T) { req := createUploadRequest(t, operations, validMap, validFiles) var reqParams params + var closers []io.Closer + var tmpFiles []string w := httptest.NewRecorder() - err := processMultipart(w, req, &reqParams, DefaultUploadMaxSize, DefaultUploadMaxMemory) + err := processMultipart(w, req, &reqParams, &closers, &tmpFiles, DefaultUploadMaxSize, DefaultUploadMaxMemory) require.NotNil(t, err) require.Equal(t, err.Error(), "operations form field could not be decoded") + cleanUp(t, closers, tmpFiles) }) t.Run("fail parse map", func(t *testing.T) { @@ -371,10 +400,13 @@ func TestProcessMultipart(t *testing.T) { req := createUploadRequest(t, validOperations, mapData, validFiles) var reqParams params + var closers []io.Closer + var tmpFiles []string w := httptest.NewRecorder() - err := processMultipart(w, req, &reqParams, DefaultUploadMaxSize, DefaultUploadMaxMemory) + err := processMultipart(w, req, &reqParams, &closers, &tmpFiles, DefaultUploadMaxSize, DefaultUploadMaxMemory) require.NotNil(t, err) require.Equal(t, err.Error(), "map form field could not be decoded") + cleanUp(t, closers, tmpFiles) }) t.Run("fail missing file", func(t *testing.T) { @@ -382,10 +414,13 @@ func TestProcessMultipart(t *testing.T) { req := createUploadRequest(t, validOperations, validMap, files) var reqParams params + var closers []io.Closer + var tmpFiles []string w := httptest.NewRecorder() - err := processMultipart(w, req, &reqParams, DefaultUploadMaxSize, DefaultUploadMaxMemory) + err := processMultipart(w, req, &reqParams, &closers, &tmpFiles, DefaultUploadMaxSize, DefaultUploadMaxMemory) require.NotNil(t, err) require.Equal(t, err.Error(), "failed to get key 0 from form") + cleanUp(t, closers, tmpFiles) }) t.Run("fail map entry with invalid operations paths prefix", func(t *testing.T) { @@ -393,29 +428,37 @@ func TestProcessMultipart(t *testing.T) { req := createUploadRequest(t, validOperations, mapData, validFiles) var reqParams params + var closers []io.Closer + var tmpFiles []string w := httptest.NewRecorder() - err := processMultipart(w, req, &reqParams, DefaultUploadMaxSize, DefaultUploadMaxMemory) + err := processMultipart(w, req, &reqParams, &closers, &tmpFiles, DefaultUploadMaxSize, DefaultUploadMaxMemory) require.NotNil(t, err) require.Equal(t, err.Error(), "invalid operations paths for key 0") + cleanUp(t, closers, tmpFiles) }) t.Run("fail parse request big body", func(t *testing.T) { req := createUploadRequest(t, validOperations, validMap, validFiles) var reqParams params + var closers []io.Closer + var tmpFiles []string w := httptest.NewRecorder() var smallMaxSize int64 = 2 - err := processMultipart(w, req, &reqParams, smallMaxSize, DefaultUploadMaxMemory) + err := processMultipart(w, req, &reqParams, &closers, &tmpFiles, smallMaxSize, DefaultUploadMaxMemory) require.NotNil(t, err) require.Equal(t, err.Error(), "failed to parse multipart form, request body too large") + cleanUp(t, closers, tmpFiles) }) t.Run("valid request", func(t *testing.T) { req := createUploadRequest(t, validOperations, validMap, validFiles) var reqParams params + var closers []io.Closer + var tmpFiles []string w := httptest.NewRecorder() - err := processMultipart(w, req, &reqParams, DefaultUploadMaxSize, DefaultUploadMaxMemory) + err := processMultipart(w, req, &reqParams, &closers, &tmpFiles, DefaultUploadMaxSize, DefaultUploadMaxMemory) require.Nil(t, err) require.Equal(t, "mutation ($file: Upload!) { singleUpload(file: $file) { id } }", reqParams.Query) require.Equal(t, "", reqParams.OperationName) @@ -428,9 +471,10 @@ func TestProcessMultipart(t *testing.T) { content, err := ioutil.ReadAll(reqParamsFile.File) require.Nil(t, err) require.Equal(t, "test1", string(content)) + cleanUp(t, closers, tmpFiles) }) - t.Run("valid request with two values", func(t *testing.T) { + t.Run("valid file list upload with payload and file reuse", func(t *testing.T) { operations := `{ "query": "mutation($req: [UploadFile!]!) { multipleUploadWithPayload(req: $req) { id } }", "variables": { "req": [ { "id": 1, "file": null }, { "id": 2, "file": null } ] } }` mapData := `{ "0": ["variables.req.0.file", "variables.req.1.file"] }` files := []file{ @@ -442,33 +486,46 @@ func TestProcessMultipart(t *testing.T) { } req := createUploadRequest(t, operations, mapData, files) - var reqParams params - w := httptest.NewRecorder() - err := processMultipart(w, req, &reqParams, DefaultUploadMaxSize, 2) - require.Nil(t, err) - require.Equal(t, "mutation($req: [UploadFile!]!) { multipleUploadWithPayload(req: $req) { id } }", reqParams.Query) - require.Equal(t, "", reqParams.OperationName) - require.Equal(t, 1, len(reqParams.Variables)) - require.NotNil(t, reqParams.Variables["req"]) - reqParamsFile, ok := reqParams.Variables["req"].([]interface{}) - require.True(t, ok) - require.Equal(t, 2, len(reqParamsFile)) - for i, item := range reqParamsFile { - itemMap := item.(map[string]interface{}) - require.Equal(t, fmt.Sprint(itemMap["id"]), fmt.Sprint(i+1)) - file := itemMap["file"].(graphql.Upload) - require.Equal(t, "a.txt", file.Filename) - require.Equal(t, int64(len("test1")), file.Size) - _, err = file.File.Seek(0, 0) - require.Nil(t, err) - content, err := ioutil.ReadAll(file.File) + test := func(uploadMaxMemory int64) { + var reqParams params + var closers []io.Closer + var tmpFiles []string + w := httptest.NewRecorder() + err := processMultipart(w, req, &reqParams, &closers, &tmpFiles, DefaultUploadMaxSize, uploadMaxMemory) require.Nil(t, err) - require.Equal(t, "test1", string(content)) + require.Equal(t, "mutation($req: [UploadFile!]!) { multipleUploadWithPayload(req: $req) { id } }", reqParams.Query) + require.Equal(t, "", reqParams.OperationName) + require.Equal(t, 1, len(reqParams.Variables)) + require.NotNil(t, reqParams.Variables["req"]) + reqParamsFile, ok := reqParams.Variables["req"].([]interface{}) + require.True(t, ok) + require.Equal(t, 2, len(reqParamsFile)) + for i, item := range reqParamsFile { + itemMap := item.(map[string]interface{}) + require.Equal(t, fmt.Sprint(itemMap["id"]), fmt.Sprint(i+1)) + file := itemMap["file"].(graphql.Upload) + require.Equal(t, "a.txt", file.Filename) + require.Equal(t, int64(len("test1")), file.Size) + require.Nil(t, err) + content, err := ioutil.ReadAll(file.File) + require.Nil(t, err) + require.Equal(t, "test1", string(content)) + } + cleanUp(t, closers, tmpFiles) } + + t.Run("payload smaller than UploadMaxMemory, stored in memory", func(t *testing.T){ + test(5000) + }) + + t.Run("payload bigger than UploadMaxMemory, persisted to disk", func(t *testing.T){ + test(2) + }) }) } func TestAddUploadToOperations(t *testing.T) { + key := "0" t.Run("fail missing all variables", func(t *testing.T) { file, _ := os.Open("path/to/file") @@ -480,9 +537,9 @@ func TestAddUploadToOperations(t *testing.T) { Size: int64(5), } path := "variables.req.0.file" - err := addUploadToOperations(request, upload, path) + err := addUploadToOperations(request, upload, key, path) require.NotNil(t, err) - require.Equal(t, "variables is missing, path: variables.req.0.file", err.Error()) + require.Equal(t, "path is missing \"variables.\" prefix, key: 0, path: variables.req.0.file", err.Error()) }) t.Run("valid variable", func(t *testing.T) { @@ -506,7 +563,7 @@ func TestAddUploadToOperations(t *testing.T) { } path := "variables.file" - err := addUploadToOperations(request, upload, path) + err := addUploadToOperations(request, upload, key, path) require.Nil(t, err) require.Equal(t, request, expected) @@ -541,7 +598,7 @@ func TestAddUploadToOperations(t *testing.T) { } path := "variables.req.0.file" - err := addUploadToOperations(request, upload, path) + err := addUploadToOperations(request, upload, key, path) require.Nil(t, err) require.Equal(t, request, expected) From bb0234760f8d30ce1d142fe117d5b857eff7cb8e Mon Sep 17 00:00:00 2001 From: hantonelli Date: Fri, 19 Apr 2019 11:56:58 +0100 Subject: [PATCH 32/39] Lint code --- example/fileupload/fileupload_test.go | 4 ++-- handler/graphql_test.go | 10 +++++----- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/example/fileupload/fileupload_test.go b/example/fileupload/fileupload_test.go index c0d735aaa1..44c8a809dd 100644 --- a/example/fileupload/fileupload_test.go +++ b/example/fileupload/fileupload_test.go @@ -256,11 +256,11 @@ func TestFileUpload(t *testing.T) { require.Nil(t, err) } - t.Run("payload smaller than UploadMaxMemory, stored in memory", func(t *testing.T){ + t.Run("payload smaller than UploadMaxMemory, stored in memory", func(t *testing.T) { test(5000) }) - t.Run("payload bigger than UploadMaxMemory, persisted to disk", func(t *testing.T){ + t.Run("payload bigger than UploadMaxMemory, persisted to disk", func(t *testing.T) { test(2) }) }) diff --git a/handler/graphql_test.go b/handler/graphql_test.go index 82fd1606e2..6facad2bd9 100644 --- a/handler/graphql_test.go +++ b/handler/graphql_test.go @@ -305,7 +305,7 @@ func TestFileUpload(t *testing.T) { }) t.Run("valid file list upload with payload and file reuse", func(t *testing.T) { - test := func (uploadMaxMemory int64) { + test := func(uploadMaxMemory int64) { mock := &executableSchemaMock{ MutationFunc: func(ctx context.Context, op *ast.OperationDefinition) *graphql.Response { require.Equal(t, len(op.VariableDefinitions), 1) @@ -333,11 +333,11 @@ func TestFileUpload(t *testing.T) { require.Equal(t, `{"data":{"multipleUploadWithPayload":[{"id":1},{"id":2}]}}`, resp.Body.String()) } - t.Run("payload smaller than UploadMaxMemory, stored in memory", func(t *testing.T){ + t.Run("payload smaller than UploadMaxMemory, stored in memory", func(t *testing.T) { test(5000) }) - t.Run("payload bigger than UploadMaxMemory, persisted to disk", func(t *testing.T){ + t.Run("payload bigger than UploadMaxMemory, persisted to disk", func(t *testing.T) { test(2) }) }) @@ -514,11 +514,11 @@ func TestProcessMultipart(t *testing.T) { cleanUp(t, closers, tmpFiles) } - t.Run("payload smaller than UploadMaxMemory, stored in memory", func(t *testing.T){ + t.Run("payload smaller than UploadMaxMemory, stored in memory", func(t *testing.T) { test(5000) }) - t.Run("payload bigger than UploadMaxMemory, persisted to disk", func(t *testing.T){ + t.Run("payload bigger than UploadMaxMemory, persisted to disk", func(t *testing.T) { test(2) }) }) From b961d34e06ee088251ffbe2929c86db4381dec24 Mon Sep 17 00:00:00 2001 From: hantonelli Date: Fri, 19 Apr 2019 12:54:04 +0100 Subject: [PATCH 33/39] Remove wrapper that is now not required --- handler/graphql.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/handler/graphql.go b/handler/graphql.go index df99efc2cf..6db19fa109 100644 --- a/handler/graphql.go +++ b/handler/graphql.go @@ -599,7 +599,7 @@ func processMultipart(w http.ResponseWriter, r *http.Request, request *params, c } for _, path := range paths { upload = graphql.Upload{ - File: ioutil.NopCloser(bytes.NewReader(fileContent)), + File: bytes.NewReader(fileContent), Size: header.Size, Filename: header.Filename, } From 43fc53f9a90429c6693bfe88afaa83b1de8591e3 Mon Sep 17 00:00:00 2001 From: hantonelli Date: Fri, 19 Apr 2019 13:01:49 +0100 Subject: [PATCH 34/39] Improve variable name --- handler/graphql.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/handler/graphql.go b/handler/graphql.go index 6db19fa109..249969e2b0 100644 --- a/handler/graphql.go +++ b/handler/graphql.go @@ -593,13 +593,13 @@ func processMultipart(w http.ResponseWriter, r *http.Request, request *params, c } } else { if r.ContentLength < uploadMaxMemory { - fileContent, err := ioutil.ReadAll(file) + fileBytes, err := ioutil.ReadAll(file) if err != nil { return fmt.Errorf("failed to read file for key %s", key) } for _, path := range paths { upload = graphql.Upload{ - File: bytes.NewReader(fileContent), + File: bytes.NewReader(fileBytes), Size: header.Size, Filename: header.Filename, } From 54226cdbb2ab5e2f687241357ec9c062667e7b8a Mon Sep 17 00:00:00 2001 From: hantonelli Date: Wed, 1 May 2019 23:21:01 +0100 Subject: [PATCH 35/39] Add bytesReader to reuse read byte array --- handler/graphql.go | 22 +++++++++++-- handler/graphql_test.go | 69 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 89 insertions(+), 2 deletions(-) diff --git a/handler/graphql.go b/handler/graphql.go index 249969e2b0..6d2a787a03 100644 --- a/handler/graphql.go +++ b/handler/graphql.go @@ -1,7 +1,6 @@ package handler import ( - "bytes" "context" "encoding/json" "errors" @@ -547,6 +546,25 @@ func sendErrorf(w http.ResponseWriter, code int, format string, args ...interfac sendError(w, code, &gqlerror.Error{Message: fmt.Sprintf(format, args...)}) } +type bytesReader struct { + s *[]byte + i int64 // current reading index + prevRune int // index of previous rune; or < 0 +} + +func (r *bytesReader) Read(b []byte) (n int, err error) { + if r.s == nil { + return 0, errors.New("byte slice pointer is nil") + } + if r.i >= int64(len(*r.s)) { + return 0, io.EOF + } + r.prevRune = -1 + n = copy(b, (*r.s)[r.i:]) + r.i += int64(n) + return +} + func processMultipart(w http.ResponseWriter, r *http.Request, request *params, closers *[]io.Closer, tmpFiles *[]string, uploadMaxSize, uploadMaxMemory int64) error { var err error if r.ContentLength > uploadMaxSize { @@ -599,7 +617,7 @@ func processMultipart(w http.ResponseWriter, r *http.Request, request *params, c } for _, path := range paths { upload = graphql.Upload{ - File: bytes.NewReader(fileBytes), + File: &bytesReader{s: &fileBytes, i: 0, prevRune: -1}, Size: header.Size, Filename: header.Filename, } diff --git a/handler/graphql_test.go b/handler/graphql_test.go index 6facad2bd9..1c9cffd473 100644 --- a/handler/graphql_test.go +++ b/handler/graphql_test.go @@ -11,6 +11,7 @@ import ( "net/http/httptest" "os" "strings" + "sync" "testing" "github.com/99designs/gqlgen/graphql" @@ -644,3 +645,71 @@ func doRequest(handler http.Handler, method string, target string, body string) handler.ServeHTTP(w, r) return w } + +func TestBytesRead(t *testing.T) { + t.Run("test concurrency", func(t *testing.T) { + // Test for the race detector, to verify a Read that doesn't yield any bytes + // is okay to use from multiple goroutines. This was our historic behavior. + // See golang.org/issue/7856 + r := bytesReader{s: &([]byte{})} + var wg sync.WaitGroup + for i := 0; i < 5; i++ { + wg.Add(2) + go func() { + defer wg.Done() + var buf [1]byte + r.Read(buf[:]) + }() + go func() { + defer wg.Done() + r.Read(nil) + }() + } + wg.Wait() + }) + + t.Run("fail to read if pointer is nil", func(t *testing.T) { + n, err := (&bytesReader{}).Read(nil) + require.Equal(t, 0, n) + require.NotNil(t, err) + require.Equal(t, "byte slice pointer is nil", err.Error()) + }) + + t.Run("read using buffer", func(t *testing.T) { + data := []byte("0123456789") + r := bytesReader{s: &data} + + got := make([]byte, 0, 11) + buf := make([]byte, 1) + for { + r, e := r.Read(buf) + if r == 0 { + if e != nil || e == io.EOF { + break + } + } + got = append(got, buf...) + } + require.Equal(t, "0123456789", string(got)) + }) + + t.Run("read updated pointer value", func(t *testing.T) { + data:= []byte("0123456789") + pointer := &data + r := bytesReader{s: pointer} + data[2] = []byte("9")[0] + + got := make([]byte, 0, 11) + buf := make([]byte, 1) + for { + r, e := r.Read(buf) + if r == 0 { + if e != nil || e == io.EOF { + break + } + } + got = append(got, buf...) + } + require.Equal(t, "0193456789", string(got)) + }) +} From f30f1c312f5e49142578705cef05ef753bcc4277 Mon Sep 17 00:00:00 2001 From: hantonelli Date: Wed, 1 May 2019 23:25:32 +0100 Subject: [PATCH 36/39] Fix fmt --- handler/graphql_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/handler/graphql_test.go b/handler/graphql_test.go index 1c9cffd473..571e9705f0 100644 --- a/handler/graphql_test.go +++ b/handler/graphql_test.go @@ -694,7 +694,7 @@ func TestBytesRead(t *testing.T) { }) t.Run("read updated pointer value", func(t *testing.T) { - data:= []byte("0123456789") + data := []byte("0123456789") pointer := &data r := bytesReader{s: pointer} data[2] = []byte("9")[0] From d9dca642e348a9e8ea8749cd48235bf4da633b63 Mon Sep 17 00:00:00 2001 From: hantonelli Date: Mon, 6 May 2019 11:21:34 +0100 Subject: [PATCH 37/39] Improve documentation --- docs/content/reference/scalars.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/content/reference/scalars.md b/docs/content/reference/scalars.md index 36f035b9f3..ae123c9fa0 100644 --- a/docs/content/reference/scalars.md +++ b/docs/content/reference/scalars.md @@ -35,7 +35,7 @@ scalar Upload Maps a `Upload` GraphQL scalar to a `graphql.Upload` struct, defined as follows: ``` type Upload struct { - File io.ReadCloser + File io.Reader Filename string Size int64 } From aeccbce0fe78665cd0d7d55c42b4df142623fe6b Mon Sep 17 00:00:00 2001 From: hantonelli Date: Mon, 6 May 2019 11:26:01 +0100 Subject: [PATCH 38/39] Update test include an example that uses io.Read interface directly --- example/fileupload/fileupload_test.go | 20 ++++++++++++++++---- handler/graphql_test.go | 24 ++++++++++++++++-------- 2 files changed, 32 insertions(+), 12 deletions(-) diff --git a/example/fileupload/fileupload_test.go b/example/fileupload/fileupload_test.go index 44c8a809dd..a23e2f2e12 100644 --- a/example/fileupload/fileupload_test.go +++ b/example/fileupload/fileupload_test.go @@ -4,6 +4,7 @@ import ( "bytes" "context" "fmt" + "io" "io/ioutil" "mime/multipart" "net/http" @@ -216,13 +217,24 @@ func TestFileUpload(t *testing.T) { require.NotNil(t, req[i].File) require.NotNil(t, req[i].File.File) ids = append(ids, req[i].ID) - content, err := ioutil.ReadAll(req[i].File.File) - require.Nil(t, err) - contents = append(contents, string(content)) + + var got []byte + buf := make([]byte, 2) + for { + n, err := req[i].File.File.Read(buf) + got = append(got, buf[:n]...) + if err != nil { + if err == io.EOF { + break + } + require.Fail(t, "unexpected error while reading", err.Error()) + } + } + contents = append(contents, string(got)) resp = append(resp, model.File{ ID: i + 1, Name: req[i].File.Filename, - Content: string(content), + Content: string(got), }) } require.ElementsMatch(t, []int{1, 2}, ids) diff --git a/handler/graphql_test.go b/handler/graphql_test.go index 571e9705f0..70946ab949 100644 --- a/handler/graphql_test.go +++ b/handler/graphql_test.go @@ -682,13 +682,17 @@ func TestBytesRead(t *testing.T) { got := make([]byte, 0, 11) buf := make([]byte, 1) for { - r, e := r.Read(buf) - if r == 0 { - if e != nil || e == io.EOF { + n, err := r.Read(buf) + if n < 0 { + require.Fail(t, "unexpected bytes read size") + } + got = append(got, buf[:n]...) + if err != nil { + if err == io.EOF { break } + require.Fail(t, "unexpected error while reading", err.Error()) } - got = append(got, buf...) } require.Equal(t, "0123456789", string(got)) }) @@ -702,13 +706,17 @@ func TestBytesRead(t *testing.T) { got := make([]byte, 0, 11) buf := make([]byte, 1) for { - r, e := r.Read(buf) - if r == 0 { - if e != nil || e == io.EOF { + n, err := r.Read(buf) + if n < 0 { + require.Fail(t, "unexpected bytes read size") + } + got = append(got, buf[:n]...) + if err != nil { + if err == io.EOF { break } + require.Fail(t, "unexpected error while reading", err.Error()) } - got = append(got, buf...) } require.Equal(t, "0193456789", string(got)) }) From 5d1dea0a104ff07f657aa18e63097bf1626174a7 Mon Sep 17 00:00:00 2001 From: Adam Date: Wed, 8 May 2019 19:40:29 +1000 Subject: [PATCH 39/39] run go generate --- example/fileupload/generated.go | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/example/fileupload/generated.go b/example/fileupload/generated.go index 05c1a2afc2..b41d741ffc 100644 --- a/example/fileupload/generated.go +++ b/example/fileupload/generated.go @@ -1541,7 +1541,7 @@ func (ec *executionContext) unmarshalInputUploadFile(ctx context.Context, v inte var fileImplementors = []string{"File"} func (ec *executionContext) _File(ctx context.Context, sel ast.SelectionSet, obj *model.File) graphql.Marshaler { - fields := graphql.CollectFields(ctx, sel, fileImplementors) + fields := graphql.CollectFields(ec.RequestContext, sel, fileImplementors) out := graphql.NewFieldSet(fields) invalid := false @@ -1578,7 +1578,7 @@ func (ec *executionContext) _File(ctx context.Context, sel ast.SelectionSet, obj var mutationImplementors = []string{"Mutation"} func (ec *executionContext) _Mutation(ctx context.Context, sel ast.SelectionSet) graphql.Marshaler { - fields := graphql.CollectFields(ctx, sel, mutationImplementors) + fields := graphql.CollectFields(ec.RequestContext, sel, mutationImplementors) ctx = graphql.WithResolverContext(ctx, &graphql.ResolverContext{ Object: "Mutation", @@ -1624,7 +1624,7 @@ func (ec *executionContext) _Mutation(ctx context.Context, sel ast.SelectionSet) var queryImplementors = []string{"Query"} func (ec *executionContext) _Query(ctx context.Context, sel ast.SelectionSet) graphql.Marshaler { - fields := graphql.CollectFields(ctx, sel, queryImplementors) + fields := graphql.CollectFields(ec.RequestContext, sel, queryImplementors) ctx = graphql.WithResolverContext(ctx, &graphql.ResolverContext{ Object: "Query", @@ -1668,7 +1668,7 @@ func (ec *executionContext) _Query(ctx context.Context, sel ast.SelectionSet) gr var __DirectiveImplementors = []string{"__Directive"} func (ec *executionContext) ___Directive(ctx context.Context, sel ast.SelectionSet, obj *introspection.Directive) graphql.Marshaler { - fields := graphql.CollectFields(ctx, sel, __DirectiveImplementors) + fields := graphql.CollectFields(ec.RequestContext, sel, __DirectiveImplementors) out := graphql.NewFieldSet(fields) invalid := false @@ -1707,7 +1707,7 @@ func (ec *executionContext) ___Directive(ctx context.Context, sel ast.SelectionS var __EnumValueImplementors = []string{"__EnumValue"} func (ec *executionContext) ___EnumValue(ctx context.Context, sel ast.SelectionSet, obj *introspection.EnumValue) graphql.Marshaler { - fields := graphql.CollectFields(ctx, sel, __EnumValueImplementors) + fields := graphql.CollectFields(ec.RequestContext, sel, __EnumValueImplementors) out := graphql.NewFieldSet(fields) invalid := false @@ -1743,7 +1743,7 @@ func (ec *executionContext) ___EnumValue(ctx context.Context, sel ast.SelectionS var __FieldImplementors = []string{"__Field"} func (ec *executionContext) ___Field(ctx context.Context, sel ast.SelectionSet, obj *introspection.Field) graphql.Marshaler { - fields := graphql.CollectFields(ctx, sel, __FieldImplementors) + fields := graphql.CollectFields(ec.RequestContext, sel, __FieldImplementors) out := graphql.NewFieldSet(fields) invalid := false @@ -1789,7 +1789,7 @@ func (ec *executionContext) ___Field(ctx context.Context, sel ast.SelectionSet, var __InputValueImplementors = []string{"__InputValue"} func (ec *executionContext) ___InputValue(ctx context.Context, sel ast.SelectionSet, obj *introspection.InputValue) graphql.Marshaler { - fields := graphql.CollectFields(ctx, sel, __InputValueImplementors) + fields := graphql.CollectFields(ec.RequestContext, sel, __InputValueImplementors) out := graphql.NewFieldSet(fields) invalid := false @@ -1825,7 +1825,7 @@ func (ec *executionContext) ___InputValue(ctx context.Context, sel ast.Selection var __SchemaImplementors = []string{"__Schema"} func (ec *executionContext) ___Schema(ctx context.Context, sel ast.SelectionSet, obj *introspection.Schema) graphql.Marshaler { - fields := graphql.CollectFields(ctx, sel, __SchemaImplementors) + fields := graphql.CollectFields(ec.RequestContext, sel, __SchemaImplementors) out := graphql.NewFieldSet(fields) invalid := false @@ -1866,7 +1866,7 @@ func (ec *executionContext) ___Schema(ctx context.Context, sel ast.SelectionSet, var __TypeImplementors = []string{"__Type"} func (ec *executionContext) ___Type(ctx context.Context, sel ast.SelectionSet, obj *introspection.Type) graphql.Marshaler { - fields := graphql.CollectFields(ctx, sel, __TypeImplementors) + fields := graphql.CollectFields(ec.RequestContext, sel, __TypeImplementors) out := graphql.NewFieldSet(fields) invalid := false