activerecord-multi-tenant 1.2.0 → 2.4.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.
Files changed (124) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/active-record-multi-tenant-tests.yml +83 -0
  3. data/.gitignore +6 -0
  4. data/.readthedocs.yaml +15 -0
  5. data/.rspec +0 -0
  6. data/.rubocop.yml +51 -0
  7. data/Appraisals +6 -22
  8. data/CHANGELOG.md +51 -0
  9. data/Gemfile +3 -1
  10. data/LICENSE +0 -0
  11. data/README.md +3 -2
  12. data/Rakefile +1 -1
  13. data/activerecord-multi-tenant.gemspec +28 -23
  14. data/docker-compose.yml +24 -18
  15. data/docs/.gitignore +3 -0
  16. data/docs/Makefile +28 -0
  17. data/docs/api-reference.sh +10 -0
  18. data/docs/requirements.in +4 -0
  19. data/docs/requirements.txt +62 -0
  20. data/docs/source/_static/api-reference/ActiveRecord/Associations/Association.html +285 -0
  21. data/docs/source/_static/api-reference/ActiveRecord/Associations/ClassMethods.html +255 -0
  22. data/docs/source/_static/api-reference/ActiveRecord/Associations.html +117 -0
  23. data/docs/source/_static/api-reference/ActiveRecord/ConnectionAdapters/SchemaStatements.html +232 -0
  24. data/docs/source/_static/api-reference/ActiveRecord/ConnectionAdapters.html +126 -0
  25. data/docs/source/_static/api-reference/ActiveRecord/QueryMethods.html +336 -0
  26. data/docs/source/_static/api-reference/ActiveRecord/SchemaDumper.html +121 -0
  27. data/docs/source/_static/api-reference/ActiveRecord.html +130 -0
  28. data/docs/source/_static/api-reference/MultiTenant/ArelTenantVisitor.html +755 -0
  29. data/docs/source/_static/api-reference/MultiTenant/ArelVisitorsDepthFirst.html +208 -0
  30. data/docs/source/_static/api-reference/MultiTenant/BaseTenantEnforcementClause.html +462 -0
  31. data/docs/source/_static/api-reference/MultiTenant/Context.html +659 -0
  32. data/docs/source/_static/api-reference/MultiTenant/ControllerExtensions.html +202 -0
  33. data/docs/source/_static/api-reference/MultiTenant/CopyFromClient.html +186 -0
  34. data/docs/source/_static/api-reference/MultiTenant/CopyFromClientHelper.html +362 -0
  35. data/docs/source/_static/api-reference/MultiTenant/Current.html +124 -0
  36. data/docs/source/_static/api-reference/MultiTenant/DatabaseStatements.html +366 -0
  37. data/docs/source/_static/api-reference/MultiTenant/FastTruncate.html +226 -0
  38. data/docs/source/_static/api-reference/MultiTenant/MigrationExtensions.html +554 -0
  39. data/docs/source/_static/api-reference/MultiTenant/MissingTenantError.html +124 -0
  40. data/docs/source/_static/api-reference/MultiTenant/ModelExtensionsClassMethods.html +492 -0
  41. data/docs/source/_static/api-reference/MultiTenant/QueryMonitor.html +257 -0
  42. data/docs/source/_static/api-reference/MultiTenant/Table.html +419 -0
  43. data/docs/source/_static/api-reference/MultiTenant/TenantEnforcementClause.html +148 -0
  44. data/docs/source/_static/api-reference/MultiTenant/TenantIsImmutable.html +135 -0
  45. data/docs/source/_static/api-reference/MultiTenant/TenantJoinEnforcementClause.html +310 -0
  46. data/docs/source/_static/api-reference/MultiTenant/TenantValueVisitor.html +239 -0
  47. data/docs/source/_static/api-reference/MultiTenant.html +1454 -0
  48. data/docs/source/_static/api-reference/MultiTenantFindBy.html +180 -0
  49. data/docs/source/_static/api-reference/Sidekiq/Client.html +302 -0
  50. data/docs/source/_static/api-reference/Sidekiq/Middleware/MultiTenant/Client.html +217 -0
  51. data/docs/source/_static/api-reference/Sidekiq/Middleware/MultiTenant/Server.html +219 -0
  52. data/docs/source/_static/api-reference/Sidekiq/Middleware/MultiTenant.html +126 -0
  53. data/docs/source/_static/api-reference/Sidekiq.html +126 -0
  54. data/docs/source/_static/api-reference/_index.html +399 -0
  55. data/docs/source/_static/api-reference/class_list.html +51 -0
  56. data/docs/source/_static/api-reference/css/common.css +1 -0
  57. data/docs/source/_static/api-reference/css/full_list.css +58 -0
  58. data/docs/source/_static/api-reference/css/style.css +497 -0
  59. data/docs/source/_static/api-reference/file.README.html +167 -0
  60. data/docs/source/_static/api-reference/file_list.html +56 -0
  61. data/docs/source/_static/api-reference/frames.html +17 -0
  62. data/docs/source/_static/api-reference/index.html +167 -0
  63. data/docs/source/_static/api-reference/js/app.js +314 -0
  64. data/docs/source/_static/api-reference/js/full_list.js +216 -0
  65. data/docs/source/_static/api-reference/js/jquery.js +4 -0
  66. data/docs/source/_static/api-reference/method_list.html +715 -0
  67. data/docs/source/_static/api-reference/top-level-namespace.html +126 -0
  68. data/docs/source/_templates/.gitignore +4 -0
  69. data/docs/source/api-reference.rst +8 -0
  70. data/docs/source/appendix.rst +26 -0
  71. data/docs/source/changelog.rst +8 -0
  72. data/docs/source/community-and-support.rst +26 -0
  73. data/docs/source/conf.py +30 -0
  74. data/docs/source/contributing.rst +70 -0
  75. data/docs/source/getting-started.rst +37 -0
  76. data/docs/source/guides-and-tutorials.rst +129 -0
  77. data/docs/source/index.rst +54 -0
  78. data/docs/source/introduction.rst +33 -0
  79. data/docs/source/license.rst +22 -0
  80. data/docs/source/troubleshooting.rst +41 -0
  81. data/docs/source/usage-guide.rst +59 -0
  82. data/lib/activerecord-multi-tenant/arel_visitors_depth_first.rb +183 -174
  83. data/lib/activerecord-multi-tenant/controller_extensions.rb +15 -4
  84. data/lib/activerecord-multi-tenant/copy_from_client.rb +4 -0
  85. data/lib/activerecord-multi-tenant/fast_truncate.rb +4 -2
  86. data/lib/activerecord-multi-tenant/habtm.rb +50 -0
  87. data/lib/activerecord-multi-tenant/migrations.rb +87 -10
  88. data/lib/activerecord-multi-tenant/model_extensions.rb +98 -34
  89. data/lib/activerecord-multi-tenant/multi_tenant.rb +102 -29
  90. data/lib/activerecord-multi-tenant/query_monitor.rb +21 -5
  91. data/lib/activerecord-multi-tenant/query_rewriter.rb +122 -91
  92. data/lib/activerecord-multi-tenant/sidekiq.rb +46 -19
  93. data/lib/activerecord-multi-tenant/table_node.rb +13 -0
  94. data/lib/activerecord-multi-tenant/version.rb +1 -1
  95. data/lib/activerecord-multi-tenant.rb +3 -13
  96. data/lib/activerecord_multi_tenant.rb +13 -0
  97. data/spec/activerecord-multi-tenant/associations_spec.rb +42 -0
  98. data/spec/activerecord-multi-tenant/controller_extensions_spec.rb +3 -2
  99. data/spec/activerecord-multi-tenant/fast_truncate_spec.rb +8 -6
  100. data/spec/activerecord-multi-tenant/model_extensions_spec.rb +347 -143
  101. data/spec/activerecord-multi-tenant/multi_tenant_spec.rb +69 -13
  102. data/spec/activerecord-multi-tenant/query_rewriter_spec.rb +60 -59
  103. data/spec/activerecord-multi-tenant/record_callback_spec.rb +0 -0
  104. data/spec/activerecord-multi-tenant/record_finding_spec.rb +11 -11
  105. data/spec/activerecord-multi-tenant/record_modifications_spec.rb +23 -4
  106. data/spec/activerecord-multi-tenant/sidekiq_spec.rb +10 -10
  107. data/spec/database.yml +0 -0
  108. data/spec/schema.rb +43 -2
  109. data/spec/spec_helper.rb +52 -16
  110. data/spec/support/format_sql.rb +20 -0
  111. metadata +126 -36
  112. data/.github/workflows/CI.yml +0 -63
  113. data/gemfiles/.bundle/config +0 -2
  114. data/gemfiles/active_record_5.2.gemfile +0 -16
  115. data/gemfiles/active_record_6.0.gemfile +0 -8
  116. data/gemfiles/active_record_6.1.gemfile +0 -8
  117. data/gemfiles/active_record_7.0.gemfile +0 -8
  118. data/gemfiles/rails_5.2.gemfile +0 -16
  119. data/gemfiles/rails_6.0.gemfile +0 -8
  120. data/gemfiles/rails_6.1.gemfile +0 -8
  121. data/gemfiles/rails_7.0.gemfile +0 -8
  122. data/lib/activerecord-multi-tenant/persistence_extension.rb +0 -13
  123. data/lib/activerecord-multi-tenant/with_lock.rb +0 -15
  124. data/spec/activerecord-multi-tenant/schema_dumper_tester.rb +0 -0
