activerecord-multi-tenant 2.2.0 → 2.3.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 +80 -0
- data/.gitignore +6 -0
- data/.readthedocs.yaml +15 -0
- data/.rspec +0 -0
- data/.rubocop.yml +51 -0
- data/Appraisals +0 -0
- data/CHANGELOG.md +6 -0
- data/Gemfile +3 -1
- data/LICENSE +0 -0
- data/README.md +2 -1
- data/Rakefile +1 -1
- data/activerecord-multi-tenant.gemspec +28 -22
- 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 +18 -8
- data/lib/activerecord-multi-tenant/model_extensions.rb +78 -37
- data/lib/activerecord-multi-tenant/multi_tenant.rb +40 -21
- data/lib/activerecord-multi-tenant/query_monitor.rb +21 -5
- data/lib/activerecord-multi-tenant/query_rewriter.rb +111 -80
- data/lib/activerecord-multi-tenant/sidekiq.rb +31 -20
- data/lib/activerecord-multi-tenant/version.rb +1 -1
- data/lib/activerecord-multi-tenant.rb +3 -12
- data/lib/activerecord_multi_tenant.rb +12 -0
- data/spec/activerecord-multi-tenant/associations_spec.rb +21 -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 +233 -153
- data/spec/activerecord-multi-tenant/multi_tenant_spec.rb +15 -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 +4 -4
- data/spec/activerecord-multi-tenant/sidekiq_spec.rb +10 -10
- data/spec/database.yml +0 -0
- data/spec/schema.rb +20 -2
- data/spec/spec_helper.rb +46 -17
- data/spec/support/format_sql.rb +20 -0
- metadata +130 -25
- data/.github/workflows/CI.yml +0 -47
- data/gemfiles/.bundle/config +0 -2
- 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_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/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 = obj.left.relation.table_name
|
|
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?(obj.table_name)
|
|
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,31 @@ 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
182
|
@tenant_model = MultiTenant.multi_tenant_model_for_table(tenant_attribute.relation.table_name)
|
|
161
183
|
end
|
|
162
184
|
|
|
163
|
-
def to_s
|
|
164
|
-
|
|
185
|
+
def to_s
|
|
186
|
+
to_sql
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
def to_str
|
|
190
|
+
to_sql
|
|
191
|
+
end
|
|
165
192
|
|
|
166
193
|
def to_sql(*)
|
|
167
194
|
collector = Arel::Collectors::SQLString.new
|
|
168
195
|
collector = @tenant_model.connection.visitor.accept tenant_arel, collector
|
|
169
196
|
collector.value
|
|
170
197
|
end
|
|
171
|
-
|
|
172
|
-
|
|
173
198
|
end
|
|
174
199
|
|
|
175
200
|
class TenantEnforcementClause < BaseTenantEnforcementClause
|
|
176
201
|
private
|
|
202
|
+
|
|
177
203
|
def tenant_arel
|
|
178
204
|
if defined?(Arel::Nodes::Quoted)
|
|
179
205
|
@tenant_attribute.eq(Arel::Nodes::Quoted.new(MultiTenant.current_tenant_id))
|
|
@@ -183,9 +209,9 @@ module MultiTenant
|
|
|
183
209
|
end
|
|
184
210
|
end
|
|
185
211
|
|
|
186
|
-
|
|
187
212
|
class TenantJoinEnforcementClause < BaseTenantEnforcementClause
|
|
188
213
|
attr_reader :table_left
|
|
214
|
+
|
|
189
215
|
def initialize(tenant_attribute, table_left)
|
|
190
216
|
super(tenant_attribute)
|
|
191
217
|
@table_left = table_left
|
|
@@ -193,20 +219,23 @@ module MultiTenant
|
|
|
193
219
|
end
|
|
194
220
|
|
|
195
221
|
private
|
|
222
|
+
|
|
196
223
|
def tenant_arel
|
|
197
224
|
@tenant_attribute.eq(@table_left[@model_left.partition_key])
|
|
198
225
|
end
|
|
199
226
|
end
|
|
200
227
|
|
|
201
|
-
|
|
202
228
|
module TenantValueVisitor
|
|
203
|
-
|
|
204
|
-
|
|
229
|
+
# rubocop:disable Naming/MethodName
|
|
230
|
+
def visit_MultiTenant_TenantEnforcementClause(obj, collector)
|
|
231
|
+
collector << obj
|
|
205
232
|
end
|
|
206
233
|
|
|
207
|
-
def visit_MultiTenant_TenantJoinEnforcementClause(
|
|
208
|
-
collector <<
|
|
234
|
+
def visit_MultiTenant_TenantJoinEnforcementClause(obj, collector)
|
|
235
|
+
collector << obj
|
|
209
236
|
end
|
|
237
|
+
|
|
238
|
+
# rubocop:enable Naming/MethodName
|
|
210
239
|
end
|
|
211
240
|
|
|
212
241
|
module DatabaseStatements
|
|
@@ -254,11 +283,12 @@ Arel::Visitors::ToSql.include(MultiTenant::TenantValueVisitor)
|
|
|
254
283
|
require 'active_record/relation'
|
|
255
284
|
module ActiveRecord
|
|
256
285
|
module QueryMethods
|
|
257
|
-
alias
|
|
286
|
+
alias build_arel_orig build_arel
|
|
287
|
+
|
|
258
288
|
def build_arel(*args)
|
|
259
289
|
arel = build_arel_orig(*args)
|
|
260
290
|
|
|
261
|
-
|
|
291
|
+
unless MultiTenant.with_write_only_mode_enabled?
|
|
262
292
|
visitor = MultiTenant::ArelTenantVisitor.new(arel)
|
|
263
293
|
|
|
264
294
|
visitor.contexts.each do |context|
|
|
@@ -270,45 +300,44 @@ module ActiveRecord
|
|
|
270
300
|
if MultiTenant.current_tenant_id
|
|
271
301
|
enforcement_clause = MultiTenant::TenantEnforcementClause.new(relation.arel_table[model.partition_key])
|
|
272
302
|
case node
|
|
273
|
-
when Arel::Nodes::Join #Arel::Nodes::OuterJoin, Arel::Nodes::RightOuterJoin, Arel::Nodes::FullOuterJoin
|
|
303
|
+
when Arel::Nodes::Join # Arel::Nodes::OuterJoin, Arel::Nodes::RightOuterJoin, Arel::Nodes::FullOuterJoin
|
|
274
304
|
node.right.expr = node.right.expr.and(enforcement_clause)
|
|
275
305
|
when Arel::Nodes::SelectCore
|
|
276
306
|
if node.wheres.empty?
|
|
277
307
|
node.wheres = [enforcement_clause]
|
|
308
|
+
elsif node.wheres[0].is_a?(Arel::Nodes::And)
|
|
309
|
+
node.wheres[0].children << enforcement_clause
|
|
278
310
|
else
|
|
279
|
-
|
|
280
|
-
node.wheres[0].children << enforcement_clause
|
|
281
|
-
else
|
|
282
|
-
node.wheres[0] = enforcement_clause.and(node.wheres[0])
|
|
283
|
-
end
|
|
311
|
+
node.wheres[0] = enforcement_clause.and(node.wheres[0])
|
|
284
312
|
end
|
|
285
313
|
else
|
|
286
|
-
raise
|
|
314
|
+
raise 'UnknownContext'
|
|
287
315
|
end
|
|
288
316
|
end
|
|
289
317
|
|
|
290
|
-
|
|
291
|
-
if node.is_a?Arel::Nodes::Join
|
|
292
|
-
node_list = [node]
|
|
293
|
-
else
|
|
294
|
-
node_list = node.source.right
|
|
295
|
-
end
|
|
318
|
+
next unless node.is_a?(Arel::Nodes::SelectCore) || node.is_a?(Arel::Nodes::Join)
|
|
296
319
|
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
320
|
+
node_list = if node.is_a? Arel::Nodes::Join
|
|
321
|
+
[node]
|
|
322
|
+
else
|
|
323
|
+
node.source.right
|
|
324
|
+
end
|
|
302
325
|
|
|
303
|
-
|
|
326
|
+
node_list.select { |n| n.is_a? Arel::Nodes::Join }.each do |node_join|
|
|
327
|
+
next unless node_join.right
|
|
304
328
|
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
329
|
+
relation_right, relation_left = relations_from_node_join(node_join)
|
|
330
|
+
|
|
331
|
+
next unless relation_right && relation_left
|
|
332
|
+
|
|
333
|
+
model_right = MultiTenant.multi_tenant_model_for_table(relation_left.table_name)
|
|
334
|
+
model_left = MultiTenant.multi_tenant_model_for_table(relation_right.table_name)
|
|
335
|
+
next unless model_right && model_left
|
|
336
|
+
|
|
337
|
+
join_enforcement_clause = MultiTenant::TenantJoinEnforcementClause.new(
|
|
338
|
+
relation_right[model_right.partition_key], relation_left
|
|
339
|
+
)
|
|
340
|
+
node_join.right.expr = node_join.right.expr.and(join_enforcement_clause)
|
|
312
341
|
end
|
|
313
342
|
end
|
|
314
343
|
end
|
|
@@ -318,6 +347,7 @@ module ActiveRecord
|
|
|
318
347
|
end
|
|
319
348
|
|
|
320
349
|
private
|
|
350
|
+
|
|
321
351
|
def relations_from_node_join(node_join)
|
|
322
352
|
if node_join.right.expr.is_a?(Arel::Nodes::Equality)
|
|
323
353
|
return node_join.right.expr.right.relation, node_join.right.expr.left.relation
|
|
@@ -325,22 +355,21 @@ module ActiveRecord
|
|
|
325
355
|
|
|
326
356
|
children = [node_join.right.expr.children].flatten
|
|
327
357
|
|
|
328
|
-
tenant_applied = children.any?
|
|
329
|
-
|
|
330
|
-
return nil, nil
|
|
358
|
+
tenant_applied = children.any? do |c|
|
|
359
|
+
c.is_a?(MultiTenant::TenantEnforcementClause) || c.is_a?(MultiTenant::TenantJoinEnforcementClause)
|
|
331
360
|
end
|
|
361
|
+
return nil, nil if tenant_applied || children.empty?
|
|
332
362
|
|
|
333
363
|
child = children.first.respond_to?(:children) ? children.first.children.first : children.first
|
|
334
364
|
if child.right.respond_to?(:relation) && child.left.respond_to?(:relation)
|
|
335
365
|
return child.right.relation, child.left.relation
|
|
336
366
|
end
|
|
337
367
|
|
|
338
|
-
|
|
368
|
+
[nil, nil]
|
|
339
369
|
end
|
|
340
370
|
end
|
|
341
371
|
end
|
|
342
372
|
|
|
343
|
-
require 'active_record/base'
|
|
344
373
|
module MultiTenantFindBy
|
|
345
374
|
def cached_find_by_statement(key, &block)
|
|
346
375
|
return super unless respond_to?(:scoped_by_tenant?) && scoped_by_tenant?
|
|
@@ -350,4 +379,6 @@ module MultiTenantFindBy
|
|
|
350
379
|
end
|
|
351
380
|
end
|
|
352
381
|
|
|
353
|
-
|
|
382
|
+
ActiveSupport.on_load(:active_record) do |base|
|
|
383
|
+
base.singleton_class.prepend(MultiTenantFindBy)
|
|
384
|
+
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,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
|
-
|
|
56
|
-
return [] unless
|
|
57
|
-
|
|
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
|
|
74
|
+
result || nil
|
|
65
75
|
end
|
|
66
76
|
end.compact
|
|
67
77
|
|
|
68
|
-
raw_push(payloads)
|
|
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)
|
|
@@ -1,12 +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'
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'activerecord_multi_tenant'
|
|
@@ -0,0 +1,12 @@
|
|
|
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/version'
|
|
12
|
+
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
|
|
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
|