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.
Files changed (58) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +34 -2
  3. data/README.md +121 -16
  4. data/VERSION +1 -1
  5. data/app/helpers/super_settings/settings_helper.rb +13 -3
  6. data/app/views/layouts/super_settings/settings.html.erb +1 -1
  7. data/config/routes.rb +1 -1
  8. data/db/migrate/20210414004553_create_super_settings.rb +1 -7
  9. data/lib/super_settings/application/api.js +4 -1
  10. data/lib/super_settings/application/helper.rb +56 -17
  11. data/lib/super_settings/application/images/arrow-down-short.svg +3 -0
  12. data/lib/super_settings/application/images/arrow-up-short.svg +3 -0
  13. data/lib/super_settings/application/images/info-circle.svg +4 -0
  14. data/lib/super_settings/application/images/pencil-square.svg +4 -0
  15. data/lib/super_settings/application/images/plus.svg +3 -1
  16. data/lib/super_settings/application/images/trash3.svg +3 -0
  17. data/lib/super_settings/application/images/x-circle.svg +4 -0
  18. data/lib/super_settings/application/index.html.erb +54 -37
  19. data/lib/super_settings/application/layout.html.erb +5 -2
  20. data/lib/super_settings/application/layout_styles.css +7 -151
  21. data/lib/super_settings/application/layout_vars.css.erb +21 -0
  22. data/lib/super_settings/application/scripts.js +100 -21
  23. data/lib/super_settings/application/style_vars.css.erb +62 -0
  24. data/lib/super_settings/application/styles.css +183 -14
  25. data/lib/super_settings/application.rb +18 -11
  26. data/lib/super_settings/attributes.rb +1 -8
  27. data/lib/super_settings/configuration.rb +9 -0
  28. data/lib/super_settings/context/current.rb +33 -0
  29. data/lib/super_settings/context.rb +3 -0
  30. data/lib/super_settings/controller_actions.rb +2 -2
  31. data/lib/super_settings/engine.rb +1 -3
  32. data/lib/super_settings/history_item.rb +1 -1
  33. data/lib/super_settings/http_client.rb +165 -0
  34. data/lib/super_settings/local_cache.rb +0 -15
  35. data/lib/super_settings/rack_application.rb +3 -3
  36. data/lib/super_settings/rest_api.rb +5 -4
  37. data/lib/super_settings/setting.rb +14 -3
  38. data/lib/super_settings/storage/active_record_storage/models.rb +28 -0
  39. data/lib/super_settings/storage/active_record_storage.rb +10 -20
  40. data/lib/super_settings/storage/history_attributes.rb +31 -0
  41. data/lib/super_settings/storage/http_storage.rb +60 -184
  42. data/lib/super_settings/storage/json_storage.rb +201 -0
  43. data/lib/super_settings/storage/mongodb_storage.rb +238 -0
  44. data/lib/super_settings/storage/redis_storage.rb +50 -111
  45. data/lib/super_settings/storage/s3_storage.rb +165 -0
  46. data/lib/super_settings/storage/storage_attributes.rb +64 -0
  47. data/lib/super_settings/storage/test_storage.rb +3 -5
  48. data/lib/super_settings/storage/transaction.rb +67 -0
  49. data/lib/super_settings/storage.rb +17 -8
  50. data/lib/super_settings/time_precision.rb +36 -0
  51. data/lib/super_settings.rb +48 -13
  52. data/super_settings.gemspec +11 -2
  53. metadata +30 -12
  54. data/lib/super_settings/application/images/edit.svg +0 -1
  55. data/lib/super_settings/application/images/info.svg +0 -1
  56. data/lib/super_settings/application/images/slash.svg +0 -1
  57. data/lib/super_settings/application/images/trash.svg +0 -1
  58. /data/{MIT-LICENSE → MIT-LICENSE.txt} +0 -0
@@ -122,7 +122,7 @@ module SuperSettings
122
122
  #
123
123
  # @return [Boolean]
124
124
  def web_ui_enabled?
125
- true
125
+ SuperSettings.configuration.controller.web_ui_enabled?
126
126
  end
127
127
 
128
128
  private
@@ -159,7 +159,7 @@ module SuperSettings
159
159
 
160
160
  def handle_root_request(request)
161
161
  response = check_authorization(request, write_required: true) do |user|
