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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: d228d49d49c7960ee308e30a8323b652c8c35911
4
- data.tar.gz: 6bb7c19653461926355c9a832d54ec99cf595e54
2
+ SHA256:
3
+ metadata.gz: 202dc11b523d09002098ed66bcb2bc734a6e4f85fe6ab9a2a7f94cac5b295be8
4
+ data.tar.gz: b824d121adb074e1ffc93d3a42f9f1d976c2a854970c904e49b98644b8282dbb
5
5
  SHA512:
6
- metadata.gz: '09e53ab5da54bad1ec25dd9c88909b14a3f63ecd39da55b66c94de7419b5fe0e1f64fcafb3b5af94797cbe9bb0f65ef706db69399652432dfa3f9342e3b167dc'
7
- data.tar.gz: ecda93af4ec0a88e2a5c69cb56dc90e13dde9f64eef14656cc26c5823ddfee2ca01089b02ce3c2cd1c0d8b5a3e57525715524310f7abf010b89f45842b13a4e1
6
+ metadata.gz: 1ca1c6ff7d958e446daa4c7df652728d86157f502a9681e5a3e90e5fc80c6ae433be93120099cb3e15e0b3f62c2d3c4497a7b187ebe2dce30dd185ce2c26bac1
7
+ data.tar.gz: d89ab5d2beddfa96b0937aa6bd414e8941ceb41b9b1748aba5600b895b1e16f5a06f2dab0748d9c2b256df0b661aaf11c7ffcb5404830cf0ea12d95a4041bd42
data/README.md CHANGED
@@ -1,8 +1,14 @@
1
+ [![Gem Version](https://badge.fury.io/rb/graphqr.svg)](https://rubygems.org/gems/graphqr)
2
+ [![Build Status](https://travis-ci.com/QultureRocks/graphqr.svg?branch=master)](https://travis-ci.com/QultureRocks/graphqr)
3
+ [![Maintainability](https://api.codeclimate.com/v1/badges/7f7b51e89e8fe4de1b23/maintainability)](https://codeclimate.com/github/QultureRocks/graphqr/maintainability)
4
+
1
5
  # GraphQR
2
6
 
3
- Welcome to your new gem! In this directory, you'll find the files you need to be able to package up your Ruby library into a gem. Put your Ruby code in the file `lib/graphqr`. To experiment with that code, run `bin/console` for an interactive prompt.
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
- If you'd like to use the pagination feature, you must have `pagy` installed.
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
- ## Usage
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
 
@@ -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'
@@ -2,8 +2,15 @@
2
2
 
3
3
  module GraphQR
4
4
  ##
5
- # TODO: add documentation
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
- # TODO: add documentation
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
@@ -2,7 +2,12 @@
2
2
 
3
3
  module GraphQR
4
4
  ##
5
- # TODO: add documentation
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
- # TODO: add documentation
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
- DEFAULT_PAGINATION_ERROR = 'No paginator defined'
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, DEFAULT_PAGINATION_ERROR unless GraphQR.paginator.present?
30
+ raise GraphQL::ExecutionError, NO_PAGINATOR_ERROR unless GraphQR.paginator.present?
27
31
 
28
- Resolvers::PagyResolver.new(value, items: arguments[:per], page: arguments[:page]) if GraphQR.use_pagy?
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
- # TODO: add documentation
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
- # TODO: add documentation
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
- # TODO: add documentation
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
- # TODO: add documentation
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
 
@@ -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
- # TODO: add documentation
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 = collection(active_record_class, type_class, scope_class)
50
+ resolver = collection_resolver(active_record_class, type_class, scope_class)
14
51
  else
15
- resolver = resource(active_record_class, type_class)
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 collection(active_record_class, type_class, scope_class)
24
- Class.new(::BaseResolver) do
25
- class_attribute :active_record_class
26
- self.active_record_class = active_record_class
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 resource(active_record_class, type_class)
42
- Class.new(::BaseResolver) do
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
- record = self.class.active_record_class.find(id)
52
-
53
- context[:policy_provider].allowed?(action: :show?, record: record)
75
+ @id = id
76
+ super()
77
+ end
54
78
 
55
- record
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
@@ -2,8 +2,15 @@
2
2
 
3
3
  module GraphQR
4
4
  ##
5
- # TODO: add documentation
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)
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module GraphQR
4
- VERSION = '0.0.3'
4
+ VERSION = '0.0.8'
5
5
  end
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.3
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: 2019-05-29 00:00:00.000000000 Z
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: '10.0'
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: '10.0'
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/base.rb
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
- rubyforge_project:
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
@@ -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