Skip to content

Commit

Permalink
Automatically add __typename to fields of interface type
Browse files Browse the repository at this point in the history
In #52, I added support for interface types, but with the simplifying
restriction (among others) that the user must request the field
`__typename`.  In this commit, I remove this restriction.

The basic idea is simple: we preprocess the query to add `__typename`.
The implementation isn't much more complicated!  Although it required
some new wiring in a few places.

Issue: #8

Test plan: make tesc

Reviewers: marksandstrom, adam, miguel
  • Loading branch information
benjaminjkraft committed Aug 25, 2021
1 parent 1e87553 commit c37edb9
Show file tree
Hide file tree
Showing 16 changed files with 189 additions and 62 deletions.
18 changes: 5 additions & 13 deletions generate/convert.go
Original file line number Diff line number Diff line change
Expand Up @@ -259,19 +259,6 @@ func (g *generator) convertDefinition(
return nil, errorf(pos, "not implemented: %v", def.Kind)
}

// We need to request __typename so we know which concrete type to use.
hasTypename := false
for _, selection := range selectionSet {
field, ok := selection.(*ast.Field)
if ok && field.Name == "__typename" {
hasTypename = true
}
}
if !hasTypename {
// TODO(benkraft): Instead, modify the query to add __typename.
return nil, errorf(pos, "union/interface type %s must request __typename", def.Name)
}

