activerecord-tenanted 0.8.0 → 0.9.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 +4 -4
- data/lib/active_record/tenanted/associations.rb +1 -1
- data/lib/active_record/tenanted/database_adapters/base.rb +27 -0
- data/lib/active_record/tenanted/database_adapters/sqlite.rb +24 -13
- data/lib/active_record/tenanted/database_configurations/base_config.rb +10 -2
- data/lib/active_record/tenanted/storage.rb +6 -0
- data/lib/active_record/tenanted/tenant.rb +17 -3
- data/lib/active_record/tenanted/version.rb +1 -1
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 92b0d3851167bdcfb0abd394b770ad1d13474c3cf9716caed9f0bf3b9a49a226
|
|
4
|
+
data.tar.gz: 5cb5eee41c5e6723042c536fe04db854519a076e2223ac60bd5df862185978c5
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 42beaf7932556597872dc01cff858c4f934f1f545e114f3fa0e48890d06d962847a34ec40ac81dab6fa8d3bde253a62ffa41b1645d3a5fe2f07aa2c0f6e670fd
|
|
7
|
+
data.tar.gz: 1a117271fc26dfc6950669a6a2ccf2adac1d017ae00a30d206e540fce3325ff80d5822902b9cc8f35f85359349cb7e5c66dc061397c43ef610c30f3e56b5e7e3
|
|
@@ -11,7 +11,7 @@ module ActiveRecord
|
|
|
11
11
|
module Associations # :nodoc:
|
|
12
12
|
# Every query built on behalf of an association funnels through here, including the ones
|
|
13
13
|
# issued by a collection proxy that has outlived the tenant context it was created in.
|
|
14
|
-
def scope
|
|
14
|
+
def scope(...)
|
|
15
15
|
ensure_owner_tenant_context_safety
|
|
16
16
|
super
|
|
17
17
|
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ActiveRecord
|
|
4
|
+
module Tenanted
|
|
5
|
+
module DatabaseAdapters # :nodoc:
|
|
6
|
+
class Base
|
|
7
|
+
attr_reader :db_config
|
|
8
|
+
|
|
9
|
+
def initialize(db_config)
|
|
10
|
+
@db_config = db_config
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
# The Active Storage disk service interpolates the tenant name into a filesystem path, so a
|
|
14
|
+
# valid name must be a single, safe path segment whatever the database allows.
|
|
15
|
+
def valid_tenant_name?(tenant_name)
|
|
16
|
+
raise "#{self.class} must implement #{__method__}"
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def validate_tenant_name(tenant_name)
|
|
20
|
+
unless valid_tenant_name?(tenant_name)
|
|
21
|
+
raise BadTenantNameError, "Invalid tenant name: #{tenant_name.inspect}"
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -6,7 +6,7 @@ module ActiveRecord
|
|
|
6
6
|
#
|
|
7
7
|
# TODO: This still feels to me like it's not _quite_ right. I think we could further refactor this by:
|
|
8
8
|
#
|
|
9
|
-
# 1. Moving tenant_databases and
|
|
9
|
+
# 1. Moving tenant_databases and valid_tenant_name? to BaseConfig, and subclassing it for
|
|
10
10
|
# each database
|
|
11
11
|
# 2. Moving create_database, drop_database, database_exist?, database_ready?,
|
|
12
12
|
# acquire_ready_lock, ensure_database_directory_exists, and database_path to the SQLite
|
|
@@ -14,30 +14,41 @@ module ActiveRecord
|
|
|
14
14
|
# 3. Moving test_workerize and path_for to be SQLite connection adapter class methods,
|
|
15
15
|
# possibly into Rails
|
|
16
16
|
#
|
|
17
|
-
class SQLite
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
17
|
+
class SQLite < Base
|
|
18
|
+
# A tenant name is interpolated into the database path as a single path segment, and that
|
|
19
|
+
# path may be a URI (see #path_for), so a name is limited to the RFC 3986 unreserved
|
|
20
|
+
# characters and may not begin with a dot or a tilde. A leading dot either traverses out of
|
|
21
|
+
# the tenant directory or hides the database from the glob in #tenant_databases; a leading
|
|
22
|
+
# tilde is expanded by File.expand_path when the name begins a relative path; "%" is
|
|
23
|
+
# percent-decoded by SQLite when it opens a URI filename; "?" and "#" truncate the path; and
|
|
24
|
+
# other punctuation is either reserved in a URI or makes URI.parse raise.
|
|
25
|
+
TENANT_NAME_PATTERN = /\A[A-Za-z0-9_-][A-Za-z0-9._~-]*\z/
|
|
23
26
|
|
|
24
27
|
def tenant_databases
|
|
25
|
-
glob = path_for(db_config.
|
|
26
|
-
scanner = Regexp.new(path_for(db_config.
|
|
28
|
+
glob = path_for(db_config.database_pattern_for("*"))
|
|
29
|
+
scanner = Regexp.new(path_for(db_config.database_pattern_for("(.+)")))
|
|
27
30
|
|
|
28
31
|
Dir.glob(glob).filter_map do |path|
|
|
29
32
|
result = path.scan(scanner).flatten.first
|
|
33
|
+
|
|
30
34
|
if result.nil?
|
|
31
35
|
Rails.logger.warn "ActiveRecord::Tenanted: Cannot parse tenant name from filename #{path.inspect}"
|
|
36
|
+
next
|
|
32
37
|
end
|
|
38
|
+
|
|
39
|
+
unless valid_tenant_name?(result)
|
|
40
|
+
Rails.logger.warn "ActiveRecord::Tenanted: Skipping database with an invalid tenant name #{result.inspect} in #{path.inspect}"
|
|
41
|
+
next
|
|
42
|
+
end
|
|
43
|
+
|
|
33
44
|
result
|
|
34
45
|
end
|
|
35
46
|
end
|
|
36
47
|
|
|
37
|
-
def
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
48
|
+
def valid_tenant_name?(tenant_name)
|
|
49
|
+
tenant_name.encoding.ascii_compatible? &&
|
|
50
|
+
tenant_name.valid_encoding? &&
|
|
51
|
+
TENANT_NAME_PATTERN.match?(tenant_name)
|
|
41
52
|
end
|
|
42
53
|
|
|
43
54
|
def create_database
|
|
@@ -27,7 +27,13 @@ module ActiveRecord
|
|
|
27
27
|
|
|
28
28
|
config_adapter.validate_tenant_name(tenant_name)
|
|
29
29
|
|
|
30
|
-
|
|
30
|
+
database_pattern_for(tenant_name)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Interpolates a glob or a regular expression into the database path. The pattern is not a
|
|
34
|
+
# tenant name and is not validated as one.
|
|
35
|
+
def database_pattern_for(pattern) # :nodoc:
|
|
36
|
+
db = sprintf(database, tenant: pattern)
|
|
31
37
|
|
|
32
38
|
if test_worker_id
|
|
33
39
|
db = config_adapter.test_workerize(db, test_worker_id)
|
|
@@ -41,10 +47,12 @@ module ActiveRecord
|
|
|
41
47
|
end
|
|
42
48
|
|
|
43
49
|
def new_tenant_config(tenant_name)
|
|
50
|
+
database = database_for(tenant_name) # validates the tenant name before it is used below
|
|
51
|
+
|
|
44
52
|
config_name = "#{name}_#{tenant_name}"
|
|
45
53
|
config_hash = configuration_hash.dup.tap do |hash|
|
|
46
54
|
hash[:tenant] = tenant_name
|
|
47
|
-
hash[:database] =
|
|
55
|
+
hash[:database] = database
|
|
48
56
|
hash[:tenanted_config_name] = name
|
|
49
57
|
end
|
|
50
58
|
Tenanted::DatabaseConfigurations::TenantConfig.new(env_name, config_name, config_hash)
|
|
@@ -10,6 +10,12 @@ module ActiveRecord
|
|
|
10
10
|
raise NoTenantError, "Cannot access Active Storage Disk service without a tenant"
|
|
11
11
|
end
|
|
12
12
|
|
|
13
|
+
# The tenant is read from the connection context, which Rails' own `connected_to` can
|
|
14
|
+
# set without going through this gem's tenant API, so the name is checked here too. A
|
|
15
|
+
# shard set that way may be a Symbol or an Integer.
|
|
16
|
+
tenant = tenant.to_s
|
|
17
|
+
klass.tenanted_root_config.config_adapter.validate_tenant_name(tenant)
|
|
18
|
+
|
|
13
19
|
sprintf(@root, tenant: tenant)
|
|
14
20
|
else
|
|
15
21
|
super
|
|
@@ -205,7 +205,7 @@ module ActiveRecord
|
|
|
205
205
|
when UNTENANTED_SENTINEL
|
|
206
206
|
# no-op
|
|
207
207
|
else
|
|
208
|
-
tenant_name = tenant_name
|
|
208
|
+
tenant_name = validated_tenant_name(tenant_name)
|
|
209
209
|
end
|
|
210
210
|
|
|
211
211
|
run_callbacks :set_current_tenant do
|
|
@@ -214,11 +214,14 @@ module ActiveRecord
|
|
|
214
214
|
end
|
|
215
215
|
|
|
216
216
|
def tenant_exist?(tenant_name)
|
|
217
|
-
tenanted_root_config
|
|
217
|
+
root_config = tenanted_root_config
|
|
218
|
+
return false unless root_config.config_adapter.valid_tenant_name?(tenant_name.to_s)
|
|
219
|
+
|
|
220
|
+
root_config.new_tenant_config(tenant_name).config_adapter.database_ready?
|
|
218
221
|
end
|
|
219
222
|
|
|
220
223
|
def with_tenant(tenant_name, prohibit_shard_swapping: true, &block)
|
|
221
|
-
tenant_name = tenant_name
|
|
224
|
+
tenant_name = validated_tenant_name(tenant_name) unless tenant_name == UNTENANTED_SENTINEL
|
|
222
225
|
|
|
223
226
|
if tenant_name == current_tenant
|
|
224
227
|
run_callbacks :with_tenant, &block
|
|
@@ -262,6 +265,8 @@ module ActiveRecord
|
|
|
262
265
|
end
|
|
263
266
|
|
|
264
267
|
def destroy_tenant(tenant_name)
|
|
268
|
+
tenant_name = validated_tenant_name(tenant_name)
|
|
269
|
+
|
|
265
270
|
ActiveRecord::Base.logger.info " DESTROY [tenant=#{tenant_name}] Destroying tenant database"
|
|
266
271
|
|
|
267
272
|
with_tenant(tenant_name, prohibit_shard_swapping: false) do
|
|
@@ -336,6 +341,15 @@ module ActiveRecord
|
|
|
336
341
|
end
|
|
337
342
|
|
|
338
343
|
private
|
|
344
|
+
# A tenant name reaches the filesystem through the Active Storage disk service as well as
|
|
345
|
+
# through the database path, and the disk service reads it from the tenant context rather
|
|
346
|
+
# than from a database configuration. Validate it as the context is set.
|
|
347
|
+
def validated_tenant_name(tenant_name)
|
|
348
|
+
tenant_name = tenant_name.to_s
|
|
349
|
+
tenanted_root_config.config_adapter.validate_tenant_name(tenant_name)
|
|
350
|
+
tenant_name
|
|
351
|
+
end
|
|
352
|
+
|
|
339
353
|
def retrieve_connection_pool(strict:)
|
|
340
354
|
role = current_role
|
|
341
355
|
shard = current_tenant
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: activerecord-tenanted
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.9.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Mike Dalessio
|
|
@@ -75,6 +75,7 @@ files:
|
|
|
75
75
|
- lib/active_record/tenanted/connection_adapter.rb
|
|
76
76
|
- lib/active_record/tenanted/console.rb
|
|
77
77
|
- lib/active_record/tenanted/database_adapter.rb
|
|
78
|
+
- lib/active_record/tenanted/database_adapters/base.rb
|
|
78
79
|
- lib/active_record/tenanted/database_adapters/sqlite.rb
|
|
79
80
|
- lib/active_record/tenanted/database_configurations.rb
|
|
80
81
|
- lib/active_record/tenanted/database_configurations/base_config.rb
|