1use std::collections::HashMap;
5use std::fmt::Debug;
6use std::fmt::Display;
7use std::fmt::Formatter;
8use std::marker::PhantomData;
9use std::sync::Arc;
10
11use serde::de::DeserializeOwned;
12use serde::Deserialize;
13use serde::Serialize;
14use thiserror::Error;
15
16use crate::introspect::IntrospectionResponse;
17use crate::worker::JsWorker;
18
19#[derive(Debug, Serialize)]
22#[serde(rename_all = "camelCase")]
23pub struct QueryPlanOptions {
25 pub auto_fragmentization: bool,
27}
28
29impl Default for QueryPlanOptions {
31 fn default() -> Self {
33 Self {
34 auto_fragmentization: false,
35 }
36 }
37}
38
39#[derive(Debug, Serialize)]
40#[serde(rename_all = "camelCase")]
41pub struct OperationalContext {
44 pub schema: String,
46 pub query: String,
48 pub operation_name: String,
50}
51
52#[derive(Debug, Error, Serialize, Deserialize, PartialEq, Eq, Clone)]
60#[serde(rename_all = "camelCase")]
61pub struct PlanError {
62 pub message: Option<String>,
64 #[serde(deserialize_with = "none_only_if_value_is_null_or_empty_object")]
66 pub extensions: Option<PlanErrorExtensions>,
67 #[serde(skip_serializing, default)]
70 pub validation_error: bool,
71}
72
73fn none_only_if_value_is_null_or_empty_object<'de, D, T>(data: D) -> Result<Option<T>, D::Error>
80where
81 D: serde::de::Deserializer<'de>,
82 T: serde::Deserialize<'de>,
83{
84 #[derive(Deserialize)]
85 #[serde(untagged)]
86 enum OptionOrValue<T> {
87 Opt(Option<T>),
88 Val(serde_json::value::Value),
89 }
90
91 let as_option_or_value: Result<OptionOrValue<T>, D::Error> =
92 serde::Deserialize::deserialize(data);
93
94 match as_option_or_value {
95 Ok(OptionOrValue::Opt(t)) => Ok(t),
96 Ok(OptionOrValue::Val(obj)) => {
97 if let serde_json::value::Value::Object(o) = &obj {
98 if o.is_empty() {
99 return Ok(None);
100 }
101 }
102
103 Err(serde::de::Error::custom(format!(
104 "invalid neither null nor empty object: found {obj:?}"
105 )))
106 }
107 Err(e) => Err(e),
108 }
109}
110
111impl Display for PlanError {
112 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
113 if let Some(msg) = &self.message {
114 f.write_fmt(format_args!("{code}: {msg}", code = self.code(), msg = msg))
115 } else {
116 f.write_str(self.code())
117 }
118 }
119}
120
121#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)]
122pub struct PlanErrorExtensions {
124 pub code: String,
126 #[serde(skip_serializing_if = "Option::is_none")]
127 pub exception: Option<ExtensionsException>,
129}
130
131#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)]
132pub struct ExtensionsException {
134 pub stacktrace: String,
136}
137
138impl PlanError {
140 pub fn code(&self) -> &str {
142 match self.extensions {
143 Some(ref ext) => &ext.code,
144 None => "UNKNOWN",
145 }
146 }
147}
148
149#[derive(Deserialize, Debug)]
152pub struct BridgeSetupResult<T> {
154 pub data: Option<T>,
156 pub errors: Option<Vec<PlannerError>>,
158}
159
160#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)]
161#[serde(rename_all = "camelCase")]
162pub struct Location {
164 pub line: u32,
166 pub column: u32,
168}
169
170#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)]
171#[serde(untagged)]
172pub enum PlannerError {
174 WorkerGraphQLError(WorkerGraphQLError),
176 WorkerError(WorkerError),
178}
179
180impl PlannerError {
181 pub fn is_validation_error(&self) -> bool {
183 let PlannerError::WorkerGraphQLError(err) = self else {
184 return false;
185 };
186 err.validation_error
187 }
188}
189
190impl From<WorkerGraphQLError> for PlannerError {
191 fn from(e: WorkerGraphQLError) -> Self {
192 Self::WorkerGraphQLError(e)
193 }
194}
195
196impl From<WorkerError> for PlannerError {
197 fn from(e: WorkerError) -> Self {
198 Self::WorkerError(e)
199 }
200}
201
202impl std::fmt::Display for PlannerError {
203 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
204 match self {
205 Self::WorkerGraphQLError(graphql_error) => {
206 write!(f, "{graphql_error}")
207 }
208 Self::WorkerError(error) => {
209 write!(f, "{error}")
210 }
211 }
212 }
213}
214
215#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)]
218pub struct WorkerError {
219 pub message: Option<String>,
221 pub name: Option<String>,
223 pub stack: Option<String>,
225 pub extensions: Option<PlanErrorExtensions>,
227 #[serde(default)]
230 pub locations: Vec<Location>,
231}
232
233impl std::fmt::Display for WorkerError {
234 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
235 write!(
236 f,
237 "{}",
238 self.message
239 .clone()
240 .unwrap_or_else(|| "unknown error".to_string())
241 )
242 }
243}
244
245#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)]
250#[serde(rename_all = "camelCase")]
251pub struct WorkerGraphQLError {
252 pub name: String,
254 pub message: String,
258 #[serde(default)]
261 pub locations: Vec<Location>,
262 pub extensions: Option<PlanErrorExtensions>,
264 pub original_error: Option<Box<WorkerError>>,
266 #[serde(default)]
268 pub causes: Vec<Box<WorkerError>>,
269 #[serde(default, skip_serializing)]
271 pub validation_error: bool,
272}
273
274impl std::fmt::Display for WorkerGraphQLError {
275 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
276 write!(
277 f,
278 "{}\ncaused by\n{}",
279 self.message,
280 self.causes
281 .iter()
282 .map(std::string::ToString::to_string)
283 .collect::<Vec<_>>()
284 .join("\n")
285 )
286 }
287}
288
289#[derive(Deserialize, Serialize, Debug, PartialEq, Eq, Clone)]
290#[serde(rename_all = "camelCase")]
291pub struct ReferencedFieldsForType {
294 #[serde(default)]
296 pub field_names: Vec<String>,
297 #[serde(default)]
299 pub is_interface: bool,
300}
301
302#[derive(Deserialize, Serialize, Debug, PartialEq, Eq, Clone)]
303#[serde(rename_all = "camelCase")]
304pub struct UsageReporting {
307 pub stats_report_key: String,
311 #[serde(default)]
313 pub referenced_fields_by_type: HashMap<String, ReferencedFieldsForType>,
314}
315
316#[derive(Deserialize, Debug)]
317#[serde(rename_all = "camelCase")]
318pub struct PlanResult<T> {
320 pub data: Option<T>,
322 pub usage_reporting: UsageReporting,
325 pub errors: Option<Vec<PlanError>>,
327}
328
329#[derive(Debug)]
331pub struct PlanSuccess<T> {
332 pub data: T,
334 pub usage_reporting: UsageReporting,
337}
338
339#[derive(Deserialize, Debug)]
340#[serde(rename_all = "camelCase")]
341pub struct ApiSchema {
343 pub schema: String,
345}
346
347#[derive(Debug, Clone)]
349pub struct PlanErrors {
350 pub errors: Arc<Vec<PlanError>>,
352 pub usage_reporting: UsageReporting,
355}
356
357impl std::fmt::Display for PlanErrors {
358 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
359 f.write_fmt(format_args!(
360 "query validation errors: {}",
361 self.errors
362 .iter()
363 .map(|e| e
364 .message
365 .clone()
366 .unwrap_or_else(|| "UNKNWON ERROR".to_string()))
367 .collect::<Vec<String>>()
368 .join(", ")
369 ))
370 }
371}
372
373impl<T> PlanResult<T>
374where
375 T: DeserializeOwned + Send + Debug + 'static,
376{
377 pub fn into_result(self) -> Result<PlanSuccess<T>, PlanErrors> {
379 let usage_reporting = self.usage_reporting;
380 if let Some(data) = self.data {
381 Ok(PlanSuccess {
382 data,
383 usage_reporting,
384 })
385 } else {
386 let errors = Arc::new(self.errors.unwrap_or_else(|| {
387 vec![PlanError {
388 message: Some("an unknown error occured".to_string()),
389 extensions: None,
390 validation_error: false,
391 }]
392 }));
393 Err(PlanErrors {
394 errors,
395 usage_reporting,
396 })
397 }
398 }
399}
400
401#[derive(Deserialize, Debug)]
402#[serde(rename_all = "camelCase")]
403pub struct HeapStatistics {
405 pub heap_total: u64,
407 pub heap_used: u64,
409 pub external: u64,
411}
412
413pub struct Planner<T>
416where
417 T: DeserializeOwned + Send + Debug + 'static,
418{
419 worker: Arc<JsWorker>,
420 schema_id: u64,
421 t: PhantomData<T>,
422}
423
424impl<T> Debug for Planner<T>
425where
426 T: DeserializeOwned + Send + Debug + 'static,
427{
428 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
429 f.debug_struct("Planner")
430 .field("schema_id", &self.schema_id)
431 .finish()
432 }
433}
434
435impl<T> Planner<T>
436where
437 T: DeserializeOwned + Send + Debug + 'static,
438{
439 pub async fn new(
441 schema: String,
442 config: QueryPlannerConfig,
443 ) -> Result<Self, Vec<PlannerError>> {
444 let schema_id: u64 = rand::random();
445 let worker = JsWorker::new(include_str!("../bundled/plan_worker.js"));
446 let worker_is_set_up = worker
447 .request::<PlanCmd, BridgeSetupResult<serde_json::Value>>(PlanCmd::UpdateSchema {
448 schema,
449 config,
450 schema_id,
451 })
452 .await
453 .map_err(|e| {
454 vec![WorkerError {
455 name: Some("planner setup error".to_string()),
456 message: Some(e.to_string()),
457 stack: None,
458 extensions: None,
459 locations: Default::default(),
460 }
461 .into()]
462 });
463
464 match worker_is_set_up {
470 Err(setup_error) => {
471 let _ = worker
472 .request::<PlanCmd, serde_json::Value>(PlanCmd::Exit { schema_id })
473 .await;
474 return Err(setup_error);
475 }
476 Ok(setup) => {
477 if let Some(error) = setup.errors {
478 let _ = worker.send(None, PlanCmd::Exit { schema_id }).await;
479 return Err(error);
480 }
481 }
482 }
483
484 let worker = Arc::new(worker);
485
486 Ok(Self {
487 worker,
488 schema_id,
489 t: PhantomData,
490 })
491 }
492
493 pub async fn update(
495 &self,
496 schema: String,
497 config: QueryPlannerConfig,
498 ) -> Result<Self, Vec<PlannerError>> {
499 let schema_id: u64 = rand::random();
500
501 let worker_is_set_up = self
502 .worker
503 .request::<PlanCmd, BridgeSetupResult<serde_json::Value>>(PlanCmd::UpdateSchema {
504 schema,
505 config,
506 schema_id,
507 })
508 .await
509 .map_err(|e| {
510 vec![WorkerError {
511 name: Some("planner setup error".to_string()),
512 message: Some(e.to_string()),
513 stack: None,
514 extensions: None,
515 locations: Default::default(),
516 }
517 .into()]
518 });
519
520 match worker_is_set_up {
522 Err(setup_error) => {
523 return Err(setup_error);
524 }
525 Ok(setup) => {
526 if let Some(error) = setup.errors {
527 return Err(error);
528 }
529 }
530 }
531
532 Ok(Self {
533 worker: self.worker.clone(),
534 schema_id,
535 t: PhantomData,
536 })
537 }
538
539 pub async fn plan(
541 &self,
542 query: String,
543 operation_name: Option<String>,
544 options: PlanOptions,
545 ) -> Result<PlanResult<T>, crate::error::Error> {
546 self.worker
547 .request(PlanCmd::Plan {
548 query,
549 operation_name,
550 schema_id: self.schema_id,
551 options,
552 })
553 .await
554 }
555
556 pub async fn api_schema(&self) -> Result<ApiSchema, crate::error::Error> {
558 self.worker
559 .request(PlanCmd::ApiSchema {
560 schema_id: self.schema_id,
561 })
562 .await
563 }
564
565 pub async fn introspect(
567 &self,
568 query: String,
569 ) -> Result<IntrospectionResponse, crate::error::Error> {
570 self.worker
571 .request(PlanCmd::Introspect {
572 query,
573 schema_id: self.schema_id,
574 })
575 .await
576 }
577
578 pub async fn operation_signature(
580 &self,
581 query: String,
582 operation_name: Option<String>,
583 ) -> Result<String, crate::error::Error> {
584 self.worker
585 .request(PlanCmd::Signature {
586 query,
587 operation_name,
588 schema_id: self.schema_id,
589 })
590 .await
591 }
592
593 pub async fn subgraphs(&self) -> Result<HashMap<String, String>, crate::error::Error> {
595 self.worker
596 .request(PlanCmd::Subgraphs {
597 schema_id: self.schema_id,
598 })
599 .await
600 }
601
602 pub async fn get_heap_statistics(&self) -> Result<HeapStatistics, crate::error::Error> {
604 self.worker.request(PlanCmd::GetHeapStatistics).await
605 }
606}
607
608impl<T> Drop for Planner<T>
609where
610 T: DeserializeOwned + Send + Debug + 'static,
611{
612 fn drop(&mut self) {
613 let worker_clone = self.worker.clone();
615 let schema_id = self.schema_id;
616 let _ = std::thread::spawn(move || {
617 let runtime = tokio::runtime::Builder::new_current_thread()
618 .build()
619 .unwrap();
620
621 let _ = runtime.block_on(async move {
622 worker_clone.send(None, PlanCmd::Exit { schema_id }).await
623 });
624 })
625 .join();
626 }
627}
628
629#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Hash, Default)]
631#[serde(rename_all = "camelCase")]
632pub struct PlanOptions {
633 pub override_conditions: Vec<String>,
635}
636
637#[derive(Serialize, Debug, Clone, PartialEq, Eq, Hash)]
638#[serde(tag = "kind")]
639enum PlanCmd {
640 #[serde(rename_all = "camelCase")]
641 UpdateSchema {
642 schema: String,
643 config: QueryPlannerConfig,
644 schema_id: u64,
645 },
646 #[serde(rename_all = "camelCase")]
647 Plan {
648 query: String,
649 operation_name: Option<String>,
650 schema_id: u64,
651 options: PlanOptions,
652 },
653 #[serde(rename_all = "camelCase")]
654 ApiSchema { schema_id: u64 },
655 #[serde(rename_all = "camelCase")]
656 Introspect { query: String, schema_id: u64 },
657 #[serde(rename_all = "camelCase")]
658 Signature {
659 query: String,
660 operation_name: Option<String>,
661 schema_id: u64,
662 },
663 #[serde(rename_all = "camelCase")]
664 Subgraphs { schema_id: u64 },
665 #[serde(rename_all = "camelCase")]
666 Exit { schema_id: u64 },
667 #[serde(rename_all = "camelCase")]
668 GetHeapStatistics,
669}
670
671#[derive(Serialize, Debug, Clone, PartialEq, Eq, Hash)]
672#[serde(rename_all = "camelCase")]
673pub struct QueryPlannerConfig {
675 pub incremental_delivery: Option<IncrementalDeliverySupport>,
684 pub graphql_validation: bool,
686 pub reuse_query_fragments: Option<bool>,
699
700 pub generate_query_fragments: Option<bool>,
705
706 pub debug: Option<QueryPlannerDebugConfig>,
710 pub type_conditioned_fetching: bool,
718}
719
720impl Default for QueryPlannerConfig {
721 fn default() -> Self {
722 Self {
723 incremental_delivery: Some(IncrementalDeliverySupport {
724 enable_defer: Some(false),
725 }),
726 graphql_validation: true,
727 reuse_query_fragments: None,
728 generate_query_fragments: None,
729 debug: Default::default(),
730 type_conditioned_fetching: false,
731 }
732 }
733}
734
735#[derive(Serialize, Debug, Clone, PartialEq, Eq, Hash)]
736#[serde(rename_all = "camelCase")]
737pub struct IncrementalDeliverySupport {
739 #[serde(default)]
745 pub enable_defer: Option<bool>,
746}
747
748#[derive(Serialize, Debug, Clone, Default, PartialEq, Eq, Hash)]
749#[serde(rename_all = "camelCase")]
750pub struct QueryPlannerDebugConfig {
752 pub bypass_planner_for_single_subgraph: Option<bool>,
755
756 pub max_evaluated_plans: Option<u32>,
773
774 pub paths_limit: Option<u32>,
787}
788#[cfg(test)]
789mod tests {
790 use futures::stream;
791 use futures::stream::StreamExt;
792
793 use std::collections::BTreeMap;
794
795 use super::*;
796
797 const QUERY: &str = include_str!("testdata/query.graphql");
798 const QUERY2: &str = include_str!("testdata/query2.graphql");
799 const MULTIPLE_QUERIES: &str = include_str!("testdata/query_with_multiple_operations.graphql");
800 const NO_OPERATION: &str = include_str!("testdata/no_operation.graphql");
801 const QUERY_REUSE_QUERY_FRAGMENTS: &str =
802 include_str!("testdata/query_reuse_query_fragments.graphql");
803 const QUERY_GENERATE_QUERY_FRAGMENTS: &str =
804 include_str!("testdata/query_generate_query_fragments.graphql");
805
806 const MULTIPLE_ANONYMOUS_QUERIES: &str =
807 include_str!("testdata/query_with_multiple_anonymous_operations.graphql");
808 const NAMED_QUERY: &str = include_str!("testdata/named_query.graphql");
809 const SCHEMA: &str = include_str!("testdata/schema.graphql");
810 const SCHEMA_WITHOUT_REVIEW_BODY: &str =
811 include_str!("testdata/schema_without_review_body.graphql");
812 const SCHEMA_REUSE_QUERY_FRAGMENTS: &str =
813 include_str!("testdata/schema_reuse_query_fragments.graphql");
814 const SCHEMA_GENERATE_QUERY_FRAGMENTS: &str =
815 include_str!("testdata/schema_generate_query_fragments.graphql");
816 const CORE_IN_V0_1: &str = include_str!("testdata/core_in_v0.1.graphql");
817 const UNSUPPORTED_FEATURE: &str = include_str!("testdata/unsupported_feature.graphql");
818 const UNSUPPORTED_FEATURE_FOR_EXECUTION: &str =
819 include_str!("testdata/unsupported_feature_for_execution.graphql");
820 const UNSUPPORTED_FEATURE_FOR_SECURITY: &str =
821 include_str!("testdata/unsupported_feature_for_security.graphql");
822 const PROGRESSIVE_OVERRIDE: &str = include_str!("testdata/progressive_override.graphql");
823
824 #[tokio::test]
825 async fn anonymous_query_works() {
826 let planner =
827 Planner::<serde_json::Value>::new(SCHEMA.to_string(), QueryPlannerConfig::default())
828 .await
829 .unwrap();
830
831 let payload = planner
832 .plan(QUERY.to_string(), None, PlanOptions::default())
833 .await
834 .unwrap()
835 .into_result()
836 .unwrap();
837 insta::assert_snapshot!(serde_json::to_string_pretty(&payload.data).unwrap());
838 insta::with_settings!({sort_maps => true}, {
839 insta::assert_json_snapshot!(payload.usage_reporting);
840 });
841 }
842
843 #[tokio::test]
847 async fn progressive_override() {
848 let query = "{ t { a } }";
849 let planner = Planner::<serde_json::Value>::new(
850 PROGRESSIVE_OVERRIDE.to_string(),
851 QueryPlannerConfig::default(),
852 )
853 .await
854 .unwrap();
855
856 let payload1 = planner
857 .plan(
858 query.to_string(),
859 None,
860 PlanOptions {
861 override_conditions: vec!["foo".to_string()],
862 ..Default::default()
863 },
864 )
865 .await
866 .unwrap()
867 .into_result()
868 .unwrap();
869 insta::assert_snapshot!(serde_json::to_string_pretty(&payload1.data).unwrap());
870
871 let payload2 = planner
872 .plan(query.to_string(), None, PlanOptions::default())
873 .await
874 .unwrap()
875 .into_result()
876 .unwrap();
877 insta::assert_snapshot!(serde_json::to_string_pretty(&payload2.data).unwrap());
878 }
879
880 #[tokio::test]
881 async fn named_query_works() {
882 let planner =
883 Planner::<serde_json::Value>::new(SCHEMA.to_string(), QueryPlannerConfig::default())
884 .await
885 .unwrap();
886
887 let payload = planner
888 .plan(NAMED_QUERY.to_string(), None, PlanOptions::default())
889 .await
890 .unwrap()
891 .into_result()
892 .unwrap();
893 insta::assert_snapshot!(serde_json::to_string_pretty(&payload.data).unwrap());
894 insta::with_settings!({sort_maps => true}, {
895 insta::assert_json_snapshot!(payload.usage_reporting);
896 });
897 }
898
899 #[tokio::test]
900 async fn named_query_with_several_choices_works() {
901 let planner =
902 Planner::<serde_json::Value>::new(SCHEMA.to_string(), QueryPlannerConfig::default())
903 .await
904 .unwrap();
905
906 let payload = planner
907 .plan(
908 MULTIPLE_QUERIES.to_string(),
909 Some("MyFirstName".to_string()),
910 PlanOptions::default(),
911 )
912 .await
913 .unwrap()
914 .into_result()
915 .unwrap();
916 insta::assert_snapshot!(serde_json::to_string_pretty(&payload.data).unwrap());
917 insta::with_settings!({sort_maps => true}, {
918 insta::assert_json_snapshot!(payload.usage_reporting);
919 });
920 }
921
922 #[tokio::test]
923 async fn named_query_with_operation_name_works() {
924 let planner =
925 Planner::<serde_json::Value>::new(SCHEMA.to_string(), QueryPlannerConfig::default())
926 .await
927 .unwrap();
928
929 let payload = planner
930 .plan(
931 NAMED_QUERY.to_string(),
932 Some("MyFirstAndLastName".to_string()),
933 PlanOptions::default(),
934 )
935 .await
936 .unwrap()
937 .into_result()
938 .unwrap();
939 insta::assert_snapshot!(serde_json::to_string_pretty(&payload.data).unwrap());
940 }
941
942 #[tokio::test]
943 async fn reuse_query_fragments_defaults_to_true() {
944 let planner = Planner::<serde_json::Value>::new(
945 SCHEMA_REUSE_QUERY_FRAGMENTS.to_string(),
946 QueryPlannerConfig::default(),
947 )
948 .await
949 .unwrap();
950
951 let payload = planner
952 .plan(
953 QUERY_REUSE_QUERY_FRAGMENTS.to_string(),
954 None,
955 PlanOptions::default(),
956 )
957 .await
958 .unwrap()
959 .into_result()
960 .unwrap();
961 insta::assert_snapshot!(serde_json::to_string_pretty(&payload.data).unwrap());
962 }
963
964 #[tokio::test]
965 async fn reuse_query_fragments_explicit_true() {
966 let planner = Planner::<serde_json::Value>::new(
967 SCHEMA_REUSE_QUERY_FRAGMENTS.to_string(),
968 QueryPlannerConfig {
969 generate_query_fragments: Default::default(),
970 reuse_query_fragments: Some(true),
971 ..Default::default()
972 },
973 )
974 .await
975 .unwrap();
976
977 let payload = planner
978 .plan(
979 QUERY_REUSE_QUERY_FRAGMENTS.to_string(),
980 None,
981 PlanOptions::default(),
982 )
983 .await
984 .unwrap()
985 .into_result()
986 .unwrap();
987 insta::assert_snapshot!(serde_json::to_string_pretty(&payload.data).unwrap());
988 }
989
990 #[tokio::test]
991 async fn reuse_query_fragments_false() {
992 let planner = Planner::<serde_json::Value>::new(
993 SCHEMA_REUSE_QUERY_FRAGMENTS.to_string(),
994 QueryPlannerConfig {
995 generate_query_fragments: Default::default(),
996 reuse_query_fragments: Some(false),
997 ..Default::default()
998 },
999 )
1000 .await
1001 .unwrap();
1002
1003 let payload = planner
1004 .plan(
1005 QUERY_REUSE_QUERY_FRAGMENTS.to_string(),
1006 None,
1007 PlanOptions::default(),
1008 )
1009 .await
1010 .unwrap()
1011 .into_result()
1012 .unwrap();
1013 insta::assert_snapshot!(serde_json::to_string_pretty(&payload.data).unwrap());
1014 }
1015
1016 #[tokio::test]
1017 async fn generate_query_fragments_defaults_to_false() {
1018 let planner = Planner::<serde_json::Value>::new(
1019 SCHEMA_GENERATE_QUERY_FRAGMENTS.to_string(),
1020 QueryPlannerConfig::default(),
1021 )
1022 .await
1023 .unwrap();
1024
1025 let payload = planner
1026 .plan(
1027 QUERY_GENERATE_QUERY_FRAGMENTS.to_string(),
1028 None,
1029 PlanOptions::default(),
1030 )
1031 .await
1032 .unwrap()
1033 .into_result()
1034 .unwrap();
1035 insta::assert_snapshot!(serde_json::to_string_pretty(&payload.data).unwrap());
1036 }
1037
1038 #[tokio::test]
1039 async fn generate_query_fragments_explicit_false() {
1040 let planner = Planner::<serde_json::Value>::new(
1041 SCHEMA_GENERATE_QUERY_FRAGMENTS.to_string(),
1042 QueryPlannerConfig {
1043 generate_query_fragments: Some(false),
1044 ..Default::default()
1045 },
1046 )
1047 .await
1048 .unwrap();
1049
1050 let payload = planner
1051 .plan(
1052 QUERY_GENERATE_QUERY_FRAGMENTS.to_string(),
1053 None,
1054 PlanOptions::default(),
1055 )
1056 .await
1057 .unwrap()
1058 .into_result()
1059 .unwrap();
1060 insta::assert_snapshot!(serde_json::to_string_pretty(&payload.data).unwrap());
1061 }
1062
1063 #[tokio::test]
1064 async fn generate_query_fragments_true() {
1065 let planner = Planner::<serde_json::Value>::new(
1066 SCHEMA_GENERATE_QUERY_FRAGMENTS.to_string(),
1067 QueryPlannerConfig {
1068 generate_query_fragments: Some(true),
1069 ..Default::default()
1070 },
1071 )
1072 .await
1073 .unwrap();
1074
1075 let payload = planner
1076 .plan(
1077 QUERY_GENERATE_QUERY_FRAGMENTS.to_string(),
1078 None,
1079 PlanOptions::default(),
1080 )
1081 .await
1082 .unwrap()
1083 .into_result()
1084 .unwrap();
1085 insta::assert_snapshot!(serde_json::to_string_pretty(&payload.data).unwrap());
1086 }
1087
1088 #[tokio::test]
1089 async fn parse_errors_return_the_right_usage_reporting_data() {
1090 let planner =
1091 Planner::<serde_json::Value>::new(SCHEMA.to_string(), QueryPlannerConfig::default())
1092 .await
1093 .unwrap();
1094
1095 let payload = planner
1096 .plan(
1097 "this query will definitely not parse".to_string(),
1098 None,
1099 PlanOptions::default(),
1100 )
1101 .await
1102 .unwrap()
1103 .into_result()
1104 .unwrap_err();
1105
1106 assert_eq!(
1107 "Syntax Error: Unexpected Name \"this\".",
1108 payload.errors[0].message.as_ref().unwrap()
1109 );
1110 assert_eq!(
1111 "## GraphQLParseFailure\n",
1112 payload.usage_reporting.stats_report_key
1113 );
1114 }
1115
1116 #[tokio::test]
1117 async fn validation_errors_return_the_right_usage_reporting_data() {
1118 let planner =
1119 Planner::<serde_json::Value>::new(SCHEMA.to_string(), QueryPlannerConfig::default())
1120 .await
1121 .unwrap();
1122
1123 let payload = planner
1124 .plan(
1125 "\
1127 fragment thatUserFragment1 on User {
1128 id
1129 ...thatUserFragment2
1130 }
1131 fragment thatUserFragment2 on User {
1132 id
1133 ...thatUserFragment1
1134 }
1135 query { me { id ...thatUserFragment1 } }"
1136 .to_string(),
1137 None,
1138 PlanOptions::default(),
1139 )
1140 .await
1141 .unwrap()
1142 .into_result()
1143 .unwrap_err();
1144
1145 assert_eq!(
1146 "Cannot spread fragment \"thatUserFragment1\" within itself via \"thatUserFragment2\".",
1147 payload.errors[0].message.as_ref().unwrap()
1148 );
1149 assert_eq!(
1150 "## GraphQLValidationFailure\n",
1151 payload.usage_reporting.stats_report_key
1152 );
1153 }
1154
1155 #[tokio::test]
1156 async fn unknown_operation_name_errors_return_the_right_usage_reporting_data() {
1157 let planner =
1158 Planner::<serde_json::Value>::new(SCHEMA.to_string(), QueryPlannerConfig::default())
1159 .await
1160 .unwrap();
1161
1162 let payload = planner
1163 .plan(
1164 QUERY.to_string(),
1165 Some("ThisOperationNameDoesntExist".to_string()),
1166 PlanOptions::default(),
1167 )
1168 .await
1169 .unwrap()
1170 .into_result()
1171 .unwrap_err();
1172
1173 assert_eq!(
1174 "Unknown operation named \"ThisOperationNameDoesntExist\"",
1175 payload.errors[0].message.as_ref().unwrap()
1176 );
1177 assert_eq!(
1178 "## GraphQLUnknownOperationName\n",
1179 payload.usage_reporting.stats_report_key
1180 );
1181 }
1182
1183 #[tokio::test]
1184 async fn must_provide_operation_name_errors_return_the_right_usage_reporting_data() {
1185 let planner =
1186 Planner::<serde_json::Value>::new(SCHEMA.to_string(), QueryPlannerConfig::default())
1187 .await
1188 .unwrap();
1189
1190 let payload = planner
1191 .plan(MULTIPLE_QUERIES.to_string(), None, PlanOptions::default())
1192 .await
1193 .unwrap()
1194 .into_result()
1195 .unwrap_err();
1196
1197 assert_eq!(
1198 "Must provide operation name if query contains multiple operations.",
1199 payload.errors[0].message.as_ref().unwrap()
1200 );
1201 assert_eq!(
1202 "## GraphQLUnknownOperationName\n",
1203 payload.usage_reporting.stats_report_key
1204 );
1205 }
1206
1207 #[tokio::test]
1208 async fn multiple_anonymous_queries_return_the_expected_usage_reporting_data() {
1209 let planner =
1210 Planner::<serde_json::Value>::new(SCHEMA.to_string(), QueryPlannerConfig::default())
1211 .await
1212 .unwrap();
1213
1214 let payload = planner
1215 .plan(
1216 MULTIPLE_ANONYMOUS_QUERIES.to_string(),
1217 None,
1218 PlanOptions::default(),
1219 )
1220 .await
1221 .unwrap()
1222 .into_result()
1223 .unwrap_err();
1224
1225 assert_eq!(
1226 "This anonymous operation must be the only defined operation.",
1227 payload.errors[0].message.as_ref().unwrap()
1228 );
1229 assert_eq!(
1230 "## GraphQLValidationFailure\n",
1231 payload.usage_reporting.stats_report_key
1232 );
1233 }
1234
1235 #[tokio::test]
1236 async fn no_operation_in_document() {
1237 let planner =
1238 Planner::<serde_json::Value>::new(SCHEMA.to_string(), QueryPlannerConfig::default())
1239 .await
1240 .unwrap();
1241
1242 let payload = planner
1243 .plan(NO_OPERATION.to_string(), None, PlanOptions::default())
1244 .await
1245 .unwrap()
1246 .into_result()
1247 .unwrap_err();
1248
1249 assert_eq!(
1250 "Fragment \"thatUserFragment1\" is never used.",
1251 payload.errors[0].message.as_ref().unwrap()
1252 );
1253 assert_eq!(
1254 "## GraphQLValidationFailure\n",
1255 payload.usage_reporting.stats_report_key
1256 );
1257 }
1258
1259 #[tokio::test]
1260 async fn invalid_graphql_validation_1_is_caught() {
1268 let errors= vec![PlanError {
1269 message: Some("Cannot spread fragment \"thatUserFragment1\" within itself via \"thatUserFragment2\".".to_string()),
1270 extensions: Some(PlanErrorExtensions {
1271 code: String::from("GRAPHQL_VALIDATION_FAILED"),
1272 exception: None,
1273 }),
1274 validation_error: true,
1275 }];
1276
1277 assert_errors(
1278 errors,
1279 "\
1281 fragment thatUserFragment1 on User {
1282 id
1283 ...thatUserFragment2
1284 }
1285 fragment thatUserFragment2 on User {
1286 id
1287 ...thatUserFragment1
1288 }
1289 query { me { id ...thatUserFragment1 } }"
1290 .to_string(),
1291 None,
1292 )
1293 .await;
1294 }
1295
1296 #[tokio::test]
1297 async fn invalid_graphql_validation_2_is_caught() {
1305 let errors = vec![PlanError {
1306 message: Some(
1307 "Field \"id\" must not have a selection since type \"ID!\" has no subfields."
1308 .to_string(),
1309 ),
1310 extensions: Some(PlanErrorExtensions {
1311 code: String::from("GRAPHQL_VALIDATION_FAILED"),
1312 exception: None,
1313 }),
1314 validation_error: true,
1315 }];
1316
1317 assert_errors(
1318 errors,
1319 "{ me { id { absolutelyNotAcceptableLeaf } } }".to_string(),
1321 None,
1322 )
1323 .await;
1324 }
1325
1326 #[tokio::test]
1327 async fn invalid_graphql_validation_3_is_caught() {
1335 let errors = vec![PlanError {
1336 message: Some("Fragment \"UnusedTestFragment\" is never used.".to_string()),
1337 extensions: Some(PlanErrorExtensions {
1338 code: String::from("GRAPHQL_VALIDATION_FAILED"),
1339 exception: None,
1340 }),
1341 validation_error: true,
1342 }];
1343
1344 assert_errors(
1345 errors,
1346 "fragment UnusedTestFragment on User { id } query { me { id } }".to_string(),
1348 None,
1349 )
1350 .await;
1351 }
1352
1353 #[tokio::test]
1354 async fn invalid_federation_validation_is_caught() {
1355 let errors = vec![PlanError {
1356 message: Some(
1357 "Must provide operation name if query contains multiple operations.".to_string(),
1358 ),
1359 extensions: Some(PlanErrorExtensions {
1360 code: "GRAPHQL_VALIDATION_FAILED".to_string(),
1361 exception: None,
1362 }),
1363 validation_error: false,
1364 }];
1365
1366 assert_errors(
1367 errors, "query Operation1 { me { id } } query Operation2 { me { id } }".to_string(),
1370 None,
1371 )
1372 .await;
1373 }
1374
1375 #[tokio::test]
1376 async fn invalid_schema_is_caught() {
1377 let expected_errors: Vec<PlannerError> = vec![WorkerGraphQLError {
1378 name: "GraphQLError".to_string(),
1379 message: "Syntax Error: Unexpected Name \"Garbage\".".to_string(),
1380 extensions: None,
1381 locations: vec![Location { line: 1, column: 1 }],
1382 original_error: None,
1383 causes: vec![],
1384 validation_error: false,
1385 }
1386 .into()];
1387
1388 let actual_error =
1389 Planner::<serde_json::Value>::new("Garbage".to_string(), QueryPlannerConfig::default())
1390 .await
1391 .unwrap_err();
1392
1393 assert_eq!(expected_errors, actual_error);
1394 }
1395
1396 #[tokio::test]
1397 async fn syntactically_incorrect_query_is_caught() {
1398 let errors = vec![PlanError {
1399 message: Some("Syntax Error: Unexpected Name \"Garbage\".".to_string()),
1400 extensions: Some(PlanErrorExtensions {
1401 code: String::from("GRAPHQL_PARSE_FAILED"),
1402 exception: None,
1403 }),
1404 validation_error: false,
1405 }];
1406
1407 assert_errors(errors, "Garbage".to_string(), None).await;
1408 }
1409
1410 #[tokio::test]
1411 async fn query_missing_subfields() {
1412 let expected_error_message = r#"Field "reviews" of type "[Review]" must have a selection of subfields. Did you mean "reviews { ... }"?"#;
1413
1414 let errors = vec![PlanError {
1415 message: Some(expected_error_message.to_string()),
1416 extensions: Some(PlanErrorExtensions {
1417 code: String::from("GRAPHQL_VALIDATION_FAILED"),
1418 exception: None,
1419 }),
1420 validation_error: true,
1421 }];
1422
1423 assert_errors(
1424 errors,
1425 "query ExampleQuery { me { id reviews } }".to_string(),
1427 None,
1428 )
1429 .await;
1430 }
1431
1432 #[tokio::test]
1433 async fn query_field_that_doesnt_exist() {
1434 let expected_error_message = r#"Cannot query field "thisDoesntExist" on type "Query"."#;
1435 let errors = vec![PlanError {
1436 message: Some(expected_error_message.to_string()),
1437 extensions: Some(PlanErrorExtensions {
1438 code: String::from("GRAPHQL_VALIDATION_FAILED"),
1439 exception: None,
1440 }),
1441 validation_error: true,
1442 }];
1443
1444 assert_errors(
1445 errors,
1446 "query ExampleQuery { thisDoesntExist }".to_string(),
1448 None,
1449 )
1450 .await;
1451 }
1452
1453 async fn assert_errors(
1454 expected_errors: Vec<PlanError>,
1455 query: String,
1456 operation_name: Option<String>,
1457 ) {
1458 let planner =
1459 Planner::<serde_json::Value>::new(SCHEMA.to_string(), QueryPlannerConfig::default())
1460 .await
1461 .unwrap();
1462
1463 let actual = planner
1464 .plan(query, operation_name, PlanOptions::default())
1465 .await
1466 .unwrap();
1467
1468 assert_eq!(expected_errors, actual.errors.unwrap());
1469 }
1470
1471 #[tokio::test]
1472 async fn it_doesnt_race() {
1473 let planner =
1474 Planner::<serde_json::Value>::new(SCHEMA.to_string(), QueryPlannerConfig::default())
1475 .await
1476 .unwrap();
1477
1478 let query_1_response = planner
1479 .plan(QUERY.to_string(), None, PlanOptions::default())
1480 .await
1481 .unwrap()
1482 .data
1483 .unwrap();
1484
1485 let query_2_response = planner
1486 .plan(QUERY2.to_string(), None, PlanOptions::default())
1487 .await
1488 .unwrap()
1489 .data
1490 .unwrap();
1491
1492 let all_futures = stream::iter((0..1000).map(|i| {
1493 let (query, fut) = if i % 2 == 0 {
1494 (
1495 QUERY,
1496 planner.plan(QUERY.to_string(), None, PlanOptions::default()),
1497 )
1498 } else {
1499 (
1500 QUERY2,
1501 planner.plan(QUERY2.to_string(), None, PlanOptions::default()),
1502 )
1503 };
1504
1505 async move { (query, fut.await.unwrap()) }
1506 }));
1507
1508 all_futures
1509 .for_each_concurrent(None, |fut| async {
1510 let (query, plan_result) = fut.await;
1511 if query == QUERY {
1512 assert_eq!(query_1_response, plan_result.data.unwrap());
1513 } else {
1514 assert_eq!(query_2_response, plan_result.data.unwrap());
1515 }
1516 })
1517 .await;
1518 }
1519
1520 #[tokio::test]
1521 async fn error_on_core_in_v0_1() {
1522 let expected_errors: Vec<PlannerError> = vec![
1523 WorkerGraphQLError {
1524 name: "GraphQLError".to_string(),
1525 message: r#"one or more checks failed. Caused by:
1526the `for:` argument is unsupported by version v0.1 of the core spec. Please upgrade to at least @core v0.2 (https://specs.apollo.dev/core/v0.2).
1527
1528GraphQL request:2:1
15291 | schema
15302 | @core(feature: "https://specs.apollo.dev/core/v0.1")
1531 | ^
15323 | @core(feature: "https://specs.apollo.dev/join/v0.1", for: EXECUTION)
1533
1534GraphQL request:3:1
15352 | @core(feature: "https://specs.apollo.dev/core/v0.1")
15363 | @core(feature: "https://specs.apollo.dev/join/v0.1", for: EXECUTION)
1537 | ^
15384 | @core(
1539
1540GraphQL request:4:1
15413 | @core(feature: "https://specs.apollo.dev/join/v0.1", for: EXECUTION)
15424 | @core(
1543 | ^
15445 | feature: "https://specs.apollo.dev/something-unsupported/v0.1"
1545
1546feature https://specs.apollo.dev/something-unsupported/v0.1 is for: SECURITY but is unsupported
1547
1548GraphQL request:4:1
15493 | @core(feature: "https://specs.apollo.dev/join/v0.1", for: EXECUTION)
15504 | @core(
1551 | ^
15525 | feature: "https://specs.apollo.dev/something-unsupported/v0.1""#.to_string(),
1553 locations: Default::default(),
1554 extensions: Some(PlanErrorExtensions {
1555 code: "CheckFailed".to_string(),
1556 exception: None
1557 }),
1558 original_error: None,
1559 causes: vec![
1560 Box::new(WorkerError {
1561 message: Some("the `for:` argument is unsupported by version v0.1 of the core spec. Please upgrade to at least @core v0.2 (https://specs.apollo.dev/core/v0.2).".to_string()),
1562 name: None,
1563 stack: None,
1564 extensions: Some(PlanErrorExtensions { code: "UNSUPPORTED_LINKED_FEATURE".to_string(), exception: None }),
1565 locations: vec![Location { line: 2, column: 1 }, Location { line: 3, column: 1 }, Location { line: 4, column: 1 }]
1566 }),
1567 Box::new(WorkerError {
1568 message: Some("feature https://specs.apollo.dev/something-unsupported/v0.1 is for: SECURITY but is unsupported".to_string()),
1569 name: None,
1570 stack: None,
1571 extensions: Some(PlanErrorExtensions { code: "UNSUPPORTED_LINKED_FEATURE".to_string(), exception: None }),
1572 locations: vec![Location { line: 4, column: 1 }]
1573 })
1574 ],
1575 validation_error: false,
1576 }.into()
1577 ];
1578 let actual_errors = Planner::<serde_json::Value>::new(
1579 CORE_IN_V0_1.to_string(),
1580 QueryPlannerConfig::default(),
1581 )
1582 .await
1583 .unwrap_err();
1584
1585 pretty_assertions::assert_eq!(expected_errors, actual_errors);
1586 }
1587
1588 #[tokio::test]
1589 async fn unsupported_feature_without_for() {
1590 Planner::<serde_json::Value>::new(
1593 UNSUPPORTED_FEATURE.to_string(),
1594 QueryPlannerConfig::default(),
1595 )
1596 .await
1597 .unwrap();
1598 }
1599
1600 #[tokio::test]
1601 async fn unsupported_feature_for_execution() {
1602 let expected_errors: Vec<PlannerError> = vec![
1603 WorkerGraphQLError {
1604 name: "GraphQLError".to_string(),
1605 message: r#"one or more checks failed. Caused by:
1606feature https://specs.apollo.dev/unsupported-feature/v0.1 is for: EXECUTION but is unsupported
1607
1608GraphQL request:4:9
16093 | @core(feature: "https://specs.apollo.dev/join/v0.1", for: EXECUTION)
16104 | @core(
1611 | ^
16125 | feature: "https://specs.apollo.dev/unsupported-feature/v0.1""#.to_string(),
1613 locations: Default::default(),
1614 extensions: Some(PlanErrorExtensions {
1615 code: "CheckFailed".to_string(),
1616 exception: None
1617 }),
1618 original_error: None,
1619 causes: vec![
1620 Box::new(WorkerError {
1621 message: Some("feature https://specs.apollo.dev/unsupported-feature/v0.1 is for: EXECUTION but is unsupported".to_string()),
1622 name: None,
1623 stack: None,
1624 extensions: Some(PlanErrorExtensions { code: "UNSUPPORTED_LINKED_FEATURE".to_string(), exception: None }),
1625 locations: vec![Location { line: 4, column: 9 }]
1626 }),
1627 ],
1628 validation_error: false,
1629 }.into()
1630 ];
1631 let actual_errors = Planner::<serde_json::Value>::new(
1632 UNSUPPORTED_FEATURE_FOR_EXECUTION.to_string(),
1633 QueryPlannerConfig::default(),
1634 )
1635 .await
1636 .unwrap_err();
1637
1638 pretty_assertions::assert_eq!(expected_errors, actual_errors);
1639 }
1640
1641 #[tokio::test]
1642 async fn unsupported_feature_for_security() {
1643 let expected_errors: Vec<PlannerError> = vec![WorkerGraphQLError {
1644 name:"GraphQLError".into(),
1645 message: r#"one or more checks failed. Caused by:
1646feature https://specs.apollo.dev/unsupported-feature/v0.1 is for: SECURITY but is unsupported
1647
1648GraphQL request:4:9
16493 | @core(feature: "https://specs.apollo.dev/join/v0.1", for: EXECUTION)
16504 | @core(
1651 | ^
16525 | feature: "https://specs.apollo.dev/unsupported-feature/v0.1""#.to_string(),
1653 locations: vec![],
1654 extensions: Some(PlanErrorExtensions {
1655 code: "CheckFailed".to_string(),
1656 exception: None
1657 }),
1658 original_error: None,
1659 causes: vec![Box::new(WorkerError {
1660 message: Some("feature https://specs.apollo.dev/unsupported-feature/v0.1 is for: SECURITY but is unsupported".to_string()),
1661 extensions: Some(PlanErrorExtensions {
1662 code: "UNSUPPORTED_LINKED_FEATURE".to_string(),
1663 exception: None
1664 }),
1665 name: None,
1666 stack: None,
1667 locations: vec![Location { line: 4, column: 9 }]
1668 })],
1669 validation_error: false,
1670 }
1671 .into()];
1672 let actual_errors = Planner::<serde_json::Value>::new(
1673 UNSUPPORTED_FEATURE_FOR_SECURITY.to_string(),
1674 QueryPlannerConfig::default(),
1675 )
1676 .await
1677 .unwrap_err();
1678
1679 pretty_assertions::assert_eq!(expected_errors, actual_errors);
1680 }
1681
1682 #[tokio::test]
1683 async fn api_schema() {
1684 let planner =
1685 Planner::<serde_json::Value>::new(SCHEMA.to_string(), QueryPlannerConfig::default())
1686 .await
1687 .unwrap();
1688
1689 let api_schema = planner.api_schema().await.unwrap();
1690 insta::assert_snapshot!(api_schema.schema);
1691 }
1692 static INTROSPECTION: &str = r#"
1694query IntrospectionQuery {
1695__schema {
1696 queryType {
1697 name
1698 }
1699 mutationType {
1700 name
1701 }
1702 subscriptionType {
1703 name
1704 }
1705 types {
1706 ...FullType
1707 }
1708 directives {
1709 name
1710 description
1711 locations
1712 args {
1713 ...InputValue
1714 }
1715 }
1716}
1717}
1718
1719fragment FullType on __Type {
1720kind
1721name
1722description
1723
1724fields(includeDeprecated: true) {
1725 name
1726 description
1727 args {
1728 ...InputValue
1729 }
1730 type {
1731 ...TypeRef
1732 }
1733 isDeprecated
1734 deprecationReason
1735}
1736inputFields {
1737 ...InputValue
1738}
1739interfaces {
1740 ...TypeRef
1741}
1742enumValues(includeDeprecated: true) {
1743 name
1744 description
1745 isDeprecated
1746 deprecationReason
1747}
1748possibleTypes {
1749 ...TypeRef
1750}
1751}
1752
1753fragment InputValue on __InputValue {
1754name
1755description
1756type {
1757 ...TypeRef
1758}
1759defaultValue
1760}
1761
1762fragment TypeRef on __Type {
1763kind
1764name
1765ofType {
1766 kind
1767 name
1768 ofType {
1769 kind
1770 name
1771 ofType {
1772 kind
1773 name
1774 ofType {
1775 kind
1776 name
1777 ofType {
1778 kind
1779 name
1780 ofType {
1781 kind
1782 name
1783 ofType {
1784 kind
1785 name
1786 }
1787 }
1788 }
1789 }
1790 }
1791 }
1792}
1793}
1794"#;
1795 #[tokio::test]
1796 async fn introspect() {
1797 let planner =
1798 Planner::<serde_json::Value>::new(SCHEMA.to_string(), QueryPlannerConfig::default())
1799 .await
1800 .unwrap();
1801
1802 let introspection_response = planner.introspect(INTROSPECTION.to_string()).await.unwrap();
1803 insta::assert_json_snapshot!(serde_json::to_value(introspection_response).unwrap());
1804 }
1805
1806 #[tokio::test]
1807 async fn planner_update() {
1808 let query = "{ me { id name {first } reviews { id author { name { first } } body } } }";
1809 let planner = Planner::<serde_json::Value>::new(
1810 SCHEMA_WITHOUT_REVIEW_BODY.to_string(),
1811 QueryPlannerConfig::default(),
1812 )
1813 .await
1814 .unwrap();
1815 let query_plan1 = planner
1816 .plan(query.to_string(), None, PlanOptions::default())
1817 .await
1818 .unwrap()
1819 .into_result()
1820 .unwrap_err();
1821 insta::assert_snapshot!(&format!("{query_plan1:#?}"));
1822 let api_schema1 = planner.api_schema().await.unwrap();
1823 insta::assert_snapshot!(api_schema1.schema);
1824 let introspected_schema1 = planner.introspect(INTROSPECTION.to_string()).await.unwrap();
1825
1826 let updated_planner = planner
1827 .update(SCHEMA.to_string(), QueryPlannerConfig::default())
1828 .await
1829 .unwrap();
1830 let query_plan2 = updated_planner
1831 .plan(query.to_string(), None, PlanOptions::default())
1832 .await
1833 .unwrap()
1834 .into_result()
1835 .unwrap();
1836 insta::assert_snapshot!(serde_json::to_string_pretty(&query_plan2.data).unwrap());
1837 let api_schema2 = updated_planner.api_schema().await.unwrap();
1838 insta::assert_snapshot!(api_schema2.schema);
1839
1840 assert_eq!(
1842 planner.introspect(INTROSPECTION.to_string()).await.unwrap(),
1843 introspected_schema1
1844 );
1845
1846 let introspected_schema2 = updated_planner
1847 .introspect(INTROSPECTION.to_string())
1848 .await
1849 .unwrap();
1850 assert_ne!(introspected_schema1, introspected_schema2);
1851
1852 drop(planner);
1854
1855 assert_eq!(
1856 query_plan2.data,
1857 updated_planner
1858 .plan(query.to_string(), None, PlanOptions::default())
1859 .await
1860 .unwrap()
1861 .into_result()
1862 .unwrap()
1863 .data
1864 );
1865 }
1866
1867 #[tokio::test]
1868 async fn get_operation_signature() {
1869 let planner =
1870 Planner::<serde_json::Value>::new(SCHEMA.to_string(), QueryPlannerConfig::default())
1871 .await
1872 .unwrap();
1873
1874 let signature = planner
1875 .operation_signature(NAMED_QUERY.to_string(), None)
1876 .await
1877 .unwrap();
1878 insta::assert_snapshot!(signature);
1879 }
1880
1881 #[tokio::test]
1882 async fn subgraphs() {
1883 let planner =
1884 Planner::<serde_json::Value>::new(SCHEMA.to_string(), QueryPlannerConfig::default())
1885 .await
1886 .unwrap();
1887
1888 let subgraphs = planner.subgraphs().await.unwrap();
1889 let subgraphs: BTreeMap<String, String> = subgraphs.into_iter().collect();
1890 for schema in subgraphs.values() {
1891 insta::assert_snapshot!(schema);
1892 }
1893 }
1894
1895 #[tokio::test]
1896 async fn heap_statistics() {
1897 let planner =
1898 Planner::<serde_json::Value>::new(SCHEMA.to_string(), QueryPlannerConfig::default())
1899 .await
1900 .unwrap();
1901
1902 let _subgraphs = planner.subgraphs().await.unwrap();
1903 let statistics = planner.get_heap_statistics().await.unwrap();
1904
1905 println!("statistics: {statistics:?}");
1906 }
1907}
1908
1909#[cfg(test)]
1910mod planning_error {
1911 use std::collections::HashMap;
1912
1913 use crate::planner::PlanError;
1914 use crate::planner::PlanErrorExtensions;
1915 use crate::planner::ReferencedFieldsForType;
1916 use crate::planner::UsageReporting;
1917
1918 #[test]
1919 #[should_panic(
1920 expected = "Result::unwrap()` on an `Err` value: Error(\"missing field `extensions`\", line: 1, column: 2)"
1921 )]
1922 fn deserialize_empty_planning_error() {
1923 let raw = "{}";
1924 serde_json::from_str::<PlanError>(raw).unwrap();
1925 }
1926
1927 #[test]
1928 #[should_panic(
1929 expected = "Result::unwrap()` on an `Err` value: Error(\"missing field `extensions`\", line: 1, column: 44)"
1930 )]
1931 fn deserialize_planning_error_missing_extension() {
1932 let raw = r#"{ "message": "something terrible happened" }"#;
1933 serde_json::from_str::<PlanError>(raw).unwrap();
1934 }
1935
1936 #[test]
1937 fn deserialize_planning_error_with_extension() {
1938 let raw = r#"{
1939 "message": "something terrible happened",
1940 "extensions": {
1941 "code": "E_TEST_CASE"
1942 }
1943 }"#;
1944
1945 let expected = PlanError {
1946 message: Some("something terrible happened".to_string()),
1947 extensions: Some(PlanErrorExtensions {
1948 code: "E_TEST_CASE".to_string(),
1949 exception: None,
1950 }),
1951 validation_error: false,
1952 };
1953
1954 assert_eq!(expected, serde_json::from_str(raw).unwrap());
1955 }
1956
1957 #[test]
1958 fn deserialize_planning_error_with_empty_object_extension() {
1959 let raw = r#"{
1960 "extensions": {}
1961 }"#;
1962 let expected = PlanError {
1963 message: None,
1964 extensions: None,
1965 validation_error: false,
1966 };
1967
1968 assert_eq!(expected, serde_json::from_str(raw).unwrap());
1969 }
1970
1971 #[test]
1972 fn deserialize_planning_error_with_null_extension() {
1973 let raw = r#"{
1974 "extensions": null
1975 }"#;
1976 let expected = PlanError {
1977 message: None,
1978 extensions: None,
1979 validation_error: false,
1980 };
1981
1982 assert_eq!(expected, serde_json::from_str(raw).unwrap());
1983 }
1984
1985 #[test]
1986 fn deserialize_referenced_fields_for_type_defaults() {
1987 let raw = r#"{}"#;
1988 let expected = ReferencedFieldsForType {
1989 field_names: Vec::new(),
1990 is_interface: false,
1991 };
1992
1993 assert_eq!(expected, serde_json::from_str(raw).unwrap());
1994 }
1995
1996 #[test]
1997 fn deserialize_usage_reporting_with_defaults() {
1998 let raw = r#"{
1999 "statsReportKey": "thisIsAtest"
2000 }"#;
2001 let expected = UsageReporting {
2002 stats_report_key: "thisIsAtest".to_string(),
2003 referenced_fields_by_type: HashMap::new(),
2004 };
2005
2006 assert_eq!(expected, serde_json::from_str(raw).unwrap());
2007 }
2008}
2009
2010#[cfg(test)]
2011mod error_display {
2012 use super::*;
2013
2014 #[test]
2015 fn error_on_core_in_v0_1_display() {
2016 let expected = r#"one or more checks failed
2017caused by
2018the `for:` argument is unsupported by version v0.1 of the core spec. Please upgrade to at least @core v0.2 (https://specs.apollo.dev/core/v0.2).
2019feature https://specs.apollo.dev/something-unsupported/v0.1 is for: SECURITY but is unsupported"#;
2020
2021 let error_to_display: PlannerError = WorkerGraphQLError {
2022 name: "CheckFailed".to_string(),
2023 message: "one or more checks failed".to_string(),
2024 locations: Default::default(),
2025 extensions: Some(PlanErrorExtensions {
2026 code: "CheckFailed".to_string(),
2027 exception: None
2028 }),
2029 original_error: None,
2030 causes: vec![
2031 Box::new(WorkerError {
2032 message: Some("the `for:` argument is unsupported by version v0.1 of the core spec. Please upgrade to at least @core v0.2 (https://specs.apollo.dev/core/v0.2).".to_string()),
2033 name: None,
2034 stack: None,
2035 extensions: Some(PlanErrorExtensions { code: "ForUnsupported".to_string(), exception: None }),
2036 locations: vec![Location { line: 2, column: 1 }, Location { line: 3, column: 1 }, Location { line: 4, column: 1 }]
2037 }),
2038 Box::new(WorkerError {
2039 message: Some("feature https://specs.apollo.dev/something-unsupported/v0.1 is for: SECURITY but is unsupported".to_string()),
2040 name: None,
2041 stack: None,
2042 extensions: Some(PlanErrorExtensions { code: "UnsupportedFeature".to_string(), exception: None }),
2043 locations: vec![Location { line: 4, column: 1 }]
2044 })
2045 ],
2046 validation_error: false,
2047 }.into();
2048
2049 assert_eq!(expected.to_string(), error_to_display.to_string());
2050 }
2051
2052 #[test]
2053 fn unsupported_feature_for_execution_display() {
2054 let expected = r#"one or more checks failed
2055caused by
2056feature https://specs.apollo.dev/unsupported-feature/v0.1 is for: EXECUTION but is unsupported"#;
2057
2058 let error_to_display: PlannerError = WorkerGraphQLError {
2059 name: "CheckFailed".to_string(),
2060 message: "one or more checks failed".to_string(),
2061 locations: Default::default(),
2062 extensions: Some(PlanErrorExtensions {
2063 code: "CheckFailed".to_string(),
2064 exception: None
2065 }),
2066 original_error: None,
2067 causes: vec![
2068 Box::new(WorkerError {
2069 message: Some("feature https://specs.apollo.dev/unsupported-feature/v0.1 is for: EXECUTION but is unsupported".to_string()),
2070 name: None,
2071 stack: None,
2072 extensions: Some(PlanErrorExtensions { code: "UnsupportedFeature".to_string(), exception: None }),
2073 locations: vec![Location { line: 4, column: 9 }]
2074 }),
2075 ],
2076 validation_error: false,
2077 }.into();
2078
2079 assert_eq!(expected.to_string(), error_to_display.to_string());
2080 }
2081
2082 #[test]
2083 fn unsupported_feature_for_security_display() {
2084 let expected = r#"one or more checks failed
2085caused by
2086feature https://specs.apollo.dev/unsupported-feature/v0.1 is for: SECURITY but is unsupported"#;
2087
2088 let error_to_display: PlannerError = WorkerGraphQLError {
2089 name: "CheckFailed".into(),
2090 message: "one or more checks failed".to_string(),
2091 locations: vec![],
2092 extensions: Some(PlanErrorExtensions {
2093 code: "CheckFailed".to_string(),
2094 exception: None
2095 }),
2096 original_error: None,
2097 causes: vec![Box::new(WorkerError {
2098 message: Some("feature https://specs.apollo.dev/unsupported-feature/v0.1 is for: SECURITY but is unsupported".to_string()),
2099 extensions: Some(PlanErrorExtensions {
2100 code: "UnsupportedFeature".to_string(),
2101 exception: None
2102 }),
2103 name: None,
2104 stack: None,
2105 locations: vec![Location { line: 4, column: 9 }]
2106 })],
2107 validation_error: false,
2108 }
2109 .into();
2110
2111 assert_eq!(expected.to_string(), error_to_display.to_string());
2112 }
2113
2114 #[tokio::test]
2115 async fn defer_with_fragment() {
2116 let schema = r#"
2117 schema
2118 @link(url: "https://specs.apollo.dev/link/v1.0")
2119 @link(url: "https://specs.apollo.dev/join/v0.2", for: EXECUTION)
2120 {
2121 query: Query
2122 }
2123
2124 directive @join__field(graph: join__Graph!, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION
2125 directive @join__graph(name: String!, url: String!) on ENUM_VALUE
2126 directive @join__implements(graph: join__Graph!, interface: String!) repeatable on OBJECT | INTERFACE
2127 directive @join__type(graph: join__Graph!, key: join__FieldSet, extension: Boolean! = false, resolvable: Boolean! = true) repeatable on OBJECT | INTERFACE | UNION | ENUM | INPUT_OBJECT | SCALAR
2128 directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA
2129
2130 scalar link__Import
2131 enum link__Purpose {
2132 SECURITY
2133 EXECUTION
2134 }
2135
2136 type Computer
2137 @join__type(graph: COMPUTERS)
2138 {
2139 id: ID!
2140 errorField: String
2141 nonNullErrorField: String!
2142 }
2143
2144 scalar join__FieldSet
2145
2146 enum join__Graph {
2147 COMPUTERS @join__graph(name: "computers", url: "http://localhost:4001/")
2148 }
2149
2150
2151 type Query
2152 @join__type(graph: COMPUTERS)
2153 {
2154 computer(id: ID!): Computer
2155 }"#;
2156
2157 let planner = Planner::<serde_json::Value>::new(
2158 schema.to_string(),
2159 QueryPlannerConfig {
2160 generate_query_fragments: Default::default(),
2161 incremental_delivery: Some(IncrementalDeliverySupport {
2162 enable_defer: Some(true),
2163 }),
2164 graphql_validation: true,
2165 reuse_query_fragments: None,
2166 debug: Default::default(),
2167 type_conditioned_fetching: false,
2168 },
2169 )
2170 .await
2171 .unwrap();
2172
2173 let plan_response = planner
2174 .plan(
2175 r#"query {
2176 computer(id: "Computer1") {
2177 id
2178 ...ComputerErrorField @defer
2179 }
2180 }
2181 fragment ComputerErrorField on Computer {
2182 errorField
2183 }"#
2184 .to_string(),
2185 None,
2186 PlanOptions::default(),
2187 )
2188 .await
2189 .unwrap()
2190 .data
2191 .unwrap();
2192
2193 insta::assert_snapshot!(serde_json::to_string_pretty(&plan_response).unwrap());
2194 }
2195
2196 #[tokio::test]
2197 async fn defer_query_plan() {
2198 let schema = r#"schema
2199 @core(feature: "https://specs.apollo.dev/core/v0.1")
2200 @core(feature: "https://specs.apollo.dev/join/v0.1")
2201 @core(feature: "https://specs.apollo.dev/inaccessible/v0.1")
2202 {
2203 query: Query
2204 }
2205 directive @core(feature: String!) repeatable on SCHEMA
2206 directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet) on FIELD_DEFINITION
2207 directive @join__type(graph: join__Graph!, key: join__FieldSet) repeatable on OBJECT | INTERFACE
2208 directive @join__owner(graph: join__Graph!) on OBJECT | INTERFACE
2209 directive @join__graph(name: String!, url: String!) on ENUM_VALUE
2210 directive @inaccessible on OBJECT | FIELD_DEFINITION | INTERFACE | UNION
2211 scalar join__FieldSet
2212 enum join__Graph {
2213 USER @join__graph(name: "user", url: "http://localhost:4001/graphql")
2214 ORGA @join__graph(name: "orga", url: "http://localhost:4002/graphql")
2215 }
2216 type Query {
2217 currentUser: User @join__field(graph: USER)
2218 }
2219 type User
2220 @join__owner(graph: USER)
2221 @join__type(graph: ORGA, key: "id")
2222 @join__type(graph: USER, key: "id"){
2223 id: ID!
2224 name: String
2225 activeOrganization: Organization
2226 }
2227 type Organization
2228 @join__owner(graph: ORGA)
2229 @join__type(graph: ORGA, key: "id")
2230 @join__type(graph: USER, key: "id") {
2231 id: ID
2232 creatorUser: User
2233 name: String
2234 nonNullId: ID!
2235 suborga: [Organization]
2236 }"#;
2237
2238 let planner = Planner::<serde_json::Value>::new(
2239 schema.to_string(),
2240 QueryPlannerConfig {
2241 generate_query_fragments: Default::default(),
2242 incremental_delivery: Some(IncrementalDeliverySupport {
2243 enable_defer: Some(true),
2244 }),
2245 graphql_validation: true,
2246 reuse_query_fragments: None,
2247 debug: Default::default(),
2248 type_conditioned_fetching: false,
2249 },
2250 )
2251 .await
2252 .unwrap();
2253
2254 insta::assert_snapshot!(serde_json::to_string_pretty(&planner
2255 .plan(
2256 "query { currentUser { activeOrganization { id suborga { id ...@defer { nonNullId } } } } }"
2257 .to_string(),
2258 None,
2259 PlanOptions::default(),
2260 )
2261 .await
2262 .unwrap()
2263 .data
2264 .unwrap()).unwrap());
2265 }
2266
2267 #[tokio::test]
2268 async fn propagate_internal_qp_errors() {
2269 let schema = r#"
2270 schema
2271 @link(url: "https://specs.apollo.dev/link/v1.0")
2272 @link(url: "https://specs.apollo.dev/join/v0.2", for: EXECUTION)
2273 {
2274 query: Query
2275 subscription: Subscription
2276 }
2277
2278 directive @join__field(graph: join__Graph!, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION
2279 directive @join__graph(name: String!, url: String!) on ENUM_VALUE
2280 directive @join__implements(graph: join__Graph!, interface: String!) repeatable on OBJECT | INTERFACE
2281 directive @join__type(graph: join__Graph!, key: join__FieldSet, extension: Boolean! = false, resolvable: Boolean! = true) repeatable on OBJECT | INTERFACE | UNION | ENUM | INPUT_OBJECT | SCALAR
2282 directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA
2283
2284 scalar link__Import
2285 enum link__Purpose {
2286 SECURITY
2287 EXECUTION
2288 }
2289
2290 type Computer
2291 @join__type(graph: COMPUTERS)
2292 {
2293 id: ID!
2294 errorField: String
2295 nonNullErrorField: String!
2296 gpus: [GPU]
2297 }
2298
2299 type GPU @join__type(graph: COMPUTERS) @join__type(graph: GPUS) {
2300 id: ID!
2301 wattage: Int! @join__field(graph: GPUS)
2302 }
2303
2304 scalar join__FieldSet
2305
2306 enum join__Graph {
2307 COMPUTERS @join__graph(name: "computers", url: "http://localhost:4001/")
2308 GPUS @join__graph(name: "gpus", url: "http://localhost:4002/")
2309 }
2310
2311
2312 type Query
2313 @join__type(graph: COMPUTERS)
2314 {
2315 computer(id: ID!): Computer
2316 }
2317 type Subscription @join__type(graph: COMPUTERS) {
2318 computer(id: ID!): Computer
2319 }"#;
2320
2321 let planner = Planner::<serde_json::Value>::new(
2322 schema.to_string(),
2323 QueryPlannerConfig {
2324 incremental_delivery: Some(IncrementalDeliverySupport {
2325 enable_defer: Some(true),
2326 }),
2327 graphql_validation: true,
2328 reuse_query_fragments: None,
2329 generate_query_fragments: None,
2330 debug: Default::default(),
2331 type_conditioned_fetching: false,
2332 },
2333 )
2334 .await
2335 .unwrap();
2336
2337 insta::assert_snapshot!(serde_json::to_string_pretty(
2338 &planner
2339 .plan(
2340 "subscription { computer(id: 1) { ... @defer { gpus { wattage } } } }"
2341 .to_string(),
2342 None,
2343 PlanOptions::default(),
2344 )
2345 .await
2346 .unwrap()
2347 .errors
2348 .unwrap()
2349 )
2350 .unwrap());
2351 }
2352
2353 static TYPED_CONDITION_SCHEMA: &str = include_str!("testdata/typed_conditions.graphql");
2354
2355 #[tokio::test]
2356 async fn typed_condition_field_merging_disabled() {
2357 let planner = Planner::<serde_json::Value>::new(
2358 TYPED_CONDITION_SCHEMA.to_string(),
2359 QueryPlannerConfig {
2360 generate_query_fragments: Default::default(),
2361 incremental_delivery: Some(IncrementalDeliverySupport {
2362 enable_defer: Some(true),
2363 }),
2364 graphql_validation: true,
2365 reuse_query_fragments: None,
2366 debug: Default::default(),
2367 type_conditioned_fetching: false,
2368 },
2369 )
2370 .await
2371 .unwrap();
2372
2373 insta::assert_snapshot!(serde_json::to_string_pretty(
2374 &planner
2375 .plan(
2376 "query Search($movieParams: String, $articleParams: String) {
2377 search {
2378 __typename
2379 ... on MovieResult {
2380 id
2381 sections {
2382 ... on EntityCollectionSection {
2383 id
2384 artwork(params: $movieParams)
2385 }
2386 }
2387 }
2388 ... on ArticleResult {
2389 id
2390 sections {
2391 ... on EntityCollectionSection {
2392 id
2393 artwork(params: $articleParams)
2394 title
2395 }
2396 }
2397 }
2398 }
2399 }"
2400 .to_string(),
2401 None,
2402 PlanOptions::default(),
2403 )
2404 .await
2405 .unwrap()
2406 .data
2407 .unwrap()
2408 )
2409 .unwrap());
2410 }
2411 #[tokio::test]
2412 async fn typed_condition_field_merging_enabled() {
2413 let planner = Planner::<serde_json::Value>::new(
2414 TYPED_CONDITION_SCHEMA.to_string(),
2415 QueryPlannerConfig {
2416 generate_query_fragments: Default::default(),
2417 incremental_delivery: Some(IncrementalDeliverySupport {
2418 enable_defer: Some(true),
2419 }),
2420 graphql_validation: true,
2421 reuse_query_fragments: None,
2422 debug: Default::default(),
2423 type_conditioned_fetching: true,
2424 },
2425 )
2426 .await
2427 .unwrap();
2428
2429 insta::assert_snapshot!(serde_json::to_string_pretty(
2430 &planner
2431 .plan(
2432 "query Search($movieParams: String, $articleParams: String) {
2433 search {
2434 __typename
2435 ... on MovieResult {
2436 id
2437 sections {
2438 ... on EntityCollectionSection {
2439 id
2440 artwork(params: $movieParams)
2441 }
2442 }
2443 }
2444 ... on ArticleResult {
2445 id
2446 sections {
2447 ... on EntityCollectionSection {
2448 id
2449 artwork(params: $articleParams)
2450 title
2451 }
2452 }
2453 }
2454 }
2455 }"
2456 .to_string(),
2457 None,
2458 PlanOptions {
2459 ..Default::default()
2460 },
2461 )
2462 .await
2463 .unwrap()
2464 .data
2465 .unwrap()
2466 )
2467 .unwrap());
2468 }
2469
2470 #[tokio::test]
2471 async fn extracts_cost_directives() {
2472 let schema = include_str!("testdata/custom_cost_schema.graphql");
2473 let planner = Planner::<serde_json::Value>::new(schema.to_string(), Default::default())
2474 .await
2475 .expect("can create planner");
2476 let subgraphs = planner.subgraphs().await.expect("can extract subgraphs");
2477
2478 for (name, schema) in subgraphs {
2479 insta::assert_snapshot!(name, schema);
2480 }
2481 }
2482}