tenancy 0.2.0 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -3,65 +3,28 @@ module Tenancy
3
3
  extend ActiveSupport::Concern
4
4
 
5
5
  module ClassMethods
6
- attr_reader :scope_fields
7
-
8
- def scope_fields
9
- @scope_fields ||= []
10
- end
11
6
 
12
7
  def scope_to(*resources)
13
- options = resources.extract_options!.dup
14
- raise ArgumentError, 'options should be blank if there are multiple resources' if resources.count > 1 and options.present?
15
-
16
- resources.each do |resource|
17
- resource = resource.to_sym
18
- resource_class_name ||= (options[:class_name].to_s.presence || resource.to_s).classify
19
- resource_class = resource_class_name.constantize
20
-
21
- # validates and belongs_to
22
- validates resource, presence: true
23
- belongs_to resource, options
24
-
25
- # default_scope
26
- resource_foreign_key = reflect_on_association(resource).foreign_key
27
- scope_fields << resource_foreign_key
28
- default_scope { where(:"#{resource_foreign_key}" => resource_class.current_id) if resource_class.current_id }
29
-
30
- # override to return current resource instance
31
- # so that it doesn't touch db
32
- define_method(resource) do |reload=false|
33
- return super(reload) if reload
34
- return resource_class.current if send(resource_foreign_key) == resource_class.current_id
35
- super(reload)
36
- end
37
- end
8
+ tenancy_scoping.scope_to(resources)
38
9
  end
39
10
 
40
- # inspired by: https://github.com/goncalossilva/acts_as_paranoid/blob/rails3.2/lib/acts_as_paranoid/core.rb#L76
41
- def without_scope(*resources)
42
- scope = where(nil).with_default_scope
43
- resources.each do |resource|
44
- resource = resource.to_sym
45
- reflection = reflect_on_association(resource)
46
- next if reflection.nil?
47
-
48
- resource_scope_sql = where(nil).table[reflection.foreign_key].eq(reflection.klass.current_id).to_sql
49
-
50
- scope.where_values.delete_if { |query| query.to_sql == resource_scope_sql }
51
- end
52
-
53
- scope
11
+ def tenant_scope(*resources)
12
+ tenancy_scoping.tenant_scope(resources)
54
13
  end
55
14
 
56
15
  def validates_uniqueness_in_scope(fields, args={})
57
- if args[:scope]
58
- args[:scope] = Array.wrap(args[:scope]) << scope_fields
59
- else
60
- args[:scope] = scope_fields
61
- end
62
-
63
- validates_uniqueness_of(fields, args)
16
+ tenancy_scoping.validates_uniqueness_in_scope(fields, args)
64
17
  end
18
+
19
+ private
20
+
21
+ def tenancy_scoping
22
+ @tenancy_scoping ||= if defined?(::ActiveRecord) && ancestors.include?(::ActiveRecord::Base)
23
+ Scoping::ActiveRecord.new(self)
24
+ elsif defined?(Mongoid) && ancestors.include?(Mongoid::Document)
25
+ Scoping::Mongoid.new(self)
26
+ end
27
+ end
65
28
  end
66
29
  end
67
30
  end
