activerecord-multi-tenant 2.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 +8 -0
- data/CHANGELOG.md +13 -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 +80 -39
- data/lib/activerecord-multi-tenant/multi_tenant.rb +49 -25
- data/lib/activerecord-multi-tenant/query_monitor.rb +21 -5
- data/lib/activerecord-multi-tenant/query_rewriter.rb +118 -85
- data/lib/activerecord-multi-tenant/sidekiq.rb +31 -20
- 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 -12
- data/lib/activerecord_multi_tenant.rb +13 -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 +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 +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 +131 -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 = 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,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
|
|
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
|
-
|
|
322
|
+
node_list = if node.is_a? Arel::Nodes::Join
|
|
323
|
+
[node]
|
|
324
|
+
else
|
|
325
|
+
node.source.right
|
|
326
|
+
end
|
|
302
327
|
|
|
303
|
-
|
|
328
|
+
node_list.select { |n| n.is_a? Arel::Nodes::Join }.each do |node_join|
|
|
329
|
+
next unless node_join.right
|
|
304
330
|
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
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?
|
|
329
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,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)
|
|
@@ -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,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,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
|
|
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
|