1use async_graphql::{Response, ServerError};
38use serde_json::{json, Map, Value};
39use std::collections::HashMap;
40
41fn code_for(error: &ServerError) -> &'static str {
49 if let Some(extensions) = &error.extensions {
50 if let Some(Value::String(code)) = extensions.get("code").map(value_of) {
51 return match code.as_str() {
52 "validation-failed" => "validation-failed",
53 "bad-request" => "bad-request",
54 "access-denied" => "access-denied",
58 "not-found" => "not-found",
62 "permission-error" => "permission-error",
63 "constraint-violation" => "constraint-violation",
64 "data-exception" => "data-exception",
65 "not-supported" => "not-supported",
66 "parse-failed" => "parse-failed",
67 _ => "unexpected",
68 };
69 }
70 }
71
72 let message = error.message.to_ascii_lowercase();
73 if message.contains("invalid value for argument")
77 || message.contains("does not contain the value")
78 || message.contains("is not defined by operation")
79 || message.contains("not configured for mutations")
84 || message.contains("not configured for subscriptions")
85 {
86 "validation-failed"
87 } else if message.contains("permission") || message.contains("not allowed") {
88 "permission-error"
89 } else if message.contains("violates") || message.contains("constraint") {
90 "constraint-violation"
91 } else if message.contains("unknown argument")
92 || message.contains("cannot query field")
93 || message.contains("expected")
94 || message.contains("unknown field")
95 || message.starts_with("variable \"")
99 {
100 "validation-failed"
101 } else {
102 "postgres-error"
103 }
104}
105
106fn value_of(value: &async_graphql::Value) -> Value {
107 serde_json::to_value(value).unwrap_or(Value::Null)
108}
109
110fn path_for(error: &ServerError) -> String {
116 use async_graphql::PathSegment;
117
118 if let Some(Value::String(path)) = error
123 .extensions
124 .as_ref()
125 .and_then(|extensions| extensions.get("path"))
126 .map(value_of)
127 {
128 return path;
129 }
130
131 if error.path.is_empty() {
132 return "$".to_string();
133 }
134 let mut rendered = String::from("$");
144 for segment in &error.path {
145 if let PathSegment::Field(name) = segment {
146 rendered.push_str(".selectionSet.");
147 rendered.push_str(name);
148 }
149 }
150 rendered
151}
152
153pub fn envelope(response: Response) -> Value {
155 if !response.errors.is_empty() {
156 let errors: Vec<Value> = response
157 .errors
158 .iter()
159 .map(|error| {
160 json!({
161 "message": error.message,
162 "extensions": {
163 "path": path_for(error),
164 "code": code_for(error),
165 }
166 })
167 })
168 .collect();
169 return json!({ "errors": errors });
171 }
172
173 let mut body = Map::new();
174 body.insert("data".to_string(), value_of(&response.data));
175 Value::Object(body)
176}
177
178#[derive(Debug, Clone, Copy, PartialEq, Eq)]
184pub enum Endpoint {
185 Current,
187 Legacy,
189}
190
191pub fn status_for(body: &Value, endpoint: Endpoint) -> u16 {
197 if endpoint == Endpoint::Current {
198 return 200;
199 }
200 let refused = body
201 .get("errors")
202 .and_then(Value::as_array)
203 .is_some_and(|errors| {
204 errors.iter().any(|error| {
205 error.get("message").and_then(Value::as_str) == Some(crate::role::CHECK_FAILED)
206 })
207 });
208 match refused {
209 true => 400,
210 false => 200,
211 }
212}
213
214pub fn denied(message: &str) -> Value {
221 let mut error = ServerError::new(message, None);
222 let mut extensions = async_graphql::ErrorExtensionValues::default();
223 extensions.set("code", "access-denied");
224 error.extensions = Some(extensions);
225
226 let mut response = Response::new(async_graphql::Value::Null);
227 response.errors = vec![error];
228 envelope(response)
229}
230
231pub fn persisted_query(request: &async_graphql::Request) -> bool {
240 request.extensions.contains_key("persistedQuery")
241}
242
243pub fn not_supported(message: &str) -> Value {
245 let mut error = ServerError::new(message, None);
246 let mut extensions = async_graphql::ErrorExtensionValues::default();
247 extensions.set("code", "not-supported");
248 extensions.set("path", "$");
249 error.extensions = Some(extensions);
250
251 let mut response = Response::new(async_graphql::Value::Null);
252 response.errors = vec![error];
253 envelope(response)
254}
255
256pub fn malformed(message: &str) -> Value {
262 let mut error = ServerError::new(message, None);
263 let mut extensions = async_graphql::ErrorExtensionValues::default();
264 extensions.set("code", "bad-request");
265 extensions.set("path", "$");
266 error.extensions = Some(extensions);
267
268 let mut response = Response::new(async_graphql::Value::Null);
269 response.errors = vec![error];
270 envelope(response)
271}
272
273#[cfg(test)]
274mod tests {
275 use super::*;
276 use async_graphql::{PathSegment, ServerError};
277 use pretty_assertions::assert_eq;
278
279 fn error_at(message: &str, path: Vec<PathSegment>) -> ServerError {
280 let mut error = ServerError::new(message, None);
281 error.path = path;
282 error
283 }
284
285 #[test]
286 fn only_a_refused_write_is_answered_with_a_failure_status() {
287 let refused = json!({"errors": [{"message": crate::role::CHECK_FAILED,
290 "extensions": {"code": "permission-error"}}]});
291 assert_eq!(status_for(&refused, Endpoint::Legacy), 400);
292 assert_eq!(status_for(&refused, Endpoint::Current), 200);
294
295 let denied = json!({"errors": [{"message": "not allowed",
297 "extensions": {"code": "permission-error"}}]});
298 assert_eq!(status_for(&denied, Endpoint::Legacy), 200);
299 assert_eq!(
300 status_for(&json!({"data": {"article": []}}), Endpoint::Legacy),
301 200
302 );
303 }
304
305 #[test]
306 fn a_role_may_be_refused_the_schema_without_being_refused_its_rows() {
307 let asked = |query: &str| {
308 let doc = async_graphql::parser::parse_query(query).unwrap();
309 introspection_refusal(&doc, "reader").map(|e| e.message)
310 };
311 let refusal = Some("introspection is disabled for role: \"reader\"".to_string());
312 assert_eq!(asked("{ __schema { types { name } } }"), refusal);
313 assert_eq!(asked("{ __type(name: \"article\") { name } }"), refusal);
314 assert_eq!(asked("{ article { __typename id } }"), None);
317 assert_eq!(asked("{ article { id } }"), None);
318 }
319
320 #[test]
321 fn a_failed_response_has_no_data_key() {
322 let mut response = Response::new(async_graphql::Value::Null);
323 response.errors = vec![error_at("no", vec![])];
324 let body = envelope(response);
325 assert!(body.get("data").is_none());
326 assert!(body.get("errors").is_some());
327 }
328
329 #[test]
330 fn a_successful_response_has_no_errors_key() {
331 let body = envelope(Response::new(async_graphql::Value::Null));
332 assert!(body.get("errors").is_none());
333 assert!(body.get("data").is_some());
334 }
335
336 #[test]
337 fn path_is_rooted_at_the_selection_set() {
338 let error = error_at(
339 "boom",
340 vec![
341 PathSegment::Field("insert_author".to_string()),
342 PathSegment::Index(0),
343 PathSegment::Field("name".to_string()),
344 ],
345 );
346 assert_eq!(
347 path_for(&error),
348 "$.selectionSet.insert_author.selectionSet.name"
349 );
350 }
351
352 #[test]
359 fn a_persisted_query_is_refused_as_unsupported() {
360 let plain = async_graphql::Request::new("{ author { id } }");
361 assert!(!persisted_query(&plain));
362
363 let mut carried = async_graphql::Request::new("{ author { id } }");
364 carried.extensions.0.insert(
365 "persistedQuery".to_string(),
366 async_graphql::Value::Object(Default::default()),
367 );
368 assert!(persisted_query(&carried));
369
370 assert_eq!(
371 not_supported("PersistedQueryNotSupported"),
372 json!({"errors": [{
373 "message": "PersistedQueryNotSupported",
374 "extensions": {"path": "$", "code": "not-supported"}
375 }]})
376 );
377 }
378
379 #[test]
380 fn a_path_given_with_the_error_is_the_one_answered() {
381 let mut error = error_at(
382 "boom",
383 vec![PathSegment::Field("insert_author".to_string())],
384 );
385 let mut extensions = async_graphql::ErrorExtensionValues::default();
386 extensions.set("code", "permission-error");
387 extensions.set("path", "$.selectionSet.insert_author.args.objects");
388 error.extensions = Some(extensions);
389 assert_eq!(
390 path_for(&error),
391 "$.selectionSet.insert_author.args.objects"
392 );
393 }
394
395 #[test]
396 fn an_error_with_no_path_is_rooted_at_the_document() {
397 assert_eq!(path_for(&error_at("boom", vec![])), "$");
398 }
399
400 #[test]
401 fn a_validation_message_is_coded_as_one() {
402 let error = ServerError::new("Unknown argument \"filter\" on field \"author\"", None);
403 assert_eq!(code_for(&error), "validation-failed");
404 }
405
406 #[test]
407 fn a_constraint_message_is_coded_as_one() {
408 let error = ServerError::new("duplicate key value violates unique constraint", None);
409 assert_eq!(code_for(&error), "constraint-violation");
410 }
411}
412
413pub fn allow_unused_variables(request: async_graphql::Request) -> async_graphql::Request {
429 prepare(None, request, None).unwrap_or_else(|(request, _)| request)
430}
431
432#[allow(clippy::result_large_err)]
451pub fn prepare(
452 schema: Option<&async_graphql::dynamic::Schema>,
453 request: async_graphql::Request,
454 introspection_disabled_for: Option<&str>,
459) -> Result<async_graphql::Request, (async_graphql::Request, Vec<ServerError>)> {
460 let mut request = rewrite_document(schema, request);
461 let doc = match async_graphql::parser::parse_query(&request.query) {
472 Ok(doc) => doc,
473 Err(_) => {
474 return Err((
475 request,
476 vec![coded(
477 "not a valid graphql query".to_string(),
478 async_graphql::Pos::default(),
479 "$.query",
480 )],
481 ))
482 }
483 };
484 if let Some(schema) = schema {
485 if let Some(role) = introspection_disabled_for {
486 if let Some(error) = introspection_refusal(&doc, role) {
487 return Err((request, vec![error]));
488 }
489 }
490 let mut errors = variable_errors(&doc, schema.registry(), &request.variables);
491 errors.truncate(1);
504 if !errors.is_empty() {
505 return Err((request, errors));
506 }
507 }
508 let _ = &mut request;
509 Ok(request)
510}
511
512fn introspection_refusal(
521 doc: &async_graphql::parser::types::ExecutableDocument,
522 role: &str,
523) -> Option<ServerError> {
524 use async_graphql::parser::types::{OperationType, Selection};
525
526 let operations: Vec<_> = match &doc.operations {
527 async_graphql::parser::types::DocumentOperations::Single(one) => vec![one],
528 async_graphql::parser::types::DocumentOperations::Multiple(many) => many.values().collect(),
529 };
530
531 let asks = operations.iter().any(|operation| {
532 operation.node.ty == OperationType::Query
533 && operation
534 .node
535 .selection_set
536 .node
537 .items
538 .iter()
539 .any(|item| match &item.node {
540 Selection::Field(field) => {
541 matches!(field.node.name.node.as_str(), "__schema" | "__type")
542 }
543 _ => false,
544 })
545 });
546
547 match asks {
548 false => None,
549 true => {
550 let mut error = ServerError::new(
551 format!("introspection is disabled for role: \"{}\"", role),
552 None,
553 );
554 let mut extensions = async_graphql::ErrorExtensionValues::default();
555 extensions.set("code", "not-supported");
556 error.extensions = Some(extensions);
557 Some(error)
558 }
559 }
560}
561
562fn rewrite_document(
571 schema: Option<&async_graphql::dynamic::Schema>,
572 mut request: async_graphql::Request,
573) -> async_graphql::Request {
574 use async_graphql::parser::types::{
575 DocumentOperations, ExecutableDocument, Selection, SelectionSet,
576 };
577 use async_graphql::Name;
578 use async_graphql_value::Value;
581
582 let Ok(mut doc): Result<ExecutableDocument, _> =
583 async_graphql::parser::parse_query(&request.query)
584 else {
585 return request;
586 };
587
588 fn from_value(value: &Value, used: &mut std::collections::HashSet<Name>) {
589 match value {
590 Value::Variable(name) => {
591 used.insert(name.clone());
592 }
593 Value::List(items) => items.iter().for_each(|item| from_value(item, used)),
594 Value::Object(fields) => fields.values().for_each(|field| from_value(field, used)),
595 _ => {}
596 }
597 }
598
599 fn from_selection_set(set: &SelectionSet, used: &mut std::collections::HashSet<Name>) {
600 for selection in &set.items {
601 let (directives, arguments, nested) = match &selection.node {
602 Selection::Field(field) => (
603 &field.node.directives,
604 Some(&field.node.arguments),
605 Some(&field.node.selection_set),
606 ),
607 Selection::FragmentSpread(spread) => (&spread.node.directives, None, None),
608 Selection::InlineFragment(fragment) => (
609 &fragment.node.directives,
610 None,
611 Some(&fragment.node.selection_set),
612 ),
613 };
614 for directive in directives {
615 for (_, value) in &directive.node.arguments {
616 from_value(&value.node, used);
617 }
618 }
619 for (_, value) in arguments.into_iter().flatten() {
620 from_value(&value.node, used);
621 }
622 if let Some(nested) = nested {
623 from_selection_set(&nested.node, used);
624 }
625 }
626 }
627
628 let mut used = std::collections::HashSet::new();
633 for (_, operation) in doc.operations.iter() {
634 for directive in &operation.node.directives {
635 for (_, value) in &directive.node.arguments {
636 from_value(&value.node, &mut used);
637 }
638 }
639 from_selection_set(&operation.node.selection_set.node, &mut used);
640 }
641 for fragment in doc.fragments.values() {
642 from_selection_set(&fragment.node.selection_set.node, &mut used);
643 }
644
645 let mut edited = false;
646 {
647 let mut prune = |operation: &mut async_graphql::Positioned<
648 async_graphql::parser::types::OperationDefinition,
649 >| {
650 let before = operation.node.variable_definitions.len();
651 operation
652 .node
653 .variable_definitions
654 .retain(|definition| used.contains(&definition.node.name.node));
655 edited |= operation.node.variable_definitions.len() != before;
656 };
657 match &mut doc.operations {
658 DocumentOperations::Single(operation) => prune(operation),
659 DocumentOperations::Multiple(operations) => operations.values_mut().for_each(prune),
660 }
661 }
662
663 if let Some(schema) = schema {
674 let registry = schema.registry();
675 {
676 let mut coerce = |operation: &mut async_graphql::Positioned<
677 async_graphql::parser::types::OperationDefinition,
678 >| {
679 use async_graphql::parser::types::OperationType;
680 let root = match operation.node.ty {
681 OperationType::Query => Some(registry.query_type.as_str()),
682 OperationType::Mutation => registry.mutation_type.as_deref(),
683 OperationType::Subscription => registry.subscription_type.as_deref(),
684 };
685 if let Some(root) = root {
686 edited |= coerce_selection_set(
687 registry,
688 &mut operation.node.selection_set.node,
689 root,
690 );
691 }
692 };
693 match &mut doc.operations {
694 DocumentOperations::Single(operation) => coerce(operation),
695 DocumentOperations::Multiple(operations) => {
696 operations.values_mut().for_each(coerce)
697 }
698 }
699 }
700 for fragment in doc.fragments.values_mut() {
704 let on = fragment.node.type_condition.node.on.node.to_string();
705 edited |= coerce_selection_set(registry, &mut fragment.node.selection_set.node, &on);
706 }
707 }
708
709 if edited {
710 request.set_parsed_query(doc);
711 }
712 request
713}
714
715fn coerce_selection_set(
722 registry: &async_graphql::registry::Registry,
723 set: &mut async_graphql::parser::types::SelectionSet,
724 type_name: &str,
725) -> bool {
726 use async_graphql::parser::types::Selection;
727 use async_graphql::registry::MetaTypeName;
728
729 let mut edited = false;
730 for selection in &mut set.items {
731 match &mut selection.node {
732 Selection::Field(field) => {
733 let meta = registry
734 .types
735 .get(type_name)
736 .and_then(|ty| ty.field_by_name(field.node.name.node.as_str()));
737 for (name, value) in &mut field.node.arguments {
738 if name.node.as_str() == "limit" {
741 continue;
742 }
743 let Some(argument) = meta.and_then(|meta| meta.args.get(name.node.as_str()))
744 else {
745 continue;
746 };
747 let ty = argument.ty.clone();
748 edited |= coerce_value(registry, &mut value.node, &ty);
749 }
750 if let Some(meta) = meta {
751 let inner = MetaTypeName::concrete_typename(&meta.ty).to_string();
752 edited |=
753 coerce_selection_set(registry, &mut field.node.selection_set.node, &inner);
754 }
755 }
756 Selection::InlineFragment(fragment) => {
757 let inner = fragment
758 .node
759 .type_condition
760 .as_ref()
761 .map(|condition| condition.node.on.node.to_string())
762 .unwrap_or_else(|| type_name.to_string());
763 edited |=
764 coerce_selection_set(registry, &mut fragment.node.selection_set.node, &inner);
765 }
766 Selection::FragmentSpread(_) => {}
767 }
768 }
769 edited
770}
771
772fn coerce_value(
774 registry: &async_graphql::registry::Registry,
775 value: &mut async_graphql_value::Value,
776 expected: &str,
777) -> bool {
778 use async_graphql::registry::{MetaType, MetaTypeName};
779 use async_graphql_value::Value;
780
781 match value {
782 Value::List(items) => {
783 let inner = match MetaTypeName::create(expected).unwrap_non_null() {
786 MetaTypeName::List(inner) => inner.to_string(),
787 _ => expected.to_string(),
788 };
789 let mut edited = false;
790 for item in items {
791 edited |= coerce_value(registry, item, &inner);
792 }
793 edited
794 }
795 Value::Object(fields) => {
796 let name = MetaTypeName::concrete_typename(expected);
797 let Some(MetaType::InputObject { input_fields, .. }) = registry.types.get(name) else {
798 return false;
799 };
800 let types: Vec<(async_graphql::Name, String)> = fields
801 .keys()
802 .filter_map(|key| {
803 input_fields
804 .get(key.as_str())
805 .map(|field| (key.clone(), field.ty.clone()))
806 })
807 .collect();
808 let mut edited = false;
809 for (key, ty) in types {
810 if let Some(item) = fields.get_mut(&key) {
811 edited |= coerce_value(registry, item, &ty);
812 }
813 }
814 edited
815 }
816 Value::String(text) => {
817 let Some(coerced) = as_written(text, MetaTypeName::concrete_typename(expected)) else {
818 return false;
819 };
820 *value = coerced;
821 true
822 }
823 _ => false,
824 }
825}
826
827fn as_written(text: &str, expected: &str) -> Option<async_graphql_value::Value> {
832 use async_graphql_value::{Number, Value};
833 match expected {
834 "Int" => text.parse::<i64>().ok().map(|n| Value::Number(n.into())),
835 "Float" => text
836 .parse::<f64>()
837 .ok()
838 .and_then(Number::from_f64)
839 .map(Value::Number),
840 "Boolean" => match text {
841 "true" => Some(Value::Boolean(true)),
842 "false" => Some(Value::Boolean(false)),
843 _ => None,
844 },
845 _ => None,
846 }
847}
848
849#[cfg(test)]
850mod unused_variable_tests {
851 use super::*;
852
853 fn declarations(query: &str) -> Vec<String> {
854 let request = allow_unused_variables(async_graphql::Request::new(query));
855 let mut request = request;
858 let doc = request.parsed_query().expect("the query parses");
859 doc.operations
860 .iter()
861 .flat_map(|(_, operation)| operation.node.variable_definitions.iter())
862 .map(|definition| definition.node.name.node.to_string())
863 .collect()
864 }
865
866 #[test]
867 fn a_variable_nothing_names_is_dropped() {
868 assert_eq!(
869 declarations("query ($tags: jsonb) { article(where: {id: {_eq: 1}}) { id } }"),
870 Vec::<String>::new()
871 );
872 }
873
874 #[test]
875 fn a_variable_an_argument_names_is_kept() {
876 assert_eq!(
877 declarations("query ($id: Int) { article(where: {id: {_eq: $id}}) { id } }"),
878 vec!["id".to_string()]
879 );
880 }
881
882 #[test]
883 fn a_variable_named_inside_a_list_is_kept() {
884 assert_eq!(
885 declarations("query ($id: Int) { article(where: {_or: [{id: {_eq: $id}}]}) { id } }"),
886 vec!["id".to_string()]
887 );
888 }
889
890 #[test]
891 fn a_variable_only_a_fragment_names_is_kept() {
892 assert_eq!(
893 declarations(
894 "query ($n: Int) { author { ...rows } } \
895 fragment rows on author { articles(limit: $n) { id } }"
896 ),
897 vec!["n".to_string()]
898 );
899 }
900
901 #[test]
902 fn a_document_that_does_not_parse_is_left_alone() {
903 let request = allow_unused_variables(async_graphql::Request::new("query ("));
904 assert_eq!(request.query, "query (");
905 }
906}
907
908fn variable_errors(
922 doc: &async_graphql::parser::types::ExecutableDocument,
923 registry: &async_graphql::registry::Registry,
924 variables: &async_graphql::Variables,
925) -> Vec<ServerError> {
926 use async_graphql::parser::types::{DocumentOperations, OperationType};
927
928 let mut errors: Vec<ServerError> = Vec::new();
929
930 let operations: Vec<
931 &async_graphql::Positioned<async_graphql::parser::types::OperationDefinition>,
932 > = match &doc.operations {
933 DocumentOperations::Single(operation) => vec![operation],
934 DocumentOperations::Multiple(operations) => operations.values().collect(),
935 };
936
937 for operation in operations {
938 let root = match operation.node.ty {
939 OperationType::Query => Some(registry.query_type.as_str()),
940 OperationType::Mutation => registry.mutation_type.as_deref(),
941 OperationType::Subscription => registry.subscription_type.as_deref(),
942 };
943 let Some(root) = root else {
944 if operation.node.ty == OperationType::Mutation {
948 errors.push(coded("no mutations exist".to_string(), operation.pos, "$"));
949 }
950 continue;
951 };
952
953 let mut declared: HashMap<&str, String> = HashMap::new();
955 for definition in &operation.node.variable_definitions {
956 let name = definition.node.name.node.as_str();
957 let written = definition.node.var_type.node.to_string();
958 let defaulted = definition
964 .node
965 .default_value
966 .as_ref()
967 .is_some_and(|value| !matches!(value.node, async_graphql::Value::Null));
968 let effective = match (definition.node.var_type.node.nullable, defaulted) {
969 (true, true) => format!("{}!", written),
970 _ => written.clone(),
971 };
972 declared.insert(name, effective);
973
974 let given = variables.get(&async_graphql::Name::new(name));
978 if !definition.node.var_type.node.nullable
979 && matches!(given, Some(async_graphql::Value::Null))
980 {
981 errors.push(coded(
982 format!("null value found for non-nullable type: \"{}\"", written),
983 definition.pos,
984 "$",
985 ));
986 }
987 }
988 let mut scope = Usage {
992 registry,
993 fragments: &doc.fragments,
994 declared: &declared,
995 variables,
996 errors: &mut errors,
997 spreading: Vec::new(),
998 expansions: 0,
999 };
1000 scope.selection_set(&operation.node.selection_set.node, root, "$");
1001 }
1002
1003 errors
1004}
1005
1006struct Usage<'a> {
1008 registry: &'a async_graphql::registry::Registry,
1009 fragments: &'a std::collections::HashMap<
1010 async_graphql::Name,
1011 async_graphql::Positioned<async_graphql::parser::types::FragmentDefinition>,
1012 >,
1013 declared: &'a HashMap<&'a str, String>,
1014 variables: &'a async_graphql::Variables,
1017 errors: &'a mut Vec<ServerError>,
1018 spreading: Vec<String>,
1027 expansions: usize,
1031}
1032
1033impl Usage<'_> {
1034 fn selection_set(
1039 &mut self,
1040 set: &async_graphql::parser::types::SelectionSet,
1041 type_name: &str,
1042 path: &str,
1043 ) {
1044 use async_graphql::parser::types::Selection;
1045 use async_graphql::registry::MetaTypeName;
1046
1047 for selection in &set.items {
1048 match &selection.node {
1049 Selection::Field(field) => {
1050 let named = field.node.name.node.as_str();
1051 let parent = self.registry.types.get(type_name);
1052 let meta = parent.and_then(|ty| ty.field_by_name(named));
1053 let here = format!("{}.selectionSet.{}", path, field.node.name.node);
1054 if meta.is_none() && parent.is_some() && !named.starts_with("__") {
1063 self.errors.push(coded(
1064 format!("field '{}' not found in type: '{}'", named, type_name),
1065 field.pos,
1066 &here,
1067 ));
1068 continue;
1069 }
1070 for (name, value) in &field.node.arguments {
1071 let Some(argument) =
1072 meta.and_then(|meta| meta.args.get(name.node.as_str()))
1073 else {
1074 if meta.is_some() {
1079 self.errors.push(coded(
1080 format!("'{}' has no argument named '{}'", named, name.node),
1081 name.pos,
1082 &here,
1083 ));
1084 }
1085 continue;
1086 };
1087 let expected = relax(&argument.ty, argument.default_value.is_some());
1092 let at = format!("{}.args.{}", here, name.node);
1093 self.rows_asked_for(
1094 name.node.as_str(),
1095 &value.node,
1096 &expected,
1097 value.pos,
1098 &at,
1099 );
1100 self.object_expected(&value.node, &expected, value.pos, &at);
1101 self.value(&value.node, &expected, value.pos, &at);
1102 }
1103 self.one_operator_per_column(&field.node.arguments, &here);
1104 self.directives(&field.node.directives, &here);
1105 if let Some(meta) = meta {
1106 let inner = MetaTypeName::concrete_typename(&meta.ty).to_string();
1107 self.selection_set(&field.node.selection_set.node, &inner, &here);
1108 }
1109 }
1110 Selection::InlineFragment(fragment) => {
1111 let inner = fragment
1112 .node
1113 .type_condition
1114 .as_ref()
1115 .map(|condition| condition.node.on.node.to_string())
1116 .unwrap_or_else(|| type_name.to_string());
1117 self.directives(&fragment.node.directives, path);
1118 self.selection_set(&fragment.node.selection_set.node, &inner, path);
1119 }
1120 Selection::FragmentSpread(spread) => {
1121 let name = spread.node.fragment_name.node.to_string();
1122 self.directives(&spread.node.directives, path);
1123 if let Some(began) = self.spreading.iter().position(|held| held == &name) {
1130 let cycle: Vec<&str> =
1131 self.spreading[began..].iter().map(String::as_str).collect();
1132 self.errors.push(coded(
1133 format!("the fragment definition(s) {} form a cycle", listed(&cycle)),
1134 spread.pos,
1135 &format!("{}.selectionSet", path),
1139 ));
1140 continue;
1141 }
1142 self.expansions += 1;
1143 if self.expansions > SPREAD_BUDGET {
1144 continue;
1145 }
1146 if let Some(fragment) = self.fragments.get(&async_graphql::Name::new(&name)) {
1147 let on = fragment.node.type_condition.node.on.node.to_string();
1148 let here = format!("{}.selectionSet.{}", path, name);
1152 self.spreading.push(name);
1153 self.selection_set(&fragment.node.selection_set.node, &on, &here);
1154 self.spreading.pop();
1155 }
1156 }
1157 }
1158 }
1159 }
1160
1161 fn directives(
1162 &mut self,
1163 directives: &[async_graphql::Positioned<async_graphql::parser::types::Directive>],
1164 path: &str,
1165 ) {
1166 for directive in directives {
1167 let meta = self
1168 .registry
1169 .directives
1170 .get(directive.node.name.node.as_str());
1171 for (name, value) in &directive.node.arguments {
1172 let Some(argument) = meta.and_then(|meta| meta.args.get(name.node.as_str())) else {
1173 continue;
1174 };
1175 let expected = relax(&argument.ty, argument.default_value.is_some());
1176 let at = format!("{}.args.{}", path, name.node);
1177 self.value(&value.node, &expected, value.pos, &at);
1178 }
1179 }
1180 }
1181
1182 fn stands_for_null(&self, value: &async_graphql_value::Value) -> bool {
1189 use async_graphql_value::Value;
1190 match value {
1191 Value::Null => true,
1192 Value::Variable(name) => matches!(
1193 self.variables.get(&async_graphql::Name::new(name)),
1194 Some(async_graphql::Value::Null)
1195 ),
1196 _ => false,
1197 }
1198 }
1199
1200 fn value(
1202 &mut self,
1203 value: &async_graphql_value::Value,
1204 expected: &str,
1205 pos: async_graphql::Pos,
1206 path: &str,
1207 ) {
1208 use async_graphql::registry::{MetaType, MetaTypeName};
1209 use async_graphql_value::Value;
1210
1211 match value {
1212 Value::Variable(name) => {
1213 let Some(declared) = self.declared.get(name.as_str()) else {
1214 return;
1216 };
1217 if !MetaTypeName::create(expected).is_subtype(&MetaTypeName::create(declared)) {
1218 self.errors.push(coded(
1219 format!(
1220 "variable '{}' is declared as '{}', but used where '{}' is expected",
1221 name, declared, expected
1222 ),
1223 pos,
1224 path,
1225 ));
1226 return;
1227 }
1228 if let Some(given) = self.variables.get(&async_graphql::Name::new(name)) {
1233 let given: async_graphql_value::Value = given.clone().into();
1234 self.enumerated(&given, expected, pos, path, true);
1235 self.label_path(&given, expected, pos, path);
1236 self.raster_hex(&given, expected, pos, path);
1237 }
1238 }
1239 Value::Enum(_) | Value::String(_) => {
1240 self.enumerated(value, expected, pos, path, false);
1241 self.label_path(value, expected, pos, path);
1242 self.raster_hex(value, expected, pos, path);
1243 }
1244 Value::Number(_) => self.integral(value, expected, pos, path),
1245 Value::List(items) => {
1246 let inner = match MetaTypeName::create(expected).unwrap_non_null() {
1250 MetaTypeName::List(inner) => inner.to_string(),
1251 _ => expected.to_string(),
1252 };
1253 for (index, item) in items.iter().enumerate() {
1254 self.value(item, &inner, pos, &format!("{}[{}]", path, index));
1255 }
1256 }
1257 Value::Object(fields) => {
1258 let name = MetaTypeName::concrete_typename(expected);
1259 let Some(MetaType::InputObject { input_fields, .. }) =
1260 self.registry.types.get(name)
1261 else {
1262 return;
1263 };
1264 let comparison = name.ends_with("_comparison_exp");
1272 let base = match MetaTypeName::create(expected).unwrap_non_null() {
1277 MetaTypeName::List(_) => format!("{}[0]", path),
1278 _ => path.to_string(),
1279 };
1280 for (key, item) in fields {
1281 let at = format!("{}.{}", base, key.as_str());
1282 let Some(field) = input_fields.get(key.as_str()) else {
1283 self.errors.push(coded(
1288 format!("field '{}' not found in type: '{}'", key.as_str(), name),
1289 pos,
1290 &at,
1291 ));
1292 continue;
1293 };
1294 if comparison && self.stands_for_null(item) {
1295 self.errors.push(coded(
1296 format!(
1297 "unexpected null value for type '{}'",
1298 MetaTypeName::concrete_typename(&field.ty)
1299 ),
1300 pos,
1301 &at,
1302 ));
1303 continue;
1304 }
1305 let ty = relax(&field.ty, field.default_value.is_some());
1306 self.value(item, &ty, pos, &at);
1307 }
1308 }
1309 _ => {}
1310 }
1311 }
1312}
1313
1314impl Usage<'_> {
1315 fn object_expected(
1327 &mut self,
1328 value: &async_graphql_value::Value,
1329 expected: &str,
1330 pos: async_graphql::Pos,
1331 path: &str,
1332 ) {
1333 use async_graphql::registry::{MetaType, MetaTypeName};
1334
1335 if !matches!(MetaTypeName::create(expected), MetaTypeName::NonNull(_)) {
1336 return;
1337 }
1338 if !self.stands_for_null(value) {
1339 return;
1340 }
1341 let name = MetaTypeName::concrete_typename(expected);
1342 if !matches!(
1343 self.registry.types.get(name),
1344 Some(MetaType::InputObject { .. })
1345 ) {
1346 return;
1347 }
1348 self.errors.push(coded(
1349 format!("expected an object for type '{}', but found null", name),
1350 pos,
1351 path,
1352 ));
1353 }
1354
1355 fn one_operator_per_column(
1368 &mut self,
1369 arguments: &[(
1370 async_graphql::Positioned<async_graphql::Name>,
1371 async_graphql::Positioned<async_graphql_value::Value>,
1372 )],
1373 path: &str,
1374 ) {
1375 use async_graphql_value::Value;
1376
1377 const OPERATORS: [&str; 7] = [
1378 "_set",
1379 "_inc",
1380 "_append",
1381 "_prepend",
1382 "_delete_key",
1383 "_delete_elem",
1384 "_delete_at_path",
1385 ];
1386
1387 let mut seen: Vec<&str> = Vec::new();
1388 let mut twice: Vec<&str> = Vec::new();
1389 for (name, value) in arguments {
1390 if !OPERATORS.contains(&name.node.as_str()) {
1391 continue;
1392 }
1393 let Value::Object(columns) = &value.node else {
1394 continue;
1395 };
1396 for column in columns.keys() {
1397 let column = column.as_str();
1398 match seen.contains(&column) {
1399 true if !twice.contains(&column) => twice.push(column),
1400 true => {}
1401 false => seen.push(column),
1402 }
1403 }
1404 }
1405 if twice.is_empty() {
1406 return;
1407 }
1408 let listed = twice
1411 .iter()
1412 .map(|column| format!("'{}'", column))
1413 .collect::<Vec<String>>()
1414 .join(", ");
1415 self.errors.push(coded(
1416 format!("Column found in multiple operators: [{}].", listed),
1417 arguments
1418 .first()
1419 .map(|(name, _)| name.pos)
1420 .unwrap_or_default(),
1421 &format!("{}.args", path),
1422 ));
1423 }
1424
1425 fn integral(
1433 &mut self,
1434 value: &async_graphql_value::Value,
1435 expected: &str,
1436 pos: async_graphql::Pos,
1437 path: &str,
1438 ) {
1439 use async_graphql::registry::MetaTypeName;
1440 use async_graphql_value::Value;
1441
1442 if MetaTypeName::concrete_typename(expected) != "Int" {
1443 return;
1444 }
1445 let Value::Number(number) = value else {
1446 return;
1447 };
1448 if number.as_i64().is_some_and(|n| i32::try_from(n).is_ok()) {
1449 return;
1450 }
1451 let Some(shown) = number.as_f64().map(shown_as_haskell_would) else {
1452 return;
1453 };
1454 self.errors.push(coded_as(
1455 "parse-failed",
1456 format!(
1457 "The value {} lies outside the bounds or is not an integer. \
1458 Maybe it is a float, or is there integer overflow?",
1459 shown
1460 ),
1461 pos,
1462 path,
1463 ));
1464 }
1465
1466 fn rows_asked_for(
1480 &mut self,
1481 argument: &str,
1482 value: &async_graphql_value::Value,
1483 expected: &str,
1484 pos: async_graphql::Pos,
1485 path: &str,
1486 ) {
1487 use async_graphql::registry::MetaTypeName;
1488 use async_graphql_value::Value;
1489
1490 if argument != "limit" || MetaTypeName::concrete_typename(expected) != "Int" {
1491 return;
1492 }
1493 let value = match value {
1495 Value::Variable(name) => {
1496 match self.variables.get(&async_graphql::Name::new(name.as_str())) {
1497 Some(given) => std::borrow::Cow::Owned(given.clone().into()),
1498 None => return,
1499 }
1500 }
1501 other => std::borrow::Cow::Borrowed(other),
1502 };
1503
1504 let found = match value.as_ref() {
1505 Value::String(_) => "a string",
1506 Value::Number(number) => match number.as_i64() {
1507 None => return,
1511 Some(written) if i32::try_from(written).is_err() => return,
1512 Some(written) if written >= 0 => return,
1514 Some(_) => "an integer",
1515 },
1516 _ => return,
1517 };
1518 self.errors.push(coded(
1519 format!(
1520 "expected a non-negative 32-bit integer for type 'Int', but found {}",
1521 found
1522 ),
1523 pos,
1524 path,
1525 ));
1526 }
1527
1528 fn raster_hex(
1542 &mut self,
1543 value: &async_graphql_value::Value,
1544 expected: &str,
1545 pos: async_graphql::Pos,
1546 path: &str,
1547 ) {
1548 use async_graphql::registry::MetaTypeName;
1549 use async_graphql_value::Value;
1550
1551 if MetaTypeName::concrete_typename(expected) != "raster" {
1552 return;
1553 }
1554 let Value::String(written) = value else {
1555 return;
1556 };
1557 if written.len() % 2 == 0 && written.chars().all(|c| c.is_ascii_hexdigit()) {
1558 return;
1559 }
1560 self.errors.push(coded_as(
1561 "parse-failed",
1562 "invalid hexadecimal representation of raster well known binary format".to_string(),
1563 pos,
1564 path,
1565 ));
1566 }
1567
1568 fn label_path(
1584 &mut self,
1585 value: &async_graphql_value::Value,
1586 expected: &str,
1587 pos: async_graphql::Pos,
1588 path: &str,
1589 ) {
1590 use async_graphql::registry::MetaTypeName;
1591 use async_graphql_value::Value;
1592
1593 if MetaTypeName::concrete_typename(expected) != "ltree" {
1594 return;
1595 }
1596 let Value::String(written) = value else {
1597 return;
1598 };
1599 if written.is_empty() || written.split('.').all(|label| !label.is_empty()) {
1602 return;
1603 }
1604 self.errors.push(coded_as(
1605 "parse-failed",
1606 "Expecting label path: a sequence of zero or more labels separated by \
1607 dots, for example L1.L2.L3"
1608 .to_string(),
1609 pos,
1610 path,
1611 ));
1612 }
1613
1614 fn enumerated(
1620 &mut self,
1621 value: &async_graphql_value::Value,
1622 expected: &str,
1623 pos: async_graphql::Pos,
1624 path: &str,
1625 from_variable: bool,
1631 ) {
1632 use async_graphql::registry::{MetaType, MetaTypeName};
1633 use async_graphql_value::Value;
1634
1635 let name = MetaTypeName::concrete_typename(expected);
1636 let Some(MetaType::Enum { enum_values, .. }) = self.registry.types.get(name) else {
1637 return;
1638 };
1639 let written = match value {
1640 Value::Enum(written) => written.as_str(),
1641 Value::String(written) if from_variable => written.as_str(),
1642 Value::String(_) => {
1643 self.errors.push(coded(
1644 format!(
1645 "expected an enum value for type '{}', but found a string",
1646 name
1647 ),
1648 pos,
1649 path,
1650 ));
1651 return;
1652 }
1653 _ => return,
1654 };
1655 if enum_values.contains_key(written) {
1656 return;
1657 }
1658 let mut allowed: Vec<&str> = enum_values.keys().map(String::as_str).collect();
1661 allowed.sort_unstable();
1662 let listed = allowed
1663 .iter()
1664 .map(|value| format!("'{}'", value))
1665 .collect::<Vec<_>>()
1666 .join(", ");
1667 self.errors.push(coded(
1668 format!(
1669 "expected one of the values [{}] for type '{}', but found '{}'",
1670 listed, name, written
1671 ),
1672 pos,
1673 path,
1674 ));
1675 }
1676}
1677
1678const SPREAD_BUDGET: usize = 1000;
1686
1687fn listed(names: &[&str]) -> String {
1690 match names {
1691 [] => String::new(),
1692 [one] => one.to_string(),
1693 [rest @ .., last] => format!("{} and {}", rest.join(", "), last),
1694 }
1695}
1696
1697fn shown_as_haskell_would(value: f64) -> String {
1708 if !value.is_finite() {
1709 return value.to_string();
1710 }
1711 let magnitude = value.abs();
1712 let with_a_point = |rendered: String| match rendered.contains('.') {
1713 true => rendered,
1714 false => format!("{}.0", rendered),
1715 };
1716 if magnitude != 0.0 && (magnitude < 0.1 || magnitude >= 1e7) {
1717 let rendered = format!("{:e}", value);
1718 let (mantissa, exponent) = match rendered.split_once('e') {
1719 Some(halves) => halves,
1720 None => return with_a_point(rendered),
1721 };
1722 return format!("{}e{}", with_a_point(mantissa.to_string()), exponent);
1723 }
1724 with_a_point(format!("{}", value))
1725}
1726
1727fn relax(ty: &str, has_default: bool) -> String {
1733 match has_default {
1734 true => ty.strip_suffix('!').unwrap_or(ty).to_string(),
1735 false => ty.to_string(),
1736 }
1737}
1738
1739fn coded(message: String, pos: async_graphql::Pos, path: &str) -> ServerError {
1746 coded_as("validation-failed", message, pos, path)
1747}
1748
1749fn coded_as(
1751 code: &'static str,
1752 message: String,
1753 pos: async_graphql::Pos,
1754 path: &str,
1755) -> ServerError {
1756 let mut error = ServerError::new(message, Some(pos));
1757 let mut extensions = async_graphql::ErrorExtensionValues::default();
1758 extensions.set("code", code);
1759 extensions.set("path", path);
1760 error.extensions = Some(extensions);
1761 error
1762}
1763
1764#[cfg(test)]
1765mod variable_position_tests {
1766 use super::*;
1767 use async_graphql::dynamic::*;
1768
1769 fn schema() -> async_graphql::dynamic::Schema {
1773 let filter = InputObject::new("author_bool_exp")
1774 .field(InputValue::new("id", TypeRef::named("Int")))
1775 .field(InputValue::new(
1776 "name",
1777 TypeRef::named("String_comparison_exp"),
1778 ));
1779 let comparison = InputObject::new("String_comparison_exp")
1780 .field(InputValue::new("_eq", TypeRef::named("String")))
1781 .field(InputValue::new("_in", TypeRef::named_nn_list("String")));
1782 let ltree = InputObject::new("ltree_comparison_exp")
1785 .field(InputValue::new("_ancestor", TypeRef::named("ltree")))
1786 .field(InputValue::new(
1787 "_ancestor_any",
1788 TypeRef::named_list("ltree"),
1789 ));
1790 let kind = Enum::new("kind_enum").item("leaf").item("branch");
1791 let kind_comparison = InputObject::new("kind_enum_comparison_exp")
1792 .field(InputValue::new("_eq", TypeRef::named("kind_enum")));
1793 let raster_comparison = InputObject::new("raster_comparison_exp").field(InputValue::new(
1794 "_st_intersects_rast",
1795 TypeRef::named("raster"),
1796 ));
1797 let tree_filter = InputObject::new("tree_bool_exp")
1798 .field(InputValue::new(
1799 "rast",
1800 TypeRef::named("raster_comparison_exp"),
1801 ))
1802 .field(InputValue::new(
1803 "path",
1804 TypeRef::named("ltree_comparison_exp"),
1805 ))
1806 .field(InputValue::new(
1807 "kind",
1808 TypeRef::named("kind_enum_comparison_exp"),
1809 ));
1810 let insert = InputObject::new("author_insert_input")
1811 .field(InputValue::new("name", TypeRef::named("String")))
1812 .field(InputValue::new("count", TypeRef::named("Int")));
1813 let query = Object::new("query_root")
1814 .field(
1815 Field::new("author", TypeRef::named_nn_list_nn("author"), |_| {
1816 FieldFuture::new(async move { Ok(Some(async_graphql::Value::List(vec![]))) })
1817 })
1818 .argument(InputValue::new("limit", TypeRef::named("Int")))
1819 .argument(InputValue::new("where", TypeRef::named("author_bool_exp"))),
1820 )
1821 .field(
1822 Field::new("tree", TypeRef::named_nn_list_nn("tree"), |_| {
1823 FieldFuture::new(async move { Ok(Some(async_graphql::Value::List(vec![]))) })
1824 })
1825 .argument(InputValue::new("where", TypeRef::named("tree_bool_exp"))),
1826 );
1827 let mutation = Object::new("mutation_root")
1828 .field(
1829 Field::new("insert_author_one", TypeRef::named("author"), |_| {
1830 FieldFuture::new(async move { Ok(None::<async_graphql::Value>) })
1831 })
1832 .argument(InputValue::new(
1833 "object",
1834 TypeRef::named_nn("author_insert_input"),
1835 )),
1836 )
1837 .field(
1838 Field::new("insert_author", TypeRef::named("author"), |_| {
1839 FieldFuture::new(async move { Ok(None::<async_graphql::Value>) })
1840 })
1841 .argument(InputValue::new(
1842 "objects",
1843 TypeRef::named_nn_list_nn("author_insert_input"),
1844 ))
1845 .argument(
1846 InputValue::new("update_columns", TypeRef::named_nn_list_nn("String"))
1847 .default_value(async_graphql::Value::List(Vec::new())),
1848 ),
1849 )
1850 .field(
1851 Field::new(
1852 "update_author",
1853 TypeRef::named("author_mutation_response"),
1854 |_| FieldFuture::new(async move { Ok(None::<async_graphql::Value>) }),
1855 )
1856 .argument(InputValue::new(
1857 "where",
1858 TypeRef::named_nn("author_bool_exp"),
1859 ))
1860 .argument(InputValue::new("_set", TypeRef::named("author_set_input")))
1861 .argument(InputValue::new("_inc", TypeRef::named("author_inc_input"))),
1862 );
1863 let author = Object::new("author").field(Field::new("id", TypeRef::named("Int"), |_| {
1864 FieldFuture::new(async move { Ok(None::<async_graphql::Value>) })
1865 }));
1866 let tree = Object::new("tree")
1867 .field(Field::new("id", TypeRef::named("Int"), |_| {
1868 FieldFuture::new(async move { Ok(None::<async_graphql::Value>) })
1869 }))
1870 .field(Field::new("path", TypeRef::named("ltree"), |_| {
1871 FieldFuture::new(async move { Ok(None::<async_graphql::Value>) })
1872 }));
1873 let set = InputObject::new("author_set_input")
1875 .field(InputValue::new("name", TypeRef::named("String")))
1876 .field(InputValue::new("count", TypeRef::named("Int")));
1877 let inc = InputObject::new("author_inc_input")
1878 .field(InputValue::new("count", TypeRef::named("Int")));
1879 let response = Object::new("author_mutation_response").field(Field::new(
1880 "affected_rows",
1881 TypeRef::named_nn("Int"),
1882 |_| FieldFuture::new(async move { Ok(None::<async_graphql::Value>) }),
1883 ));
1884 Schema::build("query_root", Some("mutation_root"), None)
1885 .register(Scalar::new("ltree"))
1886 .register(Scalar::new("raster"))
1887 .register(raster_comparison)
1888 .register(kind)
1889 .register(kind_comparison)
1890 .register(filter)
1891 .register(comparison)
1892 .register(ltree)
1893 .register(tree_filter)
1894 .register(insert)
1895 .register(set)
1896 .register(inc)
1897 .register(response)
1898 .register(author)
1899 .register(tree)
1900 .register(query)
1901 .register(mutation)
1902 .finish()
1903 .expect("the test schema builds")
1904 }
1905
1906 fn refusals(query: &str, variables: &str) -> Vec<String> {
1907 let request =
1908 async_graphql::Request::new(query).variables(async_graphql::Variables::from_json(
1909 serde_json::from_str(variables).expect("the variables are JSON"),
1910 ));
1911 match prepare(Some(&schema()), request, None) {
1912 Ok(_) => Vec::new(),
1913 Err((_, errors)) => errors.into_iter().map(|e| e.message).collect(),
1914 }
1915 }
1916
1917 fn refusals_with_paths(query: &str, variables: &str) -> Vec<String> {
1919 let request =
1920 async_graphql::Request::new(query).variables(async_graphql::Variables::from_json(
1921 serde_json::from_str(variables).expect("the variables are JSON"),
1922 ));
1923 match prepare(Some(&schema()), request, None) {
1924 Ok(_) => Vec::new(),
1925 Err((_, errors)) => errors.iter().map(path_for).collect(),
1926 }
1927 }
1928
1929 #[test]
1932 fn a_null_written_into_a_comparison_is_refused() {
1933 assert_eq!(
1934 refusals("{ author(where: {name: {_eq: null}}) { id } }", "{}"),
1935 vec!["unexpected null value for type 'String'"]
1936 );
1937 }
1938
1939 #[test]
1940 fn a_variable_standing_for_a_null_is_refused_there_too() {
1941 assert_eq!(
1942 refusals(
1943 "query ($n: String) { author(where: {name: {_eq: $n}}) { id } }",
1944 r#"{"n": null}"#
1945 ),
1946 vec!["unexpected null value for type 'String'"]
1947 );
1948 }
1949
1950 #[test]
1955 fn a_refusal_names_the_argument_it_was_found_in() {
1956 let paths =
1957 |query: &str, variables: &str| -> Vec<String> { refusals_with_paths(query, variables) };
1958 assert_eq!(
1960 paths(
1961 "mutation ($n: Int!) { insert_author_one(object: {name: $n}) { id } }",
1962 r#"{"n": 1}"#
1963 ),
1964 vec!["$.selectionSet.insert_author_one.args.object.name"]
1965 );
1966 assert_eq!(
1968 paths("{ author(where: {name: {_eq: null}}) { id } }", "{}"),
1969 vec!["$.selectionSet.author.args.where.name._eq"]
1970 );
1971 assert_eq!(
1974 paths(
1975 "mutation ($n: Int!) { insert_author(objects: {name: $n}) { id } }",
1976 r#"{"n": 1}"#
1977 ),
1978 vec!["$.selectionSet.insert_author.args.objects[0].name"]
1979 );
1980 assert_eq!(
1982 paths(
1983 "mutation ($n: Int!) { insert_author(objects: [{name: \"a\"}, \
1984 {name: $n}]) { id } }",
1985 r#"{"n": 1}"#
1986 ),
1987 vec!["$.selectionSet.insert_author.args.objects[1].name"]
1988 );
1989 }
1990
1991 #[test]
1994 fn a_null_written_where_an_object_is_required_is_refused_as_one() {
1995 assert_eq!(
1996 refusals(
1997 "mutation { update_author(where: null, _set: {name: \"a\"}) \
1998 { affected_rows } }",
1999 "{}"
2000 ),
2001 vec!["expected an object for type 'author_bool_exp', but found null"]
2002 );
2003 assert_eq!(
2004 refusals_with_paths(
2005 "mutation { update_author(where: null, _set: {name: \"a\"}) \
2006 { affected_rows } }",
2007 "{}"
2008 ),
2009 vec!["$.selectionSet.update_author.args.where"]
2010 );
2011 assert_eq!(
2015 refusals(
2016 "mutation ($w: author_bool_exp!) { update_author(where: $w, \
2017 _set: {name: \"a\"}) { affected_rows } }",
2018 r#"{"w": null}"#
2019 ),
2020 vec!["null value found for non-nullable type: \"author_bool_exp!\""]
2021 );
2022 }
2023
2024 #[test]
2027 fn a_null_written_where_an_object_is_optional_is_left_alone() {
2028 assert_eq!(
2029 refusals("{ author(where: null) { id } }", "{}"),
2030 Vec::<String>::new()
2031 );
2032 }
2033
2034 #[test]
2037 fn a_column_written_by_two_operators_is_refused_before_the_statement() {
2038 assert_eq!(
2039 refusals(
2040 "mutation { update_author(_set: {name: \"a\", count: 1}, \
2041 _inc: {count: 2}, where: {}) { affected_rows } }",
2042 "{}"
2043 ),
2044 vec!["Column found in multiple operators: ['count']."]
2045 );
2046 assert_eq!(
2047 refusals_with_paths(
2048 "mutation { update_author(_set: {count: 1}, _inc: {count: 2}, \
2049 where: {}) { affected_rows } }",
2050 "{}"
2051 ),
2052 vec!["$.selectionSet.update_author.args"]
2053 );
2054 assert_eq!(
2056 refusals(
2057 "mutation { update_author(_set: {name: \"a\"}, _inc: {count: 2}, \
2058 where: {}) { affected_rows } }",
2059 "{}"
2060 ),
2061 Vec::<String>::new()
2062 );
2063 }
2064
2065 #[test]
2068 fn a_request_is_refused_once_however_many_faults_it_has() {
2069 assert_eq!(
2070 refusals("{ author { nope } tree { alsoNope } }", "{}"),
2071 vec!["field 'nope' not found in type: 'author'"]
2072 );
2073 assert_eq!(
2077 refusals(
2078 "mutation { insert_author(objects: [{nope: 1}], unknownArg: 2) { id } }",
2079 "{}"
2080 ),
2081 vec!["field 'nope' not found in type: 'author_insert_input'"]
2082 );
2083 }
2084
2085 #[test]
2088 fn a_raster_that_is_not_hexadecimal_is_refused_as_that() {
2089 let invalid = "invalid hexadecimal representation of raster well known binary format";
2090 assert_eq!(
2091 refusals(
2092 "query ($r: raster) { tree(where: {rast: {_st_intersects_rast: $r}}) { path } }",
2093 r#"{"r": "this is invalid raster value"}"#
2094 ),
2095 vec![invalid]
2096 );
2097 assert_eq!(
2098 refusals_with_paths(
2099 "{ tree(where: {rast: {_st_intersects_rast: \"zz\"}}) { path } }",
2100 "{}"
2101 ),
2102 vec!["$.selectionSet.tree.args.where.rast._st_intersects_rast"]
2103 );
2104 assert_eq!(
2106 refusals(
2107 "{ tree(where: {rast: {_st_intersects_rast: \"abc\"}}) { path } }",
2108 "{}"
2109 ),
2110 vec![invalid]
2111 );
2112 assert!(refusals(
2115 "{ tree(where: {rast: {_st_intersects_rast: \"0100\"}}) { path } }",
2116 "{}"
2117 )
2118 .is_empty());
2119 }
2120
2121 #[test]
2125 fn fragments_that_spread_each_other_are_named_as_a_cycle() {
2126 let cycle = "query { author { ...a } } \
2127 fragment a on author { id ...b } \
2128 fragment b on author { id ...a }";
2129 assert_eq!(
2130 refusals(cycle, "{}"),
2131 vec!["the fragment definition(s) a and b form a cycle"]
2132 );
2133 assert_eq!(
2136 refusals_with_paths(cycle, "{}"),
2137 vec!["$.selectionSet.author.selectionSet.a.selectionSet.b.selectionSet"]
2138 );
2139 assert_eq!(
2141 refusals(
2142 "query { author { ...a } } fragment a on author { id ...a }",
2143 "{}"
2144 ),
2145 vec!["the fragment definition(s) a form a cycle"]
2146 );
2147 assert!(refusals(
2149 "query { author { ...a } articles: author { ...a } } \
2150 fragment a on author { id }",
2151 "{}"
2152 )
2153 .is_empty());
2154 }
2155
2156 #[test]
2159 fn a_number_that_is_not_an_int_is_refused_as_one() {
2160 let bounds = |value: &str| {
2161 format!(
2162 "The value {} lies outside the bounds or is not an integer. \
2163 Maybe it is a float, or is there integer overflow?",
2164 value
2165 )
2166 };
2167 assert_eq!(
2168 refusals(
2169 "mutation { insert_author_one(object: {count: 2147483648}) { id } }",
2170 "{}"
2171 ),
2172 vec![bounds("2.147483648e9")]
2173 );
2174 assert_eq!(
2175 refusals_with_paths(
2176 "mutation { insert_author(objects: [{count: 2147483648}]) { id } }",
2177 "{}"
2178 ),
2179 vec!["$.selectionSet.insert_author.args.objects[0].count"]
2180 );
2181 for good in ["2147483647", "-2147483648", "0"] {
2183 assert!(
2184 refusals(
2185 &format!(
2186 "mutation {{ insert_author_one(object: {{count: {}}}) {{ id }} }}",
2187 good
2188 ),
2189 "{}"
2190 )
2191 .is_empty(),
2192 "{} was refused",
2193 good
2194 );
2195 }
2196 }
2197
2198 #[test]
2201 fn a_number_is_shown_the_way_the_message_spells_it() {
2202 assert_eq!(shown_as_haskell_would(2147483648.0), "2.147483648e9");
2203 assert_eq!(shown_as_haskell_would(1.5), "1.5");
2204 assert_eq!(shown_as_haskell_would(1.0), "1.0");
2205 assert_eq!(shown_as_haskell_would(0.0), "0.0");
2206 assert_eq!(shown_as_haskell_would(-2147483649.0), "-2.147483649e9");
2207 assert_eq!(shown_as_haskell_would(1e-3), "1.0e-3");
2208 assert_eq!(shown_as_haskell_would(9999999.0), "9999999.0");
2211 assert_eq!(shown_as_haskell_would(10000000.0), "1.0e7");
2212 assert_eq!(shown_as_haskell_would(0.1), "0.1");
2213 }
2214
2215 #[test]
2218 fn a_limit_is_a_number_of_rows_and_not_any_integer() {
2219 let refused = |q: &str| refusals(q, "{}");
2220 assert_eq!(
2221 refused("{ author(limit: -1) { id } }"),
2222 vec!["expected a non-negative 32-bit integer for type 'Int', but found an integer"]
2223 );
2224 assert_eq!(
2225 refused("{ author(limit: \"3\") { id } }"),
2226 vec!["expected a non-negative 32-bit integer for type 'Int', but found a string"]
2227 );
2228 assert_eq!(
2229 refusals_with_paths("{ author(limit: -1) { id } }", "{}"),
2230 vec!["$.selectionSet.author.args.limit"]
2231 );
2232 assert_eq!(
2234 refusals(
2235 "query ($n: Int) { author(limit: $n) { id } }",
2236 r#"{"n": -1}"#
2237 ),
2238 vec!["expected a non-negative 32-bit integer for type 'Int', but found an integer"]
2239 );
2240 assert!(refused("{ author(limit: 0) { id } }").is_empty());
2242 assert!(refused("{ author(limit: 2147483647) { id } }").is_empty());
2243 assert!(refused("{ author(limit: null) { id } }").is_empty());
2244 assert_eq!(
2247 refused("{ author(limit: 4294967295) { id } }"),
2248 vec![
2249 "The value 4.294967295e9 lies outside the bounds or is not an \
2250 integer. Maybe it is a float, or is there integer overflow?"
2251 ]
2252 );
2253 }
2254
2255 #[test]
2258 fn a_quoted_enum_value_is_not_a_member_that_is_missing() {
2259 assert_eq!(
2260 refusals("{ tree(where: {kind: {_eq: \"leaf\"}}) { path } }", "{}"),
2261 vec!["expected an enum value for type 'kind_enum', but found a string"]
2262 );
2263 assert_eq!(
2264 refusals("{ tree(where: {kind: {_eq: twig}}) { path } }", "{}"),
2265 vec!["expected one of the values ['branch', 'leaf'] for type 'kind_enum', but found 'twig'"]
2266 );
2267 assert_eq!(
2270 refusals(
2271 "query ($k: kind_enum) { tree(where: {kind: {_eq: $k}}) { path } }",
2272 r#"{"k": "twig"}"#
2273 ),
2274 vec!["expected one of the values ['branch', 'leaf'] for type 'kind_enum', but found 'twig'"]
2275 );
2276 assert!(refusals(
2277 "query ($k: kind_enum) { tree(where: {kind: {_eq: $k}}) { path } }",
2278 r#"{"k": "leaf"}"#
2279 )
2280 .is_empty());
2281 }
2282
2283 #[test]
2287 fn a_document_that_does_not_parse_is_not_a_query() {
2288 assert_eq!(
2289 refusals("{ author(where: {name: {_eq: \"unclosed}}) { id } }", "{}"),
2290 vec!["not a valid graphql query"]
2291 );
2292 assert_eq!(
2293 refusals_with_paths("query { author(", "{}"),
2294 vec!["$.query"]
2295 );
2296 assert_eq!(
2299 refusals("{ nothing_here { id } }", "{}"),
2300 vec!["field 'nothing_here' not found in type: 'query_root'"]
2301 );
2302 }
2303
2304 #[test]
2308 fn a_label_path_with_an_empty_label_is_refused() {
2309 let empty = "Expecting label path: a sequence of zero or more labels \
2310 separated by dots, for example L1.L2.L3";
2311 assert_eq!(
2312 refusals(
2313 "{ tree(where: {path: {_ancestor: \"a.b.\"}}) { path } }",
2314 "{}"
2315 ),
2316 vec![empty]
2317 );
2318 assert_eq!(
2319 refusals(
2320 "{ tree(where: {path: {_ancestor: \".a\"}}) { path } }",
2321 "{}"
2322 ),
2323 vec![empty]
2324 );
2325 assert_eq!(
2326 refusals(
2327 "{ tree(where: {path: {_ancestor: \"a..b\"}}) { path } }",
2328 "{}"
2329 ),
2330 vec![empty]
2331 );
2332 assert_eq!(
2334 refusals_with_paths(
2335 "{ tree(where: {path: {_ancestor_any: [\"a\", \"b.\"]}}) { path } }",
2336 "{}"
2337 ),
2338 vec!["$.selectionSet.tree.args.where.path._ancestor_any[1]"]
2339 );
2340 for good in ["a", "a.b", "", "a-b", "a_b", "1.2", "Ünï", "a@b"] {
2343 assert!(
2344 refusals(
2345 &format!(
2346 "{{ tree(where: {{path: {{_ancestor: \"{}\"}}}}) {{ path }} }}",
2347 good
2348 ),
2349 "{}"
2350 )
2351 .is_empty(),
2352 "{} was refused",
2353 good
2354 );
2355 }
2356 }
2357
2358 #[test]
2359 fn a_variable_that_was_not_given_is_not_a_null() {
2360 assert!(refusals(
2361 "query ($n: String) { author(where: {name: {_eq: $n}}) { id } }",
2362 "{}"
2363 )
2364 .is_empty());
2365 }
2366
2367 #[test]
2369 fn a_null_written_anywhere_else_is_left_alone() {
2370 assert!(refusals(
2371 "mutation { insert_author_one(object: {name: null}) { id } }",
2372 "{}"
2373 )
2374 .is_empty());
2375 }
2376
2377 #[test]
2378 fn a_variable_of_the_wrong_type_is_refused() {
2379 assert_eq!(
2380 refusals("query ($s: String) { author(limit: $s) { id } }", "{}"),
2381 vec!["variable 's' is declared as 'String', but used where 'Int' is expected"]
2382 );
2383 }
2384
2385 #[test]
2386 fn the_check_reaches_inside_an_input_object() {
2387 assert_eq!(
2388 refusals(
2389 "query ($s: String) { author(where: {id: $s}) { id } }",
2390 "{}"
2391 ),
2392 vec!["variable 's' is declared as 'String', but used where 'Int' is expected"]
2393 );
2394 }
2395
2396 #[test]
2397 fn the_check_reaches_inside_a_list() {
2398 assert_eq!(
2399 refusals(
2400 "mutation ($n: Int) { insert_author(objects: [{name: $n}]) { id } }",
2401 "{}"
2402 ),
2403 vec!["variable 'n' is declared as 'Int', but used where 'String' is expected"]
2404 );
2405 }
2406
2407 #[test]
2408 fn a_nullable_variable_cannot_fill_a_non_null_place() {
2409 assert_eq!(
2410 refusals(
2411 "mutation ($a: author_insert_input) { insert_author_one(object: $a) { id } }",
2412 "{}"
2413 ),
2414 vec![
2415 "variable 'a' is declared as 'author_insert_input', but used where \
2416 'author_insert_input!' is expected"
2417 ]
2418 );
2419 }
2420
2421 #[test]
2422 fn a_default_lets_it() {
2423 assert!(refusals(
2426 "mutation ($a: author_insert_input = {name: \"x\"}) \
2427 { insert_author_one(object: $a) { id } }",
2428 "{}"
2429 )
2430 .is_empty());
2431 }
2432
2433 #[test]
2434 fn a_default_of_null_does_not() {
2435 assert_eq!(
2436 refusals(
2437 "mutation ($a: author_insert_input = null) \
2438 { insert_author_one(object: $a) { id } }",
2439 "{}"
2440 ),
2441 vec![
2442 "variable 'a' is declared as 'author_insert_input', but used where \
2443 'author_insert_input!' is expected"
2444 ]
2445 );
2446 }
2447
2448 #[test]
2449 fn a_default_on_the_place_lets_it_too() {
2450 assert!(refusals(
2453 "mutation ($c: [String!]) { insert_author(objects: [], update_columns: $c) { id } }",
2454 "{}"
2455 )
2456 .is_empty());
2457 }
2458
2459 #[test]
2460 fn a_non_null_variable_fits_a_nullable_place() {
2461 assert!(refusals(
2462 "query ($n: Int!) { author(limit: $n) { id } }",
2463 "{\"n\": 1}"
2464 )
2465 .is_empty());
2466 }
2467
2468 #[test]
2469 fn a_non_null_variable_given_null_is_refused() {
2470 assert_eq!(
2471 refusals(
2472 "query ($n: Int! = 1) { author(limit: $n) { id } }",
2473 "{\"n\": null}"
2474 ),
2475 vec!["null value found for non-nullable type: \"Int!\""]
2476 );
2477 }
2478
2479 #[test]
2480 fn a_nullable_variable_given_null_is_not() {
2481 assert!(refusals(
2482 "query ($n: Int) { author(limit: $n) { id } }",
2483 "{\"n\": null}"
2484 )
2485 .is_empty());
2486 }
2487
2488 #[test]
2489 fn a_variable_used_through_a_fragment_is_checked() {
2490 assert_eq!(
2491 refusals(
2492 "query ($s: String) { author { ...rows } } \
2493 fragment rows on author { id }",
2494 "{}"
2495 ),
2496 Vec::<String>::new()
2497 );
2498 assert_eq!(
2499 refusals(
2500 "query ($s: String) { ...roots } \
2501 fragment roots on query_root { author(limit: $s) { id } }",
2502 "{}"
2503 ),
2504 vec!["variable 's' is declared as 'String', but used where 'Int' is expected"]
2505 );
2506 }
2507}
2508
2509#[cfg(test)]
2510mod coercion_tests {
2511 use super::*;
2512 use async_graphql::dynamic::*;
2513
2514 fn schema() -> async_graphql::dynamic::Schema {
2517 let insert = InputObject::new("test_types_insert_input")
2518 .field(InputValue::new("c1_smallint", TypeRef::named("Int")))
2519 .field(InputValue::new("c6_real", TypeRef::named("Float")))
2520 .field(InputValue::new("c20_boolean", TypeRef::named("Boolean")))
2521 .field(InputValue::new("c13_text", TypeRef::named("String")));
2522 let row = Object::new("test_types").field(Field::new(
2523 "c1_smallint",
2524 TypeRef::named("Int"),
2525 |_| FieldFuture::new(async move { Ok(None::<async_graphql::Value>) }),
2526 ));
2527 let query = Object::new("query_root").field(
2528 Field::new(
2529 "test_types",
2530 TypeRef::named_nn_list_nn("test_types"),
2531 |_| FieldFuture::new(async move { Ok(Some(async_graphql::Value::List(vec![]))) }),
2532 )
2533 .argument(InputValue::new("limit", TypeRef::named("Int")))
2534 .argument(InputValue::new("offset", TypeRef::named("Int"))),
2535 );
2536 let mutation = Object::new("mutation_root").field(
2537 Field::new("insert_test_types", TypeRef::named("test_types"), |_| {
2538 FieldFuture::new(async move { Ok(None::<async_graphql::Value>) })
2539 })
2540 .argument(InputValue::new(
2541 "objects",
2542 TypeRef::named_nn_list_nn("test_types_insert_input"),
2543 )),
2544 );
2545 Schema::build("query_root", Some("mutation_root"), None)
2546 .register(insert)
2547 .register(row)
2548 .register(query)
2549 .register(mutation)
2550 .finish()
2551 .expect("the schema builds")
2552 }
2553
2554 fn rewritten(query: &str) -> String {
2555 let schema = schema();
2556 let request = prepare(Some(&schema), async_graphql::Request::new(query), None)
2557 .unwrap_or_else(|(request, _)| request);
2558 let mut request = request;
2559 format!("{:?}", request.parsed_query().expect("the query parses"))
2560 }
2561
2562 #[test]
2563 fn an_offset_written_as_a_string_becomes_a_number() {
2564 let printed = rewritten("{ test_types(offset: \"1\") { c1_smallint } }");
2565 assert!(printed.contains("Number(1)"), "{}", printed);
2566 }
2567
2568 #[test]
2571 fn a_limit_written_as_a_string_is_left_alone() {
2572 let printed = rewritten("{ test_types(limit: \"3\") { c1_smallint } }");
2573 assert!(printed.contains("String(\"3\")"), "{}", printed);
2574 }
2575
2576 #[test]
2577 fn a_column_written_as_a_string_becomes_what_the_column_is() {
2578 let printed = rewritten(
2579 "mutation { insert_test_types(objects: [{ \
2580 c1_smallint: \"32767\", c6_real: \"0.5\", c20_boolean: \"true\" }]) \
2581 { c1_smallint } }",
2582 );
2583 assert!(printed.contains("Number(32767)"), "{}", printed);
2584 assert!(printed.contains("Number(0.5)"), "{}", printed);
2585 assert!(printed.contains("Boolean(true)"), "{}", printed);
2586 }
2587
2588 #[test]
2591 fn a_string_written_where_a_string_belongs_stays_one() {
2592 let printed = rewritten(
2593 "mutation { insert_test_types(objects: [{ c13_text: \"32767\" }]) { c1_smallint } }",
2594 );
2595 assert!(printed.contains("String(\"32767\")"), "{}", printed);
2596 }
2597}