api-blocks 0.5.5 → 0.10.0

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
2
  SHA256:
3
- metadata.gz: 867eca270f42b7339503a286c597ee87a84416f5f6d8f1f845fa51675495feab
4
- data.tar.gz: c0dee15ccd07d8ee0463c2a7570af225771421c8e84b4e064135236a21c5704c
3
+ metadata.gz: 461a298ed182b8cf8ecc2f5d4bcdd8f3b481a15e61d52b36b2e44ab756036b8d
4
+ data.tar.gz: e29100b54ee368936e3dbbb03d4bcf47b39cdf27e85c0011fb427af4b5c82ab3
5
5
  SHA512:
6
- metadata.gz: 7ef452172c3fb99bfa2bf8dfc9e8058eb5c63d4b41c8dfa71ada4ab51363db90bd9a675eee101d924032fb2a96ffdc1fe4dac2eab0f136b277055a2930e90c64
7
- data.tar.gz: a28963d3e7ff821b29946411a86f4635aae07e17c536b1baa7a39d27353a41b54e3296240e2121475df125b8d639183d73558f0fe7a580582928be7aaed04770
6
+ metadata.gz: 4da508313a90671bbdc6f58b2c5663f73e221fc5bc7c9ea19a92e6eaed9cae099bf8a130dacbacc74e937015ee2b1ef0693f62df3c563f534ee36de9dbfb3be2
7
+ data.tar.gz: aa0915f46a5560f278b738c939f311eee70e8b5c8960ba64b309bf8ceb55d6acdddf715b07d7c72a6f67ee3093fd221ba6b2694caef546f295451d58d9280915
@@ -1,82 +1,90 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative 'join_keys'
4
+
3
5
  # Monkey-patch Blueprinter::AssociationExtractor to use `batch-loader` gem in
4
6
  # order to avoid n+1 queries when serializing associations.
5
7
  #
6
8
  # This does not support associations defined using a `proc` as
7
9
  # `options[:blueprint]`
8
10
  #
9
- class Blueprinter::AssociationExtractor < Blueprinter::Extractor
10
- alias_method :original_extract, :extract
11
+ module Blueprinter
12
+ class AssociationExtractor < Blueprinter::Extractor
13
+ alias original_extract extract
11
14
 
12
- def extract(association_name, object, local_options, options = {})
13
- association = object.association(association_name)
15
+ def extract(association_name, object, local_options, options = {})
16
+ return original_extract(association_name, object, local_options, options) unless options.fetch(:batch, true)
14
17
 
15
- if association.is_a?(ActiveRecord::Associations::HasManyThroughAssociation)
16
- return original_extract(association_name, object, local_options, options)
17
- end
18
+ association = object.association(association_name)
18
19
 
19
- if options[:blueprint].is_a?(Proc)
20
- raise "Cannot load blueprints with a `proc` blueprint option with batch-loader"
21
- end
20
+ if association.is_a?(ActiveRecord::Associations::HasManyThroughAssociation)
21
+ return original_extract(association_name, object, local_options, options)
22
+ end
22
23
 
23
- join_key = association.reflection.join_keys
24
- association_id = object.send(join_key.foreign_key)
25
- association_klass = association.reflection.class_name
24
+ raise 'Cannot load blueprints with a `proc` blueprint option with batch-loader' if options[:blueprint].is_a?(Proc)
26
25
 
27
- default_value = case association
28
- when ActiveRecord::Associations::HasManyAssociation
29
- []
30
- end
26
+ join_key = ::ApiBlocks::Blueprinter::JoinKeys.join_keys(
27
+ association.reflection
28
+ )
31
29
 
32
- view = options[:view] || :default
33
- scope = if options[:block].present?
34
- options[:block].call(object, local_options)
35
- else
36
- {}
37
- end
30
+ association_id = object.send(join_key.foreign_key)
31
+ association_klass = association.reflection.class_name
32
+
33
+ default_value = case association
34
+ when ActiveRecord::Associations::HasManyAssociation
35
+ []
36
+ end
37
+
38
+ view = options[:view] || :default
39
+ scope = if options[:block].present?
40
+ options[:block].call(object, local_options)
41
+ else
42
+ {}
43
+ end
38
44
 
