@@ -11,9 +11,8 @@ class ValidationError < StandardError; end
1111 field :openai_model , type : :string , default : ENV [ "OPENAI_MODEL" ]
1212 field :brand_fetch_client_id , type : :string , default : ENV [ "BRAND_FETCH_CLIENT_ID" ]
1313
14- # Single hash field for all dynamic provider credentials and other dynamic settings
15- # This allows unlimited dynamic fields without declaring them upfront
16- field :dynamic_fields , type : :hash , default : { }
14+ # Dynamic fields are now stored as individual entries with "dynamic:" prefix
15+ # This prevents race conditions and ensures each field is independently managed
1716
1817 # Onboarding and app settings
1918 ONBOARDING_STATES = %w[ open closed invite_only ] . freeze
@@ -50,16 +49,16 @@ def onboarding_state=(state)
5049 end
5150
5251 # Support dynamic field access via bracket notation
53- # First checks if it's a declared field, then falls back to dynamic_fields hash
52+ # First checks if it's a declared field, then falls back to individual dynamic entries
5453 def []( key )
5554 key_str = key . to_s
5655
5756 # Check if it's a declared field first
5857 if respond_to? ( key_str )
5958 public_send ( key_str )
6059 else
61- # Fall back to dynamic_fields hash
62- dynamic_fields [ key_str ]
60+ # Fall back to individual dynamic entry lookup
61+ find_by ( var : dynamic_key_name ( key_str ) ) &. value
6362 end
6463 end
6564
@@ -70,38 +69,50 @@ def []=(key, value)
7069 if respond_to? ( "#{ key_str } =" )
7170 public_send ( "#{ key_str } =" , value )
7271 else
73- # Otherwise, manage in dynamic_fields hash
74- current_dynamic = dynamic_fields . dup
72+ # Store as individual dynamic entry
73+ dynamic_key = dynamic_key_name ( key_str )
7574 if value . nil?
76- current_dynamic . delete ( key_str ) # treat nil as delete
75+ where ( var : dynamic_key ) . destroy_all
76+ clear_cache
7777 else
78- current_dynamic [ key_str ] = value
78+ # Use upsert for atomic insert/update to avoid race conditions
79+ upsert ( { var : dynamic_key , value : value . to_yaml } , unique_by : :var )
80+ clear_cache
7981 end
80- self . dynamic_fields = current_dynamic # persists & busts cache
8182 end
8283 end
8384
8485 # Check if a dynamic field exists (useful to distinguish nil value vs missing key)
8586 def key? ( key )
8687 key_str = key . to_s
87- respond_to? ( key_str ) || dynamic_fields . key? ( key_str )
88+ return true if respond_to? ( key_str )
89+
90+ # Check if dynamic entry exists
91+ where ( var : dynamic_key_name ( key_str ) ) . exists?
8892 end
8993
9094 # Delete a dynamic field
9195 def delete ( key )
9296 key_str = key . to_s
9397 return nil if respond_to? ( key_str ) # Can't delete declared fields
9498
95- current_dynamic = dynamic_fields . dup
96- value = current_dynamic . delete ( key_str )
97- self . dynamic_fields = current_dynamic
99+ dynamic_key = dynamic_key_name ( key_str )
100+ value = self [ key_str ]
101+ where ( var : dynamic_key ) . destroy_all
102+ clear_cache
98103 value
99104 end
100105
101106 # List all dynamic field keys (excludes declared fields)
102107 def dynamic_keys
103- dynamic_fields . keys
108+ where ( "var LIKE ?" , "dynamic:%" ) . pluck ( :var ) . map { | var | var . sub ( /^dynamic:/ , "" ) }
104109 end
110+
111+ private
112+
113+ def dynamic_key_name ( key_str )
114+ "dynamic:#{ key_str } "
115+ end
105116 end
106117
107118 # Validates OpenAI configuration requires model when custom URI base is set
0 commit comments