pg_multitenant_schemas 0.2.2 โ†’ 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 (45) 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 -2
  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/README.md +4 -4
  13. data/READY_TO_RELEASE.md +308 -0
  14. data/RELEASE_NOTES.md +194 -0
  15. data/RELEASE_STEPS.md +122 -0
  16. data/WORKFLOW_FIXES.md +188 -0
  17. data/docs/index.html +929 -0
  18. data/docs/testing_rails_tasks.md +128 -0
  19. data/lib/pg_multitenant_schemas/configuration.rb +7 -1
  20. data/lib/pg_multitenant_schemas/context.rb +92 -3
  21. data/lib/pg_multitenant_schemas/migration_schema_operations.rb +99 -5
  22. data/lib/pg_multitenant_schemas/migrator.rb +66 -9
  23. data/lib/pg_multitenant_schemas/rails/controller_concern.rb +81 -6
  24. data/lib/pg_multitenant_schemas/rails/generators/install_generator.rb +27 -0
  25. data/lib/pg_multitenant_schemas/rails/generators/tenant_migration_generator.rb +30 -0
  26. data/lib/pg_multitenant_schemas/rails/model_concern.rb +3 -3
  27. data/lib/pg_multitenant_schemas/rails/railtie.rb +31 -1
  28. data/lib/pg_multitenant_schemas/schema_switcher.rb +107 -7
  29. data/lib/pg_multitenant_schemas/tasks/tenant_tasks.rake +0 -3
  30. data/lib/pg_multitenant_schemas/tenant_resolver.rb +3 -3
  31. data/lib/pg_multitenant_schemas/ui/app/assets/stylesheets/pg_multitenant_schemas/ui/application.css +94 -0
  32. data/lib/pg_multitenant_schemas/ui/app/controllers/pg_multitenant_schemas/ui/tenants_controller.rb +130 -0
  33. data/lib/pg_multitenant_schemas/ui/app/views/layouts/pg_multitenant_schemas/ui/application.html.erb +29 -0
  34. data/lib/pg_multitenant_schemas/ui/app/views/pg_multitenant_schemas/ui/tenants/index.html.erb +85 -0
  35. data/lib/pg_multitenant_schemas/ui/app/views/pg_multitenant_schemas/ui/tenants/migration_status.html.erb +45 -0
  36. data/lib/pg_multitenant_schemas/ui/app/views/pg_multitenant_schemas/ui/tenants/new.html.erb +50 -0
  37. data/lib/pg_multitenant_schemas/ui/config/routes.rb +20 -0
  38. data/lib/pg_multitenant_schemas/ui/engine.rb +49 -0
  39. data/lib/pg_multitenant_schemas/ui.rb +9 -0
  40. data/lib/pg_multitenant_schemas/version.rb +1 -1
  41. data/lib/pg_multitenant_schemas.rb +155 -29
  42. data/pg_multitenant_schemas.gemspec +18 -12
  43. data/pre-release-check.sh +46 -0
  44. metadata +44 -17
  45. data/lib/pg_multitenant_schemas/tasks/pg_multitenant_schemas.rake +0 -65
