activerecord-tenanted 0.5.0 → 0.6.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e6045a554346528724708d7acfcefa45c3445f5dd1bbc184426ce13e995bbb83
4
- data.tar.gz: 5c752644fcb587efff882e8c117933147dccc5771e74c3ea3d9b967eaa33f5dc
3
+ metadata.gz: 62c3b9d7f37db44de60c7559e4b10e1407171ab29aa874e2687853e8acc38bb1
4
+ data.tar.gz: 27b2ee871ff4ce67b60480e83694cf6a2aeac504b3ab39f96ea0eb243b5f3a47
5
5
  SHA512:
6
- metadata.gz: cb160898373d824906708a959d59b40d1f76fa398b2fcdebadd57b590ddd63a3da2fcbe53eb8787a5af041f108eb27516afb27febe4aa62eb76c9a1cd7cccd4f
7
- data.tar.gz: e001ccf67a7e4891df031ee6ceb34c20f227cb3ef3499decf2e5cce59b638a76f9a8b4200c55105910a74c3f7478c11b32aeb34d8f7da481d8b388a51997b899
6
+ metadata.gz: 7eff54730cbc865dd8ecb1169e146194b0edceaed0527059ed110537aedd57901a5a5af5b85c522bfebcf50714c468984f91ac4f5c8e40077b2e737497eac550
7
+ data.tar.gz: fafa6f206173a7057e090407f5cf6698bd7f1d34885509793568f1e76baedbe61b7eeea980493b5af0df30c3f5bd20400c799eebecf62785f458e925afbee57f
@@ -2,18 +2,27 @@
2
2
 
3
3
  module ActiveRecord
4
4
  module Tenanted
5
- module ConnectionAdapter # :nodoc:
5
+ #
6
+ # Extends ActiveRecord::ConnectionAdapters::AbstractAdapter with a `tenant` attribute.
7
+ #
8
+ # This is useful in conjunction with the `:tenant` query log tag, which configures logging of
9
+ # the tenant in SQL query logs (when `config.active_record.query_log_tags_enabled` is set to
10
+ # `true`). For example:
11
+ #
12
+ # Rails.application.config.active_record.query_log_tags_enabled = true
13
+ # Rails.application.config.active_record.query_log_tags = [ :tenant ]
14
+ #
15
+ # will cause the application to emit logs like:
16
+ #
17
+ # User Load (0.2ms) SELECT "users".* FROM "users" ORDER BY "users"."id" ASC LIMIT 1 /*tenant='foo'*/
18
+ #
19
+ module ConnectionAdapter
6
20
  extend ActiveSupport::Concern
7
21
 
8
22
  prepended do
9
23
  attr_accessor :tenant
10
24
  end
11
25
 
12
- def log(sql, name = "SQL", binds = [], type_casted_binds = [], async: false, allow_retry: false, &block)
13
- name = [ name, "[tenant=#{tenant}]" ].compact.join(" ") if tenanted?
14
- super
15
- end
16
-
17
26
  def tenanted?
18
27
  tenant.present?
19
28
  end
@@ -5,7 +5,11 @@ module ActiveRecord
5
5
  module Console # :nodoc:
6
6
  module IRBConsole
7
7
  def start
8
- ActiveRecord::Tenanted::DatabaseTasks.set_current_tenant if Rails.env.local?
8
+ # TODO: we could be setting the current tenant for all tenanted configs.
9
+ if Rails.env.local? && ActiveRecord::Tenanted.connection_class
10
+ config = ActiveRecord::Tenanted.connection_class.connection_pool.db_config
11
+ ActiveRecord::Tenanted::DatabaseTasks.new(config).set_current_tenant
12
+ end
9
13
  super
10
14
  end
11
15
  end
@@ -2,24 +2,29 @@
2
2
 
3
3
  module ActiveRecord
4
4
  module Tenanted
