multitenant-mysql 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +17 -0
- data/.rspec +2 -0
- data/.travis.yml +4 -0
- data/Gemfile +6 -0
- data/LICENSE +22 -0
- data/README.rdoc +101 -0
- data/Rakefile +2 -0
- data/lib/generators/multitenant/install/install_generator.rb +18 -0
- data/lib/generators/multitenant/install/templates/multitenant_mysql_conf.rb +17 -0
- data/lib/generators/multitenant/migrations/migration_builder.rb +44 -0
- data/lib/generators/multitenant/migrations/migrations_generator.rb +14 -0
- data/lib/generators/multitenant/views_and_triggers/trigger_generator.rb +38 -0
- data/lib/generators/multitenant/views_and_triggers/view_generator.rb +28 -0
- data/lib/generators/multitenant/views_and_triggers/views_and_triggers_generator.rb +20 -0
- data/lib/multitenant-mysql.rb +58 -0
- data/lib/multitenant-mysql/action_controller_extension.rb +17 -0
- data/lib/multitenant-mysql/active_record_extension.rb +10 -0
- data/lib/multitenant-mysql/conf_file.rb +22 -0
- data/lib/multitenant-mysql/connection_switcher.rb +48 -0
- data/lib/multitenant-mysql/version.rb +5 -0
- data/multitenant-mysql.gemspec +19 -0
- data/rails/init.rb +34 -0
- data/spec/action_controller_extension_spec.rb +20 -0
- data/spec/conf_file_spec.rb +41 -0
- data/spec/connection_switcher_spec.rb +37 -0
- data/spec/multitenant_mysql_spec.rb +67 -0
- data/spec/rails/active_record_base_spec.rb +26 -0
- data/spec/spec_helper.rb +11 -0
- data/spec/support/multitenant_mysql_conf.rb +4 -0
- metadata +97 -0
data/.gitignore
ADDED
data/.rspec
ADDED
data/.travis.yml
ADDED
data/Gemfile
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2012 Eugene Korpan
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.rdoc
ADDED
@@ -0,0 +1,101 @@
|
|
1
|
+
== Multitenant::Mysql
|
2
|
+
|
3
|
+
Web app often face multitenancy problem and there are already few gems that help to solve this issue but this one uses different approach.
|
4
|
+
Instead of default scopes or creating a separate database for every single tenant this gem uses mysql views and triggers.
|
5
|
+
The idea is taken from http://blog.empowercampaigns.com/post/1044240481/multi-tenant-data-and-mysql
|
6
|
+
|
7
|
+
The advantages of such approach:
|
8
|
+
- no default scopes
|
9
|
+
- only one db
|
10
|
+
- the biggest advantage is that responsebility about the data is moved to the db layer. It means the customer shouldn't be warry about developer's mistake before very critical demo and the logic of the app is much simplier.
|
11
|
+
|
12
|
+
== Code Status
|
13
|
+
|
14
|
+
* {<img src="https://travis-ci.org/eugenekorpan/multitenant-mysql.png?branch=master"/>}[http://travis-ci.org/eugenekorpan/multitenant-mysql]
|
15
|
+
* {<img src="https://gemnasium.com/eugenekorpan/multitenant-mysql.png" alt="Dependency Status" />}[https://gemnasium.com/eugenekorpan/multitenant-mysql]
|
16
|
+
* {<img src="https://codeclimate.com/github/eugenekorpan/multitenant-mysql.png" />}[https://codeclimate.com/github/eugenekorpan/multitenant-mysql]
|
17
|
+
|
18
|
+
|
19
|
+
== Installation
|
20
|
+
|
21
|
+
1 Add this line to your application's Gemfile:
|
22
|
+
|
23
|
+
gem 'multitenant-mysql'
|
24
|
+
|
25
|
+
2 And then execute:
|
26
|
+
|
27
|
+
$ bundle
|
28
|
+
|
29
|
+
== Usage
|
30
|
+
|
31
|
+
1 run
|
32
|
+
rails g multitenant:install
|
33
|
+
|
34
|
+
this will create a sample of config file in "rails_root/config/multitenant_mysql_conf.rb"
|
35
|
+
This is the place where you list all your tenant dependent models and the model which contains all the tenants.
|
36
|
+
E.g:
|
37
|
+
|
38
|
+
Multitenant::Mysql.active_record_configs = {
|
39
|
+
models: ['Book', 'Task'],
|
40
|
+
tenant_model: { name: 'Subdomain' }
|
41
|
+
}
|
42
|
+
|
43
|
+
Important: Before moving on you have to update this file as all further steps use those configs.
|
44
|
+
|
45
|
+
2 generate migrations based on configs
|
46
|
+
rails g multitenant:migrations
|
47
|
+
|
48
|
+
3 migrate the database
|
49
|
+
rake db:migrate
|
50
|
+
|
51
|
+
4 generate mysql views and triggers
|
52
|
+
rails g multitenant:views_and_triggers
|
53
|
+
|
54
|
+
5 in ApplicationController
|
55
|
+
set_current_tenant :tenant_method
|
56
|
+
|
57
|
+
where `:tenant_method` is a methods which produces the current tenant name. It can be subdomain or current user's company name
|
58
|
+
E.g.
|
59
|
+
|
60
|
+
class ApplicationController < ActionController::Base
|
61
|
+
|
62
|
+
set_current_tenant :tenant_name
|
63
|
+
|
64
|
+
def tenant_name
|
65
|
+
current_user.tenant.name # or request.subdomain
|
66
|
+
end
|
67
|
+
|
68
|
+
end
|
69
|
+
|
70
|
+
if method used by `set_current_tenant` returns blank name then `root` account is used
|
71
|
+
|
72
|
+
== Host It Works
|
73
|
+
|
74
|
+
About the main principles you can read here http://blog.empowercampaigns.com/post/1044240481/multi-tenant-data-and-mysql.
|
75
|
+
|
76
|
+
As for gem implementation.
|
77
|
+
There are three main things to make it work:
|
78
|
+
|
79
|
+
- models that are tenant dependend
|
80
|
+
This one should be pretty obvious, all you need to do is just specify models in config file, no code inside the model;
|
81
|
+
|
82
|
+
- model which stores all tenants (in this case this is just a username of mysql account)
|
83
|
+
The information about this one you need to provide in config file.
|
84
|
+
This one is very simple, all you need is a column like `name` or `title` (or you can specify in config file) and by creating new entry you create MySql account (new tenant). Only after creating new entry with particular name you are able to use new tenant (otherwise there is just no MySql account and it won't work for particular tenant)
|
85
|
+
|
86
|
+
- `set_current_tenant` in ApplicationController
|
87
|
+
As a param of this method is the name of ApplicationController method which returns the name of current tenant.
|
88
|
+
Behind the scenes it creates a before_filter which uses current tenant name to establish appropriate connection to db.
|
89
|
+
|
90
|
+
== Feedback
|
91
|
+
|
92
|
+
So far it is tested with ruby 1.9.3 and rails 3.2
|
93
|
+
If you get any problems please feel free to post an issue
|
94
|
+
|
95
|
+
== Contributing
|
96
|
+
|
97
|
+
1. Fork it
|
98
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
99
|
+
3. Commit your changes (`git commit -am 'Added some feature'`)
|
100
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
101
|
+
5. Create new Pull Request
|
data/Rakefile
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
require 'rails/generators'
|
2
|
+
|
3
|
+
module Multitenant
|
4
|
+
class InstallGenerator < Rails::Generators::Base
|
5
|
+
desc "copy config file into rails app"
|
6
|
+
|
7
|
+
CONFIG_FILE_NAME = 'multitenant_mysql_conf.rb'
|
8
|
+
|
9
|
+
def copy_conf_file_into_app
|
10
|
+
dest = Rails.root.to_s + "/config/#{CONFIG_FILE_NAME}"
|
11
|
+
return if FileTest.exist?(dest) # to prevent overwritting of existing file
|
12
|
+
src = File.expand_path(File.dirname(__FILE__)) + "/templates/#{CONFIG_FILE_NAME}"
|
13
|
+
FileUtils.copy_file src, dest
|
14
|
+
p "The file was created `#{dest}`"
|
15
|
+
end
|
16
|
+
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
# simple example
|
2
|
+
#
|
3
|
+
# Multitenant::Mysql.active_record_configs = {
|
4
|
+
# models: ['Book', 'Task'],
|
5
|
+
# tenant_model: { name: 'Subdomain', tenant_name_attr: name }
|
6
|
+
# }
|
7
|
+
#
|
8
|
+
# where:
|
9
|
+
# models - list of tenant related models
|
10
|
+
# tenant_model - model where tenant info is stored
|
11
|
+
# name - model name
|
12
|
+
# tenant_name_attr - attribute used to fetch tenant name
|
13
|
+
|
14
|
+
Multitenant::Mysql.active_record_configs = {
|
15
|
+
models: [],
|
16
|
+
tenant_model: { name: '' }
|
17
|
+
}
|
@@ -0,0 +1,44 @@
|
|
1
|
+
module Multitenant
|
2
|
+
class MigrationBuilder
|
3
|
+
|
4
|
+
class << self
|
5
|
+
|
6
|
+
MIGRATION_NAME = 'add_tenant_column_to_models'
|
7
|
+
|
8
|
+
def run
|
9
|
+
return if migration_exists?
|
10
|
+
|
11
|
+
actions = Multitenant::Mysql.models.map { |model_name|
|
12
|
+
model = model_name.constantize
|
13
|
+
"add_column :#{model.table_name}, :tenant, :string"
|
14
|
+
}
|
15
|
+
|
16
|
+
dest_path = Rails.root.to_s + "/db/migrate/#{migration_number}_#{MIGRATION_NAME}.rb"
|
17
|
+
migration = File.new(dest_path, "w")
|
18
|
+
migration.puts(migration_code(actions))
|
19
|
+
migration.close
|
20
|
+
|
21
|
+
p "==================== Generated Migration =================="
|
22
|
+
p dest_path
|
23
|
+
end
|
24
|
+
|
25
|
+
def migration_number
|
26
|
+
Time.now.utc.strftime("%Y%m%d%H%M%S").to_i
|
27
|
+
end
|
28
|
+
|
29
|
+
def migration_code(actions)
|
30
|
+
%Q(class AddTenantColumnToModels < ActiveRecord::Migration
|
31
|
+
def change
|
32
|
+
#{actions.join("\n ")}
|
33
|
+
end
|
34
|
+
end)
|
35
|
+
end
|
36
|
+
|
37
|
+
def migration_exists?
|
38
|
+
migrations = Dir.entries(Rails.root.to_s + "/db/migrate")
|
39
|
+
migrations.any? { |m| m.include?(MIGRATION_NAME) }
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
require 'rails/generators'
|
2
|
+
|
3
|
+
require_relative './migration_builder'
|
4
|
+
require Rails.root.to_s + '/config/multitenant_mysql_conf'
|
5
|
+
|
6
|
+
module Multitenant
|
7
|
+
class MigrationsGenerator < Rails::Generators::Base
|
8
|
+
desc 'create migration to add tenant column to listed models'
|
9
|
+
|
10
|
+
def generate_migration
|
11
|
+
Multitenant::MigrationBuilder.run
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
module Multitenant
|
2
|
+
class TriggerGenerator
|
3
|
+
|
4
|
+
class << self
|
5
|
+
def run
|
6
|
+
Multitenant::Mysql.models.each do |model_name|
|
7
|
+
model = model_name.constantize
|
8
|
+
trigger_name = model.original_table_name + '_tenant_trigger'
|
9
|
+
|
10
|
+
return if trigger_exists?(trigger_name)
|
11
|
+
|
12
|
+
trigger_sql = %Q(
|
13
|
+
CREATE TRIGGER #{trigger_name}
|
14
|
+
BEFORE INSERT ON #{model.original_table_name}
|
15
|
+
FOR EACH ROW
|
16
|
+
SET new.tenant = SUBSTRING_INDEX(USER(), '@', 1);
|
17
|
+
)
|
18
|
+
|
19
|
+
p trigger_sql
|
20
|
+
ActiveRecord::Base.connection.execute(trigger_sql)
|
21
|
+
p "==================== Generated Trigger: #{trigger_name} =================="
|
22
|
+
end
|
23
|
+
|
24
|
+
end
|
25
|
+
|
26
|
+
def trigger_exists?(trigger_name)
|
27
|
+
config = Rails.configuration.database_configuration[Rails.env]
|
28
|
+
db_name = config['database']
|
29
|
+
|
30
|
+
find_trigger_sql = "SELECT trigger_name FROM information_schema.TRIGGERS where trigger_schema = '#{db_name}';"
|
31
|
+
result = ActiveRecord::Base.connection.execute(find_trigger_sql).to_a.flatten
|
32
|
+
|
33
|
+
result.include?(trigger_name)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
module Multitenant
|
2
|
+
class ViewGenerator
|
3
|
+
|
4
|
+
def self.run
|
5
|
+
Multitenant::Mysql.models.each do |model_name|
|
6
|
+
model = model_name.constantize
|
7
|
+
columns = model.column_names.join(', ')
|
8
|
+
view_name = model_name.to_s.downcase.pluralize + "_view"
|
9
|
+
|
10
|
+
# stop if view already exists
|
11
|
+
return if ActiveRecord::Base.connection.table_exists?(view_name)
|
12
|
+
|
13
|
+
view_sql = %Q(
|
14
|
+
CREATE VIEW #{view_name} AS
|
15
|
+
SELECT #{columns}
|
16
|
+
FROM #{model.table_name}
|
17
|
+
WHERE tenant = SUBSTRING_INDEX(USER(), '@', 1);
|
18
|
+
)
|
19
|
+
|
20
|
+
p view_sql
|
21
|
+
|
22
|
+
ActiveRecord::Base.connection.execute(view_sql)
|
23
|
+
p "==================== Generated View: #{view_name} =================="
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
require 'rails/generators'
|
2
|
+
|
3
|
+
require_relative './trigger_generator'
|
4
|
+
require_relative './view_generator'
|
5
|
+
|
6
|
+
require Rails.root.to_s + '/config/multitenant_mysql_conf'
|
7
|
+
|
8
|
+
module Multitenant
|
9
|
+
class ViewsAndTriggersGenerator < Rails::Generators::Base
|
10
|
+
desc "based on specified models will create appropriate mysql views, triggers and migrations"
|
11
|
+
|
12
|
+
def generate_mysql_views
|
13
|
+
Multitenant::ViewGenerator.run
|
14
|
+
end
|
15
|
+
|
16
|
+
def generate_mysql_triggers
|
17
|
+
Multitenant::TriggerGenerator.run
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
require 'multitenant-mysql/version'
|
2
|
+
require 'multitenant-mysql/active_record_extension'
|
3
|
+
require 'multitenant-mysql/action_controller_extension'
|
4
|
+
require 'multitenant-mysql/conf_file'
|
5
|
+
|
6
|
+
require_relative '../rails/init'
|
7
|
+
|
8
|
+
module Multitenant
|
9
|
+
module Mysql
|
10
|
+
|
11
|
+
class << self
|
12
|
+
|
13
|
+
DEFAULT_TENANT_NAME_ATTR = [:name, :title]
|
14
|
+
|
15
|
+
def active_record_configs=(configs)
|
16
|
+
@configs = configs
|
17
|
+
end
|
18
|
+
|
19
|
+
def active_record_configs
|
20
|
+
@configs
|
21
|
+
end
|
22
|
+
|
23
|
+
alias_method :arc, :active_record_configs
|
24
|
+
|
25
|
+
def tenant
|
26
|
+
arc[:tenant_model][:name].constantize
|
27
|
+
rescue
|
28
|
+
if arc.blank? || arc[:tenant_model].blank? || arc[:tenant_model][:name].blank?
|
29
|
+
raise "
|
30
|
+
Multitenant::Mysql: You should specify model which stores info about tenants.
|
31
|
+
E.g. in initializer:
|
32
|
+
Multitenant::Mysql.arc = {
|
33
|
+
tenant_model: { name: 'Subdomain' }
|
34
|
+
}
|
35
|
+
"
|
36
|
+
else
|
37
|
+
raise "Please check your multitenant-mysql configs. Seems like you are trying to use model which doesn't exist: #{arc[:tenant_model][:name]}"
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def models
|
42
|
+
active_record_configs[:models]
|
43
|
+
end
|
44
|
+
|
45
|
+
def tenant_name_attr
|
46
|
+
return active_record_configs[:tenant_model][:tenant_name_attr] if active_record_configs[:tenant_model][:tenant_name_attr]
|
47
|
+
|
48
|
+
DEFAULT_TENANT_NAME_ATTR.each do |name|
|
49
|
+
return name if tenant.column_names.include?(name.to_s)
|
50
|
+
end
|
51
|
+
|
52
|
+
raise 'You should specify tenant name attribute or use one default: name, title'
|
53
|
+
end
|
54
|
+
|
55
|
+
end
|
56
|
+
|
57
|
+
end
|
58
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
require_relative './connection_switcher'
|
2
|
+
|
3
|
+
class ActionController::Base
|
4
|
+
def self.set_current_tenant(tenant_method)
|
5
|
+
require Multitenant::Mysql::ConfFile.path
|
6
|
+
|
7
|
+
raise "you should provide tenant method" unless tenant_method
|
8
|
+
|
9
|
+
@@tenant_method = tenant_method
|
10
|
+
|
11
|
+
before_filter :establish_tenant_connection
|
12
|
+
|
13
|
+
def establish_tenant_connection
|
14
|
+
Multitenant::Mysql::ConnectionSwitcher.new(self, @@tenant_method).execute
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,10 @@
|
|
1
|
+
class ActiveRecord::Base
|
2
|
+
def self.acts_as_tenant
|
3
|
+
after_create do
|
4
|
+
config = Rails.configuration.database_configuration
|
5
|
+
password = config[Rails.env]["password"]
|
6
|
+
ActiveRecord::Base.connection.execute "GRANT ALL PRIVILEGES ON *.* TO '#{self.name}'@'localhost' IDENTIFIED BY '#{password}' WITH GRANT OPTION;"
|
7
|
+
ActiveRecord::Base.connection.execute "flush privileges;"
|
8
|
+
end
|
9
|
+
end
|
10
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
module Multitenant
|
2
|
+
module Mysql
|
3
|
+
class ConfFile
|
4
|
+
def self.path=(path)
|
5
|
+
@path = path
|
6
|
+
end
|
7
|
+
|
8
|
+
def self.path
|
9
|
+
# workaround to reqire conf file in rails app
|
10
|
+
@path ||= default_path
|
11
|
+
end
|
12
|
+
|
13
|
+
def self.full_path
|
14
|
+
path << '.rb'
|
15
|
+
end
|
16
|
+
|
17
|
+
def self.default_path
|
18
|
+
Rails.root.to_s + '/config/multitenant_mysql_conf'
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
module Multitenant
|
2
|
+
module Mysql
|
3
|
+
class NoTenantRegistratedError < StandardError; end;
|
4
|
+
|
5
|
+
class Tenant
|
6
|
+
def self.exists? tenant_name
|
7
|
+
return true if tenant_name.blank?
|
8
|
+
if Multitenant::Mysql.tenant.where(Multitenant::Mysql.tenant_name_attr => tenant_name).blank?
|
9
|
+
raise Multitenant::Mysql::NoTenantRegistratedError.new("No tenant registered: #{tenant_name}")
|
10
|
+
end
|
11
|
+
true
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
class ConnectionSwitcher
|
16
|
+
attr_accessor :action_controller, :method
|
17
|
+
|
18
|
+
def initialize(action_controller, method)
|
19
|
+
@action_controller = action_controller
|
20
|
+
@method = method
|
21
|
+
end
|
22
|
+
|
23
|
+
def self.set_tenant(tenant_name)
|
24
|
+
return unless Tenant.exists?(tenant_name)
|
25
|
+
|
26
|
+
config = Rails.configuration.database_configuration[Rails.env]
|
27
|
+
config['username'] = tenant_name.blank? ? 'root' : tenant_name
|
28
|
+
ActiveRecord::Base.establish_connection(config)
|
29
|
+
end
|
30
|
+
|
31
|
+
def execute
|
32
|
+
config = db_config
|
33
|
+
|
34
|
+
tenant_name = action_controller.send(method)
|
35
|
+
return unless Tenant.exists?(tenant_name)
|
36
|
+
|
37
|
+
config['username'] = tenant_name.blank? ? 'root' : tenant_name
|
38
|
+
ActiveRecord::Base.establish_connection(config)
|
39
|
+
true
|
40
|
+
end
|
41
|
+
|
42
|
+
def db_config
|
43
|
+
Rails.configuration.database_configuration[Rails.env]
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
require File.expand_path('../lib/multitenant-mysql/version', __FILE__)
|
3
|
+
|
4
|
+
Gem::Specification.new do |gem|
|
5
|
+
gem.authors = ["Eugene Korpan"]
|
6
|
+
gem.email = ["korpan.eugene@gamil.com"]
|
7
|
+
gem.description = %q{Integrates multi-tenancy into Rail application with MySql db}
|
8
|
+
gem.summary = %q{Add multi-tenancy to Rails application using MySql views}
|
9
|
+
gem.homepage = "https://github.com/eugenekorpan/multitenant-mysql"
|
10
|
+
|
11
|
+
gem.files = `git ls-files`.split($\)
|
12
|
+
gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
|
13
|
+
gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
|
14
|
+
gem.name = "multitenant-mysql"
|
15
|
+
gem.require_paths = ["lib"]
|
16
|
+
gem.version = Multitenant::Mysql::VERSION
|
17
|
+
|
18
|
+
gem.add_development_dependency "rspec"
|
19
|
+
end
|
data/rails/init.rb
ADDED
@@ -0,0 +1,34 @@
|
|
1
|
+
ActiveRecord::Base.class_eval do
|
2
|
+
def self.inherited(child)
|
3
|
+
|
4
|
+
return unless FileTest.exist?(Multitenant::Mysql::ConfFile.full_path) # do nothing if no config file provided
|
5
|
+
|
6
|
+
require Multitenant::Mysql::ConfFile.path
|
7
|
+
|
8
|
+
model_name = child.to_s
|
9
|
+
if Multitenant::Mysql.models.include? model_name
|
10
|
+
view_name = model_name.to_s.downcase.pluralize + "_view"
|
11
|
+
|
12
|
+
# check whether the view exists in db
|
13
|
+
if ActiveRecord::Base.connection.table_exists? view_name
|
14
|
+
child.class_eval do
|
15
|
+
cattr_accessor :original_table_name
|
16
|
+
|
17
|
+
self.original_table_name = self.table_name
|
18
|
+
self.table_name = view_name
|
19
|
+
self.primary_key = :id
|
20
|
+
|
21
|
+
def self.new(*args)
|
22
|
+
object = super(*args)
|
23
|
+
object.id = nil # workaround for https://github.com/rails/rails/issues/5982
|
24
|
+
object
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
if Multitenant::Mysql.tenant == child
|
31
|
+
child.send :acts_as_tenant
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe ActionController::Base do
|
4
|
+
subject { ActionController::Base }
|
5
|
+
|
6
|
+
context '.set_current_tenant' do
|
7
|
+
it 'should raise an error when no tenant method provided' do
|
8
|
+
expect { subject.set_current_tenant }.to raise_error
|
9
|
+
end
|
10
|
+
|
11
|
+
it 'should establish connection' do
|
12
|
+
mock = double('Multitenant::Mysql::ConnectionSwitcher')
|
13
|
+
mock.stub(:execute).and_return('ok')
|
14
|
+
Multitenant::Mysql::ConnectionSwitcher.should_receive(:new).and_return(mock)
|
15
|
+
subject.set_current_tenant(:name)
|
16
|
+
|
17
|
+
expect( subject.new.establish_tenant_connection ).to eql('ok')
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Multitenant::Mysql::ConfFile do
|
4
|
+
subject { Multitenant::Mysql::ConfFile }
|
5
|
+
|
6
|
+
context '.path' do
|
7
|
+
it 'should return path' do
|
8
|
+
subject.path = '/conf/file'
|
9
|
+
expect(subject.path).to eql('/conf/file')
|
10
|
+
end
|
11
|
+
|
12
|
+
it 'should return default path to conf file in rails app' do
|
13
|
+
path = 'Rails.root/config/multitenant_mysql_conf'
|
14
|
+
|
15
|
+
subject.stub(:default_path).and_return(path)
|
16
|
+
subject.path = nil
|
17
|
+
|
18
|
+
expect(subject.path).to eql(path)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
context '.full_path' do
|
23
|
+
it 'should return path with extension of the file' do
|
24
|
+
subject.path = '/conf/file'
|
25
|
+
expect(subject.full_path).to eql('/conf/file.rb')
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
context '.default_path' do
|
30
|
+
it 'should return path to conf file in rails app' do
|
31
|
+
class Rails
|
32
|
+
def self.root
|
33
|
+
'Rails.root'
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
expect(subject.default_path).to eql('Rails.root/config/multitenant_mysql_conf')
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Multitenant::Mysql::ConnectionSwitcher do
|
4
|
+
before do
|
5
|
+
Multitenant::Mysql.stub(:tenant_name_attr).and_return('name')
|
6
|
+
end
|
7
|
+
|
8
|
+
context '#execute' do
|
9
|
+
it 'should raise error if there is no tenant account in db' do
|
10
|
+
ar_mock = double('ActiveRecord::Relation')
|
11
|
+
ar_mock.stub(:where).and_return(nil)
|
12
|
+
Multitenant::Mysql.stub(:tenant).and_return(ar_mock)
|
13
|
+
|
14
|
+
ac_mock = double('ActionController::Base')
|
15
|
+
ac_mock.should_receive(:send).and_return('unexisting tenant')
|
16
|
+
switcher = Multitenant::Mysql::ConnectionSwitcher.new(ac_mock, :tenant_method)
|
17
|
+
switcher.stub(:db_config).and_return({ username: 'root' })
|
18
|
+
|
19
|
+
expect { switcher.execute }.to raise_error(Multitenant::Mysql::NoTenantRegistratedError)
|
20
|
+
end
|
21
|
+
|
22
|
+
it 'should change db connection' do
|
23
|
+
ar_mock = double('ActiveRecord::Relation')
|
24
|
+
ar_mock.stub(:where).and_return(:some_result)
|
25
|
+
Multitenant::Mysql.stub(:tenant).and_return(ar_mock)
|
26
|
+
|
27
|
+
ac_mock = double('ActionController::Base')
|
28
|
+
ac_mock.should_receive(:tenant_method)
|
29
|
+
switcher = Multitenant::Mysql::ConnectionSwitcher.new(ac_mock, :tenant_method)
|
30
|
+
switcher.stub(:db_config).and_return({ username: 'root' })
|
31
|
+
|
32
|
+
ActiveRecord::Base.should_receive(:establish_connection)
|
33
|
+
|
34
|
+
expect( switcher.execute ).to be
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,67 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Multitenant::Mysql do
|
4
|
+
subject { Multitenant::Mysql }
|
5
|
+
|
6
|
+
context 'active record configs' do
|
7
|
+
|
8
|
+
it 'should raise error if no tenant name provided' do
|
9
|
+
subject.active_record_configs = nil
|
10
|
+
expect {
|
11
|
+
subject.tenant_name_attr
|
12
|
+
}.to raise_error
|
13
|
+
end
|
14
|
+
|
15
|
+
context 'tenant name attribute' do
|
16
|
+
it 'should use name or title by default' do
|
17
|
+
mock = double()
|
18
|
+
mock.stub(:column_names).and_return(['name'])
|
19
|
+
subject.stub(:tenant).and_return(mock)
|
20
|
+
subject.stub(:active_record_configs).and_return({tenant_model: {}})
|
21
|
+
expect(subject.tenant_name_attr).to eql(:name)
|
22
|
+
|
23
|
+
mock.stub(:column_names).and_return(['title'])
|
24
|
+
expect(subject.tenant_name_attr).to eql(:title)
|
25
|
+
end
|
26
|
+
|
27
|
+
it 'should use attribute from configs' do
|
28
|
+
subject.stub(:active_record_configs).and_return({tenant_model: {tenant_name_attr: 'subdomain'}})
|
29
|
+
expect(subject.tenant_name_attr).to eql('subdomain')
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
context '.tenant' do
|
34
|
+
Subdomain = :constant
|
35
|
+
|
36
|
+
it 'should find and return appropriate model' do
|
37
|
+
subject.active_record_configs = { tenant_model: { name: 'Subdomain' } }
|
38
|
+
expect(subject.tenant).to eq(Subdomain)
|
39
|
+
end
|
40
|
+
|
41
|
+
context 'invalid data' do
|
42
|
+
it 'should raise error if no data provided' do
|
43
|
+
subject.active_record_configs = {}
|
44
|
+
expect { subject.tenant }.to raise_error(RuntimeError)
|
45
|
+
end
|
46
|
+
|
47
|
+
it 'should raise error if invalid data provided' do
|
48
|
+
subject.active_record_configs = { tenant_model: { name: nil } }
|
49
|
+
expect { subject.tenant }.to raise_error(RuntimeError)
|
50
|
+
end
|
51
|
+
|
52
|
+
it 'should raise error if invalid model provided' do
|
53
|
+
subject.active_record_configs = { tenant_model: { name: 'UnexistingModelLa' } }
|
54
|
+
expect { subject.tenant }.to raise_error(RuntimeError)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
context '.models' do
|
60
|
+
it 'should return listed models' do
|
61
|
+
subject.active_record_configs = { models: ['Book', 'Task'] }
|
62
|
+
expect(subject.models).to have(2).items
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
end
|
67
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe ActiveRecord::Base do
|
4
|
+
subject { ActiveRecord::Base }
|
5
|
+
|
6
|
+
before do
|
7
|
+
Multitenant::Mysql::ConfFile.path = CONF_FILE_PATH
|
8
|
+
end
|
9
|
+
|
10
|
+
it 'should respond to acts_as_tenant' do
|
11
|
+
subject.should respond_to(:acts_as_tenant)
|
12
|
+
end
|
13
|
+
|
14
|
+
it 'should redefine table name and primary key and keep original table name' do
|
15
|
+
ActiveRecord::Base.stub_chain(:connection, :table_exists?).and_return(true)
|
16
|
+
|
17
|
+
Multitenant::Mysql.stub(:tenant).and_return(true)
|
18
|
+
|
19
|
+
class Book < ActiveRecord::Base
|
20
|
+
end
|
21
|
+
|
22
|
+
expect(Book.table_name).to eql('books_view')
|
23
|
+
expect(Book.primary_key).to eql('id')
|
24
|
+
expect(Book.original_table_name).to eql('books')
|
25
|
+
end
|
26
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,11 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'bundler/setup'
|
3
|
+
|
4
|
+
require 'active_record'
|
5
|
+
require 'action_controller'
|
6
|
+
require 'multitenant-mysql'
|
7
|
+
|
8
|
+
RSpec.configure do |config|
|
9
|
+
Multitenant::Mysql::ConfFile.path = File.expand_path('../', __FILE__) + '/support/multitenant_mysql_conf'
|
10
|
+
CONF_FILE_PATH = File.expand_path('../', __FILE__) + '/support/multitenant_mysql_conf'
|
11
|
+
end
|
metadata
ADDED
@@ -0,0 +1,97 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: multitenant-mysql
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.0.0
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Eugene Korpan
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2013-02-17 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: rspec
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ! '>='
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: '0'
|
22
|
+
type: :development
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
25
|
+
none: false
|
26
|
+
requirements:
|
27
|
+
- - ! '>='
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: '0'
|
30
|
+
description: Integrates multi-tenancy into Rail application with MySql db
|
31
|
+
email:
|
32
|
+
- korpan.eugene@gamil.com
|
33
|
+
executables: []
|
34
|
+
extensions: []
|
35
|
+
extra_rdoc_files: []
|
36
|
+
files:
|
37
|
+
- .gitignore
|
38
|
+
- .rspec
|
39
|
+
- .travis.yml
|
40
|
+
- Gemfile
|
41
|
+
- LICENSE
|
42
|
+
- README.rdoc
|
43
|
+
- Rakefile
|
44
|
+
- lib/generators/multitenant/install/install_generator.rb
|
45
|
+
- lib/generators/multitenant/install/templates/multitenant_mysql_conf.rb
|
46
|
+
- lib/generators/multitenant/migrations/migration_builder.rb
|
47
|
+
- lib/generators/multitenant/migrations/migrations_generator.rb
|
48
|
+
- lib/generators/multitenant/views_and_triggers/trigger_generator.rb
|
49
|
+
- lib/generators/multitenant/views_and_triggers/view_generator.rb
|
50
|
+
- lib/generators/multitenant/views_and_triggers/views_and_triggers_generator.rb
|
51
|
+
- lib/multitenant-mysql.rb
|
52
|
+
- lib/multitenant-mysql/action_controller_extension.rb
|
53
|
+
- lib/multitenant-mysql/active_record_extension.rb
|
54
|
+
- lib/multitenant-mysql/conf_file.rb
|
55
|
+
- lib/multitenant-mysql/connection_switcher.rb
|
56
|
+
- lib/multitenant-mysql/version.rb
|
57
|
+
- multitenant-mysql.gemspec
|
58
|
+
- rails/init.rb
|
59
|
+
- spec/action_controller_extension_spec.rb
|
60
|
+
- spec/conf_file_spec.rb
|
61
|
+
- spec/connection_switcher_spec.rb
|
62
|
+
- spec/multitenant_mysql_spec.rb
|
63
|
+
- spec/rails/active_record_base_spec.rb
|
64
|
+
- spec/spec_helper.rb
|
65
|
+
- spec/support/multitenant_mysql_conf.rb
|
66
|
+
homepage: https://github.com/eugenekorpan/multitenant-mysql
|
67
|
+
licenses: []
|
68
|
+
post_install_message:
|
69
|
+
rdoc_options: []
|
70
|
+
require_paths:
|
71
|
+
- lib
|
72
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
73
|
+
none: false
|
74
|
+
requirements:
|
75
|
+
- - ! '>='
|
76
|
+
- !ruby/object:Gem::Version
|
77
|
+
version: '0'
|
78
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
79
|
+
none: false
|
80
|
+
requirements:
|
81
|
+
- - ! '>='
|
82
|
+
- !ruby/object:Gem::Version
|
83
|
+
version: '0'
|
84
|
+
requirements: []
|
85
|
+
rubyforge_project:
|
86
|
+
rubygems_version: 1.8.21
|
87
|
+
signing_key:
|
88
|
+
specification_version: 3
|
89
|
+
summary: Add multi-tenancy to Rails application using MySql views
|
90
|
+
test_files:
|
91
|
+
- spec/action_controller_extension_spec.rb
|
92
|
+
- spec/conf_file_spec.rb
|
93
|
+
- spec/connection_switcher_spec.rb
|
94
|
+
- spec/multitenant_mysql_spec.rb
|
95
|
+
- spec/rails/active_record_base_spec.rb
|
96
|
+
- spec/spec_helper.rb
|
97
|
+
- spec/support/multitenant_mysql_conf.rb
|