acts_as_tenant 0.4.4 → 0.5.2

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
- SHA1:
3
- metadata.gz: 9820de48ce59a3e8efae341784175dadf04820cf
4
- data.tar.gz: 28cc62d3e62171c690fd9ba48bbd53c29c5efdc7
2
+ SHA256:
3
+ metadata.gz: 82765cfa1d00a63aa012f3ab5de9ddd60ae2c8041df405e08ae70ef20382d7ee
4
+ data.tar.gz: ba1920a299f84a4e6424304539c05ca69515e3b17e73ffb1b6635fbce79ae77c
5
5
  SHA512:
6
- metadata.gz: aa052469cd82efe7062999976d4782f2240718731331698ced6ceb324646aafd431e15ca66a98d55a398192ab410bf0a0620e962e9128a3daf7220dff871c4f3
7
- data.tar.gz: 54d961326dedf594be8b87b9603feb063c4ee43a8bb2b8a86a973571b6f01cafd48cc4bd86127ca71771d0e91f275e61549f366c80be425a24df7af1ac1fa3cc
6
+ metadata.gz: 62854b28ba5293d5437f5614032e992d914bc0741be3e8b925231116802d85b19d9631fc2e0ff2a7775e3259a998e4239f0b38c7ddf97c6817a0db96fbf488c5
7
+ data.tar.gz: 2e6c875895dd15882ffc4454f0c0bfae4fc3e77df17deae58710835e92bf32761247ab3b60cdfe9a83b81f72e2ea698c59f0155c046dfe2af5c62450a4bd65ca
data/README.md CHANGED
@@ -1,9 +1,8 @@
1
- Acts As Tenant
2
- ==============
1
+ # Acts As Tenant
3
2
 
