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_parent_scope ⇒ Object
Current parent_scope value (for two-level partitioning).
-
#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`).
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_definitions ⇒ Object
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_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.
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_scope ⇒ Object
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_scope ⇒ Object
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 |