pg_multitenant_schemas 0.2.3 → 0.3.6

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.
Files changed (39) hide show
  1. checksums.yaml +4 -4
  2. data/.agent.md +54 -0
  3. data/.rubocop.yml +7 -1
  4. data/.ruby-version +1 -1
  5. data/CHANGELOG.md +58 -0
  6. data/DELIVERY_SUMMARY.md +318 -0
  7. data/GITHUB_ACTIONS_QUICK_START.md +110 -0
  8. data/GITHUB_ACTIONS_SETUP.md +313 -0
  9. data/GITHUB_ACTIONS_SUMMARY.md +231 -0
  10. data/LICENSE.txt +1 -1
  11. data/LOCAL_TESTING_SUMMARY.md +30 -13
  12. data/READY_TO_RELEASE.md +308 -0
  13. data/RELEASE_NOTES.md +194 -0
  14. data/RELEASE_STEPS.md +122 -0
  15. data/WORKFLOW_FIXES.md +188 -0
  16. data/docs/index.html +929 -0
  17. data/lib/pg_multitenant_schemas/configuration.rb +7 -1
  18. data/lib/pg_multitenant_schemas/context.rb +92 -3
  19. data/lib/pg_multitenant_schemas/rails/controller_concern.rb +81 -6
  20. data/lib/pg_multitenant_schemas/rails/generators/install_generator.rb +27 -0
  21. data/lib/pg_multitenant_schemas/rails/generators/tenant_migration_generator.rb +30 -0
  22. data/lib/pg_multitenant_schemas/rails/model_concern.rb +3 -3
  23. data/lib/pg_multitenant_schemas/rails/railtie.rb +27 -0
  24. data/lib/pg_multitenant_schemas/schema_switcher.rb +73 -7
  25. data/lib/pg_multitenant_schemas/tenant_resolver.rb +3 -3
  26. data/lib/pg_multitenant_schemas/ui/app/assets/stylesheets/pg_multitenant_schemas/ui/application.css +94 -0
  27. data/lib/pg_multitenant_schemas/ui/app/controllers/pg_multitenant_schemas/ui/tenants_controller.rb +130 -0
  28. data/lib/pg_multitenant_schemas/ui/app/views/layouts/pg_multitenant_schemas/ui/application.html.erb +29 -0
  29. data/lib/pg_multitenant_schemas/ui/app/views/pg_multitenant_schemas/ui/tenants/index.html.erb +85 -0
  30. data/lib/pg_multitenant_schemas/ui/app/views/pg_multitenant_schemas/ui/tenants/migration_status.html.erb +45 -0
  31. data/lib/pg_multitenant_schemas/ui/app/views/pg_multitenant_schemas/ui/tenants/new.html.erb +50 -0
  32. data/lib/pg_multitenant_schemas/ui/config/routes.rb +20 -0
  33. data/lib/pg_multitenant_schemas/ui/engine.rb +49 -0
  34. data/lib/pg_multitenant_schemas/ui.rb +9 -0
  35. data/lib/pg_multitenant_schemas/version.rb +1 -1
  36. data/lib/pg_multitenant_schemas.rb +155 -29
  37. data/pg_multitenant_schemas.gemspec +15 -9
  38. data/pre-release-check.sh +46 -0
  39. metadata +39 -12
@@ -12,12 +12,18 @@ module PgMultitenantSchemas
12
12
  def initialize
13
13
  @tenant_model_class = "Tenant"
14
14
  @default_schema = "public"
15
+ # Enable development fallback with cookie support by default in development
15
16
  @development_fallback = false
17
+ begin
18
+ @development_fallback = ::Rails.env&.development? == true if defined?(::Rails)
19
+ rescue StandardError
20
+ # If Rails is not available, keep default
21
+ end
16
22
  @excluded_subdomains = %w[www api admin mail ftp blog support help docs staging]
17
23
  @common_tlds = %w[com org net edu gov mil int co uk ca au de fr jp cn dev test]
18
24
  @auto_create_schemas = true
19
25
  @connection_class = "ApplicationRecord"
