graphql 1.8.0.pre9 → 1.8.0.pre10

Sign up to get free protection for your applications and to get access to all the features.
Files changed (59) hide show
  1. checksums.yaml +4 -4
  2. data/lib/graphql/argument.rb +1 -0
  3. data/lib/graphql/base_type.rb +2 -0
  4. data/lib/graphql/compatibility/query_parser_specification.rb +110 -0
  5. data/lib/graphql/deprecated_dsl.rb +15 -3
  6. data/lib/graphql/directive.rb +1 -0
  7. data/lib/graphql/enum_type.rb +2 -0
  8. data/lib/graphql/execution/multiplex.rb +1 -1
  9. data/lib/graphql/field.rb +2 -0
  10. data/lib/graphql/introspection/entry_points.rb +2 -2
  11. data/lib/graphql/introspection/schema_field.rb +1 -1
  12. data/lib/graphql/introspection/type_by_name_field.rb +1 -1
  13. data/lib/graphql/language/parser.rb +25 -25
  14. data/lib/graphql/language/parser.y +7 -7
  15. data/lib/graphql/query/arguments.rb +12 -0
  16. data/lib/graphql/relay/mutation/instrumentation.rb +1 -1
  17. data/lib/graphql/relay/mutation/resolve.rb +5 -1
  18. data/lib/graphql/schema.rb +5 -1
  19. data/lib/graphql/schema/argument.rb +1 -0
  20. data/lib/graphql/schema/build_from_definition.rb +60 -18
  21. data/lib/graphql/schema/enum.rb +1 -0
  22. data/lib/graphql/schema/enum_value.rb +1 -0
  23. data/lib/graphql/schema/field.rb +44 -31
  24. data/lib/graphql/schema/field/dynamic_resolve.rb +4 -8
  25. data/lib/graphql/schema/input_object.rb +30 -19
  26. data/lib/graphql/schema/interface.rb +12 -5
  27. data/lib/graphql/schema/member.rb +10 -0
  28. data/lib/graphql/schema/member/build_type.rb +3 -1
  29. data/lib/graphql/schema/member/has_arguments.rb +50 -0
  30. data/lib/graphql/schema/member/has_fields.rb +1 -1
  31. data/lib/graphql/schema/member/instrumentation.rb +4 -4
  32. data/lib/graphql/schema/mutation.rb +195 -0
  33. data/lib/graphql/schema/object.rb +4 -5
  34. data/lib/graphql/schema/relay_classic_mutation.rb +85 -0
  35. data/lib/graphql/schema/scalar.rb +1 -0
  36. data/lib/graphql/schema/traversal.rb +1 -1
  37. data/lib/graphql/schema/union.rb +1 -0
  38. data/lib/graphql/subscriptions/action_cable_subscriptions.rb +1 -1
  39. data/lib/graphql/unresolved_type_error.rb +3 -2
  40. data/lib/graphql/upgrader/member.rb +194 -19
  41. data/lib/graphql/version.rb +1 -1
  42. data/spec/fixtures/upgrader/delete_project.original.rb +28 -0
  43. data/spec/fixtures/upgrader/delete_project.transformed.rb +27 -0
  44. data/spec/fixtures/upgrader/increment_count.original.rb +59 -0
  45. data/spec/fixtures/upgrader/increment_count.transformed.rb +50 -0
  46. data/spec/graphql/execution/multiplex_spec.rb +1 -1
  47. data/spec/graphql/language/parser_spec.rb +0 -74
  48. data/spec/graphql/query/serial_execution/value_resolution_spec.rb +2 -8
  49. data/spec/graphql/query_spec.rb +26 -0
  50. data/spec/graphql/relay/mutation_spec.rb +2 -2
  51. data/spec/graphql/schema/build_from_definition_spec.rb +59 -0
  52. data/spec/graphql/schema/field_spec.rb +24 -0
  53. data/spec/graphql/schema/input_object_spec.rb +1 -0
  54. data/spec/graphql/schema/interface_spec.rb +4 -1
  55. data/spec/graphql/schema/mutation_spec.rb +99 -0
  56. data/spec/graphql/schema/relay_classic_mutation_spec.rb +28 -0
  57. data/spec/support/jazz.rb +25 -1
  58. data/spec/support/star_wars/schema.rb +17 -27
  59. metadata +17 -2
