graphql_rails 2.2.0 → 2.4.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.
Files changed (50) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ruby.yml +3 -2
  3. data/.ruby-version +1 -1
  4. data/CHANGELOG.md +16 -0
  5. data/Gemfile.lock +17 -17
  6. data/docs/README.md +23 -45
  7. data/docs/_sidebar.md +0 -1
  8. data/docs/components/controller.md +15 -1
  9. data/docs/components/decorator.md +1 -1
  10. data/docs/components/model.md +100 -5
  11. data/docs/components/routes.md +61 -15
  12. data/graphql_rails.gemspec +1 -1
  13. data/lib/generators/graphql_rails/templates/graphql_controller.erb +1 -1
  14. data/lib/graphql_rails/attributes/attribute.rb +16 -14
  15. data/lib/graphql_rails/attributes/attribute_configurable.rb +24 -0
  16. data/lib/graphql_rails/attributes/attribute_name_parser.rb +4 -4
  17. data/lib/graphql_rails/attributes/input_attribute.rb +19 -2
  18. data/lib/graphql_rails/attributes/type_parseable.rb +4 -5
  19. data/lib/graphql_rails/controller/action_configuration.rb +1 -1
  20. data/lib/graphql_rails/controller/build_controller_action_resolver.rb +2 -0
  21. data/lib/graphql_rails/controller/configuration.rb +1 -1
  22. data/lib/graphql_rails/controller/request/format_errors.rb +1 -1
  23. data/lib/graphql_rails/controller/request.rb +3 -2
  24. data/lib/graphql_rails/controller.rb +1 -1
  25. data/lib/graphql_rails/decorator/relation_decorator.rb +24 -20
  26. data/lib/graphql_rails/decorator.rb +12 -4
  27. data/lib/graphql_rails/errors/custom_execution_error.rb +1 -1
  28. data/lib/graphql_rails/errors/execution_error.rb +1 -1
  29. data/lib/graphql_rails/errors/system_error.rb +11 -1
  30. data/lib/graphql_rails/errors/validation_error.rb +14 -1
  31. data/lib/graphql_rails/model/find_or_build_graphql_input_type.rb +28 -0
  32. data/lib/graphql_rails/model/find_or_build_graphql_type.rb +14 -5
  33. data/lib/graphql_rails/model/find_or_build_graphql_type_class.rb +4 -3
  34. data/lib/graphql_rails/model/input.rb +3 -3
  35. data/lib/graphql_rails/router/build_schema_action_type.rb +112 -0
  36. data/lib/graphql_rails/router/event_route.rb +52 -0
  37. data/lib/graphql_rails/router/mutation_route.rb +1 -1
  38. data/lib/graphql_rails/router/query_route.rb +1 -1
  39. data/lib/graphql_rails/router/resource_routes_builder.rb +0 -8
  40. data/lib/graphql_rails/router/route.rb +4 -3
  41. data/lib/graphql_rails/router/schema_builder.rb +18 -26
  42. data/lib/graphql_rails/router.rb +32 -16
  43. data/lib/graphql_rails/rspec_controller_helpers.rb +3 -1
  44. data/lib/graphql_rails/types/hidable_by_group.rb +23 -3
  45. data/lib/graphql_rails/types/input_object_type.rb +16 -0
  46. data/lib/graphql_rails/version.rb +1 -1
  47. metadata +11 -16
  48. data/docs/getting_started/quick_start.md +0 -62
  49. data/lib/graphql_rails/model/build_graphql_input_type.rb +0 -43
  50. data/lib/graphql_rails/router/subscription_route.rb +0 -22
@@ -17,6 +17,7 @@ module GraphqlRails
17
17
 
18
18
  chainable_option :description
19
19
  chainable_option :options, default: {}
20
+ chainable_option :extras, default: []
20
21
  chainable_option :type
21
22
  end
22
23
 
@@ -32,6 +33,14 @@ module GraphqlRails
32
33
  groups(*args)
33
34
  end
34
35
 