20
- @logger = defined?(Rails) && Rails.respond_to?(:logger) ? Rails.logger : nil
26
+ @logger = defined?(::Rails) && ::Rails.respond_to?(:logger) ? ::Rails.logger : nil
21
27
  end
22
28
 
23
29
  def tenant_model
@@ -2,36 +2,90 @@
2
2
 
3
3
  module PgMultitenantSchemas
4
4
  # Thread-safe tenant context management
5
+ #
6
+ # The Context class manages the current tenant and schema in a thread-safe manner
7
+ # using Thread.current storage. This ensures proper isolation in multi-threaded
8
+ # environments like Rails servers.
9
+ #
10
+ # @example Get current schema
11
+ # PgMultitenantSchemas::Context.current_schema #=> "tenant_123"
12
+ #
13
+ # @example Switch to tenant context
14
+ # tenant = Tenant.find(1)
15
+ # PgMultitenantSchemas::Context.switch_to_tenant(tenant)
16
+ #
17
+ # @example Use block-based context
18
+ # PgMultitenantSchemas::Context.with_tenant(tenant) do
19
+ # User.all # Queries tenant's schema
20
+ # end
5
21
  class Context
6
22
  class << self
23
+ # Get the current tenant object
24
+ #
25
+ # @return [Object, nil] The current tenant or nil if no tenant context is set
7
26
  def current_tenant
8
27
  Thread.current[:pg_multitenant_current_tenant]
9
28
  end
10
29
 
30
+ # Set the current tenant object
31
+ #
32
+ # @param tenant [Object] The tenant object to set as current
33
+ # @return [Object] The tenant object
11
34
  def current_tenant=(tenant)
12
35
  Thread.current[:pg_multitenant_current_tenant] = tenant
13
36
  end
14
37
 
38
+ # Get the current tenant schema name
39
+ #
40
+ # @return [String] The current schema name or default schema if not set
41
+ # @example
42
+ # PgMultitenantSchemas::Context.current_schema #=> "tenant_123"
15
43
  def current_schema
16
44
  Thread.current[:pg_multitenant_current_schema] || PgMultitenantSchemas.configuration.default_schema
17
45
  end
18
46
 
47
+ # Set the current tenant schema name
48
+ #
49
+ # @param schema_name [String] The schema name to set as current
50
+ # @return [String] The schema name
19
51
  def current_schema=(schema_name)
20
52
  Thread.current[:pg_multitenant_current_schema] = schema_name
21
53
  end
22
54
 
55
+ # Reset the current tenant context to default
56
+ #
57
+ # Clears both tenant and schema context and restores the default schema
58
+ #
59
+ # @return [void]
60
+ # @example
61
+ # PgMultitenantSchemas::Context.reset!
23
62
  def reset!
24
63
  Thread.current[:pg_multitenant_current_tenant] = nil
25
64
  Thread.current[:pg_multitenant_current_schema] = nil
26
65
  switch_to_schema(PgMultitenantSchemas.configuration.default_schema)
27
66
  end
28
67
 
68
+ # Switch to a specific schema
69
+ #
70
+ # @param schema_name [String] The schema name to switch to
71
+ # @return [void]
72
+ # @example
73
+ # PgMultitenantSchemas::Context.switch_to_schema('tenant_123')
29
74
  def switch_to_schema(schema_name)
30
75
  schema_name = PgMultitenantSchemas.configuration.default_schema if schema_name.blank?
31
76
  SchemaSwitcher.switch_schema(schema_name)
32
77
  self.current_schema = schema_name
33
78
  end
34
79
 
80
+ # Switch to a specific tenant's schema
81
+ #
82
+ # @param tenant [Object, String] The tenant object (must respond to :subdomain) or schema name
83
+ # @return [void]
84
+ # @example With tenant object
85
+ # tenant = Tenant.find(1)
86
+ # PgMultitenantSchemas::Context.switch_to_tenant(tenant)
87
+ # @example With schema name
88
+ # PgMultitenantSchemas::Context.switch_to_tenant('tenant_123')
35
89
  def switch_to_tenant(tenant)
36
90
  if tenant
37
91
  schema_name = tenant.respond_to?(:subdomain) ? tenant.subdomain : tenant.to_s