39
- BatchLoader.for(association_id).batch(
40
- default_value: default_value,
41
- key: [association_name, association_klass, view, options[:blueprint], scope],
42
- ) do |ids, loader, args|
43
- model = association_klass.safe_constantize
44
- scope = args[:key].last
45
+ BatchLoader.for(association_id).batch(
46
+ default_value: default_value,
47
+ key: [association_name, association_klass, view, options[:blueprint], scope]
48
+ ) do |ids, loader, args|
49
+ model = association_klass.safe_constantize
50
+ scope = args[:key].last
45
51
 
46
- case association
47
- when ActiveRecord::Associations::HasManyAssociation
48
- model.where(join_key.key => ids).merge(scope).each do |record|
49
- loader.call(record.send(join_key.key)) do |memo|
50
- memo << render_blueprint(record, local_options, options)
52
+ case association
53
+ when ActiveRecord::Associations::HasManyAssociation
54
+ model.where(join_key.key => ids).merge(scope).each do |record|
55
+ loader.call(record.send(join_key.key)) do |memo|
56
+ memo << render_blueprint(record, local_options, options)
57
+ memo.uniq
58
+ end
51
59
  end
60
+ when ActiveRecord::Associations::HasOneAssociation
61
+ model.where(join_key.key => ids).merge(scope).each do |record|
62
+ loader.call(
63
+ record.send(join_key.key),
64
+ render_blueprint(record, local_options, options)
65
+ )
66
+ end
67
+ when ActiveRecord::Associations::BelongsToAssociation
68
+ model.where(join_key.key => ids).merge(scope).each do |record|
69
+ loader.call(
70
+ record.id,
71
+ render_blueprint(record, local_options, options)
72
+ )
73
+ end
74
+ else
75
+ raise "unsupported association kind #{association.class.name}"
52
76
  end
53
- when ActiveRecord::Associations::HasOneAssociation
54
- model.where(join_key.key => ids).merge(scope).each do |record|
55
- loader.call(
56
- record.send(join_key.key),
57
- render_blueprint(record, local_options, options)
58
- )
59
- end
60
- when ActiveRecord::Associations::BelongsToAssociation
61
- model.where(join_key.key => ids).merge(scope).each do |record|
62
- loader.call(
63
- record.id,
64
- render_blueprint(record, local_options, options)
65
- )
66
- end
67
- else
68
- raise "unsupported association kind #{association.class.name}"
69
77
  end
70
78
  end
71
- end
72
79
 
73
- private
80
+ private
74
81
 
75
- def render_blueprint(value, local_options, options = {})
76
- return default_value(options) if value.nil?
82
+ def render_blueprint(value, local_options, options = {})
83
+ return default_value(options) if value.nil?
77
84
 
78
- view = options[:view] || :default
79
- blueprint = association_blueprint(options[:blueprint], value)
80
- blueprint.prepare(value, view_name: view, local_options: local_options)
85
+ view = options[:view] || :default
86
+ blueprint = association_blueprint(options[:blueprint], value)
87
+ blueprint.prepare(value, view_name: view, local_options: local_options)
88
+ end
81
89
  end
82
90
  end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ # lib/join_keys.rb
4
+
5
+ module ApiBlocks
6
+ module Blueprinter
7
+ class JoinKeys
8
+ # Based on https://github.com/MaxLap/activerecord_where_assoc/blob/100318de80dea5f3c177526c3f824fda307ebc04/lib/active_record_where_assoc/active_record_compat.rb
9
+ if ActiveRecord.gem_version >= Gem::Version.new('6.1.0.rc1')
10
+ JoinKeys = Struct.new(:key, :foreign_key)
11
+ def self.join_keys(reflection)
12
+ JoinKeys.new(reflection.join_primary_key, reflection.join_foreign_key)
13
+ end
14
+
15
+ elsif ActiveRecord.gem_version >= Gem::Version.new('5.1')
16
+ def self.join_keys(reflection)
17
+ reflection.join_keys
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  # frozen_string_litreal: true
2
4
 
3
5
  require 'pundit'
@@ -17,74 +19,75 @@ require 'active_support/core_ext/module'
17
19
  # pundit_scope :api, :v1
