graphql 0.9.5 → 0.10.0
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/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) {
|