graphql 0.10.9 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. checksums.yaml +4 -4
  2. data/lib/graphql.rb +8 -5
  3. data/lib/graphql/definition_helpers/defined_by_config.rb +2 -1
  4. data/lib/graphql/field.rb +34 -7
  5. data/lib/graphql/input_object_type.rb +4 -3
  6. data/lib/graphql/invalid_null_error.rb +22 -0
  7. data/lib/graphql/language/nodes.rb +165 -58
  8. data/lib/graphql/language/transform.rb +5 -6
  9. data/lib/graphql/non_null_type.rb +0 -4
  10. data/lib/graphql/query.rb +1 -2
  11. data/lib/graphql/query/arguments.rb +39 -7
  12. data/lib/graphql/query/literal_input.rb +1 -1
  13. data/lib/graphql/query/serial_execution.rb +30 -1
  14. data/lib/graphql/query/serial_execution/execution_context.rb +30 -0
  15. data/lib/graphql/query/serial_execution/field_resolution.rb +27 -21
  16. data/lib/graphql/query/serial_execution/operation_resolution.rb +9 -6
  17. data/lib/graphql/query/serial_execution/selection_resolution.rb +49 -56
  18. data/lib/graphql/query/{base_execution → serial_execution}/value_resolution.rb +35 -24
  19. data/lib/graphql/static_validation/complexity_validator.rb +27 -0
  20. data/lib/graphql/static_validation/literal_validator.rb +4 -4
  21. data/lib/graphql/static_validation/rules/argument_literals_are_compatible.rb +1 -0
  22. data/lib/graphql/static_validation/rules/variable_default_values_are_correctly_typed.rb +1 -1
  23. data/lib/graphql/static_validation/rules/variables_are_input_types.rb +1 -1
  24. data/lib/graphql/static_validation/validator.rb +1 -1
  25. data/lib/graphql/version.rb +1 -1
  26. data/readme.md +8 -4
  27. data/spec/graphql/execution_error_spec.rb +7 -1
  28. data/spec/graphql/field_spec.rb +53 -0
  29. data/spec/graphql/input_object_type_spec.rb +8 -1
  30. data/spec/graphql/introspection/schema_type_spec.rb +2 -1
  31. data/spec/graphql/language/transform_spec.rb +14 -14
  32. data/spec/graphql/object_type_spec.rb +7 -0
  33. data/spec/graphql/query/arguments_spec.rb +5 -5
  34. data/spec/graphql/query/executor_spec.rb +31 -0
  35. data/spec/graphql/query/serial_execution/execution_context_spec.rb +55 -0
  36. data/spec/graphql/query/{base_execution → serial_execution}/value_resolution_spec.rb +1 -1
  37. data/spec/graphql/query/variables_spec.rb +1 -1
  38. data/spec/graphql/static_validation/complexity_validator.rb +15 -0
  39. data/spec/graphql/static_validation/rules/argument_literals_are_compatible_spec.rb +2 -1
  40. data/spec/graphql/static_validation/validator_spec.rb +1 -1
  41. data/spec/support/dairy_app.rb +15 -0
  42. data/spec/support/minimum_input_object.rb +13 -0
  43. metadata +14 -6
  44. data/lib/graphql/query/base_execution.rb +0 -32
@@ -21,10 +21,6 @@ class GraphQL::NonNullType < GraphQL::BaseType
21
21
  of_type.coerce_input(value)
22
22
  end
23
23
 
24
- def coerce_result(value)
25
- of_type.coerce_result(value)
26
- end
27
-
28
24
  def kind
29
25
  GraphQL::TypeKinds::NON_NULL
30
26
  end
@@ -44,7 +44,7 @@ class GraphQL::Query
44
44
  @operations = {}
45
45
  @provided_variables = variables
46
46
  @document = GraphQL.parse(query_string)
47
- @document.parts.each do |part|
47
+ @document.definitions.each do |part|
48
48
  if part.is_a?(GraphQL::Language::Nodes::FragmentDefinition)
49
49
  @fragments[part.name] = part
50
50
  elsif part.is_a?(GraphQL::Language::Nodes::OperationDefinition)
@@ -104,7 +104,6 @@ class GraphQL::Query
104
104
  end
