ros-apartment 3.2.0 → 3.3.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 (41) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +1 -0
  3. data/.rubocop.yml +93 -2
  4. data/.ruby-version +1 -1
  5. data/Appraisals +15 -0
  6. data/CLAUDE.md +210 -0
  7. data/README.md +1 -1
  8. data/Rakefile +6 -5
  9. data/docs/adapters.md +177 -0
  10. data/docs/architecture.md +274 -0
  11. data/docs/elevators.md +226 -0
  12. data/docs/images/log_example.png +0 -0
  13. data/lib/apartment/CLAUDE.md +300 -0
  14. data/lib/apartment/adapters/CLAUDE.md +314 -0
  15. data/lib/apartment/adapters/abstract_adapter.rb +24 -15
  16. data/lib/apartment/adapters/jdbc_mysql_adapter.rb +1 -1
  17. data/lib/apartment/adapters/jdbc_postgresql_adapter.rb +3 -3
  18. data/lib/apartment/adapters/mysql2_adapter.rb +2 -2
  19. data/lib/apartment/adapters/postgresql_adapter.rb +42 -19
  20. data/lib/apartment/adapters/sqlite3_adapter.rb +7 -7
  21. data/lib/apartment/console.rb +1 -1
  22. data/lib/apartment/custom_console.rb +7 -7
  23. data/lib/apartment/elevators/CLAUDE.md +292 -0
  24. data/lib/apartment/elevators/domain.rb +1 -1
  25. data/lib/apartment/elevators/generic.rb +1 -1
  26. data/lib/apartment/elevators/host_hash.rb +3 -3
  27. data/lib/apartment/elevators/subdomain.rb +9 -5
  28. data/lib/apartment/log_subscriber.rb +1 -1
  29. data/lib/apartment/migrator.rb +2 -2
  30. data/lib/apartment/model.rb +1 -1
  31. data/lib/apartment/railtie.rb +3 -3
  32. data/lib/apartment/tasks/enhancements.rb +1 -1
  33. data/lib/apartment/tasks/task_helper.rb +4 -4
  34. data/lib/apartment/tenant.rb +3 -3
  35. data/lib/apartment/version.rb +1 -1
  36. data/lib/apartment.rb +15 -9
  37. data/lib/generators/apartment/install/install_generator.rb +1 -1
  38. data/lib/generators/apartment/install/templates/apartment.rb +2 -2
  39. data/lib/tasks/apartment.rake +25 -25
  40. data/ros-apartment.gemspec +3 -3
  41. metadata +22 -11
