graphql_rails 2.1.0 → 2.3.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 (35) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ruby.yml +1 -1
  3. data/.ruby-version +1 -1
  4. data/CHANGELOG.md +14 -0
  5. data/Gemfile.lock +117 -116
  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 +62 -0
  11. data/docs/components/routes.md +45 -8
  12. data/lib/graphql_rails/attributes/attribute.rb +8 -14
  13. data/lib/graphql_rails/attributes/attribute_configurable.rb +15 -0
  14. data/lib/graphql_rails/attributes/input_attribute.rb +17 -1
  15. data/lib/graphql_rails/attributes/type_parseable.rb +1 -7
  16. data/lib/graphql_rails/controller/build_controller_action_resolver.rb +2 -0
  17. data/lib/graphql_rails/controller/log_controller_action.rb +7 -2
  18. data/lib/graphql_rails/controller.rb +1 -1
  19. data/lib/graphql_rails/decorator/relation_decorator.rb +22 -18
  20. data/lib/graphql_rails/decorator.rb +12 -4
  21. data/lib/graphql_rails/errors/system_error.rb +11 -1
  22. data/lib/graphql_rails/errors/validation_error.rb +14 -1
  23. data/lib/graphql_rails/input_configurable.rb +1 -1
  24. data/lib/graphql_rails/model/find_or_build_graphql_type.rb +1 -5
  25. data/lib/graphql_rails/router/build_schema_action_type.rb +112 -0
  26. data/lib/graphql_rails/router/mutation_route.rb +4 -0
  27. data/lib/graphql_rails/router/query_route.rb +4 -0
  28. data/lib/graphql_rails/router/resource_routes_builder.rb +8 -0
  29. data/lib/graphql_rails/router/route.rb +3 -2
  30. data/lib/graphql_rails/router/schema_builder.rb +14 -18
  31. data/lib/graphql_rails/router/subscription_route.rb +22 -0
  32. data/lib/graphql_rails/router.rb +32 -10
  33. data/lib/graphql_rails/version.rb +1 -1
  34. metadata +5 -4
  35. data/docs/getting_started/quick_start.md +0 -62
@@ -73,14 +73,15 @@ end
73
73
 
74
74
  This will generate `userDetails` field on GraphQL side.
75
75
 
76
- ## _query_ and _mutation_
76
+ ## _query_ and _mutation_ & _subscription_
77
77
 
78
- in case you want to have non-CRUD controller with custom actions you can define your own `query`/`mutation` actions like this:
78
+ in case you want to have non-CRUD controller with custom actions you can define your own `query`/`mutation`/`subscription` actions like this:
79
79
 
80
80
  ```ruby
81
81
  MyGraphqlSchema = GraphqlRails::Router.draw do
82
82
  mutation :logIn, to: 'sessions#login'
83
- query :me, to 'users#current_user'
83
+ query :me, to: 'users#current_user'
84
+ subscribtion :new_message, to: 'messages#created'
84
85
  end
85
86
  ```
86
87
 
@@ -88,18 +89,54 @@ end
88
89
 
89
90
  ### _module_ options
90
91
 
91
- currently `scope` method accepts single option: `module`. `module` allows to specify controller namespace. So you can use scoped controllers, like so:
92
+ If you want to want to route everything to controllers, located at `controllers/admin/top_secret`, you can use scope with `module` param:
92
93
 
93
94
  ```ruby
94
- MyGraphqlSchema = GraphqlRails::Router.draw do
95
- scope module: 'admin/top_secret' do
96
- mutation :logIn, to: 'sessions#login' # this will trigger Admin::TopSecret::SessionsController
97
- end
95
+ scope module: 'admin/top_secret' do
96
+ mutation :logIn, to: 'sessions#login' # this will trigger Admin::TopSecret::SessionsController
97
+ end
98
+ ```
99
+
100
+ ### Named scope
98
101
 
102
+ If you want to nest some routes under some other node, you can use named scope:
103
+
104
+ ```ruby
105
+ scope :admin do
99
106
  mutation :logIn, to: 'sessions#login' # this will trigger ::SessionsController
100
107
  end
101
108
  ```
102
109
 