36
+ def hidden_in_groups(new_groups = ChainableOptions::NOT_SET)
37
+ @hidden_in_groups ||= []
38
+ return @hidden_in_groups if new_groups == ChainableOptions::NOT_SET
39
+
40
+ @hidden_in_groups = Array(new_groups).map(&:to_s)
41
+ self
42
+ end
43
+
35
44
  def required(new_value = true) # rubocop:disable Style/OptionalBooleanParameter
36
45
  @required = new_value
37
46
  self
@@ -40,6 +49,21 @@ module GraphqlRails
40
49
  def optional(new_value = true) # rubocop:disable Style/OptionalBooleanParameter
41
50
  required(!new_value)
42
51
  end
52
+
53
+ def deprecated(reason = 'Deprecated')
54
+ @deprecation_reason = \
55
+ if [false, nil].include?(reason)
56
+ nil
57
+ else
58
+ reason.is_a?(String) ? reason : 'Deprecated'
59
+ end
60
+
61
+ self
62
+ end
63
+
64
+ def deprecation_reason
65
+ @deprecation_reason
66
+ end
43
67
  end
44
68
  end
45
69
  end
@@ -3,7 +3,7 @@
3
3
  module GraphqlRails
4
4
  module Attributes
5
5
  # Parses attribute name and can generates graphql scalar type,
6
- # grapqhl name and etc. based on that
6
+ # graphql name and etc. based on that
7
7
  class AttributeNameParser
8
8
  def initialize(original_name, options: {})
9
9
  @original_name = original_name.to_s
@@ -13,9 +13,9 @@ module GraphqlRails
13
13
  def field_name
14
14
  @field_name ||= \
15
15
  if original_format?
16
- preprocesed_name
16
+ preprocessed_name
17
17
  else
18
- preprocesed_name.camelize(:lower)
18
+ preprocessed_name.camelize(:lower)
19
19
  end
20
20
  end
21
21
 
@@ -47,7 +47,7 @@ module GraphqlRails
47
47
  options[:input_format] == :original || options[:attribute_name_format] == :original
48
48
  end
49
49
 
50
- def preprocesed_name
50
+ def preprocessed_name
51
51
  if name.end_with?('?')
52
52
  "is_#{name.remove(/\?\Z/)}"
53
53
  else
@@ -12,6 +12,7 @@ module GraphqlRails
12
12
 
13
13
  chainable_option :subtype
14
14
  chainable_option :enum
15
+ chainable_option :default_value
15
16
 
16
17
  def initialize(name, config:)
17
18
  @config = config
@@ -25,7 +26,15 @@ module GraphqlRails
25
26
  end
26
27
 
27
28
  def input_argument_options
28
- { required: required?, description: description, camelize: false, groups: groups }
29
+ {
30
+ required: required?,
31
+ description: description,
32
+ camelize: false,
33
+ groups: groups,
34
+ hidden_in_groups: hidden_in_groups,
35
+ **default_value_option,
36
+ **deprecation_reason_params
37
+ }
29
38
  end
30
39
 
31
40
  def paginated?
@@ -36,12 +45,20 @@ module GraphqlRails
36
45
 
37
46
  attr_reader :initial_name, :config
38
47
 
48
+ def default_value_option
49
+ { default_value: default_value }.compact
50
+ end
51
+
39
52
  def attribute_name_parser
40
53
  @attribute_name_parser ||= AttributeNameParser.new(
41
54
  initial_name, options: attribute_naming_options
42
55
  )
43
56
  end
44
57
 
58
+ def deprecation_reason_params
59
+ { deprecation_reason: deprecation_reason }.compact
60
+ end
61
+
45
62
  def attribute_naming_options
46
63
  options.slice(:input_format)
47
64
  end
@@ -63,7 +80,7 @@ module GraphqlRails
63
80
  end
64
81
 
65
82
  def raw_input_type
66
- return type if type.is_a?(GraphQL::InputObjectType)
83
+ return type if type.is_a?(GraphQL::Schema::InputObject)
67
84
  return type.graphql_input_type if type.is_a?(Model::Input)