18
20
  # end
19
21
  #
20
- module ApiBlocks::Controller
21
- extend ActiveSupport::Concern
22
-
23
- included do
24
- self.responder = ApiBlocks::Responder
22
+ module ApiBlocks
23
+ module Controller
24
+ extend ActiveSupport::Concern
25
25
 
26
- before_action :verify_request_format!
26
+ included do
27
+ self.responder = ApiBlocks::Responder
27
28
 
28
- include Pundit
29
- rescue_from Pundit::NotAuthorizedError, with: :render_forbidden_error
29
+ before_action :verify_request_format!
30
30
 
31
- # Enable pundit after_action hooks to ensure policies are consistently
32
- # used.
33
- after_action :verify_authorized
34
- after_action :verify_policy_scoped, except: :create
31
+ include Pundit
32
+ rescue_from Pundit::NotAuthorizedError, with: :render_forbidden_error
35
33
 
36
- # Override policy_scope to lookup pundit policies under the `scope`
37
- # namespace
38
- def policy_scope(scope)
39
- api_scope = self.class.inherited_pundit_api_scope || []
40
-
41
- super(api_scope + [scope])
42
- end
34
+ # Enable pundit after_action hooks to ensure policies are consistently
35
+ # used.
36
+ after_action :verify_authorized
37
+ after_action :verify_policy_scoped, except: :create
43
38
 
44
- # Override authorize to lookup pundit policies under the `scope`
45
- # namespace
46
- def authorize(record, query = nil)
47
- api_scope = self.class.inherited_pundit_api_scope || []
39
+ # Override policy_scope to lookup pundit policies under the `scope`
40
+ # namespace
41
+ def policy_scope(scope, policy_scope_class: nil)
42
+ api_scope = self.class.inherited_pundit_api_scope || []
48
43
 
49
- super(api_scope + [record], query)
50
- end
44
+ super(api_scope + [scope], policy_scope_class: policy_scope_class)
45
+ end
51
46
 
52
- handle_api_error Pundit::NotAuthorizedError do |error|
53
- [{ detail: error.message }, :forbidden]
54
- end
55
- end
47
+ # Override authorize to lookup pundit policies under the `scope`
48
+ # namespace
49
+ def authorize(record, query = nil, policy_class: nil)
50
+ api_scope = self.class.inherited_pundit_api_scope || []
56
51
 
57
- class_methods do
58
- # Returns the `pundit_api_scope` value that was defined last looking up into
59
- # the inheritance chain of the current class.
60
- def inherited_pundit_api_scope
61
- ancestors
62
- .select { |a| a.respond_to?(:pundit_api_scope) }
63
- .find(&:pundit_api_scope)
64
- .pundit_api_scope
65
- end
52
+ super(api_scope + [record], query, policy_class: policy_class)
53
+ end
66
54
 
67
- # Provide a default scope to pundit's `PolicyFinder`.
68
- def pundit_scope(*scope)
69
- @pundit_api_scope ||= scope
55
+ handle_api_error Pundit::NotAuthorizedError do |error|
56
+ [{ detail: error.message }, :forbidden]
57
+ end
70
58
  end
71
59
 
72
- def pundit_api_scope
73
- @pundit_api_scope
74
- end
60
+ class_methods do
61
+ # Returns the `pundit_api_scope` value that was defined last looking up into
62
+ # the inheritance chain of the current class.
63
+ def inherited_pundit_api_scope
64
+ ancestors
65
+ .select { |a| a.respond_to?(:pundit_api_scope) }
66
+ .find(&:pundit_api_scope)
67
+ .pundit_api_scope
68
+ end
75
69
 
70
+ # Provide a default scope to pundit's `PolicyFinder`.
71
+ def pundit_scope(*scope)
72
+ @pundit_api_scope ||= scope
73
+ end
76
74
 
77
- # Defines a error handler that returns
78
- def handle_api_error(error_class)
79
- rescue_from error_class do |ex|
80
- problem, status =
81
- if block_given?
82
- yield ex
83
- else
84
- [{ detail: ex.message }, :ok]
85
- end
75
+ def pundit_api_scope
76
+ @pundit_api_scope
77
+ end
86
78
 
