Skip to content

Commit

Permalink
Merge pull request #59 from jensneuse/10-undefined-types-in-middlewares
Browse files Browse the repository at this point in the history
undefined types in middlewares
  • Loading branch information
jensneuse authored Apr 7, 2019
2 parents ec82450 + c6c8ce0 commit 99d4d14
Show file tree
Hide file tree
Showing 18 changed files with 368 additions and 82 deletions.
4 changes: 4 additions & 0 deletions hack/middleware/asset_middleware.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@ import (
type AssetUrlMiddleware struct {
}

func (a *AssetUrlMiddleware) PrepareSchema(ctx context.Context, l *lookup.Lookup, w *lookup.Walker, parser *parser.Parser, mod *parser.ManualAstMod) error {
return nil
}

func (a *AssetUrlMiddleware) OnRequest(ctx context.Context, l *lookup.Lookup, w *lookup.Walker, parser *parser.Parser, mod *parser.ManualAstMod) error {

w.SetLookup(l)
Expand Down
17 changes: 17 additions & 0 deletions pkg/lexer/lexer.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,23 @@ func (l *Lexer) SetTypeSystemInput(input []byte) error {
return nil
}

func (l *Lexer) ExtendTypeSystemInput(input []byte) error {

if len(l.input) != l.typeSystemEndPosition {
return fmt.Errorf("ExtendTypeSystemInput: you must not extend the type system input after setting the executable input")
}

actual := len(l.input) + len(input)
if actual > maxInput {
return fmt.Errorf("ExtendTypeSystemInput: input size must not be > %d, got: %d", maxInput, actual)
}

l.input = append(l.input, input...)
l.typeSystemEndPosition = len(l.input)

return nil
}

func (l *Lexer) ResetTypeSystemInput() {
l.input = l._storage[:0]
l.inputPosition = 0
Expand Down
69 changes: 69 additions & 0 deletions pkg/lexer/lexer_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -442,6 +442,75 @@ baz
mustPeekAndRead(keyword.FLOAT, "13.37"),
)
})
t.Run("extend type system input", func(t *testing.T) {
t.Run("invalid flow", func(t *testing.T) {
l := NewLexer()
err := l.SetTypeSystemInput([]byte("foo"))
if err != nil {
t.Fatal(err)
}
err = l.SetExecutableInput([]byte("bar"))
if err != nil {
t.Fatal(err)
}
err = l.ExtendTypeSystemInput([]byte("baz"))
if err == nil {
t.Fatal("want err")
}
})
t.Run("valid flow", func(t *testing.T) {
l := NewLexer()
err := l.SetTypeSystemInput([]byte("foo"))
if err != nil {
t.Fatal(err)
}

foo := l.Read()
if string(l.ByteSlice(foo.Literal)) != "foo" {
t.Fatal("want foo")
}

err = l.ExtendTypeSystemInput([]byte(" bar"))
if err != nil {
t.Fatal(err)
}

bar := l.Read()
if string(l.ByteSlice(bar.Literal)) != "bar" {
t.Fatal("want bar")
}

err = l.ExtendTypeSystemInput([]byte(" baz"))
if err != nil {
t.Fatal(err)
}

baz := l.Read()
if string(l.ByteSlice(baz.Literal)) != "baz" {
t.Fatal("want baz")
}

err = l.SetExecutableInput([]byte("bal bat"))
if err != nil {
t.Fatal(err)
}

bal := l.Read()
if string(l.ByteSlice(bal.Literal)) != "bal" {
t.Fatal("want bal")
}

err = l.SetTypeSystemInput([]byte("foo2"))
if err != nil {
t.Fatal(err)
}

foo2 := l.Read()
if string(l.ByteSlice(foo2.Literal)) != "foo2" {
t.Fatal("want foo2")
}
})
})
}

