You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
GraphQL-Ruby ships with some validations based on {% internal_link "query analysis", "/queries/ast_analysis" %}. You can customize them as-needed, too.
12
12
13
+
## Prevent deeply-nested queries
14
+
15
+
You can also reject queries based on the depth of their nesting. You can define `max_depth` at schema-level or query-level:
16
+
17
+
```ruby
18
+
# Schema-level:
19
+
classMySchema < GraphQL::Schema
20
+
# ...
21
+
max_depth 15
22
+
end
23
+
24
+
# Query-level, which overrides the schema-level setting:
25
+
MySchema.execute(query_string, max_depth:20)
26
+
```
27
+
28
+
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`.
29
+
30
+
You can use `nil` to disable the validation:
31
+
32
+
```ruby
33
+
# This query won't be validated:
34
+
MySchema.execute(query_string, max_depth:nil)
35
+
```
36
+
37
+
To get a feeling for depth of queries in your system, you can extend {{ "GraphQL::Analysis::AST::QueryDepth" | api_doc }}. Hook it up to log out values from each query:
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:
@@ -121,42 +161,159 @@ class Types::BaseField < GraphQL::Schema::Field
121
161
end
122
162
```
123
163
124
-
## Prevent deeply-nested queries
164
+
## How complexity scoring works
125
165
126
-
You can also reject queries based on the depth of their nesting. You can define `max_depth` at schema-level or query-level:
166
+
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):
127
167
128
-
```ruby
129
-
# Schema-level:
130
-
classMySchema < GraphQL::Schema
131
-
# ...
132
-
max_depth 15
133
-
end
134
-
135
-
# Query-level, which overrides the schema-level setting:
136
-
MySchema.execute(query_string, max_depth:20)
168
+
```graphql
169
+
query {
170
+
node(id: "123") { # interface Node
171
+
id
172
+
...onHasMetafields { # interface HasMetafields
173
+
metafield(key: "a") {
174
+
value
175
+
}
176
+
metafields(first: 10) {
177
+
nodes {
178
+
value
179
+
}
180
+
}
181
+
}
182
+
...onProduct { # implements HasMetafields
183
+
title
184
+
metafield(key: "a") {
185
+
definition {
186
+
description
187
+
}
188
+
}
189
+
}
190
+
...onPriceList {
191
+
name
192
+
catalog {
193
+
id
194
+
}
195
+
}
196
+
}
197
+
}
137
198
```
138
199
139
-
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`.
200
+
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:
140
201
141
-
You can use `nil` to disable the validation:
202
+
- Leaf fields cost `0`
203
+
- Composite fields cost `1`
204
+
- Connection fields cost `children * input size`
142
205
143
-
```ruby
144
-
# This query won't be validated:
145
-
MySchema.execute(query_string, max_depth:nil)
206
+
Given these parameters, we get an itemized scoring distribution of:
207
+
208
+
```graphql
209
+
query {
210
+
node(id: "123") { # 1, composite
211
+
id # 0, leaf
212
+
...onHasMetafields {
213
+
metafield(key: "a") { # 1, composite
214
+
value # 0, leaf
215
+
}
216
+
metafields(first: 10) { # 1 * 10, connection
217
+
nodes { # 1, composite
218
+
value # 0, leaf
219
+
}
220
+
}
221
+
}
222
+
...onProduct {
223
+
title # 0, leaf
224
+
metafield(key: "a") { # 1, composite
225
+
definition { # 1, composite
226
+
description # 0, leaf
227
+
}
228
+
}
229
+
}
230
+
...onPriceList {
231
+
name # 0, leaf
232
+
catalog { # 1, composite
233
+
id # 0, leaf
234
+
}
235
+
}
236
+
}
237
+
}
146
238
```
147
239
148
-
To get a feeling for depth of queries in your system, you can extend {{ "GraphQL::Analysis::AST::QueryDepth" | api_doc }}. Hook it up to log out values from each query:
240
+
However, we cannot naively tally these itemized scores without over-costing the query. Consider:
241
+
242
+
- The `node` scope makes many _possible_ selections on an abstract type, so we need the maximum among concrete possibilities for a fair representation.
243
+
- 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.
244
+
245
+
To reconcile these possibilities, the [complexity algorithm](https://github.com/rmosolgo/graphql-ruby/blob/master/lib/graphql/analysis/ast/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):
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`:
288
+
289
+
```graphql
290
+
query {
291
+
node(id: "123") { # max(11, 12, 1) = 12
292
+
id
293
+
...onHasMetafields { # 1 + 10 = 11
294
+
metafield(key: "a") { # 1
295
+
value
296
+
}
297
+
metafields(first: 10) { # 10
298
+
nodes {
299
+
value
300
+
}
301
+
}
302
+
}
303
+
...onProduct { # 1 + 11 from HasMetafields = 12
304
+
title
305
+
metafield(key: "a") { # duplicated in HasMetafields
0 commit comments