with_recursive_tree 0.3.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 108a3060c431b14be0274538083b1251da56c3c5404ff96628007c849a86b94c
4
- data.tar.gz: 6d3619942217a168e4eb8bfc04533b5aa78edd6e0bf428eac9cd628007a47582
3
+ metadata.gz: 135e72700b84ad5ba2129e6b2e6afe8ab4ada838d83f696085e1e0fb0c33bb86
4
+ data.tar.gz: bf922dd155ba9cf2cea2833fce5ebf2cec06a68043eb0b5866259363065f22df
5
5
  SHA512:
6
- metadata.gz: be39718928d3296a0dcbd9e2d149a6d8c7dcb3fece985620b433a4e45f5f83a4bdcc3e87f8b6840b2b6194d6428b476ba260a49c874754454622bc88811d4b06
7
- data.tar.gz: 7e66c836eced475a35d398a4bceb47cd1ca441178b823b5be7c82d3a47534dd9e8ff1b9fd3842e72c1c288233c4a2c40b0703c7bdaad2e75e0273d551268ebfb
6
+ metadata.gz: 49af47e2ede3b23bb0063ca1ee6082580c0e84b71d7974b2d5fbb5d7aacdfbb5510b49a49f2242bb6377956a73ad42435065dc95a7d11ce6f37796ca354b12c6
7
+ data.tar.gz: 7def7190b8c6540a1e655ddadf63be9611239e45c8ea046766a6be9dacc360a55aaf880c37daa521f72b1cdf8ad43fa95721dc129fb5ab5521094574dbd8de99
data/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # with_recursive_tree
2
2
 
3
- Tree structures for ActiveRecord using CTE (Common Table Expressions). Traverse the whole tree with just one query.
3
+ Tree structures for ActiveRecord using CTEs (Common Table Expressions). Traverse an entire tree with just one query.
4
4
 
5
5
  ## Installation
6
6
 
@@ -18,7 +18,7 @@ $ bundle
18
18
 
19
19
  ## Usage
20
20
 
21
- First, your model needs a reference to the its parent. Typically, this is a `parent_id` column in your table. Once you have that reference, you can add `with_recursive_tree` to your model:
21
+ First, your model needs a reference to its parent. Typically, this is a `parent_id` column in your table. Once you have that reference, you can add `with_recursive_tree` to your model:
22
22
 
23
23
  ```ruby
24
24
  class Category < ApplicationRecord
@@ -28,10 +28,10 @@ end
28
28
 
29
29
  By doing this, with_recursive_tree will add 2 associations:
30
30
 
31
- * `parent`: the parent of the node
32
- * `children`: the children of this node
31
+ * `parent`: the node's parent
32
+ * `children`: the node's children
33
33
 
34
- To build these associations, with_recursive_tree will use the `id` and the `parent_id` columns as the primary and foreign keys, respectively. If you want to specify different primary and foreign keys, you can do that by passing the `primary_key` and `foreign_key` options. For example, for a categories table whose primary key is `category_id` and the parent record id is `parent_category_id`, you would set it up as follows:
34
+ To build these associations, with_recursive_tree will use the `id` and the `parent_id` columns as the primary and foreign keys, respectively. If you want to specify different primary and foreign keys, you can specify them by passing the `primary_key` and `foreign_key` options. For example, for a categories table whose primary key is `category_id` and the parent record id is `parent_category_id`, you would set it up as follows:
35
35
 
36
36
  ```ruby
37
37
  class Category < ApplicationRecord
@@ -39,6 +39,27 @@ class Category < ApplicationRecord
39
39
  end