5
- class DatabaseAdapter # :nodoc:
6
- ADAPTERS = {
7
- "sqlite3" => "ActiveRecord::Tenanted::DatabaseAdapters::SQLite",
8
- }.freeze
5
+ module DatabaseAdapter # :nodoc:
6
+ # Hash of registered database configuration adapters
7
+ @adapters = {}
9
8
 
10
9
  class << self
10
+ def register(name, class_name)
11
+ @adapters[name.to_s] = class_name
12
+ end
13
+
11
14
  def new(db_config)
12
- adapter_class_name = ADAPTERS[db_config.adapter]
15
+ adapter_class_name = @adapters[db_config.adapter]
13
16
 
14
17
  if adapter_class_name.nil?
15
18
  raise ActiveRecord::Tenanted::UnsupportedDatabaseError,
16
19
  "Unsupported database adapter for tenanting: #{db_config.adapter}. " \
17
- "Supported adapters: #{ADAPTERS.keys.join(', ')}"
20
+ "Supported adapters: #{@adapters.keys.join(', ')}"
18
21
  end
19
22
 
20
23
  adapter_class_name.constantize.new(db_config)
21
24
  end
22
25
  end
26
+
27
+ register "sqlite3", "ActiveRecord::Tenanted::DatabaseAdapters::SQLite"
23
28
  end
24
29
  end
25
30
  end
@@ -18,11 +18,11 @@ module ActiveRecord
18
18
  end
19
19
 
20
20
  def new_connection
21
- # TODO: The Rails SQLite adapter doesn't handle directory creation for file: URIs. I would
22
- # like to fix that upstream, and remove this line.
21
+ # TODO: This line can be removed once rails/rails@f1f60dc1 is in a released version of
22
+ # Rails, and this gem's dependency has been bumped to require that version or later.
23
23
  config_adapter.ensure_database_directory_exists
24
24
 
25
- super.tap { |conn| conn.tenant = tenant }
25
+ super.tap { |connection| connection.tenant = tenant }
26
26
  end
27
27
 
28
28
  def tenanted_config_name
@@ -1,54 +1,63 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "rake"
4
+
3
5
  module ActiveRecord
4
6
  module Tenanted
5
- module DatabaseTasks # :nodoc:
6
- extend self
7
-
8
- def migrate_all
9
- raise ArgumentError, "Could not find a tenanted database" unless config = base_config
7
+ class DatabaseTasks # :nodoc:
8
+ include Rake::DSL
10
9
 
11
- tenants = config.tenants.presence || [ get_current_tenant ].compact
12
- tenants.each do |tenant|
13
- tenant_config = config.new_tenant_config(tenant)
14
- migrate(tenant_config)
10
+ class << self
11
+ def verbose?
12
+ ActiveRecord::Tasks::DatabaseTasks.send(:verbose?)
15
13
  end
16
14
  end
17
15
 
18
- def migrate_tenant(tenant_name = set_current_tenant)
19
- raise ArgumentError, "Could not find a tenanted database" unless config = base_config
16
+ attr_reader :config
17
+
18
+ def initialize(config)
19
+ unless config.is_a?(ActiveRecord::Tenanted::DatabaseConfigurations::BaseConfig)
20
+ raise TypeError, "Argument must be an instance of ActiveRecord::Tenanted::DatabaseConfigurations::BaseConfig"
21
+ end
22
+ @config = config
23
+ end
20
24
 
21
- tenant_config = config.new_tenant_config(tenant_name)
25
+ def migrate_all
26
+ tenants.each do |tenant|
27
+ migrate_tenant(tenant)
28
+ end
29
+ end
22
30
 
23
- migrate(tenant_config)
31
+ def migrate_tenant(tenant = set_current_tenant)
32
+ db_config = config.new_tenant_config(tenant)
33
+ migrate(db_config)
24
34
  end
25
35
 
26
36
  def drop_all
