fake-multitenancy 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.gitignore +9 -0
- data/.rspec +2 -0
- data/.ruby-version +1 -0
- data/.travis.yml +3 -0
- data/Gemfile +4 -0
- data/README.md +71 -0
- data/Rakefile +1 -0
- data/bin/console +14 -0
- data/bin/setup +7 -0
- data/fake-multitenancy.gemspec +27 -0
- data/lib/fake/multitenancy.rb +14 -0
- data/lib/fake/multitenancy/abstract_adapter.rb +22 -0
- data/lib/fake/multitenancy/active_record/base.rb +17 -0
- data/lib/fake/multitenancy/active_record/base/multitenancy.rb +47 -0
- data/lib/fake/multitenancy/active_record/schema_dumper.rb +118 -0
- data/lib/fake/multitenancy/version.rb +5 -0
- metadata +136 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: ad491e2b416140b470ffc0fc91d6e22c1563f437
|
4
|
+
data.tar.gz: a1bfd12142c1c8a459db2c37f2b28561c96e17dd
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: c861a127aac42c62600a9596838c529158f6267c20ae9d000d69094dc0d9381c17cb380fc8bbd461ca8d58581d7977269a713c7890706e910ad74847221bb233
|
7
|
+
data.tar.gz: 96e869fcc2420c98e0c3e3b7ddb771e2a79700aaa04fa4cfaa4da1c6879e74e5c7c782fad12482380d7c946e16feea09b0b63a59a47aa667a7f336db6845eb01
|
data/.gitignore
ADDED
data/.rspec
ADDED
data/.ruby-version
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
2.0.0-p643
|
data/.travis.yml
ADDED
data/Gemfile
ADDED
data/README.md
ADDED
@@ -0,0 +1,71 @@
|
|
1
|
+
# Fake-Multitenancy
|
2
|
+
|
3
|
+
With fake-multitenancy, you can serve several clients with one single database. Each client (tenant) will have it's own incremented ids. Data is transparently isolated between tenants.
|
4
|
+
|
5
|
+
|
6
|
+
## Installation
|
7
|
+
|
8
|
+
Add this line to your application's Gemfile:
|
9
|
+
|
10
|
+
```ruby
|
11
|
+
gem 'fake-multitenancy'
|
12
|
+
```
|
13
|
+
|
14
|
+
And then execute:
|
15
|
+
|
16
|
+
$ bundle
|
17
|
+
|
18
|
+
Or install it yourself as:
|
19
|
+
|
20
|
+
$ gem install fake-multitenancy
|
21
|
+
|
22
|
+
## Usage
|
23
|
+
|
24
|
+
There's no much to do. Just install the gem and multitenancy will work out of the box. You next tables created with migrations will be multitenant ready.
|
25
|
+
|
26
|
+
You just need to have a model named Tenant. This class should respond to an instance method called `name` returning a String. Tenant should have a class method named "current" that returns an instance of Tenant.
|
27
|
+
|
28
|
+
While you should consider a database powered solution, here is the simplest implementation to test this gem :
|
29
|
+
|
30
|
+
```ruby
|
31
|
+
class Tenant
|
32
|
+
def switch
|
33
|
+
@@current = self
|
34
|
+
end
|
35
|
+
|
36
|
+
attr_accessor :name
|
37
|
+
|
38
|
+
|
39
|
+
def self.find(name)
|
40
|
+
new.tap{ |t| t.name = name }
|
41
|
+
end
|
42
|
+
|
43
|
+
def self.current
|
44
|
+
@@current
|
45
|
+
end
|
46
|
+
end
|
47
|
+
```
|
48
|
+
|
49
|
+
## Internals
|
50
|
+
|
51
|
+
|
52
|
+
This gem does the followings :
|
53
|
+
- Adds columns `multitenant_id` and `tenant` to all your tables (via migrations) ;
|
54
|
+
- It turns the `id` column to an indexed integer, unique, not primary key, that is assigned in a callback
|
55
|
+
- Exclude tables from multitenancy when the parameter `multitenant: false` is set on create_table calls ;
|
56
|
+
- Add a default_scope to all you models inheriting from ActiveRecord::Base ;
|
57
|
+
- Modify schema.rb generation internals by excluding the multitenancy.
|
58
|
+
|
59
|
+
## Development
|
60
|
+
|
61
|
+
After checking out the repo, run `bin/setup` to install dependencies. Then, run `bin/console` for an interactive prompt that will allow you to experiment.
|
62
|
+
|
63
|
+
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` to create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
|
64
|
+
|
65
|
+
## Contributing
|
66
|
+
|
67
|
+
1. Fork it ( https://github.com/[my-github-username]/fake-multitenancy/fork )
|
68
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
69
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
70
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
71
|
+
5. Create a new Pull Request
|
data/Rakefile
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require "bundler/gem_tasks"
|
data/bin/console
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require "bundler/setup"
|
4
|
+
require "fake/multitenancy"
|
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
ADDED
@@ -0,0 +1,27 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'fake/multitenancy/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "fake-multitenancy"
|
8
|
+
spec.version = Fake::Multitenancy::VERSION
|
9
|
+
spec.authors = ["Ihcène Medjber"]
|
10
|
+
spec.email = ["ihcene@aritylabs.com"]
|
11
|
+
|
12
|
+
spec.summary = %q{Serve several clients with one single database.}
|
13
|
+
spec.description = %q{Serve several clients with one single database with incremental and secure ids by tenant.}
|
14
|
+
spec.homepage = "http://github.com/ihcene/fake-multitenancy"
|
15
|
+
|
16
|
+
spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
|
17
|
+
spec.bindir = "exe"
|
18
|
+
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
19
|
+
spec.require_paths = ["lib"]
|
20
|
+
|
21
|
+
spec.add_dependency "activerecord", "~> 4.2"
|
22
|
+
|
23
|
+
spec.add_development_dependency "bundler", "~> 1.9"
|
24
|
+
spec.add_development_dependency "rake", "~> 10.0"
|
25
|
+
spec.add_development_dependency 'rspec', '~> 3.2', '>= 3.2.0'
|
26
|
+
spec.add_development_dependency 'sqlite3', '~> 1.3'
|
27
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
require "fake/multitenancy/version"
|
2
|
+
require "active_record"
|
3
|
+
require "active_support"
|
4
|
+
require "active_support/core_ext"
|
5
|
+
|
6
|
+
module Fake
|
7
|
+
module Multitenancy
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
require 'fake/multitenancy/abstract_adapter'
|
12
|
+
require 'fake/multitenancy/active_record/base/multitenancy'
|
13
|
+
require 'fake/multitenancy/active_record/schema_dumper'
|
14
|
+
require 'fake/multitenancy/active_record/base'
|
@@ -0,0 +1,22 @@
|
|
1
|
+
class ActiveRecord::ConnectionAdapters::AbstractAdapter
|
2
|
+
concerning :MultitenantSchemaStatements do
|
3
|
+
def create_table(table_name, options = {}, &block)
|
4
|
+
multitenant = options.fetch(:multitenant, true) && table_name != "schema_migrations"
|
5
|
+
|
6
|
+
if multitenant
|
7
|
+
super table_name, options.merge(primary_key: :multitenant_id) do |t|
|
8
|
+
t.integer :id, null: false, unique: true
|
9
|
+
t.string :tenant, null: false
|
10
|
+
|
11
|
+
block.call(t) if block_given?
|
12
|
+
end
|
13
|
+
|
14
|
+
add_index(table_name, [:id, :tenant], unique: true)
|
15
|
+
add_index(table_name, :id)
|
16
|
+
add_index(table_name, :tenant)
|
17
|
+
else
|
18
|
+
super(table_name, options, &block)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
class ActiveRecord::Base
|
2
|
+
def self.inherited(klass)
|
3
|
+
super
|
4
|
+
|
5
|
+
return if klass.name.eql?('SchemaMigration')
|
6
|
+
|
7
|
+
klass.class_eval do
|
8
|
+
default_scope do
|
9
|
+
if multitenant?
|
10
|
+
where(tenant: Tenant.current.name)
|
11
|
+
else
|
12
|
+
scoped
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
ActiveRecord::Base.instance_eval do
|
2
|
+
concerning :Multitenacy do
|
3
|
+
included do
|
4
|
+
before_create do
|
5
|
+
if self.class.multitenant?
|
6
|
+
self.tenant = Tenant.current.name
|
7
|
+
self.id = (self.class.where(tenant: tenant).maximum(:id) || 0) + 1
|
8
|
+
end
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
def save
|
13
|
+
if self.class.multitenant? && !persisted?
|
14
|
+
3.times do
|
15
|
+
begin
|
16
|
+
super
|
17
|
+
break
|
18
|
+
|
19
|
+
rescue ActiveRecord::RecordNotUnique
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
else
|
24
|
+
super
|
25
|
+
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
module ClassMethods
|
30
|
+
def primary_key
|
31
|
+
multitenant? ? "id" : super
|
32
|
+
end
|
33
|
+
|
34
|
+
def multitenant?
|
35
|
+
columns.map(&:name).include?("tenant")
|
36
|
+
end
|
37
|
+
|
38
|
+
def find_by_sql(*)
|
39
|
+
if multitenant? && !Rails.env.production?
|
40
|
+
Rails.logger.warn "WARNING : You are using find_by_sql. Make sure you include a multitenacy limitation clause: \"where `tenant` = :tenant, tenant: Tenant.current.name\""
|
41
|
+
end
|
42
|
+
|
43
|
+
super
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,118 @@
|
|
1
|
+
class ActiveRecord::SchemaDumper
|
2
|
+
def table(table, stream)
|
3
|
+
columns = @connection.columns(table)
|
4
|
+
begin
|
5
|
+
tbl = StringIO.new
|
6
|
+
|
7
|
+
# first dump primary key column
|
8
|
+
if @connection.respond_to?(:pk_and_sequence_for)
|
9
|
+
pk, _ = @connection.pk_and_sequence_for(table)
|
10
|
+
elsif @connection.respond_to?(:primary_key)
|
11
|
+
pk = @connection.primary_key(table)
|
12
|
+
end
|
13
|
+
|
14
|
+
multitenant = (pk == 'multitenant_id')
|
15
|
+
|
16
|
+
tbl.print " create_table #{remove_prefix_and_suffix(table).inspect}"
|
17
|
+
pkcol = columns.detect { |c| c.name == pk }
|
18
|
+
if pkcol
|
19
|
+
unless multitenant
|
20
|
+
tbl.print %Q(, multitenant: false)
|
21
|
+
end
|
22
|
+
|
23
|
+
if pk != 'id' && pk != 'multitenant_id'
|
24
|
+
tbl.print %Q(, primary_key: "#{pk}")
|
25
|
+
elsif pkcol.sql_type == 'uuid'
|
26
|
+
tbl.print ", id: :uuid"
|
27
|
+
tbl.print %Q(, default: "#{pkcol.default_function}") if pkcol.default_function
|
28
|
+
else
|
29
|
+
end
|
30
|
+
else
|
31
|
+
tbl.print ", id: false"
|
32
|
+
end
|
33
|
+
tbl.print ", force: true"
|
34
|
+
tbl.puts " do |t|"
|
35
|
+
|
36
|
+
# then dump all non-primary key columns
|
37
|
+
column_specs = columns.map do |column|
|
38
|
+
raise StandardError, "Unknown type '#{column.sql_type}' for column '#{column.name}'" unless @connection.valid_type?(column.type)
|
39
|
+
next if column.name == pk
|
40
|
+
@connection.column_spec(column, @types)
|
41
|
+
end.compact
|
42
|
+
|
43
|
+
# find all migration keys used in this table
|
44
|
+
keys = @connection.migration_keys
|
45
|
+
|
46
|
+
# figure out the lengths for each column based on above keys
|
47
|
+
lengths = keys.map { |key|
|
48
|
+
column_specs.map { |spec|
|
49
|
+
spec[key] ? spec[key].length + 2 : 0
|
50
|
+
}.max
|
51
|
+
}
|
52
|
+
|
53
|
+
# the string we're going to sprintf our values against, with standardized column widths
|
54
|
+
format_string = lengths.map{ |len| "%-#{len}s" }
|
55
|
+
|
56
|
+
# find the max length for the 'type' column, which is special
|
57
|
+
type_length = column_specs.map{ |column| column[:type].length }.max
|
58
|
+
|
59
|
+
# add column type definition to our format string
|
60
|
+
format_string.unshift " t.%-#{type_length}s "
|
61
|
+
|
62
|
+
format_string *= ''
|
63
|
+
|
64
|
+
column_specs.each do |colspec|
|
65
|
+
next if multitenant && colspec[:name].in?(['"id"', '"tenant"'])
|
66
|
+
|
67
|
+
values = keys.zip(lengths).map{ |key, len| colspec.key?(key) ? colspec[key] + ", " : " " * len }
|
68
|
+
values.unshift colspec[:type]
|
69
|
+
tbl.print((format_string % values).gsub(/,\s*$/, ''))
|
70
|
+
tbl.puts
|
71
|
+
end
|
72
|
+
|
73
|
+
tbl.puts " end"
|
74
|
+
tbl.puts
|
75
|
+
|
76
|
+
indexes(table, tbl)
|
77
|
+
|
78
|
+
tbl.rewind
|
79
|
+
stream.print tbl.read
|
80
|
+
rescue => e
|
81
|
+
stream.puts "# Could not dump table #{table.inspect} because of following #{e.class}"
|
82
|
+
stream.puts "# #{e.message}"
|
83
|
+
stream.puts
|
84
|
+
end
|
85
|
+
|
86
|
+
stream
|
87
|
+
end
|
88
|
+
|
89
|
+
def indexes(table, stream)
|
90
|
+
indexes = @connection.indexes(table)
|
91
|
+
.reject{ |index| index.columns.in?([['id'], ['tenant'] ,['id', 'tenant']]) }
|
92
|
+
|
93
|
+
if indexes.any?
|
94
|
+
add_index_statements = indexes.map do |index|
|
95
|
+
statement_parts = [
|
96
|
+
"add_index #{remove_prefix_and_suffix(index.table).inspect}",
|
97
|
+
index.columns.inspect,
|
98
|
+
"name: #{index.name.inspect}",
|
99
|
+
]
|
100
|
+
statement_parts << 'unique: true' if index.unique
|
101
|
+
|
102
|
+
index_lengths = (index.lengths || []).compact
|
103
|
+
statement_parts << "length: #{Hash[index.columns.zip(index.lengths)].inspect}" if index_lengths.any?
|
104
|
+
|
105
|
+
index_orders = index.orders || {}
|
106
|
+
statement_parts << "order: #{index.orders.inspect}" if index_orders.any?
|
107
|
+
statement_parts << "where: #{index.where.inspect}" if index.where
|
108
|
+
statement_parts << "using: #{index.using.inspect}" if index.using
|
109
|
+
statement_parts << "type: #{index.type.inspect}" if index.type
|
110
|
+
|
111
|
+
" #{statement_parts.join(', ')}"
|
112
|
+
end
|
113
|
+
|
114
|
+
stream.puts add_index_statements.sort.join("\n")
|
115
|
+
stream.puts
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|
metadata
ADDED
@@ -0,0 +1,136 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: fake-multitenancy
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Ihcène Medjber
|
8
|
+
autorequire:
|
9
|
+
bindir: exe
|
10
|
+
cert_chain: []
|
11
|
+
date: 2015-03-30 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: activerecord
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ~>
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '4.2'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ~>
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '4.2'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: bundler
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ~>
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '1.9'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ~>
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '1.9'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: rake
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ~>
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '10.0'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ~>
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '10.0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: rspec
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - ~>
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '3.2'
|
62
|
+
- - '>='
|
63
|
+
- !ruby/object:Gem::Version
|
64
|
+
version: 3.2.0
|
65
|
+
type: :development
|
66
|
+
prerelease: false
|
67
|
+
version_requirements: !ruby/object:Gem::Requirement
|
68
|
+
requirements:
|
69
|
+
- - ~>
|
70
|
+
- !ruby/object:Gem::Version
|
71
|
+
version: '3.2'
|
72
|
+
- - '>='
|
73
|
+
- !ruby/object:Gem::Version
|
74
|
+
version: 3.2.0
|
75
|
+
- !ruby/object:Gem::Dependency
|
76
|
+
name: sqlite3
|
77
|
+
requirement: !ruby/object:Gem::Requirement
|
78
|
+
requirements:
|
79
|
+
- - ~>
|
80
|
+
- !ruby/object:Gem::Version
|
81
|
+
version: '1.3'
|
82
|
+
type: :development
|
83
|
+
prerelease: false
|
84
|
+
version_requirements: !ruby/object:Gem::Requirement
|
85
|
+
requirements:
|
86
|
+
- - ~>
|
87
|
+
- !ruby/object:Gem::Version
|
88
|
+
version: '1.3'
|
89
|
+
description: Serve several clients with one single database with incremental and secure
|
90
|
+
ids by tenant.
|
91
|
+
email:
|
92
|
+
- ihcene@aritylabs.com
|
93
|
+
executables: []
|
94
|
+
extensions: []
|
95
|
+
extra_rdoc_files: []
|
96
|
+
files:
|
97
|
+
- .gitignore
|
98
|
+
- .rspec
|
99
|
+
- .ruby-version
|
100
|
+
- .travis.yml
|
101
|
+
- Gemfile
|
102
|
+
- README.md
|
103
|
+
- Rakefile
|
104
|
+
- bin/console
|
105
|
+
- bin/setup
|
106
|
+
- fake-multitenancy.gemspec
|
107
|
+
- lib/fake/multitenancy.rb
|
108
|
+
- lib/fake/multitenancy/abstract_adapter.rb
|
109
|
+
- lib/fake/multitenancy/active_record/base.rb
|
110
|
+
- lib/fake/multitenancy/active_record/base/multitenancy.rb
|
111
|
+
- lib/fake/multitenancy/active_record/schema_dumper.rb
|
112
|
+
- lib/fake/multitenancy/version.rb
|
113
|
+
homepage: http://github.com/ihcene/fake-multitenancy
|
114
|
+
licenses: []
|
115
|
+
metadata: {}
|
116
|
+
post_install_message:
|
117
|
+
rdoc_options: []
|
118
|
+
require_paths:
|
119
|
+
- lib
|
120
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
121
|
+
requirements:
|
122
|
+
- - '>='
|
123
|
+
- !ruby/object:Gem::Version
|
124
|
+
version: '0'
|
125
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
126
|
+
requirements:
|
127
|
+
- - '>='
|
128
|
+
- !ruby/object:Gem::Version
|
129
|
+
version: '0'
|
130
|
+
requirements: []
|
131
|
+
rubyforge_project:
|
132
|
+
rubygems_version: 2.0.14
|
133
|
+
signing_key:
|
134
|
+
specification_version: 4
|
135
|
+
summary: Serve several clients with one single database.
|
136
|
+
test_files: []
|