fake-multitenancy 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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
@@ -0,0 +1,9 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --format documentation
2
+ --color
data/.ruby-version ADDED
@@ -0,0 +1 @@
1
+ 2.0.0-p643
data/.travis.yml ADDED
@@ -0,0 +1,3 @@
1
+ language: ruby
2
+ rvm:
3
+ - 2.2.1
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in fake-multitenancy.gemspec
4
+ gemspec
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,7 @@
1
+ #!/bin/bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+
5
+ bundle install
6
+
7
+ # Do any other automated setup that you need to do here
@@ -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
@@ -0,0 +1,5 @@
1
+ module Fake
2
+ module Multitenancy
3
+ VERSION = "0.1.0"
4
+ end
5
+ 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: []