27
- raise ArgumentError, "Could not find a tenanted database" unless config = base_config
28
-
29
- config.tenants.each do |tenant|
30
- db_config = config.new_tenant_config(tenant)
31
- db_config.config_adapter.drop_database
32
- $stdout.puts "Dropped database '#{db_config.database}'" if verbose?
37
+ tenants.each do |tenant|
38
+ drop_tenant(tenant)
33
39
  end
34
40
  end
35
41
 
36
- def base_config
37
- db_configs = ActiveRecord::Base.configurations.configs_for(
38
- env_name: ActiveRecord::Tasks::DatabaseTasks.env,
39
- include_hidden: true
40
- )
41
- db_configs.detect { |c| c.configuration_hash[:tenanted] }
42
+ def drop_tenant(tenant = set_current_tenant)
43
+ db_config = config.new_tenant_config(tenant)
44
+ db_config.config_adapter.drop_database
45
+ $stdout.puts "Dropped database '#{db_config.database}'" if verbose?
46
+ end
47
+
48
+ def tenants
49
+ config.tenants.presence || [ get_default_tenant ].compact
42
50
  end
43
51
 
44
- def get_current_tenant
52
+ def get_default_tenant
53
+ # TODO: needs to work with multiple tenanted configs, maybe using ENV["ARTENANT_#{config.name}"]
45
54
  tenant = ENV["ARTENANT"]
46
55
 
47
56
  if tenant.present?
48
57
  $stdout.puts "Setting current tenant to #{tenant.inspect}" if verbose?
49
58
  elsif Rails.env.local?
50
59
  tenant = Rails.application.config.active_record_tenanted.default_tenant
51
- $stdout.puts "Defaulting current tenant to #{tenant.inspect}" if verbose?
60
+ $stdout.puts "Defaulting current tenant for #{config.name.inspect} to #{tenant.inspect}" if verbose?
52
61
  else
53
62
  tenant = nil
54
63
  $stdout.puts "Cannot determine an implicit tenant: ARTENANT not set, and Rails.env is not local." if verbose?
@@ -64,7 +73,7 @@ module ActiveRecord
64
73
  end
65
74
 
66
75
  if connection_class.current_tenant.nil?
67
- connection_class.current_tenant = get_current_tenant
76
+ connection_class.current_tenant = get_default_tenant
68
77
  else
69
78
  connection_class.current_tenant
70
79
  end
@@ -107,7 +116,49 @@ module ActiveRecord
107
116
  end
108
117
 
109
118
  def verbose?
110
- ActiveRecord::Tasks::DatabaseTasks.send(:verbose?)
119
+ self.class.verbose?
120
+ end
121
+
122
+ def register_rake_tasks
123
+ name = config.name
124
+
125
+ desc "Migrate tenanted #{name} databases for current environment"
126
+ task "db:migrate:#{name}" => "load_config" do
127
+ verbose_was = ActiveRecord::Migration.verbose
128
+ ActiveRecord::Migration.verbose = ActiveRecord::Tenanted::DatabaseTasks.verbose?
129
+
130
+ tenant = ENV["ARTENANT"]
131
+ if tenant.present?
132
+ migrate_tenant(tenant)
133
+ else
134
+ migrate_all
135
+ end
136
+ ensure
137
+ ActiveRecord::Migration.verbose = verbose_was
138
+ end
139
+ task "db:migrate" => "db:migrate:#{name}"
140
+ task "db:prepare" => "db:migrate:#{name}"
141
+
142
+ desc "Drop tenanted #{name} databases for current environment"
143
+ task "db:drop:#{name}" => "load_config" do
144
+ verbose_was = ActiveRecord::Migration.verbose
145
+ ActiveRecord::Migration.verbose = ActiveRecord::Tenanted::DatabaseTasks.verbose?
146
+
147
+ tenant = ENV["ARTENANT"]
148
+ if tenant.present?
149
+ drop_tenant(tenant)
150
+ else
151
+ drop_all
152
+ end
153
+ ensure
154
+ ActiveRecord::Migration.verbose = verbose_was
155
+ end
156
+ task "db:drop" => "db:drop:#{name}"
157
+
158
+ # TODO: Rails' database tasks include "db:seed" in the tasks that "db:reset" runs.
159
+ desc "Drop and recreate tenanted #{name} database from its schema for the current environment"
160
+ task "db:reset:#{name}" => [ "db:drop:#{name}", "db:migrate:#{name}" ]
161
+ task "db:reset" => "db:reset:#{name}"
111
162
  end
