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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: deffd163ccbba074051f5e7df4138216f24da7c8fa068c5a25fdaa7c83326799
4
- data.tar.gz: af00d68c45251327f0e73867c16bdb4171110929f02290db374178c88718f0ec
3
+ metadata.gz: 6f33b56ee26912045f574c3ec5d6e0bd9ddd283517ebcb7f7ed96173b83cc745
4
+ data.tar.gz: 2a585b3400896dc3f502550a56f37761a588f23cb2504e72fe5e41fbe42bfa25
5
5
  SHA512:
6
- metadata.gz: d496dfbfeaba9952c624f15680323d83fe055a03f48f17c660a5dee8e3512a4692bbe249212bfdf1830026ec0e726cb312acc0134a8c8260e95433b6c325abc8
7
- data.tar.gz: de6887cbbe6ec06ba1cb20e75769377008a5d7fa4eb1a55bcbb6be87e3cdff508890209a0984dc3cdf4ae293d895ce946eb807582097c65e052a9f0b0fcef081
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.3
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 = (payload.encrypted ? "<em>n/a</em>" : escapeHTML(history.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
@@ -34,5 +34,14 @@ module SuperSettings
34
34
  changed_by
35
35
  end
36
36
  end
37
+
38
+ def as_json
39
+ {
40
+ value: value,
41
+ changed_by: changed_by,
42
+ created_at: created_at&.utc&.iso8601(6),
43
+ deleted: deleted?
44
+ }
45
+ end
37
46
  end
38
47
  end
@@ -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 unless updated_at && changed?(:updated_at)
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: Time.now)
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
- changes.each do |setting|
47
- existing[setting.key] = setting
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
- 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
- }
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
- setting.new_history.clear
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
- history_json = JSON.dump(setting_history)
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(setting)
112
- doc = setting.as_bson
113
- history = setting.new_history.collect(&:as_bson)
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: setting.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
- @created_at = SuperSettings::Coerce.time(value)
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
- @updated_at = SuperSettings::Coerce.time(value)
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
- self.updated_at ||= Time.now
57
- self.created_at ||= updated_at
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
- # :nocov:
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.
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: super_settings
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.0.3
4
+ version: 2.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Brian Durand