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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 0b1731c4e30b01347126564bdbc96889ceef49b481a4897b402d3e5fe3ddf297
4
- data.tar.gz: 43698dcccb60a541c39a8261949602ee42708f5fab5cf6422a20117581e9f200
3
+ metadata.gz: f4138ddbff135b1b100c0ddaba90b7ac5ca35bdbce7f031790dc8bade47df719
4
+ data.tar.gz: d8ac485e9aadea630e26ec7dbc852f2e4593eff23bd9be069220dc0928429af9
5
5
  SHA512:
6
- metadata.gz: db1b136428f03f3b98c1d439add7d393e4aa171e88a057d21a76a63fd14b53307a954925419bad560131cea6127cbdaf45726f60548c7b67f0500b81f4489580
7
- data.tar.gz: 596769d5655d197e4c273fb789e4ed3fdaf26b3600ad045f5922b8f906dadd02a42f80b56c15dcc0fe3340880f1af7af6578a3aa815079e93c3969e2c1e1b799
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, 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.
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 account may have access to multiple clients. `acts_as_multi_tenant` supports this with the `current: :multiple` option. When this is set, `Client.current` will be an array of clients. Queries will be filtered to ANY of those clients.
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
- ```ruby
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
- identifier: ->(req) {
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
@@ -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
- module Impl
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, current: :single)
18
- cattr_accessor :tenant_identifier, :tenant_thread_var, :multi_tenant_impl
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.multi_tenant_impl = case current
22
- when :single then MultiTenant::Impl::SingleCurrent.new(self)
23
- when :multiple then MultiTenant::Impl::MultipleCurrent.new(self)
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 ClassMethods
86
+ module TenantSetters
56
87
  #
57
- # Return the current tenant record, if any. Thread-safe.
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
- # @return the current tenant record
91
+ # @param record_or_identifier the record or the identifier in the 'tenant_identifier' column.
60
92
  #
61
- def current
62
- Thread.current.thread_variable_get(tenant_thread_var) || resolve_tenant(nil)
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 record. You may either pass an ActiveRecord Client record, OR the value
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 record_or_identifier the record or the identifier in the 'tenant_identifier' column.
102
+ # @param records_or_identifiers array of the records or identifiers in the 'tenant_identifier' column.
70
103
  #
71
- def current=(record_or_identifier)
72
- obj = resolve_tenant record_or_identifier
73
- Thread.current.thread_variable_set tenant_thread_var, obj
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
- old_current = self.current
121
+ old_tenants = self.current_tenants
82
122
  all.each do |tenant|
83
- self.current = tenant
123
+ self.current_tenant = tenant
84
124
  yield if block_given?
85
125
  end
86
126
  ensure
87
- self.current = old_current
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
- old_current = self.current
96
- self.current = record_or_identifier
135
+ old_tenants = self.current_tenants
136
+ self.current_tenant = record_or_identifier
97
137
  yield if block_given?
98
138
  ensure
99
- self.current = old_current
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
- old_current = self.current
108
- self.current = resolve_tenant nil
159
+ old_tenants = self.current_tenants
160
+ self.current_tenant = nil
109
161
  yield if block_given?
110
162
  ensure
111
- self.current = old_current
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 tenant_class.multi_tenant_impl.belongs_to_tenant_instance_methods
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
- impl = self.delegate_class.tenant_class.multi_tenant_impl
26
- default_scope(&impl.belongs_to_tenant_through_default_scope(self, ref))
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
- tenant_identifier = identifier.(request)
81
+ id_resp = identifier.(request)
82
+ records_or_identifiers = Array(id_resp)
81
83
 
82
- if (matching_globals = impl.matching_globals(tenant_identifier, globals)).any?
83
- allowed = matching_globals.any? { |allowed_paths|
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 allowed ? @app.call(env) : not_found.(tenant_identifier)
88
+ return @app.call(env) if allowed
87
89
 
88
- elsif (tenant_class.current = tenant_identifier) and tenant_class.current?
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
- return not_found.(tenant_identifier)
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
- # Infers and returns the tenant model class this middleware is handling
105
- def tenant_class
106
- @tenant_class ||= if self.model.respond_to?(:call)
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?(:constantize)
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
- 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?
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 = reflection.klass
57
- self.proxied_tenant_inverse_assoc = reflection.inverse_of.name
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
- impl = self.proxied_tenant_class.multi_tenant_impl
61
- self.extend impl.proxies_to_tenant_class_methods(reflection)
62
- self.extend ClassMethods
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 given to proxies.
80
+ # Class methods for tenant proxies that have a singular inverse association (i.e. belongs_to or has_one).
76
81
  #
77
- module ClassMethods
78
- def multi_tenant_impl
79
- proxied_tenant_class.multi_tenant_impl
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
@@ -1,4 +1,4 @@
1
1
  module MultiTenant
2
2
  # Gem version
3
- VERSION = '1.2.1'.freeze
3
+ VERSION = '2.0.0.pre.rc1'.freeze
4
4
  end
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: 1.2.1
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-23 00:00:00.000000000 Z
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: '0'
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