File: ast_analysis.md

package info (click to toggle)
ruby-graphql 1.13.15-1
  • links: PTS, VCS
  • area: main
  • in suites: bookworm
  • size: 8,904 kB
  • sloc: ruby: 69,655; yacc: 444; javascript: 330; makefile: 6
file content (140 lines) | stat: -rw-r--r-- 4,345 bytes parent folder | download
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
---
layout: guide
doc_stub: false
search: true
section: Queries
title: Ahead-of-Time AST Analysis
desc: Check incoming query strings and reject them if they don't pass your checks
index: 1
redirect_from:
  - /queries/analysis/
---

You can do ahead-of-time analysis for your queries.

The primitive for analysis is {{ "GraphQL::Analysis::AST::Analyzer" | api_doc }}. Analyzers must inherit from this base class and implement the desired methods for analysis.

### Using Analyzers

Query analyzers are added to the schema with `query_analyzer`, for example:

```ruby
class MySchema < GraphQL::Schema
  query_analyzer MyQueryAnalyzer
end
```

Pass the **class** (and not an _instance_) of your analyzer. The analysis engine will take care of instantiating your analyzers with the query.

## Analyzer API

Analyzers respond to methods similar to AST visitors. They're named like `on_enter_#{ast_node}` and `on_leave_#{ast_node}`. Methods are called with three arguments:

- `node`: The current AST node (being entered or left)
- `parent`: The AST node which preceeds this one in the tree
- `visitor`: A {{ "GraphQL::Analysis::AST::Visitor" | api_doc }} which is managing this analysis run

For example:

```ruby
class BasicCounterAnalyzer < GraphQL::Analysis::AST::Analyzer
  def initialize(query_or_multiplex)
    super
    @fields = Set.new
    @arguments = Set.new
  end

  # Visitors are all defined on the AST::Analyzer base class
  # We override them for custom analyzers.
  def on_leave_field(node, _parent, _visitor)
    @fields.add(node.name)
  end

  def result
    # Do something with the gathered result.
    Analytics.log(@fields)
  end
end
```

In this example, we counted every field, no matter if it was on fragment definitions
or if it was skipped by directives. If we want to detect those contexts, we can use helper
methods:

```ruby
class BasicFieldAnalyzer < GraphQL::Analysis::AST::Analyzer
  def initialize(query_or_multiplex)
    super
    @fields = Set.new
  end

  # Visitors are all defined on the AST::Analyzer base class
  # We override them for custom analyzers.
  def on_leave_field(node, _parent, visitor)
    if visitor.skipping? || visitor.visiting_fragment_definition?
      # We don't want to count skipped fields or fields
      # inside fragment definitions
    else
      @fields.add(node.name)
    end
  end

  def result
    Analytics.log(@fields)
  end
end
```

See {{ "GraphQL::Analysis::AST::Visitor" | api_doc }} for more information about the `visitor` object.

### Field Arguments

Usually, analyzers will use `on_enter_field` and `on_leave_field` to process queries. To get a field's arguments during analysis, use `visitor.query.arguments_for(node, visitor.field_definition)` ({{ "GraphQL::Query#arguments_for" | api_doc }}). That method returns coerced argument values and normalizes argument literals and variable values.

### Errors

It is still possible to return errors from an analyzer. To reject a query and halt its execution, you may return {{ "GraphQL::AnalysisError" | api_doc }} in the `result` method:

```ruby
class NoFieldsCalledHello < GraphQL::Analysis::AST::Analyzer
  def on_leave_field(node, _parent, visitor)
    if node.name == "hello"
      @field_called_hello = true
    end
  end

  def result
    GraphQL::AnalysisError.new("A field called `hello` was found.") if @field_called_hello
  end
end
```

### Conditional Analysis

Some analyzers might only make sense in certain context, or some might be too expensive to run for every query. To handle these scenarios, your analyzers may answer to an `analyze?` method:

```ruby
class BasicFieldAnalyzer < GraphQL::Analysis::AST::Analyzer
  # Use the analyze? method to enable or disable a certain analyzer
  # at query time.
  def analyze?
    !!subject.context[:should_analyze]
  end

  def on_leave_field(node, _parent, visitor)
    # ...
  end

  def result
    # ...
  end
end
```

## Analyzing Multiplexes

Analyzers are initialized with the _unit of analysis_, available as `subject`.

When analyzers are hooked up to multiplexes, `query` is `nil`, but `multiplex` returns the subject of analysis. You can use `visitor.query` inside visit methods to reference the query that owns the current AST node.

Note that some built-in analyzers (eg `AST::MaxQueryDepth`) support multiplexes even though `Query` is in their name.