multitenant-pg 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,10 @@
1
+ *.gem
2
+ .bundle
3
+ Gemfile.lock
4
+ pkg/*
5
+ .DS_Store
6
+
7
+ # test files
8
+ spec/*.log
9
+ spec/*.sqlite3
10
+
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --color
2
+ --format documentation
3
+ --backtrace
data/.rvmrc ADDED
@@ -0,0 +1 @@
1
+ rvm use ree-1.8.7-2011.03@multitenant --create
@@ -0,0 +1,5 @@
1
+ before_script:
2
+ - "psql -c 'create database myapp_test;' -U postgres"
3
+ rvm:
4
+ - 1.8.7
5
+ - 1.9.2
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source "http://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in multitenant.gemspec
4
+ gemspec
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2011 Fabio Kuhn
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,57 @@
1
+ = Multitenant
2
+
3
+ Build a Rails multitenancy application with PostgreSQL schemas.
4
+
5
+ == Example usage
6
+
7
+ class Tenant < ActiveRecord::Base
8
+ set_table_name 'public.tenants'
9
+ validates_presence_of :schema_name
10
+ validates_uniqueness_of :schema_name
11
+
12
+ after_create :setup_schema
13
+
14
+ private
15
+ def setup_schema
16
+ unless Multitenant::SchemaUtils.schema_exists?(schema_name)
17
+ Multitenant::SchemaUtils.create_schema(schema_name)
18
+ Multitenant::SchemaUtils.load_schema_into_schema(schema_name)
19
+ end
20
+ end
21
+ end
22
+
23
+ Multitenant.with_tenant current_tenant do
24
+ # queries within this block are automatically
25
+ # scoped to the current tenant
26
+ User.all
27
+
28
+ # new objects created within this block are automatically
29
+ # assigned to the current tenant
30
+ User.create :name => 'Bob'
31
+ end
32
+
33
+ == Features
34
+
35
+ * Rails 3 compatible
36
+ * Restrict database queries to only lookup objects for the current tenant
37
+ * Utility tools for creating, migrating and accessing schemas
38
+
39
+ == Contributing
40
+
41
+ * Check out the latest master to make sure the feature hasn't been implemented or the bug hasn't been fixed yet
42
+ * Check out the issue tracker to make sure someone already hasn't requested it and/or contributed it
43
+ * Fork the project
44
+ * Start a feature/bugfix branch
45
+ * Commit and push until you are happy with your contribution
46
+ * Make sure to add tests for it. This is important so I don't break it in a future version unintentionally.
47
+ * Please try not to mess with the Rakefile, version, or history. If you want to have your own version, or is otherwise necessary, that is fine, but please isolate to its own commit so I can cherry-pick around it.
48
+
49
+ == Credits
50
+
51
+ Ryan Sonnek for building the multitenant gem and Guy Naor for inspiration.
52
+
53
+ == Copyright
54
+
55
+ Copyright (c) 2011 Fabio Kuhn. See LICENSE.txt for
56
+ further details.
57
+
@@ -0,0 +1,6 @@
1
+ require 'bundler'
2
+ Bundler::GemHelper.install_tasks
3
+
4
+ require 'rspec/core/rake_task'
5
+ RSpec::Core::RakeTask.new('spec')
6
+ task :default => :spec
@@ -0,0 +1,2 @@
1
+ require 'multitenant'
2
+ require 'multitenant/version'
@@ -0,0 +1,22 @@
1
+ require 'multitenant/schema_utils'
2
+
3
+ # Multitenant: making cross tenant data leaks a thing of the past...since 2011
4
+ module Multitenant
5
+ class << self
6
+ attr_accessor :current_tenant
7
+
8
+ # execute a block scoped to the current tenant
9
+ # unsets the current tenant after execution
10
+ def with_tenant(tenant = nil, &block)
11
+ previous_tenant = Multitenant.current_tenant
12
+ Multitenant.current_tenant = tenant if tenant
13
+
14
+ SchemaUtils.with_schema(Multitenant.current_tenant.schema_name) do
15
+ yield
16
+ end
17
+
18
+ ensure
19
+ Multitenant.current_tenant = previous_tenant
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,85 @@
1
+ require 'active_record'
2
+
3
+ module Multitenant
4
+ module SchemaUtils
5
+ class << self
6
+ def with_schema(schema_name)
7
+ old_search_path = connection.schema_search_path
8
+ add_schema_to_path(schema_name)
9
+ connection.schema_search_path = schema_name
10
+ result = yield
11
+
12
+ connection.schema_search_path = old_search_path
13
+ reset_search_path
14
+ result
15
+ end
16
+
17
+ def add_schema_to_path(schema_name)
18
+ connection.execute "SET search_path TO #{schema_name}"
19
+ end
20
+
21
+ def reset_search_path
22
+ connection.execute "SET search_path TO #{connection.schema_search_path}"
23
+ ActiveRecord::Base.connection.reset!
24
+ end
25
+
26
+ def current_search_path
27
+ connection.select_value "SHOW search_path"
28
+ end
29
+
30
+ def create_schema(schema_name)
31
+ raise "#{schema_name} already exists" if schema_exists?(schema_name)
32
+
33
+ ActiveRecord::Base.logger.info "Create #{schema_name}"
34
+ connection.execute "CREATE SCHEMA #{schema_name}"
35
+ end
36
+
37
+ def drop_schema(schema_name)
38
+ raise "#{schema_name} does not exists" unless schema_exists?(schema_name)
39
+
40
+ ActiveRecord::Base.logger.info "Drop schema #{schema_name}"
41
+ connection.execute "DROP SCHEMA #{schema_name} CASCADE"
42
+ end
43
+
44
+ def migrate_schema(schema_name, version = nil)
45
+ with_schema(schema_name) do
46
+ ActiveRecord::Migrator.migrate(ActiveRecord::Migrator.migrations_paths, version ? version.to_i : nil)
47
+ end
48
+ end
49
+
50
+ def load_schema_into_schema(schema_name)
51
+ ActiveRecord::Base.logger.info "Enter schema #{schema_name}."
52
+ with_schema(schema_name) do
53
+ file = "#{Rails.root}/db/schema.rb"
54
+ if File.exists?(file)
55
+ ActiveRecord::Base.logger.info "Load the schema #{file}"
56
+ load(file)
57
+ else
58
+ raise "#{file} desn't exist yet. It's possible that you just ran a migration!"
59
+ end
60
+ end
61
+ end
62
+
63
+ def schema_exists?(schema_name)
64
+ all_schemas.include?(schema_name)
65
+ end
66
+
67
+ def all_schemas
68
+ connection.select_values("SELECT * FROM pg_namespace WHERE nspname != 'information_schema' AND nspname NOT LIKE 'pg%'")
69
+ end
70
+
71
+ def with_all_schemas
72
+ all_schemas.each do |schema_name|
73
+ with_schema(schema_name) do
74
+ yield
75
+ end
76
+ end
77
+ end
78
+
79
+ protected
80
+ def connection
81
+ ActiveRecord::Base.connection
82
+ end
83
+ end
84
+ end
85
+ end
@@ -0,0 +1,3 @@
1
+ module Multitenant
2
+ VERSION = "0.1.0"
3
+ end
@@ -0,0 +1,26 @@
1
+ # -*- encoding: utf-8 -*-
2
+ $:.push File.expand_path("../lib", __FILE__)
3
+ require "multitenant/version"
4
+
5
+ Gem::Specification.new do |s|
6
+ s.name = "multitenant-pg"
7
+ s.version = Multitenant::VERSION
8
+ s.platform = Gem::Platform::RUBY
9
+ s.authors = ["Fabio Kuhn"]
10
+ s.email = ["mordaroso@gmail.com"]
11
+ s.homepage = "http://github.com/mordaroso/multitenant-pg"
12
+ s.summary = %q{scope database queries to current tenant}
13
+ s.description = %q{Rails multitenancy with PostgreSQL schemas.}
14
+
15
+ s.add_dependency(%q<activerecord>, ['>= 3.1'])
16
+ s.add_development_dependency(%q<pg>, ["~> 0.11.0"])
17
+ s.add_development_dependency(%q<rspec>, ['~> 2.7.0'])
18
+ s.add_development_dependency(%q<rspec-core>, ['~> 2.7.0'])
19
+ s.add_development_dependency(%q<rspec-mocks>, ['~> 2.7.0'])
20
+ s.add_development_dependency(%q<database_cleaner>, ['>= 0.5.0'])
21
+
22
+ s.files = `git ls-files`.split("\n")
23
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
24
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
25
+ s.require_paths = ["lib"]
26
+ end
@@ -0,0 +1,7 @@
1
+ test:
2
+ adapter: postgresql
3
+ host: localhost
4
+ encoding: utf8
5
+ template: template0
6
+ schema: public
7
+ database: multitenant_test
@@ -0,0 +1,19 @@
1
+ ActiveRecord::Schema.define(:version => 1) do
2
+ create_table :companies, :force => true do |t|
3
+ t.column :name, :string
4
+ end
5
+
6
+ create_table :users, :force => true do |t|
7
+ t.column :name, :string
8
+ t.column :company_id, :integer
9
+ end
10
+
11
+ create_table :tenants, :force => true do |t|
12
+ t.column :name, :string
13
+ t.column :schema_name, :string
14
+ end
15
+
16
+ create_table :items, :force => true do |t|
17
+ t.column :name, :string
18
+ end
19
+ end
@@ -0,0 +1,19 @@
1
+ ActiveRecord::Schema.define(:version => 1) do
2
+ create_table :companies, :force => true do |t|
3
+ t.column :name, :string
4
+ end
5
+
6
+ create_table :users, :force => true do |t|
7
+ t.column :name, :string
8
+ t.column :company_id, :integer
9
+ end
10
+
11
+ create_table :tenants, :force => true do |t|
12
+ t.column :name, :string
13
+ t.column :schema_name, :string
14
+ end
15
+
16
+ create_table :items, :force => true do |t|
17
+ t.column :name, :string
18
+ end
19
+ end
@@ -0,0 +1,134 @@
1
+ require 'spec_helper'
2
+
3
+ class Company < ActiveRecord::Base
4
+ has_many :users
5
+ end
6
+
7
+ class User < ActiveRecord::Base
8
+ belongs_to :company
9
+ end
10
+
11
+ class Tenant < ActiveRecord::Base
12
+ set_table_name 'public.tenants'
13
+
14
+ after_create :setup_schema
15
+
16
+ def setup_schema
17
+ unless Multitenant::SchemaUtils.schema_exists?(schema_name)
18
+ Multitenant::SchemaUtils.create_schema(schema_name)
19
+ Multitenant::SchemaUtils.load_schema_into_schema(schema_name)
20
+ end
21
+ end
22
+ end
23
+
24
+ module Rails
25
+ end
26
+
27
+ describe Multitenant do
28
+ before(:all) do
29
+ load File.join(File.dirname(__FILE__), '/fixtures/db/schema.rb')
30
+ end
31
+
32
+ before(:each) do
33
+ #ActiveRecord::Base.connection.schema_search_path = 'public'
34
+ Rails.stub(:root).and_return(File.join(File.dirname(__FILE__), 'fixtures'))
35
+
36
+ @tenant = Tenant.create(:name => 'foo', :schema_name => 'foo')
37
+ @tenant2 = Tenant.create(:name => 'bar', :schema_name => 'bar')
38
+ end
39
+
40
+ after do
41
+ Multitenant.current_tenant = nil;
42
+ Multitenant::SchemaUtils.with_all_schemas do
43
+ DatabaseCleaner.clean_with :truncation
44
+ end
45
+ end
46
+
47
+ describe 'Multitenant.current_tenant' do
48
+ before { Multitenant.current_tenant = :foo }
49
+ it { Multitenant.current_tenant == :foo }
50
+ end
51
+
52
+ describe 'Multitenant.with_tenant block' do
53
+ before do
54
+ @executed = false
55
+ Multitenant.with_tenant @tenant do
56
+ Multitenant.current_tenant.should == @tenant
57
+ @executed = true
58
+ end
59
+ end
60
+ it 'clears current_tenant after block runs' do
61
+ Multitenant.current_tenant.should == nil
62
+ end
63
+ it 'yields the block' do
64
+ @executed.should == true
65
+ end
66
+ end
67
+
68
+ describe 'Multitenant.with_tenant block with a previous tenant' do
69
+ before do
70
+ @previous = :whatever
71
+ Multitenant.current_tenant = @previous
72
+ @executed = false
73
+ Multitenant.with_tenant @tenant do
74
+ Multitenant.current_tenant.should == @tenant
75
+ Multitenant::SchemaUtils.current_search_path.should == @tenant.schema_name
76
+ @executed = true
77
+ end
78
+ end
79
+
80
+ it 'resets current_tenant after block runs' do
81
+ Multitenant.current_tenant.should == @previous
82
+ #ActiveRecord::Base.connection.schema_search_path.should == 'public'
83
+ end
84
+
85
+ it 'yields the block' do
86
+ @executed.should == true
87
+ end
88
+ end
89
+
90
+ describe 'Multitenant.with_tenant block that raises error' do
91
+ before do
92
+ @executed = false
93
+ expect {
94
+ Multitenant.with_tenant @tenant do
95
+ @executed = true
96
+ raise 'expected error'
97
+ end
98
+ }.to raise_error('expected error')
99
+ end
100
+
101
+ it 'clears current_tenant after block runs' do
102
+ Multitenant.current_tenant.should == nil
103
+ end
104
+
105
+ it 'yields the block' do
106
+ @executed.should == true
107
+ end
108
+ end
109
+
110
+ describe 'User.all when current_tenant is set' do
111
+ before do
112
+ Multitenant.with_tenant @tenant do
113
+ Multitenant::SchemaUtils.current_search_path.should == @tenant.schema_name
114
+ @company = Company.create!(:name => 'foo')
115
+ @user = @company.users.create! :name => 'bob'
116
+ end
117
+
118
+ Multitenant.with_tenant @tenant2 do
119
+ Multitenant::SchemaUtils.current_search_path.should == @tenant2.schema_name
120
+ company = Company.create!(:name => 'bar')
121
+ user = company.users.create! :name => 'frank'
122
+ end
123
+
124
+ Multitenant.with_tenant @tenant do
125
+ Multitenant::SchemaUtils.current_search_path.should == @tenant.schema_name
126
+ @users = User.all
127
+ end
128
+ end
129
+
130
+ it { @users.length.should == 1 }
131
+
132
+ it { @users.should == [@user] }
133
+ end
134
+ end
@@ -0,0 +1,19 @@
1
+ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
2
+ $LOAD_PATH.unshift(File.dirname(__FILE__))
3
+ require 'rspec'
4
+ require 'multitenant-pg'
5
+ require 'active_record'
6
+ require 'logger'
7
+ require 'database_cleaner'
8
+
9
+ config = YAML::load(IO.read(File.join(File.dirname(__FILE__), 'database.yml')))
10
+ ActiveRecord::Base.logger = Logger.new(File.join(File.dirname(__FILE__), "debug.log"))
11
+ ActiveRecord::Base.establish_connection(config[ENV['DB'] || 'test'])
12
+
13
+ # Requires supporting files with custom matchers and macros, etc,
14
+ # in ./support/ and its subdirectories.
15
+ Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each {|f| require f}
16
+
17
+ RSpec.configure do |config|
18
+ config.mock_with :rspec
19
+ end
metadata ADDED
@@ -0,0 +1,181 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: multitenant-pg
3
+ version: !ruby/object:Gem::Version
4
+ hash: 27
5
+ prerelease:
6
+ segments:
7
+ - 0
8
+ - 1
9
+ - 0
10
+ version: 0.1.0
11
+ platform: ruby
12
+ authors:
13
+ - Fabio Kuhn
14
+ autorequire:
15
+ bindir: bin
16
+ cert_chain: []
17
+
18
+ date: 2011-11-04 00:00:00 Z
19
+ dependencies:
20
+ - !ruby/object:Gem::Dependency
21
+ name: activerecord
22
+ prerelease: false
23
+ requirement: &id001 !ruby/object:Gem::Requirement
24
+ none: false
25
+ requirements:
26
+ - - ">="
27
+ - !ruby/object:Gem::Version
28
+ hash: 5
29
+ segments:
30
+ - 3
31
+ - 1
32
+ version: "3.1"
33
+ type: :runtime
34
+ version_requirements: *id001
35
+ - !ruby/object:Gem::Dependency
36
+ name: pg
37
+ prerelease: false
38
+ requirement: &id002 !ruby/object:Gem::Requirement
39
+ none: false
40
+ requirements:
41
+ - - ~>
42
+ - !ruby/object:Gem::Version
43
+ hash: 51
44
+ segments:
45
+ - 0
46
+ - 11
47
+ - 0
48
+ version: 0.11.0
49
+ type: :development
50
+ version_requirements: *id002
51
+ - !ruby/object:Gem::Dependency
52
+ name: rspec
53
+ prerelease: false
54
+ requirement: &id003 !ruby/object:Gem::Requirement
55
+ none: false
56
+ requirements:
57
+ - - ~>
58
+ - !ruby/object:Gem::Version
59
+ hash: 19
60
+ segments:
61
+ - 2
62
+ - 7
63
+ - 0
64
+ version: 2.7.0
65
+ type: :development
66
+ version_requirements: *id003
67
+ - !ruby/object:Gem::Dependency
68
+ name: rspec-core
69
+ prerelease: false
70
+ requirement: &id004 !ruby/object:Gem::Requirement
71
+ none: false
72
+ requirements:
73
+ - - ~>
74
+ - !ruby/object:Gem::Version
75
+ hash: 19
76
+ segments:
77
+ - 2
78
+ - 7
79
+ - 0
80
+ version: 2.7.0
81
+ type: :development
82
+ version_requirements: *id004
83
+ - !ruby/object:Gem::Dependency
84
+ name: rspec-mocks
85
+ prerelease: false
86
+ requirement: &id005 !ruby/object:Gem::Requirement
87
+ none: false
88
+ requirements:
89
+ - - ~>
90
+ - !ruby/object:Gem::Version
91
+ hash: 19
92
+ segments:
93
+ - 2
94
+ - 7
95
+ - 0
96
+ version: 2.7.0
97
+ type: :development
98
+ version_requirements: *id005
99
+ - !ruby/object:Gem::Dependency
100
+ name: database_cleaner
101
+ prerelease: false
102
+ requirement: &id006 !ruby/object:Gem::Requirement
103
+ none: false
104
+ requirements:
105
+ - - ">="
106
+ - !ruby/object:Gem::Version
107
+ hash: 11
108
+ segments:
109
+ - 0
110
+ - 5
111
+ - 0
112
+ version: 0.5.0
113
+ type: :development
114
+ version_requirements: *id006
115
+ description: Rails multitenancy with PostgreSQL schemas.
116
+ email:
117
+ - mordaroso@gmail.com
118
+ executables: []
119
+
120
+ extensions: []
121
+
122
+ extra_rdoc_files: []
123
+
124
+ files:
125
+ - .gitignore
126
+ - .rspec
127
+ - .rvmrc
128
+ - .travis.yml
129
+ - Gemfile
130
+ - LICENSE.txt
131
+ - README.rdoc
132
+ - Rakefile
133
+ - lib/multitenant-pg.rb
134
+ - lib/multitenant.rb
135
+ - lib/multitenant/schema_utils.rb
136
+ - lib/multitenant/version.rb
137
+ - multitenant-pg.gemspec
138
+ - spec/database.yml
139
+ - spec/fixtures/db/schema.rb
140
+ - spec/fixtures/schema.rb
141
+ - spec/multitenant_spec.rb
142
+ - spec/spec_helper.rb
143
+ homepage: http://github.com/mordaroso/multitenant-pg
144
+ licenses: []
145
+
146
+ post_install_message:
147
+ rdoc_options: []
148
+
149
+ require_paths:
150
+ - lib
151
+ required_ruby_version: !ruby/object:Gem::Requirement
152
+ none: false
153
+ requirements:
154
+ - - ">="
155
+ - !ruby/object:Gem::Version
156
+ hash: 3
157
+ segments:
158
+ - 0
159
+ version: "0"
160
+ required_rubygems_version: !ruby/object:Gem::Requirement
161
+ none: false
162
+ requirements:
163
+ - - ">="
164
+ - !ruby/object:Gem::Version
165
+ hash: 3
166
+ segments:
167
+ - 0
168
+ version: "0"
169
+ requirements: []
170
+
171
+ rubyforge_project:
172
+ rubygems_version: 1.8.8
173
+ signing_key:
174
+ specification_version: 3
175
+ summary: scope database queries to current tenant
176
+ test_files:
177
+ - spec/database.yml
178
+ - spec/fixtures/db/schema.rb
179
+ - spec/fixtures/schema.rb
180
+ - spec/multitenant_spec.rb
181
+ - spec/spec_helper.rb