db_config 0.0.1 → 0.1.9

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 0a0734804344d2c70d69cb5527ee0e76b0e25e08133bce0e1d0815075934d61e
4
- data.tar.gz: b34981648d8a394fb52c9bdb0e6743dae46beabcd73f4bff15ad53fa587a19aa
3
+ metadata.gz: 5d5c54735150023b281a43c825dac7af70b17ff1f80ab1f5e38adf732b87f178
4
+ data.tar.gz: 3afc236097de31e5f932fdfcdc5399ec1cc26fa0e16068b654a272f7b6179591
5
5
  SHA512:
6
- metadata.gz: 29775d4ddaeb1d94b9f217b92d3b1db973a9601ee61697bd20cb187275fa847f358a40b1541b54f7b93b52a0a6e7f67f27fc6b78efdca506a338682005d7ab68
7
- data.tar.gz: f03919cf0ed1539754d00c9dab98048d4c63f3b9b832aafec5a949415dac6f7b3199088f69802d42934fbdaaf46ed2f42f975688d2bcdc8df1aaf1affdf3f1f8
6
+ metadata.gz: eda9e948efb6464e99541ed0e58a3cd7539e9491347075922a08f10aa006ce16f563dba38bce0a3370e09964e6e5c2a9576c3be9e7b3a31ec7cccf41505bd9dc
7
+ data.tar.gz: d211e5e4b58798e88dbfad1c02b1e402d1e4baf9417af1ebbca0c357175d571435e7d8eae94bc8e920321631e2b968f530da242b40d2aba4452f169d8fff0803
data/MIT-LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright Paul Bob
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,235 @@
1
+ # DBConfig
2
+
3
+ Database-backed configuration store for Rails with automatic type conversion, default values, and high-performance eager loading.
4
+
5
+ ## Features
6
+
7
+ - **Type-safe storage**: Auto-detects and converts strings, integers, floats, booleans, arrays, hashes, and nil
8
+
9
+ - **Eager loading**: Cache frequently accessed configs for near-zero database overhead
10
+ - **Simple API**: `get`/`read`, `get!`, `set`/`write`, `update`, `delete`, `exist?`, `fetch` methods
11
+
12
+ ## Installation & Setup
13
+
14
+ ```ruby
15
+ # Gemfile
16
+ gem "db_config"
17
+ ```
18
+
19
+ ```bash
20
+ bundle install
21
+ ```
22
+ ```bash
23
+ rails generate db_config:install
24
+ ```
25
+ ```bash
26
+ rails db:migrate
27
+ ```
28
+
29
+ ## Avo Integration
30
+
31
+ > [!TIP]
32
+ > If you're using [Avo](https://avohq.io), this gem ships with a pre-configured resource out of the box that should be visible on the menu as "Configurations". If you're using Avo Pro with a custom menu, you can render the resource using:
33
+ > ```ruby
34
+ > resource :db_config
35
+ > ```
36
+
37
+ ## Usage
38
+
39
+ ### Set any data type - auto-detected and preserved
40
+ ```ruby
41
+ DBConfig.set(:max_users, 1000)
42
+ DBConfig.write(:site_title, "My App") # write is an alias for set
43
+ DBConfig.set(:enabled, true)
44
+ DBConfig.set(:rate, 0.05)
45
+ DBConfig.set(:tags, ["ruby", "rails"])
46
+ DBConfig.set(:config, {api: "https://api.example.com", timeout: 30})
47
+ DBConfig.set(:feature, nil)
48
+ ```
49
+
50
+ ### Get with type preservation
51
+ ```ruby
52
+ DBConfig.get(:max_users) # => 1000 (Integer)
53
+ DBConfig.read(:enabled) # => true (Boolean) - alias for get
54
+
55
+ # Get missing config (safe - returns nil)
56
+ DBConfig.get(:missing_key) # => nil
57
+
58
+ # Get missing config with get! (raises error)
59
+ DBConfig.get!(:missing_key) # => raises DBConfig::NotFoundError
60
+
61
+ # Check if a config exists
62
+ DBConfig.exist?(:page_size) # => true or false
63
+
64
+ # Fetch with block - stores block result if key doesn't exist
65
+ DBConfig.fetch(:page_size) { 25 } # => 25 (stores if not found)
66
+
67
+ # Use || operator for fallback values
68
+ DBConfig.get(:page_size) || 25 # => 25 if :page_size not set
69
+
70
+ ```
71
+
72
+ ### Exception Handling
73
+
74
+ ```ruby
75
+ # Strict get - raises exception if not found
76
+ begin
77
+ value = DBConfig.get!(:api_token)
78
+ rescue DBConfig::NotFoundError => e
79
+ Rails.logger.warn "Missing config: #{e.message}"
80
+ value = "default_token"
81
+ end
82
+ ```
83
+
84
+ ### Managing Configurations
85
+
86
+ ```ruby
87
+ # Update configurations with type safety
88
+ DBConfig.set(:max_users, 1000) # Set value and type to Integer
89
+ DBConfig.update(:max_users, value: 500) # Update value keeping same type
90
+
91
+ DBConfig.set(:site_enabled, "true") # Set value and type to String
92
+ DBConfig.update(:site_enabled, type: "Boolean") # Convert "true" string to boolean true
93
+
94
+ DBConfig.set(:cache_ttl, 3600) # Set value and type to Integer
95
+ DBConfig.update(:cache_ttl, value: 4600, eager_load: true) # Update value and enable eager loading
96
+
97
+ DBConfig.delete(:old_setting) # => true if deleted, false if not found
98
+ ```
99
+
100
+ ### ⚡ Eager Loading
101
+
102
+ > [!NOTE]
103
+ > Eager loaded configs are cached per request and automatically synced when changed
104
+
105
+ ```ruby
106
+ # Mark configs for eager loading (loaded once per request, cached)
107
+ DBConfig.set(:api_key, "secret123")
108
+ DBConfig.update(:api_key, eager_load: true) # Enable eager loading
109
+
110
+ # Now served from cache (no database query)
111
+ DBConfig.get(:api_key) # Served from cache
112
+
113
+ DBConfig.update(:api_key, eager_load: false) # Disable eager loading
114
+ ```
115
+
116
+ > [!TIP]
117
+ > **Good for**: API keys, site settings, frequently accessed configs
118
+ > **Avoid**: Rarely used configs, large data, user-specific settings
119
+
120
+ ## Supported Types
121
+
122
+ Auto-detects and preserves: String, Integer, Float, Boolean, Array, Hash, nil
123
+
124
+ ```ruby
125
+ # All types preserved exactly
126
+ DBConfig.set(:string, "hello") # => "hello" (String)
127
+ DBConfig.set(:integer, 42) # => 42 (Integer)
128
+ DBConfig.set(:float, 3.14) # => 3.14 (Float)
129
+ DBConfig.set(:boolean, true) # => true (Boolean)
130
+ DBConfig.set(:array, [1, 2, 3]) # => [1, 2, 3] (Array)
131
+ DBConfig.set(:hash, {a: 1}) # => {a: 1} (Hash)
132
+ DBConfig.set(:null, nil) # => nil (NilClass)
133
+ ```
134
+
135
+ ## API Reference
136
+
137
+ ### Reading Methods
138
+ | Method | Description | Raises Errors | Example |
139
+ |--------|-------------|---------------|---------|
140
+ | `get(key)` | Safe retrieval, returns nil if not found | No | `DBConfig.get(:api_key)` |
141
+ | `get!(key)` | Strict retrieval | Yes, NotFoundError | `DBConfig.get!(:api_key)` |
142
+ | `read(key)` | Alias for get() | No | `DBConfig.read(:api_key)` |
143
+ | `exist?(key)` | Check if key exists | No | `DBConfig.exist?(:api_key)` |
144
+ | `fetch(key, &block)` | Get or store block result | No | `DBConfig.fetch(:size) { 25 }` |
145
+
146
+ **Fallback Pattern:**
147
+ Use the `||` operator to provide fallback values when keys don't exist:
148
+
149
+ ```ruby
150
+ # Recommended pattern for fallback values
151
+ page_size = DBConfig.get(:page_size) || 25
152
+ ```
153
+
154
+ ### Writing Methods
155
+ | Method | Description | Example |
156
+ |--------|-------------|---------|
157
+ | `set(key, value)` | Store configuration | `DBConfig.set(:api_key, "secret")` |
158
+ | `write(key, value)` | Alias for set() | `DBConfig.write(:api_key, "secret")` |
159
+ | `update(key, **options)` | Update existing config | `DBConfig.update(:key, value: 42)` |
160
+ | `delete(key)` | Remove configuration | `DBConfig.delete(:old_key)` |
161
+
162
+ ### Method Details
163
+
164
+ #### `DBConfig.fetch(key, &block)`
165
+ Gets the value if it exists, or executes the block and stores the result if it doesn't.
166
+
167
+ ```ruby
168
+ # If key exists, returns existing value (block not executed)
169
+ DBConfig.set(:api_timeout, 30)
170
+ timeout = DBConfig.fetch(:api_timeout) { 60 } # => 30 (existing value)
171
+
172
+ # If key doesn't exist, executes block and stores result
173
+ cache_size = DBConfig.fetch(:cache_size) { 100 } # => 100 (stores 100)
174
+ cache_size = DBConfig.fetch(:cache_size) { 200 } # => 100 (returns stored value)
175
+
176
+ # Works with any data type
177
+ features = DBConfig.fetch(:features) { ["feature1", "feature2"] }
178
+ config = DBConfig.fetch(:api_config) { {endpoint: "api.com", timeout: 30} }
179
+
180
+ # Returns nil if no block given and key doesn't exist
181
+ DBConfig.fetch(:missing_key) # => nil
182
+ ```
183
+
184
+ #### `DBConfig.update(key, **options)`
185
+ Updates configuration entry with intelligent type conversion and validation.
186
+
187
+ **Parameters:**
188
+ - `key` (Symbol/String) - Configuration key (must exist)
189
+ - `**options` - Keyword arguments for updates:
190
+ - `value: Any` - New value to store (with type compatibility checking)
191
+ - `type: String` - Target type for conversion ("String", "Integer", "Float", "Boolean", "Array", "Hash", "NilClass")
192
+ - `eager_load: Boolean` - Enable/disable eager loading
193
+
194
+ **Returns:** The updated value in its final type
195
+
196
+ **Type Compatibility:**
197
+ - When updating `value`: New values can change type automatically (e.g., "hello" → 42 changes String to Integer)
198
+ - When updating `type`: Only compatible conversions are allowed (e.g., "123" → Integer, "hello" → Integer fails)
199
+ - Incompatible type conversions raise `ArgumentError` with detailed message
200
+
201
+ **Examples:**
202
+ ```ruby
203
+ # Update value (can change type automatically)
204
+ DBConfig.set(:config_value, "hello")
205
+ DBConfig.update(:config_value, value: 42) # String → Integer (automatic)
206
+
207
+ # Change type explicitly (requires compatibility)
208
+ DBConfig.set(:enabled, "true") # "true" is set as a string
209
+ DBConfig.update(:enabled, type: "Boolean") # "true" → true (compatible conversion)
210
+
211
+ # Update multiple attributes
212
+ DBConfig.update(:cache_size, value: 1000, eager_load: true)
213
+
214
+ # Examples from the specification
215
+ DBConfig.update(:key, eager_load: true)
216
+ DBConfig.update(:key, type: "Boolean") # Only works if current value is compatible
217
+ DBConfig.update(:key, eager_load: true, value: true) # ✅ Always works
218
+
219
+ # Fails only on incompatible TYPE conversions (not value updates)
220
+ DBConfig.set(:username, "admin")
221
+ DBConfig.update(:username, type: "Integer") # ❌ Fails: "admin" can't become Integer
222
+ DBConfig.update(:username, value: 123) # ✅ Works: new value can be any type
223
+ ```
224
+
225
+ **Raises:**
226
+ - `DBConfig::NotFoundError` if key doesn't exist
227
+ - `ArgumentError` for invalid types or incompatible conversions
228
+
229
+ ## Contributing
230
+
231
+ Bug reports and pull requests are welcome on GitHub at https://github.com/avo-hq/db_config.
232
+
233
+ ## License
234
+
235
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
data/Rakefile ADDED
@@ -0,0 +1,3 @@
1
+ require "bundler/setup"
2
+
3
+ require "bundler/gem_tasks"
@@ -0,0 +1,121 @@
1
+ class Avo::Resources::DbConfig < Avo::BaseResource
2
+ self.model_class = ::DBConfig::Record
3
+ self.title = :key
4
+ self.search = {
5
+ query: -> {
6
+ query.ransack(
7
+ key_cont: q,
8
+ id_cont: q,
9
+ value_cont: q,
10
+ value_type_cont: q,
11
+ eager_load_cont: q,
12
+ m: "or"
13
+ ).result(distinct: false)
14
+ },
15
+ item: -> {
16
+ {
17
+ title: "",
18
+ description: "Key: <strong>#{record.key}</strong> <br> Value: <strong>#{record.value}</strong> <br> Type: <strong>#{record.value_type}</strong> <br> Eager Load: <strong>#{record.eager_load}</strong>".html_safe
19
+ }
20
+ }
21
+ }
22
+
23
+ self.description = -> {
24
+ base = if view.form?
25
+ "If you're not familiar with DBConfig, please refer to the <a href='https://github.com/avo-hq/db_config' target='_blank'>DBConfig</a> documentation.".html_safe
26
+ elsif view.index?
27
+ "This is used to manage the <a href='https://github.com/avo-hq/db_config' target='_blank'>DBConfig</a> configurations.<br>Search is performed on all fields.".html_safe
28
+ end
29
+
30
+ if view.new?
31
+ base += "<br>This is a new configuration. Please fill in the key and the type values and click the 'Save' button.<br>
32
+ You'll be able to edit the value on the next page.".html_safe
33
+ end
34
+
35
+ base
36
+ }
37
+
38
+ VALUE_TYPE_CONVERSIONS = {
39
+ "String" => :text,
40
+ "Integer" => :number,
41
+ "Boolean" => :boolean,
42
+ "Hash" => :code,
43
+ "Array" => :text,
44
+ "Float" => :number
45
+ }
46
+
47
+ def fields
48
+ field :id, as: :id
49
+ field :key
50
+
51
+ if record.present?
52
+ field :value, as: VALUE_TYPE_CONVERSIONS[record.value_type], update_using: ->{
53
+ return value unless record.value_type == "Boolean"
54
+
55
+ case value
56
+ when "1"
57
+ "true"
58
+ when "0"
59
+ "false"
60
+ else
61
+ value
62
+ end
63
+ }, visible: -> { !resource.view.new? }
64
+ else
65
+ field :value, visible: -> { !resource.view.new? }
66
+ end
67
+
68
+ field :value_type,
69
+ as: :select,
70
+ name: "Type",
71
+ options: DBConfig::Record::VALUE_TYPES, disabled: -> { record.persisted? },
72
+ help: -> {
73
+ return if !record.persisted?
74
+
75
+ path, data = Avo::Resources::DbConfig::ForceChangeType.link_arguments(resource: resource)
76
+
77
+ "Can't change the type of a configuration that has already been set.<br>
78
+ Click #{link_to("here", path, data: data).html_safe} to force the type change."
79
+ }
80
+
81
+ field :eager_load, as: :boolean
82
+ end
83
+
84
+ def self.plural_name
85
+ "Configurations"
86
+ end
87
+
88
+ def self.singular_name
89
+ "Configuration"
90
+ end
91
+
92
+ class ForceChangeType < Avo::BaseAction
93
+ self.name = "Force Change Type"
94
+ self.message = "Are you sure you want to change the type of this configuration?<br>
95
+ This will reset the type to the selected type and will remove the current value."
96
+ self.confirm_button_label = "Force Change"
97
+ self.cancel_button_label = "Cancel"
98
+ self.visible = -> {
99
+ resource.view.form? && resource.record&.persisted?
100
+ }
101
+
102
+ def fields
103
+ field :value_type, as: :select, options: DBConfig::Record::VALUE_TYPES
104
+ end
105
+
106
+ DEFAULT_VALUE_TYPES = {
107
+ "String" => "",
108
+ "Integer" => 0,
109
+ "Float" => 0.0,
110
+ "Boolean" => false,
111
+ "Array" => [],
112
+ "Hash" => {},
113
+ "NilClass" => nil
114
+ }
115
+
116
+ def handle(query:, fields:, **)
117
+ query.first.update!(value_type: fields[:value_type], value: DEFAULT_VALUE_TYPES[fields[:value_type]])
118
+ succeed "Type changed successfully."
119
+ end
120
+ end
121
+ end
@@ -0,0 +1,7 @@
1
+ # This controller has been generated to enable Rails' resource routes.
2
+ # More information on https://docs.avohq.io/3.0/controllers.html
3
+ class Avo::DbConfigsController < Avo::ResourcesController
4
+ def after_create_path
5
+ edit_resource_path(record: @record, resource: @resource)
6
+ end
7
+ end
@@ -0,0 +1,9 @@
1
+ module DBConfig
2
+ class Current < ActiveSupport::CurrentAttributes
3
+ attribute :cached_records, default: {}
4
+
5
+ def load_eager_configs!
6
+ self.cached_records = DBConfig::Record.where(eager_load: true).index_by(&:key)
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,15 @@
1
+ module DBConfig
2
+ class Middleware
3
+ def initialize(app)
4
+ @app = app
5
+ end
6
+
7
+ def call(env)
8
+ # Load eager configs for this request
9
+ DBConfig::Current.load_eager_configs!
10
+
11
+ # Continue with the request
12
+ @app.call(env)
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,40 @@
1
+ require "rails/railtie"
2
+ require "db_config/middleware"
3
+
4
+ module DBConfig
5
+ class Railtie < ::Rails::Railtie
6
+ railtie_name :db_config
7
+
8
+ def self.root
9
+ @root ||= Pathname.new(File.expand_path('../..', __dir__))
10
+ end
11
+
12
+ rake_tasks do
13
+ load "tasks/db_config_tasks.rake"
14
+ end
15
+
16
+ generators do
17
+ require "generators/db_config/install/install_generator"
18
+ end
19
+
20
+ initializer "db_config.load_app_instance_data" do |app|
21
+ # This can be used for any setup that needs to happen after Rails is loaded
22
+ end
23
+
24
+ initializer "db_config.autoload" do |app|
25
+ if defined?(Avo)
26
+ db_config_app_directory = DBConfig::Railtie.root.join("app/avo").to_s
27
+ db_config_avo_controllers_directory = DBConfig::Railtie.root.join("app/controllers/avo").to_s
28
+
29
+ ActiveSupport::Dependencies.autoload_paths.delete(db_config_app_directory)
30
+
31
+ Rails.autoloaders.main.push_dir(db_config_app_directory, namespace: Avo)
32
+ Rails.autoloaders.main.push_dir(db_config_avo_controllers_directory, namespace: Avo)
33
+ end
34
+ end
35
+
36
+ initializer "db_config.add_middleware" do |app|
37
+ app.middleware.use DBConfig::Middleware
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,38 @@
1
+ module DBConfig
2
+ class Record < ActiveRecord::Base
3
+ self.table_name_prefix = "db_config_"
4
+
5
+ VALUE_TYPES = %w[String Integer Float Boolean Array Hash NilClass]
6
+
7
+ validates :key, presence: true, uniqueness: true
8
+ validates :value_type, presence: true, inclusion: {in: VALUE_TYPES}
9
+ validates :eager_load, inclusion: {in: [true, false]}
10
+
11
+ # Sync cache automatically on any changes
12
+ after_save :sync_cache
13
+ after_destroy :sync_cache
14
+
15
+ def self.ransackable_attributes(auth_object = nil)
16
+ authorizable_ransackable_attributes
17
+ end
18
+
19
+ def self.ransackable_associations(auth_object = nil)
20
+ authorizable_ransackable_associations
21
+ end
22
+
23
+ private
24
+
25
+ def sync_cache
26
+ # Only sync if Current attributes are available (i.e., in a request context)
27
+ return unless defined?(DBConfig::Current)
28
+
29
+ if destroyed?
30
+ # Remove from cache if destroyed
31
+ DBConfig::Current.cached_records.delete(key)
32
+ else
33
+ # Update cache with current record
34
+ DBConfig::Current.cached_records[key] = self
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,3 @@
1
+ module DBConfig
2
+ VERSION = "0.1.9"
3
+ end
data/lib/db_config.rb ADDED
@@ -0,0 +1,293 @@
1
+ require "db_config/version"
2
+ require "db_config/railtie"
3
+ require "db_config/record"
4
+ require "db_config/current"
5
+ require "json"
6
+
7
+ module DBConfig
8
+ class NotFoundError < StandardError; end
9
+
10
+ class << self
11
+ def get(key)
12
+ record = get_record(key)
13
+ record ? convert_value(record.value, record.value_type) : nil
14
+ end
15
+
16
+ def get!(key)
17
+ record = get_record(key)
18
+
19
+ if record
20
+ convert_value(record.value, record.value_type)
21
+ else
22
+ raise NotFoundError, "DBConfig not found for key: #{key}"
23
+ end
24
+ end
25
+
26
+ def set(key, value)
27
+ value_str = serialize_value(value)
28
+ value_type = determine_type(value)
29
+ key = key.to_s
30
+
31
+ record = Current.cached_records[key] || DBConfig::Record.find_or_initialize_by(key:)
32
+
33
+ record.update!(
34
+ value: value_str,
35
+ value_type: value_type,
36
+ eager_load: record.eager_load || false
37
+ )
38
+
39
+ convert_value(record.value, record.value_type)
40
+ end
41
+
42
+ def delete(key)
43
+ key_str = key.to_s
44
+ record = DBConfig::Record.find_by(key: key_str)
45
+
46
+ if record
47
+ record.destroy!
48
+ true
49
+ else
50
+ false
51
+ end
52
+ end
53
+
54
+ def update(key, **kwargs)
55
+ key_str = key.to_s
56
+ record = get_record(key_str)
57
+ raise NotFoundError, "DBConfig not found for key: #{key}" unless record
58
+
59
+ # Extract current values
60
+ current_value = convert_value(record.value, record.value_type)
61
+ current_type = record.value_type
62
+
63
+ # Process updates
64
+ updates = {}
65
+
66
+ # Handle value update - new values can change type freely
67
+ if kwargs.key?(:value)
68
+ new_value = kwargs[:value]
69
+ new_type = determine_type(new_value)
70
+
71
+ updates[:value] = serialize_value(new_value)
72
+ updates[:value_type] = new_type
73
+ end
74
+
75
+ # Handle type update with compatibility check
76
+ if kwargs.key?(:type)
77
+ target_type = kwargs[:type]
78
+
79
+ # Validate target type
80
+ unless DBConfig::Record::VALUE_TYPES.include?(target_type)
81
+ raise ArgumentError, "Invalid type: #{target_type}. Must be one of: #{DBConfig::Record::VALUE_TYPES.join(", ")}"
82
+ end
83
+
84
+ # Check if current value can be converted to target type
85
+ unless value_convertible_to_type?(current_value, current_type, target_type)
86
+ raise ArgumentError, "Can't modify type because value \"#{current_value}\" doesn't support conversion from \"#{current_type}\" to \"#{target_type}\""
87
+ end
88
+
89
+ # Convert value to new type
90
+ converted_value = convert_value_to_type(current_value, current_type, target_type)
91
+ updates[:value] = serialize_value(converted_value)
92
+ updates[:value_type] = target_type
93
+ end
94
+
95
+ # Handle eager_load update
96
+ if kwargs.key?(:eager_load)
97
+ updates[:eager_load] = kwargs[:eager_load]
98
+ end
99
+
100
+ # Apply updates
101
+ record.update!(updates)
102
+
103
+ # Return the updated value in its new type
104
+ convert_value(record.value, record.value_type)
105
+ end
106
+
107
+ def exist?(key)
108
+ get_record(key) != nil
109
+ end
110
+
111
+ def fetch(key, &block)
112
+ record = get_record(key)
113
+
114
+ if record
115
+ convert_value(record.value, record.value_type)
116
+ elsif block_given?
117
+ # Execute block and store the result
118
+ value = yield
119
+ set(key, value)
120
+ end
121
+
122
+ # Return nil if key doesn't exist and no block given
123
+ end
124
+
125
+ # Aliases for convenience
126
+ alias_method :read, :get
127
+ alias_method :write, :set
128
+
129
+ private
130
+
131
+ # Get record from cache or database, returns nil if not found
132
+ def get_record(key)
133
+ key = key.to_s
134
+
135
+ Current.cached_records[key] || DBConfig::Record.find_by(key:)
136
+ end
137
+
138
+ def determine_type(value)
139
+ case value
140
+ when NilClass
141
+ "NilClass"
142
+ when String
143
+ "String"
144
+ when Integer
145
+ "Integer"
146
+ when Float
147
+ "Float"
148
+ when TrueClass, FalseClass
149
+ "Boolean"
150
+ when Array
151
+ "Array"
152
+ when Hash
153
+ "Hash"
154
+ else
155
+ "String"
156
+ end
157
+ end
158
+
159
+ def serialize_value(value)
160
+ case value
161
+ when NilClass
162
+ nil
163
+ when Array, Hash
164
+ JSON.generate(value)
165
+ else
166
+ value.to_s
167
+ end
168
+ end
169
+
170
+ def convert_value(value_str, value_type)
171
+ case value_type
172
+ when "NilClass"
173
+ nil
174
+ when "Boolean"
175
+ value_str == "true"
176
+ when "Integer"
177
+ value_str.to_i
178
+ when "Float"
179
+ value_str.to_f
180
+ when "Array"
181
+ JSON.parse(value_str)
182
+ when "Hash"
183
+ JSON.parse(value_str)
184
+ else
185
+ value_str
186
+ end
187
+ end
188
+
189
+ def types_compatible?(current_value, current_type, new_value, new_type)
190
+ # If types are the same, always compatible
191
+ return true if current_type == new_type
192
+
193
+ # Allow specific compatible conversions
194
+ case [current_type, new_type]
195
+ when ["Boolean", "String"], ["Integer", "String"], ["Float", "String"], ["NilClass", "String"]
196
+ true
197
+ when ["String", "Boolean"]
198
+ ["true", "false"].include?(current_value.to_s.downcase)
199
+ when ["String", "Integer"]
200
+ current_value.to_s.match?(/\A-?\d+\z/)
201
+ when ["String", "Float"]
202
+ current_value.to_s.match?(/\A-?\d*\.?\d+\z/)
203
+ when ["Integer", "Float"]
204
+ true
205
+ when ["Float", "Integer"]
206
+ current_value.to_f % 1 == 0 # Only if float is a whole number
207
+ when ["Boolean", "Integer"]
208
+ true # true -> 1, false -> 0
209
+ when ["Integer", "Boolean"]
210
+ [0, 1].include?(current_value)
211
+ else
212
+ false
213
+ end
214
+ end
215
+
216
+ def value_convertible_to_type?(value, current_type, target_type)
217
+ # If types are the same, always convertible
218
+ return true if current_type == target_type
219
+
220
+ case target_type
221
+ when "String"
222
+ true # Everything can be converted to string
223
+ when "Boolean"
224
+ case current_type
225
+ when "String"
226
+ ["true", "false", "1", "0"].include?(value.to_s.downcase)
227
+ when "Integer"
228
+ [0, 1].include?(value)
229
+ else
230
+ false
231
+ end
232
+ when "Integer"
233
+ case current_type
234
+ when "String"
235
+ value.to_s.match?(/\A-?\d+\z/)
236
+ when "Float"
237
+ value.to_f % 1 == 0
238
+ when "Boolean"
239
+ true
240
+ else
241
+ false
242
+ end
243
+ when "Float"
244
+ case current_type
245
+ when "String"
246
+ value.to_s.match?(/\A-?\d*\.?\d+\z/)
247
+ when "Integer", "Boolean"
248
+ true
249
+ else
250
+ false
251
+ end
252
+ else
253
+ false # Array, Hash, NilClass conversions not supported
254
+ end
255
+ end
256
+
257
+ def convert_value_to_type(value, current_type, target_type)
258
+ return value if current_type == target_type
259
+
260
+ case target_type
261
+ when "String"
262
+ value.to_s
263
+ when "Boolean"
264
+ case current_type
265
+ when "String"
266
+ ["true", "1"].include?(value.to_s.downcase)
267
+ when "Integer"
268
+ value == 1
269
+ end
270
+ when "Integer"
271
+ case current_type
272
+ when "String"
273
+ value.to_i
274
+ when "Float"
275
+ value.to_i
276
+ when "Boolean"
277
+ value ? 1 : 0
278
+ end
279
+ when "Float"
280
+ case current_type
281
+ when "String"
282
+ value.to_f
283
+ when "Integer"
284
+ value.to_f
285
+ when "Boolean"
286
+ value ? 1.0 : 0.0
287
+ end
288
+ else
289
+ raise ArgumentError, "Conversion to #{target_type} not supported"
290
+ end
291
+ end
292
+ end
293
+ end
@@ -0,0 +1,83 @@
1
+ require "rails/generators"
2
+ require "rails/generators/active_record"
3
+
4
+ module DbConfig
5
+ module Generators
6
+ class InstallGenerator < Rails::Generators::Base
7
+ include ActiveRecord::Generators::Migration
8
+
9
+ source_root File.expand_path("templates", __dir__)
10
+ desc "Creates a DBConfig migration and shows usage information"
11
+
12
+ def copy_migration
13
+ migration_template "create_db_config.rb", File.join(db_migrate_path, "create_db_config.rb")
14
+ end
15
+
16
+ def show_usage
17
+ say ""
18
+ say "=" * 70
19
+ say "DB_CONFIG INSTALLATION COMPLETE", :green
20
+ say "=" * 70
21
+ say ""
22
+ say "Run the migration:"
23
+ say " rails db:migrate", :yellow
24
+ say ""
25
+ say "Usage examples:", :green
26
+ say ""
27
+ say " # Set different types of configuration values"
28
+ say " DBConfig.set(:homepage_cta, 'Click here now!')", :cyan
29
+ say " DBConfig.write(:max_users, 1000) # alias for set", :cyan
30
+ say " DBConfig.set(:maintenance_mode, false)", :cyan
31
+ say " DBConfig.set(:allowed_countries, ['US', 'CA', 'UK'])", :cyan
32
+ say " DBConfig.set(:api_config, { endpoint: 'api.com', timeout: 30 })", :cyan
33
+ say ""
34
+ say " # Get configuration values (returns original data type)"
35
+ say " DBConfig.get(:homepage_cta)", :cyan
36
+ say " # => 'Click here now!'"
37
+ say " DBConfig.read(:allowed_countries) # alias for get", :cyan
38
+ say " # => ['US', 'CA', 'UK']"
39
+ say ""
40
+ say " # Check if configuration exists"
41
+ say " DBConfig.exist?(:page_size)", :cyan
42
+ say " # => true or false"
43
+ say ""
44
+ say " # Fetch with block - stores block result if key doesn't exist"
45
+ say " page_size = DBConfig.fetch(:page_size) { 25 }", :cyan
46
+ say " debug_mode = DBConfig.fetch(:debug_mode) { false }", :cyan
47
+ say ""
48
+ say " # Use || operator for fallback values"
49
+ say " page_size = DBConfig.get(:page_size) || 25", :cyan
50
+ say " admin_emails = DBConfig.get(:admin_emails) || []", :cyan
51
+ say ""
52
+
53
+ say " # Enable/disable eager loading for a config"
54
+ say " DBConfig.eager_load(:homepage_cta, true)", :cyan
55
+ say ""
56
+ say " # Handle missing configs"
57
+ say " begin"
58
+ say " DBConfig.get(:missing_key)"
59
+ say " rescue DBConfig::NotFoundError => e"
60
+ say " puts e.message"
61
+ say " end", :cyan
62
+ say ""
63
+ say "Supported data types: String, Integer, Float, Boolean, Array, Hash", :green
64
+ say ""
65
+ say "=" * 70
66
+ end
67
+
68
+ private
69
+
70
+ def db_migrate_path
71
+ configured_migrate_path || default_migrate_path
72
+ end
73
+
74
+ def configured_migrate_path
75
+ Rails.application.config.paths["db/migrate"]&.first
76
+ end
77
+
78
+ def default_migrate_path
79
+ Rails.root.join("db", "migrate")
80
+ end
81
+ end
82
+ end
83
+ end
@@ -0,0 +1,15 @@
1
+ class CreateDbConfig < ActiveRecord::Migration<%= "[#{ActiveRecord::Migration.current_version}]" if ActiveRecord::Migration.respond_to?(:current_version) %>
2
+ def change
3
+ create_table :db_config_records do |t|
4
+ t.string :key, null: false
5
+ t.string :value
6
+ t.string :value_type, null: false, default: "String"
7
+ t.boolean :eager_load, null: false, default: false
8
+
9
+ t.timestamps null: false
10
+ end
11
+
12
+ add_index :db_config_records, :key, unique: true
13
+ add_index :db_config_records, :eager_load
14
+ end
15
+ end
@@ -0,0 +1,4 @@
1
+ # desc "Explaining what the task does"
2
+ # task :db_config do
3
+ # # Task goes here
4
+ # end
metadata CHANGED
@@ -1,43 +1,93 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: db_config
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.1
4
+ version: 0.1.9
5
5
  platform: ruby
