Module: TypedEAV::HasTypedEAV::InstanceMethods

Defined in:
lib/typed_eav/has_typed_eav/instance_methods.rb

Overview

Per-record API mixed into host AR models by the ‘has_typed_eav` macro. Reads/writes typed values via field name, returns scope/parent_scope via the configured accessor methods, and builds the collision-collapsed per-instance definition map (delegating to `Partition.definitions_by_name` so the class-query path and the instance path share one source of truth).

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



48
49
50
51
52
53
54
55
56
57
58
# File 'lib/typed_eav/has_typed_eav/instance_methods.rb', line 48

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



110
111
112
113
114
115
116
117
118
119
120
# File 'lib/typed_eav/has_typed_eav/instance_methods.rb', line 110

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=

Bulk assign values by field NAME. Coexists with (rather than replaces) the ‘accepts_nested_attributes_for :typed_values` setter declared on the host model, which accepts entries keyed by field ID.

The nested-attributes setter is the standard Rails form contract (forms post ‘field_id` as a hidden input per value row). This setter takes entries keyed by field name and translates them to field IDs before handing off to the nested-attributes setter. It also enforces the `types:` restriction declared on `has_typed_eav` and supports `_destroy: true` for removing a value by name.

record.typed_eav_attributes = [
  { name: "age",       value: 30 },
  { name: "email",     value: "test@example.com" },
  { name: "old_field", _destroy: true },
]

Pick the one that fits: forms -> typed_values_attributes=, scripting -> typed_eav_attributes=. They can’t both run in the same save.



79
80
81
82
83
84
85
86
87
88
# File 'lib/typed_eav/has_typed_eav/instance_methods.rb', line 79

def typed_eav_attributes=(attributes)
  fields_by_name     = typed_eav_defs_by_name
  values_by_field_id = typed_values.index_by(&:field_id)

  nested = normalize_typed_eav_attributes(attributes).filter_map do |attrs|
    build_or_update_typed_value(attrs, fields_by_name, values_by_field_id)
  end

  self.typed_values_attributes = nested if nested.any?
end

#typed_eav_definitionsObject

The field definitions available for this record



12
13
14
15
16
17
# File 'lib/typed_eav/has_typed_eav/instance_methods.rb', line 12

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.



130
131
132
133
134
135
136
137
138
139
140
# File 'lib/typed_eav/has_typed_eav/instance_methods.rb', line 130

def typed_eav_hash
  winning_ids_by_name = typed_eav_defs_by_name.transform_values(&:id)

  loaded_typed_values_with_fields.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

    assign_hash_value(hash, tv, winning_ids_by_name)
  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`.



33
34
35
36
37
# File 'lib/typed_eav/has_typed_eav/instance_methods.rb', line 33

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)



20
21
22
23
24
# File 'lib/typed_eav/has_typed_eav/instance_methods.rb', line 20

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.



100
101
102
103
104
105
106
107
# File 'lib/typed_eav/has_typed_eav/instance_methods.rb', line 100

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 }
  select_winning_value(candidates, winning)&.value
end