super_settings 1.0.2 → 2.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +22 -0
- data/README.md +110 -16
- data/VERSION +1 -1
- data/app/helpers/super_settings/settings_helper.rb +13 -3
- data/app/views/layouts/super_settings/settings.html.erb +1 -1
- data/config/routes.rb +1 -1
- data/db/migrate/20210414004553_create_super_settings.rb +1 -7
- data/lib/super_settings/application/api.js +4 -1
- data/lib/super_settings/application/helper.rb +56 -17
- data/lib/super_settings/application/images/arrow-down-short.svg +3 -0
- data/lib/super_settings/application/images/arrow-up-short.svg +3 -0
- data/lib/super_settings/application/images/info-circle.svg +4 -0
- data/lib/super_settings/application/images/pencil-square.svg +4 -0
- data/lib/super_settings/application/images/plus.svg +3 -1
- data/lib/super_settings/application/images/trash3.svg +3 -0
- data/lib/super_settings/application/images/x-circle.svg +4 -0
- data/lib/super_settings/application/index.html.erb +54 -37
- data/lib/super_settings/application/layout.html.erb +5 -2
- data/lib/super_settings/application/layout_styles.css +7 -151
- data/lib/super_settings/application/layout_vars.css.erb +21 -0
- data/lib/super_settings/application/scripts.js +100 -21
- data/lib/super_settings/application/style_vars.css.erb +62 -0
- data/lib/super_settings/application/styles.css +183 -14
- data/lib/super_settings/application.rb +18 -11
- data/lib/super_settings/attributes.rb +1 -8
- data/lib/super_settings/configuration.rb +9 -0
- data/lib/super_settings/controller_actions.rb +2 -2
- data/lib/super_settings/engine.rb +1 -1
- data/lib/super_settings/history_item.rb +1 -1
- data/lib/super_settings/http_client.rb +165 -0
- data/lib/super_settings/rack_application.rb +3 -3
- data/lib/super_settings/rest_api.rb +5 -4
- data/lib/super_settings/setting.rb +13 -2
- data/lib/super_settings/storage/active_record_storage.rb +7 -0
- data/lib/super_settings/storage/history_attributes.rb +31 -0
- data/lib/super_settings/storage/http_storage.rb +60 -184
- data/lib/super_settings/storage/json_storage.rb +201 -0
- data/lib/super_settings/storage/mongodb_storage.rb +238 -0
- data/lib/super_settings/storage/redis_storage.rb +49 -111
- data/lib/super_settings/storage/s3_storage.rb +165 -0
- data/lib/super_settings/storage/storage_attributes.rb +64 -0
- data/lib/super_settings/storage/test_storage.rb +3 -5
- data/lib/super_settings/storage/transaction.rb +67 -0
- data/lib/super_settings/storage.rb +13 -6
- data/lib/super_settings/time_precision.rb +36 -0
- data/lib/super_settings.rb +11 -0
- data/super_settings.gemspec +4 -2
- metadata +22 -9
- data/lib/super_settings/application/images/edit.svg +0 -1
- data/lib/super_settings/application/images/info.svg +0 -1
- data/lib/super_settings/application/images/slash.svg +0 -1
- data/lib/super_settings/application/images/trash.svg +0 -1
@@ -109,6 +109,7 @@ module SuperSettings
|
|
109
109
|
# @param time [Time]
|
110
110
|
# @return [Array<Setting>]
|
111
111
|
def updated_since(time)
|
112
|
+
time = SuperSettings::Coerce.time(time)
|
112
113
|
storage.with_connection do
|
113
114
|
storage.updated_since(time).collect { |record| new(record) }
|
114
115
|
end
|
@@ -219,6 +220,16 @@ module SuperSettings
|
|
219
220
|
next if Coerce.blank?(setting_params["key"])
|
220
221
|
next if ["value_type", "value", "description", "deleted"].all? { |name| Coerce.blank?(setting_params[name]) }
|
221
222
|
|
223
|
+
key_was = setting_params["key_was"]
|
224
|
+
if key_was && !changed.include?(key_was)
|
225
|
+
old_setting = Setting.find_by_key(key_was)
|
226
|
+
if old_setting
|
227
|
+
old_setting.deleted = true
|
228
|
+
old_setting.changed_by = changed_by
|
229
|
+
changed[old_setting.key] = old_setting
|
230
|
+
end
|
231
|
+
end
|
232
|
+
|
222
233
|
key = setting_params["key"]
|
223
234
|
setting = changed[key] || Setting.find_by_key(key)
|
224
235
|
unless setting
|
@@ -372,7 +383,7 @@ module SuperSettings
|
|
372
383
|
#
|
373
384
|
# @param val [Time, DateTime]
|
374
385
|
def created_at=(val)
|
375
|
-
val =
|
386
|
+
val = TimePrecision.new(val).time
|
376
387
|
will_change!(:created_at, val) unless created_at == val
|
377
388
|
@record.created_at = val
|
378
389
|
end
|
@@ -388,7 +399,7 @@ module SuperSettings
|
|
388
399
|
#
|
389
400
|
# @param val [Time, DateTime]
|
390
401
|
def updated_at=(val)
|
391
|
-
val =
|
402
|
+
val = TimePrecision.new(val).time
|
392
403
|
will_change!(:updated_at, val) unless updated_at == val
|
393
404
|
@record.updated_at = val
|
394
405
|
end
|
@@ -60,6 +60,13 @@ module SuperSettings
|
|
60
60
|
Model.transaction(&block)
|
61
61
|
end
|
62
62
|
|
63
|
+
def destroy_all
|
64
|
+
ApplicationRecord.transaction do
|
65
|
+
Model.delete_all
|
66
|
+
HistoryModel.delete_all
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
63
70
|
protected
|
64
71
|
|
65
72
|
# Only load settings asynchronously if there is an extra database connection left in the
|
@@ -0,0 +1,31 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SuperSettings
|
4
|
+
module Storage
|
5
|
+
# Generic class that can be extended to represent a history record for a setting in memory.
|
6
|
+
class HistoryAttributes
|
7
|
+
include SuperSettings::Attributes
|
8
|
+
|
9
|
+
attr_accessor :key, :value, :changed_by
|
10
|
+
attr_writer :deleted
|
11
|
+
attr_reader :created_at
|
12
|
+
|
13
|
+
def initialize(*)
|
14
|
+
@key = nil
|
15
|
+
@value = nil
|
16
|
+
@changed_by = nil
|
17
|
+
@created_at = nil
|
18
|
+
@deleted = false
|
19
|
+
super
|
20
|
+
end
|
21
|
+
|
22
|
+
def created_at=(val)
|
23
|
+
@created_at = TimePrecision.new(val).time
|
24
|
+
end
|
25
|
+
|
26
|
+
def deleted?
|
27
|
+
!!@deleted
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -1,59 +1,51 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require "json"
|
4
|
-
require "net/http"
|
5
|
-
|
6
3
|
module SuperSettings
|
7
4
|
module Storage
|
8
5
|
# SuperSettings::Storage model that reads from a remote service running the SuperSettings REST API.
|
9
6
|
# This storage engine is read only. It is intended to allow microservices to read settings from a
|
10
7
|
# central application that exposes the SuperSettings::RestAPI.
|
11
|
-
|
8
|
+
#
|
9
|
+
# You must the the base_url class attribute to the base URL of a SuperSettings REST API endpoint.
|
10
|
+
# You can also set the timeout, headers, and query_params used in reqeusts to the API.
|
11
|
+
class HttpStorage < StorageAttributes
|
12
12
|
include Storage
|
13
|
+
include Transaction
|
13
14
|
|
14
15
|
DEFAULT_HEADERS = {"Accept" => "application/json"}.freeze
|
15
16
|
DEFAULT_TIMEOUT = 5.0
|
16
17
|
|
18
|
+
@base_url = nil
|
19
|
+
@timeout = nil
|
17
20
|
@headers = {}
|
18
21
|
@query_params = {}
|
22
|
+
@http_client = nil
|
23
|
+
@http_client_hash = nil
|
19
24
|
|
20
|
-
|
21
|
-
|
22
|
-
class Error < StandardError
|
23
|
-
end
|
24
|
-
|
25
|
-
class NotFoundError < Error
|
25
|
+
class HistoryStorage < HistoryAttributes
|
26
26
|
end
|
27
27
|
|
28
|
-
class
|
29
|
-
|
30
|
-
|
31
|
-
def initialize(message, errors:)
|
32
|
-
super(message)
|
33
|
-
@errors = errors
|
34
|
-
end
|
35
|
-
end
|
36
|
-
|
37
|
-
class HistoryStorage
|
38
|
-
include SuperSettings::Attributes
|
39
|
-
|
40
|
-
attr_accessor :key, :value, :changed_by, :deleted
|
28
|
+
class << self
|
29
|
+
# Set the base URL for the SuperSettings REST API.
|
30
|
+
attr_accessor :base_url
|
41
31
|
|
42
|
-
|
43
|
-
|
44
|
-
super
|
45
|
-
end
|
32
|
+
# Set the timeout for requests to the SuperSettings REST API.
|
33
|
+
attr_accessor :timeout
|
46
34
|
|
47
|
-
|
48
|
-
|
49
|
-
|
35
|
+
# Add headers to this hash to add them to all requests to the SuperSettings REST API.
|
36
|
+
#
|
37
|
+
# @example
|
38
|
+
#
|
39
|
+
# SuperSettings::HttpStorage.headers["Authorization"] = "Bearer 12345"
|
40
|
+
attr_reader :headers
|
50
41
|
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
42
|
+
# Add query parameters to this hash to add them to all requests to the SuperSettings REST API.
|
43
|
+
#
|
44
|
+
# @example
|
45
|
+
#
|
46
|
+
# SuperSettings::HttpStorage.query_params["access_token"] = "12345"
|
47
|
+
attr_reader :query_params
|
55
48
|
|
56
|
-
class << self
|
57
49
|
def all
|
58
50
|
call_api(:get, "/settings")["settings"].collect do |attributes|
|
59
51
|
new(attributes)
|
@@ -68,9 +60,9 @@ module SuperSettings
|
|
68
60
|
|
69
61
|
def find_by_key(key)
|
70
62
|
record = new(call_api(:get, "/setting", key: key))
|
71
|
-
record.
|
63
|
+
record.persisted = true
|
72
64
|
record
|
73
|
-
rescue NotFoundError
|
65
|
+
rescue HttpClient::NotFoundError
|
74
66
|
nil
|
75
67
|
end
|
76
68
|
|
@@ -79,13 +71,30 @@ module SuperSettings
|
|
79
71
|
SuperSettings::Coerce.time(value)
|
80
72
|
end
|
81
73
|
|
82
|
-
|
74
|
+
def save_all(changes)
|
75
|
+
payload = []
|
76
|
+
changes.each do |setting|
|
77
|
+
setting_payload = {key: setting.key}
|
83
78
|
|
84
|
-
|
79
|
+
if setting.deleted?
|
80
|
+
setting_payload[:deleted] = true
|
81
|
+
else
|
82
|
+
setting_payload[:value] = setting.value
|
83
|
+
setting_payload[:value_type] = setting.value_type
|
84
|
+
setting_payload[:description] = setting.description
|
85
|
+
end
|
85
86
|
|
86
|
-
|
87
|
+
payload << setting_payload
|
88
|
+
end
|
87
89
|
|
88
|
-
|
90
|
+
begin
|
91
|
+
call_api(:post, "/settings", settings: payload)
|
92
|
+
rescue HttpClient::InvalidRecordError
|
93
|
+
return false
|
94
|
+
end
|
95
|
+
|
96
|
+
true
|
97
|
+
end
|
89
98
|
|
90
99
|
protected
|
91
100
|
|
@@ -96,115 +105,21 @@ module SuperSettings
|
|
96
105
|
private
|
97
106
|
|
98
107
|
def call_api(method, path, params = {})
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
request_headers = DEFAULT_HEADERS.merge(headers)
|
104
|
-
if method == :post && !params&.empty?
|
105
|
-
body = params.to_json
|
106
|
-
request_headers["content-type"] = "application/json; charset=utf8-"
|
107
|
-
end
|
108
|
-
|
109
|
-
response = http_request(method: method, uri: uri, headers: request_headers, body: body)
|
110
|
-
|
111
|
-
begin
|
112
|
-
response.value # raises exception unless response is a success
|
113
|
-
JSON.parse(response.body)
|
114
|
-
rescue Net::ProtocolError
|
115
|
-
if [404, 410].include?(response.code.to_i)
|
116
|
-
raise NotFoundError.new("#{response.code} #{response.message}")
|
117
|
-
elsif response.code.to_i == 422
|
118
|
-
raise InvalidRecordError.new("#{response.code} #{response.message}", errors: JSON.parse(response.body)["errors"])
|
119
|
-
else
|
120
|
-
raise Error.new("#{response.code} #{response.message}")
|
121
|
-
end
|
122
|
-
rescue JSON::JSONError => e
|
123
|
-
raise Error.new(e.message)
|
108
|
+
if method == :post
|
109
|
+
http_client.post(path, params)
|
110
|
+
else
|
111
|
+
http_client.get(path, params)
|
124
112
|
end
|
125
113
|
end
|
126
114
|
|
127
|
-
def
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
http.open_timeout = (timeout || DEFAULT_TIMEOUT)
|
133
|
-
if uri.scheme == "https"
|
134
|
-
http.use_ssl = true
|
135
|
-
http.verify_mode = OpenSSL::SSL::VERIFY_NONE
|
136
|
-
end
|
137
|
-
|
138
|
-
request = ((method == :post) ? Net::HTTP::Post.new(uri.request_uri) : Net::HTTP::Get.new(uri.request_uri))
|
139
|
-
set_headers(request, headers)
|
140
|
-
request.body = body if body
|
141
|
-
|
142
|
-
response = http.request(request)
|
143
|
-
ensure
|
144
|
-
begin
|
145
|
-
http.finish if http.started?
|
146
|
-
rescue IOError
|
147
|
-
end
|
148
|
-
end
|
149
|
-
|
150
|
-
if response.is_a?(Net::HTTPRedirection)
|
151
|
-
location = resp["location"]
|
152
|
-
if redirect_count < 5 && SuperSettings::Coerce.present?(location)
|
153
|
-
return http_request(method: :get, uri: URI(location), headers: headers, body: body, redirect_count: redirect_count + 1)
|
154
|
-
end
|
155
|
-
end
|
156
|
-
|
157
|
-
response
|
158
|
-
end
|
159
|
-
|
160
|
-
def api_uri(path, params)
|
161
|
-
uri = URI("#{base_url.chomp("/")}#{path}")
|
162
|
-
if params && !params.empty?
|
163
|
-
q = []
|
164
|
-
q << uri.query unless uri.query.to_s.empty?
|
165
|
-
params.each do |name, value|
|
166
|
-
q << "#{URI.encode_www_form_component(name.to_s)}=#{URI.encode_www_form_component(value.to_s)}"
|
167
|
-
end
|
168
|
-
uri.query = q.join("&")
|
115
|
+
def http_client
|
116
|
+
hash = [base_url, timeout, headers, query_params].hash
|
117
|
+
if @http_client.nil? || @http_client_hash != hash
|
118
|
+
@http_client = HttpClient.new(base_url, headers: headers, params: query_params, timeout: timeout)
|
119
|
+
@http_client_hash = hash
|
169
120
|
end
|
170
|
-
|
171
|
-
end
|
172
|
-
|
173
|
-
def set_headers(request, headers)
|
174
|
-
headers.each do |name, value|
|
175
|
-
name = name.to_s
|
176
|
-
values = Array(value)
|
177
|
-
request[name] = values[0].to_s
|
178
|
-
values[1, values.length].each do |val|
|
179
|
-
request.add_field(name, val.to_s)
|
180
|
-
end
|
181
|
-
end
|
182
|
-
end
|
183
|
-
end
|
184
|
-
|
185
|
-
def initialize(*)
|
186
|
-
@persisted = false
|
187
|
-
@deleted = false
|
188
|
-
super
|
189
|
-
end
|
190
|
-
|
191
|
-
def save!
|
192
|
-
payload = {key: key}
|
193
|
-
if deleted?
|
194
|
-
payload[:deleted] = true
|
195
|
-
else
|
196
|
-
payload[:value] = value
|
197
|
-
payload[:value_type] = value_type
|
198
|
-
payload[:description] = description
|
199
|
-
end
|
200
|
-
|
201
|
-
begin
|
202
|
-
call_api(:post, "/settings", settings: [payload])
|
203
|
-
set_persisted!
|
204
|
-
rescue InvalidRecordError
|
205
|
-
return false
|
121
|
+
@http_client
|
206
122
|
end
|
207
|
-
true
|
208
123
|
end
|
209
124
|
|
210
125
|
def history(limit: nil, offset: 0)
|
@@ -227,50 +142,11 @@ module SuperSettings
|
|
227
142
|
self
|
228
143
|
end
|
229
144
|
|
230
|
-
def key=(value)
|
231
|
-
@key = (Coerce.blank?(value) ? nil : value.to_s)
|
232
|
-
end
|
233
|
-
|
234
|
-
def raw_value=(value)
|
235
|
-
@raw_value = (Coerce.blank?(value) ? nil : value.to_s)
|
236
|
-
end
|
237
145
|
alias_method :value=, :raw_value=
|
238
146
|
alias_method :value, :raw_value
|
239
147
|
|
240
|
-
def value_type=(value)
|
241
|
-
@value_type = (Coerce.blank?(value) ? nil : value.to_s)
|
242
|
-
end
|
243
|
-
|
244
|
-
def description=(value)
|
245
|
-
@description = (Coerce.blank?(value) ? nil : value.to_s)
|
246
|
-
end
|
247
|
-
|
248
|
-
def deleted=(value)
|
249
|
-
@deleted = Coerce.boolean(value)
|
250
|
-
end
|
251
|
-
|
252
|
-
def created_at=(value)
|
253
|
-
@created_at = SuperSettings::Coerce.time(value)
|
254
|
-
end
|
255
|
-
|
256
|
-
def updated_at=(value)
|
257
|
-
@updated_at = SuperSettings::Coerce.time(value)
|
258
|
-
end
|
259
|
-
|
260
|
-
def deleted?
|
261
|
-
!!@deleted
|
262
|
-
end
|
263
|
-
|
264
|
-
def persisted?
|
265
|
-
!!@persisted
|
266
|
-
end
|
267
|
-
|
268
148
|
private
|
269
149
|
|
270
|
-
def set_persisted!
|
271
|
-
@persisted = true
|
272
|
-
end
|
273
|
-
|
274
150
|
def call_api(method, path, params = {})
|
275
151
|
self.class.send(:call_api, method, path, params)
|
276
152
|
end
|
@@ -0,0 +1,201 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SuperSettings
|
4
|
+
module Storage
|
5
|
+
# This is an abstract storage class that provides support for storing settings in a JSON file.
|
6
|
+
# The settings are stored in JSON as an array of hashes with each hash representing a setting.
|
7
|
+
#
|
8
|
+
# Setting history should be stored in separate JSON files per key and are loaded separately
|
9
|
+
# from the main settings file.
|
10
|
+
#
|
11
|
+
# This class can be used as the base for any storage class where the settings are all stored
|
12
|
+
# together in a single JSON payload.
|
13
|
+
#
|
14
|
+
# Subclasses must implement the following methods:
|
15
|
+
# - self.all
|
16
|
+
# - self.last_updated_at
|
17
|
+
# - save!
|
18
|
+
class JSONStorage < StorageAttributes
|
19
|
+
include Transaction
|
20
|
+
|
21
|
+
class HistoryStorage < HistoryAttributes
|
22
|
+
def created_at=(val)
|
23
|
+
super(TimePrecision.new(val).time)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
class << self
|
28
|
+
def all
|
29
|
+
parse_settings(settings_json_payload)
|
30
|
+
end
|
31
|
+
|
32
|
+
def updated_since(timestamp)
|
33
|
+
all.select { |setting| setting.updated_at > timestamp }
|
34
|
+
end
|
35
|
+
|
36
|
+
def find_by_key(key)
|
37
|
+
active.detect { |setting| setting.key == key }
|
38
|
+
end
|
39
|
+
|
40
|
+
def save_all(changes)
|
41
|
+
existing = {}
|
42
|
+
parse_settings(settings_json_payload).each do |setting|
|
43
|
+
existing[setting.key] = setting
|
44
|
+
end
|
45
|
+
|
46
|
+
changes.each do |setting|
|
47
|
+
existing[setting.key] = setting
|
48
|
+
end
|
49
|
+
|
50
|
+
settings = existing.values.sort_by(&:key)
|
51
|
+
changed_histories = {}
|
52
|
+
changes.collect do |setting|
|
53
|
+
history = (setting.new_history + setting.history).sort_by(&:created_at).reverse
|
54
|
+
changed_histories[setting.key] = history.collect do |history_item|
|
55
|
+
{
|
56
|
+
value: history_item.value,
|
57
|
+
changed_by: history_item.changed_by,
|
58
|
+
created_at: history_item.created_at&.iso8601(6),
|
59
|
+
deleted: history_item.deleted?
|
60
|
+
}
|
61
|
+
end
|
62
|
+
setting.new_history.clear
|
63
|
+
end
|
64
|
+
|
65
|
+
settings_json = JSON.dump(settings.collect(&:as_json))
|
66
|
+
save_settings_json(settings_json)
|
67
|
+
|
68
|
+
changed_histories.each do |setting_key, setting_history|
|
69
|
+
history_json = JSON.dump(setting_history)
|
70
|
+
save_history_json(setting_key, history_json)
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
# Heper method to load settings from a JSON string.
|
75
|
+
#
|
76
|
+
# @param json [String] JSON string to parse.
|
77
|
+
# @return [Array<SuperSettings::Storage::JSONStorage>] Array of settings.
|
78
|
+
def parse_settings(json)
|
79
|
+
return [] if Coerce.blank?(json)
|
80
|
+
|
81
|
+
JSON.parse(json).collect do |attributes|
|
82
|
+
setting = new(
|
83
|
+
key: attributes["key"],
|
84
|
+
raw_value: attributes["value"],
|
85
|
+
description: attributes["description"],
|
86
|
+
value_type: attributes["value_type"],
|
87
|
+
updated_at: Time.parse(attributes["updated_at"]),
|
88
|
+
created_at: Time.parse(attributes["created_at"]),
|
89
|
+
deleted: attributes["deleted"]
|
90
|
+
)
|
91
|
+
setting.persisted = true
|
92
|
+
setting
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
protected
|
97
|
+
|
98
|
+
# Subclasses must implement this method to return the JSON payload containing all of the
|
99
|
+
# settings as a string.
|
100
|
+
#
|
101
|
+
# @return [String] JSON string.
|
102
|
+
def settings_json_payload
|
103
|
+
# :nocov:
|
104
|
+
raise NotImplementedError
|
105
|
+
# :nocov:
|
106
|
+
end
|
107
|
+
|
108
|
+
# Subclasses must implement this method to persist the JSON payload containing all of the
|
109
|
+
# settings.
|
110
|
+
#
|
111
|
+
# @param json [String] JSON string to save.
|
112
|
+
def save_settings_json(json)
|
113
|
+
# :nocov:
|
114
|
+
raise NotImplementedError
|
115
|
+
# :nocov:
|
116
|
+
end
|
117
|
+
|
118
|
+
# Subclasses must implement this method to persist the JSON payload containing the history
|
119
|
+
# records for a setting key.
|
120
|
+
#
|
121
|
+
# @param key [String] Setting key.
|
122
|
+
# @param json [String] JSON string to save.
|
123
|
+
# @return [void]
|
124
|
+
def save_history_json(key, json)
|
125
|
+
# :nocov:
|
126
|
+
raise NotImplementedError
|
127
|
+
# :nocov:
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
def initialize(*)
|
132
|
+
@new_history = []
|
133
|
+
super
|
134
|
+
end
|
135
|
+
|
136
|
+
def created_at=(val)
|
137
|
+
super(TimePrecision.new(val).time)
|
138
|
+
end
|
139
|
+
|
140
|
+
def updated_at=(val)
|
141
|
+
super(TimePrecision.new(val).time)
|
142
|
+
end
|
143
|
+
|
144
|
+
def history(limit: nil, offset: 0)
|
145
|
+
history = fetch_history
|
146
|
+
limit ||= history.length
|
147
|
+
history[offset, limit].collect do |record|
|
148
|
+
HistoryItem.new(key: key, value: record.value, changed_by: record.changed_by, created_at: record.created_at, deleted: record.deleted?)
|
149
|
+
end
|
150
|
+
end
|
151
|
+
|
152
|
+
def create_history(changed_by:, created_at:, value: nil, deleted: false)
|
153
|
+
history = HistoryStorage.new(key: key, value: value, changed_by: changed_by, created_at: created_at, deleted: deleted)
|
154
|
+
@new_history.unshift(history)
|
155
|
+
history
|
156
|
+
end
|
157
|
+
|
158
|
+
attr_reader :new_history
|
159
|
+
|
160
|
+
def as_json
|
161
|
+
{
|
162
|
+
key: key,
|
163
|
+
value: raw_value,
|
164
|
+
value_type: value_type,
|
165
|
+
description: description,
|
166
|
+
created_at: created_at&.iso8601(6),
|
167
|
+
updated_at: updated_at&.iso8601(6),
|
168
|
+
deleted: deleted?
|
169
|
+
}
|
170
|
+
end
|
171
|
+
|
172
|
+
protected
|
173
|
+
|
174
|
+
# Subclasses must implement this method to return the JSON payload containing all of the
|
175
|
+
# history records for the setting key. The payload must be an array that contains hashes
|
176
|
+
# with the keys "value", "changed_by", "deleted", and "created_at".
|
177
|
+
def fetch_history_json
|
178
|
+
raise NotImplementedError
|
179
|
+
end
|
180
|
+
|
181
|
+
private
|
182
|
+
|
183
|
+
def fetch_history
|
184
|
+
json = fetch_history_json
|
185
|
+
history_payload = Coerce.blank?(json) ? [] : JSON.parse(json)
|
186
|
+
|
187
|
+
history_items = history_payload.collect do |attributes|
|
188
|
+
HistoryStorage.new(
|
189
|
+
key: key,
|
190
|
+
value: attributes["value"],
|
191
|
+
changed_by: attributes["changed_by"],
|
192
|
+
created_at: Time.parse(attributes["created_at"]),
|
193
|
+
deleted: attributes["deleted"]
|
194
|
+
)
|
195
|
+
end
|
196
|
+
|
197
|
+
history_items.sort_by(&:created_at).reverse
|
198
|
+
end
|
199
|
+
end
|
200
|
+
end
|
201
|
+
end
|