110
+ This action will be accessible via:
111
+
112
+ ```graphql
113
+ mutation {
114
+ admin {
115
+ logIn(email: 'john@example.com') { ... }
116
+ }
117
+ }
118
+ ```
119
+
120
+ ## _namespace_
121
+
122
+ You may wish to organize groups of controllers under a namespace. Most commonly, you might group a number of administrative controllers under an `Admin::` namespace, and place these controllers under the app/controllers/admin directory. You can route to such a group by using a namespace block:
123
+
124
+ ```ruby
125
+ namespace :admin do
126
+ resources :articles, only: :show
127
+ end
128
+ ```
129
+
130
+ On GraphQL side, you can reach such route with the following query:
131
+
132
+ ```graphql
133
+ query {
134
+ admin {
135
+ article(id: '123') { ... }
136
+ }
137
+ }
138
+ ```
139
+
103
140
  ## _group_
104
141
 
105
142
  You can have multiple routers / schemas. In order to add resources or query only to specific schema, you need wrap it with `group` method, like this:
@@ -32,8 +32,8 @@ module GraphqlRails
32
32
  [
33
33
  field_name,
34
34
  type_parser.type_arg,
35
- *description
36
- ]
35
+ description
36
+ ].compact
37
37
  end
38
38
 
39
39
  def field_options
@@ -41,21 +41,11 @@ module GraphqlRails
41
41
  method: property.to_sym,
42
42
  null: optional?,
43
43
  camelize: camelize?,
44
- groups: groups
44
+ groups: groups,
45
+ **deprecation_reason_params
45
46
  }
46
47
  end
47
48
 
48
- def argument_args
49
- [
50
- field_name,
51
- type_parser.type_arg,
52
- {
53
- description: description,
54
- required: required?
55
- }
56
- ]
57
- end
58
-
59
49
  protected
60
50
 
61
51
  attr_reader :initial_name
@@ -65,6 +55,10 @@ module GraphqlRails
65
55
  def camelize?
66
56
  options[:input_format] != :original && options[:attribute_name_format] != :original
67
57
  end
58
+
59
+ def deprecation_reason_params
60
+ { deprecation_reason: deprecation_reason }.compact
61
+ end
68
62
  end
69
63
  end
70
64
  end
@@ -40,6 +40,21 @@ module GraphqlRails
40
40
  def optional(new_value = true) # rubocop:disable Style/OptionalBooleanParameter
41
41
  required(!new_value)
42
42
  end
43
+
44
+ def deprecated(reason = 'Deprecated')
45
+ @deprecation_reason = \
46
+ if [false, nil].include?(reason)
47
+ nil
48
+ else
49
+ reason.is_a?(String) ? reason : 'Deprecated'
50
+ end
51
+
52
+ self
53
+ end
54
+
55
+ def deprecation_reason
56
+ @deprecation_reason
57
+ end
43
58
  end
44
59
  end
45
60
  end
@@ -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,14 @@ 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
+ **default_value_option,
35
+ **deprecation_reason_params
36
+ }
29
37
  end
30
38
 
31
39
  def paginated?
@@ -36,12 +44,20 @@ module GraphqlRails
36
44
 
37
45
  attr_reader :initial_name, :config
38
46
 
47
+ def default_value_option
48
+ { default_value: default_value }.compact
49
+ end
50
+
39
51
  def attribute_name_parser
40
52
  @attribute_name_parser ||= AttributeNameParser.new(
41
53
  initial_name, options: attribute_naming_options
42
54
  )
43
55
  end
44
56
 
57
+ def deprecation_reason_params
58
+ { deprecation_reason: deprecation_reason }.compact
59
+ end
60
+
45
61
  def attribute_naming_options
46
62
  options.slice(:input_format)
47
63
  end
@@ -52,12 +52,6 @@ module GraphqlRails
52
52
  GraphQL::InputObjectType
53
53
  ].freeze
54
54
 
55
- PARSEABLE_RAW_GRAPHQL_TYPES = [
56
- GraphQL::Schema::Object,
57
- GraphQL::Schema::Scalar,
58
- GraphQL::Schema::Enum
59
- ].freeze
60
-
61
55
  RAW_GRAPHQL_TYPES = (WRAPPER_TYPES + GRAPHQL_BASE_TYPES).freeze
62
56
 
63
57
  def unwrapped_scalar_type
@@ -103,7 +97,7 @@ module GraphqlRails
103
97
  def graphql_type_object?(type_class)