105
105
 
106
106
  require 'graphql/query/arguments'
107
- require 'graphql/query/base_execution'
108
107
  require 'graphql/query/context'
109
108
  require 'graphql/query/directive_chain'
110
109
  require 'graphql/query/executor'
@@ -7,8 +7,8 @@ module GraphQL
7
7
  extend Forwardable
8
8
 
9
9
  def initialize(values)
10
- @hash = values
11
- @values = values.inject({}) do |memo, (inner_key, inner_value)|
10
+ @original_values = values
11
+ @argument_values = values.inject({}) do |memo, (inner_key, inner_value)|
12
12
  memo[inner_key.to_s] = wrap_value(inner_value)
13
13
  memo
14
14
  end
@@ -17,28 +17,60 @@ module GraphQL
17
17
  # @param [String, Symbol] name or index of value to access
18
18
  # @return [Object] the argument at that key
19
19
  def [](key)
20
- @values[key.to_s]
20
+ @argument_values[key.to_s]
21
21
  end
22
22
 
23
23
  # Get the original Ruby hash
24
24
  # @return [Hash] the original values hash
25
25
  def to_h
26
- @hash
26
+ @unwrapped_values ||= unwrap_value(@original_values)
27
27
  end
28
28
 
29
- def_delegators :@values, :keys, :values, :each
29
+ def_delegators :string_key_values, :keys, :values, :each
30
30
 
31
31
  private
32
32
 
33
33
  def wrap_value(value)
34
- if value.is_a?(Array)
34
+ case value
35
+ when Array
35
36
  value.map { |item| wrap_value(item) }
36
- elsif value.is_a?(Hash)
37
+ when Hash
37
38
  self.class.new(value)
38
39
  else
39
40
  value
40
41
  end
41
42
  end
43
+
44
+ def unwrap_value(value)
45
+ case value
46
+ when Array
47
+ value.map { |item| unwrap_value(item) }
48
+ when Hash
49
+ value.inject({}) do |memo, (key, value)|
50
+ memo[key] = unwrap_value(value)
51
+ memo
52
+ end
53
+ when GraphQL::Query::Arguments
54
+ value.to_h
55
+ else
56
+ value
57
+ end
58
+ end
59
+
60
+ def string_key_values
61
+ @string_key_values ||= stringify_keys(to_h)
62
+ end
63
+
64
+ def stringify_keys(value)
65
+ case value
66
+ when Hash
67
+ value.inject({}) { |memo, (k, v)| memo[k.to_s] = stringify_keys(v); memo }
68
+ when Array
69
+ value.map { |v| stringify_keys(v) }
70
+ else
71
+ value
72
+ end
73
+ end
42
74
  end
43
75
  end
44
76
  end
@@ -48,7 +48,7 @@ module GraphQL
48
48
  module InputObjectLiteral
49
49
  def self.coerce(value, type, variables)
50
50
  hash = {}
51
- value.pairs.each do |arg|
51
+ value.arguments.each do |arg|
52
52
  field_type = type.input_fields[arg.name].type
53
53
  hash[arg.name] = LiteralInput.coerce(field_type, arg.value, variables)
54
54
  end
@@ -1,10 +1,39 @@
1
1
  module GraphQL
2
2
  class Query
3
- class SerialExecution < GraphQL::Query::BaseExecution
3
+ class SerialExecution
4
+ # This is the only required method for an Execution strategy.
5
+ # You could create a custom execution strategy and configure your schema to
6
+ # use that custom strategy instead.
7
+ #
8
+ # @param ast_operation [GraphQL::Language::Nodes::OperationDefinition] The operation definition to run
9
+ # @param root_type [GraphQL::ObjectType] either the query type or the mutation type
10
+ # @param query_obj [GraphQL::Query] the query object for this execution
11
+ # @return [Hash] a spec-compliant GraphQL result, as a hash
12
+ def execute(ast_operation, root_type, query_obj)
13
+ operation_resolution.new(
14
+ ast_operation,
15
+ root_type,
16
+ ExecutionContext.new(query_obj, self)
17
+ ).result
18
+ end
19
+
20
+ def field_resolution
21
+ self.class::FieldResolution
22
+ end
23
+
24
+ def operation_resolution
25
+ self.class::OperationResolution
26
+ end
27
+
28
+ def selection_resolution
29
+ self.class::SelectionResolution
30
+ end
4
31
  end
