Module: Familia::Features::Relationships

Defined in:
lib/familia/features/relationships.rb,
lib/familia/features/relationships/indexing.rb,
lib/familia/features/relationships/participation.rb,
lib/familia/features/relationships/score_encoding.rb,
lib/familia/features/relationships/collection_operations.rb,
lib/familia/features/relationships/indexing_relationship.rb,
lib/familia/features/relationships/participation_relationship.rb,
lib/familia/features/relationships/participation/target_methods.rb,
lib/familia/features/relationships/indexing/multi_index_generators.rb,
lib/familia/features/relationships/indexing/unique_index_generators.rb,
lib/familia/features/relationships/participation/participant_methods.rb

Overview

Unified Relationships feature for Familia v2

This feature merges the functionality of relatable_objects and relationships into a single, Valkey/Redis-native implementation that embraces the "where does this appear?" philosophy rather than "who owns this?".

Examples:

Basic usage

class Domain < Familia::Horreum

  identifier_field :domain_id

  field :domain_id
  field :display_name
  field :created_at
  field :permission_bits

  feature :relationships

  # Multi-presence participation with score encoding
  participates_in Customer, :domains,
                  score: -> { permission_encode(created_at, permission_bits) }
  participates_in Team, :domains, score: :added_at
  participates_in Organization, :all_domains, score: :created_at

  # O(1) lookups with Valkey/Redis hashes
  indexed_by :display_name, :domain_index, target: Customer

  # Participation with bidirectional control (no method collisions)
  participates_in Customer, :domains
  participates_in Team, :domains, bidirectional: false
  participates_in Organization, :domains, type: :set
end

Generated methods (collision-free)

# Participation methods
Customer.domains                    # => Familia::SortedSet
Customer.add_domain(domain, score)  # Add to customer's domains
domain.in_customer_domains?(customer) # Check membership

# Indexing methods
Customer.find_by_display_name(name) # O(1) lookup

# Bidirectional methods (collision-free naming)
domain.add_to_customer_domains(customer)  # Specific collection
domain.add_to_team_domains(team)          # Different collection
domain.in_customer_domains?(customer)     # Check specific membership

Score encoding for permissions

# Encode permission in score
score = domain.permission_encode(Familia.now, :write)
# => 1704067200.004 (timestamp + permission bits)

# Decode permission from score
decoded = domain.permission_decode(score)
# => { timestamp: 1704067200, permissions: 4, permission_list: [:write] }

# Query with permission filtering
Customer.domains_with_permission(:read)

Multi-collection operations

# Atomic updates across multiple collections
domain.update_multiple_presence([
  { key: "customer:123:domains", score: current_score },
  { key: "team:456:domains", score: permission_encode(Familia.now, :read) }
], :add, domain.identifier)

# UnsortedSet operations on collections
accessible = Domain.union_collections([
  { owner: customer, collection: :domains },
  { owner: team, collection: :domains }
], min_permission: :read)

Defined Under Namespace

Modules: CollectionOperations, Indexing, ModelClassMethods, ModelInstanceMethods, ParticipantMethods, Participation, ScoreEncoding, TargetMethods Classes: CascadeError, InvalidIdentifierError, InvalidScoreError, RelationshipError

Constant Summary collapse

IndexingRelationship =

IndexingRelationship

Stores metadata about indexing relationships defined at class level. Used to configure code generation and runtime behavior for unique_index and multi_index declarations.

Similar to ParticipationRelationship but for attribute-based lookups rather than collection membership.

Data.define(
  :field,              # Symbol - field being indexed (e.g., :email, :department)
  :index_name,         # Symbol - name of the index (e.g., :email_index, :dept_index)
  :target_class,       # Class/Symbol - parent class for instance-scoped indexes (within:)
  :cardinality,        # Symbol - :unique (1:1) or :multi (1:many)
  :query               # Boolean - whether to generate query  methods
) do
  #
  # Get the normalized config name for the target class
  #
  # @return [String] The config name (e.g., "user", "company", "test_company")
  #
  def target_class_config_name
    target_class.config_name
  end
end
ParticipationRelationship =

ParticipationRelationship

Stores metadata about participation relationships defined at class level. Used to configure code generation and runtime behavior for participates_in and class_participates_in declarations.

Data.define(
  :target_class,        # Class object that owns the collection
  :collection_name,     # Symbol name of the collection (e.g., :members, :domains)
  :score,               # Proc/Symbol/nil - score calculator for sorted sets
  :type,                # Symbol - collection type (:sorted_set, :set, :list)
  :bidirectional, # Boolean - whether to generate reverse methods
) do
  #
  # Get the normalized config name for the target class
  #
  # @return [String] The config name (e.g., "user", "perf_test_customer")
  #
  def target_class_config_name
    target_class.config_name
  end
end

Class Method Summary collapse

Class Method Details

.included(base) ⇒ Object

Feature initialization



86
87
88
89
90
91
92
93
94
95
96
97
98
99
# File 'lib/familia/features/relationships.rb', line 86

def self.included(base)
  Familia.ld "[#{base}] Relationships included"
  base.extend ModelClassMethods
  base.include ModelInstanceMethods

  # Include all relationship submodules and their class methods
  base.include ScoreEncoding

  base.include Participation
  base.extend Participation::ModelClassMethods

  base.include Indexing
  base.extend Indexing::ModelClassMethods
end