112
163
  end
113
164
  end
@@ -148,6 +148,10 @@ module ActiveRecord
148
148
  end
149
149
 
150
150
  config.after_initialize do
151
+ ActiveRecord::QueryLogs.taggings = ActiveRecord::QueryLogs.taggings.merge(
152
+ tenant: ->(context) { context[:connection].tenant }
153
+ )
154
+
151
155
  if defined?(Rails::Console)
152
156
  require "rails/commands/console/irb_console"
153
157
  Rails::Console::IRBConsole.prepend ActiveRecord::Tenanted::Console::IRBConsole
@@ -120,7 +120,8 @@ module ActiveRecord
120
120
 
121
121
  def create_tenant(tenant_name, if_not_exists: false, &block)
122
122
  created_db = false
123
- adapter = tenanted_root_config.new_tenant_config(tenant_name).config_adapter
123
+ base_config = tenanted_root_config
124
+ adapter = base_config.new_tenant_config(tenant_name).config_adapter
124
125
 
125
126
  adapter.acquire_ready_lock do
126
127
  unless adapter.database_exist?
@@ -128,7 +129,7 @@ module ActiveRecord
128
129
 
129
130
  with_tenant(tenant_name) do
130
131
  connection_pool(schema_version_check: false)
131
- ActiveRecord::Tenanted::DatabaseTasks.migrate_tenant(tenant_name)
132
+ ActiveRecord::Tenanted::DatabaseTasks.new(base_config).migrate_tenant(tenant_name)
132
133
  end
133
134
 
134
135
  created_db = true
@@ -28,6 +28,10 @@ module ActiveRecord
28
28
  ActiveRecord::ConnectionAdapters::BoundSchemaReflection.new(schema_reflection, self)
29
29
  end
30
30
 
31
+ def size
32
+ db_config.max_connections
33
+ end
34
+
31
35
  def lease_connection(...)
32
36
  raise Tenanted::NoTenantError, "Cannot connect to a tenanted database while untenanted (#{@model})."
33
37
  end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module ActiveRecord
4
4
  module Tenanted
5
- VERSION = "0.5.0"
5
+ VERSION = "0.6.0"
6
6
  end
7
7
  end
@@ -41,10 +41,19 @@ module ActiveRecord
41
41
  # Raised when an unsupported database adapter is used.
42
42
  class UnsupportedDatabaseError < Error; end
43
43
 
44
+ # Return the constantized connection class configured in `config.active_record_tenanted.connection_class`,
45
+ # or nil if none is configured.
44
46
  def self.connection_class
45
47
  # TODO: cache this / speed this up
46
48
  Rails.application.config.active_record_tenanted.connection_class&.constantize
47
49
  end
50
+
51
+ # Return an Array of the tenanted database configurations.
52
+ def self.base_configs(configurations = ActiveRecord::Base.configurations)
53
+ configurations
54
+ .configs_for(env_name: ActiveRecord::Tasks::DatabaseTasks.env, include_hidden: true)
55
+ .select { |c| c.configuration_hash[:tenanted] }
56
+ end
48
57
  end
49
58
  end
50
59
 
