Module: Familia::Features::Relationships

Defined in:
lib/familia/features/relationships.rb,
lib/familia/features/relationships/indexing.rb,
lib/familia/features/relationships/querying.rb,
lib/familia/features/relationships/tracking.rb,
lib/familia/features/relationships/cascading.rb,
lib/familia/features/relationships/membership.rb,
lib/familia/features/relationships/score_encoding.rb,
lib/familia/features/relationships/redis_operations.rb,
lib/familia/features/relationships/permission_management.rb

Overview

Unified Relationships feature for Familia v2

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

Key improvements in v2: - Multi-presence: Objects can exist in multiple collections simultaneously - Score encoding: Metadata embedded in Redis scores for efficiency - Collision-free: Method names include collection names to prevent conflicts - Redis-native: All operations use Redis commands, no Ruby iteration - Atomic operations: Multi-collection updates happen atomically

Breaking changes from v1: - Single feature: Use feature :relationships instead of separate features - Simplified identifier: Use identifier :field instead of identifier_field :field - No ownership concept: Remove owned_by, use multi-presence instead - Method naming: Generated methods include collection names for uniqueness - Score encoding: Scores can carry metadata like permissions

Examples:

Basic usage

class Domain < Familia::Horreum
  feature :relationships

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

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

  # O(1) lookups with Redis hashes
  indexed_by :display_name, in: Customer, index_name: :domain_index
  indexed_by :display_name, in: :global, index_name: :global_domain_index

  # Context-aware membership (no method collisions)
  member_of Customer, :domains
  member_of Team, :domains
  member_of Organization, :domains
end

Generated methods (collision-free)

# Tracking 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
Domain.find_by_display_name_globally(name) # Global lookup

# Membership 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(Time.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(Time.now, :read) }
], :add, domain.identifier)

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

Defined Under Namespace

Modules: Cascading, ClassMethods, Indexing, InstanceMethods, Membership, PermissionManagement, Querying, RedisOperations, ScoreEncoding, Tracking Classes: CascadeError, InvalidIdentifierError, InvalidScoreError, RelationshipError

Class Method Summary collapse

Class Method Details

.included(base) ⇒ Object

Feature initialization



102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
# File 'lib/familia/features/relationships.rb', line 102

def self.included(base)
  puts "[DEBUG] Relationships included in #{base}"
  base.extend ClassMethods
  base.include InstanceMethods

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

  puts '[DEBUG] Including Tracking module'
  base.include Tracking
  puts '[DEBUG] Extending with Tracking::ClassMethods'
  base.extend Tracking::ClassMethods
  puts "[DEBUG] Base now responds to tracked_in: #{base.respond_to?(:tracked_in)}"

  base.include Indexing
  base.extend Indexing::ClassMethods

  base.include Membership
  base.extend Membership::ClassMethods

  base.include Cascading
  base.extend Cascading::ClassMethods

  base.include Querying
  base.extend Querying::ClassMethods
end