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