From 0a92ca465691ea96186481471ec1ba01d6ecfaf8 Mon Sep 17 00:00:00 2001 From: Adam Scarr Date: Tue, 12 Mar 2019 22:21:33 +1100 Subject: [PATCH 1/2] Support map[string]interface{} in return types --- codegen/config/binder.go | 7 ++ codegen/field.go | 5 + codegen/field.gotpl | 13 +- codegen/object.go | 10 ++ codegen/object.gotpl | 2 +- codegen/testdata/schema.graphql | 6 - codegen/testserver/generated.go | 209 ++++++++++++++++++++++++++++++++ codegen/testserver/gqlgen.yml | 4 + codegen/testserver/maps.graphql | 13 ++ codegen/testserver/maps_test.go | 52 ++++++++ codegen/testserver/resolver.go | 3 + codegen/testserver/stub.go | 4 + codegen/type.gotpl | 8 +- example/todo/generated.go | 9 ++ 14 files changed, 334 insertions(+), 11 deletions(-) create mode 100644 codegen/testserver/maps.graphql create mode 100644 codegen/testserver/maps_test.go diff --git a/codegen/config/binder.go b/codegen/config/binder.go index 0c8c7d614c2..e8b3c459220 100644 --- a/codegen/config/binder.go +++ b/codegen/config/binder.go @@ -204,6 +204,13 @@ func (t *TypeReference) IsPtr() bool { return isPtr } +func (t *TypeReference) IsNilable() bool { + _, isPtr := t.GO.(*types.Pointer) + _, isMap := t.GO.(*types.Map) + _, isInterface := t.GO.(*types.Interface) + return isPtr || isMap || isInterface +} + func (t *TypeReference) IsSlice() bool { _, isSlice := t.GO.(*types.Slice) return isSlice diff --git a/codegen/field.go b/codegen/field.go index e54e9fdad1f..f5f7b22139c 100644 --- a/codegen/field.go +++ b/codegen/field.go @@ -102,6 +102,7 @@ func (b *builder) bindField(obj *Object, f *Field) error { f.IsResolver = true return nil case obj.Type == config.MapType: + f.GoFieldType = GoFieldMap return nil case b.Config.Models[obj.Name].Fields[f.Name].FieldName != "": f.GoFieldName = b.Config.Models[obj.Name].Fields[f.Name].FieldName @@ -298,6 +299,10 @@ func (f *Field) IsVariable() bool { return f.GoFieldType == GoFieldVariable } +func (f *Field) IsMap() bool { + return f.GoFieldType == GoFieldMap +} + func (f *Field) IsConcurrent() bool { if f.Object.DisableConcurrency { return false diff --git a/codegen/field.gotpl b/codegen/field.gotpl index f07a6f03674..17058fd4896 100644 --- a/codegen/field.gotpl +++ b/codegen/field.gotpl @@ -37,7 +37,7 @@ } } {{ else }} - func (ec *executionContext) _{{$object.Name}}_{{$field.Name}}(ctx context.Context, field graphql.CollectedField{{ if not $object.Root }}, obj *{{$object.Type | ref}}{{end}}) graphql.Marshaler { + func (ec *executionContext) _{{$object.Name}}_{{$field.Name}}(ctx context.Context, field graphql.CollectedField{{ if not $object.Root }}, obj {{$object.Reference | ref}}{{end}}) graphql.Marshaler { ctx = ec.Tracer.StartFieldExecution(ctx, field) defer func () { ec.Tracer.EndFieldExecution(ctx) }() rctx := &graphql.ResolverContext{ @@ -60,6 +60,17 @@ ctx = rctx // use context from middleware stack in children {{- if $field.IsResolver }} return ec.resolvers.{{ $field.ShortInvocation }} + {{- else if $field.IsMap }} + switch v := {{$field.GoReceiverName}}[{{$field.Name|quote}}].(type) { + case {{$field.TypeReference.GO | ref}}: + return v, nil + case {{$field.TypeReference.Elem.GO | ref}}: + return &v, nil + case nil: + return ({{$field.TypeReference.GO | ref}})(nil), nil + default: + return nil, fmt.Errorf("unexpected type %T for field %s", v, {{ $field.Name | quote}}) + } {{- else if $field.IsMethod }} {{- if $field.NoErr }} return {{$field.GoReceiverName}}.{{$field.GoFieldName}}({{ $field.CallArgs }}), nil diff --git a/codegen/object.go b/codegen/object.go index 7b9e3d6f597..539c3164c50 100644 --- a/codegen/object.go +++ b/codegen/object.go @@ -17,6 +17,7 @@ const ( GoFieldUndefined GoFieldType = iota GoFieldMethod GoFieldVariable + GoFieldMap ) type Object struct { @@ -80,6 +81,15 @@ func (b *builder) buildObject(typ *ast.Definition) (*Object, error) { return obj, nil } +func (o *Object) Reference() types.Type { + switch o.Type.(type) { + case *types.Pointer, *types.Slice, *types.Map: + return o.Type + } + + return types.NewPointer(o.Type) +} + type Objects []*Object func (o *Object) Implementors() string { diff --git a/codegen/object.gotpl b/codegen/object.gotpl index 13224ed0233..19da1b1988b 100644 --- a/codegen/object.gotpl +++ b/codegen/object.gotpl @@ -23,7 +23,7 @@ func (ec *executionContext) _{{$object.Name}}(ctx context.Context, sel ast.Selec } } {{- else }} -func (ec *executionContext) _{{$object.Name}}(ctx context.Context, sel ast.SelectionSet{{ if not $object.Root }},obj *{{$object.Type | ref }}{{ end }}) graphql.Marshaler { +func (ec *executionContext) _{{$object.Name}}(ctx context.Context, sel ast.SelectionSet{{ if not $object.Root }},obj {{$object.Reference | ref }}{{ end }}) graphql.Marshaler { fields := graphql.CollectFields(ctx, sel, {{$object.Name|lcFirst}}Implementors) {{if $object.Root}} ctx = graphql.WithResolverContext(ctx, &graphql.ResolverContext{ diff --git a/codegen/testdata/schema.graphql b/codegen/testdata/schema.graphql index d08a37d54c7..5d49426216f 100644 --- a/codegen/testdata/schema.graphql +++ b/codegen/testdata/schema.graphql @@ -1,7 +1,6 @@ type Query { invalidIdentifier: InvalidIdentifier collision: It - mapInput(input: Changes): Boolean recursive(input: RecursiveInputSlice): Boolean nestedInputs(input: [[OuterInput]] = [[{inner: {id: 1}}]]): Boolean nestedOutputs: [[OuterObject]] @@ -49,11 +48,6 @@ type It { id: ID! } -input Changes { - a: Int - b: Int -} - input RecursiveInputSlice { self: [RecursiveInputSlice!] } diff --git a/codegen/testserver/generated.go b/codegen/testserver/generated.go index fa25e518b52..9ba500108f4 100644 --- a/codegen/testserver/generated.go +++ b/codegen/testserver/generated.go @@ -108,6 +108,11 @@ type ComplexityRoot struct { ID func(childComplexity int) int } + MapStringInterfaceType struct { + A func(childComplexity int) int + B func(childComplexity int) int + } + ModelMethods struct { ResolverField func(childComplexity int) int NoContext func(childComplexity int) int @@ -146,6 +151,7 @@ type ComplexityRoot struct { ShapeUnion func(childComplexity int) int Autobind func(childComplexity int) int DeprecatedField func(childComplexity int) int + MapStringInterface func(childComplexity int, in map[string]interface{}) int Panics func(childComplexity int) int DefaultScalar func(childComplexity int, arg string) int ValidType func(childComplexity int) int @@ -226,6 +232,7 @@ type QueryResolver interface { ShapeUnion(ctx context.Context) (ShapeUnion, error) Autobind(ctx context.Context) (*Autobind, error) DeprecatedField(ctx context.Context) (string, error) + MapStringInterface(ctx context.Context, in map[string]interface{}) (map[string]interface{}, error) Panics(ctx context.Context) (*Panics, error) DefaultScalar(ctx context.Context, arg string) (string, error) ValidType(ctx context.Context) (*ValidType, error) @@ -393,6 +400,20 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.It.ID(childComplexity), true + case "MapStringInterfaceType.A": + if e.complexity.MapStringInterfaceType.A == nil { + break + } + + return e.complexity.MapStringInterfaceType.A(childComplexity), true + + case "MapStringInterfaceType.B": + if e.complexity.MapStringInterfaceType.B == nil { + break + } + + return e.complexity.MapStringInterfaceType.B(childComplexity), true + case "ModelMethods.ResolverField": if e.complexity.ModelMethods.ResolverField == nil { break @@ -654,6 +675,18 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.Query.DeprecatedField(childComplexity), true + case "Query.MapStringInterface": + if e.complexity.Query.MapStringInterface == nil { + break + } + + args, err := ec.field_Query_mapStringInterface_args(context.TODO(), rawArgs) + if err != nil { + return 0, false + } + + return e.complexity.Query.MapStringInterface(childComplexity, args["in"].(map[string]interface{})), true + case "Query.Panics": if e.complexity.Query.Panics == nil { break @@ -940,6 +973,20 @@ func (ec *executionContext) introspectType(name string) (*introspection.Type, er } var parsedSchema = gqlparser.MustLoadSchema( + &ast.Source{Name: "maps.graphql", Input: `extend type Query { + mapStringInterface(in: MapStringInterfaceInput): MapStringInterfaceType +} + +type MapStringInterfaceType { + a: String + b: Int +} + +input MapStringInterfaceInput { + a: String + b: Int +} +`}, &ast.Source{Name: "panics.graphql", Input: `extend type Query { panics: Panics } @@ -1441,6 +1488,20 @@ func (ec *executionContext) field_Query_mapInput_args(ctx context.Context, rawAr return args, nil } +func (ec *executionContext) field_Query_mapStringInterface_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) { + var err error + args := map[string]interface{}{} + var arg0 map[string]interface{} + if tmp, ok := rawArgs["in"]; ok { + arg0, err = ec.unmarshalOMapStringInterfaceInput2map(ctx, tmp) + if err != nil { + return nil, err + } + } + args["in"] = arg0 + return args, nil +} + func (ec *executionContext) field_Query_nestedInputs_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) { var err error args := map[string]interface{}{} @@ -2256,6 +2317,70 @@ func (ec *executionContext) _It_id(ctx context.Context, field graphql.CollectedF return ec.marshalNID2string(ctx, field.Selections, res) } +func (ec *executionContext) _MapStringInterfaceType_a(ctx context.Context, field graphql.CollectedField, obj map[string]interface{}) graphql.Marshaler { + ctx = ec.Tracer.StartFieldExecution(ctx, field) + defer func() { ec.Tracer.EndFieldExecution(ctx) }() + rctx := &graphql.ResolverContext{ + Object: "MapStringInterfaceType", + 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 + switch v := obj["a"].(type) { + case *string: + return v, nil + case string: + return &v, nil + case nil: + return (*string)(nil), nil + default: + return nil, fmt.Errorf("unexpected type %T for field %s", v, "a") + } + }) + 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) _MapStringInterfaceType_b(ctx context.Context, field graphql.CollectedField, obj map[string]interface{}) graphql.Marshaler { + ctx = ec.Tracer.StartFieldExecution(ctx, field) + defer func() { ec.Tracer.EndFieldExecution(ctx) }() + rctx := &graphql.ResolverContext{ + Object: "MapStringInterfaceType", + 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 + switch v := obj["b"].(type) { + case *int: + return v, nil + case int: + return &v, nil + case nil: + return (*int)(nil), nil + default: + return nil, fmt.Errorf("unexpected type %T for field %s", v, "b") + } + }) + if resTmp == nil { + return graphql.Null + } + res := resTmp.(*int) + rctx.Result = res + ctx = ec.Tracer.StartFieldChildExecution(ctx) + return ec.marshalOInt2ᚖint(ctx, field.Selections, res) +} + func (ec *executionContext) _ModelMethods_resolverField(ctx context.Context, field graphql.CollectedField, obj *ModelMethods) graphql.Marshaler { ctx = ec.Tracer.StartFieldExecution(ctx, field) defer func() { ec.Tracer.EndFieldExecution(ctx) }() @@ -3027,6 +3152,36 @@ func (ec *executionContext) _Query_deprecatedField(ctx context.Context, field gr return ec.marshalNString2string(ctx, field.Selections, res) } +func (ec *executionContext) _Query_mapStringInterface(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_mapStringInterface_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().MapStringInterface(rctx, args["in"].(map[string]interface{})) + }) + if resTmp == nil { + return graphql.Null + } + res := resTmp.(map[string]interface{}) + rctx.Result = res + ctx = ec.Tracer.StartFieldChildExecution(ctx) + return ec.marshalOMapStringInterfaceType2map(ctx, field.Selections, res) +} + func (ec *executionContext) _Query_panics(ctx context.Context, field graphql.CollectedField) graphql.Marshaler { ctx = ec.Tracer.StartFieldExecution(ctx, field) defer func() { ec.Tracer.EndFieldExecution(ctx) }() @@ -5083,6 +5238,32 @@ func (ec *executionContext) _It(ctx context.Context, sel ast.SelectionSet, obj * return out } +var mapStringInterfaceTypeImplementors = []string{"MapStringInterfaceType"} + +func (ec *executionContext) _MapStringInterfaceType(ctx context.Context, sel ast.SelectionSet, obj map[string]interface{}) graphql.Marshaler { + fields := graphql.CollectFields(ctx, sel, mapStringInterfaceTypeImplementors) + + out := graphql.NewFieldSet(fields) + invalid := false + for i, field := range fields { + switch field.Name { + case "__typename": + out.Values[i] = graphql.MarshalString("MapStringInterfaceType") + case "a": + out.Values[i] = ec._MapStringInterfaceType_a(ctx, field, obj) + case "b": + out.Values[i] = ec._MapStringInterfaceType_b(ctx, field, obj) + default: + panic("unknown field " + strconv.Quote(field.Name)) + } + } + out.Dispatch() + if invalid { + return graphql.Null + } + return out +} + var modelMethodsImplementors = []string{"ModelMethods"} func (ec *executionContext) _ModelMethods(ctx context.Context, sel ast.SelectionSet, obj *ModelMethods) graphql.Marshaler { @@ -5490,6 +5671,17 @@ func (ec *executionContext) _Query(ctx context.Context, sel ast.SelectionSet) gr } return res }) + case "mapStringInterface": + 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_mapStringInterface(ctx, field) + return res + }) case "panics": field := field out.Concurrently(i, func() (res graphql.Marshaler) { @@ -6521,6 +6713,9 @@ func (ec *executionContext) marshalOBoolean2ᚖbool(ctx context.Context, sel ast } func (ec *executionContext) unmarshalOChanges2map(ctx context.Context, v interface{}) (map[string]interface{}, error) { + if v == nil { + return nil, nil + } return v.(map[string]interface{}), nil } @@ -6646,6 +6841,20 @@ func (ec *executionContext) marshalOIt2ᚖgithubᚗcomᚋ99designsᚋgqlgenᚋco return ec._It(ctx, sel, v) } +func (ec *executionContext) unmarshalOMapStringInterfaceInput2map(ctx context.Context, v interface{}) (map[string]interface{}, error) { + if v == nil { + return nil, nil + } + return v.(map[string]interface{}), nil +} + +func (ec *executionContext) marshalOMapStringInterfaceType2map(ctx context.Context, sel ast.SelectionSet, v map[string]interface{}) graphql.Marshaler { + if v == nil { + return graphql.Null + } + return ec._MapStringInterfaceType(ctx, sel, v) +} + func (ec *executionContext) marshalOModelMethods2githubᚗcomᚋ99designsᚋgqlgenᚋcodegenᚋtestserverᚐModelMethods(ctx context.Context, sel ast.SelectionSet, v ModelMethods) graphql.Marshaler { return ec._ModelMethods(ctx, sel, &v) } diff --git a/codegen/testserver/gqlgen.yml b/codegen/testserver/gqlgen.yml index dff1dd02a47..0e0c0bbe251 100644 --- a/codegen/testserver/gqlgen.yml +++ b/codegen/testserver/gqlgen.yml @@ -56,3 +56,7 @@ models: model: "github.com/99designs/gqlgen/codegen/testserver.MarshalPanic" Autobind: model: "github.com/99designs/gqlgen/codegen/testserver.Autobind" + MapStringInterfaceInput: + model: "map[string]interface{}" + MapStringInterfaceType: + model: "map[string]interface{}" diff --git a/codegen/testserver/maps.graphql b/codegen/testserver/maps.graphql new file mode 100644 index 00000000000..82dc180acd2 --- /dev/null +++ b/codegen/testserver/maps.graphql @@ -0,0 +1,13 @@ +extend type Query { + mapStringInterface(in: MapStringInterfaceInput): MapStringInterfaceType +} + +type MapStringInterfaceType { + a: String + b: Int +} + +input MapStringInterfaceInput { + a: String + b: Int +} diff --git a/codegen/testserver/maps_test.go b/codegen/testserver/maps_test.go new file mode 100644 index 00000000000..e2490909a71 --- /dev/null +++ b/codegen/testserver/maps_test.go @@ -0,0 +1,52 @@ +package testserver + +import ( + "context" + "net/http/httptest" + "testing" + + "github.com/99designs/gqlgen/client" + "github.com/99designs/gqlgen/handler" + "github.com/stretchr/testify/require" +) + +func TestMaps(t *testing.T) { + resolver := &Stub{} + resolver.QueryResolver.MapStringInterface = func(ctx context.Context, in map[string]interface{}) (i map[string]interface{}, e error) { + return in, nil + } + + srv := httptest.NewServer( + handler.GraphQL( + NewExecutableSchema(Config{Resolvers: resolver}), + )) + defer srv.Close() + c := client.New(srv.URL) + t.Run("unset", func(t *testing.T) { + var resp struct { + MapStringInterface map[string]interface{} + } + err := c.Post(`query { mapStringInterface { a, b } }`, &resp) + require.NoError(t, err) + require.Nil(t, resp.MapStringInterface) + }) + + t.Run("nil", func(t *testing.T) { + var resp struct { + MapStringInterface map[string]interface{} + } + err := c.Post(`query { mapStringInterface(in: null) { a, b } }`, &resp) + require.NoError(t, err) + require.Nil(t, resp.MapStringInterface) + }) + + t.Run("values", func(t *testing.T) { + var resp struct { + MapStringInterface map[string]interface{} + } + err := c.Post(`query { mapStringInterface(in: { a: "a", b: null }) { a, b } }`, &resp) + require.NoError(t, err) + require.Equal(t, "a", resp.MapStringInterface["a"]) + require.Nil(t, resp.MapStringInterface["b"]) + }) +} diff --git a/codegen/testserver/resolver.go b/codegen/testserver/resolver.go index 8a7ed32cee3..e39b71a3298 100644 --- a/codegen/testserver/resolver.go +++ b/codegen/testserver/resolver.go @@ -116,6 +116,9 @@ func (r *queryResolver) Autobind(ctx context.Context) (*Autobind, error) { func (r *queryResolver) DeprecatedField(ctx context.Context) (string, error) { panic("not implemented") } +func (r *queryResolver) MapStringInterface(ctx context.Context, in map[string]interface{}) (map[string]interface{}, error) { + panic("not implemented") +} func (r *queryResolver) Panics(ctx context.Context) (*Panics, error) { panic("not implemented") } diff --git a/codegen/testserver/stub.go b/codegen/testserver/stub.go index 2da85a43473..183fad3024a 100644 --- a/codegen/testserver/stub.go +++ b/codegen/testserver/stub.go @@ -42,6 +42,7 @@ type Stub struct { ShapeUnion func(ctx context.Context) (ShapeUnion, error) Autobind func(ctx context.Context) (*Autobind, error) DeprecatedField func(ctx context.Context) (string, error) + MapStringInterface func(ctx context.Context, in map[string]interface{}) (map[string]interface{}, error) Panics func(ctx context.Context) (*Panics, error) DefaultScalar func(ctx context.Context, arg string) (string, error) ValidType func(ctx context.Context) (*ValidType, error) @@ -160,6 +161,9 @@ func (r *stubQuery) Autobind(ctx context.Context) (*Autobind, error) { func (r *stubQuery) DeprecatedField(ctx context.Context) (string, error) { return r.QueryResolver.DeprecatedField(ctx) } +func (r *stubQuery) MapStringInterface(ctx context.Context, in map[string]interface{}) (map[string]interface{}, error) { + return r.QueryResolver.MapStringInterface(ctx, in) +} func (r *stubQuery) Panics(ctx context.Context) (*Panics, error) { return r.QueryResolver.Panics(ctx) } diff --git a/codegen/type.gotpl b/codegen/type.gotpl index ce766d3c5f8..531ee96ad4f 100644 --- a/codegen/type.gotpl +++ b/codegen/type.gotpl @@ -1,8 +1,10 @@ {{- range $type := .ReferencedTypes }} {{ with $type.UnmarshalFunc }} func (ec *executionContext) {{ . }}(ctx context.Context, v interface{}) ({{ $type.GO | ref }}, error) { - {{- if $type.IsPtr }} + {{- if $type.IsNilable }} if v == nil { return nil, nil } + {{- end }} + {{- if $type.IsPtr }} res, err := ec.{{ $type.Elem.UnmarshalFunc }}(ctx, v) return &res, err {{- else if $type.IsSlice }} @@ -40,7 +42,7 @@ {{ with $type.MarshalFunc }} func (ec *executionContext) {{ . }}(ctx context.Context, sel ast.SelectionSet, v {{ $type.GO | ref }}) graphql.Marshaler { - {{- if $type.IsPtr }} + {{- if $type.IsNilable }} if v == nil { {{- if $type.GQL.NonNull }} if !ec.HasError(graphql.GetResolverContext(ctx)) { @@ -111,7 +113,7 @@ return {{ $type.Marshaler | call }}(v) {{- end }} {{- else }} - return ec._{{$type.Definition.Name}}(ctx, sel, {{ if not $type.IsPtr}}&{{end}} v) + return ec._{{$type.Definition.Name}}(ctx, sel, {{ if not $type.IsNilable}}&{{end}} v) {{- end }} {{- end }} } diff --git a/example/todo/generated.go b/example/todo/generated.go index 1db0bd6ebf8..b6d49c3650e 100644 --- a/example/todo/generated.go +++ b/example/todo/generated.go @@ -1909,10 +1909,19 @@ func (ec *executionContext) marshalNID2int(ctx context.Context, sel ast.Selectio } func (ec *executionContext) unmarshalNMap2map(ctx context.Context, v interface{}) (map[string]interface{}, error) { + if v == nil { + return nil, nil + } return graphql.UnmarshalMap(v) } func (ec *executionContext) marshalNMap2map(ctx context.Context, sel ast.SelectionSet, v map[string]interface{}) graphql.Marshaler { + if v == nil { + if !ec.HasError(graphql.GetResolverContext(ctx)) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } return graphql.MarshalMap(v) } From 765ff73865ac2d61eb8b6c9fe4534e5a4fbc072e Mon Sep 17 00:00:00 2001 From: Adam Scarr Date: Tue, 12 Mar 2019 22:43:25 +1100 Subject: [PATCH 2/2] Add some docs on maps --- docs/content/reference/changesets.md | 67 ++++++++++++++++++++++++++++ 1 file changed, 67 insertions(+) create mode 100644 docs/content/reference/changesets.md diff --git a/docs/content/reference/changesets.md b/docs/content/reference/changesets.md new file mode 100644 index 00000000000..1c7e9b0d9e0 --- /dev/null +++ b/docs/content/reference/changesets.md @@ -0,0 +1,67 @@ +--- +linkTitle: Changesets +title: Using maps as changesets +description: Falling back to map[string]interface{} to allow for presence checks. +menu: { main: { parent: 'reference' } } +--- + +Occasionally you need to distinguish presence from nil (undefined vs null). In gqlgen we do this using maps: + + +```graphql +type Query { + updateUser(id: ID!, changes: UserChanges!): User +} + +type UserChanges { + name: String + email: String +} +``` + +Then in config set the backing type to `map[string]interface{}` +```yaml +models: + UserChanges: + model: "map[string]interface{}" +``` + +After running go generate you should end up with a resolver that looks like this: +```go +func (r *queryResolver) UpdateUser(ctx context.Context, id int, changes map[string]interface{}) (*User, error) { + u := fetchFromDb(id) + /// apply the changes + saveToDb(u) + return u, nil +} +``` + +We often use the mapstructure library to directly apply these changesets directly to the object using reflection: +```go + +func ApplyChanges(changes map[string]interface{}, to interface{}) error { + dec, err := mapstructure.NewDecoder(&mapstructure.DecoderConfig{ + ErrorUnused: true, + TagName: "json", + Result: to, + ZeroFields: true, + // This is needed to get mapstructure to call the gqlgen unmarshaler func for custom scalars (eg Date) + DecodeHook: func(a reflect.Type, b reflect.Type, v interface{}) (interface{}, error) { + if reflect.PtrTo(b).Implements(reflect.TypeOf((*graphql.Unmarshaler)(nil)).Elem()) { + resultType := reflect.New(b) + result := resultType.MethodByName("UnmarshalGQL").Call([]reflect.Value{reflect.ValueOf(v)}) + err, _ := result[0].Interface().(error) + return resultType.Elem().Interface(), err + } + + return v, nil + }, + }) + + if err != nil { + return err + } + + return dec.Decode(changes) +} +```