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
@@ -1,6 +1,6 @@
1
1
  module GraphQL
2
2
  class Query
3
- class BaseExecution
3
+ class SerialExecution
4
4
  module ValueResolution
5
5
  def self.get_strategy_for_kind(kind)
6
6
  TYPE_KIND_STRATEGIES[kind] || raise("No value resolution strategy for #{kind}!")
@@ -8,29 +8,33 @@ module GraphQL
8
8
 
9
9
  class BaseResolution
10
10
  attr_reader :value, :field_type, :target, :parent_type,
11
- :ast_field, :query, :execution_strategy
12
- def initialize(value, field_type, target, parent_type, ast_field, query, execution_strategy)
11
+ :ast_field, :execution_context
12
+ def initialize(value, field_type, target, parent_type, ast_field, execution_context)
13
13
  @value = value
14
14
  @field_type = field_type
15
15
  @target = target
16
16
  @parent_type = parent_type
17
17
  @ast_field = ast_field
18
- @query = query
19
- @execution_strategy = execution_strategy
18
+ @execution_context = execution_context
20
19
  end
21
20
 
22
21
  def result
22
+ return nil if value.nil? || value.is_a?(GraphQL::ExecutionError)
23
+ non_null_result
24
+ end
25
+
26
+ def non_null_result
23
27
  raise NotImplementedError, "Should return a value based on initialization params"
24
28
  end
25
29
 
26
30
  def get_strategy_for_kind(*args)
27
- GraphQL::Query::BaseExecution::ValueResolution.get_strategy_for_kind(*args)
31
+ GraphQL::Query::SerialExecution::ValueResolution.get_strategy_for_kind(*args)
28
32
  end
29
33
  end
30
34
 
31
35
  class ScalarResolution < BaseResolution
32
36
  # Apply the scalar's defined `coerce_result` method to the value
33
- def result
37
+ def non_null_result
34
38
  field_type.coerce_result(value)
35
39
  end
36
40
  end
@@ -38,39 +42,44 @@ module GraphQL
38
42
  class ListResolution < BaseResolution
39
43
  # For each item in the list,
40
44
  # Resolve it with the "wrapped" type of this list
41
- def result
45
+ def non_null_result
42
46
  wrapped_type = field_type.of_type
47
+ strategy_class = get_strategy_for_kind(wrapped_type.kind)
43
48
  value.map do |item|
44
- resolved_type = wrapped_type.resolve_type(item)
45
- strategy_class = get_strategy_for_kind(resolved_type.kind)
46
- inner_strategy = strategy_class.new(item, resolved_type, target, parent_type, ast_field, query, execution_strategy)
49
+ inner_strategy = strategy_class.new(item, wrapped_type, target, parent_type, ast_field, execution_context)
47
50
  inner_strategy.result
48
51
  end
49
52
  end
50
53
  end
51
54
 
52
- class ObjectResolution < BaseResolution
53
- # Resolve the selections on this object
54
- def result
55
- resolver = execution_strategy.selection_resolution.new(value, field_type, ast_field.selections, query, execution_strategy)
56
- resolver.result
55
+ class HasPossibleTypeResolution < BaseResolution
56
+ def non_null_result
57
+ resolved_type = field_type.resolve_type(value)
58
+ strategy_class = get_strategy_for_kind(resolved_type.kind)
59
+ inner_strategy = strategy_class.new(value, resolved_type, target, parent_type, ast_field, execution_context)
60
+ inner_strategy.result
57
61
  end
58
62
  end
59
63
 
60
- class EnumResolution < BaseResolution
61
- # Get the string name for this enum value
62
- def result
63
- field_type.coerce_result(value)
64
+ class ObjectResolution < BaseResolution
65
+ # Resolve the selections on this object
66
+ def non_null_result
67
+ execution_context.strategy.selection_resolution.new(
68
+ value,
69
+ field_type,
70
+ ast_field.selections,
71
+ execution_context
72
+ ).result
64
73
  end
65
74
  end
66
75
 
67
76
  class NonNullResolution < BaseResolution
68
77
  # Get the "wrapped" type and resolve the value according to that type
69
78
  def result
79
+ raise GraphQL::InvalidNullError.new(ast_field.name, value) if value.nil? || value.is_a?(GraphQL::ExecutionError)
70
80
  wrapped_type = field_type.of_type
