From 27c9c3cbd2b396d1248fb98eff1376c068d7711f Mon Sep 17 00:00:00 2001 From: David Lutterkort Date: Thu, 28 Sep 2023 15:06:44 -0700 Subject: [PATCH] graph: Validate the id field of object types This hadn't been done before, though in practice it was probably never violated (presumably since graph-cli enforces such rules, and because trying to deploy such a schema would end badly) The `examples/validate.rs` is a tool that makes it possible to bulk validate subgraph schemas. --- graph/examples/validate.rs | 108 +++++++++++++++++++++++++++++++ graph/src/schema/input_schema.rs | 66 ++++++++++++++++++- graph/src/schema/mod.rs | 4 ++ 3 files changed, 176 insertions(+), 2 deletions(-) create mode 100644 graph/examples/validate.rs diff --git a/graph/examples/validate.rs b/graph/examples/validate.rs new file mode 100644 index 00000000000..f210f280619 --- /dev/null +++ b/graph/examples/validate.rs @@ -0,0 +1,108 @@ +/// Validate subgraph schemas by parsing them into `InputSchema` and making +/// sure that they are valid +/// +/// The input files must be in a particular format; that can be generated by +/// running this script against graph-node shard(s). Before running it, +/// change the `dbs` variable to list all databases against which it should +/// run. +/// +/// ``` +/// #! /bin/bash +/// +/// read -r -d '' query < ! { + println!("{}", msg); + println!("usage: validate schema.graphql ..."); + println!("\nValidate subgraph schemas"); + std::process::exit(1); +} + +pub fn ensure(res: Result, msg: &str) -> T { + match res { + Ok(ok) => ok, + Err(err) => { + eprintln!("{}:\n {}", msg, err); + exit(1) + } + } +} + +fn subgraph_id(schema: &s::Document) -> DeploymentHash { + let id = schema + .get_object_type_definitions() + .first() + .and_then(|obj_type| obj_type.find_directive("subgraphId")) + .and_then(|dir| dir.argument("id")) + .and_then(|arg| match arg { + s::Value::String(s) => Some(s.to_owned()), + _ => None, + }) + .unwrap_or("unknown".to_string()); + DeploymentHash::new(id).expect("subgraph id is not a valid deployment hash") +} + +#[derive(Deserialize)] +struct Entry { + id: i32, + schema: String, +} + +pub fn main() { + // Allow fulltext search in schemas + std::env::set_var("GRAPH_ALLOW_NON_DETERMINISTIC_FULLTEXT_SEARCH", "true"); + + let args: Vec = env::args().collect(); + if args.len() < 2 { + usage("please provide a subgraph schema"); + } + for arg in &args[1..] { + println!("Validating schemas from {arg}"); + let file = File::open(arg).expect("file exists"); + let rdr = BufReader::new(file); + for line in rdr.lines() { + let line = line.expect("invalid line").replace("\\\\", "\\"); + let entry = serde_json::from_str::(&line).expect("line is valid json"); + + let raw = &entry.schema; + let schema = ensure( + parse_schema(raw).map(|v| v.into_static()), + &format!("Failed to parse schema sgd{}", entry.id), + ); + let id = subgraph_id(&schema); + match InputSchema::parse(raw, id.clone()) { + Ok(_) => println!("sgd{}[{}]: OK", entry.id, id), + Err(e) => println!("sgd{}[{}]: {}", entry.id, id, e), + } + } + } +} diff --git a/graph/src/schema/input_schema.rs b/graph/src/schema/input_schema.rs index 125682dcd85..ecb8e1ec4ef 100644 --- a/graph/src/schema/input_schema.rs +++ b/graph/src/schema/input_schema.rs @@ -532,7 +532,7 @@ mod validations { graphql::{ ext::DirectiveFinder, DirectiveExt, DocumentExt, ObjectTypeExt, TypeExt, ValueExt, }, - store::{ValueType, ID}, + store::{IdType, ValueType, ID}, }, prelude::s, schema::{ @@ -556,6 +556,7 @@ mod validations { .map(Result::unwrap_err) .collect(); + errors.append(&mut validate_id_types(schema)); errors.append(&mut validate_fields(schema)); errors.append(&mut validate_fulltext_directives(schema)); @@ -802,6 +803,29 @@ mod validations { }) } + /// 1. All object types besides `_Schema_` must have an id field + /// 2. The id field must be recognized by IdType + fn validate_id_types(schema: &Schema) -> Vec { + schema + .document + .get_object_type_definitions() + .into_iter() + .filter(|obj_type| obj_type.name.ne(SCHEMA_TYPE_NAME)) + .fold(vec![], |mut errors, object_type| { + match object_type.field(&*ID) { + None => errors.push(SchemaValidationError::IdFieldMissing( + object_type.name.clone(), + )), + Some(_) => { + if let Err(e) = IdType::try_from(object_type) { + errors.push(SchemaValidationError::IllegalIdType(e.to_string())); + } + } + } + errors + }) + } + /// Checks if the schema is using types that are reserved /// by `graph-node` fn validate_reserved_types_usage(schema: &Schema) -> Result<(), SchemaValidationError> { @@ -1057,6 +1081,38 @@ mod validations { use super::*; + fn parse(schema: &str) -> Schema { + let hash = DeploymentHash::new("test").unwrap(); + Schema::parse(schema, hash).unwrap() + } + + #[test] + fn object_types_have_id() { + const NO_ID: &str = "type User @entity { name: String! }"; + const ID_BIGINT: &str = "type User @entity { id: BigInt! }"; + const INTF_NO_ID: &str = "interface Person { name: String! }"; + const ROOT_SCHEMA: &str = "type _Schema_"; + + let res = validate(&parse(NO_ID)); + assert_eq!( + res, + Err(vec![SchemaValidationError::IdFieldMissing( + "User".to_string() + )]) + ); + + let res = validate(&parse(ID_BIGINT)); + let errs = res.unwrap_err(); + assert_eq!(1, errs.len()); + assert!(matches!(errs[0], SchemaValidationError::IllegalIdType(_))); + + let res = validate(&parse(INTF_NO_ID)); + assert_eq!(Ok(()), res); + + let res = validate(&parse(ROOT_SCHEMA)); + assert_eq!(Ok(()), res); + } + #[test] fn interface_implementations_id_type() { fn check_schema(bar_id: &str, baz_id: &str, ok: bool) { @@ -1230,7 +1286,10 @@ type A @entity { let dummy_hash = DeploymentHash::new("dummy").unwrap(); for reserved_type in reserved_types { - let schema = format!("type {} @entity {{ _: Boolean }}\n", reserved_type); + let schema = format!( + "type {} @entity {{ id: String! _: Boolean }}\n", + reserved_type + ); let schema = Schema::parse(&schema, dummy_hash.clone()).unwrap(); @@ -1248,12 +1307,15 @@ type A @entity { fn test_reserved_filter_and_group_by_types_validation() { const SCHEMA: &str = r#" type Gravatar @entity { + id: String! _: Boolean } type Gravatar_filter @entity { + id: String! _: Boolean } type Gravatar_orderBy @entity { + id: String! _: Boolean } "#; diff --git a/graph/src/schema/mod.rs b/graph/src/schema/mod.rs index e3586367b45..270f846790e 100644 --- a/graph/src/schema/mod.rs +++ b/graph/src/schema/mod.rs @@ -104,6 +104,10 @@ pub enum SchemaValidationError { 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.