active_typed_store 2.7.0 → 2.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: e69cb858697a79e92cbfcfb8147a2c8c5861e94a693b7097ce8f961bc4973bf6
4
- data.tar.gz: 934c15165efc0c5f04d59c2f831b262866abedd49ec1ab0efaf978417a58783e
3
+ metadata.gz: 50dfb0f6df80a8fc7ad44f7f5ea9ae9eda71b8257e27b85c9a6a253633bb654b
4
+ data.tar.gz: 2d2c7cc056e39e9b9e23a494ebe7c973f5a99030c370324a97f9ff23ba5fff35
5
5
  SHA512:
6
- metadata.gz: 9507a3d04171ca491053553829c22662c46f301efd01712ddf47173a07bcfb1867eec373ec7d9e8d9ec907216bc8fbd383ba2fa4e79e1953c6ed313de511eb9a
7
- data.tar.gz: 010a2ac3ad8063d1ee9bab17b7437bd348ff2d78856cf970c23a6bb2603822c4c8f1dfa844e3456ec81d4b95c80c1de444b85bbf164f9a725ceaa31f095c13bf
6
+ metadata.gz: 5f99b76c42b7ee43bbb5c6513ca259788994eac8111824d4988b3a187198e2d9e5d49747b528a46674c73813876a9a99ef73d540aa0cf5c8b3e325bfeec86a82
7
+ data.tar.gz: 924a6e45783e10176782d5989380022cdd84be33d1ad2809b9ee2acf2047ea7a0d9701ace6e6b3732ea55ace5c90543af3af9ff867dd55304e2d48ad06169555
data/Gemfile.lock CHANGED
@@ -22,7 +22,7 @@ GIT
22
22
  PATH
23
23
  remote: .
24
24
  specs:
25
- active_typed_store (2.7.0)
25
+ active_typed_store (2.8.0)
26
26
  activemodel (>= 7.1)
27
27
  activerecord (>= 7.1)
28
28
  activesupport (>= 7.1)
data/README.md CHANGED
@@ -143,31 +143,6 @@ class Parcel
143
143
  end
144
144
  ```
145
145
 
146
- ### Encrypted store
147
-
148
- `encrypted: true` keeps keys of the store readable and encrypts every value like Rails `encrypts`:
149
-
150
- ```ruby
151
- class SupplierImportSetting < ActiveRecord::Base
152
- typed_store :marketplace_credentials, encrypted: true do
153
- attr :wb_token, :string
154
- attr :ozon_token, :string
155
- attr :ozon_client_id, :string
156
- end
157
- end
158
-
159
- setting.update!(wb_token: "secret", ozon_client_id: "123")
160
- # in the database: {"wb_token": "{\"p\":\"...\",\"h\":{...}}", "ozon_client_id": "{\"p\":\"...\",\"h\":{...}}"}
161
- ```
162
-
163
- - Values are encrypted by `ActiveRecord::Encryption::EncryptedAttributeType` with the same scheme as `encrypts`:
164
- [Active Record Encryption](https://guides.rubyonrails.org/active_record_encryption.html) must be configured,
165
- `encrypts` options can be passed instead of `true`: `encrypted: { deterministic: true }`,
166
- `encrypted: { previous: { key: "old key" } }`, `encrypted: { support_unencrypted_data: true }`.
167
- - Any value type is supported: the value is JSON-encoded before encryption.
168
- - Don't add `encrypts` for the store column or its accessors: it conflicts with typed_store or silently does nothing.
169
- - Like with `encrypts`, decrypted values are in `changes`, `attributes` and the store hash.
170
-
171
146
  ### Hash safety
172
147
  This gem assumes you're using a database that supports structured data types, such as `json` in `PostgreSQL` or `MySQL`, and leverages Rails' [store_accessor](https://edgeapi.rubyonrails.org/classes/ActiveRecord/Store.html) under the hood. However, there’s one caveat: JSON columns use a string-keyed hash and don’t support access via symbols. To avoid unexpected errors when accessing the hash, we raise an error if a symbol is used. You can disable this behavior by setting `config.hash_safety = false`.
173
148
 
@@ -191,6 +166,49 @@ record.params["price"] # 1
191
166
  record.params[:price] # nil - isn't the expected behavior for most applications
192
167
  ```
193
168
 
