$LOAD_PATH.unshift File.expand_path('../lib', __dir__)
require 'familia'
Familia.configure do |config|
config.uri = ENV.fetch('REDIS_URL', 'redis://localhost:2525/')
end
puts '=== Familia Through Relationships Example ==='
puts
puts 'The :through option creates join models between participants and targets,'
puts 'similar to has_many :through in ActiveRecord but for Redis.'
puts
class Membership < Familia::Horreum
logical_database 15
feature :object_identifier feature :relationships
identifier_field :objid
field :organization_objid
field :user_objid
field :role field :permissions field :invited_by field :invited_at field :joined_at field :updated_at end
class Organization < Familia::Horreum
logical_database 15
feature :object_identifier
feature :relationships
identifier_field :objid field :name
field :plan
end
class User < Familia::Horreum
logical_database 15
feature :object_identifier
feature :relationships
identifier_field :objid field :email
field :name
participates_in Organization, :members,
score: -> { Familia.now.to_f },
through: :Membership
end
puts '=== 1. Model Setup ==='
puts
puts 'Through model (Membership) requirements:'
puts ' • feature :object_identifier - enables deterministic keys'
puts ' • Fields for foreign keys: organization_objid, user_objid'
puts ' • Additional fields: role, permissions, invited_by, etc.'
puts
puts 'Participant (User) declaration:'
puts ' participates_in Organization, :members,'
puts ' score: -> { Familia.now.to_f },'
puts ' through: :Membership'
puts
puts '=== 2. Creating Objects ==='
org = Organization.new(name: 'Acme Corp', plan: 'enterprise')
org.save
puts "Created organization: #{org.name} (#{org.objid})"
alice = User.new(email: 'alice@acme.com', name: 'Alice')
alice.save
puts "Created user: #{alice.name} (#{alice.objid})"
bob = User.new(email: 'bob@acme.com', name: 'Bob')
bob.save
puts "Created user: #{bob.name} (#{bob.objid})"
charlie = User.new(email: 'charlie@acme.com', name: 'Charlie')
charlie.save
puts "Created user: #{charlie.name} (#{charlie.objid})"
puts
puts '=== 3. Adding Members with Roles ==='
puts
membership_alice = org.add_members_instance(alice, through_attrs: {
role: 'owner',
permissions: 'all',
joined_at: Familia.now.to_f,
})
puts "Added #{alice.name} as #{membership_alice.role}"
puts " Membership objid: #{membership_alice.objid}"
puts " Membership class: #{membership_alice.class.name}"
puts
membership_bob = org.add_members_instance(bob, through_attrs: {
role: 'admin',
permissions: 'read,write,invite',
invited_by: alice.objid,
invited_at: Familia.now.to_f,
joined_at: Familia.now.to_f,
})
puts "Added #{bob.name} as #{membership_bob.role}"
puts " Invited by: #{membership_bob.invited_by}"
puts
membership_charlie = org.add_members_instance(charlie, through_attrs: {
role: 'member',
permissions: 'read',
invited_by: bob.objid,
invited_at: Familia.now.to_f,
joined_at: Familia.now.to_f,
})
puts "Added #{charlie.name} as #{membership_charlie.role}"
puts
puts '=== 4. Querying Memberships ==='
puts
puts "Organization #{org.name} has #{org.members.size} members:"
org.members.members.each do |user_id|
puts " - #{user_id}"
end
puts
puts "Is Alice a member? #{alice.in_organization_members?(org)}"
puts "Is Bob a member? #{bob.in_organization_members?(org)}"
puts
membership_key = "organization:#{org.objid}:user:#{alice.objid}:membership"
loaded_membership = Membership.load(membership_key)
puts 'Direct membership lookup for Alice:'
puts " Key: #{membership_key}"
puts " Role: #{loaded_membership.role}"
puts " Permissions: #{loaded_membership.permissions}"
puts " Updated at: #{Time.at(loaded_membership.updated_at.to_f)}"
puts
puts '=== 5. Updating Membership (Idempotent) ==='
puts
puts "Promoting #{charlie.name} from member to admin..."
updated_membership = org.add_members_instance(charlie, through_attrs: {
role: 'admin',
permissions: 'read,write',
})
puts " New role: #{updated_membership.role}"
puts " Same objid? #{updated_membership.objid == membership_charlie.objid}"
puts ' (Idempotent: updates existing, no duplicates)'
puts
puts '=== 6. Removing Members ==='
puts
puts "Removing #{charlie.name} from organization..."
org.remove_members_instance(charlie)
puts " Is Charlie still a member? #{charlie.in_organization_members?(org)}"
removed_key = "organization:#{org.objid}:user:#{charlie.objid}:membership"
removed_membership = Membership.load(removed_key)
puts " Membership record exists? #{removed_membership&.exists? || false}"
puts
puts '=== 7. Backward Compatibility ==='
puts
class Project < Familia::Horreum
logical_database 15
feature :object_identifier
feature :relationships
identifier_field :objid
field :name
end
class Task < Familia::Horreum
logical_database 15
feature :object_identifier
feature :relationships
identifier_field :objid
field :title
participates_in Project, :tasks, score: -> { Familia.now.to_f }
end
project = Project.new(name: 'Website Redesign')
project.save
task = Task.new(title: 'Design mockups')
task.save
result = project.add_tasks_instance(task)
puts "Without :through, add returns: #{result.class.name}"
puts ' (Returns the target, not a through model)'
puts
puts '=== 8. Cleanup ==='
[org, alice, bob, charlie, project, task].each do |obj|
obj.destroy! if obj&.exists?
end
puts 'Cleaned up all test objects'
puts