@@ -43,7 +97,23 @@ module PgMultitenantSchemas
43
97
  end
44
98
  end
45
99
 
46
- # Execute block within tenant context
100
+ # Execute a block within a tenant context
101
+ #
102
+ # All database queries within the block will be executed in the tenant's schema.
103
+ # The previous context is restored after the block completes, even if an error occurs.
104
+ #
105
+ # @param tenant_or_schema [Object, String] The tenant object or schema name
106
+ # @yield Executes the given block in the tenant's schema context
107
+ # @return [Object] The return value of the block
108
+ # @example With tenant object
109
+ # tenant = Tenant.find(1)
110
+ # PgMultitenantSchemas::Context.with_tenant(tenant) do
111
+ # User.all # Queries tenant's schema
112
+ # end
113
+ # @example With schema name
114
+ # PgMultitenantSchemas::Context.with_tenant('tenant_123') do
115
+ # Order.create!(status: 'pending')
116
+ # end
47
117
  def with_tenant(tenant_or_schema)
48
118
  schema_name, tenant = extract_schema_and_tenant(tenant_or_schema)
49
119
  previous_tenant = current_tenant
@@ -82,7 +152,16 @@ module PgMultitenantSchemas
82
152
 
83
153
  public
84
154
 
85
- # Create new tenant schema
155
+ # Create a new PostgreSQL schema for a tenant
156
+ #
157
+ # @param tenant_or_schema [Object, String] The tenant object or schema name
158
+ # @return [void]
159
+ # @raise [ArgumentError] if schema name is blank
160
+ # @example With tenant object
161
+ # tenant = Tenant.create!(subdomain: 'acme')
162
+ # PgMultitenantSchemas::Context.create_tenant_schema(tenant)
163
+ # @example With schema name
164
+ # PgMultitenantSchemas::Context.create_tenant_schema('tenant_123')
86
165
  def create_tenant_schema(tenant_or_schema)
87
166
  schema_name = if tenant_or_schema.respond_to?(:subdomain)
88
167
  tenant_or_schema.subdomain
@@ -92,7 +171,17 @@ module PgMultitenantSchemas
92
171
  SchemaSwitcher.create_schema(schema_name)
93
172
  end
94
173
 
95
- # Drop tenant schema
174
+ # Drop a PostgreSQL schema for a tenant
175
+ #
176
+ # @param tenant_or_schema [Object, String] The tenant object or schema name
177
+ # @param cascade [Boolean] Whether to drop dependent objects (default: true)
178
+ # @return [void]
179
+ # @raise [ArgumentError] if schema name is blank
180
+ # @example With tenant object
181
+ # tenant = Tenant.find(1)
182
+ # PgMultitenantSchemas::Context.drop_tenant_schema(tenant)
183
+ # @example With cascade option
184
+ # PgMultitenantSchemas::Context.drop_tenant_schema('old_tenant', cascade: true)
96
185
  def drop_tenant_schema(tenant_or_schema, cascade: true)
97
186
  schema_name = if tenant_or_schema.respond_to?(:subdomain)
98
187
  tenant_or_schema.subdomain
@@ -30,18 +30,93 @@ module PgMultitenantSchemas
30
30
  end
31
31
 
32
32
  def resolve_tenant_context
33
- tenant = PgMultitenantSchemas::TenantResolver.resolve_tenant_with_fallback(request)
33
+ # Try to resolve tenant from cookie/session first (for development mode)
34
+ tenant = resolve_from_cookie_fallback(request)
35
+
36
+ # If no tenant from cookie/session, use normal resolution
37
+ tenant = PgMultitenantSchemas::TenantResolver.resolve_tenant_with_fallback(request) if tenant.nil?
38
+
34
39
  switch_to_tenant(tenant)
35
40
  rescue StandardError => e
36
41
  handle_tenant_resolution_error(e)
37
42
  end
38
43
 
