tenant_realm 1.0.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/.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: []
|