5
32
  end
6
33
  end
7
34
 
35
+ require 'graphql/query/serial_execution/execution_context'
36
+ require 'graphql/query/serial_execution/value_resolution'
8
37
  require 'graphql/query/serial_execution/field_resolution'
9
38
  require 'graphql/query/serial_execution/operation_resolution'
10
39
  require 'graphql/query/serial_execution/selection_resolution'
@@ -0,0 +1,30 @@
1
+ module GraphQL
2
+ class Query
3
+ class SerialExecution
4
+ class ExecutionContext
5
+ attr_reader :query, :strategy
6
+
7
+ def initialize(query, strategy)
8
+ @query = query
9
+ @strategy = strategy
10
+ end
11
+
12
+ def get_type(type)
13
+ @query.schema.types[type]
14
+ end
15
+
16
+ def get_fragment(name)
17
+ @query.fragments[name]
18
+ end
19
+
20
+ def get_field(type, name)
21
+ @query.schema.get_field(type, name)
22
+ end
23
+
24
+ def add_error(err)
25
+ @query.context.errors << err
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
@@ -2,22 +2,30 @@ module GraphQL
2
2
  class Query
3
3
  class SerialExecution
4
4
  class FieldResolution
5
- attr_reader :ast_node, :parent_type, :target, :query, :execution_strategy, :field, :arguments
5
+ attr_reader :ast_node, :parent_type, :target, :execution_context, :field, :arguments
6
6
 
7
- def initialize(ast_node, parent_type, target, query, execution_strategy)
7
+ def initialize(ast_node, parent_type, target, execution_context)
8
8
  @ast_node = ast_node
9
9
  @parent_type = parent_type
10
10
  @target = target
11
- @query = query
12
- @execution_strategy = execution_strategy
13
- @field = query.schema.get_field(parent_type, ast_node.name) || raise("No field found on #{parent_type.name} '#{parent_type}' for '#{ast_node.name}'")
14
- @arguments = GraphQL::Query::LiteralInput.from_arguments(ast_node.arguments, field.arguments, query.variables)
11
+ @execution_context = execution_context
12
+ @field = execution_context.get_field(parent_type, ast_node.name)
13
+ raise("No field found on #{parent_type.name} '#{parent_type}' for '#{ast_node.name}'") unless field
14
+ @arguments = GraphQL::Query::LiteralInput.from_arguments(
15
+ ast_node.arguments,
16
+ field.arguments,
17
+ execution_context.query.variables
18
+ )
15
19
  end
16
20
 
17
21
  def result
18
22
  result_name = ast_node.alias || ast_node.name
19
- result_value = get_finished_value(get_raw_value)
20
- { result_name => result_value }
23
+ raw_value = begin
24
+ get_raw_value
25
+ rescue GraphQL::ExecutionError => err
26
+ err
27
+ end
28
+ { result_name => get_finished_value(raw_value) }
21
29
  end
22
30
 
23
31
  private
@@ -25,18 +33,14 @@ module GraphQL
25
33
  # After getting the value from the field's resolve method,
26
34
  # continue by "finishing" the value, eg. executing sub-fields or coercing values
27
35
  def get_finished_value(raw_value)
28
- if raw_value.nil?
29
- nil
30
- elsif raw_value.is_a?(GraphQL::ExecutionError)
36
+ if raw_value.is_a?(GraphQL::ExecutionError)
31
37
  raw_value.ast_node = ast_node
32
- query.context.errors << raw_value
33
- nil
34
- else
35
- resolved_type = field.type.resolve_type(raw_value)
36
- strategy_class = GraphQL::Query::BaseExecution::ValueResolution.get_strategy_for_kind(resolved_type.kind)
37
- result_strategy = strategy_class.new(raw_value, resolved_type, target, parent_type, ast_node, query, execution_strategy)
38
- result_strategy.result
38
+ execution_context.add_error(raw_value)
39
39
  end
40
+
41
+ strategy_class = GraphQL::Query::SerialExecution::ValueResolution.get_strategy_for_kind(field.type.kind)
42
+ result_strategy = strategy_class.new(raw_value, field.type, target, parent_type, ast_node, execution_context)
43
+ result_strategy.result
40
44
  end
