Class: Kabk::RelationHydrator

Inherits:
Object
  • Object
show all
Defined in:
lib/kabk/relation_hydrator.rb

Overview

Hydrates <field>_display for relation fields to prevent N+1 queries.

Class Method Summary collapse

Class Method Details

.format_dates!(resource, record_hashes) ⇒ Object



71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
# File 'lib/kabk/relation_hydrator.rb', line 71

def self.format_dates!(resource, record_hashes)
  date_fields = resource.fields.select { |f| %w[date datetime].include?(f.type) }.map(&:name).map(&:to_sym)
  
  record_hashes.each do |rh|
    # Include standard audit/concurrency fields if present and not explicitly defined as fields
    [:created_at, :updated_at].each do |ts|
      if rh[ts].respond_to?(:iso8601)
        rh[ts] = rh[ts].iso8601
      elsif rh[ts].is_a?(Time) || rh[ts].is_a?(Date) || rh[ts].is_a?(DateTime)
        # Fallback formatting if it responds to strftime
        rh[ts] = rh[ts].strftime("%Y-%m-%dT%H:%M:%S.%LZ")
      end
    end

    date_fields.each do |df|
      val = rh[df]
      if val.respond_to?(:iso8601)
        rh[df] = val.iso8601
      elsif val.is_a?(Time) || val.is_a?(Date) || val.is_a?(DateTime)
        rh[df] = val.strftime("%Y-%m-%dT%H:%M:%S.%LZ")
      end
    end
  end
end

.hydrate(resource, records) ⇒ Array<Hash>

Hydrates a collection of records in-memory or via SQL joins/eager loading

Parameters:

  • resource (Kabk::Resource)
  • records (Array<Hash>, Array<Sequel::Model>)

Returns:

  • (Array<Hash>)

    Hydrated records as hashes



12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
# File 'lib/kabk/relation_hydrator.rb', line 12

def self.hydrate(resource, records)
  return [] if records.empty?

  # Convert all records to hashes immediately if they are models
  record_hashes = records.map { |r| r.is_a?(Hash) ? r.dup : r.values }

  relation_fields = resource.fields.select { |f| f.type == "relation" && f.relation }
  
  relation_fields.each do |field|
    rel_meta = field.relation
    target_resource = Registry.instance.get(rel_meta["resource"] || rel_meta[:resource])
    next unless target_resource
    
    target_model = target_resource.model_class
    value_field = (rel_meta["value_field"] || rel_meta[:value_field]).to_sym
    label_field = (rel_meta["label_field"] || rel_meta[:label_field]).to_sym
    display_key = (rel_meta["display_key"] || rel_meta[:display_key] || "#{field.name}_display").to_sym
    cardinality = rel_meta["cardinality"] || rel_meta[:cardinality] || "many_to_one"
    field_sym = field.name.to_sym

    # Extract all keys to load
    keys_to_load = record_hashes.flat_map do |rh| 
      val = rh[field_sym]
      if val.is_a?(String) && cardinality == "many_to_many"
        # In case it's stored as JSON or comma-separated
        # Attempt to parse if it looks like JSON array (SQLite)
        val = JSON.parse(val) rescue val.split(",") if val.is_a?(String)
      end
      val
    end.flatten.compact.uniq

    next if keys_to_load.empty?

    # Load relation map
    # E.g. SELECT id, full_name FROM users WHERE id IN (...)
    target_data = target_model.where(value_field => keys_to_load).select(value_field, label_field).all
    target_map = target_data.each_with_object({}) do |row, map|
      map[row[value_field]] = row[label_field]
      map[row[value_field].to_s] = row[label_field] # allow string keys too
    end

    # Map back to records
    record_hashes.each do |rh|
      val = rh[field_sym]
      if cardinality == "many_to_many"
        arr = val.is_a?(Array) ? val : (JSON.parse(val.to_s) rescue val.to_s.split(","))
        rh[display_key] = arr.map { |v| target_map[v] }.compact if arr
      else
        rh[display_key] = target_map[val] if val
      end
    end
  end

  # Finally, ensure all dates are ISO8601 formatted per spec
  format_dates!(resource, record_hashes)
  
  record_hashes
end