diff --git a/example/caching/server/server.go b/example/caching/server/server.go index 4ede246f7..f721c996c 100644 --- a/example/caching/server/server.go +++ b/example/caching/server/server.go @@ -9,6 +9,7 @@ import ( "github.com/graph-gophers/graphql-go" "github.com/graph-gophers/graphql-go/example/caching" "github.com/graph-gophers/graphql-go/example/caching/cache" + "github.com/graph-gophers/graphql-go/pkg/common" ) var schema *graphql.Schema @@ -40,12 +41,12 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { var hint *cache.Hint if cacheable(r) { ctx, hints, done := cache.Hintable(r.Context()) - response = h.Schema.Exec(ctx, p.Query, p.OperationName, p.Variables) + response = h.Schema.Exec(ctx, p.Query, p.OperationName, p.Variables, map[string]common.DirectiveVisitor{}) done() v := <-hints hint = &v } else { - response = h.Schema.Exec(r.Context(), p.Query, p.OperationName, p.Variables) + response = h.Schema.Exec(r.Context(), p.Query, p.OperationName, p.Variables, map[string]common.DirectiveVisitor{}) } responseJSON, err := json.Marshal(response) if err != nil { diff --git a/gqltesting/testing.go b/gqltesting/testing.go index f8b6c9dcb..cec43036a 100644 --- a/gqltesting/testing.go +++ b/gqltesting/testing.go @@ -12,18 +12,20 @@ import ( graphql "github.com/graph-gophers/graphql-go" "github.com/graph-gophers/graphql-go/errors" + "github.com/graph-gophers/graphql-go/pkg/common" ) // Test is a GraphQL test case to be used with RunTest(s). type Test struct { - Context context.Context - Schema *graphql.Schema - Query string - OperationName string - Variables map[string]interface{} - ExpectedResult string - ExpectedErrors []*errors.QueryError - RawResponse bool + Context context.Context + Schema *graphql.Schema + Query string + OperationName string + Variables map[string]interface{} + ExpectedResult string + ExpectedErrors []*errors.QueryError + RawResponse bool + DirectiveVisitors map[string]common.DirectiveVisitor } // RunTests runs the given GraphQL test cases as subtests. @@ -45,7 +47,7 @@ func RunTest(t *testing.T, test *Test) { if test.Context == nil { test.Context = context.Background() } - result := test.Schema.Exec(test.Context, test.Query, test.OperationName, test.Variables) + result := test.Schema.Exec(test.Context, test.Query, test.OperationName, test.Variables, test.DirectiveVisitors) checkErrors(t, test.ExpectedErrors, result.Errors) diff --git a/graphql.go b/graphql.go index a2f21f84d..7d1201338 100644 --- a/graphql.go +++ b/graphql.go @@ -8,7 +8,6 @@ import ( "time" "github.com/graph-gophers/graphql-go/errors" - "github.com/graph-gophers/graphql-go/internal/common" "github.com/graph-gophers/graphql-go/internal/exec" "github.com/graph-gophers/graphql-go/internal/exec/resolvable" "github.com/graph-gophers/graphql-go/internal/exec/selected" @@ -17,6 +16,7 @@ import ( "github.com/graph-gophers/graphql-go/internal/validation" "github.com/graph-gophers/graphql-go/introspection" "github.com/graph-gophers/graphql-go/log" + "github.com/graph-gophers/graphql-go/pkg/common" "github.com/graph-gophers/graphql-go/trace" ) @@ -181,14 +181,14 @@ func (s *Schema) ValidateWithVariables(queryString string, variables map[string] // Exec executes the given query with the schema's resolver. It panics if the schema was created // without a resolver. If the context get cancelled, no further resolvers will be called and a // the context error will be returned as soon as possible (not immediately). -func (s *Schema) Exec(ctx context.Context, queryString string, operationName string, variables map[string]interface{}) *Response { +func (s *Schema) Exec(ctx context.Context, queryString string, operationName string, variables map[string]interface{}, visitors map[string]common.DirectiveVisitor) *Response { if s.res.Resolver == (reflect.Value{}) { panic("schema created without resolver, can not exec") } - return s.exec(ctx, queryString, operationName, variables, s.res) + return s.exec(ctx, queryString, operationName, variables, visitors, s.res) } -func (s *Schema) exec(ctx context.Context, queryString string, operationName string, variables map[string]interface{}, res *resolvable.Schema) *Response { +func (s *Schema) exec(ctx context.Context, queryString string, operationName string, variables map[string]interface{}, visitors map[string]common.DirectiveVisitor, res *resolvable.Schema) *Response { doc, qErr := query.Parse(queryString) if qErr != nil { return &Response{Errors: []*errors.QueryError{qErr}} @@ -239,9 +239,10 @@ func (s *Schema) exec(ctx context.Context, queryString string, operationName str Schema: s.schema, DisableIntrospection: s.disableIntrospection, }, - Limiter: make(chan struct{}, s.maxParallelism), - Tracer: s.tracer, - Logger: s.logger, + Limiter: make(chan struct{}, s.maxParallelism), + Tracer: s.tracer, + Logger: s.logger, + Visitors: visitors, } varTypes := make(map[string]*introspection.Type) for _, v := range op.Vars { diff --git a/graphql_test.go b/graphql_test.go index 98bedce14..2a3c9cedd 100644 --- a/graphql_test.go +++ b/graphql_test.go @@ -11,6 +11,7 @@ import ( gqlerrors "github.com/graph-gophers/graphql-go/errors" "github.com/graph-gophers/graphql-go/example/starwars" "github.com/graph-gophers/graphql-go/gqltesting" + "github.com/graph-gophers/graphql-go/pkg/common" ) type helloWorldResolver1 struct{} @@ -45,6 +46,20 @@ func (r *helloSnakeResolver2) SayHello(ctx context.Context, args struct{ FullNam return "Hello " + args.FullName + "!", nil } +type customDirectiveVisitor struct{} + +func (v customDirectiveVisitor) Before(directive *common.Directive, input interface{}) error { + return nil +} + +func (v customDirectiveVisitor) After(directive *common.Directive, output interface{}) (interface{}, error) { + if value, ok := directive.Args.Get("customAttribute"); ok { + return fmt.Sprintf("Directive '%s' (with arg '%s') modified result: %s", directive.Name.Name, value.String(), output.(string)), nil + } else { + return fmt.Sprintf("Directive '%s' modified result: %s", directive.Name.Name, output.(string)), nil + } +} + type theNumberResolver struct { number int32 } @@ -213,6 +228,67 @@ func TestHelloWorld(t *testing.T) { }) } +func TestCustomDirective(t *testing.T) { + t.Parallel() + + gqltesting.RunTests(t, []*gqltesting.Test{ + { + Schema: graphql.MustParseSchema(` + directive @customDirective on FIELD_DEFINITION + + schema { + query: Query + } + + type Query { + hello_html: String! @customDirective + } + `, &helloSnakeResolver1{}), + Query: ` + { + hello_html + } + `, + ExpectedResult: ` + { + "hello_html": "Directive 'customDirective' modified result: Hello snake!" + } + `, + DirectiveVisitors: map[string]common.DirectiveVisitor{ + "customDirective": customDirectiveVisitor{}, + }, + }, + { + Schema: graphql.MustParseSchema(` + directive @customDirective( + customAttribute: String! + ) on FIELD_DEFINITION + + schema { + query: Query + } + + type Query { + say_hello(full_name: String!): String! @customDirective(customAttribute: hi) + } + `, &helloSnakeResolver1{}), + Query: ` + { + say_hello(full_name: "Johnny") + } + `, + ExpectedResult: ` + { + "say_hello": "Directive 'customDirective' (with arg 'hi') modified result: Hello Johnny!" + } + `, + DirectiveVisitors: map[string]common.DirectiveVisitor{ + "customDirective": customDirectiveVisitor{}, + }, + }, + }) +} + func TestHelloSnake(t *testing.T) { t.Parallel() @@ -3728,7 +3804,7 @@ func TestSchema_Exec_without_resolver(t *testing.T) { t.Fail() } }() - _ = s.Exec(context.Background(), tt.Args.Query, "", map[string]interface{}{}) + _ = s.Exec(context.Background(), tt.Args.Query, "", map[string]interface{}{}, map[string]common.DirectiveVisitor{}) }) } } diff --git a/internal/exec/exec.go b/internal/exec/exec.go index 7b9895e7b..838812157 100644 --- a/internal/exec/exec.go +++ b/internal/exec/exec.go @@ -10,12 +10,12 @@ import ( "time" "github.com/graph-gophers/graphql-go/errors" - "github.com/graph-gophers/graphql-go/internal/common" "github.com/graph-gophers/graphql-go/internal/exec/resolvable" "github.com/graph-gophers/graphql-go/internal/exec/selected" "github.com/graph-gophers/graphql-go/internal/query" "github.com/graph-gophers/graphql-go/internal/schema" "github.com/graph-gophers/graphql-go/log" + "github.com/graph-gophers/graphql-go/pkg/common" "github.com/graph-gophers/graphql-go/trace" ) @@ -25,6 +25,7 @@ type Request struct { Tracer trace.Tracer Logger log.Logger SubscribeResolverTimeout time.Duration + Visitors map[string]common.DirectiveVisitor } func (r *Request) handlePanic(ctx context.Context) { @@ -207,8 +208,42 @@ func execFieldSelection(ctx context.Context, r *Request, s *resolvable.Schema, f if f.field.ArgsPacker != nil { in = append(in, f.field.PackedArgs) } + + // Before hook directive visitor + if len(f.field.Directives) > 0 { + for _, directive := range f.field.Directives { + if visitor, ok := r.Visitors[directive.Name.Name]; ok { + var values = make([]interface{}, 0) + for _, inValue := range in { + values = append(values, inValue.Interface()) + } + + if err := visitor.Before(directive, values); err != nil { + return nil + } + } + } + } + + // Call method callOut := res.Method(f.field.MethodIndex).Call(in) result = callOut[0] + + // After hook directive visitor (when no error is returned from resolver) + if !f.field.HasError && len(f.field.Directives) > 0 { + for _, directive := range f.field.Directives { + if visitor, ok := r.Visitors[directive.Name.Name]; ok { + returned, err := visitor.After(directive, result.Interface()) + if err != nil { + f.field.HasError = true + callOut[1] = reflect.ValueOf(err) + } else { + result = reflect.ValueOf(returned) + } + } + } + } + if f.field.HasError && !callOut[1].IsNil() { resolverErr := callOut[1].Interface().(error) err := errors.Errorf("%s", resolverErr) diff --git a/internal/exec/packer/packer.go b/internal/exec/packer/packer.go index deadacb80..1049b728a 100644 --- a/internal/exec/packer/packer.go +++ b/internal/exec/packer/packer.go @@ -7,8 +7,8 @@ import ( "strings" "github.com/graph-gophers/graphql-go/errors" - "github.com/graph-gophers/graphql-go/internal/common" "github.com/graph-gophers/graphql-go/internal/schema" + "github.com/graph-gophers/graphql-go/pkg/common" ) type packer interface { diff --git a/internal/exec/resolvable/meta.go b/internal/exec/resolvable/meta.go index e9707516e..9f1bd9190 100644 --- a/internal/exec/resolvable/meta.go +++ b/internal/exec/resolvable/meta.go @@ -4,9 +4,9 @@ import ( "fmt" "reflect" - "github.com/graph-gophers/graphql-go/internal/common" "github.com/graph-gophers/graphql-go/internal/schema" "github.com/graph-gophers/graphql-go/introspection" + "github.com/graph-gophers/graphql-go/pkg/common" ) // Meta defines the details of the metadata schema for introspection. diff --git a/internal/exec/resolvable/resolvable.go b/internal/exec/resolvable/resolvable.go index a3a504810..2ef917a71 100644 --- a/internal/exec/resolvable/resolvable.go +++ b/internal/exec/resolvable/resolvable.go @@ -6,9 +6,9 @@ import ( "reflect" "strings" - "github.com/graph-gophers/graphql-go/internal/common" "github.com/graph-gophers/graphql-go/internal/exec/packer" "github.com/graph-gophers/graphql-go/internal/schema" + "github.com/graph-gophers/graphql-go/pkg/common" ) type Schema struct { diff --git a/internal/exec/selected/selected.go b/internal/exec/selected/selected.go index fdfcfa7a5..e19e861c5 100644 --- a/internal/exec/selected/selected.go +++ b/internal/exec/selected/selected.go @@ -6,12 +6,12 @@ import ( "sync" "github.com/graph-gophers/graphql-go/errors" - "github.com/graph-gophers/graphql-go/internal/common" "github.com/graph-gophers/graphql-go/internal/exec/packer" "github.com/graph-gophers/graphql-go/internal/exec/resolvable" "github.com/graph-gophers/graphql-go/internal/query" "github.com/graph-gophers/graphql-go/internal/schema" "github.com/graph-gophers/graphql-go/introspection" + "github.com/graph-gophers/graphql-go/pkg/common" ) type Request struct { diff --git a/internal/exec/subscribe.go b/internal/exec/subscribe.go index a42a8634d..6a197db6b 100644 --- a/internal/exec/subscribe.go +++ b/internal/exec/subscribe.go @@ -9,10 +9,10 @@ import ( "time" "github.com/graph-gophers/graphql-go/errors" - "github.com/graph-gophers/graphql-go/internal/common" "github.com/graph-gophers/graphql-go/internal/exec/resolvable" "github.com/graph-gophers/graphql-go/internal/exec/selected" "github.com/graph-gophers/graphql-go/internal/query" + "github.com/graph-gophers/graphql-go/pkg/common" ) type Response struct { diff --git a/internal/query/query.go b/internal/query/query.go index fffc88e7f..8345ec16b 100644 --- a/internal/query/query.go +++ b/internal/query/query.go @@ -5,7 +5,7 @@ import ( "text/scanner" "github.com/graph-gophers/graphql-go/errors" - "github.com/graph-gophers/graphql-go/internal/common" + "github.com/graph-gophers/graphql-go/pkg/common" ) type Document struct { diff --git a/internal/schema/schema.go b/internal/schema/schema.go index 38012040a..b32485de6 100644 --- a/internal/schema/schema.go +++ b/internal/schema/schema.go @@ -5,7 +5,7 @@ import ( "text/scanner" "github.com/graph-gophers/graphql-go/errors" - "github.com/graph-gophers/graphql-go/internal/common" + "github.com/graph-gophers/graphql-go/pkg/common" ) // Schema represents a GraphQL service's collective type system capabilities. diff --git a/internal/schema/schema_internal_test.go b/internal/schema/schema_internal_test.go index d652f5d51..6acb23a1f 100644 --- a/internal/schema/schema_internal_test.go +++ b/internal/schema/schema_internal_test.go @@ -4,7 +4,7 @@ import ( "testing" "github.com/graph-gophers/graphql-go/errors" - "github.com/graph-gophers/graphql-go/internal/common" + "github.com/graph-gophers/graphql-go/pkg/common" ) func TestParseInterfaceDef(t *testing.T) { diff --git a/internal/validation/validation.go b/internal/validation/validation.go index c8be73544..ff0ab5e80 100644 --- a/internal/validation/validation.go +++ b/internal/validation/validation.go @@ -9,9 +9,9 @@ import ( "text/scanner" "github.com/graph-gophers/graphql-go/errors" - "github.com/graph-gophers/graphql-go/internal/common" "github.com/graph-gophers/graphql-go/internal/query" "github.com/graph-gophers/graphql-go/internal/schema" + "github.com/graph-gophers/graphql-go/pkg/common" ) type varSet map[*common.InputValue]struct{} diff --git a/introspection.go b/introspection.go index 6877bcaf3..720613a08 100644 --- a/introspection.go +++ b/introspection.go @@ -6,6 +6,7 @@ import ( "github.com/graph-gophers/graphql-go/internal/exec/resolvable" "github.com/graph-gophers/graphql-go/introspection" + "github.com/graph-gophers/graphql-go/pkg/common" ) // Inspect allows inspection of the given schema. @@ -15,7 +16,7 @@ func (s *Schema) Inspect() *introspection.Schema { // ToJSON encodes the schema in a JSON format used by tools like Relay. func (s *Schema) ToJSON() ([]byte, error) { - result := s.exec(context.Background(), introspectionQuery, "", nil, &resolvable.Schema{ + result := s.exec(context.Background(), introspectionQuery, "", nil, map[string]common.DirectiveVisitor{}, &resolvable.Schema{ Meta: s.res.Meta, Query: &resolvable.Object{}, Schema: *s.schema, diff --git a/introspection/introspection.go b/introspection/introspection.go index 2f4acad0a..48d590895 100644 --- a/introspection/introspection.go +++ b/introspection/introspection.go @@ -3,8 +3,8 @@ package introspection import ( "sort" - "github.com/graph-gophers/graphql-go/internal/common" "github.com/graph-gophers/graphql-go/internal/schema" + "github.com/graph-gophers/graphql-go/pkg/common" ) type Schema struct { diff --git a/internal/common/blockstring.go b/pkg/common/blockstring.go similarity index 100% rename from internal/common/blockstring.go rename to pkg/common/blockstring.go diff --git a/internal/common/directive.go b/pkg/common/directive.go similarity index 77% rename from internal/common/directive.go rename to pkg/common/directive.go index 62dca47f8..2c58ee8f9 100644 --- a/internal/common/directive.go +++ b/pkg/common/directive.go @@ -1,5 +1,10 @@ package common +type DirectiveVisitor interface { + Before(directive *Directive, input interface{}) error + After(directive *Directive, output interface{}) (interface{}, error) +} + type Directive struct { Name Ident Args ArgumentList diff --git a/internal/common/lexer.go b/pkg/common/lexer.go similarity index 99% rename from internal/common/lexer.go rename to pkg/common/lexer.go index af385ecc1..d33d4cef3 100644 --- a/internal/common/lexer.go +++ b/pkg/common/lexer.go @@ -30,7 +30,6 @@ func NewLexer(s string, useStringDescriptions bool) *Lexer { } sc.Init(strings.NewReader(s)) - l := Lexer{sc: sc, useStringDescriptions: useStringDescriptions} l.sc.Error = l.CatchScannerError diff --git a/internal/common/lexer_test.go b/pkg/common/lexer_test.go similarity index 87% rename from internal/common/lexer_test.go rename to pkg/common/lexer_test.go index e775a9fb4..c49bcbe7a 100644 --- a/internal/common/lexer_test.go +++ b/pkg/common/lexer_test.go @@ -3,7 +3,7 @@ package common_test import ( "testing" - "github.com/graph-gophers/graphql-go/internal/common" + "github.com/graph-gophers/graphql-go/pkg/common" ) type consumeTestCase struct { @@ -94,21 +94,21 @@ func TestConsume(t *testing.T) { } } -var multilineStringTests = []consumeTestCase { +var multilineStringTests = []consumeTestCase{ { - description: "Oneline strings are okay", - definition: `"Hello World"`, - expected: "", - failureExpected: false, - useStringDescriptions: true, + description: "Oneline strings are okay", + definition: `"Hello World"`, + expected: "", + failureExpected: false, + useStringDescriptions: true, }, { description: "Multiline strings are not allowed", definition: `"Hello World"`, - expected: `graphql: syntax error: literal not terminated (line 1, column 1)`, - failureExpected: true, - useStringDescriptions: true, + expected: `graphql: syntax error: literal not terminated (line 1, column 1)`, + failureExpected: true, + useStringDescriptions: true, }, } @@ -130,5 +130,5 @@ func TestMultilineString(t *testing.T) { t.Fatalf("Test '%s' failed with error: '%s'", test.description, err.Error()) } }) - } + } } diff --git a/internal/common/literals.go b/pkg/common/literals.go similarity index 100% rename from internal/common/literals.go rename to pkg/common/literals.go diff --git a/internal/common/types.go b/pkg/common/types.go similarity index 100% rename from internal/common/types.go rename to pkg/common/types.go diff --git a/internal/common/values.go b/pkg/common/values.go similarity index 100% rename from internal/common/values.go rename to pkg/common/values.go diff --git a/relay/relay.go b/relay/relay.go index 78e4dfdd5..94e695f5a 100644 --- a/relay/relay.go +++ b/relay/relay.go @@ -9,6 +9,7 @@ import ( "strings" graphql "github.com/graph-gophers/graphql-go" + "github.com/graph-gophers/graphql-go/pkg/common" ) func MarshalID(kind string, spec interface{}) graphql.ID { @@ -58,7 +59,7 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { return } - response := h.Schema.Exec(r.Context(), params.Query, params.OperationName, params.Variables) + response := h.Schema.Exec(r.Context(), params.Query, params.OperationName, params.Variables, map[string]common.DirectiveVisitor{}) responseJSON, err := json.Marshal(response) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) diff --git a/subscriptions.go b/subscriptions.go index 013039bd5..94dfe95be 100644 --- a/subscriptions.go +++ b/subscriptions.go @@ -6,13 +6,13 @@ import ( "reflect" qerrors "github.com/graph-gophers/graphql-go/errors" - "github.com/graph-gophers/graphql-go/internal/common" "github.com/graph-gophers/graphql-go/internal/exec" "github.com/graph-gophers/graphql-go/internal/exec/resolvable" "github.com/graph-gophers/graphql-go/internal/exec/selected" "github.com/graph-gophers/graphql-go/internal/query" "github.com/graph-gophers/graphql-go/internal/validation" "github.com/graph-gophers/graphql-go/introspection" + "github.com/graph-gophers/graphql-go/pkg/common" ) // Subscribe returns a response channel for the given subscription with the schema's