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 +4 -4
- data/.github/workflows/ruby.yml +1 -1
- data/.ruby-version +1 -1
- data/CHANGELOG.md +8 -0
- data/Gemfile.lock +8 -8
- data/docs/README.md +23 -45
- data/docs/_sidebar.md +0 -1
- data/docs/components/controller.md +15 -1
- data/docs/components/decorator.md +1 -1
- data/docs/components/model.md +62 -0
- data/docs/components/routes.md +41 -5
- data/lib/graphql_rails/attributes/attribute.rb +8 -14
- data/lib/graphql_rails/attributes/attribute_configurable.rb +15 -0
- data/lib/graphql_rails/attributes/input_attribute.rb +17 -1
- data/lib/graphql_rails/controller/build_controller_action_resolver.rb +2 -0
- data/lib/graphql_rails/controller.rb +1 -1
- data/lib/graphql_rails/decorator/relation_decorator.rb +22 -18
- data/lib/graphql_rails/decorator.rb +12 -4
- data/lib/graphql_rails/errors/system_error.rb +11 -1
- data/lib/graphql_rails/errors/validation_error.rb +14 -1
- data/lib/graphql_rails/router/build_schema_action_type.rb +112 -0
- data/lib/graphql_rails/router/route.rb +3 -2
- data/lib/graphql_rails/router/schema_builder.rb +3 -16
- data/lib/graphql_rails/router.rb +24 -8
- data/lib/graphql_rails/version.rb +1 -1
- metadata +4 -4
- data/docs/getting_started/quick_start.md +0 -62
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 9f06459a3c8c16e40dffd181d7c08128558e2bdca598845b9e94f51852681e61
|
4
|
+
data.tar.gz: 1a4ddc9e11a4c5ef22240f7e36b3070bcf89b712c561cad12e41ede1dc18fbc3
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 23e0d160c19e3c56c00a474cb77eff10c6a86c3ce1c63f3cdb87f4c374f3c10da79b5f2529a14a026f81e6f3b2c32e9b2fecd671bd7b38066e07d020c65fa93a
|
7
|
+
data.tar.gz: a0ade6d1ce611baf97ab0a631770273c98953b04eb3991e3234c12a12f21e893962b62a1785ba5da48de7193c366129d29732c7121fbe6c4404d5e1f05cedda2
|
data/.github/workflows/ruby.yml
CHANGED
data/.ruby-version
CHANGED
@@ -1 +1 @@
|
|
1
|
-
3.
|
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.
|
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.
|
84
|
+
graphql (1.13.17)
|
85
85
|
i18n (1.8.11)
|
86
86
|
concurrent-ruby (~> 1.0)
|
87
|
-
loofah (2.
|
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.
|
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.
|
105
|
-
mini_portile2 (~> 2.
|
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.
|
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' #
|
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
|
60
|
+
c.attribute(:id)
|
60
61
|
|
61
|
-
c.attribute
|
62
|
-
c.attribute
|
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
|
-
#
|
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
|
-
|
78
|
-
|
79
|
-
|
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
|
-
|
82
|
-
|
84
|
+
def index
|
85
|
+
User.all
|
83
86
|
end
|
84
87
|
|
85
|
-
|
86
|
-
.
|
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
|
-
|
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
@@ -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
|
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)
|
data/docs/components/model.md
CHANGED
@@ -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`:
|
data/docs/components/routes.md
CHANGED
@@ -89,18 +89,54 @@ end
|
|
89
89
|
|
90
90
|
### _module_ options
|
91
91
|
|
92
|
-
|
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
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
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
|
-
|
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
|
-
{
|
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)
|
@@ -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(
|
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
|
-
|
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
|
-
#
|
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
|
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: '',
|
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
|
-
|
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
|
data/lib/graphql_rails/router.rb
CHANGED
@@ -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
|
-
|
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
|
-
|
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
|
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.
|
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-
|
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.
|
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!
|