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

Conditionally removing fields from introspection #1004

Closed
wants to merge 2 commits into from
Closed
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
49 changes: 48 additions & 1 deletion graphql/handler/extension/introspection.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,23 @@ package extension
import (
"context"

"github.com/99designs/gqlgen/graphql/introspection"

"github.com/99designs/gqlgen/graphql"
"github.com/vektah/gqlparser/gqlerror"
)

// EnableIntrospection enables clients to reflect all of the types available on the graph.
type Introspection struct{}
type Introspection struct {
AllowFieldFunc func(ctx context.Context, t *introspection.Type, field *introspection.Field) (bool, error)

AllowInputValueFunc func(ctx context.Context, t *introspection.Type, inputValue *introspection.InputValue) (bool, error)
}

var _ interface {
graphql.OperationContextMutator
graphql.HandlerExtension
graphql.FieldInterceptor
} = Introspection{}

func (c Introspection) ExtensionName() string {
Expand All @@ -27,3 +34,43 @@ func (c Introspection) MutateOperationContext(ctx context.Context, rc *graphql.O
rc.DisableIntrospection = false
return nil
}

func (c Introspection) InterceptField(ctx context.Context, next graphql.Resolver) (res interface{}, err error) {
res, err = next(ctx)

fc := graphql.GetFieldContext(ctx)
if fields, ok := res.([]introspection.Field); ok {
if c.AllowFieldFunc == nil {
return
}
t := fc.Parent.Result.(*introspection.Type)
var newFields []introspection.Field
for _, field := range fields {
allow, err := c.AllowFieldFunc(ctx, t, &field)
if err != nil {
return nil, err
}
if allow {
newFields = append(newFields, field)
}
}
res = newFields
} else if fields, ok := res.([]introspection.InputValue); ok {
if c.AllowInputValueFunc == nil {
return
}
t := fc.Parent.Result.(*introspection.Type)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

intropection.Type, intropection.Directive and introspection.Field will be coming.
https://spec.graphql.org/June2018/#sec-Schema-Introspection __InputValue

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was thinking to change it to something like

type Introspection struct {
	Auth IntrospectionAuth
}

type IntrospectionAuth interface {
	AllowEnumValues(ctx context.Context, enumValues []introspection.EnumValue) ([]introspection.EnumValue, error)

	AllowFields(ctx context.Context, fields []introspection.Field) ([]introspection.Field, error)

	AllowInputValues(ctx context.Context, inputValues []introspection.InputValue) ([]introspection.InputValue, error)
}

and the allow func would be something like:

AllowFieldsFunc: func(ctx context.Context, fields []introspection.Field) ([]introspection.Field, error) {
						fc := graphql.GetFieldContext(ctx)
						newFields := make([]introspection.Field, 0, len(fields))
						if t, ok := fc.Parent.Result.(*introspection.Type); ok && *t.Name() == "TestType1" {
							for _, field := range fields {
								if field.Name == "TestField1" {
									continue
								}
								newFields = append(newFields, field)
							}
							return newFields, nil
						}
						return fields, nil
					},

This way the allow func is more verbose, but is called just one time and there is no need to have one function for each of intropection.Type, intropection.Directive and introspection.Field.
What do you think?

var newFields []introspection.InputValue
for _, field := range fields {
allow, err := c.AllowInputValueFunc(ctx, t, &field)
if err != nil {
return nil, err
}
if allow {
newFields = append(newFields, field)
}
}
res = newFields
}
return res, err
}
109 changes: 109 additions & 0 deletions graphql/handler/extension/introspection_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,14 @@ package extension

import (
"context"
"strings"
"testing"

"github.com/99designs/gqlgen/graphql"

"github.com/99designs/gqlgen/graphql/introspection"
"github.com/vektah/gqlparser/ast"

"github.com/stretchr/testify/require"
)

Expand All @@ -15,3 +20,107 @@ func TestIntrospection(t *testing.T) {
require.Nil(t, Introspection{}.MutateOperationContext(context.Background(), rc))
require.Equal(t, false, rc.DisableIntrospection)
}

func TestIntrospection_InterceptField(t *testing.T) {
type fields struct {
AllowFieldFunc func(ctx context.Context, t *introspection.Type, field *introspection.Field) (bool, error)
AllowInputValueFunc func(ctx context.Context, t *introspection.Type, inputValue *introspection.InputValue) (bool, error)
}
type args struct {
kind ast.DefinitionKind
}
tests := []struct {
name string
fields fields
args args
wantRes []string
}{
{
name: "field",
fields: fields{
AllowFieldFunc: func(ctx context.Context, t *introspection.Type, field *introspection.Field) (b bool, err error) {
if *t.Name() == "TestType1" {
return !strings.HasSuffix(field.Name, "1"), nil
}
return true, nil
},
},
args: args{kind: ast.Object},
wantRes: []string{"testField2", "testField3"},
},
{
name: "inputValue",
fields: fields{
AllowInputValueFunc: func(ctx context.Context, t *introspection.Type, inputValue *introspection.InputValue) (b bool, err error) {
if *t.Name() == "TestType1" {
return !strings.HasSuffix(inputValue.Name, "1"), nil
}
return true, nil
},
},
args: args{kind: ast.InputObject},
wantRes: []string{"testField2", "testField3"},
},
{
name: "nil AllowFieldFunc",
args: args{kind: ast.Object},
wantRes: []string{"testField1", "testField2", "testField3"},
},
{
name: "nil AllowInputValueFunc",
args: args{kind: ast.Object},
wantRes: []string{"testField1", "testField2", "testField3"},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
c := Introspection{
AllowFieldFunc: tt.fields.AllowFieldFunc,
AllowInputValueFunc: tt.fields.AllowInputValueFunc,
}
typ := introspection.WrapTypeFromDef(nil, &ast.Definition{
Kind: tt.args.kind,
Name: "TestType1",
Fields: []*ast.FieldDefinition{
{Name: "testField1"},
{Name: "testField2"},
{Name: "testField3"},
},
})
ctx := graphql.WithFieldContext(context.Background(), &graphql.FieldContext{Result: typ})
ctx = graphql.WithFieldContext(ctx, &graphql.FieldContext{
Field: graphql.CollectedField{
Field: &ast.Field{
Name: tt.name,
},
},
})
gotRes, err := c.InterceptField(ctx, func(ctx context.Context) (res interface{}, err error) {
switch tt.args.kind {
case ast.Object:
return typ.Fields(false), nil
case ast.InputObject:
return typ.InputFields(), nil
}
require.Fail(t, "unexpected ast.DefinitionKind: %v", tt.args.kind)
return nil, nil
})
require.NoError(t, err)

var actualFields []string
switch tt.args.kind {
case ast.Object:
for _, field := range gotRes.([]introspection.Field) {
actualFields = append(actualFields, field.Name)
}
case ast.InputObject:
for _, field := range gotRes.([]introspection.InputValue) {
actualFields = append(actualFields, field.Name)
}
default:
require.FailNow(t, "", "unexpected ast.DefinitionKind: %v", tt.args.kind)
}
require.Equal(t, tt.wantRes, actualFields)
})
}
}