activerecord-multi-tenant 0.2.1 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,3 +1,3 @@
1
1
  module MultiTenant
2
- VERSION = '0.2.1'
2
+ VERSION = '0.3.0'
3
3
  end
@@ -0,0 +1,30 @@
1
+ require 'spec_helper'
2
+
3
+ class Account
4
+ attr_accessor :name
5
+ end
6
+
7
+ class ApplicationController < ActionController::Base
8
+ include Rails.application.routes.url_helpers
9
+ set_current_tenant_through_filter
10
+ before_action :your_method_that_finds_the_current_tenant
11
+
12
+ def your_method_that_finds_the_current_tenant
13
+ current_account = Account.new
14
+ current_account.name = 'account1'
15
+ set_current_tenant(current_account)
16
+ end
17
+ end
18
+
19
+ describe ApplicationController, type: :controller do
20
+ controller do
21
+ def index
22
+ render body: 'custom called'
23
+ end
24
+ end
25
+
26
+ it 'Finds the correct tenant using the filter command' do
27
+ get :index
28
+ expect(MultiTenant.current_tenant.name).to eq 'account1'
29
+ end
30
+ end
@@ -0,0 +1,153 @@
1
+ require 'spec_helper'
2
+
3
+ describe MultiTenant do
4
+ after { MultiTenant.current_tenant = nil }
5
+
6
+ # Setting and getting
7
+ describe 'Setting the current tenant' do
8
+ before { MultiTenant.current_tenant = :foo }
9
+ it { MultiTenant.current_tenant == :foo }
10
+ end
11
+
12
+ describe 'is_scoped_as_tenant should return the correct value when true' do
13
+ it {expect(Project.respond_to?(:scoped_by_tenant?)).to eq(true)}
14
+ end
15
+
16
+ describe 'is_scoped_as_tenant should return the correct value when false' do
17
+ it {expect(UnscopedModel.respond_to?(:scoped_by_tenant?)).to eq(false)}
18
+ end
19
+
20
+ context 'immutability' do
21
+ before do
22
+ @account = Account.create! name: 'foo'
23
+ @project = @account.projects.create! name: 'bar'
24
+ end
25
+
26
+ describe 'tenant_id should be immutable, if already set' do
27
+ it { expect { @project.account_id = @account.id + 1 }.to raise_error(MultiTenant::TenantIsImmutable) }
28
+ end
29
+
30
+ describe 'setting tenant_id to the same value should not error' do
31
+ it { expect { @project.account_id = @account.id }.not_to raise_error }
32
+ end
33
+
34
+ describe 'setting tenant_id to a string with same to_i value should not error' do
35
+ it { expect { @project.account_id = @account.id.to_s }.not_to raise_error }
36
+ end
37
+ end
38
+
39
+ describe 'tenant_id should auto populate after initialization' do
40
+ before do
41
+ @account = Account.create! name: 'foo'
42
+ MultiTenant.current_tenant = @account
43
+ end
44
+ it {expect(Project.new.account_id).to eq(@account.id)}
45
+ end
46
+
47
+ describe 'Handles custom partition_key on tenant model' do
48
+ before do
49
+ @account = Account.create! name: 'foo'
50
+ MultiTenant.current_tenant = @account
51
+ @custom_partition_key_task = CustomPartitionKeyTask.create! name: 'foo'
52
+ end
53
+
54
+ it { expect(@custom_partition_key_task.account).to eq(@account) }
55
+ end
56
+
57
+ # Scoping models
58
+ describe 'Project.all should be scoped to the current tenant if set' do
59
+ before do
60
+ @account1 = Account.create! name: 'foo'
61
+ @account2 = Account.create! name: 'bar'
62
+
63
+ @project1 = @account1.projects.create! name: 'foobar'
64
+ @project2 = @account2.projects.create! name: 'baz'
65
+
66
+ MultiTenant.current_tenant = @account1
67
+ @projects = Project.all
68
+ end
69
+
70
+ it { expect(@projects.length).to eq(1) }
71
+ it { expect(@projects).to eq([@project1]) }
72
+ end
73
+
74
+ describe 'Querying the tenant from a scoped model with a tenant set' do
75
+ before do
76
+ @account = Account.create! name: 'foo'
77
+ @project = @account.projects.create! name: 'foobar'
78
+ MultiTenant.current_tenant= @account1
79
+ end
80
+
81
+ it { @project.account }
82
+ end
83
+
84
+ # Associations
85
+ describe 'Associations should be correctly scoped by current tenant' do
86
+ before do
87
+ @account = Account.create! name: 'foo'
88
+ @project = Project.create! name: 'foobar', account: @account
89
+
90
+ MultiTenant.current_tenant = @account
91
+ @task = @project.tasks.create! name: 'baz'
92
+ end
93
+
94
+ it 'should correctly set the tenant on the task created with current_tenant set' do
95
+ expect(@task.account).to eq(@account)
96
+ end
97
+ end
98
+
99
+ describe "It should be possible to use aliased associations" do
100
+ before do
101
+ @account = Account.create! name: 'baz'
102
+ MultiTenant.current_tenant = @account
103
+ end
104
+
105
+ it { expect(AliasedTask.create(:name => 'foo', :project_alias => @project2).valid?).to eq(true) }
106
+ end
107
+
108
+ describe "It should be possible to use associations with partition_key from polymorphic" do
109
+ before do
110
+ @account = Account.create!(name: 'foo')
111
+ MultiTenant.current_tenant = @account
112
+ @project = Project.create!(name: 'project', account: @account)
113
+ @comment = Comment.new commentable: @project, account: @account
114
+ end
115
+
116
+ it { expect(@comment.save!).to eq(true) }
117
+ end
118
+
119
+ # ::with
120
+ describe "::with" do
121
+ it "should set current_tenant to the specified tenant inside the block" do
122
+ @account = Account.create!(:name => 'baz')
123
+
124
+ MultiTenant.with(@account) do
125
+ expect(MultiTenant.current_tenant).to eq(@account)
126
+ end
127
+ end
128
+
129
+ it "should reset current_tenant to the previous tenant once exiting the block" do
130
+ @account1 = Account.create!(:name => 'foo')
131
+ @account2 = Account.create!(:name => 'bar')
132
+
133
+ MultiTenant.current_tenant = @account1
134
+ MultiTenant.with @account2 do
135
+
136
+ end
137
+
138
+ expect(MultiTenant.current_tenant).to eq(@account1)
139
+ end
140
+
141
+ it "should return the value of the block" do
142
+ @account1 = Account.create!(:name => 'foo')
143
+ @account2 = Account.create!(:name => 'bar')
144
+
145
+ MultiTenant.current_tenant = @account1
146
+ value = MultiTenant.with @account2 do
147
+ "something"
148
+ end
149
+
150
+ expect(value).to eq "something"
151
+ end
152
+ end
153
+ end
@@ -0,0 +1,22 @@
1
+ require 'spec_helper'
2
+
3
+ class ProjectWithCallbacks < ActiveRecord::Base
4
+ self.table_name = :projects
5
+
6
+ multi_tenant :account
7
+
8
+ after_update do |record|
9
+ # Ensure that we don't have TenantIdWrapper here
10
+ record.account.update! name: 'callback'
11
+ end
12
+ end
13
+
14
+ describe MultiTenant, 'Callbacks' do
15
+ let(:account) { Account.create! name: 'test' }
16
+ let(:project) { ProjectWithCallbacks.create! account: account, name: 'something' }
17
+
18
+ it 'takes callbacks into account' do
19
+ project.update! name: 'something else'
20
+ expect(account.reload.name).to eq 'callback'
21
+ end
22
+ end
@@ -0,0 +1,11 @@
1
+ require 'spec_helper'
2
+
3
+ describe MultiTenant, 'Record finding' do
4
+ it 'searches for tenant object using the scope' do
5
+ account = Account.create! name: 'test'
6
+ project = account.projects.create! name: 'something'
7
+ MultiTenant.with(account) do
8
+ expect(Project.find(project.id)).to be_present
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,20 @@
1
+ require 'spec_helper'
2
+
3
+ describe MultiTenant, 'Record modifications' do
4
+ let(:account) { Account.create! name: 'test' }
5
+ let(:project) { account.projects.create! name: 'something' }
6
+
7
+ it 'includes the tenant_id in UPDATEs' do
8
+ project.update! name: 'something else'
9
+ MultiTenant.with(account) do
10
+ expect(Project.find(project.id).name).to eq 'something else'
11
+ end
12
+ end
13
+
14
+ it 'includes the tenant_id in DELETEs' do
15
+ project.destroy!
16
+ MultiTenant.with(account) do
17
+ expect(Project.find_by(id: project.id)).not_to be_present
18
+ end
19
+ end
20
+ end
data/spec/database.yml ADDED
@@ -0,0 +1,9 @@
1
+ test:
2
+ adapter: postgresql
3
+ username: postgres
4
+ database: postgres
5
+ host: localhost
6
+ port: 5600
7
+ pool: 5
8
+ timeout: 5000
9
+ prepared_statements: false
data/spec/schema.rb ADDED
@@ -0,0 +1,104 @@
1
+ false && ActiveRecord::Schema.define(version: 1) do
2
+ create_table :accounts, force: true do |t|
3
+ t.column :name, :string
4
+ t.column :subdomain, :string
5
+ t.column :domain, :string
6
+ end
7
+
8
+ create_table :projects, force: true, partition_key: :account_id do |t|
9
+ t.column :name, :string
10
+ t.column :account_id, :integer
11
+ end
12
+
13
+ create_table :managers, force: true, partition_key: :account_id do |t|
14
+ t.column :name, :string
15
+ t.column :project_id, :integer
16
+ t.column :account_id, :integer
17
+ end
18
+
19
+ create_table :tasks, force: true, partition_key: :account_id do |t|
20
+ t.column :name, :string
21
+ t.column :account_id, :integer
22
+ t.column :project_id, :integer
23
+ t.column :completed, :boolean
24
+ end
25
+
26
+ create_table :countries, force: true do |t|
27
+ t.column :name, :string
28
+ end
29
+
30
+ create_table :unscoped_models, force: true do |t|
31
+ t.column :name, :string
32
+ end
33
+
34
+ create_table :aliased_tasks, force: true, partition_key: :account_id do |t|
35
+ t.column :name, :string
36
+ t.column :project_alias_id, :integer
37
+ t.column :account_id, :integer
38
+ end
39
+
40
+ create_table :custom_partition_key_tasks, force: true, partition_key: :accountID do |t|
41
+ t.column :name, :string
42
+ t.column :accountID, :integer
43
+ end
44
+
45
+ create_table :comments, force: true, partition_key: :account_id do |t|
46
+ t.column :commentable_id, :integer
47
+ t.column :commentable_type, :string
48
+ t.column :account_id, :integer
49
+ end
50
+
51
+ create_distributed_table :accounts, :id
52
+ create_distributed_table :projects, :account_id
53
+ create_distributed_table :managers, :account_id
54
+ create_distributed_table :tasks, :account_id
55
+ create_distributed_table :aliased_tasks, :account_id
56
+ create_distributed_table :custom_partition_key_tasks, :accountID
57
+ create_distributed_table :comments, :account_id
58
+ end
59
+
60
+ class Account < ActiveRecord::Base
61
+ multi_tenant :account
62
+ has_many :projects
63
+ end
64
+
65
+ class Project < ActiveRecord::Base
66
+ multi_tenant :account
67
+ has_one :manager
68
+ has_many :tasks
69
+
70
+ validates_uniqueness_of :name, scope: [:account]
71
+ end
72
+
73
+ class Manager < ActiveRecord::Base
74
+ multi_tenant :account
75
+ belongs_to :project
76
+ end
77
+
78
+ class Task < ActiveRecord::Base
79
+ multi_tenant :account
80
+ belongs_to :project
81
+ default_scope -> { where(completed: nil).order('name') }
82
+
83
+ validates_uniqueness_of :name
84
+ end
85
+
86
+ class UnscopedModel < ActiveRecord::Base
87
+ validates_uniqueness_of :name
88
+ end
89
+
90
+ class AliasedTask < ActiveRecord::Base
91
+ multi_tenant :account
92
+ belongs_to :project_alias, class_name: 'Project'
93
+ end
94
+
95
+ class CustomPartitionKeyTask < ActiveRecord::Base
96
+ multi_tenant :account, partition_key: 'accountID'
97
+ validates_uniqueness_of :name, scope: [:account]
98
+ end
99
+
100
+ class Comment < ActiveRecord::Base
101
+ multi_tenant :account
102
+ belongs_to :commentable, polymorphic: true
103
+ belongs_to :task, -> { where(comments: { commentable_type: 'Task' }) }, foreign_key: 'commentable_id'
104
+ end
@@ -0,0 +1,43 @@
1
+ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
2
+ $LOAD_PATH.unshift(File.dirname(__FILE__))
3
+
4
+ require 'active_record/railtie'
5
+ require 'action_controller/railtie'
6
+ require 'rspec/rails'
7
+ require 'database_cleaner'
8
+
9
+ require 'activerecord-multi-tenant'
10
+
11
+ dbconfig = YAML::load(IO.read(File.join(File.dirname(__FILE__), 'database.yml')))
12
+ ActiveRecord::Base.logger = Logger.new(File.join(File.dirname(__FILE__), "debug.log"))
13
+ ActiveRecord::Base.establish_connection(dbconfig['test'])
14
+
15
+ RSpec.configure do |config|
16
+ config.after(:each) do
17
+ MultiTenant.current_tenant = nil
18
+ end
19
+
20
+ config.before(:suite) do
21
+ DatabaseCleaner[:active_record].strategy = :transaction
22
+ DatabaseCleaner[:active_record].clean_with(:truncation)
23
+ end
24
+
25
+ config.before(:each) do
26
+ DatabaseCleaner[:active_record].start
27
+ end
28
+
29
+ config.after(:each) do
30
+ DatabaseCleaner[:active_record].clean
31
+ end
32
+
33
+ config.infer_base_class_for_anonymous_controllers = true
34
+ end
35
+
36
+ module MultiTenantTest
37
+ class Application < Rails::Application; end
38
+ end
39
+
40
+ MultiTenantTest::Application.config.secret_token = 'x' * 40
41
+ MultiTenantTest::Application.config.secret_key_base = 'y' * 40
42
+
43
+ require 'schema'
metadata CHANGED
@@ -1,29 +1,43 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: activerecord-multi-tenant
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.1
4
+ version: 0.3.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: 2016-12-29 00:00:00.000000000 Z
11
+ date: 2016-12-31 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
- name: acts_as_tenant
14
+ name: request_store
15
15
  requirement: !ruby/object:Gem::Requirement
