Module: Hecks::Adapters::InMemoryOrdering

Defined in:
lib/hecks/adapters/driven/in_memory_ordering.rb

Overview

SHARED BY every adapter that holds decoded Ruby records rather than running real SQL (Memory, Lambda, Heki) — the identical dotted-path value-object member-picking Postgres/Sqlite/D1's own order_expression does (numeric member wins, else the one-field convention "value"), just walked in Ruby instead of compiled to a JSONB/json_extract path, because there is no query engine underneath any of these three to hide that walk inside.

Class Method Summary collapse

Class Method Details

.ordered(records, aggregate:, order_by:, direction:) ⇒ Object

order_by IS A RUNTIME VALUE (an HTTP query param, in the console's case), not framework-authored bluebook source — see postgres.rb's own all for the full reasoning. Whitelisted the identical way before FieldPath.dig ever runs.



23
24
25
26
27
28
29
30
31
32
33
34
# File 'lib/hecks/adapters/driven/in_memory_ordering.rb', line 23

def ordered(records, aggregate:, order_by:, direction:)
  return records unless order_by

  name = order_by.to_s.split(".").first
  raise Runtime::WiringError, "#{aggregate.name} has no attribute #{order_by.inspect} to order by" unless aggregate.lifecycle&.field.to_s == name || aggregate.attribute(name)

  path = sortable_path(aggregate, order_by)
  spec = QuerySpecification::Common::OrderBy.new(field: order_by, direction: direction)
  Ports::Query::Ordering.apply(records, spec, nil, identity: ->(record) { record.id.to_s }) do |record|
    Ports::Query::InMemory.comparable(QuerySpecification::FieldPath.dig(record, path))
  end
end

.sortable_path(aggregate, field) ⇒ Object



36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
# File 'lib/hecks/adapters/driven/in_memory_ordering.rb', line 36

def sortable_path(aggregate, field)
  name, *path = field.to_s.split(".")
  return field.to_s unless path.empty?

  attribute = aggregate.attribute(name)
  return field.to_s if aggregate.lifecycle&.field.to_s == name || attribute.nil?

  vo = aggregate.value_object(attribute.type)
  return field.to_s unless vo

  # Numeric member first, then the SOLE attribute whatever it is
  # named (single-attribute value objects strictly answer `.value`
  # — the same generalization `SqlQueryBuilder#query_expression`
  # makes for the column side, kept in lockstep so Memory and SQL
  # order the identical rows identically), and only then the bare
  # `value` convention — now purely a backstop for the multi-field
  # non-numeric shape neither rule can honestly pick a field for.
  member = vo.attributes.find { |a| %w[Integer Float].include?(a.type) }&.name ||
           vo.sole_attribute&.name || "value"
  "#{name}.#{member}"
end