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 +4 -4
- data/README.md +38 -4
- data/lib/acts_as_tenant/model_extensions.rb +24 -23
- data/lib/acts_as_tenant/version.rb +1 -1
- data/spec/dummy/app/models/global_project_with_conditions.rb +6 -0
- data/spec/dummy/app/models/global_project_with_if.rb +6 -0
- data/spec/models/model_extensions_spec.rb +34 -4
- metadata +7 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: b9dbb40015b39583a44c37496f0687641e1a66f9b70357260b1fcabe7f3c542d
|
4
|
+
data.tar.gz: 88d1ce36220fd3c8e8264b95d76afb7f883cc77da0ac9f12538cde682fa9dacf
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
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
|
-
|
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? ||
|
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
|
-
|
98
|
-
|
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
|
-
|
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
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
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
|
@@ -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
|
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
|
-
|
135
|
-
|
136
|
-
|
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.
|
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:
|
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.
|
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
|