acts_as_tenant 0.5.0 → 0.5.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: fda1a0075313a96256ff1815cf9fd08422e2cfd18934703e8325273b2b941a3e
4
- data.tar.gz: e89bef1337f0210769a0ffaca7c081a111944bdbdb5b4d282118df39f4a1d53f
3
+ metadata.gz: b9dbb40015b39583a44c37496f0687641e1a66f9b70357260b1fcabe7f3c542d
4
+ data.tar.gz: 88d1ce36220fd3c8e8264b95d76afb7f883cc77da0ac9f12538cde682fa9dacf
5
5
  SHA512:
6
- metadata.gz: b80f8de28ae30db10fdca8e9a2a290e014c87ab195b250661c4c81a7d33d761b67929b2b43f77380c3c5fbe128611ede07e135c7adc3708f9fbe406d1f7e8516
7
- data.tar.gz: ec85db56efcd7441559ef33adbc0ec25bd366108aa3bd2da9f1bd3279776e45181e9153796d2071116991c63a76e849b1411d8d852c4ba25fb0497958a59192a
6
+ metadata.gz: 7d76fbf4cc3381afdfa2c02627ad6de56ff9e4cdaa636da4103e11e38ee48a6f6ecf3f432f296bfd3b8ae5cfc690d8fc52cbc7e025953e39cf3b3dab4439d3f1
7
+ data.tar.gz: ba09cb46b06f2399de2ac2e4d6efa3369d22e287a6741bfd0fcfc483836b64ac96851dd671dd4a97bace409c7aa72f6e7fc03394250f853c55a54159e720118a
data/README.md CHANGED
@@ -1,7 +1,8 @@
1
- Acts As Tenant
2
- ==============
1
+ # Acts As Tenant
3
2
 
