acts_as_tenant 0.4.4 → 0.5.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,128 +1,40 @@
1
1
  module ActsAsTenant
2
- @@tenant_klass = nil
3
- @@models_with_global_records = []
4
-
5
- def self.set_tenant_klass(klass)
6
- @@tenant_klass = klass
7
- end
8
-
9
- def self.tenant_klass
10
- @@tenant_klass
11
- end
12
-
13
- def self.models_with_global_records
14
- @@models_with_global_records
15
- end
16
-
17
- def self.add_global_record_model model
18
- @@models_with_global_records.push(model)
19
- end
20
-
21
- def self.fkey
22
- "#{@@tenant_klass.to_s}_id"
23
- end
24
-
25
- def self.pkey
26
- :id
27
- end
28
-
29
- def self.polymorphic_type
30
- "#{@@tenant_klass.to_s}_type"
31
- end
32
-
33
- def self.current_tenant=(tenant)
34
- RequestStore.store[:current_tenant] = tenant
35
- end
36
-
37
- def self.current_tenant
38
- RequestStore.store[:current_tenant] || self.default_tenant
39
- end
40
-
41
- def self.unscoped=(unscoped)
42
- RequestStore.store[:acts_as_tenant_unscoped] = unscoped
43
- end
44
-
45
- def self.unscoped
46
- RequestStore.store[:acts_as_tenant_unscoped]
47
- end
48
-
49
- def self.unscoped?
50
- !!unscoped
51
- end
52
-
53
- class << self
54
- def default_tenant=(tenant)
55
- @default_tenant = tenant
56
- end
57
-
58
- def default_tenant
59
- @default_tenant unless unscoped
60
- end
61
- end
62
-
63
- def self.with_tenant(tenant, &block)
64
- if block.nil?
65
- raise ArgumentError, "block required"
66
- end
67
-
68
- old_tenant = self.current_tenant
69
- self.current_tenant = tenant
70
- value = block.call
71
- return value
72
-
73
- ensure
74
- self.current_tenant = old_tenant
75
- end
76
-
77
- def self.without_tenant(&block)
78
- if block.nil?
79
- raise ArgumentError, "block required"
80
- end
81
-
82
- old_tenant = current_tenant
83
- old_unscoped = unscoped
84
-
85
- self.current_tenant = nil
86
- self.unscoped = true
87
- value = block.call
88
- return value
89
-
90
- ensure
91
- self.current_tenant = old_tenant
92
- self.unscoped = old_unscoped
93
- end
94
-
95
2
  module ModelExtensions
96
- def self.included(base)
97
- base.extend(ClassMethods)
98
- end
3
+ extend ActiveSupport::Concern
99
4
 
100
- module ClassMethods
101
- def acts_as_tenant(tenant = :account, options = {})
5
+ class_methods do
6
+ def acts_as_tenant(tenant = :account, **options)
102
7
  ActsAsTenant.set_tenant_klass(tenant)
103
8
 
104
9
  ActsAsTenant.add_global_record_model(self) if options[:has_global_records]
105
10
 
106
11
  # Create the association
107
- valid_options = options.slice(:foreign_key, :class_name, :inverse_of, :optional, :primary_key)
12
+ valid_options = options.slice(:foreign_key, :class_name, :inverse_of, :optional, :primary_key, :counter_cache)
108
13
  fkey = valid_options[:foreign_key] || ActsAsTenant.fkey
109
14
  pkey = valid_options[:primary_key] || ActsAsTenant.pkey
110
15
  polymorphic_type = valid_options[:foreign_type] || ActsAsTenant.polymorphic_type
111
- belongs_to tenant, valid_options
16
+ belongs_to tenant, **valid_options
112
17
 
