super_settings 1.0.1 → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
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