4
- [![Build Status](https://travis-ci.org/ErwinM/acts_as_tenant.svg)](https://travis-ci.org/ErwinM/acts_as_tenant)
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)
5
4
 
6
- **Note**: acts_as_tenant was introduced in this [blog post](https://github.com/ErwinM/acts_as_tenant/blob/master/docs/blog_post.md).
5
+ Row-level multitenancy for Ruby on Rails apps.
7
6
 
8
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.
9
8
 
@@ -16,9 +15,27 @@ In addition, acts_as_tenant:
16
15
  * adds a method to validate uniqueness to a tenant, `validates_uniqueness_to_tenant`
17
16
  * sets up a helper method containing the current tenant
18
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).
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
 
@@ -41,7 +58,8 @@ There are three ways to set the current tenant:
41
58
  2. by setting the current tenant in the controller, and
42
59
  3. by setting the current tenant for a block.
43
60
 
44
- ### Use the subdomain to lookup the current tenant ###
61
+ ### Looking Up Tenants
62
+ #### By Subdomain to lookup the current tenant
45
63
 
46
64
  ```ruby
47
65
  class ApplicationController < ActionController::Base
@@ -49,11 +67,23 @@ class ApplicationController < ActionController::Base
49
67
  end
50
68
  ```
51
69
 
52
- This tells acts_as_tenant to use the current subdomain to identify the current tenant. In addition, it tells acts_as_tenant that tenants are represented by the Account model and this model has a column named 'subdomain' which can be used to lookup the Account using the actual subdomain. If ommitted, the parameters will default to the values used above.
70
+ This tells acts_as_tenant to use the last subdomain to identify the current tenant. In addition, it tells acts_as_tenant that tenants are represented by the Account model and this model has a column named 'subdomain' which can be used to lookup the Account using the actual subdomain. If ommitted, the parameters will default to the values used above.
71
+
72
+ By default, the *last* subdomain will be used for lookup. Pass in `subdomain_lookup: :first` to use the first subdomain instead.
73
+
74
+ #### By Domain to lookup the current tenant
75
+
76
+ ```ruby
77
+ class ApplicationController < ActionController::Base
78
+ set_current_tenant_by_subdomain_or_domain(:account, :subdomain, :domain)
79
+ end
80
+ ```
81
+
82
+ You can locate the tenant using `set_current_tenant_by_subdomain_or_domain( :account, :subdomain, :domain )` which will check for a subdomain and fallback to domain.
53
83
 
54
- Alternatively, you could locate the tenant using the method `set_current_tenant_by_subdomain_or_domain( :account, :subdomain, :domain )` which will try to match a record first by subdomain. in case it fails, by domain.
84
+ By default, the *last* subdomain will be used for lookup. Pass in `subdomain_lookup: :first` to use the first subdomain instead.
55
85
 
56
- ### Setting the current tenant in a controller, manually ###
86
+ #### Manually using before_action
57
87
 
58
88
  ```ruby
59
89
  class ApplicationController < ActionController::Base
@@ -69,6 +99,21 @@ end
69
99
 
70
100
  Setting the `current_tenant` yourself, requires you to declare `set_current_tenant_through_filter` at the top of your application_controller to tell acts_as_tenant that you are going to use a before_action to setup the current tenant. Next you should actually setup that before_action to fetch the current tenant and pass it to `acts_as_tenant` by using `set_current_tenant(current_tenant)` in the before_action.
71
101
 
102
+ If you are setting the tenant in a specific controller (except `application_controller`), it should to be included **AT THE TOP** of the file.
103
+
104
+ ```ruby
105
+ class MembersController < ActionController::Base
106
+ set_current_tenant_through_filter
107
+ before_action :set_tenant
108
+ before_action :set_member, only: [:show, :edit, :update, :destroy]
109
+
110
+ def set_tenant
111
+ set_current_tenant(current_user.account)
112
+ end
113
+ end
114
+ ```
115
+
116
+ This allows the tenant to be set before any other code runs so everything is within the current tenant.
72
117
 
73
118
  ### Setting the current tenant for a block ###
74
119
 
@@ -151,10 +196,40 @@ All options available to Rails' own `validates_uniqueness_of` are also available
151
196
 
152
197
  ### Custom foreign_key ###
153
198
 
154
- You can explicitely specifiy a foreign_key for AaT to use should the key differ from the default:
199
+ You can explicitly specifiy a foreign_key for AaT to use should the key differ from the default:
200
+
201
+ ```ruby
202
+ acts_as_tenant(:account, :foreign_key => 'accountID') # by default AaT expects account_id
203
+ ```
204
+
205
+ ### Custom primary_key ###
206
+
207
+ You can also explicitly specifiy a primary_key for AaT to use should the key differ from the default:
155
208
 
156
209
  ```ruby
157
- acts_as_tenant(:account, :foreign_key => 'accountID) # by default AaT expects account_id
210
+ acts_as_tenant(:account, :primary_key => 'primaryID') # by default AaT expects id
211
+ ```
212
+
213
+
214
+ ### Has and belongs to many ###
215
+
216
+ You can scope a model that is part of a HABTM relationship by using the `through` option.
217
+
218
+ ```ruby
219
+ class Organisation < ActiveRecord::Base
220
+ has_many :organisations_users
221
+ has_many :users, through: :organisations_users
222
+ end
223
+
224
+ class User < ActiveRecord::Base
225
+ has_many :organisations_users
226
+ acts_as_tenant :organisation, through: :organisations_users
227
+ end
228
+
229
+ class OrganisationsUser < ActiveRecord::Base
230
+ belongs_to :user
231
+ acts_as_tenant :organisation
232
+ end
158
233
  ```
159
234
 
160
235
  Configuration options
@@ -170,6 +245,37 @@ end
170
245
 
171
246
  * `config.require_tenant` when set to true will raise an ActsAsTenant::NoTenant error whenever a query is made without a tenant set.
172
247
 
248
+ `config.require_tenant` can also be assigned a lambda that is evaluated at run time. For example:
249
+
250
+ ```ruby
251
+ ActsAsTenant.configure do |config|
252
+ config.require_tenant = lambda do
253
+ if $request_env.present?
254
+ return false if $request_env["REQUEST_PATH"].start_with?("/admin/")
255
+ end
256
+ end
257
+ end
258
+ ```
259
+
260
+ `ActsAsTenant.should_require_tenant?` is used to determine if a tenant is required in the current context, either by evaluating the lambda provided, or by returning the boolean value assigned to `config.require_tenant`.
261
+
262
+ belongs_to options
263
+ ---------------------
264
+ `acts_as_tenant :account` includes the belongs_to relationship.
265
+ So when using acts_as_tenant on a model, do not add `belongs_to :account` alongside `acts_as_tenant :account`:
266
+
267
+ ```ruby
268
+ class User < ActiveRecord::Base
269
+ acts_as_tenant(:account) # YES
270
+ belongs_to :account # REDUNDANT
271
+ end
272
+ ```
273
+
274
+ You can add the following `belongs_to` options to `acts_as_tenant`:
275
+ `:foreign_key, :class_name, :inverse_of, :optional, :primary_key, :counter_cache`
276
+
277
+ Example: `acts_as_tenant(:account, counter_cache: true)`
278
+
173
279
  Sidekiq support
174
280
  ---------------
175
281
 
@@ -183,24 +289,42 @@ require 'acts_as_tenant/sidekiq'
183
289
  Testing
184
290
  ---------------
185
291
 
186
- If you set the `current_tenant` in your tests, make sure to clean up the tenant after each test by calling `ActsAsTenant.current_tenant = nil`. If you are manually setting the `current_tenant` in integration tests, please be aware that the value will not survive across multiple requests, even if they take place within the same test.
292
+ If you set the `current_tenant` in your tests, make sure to clean up the tenant after each test by calling `ActsAsTenant.current_tenant = nil`. Integration tests are more difficult: manually setting the `current_tenant` value will not survive across multiple requests, even if they take place within the same test. This can result in undesired boilerplate to set the desired tenant. Moreover, the efficacy of the test can be compromised because the set `current_tenant` value will carry over into the request-response cycle.
187
293
 
188
- If you'd like to set a default tenant that will survive across multiple requests, assign a value to `default_tenant`. You might use a before hook like this:
294
+ To address this issue, ActsAsTenant provides for a `test_tenant` value that can be set to allow for setup and post-request expectation testing. It should be used in conjunction with middleware that clears out this value while an integration test is processing. A typical Rails and RSpec setup might look like:
189
295
 
190
296
  ```ruby
191
- # Make the default tenant globally available to the tests
192
- $default_account = Account.create!
193
- # Specify this account as the current tenant unless overridden
194
- ActsAsTenant.default_tenant = $default_account
195
- # Stub out the method setting a tenant in a controller hook
196
- allow_any_instance_of(ApplicationController).to receive(:set_current_tenant)
297
+ # test.rb
298
+ require_dependency 'acts_as_tenant/test_tenant_middleware'
299
+
300
+ Rails.application.configure do
301
+ config.middleware.use ActsAsTenant::TestTenantMiddleware
302
+ end
197
303
  ```
198
304
 
199
- This can later be overridden by using any of the standard methods for specifying a different tenant. If you don't want this setting to apply to all of your tests, remember to clear it when you're finished by setting `ActsAsTenant.default_tenant = nil`.
305
+ ```ruby
306
+ # spec_helper.rb
307
+ config.before(:suite) do |example|
308
+ # Make the default tenant globally available to the tests
309
+ $default_account = Account.create!
310
+ end
311
+
312
+ config.before(:each) do |example|
313
+ if example.metadata[:type] == :request
314
+ # Set the `test_tenant` value for integration tests
315
+ ActsAsTenant.test_tenant = $default_account
316
+ else
317
+ # Otherwise just use current_tenant
318
+ ActsAsTenant.current_tenant = $default_account
319
+ end
320
+ end
200
321
 
201
- To Do
202
- -----
203
- * ...
322
+ config.after(:each) do |example|
323
+ # Clear any tenancy that might have been set
324
+ ActsAsTenant.current_tenant = nil
325
+ ActsAsTenant.test_tenant = nil
326
+ end
327
+ ```
204
328
 
205
329
  Bug reports & suggested improvements
206
330
  ------------------------------------
@@ -210,13 +334,20 @@ If you have found a bug or want to suggest an improvement, please use our issue
210
334
 
211
335
  If you want to contribute, fork the project, code your improvements and make a pull request on [Github](http://github.com/ErwinM/acts_as_tenant/). When doing so, please don't forget to add tests. If your contribution is fixing a bug it would be perfect if you could also submit a failing test, illustrating the issue.
212
336
 
213
- Help maintain this gem
214
- ----------------------
215
- I myself, do not work with RoR much anymore. As a result, I only check this repo a few time a year. If wants to help me maintain this gem on a more regular basis, shoot me a message!
337
+ Contributing to this gem
338
+ ------------------------
339
+
340
+ We use the Appraisal gem to run tests against supported versions of Rails to test for compatibility against them all. StandardRb also helps keep code formatted cleanly.
341
+
342
+ 1. Fork the repo
343
+ 2. Make changes
344
+ 3. Run test suite with `bundle exec appraisal`
345
+ 4. Run `bundle exec standardrb` to standardize code formatting
346
+ 5. Submit a PR
216
347
 
217
348
  Author & Credits
218
349
  ----------------
219
- acts_as_tenant is written by Erwin Matthijssen.
350
+ acts_as_tenant is written by Erwin Matthijssen & Chris Oliver.
220
351
 
221
352
  This gem was inspired by Ryan Sonnek's [Multitenant](https://github.com/wireframe/multitenant) gem and its use of default_scope.
222
353
 
data/Rakefile CHANGED
@@ -1,5 +1,7 @@
1
- require 'bundler/gem_tasks'
1
+ require "bundler/gem_tasks"
2
2
 
3
- require 'rspec/core/rake_task'
3
+ APP_RAKEFILE = File.expand_path("spec/dummy/Rakefile", __dir__)
4
+
5
+ require "rspec/core/rake_task"
4
6
  RSpec::Core::RakeTask.new(:spec)
5
- task :default => :spec
7
+ task default: :spec
@@ -1,26 +1,13 @@
1
1
  module ActsAsTenant
2
- @@configuration = nil
3
-
4
- def self.configure
5
- @@configuration = Configuration.new
6
-
7
- if block_given?
8
- yield configuration
9
- end
10
-
11
- configuration
12
- end
13
-
14
- def self.configuration
15
- @@configuration || configure
16
- end
17
-
18
2
  class Configuration
19
- attr_writer :require_tenant
20
-
3
+ attr_writer :require_tenant, :pkey
4
+
21
5
  def require_tenant
22
6
  @require_tenant ||= false
23
7
  end
24
-
8
+
9
+ def pkey
10
+ @pkey ||= :id
11
+ end
25
12
  end
26
13
  end
@@ -0,0 +1,13 @@
1
+ module ActsAsTenant
2
+ module ControllerExtensions
3
+ module Filter
4
+ extend ActiveSupport::Concern
5
+
6
+ private
7
+
8
+ def set_current_tenant(current_tenant_object)
9
+ ActsAsTenant.current_tenant = current_tenant_object
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,20 @@
1
+ module ActsAsTenant
2
+ module ControllerExtensions
3
+ module Subdomain
4
+ extend ActiveSupport::Concern
5
+
6
+ included do
7
+ cattr_accessor :tenant_class, :tenant_column, :subdomain_lookup
8
+ before_action :find_tenant_by_subdomain
9
+ end
10
+
11
+ private
12
+
13
+ def find_tenant_by_subdomain
14
+ if (subdomain = request.subdomains.send(subdomain_lookup))
15
+ ActsAsTenant.current_tenant = tenant_class.where(tenant_column => subdomain.downcase).first
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,20 @@
1
+ module ActsAsTenant
2
+ module ControllerExtensions
3
+ module SubdomainOrDomain
4
+ extend ActiveSupport::Concern
5
+
6
+ included do
7
+ cattr_accessor :tenant_class, :tenant_primary_column, :tenant_second_column, :subdomain_lookup
8
+ before_action :find_tenant_by_subdomain_or_domain
9
+ end
10
+
11
+ private
12
+
13
+ def find_tenant_by_subdomain_or_domain
14
+ subdomain = request.subdomains.send(subdomain_lookup)
15
+ query = subdomain.present? ? {tenant_primary_column => subdomain.downcase} : {tenant_second_column => request.domain.downcase}
16
+ ActsAsTenant.current_tenant = tenant_class.where(query).first
17
+ end
18
+ end
19
+ end
20
+ end
@@ -1,85 +1,36 @@
1
1
  module ActsAsTenant
2
2
  module ControllerExtensions
3
+ autoload :Filter, "acts_as_tenant/controller_extensions/filter"
4
+ autoload :Subdomain, "acts_as_tenant/controller_extensions/subdomain"
5
+ autoload :SubdomainOrDomain, "acts_as_tenant/controller_extensions/subdomain_or_domain"
3
6
 
4
7
  # this method allows setting the current_tenant by reading the subdomain and looking
5
8
  # it up in the tenant-model passed to the method. The method will look for the subdomain
6
9
  # in a column referenced by the second argument.
7
- def set_current_tenant_by_subdomain(tenant = :account, column = :subdomain )
8
- self.class_eval do
9
- cattr_accessor :tenant_class, :tenant_column
10
- end
10
+ def set_current_tenant_by_subdomain(tenant = :account, column = :subdomain, subdomain_lookup: :last)
11
+ include Subdomain
11
12
 
12
13
  self.tenant_class = tenant.to_s.camelcase.constantize
13
14
  self.tenant_column = column.to_sym
14
-
15
- self.class_eval do
16
-
17
- before_action :find_tenant_by_subdomain
18
- helper_method :current_tenant if respond_to?(:helper_method)
19
-
20
-
21
- private
22
- def find_tenant_by_subdomain
23
- if request.subdomains.last
24
- ActsAsTenant.current_tenant = tenant_class.where(tenant_column => request.subdomains.last.downcase).first
25
- end
26
- end
27
-
28
- def current_tenant
29
- ActsAsTenant.current_tenant
30
- end
31
- end
15
+ self.subdomain_lookup = subdomain_lookup
32
16
  end
33
17
 
34
18
  # 01/27/2014 Christian Yerena / @preth00nker
35
19
  # this method adds the possibility of use the domain as a possible second argument to find
36
20
  # the current_tenant.
37
- def set_current_tenant_by_subdomain_or_domain(tenant = :account, primary_column = :subdomain, second_column = :domain )
38
- self.class_eval do
39
- cattr_accessor :tenant_class, :tenant_primary_column, :tenant_second_column
40
- end
21
+ def set_current_tenant_by_subdomain_or_domain(tenant = :account, primary_column = :subdomain, second_column = :domain, subdomain_lookup: :last)
22
+ include SubdomainOrDomain
41
23
 
42
24
  self.tenant_class = tenant.to_s.camelcase.constantize
43
25
  self.tenant_primary_column = primary_column.to_sym
44
26
  self.tenant_second_column = second_column.to_sym
45
-
46
- self.class_eval do
47
-
48
- before_action :find_tenant_by_subdomain_or_domain
49
- helper_method :current_tenant if respond_to?(:helper_method)
50
-
51
-
52
- private
53
- def find_tenant_by_subdomain_or_domain
54
- if request.subdomains.last
55
- ActsAsTenant.current_tenant = tenant_class.where(tenant_primary_column => request.subdomains.last.downcase).first
56
- else
57
- ActsAsTenant.current_tenant = tenant_class.where(tenant_second_column => request.domain.downcase).first
58
- end
59
- end
60
-
61
- def current_tenant
62
- ActsAsTenant.current_tenant
63
- end
64
- end
27
+ self.subdomain_lookup = subdomain_lookup
65
28
  end
66
29
 
67
-
68
30
  # This method sets up a method that allows manual setting of the current_tenant. This method should
69
31
  # be used in a before_action. In addition, a helper is setup that returns the current_tenant
70
32
  def set_current_tenant_through_filter
71
- self.class_eval do
72
- helper_method :current_tenant if respond_to?(:helper_method)
73
-
74
- private
75
- def set_current_tenant(current_tenant_object)
76
- ActsAsTenant.current_tenant = current_tenant_object
77
- end
78
-
79
- def current_tenant
80
- ActsAsTenant.current_tenant
81
- end
82
- end
33
+ include Filter
83
34
  end
84
35
  end
85
36
  end
@@ -5,15 +5,11 @@ module ActsAsTenant
5
5
  module Errors
6
6
  class ModelNotScopedByTenant < ActsAsTenant::Error
7
7
  end
8
-
8
+
9
9
  class NoTenantSet < ActsAsTenant::Error
10
10
  end
11
-
12
- class ModelNotScopedByTenant < ActsAsTenant::Error
13
- end
14
-
11
+
15
12
  class TenantIsImmutable < ActsAsTenant::Error
16
13
  end
17
-
18
14
  end
19
15
  end