acts_as_tenant 2.0.2 → 2.2.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.
- checksums.yaml +4 -4
- data/README.md +44 -1
- data/lib/acts_as_tenant/locale/en.yml +7 -0
- data/lib/acts_as_tenant/model_extensions.rb +52 -24
- data/lib/acts_as_tenant/version.rb +1 -1
- data/lib/acts_as_tenant.rb +4 -0
- metadata +3 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 0ce468857038499bb74b14492a111731539baa6a6bee4a9cecefa9a598b8d48e
|
|
4
|
+
data.tar.gz: 89e9b45ba6df4ce362a7c17e857cd16d8c28fc10714d021e1a7bcee34811a140
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 576e189176dc1099e68db52d2fa6a0c62fd8d36321b23d5b3264ea325c101a34c95c1921dae48d8d7111a0426dc305ecc586575a4486b3275e15cae24733fc4c
|
|
7
|
+
data.tar.gz: a588f22a10de60ce2ed08d3b00e68ee07c801fa0384c8e9e238d5a024e77567f8f96c2a146290dffb38703ff5e579d96adaae5bd8b7f67f717297a87ba1901c0
|
data/README.md
CHANGED
|
@@ -208,7 +208,50 @@ New records are assigned the current tenant unless they already have one. A reco
|
|
|
208
208
|
|
|
209
209
|
`belongs_to` associations are validated against the current tenant regardless of whether they are declared before or after `acts_as_tenant`.
|
|
210
210
|
|
|
211
|
-
|
|
211
|
+
Associated records are looked up through the associated model's default scopes and the association's scope, and must belong to the same tenant as the record. This also applies when no tenant is set (for example in an admin panel or inside `without_tenant`). Associated records without a tenant, such as global records, are allowed. A record without a tenant can only refer to records of the current tenant.
|
|
212
|
+
|
|
213
|
+
If the associated model has other default scopes, such as soft deletes, records hidden by them fail the validation. Override `tenant_validation_scope` to change the lookup for all associations to that model:
|
|
214
|
+
|
|
215
|
+
```ruby
|
|
216
|
+
class Project < ActiveRecord::Base
|
|
217
|
+
acts_as_tenant :account
|
|
218
|
+
default_scope { where(deleted_at: nil) }
|
|
219
|
+
|
|
220
|
+
# Tasks can refer to soft-deleted projects
|
|
221
|
+
def self.tenant_validation_scope(relation)
|
|
222
|
+
relation.unscope(where: :deleted_at)
|
|
223
|
+
end
|
|
224
|
+
end
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
To change the lookup for all models, override it in `ApplicationRecord`. The tenants are always compared after the lookup, so a scope that removes the tenant condition can't accept records of another tenant.
|
|
228
|
+
|
|
229
|
+
To skip the validation of an association, including ones declared elsewhere such as in a gem, override `validate_tenant_association?`:
|
|
230
|
+
|
|
231
|
+
```ruby
|
|
232
|
+
class Comment < ApplicationRecord
|
|
233
|
+
acts_as_tenant :account
|
|
234
|
+
include Trackable # declares belongs_to :source
|
|
235
|
+
|
|
236
|
+
def validate_tenant_association?(reflection)
|
|
237
|
+
reflection.name != :source
|
|
238
|
+
end
|
|
239
|
+
end
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
The validation error messages can be overridden in your locale files, using the same lookup as other Active Record errors:
|
|
243
|
+
|
|
244
|
+
```yaml
|
|
245
|
+
en:
|
|
246
|
+
activerecord:
|
|
247
|
+
errors:
|
|
248
|
+
messages:
|
|
249
|
+
acts_as_tenant:
|
|
250
|
+
tenant_mismatch: "must be the current tenant"
|
|
251
|
+
association_invalid: "is invalid"
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
Messages can also be overridden for one model or one attribute, such as `activerecord.errors.models.task.acts_as_tenant.association_invalid` or `activerecord.errors.models.task.attributes.project_id.acts_as_tenant.association_invalid`.
|
|
212
255
|
|
|
213
256
|
### Validating attribute uniqueness
|
|
214
257
|
|
|
@@ -78,14 +78,21 @@ module ActsAsTenant
|
|
|
78
78
|
else
|
|
79
79
|
reflection.klass.polymorphic_name
|
|
80
80
|
end
|
|
81
|
-
[id, type]
|
|
81
|
+
[id.to_s, type]
|
|
82
82
|
end
|
|
83
83
|
end
|
|
84
84
|
|
|
85
|
-
#
|
|
85
|
+
# Returns [tenant id, tenant class] for the current tenant, or nil if none is set
|
|
86
|
+
current_tenant_identity = lambda do
|
|
87
|
+
current_tenant = ActsAsTenant.current_tenant
|
|
88
|
+
[current_tenant.public_send(pkey).to_s, current_tenant.class.polymorphic_name] if current_tenant
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# Compared directly since the lookup scope may not filter by tenant.
|
|
92
|
+
# Records without a tenant, such as global records, are compared with the current tenant.
|
|
86
93
|
tenant_mismatch = lambda do |record, associated|
|
|
87
|
-
if
|
|
88
|
-
record_tenant = tenant_identity.call(record)
|
|
94
|
+
if associated.class.respond_to?(:scoped_by_tenant?)
|
|
95
|
+
record_tenant = tenant_identity.call(record) || current_tenant_identity.call
|
|
89
96
|
associated_tenant = tenant_identity.call(associated)
|
|
90
97
|
|
|
91
98
|
record_tenant && associated_tenant && record_tenant != associated_tenant
|
|
@@ -94,8 +101,8 @@ module ActsAsTenant
|
|
|
94
101
|
|
|
95
102
|
# Records must belong to the current tenant, matching what the default scope would find
|
|
96
103
|
validate do |record|
|
|
97
|
-
|
|
98
|
-
next unless
|
|
104
|
+
current_id, current_type = current_tenant_identity.call
|
|
105
|
+
next unless current_id
|
|
99
106
|
|
|
100
107
|
tenant_attributes = [fkey, polymorphic_type].compact
|
|
101
108
|
next unless record.new_record? || tenant_attributes.any? { |attr| record.will_save_change_to_attribute?(attr) }
|
|
@@ -104,10 +111,10 @@ module ActsAsTenant
|
|
|
104
111
|
next if record_tenant.nil?
|
|
105
112
|
|
|
106
113
|
record_id, record_type = record_tenant
|
|
107
|
-
matches = record_id
|
|
108
|
-
matches &&= record_type ==
|
|
114
|
+
matches = record_id == current_id
|
|
115
|
+
matches &&= record_type == current_type if options[:polymorphic]
|
|
109
116
|
|
|
110
|
-
record.errors.add(fkey, "
|
|
117
|
+
record.errors.add(fkey, :"acts_as_tenant.tenant_mismatch") unless matches
|
|
111
118
|
end
|
|
112
119
|
|
|
113
120
|
# Associations are looked up at validation time so belongs_to associations
|
|
@@ -116,35 +123,45 @@ module ActsAsTenant
|
|
|
116
123
|
associations = record.class.reflect_on_all_associations(:belongs_to)
|
|
117
124
|
polymorphic_foreign_keys = associations.select(&:polymorphic?).map(&:foreign_key)
|
|
118
125
|
|
|
119
|
-
associations.each do |
|
|
120
|
-
|
|
121
|
-
|
|
126
|
+
associations.each do |association|
|
|
127
|
+
next if association.name == tenant.to_sym
|
|
128
|
+
|
|
129
|
+
attrs = Array(association.foreign_key).map(&:to_sym)
|
|
130
|
+
key_changed = attrs.any? { |attr| record.will_save_change_to_attribute?(attr) }
|
|
122
131
|
|
|
123
|
-
if
|
|
124
|
-
next unless
|
|
132
|
+
if association.polymorphic?
|
|
133
|
+
next unless key_changed || record.will_save_change_to_attribute?(association.foreign_type)
|
|
125
134
|
else
|
|
126
135
|
# Associations sharing a polymorphic foreign key are checked through the polymorphic association
|
|
127
|
-
next if polymorphic_foreign_keys.include?(
|
|
128
|
-
next unless
|
|
136
|
+
next if polymorphic_foreign_keys.include?(association.foreign_key)
|
|
137
|
+
next unless key_changed
|
|
129
138
|
end
|
|
130
139
|
|
|
131
|
-
|
|
132
|
-
next if
|
|
140
|
+
values = attrs.map { |attr| record.read_attribute_for_validation(attr) }
|
|
141
|
+
next if values.any?(&:nil?)
|
|
133
142
|
|
|
134
|
-
|
|
135
|
-
|
|
143
|
+
next unless record.validate_tenant_association?(association)
|
|
144
|
+
|
|
145
|
+
klass = if association.polymorphic?
|
|
146
|
+
type = record.read_attribute(association.foreign_type)&.safe_constantize
|
|
136
147
|
type if type.is_a?(Class) && type < ActiveRecord::Base
|
|
137
148
|
else
|
|
138
|
-
|
|
149
|
+
# Raises for composite keys whose column counts don't match, like loading the association would
|
|
150
|
+
association.check_validity! if attrs.size > 1
|
|
151
|
+
association.klass
|
|
139
152
|
end
|
|
140
153
|
|
|
141
154
|
associated = if klass
|
|
142
|
-
|
|
143
|
-
relation.
|
|
155
|
+
# The class itself keeps Rails' cached find_by when there are no scopes
|
|
156
|
+
relation = association.scope ? association.scope_for(klass.all, record) : klass
|
|
157
|
+
relation = klass.tenant_validation_scope(relation)
|
|
158
|
+
relation.find_by(Array(association.association_primary_key(klass)).zip(values).to_h)
|
|
144
159
|
end
|
|
145
160
|
|
|
146
161
|
if associated.nil? || tenant_mismatch.call(record, associated)
|
|
147
|
-
|
|
162
|
+
# Composite keys often include the tenant column, which is not the one to blame
|
|
163
|
+
error_attr = (attrs - [fkey]).first || attrs.first
|
|
164
|
+
record.errors.add(error_attr, :"acts_as_tenant.association_invalid")
|
|
148
165
|
end
|
|
149
166
|
end
|
|
150
167
|
end
|
|
@@ -185,6 +202,12 @@ module ActsAsTenant
|
|
|
185
202
|
end
|
|
186
203
|
end
|
|
187
204
|
|
|
205
|
+
# The relation used to find associated records when validating belongs_to associations.
|
|
206
|
+
# Override to change the lookup for all associations to this model, for example to include soft-deleted records.
|
|
207
|
+
def tenant_validation_scope(relation)
|
|
208
|
+
relation
|
|
209
|
+
end
|
|
210
|
+
|
|
188
211
|
def validates_uniqueness_to_tenant(*fields)
|
|
189
212
|
args = fields.extract_options!
|
|
190
213
|
raise ActsAsTenant::Errors::ModelNotScopedByTenant unless respond_to?(:scoped_by_tenant?)
|
|
@@ -222,5 +245,10 @@ module ActsAsTenant
|
|
|
222
245
|
end
|
|
223
246
|
end
|
|
224
247
|
end
|
|
248
|
+
|
|
249
|
+
# Override to skip the tenant validation of a belongs_to association
|
|
250
|
+
def validate_tenant_association?(reflection)
|
|
251
|
+
true
|
|
252
|
+
end
|
|
225
253
|
end
|
|
226
254
|
end
|
data/lib/acts_as_tenant.rb
CHANGED
|
@@ -137,6 +137,10 @@ module ActsAsTenant
|
|
|
137
137
|
end
|
|
138
138
|
end
|
|
139
139
|
|
|
140
|
+
ActiveSupport.on_load(:i18n) do
|
|
141
|
+
I18n.load_path << File.expand_path("acts_as_tenant/locale/en.yml", __dir__)
|
|
142
|
+
end
|
|
143
|
+
|
|
140
144
|
ActiveSupport.on_load(:active_record) do |base|
|
|
141
145
|
base.include ActsAsTenant::ModelExtensions
|
|
142
146
|
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: acts_as_tenant
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 2.0
|
|
4
|
+
version: 2.2.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Erwin Matthijssen
|
|
@@ -44,6 +44,7 @@ files:
|
|
|
44
44
|
- lib/acts_as_tenant/controller_extensions/subdomain.rb
|
|
45
45
|
- lib/acts_as_tenant/controller_extensions/subdomain_or_domain.rb
|
|
46
46
|
- lib/acts_as_tenant/errors.rb
|
|
47
|
+
- lib/acts_as_tenant/locale/en.yml
|
|
47
48
|
- lib/acts_as_tenant/model_extensions.rb
|
|
48
49
|
- lib/acts_as_tenant/sidekiq.rb
|
|
49
50
|
- lib/acts_as_tenant/tenant_helper.rb
|
|
@@ -68,7 +69,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
68
69
|
- !ruby/object:Gem::Version
|
|
69
70
|
version: '0'
|
|
70
71
|
requirements: []
|
|
71
|
-
rubygems_version: 4.0.
|
|
72
|
+
rubygems_version: 4.0.20
|
|
72
73
|
specification_version: 4
|
|
73
74
|
summary: Add multi-tenancy to Rails applications using a shared db strategy
|
|
74
75
|
test_files: []
|