use crate::worker::JsWorker;
use serde::{de::DeserializeOwned, Deserialize, Serialize};
use std::collections::HashMap;
use std::fmt::{Debug, Display, Formatter};
use std::marker::PhantomData;
use std::sync::Arc;
use thiserror::Error;
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct QueryPlanOptions {
pub auto_fragmentization: bool,
}
impl QueryPlanOptions {
pub fn default() -> QueryPlanOptions {
QueryPlanOptions {
auto_fragmentization: false,
}
}
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct OperationalContext {
pub schema: String,
pub query: String,
pub operation_name: String,
}
#[derive(Debug, Error, Serialize, Deserialize, PartialEq, Eq)]
pub struct PlanError {
pub message: Option<String>,
#[serde(deserialize_with = "none_only_if_value_is_null_or_empty_object")]
pub extensions: Option<PlanErrorExtensions>,
}
fn none_only_if_value_is_null_or_empty_object<'de, D, T>(data: D) -> Result<Option<T>, D::Error>
where
D: serde::de::Deserializer<'de>,
T: serde::Deserialize<'de>,
{
#[derive(Deserialize)]
#[serde(untagged)]
enum OptionOrValue<T> {
Opt(Option<T>),
Val(serde_json::value::Value),
}
let as_option_or_value: Result<OptionOrValue<T>, D::Error> =
serde::Deserialize::deserialize(data);
match as_option_or_value {
Ok(OptionOrValue::Opt(t)) => Ok(t),
Ok(OptionOrValue::Val(obj)) => {
if let serde_json::value::Value::Object(o) = &obj {
if o.is_empty() {
return Ok(None);
}
}
Err(serde::de::Error::custom(format!(
"invalid neither null nor empty object: found {:?}",
obj,
)))
}
Err(e) => Err(e),
}
}
impl Display for PlanError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
if let Some(msg) = &self.message {
f.write_fmt(format_args!("{code}: {msg}", code = self.code(), msg = msg))
} else {
f.write_str(self.code())
}
}
}
#[derive(Debug, Serialize, Deserialize, PartialEq, Eq)]
pub struct PlanErrorExtensions {
pub code: String,
}
impl PlanError {
pub fn code(&self) -> &str {
match self.extensions {
Some(ref ext) => &*ext.code,
None => "UNKNOWN",
}
}
}
#[derive(Deserialize, Debug)]
pub struct BridgeSetupResult<T> {
pub data: Option<T>,
pub errors: Option<Vec<PlannerError>>,
}
#[derive(Serialize, Deserialize, Debug, PartialEq, Eq)]
pub struct Location {
pub line: u32,
pub column: u32,
}
#[derive(Serialize, Deserialize, Debug, PartialEq, Eq)]
#[serde(untagged)]
pub enum PlannerError {
WorkerGraphQLError(WorkerGraphQLError),
WorkerError(WorkerError),
}
impl From<WorkerGraphQLError> for PlannerError {
fn from(e: WorkerGraphQLError) -> Self {
Self::WorkerGraphQLError(e)
}
}
impl From<WorkerError> for PlannerError {
fn from(e: WorkerError) -> Self {
Self::WorkerError(e)
}
}
impl std::fmt::Display for PlannerError {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
Self::WorkerGraphQLError(graphql_error) => {
write!(f, "{}", graphql_error)
}
Self::WorkerError(error) => {
write!(f, "{}", error)
}
}
}
}
#[derive(Serialize, Deserialize, Debug, PartialEq, Eq)]
pub struct WorkerError {
pub message: Option<String>,
pub name: Option<String>,
pub stack: Option<String>,
pub extensions: Option<PlanErrorExtensions>,
#[serde(default)]
pub locations: Vec<Location>,
}
impl std::fmt::Display for WorkerError {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(
f,
"{}",
self.message
.clone()
.unwrap_or_else(|| "unknown error".to_string())
)
}
}
#[derive(Serialize, Deserialize, Debug, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct WorkerGraphQLError {
pub name: String,
pub message: String,
#[serde(default)]
pub locations: Vec<Location>,
pub extensions: Option<PlanErrorExtensions>,
pub original_error: Option<Box<WorkerError>>,
#[serde(default)]
pub causes: Vec<Box<WorkerError>>,
}
impl std::fmt::Display for WorkerGraphQLError {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(
f,
"{}\ncaused by\n{}",
self.message,
self.causes
.iter()
.map(std::string::ToString::to_string)
.collect::<Vec<_>>()
.join("\n")
)
}
}
#[derive(Deserialize, Serialize, Debug, PartialEq, Eq, Clone)]
#[serde(rename_all = "camelCase")]
pub struct ReferencedFieldsForType {
#[serde(default)]
pub field_names: Vec<String>,
#[serde(default)]
pub is_interface: bool,
}
#[derive(Deserialize, Serialize, Debug, PartialEq, Eq, Clone)]
#[serde(rename_all = "camelCase")]
pub struct UsageReporting {
pub stats_report_key: String,
#[serde(default)]
pub referenced_fields_by_type: HashMap<String, ReferencedFieldsForType>,
}
#[derive(Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct PlanResult<T> {
pub data: Option<T>,
pub usage_reporting: UsageReporting,
pub errors: Option<Vec<PlanError>>,
}
#[derive(Debug)]
pub struct PlanSuccess<T> {
pub data: T,
pub usage_reporting: UsageReporting,
}
#[derive(Debug, Clone)]
pub struct PlanErrors {
pub errors: Arc<Vec<PlanError>>,
pub usage_reporting: UsageReporting,
}
impl std::fmt::Display for PlanErrors {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_fmt(format_args!(
"query validation errors: {}",
self.errors
.iter()
.map(|e| e
.message
.clone()
.unwrap_or_else(|| "UNKNWON ERROR".to_string()))
.collect::<Vec<String>>()
.join(", ")
))
}
}
impl<T> PlanResult<T>
where
T: DeserializeOwned + Send + Debug + 'static,
{
pub fn into_result(self) -> Result<PlanSuccess<T>, PlanErrors> {
let usage_reporting = self.usage_reporting;
if let Some(data) = self.data {
Ok(PlanSuccess {
data,
usage_reporting,
})
} else {
let errors = Arc::new(self.errors.unwrap_or_else(|| {
vec![PlanError {
message: Some("an unknown error occured".to_string()),
extensions: None,
}]
}));
Err(PlanErrors {
errors,
usage_reporting,
})
}
}
}
pub struct Planner<T>
where
T: DeserializeOwned + Send + Debug + 'static,
{
worker: Arc<JsWorker>,
t: PhantomData<T>,
}
impl<T> Debug for Planner<T>
where
T: DeserializeOwned + Send + Debug + 'static,
{
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Planner").finish()
}
}
impl<T> Planner<T>
where
T: DeserializeOwned + Send + Debug + 'static,
{
pub async fn new(
schema: String,
config: QueryPlannerConfig,
) -> Result<Self, Vec<PlannerError>> {
let worker = JsWorker::new(include_str!("../bundled/plan_worker.js"));
let worker_is_set_up = worker
.request::<PlanCmd, BridgeSetupResult<serde_json::Value>>(PlanCmd::UpdateSchema {
schema,
config,
})
.await
.map_err(|e| {
vec![WorkerError {
name: Some("planner setup error".to_string()),
message: Some(e.to_string()),
stack: None,
extensions: None,
locations: Default::default(),
}
.into()]
});
match worker_is_set_up {
Err(setup_error) => {
let _ = worker
.request::<PlanCmd, serde_json::Value>(PlanCmd::Exit)
.await;
return Err(setup_error);
}
Ok(setup) => {
if let Some(error) = setup.errors {
let _ = worker.send(PlanCmd::Exit).await;
return Err(error);
}
}
}
let worker = Arc::new(worker);
Ok(Self {
worker,
t: Default::default(),
})
}
pub async fn plan(
&self,
query: String,
operation_name: Option<String>,
) -> Result<PlanResult<T>, crate::error::Error> {
self.worker
.request(PlanCmd::Plan {
query,
operation_name,
})
.await
}
}
impl<T> Drop for Planner<T>
where
T: DeserializeOwned + Send + Debug + 'static,
{
fn drop(&mut self) {
let worker_clone = self.worker.clone();
let _ = std::thread::spawn(|| {
let runtime = tokio::runtime::Builder::new_current_thread()
.build()
.unwrap();
let _ = runtime.block_on(async move { worker_clone.send(PlanCmd::Exit).await });
})
.join();
}
}
#[derive(Serialize, Debug, Clone)]
#[serde(tag = "kind")]
enum PlanCmd {
UpdateSchema {
schema: String,
config: QueryPlannerConfig,
},
#[serde(rename_all = "camelCase")]
Plan {
query: String,
operation_name: Option<String>,
},
Exit,
}
#[derive(Serialize, Debug, Clone)]
#[serde(rename_all = "camelCase")]
pub struct QueryPlannerConfig {
pub incremental_delivery: Option<IncrementalDeliverySupport>,
}
impl Default for QueryPlannerConfig {
fn default() -> Self {
Self {
incremental_delivery: Some(IncrementalDeliverySupport {
enable_defer: Some(false),
}),
}
}
}
#[derive(Serialize, Debug, Clone)]
#[serde(rename_all = "camelCase")]
pub struct IncrementalDeliverySupport {
#[serde(default)]
pub enable_defer: Option<bool>,
}
#[cfg(test)]
mod tests {
use super::*;
use futures::stream::{self, StreamExt};
const QUERY: &str = include_str!("testdata/query.graphql");
const QUERY2: &str = include_str!("testdata/query2.graphql");
const MULTIPLE_QUERIES: &str = include_str!("testdata/query_with_multiple_operations.graphql");
const NO_OPERATION: &str = include_str!("testdata/no_operation.graphql");
const MULTIPLE_ANONYMOUS_QUERIES: &str =
include_str!("testdata/query_with_multiple_anonymous_operations.graphql");
const NAMED_QUERY: &str = include_str!("testdata/named_query.graphql");
const SCHEMA: &str = include_str!("testdata/schema.graphql");
const CORE_IN_V0_1: &str = include_str!("testdata/core_in_v0.1.graphql");
const UNSUPPORTED_FEATURE: &str = include_str!("testdata/unsupported_feature.graphql");
const UNSUPPORTED_FEATURE_FOR_EXECUTION: &str =
include_str!("testdata/unsupported_feature_for_execution.graphql");
const UNSUPPORTED_FEATURE_FOR_SECURITY: &str =
include_str!("testdata/unsupported_feature_for_security.graphql");
#[tokio::test]
async fn anonymous_query_works() {
let planner =
Planner::<serde_json::Value>::new(SCHEMA.to_string(), QueryPlannerConfig::default())
.await
.unwrap();
let payload = planner
.plan(QUERY.to_string(), None)
.await
.unwrap()
.into_result()
.unwrap();
insta::assert_snapshot!(serde_json::to_string_pretty(&payload.data).unwrap());
insta::with_settings!({sort_maps => true}, {
insta::assert_json_snapshot!(payload.usage_reporting);
});
}
#[tokio::test]
async fn named_query_works() {
let planner =
Planner::<serde_json::Value>::new(SCHEMA.to_string(), QueryPlannerConfig::default())
.await
.unwrap();
let payload = planner
.plan(NAMED_QUERY.to_string(), None)
.await
.unwrap()
.into_result()
.unwrap();
insta::assert_snapshot!(serde_json::to_string_pretty(&payload.data).unwrap());
insta::with_settings!({sort_maps => true}, {
insta::assert_json_snapshot!(payload.usage_reporting);
});
}
#[tokio::test]
async fn named_query_with_several_choices_works() {
let planner =
Planner::<serde_json::Value>::new(SCHEMA.to_string(), QueryPlannerConfig::default())
.await
.unwrap();
let payload = planner
.plan(
MULTIPLE_QUERIES.to_string(),
Some("MyFirstName".to_string()),
)
.await
.unwrap()
.into_result()
.unwrap();
insta::assert_snapshot!(serde_json::to_string_pretty(&payload.data).unwrap());
insta::with_settings!({sort_maps => true}, {
insta::assert_json_snapshot!(payload.usage_reporting);
});
}
#[tokio::test]
async fn named_query_with_operation_name_works() {
let planner =
Planner::<serde_json::Value>::new(SCHEMA.to_string(), QueryPlannerConfig::default())
.await
.unwrap();
let payload = planner
.plan(
NAMED_QUERY.to_string(),
Some("MyFirstAndLastName".to_string()),
)
.await
.unwrap()
.into_result()
.unwrap();
insta::assert_snapshot!(serde_json::to_string_pretty(&payload.data).unwrap());
insta::with_settings!({sort_maps => true}, {
insta::assert_json_snapshot!(payload.usage_reporting);
});
}
#[tokio::test]
async fn parse_errors_return_the_right_usage_reporting_data() {
let planner =
Planner::<serde_json::Value>::new(SCHEMA.to_string(), QueryPlannerConfig::default())
.await
.unwrap();
let payload = planner
.plan("this query will definitely not parse".to_string(), None)
.await
.unwrap()
.into_result()
.unwrap_err();
assert_eq!(
"Syntax Error: Unexpected Name \"this\".",
payload.errors[0].message.as_ref().clone().unwrap()
);
assert_eq!(
"## GraphQLParseFailure\n",
payload.usage_reporting.stats_report_key
);
}
#[tokio::test]
async fn validation_errors_return_the_right_usage_reporting_data() {
let planner =
Planner::<serde_json::Value>::new(SCHEMA.to_string(), QueryPlannerConfig::default())
.await
.unwrap();
let payload = planner
.plan(
"\
fragment thatUserFragment1 on User {
id
...thatUserFragment2
}
fragment thatUserFragment2 on User {
id
...thatUserFragment1
}
query { me { id ...thatUserFragment1 } }"
.to_string(),
None,
)
.await
.unwrap()
.into_result()
.unwrap_err();
assert_eq!(
"Cannot spread fragment \"thatUserFragment1\" within itself via \"thatUserFragment2\".",
payload.errors[0].message.as_ref().clone().unwrap()
);
assert_eq!(
"## GraphQLValidationFailure\n",
payload.usage_reporting.stats_report_key
);
}
#[tokio::test]
async fn unknown_operation_name_errors_return_the_right_usage_reporting_data() {
let planner =
Planner::<serde_json::Value>::new(SCHEMA.to_string(), QueryPlannerConfig::default())
.await
.unwrap();
let payload = planner
.plan(
QUERY.to_string(),
Some("ThisOperationNameDoesntExist".to_string()),
)
.await
.unwrap()
.into_result()
.unwrap_err();
assert_eq!(
"Unknown operation named \"ThisOperationNameDoesntExist\"",
payload.errors[0].message.as_ref().clone().unwrap()
);
assert_eq!(
"## GraphQLUnknownOperationName\n",
payload.usage_reporting.stats_report_key
);
}
#[tokio::test]
async fn must_provide_operation_name_errors_return_the_right_usage_reporting_data() {
let planner =
Planner::<serde_json::Value>::new(SCHEMA.to_string(), QueryPlannerConfig::default())
.await
.unwrap();
let payload = planner
.plan(MULTIPLE_QUERIES.to_string(), None)
.await
.unwrap()
.into_result()
.unwrap_err();
assert_eq!(
"Must provide operation name if query contains multiple operations.",
payload.errors[0].message.as_ref().clone().unwrap()
);
assert_eq!(
"## GraphQLUnknownOperationName\n",
payload.usage_reporting.stats_report_key
);
}
#[tokio::test]
async fn multiple_anonymous_queries_return_the_expected_usage_reporting_data() {
let planner =
Planner::<serde_json::Value>::new(SCHEMA.to_string(), QueryPlannerConfig::default())
.await
.unwrap();
let payload = planner
.plan(MULTIPLE_ANONYMOUS_QUERIES.to_string(), None)
.await
.unwrap()
.into_result()
.unwrap_err();
assert_eq!(
"This anonymous operation must be the only defined operation.",
payload.errors[0].message.as_ref().clone().unwrap()
);
assert_eq!(
"## GraphQLValidationFailure\n",
payload.usage_reporting.stats_report_key
);
}
#[tokio::test]
async fn no_operation_in_document() {
let planner =
Planner::<serde_json::Value>::new(SCHEMA.to_string(), QueryPlannerConfig::default())
.await
.unwrap();
let payload = planner
.plan(NO_OPERATION.to_string(), None)
.await
.unwrap()
.into_result()
.unwrap_err();
assert_eq!(
"Fragment \"thatUserFragment1\" is never used.",
payload.errors[0].message.as_ref().clone().unwrap()
);
assert_eq!(
"## GraphQLValidationFailure\n",
payload.usage_reporting.stats_report_key
);
}
#[tokio::test]
async fn invalid_graphql_validation_1_is_caught() {
let errors= vec![PlanError {
message: Some("Cannot spread fragment \"thatUserFragment1\" within itself via \"thatUserFragment2\".".to_string()),
extensions: None,
}];
assert_errors(
errors,
"\
fragment thatUserFragment1 on User {
id
...thatUserFragment2
}
fragment thatUserFragment2 on User {
id
...thatUserFragment1
}
query { me { id ...thatUserFragment1 } }"
.to_string(),
None,
)
.await;
}
#[tokio::test]
async fn invalid_graphql_validation_2_is_caught() {
let errors = vec![PlanError {
message: Some(
"Field \"id\" must not have a selection since type \"ID!\" has no subfields."
.to_string(),
),
extensions: None,
}];
assert_errors(
errors,
"{ me { id { absolutelyNotAcceptableLeaf } } }".to_string(),
None,
)
.await;
}
#[tokio::test]
async fn invalid_graphql_validation_3_is_caught() {
let errors = vec![PlanError {
message: Some("Fragment \"UnusedTestFragment\" is never used.".to_string()),
extensions: None,
}];
assert_errors(
errors,
"fragment UnusedTestFragment on User { id } query { me { id } }".to_string(),
None,
)
.await;
}
#[tokio::test]
async fn invalid_federation_validation_is_caught() {
let errors = vec![PlanError {
message: Some(
"Must provide operation name if query contains multiple operations.".to_string(),
),
extensions: Some(PlanErrorExtensions {
code: "INVALID_GRAPHQL".to_string(),
}),
}];
assert_errors(
errors, "query Operation1 { me { id } } query Operation2 { me { id } }".to_string(),
None,
)
.await;
}
#[tokio::test]
async fn invalid_schema_is_caught() {
let expected_errors: Vec<PlannerError> = vec![WorkerGraphQLError {
name: "GraphQLError".to_string(),
message: "Syntax Error: Unexpected Name \"Garbage\".".to_string(),
extensions: None,
locations: vec![Location { line: 1, column: 1 }],
original_error: None,
causes: vec![],
}
.into()];
let actual_error =
Planner::<serde_json::Value>::new("Garbage".to_string(), QueryPlannerConfig::default())
.await
.unwrap_err();
assert_eq!(expected_errors, actual_error);
}
#[tokio::test]
async fn syntactically_incorrect_query_is_caught() {
let errors = vec![PlanError {
message: Some("Syntax Error: Unexpected Name \"Garbage\".".to_string()),
extensions: None,
}];
assert_errors(errors, "Garbage".to_string(), None).await;
}
#[tokio::test]
async fn query_missing_subfields() {
let expected_error_message = r#"Field "reviews" of type "[Review]" must have a selection of subfields. Did you mean "reviews { ... }"?"#;
let errors = vec![PlanError {
message: Some(expected_error_message.to_string()),
extensions: None,
}];
assert_errors(
errors,
"query ExampleQuery { me { id reviews } }".to_string(),
None,
)
.await;
}
#[tokio::test]
async fn query_field_that_doesnt_exist() {
let expected_error_message = r#"Cannot query field "thisDoesntExist" on type "Query"."#;
let errors = vec![PlanError {
message: Some(expected_error_message.to_string()),
extensions: None,
}];
assert_errors(
errors,
"query ExampleQuery { thisDoesntExist }".to_string(),
None,
)
.await;
}
async fn assert_errors(
expected_errors: Vec<PlanError>,
query: String,
operation_name: Option<String>,
) {
let planner =
Planner::<serde_json::Value>::new(SCHEMA.to_string(), QueryPlannerConfig::default())
.await
.unwrap();
let actual = planner.plan(query, operation_name).await.unwrap();
assert_eq!(expected_errors, actual.errors.unwrap());
}
#[tokio::test]
async fn it_doesnt_race() {
let planner =
Planner::<serde_json::Value>::new(SCHEMA.to_string(), QueryPlannerConfig::default())
.await
.unwrap();
let query_1_response = planner
.plan(QUERY.to_string(), None)
.await
.unwrap()
.data
.unwrap();
let query_2_response = planner
.plan(QUERY2.to_string(), None)
.await
.unwrap()
.data
.unwrap();
let all_futures = stream::iter((0..1000).map(|i| {
let (query, fut) = if i % 2 == 0 {
(QUERY, planner.plan(QUERY.to_string(), None))
} else {
(QUERY2, planner.plan(QUERY2.to_string(), None))
};
async move { (query, fut.await.unwrap()) }
}));
all_futures
.for_each_concurrent(None, |fut| async {
let (query, plan_result) = fut.await;
if query == QUERY {
assert_eq!(query_1_response, plan_result.data.unwrap());
} else {
assert_eq!(query_2_response, plan_result.data.unwrap());
}
})
.await;
}
#[tokio::test]
async fn error_on_core_in_v0_1() {
let expected_errors: Vec<PlannerError> = vec![
WorkerGraphQLError {
name: "GraphQLError".to_string(),
message: r#"one or more checks failed. Caused by:
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).
GraphQL request:2:1
1 | schema
2 | @core(feature: "https://specs.apollo.dev/core/v0.1")
| ^
3 | @core(feature: "https://specs.apollo.dev/join/v0.1", for: EXECUTION)
GraphQL request:3:1
2 | @core(feature: "https://specs.apollo.dev/core/v0.1")
3 | @core(feature: "https://specs.apollo.dev/join/v0.1", for: EXECUTION)
| ^
4 | @core(
GraphQL request:4:1
3 | @core(feature: "https://specs.apollo.dev/join/v0.1", for: EXECUTION)
4 | @core(
| ^
5 | feature: "https://specs.apollo.dev/something-unsupported/v0.1"
feature https://specs.apollo.dev/something-unsupported/v0.1 is for: SECURITY but is unsupported
GraphQL request:4:1
3 | @core(feature: "https://specs.apollo.dev/join/v0.1", for: EXECUTION)
4 | @core(
| ^
5 | feature: "https://specs.apollo.dev/something-unsupported/v0.1""#.to_string(),
locations: Default::default(),
extensions: Some(PlanErrorExtensions {
code: "CheckFailed".to_string(),
}),
original_error: None,
causes: vec![
Box::new(WorkerError {
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()),
name: None,
stack: None,
extensions: Some(PlanErrorExtensions { code: "UNSUPPORTED_LINKED_FEATURE".to_string() }),
locations: vec![Location { line: 2, column: 1 }, Location { line: 3, column: 1 }, Location { line: 4, column: 1 }]
}),
Box::new(WorkerError {
message: Some("feature https://specs.apollo.dev/something-unsupported/v0.1 is for: SECURITY but is unsupported".to_string()),
name: None,
stack: None,
extensions: Some(PlanErrorExtensions { code: "UNSUPPORTED_LINKED_FEATURE".to_string() }),
locations: vec![Location { line: 4, column: 1 }]
})
],
}.into()
];
let actual_errors = Planner::<serde_json::Value>::new(
CORE_IN_V0_1.to_string(),
QueryPlannerConfig::default(),
)
.await
.unwrap_err();
pretty_assertions::assert_eq!(expected_errors, actual_errors);
}
#[tokio::test]
async fn unsupported_feature_without_for() {
Planner::<serde_json::Value>::new(
UNSUPPORTED_FEATURE.to_string(),
QueryPlannerConfig::default(),
)
.await
.unwrap();
}
#[tokio::test]
async fn unsupported_feature_for_execution() {
let expected_errors: Vec<PlannerError> = vec![
WorkerGraphQLError {
name: "GraphQLError".to_string(),
message: r#"one or more checks failed. Caused by:
feature https://specs.apollo.dev/unsupported-feature/v0.1 is for: EXECUTION but is unsupported
GraphQL request:4:9
3 | @core(feature: "https://specs.apollo.dev/join/v0.1", for: EXECUTION)
4 | @core(
| ^
5 | feature: "https://specs.apollo.dev/unsupported-feature/v0.1""#.to_string(),
locations: Default::default(),
extensions: Some(PlanErrorExtensions {
code: "CheckFailed".to_string(),
}),
original_error: None,
causes: vec![
Box::new(WorkerError {
message: Some("feature https://specs.apollo.dev/unsupported-feature/v0.1 is for: EXECUTION but is unsupported".to_string()),
name: None,
stack: None,
extensions: Some(PlanErrorExtensions { code: "UNSUPPORTED_LINKED_FEATURE".to_string() }),
locations: vec![Location { line: 4, column: 9 }]
}),
],
}.into()
];
let actual_errors = Planner::<serde_json::Value>::new(
UNSUPPORTED_FEATURE_FOR_EXECUTION.to_string(),
QueryPlannerConfig::default(),
)
.await
.unwrap_err();
pretty_assertions::assert_eq!(expected_errors, actual_errors);
}
#[tokio::test]
async fn unsupported_feature_for_security() {
let expected_errors: Vec<PlannerError> = vec![WorkerGraphQLError {
name:"GraphQLError".into(),
message: r#"one or more checks failed. Caused by:
feature https://specs.apollo.dev/unsupported-feature/v0.1 is for: SECURITY but is unsupported
GraphQL request:4:9
3 | @core(feature: "https://specs.apollo.dev/join/v0.1", for: EXECUTION)
4 | @core(
| ^
5 | feature: "https://specs.apollo.dev/unsupported-feature/v0.1""#.to_string(),
locations: vec![],
extensions: Some(PlanErrorExtensions {
code: "CheckFailed".to_string(),
}),
original_error: None,
causes: vec![Box::new(WorkerError {
message: Some("feature https://specs.apollo.dev/unsupported-feature/v0.1 is for: SECURITY but is unsupported".to_string()),
extensions: Some(PlanErrorExtensions {
code: "UNSUPPORTED_LINKED_FEATURE".to_string(),
}),
name: None,
stack: None,
locations: vec![Location { line: 4, column: 9 }]
})],
}
.into()];
let actual_errors = Planner::<serde_json::Value>::new(
UNSUPPORTED_FEATURE_FOR_SECURITY.to_string(),
QueryPlannerConfig::default(),
)
.await
.unwrap_err();
pretty_assertions::assert_eq!(expected_errors, actual_errors);
}
}
#[cfg(test)]
mod planning_error {
use std::collections::HashMap;
use crate::planner::{PlanError, PlanErrorExtensions, ReferencedFieldsForType, UsageReporting};
#[test]
#[should_panic(
expected = "Result::unwrap()` on an `Err` value: Error(\"missing field `extensions`\", line: 1, column: 2)"
)]
fn deserialize_empty_planning_error() {
let raw = "{}";
serde_json::from_str::<PlanError>(raw).unwrap();
}
#[test]
#[should_panic(
expected = "Result::unwrap()` on an `Err` value: Error(\"missing field `extensions`\", line: 1, column: 44)"
)]
fn deserialize_planning_error_missing_extension() {
let raw = r#"{ "message": "something terrible happened" }"#;
serde_json::from_str::<PlanError>(raw).unwrap();
}
#[test]
fn deserialize_planning_error_with_extension() {
let raw = r#"{
"message": "something terrible happened",
"extensions": {
"code": "E_TEST_CASE"
}
}"#;
let expected = PlanError {
message: Some("something terrible happened".to_string()),
extensions: Some(PlanErrorExtensions {
code: "E_TEST_CASE".to_string(),
}),
};
assert_eq!(expected, serde_json::from_str(raw).unwrap());
}
#[test]
fn deserialize_planning_error_with_empty_object_extension() {
let raw = r#"{
"extensions": {}
}"#;
let expected = PlanError {
message: None,
extensions: None,
};
assert_eq!(expected, serde_json::from_str(raw).unwrap());
}
#[test]
fn deserialize_planning_error_with_null_extension() {
let raw = r#"{
"extensions": null
}"#;
let expected = PlanError {
message: None,
extensions: None,
};
assert_eq!(expected, serde_json::from_str(raw).unwrap());
}
#[test]
fn deserialize_referenced_fields_for_type_defaults() {
let raw = r#"{}"#;
let expected = ReferencedFieldsForType {
field_names: Vec::new(),
is_interface: false,
};
assert_eq!(expected, serde_json::from_str(raw).unwrap());
}
#[test]
fn deserialize_usage_reporting_with_defaults() {
let raw = r#"{
"statsReportKey": "thisIsAtest"
}"#;
let expected = UsageReporting {
stats_report_key: "thisIsAtest".to_string(),
referenced_fields_by_type: HashMap::new(),
};
assert_eq!(expected, serde_json::from_str(raw).unwrap());
}
}
#[cfg(test)]
mod error_display {
use super::*;
#[test]
fn error_on_core_in_v0_1_display() {
let expected = r#"one or more checks failed
caused by
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).
feature https://specs.apollo.dev/something-unsupported/v0.1 is for: SECURITY but is unsupported"#;
let error_to_display: PlannerError = WorkerGraphQLError {
name: "CheckFailed".to_string(),
message: "one or more checks failed".to_string(),
locations: Default::default(),
extensions: Some(PlanErrorExtensions {
code: "CheckFailed".to_string(),
}),
original_error: None,
causes: vec![
Box::new(WorkerError {
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()),
name: None,
stack: None,
extensions: Some(PlanErrorExtensions { code: "ForUnsupported".to_string() }),
locations: vec![Location { line: 2, column: 1 }, Location { line: 3, column: 1 }, Location { line: 4, column: 1 }]
}),
Box::new(WorkerError {
message: Some("feature https://specs.apollo.dev/something-unsupported/v0.1 is for: SECURITY but is unsupported".to_string()),
name: None,
stack: None,
extensions: Some(PlanErrorExtensions { code: "UnsupportedFeature".to_string() }),
locations: vec![Location { line: 4, column: 1 }]
})
],
}.into();
assert_eq!(expected.to_string(), error_to_display.to_string());
}
#[test]
fn unsupported_feature_for_execution_display() {
let expected = r#"one or more checks failed
caused by
feature https://specs.apollo.dev/unsupported-feature/v0.1 is for: EXECUTION but is unsupported"#;
let error_to_display: PlannerError = WorkerGraphQLError {
name: "CheckFailed".to_string(),
message: "one or more checks failed".to_string(),
locations: Default::default(),
extensions: Some(PlanErrorExtensions {
code: "CheckFailed".to_string(),
}),
original_error: None,
causes: vec![
Box::new(WorkerError {
message: Some("feature https://specs.apollo.dev/unsupported-feature/v0.1 is for: EXECUTION but is unsupported".to_string()),
name: None,
stack: None,
extensions: Some(PlanErrorExtensions { code: "UnsupportedFeature".to_string() }),
locations: vec![Location { line: 4, column: 9 }]
}),
],
}.into();
assert_eq!(expected.to_string(), error_to_display.to_string());
}
#[test]
fn unsupported_feature_for_security_display() {
let expected = r#"one or more checks failed
caused by
feature https://specs.apollo.dev/unsupported-feature/v0.1 is for: SECURITY but is unsupported"#;
let error_to_display: PlannerError = WorkerGraphQLError {
name: "CheckFailed".into(),
message: "one or more checks failed".to_string(),
locations: vec![],
extensions: Some(PlanErrorExtensions {
code: "CheckFailed".to_string(),
}),
original_error: None,
causes: vec![Box::new(WorkerError {
message: Some("feature https://specs.apollo.dev/unsupported-feature/v0.1 is for: SECURITY but is unsupported".to_string()),
extensions: Some(PlanErrorExtensions {
code: "UnsupportedFeature".to_string(),
}),
name: None,
stack: None,
locations: vec![Location { line: 4, column: 9 }]
})],
}
.into();
assert_eq!(expected.to_string(), error_to_display.to_string());
}
}