44
+ private
45
+
46
+ # Try to resolve tenant from cookie/session-based schema (if available)
47
+ def resolve_from_cookie_fallback(request)
48
+ return nil unless should_use_cookie_fallback?
49
+
50
+ schema = extract_schema_from_request(request)
51
+ return nil unless schema.present? && PgMultitenantSchemas::SchemaSwitcher.schema_exists?(schema)
52
+
53
+ PgMultitenantSchemas::Context.current_schema = schema
54
+ @current_tenant = nil
55
+ VirtualTenant.new(schema)
56
+ rescue StandardError => e
57
+ ::Rails.logger.debug "PgMultitenantSchemas: resolve_from_cookie_fallback failed: #{e.class} #{e.message}"
58
+ nil
59
+ end
60
+
61
+ # Extract schema from session or cookies
62
+ def extract_schema_from_request(request)
63
+ # Try session first (most reliable)
64
+ if respond_to?(:session) && session.present? && session[:pg_multitenant_selected_schema].present?
65
+ return session[:pg_multitenant_selected_schema]
66
+ end
67
+
68
+ # Try various cookie sources
69
+ extract_schema_from_cookies(request)
70
+ end
71
+
72
+ # Extract schema from multiple cookie sources
73
+ def extract_schema_from_cookies(request)
74
+ if request.respond_to?(:cookie_jar)
75
+ request.cookie_jar[:pg_multitenant_selected_schema]
76
+ elsif request.respond_to?(:cookies)
77
+ request.cookies[:pg_multitenant_selected_schema]
78
+ elsif respond_to?(:cookies)
79
+ cookies[:pg_multitenant_selected_schema]
80
+ else
81
+ extract_schema_from_cookie_header(request)
82
+ end
83
+ end
84
+
85
+ # Parse schema from raw HTTP_COOKIE header
86
+ def extract_schema_from_cookie_header(request)
87
+ cookie_header = request.env["HTTP_COOKIE"]
88
+ return nil unless cookie_header.present?
89
+
90
+ # Parse "key1=value1; key2=value2; ..." format
91
+ cookie_header.split("; ").find do |c|
92
+ c.start_with?("pg_multitenant_selected_schema=")
93
+ end&.split("=")&.last
94
+ end
95
+
96
+ # Virtual tenant object for cookie-based schema resolution
97
+ class VirtualTenant
98
+ attr_reader :subdomain, :schema
99
+
100
+ def initialize(schema)
101
+ @subdomain = schema
102
+ @schema = schema
103
+ end
104
+ end
105
+
106
+ # Check if we should try cookie-based resolution
107
+ def should_use_cookie_fallback?
108
+ return false unless PgMultitenantSchemas.configuration.development_fallback
109
+ return false unless ::Rails.env.development?
110
+
111
+ # If we reach here, we should attempt cookie-based resolution
112
+ true
113
+ end
114
+
39
115
  def reset_tenant_context
40
116
  @current_tenant = nil
41
117
  PgMultitenantSchemas::Context.reset!
42
- rescue StandardError => e
43
- Rails.logger.error "PgMultitenantSchemas: Failed to reset tenant context: #{e.message}"
44
- # Don't raise - this is cleanup code
118
+ rescue StandardError
119
+ # Silently ignore errors during context reset
45
120
  end
46
121
 
47
122
  protected
@@ -53,9 +128,9 @@ module PgMultitenantSchemas
53
128
 
54
129
  # Override this method to customize error handling
55
130
  def handle_tenant_resolution_error(error)
56
- Rails.logger.error "PgMultitenantSchemas: Tenant resolution failed: #{error.message}"
131
+ ::Rails.logger.error "PgMultitenantSchemas: Tenant resolution failed: #{error.message}"
57
132
 
58
- raise error unless PgMultitenantSchemas.configuration.development_fallback && Rails.env.development?
133
+ raise error unless PgMultitenantSchemas.configuration.development_fallback && ::Rails.env.development?
59
134
 
60
135
  switch_to_tenant(nil) # Fall back to default schema in development
61
136
  end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails/generators"
