graphqr 0.0.2 → 0.0.7

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: 30f68dd1bcb6907786a2bd497b056fc44e5cd5ff
4
- data.tar.gz: 33e51b0fe16f514a13afe9be920abd2f1adf4c5b
2
+ SHA256:
3
+ metadata.gz: 56b827169ae4fc8970f688a1405c1cfce763fdb7619e2b5d64af996ae983de74
4
+ data.tar.gz: 5c6b08ea0a203fb0a3e65c7ccef3cdcf511355715fd38315ae50f536d0dcee83
5
5
  SHA512:
6
- metadata.gz: d7b24cac875d94516b5dd8f08c69f4b6086310127acaa50daf0baf15427b9ad3c69edd3c711fbb7fb86f641ca0a94d7ce06260c0e5263a1ccdff25e62ef157b8
7
- data.tar.gz: 3635d74b54fddd9b2753cd7d21872f4be500b4400060d899783b7c6d7ce88b09e50be1fbe56ce2c467c7fb3d715b68af276eedfd760a03f84bd283039a8eb257
6
+ metadata.gz: 6abd868b81b35aaae90b89544e66f0a8a33e75f468ed9004e0a102d331be8e7ebead88092db3dbb6bb17edc1d1b8e1de282fa64d1e597a96af069e57379a6ffe
7
+ data.tar.gz: 5c52447a2c4df95b8a14f3caed8b84170667302381ee0e3b8d071f4aa38b06a3d2dd5446a2c7a3be07ceaeeab04d08a49984033d4dd85e0f6bec04784d5f7029
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,25 +1,43 @@
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
13
22
 
23
+ def policy_provider
24
+ GraphQR.config.policy_provider
25
+ end
26
+
14
27
  def use_pagy?
15
28
  paginator == :pagy
16
29
  end
30
+
31
+ def use_pundit?
32
+ policy_provider == :pundit
33
+ end
17
34
  end
18
35
  end
19
36
 
20
37
  require 'graphqr/hooks'
21
38
 
22
39
  require 'graphqr/fields/base_field'
40
+ require 'graphqr/pagination/resolvers/record_page_number_resolver'
23
41
  require 'graphqr/pagination/types/pagination_page_info_type'
24
42
  require 'graphqr/pagination/pagination_extension'
25
43
 
@@ -29,11 +47,20 @@ rescue NameError
29
47
  Kernel.warn 'Pagy not found'
30
48
  end
31
49
 
50
+ require 'graphqr/policies/authorize_graphql'
51
+ begin
52
+ require 'graphqr/policies/pundit_provider'
53
+ rescue LoadError
54
+ Kernel.warn 'Pundit not found'
55
+ end
56
+
32
57
  require 'graphqr/apply_scopes'
33
58
  require 'graphqr/authorized'
34
- require 'graphqr/base'
35
59
  require 'graphqr/pagination'
36
60
  require 'graphqr/permitted_fields_extension'
61
+ require 'graphqr/base_resolver'
62
+ require 'graphqr/base_resolvers'
37
63
  require 'graphqr/query_field'
64
+ require 'graphqr/relation_fields'
38
65
  require 'graphqr/scope_items'
39
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,13 +2,33 @@
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
 
11
- # Returns the selected paginator
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
+
29
+ ##
30
+ # Returns the selected paginator.
31
+ # If no paginator is selected, it tries to find the one used
12
32
  def paginator
13
33
  if instance_variable_defined? :@paginator
14
34
  @paginator
@@ -29,11 +49,46 @@ module GraphQR # rubocop:disable Style/Documentation
29
49
  end
30
50
  end
31
51
 
52
+ ##
53
+ # Returns the selected policy_provider.
54
+ # If no policy_provider is selected, it tries to find the one used
55
+ def policy_provider
56
+ if instance_variable_defined? :@policy_provider
57
+ @policy_provider
58
+ else
59
+ set_policy_provider
60
+ end
61
+ end
62
+
63
+ ##
64
+ # Sets the preferred policy_provider
65
+ # TODO: support CanCan
66
+ def policy_provider=(policy_provider)
67
+ case policy_provider.to_sym
68
+ when :pundit
69
+ use_pundit
70
+ else
71
+ raise StandardError, "Unknown policy_provider: #{policy_provider}"
72
+ end
73
+ end
74
+
32
75
  private
33
76
 
34
77
  def set_paginator
35
78
  use_pagy if defined?(Pagy)
36
79
  end
80
+
81
+ def use_pagy
82
+ @paginator = :pagy
83
+ end
84
+
85
+ def set_policy_provider
86
+ use_pundit if defined?(Pundit)
87
+ end
88
+
89
+ def use_pundit
90
+ @policy_provider = :pundit
91
+ end
37
92
  end
38
93
 
39
94
  class << self
@@ -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 }
38
+ pagy.ordered_record_ids = @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
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
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'pundit'
4
+
5
+ module GraphQR
6
+ module Policies
7
+ ##
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.
19
+ class PunditProvider
20
+ attr_reader :policy_context
21
+
22
+ def initialize(policy_context:)
23
+ @policy_context = policy_context
24
+ end
25
+
26
+ def allowed?(action:, record:, policy_class: nil)
27
+ policy = policy_for(record: record, policy_class: policy_class)
28
+
29
+ policy.apply(action)
30
+ end
31
+
32
+ def authorized_records(records:)
33
+ Pundit.policy_scope(policy_context, records)
34
+ end
35
+
36
+ def permitted_field?(record:, field_name:)
37
+ policy = policy_for(record: record)
38
+
39
+ policy.permitted_fields.include?(field_name)
40
+ end
41
+
42
+ private
43
+
44
+ def policy_for(record:, policy_class: nil)
45
+ policy_class ||= policy_class_for(record: record)
46
+ policy_class.new(policy_context, record)
47
+ end
48
+
49
+ def policy_class_for(record:)
50
+ Pundit::PolicyFinder.new(record).policy!
51
+ end
52
+ end
53
+ end
54
+ end
@@ -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.2'
4
+ VERSION = '0.0.7'
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.2
4
+ version: 0.0.7
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-28 00:00:00.000000000 Z
13
+ date: 2020-02-04 00:00:00.000000000 Z
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency
16
16
  name: graphql
@@ -144,16 +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
159
+ - lib/graphqr/policies/pundit_provider.rb
156
160
  - lib/graphqr/query_field.rb
161
+ - lib/graphqr/relation_fields.rb
157
162
  - lib/graphqr/scope_items.rb
158
163
  - lib/graphqr/version.rb
159
164
  - spec/graphqr_spec.rb
@@ -177,8 +182,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
177
182
  - !ruby/object:Gem::Version
178
183
  version: '0'
179
184
  requirements: []
180
- rubyforge_project:
181
- rubygems_version: 2.6.14
185
+ rubygems_version: 3.0.6
182
186
  signing_key:
183
187
  specification_version: 4
184
188
  summary: Extensions and helpers for graphql-ruby
@@ -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