Module: TypedEAV::HasTypedEAV::InstanceMethods

Defined in:
lib/typed_eav/has_typed_eav.rb

Overview

──────────────────────────────────────────────────Instance methods ──────────────────────────────────────────────────

Instance Method Summary collapse

Instance Method Details

#initialize_typed_valuesObject

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`).



682
683
684
685
686
687
688
689
690
691
692
# File 'lib/typed_eav/has_typed_eav.rb', line 682

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



789
790
791
792
793
794
795
796
797
798
799
# File 'lib/typed_eav/has_typed_eav.rb', line 789

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.



727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
# File 'lib/typed_eav/has_typed_eav.rb', line 727

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_definitionsObject

The field definitions available for this record



646
647
648
649
650
651
# File 'lib/typed_eav/has_typed_eav.rb', line 646

def typed_eav_definitions
  self.class.typed_eav_definitions(
    scope: typed_eav_scope,
    parent_scope: typed_eav_parent_scope,
  )
end

#typed_eav_hashObject

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.



809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
# File 'lib/typed_eav/has_typed_eav.rb', line 809

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_parent_scopeObject

Current parent_scope value (for two-level partitioning).

Returns nil for models that did not declare ‘parent_scope_method:` —the method is defined unconditionally so callers (e.g. the Value-side cross-axis validator) can `respond_to?` and read uniformly without branching on `parent_scope_method` configuration. Mirrors the `&.to_s` normalization on `typed_eav_scope`.



667
668
669
670
671
# File 'lib/typed_eav/has_typed_eav.rb', line 667

def typed_eav_parent_scope
  return nil unless self.class.typed_eav_parent_scope_method

  send(self.class.typed_eav_parent_scope_method)&.to_s
end

#typed_eav_scopeObject

Current scope value (for multi-tenant)



654
655
656
657
658
# File 'lib/typed_eav/has_typed_eav.rb', line 654

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.



773
774
775
776
777
778
779
780
781
782
783
784
785
# File 'lib/typed_eav/has_typed_eav.rb', line 773

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