162
- [200, {"content-type" => "text/html; charset=utf-8", "cache-control" => "no-cache"}, [Application.new(:default, add_to_head(request)).render("index.html.erb")]]
162
+ [200, {"content-type" => "text/html; charset=utf-8", "cache-control" => "no-cache"}, [Application.new(layout: :default, add_to_head: add_to_head(request), color_scheme: SuperSettings.configuration.controller.color_scheme).render]]
163
163
  end
164
164
 
165
165
  if [401, 403].include?(response.first)
@@ -236,7 +236,7 @@ module SuperSettings
236
236
 
237
237
  def post_params(request)
238
238
  if request.content_type.to_s.match?(/\Aapplication\/json/i) && request.body
239
- request.params.merge(JSON.parse(request.body.string))
239
+ request.params.merge(JSON.parse(request.body.read))
240
240
  else
241
241
  request.params
242
242
  end
@@ -23,7 +23,7 @@ module SuperSettings
23
23
  # ...
24
24
  # ]
25
25
  def index
26
- settings = Setting.active.sort_by(&:key)
26
+ settings = Setting.active.reject(&:deleted?).sort_by(&:key)
27
27
  {settings: settings.collect(&:as_json)}
28
28
  end
29
29
 
@@ -46,7 +46,8 @@ module SuperSettings
46
46
  # updated_at: iso8601 string
47
47
  # }
48
48
  def show(key)
49
- Setting.find_by_key(key)&.as_json
49
+ setting = Setting.find_by_key(key)
50
+ setting.as_json if setting && !setting.deleted?
50
51
  end
51
52
 
52
53
  # The update operation uses a transaction to atomically update all settings.
@@ -156,7 +157,7 @@ module SuperSettings
156
157
  #
157
158
  # @example
158
159
  # GET /last_updated_at
159
- # #
160
+ #
160
161
  # The response payload is:
161
162
  # {
162
163
  # last_updated_at: iso8601 string
@@ -188,7 +189,7 @@ module SuperSettings
188
189
  # ]
189
190
  def updated_since(time)
190
191
  time = Coerce.time(time)
191
- settings = Setting.updated_since(time)
192
+ settings = Setting.updated_since(time).reject(&:deleted?)
192
193
  {settings: settings.collect(&:as_json)}
193
194
  end
194
195
  end
@@ -54,7 +54,7 @@ module SuperSettings
54
54
  # @api private
55
55
  def storage
56
56
  if @storage == NOT_SET
57
- if defined?(::SuperSettings::Storage::ActiveRecordStorage)
57
+ if defined?(ActiveRecord) && defined?(::SuperSettings::Storage::ActiveRecordStorage)
58
58
  ::SuperSettings::Storage::ActiveRecordStorage
59
59
  else
60
60
  raise ArgumentError.new("No storage class defined for #{name}")
@@ -109,6 +109,7 @@ module SuperSettings
109
109
  # @param time [Time]
110
110
  # @return [Array<Setting>]
111
111
  def updated_since(time)
112
+ time = SuperSettings::Coerce.time(time)
112
113
  storage.with_connection do
113
114
  storage.updated_since(time).collect { |record| new(record) }
114
115
  end
@@ -219,6 +220,16 @@ module SuperSettings
219
220
  next if Coerce.blank?(setting_params["key"])
220
221
  next if ["value_type", "value", "description", "deleted"].all? { |name| Coerce.blank?(setting_params[name]) }
221
222
 
223
+ key_was = setting_params["key_was"]
224
+ if key_was && !changed.include?(key_was)
225
+ old_setting = Setting.find_by_key(key_was)
226
+ if old_setting
227
+ old_setting.deleted = true
228
+ old_setting.changed_by = changed_by
229
+ changed[old_setting.key] = old_setting
230
+ end
231
+ end
232
+
222
233
  key = setting_params["key"]
223
234
  setting = changed[key] || Setting.find_by_key(key)
224
235
  unless setting
@@ -372,7 +383,7 @@ module SuperSettings
372
383
  #
373
384
  # @param val [Time, DateTime]
374
385
  def created_at=(val)
375
- val = Coerce.time(val)
386
+ val = TimePrecision.new(val).time
376
387
  will_change!(:created_at, val) unless created_at == val
377
388
  @record.created_at = val
378
389
  end
@@ -388,7 +399,7 @@ module SuperSettings
388
399
  #
389
400
  # @param val [Time, DateTime]
390
401
  def updated_at=(val)
391
- val = Coerce.time(val)
402
+ val = TimePrecision.new(val).time
392
403
  will_change!(:updated_at, val) unless updated_at == val
393
404
  @record.updated_at = val
394
405
  end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SuperSettings
