activerecord-multi-tenant 1.2.0 → 2.0.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: fa2e595cc76e33a877613504121106791680118da0a0c4957ff1190e75e99c3a
4
- data.tar.gz: 8a232e3431462d5b8b3972f0825664c210cc51729b2e94d08c5529851365d7f8
3
+ metadata.gz: 457cfac4996b93bb7b0b25cd99c2687f2742a8bc580888008baa4f903255cd74
4
+ data.tar.gz: 8c369e04a906752deda52d9b7b78a2377551e4b85f74e3b6d4b0742d30147783
5
5
  SHA512:
6
- metadata.gz: a2df2315ca041de29d2e82786a33904a9ac1dedd3f1ee4a2e470822cff977e9f4548ab0d33bd6eefec8055b9fc74ff8db2fdcf4c93cba4992aaaa681dcd1a695
7
- data.tar.gz: 9da5eec93a1bcd33bff29ab1e0035022592802f512d27139608e92988a17365d3fe4cff0a73fc0f83e9513cea9dd2bf6233bb4deac9533320fb0884f7ef8b219
6
+ metadata.gz: 7715dadcbd9cd13d86135518d3fc62283d9ab85e802016ab1be15fe39d815ec344c1d3e4699f96870b6e50b6d3b66b4a38f780d07ad6d3d4b8e21668deacca18
7
+ data.tar.gz: d1d364ebde87012cc5b91a6cf812a1a8cc0f89bb5ee8859b3de01ab0392dc5a62b74a3f93ac721d3e583343b703fc872d73c1f301cff9716e686492f06922d36
@@ -19,10 +19,12 @@ jobs:
19
19
  - '3.0'
20
20
  - '3.1'
21
21
  gemfile:
22
+ - rails_5.2.3
22
23
  - rails_5.2
23
24
  - rails_6.0
24
25
  - rails_6.1
25
26
  - rails_7.0
27
+ - active_record_5.2.3
26
28
  - active_record_5.2
27
29
  - active_record_6.0
28
30
  - active_record_6.1
@@ -47,6 +49,14 @@ jobs:
47
49
  gemfile: 'rails_5.2'
48
50
  - ruby: '3.1'
49
51
  gemfile: 'active_record_5.2'
52
+ - ruby: '3.0'
53
+ gemfile: 'rails_5.2.3'
54
+ - ruby: '3.0'
55
+ gemfile: 'active_record_5.2.3'
56
+ - ruby: '3.1'
57
+ gemfile: 'rails_5.2.3'
58
+ - ruby: '3.1'
59
+ gemfile: 'active_record_5.2.3'
50
60
  name: Ruby ${{ matrix.ruby }} / ${{ matrix.gemfile }} ${{ (matrix.prepared_statements && 'w/ prepared statements') || '' }}
51
61
  env:
52
62
  BUNDLE_GEMFILE: gemfiles/${{ matrix.gemfile }}.gemfile
data/CHANGELOG.md CHANGED
@@ -1,5 +1,14 @@
1
1
  # Changelog
2
2
 
