ros-apartment 3.1.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.
- checksums.yaml +4 -4
- data/.gitignore +1 -0
- data/.rubocop.yml +142 -8
- data/.ruby-version +1 -1
- data/Appraisals +125 -30
- data/CLAUDE.md +210 -0
- data/CODE_OF_CONDUCT.md +71 -0
- data/Gemfile +15 -0
- data/README.md +49 -31
- data/Rakefile +44 -25
- data/docs/adapters.md +177 -0
- data/docs/architecture.md +274 -0
- data/docs/elevators.md +226 -0
- data/docs/images/log_example.png +0 -0
- data/lib/apartment/CLAUDE.md +300 -0
- data/lib/apartment/active_record/connection_handling.rb +2 -2
- data/lib/apartment/active_record/postgres/schema_dumper.rb +20 -0
- data/lib/apartment/active_record/postgresql_adapter.rb +19 -4
- data/lib/apartment/adapters/CLAUDE.md +314 -0
- data/lib/apartment/adapters/abstract_adapter.rb +24 -15
- data/lib/apartment/adapters/jdbc_mysql_adapter.rb +1 -1
- data/lib/apartment/adapters/jdbc_postgresql_adapter.rb +3 -3
- data/lib/apartment/adapters/mysql2_adapter.rb +2 -2
- data/lib/apartment/adapters/postgresql_adapter.rb +55 -36
- data/lib/apartment/adapters/sqlite3_adapter.rb +7 -7
- data/lib/apartment/console.rb +1 -1
- data/lib/apartment/custom_console.rb +7 -7
- data/lib/apartment/deprecation.rb +2 -5
- data/lib/apartment/elevators/CLAUDE.md +292 -0
- data/lib/apartment/elevators/domain.rb +1 -1
- data/lib/apartment/elevators/generic.rb +1 -1
- data/lib/apartment/elevators/host_hash.rb +3 -3
- data/lib/apartment/elevators/subdomain.rb +9 -5
- data/lib/apartment/log_subscriber.rb +1 -1
- data/lib/apartment/migrator.rb +17 -5
- data/lib/apartment/model.rb +1 -1
- data/lib/apartment/railtie.rb +3 -3
- data/lib/apartment/tasks/enhancements.rb +1 -1
- data/lib/apartment/tasks/task_helper.rb +6 -4
- data/lib/apartment/tenant.rb +3 -3
- data/lib/apartment/version.rb +1 -1
- data/lib/apartment.rb +23 -11
- data/lib/generators/apartment/install/install_generator.rb +1 -1
- data/lib/generators/apartment/install/templates/apartment.rb +2 -2
- data/lib/tasks/apartment.rake +25 -25
- data/ros-apartment.gemspec +10 -35
- metadata +44 -245
- data/.rubocop_todo.yml +0 -439
- /data/{CHANGELOG.md → legacy_CHANGELOG.md} +0 -0
|
@@ -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`
|
|
@@ -9,14 +9,14 @@ module Apartment
|
|
|
9
9
|
#
|
|
10
10
|
class HostHash < Generic
|
|
11
11
|
def initialize(app, hash = {}, processor = nil)
|
|
12
|
-
super
|
|
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
|
|
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
|
-
#
|
|
26
|
-
#
|
|
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
|
-
#
|
|
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
|
data/lib/apartment/migrator.rb
CHANGED
|
@@ -4,30 +4,42 @@ require 'apartment/tenant'
|
|
|
4
4
|
|
|
5
5
|
module Apartment
|
|
6
6
|
module Migrator
|
|
7
|
-
|
|
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']
|
|
12
|
+
version = ENV['VERSION']&.to_i
|
|
13
13
|
|
|
14
14
|
migration_scope_block = ->(migration) { ENV['SCOPE'].blank? || (ENV['SCOPE'] == migration.scope) }
|
|
15
15
|
|
|
16
|
-
ActiveRecord::
|
|
16
|
+
if ActiveRecord.version >= Gem::Version.new('7.2.0')
|
|
17
|
+
ActiveRecord::Base.connection_pool.migration_context.migrate(version, &migration_scope_block)
|
|
18
|
+
else
|
|
19
|
+
ActiveRecord::Base.connection.migration_context.migrate(version, &migration_scope_block)
|
|
20
|
+
end
|
|
17
21
|
end
|
|
18
22
|
end
|
|
19
23
|
|
|
20
24
|
# Migrate up/down to a specific version
|
|
21
25
|
def run(direction, database, version)
|
|
22
26
|
Tenant.switch(database) do
|
|
23
|
-
ActiveRecord::
|
|
27
|
+
if ActiveRecord.version >= Gem::Version.new('7.2.0')
|
|
28
|
+
ActiveRecord::Base.connection_pool.migration_context.run(direction, version)
|
|
29
|
+
else
|
|
30
|
+
ActiveRecord::Base.connection.migration_context.run(direction, version)
|
|
31
|
+
end
|
|
24
32
|
end
|
|
25
33
|
end
|
|
26
34
|
|
|
27
35
|
# rollback latest migration `step` number of times
|
|
28
36
|
def rollback(database, step = 1)
|
|
29
37
|
Tenant.switch(database) do
|
|
30
|
-
ActiveRecord::
|
|
38
|
+
if ActiveRecord.version >= Gem::Version.new('7.2.0')
|
|
39
|
+
ActiveRecord::Base.connection_pool.migration_context.rollback(step)
|
|
40
|
+
else
|
|
41
|
+
ActiveRecord::Base.connection.migration_context.rollback(step)
|
|
42
|
+
end
|
|
31
43
|
end
|
|
32
44
|
end
|
|
33
45
|
end
|
data/lib/apartment/model.rb
CHANGED
|
@@ -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?
|
|
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
|
data/lib/apartment/railtie.rb
CHANGED
|
@@ -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(
|
|
51
|
+
next unless listener.instance_variable_get(:@delegate).is_a?(ActiveRecord::LogSubscriber)
|
|
52
52
|
|
|
53
|
-
ActiveSupport::Notifications.unsubscribe
|
|
53
|
+
ActiveSupport::Notifications.unsubscribe(listener)
|
|
54
54
|
end
|
|
55
55
|
|
|
56
|
-
Apartment::LogSubscriber.attach_to
|
|
56
|
+
Apartment::LogSubscriber.attach_to(:active_record)
|
|
57
57
|
end
|
|
58
58
|
end
|
|
59
59
|
|
|
@@ -2,9 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
module Apartment
|
|
4
4
|
module TaskHelper
|
|
5
|
-
def self.each_tenant
|
|
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
|
+
yield(tenant)
|
|
9
|
+
end
|
|
8
10
|
end
|
|
9
11
|
end
|
|
10
12
|
|
|
@@ -42,9 +44,9 @@ module Apartment
|
|
|
42
44
|
create_tenant(tenant_name) if strategy == :create_tenant
|
|
43
45
|
|
|
44
46
|
puts("Migrating #{tenant_name} tenant")
|
|
45
|
-
Apartment::Migrator.migrate
|
|
47
|
+
Apartment::Migrator.migrate(tenant_name)
|
|
46
48
|
rescue Apartment::TenantNotFound => e
|
|
47
|
-
raise
|
|
49
|
+
raise(e) if strategy == :raise_exception
|
|
48
50
|
|
|
49
51
|
puts e.message
|
|
50
52
|
end
|
data/lib/apartment/tenant.rb
CHANGED
|
@@ -32,13 +32,13 @@ module Apartment
|
|
|
32
32
|
end
|
|
33
33
|
|
|
34
34
|
begin
|
|
35
|
-
require
|
|
35
|
+
require("apartment/adapters/#{adapter_method}")
|
|
36
36
|
rescue LoadError
|
|
37
|
-
raise
|
|
37
|
+
raise("The adapter `#{adapter_method}` is not yet supported")
|
|
38
38
|
end
|
|
39
39
|
|
|
40
40
|
unless respond_to?(adapter_method)
|
|
41
|
-
raise
|
|
41
|
+
raise(AdapterNotFound, "database configuration specifies nonexistent #{config[:adapter]} adapter")
|
|
42
42
|
end
|
|
43
43
|
|
|
44
44
|
send(adapter_method, config)
|
data/lib/apartment/version.rb
CHANGED
data/lib/apartment.rb
CHANGED
|
@@ -5,19 +5,25 @@ require 'active_support/core_ext/object/blank'
|
|
|
5
5
|
require 'forwardable'
|
|
6
6
|
require 'active_record'
|
|
7
7
|
require 'apartment/tenant'
|
|
8
|
+
require 'apartment/deprecation'
|
|
8
9
|
|
|
9
10
|
require_relative 'apartment/log_subscriber'
|
|
10
11
|
require_relative 'apartment/active_record/connection_handling'
|
|
11
12
|
require_relative 'apartment/active_record/schema_migration'
|
|
12
13
|
require_relative 'apartment/active_record/internal_metadata'
|
|
13
14
|
|
|
15
|
+
if ActiveRecord.version.release >= Gem::Version.new('7.1')
|
|
16
|
+
require_relative 'apartment/active_record/postgres/schema_dumper'
|
|
17
|
+
end
|
|
18
|
+
|
|
14
19
|
# Apartment main definitions
|
|
15
20
|
module Apartment
|
|
16
21
|
class << self
|
|
17
22
|
extend Forwardable
|
|
18
23
|
|
|
19
24
|
ACCESSOR_METHODS = %i[use_schemas use_sql seed_after_create prepend_environment default_tenant
|
|
20
|
-
append_environment with_multi_server_setup tenant_presence_check
|
|
25
|
+
append_environment with_multi_server_setup tenant_presence_check
|
|
26
|
+
active_record_log pg_exclude_clone_tables].freeze
|
|
21
27
|
|
|
22
28
|
WRITER_METHODS = %i[tenant_names database_schema_file excluded_models
|
|
23
29
|
persistent_schemas connection_class
|
|
@@ -35,7 +41,7 @@ module Apartment
|
|
|
35
41
|
|
|
36
42
|
# configure apartment with available options
|
|
37
43
|
def configure
|
|
38
|
-
yield
|
|
44
|
+
yield(self) if block_given?
|
|
39
45
|
end
|
|
40
46
|
|
|
41
47
|
def tenant_names
|
|
@@ -47,11 +53,11 @@ module Apartment
|
|
|
47
53
|
end
|
|
48
54
|
|
|
49
55
|
def tld_length=(_)
|
|
50
|
-
Apartment::
|
|
56
|
+
Apartment::DEPRECATOR.warn('`config.tld_length` have no effect because it was removed in https://github.com/influitive/apartment/pull/309')
|
|
51
57
|
end
|
|
52
58
|
|
|
53
59
|
def db_config_for(tenant)
|
|
54
|
-
|
|
60
|
+
tenants_with_config[tenant] || connection_config
|
|
55
61
|
end
|
|
56
62
|
|
|
57
63
|
# Whether or not db:migrate should also migrate tenants
|
|
@@ -62,9 +68,10 @@ module Apartment
|
|
|
62
68
|
@db_migrate_tenants = true
|
|
63
69
|
end
|
|
64
70
|
|
|
65
|
-
# How to handle
|
|
66
|
-
#
|
|
67
|
-
#
|
|
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
|
|
68
75
|
def db_migrate_tenant_missing_strategy
|
|
69
76
|
valid = %i[rescue_exception raise_exception create_tenant]
|
|
70
77
|
value = @db_migrate_tenant_missing_strategy || :rescue_exception
|
|
@@ -74,7 +81,7 @@ module Apartment
|
|
|
74
81
|
key_name = 'config.db_migrate_tenant_missing_strategy'
|
|
75
82
|
opt_names = valid.join(', ')
|
|
76
83
|
|
|
77
|
-
raise
|
|
84
|
+
raise(ApartmentError, "Option #{value} not valid for `#{key_name}`. Use one of #{opt_names}")
|
|
78
85
|
end
|
|
79
86
|
|
|
80
87
|
# Default to empty array
|
|
@@ -120,14 +127,19 @@ module Apartment
|
|
|
120
127
|
def extract_tenant_config
|
|
121
128
|
return {} unless @tenant_names
|
|
122
129
|
|
|
130
|
+
# Execute callable (proc/lambda) to get dynamic tenant list from database
|
|
123
131
|
values = @tenant_names.respond_to?(:call) ? @tenant_names.call : @tenant_names
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
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
|
|
127
137
|
end
|
|
128
138
|
end
|
|
129
139
|
values.with_indifferent_access
|
|
130
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
|
|
131
143
|
{}
|
|
132
144
|
end
|
|
133
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
|
|
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
|
|
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
|
|
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
|