Skip to content

Commit 5f512ae

Browse files
committed
graphql: Mix the introspection schema into the API schema
That makes it possible to use the same schema for data nad for introspection queries
1 parent c89d8c3 commit 5f512ae

File tree

6 files changed

+222
-12
lines changed

6 files changed

+222
-12
lines changed

graph/src/data/graphql/ext.rs

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -307,6 +307,41 @@ impl DirectiveFinder for Vec<Directive> {
307307
}
308308
}
309309

310+
pub trait TypeDefinitionExt {
311+
fn name(&self) -> &str;
312+
313+
// Return `true` if this is the definition of a type from the
314+
// introspection schema
315+
fn is_introspection(&self) -> bool {
316+
self.name().starts_with("__")
317+
}
318+
}
319+
320+
impl TypeDefinitionExt for TypeDefinition {
321+
fn name(&self) -> &str {
322+
match self {
323+
TypeDefinition::Scalar(t) => &t.name,
324+
TypeDefinition::Object(t) => &t.name,
325+
TypeDefinition::Interface(t) => &t.name,
326+
TypeDefinition::Union(t) => &t.name,
327+
TypeDefinition::Enum(t) => &t.name,
328+
TypeDefinition::InputObject(t) => &t.name,
329+
}
330+
}
331+
}
332+
333+
pub trait FieldExt {
334+
// Return `true` if this is the name of one of the query fields from the
335+
// introspection schema
336+
fn is_introspection(&self) -> bool;
337+
}
338+
339+
impl FieldExt for Field {
340+
fn is_introspection(&self) -> bool {
341+
&self.name == "__schema" || &self.name == "__type"
342+
}
343+
}
344+
310345
#[cfg(test)]
311346
mod directive_finder_tests {
312347
use graphql_parser::parse_schema;

graph/src/data/introspection.graphql

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
# A GraphQL introspection schema for inclusion in a subgraph's API schema.
2+
# The schema differs from the 'standard' introspection schema in that it
3+
# doesn't have a Query type nor scalar declarations as they come from the
4+
# API schema.
5+
6+
type __Schema {
7+
types: [__Type!]!
8+
queryType: __Type!
9+
mutationType: __Type
10+
subscriptionType: __Type
11+
directives: [__Directive!]!
12+
}
13+
14+
type __Type {
15+
kind: __TypeKind!
16+
name: String
17+
description: String
18+
19+
# OBJECT and INTERFACE only
20+
fields(includeDeprecated: Boolean = false): [__Field!]
21+
22+
# OBJECT only
23+
interfaces: [__Type!]
24+
25+
# INTERFACE and UNION only
26+
possibleTypes: [__Type!]
27+
28+
# ENUM only
29+
enumValues(includeDeprecated: Boolean = false): [__EnumValue!]
30+
31+
# INPUT_OBJECT only
32+
inputFields: [__InputValue!]
33+
34+
# NON_NULL and LIST only
35+
ofType: __Type
36+
}
37+
38+
type __Field {
39+
name: String!
40+
description: String
41+
args: [__InputValue!]!
42+
type: __Type!
43+
isDeprecated: Boolean!
44+
deprecationReason: String
45+
}
46+
47+
type __InputValue {
48+
name: String!
49+
description: String
50+
type: __Type!
51+
defaultValue: String
52+
}
53+
54+
type __EnumValue {
55+
name: String!
56+
description: String
57+
isDeprecated: Boolean!
58+
deprecationReason: String
59+
}
60+
61+
enum __TypeKind {
62+
SCALAR
63+
OBJECT
64+
INTERFACE
65+
UNION
66+
ENUM
67+
INPUT_OBJECT
68+
LIST
69+
NON_NULL
70+
}
71+
72+
type __Directive {
73+
name: String!
74+
description: String
75+
locations: [__DirectiveLocation!]!
76+
args: [__InputValue!]!
77+
}
78+
79+
enum __DirectiveLocation {
80+
QUERY
81+
MUTATION
82+
SUBSCRIPTION
83+
FIELD
84+
FRAGMENT_DEFINITION
85+
FRAGMENT_SPREAD
86+
INLINE_FRAGMENT
87+
SCHEMA
88+
SCALAR
89+
OBJECT
90+
FIELD_DEFINITION
91+
ARGUMENT_DEFINITION
92+
INTERFACE
93+
UNION
94+
ENUM
95+
ENUM_VALUE
96+
INPUT_OBJECT
97+
INPUT_FIELD_DEFINITION
98+
}

graph/src/data/schema.rs

Lines changed: 73 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ use crate::data::graphql::ext::{DirectiveExt, DirectiveFinder, DocumentExt, Type
33
use crate::data::store::ValueType;
44
use crate::data::subgraph::{DeploymentHash, SubgraphName};
55
use crate::prelude::{
6+
lazy_static,
67
q::Value,
78
s::{self, Definition, InterfaceType, ObjectType, TypeDefinition, *},
89
};
@@ -355,8 +356,15 @@ pub struct ApiSchema {
355356
}
356357

357358
impl ApiSchema {
358-
/// `api_schema` will typically come from `fn api_schema` in the graphql crate.
359-
pub fn from_api_schema(api_schema: Schema) -> Result<Self, anyhow::Error> {
359+
/// `api_schema` will typically come from `fn api_schema` in the graphql
360+
/// crate.
361+
///
362+
/// In addition, the API schema has an introspection schema mixed into
363+
/// `api_schema`. In particular, the `Query` type has fields called
364+
/// `__schema` and `__type`
365+
pub fn from_api_schema(mut api_schema: Schema) -> Result<Self, anyhow::Error> {
366+
add_introspection_schema(&mut api_schema.document);
367+
360368
let query_type = api_schema
361369
.document
362370
.get_root_query_type()
@@ -397,6 +405,69 @@ impl ApiSchema {
397405
}
398406
}
399407

408+
fn add_introspection_schema(schema: &mut Document) {
409+
lazy_static! {
410+
static ref INTROSPECTION_SCHEMA: Document = {
411+
let schema = include_str!("introspection.graphql");
412+
parse_schema(schema).expect("the schema `introspection.graphql` is invalid")
413+
};
414+
}
415+
416+
fn introspection_fields() -> Vec<Field> {
417+
// Generate fields for the root query fields in an introspection schema,
418+
// the equivalent of the fields of the `Query` type:
419+
//
420+
// type Query {
421+
// __schema: __Schema!
422+
// __type(name: String!): __Type
423+
// }
424+
425+
let type_args = vec![InputValue {
426+
position: Pos::default(),
427+
description: None,
428+
name: "name".to_string(),
429+
value_type: Type::NonNullType(Box::new(Type::NamedType("String".to_string()))),
430+
default_value: None,
431+
directives: vec![],
432+
}];
433+
434+
vec![
435+
Field {
436+
position: Pos::default(),
437+
description: None,
438+
name: "__schema".to_string(),
439+
arguments: vec![],
440+
field_type: Type::NonNullType(Box::new(Type::NamedType("__Schema".to_string()))),
441+
directives: vec![],
442+
},
443+
Field {
444+
position: Pos::default(),
445+
description: None,
446+
name: "__type".to_string(),
447+
arguments: type_args,
448+
field_type: Type::NamedType("__Type".to_string()),
449+
directives: vec![],
450+
},
451+
]
452+
}
453+
454+
schema
455+
.definitions
456+
.extend(INTROSPECTION_SCHEMA.definitions.iter().cloned());
457+
458+
let query_type = schema
459+
.definitions
460+
.iter_mut()
461+
.filter_map(|d| match d {
462+
Definition::TypeDefinition(TypeDefinition::Object(t)) if t.name == "Query" => Some(t),
463+
_ => None,
464+
})
465+
.peekable()
466+
.next()
467+
.expect("no root `Query` in the schema");
468+
query_type.fields.append(&mut introspection_fields());
469+
}
470+
400471
/// A validated and preprocessed GraphQL schema for a subgraph.
401472
#[derive(Clone, Debug, PartialEq)]
402473
pub struct Schema {

graphql/src/introspection/resolver.rs

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
use graph::data::graphql::ext::{FieldExt, TypeDefinitionExt};
12
use graphql_parser::Pos;
23
use std::collections::{BTreeMap, HashMap};
34

@@ -10,18 +11,21 @@ use crate::schema::ast as sast;
1011

1112
type TypeObjectsMap = BTreeMap<String, r::Value>;
1213

14+
/// Our Schema has the introspection schema mixed in. When we build the
15+
/// `TypeObjectsMap`, suppress types and fields that belong to the
16+
/// introspection schema
1317
fn schema_type_objects(schema: &Schema) -> TypeObjectsMap {
14-
sast::get_type_definitions(&schema.document).iter().fold(
15-
BTreeMap::new(),
16-
|mut type_objects, typedef| {
18+
sast::get_type_definitions(&schema.document)
19+
.iter()
20+
.filter(|def| !def.is_introspection())
21+
.fold(BTreeMap::new(), |mut type_objects, typedef| {
1722
let type_name = sast::get_type_name(typedef);
1823
if !type_objects.contains_key(type_name) {
1924
let type_object = type_definition_object(schema, &mut type_objects, typedef);
2025
type_objects.insert(type_name.to_owned(), type_object);
2126
}
2227
type_objects
23-
},
24-
)
28+
})
2529
}
2630

2731
fn type_object(schema: &Schema, type_objects: &mut TypeObjectsMap, t: &s::Type) -> r::Value {
@@ -167,6 +171,7 @@ fn field_objects(
167171
r::Value::List(
168172
fields
169173
.iter()
174+
.filter(|field| !field.is_introspection())
170175
.map(|field| field_object(schema, type_objects, field))
171176
.collect(),
172177
)

graphql/src/schema/api.rs

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -73,9 +73,8 @@ impl TryFrom<&r::Value> for ErrorPolicy {
7373
/// Derives a full-fledged GraphQL API schema from an input schema.
7474
///
7575
/// The input schema should only have type/enum/interface/union definitions
76-
/// and must not include a root Query type. This Query type is derived,
77-
/// with all its fields and their input arguments, based on the existing
78-
/// types.
76+
/// and must not include a root Query type. This Query type is derived, with
77+
/// all its fields and their input arguments, based on the existing types.
7978
pub fn api_schema(input_schema: &Document) -> Result<Document, APISchemaError> {
8079
// Refactor: Take `input_schema` by value.
8180
let object_types = input_schema.get_object_type_definitions();
@@ -528,9 +527,9 @@ fn add_query_type(
528527

529528
let mut fields = object_types
530529
.iter()
531-
.map(|t| &t.name)
530+
.map(|t| t.name.as_str())
532531
.filter(|name| !name.eq(&SCHEMA_TYPE_NAME))
533-
.chain(interface_types.iter().map(|t| &t.name))
532+
.chain(interface_types.iter().map(|t| t.name.as_str()))
534533
.flat_map(|name| query_fields_for_type(name))
535534
.collect::<Vec<Field>>();
536535
let mut fulltext_fields = schema

store/postgres/tests/store.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
use graph::data::graphql::ext::TypeDefinitionExt;
12
use graph_chain_ethereum::{Mapping, MappingABI};
23
use graph_mock::MockMetricsRegistry;
34
use hex_literal::hex;
@@ -1480,6 +1481,7 @@ fn subgraph_schema_types_have_subgraph_id_directive() {
14801481
s::Definition::TypeDefinition(typedef) => Some(typedef),
14811482
_ => None,
14821483
})
1484+
.filter(|typedef| !typedef.is_introspection())
14831485
{
14841486
// Verify that all types have a @subgraphId directive on them
14851487
let directive = match typedef {

0 commit comments

Comments
 (0)