Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add input path in unmarshaling errors #1115

Merged
merged 8 commits into from
Jul 26, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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