graphql_rails 2.2.0 → 2.3.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 1909c3b76c9addab63bcc0337232c147ceebde0d1f2252da93606b48d1843c64
4
- data.tar.gz: 6b405941e4bae0e98abbcc07b93ac1a95fa22bcbdfcaa962d241740a15453ba7
3
+ metadata.gz: 9f06459a3c8c16e40dffd181d7c08128558e2bdca598845b9e94f51852681e61
4
+ data.tar.gz: 1a4ddc9e11a4c5ef22240f7e36b3070bcf89b712c561cad12e41ede1dc18fbc3
5
5
  SHA512:
6
- metadata.gz: f5292647cd5c49c51bec7f9f1df33be882637ac83bd54fb23259fc2b5c6d8f351f3501c5db23f0ff7c10daafafed531b4026ff3790ff281cfd71890ff2b2c007
7
- data.tar.gz: 054adf43e83c54be23fb33547ee521e17ae8f180f5f4c05bf3a7b78f554367e6531483d9bf66af75fe4567940f2931c56f93c2adc6af412efba8e644043af8bc
6
+ metadata.gz: 23e0d160c19e3c56c00a474cb77eff10c6a86c3ce1c63f3cdb87f4c374f3c10da79b5f2529a14a026f81e6f3b2c32e9b2fecd671bd7b38066e07d020c65fa93a
7
+ data.tar.gz: a0ade6d1ce611baf97ab0a631770273c98953b04eb3991e3234c12a12f21e893962b62a1785ba5da48de7193c366129d29732c7121fbe6c4404d5e1f05cedda2
@@ -4,7 +4,7 @@ jobs:
4
4
  specs:
5
5
  strategy:
6
6
  matrix:
7
- ruby-version: ['2.6', '2.7', '3.0']
7
+ ruby-version: ['2.7', '3.0', '3.1']
8
8
 
9
9
  runs-on: ubuntu-latest
10
10
  env:
data/.ruby-version CHANGED
@@ -1 +1 @@
1
- 3.0.1
1
+ 3.1.2
data/CHANGELOG.md CHANGED
@@ -9,6 +9,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
9
9
 
10
10
  * Added/Changed/Deprecated/Removed/Fixed/Security: YOUR CHANGE HERE
11
11
 
12
+ ## [2.3.0](2022-11-25)
13
+
14
+ * Added support for Ruby 3.1.2, keyword arguments for decorators support included
15
+ * Added: error backtrace to SystemError
16
+ * Fixed: skip "base" field name in validation error messages
17
+ * Added: router namespaces and named scopes
18
+ * Added: `deprecate` method/option for attributes and input attributes
19
+
12
20
  ## [2.2.0](2022-01-25)
13
21
 
14
22
  * Added: support for subscription type
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- graphql_rails (2.2.0)
4
+ graphql_rails (2.3.0)
5
5
  activesupport (>= 4)
6
6
  graphql (~> 1.12, >= 1.12.4)
7
7
 
@@ -81,10 +81,10 @@ GEM
81
81
  erubi (1.10.0)
82
82
  globalid (1.0.0)
83
83
  activesupport (>= 5.0)
84
- graphql (1.13.6)
84
+ graphql (1.13.17)
85
85
  i18n (1.8.11)
86
86
  concurrent-ruby (~> 1.0)
87
- loofah (2.13.0)
87
+ loofah (2.18.0)
88
88
  crass (~> 1.0.2)
89
89
  nokogiri (>= 1.5.9)
90
90
  mail (2.7.1)
@@ -92,7 +92,7 @@ GEM
92
92
  marcel (1.0.2)
93
93
  method_source (1.0.0)
94
94
  mini_mime (1.1.2)
95
- mini_portile2 (2.7.1)
95
+ mini_portile2 (2.8.0)
96
96
  minitest (5.15.0)
97
97
  mongo (2.17.0)
98
98
  bson (>= 4.8.2, < 5.0.0)
@@ -101,8 +101,8 @@ GEM
101
101
  mongo (>= 2.10.5, < 3.0.0)
102
102
  ruby2_keywords (~> 0.0.5)
103
103
  nio4r (2.5.8)
104
- nokogiri (1.13.1)
105
- mini_portile2 (~> 2.7.0)
104
+ nokogiri (1.13.9)
105
+ mini_portile2 (~> 2.8.0)
106
106
  racc (~> 1.4)
107
107
  parallel (1.21.0)
108
108
  parser (3.1.0.0)
