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 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
@@ -0,0 +1,4 @@
1
+ module MultiTenant
2
+ # Gem version
3
+ VERSION = '0.5.0'.freeze
4
+ end
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: []