@@ -1,6 +1,7 @@
1
1
  require 'active_record'
2
- require_relative "./arel_visitors_depth_first.rb" unless Arel::Visitors.const_defined?(:DepthFirst)
2
+ require_relative './arel_visitors_depth_first' unless Arel::Visitors.const_defined?(:DepthFirst)
3
3
 
4
+ # Iterates AST and adds tenant enforcement clauses to all relations
4
5
  module MultiTenant
5
6
  class Table
6
7
  attr_reader :arel_table
@@ -9,9 +10,9 @@ module MultiTenant
9
10
  @arel_table = arel_table
10
11
  end
11
12
 
12
- def eql?(rhs)
13
- self.class == rhs.class &&
14
- equality_fields.eql?(rhs.equality_fields)
13
+ def eql?(other)
14
+ self.class == other.class &&
15
+ equality_fields.eql?(other.equality_fields)
15
16
  end
16
17
 
17
18
  def hash
@@ -44,6 +45,7 @@ module MultiTenant
44
45
 
45
46
  def visited_relation(relation)
46
47
  return unless @discovering
48
+
47
49
  @known_relations << Table.new(relation)
48
50
  end
49
51
 
@@ -56,9 +58,13 @@ module MultiTenant
56
58
  end
57
59
  end
58
60
 