4
+
5
+ module PgMultitenantSchemas
6
+ module Generators
7
+ # Generates the initializer used to configure PgMultitenantSchemas in a Rails app.
8
+ class InstallGenerator < ::Rails::Generators::Base
9
+ source_root File.expand_path("templates", __dir__)
10
+
11
+ desc "Creates a PgMultitenantSchemas initializer in your Rails app"
12
+
13
+ def create_initializer
14
+ create_file "config/initializers/pg_multitenant_schemas.rb", <<~RUBY
15
+ PgMultitenantSchemas.configure do |config|
16
+ config.connection_class = "ApplicationRecord"
17
+ config.tenant_model_class = "Tenant"
18
+ config.default_schema = "public"
19
+ config.excluded_subdomains = ["www", "api", "admin"]
20
+ config.development_fallback = true
21
+ config.auto_create_schemas = true
22
+ end
23
+ RUBY
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails/generators"
4
+ require "rails/generators/active_record"
5
+
6
+ module PgMultitenantSchemas
7
+ module Generators
8
+ # Generates a migration for adding a tenant to a schema.
9
+ class TenantMigrationGenerator < ::Rails::Generators::Base
10
+ include ::Rails::Generators::Migration
11
+
12
+ source_root File.expand_path("templates", __dir__)
13
+
14
+ desc "Creates a migration to add a tenant to a schema"
15
+
16
+ argument :tenant_name, type: :string, banner: "TENANT_NAME"
17
+
18
+ def self.next_migration_number(dirname)
19
+ ::ActiveRecord::Generators::Base.next_migration_number(dirname)
20
+ end
21
+
22
+ def create_migration
23
+ migration_template(
24
+ "tenant_migration.rb.erb",
25
+ "db/migrate/create_#{tenant_name}_schema.rb"
26
+ )
27
+ end
28
+ end
29
+ end
30
+ end
@@ -62,18 +62,18 @@ module PgMultitenantSchemas
62
62
 
63
63
  # Log schema operations
64
64
  def log_schema_operation(message)
65
- Rails.logger.info "PgMultitenantSchemas: #{message}"
65
+ ::Rails.logger.info "PgMultitenantSchemas: #{message}"
66
66
  end
67
67
 
68
68
  # Handle schema operation errors
69
69
  def handle_schema_error(error, operation)
70
- Rails.logger.error "PgMultitenantSchemas: Failed to #{operation} schema '#{subdomain}': #{error.message}"
70
+ ::Rails.logger.error "PgMultitenantSchemas: Failed to #{operation} schema '#{subdomain}': #{error.message}"
71
71
  raise error unless development_fallback_enabled?
72
72
  end
73
73
 
74
74
  # Check if development fallback is enabled
75
75
  def development_fallback_enabled?
76
- PgMultitenantSchemas.configuration.development_fallback && Rails.env.development?
76
+ PgMultitenantSchemas.configuration.development_fallback && ::Rails.env.development?
77
77
  end
78
78
 
79
79
  class_methods do
@@ -12,6 +12,33 @@ module PgMultitenantSchemas
12
12
  end
13
13
  end
14
14
 
15
+ initializer "pg_multitenant_schemas.middleware" do |app|
16
+ # Add cookies middleware for API-only apps in development
17
+ # Needed for cookie-based schema switching between dashboard and main app
18
+ if ::Rails.env.development?
19
+ # Insert cookies middleware at the beginning
20
+ app.config.middleware.insert_before 0, ActionDispatch::Cookies
21
+ # Session store for reading cookies properly
22
+ app.config.middleware.insert_before 1, ActionDispatch::Session::CookieStore,
23
+ key: "_pg_multitenant_session"
24
+ # Flash support for notices/alerts
25
+ app.config.middleware.insert_before 2, ActionDispatch::Flash
26
+ end
27
+ end
28
+
29
+ initializer "pg_multitenant_schemas.controller_integration" do
30
+ # Automatically extend ApplicationController with ControllerConcern
31
+ ActiveSupport.on_load(:action_controller) do
32
+ include PgMultitenantSchemas::Rails::ControllerConcern
33
+ end
34
+ end
35
+
36
+ initializer "pg_multitenant_schemas.ui.routes" do |app|
37
+ app.routes.append do
38
+ mount PgMultitenantSchemas::UI::Engine, at: "/pg_multitenant_schemas"
39
+ end
40
+ end
41
+
15
42
  # Add rake tasks
16
43
  rake_tasks do
17
44
  # Load all task files
