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 total
1
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