rails_multitenant 0.12.0 → 0.13.0.rc1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/LICENSE.txt +21 -0
- data/lib/rails_multitenant.rb +7 -4
- data/lib/rails_multitenant/global_context_registry.rb +22 -5
- data/lib/rails_multitenant/middleware/extensions.rb +3 -3
- data/lib/rails_multitenant/middleware/isolated_context_registry.rb +2 -0
- data/lib/rails_multitenant/middleware/railtie.rb +3 -1
- data/lib/rails_multitenant/multitenant_model.rb +3 -1
- data/lib/rails_multitenant/rspec.rb +2 -0
- data/lib/rails_multitenant/version.rb +3 -1
- metadata +20 -49
- data/.gitignore +0 -16
- data/.rspec +0 -3
- data/.ruby-version +0 -1
- data/.travis.yml +0 -16
- data/Appraisals +0 -28
- data/CHANGELOG.md +0 -61
- data/CODE_OF_CONDUCT.md +0 -13
- data/Gemfile +0 -4
- data/README.md +0 -120
- data/Rakefile +0 -8
- data/bin/console +0 -14
- data/bin/setup +0 -7
- data/gemfiles/rails_4.2.gemfile +0 -9
- data/gemfiles/rails_5.0.gemfile +0 -9
- data/gemfiles/rails_5.1.gemfile +0 -8
- data/gemfiles/rails_5.2.gemfile +0 -8
- data/gemfiles/rails_6.0.gemfile +0 -8
- data/rails_multitenant.gemspec +0 -32
- data/spec/be_multitenant_on_matcher_spec.rb +0 -15
- data/spec/current_spec.rb +0 -118
- data/spec/db/database.yml +0 -3
- data/spec/db/schema.rb +0 -60
- data/spec/external_item_spec.rb +0 -36
- data/spec/external_item_with_optional_org_spec.rb +0 -25
- data/spec/global_context_registry_spec.rb +0 -113
- data/spec/item_spec.rb +0 -78
- data/spec/item_subtype_spec.rb +0 -37
- data/spec/item_with_optional_org_spec.rb +0 -26
- data/spec/middleware_isolated_context_registry_spec.rb +0 -15
- data/spec/rails_multitenant_spec.rb +0 -51
- data/spec/spec_helper.rb +0 -54
data/Gemfile
DELETED
data/README.md
DELETED
@@ -1,120 +0,0 @@
|
|
1
|
-
# RailsMultitenant
|
2
|
-
|
3
|
-
rails_multitenant is a gem for isolating ActiveRecord models from different tenants. The gem assumes tables storing
|
4
|
-
multi-tenant models include an appropriate tenant id column.
|
5
|
-
|
6
|
-
## Installation
|
7
|
-
|
8
|
-
Add this line to your application's Gemfile:
|
9
|
-
|
10
|
-
```ruby
|
11
|
-
gem 'rails_multitenant'
|
12
|
-
```
|
13
|
-
|
14
|
-
And then execute:
|
15
|
-
|
16
|
-
$ bundle
|
17
|
-
|
18
|
-
Or install it yourself as:
|
19
|
-
|
20
|
-
$ gem install rails_multitenant
|
21
|
-
|
22
|
-
If you're using Rails, there's nothing else you need to do.
|
23
|
-
|
24
|
-
Otherwise, you need to insert `RailsMultitenant::Middleware::IsolatedContextRegistry` into your middleware stack
|
25
|
-
|
26
|
-
## Usage
|
27
|
-
|
28
|
-
The gem supports two multi-tenancy strategies:
|
29
|
-
|
30
|
-
1. Based on a model attribute, typically a foreign key to an entity owned by another service
|
31
|
-
2. Based on a model association
|
32
|
-
|
33
|
-
The gem uses ActiveRecord default scopes to make isolating tenants fairly transparent.
|
34
|
-
|
35
|
-
### Multi-tenancy Based on Model Attributes
|
36
|
-
|
37
|
-
The following model is multi-tenant based on an `organization_id` attribute:
|
38
|
-
|
39
|
-
```ruby
|
40
|
-
class Product < ActiveRecord::Base
|
41
|
-
include RailsMultitenant::MultitenantModel
|
42
|
-
|
43
|
-
multitenant_on :organization_id
|
44
|
-
end
|
45
|
-
```
|
46
|
-
|
47
|
-
The model can then be used as follows:
|
48
|
-
|
49
|
-
```ruby
|
50
|
-
RailsMultitenant::GlobalContextRegistry[:organization_id] = 'my-org'
|
51
|
-
|
52
|
-
# Only returns products from 'my-org'
|
53
|
-
Product.all
|
54
|
-
|
55
|
-
# Returns products across all orgs
|
56
|
-
Product.strip_organization_scope.all
|
57
|
-
|
58
|
-
# Or set the current organization in block form
|
59
|
-
RailsMultitenant::GlobalContextRegistry.with_isolated_registry(organization_id: 'my-org') do
|
60
|
-
# Only returns products from 'my-org'
|
61
|
-
Product.all
|
62
|
-
end
|
63
|
-
```
|
64
|
-
|
65
|
-
By default this adds an ActiveRecord validation to ensure the multi-tenant attribute is present but this can be disabled
|
66
|
-
by passing `required: false` to `multitenant_on`.
|
67
|
-
|
68
|
-
### Multi-tenancy Based on Associated Models
|
69
|
-
|
70
|
-
The following model is multi-tenant based on an `Organization` model:
|
71
|
-
|
72
|
-
```ruby
|
73
|
-
class Product < ActiveRecord::Base
|
74
|
-
include RailsMultitenant::MultitenantModel
|
75
|
-
|
76
|
-
multitenant_on_model :organization
|
77
|
-
end
|
78
|
-
```
|
79
|
-
|
80
|
-
The model can then be used as follows:
|
81
|
-
|
82
|
-
```ruby
|
83
|
-
Organization.current_id = 1
|
84
|
-
|
85
|
-
# Only returns products from organization 1
|
86
|
-
Product.all
|
87
|
-
|
88
|
-
# Use the automatically generated belongs_to association to get
|
89
|
-
# a product's organization
|
90
|
-
Product.first.organization
|
91
|
-
|
92
|
-
# Or set the current organization in block form
|
93
|
-
Organization.as_current_id(1) do
|
94
|
-
# Only returns products from organization 1
|
95
|
-
Product.all
|
96
|
-
end
|
97
|
-
```
|
98
|
-
|
99
|
-
By default this adds an ActiveRecord validation to ensure the tenant model is present but this can be disabled
|
100
|
-
by passing `required: false` to `multitenant_on_model`.
|
101
|
-
|
102
|
-
### Shorthand
|
103
|
-
|
104
|
-
When using `rails-multitenant` in a project, it is common to need to set values in `RailsMultitenant::GlobalContextRegistry` at the rails console.
|
105
|
-
|
106
|
-
This is difficult to type. Alternatively you can shorten it to `RailsMultitenant`. For example you might type `RailsMultitenant[:organization_id] = 'some value'` and it will have the same effect as the long version.
|
107
|
-
|
108
|
-
This is mainly intended as a console convenience. Using the long form in source code is fine, and more explicit.
|
109
|
-
|
110
|
-
|
111
|
-
## Development
|
112
|
-
|
113
|
-
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake false` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
114
|
-
|
115
|
-
To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
|
116
|
-
|
117
|
-
## Contributing
|
118
|
-
|
119
|
-
Bug reports and pull requests are welcome on GitHub at https://github.com/salsify/rails-multitenant. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](contributor-covenant.org) code of conduct.
|
120
|
-
|
data/Rakefile
DELETED
data/bin/console
DELETED
@@ -1,14 +0,0 @@
|
|
1
|
-
#!/usr/bin/env ruby
|
2
|
-
|
3
|
-
require "bundler/setup"
|
4
|
-
require "rails_multitenant"
|
5
|
-
|
6
|
-
# You can add fixtures and/or initialization code here to make experimenting
|
7
|
-
# with your gem easier. You can also use a different console, if you like.
|
8
|
-
|
9
|
-
# (If you use this, don't forget to add pry to your Gemfile!)
|
10
|
-
# require "pry"
|
11
|
-
# Pry.start
|
12
|
-
|
13
|
-
require "irb"
|
14
|
-
IRB.start
|
data/bin/setup
DELETED
data/gemfiles/rails_4.2.gemfile
DELETED
data/gemfiles/rails_5.0.gemfile
DELETED
data/gemfiles/rails_5.1.gemfile
DELETED
data/gemfiles/rails_5.2.gemfile
DELETED
data/gemfiles/rails_6.0.gemfile
DELETED
data/rails_multitenant.gemspec
DELETED
@@ -1,32 +0,0 @@
|
|
1
|
-
# coding: utf-8
|
2
|
-
lib = File.expand_path('../lib', __FILE__)
|
3
|
-
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
-
require 'rails_multitenant/version'
|
5
|
-
|
6
|
-
Gem::Specification.new do |spec|
|
7
|
-
spec.name = "rails_multitenant"
|
8
|
-
spec.version = RailsMultitenant::VERSION
|
9
|
-
spec.authors = ["Pat Breault"]
|
10
|
-
spec.email = ["pbreault@salsify.com"]
|
11
|
-
spec.summary = %q{Automatically configures multiple tenants in a Rails environment}
|
12
|
-
spec.description = %q{Handles multiple tenants in a Rails environment}
|
13
|
-
spec.homepage = "https://github.com/salsify/rails-multitenant"
|
14
|
-
spec.license = 'MIT'
|
15
|
-
|
16
|
-
spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
|
17
|
-
spec.test_files = Dir.glob('spec/**/*')
|
18
|
-
spec.require_paths = ["lib"]
|
19
|
-
|
20
|
-
spec.required_ruby_version = '>= 2.4.0'
|
21
|
-
|
22
|
-
spec.add_dependency 'activerecord', '>= 4.2', '< 6.1'
|
23
|
-
spec.add_dependency 'activesupport', '>= 4.2', '< 6.1'
|
24
|
-
|
25
|
-
spec.add_development_dependency 'appraisal'
|
26
|
-
spec.add_development_dependency 'coveralls'
|
27
|
-
spec.add_development_dependency 'database_cleaner', '>= 1.2'
|
28
|
-
spec.add_development_dependency 'rake', '>= 12.0'
|
29
|
-
spec.add_development_dependency 'rspec', '~> 3.8.0'
|
30
|
-
spec.add_development_dependency 'simplecov', '~> 0.15.1'
|
31
|
-
spec.add_development_dependency 'sqlite3', '~> 1.4.0'
|
32
|
-
end
|
@@ -1,15 +0,0 @@
|
|
1
|
-
require 'rails_multitenant/rspec'
|
2
|
-
|
3
|
-
describe "be_multitenant_on matcher" do
|
4
|
-
it "accepts a valid context field id" do
|
5
|
-
expect(ExternalItem).to be_multitenant_on(:external_organization_id)
|
6
|
-
end
|
7
|
-
|
8
|
-
it "rejects an invalid context field id" do
|
9
|
-
expect(ExternalItem).not_to be_multitenant_on(:other_field)
|
10
|
-
end
|
11
|
-
|
12
|
-
it "rejects classes that don't have a context field id" do
|
13
|
-
expect(String).not_to be_multitenant_on(:other_field)
|
14
|
-
end
|
15
|
-
end
|
data/spec/current_spec.rb
DELETED
@@ -1,118 +0,0 @@
|
|
1
|
-
describe RailsMultitenant::GlobalContextRegistry::Current do
|
2
|
-
|
3
|
-
class TestClass
|
4
|
-
include RailsMultitenant::GlobalContextRegistry::Current
|
5
|
-
provide_default :new
|
6
|
-
|
7
|
-
attr_accessor :id
|
8
|
-
|
9
|
-
def initialize(id: :default)
|
10
|
-
@id = id
|
11
|
-
end
|
12
|
-
end
|
13
|
-
|
14
|
-
class SubClass < TestClass
|
15
|
-
end
|
16
|
-
|
17
|
-
class DependentClass
|
18
|
-
include RailsMultitenant::GlobalContextRegistry::Current
|
19
|
-
provide_default { new }
|
20
|
-
global_context_dependent_on TestClass
|
21
|
-
end
|
22
|
-
|
23
|
-
class NoDefaultTestClass
|
24
|
-
include RailsMultitenant::GlobalContextRegistry::Current
|
25
|
-
end
|
26
|
-
|
27
|
-
describe 'current' do
|
28
|
-
it 'returns default value when supplied' do
|
29
|
-
expect(TestClass.current.id).to eq(:default)
|
30
|
-
expect(SubClass.current.id).to eq(:default)
|
31
|
-
end
|
32
|
-
|
33
|
-
it 'returns nil when no default supplied' do
|
34
|
-
expect(NoDefaultTestClass.current).to be_nil
|
35
|
-
end
|
36
|
-
end
|
37
|
-
|
38
|
-
describe 'current!' do
|
39
|
-
it 'returns current value when set' do
|
40
|
-
expect(TestClass.current.id).to eq(:default)
|
41
|
-
end
|
42
|
-
|
43
|
-
it 'raises an error when current not set' do
|
44
|
-
NoDefaultTestClass.clear_current!
|
45
|
-
expect { NoDefaultTestClass.current! }.to raise_error('No current NoDefaultTestClass set')
|
46
|
-
end
|
47
|
-
end
|
48
|
-
|
49
|
-
describe 'current=' do
|
50
|
-
it 'stores the provided object' do
|
51
|
-
provided = TestClass.new(id: :provided)
|
52
|
-
TestClass.current = provided
|
53
|
-
expect(TestClass.current).to equal(provided)
|
54
|
-
end
|
55
|
-
|
56
|
-
it 'clears dependencies' do
|
57
|
-
dependent = DependentClass.current
|
58
|
-
TestClass.current = TestClass.new
|
59
|
-
expect(DependentClass.current).not_to equal(dependent)
|
60
|
-
end
|
61
|
-
end
|
62
|
-
|
63
|
-
describe 'current?' do
|
64
|
-
it 'returns false when uninitialized' do
|
65
|
-
TestClass.clear_current!
|
66
|
-
expect(TestClass.current?).to be false
|
67
|
-
end
|
68
|
-
|
69
|
-
it 'returns true when initialized' do
|
70
|
-
TestClass.current = TestClass.new
|
71
|
-
expect(TestClass.current?).to be true
|
72
|
-
end
|
73
|
-
end
|
74
|
-
|
75
|
-
describe 'as_current' do
|
76
|
-
let(:test_class1) { TestClass.new }
|
77
|
-
let(:test_class2) { TestClass.new }
|
78
|
-
|
79
|
-
it 'sets and restores current' do
|
80
|
-
TestClass.current = test_class1
|
81
|
-
TestClass.as_current(test_class2) do
|
82
|
-
expect(TestClass.current).to equal(test_class2)
|
83
|
-
end
|
84
|
-
expect(TestClass.current).to equal(test_class1)
|
85
|
-
end
|
86
|
-
end
|
87
|
-
|
88
|
-
context 'instance methods' do
|
89
|
-
describe 'current?' do
|
90
|
-
it 'returns false when not the current instance' do
|
91
|
-
TestClass.clear_current!
|
92
|
-
expect(TestClass.new.current?).to be false
|
93
|
-
|
94
|
-
TestClass.current = TestClass.new
|
95
|
-
expect(TestClass.new.current?).to be false
|
96
|
-
end
|
97
|
-
|
98
|
-
it 'returns true when it is the current instance' do
|
99
|
-
test_class = TestClass.new
|
100
|
-
TestClass.current = test_class
|
101
|
-
expect(test_class.current?).to be true
|
102
|
-
end
|
103
|
-
end
|
104
|
-
|
105
|
-
describe 'as_current' do
|
106
|
-
let(:test_class1) { TestClass.new }
|
107
|
-
let(:test_class2) { TestClass.new }
|
108
|
-
|
109
|
-
it 'sets and restores current' do
|
110
|
-
TestClass.current = test_class1
|
111
|
-
test_class2.as_current do
|
112
|
-
expect(TestClass.current).to equal(test_class2)
|
113
|
-
end
|
114
|
-
expect(TestClass.current).to equal(test_class1)
|
115
|
-
end
|
116
|
-
end
|
117
|
-
end
|
118
|
-
end
|
data/spec/db/database.yml
DELETED
data/spec/db/schema.rb
DELETED
@@ -1,60 +0,0 @@
|
|
1
|
-
# encoding: UTF-8
|
2
|
-
|
3
|
-
ActiveRecord::Schema.define(version: 0) do
|
4
|
-
|
5
|
-
create_table(:organizations, force: true)
|
6
|
-
|
7
|
-
create_table(:dependent_models, force: true)
|
8
|
-
|
9
|
-
create_table(:items, force: true) do |t|
|
10
|
-
t.integer :organization_id
|
11
|
-
t.string :type
|
12
|
-
end
|
13
|
-
|
14
|
-
create_table(:item_with_optional_orgs, force: true) do |t|
|
15
|
-
t.integer :organization_id
|
16
|
-
t.string :type
|
17
|
-
end
|
18
|
-
|
19
|
-
create_table(:external_items, force: true) do |t|
|
20
|
-
t.integer :external_organization_id
|
21
|
-
end
|
22
|
-
|
23
|
-
create_table(:external_item_with_optional_orgs, force: true) do |t|
|
24
|
-
t.integer :external_organization_id
|
25
|
-
end
|
26
|
-
end
|
27
|
-
|
28
|
-
class Organization < ActiveRecord::Base
|
29
|
-
include RailsMultitenant::GlobalContextRegistry::CurrentInstance
|
30
|
-
end
|
31
|
-
|
32
|
-
class DependentModel < ActiveRecord::Base
|
33
|
-
include RailsMultitenant::GlobalContextRegistry::CurrentInstance
|
34
|
-
global_context_dependent_on Organization
|
35
|
-
end
|
36
|
-
|
37
|
-
class Item < ActiveRecord::Base
|
38
|
-
include RailsMultitenant::MultitenantModel
|
39
|
-
multitenant_on_model :organization
|
40
|
-
end
|
41
|
-
|
42
|
-
class ItemWithOptionalOrg < ActiveRecord::Base
|
43
|
-
include RailsMultitenant::MultitenantModel
|
44
|
-
multitenant_on_model :organization, required: false
|
45
|
-
end
|
46
|
-
|
47
|
-
class ItemSubtype < Item
|
48
|
-
|
49
|
-
end
|
50
|
-
|
51
|
-
class ExternalItem < ActiveRecord::Base
|
52
|
-
include RailsMultitenant::MultitenantModel
|
53
|
-
multitenant_on :external_organization_id
|
54
|
-
end
|
55
|
-
|
56
|
-
class ExternalItemWithOptionalOrg < ActiveRecord::Base
|
57
|
-
include RailsMultitenant::MultitenantModel
|
58
|
-
multitenant_on :external_organization_id, required: false
|
59
|
-
end
|
60
|
-
|
data/spec/external_item_spec.rb
DELETED
@@ -1,36 +0,0 @@
|
|
1
|
-
include RailsMultitenant
|
2
|
-
|
3
|
-
describe ExternalItem do
|
4
|
-
|
5
|
-
let!(:external_item1) { as_external_org(1) { ExternalItem.create! } }
|
6
|
-
|
7
|
-
let!(:external_item2) { as_external_org(2) { ExternalItem.create! } }
|
8
|
-
let!(:external_item3) { as_external_org(2) { ExternalItem.create! } }
|
9
|
-
|
10
|
-
specify 'org1 has the correct external items' do
|
11
|
-
as_external_org(1) do
|
12
|
-
expect(ExternalItem.all).to eq [external_item1]
|
13
|
-
end
|
14
|
-
end
|
15
|
-
|
16
|
-
specify 'org2 has the correct external items' do
|
17
|
-
as_external_org(2) do
|
18
|
-
expect(ExternalItem.all).to match_array [external_item2, external_item3]
|
19
|
-
end
|
20
|
-
end
|
21
|
-
|
22
|
-
it 'does not return external items from other orgs' do
|
23
|
-
as_external_org(2) do
|
24
|
-
expect(ExternalItem.where(id: external_item1.id)).to eq []
|
25
|
-
end
|
26
|
-
end
|
27
|
-
|
28
|
-
it 'allows the organization scope to be removed' do
|
29
|
-
expect(ExternalItem.strip_external_organization_scope.count).to eq 3
|
30
|
-
end
|
31
|
-
|
32
|
-
def as_external_org(id, &block)
|
33
|
-
GlobalContextRegistry.with_isolated_registry(external_organization_id: id, &block)
|
34
|
-
end
|
35
|
-
|
36
|
-
end
|