3
+ ## 2.0.0 2022-05-19
4
+
5
+ * Replace RequestStore with CurrentAttributes [#139](https://github.com/citusdata/activerecord-multi-tenant/pull/139)
6
+ * Support changing table_name after calling multi_tenant [#128](https://github.com/citusdata/activerecord-multi-tenant/pull/128)
7
+ * Allow to use uuid as primary key on partition table [#112](https://github.com/citusdata/activerecord-multi-tenant/pull/112)
8
+ * Support latest Rails 5.2 [#145](https://github.com/citusdata/activerecord-multi-tenant/pull/145)
9
+ * Support optional: true for belongs_to [#147](https://github.com/citusdata/activerecord-multi-tenant/pull/147)
10
+
11
+
3
12
  ## 1.2.0 2022-03-29
4
13
 
5
14
  * Test Rails 7 & Ruby 3
@@ -15,8 +15,7 @@ Gem::Specification.new do |s|
15
15
  s.homepage = 'https://github.com/citusdata/activerecord-multi-tenant'
16
16
  s.license = 'MIT'
17
17
 
18
- s.add_runtime_dependency('request_store', '>= 1.0.5')
19
- s.add_dependency('rails','>= 4.2')
18
+ s.add_dependency 'rails', '>= 5.2'
20
19
 
21
20
  s.add_development_dependency 'rspec', '>= 3.0'
22
21
  s.add_development_dependency 'rspec-rails'
@@ -0,0 +1,16 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "appraisal"
6
+ gem "activerecord", "~> 5.2.0", "< 5.2.4" # FIXME
7
+ gem "i18n", "~> 0.9.5"
8
+ gem "nokogiri", "~> 1.7.1"
9
+ gem "nio4r", "~> 2.3.1"
10
+ gem "sprockets", "~> 3.7.1"
11
+ gem "byebug", "~> 11.0"
12
+ gem "rake", "12.0.0"
13
+ gem "redis", "3.3.3"
14
+ gem "pry-byebug", "3.9.0"
15
+
16
+ gemspec path: "../"
@@ -3,7 +3,7 @@
3
3
  source "https://rubygems.org"
4
4
 
5
5
  gem "appraisal"
6
- gem "activerecord", "~> 5.2.0", "< 5.2.4" # FIXME
6
+ gem "activerecord", "~> 5.2.0"
7
7
  gem "i18n", "~> 0.9.5"
8
8
  gem "nokogiri", "~> 1.7.1"
9
9
  gem "nio4r", "~> 2.3.1"
@@ -0,0 +1,16 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "appraisal"
6
+ gem "rails", "~> 5.2.0", "< 5.2.4" # FIXME
7
+ gem "i18n", "~> 0.9.5"
8
+ gem "nokogiri", "~> 1.7.1"
9
+ gem "nio4r", "~> 2.3.1"
10
+ gem "sprockets", "~> 3.7.1"
11
+ gem "byebug", "~> 11.0"
12
+ gem "rake", "12.0.0"
13
+ gem "redis", "3.3.3"
14
+ gem "pry-byebug", "3.9.0"
15
+
16
+ gemspec path: "../"
@@ -3,7 +3,7 @@
3
3
  source "https://rubygems.org"
4
4
 
5
5
  gem "appraisal"
6
- gem "rails", "~> 5.2.0", "< 5.2.4" # FIXME
6
+ gem "rails", "~> 5.2.0"
7
7
  gem "i18n", "~> 0.9.5"
8
8
  gem "nokogiri", "~> 1.7.1"
9
9
  gem "nio4r", "~> 2.3.1"
@@ -6,7 +6,13 @@ module MultiTenant
6
6
  if to_s.underscore.to_sym == tenant_name
7
7
  unless MultiTenant.with_write_only_mode_enabled?
8
8
  # This is the tenant model itself. Workaround for https://github.com/citusdata/citus/issues/687
9
- before_create -> { self.id ||= self.class.connection.select_value("SELECT nextval('" + [self.class.table_name, self.class.primary_key, 'seq'].join('_') + "'::regclass)") }
9
+ before_create -> do
10
+ if self.class.columns_hash[self.class.primary_key].type == :uuid
11
+ self.id ||= SecureRandom.uuid
12
+ else
13
+ self.id ||= self.class.connection.select_value("SELECT nextval('#{self.class.table_name}_#{self.class.primary_key}_seq'::regclass)")
14
+ end
15
+ end
10
16
  end
11
17
  else
12
18
  class << self
@@ -38,18 +44,18 @@ module MultiTenant
38
44
 
39
45
  def inherited(subclass)
40
46
  super
41
- MultiTenant.register_multi_tenant_model(subclass.table_name, subclass) if subclass.table_name
47
+ MultiTenant.register_multi_tenant_model(subclass)
42
48
  end
43
49
  end
44
50
 
45
- MultiTenant.register_multi_tenant_model(table_name, self) if table_name
51
+ MultiTenant.register_multi_tenant_model(self)
46
52
 
47
53
  @partition_key = options[:partition_key] || MultiTenant.partition_key(tenant_name)
48
54
  partition_key = @partition_key
49
55
 
50
56
  # Create an implicit belongs_to association only if tenant class exists
51
57
  if MultiTenant.tenant_klass_defined?(tenant_name)
52
- belongs_to tenant_name, **options.slice(:class_name, :inverse_of).merge(foreign_key: options[:partition_key])
58
+ belongs_to tenant_name, **options.slice(:class_name, :inverse_of, :optional).merge(foreign_key: options[:partition_key])
53
59
  end
54
60
 
55
61
  # New instances should have the tenant set
@@ -1,6 +1,10 @@
1
- require 'request_store'
1
+ require 'active_support/current_attributes'
2
2
 
3
3
  module MultiTenant
4
+ class Current < ::ActiveSupport::CurrentAttributes
5
+ attribute :tenant
6
+ end
7
+
4
8
  def self.tenant_klass_defined?(tenant_name)
5
9
  !!tenant_name.to_s.classify.safe_constantize
6
10
  end
@@ -24,13 +28,23 @@ module MultiTenant
24
28
  def self.with_lock_workaround_enabled?; @@enable_with_lock_workaround; end
25
29
 
26
30
  # Registry that maps table names to models (used by the query rewriter)
27
- def self.register_multi_tenant_model(table_name, model_klass)
28
- @@multi_tenant_models ||= {}
29
- @@multi_tenant_models[table_name.to_s] = model_klass
31
+ def self.register_multi_tenant_model(model_klass)
32
+ @@multi_tenant_models ||= []
33
+ @@multi_tenant_models.push(model_klass)
34
+
35
+ remove_class_variable(:@@multi_tenant_model_table_names) if defined?(@@multi_tenant_model_table_names)
30
36
  end
37
+
31
38
  def self.multi_tenant_model_for_table(table_name)
32
- @@multi_tenant_models ||= {}
33
- @@multi_tenant_models[table_name.to_s]
39
+ @@multi_tenant_models ||= []
40
+
41
+ if !defined?(@@multi_tenant_model_table_names)
42
+ @@multi_tenant_model_table_names = @@multi_tenant_models.map { |model|
43
+ [model.table_name, model] if model.table_name
44
+ }.compact.to_h
45
+ end
46
+
47
+ @@multi_tenant_model_table_names[table_name.to_s]
34
48
  end
35
49
 
36
50
  def self.multi_tenant_model_for_arel(arel)
@@ -43,11 +57,11 @@ module MultiTenant
43
57
  end
44
58
 
45
59
  def self.current_tenant=(tenant)
46
- RequestStore.store[:current_tenant] = tenant
60
+ Current.tenant = tenant
47
61
  end
48
62
 
49
63
  def self.current_tenant
50
- RequestStore.store[:current_tenant]
64
+ Current.tenant
51
65
  end
52
66
 
53
67
  def self.current_tenant_id
@@ -295,12 +295,9 @@ module ActiveRecord
295
295
  end
296
296
 
297
297
  node_list.select{ |n| n.is_a? Arel::Nodes::Join }.each do |node_join|
298
- if (!node_join.right ||
299
- (ActiveRecord::VERSION::MAJOR == 5 &&
300
- !node_join.right.expr.right.is_a?(Arel::Attributes::Attribute)))
298
+ if !node_join.right
301
299
  next
302
300
  end
303
-
304
301
  relation_right, relation_left = relations_from_node_join(node_join)
305
302
 
306
303
  next unless relation_right && relation_left
@@ -322,13 +319,13 @@ module ActiveRecord
322
319
 
323
320
  private
324
321
  def relations_from_node_join(node_join)
325
- if ActiveRecord::VERSION::MAJOR == 5 || node_join.right.expr.is_a?(Arel::Nodes::Equality)
322
+ if node_join.right.expr.is_a?(Arel::Nodes::Equality)
326
323
  return node_join.right.expr.right.relation, node_join.right.expr.left.relation
327
324
  end
328
325
 
329
- children = node_join.right.expr.children
326
+ children = [node_join.right.expr.children].flatten
330
327
 
331
- tenant_applied = children.any?(MultiTenant::TenantEnforcementClause) || children.any?(MultiTenant::TenantJoinEnforcementClause)
328
+ tenant_applied = children.any?{|c| c.is_a?(MultiTenant::TenantEnforcementClause) || c.is_a?(MultiTenant::TenantJoinEnforcementClause)}
332
329
  if tenant_applied || children.empty?
333
330
  return nil, nil
334
331
  end
@@ -1,3 +1,3 @@
1
1
  module MultiTenant
2
- VERSION = '1.2.0'
2
+ VERSION = '2.0.0'
3
3
  end
@@ -70,6 +70,47 @@ describe MultiTenant do
70
70
  it { expect(@partition_key_not_model_task.non_model_id).to be 77 }
71
71
  end
72
72
 
73
+ describe 'Changes table_name after multi_tenant called' do
74
+ before do
75
+ account_klass.has_many(:posts, anonymous_class: post_klass)
76
+ post_klass.belongs_to(:account, anonymous_class: account_klass)
77
+
78
+ @account1 = account_klass.create! name: 'foo'
79
+ @account2 = account_klass.create! name: 'bar'
80
+
81
+ @post1 = @account1.posts.create! name: 'foobar'
82
+ @post2 = @account2.posts.create! name: 'baz'
83
+
84
+ MultiTenant.current_tenant = @account1
85
+ @posts = post_klass.all
86
+ end
87
+
88
+ let(:account_klass) do
89
+ Class.new(Account) do
90
+ def self.name
91
+ 'Account'
92
+ end
93
+ end
94
+ end
95
+
96
+ let(:post_klass) do
97
+ Class.new(ActiveRecord::Base) do
98
+ self.table_name = 'unknown'
99
+
100
+ multi_tenant(:account)
101
+
102
+ self.table_name = 'posts'
103
+
104
+ def self.name
105
+ 'Post'
106
+ end
107
+ end
108
+ end
109
+
110
+ it { expect(@posts.length).to eq(1) }
111
+ it { expect(@posts).to eq([@post1]) }
112
+ end
113
+
73
114
  # Scoping models
74
115
  describe 'Project.all should be scoped to the current tenant if set' do
75
116
  before do
@@ -144,6 +185,16 @@ describe MultiTenant do
144
185
  end
145
186
  end
146
187
 
188
+ it 'handles belongs_to with optional: true' do
189
+ MultiTenant.with(account) do
190
+ sub_task
191
+ end
192
+
193
+ record = sub_task.optional_sub_tasks.create!
194
+ expect(record.reload.sub_task).to eq(sub_task)
195
+ expect(record.account_id).to eq(nil)
196
+ end
197
+
147
198
  it 'handles has_many through' do
148
199
  MultiTenant.with(account) do
149
200
  expect(project.sub_tasks).to eq [sub_task]
data/spec/schema.rb CHANGED
@@ -34,6 +34,13 @@ ARGV.grep(/\w+_spec\.rb/).empty? && ActiveRecord::Schema.define(version: 1) do
34
34
  t.column :type, :string
35
35
  end
36
36
 
37
+ create_table :optional_sub_tasks, force: true do |t|
38
+ t.references :account, :integer
39
+ t.column :sub_task_id, :integer
40
+ t.column :name, :string
41
+ t.column :type, :string
42
+ end
43
+
37
44
  create_table :countries, force: true do |t|
38
45
  t.column :name, :string
39
46
  end
@@ -106,6 +113,11 @@ ARGV.grep(/\w+_spec\.rb/).empty? && ActiveRecord::Schema.define(version: 1) do
106
113
  t.column :domain_id, :integer
107
114
  end
108
115
 
116
+ create_table :posts, force: true, partition_key: :account_id do |t|
117
+ t.column :account_id, :integer
118
+ t.column :name, :string
119
+ end
120
+
109
121
  create_distributed_table :accounts, :id
110
122
  create_distributed_table :projects, :account_id
111
123
  create_distributed_table :managers, :account_id
@@ -121,6 +133,7 @@ ARGV.grep(/\w+_spec\.rb/).empty? && ActiveRecord::Schema.define(version: 1) do
121
133
  create_distributed_table :allowed_places, :account_id
122
134
  create_distributed_table :domains, :account_id
123
135
  create_distributed_table :pages, :account_id
136
+ create_distributed_table :posts, :account_id
124
137
  create_reference_table :categories
125
138
  end
126
139
 
@@ -128,6 +141,7 @@ class Account < ActiveRecord::Base
128
141
  multi_tenant :account
129
142
  has_many :projects
130
143
  has_one :manager, inverse_of: :account
144
+ has_many :optional_sub_tasks
131
145
  end
132
146
 
133
147
  class Project < ActiveRecord::Base
@@ -159,6 +173,15 @@ class SubTask < ActiveRecord::Base
159
173
  multi_tenant :account
160
174
  belongs_to :task
161
175
  has_one :project, through: :task
176
+ has_many :optional_sub_tasks
177
+ end
178
+
179
+ with_belongs_to_required_by_default do
180
+ class OptionalSubTask < ActiveRecord::Base
181
+ multi_tenant :account, optional: true
182
+ belongs_to :account, optional: true
183
+ belongs_to :sub_task
184
+ end
162
185
  end
163
186
 
164
187
  class StiSubTask < SubTask
@@ -198,6 +221,7 @@ class Comment < ActiveRecord::Base
198
221
  end
199
222
 
200
223
  class Organization < ActiveRecord::Base
224
+ multi_tenant :organization
201
225
  has_many :uuid_records
202
226
  end
203
227
 
data/spec/spec_helper.rb CHANGED
@@ -46,4 +46,11 @@ def uses_prepared_statements?
46
46
  ActiveRecord::Base.connection.prepared_statements
47
47
  end
48
48
 
49
+ def with_belongs_to_required_by_default(&block)
50
+ default_value = ActiveRecord::Base.belongs_to_required_by_default
51
+ ActiveRecord::Base.belongs_to_required_by_default = true
52
+ yield
53
+ ensure
54
+ ActiveRecord::Base.belongs_to_required_by_default = default_value
55
+ end
49
56
  require 'schema'
metadata CHANGED
@@ -1,43 +1,29 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: activerecord-multi-tenant
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.2.0
4
+ version: 2.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Citus Data
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2022-04-11 00:00:00.000000000 Z
11
+ date: 2022-05-19 00:00:00.000000000 Z
12
12
  dependencies:
13
- - !ruby/object:Gem::Dependency
14
- name: request_store
15
- requirement: !ruby/object:Gem::Requirement
16
- requirements:
17
- - - ">="
18
- - !ruby/object:Gem::Version
19
- version: 1.0.5
20
- type: :runtime
21
- prerelease: false
22
- version_requirements: !ruby/object:Gem::Requirement
23
- requirements:
24
- - - ">="
25
- - !ruby/object:Gem::Version
26
- version: 1.0.5
27
13
  - !ruby/object:Gem::Dependency
28
14
  name: rails
29
15
  requirement: !ruby/object:Gem::Requirement
30
16
  requirements:
31
17
  - - ">="
32
18
  - !ruby/object:Gem::Version
33
- version: '4.2'
19
+ version: '5.2'
34
20
  type: :runtime
35
21
  prerelease: false
36
22
  version_requirements: !ruby/object:Gem::Requirement
37
23
  requirements:
38
24
  - - ">="
39
25
  - !ruby/object:Gem::Version
40
- version: '4.2'
26
+ version: '5.2'
41
27
  - !ruby/object:Gem::Dependency
42
28
  name: rspec
43
29
  requirement: !ruby/object:Gem::Requirement
@@ -168,10 +154,12 @@ files:
168
154
  - activerecord-multi-tenant.gemspec
169
155
  - docker-compose.yml
170
156
  - gemfiles/.bundle/config
157
+ - gemfiles/active_record_5.2.3.gemfile
171
158
  - gemfiles/active_record_5.2.gemfile
172
159
  - gemfiles/active_record_6.0.gemfile
173
160
  - gemfiles/active_record_6.1.gemfile
174
161
  - gemfiles/active_record_7.0.gemfile
162
+ - gemfiles/rails_5.2.3.gemfile
175
163
  - gemfiles/rails_5.2.gemfile
176
164
  - gemfiles/rails_6.0.gemfile
177
165
  - gemfiles/rails_6.1.gemfile