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 "invalid-jwt" => "invalid-jwt",
60 "jwt-missing-role-claims" => "jwt-missing-role-claims",
62 "not-found" => "not-found",
66 "permission-error" => "permission-error",
67 "constraint-violation" => "constraint-violation",
68 "data-exception" => "data-exception",
69 "not-supported" => "not-supported",
70 "parse-failed" => "parse-failed",
71 _ => "unexpected",
72 };
73 }
74 }
75
76 let message = error.message.to_ascii_lowercase();
77 if message.contains("invalid value for argument")
81 || message.contains("does not contain the value")
82 || message.contains("is not defined by operation")
83 || message.contains("not configured for mutations")
88 || message.contains("not configured for subscriptions")
89 {
90 "validation-failed"
91 } else if message.contains("permission") || message.contains("not allowed") {
92 "permission-error"
93 } else if message.contains("violates") || message.contains("constraint") {
94 "constraint-violation"
95 } else if message.contains("unknown argument")
96 || message.contains("cannot query field")
97 || message.contains("expected")
98 || message.contains("unknown field")
99 || message.starts_with("variable \"")
103 {
104 "validation-failed"
105 } else {
106 "postgres-error"
107 }
108}
109
110fn value_of(value: &async_graphql::Value) -> Value {
111 serde_json::to_value(value).unwrap_or(Value::Null)
112}
113
114fn path_for(error: &ServerError) -> String {
120 use async_graphql::PathSegment;
121
122 if let Some(Value::String(path)) = error
127 .extensions
128 .as_ref()
129 .and_then(|extensions| extensions.get("path"))
130 .map(value_of)
131 {
132 return path;
133 }
134
135 if error.path.is_empty() {
136 return "$".to_string();
137 }
138 let mut rendered = String::from("$");
148 for segment in &error.path {
149 if let PathSegment::Field(name) = segment {
150 rendered.push_str(".selectionSet.");
151 rendered.push_str(name);
152 }
153 }
154 rendered
155}
156
157pub fn envelope(response: Response) -> Value {
159 if !response.errors.is_empty() {
160 let errors: Vec<Value> = response
161 .errors
162 .iter()
163 .map(|error| {
164 json!({
165 "message": error.message,
166 "extensions": {
167 "path": path_for(error),
168 "code": code_for(error),
169 }
170 })
171 })
172 .collect();
173 return json!({ "errors": errors });
175 }
176
177 let mut body = Map::new();
178 body.insert("data".to_string(), value_of(&response.data));
179 Value::Object(body)
180}
181
182#[derive(Debug, Clone, Copy, PartialEq, Eq)]
188pub enum Endpoint {
189 Current,
191 Legacy,
193}
194
195pub fn status_for(body: &Value, endpoint: Endpoint) -> u16 {
201 if endpoint == Endpoint::Current {
202 return 200;
203 }
204 let refused = body
205 .get("errors")
206 .and_then(Value::as_array)
207 .is_some_and(|errors| {
208 errors.iter().any(|error| {
209 error.get("message").and_then(Value::as_str) == Some(crate::role::CHECK_FAILED)
210 })
211 });
212 match refused {
213 true => 400,
214 false => 200,
215 }
216}
217
218pub fn denied(message: &str) -> Value {
225 refused(message, "access-denied")
226}
227
228#[doc(hidden)]
238pub fn invalid_jwt(reason: &str) -> Value {
239 refused(&format!("Could not verify JWT: {reason}"), "invalid-jwt")
240}
241
242#[doc(hidden)]
250pub fn missing_role_claim(claim: &str) -> Value {
251 refused(
252 &format!("JWT claim does not contain {claim}"),
253 "jwt-missing-role-claims",
254 )
255}
256
257fn refused(message: &str, code: &str) -> Value {
258 let mut error = ServerError::new(message, None);
259 let mut extensions = async_graphql::ErrorExtensionValues::default();
260 extensions.set("code", code);
261 error.extensions = Some(extensions);
262
263 let mut response = Response::new(async_graphql::Value::Null);
264 response.errors = vec![error];
265 envelope(response)
266}
267
268pub fn persisted_query(request: &async_graphql::Request) -> bool {
277 request.extensions.contains_key("persistedQuery")
278}
279
280pub fn not_supported(message: &str) -> Value {
282 let mut error = ServerError::new(message, None);
283 let mut extensions = async_graphql::ErrorExtensionValues::default();
284 extensions.set("code", "not-supported");
285 extensions.set("path", "$");
286 error.extensions = Some(extensions);
287
288 let mut response = Response::new(async_graphql::Value::Null);
289 response.errors = vec![error];
290 envelope(response)
291}
292
293pub fn malformed(message: &str) -> Value {
299 let mut error = ServerError::new(message, None);
300 let mut extensions = async_graphql::ErrorExtensionValues::default();
301 extensions.set("code", "bad-request");
302 extensions.set("path", "$");
303 error.extensions = Some(extensions);
304
305 let mut response = Response::new(async_graphql::Value::Null);
306 response.errors = vec![error];
307 envelope(response)
308}
309
310#[cfg(test)]
311mod tests {
312 use super::*;
313 use async_graphql::{PathSegment, ServerError};
314 use pretty_assertions::assert_eq;
315
316 fn error_at(message: &str, path: Vec<PathSegment>) -> ServerError {
317 let mut error = ServerError::new(message, None);
318 error.path = path;
319 error
320 }
321
322 #[test]
323 fn only_a_refused_write_is_answered_with_a_failure_status() {
324 let refused = json!({"errors": [{"message": crate::role::CHECK_FAILED,
327 "extensions": {"code": "permission-error"}}]});
328 assert_eq!(status_for(&refused, Endpoint::Legacy), 400);
329 assert_eq!(status_for(&refused, Endpoint::Current), 200);
331
332 let denied = json!({"errors": [{"message": "not allowed",
334 "extensions": {"code": "permission-error"}}]});
335 assert_eq!(status_for(&denied, Endpoint::Legacy), 200);
336 assert_eq!(
337 status_for(&json!({"data": {"article": []}}), Endpoint::Legacy),
338 200
339 );
340 }
341
342 #[test]
343 fn a_role_may_be_refused_the_schema_without_being_refused_its_rows() {
344 let asked = |query: &str| {
345 let doc = async_graphql::parser::parse_query(query).unwrap();
346 introspection_refusal(&doc, "reader").map(|e| e.message)
347 };
348 let refusal = Some("introspection is disabled for role: \"reader\"".to_string());
349 assert_eq!(asked("{ __schema { types { name } } }"), refusal);
350 assert_eq!(asked("{ __type(name: \"article\") { name } }"), refusal);
351 assert_eq!(asked("{ article { __typename id } }"), None);
354 assert_eq!(asked("{ article { id } }"), None);
355 }
356
357 #[test]
358 fn a_failed_response_has_no_data_key() {
359 let mut response = Response::new(async_graphql::Value::Null);
360 response.errors = vec![error_at("no", vec![])];
361 let body = envelope(response);
362 assert!(body.get("data").is_none());
363 assert!(body.get("errors").is_some());
364 }
365
366 #[test]
367 fn a_successful_response_has_no_errors_key() {
368 let body = envelope(Response::new(async_graphql::Value::Null));
369 assert!(body.get("errors").is_none());
370 assert!(body.get("data").is_some());
371 }
372
373 #[test]
374 fn path_is_rooted_at_the_selection_set() {
375 let error = error_at(
376 "boom",
377 vec![
378 PathSegment::Field("insert_author".to_string()),
379 PathSegment::Index(0),
380 PathSegment::Field("name".to_string()),
381 ],
382 );
383 assert_eq!(
384 path_for(&error),
385 "$.selectionSet.insert_author.selectionSet.name"
386 );
387 }
388
389 #[test]
396 fn a_persisted_query_is_refused_as_unsupported() {
397 let plain = async_graphql::Request::new("{ author { id } }");
398 assert!(!persisted_query(&plain));
399
400 let mut carried = async_graphql::Request::new("{ author { id } }");
401 carried.extensions.0.insert(
402 "persistedQuery".to_string(),
403 async_graphql::Value::Object(Default::default()),
404 );
405 assert!(persisted_query(&carried));
406
407 assert_eq!(
408 not_supported("PersistedQueryNotSupported"),
409 json!({"errors": [{
410 "message": "PersistedQueryNotSupported",
411 "extensions": {"path": "$", "code": "not-supported"}
412 }]})
413 );
414 }
415
416 #[test]
417 fn a_path_given_with_the_error_is_the_one_answered() {
418 let mut error = error_at(
419 "boom",
420 vec![PathSegment::Field("insert_author".to_string())],
421 );
422 let mut extensions = async_graphql::ErrorExtensionValues::default();
423 extensions.set("code", "permission-error");
424 extensions.set("path", "$.selectionSet.insert_author.args.objects");
425 error.extensions = Some(extensions);
426 assert_eq!(
427 path_for(&error),
428 "$.selectionSet.insert_author.args.objects"
429 );
430 }
431
432 #[test]
433 fn an_error_with_no_path_is_rooted_at_the_document() {
434 assert_eq!(path_for(&error_at("boom", vec![])), "$");
435 }
436
437 #[test]
438 fn a_validation_message_is_coded_as_one() {
439 let error = ServerError::new("Unknown argument \"filter\" on field \"author\"", None);
440 assert_eq!(code_for(&error), "validation-failed");
441 }
442
443 #[test]
444 fn a_constraint_message_is_coded_as_one() {
445 let error = ServerError::new("duplicate key value violates unique constraint", None);
446 assert_eq!(code_for(&error), "constraint-violation");
447 }
448}
449
450pub fn allow_unused_variables(request: async_graphql::Request) -> async_graphql::Request {
466 prepare(None, request, None).unwrap_or_else(|(request, _)| request)
467}
468
469#[allow(clippy::result_large_err)]
488pub fn prepare(
489 schema: Option<&async_graphql::dynamic::Schema>,
490 request: async_graphql::Request,
491 introspection_disabled_for: Option<&str>,
496) -> Result<async_graphql::Request, (async_graphql::Request, Vec<ServerError>)> {
497 let mut request = rewrite_document(schema, request);
498 let doc = match async_graphql::parser::parse_query(&request.query) {
509 Ok(doc) => doc,
510 Err(_) => {
511 return Err((
512 request,
513 vec![coded(
514 "not a valid graphql query".to_string(),
515 async_graphql::Pos::default(),
516 "$.query",
517 )],
518 ))
519 }
520 };
521 if let Some(schema) = schema {
522 if let Some(role) = introspection_disabled_for {
523 if let Some(error) = introspection_refusal(&doc, role) {
524 return Err((request, vec![error]));
525 }
526 }
527 let mut errors = variable_errors(&doc, schema.registry(), &request.variables);
528 errors.truncate(1);
541 if !errors.is_empty() {
542 return Err((request, errors));
543 }
544 }
545 let _ = &mut request;
546 Ok(request)
547}
548
549fn introspection_refusal(
558 doc: &async_graphql::parser::types::ExecutableDocument,
559 role: &str,
560) -> Option<ServerError> {
561 use async_graphql::parser::types::{OperationType, Selection};
562
563 let operations: Vec<_> = match &doc.operations {
564 async_graphql::parser::types::DocumentOperations::Single(one) => vec![one],
565 async_graphql::parser::types::DocumentOperations::Multiple(many) => many.values().collect(),
566 };
567
568 let asks = operations.iter().any(|operation| {
569 operation.node.ty == OperationType::Query
570 && operation
571 .node
572 .selection_set
573 .node
574 .items
575 .iter()
576 .any(|item| match &item.node {
577 Selection::Field(field) => {
578 matches!(field.node.name.node.as_str(), "__schema" | "__type")
579 }
580 _ => false,
581 })
582 });
583
584 match asks {
585 false => None,
586 true => {
587 let mut error = ServerError::new(
588 format!("introspection is disabled for role: \"{}\"", role),
589 None,
590 );
591 let mut extensions = async_graphql::ErrorExtensionValues::default();
592 extensions.set("code", "not-supported");
593 error.extensions = Some(extensions);
594 Some(error)
595 }
596 }
597}
598
599fn rewrite_document(
608 schema: Option<&async_graphql::dynamic::Schema>,
609 mut request: async_graphql::Request,
610) -> async_graphql::Request {
611 use async_graphql::parser::types::{
612 DocumentOperations, ExecutableDocument, Selection, SelectionSet,
613 };
614 use async_graphql::Name;
615 use async_graphql_value::Value;
618
619 let Ok(mut doc): Result<ExecutableDocument, _> =
620 async_graphql::parser::parse_query(&request.query)
621 else {
622 return request;
623 };
624
625 fn from_value(value: &Value, used: &mut std::collections::HashSet<Name>) {
626 match value {
627 Value::Variable(name) => {
628 used.insert(name.clone());
629 }
630 Value::List(items) => items.iter().for_each(|item| from_value(item, used)),
631 Value::Object(fields) => fields.values().for_each(|field| from_value(field, used)),
632 _ => {}
633 }
634 }
635
636 fn from_selection_set(set: &SelectionSet, used: &mut std::collections::HashSet<Name>) {
637 for selection in &set.items {
638 let (directives, arguments, nested) = match &selection.node {
639 Selection::Field(field) => (
640 &field.node.directives,
641 Some(&field.node.arguments),
642 Some(&field.node.selection_set),
643 ),
644 Selection::FragmentSpread(spread) => (&spread.node.directives, None, None),
645 Selection::InlineFragment(fragment) => (
646 &fragment.node.directives,
647 None,
648 Some(&fragment.node.selection_set),
649 ),
650 };
651 for directive in directives {
652 for (_, value) in &directive.node.arguments {
653 from_value(&value.node, used);
654 }
655 }
656 for (_, value) in arguments.into_iter().flatten() {
657 from_value(&value.node, used);
658 }
659 if let Some(nested) = nested {
660 from_selection_set(&nested.node, used);
661 }
662 }
663 }
664
665 let mut used = std::collections::HashSet::new();
670 for (_, operation) in doc.operations.iter() {
671 for directive in &operation.node.directives {
672 for (_, value) in &directive.node.arguments {
673 from_value(&value.node, &mut used);
674 }
675 }
676 from_selection_set(&operation.node.selection_set.node, &mut used);
677 }
678 for fragment in doc.fragments.values() {
679 from_selection_set(&fragment.node.selection_set.node, &mut used);
680 }
681
682 let mut edited = false;
683 {
684 let mut prune = |operation: &mut async_graphql::Positioned<
685 async_graphql::parser::types::OperationDefinition,
686 >| {
687 let before = operation.node.variable_definitions.len();
688 operation
689 .node
690 .variable_definitions
691 .retain(|definition| used.contains(&definition.node.name.node));
692 edited |= operation.node.variable_definitions.len() != before;
693 };
694 match &mut doc.operations {
695 DocumentOperations::Single(operation) => prune(operation),
696 DocumentOperations::Multiple(operations) => operations.values_mut().for_each(prune),
697 }
698 }
699
700 if let Some(schema) = schema {
711 let registry = schema.registry();
712 {
713 let mut coerce = |operation: &mut async_graphql::Positioned<
714 async_graphql::parser::types::OperationDefinition,
715 >| {
716 use async_graphql::parser::types::OperationType;
717 let root = match operation.node.ty {
718 OperationType::Query => Some(registry.query_type.as_str()),
719 OperationType::Mutation => registry.mutation_type.as_deref(),
720 OperationType::Subscription => registry.subscription_type.as_deref(),
721 };
722 if let Some(root) = root {
723 edited |= coerce_selection_set(
724 registry,
725 &mut operation.node.selection_set.node,
726 root,
727 );
728 }
729 };
730 match &mut doc.operations {
731 DocumentOperations::Single(operation) => coerce(operation),
732 DocumentOperations::Multiple(operations) => {
733 operations.values_mut().for_each(coerce)
734 }
735 }
736 }
737 for fragment in doc.fragments.values_mut() {
741 let on = fragment.node.type_condition.node.on.node.to_string();
742 edited |= coerce_selection_set(registry, &mut fragment.node.selection_set.node, &on);
743 }
744 }
745
746 if edited {
747 request.set_parsed_query(doc);
748 }
749 request
750}
751
752fn coerce_selection_set(
759 registry: &async_graphql::registry::Registry,
760 set: &mut async_graphql::parser::types::SelectionSet,
761 type_name: &str,
762) -> bool {
763 use async_graphql::parser::types::Selection;
764 use async_graphql::registry::MetaTypeName;
765
766 let mut edited = false;
767 for selection in &mut set.items {
768 match &mut selection.node {
769 Selection::Field(field) => {
770 let meta = registry
771 .types
772 .get(type_name)
773 .and_then(|ty| ty.field_by_name(field.node.name.node.as_str()));
774 for (name, value) in &mut field.node.arguments {
775 if name.node.as_str() == "limit" {
778 continue;
779 }
780 let Some(argument) = meta.and_then(|meta| meta.args.get(name.node.as_str()))
781 else {
782 continue;
783 };
784 let ty = argument.ty.clone();
785 edited |= coerce_value(registry, &mut value.node, &ty);
786 }
787 if let Some(meta) = meta {
788 let inner = MetaTypeName::concrete_typename(&meta.ty).to_string();
789 edited |=
790 coerce_selection_set(registry, &mut field.node.selection_set.node, &inner);
791 }
792 }
793 Selection::InlineFragment(fragment) => {
794 let inner = fragment
795 .node
796 .type_condition
797 .as_ref()
798 .map(|condition| condition.node.on.node.to_string())
799 .unwrap_or_else(|| type_name.to_string());
800 edited |=
801 coerce_selection_set(registry, &mut fragment.node.selection_set.node, &inner);
802 }
803 Selection::FragmentSpread(_) => {}
804 }
805 }
806 edited
807}
808
809fn coerce_value(
811 registry: &async_graphql::registry::Registry,
812 value: &mut async_graphql_value::Value,
813 expected: &str,
814) -> bool {
815 use async_graphql::registry::{MetaType, MetaTypeName};
816 use async_graphql_value::Value;
817
818 match value {
819 Value::List(items) => {
820 let inner = match MetaTypeName::create(expected).unwrap_non_null() {
823 MetaTypeName::List(inner) => inner.to_string(),
824 _ => expected.to_string(),
825 };
826 let mut edited = false;
827 for item in items {
828 edited |= coerce_value(registry, item, &inner);
829 }
830 edited
831 }
832 Value::Object(fields) => {
833 let name = MetaTypeName::concrete_typename(expected);
834 let Some(MetaType::InputObject { input_fields, .. }) = registry.types.get(name) else {
835 return false;
836 };
837 let types: Vec<(async_graphql::Name, String)> = fields
838 .keys()
839 .filter_map(|key| {
840 input_fields
841 .get(key.as_str())
842 .map(|field| (key.clone(), field.ty.clone()))
843 })
844 .collect();
845 let mut edited = false;
846 for (key, ty) in types {
847 if let Some(item) = fields.get_mut(&key) {
848 edited |= coerce_value(registry, item, &ty);
849 }
850 }
851 edited
852 }
853 Value::String(text) => {
854 let Some(coerced) = as_written(text, MetaTypeName::concrete_typename(expected)) else {
855 return false;
856 };
857 *value = coerced;
858 true
859 }
860 _ => false,
861 }
862}
863
864fn as_written(text: &str, expected: &str) -> Option<async_graphql_value::Value> {
869 use async_graphql_value::{Number, Value};
870 match expected {
871 "Int" => text.parse::<i64>().ok().map(|n| Value::Number(n.into())),
872 "Float" => text
873 .parse::<f64>()
874 .ok()
875 .and_then(Number::from_f64)
876 .map(Value::Number),
877 "Boolean" => match text {
878 "true" => Some(Value::Boolean(true)),
879 "false" => Some(Value::Boolean(false)),
880 _ => None,
881 },
882 _ => None,
883 }
884}
885
886#[cfg(test)]
887mod unused_variable_tests {
888 use super::*;
889
890 fn declarations(query: &str) -> Vec<String> {
891 let request = allow_unused_variables(async_graphql::Request::new(query));
892 let mut request = request;
895 let doc = request.parsed_query().expect("the query parses");
896 doc.operations
897 .iter()
898 .flat_map(|(_, operation)| operation.node.variable_definitions.iter())
899 .map(|definition| definition.node.name.node.to_string())
900 .collect()
901 }
902
903 #[test]
904 fn a_variable_nothing_names_is_dropped() {
905 assert_eq!(
906 declarations("query ($tags: jsonb) { article(where: {id: {_eq: 1}}) { id } }"),
907 Vec::<String>::new()
908 );
909 }
910
911 #[test]
912 fn a_variable_an_argument_names_is_kept() {
913 assert_eq!(
914 declarations("query ($id: Int) { article(where: {id: {_eq: $id}}) { id } }"),
915 vec!["id".to_string()]
916 );
917 }
918
919 #[test]
920 fn a_variable_named_inside_a_list_is_kept() {
921 assert_eq!(
922 declarations("query ($id: Int) { article(where: {_or: [{id: {_eq: $id}}]}) { id } }"),
923 vec!["id".to_string()]
924 );
925 }
926
927 #[test]
928 fn a_variable_only_a_fragment_names_is_kept() {
929 assert_eq!(
930 declarations(
931 "query ($n: Int) { author { ...rows } } \
932 fragment rows on author { articles(limit: $n) { id } }"
933 ),
934 vec!["n".to_string()]
935 );
936 }
937
938 #[test]
939 fn a_document_that_does_not_parse_is_left_alone() {
940 let request = allow_unused_variables(async_graphql::Request::new("query ("));
941 assert_eq!(request.query, "query (");
942 }
943}
944
945fn variable_errors(
959 doc: &async_graphql::parser::types::ExecutableDocument,
960 registry: &async_graphql::registry::Registry,
961 variables: &async_graphql::Variables,
962) -> Vec<ServerError> {
963 use async_graphql::parser::types::{DocumentOperations, OperationType};
964
965 let mut errors: Vec<ServerError> = Vec::new();
966
967 let operations: Vec<
968 &async_graphql::Positioned<async_graphql::parser::types::OperationDefinition>,
969 > = match &doc.operations {
970 DocumentOperations::Single(operation) => vec![operation],
971 DocumentOperations::Multiple(operations) => operations.values().collect(),
972 };
973
974 for operation in operations {
975 let root = match operation.node.ty {
976 OperationType::Query => Some(registry.query_type.as_str()),
977 OperationType::Mutation => registry.mutation_type.as_deref(),
978 OperationType::Subscription => registry.subscription_type.as_deref(),
979 };
980 let Some(root) = root else {
981 if operation.node.ty == OperationType::Mutation {
985 errors.push(coded("no mutations exist".to_string(), operation.pos, "$"));
986 }
987 continue;
988 };
989
990 let mut declared: HashMap<&str, String> = HashMap::new();
992 for definition in &operation.node.variable_definitions {
993 let name = definition.node.name.node.as_str();
994 let written = definition.node.var_type.node.to_string();
995 let defaulted = definition
1001 .node
1002 .default_value
1003 .as_ref()
1004 .is_some_and(|value| !matches!(value.node, async_graphql::Value::Null));
1005 let effective = match (definition.node.var_type.node.nullable, defaulted) {
1006 (true, true) => format!("{}!", written),
1007 _ => written.clone(),
1008 };
1009 declared.insert(name, effective);
1010
1011 let given = variables.get(&async_graphql::Name::new(name));
1015 if !definition.node.var_type.node.nullable
1016 && matches!(given, Some(async_graphql::Value::Null))
1017 {
1018 errors.push(coded(
1019 format!("null value found for non-nullable type: \"{}\"", written),
1020 definition.pos,
1021 "$",
1022 ));
1023 }
1024 }
1025 let mut scope = Usage {
1029 registry,
1030 fragments: &doc.fragments,
1031 declared: &declared,
1032 variables,
1033 errors: &mut errors,
1034 spreading: Vec::new(),
1035 expansions: 0,
1036 };
1037 scope.selection_set(&operation.node.selection_set.node, root, "$");
1038 }
1039
1040 errors
1041}
1042
1043struct Usage<'a> {
1045 registry: &'a async_graphql::registry::Registry,
1046 fragments: &'a std::collections::HashMap<
1047 async_graphql::Name,
1048 async_graphql::Positioned<async_graphql::parser::types::FragmentDefinition>,
1049 >,
1050 declared: &'a HashMap<&'a str, String>,
1051 variables: &'a async_graphql::Variables,
1054 errors: &'a mut Vec<ServerError>,
1055 spreading: Vec<String>,
1064 expansions: usize,
1068}
1069
1070impl Usage<'_> {
1071 fn selection_set(
1076 &mut self,
1077 set: &async_graphql::parser::types::SelectionSet,
1078 type_name: &str,
1079 path: &str,
1080 ) {
1081 use async_graphql::parser::types::Selection;
1082 use async_graphql::registry::MetaTypeName;
1083
1084 for selection in &set.items {
1085 match &selection.node {
1086 Selection::Field(field) => {
1087 let named = field.node.name.node.as_str();
1088 let parent = self.registry.types.get(type_name);
1089 let meta = parent.and_then(|ty| ty.field_by_name(named));
1090 let here = format!("{}.selectionSet.{}", path, field.node.name.node);
1091 if meta.is_none() && parent.is_some() && !named.starts_with("__") {
1100 self.errors.push(coded(
1101 format!("field '{}' not found in type: '{}'", named, type_name),
1102 field.pos,
1103 &here,
1104 ));
1105 continue;
1106 }
1107 for (name, value) in &field.node.arguments {
1108 let Some(argument) =
1109 meta.and_then(|meta| meta.args.get(name.node.as_str()))
1110 else {
1111 if meta.is_some() {
1116 self.errors.push(coded(
1117 format!("'{}' has no argument named '{}'", named, name.node),
1118 name.pos,
1119 &here,
1120 ));
1121 }
1122 continue;
1123 };
1124 let expected = relax(&argument.ty, argument.default_value.is_some());
1129 let at = format!("{}.args.{}", here, name.node);
1130 self.rows_asked_for(
1131 name.node.as_str(),
1132 &value.node,
1133 &expected,
1134 value.pos,
1135 &at,
1136 );
1137 self.object_expected(&value.node, &expected, value.pos, &at);
1138 self.value(&value.node, &expected, value.pos, &at);
1139 }
1140 self.one_operator_per_column(&field.node.arguments, &here);
1141 self.directives(&field.node.directives, &here);
1142 if let Some(meta) = meta {
1143 let inner = MetaTypeName::concrete_typename(&meta.ty).to_string();
1144 self.selection_set(&field.node.selection_set.node, &inner, &here);
1145 }
1146 }
1147 Selection::InlineFragment(fragment) => {
1148 let inner = fragment
1149 .node
1150 .type_condition
1151 .as_ref()
1152 .map(|condition| condition.node.on.node.to_string())
1153 .unwrap_or_else(|| type_name.to_string());
1154 self.directives(&fragment.node.directives, path);
1155 self.selection_set(&fragment.node.selection_set.node, &inner, path);
1156 }
1157 Selection::FragmentSpread(spread) => {
1158 let name = spread.node.fragment_name.node.to_string();
1159 self.directives(&spread.node.directives, path);
1160 if let Some(began) = self.spreading.iter().position(|held| held == &name) {
1167 let cycle: Vec<&str> =
1168 self.spreading[began..].iter().map(String::as_str).collect();
1169 self.errors.push(coded(
1170 format!("the fragment definition(s) {} form a cycle", listed(&cycle)),
1171 spread.pos,
1172 &format!("{}.selectionSet", path),
1176 ));
1177 continue;
1178 }
1179 self.expansions += 1;
1180 if self.expansions > SPREAD_BUDGET {
1181 continue;
1182 }
1183 if let Some(fragment) = self.fragments.get(&async_graphql::Name::new(&name)) {
1184 let on = fragment.node.type_condition.node.on.node.to_string();
1185 let here = format!("{}.selectionSet.{}", path, name);
1189 self.spreading.push(name);
1190 self.selection_set(&fragment.node.selection_set.node, &on, &here);
1191 self.spreading.pop();
1192 }
1193 }
1194 }
1195 }
1196 }
1197
1198 fn directives(
1199 &mut self,
1200 directives: &[async_graphql::Positioned<async_graphql::parser::types::Directive>],
1201 path: &str,
1202 ) {
1203 for directive in directives {
1204 let meta = self
1205 .registry
1206 .directives
1207 .get(directive.node.name.node.as_str());
1208 for (name, value) in &directive.node.arguments {
1209 let Some(argument) = meta.and_then(|meta| meta.args.get(name.node.as_str())) else {
1210 continue;
1211 };
1212 let expected = relax(&argument.ty, argument.default_value.is_some());
1213 let at = format!("{}.args.{}", path, name.node);
1214 self.value(&value.node, &expected, value.pos, &at);
1215 }
1216 }
1217 }
1218
1219 fn stands_for_null(&self, value: &async_graphql_value::Value) -> bool {
1226 use async_graphql_value::Value;
1227 match value {
1228 Value::Null => true,
1229 Value::Variable(name) => matches!(
1230 self.variables.get(&async_graphql::Name::new(name)),
1231 Some(async_graphql::Value::Null)
1232 ),
1233 _ => false,
1234 }
1235 }
1236
1237 fn value(
1239 &mut self,
1240 value: &async_graphql_value::Value,
1241 expected: &str,
1242 pos: async_graphql::Pos,
1243 path: &str,
1244 ) {
1245 use async_graphql::registry::{MetaType, MetaTypeName};
1246 use async_graphql_value::Value;
1247
1248 match value {
1249 Value::Variable(name) => {
1250 let Some(declared) = self.declared.get(name.as_str()) else {
1251 return;
1253 };
1254 if !MetaTypeName::create(expected).is_subtype(&MetaTypeName::create(declared)) {
1255 self.errors.push(coded(
1256 format!(
1257 "variable '{}' is declared as '{}', but used where '{}' is expected",
1258 name, declared, expected
1259 ),
1260 pos,
1261 path,
1262 ));
1263 return;
1264 }
1265 if let Some(given) = self.variables.get(&async_graphql::Name::new(name)) {
1270 let given: async_graphql_value::Value = given.clone().into();
1271 self.enumerated(&given, expected, pos, path, true);
1272 self.label_path(&given, expected, pos, path);
1273 self.raster_hex(&given, expected, pos, path);
1274 }
1275 }
1276 Value::Enum(_) | Value::String(_) => {
1277 self.enumerated(value, expected, pos, path, false);
1278 self.label_path(value, expected, pos, path);
1279 self.raster_hex(value, expected, pos, path);
1280 }
1281 Value::Number(_) => self.integral(value, expected, pos, path),
1282 Value::List(items) => {
1283 let inner = match MetaTypeName::create(expected).unwrap_non_null() {
1287 MetaTypeName::List(inner) => inner.to_string(),
1288 _ => expected.to_string(),
1289 };
1290 for (index, item) in items.iter().enumerate() {
1291 self.value(item, &inner, pos, &format!("{}[{}]", path, index));
1292 }
1293 }
1294 Value::Object(fields) => {
1295 let name = MetaTypeName::concrete_typename(expected);
1296 let Some(MetaType::InputObject { input_fields, .. }) =
1297 self.registry.types.get(name)
1298 else {
1299 return;
1300 };
1301 let comparison = name.ends_with("_comparison_exp");
1309 let base = match MetaTypeName::create(expected).unwrap_non_null() {
1314 MetaTypeName::List(_) => format!("{}[0]", path),
1315 _ => path.to_string(),
1316 };
1317 for (key, item) in fields {
1318 let at = format!("{}.{}", base, key.as_str());
1319 let Some(field) = input_fields.get(key.as_str()) else {
1320 self.errors.push(coded(
1325 format!("field '{}' not found in type: '{}'", key.as_str(), name),
1326 pos,
1327 &at,
1328 ));
1329 continue;
1330 };
1331 if comparison && self.stands_for_null(item) {
1332 self.errors.push(coded(
1333 format!(
1334 "unexpected null value for type '{}'",
1335 MetaTypeName::concrete_typename(&field.ty)
1336 ),
1337 pos,
1338 &at,
1339 ));
1340 continue;
1341 }
1342 let ty = relax(&field.ty, field.default_value.is_some());
1343 self.value(item, &ty, pos, &at);
1344 }
1345 }
1346 _ => {}
1347 }
1348 }
1349}
1350
1351impl Usage<'_> {
1352 fn object_expected(
1364 &mut self,
1365 value: &async_graphql_value::Value,
1366 expected: &str,
1367 pos: async_graphql::Pos,
1368 path: &str,
1369 ) {
1370 use async_graphql::registry::{MetaType, MetaTypeName};
1371
1372 if !matches!(MetaTypeName::create(expected), MetaTypeName::NonNull(_)) {
1373 return;
1374 }
1375 if !self.stands_for_null(value) {
1376 return;
1377 }
1378 let name = MetaTypeName::concrete_typename(expected);
1379 if !matches!(
1380 self.registry.types.get(name),
1381 Some(MetaType::InputObject { .. })
1382 ) {
1383 return;
1384 }
1385 self.errors.push(coded(
1386 format!("expected an object for type '{}', but found null", name),
1387 pos,
1388 path,
1389 ));
1390 }
1391
1392 fn one_operator_per_column(
1405 &mut self,
1406 arguments: &[(
1407 async_graphql::Positioned<async_graphql::Name>,
1408 async_graphql::Positioned<async_graphql_value::Value>,
1409 )],
1410 path: &str,
1411 ) {
1412 use async_graphql_value::Value;
1413
1414 const OPERATORS: [&str; 7] = [
1415 "_set",
1416 "_inc",
1417 "_append",
1418 "_prepend",
1419 "_delete_key",
1420 "_delete_elem",
1421 "_delete_at_path",
1422 ];
1423
1424 let mut seen: Vec<&str> = Vec::new();
1425 let mut twice: Vec<&str> = Vec::new();
1426 for (name, value) in arguments {
1427 if !OPERATORS.contains(&name.node.as_str()) {
1428 continue;
1429 }
1430 let Value::Object(columns) = &value.node else {
1431 continue;
1432 };
1433 for column in columns.keys() {
1434 let column = column.as_str();
1435 match seen.contains(&column) {
1436 true if !twice.contains(&column) => twice.push(column),
1437 true => {}
1438 false => seen.push(column),
1439 }
1440 }
1441 }
1442 if twice.is_empty() {
1443 return;
1444 }
1445 let listed = twice
1448 .iter()
1449 .map(|column| format!("'{}'", column))
1450 .collect::<Vec<String>>()
1451 .join(", ");
1452 self.errors.push(coded(
1453 format!("Column found in multiple operators: [{}].", listed),
1454 arguments
1455 .first()
1456 .map(|(name, _)| name.pos)
1457 .unwrap_or_default(),
1458 &format!("{}.args", path),
1459 ));
1460 }
1461
1462 fn integral(
1470 &mut self,
1471 value: &async_graphql_value::Value,
1472 expected: &str,
1473 pos: async_graphql::Pos,
1474 path: &str,
1475 ) {
1476 use async_graphql::registry::MetaTypeName;
1477 use async_graphql_value::Value;
1478
1479 if MetaTypeName::concrete_typename(expected) != "Int" {
1480 return;
1481 }
1482 let Value::Number(number) = value else {
1483 return;
1484 };
1485 if number.as_i64().is_some_and(|n| i32::try_from(n).is_ok()) {
1486 return;
1487 }
1488 let Some(shown) = number.as_f64().map(shown_as_haskell_would) else {
1489 return;
1490 };
1491 self.errors.push(coded_as(
1492 "parse-failed",
1493 format!(
1494 "The value {} lies outside the bounds or is not an integer. \
1495 Maybe it is a float, or is there integer overflow?",
1496 shown
1497 ),
1498 pos,
1499 path,
1500 ));
1501 }
1502
1503 fn rows_asked_for(
1517 &mut self,
1518 argument: &str,
1519 value: &async_graphql_value::Value,
1520 expected: &str,
1521 pos: async_graphql::Pos,
1522 path: &str,
1523 ) {
1524 use async_graphql::registry::MetaTypeName;
1525 use async_graphql_value::Value;
1526
1527 if argument != "limit" || MetaTypeName::concrete_typename(expected) != "Int" {
1528 return;
1529 }
1530 let value = match value {
1532 Value::Variable(name) => {
1533 match self.variables.get(&async_graphql::Name::new(name.as_str())) {
1534 Some(given) => std::borrow::Cow::Owned(given.clone().into()),
1535 None => return,
1536 }
1537 }
1538 other => std::borrow::Cow::Borrowed(other),
1539 };
1540
1541 let found = match value.as_ref() {
1542 Value::String(_) => "a string",
1543 Value::Number(number) => match number.as_i64() {
1544 None => return,
1548 Some(written) if i32::try_from(written).is_err() => return,
1549 Some(written) if written >= 0 => return,
1551 Some(_) => "an integer",
1552 },
1553 _ => return,
1554 };
1555 self.errors.push(coded(
1556 format!(
1557 "expected a non-negative 32-bit integer for type 'Int', but found {}",
1558 found
1559 ),
1560 pos,
1561 path,
1562 ));
1563 }
1564
1565 fn raster_hex(
1579 &mut self,
1580 value: &async_graphql_value::Value,
1581 expected: &str,
1582 pos: async_graphql::Pos,
1583 path: &str,
1584 ) {
1585 use async_graphql::registry::MetaTypeName;
1586 use async_graphql_value::Value;
1587
1588 if MetaTypeName::concrete_typename(expected) != "raster" {
1589 return;
1590 }
1591 let Value::String(written) = value else {
1592 return;
1593 };
1594 if written.len() % 2 == 0 && written.chars().all(|c| c.is_ascii_hexdigit()) {
1595 return;
1596 }
1597 self.errors.push(coded_as(
1598 "parse-failed",
1599 "invalid hexadecimal representation of raster well known binary format".to_string(),
1600 pos,
1601 path,
1602 ));
1603 }
1604
1605 fn label_path(
1621 &mut self,
1622 value: &async_graphql_value::Value,
1623 expected: &str,
1624 pos: async_graphql::Pos,
1625 path: &str,
1626 ) {
1627 use async_graphql::registry::MetaTypeName;
1628 use async_graphql_value::Value;
1629
1630 if MetaTypeName::concrete_typename(expected) != "ltree" {
1631 return;
1632 }
1633 let Value::String(written) = value else {
1634 return;
1635 };
1636 if written.is_empty() || written.split('.').all(|label| !label.is_empty()) {
1639 return;
1640 }
1641 self.errors.push(coded_as(
1642 "parse-failed",
1643 "Expecting label path: a sequence of zero or more labels separated by \
1644 dots, for example L1.L2.L3"
1645 .to_string(),
1646 pos,
1647 path,
1648 ));
1649 }
1650
1651 fn enumerated(
1657 &mut self,
1658 value: &async_graphql_value::Value,
1659 expected: &str,
1660 pos: async_graphql::Pos,
1661 path: &str,
1662 from_variable: bool,
1668 ) {
1669 use async_graphql::registry::{MetaType, MetaTypeName};
1670 use async_graphql_value::Value;
1671
1672 let name = MetaTypeName::concrete_typename(expected);
1673 let Some(MetaType::Enum { enum_values, .. }) = self.registry.types.get(name) else {
1674 return;
1675 };
1676 let written = match value {
1677 Value::Enum(written) => written.as_str(),
1678 Value::String(written) if from_variable => written.as_str(),
1679 Value::String(_) => {
1680 self.errors.push(coded(
1681 format!(
1682 "expected an enum value for type '{}', but found a string",
1683 name
1684 ),
1685 pos,
1686 path,
1687 ));
1688 return;
1689 }
1690 _ => return,
1691 };
1692 if enum_values.contains_key(written) {
1693 return;
1694 }
1695 let mut allowed: Vec<&str> = enum_values.keys().map(String::as_str).collect();
1698 allowed.sort_unstable();
1699 let listed = allowed
1700 .iter()
1701 .map(|value| format!("'{}'", value))
1702 .collect::<Vec<_>>()
1703 .join(", ");
1704 self.errors.push(coded(
1705 format!(
1706 "expected one of the values [{}] for type '{}', but found '{}'",
1707 listed, name, written
1708 ),
1709 pos,
1710 path,
1711 ));
1712 }
1713}
1714
1715const SPREAD_BUDGET: usize = 1000;
1723
1724fn listed(names: &[&str]) -> String {
1727 match names {
1728 [] => String::new(),
1729 [one] => one.to_string(),
1730 [rest @ .., last] => format!("{} and {}", rest.join(", "), last),
1731 }
1732}
1733
1734fn shown_as_haskell_would(value: f64) -> String {
1745 if !value.is_finite() {
1746 return value.to_string();
1747 }
1748 let magnitude = value.abs();
1749 let with_a_point = |rendered: String| match rendered.contains('.') {
1750 true => rendered,
1751 false => format!("{}.0", rendered),
1752 };
1753 if magnitude != 0.0 && (magnitude < 0.1 || magnitude >= 1e7) {
1754 let rendered = format!("{:e}", value);
1755 let (mantissa, exponent) = match rendered.split_once('e') {
1756 Some(halves) => halves,
1757 None => return with_a_point(rendered),
1758 };
1759 return format!("{}e{}", with_a_point(mantissa.to_string()), exponent);
1760 }
1761 with_a_point(format!("{}", value))
1762}
1763
1764fn relax(ty: &str, has_default: bool) -> String {
1770 match has_default {
1771 true => ty.strip_suffix('!').unwrap_or(ty).to_string(),
1772 false => ty.to_string(),
1773 }
1774}
1775
1776fn coded(message: String, pos: async_graphql::Pos, path: &str) -> ServerError {
1783 coded_as("validation-failed", message, pos, path)
1784}
1785
1786fn coded_as(
1788 code: &'static str,
1789 message: String,
1790 pos: async_graphql::Pos,
1791 path: &str,
1792) -> ServerError {
1793 let mut error = ServerError::new(message, Some(pos));
1794 let mut extensions = async_graphql::ErrorExtensionValues::default();
1795 extensions.set("code", code);
1796 extensions.set("path", path);
1797 error.extensions = Some(extensions);
1798 error
1799}
1800
1801#[cfg(test)]
1802mod variable_position_tests {
1803 use super::*;
1804 use async_graphql::dynamic::*;
1805
1806 fn schema() -> async_graphql::dynamic::Schema {
1810 let filter = InputObject::new("author_bool_exp")
1811 .field(InputValue::new("id", TypeRef::named("Int")))
1812 .field(InputValue::new(
1813 "name",
1814 TypeRef::named("String_comparison_exp"),
1815 ));
1816 let comparison = InputObject::new("String_comparison_exp")
1817 .field(InputValue::new("_eq", TypeRef::named("String")))
1818 .field(InputValue::new("_in", TypeRef::named_nn_list("String")));
1819 let ltree = InputObject::new("ltree_comparison_exp")
1822 .field(InputValue::new("_ancestor", TypeRef::named("ltree")))
1823 .field(InputValue::new(
1824 "_ancestor_any",
1825 TypeRef::named_list("ltree"),
1826 ));
1827 let kind = Enum::new("kind_enum").item("leaf").item("branch");
1828 let kind_comparison = InputObject::new("kind_enum_comparison_exp")
1829 .field(InputValue::new("_eq", TypeRef::named("kind_enum")));
1830 let raster_comparison = InputObject::new("raster_comparison_exp").field(InputValue::new(
1831 "_st_intersects_rast",
1832 TypeRef::named("raster"),
1833 ));
1834 let tree_filter = InputObject::new("tree_bool_exp")
1835 .field(InputValue::new(
1836 "rast",
1837 TypeRef::named("raster_comparison_exp"),
1838 ))
1839 .field(InputValue::new(
1840 "path",
1841 TypeRef::named("ltree_comparison_exp"),
1842 ))
1843 .field(InputValue::new(
1844 "kind",
1845 TypeRef::named("kind_enum_comparison_exp"),
1846 ));
1847 let insert = InputObject::new("author_insert_input")
1848 .field(InputValue::new("name", TypeRef::named("String")))
1849 .field(InputValue::new("count", TypeRef::named("Int")));
1850 let query = Object::new("query_root")
1851 .field(
1852 Field::new("author", TypeRef::named_nn_list_nn("author"), |_| {
1853 FieldFuture::new(async move { Ok(Some(async_graphql::Value::List(vec![]))) })
1854 })
1855 .argument(InputValue::new("limit", TypeRef::named("Int")))
1856 .argument(InputValue::new("where", TypeRef::named("author_bool_exp"))),
1857 )
1858 .field(
1859 Field::new("tree", TypeRef::named_nn_list_nn("tree"), |_| {
1860 FieldFuture::new(async move { Ok(Some(async_graphql::Value::List(vec![]))) })
1861 })
1862 .argument(InputValue::new("where", TypeRef::named("tree_bool_exp"))),
1863 );
1864 let mutation = Object::new("mutation_root")
1865 .field(
1866 Field::new("insert_author_one", TypeRef::named("author"), |_| {
1867 FieldFuture::new(async move { Ok(None::<async_graphql::Value>) })
1868 })
1869 .argument(InputValue::new(
1870 "object",
1871 TypeRef::named_nn("author_insert_input"),
1872 )),
1873 )
1874 .field(
1875 Field::new("insert_author", TypeRef::named("author"), |_| {
1876 FieldFuture::new(async move { Ok(None::<async_graphql::Value>) })
1877 })
1878 .argument(InputValue::new(
1879 "objects",
1880 TypeRef::named_nn_list_nn("author_insert_input"),
1881 ))
1882 .argument(
1883 InputValue::new("update_columns", TypeRef::named_nn_list_nn("String"))
1884 .default_value(async_graphql::Value::List(Vec::new())),
1885 ),
1886 )
1887 .field(
1888 Field::new(
1889 "update_author",
1890 TypeRef::named("author_mutation_response"),
1891 |_| FieldFuture::new(async move { Ok(None::<async_graphql::Value>) }),
1892 )
1893 .argument(InputValue::new(
1894 "where",
1895 TypeRef::named_nn("author_bool_exp"),
1896 ))
1897 .argument(InputValue::new("_set", TypeRef::named("author_set_input")))
1898 .argument(InputValue::new("_inc", TypeRef::named("author_inc_input"))),
1899 );
1900 let author = Object::new("author").field(Field::new("id", TypeRef::named("Int"), |_| {
1901 FieldFuture::new(async move { Ok(None::<async_graphql::Value>) })
1902 }));
1903 let tree = Object::new("tree")
1904 .field(Field::new("id", TypeRef::named("Int"), |_| {
1905 FieldFuture::new(async move { Ok(None::<async_graphql::Value>) })
1906 }))
1907 .field(Field::new("path", TypeRef::named("ltree"), |_| {
1908 FieldFuture::new(async move { Ok(None::<async_graphql::Value>) })
1909 }));
1910 let set = InputObject::new("author_set_input")
1912 .field(InputValue::new("name", TypeRef::named("String")))
1913 .field(InputValue::new("count", TypeRef::named("Int")));
1914 let inc = InputObject::new("author_inc_input")
1915 .field(InputValue::new("count", TypeRef::named("Int")));
1916 let response = Object::new("author_mutation_response").field(Field::new(
1917 "affected_rows",
1918 TypeRef::named_nn("Int"),
1919 |_| FieldFuture::new(async move { Ok(None::<async_graphql::Value>) }),
1920 ));
1921 Schema::build("query_root", Some("mutation_root"), None)
1922 .register(Scalar::new("ltree"))
1923 .register(Scalar::new("raster"))
1924 .register(raster_comparison)
1925 .register(kind)
1926 .register(kind_comparison)
1927 .register(filter)
1928 .register(comparison)
1929 .register(ltree)
1930 .register(tree_filter)
1931 .register(insert)
1932 .register(set)
1933 .register(inc)
1934 .register(response)
1935 .register(author)
1936 .register(tree)
1937 .register(query)
1938 .register(mutation)
1939 .finish()
1940 .expect("the test schema builds")
1941 }
1942
1943 fn refusals(query: &str, variables: &str) -> Vec<String> {
1944 let request =
1945 async_graphql::Request::new(query).variables(async_graphql::Variables::from_json(
1946 serde_json::from_str(variables).expect("the variables are JSON"),
1947 ));
1948 match prepare(Some(&schema()), request, None) {
1949 Ok(_) => Vec::new(),
1950 Err((_, errors)) => errors.into_iter().map(|e| e.message).collect(),
1951 }
1952 }
1953
1954 fn refusals_with_paths(query: &str, variables: &str) -> Vec<String> {
1956 let request =
1957 async_graphql::Request::new(query).variables(async_graphql::Variables::from_json(
1958 serde_json::from_str(variables).expect("the variables are JSON"),
1959 ));
1960 match prepare(Some(&schema()), request, None) {
1961 Ok(_) => Vec::new(),
1962 Err((_, errors)) => errors.iter().map(path_for).collect(),
1963 }
1964 }
1965
1966 #[test]
1969 fn a_null_written_into_a_comparison_is_refused() {
1970 assert_eq!(
1971 refusals("{ author(where: {name: {_eq: null}}) { id } }", "{}"),
1972 vec!["unexpected null value for type 'String'"]
1973 );
1974 }
1975
1976 #[test]
1977 fn a_variable_standing_for_a_null_is_refused_there_too() {
1978 assert_eq!(
1979 refusals(
1980 "query ($n: String) { author(where: {name: {_eq: $n}}) { id } }",
1981 r#"{"n": null}"#
1982 ),
1983 vec!["unexpected null value for type 'String'"]
1984 );
1985 }
1986
1987 #[test]
1992 fn a_refusal_names_the_argument_it_was_found_in() {
1993 let paths =
1994 |query: &str, variables: &str| -> Vec<String> { refusals_with_paths(query, variables) };
1995 assert_eq!(
1997 paths(
1998 "mutation ($n: Int!) { insert_author_one(object: {name: $n}) { id } }",
1999 r#"{"n": 1}"#
2000 ),
2001 vec!["$.selectionSet.insert_author_one.args.object.name"]
2002 );
2003 assert_eq!(
2005 paths("{ author(where: {name: {_eq: null}}) { id } }", "{}"),
2006 vec!["$.selectionSet.author.args.where.name._eq"]
2007 );
2008 assert_eq!(
2011 paths(
2012 "mutation ($n: Int!) { insert_author(objects: {name: $n}) { id } }",
2013 r#"{"n": 1}"#
2014 ),
2015 vec!["$.selectionSet.insert_author.args.objects[0].name"]
2016 );
2017 assert_eq!(
2019 paths(
2020 "mutation ($n: Int!) { insert_author(objects: [{name: \"a\"}, \
2021 {name: $n}]) { id } }",
2022 r#"{"n": 1}"#
2023 ),
2024 vec!["$.selectionSet.insert_author.args.objects[1].name"]
2025 );
2026 }
2027
2028 #[test]
2031 fn a_null_written_where_an_object_is_required_is_refused_as_one() {
2032 assert_eq!(
2033 refusals(
2034 "mutation { update_author(where: null, _set: {name: \"a\"}) \
2035 { affected_rows } }",
2036 "{}"
2037 ),
2038 vec!["expected an object for type 'author_bool_exp', but found null"]
2039 );
2040 assert_eq!(
2041 refusals_with_paths(
2042 "mutation { update_author(where: null, _set: {name: \"a\"}) \
2043 { affected_rows } }",
2044 "{}"
2045 ),
2046 vec!["$.selectionSet.update_author.args.where"]
2047 );
2048 assert_eq!(
2052 refusals(
2053 "mutation ($w: author_bool_exp!) { update_author(where: $w, \
2054 _set: {name: \"a\"}) { affected_rows } }",
2055 r#"{"w": null}"#
2056 ),
2057 vec!["null value found for non-nullable type: \"author_bool_exp!\""]
2058 );
2059 }
2060
2061 #[test]
2064 fn a_null_written_where_an_object_is_optional_is_left_alone() {
2065 assert_eq!(
2066 refusals("{ author(where: null) { id } }", "{}"),
2067 Vec::<String>::new()
2068 );
2069 }
2070
2071 #[test]
2074 fn a_column_written_by_two_operators_is_refused_before_the_statement() {
2075 assert_eq!(
2076 refusals(
2077 "mutation { update_author(_set: {name: \"a\", count: 1}, \
2078 _inc: {count: 2}, where: {}) { affected_rows } }",
2079 "{}"
2080 ),
2081 vec!["Column found in multiple operators: ['count']."]
2082 );
2083 assert_eq!(
2084 refusals_with_paths(
2085 "mutation { update_author(_set: {count: 1}, _inc: {count: 2}, \
2086 where: {}) { affected_rows } }",
2087 "{}"
2088 ),
2089 vec!["$.selectionSet.update_author.args"]
2090 );
2091 assert_eq!(
2093 refusals(
2094 "mutation { update_author(_set: {name: \"a\"}, _inc: {count: 2}, \
2095 where: {}) { affected_rows } }",
2096 "{}"
2097 ),
2098 Vec::<String>::new()
2099 );
2100 }
2101
2102 #[test]
2105 fn a_request_is_refused_once_however_many_faults_it_has() {
2106 assert_eq!(
2107 refusals("{ author { nope } tree { alsoNope } }", "{}"),
2108 vec!["field 'nope' not found in type: 'author'"]
2109 );
2110 assert_eq!(
2114 refusals(
2115 "mutation { insert_author(objects: [{nope: 1}], unknownArg: 2) { id } }",
2116 "{}"
2117 ),
2118 vec!["field 'nope' not found in type: 'author_insert_input'"]
2119 );
2120 }
2121
2122 #[test]
2125 fn a_raster_that_is_not_hexadecimal_is_refused_as_that() {
2126 let invalid = "invalid hexadecimal representation of raster well known binary format";
2127 assert_eq!(
2128 refusals(
2129 "query ($r: raster) { tree(where: {rast: {_st_intersects_rast: $r}}) { path } }",
2130 r#"{"r": "this is invalid raster value"}"#
2131 ),
2132 vec![invalid]
2133 );
2134 assert_eq!(
2135 refusals_with_paths(
2136 "{ tree(where: {rast: {_st_intersects_rast: \"zz\"}}) { path } }",
2137 "{}"
2138 ),
2139 vec!["$.selectionSet.tree.args.where.rast._st_intersects_rast"]
2140 );
2141 assert_eq!(
2143 refusals(
2144 "{ tree(where: {rast: {_st_intersects_rast: \"abc\"}}) { path } }",
2145 "{}"
2146 ),
2147 vec![invalid]
2148 );
2149 assert!(refusals(
2152 "{ tree(where: {rast: {_st_intersects_rast: \"0100\"}}) { path } }",
2153 "{}"
2154 )
2155 .is_empty());
2156 }
2157
2158 #[test]
2162 fn fragments_that_spread_each_other_are_named_as_a_cycle() {
2163 let cycle = "query { author { ...a } } \
2164 fragment a on author { id ...b } \
2165 fragment b on author { id ...a }";
2166 assert_eq!(
2167 refusals(cycle, "{}"),
2168 vec!["the fragment definition(s) a and b form a cycle"]
2169 );
2170 assert_eq!(
2173 refusals_with_paths(cycle, "{}"),
2174 vec!["$.selectionSet.author.selectionSet.a.selectionSet.b.selectionSet"]
2175 );
2176 assert_eq!(
2178 refusals(
2179 "query { author { ...a } } fragment a on author { id ...a }",
2180 "{}"
2181 ),
2182 vec!["the fragment definition(s) a form a cycle"]
2183 );
2184 assert!(refusals(
2186 "query { author { ...a } articles: author { ...a } } \
2187 fragment a on author { id }",
2188 "{}"
2189 )
2190 .is_empty());
2191 }
2192
2193 #[test]
2196 fn a_number_that_is_not_an_int_is_refused_as_one() {
2197 let bounds = |value: &str| {
2198 format!(
2199 "The value {} lies outside the bounds or is not an integer. \
2200 Maybe it is a float, or is there integer overflow?",
2201 value
2202 )
2203 };
2204 assert_eq!(
2205 refusals(
2206 "mutation { insert_author_one(object: {count: 2147483648}) { id } }",
2207 "{}"
2208 ),
2209 vec![bounds("2.147483648e9")]
2210 );
2211 assert_eq!(
2212 refusals_with_paths(
2213 "mutation { insert_author(objects: [{count: 2147483648}]) { id } }",
2214 "{}"
2215 ),
2216 vec!["$.selectionSet.insert_author.args.objects[0].count"]
2217 );
2218 for good in ["2147483647", "-2147483648", "0"] {
2220 assert!(
2221 refusals(
2222 &format!(
2223 "mutation {{ insert_author_one(object: {{count: {}}}) {{ id }} }}",
2224 good
2225 ),
2226 "{}"
2227 )
2228 .is_empty(),
2229 "{} was refused",
2230 good
2231 );
2232 }
2233 }
2234
2235 #[test]
2238 fn a_number_is_shown_the_way_the_message_spells_it() {
2239 assert_eq!(shown_as_haskell_would(2147483648.0), "2.147483648e9");
2240 assert_eq!(shown_as_haskell_would(1.5), "1.5");
2241 assert_eq!(shown_as_haskell_would(1.0), "1.0");
2242 assert_eq!(shown_as_haskell_would(0.0), "0.0");
2243 assert_eq!(shown_as_haskell_would(-2147483649.0), "-2.147483649e9");
2244 assert_eq!(shown_as_haskell_would(1e-3), "1.0e-3");
2245 assert_eq!(shown_as_haskell_would(9999999.0), "9999999.0");
2248 assert_eq!(shown_as_haskell_would(10000000.0), "1.0e7");
2249 assert_eq!(shown_as_haskell_would(0.1), "0.1");
2250 }
2251
2252 #[test]
2255 fn a_limit_is_a_number_of_rows_and_not_any_integer() {
2256 let refused = |q: &str| refusals(q, "{}");
2257 assert_eq!(
2258 refused("{ author(limit: -1) { id } }"),
2259 vec!["expected a non-negative 32-bit integer for type 'Int', but found an integer"]
2260 );
2261 assert_eq!(
2262 refused("{ author(limit: \"3\") { id } }"),
2263 vec!["expected a non-negative 32-bit integer for type 'Int', but found a string"]
2264 );
2265 assert_eq!(
2266 refusals_with_paths("{ author(limit: -1) { id } }", "{}"),
2267 vec!["$.selectionSet.author.args.limit"]
2268 );
2269 assert_eq!(
2271 refusals(
2272 "query ($n: Int) { author(limit: $n) { id } }",
2273 r#"{"n": -1}"#
2274 ),
2275 vec!["expected a non-negative 32-bit integer for type 'Int', but found an integer"]
2276 );
2277 assert!(refused("{ author(limit: 0) { id } }").is_empty());
2279 assert!(refused("{ author(limit: 2147483647) { id } }").is_empty());
2280 assert!(refused("{ author(limit: null) { id } }").is_empty());
2281 assert_eq!(
2284 refused("{ author(limit: 4294967295) { id } }"),
2285 vec![
2286 "The value 4.294967295e9 lies outside the bounds or is not an \
2287 integer. Maybe it is a float, or is there integer overflow?"
2288 ]
2289 );
2290 }
2291
2292 #[test]
2295 fn a_quoted_enum_value_is_not_a_member_that_is_missing() {
2296 assert_eq!(
2297 refusals("{ tree(where: {kind: {_eq: \"leaf\"}}) { path } }", "{}"),
2298 vec!["expected an enum value for type 'kind_enum', but found a string"]
2299 );
2300 assert_eq!(
2301 refusals("{ tree(where: {kind: {_eq: twig}}) { path } }", "{}"),
2302 vec!["expected one of the values ['branch', 'leaf'] for type 'kind_enum', but found 'twig'"]
2303 );
2304 assert_eq!(
2307 refusals(
2308 "query ($k: kind_enum) { tree(where: {kind: {_eq: $k}}) { path } }",
2309 r#"{"k": "twig"}"#
2310 ),
2311 vec!["expected one of the values ['branch', 'leaf'] for type 'kind_enum', but found 'twig'"]
2312 );
2313 assert!(refusals(
2314 "query ($k: kind_enum) { tree(where: {kind: {_eq: $k}}) { path } }",
2315 r#"{"k": "leaf"}"#
2316 )
2317 .is_empty());
2318 }
2319
2320 #[test]
2324 fn a_document_that_does_not_parse_is_not_a_query() {
2325 assert_eq!(
2326 refusals("{ author(where: {name: {_eq: \"unclosed}}) { id } }", "{}"),
2327 vec!["not a valid graphql query"]
2328 );
2329 assert_eq!(
2330 refusals_with_paths("query { author(", "{}"),
2331 vec!["$.query"]
2332 );
2333 assert_eq!(
2336 refusals("{ nothing_here { id } }", "{}"),
2337 vec!["field 'nothing_here' not found in type: 'query_root'"]
2338 );
2339 }
2340
2341 #[test]
2345 fn a_label_path_with_an_empty_label_is_refused() {
2346 let empty = "Expecting label path: a sequence of zero or more labels \
2347 separated by dots, for example L1.L2.L3";
2348 assert_eq!(
2349 refusals(
2350 "{ tree(where: {path: {_ancestor: \"a.b.\"}}) { path } }",
2351 "{}"
2352 ),
2353 vec![empty]
2354 );
2355 assert_eq!(
2356 refusals(
2357 "{ tree(where: {path: {_ancestor: \".a\"}}) { path } }",
2358 "{}"
2359 ),
2360 vec![empty]
2361 );
2362 assert_eq!(
2363 refusals(
2364 "{ tree(where: {path: {_ancestor: \"a..b\"}}) { path } }",
2365 "{}"
2366 ),
2367 vec![empty]
2368 );
2369 assert_eq!(
2371 refusals_with_paths(
2372 "{ tree(where: {path: {_ancestor_any: [\"a\", \"b.\"]}}) { path } }",
2373 "{}"
2374 ),
2375 vec!["$.selectionSet.tree.args.where.path._ancestor_any[1]"]
2376 );
2377 for good in ["a", "a.b", "", "a-b", "a_b", "1.2", "Ünï", "a@b"] {
2380 assert!(
2381 refusals(
2382 &format!(
2383 "{{ tree(where: {{path: {{_ancestor: \"{}\"}}}}) {{ path }} }}",
2384 good
2385 ),
2386 "{}"
2387 )
2388 .is_empty(),
2389 "{} was refused",
2390 good
2391 );
2392 }
2393 }
2394
2395 #[test]
2396 fn a_variable_that_was_not_given_is_not_a_null() {
2397 assert!(refusals(
2398 "query ($n: String) { author(where: {name: {_eq: $n}}) { id } }",
2399 "{}"
2400 )
2401 .is_empty());
2402 }
2403
2404 #[test]
2406 fn a_null_written_anywhere_else_is_left_alone() {
2407 assert!(refusals(
2408 "mutation { insert_author_one(object: {name: null}) { id } }",
2409 "{}"
2410 )
2411 .is_empty());
2412 }
2413
2414 #[test]
2415 fn a_variable_of_the_wrong_type_is_refused() {
2416 assert_eq!(
2417 refusals("query ($s: String) { author(limit: $s) { id } }", "{}"),
2418 vec!["variable 's' is declared as 'String', but used where 'Int' is expected"]
2419 );
2420 }
2421
2422 #[test]
2423 fn the_check_reaches_inside_an_input_object() {
2424 assert_eq!(
2425 refusals(
2426 "query ($s: String) { author(where: {id: $s}) { id } }",
2427 "{}"
2428 ),
2429 vec!["variable 's' is declared as 'String', but used where 'Int' is expected"]
2430 );
2431 }
2432
2433 #[test]
2434 fn the_check_reaches_inside_a_list() {
2435 assert_eq!(
2436 refusals(
2437 "mutation ($n: Int) { insert_author(objects: [{name: $n}]) { id } }",
2438 "{}"
2439 ),
2440 vec!["variable 'n' is declared as 'Int', but used where 'String' is expected"]
2441 );
2442 }
2443
2444 #[test]
2445 fn a_nullable_variable_cannot_fill_a_non_null_place() {
2446 assert_eq!(
2447 refusals(
2448 "mutation ($a: author_insert_input) { insert_author_one(object: $a) { id } }",
2449 "{}"
2450 ),
2451 vec![
2452 "variable 'a' is declared as 'author_insert_input', but used where \
2453 'author_insert_input!' is expected"
2454 ]
2455 );
2456 }
2457
2458 #[test]
2459 fn a_default_lets_it() {
2460 assert!(refusals(
2463 "mutation ($a: author_insert_input = {name: \"x\"}) \
2464 { insert_author_one(object: $a) { id } }",
2465 "{}"
2466 )
2467 .is_empty());
2468 }
2469
2470 #[test]
2471 fn a_default_of_null_does_not() {
2472 assert_eq!(
2473 refusals(
2474 "mutation ($a: author_insert_input = null) \
2475 { insert_author_one(object: $a) { id } }",
2476 "{}"
2477 ),
2478 vec![
2479 "variable 'a' is declared as 'author_insert_input', but used where \
2480 'author_insert_input!' is expected"
2481 ]
2482 );
2483 }
2484
2485 #[test]
2486 fn a_default_on_the_place_lets_it_too() {
2487 assert!(refusals(
2490 "mutation ($c: [String!]) { insert_author(objects: [], update_columns: $c) { id } }",
2491 "{}"
2492 )
2493 .is_empty());
2494 }
2495
2496 #[test]
2497 fn a_non_null_variable_fits_a_nullable_place() {
2498 assert!(refusals(
2499 "query ($n: Int!) { author(limit: $n) { id } }",
2500 "{\"n\": 1}"
2501 )
2502 .is_empty());
2503 }
2504
2505 #[test]
2506 fn a_non_null_variable_given_null_is_refused() {
2507 assert_eq!(
2508 refusals(
2509 "query ($n: Int! = 1) { author(limit: $n) { id } }",
2510 "{\"n\": null}"
2511 ),
2512 vec!["null value found for non-nullable type: \"Int!\""]
2513 );
2514 }
2515
2516 #[test]
2517 fn a_nullable_variable_given_null_is_not() {
2518 assert!(refusals(
2519 "query ($n: Int) { author(limit: $n) { id } }",
2520 "{\"n\": null}"
2521 )
2522 .is_empty());
2523 }
2524
2525 #[test]
2526 fn a_variable_used_through_a_fragment_is_checked() {
2527 assert_eq!(
2528 refusals(
2529 "query ($s: String) { author { ...rows } } \
2530 fragment rows on author { id }",
2531 "{}"
2532 ),
2533 Vec::<String>::new()
2534 );
2535 assert_eq!(
2536 refusals(
2537 "query ($s: String) { ...roots } \
2538 fragment roots on query_root { author(limit: $s) { id } }",
2539 "{}"
2540 ),
2541 vec!["variable 's' is declared as 'String', but used where 'Int' is expected"]
2542 );
2543 }
2544}
2545
2546#[cfg(test)]
2547mod coercion_tests {
2548 use super::*;
2549 use async_graphql::dynamic::*;
2550
2551 fn schema() -> async_graphql::dynamic::Schema {
2554 let insert = InputObject::new("test_types_insert_input")
2555 .field(InputValue::new("c1_smallint", TypeRef::named("Int")))
2556 .field(InputValue::new("c6_real", TypeRef::named("Float")))
2557 .field(InputValue::new("c20_boolean", TypeRef::named("Boolean")))
2558 .field(InputValue::new("c13_text", TypeRef::named("String")));
2559 let row = Object::new("test_types").field(Field::new(
2560 "c1_smallint",
2561 TypeRef::named("Int"),
2562 |_| FieldFuture::new(async move { Ok(None::<async_graphql::Value>) }),
2563 ));
2564 let query = Object::new("query_root").field(
2565 Field::new(
2566 "test_types",
2567 TypeRef::named_nn_list_nn("test_types"),
2568 |_| FieldFuture::new(async move { Ok(Some(async_graphql::Value::List(vec![]))) }),
2569 )
2570 .argument(InputValue::new("limit", TypeRef::named("Int")))
2571 .argument(InputValue::new("offset", TypeRef::named("Int"))),
2572 );
2573 let mutation = Object::new("mutation_root").field(
2574 Field::new("insert_test_types", TypeRef::named("test_types"), |_| {
2575 FieldFuture::new(async move { Ok(None::<async_graphql::Value>) })
2576 })
2577 .argument(InputValue::new(
2578 "objects",
2579 TypeRef::named_nn_list_nn("test_types_insert_input"),
2580 )),
2581 );
2582 Schema::build("query_root", Some("mutation_root"), None)
2583 .register(insert)
2584 .register(row)
2585 .register(query)
2586 .register(mutation)
2587 .finish()
2588 .expect("the schema builds")
2589 }
2590
2591 fn rewritten(query: &str) -> String {
2592 let schema = schema();
2593 let request = prepare(Some(&schema), async_graphql::Request::new(query), None)
2594 .unwrap_or_else(|(request, _)| request);
2595 let mut request = request;
2596 format!("{:?}", request.parsed_query().expect("the query parses"))
2597 }
2598
2599 #[test]
2600 fn an_offset_written_as_a_string_becomes_a_number() {
2601 let printed = rewritten("{ test_types(offset: \"1\") { c1_smallint } }");
2602 assert!(printed.contains("Number(1)"), "{}", printed);
2603 }
2604
2605 #[test]
2608 fn a_limit_written_as_a_string_is_left_alone() {
2609 let printed = rewritten("{ test_types(limit: \"3\") { c1_smallint } }");
2610 assert!(printed.contains("String(\"3\")"), "{}", printed);
2611 }
2612
2613 #[test]
2614 fn a_column_written_as_a_string_becomes_what_the_column_is() {
2615 let printed = rewritten(
2616 "mutation { insert_test_types(objects: [{ \
2617 c1_smallint: \"32767\", c6_real: \"0.5\", c20_boolean: \"true\" }]) \
2618 { c1_smallint } }",
2619 );
2620 assert!(printed.contains("Number(32767)"), "{}", printed);
2621 assert!(printed.contains("Number(0.5)"), "{}", printed);
2622 assert!(printed.contains("Boolean(true)"), "{}", printed);
2623 }
2624
2625 #[test]
2628 fn a_string_written_where_a_string_belongs_stays_one() {
2629 let printed = rewritten(
2630 "mutation { insert_test_types(objects: [{ c13_text: \"32767\" }]) { c1_smallint } }",
2631 );
2632 assert!(printed.contains("String(\"32767\")"), "{}", printed);
2633 }
2634}