forked from absinthe-graphql/absinthe
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Introduce complexity analysis phase (absinthe-graphql#260)
* Introduce complexity analysis phase By default adds 1 per field in the query. This can be overriden by using a custom complexity analyser that is 2-arity function. The first argument is the field's context and the second argument is the complexity of the children. Heavily influnced by Sangria. * Provide info struct to arity-3 complexity funs * Error for each operation or field that is too complex * Fix typo in field docs * Move complexity error handling to result phase * Add nested error case complexity tests Thank you to Bruce Williams for help with the tests * Support non_neg_integer complexity * Fix overwriting existing field complexity * Fix complexity_t to include non_neg_integer * Raise on negative complexity value * Document 3-arity complexity analyser * Add 3-arity fun and remote call to complexity_t type * Add :flat flag to skip walking selections after flatten * Only run complexity analysis when enabled * Document complexity options and rename analyse -> analyze * Fix complexity remote call definition
- Loading branch information
1 parent
0dbc20b
commit 7529731
Showing
14 changed files
with
525 additions
and
4 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -7,4 +7,5 @@ erl_crash.dump | |
*.ez | ||
src/*.erl | ||
.tool-versions* | ||
missing_rules.rb | ||
missing_rules.rb | ||
.DS_Store |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
defmodule Absinthe.Complexity do | ||
@moduledoc """ | ||
Extra metadata passed to aid complexity analysis functions, describing the | ||
current field's environment. | ||
""" | ||
alias Absinthe.{Blueprint, Schema} | ||
|
||
@enforce_keys [:context, :root_value, :schema, :definition] | ||
defstruct [:context, :root_value, :schema, :definition] | ||
|
||
@typedoc """ | ||
- `:definition` - The Blueprint definition for this field. | ||
- `:context` - The context passed to `Absinthe.run`. | ||
- `:root_value` - The root value passed to `Absinthe.run`, if any. | ||
- `:schema` - The current schema. | ||
""" | ||
@type t :: %__MODULE__{ | ||
definition: Blueprint.node_t, | ||
context: map, | ||
root_value: any, | ||
schema: Schema.t | ||
} | ||
|
||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,107 @@ | ||
defmodule Absinthe.Phase.Document.Complexity.Analysis do | ||
@moduledoc false | ||
|
||
# Analyses document complexity. | ||
|
||
alias Absinthe.{Blueprint, Phase, Complexity} | ||
|
||
use Absinthe.Phase | ||
|
||
@default_complexity 1 | ||
|
||
@doc """ | ||
Run complexity analysis. | ||
""" | ||
@spec run(Blueprint.t, Keyword.t) :: Phase.result_t | ||
def run(input, options \\ []) do | ||
if Keyword.get(options, :analyze_complexity, false) do | ||
do_run(input, options) | ||
else | ||
{:ok, input} | ||
end | ||
end | ||
|
||
defp do_run(input, options) do | ||
info_data = info_boilerplate(input, options) | ||
fun = &handle_node(&1, info_data) | ||
{:ok, Blueprint.update_current(input, &Blueprint.postwalk(&1, fun))} | ||
end | ||
|
||
def handle_node(%Blueprint.Document.Field{complexity: nil, | ||
fields: fields, | ||
argument_data: args, | ||
schema_node: schema_node} = node, info_data) do | ||
child_complexity = sum_complexity(fields) | ||
case field_complexity(schema_node, args, child_complexity, info_data, node) do | ||
complexity when is_integer(complexity) and complexity >= 0 -> | ||
%{node | complexity: complexity} | ||
other -> | ||
raise Absinthe.AnalysisError, field_value_error(node, other) | ||
end | ||
end | ||
def handle_node(%Blueprint.Document.Operation{complexity: nil, fields: fields} = node, _) do | ||
complexity = sum_complexity(fields) | ||
%{node | complexity: complexity} | ||
end | ||
def handle_node(node, _) do | ||
node | ||
end | ||
|
||
defp field_complexity(%{complexity: nil}, _, child_complexity, _, _) do | ||
@default_complexity + child_complexity | ||
end | ||
defp field_complexity(%{complexity: complexity}, arg, child_complexity, _, _) | ||
when is_function(complexity, 2) do | ||
complexity.(arg, child_complexity) | ||
end | ||
defp field_complexity(%{complexity: complexity}, arg, child_complexity, info_data, node) | ||
when is_function(complexity, 3) do | ||
info = struct(Complexity, Map.put(info_data, :definition, node)) | ||
complexity.(arg, child_complexity, info) | ||
end | ||
defp field_complexity(%{complexity: {mod, fun}}, arg, child_complexity, info_data, node) do | ||
info = struct(Complexity, Map.put(info_data, :definition, node)) | ||
apply(mod, fun, [arg, child_complexity, info]) | ||
end | ||
defp field_complexity(%{complexity: complexity}, _, _, _, _) do | ||
complexity | ||
end | ||
|
||
defp field_value_error(field, value) do | ||
""" | ||
Invalid value returned from complexity analyzer. | ||
Analyzing field: | ||
#{field.name} | ||
Defined at: | ||
#{field.schema_node.__reference__.location.file}:#{field.schema_node.__reference__.location.line} | ||
Got value: | ||
#{inspect value} | ||
The complexity value must be a non negative integer. | ||
""" | ||
end | ||
|
||
defp sum_complexity(fields) do | ||
Enum.reduce(fields, 0, &sum_complexity/2) | ||
end | ||
|
||
defp sum_complexity(%{complexity: complexity}, acc) do | ||
complexity + acc | ||
end | ||
|
||
# Execution context data that's common to all fields | ||
defp info_boilerplate(bp_root, options) do | ||
%{ | ||
context: Keyword.get(options, :context, %{}), | ||
root_value: Keyword.get(options, :root_value, %{}), | ||
schema: bp_root.schema | ||
} | ||
end | ||
|
||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,56 @@ | ||
defmodule Absinthe.Phase.Document.Complexity.Result do | ||
@moduledoc false | ||
|
||
# Collects complexity errors into the result. | ||
|
||
alias Absinthe.{Blueprint, Phase} | ||
|
||
use Absinthe.Phase | ||
|
||
@doc """ | ||
Run the validation. | ||
""" | ||
@spec run(Blueprint.t, Keyword.t) :: Phase.result_t | ||
def run(input, options \\ []) do | ||
max = Keyword.get(options, :max_complexity, :infinity) | ||
operation = Blueprint.current_operation(input) | ||
fun = &handle_node(&1, max, &2) | ||
{operation, errors} = Blueprint.prewalk(operation, [], fun) | ||
result = Blueprint.update_current(input, fn(_) -> operation end) | ||
result = put_in(result.resolution.validation, errors) | ||
case {errors, Map.new(options)} do | ||
{[_|_], %{jump_phases: true, result_phase: abort_phase}} -> | ||
{:jump, result, abort_phase} | ||
_ -> | ||
{:ok, result} | ||
end | ||
end | ||
|
||
defp handle_node(%{complexity: complexity} = node, max, errors) | ||
when is_integer(complexity) and complexity > max do | ||
error = error(node, complexity, max) | ||
node = | ||
node | ||
|> flag_invalid(:too_complex) | ||
|> put_error(error) | ||
{node, [error | errors]} | ||
end | ||
defp handle_node(%{complexity: _} = node, _, errors) do | ||
{:halt, node, errors} | ||
end | ||
defp handle_node(node, _, errors) do | ||
{node, errors} | ||
end | ||
|
||
defp error(%{name: name, source_location: location}, complexity, max) do | ||
Phase.Error.new( | ||
__MODULE__, | ||
error_message(name, complexity, max), | ||
location: location | ||
) | ||
end | ||
|
||
def error_message(name, complexity, max) do | ||
"#{name} is too complex: complexity is #{complexity} and maximum is #{max}" | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.