87
- render problem: problem, status: status
79
+ # Defines a error handler that returns
80
+ def handle_api_error(error_class)
81
+ rescue_from error_class do |ex|
82
+ problem, status =
83
+ if block_given?
84
+ yield ex
85
+ else
86
+ [{ detail: ex.message }, :ok]
87
+ end
88
+
89
+ render problem: problem, status: status
90
+ end
88
91
  end
89
92
  end
90
93
  end
@@ -1,9 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # ApiBlocks::Doorkeeper implements API extensions for doorkeeper.
4
- module ApiBlocks::Doorkeeper
5
- extend ActiveSupport::Autoload
4
+ module ApiBlocks
5
+ module Doorkeeper
6
+ extend ActiveSupport::Autoload
6
7
 
7
- autoload :Passwords
8
- autoload :Invitations
8
+ autoload :Passwords
9
+ autoload :Invitations
10
+ end
9
11
  end
@@ -1,9 +1,13 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # ApiBlocks::Doorkeeper::Invitations implements an API invitation workflow.
4
- module ApiBlocks::Doorkeeper::Invitations
5
- extend ActiveSupport::Autoload
4
+ module ApiBlocks
5
+ module Doorkeeper
6
+ module Invitations
7
+ extend ActiveSupport::Autoload
6
8
 
7
- autoload :Controller
8
- autoload :Application
9
+ autoload :Controller
10
+ autoload :Application
11
+ end
12
+ end
9
13
  end
@@ -8,10 +8,16 @@
8
8
  #
9
9
  # @private
10
10
  #
11
- module ApiBlocks::Doorkeeper::Invitations::Application
12
- extend ActiveSupport::Concern
11
+ module ApiBlocks
12
+ module Doorkeeper
13
+ module Invitations
14
+ module Application
15
+ extend ActiveSupport::Concern
13
16
 
14
- included do
15
- validates :invitation_uri, "doorkeeper/redirect_uri": true
17
+ included do
18
+ validates :invitation_uri, "doorkeeper/redirect_uri": true
19
+ end
20
+ end
21
+ end
16
22
  end
17
23
  end
@@ -2,101 +2,106 @@
2
2
 
3
3
  # ApiBlocks::Doorkeeper::Invitations::Controller implements a devise invitable
4
4
  # API controller.