@@ -114,7 +114,7 @@ GEM
114
114
  byebug (~> 11.0)
115
115
  pry (~> 0.13.0)
116
116
  racc (1.6.0)
117
- rack (2.2.3)
117
+ rack (2.2.3.1)
118
118
  rack-test (1.1.0)
119
119
  rack (>= 1.0, < 3)
120
120
  rails (6.1.4.4)
@@ -135,7 +135,7 @@ GEM
135
135
  rails-dom-testing (2.0.3)
136
136
  activesupport (>= 4.2.0)
137
137
  nokogiri (>= 1.6)
138
- rails-html-sanitizer (1.4.2)
138
+ rails-html-sanitizer (1.4.3)
139
139
  loofah (~> 2.3)
140
140
  railties (6.1.4.4)
141
141
  actionpack (= 6.1.4.4)
data/docs/README.md CHANGED
@@ -42,11 +42,12 @@ GraphqlRails::Router.draw do
42
42
  resources :users
43
43
 
44
44
  # if you want custom queries or mutation
45
- query 'searchLogs', to: 'logs#search' # redirects request to LogsController
46
- mutation 'changeUserPassword', to: 'users#change_password'
45
+ query 'searchLogs', to: 'logs#search' # action is handled by LogsController#search
47
46
  end
48
47
  ```
49
48
 
49
+ See [Routes docs](components/routes.md) for more info.
50
+
50
51
  ### Define your Graphql model
51
52
 
52
53
  ```ruby
@@ -56,67 +57,42 @@ class User # works with any class including ActiveRecord
56
57
 
57
58
  graphql do |c|
58
59
  # most common attributes, like :id, :name, :title has default type, so you don't have to specify it (but you can!)
59
- c.attribute :id
60
+ c.attribute(:id)
60
61
 
61
- c.attribute :email, type: :string
62
- c.attribute :surname, type: :string
62
+ c.attribute(:email).type('String')
63
+ c.attribute(:surname).type('String')
63
64
  end
64
65
  end
65
66
  ```
66
67
 
68
+ See [Model docs](components/model.md) for more info.
69
+
67
70
  ### Define controller
68
71
 
69
72
  ```ruby
70
73
  # app/controllers/graphql/users_controller.rb
71
74
  class Graphql::UsersController < GraphqlApplicationController
72
- # graphql requires to describe which attributes controller action accepts and which returns
73
- action(:change_user_password)
74
- .permit(:password!, :id!) # Bang (!) indicates that attribute is required
75
- .returns('User!')
75
+ model('User') # specify that all actions returns User by default
76
76
 
77
- def change_user_password
78
- user = User.find(params[:id])
79
- user.update!(password: params[:password])
77
+ # DRUD actions description
78
+ action(:index).permit(id: 'ID!').returns_many
79
+ action(:show).permit(id: 'ID!').returns_single
80
+ action(:create).permit(email: 'String!').returns_single
81
+ action(:update).permit(id: 'ID!', email: 'String!').returns_single
82
+ action(:destroy).permit(id: 'ID!').returns_single
80
83
 
81
- # returned value needs to have all methods defined in model `graphql do` part
82
- user # or SomeDecorator.new(user)
84
+ def index
85
+ User.all
83
86
  end
84
87
 
85
- action(:search)
86
- .permit(search_fields!: SearchFieldsInput) # you can specify your own input fields
87
- .returns('[User!]!')
88
- def search
88
+ def show
89
+ User.find(params[:id])
89
90
  end
91
+ # ... code for create / update / destroy is skipped ...
90
92
  end
91
93
  ```
92
94
 
93
- ## Routes
94
-
95
- ```ruby
96
- GraphqlRails::Router.draw do
97
- # generates `friend`, `createFriend`, `updateFriend`, `destroyFriend`, `friends` routes
98
- resources :friends
99
- resources :shops, only: [:show, :index] # generates `shop` and `shops` routes only
100
- resources :orders, except: :update # generates all routes except `updateOrder`
101
-
102
- resources :users do
103
- # generates `findUser` query
104
- query :find, on: :member
105
-
106
- # generates `searchUsers` query
107
- query :search, on: :collection
108
- end
109
-
110
- # you can use namespaced controllers too:
111
- scope module: 'admin' do
112
- # `updateTranslations` route will be handled by `Admin::TranslationsController`
113
- mutation :updateTranslations, to: 'translations#update'
114
-
115
- # all :groups routes will be handled by `Admin::GroupsController`
116
- resources :groups
117
- end
118
- end
119
- ```
95
+ See [Controller docs](components/controlle.md) for more info.
120
96
 
121
97
  ## Testing your GraphqlRails::Controller in RSpec
122
98
 
@@ -134,6 +110,8 @@ RSpec.configure do |config|
134
110
  end
135
111
  ```
