acts_as_multi_tenant 1.2.1 → 2.0.0.pre.rc1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
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