graphql 0.9.5 → 0.10.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/lib/graphql/base_type.rb +24 -0
- data/lib/graphql/definition_helpers/defined_by_config.rb +5 -4
- data/lib/graphql/definition_helpers/non_null_with_bang.rb +1 -1
- data/lib/graphql/definition_helpers/type_definer.rb +1 -1
- data/lib/graphql/enum_type.rb +16 -3
- data/lib/graphql/input_object_type.rb +10 -0
- data/lib/graphql/language.rb +0 -5
- data/lib/graphql/language/nodes.rb +79 -75
- data/lib/graphql/language/parser.rb +109 -106
- data/lib/graphql/language/transform.rb +100 -91
- data/lib/graphql/language/visitor.rb +78 -74
- data/lib/graphql/list_type.rb +5 -0
- data/lib/graphql/non_null_type.rb +6 -2
- data/lib/graphql/query.rb +58 -9
- data/lib/graphql/query/arguments.rb +29 -26
- data/lib/graphql/query/base_execution/value_resolution.rb +3 -3
- data/lib/graphql/query/directive_chain.rb +1 -1
- data/lib/graphql/query/executor.rb +6 -27
- data/lib/graphql/query/literal_input.rb +89 -0
- data/lib/graphql/query/ruby_input.rb +20 -0
- data/lib/graphql/query/serial_execution/field_resolution.rb +1 -1
- data/lib/graphql/query/variables.rb +39 -0
- data/lib/graphql/scalar_type.rb +27 -5
- data/lib/graphql/schema.rb +5 -0
- data/lib/graphql/schema/type_expression.rb +28 -0
- data/lib/graphql/static_validation/literal_validator.rb +5 -2
- data/lib/graphql/static_validation/rules/fields_are_defined_on_type.rb +0 -2
- data/lib/graphql/static_validation/rules/variable_default_values_are_correctly_typed.rb +1 -1
- data/lib/graphql/version.rb +1 -1
- data/readme.md +3 -2
- data/spec/graphql/enum_type_spec.rb +7 -2
- data/spec/graphql/input_object_type_spec.rb +45 -0
- data/spec/graphql/language/parser_spec.rb +2 -1
- data/spec/graphql/language/transform_spec.rb +5 -0
- data/spec/graphql/query/base_execution/value_resolution_spec.rb +46 -0
- data/spec/graphql/query/context_spec.rb +37 -0
- data/spec/graphql/query/executor_spec.rb +33 -0
- data/spec/graphql/query_spec.rb +110 -26
- data/spec/graphql/schema/type_expression_spec.rb +38 -0
- data/spec/graphql/static_validation/rules/argument_literals_are_compatible_spec.rb +2 -2
- data/spec/support/dairy_app.rb +17 -17
- data/spec/support/dairy_data.rb +2 -2
- metadata +12 -2
@@ -1,34 +1,37 @@
|
|
1
|
-
|
2
|
-
class
|
3
|
-
|
1
|
+
module GraphQL
|
2
|
+
class Query
|
3
|
+
# Read-only access to values, normalizing all keys to strings
|
4
|
+
#
|
5
|
+
# {Arguments} recursively wraps the input in {Arguments} instances.
|
6
|
+
class Arguments
|
7
|
+
extend Forwardable
|
4
8
|
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
end
|
12
|
-
end
|
9
|
+
def initialize(values)
|
10
|
+
@values = values.inject({}) do |memo, (inner_key, inner_value)|
|
11
|
+
memo[inner_key] = wrap_value(inner_value)
|
12
|
+
memo
|
13
|
+
end
|
14
|
+
end
|
13
15
|
|
14
|
-
|
16
|
+
# @param [String, Symbol] name or index of value to access
|
17
|
+
# @return [Object] the argument at that key
|
18
|
+
def [](key)
|
19
|
+
@values[key.to_s]
|
20
|
+
end
|
15
21
|
|
16
|
-
|
17
|
-
@hash[key.to_s]
|
18
|
-
end
|
22
|
+
def_delegators :@values_hash, :keys, :values, :each
|
19
23
|
|
20
|
-
|
24
|
+
private
|
21
25
|
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
value
|
26
|
+
def wrap_value(value)
|
27
|
+
if value.is_a?(Array)
|
28
|
+
value.map { |item| wrap_value(item) }
|
29
|
+
elsif value.is_a?(Hash)
|
30
|
+
self.class.new(value)
|
31
|
+
else
|
32
|
+
value
|
33
|
+
end
|
34
|
+
end
|
32
35
|
end
|
33
36
|
end
|
34
37
|
end
|
@@ -29,9 +29,9 @@ module GraphQL
|
|
29
29
|
end
|
30
30
|
|
31
31
|
class ScalarResolution < BaseResolution
|
32
|
-
# Apply the scalar's defined `
|
32
|
+
# Apply the scalar's defined `coerce_result` method to the value
|
33
33
|
def result
|
34
|
-
field_type.
|
34
|
+
field_type.coerce_result(value)
|
35
35
|
end
|
36
36
|
end
|
37
37
|
|
@@ -60,7 +60,7 @@ module GraphQL
|
|
60
60
|
class EnumResolution < BaseResolution
|
61
61
|
# Get the string name for this enum value
|
62
62
|
def result
|
63
|
-
|
63
|
+
field_type.coerce_result(value)
|
64
64
|
end
|
65
65
|
end
|
66
66
|
|
@@ -30,7 +30,7 @@ class GraphQL::Query::DirectiveChain
|
|
30
30
|
@result = block.call
|
31
31
|
else
|
32
32
|
applicable_directives.map do |(ast_directive, directive)|
|
33
|
-
args = GraphQL::Query::
|
33
|
+
args = GraphQL::Query::LiteralInput.from_arguments(ast_directive.arguments, directive.arguments, query.variables)
|
34
34
|
@result = directive.resolve(args, block)
|
35
35
|
end
|
36
36
|
@result ||= {}
|
@@ -1,23 +1,11 @@
|
|
1
1
|
module GraphQL
|
2
2
|
class Query
|
3
3
|
class Executor
|
4
|
-
class OperationNameMissingError < StandardError
|
5
|
-
def initialize(names)
|
6
|
-
msg = "You must provide an operation name from: #{names.join(", ")}"
|
7
|
-
super(msg)
|
8
|
-
end
|
9
|
-
end
|
10
|
-
|
11
4
|
# @return [GraphQL::Query] the query being executed
|
12
5
|
attr_reader :query
|
13
6
|
|
14
|
-
|
15
|
-
attr_reader :operation_name
|
16
|
-
|
17
|
-
|
18
|
-
def initialize(query, operation_name)
|
7
|
+
def initialize(query)
|
19
8
|
@query = query
|
20
|
-
@operation_name = operation_name
|
21
9
|
end
|
22
10
|
|
23
11
|
# Evalute {operation_name} on {query}. Handle errors by putting them in the "errors" key.
|
@@ -25,19 +13,20 @@ module GraphQL
|
|
25
13
|
# @return [Hash] A GraphQL response, with either a "data" key or an "errors" key
|
26
14
|
def result
|
27
15
|
execute
|
28
|
-
rescue OperationNameMissingError => err
|
16
|
+
rescue GraphQL::Query::OperationNameMissingError, GraphQL::Query::VariableMissingError => err
|
29
17
|
{"errors" => [{"message" => err.message}]}
|
30
18
|
rescue StandardError => err
|
31
19
|
query.debug && raise(err)
|
32
|
-
message = "Something went wrong during query execution: #{err}"
|
20
|
+
message = "Something went wrong during query execution: #{err}" #\n#{err.backtrace.join("\n ")}"
|
33
21
|
{"errors" => [{"message" => message}]}
|
34
22
|
end
|
35
23
|
|
36
24
|
private
|
37
25
|
|
38
26
|
def execute
|
39
|
-
|
40
|
-
|
27
|
+
operation = query.selected_operation
|
28
|
+
return {} if operation.nil?
|
29
|
+
|
41
30
|
if operation.operation_type == "query"
|
42
31
|
root_type = query.schema.query
|
43
32
|
execution_strategy_class = query.schema.query_execution_strategy
|
@@ -57,16 +46,6 @@ module GraphQL
|
|
57
46
|
|
58
47
|
result
|
59
48
|
end
|
60
|
-
|
61
|
-
def find_operation(operation_name, operations)
|
62
|
-
if operations.length == 1
|
63
|
-
operations.values.first
|
64
|
-
elsif !operations.key?(operation_name)
|
65
|
-
raise OperationNameMissingError, operations.keys
|
66
|
-
else
|
67
|
-
operations[operation_name]
|
68
|
-
end
|
69
|
-
end
|
70
49
|
end
|
71
50
|
end
|
72
51
|
end
|
@@ -0,0 +1,89 @@
|
|
1
|
+
module GraphQL
|
2
|
+
class Query
|
3
|
+
# Turn query string values into something useful for query execution
|
4
|
+
class LiteralInput
|
5
|
+
attr_reader :variables, :value, :type
|
6
|
+
def initialize(type, incoming_value, variables)
|
7
|
+
@type = type
|
8
|
+
@value = incoming_value
|
9
|
+
@variables = variables
|
10
|
+
end
|
11
|
+
|
12
|
+
def graphql_value
|
13
|
+
if value.is_a?(GraphQL::Language::Nodes::VariableIdentifier)
|
14
|
+
variables[value.name] # Already cleaned up with RubyInput
|
15
|
+
elsif type.kind.input_object?
|
16
|
+
input_values = {}
|
17
|
+
inner_type = type.unwrap
|
18
|
+
inner_type.input_fields.each do |arg_name, arg_defn|
|
19
|
+
ast_arg = value.pairs.find { |ast_arg| ast_arg.name == arg_name }
|
20
|
+
raw_value = resolve_argument_value(ast_arg, arg_defn, variables)
|
21
|
+
reduced_value = coerce(arg_defn.type, raw_value, variables)
|
22
|
+
input_values[arg_name] = reduced_value
|
23
|
+
end
|
24
|
+
input_values
|
25
|
+
elsif type.kind.list?
|
26
|
+
inner_type = type.of_type
|
27
|
+
value.map { |item| coerce(inner_type, item, variables) }
|
28
|
+
elsif type.kind.non_null?
|
29
|
+
inner_type = type.of_type
|
30
|
+
coerce(inner_type, value, variables)
|
31
|
+
elsif type.kind.scalar?
|
32
|
+
type.coerce_input!(value)
|
33
|
+
elsif type.kind.enum?
|
34
|
+
value_name = value.name # it's a Nodes::Enum
|
35
|
+
type.coerce_input!(value_name)
|
36
|
+
else
|
37
|
+
raise "Unknown input #{value} of type #{type}"
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def self.coerce(type, value, variables)
|
42
|
+
input = self.new(type, value, variables)
|
43
|
+
input.graphql_value
|
44
|
+
end
|
45
|
+
|
46
|
+
def self.from_arguments(ast_arguments, argument_defns, variables)
|
47
|
+
values_hash = {}
|
48
|
+
argument_defns.each do |arg_name, arg_defn|
|
49
|
+
ast_arg = ast_arguments.find { |ast_arg| ast_arg.name == arg_name }
|
50
|
+
arg_value = nil
|
51
|
+
if ast_arg
|
52
|
+
arg_value = coerce(arg_defn.type, ast_arg.value, variables)
|
53
|
+
end
|
54
|
+
if arg_value.nil?
|
55
|
+
arg_value = arg_defn.default_value
|
56
|
+
end
|
57
|
+
values_hash[arg_name] = arg_value
|
58
|
+
end
|
59
|
+
GraphQL::Query::Arguments.new(values_hash)
|
60
|
+
end
|
61
|
+
|
62
|
+
private
|
63
|
+
|
64
|
+
def coerce(*args)
|
65
|
+
self.class.coerce(*args)
|
66
|
+
end
|
67
|
+
|
68
|
+
def resolve_argument_value(*args)
|
69
|
+
self.class.resolve_argument_value(*args)
|
70
|
+
end
|
71
|
+
|
72
|
+
# Prefer values in this order:
|
73
|
+
# - Literal value from the query string
|
74
|
+
# - Variable value from query varibles
|
75
|
+
# - Default value from Argument definition
|
76
|
+
def self.resolve_argument_value(ast_arg, arg_defn, variables)
|
77
|
+
if !ast_arg.nil?
|
78
|
+
raw_value = ast_arg.value
|
79
|
+
end
|
80
|
+
|
81
|
+
if raw_value.nil?
|
82
|
+
raw_value = arg_defn.default_value
|
83
|
+
end
|
84
|
+
|
85
|
+
raw_value
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module GraphQL
|
2
|
+
class Query
|
3
|
+
# Turn Ruby values into something useful for query execution
|
4
|
+
class RubyInput
|
5
|
+
def initialize(type, incoming_value)
|
6
|
+
@type = type
|
7
|
+
@incoming_value = incoming_value
|
8
|
+
end
|
9
|
+
|
10
|
+
def graphql_value
|
11
|
+
@type.coerce_input!(@incoming_value)
|
12
|
+
end
|
13
|
+
|
14
|
+
def self.coerce(type, value)
|
15
|
+
input = self.new(type, value)
|
16
|
+
input.graphql_value
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -11,7 +11,7 @@ module GraphQL
|
|
11
11
|
@query = query
|
12
12
|
@execution_strategy = execution_strategy
|
13
13
|
@field = query.schema.get_field(parent_type, ast_node.name) || raise("No field found on #{parent_type.name} '#{parent_type}' for '#{ast_node.name}'")
|
14
|
-
@arguments = GraphQL::Query::
|
14
|
+
@arguments = GraphQL::Query::LiteralInput.from_arguments(ast_node.arguments, field.arguments, query.variables)
|
15
15
|
end
|
16
16
|
|
17
17
|
def result
|
@@ -0,0 +1,39 @@
|
|
1
|
+
module GraphQL
|
2
|
+
class Query
|
3
|
+
# Read-only access to query variables, applying default values if needed.
|
4
|
+
class Variables
|
5
|
+
def initialize(schema, ast_variables, provided_variables)
|
6
|
+
@schema = schema
|
7
|
+
@provided_variables = provided_variables
|
8
|
+
@storage = ast_variables.each_with_object({}) do |ast_variable, memo|
|
9
|
+
variable_name = ast_variable.name
|
10
|
+
memo[variable_name] = get_graphql_value(ast_variable)
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
def [](key)
|
15
|
+
@storage.fetch(key)
|
16
|
+
end
|
17
|
+
|
18
|
+
private
|
19
|
+
|
20
|
+
# Find the right value for this variable:
|
21
|
+
# - First, use the value provided at runtime
|
22
|
+
# - Then, fall back to the default value from the query string
|
23
|
+
# If it's still nil, raise an error if it's required.
|
24
|
+
def get_graphql_value(ast_variable)
|
25
|
+
variable_type = @schema.type_from_ast(ast_variable.type)
|
26
|
+
variable_name = ast_variable.name
|
27
|
+
default_value = ast_variable.default_value
|
28
|
+
provided_value = @provided_variables[variable_name]
|
29
|
+
if !provided_value.nil?
|
30
|
+
graphql_value = GraphQL::Query::RubyInput.coerce(variable_type, provided_value)
|
31
|
+
elsif !default_value.nil?
|
32
|
+
graphql_value = GraphQL::Query::LiteralInput.coerce(variable_type, default_value, {})
|
33
|
+
elsif variable_type.kind.non_null?
|
34
|
+
raise GraphQL::Query::VariableMissingError.new(variable_name, variable_type)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
data/lib/graphql/scalar_type.rb
CHANGED
@@ -1,16 +1,38 @@
|
|
1
1
|
module GraphQL
|
2
2
|
# The parent type for scalars, eg {GraphQL::STRING_TYPE}, {GraphQL::INT_TYPE}
|
3
3
|
#
|
4
|
+
# @example defining a type for Time
|
5
|
+
# TimeType = GraphQL::ObjectType.define do
|
6
|
+
# name "Time"
|
7
|
+
# description "Time since epoch in seconds"
|
8
|
+
#
|
9
|
+
# coerce_input ->(value) { Time.at(Float(value)) }
|
10
|
+
# coerce_result ->(value) { value.to_f }
|
11
|
+
# end
|
12
|
+
#
|
4
13
|
class ScalarType < GraphQL::BaseType
|
5
|
-
defined_by_config :name, :coerce, :description
|
14
|
+
defined_by_config :name, :coerce, :coerce_input, :coerce_result, :description
|
6
15
|
attr_accessor :name, :description
|
7
16
|
|
8
|
-
def coerce(
|
9
|
-
|
17
|
+
def coerce=(proc)
|
18
|
+
self.coerce_input = proc
|
19
|
+
self.coerce_result = proc
|
10
20
|
end
|
11
21
|
|
12
|
-
def
|
13
|
-
@
|
22
|
+
def coerce_input(value)
|
23
|
+
@coerce_input_proc.call(value)
|
24
|
+
end
|
25
|
+
|
26
|
+
def coerce_input=(proc)
|
27
|
+
@coerce_input_proc = proc unless proc.nil?
|
28
|
+
end
|
29
|
+
|
30
|
+
def coerce_result(value)
|
31
|
+
@coerce_result_proc.call(value)
|
32
|
+
end
|
33
|
+
|
34
|
+
def coerce_result=(proc)
|
35
|
+
@coerce_result_proc = proc unless proc.nil?
|
14
36
|
end
|
15
37
|
|
16
38
|
def kind
|
data/lib/graphql/schema.rb
CHANGED
@@ -58,6 +58,10 @@ class GraphQL::Schema
|
|
58
58
|
end
|
59
59
|
end
|
60
60
|
|
61
|
+
def type_from_ast(ast_node)
|
62
|
+
GraphQL::Schema::TypeExpression.new(self, ast_node).type
|
63
|
+
end
|
64
|
+
|
61
65
|
class InvalidTypeError < StandardError
|
62
66
|
def initialize(type, errors)
|
63
67
|
super("Type #{type.respond_to?(:name) ? type.name : "Unnamed type" } is invalid: #{errors.join(", ")}")
|
@@ -70,6 +74,7 @@ require 'graphql/schema/field_validator'
|
|
70
74
|
require 'graphql/schema/implementation_validator'
|
71
75
|
require 'graphql/schema/middleware_chain'
|
72
76
|
require 'graphql/schema/rescue_middleware'
|
77
|
+
require 'graphql/schema/type_expression'
|
73
78
|
require 'graphql/schema/type_reducer'
|
74
79
|
require 'graphql/schema/type_map'
|
75
80
|
require 'graphql/schema/type_validator'
|
@@ -0,0 +1,28 @@
|
|
1
|
+
module GraphQL
|
2
|
+
class Schema
|
3
|
+
class TypeExpression
|
4
|
+
def initialize(schema, ast_node)
|
5
|
+
@schema = schema
|
6
|
+
@ast_node = ast_node
|
7
|
+
end
|
8
|
+
def type
|
9
|
+
@type ||= build_type(@schema, @ast_node)
|
10
|
+
end
|
11
|
+
|
12
|
+
private
|
13
|
+
|
14
|
+
def build_type(schema, ast_node)
|
15
|
+
if ast_node.is_a?(GraphQL::Language::Nodes::TypeName)
|
16
|
+
type_name = ast_node.name
|
17
|
+
schema.types[type_name]
|
18
|
+
elsif ast_node.is_a?(GraphQL::Language::Nodes::NonNullType)
|
19
|
+
ast_inner_type = ast_node.of_type
|
20
|
+
build_type(schema, ast_inner_type).to_non_null_type
|
21
|
+
elsif ast_node.is_a?(GraphQL::Language::Nodes::ListType)
|
22
|
+
ast_inner_type = ast_node.of_type
|
23
|
+
build_type(schema, ast_inner_type).to_list_type
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -7,9 +7,9 @@ class GraphQL::StaticValidation::LiteralValidator
|
|
7
7
|
item_type = type.of_type
|
8
8
|
ast_value.all? { |val| validate(val, item_type) }
|
9
9
|
elsif type.kind.scalar?
|
10
|
-
!type.
|
10
|
+
!type.coerce_input(ast_value).nil?
|
11
11
|
elsif type.kind.enum? && ast_value.is_a?(GraphQL::Language::Nodes::Enum)
|
12
|
-
!type.
|
12
|
+
!type.coerce_input(ast_value.name).nil?
|
13
13
|
elsif type.kind.input_object? && ast_value.is_a?(GraphQL::Language::Nodes::InputObject)
|
14
14
|
fields = type.input_fields
|
15
15
|
ast_value.pairs.all? do |value|
|
@@ -17,6 +17,9 @@ class GraphQL::StaticValidation::LiteralValidator
|
|
17
17
|
present_if_required = field_type.kind.non_null? ? !value.nil? : true
|
18
18
|
present_if_required && validate(value.value, field_type)
|
19
19
|
end
|
20
|
+
elsif ast_value.is_a?(GraphQL::Language::Nodes::VariableIdentifier)
|
21
|
+
# Todo: somehow pass in the document's variable definitions and validate this
|
22
|
+
true
|
20
23
|
else
|
21
24
|
false
|
22
25
|
end
|
@@ -1,8 +1,6 @@
|
|
1
1
|
class GraphQL::StaticValidation::FieldsAreDefinedOnType
|
2
2
|
include GraphQL::StaticValidation::Message::MessageHelper
|
3
3
|
|
4
|
-
IS_FIELD = Proc.new {|f| f.is_a?(GraphQL::Language::Nodes::Field) }
|
5
|
-
|
6
4
|
def validate(context)
|
7
5
|
visitor = context.visitor
|
8
6
|
visitor[GraphQL::Language::Nodes::Field] << -> (node, parent) {
|