@@ -4,9 +4,12 @@ module GraphQL
4
4
  class InputObject < GraphQL::Schema::Member
5
5
  extend GraphQL::Schema::Member::AcceptsDefinition
6
6
  extend GraphQL::Delegate
7
+ extend GraphQL::Schema::Member::HasArguments
7
8
 
8
9
  def initialize(values, context:, defaults_used:)
9
10
  @arguments = self.class.arguments_class.new(values, context: context, defaults_used: defaults_used)
11
+ # Symbolized, underscored hash:
12
+ @ruby_style_hash = @arguments.to_kwargs
10
13
  @context = context
11
14
  end
12
15
 
@@ -16,40 +19,48 @@ module GraphQL
16
19
  # @return [GraphQL::Query::Arguments] The underlying arguments instance
17
20
  attr_reader :arguments
18
21
 
19
- # A lot of methods work just like GraphQL::Arguments
20
- def_delegators :@arguments, :[], :key?, :to_h
21
- def_delegators :to_h, :keys, :values, :each, :any?
22
+ # Ruby-like hash behaviors, read-only
23
+ def_delegators :@ruby_style_hash, :to_h, :keys, :values, :each, :any?
24
+
25
+ # Lookup a key on this object, it accepts new-style underscored symbols
26
+ # Or old-style camelized identifiers.
27
+ # @param key [Symbol, String]
28
+ def [](key)
29
+ if @ruby_style_hash.key?(key)
30
+ @ruby_style_hash[key]
31
+ else
32
+ @arguments[key]
33
+ end
34
+ end
35
+
36
+ def key?(key)
37
+ @ruby_style_hash.key?(key) || @arguments.key?(key)
38
+ end
39
+
40
+ # A copy of the Ruby-style hash
41
+ def to_kwargs
42
+ @ruby_style_hash.dup
43
+ end
22
44
 
23
45
  class << self
24
46
  # @return [Class<GraphQL::Arguments>]
25
47
  attr_accessor :arguments_class
26
48
 
27
- def argument(*args, **kwargs)
28
- kwargs[:owner] = self
29
- argument = GraphQL::Schema::Argument.new(*args, **kwargs)
30
- arg_name = argument.graphql_definition.name
31
- own_arguments[arg_name] = argument
49
+ def argument(*args)
50
+ argument_defn = super
32
51
  # Add a method access
52
+ arg_name = argument_defn.graphql_definition.name
33
53
  define_method(Member::BuildType.underscore(arg_name)) do
34
54
  @arguments.public_send(arg_name)
35
55
  end
36
56
  end
37
57
 
38
- # @return [Hash<String => GraphQL::Schema::Argument] Input fields on this input object, keyed by name.
39
- def arguments
40
- inherited_arguments = (superclass <= GraphQL::Schema::InputObject ? superclass.arguments : {})
41
- # Local definitions override inherited ones
42
- inherited_arguments.merge(own_arguments)
43
- end
44
-
45
- def own_arguments
46
- @own_arguments ||= {}
47
- end
48
-
49
58
  def to_graphql
50
59
  type_defn = GraphQL::InputObjectType.new
51
60
  type_defn.name = graphql_name
52
61
  type_defn.description = description
62
+ type_defn.metadata[:type_class] = self
63
+ type_defn.mutation = mutation
53
64
  arguments.each do |name, arg|
54
65
  type_defn.arguments[arg.graphql_definition.name] = arg.graphql_definition
55
66
  end
@@ -4,16 +4,22 @@ module GraphQL
4
4
  class Interface < GraphQL::Schema::Member
5
5
  extend GraphQL::Schema::Member::HasFields
