activerecord-tenanted 0.7.0 → 0.8.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/lib/active_record/tenanted/associations.rb +32 -0
- data/lib/active_record/tenanted/global_id.rb +7 -2
- data/lib/active_record/tenanted/message_pack.rb +26 -0
- data/lib/active_record/tenanted/railtie.rb +8 -0
- data/lib/active_record/tenanted/tenant.rb +120 -16
- data/lib/active_record/tenanted/untenanted_connection_pool.rb +3 -0
- data/lib/active_record/tenanted/version.rb +1 -1
- metadata +3 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 96309750320bfc1e4e183b299c46a3ac8e925ae71ae58726146a3823747c48c2
|
|
4
|
+
data.tar.gz: 59324d68cac1e4c1669d1b7c797f21eff549acfed7ffb54ec053057aea1a69ae
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: ac643449dfda377954739d2210b2f011e832cffed4599dca4c0aa8a5fe692a87dffca7ed2e719667dd911ddbc52523ac9dae3e4bccd419282d186366ae7cf42e
|
|
7
|
+
data.tar.gz: 36f29134a66ed8619e26e6f12b04e497ec2e2ec1e8db350dfe6c505630394dbddd2a896ce6b11e1ba1956f0d34309bfda4d2951813ac2c7c1f9db622c403f6d2
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ActiveRecord
|
|
4
|
+
module Tenanted
|
|
5
|
+
# Tenant context is checked when an association reads or writes the database, and not when the
|
|
6
|
+
# Association object is created. Serializers create associations to dump and restore their
|
|
7
|
+
# targets without ever going near a connection, and must not be tripped up by the check.
|
|
8
|
+
#
|
|
9
|
+
# `klass` is resolved at both of these seams, so a polymorphic association is checked against
|
|
10
|
+
# the class it actually points at rather than being presumed tenanted.
|
|
11
|
+
module Associations # :nodoc:
|
|
12
|
+
# Every query built on behalf of an association funnels through here, including the ones
|
|
13
|
+
# issued by a collection proxy that has outlived the tenant context it was created in.
|
|
14
|
+
def scope
|
|
15
|
+
ensure_owner_tenant_context_safety
|
|
16
|
+
super
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
private
|
|
20
|
+
# Called by the collection and singular association readers, so that the exception is
|
|
21
|
+
# raised at the call site that made the mistake and not at the eventual query.
|
|
22
|
+
def ensure_klass_exists!
|
|
23
|
+
super
|
|
24
|
+
ensure_owner_tenant_context_safety
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def ensure_owner_tenant_context_safety
|
|
28
|
+
owner.ensure_tenant_context_safety if owner.class.tenanted? && klass&.tenanted?
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
@@ -9,10 +9,15 @@ module ActiveRecord
|
|
|
9
9
|
params && params[:tenant]
|
|
10
10
|
end
|
|
11
11
|
|
|
12
|
-
class Locator
|
|
12
|
+
class Locator < ::GlobalID::Locator::UnscopedLocator
|
|
13
13
|
def locate(gid, options = {})
|
|
14
14
|
ensure_tenant_context_safety(gid)
|
|
15
|
-
|
|
15
|
+
super
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def locate_many(gids, options = {})
|
|
19
|
+
gids.each { |gid| ensure_tenant_context_safety(gid) }
|
|
20
|
+
super
|
|
16
21
|
end
|
|
17
22
|
|
|
18
23
|
private
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ActiveRecord
|
|
4
|
+
module Tenanted
|
|
5
|
+
module MessagePack # :nodoc:
|
|
6
|
+
# This patch sees every record in a payload, unlike the Marshal, YAML, and JSON hooks, which
|
|
7
|
+
# are methods on the record and so only ever run for tenanted models.
|
|
8
|
+
module Decoder
|
|
9
|
+
def build_record(entry)
|
|
10
|
+
class_name, attributes_hash, * = entry
|
|
11
|
+
|
|
12
|
+
if ActiveSupport::MessagePack::Extensions.load_class(class_name).tenanted?
|
|
13
|
+
has_tenant = attributes_hash.key?("tenant")
|
|
14
|
+
tenant_name = attributes_hash.delete("tenant")
|
|
15
|
+
|
|
16
|
+
super.tap do |record|
|
|
17
|
+
record.instance_variable_set(:@tenant, tenant_name) if has_tenant
|
|
18
|
+
end
|
|
19
|
+
else
|
|
20
|
+
super
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -75,6 +75,7 @@ module ActiveRecord
|
|
|
75
75
|
ActiveSupport.on_load(:active_record) do
|
|
76
76
|
prepend ActiveRecord::Tenanted::Base
|
|
77
77
|
ActiveRecord::Relation.prepend ActiveRecord::Tenanted::Relation
|
|
78
|
+
ActiveRecord::Associations::Association.prepend ActiveRecord::Tenanted::Associations
|
|
78
79
|
end
|
|
79
80
|
end
|
|
80
81
|
|
|
@@ -93,6 +94,13 @@ module ActiveRecord
|
|
|
93
94
|
config.active_record.check_schema_cache_dump_version = false
|
|
94
95
|
end
|
|
95
96
|
|
|
97
|
+
initializer "active_record_tenanted.message_pack" do
|
|
98
|
+
ActiveSupport.on_load(:message_pack) do
|
|
99
|
+
require "active_record/message_pack"
|
|
100
|
+
ActiveRecord::MessagePack::Decoder.prepend ActiveRecord::Tenanted::MessagePack::Decoder
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
|
|
96
104
|
initializer "active_record_tenanted.monkey_patches" do
|
|
97
105
|
ActiveSupport.on_load(:active_record) do
|
|
98
106
|
ActiveRecord::Tenanted::Patches::Attributes.apply_patch
|
|
@@ -9,13 +9,24 @@ module ActiveRecord
|
|
|
9
9
|
prepended do
|
|
10
10
|
attr_reader :tenant
|
|
11
11
|
|
|
12
|
-
before_save :
|
|
12
|
+
before_save :ensure_belongs_to_tenant_safety, prepend: true
|
|
13
|
+
before_save :ensure_tenant_context_safety, prepend: true
|
|
14
|
+
before_destroy :ensure_tenant_context_safety, prepend: true
|
|
13
15
|
end
|
|
14
16
|
|
|
15
17
|
def cache_key
|
|
16
18
|
tenant ? "#{tenant}/#{super}" : super
|
|
17
19
|
end
|
|
18
20
|
|
|
21
|
+
def ==(other)
|
|
22
|
+
super && tenant == other.tenant
|
|
23
|
+
end
|
|
24
|
+
alias eql? ==
|
|
25
|
+
|
|
26
|
+
def hash
|
|
27
|
+
[ super, tenant ].hash
|
|
28
|
+
end
|
|
29
|
+
|
|
19
30
|
def inspect
|
|
20
31
|
return super unless tenant
|
|
21
32
|
|
|
@@ -30,14 +41,114 @@ module ActiveRecord
|
|
|
30
41
|
super(options.merge(tenant: tenant))
|
|
31
42
|
end
|
|
32
43
|
|
|
33
|
-
def
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
44
|
+
def reload(...)
|
|
45
|
+
ensure_tenant_context_safety
|
|
46
|
+
|
|
47
|
+
super
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def valid?(...)
|
|
51
|
+
ensure_tenant_context_safety
|
|
52
|
+
|
|
53
|
+
super
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def delete
|
|
57
|
+
ensure_tenant_context_safety
|
|
58
|
+
|
|
59
|
+
super
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# #update_column delegates here
|
|
63
|
+
def update_columns(...)
|
|
64
|
+
ensure_tenant_context_safety
|
|
65
|
+
|
|
66
|
+
super
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def touch(...)
|
|
70
|
+
ensure_tenant_context_safety
|
|
71
|
+
|
|
72
|
+
super
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# #decrement! delegates here
|
|
76
|
+
def increment!(...)
|
|
77
|
+
ensure_tenant_context_safety
|
|
78
|
+
|
|
79
|
+
super
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# A belongs_to writes its foreign key onto this record, so nothing else notices when the
|
|
83
|
+
# target came from another tenant's database.
|
|
84
|
+
def ensure_belongs_to_tenant_safety
|
|
85
|
+
self.class.reflect_on_all_associations(:belongs_to).each do |reflection|
|
|
86
|
+
# Avoid creating the Association object for a belongs_to that was never touched
|
|
87
|
+
next unless association_cached?(reflection.name)
|
|
88
|
+
|
|
89
|
+
target = association(reflection.name).target
|
|
90
|
+
|
|
91
|
+
if target && target.class.tenanted? && target.tenant != tenant
|
|
92
|
+
raise WrongTenantError,
|
|
93
|
+
"#{self.class} model belongs to tenant #{tenant.inspect}, but its " \
|
|
94
|
+
"#{reflection.name} association belongs to tenant #{target.tenant.inspect}"
|
|
37
95
|
end
|
|
38
96
|
end
|
|
39
97
|
end
|
|
40
98
|
|
|
99
|
+
def ensure_tenant_context_safety
|
|
100
|
+
self_tenant = self.tenant
|
|
101
|
+
current_tenant = self.class.current_tenant
|
|
102
|
+
|
|
103
|
+
if current_tenant.nil?
|
|
104
|
+
raise NoTenantError, "Cannot connect to a tenanted database while untenanted (#{self.class})"
|
|
105
|
+
elsif self_tenant != current_tenant
|
|
106
|
+
raise WrongTenantError,
|
|
107
|
+
"#{self.class} model belongs to tenant #{self_tenant.inspect}, " \
|
|
108
|
+
"but current tenant is #{current_tenant.inspect}"
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# The tenant is an ordinary attribute in serialized snapshots of the record, the same way Job
|
|
113
|
+
# and GlobalId serialize the tenant. Rails's only callers of this method are the serializers
|
|
114
|
+
# (Marshal 7.1 format and MessagePack); it does not feed the persistence write path.
|
|
115
|
+
def attributes_for_database
|
|
116
|
+
super.merge!("tenant" => tenant)
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def encode_with(coder)
|
|
120
|
+
super
|
|
121
|
+
coder["tenant"] = tenant
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def init_with(coder, &block)
|
|
125
|
+
super
|
|
126
|
+
|
|
127
|
+
# Psych::Coder does not implement #key?, but exposes the underlying hash as #map
|
|
128
|
+
@tenant = coder.map["tenant"] if coder.map.key?("tenant")
|
|
129
|
+
|
|
130
|
+
self
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def from_json(json, include_root = include_root_in_json)
|
|
134
|
+
hash = ActiveSupport::JSON.decode(json)
|
|
135
|
+
hash = hash.values.first if include_root
|
|
136
|
+
|
|
137
|
+
@tenant = hash.delete("tenant") if hash.key?("tenant")
|
|
138
|
+
|
|
139
|
+
self.attributes = hash
|
|
140
|
+
self
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def marshal_load(state)
|
|
144
|
+
has_tenant = state[0].key?("tenant")
|
|
145
|
+
tenant_name = state[0].delete("tenant")
|
|
146
|
+
|
|
147
|
+
super
|
|
148
|
+
|
|
149
|
+
@tenant = tenant_name if has_tenant
|
|
150
|
+
end
|
|
151
|
+
|
|
41
152
|
alias to_gid to_global_id
|
|
42
153
|
alias to_sgid to_signed_global_id
|
|
43
154
|
|
|
@@ -50,17 +161,10 @@ module ActiveRecord
|
|
|
50
161
|
super
|
|
51
162
|
end
|
|
52
163
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
if current_tenant.nil?
|
|
58
|
-
raise NoTenantError, "Cannot connect to a tenanted database while untenanted (#{self.class})"
|
|
59
|
-
elsif self_tenant != current_tenant
|
|
60
|
-
raise WrongTenantError,
|
|
61
|
-
"#{self.class} model belongs to tenant #{self_tenant.inspect}, " \
|
|
62
|
-
"but current tenant is #{current_tenant.inspect}"
|
|
63
|
-
end
|
|
164
|
+
# Presenting the tenant as one more serializable attribute lets ActiveModel apply the
|
|
165
|
+
# `only:` and `except:` options to it, and read it back through the `tenant` reader.
|
|
166
|
+
def attribute_names_for_serialization
|
|
167
|
+
super + [ "tenant" ]
|
|
64
168
|
end
|
|
65
169
|
end
|
|
66
170
|
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: activerecord-tenanted
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.8.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Mike Dalessio
|
|
@@ -69,6 +69,7 @@ files:
|
|
|
69
69
|
- README.md
|
|
70
70
|
- Rakefile
|
|
71
71
|
- lib/active_record/tenanted.rb
|
|
72
|
+
- lib/active_record/tenanted/associations.rb
|
|
72
73
|
- lib/active_record/tenanted/base.rb
|
|
73
74
|
- lib/active_record/tenanted/cable_connection.rb
|
|
74
75
|
- lib/active_record/tenanted/connection_adapter.rb
|
|
@@ -83,6 +84,7 @@ files:
|
|
|
83
84
|
- lib/active_record/tenanted/job.rb
|
|
84
85
|
- lib/active_record/tenanted/lru.rb
|
|
85
86
|
- lib/active_record/tenanted/mailer.rb
|
|
87
|
+
- lib/active_record/tenanted/message_pack.rb
|
|
86
88
|
- lib/active_record/tenanted/mutex.rb
|
|
87
89
|
- lib/active_record/tenanted/patches.rb
|
|
88
90
|
- lib/active_record/tenanted/railtie.rb
|