59
- class ArelTenantVisitor < Arel::Visitors.const_defined?(:DepthFirst) ? Arel::Visitors::DepthFirst : ::MultiTenant::ArelVisitorsDepthFirst
61
+ class ArelTenantVisitor < if Arel::Visitors.const_defined?(:DepthFirst)
62
+ Arel::Visitors::DepthFirst
63
+ else
64
+ ::MultiTenant::ArelVisitorsDepthFirst
65
+ end
60
66
  def initialize(arel)
61
- super(Proc.new {})
67
+ super(proc {})
62
68
  @statement_node_id = nil
63
69
 
64
70
  @contexts = []
@@ -68,61 +74,72 @@ module MultiTenant
68
74
 
69
75
  attr_reader :contexts
70
76
 
77
+ # rubocop:disable Naming/MethodName
71
78
  def visit_Arel_Attributes_Attribute(*args)
72
79
  return if @current_context.nil?
80
+
73
81
  super(*args)
74
82
  end
75
83
 
76
- def visit_Arel_Nodes_Equality(o, *args)
77
- if o.left.is_a?(Arel::Attributes::Attribute)
78
- table_name = o.left.relation.table_name
84
+ def visit_Arel_Nodes_Equality(obj, *args)
85
+ if obj.left.is_a?(Arel::Attributes::Attribute)
86
+ table_name = MultiTenant::TableNode.table_name(obj.left.relation)
79
87
  model = MultiTenant.multi_tenant_model_for_table(table_name)
80
- @current_context.visited_handled_relation(o.left.relation) if model.present? && o.left.name.to_s == model.partition_key.to_s
88
+ if model.present? && obj.left.name.to_s == model.partition_key.to_s
89
+ @current_context.visited_handled_relation(obj.left.relation)
90
+ end
81
91
  end
82
- super(o, *args)
92
+ super(obj, *args)
83
93
  end
84
94
 
85
- def visit_MultiTenant_TenantEnforcementClause(o, *)
86
- @current_context.visited_handled_relation(o.tenant_attribute.relation)
95
+ def visit_MultiTenant_TenantEnforcementClause(obj, *)
96
+ @current_context.visited_handled_relation(obj.tenant_attribute.relation)
87
97
  end
88
98
 
89
- def visit_MultiTenant_TenantJoinEnforcementClause(o, *)
90
- @current_context.visited_handled_relation(o.tenant_attribute.relation)
99
+ def visit_MultiTenant_TenantJoinEnforcementClause(obj, *)
100
+ @current_context.visited_handled_relation(obj.tenant_attribute.relation)
91
101
  end
92
102
 
93
- def visit_Arel_Table(o, _collector = nil)
94
- @current_context.visited_relation(o) if tenant_relation?(o.table_name)
103
+ def visit_Arel_Table(obj, _collector = nil)
104
+ @current_context.visited_relation(obj) if tenant_relation?(MultiTenant::TableNode.table_name(obj))
95
105
  end
96
- alias :visit_Arel_Nodes_TableAlias :visit_Arel_Table
97
106
 
98
- def visit_Arel_Nodes_SelectCore(o, *args)
99
- nest_context(o) do
107
+ alias visit_Arel_Nodes_TableAlias visit_Arel_Table
108
+
109
+ def visit_Arel_Nodes_SelectCore(obj, *_args)
110
+ nest_context(obj) do
100
111
  @current_context.discover_relations do
