super_settings 0.0.0.rc1
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 +7 -0
- data/CHANGELOG.md +9 -0
- data/MIT-LICENSE +20 -0
- data/README.md +313 -0
- data/VERSION +1 -0
- data/app/helpers/super_settings/settings_helper.rb +32 -0
- data/app/views/layouts/super_settings/settings.html.erb +20 -0
- data/config/routes.rb +13 -0
- data/db/migrate/20210414004553_create_super_settings.rb +34 -0
- data/lib/super_settings/application/api.js +88 -0
- data/lib/super_settings/application/helper.rb +119 -0
- data/lib/super_settings/application/images/edit.svg +1 -0
- data/lib/super_settings/application/images/info.svg +1 -0
- data/lib/super_settings/application/images/plus.svg +1 -0
- data/lib/super_settings/application/images/slash.svg +1 -0
- data/lib/super_settings/application/images/trash.svg +1 -0
- data/lib/super_settings/application/index.html.erb +169 -0
- data/lib/super_settings/application/layout.html.erb +22 -0
- data/lib/super_settings/application/layout_styles.css +193 -0
- data/lib/super_settings/application/scripts.js +718 -0
- data/lib/super_settings/application/styles.css +122 -0
- data/lib/super_settings/application.rb +38 -0
- data/lib/super_settings/attributes.rb +24 -0
- data/lib/super_settings/coerce.rb +66 -0
- data/lib/super_settings/configuration.rb +144 -0
- data/lib/super_settings/controller_actions.rb +81 -0
- data/lib/super_settings/encryption.rb +76 -0
- data/lib/super_settings/engine.rb +70 -0
- data/lib/super_settings/history_item.rb +26 -0
- data/lib/super_settings/local_cache.rb +306 -0
- data/lib/super_settings/rack_middleware.rb +210 -0
- data/lib/super_settings/rest_api.rb +195 -0
- data/lib/super_settings/setting.rb +599 -0
- data/lib/super_settings/storage/active_record_storage.rb +123 -0
- data/lib/super_settings/storage/http_storage.rb +279 -0
- data/lib/super_settings/storage/redis_storage.rb +293 -0
- data/lib/super_settings/storage/test_storage.rb +158 -0
- data/lib/super_settings/storage.rb +254 -0
- data/lib/super_settings/version.rb +5 -0
- data/lib/super_settings.rb +213 -0
- data/lib/tasks/super_settings.rake +9 -0
- data/super_settings.gemspec +35 -0
- metadata +113 -0
@@ -0,0 +1,279 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "json"
|
4
|
+
require "net/http"
|
5
|
+
|
6
|
+
# SuperSettings::Storage model that reads from a remote service running the SuperSettings REST API.
|
7
|
+
# This storage engine is read only. It is intended to allow microservices to read settings from a
|
8
|
+
# central application that exposes the SuperSettings::RestAPI.
|
9
|
+
module SuperSettings
|
10
|
+
module Storage
|
11
|
+
class HttpStorage
|
12
|
+
include Storage
|
13
|
+
|
14
|
+
DEFAULT_HEADERS = {"Accept" => "application/json"}.freeze
|
15
|
+
DEFAULT_TIMEOUT = 5.0
|
16
|
+
|
17
|
+
attr_reader :key, :raw_value, :description, :value_type, :updated_at, :created_at
|
18
|
+
|
19
|
+
class Error < StandardError
|
20
|
+
end
|
21
|
+
|
22
|
+
class NotFoundError < Error
|
23
|
+
end
|
24
|
+
|
25
|
+
class InvalidRecordError < Error
|
26
|
+
attr_reader :errors
|
27
|
+
|
28
|
+
def initialize(message, errors:)
|
29
|
+
super(message)
|
30
|
+
@errors = errors
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
class HistoryStorage
|
35
|
+
include SuperSettings::Attributes
|
36
|
+
|
37
|
+
attr_accessor :key, :value, :changed_by, :deleted
|
38
|
+
|
39
|
+
def created_at=(val)
|
40
|
+
@created_at = SuperSettings::Coerce.time(val)
|
41
|
+
end
|
42
|
+
|
43
|
+
def deleted?
|
44
|
+
!!(defined?(@deleted) && @deleted)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
class << self
|
49
|
+
def all
|
50
|
+
call_api(:get, "/settings")["settings"].collect do |attributes|
|
51
|
+
new(attributes)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
def updated_since(time)
|
56
|
+
call_api(:get, "/settings/updated_since", time: time)["settings"].collect do |attributes|
|
57
|
+
new(attributes)
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
def find_by_key(key)
|
62
|
+
record = new(call_api(:get, "/setting", key: key))
|
63
|
+
record.send(:set_persisted!)
|
64
|
+
record
|
65
|
+
rescue NotFoundError
|
66
|
+
nil
|
67
|
+
end
|
68
|
+
|
69
|
+
def last_updated_at
|
70
|
+
value = call_api(:get, "/settings/last_updated_at")["last_updated_at"]
|
71
|
+
SuperSettings::Coerce.time(value)
|
72
|
+
end
|
73
|
+
|
74
|
+
attr_accessor :base_url
|
75
|
+
|
76
|
+
attr_accessor :timeout
|
77
|
+
|
78
|
+
def headers
|
79
|
+
@headers ||= {}
|
80
|
+
end
|
81
|
+
|
82
|
+
def query_params
|
83
|
+
@query_params ||= {}
|
84
|
+
end
|
85
|
+
|
86
|
+
protected
|
87
|
+
|
88
|
+
def default_load_asynchronous?
|
89
|
+
true
|
90
|
+
end
|
91
|
+
|
92
|
+
private
|
93
|
+
|
94
|
+
def call_api(method, path, params = {})
|
95
|
+
url_params = (method == :get ? query_params.merge(params) : query_params)
|
96
|
+
uri = api_uri(path, url_params)
|
97
|
+
|
98
|
+
body = nil
|
99
|
+
request_headers = DEFAULT_HEADERS.merge(headers)
|
100
|
+
if method == :post && !params&.empty?
|
101
|
+
body = params.to_json
|
102
|
+
request_headers["Content-Type"] = "application/json; charset=utf8-"
|
103
|
+
end
|
104
|
+
|
105
|
+
response = http_request(method: method, uri: uri, headers: request_headers, body: body)
|
106
|
+
|
107
|
+
begin
|
108
|
+
response.value # raises exception unless response is a success
|
109
|
+
JSON.parse(response.body)
|
110
|
+
rescue Net::ProtocolError
|
111
|
+
if [404, 410].include?(response.code.to_i)
|
112
|
+
raise NotFoundError.new("#{response.code} #{response.message}")
|
113
|
+
elsif response.code.to_i == 422
|
114
|
+
raise InvalidRecordError.new("#{response.code} #{response.message}", errors: JSON.parse(response.body)["errors"])
|
115
|
+
else
|
116
|
+
raise Error.new("#{response.code} #{response.message}")
|
117
|
+
end
|
118
|
+
rescue JSON::JSONError => e
|
119
|
+
raise Error.new(e.message)
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
def http_request(method:, uri:, headers: {}, body: nil, redirect_count: 0)
|
124
|
+
response = nil
|
125
|
+
http = Net::HTTP.new(uri.host, uri.port || uri.inferred_port)
|
126
|
+
begin
|
127
|
+
http.read_timeout = (timeout || DEFAULT_TIMEOUT)
|
128
|
+
http.open_timeout = (timeout || DEFAULT_TIMEOUT)
|
129
|
+
if uri.scheme == "https"
|
130
|
+
http.use_ssl = true
|
131
|
+
http.verify_mode = OpenSSL::SSL::VERIFY_NONE
|
132
|
+
end
|
133
|
+
|
134
|
+
request = (method == :post ? Net::HTTP::Post.new(uri.request_uri) : Net::HTTP::Get.new(uri.request_uri))
|
135
|
+
set_headers(request, headers)
|
136
|
+
request.body = body if body
|
137
|
+
|
138
|
+
response = http.request(request)
|
139
|
+
ensure
|
140
|
+
begin
|
141
|
+
http.finish if http.started?
|
142
|
+
rescue IOError
|
143
|
+
end
|
144
|
+
end
|
145
|
+
|
146
|
+
if response.is_a?(Net::HTTPRedirection)
|
147
|
+
location = resp["Location"]
|
148
|
+
if redirect_count < 5 && SuperSettings::Coerce.present?(location)
|
149
|
+
return http_request(method: :get, uri: URI(location), headers: headers, body: body, redirect_count: redirect_count + 1)
|
150
|
+
end
|
151
|
+
end
|
152
|
+
|
153
|
+
response
|
154
|
+
end
|
155
|
+
|
156
|
+
def api_uri(path, params)
|
157
|
+
uri = URI("#{base_url.chomp("/")}#{path}")
|
158
|
+
if params && !params.empty?
|
159
|
+
q = []
|
160
|
+
q << uri.query unless uri.query.to_s.empty?
|
161
|
+
params.each do |name, value|
|
162
|
+
q << "#{URI.encode_www_form_component(name.to_s)}=#{URI.encode_www_form_component(value.to_s)}"
|
163
|
+
end
|
164
|
+
uri.query = q.join("&")
|
165
|
+
end
|
166
|
+
uri
|
167
|
+
end
|
168
|
+
|
169
|
+
def set_headers(request, headers)
|
170
|
+
headers.each do |name, value|
|
171
|
+
name = name.to_s
|
172
|
+
values = Array(value)
|
173
|
+
request[name] = values[0].to_s
|
174
|
+
values[1, values.length].each do |val|
|
175
|
+
request.add_field(name, val.to_s)
|
176
|
+
end
|
177
|
+
end
|
178
|
+
end
|
179
|
+
end
|
180
|
+
|
181
|
+
def save!
|
182
|
+
payload = {key: key}
|
183
|
+
if deleted?
|
184
|
+
payload[:deleted] = true
|
185
|
+
else
|
186
|
+
payload[:value] = value
|
187
|
+
payload[:value_type] = value_type
|
188
|
+
payload[:description] = description
|
189
|
+
end
|
190
|
+
|
191
|
+
begin
|
192
|
+
call_api(:post, "/settings", settings: [payload])
|
193
|
+
set_persisted!
|
194
|
+
rescue InvalidRecordError
|
195
|
+
return false
|
196
|
+
end
|
197
|
+
true
|
198
|
+
end
|
199
|
+
|
200
|
+
def history(limit: nil, offset: 0)
|
201
|
+
params = {key: key}
|
202
|
+
params[:offset] = offset if offset > 0
|
203
|
+
params[:limit] = limit if limit
|
204
|
+
history = call_api(:get, "/setting/history", params)
|
205
|
+
history["histories"].collect do |attributes|
|
206
|
+
HistoryItem.new(key: key, value: attributes["value"], changed_by: attributes["changed_by"], created_at: attributes["created_at"], deleted: attributes["deleted"])
|
207
|
+
end
|
208
|
+
end
|
209
|
+
|
210
|
+
def create_history(changed_by:, created_at:, value: nil, deleted: false)
|
211
|
+
# No-op since history is maintained by the source system.
|
212
|
+
end
|
213
|
+
|
214
|
+
def reload
|
215
|
+
self.class.find_by_key(key)
|
216
|
+
self.attributes = self.class.find_by_key(key).attributes
|
217
|
+
self
|
218
|
+
end
|
219
|
+
|
220
|
+
def key=(value)
|
221
|
+
@key = (Coerce.blank?(value) ? nil : value.to_s)
|
222
|
+
end
|
223
|
+
|
224
|
+
def raw_value=(value)
|
225
|
+
@raw_value = (Coerce.blank?(value) ? nil : value.to_s)
|
226
|
+
end
|
227
|
+
alias_method :value=, :raw_value=
|
228
|
+
alias_method :value, :raw_value
|
229
|
+
|
230
|
+
def value_type=(value)
|
231
|
+
@value_type = (Coerce.blank?(value) ? nil : value.to_s)
|
232
|
+
end
|
233
|
+
|
234
|
+
def description=(value)
|
235
|
+
@description = (Coerce.blank?(value) ? nil : value.to_s)
|
236
|
+
end
|
237
|
+
|
238
|
+
def deleted=(value)
|
239
|
+
@deleted = Coerce.boolean(value)
|
240
|
+
end
|
241
|
+
|
242
|
+
def created_at=(value)
|
243
|
+
@created_at = SuperSettings::Coerce.time(value)
|
244
|
+
end
|
245
|
+
|
246
|
+
def updated_at=(value)
|
247
|
+
@updated_at = SuperSettings::Coerce.time(value)
|
248
|
+
end
|
249
|
+
|
250
|
+
def deleted?
|
251
|
+
!!(defined?(@deleted) && @deleted)
|
252
|
+
end
|
253
|
+
|
254
|
+
def persisted?
|
255
|
+
!!(defined?(@persisted) && @persisted)
|
256
|
+
end
|
257
|
+
|
258
|
+
protected
|
259
|
+
|
260
|
+
def redact_history!
|
261
|
+
# No-op since history is maintained by the source system.
|
262
|
+
end
|
263
|
+
|
264
|
+
private
|
265
|
+
|
266
|
+
def set_persisted!
|
267
|
+
@persisted = true
|
268
|
+
end
|
269
|
+
|
270
|
+
def call_api(method, path, params = {})
|
271
|
+
self.class.send(:call_api, method, path, params)
|
272
|
+
end
|
273
|
+
|
274
|
+
def encrypted=(value)
|
275
|
+
# No op; needed for API compatibility
|
276
|
+
end
|
277
|
+
end
|
278
|
+
end
|
279
|
+
end
|
@@ -0,0 +1,293 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "json"
|
4
|
+
|
5
|
+
# Redis implementation of the SuperSettings::Storage model.
|
6
|
+
#
|
7
|
+
# You must define the redis connection to use by setting the redis attribute on the class.
|
8
|
+
# This can either be a `Redis` object or a block that yields a `Redis` object. You can use the
|
9
|
+
# block form if you need to get the `Redis` object at runtime instead of having a static object.
|
10
|
+
#
|
11
|
+
# ```ruby
|
12
|
+
# SuperSettings::Storage::RedisStorage.redis = Redis.new(url: ENV["REDIS_URL"])
|
13
|
+
#
|
14
|
+
# SuperSettings::Storage::RedisStorage.redis = lambda { RedisClient.get(:settings) }
|
15
|
+
# ```
|
16
|
+
#
|
17
|
+
# You can also use the [connection_pool]() gem to provide a pool of Redis connecions for
|
18
|
+
# a multi-threaded application. The connection_pool gem is not a dependency of this gem,
|
19
|
+
# so you would need to add it to your application dependencies to use it.
|
20
|
+
#
|
21
|
+
# ```ruby
|
22
|
+
# SuperSettings::Storage::RedisStorage.redis = ConnectionPool.new(size: 5) { Redis.new(url: ENV["REDIS_URL"]) }
|
23
|
+
# ```
|
24
|
+
module SuperSettings
|
25
|
+
module Storage
|
26
|
+
class RedisStorage
|
27
|
+
include Storage
|
28
|
+
|
29
|
+
SETTINGS_KEY = "SuperSettings.settings"
|
30
|
+
UPDATED_KEY = "SuperSettings.order_by_updated_at"
|
31
|
+
|
32
|
+
class HistoryStorage
|
33
|
+
HISTORY_KEY_PREFIX = "SuperSettings.history"
|
34
|
+
|
35
|
+
include SuperSettings::Attributes
|
36
|
+
|
37
|
+
attr_accessor :key, :value, :changed_by, :deleted
|
38
|
+
attr_reader :created_at
|
39
|
+
|
40
|
+
class << self
|
41
|
+
def find_all_by_key(key:, offset: 0, limit: nil)
|
42
|
+
end_index = (limit.nil? ? -1 : offset + limit - 1)
|
43
|
+
return [] unless end_index >= -1
|
44
|
+
payloads = RedisStorage.with_redis { |redis| redis.lrange("#{HISTORY_KEY_PREFIX}.#{key}", offset, end_index) }
|
45
|
+
payloads.collect do |json|
|
46
|
+
record = new(JSON.parse(json))
|
47
|
+
record.key = key.to_s
|
48
|
+
record
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
def create!(attributes)
|
53
|
+
record = new(attributes)
|
54
|
+
record.save!
|
55
|
+
record
|
56
|
+
end
|
57
|
+
|
58
|
+
def destroy_all_by_key(key)
|
59
|
+
RedisStorage.transaction do |redis|
|
60
|
+
redis.del("#{HISTORY_KEY_PREFIX}.#{key}")
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
def redis_key(key)
|
65
|
+
"#{HISTORY_KEY_PREFIX}.#{key}"
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
def created_at=(val)
|
70
|
+
@created_at = SuperSettings::Coerce.time(val)
|
71
|
+
end
|
72
|
+
|
73
|
+
def save!
|
74
|
+
raise ArgumentError.new("Missing key") if Coerce.blank?(key)
|
75
|
+
RedisStorage.transaction do |redis|
|
76
|
+
redis.lpush(self.class.redis_key(key), payload_json.to_json)
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
def deleted?
|
81
|
+
!!(defined?(@deleted) && @deleted)
|
82
|
+
end
|
83
|
+
|
84
|
+
private
|
85
|
+
|
86
|
+
def payload_json
|
87
|
+
payload = {
|
88
|
+
value: value,
|
89
|
+
changed_by: changed_by,
|
90
|
+
created_at: created_at.to_f
|
91
|
+
}
|
92
|
+
payload[:deleted] = true if deleted?
|
93
|
+
payload
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
attr_reader :key, :raw_value, :description, :value_type, :updated_at, :created_at
|
98
|
+
attr_accessor :changed_by
|
99
|
+
|
100
|
+
class << self
|
101
|
+
def all
|
102
|
+
with_redis do |redis|
|
103
|
+
redis.hgetall(SETTINGS_KEY).values.collect { |json| load_from_json(json) }
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
def updated_since(time)
|
108
|
+
time = SuperSettings::Coerce.time(time)
|
109
|
+
with_redis do |redis|
|
110
|
+
min_score = time.to_f
|
111
|
+
keys = redis.zrangebyscore(UPDATED_KEY, min_score, "+inf")
|
112
|
+
return [] if keys.empty?
|
113
|
+
|
114
|
+
settings = []
|
115
|
+
redis.hmget(SETTINGS_KEY, *keys).each do |json|
|
116
|
+
settings << load_from_json(json) if json
|
117
|
+
end
|
118
|
+
settings
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
def find_by_key(key)
|
123
|
+
json = with_redis { |redis| redis.hget(SETTINGS_KEY, key) }
|
124
|
+
return nil unless json
|
125
|
+
load_from_json(json)
|
126
|
+
end
|
127
|
+
|
128
|
+
def last_updated_at
|
129
|
+
result = with_redis { |redis| redis.zrevrange(UPDATED_KEY, 0, 1, withscores: true).first }
|
130
|
+
return nil unless result
|
131
|
+
Time.at(result[1])
|
132
|
+
end
|
133
|
+
|
134
|
+
def destroy_all
|
135
|
+
all.each(&:destroy)
|
136
|
+
end
|
137
|
+
|
138
|
+
attr_writer :redis
|
139
|
+
|
140
|
+
def with_redis(&block)
|
141
|
+
connection = (@redis.is_a?(Proc) ? @redis.call : @redis)
|
142
|
+
if defined?(ConnectionPool) && connection.is_a?(ConnectionPool)
|
143
|
+
connection.with(&block)
|
144
|
+
else
|
145
|
+
block.call(connection)
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
149
|
+
def transaction(&block)
|
150
|
+
if Thread.current[:super_settings_transaction_redis]
|
151
|
+
block.call(Thread.current[:super_settings_transaction_redis])
|
152
|
+
else
|
153
|
+
begin
|
154
|
+
with_redis do |redis|
|
155
|
+
redis.multi do |multi_redis|
|
156
|
+
Thread.current[:super_settings_transaction_redis] = multi_redis
|
157
|
+
Thread.current[:super_settings_transaction_after_commit] = []
|
158
|
+
block.call(multi_redis)
|
159
|
+
end
|
160
|
+
after_commits = Thread.current[:super_settings_transaction_after_commit]
|
161
|
+
Thread.current[:super_settings_transaction_after_commit] = nil
|
162
|
+
after_commits.each(&:call)
|
163
|
+
end
|
164
|
+
ensure
|
165
|
+
Thread.current[:super_settings_transaction_redis] = nil
|
166
|
+
Thread.current[:super_settings_transaction_after_commit] = nil
|
167
|
+
end
|
168
|
+
end
|
169
|
+
end
|
170
|
+
|
171
|
+
protected
|
172
|
+
|
173
|
+
def default_load_asynchronous?
|
174
|
+
true
|
175
|
+
end
|
176
|
+
|
177
|
+
private
|
178
|
+
|
179
|
+
def load_from_json(json)
|
180
|
+
attributes = JSON.parse(json)
|
181
|
+
setting = new(attributes)
|
182
|
+
setting.send(:set_persisted!)
|
183
|
+
setting
|
184
|
+
end
|
185
|
+
end
|
186
|
+
|
187
|
+
def history(limit: nil, offset: 0)
|
188
|
+
HistoryStorage.find_all_by_key(key: key, limit: limit, offset: offset).collect do |record|
|
189
|
+
HistoryItem.new(key: key, value: record.value, changed_by: record.changed_by, created_at: record.created_at, deleted: record.deleted?)
|
190
|
+
end
|
191
|
+
end
|
192
|
+
|
193
|
+
def create_history(changed_by:, created_at:, value: nil, deleted: false)
|
194
|
+
HistoryStorage.create!(key: key, value: value, deleted: deleted, changed_by: changed_by, created_at: created_at)
|
195
|
+
end
|
196
|
+
|
197
|
+
def save!
|
198
|
+
self.updated_at ||= Time.now
|
199
|
+
self.created_at ||= updated_at
|
200
|
+
self.class.transaction do |redis|
|
201
|
+
redis.hset(SETTINGS_KEY, key, payload_json)
|
202
|
+
redis.zadd(UPDATED_KEY, updated_at.to_f, key)
|
203
|
+
set_persisted!
|
204
|
+
end
|
205
|
+
true
|
206
|
+
end
|
207
|
+
|
208
|
+
def destroy
|
209
|
+
self.class.transaction do |redis|
|
210
|
+
redis.hdel(SETTINGS_KEY, key)
|
211
|
+
redis.zrem(UPDATED_KEY, key)
|
212
|
+
HistoryStorage.destroy_all_by_key(key)
|
213
|
+
end
|
214
|
+
end
|
215
|
+
|
216
|
+
def key=(value)
|
217
|
+
@key = (Coerce.blank?(value) ? nil : value.to_s)
|
218
|
+
end
|
219
|
+
|
220
|
+
def raw_value=(value)
|
221
|
+
@raw_value = (Coerce.blank?(value) ? nil : value.to_s)
|
222
|
+
end
|
223
|
+
|
224
|
+
def value_type=(value)
|
225
|
+
@value_type = (Coerce.blank?(value) ? nil : value.to_s)
|
226
|
+
end
|
227
|
+
|
228
|
+
def description=(value)
|
229
|
+
@description = (Coerce.blank?(value) ? nil : value.to_s)
|
230
|
+
end
|
231
|
+
|
232
|
+
def deleted=(value)
|
233
|
+
@deleted = Coerce.boolean(value)
|
234
|
+
end
|
235
|
+
|
236
|
+
def created_at=(value)
|
237
|
+
@created_at = SuperSettings::Coerce.time(value)
|
238
|
+
end
|
239
|
+
|
240
|
+
def updated_at=(value)
|
241
|
+
@updated_at = SuperSettings::Coerce.time(value)
|
242
|
+
end
|
243
|
+
|
244
|
+
def deleted?
|
245
|
+
!!(defined?(@deleted) && @deleted)
|
246
|
+
end
|
247
|
+
|
248
|
+
def persisted?
|
249
|
+
!!(defined?(@persisted) && @persisted)
|
250
|
+
end
|
251
|
+
|
252
|
+
protected
|
253
|
+
|
254
|
+
def redact_history!
|
255
|
+
after_commit do
|
256
|
+
histories = HistoryStorage.find_all_by_key(key: key)
|
257
|
+
histories.each { |item| item.value = nil }
|
258
|
+
self.class.transaction do
|
259
|
+
HistoryStorage.destroy_all_by_key(key)
|
260
|
+
histories.reverse.each(&:save!)
|
261
|
+
end
|
262
|
+
end
|
263
|
+
end
|
264
|
+
|
265
|
+
private
|
266
|
+
|
267
|
+
def after_commit(&block)
|
268
|
+
if Thread.current[:super_settings_transaction_after_commit]
|
269
|
+
Thread.current[:super_settings_transaction_after_commit] << block
|
270
|
+
else
|
271
|
+
block.call
|
272
|
+
end
|
273
|
+
end
|
274
|
+
|
275
|
+
def set_persisted!
|
276
|
+
@persisted = true
|
277
|
+
end
|
278
|
+
|
279
|
+
def payload_json
|
280
|
+
payload = {
|
281
|
+
key: key,
|
282
|
+
raw_value: raw_value,
|
283
|
+
value_type: value_type,
|
284
|
+
description: description,
|
285
|
+
created_at: created_at.to_f,
|
286
|
+
updated_at: updated_at.to_f
|
287
|
+
}
|
288
|
+
payload[:deleted] = true if deleted?
|
289
|
+
payload.to_json
|
290
|
+
end
|
291
|
+
end
|
292
|
+
end
|
293
|
+
end
|