api-blocks 0.7.0 → 0.8.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: d75318555853ecc826f701851966802312197cdd96df8db360727ea962c27dc0
4
- data.tar.gz: aeab6992d7a5849d6c8cd95f19217a2ca26a7f17342d9b4953d0790ace882707
3
+ metadata.gz: c1886cf50c90d96f98f8d0d3157f4a1ef8e0b75fb4654d7954ed3efbd879113a
4
+ data.tar.gz: 5d108444c19bd159fd1fd5ec6347076e9dc76f25cdeecb269582d0d535ca7ba3
5
5
  SHA512:
6
- metadata.gz: 283db07ca95c270badcffe63a92861dbc2f4adac6eb7fcf4115ef2285ce2dbb945637292dfdfab71a5906a8b39913939f24813ee088cb8941066f855cffd5f2a
7
- data.tar.gz: c6d8718960fde579ab983e6e69c2aab7fa6e3a7c364be1c89e6caa1f058b70b7e1ee099eae34dfceabf2c96e942f0b09b100dec8697e0abdd2b26791fccc5848
6
+ metadata.gz: e7431dc25dcf77ecd1d9595114eaec72cf11e549dcdf799cbd8f62dd4b410fceb6395ea2043bac7a58c5ac691ca9ef66178e07bb7277a7dabea4a5567b4c4046
7
+ data.tar.gz: 81e3a7fa03e212da25370561739a5749450dcc80e3a95d6c81240b586fab4d5cb61fab0baeb6942b0e118561cd57c19e2236cb412a4c58f5016c53f430e2cd63
@@ -1,86 +1,89 @@
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
- if !options.fetch(:batch, true)
14
- return original_extract(association_name, object, local_options, options)
15
- end
15
+ def extract(association_name, object, local_options, options = {})
16
+ return original_extract(association_name, object, local_options, options) unless options.fetch(:batch, true)
16
17
 
17
- association = object.association(association_name)
18
+ association = object.association(association_name)
18
19
 
19
- if association.is_a?(ActiveRecord::Associations::HasManyThroughAssociation)
20
- return original_extract(association_name, object, local_options, options)
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
- if options[:blueprint].is_a?(Proc)
24
- raise "Cannot load blueprints with a `proc` blueprint option with batch-loader"
25
- end
24
+ raise 'Cannot load blueprints with a `proc` blueprint option with batch-loader' if options[:blueprint].is_a?(Proc)
26
25
 
27
- join_key = association.reflection.join_keys
28
- association_id = object.send(join_key.foreign_key)
29
- association_klass = association.reflection.class_name
26
+ join_key = ::ApiBlocks::Blueprinter::JoinKeys.join_keys(
27
+ association.reflection
28
+ )
30
29
 
31
- default_value = case association
32
- when ActiveRecord::Associations::HasManyAssociation
33
- []
34
- end
30
+ association_id = object.send(join_key.foreign_key)
31
+ association_klass = association.reflection.class_name
35
32
 
36
- view = options[:view] || :default
37
- scope = if options[:block].present?
38
- options[:block].call(object, local_options)
39
- else
40
- {}
41
- end
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
42
44
 
43
- BatchLoader.for(association_id).batch(
44
- default_value: default_value,
45
- key: [association_name, association_klass, view, options[:blueprint], scope],
46
- ) do |ids, loader, args|
47
- model = association_klass.safe_constantize
48
- 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
49
51
 
50
- case association
51
- when ActiveRecord::Associations::HasManyAssociation
52
- model.where(join_key.key => ids).merge(scope).each do |record|
53
- loader.call(record.send(join_key.key)) do |memo|
54
- 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
+ end
55
58
  end
59
+ when ActiveRecord::Associations::HasOneAssociation
60
+ model.where(join_key.key => ids).merge(scope).each do |record|
61
+ loader.call(
62
+ record.send(join_key.key),
63
+ render_blueprint(record, local_options, options)
64
+ )
65
+ end
66
+ when ActiveRecord::Associations::BelongsToAssociation
67
+ model.where(join_key.key => ids).merge(scope).each do |record|
68
+ loader.call(
69
+ record.id,
70
+ render_blueprint(record, local_options, options)
71
+ )
72
+ end
73
+ else
74
+ raise "unsupported association kind #{association.class.name}"
56
75
  end
57
- when ActiveRecord::Associations::HasOneAssociation
58
- model.where(join_key.key => ids).merge(scope).each do |record|
59
- loader.call(
60
- record.send(join_key.key),
61
- render_blueprint(record, local_options, options)
62
- )
63
- end
64
- when ActiveRecord::Associations::BelongsToAssociation
65
- model.where(join_key.key => ids).merge(scope).each do |record|
66
- loader.call(
67
- record.id,
68
- render_blueprint(record, local_options, options)
69
- )
70
- end
71
- else
72
- raise "unsupported association kind #{association.class.name}"
73
76
  end
74
77
  end
75
- end
76
78
 
77
- private
79
+ private
78
80
 
79
- def render_blueprint(value, local_options, options = {})
80
- return default_value(options) if value.nil?
81
+ def render_blueprint(value, local_options, options = {})
82
+ return default_value(options) if value.nil?
81
83
 
82
- view = options[:view] || :default
83
- blueprint = association_blueprint(options[:blueprint], value)
84
- blueprint.prepare(value, view_name: view, local_options: local_options)
84
+ view = options[:view] || :default
85
+ blueprint = association_blueprint(options[:blueprint], value)
86
+ blueprint.prepare(value, view_name: view, local_options: local_options)
87
+ end
85
88
  end
86
89
  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, policy_scope_class: nil)
39
- api_scope = self.class.inherited_pundit_api_scope || []
40
-
41
- super(api_scope + [scope], policy_scope_class: policy_scope_class)
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, policy_class: 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, policy_class: policy_class)
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