@@ -0,0 +1,13 @@
1
+ module Tenancy
2
+ class Scoping
3
+ autoload :ActiveRecord, "tenancy/scoping/active_record"
4
+ autoload :Mongoid, "tenancy/scoping/mongoid"
5
+
6
+ attr_reader :klass, :tenants
7
+
8
+ def initialize(klass)
9
+ @klass = klass
10
+ @tenants = []
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,52 @@
1
+ module Tenancy
2
+ class Scoping::ActiveRecord < Scoping
3
+
4
+ def scope_to(tenant_names)
5
+ options = tenant_names.extract_options!.dup
6
+ raise ArgumentError, "options should be blank if there are multiple tenants" if tenant_names.count > 1 and options.present?
7
+
8
+ tenant_names.each do |tenant_name|
9
+ # validates and belongs_to
10
+ klass.validates tenant_name, presence: true
11
+ klass.belongs_to tenant_name, options
12
+
13
+ tenant = Tenant.new(tenant_name, options[:class_name], klass)
14
+ self.tenants << tenant
15
+
16
+ # default_scope
17
+ klass.send(:default_scope, lambda { klass.where(:"#{tenant.foreign_key}" => tenant.klass.current_id) if tenant.klass.current_id })
18
+
19
+ # override to return current tenant instance
20
+ # so that it doesn"t touch db
21
+ klass.send(:define_method, tenant_name, lambda { |reload=false|
22
+ return super(reload) if reload
23
+ return tenant.klass.current if send(tenant.foreign_key) == tenant.klass.current_id
24
+ super(reload)
25
+ })
26
+ end
27
+ end
28
+
29
+ def tenant_scope(tenant_names)
30
+ scope = klass.where(nil).with_default_scope
31
+ tenants.each do |tenant|
32
+ next if tenant_names.include?(tenant.name.to_sym)
33
+
34
+ tenant_scope_sql = klass.where(nil).table[tenant.foreign_key].eq(tenant.klass.current_id).to_sql
35
+ scope.where_values.delete_if { |query| query.to_sql == tenant_scope_sql }
36
+ end
37
+
38
+ scope
39
+ end
40
+
41
+ def validates_uniqueness_in_scope(fields, args={})
42
+ foreign_keys = tenants.map(&:foreign_key)
43
+ if args[:scope]
44
+ args[:scope] = Array.wrap(args[:scope]) << foreign_keys
45
+ else
46
+ args[:scope] = foreign_keys
47
+ end
48
+
49
+ klass.validates_uniqueness_of(fields, args)
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,79 @@
1
+ module Tenancy
2
+ class Scoping::Mongoid < Scoping
3
+
4
+ def scope_to(tenant_names)
5
+ options = tenant_names.extract_options!.dup
6
+ raise ArgumentError, "options should be blank if there are multiple tenants" if tenant_names.count > 1 and options.present?
7
+
8
+ tenant_names.each do |tenant_name|
9
+ # validates and belongs_to
10
+ klass.validates tenant_name, presence: true
11
+ klass.belongs_to tenant_name, options
12
+
13
+ tenant = Tenant.new(tenant_name, options[:class_name], klass)
14
+ self.tenants << tenant
15
+
16
+ # default_scope
17
+ klass.default_scope lambda {
18
+ if tenant.klass.current_id
19
+ klass.where(:"#{tenant.foreign_key}" => tenant.klass.current_id)
20
+ else
21
+ klass.where(nil)
22
+ end
23
+ }
24
+
25
+ # override to return current tenant_name instance
26
+ # so that it doesn't touch db
27
+ klass.send(:define_method, :"#{tenant_name}_with_tenant", lambda { |reload=false|
28
+ return send(:"#{tenant_name}_without_tenant", reload) if reload
29
+ return tenant.klass.current if send(tenant.foreign_key) == tenant.klass.current_id
30
+ send(:"#{tenant_name}_without_tenant", reload)
31
+ })
32
+ klass.alias_method_chain :"#{tenant_name}", :tenant
33
+
34
+ # override getter for mongoid 3.1
35
+ if ::Mongoid::VERSION.start_with?("3.1.")
36
+ klass.send(:define_method, tenant.foreign_key, lambda {
37
+ value = super()
38
+ if value.nil? && new_record?
39
+ self[tenant.foreign_key] = tenant.klass.current_id
40
+ end
41
+ self[tenant.foreign_key]
42
+ })
43
+ end
44
+ end
45
+
46
+ # tenants variable is for lambda
47
+ tenants = self.tenants
48
+ klass.send(:define_method, :shard_key_selector, lambda {
49
+ selector = super()
50
+ tenants.each do |tenant|
51
+ selector[tenant.foreign_key.to_s] = send(tenant.foreign_key) if tenant.klass.current_id
52
+ end
53
+ selector
54
+ })
55
+ end
56
+
57
+ def tenant_scope(tenant_names)
58
+ scope = klass.where(nil)
59
+ tenants.each do |tenant|
60
+ next if tenant_names.include?(tenant.name.to_sym)
61
+
62
+ scope.selector.delete(tenant.foreign_key.to_s)
63
+ end
64
+
65
+ scope
66
+ end
67
+
68
+ def validates_uniqueness_in_scope(fields, args={})
69
+ foreign_keys = tenants.map(&:foreign_key)
70
+ if args[:scope]
71
+ args[:scope] = Array.wrap(args[:scope]) << foreign_keys
72
+ else
73
+ args[:scope] = foreign_keys
74
+ end
75
+
76
+ klass.validates_uniqueness_of(fields, args)
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,13 @@
1
+ module Tenancy
2
+ class Tenant
3
+ attr_accessor :name, :klass, :klass_name, :foreign_key
4
+
5
+ def initialize(name, klass_name, host_klass)
6
+ @name = name.to_sym
7
+ @klass_name = (klass_name.to_s.presence || name.to_s).classify
8
+ @klass = @klass_name.constantize
9
+ @foreign_key = host_klass.reflect_on_association(@name).foreign_key.to_sym
10
+ end
11
+
12
+ end
13
+ end
@@ -1,3 +1,3 @@
1
1
  module Tenancy