113
18
  default_scope lambda {
114
- if ActsAsTenant.configuration.require_tenant && ActsAsTenant.current_tenant.nil? && !ActsAsTenant.unscoped?
19
+ if ActsAsTenant.should_require_tenant? && ActsAsTenant.current_tenant.nil? && !ActsAsTenant.unscoped?
115
20
  raise ActsAsTenant::Errors::NoTenantSet
116
21
  end
22
+
117
23
  if ActsAsTenant.current_tenant
118
- keys = [ActsAsTenant.current_tenant.send(pkey)]
24
+ keys = [ActsAsTenant.current_tenant.send(pkey)].compact
119
25
  keys.push(nil) if options[:has_global_records]
120
26
 
121
- query_criteria = { fkey.to_sym => keys }
122
- query_criteria.merge!({ polymorphic_type.to_sym => ActsAsTenant.current_tenant.class.to_s }) if options[:polymorphic]
123
- where(query_criteria)
27
+ if options[:through]
28
+ query_criteria = {options[:through] => {fkey.to_sym => keys}}
29
+ query_criteria[polymorphic_type.to_sym] = ActsAsTenant.current_tenant.class.to_s if options[:polymorphic]
30
+ joins(options[:through]).where(query_criteria)
31
+ else
32
+ query_criteria = {fkey.to_sym => keys}
33
+ query_criteria[polymorphic_type.to_sym] = ActsAsTenant.current_tenant.class.to_s if options[:polymorphic]
34
+ where(query_criteria)
35
+ end
124
36
  else
125
- ActiveRecord::VERSION::MAJOR < 4 ? scoped : all
37
+ all
126
38
  end
127
39
  }
128
40
 
@@ -130,31 +42,30 @@ module ActsAsTenant
130
42
  # - new instances should have the tenant set
131
43
  # - validate that associations belong to the tenant, currently only for belongs_to
132
44
  #
133
- before_validation Proc.new {|m|
45
+ before_validation proc { |m|
134
46
  if ActsAsTenant.current_tenant
135
47
  if options[:polymorphic]
136
- m.send("#{fkey}=".to_sym, ActsAsTenant.current_tenant.class.to_s) if m.send("#{fkey}").nil?
137
- m.send("#{polymorphic_type}=".to_sym, ActsAsTenant.current_tenant.class.to_s) if m.send("#{polymorphic_type}").nil?
48
+ m.send("#{fkey}=".to_sym, ActsAsTenant.current_tenant.class.to_s) if m.send(fkey.to_s).nil?
49
+ m.send("#{polymorphic_type}=".to_sym, ActsAsTenant.current_tenant.class.to_s) if m.send(polymorphic_type.to_s).nil?
138
50
  else
139
51
  m.send "#{fkey}=".to_sym, ActsAsTenant.current_tenant.send(pkey)
140
52
  end
141
53
  end
142
- }, :on => :create
54
+ }, on: :create
143
55
 
144
- polymorphic_foreign_keys = reflect_on_all_associations(:belongs_to).select do |a|
56
+ polymorphic_foreign_keys = reflect_on_all_associations(:belongs_to).select { |a|
145
57
  a.options[:polymorphic]
146
- end.map { |a| a.foreign_key }
58
+ }.map { |a| a.foreign_key }
147
59
 
148
60
  reflect_on_all_associations(:belongs_to).each do |a|
149
61
  unless a == reflect_on_association(tenant) || polymorphic_foreign_keys.include?(a.foreign_key)
150
- association_class = a.options[:class_name].nil? ? a.name.to_s.classify.constantize : a.options[:class_name].constantize
151
62
  validates_each a.foreign_key.to_sym do |record, attr, value|
152
- primary_key = if association_class.respond_to?(:primary_key)
153
- association_class.primary_key
154
- else
155
- a.primary_key
156
- end.to_sym
157
- record.errors.add attr, "association is invalid [ActsAsTenant]" unless value.nil? || association_class.where(primary_key => value).any?
63
+ primary_key = if a.respond_to?(:active_record_primary_key)
64
+ a.active_record_primary_key
65
+ else
66
+ a.primary_key
67
+ end.to_sym
68
+ record.errors.add attr, "association is invalid [ActsAsTenant]" unless value.nil? || a.klass.where(primary_key => value).any?
158
69
  end
159
70
  end
160
71
  end
@@ -163,27 +74,23 @@ module ActsAsTenant
163
74
  # - Rewrite the accessors to make tenant immutable
164
75
  # - Add an override to prevent unnecessary db hits
165
76
  # - Add a helper method to verify if a model has been scoped by AaT
166
- to_include = Module.new do
77
+ to_include = Module.new {
167
78
  define_method "#{fkey}=" do |integer|
168
- write_attribute("#{fkey}", integer)
169
- raise ActsAsTenant::Errors::TenantIsImmutable if send("#{fkey}_changed?") && persisted? && !send("#{fkey}_was").nil?
79
+ write_attribute(fkey.to_s, integer)
80
+ raise ActsAsTenant::Errors::TenantIsImmutable if tenant_modified?
170
81
  integer
171
82
  end
172
83
 
173
- define_method "#{ActsAsTenant.tenant_klass.to_s}=" do |model|
84
+ define_method "#{ActsAsTenant.tenant_klass}=" do |model|
174
85
  super(model)
175
- raise ActsAsTenant::Errors::TenantIsImmutable if send("#{fkey}_changed?") && persisted? && !send("#{fkey}_was").nil?
86
+ raise ActsAsTenant::Errors::TenantIsImmutable if tenant_modified?
176
87
  model
177
88
  end
178
89
 
179
- define_method "#{ActsAsTenant.tenant_klass.to_s}" do
180
- if !ActsAsTenant.current_tenant.nil? && send(fkey) == ActsAsTenant.current_tenant.send(pkey)
181
- return ActsAsTenant.current_tenant
182
- else
183
- super()
184
- end
90
+ define_method :tenant_modified? do
91
+ will_save_change_to_attribute?(fkey) && persisted? && attribute_in_database(fkey).present?
185
92
  end
186
- end
93
+ }
187
94
  include to_include
188
95
 
189
96
  class << self
@@ -193,37 +100,38 @@ module ActsAsTenant
193
100
  end
194
101
  end
195
102
 
196
- def validates_uniqueness_to_tenant(fields, args ={})
103
+ def validates_uniqueness_to_tenant(fields, args = {})
197
104
  raise ActsAsTenant::Errors::ModelNotScopedByTenant unless respond_to?(:scoped_by_tenant?)
105
+
198
106
  fkey = reflect_on_association(ActsAsTenant.tenant_klass).foreign_key
199
- pkey = reflect_on_association(ActsAsTenant.tenant_klass).active_record_primary_key
200
- #tenant_id = lambda { "#{ActsAsTenant.fkey}"}.call
201
- if args[:scope]
202
- args[:scope] = Array(args[:scope]) << fkey
107
+
108
+ validation_args = args.clone
109
+ validation_args[:scope] = if args[:scope]
110
+ Array(args[:scope]) << fkey
203
111
  else
204
- args[:scope] = fkey
112
+ fkey
205
113
  end
206
114
 
207
- validates_uniqueness_of(fields, args)
115
+ # validating within tenant scope
116
+ validates_uniqueness_of(fields, validation_args)
208
117
 
209
118
  if ActsAsTenant.models_with_global_records.include?(self)
210
- validate do |instance|
211
- Array(fields).each do |field|
212
- if instance.new_record?
213
- unless self.class.where(fkey.to_sym => [nil, instance[fkey]],
214
- field.to_sym => instance[field]).empty?
215
- errors.add(field, 'has already been taken')
216
- end
217
- else
218
- unless self.class.where(fkey.to_sym => [nil, instance[fkey]],
219
- field.to_sym => instance[field])
220
- .where.not(:id => instance.id).empty?
221
- errors.add(field, 'has already been taken')
222
- end
223
-
224
- end
225
- end
226
- end
119
+ arg_if = args.delete(:if)
120
+ arg_condition = args.delete(:conditions)
121
+
122
+ # if tenant is not set (instance is global) - validating globally
123
+ global_validation_args = args.merge(
124
+ if: ->(instance) { instance[fkey].blank? && (arg_if.blank? || arg_if.call(instance)) }
125
+ )
126
+ validates_uniqueness_of(fields, global_validation_args)
127
+
128
+ # if tenant is set (instance is not global) and records can be global - validating within records with blank tenant
129
+ blank_tenant_validation_args = args.merge({
130
+ conditions: -> { arg_condition.blank? ? where(fkey => nil) : arg_condition.call.where(fkey => nil) },
131
+ if: ->(instance) { instance[fkey].present? && (arg_if.blank? || arg_if.call(instance)) }
132
+ })
133
+
134
+ validates_uniqueness_of(fields, blank_tenant_validation_args)
227
135
  end
228
136
  end
229
137
  end
@@ -2,11 +2,13 @@ module ActsAsTenant::Sidekiq
2
2
  # Get the current tenant and store in the message to be sent to Sidekiq.
3
3
  class Client
4
4
  def call(worker_class, msg, queue, redis_pool)
5
- msg['acts_as_tenant'] ||=
6
- {
7
- 'class' => ActsAsTenant.current_tenant.class.name,
8
- 'id' => ActsAsTenant.current_tenant.id
9
- } if ActsAsTenant.current_tenant.present?
5
+ if ActsAsTenant.current_tenant.present?
6
+ msg["acts_as_tenant"] ||=
7
+ {
8
+ "class" => ActsAsTenant.current_tenant.class.name,
9
+ "id" => ActsAsTenant.current_tenant.id
10
+ }
11
+ end
10
12
 
11
13
  yield
12
14
  end
@@ -15,8 +17,8 @@ module ActsAsTenant::Sidekiq
15
17
  # Pull the tenant out and run the current thread with it.
16
18
  class Server
17
19
  def call(worker_class, msg, queue)
18
- if msg.has_key?('acts_as_tenant')
19
- account = msg['acts_as_tenant']['class'].constantize.find msg['acts_as_tenant']['id']
20
+ if msg.has_key?("acts_as_tenant")
21
+ account = msg["acts_as_tenant"]["class"].constantize.find msg["acts_as_tenant"]["id"]
20
22
  ActsAsTenant.with_tenant account do
21
23
  yield
22
24
  end
@@ -40,6 +42,8 @@ Sidekiq.configure_server do |config|
40
42
  config.server_middleware do |chain|
41
43
  if defined?(Sidekiq::Middleware::Server::RetryJobs)
42
44
  chain.insert_before Sidekiq::Middleware::Server::RetryJobs, ActsAsTenant::Sidekiq::Server
45
+ elsif defined?(Sidekiq::Batch::Server)
46
+ chain.insert_before Sidekiq::Batch::Server, ActsAsTenant::Sidekiq::Server
43
47
  else
44
48
  chain.add ActsAsTenant::Sidekiq::Server
45
49
  end
@@ -0,0 +1,7 @@
1
+ module ActsAsTenant
2
+ module TenantHelper
3
+ def current_tenant
4
+ ActsAsTenant.current_tenant
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,15 @@
1
+ module ActsAsTenant
2
+ class TestTenantMiddleware
3
+ def initialize(app)
4
+ @app = app
5
+ end
6
+
7
+ def call(env)
8
+ previously_set_test_tenant = ActsAsTenant.test_tenant
9
+ ActsAsTenant.test_tenant = nil
10
+ @app.call(env)
11
+ ensure
12
+ ActsAsTenant.test_tenant = previously_set_test_tenant
13
+ end
14
+ end
15
+ end
@@ -1,3 +1,3 @@
1
1
  module ActsAsTenant
2
- VERSION = "0.4.4"
2
+ VERSION = "0.5.2"
3
3
  end
@@ -1,27 +1,143 @@
1
1
  require "request_store"
2
2
 
3
- #$LOAD_PATH.unshift(File.dirname(__FILE__))
4
-
5
3
  require "acts_as_tenant/version"
6
4
  require "acts_as_tenant/errors"
7
- require "acts_as_tenant/configuration"
8
- require "acts_as_tenant/controller_extensions"
9
- require "acts_as_tenant/model_extensions"
10
5
 
11
- #$LOAD_PATH.shift
6
+ module ActsAsTenant
7
+ autoload :Configuration, "acts_as_tenant/configuration"
8
+ autoload :ControllerExtensions, "acts_as_tenant/controller_extensions"
9
+ autoload :ModelExtensions, "acts_as_tenant/model_extensions"
10
+ autoload :TenantHelper, "acts_as_tenant/tenant_helper"
11
+
12
+ @@configuration = nil
13
+ @@tenant_klass = nil
14
+ @@models_with_global_records = []
15
+
16
+ class << self
17
+ attr_writer :default_tenant
18
+ end
19
+
20
+ def self.configure
21
+ @@configuration = Configuration.new
22
+ yield configuration if block_given?
23
+ configuration
24
+ end
25
+
26
+ def self.configuration
27
+ @@configuration || configure
28
+ end
29
+
30
+ def self.set_tenant_klass(klass)
31
+ @@tenant_klass = klass
32
+ end
33
+
34
+ def self.tenant_klass
35
+ @@tenant_klass
36
+ end
37
+
38
+ def self.models_with_global_records
39
+ @@models_with_global_records
40
+ end
41
+
42
+ def self.add_global_record_model model
43
+ @@models_with_global_records.push(model)
44
+ end
45
+
46
+ def self.fkey
47
+ "#{@@tenant_klass}_id"
48
+ end
49
+
50
+ def self.pkey
51
+ ActsAsTenant.configuration.pkey
52
+ end
53
+
54
+ def self.polymorphic_type
55
+ "#{@@tenant_klass}_type"
56
+ end
57
+
58
+ def self.current_tenant=(tenant)
59
+ RequestStore.store[:current_tenant] = tenant
60
+ end
12
61
 
13
- if defined?(ActiveRecord::Base)
14
- ActiveRecord::Base.send(:include, ActsAsTenant::ModelExtensions)
62
+ def self.current_tenant
63
+ RequestStore.store[:current_tenant] || test_tenant || default_tenant
64
+ end
65
+
66
+ def self.test_tenant=(tenant)
67
+ Thread.current[:test_tenant] = tenant
68
+ end
69
+
70
+ def self.test_tenant
71
+ Thread.current[:test_tenant]
72
+ end
73
+
74
+ def self.unscoped=(unscoped)
75
+ RequestStore.store[:acts_as_tenant_unscoped] = unscoped
76
+ end
77
+
78
+ def self.unscoped
79
+ RequestStore.store[:acts_as_tenant_unscoped]
80
+ end
81
+
82
+ def self.unscoped?
83
+ !!unscoped
84
+ end
85
+
86
+ def self.default_tenant
87
+ @default_tenant unless unscoped
88
+ end
89
+
90
+ def self.with_tenant(tenant, &block)
91
+ if block.nil?
92
+ raise ArgumentError, "block required"
93
+ end
94
+
95
+ old_tenant = current_tenant
96
+ self.current_tenant = tenant
97
+ value = block.call
98
+ value
99
+ ensure
100
+ self.current_tenant = old_tenant
101
+ end
102
+
103
+ def self.without_tenant(&block)
104
+ if block.nil?
105
+ raise ArgumentError, "block required"
106
+ end
107
+
108
+ old_tenant = current_tenant
109
+ old_test_tenant = test_tenant
110
+ old_unscoped = unscoped
111
+
112
+ self.current_tenant = nil
113
+ self.test_tenant = nil
114
+ self.unscoped = true
115
+ value = block.call
116
+ value
117
+ ensure
118
+ self.current_tenant = old_tenant
119
+ self.test_tenant = old_test_tenant
120
+ self.unscoped = old_unscoped
121
+ end
122
+
123
+ def self.should_require_tenant?
124
+ if configuration.require_tenant.respond_to?(:call)
125
+ !!configuration.require_tenant.call
126
+ else
127
+ !!configuration.require_tenant
128
+ end
129
+ end
15
130
  end
16
131
 
17
- if defined?(ActionController::Base)
18
- ActionController::Base.extend ActsAsTenant::ControllerExtensions
132
+ ActiveSupport.on_load(:active_record) do |base|
133
+ base.include ActsAsTenant::ModelExtensions
19
134
  end
20
135
 
21
- if defined?(ActionController::API)
22
- ActionController::API.extend ActsAsTenant::ControllerExtensions
136
+ ActiveSupport.on_load(:action_controller) do |base|
137
+ base.extend ActsAsTenant::ControllerExtensions
138
+ base.include ActsAsTenant::TenantHelper
23
139
  end
24
140
 
25
- module ActsAsTenant
141
+ ActiveSupport.on_load(:action_view) do |base|
142
+ base.include ActsAsTenant::TenantHelper
26
143
  end
27
-