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
-
#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=)
Bulk assign values by field NAME.
-
#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`).
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_definitions ⇒ Object
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_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.
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_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`.
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_scope ⇒ Object
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 |