super_settings 1.0.1 → 2.0.0
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 +4 -4
- data/CHANGELOG.md +34 -2
- data/README.md +121 -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/context/current.rb +33 -0
- data/lib/super_settings/context.rb +3 -0
- data/lib/super_settings/controller_actions.rb +2 -2
- data/lib/super_settings/engine.rb +1 -3
- data/lib/super_settings/history_item.rb +1 -1
- data/lib/super_settings/http_client.rb +165 -0
- data/lib/super_settings/local_cache.rb +0 -15
- data/lib/super_settings/rack_application.rb +3 -3
- data/lib/super_settings/rest_api.rb +5 -4
- data/lib/super_settings/setting.rb +14 -3
- data/lib/super_settings/storage/active_record_storage/models.rb +28 -0
- data/lib/super_settings/storage/active_record_storage.rb +10 -20
- 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 +50 -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 +17 -8
- data/lib/super_settings/time_precision.rb +36 -0
- data/lib/super_settings.rb +48 -13
- data/super_settings.gemspec +11 -2
- metadata +30 -12
- 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
- /data/{MIT-LICENSE → MIT-LICENSE.txt} +0 -0
@@ -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
|
@@ -0,0 +1,238 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "mongo"
|
4
|
+
|
5
|
+
module SuperSettings
|
6
|
+
module Storage
|
7
|
+
# MongoDB implementation of the SuperSettings::Storage model.
|
8
|
+
#
|
9
|
+
# You must define the connection URL to use by setting the `url` or `mongodb` attribute on the class.
|
10
|
+
#
|
11
|
+
# @example
|
12
|
+
# SuperSettings::Storage::MongoDBStorage.url = "mongodb://user:password@localhost:27017/super_settings"
|
13
|
+
#
|
14
|
+
# @example
|
15
|
+
# SuperSettings::Storage::MongoDBStorage.mongodb = Mongo::Client.new("mongodb://user:password@localhost:27017/super_settings")
|
16
|
+
class MongoDBStorage < StorageAttributes
|
17
|
+
include Storage
|
18
|
+
include Transaction
|
19
|
+
|
20
|
+
DEFAULT_COLLECTION_NAME = "super_settings"
|
21
|
+
|
22
|
+
@mongodb = nil
|
23
|
+
@url = nil
|
24
|
+
@url_hash = @url.hash
|
25
|
+
@collection_name = DEFAULT_COLLECTION_NAME
|
26
|
+
@mutex = Mutex.new
|
27
|
+
|
28
|
+
class HistoryStorage < HistoryAttributes
|
29
|
+
def as_bson
|
30
|
+
attributes = {
|
31
|
+
value: value,
|
32
|
+
changed_by: changed_by,
|
33
|
+
created_at: created_at
|
34
|
+
}
|
35
|
+
attributes[:deleted] = true if deleted?
|
36
|
+
attributes
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
class << self
|
41
|
+
attr_writer :url, :mongodb
|
42
|
+
attr_accessor :collection_name
|
43
|
+
|
44
|
+
def mongodb
|
45
|
+
if @mongodb.nil? || @url_hash != @url.hash
|
46
|
+
@mutex.synchronize do
|
47
|
+
unless @url_hash == @url.hash
|
48
|
+
@url_hash = @url.hash
|
49
|
+
@mongodb = Mongo::Client.new(@url)
|
50
|
+
create_indexes!(@mongodb)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
@mongodb
|
55
|
+
end
|
56
|
+
|
57
|
+
def settings_collection
|
58
|
+
mongodb[collection_name]
|
59
|
+
end
|
60
|
+
|
61
|
+
def updated_since(time)
|
62
|
+
time = TimePrecision.new(time, :millisecond).time
|
63
|
+
settings_collection.find(updated_at: {"$gt": time}).projection(history: 0).sort({updated_at: -1}).collect do |attributes|
|
64
|
+
record = new(attributes)
|
65
|
+
record.persisted = true
|
66
|
+
record
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
def all
|
71
|
+
settings_collection.find.projection(history: 0).collect do |attributes|
|
72
|
+
record = new(attributes)
|
73
|
+
record.persisted = true
|
74
|
+
record
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
def find_by_key(key)
|
79
|
+
query = {
|
80
|
+
key: key,
|
81
|
+
deleted: false
|
82
|
+
}
|
83
|
+
record = settings_collection.find(query).projection(history: 0).first
|
84
|
+
new(record) if record
|
85
|
+
end
|
86
|
+
|
87
|
+
def last_updated_at
|
88
|
+
last_updated_setting = settings_collection.find.projection(updated_at: 1).sort(updated_at: -1).limit(1).first
|
89
|
+
last_updated_setting["updated_at"] if last_updated_setting
|
90
|
+
end
|
91
|
+
|
92
|
+
def destroy_all
|
93
|
+
settings_collection.delete_many({})
|
94
|
+
end
|
95
|
+
|
96
|
+
def save_all(changes)
|
97
|
+
upserts = changes.collect { |setting| upsert(setting) }
|
98
|
+
changes.each { |setting| setting.new_history.clear }
|
99
|
+
settings_collection.bulk_write(upserts)
|
100
|
+
true
|
101
|
+
end
|
102
|
+
|
103
|
+
protected
|
104
|
+
|
105
|
+
def default_load_asynchronous?
|
106
|
+
true
|
107
|
+
end
|
108
|
+
|
109
|
+
private
|
110
|
+
|
111
|
+
def upsert(setting)
|
112
|
+
doc = setting.as_bson
|
113
|
+
history = setting.new_history.collect(&:as_bson)
|
114
|
+
{
|
115
|
+
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
|
+
},
|
122
|
+
upsert: true
|
123
|
+
}
|
124
|
+
}
|
125
|
+
end
|
126
|
+
|
127
|
+
def create_indexes!(client)
|
128
|
+
collection = client[collection_name]
|
129
|
+
collection_exists = client.database.collection_names.include?(collection.name)
|
130
|
+
existing_indexes = (collection_exists ? collection.indexes.to_a : [])
|
131
|
+
|
132
|
+
unique_key_index = {key: 1}
|
133
|
+
unless existing_indexes.any? { |index| index["key"] == unique_key_index }
|
134
|
+
collection.indexes.create_one(unique_key_index, unique: true)
|
135
|
+
end
|
136
|
+
|
137
|
+
updated_at_index = {updated_at: -1}
|
138
|
+
unless existing_indexes.any? { |index| index["key"] == updated_at_index }
|
139
|
+
collection.indexes.create_one(updated_at_index)
|
140
|
+
end
|
141
|
+
|
142
|
+
history_created_at_desc_index = {key: 1, "history.created_at": -1}
|
143
|
+
unless existing_indexes.any? { |index| index["key"] == history_created_at_desc_index }
|
144
|
+
collection.indexes.create_one(history_created_at_desc_index)
|
145
|
+
end
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
149
|
+
attr_reader :new_history
|
150
|
+
|
151
|
+
def initialize(*)
|
152
|
+
@new_history = []
|
153
|
+
super
|
154
|
+
end
|
155
|
+
|
156
|
+
def history(limit: nil, offset: 0)
|
157
|
+
pipeline = [
|
158
|
+
{
|
159
|
+
"$match": {key: key}
|
160
|
+
},
|
161
|
+
{
|
162
|
+
"$addFields": {
|
163
|
+
history: {
|
164
|
+
"$sortArray": {
|
165
|
+
input: "$history",
|
166
|
+
sortBy: {created_at: -1}
|
167
|
+
}
|
168
|
+
}
|
169
|
+
}
|
170
|
+
}
|
171
|
+
]
|
172
|
+
|
173
|
+
if limit || offset > 0
|
174
|
+
pipeline << {
|
175
|
+
"$addFields": {
|
176
|
+
history: {
|
177
|
+
"$slice": ["$history", offset, (limit || {"$size": "$history"})]
|
178
|
+
}
|
179
|
+
}
|
180
|
+
}
|
181
|
+
end
|
182
|
+
|
183
|
+
pipeline << {
|
184
|
+
"$project": {
|
185
|
+
_id: 0,
|
186
|
+
history: 1
|
187
|
+
}
|
188
|
+
}
|
189
|
+
|
190
|
+
record = self.class.settings_collection.aggregate(pipeline).to_a.first
|
191
|
+
return [] unless record && record["history"].is_a?(Array)
|
192
|
+
|
193
|
+
record["history"].collect do |record|
|
194
|
+
HistoryItem.new(key: key, value: record["value"], changed_by: record["changed_by"], created_at: record["created_at"], deleted: record["deleted"])
|
195
|
+
end
|
196
|
+
end
|
197
|
+
|
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
|
+
def created_at=(val)
|
206
|
+
super(TimePrecision.new(val, :millisecond).time)
|
207
|
+
end
|
208
|
+
|
209
|
+
def updated_at=(val)
|
210
|
+
super(TimePrecision.new(val, :millisecond).time)
|
211
|
+
end
|
212
|
+
|
213
|
+
def destroy
|
214
|
+
settings_collection.delete_one(key: key)
|
215
|
+
end
|
216
|
+
|
217
|
+
def as_bson
|
218
|
+
{
|
219
|
+
key: key,
|
220
|
+
raw_value: raw_value,
|
221
|
+
value_type: value_type,
|
222
|
+
description: description,
|
223
|
+
created_at: created_at,
|
224
|
+
updated_at: updated_at,
|
225
|
+
deleted: deleted?
|
226
|
+
}
|
227
|
+
end
|
228
|
+
end
|
229
|
+
|
230
|
+
def created_at=(val)
|
231
|
+
super(TimePrecision.new(val, :millisecond).time)
|
232
|
+
end
|
233
|
+
|
234
|
+
def updated_at=(val)
|
235
|
+
super(TimePrecision.new(val, :millisecond).time)
|
236
|
+
end
|
237
|
+
end
|
238
|
+
end
|
@@ -1,6 +1,6 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require "
|
3
|
+
require "redis"
|
4
4
|
|
5
5
|
module SuperSettings
|
6
6
|
module Storage
|
@@ -22,20 +22,15 @@ module SuperSettings
|
|
22
22
|
#
|
23
23
|
# @example
|
24
24
|
# SuperSettings::Storage::RedisStorage.redis = ConnectionPool.new(size: 5) { Redis.new(url: ENV["REDIS_URL"]) }
|
25
|
-
class RedisStorage
|
26
|
-
include
|
25
|
+
class RedisStorage < StorageAttributes
|
26
|
+
include Transaction
|
27
27
|
|
28
28
|
SETTINGS_KEY = "SuperSettings.settings"
|
29
29
|
UPDATED_KEY = "SuperSettings.order_by_updated_at"
|
30
30
|
|
31
|
-
class HistoryStorage
|
31
|
+
class HistoryStorage < HistoryAttributes
|
32
32
|
HISTORY_KEY_PREFIX = "SuperSettings.history"
|
33
33
|
|
34
|
-
include SuperSettings::Attributes
|
35
|
-
|
36
|
-
attr_accessor :key, :value, :changed_by, :deleted
|
37
|
-
attr_reader :created_at
|
38
|
-
|
39
34
|
class << self
|
40
35
|
def find_all_by_key(key:, offset: 0, limit: nil)
|
41
36
|
end_index = (limit.nil? ? -1 : offset + limit - 1)
|
@@ -54,10 +49,8 @@ module SuperSettings
|
|
54
49
|
record
|
55
50
|
end
|
56
51
|
|
57
|
-
def destroy_all_by_key(key)
|
58
|
-
|
59
|
-
redis.del("#{HISTORY_KEY_PREFIX}.#{key}")
|
60
|
-
end
|
52
|
+
def destroy_all_by_key(key, redis)
|
53
|
+
redis.del("#{HISTORY_KEY_PREFIX}.#{key}")
|
61
54
|
end
|
62
55
|
|
63
56
|
def redis_key(key)
|
@@ -65,24 +58,20 @@ module SuperSettings
|
|
65
58
|
end
|
66
59
|
end
|
67
60
|
|
68
|
-
def initialize(*)
|
69
|
-
@deleted = false
|
70
|
-
super
|
71
|
-
end
|
72
|
-
|
73
|
-
def created_at=(val)
|
74
|
-
@created_at = SuperSettings::Coerce.time(val)
|
75
|
-
end
|
76
|
-
|
77
61
|
def save!
|
78
62
|
raise ArgumentError.new("Missing key") if Coerce.blank?(key)
|
79
|
-
|
80
|
-
|
63
|
+
|
64
|
+
RedisStorage.transaction do |changes|
|
65
|
+
changes << self
|
81
66
|
end
|
82
67
|
end
|
83
68
|
|
84
|
-
def
|
85
|
-
|
69
|
+
def save_to_redis(redis)
|
70
|
+
redis.lpush(self.class.redis_key(key), payload_json.to_json)
|
71
|
+
end
|
72
|
+
|
73
|
+
def created_at
|
74
|
+
SuperSettings::Storage::RedisStorage.time_at_microseconds(super)
|
86
75
|
end
|
87
76
|
|
88
77
|
private
|
@@ -91,16 +80,13 @@ module SuperSettings
|
|
91
80
|
payload = {
|
92
81
|
value: value,
|
93
82
|
changed_by: changed_by,
|
94
|
-
created_at: created_at
|
83
|
+
created_at: SuperSettings::Storage::RedisStorage.microseconds(created_at)
|
95
84
|
}
|
96
85
|
payload[:deleted] = true if deleted?
|
97
86
|
payload
|
98
87
|
end
|
99
88
|
end
|
100
89
|
|
101
|
-
attr_reader :key, :raw_value, :description, :value_type, :updated_at, :created_at
|
102
|
-
attr_accessor :changed_by
|
103
|
-
|
104
90
|
class << self
|
105
91
|
def all
|
106
92
|
with_redis do |redis|
|
@@ -109,15 +95,15 @@ module SuperSettings
|
|
109
95
|
end
|
110
96
|
|
111
97
|
def updated_since(time)
|
112
|
-
|
98
|
+
min_score = microseconds(time)
|
113
99
|
with_redis do |redis|
|
114
|
-
min_score = time.to_f
|
115
100
|
keys = redis.zrangebyscore(UPDATED_KEY, min_score, "+inf")
|
116
101
|
return [] if keys.empty?
|
117
102
|
|
118
103
|
settings = []
|
119
104
|
redis.hmget(SETTINGS_KEY, *keys).each do |json|
|
120
|
-
|
105
|
+
setting = load_from_json(json) if json
|
106
|
+
settings << setting if setting && setting.updated_at > time
|
121
107
|
end
|
122
108
|
settings
|
123
109
|
end
|
@@ -133,7 +119,7 @@ module SuperSettings
|
|
133
119
|
def last_updated_at
|
134
120
|
result = with_redis { |redis| redis.zrevrange(UPDATED_KEY, 0, 1, withscores: true).first }
|
135
121
|
return nil unless result
|
136
|
-
|
122
|
+
time_at_microseconds(result[1])
|
137
123
|
end
|
138
124
|
|
139
125
|
def destroy_all
|
@@ -151,26 +137,23 @@ module SuperSettings
|
|
151
137
|
end
|
152
138
|
end
|
153
139
|
|
154
|
-
def
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
with_redis do |redis|
|
160
|
-
redis.multi do |multi_redis|
|
161
|
-
Thread.current[:super_settings_transaction_redis] = multi_redis
|
162
|
-
Thread.current[:super_settings_transaction_after_commit] = []
|
163
|
-
block.call(multi_redis)
|
164
|
-
end
|
165
|
-
after_commits = Thread.current[:super_settings_transaction_after_commit]
|
166
|
-
Thread.current[:super_settings_transaction_after_commit] = nil
|
167
|
-
after_commits.each(&:call)
|
140
|
+
def save_all(changes)
|
141
|
+
with_redis do |redis|
|
142
|
+
redis.multi do |multi_redis|
|
143
|
+
changes.each do |object|
|
144
|
+
object.save_to_redis(multi_redis)
|
168
145
|
end
|
169
|
-
ensure
|
170
|
-
Thread.current[:super_settings_transaction_redis] = nil
|
171
|
-
Thread.current[:super_settings_transaction_after_commit] = nil
|
172
146
|
end
|
173
147
|
end
|
148
|
+
true
|
149
|
+
end
|
150
|
+
|
151
|
+
def time_at_microseconds(time)
|
152
|
+
TimePrecision.new(time, :microsecond).time
|
153
|
+
end
|
154
|
+
|
155
|
+
def microseconds(time)
|
156
|
+
TimePrecision.new(time, :microsecond).to_f
|
174
157
|
end
|
175
158
|
|
176
159
|
protected
|
@@ -184,7 +167,7 @@ module SuperSettings
|
|
184
167
|
def load_from_json(json)
|
185
168
|
attributes = JSON.parse(json)
|
186
169
|
setting = new(attributes)
|
187
|
-
setting.
|
170
|
+
setting.persisted = true
|
188
171
|
setting
|
189
172
|
end
|
190
173
|
end
|
@@ -205,83 +188,39 @@ module SuperSettings
|
|
205
188
|
HistoryStorage.create!(key: key, value: value, deleted: deleted, changed_by: changed_by, created_at: created_at)
|
206
189
|
end
|
207
190
|
|
208
|
-
def
|
209
|
-
|
210
|
-
self.
|
211
|
-
self.class.transaction do |redis|
|
212
|
-
redis.hset(SETTINGS_KEY, key, payload_json)
|
213
|
-
redis.zadd(UPDATED_KEY, updated_at.to_f, key)
|
214
|
-
set_persisted!
|
215
|
-
end
|
216
|
-
true
|
191
|
+
def save_to_redis(redis)
|
192
|
+
redis.hset(SETTINGS_KEY, key, payload_json)
|
193
|
+
redis.zadd(UPDATED_KEY, self.class.microseconds(updated_at), key)
|
217
194
|
end
|
218
195
|
|
219
196
|
def destroy
|
220
|
-
self.class.
|
221
|
-
redis.
|
222
|
-
|
223
|
-
|
197
|
+
self.class.with_redis do |redis|
|
198
|
+
redis.multi do |multi_redis|
|
199
|
+
multi_redis.hdel(SETTINGS_KEY, key)
|
200
|
+
multi_redis.zrem(UPDATED_KEY, key)
|
201
|
+
HistoryStorage.destroy_all_by_key(key, multi_redis)
|
202
|
+
end
|
224
203
|
end
|
225
204
|
end
|
226
205
|
|
227
|
-
def
|
228
|
-
|
206
|
+
def created_at
|
207
|
+
self.class.time_at_microseconds(super)
|
229
208
|
end
|
230
209
|
|
231
|
-
def
|
232
|
-
|
233
|
-
end
|
234
|
-
|
235
|
-
def value_type=(value)
|
236
|
-
@value_type = (Coerce.blank?(value) ? nil : value.to_s)
|
237
|
-
end
|
238
|
-
|
239
|
-
def description=(value)
|
240
|
-
@description = (Coerce.blank?(value) ? nil : value.to_s)
|
241
|
-
end
|
242
|
-
|
243
|
-
def deleted=(value)
|
244
|
-
@deleted = Coerce.boolean(value)
|
245
|
-
end
|
246
|
-
|
247
|
-
def created_at=(value)
|
248
|
-
@created_at = SuperSettings::Coerce.time(value)
|
249
|
-
end
|
250
|
-
|
251
|
-
def updated_at=(value)
|
252
|
-
@updated_at = SuperSettings::Coerce.time(value)
|
253
|
-
end
|
254
|
-
|
255
|
-
def deleted?
|
256
|
-
!!@deleted
|
257
|
-
end
|
258
|
-
|
259
|
-
def persisted?
|
260
|
-
!!@persisted
|
210
|
+
def updated_at
|
211
|
+
self.class.time_at_microseconds(super)
|
261
212
|
end
|
262
213
|
|
263
214
|
private
|
264
215
|
|
265
|
-
def after_commit(&block)
|
266
|
-
if Thread.current[:super_settings_transaction_after_commit]
|
267
|
-
Thread.current[:super_settings_transaction_after_commit] << block
|
268
|
-
else
|
269
|
-
block.call
|
270
|
-
end
|
271
|
-
end
|
272
|
-
|
273
|
-
def set_persisted!
|
274
|
-
@persisted = true
|
275
|
-
end
|
276
|
-
|
277
216
|
def payload_json
|
278
217
|
payload = {
|
279
218
|
key: key,
|
280
219
|
raw_value: raw_value,
|
281
220
|
value_type: value_type,
|
282
221
|
description: description,
|
283
|
-
created_at: created_at
|
284
|
-
updated_at: updated_at
|
222
|
+
created_at: self.class.microseconds(created_at),
|
223
|
+
updated_at: self.class.microseconds(updated_at)
|
285
224
|
}
|
286
225
|
payload[:deleted] = true if deleted?
|
287
226
|
payload.to_json
|