Module: WithRecursiveTree::ClassMethods

Defined in:
lib/with_recursive_tree.rb

Instance Method Summary collapse

Instance Method Details

#with_recursive_tree(primary_key: :id, foreign_key: :parent_id, foreign_key_type: nil, order: nil) ⇒ Object



5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
# File 'lib/with_recursive_tree.rb', line 5

def with_recursive_tree(primary_key: :id, foreign_key: :parent_id, foreign_key_type: nil, order: nil)
  include InstanceMethods

  scope_condition = foreign_key_type.present? ? -> { where foreign_key_type => self.class.name } : -> { self }

  belongs_to :parent, scope_condition, class_name: name, primary_key: primary_key, foreign_key: foreign_key, inverse_of: :children, optional: true

  has_many :children, -> { scope_condition.call.order(order) }, class_name: name, primary_key: primary_key, foreign_key: foreign_key, inverse_of: :parent

  define_singleton_method(:with_recursive_tree_primary_key) { primary_key }
  define_singleton_method(:with_recursive_tree_foreign_key) { foreign_key }
  define_singleton_method(:with_recursive_tree_foreign_key_type) { foreign_key_type }
  define_singleton_method(:with_recursive_tree_order) { order || primary_key }
  define_singleton_method(:with_recursive_tree_order_column) do
    if with_recursive_tree_order.is_a?(Hash)
      with_recursive_tree_order.keys.first
    else
      with_recursive_tree_order.to_s.split(" ").first
    end
  end

  if foreign_key_type.present?
    before_save do
      if send(:"#{foreign_key}_changed?")
        if send(foreign_key).present?
          # When setting a parent, the foreign_key_type is automatically set to the model's class name if it's blank
          send(:"#{foreign_key_type}=", self.class.name) if send(foreign_key_type).blank?
        elsif send(foreign_key).nil?
          # When clearing parent, the foreign_key_type is to nil unless it's explicitly set to the model's class name
          send(:"#{foreign_key_type}=", nil) unless send(foreign_key_type) == self.class.name
        end
      end
    end
  end

  scope :bfs, -> {
    if defined?(ActiveRecord::ConnectionAdapters::MySQL)
      order(:depth, with_recursive_tree_order)
    else
      order(:depth)
    end
  }
  scope :dfs, -> do
    if defined?(ActiveRecord::ConnectionAdapters::MySQL)
      order(with_recursive_tree_order, :path)
    elsif defined?(ActiveRecord::ConnectionAdapters::PostgreSQL)
      order(:path)
    elsif defined?(ActiveRecord::ConnectionAdapters::SQLite3)
      self
    end
  end
  scope :roots, -> {
    if with_recursive_tree_foreign_key_type.present?
      # Root conditions with foreign_key_type:
      # 1. foreign_key is nil AND foreign_key_type is nil
      # 2. foreign_key is nil AND foreign_key_type matches the model's class name
      # 3. foreign_key is not nil AND foreign_key_type is different from model's class name
      where(with_recursive_tree_foreign_key => nil)
        .where(with_recursive_tree_foreign_key_type => [nil, name])
        .or(
          where.not(with_recursive_tree_foreign_key => nil)
            .where.not(with_recursive_tree_foreign_key_type => name)
        )
    else
      where with_recursive_tree_foreign_key => nil
    end
  }
end