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.
- checksums.yaml +4 -4
- data/.agent.md +54 -0
- data/.rubocop.yml +7 -1
- data/.ruby-version +1 -1
- data/CHANGELOG.md +58 -2
- data/DELIVERY_SUMMARY.md +318 -0
- data/GITHUB_ACTIONS_QUICK_START.md +110 -0
- data/GITHUB_ACTIONS_SETUP.md +313 -0
- data/GITHUB_ACTIONS_SUMMARY.md +231 -0
- data/LICENSE.txt +1 -1
- data/LOCAL_TESTING_SUMMARY.md +30 -13
- data/README.md +4 -4
- data/READY_TO_RELEASE.md +308 -0
- data/RELEASE_NOTES.md +194 -0
- data/RELEASE_STEPS.md +122 -0
- data/WORKFLOW_FIXES.md +188 -0
- data/docs/index.html +929 -0
- data/docs/testing_rails_tasks.md +128 -0
- data/lib/pg_multitenant_schemas/configuration.rb +7 -1
- data/lib/pg_multitenant_schemas/context.rb +92 -3
- data/lib/pg_multitenant_schemas/migration_schema_operations.rb +99 -5
- data/lib/pg_multitenant_schemas/migrator.rb +66 -9
- data/lib/pg_multitenant_schemas/rails/controller_concern.rb +81 -6
- data/lib/pg_multitenant_schemas/rails/generators/install_generator.rb +27 -0
- data/lib/pg_multitenant_schemas/rails/generators/tenant_migration_generator.rb +30 -0
- data/lib/pg_multitenant_schemas/rails/model_concern.rb +3 -3
- data/lib/pg_multitenant_schemas/rails/railtie.rb +31 -1
- data/lib/pg_multitenant_schemas/schema_switcher.rb +107 -7
- data/lib/pg_multitenant_schemas/tasks/tenant_tasks.rake +0 -3
- data/lib/pg_multitenant_schemas/tenant_resolver.rb +3 -3
- data/lib/pg_multitenant_schemas/ui/app/assets/stylesheets/pg_multitenant_schemas/ui/application.css +94 -0
- data/lib/pg_multitenant_schemas/ui/app/controllers/pg_multitenant_schemas/ui/tenants_controller.rb +130 -0
- data/lib/pg_multitenant_schemas/ui/app/views/layouts/pg_multitenant_schemas/ui/application.html.erb +29 -0
- data/lib/pg_multitenant_schemas/ui/app/views/pg_multitenant_schemas/ui/tenants/index.html.erb +85 -0
- data/lib/pg_multitenant_schemas/ui/app/views/pg_multitenant_schemas/ui/tenants/migration_status.html.erb +45 -0
- data/lib/pg_multitenant_schemas/ui/app/views/pg_multitenant_schemas/ui/tenants/new.html.erb +50 -0
- data/lib/pg_multitenant_schemas/ui/config/routes.rb +20 -0
- data/lib/pg_multitenant_schemas/ui/engine.rb +49 -0
- data/lib/pg_multitenant_schemas/ui.rb +9 -0
- data/lib/pg_multitenant_schemas/version.rb +1 -1
- data/lib/pg_multitenant_schemas.rb +155 -29
- data/pg_multitenant_schemas.gemspec +18 -12
- data/pre-release-check.sh +46 -0
- metadata +44 -17
- 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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
42
|
-
|
|
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
|
-
|
|
57
|
+
applied_versions
|
|
48
58
|
end
|
|
49
59
|
|
|
50
60
|
def migration_paths
|
|
51
|
-
|
|
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
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
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
|
|
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
|
|
43
|
-
|
|
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
|