graphql 0.9.2 → 0.9.3

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 443760bcd1ca8e0c763ba9216cfc511c8c3d1185
4
- data.tar.gz: df070125ce170c79d75bb7f5abe4c4303aaf6332
3
+ metadata.gz: 90c4327894852ac5fa30563c6f75585be564033d
4
+ data.tar.gz: b5ea5c2c7710196f76bb2773f55cda6380075336
5
5
  SHA512:
6
- metadata.gz: 9bbe4e2c99893d0bbb4d68ede2bff4acb9e3f8f11b9ff339a4e159db0baf4ccea21f3168ca69eb3cfb1737528f7dcf1a389df2f219b397c1a321416c061ab2af
7
- data.tar.gz: ed66af0387e274413d89bed95e9b1f4e36c5d90b542505759046f53d8b6a310894a09f0ca4197872eab1b50bda74b01ad1a745a80f149fba042a8628d02ebfad
6
+ metadata.gz: f939c511550ce296668dd3154863cb5401ef7d8710b5dd6a165387555ebce6f769ea587d1a798b9ce39b8b69f39ca1120adff150579e892c84fed22d0f26e7be
7
+ data.tar.gz: b12940f1e651fe6de8fa33ac0ee1a68ed8041e4120a5c48b52a53d4e239f5470d547b616639876d8c6f01b194ea7ab59b0796a2dd5d3c1d98bd06ef79b4b77d9
@@ -3,6 +3,16 @@ require "parslet"
3
3
  require "singleton"
4
4
 
5
5
  module GraphQL
6
+ class ParseError < StandardError
7
+ attr_reader :line, :col, :query
8
+ def initialize(message, line, col, query)
9
+ super(message)
10
+ @line = line
11
+ @col = col
12
+ @query = query
13
+ end
14
+ end
15
+
6
16
  # Turn a query string into an AST
7
17
  # @param string [String] a GraphQL query string
8
18
  # @param as [Symbol] If you want to use this to parse some _piece_ of a document, pass the rule name (from {GraphQL::Parser})
@@ -12,8 +22,8 @@ module GraphQL
12
22
  tree = parser.parse(string)
13
23
  GraphQL::TRANSFORM.apply(tree)
14
24
  rescue Parslet::ParseFailed => error
15
- line, col = error.cause.source.line_and_column
16
- raise [error.message, line, col, string].join(", ")
25
+ line, col = error.cause.source.line_and_column(error.cause.pos)
26
+ raise GraphQL::ParseError.new(error.message, line, col, string)
17
27
  end
18
28
  end
19
29
 
@@ -31,6 +31,7 @@ class GraphQL::Field
31
31
  DEFAULT_RESOLVE = -> (o, a, c) { GraphQL::Query::DEFAULT_RESOLVE }
32
32
  include GraphQL::DefinitionHelpers::DefinedByConfig
33
33
  attr_accessor :arguments, :deprecation_reason, :name, :description, :type
34
+ attr_reader :resolve_proc
34
35
  defined_by_config :arguments, :deprecation_reason, :name, :description, :type, :resolve
35
36
 
36
37
  def initialize
@@ -13,7 +13,7 @@ class GraphQL::Query
13
13
  # @param debug [Boolean] if true, errors are raised, if false, errors are put in the `errors` key
14
14
  # @param validate [Boolean] if true, `query_string` will be validated with {StaticValidation::Validator}
15
15
  # @param operation_name [String] if the query string contains many operations, this is the one which should be executed
16
- def initialize(schema, query_string, context: nil, variables: {}, debug: true, validate: true, operation_name: nil)
16
+ def initialize(schema, query_string, context: nil, variables: {}, debug: false, validate: true, operation_name: nil)
17
17
  @schema = schema
18
18
  @debug = debug
19
19
  @context = Context.new(values: context)
@@ -21,14 +21,6 @@ module GraphQL
21
21
  get_class :FieldResolution
22
22
  end
23
23
 
24
- def fragment_spread_resolution
25
- get_class :FragmentSpreadResolution
26
- end
27
-
28
- def inline_fragment_resolution
29
- get_class :InlineFragmentResolution
30
- end
31
-
32
24
  def operation_resolution
33
25
  get_class :OperationResolution
34
26
  end
@@ -6,7 +6,5 @@ module GraphQL
6
6
  end
7
7
 
8
8
  require 'graphql/query/serial_execution/field_resolution'
9
- require 'graphql/query/serial_execution/fragment_spread_resolution'
10
- require 'graphql/query/serial_execution/inline_fragment_resolution'
11
9
  require 'graphql/query/serial_execution/operation_resolution'
