graphql_rails 2.1.0 → 2.3.0

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