136
112
 
113
+ See [Testing docs](testing/testing.md) for more info.
114
+
137
115
  ### Helper methods
138
116
 
139
117
  There are 3 helper methods:
data/docs/_sidebar.md CHANGED
@@ -1,7 +1,6 @@
1
1
  * [Home](README)
2
2
  * Getting started
3
3
  * [Setup](getting_started/setup)
4
- * [Quick start](getting_started/quick_start)
5
4
  * Components
6
5
  * [Routes](components/routes)
7
6
  * [Model](components/model)
@@ -144,7 +144,7 @@ end
144
144
  If you do not specify `subtype` then default (without name) input will be used. You need to specify subtype if you want to use non-default input:
145
145
 
146
146
  ```ruby
147
- class OrderController < GraphqlRails::Controller
147
+ class UsersController < GraphqlRails::Controller
148
148
  # this is the input with email and full_name:
149
149
  action(:create)
150
150
  .permit_input(:input, type: 'User!')
@@ -155,6 +155,20 @@ class OrderController < GraphqlRails::Controller
155
155
  end
156
156
  ```
157
157
 
158
+ #### *deprecated*
159
+
160
+ You can mark input input as deprecated with `deprecated` option:
161
+
162
+ ```ruby
163
+ class UsersController < GraphqlRails::Controller
164
+ action(:create)
165
+ .permit_input(:input, type: 'User', deprecated: true)
166
+
167
+ action(:update)
168
+ .permit_input(:input, type: 'User', deprecated: 'use updateBasicUser instead')
169
+ end
170
+ ```
171
+
158
172
  ### *paginated*
159
173
 
160
174
  You can mark collection action as `paginated`. In this case controller will return relay connection type and it will be possible to return only partial results. No need to do anything on controller side (you should always return full list of items)
@@ -46,7 +46,7 @@ class UserDecorator < SimpleDelegator
46
46
  include GraphqlRails::Model
47
47
  include GraphqlRails::Decorator
48
48
 
49
- graphql_rails do
49
+ graphql do |c|
50
50
  # some setup, attributes, etc...
51
51
  end
52
52
 
@@ -131,6 +131,21 @@ class User
131
131
  end
132
132
  ```
133
133
 
134
+ ### attribute.deprecated
135
+
136
+ Attribute can be marked as deprecated with `deprecated` method:
137
+
138
+ ```ruby
139
+ class User
140
+ include GraphqlRails::Model
141
+
142
+ graphql do |c|
143
+ c.attribute(:legacy_name).deprecated
144
+ c.attribute(:legacy_id).deprecated('This is my custom deprecation reason')
145
+ end
146
+ end
147
+ ```
148
+
134
149
  ### attribute.groups
135
150
 
136
151
  Groups are handy feature when you want to have multiple schemas. For example, you want to have public graphql endpoint and internal graphql endpoint where each group has some unique nodes. If attribute has `groups` set, then this attribute will be visible only in appropriate group schemas.
@@ -263,6 +278,23 @@ class User
263
278
  end
264
279
  ```
265
280
 
281
+ #### *deprecated*
282
+
283
+ You can mark input input as deprecated with `deprecated` option:
284
+
285
+
286
+ ```ruby
287
+ class User
288
+ include GraphqlRails::Model
289
+
290
+ graphql.attribute(:avatar_url)
291
+ .permit_input(:size, type: :int!, deprecated: true)
292
+
293
+ graphql.attribute(:logo_url)
294
+ .permit_input(:size, type: :int!, deprecated: 'custom image size is deprecated')
295
+ end
296
+ ```
297
+
266
298
  ### attribute.paginated
267
299
 
268
300
  You can mark collection method as `paginated`. In this case method will return relay connection type and it will be possible to return only partial results. No need to do anything on method side (you should always return full list of items)
@@ -523,6 +555,36 @@ class User
523
555
  end