71
- resolved_type = wrapped_type.resolve_type(value)
72
- strategy_class = get_strategy_for_kind(resolved_type.kind)
73
- inner_strategy = strategy_class.new(value, resolved_type, target, parent_type, ast_field, query, execution_strategy)
81
+ strategy_class = get_strategy_for_kind(wrapped_type.kind)
82
+ inner_strategy = strategy_class.new(value, wrapped_type, target, parent_type, ast_field, execution_context)
74
83
  inner_strategy.result
75
84
  end
76
85
  end
@@ -79,8 +88,10 @@ module GraphQL
79
88
  GraphQL::TypeKinds::SCALAR => ScalarResolution,
80
89
  GraphQL::TypeKinds::LIST => ListResolution,
81
90
  GraphQL::TypeKinds::OBJECT => ObjectResolution,
82
- GraphQL::TypeKinds::ENUM => EnumResolution,
91
+ GraphQL::TypeKinds::ENUM => ScalarResolution,
83
92
  GraphQL::TypeKinds::NON_NULL => NonNullResolution,
93
+ GraphQL::TypeKinds::INTERFACE => HasPossibleTypeResolution,
94
+ GraphQL::TypeKinds::UNION => HasPossibleTypeResolution,
84
95
  }
85
96
  end
86
97
  end
@@ -0,0 +1,27 @@
1
+ class GraphQL::StaticValidation::ComplexityValidator
2
+ include GraphQL::StaticValidation::Message::MessageHelper
3
+
4
+ def initialize(max_fields:, list_multiplier:)
5
+ @max_fields = max_fields
6
+ @list_multiplier = list_multiplier
7
+ end
8
+
9
+ def validate(context)
10
+ visitor = context.visitor
11
+ complexity = 0
12
+ visitor[GraphQL::Language::Nodes::Field] << -> (node, parent) {
13
+ field_type = field_definition.type
14
+ if field_type.kind.list?
15
+ complexity += list_multiplier
16
+ else
17
+ complexity += 1
18
+ end
19
+ }
20
+
21
+ visitor[GraphQL::Language::Nodes::Document].exit << -> (node, parent) {
22
+ if complexity > @max_fields
23
+ context.errors << message("This query is too complex. Request fewer fields.", node)
24
+ end
25
+ }
26
+ end
27
+ end
@@ -29,16 +29,16 @@ class GraphQL::StaticValidation::LiteralValidator
29
29
  .values
30
30
  .select { |f| f.type.kind.non_null? }
31
31
  .map(&:name)
32
- present_field_names = ast_node.pairs.map(&:name)
32
+ present_field_names = ast_node.arguments.map(&:name)
33
33
  missing_required_field_names = required_field_names - present_field_names
34
34
  missing_required_field_names.none?
35
35
  end
36
36
 
37
37
  def present_input_field_values_are_valid(type, ast_node)
38
38
  fields = type.input_fields
39
- ast_node.pairs.all? do |value|
40
- field_type = fields[value.name].type
41
- validate(value.value, field_type)
39
+ ast_node.arguments.all? do |value|
40
+ field = fields[value.name]
41
+ field ? validate(value.value, field.type) : true
42
42
  end
43
43
  end
44
44
 
@@ -3,6 +3,7 @@ class GraphQL::StaticValidation::ArgumentLiteralsAreCompatible < GraphQL::Static
3
3
  return if node.value.is_a?(GraphQL::Language::Nodes::VariableIdentifier)
4
4
  validator = GraphQL::StaticValidation::LiteralValidator.new
5
5
  arg_defn = defn.arguments[node.name]
6
+ return unless arg_defn
6
7
  valid = validator.validate(node.value, arg_defn.type)
7
8
  if !valid
8
9
  kind_of_node = node_type(parent)
@@ -3,7 +3,7 @@ class GraphQL::StaticValidation::VariableDefaultValuesAreCorrectlyTyped
3
3
 
4
4
  def validate(context)
5
5
  literal_validator = GraphQL::StaticValidation::LiteralValidator.new
