acts_as_multi_tenant 0.5.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/LICENSE +20 -0
- data/README.md +128 -0
- data/lib/acts_as_multi_tenant.rb +7 -0
- data/lib/multi_tenant/acts_as_tenant.rb +114 -0
- data/lib/multi_tenant/belongs_to_tenant.rb +70 -0
- data/lib/multi_tenant/belongs_to_tenant_through.rb +49 -0
- data/lib/multi_tenant/middleware.rb +114 -0
- data/lib/multi_tenant/proxies_to_tenant.rb +101 -0
- data/lib/multi_tenant/version.rb +4 -0
- metadata +88 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: a7efff0258142b5081c4778a8626aceefd8d78cb
|
4
|
+
data.tar.gz: 6305edc6f12418e6ca9e02924c85efe276c1d18e
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: f847b489613a624ee807987c4b83c44215ebb9cad639a98f62477e4570f4636cf14997577a97121ba5491983179a0bb5780fa5108722b468713ab3e33b6c3be4
|
7
|
+
data.tar.gz: 7a7600f3c384445fc3c7e513bed0525bcd90778c46c7afefd2a3934c5b5187072d45076b348370f1b2210c7e0395f46ae31da5f0e00da9e19971c43b2a2d0ac7
|
data/LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2017 Consolo Services Group, Inc.
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
4
|
+
of this software and associated documentation files (the "Software"), to deal
|
5
|
+
in the Software without restriction, including without limitation the rights
|
6
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
7
|
+
copies of the Software, and to permit persons to whom the Software is
|
8
|
+
furnished to do so, subject to the following conditions:
|
9
|
+
|
10
|
+
The above copyright notice and this permission notice shall be included in
|
11
|
+
all copies or substantial portions of the Software.
|
12
|
+
|
13
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
14
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
15
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
16
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
17
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
18
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
19
|
+
THE SOFTWARE.
|
20
|
+
|
data/README.md
ADDED
@@ -0,0 +1,128 @@
|
|
1
|
+
# acts_as_multi_tenant
|
2
|
+
|
3
|
+
Keep multiple tenants in a single ActiveRecord database, and keep their data separate. Let's say the `Client` AR model represents your "tenants". Some Rack middleware will keep track of the "current" client in the request cycle, which will automatically filter *all ActiveRecord queries* by that client, and automatically associate new records to the client as well.
|
4
|
+
|
5
|
+
There are 3 main components:
|
6
|
+
|
7
|
+
* `MultiTenant::Middleware` Rack middleware to wrap the request with the current tenant
|
8
|
+
* `acts_as_tenant` ActiveRecord macro to specify which model houses all those tenants
|
9
|
+
* `belongs_to_tenant` Extention of the ActiveRecord `belongs_to` macro to specify that a model belongs to a tenant
|
10
|
+
|
11
|
+
## MultiTenant::Middleware
|
12
|
+
|
13
|
+
Add the middleware in **config.ru**, or wherever you add middleware.
|
14
|
+
|
15
|
+
```ruby
|
16
|
+
use MultiTenant::Middleware,
|
17
|
+
# (required) The tenant model we want to set "current" on.
|
18
|
+
# Can be the class String name, a Proc, the class itself, or the class + a scope.
|
19
|
+
model: -> { Client.active },
|
20
|
+
|
21
|
+
# (required) Fetch the identifier of the current tenant from a Rack::Request object
|
22
|
+
identifier: ->(req) { req.host.split(/\./)[0] },
|
23
|
+
|
24
|
+
# (optional) Array of tentants that don't exist in the database, but should be allowed through anyway.
|
25
|
+
# IMPORTANT For these, Tenant.current will be nil!
|
26
|
+
global_identifiers: %w(global),
|
27
|
+
|
28
|
+
# (optional) Array of Strings or Regexps for paths that don't require a tenant. Only applies
|
29
|
+
# when the tenant isn't specified in the request - not when a given tenant can't be found.
|
30
|
+
global_paths: [
|
31
|
+
'/about',
|
32
|
+
%r{^/api/v\d+/login$},
|
33
|
+
],
|
34
|
+
|
35
|
+
# (optional) Returns a Rack response when a tenant couldn't be found in the db, or when
|
36
|
+
# a tenant isn't given (and isn't in the `global_paths` list)
|
37
|
+
not_found: ->(x) {
|
38
|
+
body = {errors: ["'%s' is not a valid tenant!" % x]}.to_json
|
39
|
+
[400, {'Content-Type' => 'application/json', 'Content-Length' => body.size.to_s}, [body]]
|
40
|
+
}
|
41
|
+
```
|
42
|
+
|
43
|
+
## acts_as_tenant
|
44
|
+
|
45
|
+
Let's say that your tenants are stored with the `Client` model, and the `code` column stores each client's unique lookup/identifier value.
|
46
|
+
|
47
|
+
```ruby
|
48
|
+
class Client < ActiveRecord::Base
|
49
|
+
acts_as_tenant using: :code
|
50
|
+
end
|
51
|
+
```
|
52
|
+
|
53
|
+
## belongs_to_tenant
|
54
|
+
|
55
|
+
Now tell your models that they belong to a tenant. You can use all the normal ActiveRecord `belongs_to` arguments, including the scope `Proc` and the options `Hash`.
|
56
|
+
|
57
|
+
```ruby
|
58
|
+
class Widget < ActiveRecord::Base
|
59
|
+
belongs_to_tenant :client
|
60
|
+
end
|
61
|
+
|
62
|
+
class Spline < ActiveRecord::Base
|
63
|
+
belongs_to_tenant :client
|
64
|
+
end
|
65
|
+
```
|
66
|
+
|
67
|
+
## belongs_to_tenant_through
|
68
|
+
|
69
|
+
Maybe you have a model that indirectly belongs to several tenants. For example, a User may have multiple Memberships, each of which belongs to a different Client.
|
70
|
+
|
71
|
+
```ruby
|
72
|
+
class User < ActiveRecord::Base
|
73
|
+
has_many :memberships
|
74
|
+
belongs_to_tenant_through :memberships
|
75
|
+
end
|
76
|
+
|
77
|
+
class Membership < ActiveRecord::Base
|
78
|
+
belongs_to_tenant :client
|
79
|
+
belongs_to :user
|
80
|
+
end
|
81
|
+
```
|
82
|
+
|
83
|
+
## proxies_to_tenant
|
84
|
+
|
85
|
+
Let's say you need a layer of indirection between clients and their records, to allow multiple clients to all share their records. Let's call it a License: several clients can be signed onto a single license, and records are associated to the license itself. Therefore, all clients with that license will share a single pool of records.
|
86
|
+
|
87
|
+
See the full documenation for MultiTenant::ProxiesToTenant for a list of compatible association configurations. But here's on example of a valid configuration:
|
88
|
+
|
89
|
+
```ruby
|
90
|
+
# The tenant model that's hooked up to the Rack middleware and holds the "current" tenant
|
91
|
+
class Client < ActiveRecord::Base
|
92
|
+
belongs_to :license
|
93
|
+
acts_as_tenant
|
94
|
+
end
|
95
|
+
|
96
|
+
# The proxy model that's (potentially) associated with multiple tenants
|
97
|
+
class License < ActiveRecord::Base
|
98
|
+
has_many :clients, inverse_of: :license
|
99
|
+
proxies_to_tenant :clients
|
100
|
+
end
|
101
|
+
|
102
|
+
# Widets will be associated to a License (instead of a Client), therefore they are automatically
|
103
|
+
# shared with all Clients who use that License.
|
104
|
+
class Widget < ActiveRecord::Base
|
105
|
+
belongs_to_tenant :license
|
106
|
+
has_many :clients, through: :license # not required - just for clarity
|
107
|
+
end
|
108
|
+
|
109
|
+
# Splines, on the other hand, still belong directly to individual Clients like normal.
|
110
|
+
class Spline < ActiveRecord::Base
|
111
|
+
belongs_to_tenant :client
|
112
|
+
end
|
113
|
+
|
114
|
+
# This is how it works behind the scenes
|
115
|
+
License.current == Client.current.license
|
116
|
+
```
|
117
|
+
|
118
|
+
## Testing
|
119
|
+
|
120
|
+
bundle install
|
121
|
+
bundle exec rake test
|
122
|
+
|
123
|
+
By default, bundler will install the latest (supported) version of ActiveRecord. To specify a version to test against, run:
|
124
|
+
|
125
|
+
AR=4.2 bundle update activerecord
|
126
|
+
bundle exec rake test
|
127
|
+
|
128
|
+
Look inside `Gemfile` to see all testable versions.
|
@@ -0,0 +1,7 @@
|
|
1
|
+
require 'active_record'
|
2
|
+
require_relative 'multi_tenant/version'
|
3
|
+
require_relative 'multi_tenant/acts_as_tenant'
|
4
|
+
require_relative 'multi_tenant/proxies_to_tenant'
|
5
|
+
require_relative 'multi_tenant/belongs_to_tenant'
|
6
|
+
require_relative 'multi_tenant/belongs_to_tenant_through'
|
7
|
+
require_relative 'multi_tenant/middleware'
|
@@ -0,0 +1,114 @@
|
|
1
|
+
#
|
2
|
+
# The main acts_as_multi_tenant module.
|
3
|
+
#
|
4
|
+
module MultiTenant
|
5
|
+
#
|
6
|
+
# Contains helpers to turn an ActiveRecord model into the tenant source.
|
7
|
+
#
|
8
|
+
module ActsAsTenant
|
9
|
+
#
|
10
|
+
# Use this ActiveRecord model as the tenant source.
|
11
|
+
#
|
12
|
+
# @param using [String] (optional) column that contains the unique lookup identifier. Defaults to :code.
|
13
|
+
#
|
14
|
+
def acts_as_tenant(using: :code)
|
15
|
+
cattr_accessor :tenant_identifier, :tenant_thread_var
|
16
|
+
self.tenant_identifier = using
|
17
|
+
self.tenant_thread_var = "current_tenant_#{object_id}".freeze # allows there to be multiple tenant classes
|
18
|
+
self.extend MultiTenant::ActsAsTenant::ClassMethods
|
19
|
+
end
|
20
|
+
|
21
|
+
#
|
22
|
+
# Returns true if this model is being used as a tenant.
|
23
|
+
#
|
24
|
+
# @return [Boolean]
|
25
|
+
#
|
26
|
+
def acts_as_tenant?
|
27
|
+
respond_to? :tenant_identifier
|
28
|
+
end
|
29
|
+
|
30
|
+
#
|
31
|
+
# Class methods applied to the tenant model.
|
32
|
+
#
|
33
|
+
# class Client < ActiveRecord::Base
|
34
|
+
# acts_as_tenant using: :code
|
35
|
+
# end
|
36
|
+
#
|
37
|
+
# Client.current
|
38
|
+
# => # the current client set by the middleware, or nil
|
39
|
+
#
|
40
|
+
# # Manually set the current client, where 'acme' is in the 'code' col in the db
|
41
|
+
# Client.current = 'acme'
|
42
|
+
#
|
43
|
+
# # Manually set the current client to an AR record
|
44
|
+
# Client.current
|
45
|
+
#
|
46
|
+
module ClassMethods
|
47
|
+
#
|
48
|
+
# Return the current tenant record, if any. Thread-safe.
|
49
|
+
#
|
50
|
+
# @return the current tenant record
|
51
|
+
#
|
52
|
+
def current
|
53
|
+
Thread.current.thread_variable_get tenant_thread_var
|
54
|
+
end
|
55
|
+
|
56
|
+
#
|
57
|
+
# Set the current tenant record. You may either pass an ActiveRecord Client record, OR the value
|
58
|
+
# of the `:using` option you passed to `acts_as_tenant`. Thread-safe.
|
59
|
+
#
|
60
|
+
# @param record_or_identifier the record or the identifier in the 'tenant_identifier' column.
|
61
|
+
#
|
62
|
+
def current=(record_or_identifier)
|
63
|
+
obj = if record_or_identifier.is_a? self
|
64
|
+
record_or_identifier
|
65
|
+
elsif record_or_identifier
|
66
|
+
where({tenant_identifier => record_or_identifier}).first
|
67
|
+
else
|
68
|
+
nil
|
69
|
+
end
|
70
|
+
Thread.current.thread_variable_set tenant_thread_var, obj
|
71
|
+
end
|
72
|
+
|
73
|
+
#
|
74
|
+
# Loops through each tenant, sets it as current, and yields to any given block.
|
75
|
+
# At the end, current is always set back to what it was originally.
|
76
|
+
#
|
77
|
+
def with_each_tenant
|
78
|
+
old_current = self.current
|
79
|
+
all.each do |tenant|
|
80
|
+
self.current = tenant
|
81
|
+
yield if block_given?
|
82
|
+
end
|
83
|
+
ensure
|
84
|
+
self.current = old_current
|
85
|
+
end
|
86
|
+
|
87
|
+
#
|
88
|
+
# Sets the given tenant as the current one and yields to a given block.
|
89
|
+
# At the end, current is always set back to what it was originally.
|
90
|
+
#
|
91
|
+
def with_tenant(record_or_identifier)
|
92
|
+
old_current = self.current
|
93
|
+
self.current = record_or_identifier
|
94
|
+
yield if block_given?
|
95
|
+
ensure
|
96
|
+
self.current = old_current
|
97
|
+
end
|
98
|
+
|
99
|
+
#
|
100
|
+
# Sets current to nil and yields to the block.
|
101
|
+
# At the end, current is always set back to what it was originally.
|
102
|
+
#
|
103
|
+
def without_tenant
|
104
|
+
old_current = self.current
|
105
|
+
self.current = nil
|
106
|
+
yield if block_given?
|
107
|
+
ensure
|
108
|
+
self.current = old_current
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
ActiveRecord::Base.extend MultiTenant::ActsAsTenant
|
@@ -0,0 +1,70 @@
|
|
1
|
+
module MultiTenant
|
2
|
+
#
|
3
|
+
# Module with helpers for telling a model that it belongs to a tenant.
|
4
|
+
#
|
5
|
+
module BelongsToTenant
|
6
|
+
#
|
7
|
+
# Bind this models' records to a tenant. You *must* specify the association name, and you *may* follow it
|
8
|
+
# up with any of the standard 'belongs_to' arguments (i.e. a scope and/or an options Hash).
|
9
|
+
#
|
10
|
+
# class Widget < ActiveRecord::Base
|
11
|
+
# belongs_to_tenant :customer
|
12
|
+
# end
|
13
|
+
#
|
14
|
+
# @param association_name [Symbol] Name of the association to the tenant
|
15
|
+
# @param scope [Proc] (optional) Proc holding an Arel scope for the lookup - same that the normal `belongs_to` method accepts.
|
16
|
+
# @param options [Hash] (optional) Hash with association options - same that the normal `belongs_to` methods accepts.
|
17
|
+
#
|
18
|
+
def belongs_to_tenant(association_name, scope = nil, options = {})
|
19
|
+
belongs_to association_name, scope, options
|
20
|
+
reflection = reflections[association_name.to_s]
|
21
|
+
unless reflection.klass.acts_as_tenant? or reflection.klass.proxies_to_tenant?
|
22
|
+
raise "`belongs_to_tenant :#{association_name}` failed because #{reflection.klass.name} has not used `acts_as_tenant` or `proxies_to_tenant`."
|
23
|
+
end
|
24
|
+
|
25
|
+
cattr_accessor :tenant_class, :tenant_foreign_key, :tenant_primary_key
|
26
|
+
self.tenant_class = reflection.klass
|
27
|
+
self.tenant_foreign_key = reflection.foreign_key.to_sym
|
28
|
+
self.tenant_primary_key = reflection.active_record_primary_key.to_sym
|
29
|
+
|
30
|
+
before_validation :assign_to_tenant
|
31
|
+
validates_presence_of tenant_foreign_key
|
32
|
+
|
33
|
+
self.class_eval do
|
34
|
+
include MultiTenant::BelongsToTenant::InstanceMethods
|
35
|
+
|
36
|
+
default_scope {
|
37
|
+
current = tenant_class.current
|
38
|
+
current ? where({tenant_foreign_key => current.send(tenant_primary_key)}) : where('1=1')
|
39
|
+
}
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
#
|
44
|
+
# Returns true if this model belongs to a tenant.
|
45
|
+
#
|
46
|
+
# @return [Boolean]
|
47
|
+
#
|
48
|
+
def belongs_to_tenant?
|
49
|
+
respond_to? :tenant_class
|
50
|
+
end
|
51
|
+
|
52
|
+
#
|
53
|
+
# Instance methods given to tenant-owned models.
|
54
|
+
#
|
55
|
+
module InstanceMethods
|
56
|
+
private
|
57
|
+
|
58
|
+
#
|
59
|
+
# Assign this model to the current tenant (if any)
|
60
|
+
#
|
61
|
+
def assign_to_tenant
|
62
|
+
if self.class.tenant_class.current and send(self.class.tenant_foreign_key).blank?
|
63
|
+
send "#{self.class.tenant_foreign_key}=", self.class.tenant_class.current.send(self.class.tenant_primary_key)
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
ActiveRecord::Base.extend MultiTenant::BelongsToTenant
|
@@ -0,0 +1,49 @@
|
|
1
|
+
module MultiTenant
|
2
|
+
#
|
3
|
+
# Module with helpers for telling a model that it belongs to a tenant through one of its associations.
|
4
|
+
#
|
5
|
+
module BelongsToTenantThrough
|
6
|
+
#
|
7
|
+
# Declare that this model has an association that belongs to a tenant. The assocation must be declared
|
8
|
+
# BEFORE this is called.
|
9
|
+
#
|
10
|
+
# class User < ActiveRecord::Base
|
11
|
+
# has_many :memberships
|
12
|
+
# belongs_to_tenant_through :memberships
|
13
|
+
# end
|
14
|
+
#
|
15
|
+
# @param association_name [Symbol] Name of the association to the tenant
|
16
|
+
#
|
17
|
+
def belongs_to_tenant_through(association_name)
|
18
|
+
ref = reflections[association_name.to_s]
|
19
|
+
raise "`belongs_to_tenant_through :#{association_name}` failed because the association `:#{association_name}` has not been declared" if ref.nil?
|
20
|
+
raise "`belongs_to_tenant_through :#{association_name}` failed because #{ref.klass.name} has not used `belongs_to_tenant`" unless ref.klass.belongs_to_tenant?
|
21
|
+
|
22
|
+
cattr_accessor :delegate_class
|
23
|
+
self.delegate_class = ref.klass
|
24
|
+
|
25
|
+
self.class_eval do
|
26
|
+
default_scope {
|
27
|
+
tenant = delegate_class.tenant_class.current
|
28
|
+
next where('1=1') if tenant.nil?
|
29
|
+
|
30
|
+
# Using straight sql so we can JOIN against two columns. Otherwise one must go into "WHERE", and Arel would mistakenly apply it to UPDATEs and DELETEs.
|
31
|
+
quoted_tenant_id = connection.quote tenant.send delegate_class.tenant_primary_key
|
32
|
+
joins("INNER JOIN #{ref.klass.table_name} ON #{ref.klass.table_name}.#{ref.foreign_key}=#{table_name}.#{ref.active_record_primary_key} AND #{ref.klass.table_name}.#{ref.klass.tenant_foreign_key}=#{quoted_tenant_id}").
|
33
|
+
readonly(false) # using "joins" makes records readonly, which we don't want
|
34
|
+
}
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
#
|
39
|
+
# Returns true if this model belongs to a tenant through one of its associations.
|
40
|
+
#
|
41
|
+
# @return [Boolean]
|
42
|
+
#
|
43
|
+
def belongs_to_tenant_through?
|
44
|
+
respond_to? :delegate_class
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
ActiveRecord::Base.extend MultiTenant::BelongsToTenantThrough
|
@@ -0,0 +1,114 @@
|
|
1
|
+
require 'set'
|
2
|
+
require 'rack'
|
3
|
+
|
4
|
+
module MultiTenant
|
5
|
+
#
|
6
|
+
# Rack middleware that sets the current tenant during each request (in a thread-safe manner). During a request,
|
7
|
+
# you can access the current tenant from "Tenant.current", where 'Tenant' is name of the ActiveRecord model.
|
8
|
+
#
|
9
|
+
# use MultiTenant::Middleware,
|
10
|
+
# # The ActiveRecord model that represents the tenants. Or a Proc returning it, or it's String name.
|
11
|
+
# model: -> { Tenant },
|
12
|
+
#
|
13
|
+
# # A Proc that returns the tenant identifier that's used to look up the tenant. (i.e. :using option passed to acts_as_tenant)
|
14
|
+
# identifier: ->(req) { req.host.split(/\./)[0] },
|
15
|
+
#
|
16
|
+
# # (optional) Array of tentants that don't exist in the database, but should be allowed through anyway.
|
17
|
+
# # IMPORTANT For these, Tenant.current will be nil!
|
18
|
+
# global_identifiers: %w(global),
|
19
|
+
#
|
20
|
+
# # (optional) Array of Strings or Regexps for paths that don't require a tenant. Only applies
|
21
|
+
# # when the tenant isn't specified in the request - not when a given tenant can't be found.
|
22
|
+
# global_paths: [
|
23
|
+
# '/about',
|
24
|
+
# %r{^/api/v\d+/login$},
|
25
|
+
# ],
|
26
|
+
#
|
27
|
+
# # (optional) Returns a Rack response when a tenant couldn't be found in the db, or when
|
28
|
+
# # a tenant isn't given (and isn't in the `global_paths` list)
|
29
|
+
# not_found: ->(x) {
|
30
|
+
# body = {errors: ["'#{x}' is not a valid tenant. I'm sorry. I'm so sorry."]}.to_json
|
31
|
+
# [400, {'Content-Type' => 'application/json', 'Content-Length' => body.size.to_s}, [body]]
|
32
|
+
# }
|
33
|
+
#
|
34
|
+
class Middleware
|
35
|
+
# @return [Proc|String|Class] The ActiveRecord model that holds all the tenants
|
36
|
+
attr_accessor :model
|
37
|
+
|
38
|
+
# @return [Proc] A Proc which accepts a Rack::Request and returns some identifier for tenant lookup
|
39
|
+
attr_accessor :identifier
|
40
|
+
|
41
|
+
# @return [Set<String>] array of "fake" identifiers that will be allowed through, but without setting a current tentant
|
42
|
+
attr_accessor :global_identifiers
|
43
|
+
|
44
|
+
# @return [Set<String>] An array of path strings that don't requite a tenant to be given
|
45
|
+
attr_accessor :global_strings
|
46
|
+
|
47
|
+
# @return [Set<String>] An array of path regexes that don't requite a tenant to be given
|
48
|
+
attr_accessor :global_regexes
|
49
|
+
|
50
|
+
# @return [Proc] A Proc which accepts a (non-existent or blank) tenant identifier and returns a rack response describing
|
51
|
+
# the error. Defaults to a 404 and some shitty html.
|
52
|
+
attr_accessor :not_found
|
53
|
+
|
54
|
+
# Default Proc for the not_found option
|
55
|
+
DEFAULT_NOT_FOUND = ->(x) {
|
56
|
+
[404, {'Content-Type' => 'text/html', 'Content-Length' => (33 + x.to_s.size).to_s}, ['<h1>\'%s\' is not a valid tenant</h1>' % x.to_s]]
|
57
|
+
}
|
58
|
+
|
59
|
+
#
|
60
|
+
# Initialize a new multi tenant Rack middleware.
|
61
|
+
#
|
62
|
+
# @param app the Rack app
|
63
|
+
# @param opts [Hash] Required: :model, :identifier. Optional: :global_identifiers, :global_paths, :not_found.
|
64
|
+
#
|
65
|
+
def initialize(app, opts)
|
66
|
+
@app = app
|
67
|
+
self.model = opts.fetch :model
|
68
|
+
self.identifier = opts.fetch :identifier
|
69
|
+
self.global_identifiers = Set.new(Array(opts[:global_identifiers]))
|
70
|
+
self.global_strings = Set.new(Array(opts[:global_paths]).select { |x| x.is_a? String })
|
71
|
+
self.global_regexes = Array(opts[:global_paths]).select { |x| x.is_a? Regexp }
|
72
|
+
self.not_found = opts[:not_found] || DEFAULT_NOT_FOUND
|
73
|
+
end
|
74
|
+
|
75
|
+
# Rack request call
|
76
|
+
def call(env)
|
77
|
+
request = Rack::Request.new env
|
78
|
+
tenant_identifier = identifier.(request)
|
79
|
+
|
80
|
+
if tenant_identifier.blank?
|
81
|
+
if global_strings.include? request.path or global_regexes.any? { |x| x =~ request.path }
|
82
|
+
tenant_class.current = nil
|
83
|
+
return @app.call env
|
84
|
+
else
|
85
|
+
return not_found.(tenant_identifier)
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
tenant_record = tenant_identifier.present? ? tenant_class.where({tenant_class.tenant_identifier => tenant_identifier}).first : nil
|
90
|
+
if tenant_record
|
91
|
+
tenant_class.current = tenant_record
|
92
|
+
return @app.call env
|
93
|
+
elsif global_identifiers.include? tenant_identifier
|
94
|
+
tenant_class.current = nil
|
95
|
+
return @app.call env
|
96
|
+
else
|
97
|
+
return not_found.(tenant_identifier)
|
98
|
+
end
|
99
|
+
ensure
|
100
|
+
tenant_class.current = nil
|
101
|
+
end
|
102
|
+
|
103
|
+
# Infers and returns the tenant model class this middleware is handling
|
104
|
+
def tenant_class
|
105
|
+
@tenant_class ||= if self.model.respond_to?(:call)
|
106
|
+
self.model.call
|
107
|
+
elsif self.model.respond_to?(:constantize)
|
108
|
+
self.model.constantize
|
109
|
+
else
|
110
|
+
self.model
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
@@ -0,0 +1,101 @@
|
|
1
|
+
module MultiTenant
|
2
|
+
#
|
3
|
+
# Helpers for setting a proxy model to your tenant model. So your records can `acts_as_tenant` to the proxy model instead of directly to the tenant.
|
4
|
+
#
|
5
|
+
# However, only certain types of associations are supported. (We could probably support all types, but since each type requires a special implementation, we've only added the ones we've needed so far.)
|
6
|
+
#
|
7
|
+
# Configuration I: has_many, inverse of belongs_to
|
8
|
+
#
|
9
|
+
# # The tenant model that's hooked up to the Rack middleware and holds the "current" tenant
|
10
|
+
# class Client < ActiveRecord::Base
|
11
|
+
# belongs_to :license
|
12
|
+
# acts_as_tenant
|
13
|
+
# end
|
14
|
+
#
|
15
|
+
# # The proxy model that's (potentially) associated with multiple tenants
|
16
|
+
# class License < ActiveRecord::Base
|
17
|
+
# has_many :clients, inverse_of: :license
|
18
|
+
# proxies_to_tenant :clients
|
19
|
+
# end
|
20
|
+
#
|
21
|
+
# # Widets will be associated to a License (instead of a Client), therefore they are automatically
|
22
|
+
# # shared with all Clients who use that License.
|
23
|
+
# class Widget < ActiveRecord::Base
|
24
|
+
# belongs_to_tenant :license
|
25
|
+
# has_many :clients, through: :license # not required - just for clarity
|
26
|
+
# end
|
27
|
+
#
|
28
|
+
# # Splines, on the other hand, still belong directly to individual Clients like normal.
|
29
|
+
# class Spline < ActiveRecord::Base
|
30
|
+
# belongs_to_tenant :client
|
31
|
+
# end
|
32
|
+
#
|
33
|
+
# # This is what's going on behind the scenes. Not too complicated, all things considered.
|
34
|
+
# License.current == Client.current.license
|
35
|
+
#
|
36
|
+
# Configuration II: has_one, inverse of belongs_to:
|
37
|
+
# License has_one Client, and Client belongs_to License.
|
38
|
+
#
|
39
|
+
# Configuration III: belongs_to, inverse of has_one:
|
40
|
+
# License belongs_to Client, and Client has_one License.
|
41
|
+
#
|
42
|
+
module ProxiesToTenant
|
43
|
+
#
|
44
|
+
# Declare a model as a proxy to tenant model.
|
45
|
+
#
|
46
|
+
# @param association_name [Symbol] the association that's the *real* tenant. You must define the association yourself (e.g. belongs_to) along with the `:inverse_of` option.
|
47
|
+
# @param scope [Proc] (optional) An AR scope that will be run *against the proxy model*, i.e. *this* model. Useful for when the association's `:inverse_of` is a `has_many` or `has_many_and_belongs_to`.
|
48
|
+
#
|
49
|
+
def proxies_to_tenant(association_name, scope = nil)
|
50
|
+
reflection = reflections[association_name.to_s]
|
51
|
+
raise "`proxies_to_tenant :#{association_name}`: unable to find association `:#{association_name}`. Make sure you create the association *first*." if reflection.nil?
|
52
|
+
raise "`proxies_to_tenant :#{association_name}`: #{reflection.klass.name} must use `acts_as_tenant`" if !reflection.klass.acts_as_tenant?
|
53
|
+
raise "`proxies_to_tenant :#{association_name}`: the `:#{association_name}` association must use the `:inverse_of` option." if reflection.inverse_of.nil?
|
54
|
+
|
55
|
+
case [reflection.macro, reflection.inverse_of.macro]
|
56
|
+
when [:has_many, :belongs_to], [:has_one, :belongs_to], [:belongs_to, :has_one]
|
57
|
+
self.extend SingularInverseAssociation
|
58
|
+
else
|
59
|
+
raise "`proxies_to_tenant` does not currently support `#{reflection.macro}` associations with `#{reflection.inverse_of.macro} inverses."
|
60
|
+
end
|
61
|
+
|
62
|
+
cattr_accessor :proxied_tenant_class, :proxied_tenant_inverse_assoc, :proxied_tenant_inverse_scope
|
63
|
+
self.proxied_tenant_class = reflection.klass
|
64
|
+
self.proxied_tenant_inverse_assoc = reflection.inverse_of.name
|
65
|
+
self.proxied_tenant_inverse_scope = scope
|
66
|
+
end
|
67
|
+
|
68
|
+
#
|
69
|
+
# Returns true if this model is proxying to a tenant.
|
70
|
+
#
|
71
|
+
# @return [Boolean]
|
72
|
+
#
|
73
|
+
def proxies_to_tenant?
|
74
|
+
respond_to? :proxied_tenant_class
|
75
|
+
end
|
76
|
+
|
77
|
+
private
|
78
|
+
|
79
|
+
# Class methods for tenant proxies that have a singular inverse association (i.e. belongs_to or has_one).
|
80
|
+
module SingularInverseAssociation
|
81
|
+
# Returns the "current" record of the proxy model
|
82
|
+
def current
|
83
|
+
if (tenant = proxied_tenant_class.current)
|
84
|
+
tenant.send proxied_tenant_inverse_assoc
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
# NOTE just some thoughts on *maybe* how to support this if we ever need it.
|
90
|
+
module PluralInverseAssociation
|
91
|
+
# Returns the "current" record of the proxy model
|
92
|
+
def current
|
93
|
+
if (tenant = proxied_tenant_class.current)
|
94
|
+
tenant.send(proxied_tenant_inverse_assoc).instance_eval(&proxied_tenant_inverse_scope).first
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
ActiveRecord::Base.extend MultiTenant::ProxiesToTenant
|
metadata
ADDED
@@ -0,0 +1,88 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: acts_as_multi_tenant
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.5.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Jordan Hollinger
|
8
|
+
- Andrew Coleman
|
9
|
+
- Taylor Redden
|
10
|
+
autorequire:
|
11
|
+
bindir: bin
|
12
|
+
cert_chain: []
|
13
|
+
date: 2017-08-18 00:00:00.000000000 Z
|
14
|
+
dependencies:
|
15
|
+
- !ruby/object:Gem::Dependency
|
16
|
+
name: activerecord
|
17
|
+
requirement: !ruby/object:Gem::Requirement
|
18
|
+
requirements:
|
19
|
+
- - ">="
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: '4.2'
|
22
|
+
- - "<"
|
23
|
+
- !ruby/object:Gem::Version
|
24
|
+
version: '5.2'
|
25
|
+
type: :runtime
|
26
|
+
prerelease: false
|
27
|
+
version_requirements: !ruby/object:Gem::Requirement
|
28
|
+
requirements:
|
29
|
+
- - ">="
|
30
|
+
- !ruby/object:Gem::Version
|
31
|
+
version: '4.2'
|
32
|
+
- - "<"
|
33
|
+
- !ruby/object:Gem::Version
|
34
|
+
version: '5.2'
|
35
|
+
- !ruby/object:Gem::Dependency
|
36
|
+
name: rack
|
37
|
+
requirement: !ruby/object:Gem::Requirement
|
38
|
+
requirements:
|
39
|
+
- - ">="
|
40
|
+
- !ruby/object:Gem::Version
|
41
|
+
version: '0'
|
42
|
+
type: :runtime
|
43
|
+
prerelease: false
|
44
|
+
version_requirements: !ruby/object:Gem::Requirement
|
45
|
+
requirements:
|
46
|
+
- - ">="
|
47
|
+
- !ruby/object:Gem::Version
|
48
|
+
version: '0'
|
49
|
+
description: An ActiveRecord plugin for multi-tenant databases
|
50
|
+
email: jordan.hollinger@gmail.com
|
51
|
+
executables: []
|
52
|
+
extensions: []
|
53
|
+
extra_rdoc_files: []
|
54
|
+
files:
|
55
|
+
- LICENSE
|
56
|
+
- README.md
|
57
|
+
- lib/acts_as_multi_tenant.rb
|
58
|
+
- lib/multi_tenant/acts_as_tenant.rb
|
59
|
+
- lib/multi_tenant/belongs_to_tenant.rb
|
60
|
+
- lib/multi_tenant/belongs_to_tenant_through.rb
|
61
|
+
- lib/multi_tenant/middleware.rb
|
62
|
+
- lib/multi_tenant/proxies_to_tenant.rb
|
63
|
+
- lib/multi_tenant/version.rb
|
64
|
+
homepage: https://github.com/consolo/acts_as_multi_tenant
|
65
|
+
licenses:
|
66
|
+
- MIT
|
67
|
+
metadata: {}
|
68
|
+
post_install_message:
|
69
|
+
rdoc_options: []
|
70
|
+
require_paths:
|
71
|
+
- lib
|
72
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
73
|
+
requirements:
|
74
|
+
- - ">="
|
75
|
+
- !ruby/object:Gem::Version
|
76
|
+
version: 2.1.0
|
77
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
78
|
+
requirements:
|
79
|
+
- - ">="
|
80
|
+
- !ruby/object:Gem::Version
|
81
|
+
version: '0'
|
82
|
+
requirements: []
|
83
|
+
rubyforge_project:
|
84
|
+
rubygems_version: 2.5.2
|
85
|
+
signing_key:
|
86
|
+
specification_version: 4
|
87
|
+
summary: An ActiveRecord plugin for multi-tenant databases
|
88
|
+
test_files: []
|