granate
granate copied to clipboard
granate (relay part) gets confused by similar types
When graphql type A
is a subset of B
, then B
can be "converted" to A
, and look like an A
in the graphql response to a query. This is because graphql types is represented as interfaces in go, and it is a feature that a type struct b {}
can act as both an A
and a B
if it satisfies both those interfaces.
Let me explain.
Graphql schema:
type Admin extends Node {
id: ID!
name: String!
}
type User extends Node {
id: ID!
name: String!
userSpecific: Bool
}
From this, granate
generates interfaces:
// schema/adapters.go
type AdminInterface interface {
IdField(context.Context) (*string, error)
NameField(context.Context) (*string, error)
}
type UserInterface interface {
IdField(context.Context) (*string, error)
NameField(context.Context) (*string, error)
UserSpecificField(context.Context) (*string, error)
}
The important thing here is that anything that satisfies UserInterface
also satisfies the AdminInterface
which tricks the following logic:
// schema/definitions.go
func init() {
nodeDefinitions = relay.NewNodeDefinitions(relay.NodeDefinitionsConfig{
IDFetcher: func(id string, info graphql.ResolveInfo, ctx context.Context) (interface{}, error) {
// ...
},
TypeResolve: func(p graphql.ResolveTypeParams) *graphql.Object {
switch p.Value.(type) {
case AdminInterface: //< UserInterface satisfies this condition
return adminDefinition
case UserInterface:
return userDefinition
}
return nil
},
})
This means that this GraphQL request
query get_user {
node(id:"VXNlcjox") { # id: "User:1"
id
... on User {
name
userSpecific
}
... on Admin {
name
}
}
}
returns this response
{
"data": {
"node": {
"name": "Jon Jonsen",
"id": "QWRtaW46MQ==" // id: "Admin:1"
// no userSpecific field, because this is an Admin now
}
}
}
I circumvented the problem by adding a dummy field to the graphql types that fell victim to this .. well, it is a feature, but for our purposes it's a bug.
My new graphql schema:
type Admin extends Node {
id: ID!
name: String!
# Do not consume this field /// Document that this field is not part of api
isAdmin: Bool
}
type User extends Node {
id: ID!
name: String!
userSpecific: Bool
# Do not consume this field /// Also here
isUser: Bool
}
Thanks to @noh4ck for helping me finding the cause and a workaround.