101
- visit o.source
112
+ visit obj.source
102
113
  end
103
- visit o.wheres
104
- visit o.groups
105
- visit o.windows
106
- if defined?(o.having)
107
- visit o.having
114
+ visit obj.wheres
115
+ visit obj.groups
116
+ visit obj.windows
117
+ if defined?(obj.having)
118
+ visit obj.having
108
119
  else
109
- visit o.havings
120
+ visit obj.havings
110
121
  end
111
122
  end
112
123
  end
113
124
 
114
- def visit_Arel_Nodes_OuterJoin(o, collector = nil)
115
- nest_context(o) do
125
+ # rubocop:enable Naming/MethodName
126
+
127
+ # rubocop:disable Naming/MethodName
128
+ def visit_Arel_Nodes_OuterJoin(obj, _collector = nil)
129
+ nest_context(obj) do
116
130
  @current_context.discover_relations do
117
- visit o.left
118
- visit o.right
131
+ visit obj.left
132
+ visit obj.right
119
133
  end
120
134
  end
121
135
  end
122
- alias :visit_Arel_Nodes_FullOuterJoin :visit_Arel_Nodes_OuterJoin
123
- alias :visit_Arel_Nodes_RightOuterJoin :visit_Arel_Nodes_OuterJoin
124
136
 
125
- alias :visit_ActiveModel_Attribute :terminal
137
+ # rubocop:enable Naming/MethodName
138
+
139
+ alias visit_Arel_Nodes_FullOuterJoin visit_Arel_Nodes_OuterJoin
140
+ alias visit_Arel_Nodes_RightOuterJoin visit_Arel_Nodes_OuterJoin
141
+
142
+ alias visit_ActiveModel_Attribute terminal
126
143
 
127
144
  private
128
145
 
@@ -138,13 +155,16 @@ module MultiTenant
138
155
  DISPATCH
139
156
  end
140
157
 
158
+ # rubocop:disable Naming/AccessorMethodName
141
159
  def get_dispatch_cache
142
160
  dispatch
143
161
  end
144
162
 
145
- def nest_context(o)
163
+ # rubocop:enable Naming/AccessorMethodName
164
+
165
+ def nest_context(obj)
146
166
  old_context = @current_context
147
- @current_context = Context.new(o)
167
+ @current_context = Context.new(obj)
148
168
  @contexts << @current_context
149
169
 
150
170
  yield
@@ -155,25 +175,33 @@ module MultiTenant
155
175
 
156
176
  class BaseTenantEnforcementClause < Arel::Nodes::Node
157
177
  attr_reader :tenant_attribute
178
+
158
179
  def initialize(tenant_attribute)
180
+ super()
159
181
  @tenant_attribute = tenant_attribute
160
- @tenant_model = MultiTenant.multi_tenant_model_for_table(tenant_attribute.relation.table_name)
182
+ @tenant_model = MultiTenant.multi_tenant_model_for_table(
183
+ MultiTenant::TableNode.table_name(tenant_attribute.relation)
184
+ )
185
+ end
186
+
187
+ def to_s
188
+ to_sql
161
189
  end
162
190
 
163
- def to_s; to_sql; end
164
- def to_str; to_sql; end
191
+ def to_str
192
+ to_sql
193
+ end
165
194
 
166
195
  def to_sql(*)
167
196
  collector = Arel::Collectors::SQLString.new
168
197
  collector = @tenant_model.connection.visitor.accept tenant_arel, collector
169
198
  collector.value
170
199
  end
171
-
172
-
173
200
  end
174
201
 
175
202
  class TenantEnforcementClause < BaseTenantEnforcementClause
176
203
  private
204
+
177
205
  def tenant_arel
178
206
  if defined?(Arel::Nodes::Quoted)
179
207
  @tenant_attribute.eq(Arel::Nodes::Quoted.new(MultiTenant.current_tenant_id))
@@ -183,36 +211,39 @@ module MultiTenant
183
211
  end
184
212
  end
185
213
 
186
-
187
214
  class TenantJoinEnforcementClause < BaseTenantEnforcementClause
188
215
  attr_reader :table_left
216
+
189
217
  def initialize(tenant_attribute, table_left)
190
218
  super(tenant_attribute)
191
219
  @table_left = table_left
192
- @model_left = MultiTenant.multi_tenant_model_for_table(table_left.table_name)
220
+ @model_left = MultiTenant.multi_tenant_model_for_table(MultiTenant::TableNode.table_name(table_left))
193
221
  end
194
222
 
195
223
  private
224
+
196
225
  def tenant_arel