2
- VERSION = "0.2.0"
2
+ VERSION = "1.0.0"
3
3
  end
@@ -1,57 +1,101 @@
1
- require 'spec_helper'
1
+ require "spec_helper"
2
2
 
3
- describe "Tenancy::Resource" do
4
- before(:all) do
5
- @camyp = Portal.create(id: 1, domain_name: 'yp.com.kh')
6
- @panpage = Portal.create(id: 2, domain_name: 'panpages.my')
7
- @yoolk = Portal.create(id: 3, domain_name: 'yoolk.com')
8
- end
3
+ if defined?(ActiveRecord)
4
+ describe "Tenancy::Resource" do
5
+ let(:camyp) { Portal.create(id: 1, domain_name: "yp.com.kh") }
6
+ let(:panpage) { Portal.create(id: 2, domain_name: "panpages.my") }
7
+ let(:yoolk) { Portal.create(id: 3, domain_name: "yoolk.com") }
9
8
 
10
- after(:all) do
11
- Portal.delete_all
12
- end
9
+ it "set current with instance" do
10
+ Portal.current = camyp
13
11
 
14
- before(:each) { RequestStore.store[:'Portal.current'] = nil }
12
+ Portal.current.should == camyp
13
+ RequestStore.store[:"Portal.current"].should == camyp
14
+ end
15
15
 
16
- it "set current with instance" do
17
- Portal.current = @camyp
16
+ it "set current with id" do
17
+ Portal.current = panpage.id
18
18
 
19
- Portal.current.should == @camyp
20
- RequestStore.store[:'Portal.current'].should == @camyp
21
- end
19
+ Portal.current.should == panpage
20
+ RequestStore.store[:"Portal.current"].should == panpage
21
+ end
22
22
 
23
- it "set current with id" do
24
- Portal.current = @panpage.id
23
+ it "set current with nil" do
24
+ Portal.current = panpage
25
+ Portal.current = nil
25
26
 
26
- Portal.current.should == @panpage
27
- RequestStore.store[:'Portal.current'].should == @panpage
28
- end
27
+ Portal.current.should == nil
28
+ RequestStore.store[:"Portal.current"].should == nil
29
+ end
29
30
 
30
- it "set current with nil" do
31
- Portal.current = @panpage
32
- Portal.current = nil
31
+ it "#current_id" do
32
+ Portal.current = yoolk
33
33
 
34
- Portal.current.should == nil
35
- RequestStore.store[:'Portal.current'].should == nil
36
- end
34
+ Portal.current_id.should == yoolk.id
35
+ end
37
36
 
38
- it "#current_id" do
39
- Portal.current = @yoolk
37
+ it "#with_scope with block" do
38
+ Portal.current.should == nil
40
39
 
41
- Portal.current_id.should == @yoolk.id
40
+ Portal.with_tenant(yoolk) do
41
+ Portal.current.should == yoolk
42
+ end
43
+
44
+ Portal.current.should == nil
45
+ end
46
+
47
+ it "#with_scope without block" do
48
+ expect { Portal.with_tenant(yoolk) }.to raise_error(ArgumentError)
49
+ end
42
50
  end
