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,599 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SuperSettings
4
+ # This is the model for interacting with settings. This class provides methods for finding, validating, and
5
+ # updating settings.
6
+ #
7
+ # This class does not deal with actually persisting settings to and fetching them from a data store.
8
+ # You need to specify the storage engine you want to use with the `storage` class method. This gem
9
+ # ships with storage engines for ActiveRecord, Redis, and HTTP (microservice). See the SuperSettings::Storage
10
+ # class for more details.
11
+ class Setting
12
+ LAST_UPDATED_CACHE_KEY = "SuperSettings.last_updated_at"
13
+
14
+ STRING = "string"
15
+ INTEGER = "integer"
16
+ FLOAT = "float"
17
+ BOOLEAN = "boolean"
18
+ DATETIME = "datetime"
19
+ ARRAY = "array"
20
+ SECRET = "secret"
21
+
22
+ VALUE_TYPES = [STRING, INTEGER, FLOAT, BOOLEAN, DATETIME, ARRAY, SECRET].freeze
23
+
24
+ ARRAY_DELIMITER = /[\n\r]+/.freeze
25
+
26
+ # Exception raised if you try to save with invalid data.
27
+ class InvalidRecordError < StandardError
28
+ end
29
+
30
+ include Attributes
31
+
32
+ # The changed_by attribute is used to temporarily store an identifier for the user
33
+ # who made a change to a setting to be stored in the history table. This value is optional
34
+ # and is cleared after the record is saved.
35
+ attr_accessor :changed_by
36
+
37
+ class << self
38
+ # Set a cache to use for caching values. This feature is optional. The cache must respond
39
+ # to `delete(key)` and `fetch(key, &block)`. If you are running in a Rails environment,
40
+ # you can use `Rails.cache` or any ActiveSupport::Cache::Store object.
41
+ attr_accessor :cache
42
+
43
+ # Set the storage class to use for persisting data.
44
+ attr_writer :storage
45
+
46
+ # @return [Class] The storage class to use for persisting data.
47
+ # @api private
48
+ def storage
49
+ if defined?(@storage)
50
+ @storage
51
+ elsif defined?(::SuperSettings::Storage::ActiveRecordStorage)
52
+ ::SuperSettings::Storage::ActiveRecordStorage
53
+ else
54
+ raise ArgumentError.new("No storage class defined for #{name}")
55
+ end
56
+ end
57
+
58
+ # Create a new setting with the specified attributes.
59
+ # @param attributes [Hash] hash of attribute names and values
60
+ # @return [Setting]
61
+ def create!(attributes)
62
+ setting = new(attributes)
63
+ storage.with_connection do
64
+ setting.save!
65
+ end
66
+ setting
67
+ end
68
+
69
+ # Get all the settings. This will even return settings that have been marked as deleted.
70
+ # If you just want current settings, then call #active instead.
71
+ # @return [Array<Setting>]
72
+ def all
73
+ storage.with_connection do
74
+ storage.all.collect { |record| new(record) }
75
+ end
76
+ end
77
+
78
+ # Get all the current settings.
79
+ # @return [Array<Setting>]
80
+ def active
81
+ storage.with_connection do
82
+ storage.active.collect { |record| new(record) }
83
+ end
84
+ end
85
+
86
+ # Get all settings that have been updated since the specified time stamp.
87
+ # @param time [Time]
88
+ # @return [Array<Setting>]
89
+ def updated_since(time)
90
+ storage.with_connection do
91
+ storage.updated_since(time).collect { |record| new(record) }
92
+ end
93
+ end
94
+
95
+ # Get a setting by its unique key.
96
+ # @return Setting
97
+ def find_by_key(key)
98
+ record = storage.with_connection { storage.find_by_key(key) }
99
+ if record
100
+ new(record)
101
+ end
102
+ end
103
+
104
+ # Return the maximum updated at value from all the rows. This is used in the caching
105
+ # scheme to determine if data needs to be reloaded from the database.
106
+ # @return [Time]
107
+ def last_updated_at
108
+ fetch_from_cache(LAST_UPDATED_CACHE_KEY) do
109
+ storage.with_connection { storage.last_updated_at }
110
+ end
111
+ end
112
+
113
+ # Bulk update settings in a single database transaction. No changes will be saved
114
+ # if there are any invalid records.
115
+ #
116
+ # Example:
117
+ #
118
+ # ```
119
+ # SuperSettings.bulk_update([
120
+ # {
121
+ # key: "setting-key",
122
+ # value: "foobar",
123
+ # value_type: "string",
124
+ # description: "A sample setting"
125
+ # },
126
+ # {
127
+ # key: "setting-to-delete",
128
+ # deleted: true
129
+ # }
130
+ # ])
131
+ # ```
132
+ #
133
+ # @param params [Array] Array of hashes with setting attributes. Each hash must include
134
+ # a "key" element to identify the setting. To update a key, it must also include at least
135
+ # one of "value", "value_type", or "description". If one of these attributes is present in
136
+ # the hash, it will be updated. If a setting with the given key does not exist, it will be created.
137
+ # A setting may also be deleted by providing the attribute "deleted: true".
138
+ # @return [Array] Boolean indicating if update succeeded, Array of settings affected by the update;
139
+ # if the settings were not updated, the `errors` on the settings that failed validation will be filled.
140
+ def bulk_update(params, changed_by = nil)
141
+ all_valid, settings = update_settings(params, changed_by)
142
+ if all_valid
143
+ storage.with_connection do
144
+ storage.transaction do
145
+ settings.each do |setting|
146
+ setting.save!
147
+ end
148
+ end
149
+ end
150
+ clear_last_updated_cache
151
+ end
152
+ [all_valid, settings]
153
+ end
154
+
155
+ # Determine the value type from a value.
156
+ def value_type(value)
157
+ case value
158
+ when Integer
159
+ INTEGER
160
+ when Numeric
161
+ FLOAT
162
+ when TrueClass, FalseClass
163
+ BOOLEAN
164
+ when Time, Date
165
+ DATETIME
166
+ when Array
167
+ ARRAY
168
+ else
169
+ STRING
170
+ end
171
+ end
172
+
173
+ # Clear the last updated timestamp from the cache.
174
+ # @api private
175
+ def clear_last_updated_cache
176
+ cache&.delete(Setting::LAST_UPDATED_CACHE_KEY)
177
+ end
178
+
179
+ private
180
+
181
+ # Updates settings in memory from an array of parameters.
182
+ # @param params [Array<Hash>] Each hash must contain a `key` element and may contain elements
183
+ # for `value`, `value_type`, `description`, and `deleted`.
184
+ # @param changed_by [String] Value to be stored in the history for each setting
185
+ # @return [Array] The first value is a boolean indicating if all the settings are valid,
186
+ # the second is an array of settings with their attributes updated in memory and ready to be saved.
187
+ def update_settings(params, changed_by)
188
+ changed = {}
189
+ all_valid = true
190
+
191
+ params.each do |setting_params|
192
+ setting_params = stringify_keys(setting_params)
193
+ next if Coerce.blank?(setting_params["key"])
194
+ next if ["value_type", "value", "description", "deleted"].all? { |name| Coerce.blank?(setting_params[name]) }
195
+
196
+ key = setting_params["key"]
197
+ setting = changed[key] || Setting.find_by_key(key)
198
+ unless setting
199
+ next if Coerce.present?(setting_params["delete"])
200
+ setting = Setting.new(key: setting_params["key"])
201
+ end
202
+
203
+ if Coerce.boolean(setting_params["deleted"])
204
+ setting.deleted = true
205
+ setting.changed_by = changed_by
206
+ else
207
+ setting.value_type = setting_params["value_type"] if setting_params.include?("value_type")
208
+ setting.value = setting_params["value"] if setting_params.include?("value")
209
+ setting.description = setting_params["description"] if setting_params.include?("description")
210
+ setting.deleted = false if setting.deleted?
211
+ setting.changed_by = changed_by
212
+ all_valid &= setting.valid?
213
+ end
214
+ changed[setting.key] = setting
215
+ end
216
+
217
+ [all_valid, changed.values]
218
+ end
219
+
220
+ def fetch_from_cache(key, &block)
221
+ if cache
222
+ cache.fetch(key, expires_in: 60, &block)
223
+ else
224
+ block.call
225
+ end
226
+ end
227
+
228
+ def stringify_keys(hash)
229
+ transformed = {}
230
+ hash.each do |key, value|
231
+ transformed[key.to_s] = value
232
+ end
233
+ transformed
234
+ end
235
+ end
236
+
237
+ # @param attributes [Hash]
238
+ def initialize(attributes = {})
239
+ @changes = {}
240
+ @errors = {}
241
+ if attributes.is_a?(Storage)
242
+ @record = attributes
243
+ else
244
+ @record = self.class.storage.new
245
+ self.attributes = attributes
246
+ self.value_type ||= STRING
247
+ end
248
+ end
249
+
250
+ # @return [String] the unique key for the setting.
251
+ def key
252
+ @record.key
253
+ end
254
+
255
+ # Set the value of the setting.
256
+ # @param val [String]
257
+ def key=(val)
258
+ val = val&.to_s
259
+ will_change!(:key, val) unless key == val
260
+ @record.key = val
261
+ end
262
+
263
+ # @return [Object] the value of a setting coerced to the appropriate class depending on its value type.
264
+ def value
265
+ if deleted?
266
+ nil
267
+ else
268
+ coerce(raw_value)
269
+ end
270
+ end
271
+
272
+ # Set the value of the setting.
273
+ # @param val [Object]
274
+ def value=(val)
275
+ val = serialize(val) unless val.is_a?(Array)
276
+ val = val.join("\n") if val.is_a?(Array)
277
+ self.raw_value = val
278
+ end
279
+
280
+ def value_type
281
+ @record.value_type
282
+ end
283
+
284
+ # Set the value type of the setting.
285
+ # @param val [String] one of string, integer, float, boolean, datetime, array, or secret.
286
+ def value_type=(val)
287
+ val = val&.to_s
288
+ will_change!(:value_type, val) unless value_type == val
289
+ @record.value_type = val
290
+ end
291
+
292
+ def description
293
+ @record.description
294
+ end
295
+
296
+ # @param val [String]
297
+ def description=(val)
298
+ val = val&.to_s
299
+ val = nil if val&.empty?
300
+ will_change!(:description, val) unless description == val
301
+ @record.description = val
302
+ end
303
+
304
+ def deleted?
305
+ @record.deleted?
306
+ end
307
+
308
+ alias_method :deleted, :deleted?
309
+
310
+ # Set the deleted flag on the setting. Deleted settings are not visible but are not actually
311
+ # removed from the data store.
312
+ # @param val [Boolean]
313
+ def deleted=(val)
314
+ val = Coerce.boolean(val)
315
+ will_change!(:deleted, val) unless deleted? == val
316
+ @record.deleted = val
317
+ end
318
+
319
+ def created_at
320
+ @record.created_at
321
+ end
322
+
323
+ # @param val [Time, DateTime]
324
+ def created_at=(val)
325
+ val = Coerce.time(val)
326
+ will_change!(:created_at, val) unless created_at == val
327
+ @record.created_at = val
328
+ end
329
+
330
+ def updated_at
331
+ @record.updated_at
332
+ end
333
+
334
+ # @param val [Time, DateTime]
335
+ def updated_at=(val)
336
+ val = Coerce.time(val)
337
+ will_change!(:updated_at, val) unless updated_at == val
338
+ @record.updated_at = val
339
+ end
340
+
341
+ # @return [true] if the setting has a string value type.
342
+ def string?
343
+ value_type == STRING
344
+ end
345
+
346
+ # @return [true] if the setting has an integer value type.
347
+ def integer?
348
+ value_type == INTEGER
349
+ end
350
+
351
+ # @return [true] if the setting has a float value type.
352
+ def float?
353
+ value_type == FLOAT
354
+ end
355
+
356
+ # @return [true] if the setting has a boolean value type.
357
+ def boolean?
358
+ value_type == BOOLEAN
359
+ end
360
+
361
+ # @return [true] if the setting has a datetime value type.
362
+ def datetime?
363
+ value_type == DATETIME
364
+ end
365
+
366
+ # @return [true] if the setting has an array value type.
367
+ def array?
368
+ value_type == ARRAY
369
+ end
370
+
371
+ # @return [true] if the setting has a secret value type.
372
+ def secret?
373
+ value_type == SECRET
374
+ end
375
+
376
+ # @return [true] if the setting is a secret setting and the value is encrypted in the database.
377
+ def encrypted?
378
+ secret? && Encryption.encrypted?(raw_value)
379
+ end
380
+
381
+ # Save the setting to the data storage engine.
382
+ # @return [void]
383
+ def save!
384
+ set_raw_value
385
+
386
+ unless valid?
387
+ raise InvalidRecordError.new(errors.values.join("; "))
388
+ end
389
+
390
+ timestamp = Time.now
391
+ self.created_at ||= timestamp
392
+ self.updated_at = timestamp unless updated_at && changed?(:updated_at)
393
+
394
+ self.class.storage.with_connection do
395
+ self.class.storage.transaction do
396
+ @record.save!
397
+ end
398
+
399
+ begin
400
+ self.class.clear_last_updated_cache
401
+ redact_history! if history_needs_redacting?
402
+ ensure
403
+ clear_changes
404
+ end
405
+ end
406
+ nil
407
+ end
408
+
409
+ # @return [Boolean] true if the record has been stored in the data storage engine.
410
+ def persisted?
411
+ @record.persisted?
412
+ end
413
+
414
+ # @return [Boolean] true if the record has valid data.
415
+ def valid?
416
+ validate!
417
+ @errors.empty?
418
+ end
419
+
420
+ # @return [Hash<String, Array<String>>] hash of errors generated from the last call to `valid?`
421
+ attr_reader :errors
422
+
423
+ # Mark the record as deleted. The record will not actually be deleted since it's still needed
424
+ # for caching purposes, but it will no longer be returned by queries.
425
+ def delete!
426
+ update!(deleted: true)
427
+ end
428
+
429
+ # Update the setting attributes and save it.
430
+ # @param attributes [Hash]
431
+ # @return [void]
432
+ def update!(attributes)
433
+ self.attributes = attributes
434
+ save!
435
+ end
436
+
437
+ # Return array of history items reflecting changes made to the setting over time. Items
438
+ # should be returned in reverse chronological order so that the most recent changes are first.
439
+ # @return [Array<SuperSettings::History>]
440
+ def history(limit: nil, offset: 0)
441
+ @record.history(limit: limit, offset: offset)
442
+ end
443
+
444
+ # Serialize to a hash that is used for rendering JSON responses.
445
+ # @return [Hash]
446
+ def as_json(options = nil)
447
+ attributes = {
448
+ key: key,
449
+ value: value,
450
+ value_type: value_type,
451
+ description: description,
452
+ created_at: created_at,
453
+ updated_at: updated_at
454
+ }
455
+ attributes[:encrypted] = encrypted? if secret?
456
+ attributes[:deleted] = true if deleted?
457
+ attributes
458
+ end
459
+
460
+ # Serialize to a JSON string.
461
+ # @return [String]
462
+ def to_json(options = nil)
463
+ as_json.to_json(options)
464
+ end
465
+
466
+ private
467
+
468
+ # Coerce a value for the appropriate value type.
469
+ def coerce(value)
470
+ return nil if value.respond_to?(:empty?) ? value.empty? : value.to_s.empty?
471
+
472
+ case value_type
473
+ when Setting::STRING
474
+ value.freeze
475
+ when Setting::INTEGER
476
+ Integer(value)
477
+ when Setting::FLOAT
478
+ Float(value)
479
+ when Setting::BOOLEAN
480
+ Coerce.boolean(value)
481
+ when Setting::DATETIME
482
+ Coerce.time(value).freeze
483
+ when Setting::ARRAY
484
+ if value.is_a?(String)
485
+ value.split(Setting::ARRAY_DELIMITER).map(&:freeze).freeze
486
+ else
487
+ Array(value).reject { |v| v.respond_to?(:empty?) ? v.empty? : v.to_s.empty? }.collect { |v| v.to_s.freeze }.freeze
488
+ end
489
+ when Setting::SECRET
490
+ begin
491
+ Encryption.decrypt(value).freeze
492
+ rescue Encryption::InvalidSecretError
493
+ nil
494
+ end
495
+ else
496
+ value.freeze
497
+ end
498
+ rescue ArgumentError
499
+ nil
500
+ end
501
+
502
+ # Format the value so it can be saved as a string in the database.
503
+ def serialize(value)
504
+ if value.nil? || value.to_s.empty?
505
+ nil
506
+ elsif value.is_a?(Time) || value.is_a?(DateTime)
507
+ value.utc.iso8601(6)
508
+ else
509
+ coerce(value.to_s)
510
+ end
511
+ end
512
+
513
+ # Set the raw string value that will be persisted to the data store.
514
+ def set_raw_value
515
+ if value_type == Setting::SECRET && !raw_value.to_s.empty? && (changed?(:raw_value) || !Encryption.encrypted?(raw_value))
516
+ self.raw_value = Encryption.encrypt(raw_value)
517
+ end
518
+ record_value_change
519
+ end
520
+
521
+ # Update the histories association whenever the value or key is changed.
522
+ def record_value_change
523
+ return unless changed?(:raw_value) || changed?(:deleted) || changed?(:key)
524
+ recorded_value = (deleted? || value_type == Setting::SECRET ? nil : raw_value)
525
+ @record.create_history(value: recorded_value, deleted: deleted?, changed_by: changed_by, created_at: Time.now)
526
+ end
527
+
528
+ def clear_changes
529
+ @changes = {}
530
+ self.changed_by = nil
531
+ end
532
+
533
+ def will_change!(attribute, value)
534
+ attribute = attribute.to_s
535
+ change = @changes[attribute]
536
+ unless change
537
+ change = [send(attribute)]
538
+ @changes[attribute] = change
539
+ end
540
+ change[1] = value # rubocop:disable Lint/UselessSetterCall
541
+ end
542
+
543
+ def changed?(attribute)
544
+ @changes.include?(attribute.to_s)
545
+ end
546
+
547
+ def history_needs_redacting?
548
+ value_type == Setting::SECRET && changed?(:value_type)
549
+ end
550
+
551
+ def redact_history!
552
+ @record.send(:redact_history!)
553
+ end
554
+
555
+ def raw_value=(val)
556
+ val = val&.to_s
557
+ val = nil if val&.empty?
558
+ will_change!(:raw_value, val) unless raw_value == val
559
+ @raw_value = val
560
+ @record.raw_value = val
561
+ end
562
+
563
+ def raw_value
564
+ @record.raw_value
565
+ end
566
+
567
+ def validate!
568
+ if key.to_s.empty?
569
+ add_error(:key, "cannot be empty")
570
+ elsif key.to_s.size > 190
571
+ add_error(:key, "must be less than 190 characters")
572
+ end
573
+
574
+ add_error(:value_type, "must be one of #{Setting::VALUE_TYPES.join(", ")}") unless Setting::VALUE_TYPES.include?(value_type)
575
+
576
+ add_error(:value, "must be less than 4096 characters") if raw_value.to_s.size > 4096
577
+
578
+ if !raw_value.nil? && coerce(raw_value).nil?
579
+ if value_type == Setting::INTEGER
580
+ add_error(:value, "must be an integer")
581
+ elsif value_type == Setting::FLOAT
582
+ add_error(:value, "must be a number")
583
+ elsif value_type == Setting::DATETIME
584
+ add_error(:value, "is not a valid datetime")
585
+ end
586
+ end
587
+ end
588
+
589
+ def add_error(attribute, message)
590
+ attribute = attribute.to_s
591
+ attribute_errors = @errors[attribute]
592
+ unless attribute_errors
593
+ attribute_errors = []
594
+ @errors[attribute] = attribute_errors
595
+ end
596
+ attribute_errors << "#{attribute.tr("_", " ")} #{message}"
597
+ end
598
+ end
599
+ end
@@ -0,0 +1,123 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SuperSettings
4
+ module Storage
5
+ # ActiveRecord implementation of the SuperSettings::Storage model.
6
+ #
7
+ # To use this model, you must run the migration included with the gem. The migration
8
+ # can be installed with `rake app:super_settings:install:migrations` if the gem is mounted
9
+ # as an engine in a Rails application.
10
+ class ActiveRecordStorage
11
+ # Base class that the models extend from.
12
+ class ApplicationRecord < ActiveRecord::Base
13
+ self.abstract_class = true
14
+ end
15
+
16
+ class Model < ApplicationRecord
17
+ self.table_name = "super_settings"
18
+
19
+ has_many :history_items, class_name: "SuperSettings::Storage::ActiveRecordStorage::HistoryModel", foreign_key: :key, primary_key: :key
20
+ end
21
+
22
+ class HistoryModel < ApplicationRecord
23
+ self.table_name = "super_settings_histories"
24
+
25
+ # Since these models are created automatically on a callback, ensure that the data will
26
+ # fit into the database columns since we can't handle any validation errors.
27
+ before_validation do
28
+ self.changed_by = changed_by.to_s[0, 150] if changed_by.present?
29
+ end
30
+ end
31
+
32
+ include Storage
33
+
34
+ class << self
35
+ def all
36
+ if Model.table_exists?
37
+ Model.all.collect { |model| new(model) }
38
+ else
39
+ []
40
+ end
41
+ end
42
+
43
+ def active
44
+ if Model.table_exists?
45
+ Model.where(deleted: false).collect { |model| new(model) }
46
+ else
47
+ []
48
+ end
49
+ end
50
+
51
+ def updated_since(time)
52
+ if Model.table_exists?
53
+ Model.where("updated_at > ?", time).collect { |model| new(model) }
54
+ else
55
+ []
56
+ end
57
+ end
58
+
59
+ def find_by_key(key)
60
+ model = Model.where(deleted: false).find_by(key: key) if Model.table_exists?
61
+ new(model) if model
62
+ end
63
+
64
+ def last_updated_at
65
+ if Model.table_exists?
66
+ Model.maximum(:updated_at)
67
+ end
68
+ end
69
+
70
+ def with_connection(&block)
71
+ Model.connection_pool.with_connection(&block)
72
+ end
73
+
74
+ def transaction(&block)
75
+ Model.transaction(&block)
76
+ end
77
+
78
+ protected
79
+
80
+ # Only load settings asynchronously if there is an extra database connection left in the
81
+ # connection pool or if the configuration has explicitly allowed it.
82
+ def default_load_asynchronous?
83
+ Model.connection_pool.size > Thread.list.size
84
+ end
85
+ end
86
+
87
+ delegate :key, :key=, :raw_value, :raw_value=, :value_type, :value_type=, :description, :description=,
88
+ :deleted?, :deleted=, :updated_at, :updated_at=, :created_at, :created_at=, :persisted?, :save!,
89
+ to: :@model
90
+
91
+ def initialize(attributes = {})
92
+ @model = if attributes.is_a?(Model)
93
+ attributes
94
+ else
95
+ Model.new(attributes)
96
+ end
97
+ end
98
+
99
+ def history(limit: nil, offset: 0)
100
+ finder = @model.history_items.order(id: :desc).offset(offset)
101
+ finder = finder.limit(limit) if limit
102
+ finder.collect do |record|
103
+ HistoryItem.new(key: key, value: record.value, changed_by: record.changed_by, created_at: record.created_at, deleted: record.deleted?)
104
+ end
105
+ end
106
+
107
+ def create_history(changed_by:, created_at:, value: nil, deleted: false)
108
+ history_attributes = {value: value, deleted: deleted, changed_by: changed_by, created_at: created_at}
109
+ if @model.persisted?
110
+ @model.history_items.create!(history_attributes)
111
+ else
112
+ @model.history_items.build(history_attributes)
113
+ end
114
+ end
115
+
116
+ protected
117
+
118
+ def redact_history!
119
+ @model.history_items.update_all(value: nil)
120
+ end
121
+ end
122
+ end
123
+ end