graphql 1.13.5 → 1.13.9

Sign up to get free protection for your applications and to get access to all the features.
Files changed (53) hide show
  1. checksums.yaml +4 -4
  2. data/lib/generators/graphql/core.rb +0 -7
  3. data/lib/generators/graphql/enum_generator.rb +4 -10
  4. data/lib/generators/graphql/field_extractor.rb +31 -0
  5. data/lib/generators/graphql/input_generator.rb +50 -0
  6. data/lib/generators/graphql/install/mutation_root_generator.rb +34 -0
  7. data/lib/generators/graphql/{templates → install/templates}/base_mutation.erb +0 -0
  8. data/lib/generators/graphql/{templates → install/templates}/mutation_type.erb +0 -0
  9. data/lib/generators/graphql/install_generator.rb +1 -1
  10. data/lib/generators/graphql/interface_generator.rb +7 -7
  11. data/lib/generators/graphql/mutation_create_generator.rb +22 -0
  12. data/lib/generators/graphql/mutation_delete_generator.rb +22 -0
  13. data/lib/generators/graphql/mutation_generator.rb +5 -30
  14. data/lib/generators/graphql/mutation_update_generator.rb +22 -0
  15. data/lib/generators/graphql/object_generator.rb +8 -37
  16. data/lib/generators/graphql/orm_mutations_base.rb +40 -0
  17. data/lib/generators/graphql/scalar_generator.rb +4 -2
  18. data/lib/generators/graphql/templates/enum.erb +5 -1
  19. data/lib/generators/graphql/templates/input.erb +9 -0
  20. data/lib/generators/graphql/templates/interface.erb +4 -2
  21. data/lib/generators/graphql/templates/mutation.erb +1 -1
  22. data/lib/generators/graphql/templates/mutation_create.erb +20 -0
  23. data/lib/generators/graphql/templates/mutation_delete.erb +20 -0
  24. data/lib/generators/graphql/templates/mutation_update.erb +21 -0
  25. data/lib/generators/graphql/templates/object.erb +4 -2
  26. data/lib/generators/graphql/templates/scalar.erb +3 -1
  27. data/lib/generators/graphql/templates/union.erb +4 -2
  28. data/lib/generators/graphql/type_generator.rb +46 -9
  29. data/lib/generators/graphql/union_generator.rb +5 -5
  30. data/lib/graphql/analysis/ast/visitor.rb +2 -1
  31. data/lib/graphql/dataloader/source.rb +2 -2
  32. data/lib/graphql/date_encoding_error.rb +16 -0
  33. data/lib/graphql/execution/interpreter/runtime.rb +29 -14
  34. data/lib/graphql/introspection/directive_location_enum.rb +2 -2
  35. data/lib/graphql/introspection/directive_type.rb +2 -0
  36. data/lib/graphql/introspection/schema_type.rb +5 -0
  37. data/lib/graphql/introspection/type_type.rb +9 -3
  38. data/lib/graphql/introspection.rb +4 -1
  39. data/lib/graphql/pagination/relation_connection.rb +4 -4
  40. data/lib/graphql/rubocop/graphql/default_required_true.rb +4 -4
  41. data/lib/graphql/schema/argument.rb +18 -1
  42. data/lib/graphql/schema/directive.rb +11 -0
  43. data/lib/graphql/schema/field.rb +21 -12
  44. data/lib/graphql/schema/input_object.rb +1 -1
  45. data/lib/graphql/schema/loader.rb +3 -0
  46. data/lib/graphql/schema/scalar.rb +12 -0
  47. data/lib/graphql/schema.rb +12 -5
  48. data/lib/graphql/types/iso_8601_date.rb +13 -5
  49. data/lib/graphql/types/iso_8601_date_time.rb +8 -1
  50. data/lib/graphql/types/relay/connection_behaviors.rb +2 -1
  51. data/lib/graphql/version.rb +1 -1
  52. data/lib/graphql.rb +12 -0
  53. metadata +20 -8
@@ -8,13 +8,28 @@ require_relative 'core'
8
8
 
9
9
  module Graphql
10
10
  module Generators
11
- class TypeGeneratorBase < Rails::Generators::Base
11
+ class TypeGeneratorBase < Rails::Generators::NamedBase
12
12
  include Core
13
13
 
