graphqr 0.0.3 → 0.0.8

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- 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