Skip to content

Commit

Permalink
Merge pull request 99designs#1115 from bowd/add-input-path-for-unmars…
Browse files Browse the repository at this point in the history
…haling

Add input path in unmarshaling errors
  • Loading branch information
vektah authored Jul 26, 2020
2 parents 16520b8 + 54f457a commit 1525cff
Show file tree
Hide file tree
Showing 30 changed files with 1,218 additions and 272 deletions.
1 change: 1 addition & 0 deletions codegen/args.gotpl
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ func (ec *executionContext) {{ $name }}(ctx context.Context, rawArgs map[string]
{{- range $i, $arg := . }}
var arg{{$i}} {{ $arg.TypeReference.GO | ref}}
if tmp, ok := rawArgs[{{$arg.Name|quote}}]; ok {
ctx := graphql.WithFieldInputContext(ctx, graphql.NewFieldInputWithField({{$arg.Name|quote}}))
{{- if $arg.ImplDirectives }}
directive0 := func(ctx context.Context) (interface{}, error) { return ec.{{ $arg.TypeReference.UnmarshalFunc }}(ctx, tmp) }
{{ template "implDirectives" $arg }}
Expand Down
2 changes: 2 additions & 0 deletions codegen/input.gotpl
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@
{{- range $field := .Fields }}
case {{$field.Name|quote}}:
var err error

ctx := graphql.WithFieldInputContext(ctx, graphql.NewFieldInputWithField({{$field.Name|quote}}))
{{- if $field.ImplDirectives }}
directive0 := func(ctx context.Context) (interface{}, error) { return ec.{{ $field.TypeReference.UnmarshalFunc }}(ctx, v) }
{{ template "implDirectives" $field }}
Expand Down
6 changes: 3 additions & 3 deletions codegen/testserver/directive_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -308,7 +308,7 @@ func TestDirectives(t *testing.T) {

err := c.Post(`query { directiveInputNullable(arg: {text:"invalid text",inner:{message:"123"}}) }`, &resp)

require.EqualError(t, err, `[{"message":"not valid","path":["directiveInputNullable"]}]`)
require.EqualError(t, err, `[{"message":"not valid","path":["directiveInputNullable","arg"]}]`)
require.Nil(t, resp.DirectiveInputNullable)
})
t.Run("when function errors on inner directives", func(t *testing.T) {
Expand All @@ -318,7 +318,7 @@ func TestDirectives(t *testing.T) {

err := c.Post(`query { directiveInputNullable(arg: {text:"2",inner:{message:""}}) }`, &resp)

require.EqualError(t, err, `[{"message":"not valid","path":["directiveInputNullable"]}]`)
require.EqualError(t, err, `[{"message":"not valid","path":["directiveInputNullable","arg","inner"]}]`)
require.Nil(t, resp.DirectiveInputNullable)
})
t.Run("when function errors on nullable inner directives", func(t *testing.T) {
Expand All @@ -328,7 +328,7 @@ func TestDirectives(t *testing.T) {

err := c.Post(`query { directiveInputNullable(arg: {text:"success",inner:{message:"1"},innerNullable:{message:""}}) }`, &resp)

require.EqualError(t, err, `[{"message":"not valid","path":["directiveInputNullable"]}]`)
require.EqualError(t, err, `[{"message":"not valid","path":["directiveInputNullable","arg","innerNullable"]}]`)
require.Nil(t, resp.DirectiveInputNullable)
})
t.Run("when function success", func(t *testing.T) {
Expand Down
498 changes: 438 additions & 60 deletions codegen/testserver/generated.go

Large diffs are not rendered by default.

3 changes: 3 additions & 0 deletions codegen/testserver/gqlgen.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,6 @@ autobind:
- "github.com/99designs/gqlgen/codegen/testserver/introspection"
- "github.com/99designs/gqlgen/codegen/testserver/invalid-packagename"

models:
Email:
model: "github.com/99designs/gqlgen/codegen/testserver.Email"
8 changes: 8 additions & 0 deletions codegen/testserver/models-gen.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

29 changes: 29 additions & 0 deletions codegen/testserver/mutation_with_custom_scalar.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package testserver

import (
"encoding/json"
"fmt"
"io"
"regexp"
)

var re = regexp.MustCompile("^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$")

type Email string

func (value *Email) UnmarshalGQL(v interface{}) error {
input, ok := v.(string)
if !ok {
return fmt.Errorf("email expects a string value")
}
if !re.MatchString(input) {
return fmt.Errorf("invalid email format")
}
*value = Email(input)
return nil
}

func (value Email) MarshalGQL(w io.Writer) {
output, _ := json.Marshal(string(value))
w.Write(output)
}
13 changes: 13 additions & 0 deletions codegen/testserver/mutation_with_custom_scalar.graphql
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
extend type Mutation {
updateSomething(input: SpecialInput!): String!
}

scalar Email

input SpecialInput {
nesting: NestedInput!
}

input NestedInput {
field: Email!
}
50 changes: 50 additions & 0 deletions codegen/testserver/mutation_with_custom_scalar_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package testserver

import (
"context"
"testing"

"github.com/99designs/gqlgen/client"
"github.com/99designs/gqlgen/graphql/handler"
"github.com/stretchr/testify/require"
)

func TestErrorInsideMutationArgument(t *testing.T) {
resolvers := &Stub{}
resolvers.MutationResolver.UpdateSomething = func(_ context.Context, input SpecialInput) (s string, err error) {
return "Hello world", nil
}

c := client.New(handler.NewDefaultServer(NewExecutableSchema(Config{Resolvers: resolvers})))

t.Run("mutation with correct input doesn't return error", func(t *testing.T) {
var resp map[string]interface{}
input := map[string]interface{}{
"nesting": map[string]interface{}{
"field": "[email protected]",
},
}
err := c.Post(
`mutation TestMutation($input: SpecialInput!) { updateSomething(input: $input) }`,
&resp,
client.Var("input", input),
)
require.Equal(t, resp["updateSomething"], "Hello world")
require.NoError(t, err)
})

t.Run("mutation with incorrect input returns full path", func(t *testing.T) {
var resp map[string]interface{}
input := map[string]interface{}{
"nesting": map[string]interface{}{
"field": "not-an-email",
},
}
err := c.Post(
`mutation TestMutation($input: SpecialInput!) { updateSomething(input: $input) }`,
&resp,
client.Var("input", input),
)
require.EqualError(t, err, `[{"message":"invalid email format","path":["updateSomething","input","nesting","field"]}]`)
})
}
16 changes: 14 additions & 2 deletions codegen/testserver/resolver.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,10 @@ func (r *modelMethodsResolver) ResolverField(ctx context.Context, obj *ModelMeth
panic("not implemented")
}

func (r *mutationResolver) UpdateSomething(ctx context.Context, input SpecialInput) (string, error) {
panic("not implemented")
}

func (r *overlappingFieldsResolver) OldFoo(ctx context.Context, obj *OverlappingFields) (int, error) {
panic("not implemented")
}
Expand Down Expand Up @@ -328,7 +332,9 @@ func (r *wrappedSliceResolver) Get(ctx context.Context, obj WrappedSlice, idx in
}

// BackedByInterface returns BackedByInterfaceResolver implementation.
func (r *Resolver) BackedByInterface() BackedByInterfaceResolver { return &backedByInterfaceResolver{r} }
func (r *Resolver) BackedByInterface() BackedByInterfaceResolver {
return &backedByInterfaceResolver{r}
}

// Errors returns ErrorsResolver implementation.
func (r *Resolver) Errors() ErrorsResolver { return &errorsResolver{r} }
Expand All @@ -339,8 +345,13 @@ func (r *Resolver) ForcedResolver() ForcedResolverResolver { return &forcedResol
// ModelMethods returns ModelMethodsResolver implementation.
func (r *Resolver) ModelMethods() ModelMethodsResolver { return &modelMethodsResolver{r} }

// Mutation returns MutationResolver implementation.
func (r *Resolver) Mutation() MutationResolver { return &mutationResolver{r} }

// OverlappingFields returns OverlappingFieldsResolver implementation.
func (r *Resolver) OverlappingFields() OverlappingFieldsResolver { return &overlappingFieldsResolver{r} }
func (r *Resolver) OverlappingFields() OverlappingFieldsResolver {
return &overlappingFieldsResolver{r}
}

// Panics returns PanicsResolver implementation.
func (r *Resolver) Panics() PanicsResolver { return &panicsResolver{r} }
Expand Down Expand Up @@ -370,6 +381,7 @@ type backedByInterfaceResolver struct{ *Resolver }
type errorsResolver struct{ *Resolver }
type forcedResolverResolver struct{ *Resolver }
type modelMethodsResolver struct{ *Resolver }
type mutationResolver struct{ *Resolver }
type overlappingFieldsResolver struct{ *Resolver }
type panicsResolver struct{ *Resolver }
type primitiveResolver struct{ *Resolver }
Expand Down
12 changes: 12 additions & 0 deletions codegen/testserver/stub.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

16 changes: 10 additions & 6 deletions codegen/type.gotpl
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
{{- end }}
{{- if $type.IsPtr }}
res, err := ec.{{ $type.Elem.UnmarshalFunc }}(ctx, v)
return &res, err
return &res, graphql.WrapErrorWithInputPath(ctx, err)
{{- else if $type.IsSlice }}
var vSlice []interface{}
if v != nil {
Expand All @@ -19,27 +19,31 @@
var err error
res := make([]{{$type.GO.Elem | ref}}, len(vSlice))
for i := range vSlice {
ctx := graphql.WithFieldInputContext(ctx, graphql.NewFieldInputWithIndex(i))
res[i], err = ec.{{ $type.Elem.UnmarshalFunc }}(ctx, vSlice[i])
if err != nil {
return nil, err
return nil, graphql.WrapErrorWithInputPath(ctx, err)
}
}
return res, nil
{{- else }}
{{- if $type.Unmarshaler }}
{{- if $type.CastType }}
tmp, err := {{ $type.Unmarshaler | call }}(v)
return {{ $type.GO | ref }}(tmp), err
return {{ $type.GO | ref }}(tmp), graphql.WrapErrorWithInputPath(ctx, err)
{{- else}}
return {{ $type.Unmarshaler | call }}(v)
res, err := {{ $type.Unmarshaler | call }}(v)
return res, graphql.WrapErrorWithInputPath(ctx, err)
{{- end }}
{{- else if eq ($type.GO | ref) "map[string]interface{}" }}
return v.(map[string]interface{}), nil
{{- else if $type.IsMarshaler }}
var res {{ $type.GO | ref }}
return res, res.UnmarshalGQL(v)
err := res.UnmarshalGQL(v)
return res, graphql.WrapErrorWithInputPath(ctx, err)
{{- else }}
return ec.unmarshalInput{{ $type.GQL.Name }}(ctx, v)
res, err := ec.unmarshalInput{{ $type.GQL.Name }}(ctx, v)
return res, graphql.WrapErrorWithInputPath(ctx, err)
{{- end }}
{{- end }}
}
Expand Down
54 changes: 54 additions & 0 deletions docs/content/reference/scalars.md
Original file line number Diff line number Diff line change
Expand Up @@ -148,3 +148,57 @@ models:
```
See the [example/scalars](https://github.com/99designs/gqlgen/tree/master/example/scalars) package for more examples.
## Unmarshaling Errors
The errors that occur as part of custom scalar unmarshaling will return a full path to the field.
For example, given the following schema ...
```graphql
extend type Mutation{
updateUser(userInput: UserInput!): User!
}

input UserInput {
name: String!
primaryContactDetails: ContactDetailsInput!
secondaryContactDetails: ContactDetailsInput!
}

scalar Email
input ContactDetailsInput {
email: Email!
}
```

... and the following variables:

```json

{
"userInput": {
"name": "George",
"primaryContactDetails": {
"email": "not-an-email"
},
"secondaryContactDetails": {
"email": "[email protected]"
}
}
}
```

... and an unmarshal function that returns an error if the email is invalid. The mutation will return an error containing the full path:
```json
{
"message": "email invalid",
"path": [
"updateUser",
"userInput",
"primaryContactDetails",
"email"
]
}
```


Loading

0 comments on commit 1525cff

Please sign in to comment.