super_settings 2.0.3 → 2.1.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 +10 -0
- data/VERSION +1 -1
- data/lib/super_settings/application/scripts.js +1 -1
- data/lib/super_settings/coerce.rb +2 -0
- data/lib/super_settings/history_item.rb +9 -0
- data/lib/super_settings/rest_api.rb +2 -2
- data/lib/super_settings/setting.rb +13 -6
- data/lib/super_settings/storage/active_record_storage.rb +4 -9
- data/lib/super_settings/storage/http_storage.rb +4 -4
- data/lib/super_settings/storage/json_storage.rb +54 -26
- data/lib/super_settings/storage/mongodb_storage.rb +34 -24
- data/lib/super_settings/storage/redis_storage.rb +4 -4
- data/lib/super_settings/storage/test_storage.rb +20 -7
- data/lib/super_settings/storage/transaction.rb +3 -2
- data/lib/super_settings/storage.rb +10 -3
- metadata +1 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 6f33b56ee26912045f574c3ec5d6e0bd9ddd283517ebcb7f7ed96173b83cc745
|
4
|
+
data.tar.gz: 2a585b3400896dc3f502550a56f37761a588f23cb2504e72fe5e41fbe42bfa25
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 54cf6302e4c2e760b945e829bfaa8b289f920e7a2d794ddcd711ff4bf4683e9c5ef5eb0b6275f62f2927ecd28b5dcd6f5575809acda291eef3f177a66738a28a
|
7
|
+
data.tar.gz: 8019ec60d65105961f9d7bc6cb0db0e1e740a298d02c4e99106cebc34bbe1dff7a0f5b32f89a798bd0d2a9f36dc117fc192bd8468b871f535851b8f2d4b5f077
|
data/CHANGELOG.md
CHANGED
@@ -4,6 +4,16 @@ All notable changes to this project will be documented in this file.
|
|
4
4
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
5
5
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
6
6
|
|
7
|
+
## 2.1.0
|
8
|
+
|
9
|
+
## Fixed
|
10
|
+
|
11
|
+
- More robust handling of history tracking when keys are deleted and then reused. Previously, the history was not fully recorded when a key was reused. Now the history on the old key is recorded as a delete and the history on the new key is recorded as being an update.
|
12
|
+
|
13
|
+
## Changed
|
14
|
+
|
15
|
+
- Times are now consistently encoded in UTC in ISO-8601 format with microseconds whenever they are serialized to JSON.
|
16
|
+
|
7
17
|
## 2.0.3
|
8
18
|
|
9
19
|
### Fixed
|
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
2.0
|
1
|
+
2.1.0
|
@@ -384,7 +384,7 @@
|
|
384
384
|
payload.histories.forEach(function(history) {
|
385
385
|
const date = new Date(Date.parse(history.created_at));
|
386
386
|
const dateString = dateFormatter().format(date);
|
387
|
-
const value = (
|
387
|
+
const value = (history.deleted ? '<em class="super-settings-text-danger">deleted</em>' : escapeHTML(history.value));
|
388
388
|
rowsHTML += `<tr><td class="super-settings-text-nowrap">${escapeHTML(dateString)}</td><td>${escapeHTML(history.changed_by)}</td><td>${value}</td></tr>`;
|
389
389
|
});
|
390
390
|
tbody.insertAdjacentHTML("beforeend", rowsHTML);
|
@@ -36,6 +36,7 @@ module SuperSettings
|
|
36
36
|
def time(value)
|
37
37
|
value = nil if value.nil? || value.to_s.empty?
|
38
38
|
return nil if value.nil?
|
39
|
+
|
39
40
|
time = if value.is_a?(Numeric)
|
40
41
|
Time.at(value)
|
41
42
|
elsif value.respond_to?(:to_time)
|
@@ -52,6 +53,7 @@ module SuperSettings
|
|
52
53
|
# @return [Boolean] true if the value is nil or empty.
|
53
54
|
def blank?(value)
|
54
55
|
return true if value.nil?
|
56
|
+
|
55
57
|
if value.respond_to?(:empty?)
|
56
58
|
value.empty?
|
57
59
|
else
|
@@ -145,7 +145,7 @@ module SuperSettings
|
|
145
145
|
end
|
146
146
|
|
147
147
|
payload[:histories] = histories.collect do |history|
|
148
|
-
history_values = {value: history.value, changed_by: history.changed_by_display, created_at: history.created_at}
|
148
|
+
history_values = {value: history.value, changed_by: history.changed_by_display, created_at: history.created_at.utc.iso8601(6)}
|
149
149
|
history_values[:deleted] = true if history.deleted?
|
150
150
|
history_values
|
151
151
|
end
|
@@ -163,7 +163,7 @@ module SuperSettings
|
|
163
163
|
# last_updated_at: iso8601 string
|
164
164
|
# }
|
165
165
|
def last_updated_at
|
166
|
-
{last_updated_at: Setting.last_updated_at.utc.iso8601}
|
166
|
+
{last_updated_at: Setting.last_updated_at.utc.iso8601(6)}
|
167
167
|
end
|
168
168
|
|
169
169
|
# Return settings that have been updated since a specified timestamp.
|
@@ -446,18 +446,17 @@ module SuperSettings
|
|
446
446
|
#
|
447
447
|
# @return [void]
|
448
448
|
def save!
|
449
|
-
record_value_change
|
450
|
-
|
451
449
|
unless valid?
|
452
450
|
raise InvalidRecordError.new(errors.values.join("; "))
|
453
451
|
end
|
454
452
|
|
455
453
|
timestamp = Time.now
|
456
454
|
self.created_at ||= timestamp
|
457
|
-
self.updated_at = timestamp
|
455
|
+
self.updated_at = timestamp if updated_at.nil? || !changed?(:updated_at)
|
458
456
|
|
459
457
|
self.class.storage.with_connection do
|
460
458
|
self.class.storage.transaction do
|
459
|
+
record_value_change
|
461
460
|
@record.save!
|
462
461
|
end
|
463
462
|
|
@@ -525,8 +524,8 @@ module SuperSettings
|
|
525
524
|
value: value,
|
526
525
|
value_type: value_type,
|
527
526
|
description: description,
|
528
|
-
created_at: created_at,
|
529
|
-
updated_at: updated_at
|
527
|
+
created_at: created_at&.utc&.iso8601(6),
|
528
|
+
updated_at: updated_at&.utc&.iso8601(6)
|
530
529
|
}
|
531
530
|
attributes[:deleted] = true if deleted?
|
532
531
|
attributes
|
@@ -592,8 +591,16 @@ module SuperSettings
|
|
592
591
|
# Update the histories association whenever the value or key is changed.
|
593
592
|
def record_value_change
|
594
593
|
return unless changed?(:raw_value) || changed?(:deleted) || changed?(:key)
|
594
|
+
|
595
595
|
recorded_value = (deleted? ? nil : raw_value)
|
596
|
-
@record.create_history(value: recorded_value, deleted: deleted?, changed_by: changed_by, created_at:
|
596
|
+
@record.class.create_history(key: key, value: recorded_value, deleted: deleted?, changed_by: changed_by, created_at: updated_at)
|
597
|
+
|
598
|
+
if changed?(:key)
|
599
|
+
key_was = @changes["key"][0]
|
600
|
+
if key_was
|
601
|
+
@record.class.create_history(key: key_was, changed_by: changed_by, created_at: updated_at, deleted: true)
|
602
|
+
end
|
603
|
+
end
|
597
604
|
end
|
598
605
|
|
599
606
|
def clear_changes
|
@@ -52,6 +52,10 @@ module SuperSettings
|
|
52
52
|
end
|
53
53
|
end
|
54
54
|
|
55
|
+
def create_history(key:, changed_by:, created_at:, value: nil, deleted: false)
|
56
|
+
HistoryModel.create!(key: key, value: value, deleted: deleted, changed_by: changed_by, created_at: created_at)
|
57
|
+
end
|
58
|
+
|
55
59
|
def with_connection(&block)
|
56
60
|
Model.connection_pool.with_connection(&block)
|
57
61
|
end
|
@@ -117,15 +121,6 @@ module SuperSettings
|
|
117
121
|
HistoryItem.new(key: key, value: record.value, changed_by: record.changed_by, created_at: record.created_at, deleted: record.deleted?)
|
118
122
|
end
|
119
123
|
end
|
120
|
-
|
121
|
-
def create_history(changed_by:, created_at:, value: nil, deleted: false)
|
122
|
-
history_attributes = {value: value, deleted: deleted, changed_by: changed_by, created_at: created_at}
|
123
|
-
if @model.persisted?
|
124
|
-
@model.history_items.create!(history_attributes)
|
125
|
-
else
|
126
|
-
@model.history_items.build(history_attributes)
|
127
|
-
end
|
128
|
-
end
|
129
124
|
end
|
130
125
|
end
|
131
126
|
end
|
@@ -71,6 +71,10 @@ module SuperSettings
|
|
71
71
|
SuperSettings::Coerce.time(value)
|
72
72
|
end
|
73
73
|
|
74
|
+
def create_history(key:, changed_by:, created_at:, value: nil, deleted: false)
|
75
|
+
# No-op since history is maintained by the source system.
|
76
|
+
end
|
77
|
+
|
74
78
|
def save_all(changes)
|
75
79
|
payload = []
|
76
80
|
changes.each do |setting|
|
@@ -132,10 +136,6 @@ module SuperSettings
|
|
132
136
|
end
|
133
137
|
end
|
134
138
|
|
135
|
-
def create_history(changed_by:, created_at:, value: nil, deleted: false)
|
136
|
-
# No-op since history is maintained by the source system.
|
137
|
-
end
|
138
|
-
|
139
139
|
def reload
|
140
140
|
self.class.find_by_key(key)
|
141
141
|
self.attributes = self.class.find_by_key(key).attributes
|
@@ -19,9 +19,32 @@ module SuperSettings
|
|
19
19
|
include Transaction
|
20
20
|
|
21
21
|
class HistoryStorage < HistoryAttributes
|
22
|
+
class << self
|
23
|
+
def create!(attributes)
|
24
|
+
record = new(attributes)
|
25
|
+
record.save!
|
26
|
+
record
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def initialize(*)
|
31
|
+
@storage = nil
|
32
|
+
super
|
33
|
+
end
|
34
|
+
|
35
|
+
attr_writer :storage
|
36
|
+
|
22
37
|
def created_at=(val)
|
23
38
|
super(TimePrecision.new(val).time)
|
24
39
|
end
|
40
|
+
|
41
|
+
def save!
|
42
|
+
raise ArgumentError.new("Missing key") if Coerce.blank?(key)
|
43
|
+
|
44
|
+
@storage.transaction do |changes|
|
45
|
+
changes << self
|
46
|
+
end
|
47
|
+
end
|
25
48
|
end
|
26
49
|
|
27
50
|
class << self
|
@@ -37,36 +60,54 @@ module SuperSettings
|
|
37
60
|
active.detect { |setting| setting.key == key }
|
38
61
|
end
|
39
62
|
|
63
|
+
def create_history(key:, changed_by:, created_at:, value: nil, deleted: false)
|
64
|
+
HistoryStorage.create!(key: key, value: value, changed_by: changed_by, created_at: created_at, deleted: deleted, storage: self)
|
65
|
+
end
|
66
|
+
|
40
67
|
def save_all(changes)
|
41
68
|
existing = {}
|
42
69
|
parse_settings(settings_json_payload).each do |setting|
|
43
70
|
existing[setting.key] = setting
|
44
71
|
end
|
45
72
|
|
46
|
-
|
47
|
-
|
73
|
+
history_items = []
|
74
|
+
changes.each do |record|
|
75
|
+
if record.is_a?(HistoryStorage)
|
76
|
+
history_items << record
|
77
|
+
else
|
78
|
+
existing[record.key] = record
|
79
|
+
end
|
48
80
|
end
|
49
81
|
|
50
82
|
settings = existing.values.sort_by(&:key)
|
83
|
+
|
51
84
|
changed_histories = {}
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
}
|
85
|
+
history_items.each do |history_item|
|
86
|
+
setting = existing[history_item.key]
|
87
|
+
next unless setting
|
88
|
+
|
89
|
+
history = changed_histories[history_item.key]
|
90
|
+
unless history
|
91
|
+
history = setting.history.dup
|
92
|
+
changed_histories[history_item.key] = history
|
61
93
|
end
|
62
|
-
|
94
|
+
history.unshift(history_item)
|
63
95
|
end
|
64
96
|
|
65
97
|
settings_json = JSON.dump(settings.collect(&:as_json))
|
66
98
|
save_settings_json(settings_json)
|
67
99
|
|
68
100
|
changed_histories.each do |setting_key, setting_history|
|
69
|
-
|
101
|
+
ordered_history = setting_history.sort_by { |history_item| history_item.created_at }.reverse
|
102
|
+
payload = ordered_history.collect do |history_item|
|
103
|
+
{
|
104
|
+
value: history_item.value,
|
105
|
+
changed_by: history_item.changed_by,
|
106
|
+
created_at: history_item.created_at.iso8601(6),
|
107
|
+
deleted: history_item.deleted?
|
108
|
+
}
|
109
|
+
end
|
110
|
+
history_json = JSON.dump(payload)
|
70
111
|
save_history_json(setting_key, history_json)
|
71
112
|
end
|
72
113
|
end
|
@@ -128,11 +169,6 @@ module SuperSettings
|
|
128
169
|
end
|
129
170
|
end
|
130
171
|
|
131
|
-
def initialize(*)
|
132
|
-
@new_history = []
|
133
|
-
super
|
134
|
-
end
|
135
|
-
|
136
172
|
def created_at=(val)
|
137
173
|
super(TimePrecision.new(val).time)
|
138
174
|
end
|
@@ -149,14 +185,6 @@ module SuperSettings
|
|
149
185
|
end
|
150
186
|
end
|
151
187
|
|
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
188
|
def as_json
|
161
189
|
{
|
162
190
|
key: key,
|
@@ -26,6 +26,26 @@ module SuperSettings
|
|
26
26
|
@mutex = Mutex.new
|
27
27
|
|
28
28
|
class HistoryStorage < HistoryAttributes
|
29
|
+
class << self
|
30
|
+
def create!(attributes)
|
31
|
+
record = new(attributes)
|
32
|
+
record.save!
|
33
|
+
record
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def created_at=(value)
|
38
|
+
super(TimePrecision.new(value, :millisecond).time)
|
39
|
+
end
|
40
|
+
|
41
|
+
def save!
|
42
|
+
raise ArgumentError.new("Missing key") if Coerce.blank?(key)
|
43
|
+
|
44
|
+
MongoDBStorage.transaction do |changes|
|
45
|
+
changes << self
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
29
49
|
def as_bson
|
30
50
|
attributes = {
|
31
51
|
value: value,
|
@@ -89,13 +109,16 @@ module SuperSettings
|
|
89
109
|
last_updated_setting["updated_at"] if last_updated_setting
|
90
110
|
end
|
91
111
|
|
112
|
+
def create_history(key:, changed_by:, created_at:, value: nil, deleted: false)
|
113
|
+
HistoryStorage.create!(key: key, value: value, changed_by: changed_by, created_at: created_at, deleted: deleted)
|
114
|
+
end
|
115
|
+
|
92
116
|
def destroy_all
|
93
117
|
settings_collection.delete_many({})
|
94
118
|
end
|
95
119
|
|
96
120
|
def save_all(changes)
|
97
121
|
upserts = changes.collect { |setting| upsert(setting) }
|
98
|
-
changes.each { |setting| setting.new_history.clear }
|
99
122
|
settings_collection.bulk_write(upserts)
|
100
123
|
true
|
101
124
|
end
|
@@ -108,17 +131,18 @@ module SuperSettings
|
|
108
131
|
|
109
132
|
private
|
110
133
|
|
111
|
-
def upsert(
|
112
|
-
|
113
|
-
|
134
|
+
def upsert(record)
|
135
|
+
update = {"$setOnInsert": {key: record.key}}
|
136
|
+
if record.is_a?(MongoDBStorage::HistoryStorage)
|
137
|
+
update["$push"] = {history: record.as_bson}
|
138
|
+
else
|
139
|
+
update["$set"] = record.as_bson.except(:key)
|
140
|
+
end
|
141
|
+
|
114
142
|
{
|
115
143
|
update_one: {
|
116
|
-
filter: {key:
|
117
|
-
update:
|
118
|
-
"$set": doc.except(:key, :history),
|
119
|
-
"$setOnInsert": {key: setting.key},
|
120
|
-
"$push": {history: {"$each": history}}
|
121
|
-
},
|
144
|
+
filter: {key: record.key},
|
145
|
+
update: update,
|
122
146
|
upsert: true
|
123
147
|
}
|
124
148
|
}
|
@@ -146,13 +170,6 @@ module SuperSettings
|
|
146
170
|
end
|
147
171
|
end
|
148
172
|
|
149
|
-
attr_reader :new_history
|
150
|
-
|
151
|
-
def initialize(*)
|
152
|
-
@new_history = []
|
153
|
-
super
|
154
|
-
end
|
155
|
-
|
156
173
|
def history(limit: nil, offset: 0)
|
157
174
|
pipeline = [
|
158
175
|
{
|
@@ -195,13 +212,6 @@ module SuperSettings
|
|
195
212
|
end
|
196
213
|
end
|
197
214
|
|
198
|
-
def create_history(changed_by:, created_at:, value: nil, deleted: false)
|
199
|
-
created_at = TimePrecision.new(created_at, :millisecond).time
|
200
|
-
history = HistoryStorage.new(key: key, value: value, changed_by: changed_by, created_at: created_at, deleted: deleted)
|
201
|
-
@new_history.unshift(history)
|
202
|
-
history
|
203
|
-
end
|
204
|
-
|
205
215
|
def created_at=(val)
|
206
216
|
super(TimePrecision.new(val, :millisecond).time)
|
207
217
|
end
|
@@ -116,6 +116,10 @@ module SuperSettings
|
|
116
116
|
record unless record.deleted?
|
117
117
|
end
|
118
118
|
|
119
|
+
def create_history(key:, changed_by:, created_at:, value: nil, deleted: false)
|
120
|
+
HistoryStorage.create!(key: key, value: value, deleted: deleted, changed_by: changed_by, created_at: created_at)
|
121
|
+
end
|
122
|
+
|
119
123
|
def last_updated_at
|
120
124
|
result = with_redis { |redis| redis.zrevrange(UPDATED_KEY, 0, 1, withscores: true).first }
|
121
125
|
return nil unless result
|
@@ -184,10 +188,6 @@ module SuperSettings
|
|
184
188
|
end
|
185
189
|
end
|
186
190
|
|
187
|
-
def create_history(changed_by:, created_at:, value: nil, deleted: false)
|
188
|
-
HistoryStorage.create!(key: key, value: value, deleted: deleted, changed_by: changed_by, created_at: created_at)
|
189
|
-
end
|
190
|
-
|
191
191
|
def save_to_redis(redis)
|
192
192
|
redis.hset(SETTINGS_KEY, key, payload_json)
|
193
193
|
redis.zadd(UPDATED_KEY, self.class.microseconds(updated_at), key)
|
@@ -59,6 +59,20 @@ module SuperSettings
|
|
59
59
|
settings.values.collect { |attributes| attributes[:updated_at] }.max
|
60
60
|
end
|
61
61
|
|
62
|
+
def create_history(key:, changed_by:, created_at:, value: nil, deleted: false)
|
63
|
+
history = @history[key]
|
64
|
+
unless history
|
65
|
+
history = []
|
66
|
+
@history[key] = history
|
67
|
+
end
|
68
|
+
|
69
|
+
created_at = SuperSettings::TimePrecision.new(created_at).time if created_at
|
70
|
+
item = {key: key, value: value, deleted: deleted, changed_by: changed_by, created_at: created_at}
|
71
|
+
history.unshift(item)
|
72
|
+
|
73
|
+
item
|
74
|
+
end
|
75
|
+
|
62
76
|
protected
|
63
77
|
|
64
78
|
def default_load_asynchronous?
|
@@ -85,11 +99,6 @@ module SuperSettings
|
|
85
99
|
end
|
86
100
|
end
|
87
101
|
|
88
|
-
def create_history(changed_by:, created_at:, value: nil, deleted: false)
|
89
|
-
item = {key: key, value: value, deleted: deleted, changed_by: changed_by, created_at: created_at}
|
90
|
-
self.class.history(key).unshift(item)
|
91
|
-
end
|
92
|
-
|
93
102
|
def save!
|
94
103
|
self.updated_at ||= Time.now
|
95
104
|
self.created_at ||= updated_at
|
@@ -123,11 +132,15 @@ module SuperSettings
|
|
123
132
|
end
|
124
133
|
|
125
134
|
def created_at=(value)
|
126
|
-
|
135
|
+
time = SuperSettings::Coerce.time(value)
|
136
|
+
time = SuperSettings::TimePrecision.new(time).time if time
|
137
|
+
@created_at = time
|
127
138
|
end
|
128
139
|
|
129
140
|
def updated_at=(value)
|
130
|
-
|
141
|
+
time = SuperSettings::Coerce.time(value)
|
142
|
+
time = SuperSettings::TimePrecision.new(time).time if time
|
143
|
+
@updated_at = time
|
131
144
|
end
|
132
145
|
|
133
146
|
def deleted?
|
@@ -53,8 +53,9 @@ module SuperSettings
|
|
53
53
|
end
|
54
54
|
|
55
55
|
def save!
|
56
|
-
|
57
|
-
self.
|
56
|
+
timestamp = Time.now
|
57
|
+
self.updated_at ||= timestamp if respond_to?(:updated_at)
|
58
|
+
self.created_at ||= (respond_to?(:updated_at) ? updated_at : timestamp)
|
58
59
|
|
59
60
|
self.class.transaction do |changes|
|
60
61
|
changes << self
|
@@ -67,6 +67,15 @@ module SuperSettings
|
|
67
67
|
# :nocov:
|
68
68
|
end
|
69
69
|
|
70
|
+
# Create a history item for the setting.
|
71
|
+
#
|
72
|
+
# @return [void]
|
73
|
+
def create_history(key:, changed_by:, created_at:, value: nil, deleted: false)
|
74
|
+
# :nocov:
|
75
|
+
raise NotImplementedError
|
76
|
+
# :nocov:
|
77
|
+
end
|
78
|
+
|
70
79
|
# Implementing classes can override this method to setup a thread safe connection within a block.
|
71
80
|
#
|
72
81
|
# @return [void]
|
@@ -253,9 +262,7 @@ module SuperSettings
|
|
253
262
|
#
|
254
263
|
# @return [void]
|
255
264
|
def create_history(changed_by:, created_at:, value: nil, deleted: false)
|
256
|
-
|
257
|
-
raise NotImplementedError
|
258
|
-
# :nocov:
|
265
|
+
self.class.create_history(key: key, changed_by: changed_by, created_at: created_at, value: value, deleted: deleted)
|
259
266
|
end
|
260
267
|
|
261
268
|
# Persist the record to storage.
|