169
+ ### Encryption
170
+
171
+ `active_typed_store` does not encrypt, on purpose: Rails `encrypts` works on whole columns only,
172
+ and per-key encryption inside a JSON store is not a Rails pattern (Lockbox: "encrypt the column
173
+ rather than individual accessors"). Keep secrets (API tokens, credentials) out of `typed_store`
174
+ and use the standard Rails API on a separate `text` column:
175
+
176
+ ```ruby
177
+ class AddCredentialsToModels < ActiveRecord::Migration[8.1]
178
+ def change
179
+ add_column :models, :credentials, :text, null: false, default: "{}"
180
+ end
181
+ end
182
+ ```
183
+
184
+ ```ruby
185
+ class Model < ActiveRecord::Base
186
+ typed_store(:params) do
187
+ attr :task_id, :integer
188
+ end
189
+
190
+ attribute :credentials, :json
191
+ store_accessor :credentials, :api_token, :client_id
192
+ encrypts :credentials
193
+ end
194
+
195
+ m = Model.create!(api_token: "secret")
196
+ m.api_token # => "secret"
197
+ m.ciphertext_for(:credentials) # => "{\"p\":\"...\",\"h\":{...}}" - no plaintext in the database
198
+ ```
199
+
200
+ Why this exact shape (verified on Rails 8.1 and PostgreSQL):
201
+
202
+ - `text`, not `jsonb`: the column holds ciphertext, a JSON envelope in `jsonb` only misleads SQL readers.
203
+ - `null: false, default: "{}"`: `encrypts` passes the column default through, so an empty store
204
+ reads back as `{}` without marking the record dirty.
205
+ - `attribute :json` + `store_accessor`, not `store :credentials, coder: JSON`: `store` serializes
206
+ an empty hash as `NULL`, which a `NOT NULL` column rejects, and on a nullable column
207
+ Rails <= 8.1 marks the record dirty on every read.
208
+
209
+ `previous_changes` still holds the plaintext hash: filter `encrypted_attributes` in audit-log
210
+ libraries (`audited` does, `paper_trail` since 15.0).
211
+
194
212
  ### Benchmarks
195
213
  compare `active_typed_store` with other gems
196
214
  ```ruby
@@ -10,25 +10,16 @@ module ActiveTypedStore
10
10
 
11
11
  CUSTOM_JSON = CustomJson.new
12
12
 
13
- def typed_store(store_attribute, encrypted: false, &block)
13
+ def typed_store(store_attribute, &)
14
14
  attrs = Attrs.new(store_attribute)
15
- attrs.instance_eval(&block)
16
-
17
- json_type =
18
- if encrypted
19
- # not required on load: it loads ActiveRecord::Encryption
20
- require_relative "encrypted_json"
21
- EncryptedJson.new(encrypted == true ? {} : encrypted)
22
- else
23
- CUSTOM_JSON
24
- end
15
+ attrs.instance_eval(&)
25
16
 
26
17
  define_singleton_method(:inherited) do |subclass|
27
18
  super(subclass)
28
- subclass.define_attribute(store_attribute.name, json_type)
19
+ subclass.define_attribute(store_attribute.name, CUSTOM_JSON)
29
20
  end
30
21
 
31
- define_attribute store_attribute.name, json_type
22
+ define_attribute store_attribute.name, CUSTOM_JSON
32
23
  store_accessor store_attribute, attrs.fields
33
24
 
34
25
  if ActiveTypedStore.config.hash_safety == :disallow_symbol_keys
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ActiveTypedStore
4
- VERSION = "2.7.0"
4
+ VERSION = "2.8.0"
5
5
  end
metadata CHANGED
@@ -1,14 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: active_typed_store
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.7.0
4
+ version: 2.8.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ermolaev Andrey
8
- autorequire:
9
8
  bindir: exe
10
9
  cert_chain: []
11
- date: 2026-10-15 00:00:00.000000000 Z
10
+ date: 1980-01-02 00:00:00.000000000 Z
12
11
  dependencies:
13
12
  - !ruby/object:Gem::Dependency
14
13
  name: activemodel
@@ -16,43 +15,42 @@ dependencies:
16
15
  requirements:
17
16
  - - ">="
18
17
  - !ruby/object:Gem::Version
19
- version: '7.1'
18
+ version: '0'
20
19
  type: :runtime
21
20
  prerelease: false
22
21
  version_requirements: !ruby/object:Gem::Requirement
23
22
  requirements:
24
23
  - - ">="
25
24
  - !ruby/object:Gem::Version
26
- version: '7.1'
25
+ version: '0'
27
26
  - !ruby/object:Gem::Dependency
28
27
  name: activerecord
29
28
  requirement: !ruby/object:Gem::Requirement
30
29
  requirements:
31
30
  - - ">="
32
31
  - !ruby/object:Gem::Version
33
- version: '7.1'
32
+ version: '0'
34
33
  type: :runtime
35
34
  prerelease: false
36
35
  version_requirements: !ruby/object:Gem::Requirement
37
36
  requirements:
38
37
  - - ">="
39
38
  - !ruby/object:Gem::Version
40
- version: '7.1'
39
+ version: '0'
41
40
  - !ruby/object:Gem::Dependency
42
41
  name: activesupport
43
42
  requirement: !ruby/object:Gem::Requirement
44
43
  requirements:
45
44
  - - ">="
46
45
  - !ruby/object:Gem::Version
47
- version: '7.1'
46
+ version: '0'
48
47
  type: :runtime
49
48
  prerelease: false
50
49
  version_requirements: !ruby/object:Gem::Requirement
51
50
  requirements:
52
51
  - - ">="
53
52
  - !ruby/object:Gem::Version
54
- version: '7.1'
55
- description:
53
+ version: '0'
56
54
  email:
57
55
  - andruhafirst@yandex.ru
58
56
  executables: []
@@ -72,7 +70,6 @@ files:
72
70
  - lib/active_typed_store/attrs.rb
73
71
  - lib/active_typed_store/configuration.rb
74
72
  - lib/active_typed_store/disallow_symbol_keys.rb
75
- - lib/active_typed_store/encrypted_json.rb
76
73
  - lib/active_typed_store/store.rb
77
74
  - lib/active_typed_store/version.rb
78
75
  homepage: https://github.com/corp-gp/active_typed_store
@@ -80,7 +77,6 @@ licenses:
80
77
  - MIT
81
78
  metadata:
82
79
  rubygems_mfa_required: 'true'
83
- post_install_message:
84
80
  rdoc_options: []
85
81
  require_paths:
86
82
  - lib
@@ -95,8 +91,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
95
91
  - !ruby/object:Gem::Version
96
92
  version: '0'
97
93
  requirements: []
98
- rubygems_version: 3.0.3.1
99
- signing_key:
94
+ rubygems_version: 3.6.9
100
95
  specification_version: 4
101
96
  summary: Typed store for active records json columns (postgresql, mysql, sqlite)
102
97
  test_files: []
@@ -1,46 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module ActiveTypedStore
4
- module Store
5
- # typed_store(:params, encrypted: true): keys are stored as is, every value is encrypted like Rails `encrypts`.
6
- # Options are `encrypts` options: encrypted: { deterministic: true, previous: ..., support_unencrypted_data: true }
7
- class EncryptedJson < CustomJson
8
- # value is JSON-encoded before encryption to keep its type; not encrypted value (support_unencrypted_data)
9
- # is returned as is, it may be not JSON
10
- class ValueJson < ActiveRecord::Type::Json
11
- def deserialize(value)
12
- return value unless value.is_a?(::String)
13
-
14
- ActiveSupport::JSON.decode(value)
15
- rescue JSON::ParserError
16
- value
17
- end
18
- end
19
-
20
- def initialize(scheme_options)
21
- super()
22
- @scheme_options = scheme_options
23
- end
24
-
25
- def serialize(value) = super(value.is_a?(Hash) ? value.transform_values { value_type.serialize(_1) } : value)
26
-
27
- def deserialize(value)
28
- hash = super
29
- hash.is_a?(Hash) ? hash.transform_values { value_type.deserialize(_1) } : hash
30
- end
31
-
32
- # deserialize(serialize(value)) gives the same as plain json, but encrypts and decrypts every value
33
- # on every assignment and dirty check
34
- def cast(value) = CUSTOM_JSON.cast(value)
35
- def changed_in_place?(raw_old_value, new_value) = deserialize(raw_old_value) != cast(new_value)
36
-
37
- # built lazily like `encrypts` does: encryption config may be not loaded yet when the model is defined
38
- private def value_type
39
- @value_type ||= ActiveRecord::Encryption::EncryptedAttributeType.new(
40
- scheme: ActiveRecord::Base.send(:scheme_for, **@scheme_options),
41
- cast_type: ValueJson.new,
42
- )
43
- end
44
- end
45
- end
46
- end