tenancy 0.2.0 → 1.0.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.
@@ -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