activerecord-multi-tenant 2.2.0 → 2.4.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (121) 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 +8 -0
  8. data/CHANGELOG.md +13 -0
  9. data/Gemfile +3 -1
  10. data/LICENSE +0 -0
  11. data/README.md +2 -1
  12. data/Rakefile +1 -1
  13. data/activerecord-multi-tenant.gemspec +28 -22
  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 +18 -8
  88. data/lib/activerecord-multi-tenant/model_extensions.rb +80 -39
  89. data/lib/activerecord-multi-tenant/multi_tenant.rb +49 -25
  90. data/lib/activerecord-multi-tenant/query_monitor.rb +21 -5
  91. data/lib/activerecord-multi-tenant/query_rewriter.rb +118 -85
  92. data/lib/activerecord-multi-tenant/sidekiq.rb +31 -20
  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 -12
  96. data/lib/activerecord_multi_tenant.rb +13 -0
  97. data/spec/activerecord-multi-tenant/associations_spec.rb +21 -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 +233 -153
  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 +4 -4
  106. data/spec/activerecord-multi-tenant/sidekiq_spec.rb +10 -10
  107. data/spec/database.yml +0 -0
  108. data/spec/schema.rb +20 -2
  109. data/spec/spec_helper.rb +46 -17
  110. data/spec/support/format_sql.rb +20 -0
  111. metadata +131 -25
  112. data/.github/workflows/CI.yml +0 -47
  113. data/gemfiles/.bundle/config +0 -2
  114. data/gemfiles/active_record_6.0.gemfile +0 -8
  115. data/gemfiles/active_record_6.1.gemfile +0 -8
  116. data/gemfiles/active_record_7.0.gemfile +0 -8
  117. data/gemfiles/rails_6.0.gemfile +0 -8
  118. data/gemfiles/rails_6.1.gemfile +0 -8
  119. data/gemfiles/rails_7.0.gemfile +0 -8
  120. data/lib/activerecord-multi-tenant/with_lock.rb +0 -15
  121. 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,61 +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
- next
300
- end
301
- relation_right, relation_left = relations_from_node_join(node_join)
322
+ node_list = if node.is_a? Arel::Nodes::Join
323
+ [node]
324
+ else
325
+ node.source.right
326
+ end
302
327
 
303
- next unless relation_right && relation_left
328
+ node_list.select { |n| n.is_a? Arel::Nodes::Join }.each do |node_join|
329
+ next unless node_join.right
304
330
 
305
- model_right = MultiTenant.multi_tenant_model_for_table(relation_left.table_name)
306
- model_left = MultiTenant.multi_tenant_model_for_table(relation_right.table_name)
307
- if model_right && model_left
308
- join_enforcement_clause = MultiTenant::TenantJoinEnforcementClause.new(relation_right[model_right.partition_key], relation_left)
309
- node_join.right.expr = node_join.right.expr.and(join_enforcement_clause)
310
- end
311
- end
331
+ relation_right, relation_left = relations_from_node_join(node_join)
332
+
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)
312
343
  end
313
344
  end
314
345
  end
@@ -318,6 +349,7 @@ module ActiveRecord
318
349
  end
319
350
 
320
351
  private
352
+
321
353
  def relations_from_node_join(node_join)
322
354
  if node_join.right.expr.is_a?(Arel::Nodes::Equality)
323
355
  return node_join.right.expr.right.relation, node_join.right.expr.left.relation
@@ -325,22 +357,21 @@ module ActiveRecord
325
357
 
326
358
  children = [node_join.right.expr.children].flatten
327
359
 
328
- tenant_applied = children.any?{|c| c.is_a?(MultiTenant::TenantEnforcementClause) || c.is_a?(MultiTenant::TenantJoinEnforcementClause)}
329
- if tenant_applied || children.empty?
330
- return nil, nil
360
+ tenant_applied = children.any? do |c|
361
+ c.is_a?(MultiTenant::TenantEnforcementClause) || c.is_a?(MultiTenant::TenantJoinEnforcementClause)
331
362
  end
363
+ return nil, nil if tenant_applied || children.empty?
332
364
 
333
365
  child = children.first.respond_to?(:children) ? children.first.children.first : children.first
334
366
  if child.right.respond_to?(:relation) && child.left.respond_to?(:relation)
335
367
  return child.right.relation, child.left.relation
336
368
  end
337
369
 
338
- return nil, nil
370
+ [nil, nil]
339
371
  end
340
372
  end
341
373
  end
342
374
 
343
- require 'active_record/base'
344
375
  module MultiTenantFindBy
345
376
  def cached_find_by_statement(key, &block)
346
377
  return super unless respond_to?(:scoped_by_tenant?) && scoped_by_tenant?
@@ -350,4 +381,6 @@ module MultiTenantFindBy
350
381
  end
351
382
  end
352
383
 
353
- 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,6 +34,8 @@ 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.
36
39
  Sidekiq.configure_server do |config|
37
40
  config.server_middleware do |chain|
38
41
  chain.add Sidekiq::Middleware::MultiTenant::Server
@@ -48,27 +51,35 @@ Sidekiq.configure_client do |config|
48
51
  end
49
52
  end
50
53
 
51
-
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.
52
57
  module Sidekiq
53
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
54
62
  def push_bulk_with_tenants(items)
55
- job = items['jobs'].first
56
- return [] unless job # no jobs to push
57
- 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
58
68
 
59
69
  normed = normalize_item(items.except('jobs').merge('args' => []))
60
70
  payloads = items['jobs'].map do |job|
61
71
  MultiTenant.with(job['tenant_id']) do
62
72
  copy = normed.merge('args' => job['args'], 'jid' => SecureRandom.hex(12), 'enqueued_at' => Time.now.to_f)
63
73
  result = process_single(items['class'], copy)
64
- result ? result : nil
74
+ result || nil
65
75
  end
66
76
  end.compact
67
77
 
68
- raw_push(payloads) if !payloads.empty?
78
+ raw_push(payloads) unless payloads.empty?
69
79
  payloads.collect { |payload| payload['jid'] }
70
80
  end
71
81
 
82
+ # Enabling the push_bulk_with_tenants method to be called directly on the Sidekiq::Client class
72
83
  class << self
73
84
  def push_bulk_with_tenants(items)
74
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 = '2.2.0'
2
+ VERSION = '2.4.0'.freeze
3
3
  end
@@ -1,12 +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'
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'
@@ -5,8 +5,11 @@ describe MultiTenant, 'Association methods' do
5
5
  let(:account2) { Account.create! name: 'test2' }
6
6
  let(:project1) { Project.create! name: 'something1', account: account1 }
7
7
  let(:project2) { Project.create! name: 'something2', account: account2, id: project1.id }
8
+
8
9
  let(:task1) { Task.create! name: 'task1', project: project1, account: account1 }
9
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] }
10
13
 
11
14
  context 'include the tenant_id in queries and' do
12
15
  it 'creates a task with correct account_id' do
@@ -17,5 +20,23 @@ describe MultiTenant, 'Association methods' do
17
20
  expect(project2.tasks.count).to eq(1)
18
21
  expect(project2.tasks.first.account_id).to eq(account2.id) # has_many
19
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
20
41
  end
21
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