6
6
  extend GraphQL::Schema::Member::AcceptsDefinition
7
- field_class GraphQL::Schema::Field
8
7
 
9
8
  class << self
9
+ # Always set up a `self::Implementation` module,
10
+ # which may or may not be added to.
11
+ def inherited(child_cls)
12
+ # This module will be mixed in to each object class,
13
+ # so it can contain methods to implement fields.
14
+ # It's always added to interfaces, but sometimes it's left empty.
15
+ child_cls.const_set(:Implementation, Module.new)
16
+ end
17
+
10
18
  # When this interface is added to a `GraphQL::Schema::Object`,
11
19
  # it calls this method. We add methods to the object by convention,
12
20
  # a nested module named `Implementation`
13
- def apply_implemented(object_class)
14
- if defined?(self::Implementation)
15
- object_class.include(self::Implementation)
16
- end
21
+ def implemented(object_class)
22
+ object_class.include(self::Implementation)
17
23
  end
18
24
 
19
25
  def orphan_types(*types)
@@ -35,6 +41,7 @@ module GraphQL
35
41
  field_defn = field_inst.graphql_definition
36
42
  type_defn.fields[field_defn.name] = field_defn
37
43
  end
44
+ type_defn.metadata[:type_class] = self
38
45
  if respond_to?(:resolve_type)
39
46
  type_defn.resolve_type = method(:resolve_type)
40
47
  end
@@ -87,6 +87,15 @@ module GraphQL
87
87
  end
88
88
  end
89
89
 
90
+ # The mutation this type was derived from, if it was derived from a mutation
91
+ # @return [Class]
92
+ def mutation(mutation_class = nil)
93
+ if mutation_class
94
+ @mutation = mutation_class
95
+ end
96
+ @mutation
97
+ end
98
+
90
99
  def to_graphql
91
100
  raise NotImplementedError
92
101
  end
@@ -111,6 +120,7 @@ end
111
120
 
112
121
  require 'graphql/schema/member/list_type_proxy'
113
122
  require 'graphql/schema/member/non_null_type_proxy'
123
+ require 'graphql/schema/member/has_arguments'
114
124
  require 'graphql/schema/member/has_fields'
115
125
  require 'graphql/schema/member/instrumentation'
116
126
  require 'graphql/schema/member/build_type'
@@ -64,12 +64,14 @@ module GraphQL
64
64
  raise ArgumentError, LIST_TYPE_ERROR
65
65
  end
66
66
  when Class
67
- if Class < GraphQL::Schema::Member
67
+ if type_expr < GraphQL::Schema::Member
68
68
  type_expr.graphql_definition
69
69
  else
70
70
  # Eg `String` => GraphQL::STRING_TYPE
71
71
  parse_type(type_expr.name, null: true)
72
72
  end
73
+ else
74
+ raise "Unexpected type_expr input: #{type_expr} (#{type_expr.class})"
73
75
  end
74
76
 
75
77
  # Apply list_type first, that way the
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+ module GraphQL
3
+ class Schema
4
+ class Member
5
+ module HasArguments
6
+ def self.included(cls)
7
+ cls.extend(ArgumentClassAccessor)
8
+ end
9
+
10
+ def self.extended(cls)
11
+ cls.extend(ArgumentClassAccessor)
12
+ end
13
+
14
+ # @see {GraphQL::Schema::Argument#initialize} for parameters
15
+ # @return [GraphQL::Schema::Argument] An instance of {arguments_class}, created from `*args`
16
+ def argument(*args, **kwargs, &block)
17
+ kwargs[:owner] = self
18
+ arg_defn = self.argument_class.new(*args, **kwargs, &block)
19
+ own_arguments[arg_defn.name] = arg_defn
20
+ end
21
+
22
+ # @return [Hash<String => GraphQL::Schema::Argument] Arguments defined on this thing, keyed by name. Includes inherited definitions
23
+ def arguments
24
+ inherited_arguments = ((self.is_a?(Class) && superclass <= GraphQL::Schema::Member::HasArguments) ? superclass.arguments : {})
25
+ # Local definitions override inherited ones
26
+ inherited_arguments.merge(own_arguments)
27
+ end
28
+
29
+ # @param new_arg_class [Class] A class to use for building argument definitions
30
+ def argument_class(new_arg_class = nil)
31
+ self.class.argument_class(new_arg_class)
32
+ end
33
+
34
+ module ArgumentClassAccessor
35
+ def argument_class(new_arg_class = nil)
36
+ if new_arg_class
37
+ @argument_class = new_arg_class
38
+ else
39
+ @argument_class || (superclass.respond_to?(:argument_class) ? superclass.argument_class : GraphQL::Schema::Argument)
40
+ end
41
+ end
42
+ end
43
+
44
+ def own_arguments
45
+ @own_arguments ||= {}
46
+ end
47
+ end
48
+ end
49
+ end
50
+ end
@@ -34,7 +34,7 @@ module GraphQL
34
34
  if new_field_class
