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 +4 -4
- data/lib/graphql.rb +12 -2
- data/lib/graphql/field.rb +1 -0
- data/lib/graphql/query.rb +1 -1
- data/lib/graphql/query/base_execution.rb +0 -8
- data/lib/graphql/query/serial_execution.rb +0 -2
- data/lib/graphql/query/serial_execution/selection_resolution.rb +69 -26
- data/lib/graphql/schema.rb +9 -1
- data/lib/graphql/schema/type_reducer.rb +6 -0
- data/lib/graphql/static_validation/rules/variable_usages_are_allowed.rb +3 -1
- data/lib/graphql/static_validation/rules/variables_are_used_and_defined.rb +114 -15
- data/lib/graphql/version.rb +1 -1
- data/readme.md +2 -6
- data/spec/graphql/directive_spec.rb +1 -1
- data/spec/graphql/id_type_spec.rb +1 -2
- data/spec/graphql/interface_type_spec.rb +1 -2
- data/spec/graphql/introspection/directive_type_spec.rb +1 -1
- data/spec/graphql/introspection/introspection_query_spec.rb +1 -1
- data/spec/graphql/introspection/schema_type_spec.rb +2 -1
- data/spec/graphql/introspection/type_type_spec.rb +4 -4
- data/spec/graphql/query/executor_spec.rb +49 -4
- data/spec/graphql/query_spec.rb +16 -2
- data/spec/graphql/schema/type_reducer_spec.rb +20 -0
- data/spec/graphql/static_validation/rules/variables_are_used_and_defined_spec.rb +19 -11
- data/spec/graphql/static_validation/validator_spec.rb +7 -3
- data/spec/support/dairy_data.rb +5 -1
- metadata +2 -4
- data/lib/graphql/query/serial_execution/fragment_spread_resolution.rb +0 -22
- data/lib/graphql/query/serial_execution/inline_fragment_resolution.rb +0 -21
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 90c4327894852ac5fa30563c6f75585be564033d
|
4
|
+
data.tar.gz: b5ea5c2c7710196f76bb2773f55cda6380075336
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: f939c511550ce296668dd3154863cb5401ef7d8710b5dd6a165387555ebce6f769ea587d1a798b9ce39b8b69f39ca1120adff150579e892c84fed22d0f26e7be
|
7
|
+
data.tar.gz: b12940f1e651fe6de8fa33ac0ee1a68ed8041e4120a5c48b52a53d4e239f5470d547b616639876d8c6f01b194ea7ab59b0796a2dd5d3c1d98bd06ef79b4b77d9
|
data/lib/graphql.rb
CHANGED
@@ -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
|
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
|
|
data/lib/graphql/field.rb
CHANGED
@@ -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
|
data/lib/graphql/query.rb
CHANGED
@@ -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:
|
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
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
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
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
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(
|
45
|
-
chain = GraphQL::Query::DirectiveChain.new(
|
46
|
-
|
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
|
data/lib/graphql/schema.rb
CHANGED
@@ -21,11 +21,19 @@ class GraphQL::Schema
|
|
21
21
|
end
|
22
22
|
|
23
23
|
# A `{ name => type }` hash of types in this schema
|
24
|
-
# @
|
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
|
-
|
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
|
-
|
6
|
-
|
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
|
-
|
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
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
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
|
-
|
27
|
-
|
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
|
data/lib/graphql/version.rb
CHANGED
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
|
-
|
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) {
|
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(:
|
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(:
|
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 }
|
@@ -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) {
|
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) {
|
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(:
|
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,
|
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,
|
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,
|
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(:
|
7
|
-
|
6
|
+
let(:schema) { DummySchema }
|
7
|
+
let(:variables) { {"cheeseId" => 2} }
|
8
|
+
let(:result) { schema.execute(
|
8
9
|
query_string,
|
9
|
-
variables:
|
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
|
data/spec/graphql/query_spec.rb
CHANGED
@@ -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(
|
6
|
-
|
7
|
-
|
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: $
|
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: $
|
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 $
|
31
|
-
"locations"=>[{"line"=>
|
38
|
+
"message"=>"Variable $notUsedVar is declared by getCheese but not used",
|
39
|
+
"locations"=>[{"line"=>2, "column"=>5}]
|
32
40
|
},
|
33
41
|
{
|
34
|
-
"message"=>"Variable $
|
35
|
-
"locations"=>[{"line"=>
|
42
|
+
"message"=>"Variable $undefinedVar is used by getCheese but not declared",
|
43
|
+
"locations"=>[{"line"=>11, "column"=>30}]
|
36
44
|
},
|
37
45
|
{
|
38
|
-
"message"=>"Variable $
|
39
|
-
"locations"=>[{"line"=>
|
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: $
|
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(
|
49
|
+
# nonsenseField, nonsenseArg, bogusField, bogusArg, undefinedVar
|
50
|
+
assert_equal(5, errors.length)
|
47
51
|
end
|
48
52
|
end
|
49
53
|
|
data/spec/support/dairy_data.rb
CHANGED
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.
|
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-
|
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
|