@@ -0,0 +1,128 @@
1
+ # ๐Ÿงช Testing Rails Tasks in PG Multitenant Schemas
2
+
3
+ ## ๐Ÿšจ Important: Testing Context
4
+
5
+ The `rails tenants:list` command only works **within a Rails application** that includes this gem, not in the gem directory itself.
6
+
7
+ ## ๐Ÿ”ง Fixed Issues
8
+
9
+ โœ… **Removed circular dependency** in rake task loading
10
+ โœ… **Eliminated duplicate task files** (`pg_multitenant_schemas.rake` was identical to `tenant_tasks.rake`)
11
+ โœ… **Updated railtie** to load all task files properly
12
+ โœ… **Added missing `list_schemas` method** to `SchemaSwitcher` class
13
+ โœ… **Fixed Rails 8 migration_context compatibility** - Updated migration handling for Rails 8
14
+
15
+ ## ๐Ÿš€ How to Test the Rails Tasks
16
+
17
+ ### **Option 1: Create a Test Rails App**
18
+
19
+ ```bash
20
+ # Create a new Rails app for testing
21
+ rails new test_multitenancy --database=postgresql
22
+ cd test_multitenancy
23
+
24
+ # Add the gem to Gemfile
25
+ echo 'gem "pg_multitenant_schemas", path: "/Users/rubenpaz/personal/pg_multitenant_schemas"' >> Gemfile
26
+
27
+ # Install the gem
28
+ bundle install
29
+
30
+ # Now you can test the tasks
31
+ rails tenants:list
32
+ rails tenants:status
33
+ rails tenants:migrate
34
+ ```
35
+
36
+ ### **Option 2: Use an Existing Rails App**
37
+
38
+ ```bash
39
+ # Navigate to your existing Rails app
40
+ cd /Users/rubenpaz/personal/lbyte-security # or wherever your Rails app is
41
+
42
+ # Add gem to Gemfile if not already added
43
+ # gem "pg_multitenant_schemas", path: "/Users/rubenpaz/personal/pg_multitenant_schemas"
44
+
45
+ # Install and test
46
+ bundle install
47
+ rails tenants:list
48
+ ```
49
+
50
+ ### **Option 3: Test in the Example Directory**
51
+
52
+ ```bash
53
+ # If there's a Rails example in the gem
54
+ cd /Users/rubenpaz/personal/pg_multitenant_schemas/examples
55
+ # Follow instructions in that directory
56
+ ```
57
+
58
+ ## ๐Ÿ“‹ Available Tasks
59
+
60
+ After including the gem in a Rails app, you'll have these tasks:
61
+
62
+ ### **Basic Tasks**
63
+ - `rails tenants:list` - List all tenant schemas
64
+ - `rails tenants:status` - Show migration status for all tenants
65
+ - `rails tenants:migrate` - Run migrations for all tenant schemas
66
+
67
+ ### **Advanced Tasks**
68
+ - `rails tenants:migrate_tenant[schema_name]` - Run migrations for specific tenant
69
+ - `rails tenants:create[schema_name]` - Setup new tenant with schema and migrations
70
+ - `rails tenants:setup` - Setup schemas and run migrations for all existing tenants
71
+
72
+ ### **Management Tasks**
73
+ - `rails tenants:new[attributes]` - Create new tenant with attributes (JSON format)
74
+ - `rails tenants:drop[schema_name]` - Drop tenant schema (DANGEROUS)
75
+ - `rails tenants:rollback[schema_name,steps]` - Rollback migrations for a tenant
76
+
77
+ ### **Convenience Aliases**
78
+ - `rails tenants:db:create[schema_name]` - Alias for `tenants:create`
79
+ - `rails tenants:db:migrate` - Alias for `tenants:migrate`
80
+ - `rails tenants:db:status` - Alias for `tenants:status`
81
+
82
+ ### **Legacy Tasks (Deprecated)**
83
+ - `rails pg_multitenant_schemas:list_schemas` - Use `tenants:list` instead
84
+ - `rails pg_multitenant_schemas:migrate_all` - Use `tenants:migrate` instead
85
+
86
+ ## ๐Ÿ› ๏ธ Troubleshooting
87
+
88
+ ### "Command not found" or "stack level too deep"
89
+ - โœ… **Fixed**: Circular dependency in task loading removed
90
+ - Make sure you're in a Rails application directory
91
+ - Run `bundle install` after adding the gem
92
+
93
+ ### "NoMethodError: undefined method 'list_schemas'"
94
+ - โœ… **Fixed**: Added missing `list_schemas` method to `SchemaSwitcher`
95
+ - Update to the latest version of the gem
96
+
97
+ ### "NoMethodError: undefined method 'migration_context'"
98
+ - โœ… **Fixed**: Updated migration handling for Rails 8 compatibility
99
+ - The gem now properly handles migration context access across Rails versions
100
+ - Update to the latest version of the gem
101
+
102
+ ### "No such task"
103
+ - Ensure the gem is properly added to your Rails app's Gemfile
104
+ - Run `bundle install`
105
+ - Check that the gem is loading: `rails runner "puts PgMultitenantSchemas::VERSION"`
106
+
107
+ ### "Environment not loaded"
108
+ - The tasks require `:environment`, so they need a proper Rails environment
109
+ - Make sure your Rails app's database is configured and accessible
110
+
111
+ ## ๐Ÿงช Quick Test
112
+
113
+ To verify everything works, create a minimal test:
114
+
115
+ ```bash
116
+ # In your Rails app directory
117
+ rails runner "puts 'Gem loaded: ' + PgMultitenantSchemas::VERSION"
118
+ rails tenants:list
119
+ ```
120
+
121
+ ## ๐Ÿ“ Task File Structure
122
+
123
+ The gem now has a clean task structure:
124
+ - `basic_tasks.rake` - List, status, migrate tasks
125
+ - `advanced_tasks.rake` - Advanced tenant management
126
+ - `tenant_tasks.rake` - Management tasks + aliases + legacy compatibility
127
+
128
+ All are loaded automatically via the Rails railtie when you include the gem in your Rails application.
@@ -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
@@ -34,21 +34,115 @@ module PgMultitenantSchemas
34
34
  end