4
+ module Storage
5
+ class ActiveRecordStorage
6
+ # Base class that the models extend from.
7
+ class ApplicationRecord < ActiveRecord::Base
8
+ self.abstract_class = true
9
+ end
10
+
11
+ class Model < ApplicationRecord
12
+ self.table_name = "super_settings"
13
+
14
+ has_many :history_items, class_name: "SuperSettings::Storage::ActiveRecordStorage::HistoryModel", foreign_key: :key, primary_key: :key
15
+ end
16
+
17
+ class HistoryModel < ApplicationRecord
18
+ self.table_name = "super_settings_histories"
19
+
20
+ # Since these models are created automatically on a callback, ensure that the data will
21
+ # fit into the database columns since we can't handle any validation errors.
22
+ before_validation do
23
+ self.changed_by = changed_by.to_s[0, 150] if changed_by.present?
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
@@ -10,26 +10,9 @@ module SuperSettings
10
10
  # @example
11
11
  # rake app:super_settings:install:migrations
12
12
  class ActiveRecordStorage
13
- # Base class that the models extend from.
14
- class ApplicationRecord < ActiveRecord::Base
15
- self.abstract_class = true
16
- end
17
-
18
- class Model < ApplicationRecord
19
- self.table_name = "super_settings"
20
-
21
- has_many :history_items, class_name: "SuperSettings::Storage::ActiveRecordStorage::HistoryModel", foreign_key: :key, primary_key: :key
22
- end
23
-
24
- class HistoryModel < ApplicationRecord
25
- self.table_name = "super_settings_histories"
26
-
27
- # Since these models are created automatically on a callback, ensure that the data will
28
- # fit into the database columns since we can't handle any validation errors.
29
- before_validation do
30
- self.changed_by = changed_by.to_s[0, 150] if changed_by.present?
31
- end
32
- end
13
+ autoload :ApplicationRecord, File.join(__dir__, "active_record_storage/models")
14
+ autoload :Model, File.join(__dir__, "active_record_storage/models")
15
+ autoload :HistoryModel, File.join(__dir__, "active_record_storage/models")
33
16
 
34
17
  include Storage
35
18
 
@@ -77,6 +60,13 @@ module SuperSettings
77
60
  Model.transaction(&block)
78
61
  end
79
62
 
63
+ def destroy_all
64
+ ApplicationRecord.transaction do
65
+ Model.delete_all
66
+ HistoryModel.delete_all
67
+ end
68
+ end
69
+
80
70
  protected
81
71
 
82
72
  # Only load settings asynchronously if there is an extra database connection left in the
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SuperSettings
4
+ module Storage
5
+ # Generic class that can be extended to represent a history record for a setting in memory.
6
+ class HistoryAttributes
7
+ include SuperSettings::Attributes
8
+
9
+ attr_accessor :key, :value, :changed_by
10
+ attr_writer :deleted
11
+ attr_reader :created_at
12
+
13
+ def initialize(*)
14
+ @key = nil
15
+ @value = nil
16
+ @changed_by = nil
17
+ @created_at = nil
18
+ @deleted = false
19
+ super
20
+ end
21
+
22
+ def created_at=(val)
23
+ @created_at = TimePrecision.new(val).time
24
+ end
25
+
26
+ def deleted?
27
+ !!@deleted
28
+ end
29
+ end
30
+ end
31
+ end
@@ -1,59 +1,51 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "json"
4
- require "net/http"
5
-
6
3
  module SuperSettings
7
4
  module Storage
8
5
  # SuperSettings::Storage model that reads from a remote service running the SuperSettings REST API.
9
6
  # This storage engine is read only. It is intended to allow microservices to read settings from a
10
7
  # central application that exposes the SuperSettings::RestAPI.
11
- class HttpStorage
8
+ #
9
+ # You must the the base_url class attribute to the base URL of a SuperSettings REST API endpoint.
10
+ # You can also set the timeout, headers, and query_params used in reqeusts to the API.
11
+ class HttpStorage < StorageAttributes
12
12
  include Storage
13
+ include Transaction
13
14
 
14
15
  DEFAULT_HEADERS = {"Accept" => "application/json"}.freeze
15
16
  DEFAULT_TIMEOUT = 5.0
16
17
 
18
+ @base_url = nil
19
+ @timeout = nil
17
20
  @headers = {}
18
21
  @query_params = {}
22
+ @http_client = nil
23
+ @http_client_hash = nil
19
24
 
