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 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: []