graphql 1.8.0.pre9 → 1.8.0.pre10

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