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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e172e79670c2137d3641f2062a07e351ad5291db58ffabad5cd15e8e52850583
4
- data.tar.gz: 3445b7da7ef300fbecaa402e351c139f4c994e0ef5c990700ac57d69a9c02c1a
3
+ metadata.gz: 0ce468857038499bb74b14492a111731539baa6a6bee4a9cecefa9a598b8d48e
4
+ data.tar.gz: 89e9b45ba6df4ce362a7c17e857cd16d8c28fc10714d021e1a7bcee34811a140
5
5
  SHA512:
6
- metadata.gz: bf02fcd6565346a41cbdb033952b25c48d79029a496239e2e785386527f0be23acbbe21983b0bc23b39f79c31752857b6eaa2f68bd9a0014f2313e7c8ff7573d
7
- data.tar.gz: 9d1b38f89c0a0c26733effc2331d6d9adef3a523dd4333204c8c8234d4c92d363e4d923fcc7b854c517a04946362362a30760639ccbf63dbf65513b10c4ef998
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
- When no tenant is set (for example in an admin panel or inside `without_tenant`), `belongs_to` associations to tenanted models are validated to belong to the same tenant as the record. Associated records without a tenant, such as global records, are allowed.
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
 
@@ -0,0 +1,7 @@
1
+ en:
2
+ activerecord:
3
+ errors:
4
+ messages:
5
+ acts_as_tenant:
6
+ tenant_mismatch: "must be the current tenant [ActsAsTenant]"
7
+ association_invalid: "association is invalid [ActsAsTenant]"
@@ -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
- # Without a current tenant the association lookup isn't scoped, so compare the tenants directly
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 ActsAsTenant.current_tenant.nil? && associated.class.respond_to?(:scoped_by_tenant?)
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
- current_tenant = ActsAsTenant.current_tenant
98
- next unless current_tenant
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.to_s == current_tenant.public_send(pkey).to_s
108
- matches &&= record_type == current_tenant.class.polymorphic_name if options[:polymorphic]
114
+ matches = record_id == current_id
115
+ matches &&= record_type == current_type if options[:polymorphic]
109
116
 
110
- record.errors.add(fkey, "must be the current tenant [ActsAsTenant]") unless matches
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 |a|
120
- attr = a.foreign_key.to_sym
121
- next if a.name == tenant.to_sym
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 a.polymorphic?
124
- next unless record.will_save_change_to_attribute?(attr) || record.will_save_change_to_attribute?(a.foreign_type)
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?(a.foreign_key)
128
- next unless record.will_save_change_to_attribute?(attr)
136
+ next if polymorphic_foreign_keys.include?(association.foreign_key)
137
+ next unless key_changed
129
138
  end
130
139
 
131
- value = record.read_attribute_for_validation(attr)
132
- next if value.nil?
140
+ values = attrs.map { |attr| record.read_attribute_for_validation(attr) }
141
+ next if values.any?(&:nil?)
133
142
 
134
- klass = if a.polymorphic?
135
- type = record.read_attribute(a.foreign_type)&.safe_constantize
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
- a.klass
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
- relation = a.scope ? a.scope_for(klass.all, record) : klass
143
- relation.find_by(a.association_primary_key(klass) => value)
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
- record.errors.add attr, "association is invalid [ActsAsTenant]"
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
@@ -1,3 +1,3 @@
1
1
  module ActsAsTenant
2
- VERSION = "2.0.2"
2
+ VERSION = "2.2.0"
3
3
  end
@@ -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.2
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.21
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: []