524
556
  ```
525
557
 
558
+
559
+ #### input attribute deprecation
560
+
561
+ You can mark input attribute as deprecated with `deprecated` method:
562
+
563
+ ```ruby
564
+ class User
565
+ include GraphqlRails::Model
566
+
567
+ graphql.input do |c|
568
+ c.attribute(:full_name).deprecated('Use firstName and lastName instead')
569
+ c.attribute(:surname).deprecated
570
+ end
571
+ end
572
+ ```
573
+
574
+ #### input attribute default value
575
+
576
+ You can set default value for input attribute:
577
+
578
+ ```ruby
579
+ class User
580
+ include GraphqlRails::Model
581
+
582
+ graphql.input do |c|
583
+ c.attribute(:is_admin).type('Boolean').default_value(false)
584
+ end
585
+ end
586
+ ```
587
+
526
588
  ## graphql_context
527
589
 
528
590
  It's possible to access graphql_context in your model using method `graphql_context`:
@@ -89,18 +89,54 @@ end
89
89
 
90
90
  ### _module_ options
91
91
 
92
- 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:
93
93
 
94
94
  ```ruby
95
- MyGraphqlSchema = GraphqlRails::Router.draw do
96
- scope module: 'admin/top_secret' do
97
- mutation :logIn, to: 'sessions#login' # this will trigger Admin::TopSecret::SessionsController
98
- 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
99
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
100
106
  mutation :logIn, to: 'sessions#login' # this will trigger ::SessionsController
101
107
  end
102
108
  ```
103
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
+
104
140
  ## _group_
105
141
 
106
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
@@ -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)
@@ -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
@@ -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
@@ -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
@@ -5,6 +5,7 @@ module GraphqlRails
5
5
  # builds GraphQL::Schema based on previously defined grahiti data
6
6
  class SchemaBuilder
7
7
  require_relative './plain_cursor_encoder'
8
+ require_relative './build_schema_action_type'
8
9
 
9
10
  attr_reader :queries, :mutations, :subscriptions, :raw_actions
10
11
 
@@ -49,23 +50,9 @@ module GraphqlRails
49
50
  .uniq(&:name)
50
51
  .reverse
51
52
 
52
- return if group_routes.empty?
53
+ return if group_routes.empty? && type_name != 'Query'
53
54
 
54
- build_type(type_name, group_routes)
55
- end
56
-
57
- def build_type(type_name, group_routes)
58
- Class.new(GraphQL::Schema::Object) do
59
- graphql_name(type_name)
60
-
61
- group_routes.each do |route|
62
- field(*route.name, **route.field_options)
63
- end
64
-
65
- def self.inspect
66
- "#{GraphQL::Schema::Object}(#{graphql_name})"
67
- end
68
- end
55
+ BuildSchemaActionType.call(type_name: type_name, routes: group_routes)
69
56
  end
70
57
  end
71
58
  end
@@ -21,9 +21,10 @@ module GraphqlRails
21
21
  end
22
22
  end
23
23
 
24
- attr_reader :routes, :namespace_name, :raw_graphql_actions
24
+ attr_reader :routes, :namespace_name, :raw_graphql_actions, :scope_names
25
25
 
26
- def initialize(module_name: '', group_names: [])
26
+ def initialize(module_name: '', group_names: [], scope_names: [])
27
+ @scope_names = scope_names
27
28
  @module_name = module_name
28
29
  @group_names = group_names
29
30
  @routes ||= Set.new
@@ -37,13 +38,16 @@ module GraphqlRails
37
38
  routes.merge(scoped_router.routes)
38
39
  end
39
40
 
40
- def scope(**options, &block)
41
- full_module_name = [module_name, options[:module]].reject(&:empty?).join('/')
42
- scoped_router = router_with(module_name: full_module_name)
41
+ def scope(new_scope_name = nil, **options, &block)
42
+ scoped_router = router_with_scope_params(new_scope_name, **options)
43
43
  scoped_router.instance_eval(&block)
44
44
  routes.merge(scoped_router.routes)
45
45
  end
46
46
 
47
+ def namespace(namespace_name, &block)
48
+ scope(path: namespace_name, module: namespace_name, &block)
49
+ end
50
+
47
51
  def resources(name, **options, &block)
48
52
  builder_options = full_route_options(options)
49
53
  routes_builder = ResourceRoutesBuilder.new(name, **builder_options)
@@ -87,13 +91,25 @@ module GraphqlRails
87
91
 
88
92
  attr_reader :module_name, :group_names
89
93
 
94
+ def router_with_scope_params(new_scope_name, **options)
95
+ new_scope_name ||= options[:path]
96
+
97
+ full_module_name = [module_name, options[:module]].select(&:present?).join('/')
98
+ full_scope_names = [*scope_names, new_scope_name].select(&:present?)
99
+
100
+ router_with(module_name: full_module_name, scope_names: full_scope_names)
101
+ end
102
+
90
103
  def router_with(new_router_options = {})
91
- default_options = { module_name: module_name, group_names: group_names }
92
- full_options = default_options.merge(new_router_options)
104
+ full_options = default_router_options.merge(new_router_options)
93
105
 
94
106
  self.class.new(**full_options)
95
107
  end
96
108
 
109
+ def default_router_options
110
+ { module_name: module_name, group_names: group_names, scope_names: scope_names }
111
+ end
112
+
97
113
  def add_raw_action(name, *args, &block)
98
114
  raw_graphql_actions << { name: name, args: args, block: block }
99
115
  end
@@ -111,7 +127,7 @@ module GraphqlRails
111
127
  end
112
128
 
113
129
  def default_route_options
114
- { module: module_name, on: :member }
130
+ { module: module_name, on: :member, scope_names: scope_names }
115
131
  end
116
132
  end
117
133
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module GraphqlRails
4
- VERSION = '2.2.0'
4
+ VERSION = '2.3.0'
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: graphql_rails
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.2.0
4
+ version: 2.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Povilas Jurčys
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2022-01-25 00:00:00.000000000 Z
11
+ date: 2022-11-25 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: graphql
@@ -156,7 +156,6 @@ files:
156
156
  - docs/components/decorator.md
157
157
  - docs/components/model.md
158
158
  - docs/components/routes.md
159
- - docs/getting_started/quick_start.md
160
159
  - docs/getting_started/setup.md
161
160
  - docs/index.html
162
161
  - docs/logging_and_monitoring/logging_and_monitoring.md
@@ -220,6 +219,7 @@ files:
220
219
  - lib/graphql_rails/query_runner.rb
221
220
  - lib/graphql_rails/railtie.rb
222
221
  - lib/graphql_rails/router.rb
222
+ - lib/graphql_rails/router/build_schema_action_type.rb
223
223
  - lib/graphql_rails/router/mutation_route.rb
224
224
  - lib/graphql_rails/router/plain_cursor_encoder.rb
225
225
  - lib/graphql_rails/router/query_route.rb
@@ -255,7 +255,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
255
255
  - !ruby/object:Gem::Version
256
256
  version: '0'
257
257
  requirements: []
258
- rubygems_version: 3.2.15
258
+ rubygems_version: 3.3.7
259
259
  signing_key:
260
260
  specification_version: 4
261
261
  summary: Rails style structure for GraphQL API.
@@ -1,62 +0,0 @@
1
- # Quick Start
2
-
3
- ## Generate initial code
4
-
5
- ```bash
6
- bundle exec rails g graphql_rails:install
7
- ```
8
-
9
- ## Define GraphQL schema as RoR routes
10
-
11
- ```ruby
12
- # config/graphql/routes.rb
13
- GraphqlRails::Router.draw do
14
- # will create createUser, updateUser, destroyUser mutations and user, users queries.
15
- # expects that UsersController class exist
16
- resources :users
17
-
18
- # if you want custom queries or mutation
19
- query 'searchLogs', to: 'logs#search' # redirects request to LogsController
20
- mutation 'changeUserPassword', to: 'users#change_password'
21
- end
22
- ```
23
-
24
- ## Define your Graphql model
25
-
26
- ```ruby
27
- class User # works with any class including ActiveRecord
28
- include GraphqlRails::Model
29
-
30
- graphql do |c|
31
- # most common attributes, like :id, :name, :title has default type, so you don't have to specify it (but you can!)
32
- c.attribute :id
33
-
34
- c.attribute :email, :string
35
- c.attribute :surname, :string
36
- end
37
- end
38
- ```
39
-
40
- ## Define controller
41
-
42
- ```ruby
43
- class UsersController < ApplicationGraphqlController
44
- # graphql requires to describe which attributes controller action accepts and which returns
45
- action(:change_user_password)
46
- .permit(:password!, :id!) # Bang (!) indicates that attribute is required
47
-
48
- def change_user_password
49
- user = User.find(params[:id])
50
- user.update!(password: params[:password])
51
-
52
- # returned value needs to have all methods defined in model `graphql do` part
53
- user # or SomeDecorator.new(user)
54
- end
55
-
56
- action(:search).permit(search_fields!: SearchFieldsInput) # you can specify your own input fields
57
- def search
58
- end
59
- end
60
- ```
61
-
62
- Congrats, you are done!