Module: TypedEAV::HasTypedEAV::InstanceMethods
- Defined in:
- lib/typed_eav/has_typed_eav.rb
Overview
──────────────────────────────────────────────────Instance methods ──────────────────────────────────────────────────
Instance Method Summary collapse
-
#initialize_typed_values ⇒ Object
Build missing values with defaults for all available fields.
-
#set_typed_eav_value(name, value) ⇒ Object
Set a specific field’s value by name.
-
#typed_eav_attributes=(attributes) ⇒ Object
(also: #typed_eav=)
rubocop:disable Metrics/AbcSize – branches on existing/new/destroy and type-restriction in one place; splitting would obscure the precedence rules.
-
#typed_eav_definitions ⇒ Object
The field definitions available for this record.
-
#typed_eav_hash ⇒ Object
Hash of all field values: { “field_name” => value, … }.
-
#typed_eav_scope ⇒ Object
Current scope value (for multi-tenant).
-
#typed_eav_value(name) ⇒ Object
Get a specific field’s value by name.
Instance Method Details
#initialize_typed_values ⇒ Object
Build missing values with defaults for all available fields. Useful in forms to show all fields even when no value exists yet.
Iterates the collision-collapsed view (‘typed_eav_defs_by_name`) rather than the raw definitions list. Otherwise, when a record’s scope partition has both a global (scope=NULL) and a same-name scoped field, ‘for_entity` returns BOTH rows and the form would render two inputs for the same name — but only the scoped one round-trips on save (it wins in `typed_eav_defs_by_name`).
307 308 309 310 311 312 313 314 315 316 317 |
# File 'lib/typed_eav/has_typed_eav.rb', line 307 def initialize_typed_values existing_field_ids = typed_values.loaded? ? typed_values.map(&:field_id) : typed_values.pluck(:field_id) typed_eav_defs_by_name.each_value do |field| next if existing_field_ids.include?(field.id) typed_values.build(field: field, value: field.default_value) end typed_values end |
#set_typed_eav_value(name, value) ⇒ Object
Set a specific field’s value by name
414 415 416 417 418 419 420 421 422 423 424 |
# File 'lib/typed_eav/has_typed_eav.rb', line 414 def set_typed_eav_value(name, value) field = typed_eav_defs_by_name[name.to_s] return unless field existing = typed_values.detect { |v| v.field_id == field.id } if existing existing.value = value else typed_values.build(field: field, value: value) end end |
#typed_eav_attributes=(attributes) ⇒ Object Also known as: typed_eav=
rubocop:disable Metrics/AbcSize – branches on existing/new/destroy and type-restriction in one place; splitting would obscure the precedence rules.
352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 |
# File 'lib/typed_eav/has_typed_eav.rb', line 352 def typed_eav_attributes=(attributes) attributes = attributes.to_h if attributes.respond_to?(:permitted?) attributes = attributes.values if attributes.is_a?(Hash) attributes = Array(attributes) fields_by_name = typed_eav_defs_by_name values_by_field_id = typed_values.index_by(&:field_id) nested = attributes.filter_map do |attrs| attrs = attrs.to_h.with_indifferent_access field = fields_by_name[attrs[:name]] next unless field # Enforce type restrictions. Normalized to strings at registration # time (see `has_typed_eav`), so no per-call mapping. allowed = self.class.allowed_typed_eav_types next if allowed&.exclude?(field.field_type_name) existing = values_by_field_id[field.id] if ActiveRecord::Type::Boolean.new.cast(attrs[:_destroy]) { id: existing&.id, _destroy: true } elsif existing { id: existing.id, value: attrs[:value] } else typed_values.build(field: field, value: attrs[:value]) nil # build already added it, skip nested_attributes end end.compact self.typed_values_attributes = nested if nested.any? end |
#typed_eav_definitions ⇒ Object
The field definitions available for this record
287 288 289 |
# File 'lib/typed_eav/has_typed_eav.rb', line 287 def typed_eav_definitions self.class.typed_eav_definitions(scope: typed_eav_scope) end |
#typed_eav_hash ⇒ Object
Hash of all field values: { “field_name” => value, … }. Same preload semantics as ‘typed_eav_value` — respects already-loaded associations instead of rebuilding the relation.
Collision-safe: on a global+scoped name overlap, the value attached to the winning field_id wins (scoped). Without this guard, a stray row tied to a shadowed global field could surface here even though writes route through the scoped winner.
434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 |
# File 'lib/typed_eav/has_typed_eav.rb', line 434 def typed_eav_hash winning_ids_by_name = typed_eav_defs_by_name.transform_values(&:id) rows = loaded_typed_values_with_fields rows.each_with_object({}) do |tv, hash| # Skip orphans (`tv.field` nil — definition deleted out from under # the value) so the hash isn't crashy when stale rows linger. next unless tv.field name = tv.field.name winning_id = winning_ids_by_name[name] effective_id = tv.field_id || tv.field&.id # A winner is registered for this name: only its row is allowed. # If no winner is registered (definition deleted while values # remain), fall back to first-wins so the hash isn't lossy. if winning_id hash[name] = tv.value if effective_id == winning_id else hash[name] = tv.value unless hash.key?(name) end end end |
#typed_eav_scope ⇒ Object
Current scope value (for multi-tenant)
292 293 294 295 296 |
# File 'lib/typed_eav/has_typed_eav.rb', line 292 def typed_eav_scope return nil unless self.class.typed_eav_scope_method send(self.class.typed_eav_scope_method)&.to_s end |
#typed_eav_value(name) ⇒ Object
Get a specific field’s value by name. Honors an already-loaded ‘typed_values` association so list-page callers that preloaded `typed_values: :field` don’t trigger a fresh query per record.
On a global+scoped name collision, prefer the value bound to the winning field_id (scoped wins). Without this guard, a stray value row attached to a shadowed global field would surface here even though writes route through the scoped winner. rubocop:disable Metrics/AbcSize, Metrics/PerceivedComplexity – name-collision precedence + orphan guard + already-loaded preload reuse.
398 399 400 401 402 403 404 405 406 407 408 409 410 |
# File 'lib/typed_eav/has_typed_eav.rb', line 398 def typed_eav_value(name) winning = typed_eav_defs_by_name[name.to_s] # Skip orphans (`v.field` nil — definition deleted out from under the # value via raw SQL or a missing FK cascade) so a stray row can't # crash the read path with NoMethodError. candidates = loaded_typed_values_with_fields.select { |v| v.field && v.field.name == name.to_s } tv = if winning && candidates.any? { |v| (v.field_id || v.field&.id) == winning.id } candidates.detect { |v| (v.field_id || v.field&.id) == winning.id } else candidates.first end tv&.value end |