use std::num::NonZeroUsize;
use std::ops::ControlFlow;
use std::sync::Arc;
use apollo_compiler::executable::Selection;
use serde_json_bytes::json;
use crate::Configuration;
use crate::cache::storage::CacheStorage;
use crate::compute_job;
use crate::compute_job::ComputeJobType;
use crate::graphql;
use crate::query_planner::QueryKey;
use crate::services::layers::query_analysis::ParsedDocument;
use crate::spec;
const DEFAULT_INTROSPECTION_CACHE_CAPACITY: NonZeroUsize =
unsafe { NonZeroUsize::new_unchecked(5) };
#[derive(Clone)]
pub(crate) enum IntrospectionCache {
Disabled,
Enabled {
storage: Arc<CacheStorage<String, graphql::Response>>,
},
}
impl IntrospectionCache {
pub(crate) fn new(configuration: &Configuration) -> Self {
if configuration.supergraph.introspection {
let storage = Arc::new(CacheStorage::new_in_memory(
DEFAULT_INTROSPECTION_CACHE_CAPACITY,
"introspection",
));
storage.activate();
Self::Enabled { storage }
} else {
Self::Disabled
}
}
pub(crate) fn activate(&self) {
match self {
IntrospectionCache::Disabled => {}
IntrospectionCache::Enabled { storage } => storage.activate(),
}
}
pub(crate) async fn maybe_execute(
&self,
schema: &Arc<spec::Schema>,
key: &QueryKey,
doc: &ParsedDocument,
) -> ControlFlow<graphql::Response, ()> {
Self::maybe_lone_root_typename(schema, doc)?;
if doc.operation.is_query() {
if doc.has_schema_introspection {
if doc.has_explicit_root_fields {
ControlFlow::Break(Self::mixed_fields_error())?;
} else {
ControlFlow::Break(self.cached_introspection(schema, key, doc).await)?
}
} else if !doc.has_explicit_root_fields {
ControlFlow::Break(Self::execute_introspection(schema, doc))?
}
}
ControlFlow::Continue(())
}
fn maybe_lone_root_typename(
schema: &Arc<spec::Schema>,
doc: &ParsedDocument,
) -> ControlFlow<graphql::Response, ()> {
if doc.operation.selection_set.selections.len() == 1 {
if let Selection::Field(field) = &doc.operation.selection_set.selections[0] {
if field.name == "__typename" && field.directives.is_empty() {
let key = field.response_key().as_str();
let object_type_name = schema
.api_schema()
.root_operation(doc.operation.operation_type)
.expect("validation should have caught undefined root operation")
.as_str();
let data = json!({key: object_type_name});
ControlFlow::Break(graphql::Response::builder().data(data).build())?
}
}
}
ControlFlow::Continue(())
}
fn mixed_fields_error() -> graphql::Response {
let error = graphql::Error::builder()
.message(
"\
Mixed queries with both schema introspection and concrete fields \
are not supported yet: https://github.com/apollographql/router/issues/2789\
",
)
.extension_code("MIXED_INTROSPECTION")
.build();
graphql::Response::builder().error(error).build()
}
async fn cached_introspection(
&self,
schema: &Arc<spec::Schema>,
key: &QueryKey,
doc: &ParsedDocument,
) -> graphql::Response {
let storage = match self {
IntrospectionCache::Enabled { storage } => storage,
IntrospectionCache::Disabled => {
let error = graphql::Error::builder()
.message(String::from("introspection has been disabled"))
.extension_code("INTROSPECTION_DISABLED")
.build();
return graphql::Response::builder().error(error).build();
}
};
let query = key.filtered_query.clone();
let cache_key = query;
if let Some(response) = storage.get(&cache_key, |_| unreachable!()).await {
return response;
}
let schema = schema.clone();
let doc = doc.clone();
let response = compute_job::execute(ComputeJobType::Introspection, move || {
Self::execute_introspection(&schema, &doc)
})
.await
.expect("Introspection panicked");
storage.insert(cache_key, response.clone()).await;
response
}
fn execute_introspection(schema: &spec::Schema, doc: &ParsedDocument) -> graphql::Response {
let api_schema = schema.api_schema();
let operation = &doc.operation;
let variable_values = Default::default();
match apollo_compiler::request::coerce_variable_values(
api_schema,
operation,
&variable_values,
)
.and_then(|variable_values| {
apollo_compiler::introspection::partial_execute(
api_schema,
&schema.implementers_map,
&doc.executable,
operation,
&variable_values,
)
}) {
Ok(response) => response.into(),
Err(e) => {
let error = e.to_graphql_error(&doc.executable.sources);
graphql::Response::builder().error(error).build()
}
}
}
}