implementationTypes := g.schema.GetPossibleTypes(def)
goType := &goInterfaceType{
GoName: name,
Expand All @@ -283,6 +270,11 @@ func (g *generator) convertDefinition(

for i, implDef := range implementationTypes {
implName, implNamePrefix := g.typeName(namePrefix, implDef)
// TODO(benkraft): In principle we should skip generating a Go
// field for __typename each of these impl-defs if you didn't
// request it (and it was automatically added by
// preprocessQueryDocument). But in practice it doesn't really
// hurt, and would be extra work to avoid, so we just leave it.
implTyp, err := g.convertDefinition(
implName, implNamePrefix, implDef, pos, selectionSet, queryOptions)
if err != nil {
Expand Down
80 changes: 76 additions & 4 deletions generate/generate.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import (

"github.com/vektah/gqlparser/v2/ast"
"github.com/vektah/gqlparser/v2/formatter"
"github.com/vektah/gqlparser/v2/validator"
"golang.org/x/tools/imports"
)

Expand Down Expand Up @@ -140,17 +141,88 @@ func (g *generator) getArgument(
}, nil
}

// Preprocess each query to make any changes that genqlient needs.
//
// At present, the only change is that we add __typename, if not already
// requested, to each field of interface type, so we can use the right types
// when unmarshaling.
func (g *generator) preprocessQueryDocument(doc *ast.QueryDocument) {
var observers validator.Events
// We want to ensure that everywhere you ask for some list of fields (a
// selection-set) from an interface (or union) type, you ask for its
// __typename field. There are four places we might find a selection-set:
// at the toplevel of a query, on a field, or in an inline or named
// fragment. The toplevel of a query must be an object type, so we don't
// need to consider that.
// TODO(benkraft): Once we support fragments, figure out whether we need to
// traverse inline/named fragments here too.
observers.OnField(func(_ *validator.Walker, field *ast.Field) {
// We are interested in a field from the query like
// field { subField ... }
// where the schema looks like
// type ... { # or interface/union
// field: FieldType # or [FieldType!]! etc.
// }
// interface FieldType { # or union
// subField: ...
// }
// If FieldType is an interface/union, and none of the subFields is
// __typename, we want to change the query to
// field { __typename subField ... }

fieldType := g.schema.Types[field.Definition.Type.Name()]
if fieldType.Kind != ast.Interface && fieldType.Kind != ast.Union {
return // a concrete type
}

hasTypename := false
for _, selection := range field.SelectionSet {
// Check if we already selected __typename. We ignore fragments,
// because we want __typename as a toplevel field.
subField, ok := selection.(*ast.Field)
if ok && subField.Name == "__typename" {
hasTypename = true
}
}
if !hasTypename {
// Ok, we need to add the field!
field.SelectionSet = append(ast.SelectionSet{
&ast.Field{
Alias: "__typename", Name: "__typename",
// Fake definition for the magic field __typename cribbed
// from gqlparser's validator/walk.go, equivalent to
// __typename: String
// TODO(benkraft): This should in principle be
// __typename: String!
// But genqlient doesn't care, so we just match gqlparser.
Definition: &ast.FieldDefinition{
Name: "__typename",
Type: ast.NamedType("String", nil /* pos */),
},
// Definition of the object that contains this field, i.e.
// FieldType.
ObjectDefinition: fieldType,
},
}, field.SelectionSet...)
}
})
validator.Walk(g.schema, doc, &observers)
}

func (g *generator) addOperation(op *ast.OperationDefinition) error {
if op.Name == "" {
return errorf(op.Position, "operations must have operation-names")
}

var builder strings.Builder
f := formatter.NewFormatter(&builder)
f.FormatQueryDocument(&ast.QueryDocument{
queryDoc := &ast.QueryDocument{
Operations: ast.OperationList{op},
// TODO: handle fragments
})
}
g.preprocessQueryDocument(queryDoc)

var builder strings.Builder
f := formatter.NewFormatter(&builder)
f.FormatQueryDocument(queryDoc)

commentLines, directive, err := g.parsePrecedingComment(op, op.Position)
if err != nil {
Expand Down
4 changes: 4 additions & 0 deletions generate/genqlient_directive.go
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,10 @@ func (g *generator) parsePrecedingComment(
pos *ast.Position,
) (comment string, directive *GenqlientDirective, err error) {
directive = new(GenqlientDirective)
if pos == nil || pos.Src == nil { // node was added by genqlient itself
return "", directive, nil // treated as if there were no comment
}

var commentLines []string
sourceLines := strings.Split(pos.Src.Input, "\n")
for i := pos.Line - 1; i > 0; i-- {
Expand Down
5 changes: 0 additions & 5 deletions generate/testdata/errors/MissingTypeName.go

This file was deleted.

1 change: 0 additions & 1 deletion generate/testdata/errors/MissingTypeName.graphql

This file was deleted.

11 changes: 0 additions & 11 deletions generate/testdata/errors/MissingTypeName.schema.graphql

This file was deleted.

2 changes: 0 additions & 2 deletions generate/testdata/queries/InterfaceListField.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ query InterfaceListField {
id
name
children {
__typename
id
name
}
Expand All @@ -13,7 +12,6 @@ query InterfaceListField {
id
name
children {
__typename
id
name
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
query InterfaceListOfListOfListsField {
listOfListsOfListsOfContent { __typename id name }
listOfListsOfListsOfContent { id name }
# @genqlient(pointer: true)
withPointer: listOfListsOfListsOfContent { __typename id name }
withPointer: listOfListsOfListsOfContent { id name }
}
3 changes: 0 additions & 3 deletions generate/testdata/queries/InterfaceNesting.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,10 @@ query InterfaceNesting {
root {
id
children {
__typename
id
parent {
__typename
id
children {
__typename
id
}
}
Expand Down
5 changes: 3 additions & 2 deletions generate/testdata/queries/InterfaceNoFragments.graphql
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
query InterfaceNoFragmentsQuery {
root { id name } # (make sure sibling fields work)
randomItem { __typename id name }
randomItem { id name }
randomItemWithTypeName: randomItem { __typename id name }
# @genqlient(pointer: true)
withPointer: randomItem { __typename id name }
withPointer: randomItem { id name }
}

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"operations": [
{
"operationName": "InterfaceNesting",
"query": "\nquery InterfaceNesting {\n\troot {\n\t\tid\n\t\tchildren {\n\t\t\t__typename\n\t\t\tid\n\t\t\tparent {\n\t\t\t\t__typename\n\t\t\t\tid\n\t\t\t\tchildren {\n\t\t\t\t\t__typename\n\t\t\t\t\tid\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n}\n",
"query": "\nquery InterfaceNesting {\n\troot {\n\t\tid\n\t\tchildren {\n\t\t\t__typename\n\t\t\tid\n\t\t\tparent {\n\t\t\t\tid\n\t\t\t\tchildren {\n\t\t\t\t\t__typename\n\t\t\t\t\tid\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n}\n",
"sourceLocation": "testdata/queries/InterfaceNesting.graphql"
}
]
Expand Down
Loading

0 comments on commit c37edb9

Please sign in to comment.