20
- attr_reader :key, :raw_value, :description, :value_type, :updated_at, :created_at
21
-
22
- class Error < StandardError
23
- end
24
-
25
- class NotFoundError < Error
25
+ class HistoryStorage < HistoryAttributes
26
26
  end
27
27
 
28
- class InvalidRecordError < Error
29
- attr_reader :errors
30
-
31
- def initialize(message, errors:)
32
- super(message)
33
- @errors = errors
34
- end
35
- end
36
-
37
- class HistoryStorage
38
- include SuperSettings::Attributes
39
-
40
- attr_accessor :key, :value, :changed_by, :deleted
28
+ class << self
29
+ # Set the base URL for the SuperSettings REST API.
30
+ attr_accessor :base_url
41
31
 
42
- def initialize(*)
43
- @deleted = false
44
- super
45
- end
32
+ # Set the timeout for requests to the SuperSettings REST API.
33
+ attr_accessor :timeout
46
34
 
47
- def created_at=(val)
48
- @created_at = SuperSettings::Coerce.time(val)
49
- end
35
+ # Add headers to this hash to add them to all requests to the SuperSettings REST API.
36
+ #
37
+ # @example
38
+ #
39
+ # SuperSettings::HttpStorage.headers["Authorization"] = "Bearer 12345"
40
+ attr_reader :headers
50
41
 
51
- def deleted?
52
- !!@deleted
53
- end
54
- end
42
+ # Add query parameters to this hash to add them to all requests to the SuperSettings REST API.
43
+ #
44
+ # @example
45
+ #
46
+ # SuperSettings::HttpStorage.query_params["access_token"] = "12345"
47
+ attr_reader :query_params
55
48
 
56
- class << self
57
49
  def all
58
50
  call_api(:get, "/settings")["settings"].collect do |attributes|
59
51
  new(attributes)
@@ -68,9 +60,9 @@ module SuperSettings
68
60
 
69
61
  def find_by_key(key)
70
62
  record = new(call_api(:get, "/setting", key: key))
71
- record.send(:set_persisted!)
63
+ record.persisted = true
72
64
  record
73
- rescue NotFoundError
65
+ rescue HttpClient::NotFoundError
74
66
  nil
75
67
  end
76
68
 
@@ -79,13 +71,30 @@ module SuperSettings
79
71
  SuperSettings::Coerce.time(value)
80
72
  end
81
73
 
82
- attr_accessor :base_url
74
+ def save_all(changes)
75
+ payload = []
76
+ changes.each do |setting|
77
+ setting_payload = {key: setting.key}
83
78
 
84
- attr_accessor :timeout
79
+ if setting.deleted?
80
+ setting_payload[:deleted] = true
81
+ else
82
+ setting_payload[:value] = setting.value
83
+ setting_payload[:value_type] = setting.value_type
84
+ setting_payload[:description] = setting.description
85
+ end
85
86
 
86
- attr_reader :headers
87
+ payload << setting_payload
88
+ end
87
89
 
88
- attr_reader :query_params
90
+ begin
91
+ call_api(:post, "/settings", settings: payload)
92
+ rescue HttpClient::InvalidRecordError
93
+ return false
94
+ end
95
+
96
+ true
97
+ end
89
98
 
90
99
  protected
91
100
 
@@ -96,115 +105,21 @@ module SuperSettings
96
105
  private
97
106
 
98
107
  def call_api(method, path, params = {})
99
- url_params = ((method == :get) ? query_params.merge(params) : query_params)
100
- uri = api_uri(path, url_params)
101
-
102
- body = nil
103
- request_headers = DEFAULT_HEADERS.merge(headers)
104
- if method == :post && !params&.empty?
105
- body = params.to_json
106
- request_headers["content-type"] = "application/json; charset=utf8-"
107
- end
108
-
109
- response = http_request(method: method, uri: uri, headers: request_headers, body: body)
110
-
111
- begin
112
- response.value # raises exception unless response is a success
113
- JSON.parse(response.body)
114
- rescue Net::ProtocolError
115
- if [404, 410].include?(response.code.to_i)
116
- raise NotFoundError.new("#{response.code} #{response.message}")
117
- elsif response.code.to_i == 422
118
- raise InvalidRecordError.new("#{response.code} #{response.message}", errors: JSON.parse(response.body)["errors"])
119
- else
120
- raise Error.new("#{response.code} #{response.message}")
121
- end
122
- rescue JSON::JSONError => e
123
- raise Error.new(e.message)
108
+ if method == :post
109
+ http_client.post(path, params)
110
+ else
111
+ http_client.get(path, params)
124
112
  end
