multitenant-mysql 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,17 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --color
2
+ --format documentation
@@ -0,0 +1,4 @@
1
+ language: ruby
2
+ rvm:
3
+ - 1.9.3
4
+ script: bundle exec rspec spec
data/Gemfile ADDED
@@ -0,0 +1,6 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in multitenant-mysql.gemspec
4
+ gemspec
5
+ gem 'rails'
6
+ gem 'rspec'
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.
@@ -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
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env rake
2
+ require "bundler/gem_tasks"
@@ -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,5 @@
1
+ module Multitenant
2
+ module Mysql
3
+ VERSION = "1.0.0"
4
+ end
5
+ 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
@@ -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
@@ -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
@@ -0,0 +1,4 @@
1
+ Multitenant::Mysql.active_record_configs = {
2
+ models: ['Book'],
3
+ tenant_model: { name: 'Subdomain', tenant_name_attr: 'title' }
4
+ }
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