1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321
|
---
title: Complexity & Depth
layout: guide
doc_stub: false
search: true
section: Queries
desc: Limiting query depth and field selections
index: 4
---
GraphQL-Ruby ships with some validations based on {% internal_link "query analysis", "/queries/ast_analysis" %}. You can customize them as-needed, too.
## Prevent deeply-nested queries
You can also reject queries based on the depth of their nesting. You can define `max_depth` at schema-level or query-level:
```ruby
# 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)
```
By default, **introspection fields are counted**. The default introspection query requires at least `max_depth 13`. You can also configure your schema not to count introspection fields with `max_depth ..., count_introspection_fields: false`.
You can use `nil` to disable the validation:
```ruby
# 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::QueryDepth" | api_doc }}. Hook it up to log out values from each query:
```ruby
class LogQueryDepth < GraphQL::Analysis::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
```
## Prevent complex queries
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:
```ruby
# 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:
```ruby
class MySchema < GraphQL::Schema
# ...
max_complexity 100
end
```
Or, at the query-level, which overrides the schema-level setting:
```ruby
MySchema.execute(query_string, max_complexity: 100)
```
Using `nil` will disable the validation:
```ruby
# 😧 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::QueryComplexity" | api_doc }}. Hook it up to log out values from each query:
```ruby
class LogQueryComplexityAnalyzer < GraphQL::Analysis::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, **introspection fields are counted**. You can also configure your schema not to count introspection fields with `max_complexity ..., count_introspection_fields: false`.
#### Connection fields
By default, GraphQL-Ruby calculates a complexity value for connection fields by:
- adding `1` for `pageInfo` and each of its subselections
- adding `1` for `count`, `totalCount`, or `total`
- adding `1` for the connection field itself
- multiplying 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`:
```graphql
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`:
```ruby
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
```
## How complexity scoring works
GraphQL Ruby's complexity scoring algorithm is biased towards selection fairness. While highly accurate, its results are not always intuitive. Here's an example query performed on the [Shopify Admin API](https://shopify.dev/docs/api/admin-graphql):
```graphql
query {
node(id: "123") { # interface Node
id
...on HasMetafields { # interface HasMetafields
metafield(key: "a") {
value
}
metafields(first: 10) {
nodes {
value
}
}
}
...on Product { # implements HasMetafields
title
metafield(key: "a") {
definition {
description
}
}
}
...on PriceList {
name
catalog {
id
}
}
}
}
```
First, GraphQL Ruby allows field definitions to specify a `complexity` attribute that provides a complexity score (or a proc that computes a score) for each field. Let's say that this schema defines a system where:
- Leaf fields cost `0`
- Composite fields cost `1`
- Connection fields cost `children * input size`
Given these parameters, we get an itemized scoring distribution of:
```graphql
query {
node(id: "123") { # 1, composite
id # 0, leaf
...on HasMetafields {
metafield(key: "a") { # 1, composite
value # 0, leaf
}
metafields(first: 10) { # 1 * 10, connection
nodes { # 1, composite
value # 0, leaf
}
}
}
...on Product {
title # 0, leaf
metafield(key: "a") { # 1, composite
definition { # 1, composite
description # 0, leaf
}
}
}
...on PriceList {
name # 0, leaf
catalog { # 1, composite
id # 0, leaf
}
}
}
}
```
However, we cannot naively tally these itemized scores without over-costing the query. Consider:
- The `node` scope makes many _possible_ selections on an abstract type, so we need the maximum among concrete possibilities for a fair representation.
- A `node.metafield` selection path is duplicated across the `HasMetafields` and `Product` selection scopes. This path will only resolve once, so should also only cost once.
To reconcile these possibilities, the [complexity algorithm](https://github.com/rmosolgo/graphql-ruby/blob/master/lib/graphql/analysis/query_complexity.rb) breaks the selection down into a tree of types mapped to possible selections, across which lexical selections can be coalesced and deduplicated (pseudocode):
```ruby
{
Schema::Query => {
"node" => {
Schema::Node => {
"id" => nil,
},
Schema::HasMetafields => {
"metafield" => {
Schema::Metafield => {
"value" => nil,
},
},
"metafields" => {
Schema::Metafield => {
"nodes" => { ... },
},
},
},
Schema::Product => {
"title" => nil,
"metafield" => {
Schema::Metafield => {
"definition" => { ... },
},
},
},
Schema::PriceList => {
"name" => nil,
"catalog" => {
Schema::Catalog => {
"id" => nil,
},
},
},
},
},
}
```
This aggregation provides a new perspective on the scoring where _possible typed selections_ have costs rather than individual fields. In this normalized view, `Product` acquires the `HasMetafields` interface costs, and ignores a duplicated path. Ultimately the maximum of possible typed costs is used, making this query cost `12`:
```graphql
query {
node(id: "123") { # max(11, 12, 1) = 12
id
...on HasMetafields { # 1 + 10 = 11
metafield(key: "a") { # 1
value
}
metafields(first: 10) { # 10
nodes {
value
}
}
}
...on Product { # 1 + 11 from HasMetafields = 12
title
metafield(key: "a") { # duplicated in HasMetafields
definition { # 1
description
}
}
}
...on PriceList { # 1 = 1
name
catalog { # 1
id
}
}
}
}
```
|