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



82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
# File 'lib/kabk/relation_hydrator.rb', line 82

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
    %i[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
70
71
72
73
74
75
76
77
78
79
80
# 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" && val.is_a?(String)
        # Handle JSON or comma-separated string values.
        val = begin
          JSON.parse(val)
        rescue StandardError
          val.split(",")
        end
      end
      val
    end.flatten.compact.uniq

    next if keys_to_load.empty?

    # Load relation map.
    # Example: 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 = if val.is_a?(Array)
                val
              else
                begin
                  JSON.parse(val.to_s)
                rescue StandardError
                  val.to_s.split(",")
                end
              end
        rh[display_key] = arr.map { |v| target_map[v] }.compact if arr
      elsif val
        rh[display_key] = target_map[val]
      end
    end
  end

  # Format dates to ISO8601 per standard protocol.
  format_dates!(resource, record_hashes)

  record_hashes
end