6
- context.visitor[GraphQL::Language::Nodes::Variable] << -> (node, parent) {
6
+ context.visitor[GraphQL::Language::Nodes::VariableDefinition] << -> (node, parent) {
7
7
  if !node.default_value.nil?
8
8
  validate_default_value(node, literal_validator, context)
9
9
  end
@@ -2,7 +2,7 @@ class GraphQL::StaticValidation::VariablesAreInputTypes
2
2
  include GraphQL::StaticValidation::Message::MessageHelper
3
3
 
4
4
  def validate(context)
5
- context.visitor[GraphQL::Language::Nodes::Variable] << -> (node, parent) {
5
+ context.visitor[GraphQL::Language::Nodes::VariableDefinition] << -> (node, parent) {
6
6
  validate_is_input_type(node, context)
7
7
  }
8
8
  end
@@ -42,7 +42,7 @@ class GraphQL::StaticValidation::Validator
42
42
  def initialize(schema, document)
43
43
  @schema = schema
44
44
  @document = document
45
- @fragments = document.parts.each_with_object({}) do |part, memo|
45
+ @fragments = document.definitions.each_with_object({}) do |part, memo|
46
46
  part.is_a?(GraphQL::Language::Nodes::FragmentDefinition) && memo[part.name] = part
47
47
  end
48
48
  @errors = []
@@ -1,3 +1,3 @@
1
1
  module GraphQL
2
- VERSION = "0.10.9"
2
+ VERSION = "0.11.0"
3
3
  end
data/readme.md CHANGED
@@ -102,6 +102,7 @@ If you're building a backend for [Relay](http://facebook.github.io/relay/), you'
102
102
  https://medium.com/@gauravtiwari/graphql-and-relay-on-rails-first-relay-powered-react-component-cb3f9ee95eca
103
103
 
104
104
  #### Tutorials
105
+
105
106
  1. https://medium.com/@khor/relay-facebook-on-rails-8b4af2057152
106
107
  2. https://blog.jacobwgillespie.com/from-rest-to-graphql-b4e95e94c26b#.4cjtklrwt
107
108
  3. http://mgiroux.me/2015/getting-started-with-rails-graphql-relay/
@@ -111,12 +112,15 @@ https://medium.com/@gauravtiwari/graphql-and-relay-on-rails-first-relay-powered-
111
112
 
112
113
  - Code clean-up
113
114
  - Raise if you try to configure an attribute which doesn't suit the type (ie, if you try to define `resolve` on an ObjectType, it should somehow raise)
114
- - Clean up file structure in `lib/query` (don't need serial_execution namespace anymore)
115
- - Accept strings for circular type references
115
+ - make `DefinitionHelpers` more friendly for extension
116
116
  - Interface's possible types should be a property of the schema, not the interface
117
- - Statically validate type of variables (see early return in LiteralValidator)
117
+ - Type lookup should be by type name (to support reloaded constants in Rails code)
118
+ - Add a complexity validator (reject queries if they're too big)
119
+ - Add a custom dump for Relay (it expects default value strings to be double-quoted)
120
+ - Make variable validation provide a specific, useful message
121
+ - Add docs for shared behaviors & DRY code
122
+ - After releasing the next version, use it for [graphql-libgraphqlparser](https://github.com/rmosolgo/graphql-libgraphqlparser-ruby)
118
123
  - Big ideas:
119
- - Use [graphql-parser](https://github.com/shopify/graphql-parser) (Ruby bindings for [libgraphqlparser](https://github.com/graphql/libgraphqlparser)) instead of Parslet ([underway-ish](https://github.com/rmosolgo/graphql-libgraphqlparser-ruby))
120
124
  - Revamp the fixture Schema to be more useful (better names, more extensible)
121
125
  - __Subscriptions__
122
126
  - This is a good chance to make an `Operation` abstraction of which `query`, `mutation` and `subscription` are members
@@ -18,6 +18,7 @@ describe GraphQL::ExecutionError do
18
18
  }
19
19
  flavor
20
20
  }
21
+ executionError
21
22
  }
22
23
 
23
24
  fragment similarCheeseFields on Cheese {
@@ -36,7 +37,8 @@ describe GraphQL::ExecutionError do
36
37
  "flavor" => "Manchego",
37
38
  },
38
39
  "flavor" => "Brie",
39
- }
40
+ },
41
+ "executionError" => nil,
40
42
  },
41
43
  "errors"=>[
42
44
  {
@@ -47,6 +49,10 @@ describe GraphQL::ExecutionError do
47
49
  "message"=>"No cheeses are made from Yak milk!",
48
50
  "locations"=>[{"line"=>8, "column"=>9}]
49
51
  },
52
+ {
53
+ "message"=>"There was an execution error",
54
+ "locations"=>[{"line"=>16, "column"=>7}]
55
+ },
50
56
  ]
51
57
  }
52
58
  assert_equal(expected_result, result)