197
226
  @tenant_attribute.eq(@table_left[@model_left.partition_key])
198
227
  end
199
228
  end
200
229
 
201
-
202
230
  module TenantValueVisitor
203
- def visit_MultiTenant_TenantEnforcementClause(o, collector)
204
- collector << o
231
+ # rubocop:disable Naming/MethodName
232
+ def visit_MultiTenant_TenantEnforcementClause(obj, collector)
233
+ collector << obj
205
234
  end
206
235
 
207
- def visit_MultiTenant_TenantJoinEnforcementClause(o, collector)
208
- collector << o
236
+ def visit_MultiTenant_TenantJoinEnforcementClause(obj, collector)
237
+ collector << obj
209
238
  end
239
+
240
+ # rubocop:enable Naming/MethodName
210
241
  end
211
242
 
212
243
  module DatabaseStatements
213
244
  def join_to_update(update, *args)
214
245
  update = super(update, *args)
215
- model = MultiTenant.multi_tenant_model_for_table(update.ast.relation.table_name)
246
+ model = MultiTenant.multi_tenant_model_for_table(MultiTenant::TableNode.table_name(update.ast.relation))
216
247
  if model.present? && !MultiTenant.with_write_only_mode_enabled? && MultiTenant.current_tenant_id.present?
217
248
  update.where(MultiTenant::TenantEnforcementClause.new(model.arel_table[model.partition_key]))
218
249
  end
@@ -221,7 +252,7 @@ module MultiTenant
221
252
 
222
253
  def join_to_delete(delete, *args)
223
254
  delete = super(delete, *args)
224
- model = MultiTenant.multi_tenant_model_for_table(delete.ast.left.table_name)
255
+ model = MultiTenant.multi_tenant_model_for_table(MultiTenant::TableNode.table_name(delete.ast.left))
225
256
  if model.present? && !MultiTenant.with_write_only_mode_enabled? && MultiTenant.current_tenant_id.present?
226
257
  delete.where(MultiTenant::TenantEnforcementClause.new(model.arel_table[model.partition_key]))
227
258
  end
@@ -254,64 +285,61 @@ Arel::Visitors::ToSql.include(MultiTenant::TenantValueVisitor)
254
285
  require 'active_record/relation'
255
286
  module ActiveRecord
256
287
  module QueryMethods
257
- alias :build_arel_orig :build_arel
288
+ alias build_arel_orig build_arel
289
+
258
290
  def build_arel(*args)
259
291
  arel = build_arel_orig(*args)
260
292
 
261
- if !MultiTenant.with_write_only_mode_enabled?
293
+ unless MultiTenant.with_write_only_mode_enabled?
262
294
  visitor = MultiTenant::ArelTenantVisitor.new(arel)
263
295
 
264
296
  visitor.contexts.each do |context|
265
297
  node = context.arel_node
266
298
 
267
299
  context.unhandled_relations.each do |relation|
268
- model = MultiTenant.multi_tenant_model_for_table(relation.arel_table.table_name)
300
+ model = MultiTenant.multi_tenant_model_for_table(MultiTenant::TableNode.table_name(relation.arel_table))
269
301
 
270
302
  if MultiTenant.current_tenant_id
271
303
  enforcement_clause = MultiTenant::TenantEnforcementClause.new(relation.arel_table[model.partition_key])
272
304
  case node
273
- when Arel::Nodes::Join #Arel::Nodes::OuterJoin, Arel::Nodes::RightOuterJoin, Arel::Nodes::FullOuterJoin
305
+ when Arel::Nodes::Join # Arel::Nodes::OuterJoin, Arel::Nodes::RightOuterJoin, Arel::Nodes::FullOuterJoin
274
306
  node.right.expr = node.right.expr.and(enforcement_clause)
275
307
  when Arel::Nodes::SelectCore
276
308
  if node.wheres.empty?
277
309
  node.wheres = [enforcement_clause]
310
+ elsif node.wheres[0].is_a?(Arel::Nodes::And)
311
+ node.wheres[0].children << enforcement_clause
278
312
  else
279
- if node.wheres[0].is_a?(Arel::Nodes::And)
280
- node.wheres[0].children << enforcement_clause
281
- else
282
- node.wheres[0] = enforcement_clause.and(node.wheres[0])
283
- end
313
+ node.wheres[0] = enforcement_clause.and(node.wheres[0])
284
314
  end
285
315
  else
286
- raise "UnknownContext"
316
+ raise 'UnknownContext'
287
317
  end
288
318
  end
289
319
 
290
- if node.is_a?(Arel::Nodes::SelectCore) || node.is_a?(Arel::Nodes::Join)
291
- if node.is_a?Arel::Nodes::Join
292
- node_list = [node]
293
- else
294
- node_list = node.source.right
295
- end
320
+ next unless node.is_a?(Arel::Nodes::SelectCore) || node.is_a?(Arel::Nodes::Join)
296
321
 