5
- module ApiBlocks::Doorkeeper::Invitations::Controller
6
- extend ActiveSupport::Concern
7
-
8
- included do # rubocop:disable Metrics/BlockLength
9
- skip_after_action :verify_authorized
10
- skip_after_action :verify_policy_scoped
11
-
12
- # Initialize a new invitation.
13
- def create
14
- user = user_model.invite!(
15
- create_params, current_user, application: oauth_application,
16
- )
17
-
18
- return render(status: :no_content) if user.errors.empty?
19
-
20
- respond_with(user)
21
- end
22
-
23
- # Renders informations about the invited user.
24
- def show
25
- user = user_model.find_by_invitation_token(params[:invitation_token], false)
26
-
27
- if user.nil? || !user.persisted?
28
- return render(
29
- problem: { details: "invalid invitation token" },
30
- status: :bad_request
31
- )
5
+ module ApiBlocks
6
+ module Doorkeeper
7
+ module Invitations
8
+ module Controller
9
+ extend ActiveSupport::Concern
10
+
11
+ included do # rubocop:disable Metrics/BlockLength
12
+ skip_after_action :verify_authorized
13
+ skip_after_action :verify_policy_scoped
14
+
15
+ # Initialize a new invitation.
16
+ def create
17
+ user = user_model.invite!(
18
+ create_params, current_user, application: oauth_application
19
+ )
20
+
21
+ return render(status: :no_content) if user.errors.empty?
22
+
23
+ respond_with(user)
24
+ end
25
+
26
+ # Renders informations about the invited user.
27
+ def show
28
+ user = user_model.find_by_invitation_token(params[:invitation_token], false)
29
+
30
+ if user.nil? || !user.persisted?
31
+ return render(
32
+ problem: { details: 'invalid invitation token' },
33
+ status: :bad_request
34
+ )
35
+ end
36
+
37
+ respond_with(user)
38
+ end
39
+
40
+ # Redirects to the application's redirect uri.
41
+ def callback
42
+ query = {
43
+ invitation_token: params[:invitation_token]
44
+ }.to_query
45
+
46
+ redirect_to("#{oauth_application.invitation_uri}?#{query}")
47
+ end
48
+
49
+ # Finalize the invitation.
50
+ def update
51
+ user = user_model.accept_invitation!(update_params)
52
+
53
+ return respond_with(user) unless user.errors.empty?
54
+
55
+ user.unlock_access! if unlockable?(user)
56
+
57
+ respond_with(::Doorkeeper::OAuth::TokenResponse.new(
58
+ access_token(oauth_application, user)
59
+ ).body)
60
+ end
61
+
62
+ private
63
+
64
+ def create_params
65
+ params.require(:user).permit(:email)
66
+ end
67
+
68
+ def update_params
69
+ params.require(:user).permit(
70
+ :invitation_token, :password, :password_confirmation
71
+ )
72
+ end
73
+
74
+ # Copied over from devise base controller in order to determine wether a ser
75
+ # is unlockable or not.
76
+ def unlockable?(resource)
77
+ resource.respond_to?(:unlock_access!) &&
78
+ resource.respond_to?(:unlock_strategy_enabled?) &&
79
+ resource.unlock_strategy_enabled?(:email)
80
+ end
81
+
82
+ # Returns a new access token for this user.
83
+ def access_token(application, user)
84
+ ::Doorkeeper::AccessToken.find_or_create_for(
85
+ application,
86
+ user.id,
87
+ ::Doorkeeper.configuration.default_scopes,
88
+ ::Doorkeeper.configuration.access_token_expires_in,
89
+ true
90
+ )
91
+ end
92
+
93
+ def oauth_application
94
+ @oauth_application ||= ::Doorkeeper::Application.find_by!(
95
+ uid: params[:client_id]
96
+ )
97
+ end
98
+
99
+ # Returns the user model class.
100
+ def user_model
101
+ raise 'the method `user_model` must be implemented on your invitations controller'
102
+ end
103
+ end
32
104
  end
33
-
34
- respond_with(user)
35
- end
36
-
37
- # Redirects to the application's redirect uri.
38
- def callback
39
- query = {
40
- invitation_token: params[:invitation_token]
41
- }.to_query
42
-
43
- redirect_to("#{oauth_application.invitation_uri}?#{query}")
44
- end
45
-
46
- # Finalize the invitation.
47
- def update
48
- user = user_model.accept_invitation!(update_params)
49
-
50
- return respond_with(user) unless user.errors.empty?
51
-
52
- user.unlock_access! if unlockable?(user)
53
-
54
- respond_with(Doorkeeper::OAuth::TokenResponse.new(
55
- access_token(oauth_application, user)
56
- ).body)
57
- end
58
-
59
- private
60
-
61
- def create_params
62
- params.require(:user).permit(:email)
63
- end
64
-
65
- def update_params
66
- params.require(:user).permit(
67
- :invitation_token, :password, :password_confirmation
68
- )
69
- end
70
-
71
- # Copied over from devise base controller in order to determine wether a ser
72
- # is unlockable or not.
73
- def unlockable?(resource)
74
- resource.respond_to?(:unlock_access!) &&
75
- resource.respond_to?(:unlock_strategy_enabled?) &&
76
- resource.unlock_strategy_enabled?(:email)
77
- end
78
-
79
- # Returns a new access token for this user.
80
- def access_token(application, user)
81
- Doorkeeper::AccessToken.find_or_create_for(
82
- application,
83
- user.id,
84
- Doorkeeper.configuration.default_scopes,
85
- Doorkeeper.configuration.access_token_expires_in,
86
- true
87
- )
88
- end
89
-
90
- def oauth_application
91
- @oauth_application ||= Doorkeeper::Application.find_by!(
92
- uid: params[:client_id]
93
- )
94
- end
95
-
96
-
97
- # Returns the user model class.
98
- def user_model
99
- raise 'the method `user_model` must be implemented on your invitations controller' # rubocop:disable Metrics/LineLength
100
105
  end
101
106
  end
102
107
  end