-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* related changes query * implement filtering syntax * feat: add summary * tests
- Loading branch information
1 parent
6dd2708
commit a121315
Showing
9 changed files
with
338 additions
and
24 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,3 +1,56 @@ | ||
package query | ||
|
||
import ( | ||
"fmt" | ||
"strings" | ||
) | ||
|
||
var LocalFilter = "deleted_at is NULL AND agent_id = '00000000-0000-0000-0000-000000000000' OR agent_id IS NULL" | ||
|
||
// parseFilteringQuery parses a filtering query string. | ||
// It returns four slices: 'in', 'notIN', 'prefix', and 'suffix'. | ||
func parseFilteringQuery(query string) (in, notIN, prefix, suffix []string) { | ||
items := strings.Split(query, ",") | ||
|
||
for _, item := range items { | ||
if strings.HasPrefix(item, "!") { | ||
notIN = append(notIN, strings.TrimPrefix(item, "!")) | ||
} else if strings.HasPrefix(item, "*") { | ||
suffix = append(suffix, strings.TrimPrefix(item, "*")) | ||
} else if strings.HasSuffix(item, "*") { | ||
prefix = append(prefix, strings.TrimSuffix(item, "*")) | ||
} else { | ||
in = append(in, item) | ||
} | ||
} | ||
|
||
return | ||
} | ||
|
||
func parseAndBuildFilteringQuery(query string, field string) ([]string, map[string]any) { | ||
var clauses []string | ||
var args = map[string]any{} | ||
|
||
in, notIN, prefixes, suffixes := parseFilteringQuery(query) | ||
if len(in) > 0 { | ||
clauses = append(clauses, fmt.Sprintf("%s IN @field_in", field)) | ||
args["field_in"] = in | ||
} | ||
|
||
if len(notIN) > 0 { | ||
clauses = append(clauses, fmt.Sprintf("%s NOT IN @field_not_in", field)) | ||
args["field_not_in"] = notIN | ||
} | ||
|
||
for i, p := range prefixes { | ||
clauses = append(clauses, fmt.Sprintf("%s LIKE @%s_prefix_%d", field, field, i)) | ||
args[fmt.Sprintf("prefix_%d", i)] = fmt.Sprintf("%s%%", p) | ||
} | ||
|
||
for i, s := range suffixes { | ||
clauses = append(clauses, fmt.Sprintf("%s LIKE @%s_suffix_%d", field, field, i)) | ||
args[fmt.Sprintf("suffix_%d", i)] = fmt.Sprintf("%%%s", s) | ||
} | ||
|
||
return clauses, args | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,114 @@ | ||
package query | ||
|
||
import ( | ||
"fmt" | ||
"strings" | ||
"time" | ||
|
||
"github.com/flanksource/commons/collections" | ||
"github.com/flanksource/duty/api" | ||
"github.com/flanksource/duty/context" | ||
"github.com/flanksource/duty/models" | ||
"github.com/google/uuid" | ||
"github.com/samber/lo" | ||
"github.com/timberio/go-datemath" | ||
) | ||
|
||
const ( | ||
CatalogChangeRecursiveUpstream = "upstream" | ||
CatalogChangeRecursiveDownstream = "downstream" | ||
CatalogChangeRecursiveBoth = "both" | ||
) | ||
|
||
type CatalogChangesSearchRequest struct { | ||
CatalogID uuid.UUID `query:"id"` | ||
ConfigType string `query:"config_type"` | ||
ChangeType string `query:"type"` | ||
From string `query:"from"` | ||
|
||
// upstream | downstream | both | ||
Recursive string `query:"recursive"` | ||
|
||
fromParsed time.Time | ||
} | ||
|
||
func (t *CatalogChangesSearchRequest) Validate() error { | ||
if t.CatalogID == uuid.Nil { | ||
return fmt.Errorf("catalog id is required") | ||
} | ||
|
||
if t.Recursive != "" && !lo.Contains([]string{CatalogChangeRecursiveUpstream, CatalogChangeRecursiveDownstream, CatalogChangeRecursiveBoth}, t.Recursive) { | ||
return fmt.Errorf("recursive must be one of 'upstream', 'downstream' or 'both'") | ||
} | ||
|
||
if t.From != "" { | ||
if expr, err := datemath.Parse(t.From); err != nil { | ||
return fmt.Errorf("invalid 'from' param: %w", err) | ||
} else { | ||
t.fromParsed = expr.Time() | ||
} | ||
} | ||
|
||
return nil | ||
} | ||
|
||
type CatalogChangesSearchResponse struct { | ||
Summary map[string]int `json:"summary,omitempty"` | ||
Changes []models.ConfigChange `json:"changes,omitempty"` | ||
} | ||
|
||
func (t *CatalogChangesSearchResponse) Summarize() { | ||
t.Summary = make(map[string]int) | ||
for _, c := range t.Changes { | ||
t.Summary[c.ChangeType]++ | ||
} | ||
} | ||
|
||
func FindCatalogChanges(ctx context.Context, req CatalogChangesSearchRequest) (*CatalogChangesSearchResponse, error) { | ||
if err := req.Validate(); err != nil { | ||
return nil, api.Errorf(api.EINVALID, "bad request: %v", err) | ||
} | ||
|
||
args := map[string]any{ | ||
"catalog_id": req.CatalogID, | ||
"recursive": req.Recursive, | ||
} | ||
|
||
var clauses []string | ||
query := "SELECT cc.* FROM related_changes_recursive(@catalog_id, @recursive) cc" | ||
if req.Recursive == "" { | ||
query = "SELECT cc.* FROM config_changes cc" | ||
clauses = append(clauses, "cc.config_id = @catalog_id") | ||
} | ||
|
||
if req.ConfigType != "" { | ||
query += " LEFT JOIN config_items ON cc.config_id = config_items.id" | ||
|
||
_clauses, _args := parseAndBuildFilteringQuery(req.ConfigType, "config_items.type") | ||
clauses = append(clauses, _clauses...) | ||
args = collections.MergeMap(args, _args) | ||
} | ||
|
||
if req.ChangeType != "" { | ||
_clauses, _args := parseAndBuildFilteringQuery(req.ChangeType, "cc.change_type") | ||
clauses = append(clauses, _clauses...) | ||
args = collections.MergeMap(args, _args) | ||
} | ||
|
||
if !req.fromParsed.IsZero() { | ||
clauses = append(clauses, "cc.created_at >= @from") | ||
args["from"] = req.fromParsed | ||
} | ||
|
||
if len(clauses) > 0 { | ||
query += fmt.Sprintf(" WHERE %s", strings.Join(clauses, " AND ")) | ||
} | ||
|
||
var output CatalogChangesSearchResponse | ||
if err := ctx.DB().Raw(query, args).Find(&output.Changes).Error; err != nil { | ||
return nil, err | ||
} | ||
|
||
output.Summarize() | ||
return &output, nil | ||
} |
Oops, something went wrong.