4
- [![Build Status](https://github.com/ErwinM/acts_as_tenant/workflows/Tests/badge.svg)](https://github.com/ErwinM/acts_as_tenant/actions)
3
+ [![Build Status](https://github.com/ErwinM/acts_as_tenant/workflows/Tests/badge.svg)](https://github.com/ErwinM/acts_as_tenant/actions) [![Gem Version](https://badge.fury.io/rb/acts_as_tenant.svg)](https://badge.fury.io/rb/acts_as_tenant)
4
+
5
+ Row-level multitenancy for Ruby on Rails apps.
5
6
 
6
7
  This gem was born out of our own need for a fail-safe and out-of-the-way manner to add multi-tenancy to our Rails app through a shared database strategy, that integrates (near) seamless with Rails.
7
8
 
@@ -16,9 +17,25 @@ In addition, acts_as_tenant:
16
17
 
17
18
  **Note**: acts_as_tenant was introduced in this [blog post](https://github.com/ErwinM/acts_as_tenant/blob/master/docs/blog_post.md).
18
19
 
20
+ **Row-level vs schema multitenancy**
21
+
22
+ What's the difference?
23
+
24
+ Row-level multitenancy each model must have a tenant ID column on it. This makes it easy to filter records for each tenant using your standard database columns and indexes. ActsAsTenant uses row-level multitenancy.
25
+
26
+ Schema multitenancy uses database schemas to handle multitenancy. For this approach, your database has multiple schemas and each schema contains your database tables. Schemas require migrations to be run against each tenant and generally makes it harder to scale as you add more tenants. The Apartment gem uses schema multitenancy.
27
+
28
+ #### 🎬 Walkthrough
29
+
30
+ Want to see how it works? Check out [the ActsAsTenant walkthrough video](https://www.youtube.com/watch?v=BIyxM9f8Jus):
31
+
32
+ <a href="https://www.youtube.com/watch?v=BIyxM9f8Jus">
33
+ <img src="https://i.imgur.com/DLRPzhv.png" width="300" height="auto" alt="ActsAsTenant Walkthrough Video">
34
+ </a>
35
+
19
36
  Installation
20
37
  ------------
21
- acts_as_tenant will only work on Rails 3.1 and up. This is due to changes made to the handling of `default_scope`, an essential pillar of the gem.
38
+ acts_as_tenant will only work on Rails 5.2 and up. This is due to changes made to the handling of `default_scope`, an essential pillar of the gem.
22
39
 
23
40
  To use it, add it to your Gemfile:
24
41
 
@@ -206,6 +223,23 @@ end
206
223
 
207
224
  * `config.require_tenant` when set to true will raise an ActsAsTenant::NoTenant error whenever a query is made without a tenant set.
208
225
 
226
+ belongs_to options
227
+ ---------------------
228
+ `acts_as_tenant :account` includes the belongs_to relationship.
229
+ So when using acts_as_tenant on a model, do not add `belongs_to :account` alongside `acts_as_tenant :account`:
230
+
231
+ ```
232
+ class User < ActiveRecord::Base
233
+ acts_as_tenant(:account) # YES
234
+ belongs_to :account # REDUNDANT
235
+ end
236
+ ```
237
+
238
+ You can add the following `belongs_to` options to `acts_as_tenant`:
239
+ `:foreign_key, :class_name, :inverse_of, :optional, :primary_key, :counter_cache`
240
+
241
+ Example: `acts_as_tenant(:account, counter_cache: true)`
242
+
209
243
  Sidekiq support
210
244
  ---------------
211
245
 
@@ -28,7 +28,7 @@ module ActsAsTenant
28
28
  query_criteria[polymorphic_type.to_sym] = ActsAsTenant.current_tenant.class.to_s if options[:polymorphic]
29
29
  where(query_criteria)
30
30
  else
31
- ActiveRecord::VERSION::MAJOR < 4 ? scoped : all
31
+ all
32
32
  end
33
33
  }
34
34
 
@@ -53,14 +53,13 @@ module ActsAsTenant
53
53
 
54
54
  reflect_on_all_associations(:belongs_to).each do |a|
55
55
  unless a == reflect_on_association(tenant) || polymorphic_foreign_keys.include?(a.foreign_key)
56
- association_class = a.options[:class_name].nil? ? a.name.to_s.classify.constantize : a.options[:class_name].constantize
57
56
  validates_each a.foreign_key.to_sym do |record, attr, value|
58
57
  primary_key = if a.respond_to?(:active_record_primary_key)
59
58
  a.active_record_primary_key
60
59
  else
61
60
  a.primary_key
62
61
  end.to_sym
63
- record.errors.add attr, "association is invalid [ActsAsTenant]" unless value.nil? || association_class.where(primary_key => value).any?
62
+ record.errors.add attr, "association is invalid [ActsAsTenant]" unless value.nil? || a.klass.where(primary_key => value).any?
64
63
  end
65
64
  end
66
65
  end
@@ -93,34 +92,36 @@ module ActsAsTenant
93
92
 
94
93
  def validates_uniqueness_to_tenant(fields, args = {})
95
94
  raise ActsAsTenant::Errors::ModelNotScopedByTenant unless respond_to?(:scoped_by_tenant?)
95
+
96
96
  fkey = reflect_on_association(ActsAsTenant.tenant_klass).foreign_key
97
- # tenant_id = lambda { "#{ActsAsTenant.fkey}"}.call
98
- args[:scope] = if args[:scope]
97
+
98
+ validation_args = args.clone
99
+ validation_args[:scope] = if args[:scope]
99
100
  Array(args[:scope]) << fkey
100
101
  else
101
102
  fkey
102
103
  end
103
104
 
104
- validates_uniqueness_of(fields, args)
105
+ # validating within tenant scope
106
+ validates_uniqueness_of(fields, validation_args)
105
107
 
106
108
  if ActsAsTenant.models_with_global_records.include?(self)
107
- validate do |instance|
108
- Array(fields).each do |field|
109
- if instance.new_record?
110
- unless self.class.where(fkey.to_sym => [nil, instance[fkey]],
111
- field.to_sym => instance[field]).empty?
112
- errors.add(field, "has already been taken")
113
- end
114
- else
115
- unless self.class.where(fkey.to_sym => [nil, instance[fkey]],
116
- field.to_sym => instance[field])
117
- .where.not(id: instance.id).empty?
118
- errors.add(field, "has already been taken")
119
- end
120
-
121
- end
122
- end
123
- end
109
+ arg_if = args.delete(:if)
110
+ arg_condition = args.delete(:conditions)
111
+
112
+ # if tenant is not set (instance is global) - validating globally
113
+ global_validation_args = args.merge(
114
+ if: ->(instance) { instance[fkey].blank? && (arg_if.blank? || arg_if.call(instance)) }
115
+ )
116
+ validates_uniqueness_of(fields, global_validation_args)
117
+
118
+ # if tenant is set (instance is not global) and records can be global - validating within records with blank tenant
119
+ blank_tenant_validation_args = args.merge({
120
+ conditions: -> { arg_condition.blank? ? where(fkey => nil) : arg_condition.call.where(fkey => nil) },
121
+ if: ->(instance) { instance[fkey].present? && (arg_if.blank? || arg_if.call(instance)) }
122
+ })
123
+
124
+ validates_uniqueness_of(fields, blank_tenant_validation_args)
124
125
  end
125
126
  end
126
127
  end
@@ -1,3 +1,3 @@
1
1
  module ActsAsTenant
2
- VERSION = "0.5.0"
2
+ VERSION = "0.5.1"
3
3
  end
@@ -0,0 +1,6 @@
1
+ class GlobalProjectWithConditions < ActiveRecord::Base
2
+ self.table_name = "projects"
3
+
4
+ acts_as_tenant :account, has_global_records: true
5
+ validates_uniqueness_to_tenant :name, conditions: -> { where(name: "foo") }
6
+ end
@@ -0,0 +1,6 @@
1
+ class GlobalProjectWithIf < ActiveRecord::Base
2
+ self.table_name = "projects"
3
+
4
+ acts_as_tenant :account, has_global_records: true
5
+ validates_uniqueness_to_tenant :name, if: ->(instance) { instance.name == "foo" }
6
+ end
@@ -113,6 +113,11 @@ describe ActsAsTenant do
113
113
  expect(GlobalProject.all.count).to eq(GlobalProject.unscoped.where(account: [nil]).count)
114
114
  end
115
115
 
116
+ it "should add the model to ActsAsTenant.models_with_global_records" do
117
+ expect(ActsAsTenant.models_with_global_records.include?(GlobalProject)).to be_truthy
118
+ expect(ActsAsTenant.models_with_global_records.include?(Project)).to be_falsy
119
+ end
120
+
116
121
  context "should validate tenant records against global & tenant records" do
117
122
  it "global records are valid" do
118
123
  expect(global_projects(:global).valid?).to be(true)
@@ -122,7 +127,13 @@ describe ActsAsTenant do
122
127
  expect(GlobalProject.new(name: "foo new").valid?).to be(true)
123
128
  end
124
129
 
125
- it "is invalid with with duplicate tenant records" do
130
+ it "is valid if tenant is different" do
131
+ ActsAsTenant.current_tenant = accounts(:bar)
132
+
133
+ expect(GlobalProject.new(name: "global foo").valid?).to be(true)
134
+ end
135
+
136
+ it "is invalid with duplicate tenant records" do
126
137
  expect(GlobalProject.new(name: "global foo").valid?).to be(false)
127
138
  end
128
139
 
@@ -131,9 +142,28 @@ describe ActsAsTenant do
131
142
  end
132
143
  end
133
144
 
134
- it "should add the model to ActsAsTenant.models_with_global_records" do
135
- expect(ActsAsTenant.models_with_global_records.include?(GlobalProject)).to be_truthy
136
- expect(ActsAsTenant.models_with_global_records.include?(Project)).to be_falsy
145
+ context "should validate global records against global & tenant records" do
146
+ before do
147
+ ActsAsTenant.current_tenant = nil
148
+ end
149
+
150
+ it "is invalid if global record conflicts with tenant record" do
151
+ expect(GlobalProject.new(name: "global foo").valid?).to be(false)
152
+ end
153
+ end
154
+
155
+ context "with conditions in args" do
156
+ it "respects conditions" do
157
+ expect(GlobalProjectWithConditions.new(name: "foo").valid?).to be(false)
158
+ expect(GlobalProjectWithConditions.new(name: "global foo").valid?).to be(true)
159
+ end
160
+ end
161
+
162
+ context "with if in args" do
163
+ it "respects if" do
164
+ expect(GlobalProjectWithIf.new(name: "foo").valid?).to be(false)
165
+ expect(GlobalProjectWithIf.new(name: "global foo").valid?).to be(true)
166
+ end
137
167
  end
138
168
  end
139
169
 
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: 0.5.0
4
+ version: 0.5.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Erwin Matthijssen
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2020-11-18 00:00:00.000000000 Z
12
+ date: 2021-09-09 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: request_store
@@ -181,6 +181,8 @@ files:
181
181
  - spec/dummy/app/models/custom_foreign_key_task.rb
182
182
  - spec/dummy/app/models/custom_primary_key_task.rb
183
183
  - spec/dummy/app/models/global_project.rb
184
+ - spec/dummy/app/models/global_project_with_conditions.rb
185
+ - spec/dummy/app/models/global_project_with_if.rb
184
186
  - spec/dummy/app/models/manager.rb
185
187
  - spec/dummy/app/models/polymorphic_tenant_comment.rb
186
188
  - spec/dummy/app/models/project.rb
@@ -252,7 +254,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
252
254
  - !ruby/object:Gem::Version
253
255
  version: '0'
254
256
  requirements: []
255
- rubygems_version: 3.1.4
257
+ rubygems_version: 3.2.22
256
258
  signing_key:
257
259
  specification_version: 4
258
260
  summary: Add multi-tenancy to Rails applications using a shared db strategy
@@ -285,6 +287,8 @@ test_files:
285
287
  - spec/dummy/app/models/custom_foreign_key_task.rb
286
288
  - spec/dummy/app/models/custom_primary_key_task.rb
287
289
  - spec/dummy/app/models/global_project.rb
290
+ - spec/dummy/app/models/global_project_with_conditions.rb
291
+ - spec/dummy/app/models/global_project_with_if.rb
288
292
  - spec/dummy/app/models/manager.rb
289
293
  - spec/dummy/app/models/polymorphic_tenant_comment.rb
290
294
  - spec/dummy/app/models/project.rb