41
45
 
42
46
 
@@ -44,12 +48,14 @@ module GraphQL
44
48
  # - Any middleware on this schema
45
49
  # - The field's resolve method
46
50
  def get_raw_value
47
- steps = query.schema.middleware + [get_middleware_proc_from_field_resolve]
51
+ steps = execution_context.query.schema.middleware + [get_middleware_proc_from_field_resolve]
48
52
  chain = GraphQL::Schema::MiddlewareChain.new(
49
53
  steps: steps,
50
- arguments: [parent_type, target, field, arguments, query.context]
54
+ arguments: [parent_type, target, field, arguments, execution_context.query.context]
51
55
  )
52
- chain.call
56
+ value = chain.call
57
+ raise value if value.instance_of?(GraphQL::ExecutionError)
58
+ value
53
59
  end
54
60
 
55
61
 
@@ -2,19 +2,22 @@ module GraphQL
2
2
  class Query
3
3
  class SerialExecution
4
4
  class OperationResolution
5
- attr_reader :query, :target, :ast_operation_definition, :execution_strategy
5
+ attr_reader :query, :target, :ast_operation_definition, :execution_context
6
6
 
7
- def initialize(ast_operation_definition, target, query, execution_strategy)
7
+ def initialize(ast_operation_definition, target, execution_context)
8
8
  @ast_operation_definition = ast_operation_definition
9
- @query = query
10
9
  @target = target
11
- @execution_strategy = execution_strategy
10
+ @execution_context = execution_context
12
11
  end
13
12
 
14
13
  def result
15
14
  selections = ast_operation_definition.selections
16
- resolver = execution_strategy.selection_resolution.new(nil, target, selections, query, execution_strategy)
17
- resolver.result
15
+ execution_context.strategy.selection_resolution.new(
16
+ nil,
17
+ target,
18
+ selections,
19
+ execution_context
20
+ ).result
18
21
  end
19
22
  end
20
23
  end
@@ -2,33 +2,24 @@ module GraphQL
2
2
  class Query
3
3
  class SerialExecution
4
4
  class SelectionResolution
5
- attr_reader :target, :type, :selections, :query, :execution_strategy
5
+ attr_reader :target, :type, :selections, :execution_context
6
6
 
7
- def initialize(target, type, selections, query, execution_strategy)
7
+ def initialize(target, type, selections, execution_context)
8
8
  @target = target
9
9
  @type = type
10
10
  @selections = selections
11
- @query = query
12
- @execution_strategy = execution_strategy
11
+ @execution_context = execution_context
13
12
  end
14
13
 
15
14
  def result
16
- # In a first pass, we flatten the selection by merging in fields from
17
- # any fragments - this prevents us from resolving the same fields
18
- # more than one time in cases where fragments repeat fields.
19
- # Then, In a second pass, we resolve the flattened set of fields
20
- selections
21
- .reduce({}){|memo, ast_node|
22
- flattened_selections = flatten_selection(ast_node)
23
- flattened_selections.each do |name, selection|
24
- merge_into_result(memo, selection)
25
- end
26
- memo
27
- }
15
+ flatten_and_merge_selections(selections)
28
16
  .values