125
113
  end
126
114
 
127
- def http_request(method:, uri:, headers: {}, body: nil, redirect_count: 0)
128
- response = nil
129
- http = Net::HTTP.new(uri.host, uri.port || uri.inferred_port)
130
- begin
131
- http.read_timeout = (timeout || DEFAULT_TIMEOUT)
132
- http.open_timeout = (timeout || DEFAULT_TIMEOUT)
133
- if uri.scheme == "https"
134
- http.use_ssl = true
135
- http.verify_mode = OpenSSL::SSL::VERIFY_NONE
136
- end
137
-
138
- request = ((method == :post) ? Net::HTTP::Post.new(uri.request_uri) : Net::HTTP::Get.new(uri.request_uri))
139
- set_headers(request, headers)
140
- request.body = body if body
141
-
142
- response = http.request(request)
143
- ensure
144
- begin
145
- http.finish if http.started?
146
- rescue IOError
147
- end
148
- end
149
-
150
- if response.is_a?(Net::HTTPRedirection)
151
- location = resp["location"]
152
- if redirect_count < 5 && SuperSettings::Coerce.present?(location)
153
- return http_request(method: :get, uri: URI(location), headers: headers, body: body, redirect_count: redirect_count + 1)
154
- end
155
- end
156
-
157
- response
158
- end
159
-
160
- def api_uri(path, params)
161
- uri = URI("#{base_url.chomp("/")}#{path}")
162
- if params && !params.empty?
163
- q = []
164
- q << uri.query unless uri.query.to_s.empty?
165
- params.each do |name, value|
166
- q << "#{URI.encode_www_form_component(name.to_s)}=#{URI.encode_www_form_component(value.to_s)}"
167
- end
168
- uri.query = q.join("&")
115
+ def http_client
116
+ hash = [base_url, timeout, headers, query_params].hash
117
+ if @http_client.nil? || @http_client_hash != hash
118
+ @http_client = HttpClient.new(base_url, headers: headers, params: query_params, timeout: timeout)
119
+ @http_client_hash = hash
169
120
  end
170
- uri
171
- end
172
-
173
- def set_headers(request, headers)
174
- headers.each do |name, value|
175
- name = name.to_s
176
- values = Array(value)
177
- request[name] = values[0].to_s
178
- values[1, values.length].each do |val|
179
- request.add_field(name, val.to_s)
180
- end
181
- end
182
- end
183
- end
184
-
185
- def initialize(*)
186
- @persisted = false
187
- @deleted = false
188
- super
189
- end
190
-
191
- def save!
192
- payload = {key: key}
193
- if deleted?
194
- payload[:deleted] = true
195
- else
196
- payload[:value] = value
197
- payload[:value_type] = value_type
198
- payload[:description] = description
199
- end
200
-
201
- begin
202
- call_api(:post, "/settings", settings: [payload])
203
- set_persisted!
204
- rescue InvalidRecordError
205
- return false
121
+ @http_client
206
122
  end
207
- true
208
123
  end
209
124
 
210
125
  def history(limit: nil, offset: 0)
@@ -227,50 +142,11 @@ module SuperSettings
227
142
  self
228
143
  end
229
144
 
230
- def key=(value)
231
- @key = (Coerce.blank?(value) ? nil : value.to_s)
232
- end
233
-
234
- def raw_value=(value)
235
- @raw_value = (Coerce.blank?(value) ? nil : value.to_s)
236
- end
237
145
  alias_method :value=, :raw_value=
238
146
  alias_method :value, :raw_value
239
147
 
240
- def value_type=(value)
241
- @value_type = (Coerce.blank?(value) ? nil : value.to_s)
242
- end
243
-
244
- def description=(value)
245
- @description = (Coerce.blank?(value) ? nil : value.to_s)
246
- end
247
-
248
- def deleted=(value)
249
- @deleted = Coerce.boolean(value)
250
- end
251
-
252
- def created_at=(value)
253
- @created_at = SuperSettings::Coerce.time(value)
254
- end
255
-
256
- def updated_at=(value)
257
- @updated_at = SuperSettings::Coerce.time(value)
258
- end
259
-
260
- def deleted?
261
- !!@deleted
262
- end
263
-
264
- def persisted?
265
- !!@persisted
266
- end
267
-
268
148
  private
269
149
 
270
- def set_persisted!
271
- @persisted = true
272
- end
273
-
274
150
  def call_api(method, path, params = {})
275
151
  self.class.send(:call_api, method, path, params)
276
152
  end