var introspectionQuery = `query IntrospectionQuery {
Expand Down
13 changes: 13 additions & 0 deletions pkg/middleware/context_middleware.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,19 @@ query myDocuments {
type ContextMiddleware struct {
}

var contextMiddlewareSchemaExtension = []byte(`
directive @addArgumentFromContext(
name: String!
contextKey: String!
) on FIELD_DEFINITION`)

func (a *ContextMiddleware) PrepareSchema(ctx context.Context, l *lookup.Lookup, w *lookup.Walker, parser *parser.Parser, mod *parser.ManualAstMod) error {

err := parser.ExtendTypeSystemDefinition(contextMiddlewareSchemaExtension)

return err
}

func (a *ContextMiddleware) OnResponse(ctx context.Context, response *[]byte, l *lookup.Lookup, w *lookup.Walker, parser *parser.Parser, mod *parser.ManualAstMod) (err error) {
return nil
}
Expand Down
7 changes: 0 additions & 7 deletions pkg/middleware/context_middleware_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -107,13 +107,6 @@ func TestContextMiddleware(t *testing.T) {
}

const publicSchema = `
directive @addArgumentFromContext(
name: String!
contextKey: String!
) on FIELD_DEFINITION
scalar String
schema {
query: Query
}
Expand Down
10 changes: 10 additions & 0 deletions pkg/middleware/graphql_middleware.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,17 @@ import (
"github.com/jensneuse/graphql-go-tools/pkg/parser"
)

// GraphqlMiddleware is the interface to be implemented when writing middlewares
type GraphqlMiddleware interface {
// PrepareSchema is used to bring the schema in a valid state
// Example usages might be:
// - adding necessary directives to the schema, e.g. adding the context directive so that the context middleware works
// - define the graphql internal scalar types so that the validation middleware can do its thing
PrepareSchema(ctx context.Context, l *lookup.Lookup, w *lookup.Walker, parser *parser.Parser, mod *parser.ManualAstMod) error
// OnRequest is the handler func for a request from the client
// this can be used to transform the query and/or variables before sending it to the backend
OnRequest(ctx context.Context, l *lookup.Lookup, w *lookup.Walker, parser *parser.Parser, mod *parser.ManualAstMod) error
// OnResponse is the handler func for the response from the backend server
// this can be used to transform the response before sending the result back to the client
OnResponse(ctx context.Context, response *[]byte, l *lookup.Lookup, w *lookup.Walker, parser *parser.Parser, mod *parser.ManualAstMod) (err error)
}
30 changes: 9 additions & 21 deletions pkg/middleware/invoke_middleware.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,41 +3,29 @@ package middleware
import (
"bytes"
"context"
"github.com/jensneuse/graphql-go-tools/pkg/lookup"
"github.com/jensneuse/graphql-go-tools/pkg/parser"
"github.com/jensneuse/graphql-go-tools/pkg/printer"
)