@@ -1,78 +1,20 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- namespace :db do
4
- desc "Migrate the database for tenant ARTENANT"
5
- task "migrate:tenant" => "load_config" do
6
- unless ActiveRecord::Tenanted.connection_class
7
- warn "ActiveRecord::Tenanted integration is not configured via connection_class"
8
- next
9
- end
10
-
11
- unless ActiveRecord::Tenanted::DatabaseTasks.base_config
12
- warn "WARNING: No tenanted database found, skipping tenanted migration"
13
- else
14
- begin
15
- verbose_was = ActiveRecord::Migration.verbose
16
- ActiveRecord::Migration.verbose = ActiveRecord::Tenanted::DatabaseTasks.verbose?
17
-
18
- ActiveRecord::Tenanted::DatabaseTasks.migrate_tenant
19
- ensure
20
- ActiveRecord::Migration.verbose = verbose_was
21
- end
22
- end
23
- end
24
-
25
- desc "Migrate the database for all existing tenants"
26
- task "migrate:tenant:all" => "load_config" do
27
- unless ActiveRecord::Tenanted.connection_class
28
- warn "ActiveRecord::Tenanted integration is not configured via connection_class"
29
- next
30
- end
31
-
32
- verbose_was = ActiveRecord::Migration.verbose
33
- ActiveRecord::Migration.verbose = ActiveRecord::Tenanted::DatabaseTasks.verbose?
34
-
35
- ActiveRecord::Tenanted::DatabaseTasks.migrate_all
36
- ensure
37
- ActiveRecord::Migration.verbose = verbose_was
38
- end
39
-
40
- desc "Drop and recreate all tenant databases from their schema for the current environment"
41
- task "reset:tenant" => [ "db:drop:tenant", "db:migrate:tenant" ]
42
-
43
- desc "Drop all tenanted databases for the current environment"
44
- task "drop:tenant" => "load_config" do
45
- unless ActiveRecord::Tenanted::DatabaseTasks.base_config
46
- warn "WARNING: No tenanted database found, skipping tenanted reset"
47
- else
48
- begin
49
- verbose_was = ActiveRecord::Migration.verbose
50
- ActiveRecord::Migration.verbose = ActiveRecord::Tenanted::DatabaseTasks.verbose?
51
-
52
- ActiveRecord::Tenanted::DatabaseTasks.drop_all
53
- ensure
54
- ActiveRecord::Migration.verbose = verbose_was
55
- end
56
- end
3
+ # Ensure a default tenant is set for database tasks that may need it.
4
+ desc "Set the current tenant to ARTENANT if present, else the environment default"
5
+ task "db:tenant" => "load_config" do
6
+ unless ActiveRecord::Tenanted.connection_class
7
+ warn "ActiveRecord::Tenanted integration is not configured via connection_class"
8
+ next
57
9
  end
58
10
 
59
- desc "Set the current tenant to ARTENANT if present, else the environment default"
60
- task "tenant" => "load_config" do
61
- unless ActiveRecord::Tenanted.connection_class
62
- warn "ActiveRecord::Tenanted integration is not configured via connection_class"
63
- next
64
- end
65
-
66
- ActiveRecord::Tenanted::DatabaseTasks.set_current_tenant
67
- end
11
+ config = ActiveRecord::Tenanted.connection_class.connection_pool.db_config
12
+ ActiveRecord::Tenanted::DatabaseTasks.new(config).set_current_tenant
68
13
  end
69
-
70
- # Decorate database tasks with the tenanted version.
71
- task "db:migrate" => "db:migrate:tenant:all"
72
- task "db:prepare" => "db:migrate:tenant:all"
73
- task "db:reset" => "db:reset:tenant"
74
- task "db:drop" => "db:drop:tenant"
75
-
76
- # Ensure a default tenant is set for database tasks that may need it.
77
14
  task "db:fixtures:load" => "db:tenant"
78
15
  task "db:seed" => "db:tenant"
16
+
17
+ # Create tenanted rake tasks
18
+ ActiveRecord::Tenanted.base_configs(ActiveRecord::DatabaseConfigurations.new(ActiveRecord::Tasks::DatabaseTasks.setup_initial_database_yaml)).each do |config|
19
+ ActiveRecord::Tenanted::DatabaseTasks.new(config).register_rake_tasks
20
+ end
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.5.0
4
+ version: 0.6.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Mike Dalessio