297
- node_list.select{ |n| n.is_a? Arel::Nodes::Join }.each do |node_join|
298
- if (!node_join.right ||
299
- (ActiveRecord::VERSION::MAJOR == 5 &&
300
- !node_join.right.expr.right.is_a?(Arel::Attributes::Attribute)))
301
- next
302
- end
322
+ node_list = if node.is_a? Arel::Nodes::Join
323
+ [node]
324
+ else
325
+ node.source.right
326
+ end
303
327
 
304
- relation_right, relation_left = relations_from_node_join(node_join)
328
+ node_list.select { |n| n.is_a? Arel::Nodes::Join }.each do |node_join|
329
+ next unless node_join.right
305
330
 
306
- next unless relation_right && relation_left
331
+ relation_right, relation_left = relations_from_node_join(node_join)
307
332
 
308
- model_right = MultiTenant.multi_tenant_model_for_table(relation_left.table_name)
309
- model_left = MultiTenant.multi_tenant_model_for_table(relation_right.table_name)
310
- if model_right && model_left
311
- join_enforcement_clause = MultiTenant::TenantJoinEnforcementClause.new(relation_left[model_left.partition_key], relation_right)
312
- node_join.right.expr = node_join.right.expr.and(join_enforcement_clause)
313
- end
314
- end
333
+ next unless relation_right && relation_left
334
+
335
+ model_right = MultiTenant.multi_tenant_model_for_table(MultiTenant::TableNode.table_name(relation_left))
336
+ model_left = MultiTenant.multi_tenant_model_for_table(MultiTenant::TableNode.table_name(relation_right))
337
+ next unless model_right && model_left
338
+
339
+ join_enforcement_clause = MultiTenant::TenantJoinEnforcementClause.new(
340
+ relation_right[model_right.partition_key], relation_left
341
+ )
342
+ node_join.right.expr = node_join.right.expr.and(join_enforcement_clause)
315
343
  end
316
344
  end
317
345
  end
@@ -321,28 +349,29 @@ module ActiveRecord
321
349
  end
322
350
 
323
351
  private
352
+
324
353
  def relations_from_node_join(node_join)
325
- if ActiveRecord::VERSION::MAJOR == 5 || node_join.right.expr.is_a?(Arel::Nodes::Equality)
354
+ if node_join.right.expr.is_a?(Arel::Nodes::Equality)
326
355
  return node_join.right.expr.right.relation, node_join.right.expr.left.relation
327
356
  end
328
357
 
329
- children = node_join.right.expr.children
358
+ children = [node_join.right.expr.children].flatten
330
359
 
331
- tenant_applied = children.any?(MultiTenant::TenantEnforcementClause) || children.any?(MultiTenant::TenantJoinEnforcementClause)
332
- if tenant_applied || children.empty?
333
- return nil, nil
360
+ tenant_applied = children.any? do |c|
361
+ c.is_a?(MultiTenant::TenantEnforcementClause) || c.is_a?(MultiTenant::TenantJoinEnforcementClause)
334
362
  end
363
+ return nil, nil if tenant_applied || children.empty?
335
364
 
336
- if children[0].right.respond_to?('relation') && children[0].left.respond_to?('relation')
337
- return children[0].right.relation, children[0].left.relation
365
+ child = children.first.respond_to?(:children) ? children.first.children.first : children.first
366
+ if child.right.respond_to?(:relation) && child.left.respond_to?(:relation)
367
+ return child.right.relation, child.left.relation
338
368
  end
339
369
 
340
- return nil, nil
370
+ [nil, nil]
341
371
  end
342
372
  end
343
373
  end
344
374
 
345
- require 'active_record/base'
346
375
  module MultiTenantFindBy
347
376
  def cached_find_by_statement(key, &block)
348
377
  return super unless respond_to?(:scoped_by_tenant?) && scoped_by_tenant?
@@ -352,4 +381,6 @@ module MultiTenantFindBy
352
381
  end
353
382
  end
354
383
 
355
- ActiveRecord::Base.singleton_class.prepend(MultiTenantFindBy)
384
+ ActiveSupport.on_load(:active_record) do |base|
385
+ base.singleton_class.prepend(MultiTenantFindBy)
386
+ end
@@ -1,14 +1,17 @@
1
1
  require 'sidekiq/client'
2
2
 
3
+ # Adds methods to handle tenant information both in the client and server.
3
4
  module Sidekiq::Middleware::MultiTenant
4
5
  # Get the current tenant and store in the message to be sent to Sidekiq.
5
6
  class Client
