graphql_rails 2.2.0 → 2.4.0

Sign up to get free protection for your applications and to get access to all the features.
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