organizations 0.8.1 → 0.9.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/CHANGELOG.md +10 -0
- data/README.md +35 -0
- data/lib/organizations/controller_helpers.rb +14 -6
- data/lib/organizations/current.rb +32 -0
- data/lib/organizations/version.rb +1 -1
- data/lib/organizations.rb +16 -0
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: d3074b8343b80c07b277e1038a0181981274d1baaa7a52af85dd425edf09c5d3
|
|
4
|
+
data.tar.gz: 8529247495ba8c215216cf4acea19e7764b3e20ac16d997a1ca3b1ecc49f86d7
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: cb91ccb8891c4a17c65a85698c13c15931505c3d32688bc075402fdc8582dbd0ecaf2a6a0e581696f471991cd23344998563f340206855c9532352da22086fc7
|
|
7
|
+
data.tar.gz: 1e15480d1f104e0fddea00dd2509223a7b43707e9eccd8930461c24928c3c81338387ba6e95e6b093058ee1bed2741c15b8c2ba919d8ba2449cdd98388f54a62
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
## [Unreleased]
|
|
2
2
|
|
|
3
|
+
## [0.9.0] - 2026-10-25
|
|
4
|
+
|
|
5
|
+
**The current organization, outside controllers.** One addition, opt-in to read. No migration is needed.
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- **`Organizations::Current`**, an `ActiveSupport::CurrentAttributes` with one attribute, `organization`. The controller helpers set it whenever `current_organization` resolves or changes (a fallback, `switch_to_organization!`, `current_organization =`, a cleared session), so models and mailers in the same request can read it. Rails resets it before every request and every job. Session-based resolution is unchanged.
|
|
10
|
+
- **`Organizations.with_organization(organization) { ... }`** for jobs, scripts and consoles. It sets the organization for the block, returns the block's value, and puts back the previous one afterwards, also when the block raises, so blocks nest.
|
|
11
|
+
- The README documents the one seam for a host's tenant context (for example a PostgreSQL setting that row-level security policies read): prepend a module that overrides `Organizations::Current#organization=`, which every change goes through, and add a `resets` callback. Asked for by ReplyBrain, whose reused gems must be scoped per request and per job.
|
|
12
|
+
|
|
3
13
|
## [0.8.1] - 2026-10-25
|
|
4
14
|
|
|
5
15
|
**The joins take the lock order of the destroys.** A patch release with one fix. No migration is needed.
|
data/README.md
CHANGED
|
@@ -2033,6 +2033,41 @@ Organizations.configure do |config|
|
|
|
2033
2033
|
end
|
|
2034
2034
|
```
|
|
2035
2035
|
|
|
2036
|
+
### The current organization outside controllers
|
|
2037
|
+
|
|
2038
|
+
Once `current_organization` resolves in a request, the same organization is in `Organizations::Current.organization`, readable from models, mailers and anything else that runs in that request. It follows switches, and Rails resets it when the request ends. Resolve it early, with `before_action :require_organization!` or a `current_organization` call, so the rest of the request sees it.
|
|
2039
|
+
|
|
2040
|
+
Jobs, scripts and consoles have no controller, so they say which organization they act for:
|
|
2041
|
+
|
|
2042
|
+
```ruby
|
|
2043
|
+
class SendInvoiceJob < ApplicationJob
|
|
2044
|
+
def perform(invoice)
|
|
2045
|
+
Organizations.with_organization(invoice.organization) do
|
|
2046
|
+
InvoiceMailer.paid(invoice).deliver_now
|
|
2047
|
+
end
|
|
2048
|
+
end
|
|
2049
|
+
end
|
|
2050
|
+
```
|
|
2051
|
+
|
|
2052
|
+
The block puts back whatever was current before, even when it raises, so blocks nest. Rails resets `Organizations::Current` before every job too.
|
|
2053
|
+
|
|
2054
|
+
Every change goes through one writer, `Organizations::Current.organization=`, and every reset through its `resets` callbacks. Those two points are where to hook your own tenant context, such as the PostgreSQL setting your row-level security policies read (a `SET LOCAL` in the transaction they run in):
|
|
2055
|
+
|
|
2056
|
+
```ruby
|
|
2057
|
+
# config/initializers/organizations.rb
|
|
2058
|
+
module TenantContext
|
|
2059
|
+
def organization=(organization)
|
|
2060
|
+
super
|
|
2061
|
+
Tenant.enter(organization) # your code: remember it, set the database setting...
|
|
2062
|
+
end
|
|
2063
|
+
end
|
|
2064
|
+
|
|
2065
|
+
Organizations::Current.prepend(TenantContext)
|
|
2066
|
+
Organizations::Current.resets { Tenant.leave }
|
|
2067
|
+
```
|
|
2068
|
+
|
|
2069
|
+
Session-based resolution is unchanged: the session still decides which organization is current, and `Organizations::Current` only publishes it.
|
|
2070
|
+
|
|
2036
2071
|
### Integrates with acts_as_tenant
|
|
2037
2072
|
|
|
2038
2073
|
For automatic query scoping, include the integration concern:
|
|
@@ -38,7 +38,7 @@ module Organizations
|
|
|
38
38
|
# Returns the current organization from session
|
|
39
39
|
# Validates membership - if user was removed, auto-switches to next available org
|
|
40
40
|
# Falls back to most recently joined org if no session set
|
|
41
|
-
# Memoized within the request
|
|
41
|
+
# Memoized within the request, and published as Organizations::Current.organization
|
|
42
42
|
# @return [Organizations::Organization, nil]
|
|
43
43
|
def current_organization
|
|
44
44
|
return @_current_organization if defined?(@_current_organization)
|
|
@@ -58,7 +58,7 @@ module Organizations
|
|
|
58
58
|
if org && user.is_member_of?(org)
|
|
59
59
|
# Valid membership - use this org
|
|
60
60
|
user._current_organization_id = org.id
|
|
61
|
-
|
|
61
|
+
self.resolved_current_organization = org
|
|
62
62
|
else
|
|
63
63
|
# User was removed from this org OR no session set
|
|
64
64
|
# Auto-switch to next available org (most recently joined)
|
|
@@ -68,9 +68,9 @@ module Organizations
|
|
|
68
68
|
if fallback_org
|
|
69
69
|
session[session_key] = fallback_org.id
|
|
70
70
|
user._current_organization_id = fallback_org.id
|
|
71
|
-
|
|
71
|
+
self.resolved_current_organization = fallback_org
|
|
72
72
|
else
|
|
73
|
-
|
|
73
|
+
self.resolved_current_organization = nil
|
|
74
74
|
end
|
|
75
75
|
end
|
|
76
76
|
end
|
|
@@ -459,7 +459,7 @@ module Organizations
|
|
|
459
459
|
|
|
460
460
|
if org
|
|
461
461
|
session[session_key] = org.id
|
|
462
|
-
|
|
462
|
+
self.resolved_current_organization = org
|
|
463
463
|
forget_current_membership!
|
|
464
464
|
|
|
465
465
|
# Update user's context
|
|
@@ -578,13 +578,21 @@ module Organizations
|
|
|
578
578
|
def clear_organization_session!
|
|
579
579
|
session_key = Organizations.configuration.session_key
|
|
580
580
|
session.delete(session_key)
|
|
581
|
-
|
|
581
|
+
self.resolved_current_organization = nil
|
|
582
582
|
forget_current_membership!
|
|
583
583
|
|
|
584
584
|
user = organizations_current_user
|
|
585
585
|
user&.clear_organization_cache!
|
|
586
586
|
end
|
|
587
587
|
|
|
588
|
+
# Memoizes the organization for this request and publishes it to
|
|
589
|
+
# Organizations::Current, so models and the host's tenant hook see the
|
|
590
|
+
# one the helpers resolved.
|
|
591
|
+
def resolved_current_organization=(organization)
|
|
592
|
+
@_current_organization = organization
|
|
593
|
+
Organizations::Current.organization = organization
|
|
594
|
+
end
|
|
595
|
+
|
|
588
596
|
# Drop the memoized membership so the next current_membership call reads
|
|
589
597
|
# it again for the organization that is current by then. Assigning nil
|
|
590
598
|
# would not do: current_membership memoizes nil too, so it would keep
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "active_support/current_attributes"
|
|
4
|
+
|
|
5
|
+
module Organizations
|
|
6
|
+
# The organization the current request or job acts for.
|
|
7
|
+
#
|
|
8
|
+
# The controller helpers set it whenever `current_organization` resolves
|
|
9
|
+
# or changes (a switch, a fallback, a sign-out from the organization).
|
|
10
|
+
# Jobs and scripts set it with `Organizations.with_organization`. Rails
|
|
11
|
+
# resets it before every request and every job, so a value never leaks
|
|
12
|
+
# from one to the next.
|
|
13
|
+
#
|
|
14
|
+
# Organizations::Current.organization # => #<Organizations::Organization id: 42>
|
|
15
|
+
#
|
|
16
|
+
# It is the one place a host hooks its own tenant context, such as a
|
|
17
|
+
# PostgreSQL setting that row-level security policies read: every change
|
|
18
|
+
# goes through `organization=`, and every reset through `resets`.
|
|
19
|
+
#
|
|
20
|
+
# module TenantContext
|
|
21
|
+
# def organization=(organization)
|
|
22
|
+
# super
|
|
23
|
+
# Tenant.enter(organization)
|
|
24
|
+
# end
|
|
25
|
+
# end
|
|
26
|
+
#
|
|
27
|
+
# Organizations::Current.prepend(TenantContext)
|
|
28
|
+
# Organizations::Current.resets { Tenant.leave }
|
|
29
|
+
class Current < ActiveSupport::CurrentAttributes
|
|
30
|
+
attribute :organization
|
|
31
|
+
end
|
|
32
|
+
end
|
data/lib/organizations.rb
CHANGED
|
@@ -112,6 +112,7 @@ module Organizations
|
|
|
112
112
|
autoload :ActsAsTenantIntegration, "organizations/acts_as_tenant_integration"
|
|
113
113
|
autoload :TestHelpers, "organizations/test_helpers"
|
|
114
114
|
autoload :CurrentUserResolution, "organizations/current_user_resolution"
|
|
115
|
+
autoload :Current, "organizations/current"
|
|
115
116
|
autoload :InvitationAcceptanceResult, "organizations/invitation_acceptance_result"
|
|
116
117
|
autoload :InvitationAcceptanceFailure, "organizations/invitation_acceptance_failure"
|
|
117
118
|
autoload :EmailNormalizer, "organizations/email_normalizer"
|
|
@@ -213,6 +214,21 @@ module Organizations
|
|
|
213
214
|
raise
|
|
214
215
|
end
|
|
215
216
|
|
|
217
|
+
# Runs the block with `organization` as the current organization, then
|
|
218
|
+
# puts back whatever was current before, even if the block raises. For
|
|
219
|
+
# jobs, scripts and consoles, where no controller resolves one.
|
|
220
|
+
#
|
|
221
|
+
# @example
|
|
222
|
+
# Organizations.with_organization(invoice.organization) do
|
|
223
|
+
# InvoiceMailer.paid(invoice).deliver_now
|
|
224
|
+
# end
|
|
225
|
+
#
|
|
226
|
+
# @param organization [Organizations::Organization, nil]
|
|
227
|
+
# @return the block's value
|
|
228
|
+
def with_organization(organization, &)
|
|
229
|
+
Current.set(organization: organization, &)
|
|
230
|
+
end
|
|
231
|
+
|
|
216
232
|
# Reset configuration to defaults
|
|
217
233
|
# Primarily used in tests
|
|
218
234
|
def reset_configuration!
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: organizations
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.9.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- rameerez
|
|
@@ -155,6 +155,7 @@ files:
|
|
|
155
155
|
- lib/organizations/callbacks.rb
|
|
156
156
|
- lib/organizations/configuration.rb
|
|
157
157
|
- lib/organizations/controller_helpers.rb
|
|
158
|
+
- lib/organizations/current.rb
|
|
158
159
|
- lib/organizations/current_user_resolution.rb
|
|
159
160
|
- lib/organizations/email_normalizer.rb
|
|
160
161
|
- lib/organizations/engine.rb
|