use graphql_parser::query::{
Definition, Document, FragmentDefinition, OperationDefinition, Selection, SelectionSet,
};
pub fn parse_graphql_document(
query: &str,
) -> Result<Document<'_, String>, ComplexityValidationError> {
if query.trim().is_empty() {
return Err(ComplexityValidationError::MalformedQuery("Empty query".to_string()));
}
graphql_parser::parse_query::<String>(query)
.map_err(|e| ComplexityValidationError::MalformedQuery(format!("{e}")))
}
pub const DEFAULT_MAX_ALIASES: usize = 30;
pub const MAX_VARIABLES_COUNT: usize = 1_000;
#[derive(Debug, Clone)]
pub struct ComplexityConfig {
pub max_depth: usize,
pub max_complexity: usize,
pub max_aliases: usize,
}
impl Default for ComplexityConfig {
fn default() -> Self {
Self {
max_depth: 10,
max_complexity: 100,
max_aliases: DEFAULT_MAX_ALIASES,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct QueryMetrics {
pub depth: usize,
pub complexity: usize,
pub alias_count: usize,
}
#[derive(Debug, thiserror::Error, Clone)]
#[non_exhaustive]
pub enum ComplexityValidationError {
#[error("Query exceeds maximum depth of {max_depth}: depth = {actual_depth}")]
QueryTooDeep {
max_depth: usize,
actual_depth: usize,
},
#[error("Query exceeds maximum complexity of {max_complexity}: score = {actual_complexity}")]
QueryTooComplex {
max_complexity: usize,
actual_complexity: usize,
},
#[error("Query exceeds maximum alias count of {max_aliases}: count = {actual_aliases}")]
TooManyAliases {
max_aliases: usize,
actual_aliases: usize,
},
#[error("Invalid variables: {0}")]
InvalidVariables(String),
#[error("Malformed GraphQL query: {0}")]
MalformedQuery(String),
}
#[derive(Debug, Clone)]
pub struct RequestValidator {
max_depth: usize,
max_complexity: usize,
max_aliases_per_query: usize,
validate_depth: bool,
validate_complexity: bool,
}
impl RequestValidator {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub const fn from_config(config: &ComplexityConfig) -> Self {
Self {
max_depth: config.max_depth,
max_complexity: config.max_complexity,
max_aliases_per_query: config.max_aliases,
validate_depth: true,
validate_complexity: true,
}
}
#[must_use]
pub const fn with_max_depth(mut self, max_depth: usize) -> Self {
self.max_depth = max_depth;
self
}
#[must_use]
pub const fn with_max_complexity(mut self, max_complexity: usize) -> Self {
self.max_complexity = max_complexity;
self
}
#[must_use]
pub const fn with_depth_validation(mut self, enabled: bool) -> Self {
self.validate_depth = enabled;
self
}
#[must_use]
pub const fn with_complexity_validation(mut self, enabled: bool) -> Self {
self.validate_complexity = enabled;
self
}
#[must_use]
pub const fn with_max_aliases(mut self, max_aliases: usize) -> Self {
self.max_aliases_per_query = max_aliases;
self
}
pub fn analyze(&self, query: &str) -> Result<QueryMetrics, ComplexityValidationError> {
if query.trim().is_empty() {
return Err(ComplexityValidationError::MalformedQuery("Empty query".to_string()));
}
let document = graphql_parser::parse_query::<String>(query)
.map_err(|e| ComplexityValidationError::MalformedQuery(format!("{e}")))?;
let fragments = collect_fragments(&document);
Ok(QueryMetrics {
depth: self.calculate_depth_ast(&document, &fragments),
complexity: self.calculate_complexity_ast(&document, &fragments),
alias_count: self.count_aliases_ast(&document),
})
}
#[must_use]
pub const fn is_no_op(&self) -> bool {
!self.validate_depth && !self.validate_complexity && self.max_aliases_per_query == 0
}
pub fn validate_query(&self, query: &str) -> Result<(), ComplexityValidationError> {
if query.trim().is_empty() {
return Err(ComplexityValidationError::MalformedQuery("Empty query".to_string()));
}
if self.is_no_op() {
return Ok(());
}
let document = graphql_parser::parse_query::<String>(query)
.map_err(|e| ComplexityValidationError::MalformedQuery(format!("{e}")))?;
self.validate_query_doc(&document)
}
pub fn validate_query_doc<'a>(
&self,
document: &'a Document<'a, String>,
) -> Result<(), ComplexityValidationError> {
if self.is_no_op() {
return Ok(());
}
let fragments = collect_fragments(document);
if self.validate_depth {
let depth = self.calculate_depth_ast(document, &fragments);
if depth > self.max_depth {
return Err(ComplexityValidationError::QueryTooDeep {
max_depth: self.max_depth,
actual_depth: depth,
});
}
}
if self.validate_complexity {
let complexity = self.calculate_complexity_ast(document, &fragments);
if complexity > self.max_complexity {
return Err(ComplexityValidationError::QueryTooComplex {
max_complexity: self.max_complexity,
actual_complexity: complexity,
});
}
}
let alias_count = self.count_aliases_ast(document);
if alias_count > self.max_aliases_per_query {
return Err(ComplexityValidationError::TooManyAliases {
max_aliases: self.max_aliases_per_query,
actual_aliases: alias_count,
});
}
Ok(())
}
pub fn validate_variables(
&self,
variables: Option<&serde_json::Value>,
) -> Result<(), ComplexityValidationError> {
if let Some(vars) = variables {
if !vars.is_object() {
return Err(ComplexityValidationError::InvalidVariables(
"Variables must be an object".to_string(),
));
}
let obj = vars.as_object().expect("invariant: vars.is_object() checked above");
if obj.len() > MAX_VARIABLES_COUNT {
return Err(ComplexityValidationError::InvalidVariables(format!(
"Too many variables: {} exceeds maximum of {}",
obj.len(),
MAX_VARIABLES_COUNT
)));
}
}
Ok(())
}
fn calculate_depth_ast(
&self,
document: &Document<String>,
fragments: &[&FragmentDefinition<String>],
) -> usize {
document
.definitions
.iter()
.map(|def| match def {
Definition::Operation(op) => match op {
OperationDefinition::Query(q) => {
self.selection_set_depth(&q.selection_set, fragments, 0)
},
OperationDefinition::Mutation(m) => {
self.selection_set_depth(&m.selection_set, fragments, 0)
},
OperationDefinition::Subscription(s) => {
self.selection_set_depth(&s.selection_set, fragments, 0)
},
OperationDefinition::SelectionSet(ss) => {
self.selection_set_depth(ss, fragments, 0)
},
},
Definition::Fragment(f) => self.selection_set_depth(&f.selection_set, fragments, 0),
})
.max()
.unwrap_or(0)
}
fn selection_set_depth(
&self,
selection_set: &SelectionSet<String>,
fragments: &[&FragmentDefinition<String>],
recursion_depth: usize,
) -> usize {
if recursion_depth > 32 {
return self.max_depth + 1;
}
if selection_set.items.is_empty() {
return 0;
}
let max_child = selection_set
.items
.iter()
.map(|sel| match sel {
Selection::Field(field) => {
if field.selection_set.items.is_empty() {
0
} else {
self.selection_set_depth(&field.selection_set, fragments, recursion_depth)
}
},
Selection::InlineFragment(inline) => {
self.selection_set_depth(&inline.selection_set, fragments, recursion_depth)
},
Selection::FragmentSpread(spread) => {
if let Some(frag) = fragments.iter().find(|f| f.name == spread.fragment_name) {
self.selection_set_depth(
&frag.selection_set,
fragments,
recursion_depth + 1,
)
} else {
self.max_depth
}
},
})
.max()
.unwrap_or(0);
1 + max_child
}
fn calculate_complexity_ast(
&self,
document: &Document<String>,
fragments: &[&FragmentDefinition<String>],
) -> usize {
document
.definitions
.iter()
.map(|def| match def {
Definition::Operation(op) => match op {
OperationDefinition::Query(q) => {
self.selection_set_complexity(&q.selection_set, fragments, 0)
},
OperationDefinition::Mutation(m) => {
self.selection_set_complexity(&m.selection_set, fragments, 0)
},
OperationDefinition::Subscription(s) => {
self.selection_set_complexity(&s.selection_set, fragments, 0)
},
OperationDefinition::SelectionSet(ss) => {
self.selection_set_complexity(ss, fragments, 0)
},
},
Definition::Fragment(_) => 0,
})
.sum()
}
fn selection_set_complexity(
&self,
selection_set: &SelectionSet<String>,
fragments: &[&FragmentDefinition<String>],
recursion_depth: usize,
) -> usize {
if recursion_depth > 32 {
return self.max_complexity + 1;
}
selection_set
.items
.iter()
.map(|sel| match sel {
Selection::Field(field) => {
let multiplier = extract_limit_multiplier(&field.arguments);
if field.selection_set.items.is_empty() {
1
} else {
let nested = self.selection_set_complexity(
&field.selection_set,
fragments,
recursion_depth,
);
1 + nested * multiplier
}
},
Selection::InlineFragment(inline) => {
self.selection_set_complexity(&inline.selection_set, fragments, recursion_depth)
},
Selection::FragmentSpread(spread) => {
if let Some(frag) = fragments.iter().find(|f| f.name == spread.fragment_name) {
self.selection_set_complexity(
&frag.selection_set,
fragments,
recursion_depth + 1,
)
} else {
10
}
},
})
.sum()
}
fn count_aliases_ast(&self, document: &Document<String>) -> usize {
document
.definitions
.iter()
.map(|def| match def {
Definition::Operation(op) => {
let ss = match op {
OperationDefinition::Query(q) => &q.selection_set,
OperationDefinition::Mutation(m) => &m.selection_set,
OperationDefinition::Subscription(s) => &s.selection_set,
OperationDefinition::SelectionSet(ss) => ss,
};
count_aliases_in_selection_set(ss)
},
Definition::Fragment(f) => count_aliases_in_selection_set(&f.selection_set),
})
.sum()
}
}
impl Default for RequestValidator {
fn default() -> Self {
Self {
max_depth: 10,
max_complexity: 100,
max_aliases_per_query: DEFAULT_MAX_ALIASES,
validate_depth: true,
validate_complexity: true,
}
}
}
fn collect_fragments<'a>(
document: &'a Document<'a, String>,
) -> Vec<&'a FragmentDefinition<'a, String>> {
document
.definitions
.iter()
.filter_map(|def| {
if let Definition::Fragment(f) = def {
Some(f)
} else {
None
}
})
.collect()
}
fn extract_limit_multiplier(arguments: &[(String, graphql_parser::query::Value<String>)]) -> usize {
for (name, value) in arguments {
if matches!(name.as_str(), "first" | "limit" | "take" | "last") {
if let graphql_parser::query::Value::Int(n) = value {
#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
let limit = n.as_i64().unwrap_or(10) as usize;
return limit.clamp(1, 100);
}
}
}
1
}
fn count_aliases_in_selection_set(selection_set: &SelectionSet<String>) -> usize {
selection_set
.items
.iter()
.map(|sel| match sel {
Selection::Field(field) => {
let self_alias = usize::from(field.alias.is_some());
self_alias + count_aliases_in_selection_set(&field.selection_set)
},
Selection::InlineFragment(inline) => {
count_aliases_in_selection_set(&inline.selection_set)
},
Selection::FragmentSpread(_) => 0,
})
.sum()
}