@@ -1,16 +1,36 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module PgMultitenantSchemas
4
- # Core schema switching functionality
4
+ # Low-level PostgreSQL schema switching and management
5
+ #
6
+ # The SchemaSwitcher class provides direct access to PostgreSQL schema operations.
7
+ # It handles both Rails and raw PG connections transparently.
8
+ #
9
+ # @example Switching schemas
10
+ # PgMultitenantSchemas::SchemaSwitcher.switch_schema('tenant_123')
11
+ #
12
+ # @example Creating a schema
13
+ # PgMultitenantSchemas::SchemaSwitcher.create_schema('new_tenant')
14
+ #
15
+ # @example Listing all schemas
16
+ # PgMultitenantSchemas::SchemaSwitcher.list_schemas
17
+ # #=> ["public", "tenant_123", "tenant_456"]
5
18
  class SchemaSwitcher
6
19
  class << self
7
20
  # Initialize connection management
21
+ #
22
+ # Called by the Railtie when Rails starts. No special initialization needed.
23
+ #
24
+ # @return [void]
8
25
  def initialize_connection
9
26
  # This is called by the Railtie when Rails starts
10
27
  # No special initialization needed as we use the configured connection class
11
28
  end
12
29
 
13
30
  # Get the database connection from the configured connection class
31
+ #
32
+ # @return [ActiveRecord::ConnectionAdapters::AbstractAdapter, PG::Connection]
33
+ # @raise [PgMultitenantSchemas::ConnectionError] if connection cannot be established
14
34
  def connection
15
35
  connection_class = PgMultitenantSchemas.configuration.connection_class
16
36
  if connection_class.is_a?(String)
@@ -22,7 +42,15 @@ module PgMultitenantSchemas
22
42
  raise ConnectionError, "Failed to get database connection: #{e.message}"
23
43
  end
24
44
 
25
- # Switches the search_path to the specified schema
45
+ # Switch the current PostgreSQL schema
46
+ #
47
+ # Sets the search_path to the specified schema using SET search_path.
48
+ #
49
+ # @param schema_name [String] The schema name to switch to
50
+ # @return [void]
51
+ # @raise [ArgumentError] if schema name is blank
52
+ # @example
53
+ # PgMultitenantSchemas::SchemaSwitcher.switch_schema('tenant_123')
26
54
  def switch_schema(schema_name)
27
55
  raise ArgumentError, "Schema name cannot be empty" if schema_name.nil? || schema_name.strip.empty?
28
56
 
@@ -31,13 +59,25 @@ module PgMultitenantSchemas
31
59
  execute_sql(conn, "SET search_path TO #{quoted_schema};")
32
60
  end
33
61
 
34
- # Reset to default schema
62
+ # Reset to the default PostgreSQL schema
63
+ #
64
+ # Sets search_path back to 'public'.
65
+ #
66
+ # @return [void]
35
67
  def reset_schema
36
68
  conn = connection
37
69
  execute_sql(conn, "SET search_path TO public;")
38
70
  end
39
71
 
40
- # Create a new schema
72
+ # Create a new PostgreSQL schema
73
+ #
74
+ # Uses CREATE SCHEMA IF NOT EXISTS for idempotency.
75
+ #
76
+ # @param schema_name [String] The schema name to create
77
+ # @return [void]
78
+ # @raise [ArgumentError] if schema name is blank
79
+ # @example
80
+ # PgMultitenantSchemas::SchemaSwitcher.create_schema('tenant_123')
41
81
  def create_schema(schema_name)
42
82
  raise ArgumentError, "Schema name cannot be empty" if schema_name.nil? || schema_name.strip.empty?
43
83
 
@@ -46,7 +86,16 @@ module PgMultitenantSchemas
46
86
  execute_sql(conn, "CREATE SCHEMA IF NOT EXISTS #{quoted_schema};")
47
87
  end
48
88
 
49
- # Drop a schema
89
+ # Drop a PostgreSQL schema
90
+ #
91
+ # @param schema_name [String] The schema name to drop
92
+ # @param cascade [Boolean] Whether to drop dependent objects (default: true)
93
+ # @return [void]
94
+ # @raise [ArgumentError] if schema name is blank
95
+ # @example
96
+ # PgMultitenantSchemas::SchemaSwitcher.drop_schema('old_tenant')
97
+ # @example With CASCADE option
98
+ # PgMultitenantSchemas::SchemaSwitcher.drop_schema('old_tenant', cascade: true)
50
99
  def drop_schema(schema_name, cascade: true)
