activerecord-multi-tenant-next 2.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.
@@ -0,0 +1,431 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'active_record'
4
+ require_relative 'arel_visitors_depth_first' unless Arel::Visitors.const_defined?(:DepthFirst)
5
+
6
+ # Iterates AST and adds tenant enforcement clauses to all relations
7
+ module MultiTenant
8
+ class Table
9
+ attr_reader :arel_table
10
+
11
+ def initialize(arel_table)
12
+ @arel_table = arel_table
13
+ end
14
+
15
+ def eql?(other)
16
+ self.class == other.class &&
17
+ equality_fields.eql?(other.equality_fields)
18
+ end
19
+
20
+ def hash
21
+ equality_fields.hash
22
+ end
23
+
24
+ protected
25
+
26
+ def equality_fields
27
+ [arel_table.name, arel_table.table_alias]
28
+ end
29
+ end
30
+
31
+ class Context
32
+ attr_reader :arel_node, :known_relations, :handled_relations
33
+
34
+ def initialize(arel_node)
35
+ @arel_node = arel_node
36
+ @known_relations = []
37
+ @handled_relations = []
38
+ @discovering = false
39
+ end
40
+
41
+ def discover_relations
42
+ old_discovering = @discovering
43
+ @discovering = true
44
+ yield
45
+ @discovering = old_discovering
46
+ end
47
+
48
+ def visited_relation(relation)
49
+ return unless @discovering
50
+
51
+ @known_relations << Table.new(relation)
52
+ end
53
+
54
+ def visited_handled_relation(relation)
55
+ @handled_relations << Table.new(relation)
56
+ end
57
+
58
+ def unhandled_relations
59
+ known_relations.uniq - handled_relations
60
+ end
61
+ end
62
+
63
+ class ArelTenantVisitor < if Arel::Visitors.const_defined?(:DepthFirst)
64
+ Arel::Visitors::DepthFirst
65
+ else
66
+ ::MultiTenant::ArelVisitorsDepthFirst
67
+ end
68
+ def initialize(arel)
69
+ super(proc {})
70
+ @statement_node_id = nil
71
+
72
+ @contexts = []
73
+ @current_context = nil
74
+ accept(arel.ast)
75
+ end
76
+
77
+ attr_reader :contexts
78
+
79
+ # rubocop:disable Naming/MethodName
80
+ def visit_Arel_Attributes_Attribute(*args)
81
+ return if @current_context.nil?
82
+
83
+ super
84
+ end
85
+
86
+ def visit_Arel_Nodes_Equality(obj, *args)
87
+ if obj.left.is_a?(Arel::Attributes::Attribute)
88
+ table_name = MultiTenant::TableNode.table_name(obj.left.relation)
89
+ model = MultiTenant.multi_tenant_model_for_table(table_name)
90
+ if model.present? && obj.left.name.to_s == model.partition_key.to_s
91
+ @current_context.visited_handled_relation(obj.left.relation)
92
+ end
93
+ end
94
+ super
95
+ end
96
+
97
+ def visit_MultiTenant_TenantEnforcementClause(obj, *)
98
+ @current_context.visited_handled_relation(obj.tenant_attribute.relation)
99
+ end
100
+
101
+ def visit_MultiTenant_TenantJoinEnforcementClause(obj, *)
102
+ @current_context.visited_handled_relation(obj.tenant_attribute.relation)
103
+ end
104
+
105
+ def visit_Arel_Table(obj, _collector = nil)
106
+ @current_context.visited_relation(obj) if tenant_relation?(MultiTenant::TableNode.table_name(obj))
107
+ end
108
+
109
+ alias visit_Arel_Nodes_TableAlias visit_Arel_Table
110
+
111
+ # A SelectManager can appear inside a condition, e.g. as the right-hand
112
+ # side of an IN. Neither Arel's DepthFirst (Rails 6.0) nor the gem's copy
113
+ # of it (Rails 6.1+) knows how to visit one, so descend into its AST here.
114
+ def visit_Arel_SelectManager(obj, *)
115
+ visit obj.ast
116
+ end
117
+
118
+ def visit_Arel_Nodes_SelectCore(obj, *_args)
119
+ nest_context(obj) do
120
+ @current_context.discover_relations do
121
+ visit obj.source
122
+ end
123
+ visit obj.wheres
124
+ visit obj.groups
125
+ visit obj.windows
126
+ if defined?(obj.having)
127
+ visit obj.having
128
+ else
129
+ visit obj.havings
130
+ end
131
+ end
132
+ end
133
+
134
+ # rubocop:enable Naming/MethodName
135
+
136
+ # rubocop:disable Naming/MethodName
137
+ def visit_Arel_Nodes_OuterJoin(obj, _collector = nil)
138
+ nest_context(obj) do
139
+ @current_context.discover_relations do
140
+ visit obj.left
141
+ visit obj.right
142
+ end
143
+ end
144
+ end
145
+
146
+ # rubocop:enable Naming/MethodName
147
+
148
+ alias visit_Arel_Nodes_FullOuterJoin visit_Arel_Nodes_OuterJoin
149
+ alias visit_Arel_Nodes_RightOuterJoin visit_Arel_Nodes_OuterJoin
150
+
151
+ alias visit_ActiveModel_Attribute terminal
152
+
153
+ private
154
+
155
+ def tenant_relation?(table_name)
156
+ MultiTenant.multi_tenant_model_for_table(table_name).present?
157
+ end
158
+
159
+ DISPATCH = Hash.new do |hash, klass|
160
+ hash[klass] = "visit_#{(klass.name || '').gsub('::', '_')}"
161
+ end
162
+
163
+ def dispatch
164
+ DISPATCH
165
+ end
166
+
167
+ # rubocop:disable Naming/AccessorMethodName
168
+ def get_dispatch_cache
169
+ dispatch
170
+ end
171
+
172
+ # rubocop:enable Naming/AccessorMethodName
173
+
174
+ def nest_context(obj)
175
+ old_context = @current_context
176
+ @current_context = Context.new(obj)
177
+ @contexts << @current_context
178
+
179
+ yield
180
+
181
+ @current_context = old_context
182
+ end
183
+ end
184
+
185
+ class BaseTenantEnforcementClause < Arel::Nodes::Node
186
+ attr_reader :tenant_attribute
187
+
188
+ def initialize(tenant_attribute)
189
+ super()
190
+ @tenant_attribute = tenant_attribute
191
+ @tenant_model = MultiTenant.multi_tenant_model_for_table(
192
+ MultiTenant::TableNode.table_name(tenant_attribute.relation)
193
+ )
194
+ end
195
+
196
+ def to_s
197
+ to_sql
198
+ end
199
+
200
+ def to_str
201
+ to_sql
202
+ end
203
+
204
+ def to_sql(*)
205
+ collector = Arel::Collectors::SQLString.new
206
+ collector = @tenant_model.connection.visitor.accept tenant_arel, collector
207
+ collector.value
208
+ end
209
+ end
210
+
211
+ class TenantEnforcementClause < BaseTenantEnforcementClause
212
+ private
213
+
214
+ def tenant_arel
215
+ if defined?(Arel::Nodes::Quoted)
216
+ @tenant_attribute.eq(Arel::Nodes::Quoted.new(MultiTenant.current_tenant_id))
217
+ else
218
+ @tenant_attribute.eq(MultiTenant.current_tenant_id)
219
+ end
220
+ end
221
+ end
222
+
223
+ class TenantJoinEnforcementClause < BaseTenantEnforcementClause
224
+ attr_reader :table_left
225
+
226
+ def initialize(tenant_attribute, table_left)
227
+ super(tenant_attribute)
228
+ @table_left = table_left
229
+ @model_left = MultiTenant.multi_tenant_model_for_table(MultiTenant::TableNode.table_name(table_left))
230
+ end
231
+
232
+ private
233
+
234
+ def tenant_arel
235
+ @tenant_attribute.eq(@table_left[@model_left.partition_key])
236
+ end
237
+ end
238
+
239
+ module TenantValueVisitor
240
+ # rubocop:disable Naming/MethodName
241
+ def visit_MultiTenant_TenantEnforcementClause(obj, collector)
242
+ collector << obj
243
+ end
244
+
245
+ def visit_MultiTenant_TenantJoinEnforcementClause(obj, collector)
246
+ collector << obj
247
+ end
248
+
249
+ # rubocop:enable Naming/MethodName
250
+ end
251
+
252
+ module DatabaseStatements
253
+ def join_to_update(update, *args)
254
+ update = super
255
+ model = MultiTenant.multi_tenant_model_for_table(MultiTenant::TableNode.table_name(update.ast.relation))
256
+ if model.present? && !MultiTenant.with_write_only_mode_enabled? && MultiTenant.current_tenant_id.present?
257
+ update.where(MultiTenant::TenantEnforcementClause.new(model.arel_table[model.partition_key]))
258
+ end
259
+ update
260
+ end
261
+
262
+ def join_to_delete(delete, *args)
263
+ delete = super
264
+ model = MultiTenant.multi_tenant_model_for_table(MultiTenant::TableNode.table_name(delete.ast.left))
265
+ if model.present? && !MultiTenant.with_write_only_mode_enabled? && MultiTenant.current_tenant_id.present?
266
+ delete.where(MultiTenant::TenantEnforcementClause.new(model.arel_table[model.partition_key]))
267
+ end
268
+ delete
269
+ end
270
+
271
+ def update(arel, name = nil, binds = [])
272
+ model = MultiTenant.multi_tenant_model_for_arel(arel)
273
+ if model.present? && !MultiTenant.with_write_only_mode_enabled? && MultiTenant.current_tenant_id.present?
274
+ arel.where(MultiTenant::TenantEnforcementClause.new(model.arel_table[model.partition_key]))
275
+ end
276
+ super
277
+ end
278
+
279
+ def delete(arel, name = nil, binds = [])
280
+ model = MultiTenant.multi_tenant_model_for_arel(arel)
281
+ if model.present? && !MultiTenant.with_write_only_mode_enabled? && MultiTenant.current_tenant_id.present?
282
+ arel.where(MultiTenant::TenantEnforcementClause.new(model.arel_table[model.partition_key]))
283
+ end
284
+ super
285
+ end
286
+ end
287
+ end
288
+
289
+ require 'active_record/connection_adapters/abstract_adapter'
290
+ ActiveRecord::ConnectionAdapters::AbstractAdapter.prepend(MultiTenant::DatabaseStatements)
291
+
292
+ Arel::Visitors::ToSql.include(MultiTenant::TenantValueVisitor)
293
+
294
+ module MultiTenant
295
+ # Rails 8.1 compiles update_all on a joined relation for PostgreSQL as
296
+ #
297
+ # UPDATE t AS __active_record_update_alias SET ...
298
+ # FROM t INNER JOIN ... WHERE t.id = __active_record_update_alias.id
299
+ #
300
+ # Citus only plans that self-join when it is also on the distribution
301
+ # column, so for multi-tenant tables add the partition key to it. This also
302
+ # keeps the rows being updated inside the tenant. Older Rails versions use
303
+ # an IN (subquery) here and never build the alias, so this is a no-op there.
304
+ module UpdateJoinPartitionKey
305
+ private
306
+
307
+ def prepare_update_statement(statement)
308
+ stmt = super
309
+ return stmt unless stmt.relation.respond_to?(:left) && stmt.relation.left.is_a?(Arel::Nodes::TableAlias)
310
+
311
+ table = statement.relation.left
312
+ model = MultiTenant.multi_tenant_model_for_table(MultiTenant::TableNode.table_name(table))
313
+ return stmt if model.nil? || model.partition_key.blank? || model.partition_key.to_s == model.primary_key.to_s
314
+
315
+ stmt.wheres << stmt.relation.left[model.partition_key].eq(table[model.partition_key])
316
+ stmt
317
+ end
318
+ end
319
+ end
320
+
321
+ Arel::Visitors::PostgreSQL.prepend(MultiTenant::UpdateJoinPartitionKey)
322
+
323
+ module MultiTenant
324
+ module QueryMethodsExtensions
325
+ def build_arel(*)
326
+ arel = super
327
+
328
+ unless MultiTenant.with_write_only_mode_enabled?
329
+ visitor = MultiTenant::ArelTenantVisitor.new(arel)
330
+
331
+ visitor.contexts.each do |context|
332
+ node = context.arel_node
333
+
334
+ context.unhandled_relations.each do |relation|
335
+ model = MultiTenant.multi_tenant_model_for_table(MultiTenant::TableNode.table_name(relation.arel_table))
336
+
337
+ if MultiTenant.current_tenant_id
338
+ enforcement_clause = MultiTenant::TenantEnforcementClause.new(relation.arel_table[model.partition_key])
339
+ case node
340
+ when Arel::Nodes::Join # Arel::Nodes::OuterJoin, Arel::Nodes::RightOuterJoin, Arel::Nodes::FullOuterJoin
341
+ node.right.expr = node.right.expr.and(enforcement_clause)
342
+ when Arel::Nodes::SelectCore
343
+ if node.wheres.empty?
344
+ node.wheres = [enforcement_clause]
345
+ elsif node.wheres[0].is_a?(Arel::Nodes::And)
346
+ node.wheres[0].children << enforcement_clause
347
+ else
348
+ node.wheres[0] = enforcement_clause.and(node.wheres[0])
349
+ end
350
+ else
351
+ raise 'UnknownContext'
352
+ end
353
+ end
354
+
355
+ next unless node.is_a?(Arel::Nodes::SelectCore) || node.is_a?(Arel::Nodes::Join)
356
+
357
+ node_list = if node.is_a? Arel::Nodes::Join
358
+ [node]
359
+ else
360
+ node.source.right
361
+ end
362
+
363
+ node_list.select { |n| n.is_a? Arel::Nodes::Join }.each do |node_join|
364
+ next unless node_join.right
365
+
366
+ relation_right, relation_left = relations_from_node_join(node_join)
367
+
368
+ next unless relation_right && relation_left
369
+
370
+ model_right = MultiTenant.multi_tenant_model_for_table(MultiTenant::TableNode.table_name(relation_left))
371
+ model_left = MultiTenant.multi_tenant_model_for_table(MultiTenant::TableNode.table_name(relation_right))
372
+ next unless model_right && model_left
373
+
374
+ join_enforcement_clause = MultiTenant::TenantJoinEnforcementClause.new(
375
+ relation_right[model_right.partition_key], relation_left
376
+ )
377
+ node_join.right.expr = node_join.right.expr.and(join_enforcement_clause)
378
+ end
379
+ end
380
+ end
381
+ end
382
+
383
+ arel
384
+ end
385
+
386
+ private
387
+
388
+ def relations_from_node_join(node_join)
389
+ if node_join.right.expr.is_a?(Arel::Nodes::Equality)
390
+ return node_join.right.expr.right.relation, node_join.right.expr.left.relation
391
+ end
392
+
393
+ children = [node_join.right.expr.children].flatten
394
+
395
+ tenant_applied = children.any? do |c|
396
+ c.is_a?(MultiTenant::TenantEnforcementClause) || c.is_a?(MultiTenant::TenantJoinEnforcementClause)
397
+ end
398
+ return nil, nil if tenant_applied || children.empty?
399
+
400
+ child = children.first.respond_to?(:children) ? children.first.children.first : children.first
401
+ if child.right.respond_to?(:relation) && child.left.respond_to?(:relation)
402
+ return child.right.relation, child.left.relation
403
+ end
404
+
405
+ [nil, nil]
406
+ end
407
+ end
408
+ end
409
+
410
+ require 'active_record/relation'
411
+ ActiveRecord::QueryMethods.prepend(MultiTenant::QueryMethodsExtensions)
412
+
413
+ module MultiTenantFindBy
414
+ if ActiveRecord.gem_version >= Gem::Version.create('7.2.0')
415
+ def cached_find_by_statement(connection, key, &block)
416
+ return super unless respond_to?(:scoped_by_tenant?) && scoped_by_tenant?
417
+
418
+ super(connection, Array.wrap(key) + [MultiTenant.current_tenant_id.to_s], &block)
419
+ end
420
+ else
421
+ def cached_find_by_statement(key, &block)
422
+ return super unless respond_to?(:scoped_by_tenant?) && scoped_by_tenant?
423
+
424
+ super(Array.wrap(key) + [MultiTenant.current_tenant_id.to_s], &block)
425
+ end
426
+ end
427
+ end
428
+
429
+ ActiveSupport.on_load(:active_record) do |base|
430
+ base.singleton_class.prepend(MultiTenantFindBy)
431
+ end
@@ -0,0 +1,90 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Arel
4
+ module ActiveRecordRelationExtension
5
+ # Overrides the delete_all method to include tenant scoping
6
+ def delete_all
7
+ model = MultiTenant.multi_tenant_model_for_table(table_name)
8
+
9
+ # Call the original delete_all method if the current tenant is identified by an ID
10
+ return super if model.nil? || MultiTenant.current_tenant_is_id? || MultiTenant.current_tenant.nil?
11
+
12
+ stmt = Arel::DeleteManager.new.from(table)
13
+ stmt.wheres = [generate_in_condition_subquery]
14
+
15
+ # Execute the delete statement using the connection and return the result
16
+ klass.connection.delete(stmt, "#{klass} Delete All").tap { reset }
17
+ end
18
+
19
+ # Overrides the update_all method to include tenant scoping
20
+ def update_all(updates)
21
+ model = MultiTenant.multi_tenant_model_for_table(table_name)
22
+
23
+ # Call the original update_all method if the current tenant is identified by an ID
24
+ return super if model.nil? || MultiTenant.current_tenant_is_id? || MultiTenant.current_tenant.nil?
25
+
26
+ if updates.is_a?(Hash)
27
+ if klass.locking_enabled? &&
28
+ !updates.key?(klass.locking_column) &&
29
+ !updates.key?(klass.locking_column.to_sym)
30
+ attr = table[klass.locking_column]
31
+ updates[attr.name] = _increment_attribute(attr)
32
+ end
33
+ values = _substitute_values(updates)
34
+ else
35
+ values = Arel.sql(klass.sanitize_sql_for_assignment(updates, table.name))
36
+ end
37
+
38
+ stmt = Arel::UpdateManager.new
39
+ stmt.table(table)
40
+ stmt.set values
41
+ stmt.wheres = [generate_in_condition_subquery]
42
+
43
+ klass.connection.update(stmt, "#{klass} Update All").tap { reset }
44
+ end
45
+
46
+ private
47
+
48
+ # The generate_in_condition_subquery method generates a subquery that selects
49
+ # records associated with the current tenant.
50
+ def generate_in_condition_subquery
51
+ # Get the tenant key and tenant ID based on the current tenant
52
+ tenant_key = MultiTenant.partition_key(MultiTenant.current_tenant_class)
53
+ tenant_id = MultiTenant.current_tenant_id
54
+
55
+ # Build an Arel query. Only Active Record 7.2 and 8.0 take the
56
+ # connection as the first argument of build_arel; every other version
57
+ # takes only aliases, which is required (without a default) in 6.0 and 8.1.
58
+ arel = if eager_loading?
59
+ apply_join_dependency.arel
60
+ elsif ActiveRecord.gem_version >= Gem::Version.create('7.2.0') &&
61
+ ActiveRecord.gem_version < Gem::Version.create('8.1.0')
62
+ build_arel(klass.connection)
63
+ else
64
+ build_arel(nil)
65
+ end
66
+
67
+ arel.source.left = table
68
+
69
+ # If the tenant ID is present and the tenant key is a column in the model,
70
+ # add a condition to only include records where the tenant key equals the tenant ID
71
+ if tenant_id && klass.column_names.include?(tenant_key)
72
+ tenant_condition = table[tenant_key].eq(tenant_id)
73
+ unless arel.constraints.any? { |node| node.to_sql.include?(tenant_condition.to_sql) }
74
+ arel = arel.where(tenant_condition)
75
+ end
76
+ end
77
+
78
+ # Clone the query, clear its projections, and set its projection to the primary key of the table
79
+ subquery = arel.clone
80
+ subquery.projections.clear
81
+ subquery = subquery.project(table[primary_key])
82
+
83
+ # Create an IN condition node with the primary key of the table and the subquery
84
+ Arel::Nodes::In.new(table[primary_key], subquery.ast)
85
+ end
86
+ end
87
+ end
88
+
89
+ # Patch ActiveRecord::Relation with the extension module
90
+ ActiveRecord::Relation.prepend(Arel::ActiveRecordRelationExtension)
@@ -0,0 +1,91 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'sidekiq/client'
4
+
5
+ # Adds methods to handle tenant information both in the client and server.
6
+ module Sidekiq::Middleware::MultiTenant
7
+ # Get the current tenant and store in the message to be sent to Sidekiq.
8
+ class Client
9
+ def call(_worker_class, msg, _queue, _redis_pool)
10
+ if MultiTenant.current_tenant.present?
11
+ msg['multi_tenant'] ||=
12
+ {
13
+ 'class' => MultiTenant.current_tenant_class,
14
+ 'id' => MultiTenant.current_tenant_id
15
+ }
16
+ end
17
+
18
+ yield
19
+ end
20
+ end
21
+
22
+ # Pull the tenant out and run the current thread with it.
23
+ class Server
24
+ def call(_worker_class, msg, _queue, &block)
25
+ if msg.key?('multi_tenant')
26
+ tenant = begin
27
+ msg['multi_tenant']['class'].constantize.find(msg['multi_tenant']['id'])
28
+ rescue ActiveRecord::RecordNotFound
29
+ msg['multi_tenant']['id']
30
+ end
31
+ MultiTenant.with(tenant, &block)
32
+ else
33
+ yield
34
+ end
35
+ end
36
+ end
37
+ end
38
+
39
+ # Configure Sidekiq to use the multi-tenant client and server middleware to add (client/server)/process(server)
40
+ # tenant information.
41
+ Sidekiq.configure_server do |config|
42
+ config.server_middleware do |chain|
43
+ chain.add Sidekiq::Middleware::MultiTenant::Server
44
+ end
45
+ config.client_middleware do |chain|
46
+ chain.add Sidekiq::Middleware::MultiTenant::Client
47
+ end
48
+ end
49
+
50
+ Sidekiq.configure_client do |config|
51
+ config.client_middleware do |chain|
52
+ chain.add Sidekiq::Middleware::MultiTenant::Client
53
+ end
54
+ end
55
+
56
+ # Bulk push support for Sidekiq while setting multi-tenant information.
57
+ # This is a copy of the Sidekiq::Client#push_bulk method with the addition of
58
+ # setting the multi-tenant information for each job.
59
+ module Sidekiq
60
+ class Client
61
+ # Allows the caller to enqueue multiple Sidekiq jobs with
62
+ # tenant information in a single call. It ensures that each job is processed
63
+ # within the correct tenant context and returns an array of job IDs for the enqueued jobs
64
+ def push_bulk_with_tenants(items)
65
+ first_job = items['jobs'].first
66
+ return [] unless first_job # no jobs to push
67
+ unless first_job.is_a?(Hash)
68
+ raise ArgumentError, "Bulk arguments must be an Array of Hashes: [{ 'args' => [1], 'tenant_id' => 1 }, ...]"
69
+ end
70
+
71
+ normed = normalize_item(items.except('jobs').merge('args' => []))
72
+ payloads = items['jobs'].map do |job|
73
+ MultiTenant.with(job['tenant_id']) do
74
+ copy = normed.merge('args' => job['args'], 'jid' => SecureRandom.hex(12), 'enqueued_at' => Time.now.to_f)
75
+ result = process_single(items['class'], copy)
76
+ result || nil
77
+ end
78
+ end.compact
79
+
80
+ raw_push(payloads) unless payloads.empty?
81
+ payloads.collect { |payload| payload['jid'] }
82
+ end
83
+
84
+ # Enabling the push_bulk_with_tenants method to be called directly on the Sidekiq::Client class
85
+ class << self
86
+ def push_bulk_with_tenants(items)
87
+ new.push_bulk_with_tenants(items)
88
+ end
89
+ end
90
+ end
91
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MultiTenant
4
+ module TableNode
5
+ # Return table name
6
+ def self.table_name(node)
7
+ # NOTE: Arel::Nodes::Table#table_name is removed in Rails 7.1
8
+ if node.is_a?(Arel::Nodes::TableAlias)
9
+ node.table_name
10
+ else
11
+ node.name
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MultiTenant
4
+ VERSION = '2.5.0'
5
+ end
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Lets Bundler auto-require the gem under its published name.
4
+ require_relative 'activerecord-multi-tenant'
@@ -0,0 +1,3 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'activerecord_multi_tenant'
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'activerecord-multi-tenant/controller_extensions' if Object.const_defined?(:ActionController)
4
+ require_relative 'activerecord-multi-tenant/copy_from_client'
5
+ require_relative 'activerecord-multi-tenant/fast_truncate'
6
+ require_relative 'activerecord-multi-tenant/migrations'
7
+ require_relative 'activerecord-multi-tenant/model_extensions'
8
+ require_relative 'activerecord-multi-tenant/multi_tenant'
9
+ require_relative 'activerecord-multi-tenant/query_rewriter'
10
+ require_relative 'activerecord-multi-tenant/query_monitor'
11
+ require_relative 'activerecord-multi-tenant/table_node'
12
+ require_relative 'activerecord-multi-tenant/version'
13
+ require_relative 'activerecord-multi-tenant/habtm'
14
+ require_relative 'activerecord-multi-tenant/relation_extension'