graphql 0.9.2 → 0.9.3

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.
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