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.
Files changed (36) hide show
  1. checksums.yaml +4 -4
  2. data/.ruby-version +1 -0
  3. data/CHANGELOG.md +46 -0
  4. data/README.md +269 -16
  5. data/docs/README.md +77 -0
  6. data/docs/configuration.md +340 -0
  7. data/docs/context.md +292 -0
  8. data/docs/errors.md +498 -0
  9. data/docs/integration_testing.md +454 -0
  10. data/docs/migrator.md +291 -0
  11. data/docs/rails_integration.md +468 -0
  12. data/docs/schema_switcher.md +182 -0
  13. data/docs/tenant_resolver.md +394 -0
  14. data/docs/testing.md +358 -0
  15. data/examples/context_management.rb +198 -0
  16. data/examples/migration_workflow.rb +50 -0
  17. data/examples/rails_integration/controller_examples.rb +368 -0
  18. data/examples/schema_operations.rb +124 -0
  19. data/lib/pg_multitenant_schemas/configuration.rb +4 -4
  20. data/lib/pg_multitenant_schemas/migration_display_reporter.rb +30 -0
  21. data/lib/pg_multitenant_schemas/migration_executor.rb +81 -0
  22. data/lib/pg_multitenant_schemas/migration_schema_operations.rb +54 -0
  23. data/lib/pg_multitenant_schemas/migration_status_reporter.rb +65 -0
  24. data/lib/pg_multitenant_schemas/migrator.rb +89 -0
  25. data/lib/pg_multitenant_schemas/schema_switcher.rb +40 -66
  26. data/lib/pg_multitenant_schemas/tasks/advanced_tasks.rake +21 -0
  27. data/lib/pg_multitenant_schemas/tasks/basic_tasks.rake +20 -0
  28. data/lib/pg_multitenant_schemas/tasks/pg_multitenant_schemas.rake +53 -143
  29. data/lib/pg_multitenant_schemas/tasks/tenant_tasks.rake +65 -0
  30. data/lib/pg_multitenant_schemas/tenant_task_helpers.rb +102 -0
  31. data/lib/pg_multitenant_schemas/version.rb +1 -1
  32. data/lib/pg_multitenant_schemas.rb +10 -5
  33. data/pg_multitenant_schemas.gemspec +10 -9
  34. data/rails_integration/app/controllers/application_controller.rb +6 -0
  35. data/rails_integration/app/models/tenant.rb +6 -0
  36. metadata +39 -17
