Skip to content

Commit

Permalink
Fix GraphQL interfaces with @hasInverse (#4605)
Browse files Browse the repository at this point in the history
  • Loading branch information
MichaelJCompton authored Jan 21, 2020
1 parent 52960be commit 3b96ce7
Show file tree
Hide file tree
Showing 7 changed files with 832 additions and 26 deletions.
6 changes: 1 addition & 5 deletions graphql/schema/gqlschema.go
Original file line number Diff line number Diff line change
Expand Up @@ -232,11 +232,7 @@ var fieldValidations []func(typ *ast.Definition, field *ast.FieldDefinition) *gq

func copyAstFieldDef(src *ast.FieldDefinition) *ast.FieldDefinition {
var dirs ast.DirectiveList
for _, d := range src.Directives {
if d.Name != inverseDirective {
dirs = append(dirs, d)
}
}
dirs = append(dirs, src.Directives...)

// Lets leave out copying the arguments as types in input schemas are not supposed to contain
// them. We add arguments for filters and order statements later.
Expand Down
64 changes: 50 additions & 14 deletions graphql/schema/rules.go
Original file line number Diff line number Diff line change
Expand Up @@ -245,34 +245,70 @@ func hasInverseValidation(sch *ast.Schema, typ *ast.Definition,
)
}

if errMsg := isInverse(typ.Name, field.Name, invTypeName, invField); errMsg != "" {
if errMsg := isInverse(sch, typ.Name, field.Name, invTypeName, invField); errMsg != "" {
return gqlerror.ErrorPosf(dir.Position, errMsg)
}

invDirective := invField.Directives.ForName(inverseDirective)
if invDirective == nil {
invField.Directives = append(invField.Directives, &ast.Directive{
Name: inverseDirective,
Arguments: []*ast.Argument{
{
Name: inverseArg,
Value: &ast.Value{
Raw: field.Name,
Position: dir.Position,
Kind: ast.EnumValue,
addDirective := func(fld *ast.FieldDefinition) {
fld.Directives = append(fld.Directives, &ast.Directive{
Name: inverseDirective,
Arguments: []*ast.Argument{
{
Name: inverseArg,
Value: &ast.Value{
Raw: field.Name,
Position: dir.Position,
Kind: ast.EnumValue,
},
},
},
},
Position: dir.Position,
})
Position: dir.Position,
})
}

addDirective(invField)

// If it was an interface, we also need to copy the @hasInverse directive
// to all implementing types
if invType.Kind == ast.Interface {
for _, t := range sch.Types {
if implements(t, invType) {
f := t.Fields.ForName(invFieldName)
if f != nil {
addDirective(f)
}
}
}
}
}

return nil
}

func isInverse(expectedInvType, expectedInvField, typeName string,
func implements(typ *ast.Definition, intfc *ast.Definition) bool {
for _, t := range typ.Interfaces {
if t == intfc.Name {
return true
}
}
return false
}

func isInverse(sch *ast.Schema, expectedInvType, expectedInvField, typeName string,
field *ast.FieldDefinition) string {

// We might have copied this directive in from an interface we are implementing.
// If so, make the check for that interface.
parentInt := parentInterface(sch, sch.Types[expectedInvType], expectedInvField)
if parentInt != nil {
fld := parentInt.Fields.ForName(expectedInvField)
if fld.Directives != nil && fld.Directives.ForName(inverseDirective) != nil {
expectedInvType = parentInt.Name
}
}

invType := field.Type.Name()
if invType != expectedInvType {
return fmt.Sprintf(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
type Author {
id: ID!
name: String! @search(by: [hash])
posts: [Post]
}

interface Post {
id: ID!
text: String @search(by: [fulltext])
datePublished: DateTime @search
author: Author! @hasInverse(field: posts)
}

type Question implements Post {
answered: Boolean
}

type Answer implements Post {
markedUseful: Boolean
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
type Author {
id: ID!
name: String! @search(by: [hash])
posts: [Post] @hasInverse(field: author)
}

interface Post {
id: ID!
text: String @search(by: [fulltext])
datePublished: DateTime @search
author: Author!
}

type Question implements Post {
answered: Boolean
}

type Answer implements Post {
markedUseful: Boolean
}
Loading

0 comments on commit 3b96ce7

Please sign in to comment.