35
35
  @field_class = new_field_class
36
36
  else
37
- @field_class || superclass.field_class
37
+ @field_class || (superclass.respond_to?(:field_class) ? superclass.field_class : GraphQL::Schema::Field)
38
38
  end
39
39
  end
40
40
 
@@ -8,9 +8,9 @@ module GraphQL
8
8
  module_function
9
9
  def instrument(type, field)
10
10
  return_type = field.type.unwrap
11
- if return_type.metadata[:object_class] ||
11
+ if (return_type.is_a?(GraphQL::ObjectType) && return_type.metadata[:type_class]) ||
12
12
  return_type.is_a?(GraphQL::InterfaceType) ||
13
- (return_type.is_a?(GraphQL::UnionType) && return_type.possible_types.any? { |t| t.metadata[:object_class] })
13
+ (return_type.is_a?(GraphQL::UnionType) && return_type.possible_types.any? { |t| t.metadata[:type_class] })
14
14
  field = apply_proxy(field)
15
15
  end
16
16
 
@@ -25,7 +25,7 @@ module GraphQL
25
25
  else
26
26
  root_type = query.irep_selection.return_type
27
27
  # If it has a wrapper, apply it
28
- wrapper_class = root_type.metadata[:object_class]
28
+ wrapper_class = root_type.metadata[:type_class]
29
29
  if wrapper_class
30
30
  new_root_value = wrapper_class.new(query.root_value, query.context)
31
31
  query.root_value = new_root_value
@@ -99,7 +99,7 @@ module GraphQL
99
99
  raise "unexpected proxying type #{type} for #{obj} at #{ctx.owner_type}.#{ctx.field.name}"
100
100
  end
101
101
 
102
- if concrete_type && (object_class = concrete_type.metadata[:object_class])
102
+ if concrete_type && (object_class = concrete_type.metadata[:type_class])
103
103
  # use the query-level context here, since it won't be field-specific anyways
104
104
  query_ctx = ctx.query.context
105
105
  object_class.new(obj, query_ctx)