@@ -0,0 +1,394 @@
1
+ # Tenant Resolver - Tenant Identification and Resolution
2
+
3
+ **File**: `lib/pg_multitenant_schemas/tenant_resolver.rb`
4
+
5
+ ## 📋 Overview
6
+
7
+ The `TenantResolver` is responsible for identifying and resolving tenant information from various sources like HTTP requests, subdomains, headers, or custom logic. It provides flexible tenant identification strategies for different application architectures.
8
+
9
+ ## 🎯 Purpose
10
+
11
+ - **Request Analysis**: Extract tenant information from HTTP requests
12
+ - **Flexible Resolution**: Support multiple tenant identification strategies
13
+ - **Caching**: Efficient tenant lookup with caching mechanisms
14
+ - **Error Handling**: Graceful handling of tenant resolution failures
15
+
16
+ ## 🔧 Key Methods
17
+
18
+ ### Core Resolution
19
+
20
+ ```ruby
21
+ # Resolve tenant from request
22
+ TenantResolver.resolve_from_request(request)
23
+
24
+ # Resolve tenant by subdomain
25
+ TenantResolver.resolve_by_subdomain('acme')
26
+
27
+ # Resolve tenant by custom header
28
+ TenantResolver.resolve_by_header(request, 'X-Tenant-ID')
29
+
30
+ # Resolve tenant by domain
31
+ TenantResolver.resolve_by_domain('acme.myapp.com')
32
+ ```
33
+
34
+ ### Configuration and Caching
35
+
36
+ ```ruby
37
+ # Configure resolution strategy
38
+ TenantResolver.configure do |config|
39
+ config.strategy = :subdomain
40
+ config.cache_enabled = true
41
+ config.cache_ttl = 5.minutes
42
+ end
43
+
44
+ # Clear tenant cache
45
+ TenantResolver.clear_cache!
46
+
47
+ # Get cached tenant
48
+ TenantResolver.cached_tenant(identifier)
49
+ ```
50
+
51
+ ## 🏗️ Implementation Details
52
+
53
+ ### Resolution Strategies
54
+
55
+ #### Subdomain Strategy
56
+
57
+ ```ruby
58
+ def resolve_by_subdomain(subdomain)
59
+ return nil if subdomain.blank?
60
+
61
+ tenant_model.find_by(subdomain: subdomain)
62
+ end
63
+
64
+ # Usage in controller
65
+ def resolve_tenant_from_request
66
+ subdomain = request.subdomain
67
+ TenantResolver.resolve_by_subdomain(subdomain)
68
+ end
69
+ ```
70
+
71
+ #### Domain Strategy
72
+
73
+ ```ruby
74
+ def resolve_by_domain(domain)
75
+ return nil if domain.blank?
76
+
77
+ # Extract domain from full host
78
+ domain = extract_domain(domain) if domain.include?('.')
79
+
80
+ tenant_model.find_by(domain: domain)
81
+ end
82
+ ```
83
+
84
+ #### Header Strategy
85
+
86
+ ```ruby
87
+ def resolve_by_header(request, header_name = 'X-Tenant-ID')
88
+ tenant_id = request.headers[header_name]
89
+ return nil if tenant_id.blank?
90
+
91
+ tenant_model.find(tenant_id)
92
+ rescue ActiveRecord::RecordNotFound
93
+ nil
94
+ end
95
+ ```
96
+
97
+ #### Custom Strategy
98
+
99
+ ```ruby
100
+ def resolve_by_custom_logic(request)
101
+ # Implement custom tenant resolution logic
102
+ # Examples:
103
+ # - Path-based: /tenants/acme/dashboard
104
+ # - JWT token: Extract tenant from JWT
105
+ # - Database lookup: Complex tenant hierarchies
106
+
107
+ case request.path
108
+ when /^\/tenants\/(\w+)/
109
+ subdomain = $1
110
+ resolve_by_subdomain(subdomain)
111
+ else
112
+ resolve_by_subdomain(request.subdomain)
113
+ end
114
+ end
115
+ ```
116
+
117
+ ### Caching Implementation
118
+
119
+ ```ruby
120
+ class TenantResolver
121
+ class << self
122
+ def cached_tenant(identifier)
123
+ return resolve_tenant(identifier) unless cache_enabled?
124
+
125
+ Rails.cache.fetch(cache_key(identifier), expires_in: cache_ttl) do
126
+ resolve_tenant(identifier)
127
+ end
128
+ end
129
+
130
+ private
131
+
132
+ def cache_key(identifier)
133
+ "pg_multitenant:tenant:#{identifier}"
134
+ end
135
+
136
+ def cache_enabled?
137
+ configuration.cache_enabled
138
+ end
139
+
140
+ def cache_ttl
141
+ configuration.cache_ttl || 5.minutes
142
+ end
143
+ end
144
+ end
145
+ ```
146
+
147
+ ## 🔄 Usage Patterns
148
+
149
+ ### Rails Controller Integration
150
+
151
+ ```ruby
152
+ class ApplicationController < ActionController::Base
153
+ before_action :resolve_and_set_tenant
154
+
155
+ private
156
+
157
+ def resolve_and_set_tenant
158
+ @current_tenant = TenantResolver.resolve_from_request(request)
159
+
160
+ if @current_tenant
161
+ PgMultitenantSchemas::Context.switch_to_tenant(@current_tenant)
162
+ else
163
+ handle_tenant_not_found
164
+ end
165
+ end
166
+
167
+ def handle_tenant_not_found
168
+ # Handle tenant resolution failure
169
+ case request.format
170
+ when :json
171
+ render json: { error: 'Tenant not found' }, status: :not_found
172
+ else
173
+ redirect_to tenant_selection_path
174
+ end
175
+ end
176
+ end
177
+ ```
178
+
179
+ ### API Applications
180
+
181
+ ```ruby
182
+ class ApiController < ActionController::API
183
+ before_action :authenticate_and_resolve_tenant
184
+
185
+ private
186
+
187
+ def authenticate_and_resolve_tenant
188
+ # Extract tenant from JWT token
189
+ token = request.headers['Authorization']&.split(' ')&.last
190
+ payload = JWT.decode(token, Rails.application.secret_key_base).first
191
+
192
+ tenant_id = payload['tenant_id']
193
+ @current_tenant = TenantResolver.resolve_by_id(tenant_id)
194
+
195
+ if @current_tenant
196
+ PgMultitenantSchemas::Context.switch_to_tenant(@current_tenant)
197
+ else
198
+ render json: { error: 'Invalid tenant' }, status: :unauthorized
199
+ end
200
+ rescue JWT::DecodeError
201
+ render json: { error: 'Invalid token' }, status: :unauthorized
202
+ end
203
+ end
204
+ ```
205
+
206
+ ### Background Jobs
207
+
208
+ ```ruby
209
+ class TenantJob < ApplicationJob
210
+ def perform(tenant_identifier, *args)
211
+ tenant = TenantResolver.resolve_by_subdomain(tenant_identifier)
212
+
213
+ if tenant
214
+ PgMultitenantSchemas::Context.with_tenant(tenant) do
215
+ process_tenant_specific_task(*args)
216
+ end
217
+ else
218
+ Rails.logger.error "Tenant not found: #{tenant_identifier}"
219
+ end
220
+ end
221
+ end
222
+ ```
223
+
224
+ ### Multi-Strategy Resolution
225
+
226
+ ```ruby
227
+ class FlexibleTenantResolver < TenantResolver
228
+ def self.resolve_from_request(request)
229
+ # Try multiple strategies in order
230
+ strategies = [:header, :subdomain, :path, :session]
231
+
232
+ strategies.each do |strategy|
233
+ tenant = try_strategy(strategy, request)
234
+ return tenant if tenant
235
+ end
236
+
237
+ nil
238
+ end
239
+
240
+ private
241
+
242
+ def self.try_strategy(strategy, request)
243
+ case strategy
244
+ when :header
245
+ resolve_by_header(request, 'X-Tenant-ID')
246
+ when :subdomain
247
+ resolve_by_subdomain(request.subdomain)
248
+ when :path
249
+ resolve_by_path(request.path)
250
+ when :session
251
+ resolve_by_session(request.session)
252
+ end
253
+ rescue StandardError => e
254
+ Rails.logger.warn "Tenant resolution strategy #{strategy} failed: #{e.message}"
255
+ nil
256
+ end
257
+ end
258
+ ```
259
+
260
+ ## 🎛️ Configuration Options
261
+
262
+ ### Basic Configuration
263
+
264
+ ```ruby
265
+ TenantResolver.configure do |config|
266
+ config.strategy = :subdomain # Default resolution strategy
267
+ config.cache_enabled = true # Enable tenant caching
268
+ config.cache_ttl = 5.minutes # Cache time-to-live
269
+ config.fallback_strategy = :session # Fallback if primary fails
270
+ end
271
+ ```
272
+
273
+ ### Advanced Configuration
274
+
275
+ ```ruby
276
+ TenantResolver.configure do |config|
277
+ # Resolution strategies
278
+ config.primary_strategy = :subdomain
279
+ config.fallback_strategies = [:header, :session]
280
+
281
+ # Caching
282
+ config.cache_enabled = Rails.env.production?
283
+ config.cache_ttl = 10.minutes
284
+ config.cache_namespace = 'tenant_resolver'
285
+
286
+ # Error handling
287
+ config.raise_on_not_found = false
288
+ config.log_resolution_failures = true
289
+
290
+ # Custom resolvers
291
+ config.custom_resolvers = {
292
+ api: ApiTenantResolver,
293
+ admin: AdminTenantResolver
294
+ }
295
+ end
296
+ ```
297
+
298
+ ## 🚨 Important Considerations
299
+
300
+ ### Performance
301
+
302
+ - **Caching**: Enable caching for production environments
303
+ - **Database Queries**: Minimize tenant lookup queries
304
+ - **Index Optimization**: Ensure tenant lookup fields are indexed
305
+
306
+ ### Security
307
+
308
+ - **Input Validation**: Validate tenant identifiers to prevent injection
309
+ - **Access Control**: Verify user has access to resolved tenant
310
+ - **Audit Logging**: Log tenant resolution for security auditing
311
+
312
+ ### Error Handling
313
+
314
+ - **Graceful Degradation**: Handle tenant resolution failures gracefully
315
+ - **Fallback Strategies**: Implement fallback resolution methods
316
+ - **User Experience**: Provide clear error messages for tenant issues
317
+
318
+ ## 🔍 Debugging and Monitoring
319
+
320
+ ### Debug Tenant Resolution
321
+
322
+ ```ruby
323
+ # Add logging to tenant resolution
324
+ module TenantResolutionLogger
325
+ def resolve_from_request(request)
326
+ Rails.logger.debug "Resolving tenant from: #{request.host}"
327
+
328
+ tenant = super(request)
329
+
330
+ if tenant
331
+ Rails.logger.debug "Resolved tenant: #{tenant.subdomain}"
332
+ else
333
+ Rails.logger.warn "Failed to resolve tenant from: #{request.host}"
334
+ end
335
+
336
+ tenant
337
+ end
338
+ end
339
+
340
+ TenantResolver.prepend(TenantResolutionLogger)
341
+ ```
342
+
343
+ ### Monitor Resolution Performance
344
+
345
+ ```ruby
346
+ # Track resolution time
347
+ def resolve_with_timing(identifier)
348
+ start_time = Time.current
349
+
350
+ tenant = resolve_without_timing(identifier)
351
+
352
+ duration = Time.current - start_time
353
+ Rails.logger.info "Tenant resolution took #{duration * 1000}ms"
354
+
355
+ tenant
356
+ end
357
+
358
+ alias_method :resolve_without_timing, :resolve_tenant
359
+ alias_method :resolve_tenant, :resolve_with_timing
360
+ ```
361
+
362
+ ### Cache Statistics
363
+
364
+ ```ruby
365
+ # Monitor cache hit rates
366
+ class TenantCacheMonitor
367
+ def self.cache_stats
368
+ {
369
+ hits: Rails.cache.fetch('tenant_cache_hits', raw: true) || 0,
370
+ misses: Rails.cache.fetch('tenant_cache_misses', raw: true) || 0,
371
+ hit_rate: calculate_hit_rate
372
+ }
373
+ end
374
+
375
+ def self.increment_hits
376
+ Rails.cache.increment('tenant_cache_hits', 1, raw: true, expires_in: 1.day)
377
+ end
378
+
379
+ def self.increment_misses
380
+ Rails.cache.increment('tenant_cache_misses', 1, raw: true, expires_in: 1.day)
381
+ end
382
+ end
383
+ ```
384
+
385
+ ## 🔗 Related Components
386
+
387
+ - **[Context](context.md)**: Uses resolved tenant for context switching
388
+ - **[Configuration](configuration.md)**: Tenant model and resolution configuration
389
+ - **[Rails Integration](rails_integration.md)**: Controller and middleware integration
390
+ - **[Errors](errors.md)**: Tenant resolution error handling
391
+
392
+ ## 📝 Examples
393
+
394
+ See [examples/tenant_resolution.rb](../examples/) for complete tenant resolution patterns and [Rails integration examples](../examples/rails_integration/) for framework-specific implementations.