40
40
  ```
41
41
 
42
+ For polymorphic associations where a node can have different types of parents, you can use the `foreign_key_type` option to specify a column that stores the parent model's class name:
43
+
44
+ ```ruby
45
+ class Comment < ApplicationRecord
46
+ with_recursive_tree foreign_key: :parent_id, foreign_key_type: :parent_type
47
+ end
48
+ ```
49
+
50
+ This allows nodes to belong to different parent model types while maintaining proper tree structure constraints.
51
+
52
+ When using `foreign_key_type`, a node is considered a root if it meets any of these conditions:
53
+
54
+ 1. Both `foreign_key` and `foreign_key_type` are `nil` (traditional root)
55
+ 2. `foreign_key` is `nil` and `foreign_key_type` matches the model's class name
56
+ 3. `foreign_key` is not `nil` but `foreign_key_type` is different from the model's class name (belongs to a different model type)
57
+
58
+ The `foreign_key_type` value is automatically managed through a `before_save` callback:
59
+
60
+ * When setting a parent (`foreign_key`), the `foreign_key_type` is automatically set to the model's class name if it's blank
61
+ * When clearing a parent (`foreign_key` becomes `nil`), the `foreign_key_type` is set to `nil` unless it's explicitly set to the model's class name
62
+
42
63
  Lastly, you can specify how to sort each node's `children` by passing the `order` option to `with_recursive_tree`. If no `order` option is set, it will default to `id`. This option is useful especially when you need to traverse the tree in a specific order. For example:
43
64
 
44
65
  ```ruby
@@ -49,24 +70,24 @@ end
49
70
 
50
71
  ### Class methods
51
72
 
52
- | Method | Description |
53
- |--------|-------------|
73
+ | Method | Description |
74
+ | --------- | ----------------------------------------- |
54
75
  | `::roots` | Returns all roots (nodes without parent). |
55
76
 
56
77
  ### Instance methods
57
78
 
58
- | Method | Description |
59
- |--------|-------------|
60
- | `#ancestors` | Returns all ancestors of the node. |
61
- | `#descendants` | Returns all descendants of the node (subtree). |
62
- | `#leaf?` | Returns whether the node is a leaf (has no children). |
63
- | `#depth` | Returns the depth of the current node. |
64
- | `#root` | Returns the root node of the current node's tree. |
65
- | `#root?` | Returns whether the node is a root (has no parent). |
66
- | `#self_and_ancestors` | Returns the node and all its ancestors. |
67
- | `#self_and_descendants` | Returns the node and all its descendants (subtree). |
68
- | `#self_and_siblings` | Returns the current node and all its siblings. |
69
- | `#siblings` | Returns the current node's siblings. |
79
+ | Method | Description |
80
+ | ----------------------- | ----------------------------------------------------- |
81
+ | `#ancestors` | Returns all ancestors of the node. |
82
+ | `#descendants` | Returns all descendants of the node (subtree). |
83
+ | `#leaf?` | Returns whether the node is a leaf (has no children). |
84
+ | `#depth` | Returns the depth of the current node. |
85
+ | `#root` | Returns the root node of the current node's tree. |
86
+ | `#root?` | Returns whether the node is a root (has no parent). |
87
+ | `#self_and_ancestors` | Returns the node and all its ancestors. |
88
+ | `#self_and_descendants` | Returns the node and all its descendants (subtree). |
89
+ | `#self_and_siblings` | Returns the current node and all its siblings. |
90
+ | `#siblings` | Returns the current node's siblings. |
70
91
 
71
92
  ### Tree traversing
72
93
 
@@ -74,7 +95,27 @@ You can traverse the tree using `#descendants` or `#self_and_descendants` in com
74
95
 
75
96
  For example, given the following tree:
76
97
 
77
- ![sample tree](/assets/tree.png)
98
+ ```mermaid
99
+ flowchart TD
100
+ A
101
+ A --> B
102
+ A --> L
103
+ B --> C
104
+ B --> H
105
+ C --> D
106
+ D --> E
107
+ D --> F
108
+ D --> G
109
+ H --> I
110
+ I --> J
111
+ J --> K
112
+ L --> M
113
+ L --> N
114
+ N --> O
115
+ N --> P
116
+ N --> Q
117
+ N --> R
118
+ ```
78
119
 
79
120
  and the following class:
80
121
 
@@ -1,3 +1,3 @@
1
1
  module WithRecursiveTree
2
- VERSION = "0.3.0"
2
+ VERSION = "0.4.0"
3
3
  end