6
- def call(worker_class, msg, queue, redis_pool)
7
- msg['multi_tenant'] ||=
8
- {
9
- 'class' => MultiTenant.current_tenant_class,
10
- 'id' => MultiTenant.current_tenant_id
11
- } if MultiTenant.current_tenant.present?
7
+ def call(_worker_class, msg, _queue, _redis_pool)
8
+ if MultiTenant.current_tenant.present?
9
+ msg['multi_tenant'] ||=
10
+ {
11
+ 'class' => MultiTenant.current_tenant_class,
12
+ 'id' => MultiTenant.current_tenant_id
13
+ }
14
+ end
12
15
 
13
16
  yield
14
17
  end
@@ -16,16 +19,14 @@ module Sidekiq::Middleware::MultiTenant
16
19
 
17
20
  # Pull the tenant out and run the current thread with it.
18
21
  class Server
19
- def call(worker_class, msg, queue)
20
- if msg.has_key?('multi_tenant')
22
+ def call(_worker_class, msg, _queue, &block)
23
+ if msg.key?('multi_tenant')
21
24
  tenant = begin
22
- msg['multi_tenant']['class'].constantize.find(msg['multi_tenant']['id'])
23
- rescue ActiveRecord::RecordNotFound
24
- msg['multi_tenant']['id']
25
- end
26
- MultiTenant.with(tenant) do
27
- yield
25
+ msg['multi_tenant']['class'].constantize.find(msg['multi_tenant']['id'])
26
+ rescue ActiveRecord::RecordNotFound
27
+ msg['multi_tenant']['id']
28
28
  end
29
+ MultiTenant.with(tenant, &block)
29
30
  else
30
31
  yield
31
32
  end
@@ -33,26 +34,52 @@ module Sidekiq::Middleware::MultiTenant
33
34
  end
34
35
  end
35
36
 
37
+ # Configure Sidekiq to use the multi-tenant client and server middleware to add (client/server)/process(server)
38
+ # tenant information.
39
+ Sidekiq.configure_server do |config|
40
+ config.server_middleware do |chain|
41
+ chain.add Sidekiq::Middleware::MultiTenant::Server
42
+ end
43
+ config.client_middleware do |chain|
44
+ chain.add Sidekiq::Middleware::MultiTenant::Client
45
+ end
46
+ end
47
+
48
+ Sidekiq.configure_client do |config|
49
+ config.client_middleware do |chain|
50
+ chain.add Sidekiq::Middleware::MultiTenant::Client
51
+ end
52
+ end
53
+
54
+ # Bulk push support for Sidekiq while setting multi-tenant information.
55
+ # This is a copy of the Sidekiq::Client#push_bulk method with the addition of
56
+ # setting the multi-tenant information for each job.
36
57
  module Sidekiq
37
58
  class Client
59
+ # Allows the caller to enqueue multiple Sidekiq jobs with
60
+ # tenant information in a single call. It ensures that each job is processed
61
+ # within the correct tenant context and returns an array of job IDs for the enqueued jobs
38
62
  def push_bulk_with_tenants(items)
39
- job = items['jobs'].first
40
- return [] unless job # no jobs to push
41
- raise ArgumentError, "Bulk arguments must be an Array of Hashes: [{ 'args' => [1], 'tenant_id' => 1 }, ...]" if !job.is_a?(Hash)
63
+ first_job = items['jobs'].first
64
+ return [] unless first_job # no jobs to push
65
+ unless first_job.is_a?(Hash)
66
+ raise ArgumentError, "Bulk arguments must be an Array of Hashes: [{ 'args' => [1], 'tenant_id' => 1 }, ...]"
67
+ end
42
68
 
43
69
  normed = normalize_item(items.except('jobs').merge('args' => []))
44
70
  payloads = items['jobs'].map do |job|
45
71
  MultiTenant.with(job['tenant_id']) do
46
72
  copy = normed.merge('args' => job['args'], 'jid' => SecureRandom.hex(12), 'enqueued_at' => Time.now.to_f)
47
73
  result = process_single(items['class'], copy)
48
- result ? result : nil
74
+ result || nil
49
75
  end
50
76
  end.compact
51
77
 
52
- raw_push(payloads) if !payloads.empty?
78
+ raw_push(payloads) unless payloads.empty?
53
79
  payloads.collect { |payload| payload['jid'] }
54
80
  end
55
81
 
82
+ # Enabling the push_bulk_with_tenants method to be called directly on the Sidekiq::Client class
56
83
  class << self
57
84
  def push_bulk_with_tenants(items)
58
85
  new.push_bulk_with_tenants(items)