14
- argument :type_name,
15
- type: :string,
16
- banner: "TypeName",
17
- desc: "Name of this object type (expressed as Ruby or GraphQL)"
14
+ class_option 'namespaced_types',
15
+ type: :boolean,
16
+ required: false,
17
+ default: false,
18
+ banner: "Namespaced",
19
+ desc: "If the generated types will be namespaced"
20
+
21
+ argument :custom_fields,
22
+ type: :array,
23
+ default: [],
24
+ banner: "name:type name:type ...",
25
+ desc: "Fields for this object (type may be expressed as Ruby or GraphQL)"
26
+
27
+
28
+ attr_accessor :graphql_type
29
+
30
+ def create_type_file
31
+ template "#{graphql_type}.erb", "#{options[:directory]}/types#{subdirectory}/#{type_file_name}.rb"
32
+ end
18
33
 
19
34
  # Take a type expression in any combination of GraphQL or Ruby styles
20
35
  # and return it in a specified output style
@@ -60,12 +75,12 @@ module Graphql
60
75
 
61
76
  # @return [String] The user-provided type name, normalized to Ruby code
62
77
  def type_ruby_name
63
- @type_ruby_name ||= self.class.normalize_type_expression(type_name, mode: :ruby)[0]
78
+ @type_ruby_name ||= self.class.normalize_type_expression(name, mode: :ruby)[0]
64
79
  end
65
80
 
66
81
  # @return [String] The user-provided type name, as a GraphQL name
67
82
  def type_graphql_name
68
- @type_graphql_name ||= self.class.normalize_type_expression(type_name, mode: :graphql)[0]
83
+ @type_graphql_name ||= self.class.normalize_type_expression(name, mode: :graphql)[0]
69
84
  end
70
85
 
71
86
  # @return [String] The user-provided type name, as a file name (without extension)
@@ -82,6 +97,24 @@ module Graphql
82
97
  }
83
98
  end
84
99
 
100
+ def ruby_class_name
101
+ class_prefix =
102
+ if options[:namespaced_types]
103
+ "#{graphql_type.pluralize.camelize}::"
104
+ else
105
+ ""
106
+ end
107
+ @ruby_class_name || class_prefix + type_ruby_name.sub(/^Types::/, "")
108
+ end
109
+
110
+ def subdirectory
111
+ if options[:namespaced_types]
112
+ "/#{graphql_type.pluralize}"
113
+ else
114
+ ""
115
+ end
116
+ end
117
+
85
118
  class NormalizedField
86
119
  def initialize(name, type_expr, null)
87
120
  @name = name
@@ -89,8 +122,12 @@ module Graphql
89
122
  @null = null
90
123
  end
91
124
 
92
- def to_ruby
93
- "field :#{@name}, #{@type_expr}, null: #{@null}"
125
+ def to_object_field
126
+ "field :#{@name}, #{@type_expr}#{@null ? '' : ', null: false'}"
127
+ end
128
+
129
+ def to_input_argument
130
+ "argument :#{@name}, #{@type_expr}, required: false"
94
131
  end
95
132
  end
96
133
  end
@@ -19,14 +19,14 @@ module Graphql
19
19
  banner: "type type ...",
20
20
  desc: "Possible types for this union (expressed as Ruby or GraphQL)"
21
21
 
22
- def create_type_file
23
- template "union.erb", "#{options[:directory]}/types/#{type_file_name}.rb"
24
- end
25
-
26
22
  private
27
23
 
24
+ def graphql_type
25
+ "union"
26
+ end
27
+
28
28
  def normalized_possible_types
29
- possible_types.map { |t| self.class.normalize_type_expression(t, mode: :ruby)[0] }
29
+ custom_fields.map { |t| self.class.normalize_type_expression(t, mode: :ruby)[0] }
30
30
  end
31
31
  end
32
32
  end
@@ -100,7 +100,8 @@ module GraphQL
100
100
  def on_field(node, parent)
101
101
  @response_path.push(node.alias || node.name)
102
102
  parent_type = @object_types.last
103
- field_definition = @schema.get_field(parent_type, node.name, @query.context)
103
+ # This could be nil if the previous field wasn't found:
104
+ field_definition = parent_type && @schema.get_field(parent_type, node.name, @query.context)
104
105
  @field_definitions.push(field_definition)
105
106
  if !field_definition.nil?
106
107
  next_object_type = field_definition.type.unwrap
@@ -138,8 +138,8 @@ module GraphQL
138
138
  # @api private
139
139
  def result_for(key)
140
140
  if !@results.key?(key)
141
- raise <<-ERR
142
- Invariant: fetching result for a key on #{self.class} that hasn't been loaded yet (#{key.inspect}, loaded: #{@results.keys})
141
+ raise GraphQL::InvariantError, <<-ERR
142
+ Fetching result for a key on #{self.class} that hasn't been loaded yet (#{key.inspect}, loaded: #{@results.keys})
143
143
 
