acts_as_multi_tenant 0.5.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 +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: []
|