acts_as_multi_tenant 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
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: []