144
144
  This key should have been loaded already. This is a bug in GraphQL::Dataloader, please report it on GitHub: https://github.com/rmosolgo/graphql-ruby/issues/new.
145
145
  ERR
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+ module GraphQL
3
+ # This error is raised when `Types::ISO8601Date` is asked to return a value
4
+ # that cannot be parsed to a Ruby Date.
5
+ #
6
+ # @see GraphQL::Types::ISO8601Date which raises this error
7
+ class DateEncodingError < GraphQL::RuntimeTypeError
8
+ # The value which couldn't be encoded
9
+ attr_reader :date_value
10
+
11
+ def initialize(value)
12
+ @date_value = value
13
+ super("Date cannot be parsed: #{value}. \nDate must be be able to be parsed as a Ruby Date object.")
14
+ end
15
+ end
16
+ end
@@ -207,7 +207,7 @@ module GraphQL
207
207
  # Root .authorized? returned false.
208
208
  @response = nil
209
209
  else
210
- resolve_with_directives(object_proxy, root_operation.directives) do # execute query level directives
210
+ call_method_on_directives(:resolve, object_proxy, root_operation.directives) do # execute query level directives
211
211
  gathered_selections = gather_selections(object_proxy, root_type, root_operation.selections)
212
212
  # This is kind of a hack -- `gathered_selections` is an Array if any of the selections
213
213
  # require isolation during execution (because of runtime directives). In that case,
@@ -227,7 +227,7 @@ module GraphQL
227
227
 
