-
Notifications
You must be signed in to change notification settings - Fork 1.1k
Expand file tree
/
Copy pathmod.rs
More file actions
333 lines (292 loc) · 12.1 KB
/
mod.rs
File metadata and controls
333 lines (292 loc) · 12.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
use crate::data::graphql::ext::DocumentExt;
use crate::data::subgraph::DeploymentHash;
use crate::prelude::{anyhow, s};
use anyhow::Error;
use graphql_parser::{self, Pos};
use serde::{Deserialize, Serialize};
use thiserror::Error;
use std::collections::BTreeMap;
use std::fmt;
use std::iter::FromIterator;
/// Generate full-fledged API schemas from existing GraphQL schemas.
mod api;
/// Utilities for working with GraphQL schema ASTs.
pub mod ast;
mod entity_key;
mod entity_type;
mod fulltext;
mod input_schema;
pub use api::{is_introspection_field, APISchemaError, INTROSPECTION_QUERY_TYPE};
pub use api::{ApiSchema, ErrorPolicy};
pub use entity_key::EntityKey;
pub use entity_type::{AsEntityTypeName, EntityType};
pub use fulltext::{FulltextAlgorithm, FulltextConfig, FulltextDefinition, FulltextLanguage};
pub use input_schema::{Field, InputSchema, InterfaceType, ObjectType};
pub const SCHEMA_TYPE_NAME: &str = "_Schema_";
pub const INTROSPECTION_SCHEMA_FIELD_NAME: &str = "__schema";
pub const META_FIELD_TYPE: &str = "_Meta_";
pub const META_FIELD_NAME: &str = "_meta";
pub const INTROSPECTION_TYPE_FIELD_NAME: &str = "__type";
pub const BLOCK_FIELD_TYPE: &str = "_Block_";
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct Strings(Vec<String>);
impl fmt::Display for Strings {
fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> {
let s = self.0.join(", ");
write!(f, "{}", s)
}
}
#[derive(Debug, Error, PartialEq, Eq)]
pub enum SchemaValidationError {
#[error("Invalid schema: {0}")]
InvalidSchema(String),
#[error("Interface `{0}` not defined")]
InterfaceUndefined(String),
#[error("@entity directive missing on the following types: `{0}`")]
EntityDirectivesMissing(Strings),
#[error(
"Entity type `{0}` does not satisfy interface `{1}` because it is missing \
the following fields: {2}"
)]
InterfaceFieldsMissing(String, String, Strings), // (type, interface, missing_fields)
#[error("Implementors of interface `{0}` use different id types `{1}`. They must all use the same type")]
InterfaceImplementorsMixId(String, String),
#[error("Field `{1}` in type `{0}` has invalid @derivedFrom: {2}")]
InvalidDerivedFrom(String, String, String), // (type, field, reason)
#[error("The following type names are reserved: `{0}`")]
UsageOfReservedTypes(Strings),
#[error("_Schema_ type is only for @fulltext and must not have any fields")]
SchemaTypeWithFields,
#[error("The _Schema_ type only allows @fulltext directives")]
InvalidSchemaTypeDirectives,
#[error("Type `{0}`, field `{1}`: type `{2}` is not defined")]
FieldTypeUnknown(String, String, String), // (type_name, field_name, field_type)
#[error("Imported type `{0}` does not exist in the `{1}` schema")]
ImportedTypeUndefined(String, String), // (type_name, schema)
#[error("Fulltext directive name undefined")]
FulltextNameUndefined,
#[error("Fulltext directive name overlaps with type: {0}")]
FulltextNameConflict(String),
#[error("Fulltext directive name overlaps with an existing entity field or a top-level query field: {0}")]
FulltextNameCollision(String),
#[error("Fulltext language is undefined")]
FulltextLanguageUndefined,
#[error("Fulltext language is invalid: {0}")]
FulltextLanguageInvalid(String),
#[error("Fulltext algorithm is undefined")]
FulltextAlgorithmUndefined,
#[error("Fulltext algorithm is invalid: {0}")]
FulltextAlgorithmInvalid(String),
#[error("Fulltext include is invalid")]
FulltextIncludeInvalid,
#[error("Fulltext directive requires an 'include' list")]
FulltextIncludeUndefined,
#[error("Fulltext 'include' list must contain an object")]
FulltextIncludeObjectMissing,
#[error(
"Fulltext 'include' object must contain 'entity' (String) and 'fields' (List) attributes"
)]
FulltextIncludeEntityMissingOrIncorrectAttributes,
#[error("Fulltext directive includes an entity not found on the subgraph schema")]
FulltextIncludedEntityNotFound,
#[error("Fulltext include field must have a 'name' attribute")]
FulltextIncludedFieldMissingRequiredProperty,
#[error("Fulltext entity field, {0}, not found or not a string")]
FulltextIncludedFieldInvalid(String),
#[error("Type {0} is missing an `id` field")]
IdFieldMissing(String),
#[error("{0}")]
IllegalIdType(String),
}
/// A validated and preprocessed GraphQL schema for a subgraph.
#[derive(Clone, Debug, PartialEq)]
pub struct Schema {
pub id: DeploymentHash,
pub document: s::Document,
// Maps type name to implemented interfaces.
pub interfaces_for_type: BTreeMap<String, Vec<s::InterfaceType>>,
// Maps an interface name to the list of entities that implement it.
pub types_for_interface: BTreeMap<String, Vec<s::ObjectType>>,
}
impl Schema {
/// Create a new schema. The document must already have been validated
//
// TODO: The way some validation is expected to be done beforehand, and
// some is done here makes it incredibly murky whether a `Schema` is
// fully validated. The code should be changed to make sure that a
// `Schema` is always fully valid
pub fn new(id: DeploymentHash, document: s::Document) -> Result<Self, SchemaValidationError> {
let (interfaces_for_type, types_for_interface) = Self::collect_interfaces(&document)?;
let mut schema = Schema {
id: id.clone(),
document,
interfaces_for_type,
types_for_interface,
};
schema.add_subgraph_id_directives(id);
Ok(schema)
}
fn collect_interfaces(
document: &s::Document,
) -> Result<
(
BTreeMap<String, Vec<s::InterfaceType>>,
BTreeMap<String, Vec<s::ObjectType>>,
),
SchemaValidationError,
> {
// Initialize with an empty vec for each interface, so we don't
// miss interfaces that have no implementors.
let mut types_for_interface =
BTreeMap::from_iter(document.definitions.iter().filter_map(|d| match d {
s::Definition::TypeDefinition(s::TypeDefinition::Interface(t)) => {
Some((t.name.to_string(), vec![]))
}
_ => None,
}));
let mut interfaces_for_type = BTreeMap::<_, Vec<_>>::new();
for object_type in document.get_object_type_definitions() {
for implemented_interface in &object_type.implements_interfaces {
let interface_type = document
.definitions
.iter()
.find_map(|def| match def {
s::Definition::TypeDefinition(s::TypeDefinition::Interface(i))
if i.name.eq(implemented_interface) =>
{
Some(i.clone())
}
_ => None,
})
.ok_or_else(|| {
SchemaValidationError::InterfaceUndefined(implemented_interface.clone())
})?;
Self::validate_interface_implementation(object_type, &interface_type)?;
interfaces_for_type
.entry(object_type.name.to_owned())
.or_default()
.push(interface_type);
types_for_interface
.get_mut(implemented_interface)
.unwrap()
.push(object_type.clone());
}
}
Ok((interfaces_for_type, types_for_interface))
}
pub fn parse(raw: &str, id: DeploymentHash) -> Result<Self, Error> {
let document = graphql_parser::parse_schema(raw)?.into_static();
Schema::new(id, document).map_err(Into::into)
}
/// Returned map has one an entry for each interface in the schema.
pub fn types_for_interface(&self) -> &BTreeMap<String, Vec<s::ObjectType>> {
&self.types_for_interface
}
/// Returns `None` if the type implements no interfaces.
pub fn interfaces_for_type(&self, type_name: &str) -> Option<&Vec<s::InterfaceType>> {
self.interfaces_for_type.get(type_name)
}
// Adds a @subgraphId(id: ...) directive to object/interface/enum types in the schema.
pub fn add_subgraph_id_directives(&mut self, id: DeploymentHash) {
for definition in self.document.definitions.iter_mut() {
let subgraph_id_argument = (String::from("id"), s::Value::String(id.to_string()));
let subgraph_id_directive = s::Directive {
name: "subgraphId".to_string(),
position: Pos::default(),
arguments: vec![subgraph_id_argument],
};
if let s::Definition::TypeDefinition(ref mut type_definition) = definition {
let (name, directives) = match type_definition {
s::TypeDefinition::Object(object_type) => {
(&object_type.name, &mut object_type.directives)
}
s::TypeDefinition::Interface(interface_type) => {
(&interface_type.name, &mut interface_type.directives)
}
s::TypeDefinition::Enum(enum_type) => {
(&enum_type.name, &mut enum_type.directives)
}
s::TypeDefinition::Scalar(scalar_type) => {
(&scalar_type.name, &mut scalar_type.directives)
}
s::TypeDefinition::InputObject(input_object_type) => {
(&input_object_type.name, &mut input_object_type.directives)
}
s::TypeDefinition::Union(union_type) => {
(&union_type.name, &mut union_type.directives)
}
};
if !name.eq(SCHEMA_TYPE_NAME)
&& !directives
.iter()
.any(|directive| directive.name.eq("subgraphId"))
{
directives.push(subgraph_id_directive);
}
};
}
}
/// Validate that `object` implements `interface`.
fn validate_interface_implementation(
object: &s::ObjectType,
interface: &s::InterfaceType,
) -> Result<(), SchemaValidationError> {
// Check that all fields in the interface exist in the object with same name and type.
let mut missing_fields = vec![];
for i in &interface.fields {
if !object
.fields
.iter()
.any(|o| o.name.eq(&i.name) && o.field_type.eq(&i.field_type))
{
missing_fields.push(i.to_string().trim().to_owned());
}
}
if !missing_fields.is_empty() {
Err(SchemaValidationError::InterfaceFieldsMissing(
object.name.clone(),
interface.name.clone(),
Strings(missing_fields),
))
} else {
Ok(())
}
}
fn subgraph_schema_object_type(&self) -> Option<&s::ObjectType> {
self.document
.get_object_type_definitions()
.into_iter()
.find(|object_type| object_type.name.eq(SCHEMA_TYPE_NAME))
}
}
#[test]
fn non_existing_interface() {
let schema = "type Foo implements Bar @entity { foo: Int }";
let res = Schema::parse(schema, DeploymentHash::new("dummy").unwrap());
let error = res
.unwrap_err()
.downcast::<SchemaValidationError>()
.unwrap();
assert_eq!(
error,
SchemaValidationError::InterfaceUndefined("Bar".to_owned())
);
}
#[test]
fn invalid_interface_implementation() {
let schema = "
interface Foo {
x: Int,
y: Int
}
type Bar implements Foo @entity {
x: Boolean
}
";
let res = Schema::parse(schema, DeploymentHash::new("dummy").unwrap());
assert_eq!(
res.unwrap_err().to_string(),
"Entity type `Bar` does not satisfy interface `Foo` because it is missing \
the following fields: x: Int, y: Int",
);
}