29
- .reduce({}){|memo, ast_node|
30
- memo.merge(resolve_field(ast_node))
17
+ .reduce({}) { |result, ast_node|
18
+ result.merge(resolve_field(ast_node))
31
19
  }
20
+ rescue GraphQL::InvalidNullError => err
21
+ execution_context.add_error(err) unless err.parent_error?
22
+ nil
32
23
  end
33
24
 
34
25
  private
@@ -46,67 +37,59 @@ module GraphQL
46
37
 
47
38
  def flatten_field(ast_node)
48
39
  result_name = ast_node.alias || ast_node.name
49
- return { result_name => ast_node }
40
+ { result_name => ast_node }
50
41
  end
51
42
 
52
43
  def flatten_inline_fragment(ast_node)
53
- chain = GraphQL::Query::DirectiveChain.new(ast_node, query) {
44
+ chain = GraphQL::Query::DirectiveChain.new(ast_node, execution_context.query) {
54
45
  flatten_fragment(ast_node)
55
46
  }
56
47
  chain.result
57
48
  end
58
49
 
59
50
  def flatten_fragment_spread(ast_node)
60
- ast_fragment_defn = query.fragments[ast_node.name]
61
- chain = GraphQL::Query::DirectiveChain.new(ast_node, query) {
51
+ ast_fragment_defn = execution_context.get_fragment(ast_node.name)
52
+ chain = GraphQL::Query::DirectiveChain.new(ast_node, execution_context.query) {
62
53
  flatten_fragment(ast_fragment_defn)
63
54
  }
64
55
  chain.result
65
56
  end
66
57
 
67
58
  def flatten_fragment(ast_fragment)
68
- if !fragment_type_can_apply?(ast_fragment)
69
- return {}
70
- end
71
-
72
- flat_result = {}
73
- ast_fragment.selections.each do |selection|
74
- flat_selection = flatten_selection(selection)
75
- flat_selection.each do |name, selection|
76
- merge_into_result(flat_result, selection)
77
- end
78
- end
79
- flat_result
59
+ return {} unless fragment_type_can_apply?(ast_fragment)
60
+ flatten_and_merge_selections(ast_fragment.selections)
80
61
  end
81
62
 
82
63
  def fragment_type_can_apply?(ast_fragment)
83
- child_type = query.schema.types[ast_fragment.type]
64
+ child_type = execution_context.get_type(ast_fragment.type)
84
65
  resolved_type = GraphQL::Query::TypeResolver.new(target, child_type, type).type
85
66
  !resolved_type.nil?
86
67
  end
87
68
 
88
69
  def merge_fields(field1, field2)
89
- field_type = query.schema.get_field(type, field2.name).type.unwrap
90
-
91
- if field_type.kind.fields?
92
- # create a new ast field node merging selections from each field.
93
- # Because of static validation, we can assume that name, alias,
94
- # arguments, and directives are exactly the same for fields 1 and 2.
95
- GraphQL::Language::Nodes::Field.new(
96
- name: field2.name,
97
- alias: field2.alias,
98
- arguments: field2.arguments,
99
- directives: field2.directives,
100
- selections: field1.selections + field2.selections
101
- )
102
- else
103
- field2
104
- end
70
+ field_type = execution_context.get_field(type, field2.name).type.unwrap
71
+ return field2 unless field_type.kind.fields?
72
+
73
+ # create a new ast field node merging selections from each field.
74
+ # Because of static validation, we can assume that name, alias,
75
+ # arguments, and directives are exactly the same for fields 1 and 2.
76
+ GraphQL::Language::Nodes::Field.new(
77
+ name: field2.name,
78
+ alias: field2.alias,
79
+ arguments: field2.arguments,
80
+ directives: field2.directives,
81
+ selections: field1.selections + field2.selections
82
+ )
105
83
  end
106
84
 
107
85
  def resolve_field(ast_node)
108
- chain = GraphQL::Query::DirectiveChain.new(ast_node, query) {
109
- execution_strategy.field_resolution.new(ast_node, type, target, query, execution_strategy).result
86
+ chain = GraphQL::Query::DirectiveChain.new(ast_node, execution_context.query) {
87
+ execution_context.strategy.field_resolution.new(
88
+ ast_node,
89
+ type,
90
+ target,
91
+ execution_context
92
+ ).result
110
93
  }
111
94
  chain.result
112
95
  end
@@ -118,10 +101,20 @@ module GraphQL
118
101
  selection.name
119
102
  end
120
103
 
121
- if memo.has_key?(name)
122
- memo[name] = merge_fields(memo[name], selection)
104
+ memo[name] = if memo.has_key?(name)
105
+ merge_fields(memo[name], selection)
123
106
  else
124
- memo[name] = selection
107
+ selection
108
+ end
109
+ end
110
+
111
+ def flatten_and_merge_selections(selections)
112
+ selections.reduce({}) do |result, ast_node|
113
+ flattened_selections = flatten_selection(ast_node)
114
+ flattened_selections.each do |name, selection|
115
+ merge_into_result(result, selection)
116
+ end
117
+ result
125
118
  end
126
119
  end
127
120
  end