35
35
 
36
36
  def run_migrations
37
- ActiveRecord::Base.connection.migrate
37
+ # Use the migration context to run migrations
38
+ migration_context_obj = migration_context
39
+ return unless migration_context_obj
40
+
41
+ migration_context_obj.migrate
38
42
  end
39
43
 
40
44
  def pending_migrations
41
- ActiveRecord::Base.connection.migration_context.migrations.reject do |migration|
42
- ActiveRecord::Base.connection.migration_context.get_all_versions.include?(migration.version)
45
+ migration_context_obj = migration_context
46
+ return [] unless migration_context_obj
47
+
48
+ all_migrations = migration_context_obj.migrations
49
+ applied_version_list = applied_versions
50
+
51
+ all_migrations.reject do |migration|
52
+ applied_version_list.include?(migration.version)
43
53
  end
44
54
  end
45
55
 
46
56
  def applied_migrations
47
- ActiveRecord::Base.connection.migration_context.get_all_versions
57
+ applied_versions
48
58
  end
49
59
 
50
60
  def migration_paths
51
- ActiveRecord::Base.connection.migration_context.migrations_paths
61
+ migration_context_obj = migration_context
62
+ if migration_context_obj.respond_to?(:migrations_paths)
63
+ migration_context_obj.migrations_paths
64
+ elsif defined?(::Rails) && ::Rails.application
65
+ # Fallback to default Rails migration paths
66
+ ::Rails.application.paths["db/migrate"].expanded
67
+ else
68
+ ["db/migrate"]
69
+ end
70
+ end
71
+
72
+ def migration_context
73
+ # Return nil if ActiveRecord is not available (for tests)
74
+ return nil unless defined?(ActiveRecord::Base)
75
+
76
+ # Rails 8 compatibility: Try multiple approaches
77
+ find_migration_context
78
+ rescue StandardError => e
79
+ # Use explicit Rails logger to avoid namespace conflicts
80
+ ::Rails.logger&.warn("Failed to get migration context: #{e.message}") if defined?(::Rails)
81
+ nil
82
+ end
83
+
84
+ def find_migration_context
85
+ if ActiveRecord::Base.respond_to?(:migration_context)
86
+ # Rails 8+: Try base migration context first
87
+ ActiveRecord::Base.migration_context
88
+ elsif ActiveRecord::Base.connection.respond_to?(:migration_context)
89
+ # Rails 7: Use connection migration context
90
+ ActiveRecord::Base.connection.migration_context
91
+ elsif defined?(ActiveRecord::MigrationContext)
92
+ # Fallback: Create a new migration context with default paths
93
+ create_fallback_migration_context
94
+ end
95
+ end
96
+
97
+ def create_fallback_migration_context
98
+ paths = if defined?(::Rails) && ::Rails.application
99
+ ::Rails.application.paths["db/migrate"].expanded
100
+ else
101
+ ["db/migrate"]
102
+ end
103
+ ActiveRecord::MigrationContext.new(paths)
104
+ end
105
+
106
+ # Get applied migration versions with Rails 8 compatibility
107
+ def applied_versions
108
+ # Return empty array if ActiveRecord is not available (for tests)
109
+ return [] unless defined?(ActiveRecord::Base)
110
+
111
+ applied_versions_from_context || applied_versions_from_database
112
+ rescue StandardError => e
113
+ # If anything fails, return empty array
114
+ ::Rails.logger&.warn("Failed to get applied versions: #{e.message}") if defined?(::Rails)
115
+ []
116
+ end
117
+
118
+ def applied_versions_from_context
119
+ # Try using migration context first (for tests and Rails 7)
120
+ migration_context_obj = migration_context
121
+ return migration_context_obj.get_all_versions if migration_context_obj.respond_to?(:get_all_versions)
122
+
123
+ nil
124
+ end
125
+
126
+ def applied_versions_from_database
127
+ # Fallback to direct database query (Rails 8)
128
+ connection = ActiveRecord::Base.connection
129
+ table_name = "schema_migrations"
130
+
131
+ ensure_schema_migrations_table_exists(connection, table_name)
132
+
133
+ # Query the table directly for maximum compatibility
134
+ connection.select_values("SELECT version FROM #{table_name} ORDER BY version")
135
+ end
136
+
137
+ def ensure_schema_migrations_table_exists(connection, table_name)
138
+ # Ensure the schema_migrations table exists
139
+ return if connection.table_exists?(table_name)
140
+
141
+ # Create the table if it doesn't exist
142
+ connection.create_table(table_name, id: false) do |t|
143
+ t.string :version, null: false
144
+ end
145
+ connection.add_index(table_name, :version, unique: true, name: "unique_schema_migrations")
52
146
  end