// InvokeMiddleware is a one off middleware invocation helper
// This should only be used for testing as it's a waste of resources
// It makes use of panics to don't use this in production!
func InvokeMiddleware(middleware GraphqlMiddleware, ctx context.Context, schema, request string) (result string, err error) {
parse := parser.NewParser()
if err = parse.ParseTypeSystemDefinition([]byte(schema)); err != nil {
return
}
if err = parse.ParseExecutableDefinition([]byte(request)); err != nil {

invoker := NewInvoker(middleware)
err = invoker.SetSchema([]byte(schema))
if err != nil {
return
}
astPrint := printer.New()
look := lookup.New(parse)
walk := lookup.NewWalker(1024, 8)
mod := parser.NewManualAstMod(parse)
walk.SetLookup(look)

if err = middleware.OnRequest(ctx, look, walk, parse, mod); err != nil {
err = invoker.InvokeMiddleWares(ctx, []byte(request))
if err != nil {
return
}

walk.SetLookup(look)
walk.WalkExecutable()

astPrint.SetInput(parse, look, walk)
buff := bytes.Buffer{}
if err = astPrint.PrintExecutableSchema(&buff); err != nil {
err = invoker.RewriteRequest(&buff)
if err != nil {
return
}

result = buff.String()
return
return buff.String(), err
}
19 changes: 17 additions & 2 deletions pkg/middleware/invoker.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,14 +39,19 @@ func (i *Invoker) SetSchema(schema []byte) error {

func (i *Invoker) InvokeMiddleWares(ctx context.Context, request []byte) (err error) {

err = i.middlewaresPrepareSchema(ctx)
if err != nil {
return err
}

err = i.parse.ParseExecutableDefinition(request)
if err != nil {
return err
}

i.walk.SetLookup(i.look)

return i.invokeMiddleWares(ctx)
return i.middlewaresOnRequest(ctx)
}

func (i *Invoker) RewriteRequest(w io.Writer) error {
Expand All @@ -55,7 +60,17 @@ func (i *Invoker) RewriteRequest(w io.Writer) error {
return i.astPrint.PrintExecutableSchema(w)
}

func (i *Invoker) invokeMiddleWares(ctx context.Context) error {
func (i *Invoker) middlewaresPrepareSchema(ctx context.Context) error {
for j := range i.middleWares {
err := i.middleWares[j].PrepareSchema(ctx, i.look, i.walk, i.parse, i.mod)
if err != nil {
return err
}
}
return nil
}

func (i *Invoker) middlewaresOnRequest(ctx context.Context) error {
for j := range i.middleWares {
err := i.middleWares[j].OnRequest(ctx, i.look, i.walk, i.parse, i.mod)
if err != nil {
Expand Down
26 changes: 26 additions & 0 deletions pkg/middleware/validation_middleware.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,32 @@ import (
type ValidationMiddleware struct {
}

var validationMiddlewareSchemaExtension = []byte(`
scalar Int
scalar Float
scalar String
scalar Boolean
scalar ID
directive @include(
if: Boolean!
) on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT
directive @skip(
if: Boolean!
) on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT
directive @deprecated(
reason: String = "No longer supported"
) on FIELD_DEFINITION | ENUM_VALUE
`)

// PrepareSchema adds the base scalar and directive types to the schema so that the user doesn't have to add them
// if we omit these definitions from the schema definition the validation will fail
func (v *ValidationMiddleware) PrepareSchema(ctx context.Context, l *lookup.Lookup, w *lookup.Walker, parser *parser.Parser, mod *parser.ManualAstMod) error {

err := parser.ExtendTypeSystemDefinition(validationMiddlewareSchemaExtension)

return err
}

func (v *ValidationMiddleware) OnRequest(ctx context.Context, l *lookup.Lookup, w *lookup.Walker, parser *parser.Parser, mod *parser.ManualAstMod) error {

w.SetLookup(l)
Expand Down
19 changes: 17 additions & 2 deletions pkg/middleware/validation_middleware_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,31 @@ import (
func TestValidationMiddleware(t *testing.T) {
t.Run("valid", func(t *testing.T) {
query := `query myDocuments {documents {sensitiveInformation}}`
_, err := InvokeMiddleware(&ValidationMiddleware{}, nil, publicSchema, query)
_, err := InvokeMiddleware(&ValidationMiddleware{}, nil, validationMiddlewarePublicSchema, query)
if err != nil {
t.Fatal(err)
}
})
t.Run("invalid", func(t *testing.T) {
query := `query myDocuments {documents {fieldNotExists}}`
_, err := InvokeMiddleware(&ValidationMiddleware{}, nil, publicSchema, query)
_, err := InvokeMiddleware(&ValidationMiddleware{}, nil, validationMiddlewarePublicSchema, query)
if err == nil {
t.Fatal("want err")
}
})
}

const validationMiddlewarePublicSchema = `
schema {
query: Query
}
type Query {
documents: [Document]
}
type Document implements Node {
owner: String
sensitiveInformation: String
}
`
16 changes: 7 additions & 9 deletions pkg/parser/executabledefinition_parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,33 +5,31 @@ import (
"github.com/jensneuse/graphql-go-tools/pkg/lexing/keyword"
)

func (p *Parser) parseExecutableDefinition() (executableDefinition document.ExecutableDefinition, err error) {

executableDefinition = p.makeExecutableDefinition()
func (p *Parser) parseExecutableDefinition() (err error) {

for {
next := p.l.Peek(true)

switch next {
case keyword.CURLYBRACKETOPEN:

err := p.parseAnonymousOperation(&executableDefinition)
err := p.parseAnonymousOperation(&p.ParsedDefinitions.ExecutableDefinition)
if err != nil {
return executableDefinition, err
return err
}

case keyword.FRAGMENT:

err := p.parseFragmentDefinition(&executableDefinition.FragmentDefinitions)
err := p.parseFragmentDefinition(&p.ParsedDefinitions.ExecutableDefinition.FragmentDefinitions)
if err != nil {
return executableDefinition, err
return err
}

case keyword.QUERY, keyword.MUTATION, keyword.SUBSCRIPTION:

err := p.parseOperationDefinition(&executableDefinition.OperationDefinitions)
err := p.parseOperationDefinition(&p.ParsedDefinitions.ExecutableDefinition.OperationDefinitions)
if err != nil {
return executableDefinition, err
return err
}

default:
Expand Down
Loading

0 comments on commit 99d4d14

Please sign in to comment.