super_settings 2.0.2 → 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: f22da070d026588b18f51a2fa161ef32fc08d0179a7b10f3251d55f954da9fc4
4
- data.tar.gz: 31fe028eafb566c3e05fe99e08f4b7c8113d5812ac82871e315c71724bae0c7a
3
+ metadata.gz: 6f33b56ee26912045f574c3ec5d6e0bd9ddd283517ebcb7f7ed96173b83cc745
4
+ data.tar.gz: 2a585b3400896dc3f502550a56f37761a588f23cb2504e72fe5e41fbe42bfa25
5
5
  SHA512:
6
- metadata.gz: 1df8b3b2c0b6d99f7f3902c5e8fbc056f83446cfed7c990e9208b6aa18131503ad02f78b8d109b0e2f84e62f1466f77138b27b2391190622dab9a4fb9d948a1e
7
- data.tar.gz: 2c3d3f56c9d7a67a745c012bc1f77c55d3e74012e79a018b9d191bf32d3125062992f4d3fc255b432e11ada92605bcf0ac8df3e8e928f239c432f079546892c4
6
+ metadata.gz: 54cf6302e4c2e760b945e829bfaa8b289f920e7a2d794ddcd711ff4bf4683e9c5ef5eb0b6275f62f2927ecd28b5dcd6f5575809acda291eef3f177a66738a28a
7
+ data.tar.gz: 8019ec60d65105961f9d7bc6cb0db0e1e740a298d02c4e99106cebc34bbe1dff7a0f5b32f89a798bd0d2a9f36dc117fc192bd8468b871f535851b8f2d4b5f077
data/CHANGELOG.md CHANGED
@@ -4,6 +4,22 @@ 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
+
17
+ ## 2.0.3
18
+
19
+ ### Fixed
20
+
21
+ - Fixed ActiveRecord code handling changing a setting key to one that had previously been used. The previous code relied on a unique key constraint error to detect this condition, but Postgres does not handle this well since it invalidates the entire transaction. Now the code checks for the uniqueness of the key before attempting to save the setting.
22
+
7
23
  ## 2.0.2
8
24
 
9
25
  ### Fixed
data/VERSION CHANGED
@@ -1 +1 @@
1
- 2.0.2
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
@@ -89,17 +93,17 @@ module SuperSettings
89
93
  end
90
94
 
91
95
  def save!
92
- begin
96
+ # Check if another record with the same key exists. If it does, then we need to update
97
+ # that record instead and delete the current one.
98
+ duplicate = @model.class.find_by(key: @model.key)
99
+ if duplicate.nil? || duplicate == @model
93
100
  @model.save!
94
- rescue ActiveRecord::RecordNotUnique => e
95
- # Gracefully handle duplicate key constraint on the database; in this case the existing
96
- # record should be updated.
97
- duplicate = @model.class.find_by(key: @model.key)
98
- raise e if duplicate == @model
101
+ else
99
102
  duplicate.raw_value = @model.raw_value
100
103
  duplicate.value_type = @model.value_type
101
104
  duplicate.description = @model.description
102
- duplicate.deleted = false
105
+ duplicate.deleted = @model.deleted
106
+
103
107
  @model.transaction do
104
108
  if @model.persisted?
105
109
  @model.reload.update!(deleted: true)
@@ -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,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: super_settings
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.0.2
4
+ version: 2.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Brian Durand
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2024-11-12 00:00:00.000000000 Z
11
+ date: 2024-11-22 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler