acts_as_tenant 0.4.4 → 0.5.2

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.
@@ -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
-