active_typed_store 2.5.0 → 2.7.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: 27497c160e7ea837aa40cbf08e2eca56d71f4685e98aa45dfbd9393774bed779
4
- data.tar.gz: df7b297c0b689c97ebde94e60cf21ea783cdc716bdc5ed131707a3886cc63222
3
+ metadata.gz: e69cb858697a79e92cbfcfb8147a2c8c5861e94a693b7097ce8f961bc4973bf6
4
+ data.tar.gz: 934c15165efc0c5f04d59c2f831b262866abedd49ec1ab0efaf978417a58783e
5
5
  SHA512:
6
- metadata.gz: 5e37554abe69e485bf0a46d26309934ea7fd01071c4ad0ff066db29b0ce3b94bfff490bd7316b775dedf3116fff78afcded3085590d311ead02653772a0a9acc
7
- data.tar.gz: 11a504ffe39d31a58b6a56d938041e6978a826c8136ea6cf041884d01c988df0433ff5dab33d1738a53810db273e0635ffd112b2b233a794e0b07b8dd6d5f9a0
6
+ metadata.gz: 9507a3d04171ca491053553829c22662c46f301efd01712ddf47173a07bcfb1867eec373ec7d9e8d9ec907216bc8fbd383ba2fa4e79e1953c6ed313de511eb9a
7
+ data.tar.gz: 010a2ac3ad8063d1ee9bab17b7437bd348ff2d78856cf970c23a6bb2603822c4c8f1dfa844e3456ec81d4b95c80c1de444b85bbf164f9a725ceaa31f095c13bf
data/Gemfile.lock CHANGED
@@ -22,10 +22,10 @@ GIT
22
22
  PATH
23
23
  remote: .
24
24
  specs:
25
- active_typed_store (2.5.0)
26
- activemodel
27
- activerecord
28
- activesupport
25
+ active_typed_store (2.7.0)
26
+ activemodel (>= 7.1)
27
+ activerecord (>= 7.1)
28
+ activesupport (>= 7.1)
29
29
 
30
30
  GEM
31
31
  remote: https://rubygems.org/
data/README.md CHANGED
@@ -143,6 +143,31 @@ 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
+
146
171
  ### Hash safety
147
172
  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`.
148
173
 
@@ -0,0 +1,46 @@
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
@@ -4,22 +4,31 @@ module ActiveTypedStore
4
4
  module Store
5
5
  class CustomJson < ActiveRecord::Type::Json
6
6
  def changed_in_place?(raw_old_value, new_value)
7
- deserialize(raw_old_value) != new_value.as_json
7
+ deserialize(raw_old_value) != deserialize(serialize(new_value))
8
8
  end
9
9
  end
10
10
 
11
11
  CUSTOM_JSON = CustomJson.new
12
12
 
13
- def typed_store(store_attribute, &)
13
+ def typed_store(store_attribute, encrypted: false, &block)
14
14
  attrs = Attrs.new(store_attribute)
15
- attrs.instance_eval(&)
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
16
25
 
17
26
  define_singleton_method(:inherited) do |subclass|
18
27
  super(subclass)
19
- subclass.define_attribute(store_attribute.name, CUSTOM_JSON)
28
+ subclass.define_attribute(store_attribute.name, json_type)
20
29
  end
21
30
 
22
- define_attribute store_attribute.name, CUSTOM_JSON
31
+ define_attribute store_attribute.name, json_type
23
32
  store_accessor store_attribute, attrs.fields
24
33
 
25
34
  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.5.0"
4
+ VERSION = "2.7.0"
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: active_typed_store
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.5.0
4
+ version: 2.7.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ermolaev Andrey
8
- autorequire:
8
+ autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2026-02-04 00:00:00.000000000 Z
11
+ date: 2026-10-15 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activemodel
@@ -16,43 +16,43 @@ dependencies:
16
16
  requirements:
17
17
  - - ">="
18
18
  - !ruby/object:Gem::Version
19
- version: '0'
19
+ version: '7.1'
20
20
  type: :runtime
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
24
  - - ">="
25
25
  - !ruby/object:Gem::Version
26
- version: '0'
26
+ version: '7.1'
27
27
  - !ruby/object:Gem::Dependency
28
28
  name: activerecord
29
29
  requirement: !ruby/object:Gem::Requirement
30
30
  requirements:
31
31
  - - ">="
32
32
  - !ruby/object:Gem::Version
33
- version: '0'
33
+ version: '7.1'
34
34
  type: :runtime
35
35
  prerelease: false
36
36
  version_requirements: !ruby/object:Gem::Requirement
37
37
  requirements:
38
38
  - - ">="
39
39
  - !ruby/object:Gem::Version
40
- version: '0'
40
+ version: '7.1'
41
41
  - !ruby/object:Gem::Dependency
42
42
  name: activesupport
43
43
  requirement: !ruby/object:Gem::Requirement
44
44
  requirements:
45
45
  - - ">="
46
46
  - !ruby/object:Gem::Version
47
- version: '0'
47
+ version: '7.1'
48
48
  type: :runtime
49
49
  prerelease: false
50
50
  version_requirements: !ruby/object:Gem::Requirement
51
51
  requirements:
52
52
  - - ">="
53
53
  - !ruby/object:Gem::Version
54
- version: '0'
55
- description:
54
+ version: '7.1'
55
+ description:
56
56
  email:
57
57
  - andruhafirst@yandex.ru
58
58
  executables: []
@@ -72,6 +72,7 @@ files:
72
72
  - lib/active_typed_store/attrs.rb
73
73
  - lib/active_typed_store/configuration.rb
74
74
  - lib/active_typed_store/disallow_symbol_keys.rb
75
+ - lib/active_typed_store/encrypted_json.rb
75
76
  - lib/active_typed_store/store.rb
76
77
  - lib/active_typed_store/version.rb
77
78
  homepage: https://github.com/corp-gp/active_typed_store
@@ -79,7 +80,7 @@ licenses:
79
80
  - MIT
80
81
  metadata:
81
82
  rubygems_mfa_required: 'true'
82
- post_install_message:
83
+ post_install_message:
83
84
  rdoc_options: []
84
85
  require_paths:
85
86
  - lib
@@ -94,8 +95,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
94
95
  - !ruby/object:Gem::Version
95
96
  version: '0'
96
97
  requirements: []
97
- rubygems_version: 3.3.7
98
- signing_key:
98
+ rubygems_version: 3.0.3.1
99
+ signing_key:
99
100
  specification_version: 4
100
101
  summary: Typed store for active records json columns (postgresql, mysql, sqlite)
101
102
  test_files: []