Migrate detection page to TrackerPattern GraphQL types
- Add TrackerPattern type, connection, and filter to GraphQL schema - Add uncategorisedTrackerPatterns field on CookieBanner - Add updateTrackerPattern, deleteTrackerPattern, moveTrackerPatternToCategory mutations - Implement all resolvers backed by tracker_patterns table - Add TrackerPattern service methods (CRUD + list/count) - Add LoadUncategorisedByCookieBannerID on TrackerPatterns coredata - Update detection page to use TrackerPattern fragment, queries, and mutations - CookieCategory resolver uses dataloader (not just struct ID) Signed-off-by: Émile Ré <emile@getprobo.com>
This commit is contained in:
@@ -213,6 +213,42 @@ func (r *cookieBannerResolver) UncategorisedPatterns(ctx context.Context, obj *t
|
||||
return types.NewCookiePatternConnectionWithFilter(p, r, obj.ID, coredataFilter), nil
|
||||
}
|
||||
|
||||
// UncategorisedTrackerPatterns is the resolver for the uncategorisedTrackerPatterns field.
|
||||
func (r *cookieBannerResolver) UncategorisedTrackerPatterns(ctx context.Context, obj *types.CookieBanner, first *int, after *page.CursorKey, last *int, before *page.CursorKey, orderBy *types.TrackerPatternOrderBy, filter *types.TrackerPatternFilter) (*types.TrackerPatternConnection, error) {
|
||||
if err := r.authorize(ctx, obj.ID, probo.ActionCookiePatternList); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
pageOrderBy := page.OrderBy[coredata.CookiePatternOrderField]{
|
||||
Field: coredata.CookiePatternOrderFieldName,
|
||||
Direction: page.OrderDirectionAsc,
|
||||
}
|
||||
if orderBy != nil {
|
||||
pageOrderBy = page.OrderBy[coredata.CookiePatternOrderField]{
|
||||
Field: orderBy.Field,
|
||||
Direction: orderBy.Direction,
|
||||
}
|
||||
}
|
||||
|
||||
cursor := types.NewCursor(first, after, last, before, pageOrderBy)
|
||||
scope := coredata.NewScopeFromObjectID(obj.ID)
|
||||
|
||||
coredataFilter := coredata.NewCookiePatternFilter(nil, nil, nil)
|
||||
if filter != nil {
|
||||
coredataFilter = coredataFilter.WithQuery(filter.Query).WithSource(filter.Source)
|
||||
}
|
||||
|
||||
patterns, err := r.cookieBanner.ListUncategorisedTrackerPatterns(ctx, scope, obj.ID, cursor, coredataFilter)
|
||||
if err != nil {
|
||||
r.logger.ErrorCtx(ctx, "cannot list uncategorised tracker patterns", log.Error(err))
|
||||
return nil, gqlutils.Internal(ctx)
|
||||
}
|
||||
|
||||
p := page.NewPage(patterns, cursor)
|
||||
|
||||
return types.NewTrackerPatternConnectionWithFilter(p, r, obj.ID, filter), nil
|
||||
}
|
||||
|
||||
// Permission is the resolver for the permission field.
|
||||
func (r *cookieBannerResolver) Permission(ctx context.Context, obj *types.CookieBanner, action string) (bool, error) {
|
||||
return r.Resolver.Permission(ctx, obj, action)
|
||||
@@ -997,6 +1033,182 @@ func (r *mutationResolver) UpsertCookieBannerTranslation(ctx context.Context, in
|
||||
}, nil
|
||||
}
|
||||
|
||||
// UpdateTrackerPattern is the resolver for the updateTrackerPattern field.
|
||||
func (r *mutationResolver) UpdateTrackerPattern(ctx context.Context, input types.UpdateTrackerPatternInput) (*types.UpdateTrackerPatternPayload, error) {
|
||||
if err := r.authorize(ctx, input.TrackerPatternID, probo.ActionCookiePatternUpdate); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
scope := coredata.NewScopeFromObjectID(input.TrackerPatternID)
|
||||
|
||||
pattern, err := r.cookieBanner.UpdateTrackerPattern(
|
||||
ctx,
|
||||
scope,
|
||||
cookiebanner.UpdateTrackerPatternRequest{
|
||||
TrackerPatternID: input.TrackerPatternID,
|
||||
DisplayName: input.DisplayName,
|
||||
MaxAgeSeconds: gqlutils.UnwrapOmittable(input.MaxAgeSeconds),
|
||||
Description: input.Description,
|
||||
Excluded: input.Excluded,
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
if errors.Is(err, cookiebanner.ErrCookiePatternNotFound) {
|
||||
return nil, gqlutils.NotFound(ctx, err)
|
||||
}
|
||||
r.logger.ErrorCtx(ctx, "cannot update tracker pattern", log.Error(err))
|
||||
return nil, gqlutils.Internal(ctx)
|
||||
}
|
||||
|
||||
bannerScope := coredata.NewScopeFromObjectID(pattern.CookieBannerID)
|
||||
banner, err := r.cookieBanner.GetCookieBanner(ctx, bannerScope, pattern.CookieBannerID)
|
||||
if err != nil {
|
||||
r.logger.ErrorCtx(ctx, "cannot get cookie banner", log.Error(err))
|
||||
return nil, gqlutils.Internal(ctx)
|
||||
}
|
||||
|
||||
return &types.UpdateTrackerPatternPayload{
|
||||
TrackerPattern: types.NewTrackerPatternNode(pattern),
|
||||
CookieBanner: types.NewCookieBanner(banner),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// DeleteTrackerPattern is the resolver for the deleteTrackerPattern field.
|
||||
func (r *mutationResolver) DeleteTrackerPattern(ctx context.Context, input types.DeleteTrackerPatternInput) (*types.DeleteTrackerPatternPayload, error) {
|
||||
if err := r.authorize(ctx, input.TrackerPatternID, probo.ActionCookiePatternDelete); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
scope := coredata.NewScopeFromObjectID(input.TrackerPatternID)
|
||||
|
||||
pattern, err := r.cookieBanner.GetTrackerPattern(ctx, scope, input.TrackerPatternID)
|
||||
if err != nil {
|
||||
if errors.Is(err, cookiebanner.ErrCookiePatternNotFound) {
|
||||
return nil, gqlutils.NotFound(ctx, err)
|
||||
}
|
||||
r.logger.ErrorCtx(ctx, "cannot get tracker pattern", log.Error(err))
|
||||
return nil, gqlutils.Internal(ctx)
|
||||
}
|
||||
|
||||
bannerID := pattern.CookieBannerID
|
||||
|
||||
if err := r.cookieBanner.DeleteTrackerPattern(ctx, scope, input.TrackerPatternID); err != nil {
|
||||
if errors.Is(err, cookiebanner.ErrCookiePatternNotFound) {
|
||||
return nil, gqlutils.NotFound(ctx, err)
|
||||
}
|
||||
r.logger.ErrorCtx(ctx, "cannot delete tracker pattern", log.Error(err))
|
||||
return nil, gqlutils.Internal(ctx)
|
||||
}
|
||||
|
||||
bannerScope := coredata.NewScopeFromObjectID(bannerID)
|
||||
banner, err := r.cookieBanner.GetCookieBanner(ctx, bannerScope, bannerID)
|
||||
if err != nil {
|
||||
r.logger.ErrorCtx(ctx, "cannot get cookie banner", log.Error(err))
|
||||
return nil, gqlutils.Internal(ctx)
|
||||
}
|
||||
|
||||
return &types.DeleteTrackerPatternPayload{
|
||||
DeletedTrackerPatternID: input.TrackerPatternID,
|
||||
CookieBanner: types.NewCookieBanner(banner),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// MoveTrackerPatternToCategory is the resolver for the moveTrackerPatternToCategory field.
|
||||
func (r *mutationResolver) MoveTrackerPatternToCategory(ctx context.Context, input types.MoveTrackerPatternToCategoryInput) (*types.MoveTrackerPatternToCategoryPayload, error) {
|
||||
if err := r.authorize(ctx, input.TrackerPatternID, probo.ActionCookiePatternUpdate); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := r.authorize(ctx, input.TargetCookieCategoryID, probo.ActionCookieCategoryUpdate); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
scope := coredata.NewScopeFromObjectID(input.TrackerPatternID)
|
||||
|
||||
result, err := r.cookieBanner.MoveTrackerPatternToCategory(
|
||||
ctx,
|
||||
scope,
|
||||
cookiebanner.MoveTrackerPatternToCategoryRequest{
|
||||
TrackerPatternID: input.TrackerPatternID,
|
||||
TargetCookieCategoryID: input.TargetCookieCategoryID,
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
switch {
|
||||
case errors.Is(err, cookiebanner.ErrCategoryNotFound):
|
||||
return nil, gqlutils.NotFound(ctx, err)
|
||||
case errors.Is(err, cookiebanner.ErrCookiePatternNotFound):
|
||||
return nil, gqlutils.NotFound(ctx, err)
|
||||
case errors.Is(err, cookiebanner.ErrCategoriesBannerMismatch):
|
||||
return nil, gqlutils.NotFoundf(ctx, "tracker pattern or target category not found")
|
||||
default:
|
||||
r.logger.ErrorCtx(ctx, "cannot move tracker pattern to category", log.Error(err))
|
||||
return nil, gqlutils.Internal(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
return &types.MoveTrackerPatternToCategoryPayload{
|
||||
TrackerPattern: types.NewTrackerPatternNode(result.TrackerPattern),
|
||||
CookieBanner: types.NewCookieBanner(result.Banner),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// CookieCategory is the resolver for the cookieCategory field.
|
||||
func (r *trackerPatternResolver) CookieCategory(ctx context.Context, obj *types.TrackerPattern) (*types.CookieCategory, error) {
|
||||
if err := r.authorize(ctx, obj.CookieCategory.ID, probo.ActionCookieCategoryGet); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
loaders := dataloader.FromContext(ctx)
|
||||
|
||||
category, err := loaders.CookieCategory.Load(ctx, obj.CookieCategory.ID)
|
||||
if err != nil {
|
||||
if errors.Is(err, coredata.ErrResourceNotFound) || errors.Is(err, dataloadgen.ErrNotFound) {
|
||||
return nil, nil
|
||||
}
|
||||
r.logger.ErrorCtx(ctx, "cannot get cookie category", log.Error(err))
|
||||
return nil, gqlutils.Internal(ctx)
|
||||
}
|
||||
|
||||
return types.NewCookieCategory(category), nil
|
||||
}
|
||||
|
||||
// DetectedCount is the resolver for the detectedCount field.
|
||||
func (r *trackerPatternResolver) DetectedCount(ctx context.Context, obj *types.TrackerPattern) (int, error) {
|
||||
scope := coredata.NewScopeFromObjectID(obj.ID)
|
||||
|
||||
count, err := r.cookieBanner.CountDetectedTrackersByPatternID(ctx, scope, obj.ID)
|
||||
if err != nil {
|
||||
r.logger.ErrorCtx(ctx, "cannot count detected trackers", log.Error(err))
|
||||
return 0, gqlutils.Internal(ctx)
|
||||
}
|
||||
|
||||
return count, nil
|
||||
}
|
||||
|
||||
// Permission is the resolver for the permission field.
|
||||
func (r *trackerPatternResolver) Permission(ctx context.Context, obj *types.TrackerPattern, action string) (bool, error) {
|
||||
return r.Resolver.Permission(ctx, obj, action)
|
||||
}
|
||||
|
||||
// TotalCount is the resolver for the totalCount field.
|
||||
func (r *trackerPatternConnectionResolver) TotalCount(ctx context.Context, obj *types.TrackerPatternConnection) (int, error) {
|
||||
scope := coredata.NewScopeFromObjectID(obj.ParentID)
|
||||
|
||||
filter := coredata.NewCookiePatternFilter(nil, nil, nil)
|
||||
if obj.Filter != nil {
|
||||
filter = filter.WithQuery(obj.Filter.Query).WithSource(obj.Filter.Source)
|
||||
}
|
||||
|
||||
count, err := r.cookieBanner.CountUncategorisedTrackerPatterns(ctx, scope, obj.ParentID, filter)
|
||||
if err != nil {
|
||||
r.logger.ErrorCtx(ctx, "cannot count uncategorised tracker patterns", log.Error(err))
|
||||
return 0, gqlutils.Internal(ctx)
|
||||
}
|
||||
|
||||
return count, nil
|
||||
}
|
||||
|
||||
// CookieBanner returns schema.CookieBannerResolver implementation.
|
||||
func (r *Resolver) CookieBanner() schema.CookieBannerResolver { return &cookieBannerResolver{r} }
|
||||
|
||||
@@ -1026,6 +1238,14 @@ func (r *Resolver) CookiePatternConnection() schema.CookiePatternConnectionResol
|
||||
return &cookiePatternConnectionResolver{r}
|
||||
}
|
||||
|
||||
// TrackerPattern returns schema.TrackerPatternResolver implementation.
|
||||
func (r *Resolver) TrackerPattern() schema.TrackerPatternResolver { return &trackerPatternResolver{r} }
|
||||
|
||||
// TrackerPatternConnection returns schema.TrackerPatternConnectionResolver implementation.
|
||||
func (r *Resolver) TrackerPatternConnection() schema.TrackerPatternConnectionResolver {
|
||||
return &trackerPatternConnectionResolver{r}
|
||||
}
|
||||
|
||||
type cookieBannerResolver struct{ *Resolver }
|
||||
type cookieBannerConnectionResolver struct{ *Resolver }
|
||||
type cookieBannerVersionResolver struct{ *Resolver }
|
||||
@@ -1033,3 +1253,5 @@ type cookieCategoryResolver struct{ *Resolver }
|
||||
type cookieCategoryConnectionResolver struct{ *Resolver }
|
||||
type cookiePatternResolver struct{ *Resolver }
|
||||
type cookiePatternConnectionResolver struct{ *Resolver }
|
||||
type trackerPatternResolver struct{ *Resolver }
|
||||
type trackerPatternConnectionResolver struct{ *Resolver }
|
||||
|
||||
@@ -160,6 +160,15 @@ type CookieBanner implements Node {
|
||||
filter: CookiePatternFilter
|
||||
): CookiePatternConnection @goField(forceResolver: true)
|
||||
|
||||
uncategorisedTrackerPatterns(
|
||||
first: Int
|
||||
after: CursorKey
|
||||
last: Int
|
||||
before: CursorKey
|
||||
orderBy: TrackerPatternOrder
|
||||
filter: TrackerPatternFilter
|
||||
): TrackerPatternConnection @goField(forceResolver: true)
|
||||
|
||||
createdAt: Datetime!
|
||||
updatedAt: Datetime!
|
||||
|
||||
@@ -285,6 +294,56 @@ type CookiePatternEdge {
|
||||
node: CookiePattern!
|
||||
}
|
||||
|
||||
type TrackerPattern implements Node {
|
||||
id: ID!
|
||||
cookieCategory: CookieCategory @goField(forceResolver: true)
|
||||
trackerType: TrackerType!
|
||||
pattern: String!
|
||||
matchType: CookiePatternMatchType!
|
||||
displayName: String!
|
||||
maxAgeSeconds: Int
|
||||
description: String!
|
||||
source: CookieSource
|
||||
excluded: Boolean!
|
||||
detectedCount: Int! @goField(forceResolver: true)
|
||||
lastMatchedAt: Datetime
|
||||
createdAt: Datetime!
|
||||
updatedAt: Datetime!
|
||||
|
||||
permission(action: String!): Boolean! @goField(forceResolver: true)
|
||||
}
|
||||
|
||||
type TrackerPatternConnection
|
||||
@goModel(
|
||||
model: "go.probo.inc/probo/pkg/server/api/console/v1/types.TrackerPatternConnection"
|
||||
) {
|
||||
totalCount: Int! @goField(forceResolver: true)
|
||||
edges: [TrackerPatternEdge!]!
|
||||
pageInfo: PageInfo!
|
||||
}
|
||||
|
||||
type TrackerPatternEdge {
|
||||
cursor: CursorKey!
|
||||
node: TrackerPattern!
|
||||
}
|
||||
|
||||
input TrackerPatternOrder
|
||||
@goModel(
|
||||
model: "go.probo.inc/probo/pkg/server/api/console/v1/types.TrackerPatternOrderBy"
|
||||
) {
|
||||
direction: OrderDirection!
|
||||
field: CookiePatternOrderField!
|
||||
}
|
||||
|
||||
input TrackerPatternFilter
|
||||
@goModel(
|
||||
model: "go.probo.inc/probo/pkg/server/api/console/v1/types.TrackerPatternFilter"
|
||||
) {
|
||||
query: String
|
||||
source: CookieSource
|
||||
trackerType: TrackerType
|
||||
}
|
||||
|
||||
type CookieBannerVersion implements Node {
|
||||
id: ID!
|
||||
version: Int!
|
||||
@@ -382,6 +441,15 @@ extend type Mutation {
|
||||
upsertCookieBannerTranslation(
|
||||
input: UpsertCookieBannerTranslationInput!
|
||||
): UpsertCookieBannerTranslationPayload!
|
||||
updateTrackerPattern(
|
||||
input: UpdateTrackerPatternInput!
|
||||
): UpdateTrackerPatternPayload!
|
||||
deleteTrackerPattern(
|
||||
input: DeleteTrackerPatternInput!
|
||||
): DeleteTrackerPatternPayload!
|
||||
moveTrackerPatternToCategory(
|
||||
input: MoveTrackerPatternToCategoryInput!
|
||||
): MoveTrackerPatternToCategoryPayload!
|
||||
}
|
||||
|
||||
input CreateCookieBannerInput {
|
||||
@@ -547,3 +615,35 @@ type UpsertCookieBannerTranslationPayload {
|
||||
cookieBannerTranslation: CookieBannerTranslation!
|
||||
cookieBanner: CookieBanner!
|
||||
}
|
||||
|
||||
input UpdateTrackerPatternInput {
|
||||
trackerPatternId: ID!
|
||||
displayName: String
|
||||
maxAgeSeconds: Int @goField(omittable: true)
|
||||
description: String
|
||||
excluded: Boolean
|
||||
}
|
||||
|
||||
input DeleteTrackerPatternInput {
|
||||
trackerPatternId: ID!
|
||||
}
|
||||
|
||||
input MoveTrackerPatternToCategoryInput {
|
||||
trackerPatternId: ID!
|
||||
targetCookieCategoryId: ID!
|
||||
}
|
||||
|
||||
type UpdateTrackerPatternPayload {
|
||||
trackerPattern: TrackerPattern!
|
||||
cookieBanner: CookieBanner!
|
||||
}
|
||||
|
||||
type DeleteTrackerPatternPayload {
|
||||
deletedTrackerPatternId: ID!
|
||||
cookieBanner: CookieBanner!
|
||||
}
|
||||
|
||||
type MoveTrackerPatternToCategoryPayload {
|
||||
trackerPattern: TrackerPattern!
|
||||
cookieBanner: CookieBanner!
|
||||
}
|
||||
|
||||
102
pkg/server/api/console/v1/types/tracker_pattern.go
Normal file
102
pkg/server/api/console/v1/types/tracker_pattern.go
Normal file
@@ -0,0 +1,102 @@
|
||||
// Copyright (c) 2026 Probo Inc <hello@getprobo.com>.
|
||||
//
|
||||
// Permission to use, copy, modify, and/or distribute this software for any
|
||||
// purpose with or without fee is hereby granted, provided that the above
|
||||
// copyright notice and this permission notice appear in all copies.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
||||
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
||||
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
||||
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
||||
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
|
||||
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
||||
// PERFORMANCE OF THIS SOFTWARE.
|
||||
|
||||
package types
|
||||
|
||||
import (
|
||||
"go.probo.inc/probo/pkg/coredata"
|
||||
"go.probo.inc/probo/pkg/gid"
|
||||
"go.probo.inc/probo/pkg/page"
|
||||
)
|
||||
|
||||
type (
|
||||
TrackerPatternOrderBy OrderBy[coredata.CookiePatternOrderField]
|
||||
|
||||
TrackerPatternConnection struct {
|
||||
TotalCount int
|
||||
Edges []*TrackerPatternEdge
|
||||
PageInfo PageInfo
|
||||
|
||||
Resolver any
|
||||
ParentID gid.GID
|
||||
Filter *TrackerPatternFilter
|
||||
}
|
||||
|
||||
TrackerPatternFilter struct {
|
||||
Query *string
|
||||
Source *coredata.CookieSource
|
||||
TrackerType *coredata.TrackerType
|
||||
}
|
||||
)
|
||||
|
||||
func NewTrackerPatternConnection(
|
||||
p *page.Page[*coredata.TrackerPattern, coredata.CookiePatternOrderField],
|
||||
parentType any,
|
||||
parentID gid.GID,
|
||||
) *TrackerPatternConnection {
|
||||
edges := make([]*TrackerPatternEdge, len(p.Data))
|
||||
|
||||
for i := range edges {
|
||||
edges[i] = NewTrackerPatternEdge(p.Data[i], p.Cursor.OrderBy.Field)
|
||||
}
|
||||
|
||||
return &TrackerPatternConnection{
|
||||
Edges: edges,
|
||||
PageInfo: *NewPageInfo(p),
|
||||
|
||||
Resolver: parentType,
|
||||
ParentID: parentID,
|
||||
}
|
||||
}
|
||||
|
||||
func NewTrackerPatternConnectionWithFilter(
|
||||
p *page.Page[*coredata.TrackerPattern, coredata.CookiePatternOrderField],
|
||||
parentType any,
|
||||
parentID gid.GID,
|
||||
filter *TrackerPatternFilter,
|
||||
) *TrackerPatternConnection {
|
||||
conn := NewTrackerPatternConnection(p, parentType, parentID)
|
||||
conn.Filter = filter
|
||||
return conn
|
||||
}
|
||||
|
||||
func NewTrackerPatternEdge(tp *coredata.TrackerPattern, orderBy coredata.CookiePatternOrderField) *TrackerPatternEdge {
|
||||
return &TrackerPatternEdge{
|
||||
Cursor: tp.CursorKey(orderBy),
|
||||
Node: NewTrackerPatternNode(tp),
|
||||
}
|
||||
}
|
||||
|
||||
func NewTrackerPatternNode(tp *coredata.TrackerPattern) *TrackerPattern {
|
||||
return &TrackerPattern{
|
||||
ID: tp.ID,
|
||||
CookieCategory: &CookieCategory{
|
||||
ID: tp.CookieCategoryID,
|
||||
CookieBanner: &CookieBanner{
|
||||
ID: tp.CookieBannerID,
|
||||
},
|
||||
},
|
||||
TrackerType: tp.TrackerType,
|
||||
Pattern: tp.Pattern,
|
||||
MatchType: tp.MatchType,
|
||||
DisplayName: tp.DisplayName,
|
||||
MaxAgeSeconds: tp.MaxAgeSeconds,
|
||||
Description: tp.Description,
|
||||
Source: tp.Source,
|
||||
Excluded: tp.Excluded,
|
||||
LastMatchedAt: tp.LastMatchedAt,
|
||||
CreatedAt: tp.CreatedAt,
|
||||
UpdatedAt: tp.UpdatedAt,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user