@@ -2,15 +2,18 @@ require "with_recursive_tree/version"
2
2
 
3
3
  module WithRecursiveTree
4
4
  module ClassMethods
5
- def with_recursive_tree(primary_key: :id, foreign_key: :parent_id, order: nil)
5
+ def with_recursive_tree(primary_key: :id, foreign_key: :parent_id, foreign_key_type: nil, order: nil)
6
6
  include InstanceMethods
7
7
 
8
- belongs_to :parent, class_name: name, primary_key: primary_key, foreign_key: foreign_key, inverse_of: :children, optional: true
8
+ scope_condition = foreign_key_type.present? ? -> { where foreign_key_type => self.class.name } : -> { self }
9
9
 
10
- has_many :children, -> { order order }, class_name: name, primary_key: primary_key, foreign_key: foreign_key, inverse_of: :parent
10
+ belongs_to :parent, scope_condition, class_name: name, primary_key: primary_key, foreign_key: foreign_key, inverse_of: :children, optional: true
11
+
12
+ has_many :children, -> { scope_condition.call.order(order) }, class_name: name, primary_key: primary_key, foreign_key: foreign_key, inverse_of: :parent
11
13
 
12
14
  define_singleton_method(:with_recursive_tree_primary_key) { primary_key }
13
15
  define_singleton_method(:with_recursive_tree_foreign_key) { foreign_key }
16
+ define_singleton_method(:with_recursive_tree_foreign_key_type) { foreign_key_type }
14
17
  define_singleton_method(:with_recursive_tree_order) { order || primary_key }
15
18
  define_singleton_method(:with_recursive_tree_order_column) do
16
19
  if with_recursive_tree_order.is_a?(Hash)
@@ -20,6 +23,20 @@ module WithRecursiveTree
20
23
  end
21
24
  end
22
25
 