68
85
  end
69
86
  end
@@ -42,14 +42,13 @@ module GraphqlRails
42
42
  WRAPPER_TYPES = [
43
43
  GraphQL::Schema::List,
44
44
  GraphQL::Schema::NonNull,
45
- GraphQL::NonNullType,
46
- GraphQL::ListType
45
+ GraphQL::Language::Nodes::NonNullType,
46
+ GraphQL::Language::Nodes::ListType
47
47
  ].freeze
48
48
 
49
49
  GRAPHQL_BASE_TYPES = [
50
- GraphQL::BaseType,
51
- GraphQL::ObjectType,
52
- GraphQL::InputObjectType
50
+ GraphQL::Schema::Object,
51
+ GraphQL::Schema::InputObject
53
52
  ].freeze
54
53
 
55
54
  RAW_GRAPHQL_TYPES = (WRAPPER_TYPES + GRAPHQL_BASE_TYPES).freeze
@@ -7,7 +7,7 @@ require 'graphql_rails/errors/error'
7
7
 
8
8
  module GraphqlRails
9
9
  class Controller
10
- # stores all graphql_rails contoller specific config
10
+ # stores all graphql_rails controller specific config
11
11
  class ActionConfiguration
12
12
  class MissingConfigurationError < GraphqlRails::Error; end
13
13
  class DeprecatedDefaultModelError < GraphqlRails::Error; end
@@ -19,6 +19,8 @@ module GraphqlRails
19
19
  action = build_action
20
20
 
21
21
  Class.new(ControllerActionResolver) do
22
+ graphql_name("ControllerActionResolver#{SecureRandom.hex}")
23
+
22
24
  type(*action.type_args, **action.type_options)
23
25
  description(action.description)
24
26
  controller(action.controller)
@@ -7,7 +7,7 @@ require 'graphql_rails/errors/error'
7
7
 
8
8
  module GraphqlRails
9
9
  class Controller
10
- # stores all graphql_rails contoller specific config
10
+ # stores all graphql_rails controller specific config
11
11
  class Configuration
12
12
  class InvalidActionConfiguration < GraphqlRails::Error; end
13
13
 
@@ -8,7 +8,7 @@ require 'graphql_rails/errors/custom_execution_error'
8
8
  module GraphqlRails
9
9
  class Controller
10
10
  class Request
11
- # Converts user provided free-form errors in to meaningfull graphql error classes
11
+ # Converts user provided free-form errors in to meaningful graphql error classes
12
12
  class FormatErrors
13
13
  include Service
14
14
 
@@ -7,11 +7,12 @@ module GraphqlRails
7
7
  require 'graphql_rails/controller/request/format_errors'
8
8
 
9
9
  attr_accessor :object_to_return
10
- attr_reader :errors, :context
10
+ attr_reader :errors, :context, :lookahead
11
11
 
12
12
  def initialize(graphql_object, inputs, context)
13
13
  @graphql_object = graphql_object
14
- @inputs = inputs
14
+ @inputs = inputs.except(:lookahead)
15
+ @lookahead = inputs[:lookahead]
15
16
  @context = context
16
17
  end
17
18
 
@@ -90,7 +90,7 @@ module GraphqlRails
90
90
  if error.is_a?(GraphQL::ExecutionError)
91
91
  render error: error
92
92
  else
93
- render error: SystemError.new(error.message)
93
+ render error: SystemError.new(error)
94
94
  end
95
95
  end
96
96
 
@@ -2,50 +2,51 @@
2
2
 
3
3
  module GraphqlRails
4
4
  module Decorator
5
- # wrapps active record relation and returns decorated object instead
5
+ # wraps active record relation and returns decorated object instead
6
6
  class RelationDecorator
7
7
  delegate :map, :each, to: :to_a
8
- delegate :limit_value, :offset_value, :count, :size, :empty?, to: :relation
8
+ delegate :limit_value, :offset_value, :count, :size, :empty?, :loaded?, to: :relation
9
9
 