51
+ end
52
+
53
+ if defined?(Mongoid)
54
+ describe "Tenancy::Resource" do
55
+ let(:camyp) { Mongo::Portal.create(domain_name: "yp.com.kh") }
56
+ let(:panpage) { Mongo::Portal.create(domain_name: "panpages.my") }
57
+ let(:yoolk) { Mongo::Portal.create(domain_name: "yoolk.com") }
43
58
 
44
- it "#with with block" do
45
- Portal.current.should == nil
59
+ it "set current with instance" do
60
+ Mongo::Portal.current = camyp
46
61
 
47
- Portal.with(@yoolk) do
48
- Portal.current.should == @yoolk
62
+ Mongo::Portal.current.should == camyp
63
+ RequestStore.store[:"Mongo::Portal.current"].should == camyp
49
64
  end
50
65
 
51
- Portal.current.should == nil
52
- end
66
+ it "set current with id" do
67
+ Mongo::Portal.current = panpage.id
68
+
69
+ Mongo::Portal.current.should == panpage
70
+ RequestStore.store[:"Mongo::Portal.current"].should == panpage
71
+ end
72
+
73
+ it "set current with nil" do
74
+ Mongo::Portal.current = panpage
75
+ Mongo::Portal.current = nil
76
+
77
+ Mongo::Portal.current.should == nil
78
+ RequestStore.store[:"Mongo::Portal.current"].should == nil
79
+ end
80
+
81
+ it "#current_id" do
82
+ Mongo::Portal.current = yoolk
53
83
 
54
- it "#with without block" do
55
- expect { Portal.with(@yoolk) }.to raise_error(ArgumentError)
84
+ Mongo::Portal.current_id.should == yoolk.id
85
+ end
86
+
87
+ it "#with_scope with block" do
88
+ Mongo::Portal.current.should == nil
89
+
90
+ Mongo::Portal.with_tenant(yoolk) do
91
+ Mongo::Portal.current.should == yoolk
92
+ end
93
+
94
+ Mongo::Portal.current.should == nil
95
+ end
96
+
97
+ it "#with_scope without block" do
98
+ expect { Mongo::Portal.with_tenant(yoolk) }.to raise_error(ArgumentError)
99
+ end
56
100
  end
57
101
  end
