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



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_definitionsObject

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



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_scopeObject

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