10
10
  def self.decorates?(object)
11
11
  (defined?(ActiveRecord) && object.is_a?(ActiveRecord::Relation)) ||
12
12
  defined?(Mongoid) && object.is_a?(Mongoid::Criteria)
13
13
  end
14
14
 
15
- def initialize(decorator:, relation:, decorator_args: [])
15
+ def initialize(decorator:, relation:, decorator_args: [], decorator_kwargs: {})
16
16
  @relation = relation
17
17
  @decorator = decorator
18
18
  @decorator_args = decorator_args
19
+ @decorator_kwargs = decorator_kwargs
19
20
  end
20
21
 
21
22
  %i[where limit order group offset from select having all unscope].each do |method_name|
22
- define_method method_name do |*args, &block|
23
- chainable_method(method_name, *args, &block)
23
+ define_method method_name do |*args, **kwargs, &block|
24
+ chainable_method(method_name, *args, **kwargs, &block)
24
25
  end
25
26
  end
26
27
 
27
28
  %i[first second last find find_by].each do |method_name|
28
- define_method method_name do |*args, &block|
29
- decoratable_object_method(method_name, *args, &block)
29
+ define_method method_name do |*args, **kwargs, &block|
30
+ decoratable_object_method(method_name, *args, **kwargs, &block)
30
31
  end
31
32
  end
32
33
 
33
34
  %i[find_each].each do |method_name|
34
- define_method method_name do |*args, &block|
35
- decoratable_block_method(method_name, *args, &block)
35
+ define_method method_name do |*args, **kwargs, &block|
36
+ decoratable_block_method(method_name, *args, **kwargs, &block)
36
37
  end
37
38
  end
38
39
 
39
40
  def to_a
40
- @to_a ||= relation.to_a.map { |it| decorator.new(it, *decorator_args) }
41
+ @to_a ||= relation.to_a.map { |it| decorator.new(it, *decorator_args, **decorator_kwargs) }
41
42
  end
42
43
 
43
44
  private
44
45
 
45
- attr_reader :relation, :decorator, :decorator_args
46
+ attr_reader :relation, :decorator, :decorator_args, :decorator_kwargs
46
47
 
47
- def decoratable_object_method(method_name, *args, &block)
48
- object = relation.public_send(method_name, *args, &block)
48
+ def decoratable_object_method(method_name, *args, **kwargs, &block)
49
+ object = relation.public_send(method_name, *args, **kwargs, &block)
49
50
  decorate(object)
50
51
  end
51
52
 
@@ -53,22 +54,25 @@ module GraphqlRails
53
54
  return object_or_list if object_or_list.blank?
54
55
 
55
56
  if object_or_list.is_a?(Array)
56
- object_or_list.map { |it| decorator.new(it, *decorator_args) }
57
+ object_or_list.map { |it| decorator.new(it, *decorator_args, **decorator_kwargs) }
57
58
  else
58
- decorator.new(object_or_list, *decorator_args)
59
+ decorator.new(object_or_list, *decorator_args, **decorator_kwargs)
59
60
  end
60
61
  end
61
62
 
62
- def decoratable_block_method(method_name, *args)
63
- relation.public_send(method_name, *args) do |object, *other_args|
63
+ def decoratable_block_method(method_name, *args, **kwargs)
64
+ relation.public_send(method_name, *args, **kwargs) do |object, *other_args|
64
65
  decorated_object = decorate(object)
65
66
  yield(decorated_object, *other_args)
66
67
  end
67
68
  end
68
69
 
69
- def chainable_method(method_name, *args, &block)
70
- new_relation = relation.public_send(method_name, *args, &block)
71
- self.class.new(decorator: decorator, relation: new_relation, decorator_args: decorator_args)
70
+ def chainable_method(method_name, *args, **kwargs, &block)
71
+ new_relation = relation.public_send(method_name, *args, **kwargs, &block)
72
+ self.class.new(
73
+ decorator: decorator, relation: new_relation,
74
+ decorator_args: decorator_args, decorator_kwargs: decorator_kwargs
75
+ )
72
76
  end