@@ -0,0 +1,292 @@
1
+ # lib/apartment/elevators/ - Rack Middleware for Tenant Switching
2
+
3
+ This directory contains Rack middleware components ("elevators") that automatically detect and switch to the appropriate tenant based on incoming HTTP requests.
4
+
5
+ ## Purpose
6
+
7
+ Elevators intercept incoming requests and establish tenant context **before** the application processes the request. This eliminates the need for manual tenant switching in controllers.
8
+
9
+ ## Metaphor
10
+
11
+ Like a physical elevator taking you to different floors, these middleware components "elevate" your request to the correct tenant context.
12
+
13
+ ## File Structure
14
+
15
+ ```
16
+ elevators/
17
+ ├── generic.rb # Base elevator with customizable logic
18
+ ├── subdomain.rb # Switch based on subdomain (e.g., acme.example.com)
19
+ ├── first_subdomain.rb # Switch based on first subdomain in chain
20
+ ├── domain.rb # Switch based on domain (excluding www and TLD)
21
+ ├── host.rb # Switch based on full hostname
22
+ └── host_hash.rb # Switch based on hostname → tenant hash mapping
23
+ ```
24
+
25
+ ## How Elevators Work
26
+
27
+ ### Rack Middleware Pattern
28
+
29
+ All elevators are Rack middleware that intercept requests, extract tenant identifier, switch context, invoke next middleware, and ensure cleanup. See `generic.rb` for base implementation.
30
+
31
+ ### Request Lifecycle with Elevator
32
+
33
+ HTTP Request → Elevator extracts tenant → Switch to tenant → Application processes → Automatic cleanup (ensure block) → HTTP Response
34
+
35
+ **See**: `Generic#call` method for middleware call pattern.
36
+
37
+ ## Generic Elevator - Base Class
38
+
39
+ **Location**: `generic.rb`
40
+
41
+ ### Purpose
42
+
43
+ Provides base implementation and allows custom tenant resolution via Proc or subclass.
44
+
45
+ ### Implementation
46
+
47
+ Accepts optional Proc in initializer or expects `parse_tenant_name(request)` override in subclass. See `Generic` class implementation in `generic.rb`.
48
+
49
+ ### Usage Patterns
50
+
51
+ **With Proc**: Pass Proc to Generic that extracts tenant from Rack::Request.
52
+
53
+ **Via Subclass**: Inherit from Generic and override `parse_tenant_name`.
54
+
55
+ **See**: `generic.rb` and README.md for usage examples.
56
+
57
+ ## Subdomain Elevator
58
+
59
+ **Location**: `subdomain.rb`
60
+
61
+ ### Strategy
62
+
63
+ Extract first subdomain from hostname.
64
+
65
+ ### Implementation
66
+
67
+ Uses `request.subdomain` and checks against `excluded_subdomains` class attribute. Returns nil for excluded subdomains. See `Subdomain#parse_tenant_name` in `subdomain.rb`.
68
+
69
+ ### Configuration
70
+
71
+ Add to middleware stack in `application.rb` and configure `excluded_subdomains` class attribute. See README.md for examples.
72
+
73
+ ### Behavior
74
+
75
+ | Request URL | Subdomain | Excluded? | Tenant |
76
+ |------------------------------|-----------|-----------|-------------|
77
+ | http://acme.example.com | acme | No | acme |
78
+ | http://widgets.example.com | widgets | No | widgets |
79
+ | http://www.example.com | www | Yes | (default) |
80
+ | http://api.example.com | api | Yes | (default) |
81
+ | http://example.com | (empty) | N/A | (default) |
82
+
83
+ ### Why PublicSuffix Dependency?
84
+
85
+ **Rationale**: International domains require proper TLD parsing. Without PublicSuffix, `example.co.uk` would incorrectly parse `.uk` as the TLD rather than `.co.uk`, causing subdomain extraction to fail.
86
+
87
+ **Trade-off**: Adds gem dependency, but necessary for international domain support.
88
+
89
+ ## FirstSubdomain Elevator
90
+
91
+ **Location**: `first_subdomain.rb`
92
+
93
+ ### Strategy
94
+
95
+ Extract **first** subdomain from chain (for nested subdomains).
96
+
97
+ ### Implementation
98
+
99
+ Splits subdomain on `.` and takes first part. See `FirstSubdomain#parse_tenant_name` in `first_subdomain.rb`.
100
+
101
+ ### Configuration
102
+
103
+ Add to middleware stack and configure excluded subdomains. See README.md for configuration.
104
+
105
+ ### Use Case
106
+
107
+ Multi-level subdomain structures where tenant is always leftmost:
108
+ - `{tenant}.api.example.com`
109
+ - `{tenant}.app.example.com`
110
+ - `{tenant}.staging.example.com`
111
+
112
+ ### Note
113
+
114
+ In current v3 implementation, `Subdomain` and `FirstSubdomain` may behave identically depending on Rails version due to how `request.subdomain` works. For true nested support, test thoroughly or use custom elevator.
115
+
116
+ ## Domain Elevator
117
+
118
+ **Location**: `domain.rb`
119
+
120
+ ### Strategy
121
+
122
+ Use domain name (excluding 'www' and top-level domain) as tenant.
123
+
124
+ ### Implementation
125
+
126
+ Extracts domain name excluding TLD and 'www' prefix. See `Domain#parse_tenant_name` in `domain.rb`.
127
+
128
+ ### Configuration
129
+
130
+ Add to middleware stack. See README.md.
131
+
132
+ ### Use Case
133
+
134
+ When full domain (not subdomain) identifies tenant:
135
+ - `acme-corp.com` → tenant: acme-corp
136
+ - `widgets-inc.com` → tenant: widgets-inc
137
+
138
+ ## Host Elevator
139
+
140
+ **Location**: `host.rb`
141
+
142
+ ### Strategy
143
+
144
+ Use **full hostname** as tenant, optionally ignoring specified first subdomains.
145
+
146
+ ### Implementation
147
+
148
+ Uses full hostname as tenant, optionally ignoring specified first subdomains. See `Host#parse_tenant_name` in `host.rb`.
149
+
150
+ ### Configuration
151
+
152
+ Add to middleware stack and configure `ignored_first_subdomains`. See README.md.
153
+
154
+ ### Use Case
155
+
156
+ When each full hostname represents a different tenant:
157
+ - Tenants use custom domains: `acme-corp.com`, `widgets-inc.net`
158
+ - Internal apps: `billing.internal.company.com`, `crm.internal.company.com`
159
+
160
+ ## HostHash Elevator
161
+
162
+ **Location**: `host_hash.rb`
163
+
164
+ ### Strategy
165
+
166
+ Direct **mapping** from hostname to tenant name via hash.
167
+
168
+ ### Implementation
169
+
170
+ Accepts hash mapping hostnames to tenant names. See `HostHash` implementation in `host_hash.rb`.
171
+
172
+ ### Configuration
173
+
174
+ Pass hash to HostHash initializer when adding to middleware stack. See README.md for examples.
175
+
176
+ ### Use Cases
177
+
178
+ - **Custom domains**: Each tenant has their own domain
179
+ - **Explicit mapping**: No parsing logic, direct control
180
+ - **Different TLDs**: .com, .io, .net, etc.
181
+
182
+ ### Advantages
183
+
184
+ - ✅ Explicit control
185
+ - ✅ No parsing ambiguity
186
+ - ✅ Works with any hostname pattern
187
+
188
+ ### Disadvantages
189
+
190
+ - ❌ Requires manual configuration per tenant
191
+ - ❌ Not dynamic (requires app restart for changes)
192
+ - ❌ Doesn't scale to hundreds of tenants
193
+
194
+ ## Middleware Positioning
195
+
196
+ ### Why Position Matters
197
+
198
+ **Critical constraint**: Elevators must run before session and authentication middleware.
199
+
200
+ **Why this matters**: Session middleware loads user data based on session ID. If session loads before tenant is established, you get the wrong tenant's session data. This creates security vulnerabilities where User A sees User B's data.
201
+
202
+ **Example failure**: Without proper positioning, `www.acme.com` might load session data from `widgets.com` tenant if session middleware runs first.
203
+
204
+ **How to verify**: Run `Rails.application.middleware` and confirm elevator appears before `ActionDispatch::Session::CookieStore` and authentication middleware like `Warden::Manager`.
205
+
206
+ ## Creating Custom Elevators
207
+
208
+ ### Method 1: Using Proc with Generic
209
+
210
+ Pass Proc to Generic elevator for inline tenant detection logic. See `generic.rb` and README.md.
211
+
212
+ ### Method 2: Subclassing Generic
213
+
214
+ Create custom class inheriting from Generic, override `parse_tenant_name(request)`. Supports multi-strategy fallback logic. See `generic.rb` for base class.
215
+
216
+ ## Error Handling
217
+
218
+ ### Handling Missing Tenants
219
+
220
+ Custom elevators can rescue `Apartment::TenantNotFound` and return appropriate HTTP responses (404, redirect, etc.). See `generic.rb` for base call pattern.
221
+
222
+ ### Custom Error Pages
223
+
224
+ Override `call(env)` method to wrap `super` in rescue block and handle errors. See existing elevator implementations for patterns.
225
+
226
+ ## Testing Elevators
227
+
228
+ ### Unit Testing
229
+
230
+ Use `Rack::MockRequest` to create test requests and mock `Apartment::Tenant.switch`. See `spec/unit/elevators/` for test patterns.
231
+
232
+ ### Integration Testing
233
+
234
+ Create test tenants in before hooks, make requests to different subdomains/hosts, verify correct tenant context. See `spec/integration/` for examples.
235
+
236
+ ## Performance Considerations
237
+
238
+ ### Why Caching Matters for Custom Elevators
239
+
240
+ **Problem**: If your custom elevator queries the database to resolve tenant (e.g., looking up tenant by API key), you add database latency to **every request**.
241
+
242
+ **Impact**: 10-50ms per request × thousands of requests = significant overhead.
243
+
244
+ **Solution**: Cache tenant name lookups. Trade-off is stale cache if tenants are renamed, but this is rare.
245
+
246
+ ### Why Preloaded Hash Maps Beat Database Queries
247
+
248
+ **Database query approach**: SELECT tenant_name FROM tenants WHERE subdomain = ? — runs on every request.
249
+
250
+ **Hash map approach**: Loaded once at boot, O(1) lookup with no I/O.
251
+
252
+ **Trade-off**: Hash maps don't update without restart, but for most applications tenant-to-subdomain mapping is stable.
253
+
254
+ ### Why Monitor Elevator Performance
255
+
256
+ **Hidden cost**: Elevator runs on every request. 10ms overhead is 10% latency penalty on a 100ms request.
257
+
258
+ **Target**: Elevator should complete in <5ms. If >100ms, investigate and add logging.
259
+
260
+ ## Common Issues
261
+
262
+ ### Issue: Elevator Not Triggering
263
+
264
+ **Symptoms**: Tenant always default
265
+
266
+ **Causes**: Elevator not in middleware stack, `parse_tenant_name` returning nil, or incorrect middleware positioning
267
+
268
+ **Debug**: Add logging to `parse_tenant_name` to inspect extracted tenant values.
269
+
270
+ ### Issue: TenantNotFound Errors
271
+
272
+ **Symptoms**: 500 errors on some requests
273
+
274
+ **Causes**: Tenant doesn't exist or subdomain not in tenant list
275
+
276
+ **Solution**: Add error handling in custom elevator or validate tenant existence before switching.
277
+
278
+ ## Best Practices
279
+
280
+ 1. **Position elevators early** in middleware stack
281
+ 2. **Handle errors gracefully** (don't expose internals)
282
+ 3. **Cache lookups** if using database queries
283
+ 4. **Test thoroughly** with multiple tenants
284
+ 5. **Monitor performance** (log slow switches)
285
+ 6. **Document custom logic** for maintainability
286
+
287
+ ## References
288
+
289
+ - Rack middleware: https://github.com/rack/rack/wiki/Middleware
290
+ - Rack::Request: https://www.rubydoc.info/github/rack/rack/Rack/Request
291
+ - Rails middleware: https://guides.rubyonrails.org/rails_on_rack.html
292
+ - Generic elevator: `generic.rb`
@@ -16,7 +16,7 @@ module Apartment
16
16
  def parse_tenant_name(request)
17
17
  return nil if request.host.blank?
18
18
 
19
- request.host.match(/(www\.)?(?<sld>[^.]*)/)['sld']
19
+ request.host.match(/(?:www\.)?(?<sld>[^.]*)/)['sld']
20
20
  end
21
21
  end
22
22
  end
@@ -26,7 +26,7 @@ module Apartment
26
26
  end
27
27
 
28
28
  def parse_tenant_name(_request)
29
- raise 'Override'
29
+ raise('Override')
30
30
  end
31
31
  end
32
32
  end
@@ -9,14 +9,14 @@ module Apartment
9
9
  #
10
10
  class HostHash < Generic
11
11
  def initialize(app, hash = {}, processor = nil)
12
- super app, processor
12
+ super(app, processor)
13
13
  @hash = hash
14
14
  end
15
15
 
16
16
  def parse_tenant_name(request)
17
17
  unless @hash.key?(request.host)
18
- raise TenantNotFound,
19
- "Cannot find tenant for host #{request.host}"
18
+ raise(TenantNotFound,
19
+ "Cannot find tenant for host #{request.host}")
20
20
  end
21
21
 
22
22
  @hash[request.host]
@@ -22,8 +22,9 @@ module Apartment
22
22
  def parse_tenant_name(request)
23
23
  request_subdomain = subdomain(request.host)
24
24
 
25
- # If the domain acquired is set to be excluded, set the tenant to whatever is currently
26
- # next in line in the schema search path.
25
+ # Excluded subdomains (www, api, admin) return nil uses default tenant.
26
+ # Returning nil instead of default_tenant name allows Apartment to decide
27
+ # the fallback behavior.
27
28
  tenant = if self.class.excluded_subdomains.include?(request_subdomain)
28
29
  nil
29
30
  else
@@ -35,11 +36,11 @@ module Apartment
35
36
 
36
37
  protected
37
38
 
38
- # *Almost* a direct ripoff of ActionDispatch::Request subdomain methods
39
+ # Subdomain extraction using PublicSuffix to handle international TLDs correctly.
40
+ # Examples: api.example.com → "api", www.example.co.uk → "www"
39
41
 
40
- # Only care about the first subdomain for the database name
41
42
  def subdomain(host)
42
- subdomains(host).first
43
+ subdomains(host).first # Only first subdomain matters for tenant resolution
43
44
  end
44
45
 
45
46
  def subdomains(host)
@@ -50,6 +51,7 @@ module Apartment
50
51
  !ip_host?(host) && domain_valid?(host)
51
52
  end
52
53
 
54
+ # Reject IP addresses (127.0.0.1, 192.168.1.1) - no subdomain concept
53
55
  def ip_host?(host)
54
56
  !/^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$/.match(host).nil?
55
57
  end
@@ -58,6 +60,8 @@ module Apartment
58
60
  PublicSuffix.valid?(host, ignore_private: true)
59
61
  end
60
62
 
63
+ # PublicSuffix.parse handles TLDs correctly: example.co.uk has TLD "co.uk"
64
+ # .trd (third-level domain) returns subdomain parts, excluding TLD
61
65
  def parse_host(host)
62
66
  (PublicSuffix.parse(host, ignore_private: true).trd || '').split('.')
63
67
  end
@@ -14,7 +14,7 @@ module Apartment
14
14
 
15
15
  private
16
16
 
17
- def debug(progname = nil, &blk)
17
+ def debug(progname = nil, &)
18
18
  progname = " #{apartment_log}#{progname}" unless progname.nil?
19
19
 
20
20
  super
@@ -4,12 +4,12 @@ require 'apartment/tenant'
4
4
 
5
5
  module Apartment
6
6
  module Migrator
7
- extend self
7
+ module_function
8
8
 
9
9
  # Migrate to latest
10
10
  def migrate(database)
11
11
  Tenant.switch(database) do
12
- version = ENV['VERSION'] ? ENV['VERSION'].to_i : nil
12
+ version = ENV['VERSION']&.to_i
13
13
 
14
14
  migration_scope_block = ->(migration) { ENV['SCOPE'].blank? || (ENV['SCOPE'] == migration.scope) }
15
15
 
@@ -14,7 +14,7 @@ module Apartment
14
14
  # Modifying the cache key to have a reference to the current tenant,
15
15
  # so the cached statement is referring only to the tenant in which we've
16
16
  # executed this
17
- cache_key = if key.is_a? String
17
+ cache_key = if key.is_a?(String)
18
18
  "#{Apartment::Tenant.current}_#{key}"
19
19
  else
20
20
  # NOTE: In Rails 6.0.4 we start receiving an ActiveRecord::Reflection::BelongsToReflection
@@ -48,12 +48,12 @@ module Apartment
48
48
  # NOTE: Load the custom log subscriber if enabled
49
49
  if Apartment.active_record_log
50
50
  ActiveSupport::Notifications.notifier.listeners_for('sql.active_record').each do |listener|
51
- next unless listener.instance_variable_get('@delegate').is_a?(ActiveRecord::LogSubscriber)
51
+ next unless listener.instance_variable_get(:@delegate).is_a?(ActiveRecord::LogSubscriber)
52
52
 
53
- ActiveSupport::Notifications.unsubscribe listener
53
+ ActiveSupport::Notifications.unsubscribe(listener)
54
54
  end
55
55
 
56
- Apartment::LogSubscriber.attach_to :active_record
56
+ Apartment::LogSubscriber.attach_to(:active_record)
57
57
  end
58
58
  end
59
59
 
@@ -46,7 +46,7 @@ module Apartment
46
46
  end
47
47
 
48
48
  def inserted_task_name(task)
49
- task.name.sub(/db:/, 'apartment:')
49
+ task.name.sub('db:', 'apartment:')
50
50
  end
51
51
  end
52
52
  end
@@ -2,10 +2,10 @@
2
2
 
3
3
  module Apartment
4
4
  module TaskHelper
5
- def self.each_tenant(&block)
5
+ def self.each_tenant
6
6
  Parallel.each(tenants_without_default, in_threads: Apartment.parallel_migration_threads) do |tenant|
7
7
  Rails.application.executor.wrap do
8
- block.call(tenant)
8
+ yield(tenant)
9
9
  end
10
10
  end
11
11
  end
@@ -44,9 +44,9 @@ module Apartment
44
44
  create_tenant(tenant_name) if strategy == :create_tenant
45
45
 
46
46
  puts("Migrating #{tenant_name} tenant")
47
- Apartment::Migrator.migrate tenant_name
47
+ Apartment::Migrator.migrate(tenant_name)
48
48
  rescue Apartment::TenantNotFound => e
49
- raise e if strategy == :raise_exception
49
+ raise(e) if strategy == :raise_exception
50
50
 
51
51
  puts e.message
52
52
  end
@@ -32,13 +32,13 @@ module Apartment
32
32
  end
33
33
 
34
34
  begin
35
- require "apartment/adapters/#{adapter_method}"
35
+ require("apartment/adapters/#{adapter_method}")
36
36
  rescue LoadError
37
- raise "The adapter `#{adapter_method}` is not yet supported"
37
+ raise("The adapter `#{adapter_method}` is not yet supported")
38
38
  end
39
39
 
40
40
  unless respond_to?(adapter_method)
41
- raise AdapterNotFound, "database configuration specifies nonexistent #{config[:adapter]} adapter"
41
+ raise(AdapterNotFound, "database configuration specifies nonexistent #{config[:adapter]} adapter")
42
42
  end
43
43
 
44
44
  send(adapter_method, config)
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Apartment
4
- VERSION = '3.2.0'
4
+ VERSION = '3.3.0'
5
5
  end
data/lib/apartment.rb CHANGED
@@ -41,7 +41,7 @@ module Apartment
41
41
 
42
42
  # configure apartment with available options
43
43
  def configure
44
- yield self if block_given?
44
+ yield(self) if block_given?
45
45
  end
46
46
 
47
47
  def tenant_names
@@ -57,7 +57,7 @@ module Apartment
57
57
  end
58
58
 
59
59
  def db_config_for(tenant)
60
- (tenants_with_config[tenant] || connection_config)
60
+ tenants_with_config[tenant] || connection_config
61
61
  end
62
62
 
63
63
  # Whether or not db:migrate should also migrate tenants
@@ -68,9 +68,10 @@ module Apartment
68
68
  @db_migrate_tenants = true
69
69
  end
70
70
 
71
- # How to handle tenant missing on db:migrate
72
- # defaults to :rescue_exception
73
- # available options: rescue_exception, raise_exception, create_tenant
71
+ # How to handle missing tenants during db:migrate
72
+ # :rescue_exception (default) - Log error, continue with other tenants
73
+ # :raise_exception - Stop migration immediately
74
+ # :create_tenant - Automatically create missing tenant and migrate
74
75
  def db_migrate_tenant_missing_strategy
75
76
  valid = %i[rescue_exception raise_exception create_tenant]
76
77
  value = @db_migrate_tenant_missing_strategy || :rescue_exception
@@ -80,7 +81,7 @@ module Apartment
80
81
  key_name = 'config.db_migrate_tenant_missing_strategy'
81
82
  opt_names = valid.join(', ')
82
83
 
83
- raise ApartmentError, "Option #{value} not valid for `#{key_name}`. Use one of #{opt_names}"
84
+ raise(ApartmentError, "Option #{value} not valid for `#{key_name}`. Use one of #{opt_names}")
84
85
  end
85
86
 
86
87
  # Default to empty array
@@ -126,14 +127,19 @@ module Apartment
126
127
  def extract_tenant_config
127
128
  return {} unless @tenant_names
128
129
 
130
+ # Execute callable (proc/lambda) to get dynamic tenant list from database
129
131
  values = @tenant_names.respond_to?(:call) ? @tenant_names.call : @tenant_names
130
- unless values.is_a? Hash
131
- values = values.each_with_object({}) do |tenant, hash|
132
- hash[tenant] = connection_config
132
+
133
+ # Normalize arrays to hash format (tenant_name => connection_config)
134
+ unless values.is_a?(Hash)
135
+ values = values.index_with do |_tenant|
136
+ connection_config
133
137
  end
134
138
  end
135
139
  values.with_indifferent_access
136
140
  rescue ActiveRecord::StatementInvalid
141
+ # Database query failed (table doesn't exist yet, connection issue)
142
+ # Return empty hash to allow app to boot without tenants
137
143
  {}
138
144
  end
139
145
  end
@@ -5,7 +5,7 @@ module Apartment
5
5
  source_root File.expand_path('templates', __dir__)
6
6
 
7
7
  def copy_files
8
- template 'apartment.rb', File.join('config', 'initializers', 'apartment.rb')
8
+ template('apartment.rb', File.join('config', 'initializers', 'apartment.rb'))
9
9
  end
10
10
  end
11
11
  end
@@ -50,7 +50,7 @@ Apartment.configure do |config|
50
50
  # end
51
51
  # end
52
52
  #
53
- config.tenant_names = -> { ToDo_Tenant_Or_User_Model.pluck :database }
53
+ config.tenant_names = -> { ToDo_Tenant_Or_User_Model.pluck(:database) }
54
54
 
55
55
  # PostgreSQL:
56
56
  # Specifies whether to use PostgreSQL schemas or create a new database per Tenant.
@@ -111,6 +111,6 @@ end
111
111
  # }
112
112
 
113
113
  # Rails.application.config.middleware.use Apartment::Elevators::Domain
114
- Rails.application.config.middleware.use Apartment::Elevators::Subdomain
114
+ Rails.application.config.middleware.use(Apartment::Elevators::Subdomain)
115
115
  # Rails.application.config.middleware.use Apartment::Elevators::FirstSubdomain
116
116
  # Rails.application.config.middleware.use Apartment::Elevators::Host