Skip to content

Commit 2be1448

Browse files
committed
feat(api): add pagination to catalog access search and config tree traversal
Implements proper pagination by counting total results before applying limit/offset. Adds new ConfigTree function to build hierarchical config relationships including parents, children, and related configs.
1 parent 48a082c commit 2be1448

2 files changed

Lines changed: 251 additions & 2 deletions

File tree

query/config_access.go

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,10 +36,17 @@ func FindCatalogAccess(ctx context.Context, req CatalogAccessSearchRequest) (res
3636
q = q.Where("config_id IN ?", configIDs)
3737
}
3838

39-
if err := q.Find(&output.Access).Error; err != nil {
39+
if err := q.Count(&output.Total).Error; err != nil {
40+
return nil, err
41+
}
42+
if output.Total == 0 {
43+
timer.Results(output.Access)
44+
return &output, nil
45+
}
46+
47+
if err := q.Limit(req.PageSize).Offset((req.Page - 1) * req.PageSize).Find(&output.Access).Error; err != nil {
4048
return nil, err
4149
}
42-
output.Total = int64(len(output.Access))
4350
timer.Results(output.Access)
4451
return &output, nil
4552
}

query/config_tree.go

Lines changed: 242 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,242 @@
1+
package query
2+
3+
import (
4+
"strings"
5+
6+
"github.com/flanksource/duty/context"
7+
"github.com/flanksource/duty/models"
8+
"github.com/google/uuid"
9+
"github.com/samber/lo"
10+
)
11+
12+
type ConfigTreeNode struct {
13+
models.ConfigItem `json:",inline"`
14+
EdgeType string `json:"edgeType,omitempty"`
15+
Relation string `json:"relation,omitempty"`
16+
Children []*ConfigTreeNode `json:"children,omitempty"`
17+
}
18+
19+
type ConfigTreeOptions struct {
20+
Direction RelationDirection
21+
Incoming RelationType
22+
Outgoing RelationType
23+
}
24+
25+
func ConfigTree(ctx context.Context, configID uuid.UUID, opts ConfigTreeOptions) (*ConfigTreeNode, error) {
26+
config, err := GetCachedConfig(ctx, configID.String())
27+
if err != nil {
28+
return nil, err
29+
}
30+
if config == nil {
31+
return nil, nil
32+
}
33+
34+
parents := resolveParentsFromPath(ctx, config)
35+
36+
childIDs, err := ExpandConfigChildren(ctx, []uuid.UUID{config.ID})
37+
if err != nil {
38+
return nil, err
39+
}
40+
childIDs = lo.Filter(childIDs, func(id uuid.UUID, _ int) bool { return id != config.ID })
41+
42+
var children []models.ConfigItem
43+
if len(childIDs) > 0 {
44+
children, err = GetConfigsByIDs(ctx, childIDs)
45+
if err != nil {
46+
return nil, err
47+
}
48+
}
49+
50+
if opts.Direction == "" {
51+
opts.Direction = All
52+
}
53+
relType := Hard
54+
if opts.Incoming != "" {
55+
relType = opts.Incoming
56+
}
57+
outType := Hard
58+
if opts.Outgoing != "" {
59+
outType = opts.Outgoing
60+
}
61+
62+
related, err := GetRelatedConfigs(ctx, RelationQuery{
63+
ID: config.ID,
64+
Relation: opts.Direction,
65+
Incoming: relType,
66+
Outgoing: outType,
67+
})
68+
if err != nil {
69+
return nil, err
70+
}
71+
72+
return buildConfigTree(config, parents, children, related), nil
73+
}
74+
75+
func resolveParentsFromPath(ctx context.Context, config *models.ConfigItem) []models.ConfigItem {
76+
if config.Path == "" {
77+
return nil
78+
}
79+
segments := strings.Split(config.Path, ".")
80+
var parentIDs []uuid.UUID
81+
for _, seg := range segments {
82+
id, err := uuid.Parse(seg)
83+
if err != nil || id == config.ID {
84+
continue
85+
}
86+
parentIDs = append(parentIDs, id)
87+
}
88+
if len(parentIDs) == 0 {
89+
return nil
90+
}
91+
items, err := GetConfigsByIDs(ctx, parentIDs)
92+
if err != nil || len(items) == 0 {
93+
return nil
94+
}
95+
byID := make(map[uuid.UUID]models.ConfigItem, len(items))
96+
for _, ci := range items {
97+
byID[ci.ID] = ci
98+
}
99+
var parents []models.ConfigItem
100+
for _, id := range parentIDs {
101+
if ci, ok := byID[id]; ok {
102+
parents = append(parents, ci)
103+
}
104+
}
105+
return parents
106+
}
107+
108+
func ExpandConfigChildren(ctx context.Context, ids []uuid.UUID) ([]uuid.UUID, error) {
109+
allIDs := make(map[uuid.UUID]struct{}, len(ids))
110+
for _, id := range ids {
111+
allIDs[id] = struct{}{}
112+
}
113+
for _, id := range ids {
114+
var children []uuid.UUID
115+
if err := ctx.DB().Raw("SELECT child_id FROM lookup_config_children(?, -1)", id.String()).
116+
Scan(&children).Error; err != nil {
117+
return nil, err
118+
}
119+
for _, child := range children {
120+
allIDs[child] = struct{}{}
121+
}
122+
}
123+
return lo.Keys(allIDs), nil
124+
}
125+
126+
type ptrNode struct {
127+
models.ConfigItem
128+
edgeType string
129+
relation string
130+
children []*ptrNode
131+
}
132+
133+
func buildConfigTree(config *models.ConfigItem, parents []models.ConfigItem, children []models.ConfigItem, related []RelatedConfig) *ConfigTreeNode {
134+
nodes := make(map[uuid.UUID]*ptrNode)
135+
136+
for _, p := range parents {
137+
nodes[p.ID] = &ptrNode{ConfigItem: p, edgeType: "parent"}
138+
}
139+
140+
targetNode := &ptrNode{ConfigItem: *config, edgeType: "target"}
141+
nodes[config.ID] = targetNode
142+
143+
if len(parents) > 0 {
144+
for i := 0; i < len(parents)-1; i++ {
145+
nodes[parents[i].ID].children = append(nodes[parents[i].ID].children, nodes[parents[i+1].ID])
146+
}
147+
nodes[parents[len(parents)-1].ID].children = append(nodes[parents[len(parents)-1].ID].children, targetNode)
148+
}
149+
150+
for _, c := range children {
151+
nodes[c.ID] = &ptrNode{ConfigItem: c, edgeType: "child"}
152+
}
153+
for _, c := range children {
154+
parentID := lo.FromPtr(c.ParentID)
155+
if parent, ok := nodes[parentID]; ok {
156+
parent.children = append(parent.children, nodes[c.ID])
157+
} else {
158+
targetNode.children = append(targetNode.children, nodes[c.ID])
159+
}
160+
}
161+
162+
parentIDs := make(map[uuid.UUID]bool, len(parents))
163+
for _, p := range parents {
164+
parentIDs[p.ID] = true
165+
}
166+
167+
for _, rc := range related {
168+
if parentIDs[rc.ID] || rc.ID == config.ID {
169+
continue
170+
}
171+
if _, exists := nodes[rc.ID]; !exists {
172+
nodes[rc.ID] = &ptrNode{
173+
ConfigItem: relatedToConfigItem(rc),
174+
edgeType: "related",
175+
relation: rc.Relation,
176+
}
177+
}
178+
}
179+
180+
wired := make(map[uuid.UUID]bool)
181+
for _, rc := range related {
182+
if parentIDs[rc.ID] || rc.ID == config.ID || wired[rc.ID] {
183+
continue
184+
}
185+
wired[rc.ID] = true
186+
node := nodes[rc.ID]
187+
if rc.Path != "" {
188+
segments := strings.Split(rc.Path, ".")
189+
if parentStr := segments[len(segments)-1]; parentStr != "" {
190+
if pid, err := uuid.Parse(parentStr); err == nil {
191+
if parent, ok := nodes[pid]; ok && parent != node && !parentIDs[pid] {
192+
parent.children = append(parent.children, node)
193+
continue
194+
}
195+
}
196+
}
197+
}
198+
targetNode.children = append(targetNode.children, node)
199+
}
200+
201+
var root *ptrNode
202+
if len(parents) > 0 {
203+
root = nodes[parents[0].ID]
204+
} else {
205+
root = targetNode
206+
}
207+
208+
return toConfigTreeNode(root, make(map[*ptrNode]bool))
209+
}
210+
211+
func toConfigTreeNode(n *ptrNode, visited map[*ptrNode]bool) *ConfigTreeNode {
212+
result := &ConfigTreeNode{
213+
ConfigItem: n.ConfigItem,
214+
EdgeType: n.edgeType,
215+
Relation: n.relation,
216+
}
217+
if visited[n] {
218+
return result
219+
}
220+
visited[n] = true
221+
for _, c := range n.children {
222+
result.Children = append(result.Children, toConfigTreeNode(c, visited))
223+
}
224+
return result
225+
}
226+
227+
func relatedToConfigItem(rc RelatedConfig) models.ConfigItem {
228+
ci := models.ConfigItem{
229+
ID: rc.ID,
230+
}
231+
ci.Name = &rc.Name
232+
ci.Type = &rc.Type
233+
ci.Tags = rc.Tags
234+
ci.Status = rc.Status
235+
ci.Health = rc.Health
236+
ci.Path = rc.Path
237+
ci.CreatedAt = rc.CreatedAt
238+
ci.UpdatedAt = &rc.UpdatedAt
239+
ci.DeletedAt = rc.DeletedAt
240+
ci.AgentID = rc.AgentID
241+
return ci
242+
}

0 commit comments

Comments
 (0)