Module: Models::Lineage::ClassMethods

Defined in:
app/concerns/models/lineage.rb

Overview

ActiveSupport::Concern mixin: class methods.

Belongs to collapse

Has many collapse

Instance Method Summary collapse

Instance Method Details

#acts_as_lineage(options = {}) ⇒ void

This method returns an undefined value.

Sets up a self-referential parent/children tree (belongs_to :parent,
has_many :children) plus lineage scopes and class helpers.

Parameters:

  • options (Hash) (defaults to: {})

    configuration overrides for the tree

Options Hash (options):

  • foreign_key (Symbol)

    column holding the parent id (default: :parent_id)

  • order (Symbol, String, nil)

    ordering applied to the children association and root scopes

  • counter_cache (Symbol, Boolean, nil)

    counter cache column on the parent (default: nil)

  • dependent (Symbol)

    dependent strategy for children (default: :destroy)



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
73
# File 'app/concerns/models/lineage.rb', line 25

def acts_as_lineage(options = {})
  configuration = {
    foreign_key: :parent_id,
    order: nil,
    counter_cache: nil,
    dependent: :destroy
  }

  configuration.update(options) if options.is_a?(Hash)

  # The parent node of this record.
  # @return [ApplicationRecord, nil] the parent node
  belongs_to :parent,
             class_name: name,
             foreign_key: configuration[:foreign_key],
             counter_cache: configuration[:counter_cache],
             inverse_of: :children,
             optional: true

  # The direct child nodes of this record.
  # @return [ActiveRecord::Relation] the child nodes
  has_many :children,
           -> { order(configuration[:order]) },
           class_name: name,
           foreign_key: configuration[:foreign_key],
           dependent: configuration[:dependent],
           inverse_of: :parent

  # NOTE: scopes moved to class_eval to use configured foreign_key
  class_eval %{

    def self.roots
      order_option = "#{configuration.dig(:order)}"
      r = where(#{configuration[:foreign_key]}: nil)
      r = r.order(order_option) if order_option.present?
      r
    end

    def self.root
      self.roots.first
    end

    scope :parents_only, -> { where(#{configuration[:foreign_key]}: nil) }
    scope :children_only, -> { where.not(#{configuration[:foreign_key]}: nil) }
    scope :descendants_lin, ->(pid) { where(id: self_and_descendants_ids(pid) ) }
    scope :ancestors_lin, ->(pid) { where(id: self_and_ancestors_ids(pid)) }
    scope :descendants_only, ->(pid) { where(id: descendants_ids(pid)) }
  }, __FILE__, __LINE__ - 18
end

#ancestors_ids(*ids) ⇒ Array<Integer>

Returns the ids of all ancestors of the given records, at any depth.

Parameters:

  • ids (Array<Integer>)

    ids of the records to start from

Returns:

  • (Array<Integer>)

    ancestor ids, excluding the given ids



106
107
108
109
110
111
112
113
114
115
116
117
118
119
# File 'app/concerns/models/lineage.rb', line 106

def ancestors_ids(*ids)
  ids = [ids].flatten.filter_map(&:presence).uniq

  return [] if ids.blank?

  foreign_key = reflect_on_all_associations.find { |a| a.name == :children }.foreign_key
  sql = Arel.sql("with recursive t(level,#{foreign_key},id) as (select 0,#{foreign_key},id from #{table_name} where id IN (#{ids.join(',')}) union select t.level+1,c.#{foreign_key},c.id from #{table_name} c join t on c.id = t.#{foreign_key}) select id from t where id not in (#{ids.join(',')});")
  res = ActiveRecord::Base.lease_connection.execute(sql)
  begin
    res.map { |r| r['id'].to_i }.filter_map(&:presence).uniq
  rescue StandardError
    []
  end
end

#childrenActiveRecord::Relation, ActiveRecord::Relation<Child>

The direct child nodes of this record.

Returns:

  • (ActiveRecord::Relation)

    the child nodes

  • (ActiveRecord::Relation<Child>)

See Also:



46
47
48
49
50
51
# File 'app/concerns/models/lineage.rb', line 46

has_many :children,
-> { order(configuration[:order]) },
class_name: name,
foreign_key: configuration[:foreign_key],
dependent: configuration[:dependent],
inverse_of: :parent

#descendants_ids(*ids) ⇒ Array<Integer>

Returns the ids of all descendants of the given records, at any depth.

Parameters:

  • ids (Array<Integer>)

    ids of the records to start from

Returns:

  • (Array<Integer>)

    descendant ids, excluding the given ids



79
80
81
82
83
84
85
86
87
88
89
90
91
92
# File 'app/concerns/models/lineage.rb', line 79

def descendants_ids(*ids)
  ids = [ids].flatten.filter_map(&:presence).uniq

  return [] if ids.blank?

  foreign_key = reflect_on_all_associations.find { |a| a.name == :children }.foreign_key
  sql = Arel.sql("with recursive t(level,#{foreign_key},id) as (select 0,#{foreign_key},id from #{table_name} where #{foreign_key} IN (#{ids.join(',')}) union select t.level+1,c.#{foreign_key},c.id from #{table_name} c join t on c.#{foreign_key} = t.id) select distinct id from t order by id;")
  res = ActiveRecord::Base.lease_connection.execute(sql)
  begin
    res.map { |r| r['id'].to_i }.filter_map(&:presence).uniq
  rescue StandardError
    []
  end
end

#generate_order_by(*ids) ⇒ Arel::Nodes::SqlLiteral

Builds an ORDER BY fragment that sorts records in the order of the given ids.

Parameters:

  • ids (Array<Integer>)

    ids in the desired order

Returns:

  • (Arel::Nodes::SqlLiteral)

    SQL fragment ordering by id position



160
161
162
# File 'app/concerns/models/lineage.rb', line 160

def generate_order_by(*ids)
  Arel.sql([ids].flatten.map(&:presence).uniq.compact.map { |i| "#{table_name}.id = #{i} desc" }.join(','))
end

#parentApplicationRecord, ...

The parent node of this record.

Returns:

See Also:



37
38
39
40
41
42
# File 'app/concerns/models/lineage.rb', line 37

belongs_to :parent,
class_name: name,
foreign_key: configuration[:foreign_key],
counter_cache: configuration[:counter_cache],
inverse_of: :children,
optional: true

#root_ids(*ids) ⇒ Array<Integer>

Returns the ids of the root records of the trees containing the given records.

Parameters:

  • ids (Array<Integer>)

    ids of the records to start from

Returns:

  • (Array<Integer>)

    root ids



125
126
127
128
129
130
131
132
133
134
135
136
137
138
# File 'app/concerns/models/lineage.rb', line 125

def root_ids(*ids)
  ids = ids.flatten.filter_map(&:presence).uniq

  return [] if ids.blank?

  foreign_key = reflect_on_all_associations.find { |a| a.name == :children }.foreign_key
  sql = Arel.sql("with recursive t(level,parent_id,id) as (select 0,#{foreign_key},id from #{table_name} where id IN (#{ids.join(',')}) union select t.level + 1,c.#{foreign_key},c.id from #{table_name} c join t on c.id = t.parent_id) select id from t where t.parent_id IS NULL;")
  res = ActiveRecord::Base.lease_connection.execute(sql)
  begin
    res.map { |r| r['id'].to_i }.filter_map(&:presence).uniq
  rescue StandardError
    []
  end
end

#self_ancestors_and_descendants_ids(*ids) ⇒ Array<Integer>

Returns the given ids plus the ids of all their ancestors and descendants.

Parameters:

  • ids (Array<Integer>)

    ids of the records to start from

Returns:

  • (Array<Integer>)

    the given ids, their ancestor ids, and their descendant ids



152
153
154
# File 'app/concerns/models/lineage.rb', line 152

def self_ancestors_and_descendants_ids(*ids)
  (ancestors_ids(ids) + ids + descendants_ids(ids)).flatten.filter_map(&:presence).uniq
end

#self_and_ancestors_ids(*ids) ⇒ Array<Integer>

Returns the given ids plus the ids of all their ancestors.

Parameters:

  • ids (Array<Integer>)

    ids of the records to start from

Returns:

  • (Array<Integer>)

    the given ids and their ancestor ids



144
145
146
# File 'app/concerns/models/lineage.rb', line 144

def self_and_ancestors_ids(*ids)
  (ancestors_ids(ids) + ids).flatten.filter_map(&:presence).uniq
end

#self_and_descendants_ids(*ids) ⇒ Array<Integer>

Returns the given ids plus the ids of all their descendants.

Parameters:

  • ids (Array<Integer>)

    ids of the records to start from

Returns:

  • (Array<Integer>)

    the given ids and their descendant ids



98
99
100
# File 'app/concerns/models/lineage.rb', line 98

def self_and_descendants_ids(*ids)
  (ids + descendants_ids(ids)).flatten.filter_map(&:presence).uniq
end