diff --git a/src/core/blueprint/operators/apollo_federation.rs b/src/core/blueprint/operators/apollo_federation.rs index a078e6fe8c..e91cb97a26 100644 --- a/src/core/blueprint/operators/apollo_federation.rs +++ b/src/core/blueprint/operators/apollo_federation.rs @@ -9,9 +9,10 @@ use crate::core::config::{ ApolloFederation, Config, ConfigModule, EntityResolver, Field, GraphQLOperationType, Resolver, }; use crate::core::ir::model::IR; +use crate::core::mustache::Segment; use crate::core::try_fold::TryFold; use crate::core::valid::{Valid, Validator}; -use crate::core::{config, Type}; +use crate::core::{config, Mustache, Type}; pub struct CompileEntityResolver<'a> { config_module: &'a ConfigModule, @@ -19,6 +20,48 @@ pub struct CompileEntityResolver<'a> { operation_type: &'a GraphQLOperationType, } +fn validate_expressions<'a>( + type_name: &str, + config_module: &ConfigModule, + expr_iter: impl Iterator, +) -> Valid<(), String> { + Valid::from_iter(expr_iter, |segment| { + if let Segment::Expression(expr) = segment { + if expr.len() > 1 && expr[0].as_str() == "value" { + return validate_iter(config_module, type_name, expr.iter().skip(1)); + } else { + Valid::succeed(()) + } + } else { + Valid::succeed(()) + } + }) + .map(|_| ()) +} + +fn validate_iter<'a>( + config_module: &ConfigModule, + current_type: &str, + fields_iter: impl Iterator, +) -> Valid<(), String> { + let mut current_type = current_type; + Valid::from_iter(fields_iter.enumerate(), |(index, key)| { + if let Some(type_def) = config_module.types.get(current_type) { + if !type_def.fields.contains_key(key) { + return Valid::fail(format!( + "Invalid key at index {}: '{}' is not a field of '{}'", + index, key, current_type + )); + } + current_type = type_def.fields[key].type_of.name(); + } else { + return Valid::fail(format!("Type '{}' not found in config", current_type)); + } + Valid::succeed(()) + }) + .map(|_| ()) +} + pub fn compile_entity_resolver(inputs: CompileEntityResolver<'_>) -> Valid { let CompileEntityResolver { config_module, entity_resolver, operation_type } = inputs; let mut resolver_by_type = HashMap::new(); @@ -30,17 +73,32 @@ pub fn compile_entity_resolver(inputs: CompileEntityResolver<'_>) -> Valid compile_http( - config_module, - http, - // inner resolver should resolve only single instance of type, not a list - false, - ), + Resolver::Http(http) => { + Valid::from_iter(http.query.iter(), |query| { + let mustache = Mustache::parse(&query.value); + validate_expressions(type_name, config_module, mustache.segments().iter()) + }) + .and_then(|_| { + let mustache = Mustache::parse(&http.path); + validate_expressions(type_name, config_module, mustache.segments().iter()) + }) + .and(validate_iter( + config_module, + type_name, + http.batch_key.iter(), + )) + .and(compile_http( + config_module, + http, + // inner resolver should resolve only single instance of type, not + // a list + false, + )) + } Resolver::Grpc(grpc) => compile_grpc(super::CompileGrpc { config_module, operation_type, diff --git a/tests/core/snapshots/apollo-federation-validation.md_error.snap b/tests/core/snapshots/apollo-federation-validation.md_error.snap new file mode 100644 index 0000000000..894633b229 --- /dev/null +++ b/tests/core/snapshots/apollo-federation-validation.md_error.snap @@ -0,0 +1,33 @@ +--- +source: tests/core/spec.rs +expression: errors +--- +[ + { + "message": "Invalid key at index 0: 'blogId' is not a field of 'Blog'", + "trace": [ + "Query", + "_entities", + "_entities" + ], + "description": null + }, + { + "message": "Invalid key at index 1: 'blogId' is not a field of 'Blog'", + "trace": [ + "Query", + "_entities", + "_entities" + ], + "description": null + }, + { + "message": "Invalid key at index 1: 'userId' is not a field of 'Blog'", + "trace": [ + "Query", + "_entities", + "_entities" + ], + "description": null + } +] diff --git a/tests/execution/apollo-federation-validation.md b/tests/execution/apollo-federation-validation.md new file mode 100644 index 0000000000..89cc6db859 --- /dev/null +++ b/tests/execution/apollo-federation-validation.md @@ -0,0 +1,32 @@ +--- +error: true +--- + +# Apollo federation validation + +```graphql @config +schema + @server(port: 8000) + @upstream(baseURL: "http://jsonplaceholder.typicode.com", httpCache: 42, batch: {delay: 100}) { + query: Query +} + +type Query { + post(id: Int!): Post @http(path: "/posts/{{.args.id}}") +} + +type User @http(path: "/users/{{.value.blog.userId}}") { + id: Int! + username: String! + blog: Blog! +} + +type Post @http(path: "/posts", query: [{key: "id", value: "{{.value.id}}"}], batchKey: ["blog", "blogId"]) { + id: Int! + blog: Blog! +} + +type Blog @http(path: "/posts", query: [{key: "id", value: "{{.value.blogId}}"}]) { + id: Int! +} +```