12
10
  require 'graphql/query/serial_execution/selection_resolution'
@@ -4,12 +4,6 @@ module GraphQL
4
4
  class SelectionResolution
5
5
  attr_reader :target, :type, :selections, :query, :execution_strategy
6
6
 
7
- RESOLUTION_STRATEGIES = {
8
- GraphQL::Language::Nodes::Field => :field_resolution,
9
- GraphQL::Language::Nodes::FragmentSpread => :fragment_spread_resolution,
10
- GraphQL::Language::Nodes::InlineFragment => :inline_fragment_resolution,
11
- }
12
-
13
7
  def initialize(target, type, selections, query, execution_strategy)
14
8
  @target = target
15
9
  @type = type
@@ -19,34 +13,83 @@ module GraphQL
19
13
  end
20
14
 
21
15
  def result
22
- selections.reduce({}) do |memo, ast_field|
23
- field_value = resolve_field(ast_field)
24
- deep_merge memo, field_value
25
- end
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
+ flatten_selection(ast_node).each do |name, selection|
23
+ if memo.has_key? name
24
+ memo[name] = merge_fields(memo[name], selection)
25
+ else
26
+ memo[name] = selection
27
+ end
28
+ end
29
+
30
+ memo
31
+ }
32
+ .values
33
+ .reduce({}){|memo, ast_node|
34
+ memo.merge(resolve_field(ast_node))
35
+ }
26
36
  end
27
37
 
28
38
  private
29
39
 
30
- def deep_merge(h1, h2)
31
- h1.merge(h2) do |key, oldval, newval|
32
- if oldval.is_a?(Array) && newval.is_a?(Array)
33
- oldval.each_index.map do |i|
34
- deep_merge oldval[i], newval[i]
35
- end
36
- elsif oldval.is_a?(Hash) && newval.is_a?(Hash)
37
- deep_merge(oldval, newval)
38
- else
39
- newval
40
+ def flatten_selection(ast_node)
41
+ return {(ast_node.alias || ast_node.name) => ast_node} if ast_node.is_a?(GraphQL::Language::Nodes::Field)
42
+
43
+ ast_fragment = get_fragment(ast_node)
44
+ return {} unless fragment_type_can_apply?(ast_fragment)
45
+
46
+ chain = GraphQL::Query::DirectiveChain.new(ast_node, query) {
47
+ ast_fragment.selections.reduce({}) do |memo, selection|
48
+ memo.merge(flatten_selection(selection))
40
49
  end
50
+ }
51
+
52
+ chain.result || {}
53
+ end
54
+
55
+ def get_fragment(ast_node)
56
+ if ast_node.is_a? GraphQL::Language::Nodes::FragmentSpread
57
+ query.fragments[ast_node.name]
58
+ elsif ast_node.is_a? GraphQL::Language::Nodes::InlineFragment
59
+ ast_node
60
+ else
61
+ raise 'Unrecognized fragment node'
62
+ end
63
+ end
64
+
65
+ def fragment_type_can_apply?(ast_fragment)
66
+ child_type = query.schema.types[ast_fragment.type]
67
+ resolved_type = GraphQL::Query::TypeResolver.new(target, child_type, type).type
68
+ !resolved_type.nil?
69
+ end
70
+
71
+ def merge_fields(field1, field2)
72
+ field_type = query.schema.get_field(type, field2.name).type.unwrap
73
+
74
+ if field_type.is_a?(GraphQL::ObjectType)
75
+ # create a new ast field node merging selections from each field.
76
+ # Because of static validation, we can assume that name, alias,
77
+ # arguments, and directives are exactly the same for fields 1 and 2.
78
+ GraphQL::Language::Nodes::Field.new(
79
+ name: field2.name,
80
+ alias: field2.alias,
81
+ arguments: field2.arguments,
82
+ directives: field2.directives,
83
+ selections: field1.selections + field2.selections
84
+ )
85
+ else
86
+ field2
41
87
  end
42
88
  end
43
89
 