@@ -5,6 +5,59 @@ describe GraphQL::Field do
5
5
  field = GraphQL::Field.define do
6
6
  type(-> { DairyProductUnion })
7
7
  end
8
+
8
9
  assert_equal(DairyProductUnion, field.type)
9
10
  end
11
+
12
+ it "accepts a string as a type" do
13
+ field = GraphQL::Field.define do
14
+ type("DairyProductUnion")
15
+ end
16
+
17
+ assert_equal(DairyProductUnion, field.type)
18
+ end
19
+
20
+
21
+ describe '.property ' do
22
+ let(:field) do
23
+ GraphQL::Field.define do
24
+ # satisfies 'can define by config' below
25
+ property :internal_prop
26
+ end
27
+ end
28
+
29
+ it 'can define by config' do
30
+ assert_equal(field.property, :internal_prop)
31
+ end
32
+
33
+ it 'has nil property if not defined' do
34
+ no_prop_field = GraphQL::Field.define { }
35
+ assert_equal(no_prop_field.property, nil)
36
+ end
37
+
38
+ describe 'default resolver' do
39
+ def acts_like_default_resolver(field, old_prop, new_prop)
40
+ object = OpenStruct.new(old_prop => 'old value', new_prop => 'new value')
41
+
42
+ old_result = field.resolve(object, nil, nil)
43
+ field.property = new_prop
44
+ new_result = field.resolve(object, nil, nil)
45
+ field.property = nil
46
+ unset_result = field.resolve(object, nil, nil)
47
+
48
+ assert_equal(old_result, 'old value')
49
+ assert_equal(new_result, 'new value')
50
+ assert_equal(unset_result, GraphQL::Query::DEFAULT_RESOLVE)
51
+ end
52
+
53
+ it 'responds to changes in property' do
54
+ acts_like_default_resolver(field, :internal_prop, :new_prop)
55
+ end
56
+
57
+ it 'is reassigned if resolve is set to nil' do
58
+ field.resolve = nil
59
+ acts_like_default_resolver(field, :internal_prop, :new_prop)
60
+ end
61
+ end
62
+ end
10
63
  end
@@ -10,12 +10,19 @@ describe GraphQL::InputObjectType do
10
10
  assert(DairyProductInputType.input_fields["fatContent"])
11
11
  end
12
12
 
13
+ describe "input validation" do
14
+ it "Accepts anything that yields key-value pairs to #all?" do
15
+ values_obj = MinimumInputObject.new
16
+ assert DairyProductInputType.valid_non_null_input?(values_obj)
17
+ end
18
+ end
19
+
13
20
  describe "when sent into a query" do
14
21
  let(:variables) { {} }
15
22
  let(:result) { DummySchema.execute(query_string, variables: variables) }
16
23
 
17
24
  describe "list inputs" do
