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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e1981c6ea91e6ef43e674e0f6d3bba9eebe8b1852f0745df774cf571c03d3c54
4
- data.tar.gz: 4f8d4c87cdb079ec884aa3391384cae1134f025204aacbe2c4ad22a6ae204f7b
3
+ metadata.gz: 96309750320bfc1e4e183b299c46a3ac8e925ae71ae58726146a3823747c48c2
4
+ data.tar.gz: 59324d68cac1e4c1669d1b7c797f21eff549acfed7ffb54ec053057aea1a69ae
5
5
  SHA512:
6
- metadata.gz: c3258b8a167dd07e0f8f63ce0307429cd626761b4e26bb6ef3279063c119b595660613498667eafbcf5ea3eedc2f0d5603d395f14e8ec05ab1fdf9300bf2c5dc
7
- data.tar.gz: 11e72266e807ae48685454eadbed040b586d1ddc6bc284954ae36267afc2c2ef0ae3d19dbc67408bb7d3705fb352319d2a4e8268cdf520e02910725c742f41c4
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
- gid.model_class.find(gid.model_id)
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 :ensure_tenant_context_safety
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 association(name)
34
- super.tap do |assoc|
35
- if assoc.reflection.polymorphic? || assoc.reflection.klass.tenanted?
36
- ensure_tenant_context_safety
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
- def ensure_tenant_context_safety
54
- self_tenant = self.tenant
55
- current_tenant = self.class.current_tenant
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
 
@@ -32,6 +32,9 @@ module ActiveRecord
32
32
  db_config.max_connections
33
33
  end
34
34
 
35
+ def clear_query_cache
36
+ end
37
+
35
38
  def lease_connection(...)
36
39
  raise Tenanted::NoTenantError, "Cannot connect to a tenanted database while untenanted (#{@model})."
37
40
  end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module ActiveRecord
4
4
  module Tenanted
5
- VERSION = "0.7.0"
5
+ VERSION = "0.8.0"
6
6
  end
7
7
  end
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.7.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