104
98
  return false unless type_class.is_a?(Class)
105
99
 
106
- PARSEABLE_RAW_GRAPHQL_TYPES.any? { |parent_type| type_class < parent_type }
100
+ type_class < GraphQL::Schema::Member
107
101
  end
108
102
 
109
103
  def applicable_graphql_type?(type)
@@ -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)
@@ -62,9 +62,14 @@ module GraphqlRails
62
62
  end
63
63
 
64
64
  def parameter_filter_class
65
- return ActiveSupport::ParameterFilter if Object.const_defined?('ActiveSupport::ParameterFilter')
65
+ if ActiveSupport.gem_version.segments.first < 6
66
+ return ActiveSupport::ParameterFilter if Object.const_defined?('ActiveSupport::ParameterFilter')
66
67
 
67
- ActionDispatch::Http::ParameterFilter
68
+ ActionDispatch::Http::ParameterFilter
69
+ else
70
+ require 'active_support/parameter_filter'
71
+ ActiveSupport::ParameterFilter
72
+ end
68
73
  end
69
74
  end
70
75
  end
@@ -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
 
@@ -12,40 +12,41 @@ module GraphqlRails
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,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
@@ -26,7 +26,7 @@ module GraphqlRails
26
26
  pagination_options = nil if pagination_options == false
27
27
 
28
28
  @pagination_options = pagination_options
29
- permit(:before, :after, first: :int, last: :int)
29
+ self
30
30
  end
31
31
 
32
32
  def paginated?
@@ -46,11 +46,7 @@ module GraphqlRails
46
46
 
47
47
  def find_or_build_dynamic_type(attribute)
48
48
  graphql_model = attribute.graphql_model
49
- if graphql_model
50
- find_or_build_graphql_model_type(graphql_model)
51
- else
52
- AddFieldsToGraphqlType.call(klass: klass, attributes: [attribute])
53
- end
49
+ find_or_build_graphql_model_type(graphql_model) if graphql_model
54
50
  end
55
51
 
56
52
  def find_or_build_graphql_model_type(graphql_model)
@@ -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
@@ -13,6 +13,10 @@ module GraphqlRails
13
13
  def mutation?
14
14
  true
15
15
  end
16
+
17
+ def subscription?
18
+ false
19
+ end
16
20
  end
17
21
  end
18
22
  end
@@ -13,6 +13,10 @@ module GraphqlRails
13
13
  def mutation?
14
14
  false
15
15
  end
16
+
17
+ def subscription?
18
+ false
19
+ end
16
20
  end
17
21
  end
18
22
  end
@@ -28,6 +28,10 @@ module GraphqlRails
28
28
  routes << build_mutation(*args, **kwargs)
29
29
  end
30
30
 
31
+ def subscription(*args, **kwargs)
32
+ routes << build_subscription(*args, **kwargs)
33
+ end
34
+
31
35
  private
32
36
 
33
37
  attr_reader :autogenerated_action_names, :name, :options
@@ -62,6 +66,10 @@ module GraphqlRails
62
66
  build_route(QueryRoute, *args, **kwargs)
63
67
  end
64
68
 
69
+ def build_subscription(*args, **kwargs)
70
+ build_route(SubscriptionRoute, *args, **kwargs)
71
+ end
72
+
65
73
  # rubocop:disable Metrics/ParameterLists
66
74
  def build_route(builder, action, prefix: action, suffix: false, on: :member, **custom_options)
67
75
  if suffix == true
@@ -6,15 +6,16 @@ module GraphqlRails
6
6
  class Router
7
7
  # Generic class for any type graphql action. Should not be used directly
8
8
  class Route
9
- attr_reader :name, :module_name, :on, :relative_path, :groups
9
+ attr_reader :name, :module_name, :on, :relative_path, :groups, :scope_names
10
10
 
11
- def initialize(name, to: '', on:, groups: nil, **options)
11
+ def initialize(name, on:, to: '', groups: nil, scope_names: [], **options) # rubocop:disable Metrics/ParameterLists
12
12
  @name = name.to_s.camelize(:lower)
13
13
  @module_name = options[:module].to_s
14
14
  @function = options[:function]
15
15
  @groups = groups
16
16
  @relative_path = to
17
17
  @on = on.to_sym
18
+ @scope_names = scope_names
18
19
  end
19
20
 
20
21
  def path