tenant_realm 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.ruby-version +1 -0
- data/README.md +121 -0
- data/Rakefile +8 -0
- data/lib/generators/templates/tenant_realm.tt +31 -0
- data/lib/generators/tenant_realm_generator.rb +12 -0
- data/lib/tasks/migrate.rake +30 -0
- data/lib/tenant_realm/cache/base_cache.rb +54 -0
- data/lib/tenant_realm/cache/kredis_cache.rb +51 -0
- data/lib/tenant_realm/class_methods.rb +13 -0
- data/lib/tenant_realm/config.rb +25 -0
- data/lib/tenant_realm/configuration/cache.rb +14 -0
- data/lib/tenant_realm/db_context.rb +127 -0
- data/lib/tenant_realm/helpers.rb +21 -0
- data/lib/tenant_realm/railtie.rb +11 -0
- data/lib/tenant_realm/tenant.rb +58 -0
- data/lib/tenant_realm/utils.rb +53 -0
- data/lib/tenant_realm/version.rb +5 -0
- data/lib/tenant_realm.rb +65 -0
- data/tenant_realm.gemspec +43 -0
- metadata +63 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: ec5080c5a6b4e997fd094811df564e8cb6c7520030a1fb0617917a155dd05223
|
4
|
+
data.tar.gz: 159100e048f540997d9e9ffa33ba215c035357573bb297a65afa52bd6547d593
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 789611a5e9bc41b794a65e39d4d4cb48a69ad6d37a6d1bde72cb10e6f2bf499278f6715c5f50a7ba26023360408919bf921e33e72b9b994263747582eb0e8dc8
|
7
|
+
data.tar.gz: a7dc2cddb27f123a9772bd53d4e4b1f3d39f9f5cad245f83de9329aa9c3bd6acebde917cb42ce84927b12b896396f8837fe715f5f22ede2cbcec365cb2d72bfc
|
data/.ruby-version
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
3.2.2
|
data/README.md
ADDED
@@ -0,0 +1,121 @@
|
|
1
|
+
Tenant Realm is a lightweight gem provides some helpers to support working on multi-tenant easily using [Multiple Database with Active Record](https://guides.rubyonrails.org/active_record_multiple_databases.html).
|
2
|
+
|
3
|
+
# Installation
|
4
|
+
|
5
|
+
```sh
|
6
|
+
gem 'tenant_realm'
|
7
|
+
```
|
8
|
+
|
9
|
+
# CLI
|
10
|
+
|
11
|
+
- init `tenant_realm`
|
12
|
+
|
13
|
+
```sh
|
14
|
+
rails g tenant_realm
|
15
|
+
```
|
16
|
+
|
17
|
+
- run migration for all tenants
|
18
|
+
|
19
|
+
```sh
|
20
|
+
rake tenant_realm:migrate
|
21
|
+
```
|
22
|
+
|
23
|
+
# Configuration
|
24
|
+
|
25
|
+
```rb
|
26
|
+
# frozen_string_literal: true
|
27
|
+
|
28
|
+
Rails.application.config.to_prepare do
|
29
|
+
TenantRealm.configure do |config|
|
30
|
+
# required
|
31
|
+
# identifier is extracted from `identifier_resolver`
|
32
|
+
config.fetch_tenant = lambda { |identifier|
|
33
|
+
# get tenant using identifier
|
34
|
+
}
|
35
|
+
|
36
|
+
# required
|
37
|
+
config.fetch_tenants = lambda {
|
38
|
+
# get tenant list
|
39
|
+
}
|
40
|
+
|
41
|
+
# optional
|
42
|
+
# default is getting from key :db_config from tenant
|
43
|
+
config.dig_db_config = lambda { |tenant|
|
44
|
+
tenant[:db_config]
|
45
|
+
}
|
46
|
+
|
47
|
+
# optional
|
48
|
+
# set this when you wanna skip the switch database
|
49
|
+
config.skip_resolver = lambda { |request|
|
50
|
+
request.env['REQUEST_PATH'].match?(/health|favicon.ico/)
|
51
|
+
}
|
52
|
+
|
53
|
+
# required
|
54
|
+
# method to extract the identifier of tenant
|
55
|
+
config.identifier_resolver = lambda { |request|
|
56
|
+
# get identifier from request
|
57
|
+
# tenant is retrieved using its domain
|
58
|
+
domain = request.referer || request.origin
|
59
|
+
domain ? URI(domain).host : nil
|
60
|
+
}
|
61
|
+
|
62
|
+
# optional
|
63
|
+
# extract the sharding name for multi_db
|
64
|
+
# default will be this proc return value -> tenant's shard_name -> slug -> id
|
65
|
+
config.shard_name_from_tenant = lambda { |tenant|
|
66
|
+
tenant[:slug]
|
67
|
+
}
|
68
|
+
|
69
|
+
# optional
|
70
|
+
# default will be TenantRealm::CurrentTenant
|
71
|
+
config.current_tenant = MyCurrentTenant
|
72
|
+
|
73
|
+
# optional
|
74
|
+
config.cache do |cache_config|
|
75
|
+
# using cache service
|
76
|
+
cache_config.service = :redis
|
77
|
+
|
78
|
+
# when will the tenant list and tenant are expired
|
79
|
+
cache_config.expires_in = 6.days
|
80
|
+
|
81
|
+
# list keys to cache the tenant
|
82
|
+
cache_config.tenant_uniq_cols = %i[slug domain]
|
83
|
+
|
84
|
+
# in case the column is not simple like above
|
85
|
+
# this option will override the one above
|
86
|
+
cache_config.tenant_keys_resolver = lambda { |tenant|
|
87
|
+
[
|
88
|
+
tenant[:slug],
|
89
|
+
tenant[:settings][:domain]
|
90
|
+
]
|
91
|
+
}
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
95
|
+
```
|
96
|
+
|
97
|
+
# Customize
|
98
|
+
|
99
|
+
- Custom current tenant
|
100
|
+
|
101
|
+
```rb
|
102
|
+
# frozen_string_literal: true
|
103
|
+
|
104
|
+
class CurrentTenant < TenantRealm::CurrentTenant
|
105
|
+
attribute :additional_info
|
106
|
+
|
107
|
+
def tenant=(tenant)
|
108
|
+
super
|
109
|
+
|
110
|
+
self.additional_info = {
|
111
|
+
domain: tenant[:settings][:domain]
|
112
|
+
}
|
113
|
+
end
|
114
|
+
end
|
115
|
+
```
|
116
|
+
|
117
|
+
# TODO
|
118
|
+
|
119
|
+
- [ ] support Row-Level Security (RLS)
|
120
|
+
|
121
|
+
- [ ] support multi-schema
|
data/Rakefile
ADDED
@@ -0,0 +1,31 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
Rails.application.config.to_prepare do
|
4
|
+
TenantRealm.configure do |config|
|
5
|
+
config.fetch_tenant = lambda { |identifier|
|
6
|
+
# get tenant using identifier
|
7
|
+
}
|
8
|
+
|
9
|
+
config.fetch_tenants = lambda {
|
10
|
+
# get tenant list
|
11
|
+
}
|
12
|
+
|
13
|
+
config.dig_db_config = lambda { |tenant|
|
14
|
+
tenant[:db_config]
|
15
|
+
}
|
16
|
+
|
17
|
+
config.skip_resolver = lambda { |request|
|
18
|
+
request.env['REQUEST_PATH'].match?(/health|favicon.ico/)
|
19
|
+
}
|
20
|
+
|
21
|
+
config.identifier_resolver = lambda { |request|
|
22
|
+
# get identifier from request
|
23
|
+
# domain = request.referer || request.origin
|
24
|
+
# domain ? URI(domain).host : nil
|
25
|
+
}
|
26
|
+
|
27
|
+
config.shard_name_from_tenant = lambda { |tenant|
|
28
|
+
tenant[:slug]
|
29
|
+
}
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class TenantRealmGenerator < Rails::Generators::Base
|
4
|
+
source_root File.expand_path('templates', __dir__)
|
5
|
+
|
6
|
+
def generate_tenant_realm
|
7
|
+
template(
|
8
|
+
'tenant_realm.tt',
|
9
|
+
'config/initializers/tenant_realm.rb'
|
10
|
+
)
|
11
|
+
end
|
12
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
namespace :tenant_realm do
|
4
|
+
desc 'Migrate db for all tenants'
|
5
|
+
task migrate: :environment do
|
6
|
+
tenants = TenantRealm::Tenant.tenants
|
7
|
+
|
8
|
+
tenants.each do |tenant|
|
9
|
+
shard = TenantRealm::Utils.shard_name_from_tenant(tenant:)
|
10
|
+
|
11
|
+
puts "Migrating #{shard}"
|
12
|
+
|
13
|
+
db_config = TenantRealm::Utils.dig_db_config(tenant:)
|
14
|
+
|
15
|
+
if db_config.blank?
|
16
|
+
puts "Skip Migrating #{shard}"
|
17
|
+
|
18
|
+
next
|
19
|
+
end
|
20
|
+
|
21
|
+
db_config = TenantRealm::DbContext.root_db_config.merge(
|
22
|
+
database: db_config[:database]
|
23
|
+
)
|
24
|
+
|
25
|
+
ActiveRecord::Tasks::DatabaseTasks.with_temporary_connection(db_config) do
|
26
|
+
ActiveRecord::Tasks::DatabaseTasks.migrate
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module TenantRealm
|
4
|
+
module Cache
|
5
|
+
class BaseCache
|
6
|
+
class << self
|
7
|
+
def cache_tenants(_tenants)
|
8
|
+
raise NotImplementedError
|
9
|
+
end
|
10
|
+
|
11
|
+
def tenants
|
12
|
+
raise NotImplementedError
|
13
|
+
end
|
14
|
+
|
15
|
+
def cache_tenant(_tenant)
|
16
|
+
raise NotImplementedError
|
17
|
+
end
|
18
|
+
|
19
|
+
def tenant(_identifier)
|
20
|
+
raise NotImplementedError
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
def tenant_unique_keys(tenant)
|
26
|
+
config = Configuration::Cache
|
27
|
+
|
28
|
+
if config.tenant_keys_resolver.present?
|
29
|
+
Helpers.raise_if_not_proc(config.tenant_keys_resolver, 'cache_config.tenant_keys_resolver')
|
30
|
+
Helpers.wrap_array(config.tenant_keys_resolver.call(tenant))
|
31
|
+
elsif config.tenant_uniq_cols.present?
|
32
|
+
cols = Helpers.wrap_array(config.tenant_uniq_cols)
|
33
|
+
|
34
|
+
cols.map do |col|
|
35
|
+
tenant[col]
|
36
|
+
end
|
37
|
+
else
|
38
|
+
[
|
39
|
+
tenant[:slug]
|
40
|
+
]
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def tenants_key
|
45
|
+
'tenant_realm:tenants'
|
46
|
+
end
|
47
|
+
|
48
|
+
def tenant_key
|
49
|
+
'tenant_realm:tenant'
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'redis'
|
4
|
+
require 'kredis'
|
5
|
+
|
6
|
+
require_relative 'base_cache'
|
7
|
+
|
8
|
+
module TenantRealm
|
9
|
+
module Cache
|
10
|
+
class KredisCache < BaseCache
|
11
|
+
class << self
|
12
|
+
def cache_tenants(tenants)
|
13
|
+
return if tenants.blank?
|
14
|
+
|
15
|
+
cached_tenants = tenants_kredis
|
16
|
+
cached_tenants.value = tenants
|
17
|
+
tenants
|
18
|
+
end
|
19
|
+
|
20
|
+
def tenants
|
21
|
+
cached_tenants = tenants_kredis
|
22
|
+
cached_tenants.value&.map(&:deep_symbolize_keys) || []
|
23
|
+
end
|
24
|
+
|
25
|
+
def cache_tenant(tenant)
|
26
|
+
tenant_unique_keys(tenant).each do |key|
|
27
|
+
cached_tenant = tenant_kredis(key)
|
28
|
+
cached_tenant.value = tenant
|
29
|
+
end
|
30
|
+
|
31
|
+
tenant
|
32
|
+
end
|
33
|
+
|
34
|
+
def tenant(identifier)
|
35
|
+
cached_tenant = tenant_kredis(identifier)
|
36
|
+
cached_tenant.value&.deep_symbolize_keys
|
37
|
+
end
|
38
|
+
|
39
|
+
private
|
40
|
+
|
41
|
+
def tenants_kredis
|
42
|
+
Kredis.json(tenants_key, expires_in: Configuration::Cache.expires_in)
|
43
|
+
end
|
44
|
+
|
45
|
+
def tenant_kredis(identifier)
|
46
|
+
Kredis.json("#{tenant_key}:#{identifier}", expires_in: Configuration::Cache.expires_in)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module TenantRealm
|
4
|
+
class Config
|
5
|
+
class << self
|
6
|
+
attr_accessor(
|
7
|
+
:fetch_tenant,
|
8
|
+
:fetch_tenants,
|
9
|
+
:dig_db_config,
|
10
|
+
:skip_resolver,
|
11
|
+
:current_tenant,
|
12
|
+
:identifier_resolver,
|
13
|
+
:shard_name_from_tenant
|
14
|
+
)
|
15
|
+
|
16
|
+
def cache
|
17
|
+
yield Configuration::Cache
|
18
|
+
end
|
19
|
+
|
20
|
+
def current
|
21
|
+
@current ||= current_tenant || CurrentTenant
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,127 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'utils'
|
4
|
+
require_relative 'tenant'
|
5
|
+
|
6
|
+
module TenantRealm
|
7
|
+
class DbContext
|
8
|
+
# to cache all tenant's connections
|
9
|
+
@@shards = {}
|
10
|
+
|
11
|
+
# to cache all tenant's connections already connected
|
12
|
+
@@connected_shards = nil
|
13
|
+
|
14
|
+
class << self
|
15
|
+
def root_db_config
|
16
|
+
@@db_config ||= ActiveRecord::Base.connection_db_config.configuration_hash.deep_dup.freeze
|
17
|
+
end
|
18
|
+
|
19
|
+
def init_shards
|
20
|
+
config = Utils.load_database_yml
|
21
|
+
tenants = Tenant.tenants
|
22
|
+
|
23
|
+
@@shards = config[Rails.env].keys.each_with_object({}) do |shard, shards|
|
24
|
+
shards[shard] = shard
|
25
|
+
end
|
26
|
+
|
27
|
+
tenants.each do |tenant|
|
28
|
+
db_config = Utils.dig_db_config(tenant:)
|
29
|
+
shard = Utils.shard_name_from_tenant(tenant:)
|
30
|
+
next if db_config.blank?
|
31
|
+
|
32
|
+
add_shard(shard:, db_config:)
|
33
|
+
end
|
34
|
+
|
35
|
+
ActiveRecord::Base.connects_to(shards: connected_shards)
|
36
|
+
end
|
37
|
+
|
38
|
+
def connected_shards
|
39
|
+
@@connected_shards ||= build_connected_shards
|
40
|
+
end
|
41
|
+
|
42
|
+
def add_shard(shard:, db_config:)
|
43
|
+
return if shard_exist?(shard:) || db_config.blank?
|
44
|
+
|
45
|
+
shard_name = shard.underscore
|
46
|
+
config = Utils.load_database_yml
|
47
|
+
tenant_shard_config = root_db_config.merge(database: db_config[:database])
|
48
|
+
config[Rails.env][shard_name] = JSON.parse(tenant_shard_config.to_json)
|
49
|
+
@@shards[shard_name] = shard_name
|
50
|
+
|
51
|
+
# sometimes the dynamic tenant's db_config causes connection pool checkout
|
52
|
+
# write tenants' db_config to database.yml
|
53
|
+
# to fix this problem when restart server
|
54
|
+
File.open('config/database.yml', 'w') do |f|
|
55
|
+
YAML.dump(config, f)
|
56
|
+
end
|
57
|
+
|
58
|
+
ActiveRecord::Base.configurations.configurations << ActiveRecord::DatabaseConfigurations::HashConfig.new(
|
59
|
+
Rails.env,
|
60
|
+
shard_name,
|
61
|
+
tenant_shard_config
|
62
|
+
)
|
63
|
+
|
64
|
+
sym_shard = shard_name.to_sym
|
65
|
+
|
66
|
+
connected_shards[sym_shard] = {
|
67
|
+
writing: sym_shard,
|
68
|
+
reading: sym_shard
|
69
|
+
}
|
70
|
+
end
|
71
|
+
|
72
|
+
def shard_exist?(shard:)
|
73
|
+
@@shards.key?(shard.underscore)
|
74
|
+
end
|
75
|
+
|
76
|
+
def switch_database(shard:, db_config: nil, &block)
|
77
|
+
add_shard(shard:, db_config:)
|
78
|
+
|
79
|
+
ActiveRecord::Base.connected_to(shard: shard.underscore.to_sym, &block)
|
80
|
+
end
|
81
|
+
|
82
|
+
def flush_connection!
|
83
|
+
ActiveRecord::Base.connection_handler.clear_active_connections!(:all)
|
84
|
+
ActiveRecord::Base.connection_handler.flush_idle_connections!(:all)
|
85
|
+
end
|
86
|
+
|
87
|
+
def run_migrate(db_config:)
|
88
|
+
ActiveRecord::Tasks::DatabaseTasks.with_temporary_connection(
|
89
|
+
root_db_config.merge(database: db_config[:database])
|
90
|
+
) do
|
91
|
+
ActiveRecord::Tasks::DatabaseTasks.migrate
|
92
|
+
end
|
93
|
+
|
94
|
+
false
|
95
|
+
rescue StandardError
|
96
|
+
flush_connection!
|
97
|
+
|
98
|
+
true
|
99
|
+
end
|
100
|
+
|
101
|
+
def create_db(shard:, affix: nil)
|
102
|
+
database = [shard, affix].compact.join('-').underscore
|
103
|
+
db_config = root_db_config.merge(database:).tap do |config|
|
104
|
+
config[:host] ||= 'localhost'
|
105
|
+
config[:port] ||= 3306
|
106
|
+
end
|
107
|
+
|
108
|
+
ActiveRecord::Base.connection.create_database(database)
|
109
|
+
|
110
|
+
db_config
|
111
|
+
end
|
112
|
+
|
113
|
+
private
|
114
|
+
|
115
|
+
def build_connected_shards
|
116
|
+
@@shards.keys.each_with_object({}) do |shard, shards|
|
117
|
+
sym_shard = shard.to_sym
|
118
|
+
|
119
|
+
shards[sym_shard] = {
|
120
|
+
writing: sym_shard,
|
121
|
+
reading: sym_shard
|
122
|
+
}
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|
126
|
+
end
|
127
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module TenantRealm
|
4
|
+
class Helpers
|
5
|
+
class << self
|
6
|
+
def wrap_array(data)
|
7
|
+
return [] if data.blank?
|
8
|
+
|
9
|
+
data.is_a?(Array) ? data : [data]
|
10
|
+
end
|
11
|
+
|
12
|
+
def dev_log(message)
|
13
|
+
p message if Rails.env.development?
|
14
|
+
end
|
15
|
+
|
16
|
+
def raise_if_not_proc(source, name)
|
17
|
+
raise Error, "#{name} must be a Proc" unless source.is_a?(Proc)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'cache/kredis_cache'
|
4
|
+
|
5
|
+
module TenantRealm
|
6
|
+
class Tenant
|
7
|
+
@@cache = {
|
8
|
+
redis: Cache::KredisCache,
|
9
|
+
kredis: Cache::KredisCache
|
10
|
+
}
|
11
|
+
|
12
|
+
class << self
|
13
|
+
def tenants
|
14
|
+
if cache.present?
|
15
|
+
tenants = cache.tenants
|
16
|
+
return tenants if tenants.present?
|
17
|
+
|
18
|
+
tenants = Utils.fetch_tenants
|
19
|
+
cache.cache_tenants(tenants)
|
20
|
+
tenants
|
21
|
+
else
|
22
|
+
Utils.fetch_tenants
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def tenant(identifier)
|
27
|
+
if cache.present?
|
28
|
+
tenant = cache.tenant(identifier)
|
29
|
+
return tenant if tenant.present?
|
30
|
+
|
31
|
+
tenant = Utils.fetch_tenant(identifier)
|
32
|
+
cache.cache_tenant(tenant)
|
33
|
+
tenant
|
34
|
+
else
|
35
|
+
Utils.fetch_tenant
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def cache_tenants(tenants)
|
40
|
+
return Helpers.dev_log('Tenant Realm: Skip cache tenants because cache not configured') if cache.blank?
|
41
|
+
|
42
|
+
cache.cache_tenants(tenants)
|
43
|
+
end
|
44
|
+
|
45
|
+
def cache_tenant(tenant)
|
46
|
+
return Helpers.dev_log('Tenant Realm: Skip cache tenant because cache not configured') if cache.blank?
|
47
|
+
|
48
|
+
cache.cache_tenant(tenant)
|
49
|
+
end
|
50
|
+
|
51
|
+
private
|
52
|
+
|
53
|
+
def cache
|
54
|
+
@cache ||= @@cache[Configuration::Cache.service]
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module TenantRealm
|
4
|
+
class Utils
|
5
|
+
class << self
|
6
|
+
def load_database_yml
|
7
|
+
YAML.load_file('config/database.yml', aliases: true)
|
8
|
+
end
|
9
|
+
|
10
|
+
def fetch_tenants
|
11
|
+
Helpers.raise_if_not_proc(Config.fetch_tenants, 'config.fetch_tenants')
|
12
|
+
|
13
|
+
(Config.fetch_tenants.call.presence || []).map(&:deep_symbolize_keys)
|
14
|
+
end
|
15
|
+
|
16
|
+
def fetch_tenant(identifier)
|
17
|
+
Helpers.raise_if_not_proc(Config.fetch_tenant, 'config.fetch_tenant')
|
18
|
+
|
19
|
+
Config.fetch_tenant.call(identifier)&.deep_symbolize_keys
|
20
|
+
end
|
21
|
+
|
22
|
+
def dig_db_config(tenant:)
|
23
|
+
if Config.dig_db_config.is_a?(Proc)
|
24
|
+
Config.dig_db_config.call(tenant)
|
25
|
+
else
|
26
|
+
key = Config.dig_db_config || :db_config
|
27
|
+
|
28
|
+
tenant[key.to_sym]
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def shard_name_from_tenant(tenant:)
|
33
|
+
shard = if Config.shard_name_from_tenant.is_a?(Proc)
|
34
|
+
Config.shard_name_from_tenant.call(tenant)
|
35
|
+
else
|
36
|
+
key = Config.shard_name_from_tenant || :shard_name
|
37
|
+
|
38
|
+
tenant[key.to_sym] || tenant[:slug] || tenant[:id]
|
39
|
+
end
|
40
|
+
|
41
|
+
shard.underscore
|
42
|
+
end
|
43
|
+
|
44
|
+
def identifier_resolver(request)
|
45
|
+
raise Error, 'config.identifier_resolver must be provided' if Config.identifier_resolver.blank?
|
46
|
+
|
47
|
+
Helpers.raise_if_not_proc(Config.identifier_resolver, 'config.identifier_resolver')
|
48
|
+
|
49
|
+
Config.identifier_resolver.call(request)
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
data/lib/tenant_realm.rb
ADDED
@@ -0,0 +1,65 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'tenant_realm/config'
|
4
|
+
require_relative 'tenant_realm/version'
|
5
|
+
require_relative 'tenant_realm/helpers'
|
6
|
+
require_relative 'tenant_realm/db_context'
|
7
|
+
require_relative 'tenant_realm/class_methods'
|
8
|
+
require_relative 'tenant_realm/configuration/cache'
|
9
|
+
|
10
|
+
module TenantRealm
|
11
|
+
require 'tenant_realm/railtie' if defined?(Rails)
|
12
|
+
|
13
|
+
class Error < StandardError; end
|
14
|
+
|
15
|
+
class CurrentTenant < ActiveSupport::CurrentAttributes
|
16
|
+
attribute :tenant
|
17
|
+
end
|
18
|
+
|
19
|
+
class Railtie < Rails::Railtie
|
20
|
+
config.before_configuration do
|
21
|
+
Helpers.dev_log('Tenant Realm: Init shard resolver')
|
22
|
+
|
23
|
+
config.active_record.shard_selector = { lock: false }
|
24
|
+
config.active_record.shard_resolver = lambda { |request|
|
25
|
+
skip_switch_db = if Config.skip_resolver.is_a?(Proc)
|
26
|
+
Config.skip_resolver.call(request)
|
27
|
+
else
|
28
|
+
false
|
29
|
+
end
|
30
|
+
|
31
|
+
return :primary if skip_switch_db
|
32
|
+
|
33
|
+
identifier = Utils.identifier_resolver(request)
|
34
|
+
tenant = Tenant.tenant(identifier)
|
35
|
+
db_config = Utils.dig_db_config(tenant:)
|
36
|
+
shard = Utils.shard_name_from_tenant(tenant:)
|
37
|
+
|
38
|
+
return :primary if db_config.blank?
|
39
|
+
|
40
|
+
Config.current.tenant = tenant
|
41
|
+
DbContext.add_shard(shard:, db_config:)
|
42
|
+
ActiveRecord::Base.connects_to(shards: DbContext.connected_shards)
|
43
|
+
|
44
|
+
shard
|
45
|
+
}
|
46
|
+
end
|
47
|
+
|
48
|
+
initializer 'active_record.setup_tenant_realm' do
|
49
|
+
Helpers.dev_log('Tenant Realm: Init database shards')
|
50
|
+
|
51
|
+
ActiveSupport.on_load(:active_record) do
|
52
|
+
ActiveRecord::Base.default_shard = :primary
|
53
|
+
ActiveRecord::Base.connects_to(
|
54
|
+
shards: {
|
55
|
+
primary: { writing: :primary, reading: :primary }
|
56
|
+
}
|
57
|
+
)
|
58
|
+
|
59
|
+
DbContext.init_shards
|
60
|
+
rescue ActiveRecord::ActiveRecordError
|
61
|
+
nil
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'lib/tenant_realm/version'
|
4
|
+
|
5
|
+
Gem::Specification.new do |spec|
|
6
|
+
spec.name = 'tenant_realm'
|
7
|
+
spec.version = TenantRealm::VERSION
|
8
|
+
spec.authors = ['Alpha']
|
9
|
+
spec.email = ['alphanolucifer@gmail.com']
|
10
|
+
|
11
|
+
spec.summary = 'Ruby on Rails gem to support multi-tenant'
|
12
|
+
spec.description = 'Ruby on Rails gem to support multi-tenant'
|
13
|
+
spec.homepage = 'https://github.com/zgid123/tenant_realm'
|
14
|
+
spec.license = 'MIT'
|
15
|
+
spec.required_ruby_version = '>= 3.1.0'
|
16
|
+
|
17
|
+
spec.metadata['homepage_uri'] = spec.homepage
|
18
|
+
|
19
|
+
spec.files = Dir.chdir(__dir__) do
|
20
|
+
`git ls-files -z`.split("\x0").reject do |f|
|
21
|
+
(File.expand_path(f) == __FILE__) ||
|
22
|
+
f.start_with?(
|
23
|
+
*%w[
|
24
|
+
bin/
|
25
|
+
test/
|
26
|
+
spec/
|
27
|
+
features/
|
28
|
+
.git
|
29
|
+
.circleci
|
30
|
+
appveyor
|
31
|
+
examples/
|
32
|
+
Gemfile
|
33
|
+
.rubocop.yml
|
34
|
+
.vscode/settings.json
|
35
|
+
LICENSE.txt
|
36
|
+
lefthook.yml
|
37
|
+
]
|
38
|
+
)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
spec.require_paths = ['lib']
|
43
|
+
end
|
metadata
ADDED
@@ -0,0 +1,63 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: tenant_realm
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.0.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Alpha
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2024-01-27 00:00:00.000000000 Z
|
12
|
+
dependencies: []
|
13
|
+
description: Ruby on Rails gem to support multi-tenant
|
14
|
+
email:
|
15
|
+
- alphanolucifer@gmail.com
|
16
|
+
executables: []
|
17
|
+
extensions: []
|
18
|
+
extra_rdoc_files: []
|
19
|
+
files:
|
20
|
+
- ".ruby-version"
|
21
|
+
- README.md
|
22
|
+
- Rakefile
|
23
|
+
- lib/generators/templates/tenant_realm.tt
|
24
|
+
- lib/generators/tenant_realm_generator.rb
|
25
|
+
- lib/tasks/migrate.rake
|
26
|
+
- lib/tenant_realm.rb
|
27
|
+
- lib/tenant_realm/cache/base_cache.rb
|
28
|
+
- lib/tenant_realm/cache/kredis_cache.rb
|
29
|
+
- lib/tenant_realm/class_methods.rb
|
30
|
+
- lib/tenant_realm/config.rb
|
31
|
+
- lib/tenant_realm/configuration/cache.rb
|
32
|
+
- lib/tenant_realm/db_context.rb
|
33
|
+
- lib/tenant_realm/helpers.rb
|
34
|
+
- lib/tenant_realm/railtie.rb
|
35
|
+
- lib/tenant_realm/tenant.rb
|
36
|
+
- lib/tenant_realm/utils.rb
|
37
|
+
- lib/tenant_realm/version.rb
|
38
|
+
- tenant_realm.gemspec
|
39
|
+
homepage: https://github.com/zgid123/tenant_realm
|
40
|
+
licenses:
|
41
|
+
- MIT
|
42
|
+
metadata:
|
43
|
+
homepage_uri: https://github.com/zgid123/tenant_realm
|
44
|
+
post_install_message:
|
45
|
+
rdoc_options: []
|
46
|
+
require_paths:
|
47
|
+
- lib
|
48
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
49
|
+
requirements:
|
50
|
+
- - ">="
|
51
|
+
- !ruby/object:Gem::Version
|
52
|
+
version: 3.1.0
|
53
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
54
|
+
requirements:
|
55
|
+
- - ">="
|
56
|
+
- !ruby/object:Gem::Version
|
57
|
+
version: '0'
|
58
|
+
requirements: []
|
59
|
+
rubygems_version: 3.4.13
|
60
|
+
signing_key:
|
61
|
+
specification_version: 4
|
62
|
+
summary: Ruby on Rails gem to support multi-tenant
|
63
|
+
test_files: []
|