44
- def resolve_field(ast_field)
45
- chain = GraphQL::Query::DirectiveChain.new(ast_field, query) {
46
- strategy_name = RESOLUTION_STRATEGIES[ast_field.class]
47
- strategy_class = execution_strategy.public_send(strategy_name)
48
- strategy = strategy_class.new(ast_field, type, target, query, execution_strategy)
49
- strategy.result
90
+ def resolve_field(ast_node)
91
+ chain = GraphQL::Query::DirectiveChain.new(ast_node, query) {
92
+ execution_strategy.field_resolution.new(ast_node, type, target, query, execution_strategy).result
50
93
  }
51
94
  chain.result
52
95
  end
@@ -21,11 +21,19 @@ class GraphQL::Schema
21
21
  end
22
22
 
23
23
  # A `{ name => type }` hash of types in this schema
24
- # @returns Hash
24
+ # @return [Hash]
25
25
  def types
26
26
  @types ||= TypeReducer.find_all([query, mutation, GraphQL::Introspection::SchemaType].compact)
27
27
  end
28
28
 
29
+ # Execute a query on itself.
30
+ # See {Query#initialize} for arguments.
31
+ # @return [Hash] query result, ready to be serialized as JSON
32
+ def execute(*args)
33
+ query = GraphQL::Query.new(self, *args)
34
+ query.result
35
+ end
36
+
29
37
  # Resolve field named `field_name` for type `parent_type`.
30
38
  # Handles dynamic fields `__typename`, `__type` and `__schema`, too
31
39
  def get_field(parent_type, field_name)
@@ -48,6 +48,12 @@ class GraphQL::Schema::TypeReducer
48
48
  reduce_type(possible_type, type_hash)
49
49
  end
50
50
  end
51
+ if type.kind.input_object?
52
+ type.input_fields.each do |name, input_field|
53
+ reduce_type(input_field.type, type_hash)
54
+ end
55
+ end
56
+
51
57
  type_hash
52
58
  end
53
59
 
@@ -17,7 +17,9 @@ class GraphQL::StaticValidation::VariableUsagesAreAllowed
17
17
  arguments = context.directive_definition.arguments
18
18
  end
19
19
  var_defn_ast = declared_variables[node.value.name]
20
- validate_usage(arguments, node, var_defn_ast, context)
20
+ # Might be undefined :(
21
+ # VariablesAreUsedAndDefined can't finalize its search until the end of the document.
22
+ var_defn_ast && validate_usage(arguments, node, var_defn_ast, context)
21
23
  }
22
24
  end
23
25
 
@@ -1,31 +1,130 @@
1
+ # The problem is
2
+ # - Variable usage must be determined at the OperationDefinition level
3
+ # - You can't tell how fragments use variables until you visit FragmentDefinitions (which may be at the end of the document)
4
+ #
5
+ # So, this validator includes some crazy logic to follow fragment spreads recursively, while avoiding infinite loops.
6
+ #
7
+ # `graphql-js` solves this problem by:
8
+ # - re-visiting the AST for each validator
9
+ # - allowing validators to say `followSpreads: true`
10
+ #
1
11
  class GraphQL::StaticValidation::VariablesAreUsedAndDefined
2
12
  include GraphQL::StaticValidation::Message::MessageHelper
3
13
 
14
+ class VariableUsage
15
+ attr_accessor :ast_node, :used_by, :declared_by
16
+ def used?
17
+ !!@used_by
18
+ end
19
+
20
+ def declared?
21
+ !!@declared_by
22
+ end
23
+ end
24
+
25
+ def variable_hash
26
+ Hash.new {|h, k| h[k] = VariableUsage.new }
27
+ end
28
+
4
29
  def validate(context)
5
- # holds { name => used? } pairs
6
- declared_variables = {}
30
+ variable_usages_for_context = Hash.new {|hash, key| hash[key] = variable_hash }
31
+ spreads_for_context = Hash.new {|hash, key| hash[key] = [] }
32
+ variable_context_stack = []
33
+
34
+ # OperationDefinitions and FragmentDefinitions
35
+ # both push themselves onto the context stack (and pop themselves off)
36
+ push_variable_context_stack = -> (node, parent) {
37
+ # initialize the hash of vars for this context:
38
+ variable_usages_for_context[node]
39
+ variable_context_stack.push(node)
40
+ }
41
+
42
+ pop_variable_context_stack = -> (node, parent) {
43
+ variable_context_stack.pop
44
+ }
7
45
 
46
+
47
+ context.visitor[GraphQL::Language::Nodes::OperationDefinition] << push_variable_context_stack
8
48
  context.visitor[GraphQL::Language::Nodes::OperationDefinition] << -> (node, parent) {
9
- declared_variables = node.variables.each_with_object({}) { |var, memo| memo[var.name] = false }
49
+ # mark variables as defined:
50
+ var_hash = variable_usages_for_context[node]
51
+ node.variables.each { |var| var_hash[var.name].declared_by = node }
52
+ }
53
+ context.visitor[GraphQL::Language::Nodes::OperationDefinition].leave << pop_variable_context_stack
54
+
55
+ context.visitor[GraphQL::Language::Nodes::FragmentDefinition] << push_variable_context_stack
56
+ context.visitor[GraphQL::Language::Nodes::FragmentDefinition].leave << pop_variable_context_stack
57
+
58
+ # For FragmentSpreads:
59
+ # - find the context on the stack
60
+ # - mark the context as containing this spread
61
+ context.visitor[GraphQL::Language::Nodes::FragmentSpread] << -> (node, parent) {
62
+ variable_context = variable_context_stack.last
63
+ spreads_for_context[variable_context] << node.name
10
64
  }