@@ -0,0 +1,195 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GraphQL
4
+ class Schema
5
+ # This base class accepts configuration for a mutation root field,
6
+ # then it can be hooked up to your mutation root object type.
7
+ #
8
+ # If you want to customize how this class generates types, in your base class,
9
+ # override the various `generate_*` methods.
10
+ #
11
+ # @see {GraphQL::Schema::RelayClassicMutation} for an extension of this class with some conventions built-in.
12
+ #
13
+ # @example Creating a comment
14
+ # # Define the mutation:
15
+ # class Mutations::CreateComment < GraphQL::Schema::Mutation
16
+ # argument :body, String, required: true
17
+ # argument :post_id, ID, required: true
18
+ #
19
+ # field :comment, Types::Comment, null: true
20
+ # field :error_messages, [String], null: false
21
+ #
22
+ # def resolve(body:, post_id:)
23
+ # post = Post.find(post_id)
24
+ # comment = post.comments.build(body: body, author: context[:current_user])
25
+ # if comment.save
26
+ # # Successful creation, return the created object with no errors
27
+ # {
28
+ # comment: comment,
29
+ # errors: [],
30
+ # }
31
+ # else
32
+ # # Failed save, return the errors to the client
33
+ # {
34
+ # comment: nil,
35
+ # errors: comment.errors.full_messages
36
+ # }
37
+ # end
38
+ # end
39
+ # end
40
+ #
41
+ # # Hook it up to your mutation:
42
+ # class Types::Mutation < GraphQL::Schema::Object
43
+ # field :create_comment, mutation: Mutations::CreateComment
44
+ # end
45
+ #
46
+ # # Call it from GraphQL:
47
+ # result = MySchema.execute <<-GRAPHQL
48
+ # mutation {
49
+ # createComment(postId: "1", body: "Nice Post!") {
50
+ # errors
51
+ # comment {
52
+ # body
53
+ # author {
54
+ # login
55
+ # }
56
+ # }
57
+ # }
58
+ # }
59
+ # GRAPHQL
60
+ #
61
+ class Mutation < GraphQL::Schema::Member
62
+ extend GraphQL::Schema::Member::HasFields
63
+ extend GraphQL::Schema::Member::HasArguments
64
+
65
+ # @param object [Object] the initialize object, pass to {Query.initialize} as `root_value`
66
+ # @param context [GraphQL::Query::Context]
67
+ def initialize(object:, context:, arguments:)
68
+ @object = object
69
+ @context = context
70
+ end
71
+
72
+ # @return [Object] the root value of the operation
73
+ attr_reader :object
74
+
75
+ # @return [GraphQL::Query::Context]
76
+ attr_reader :context
77
+
78
+ # Do the work. Everything happens here.
79
+ # @return [Hash] A key for each field in the return type
80
+ # @return [Object] An object corresponding to the return type
81
+ def resolve(**args)
82
+ raise NotImplementedError, "#{self.class.name}#resolve should execute side effects and return a Symbol-keyed hash"
83
+ end
84
+
85
+ class << self
86
+ # Override the method from HasFields to support `field: Mutation.field`, for backwards compat.
87
+ #
88
+ # If called without any arguments, returns a `GraphQL::Field`.
89
+ # @see {GraphQL::Schema::Member::HasFields.field} for default behavior
90
+ def field(*args, &block)
91
+ if args.none? && !block_given?
92
+ graphql_field.graphql_definition
93
+ else
94
+ super(*args, &block)
95
+ end
96
+ end
97
+
98
+ # Call this method to get the derived return type of the mutation,
99
+ # or use it as a configuration method to assign a return type
100
+ # instead of generating one.
101
+ # @param new_payload_type [Class, nil] If a type definition class is provided, it will be used as the return type of the mutation field
102
+ # @return [Class] The object type which this mutation returns.
103
+ def payload_type(new_payload_type = nil)
104
+ if new_payload_type
105
+ @payload_type = new_payload_type
106
+ end
107
+ @payload_type ||= generate_payload_type
108
+ end
109
+
110
+ # @return [GraphQL::Schema::Field] The generated field instance for this mutation
111
+ # @see {GraphQL::Schema::Field}'s `mutation:` option, don't call this directly
112
+ def graphql_field
113
+ @graphql_field ||= generate_field
114
+ end
115
+
116
+ # @param new_name [String, nil] if present, override the class name to set this value
117
+ # @return [String] The name of this mutation in the GraphQL schema (used for naming derived types and fields)
118
+ def graphql_name(new_name = nil)
119
+ if new_name
120
+ @graphql_name = new_name
121
+ end
122
+ @graphql_name ||= self.name.split("::").last
123
+ end
124
+
125
+ # An object class to use for deriving payload types
126
+ # @param new_class [Class, nil] Defaults to {GraphQL::Schema::Object}
127
+ # @return [Class]
128
+ def object_class(new_class = nil)
129
+ if new_class
130
+ @object_class = new_class
131
+ end
132
+ @object_class || (superclass.respond_to?(:object_class) ? superclass.object_class : GraphQL::Schema::Object)
133
+ end
134
+
135
+ # Additional info injected into {#resolve}
136
+ # @see {GraphQL::Schema::Field#extras}
137
+ def extras(new_extras = nil)
138
+ if new_extras
139
+ @extras = new_extras
140
+ end
141
+ @extras || []
142
+ end
143
+
144
+ private
145
+
146
+ # Build a subclass of {.object_class} based on `self`.
147
+ # This value will be cached as `{.payload_type}`.
148
+ # Override this hook to customize return type generation.
149
+ def generate_payload_type
150
+ mutation_name = graphql_name
151
+ mutation_fields = fields
152
+ mutation_class = self
153
+ Class.new(object_class) do
154
+ graphql_name("#{mutation_name}Payload")
155
+ description("Autogenerated return type of #{mutation_name}")
156
+ mutation(mutation_class)
157
+ mutation_fields.each do |name, f|
158
+ field(name, field: f)
159
+ end
160
+ end
161
+ end
162
+
163
+ # This name will be used for the {.graphql_field}.
164
+ def field_name
165
+ graphql_name.sub(/^[A-Z]/, &:downcase)
166
+ end
167
+
168
+ # Build an instance of {.field_class} which will be used to execute this mutation.
169
+ # To customize field generation, override this method.
170
+ def generate_field
171
+ # TODO support deprecation_reason
172
+ self.field_class.new(
173
+ field_name,
174
+ payload_type,
175
+ description,
176
+ resolve: self.method(:resolve_field),\
177
+ mutation_class: self,
178
+ arguments: arguments,
179
+ null: true,
180
+ )
181
+ end
182
+
183
+ # This is basically the `.call` behavior for the generated field,
184
+ # instantiating the Mutation class and calling its {#resolve} method
185
+ # with Ruby keyword arguments.
186
+ def resolve_field(obj, args, ctx)
187
+ mutation = self.new(object: obj, arguments: args, context: ctx.query.context)
188
+ ruby_kwargs = args.to_kwargs
189
+ extras.each { |e| ruby_kwargs[e] = ctx.public_send(e) }
190
+ mutation.resolve(**ruby_kwargs)
191
+ end
192
+ end
193
+ end
194
+ end
195
+ end
@@ -4,6 +4,7 @@ module GraphQL
4
4
  class Schema
