graph-gophers / graphql-go

GraphQL server with a focus on ease of use
BSD 2-Clause "Simplified" License
4.64k stars 491 forks source link

[Bug] GraphQL server returns interface type for __typename when UseFieldResolvers is enabled #630

Open magicoder10 opened 7 months ago

magicoder10 commented 7 months ago

Input

Schema

interface Animal {
    name: String!
}

type Dog implements Animal {
    name: String!
    breed: String!
}

type Cat implements Animal {
    name: String!
    livesLeft: Int!
}

type Query {
    animals: [Animal!]!
}

Query

query {
  animals {
    name
    __typename
  }
}

Output

Without UseFieldResolvers

schema := graphql.MustParseSchema(schemaString, &query{})
    http.Handle("/graphql", &relay.Handler{Schema: schema})
    log.Fatal(http.ListenAndServe(":8080", nil))
{
  "data": {
    "animals": [
      {
        "name": "Buddy",
        "__typename": "Dog"
      },
      {
        "name": "Whiskers",
        "__typename": "Cat"
      }
    ]
  }
}

With UseFieldResolvers

schema := graphql.MustParseSchema(schemaString, &query{}, graphql.UseFieldResolvers())
    http.Handle("/graphql", &relay.Handler{Schema: schema})
    log.Fatal(http.ListenAndServe(":8080", nil))
{
  "data": {
    "animals": [
      {
        "name": "Buddy",
        "__typename": "Animal"
      },
      {
        "name": "Whiskers",
        "__typename": "Animal"
      }
    ]
  }
}

This bug leads to

{
  "errors": [
    {
      "message": "panic occurred: {{\"Dog\" {'\\x05' '\\f'}}} does not implement \"Animal\""
    }
  ]
}
pavelnikolov commented 7 months ago

@magicoder10 thank you for reporting this. Can I, please, have the code for the resolver as well?

pavelnikolov commented 6 days ago

Initially this library was developed without field resolvers. There had to be a corresponding method for each GraphQL field. Therefore it made sense to represent GraphQL interfaces by corresponding Go interfaces which have a method for each of the GraphQL interface fields. If we choose to use field resolvers in our schema we would need to still use methods for the GraphQL interface fields. Here is a working example:


type Cat struct {
    name      string
    LivesLeft int32
}

func (c *Cat) Name() string {
    return c.name
}

type Dog struct {
    name  string
    Breed string
}

func (c *Dog) Name() string {
    return c.name
}

type Animal interface {
    Name() string
}

type AnimalsQuery struct {
    animals []Animal
}

func (q *AnimalsQuery) Animals() []*animalResolver {
    return toAnimalResolvers(q.animals)
}

type animalResolver struct {
    Animal
}

func (r *animalResolver) ToDog() (*Dog, bool) {
    d, ok := r.Animal.(*Dog)
    return d, ok
}

func (r *animalResolver) ToCat() (*Cat, bool) {
    c, ok := r.Animal.(*Cat)
    return c, ok
}

func toAnimalResolvers(animals []Animal) []*animalResolver {
    resolvers := make([]*animalResolver, len(animals))
    for i, animal := range animals {
        resolvers[i] = &animalResolver{Animal: animal}
    }
    return resolvers
}

func Test_issue630(t *testing.T) {

    schema := `
    interface Animal {
        name: String!
    }

    type Dog implements Animal {
        name: String!
        breed: String!
    }

    type Cat implements Animal {
        name: String!
        livesLeft: Int!
    }

    type Query {
        animals: [Animal!]!
    }
    `

    animals := []Animal{
        &Dog{name: "Fido", Breed: "Beagle"},
        &Cat{name: "Whiskers", LivesLeft: 9},
    }
    gqltesting.RunTests(t, []*gqltesting.Test{
        {
            Schema: graphql.MustParseSchema(schema, &AnimalsQuery{animals: animals}, graphql.UseFieldResolvers()),
            Query: `
                query {
                    animals {
                        name
                        __typename
                    }
                }
            `,
            ExpectedResult: `
                {"animals":[{"__typename":"Dog","name":"Fido"},{"__typename":"Cat","name":"Whiskers"}]}
            `,
        },
    })
}

I understand that this might be confusing and maybe this needs to be either better documented or the library needs to be changed in a way to allow field resolution of GraphQL interface fields 🤔