Module: TypedEAV::Partition

Defined in:
lib/typed_eav/partition.rb

Overview

Partition-aware visibility for schema objects keyed by the canonical (entity_type, scope, parent_scope) tuple.

This module is deliberately explicit: callers pass already-resolved scope values. Ambient resolution (TypedEAV.current_scope, with_scope, unscoped) stays with the adapters that know their calling context.

Class Method Summary collapse

Class Method Details

.definitions_by_name(defs) ⇒ Object

Indexes field definitions by name with deterministic three-way collision resolution: when global (scope=NULL, parent_scope=NULL), scope-only (scope set, parent_scope=NULL), and full-triple (both set) fields share a name, the most-specific row wins.

Sort key [scope.nil? ? 0 : 1, parent_scope.nil? ? 0 : 1] orders rows:

[0, 0] global              (least specific) -> comes first
[1, 0] scope-only          (middle)
[1, 1] full triple         (most specific)  -> comes last

index_by(&:name) keeps the LAST entry on duplicate keys (Rails convention via Array#to_h), so most-specific wins. The two-key sort extends the prior "scoped beats global" rule into "two-key beats one-key beats global" without changing the index_by-last-wins mechanism. The (scope=NULL, parent_scope=NOT NULL) slot is unreachable by construction (orphan-parent invariant in Field::Base), so the ordering is exhaustive across the three valid shapes.

Shared by the class-query path (FilterQuery / BulkRead / EntityQuery) and the instance path (HasTypedEAV::InstanceMethods#typed_eav_defs_by_name) so the two cannot drift. Lives on Partition because partition-tuple precedence is a partition concept.



71
72
73
74
75
# File 'lib/typed_eav/partition.rb', line 71

def definitions_by_name(defs)
  defs.to_a
      .sort_by { |d| [d.scope.nil? ? 0 : 1, d.parent_scope.nil? ? 0 : 1] }
      .index_by(&:name)
end

.definitions_multimap_by_name(defs) ⇒ Object

Indexes field definitions by name into a multi-map (one name -> array of fields). Used by the class-query path under TypedEAV.unscoped { }, where the same field name may legitimately exist across multiple tenant partitions and we must OR-across all matching field_ids per filter rather than collapse to a single row.



82
83
84
# File 'lib/typed_eav/partition.rb', line 82

def definitions_multimap_by_name(defs)
  defs.to_a.group_by(&:name)
end

.effective_fields_by_name(entity_type:, scope: nil, parent_scope: nil, mode: :partition) ⇒ Object

One visible field per name after collision resolution. Most-specific wins: full tuple beats scope-only, scope-only beats global.



40
41
42
43
44
45
46
47
# File 'lib/typed_eav/partition.rb', line 40

def effective_fields_by_name(entity_type:, scope: nil, parent_scope: nil, mode: :partition)
  fields = visible_fields(entity_type: entity_type, scope: scope, parent_scope: parent_scope, mode: mode)
  if mode == :all_partitions
    definitions_multimap_by_name(fields)
  else
    definitions_by_name(fields)
  end
end

.find_visible_section!(id, entity_type:, scope: nil, parent_scope: nil, mode: :partition) ⇒ TypedEAV::Section

Looks up a single Section by id constrained to the caller's partition tuple. Documented-public surface: apps building admin UIs that need to authorize a section lookup before editing, rendering, or destroying it should call this rather than Section.find(id), which would happily return a section belonging to another tenant's partition.

Visibility merge matches visible_sections: rows whose (entity_type, scope, parent_scope) is either the requested tuple or the global (scope: nil, parent_scope: nil) partition are eligible. The most-specific-wins precedence used for field collision resolution does not apply here — section lookup is by primary key, not by name.

Sibling documented-public methods on this module that share the same partition-visibility surface: visible_fields, effective_fields_by_name, definitions_by_name, definitions_multimap_by_name, visible_sections.

Parameters:

  • id (Integer, String)

    the section's primary key. Blank input is the caller's responsibility to guard upstream; this method does not silently swallow nil / "" — it forwards to ActiveRecord::Relation#find, which raises ActiveRecord::RecordNotFound on blank.

  • entity_type (String)

    the host AR class name the section belongs to (matches Section#entity_type).

  • scope (Object, nil) (defaults to: nil)

    the resolved scope value from the caller's partition. nil means the global partition only.

  • parent_scope (Object, nil) (defaults to: nil)

    the resolved parent_scope value. Must be nil when scope is blank (the orphan-parent invariant shared with visible_sections).

  • mode (Symbol) (defaults to: :partition)

    :partition (default) restricts to the caller's tuple plus the global tuple. :all_partitions is the deliberate admin bypass that ignores the tuple entirely — distinct from scope: nil, which means "global partition only."

Returns:

  • (TypedEAV::Section)

    the section record when it belongs to the caller's partition or the global tuple.

Raises:

  • (ActiveRecord::RecordNotFound)

    when the section's (scope, parent_scope) falls outside the visibility merge for the requested tuple, or when no section with id exists.

  • (ArgumentError)

    when parent_scope is present and scope is blank (orphan-parent), or when mode is neither :partition nor :all_partitions.



139
140
141
# File 'lib/typed_eav/partition.rb', line 139

def find_visible_section!(id, entity_type:, scope: nil, parent_scope: nil, mode: :partition)
  visible_sections(entity_type: entity_type, scope: scope, parent_scope: parent_scope, mode: mode).find(id)
end

.visible_fields(entity_type: nil, scope: nil, parent_scope: nil, mode: :partition) ⇒ Object

All field definitions visible from a tuple: pure global rows, scope-only rows, and full-tuple rows. Passing mode: :all_partitions is the deliberate admin bypass; it is distinct from scope: nil, which means the global partition only.

Raises:

  • (ArgumentError)


24
25
26
27
28
29
30
31
32
33
34
35
36
# File 'lib/typed_eav/partition.rb', line 24

def visible_fields(entity_type: nil, scope: nil, parent_scope: nil, mode: :partition)
  validate_mode!(mode)
  fields = TypedEAV::Field::Base.all
  fields = fields.where(entity_type: entity_type) if entity_type
  return fields if mode == :all_partitions

  raise ArgumentError, ORPHAN_PARENT_MESSAGE unless ScopeTuple.invariant_satisfied?(scope, parent_scope)

  fields.where(
    scope: [scope, nil].uniq,
    parent_scope: [parent_scope, nil].uniq,
  )
end

.visible_sections(entity_type:, scope: nil, parent_scope: nil, mode: :partition) ⇒ Object

All sections visible from the same tuple as field definitions.

Raises:

  • (ArgumentError)


87
88
89
90
91
92
93
94
# File 'lib/typed_eav/partition.rb', line 87

def visible_sections(entity_type:, scope: nil, parent_scope: nil, mode: :partition)
  validate_mode!(mode)
  return TypedEAV::Section.where(entity_type: entity_type) if mode == :all_partitions

  raise ArgumentError, ORPHAN_PARENT_MESSAGE unless ScopeTuple.invariant_satisfied?(scope, parent_scope)

  TypedEAV::Section.for_entity(entity_type, scope: scope, parent_scope: parent_scope)
end