@@ -0,0 +1,13 @@
1
+ module MultiTenant
2
+ module TableNode
3
+ # Return table name
4
+ def self.table_name(node)
5
+ # NOTE: Arel::Nodes::Table#table_name is removed in Rails 7.1
6
+ if node.is_a?(Arel::Nodes::TableAlias)
7
+ node.table_name
8
+ else
9
+ node.name
10
+ end
11
+ end
12
+ end
13
+ end
@@ -1,3 +1,3 @@
1
1
  module MultiTenant
2
- VERSION = '1.2.0'
2
+ VERSION = '2.4.0'.freeze
3
3
  end
@@ -1,13 +1,3 @@
1
- if Object.const_defined?(:ActionController)
2
- require_relative 'activerecord-multi-tenant/controller_extensions'
3
- end
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/version'
12
- require_relative 'activerecord-multi-tenant/with_lock'
13
- require_relative 'activerecord-multi-tenant/persistence_extension'
1
+ # frozen_string_literal: true
2
+
3
+ require 'activerecord_multi_tenant'
@@ -0,0 +1,13 @@
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'
@@ -0,0 +1,42 @@
1
+ require 'spec_helper'
2
+
3
+ describe MultiTenant, 'Association methods' do
4
+ let(:account1) { Account.create! name: 'test1' }
5
+ let(:account2) { Account.create! name: 'test2' }
6
+ let(:project1) { Project.create! name: 'something1', account: account1 }
7
+ let(:project2) { Project.create! name: 'something2', account: account2, id: project1.id }
8
+
9
+ let(:task1) { Task.create! name: 'task1', project: project1, account: account1 }
10
+ let(:task2) { Task.create! name: 'task2', project: project2, account: account2, id: task1.id }
11
+ let(:manager1) { Manager.create! name: 'manager1', account: account1, tasks: [task1] }
12
+ let(:project3) { Project.create! name: 'something3', account: account1, managers: [manager1] }
13
+
14
+ context 'include the tenant_id in queries and' do
15
+ it 'creates a task with correct account_id' do
16
+ expect(project2.tasks.create(name: 'task3').account_id).to eq(account2.id)
17
+ end
18
+ it 'return correct account_id' do
19
+ expect(task1.project.account_id).to_not eq(task2.project.account_id) # belongs_to
20
+ expect(project2.tasks.count).to eq(1)
21
+ expect(project2.tasks.first.account_id).to eq(account2.id) # has_many
22
+ end
23
+
24
+ it 'check has_many_belongs_to' do
25
+ MultiTenant.with(account1) do
26
+ expect(manager1.tasks.first.account_id).to eq(task1.account_id) # has_many
27
+ end
28
+ end
29
+
30
+ it 'check has_many_belongs_to without tenant in the intermediate table' do
31
+ MultiTenant.with(account1) do
32
+ expect(manager1.tasks.first.account_id).to eq(task1.account_id) # has_many
33
+ end
34
+ end
35
+
36
+ it 'check has_many_belongs_to tenant_enabled false' do
37
+ MultiTenant.with(account1) do
38
+ expect(project3.managers.first.id).to eq(manager1.id) # has_many
39
+ end
40
+ end
41
+ end
42
+ end
@@ -1,6 +1,8 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'spec_helper'
2
4
 
3
- describe "Controller Extensions", type: :controller do
5
+ describe 'Controller Extensions', type: :controller do
4
6
  class Account
5
7
  attr_accessor :name
6
8
  end
@@ -31,7 +33,6 @@ describe "Controller Extensions", type: :controller do
31
33
  end
32
34
  end
33
35
 
34
-
35
36
  class APIApplicationController < ActionController::API
36
37
  include Rails.application.routes.url_helpers
37
38
  set_current_tenant_through_filter
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'spec_helper'
2
4
 
3
5
  describe MultiTenant::FastTruncate do
@@ -5,19 +7,19 @@ describe MultiTenant::FastTruncate do
5
7
  MultiTenant::FastTruncate.run
6
8
  end
7
9
 
8
- it "truncates tables that have exactly one row inserted" do
10
+ it 'truncates tables that have exactly one row inserted' do
9
11
  Account.create! name: 'foo'
10
- expect {
12
+ expect do
11
13
  MultiTenant::FastTruncate.run
12
- }.to change { Account.count }.from(1).to(0)
14
+ end.to change { Account.count }.from(1).to(0)
13
15
  end
14
16
 
15
- it "truncates tables that have more than one row inserted" do
17
+ it 'truncates tables that have more than one row inserted' do
16
18
  Account.create! name: 'foo'
17
19
  Account.create! name: 'bar'
18
20
 
19
- expect {
21
+ expect do
20
22
  MultiTenant::FastTruncate.run
21
- }.to change { Account.count }.from(2).to(0)
23
+ end.to change { Account.count }.from(2).to(0)
22
24
  end
23
25
  end