51
100
  raise ArgumentError, "Schema name cannot be empty" if schema_name.nil? || schema_name.strip.empty?
52
101
 
@@ -56,7 +105,14 @@ module PgMultitenantSchemas
56
105
  execute_sql(conn, "DROP SCHEMA IF EXISTS #{quoted_schema} #{cascade_option};")
57
106
  end
58
107
 
59
- # Check if schema exists
108
+ # Check if a schema exists in the database
109
+ #
110
+ # @param schema_name [String] The schema name to check
111
+ # @return [Boolean] true if the schema exists, false otherwise
112
+ # @example
113
+ # if PgMultitenantSchemas::SchemaSwitcher.schema_exists?('tenant_123')
114
+ # # Schema exists
115
+ # end
60
116
  def schema_exists?(schema_name)
61
117
  return false if schema_name.nil? || schema_name.strip.empty?
62
118
 
@@ -73,7 +129,12 @@ module PgMultitenantSchemas
73
129
  [true, "t", "true"].include?(value)
74
130
  end
75
131
 
76
- # Get current schema
132
+ # Get the current PostgreSQL schema
133
+ #
134
+ # @return [String] The current schema name
135
+ # @example
136
+ # PgMultitenantSchemas::SchemaSwitcher.current_schema
137
+ # #=> "tenant_123"
77
138
  def current_schema
78
139
  conn = connection
79
140
  result = execute_sql(conn, "SELECT current_schema()")
@@ -81,6 +142,11 @@ module PgMultitenantSchemas
81
142
  end
82
143
 
83
144
  # List all schemas in the database
145
+ #
146
+ # @return [Array<String>] Array of schema names
147
+ # @example
148
+ # PgMultitenantSchemas::SchemaSwitcher.list_schemas
149
+ # #=> ["public", "tenant_123", "tenant_456"]
84
150
  def list_schemas
85
151
  conn = connection
86
152
  result = execute_sql(conn, <<~SQL)
@@ -63,7 +63,7 @@ module PgMultitenantSchemas
63
63
  tenant_model.find_by(subdomain: subdomain, status: "active")
64
64
  end
65
65
  rescue StandardError => e
66
- Rails.logger.error "PgMultitenantSchemas: Error finding tenant '#{subdomain}': #{e.message}"
66
+ ::Rails.logger.error "PgMultitenantSchemas: Error finding tenant '#{subdomain}': #{e.message}"
67
67
  nil
68
68
  end
69
69
 
@@ -80,8 +80,8 @@ module PgMultitenantSchemas
80
80
  tenant = resolve_tenant_from_request(request)
81
81
 
82
82
  # If no tenant found and development fallback is enabled
83
- if tenant.nil? && PgMultitenantSchemas.configuration.development_fallback && Rails.env.development?
84
- Rails.logger.info "PgMultitenantSchemas: No tenant found, using development fallback"
83
+ if tenant.nil? && PgMultitenantSchemas.configuration.development_fallback && ::Rails.env.development?
84
+ ::Rails.logger.info "PgMultitenantSchemas: No tenant found, using development fallback"
85
85
  return nil # This will cause switch to default schema
86
86
  end
87
87
 