18
- let(:variables) { {"search" => [{"source" => "COW"}]} }
25
+ let(:variables) { {"search" => [MinimumInputObject.new]} }
19
26
  let(:query_string) {%|
20
27
  query getCheeses($search: [DairyProductInput]!){
21
28
  sheep: searchDairy(product: [{source: SHEEP, fatContent: 0.1}]) {
@@ -26,7 +26,8 @@ describe GraphQL::Introspection::SchemaType do
26
26
  {"name"=>"cow"},
27
27
  {"name"=>"searchDairy"},
28
28
  {"name"=>"error"},
29
- {"name"=>"maybeNull"},
29
+ {"name"=>"executionError"},
30
+ {"name"=>"maybeNull"}
30
31
  ]
31
32
  },
32
33
  "mutationType"=> {
@@ -45,11 +45,11 @@ describe GraphQL::Language::Transform do
45
45
  }
46
46
  |
47
47
  res = get_result(query, debug: false)
48
- assert_equal(4, res.parts.length)
48
+ assert_equal(4, res.definitions.length)
49
49
 
50
50
  res = get_result("{ me {id, birthdate} } # query shorthand")
51
- assert_equal(1, res.parts.length)
52
- assert_equal("me", res.parts.first.selections.first.name)
51
+ assert_equal(1, res.definitions.length)
52
+ assert_equal("me", res.definitions.first.selections.first.name)
53
53
  end
54
54
 
55
55
  it 'transforms operation definitions' do
@@ -117,16 +117,16 @@ describe GraphQL::Language::Transform do
117
117
  res_empty = get_result(%q|{}|, parse: :value_input_object)
118
118
  res_empty_space = get_result(%q|{ }|, parse: :value_input_object)
119
119
 
120
- assert_equal('one', res_one_pair.pairs[0].name)
121
- assert_equal(1 , res_one_pair.pairs[0].value)
120
+ assert_equal('one', res_one_pair.arguments[0].name)
121
+ assert_equal(1 , res_one_pair.arguments[0].value)
122
122
 
123
- assert_equal('first' , res_two_pair.pairs[0].name)
124
- assert_equal('Apple' , res_two_pair.pairs[0].value)
125
- assert_equal('second', res_two_pair.pairs[1].name)
126
- assert_equal('Banana', res_two_pair.pairs[1].value)
123
+ assert_equal('first' , res_two_pair.arguments[0].name)
124
+ assert_equal('Apple' , res_two_pair.arguments[0].value)
125
+ assert_equal('second', res_two_pair.arguments[1].name)
126
+ assert_equal('Banana', res_two_pair.arguments[1].value)
127
127
 
128
- assert_equal([], res_empty.pairs)
129
- assert_equal([], res_empty_space.pairs)
128
+ assert_equal([], res_empty.arguments)
129
+ assert_equal([], res_empty_space.arguments)
130
130
  end
131
131
 
132
132
  it 'transforms directives' do
@@ -141,12 +141,12 @@ describe GraphQL::Language::Transform do
141
141
  end
142
142
 
143
143
  it 'transforms unnamed operations' do
144
- assert_equal(1, get_result("query { me }").parts.length)
145
- assert_equal(1, get_result("mutation { touch }").parts.length)
144
+ assert_equal(1, get_result("query { me }").definitions.length)
145
+ assert_equal(1, get_result("mutation { touch }").definitions.length)
146
146
  end
147
147
 
148
148
  it 'transforms escaped characters' do
149
149
  res = get_result("{quoted: \"\\\" \\\\ \\/ \\b \\f \\n \\r \\t\"}", parse: :value_input_object)
150
- assert_equal("\" \\ / \b \f \n \r \t", res.pairs[0].value)
150
+ assert_equal("\" \\ / \b \f \n \r \t", res.arguments[0].value)
151
151
  end
152
152
  end
@@ -24,5 +24,12 @@ describe GraphQL::ObjectType do
24
24
  assert_equal(GraphQL::TypeKinds::NON_NULL, field.type.kind)
25
25
  assert_equal(GraphQL::TypeKinds::SCALAR, field.type.of_type.kind)
26
26
  end
27
+
28
+ it 'exposes defined field property' do
29
+ field_without_prop = CheeseType.fields['flavor']
30
+ field_with_prop = CheeseType.fields['fatContent']
31
+ assert_equal(field_without_prop.property, nil)
32
+ assert_equal(field_with_prop.property, :fat_content)
33
+ end
27
34
  end
28
35
  end
@@ -1,14 +1,14 @@
1
1
  require "spec_helper"
2
2
 
3
3
  describe GraphQL::Query::Arguments do
4
- let(:arguments) { GraphQL::Query::Arguments.new({ a: 1, b: 2 }) }
4
+ let(:arguments) { GraphQL::Query::Arguments.new({ a: 1, b: 2, c: GraphQL::Query::Arguments.new({ d: 3, e: 4}) }) }
5
5
 
6
6
  it 'returns keys as strings' do
7
- assert_equal(['a', 'b'], arguments.keys)
7
+ assert_equal(['a', 'b', 'c'], arguments.keys)
8
8
  end
9
9
 
10
10
  it 'delegates values to values hash' do
11
- assert_equal([1, 2], arguments.values)
11
+ assert_equal([1, 2, {'d' => 3, 'e' => 4}], arguments.values)
12
12
  end
13
13
 
14
14
  it 'delegates each to values hash' do
@@ -16,10 +16,10 @@ describe GraphQL::Query::Arguments do
16
16
  arguments.each do |key, value|
17
17
  pairs << [key, value]
18
18
  end
19
- assert_equal([['a', 1], ['b', 2]], pairs)
19
+ assert_equal([['a', 1], ['b', 2], ['c', {'d' => 3, 'e' => 4}]], pairs)
20
20
  end
21
21
 
22
22
  it 'returns original Ruby hash values with to_h' do
23
- assert_equal({ a: 1, b: 2 }, arguments.to_h)
23
+ assert_equal({ a: 1, b: 2, c: { d: 3, e: 4 } }, arguments.to_h)
24
24
  end
25
25
  end