-
Notifications
You must be signed in to change notification settings - Fork 1.8k
Description
The schema introspection definitions (https://facebook.github.io/graphql/June2018/#sec-Schema-Introspection) indicate that the interfaces
field of __Type
is a nullable list.
interfaces: [__Type!]
Our implementation of GraphQL (Lacinia) does, in fact, return a null for objects that do not implement an interface, rather than an empty list.
This results in an exception in the data pane at GraphiQL startup.
Error: Introspection result missing interfaces: {"kind":"OBJECT","name":"QueryRoot","description":"Root of all queries.","fields":[{"name":"hello","description":"Simplest possible query.","args":[],"type":{"kind":"SCALAR","name":"String","ofType":null},"isDeprecated":false,"deprecationReason":null}],"inputFields":null,"interfaces":null,"enumValues":null,"possibleTypes":null}
at buildObjectDef (http://localhost:8888/assets/graphiql/graphiql.js:33585:13)
at buildType (http://localhost:8888/assets/graphiql/graphiql.js:33559:18)
at getNamedType (http://localhost:8888/assets/graphiql/graphiql.js:33524:19)
at http://localhost:8888/assets/graphiql/graphiql.js:33703:12
at Array.map (<anonymous>)
at buildClientSchema (http://localhost:8888/assets/graphiql/graphiql.js:33702:41)
at http://localhost:8888/assets/graphiql/graphiql.js:2174:55
even though the GraphQL introspection query (here formatted for readability) matches, AFAIK, the introspection schema:
{
"data": {
"__schema": {
"queryType": {
"name": "QueryRoot"
},
"mutationType": null,
"subscriptionType": {
"name": "SubscriptionRoot"
},
"types": [
{
"kind": "SCALAR",
"name": "Boolean",
"description": null,
"fields": null,
"inputFields": null,
"interfaces": null,
"enumValues": null,
"possibleTypes": null
},
{
"kind": "SCALAR",
"name": "Float",
"description": null,
"fields": null,
"inputFields": null,
"interfaces": null,
"enumValues": null,
"possibleTypes": null
},
{
"kind": "SCALAR",
"name": "ID",
"description": null,
"fields": null,
"inputFields": null,
"interfaces": null,
"enumValues": null,
"possibleTypes": null
},
{
"kind": "SCALAR",
"name": "Int",
"description": null,
"fields": null,
"inputFields": null,
"interfaces": null,
"enumValues": null,
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "QueryRoot",
"description": "Root of all queries.",
"fields": [
{
"name": "hello",
"description": "Simplest possible query.",
"args": [],
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
}
],
"inputFields": null,
"interfaces": null,
"enumValues": null,
"possibleTypes": null
},
{
"kind": "SCALAR",
"name": "String",
"description": null,
"fields": null,
"inputFields": null,
"interfaces": null,
"enumValues": null,
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "SubscriptionRoot",
"description": "Root of all subscriptions.",
"fields": [
{
"name": "ticks",
"description": null,
"args": [
{
"name": "count",
"description": "Number of ticks to send via subscription.",
"type": {
"kind": "SCALAR",
"name": "Int",
"ofType": null
},
"defaultValue": "5"
}
],
"type": {
"kind": "OBJECT",
"name": "tick",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
}
],
"inputFields": null,
"interfaces": null,
"enumValues": null,
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "tick",
"description": "A subscription response.",
"fields": [
{
"name": "count",
"description": null,
"args": [],
"type": {
"kind": "SCALAR",
"name": "Int",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "time_ms",
"description": "Time when tick is emittied (as string-ified long milliseconds since epoch).",
"args": [],
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
}
],
"inputFields": null,
"interfaces": null,
"enumValues": null,
"possibleTypes": null
}
],
"directives": [
{
"name": "skip",
"description": "Skip the selection only when the `if` argument is true.",
"locations": [
"INLINE_FRAGMENT",
"FIELD",
"FRAGMENT_SPREAD"
],
"args": [
{
"name": "if",
"description": "Triggering argument for skip directive.",
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "Boolean",
"ofType": null
}
},
"defaultValue": null
}
]
},
{
"name": "include",
"description": "Include the selection only when the `if` argument is true.",
"locations": [
"INLINE_FRAGMENT",
"FIELD",
"FRAGMENT_SPREAD"
],
"args": [
{
"name": "if",
"description": "Triggering argument for include directive.",
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "Boolean",
"ofType": null
}
},
"defaultValue": null
}
]
}
]
}
}
}
You can see here in the code (buildClientSchema.js.flow, line 213):
function buildObjectDef(
objectIntrospection: IntrospectionObjectType,
): GraphQLObjectType {
if (!objectIntrospection.interfaces) {
throw new Error(
'Introspection result missing interfaces: ' +
JSON.stringify(objectIntrospection),
);
}
return new GraphQLObjectType({
name: objectIntrospection.name,
description: objectIntrospection.description,
interfaces: objectIntrospection.interfaces.map(getInterfaceType),
fields: () => buildFieldDefMap(objectIntrospection),
});
}
... there's a check that interfaces are returned ... which is IMO not valid, as interfaces
is nullable and a null was returned.
A quick scan of the source code shows that this may apply to possibleTypes
, enumValues
, inputFields
, fields
(in __Type
) and __Field.args
.
By contrast, __Directive.args
, which is obviously more recent, was added to the spec as args: [__InputValue!]!
.
I'm looking into a workaround where we define these fields as non-null, to force an empty list to be emitted into the response; still either the spec should be updated, or GraphiQL should be updated to match the current spec.