GraphQL-Ruby ships with some validations based on query analysis. You can customize them as-needed, too.
Fields have a “complexity” value which can be configured in their definition. It can be a constant (numeric) value, or a proc. If no complexity is defined for a field, it will default to a value of 1. It can be defined as a keyword or inside the configuration block. For example:
# Constant complexity:
field :top_score, Integer, null: false, complexity: 10
# Dynamic complexity:
field :top_scorers, [PlayerType], null: false do
argument :limit, Integer, limit: false, default_value: 5
complexity ->(ctx, args, child_complexity) {
if ctx[:current_user].staff?
# no limit for staff users
0
else
# `child_complexity` is the value for selections
# which were made on the items of this list.
#
# We don't know how many items will be fetched because
# we haven't run the query yet, but we can estimate by
# using the `limit` argument which we defined above.
args[:limit] * child_complexity
end
}
end
Then, define your max_complexity at the schema-level:
class MySchema < GraphQL::Schema
# ...
max_complexity 100
end
Or, at the query-level, which overrides the schema-level setting:
MySchema.execute(query_string, max_complexity: 100)
Using nil will disable the validation:
# 😧 Anything goes!
MySchema.execute(query_string, max_complexity: nil)
To get a feeling for complexity of queries in your system, you can extend GraphQL::Analysis::AST::QueryComplexity. Hook it up to log out values from each query:
class LogQueryComplexityAnalyzer < GraphQL::Analysis::AST::QueryComplexity
# Override this method to _do something_ with the calculated complexity value
def result
complexity = super
message = "[GraphQL Query Complexity] #{complexity} | staff? #{query.context[:current_user].staff?}"
Rails.logger.info(message)
end
end
class MySchema < GraphQL::Schema
query_analyzer(LogQueryComplexityAnalyzer)
end
By default, GraphQL-Ruby calculates a complexity value for connection fields by:
1 for pageInfo and each of its subselections1 for count, totalCount, or total1 for the connection field itselfmultiplying the complexity of other fields by the largest possible page size, which is the greater of first: or last:, or if neither of those are given it will go through each of default_page_size, the schema’s default_page_size, max_page_size, and then the schema’s default_max_page_size.
(If no default page size or max page size can be determined, then the analysis crashes with an internal error – set default_page_size or default_max_page_size in your schema to prevent this.)
For example, this query has complexity 26:
query {
author { # +1
name # +1
books(first: 10) { # +1
nodes { # +10 (+1, multiplied by `first:` above)
title # +10 (ditto)
}
pageInfo { # +1
endCursor # +1
}
totalCount # +1
}
}
}
To customize this behavior, implement def calculate_complexity(query:, nodes:, child_complexity:) in your base field class, handling the case where self.connection? is true:
class Types::BaseField < GraphQL::Schema::Field
def calculate_complexity(query:, nodes:, child_complexity:)
if connection?
# Custom connection calculation goes here
else
super
end
end
end
You can also reject queries based on the depth of their nesting. You can define max_depth at schema-level or query-level:
# Schema-level:
class MySchema < GraphQL::Schema
# ...
max_depth 15
end
# Query-level, which overrides the schema-level setting:
MySchema.execute(query_string, max_depth: 20)
(Note: the default introspection query from GraphiQL requires at least max_depth 13.)
You can use nil to disable the validation:
# This query won't be validated:
MySchema.execute(query_string, max_depth: nil)
To get a feeling for depth of queries in your system, you can extend GraphQL::Analysis::AST::QueryDepth. Hook it up to log out values from each query:
class LogQueryDepth < GraphQL::Analysis::AST::QueryDepth
def result
query_depth = super
message = "[GraphQL Query Depth] #{query_depth} || staff? #{query.context[:current_user].staff?}"
Rails.logger.info(message)
end
end
class MySchema < GraphQL::Schema
query_analyzer(LogQueryDepth)
end