use async_graphql::{Response, ServerError};
use serde_json::{json, Map, Value};
use std::collections::HashMap;
fn code_for(error: &ServerError) -> &'static str {
if let Some(extensions) = &error.extensions {
if let Some(Value::String(code)) = extensions.get("code").map(value_of) {
return match code.as_str() {
"validation-failed" => "validation-failed",
"bad-request" => "bad-request",
"access-denied" => "access-denied",
"invalid-jwt" => "invalid-jwt",
"jwt-missing-role-claims" => "jwt-missing-role-claims",
"not-found" => "not-found",
"permission-error" => "permission-error",
"constraint-violation" => "constraint-violation",
"data-exception" => "data-exception",
"not-supported" => "not-supported",
"parse-failed" => "parse-failed",
_ => "unexpected",
};
}
}
let message = error.message.to_ascii_lowercase();
if message.contains("invalid value for argument")
|| message.contains("does not contain the value")
|| message.contains("is not defined by operation")
|| message.contains("not configured for mutations")
|| message.contains("not configured for subscriptions")
{
"validation-failed"
} else if message.contains("permission") || message.contains("not allowed") {
"permission-error"
} else if message.contains("violates") || message.contains("constraint") {
"constraint-violation"
} else if message.contains("unknown argument")
|| message.contains("cannot query field")
|| message.contains("expected")
|| message.contains("unknown field")
|| message.starts_with("variable \"")
{
"validation-failed"
} else {
"postgres-error"
}
}
fn value_of(value: &async_graphql::Value) -> Value {
serde_json::to_value(value).unwrap_or(Value::Null)
}
fn path_for(error: &ServerError) -> String {
use async_graphql::PathSegment;
if let Some(Value::String(path)) = error
.extensions
.as_ref()
.and_then(|extensions| extensions.get("path"))
.map(value_of)
{
return path;
}
if error.path.is_empty() {
return "$".to_string();
}
let mut rendered = String::from("$");
for segment in &error.path {
if let PathSegment::Field(name) = segment {
rendered.push_str(".selectionSet.");
rendered.push_str(name);
}
}
rendered
}
pub fn envelope(response: Response) -> Value {
if !response.errors.is_empty() {
let errors: Vec<Value> = response
.errors
.iter()
.map(|error| {
json!({
"message": error.message,
"extensions": {
"path": path_for(error),
"code": code_for(error),
}
})
})
.collect();
return json!({ "errors": errors });
}
let mut body = Map::new();
body.insert("data".to_string(), value_of(&response.data));
Value::Object(body)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Endpoint {
Current,
Legacy,
}
pub fn status_for(body: &Value, endpoint: Endpoint) -> u16 {
if endpoint == Endpoint::Current {
return 200;
}
let refused = body
.get("errors")
.and_then(Value::as_array)
.is_some_and(|errors| {
errors.iter().any(|error| {
error.get("message").and_then(Value::as_str) == Some(crate::role::CHECK_FAILED)
})
});
match refused {
true => 400,
false => 200,
}
}
pub fn denied(message: &str) -> Value {
refused(message, "access-denied")
}
#[doc(hidden)]
pub fn invalid_jwt(reason: &str) -> Value {
refused(&format!("Could not verify JWT: {reason}"), "invalid-jwt")
}
#[doc(hidden)]
pub fn missing_role_claim(claim: &str) -> Value {
refused(
&format!("JWT claim does not contain {claim}"),
"jwt-missing-role-claims",
)
}
fn refused(message: &str, code: &str) -> Value {
let mut error = ServerError::new(message, None);
let mut extensions = async_graphql::ErrorExtensionValues::default();
extensions.set("code", code);
error.extensions = Some(extensions);
let mut response = Response::new(async_graphql::Value::Null);
response.errors = vec![error];
envelope(response)
}
pub fn persisted_query(request: &async_graphql::Request) -> bool {
request.extensions.contains_key("persistedQuery")
}
pub fn not_supported(message: &str) -> Value {
let mut error = ServerError::new(message, None);
let mut extensions = async_graphql::ErrorExtensionValues::default();
extensions.set("code", "not-supported");
extensions.set("path", "$");
error.extensions = Some(extensions);
let mut response = Response::new(async_graphql::Value::Null);
response.errors = vec![error];
envelope(response)
}
pub fn malformed(message: &str) -> Value {
let mut error = ServerError::new(message, None);
let mut extensions = async_graphql::ErrorExtensionValues::default();
extensions.set("code", "bad-request");
extensions.set("path", "$");
error.extensions = Some(extensions);
let mut response = Response::new(async_graphql::Value::Null);
response.errors = vec![error];
envelope(response)
}
#[cfg(test)]
mod tests {
use super::*;
use async_graphql::{PathSegment, ServerError};
use pretty_assertions::assert_eq;
fn error_at(message: &str, path: Vec<PathSegment>) -> ServerError {
let mut error = ServerError::new(message, None);
error.path = path;
error
}
#[test]
fn only_a_refused_write_is_answered_with_a_failure_status() {
let refused = json!({"errors": [{"message": crate::role::CHECK_FAILED,
"extensions": {"code": "permission-error"}}]});
assert_eq!(status_for(&refused, Endpoint::Legacy), 400);
assert_eq!(status_for(&refused, Endpoint::Current), 200);
let denied = json!({"errors": [{"message": "not allowed",
"extensions": {"code": "permission-error"}}]});
assert_eq!(status_for(&denied, Endpoint::Legacy), 200);
assert_eq!(
status_for(&json!({"data": {"article": []}}), Endpoint::Legacy),
200
);
}
#[test]
fn a_role_may_be_refused_the_schema_without_being_refused_its_rows() {
let asked = |query: &str| {
let doc = async_graphql::parser::parse_query(query).unwrap();
introspection_refusal(&doc, "reader").map(|e| e.message)
};
let refusal = Some("introspection is disabled for role: \"reader\"".to_string());
assert_eq!(asked("{ __schema { types { name } } }"), refusal);
assert_eq!(asked("{ __type(name: \"article\") { name } }"), refusal);
assert_eq!(asked("{ article { __typename id } }"), None);
assert_eq!(asked("{ article { id } }"), None);
}
#[test]
fn a_failed_response_has_no_data_key() {
let mut response = Response::new(async_graphql::Value::Null);
response.errors = vec![error_at("no", vec![])];
let body = envelope(response);
assert!(body.get("data").is_none());
assert!(body.get("errors").is_some());
}
#[test]
fn a_successful_response_has_no_errors_key() {
let body = envelope(Response::new(async_graphql::Value::Null));
assert!(body.get("errors").is_none());
assert!(body.get("data").is_some());
}
#[test]
fn path_is_rooted_at_the_selection_set() {
let error = error_at(
"boom",
vec![
PathSegment::Field("insert_author".to_string()),
PathSegment::Index(0),
PathSegment::Field("name".to_string()),
],
);
assert_eq!(
path_for(&error),
"$.selectionSet.insert_author.selectionSet.name"
);
}
#[test]
fn a_persisted_query_is_refused_as_unsupported() {
let plain = async_graphql::Request::new("{ author { id } }");
assert!(!persisted_query(&plain));
let mut carried = async_graphql::Request::new("{ author { id } }");
carried.extensions.0.insert(
"persistedQuery".to_string(),
async_graphql::Value::Object(Default::default()),
);
assert!(persisted_query(&carried));
assert_eq!(
not_supported("PersistedQueryNotSupported"),
json!({"errors": [{
"message": "PersistedQueryNotSupported",
"extensions": {"path": "$", "code": "not-supported"}
}]})
);
}
#[test]
fn a_path_given_with_the_error_is_the_one_answered() {
let mut error = error_at(
"boom",
vec![PathSegment::Field("insert_author".to_string())],
);
let mut extensions = async_graphql::ErrorExtensionValues::default();
extensions.set("code", "permission-error");
extensions.set("path", "$.selectionSet.insert_author.args.objects");
error.extensions = Some(extensions);
assert_eq!(
path_for(&error),
"$.selectionSet.insert_author.args.objects"
);
}
#[test]
fn an_error_with_no_path_is_rooted_at_the_document() {
assert_eq!(path_for(&error_at("boom", vec![])), "$");
}
#[test]
fn a_validation_message_is_coded_as_one() {
let error = ServerError::new("Unknown argument \"filter\" on field \"author\"", None);
assert_eq!(code_for(&error), "validation-failed");
}
#[test]
fn a_constraint_message_is_coded_as_one() {
let error = ServerError::new("duplicate key value violates unique constraint", None);
assert_eq!(code_for(&error), "constraint-violation");
}
}
pub fn allow_unused_variables(request: async_graphql::Request) -> async_graphql::Request {
prepare(None, request, None).unwrap_or_else(|(request, _)| request)
}
#[allow(clippy::result_large_err)]
pub fn prepare(
schema: Option<&async_graphql::dynamic::Schema>,
request: async_graphql::Request,
introspection_disabled_for: Option<&str>,
) -> Result<async_graphql::Request, (async_graphql::Request, Vec<ServerError>)> {
let mut request = rewrite_document(schema, request);
let doc = match async_graphql::parser::parse_query(&request.query) {
Ok(doc) => doc,
Err(_) => {
return Err((
request,
vec![coded(
"not a valid graphql query".to_string(),
async_graphql::Pos::default(),
"$.query",
)],
))
}
};
if let Some(schema) = schema {
if let Some(role) = introspection_disabled_for {
if let Some(error) = introspection_refusal(&doc, role) {
return Err((request, vec![error]));
}
}
let mut errors = variable_errors(&doc, schema.registry(), &request.variables);
errors.truncate(1);
if !errors.is_empty() {
return Err((request, errors));
}
}
let _ = &mut request;
Ok(request)
}
fn introspection_refusal(
doc: &async_graphql::parser::types::ExecutableDocument,
role: &str,
) -> Option<ServerError> {
use async_graphql::parser::types::{OperationType, Selection};
let operations: Vec<_> = match &doc.operations {
async_graphql::parser::types::DocumentOperations::Single(one) => vec![one],
async_graphql::parser::types::DocumentOperations::Multiple(many) => many.values().collect(),
};
let asks = operations.iter().any(|operation| {
operation.node.ty == OperationType::Query
&& operation
.node
.selection_set
.node
.items
.iter()
.any(|item| match &item.node {
Selection::Field(field) => {
matches!(field.node.name.node.as_str(), "__schema" | "__type")
}
_ => false,
})
});
match asks {
false => None,
true => {
let mut error = ServerError::new(
format!("introspection is disabled for role: \"{}\"", role),
None,
);
let mut extensions = async_graphql::ErrorExtensionValues::default();
extensions.set("code", "not-supported");
error.extensions = Some(extensions);
Some(error)
}
}
}
fn rewrite_document(
schema: Option<&async_graphql::dynamic::Schema>,
mut request: async_graphql::Request,
) -> async_graphql::Request {
use async_graphql::parser::types::{
DocumentOperations, ExecutableDocument, Selection, SelectionSet,
};
use async_graphql::Name;
use async_graphql_value::Value;
let Ok(mut doc): Result<ExecutableDocument, _> =
async_graphql::parser::parse_query(&request.query)
else {
return request;
};
fn from_value(value: &Value, used: &mut std::collections::HashSet<Name>) {
match value {
Value::Variable(name) => {
used.insert(name.clone());
}
Value::List(items) => items.iter().for_each(|item| from_value(item, used)),
Value::Object(fields) => fields.values().for_each(|field| from_value(field, used)),
_ => {}
}
}
fn from_selection_set(set: &SelectionSet, used: &mut std::collections::HashSet<Name>) {
for selection in &set.items {
let (directives, arguments, nested) = match &selection.node {
Selection::Field(field) => (
&field.node.directives,
Some(&field.node.arguments),
Some(&field.node.selection_set),
),
Selection::FragmentSpread(spread) => (&spread.node.directives, None, None),
Selection::InlineFragment(fragment) => (
&fragment.node.directives,
None,
Some(&fragment.node.selection_set),
),
};
for directive in directives {
for (_, value) in &directive.node.arguments {
from_value(&value.node, used);
}
}
for (_, value) in arguments.into_iter().flatten() {
from_value(&value.node, used);
}
if let Some(nested) = nested {
from_selection_set(&nested.node, used);
}
}
}
let mut used = std::collections::HashSet::new();
for (_, operation) in doc.operations.iter() {
for directive in &operation.node.directives {
for (_, value) in &directive.node.arguments {
from_value(&value.node, &mut used);
}
}
from_selection_set(&operation.node.selection_set.node, &mut used);
}
for fragment in doc.fragments.values() {
from_selection_set(&fragment.node.selection_set.node, &mut used);
}
let mut edited = false;
{
let mut prune = |operation: &mut async_graphql::Positioned<
async_graphql::parser::types::OperationDefinition,
>| {
let before = operation.node.variable_definitions.len();
operation
.node
.variable_definitions
.retain(|definition| used.contains(&definition.node.name.node));
edited |= operation.node.variable_definitions.len() != before;
};
match &mut doc.operations {
DocumentOperations::Single(operation) => prune(operation),
DocumentOperations::Multiple(operations) => operations.values_mut().for_each(prune),
}
}
if let Some(schema) = schema {
let registry = schema.registry();
{
let mut coerce = |operation: &mut async_graphql::Positioned<
async_graphql::parser::types::OperationDefinition,
>| {
use async_graphql::parser::types::OperationType;
let root = match operation.node.ty {
OperationType::Query => Some(registry.query_type.as_str()),
OperationType::Mutation => registry.mutation_type.as_deref(),
OperationType::Subscription => registry.subscription_type.as_deref(),
};
if let Some(root) = root {
edited |= coerce_selection_set(
registry,
&mut operation.node.selection_set.node,
root,
);
}
};
match &mut doc.operations {
DocumentOperations::Single(operation) => coerce(operation),
DocumentOperations::Multiple(operations) => {
operations.values_mut().for_each(coerce)
}
}
}
for fragment in doc.fragments.values_mut() {
let on = fragment.node.type_condition.node.on.node.to_string();
edited |= coerce_selection_set(registry, &mut fragment.node.selection_set.node, &on);
}
}
if edited {
request.set_parsed_query(doc);
}
request
}
fn coerce_selection_set(
registry: &async_graphql::registry::Registry,
set: &mut async_graphql::parser::types::SelectionSet,
type_name: &str,
) -> bool {
use async_graphql::parser::types::Selection;
use async_graphql::registry::MetaTypeName;
let mut edited = false;
for selection in &mut set.items {
match &mut selection.node {
Selection::Field(field) => {
let meta = registry
.types
.get(type_name)
.and_then(|ty| ty.field_by_name(field.node.name.node.as_str()));
for (name, value) in &mut field.node.arguments {
if name.node.as_str() == "limit" {
continue;
}
let Some(argument) = meta.and_then(|meta| meta.args.get(name.node.as_str()))
else {
continue;
};
let ty = argument.ty.clone();
edited |= coerce_value(registry, &mut value.node, &ty);
}
if let Some(meta) = meta {
let inner = MetaTypeName::concrete_typename(&meta.ty).to_string();
edited |=
coerce_selection_set(registry, &mut field.node.selection_set.node, &inner);
}
}
Selection::InlineFragment(fragment) => {
let inner = fragment
.node
.type_condition
.as_ref()
.map(|condition| condition.node.on.node.to_string())
.unwrap_or_else(|| type_name.to_string());
edited |=
coerce_selection_set(registry, &mut fragment.node.selection_set.node, &inner);
}
Selection::FragmentSpread(_) => {}
}
}
edited
}
fn coerce_value(
registry: &async_graphql::registry::Registry,
value: &mut async_graphql_value::Value,
expected: &str,
) -> bool {
use async_graphql::registry::{MetaType, MetaTypeName};
use async_graphql_value::Value;
match value {
Value::List(items) => {
let inner = match MetaTypeName::create(expected).unwrap_non_null() {
MetaTypeName::List(inner) => inner.to_string(),
_ => expected.to_string(),
};
let mut edited = false;
for item in items {
edited |= coerce_value(registry, item, &inner);
}
edited
}
Value::Object(fields) => {
let name = MetaTypeName::concrete_typename(expected);
let Some(MetaType::InputObject { input_fields, .. }) = registry.types.get(name) else {
return false;
};
let types: Vec<(async_graphql::Name, String)> = fields
.keys()
.filter_map(|key| {
input_fields
.get(key.as_str())
.map(|field| (key.clone(), field.ty.clone()))
})
.collect();
let mut edited = false;
for (key, ty) in types {
if let Some(item) = fields.get_mut(&key) {
edited |= coerce_value(registry, item, &ty);
}
}
edited
}
Value::String(text) => {
let Some(coerced) = as_written(text, MetaTypeName::concrete_typename(expected)) else {
return false;
};
*value = coerced;
true
}
_ => false,
}
}
fn as_written(text: &str, expected: &str) -> Option<async_graphql_value::Value> {
use async_graphql_value::{Number, Value};
match expected {
"Int" => text.parse::<i64>().ok().map(|n| Value::Number(n.into())),
"Float" => text
.parse::<f64>()
.ok()
.and_then(Number::from_f64)
.map(Value::Number),
"Boolean" => match text {
"true" => Some(Value::Boolean(true)),
"false" => Some(Value::Boolean(false)),
_ => None,
},
_ => None,
}
}
#[cfg(test)]
mod unused_variable_tests {
use super::*;
fn declarations(query: &str) -> Vec<String> {
let request = allow_unused_variables(async_graphql::Request::new(query));
let mut request = request;
let doc = request.parsed_query().expect("the query parses");
doc.operations
.iter()
.flat_map(|(_, operation)| operation.node.variable_definitions.iter())
.map(|definition| definition.node.name.node.to_string())
.collect()
}
#[test]
fn a_variable_nothing_names_is_dropped() {
assert_eq!(
declarations("query ($tags: jsonb) { article(where: {id: {_eq: 1}}) { id } }"),
Vec::<String>::new()
);
}
#[test]
fn a_variable_an_argument_names_is_kept() {
assert_eq!(
declarations("query ($id: Int) { article(where: {id: {_eq: $id}}) { id } }"),
vec!["id".to_string()]
);
}
#[test]
fn a_variable_named_inside_a_list_is_kept() {
assert_eq!(
declarations("query ($id: Int) { article(where: {_or: [{id: {_eq: $id}}]}) { id } }"),
vec!["id".to_string()]
);
}
#[test]
fn a_variable_only_a_fragment_names_is_kept() {
assert_eq!(
declarations(
"query ($n: Int) { author { ...rows } } \
fragment rows on author { articles(limit: $n) { id } }"
),
vec!["n".to_string()]
);
}
#[test]
fn a_document_that_does_not_parse_is_left_alone() {
let request = allow_unused_variables(async_graphql::Request::new("query ("));
assert_eq!(request.query, "query (");
}
}
fn variable_errors(
doc: &async_graphql::parser::types::ExecutableDocument,
registry: &async_graphql::registry::Registry,
variables: &async_graphql::Variables,
) -> Vec<ServerError> {
use async_graphql::parser::types::{DocumentOperations, OperationType};
let mut errors: Vec<ServerError> = Vec::new();
let operations: Vec<
&async_graphql::Positioned<async_graphql::parser::types::OperationDefinition>,
> = match &doc.operations {
DocumentOperations::Single(operation) => vec![operation],
DocumentOperations::Multiple(operations) => operations.values().collect(),
};
for operation in operations {
let root = match operation.node.ty {
OperationType::Query => Some(registry.query_type.as_str()),
OperationType::Mutation => registry.mutation_type.as_deref(),
OperationType::Subscription => registry.subscription_type.as_deref(),
};
let Some(root) = root else {
if operation.node.ty == OperationType::Mutation {
errors.push(coded("no mutations exist".to_string(), operation.pos, "$"));
}
continue;
};
let mut declared: HashMap<&str, String> = HashMap::new();
for definition in &operation.node.variable_definitions {
let name = definition.node.name.node.as_str();
let written = definition.node.var_type.node.to_string();
let defaulted = definition
.node
.default_value
.as_ref()
.is_some_and(|value| !matches!(value.node, async_graphql::Value::Null));
let effective = match (definition.node.var_type.node.nullable, defaulted) {
(true, true) => format!("{}!", written),
_ => written.clone(),
};
declared.insert(name, effective);
let given = variables.get(&async_graphql::Name::new(name));
if !definition.node.var_type.node.nullable
&& matches!(given, Some(async_graphql::Value::Null))
{
errors.push(coded(
format!("null value found for non-nullable type: \"{}\"", written),
definition.pos,
"$",
));
}
}
let mut scope = Usage {
registry,
fragments: &doc.fragments,
declared: &declared,
variables,
errors: &mut errors,
spreading: Vec::new(),
expansions: 0,
};
scope.selection_set(&operation.node.selection_set.node, root, "$");
}
errors
}
struct Usage<'a> {
registry: &'a async_graphql::registry::Registry,
fragments: &'a std::collections::HashMap<
async_graphql::Name,
async_graphql::Positioned<async_graphql::parser::types::FragmentDefinition>,
>,
declared: &'a HashMap<&'a str, String>,
variables: &'a async_graphql::Variables,
errors: &'a mut Vec<ServerError>,
spreading: Vec<String>,
expansions: usize,
}
impl Usage<'_> {
fn selection_set(
&mut self,
set: &async_graphql::parser::types::SelectionSet,
type_name: &str,
path: &str,
) {
use async_graphql::parser::types::Selection;
use async_graphql::registry::MetaTypeName;
for selection in &set.items {
match &selection.node {
Selection::Field(field) => {
let named = field.node.name.node.as_str();
let parent = self.registry.types.get(type_name);
let meta = parent.and_then(|ty| ty.field_by_name(named));
let here = format!("{}.selectionSet.{}", path, field.node.name.node);
if meta.is_none() && parent.is_some() && !named.starts_with("__") {
self.errors.push(coded(
format!("field '{}' not found in type: '{}'", named, type_name),
field.pos,
&here,
));
continue;
}
for (name, value) in &field.node.arguments {
let Some(argument) =
meta.and_then(|meta| meta.args.get(name.node.as_str()))
else {
if meta.is_some() {
self.errors.push(coded(
format!("'{}' has no argument named '{}'", named, name.node),
name.pos,
&here,
));
}
continue;
};
let expected = relax(&argument.ty, argument.default_value.is_some());
let at = format!("{}.args.{}", here, name.node);
self.rows_asked_for(
name.node.as_str(),
&value.node,
&expected,
value.pos,
&at,
);
self.object_expected(&value.node, &expected, value.pos, &at);
self.value(&value.node, &expected, value.pos, &at);
}
self.one_operator_per_column(&field.node.arguments, &here);
self.directives(&field.node.directives, &here);
if let Some(meta) = meta {
let inner = MetaTypeName::concrete_typename(&meta.ty).to_string();
self.selection_set(&field.node.selection_set.node, &inner, &here);
}
}
Selection::InlineFragment(fragment) => {
let inner = fragment
.node
.type_condition
.as_ref()
.map(|condition| condition.node.on.node.to_string())
.unwrap_or_else(|| type_name.to_string());
self.directives(&fragment.node.directives, path);
self.selection_set(&fragment.node.selection_set.node, &inner, path);
}
Selection::FragmentSpread(spread) => {
let name = spread.node.fragment_name.node.to_string();
self.directives(&spread.node.directives, path);
if let Some(began) = self.spreading.iter().position(|held| held == &name) {
let cycle: Vec<&str> =
self.spreading[began..].iter().map(String::as_str).collect();
self.errors.push(coded(
format!("the fragment definition(s) {} form a cycle", listed(&cycle)),
spread.pos,
&format!("{}.selectionSet", path),
));
continue;
}
self.expansions += 1;
if self.expansions > SPREAD_BUDGET {
continue;
}
if let Some(fragment) = self.fragments.get(&async_graphql::Name::new(&name)) {
let on = fragment.node.type_condition.node.on.node.to_string();
let here = format!("{}.selectionSet.{}", path, name);
self.spreading.push(name);
self.selection_set(&fragment.node.selection_set.node, &on, &here);
self.spreading.pop();
}
}
}
}
}
fn directives(
&mut self,
directives: &[async_graphql::Positioned<async_graphql::parser::types::Directive>],
path: &str,
) {
for directive in directives {
let meta = self
.registry
.directives
.get(directive.node.name.node.as_str());
for (name, value) in &directive.node.arguments {
let Some(argument) = meta.and_then(|meta| meta.args.get(name.node.as_str())) else {
continue;
};
let expected = relax(&argument.ty, argument.default_value.is_some());
let at = format!("{}.args.{}", path, name.node);
self.value(&value.node, &expected, value.pos, &at);
}
}
}
fn stands_for_null(&self, value: &async_graphql_value::Value) -> bool {
use async_graphql_value::Value;
match value {
Value::Null => true,
Value::Variable(name) => matches!(
self.variables.get(&async_graphql::Name::new(name)),
Some(async_graphql::Value::Null)
),
_ => false,
}
}
fn value(
&mut self,
value: &async_graphql_value::Value,
expected: &str,
pos: async_graphql::Pos,
path: &str,
) {
use async_graphql::registry::{MetaType, MetaTypeName};
use async_graphql_value::Value;
match value {
Value::Variable(name) => {
let Some(declared) = self.declared.get(name.as_str()) else {
return;
};
if !MetaTypeName::create(expected).is_subtype(&MetaTypeName::create(declared)) {
self.errors.push(coded(
format!(
"variable '{}' is declared as '{}', but used where '{}' is expected",
name, declared, expected
),
pos,
path,
));
return;
}
if let Some(given) = self.variables.get(&async_graphql::Name::new(name)) {
let given: async_graphql_value::Value = given.clone().into();
self.enumerated(&given, expected, pos, path, true);
self.label_path(&given, expected, pos, path);
self.raster_hex(&given, expected, pos, path);
}
}
Value::Enum(_) | Value::String(_) => {
self.enumerated(value, expected, pos, path, false);
self.label_path(value, expected, pos, path);
self.raster_hex(value, expected, pos, path);
}
Value::Number(_) => self.integral(value, expected, pos, path),
Value::List(items) => {
let inner = match MetaTypeName::create(expected).unwrap_non_null() {
MetaTypeName::List(inner) => inner.to_string(),
_ => expected.to_string(),
};
for (index, item) in items.iter().enumerate() {
self.value(item, &inner, pos, &format!("{}[{}]", path, index));
}
}
Value::Object(fields) => {
let name = MetaTypeName::concrete_typename(expected);
let Some(MetaType::InputObject { input_fields, .. }) =
self.registry.types.get(name)
else {
return;
};
let comparison = name.ends_with("_comparison_exp");
let base = match MetaTypeName::create(expected).unwrap_non_null() {
MetaTypeName::List(_) => format!("{}[0]", path),
_ => path.to_string(),
};
for (key, item) in fields {
let at = format!("{}.{}", base, key.as_str());
let Some(field) = input_fields.get(key.as_str()) else {
self.errors.push(coded(
format!("field '{}' not found in type: '{}'", key.as_str(), name),
pos,
&at,
));
continue;
};
if comparison && self.stands_for_null(item) {
self.errors.push(coded(
format!(
"unexpected null value for type '{}'",
MetaTypeName::concrete_typename(&field.ty)
),
pos,
&at,
));
continue;
}
let ty = relax(&field.ty, field.default_value.is_some());
self.value(item, &ty, pos, &at);
}
}
_ => {}
}
}
}
impl Usage<'_> {
fn object_expected(
&mut self,
value: &async_graphql_value::Value,
expected: &str,
pos: async_graphql::Pos,
path: &str,
) {
use async_graphql::registry::{MetaType, MetaTypeName};
if !matches!(MetaTypeName::create(expected), MetaTypeName::NonNull(_)) {
return;
}
if !self.stands_for_null(value) {
return;
}
let name = MetaTypeName::concrete_typename(expected);
if !matches!(
self.registry.types.get(name),
Some(MetaType::InputObject { .. })
) {
return;
}
self.errors.push(coded(
format!("expected an object for type '{}', but found null", name),
pos,
path,
));
}
fn one_operator_per_column(
&mut self,
arguments: &[(
async_graphql::Positioned<async_graphql::Name>,
async_graphql::Positioned<async_graphql_value::Value>,
)],
path: &str,
) {
use async_graphql_value::Value;
const OPERATORS: [&str; 7] = [
"_set",
"_inc",
"_append",
"_prepend",
"_delete_key",
"_delete_elem",
"_delete_at_path",
];
let mut seen: Vec<&str> = Vec::new();
let mut twice: Vec<&str> = Vec::new();
for (name, value) in arguments {
if !OPERATORS.contains(&name.node.as_str()) {
continue;
}
let Value::Object(columns) = &value.node else {
continue;
};
for column in columns.keys() {
let column = column.as_str();
match seen.contains(&column) {
true if !twice.contains(&column) => twice.push(column),
true => {}
false => seen.push(column),
}
}
}
if twice.is_empty() {
return;
}
let listed = twice
.iter()
.map(|column| format!("'{}'", column))
.collect::<Vec<String>>()
.join(", ");
self.errors.push(coded(
format!("Column found in multiple operators: [{}].", listed),
arguments
.first()
.map(|(name, _)| name.pos)
.unwrap_or_default(),
&format!("{}.args", path),
));
}
fn integral(
&mut self,
value: &async_graphql_value::Value,
expected: &str,
pos: async_graphql::Pos,
path: &str,
) {
use async_graphql::registry::MetaTypeName;
use async_graphql_value::Value;
if MetaTypeName::concrete_typename(expected) != "Int" {
return;
}
let Value::Number(number) = value else {
return;
};
if number.as_i64().is_some_and(|n| i32::try_from(n).is_ok()) {
return;
}
let Some(shown) = number.as_f64().map(shown_as_haskell_would) else {
return;
};
self.errors.push(coded_as(
"parse-failed",
format!(
"The value {} lies outside the bounds or is not an integer. \
Maybe it is a float, or is there integer overflow?",
shown
),
pos,
path,
));
}
fn rows_asked_for(
&mut self,
argument: &str,
value: &async_graphql_value::Value,
expected: &str,
pos: async_graphql::Pos,
path: &str,
) {
use async_graphql::registry::MetaTypeName;
use async_graphql_value::Value;
if argument != "limit" || MetaTypeName::concrete_typename(expected) != "Int" {
return;
}
let value = match value {
Value::Variable(name) => {
match self.variables.get(&async_graphql::Name::new(name.as_str())) {
Some(given) => std::borrow::Cow::Owned(given.clone().into()),
None => return,
}
}
other => std::borrow::Cow::Borrowed(other),
};
let found = match value.as_ref() {
Value::String(_) => "a string",
Value::Number(number) => match number.as_i64() {
None => return,
Some(written) if i32::try_from(written).is_err() => return,
Some(written) if written >= 0 => return,
Some(_) => "an integer",
},
_ => return,
};
self.errors.push(coded(
format!(
"expected a non-negative 32-bit integer for type 'Int', but found {}",
found
),
pos,
path,
));
}
fn raster_hex(
&mut self,
value: &async_graphql_value::Value,
expected: &str,
pos: async_graphql::Pos,
path: &str,
) {
use async_graphql::registry::MetaTypeName;
use async_graphql_value::Value;
if MetaTypeName::concrete_typename(expected) != "raster" {
return;
}
let Value::String(written) = value else {
return;
};
if written.len() % 2 == 0 && written.chars().all(|c| c.is_ascii_hexdigit()) {
return;
}
self.errors.push(coded_as(
"parse-failed",
"invalid hexadecimal representation of raster well known binary format".to_string(),
pos,
path,
));
}
fn label_path(
&mut self,
value: &async_graphql_value::Value,
expected: &str,
pos: async_graphql::Pos,
path: &str,
) {
use async_graphql::registry::MetaTypeName;
use async_graphql_value::Value;
if MetaTypeName::concrete_typename(expected) != "ltree" {
return;
}
let Value::String(written) = value else {
return;
};
if written.is_empty() || written.split('.').all(|label| !label.is_empty()) {
return;
}
self.errors.push(coded_as(
"parse-failed",
"Expecting label path: a sequence of zero or more labels separated by \
dots, for example L1.L2.L3"
.to_string(),
pos,
path,
));
}
fn enumerated(
&mut self,
value: &async_graphql_value::Value,
expected: &str,
pos: async_graphql::Pos,
path: &str,
from_variable: bool,
) {
use async_graphql::registry::{MetaType, MetaTypeName};
use async_graphql_value::Value;
let name = MetaTypeName::concrete_typename(expected);
let Some(MetaType::Enum { enum_values, .. }) = self.registry.types.get(name) else {
return;
};
let written = match value {
Value::Enum(written) => written.as_str(),
Value::String(written) if from_variable => written.as_str(),
Value::String(_) => {
self.errors.push(coded(
format!(
"expected an enum value for type '{}', but found a string",
name
),
pos,
path,
));
return;
}
_ => return,
};
if enum_values.contains_key(written) {
return;
}
let mut allowed: Vec<&str> = enum_values.keys().map(String::as_str).collect();
allowed.sort_unstable();
let listed = allowed
.iter()
.map(|value| format!("'{}'", value))
.collect::<Vec<_>>()
.join(", ");
self.errors.push(coded(
format!(
"expected one of the values [{}] for type '{}', but found '{}'",
listed, name, written
),
pos,
path,
));
}
}
const SPREAD_BUDGET: usize = 1000;
fn listed(names: &[&str]) -> String {
match names {
[] => String::new(),
[one] => one.to_string(),
[rest @ .., last] => format!("{} and {}", rest.join(", "), last),
}
}
fn shown_as_haskell_would(value: f64) -> String {
if !value.is_finite() {
return value.to_string();
}
let magnitude = value.abs();
let with_a_point = |rendered: String| match rendered.contains('.') {
true => rendered,
false => format!("{}.0", rendered),
};
if magnitude != 0.0 && (magnitude < 0.1 || magnitude >= 1e7) {
let rendered = format!("{:e}", value);
let (mantissa, exponent) = match rendered.split_once('e') {
Some(halves) => halves,
None => return with_a_point(rendered),
};
return format!("{}e{}", with_a_point(mantissa.to_string()), exponent);
}
with_a_point(format!("{}", value))
}
fn relax(ty: &str, has_default: bool) -> String {
match has_default {
true => ty.strip_suffix('!').unwrap_or(ty).to_string(),
false => ty.to_string(),
}
}
fn coded(message: String, pos: async_graphql::Pos, path: &str) -> ServerError {
coded_as("validation-failed", message, pos, path)
}
fn coded_as(
code: &'static str,
message: String,
pos: async_graphql::Pos,
path: &str,
) -> ServerError {
let mut error = ServerError::new(message, Some(pos));
let mut extensions = async_graphql::ErrorExtensionValues::default();
extensions.set("code", code);
extensions.set("path", path);
error.extensions = Some(extensions);
error
}
#[cfg(test)]
mod variable_position_tests {
use super::*;
use async_graphql::dynamic::*;
fn schema() -> async_graphql::dynamic::Schema {
let filter = InputObject::new("author_bool_exp")
.field(InputValue::new("id", TypeRef::named("Int")))
.field(InputValue::new(
"name",
TypeRef::named("String_comparison_exp"),
));
let comparison = InputObject::new("String_comparison_exp")
.field(InputValue::new("_eq", TypeRef::named("String")))
.field(InputValue::new("_in", TypeRef::named_nn_list("String")));
let ltree = InputObject::new("ltree_comparison_exp")
.field(InputValue::new("_ancestor", TypeRef::named("ltree")))
.field(InputValue::new(
"_ancestor_any",
TypeRef::named_list("ltree"),
));
let kind = Enum::new("kind_enum").item("leaf").item("branch");
let kind_comparison = InputObject::new("kind_enum_comparison_exp")
.field(InputValue::new("_eq", TypeRef::named("kind_enum")));
let raster_comparison = InputObject::new("raster_comparison_exp").field(InputValue::new(
"_st_intersects_rast",
TypeRef::named("raster"),
));
let tree_filter = InputObject::new("tree_bool_exp")
.field(InputValue::new(
"rast",
TypeRef::named("raster_comparison_exp"),
))
.field(InputValue::new(
"path",
TypeRef::named("ltree_comparison_exp"),
))
.field(InputValue::new(
"kind",
TypeRef::named("kind_enum_comparison_exp"),
));
let insert = InputObject::new("author_insert_input")
.field(InputValue::new("name", TypeRef::named("String")))
.field(InputValue::new("count", TypeRef::named("Int")));
let query = Object::new("query_root")
.field(
Field::new("author", TypeRef::named_nn_list_nn("author"), |_| {
FieldFuture::new(async move { Ok(Some(async_graphql::Value::List(vec![]))) })
})
.argument(InputValue::new("limit", TypeRef::named("Int")))
.argument(InputValue::new("where", TypeRef::named("author_bool_exp"))),
)
.field(
Field::new("tree", TypeRef::named_nn_list_nn("tree"), |_| {
FieldFuture::new(async move { Ok(Some(async_graphql::Value::List(vec![]))) })
})
.argument(InputValue::new("where", TypeRef::named("tree_bool_exp"))),
);
let mutation = Object::new("mutation_root")
.field(
Field::new("insert_author_one", TypeRef::named("author"), |_| {
FieldFuture::new(async move { Ok(None::<async_graphql::Value>) })
})
.argument(InputValue::new(
"object",
TypeRef::named_nn("author_insert_input"),
)),
)
.field(
Field::new("insert_author", TypeRef::named("author"), |_| {
FieldFuture::new(async move { Ok(None::<async_graphql::Value>) })
})
.argument(InputValue::new(
"objects",
TypeRef::named_nn_list_nn("author_insert_input"),
))
.argument(
InputValue::new("update_columns", TypeRef::named_nn_list_nn("String"))
.default_value(async_graphql::Value::List(Vec::new())),
),
)
.field(
Field::new(
"update_author",
TypeRef::named("author_mutation_response"),
|_| FieldFuture::new(async move { Ok(None::<async_graphql::Value>) }),
)
.argument(InputValue::new(
"where",
TypeRef::named_nn("author_bool_exp"),
))
.argument(InputValue::new("_set", TypeRef::named("author_set_input")))
.argument(InputValue::new("_inc", TypeRef::named("author_inc_input"))),
);
let author = Object::new("author").field(Field::new("id", TypeRef::named("Int"), |_| {
FieldFuture::new(async move { Ok(None::<async_graphql::Value>) })
}));
let tree = Object::new("tree")
.field(Field::new("id", TypeRef::named("Int"), |_| {
FieldFuture::new(async move { Ok(None::<async_graphql::Value>) })
}))
.field(Field::new("path", TypeRef::named("ltree"), |_| {
FieldFuture::new(async move { Ok(None::<async_graphql::Value>) })
}));
let set = InputObject::new("author_set_input")
.field(InputValue::new("name", TypeRef::named("String")))
.field(InputValue::new("count", TypeRef::named("Int")));
let inc = InputObject::new("author_inc_input")
.field(InputValue::new("count", TypeRef::named("Int")));
let response = Object::new("author_mutation_response").field(Field::new(
"affected_rows",
TypeRef::named_nn("Int"),
|_| FieldFuture::new(async move { Ok(None::<async_graphql::Value>) }),
));
Schema::build("query_root", Some("mutation_root"), None)
.register(Scalar::new("ltree"))
.register(Scalar::new("raster"))
.register(raster_comparison)
.register(kind)
.register(kind_comparison)
.register(filter)
.register(comparison)
.register(ltree)
.register(tree_filter)
.register(insert)
.register(set)
.register(inc)
.register(response)
.register(author)
.register(tree)
.register(query)
.register(mutation)
.finish()
.expect("the test schema builds")
}
fn refusals(query: &str, variables: &str) -> Vec<String> {
let request =
async_graphql::Request::new(query).variables(async_graphql::Variables::from_json(
serde_json::from_str(variables).expect("the variables are JSON"),
));
match prepare(Some(&schema()), request, None) {
Ok(_) => Vec::new(),
Err((_, errors)) => errors.into_iter().map(|e| e.message).collect(),
}
}
fn refusals_with_paths(query: &str, variables: &str) -> Vec<String> {
let request =
async_graphql::Request::new(query).variables(async_graphql::Variables::from_json(
serde_json::from_str(variables).expect("the variables are JSON"),
));
match prepare(Some(&schema()), request, None) {
Ok(_) => Vec::new(),
Err((_, errors)) => errors.iter().map(path_for).collect(),
}
}
#[test]
fn a_null_written_into_a_comparison_is_refused() {
assert_eq!(
refusals("{ author(where: {name: {_eq: null}}) { id } }", "{}"),
vec!["unexpected null value for type 'String'"]
);
}
#[test]
fn a_variable_standing_for_a_null_is_refused_there_too() {
assert_eq!(
refusals(
"query ($n: String) { author(where: {name: {_eq: $n}}) { id } }",
r#"{"n": null}"#
),
vec!["unexpected null value for type 'String'"]
);
}
#[test]
fn a_refusal_names_the_argument_it_was_found_in() {
let paths =
|query: &str, variables: &str| -> Vec<String> { refusals_with_paths(query, variables) };
assert_eq!(
paths(
"mutation ($n: Int!) { insert_author_one(object: {name: $n}) { id } }",
r#"{"n": 1}"#
),
vec!["$.selectionSet.insert_author_one.args.object.name"]
);
assert_eq!(
paths("{ author(where: {name: {_eq: null}}) { id } }", "{}"),
vec!["$.selectionSet.author.args.where.name._eq"]
);
assert_eq!(
paths(
"mutation ($n: Int!) { insert_author(objects: {name: $n}) { id } }",
r#"{"n": 1}"#
),
vec!["$.selectionSet.insert_author.args.objects[0].name"]
);
assert_eq!(
paths(
"mutation ($n: Int!) { insert_author(objects: [{name: \"a\"}, \
{name: $n}]) { id } }",
r#"{"n": 1}"#
),
vec!["$.selectionSet.insert_author.args.objects[1].name"]
);
}
#[test]
fn a_null_written_where_an_object_is_required_is_refused_as_one() {
assert_eq!(
refusals(
"mutation { update_author(where: null, _set: {name: \"a\"}) \
{ affected_rows } }",
"{}"
),
vec!["expected an object for type 'author_bool_exp', but found null"]
);
assert_eq!(
refusals_with_paths(
"mutation { update_author(where: null, _set: {name: \"a\"}) \
{ affected_rows } }",
"{}"
),
vec!["$.selectionSet.update_author.args.where"]
);
assert_eq!(
refusals(
"mutation ($w: author_bool_exp!) { update_author(where: $w, \
_set: {name: \"a\"}) { affected_rows } }",
r#"{"w": null}"#
),
vec!["null value found for non-nullable type: \"author_bool_exp!\""]
);
}
#[test]
fn a_null_written_where_an_object_is_optional_is_left_alone() {
assert_eq!(
refusals("{ author(where: null) { id } }", "{}"),
Vec::<String>::new()
);
}
#[test]
fn a_column_written_by_two_operators_is_refused_before_the_statement() {
assert_eq!(
refusals(
"mutation { update_author(_set: {name: \"a\", count: 1}, \
_inc: {count: 2}, where: {}) { affected_rows } }",
"{}"
),
vec!["Column found in multiple operators: ['count']."]
);
assert_eq!(
refusals_with_paths(
"mutation { update_author(_set: {count: 1}, _inc: {count: 2}, \
where: {}) { affected_rows } }",
"{}"
),
vec!["$.selectionSet.update_author.args"]
);
assert_eq!(
refusals(
"mutation { update_author(_set: {name: \"a\"}, _inc: {count: 2}, \
where: {}) { affected_rows } }",
"{}"
),
Vec::<String>::new()
);
}
#[test]
fn a_request_is_refused_once_however_many_faults_it_has() {
assert_eq!(
refusals("{ author { nope } tree { alsoNope } }", "{}"),
vec!["field 'nope' not found in type: 'author'"]
);
assert_eq!(
refusals(
"mutation { insert_author(objects: [{nope: 1}], unknownArg: 2) { id } }",
"{}"
),
vec!["field 'nope' not found in type: 'author_insert_input'"]
);
}
#[test]
fn a_raster_that_is_not_hexadecimal_is_refused_as_that() {
let invalid = "invalid hexadecimal representation of raster well known binary format";
assert_eq!(
refusals(
"query ($r: raster) { tree(where: {rast: {_st_intersects_rast: $r}}) { path } }",
r#"{"r": "this is invalid raster value"}"#
),
vec![invalid]
);
assert_eq!(
refusals_with_paths(
"{ tree(where: {rast: {_st_intersects_rast: \"zz\"}}) { path } }",
"{}"
),
vec!["$.selectionSet.tree.args.where.rast._st_intersects_rast"]
);
assert_eq!(
refusals(
"{ tree(where: {rast: {_st_intersects_rast: \"abc\"}}) { path } }",
"{}"
),
vec![invalid]
);
assert!(refusals(
"{ tree(where: {rast: {_st_intersects_rast: \"0100\"}}) { path } }",
"{}"
)
.is_empty());
}
#[test]
fn fragments_that_spread_each_other_are_named_as_a_cycle() {
let cycle = "query { author { ...a } } \
fragment a on author { id ...b } \
fragment b on author { id ...a }";
assert_eq!(
refusals(cycle, "{}"),
vec!["the fragment definition(s) a and b form a cycle"]
);
assert_eq!(
refusals_with_paths(cycle, "{}"),
vec!["$.selectionSet.author.selectionSet.a.selectionSet.b.selectionSet"]
);
assert_eq!(
refusals(
"query { author { ...a } } fragment a on author { id ...a }",
"{}"
),
vec!["the fragment definition(s) a form a cycle"]
);
assert!(refusals(
"query { author { ...a } articles: author { ...a } } \
fragment a on author { id }",
"{}"
)
.is_empty());
}
#[test]
fn a_number_that_is_not_an_int_is_refused_as_one() {
let bounds = |value: &str| {
format!(
"The value {} lies outside the bounds or is not an integer. \
Maybe it is a float, or is there integer overflow?",
value
)
};
assert_eq!(
refusals(
"mutation { insert_author_one(object: {count: 2147483648}) { id } }",
"{}"
),
vec![bounds("2.147483648e9")]
);
assert_eq!(
refusals_with_paths(
"mutation { insert_author(objects: [{count: 2147483648}]) { id } }",
"{}"
),
vec!["$.selectionSet.insert_author.args.objects[0].count"]
);
for good in ["2147483647", "-2147483648", "0"] {
assert!(
refusals(
&format!(
"mutation {{ insert_author_one(object: {{count: {}}}) {{ id }} }}",
good
),
"{}"
)
.is_empty(),
"{} was refused",
good
);
}
}
#[test]
fn a_number_is_shown_the_way_the_message_spells_it() {
assert_eq!(shown_as_haskell_would(2147483648.0), "2.147483648e9");
assert_eq!(shown_as_haskell_would(1.5), "1.5");
assert_eq!(shown_as_haskell_would(1.0), "1.0");
assert_eq!(shown_as_haskell_would(0.0), "0.0");
assert_eq!(shown_as_haskell_would(-2147483649.0), "-2.147483649e9");
assert_eq!(shown_as_haskell_would(1e-3), "1.0e-3");
assert_eq!(shown_as_haskell_would(9999999.0), "9999999.0");
assert_eq!(shown_as_haskell_would(10000000.0), "1.0e7");
assert_eq!(shown_as_haskell_would(0.1), "0.1");
}
#[test]
fn a_limit_is_a_number_of_rows_and_not_any_integer() {
let refused = |q: &str| refusals(q, "{}");
assert_eq!(
refused("{ author(limit: -1) { id } }"),
vec!["expected a non-negative 32-bit integer for type 'Int', but found an integer"]
);
assert_eq!(
refused("{ author(limit: \"3\") { id } }"),
vec!["expected a non-negative 32-bit integer for type 'Int', but found a string"]
);
assert_eq!(
refusals_with_paths("{ author(limit: -1) { id } }", "{}"),
vec!["$.selectionSet.author.args.limit"]
);
assert_eq!(
refusals(
"query ($n: Int) { author(limit: $n) { id } }",
r#"{"n": -1}"#
),
vec!["expected a non-negative 32-bit integer for type 'Int', but found an integer"]
);
assert!(refused("{ author(limit: 0) { id } }").is_empty());
assert!(refused("{ author(limit: 2147483647) { id } }").is_empty());
assert!(refused("{ author(limit: null) { id } }").is_empty());
assert_eq!(
refused("{ author(limit: 4294967295) { id } }"),
vec![
"The value 4.294967295e9 lies outside the bounds or is not an \
integer. Maybe it is a float, or is there integer overflow?"
]
);
}
#[test]
fn a_quoted_enum_value_is_not_a_member_that_is_missing() {
assert_eq!(
refusals("{ tree(where: {kind: {_eq: \"leaf\"}}) { path } }", "{}"),
vec!["expected an enum value for type 'kind_enum', but found a string"]
);
assert_eq!(
refusals("{ tree(where: {kind: {_eq: twig}}) { path } }", "{}"),
vec!["expected one of the values ['branch', 'leaf'] for type 'kind_enum', but found 'twig'"]
);
assert_eq!(
refusals(
"query ($k: kind_enum) { tree(where: {kind: {_eq: $k}}) { path } }",
r#"{"k": "twig"}"#
),
vec!["expected one of the values ['branch', 'leaf'] for type 'kind_enum', but found 'twig'"]
);
assert!(refusals(
"query ($k: kind_enum) { tree(where: {kind: {_eq: $k}}) { path } }",
r#"{"k": "leaf"}"#
)
.is_empty());
}
#[test]
fn a_document_that_does_not_parse_is_not_a_query() {
assert_eq!(
refusals("{ author(where: {name: {_eq: \"unclosed}}) { id } }", "{}"),
vec!["not a valid graphql query"]
);
assert_eq!(
refusals_with_paths("query { author(", "{}"),
vec!["$.query"]
);
assert_eq!(
refusals("{ nothing_here { id } }", "{}"),
vec!["field 'nothing_here' not found in type: 'query_root'"]
);
}
#[test]
fn a_label_path_with_an_empty_label_is_refused() {
let empty = "Expecting label path: a sequence of zero or more labels \
separated by dots, for example L1.L2.L3";
assert_eq!(
refusals(
"{ tree(where: {path: {_ancestor: \"a.b.\"}}) { path } }",
"{}"
),
vec![empty]
);
assert_eq!(
refusals(
"{ tree(where: {path: {_ancestor: \".a\"}}) { path } }",
"{}"
),
vec![empty]
);
assert_eq!(
refusals(
"{ tree(where: {path: {_ancestor: \"a..b\"}}) { path } }",
"{}"
),
vec![empty]
);
assert_eq!(
refusals_with_paths(
"{ tree(where: {path: {_ancestor_any: [\"a\", \"b.\"]}}) { path } }",
"{}"
),
vec!["$.selectionSet.tree.args.where.path._ancestor_any[1]"]
);
for good in ["a", "a.b", "", "a-b", "a_b", "1.2", "Ünï", "a@b"] {
assert!(
refusals(
&format!(
"{{ tree(where: {{path: {{_ancestor: \"{}\"}}}}) {{ path }} }}",
good
),
"{}"
)
.is_empty(),
"{} was refused",
good
);
}
}
#[test]
fn a_variable_that_was_not_given_is_not_a_null() {
assert!(refusals(
"query ($n: String) { author(where: {name: {_eq: $n}}) { id } }",
"{}"
)
.is_empty());
}
#[test]
fn a_null_written_anywhere_else_is_left_alone() {
assert!(refusals(
"mutation { insert_author_one(object: {name: null}) { id } }",
"{}"
)
.is_empty());
}
#[test]
fn a_variable_of_the_wrong_type_is_refused() {
assert_eq!(
refusals("query ($s: String) { author(limit: $s) { id } }", "{}"),
vec!["variable 's' is declared as 'String', but used where 'Int' is expected"]
);
}
#[test]
fn the_check_reaches_inside_an_input_object() {
assert_eq!(
refusals(
"query ($s: String) { author(where: {id: $s}) { id } }",
"{}"
),
vec!["variable 's' is declared as 'String', but used where 'Int' is expected"]
);
}
#[test]
fn the_check_reaches_inside_a_list() {
assert_eq!(
refusals(
"mutation ($n: Int) { insert_author(objects: [{name: $n}]) { id } }",
"{}"
),
vec!["variable 'n' is declared as 'Int', but used where 'String' is expected"]
);
}
#[test]
fn a_nullable_variable_cannot_fill_a_non_null_place() {
assert_eq!(
refusals(
"mutation ($a: author_insert_input) { insert_author_one(object: $a) { id } }",
"{}"
),
vec![
"variable 'a' is declared as 'author_insert_input', but used where \
'author_insert_input!' is expected"
]
);
}
#[test]
fn a_default_lets_it() {
assert!(refusals(
"mutation ($a: author_insert_input = {name: \"x\"}) \
{ insert_author_one(object: $a) { id } }",
"{}"
)
.is_empty());
}
#[test]
fn a_default_of_null_does_not() {
assert_eq!(
refusals(
"mutation ($a: author_insert_input = null) \
{ insert_author_one(object: $a) { id } }",
"{}"
),
vec![
"variable 'a' is declared as 'author_insert_input', but used where \
'author_insert_input!' is expected"
]
);
}
#[test]
fn a_default_on_the_place_lets_it_too() {
assert!(refusals(
"mutation ($c: [String!]) { insert_author(objects: [], update_columns: $c) { id } }",
"{}"
)
.is_empty());
}
#[test]
fn a_non_null_variable_fits_a_nullable_place() {
assert!(refusals(
"query ($n: Int!) { author(limit: $n) { id } }",
"{\"n\": 1}"
)
.is_empty());
}
#[test]
fn a_non_null_variable_given_null_is_refused() {
assert_eq!(
refusals(
"query ($n: Int! = 1) { author(limit: $n) { id } }",
"{\"n\": null}"
),
vec!["null value found for non-nullable type: \"Int!\""]
);
}
#[test]
fn a_nullable_variable_given_null_is_not() {
assert!(refusals(
"query ($n: Int) { author(limit: $n) { id } }",
"{\"n\": null}"
)
.is_empty());
}
#[test]
fn a_variable_used_through_a_fragment_is_checked() {
assert_eq!(
refusals(
"query ($s: String) { author { ...rows } } \
fragment rows on author { id }",
"{}"
),
Vec::<String>::new()
);
assert_eq!(
refusals(
"query ($s: String) { ...roots } \
fragment roots on query_root { author(limit: $s) { id } }",
"{}"
),
vec!["variable 's' is declared as 'String', but used where 'Int' is expected"]
);
}
}
#[cfg(test)]
mod coercion_tests {
use super::*;
use async_graphql::dynamic::*;
fn schema() -> async_graphql::dynamic::Schema {
let insert = InputObject::new("test_types_insert_input")
.field(InputValue::new("c1_smallint", TypeRef::named("Int")))
.field(InputValue::new("c6_real", TypeRef::named("Float")))
.field(InputValue::new("c20_boolean", TypeRef::named("Boolean")))
.field(InputValue::new("c13_text", TypeRef::named("String")));
let row = Object::new("test_types").field(Field::new(
"c1_smallint",
TypeRef::named("Int"),
|_| FieldFuture::new(async move { Ok(None::<async_graphql::Value>) }),
));
let query = Object::new("query_root").field(
Field::new(
"test_types",
TypeRef::named_nn_list_nn("test_types"),
|_| FieldFuture::new(async move { Ok(Some(async_graphql::Value::List(vec![]))) }),
)
.argument(InputValue::new("limit", TypeRef::named("Int")))
.argument(InputValue::new("offset", TypeRef::named("Int"))),
);
let mutation = Object::new("mutation_root").field(
Field::new("insert_test_types", TypeRef::named("test_types"), |_| {
FieldFuture::new(async move { Ok(None::<async_graphql::Value>) })
})
.argument(InputValue::new(
"objects",
TypeRef::named_nn_list_nn("test_types_insert_input"),
)),
);
Schema::build("query_root", Some("mutation_root"), None)
.register(insert)
.register(row)
.register(query)
.register(mutation)
.finish()
.expect("the schema builds")
}
fn rewritten(query: &str) -> String {
let schema = schema();
let request = prepare(Some(&schema), async_graphql::Request::new(query), None)
.unwrap_or_else(|(request, _)| request);
let mut request = request;
format!("{:?}", request.parsed_query().expect("the query parses"))
}
#[test]
fn an_offset_written_as_a_string_becomes_a_number() {
let printed = rewritten("{ test_types(offset: \"1\") { c1_smallint } }");
assert!(printed.contains("Number(1)"), "{}", printed);
}
#[test]
fn a_limit_written_as_a_string_is_left_alone() {
let printed = rewritten("{ test_types(limit: \"3\") { c1_smallint } }");
assert!(printed.contains("String(\"3\")"), "{}", printed);
}
#[test]
fn a_column_written_as_a_string_becomes_what_the_column_is() {
let printed = rewritten(
"mutation { insert_test_types(objects: [{ \
c1_smallint: \"32767\", c6_real: \"0.5\", c20_boolean: \"true\" }]) \
{ c1_smallint } }",
);
assert!(printed.contains("Number(32767)"), "{}", printed);
assert!(printed.contains("Number(0.5)"), "{}", printed);
assert!(printed.contains("Boolean(true)"), "{}", printed);
}
#[test]
fn a_string_written_where_a_string_belongs_stays_one() {
let printed = rewritten(
"mutation { insert_test_types(objects: [{ c13_text: \"32767\" }]) { c1_smallint } }",
);
assert!(printed.contains("String(\"32767\")"), "{}", printed);
}
}