73
77
  end
74
78
  end
@@ -25,17 +25,25 @@ module GraphqlRails
25
25
  extend ActiveSupport::Concern
26
26
 
27
27
  class_methods do
28
- def decorate(object, *args)
28
+ def decorate(object, *args, **kwargs)
29
29
  if Decorator::RelationDecorator.decorates?(object)
30
- Decorator::RelationDecorator.new(relation: object, decorator: self, decorator_args: args)
30
+ decorate_with_relation_decorator(object, args, kwargs)
31
31
  elsif object.nil?
32
32
  nil
33
33
  elsif object.is_a?(Array)
34
- object.map { |item| new(item, *args) }
34
+ object.map { |item| new(item, *args, **kwargs) }
35
35
  else
36
- new(object, *args)
36
+ new(object, *args, **kwargs)
37
37
  end
38
38
  end
39
+
40
+ private
41
+
42
+ def decorate_with_relation_decorator(object, args, kwargs)
43
+ Decorator::RelationDecorator.new(
44
+ relation: object, decorator: self, decorator_args: args, decorator_kwargs: kwargs
45
+ )
46
+ end
39
47
  end
40
48
  end
41
49
  end
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module GraphqlRails
4
- # base class which is returned in case something bad happens. Contains all error rendering tructure
4
+ # base class which is returned in case something bad happens. Contains all error rendering structure
5
5
  class CustomExecutionError < ExecutionError
6
6
  attr_reader :extra_graphql_data
7
7
 
@@ -3,7 +3,7 @@
3
3
  module GraphqlRails
4
4
  require 'graphql'
5
5
 
6
- # base class which is returned in case something bad happens. Contains all error rendering tructure
6
+ # base class which is returned in case something bad happens. Contains all error rendering structure
7
7
  class ExecutionError < GraphQL::ExecutionError
8
8
  def to_h
9
9
  super.merge(extra_graphql_data)
@@ -1,8 +1,18 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module GraphqlRails
4
- # base class which is returned in case something bad happens. Contains all error rendering tructure
4
+ # Base class which is returned in case something bad happens. Contains all error rendering structure
5
5
  class SystemError < ExecutionError
6
+ delegate :backtrace, to: :original_error
7
+
8
+ attr_reader :original_error
9
+
10
+ def initialize(original_error)
11
+ super(original_error.message)
12
+
13
+ @original_error = original_error
14
+ end
15
+
6
16
  def to_h
7
17
  super.except('locations')
8
18
  end
@@ -3,10 +3,12 @@
3
3
  module GraphqlRails
4
4
  # GraphQL error that is raised when invalid data is given
5
5
  class ValidationError < ExecutionError
6
+ BASE_FIELD_NAME = 'base'
7
+
6
8
  attr_reader :short_message, :field
7
9
 
8
10
  def initialize(short_message, field)
9
- super([field.presence&.to_s&.humanize, short_message].compact.join(' '))
11
+ super([humanized_field(field), short_message].compact.join(' '))
10
12
  @short_message = short_message
11
13
  @field = field
12
14
  end
@@ -18,5 +20,16 @@ module GraphqlRails
18
20
  def to_h
19
21
  super.merge('field' => field, 'short_message' => short_message)
20
22
  end
23
+
24
+ private
25
+
26
+ def humanized_field(field)
27
+ return if field.blank?
28
+
29
+ stringified_field = field.to_s
30
+ return if stringified_field == BASE_FIELD_NAME
31
+
32
+ stringified_field.humanize
33
+ end
21
34
  end
22
35
  end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'graphql_rails/types/input_object_type'
