diff --git a/pkg/cookiebanner/service.go b/pkg/cookiebanner/service.go index 430d446bf..680851005 100644 --- a/pkg/cookiebanner/service.go +++ b/pkg/cookiebanner/service.go @@ -2709,6 +2709,128 @@ func (s *Service) CountTrackerPatternsForBanner( return count, nil } +func (s *Service) GetCommonTrackerPatternsByIDs( + ctx context.Context, + ids ...gid.GID, +) (coredata.CommonTrackerPatterns, error) { + var patterns coredata.CommonTrackerPatterns + + err := s.pg.WithConn( + ctx, + func(ctx context.Context, conn pg.Querier) error { + if err := patterns.LoadByIDs(ctx, conn, ids); err != nil { + return fmt.Errorf("cannot load common tracker patterns by ids: %w", err) + } + + return nil + }, + ) + if err != nil { + return nil, err + } + + return patterns, nil +} + +// LoadCommonTrackerPatternIDsByCommonThirdPartyID returns the IDs of +// every common tracker pattern referencing the given common third party. +// Used by the trackers list filter to translate a CommonThirdParty GID +// into a `common_tracker_pattern_id = ANY(...)` constraint without +// JOINing across entity tables in coredata. +func (s *Service) LoadCommonTrackerPatternIDsByCommonThirdPartyID( + ctx context.Context, + commonThirdPartyID gid.GID, +) ([]gid.GID, error) { + var ids []gid.GID + + err := s.pg.WithConn( + ctx, + func(ctx context.Context, conn pg.Querier) error { + var ( + patterns coredata.CommonTrackerPatterns + err error + ) + + ids, err = patterns.LoadIDsByCommonThirdPartyID(ctx, conn, commonThirdPartyID) + if err != nil { + return fmt.Errorf("cannot load common tracker pattern ids: %w", err) + } + + return nil + }, + ) + if err != nil { + return nil, err + } + + return ids, nil +} + +// LoadDistinctThirdPartyIDsByCookieBannerID returns the distinct +// org-scoped third-party IDs referenced by tracker patterns of the +// banner. The companion +// LoadDistinctCommonTrackerPatternIDsByCookieBannerID covers the +// indirect mapping through common_tracker_patterns. +func (s *Service) LoadDistinctThirdPartyIDsByCookieBannerID( + ctx context.Context, + scope coredata.Scoper, + cookieBannerID gid.GID, +) ([]gid.GID, error) { + var ids []gid.GID + + err := s.pg.WithConn( + ctx, + func(ctx context.Context, conn pg.Querier) error { + var ( + patterns coredata.TrackerPatterns + err error + ) + + ids, err = patterns.LoadDistinctThirdPartyIDsByCookieBannerID(ctx, conn, scope, cookieBannerID) + if err != nil { + return fmt.Errorf("cannot load distinct third party ids: %w", err) + } + + return nil + }, + ) + if err != nil { + return nil, err + } + + return ids, nil +} + +func (s *Service) LoadDistinctCommonTrackerPatternIDsByCookieBannerID( + ctx context.Context, + scope coredata.Scoper, + cookieBannerID gid.GID, +) ([]gid.GID, error) { + var ids []gid.GID + + err := s.pg.WithConn( + ctx, + func(ctx context.Context, conn pg.Querier) error { + var ( + patterns coredata.TrackerPatterns + err error + ) + + ids, err = patterns.LoadDistinctCommonTrackerPatternIDsByCookieBannerID(ctx, conn, scope, cookieBannerID) + if err != nil { + return fmt.Errorf("cannot load distinct common tracker pattern ids: %w", err) + } + + return nil + }, + ) + if err != nil { + return nil, err + } + + return ids, nil +} + func (s *Service) CountDetectedTrackersByPatternID( ctx context.Context, scope coredata.Scoper, diff --git a/pkg/coredata/common_third_party.go b/pkg/coredata/common_third_party.go index 9b924124c..a1c3266f8 100644 --- a/pkg/coredata/common_third_party.go +++ b/pkg/coredata/common_third_party.go @@ -507,6 +507,57 @@ func (t CommonThirdParty) Delete( return nil } +func (t *CommonThirdParties) LoadByIDs( + ctx context.Context, + conn pg.Querier, + ids []gid.GID, +) error { + q := ` +SELECT + id, + name, + slug, + category, + headquarter_address, + legal_name, + website_url, + privacy_policy_url, + service_level_agreement_url, + service_software_agreement_url, + data_processing_agreement_url, + business_associate_agreement_url, + subprocessors_list_url, + certifications, + status_page_url, + terms_of_service_url, + security_page_url, + trust_page_url, + logo_file_id, + created_at, + updated_at +FROM + common_third_parties +WHERE + id = ANY(@ids) +` + + args := pgx.StrictNamedArgs{"ids": ids} + + rows, err := conn.Query(ctx, q, args) + if err != nil { + return fmt.Errorf("cannot query common third parties: %w", err) + } + + parties, err := pgx.CollectRows(rows, pgx.RowToAddrOfStructByName[CommonThirdParty]) + if err != nil { + return fmt.Errorf("cannot collect common third parties: %w", err) + } + + *t = parties + + return nil +} + func (t *CommonThirdParties) LoadAll( ctx context.Context, conn pg.Querier, diff --git a/pkg/coredata/common_tracker_pattern.go b/pkg/coredata/common_tracker_pattern.go index dc4071662..505124285 100644 --- a/pkg/coredata/common_tracker_pattern.go +++ b/pkg/coredata/common_tracker_pattern.go @@ -443,3 +443,76 @@ ORDER BY pattern ASC; return nil } + +func (ps *CommonTrackerPatterns) LoadByIDs( + ctx context.Context, + conn pg.Querier, + ids []gid.GID, +) error { + q := ` +SELECT + id, + common_third_party_id, + tracker_type, + pattern, + match_type, + description, + max_age_seconds, + confidence, + created_at, + updated_at +FROM + common_tracker_patterns +WHERE + id = ANY(@ids) +` + + args := pgx.StrictNamedArgs{"ids": ids} + + rows, err := conn.Query(ctx, q, args) + if err != nil { + return fmt.Errorf("cannot query common tracker patterns: %w", err) + } + + patterns, err := pgx.CollectRows(rows, pgx.RowToAddrOfStructByName[CommonTrackerPattern]) + if err != nil { + return fmt.Errorf("cannot collect common tracker patterns: %w", err) + } + + *ps = patterns + + return nil +} + +// LoadIDsByCommonThirdPartyID returns just the IDs of the common tracker +// patterns linked to the given common third party. Callers use it to feed +// a `common_tracker_pattern_id = ANY(...)` filter on tracker_patterns +// without crossing the entity boundary. +func (ps *CommonTrackerPatterns) LoadIDsByCommonThirdPartyID( + ctx context.Context, + conn pg.Querier, + commonThirdPartyID gid.GID, +) ([]gid.GID, error) { + q := ` +SELECT + id +FROM + common_tracker_patterns +WHERE + common_third_party_id = @common_third_party_id +` + + args := pgx.StrictNamedArgs{"common_third_party_id": commonThirdPartyID} + + rows, err := conn.Query(ctx, q, args) + if err != nil { + return nil, fmt.Errorf("cannot query common tracker pattern ids: %w", err) + } + + ids, err := pgx.CollectRows(rows, pgx.RowTo[gid.GID]) + if err != nil { + return nil, fmt.Errorf("cannot collect common tracker pattern ids: %w", err) + } + + return ids, nil +} diff --git a/pkg/coredata/tracker_pattern.go b/pkg/coredata/tracker_pattern.go index 099b99c08..ead816265 100644 --- a/pkg/coredata/tracker_pattern.go +++ b/pkg/coredata/tracker_pattern.go @@ -864,6 +864,82 @@ WHERE return count, nil } +// LoadDistinctThirdPartyIDsByCookieBannerID returns the distinct non-null +// `third_party_id` values referenced by tracker patterns of the given +// banner. Callers feed it to ThirdParty.GetByIDs to power per-banner +// pickers without crossing the entity boundary. +func (tps *TrackerPatterns) LoadDistinctThirdPartyIDsByCookieBannerID( + ctx context.Context, + conn pg.Querier, + scope Scoper, + cookieBannerID gid.GID, +) ([]gid.GID, error) { + q := ` +SELECT DISTINCT third_party_id +FROM tracker_patterns +WHERE + %s + AND cookie_banner_id = @cookie_banner_id + AND third_party_id IS NOT NULL +` + + q = fmt.Sprintf(q, scope.SQLFragment()) + + args := pgx.StrictNamedArgs{"cookie_banner_id": cookieBannerID} + maps.Copy(args, scope.SQLArguments()) + + rows, err := conn.Query(ctx, q, args) + if err != nil { + return nil, fmt.Errorf("cannot query distinct third party ids: %w", err) + } + + ids, err := pgx.CollectRows(rows, pgx.RowTo[gid.GID]) + if err != nil { + return nil, fmt.Errorf("cannot collect distinct third party ids: %w", err) + } + + return ids, nil +} + +// LoadDistinctCommonTrackerPatternIDsByCookieBannerID returns the +// distinct non-null `common_tracker_pattern_id` values referenced by +// tracker patterns of the given banner. Callers chain this with +// CommonTrackerPatterns.LoadByIDs and CommonThirdParties.LoadByIDs to +// resolve the linked common third parties without JOINs. +func (tps *TrackerPatterns) LoadDistinctCommonTrackerPatternIDsByCookieBannerID( + ctx context.Context, + conn pg.Querier, + scope Scoper, + cookieBannerID gid.GID, +) ([]gid.GID, error) { + q := ` +SELECT DISTINCT common_tracker_pattern_id +FROM tracker_patterns +WHERE + %s + AND cookie_banner_id = @cookie_banner_id + AND common_tracker_pattern_id IS NOT NULL + AND third_party_id IS NULL +` + + q = fmt.Sprintf(q, scope.SQLFragment()) + + args := pgx.StrictNamedArgs{"cookie_banner_id": cookieBannerID} + maps.Copy(args, scope.SQLArguments()) + + rows, err := conn.Query(ctx, q, args) + if err != nil { + return nil, fmt.Errorf("cannot query distinct common tracker pattern ids: %w", err) + } + + ids, err := pgx.CollectRows(rows, pgx.RowTo[gid.GID]) + if err != nil { + return nil, fmt.Errorf("cannot collect distinct common tracker pattern ids: %w", err) + } + + return ids, nil +} + func (tps *TrackerPatterns) UpdateLastMatchedAt( ctx context.Context, tx pg.Tx, diff --git a/pkg/coredata/tracker_pattern_filter.go b/pkg/coredata/tracker_pattern_filter.go index a3063ab65..77f6ee1a4 100644 --- a/pkg/coredata/tracker_pattern_filter.go +++ b/pkg/coredata/tracker_pattern_filter.go @@ -20,12 +20,14 @@ import ( ) type TrackerPatternFilter struct { - matchType *TrackerPatternMatchType - cookieCategoryID *gid.GID - excluded *bool - query *string - source *CookieSource - trackerType *TrackerType + matchType *TrackerPatternMatchType + cookieCategoryID *gid.GID + excluded *bool + query *string + source *CookieSource + trackerType *TrackerType + thirdPartyID *gid.GID + commonTrackerPatternIDs []gid.GID } func NewTrackerPatternFilter( @@ -55,6 +57,22 @@ func (f *TrackerPatternFilter) WithTrackerType(trackerType *TrackerType) *Tracke return f } +func (f *TrackerPatternFilter) WithThirdPartyID(thirdPartyID *gid.GID) *TrackerPatternFilter { + f.thirdPartyID = thirdPartyID + return f +} + +// WithCommonTrackerPatternIDs constrains the result to tracker patterns +// whose `common_tracker_pattern_id` is in the given set. Callers +// pre-resolve this list (typically via +// CommonTrackerPatterns.LoadIDsByCommonThirdPartyID) so the filter stays +// inside the tracker_patterns table. Passing an empty (non-nil) slice +// yields no rows. +func (f *TrackerPatternFilter) WithCommonTrackerPatternIDs(ids []gid.GID) *TrackerPatternFilter { + f.commonTrackerPatternIDs = ids + return f +} + func (f *TrackerPatternFilter) SQLFragment() string { if f == nil { return "TRUE" @@ -103,6 +121,20 @@ func (f *TrackerPatternFilter) SQLFragment() string { tracker_type = @filter_tracker_type::tracker_type ELSE TRUE END + AND + CASE + WHEN @has_third_party_id_filter::boolean = false THEN TRUE + WHEN @has_third_party_id_filter::boolean = true THEN + third_party_id = @filter_third_party_id::text + ELSE TRUE + END + AND + CASE + WHEN @has_common_tracker_pattern_ids_filter::boolean = false THEN TRUE + WHEN @has_common_tracker_pattern_ids_filter::boolean = true THEN + common_tracker_pattern_id = ANY(@filter_common_tracker_pattern_ids::text[]) + ELSE TRUE + END )` } @@ -112,17 +144,21 @@ func (f *TrackerPatternFilter) SQLArguments() pgx.StrictNamedArgs { } args := pgx.StrictNamedArgs{ - "has_match_type_filter": false, - "filter_match_type": nil, - "has_cookie_category_id_filter": false, - "filter_cookie_category_id": nil, - "has_excluded_filter": false, - "filter_excluded": nil, - "filter_query": nil, - "has_source_filter": false, - "filter_source": nil, - "has_tracker_type_filter": false, - "filter_tracker_type": nil, + "has_match_type_filter": false, + "filter_match_type": nil, + "has_cookie_category_id_filter": false, + "filter_cookie_category_id": nil, + "has_excluded_filter": false, + "filter_excluded": nil, + "filter_query": nil, + "has_source_filter": false, + "filter_source": nil, + "has_tracker_type_filter": false, + "filter_tracker_type": nil, + "has_third_party_id_filter": false, + "filter_third_party_id": nil, + "has_common_tracker_pattern_ids_filter": false, + "filter_common_tracker_pattern_ids": []gid.GID{}, } if f.matchType != nil { @@ -154,5 +190,15 @@ func (f *TrackerPatternFilter) SQLArguments() pgx.StrictNamedArgs { args["filter_tracker_type"] = string(*f.trackerType) } + if f.thirdPartyID != nil { + args["has_third_party_id_filter"] = true + args["filter_third_party_id"] = *f.thirdPartyID + } + + if f.commonTrackerPatternIDs != nil { + args["has_common_tracker_pattern_ids_filter"] = true + args["filter_common_tracker_pattern_ids"] = f.commonTrackerPatternIDs + } + return args } diff --git a/pkg/thirdparty/service.go b/pkg/thirdparty/service.go index 31ae7b1fe..459eec661 100644 --- a/pkg/thirdparty/service.go +++ b/pkg/thirdparty/service.go @@ -50,6 +50,29 @@ func (s *Service) GenerateLogoURL( return &url, nil } +func (s *Service) GetCommonThirdPartiesByIDs( + ctx context.Context, + ids ...gid.GID, +) (coredata.CommonThirdParties, error) { + var parties coredata.CommonThirdParties + + err := s.pg.WithConn( + ctx, + func(ctx context.Context, conn pg.Querier) error { + if err := parties.LoadByIDs(ctx, conn, ids); err != nil { + return fmt.Errorf("cannot load common third parties by ids: %w", err) + } + + return nil + }, + ) + if err != nil { + return nil, err + } + + return parties, nil +} + func (s *Service) Search(ctx context.Context, name string) ([]*coredata.CommonThirdParty, error) { var parties coredata.CommonThirdParties