@@ -0,0 +1,94 @@
1
+ *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
2
+
3
+ body {
4
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
5
+ background: #f5f5f5;
6
+ color: #1a1a1a;
7
+ min-height: 100vh;
8
+ }
9
+
10
+ header {
11
+ background: #1e293b;
12
+ color: #fff;
13
+ padding: 0 1.5rem;
14
+ display: flex;
15
+ align-items: center;
16
+ gap: 1rem;
17
+ height: 56px;
18
+ box-shadow: 0 1px 4px rgba(0,0,0,.3);
19
+ }
20
+ header h1 { font-size: 1rem; font-weight: 600; letter-spacing: .03em; }
21
+ header span { font-size: .75rem; color: #94a3b8; }
22
+
23
+ main { max-width: 960px; margin: 2rem auto; padding: 0 1rem; }
24
+
25
+ .flash { padding: .75rem 1rem; border-radius: 6px; margin-bottom: 1.25rem; font-size: .875rem; }
26
+ .flash.notice { background: #dcfce7; color: #166534; border: 1px solid #bbf7d0; }
27
+ .flash.alert { background: #fee2e2; color: #991b1b; border: 1px solid #fecaca; }
28
+
29
+ .card { background: #fff; border-radius: 8px; box-shadow: 0 1px 3px rgba(0,0,0,.1); }
30
+ .card-header {
31
+ padding: 1rem 1.25rem;
32
+ border-bottom: 1px solid #e5e7eb;
33
+ display: flex;
34
+ align-items: center;
35
+ justify-content: space-between;
36
+ }
37
+ .card-header h2 { font-size: 1rem; font-weight: 600; }
38
+
39
+ table { width: 100%; border-collapse: collapse; font-size: .875rem; }
40
+ th, td { padding: .65rem 1.25rem; text-align: left; }
41
+ th { font-weight: 600; color: #6b7280; font-size: .75rem; text-transform: uppercase; letter-spacing: .05em; background: #f9fafb; border-bottom: 1px solid #e5e7eb; }
42
+ tr:not(:last-child) td { border-bottom: 1px solid #f3f4f6; }
43
+ tr:hover td { background: #fafafa; }
44
+
45
+ .badge {
46
+ display: inline-flex; align-items: center; gap: .3rem;
47
+ padding: .2rem .55rem; border-radius: 999px; font-size: .7rem; font-weight: 600;
48
+ }
49
+ .badge-green { background: #dcfce7; color: #166534; }
50
+ .badge-yellow { background: #fef9c3; color: #854d0e; }
51
+ .badge-red { background: #fee2e2; color: #991b1b; }
52
+ .badge-gray { background: #f3f4f6; color: #6b7280; }
53
+
54
+ .btn {
55
+ display: inline-flex; align-items: center; gap: .35rem;
56
+ padding: .45rem .9rem; border-radius: 6px; font-size: .8rem; font-weight: 500;
57
+ text-decoration: none; border: none; cursor: pointer; transition: filter .15s;
58
+ }
59
+ .btn:hover { filter: brightness(.92); }
60
+ .btn-primary { background: #3b82f6; color: #fff; }
61
+ .btn-green { background: #22c55e; color: #fff; }
62
+ .btn-yellow { background: #f59e0b; color: #fff; }
63
+ .btn-red { background: #ef4444; color: #fff; }
64
+ .btn-ghost { background: #f3f4f6; color: #374151; }
65
+ .btn-sm { padding: .3rem .65rem; font-size: .75rem; }
66
+
67
+ .actions { display: flex; gap: .4rem; flex-wrap: wrap; }
68
+ .toolbar { display: flex; gap: .6rem; align-items: center; flex-wrap: wrap; }
69
+
70
+ .schema-badge {
71
+ font-family: monospace; font-size: .78rem;
72
+ background: #eff6ff; color: #1d4ed8;
73
+ padding: .15rem .45rem; border-radius: 4px;
74
+ }
75
+
76
+ form.inline { display: inline; }
77
+
78
+ /* switch bar */
79
+ .switch-bar {
80
+ margin-bottom: 1.5rem;
81
+ background: #fff;
82
+ border: 1px solid #e5e7eb;
83
+ border-radius: 8px;
84
+ padding: .75rem 1.25rem;
85
+ display: flex; align-items: center; gap: .75rem; font-size: .875rem; flex-wrap: wrap;
86
+ }
87
+ .switch-bar label { font-weight: 500; color: #374151; }
88
+ .switch-bar select {
89
+ flex: 1; min-width: 160px; max-width: 280px;
90
+ border: 1px solid #d1d5db; border-radius: 6px;
91
+ padding: .35rem .6rem; font-size: .875rem;
92
+ }
93
+
94
+ .empty { padding: 2.5rem; text-align: center; color: #9ca3af; font-size: .9rem; }