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.
- checksums.yaml +5 -5
- data/README.md +158 -27
- data/Rakefile +5 -3
- data/lib/acts_as_tenant/configuration.rb +6 -19
- data/lib/acts_as_tenant/controller_extensions/filter.rb +13 -0
- data/lib/acts_as_tenant/controller_extensions/subdomain.rb +20 -0
- data/lib/acts_as_tenant/controller_extensions/subdomain_or_domain.rb +20 -0
- data/lib/acts_as_tenant/controller_extensions.rb +10 -59
- data/lib/acts_as_tenant/errors.rb +2 -6
- data/lib/acts_as_tenant/model_extensions.rb +63 -155
- data/lib/acts_as_tenant/sidekiq.rb +11 -7
- data/lib/acts_as_tenant/tenant_helper.rb +7 -0
- data/lib/acts_as_tenant/test_tenant_middleware.rb +15 -0
- data/lib/acts_as_tenant/version.rb +1 -1
- data/lib/acts_as_tenant.rb +130 -14
- metadata +46 -48
- data/.gitignore +0 -7
- data/.travis.yml +0 -4
- data/CHANGELOG.md +0 -119
- data/Gemfile +0 -4
- data/_config.yml +0 -1
- data/acts_as_tenant.gemspec +0 -32
- data/docs/blog_post.md +0 -67
- data/rails/init.rb +0 -2
- data/spec/active_record_helper.rb +0 -22
- data/spec/active_record_models.rb +0 -143
- data/spec/acts_as_tenant/configuration_spec.rb +0 -28
- data/spec/acts_as_tenant/model_extensions_spec.rb +0 -476
- data/spec/acts_as_tenant/sidekiq_spec.rb +0 -63
- data/spec/acts_as_tenant/tenant_by_filter_spec.rb +0 -33
- data/spec/acts_as_tenant/tenant_by_subdomain_or_domain.rb +0 -46
- data/spec/acts_as_tenant/tenant_by_subdomain_spec.rb +0 -32
- data/spec/database.yml +0 -3
- data/spec/spec_helper.rb +0 -23
@@ -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
|
-
|
97
|
-
base.extend(ClassMethods)
|
98
|
-
end
|
3
|
+
extend ActiveSupport::Concern
|
99
4
|
|
100
|
-
|
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.
|
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
|
-
|
122
|
-
|
123
|
-
|
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
|
-
|
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
|
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(
|
137
|
-
m.send("#{polymorphic_type}=".to_sym, ActsAsTenant.current_tenant.class.to_s) if m.send(
|
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
|
-
}, :
|
54
|
+
}, on: :create
|
143
55
|
|
144
|
-
polymorphic_foreign_keys = reflect_on_all_associations(:belongs_to).select
|
56
|
+
polymorphic_foreign_keys = reflect_on_all_associations(:belongs_to).select { |a|
|
145
57
|
a.options[:polymorphic]
|
146
|
-
|
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
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
record.errors.add attr, "association is invalid [ActsAsTenant]" unless value.nil? ||
|
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
|
77
|
+
to_include = Module.new {
|
167
78
|
define_method "#{fkey}=" do |integer|
|
168
|
-
write_attribute(
|
169
|
-
raise ActsAsTenant::Errors::TenantIsImmutable if
|
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
|
84
|
+
define_method "#{ActsAsTenant.tenant_klass}=" do |model|
|
174
85
|
super(model)
|
175
|
-
raise ActsAsTenant::Errors::TenantIsImmutable if
|
86
|
+
raise ActsAsTenant::Errors::TenantIsImmutable if tenant_modified?
|
176
87
|
model
|
177
88
|
end
|
178
89
|
|
179
|
-
define_method
|
180
|
-
|
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
|
-
|
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
|
-
|
200
|
-
|
201
|
-
if args[:scope]
|
202
|
-
|
107
|
+
|
108
|
+
validation_args = args.clone
|
109
|
+
validation_args[:scope] = if args[:scope]
|
110
|
+
Array(args[:scope]) << fkey
|
203
111
|
else
|
204
|
-
|
112
|
+
fkey
|
205
113
|
end
|
206
114
|
|
207
|
-
|
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
|
-
|
211
|
-
|
212
|
-
|
213
|
-
|
214
|
-
|
215
|
-
|
216
|
-
|
217
|
-
|
218
|
-
|
219
|
-
|
220
|
-
|
221
|
-
|
222
|
-
|
223
|
-
|
224
|
-
|
225
|
-
|
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
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
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?(
|
19
|
-
account = msg[
|
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,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
|
data/lib/acts_as_tenant.rb
CHANGED
@@ -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
|
-
|
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
|
-
|
14
|
-
|
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
|
-
|
18
|
-
|
132
|
+
ActiveSupport.on_load(:active_record) do |base|
|
133
|
+
base.include ActsAsTenant::ModelExtensions
|
19
134
|
end
|
20
135
|
|
21
|
-
|
22
|
-
|
136
|
+
ActiveSupport.on_load(:action_controller) do |base|
|
137
|
+
base.extend ActsAsTenant::ControllerExtensions
|
138
|
+
base.include ActsAsTenant::TenantHelper
|
23
139
|
end
|
24
140
|
|
25
|
-
|
141
|
+
ActiveSupport.on_load(:action_view) do |base|
|
142
|
+
base.include ActsAsTenant::TenantHelper
|
26
143
|
end
|
27
|
-
|