From 92a4ddd165793a5b5a3935341e08213183d88898 Mon Sep 17 00:00:00 2001 From: Joel Low Date: Thu, 28 Apr 2016 09:13:02 +0800 Subject: [PATCH] Allow querying nested unloaded associations using a database query. --- CHANGELOG.md | 7 ++++ lib/cancancan/squeel/squeel_adapter.rb | 44 ++++++++++++++++---------- spec/cancancan/squeel_spec.rb | 11 +++++++ 3 files changed, 46 insertions(+), 16 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8d4b0ea..4404231 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +## master + +### bug fixes + + - Allow querying nested unloaded associations using a database query. + [@lowjoel](https://github.com/lowjoel) + ## 0.1.3 ### enhancements diff --git a/lib/cancancan/squeel/squeel_adapter.rb b/lib/cancancan/squeel/squeel_adapter.rb index d11f2f6..14066fa 100644 --- a/lib/cancancan/squeel/squeel_adapter.rb +++ b/lib/cancancan/squeel/squeel_adapter.rb @@ -52,11 +52,14 @@ def self.match_relation?(subject, name) def self.matches_relation?(subject, name, value) relation = subject.public_send(name) klass = subject.class.reflect_on_association(name).klass + join_list = nil - relation.where do - expression, = CanCanCan::Squeel::ExpressionBuilder.build(self, klass, :==, value) + scope = relation.where do + expression, join_list = CanCanCan::Squeel::ExpressionBuilder.build(self, klass, :==, value) expression - end.any? + end + + add_joins_to_scope(scope, join_list, :inner).any? end private_class_method :matches_relation? @@ -65,6 +68,25 @@ def database_records relation.distinct end + # Builds a relation, outer joined on the provided associations. + # + # @param [ActiveRecord::Relation] scope The current scope to add the joins to. + # @param [Array>] joins The set of associations to outer join with. + # @param [Symbol] join_type The type of join; defaults to outer joins. + # @return [ActiveRecord::Relation] The built relation. + def self.add_joins_to_scope(scope, joins, join_type = :outer) + joins.reduce(scope) do |result, join| + result.joins do + join.reduce(self) do |relation, association| + relation = relation.__send__(association) + relation = relation.__send__(join_type) unless join_type == :inner + relation + end + end + end + end + private_class_method :add_joins_to_scope + private # Builds a relation that expresses the set of provided rules. @@ -82,19 +104,9 @@ def relation add_joins_to_scope(scope, join_list) end - # Builds a relation, outer joined on the provided associations. - # - # @param [ActiveRecord::Relation] scope The current scope to add the joins to. - # @param [Array>] joins The set of associations to outer join with. - # @return [ActiveRecord::Relation] The built relation. - def add_joins_to_scope(scope, joins) - joins.reduce(scope) do |result, join| - result.joins do - join.reduce(self) do |relation, association| - relation.__send__(association).outer - end - end - end + # @see CanCanCan::Squeel::SqueelAdapter.add_joins_to_scope + def add_joins_to_scope(*args) + self.class.send(:add_joins_to_scope, *args) end # This builds Squeel expression for each rule, and combines the expression with those to the left diff --git a/spec/cancancan/squeel_spec.rb b/spec/cancancan/squeel_spec.rb index 85b30d7..648692d 100644 --- a/spec/cancancan/squeel_spec.rb +++ b/spec/cancancan/squeel_spec.rb @@ -203,5 +203,16 @@ ability.can?(:read, parent) expect(parent.children).not_to be_loaded end + + context 'when unloaded associations include joins' do + it 'adds the necessary joins' do + parent = Parent.create! + ability.can(:read, Parent, children: { parent: { id: 2 }}) + expect(parent.children).to receive(:where).and_call_original + + expect(ability.can?(:read, parent)).to eq(false) + expect(parent.children).not_to be_loaded + end + end end end