53
147
  end
54
148
  end
@@ -73,15 +73,72 @@ module PgMultitenantSchemas
73
73
  puts "โช Rolling back #{steps} steps for #{schema_name}" if verbose
74
74
  original_schema = current_schema
75
75
 
76
- begin
77
- switch_to_schema(schema_name)
78
- ActiveRecord::Base.connection.migration_context.rollback(migration_paths, steps)
79
- puts " โœ… Rollback completed" if verbose
80
- rescue StandardError => e
81
- puts " โŒ Rollback failed: #{e.message}" if verbose
82
- raise
83
- ensure
84
- switch_to_schema(original_schema) if original_schema
76
+ perform_rollback(schema_name, steps, verbose)
77
+ ensure
78
+ switch_to_schema(original_schema) if original_schema
79
+ end
80
+
81
+ private
82
+
83
+ def perform_rollback(schema_name, steps, verbose)
84
+ switch_to_schema(schema_name)
85
+ migration_context_obj = migration_context
86
+
87
+ unless migration_context_obj
88
+ puts " โŒ Cannot rollback: migration context not available" if verbose
89
+ return
90
+ end
91
+
92
+ migration_context_obj.rollback(migration_paths, steps)
93
+ puts " โœ… Rollback completed" if verbose
94
+ rescue StandardError => e
95
+ puts " โŒ Rollback failed: #{e.message}" if verbose
96
+ raise
97
+ end
98
+
99
+ def migration_context
100
+ # Return nil if ActiveRecord is not available (for tests)
101
+ return nil unless defined?(ActiveRecord::Base)
102
+
103
+ # Rails 8 compatibility: Try multiple approaches
104
+ find_migration_context
105
+ rescue StandardError => e
106
+ # Use explicit Rails logger to avoid namespace conflicts
107
+ ::Rails.logger&.warn("Failed to get migration context: #{e.message}") if defined?(::Rails)
108
+ nil
109
+ end
110
+
111
+ def find_migration_context
112
+ if ActiveRecord::Base.respond_to?(:migration_context)
113
+ # Rails 8+: Try base migration context first
114
+ ActiveRecord::Base.migration_context
115
+ elsif ActiveRecord::Base.connection.respond_to?(:migration_context)
116
+ # Rails 7: Use connection migration context
117
+ ActiveRecord::Base.connection.migration_context
118
+ elsif defined?(ActiveRecord::MigrationContext)
119
+ # Fallback: Create a new migration context with default paths
120
+ create_fallback_migration_context
121
+ end
122
+ end
123
+
124
+ def create_fallback_migration_context
125
+ paths = if defined?(::Rails) && ::Rails.application
126
+ ::Rails.application.paths["db/migrate"].expanded
127
+ else
128
+ ["db/migrate"]
129
+ end
130
+ ActiveRecord::MigrationContext.new(paths)
131
+ end
132
+
133
+ def migration_paths
134
+ migration_context_obj = migration_context
135
+ if migration_context_obj.respond_to?(:migrations_paths)
136
+ migration_context_obj.migrations_paths
137
+ elsif defined?(::Rails) && ::Rails.application
138
+ # Fallback to default Rails migration paths
139
+ ::Rails.application.paths["db/migrate"].expanded
140
+ else
141
+ ["db/migrate"]
85
142
  end
86
143
  end
87
144
  end
@@ -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