16
16
  requirements:
17
17
  - - ">="
18
18
  - !ruby/object:Gem::Version
19
- version: '0'
19
+ version: 1.0.5
20
20
  type: :runtime
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
24
  - - ">="
25
25
  - !ruby/object:Gem::Version
26
- version: '0'
26
+ version: 1.0.5
27
+ - !ruby/object:Gem::Dependency
28
+ name: rails
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '3.1'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '3.1'
27
41
  - !ruby/object:Gem::Dependency
28
42
  name: rspec
29
43
  requirement: !ruby/object:Gem::Requirement
@@ -86,13 +100,38 @@ executables: []
86
100
  extensions: []
87
101
  extra_rdoc_files: []
88
102
  files:
103
+ - ".gitignore"
104
+ - Appraisals
105
+ - CHANGELOG.md
106
+ - Gemfile
107
+ - Gemfile.lock
108
+ - README.md
109
+ - Rakefile
110
+ - activerecord-multi-tenant.gemspec
111
+ - docker-compose.yml
112
+ - gemfiles/rails_3.2.gemfile
113
+ - gemfiles/rails_3.2.gemfile.lock
114
+ - gemfiles/rails_4.2.gemfile
115
+ - gemfiles/rails_4.2.gemfile.lock
116
+ - gemfiles/rails_5.0.gemfile
117
+ - gemfiles/rails_5.0.gemfile.lock
89
118
  - lib/activerecord-multi-tenant.rb
119
+ - lib/activerecord-multi-tenant/controller_extensions.rb
90
120
  - lib/activerecord-multi-tenant/copy_from_client.rb
91
121
  - lib/activerecord-multi-tenant/default_scope.rb
92
122
  - lib/activerecord-multi-tenant/migrations.rb
123
+ - lib/activerecord-multi-tenant/model_extensions.rb
93
124
  - lib/activerecord-multi-tenant/multi_tenant.rb
94
125
  - lib/activerecord-multi-tenant/referential_integrity.rb
95
126
  - lib/activerecord-multi-tenant/version.rb
127
+ - spec/activerecord-multi-tenant/controller_extensions_spec.rb
128
+ - spec/activerecord-multi-tenant/model_extensions_spec.rb
129
+ - spec/activerecord-multi-tenant/record_callback_spec.rb
130
+ - spec/activerecord-multi-tenant/record_finding_spec.rb
131
+ - spec/activerecord-multi-tenant/record_modifications_spec.rb
132
+ - spec/database.yml
133
+ - spec/schema.rb
134
+ - spec/spec_helper.rb
96
135
  homepage: https://github.com/citusdata/activerecord-multi-tenant
97
136
  licenses:
98
137
  - MIT