pg_multitenant_schemas 0.1.3 ā 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.ruby-version +1 -0
- data/CHANGELOG.md +46 -0
- data/README.md +269 -16
- data/docs/README.md +77 -0
- data/docs/configuration.md +340 -0
- data/docs/context.md +292 -0
- data/docs/errors.md +498 -0
- data/docs/integration_testing.md +454 -0
- data/docs/migrator.md +291 -0
- data/docs/rails_integration.md +468 -0
- data/docs/schema_switcher.md +182 -0
- data/docs/tenant_resolver.md +394 -0
- data/docs/testing.md +358 -0
- data/examples/context_management.rb +198 -0
- data/examples/migration_workflow.rb +50 -0
- data/examples/rails_integration/controller_examples.rb +368 -0
- data/examples/schema_operations.rb +124 -0
- data/lib/pg_multitenant_schemas/configuration.rb +4 -4
- data/lib/pg_multitenant_schemas/migration_display_reporter.rb +30 -0
- data/lib/pg_multitenant_schemas/migration_executor.rb +81 -0
- data/lib/pg_multitenant_schemas/migration_schema_operations.rb +54 -0
- data/lib/pg_multitenant_schemas/migration_status_reporter.rb +65 -0
- data/lib/pg_multitenant_schemas/migrator.rb +89 -0
- data/lib/pg_multitenant_schemas/schema_switcher.rb +40 -66
- data/lib/pg_multitenant_schemas/tasks/advanced_tasks.rake +21 -0
- data/lib/pg_multitenant_schemas/tasks/basic_tasks.rake +20 -0
- data/lib/pg_multitenant_schemas/tasks/pg_multitenant_schemas.rake +53 -143
- data/lib/pg_multitenant_schemas/tasks/tenant_tasks.rake +65 -0
- data/lib/pg_multitenant_schemas/tenant_task_helpers.rb +102 -0
- data/lib/pg_multitenant_schemas/version.rb +1 -1
- data/lib/pg_multitenant_schemas.rb +10 -5
- data/pg_multitenant_schemas.gemspec +10 -9
- data/rails_integration/app/controllers/application_controller.rb +6 -0
- data/rails_integration/app/models/tenant.rb +6 -0
- metadata +39 -17
|
@@ -0,0 +1,368 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Rails Controller Integration Examples
|
|
4
|
+
# Demonstrates various controller patterns for multi-tenant applications
|
|
5
|
+
|
|
6
|
+
# Example 1: Basic Application Controller Setup
|
|
7
|
+
# Provides tenant resolution and access control for Rails applications
|
|
8
|
+
class ApplicationController < ActionController::Base
|
|
9
|
+
include PgMultitenantSchemas::ControllerConcern
|
|
10
|
+
|
|
11
|
+
protect_from_forgery with: :exception
|
|
12
|
+
before_action :authenticate_user!
|
|
13
|
+
before_action :ensure_tenant_access
|
|
14
|
+
|
|
15
|
+
protected
|
|
16
|
+
|
|
17
|
+
# Custom tenant resolution from request
|
|
18
|
+
def resolve_tenant_from_request
|
|
19
|
+
resolve_by_subdomain || resolve_by_domain || resolve_by_user_preference
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
private
|
|
23
|
+
|
|
24
|
+
def resolve_by_subdomain
|
|
25
|
+
return nil unless request.subdomain.present?
|
|
26
|
+
|
|
27
|
+
Tenant.active.find_by(subdomain: request.subdomain)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def resolve_by_domain
|
|
31
|
+
return nil unless request.domain.present?
|
|
32
|
+
|
|
33
|
+
Tenant.active.find_by(domain: request.host)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def resolve_by_user_preference
|
|
37
|
+
return nil unless user_signed_in? && current_user.current_tenant_id.present?
|
|
38
|
+
|
|
39
|
+
current_user.tenants.find_by(id: current_user.current_tenant_id)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def tenant_access_valid?
|
|
43
|
+
@current_tenant && current_user&.has_access_to?(@current_tenant)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def ensure_tenant_access
|
|
47
|
+
return handle_missing_tenant unless @current_tenant
|
|
48
|
+
return handle_access_denied unless tenant_access_valid?
|
|
49
|
+
|
|
50
|
+
true
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def handle_missing_tenant
|
|
54
|
+
respond_to do |format|
|
|
55
|
+
format.html { redirect_to tenant_selection_path, alert: "Please select a tenant" }
|
|
56
|
+
format.json { render json: { error: "Tenant required" }, status: :unauthorized }
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def handle_access_denied
|
|
61
|
+
respond_to do |format|
|
|
62
|
+
format.html { redirect_to root_path, alert: "Access denied" }
|
|
63
|
+
format.json { render json: { error: "Access denied" }, status: :forbidden }
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Helper method to access current tenant in views
|
|
68
|
+
helper_method :current_tenant
|
|
69
|
+
attr_reader :current_tenant
|
|
70
|
+
|
|
71
|
+
# Custom error handling for tenant-related errors
|
|
72
|
+
rescue_from PgMultitenantSchemas::TenantNotFoundError do |error|
|
|
73
|
+
Rails.logger.warn "Tenant not found: #{error.message}"
|
|
74
|
+
|
|
75
|
+
respond_to do |format|
|
|
76
|
+
format.html { redirect_to tenant_selection_path, alert: "Tenant not found" }
|
|
77
|
+
format.json { render json: { error: "Tenant not found" }, status: :not_found }
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
rescue_from PgMultitenantSchemas::SchemaError do |error|
|
|
82
|
+
Rails.logger.error "Schema error: #{error.message}"
|
|
83
|
+
|
|
84
|
+
respond_to do |format|
|
|
85
|
+
format.html { redirect_to root_path, alert: "A database error occurred" }
|
|
86
|
+
format.json { render json: { error: "Database error" }, status: :internal_server_error }
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# Example 2: Admin Controller with Multi-Tenant Management
|
|
92
|
+
module Admin
|
|
93
|
+
# Controller for managing tenants in admin interface
|
|
94
|
+
# Allows administrators to view, manage, and switch between tenant contexts
|
|
95
|
+
class TenantsController < Admin::BaseController
|
|
96
|
+
# Admin controllers might need to switch between tenants
|
|
97
|
+
|
|
98
|
+
def index
|
|
99
|
+
# List all tenants (admin view, not tenant-scoped)
|
|
100
|
+
@tenants = Tenant.includes(:users, :subscriptions).order(:name)
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def show
|
|
104
|
+
@tenant = Tenant.find(params[:id])
|
|
105
|
+
|
|
106
|
+
# Get tenant-specific statistics
|
|
107
|
+
@stats = PgMultitenantSchemas::Context.with_tenant(@tenant) do
|
|
108
|
+
{
|
|
109
|
+
users_count: User.count,
|
|
110
|
+
orders_count: Order.count,
|
|
111
|
+
revenue: Order.sum(:total),
|
|
112
|
+
last_activity: User.maximum(:last_sign_in_at)
|
|
113
|
+
}
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def switch
|
|
118
|
+
@tenant = Tenant.find(params[:id])
|
|
119
|
+
|
|
120
|
+
# Allow admin to switch to tenant for support
|
|
121
|
+
if current_user.admin? && @tenant
|
|
122
|
+
session[:admin_impersonating_tenant] = @tenant.id
|
|
123
|
+
redirect_to root_path, notice: "Switched to tenant: #{@tenant.name}"
|
|
124
|
+
else
|
|
125
|
+
redirect_to admin_tenants_path, alert: "Access denied"
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def stop_impersonation
|
|
130
|
+
session.delete(:admin_impersonating_tenant)
|
|
131
|
+
redirect_to admin_tenants_path, notice: "Stopped tenant impersonation"
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
private
|
|
135
|
+
|
|
136
|
+
def resolve_tenant_from_request
|
|
137
|
+
# Admin impersonation takes precedence
|
|
138
|
+
if session[:admin_impersonating_tenant] && current_user&.admin?
|
|
139
|
+
return Tenant.find_by(id: session[:admin_impersonating_tenant])
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
# Fall back to normal resolution
|
|
143
|
+
super
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
# Example 3: API Controller with JWT-based Tenant Resolution
|
|
149
|
+
module Api
|
|
150
|
+
module V1
|
|
151
|
+
# Base controller for API endpoints with tenant resolution
|
|
152
|
+
# Supports JWT and API key authentication with tenant context
|
|
153
|
+
class BaseController < ActionController::API
|
|
154
|
+
include PgMultitenantSchemas::ControllerConcern
|
|
155
|
+
|
|
156
|
+
before_action :authenticate_api_request
|
|
157
|
+
|
|
158
|
+
protected
|
|
159
|
+
|
|
160
|
+
def resolve_tenant_from_request
|
|
161
|
+
# Extract tenant from JWT payload
|
|
162
|
+
return @jwt_payload["tenant"] if @jwt_payload&.key?("tenant")
|
|
163
|
+
|
|
164
|
+
# Extract from API key
|
|
165
|
+
return @api_key.tenant if @api_key&.tenant
|
|
166
|
+
|
|
167
|
+
# Extract from header
|
|
168
|
+
tenant_id = request.headers["X-Tenant-ID"]
|
|
169
|
+
return Tenant.find(tenant_id) if tenant_id.present?
|
|
170
|
+
|
|
171
|
+
nil
|
|
172
|
+
rescue ActiveRecord::RecordNotFound
|
|
173
|
+
nil
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
private
|
|
177
|
+
|
|
178
|
+
def authenticate_api_request
|
|
179
|
+
token = extract_token_from_header
|
|
180
|
+
|
|
181
|
+
if token.present?
|
|
182
|
+
@jwt_payload = decode_jwt_token(token)
|
|
183
|
+
@api_key = ApiKey.active.find_by(token: token) if @jwt_payload.nil?
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
return if @jwt_payload || @api_key
|
|
187
|
+
|
|
188
|
+
render json: { error: "Unauthorized" }, status: :unauthorized
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
def extract_token_from_header
|
|
192
|
+
auth_header = request.headers["Authorization"]
|
|
193
|
+
auth_header&.split&.last if auth_header&.start_with?("Bearer ")
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
def decode_jwt_token(token)
|
|
197
|
+
JWT.decode(token, Rails.application.secret_key_base, true, algorithm: "HS256").first
|
|
198
|
+
rescue JWT::DecodeError, JWT::ExpiredSignature
|
|
199
|
+
nil
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
# API-specific error handling
|
|
203
|
+
rescue_from PgMultitenantSchemas::TenantNotFoundError do |error|
|
|
204
|
+
render json: {
|
|
205
|
+
error: "tenant_not_found",
|
|
206
|
+
message: error.message,
|
|
207
|
+
code: "TENANT_NOT_FOUND"
|
|
208
|
+
}, status: :not_found
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
rescue_from PgMultitenantSchemas::SchemaError do |_error|
|
|
212
|
+
render json: {
|
|
213
|
+
error: "schema_error",
|
|
214
|
+
message: "Database schema error",
|
|
215
|
+
code: "SCHEMA_ERROR"
|
|
216
|
+
}, status: :internal_server_error
|
|
217
|
+
end
|
|
218
|
+
end
|
|
219
|
+
end
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
# Example 4: Background Job Controller Integration
|
|
223
|
+
class JobsController < ApplicationController
|
|
224
|
+
def create
|
|
225
|
+
# Enqueue job with current tenant context
|
|
226
|
+
ProcessDataJob.perform_later(
|
|
227
|
+
current_tenant.id,
|
|
228
|
+
params[:data_type],
|
|
229
|
+
params[:options]
|
|
230
|
+
)
|
|
231
|
+
|
|
232
|
+
render json: {
|
|
233
|
+
message: "Job enqueued successfully",
|
|
234
|
+
tenant: current_tenant.subdomain
|
|
235
|
+
}
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
def status
|
|
239
|
+
# Check job status within tenant context
|
|
240
|
+
job_id = params[:job_id]
|
|
241
|
+
|
|
242
|
+
# Job status checking happens in tenant context
|
|
243
|
+
job_status = Rails.cache.read("job_status:#{current_tenant.id}:#{job_id}")
|
|
244
|
+
|
|
245
|
+
render json: {
|
|
246
|
+
job_id: job_id,
|
|
247
|
+
status: job_status || "not_found",
|
|
248
|
+
tenant: current_tenant.subdomain
|
|
249
|
+
}
|
|
250
|
+
end
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
# Example 5: Multi-Strategy Tenant Resolution Controller
|
|
254
|
+
class FlexibleController < ApplicationController
|
|
255
|
+
protected
|
|
256
|
+
|
|
257
|
+
def resolve_tenant_from_request
|
|
258
|
+
# Try multiple resolution strategies in order
|
|
259
|
+
strategies = %i[
|
|
260
|
+
resolve_by_custom_header
|
|
261
|
+
resolve_by_subdomain
|
|
262
|
+
resolve_by_path_parameter
|
|
263
|
+
resolve_by_user_preference
|
|
264
|
+
resolve_by_session
|
|
265
|
+
]
|
|
266
|
+
|
|
267
|
+
strategies.each do |strategy|
|
|
268
|
+
tenant = send(strategy)
|
|
269
|
+
return tenant if tenant
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
nil
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
private
|
|
276
|
+
|
|
277
|
+
def resolve_by_custom_header
|
|
278
|
+
tenant_identifier = request.headers["X-Tenant-Identifier"]
|
|
279
|
+
return nil unless tenant_identifier.present?
|
|
280
|
+
|
|
281
|
+
Tenant.find_by(subdomain: tenant_identifier) ||
|
|
282
|
+
Tenant.find_by(slug: tenant_identifier)
|
|
283
|
+
end
|
|
284
|
+
|
|
285
|
+
def resolve_by_subdomain
|
|
286
|
+
return nil unless request.subdomain.present?
|
|
287
|
+
|
|
288
|
+
Tenant.find_by(subdomain: request.subdomain)
|
|
289
|
+
end
|
|
290
|
+
|
|
291
|
+
def resolve_by_path_parameter
|
|
292
|
+
# For routes like /t/:tenant_slug/dashboard
|
|
293
|
+
tenant_slug = params[:tenant_slug]
|
|
294
|
+
return nil unless tenant_slug.present?
|
|
295
|
+
|
|
296
|
+
Tenant.find_by(slug: tenant_slug)
|
|
297
|
+
end
|
|
298
|
+
|
|
299
|
+
def resolve_by_user_preference
|
|
300
|
+
return nil unless user_signed_in?
|
|
301
|
+
|
|
302
|
+
tenant_id = current_user.preferred_tenant_id
|
|
303
|
+
current_user.tenants.find_by(id: tenant_id) if tenant_id.present?
|
|
304
|
+
end
|
|
305
|
+
|
|
306
|
+
def resolve_by_session
|
|
307
|
+
tenant_id = session[:selected_tenant_id]
|
|
308
|
+
Tenant.find_by(id: tenant_id) if tenant_id.present?
|
|
309
|
+
end
|
|
310
|
+
end
|
|
311
|
+
|
|
312
|
+
# Example 6: WebSocket Integration
|
|
313
|
+
module ApplicationCable
|
|
314
|
+
# WebSocket connection with tenant context support
|
|
315
|
+
# Establishes tenant context for real-time communication
|
|
316
|
+
class Connection < ActionCable::Connection::Base
|
|
317
|
+
identified_by :current_user, :current_tenant
|
|
318
|
+
|
|
319
|
+
def connect
|
|
320
|
+
self.current_user = find_verified_user
|
|
321
|
+
self.current_tenant = find_tenant_for_user
|
|
322
|
+
|
|
323
|
+
# Set tenant context for the connection
|
|
324
|
+
PgMultitenantSchemas::Context.switch_to_tenant(current_tenant) if current_tenant
|
|
325
|
+
end
|
|
326
|
+
|
|
327
|
+
private
|
|
328
|
+
|
|
329
|
+
def find_verified_user
|
|
330
|
+
if (verified_user = User.find_by(id: cookies.signed[:user_id]))
|
|
331
|
+
verified_user
|
|
332
|
+
else
|
|
333
|
+
reject_unauthorized_connection
|
|
334
|
+
end
|
|
335
|
+
end
|
|
336
|
+
|
|
337
|
+
def find_tenant_for_user
|
|
338
|
+
# Extract tenant from connection params or user preferences
|
|
339
|
+
tenant_id = request.params[:tenant_id] || current_user.current_tenant_id
|
|
340
|
+
current_user.tenants.find_by(id: tenant_id)
|
|
341
|
+
end
|
|
342
|
+
end
|
|
343
|
+
end
|
|
344
|
+
|
|
345
|
+
# Example WebSocket Channel with Tenant Context
|
|
346
|
+
class NotificationsChannel < ApplicationCable::Channel
|
|
347
|
+
def subscribed
|
|
348
|
+
# Subscribe to tenant-specific notifications
|
|
349
|
+
if current_tenant
|
|
350
|
+
stream_from "notifications_#{current_tenant.id}"
|
|
351
|
+
else
|
|
352
|
+
reject
|
|
353
|
+
end
|
|
354
|
+
end
|
|
355
|
+
|
|
356
|
+
def receive(data)
|
|
357
|
+
# All operations happen within tenant context
|
|
358
|
+
PgMultitenantSchemas::Context.with_tenant(current_tenant) do
|
|
359
|
+
case data["action"]
|
|
360
|
+
when "mark_as_read"
|
|
361
|
+
notification = Notification.find(data["notification_id"])
|
|
362
|
+
notification.mark_as_read!
|
|
363
|
+
when "create"
|
|
364
|
+
Notification.create!(data["notification"])
|
|
365
|
+
end
|
|
366
|
+
end
|
|
367
|
+
end
|
|
368
|
+
end
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
# Schema Operations Example
|
|
5
|
+
# Demonstrates core schema switching and management operations
|
|
6
|
+
|
|
7
|
+
require_relative "../lib/pg_multitenant_schemas"
|
|
8
|
+
|
|
9
|
+
puts "š§ PG Multitenant Schemas - Schema Operations Example"
|
|
10
|
+
puts "====================================================="
|
|
11
|
+
|
|
12
|
+
# Example 1: Basic Schema Switching
|
|
13
|
+
puts "\nš Example 1: Basic Schema Switching"
|
|
14
|
+
puts "------------------------------------"
|
|
15
|
+
|
|
16
|
+
# Switch to a tenant schema
|
|
17
|
+
schema_name = "demo_tenant"
|
|
18
|
+
puts "Switching to schema: #{schema_name}"
|
|
19
|
+
|
|
20
|
+
begin
|
|
21
|
+
PgMultitenantSchemas::SchemaSwitcher.switch_schema(schema_name)
|
|
22
|
+
current = PgMultitenantSchemas::SchemaSwitcher.current_schema
|
|
23
|
+
puts "ā
Current schema: #{current}"
|
|
24
|
+
rescue PgMultitenantSchemas::SchemaError => e
|
|
25
|
+
puts "ā Schema switch failed: #{e.message}"
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Example 2: Schema Creation and Management
|
|
29
|
+
puts "\nšļø Example 2: Schema Creation and Management"
|
|
30
|
+
puts "---------------------------------------------"
|
|
31
|
+
|
|
32
|
+
new_schema = "example_tenant"
|
|
33
|
+
puts "Creating schema: #{new_schema}"
|
|
34
|
+
|
|
35
|
+
begin
|
|
36
|
+
# Create new schema
|
|
37
|
+
PgMultitenantSchemas::SchemaSwitcher.create_schema(new_schema)
|
|
38
|
+
puts "ā
Schema created: #{new_schema}"
|
|
39
|
+
|
|
40
|
+
# Verify schema exists
|
|
41
|
+
puts "ā
Schema exists: #{new_schema}" if PgMultitenantSchemas::SchemaSwitcher.schema_exists?(new_schema)
|
|
42
|
+
|
|
43
|
+
# List all schemas
|
|
44
|
+
schemas = PgMultitenantSchemas::SchemaSwitcher.list_schemas
|
|
45
|
+
puts "š Available schemas: #{schemas.join(", ")}"
|
|
46
|
+
rescue PgMultitenantSchemas::SchemaError => e
|
|
47
|
+
puts "ā Schema operation failed: #{e.message}"
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Example 3: Safe Schema Operations
|
|
51
|
+
puts "\nš”ļø Example 3: Safe Schema Operations"
|
|
52
|
+
puts "------------------------------------"
|
|
53
|
+
|
|
54
|
+
begin
|
|
55
|
+
# Store current schema
|
|
56
|
+
original_schema = PgMultitenantSchemas::SchemaSwitcher.current_schema
|
|
57
|
+
puts "Original schema: #{original_schema}"
|
|
58
|
+
|
|
59
|
+
# Switch to tenant schema
|
|
60
|
+
PgMultitenantSchemas::SchemaSwitcher.switch_schema("demo_tenant")
|
|
61
|
+
puts "Switched to: #{PgMultitenantSchemas::SchemaSwitcher.current_schema}"
|
|
62
|
+
|
|
63
|
+
# Execute some operations here...
|
|
64
|
+
puts "Performing tenant-specific operations..."
|
|
65
|
+
|
|
66
|
+
# Always restore original schema
|
|
67
|
+
PgMultitenantSchemas::SchemaSwitcher.switch_schema(original_schema)
|
|
68
|
+
puts "ā
Restored to original schema: #{PgMultitenantSchemas::SchemaSwitcher.current_schema}"
|
|
69
|
+
rescue PgMultitenantSchemas::SchemaError => e
|
|
70
|
+
puts "ā Error: #{e.message}"
|
|
71
|
+
# Ensure we restore the original schema even on error
|
|
72
|
+
begin
|
|
73
|
+
PgMultitenantSchemas::SchemaSwitcher.switch_schema(original_schema)
|
|
74
|
+
rescue StandardError
|
|
75
|
+
nil
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Example 4: Schema Validation
|
|
80
|
+
puts "\nā
Example 4: Schema Validation"
|
|
81
|
+
puts "-------------------------------"
|
|
82
|
+
|
|
83
|
+
test_schemas = ["valid_schema", "invalid-schema!", "", "another_valid_123"]
|
|
84
|
+
|
|
85
|
+
test_schemas.each do |schema|
|
|
86
|
+
puts "Testing schema name: '#{schema}'"
|
|
87
|
+
|
|
88
|
+
if schema.match?(/\A[a-zA-Z_][a-zA-Z0-9_]*\z/) && !schema.blank?
|
|
89
|
+
puts " ā
Valid schema name"
|
|
90
|
+
|
|
91
|
+
# Try to create (for demonstration)
|
|
92
|
+
# PgMultitenantSchemas::SchemaSwitcher.create_schema(schema)
|
|
93
|
+
else
|
|
94
|
+
puts " ā Invalid schema name format"
|
|
95
|
+
end
|
|
96
|
+
rescue PgMultitenantSchemas::SchemaError => e
|
|
97
|
+
puts " ā Schema error: #{e.message}"
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# Example 5: Schema Cleanup
|
|
101
|
+
puts "\nš§¹ Example 5: Schema Cleanup"
|
|
102
|
+
puts "----------------------------"
|
|
103
|
+
|
|
104
|
+
cleanup_schema = "example_tenant"
|
|
105
|
+
puts "Cleaning up schema: #{cleanup_schema}"
|
|
106
|
+
|
|
107
|
+
begin
|
|
108
|
+
if PgMultitenantSchemas::SchemaSwitcher.schema_exists?(cleanup_schema)
|
|
109
|
+
PgMultitenantSchemas::SchemaSwitcher.drop_schema(cleanup_schema)
|
|
110
|
+
puts "ā
Schema dropped: #{cleanup_schema}"
|
|
111
|
+
else
|
|
112
|
+
puts "ā¹ļø Schema doesn't exist: #{cleanup_schema}"
|
|
113
|
+
end
|
|
114
|
+
rescue PgMultitenantSchemas::SchemaError => e
|
|
115
|
+
puts "ā Cleanup failed: #{e.message}"
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
puts "\n⨠Schema operations example completed!"
|
|
119
|
+
puts "\nš Key Takeaways:"
|
|
120
|
+
puts " ⢠Always validate schema names before operations"
|
|
121
|
+
puts " ⢠Use proper error handling for schema operations"
|
|
122
|
+
puts " ⢠Store and restore original schema in ensure blocks"
|
|
123
|
+
puts " ⢠Check schema existence before creation or deletion"
|
|
124
|
+
puts " ⢠Use meaningful schema naming conventions"
|
|
@@ -13,11 +13,11 @@ module PgMultitenantSchemas
|
|
|
13
13
|
@tenant_model_class = "Tenant"
|
|
14
14
|
@default_schema = "public"
|
|
15
15
|
@development_fallback = false
|
|
16
|
-
@excluded_subdomains = %w[www api admin mail ftp blog support help docs]
|
|
17
|
-
@common_tlds = %w[com org net edu gov mil int co uk ca au de fr jp cn]
|
|
16
|
+
@excluded_subdomains = %w[www api admin mail ftp blog support help docs staging]
|
|
17
|
+
@common_tlds = %w[com org net edu gov mil int co uk ca au de fr jp cn dev test]
|
|
18
18
|
@auto_create_schemas = true
|
|
19
|
-
@connection_class = "
|
|
20
|
-
@logger = nil
|
|
19
|
+
@connection_class = "ApplicationRecord"
|
|
20
|
+
@logger = defined?(Rails) && Rails.respond_to?(:logger) ? Rails.logger : nil
|
|
21
21
|
end
|
|
22
22
|
|
|
23
23
|
def tenant_model
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PgMultitenantSchemas
|
|
4
|
+
# Module for migration display and reporting functionality
|
|
5
|
+
# Handles console output and summary reports for migration operations
|
|
6
|
+
module MigrationDisplayReporter
|
|
7
|
+
private
|
|
8
|
+
|
|
9
|
+
def display_migration_summary(results, verbose)
|
|
10
|
+
return unless verbose
|
|
11
|
+
|
|
12
|
+
summary = results.group_by { |r| r[:status] }.transform_values(&:count)
|
|
13
|
+
|
|
14
|
+
puts "\nš Migration Summary:"
|
|
15
|
+
puts " ā
Successful: #{summary[:success] || 0}"
|
|
16
|
+
puts " ā Failed: #{summary[:error] || 0}"
|
|
17
|
+
puts " āļø Skipped: #{summary[:skipped] || 0}"
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def display_setup_summary(results, verbose)
|
|
21
|
+
return unless verbose
|
|
22
|
+
|
|
23
|
+
summary = results.group_by { |r| r[:status] }.transform_values(&:count)
|
|
24
|
+
|
|
25
|
+
puts "\nš Setup Summary:"
|
|
26
|
+
puts " ā
Successful: #{summary[:success] || 0}"
|
|
27
|
+
puts " ā Failed: #{summary[:error] || 0}"
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PgMultitenantSchemas
|
|
4
|
+
# Module for executing migration operations
|
|
5
|
+
# Handles the core migration execution and error handling logic
|
|
6
|
+
module MigrationExecutor
|
|
7
|
+
private
|
|
8
|
+
|
|
9
|
+
def process_schemas_migration(schemas, verbose, ignore_errors)
|
|
10
|
+
puts "š Starting migrations for #{schemas.count} tenant schemas..." if verbose
|
|
11
|
+
|
|
12
|
+
schemas.map do |schema|
|
|
13
|
+
migrate_tenant(schema, verbose: verbose, raise_on_error: !ignore_errors)
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def handle_missing_schema(schema_name, verbose, raise_on_error)
|
|
18
|
+
message = "Schema '#{schema_name}' does not exist"
|
|
19
|
+
puts " ā ļø #{message}" if verbose
|
|
20
|
+
raise StandardError, message if raise_on_error
|
|
21
|
+
|
|
22
|
+
{ schema: schema_name, status: :skipped, message: message }
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def execute_tenant_migration(schema_name, verbose, raise_on_error)
|
|
26
|
+
puts " š¦ Migrating schema: #{schema_name}" if verbose
|
|
27
|
+
original_schema = current_schema
|
|
28
|
+
|
|
29
|
+
begin
|
|
30
|
+
result = perform_migration_for_schema(schema_name, verbose)
|
|
31
|
+
puts " ā
Migration completed" if verbose && result[:status] == :success
|
|
32
|
+
result
|
|
33
|
+
rescue StandardError => e
|
|
34
|
+
handle_migration_error(schema_name, e, verbose, raise_on_error)
|
|
35
|
+
ensure
|
|
36
|
+
switch_to_schema(original_schema) if original_schema
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def perform_migration_for_schema(schema_name, verbose)
|
|
41
|
+
switch_to_schema(schema_name)
|
|
42
|
+
pending_count = pending_migrations.count
|
|
43
|
+
|
|
44
|
+
if pending_count.zero?
|
|
45
|
+
puts " ā¹ļø No pending migrations" if verbose
|
|
46
|
+
return { schema: schema_name, status: :success, message: "No pending migrations" }
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
run_migrations
|
|
50
|
+
{ schema: schema_name, status: :success, message: "#{pending_count} migrations applied" }
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def handle_migration_error(schema_name, error, verbose, raise_on_error)
|
|
54
|
+
message = "Migration failed: #{error.message}"
|
|
55
|
+
puts " ā #{message}" if verbose
|
|
56
|
+
raise error if raise_on_error
|
|
57
|
+
|
|
58
|
+
{ schema: schema_name, status: :error, message: message }
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def create_tenant_schema_if_needed(schema_name, verbose)
|
|
62
|
+
if schema_exists?(schema_name)
|
|
63
|
+
puts " ā¹ļø Schema already exists" if verbose
|
|
64
|
+
else
|
|
65
|
+
create_schema(schema_name)
|
|
66
|
+
puts " ā
Schema created" if verbose
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def validate_tenant_model_exists
|
|
71
|
+
raise "Tenant model not found. Please define a Tenant model." unless defined?(Tenant)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def process_setup_for_tenants(tenants, verbose)
|
|
75
|
+
tenants.map do |tenant|
|
|
76
|
+
schema_name = extract_schema_name(tenant)
|
|
77
|
+
setup_tenant(schema_name, verbose: verbose)
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PgMultitenantSchemas
|
|
4
|
+
# Schema operations for migration management
|
|
5
|
+
module MigrationSchemaOperations
|
|
6
|
+
private
|
|
7
|
+
|
|
8
|
+
# Delegate schema operations to SchemaSwitcher
|
|
9
|
+
def switch_to_schema(schema_name)
|
|
10
|
+
SchemaSwitcher.switch_schema(schema_name)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def create_schema(schema_name)
|
|
14
|
+
SchemaSwitcher.create_schema(schema_name)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def schema_exists?(schema_name)
|
|
18
|
+
SchemaSwitcher.schema_exists?(schema_name)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def current_schema
|
|
22
|
+
SchemaSwitcher.current_schema
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def tenant_schemas
|
|
26
|
+
SchemaSwitcher.list_schemas.reject do |schema|
|
|
27
|
+
%w[information_schema pg_catalog public pg_temp_1 pg_toast_temp_1].include?(schema) ||
|
|
28
|
+
schema.start_with?("pg_toast")
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def extract_schema_name(tenant)
|
|
33
|
+
tenant.respond_to?(:subdomain) ? tenant.subdomain : tenant.to_s
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def run_migrations
|
|
37
|
+
ActiveRecord::Base.connection.migrate
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
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)
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def applied_migrations
|
|
47
|
+
ActiveRecord::Base.connection.migration_context.get_all_versions
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def migration_paths
|
|
51
|
+
ActiveRecord::Base.connection.migration_context.migrations_paths
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|