super_settings 0.0.0.rc1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (43) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +9 -0
  3. data/MIT-LICENSE +20 -0
  4. data/README.md +313 -0
  5. data/VERSION +1 -0
  6. data/app/helpers/super_settings/settings_helper.rb +32 -0
  7. data/app/views/layouts/super_settings/settings.html.erb +20 -0
  8. data/config/routes.rb +13 -0
  9. data/db/migrate/20210414004553_create_super_settings.rb +34 -0
  10. data/lib/super_settings/application/api.js +88 -0
  11. data/lib/super_settings/application/helper.rb +119 -0
  12. data/lib/super_settings/application/images/edit.svg +1 -0
  13. data/lib/super_settings/application/images/info.svg +1 -0
  14. data/lib/super_settings/application/images/plus.svg +1 -0
  15. data/lib/super_settings/application/images/slash.svg +1 -0
  16. data/lib/super_settings/application/images/trash.svg +1 -0
  17. data/lib/super_settings/application/index.html.erb +169 -0
  18. data/lib/super_settings/application/layout.html.erb +22 -0
  19. data/lib/super_settings/application/layout_styles.css +193 -0
  20. data/lib/super_settings/application/scripts.js +718 -0
  21. data/lib/super_settings/application/styles.css +122 -0
  22. data/lib/super_settings/application.rb +38 -0
  23. data/lib/super_settings/attributes.rb +24 -0
  24. data/lib/super_settings/coerce.rb +66 -0
  25. data/lib/super_settings/configuration.rb +144 -0
  26. data/lib/super_settings/controller_actions.rb +81 -0
  27. data/lib/super_settings/encryption.rb +76 -0
  28. data/lib/super_settings/engine.rb +70 -0
  29. data/lib/super_settings/history_item.rb +26 -0
  30. data/lib/super_settings/local_cache.rb +306 -0
  31. data/lib/super_settings/rack_middleware.rb +210 -0
  32. data/lib/super_settings/rest_api.rb +195 -0
  33. data/lib/super_settings/setting.rb +599 -0
  34. data/lib/super_settings/storage/active_record_storage.rb +123 -0
  35. data/lib/super_settings/storage/http_storage.rb +279 -0
  36. data/lib/super_settings/storage/redis_storage.rb +293 -0
  37. data/lib/super_settings/storage/test_storage.rb +158 -0
  38. data/lib/super_settings/storage.rb +254 -0
  39. data/lib/super_settings/version.rb +5 -0
  40. data/lib/super_settings.rb +213 -0
  41. data/lib/tasks/super_settings.rake +9 -0
  42. data/super_settings.gemspec +35 -0
  43. metadata +113 -0
