graphqr 0.0.3 → 0.0.8
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.
- checksums.yaml +5 -5
- data/README.md +252 -5
- data/lib/graphqr.rb +14 -1
- data/lib/graphqr/authorized.rb +8 -1
- data/lib/graphqr/base_resolver.rb +11 -0
- data/lib/graphqr/base_resolvers.rb +83 -0
- data/lib/graphqr/configuration.rb +19 -1
- data/lib/graphqr/fields/base_field.rb +2 -2
- data/lib/graphqr/pagination.rb +6 -1
- data/lib/graphqr/pagination/pagination_extension.rb +17 -4
- data/lib/graphqr/pagination/resolvers/pagy_resolver.rb +5 -2
- data/lib/graphqr/pagination/resolvers/record_page_number_resolver.rb +29 -0
- data/lib/graphqr/pagination/types/pagination_page_info_type.rb +2 -1
- data/lib/graphqr/permitted_fields_extension.rb +4 -1
- data/lib/graphqr/policies/authorize_graphql.rb +29 -0
- data/lib/graphqr/policies/pundit_provider.rb +11 -1
- data/lib/graphqr/query_field.rb +54 -30
- data/lib/graphqr/relation_fields.rb +41 -0
- data/lib/graphqr/scope_items.rb +8 -1
- data/lib/graphqr/version.rb +1 -1
- metadata +11 -8
- data/lib/graphqr/base.rb +0 -32
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 202dc11b523d09002098ed66bcb2bc734a6e4f85fe6ab9a2a7f94cac5b295be8
|
4
|
+
data.tar.gz: b824d121adb074e1ffc93d3a42f9f1d976c2a854970c904e49b98644b8282dbb
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 1ca1c6ff7d958e446daa4c7df652728d86157f502a9681e5a3e90e5fc80c6ae433be93120099cb3e15e0b3f62c2d3c4497a7b187ebe2dce30dd185ce2c26bac1
|
7
|
+
data.tar.gz: d89ab5d2beddfa96b0937aa6bd414e8941ceb41b9b1748aba5600b895b1e16f5a06f2dab0748d9c2b256df0b661aaf11c7ffcb5404830cf0ea12d95a4041bd42
|
data/README.md
CHANGED
@@ -1,8 +1,14 @@
|
|
1
|
+
[](https://rubygems.org/gems/graphqr)
|
2
|
+
[](https://travis-ci.com/QultureRocks/graphqr)
|
3
|
+
[](https://codeclimate.com/github/QultureRocks/graphqr/maintainability)
|
4
|
+
|
1
5
|
# GraphQR
|
2
6
|
|
3
|
-
|
7
|
+
A compilation of useful extensions and helpers for [graphql-ruby](https://github.com/rmosolgo/graphql-ruby).
|
8
|
+
|
9
|
+
- [API Documentation](https://qulturerocks.github.io/graphqr/)
|
10
|
+
- [Qulture.Rocks](https://qulture.rocks)
|
4
11
|
|
5
|
-
TODO: Delete this and the text above, and describe your gem
|
6
12
|
|
7
13
|
## Installation
|
8
14
|
|
@@ -20,11 +26,252 @@ Or install it yourself as:
|
|
20
26
|
|
21
27
|
$ gem install graphqr
|
22
28
|
|
23
|
-
|
29
|
+
## Configuration (optional)
|
30
|
+
|
31
|
+
GraphQR uses `pagy` and `pundit` by default and activates both `Authorization` and `Pagination` modules.
|
32
|
+
If you'd like to create a specific configuration, create `config/initializers/graphql.rb` with
|
33
|
+
|
34
|
+
```ruby
|
35
|
+
GraphQR.configure do |config|
|
36
|
+
config.use_pagination = true # or false to disable
|
37
|
+
config.use_authorization = true # or false to disable
|
38
|
+
|
39
|
+
config.paginator = :pagy # only pagy is available for now
|
40
|
+
config.policy_provider = :pundit # only pundit is available for now
|
41
|
+
end
|
42
|
+
```
|
43
|
+
|
44
|
+
## Modules
|
45
|
+
|
46
|
+
To use the extensions correctly add
|
47
|
+
```ruby
|
48
|
+
field_class GraphQR::Fields::BaseField
|
49
|
+
```
|
50
|
+
to your `BaseObject` class. This will add the custom options to your fields and add the necessary extensions according to the modules you activated.
|
51
|
+
|
52
|
+
```ruby
|
53
|
+
module Types
|
54
|
+
class BaseObject < GraphQL::Schema::Object
|
55
|
+
...
|
56
|
+
field_class GraphQR::Fields::BaseField
|
57
|
+
...
|
58
|
+
end
|
59
|
+
end
|
60
|
+
```
|
61
|
+
### Pagination
|
62
|
+
|
63
|
+
The Pagination module consists in a easier way of dealing with pages. Instead of using `cursors` we implemented a more Rails way using `per` and `page`.
|
64
|
+
Our implementation is (for now) based on [Pagy](https://github.com/ddnexus/pagy) so you must have it installed.
|
65
|
+
|
66
|
+
To use the Pagination module add
|
67
|
+
|
68
|
+
```ruby
|
69
|
+
|
70
|
+
extend GraphQR::Pagination
|
71
|
+
```
|
72
|
+
|
73
|
+
to any `GraphQL::Schema::Object` you'd like, but we recommend adding it to your `BaseObject` class.
|
74
|
+
|
75
|
+
```ruby
|
76
|
+
module Types
|
77
|
+
class BaseObject < GraphQL::Schema::Object
|
78
|
+
...
|
79
|
+
extend GraphQR::Pagination
|
80
|
+
...
|
81
|
+
end
|
82
|
+
end
|
83
|
+
```
|
84
|
+
|
85
|
+
#### Usage
|
86
|
+
|
87
|
+
```ruby
|
88
|
+
module Types
|
89
|
+
class QueryType < Types::BaseObject
|
90
|
+
graphql_name 'Query'
|
91
|
+
|
92
|
+
field :users, UserType.pagination_type, paginate: true
|
93
|
+
end
|
94
|
+
end
|
95
|
+
```
|
96
|
+
|
97
|
+
A `pagination_type` adds the `per` and `page` arguments and adds a `page_info` field to the response.
|
98
|
+
|
99
|
+
Example gql:
|
100
|
+
```
|
101
|
+
users(per: 10, page: 1) {
|
102
|
+
nodes {
|
103
|
+
id
|
104
|
+
name
|
105
|
+
}
|
106
|
+
}
|
107
|
+
```
|
108
|
+
|
109
|
+
### Authorization
|
110
|
+
|
111
|
+
The Authorization module in some wrappers around a `PolicyProvider` (only Pundit for now). And allows some basic behaviors.
|
112
|
+
Everything on this module depends on a `policy_provider` passed to the GraphQL context. You can add it like this:
|
113
|
+
|
114
|
+
```ruby
|
115
|
+
class GraphqlController < ApplicationController
|
116
|
+
|
117
|
+
def execute
|
118
|
+
context = {
|
119
|
+
your_context,
|
120
|
+
policy_provider: policy_provider
|
121
|
+
}
|
122
|
+
result = YourSchema.execute(query, variables: variables, context: context, operation_name: operation_name)
|
123
|
+
render json: result.to_json
|
124
|
+
rescue StandardError => e
|
125
|
+
raise e unless Rails.env.development?
|
126
|
+
|
127
|
+
handle_error_in_development(e)
|
128
|
+
end
|
129
|
+
...
|
130
|
+
end
|
131
|
+
```
|
132
|
+
|
133
|
+
#### Authorized
|
134
|
+
|
135
|
+
This module adds a check on the object policy before resolving it. It always searches for the `show?` policy of the record.
|
136
|
+
It works by extending the `authorized?` method.
|
137
|
+
|
138
|
+
To add this behavior, add
|
139
|
+
```ruby
|
140
|
+
extend GraphQR::Authorized
|
141
|
+
```
|
142
|
+
to any `GraphQL::Schema::Object` you'd like, but we recommend adding it to your `BaseObject` class.
|
143
|
+
|
144
|
+
```ruby
|
145
|
+
module Types
|
146
|
+
class BaseObject < GraphQL::Schema::Object
|
147
|
+
...
|
148
|
+
extend GraphQR::Authorized
|
149
|
+
...
|
150
|
+
end
|
151
|
+
end
|
152
|
+
```
|
153
|
+
|
154
|
+
Example:
|
155
|
+
|
156
|
+
```
|
157
|
+
users {
|
158
|
+
id
|
159
|
+
tags {
|
160
|
+
id
|
161
|
+
}
|
162
|
+
}
|
163
|
+
```
|
164
|
+
|
165
|
+
In this case, the authorization check will run for each `user`, calling `UserPolicy.show?` and for each tag, calling `TagPolicy.show?`.
|
166
|
+
If any policy returns falsy, the object is returned as `null`.
|
167
|
+
|
168
|
+
#### ScopeItems
|
169
|
+
|
170
|
+
This module adds the PolicyProvider scope to the fields that represent an `ActiveRecord::Relation`. It works by implementing the `self.scope_items` method.
|
171
|
+
|
172
|
+
To add this behavior, add
|
173
|
+
```ruby
|
174
|
+
extend GraphQR::ScopeItems
|
175
|
+
```
|
176
|
+
to any `GraphQL::Schema::Object` you'd like, but we recommend adding it to your `BaseObject` class.
|
177
|
+
|
178
|
+
```ruby
|
179
|
+
module Types
|
180
|
+
class BaseObject < GraphQL::Schema::Object
|
181
|
+
...
|
182
|
+
extend GraphQR::ScopeItems
|
183
|
+
...
|
184
|
+
end
|
185
|
+
end
|
186
|
+
```
|
187
|
+
|
188
|
+
Example:
|
189
|
+
|
190
|
+
```
|
191
|
+
users {
|
192
|
+
id
|
193
|
+
}
|
194
|
+
```
|
195
|
+
|
196
|
+
In this case, the `users` list will be scoped using `UserPolicy::Scope` provided by Pundit.
|
197
|
+
|
198
|
+
#### AuthorizeGraphQL
|
199
|
+
|
200
|
+
This module is a wrapper around the PolicyProvider authorization.
|
201
|
+
It adds the `authorize_graphql` method, similar to Pundit's `authorize`, but it returns an `GraphQL::ExecutionError` instead of a `Pundit::NotAuthorizedError`
|
202
|
+
|
203
|
+
To add this behavior, add
|
204
|
+
```ruby
|
205
|
+
include GraphQR::Policies::AuthorizeGraphQL
|
206
|
+
```
|
207
|
+
where you want to use this methos, but we recommend adding it to your `Mutations` and `Resolvers` classes.
|
208
|
+
|
209
|
+
```ruby
|
210
|
+
class BaseResolver < GraphQL::Schema::Resolver
|
211
|
+
...
|
212
|
+
include GraphQR::Policies::AuthorizeGraphQL
|
213
|
+
...
|
214
|
+
end
|
215
|
+
```
|
216
|
+
|
217
|
+
Example:
|
218
|
+
|
219
|
+
```ruby
|
220
|
+
authorize_graphql User, :index?
|
221
|
+
```
|
222
|
+
|
223
|
+
### Helpers
|
224
|
+
|
225
|
+
We also provide some helpers to make implementing GraphQL on ruby easier.
|
226
|
+
|
227
|
+
#### ApplyScopes
|
228
|
+
|
229
|
+
This modules is based on the [has_scope](https://github.com/plataformatec/has_scope/) gem.
|
230
|
+
It provides an `apply_scopes` method that can search for model scopes and use them on a collection
|
231
|
+
|
232
|
+
To add this method, add
|
233
|
+
```ruby
|
234
|
+
include GraphQR::ApplyScopes
|
235
|
+
```
|
236
|
+
where you'd like to use it, but we recommend adding it to your `Resolvers`.
|
237
|
+
|
238
|
+
```ruby
|
239
|
+
class BaseResolver < GraphQL::Schema::Resolver
|
240
|
+
...
|
241
|
+
include GraphQR::ApplyScopes
|
242
|
+
...
|
243
|
+
end
|
244
|
+
```
|
245
|
+
|
246
|
+
Example:
|
247
|
+
|
248
|
+
```ruby
|
249
|
+
apply_scopes(User, { order_by_name: true, with_id: [1,2,3] })
|
250
|
+
```
|
251
|
+
|
252
|
+
#### QueryField
|
253
|
+
|
254
|
+
This module adds the `query_field` helper.
|
255
|
+
It adds an easy way of creating simple fields with resolvers.
|
256
|
+
|
257
|
+
To add this method, add
|
258
|
+
```ruby
|
259
|
+
extend GraphQR::QueryField
|
260
|
+
```
|
261
|
+
to your `BaseObject`.
|
262
|
+
|
263
|
+
```ruby
|
264
|
+
module Types
|
265
|
+
class BaseObject < GraphQL::Schema::Object
|
266
|
+
...
|
267
|
+
extend GraphQR::QueryField
|
268
|
+
...
|
269
|
+
end
|
270
|
+
end
|
271
|
+
```
|
24
272
|
|
25
|
-
|
273
|
+
Read more about its use in the [documentation](https://qulturerocks.github.io/graphqr/GraphQR/QueryField.html)
|
26
274
|
|
27
|
-
TODO: Write usage instructions here
|
28
275
|
|
29
276
|
## Development
|
30
277
|
|
data/lib/graphqr.rb
CHANGED
@@ -1,12 +1,21 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require 'graphql'
|
4
|
+
require 'graphqr/configuration'
|
4
5
|
|
5
6
|
##
|
6
7
|
# This module represents all the extensions we made to the graphql-ruby library
|
7
8
|
# it contains helpers and integrations we need to keep our workflow as simple as possible.
|
8
9
|
module GraphQR
|
9
10
|
class << self
|
11
|
+
def use_pagination
|
12
|
+
GraphQR.config.use_pagination || true
|
13
|
+
end
|
14
|
+
|
15
|
+
def use_authorization
|
16
|
+
GraphQR.config.use_authorization || true
|
17
|
+
end
|
18
|
+
|
10
19
|
def paginator
|
11
20
|
GraphQR.config.paginator
|
12
21
|
end
|
@@ -28,6 +37,7 @@ end
|
|
28
37
|
require 'graphqr/hooks'
|
29
38
|
|
30
39
|
require 'graphqr/fields/base_field'
|
40
|
+
require 'graphqr/pagination/resolvers/record_page_number_resolver'
|
31
41
|
require 'graphqr/pagination/types/pagination_page_info_type'
|
32
42
|
require 'graphqr/pagination/pagination_extension'
|
33
43
|
|
@@ -37,6 +47,7 @@ rescue NameError
|
|
37
47
|
Kernel.warn 'Pagy not found'
|
38
48
|
end
|
39
49
|
|
50
|
+
require 'graphqr/policies/authorize_graphql'
|
40
51
|
begin
|
41
52
|
require 'graphqr/policies/pundit_provider'
|
42
53
|
rescue LoadError
|
@@ -45,9 +56,11 @@ end
|
|
45
56
|
|
46
57
|
require 'graphqr/apply_scopes'
|
47
58
|
require 'graphqr/authorized'
|
48
|
-
require 'graphqr/base'
|
49
59
|
require 'graphqr/pagination'
|
50
60
|
require 'graphqr/permitted_fields_extension'
|
61
|
+
require 'graphqr/base_resolver'
|
62
|
+
require 'graphqr/base_resolvers'
|
51
63
|
require 'graphqr/query_field'
|
64
|
+
require 'graphqr/relation_fields'
|
52
65
|
require 'graphqr/scope_items'
|
53
66
|
require 'graphqr/version'
|
data/lib/graphqr/authorized.rb
CHANGED
@@ -2,8 +2,15 @@
|
|
2
2
|
|
3
3
|
module GraphQR
|
4
4
|
##
|
5
|
-
#
|
5
|
+
# This module is the authorization extension created with our PolicyProvider.
|
6
|
+
#
|
7
|
+
# To use it add `extend GraphQR::Authorized` on the `GraphQL::Schema::Object` you want it,
|
8
|
+
# or add it on your `BaseObject`
|
6
9
|
module Authorized
|
10
|
+
##
|
11
|
+
# The `authorized? `method always runs before resolving an object.
|
12
|
+
#
|
13
|
+
# Our implementation adds a check on the `show?` method from the record Policy.
|
7
14
|
def authorized?(object, context)
|
8
15
|
policy_provider = context[:policy_provider]
|
9
16
|
|
@@ -0,0 +1,11 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module GraphQR
|
4
|
+
##
|
5
|
+
# This is the base class for all resolvers defined by the `GraphQR` gem.
|
6
|
+
# It includes authorization and scoping extensions defined by the gem.
|
7
|
+
class BaseResolver < GraphQL::Schema::Resolver
|
8
|
+
include GraphQR::ApplyScopes
|
9
|
+
include GraphQR::Policies::AuthorizeGraphQL
|
10
|
+
end
|
11
|
+
end
|
@@ -0,0 +1,83 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module GraphQR
|
4
|
+
##
|
5
|
+
# The `BaseResolvers` module defines methods used by other extensions to define resolver classes.
|
6
|
+
# All resolvers defined by this module's methods inherit from `GraphQR::BaseResolver`.
|
7
|
+
module BaseResolvers
|
8
|
+
##
|
9
|
+
# The method defines and returns a resolver class meant for resolving a paginated ActiveRecordRelation.
|
10
|
+
# The returned class implements authorization, running the `PolicyProvider`'s' `index?` action
|
11
|
+
# and `authorized_records` scope.
|
12
|
+
#
|
13
|
+
# The defined resolver does not implement `#unscoped_collection`. Define it before adding the query to the schema**
|
14
|
+
#
|
15
|
+
# ### Params:
|
16
|
+
#
|
17
|
+
# +type_class+: the `GraphQL::Schema::Object` of the ActiveRecordRelation
|
18
|
+
#
|
19
|
+
# +scope_class+: a `GraphQL::Schema::InputObject` which defines arguments to be used by `ApplyScopes`
|
20
|
+
# #### Example:
|
21
|
+
# ```
|
22
|
+
# class ObjectScope < GraphQL::Schema::InputObject
|
23
|
+
# argument :with_relation_id, ID, required: false
|
24
|
+
# ...
|
25
|
+
# end
|
26
|
+
# ```
|
27
|
+
#
|
28
|
+
# ### Example:
|
29
|
+
#
|
30
|
+
# ```
|
31
|
+
# base_collection_resolver(ObjectType, ObjectScope)
|
32
|
+
# ```
|
33
|
+
def base_collection_resolver(type_class, scope_class)
|
34
|
+
Class.new(GraphQR::BaseResolver) do
|
35
|
+
type type_class.pagination_type, null: false
|
36
|
+
|
37
|
+
argument :filter, scope_class, required: false if scope_class.present?
|
38
|
+
|
39
|
+
def resolve(filter: {})
|
40
|
+
authorize_graphql unscoped_collection, :index?
|
41
|
+
|
42
|
+
collection = apply_scopes(unscoped_collection, filter)
|
43
|
+
context[:policy_provider].authorized_records(records: collection)
|
44
|
+
end
|
45
|
+
|
46
|
+
def unscoped_collection
|
47
|
+
raise NotImplementedError
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
##
|
53
|
+
# The method defines and returns a resolver class meant for resolving a single ActiveRecord
|
54
|
+
# The returned class implements authorization, running the `PolicyProvider`'s' `show`
|
55
|
+
#
|
56
|
+
# The defined resolver does not implement `#record`. Define it before adding the query to the schema**
|
57
|
+
#
|
58
|
+
# ### Params:
|
59
|
+
#
|
60
|
+
# +type_class+: the `GraphQL::Schema::Object` of the ActiveRecord
|
61
|
+
#
|
62
|
+
# ### Example:
|
63
|
+
#
|
64
|
+
# ```
|
65
|
+
# base_resource_resolver(ObjectType)
|
66
|
+
# ```
|
67
|
+
def base_resource_resolver(type_class)
|
68
|
+
Class.new(GraphQR::BaseResolver) do
|
69
|
+
type type_class, null: false
|
70
|
+
|
71
|
+
def resolve
|
72
|
+
context[:policy_provider].allowed?(action: :show?, record: record)
|
73
|
+
|
74
|
+
record
|
75
|
+
end
|
76
|
+
|
77
|
+
def record
|
78
|
+
raise NotImplementedError
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
@@ -2,12 +2,30 @@
|
|
2
2
|
|
3
3
|
module GraphQR # rubocop:disable Style/Documentation
|
4
4
|
##
|
5
|
-
#
|
5
|
+
# Module responsible for global configuration of the gem
|
6
6
|
class Configuration
|
7
|
+
attr_writer :use_pagination, :use_authorization
|
8
|
+
|
7
9
|
def configure
|
8
10
|
yield self
|
9
11
|
end
|
10
12
|
|
13
|
+
def use_pagination
|
14
|
+
if instance_variable_defined? :@use_pagination
|
15
|
+
@use_pagination
|
16
|
+
else
|
17
|
+
@use_pagination = true
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def use_authorization
|
22
|
+
if instance_variable_defined? :@use_authorization
|
23
|
+
@use_authorization
|
24
|
+
else
|
25
|
+
@use_authorization = true
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
11
29
|
##
|
12
30
|
# Returns the selected paginator.
|
13
31
|
# If no paginator is selected, it tries to find the one used
|
@@ -18,8 +18,8 @@ module GraphQR
|
|
18
18
|
class BaseField < GraphQL::Schema::Field
|
19
19
|
def initialize(*args, paginate: false, **kwargs, &block)
|
20
20
|
super(*args, **kwargs, &block)
|
21
|
-
extension(Pagination::PaginationExtension) if paginate
|
22
|
-
extension(PermittedFieldsExtension, null: kwargs[:null])
|
21
|
+
extension(Pagination::PaginationExtension) if paginate && GraphQR.use_pagination
|
22
|
+
extension(PermittedFieldsExtension, null: kwargs[:null]) if GraphQR.use_authorization
|
23
23
|
end
|
24
24
|
end
|
25
25
|
end
|
data/lib/graphqr/pagination.rb
CHANGED
@@ -2,7 +2,12 @@
|
|
2
2
|
|
3
3
|
module GraphQR
|
4
4
|
##
|
5
|
-
#
|
5
|
+
# This module adds the GraphQL pagination types.
|
6
|
+
#
|
7
|
+
# When a field is paginated, the field `page_info` is always included with some pagination information.
|
8
|
+
#
|
9
|
+
# To use this module use `extend GraphQR::Pagination` on the GraphQL::Schema::Object you want it,
|
10
|
+
# or in your `BaseObject`
|
6
11
|
module Pagination
|
7
12
|
def pagination_type
|
8
13
|
@pagination_type ||= begin
|
@@ -3,9 +3,13 @@
|
|
3
3
|
module GraphQR
|
4
4
|
module Pagination
|
5
5
|
##
|
6
|
-
#
|
6
|
+
# The PaginationExtension is used on the `GraphQR::Fields::BaseField`.
|
7
|
+
#
|
8
|
+
# It adds the `per` and `page` arguments to the paginated field and uses the selected paginator resolver to add
|
9
|
+
# `nodes`, `edges` and `page_info` on the response
|
7
10
|
class PaginationExtension < GraphQL::Schema::FieldExtension
|
8
|
-
|
11
|
+
NO_PAGINATOR_ERROR = 'No paginator defined'
|
12
|
+
INVALID_PAGINATOR_ERROR = 'Invalid paginator'
|
9
13
|
|
10
14
|
def apply
|
11
15
|
field.argument :per, 'Int', required: false, default_value: 25,
|
@@ -23,9 +27,18 @@ module GraphQR
|
|
23
27
|
end
|
24
28
|
|
25
29
|
def after_resolve(value:, arguments:, **_kwargs)
|
26
|
-
raise GraphQL::ExecutionError,
|
30
|
+
raise GraphQL::ExecutionError, NO_PAGINATOR_ERROR unless GraphQR.paginator.present?
|
27
31
|
|
28
|
-
|
32
|
+
call_resolver(value, arguments)
|
33
|
+
end
|
34
|
+
|
35
|
+
def call_resolver(value, arguments)
|
36
|
+
case GraphQR.paginator
|
37
|
+
when :pagy
|
38
|
+
Resolvers::PagyResolver.new(value, items: arguments[:per], page: arguments[:page])
|
39
|
+
else
|
40
|
+
raise GraphQL::ExecutionError, INVALID_PAGINATOR_ERROR
|
41
|
+
end
|
29
42
|
end
|
30
43
|
end
|
31
44
|
end
|
@@ -4,7 +4,7 @@ module GraphQR
|
|
4
4
|
module Pagination
|
5
5
|
module Resolvers
|
6
6
|
##
|
7
|
-
#
|
7
|
+
# This is a resolver that uses `Pagy::Backend` and maps it to the GraphQL pagination structure.
|
8
8
|
class PagyResolver
|
9
9
|
include Pagy::Backend
|
10
10
|
|
@@ -33,7 +33,10 @@ module GraphQR
|
|
33
33
|
end
|
34
34
|
|
35
35
|
def page_info
|
36
|
-
@pagy
|
36
|
+
@pagy.tap do |pagy|
|
37
|
+
pagy.class_eval { attr_accessor :ordered_record_ids_proc }
|
38
|
+
pagy.ordered_record_ids_proc = -> { @records&.all? { |r| r&.respond_to?(:id) } ? @records.map(&:id) : [] }
|
39
|
+
end
|
37
40
|
end
|
38
41
|
end
|
39
42
|
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module GraphQR
|
4
|
+
module Pagination
|
5
|
+
module Resolvers
|
6
|
+
##
|
7
|
+
# This is a resolver that receives the id of an object and returns the page number
|
8
|
+
# in which the object is located.
|
9
|
+
class RecordPageNumberResolver < ::GraphQL::Schema::Resolver
|
10
|
+
INDEX_OFFSET = 1
|
11
|
+
|
12
|
+
type Int, null: true
|
13
|
+
|
14
|
+
argument :record_id, ID, required: true
|
15
|
+
|
16
|
+
def resolve(record_id:)
|
17
|
+
per_page = object.vars[:items]
|
18
|
+
records_ids = object.ordered_record_ids_proc.call
|
19
|
+
record_index = records_ids.find_index(record_id.to_i)
|
20
|
+
|
21
|
+
return if per_page.zero? || records_ids.blank? || record_index.blank?
|
22
|
+
|
23
|
+
record_position = (record_index + INDEX_OFFSET).to_f
|
24
|
+
(record_position / per_page).ceil
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -4,7 +4,7 @@ module GraphQR
|
|
4
4
|
module Pagination
|
5
5
|
module Types
|
6
6
|
##
|
7
|
-
#
|
7
|
+
# This defines the information about pagination in a connection.
|
8
8
|
class PaginationPageInfoType < GraphQL::Schema::Object
|
9
9
|
description 'Information about pagination in a connection.'
|
10
10
|
|
@@ -25,6 +25,7 @@ module GraphQR
|
|
25
25
|
field :next_page, Int, null: true,
|
26
26
|
method: :next,
|
27
27
|
description: 'The previous page number or nil if there is no previous page'
|
28
|
+
field :record_page_number, resolver: GraphQR::Pagination::Resolvers::RecordPageNumberResolver
|
28
29
|
end
|
29
30
|
end
|
30
31
|
end
|
@@ -2,7 +2,10 @@
|
|
2
2
|
|
3
3
|
module GraphQR
|
4
4
|
##
|
5
|
-
#
|
5
|
+
# This is an extension used on the `GraphQR::Fields::BaseField`.
|
6
|
+
#
|
7
|
+
# It is responsible for authorizing each field within a query.
|
8
|
+
# It searches if the field is defined on the `permitted_fields` method of the policy
|
6
9
|
class PermittedFieldsExtension < GraphQL::Schema::FieldExtension
|
7
10
|
def resolve(object:, arguments:, context:)
|
8
11
|
if authorized?(object, context)
|
@@ -0,0 +1,29 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module GraphQR
|
4
|
+
module Policies
|
5
|
+
##
|
6
|
+
# The AuthorizeGraphQL module defines a way of running the PolicyProvider authorization with a specific action
|
7
|
+
module AuthorizeGraphQL
|
8
|
+
DEFAULT_AUTHORIZATION_ERROR = 'You are not authorized to perform this action'
|
9
|
+
|
10
|
+
##
|
11
|
+
# This method is a wrapper around the Pundit authorize, receiving the same arguments.
|
12
|
+
# The only difference is that it turns the Pundit::NotAuthorizedError into a GraphQL::ExecutionError
|
13
|
+
#
|
14
|
+
# ### Example:
|
15
|
+
#
|
16
|
+
# ```
|
17
|
+
# authorize_graphql User, :index?
|
18
|
+
# ```
|
19
|
+
def authorize_graphql(record, action, policy_class: nil)
|
20
|
+
args = { record: record, action: action, policy_class: policy_class }
|
21
|
+
raise GraphQL::ExecutionError, DEFAULT_AUTHORIZATION_ERROR unless policy_provider.allowed?(args)
|
22
|
+
end
|
23
|
+
|
24
|
+
def policy_provider
|
25
|
+
context[:policy_provider]
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -5,7 +5,17 @@ require 'pundit'
|
|
5
5
|
module GraphQR
|
6
6
|
module Policies
|
7
7
|
##
|
8
|
-
#
|
8
|
+
# This is a wrapper around Pundit provided to keep all PolicyProviders with the same methods.
|
9
|
+
#
|
10
|
+
# If you want to use the Pundit integration with our extensions you should pass:
|
11
|
+
#
|
12
|
+
# ```
|
13
|
+
# {
|
14
|
+
# policy_provider: GraphQR::Policies::PunditProvider.new(policy_context: pundit_user)
|
15
|
+
# }
|
16
|
+
# ```
|
17
|
+
#
|
18
|
+
# To the Schema context.
|
9
19
|
class PunditProvider
|
10
20
|
attr_reader :policy_context
|
11
21
|
|
data/lib/graphqr/query_field.rb
CHANGED
@@ -1,61 +1,85 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
# rubocop:disable Metrics/ParameterLists
|
4
|
-
|
5
3
|
module GraphQR
|
6
4
|
##
|
7
|
-
#
|
5
|
+
# This extension adds the `query_field` method.
|
6
|
+
# A helper to create simple queries faster and easier
|
7
|
+
#
|
8
|
+
# To use this extension, add `extend Graphql::QueryField` on your `QueryType`
|
9
|
+
#
|
8
10
|
module QueryField
|
11
|
+
include BaseResolvers
|
12
|
+
##
|
13
|
+
# The `query_field` method is a helper to create fields and resolver without effort.
|
14
|
+
#
|
15
|
+
# ### Arguments
|
16
|
+
#
|
17
|
+
# +field_name+ _(required)_: the GraphQL query name
|
18
|
+
#
|
19
|
+
# +active_record_class+ _(required)_: the model ActiveRecord class.
|
20
|
+
# It can be represented as an array if you want it to return a collection
|
21
|
+
#
|
22
|
+
# +type_class+ _(required)_: The GraphQL type class
|
23
|
+
#
|
24
|
+
# +scope_class+: A specific InputType that contains the possible scopes that can be applied to your collection.
|
25
|
+
# Similar to the [has_scope](https://github.com/plataformatec/has_scope/) gem.
|
26
|
+
# _This argument is required for collection fields._
|
27
|
+
#
|
28
|
+
# ### Examples
|
29
|
+
# ```
|
30
|
+
# query_type :user, User, type_class: UserType
|
31
|
+
# query_type :users, [User], type_class: UserType, scope_class: UserScopeInput
|
32
|
+
# ```
|
33
|
+
#
|
34
|
+
# ### Collention fields
|
35
|
+
#
|
36
|
+
# Collection fields are always paginated using the configured `paginator`
|
37
|
+
# Its resolver will look for the `index?` method on the model Policy.
|
38
|
+
# It'll have the optional `filter` argument with `scope_class` type
|
39
|
+
#
|
40
|
+
# ### Single fields
|
41
|
+
#
|
42
|
+
# Single fields have the required `id` argument to find the exact record searched.
|
43
|
+
# Its resolver will look for the `show?` method on the model Policy.
|
44
|
+
#
|
45
|
+
# rubocop:disable Metrics/ParameterLists
|
9
46
|
def query_field(field_name, active_record_class, type_class:, scope_class: nil, **kwargs, &block)
|
10
47
|
is_collection = active_record_class.is_a? Array
|
11
48
|
if is_collection
|
12
49
|
active_record_class = active_record_class.first
|
13
|
-
resolver =
|
50
|
+
resolver = collection_resolver(active_record_class, type_class, scope_class)
|
14
51
|
else
|
15
|
-
resolver =
|
52
|
+
resolver = resource_resolver(active_record_class, type_class)
|
16
53
|
end
|
17
54
|
|
18
55
|
field(field_name, paginate: is_collection, resolver: resolver, **kwargs, &block)
|
19
56
|
end
|
57
|
+
# rubocop:enable Metrics/ParameterLists
|
20
58
|
|
21
59
|
private
|
22
60
|
|
23
|
-
def
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
type type_class.pagination_type, null: false
|
29
|
-
|
30
|
-
argument :filter, scope_class, required: false
|
31
|
-
|
32
|
-
def resolve(filter: {})
|
33
|
-
authorize_graphql active_record_class, :index?
|
34
|
-
|
35
|
-
collection = apply_scopes(active_record_class, filter)
|
36
|
-
context[:policy_provider].authorized_records(records: collection)
|
37
|
-
end
|
38
|
-
end
|
61
|
+
def collection_resolver(active_record_class, type_class, scope_class)
|
62
|
+
resolver = base_collection_resolver(type_class, scope_class)
|
63
|
+
resolver.define_method(:unscoped_collection) { @unscoped_collection ||= active_record_class }
|
64
|
+
resolver
|
39
65
|
end
|
40
66
|
|
41
|
-
def
|
42
|
-
Class.new(
|
67
|
+
def resource_resolver(active_record_class, type_class)
|
68
|
+
Class.new(base_resource_resolver(type_class)) do
|
43
69
|
class_attribute :active_record_class
|
44
70
|
self.active_record_class = active_record_class
|
45
71
|
|
46
|
-
type type_class, null: false
|
47
|
-
|
48
72
|
argument :id, 'ID', required: true
|
49
73
|
|
50
74
|
def resolve(id:)
|
51
|
-
|
52
|
-
|
53
|
-
|
75
|
+
@id = id
|
76
|
+
super()
|
77
|
+
end
|
54
78
|
|
55
|
-
|
79
|
+
def record
|
80
|
+
@record ||= active_record_class.find(@id)
|
56
81
|
end
|
57
82
|
end
|
58
83
|
end
|
59
84
|
end
|
60
85
|
end
|
61
|
-
# rubocop:enable Metrics/ParameterLists
|
@@ -0,0 +1,41 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module GraphQR
|
4
|
+
##
|
5
|
+
# @TODO doc
|
6
|
+
module RelationFields
|
7
|
+
##
|
8
|
+
# @TODO doc
|
9
|
+
# rubocop:disable Naming/PredicateName
|
10
|
+
include GraphQR::BaseResolvers
|
11
|
+
|
12
|
+
def has_many(field_name, type_class, scope_class: nil, **kwargs, &block)
|
13
|
+
type_class = type_class.first
|
14
|
+
|
15
|
+
resolver = has_many_resolver(field_name, type_class, scope_class)
|
16
|
+
|
17
|
+
field(field_name, paginate: true, resolver: resolver, **kwargs, &block)
|
18
|
+
end
|
19
|
+
|
20
|
+
def has_one(field_name, type_class, **kwargs, &block)
|
21
|
+
resolver = has_one_resolver(field_name, type_class)
|
22
|
+
|
23
|
+
field(field_name, resolver: resolver, **kwargs, &block)
|
24
|
+
end
|
25
|
+
|
26
|
+
private
|
27
|
+
|
28
|
+
def has_many_resolver(collection_name, type_class, scope_class)
|
29
|
+
resolver = base_collection_resolver(type_class, scope_class)
|
30
|
+
resolver.define_method(:unscoped_collection) { @unscoped_collection ||= object.send(collection_name) }
|
31
|
+
resolver
|
32
|
+
end
|
33
|
+
|
34
|
+
def has_one_resolver(resource_name, type_class)
|
35
|
+
resolver = base_resource_resolver(type_class)
|
36
|
+
resolver.define_method(:record) { @record ||= object.send(resource_name) }
|
37
|
+
resolver
|
38
|
+
end
|
39
|
+
# rubocop:enable Naming/PredicateName
|
40
|
+
end
|
41
|
+
end
|
data/lib/graphqr/scope_items.rb
CHANGED
@@ -2,8 +2,15 @@
|
|
2
2
|
|
3
3
|
module GraphQR
|
4
4
|
##
|
5
|
-
#
|
5
|
+
# This extension adds the PolicyProvider scope to the fields.
|
6
|
+
# When using the extension, ActiveRecord::Relation fields will be scoped.
|
7
|
+
#
|
8
|
+
# To use this extension add `extend GraphQR::ScopeItems` on the `GraphQL::Schema::Object` you want,
|
9
|
+
# or in your `BaseObject`
|
6
10
|
module ScopeItems
|
11
|
+
##
|
12
|
+
# The method checks whether the items are a ActiveRecord::Relation or not.
|
13
|
+
# If they are, it runs the PolicyProvider `authorized_records` scope.
|
7
14
|
def scope_items(items, context)
|
8
15
|
if scopable_items?(items)
|
9
16
|
context[:policy_provider].authorized_records(records: items)
|
data/lib/graphqr/version.rb
CHANGED
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: graphqr
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.8
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Manuel Puyol
|
@@ -10,7 +10,7 @@ authors:
|
|
10
10
|
autorequire:
|
11
11
|
bindir: bin
|
12
12
|
cert_chain: []
|
13
|
-
date:
|
13
|
+
date: 2020-05-18 00:00:00.000000000 Z
|
14
14
|
dependencies:
|
15
15
|
- !ruby/object:Gem::Dependency
|
16
16
|
name: graphql
|
@@ -52,14 +52,14 @@ dependencies:
|
|
52
52
|
requirements:
|
53
53
|
- - "~>"
|
54
54
|
- !ruby/object:Gem::Version
|
55
|
-
version: '
|
55
|
+
version: '13.0'
|
56
56
|
type: :development
|
57
57
|
prerelease: false
|
58
58
|
version_requirements: !ruby/object:Gem::Requirement
|
59
59
|
requirements:
|
60
60
|
- - "~>"
|
61
61
|
- !ruby/object:Gem::Version
|
62
|
-
version: '
|
62
|
+
version: '13.0'
|
63
63
|
- !ruby/object:Gem::Dependency
|
64
64
|
name: rdoc
|
65
65
|
requirement: !ruby/object:Gem::Requirement
|
@@ -144,17 +144,21 @@ files:
|
|
144
144
|
- lib/graphqr.rb
|
145
145
|
- lib/graphqr/apply_scopes.rb
|
146
146
|
- lib/graphqr/authorized.rb
|
147
|
-
- lib/graphqr/
|
147
|
+
- lib/graphqr/base_resolver.rb
|
148
|
+
- lib/graphqr/base_resolvers.rb
|
148
149
|
- lib/graphqr/configuration.rb
|
149
150
|
- lib/graphqr/fields/base_field.rb
|
150
151
|
- lib/graphqr/hooks.rb
|
151
152
|
- lib/graphqr/pagination.rb
|
152
153
|
- lib/graphqr/pagination/pagination_extension.rb
|
153
154
|
- lib/graphqr/pagination/resolvers/pagy_resolver.rb
|
155
|
+
- lib/graphqr/pagination/resolvers/record_page_number_resolver.rb
|
154
156
|
- lib/graphqr/pagination/types/pagination_page_info_type.rb
|
155
157
|
- lib/graphqr/permitted_fields_extension.rb
|
158
|
+
- lib/graphqr/policies/authorize_graphql.rb
|
156
159
|
- lib/graphqr/policies/pundit_provider.rb
|
157
160
|
- lib/graphqr/query_field.rb
|
161
|
+
- lib/graphqr/relation_fields.rb
|
158
162
|
- lib/graphqr/scope_items.rb
|
159
163
|
- lib/graphqr/version.rb
|
160
164
|
- spec/graphqr_spec.rb
|
@@ -178,11 +182,10 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
178
182
|
- !ruby/object:Gem::Version
|
179
183
|
version: '0'
|
180
184
|
requirements: []
|
181
|
-
|
182
|
-
rubygems_version: 2.6.14
|
185
|
+
rubygems_version: 3.0.6
|
183
186
|
signing_key:
|
184
187
|
specification_version: 4
|
185
188
|
summary: Extensions and helpers for graphql-ruby
|
186
189
|
test_files:
|
187
|
-
- spec/spec_helper.rb
|
188
190
|
- spec/graphqr_spec.rb
|
191
|
+
- spec/spec_helper.rb
|
data/lib/graphqr/base.rb
DELETED
@@ -1,32 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module GraphQR
|
4
|
-
##
|
5
|
-
# The Base module defines some helper methods that can be used once it is included
|
6
|
-
# it also includes the basic ApplyScopes library
|
7
|
-
module Base
|
8
|
-
include GraphQR::ApplyScopes
|
9
|
-
|
10
|
-
DEFAULT_AUTHORIZATION_ERROR = 'You are not authorized to perform this action'
|
11
|
-
|
12
|
-
##
|
13
|
-
# This method is a wrapper around the Pundit authorize, receiving the same arguments.
|
14
|
-
# The only difference is that it turns the Pundit::NotAuthorizedError into a GraphQL::ExecutionError
|
15
|
-
#
|
16
|
-
# ### Example:
|
17
|
-
#
|
18
|
-
# ```
|
19
|
-
# authorize_graphql User, :index?
|
20
|
-
# ```
|
21
|
-
def authorize_graphql(record, action, policy_class: nil)
|
22
|
-
args = { record: record, action: action, policy_class: policy_class }
|
23
|
-
raise GraphQL::ExecutionError, DEFAULT_AUTHORIZATION_ERROR unless policy_provider.allowed?(args)
|
24
|
-
end
|
25
|
-
|
26
|
-
##
|
27
|
-
# This is a helper method to get the policy provider from the context
|
28
|
-
def policy_provider
|
29
|
-
context[:policy_provider]
|
30
|
-
end
|
31
|
-
end
|
32
|
-
end
|