api-blocks 0.5.4 → 0.9.0

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
2
  SHA256:
3
- metadata.gz: c58caeda8c2e020e4fee58c89a86a023f6a8992f3f9de5d36f8c14924990340f
4
- data.tar.gz: d018108fef550f3d0c24041dee5f9f0ec42be72172e462426399a9cb59d84625
3
+ metadata.gz: 59832c91147b528479b1e2cf190d52a07c4a92d0bb066dcb95168e25b2ed5d98
4
+ data.tar.gz: 75c3345a6024fe795c4cf923764f0b3ade9df0aebe42141fbe701be82dd7fc6b
5
5
  SHA512:
6
- metadata.gz: '04967b07d1693ff958d1c5cb3c088ed2b3cbf3861b271ee920c81c6e3692dcd731917fed35d498ba07b4abfca81918e84e2dfe68f2708aed1261460be201b5f8'
7
- data.tar.gz: 83b9641a8e24ea72f62bec80f2d758c7b4c7f60b288da2617757696effa619462fc6f537e03763ebf4fb8436e165b45b20286c2d28b8503abf1aba0b9e97f4b3
6
+ metadata.gz: 776faa783cdb574d61d7d1b24ed9439f978f294aaca9b70ffd7daca0a3930aff66bedddbe02a5ca93a5051884fdb408c1031e28038688f610ac98b8d24fa67e5
7
+ data.tar.gz: 681d130b909383ea03ccea48e0b392bd57845ff3c0143387e1b631d0b6afd8457af69a3ca1964deab12cf5f541939375a3ad39c5f3b31274fd25d057f5e7a02f
@@ -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