11
65
 
66
+ # For VariableIdentifiers:
67
+ # - mark the variable as used
68
+ # - assign its AST node
12
69
  context.visitor[GraphQL::Language::Nodes::VariableIdentifier] << -> (node, parent) {
13
- if declared_variables.key?(node.name)
14
- declared_variables[node.name] = true
15
- else
16
- context.errors << message("Variable $#{node.name} is used but not declared", node)
17
- GraphQL::Language::Visitor::SKIP
18
- end
70
+ usage_context = variable_context_stack.last
71
+ declared_variables = variable_usages_for_context[usage_context]
72
+ usage = declared_variables[node.name]
73
+ usage.used_by = usage_context
74
+ usage.ast_node = node
19
75
  }
20
76
 
21
- context.visitor[GraphQL::Language::Nodes::OperationDefinition].leave << -> (node, parent) {
22
- unused_variables = declared_variables
23
- .select { |name, used| !used }
24
- .keys
25
77
 
26
- unused_variables.each do |var_name|
27
- context.errors << message("Variable $#{var_name} is declared but not used", node)
78
+ context.visitor[GraphQL::Language::Nodes::Document].leave << -> (node, parent) {
79
+ fragment_definitions = variable_usages_for_context.select { |key, value| key.is_a?(GraphQL::Language::Nodes::FragmentDefinition) }
80
+ operation_definitions = variable_usages_for_context.select { |key, value| key.is_a?(GraphQL::Language::Nodes::OperationDefinition) }
81
+
82
+ operation_definitions.each do |node, node_variables|
83
+ follow_spreads(node, node_variables, spreads_for_context, fragment_definitions, [])
84
+ create_errors(node_variables, context)
28
85
  end
29
86
  }
30
87
  end
88
+
89
+ private
90
+
91
+ # Follow spreads in `node`, looking them up from `spreads_for_context` and finding their match in `fragment_definitions`.
92
+ # Use those fragments to update {VariableUsage}s in `parent_variables`.
93
+ # Avoid infinite loops by skipping anything in `visited_fragments`.
94
+ def follow_spreads(node, parent_variables, spreads_for_context, fragment_definitions, visited_fragments)
95
+ spreads = spreads_for_context[node] - visited_fragments
96
+ spreads.each do |spread_name|
97
+ def_node, variables = fragment_definitions.find { |def_node, vars| def_node.name == spread_name }
98
+ next if !def_node
99
+ visited_fragments << spread_name
100
+ variables.each do |name, child_usage|
101
+ parent_usage = parent_variables[name]
102
+ if child_usage.used?
103
+ parent_usage.ast_node = child_usage.ast_node
104
+ parent_usage.used_by = child_usage.used_by
105
+ end
106
+ end
107
+ follow_spreads(def_node, parent_variables, spreads_for_context, fragment_definitions, visited_fragments)
108
+ end
109
+ end
110
+
111
+ # Determine all the error messages,
112
+ # Then push messages into the validation context
113
+ def create_errors(node_variables, context)
114
+ errors = []
115
+ # Declared but not used:
116
+ errors += node_variables
117
+ .select { |name, usage| usage.declared? && !usage.used? }
118
+ .map { |var_name, usage| ["Variable $#{var_name} is declared by #{usage.declared_by.name} but not used", usage.declared_by] }
119
+
120
+ # Used but not declared:
121
+ errors += node_variables
122
+ .select { |name, usage| usage.used? && !usage.declared? }
123
+ .map { |var_name, usage| ["Variable $#{var_name} is used by #{usage.used_by.name} but not declared", usage.ast_node] }
124
+
125
+ errors.each do |error_args|
126
+ context.errors << message(*error_args)
127
+ end
128
+ end
129
+
31
130
  end
@@ -1,3 +1,3 @@
1
1
  module GraphQL
2
- VERSION = "0.9.2"
2
+ VERSION = "0.9.3"
3
3
  end
data/readme.md CHANGED
@@ -63,8 +63,7 @@ See also:
63
63
  Execute GraphQL queries on a given schema, from a query string.
64
64
 
65
65
  ```ruby
66
- query = GraphQL::Query.new(Schema, query_string)
67
- result_hash = query.result
66
+ result_hash = Schema.execute(query_string)
68
67
  # {
69
68
  # "data" => {
70
69
  # "post" => {
@@ -80,7 +79,6 @@ See also:
80
79
  - [`queries_controller.rb`](https://github.com/rmosolgo/graphql-ruby-demo/blob/master/app/controllers/queries_controller.rb) for a Rails example
81
80
  - Try it on [heroku](http://graphql-ruby-demo.herokuapp.com)
82
81
 
83
-
84
82
  #### Use with Relay
85
83
 
86
84
  If you're building a backend for [Relay](http://facebook.github.io/relay/), you'll need:
@@ -91,9 +89,6 @@ If you're building a backend for [Relay](http://facebook.github.io/relay/), you'
91
89
 
92
90
  ## To Do
93
91
 
94
- - Field merging
95
- - if you were to request a field, then request it in a fragment, it would get looked up twice
96
- - https://github.com/graphql/graphql-js/issues/19#issuecomment-118515077
97
92
  - Code clean-up
98
93
  - Raise if you try to configure an attribute which doesn't suit the type
99
94
  - ie, if you try to define `resolve` on an ObjectType, it should somehow raise
@@ -101,6 +96,7 @@ If you're building a backend for [Relay](http://facebook.github.io/relay/), you'
101
96
  - Write Ruby bindings for [libgraphqlparser](https://github.com/graphql/libgraphqlparser) and use that instead of Parslet
102
97
  - Add instrumentation
103
98
  - Some way to expose what queries are run, what types & fields are accessed, how long things are taking, etc
99
+ - before-hooks for every field?
104
100
 
105
101
 
106
102
  ## Goals
@@ -1,7 +1,7 @@
1
1
  require 'spec_helper'
2
2
 
3
3
  describe GraphQL::Directive do
4
- let(:result) { GraphQL::Query.new(DummySchema, query_string, variables: {"t" => true, "f" => false}).result }
4
+ let(:result) { DummySchema.execute(query_string, variables: {"t" => true, "f" => false}) }
5
5
  describe 'on fields' do
6
6
  let(:query_string) { %|query directives($t: Boolean!, $f: Boolean!) {
7
7
  cheese(id: 1) {
@@ -1,8 +1,7 @@
1
1
  require 'spec_helper'
2
2
 
3
3
  describe GraphQL::ID_TYPE do
4
- let(:query) { GraphQL::Query.new(DummySchema, query_string)}
5
- let(:result) { query.result }
4
+ let(:result) { DummySchema.execute(query_string)}
6
5
 
7
6
  describe 'coercion for int inputs' do
8
7
  let(:query_string) { %|query getMilk { cow: milk(id: 1) { id } }| }
@@ -12,8 +12,7 @@ describe GraphQL::InterfaceType do
12
12
  end
13
13
 
14
14
  describe 'query evaluation' do
15
- let(:query) { GraphQL::Query.new(DummySchema, query_string, context: {}, variables: {"cheeseId" => 2})}
16
- let(:result) { query.result }
15
+ let(:result) { DummySchema.execute(query_string, context: {}, variables: {"cheeseId" => 2})}
17
16
  let(:query_string) {%|
18
17
  query fav {
19
18
  favoriteEdible { fatContent }
@@ -8,7 +8,7 @@ describe GraphQL::Introspection::DirectiveType do
8
8
  }
9
9
  }
10
10
  |}
11
- let(:result) { GraphQL::Query.new(DummySchema, query_string).result }
11
+ let(:result) { DummySchema.execute(query_string) }
12
12
 
13
13
  it 'shows directive info ' do
14
14
  expected = { "data" => {
@@ -2,7 +2,7 @@ require 'spec_helper'
2
2
 
3
3
  describe "GraphQL::Introspection::INTROSPECTION_QUERY" do
4
4
  let(:query_string) { GraphQL::Introspection::INTROSPECTION_QUERY }
5
- let(:result) { GraphQL::Query.new(DummySchema, query_string).result }
5
+ let(:result) { DummySchema.execute(query_string) }
6
6
 
7
7
  it 'runs' do
8
8
  assert(result["data"])
@@ -10,7 +10,8 @@ describe GraphQL::Introspection::SchemaType do
10
10
  }
11
11
  }
12
12
  |}
13
- let(:result) { GraphQL::Query.new(DummySchema, query_string).result }
13
+ let(:result) { DummySchema.execute(query_string) }
14
+
14
15
  it 'exposes the schema' do
15
16
  expected = { "data" => {
16
17
  "__schema" => {
@@ -10,7 +10,7 @@ describe GraphQL::Introspection::TypeType do
10
10
  animalProduct: __type(name: "AnimalProduct") { name, kind, possibleTypes { name }, fields { name } }
11
11
  }
12
12
  |}
13
- let(:query) { GraphQL::Query.new(DummySchema, query_string, context: {}, variables: {"cheeseId" => 2})}
13
+ let(:result) { DummySchema.execute(query_string, context: {}, variables: {"cheeseId" => 2}) }
14
14
  let(:cheese_fields) {[
15
15
  {"name"=>"id", "isDeprecated" => false, "type" => { "name" => "Non-Null", "ofType" => { "name" => "Int"}}},
16
16
  {"name"=>"flavor", "isDeprecated" => false, "type" => { "name" => "Non-Null", "ofType" => { "name" => "String"}}},
@@ -61,7 +61,7 @@ describe GraphQL::Introspection::TypeType do
61
61
  ]
62
62
  }
63
63
  }}
64
- assert_equal(expected, query.result)
64
+ assert_equal(expected, result)
65
65
  end
66
66
 
67
67
  describe 'deprecated fields' do
@@ -86,7 +86,7 @@ describe GraphQL::Introspection::TypeType do
86
86
  "enumValues"=> dairy_animals + [{"name" => "YAK", "isDeprecated" => true}],
87
87
  },
88
88
  }}
89
- assert_equal(expected, query.result)
89
+ assert_equal(expected, result)
90
90
  end
91
91
 
92
92
  describe 'input objects' do
@@ -108,7 +108,7 @@ describe GraphQL::Introspection::TypeType do
108
108
  ]
109
109
  }
110
110
  }}
111
- assert_equal(expected, query.result)
111
+ assert_equal(expected, result)
112
112
  end
113
113
  end
114
114
  end
@@ -3,14 +3,14 @@ require 'spec_helper'
3
3
  describe GraphQL::Query::Executor do
4
4
  let(:debug) { false }
5
5
  let(:operation_name) { nil }
6
- let(:query) { GraphQL::Query.new(
7
- DummySchema,
6
+ let(:schema) { DummySchema }
7
+ let(:variables) { {"cheeseId" => 2} }
8
+ let(:result) { schema.execute(
8
9
  query_string,
9
- variables: {"cheeseId" => 2},
10
+ variables: variables,
10
11
  debug: debug,
11
12
  operation_name: operation_name,
12
13
  )}
13
- let(:result) { query.result }
14
14
 
15
15
  describe "multiple operations" do
16
16
  let(:query_string) { %|
@@ -68,6 +68,51 @@ describe GraphQL::Query::Executor do
68
68
  end
69
69
 
70
70
 
71
+ describe 'fragment resolution' do
72
+ let(:schema) {
73
+ # we will raise if the dairy field is resolved more than one time
74
+ resolved = false
75
+
76
+ DummyQueryType = GraphQL::ObjectType.define do
77
+ name "Query"
78
+ field :dairy do
79
+ type DairyType
80
+ resolve -> (t, a, c) {
81
+ raise if resolved
82
+ resolved = true
83
+ DAIRY
84
+ }
85
+ end
86
+ end
87
+
88
+ GraphQL::Schema.new(query: DummyQueryType, mutation: MutationType)
89
+ }
90
+ let(:variables) { nil }
91
+ let(:query_string) { %|
92
+ query getDairy {
93
+ dairy {
94
+ id
95
+ ... on Dairy {
96
+ id
97
+ }
98
+ ...repetitiveFragment
99
+ }
100
+ }
101
+ fragment repetitiveFragment on Dairy {
102
+ id
103
+ }
104
+ |}
105
+
106
+ it 'resolves each field only one time, even when present in multiple fragments' do
107
+ expected = {"data" => {
108
+ "dairy" => { "id" => "1" }
109
+ }}
110
+ assert_equal(expected, result)
111
+ end
112
+
113
+ end
114
+
115
+
71
116
  describe 'runtime errors' do
72
117
  let(:query_string) {%| query noMilk { error }|}
73
118
  describe 'if debug: false' do
@@ -82,6 +82,22 @@ describe GraphQL::Query do
82
82
  assert_equal(GraphQL::Language::Nodes::FragmentDefinition, query.fragments['cheeseFields'].class)
83
83
  end
84
84
 
85
+ it 'correctly identifies parse error location' do
86
+ # "Correct" is a bit of an overstatement. All Parslet errors get surfaced
87
+ # at the beginning of the query they were in, since Parslet sees the query
88
+ # as invalid. It would be great to have more granularity here.
89
+ e = assert_raises(GraphQL::ParseError) do
90
+ GraphQL.parse("
91
+ query getCoupons {
92
+ allCoupons: {data{id}}
93
+ }
94
+ ")
95
+ end
96
+ assert_equal('Extra input after last repetition at line 2 char 9.', e.message)
97
+ assert_equal(2, e.line)
98
+ assert_equal(9, e.col)
99
+ end
100
+
85
101
  describe "merging fragments with different keys" do
86
102
  let(:query_string) { %|
87
103
  query getCheeseFieldsThroughDairy {
@@ -98,7 +114,6 @@ describe GraphQL::Query do
98
114
  id
99
115
  }
100
116
  }
101
-
102
117
  fragment fatContentFragment on Dairy {
103
118
  cheese {
104
119
  fatContent
@@ -107,7 +122,6 @@ describe GraphQL::Query do
107
122
  fatContent
108
123
  }
109
124
  }
110
-
111
125
  |}
112
126
 
113
127
  it "should include keys from each fragment" do
@@ -23,6 +23,26 @@ describe GraphQL::Schema::TypeReducer do
23
23
  assert_equal(DairyProductInputType, reducer.result["DairyProductInput"])
24
24
  end
25
25
 
26
+ it 'finds types from nested InputObjectTypes' do
27
+ type_child = GraphQL::InputObjectType.define do
28
+ name "InputTypeChild"
29
+ input_field :someField, GraphQL::STRING_TYPE
30
+ end
31
+
32
+ type_parent = GraphQL::InputObjectType.define do
33
+ name "InputTypeParent"
34
+ input_field :child, type_child
35
+ end
36
+
37
+ reducer = GraphQL::Schema::TypeReducer.new(type_parent, {})
38
+ expected = {
39
+ "InputTypeParent" => type_parent,
40
+ "InputTypeChild" => type_child,
41
+ "String" => GraphQL::STRING_TYPE
42
+ }
43
+ assert_equal(expected, reducer.result)
44
+ end
45
+
26
46
  describe 'when a type is invalid' do
27
47
  let(:invalid_type) {
28
48
  GraphQL::ObjectType.define do
@@ -2,13 +2,20 @@ require 'spec_helper'
2
2
 
3
3
  describe GraphQL::StaticValidation::VariablesAreUsedAndDefined do
4
4
  let(:document) { GraphQL.parse('
5
- query getCheese($id: Int, $str: String, $notUsedVar: Float, $bool: Boolean) {
6
- cheese(id: $id) {
7
- source(str: $str)
5
+ query getCheese(
6
+ $usedVar: Int,
7
+ $usedInnerVar: String,
8
+ $usedInlineFragmentVar: Boolean,
9
+ $usedFragmentVar: Int,
10
+ $notUsedVar: Float,
11
+ ) {
12
+ cheese(id: $usedVar) {
13
+ source(str: $usedInnerVar)
8
14
  whatever(undefined: $undefinedVar)
9
15
  ... on Cheese {
10
- something(bool: $bool)
16
+ something(bool: $usedInlineFragmentVar)
11
17
  }
18
+ ... outerCheeseFields
12
19
  }
13
20
  }
14
21
 
@@ -17,7 +24,8 @@ describe GraphQL::StaticValidation::VariablesAreUsedAndDefined do
17
24
  }
18
25
 
19
26
  fragment innerCheeseFields on Cheese {
20
- source(notDefined: $notDefinedVar)
27
+ source(notDefined: $undefinedFragmentVar)
28
+ someField(someArg: $usedFragmentVar)
21
29
  }
22
30
  ')}
23
31
 
@@ -27,16 +35,16 @@ describe GraphQL::StaticValidation::VariablesAreUsedAndDefined do
27
35
  it "finds variables which are used-but-not-defined or defined-but-not-used" do
28
36
  expected = [
29
37
  {
30
- "message"=>"Variable $undefinedVar is used but not declared",
31
- "locations"=>[{"line"=>5, "column"=>30}]
38
+ "message"=>"Variable $notUsedVar is declared by getCheese but not used",
39
+ "locations"=>[{"line"=>2, "column"=>5}]
32
40
  },
33
41
  {
34
- "message"=>"Variable $notUsedVar is declared but not used",
35
- "locations"=>[{"line"=>2, "column"=>5}]
42
+ "message"=>"Variable $undefinedVar is used by getCheese but not declared",
43
+ "locations"=>[{"line"=>11, "column"=>30}]
36
44
  },
37
45
  {
38
- "message"=>"Variable $notDefinedVar is used but not declared",
39
- "locations"=>[{"line"=>17, "column"=>27}]
46
+ "message"=>"Variable $undefinedFragmentVar is used by innerCheeseFields but not declared",
47
+ "locations"=>[{"line"=>24, "column"=>27}]
40
48
  },
41
49
  ]
42
50
  assert_equal(expected, errors)
@@ -32,18 +32,22 @@ describe GraphQL::StaticValidation::Validator do
32
32
  describe 'fields & arguments' do
33
33
  let(:query_string) { %|
34
34
  query getCheese($id: Int!) {
35
- cheese(id: $id, bogusArg: true) {
35
+ cheese(id: $undefinedVar, bogusArg: true) {
36
36
  source,
37
37
  nonsenseField,
38
38
  id(nonsenseArg: 1)
39
39
  bogusField(bogusArg: true)
40
40
  }
41
+
42
+ otherCheese: cheese(id: $id) {
43
+ source,
44
+ }
41
45
  }
42
46
  |}
43
47
 
44
48
  it 'handles args on invalid fields' do
45
- # nonsenseField, nonsenseArg, bogusField, bogusArg
46
- assert_equal(4, errors.length)
49
+ # nonsenseField, nonsenseArg, bogusField, bogusArg, undefinedVar
50
+ assert_equal(5, errors.length)
47
51
  end
48
52
  end
49
53
 
@@ -10,4 +10,8 @@ MILKS = {
10
10
  1 => Milk.new(1, 0.04, 1),
11
11
  }
12
12
 
13
- DAIRY = OpenStruct.new(cheese: CHEESES[1], milks: [MILKS[1]])
13
+ DAIRY = OpenStruct.new(
14
+ id: 1,
15
+ cheese: CHEESES[1],
16
+ milks: [MILKS[1]]
17
+ )
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: graphql
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.9.2
4
+ version: 0.9.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Robert Mosolgo
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2015-09-10 00:00:00.000000000 Z
11
+ date: 2015-09-15 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: parslet
@@ -215,8 +215,6 @@ files:
215
215
  - lib/graphql/query/executor.rb
216
216
  - lib/graphql/query/serial_execution.rb
217
217
  - lib/graphql/query/serial_execution/field_resolution.rb
218
- - lib/graphql/query/serial_execution/fragment_spread_resolution.rb
219
- - lib/graphql/query/serial_execution/inline_fragment_resolution.rb
220
218
  - lib/graphql/query/serial_execution/operation_resolution.rb
221
219
  - lib/graphql/query/serial_execution/selection_resolution.rb
222
220
  - lib/graphql/query/type_resolver.rb
@@ -1,22 +0,0 @@
1
- module GraphQL
2
- class Query
3
- class SerialExecution
4
- class FragmentSpreadResolution < GraphQL::Query::BaseExecution::SelectedObjectResolution
5
- attr_reader :ast_fragment, :resolved_type
6
- def initialize(ast_node, type, target, query, execution_strategy)
7
- super
8
- @ast_fragment = query.fragments[ast_node.name]
9
- child_type = query.schema.types[ast_fragment.type]
10
- @resolved_type = GraphQL::Query::TypeResolver.new(target, child_type, type).type
11
- end
12
-
13
- def result
14
- return {} if resolved_type.nil?
15
- selections = ast_fragment.selections
16
- resolver = execution_strategy.selection_resolution.new(target, resolved_type, selections, query, execution_strategy)
17
- resolver.result
18
- end
19
- end
20
- end
21
- end
22
- end
@@ -1,21 +0,0 @@
1
- module GraphQL
2
- class Query
3
- class SerialExecution
4
- class InlineFragmentResolution < GraphQL::Query::BaseExecution::SelectedObjectResolution
5
- attr_reader :resolved_type
6
- def initialize(ast_node, type, target, query, execution_strategy)
7
- super
8
- child_type = query.schema.types[ast_node.type]
9
- @resolved_type = GraphQL::Query::TypeResolver.new(target, child_type, type).type
10
- end
11
-
12
- def result
13
- return {} if resolved_type.nil?
14
- selections = ast_node.selections
15
- resolver = execution_strategy.selection_resolution.new(target, resolved_type, selections, query, execution_strategy)
16
- resolver.result
17
- end
18
- end
19
- end
20
- end
21
- end