Add uncategorised patterns GraphQL endpoint
Backend for the cookie banner detection page: a new uncategorisedPatterns connection on CookieBanner with sortable (NAME, LAST_MATCHED_AT, UPDATED_AT, SOURCE) and filterable (text ILIKE on name/description, source enum) paginated results. COALESCE handles NULL-first ordering for last_matched_at. Signed-off-by: Émile Ré <emile@getprobo.com>
This commit is contained in:
@@ -53,6 +53,17 @@ func (cp *CookiePattern) CursorKey(field CookiePatternOrderField) page.CursorKey
|
||||
switch field {
|
||||
case CookiePatternOrderFieldCreatedAt:
|
||||
return page.NewCursorKey(cp.ID, cp.CreatedAt)
|
||||
case CookiePatternOrderFieldName:
|
||||
return page.NewCursorKey(cp.ID, cp.DisplayName)
|
||||
case CookiePatternOrderFieldLastMatchedAt:
|
||||
if cp.LastMatchedAt == nil {
|
||||
return page.NewCursorKey(cp.ID, time.Time{})
|
||||
}
|
||||
return page.NewCursorKey(cp.ID, *cp.LastMatchedAt)
|
||||
case CookiePatternOrderFieldUpdatedAt:
|
||||
return page.NewCursorKey(cp.ID, cp.UpdatedAt)
|
||||
case CookiePatternOrderFieldSource:
|
||||
return page.NewCursorKey(cp.ID, string(cp.Source))
|
||||
}
|
||||
|
||||
panic(fmt.Sprintf("unsupported order by: %s", field))
|
||||
@@ -639,6 +650,115 @@ WHERE
|
||||
return nil
|
||||
}
|
||||
|
||||
func (cps *CookiePatterns) LoadUncategorisedByCookieBannerID(
|
||||
ctx context.Context,
|
||||
conn pg.Querier,
|
||||
scope Scoper,
|
||||
cookieBannerID gid.GID,
|
||||
cursor *page.Cursor[CookiePatternOrderField],
|
||||
filter *CookiePatternFilter,
|
||||
) error {
|
||||
q := `
|
||||
SELECT
|
||||
id,
|
||||
organization_id,
|
||||
cookie_banner_id,
|
||||
cookie_category_id,
|
||||
pattern,
|
||||
match_type,
|
||||
display_name,
|
||||
max_age_seconds,
|
||||
description,
|
||||
source,
|
||||
excluded,
|
||||
last_matched_at,
|
||||
created_at,
|
||||
updated_at
|
||||
FROM
|
||||
cookie_patterns
|
||||
WHERE
|
||||
%s
|
||||
AND cookie_banner_id = @cookie_banner_id
|
||||
AND cookie_category_id = (
|
||||
SELECT id FROM cookie_categories
|
||||
WHERE cookie_banner_id = @cookie_banner_id
|
||||
AND kind = @category_kind
|
||||
AND %s
|
||||
LIMIT 1
|
||||
)
|
||||
AND %s
|
||||
AND %s
|
||||
`
|
||||
|
||||
q = fmt.Sprintf(q, scope.SQLFragment(), scope.SQLFragment(), filter.SQLFragment(), cursor.SQLFragment())
|
||||
|
||||
args := pgx.StrictNamedArgs{
|
||||
"cookie_banner_id": cookieBannerID,
|
||||
"category_kind": CookieCategoryKindUncategorised,
|
||||
}
|
||||
maps.Copy(args, scope.SQLArguments())
|
||||
maps.Copy(args, filter.SQLArguments())
|
||||
maps.Copy(args, cursor.SQLArguments())
|
||||
|
||||
rows, err := conn.Query(ctx, q, args)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot query uncategorised cookie patterns: %w", err)
|
||||
}
|
||||
|
||||
patterns, err := pgx.CollectRows(rows, pgx.RowToAddrOfStructByName[CookiePattern])
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot collect uncategorised cookie patterns: %w", err)
|
||||
}
|
||||
|
||||
*cps = patterns
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (cps *CookiePatterns) CountUncategorisedByCookieBannerID(
|
||||
ctx context.Context,
|
||||
conn pg.Querier,
|
||||
scope Scoper,
|
||||
cookieBannerID gid.GID,
|
||||
filter *CookiePatternFilter,
|
||||
) (int, error) {
|
||||
q := `
|
||||
SELECT
|
||||
COUNT(id)
|
||||
FROM
|
||||
cookie_patterns
|
||||
WHERE
|
||||
%s
|
||||
AND cookie_banner_id = @cookie_banner_id
|
||||
AND cookie_category_id = (
|
||||
SELECT id FROM cookie_categories
|
||||
WHERE cookie_banner_id = @cookie_banner_id
|
||||
AND kind = @category_kind
|
||||
AND %s
|
||||
LIMIT 1
|
||||
)
|
||||
AND %s
|
||||
`
|
||||
|
||||
q = fmt.Sprintf(q, scope.SQLFragment(), scope.SQLFragment(), filter.SQLFragment())
|
||||
|
||||
args := pgx.StrictNamedArgs{
|
||||
"cookie_banner_id": cookieBannerID,
|
||||
"category_kind": CookieCategoryKindUncategorised,
|
||||
}
|
||||
maps.Copy(args, scope.SQLArguments())
|
||||
maps.Copy(args, filter.SQLArguments())
|
||||
|
||||
row := conn.QueryRow(ctx, q, args)
|
||||
|
||||
var count int
|
||||
if err := row.Scan(&count); err != nil {
|
||||
return 0, fmt.Errorf("cannot scan count: %w", err)
|
||||
}
|
||||
|
||||
return count, nil
|
||||
}
|
||||
|
||||
func (cps *CookiePatterns) MoveToCategoryByCookieCategoryID(
|
||||
ctx context.Context,
|
||||
tx pg.Tx,
|
||||
|
||||
@@ -23,6 +23,8 @@ type CookiePatternFilter struct {
|
||||
matchType *CookiePatternMatchType
|
||||
cookieCategoryID *gid.GID
|
||||
excluded *bool
|
||||
query *string
|
||||
source *CookieSource
|
||||
}
|
||||
|
||||
func NewCookiePatternFilter(
|
||||
@@ -37,6 +39,16 @@ func NewCookiePatternFilter(
|
||||
}
|
||||
}
|
||||
|
||||
func (f *CookiePatternFilter) WithQuery(query *string) *CookiePatternFilter {
|
||||
f.query = query
|
||||
return f
|
||||
}
|
||||
|
||||
func (f *CookiePatternFilter) WithSource(source *CookieSource) *CookiePatternFilter {
|
||||
f.source = source
|
||||
return f
|
||||
}
|
||||
|
||||
func (f *CookiePatternFilter) SQLFragment() string {
|
||||
if f == nil {
|
||||
return "TRUE"
|
||||
@@ -64,6 +76,20 @@ func (f *CookiePatternFilter) SQLFragment() string {
|
||||
excluded = @filter_excluded
|
||||
ELSE TRUE
|
||||
END
|
||||
AND
|
||||
CASE
|
||||
WHEN @filter_query::text IS NOT NULL AND @filter_query::text != '' THEN
|
||||
(display_name ILIKE '%' || @filter_query || '%'
|
||||
OR description ILIKE '%' || @filter_query || '%')
|
||||
ELSE TRUE
|
||||
END
|
||||
AND
|
||||
CASE
|
||||
WHEN @has_source_filter::boolean = false THEN TRUE
|
||||
WHEN @has_source_filter::boolean = true THEN
|
||||
source = @filter_source::cookie_source
|
||||
ELSE TRUE
|
||||
END
|
||||
)`
|
||||
}
|
||||
|
||||
@@ -79,6 +105,9 @@ func (f *CookiePatternFilter) SQLArguments() pgx.StrictNamedArgs {
|
||||
"filter_cookie_category_id": nil,
|
||||
"has_excluded_filter": false,
|
||||
"filter_excluded": nil,
|
||||
"filter_query": nil,
|
||||
"has_source_filter": false,
|
||||
"filter_source": nil,
|
||||
}
|
||||
|
||||
if f.matchType != nil {
|
||||
@@ -96,5 +125,14 @@ func (f *CookiePatternFilter) SQLArguments() pgx.StrictNamedArgs {
|
||||
args["filter_excluded"] = *f.excluded
|
||||
}
|
||||
|
||||
if f.query != nil {
|
||||
args["filter_query"] = *f.query
|
||||
}
|
||||
|
||||
if f.source != nil {
|
||||
args["has_source_filter"] = true
|
||||
args["filter_source"] = string(*f.source)
|
||||
}
|
||||
|
||||
return args
|
||||
}
|
||||
|
||||
@@ -19,20 +19,36 @@ import "fmt"
|
||||
type CookiePatternOrderField string
|
||||
|
||||
const (
|
||||
CookiePatternOrderFieldCreatedAt CookiePatternOrderField = "CREATED_AT"
|
||||
CookiePatternOrderFieldCreatedAt CookiePatternOrderField = "CREATED_AT"
|
||||
CookiePatternOrderFieldName CookiePatternOrderField = "NAME"
|
||||
CookiePatternOrderFieldLastMatchedAt CookiePatternOrderField = "LAST_MATCHED_AT"
|
||||
CookiePatternOrderFieldUpdatedAt CookiePatternOrderField = "UPDATED_AT"
|
||||
CookiePatternOrderFieldSource CookiePatternOrderField = "SOURCE"
|
||||
)
|
||||
|
||||
func (p CookiePatternOrderField) Column() string {
|
||||
switch p {
|
||||
case CookiePatternOrderFieldCreatedAt:
|
||||
return "created_at"
|
||||
case CookiePatternOrderFieldName:
|
||||
return "display_name"
|
||||
case CookiePatternOrderFieldLastMatchedAt:
|
||||
return "COALESCE(last_matched_at, '0001-01-01T00:00:00Z'::timestamptz)"
|
||||
case CookiePatternOrderFieldUpdatedAt:
|
||||
return "updated_at"
|
||||
case CookiePatternOrderFieldSource:
|
||||
return "source"
|
||||
}
|
||||
panic(fmt.Sprintf("unsupported order by: %s", p))
|
||||
}
|
||||
|
||||
func (p CookiePatternOrderField) IsValid() bool {
|
||||
switch p {
|
||||
case CookiePatternOrderFieldCreatedAt:
|
||||
case CookiePatternOrderFieldCreatedAt,
|
||||
CookiePatternOrderFieldName,
|
||||
CookiePatternOrderFieldLastMatchedAt,
|
||||
CookiePatternOrderFieldUpdatedAt,
|
||||
CookiePatternOrderFieldSource:
|
||||
return true
|
||||
}
|
||||
return false
|
||||
|
||||
Reference in New Issue
Block a user