26
+ if foreign_key_type.present?
27
+ before_save do
28
+ if send(:"#{foreign_key}_changed?")
29
+ if send(foreign_key).present?
30
+ # When setting a parent, the foreign_key_type is automatically set to the model's class name if it's blank
31
+ send(:"#{foreign_key_type}=", self.class.name) if send(foreign_key_type).blank?
32
+ elsif send(foreign_key).nil?
33
+ # When clearing parent, the foreign_key_type is to nil unless it's explicitly set to the model's class name
34
+ send(:"#{foreign_key_type}=", nil) unless send(foreign_key_type) == self.class.name
35
+ end
36
+ end
37
+ end
38
+ end
39
+
23
40
  scope :bfs, -> {
24
41
  if defined?(ActiveRecord::ConnectionAdapters::MySQL)
25
42
  order(:depth, with_recursive_tree_order)
@@ -36,7 +53,22 @@ module WithRecursiveTree
36
53
  self
37
54
  end
38
55
  end
39
- scope :roots, -> { where with_recursive_tree_foreign_key => nil }
56
+ scope :roots, -> {
57
+ if with_recursive_tree_foreign_key_type.present?
58
+ # Root conditions with foreign_key_type:
59
+ # 1. foreign_key is nil AND foreign_key_type is nil
60
+ # 2. foreign_key is nil AND foreign_key_type matches the model's class name
61
+ # 3. foreign_key is not nil AND foreign_key_type is different from model's class name
62
+ where(with_recursive_tree_foreign_key => nil)
63
+ .where(with_recursive_tree_foreign_key_type => [nil, name])
64
+ .or(
65
+ where.not(with_recursive_tree_foreign_key => nil)
66
+ .where.not(with_recursive_tree_foreign_key_type => name)
67
+ )
68
+ else
69
+ where with_recursive_tree_foreign_key => nil
70
+ end
71
+ }
40
72
  end
41
73
  end
42
74
 
@@ -59,18 +91,46 @@ module WithRecursiveTree
59
91
  alias_method :level, :depth
60
92
 
61
93
  def root
62
- self_and_ancestors.find_by self.class.with_recursive_tree_foreign_key => nil
94
+ return self if root?
95
+
96
+ if self.class.with_recursive_tree_foreign_key_type.present?
97
+ # For foreign_key_type, find the first ancestor that satisfies root conditions
98
+ self_and_ancestors.where(
99
+ self.class.with_recursive_tree_foreign_key => nil,
100
+ self.class.with_recursive_tree_foreign_key_type => [nil, self.class.name]
101
+ ).or(
102
+ self_and_ancestors.where.not(self.class.with_recursive_tree_foreign_key => nil)
103
+ .where.not(self.class.with_recursive_tree_foreign_key_type => self.class.name)
104
+ ).first
105
+ else
106
+ self_and_ancestors.find_by self.class.with_recursive_tree_foreign_key => nil
107
+ end
63
108
  end
64
109
 
65
110
  def root?
66
- parent.blank?
111
+ foreign_key_value = send(self.class.with_recursive_tree_foreign_key)
112
+
113
+ if self.class.with_recursive_tree_foreign_key_type.present?
114
+ foreign_key_type_value = send(self.class.with_recursive_tree_foreign_key_type)
115
+
116
+ # Root conditions with foreign_key_type:
117
+ # 1. foreign_key is nil AND foreign_key_type is nil
118
+ # 2. foreign_key is nil AND foreign_key_type matches the model's class name
119
+ # 3. foreign_key is not nil AND foreign_key_type is different from model's class name
120
+ (foreign_key_value.nil? && [nil, self.class.name].include?(foreign_key_type_value)) ||
121
+ (foreign_key_value.present? && foreign_key_type_value != self.class.name)
122
+ else
123
+ foreign_key_value.nil?
124
+ end
67
125
  end
68
126
 
69
127
  def self_and_ancestors
128
+ scope_condition = self.class.with_recursive_tree_foreign_key_type.present? ? {"tree.#{self.class.with_recursive_tree_foreign_key_type}" => self.class.name} : nil
129
+
70
130
  self.class.with_recursive(
71
131
  tree: [
72
132
  self.class.where(self.class.with_recursive_tree_primary_key => send(self.class.with_recursive_tree_primary_key)),
73
- self.class.joins("JOIN tree ON #{self.class.table_name}.#{self.class.with_recursive_tree_primary_key} = tree.#{self.class.with_recursive_tree_foreign_key}")
133
+ self.class.joins("JOIN tree ON #{self.class.table_name}.#{self.class.with_recursive_tree_primary_key} = tree.#{self.class.with_recursive_tree_foreign_key}").where(scope_condition)
74
134
  ]
75
135
  ).select("*").from("tree AS #{self.class.table_name}")
76
136
  end
@@ -92,7 +152,9 @@ module WithRecursiveTree
92
152
  "tree.path || #{self.class.table_name}.#{self.class.with_recursive_tree_primary_key} || '/'"
93
153
  end
94
154
 
95
- recursive_query = self.class.joins("JOIN tree ON #{self.class.table_name}.#{self.class.with_recursive_tree_foreign_key} = tree.#{self.class.with_recursive_tree_primary_key}").select("#{self.class.table_name}.*, #{recursive_path} AS path, depth + 1 AS depth")
155
+ scope_condition = self.class.with_recursive_tree_foreign_key_type.present? ? {"#{self.class.table_name}.#{self.class.with_recursive_tree_foreign_key_type}" => self.class.name} : nil
156
+
157
+ recursive_query = self.class.joins("JOIN tree ON #{self.class.table_name}.#{self.class.with_recursive_tree_foreign_key} = tree.#{self.class.with_recursive_tree_primary_key}").select("#{self.class.table_name}.*, #{recursive_path} AS path, depth + 1 AS depth").where scope_condition
96
158
 
97
159
  unless defined?(ActiveRecord::ConnectionAdapters::MySQL)
98
160
  recursive_query = recursive_query.order(self.class.with_recursive_tree_order)
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: with_recursive_tree
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.0
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Patricio Mac Adden
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2025-06-25 00:00:00.000000000 Z
11
+ date: 2025-09-24 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activerecord