acts_as_multi_tenant 1.2.1 → 2.0.0.pre.rc1
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 +4 -4
- data/README.md +18 -10
- data/lib/acts_as_multi_tenant.rb +1 -5
- data/lib/multi_tenant/acts_as_tenant.rb +85 -31
- data/lib/multi_tenant/belongs_to_tenant.rb +35 -2
- data/lib/multi_tenant/belongs_to_tenant_through.rb +10 -2
- data/lib/multi_tenant/middleware.rb +42 -12
- data/lib/multi_tenant/proxies_to_tenant.rb +35 -13
- data/lib/multi_tenant/version.rb +1 -1
- metadata +4 -6
- data/lib/multi_tenant/impl/multiple_current.rb +0 -104
- data/lib/multi_tenant/impl/single_current.rb +0 -124
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: f4138ddbff135b1b100c0ddaba90b7ac5ca35bdbce7f031790dc8bade47df719
|
4
|
+
data.tar.gz: d8ac485e9aadea630e26ec7dbc852f2e4593eff23bd9be069220dc0928429af9
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: b71084765bca184cb05428fa5121c5b15eaa23b9d3c6262c9508a3d70f6ab0d03c44f5458f6c6e33ff23888a5815de28d2d84a48b3dae50737d8d07b1254c0ae
|
7
|
+
data.tar.gz: bc9e15302492d77f48437523d2e2bf6a300b921b31bf90d3d406d140fd28d01972f05413497f88ef182281cd7c2adc6f7f9caf95ff0bce3939b9ff3e7b687f89
|
data/README.md
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
# acts_as_multi_tenant
|
2
2
|
|
3
|
-
Keep multiple tenants in a single ActiveRecord database
|
3
|
+
Keep multiple tenants in a single ActiveRecord database and keep their data separate. Let's say the `Client` AR model represents your "tenants". Rack middleware will keep track of the current client in the request cycle which will automatically filter *all ActiveRecord queries* by that client. New records will automatically be associated to that client as well.
|
4
4
|
|
5
5
|
There are 3 main components:
|
6
6
|
|
@@ -117,25 +117,33 @@ License.current == Client.current.license
|
|
117
117
|
|
118
118
|
## Multiple current tenants
|
119
119
|
|
120
|
-
Some applications may need to allow multiple current tenants at once. For example, a single user
|
120
|
+
Some applications may need to allow multiple current tenants at once. For example, a single user may have access to multiple clients. `acts_as_multi_tenant` has an API that allows getting and setting of multiple tenants. Queries will be filtered by ANY of them. Keep in mind that when creating new records the tenant_id column cannot automatically be set, since it doesn't know which tenant to use.
|
121
121
|
|
122
|
-
|
123
|
-
class Client < ActiveRecord::Base
|
124
|
-
acts_as_tenant using: :code, current: :multiple
|
125
|
-
end
|
126
|
-
```
|
127
|
-
|
128
|
-
When you add your middleware, your `identifier` option must also return an array:
|
122
|
+
When you add your middleware, the `identifiers` option must return an array:
|
129
123
|
|
130
124
|
```ruby
|
131
125
|
use MultiTenant::Middleware,
|
132
126
|
model: -> { Client.active },
|
133
127
|
|
134
|
-
|
128
|
+
identifiers: ->(req) {
|
135
129
|
req.params["clients"] || []
|
136
130
|
}
|
137
131
|
```
|
138
132
|
|
133
|
+
In application code, use the following pluralized methods instead of their singularized counterparts:
|
134
|
+
|
135
|
+
```ruby
|
136
|
+
Client.current_tenants = ["acme", "foo"]
|
137
|
+
|
138
|
+
Client.with_tenants ["acme", "foo"] do
|
139
|
+
# do stuff
|
140
|
+
end
|
141
|
+
|
142
|
+
Client.without_tenants do
|
143
|
+
# do stuff
|
144
|
+
end
|
145
|
+
```
|
146
|
+
|
139
147
|
## Testing
|
140
148
|
|
141
149
|
bundle install
|
data/lib/acts_as_multi_tenant.rb
CHANGED
@@ -7,9 +7,5 @@ require_relative 'multi_tenant/belongs_to_tenant_through'
|
|
7
7
|
require_relative 'multi_tenant/middleware'
|
8
8
|
|
9
9
|
module MultiTenant
|
10
|
-
|
11
|
-
NotImplemented = Class.new(StandardError)
|
12
|
-
autoload :SingleCurrent, 'multi_tenant/impl/single_current'
|
13
|
-
autoload :MultipleCurrent, 'multi_tenant/impl/multiple_current'
|
14
|
-
end
|
10
|
+
NotImplemented = Class.new(StandardError)
|
15
11
|
end
|
@@ -9,22 +9,15 @@ module MultiTenant
|
|
9
9
|
#
|
10
10
|
# Use this ActiveRecord model as the tenant source.
|
11
11
|
#
|
12
|
-
# The "current" option allows you to specify whether Client.current is a single record (the default) or an array of records.
|
13
|
-
#
|
14
12
|
# @param using [String] (optional) column that contains the unique lookup identifier. Defaults to :code.
|
15
|
-
# @param current [Symbol] :single | :multiple
|
16
13
|
#
|
17
|
-
def acts_as_tenant(using: :code
|
18
|
-
cattr_accessor :tenant_identifier, :tenant_thread_var
|
14
|
+
def acts_as_tenant(using: :code)
|
15
|
+
cattr_accessor :tenant_identifier, :tenant_thread_var
|
19
16
|
self.tenant_identifier = using
|
20
17
|
self.tenant_thread_var = "current_tenant_#{object_id}".freeze # allows there to be multiple tenant classes
|
21
|
-
self.
|
22
|
-
|
23
|
-
|
24
|
-
else raise ArgumentError, "Unknown current option '#{current}'"
|
25
|
-
end
|
26
|
-
self.extend MultiTenant::ActsAsTenant::ClassMethods
|
27
|
-
self.extend self.multi_tenant_impl.acts_as_tenant_class_methods
|
18
|
+
self.extend MultiTenant::ActsAsTenant::TenantGetters
|
19
|
+
self.extend MultiTenant::ActsAsTenant::TenantSetters
|
20
|
+
self.extend MultiTenant::ActsAsTenant::TenantHelpers
|
28
21
|
end
|
29
22
|
|
30
23
|
#
|
@@ -36,6 +29,44 @@ module MultiTenant
|
|
36
29
|
respond_to? :tenant_identifier
|
37
30
|
end
|
38
31
|
|
32
|
+
module TenantGetters
|
33
|
+
#
|
34
|
+
# Returns true if there are any current tenants set, false if not.
|
35
|
+
#
|
36
|
+
# @return [Boolean]
|
37
|
+
#
|
38
|
+
def current_tenants?
|
39
|
+
current_tenants.any?
|
40
|
+
end
|
41
|
+
alias_method :current?, :current_tenants?
|
42
|
+
alias_method :current_tenant?, :current_tenants?
|
43
|
+
|
44
|
+
#
|
45
|
+
# Returns the array of current tenants. Thread-safe.
|
46
|
+
#
|
47
|
+
# @return the array of tenant records
|
48
|
+
#
|
49
|
+
def current_tenants
|
50
|
+
Thread.current.thread_variable_get(tenant_thread_var) || []
|
51
|
+
end
|
52
|
+
|
53
|
+
#
|
54
|
+
# Return the current tenant record, if any. Thread-safe. If there are MULTIPLE current tenants set this will
|
55
|
+
# raise a RuntimeError.
|
56
|
+
#
|
57
|
+
# @return the current tenant record
|
58
|
+
#
|
59
|
+
def current_tenant
|
60
|
+
tenants = current_tenants
|
61
|
+
if tenants.size > 1
|
62
|
+
raise "#{self.name}.current/current_tenant was called when multiple current tenants were present?. Did you mean to call #{self.name}.current_tenants?"
|
63
|
+
else
|
64
|
+
tenants[0]
|
65
|
+
end
|
66
|
+
end
|
67
|
+
alias_method :current, :current_tenant
|
68
|
+
end
|
69
|
+
|
39
70
|
#
|
40
71
|
# Class methods applied to the tenant model.
|
41
72
|
#
|
@@ -52,39 +83,48 @@ module MultiTenant
|
|
52
83
|
# # Manually set the current client to an AR record
|
53
84
|
# Client.current
|
54
85
|
#
|
55
|
-
module
|
86
|
+
module TenantSetters
|
56
87
|
#
|
57
|
-
#
|
88
|
+
# Set the current tenant record. You may either pass an ActiveRecord Client record, OR the value
|
89
|
+
# of the `:using` option you passed to `acts_as_tenant`. Thread-safe.
|
58
90
|
#
|
59
|
-
# @
|
91
|
+
# @param record_or_identifier the record or the identifier in the 'tenant_identifier' column.
|
60
92
|
#
|
61
|
-
def
|
62
|
-
|
93
|
+
def current_tenant=(record_or_identifier)
|
94
|
+
self.current_tenants = Array(record_or_identifier)
|
63
95
|
end
|
96
|
+
alias_method :current=, :current_tenant=
|
64
97
|
|
65
98
|
#
|
66
|
-
# Set the current tenant
|
99
|
+
# Set the array of current tenant records. You may either pass an ActiveRecord Client record, OR the value
|
67
100
|
# of the `:using` option you passed to `acts_as_tenant`. Thread-safe.
|
68
101
|
#
|
69
|
-
# @param
|
102
|
+
# @param records_or_identifiers array of the records or identifiers in the 'tenant_identifier' column.
|
70
103
|
#
|
71
|
-
def
|
72
|
-
|
73
|
-
|
104
|
+
def current_tenants=(records_or_identifiers)
|
105
|
+
records, identifiers = Array(records_or_identifiers).partition { |x| x.is_a? self }
|
106
|
+
tenants = if identifiers.any?
|
107
|
+
records + where({tenant_identifier => identifiers}).to_a
|
108
|
+
else
|
109
|
+
records
|
110
|
+
end
|
111
|
+
Thread.current.thread_variable_set tenant_thread_var, tenants
|
74
112
|
end
|
113
|
+
end
|
75
114
|
|
115
|
+
module TenantHelpers
|
76
116
|
#
|
77
117
|
# Loops through each tenant, sets it as current, and yields to any given block.
|
78
118
|
# At the end, current is always set back to what it was originally.
|
79
119
|
#
|
80
120
|
def with_each_tenant
|
81
|
-
|
121
|
+
old_tenants = self.current_tenants
|
82
122
|
all.each do |tenant|
|
83
|
-
self.
|
123
|
+
self.current_tenant = tenant
|
84
124
|
yield if block_given?
|
85
125
|
end
|
86
126
|
ensure
|
87
|
-
self.
|
127
|
+
self.current_tenants = old_tenants
|
88
128
|
end
|
89
129
|
|
90
130
|
#
|
@@ -92,11 +132,23 @@ module MultiTenant
|
|
92
132
|
# At the end, current is always set back to what it was originally.
|
93
133
|
#
|
94
134
|
def with_tenant(record_or_identifier)
|
95
|
-
|
96
|
-
self.
|
135
|
+
old_tenants = self.current_tenants
|
136
|
+
self.current_tenant = record_or_identifier
|
97
137
|
yield if block_given?
|
98
138
|
ensure
|
99
|
-
self.
|
139
|
+
self.current_tenants = old_tenants
|
140
|
+
end
|
141
|
+
|
142
|
+
#
|
143
|
+
# Sets the given array of tenants as the current one and yields to a given block.
|
144
|
+
# At the end, current is always set back to what it was originally.
|
145
|
+
#
|
146
|
+
def with_tenants(records_or_identifiers)
|
147
|
+
old_tenants = self.current_tenants
|
148
|
+
self.current_tenants = records_or_identifiers
|
149
|
+
yield if block_given?
|
150
|
+
ensure
|
151
|
+
self.current_tenants = old_tenants
|
100
152
|
end
|
101
153
|
|
102
154
|
#
|
@@ -104,12 +156,14 @@ module MultiTenant
|
|
104
156
|
# At the end, current is always set back to what it was originally.
|
105
157
|
#
|
106
158
|
def without_tenant
|
107
|
-
|
108
|
-
self.
|
159
|
+
old_tenants = self.current_tenants
|
160
|
+
self.current_tenant = nil
|
109
161
|
yield if block_given?
|
110
162
|
ensure
|
111
|
-
self.
|
163
|
+
self.current_tenants = old_tenants
|
112
164
|
end
|
165
|
+
|
166
|
+
alias_method :without_tenants, :without_tenant
|
113
167
|
end
|
114
168
|
end
|
115
169
|
end
|
@@ -27,10 +27,16 @@ module MultiTenant
|
|
27
27
|
self.tenant_foreign_key = reflection.foreign_key.to_sym
|
28
28
|
self.tenant_primary_key = reflection.association_primary_key.to_sym
|
29
29
|
|
30
|
-
include
|
31
|
-
default_scope(&tenant_class.multi_tenant_impl.belongs_to_tenant_default_scope(self))
|
30
|
+
include MultiTenant::BelongsToTenant::InstanceMethods
|
32
31
|
|
32
|
+
before_validation :assign_to_current_tenant
|
33
33
|
validates_presence_of tenant_foreign_key
|
34
|
+
validate :ensure_assigned_to_current_tenants
|
35
|
+
|
36
|
+
default_scope {
|
37
|
+
current = tenant_class.current_tenants.map(&tenant_primary_key)
|
38
|
+
current.any? ? where({tenant_foreign_key => current}) : where('1=1')
|
39
|
+
}
|
34
40
|
end
|
35
41
|
|
36
42
|
#
|
@@ -41,6 +47,33 @@ module MultiTenant
|
|
41
47
|
def belongs_to_tenant?
|
42
48
|
respond_to? :tenant_class
|
43
49
|
end
|
50
|
+
|
51
|
+
module InstanceMethods
|
52
|
+
private
|
53
|
+
|
54
|
+
#
|
55
|
+
# Assign this model to the current tenant (if any). If there are multiple current tenants this is a no-op.
|
56
|
+
#
|
57
|
+
def assign_to_current_tenant
|
58
|
+
if self.class.tenant_class.current_tenants.size == 1
|
59
|
+
current_tenant_id = self.class.tenant_class.current_tenant.send(self.class.tenant_primary_key)
|
60
|
+
send "#{self.class.tenant_foreign_key}=", current_tenant_id
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
#
|
65
|
+
# If the tenant_id is set, make sure it's one of the current ones.
|
66
|
+
#
|
67
|
+
def ensure_assigned_to_current_tenants
|
68
|
+
_tenants_ids = self.class.tenant_class.current_tenants.map { |t|
|
69
|
+
t.send(self.class.tenant_primary_key).to_s
|
70
|
+
}
|
71
|
+
_current_id = send self.class.tenant_foreign_key
|
72
|
+
if _tenants_ids.any? and _current_id.present? and !_tenants_ids.include?(_current_id.to_s)
|
73
|
+
errors.add(self.class.tenant_foreign_key, "is incorrect")
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
44
77
|
end
|
45
78
|
end
|
46
79
|
|
@@ -22,8 +22,16 @@ module MultiTenant
|
|
22
22
|
cattr_accessor :delegate_class
|
23
23
|
self.delegate_class = ref.klass
|
24
24
|
|
25
|
-
|
26
|
-
|
25
|
+
default_scope {
|
26
|
+
tenants = delegate_class.tenant_class.current_tenants
|
27
|
+
next where('1=1') if tenants.empty?
|
28
|
+
|
29
|
+
# Using straight sql so we can JOIN against two columns. Otherwise one must go into "WHERE", and Arel would apply it to UPDATEs and DELETEs.
|
30
|
+
quoted_tenant_ids = tenants.map { |t| connection.quote t.send delegate_class.tenant_primary_key }
|
31
|
+
joins("INNER JOIN #{ref.klass.table_name} ON #{ref.klass.table_name}.#{ref.foreign_key}=#{table_name}.#{ref.association_primary_key} AND #{ref.klass.table_name}.#{ref.klass.tenant_foreign_key} IN (#{quoted_tenant_ids.join(',')})").
|
32
|
+
distinct.
|
33
|
+
readonly(false) # using "joins" makes records readonly, which we don't want
|
34
|
+
}
|
27
35
|
end
|
28
36
|
|
29
37
|
#
|
@@ -10,7 +10,8 @@ module MultiTenant
|
|
10
10
|
# # The ActiveRecord model that represents the tenants. Or a Proc returning it, or it's String name.
|
11
11
|
# model: -> { Tenant },
|
12
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)
|
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
|
+
# # Also aliased as "identifiers".
|
14
15
|
# identifier: ->(req) { req.host.split(/\./)[0] },
|
15
16
|
#
|
16
17
|
# # (optional) A Hash of fake identifiers that should be allowed through. Each identifier will have a
|
@@ -61,6 +62,7 @@ module MultiTenant
|
|
61
62
|
@app = app
|
62
63
|
self.model = opts.fetch :model
|
63
64
|
self.identifier = opts.fetch :identifier
|
65
|
+
self.identifier = opts[:identifier] || opts[:identifiers] || raise("Option :identifier or :identifiers is required")
|
64
66
|
self.globals = (opts[:globals] || {}).reduce({}) { |a, (global, patterns)|
|
65
67
|
a[global] = patterns.reduce({}) { |aa, (path, methods)|
|
66
68
|
aa[path] = methods == :any ? :any : Set.new(Array(methods).map { |m| m.to_s.upcase })
|
@@ -74,22 +76,26 @@ module MultiTenant
|
|
74
76
|
# Rack request call
|
75
77
|
def call(env)
|
76
78
|
tenant_class.current = nil
|
77
|
-
impl = tenant_class.multi_tenant_impl
|
78
79
|
|
79
80
|
request = Rack::Request.new env
|
80
|
-
|
81
|
+
id_resp = identifier.(request)
|
82
|
+
records_or_identifiers = Array(id_resp)
|
81
83
|
|
82
|
-
if (
|
83
|
-
allowed =
|
84
|
+
if (matching = matching_globals(records_or_identifiers)).any?
|
85
|
+
allowed = matching.any? { |allowed_paths|
|
84
86
|
path_matches?(request, allowed_paths)
|
85
87
|
}
|
86
|
-
return
|
88
|
+
return @app.call(env) if allowed
|
87
89
|
|
88
|
-
|
90
|
+
ids = identifiers records_or_identifiers
|
91
|
+
return not_found.(id_resp.is_a?(Array) ? ids : ids[0])
|
92
|
+
|
93
|
+
elsif (tenant_query.current_tenants = records_or_identifiers) and tenant_class.current?
|
89
94
|
return @app.call env
|
90
95
|
|
91
96
|
else
|
92
|
-
|
97
|
+
ids = identifiers records_or_identifiers
|
98
|
+
return not_found.(id_resp.is_a?(Array) ? ids : ids[0])
|
93
99
|
end
|
94
100
|
ensure
|
95
101
|
tenant_class.current = nil
|
@@ -101,11 +107,35 @@ module MultiTenant
|
|
101
107
|
}
|
102
108
|
end
|
103
109
|
|
104
|
-
|
105
|
-
|
106
|
-
|
110
|
+
def matching_globals(records_or_identifiers)
|
111
|
+
identifiers(records_or_identifiers).reduce([]) { |a, id|
|
112
|
+
a << globals[id] if globals.has_key? id
|
113
|
+
a
|
114
|
+
}
|
115
|
+
end
|
116
|
+
|
117
|
+
def identifiers(records_or_identifiers)
|
118
|
+
records_or_identifiers.map { |x|
|
119
|
+
x.is_a?(tenant_class) ? x.send(tenant_class.tenant_identifier) : x.to_s
|
120
|
+
}
|
121
|
+
end
|
122
|
+
|
123
|
+
def tenant_class(m = self.model)
|
124
|
+
@tenant_class ||= if m.respond_to?(:call)
|
125
|
+
tenant_class m.call
|
126
|
+
elsif m.respond_to? :constantize
|
127
|
+
m.constantize
|
128
|
+
elsif m.respond_to? :model
|
129
|
+
m.model
|
130
|
+
else
|
131
|
+
m
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
def tenant_query
|
136
|
+
@tenant_query ||= if self.model.respond_to?(:call)
|
107
137
|
self.model.call
|
108
|
-
elsif self.model.respond_to?
|
138
|
+
elsif self.model.respond_to? :constantize
|
109
139
|
self.model.constantize
|
110
140
|
else
|
111
141
|
self.model
|
@@ -47,19 +47,24 @@ module MultiTenant
|
|
47
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
48
|
#
|
49
49
|
def proxies_to_tenant(association_name, scope = nil)
|
50
|
-
|
51
|
-
raise "`proxies_to_tenant :#{association_name}`: unable to find association `:#{association_name}`. Make sure you create the association *first*." if
|
52
|
-
raise "`proxies_to_tenant :#{association_name}`: #{
|
53
|
-
raise "`proxies_to_tenant :#{association_name}`: the `:#{association_name}` association must use the `:inverse_of` option." if
|
50
|
+
ref = 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 ref.nil?
|
52
|
+
raise "`proxies_to_tenant :#{association_name}`: #{ref.klass.name} must use `acts_as_tenant`" if !ref.klass.acts_as_tenant?
|
53
|
+
raise "`proxies_to_tenant :#{association_name}`: the `:#{association_name}` association must use the `:inverse_of` option." if ref.inverse_of.nil?
|
54
54
|
|
55
55
|
cattr_accessor :proxied_tenant_class, :proxied_tenant_inverse_assoc, :proxied_tenant_inverse_scope
|
56
|
-
self.proxied_tenant_class =
|
57
|
-
self.proxied_tenant_inverse_assoc =
|
56
|
+
self.proxied_tenant_class = ref.klass
|
57
|
+
self.proxied_tenant_inverse_assoc = ref.inverse_of.name
|
58
58
|
self.proxied_tenant_inverse_scope = scope
|
59
59
|
|
60
|
-
|
61
|
-
|
62
|
-
|
60
|
+
extend MultiTenant::ActsAsTenant::TenantGetters
|
61
|
+
extend case [ref.macro, ref.inverse_of.macro]
|
62
|
+
when [:has_many, :belongs_to], [:has_one, :belongs_to], [:belongs_to, :has_one]
|
63
|
+
ProxiesToTenantSingularInverseAssociation
|
64
|
+
else
|
65
|
+
raise MultiTenant::NotImplemented, "`proxies_to_tenant` does not currently support `#{ref.macro}` associations with `#{ref.inverse_of.macro} inverses."
|
66
|
+
ProxiesToTenantPluralInverseAssociation
|
67
|
+
end
|
63
68
|
end
|
64
69
|
|
65
70
|
#
|
@@ -72,11 +77,28 @@ module MultiTenant
|
|
72
77
|
end
|
73
78
|
|
74
79
|
#
|
75
|
-
# Class methods
|
80
|
+
# Class methods for tenant proxies that have a singular inverse association (i.e. belongs_to or has_one).
|
76
81
|
#
|
77
|
-
module
|
78
|
-
|
79
|
-
|
82
|
+
module ProxiesToTenantSingularInverseAssociation
|
83
|
+
# Returns the current record of the proxy model
|
84
|
+
def current_tenants
|
85
|
+
proxied_tenant_class
|
86
|
+
.current_tenants
|
87
|
+
.map(&proxied_tenant_inverse_assoc)
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
#
|
92
|
+
# Class methods for tenant proxies that have a plural inverse association (i.e. has_many).
|
93
|
+
# NOTE These are just some thoughts on *maybe* how to support this if we ever need it.
|
94
|
+
#
|
95
|
+
module ProxiesToTenantPluralInverseAssociation
|
96
|
+
# Returns the current record of the proxy model
|
97
|
+
def current_tenant
|
98
|
+
raise MultiTenant::NotImplemented, "needs confirmed"
|
99
|
+
if (tenant = proxied_tenant_class.current_tenant)
|
100
|
+
tenant.send(proxied_tenant_inverse_assoc).instance_eval(&proxied_tenant_inverse_scope).first
|
101
|
+
end
|
80
102
|
end
|
81
103
|
end
|
82
104
|
end
|
data/lib/multi_tenant/version.rb
CHANGED
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: acts_as_multi_tenant
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version:
|
4
|
+
version: 2.0.0.pre.rc1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Jordan Hollinger
|
@@ -10,7 +10,7 @@ authors:
|
|
10
10
|
autorequire:
|
11
11
|
bindir: bin
|
12
12
|
cert_chain: []
|
13
|
-
date: 2018-11-
|
13
|
+
date: 2018-11-27 00:00:00.000000000 Z
|
14
14
|
dependencies:
|
15
15
|
- !ruby/object:Gem::Dependency
|
16
16
|
name: activerecord
|
@@ -58,8 +58,6 @@ files:
|
|
58
58
|
- lib/multi_tenant/acts_as_tenant.rb
|
59
59
|
- lib/multi_tenant/belongs_to_tenant.rb
|
60
60
|
- lib/multi_tenant/belongs_to_tenant_through.rb
|
61
|
-
- lib/multi_tenant/impl/multiple_current.rb
|
62
|
-
- lib/multi_tenant/impl/single_current.rb
|
63
61
|
- lib/multi_tenant/middleware.rb
|
64
62
|
- lib/multi_tenant/proxies_to_tenant.rb
|
65
63
|
- lib/multi_tenant/version.rb
|
@@ -78,9 +76,9 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
78
76
|
version: 2.1.0
|
79
77
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
80
78
|
requirements:
|
81
|
-
- - "
|
79
|
+
- - ">"
|
82
80
|
- !ruby/object:Gem::Version
|
83
|
-
version:
|
81
|
+
version: 1.3.1
|
84
82
|
requirements: []
|
85
83
|
rubyforge_project:
|
86
84
|
rubygems_version: 2.7.6
|
@@ -1,104 +0,0 @@
|
|
1
|
-
module MultiTenant
|
2
|
-
module Impl
|
3
|
-
#
|
4
|
-
# An implementation where Tenant.current is an array of tenants. All queries will be scoped to ANY of these
|
5
|
-
# tenants.
|
6
|
-
#
|
7
|
-
class MultipleCurrent
|
8
|
-
attr_reader :tenant_class
|
9
|
-
|
10
|
-
def initialize(tenant_class)
|
11
|
-
@tenant_class = tenant_class
|
12
|
-
end
|
13
|
-
|
14
|
-
def acts_as_tenant_class_methods
|
15
|
-
ActsAsTenantClassMethods
|
16
|
-
end
|
17
|
-
|
18
|
-
def belongs_to_tenant_instance_methods
|
19
|
-
BelongsToTenantInstanceMethods
|
20
|
-
end
|
21
|
-
|
22
|
-
def belongs_to_tenant_default_scope(model)
|
23
|
-
-> {
|
24
|
-
current = tenant_class.current.map(&model.tenant_primary_key)
|
25
|
-
current.any? ? model.where({model.tenant_foreign_key => current}) : model.where('1=1')
|
26
|
-
}
|
27
|
-
end
|
28
|
-
|
29
|
-
def belongs_to_tenant_through_default_scope(model, ref)
|
30
|
-
-> {
|
31
|
-
tenants = model.delegate_class.tenant_class.current
|
32
|
-
next model.where('1=1') if tenants.empty?
|
33
|
-
|
34
|
-
# Using straight sql so we can JOIN against two columns. Otherwise one must go into "WHERE", and Arel would apply it to UPDATEs and DELETEs.
|
35
|
-
quoted_tenant_ids = tenants.map { |t| model.connection.quote t.send model.delegate_class.tenant_primary_key }
|
36
|
-
model.joins("INNER JOIN #{ref.klass.table_name} ON #{ref.klass.table_name}.#{ref.foreign_key}=#{model.table_name}.#{ref.association_primary_key} AND #{ref.klass.table_name}.#{ref.klass.tenant_foreign_key} IN (#{quoted_tenant_ids.join(',')})").
|
37
|
-
distinct.
|
38
|
-
readonly(false) # using "joins" makes records readonly, which we don't want
|
39
|
-
}
|
40
|
-
end
|
41
|
-
|
42
|
-
def proxies_to_tenant_class_methods(_ref)
|
43
|
-
raise MultiTenant::Impl::NotImplemented, "`proxies_to_tenant` is not currently supported for impl `:multiple`."
|
44
|
-
end
|
45
|
-
|
46
|
-
def matching_globals(records_or_identifiers, globals)
|
47
|
-
records_or_identifiers.reduce([]) { |a, rec_or_id|
|
48
|
-
id = rec_or_id.is_a?(tenant_class) ? rec_or_id.send(tenant_class.tenant_identifier) : rec_or_id
|
49
|
-
a << globals[id] if globals.has_key? id
|
50
|
-
a
|
51
|
-
}
|
52
|
-
end
|
53
|
-
|
54
|
-
#
|
55
|
-
# Class methods given to the tenant model.
|
56
|
-
#
|
57
|
-
module ActsAsTenantClassMethods
|
58
|
-
def self.extended(model)
|
59
|
-
model.current = []
|
60
|
-
end
|
61
|
-
|
62
|
-
def current?
|
63
|
-
!current.nil? && current.any?
|
64
|
-
end
|
65
|
-
|
66
|
-
def resolve_tenant(records_or_identifiers)
|
67
|
-
if records_or_identifiers.nil?
|
68
|
-
[]
|
69
|
-
elsif records_or_identifiers.any? { |x| x.is_a? self }
|
70
|
-
records_or_identifiers
|
71
|
-
elsif records_or_identifiers.any?
|
72
|
-
where({tenant_identifier => records_or_identifiers}).to_a
|
73
|
-
else
|
74
|
-
[]
|
75
|
-
end
|
76
|
-
end
|
77
|
-
end
|
78
|
-
|
79
|
-
#
|
80
|
-
# Instance methods given to tenant-owned models.
|
81
|
-
#
|
82
|
-
module BelongsToTenantInstanceMethods
|
83
|
-
def self.included(model)
|
84
|
-
model.class_eval do
|
85
|
-
validate :ensure_assigned_to_current_tenants
|
86
|
-
end
|
87
|
-
end
|
88
|
-
|
89
|
-
private
|
90
|
-
|
91
|
-
#
|
92
|
-
# If the tenant_id is set, make sure it's one of the current ones.
|
93
|
-
#
|
94
|
-
def ensure_assigned_to_current_tenants
|
95
|
-
_tenants = self.class.tenant_class.current.map(&:id)
|
96
|
-
_tenant_id = send self.class.tenant_foreign_key
|
97
|
-
if _tenants.any? and _tenant_id.present? and !_tenants.include?(_tenant_id.to_s)
|
98
|
-
errors.add(self.class.tenant_foreign_key, "is incorrect")
|
99
|
-
end
|
100
|
-
end
|
101
|
-
end
|
102
|
-
end
|
103
|
-
end
|
104
|
-
end
|
@@ -1,124 +0,0 @@
|
|
1
|
-
module MultiTenant
|
2
|
-
module Impl
|
3
|
-
#
|
4
|
-
# An implementation where Tenant.current is set to the current tenant, or nil.
|
5
|
-
#
|
6
|
-
class SingleCurrent
|
7
|
-
attr_reader :tenant_class
|
8
|
-
|
9
|
-
def initialize(tenant_class)
|
10
|
-
@tenant_class = tenant_class
|
11
|
-
end
|
12
|
-
|
13
|
-
def acts_as_tenant_class_methods
|
14
|
-
ActsAsTenantClassMethods
|
15
|
-
end
|
16
|
-
|
17
|
-
def belongs_to_tenant_instance_methods
|
18
|
-
BelongsToTenantInstanceMethods
|
19
|
-
end
|
20
|
-
|
21
|
-
def belongs_to_tenant_default_scope(model)
|
22
|
-
-> {
|
23
|
-
current = model.tenant_class.current
|
24
|
-
current ? model.where({model.tenant_foreign_key => current.send(model.tenant_primary_key)}) : model.where('1=1')
|
25
|
-
}
|
26
|
-
end
|
27
|
-
|
28
|
-
def belongs_to_tenant_through_default_scope(model, ref)
|
29
|
-
-> {
|
30
|
-
tenant = model.delegate_class.tenant_class.current
|
31
|
-
next model.where('1=1') if tenant.nil?
|
32
|
-
|
33
|
-
# Using straight sql so we can JOIN against two columns. Otherwise one must go into "WHERE", and Arel would apply it to UPDATEs and DELETEs.
|
34
|
-
quoted_tenant_id = model.connection.quote tenant.send model.delegate_class.tenant_primary_key
|
35
|
-
model.joins("INNER JOIN #{ref.klass.table_name} ON #{ref.klass.table_name}.#{ref.foreign_key}=#{model.table_name}.#{ref.association_primary_key} AND #{ref.klass.table_name}.#{ref.klass.tenant_foreign_key}=#{quoted_tenant_id}").
|
36
|
-
readonly(false) # using "joins" makes records readonly, which we don't want
|
37
|
-
}
|
38
|
-
end
|
39
|
-
|
40
|
-
def proxies_to_tenant_class_methods(ref)
|
41
|
-
case [ref.macro, ref.inverse_of.macro]
|
42
|
-
when [:has_many, :belongs_to], [:has_one, :belongs_to], [:belongs_to, :has_one]
|
43
|
-
ProxiesToTenantSingularInverseAssociation
|
44
|
-
else
|
45
|
-
raise MultiTenant::Impl::NotImplemented, "`proxies_to_tenant` does not currently support `#{ref.macro}` associations with `#{ref.inverse_of.macro} inverses."
|
46
|
-
ProxiesToTenantPluralInverseAssociation
|
47
|
-
end
|
48
|
-
end
|
49
|
-
|
50
|
-
def matching_globals(record_or_identifier, globals)
|
51
|
-
id = record_or_identifier.is_a?(tenant_class) ? record_or_identifier.send(tenant_class.tenant_identifier) : record_or_identifier
|
52
|
-
globals.has_key?(id) ? [globals[id]] : []
|
53
|
-
end
|
54
|
-
|
55
|
-
#
|
56
|
-
# Class methods given to the tenant model.
|
57
|
-
#
|
58
|
-
module ActsAsTenantClassMethods
|
59
|
-
def current?
|
60
|
-
!current.nil?
|
61
|
-
end
|
62
|
-
|
63
|
-
def resolve_tenant(record_or_identifier)
|
64
|
-
if record_or_identifier.is_a? self
|
65
|
-
record_or_identifier
|
66
|
-
elsif record_or_identifier
|
67
|
-
where({tenant_identifier => record_or_identifier}).first
|
68
|
-
else
|
69
|
-
nil
|
70
|
-
end
|
71
|
-
end
|
72
|
-
end
|
73
|
-
|
74
|
-
#
|
75
|
-
# Instance methods given to tenant-owned models.
|
76
|
-
#
|
77
|
-
module BelongsToTenantInstanceMethods
|
78
|
-
def self.included(model)
|
79
|
-
model.class_eval do
|
80
|
-
before_validation :assign_to_tenant
|
81
|
-
end
|
82
|
-
end
|
83
|
-
|
84
|
-
private
|
85
|
-
|
86
|
-
#
|
87
|
-
# Assign this model to the current tenant (if any)
|
88
|
-
#
|
89
|
-
def assign_to_tenant
|
90
|
-
if self.class.tenant_class.current
|
91
|
-
current_tenant_id = self.class.tenant_class.current.send(self.class.tenant_primary_key)
|
92
|
-
send "#{self.class.tenant_foreign_key}=", current_tenant_id
|
93
|
-
end
|
94
|
-
end
|
95
|
-
end
|
96
|
-
|
97
|
-
#
|
98
|
-
# Class methods for tenant proxies that have a singular inverse association (i.e. belongs_to or has_one).
|
99
|
-
#
|
100
|
-
module ProxiesToTenantSingularInverseAssociation
|
101
|
-
# Returns the current record of the proxy model
|
102
|
-
def current
|
103
|
-
if (tenant = proxied_tenant_class.current)
|
104
|
-
tenant.send proxied_tenant_inverse_assoc
|
105
|
-
end
|
106
|
-
end
|
107
|
-
end
|
108
|
-
|
109
|
-
#
|
110
|
-
# Class methods for tenant proxies that have a plural inverse association (i.e. has_many).
|
111
|
-
# NOTE These are just some thoughts on *maybe* how to support this if we ever need it.
|
112
|
-
#
|
113
|
-
module ProxiesToTenantPluralInverseAssociation
|
114
|
-
# Returns the current record of the proxy model
|
115
|
-
def current
|
116
|
-
raise MultiTenant::Impl::NotImplemented, "needs confirmed"
|
117
|
-
if (tenant = proxied_tenant_class.current)
|
118
|
-
tenant.send(proxied_tenant_inverse_assoc).instance_eval(&proxied_tenant_inverse_scope).first
|
119
|
-
end
|
120
|
-
end
|
121
|
-
end
|
122
|
-
end
|
123
|
-
end
|
124
|
-
end
|