diff --git a/codegen/templates/data.go b/codegen/templates/data.go index 3041b73a62e..554a13d77a3 100644 --- a/codegen/templates/data.go +++ b/codegen/templates/data.go @@ -3,7 +3,7 @@ package templates var data = map[string]string{ "args.gotpl": "\t{{- range $i, $arg := . }}\n\t\tvar arg{{$i}} {{$arg.Signature }}\n\t\tif tmp, ok := field.Args[{{$arg.GQLName|quote}}]; ok {\n\t\t\tvar err error\n\t\t\t{{$arg.Unmarshal (print \"arg\" $i) \"tmp\" }}\n\t\t\tif err != nil {\n\t\t\t\tec.Error(err)\n\t\t\t\t{{- if $arg.Object.Stream }}\n\t\t\t\t\treturn nil\n\t\t\t\t{{- else }}\n\t\t\t\t\treturn graphql.Null\n\t\t\t\t{{- end }}\n\t\t\t}\n\t\t} {{ if $arg.Default }} else {\n\t\t\tvar tmp interface{} = {{ $arg.Default | dump }}\n\t\t\tvar err error\n\t\t\t{{$arg.Unmarshal (print \"arg\" $i) \"tmp\" }}\n\t\t\tif err != nil {\n\t\t\t\tec.Error(err)\n\t\t\t\t{{- if $arg.Object.Stream }}\n\t\t\t\t\treturn nil\n\t\t\t\t{{- else }}\n\t\t\t\t\treturn graphql.Null\n\t\t\t\t{{- end }}\n\t\t\t}\n\t\t}\n\t\t{{end }}\n\t{{- end -}}\n", "field.gotpl": "{{ $field := . }}\n{{ $object := $field.Object }}\n\n{{- if $object.Stream }}\n\tfunc (ec *executionContext) _{{$object.GQLType}}_{{$field.GQLName}}(field graphql.CollectedField) func() graphql.Marshaler {\n\t\t{{- template \"args.gotpl\" $field.Args }}\n\t\tresults, err := ec.resolvers.{{ $object.GQLType }}_{{ $field.GQLName }}({{ $field.CallArgs }})\n\t\tif err != nil {\n\t\t\tec.Error(err)\n\t\t\treturn nil\n\t\t}\n\t\treturn func() graphql.Marshaler {\n\t\t\tres, ok := <-results\n\t\t\tif !ok {\n\t\t\t\treturn nil\n\t\t\t}\n\t\t\tvar out graphql.OrderedMap\n\t\t\tout.Add(field.Alias, func() graphql.Marshaler { {{ $field.WriteJson }} }())\n\t\t\treturn &out\n\t\t}\n\t}\n{{ else }}\n\tfunc (ec *executionContext) _{{$object.GQLType}}_{{$field.GQLName}}(field graphql.CollectedField, {{if not $object.Root}}obj *{{$object.FullName}}{{end}}) graphql.Marshaler {\n\t\t{{- template \"args.gotpl\" $field.Args }}\n\n\t\t{{- if $field.IsConcurrent }}\n\t\t\treturn graphql.Defer(func() (ret graphql.Marshaler) {\n\t\t\t\tdefer func() {\n\t\t\t\t\tif r := recover(); r != nil {\n\t\t\t\t\t\tuserErr := ec.recover(r)\n\t\t\t\t\t\tec.Error(userErr)\n\t\t\t\t\t\tret = graphql.Null\n\t\t\t\t\t}\n\t\t\t\t}()\n\t\t{{- end }}\n\n\t\t\t{{- if $field.GoVarName }}\n\t\t\t\tres := obj.{{$field.GoVarName}}\n\t\t\t{{- else if $field.GoMethodName }}\n\t\t\t\t{{- if $field.NoErr }}\n\t\t\t\t\tres := {{$field.GoMethodName}}({{ $field.CallArgs }})\n\t\t\t\t{{- else }}\n\t\t\t\t\tres, err := {{$field.GoMethodName}}({{ $field.CallArgs }})\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\tec.Error(err)\n\t\t\t\t\t\treturn graphql.Null\n\t\t\t\t\t}\n\t\t\t\t{{- end }}\n\t\t\t{{- else }}\n\t\t\t\tres, err := ec.resolvers.{{ $object.GQLType }}_{{ $field.GQLName }}({{ $field.CallArgs }})\n\t\t\t\tif err != nil {\n\t\t\t\t\tec.Error(err)\n\t\t\t\t\treturn graphql.Null\n\t\t\t\t}\n\t\t\t{{- end }}\n\t\t\t{{ $field.WriteJson }}\n\t\t{{- if $field.IsConcurrent }}\n\t\t\t})\n\t\t{{- end }}\n\t}\n{{ end }}\n", - "generated.gotpl": "// This file was generated by github.com/vektah/gqlgen, DO NOT EDIT\n\npackage {{ .PackageName }}\n\nimport (\n{{- range $import := .Imports }}\n\t{{- $import.Write }}\n{{ end }}\n)\n\nfunc MakeExecutableSchema(resolvers Resolvers) graphql.ExecutableSchema {\n\treturn &executableSchema{resolvers}\n}\n\ntype Resolvers interface {\n{{- range $object := .Objects -}}\n\t{{ range $field := $object.Fields -}}\n\t\t{{ $field.ResolverDeclaration }}\n\t{{ end }}\n{{- end }}\n}\n\ntype executableSchema struct {\n\tresolvers Resolvers\n}\n\nfunc (e *executableSchema) Schema() *schema.Schema {\n\treturn parsedSchema\n}\n\nfunc (e *executableSchema) Query(ctx context.Context, doc *query.Document, variables map[string]interface{}, op *query.Operation, recover graphql.RecoverFunc) *graphql.Response {\n\t{{- if .QueryRoot }}\n\t\tec := executionContext{resolvers: e.resolvers, variables: variables, doc: doc, ctx: ctx, recover: recover}\n\n\t\tdata := ec._{{.QueryRoot.GQLType}}(op.Selections)\n\t\tvar buf bytes.Buffer\n\t\tdata.MarshalGQL(&buf)\n\n\t\treturn &graphql.Response{\n\t\t\tData: buf.Bytes(),\n\t\t\tErrors: ec.Errors,\n\t\t}\n\t{{- else }}\n\t\treturn &graphql.Response{Errors: []*errors.QueryError{ {Message: \"queries are not supported\"} }}\n\t{{- end }}\n}\n\nfunc (e *executableSchema) Mutation(ctx context.Context, doc *query.Document, variables map[string]interface{}, op *query.Operation, recover graphql.RecoverFunc) *graphql.Response {\n\t{{- if .MutationRoot }}\n\t\tec := executionContext{resolvers: e.resolvers, variables: variables, doc: doc, ctx: ctx, recover: recover}\n\n\t\tdata := ec._{{.MutationRoot.GQLType}}(op.Selections)\n\t\tvar buf bytes.Buffer\n\t\tdata.MarshalGQL(&buf)\n\n\t\treturn &graphql.Response{\n\t\t\tData: buf.Bytes(),\n\t\t\tErrors: ec.Errors,\n\t\t}\n\t{{- else }}\n\t\treturn &graphql.Response{Errors: []*errors.QueryError{ {Message: \"mutations are not supported\"} }}\n\t{{- end }}\n}\n\nfunc (e *executableSchema) Subscription(ctx context.Context, doc *query.Document, variables map[string]interface{}, op *query.Operation, recover graphql.RecoverFunc) func() *graphql.Response {\n\t{{- if .SubscriptionRoot }}\n\t\tec := executionContext{resolvers: e.resolvers, variables: variables, doc: doc, ctx: ctx, recover: recover}\n\n\t\tnext := ec._{{.SubscriptionRoot.GQLType}}(op.Selections)\n\t\tif ec.Errors != nil {\n\t\t\treturn graphql.OneShot(&graphql.Response{Data: []byte(\"null\"), Errors: ec.Errors})\n\t\t}\n\n\t\tvar buf bytes.Buffer\n\t\treturn func() *graphql.Response {\n\t\t\tbuf.Reset()\n\t\t\tdata := next()\n\t\t\tif data == nil {\n\t\t\t\treturn nil\n\t\t\t}\n\t\t\tdata.MarshalGQL(&buf)\n\n\t\t\terrs := ec.Errors\n\t\t\tec.Errors = nil\n\t\t\treturn &graphql.Response{\n\t\t\t\tData: buf.Bytes(),\n\t\t\t\tErrors: errs,\n\t\t\t}\n\t\t}\n\t{{- else }}\n\t\treturn graphql.OneShot(&graphql.Response{Errors: []*errors.QueryError{ {Message: \"subscriptions are not supported\"} }})\n\t{{- end }}\n}\n\ntype executionContext struct {\n\terrors.Builder\n\tresolvers Resolvers\n\tvariables map[string]interface{}\n\tdoc *query.Document\n\tctx context.Context\n\trecover graphql.RecoverFunc\n}\n\n{{- range $object := .Objects }}\n\t{{ template \"object.gotpl\" $object }}\n\n\t{{- range $field := $object.Fields }}\n\t\t{{ template \"field.gotpl\" $field }}\n\t{{ end }}\n{{- end}}\n\n{{- range $interface := .Interfaces }}\n\t{{ template \"interface.gotpl\" $interface }}\n{{- end }}\n\n{{- range $input := .Inputs }}\n\t{{ template \"input.gotpl\" $input }}\n{{- end }}\n\nvar parsedSchema = schema.MustParse({{.SchemaRaw|quote}})\n\nfunc (ec *executionContext) introspectSchema() *introspection.Schema {\n\treturn introspection.WrapSchema(parsedSchema)\n}\n\nfunc (ec *executionContext) introspectType(name string) *introspection.Type {\n\tt := parsedSchema.Resolve(name)\n\tif t == nil {\n\t\treturn nil\n\t}\n\treturn introspection.WrapType(t)\n}\n", + "generated.gotpl": "// This file was generated by github.com/vektah/gqlgen, DO NOT EDIT\n\npackage {{ .PackageName }}\n\nimport (\n{{- range $import := .Imports }}\n\t{{- $import.Write }}\n{{ end }}\n)\n\nfunc MakeExecutableSchema(resolvers Resolvers, opts ...ExecutableOption) graphql.ExecutableSchema {\n\tret := &executableSchema{resolvers: resolvers}\n\tfor _, opt := range opts {\n\t\topt(ret)\n\t}\n\treturn ret\n}\n\ntype Resolvers interface {\n{{- range $object := .Objects -}}\n\t{{ range $field := $object.Fields -}}\n\t\t{{ $field.ResolverDeclaration }}\n\t{{ end }}\n{{- end }}\n}\n\ntype ExecutableOption func(*executableSchema)\n\nfunc WithErrorConverter(fn func(error) string) ExecutableOption {\n\treturn func(s *executableSchema) {\n\t\ts.errorMessageFn = fn\n\t}\n}\n\ntype executableSchema struct {\n\tresolvers Resolvers\n\terrorMessageFn func(error) string\n}\n\nfunc (e *executableSchema) Schema() *schema.Schema {\n\treturn parsedSchema\n}\n\nfunc (e *executableSchema) Query(ctx context.Context, doc *query.Document, variables map[string]interface{}, op *query.Operation, recover graphql.RecoverFunc) *graphql.Response {\n\t{{- if .QueryRoot }}\n\t\tec := e.makeExecutionContext(ctx, doc, variables, recover)\n\n\t\tdata := ec._{{.QueryRoot.GQLType}}(op.Selections)\n\t\tvar buf bytes.Buffer\n\t\tdata.MarshalGQL(&buf)\n\n\t\treturn &graphql.Response{\n\t\t\tData: buf.Bytes(),\n\t\t\tErrors: ec.Errors,\n\t\t}\n\t{{- else }}\n\t\treturn &graphql.Response{Errors: []*errors.QueryError{ {Message: \"queries are not supported\"} }}\n\t{{- end }}\n}\n\nfunc (e *executableSchema) Mutation(ctx context.Context, doc *query.Document, variables map[string]interface{}, op *query.Operation, recover graphql.RecoverFunc) *graphql.Response {\n\t{{- if .MutationRoot }}\n\t\tec := e.makeExecutionContext(ctx, doc, variables, recover)\n\n\t\tdata := ec._{{.MutationRoot.GQLType}}(op.Selections)\n\t\tvar buf bytes.Buffer\n\t\tdata.MarshalGQL(&buf)\n\n\t\treturn &graphql.Response{\n\t\t\tData: buf.Bytes(),\n\t\t\tErrors: ec.Errors,\n\t\t}\n\t{{- else }}\n\t\treturn &graphql.Response{Errors: []*errors.QueryError{ {Message: \"mutations are not supported\"} }}\n\t{{- end }}\n}\n\nfunc (e *executableSchema) Subscription(ctx context.Context, doc *query.Document, variables map[string]interface{}, op *query.Operation, recover graphql.RecoverFunc) func() *graphql.Response {\n\t{{- if .SubscriptionRoot }}\n\t\tec := e.makeExecutionContext(ctx, doc, variables, recover)\n\n\t\tnext := ec._{{.SubscriptionRoot.GQLType}}(op.Selections)\n\t\tif ec.Errors != nil {\n\t\t\treturn graphql.OneShot(&graphql.Response{Data: []byte(\"null\"), Errors: ec.Errors})\n\t\t}\n\n\t\tvar buf bytes.Buffer\n\t\treturn func() *graphql.Response {\n\t\t\tbuf.Reset()\n\t\t\tdata := next()\n\t\t\tif data == nil {\n\t\t\t\treturn nil\n\t\t\t}\n\t\t\tdata.MarshalGQL(&buf)\n\n\t\t\terrs := ec.Errors\n\t\t\tec.Errors = nil\n\t\t\treturn &graphql.Response{\n\t\t\t\tData: buf.Bytes(),\n\t\t\t\tErrors: errs,\n\t\t\t}\n\t\t}\n\t{{- else }}\n\t\treturn graphql.OneShot(&graphql.Response{Errors: []*errors.QueryError{ {Message: \"subscriptions are not supported\"} }})\n\t{{- end }}\n}\n\nfunc (e *executableSchema) makeExecutionContext(ctx context.Context, doc *query.Document, variables map[string]interface{}, recover graphql.RecoverFunc) *executionContext {\n\terrBuilder := errors.Builder{ErrorMessageFn: e.errorMessageFn}\n\treturn &executionContext{\n\t\tBuilder: errBuilder, resolvers: e.resolvers, variables: variables, doc: doc, ctx: ctx, recover: recover,\n\t}\n}\n\ntype executionContext struct {\n\terrors.Builder\n\tresolvers Resolvers\n\tvariables map[string]interface{}\n\tdoc *query.Document\n\tctx context.Context\n\trecover graphql.RecoverFunc\n}\n\n{{- range $object := .Objects }}\n\t{{ template \"object.gotpl\" $object }}\n\n\t{{- range $field := $object.Fields }}\n\t\t{{ template \"field.gotpl\" $field }}\n\t{{ end }}\n{{- end}}\n\n{{- range $interface := .Interfaces }}\n\t{{ template \"interface.gotpl\" $interface }}\n{{- end }}\n\n{{- range $input := .Inputs }}\n\t{{ template \"input.gotpl\" $input }}\n{{- end }}\n\nvar parsedSchema = schema.MustParse({{.SchemaRaw|quote}})\n\nfunc (ec *executionContext) introspectSchema() *introspection.Schema {\n\treturn introspection.WrapSchema(parsedSchema)\n}\n\nfunc (ec *executionContext) introspectType(name string) *introspection.Type {\n\tt := parsedSchema.Resolve(name)\n\tif t == nil {\n\t\treturn nil\n\t}\n\treturn introspection.WrapType(t)\n}\n", "input.gotpl": "\t{{- if .IsMarshaled }}\n\tfunc Unmarshal{{ .GQLType }}(v interface{}) ({{.FullName}}, error) {\n\t\tvar it {{.FullName}}\n\n\t\tfor k, v := range v.(map[string]interface{}) {\n\t\t\tswitch k {\n\t\t\t{{- range $field := .Fields }}\n\t\t\tcase {{$field.GQLName|quote}}:\n\t\t\t\tvar err error\n\t\t\t\t{{ $field.Unmarshal (print \"it.\" $field.GoVarName) \"v\" }}\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn it, err\n\t\t\t\t}\n\t\t\t{{- end }}\n\t\t\t}\n\t\t}\n\n\t\treturn it, nil\n\t}\n\t{{- end }}\n", "interface.gotpl": "{{- $interface := . }}\n\nfunc (ec *executionContext) _{{$interface.GQLType}}(sel []query.Selection, obj *{{$interface.FullName}}) graphql.Marshaler {\n\tswitch obj := (*obj).(type) {\n\tcase nil:\n\t\treturn graphql.Null\n\t{{- range $implementor := $interface.Implementors }}\n\t\t{{- if $implementor.ValueReceiver }}\n\t\t\tcase {{$implementor.FullName}}:\n\t\t\t\treturn ec._{{$implementor.GQLType}}(sel, &obj)\n\t\t{{- end}}\n\t\tcase *{{$implementor.FullName}}:\n\t\t\treturn ec._{{$implementor.GQLType}}(sel, obj)\n\t{{- end }}\n\tdefault:\n\t\tpanic(fmt.Errorf(\"unexpected type %T\", obj))\n\t}\n}\n", "models.gotpl": "// This file was generated by github.com/vektah/gqlgen, DO NOT EDIT\n\npackage {{ .PackageName }}\n\nimport (\n{{- range $import := .Imports }}\n\t{{- $import.Write }}\n{{ end }}\n)\n\n{{ range $model := .Models }}\n\t{{- if .IsInterface }}\n\t\ttype {{.GoType}} interface {}\n\t{{- else }}\n\t\ttype {{.GoType}} struct {\n\t\t\t{{- range $field := .Fields }}\n\t\t\t\t{{- if $field.GoVarName }}\n\t\t\t\t\t{{ $field.GoVarName }} {{$field.Signature}}\n\t\t\t\t{{- else }}\n\t\t\t\t\t{{ $field.GoFKName }} {{$field.GoFKType}}\n\t\t\t\t{{- end }}\n\t\t\t{{- end }}\n\t\t}\n\t{{- end }}\n{{- end}}\n", diff --git a/codegen/templates/generated.gotpl b/codegen/templates/generated.gotpl index cb9854c1c1d..4babc19e21f 100644 --- a/codegen/templates/generated.gotpl +++ b/codegen/templates/generated.gotpl @@ -8,8 +8,12 @@ import ( {{ end }} ) -func MakeExecutableSchema(resolvers Resolvers) graphql.ExecutableSchema { - return &executableSchema{resolvers} +func MakeExecutableSchema(resolvers Resolvers, opts ...ExecutableOption) graphql.ExecutableSchema { + ret := &executableSchema{resolvers: resolvers} + for _, opt := range opts { + opt(ret) + } + return ret } type Resolvers interface { @@ -20,8 +24,17 @@ type Resolvers interface { {{- end }} } +type ExecutableOption func(*executableSchema) + +func WithErrorConverter(fn func(error) string) ExecutableOption { + return func(s *executableSchema) { + s.errorMessageFn = fn + } +} + type executableSchema struct { - resolvers Resolvers + resolvers Resolvers + errorMessageFn func(error) string } func (e *executableSchema) Schema() *schema.Schema { @@ -30,7 +43,7 @@ func (e *executableSchema) Schema() *schema.Schema { func (e *executableSchema) Query(ctx context.Context, doc *query.Document, variables map[string]interface{}, op *query.Operation, recover graphql.RecoverFunc) *graphql.Response { {{- if .QueryRoot }} - ec := executionContext{resolvers: e.resolvers, variables: variables, doc: doc, ctx: ctx, recover: recover} + ec := e.makeExecutionContext(ctx, doc, variables, recover) data := ec._{{.QueryRoot.GQLType}}(op.Selections) var buf bytes.Buffer @@ -47,7 +60,7 @@ func (e *executableSchema) Query(ctx context.Context, doc *query.Document, varia func (e *executableSchema) Mutation(ctx context.Context, doc *query.Document, variables map[string]interface{}, op *query.Operation, recover graphql.RecoverFunc) *graphql.Response { {{- if .MutationRoot }} - ec := executionContext{resolvers: e.resolvers, variables: variables, doc: doc, ctx: ctx, recover: recover} + ec := e.makeExecutionContext(ctx, doc, variables, recover) data := ec._{{.MutationRoot.GQLType}}(op.Selections) var buf bytes.Buffer @@ -64,7 +77,7 @@ func (e *executableSchema) Mutation(ctx context.Context, doc *query.Document, va func (e *executableSchema) Subscription(ctx context.Context, doc *query.Document, variables map[string]interface{}, op *query.Operation, recover graphql.RecoverFunc) func() *graphql.Response { {{- if .SubscriptionRoot }} - ec := executionContext{resolvers: e.resolvers, variables: variables, doc: doc, ctx: ctx, recover: recover} + ec := e.makeExecutionContext(ctx, doc, variables, recover) next := ec._{{.SubscriptionRoot.GQLType}}(op.Selections) if ec.Errors != nil { @@ -92,6 +105,13 @@ func (e *executableSchema) Subscription(ctx context.Context, doc *query.Document {{- end }} } +func (e *executableSchema) makeExecutionContext(ctx context.Context, doc *query.Document, variables map[string]interface{}, recover graphql.RecoverFunc) *executionContext { + errBuilder := errors.Builder{ErrorMessageFn: e.errorMessageFn} + return &executionContext{ + Builder: errBuilder, resolvers: e.resolvers, variables: variables, doc: doc, ctx: ctx, recover: recover, + } +} + type executionContext struct { errors.Builder resolvers Resolvers diff --git a/example/chat/generated.go b/example/chat/generated.go index ca2e85d677a..59e916d67d2 100644 --- a/example/chat/generated.go +++ b/example/chat/generated.go @@ -14,8 +14,12 @@ import ( schema "github.com/vektah/gqlgen/neelance/schema" ) -func MakeExecutableSchema(resolvers Resolvers) graphql.ExecutableSchema { - return &executableSchema{resolvers} +func MakeExecutableSchema(resolvers Resolvers, opts ...ExecutableOption) graphql.ExecutableSchema { + ret := &executableSchema{resolvers: resolvers} + for _, opt := range opts { + opt(ret) + } + return ret } type Resolvers interface { @@ -25,8 +29,17 @@ type Resolvers interface { Subscription_messageAdded(ctx context.Context, roomName string) (<-chan Message, error) } +type ExecutableOption func(*executableSchema) + +func WithErrorConverter(fn func(error) string) ExecutableOption { + return func(s *executableSchema) { + s.errorMessageFn = fn + } +} + type executableSchema struct { - resolvers Resolvers + resolvers Resolvers + errorMessageFn func(error) string } func (e *executableSchema) Schema() *schema.Schema { @@ -34,7 +47,7 @@ func (e *executableSchema) Schema() *schema.Schema { } func (e *executableSchema) Query(ctx context.Context, doc *query.Document, variables map[string]interface{}, op *query.Operation, recover graphql.RecoverFunc) *graphql.Response { - ec := executionContext{resolvers: e.resolvers, variables: variables, doc: doc, ctx: ctx, recover: recover} + ec := e.makeExecutionContext(ctx, doc, variables, recover) data := ec._Query(op.Selections) var buf bytes.Buffer @@ -47,7 +60,7 @@ func (e *executableSchema) Query(ctx context.Context, doc *query.Document, varia } func (e *executableSchema) Mutation(ctx context.Context, doc *query.Document, variables map[string]interface{}, op *query.Operation, recover graphql.RecoverFunc) *graphql.Response { - ec := executionContext{resolvers: e.resolvers, variables: variables, doc: doc, ctx: ctx, recover: recover} + ec := e.makeExecutionContext(ctx, doc, variables, recover) data := ec._Mutation(op.Selections) var buf bytes.Buffer @@ -60,7 +73,7 @@ func (e *executableSchema) Mutation(ctx context.Context, doc *query.Document, va } func (e *executableSchema) Subscription(ctx context.Context, doc *query.Document, variables map[string]interface{}, op *query.Operation, recover graphql.RecoverFunc) func() *graphql.Response { - ec := executionContext{resolvers: e.resolvers, variables: variables, doc: doc, ctx: ctx, recover: recover} + ec := e.makeExecutionContext(ctx, doc, variables, recover) next := ec._Subscription(op.Selections) if ec.Errors != nil { @@ -85,6 +98,13 @@ func (e *executableSchema) Subscription(ctx context.Context, doc *query.Document } } +func (e *executableSchema) makeExecutionContext(ctx context.Context, doc *query.Document, variables map[string]interface{}, recover graphql.RecoverFunc) *executionContext { + errBuilder := errors.Builder{ErrorMessageFn: e.errorMessageFn} + return &executionContext{ + Builder: errBuilder, resolvers: e.resolvers, variables: variables, doc: doc, ctx: ctx, recover: recover, + } +} + type executionContext struct { errors.Builder resolvers Resolvers diff --git a/example/dataloader/generated.go b/example/dataloader/generated.go index 0c9cbf6bc8e..6cf544453cd 100644 --- a/example/dataloader/generated.go +++ b/example/dataloader/generated.go @@ -14,8 +14,12 @@ import ( schema "github.com/vektah/gqlgen/neelance/schema" ) -func MakeExecutableSchema(resolvers Resolvers) graphql.ExecutableSchema { - return &executableSchema{resolvers} +func MakeExecutableSchema(resolvers Resolvers, opts ...ExecutableOption) graphql.ExecutableSchema { + ret := &executableSchema{resolvers: resolvers} + for _, opt := range opts { + opt(ret) + } + return ret } type Resolvers interface { @@ -27,8 +31,17 @@ type Resolvers interface { Query_torture(ctx context.Context, customerIds [][]int) ([][]Customer, error) } +type ExecutableOption func(*executableSchema) + +func WithErrorConverter(fn func(error) string) ExecutableOption { + return func(s *executableSchema) { + s.errorMessageFn = fn + } +} + type executableSchema struct { - resolvers Resolvers + resolvers Resolvers + errorMessageFn func(error) string } func (e *executableSchema) Schema() *schema.Schema { @@ -36,7 +49,7 @@ func (e *executableSchema) Schema() *schema.Schema { } func (e *executableSchema) Query(ctx context.Context, doc *query.Document, variables map[string]interface{}, op *query.Operation, recover graphql.RecoverFunc) *graphql.Response { - ec := executionContext{resolvers: e.resolvers, variables: variables, doc: doc, ctx: ctx, recover: recover} + ec := e.makeExecutionContext(ctx, doc, variables, recover) data := ec._Query(op.Selections) var buf bytes.Buffer @@ -56,6 +69,13 @@ func (e *executableSchema) Subscription(ctx context.Context, doc *query.Document return graphql.OneShot(&graphql.Response{Errors: []*errors.QueryError{{Message: "subscriptions are not supported"}}}) } +func (e *executableSchema) makeExecutionContext(ctx context.Context, doc *query.Document, variables map[string]interface{}, recover graphql.RecoverFunc) *executionContext { + errBuilder := errors.Builder{ErrorMessageFn: e.errorMessageFn} + return &executionContext{ + Builder: errBuilder, resolvers: e.resolvers, variables: variables, doc: doc, ctx: ctx, recover: recover, + } +} + type executionContext struct { errors.Builder resolvers Resolvers diff --git a/example/scalars/generated.go b/example/scalars/generated.go index 04b74f770e8..8fbd8fe9d6f 100644 --- a/example/scalars/generated.go +++ b/example/scalars/generated.go @@ -15,8 +15,12 @@ import ( schema "github.com/vektah/gqlgen/neelance/schema" ) -func MakeExecutableSchema(resolvers Resolvers) graphql.ExecutableSchema { - return &executableSchema{resolvers} +func MakeExecutableSchema(resolvers Resolvers, opts ...ExecutableOption) graphql.ExecutableSchema { + ret := &executableSchema{resolvers: resolvers} + for _, opt := range opts { + opt(ret) + } + return ret } type Resolvers interface { @@ -24,8 +28,17 @@ type Resolvers interface { Query_search(ctx context.Context, input SearchArgs) ([]User, error) } +type ExecutableOption func(*executableSchema) + +func WithErrorConverter(fn func(error) string) ExecutableOption { + return func(s *executableSchema) { + s.errorMessageFn = fn + } +} + type executableSchema struct { - resolvers Resolvers + resolvers Resolvers + errorMessageFn func(error) string } func (e *executableSchema) Schema() *schema.Schema { @@ -33,7 +46,7 @@ func (e *executableSchema) Schema() *schema.Schema { } func (e *executableSchema) Query(ctx context.Context, doc *query.Document, variables map[string]interface{}, op *query.Operation, recover graphql.RecoverFunc) *graphql.Response { - ec := executionContext{resolvers: e.resolvers, variables: variables, doc: doc, ctx: ctx, recover: recover} + ec := e.makeExecutionContext(ctx, doc, variables, recover) data := ec._Query(op.Selections) var buf bytes.Buffer @@ -53,6 +66,13 @@ func (e *executableSchema) Subscription(ctx context.Context, doc *query.Document return graphql.OneShot(&graphql.Response{Errors: []*errors.QueryError{{Message: "subscriptions are not supported"}}}) } +func (e *executableSchema) makeExecutionContext(ctx context.Context, doc *query.Document, variables map[string]interface{}, recover graphql.RecoverFunc) *executionContext { + errBuilder := errors.Builder{ErrorMessageFn: e.errorMessageFn} + return &executionContext{ + Builder: errBuilder, resolvers: e.resolvers, variables: variables, doc: doc, ctx: ctx, recover: recover, + } +} + type executionContext struct { errors.Builder resolvers Resolvers diff --git a/example/starwars/generated.go b/example/starwars/generated.go index 36d23172083..53b502249f5 100644 --- a/example/starwars/generated.go +++ b/example/starwars/generated.go @@ -16,8 +16,12 @@ import ( schema "github.com/vektah/gqlgen/neelance/schema" ) -func MakeExecutableSchema(resolvers Resolvers) graphql.ExecutableSchema { - return &executableSchema{resolvers} +func MakeExecutableSchema(resolvers Resolvers, opts ...ExecutableOption) graphql.ExecutableSchema { + ret := &executableSchema{resolvers: resolvers} + for _, opt := range opts { + opt(ret) + } + return ret } type Resolvers interface { @@ -42,8 +46,17 @@ type Resolvers interface { Query_starship(ctx context.Context, id string) (*Starship, error) } +type ExecutableOption func(*executableSchema) + +func WithErrorConverter(fn func(error) string) ExecutableOption { + return func(s *executableSchema) { + s.errorMessageFn = fn + } +} + type executableSchema struct { - resolvers Resolvers + resolvers Resolvers + errorMessageFn func(error) string } func (e *executableSchema) Schema() *schema.Schema { @@ -51,7 +64,7 @@ func (e *executableSchema) Schema() *schema.Schema { } func (e *executableSchema) Query(ctx context.Context, doc *query.Document, variables map[string]interface{}, op *query.Operation, recover graphql.RecoverFunc) *graphql.Response { - ec := executionContext{resolvers: e.resolvers, variables: variables, doc: doc, ctx: ctx, recover: recover} + ec := e.makeExecutionContext(ctx, doc, variables, recover) data := ec._Query(op.Selections) var buf bytes.Buffer @@ -64,7 +77,7 @@ func (e *executableSchema) Query(ctx context.Context, doc *query.Document, varia } func (e *executableSchema) Mutation(ctx context.Context, doc *query.Document, variables map[string]interface{}, op *query.Operation, recover graphql.RecoverFunc) *graphql.Response { - ec := executionContext{resolvers: e.resolvers, variables: variables, doc: doc, ctx: ctx, recover: recover} + ec := e.makeExecutionContext(ctx, doc, variables, recover) data := ec._Mutation(op.Selections) var buf bytes.Buffer @@ -80,6 +93,13 @@ func (e *executableSchema) Subscription(ctx context.Context, doc *query.Document return graphql.OneShot(&graphql.Response{Errors: []*errors.QueryError{{Message: "subscriptions are not supported"}}}) } +func (e *executableSchema) makeExecutionContext(ctx context.Context, doc *query.Document, variables map[string]interface{}, recover graphql.RecoverFunc) *executionContext { + errBuilder := errors.Builder{ErrorMessageFn: e.errorMessageFn} + return &executionContext{ + Builder: errBuilder, resolvers: e.resolvers, variables: variables, doc: doc, ctx: ctx, recover: recover, + } +} + type executionContext struct { errors.Builder resolvers Resolvers diff --git a/example/todo/generated.go b/example/todo/generated.go index b54977fdc92..597ee7b2a1f 100644 --- a/example/todo/generated.go +++ b/example/todo/generated.go @@ -14,8 +14,12 @@ import ( schema "github.com/vektah/gqlgen/neelance/schema" ) -func MakeExecutableSchema(resolvers Resolvers) graphql.ExecutableSchema { - return &executableSchema{resolvers} +func MakeExecutableSchema(resolvers Resolvers, opts ...ExecutableOption) graphql.ExecutableSchema { + ret := &executableSchema{resolvers: resolvers} + for _, opt := range opts { + opt(ret) + } + return ret } type Resolvers interface { @@ -26,8 +30,17 @@ type Resolvers interface { MyQuery_todos(ctx context.Context) ([]Todo, error) } +type ExecutableOption func(*executableSchema) + +func WithErrorConverter(fn func(error) string) ExecutableOption { + return func(s *executableSchema) { + s.errorMessageFn = fn + } +} + type executableSchema struct { - resolvers Resolvers + resolvers Resolvers + errorMessageFn func(error) string } func (e *executableSchema) Schema() *schema.Schema { @@ -35,7 +48,7 @@ func (e *executableSchema) Schema() *schema.Schema { } func (e *executableSchema) Query(ctx context.Context, doc *query.Document, variables map[string]interface{}, op *query.Operation, recover graphql.RecoverFunc) *graphql.Response { - ec := executionContext{resolvers: e.resolvers, variables: variables, doc: doc, ctx: ctx, recover: recover} + ec := e.makeExecutionContext(ctx, doc, variables, recover) data := ec._MyQuery(op.Selections) var buf bytes.Buffer @@ -48,7 +61,7 @@ func (e *executableSchema) Query(ctx context.Context, doc *query.Document, varia } func (e *executableSchema) Mutation(ctx context.Context, doc *query.Document, variables map[string]interface{}, op *query.Operation, recover graphql.RecoverFunc) *graphql.Response { - ec := executionContext{resolvers: e.resolvers, variables: variables, doc: doc, ctx: ctx, recover: recover} + ec := e.makeExecutionContext(ctx, doc, variables, recover) data := ec._MyMutation(op.Selections) var buf bytes.Buffer @@ -64,6 +77,13 @@ func (e *executableSchema) Subscription(ctx context.Context, doc *query.Document return graphql.OneShot(&graphql.Response{Errors: []*errors.QueryError{{Message: "subscriptions are not supported"}}}) } +func (e *executableSchema) makeExecutionContext(ctx context.Context, doc *query.Document, variables map[string]interface{}, recover graphql.RecoverFunc) *executionContext { + errBuilder := errors.Builder{ErrorMessageFn: e.errorMessageFn} + return &executionContext{ + Builder: errBuilder, resolvers: e.resolvers, variables: variables, doc: doc, ctx: ctx, recover: recover, + } +} + type executionContext struct { errors.Builder resolvers Resolvers diff --git a/neelance/errors/errors.go b/neelance/errors/errors.go index d4a4d3275ff..a442b8b96d3 100644 --- a/neelance/errors/errors.go +++ b/neelance/errors/errors.go @@ -27,6 +27,15 @@ func Errorf(format string, a ...interface{}) *QueryError { } } +// WithMessagef is the same as Errorf, except it will store the err inside +// the ResolverError field. +func WithMessagef(err error, format string, a ...interface{}) *QueryError { + return &QueryError{ + Message: fmt.Sprintf(format, a...), + ResolverError: err, + } +} + func (err *QueryError) Error() string { if err == nil { return "" @@ -42,6 +51,11 @@ var _ error = &QueryError{} type Builder struct { Errors []*QueryError + // ErrorMessageFn will be used to generate the error + // message from errors given to Error(). + // + // If ErrorMessageFn is nil, err.Error() will be used. + ErrorMessageFn func(error) string } func (c *Builder) Errorf(format string, args ...interface{}) { @@ -49,5 +63,11 @@ func (c *Builder) Errorf(format string, args ...interface{}) { } func (c *Builder) Error(err error) { - c.Errors = append(c.Errors, Errorf("%s", err.Error())) + var gqlErrMessage string + if c.ErrorMessageFn != nil { + gqlErrMessage = c.ErrorMessageFn(err) + } else { + gqlErrMessage = err.Error() + } + c.Errors = append(c.Errors, WithMessagef(err, gqlErrMessage)) } diff --git a/neelance/errors/errors_test.go b/neelance/errors/errors_test.go new file mode 100644 index 00000000000..698de10ef98 --- /dev/null +++ b/neelance/errors/errors_test.go @@ -0,0 +1,63 @@ +package errors + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestBuilder_Error(t *testing.T) { + t.Run("with err converter", func(t *testing.T) { + b := Builder{ErrorMessageFn: convertErr} + b.Error(&testErr{"err1"}) + b.Error(&publicErr{ + message: "err2", + public: "err2 public", + }) + + require.Len(t, b.Errors, 2) + assert.EqualError(t, b.Errors[0], "graphql: err1") + assert.EqualError(t, b.Errors[1], "graphql: err2 public") + }) + t.Run("without err converter", func(t *testing.T) { + var b Builder + b.Error(&testErr{"err1"}) + b.Error(&publicErr{ + message: "err2", + public: "err2 public", + }) + + require.Len(t, b.Errors, 2) + assert.EqualError(t, b.Errors[0], "graphql: err1") + assert.EqualError(t, b.Errors[1], "graphql: err2") + }) +} + +type testErr struct { + message string +} + +func (err *testErr) Error() string { + return err.message +} + +type publicErr struct { + message string + public string +} + +func (err *publicErr) Error() string { + return err.message +} + +func (err *publicErr) PublicError() string { + return err.public +} + +func convertErr(err error) string { + if errConv, ok := err.(*publicErr); ok { + return errConv.public + } + return err.Error() +} diff --git a/test/generated.go b/test/generated.go index 63c7f7e13ab..c31abb01afb 100644 --- a/test/generated.go +++ b/test/generated.go @@ -16,8 +16,12 @@ import ( introspection1 "github.com/vektah/gqlgen/test/introspection" ) -func MakeExecutableSchema(resolvers Resolvers) graphql.ExecutableSchema { - return &executableSchema{resolvers} +func MakeExecutableSchema(resolvers Resolvers, opts ...ExecutableOption) graphql.ExecutableSchema { + ret := &executableSchema{resolvers: resolvers} + for _, opt := range opts { + opt(ret) + } + return ret } type Resolvers interface { @@ -30,8 +34,17 @@ type Resolvers interface { Query_collision(ctx context.Context) (*introspection1.It, error) } +type ExecutableOption func(*executableSchema) + +func WithErrorConverter(fn func(error) string) ExecutableOption { + return func(s *executableSchema) { + s.errorMessageFn = fn + } +} + type executableSchema struct { - resolvers Resolvers + resolvers Resolvers + errorMessageFn func(error) string } func (e *executableSchema) Schema() *schema.Schema { @@ -39,7 +52,7 @@ func (e *executableSchema) Schema() *schema.Schema { } func (e *executableSchema) Query(ctx context.Context, doc *query.Document, variables map[string]interface{}, op *query.Operation, recover graphql.RecoverFunc) *graphql.Response { - ec := executionContext{resolvers: e.resolvers, variables: variables, doc: doc, ctx: ctx, recover: recover} + ec := e.makeExecutionContext(ctx, doc, variables, recover) data := ec._Query(op.Selections) var buf bytes.Buffer @@ -59,6 +72,13 @@ func (e *executableSchema) Subscription(ctx context.Context, doc *query.Document return graphql.OneShot(&graphql.Response{Errors: []*errors.QueryError{{Message: "subscriptions are not supported"}}}) } +func (e *executableSchema) makeExecutionContext(ctx context.Context, doc *query.Document, variables map[string]interface{}, recover graphql.RecoverFunc) *executionContext { + errBuilder := errors.Builder{ErrorMessageFn: e.errorMessageFn} + return &executionContext{ + Builder: errBuilder, resolvers: e.resolvers, variables: variables, doc: doc, ctx: ctx, recover: recover, + } +} + type executionContext struct { errors.Builder resolvers Resolvers diff --git a/test/resolvers_test.go b/test/resolvers_test.go index a79eb96fb82..55510566ef1 100644 --- a/test/resolvers_test.go +++ b/test/resolvers_test.go @@ -2,6 +2,117 @@ package test -import "testing" +import ( + "context" + "fmt" + "testing" + + "github.com/pkg/errors" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/vektah/gqlgen/neelance/query" + "github.com/vektah/gqlgen/test/introspection" +) func TestCompiles(t *testing.T) {} + +func TestErrorConverter(t *testing.T) { + t.Run("with", func(t *testing.T) { + testConvErr := func(e error) string { + if _, ok := errors.Cause(e).(*specialErr); ok { + return "override special error message" + } + return e.Error() + } + t.Run("special error", func(t *testing.T) { + s := MakeExecutableSchema(&testResolvers{ + nestedOutputsErr: &specialErr{}, + }, WithErrorConverter(testConvErr)) + ctx := context.Background() + doc, errs := query.Parse(`query { nestedOutputs { inner { id } } } `) + require.Nil(t, errs) + resp := s.Query(ctx, doc, nil, doc.Operations[0], nil) + require.Len(t, resp.Errors, 1) + assert.Equal(t, "override special error message", resp.Errors[0].Message) + }) + t.Run("normal error", func(t *testing.T) { + s := MakeExecutableSchema(&testResolvers{ + nestedOutputsErr: fmt.Errorf("a normal error"), + }, WithErrorConverter(testConvErr)) + ctx := context.Background() + doc, errs := query.Parse(`query { nestedOutputs { inner { id } } } `) + require.Nil(t, errs) + resp := s.Query(ctx, doc, nil, doc.Operations[0], nil) + require.Len(t, resp.Errors, 1) + assert.Equal(t, "a normal error", resp.Errors[0].Message) + }) + }) + + t.Run("without", func(t *testing.T) { + t.Run("special error", func(t *testing.T) { + s := MakeExecutableSchema(&testResolvers{ + nestedOutputsErr: &specialErr{}, + }) + ctx := context.Background() + doc, errs := query.Parse(`query { nestedOutputs { inner { id } } } `) + require.Nil(t, errs) + resp := s.Query(ctx, doc, nil, doc.Operations[0], nil) + require.Len(t, resp.Errors, 1) + assert.Equal(t, "original special error message", resp.Errors[0].Message) + }) + t.Run("normal error", func(t *testing.T) { + s := MakeExecutableSchema(&testResolvers{ + nestedOutputsErr: fmt.Errorf("a normal error"), + }) + ctx := context.Background() + doc, errs := query.Parse(`query { nestedOutputs { inner { id } } } `) + require.Nil(t, errs) + resp := s.Query(ctx, doc, nil, doc.Operations[0], nil) + require.Len(t, resp.Errors, 1) + assert.Equal(t, "a normal error", resp.Errors[0].Message) + }) + }) +} + +type testResolvers struct { + inner InnerObject + innerErr error + nestedInputs *bool + nestedInputsErr error + nestedOutputs [][]OuterObject + nestedOutputsErr error +} + +func (r *testResolvers) Query_shapes(ctx context.Context) ([]Shape, error) { + panic("implement me") +} + +func (r *testResolvers) Query_recursive(ctx context.Context, input *RecursiveInputSlice) (*bool, error) { + panic("implement me") +} + +func (r *testResolvers) Query_mapInput(ctx context.Context, input *map[string]interface{}) (*bool, error) { + panic("implement me") +} + +func (r *testResolvers) Query_collision(ctx context.Context) (*introspection.It, error) { + panic("implement me") +} + +func (r *testResolvers) OuterObject_inner(ctx context.Context, obj *OuterObject) (InnerObject, error) { + return r.inner, r.innerErr +} + +func (r *testResolvers) Query_nestedInputs(ctx context.Context, input [][]OuterInput) (*bool, error) { + return r.nestedInputs, r.nestedInputsErr +} + +func (r *testResolvers) Query_nestedOutputs(ctx context.Context) ([][]OuterObject, error) { + return r.nestedOutputs, r.nestedOutputsErr +} + +type specialErr struct{} + +func (*specialErr) Error() string { + return "original special error message" +}