@@ -0,0 +1,306 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SuperSettings
4
+ # Cache that stores the settings in memory so they can be looked up without any
5
+ # network overhead. All of the settings will be loaded in the cache and the database
6
+ # will only be checked every few seconds for changes so that lookups are very fast.
7
+ #
8
+ # The cache is thread safe and it ensures that only a single thread will ever be
9
+ # trying to update the cache at a time to avoid any dog piling effects.
10
+ class LocalCache
11
+ # @private
12
+ NOT_DEFINED = Object.new.freeze
13
+ private_constant :NOT_DEFINED
14
+
15
+ # @private
16
+ DRIFT_FACTOR = 10
17
+ private_constant :DRIFT_FACTOR
18
+
19
+ # Number of seconds that the cache will be considered fresh. The database will only be
20
+ # checked for changed settings at most this often.
21
+ attr_reader :refresh_interval
22
+
23
+ # @parem refresh_interval [Numeric] number of seconds to wait between checking for setting updates
24
+ def initialize(refresh_interval:)
25
+ @refresh_interval = refresh_interval
26
+ @lock = Mutex.new
27
+ reset
28
+ end
29
+
30
+ # Get a setting value from the cache.
31
+ #
32
+ # This method will periodically check the cache for freshness and update the cache from
33
+ # the database if there are any differences.
34
+ #
35
+ # Cache misses will be stored in the cache so that a request for a missing setting does not
36
+ # hit the database every time. This does mean that that you should not call this method with
37
+ # a large number of dynamically generated keys since that could lead to memory bloat.
38
+ #
39
+ # @param key [String, Symbol] setting key
40
+ def [](key)
41
+ ensure_cache_up_to_date!
42
+ key = key.to_s
43
+ value = @cache[key]
44
+
45
+ if value.nil? && !@cache.include?(key)
46
+ if @refreshing
47
+ value = NOT_DEFINED
48
+ else
49
+ setting = Setting.find_by_key(key)
50
+ value = (setting ? setting.value : NOT_DEFINED)
51
+ # Guard against caching too many cache missees; at some point it's better to slam
52
+ # the database rather than run out of memory.
53
+ if setting || @cache.size < 100_000
54
+ @lock.synchronize do
55
+ # For case where one thread could be iterating over the cache while it's updated causing an error
56
+ @cache = @cache.merge(key => value).freeze
57
+ @hashes = {}
58
+ end
59
+ end
60
+ end
61
+ end
62
+
63
+ return nil if value == NOT_DEFINED
64
+ value
65
+ end
66
+
67
+ # Return the setting as structured data. The keys will be split by the specified delimiter
68
+ # to create a nested hash (i.e. "a.b.c" = 1 becomes `{"a" => {"b" => {"c" => 1}}}`).
69
+ #
70
+ # See SuperSettings.structured for more details.
71
+ def structured(key = nil, delimiter: ".", max_depth: nil)
72
+ key = key.to_s
73
+ cache_key = [key, delimiter, max_depth]
74
+ cached_value = @hashes[cache_key]
75
+ return cached_value if cached_value
76
+
77
+ flattened = to_h
78
+ root_key = ""
79
+ if Coerce.present?(key)
80
+ root_key = "#{key}#{delimiter}"
81
+ reduced_hash = {}
82
+ flattened.each do |k, v|
83
+ if k.start_with?(root_key)
84
+ reduced_hash[k[root_key.length, k.length]] = v
85
+ end
86
+ end
87
+ flattened = reduced_hash
88
+ end
89
+
90
+ structured_hash = {}
91
+ flattened.each do |key, value|
92
+ set_nested_hash_value(structured_hash, key, value, 0, delimiter: delimiter, max_depth: max_depth)
93
+ end
94
+
95
+ deep_freeze_hash(structured_hash)
96
+ @lock.synchronize do
97
+ @hashes[cache_key] = structured_hash
98
+ end
99
+
100
+ structured_hash
101
+ end
102
+
103
+ # Check if the cache includes a key. Note that this will return true if you have tried
104
+ # to fetch a non-existent key since the cache will store that as undefined. This method
105
+ # is provided for testing purposes.
106
+ #
107
+ # @api private
108
+ # @param key [String, Symbol] setting key
109
+ # @return [Boolean]
110
+ def include?(key)
111
+ @cache.include?(key.to_s)
112
+ end
113
+
114
+ # Get the number of entries in the cache. Note that this will include cache misses as well.
115
+ #
116
+ # @api private
117
+ # @return the number of entries in the cache.
118
+ def size
119
+ ensure_cache_up_to_date!
120
+ @cache.size
121
+ end
122
+
123
+ # Return the cached settings as a key/value hash. Calling this method will load the cache
124
+ # with the settings if they have not already been loaded.
125
+ #
126
+ # @return [Hash]
127
+ def to_h
128
+ ensure_cache_up_to_date!
129
+ hash = {}
130
+ @cache.each do |key, data|
131
+ value, _ = data
132
+ hash[key] = value unless value == NOT_DEFINED
133
+ end
134
+ hash
135
+ end
136
+
137
+ # Return true if the cache has already been loaded from the database.
138
+ #
139
+ # @return [Boolean]
140
+ def loaded?
141
+ !!@last_refreshed
142
+ end
143
+
144
+ # Load all the settings from the database into the cache.
145
+ def load_settings(asynchronous = false)
146
+ return if @refreshing
147
+
148
+ @lock.synchronize do
149
+ return if @refreshing
150
+ @refreshing = true
151
+ @next_check_at = Time.now + @refresh_interval
152
+ end
153
+
154
+ load_block = lambda do
155
+ begin
156
+ values = {}
157
+ start_time = Time.now
158
+ Setting.active.each do |setting|
159
+ values[setting.key] = setting.value.freeze
160
+ end
161
+ set_cache_values(start_time) { values }
162
+ ensure
163
+ @refreshing = false
164
+ end
165
+ end
166
+
167
+ if asynchronous
168
+ Thread.new(&load_block)
169
+ else
170
+ load_block.call
171
+ end
172
+ end
173
+
174
+ # Load only settings that have changed since the last load.
175
+ def refresh(asynchronous = false)
176
+ last_refresh_time = @last_refreshed
177
+ return if last_refresh_time.nil?
178
+ return if @refreshing
179
+
180
+ @lock.synchronize do
181
+ return if @refreshing
182
+ @next_check_at = Time.now + @refresh_interval
183
+ return if @cache.empty?
184
+ @refreshing = true
185
+ end
186
+
187
+ refresh_block = lambda do
188
+ begin
189
+ last_db_update = Setting.last_updated_at
190
+ if last_db_update.nil? || last_db_update >= last_refresh_time - 1
191
+ merge_load(last_refresh_time)
192
+ end
193
+ ensure
194
+ @refreshing = false
195
+ end
196
+ end
197
+
198
+ if asynchronous
199
+ Thread.new(&refresh_block)
200
+ else
201
+ refresh_block.call
202
+ end
203
+ end
204
+
205
+ # Reset the cache to an unloaded state.
206
+ def reset
207
+ @lock.synchronize do
208
+ @cache = {}.freeze
209
+ @hashes = {}
210
+ @last_refreshed = nil
211
+ @next_check_at = Time.now + @refresh_interval
212
+ @refreshing = false
213
+ end
214
+ end
215
+
216
+ # Set the number of seconds to wait between cache refresh checks.
217
+ #
218
+ # @param seconds [Numeric]
219
+ def refresh_interval=(seconds)
220
+ @lock.synchronize do
221
+ @refresh_interval = seconds.to_f
222
+ @next_check_at = Time.now + @refresh_interval if @next_check_at > Time.now + @refresh_interval
223
+ end
224
+ end
225
+
226
+ # Update a single setting directly into the cache.
227
+ # @api private
228
+ def update_setting(setting)
229
+ return if Coerce.blank?(setting.key)
230
+ @lock.synchronize do
231
+ @cache = @cache.merge(setting.key => setting.value)
232
+ @hashes = {}
233
+ end
234
+ end
235
+
236
+ # Wait for the settings to be loaded if a new load was triggered. This can happen asynchronously.
237
+ # @api private
238
+ def wait_for_load
239
+ loop do
240
+ return unless @refreshing
241
+ sleep(0.001)
242
+ end
243
+ end
244
+
245
+ private
246
+
247
+ # Load just the settings have that changed since the specified timestamp.
248
+ def merge_load(last_refresh_time)
249
+ changed_settings = {}
250
+ start_time = Time.now
251
+ Setting.updated_since(last_refresh_time - 1).each do |setting|
252
+ value = (setting.deleted? ? NOT_DEFINED : setting.value)
253
+ changed_settings[setting.key] = value
254
+ end
255
+ set_cache_values(start_time) { @cache.merge(changed_settings) }
256
+ end
257
+
258
+ # Check that cache has update to date data in it. If it doesn't, then sync the
259
+ # cache with the database.
260
+ def ensure_cache_up_to_date!
261
+ if @last_refreshed.nil?
262
+ # Abort if another thread is already calling load_settings
263
+ previous_cache_id = @cache.object_id
264
+ @lock.synchronize do
265
+ return unless previous_cache_id == @cache.object_id
266
+ end
267
+ load_settings(Setting.storage.load_asynchronous?)
268
+ elsif Time.now >= @next_check_at
269
+ refresh
270
+ end
271
+ end
272
+
273
+ # Synchronized method for setting cache and sync meta data.
274
+ def set_cache_values(refreshed_at_time, &block)
275
+ @lock.synchronize do
276
+ @last_refreshed = refreshed_at_time
277
+ @refreshing = false
278
+ @cache = block.call.freeze
279
+ @hashes = {}
280
+ end
281
+ end
282
+
283
+ # Recusive method for creating a nested hash from delimited keys.
284
+ def set_nested_hash_value(hash, key, value, current_depth, delimiter:, max_depth:)
285
+ key, sub_key = (max_depth && current_depth < max_depth ? [key, nil] : key.split(delimiter, 2))
286
+ if sub_key
287
+ sub_hash = hash[key]
288
+ unless sub_hash.is_a?(Hash)
289
+ sub_hash = {}
290
+ hash[key] = sub_hash
291
+ end
292
+ set_nested_hash_value(sub_hash, sub_key, value, current_depth + 1, delimiter: delimiter, max_depth: max_depth)
293
+ else
294
+ hash[key] = value
295
+ end
296
+ end
297
+
298
+ # Recursively freeze a hash.
299
+ def deep_freeze_hash(hash)
300
+ hash.each_value do |value|
301
+ deep_freeze_hash(value) if value.is_a?(Hash)
302
+ end
303
+ hash.freeze
304
+ end
305
+ end
306
+ end
@@ -0,0 +1,210 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rack"
4
+
5
+ module SuperSettings
6
+ # Rack middleware for serving the REST API. See SuperSettings::RestAPI for more details on usage.
7
+ #
8
+ # The routes for the API can be mounted under a common path prefix specified in the initializer.
9
+ #
10
+ # You must specify some kind of authentication to use this class by at least overriding the
11
+ # `authenticated?` method in a subclass. How you do this is left up to you since you will most
12
+ # likely want to integrate in with how the rest of your application authenticates requests.
13
+ #
14
+ # You are also responsible for implementing any CSRF protection if your authentication method
15
+ # uses stateful requests (i.e. cookies or Basic auth where browser automatically include the
16
+ # credentials on every reqeust). There are other gems available that can be integrated into
17
+ # your middleware stack to provide this feature. If you need to inject meta elements into
18
+ # the page, you can do so with the `add_to_head` method.
19
+ class RackMiddleware
20
+ RESPONSE_HEADERS = {"Content-Type" => "application/json; charset=utf-8"}.freeze
21
+
22
+ # @param app [Object] Rack application or middleware for unhandled requests
23
+ # @param prefix [String] path prefix for the API routes.
24
+ def initialize(app, path_prefix = "/")
25
+ @app = app
26
+ @path_prefix = path_prefix.to_s.chomp("/")
27
+ end
28
+
29
+ def call(env)
30
+ if @path_prefix.empty? || "#{env["SCRIPT_NAME"]}#{env["PATH_INFO"]}".start_with?(@path_prefix)
31
+ handle_request(env)
32
+ else
33
+ @app.call(env)
34
+ end
35
+ end
36
+
37
+ protected
38
+
39
+ # Subclasses must implement this method.
40
+ # @param user [Object] the value returned by the `current_user` method.
41
+ # @return [Boolean] true if the user is authenticated.
42
+ def authenticated?(user)
43
+ raise NotImplementedError
44
+ end
45
+
46
+ # Subclasses can override this method to indicate if the specified user is allowed to view settings.
47
+ # @param user [Object] the value returned by the `current_user` method.
48
+ # @return [Boolean] true if the user is can view settings.
49
+ def allow_read?(user)
50
+ true
51
+ end
52
+
53
+ # Subclasses can override this method to indicate if the specified user is allowed to change settings.
54
+ # @param user [Object] the value returned by the `current_user` method.
55
+ # @return [Boolean] true if the user is can change settings.
56
+ def allow_write?(user)
57
+ true
58
+ end
59
+
60
+ # Subclasses can override this method to return the current user object. This object will
61
+ # be passed to the authenticated?, allow_read?, allow_write?, and changed_by methods.
62
+ # @param request [Rack::Request] current reqeust object
63
+ # @return [Object]
64
+ def current_user(request)
65
+ nil
66
+ end
67
+
68
+ # Subclasses can override this method to return the information about the current user that will
69
+ # be stored in the setting history when a setting is changed.
70
+ # @return [String]
71
+ def changed_by(user)
72
+ nil
73
+ end
74
+
75
+ # Subclasses can override this method to return the path to an ERB file to use as the layout
76
+ # for the HTML application. The layout can use any of the methods defined in SuperSettings::Application::Helper.
77
+ # @return [String]
78
+ def layout
79
+ "layout.html.erb"
80
+ end
81
+
82
+ # Subclasses can override this method to add custom HTML to the <head> element in the HTML application.
83
+ # This can be used to add additional script or meta tags needed for CSRF protection, etc.
84
+ # @param request [Rack::Request] current reqeust object
85
+ # @return [String]
86
+ def add_to_head(request)
87
+ end
88
+
89
+ # Subclasses can override this method to return the login URL for the application. If this is
90
+ # provided, then a user will be redirected to that URL if they are not authenticated when loading
91
+ # the HTML application.
92
+ # @param request [Rack::Request] current reqeust object
93
+ # @return [String]
94
+ def login_url(request)
95
+ end
96
+
97
+ private
98
+
99
+ def handle_request(env)
100
+ request = Rack::Request.new(env)
101
+ path = request.path[@path_prefix.length, request.path.length]
102
+ if request.get?
103
+ if path == "/" || path == ""
104
+ return handle_root_request(request)
105
+ elsif path == "/settings"
106
+ return handle_index_request(request)
107
+ elsif path == "/setting"
108
+ return handle_show_request(request)
109
+ elsif path == "/setting/history"
110
+ return handle_history_request(request)
111
+ elsif path == "/last_updated_at"
112
+ return handle_last_updated_at_request(request)
113
+ elsif path == "/updated_since"
114
+ return handle_updated_since_request(request)
115
+ end
116
+ elsif request.post?
117
+ if path == "/settings"
118
+ return handle_update_request(request)
119
+ end
120
+ end
121
+ @app.call(env)
122
+ end
123
+
124
+ def handle_root_request(request)
125
+ response = check_authorization(request, write_required: true) do |user|
126
+ [200, {"Content-Type" => "text/html; charset=utf-8"}, [Application.new(:default, add_to_head(request)).render("index.html.erb")]]
127
+ end
128
+
129
+ if [401, 403].include?(response.first)
130
+ location = login_url(request)
131
+ if location
132
+ response = [302, {"Location" => location}, []]
133
+ end
134
+ end
135
+
136
+ response
137
+ end
138
+
139
+ def handle_index_request(request)
140
+ check_authorization(request) do |user|
141
+ json_response(200, RestAPI.index)
142
+ end
143
+ end
144
+
145
+ def handle_show_request(request)
146
+ check_authorization(request) do |user|
147
+ setting = RestAPI.show(request.params["key"])
148
+ if setting
149
+ json_response(200, setting)
150
+ else
151
+ json_response(404, nil)
152
+ end
153
+ end
154
+ end
155
+
156
+ def handle_update_request(request)
157
+ check_authorization(request, write_required: true) do |user|
158
+ result = SuperSettings::RestAPI.update(post_params(request)["settings"], changed_by(user))
159
+ if result[:success]
160
+ json_response(200, result)
161
+ else
162
+ json_response(422, result)
163
+ end
164
+ end
165
+ end
166
+
167
+ def handle_history_request(request)
168
+ check_authorization(request) do |user|
169
+ history = RestAPI.history(request.params["key"], limit: request.params["limit"], offset: request.params["offset"])
170
+ if history
171
+ json_response(200, history)
172
+ else
173
+ json_response(404, nil)
174
+ end
175
+ end
176
+ end
177
+
178
+ def handle_last_updated_at_request(request)
179
+ check_authorization(request) do |user|
180
+ json_response(200, RestAPI.last_updated_at)
181
+ end
182
+ end
183
+
184
+ def handle_updated_since_request(request)
185
+ check_authorization(request) do |user|
186
+ json_response(200, RestAPI.updated_since(request.params["time"]))
187
+ end
188
+ end
189
+
190
+ def check_authorization(request, write_required: false)
191
+ user = current_user(request)
192
+ return json_response(401, error: "Authentiation required") unless authenticated?(user)
193
+ allowed = (write_required ? allow_write?(user) : allow_read?(user))
194
+ return json_response(403, error: "Access denied") unless allowed
195
+ yield(user)
196
+ end
197
+
198
+ def json_response(status, payload)
199
+ [status, RESPONSE_HEADERS, [payload.to_json]]
200
+ end
201
+
202
+ def post_params(request)
203
+ if request.content_type.to_s.match?(/\Aapplication\/json/i) && request.body
204
+ request.params.merge(JSON.parse(request.body.string))
205
+ else
206
+ request.params
207
+ end
208
+ end
209
+ end
210
+ end
@@ -0,0 +1,195 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "erb"
4
+
5
+ module SuperSettings
6
+ class RestAPI
7
+ class << self
8
+ # Get all settings sorted by key. This endpoint may be called with a REST GET request.
9
+ #
10
+ # `GET /`
11
+ #
12
+ # The response payload is:
13
+ # ```
14
+ # [
15
+ # {
16
+ # key: string,
17
+ # value: object,
18
+ # value_type: string,
19
+ # description string,
20
+ # created_at: iso8601 string,
21
+ # updated_at: iso8601 string
22
+ # },
23
+ # ...
24
+ # ]
25
+ # ```
26
+ def index
27
+ settings = Setting.active.sort_by(&:key)
28
+ {settings: settings.collect(&:as_json)}
29
+ end
30
+
31
+ # Get a setting by id.
32
+ #
33
+ # `GET /setting`
34
+ #
35
+ # Query parameters
36
+ #
37
+ # * key - setting key
38
+ #
39
+ # The response payload is:
40
+ # ```
41
+ # {
42
+ # key: string,
43
+ # value: object,
44
+ # value_type: string,
45
+ # description string,
46
+ # created_at: iso8601 string,
47
+ # updated_at: iso8601 string
48
+ # }
49
+ # ```
50
+ def show(key)
51
+ Setting.find_by_key(key)&.as_json
52
+ end
53
+
54
+ # The update operation uses a transaction to atomically update all settings.
55
+ #
56
+ # `POST /settings`
57
+ #
58
+ # The format of the parameters is an array of hashes with each setting identified by the key.
59
+ # The settings should include either `value` and `value_type` (and optionally `description`) to
60
+ # insert or update a setting, or `deleted` to delete the setting.
61
+ #
62
+ # ```
63
+ # { settings: [
64
+ # {
65
+ # key: string,
66
+ # value: object,
67
+ # value_type: string,
68
+ # description: string,
69
+ # },
70
+ # {
71
+ # key: string,
72
+ # deleted: boolean,
73
+ # },
74
+ # ...
75
+ # ]
76
+ # }
77
+ # ```
78
+ #
79
+ # The response will be either `{success: true}` or `{success: false, errors: {key => [string], ...}}`
80
+ def update(settings_params, changed_by = nil)
81
+ all_valid, settings = Setting.bulk_update(Array(settings_params), changed_by)
82
+ if all_valid
83
+ {success: true}
84
+ else
85
+ errors = {}
86
+ settings.each do |setting|
87
+ if setting.errors.any?
88
+ errors[setting.key] = setting.errors.values.flatten
89
+ end
90
+ end
91
+ {success: false, errors: errors}
92
+ end
93
+ end
94
+
95
+ # Return the history of the setting.
96
+ #
97
+ # `GET /setting/history`
98
+ #
99
+ # Query parameters
100
+ #
101
+ # * key - setting key
102
+ # * limit - number of history items to return
103
+ # * offset - index to start fetching items from (most recent items are first)
104
+ #
105
+ # The response format is:
106
+ # ```
107
+ # {
108
+ # key: string,
109
+ # encrypted: boolean,
110
+ # histories: [
111
+ # {
112
+ # value: object,
113
+ # changed_by: string,
114
+ # created_at: iso8601 string
115
+ # },
116
+ # ...
117
+ # ],
118
+ # previous_page_params: hash,
119
+ # next_page_params: hash
120
+ # }
121
+ # ```
122
+ def history(key, limit: nil, offset: 0)
123
+ setting = Setting.find_by_key(key)
124
+ return nil unless setting
125
+
126
+ offset = [offset.to_i, 0].max
127
+ limit = limit.to_i
128
+ fetch_limit = (limit > 0 ? limit + 1 : nil)
129
+ histories = setting.history(limit: fetch_limit, offset: offset)
130
+
131
+ payload = {key: setting.key, encrypted: setting.encrypted?}
132
+
133
+ if limit > 0 && !histories.empty?
134
+ if offset > 0
135
+ previous_page_params = {key: setting.key, offset: [offset - limit, 0].max, limit: limit}
136
+ end
137
+ if histories.size > limit
138
+ histories = histories.take(limit)
139
+ next_page_params = {key: setting.key, offset: offset + limit, limit: limit}
140
+ end
141
+ payload[:previous_page_params] = previous_page_params if previous_page_params
142
+ payload[:next_page_params] = next_page_params if next_page_params
143
+ end
144
+
145
+ payload[:histories] = histories.collect do |history|
146
+ history_values = {value: history.value, changed_by: history.changed_by_display, created_at: history.created_at}
147
+ history_values[:deleted] = true if history.deleted?
148
+ history_values
149
+ end
150
+
151
+ payload
152
+ end
153
+
154
+ # Return the timestamp of the most recently updated setting.
155
+ #
156
+ # `GET /last_updated_at`
157
+ # #
158
+ # The response payload is:
159
+ # {
160
+ # last_updated_at: iso8601 string
161
+ # }
162
+ def last_updated_at
163
+ {last_updated_at: Setting.last_updated_at.utc.iso8601}
164
+ end
165
+
166
+ # Return settings that have been updated since a specified timestamp.
167
+ #
168
+ # `GET /updated_since`
169
+ #
170
+ # Query parameters
171
+ #
172
+ # * time - iso8601 string
173
+ #
174
+ # The response payload is:
175
+ # ```
176
+ # [
177
+ # {
178
+ # key: string,
179
+ # value: object,
180
+ # value_type: string,
181
+ # description string,
182
+ # created_at: iso8601 string,
183
+ # updated_at: iso8601 string
184
+ # },
185
+ # ...
186
+ # ]
187
+ # ```
188
+ def updated_since(time)
189
+ time = Coerce.time(time)
190
+ settings = Setting.updated_since(time)
191
+ {settings: settings.collect(&:as_json)}
192
+ end
193
+ end
194
+ end
195
+ end