4
+ require 'graphql_rails/concerns/service'
5
+ require 'graphql_rails/model/find_or_build_graphql_type'
6
+
7
+ module GraphqlRails
8
+ module Model
9
+ # stores information about model specific config, like attributes and types
10
+ class FindOrBuildGraphqlInputType < FindOrBuildGraphqlType
11
+ include ::GraphqlRails::Service
12
+
13
+ private
14
+
15
+ def parent_class
16
+ GraphqlRails::Types::InputObjectType
17
+ end
18
+
19
+ def add_attributes_batch(attributes)
20
+ klass.class_eval do
21
+ attributes.each do |attribute|
22
+ argument(*attribute.input_argument_args, **attribute.input_argument_options)
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
@@ -19,7 +19,7 @@ module GraphqlRails
19
19
  end
20
20
 
21
21
  def call
22
- klass.tap { add_fields_to_graphql_type if new_class? || force_define_attributes }
22
+ klass.tap { add_attributes if new_class? || force_define_attributes }
23
23
  end
24
24
 
25
25
  private
@@ -28,20 +28,29 @@ module GraphqlRails
28
28
 
29
29
  delegate :klass, :new_class?, to: :type_class_finder
30
30
 
31
+ def parent_class
32
+ GraphqlRails::Types::ObjectType
33
+ end
34
+
35
+ def add_attributes_batch(attributes)
36
+ AddFieldsToGraphqlType.call(klass: klass, attributes: attributes)
37
+ end
38
+
31
39
  def type_class_finder
32
40
  @type_class_finder ||= FindOrBuildGraphqlTypeClass.new(
33
41
  name: name,
34
42
  type_name: type_name,
35
- description: description
43
+ description: description,
44
+ parent_class: parent_class
36
45
  )
37
46
  end
38
47
 
39
- def add_fields_to_graphql_type
48
+ def add_attributes
40
49
  scalar_attributes, dynamic_attributes = attributes.values.partition(&:scalar_type?)
41
50
 
42
- AddFieldsToGraphqlType.call(klass: klass, attributes: scalar_attributes)
51
+ add_attributes_batch(scalar_attributes)
43
52
  dynamic_attributes.each { |attribute| find_or_build_dynamic_type(attribute) }
44
- AddFieldsToGraphqlType.call(klass: klass, attributes: dynamic_attributes)
53
+ add_attributes_batch(dynamic_attributes)
45
54
  end
46
55
 
47
56
  def find_or_build_dynamic_type(attribute)
@@ -9,11 +9,12 @@ module GraphqlRails
9
9
 
10
10
  include ::GraphqlRails::Service
11
11
 
12
- def initialize(name:, type_name:, description: nil)
12
+ def initialize(name:, type_name:, parent_class:, description: nil)
13
13
  @name = name
14
14
  @type_name = type_name
15
15
  @description = description
16
16
  @new_class = false
17
+ @parent_class = parent_class
17
18
  end
18
19
 
19
20
  def klass
@@ -27,13 +28,13 @@ module GraphqlRails
27
28
  private
28
29
 
29
30
  attr_accessor :new_class
30
- attr_reader :name, :type_name, :description
31
+ attr_reader :name, :type_name, :description, :parent_class
31
32
 
32
33
  def build_graphql_type_klass
33
34
  graphql_type_name = name
34
35
  graphql_type_description = description
35
36
 
36
- graphql_type_klass = Class.new(GraphqlRails::Types::ObjectType) do
37
+ graphql_type_klass = Class.new(parent_class) do
37
38
  graphql_name(graphql_type_name)
38
39
  description(graphql_type_description)
39
40
  end
@@ -6,7 +6,7 @@ module GraphqlRails
6
6
  class Input
7
7
  require 'graphql_rails/concerns/chainable_options'
8
8
  require 'graphql_rails/model/configurable'
9
- require 'graphql_rails/model/build_graphql_input_type'
9
+ require 'graphql_rails/model/find_or_build_graphql_input_type'
10
10
 
11
11
  include Configurable
12
12
 
@@ -18,8 +18,8 @@ module GraphqlRails
18
18
  end
19
19
 
20
20
  def graphql_input_type
21
- @graphql_input_type ||= BuildGraphqlInputType.call(
22
- name: name, description: description, attributes: attributes
21
+ @graphql_input_type ||= FindOrBuildGraphqlInputType.call(
22
+ name: name, description: description, attributes: attributes, type_name: type_name
23
23
  )
24
24
  end
25
25
 
@@ -0,0 +1,112 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GraphqlRails
4
+ class Router
5
+ # Builds GraphQL type used in graphql schema
6
+ class BuildSchemaActionType
7
+ ROUTES_KEY = :__routes__
8
+
9
+ # @private
10
+ class SchemaActionType < GraphQL::Schema::Object
11
+ def self.inspect
12
+ "#{GraphQL::Schema::Object}(#{graphql_name})"
13
+ end
14
+
15
+ class << self
16
+ def fields_for_nested_routes(type_name_prefix:, scoped_routes:)
17
+ routes_by_scope = scoped_routes.dup
18
+ unscoped_routes = routes_by_scope.delete(ROUTES_KEY) || []
19
+
20
+ scoped_only_fields(type_name_prefix, routes_by_scope)
21
+ unscoped_routes.each { route_field(_1) }
22
+ end
23
+
24
+ private
25
+
26
+ def route_field(route)
27
+ field(*route.name, **route.field_options)
28
+ end
29
+
30
+ def scoped_only_fields(type_name_prefix, routes_by_scope)
31
+ routes_by_scope.each_pair do |scope_name, inner_scope_routes|
32
+ scope_field(scope_name, "#{type_name_prefix}#{scope_name.to_s.camelize}", inner_scope_routes)
33
+ end
34
+ end
35
+
36
+ def scope_field(scope_name, scope_type_name, scoped_routes)
37
+ scope_type = build_scope_type_class(
38
+ type_name: scope_type_name,
39
+ scoped_routes: scoped_routes
40
+ )
41
+
42
+ field(scope_name.to_s.camelize(:lower), scope_type, null: false)
43
+ define_method(scope_type_name.underscore) { self }
44
+ end
45
+
46
+ def build_scope_type_class(type_name:, scoped_routes:)
47
+ Class.new(SchemaActionType) do
48
+ graphql_name("#{type_name}Scope")
49
+
50
+ fields_for_nested_routes(
51
+ type_name_prefix: type_name,
52
+ scoped_routes: scoped_routes
53
+ )
54
+ end
55
+ end
56
+ end
57
+ end
58
+
59
+ def self.call(**kwargs)
60
+ new(**kwargs).call
61
+ end
62
+
63
+ def initialize(type_name:, routes:)
64
+ @type_name = type_name
65
+ @routes = routes
66
+ end
67
+
68
+ def call
69
+ type_name = self.type_name
70
+ scoped_routes = self.scoped_routes
71
+
72
+ Class.new(SchemaActionType) do
73
+ graphql_name(type_name)
74
+
75
+ fields_for_nested_routes(
76
+ type_name_prefix: type_name,
77
+ scoped_routes: scoped_routes
78
+ )
79
+ end
80
+ end
81
+
82
+ private
83
+
84
+ attr_reader :type_name, :routes
85
+
86
+ def scoped_routes
87
+ routes.each_with_object({}) do |route, result|
88
+ scope_names = route.scope_names.map { _1.to_s.camelize(:lower) }
89
+ path_to_routes = scope_names + [ROUTES_KEY]
90
+ deep_append(result, path_to_routes, route)
91
+ end
92
+ end
93
+
94
+ # adds array element to nested hash
95
+ # usage:
96
+ # deep_hash = { a: { b: [1] } }
97
+ # deep_append(deep_hash, [:a, :b], 2)
98
+ # deep_hash #=> { a: { b: [1, 2] } }
99
+ def deep_append(hash, keys, value)
100
+ deepest_hash = hash
101
+ *other_keys, last_key = keys
102
+
103
+ other_keys.each do |key|
104
+ deepest_hash[key] ||= {}
105
+ deepest_hash = deepest_hash[key]
106
+ end
107
+ deepest_hash[last_key] ||= []
108
+ deepest_hash[last_key] += [value]
109
+ end
110
+ end
111
+ end
112
+ end