228
228
  @dataloader.append_job {
229
229
  set_all_interpreter_context(query.root_value, nil, nil, path)
230
- resolve_with_directives(object_proxy, selections.graphql_directives) do
230
+ call_method_on_directives(:resolve, object_proxy, selections.graphql_directives) do
231
231
  evaluate_selections(
232
232
  path,
233
233
  context.scoped_context,
@@ -503,7 +503,7 @@ module GraphQL
503
503
  }
504
504
  end
505
505
 
506
- field_result = resolve_with_directives(object, directives) do
506
+ field_result = call_method_on_directives(:resolve, object, directives) do
507
507
  # Actually call the field resolver and capture the result
508
508
  app_result = begin
509
509
  query.with_error_handling do
@@ -735,7 +735,7 @@ module GraphQL
735
735
  final_result = nil
736
736
  end
737
737
  set_all_interpreter_context(continue_value, nil, nil, path) # reset this mutable state
738
- resolve_with_directives(continue_value, selections.graphql_directives) do
738
+ call_method_on_directives(:resolve, continue_value, selections.graphql_directives) do
739
739
  evaluate_selections(
740
740
  path,
741
741
  context.scoped_context,
@@ -754,6 +754,8 @@ module GraphQL
754
754
  end
755
755
  when "LIST"
756
756
  inner_type = current_type.of_type
757
+ # This is true for objects, unions, and interfaces
758
+ use_dataloader_job = !inner_type.unwrap.kind.input?
757
759
  response_list = GraphQLResultArray.new(result_name, selection_result)
758
760
  response_list.graphql_non_null_list_items = inner_type.non_null?
759
761
  set_result(selection_result, result_name, response_list)
@@ -768,12 +770,12 @@ module GraphQL
768
770
  this_idx = idx
769
771
  next_path.freeze
770
772
  idx += 1
771
- # This will update `response_list` with the lazy
772
- after_lazy(inner_value, owner: inner_type, path: next_path, ast_node: ast_node, scoped_context: scoped_context, field: field, owner_object: owner_object, arguments: arguments, result_name: this_idx, result: response_list) do |inner_inner_value|
773
- continue_value = continue_value(next_path, inner_inner_value, owner_type, field, inner_type.non_null?, ast_node, this_idx, response_list)
774
- if HALT != continue_value
775
- continue_field(next_path, continue_value, owner_type, field, inner_type, ast_node, next_selections, false, owner_object, arguments, this_idx, response_list)
773
+ if use_dataloader_job
774
+ @dataloader.append_job do
775
+ resolve_list_item(inner_value, inner_type, next_path, ast_node, scoped_context, field, owner_object, arguments, this_idx, response_list, next_selections, owner_type)
776
776
  end
777
+ else
778
+ resolve_list_item(inner_value, inner_type, next_path, ast_node, scoped_context, field, owner_object, arguments, this_idx, response_list, next_selections, owner_type)
777
779
  end
778
780
  end
779
781
  rescue NoMethodError => err
@@ -793,12 +795,25 @@ module GraphQL
793
795
  end
794
796
  end
795
797
 
796
- def resolve_with_directives(object, directives, &block)
798
+ def resolve_list_item(inner_value, inner_type, next_path, ast_node, scoped_context, field, owner_object, arguments, this_idx, response_list, next_selections, owner_type) # rubocop:disable Metrics/ParameterLists
799
+ set_all_interpreter_context(nil, nil, nil, next_path)
800
+ call_method_on_directives(:resolve_each, owner_object, ast_node.directives) do
801
+ # This will update `response_list` with the lazy
802
+ after_lazy(inner_value, owner: inner_type, path: next_path, ast_node: ast_node, scoped_context: scoped_context, field: field, owner_object: owner_object, arguments: arguments, result_name: this_idx, result: response_list) do |inner_inner_value|
803
+ continue_value = continue_value(next_path, inner_inner_value, owner_type, field, inner_type.non_null?, ast_node, this_idx, response_list)
804
+ if HALT != continue_value
805
+ continue_field(next_path, continue_value, owner_type, field, inner_type, ast_node, next_selections, false, owner_object, arguments, this_idx, response_list)
806
+ end
807
+ end
808
+ end
809
+ end
810
+
811
+ def call_method_on_directives(method_name, object, directives, &block)
797
812
  return yield if directives.nil? || directives.empty?
798
- run_directive(object, directives, 0, &block)
813
+ run_directive(method_name, object, directives, 0, &block)
799
814
  end
800
815
 
801
- def run_directive(object, directives, idx, &block)
816
+ def run_directive(method_name, object, directives, idx, &block)
802
817
  dir_node = directives[idx]
803
818
  if !dir_node
804
819
  yield
@@ -822,8 +837,8 @@ module GraphQL
822
837
  if dir_args == HALT
823
838
  nil
824
839
  else
825
- dir_defn.resolve(object, dir_args, context) do
826
- run_directive(object, directives, idx + 1, &block)
840
+ dir_defn.public_send(method_name, object, dir_args, context) do
841
+ run_directive(method_name, object, directives, idx + 1, &block)
827
842
  end
828
843
  end
829
844
  end
@@ -6,8 +6,8 @@ module GraphQL
6
6
  description "A Directive can be adjacent to many parts of the GraphQL language, "\
7
7
  "a __DirectiveLocation describes one such possible adjacencies."
8
8
 
9
- GraphQL::Directive::LOCATIONS.each do |location|
10
- value(location.to_s, GraphQL::Directive::LOCATION_DESCRIPTIONS[location], value: location)
9
+ GraphQL::Schema::Directive::LOCATIONS.each do |location|
10
+ value(location.to_s, GraphQL::Schema::Directive::LOCATION_DESCRIPTIONS[location], value: location)
11
11
  end
12
12
  introspection true
13
13
  end
@@ -19,6 +19,8 @@ module GraphQL
19
19
  field :on_fragment, Boolean, null: false, deprecation_reason: "Use `locations`.", method: :on_fragment?
20
20
  field :on_field, Boolean, null: false, deprecation_reason: "Use `locations`.", method: :on_field?
21
21
 
22
+ field :is_repeatable, Boolean, method: :repeatable?
23
+
22
24
  def args(include_deprecated:)
23
25
  args = @context.warden.arguments(@object)
24
26
  args = args.reject(&:deprecation_reason) unless include_deprecated
@@ -13,6 +13,11 @@ module GraphQL
13
13
  field :mutation_type, GraphQL::Schema::LateBoundType.new("__Type"), "If this server supports mutation, the type that mutation operations will be rooted at."
14
14
  field :subscription_type, GraphQL::Schema::LateBoundType.new("__Type"), "If this server support subscription, the type that subscription operations will be rooted at."
15
15
  field :directives, [GraphQL::Schema::LateBoundType.new("__Directive")], "A list of all directives supported by this server.", null: false
16
+ field :description, String, resolver_method: :schema_description
17
+
18
+ def schema_description
19
+ context.schema.description
20
+ end
16
21
 
17
22
  def types
18
23
  @context.warden.reachable_types.sort_by(&:graphql_name)
@@ -12,7 +12,7 @@ module GraphQL
12
12
  "possible at runtime. List and NonNull types compose other types."
13
13
 
14
14
  field :kind, GraphQL::Schema::LateBoundType.new("__TypeKind"), null: false
15
- field :name, String
15
+ field :name, String, method: :graphql_name
16
16
  field :description, String
17
17
  field :fields, [GraphQL::Schema::LateBoundType.new("__Field")] do
18
18
  argument :include_deprecated, Boolean, required: false, default_value: false
@@ -27,8 +27,14 @@ module GraphQL
27
27
  end
28
28
  field :of_type, GraphQL::Schema::LateBoundType.new("__Type")
29
29
 
30
- def name
31
- object.graphql_name
30
+ field :specified_by_url, String
31
+
32
+ def specified_by_url
33
+ if object.kind.scalar?
34
+ object.specified_by_url
35
+ else
36
+ nil
37
+ end
32
38
  end
33
39
 
34
40
  def kind
@@ -1,12 +1,13 @@
1
1
  # frozen_string_literal: true
2
2
  module GraphQL
3
3
  module Introspection
4
- def self.query(include_deprecated_args: false)
4
+ def self.query(include_deprecated_args: false, include_schema_description: false, include_is_repeatable: false, include_specified_by_url: false)
5
5
  # The introspection query to end all introspection queries, copied from
6
6
  # https://github.com/graphql/graphql-js/blob/master/src/utilities/introspectionQuery.js
7
7
  <<-QUERY
8
8
  query IntrospectionQuery {
9
9
  __schema {
10
+ #{include_schema_description ? "description" : ""}
10
11
  queryType { name }
11
12
  mutationType { name }
12
13
  subscriptionType { name }
@@ -17,6 +18,7 @@ query IntrospectionQuery {
17
18
  name
18
19
  description
19
20
  locations
21
+ #{include_is_repeatable ? "isRepeatable" : ""}
20
22
  args#{include_deprecated_args ? '(includeDeprecated: true)' : ''} {
21
23
  ...InputValue
22
24
  }
@@ -27,6 +29,7 @@ fragment FullType on __Type {
27
29
  kind
28
30
  name
29
31
  description
32
+ #{include_specified_by_url ? "specifiedByUrl" : ""}
30
33
  fields(includeDeprecated: true) {
31
34
  name
32
35
  description
@@ -47,7 +47,7 @@ module GraphQL
47
47
  def cursor_for(item)
48
48
  load_nodes
49
49
  # index in nodes + existing offset + 1 (because it's offset, not index)
50
- offset = nodes.index(item) + 1 + (@paged_nodes_offset || 0) + (relation_offset(items) || 0)
50
+ offset = nodes.index(item) + 1 + (@paged_nodes_offset || 0) - (relation_offset(items) || 0)
51
51
  encode(offset.to_s)
52
52
  end
53
53
 
@@ -116,9 +116,9 @@ module GraphQL
116
116
  if defined?(@sliced_nodes_limit)
117
117
  return
118
118
  else
119
+ next_offset = relation_offset(items) || 0
119
120
  if after_offset
120
- previous_offset = relation_offset(items) || 0
121
- relation_offset = previous_offset + after_offset
121
+ next_offset += after_offset
122
122
  end
123
123
 
124
124
  if before_offset && after_offset
@@ -136,7 +136,7 @@ module GraphQL
136
136
  end
137
137
 
138
138
  @sliced_nodes_limit = relation_limit
139
- @sliced_nodes_offset = relation_offset || 0
139
+ @sliced_nodes_offset = next_offset
140
140
  end
141
141
  end
142
142
 
@@ -25,16 +25,16 @@ module GraphQL
25
25
 
26
26
  def_node_matcher :argument_config_with_required_true?, <<-Pattern
27
27
  (
28
- send nil? :argument ... (hash <$(pair (sym :required) (true)) ...>)
28
+ send {nil? _} :argument ... (hash <$(pair (sym :required) (true)) ...>)
29
29
  )
30
30
  Pattern
31
31
 
32
32
  def on_send(node)
33
33
  argument_config_with_required_true?(node) do |required_config|
34
34
  add_offense(required_config) do |corrector|
35
- cleaned_node_source = source_without_keyword_argument(node, required_config)
36
- corrector.replace(node, cleaned_node_source)
37
- end
35
+ cleaned_node_source = source_without_keyword_argument(node, required_config)
36
+ corrector.replace(node, cleaned_node_source)
37
+ end
38
38
  end
39
39
  end
40
40
  end
@@ -48,13 +48,21 @@ module GraphQL
48
48
  # @param directives [Hash{Class => Hash}]
49
49
  # @param deprecation_reason [String]
50
50
  # @param validates [Hash, nil] Options for building validators, if any should be applied
51
- def initialize(arg_name = nil, type_expr = nil, desc = nil, required: true, type: nil, name: nil, loads: nil, description: nil, ast_node: nil, default_value: NO_DEFAULT, as: nil, from_resolver: false, camelize: true, prepare: nil, method_access: true, owner:, validates: nil, directives: nil, deprecation_reason: nil, &definition_block)
51
+ # @param replace_null_with_default [Boolean] if `true`, incoming values of `null` will be replaced with the configured `default_value`
52
+ def initialize(arg_name = nil, type_expr = nil, desc = nil, required: true, type: nil, name: nil, loads: nil, description: nil, ast_node: nil, default_value: NO_DEFAULT, as: nil, from_resolver: false, camelize: true, prepare: nil, method_access: true, owner:, validates: nil, directives: nil, deprecation_reason: nil, replace_null_with_default: false, &definition_block)
52
53
  arg_name ||= name
53
54
  @name = -(camelize ? Member::BuildType.camelize(arg_name.to_s) : arg_name.to_s)
54
55
  @type_expr = type_expr || type
55
56
  @description = desc || description
56
57
  @null = required != true
57
58
  @default_value = default_value
59
+ if replace_null_with_default
60
+ if !default_value?
61
+ raise ArgumentError, "`replace_null_with_default: true` requires a default value, please provide one with `default_value: ...`"
62
+ end
63
+ @replace_null_with_default = true
64
+ end
65
+
58
66
  @owner = owner
59
67
  @as = as
60
68
  @loads = loads
@@ -97,6 +105,10 @@ module GraphQL
97
105
  @default_value != NO_DEFAULT
98
106
  end
99
107
 
108
+ def replace_null_with_default?
109
+ @replace_null_with_default
110
+ end
111
+
100
112
  attr_writer :description
101
113
 
102
114
  # @return [String] Documentation for this argument
@@ -253,6 +265,11 @@ module GraphQL
253
265
  return
254
266
  end
255
267
 
268
+ if value.nil? && replace_null_with_default?
269
+ value = default_value
270
+ default_used = true
271
+ end
272
+
256
273
  loaded_value = nil
257
274
  coerced_value = context.schema.error_handler.with_error_handling(context) do
258
275
  type.coerce_input(value, context)
@@ -90,6 +90,11 @@ module GraphQL
90
90
  yield
91
91
  end
92
92
 
93
+ # Continuing is passed as a block, yield to continue.
94
+ def resolve_each(object, arguments, context)
95
+ yield
96
+ end
97
+
93
98
  def on_field?
94
99
  locations.include?(FIELD)
95
100
  end
@@ -128,6 +133,10 @@ module GraphQL
128
133
  @arguments = self.class.coerce_arguments(nil, arguments, Query::NullContext)
129
134
  end
130
135
 
136
+ def graphql_name
137
+ self.class.graphql_name
138
+ end
139
+
131
140
  LOCATIONS = [
132
141
  QUERY = :QUERY,
133
142
  MUTATION = :MUTATION,
@@ -147,6 +156,7 @@ module GraphQL
147
156
  ENUM_VALUE = :ENUM_VALUE,
148
157
  INPUT_OBJECT = :INPUT_OBJECT,
149
158
  INPUT_FIELD_DEFINITION = :INPUT_FIELD_DEFINITION,
159
+ VARIABLE_DEFINITION = :VARIABLE_DEFINITION,
150
160
  ]
151
161
 
152
162
  DEFAULT_DEPRECATION_REASON = 'No longer supported'
@@ -169,6 +179,7 @@ module GraphQL
169
179
  ENUM_VALUE: 'Location adjacent to an enum value definition.',
170
180
  INPUT_OBJECT: 'Location adjacent to an input object type definition.',
171
181
  INPUT_FIELD_DEFINITION: 'Location adjacent to an input object field definition.',
182
+ VARIABLE_DEFINITION: 'Location adjacent to a variable definition.',
172
183
  }
173
184
 
174
185
  private
@@ -38,7 +38,9 @@ module GraphQL
38
38
 
39
39
  # @return [Class] The GraphQL type this field belongs to. (For fields defined on mutations, it's the payload type)
40
40
  def owner_type
41
- @owner_type ||= if owner < GraphQL::Schema::Mutation
41
+ @owner_type ||= if owner.nil?
42
+ raise GraphQL::InvariantError, "Field #{original_name.inspect} (graphql name: #{graphql_name.inspect}) has no owner, but all fields should have an owner. How did this happen?!"
43
+ elsif owner < GraphQL::Schema::Mutation
42
44
  owner.payload_type
43
45
  else
44
46
  owner
@@ -188,6 +190,7 @@ module GraphQL
188
190
  # @param deprecation_reason [String] If present, the field is marked "deprecated" with this message
189
191
  # @param method [Symbol] The method to call on the underlying object to resolve this field (defaults to `name`)
190
192
  # @param hash_key [String, Symbol] The hash key to lookup on the underlying object (if its a Hash) to resolve this field (defaults to `name` or `name.to_s`)
193
+ # @param dig [Array<String, Symbol>] The nested hash keys to lookup on the underlying hash to resolve this field using dig
191
194
  # @param resolver_method [Symbol] The method on the type to call to resolve this field (defaults to `name`)
192
195
  # @param connection [Boolean] `true` if this field should get automagic connection behavior; default is to infer by `*Connection` in the return type name
193
196
  # @param connection_extension [Class] The extension to add, to implement connections. If `nil`, no extension is added.
@@ -210,7 +213,7 @@ module GraphQL
210
213
  # @param method_conflict_warning [Boolean] If false, skip the warning if this field's method conflicts with a built-in method
211
214
  # @param validates [Array<Hash>] Configurations for validating this field
212
215
  # @param legacy_edge_class [Class, nil] (DEPRECATED) If present, pass this along to the legacy field definition
213
- def initialize(type: nil, name: nil, owner: nil, null: true, field: nil, function: nil, description: nil, deprecation_reason: nil, method: nil, hash_key: nil, resolver_method: nil, resolve: nil, connection: nil, max_page_size: :not_given, scope: nil, introspection: false, camelize: true, trace: nil, complexity: 1, ast_node: nil, extras: EMPTY_ARRAY, extensions: EMPTY_ARRAY, connection_extension: self.class.connection_extension, resolver_class: nil, subscription_scope: nil, relay_node_field: false, relay_nodes_field: false, method_conflict_warning: true, broadcastable: nil, arguments: EMPTY_HASH, directives: EMPTY_HASH, validates: EMPTY_ARRAY, legacy_edge_class: nil, &definition_block)
216
+ def initialize(type: nil, name: nil, owner: nil, null: true, field: nil, function: nil, description: nil, deprecation_reason: nil, method: nil, hash_key: nil, dig: nil, resolver_method: nil, resolve: nil, connection: nil, max_page_size: :not_given, scope: nil, introspection: false, camelize: true, trace: nil, complexity: 1, ast_node: nil, extras: EMPTY_ARRAY, extensions: EMPTY_ARRAY, connection_extension: self.class.connection_extension, resolver_class: nil, subscription_scope: nil, relay_node_field: false, relay_nodes_field: false, method_conflict_warning: true, broadcastable: nil, arguments: EMPTY_HASH, directives: EMPTY_HASH, validates: EMPTY_ARRAY, legacy_edge_class: nil, &definition_block)
214
217
  if name.nil?
215
218
  raise ArgumentError, "missing first `name` argument or keyword `name:`"
216
219
  end
@@ -236,8 +239,8 @@ module GraphQL
236
239
  @resolve = resolve
237
240
  self.deprecation_reason = deprecation_reason
238
241
 
239
- if method && hash_key
240
- raise ArgumentError, "Provide `method:` _or_ `hash_key:`, not both. (called with: `method: #{method.inspect}, hash_key: #{hash_key.inspect}`)"
242
+ if method && hash_key && dig
243
+ raise ArgumentError, "Provide `method:`, `hash_key:` _or_ `dig:`, not multiple. (called with: `method: #{method.inspect}, hash_key: #{hash_key.inspect}, dig: #{dig.inspect}`)"
241
244
  end
242
245
 
243
246
  if resolver_method
@@ -245,13 +248,14 @@ module GraphQL
245
248
  raise ArgumentError, "Provide `method:` _or_ `resolver_method:`, not both. (called with: `method: #{method.inspect}, resolver_method: #{resolver_method.inspect}`)"
246
249
  end
247
250
 
248
- if hash_key
249
- raise ArgumentError, "Provide `hash_key:` _or_ `resolver_method:`, not both. (called with: `hash_key: #{hash_key.inspect}, resolver_method: #{resolver_method.inspect}`)"
251
+ if hash_key || dig
252
+ raise ArgumentError, "Provide `hash_key:`, `dig:`, _or_ `resolver_method:`, not multiple. (called with: `hash_key: #{hash_key.inspect}, dig: #{dig.inspect}, resolver_method: #{resolver_method.inspect}`)"
250
253
  end
251
254
  end
252
255
 
253
256
  # TODO: I think non-string/symbol hash keys are wrongly normalized (eg `1` will not work)
254
257
  method_name = method || hash_key || name_s
258
+ @dig_keys = dig
255
259
  resolver_method ||= name_s.to_sym
256
260
 
257
261
  @method_str = -method_name.to_s
@@ -424,11 +428,14 @@ module GraphQL
424
428
  elsif connection?
425
429
  arguments = query.arguments_for(nodes.first, self)
426
430
  max_possible_page_size = nil
427
- if arguments[:first]
428
- max_possible_page_size = arguments[:first]
429
- end
430
- if arguments[:last] && (max_possible_page_size.nil? || arguments[:last] > max_possible_page_size)
431
- max_possible_page_size = arguments[:last]
431
+ if arguments.respond_to?(:[]) # It might have been an error
432
+ if arguments[:first]
433
+ max_possible_page_size = arguments[:first]
434
+ end
435
+
436
+ if arguments[:last] && (max_possible_page_size.nil? || arguments[:last] > max_possible_page_size)
437
+ max_possible_page_size = arguments[:last]
438
+ end
432
439
  end
433
440
 
434
441
  if max_possible_page_size.nil?
@@ -822,7 +829,9 @@ module GraphQL
822
829
  end
823
830
  elsif obj.object.is_a?(Hash)
824
831
  inner_object = obj.object
825
- if inner_object.key?(@method_sym)
832
+ if @dig_keys
833
+ inner_object.dig(*@dig_keys)
834
+ elsif inner_object.key?(@method_sym)
826
835
  inner_object[@method_sym]
827
836
  else
828
837
  inner_object[@method_str]
@@ -81,7 +81,7 @@ module GraphQL
81
81
 
82
82
  def self.authorized?(obj, value, ctx)
83
83
  # Authorize each argument (but this doesn't apply if `prepare` is implemented):
84
- if value.is_a?(InputObject)
84
+ if value.respond_to?(:key?)
85
85
  arguments(ctx).each do |_name, input_obj_arg|
86
86
  input_obj_arg = input_obj_arg.type_class
87
87
  if value.key?(input_obj_arg.keyword) &&
@@ -34,6 +34,7 @@ module GraphQL
34
34
  Class.new(GraphQL::Schema) do
35
35
  orphan_types(types.values)
36
36
  directives(directives)
37
+ description(schema["description"])
37
38
 
38
39
  def self.resolve_type(*)
39
40
  raise(GraphQL::RequiredImplementationMissingError, "This schema was loaded from string, so it can't resolve types for objects")
@@ -141,6 +142,7 @@ module GraphQL
141
142
  Class.new(GraphQL::Schema::Scalar) do
142
143
  graphql_name(type["name"])
143
144
  description(type["description"])
145
+ specified_by_url(type["specifiedByUrl"])
144
146
  end
145
147
  end
146
148
  when "UNION"
@@ -160,6 +162,7 @@ module GraphQL
160
162
  graphql_name(directive["name"])
161
163
  description(directive["description"])
162
164
  locations(*directive["locations"].map(&:to_sym))
165
+ repeatable(directive["isRepeatable"])
163
166
  loader.build_arguments(self, directive["args"], type_resolver)
164
167
  end
165
168
  end
@@ -32,6 +32,18 @@ module GraphQL
32
32
  GraphQL::TypeKinds::SCALAR
33
33
  end
34
34
 
35
+ def specified_by_url(new_url = nil)
36
+ if new_url
37
+ @specified_by_url = new_url
38
+ elsif defined?(@specified_by_url)
39
+ @specified_by_url
40
+ elsif superclass.respond_to?(:specified_by_url)
41
+ superclass.specified_by_url
42
+ else
43
+ nil
44
+ end
45
+ end
46
+
35
47
  def default_scalar(is_default = nil)
36
48
  if !is_default.nil?
37
49
  @default_scalar = is_default
@@ -892,6 +892,17 @@ module GraphQL
892
892
  GraphQL::Language::DocumentFromSchemaDefinition.new(self).document
893
893
  end
894
894
 
895
+ # @return [String, nil]
896
+ def description(new_description = nil)
897
+ if new_description
898
+ @description = new_description
899
+ elsif defined?(@description)
900
+ @description
901
+ else
902
+ find_inherited_value(:description, nil)
903
+ end
904
+ end
905
+
895
906
  def find(path)
896
907
  if !@finder
897
908
  @find_cache = {}
@@ -1247,11 +1258,7 @@ module GraphQL
1247
1258
  when Module
1248
1259
  type_or_name
1249
1260
  else
1250
- raise ArgumentError, <<-ERR
1251
- Invariant: unexpected field owner for #{field_name.inspect}: #{type_or_name.inspect} (#{type_or_name.class})
1252
-
1253
- This is probably a bug in GraphQL-Ruby, please report this error on GitHub: https://github.com/rmosolgo/graphql-ruby/issues/new?template=bug_report.md
1254
- ERR
1261
+ raise GraphQL::InvariantError, "Unexpected field owner for #{field_name.inspect}: #{type_or_name.inspect} (#{type_or_name.class})"
1255
1262
  end
1256
1263
 
1257
1264
  if parent_type.kind.fields? && (field = parent_type.get_field(field_name, context))