Module: ActiveCypher::Associations

Extended by:
ActiveSupport::Concern
Defined in:
lib/active_cypher/associations.rb,
lib/active_cypher/associations/collection_proxy.rb

Overview

Note:

Because every DSL wants to be ActiveRecord when it grows up.

Module to handle association definitions (has_many, belongs_to, etc.) for ActiveCypher models.

Defined Under Namespace

Classes: CollectionProxy

Class Method Summary collapse

Class Method Details

.define_has_many_through_reader(reflection) ⇒ Object

Defines the reader method for a has_many :through association. Because sometimes you want to join tables, but with extra steps.



456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
# File 'lib/active_cypher/associations.rb', line 456

def self.define_has_many_through_reader(reflection)
  name = reflection[:name]
  through_association_name = reflection[:through]
  source_association_name = reflection[:source] || name # Default source is same name on intermediate model

  define_method(name) do
    raise ActiveCypher::PersistenceError, 'Association load attempted on unsaved record' unless persisted?

    # 1. Get reflection for the intermediate association (e.g., :friendships)
    through_reflection = self.class._reflections[through_association_name]
    unless through_reflection
      raise ArgumentError,
            "Could not find association '#{through_association_name}' specified in :through option for '#{name}'"
    end

    intermediate_class = through_reflection[:class_name].constantize

    # 2. Get reflection for the source association on the intermediate model (e.g., :to_node on Friendship)
    # Note: This assumes the intermediate model also uses ActiveCypher::Associations
    source_reflection = intermediate_class._reflections[source_association_name]
    unless source_reflection
      raise ArgumentError,
            "Could not find association '#{source_association_name}' specified as :source (or inferred) on '#{intermediate_class.name}' for '#{name}'"
    end

    final_target_class = source_reflection[:class_name].constantize

    # 3. Build the multi-hop Cyrel query.
    # Because why settle for one hop when you can have two and still not get what you want?
    start_node_alias = :start_node
    intermediate_node_alias = :intermediate_node
    final_target_node_alias = :final_target

    # Start node pattern
    start_node_pattern = Cyrel.node(self.class.label_name).as(start_node_alias)
                              .where(Cyrel.node_id(start_node_alias).eq(internal_id))

    # Intermediate node pattern (based on through_reflection)
    intermediate_node_pattern = Cyrel.node(intermediate_class.label_name).as(intermediate_node_alias)
    through_rel_type = through_reflection[:relationship]
    through_direction = through_reflection[:direction]

    first_hop_pattern = case through_direction
                        when :out then start_node_pattern.rel(:out, through_rel_type).to(intermediate_node_pattern)
                        when :in then intermediate_node_pattern.rel(:out, through_rel_type).to(start_node_pattern)
                        when :both then start_node_pattern.rel(:both,
                                                               through_rel_type).to(intermediate_node_pattern)
                        else raise ArgumentError, "Invalid direction in through_reflection: #{through_direction}"
                        end

    # Final target node pattern (based on source_reflection)
    final_target_node_pattern = Cyrel.node(final_target_class.label_name).as(final_target_node_alias)
    source_rel_type = source_reflection[:relationship]
    source_direction = source_reflection[:direction]

    second_hop_pattern = case source_direction
                         when :out then intermediate_node_pattern.rel(:out,
                                                                      source_rel_type).to(final_target_node_pattern)
                         when :in then final_target_node_pattern.rel(:out,
                                                                     source_rel_type).to(intermediate_node_pattern)
                         when :both then intermediate_node_pattern.rel(:both,
                                                                       source_rel_type).to(final_target_node_pattern)
                         else raise ArgumentError, "Invalid direction in source_reflection: #{source_direction}"
                         end

    # Combine patterns and return final target
    # Assuming Cyrel allows chaining matches or building complex patterns
    # This might need adjustment based on Cyrel's exact path-building API
    query = Cyrel.match(first_hop_pattern)
                 .match(second_hop_pattern) # Assumes .match adds to the pattern
                 .return(final_target_node_alias)
    # TODO: Add DISTINCT if needed? .return(Cyrel.distinct(final_target_node_alias))

    # Return a Relation scoped to the final target class
    Relation.new(final_target_class, query)
  end
end