@@ -0,0 +1,135 @@
1
+ require "spec_helper"
2
+
3
+ if defined?(ActiveRecord)
4
+ describe "Tenancy::Scoping::ActiveRecord" do
5
+ let(:camyp) { Portal.create(domain_name: "yp.com.kh") }
6
+ let(:panpages) { Portal.create(domain_name: "panpages.com") }
7
+ let(:listing) { Listing.create(name: "Listing 1", portal_id: camyp.id) }
8
+
9
+ describe Listing do
10
+ it { should belong_to(:portal) }
11
+
12
+ it { should validate_presence_of(:portal) }
13
+
14
+ it { should validate_uniqueness_of(:name).scoped_to(:portal_id).case_insensitive }
15
+
16
+ it "have default_scope with :portal_id field" do
17
+ Portal.current = camyp
18
+
19
+ expect(Listing.where(nil).to_sql).to eq(Listing.where(portal_id: Portal.current_id).to_sql)
20
+ end
21
+
22
+ it "doesn't have default_scope when it doesn't have current portal" do
23
+ Portal.current = nil
24
+
25
+ expect(Listing.where(nil).to_sql).not_to include(%{"listings"."portal_id" = #{Portal.current_id}})
26
+ end
27
+ end
28
+
29
+ describe Communication do
30
+ it { should belong_to(:portal) }
31
+
32
+ it { should validate_presence_of(:portal) }
33
+
34
+ it { should belong_to(:listing) }
35
+
36
+ it { should validate_presence_of(:listing) }
37
+
38
+ it { should validate_uniqueness_of(:value).scoped_to(:portal_id, :listing_id) }
39
+
40
+ it "have default_scope with :portal_id and :listing_id" do
41
+ Portal.current = camyp
42
+ Listing.current = listing
43
+
44
+ Communication.where(nil).to_sql.should == Communication.where(portal_id: Portal.current_id, listing_id: Listing.current_id).to_sql
45
+ end
46
+
47
+ it "doesn't have default_scope when it doesn't have current portal and listing" do
48
+ Portal.current = nil
49
+ Listing.current = nil
50
+
51
+ expect(Communication.where(nil).to_sql).not_to include(%{"communications"."portal_id" = #{Portal.current_id}})
52
+ expect(Communication.where(nil).to_sql).not_to include(%{"communications"."listing_id" = #{Listing.current_id}})
53
+ end
54
+ end
55
+
56
+ describe ExtraCommunication do
57
+ it { should belong_to(:portal) }
58
+
59
+ it { should belong_to(:listing) }
60
+
61
+ it "raise exception when passing two resources and options" do
62
+ expect { ExtraCommunication.scope_to(:portal, :listing, class_name: "Listing") }.to raise_error(ArgumentError)
63
+ end
64
+
65
+ it "uses the correct scope" do
66
+ listing2 = Listing.create(name: "Name 2", portal: camyp)
67
+
68
+ Portal.current = camyp
69
+ Listing.current = listing2
70
+
71
+ extra_communication = ExtraCommunication.new
72
+ expect(extra_communication.listing_id).to eq(listing2.id)
73
+ expect(extra_communication.portal_id).to eq(camyp.id)
74
+ end
75
+ end
76
+
77
+ describe "belongs_to method override" do
78
+ before(:each) { Portal.current = camyp }
79
+
80
+ it "reload belongs_to when passes true" do
81
+ listing.portal.domain_name = "abc.com"
82
+ expect(listing.portal(true).object_id).not_to eq(Portal.current.object_id)
83
+ end
84
+
85
+ it "doesn't reload belongs_to" do
86
+ listing.portal.domain_name = "abc.com"
87
+ expect(listing.portal.object_id).to eq(Portal.current.object_id)
88
+ end
89
+
90
+ it "returns different object" do
91
+ listing.portal_id = panpages.id
92
+ expect(listing.portal.object_id).not_to eq(Portal.current.object_id)
93
+ end
94
+
95
+ it "doesn't touch db" do
96
+ current_listing = listing
97
+
98
+ Portal.establish_connection(adapter: "sqlite3", database: "spec/invalid.sqlite3")
99
+ expect(current_listing.portal.object_id).to eq(Portal.current.object_id)
100
+
101
+ Portal.establish_connection(ActiveRecord::Base.connection_config)
102
+ end
103
+ end
104
+
105
+ describe "#tenant_scope" do
106
+ before(:each) { Portal.current = camyp }
107
+
108
+ it "scopes only :current_portal" do
109
+ Listing.current = listing
110
+
111
+ expect(Communication.tenant_scope(:portal).to_sql).not_to include(%{"communications"."listing_id" = #{Listing.current_id}})
112
+ expect(Communication.tenant_scope(:portal).to_sql).to eq(%{SELECT "communications".* FROM "communications" WHERE "communications"."is_active" = 't' AND "communications"."portal_id" = #{Portal.current_id}})
113
+ end
114
+
115
+ it "scopes only :current_listing" do
116
+ Listing.current = listing
117
+
118
+ expect(Communication.tenant_scope(:listing).to_sql).not_to include(%{"communications"."portal_id" = #{Portal.current_id}})
119
+ expect(Communication.tenant_scope(:listing).to_sql).to eq(%{SELECT "communications".* FROM "communications" WHERE "communications"."is_active" = 't' AND "communications"."listing_id" = #{Listing.current_id}})
120
+ end
121
+
122
+ it "scopes only :current_listing and :current_portal" do
123
+ Listing.current = listing
124
+
125
+ expect(Communication.tenant_scope(:listing, :portal).to_sql).to eq(Communication.where(nil).to_sql)
126
+ end
127
+
128
+ it "scopes nothing" do
129
+ Listing.current = listing
130
+
131
+ expect(Communication.tenant_scope(nil).to_sql).to eq(%{SELECT "communications".* FROM "communications" WHERE "communications"."is_active" = 't'})
132
+ end
133
+ end
134
+ end
135
+ end