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.
- checksums.yaml +4 -4
- data/.github/workflows/active-record-multi-tenant-tests.yml +83 -0
- data/.gitignore +6 -0
- data/.readthedocs.yaml +15 -0
- data/.rspec +0 -0
- data/.rubocop.yml +51 -0
- data/Appraisals +6 -22
- data/CHANGELOG.md +51 -0
- data/Gemfile +3 -1
- data/LICENSE +0 -0
- data/README.md +3 -2
- data/Rakefile +1 -1
- data/activerecord-multi-tenant.gemspec +28 -23
- data/docker-compose.yml +24 -18
- data/docs/.gitignore +3 -0
- data/docs/Makefile +28 -0
- data/docs/api-reference.sh +10 -0
- data/docs/requirements.in +4 -0
- data/docs/requirements.txt +62 -0
- data/docs/source/_static/api-reference/ActiveRecord/Associations/Association.html +285 -0
- data/docs/source/_static/api-reference/ActiveRecord/Associations/ClassMethods.html +255 -0
- data/docs/source/_static/api-reference/ActiveRecord/Associations.html +117 -0
- data/docs/source/_static/api-reference/ActiveRecord/ConnectionAdapters/SchemaStatements.html +232 -0
- data/docs/source/_static/api-reference/ActiveRecord/ConnectionAdapters.html +126 -0
- data/docs/source/_static/api-reference/ActiveRecord/QueryMethods.html +336 -0
- data/docs/source/_static/api-reference/ActiveRecord/SchemaDumper.html +121 -0
- data/docs/source/_static/api-reference/ActiveRecord.html +130 -0
- data/docs/source/_static/api-reference/MultiTenant/ArelTenantVisitor.html +755 -0
- data/docs/source/_static/api-reference/MultiTenant/ArelVisitorsDepthFirst.html +208 -0
- data/docs/source/_static/api-reference/MultiTenant/BaseTenantEnforcementClause.html +462 -0
- data/docs/source/_static/api-reference/MultiTenant/Context.html +659 -0
- data/docs/source/_static/api-reference/MultiTenant/ControllerExtensions.html +202 -0
- data/docs/source/_static/api-reference/MultiTenant/CopyFromClient.html +186 -0
- data/docs/source/_static/api-reference/MultiTenant/CopyFromClientHelper.html +362 -0
- data/docs/source/_static/api-reference/MultiTenant/Current.html +124 -0
- data/docs/source/_static/api-reference/MultiTenant/DatabaseStatements.html +366 -0
- data/docs/source/_static/api-reference/MultiTenant/FastTruncate.html +226 -0
- data/docs/source/_static/api-reference/MultiTenant/MigrationExtensions.html +554 -0
- data/docs/source/_static/api-reference/MultiTenant/MissingTenantError.html +124 -0
- data/docs/source/_static/api-reference/MultiTenant/ModelExtensionsClassMethods.html +492 -0
- data/docs/source/_static/api-reference/MultiTenant/QueryMonitor.html +257 -0
- data/docs/source/_static/api-reference/MultiTenant/Table.html +419 -0
- data/docs/source/_static/api-reference/MultiTenant/TenantEnforcementClause.html +148 -0
- data/docs/source/_static/api-reference/MultiTenant/TenantIsImmutable.html +135 -0
- data/docs/source/_static/api-reference/MultiTenant/TenantJoinEnforcementClause.html +310 -0
- data/docs/source/_static/api-reference/MultiTenant/TenantValueVisitor.html +239 -0
- data/docs/source/_static/api-reference/MultiTenant.html +1454 -0
- data/docs/source/_static/api-reference/MultiTenantFindBy.html +180 -0
- data/docs/source/_static/api-reference/Sidekiq/Client.html +302 -0
- data/docs/source/_static/api-reference/Sidekiq/Middleware/MultiTenant/Client.html +217 -0
- data/docs/source/_static/api-reference/Sidekiq/Middleware/MultiTenant/Server.html +219 -0
- data/docs/source/_static/api-reference/Sidekiq/Middleware/MultiTenant.html +126 -0
- data/docs/source/_static/api-reference/Sidekiq.html +126 -0
- data/docs/source/_static/api-reference/_index.html +399 -0
- data/docs/source/_static/api-reference/class_list.html +51 -0
- data/docs/source/_static/api-reference/css/common.css +1 -0
- data/docs/source/_static/api-reference/css/full_list.css +58 -0
- data/docs/source/_static/api-reference/css/style.css +497 -0
- data/docs/source/_static/api-reference/file.README.html +167 -0
- data/docs/source/_static/api-reference/file_list.html +56 -0
- data/docs/source/_static/api-reference/frames.html +17 -0
- data/docs/source/_static/api-reference/index.html +167 -0
- data/docs/source/_static/api-reference/js/app.js +314 -0
- data/docs/source/_static/api-reference/js/full_list.js +216 -0
- data/docs/source/_static/api-reference/js/jquery.js +4 -0
- data/docs/source/_static/api-reference/method_list.html +715 -0
- data/docs/source/_static/api-reference/top-level-namespace.html +126 -0
- data/docs/source/_templates/.gitignore +4 -0
- data/docs/source/api-reference.rst +8 -0
- data/docs/source/appendix.rst +26 -0
- data/docs/source/changelog.rst +8 -0
- data/docs/source/community-and-support.rst +26 -0
- data/docs/source/conf.py +30 -0
- data/docs/source/contributing.rst +70 -0
- data/docs/source/getting-started.rst +37 -0
- data/docs/source/guides-and-tutorials.rst +129 -0
- data/docs/source/index.rst +54 -0
- data/docs/source/introduction.rst +33 -0
- data/docs/source/license.rst +22 -0
- data/docs/source/troubleshooting.rst +41 -0
- data/docs/source/usage-guide.rst +59 -0
- data/lib/activerecord-multi-tenant/arel_visitors_depth_first.rb +183 -174
- data/lib/activerecord-multi-tenant/controller_extensions.rb +15 -4
- data/lib/activerecord-multi-tenant/copy_from_client.rb +4 -0
- data/lib/activerecord-multi-tenant/fast_truncate.rb +4 -2
- data/lib/activerecord-multi-tenant/habtm.rb +50 -0
- data/lib/activerecord-multi-tenant/migrations.rb +87 -10
- data/lib/activerecord-multi-tenant/model_extensions.rb +98 -34
- data/lib/activerecord-multi-tenant/multi_tenant.rb +102 -29
- data/lib/activerecord-multi-tenant/query_monitor.rb +21 -5
- data/lib/activerecord-multi-tenant/query_rewriter.rb +122 -91
- data/lib/activerecord-multi-tenant/sidekiq.rb +46 -19
- data/lib/activerecord-multi-tenant/table_node.rb +13 -0
- data/lib/activerecord-multi-tenant/version.rb +1 -1
- data/lib/activerecord-multi-tenant.rb +3 -13
- data/lib/activerecord_multi_tenant.rb +13 -0
- data/spec/activerecord-multi-tenant/associations_spec.rb +42 -0
- data/spec/activerecord-multi-tenant/controller_extensions_spec.rb +3 -2
- data/spec/activerecord-multi-tenant/fast_truncate_spec.rb +8 -6
- data/spec/activerecord-multi-tenant/model_extensions_spec.rb +347 -143
- data/spec/activerecord-multi-tenant/multi_tenant_spec.rb +69 -13
- data/spec/activerecord-multi-tenant/query_rewriter_spec.rb +60 -59
- data/spec/activerecord-multi-tenant/record_callback_spec.rb +0 -0
- data/spec/activerecord-multi-tenant/record_finding_spec.rb +11 -11
- data/spec/activerecord-multi-tenant/record_modifications_spec.rb +23 -4
- data/spec/activerecord-multi-tenant/sidekiq_spec.rb +10 -10
- data/spec/database.yml +0 -0
- data/spec/schema.rb +43 -2
- data/spec/spec_helper.rb +52 -16
- data/spec/support/format_sql.rb +20 -0
- metadata +126 -36
- data/.github/workflows/CI.yml +0 -63
- data/gemfiles/.bundle/config +0 -2
- data/gemfiles/active_record_5.2.gemfile +0 -16
- data/gemfiles/active_record_6.0.gemfile +0 -8
- data/gemfiles/active_record_6.1.gemfile +0 -8
- data/gemfiles/active_record_7.0.gemfile +0 -8
- data/gemfiles/rails_5.2.gemfile +0 -16
- data/gemfiles/rails_6.0.gemfile +0 -8
- data/gemfiles/rails_6.1.gemfile +0 -8
- data/gemfiles/rails_7.0.gemfile +0 -8
- data/lib/activerecord-multi-tenant/persistence_extension.rb +0 -13
- data/lib/activerecord-multi-tenant/with_lock.rb +0 -15
- data/spec/activerecord-multi-tenant/schema_dumper_tester.rb +0 -0
@@ -1,6 +1,7 @@
|
|
1
1
|
require 'active_record'
|
2
|
-
require_relative
|
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?(
|
13
|
-
self.class ==
|
14
|
-
equality_fields.eql?(
|
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)
|
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(
|
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(
|
77
|
-
if
|
78
|
-
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
|
-
|
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(
|
92
|
+
super(obj, *args)
|
83
93
|
end
|
84
94
|
|
85
|
-
def visit_MultiTenant_TenantEnforcementClause(
|
86
|
-
@current_context.visited_handled_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(
|
90
|
-
@current_context.visited_handled_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(
|
94
|
-
@current_context.visited_relation(
|
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
|
-
|
99
|
-
|
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
|
112
|
+
visit obj.source
|
102
113
|
end
|
103
|
-
visit
|
104
|
-
visit
|
105
|
-
visit
|
106
|
-
if defined?(
|
107
|
-
visit
|
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
|
120
|
+
visit obj.havings
|
110
121
|
end
|
111
122
|
end
|
112
123
|
end
|
113
124
|
|
114
|
-
|
115
|
-
|
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
|
118
|
-
visit
|
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
|
-
|
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
|
-
|
163
|
+
# rubocop:enable Naming/AccessorMethodName
|
164
|
+
|
165
|
+
def nest_context(obj)
|
146
166
|
old_context = @current_context
|
147
|
-
@current_context = Context.new(
|
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(
|
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
|
164
|
-
|
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(
|
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
|
-
|
204
|
-
|
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(
|
208
|
-
collector <<
|
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
|
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
|
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
|
288
|
+
alias build_arel_orig build_arel
|
289
|
+
|
258
290
|
def build_arel(*args)
|
259
291
|
arel = build_arel_orig(*args)
|
260
292
|
|
261
|
-
|
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
|
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
|
-
|
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
|
316
|
+
raise 'UnknownContext'
|
287
317
|
end
|
288
318
|
end
|
289
319
|
|
290
|
-
|
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
|
-
|
298
|
-
|
299
|
-
|
300
|
-
|
301
|
-
|
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
|
-
|
328
|
+
node_list.select { |n| n.is_a? Arel::Nodes::Join }.each do |node_join|
|
329
|
+
next unless node_join.right
|
305
330
|
|
306
|
-
|
331
|
+
relation_right, relation_left = relations_from_node_join(node_join)
|
307
332
|
|
308
|
-
|
309
|
-
|
310
|
-
|
311
|
-
|
312
|
-
|
313
|
-
|
314
|
-
|
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
|
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?
|
332
|
-
|
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
|
-
|
337
|
-
|
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
|
-
|
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
|
-
|
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(
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
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(
|
20
|
-
if msg.
|
22
|
+
def call(_worker_class, msg, _queue, &block)
|
23
|
+
if msg.key?('multi_tenant')
|
21
24
|
tenant = begin
|
22
|
-
|
23
|
-
|
24
|
-
|
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
|
-
|
40
|
-
return [] unless
|
41
|
-
|
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
|
74
|
+
result || nil
|
49
75
|
end
|
50
76
|
end.compact
|
51
77
|
|
52
|
-
raw_push(payloads)
|
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,13 +1,3 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
|
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
|
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
|
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
|
-
|
14
|
+
end.to change { Account.count }.from(1).to(0)
|
13
15
|
end
|
14
16
|
|
15
|
-
it
|
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
|
-
|
23
|
+
end.to change { Account.count }.from(2).to(0)
|
22
24
|
end
|
23
25
|
end
|