super_settings 0.0.0.rc1

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 (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