6
6
  authors:
7
- - Adrian
8
- - Paul
9
- autorequire:
7
+ - Paul Bob
8
+ autorequire:
10
9
  bindir: bin
11
10
  cert_chain: []
12
- date: 2025-07-01 00:00:00.000000000 Z
11
+ date: 2026-02-13 00:00:00.000000000 Z
13
12
  dependencies:
14
13
  - !ruby/object:Gem::Dependency
15
- name: avo
14
+ name: activerecord
16
15
  requirement: !ruby/object:Gem::Requirement
17
16
  requirements:
18
- - - '='
17
+ - - ">="
19
18
  - !ruby/object:Gem::Version
20
- version: 4.0.0.pre.dev.2
19
+ version: '6.0'
21
20
  type: :runtime
22
21
  prerelease: false
23
22
  version_requirements: !ruby/object:Gem::Requirement
24
23
  requirements:
25
- - - '='
24
+ - - ">="
26
25
  - !ruby/object:Gem::Version
27
- version: 4.0.0.pre.dev.2
28
- description: Visit https://avohq.io/ to get more information about this gem.
26
+ version: '6.0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: railties
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '6.0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '6.0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: activesupport
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '6.0'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '6.0'
55
+ description: |
56
+ DBConfig provides a powerful, database-backed configuration store for Rails applications.
57
+ Store and retrieve configuration values dynamically with automatic type detection and conversion
58
+ (strings, integers, floats, booleans, arrays, hashes, and nil). Features eager loading for
59
+ high-performance access to frequently used configs, a simple API (get/set/update/delete),
60
+ and seamless integration with Avo admin panels.
61
+
62
+ See https://github.com/avo-hq/db_config for full documentation and usage examples.
29
63
  email:
30
- - adrian@adrianthedev.com
31
64
  - paul.ionut.bob@gmail.com
32
65
  executables: []
33
66
  extensions: []
34
67
  extra_rdoc_files: []
35
- files: []
36
- homepage: https://avohq.io/
68
+ files:
69
+ - MIT-LICENSE
70
+ - README.md
71
+ - Rakefile
72
+ - app/avo/resources/db_config.rb
73
+ - app/controllers/avo/db_configs_controller.rb
74
+ - lib/db_config.rb
75
+ - lib/db_config/current.rb
76
+ - lib/db_config/middleware.rb
77
+ - lib/db_config/railtie.rb
78
+ - lib/db_config/record.rb
79
+ - lib/db_config/version.rb
80
+ - lib/generators/db_config/install/install_generator.rb
81
+ - lib/generators/db_config/install/templates/create_db_config.rb
82
+ - lib/tasks/db_config_tasks.rake
83
+ homepage: https://github.com/avo-hq/db_config
37
84
  licenses:
38
- - Commercial
39
- metadata: {}
40
- post_install_message:
85
+ - MIT
86
+ metadata:
87
+ homepage_uri: https://github.com/avo-hq/db_config
88
+ source_code_uri: https://github.com/avo-hq/db_config
89
+ changelog_uri: https://github.com/avo-hq/db_config/blob/main/CHANGELOG.md
90
+ post_install_message:
41
91
  rdoc_options: []
42
92
  require_paths:
43
93
  - lib
@@ -53,7 +103,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
53
103
  version: '0'
54
104
  requirements: []
55
105
  rubygems_version: 3.5.22
56
- signing_key:
106
+ signing_key:
57
107
  specification_version: 4
58
- summary: Visit https://avohq.io/ to get more information about this gem.
108
+ summary: Database-backed configuration store for Rails with automatic type conversion,
109
+ eager loading, and Avo integration
59
110
  test_files: []