5
5
  class Object < GraphQL::Schema::Member
6
6
  extend GraphQL::Schema::Member::AcceptsDefinition
7
+ extend GraphQL::Schema::Member::HasFields
7
8
 
8
9
  # @return [Object] the application object this type is wrapping
9
10
  attr_reader :object
@@ -16,9 +17,6 @@ module GraphQL
16
17
  @context = context
17
18
  end
18
19
 
19
- extend GraphQL::Schema::Member::HasFields
20
- field_class GraphQL::Schema::Field
21
-
22
20
  class << self
23
21
  def implements(*new_interfaces)
24
22
  new_interfaces.each do |int|
@@ -28,7 +26,7 @@ module GraphQL
28
26
  own_fields[name] = field
29
27
  end
30
28
  # And call the implemented hook
31
- int.apply_implemented(self)
29
+ int.implemented(self)
32
30
  else
33
31
  int.all_fields.each do |f|
34
32
  field(f.name, field: f)
@@ -53,13 +51,14 @@ module GraphQL
53
51
  obj_type.description = description
54
52
  obj_type.interfaces = interfaces
55
53
  obj_type.introspection = introspection
54
+ obj_type.mutation = mutation
56
55
 
57
56
  fields.each do |field_name, field_inst|
58
57
  field_defn = field_inst.to_graphql
59
58
  obj_type.fields[field_defn.name] = field_defn
60
59
  end
61
60
 
62
- obj_type.metadata[:object_class] = self
61
+ obj_type.metadata[:type_class] = self
63
62
 
64
63
  obj_type
65
64
  end