Add cookie pattern entity to group detected cookies

Introduce a cookie_patterns table that groups cookies sharing a common
prefix (e.g. phc_*) into a single manageable row. Every cookie now
belongs to a pattern (EXACT or PREFIX match type). Category, description,
and display metadata move from cookies to patterns, making patterns the
unit of management and display in the console and published snapshots.

Signed-off-by: Émile Ré <emile@getprobo.com>
This commit is contained in:
Émile Ré
2026-04-29 13:52:01 +04:00
parent 48606f34c1
commit f9fec45eb1
9 changed files with 980 additions and 143 deletions

View File

@@ -335,17 +335,17 @@ func CanonicalizeOrigin(raw string) string {
func buildSnapshot( func buildSnapshot(
banner *coredata.CookieBanner, banner *coredata.CookieBanner,
categories coredata.CookieCategories, categories coredata.CookieCategories,
allCookies coredata.Cookies, allPatterns coredata.CookiePatterns,
translations coredata.CookieBannerTranslations, translations coredata.CookieBannerTranslations,
) coredata.CookieBannerVersionSnapshot { ) coredata.CookieBannerVersionSnapshot {
cookiesByCategory := make(map[gid.GID]coredata.CookieItems) cookiesByCategory := make(map[gid.GID]coredata.CookieItems)
for _, c := range allCookies { for _, p := range allPatterns {
cookiesByCategory[c.CookieCategoryID] = append( cookiesByCategory[p.CookieCategoryID] = append(
cookiesByCategory[c.CookieCategoryID], cookiesByCategory[p.CookieCategoryID],
coredata.CookieItem{ coredata.CookieItem{
Name: c.Name, Name: p.DisplayName,
Duration: c.Duration, Duration: p.Duration,
Description: c.Description, Description: p.Description,
}, },
) )
} }
@@ -448,10 +448,10 @@ func (s *Service) ensureDraftVersion(
scope coredata.Scoper, scope coredata.Scoper,
banner *coredata.CookieBanner, banner *coredata.CookieBanner,
categories coredata.CookieCategories, categories coredata.CookieCategories,
allCookies coredata.Cookies, allPatterns coredata.CookiePatterns,
translations coredata.CookieBannerTranslations, translations coredata.CookieBannerTranslations,
) (*coredata.CookieBannerVersion, error) { ) (*coredata.CookieBannerVersion, error) {
snapshot := buildSnapshot(banner, categories, allCookies, translations) snapshot := buildSnapshot(banner, categories, allPatterns, translations)
var latest coredata.CookieBannerVersion var latest coredata.CookieBannerVersion
err := latest.LoadLatestByCookieBannerID(ctx, tx, scope, banner.ID) err := latest.LoadLatestByCookieBannerID(ctx, tx, scope, banner.ID)
@@ -514,9 +514,9 @@ func (s *Service) ensureDraftVersionForBanner(
return nil, fmt.Errorf("cannot load cookie categories: %w", err) return nil, fmt.Errorf("cannot load cookie categories: %w", err)
} }
var allCookies coredata.Cookies var allPatterns coredata.CookiePatterns
if err := allCookies.LoadAllByCookieBannerID(ctx, tx, scope, bannerID); err != nil { if err := allPatterns.LoadAllByCookieBannerID(ctx, tx, scope, bannerID); err != nil {
return nil, fmt.Errorf("cannot load cookies: %w", err) return nil, fmt.Errorf("cannot load cookie patterns: %w", err)
} }
var translations coredata.CookieBannerTranslations var translations coredata.CookieBannerTranslations
@@ -524,7 +524,7 @@ func (s *Service) ensureDraftVersionForBanner(
return nil, fmt.Errorf("cannot load cookie banner translations: %w", err) return nil, fmt.Errorf("cannot load cookie banner translations: %w", err)
} }
return s.ensureDraftVersion(ctx, tx, scope, &banner, categories, allCookies, translations) return s.ensureDraftVersion(ctx, tx, scope, &banner, categories, allPatterns, translations)
} }
func (s *Service) CreateCookieBanner( func (s *Service) CreateCookieBanner(
@@ -594,18 +594,35 @@ func (s *Service) CreateCookieBanner(
slugToGID[dc.Slug] = category.ID slugToGID[dc.Slug] = category.ID
if dc.Kind == coredata.CookieCategoryKindNecessary { if dc.Kind == coredata.CookieCategoryKindNecessary {
consentCookie := &coredata.Cookie{ consentPattern := &coredata.CookiePattern{
ID: gid.New(scope.GetTenantID(), coredata.CookieEntityType), ID: gid.New(scope.GetTenantID(), coredata.CookiePatternEntityType),
OrganizationID: banner.OrganizationID, OrganizationID: banner.OrganizationID,
CookieBannerID: banner.ID, CookieBannerID: banner.ID,
CookieCategoryID: category.ID, CookieCategoryID: category.ID,
Name: "probo_consent", Pattern: "probo_consent",
MatchType: coredata.CookiePatternMatchTypeExact,
DisplayName: "probo_consent",
Duration: fmt.Sprintf("%d days", req.ConsentExpiryDays), Duration: fmt.Sprintf("%d days", req.ConsentExpiryDays),
Description: "Stores your cookie consent preferences for this website.", Description: "Stores your cookie consent preferences for this website.",
Source: coredata.CookieSourceScript, Source: coredata.CookieSourceScript,
CreatedAt: now, CreatedAt: now,
UpdatedAt: now, UpdatedAt: now,
} }
if err := consentPattern.Insert(ctx, tx, scope); err != nil {
return fmt.Errorf("cannot insert probo_consent pattern: %w", err)
}
consentCookie := &coredata.Cookie{
ID: gid.New(scope.GetTenantID(), coredata.CookieEntityType),
OrganizationID: banner.OrganizationID,
CookieBannerID: banner.ID,
CookiePatternID: consentPattern.ID,
Name: "probo_consent",
Duration: fmt.Sprintf("%d days", req.ConsentExpiryDays),
Source: coredata.CookieSourceScript,
CreatedAt: now,
UpdatedAt: now,
}
if err := consentCookie.Insert(ctx, tx, scope); err != nil { if err := consentCookie.Insert(ctx, tx, scope); err != nil {
return fmt.Errorf("cannot insert probo_consent cookie: %w", err) return fmt.Errorf("cannot insert probo_consent cookie: %w", err)
} }
@@ -1199,12 +1216,14 @@ func (s *Service) CreateCookie(
now := time.Now() now := time.Now()
cookie = &coredata.Cookie{ pattern := &coredata.CookiePattern{
ID: gid.New(scope.GetTenantID(), coredata.CookieEntityType), ID: gid.New(scope.GetTenantID(), coredata.CookiePatternEntityType),
OrganizationID: category.OrganizationID, OrganizationID: category.OrganizationID,
CookieBannerID: category.CookieBannerID, CookieBannerID: category.CookieBannerID,
CookieCategoryID: category.ID, CookieCategoryID: category.ID,
Name: req.Name, Pattern: req.Name,
MatchType: coredata.CookiePatternMatchTypeExact,
DisplayName: req.Name,
Duration: req.Duration, Duration: req.Duration,
Description: req.Description, Description: req.Description,
Source: coredata.CookieSourceScript, Source: coredata.CookieSourceScript,
@@ -1212,6 +1231,25 @@ func (s *Service) CreateCookie(
UpdatedAt: now, UpdatedAt: now,
} }
if err := pattern.Insert(ctx, tx, scope); err != nil {
if errors.Is(err, coredata.ErrResourceAlreadyExists) {
return ErrCookieNameAlreadyExists
}
return fmt.Errorf("cannot insert cookie pattern: %w", err)
}
cookie = &coredata.Cookie{
ID: gid.New(scope.GetTenantID(), coredata.CookieEntityType),
OrganizationID: category.OrganizationID,
CookieBannerID: category.CookieBannerID,
CookiePatternID: pattern.ID,
Name: req.Name,
Duration: req.Duration,
Source: coredata.CookieSourceScript,
CreatedAt: now,
UpdatedAt: now,
}
if err := cookie.Insert(ctx, tx, scope); err != nil { if err := cookie.Insert(ctx, tx, scope); err != nil {
if errors.Is(err, coredata.ErrResourceAlreadyExists) { if errors.Is(err, coredata.ErrResourceAlreadyExists) {
return ErrCookieNameAlreadyExists return ErrCookieNameAlreadyExists
@@ -1260,6 +1298,33 @@ func (s *Service) GetCookie(
return &cookie, nil return &cookie, nil
} }
func (s *Service) GetCookiePattern(
ctx context.Context,
scope coredata.Scoper,
cookiePatternID gid.GID,
) (*coredata.CookiePattern, error) {
var pattern coredata.CookiePattern
err := s.pg.WithConn(
ctx,
func(ctx context.Context, conn pg.Querier) error {
if err := pattern.LoadByID(ctx, conn, scope, cookiePatternID); err != nil {
if errors.Is(err, coredata.ErrResourceNotFound) {
return ErrCookieNotFound
}
return fmt.Errorf("cannot load cookie pattern: %w", err)
}
return nil
},
)
if err != nil {
return nil, err
}
return &pattern, nil
}
func (s *Service) UpdateCookie( func (s *Service) UpdateCookie(
ctx context.Context, ctx context.Context,
scope coredata.Scoper, scope coredata.Scoper,
@@ -1281,22 +1346,13 @@ func (s *Service) UpdateCookie(
return fmt.Errorf("cannot load cookie: %w", err) return fmt.Errorf("cannot load cookie: %w", err)
} }
if req.Name != nil {
cookie.Name = *req.Name
}
if req.Duration != nil { if req.Duration != nil {
cookie.Duration = *req.Duration cookie.Duration = *req.Duration
} }
if req.Description != nil {
cookie.Description = *req.Description
}
cookie.UpdatedAt = time.Now() cookie.UpdatedAt = time.Now()
if err := cookie.Update(ctx, tx, scope); err != nil { if err := cookie.Update(ctx, tx, scope); err != nil {
if errors.Is(err, coredata.ErrResourceAlreadyExists) {
return ErrCookieNameAlreadyExists
}
return fmt.Errorf("cannot update cookie: %w", err) return fmt.Errorf("cannot update cookie: %w", err)
} }
@@ -1354,7 +1410,7 @@ func (s *Service) ListCookiesForCategory(
err := s.pg.WithConn( err := s.pg.WithConn(
ctx, ctx,
func(ctx context.Context, conn pg.Querier) error { func(ctx context.Context, conn pg.Querier) error {
if err := cookies.LoadByCookieCategoryID(ctx, conn, scope, categoryID, cursor); err != nil { if err := cookies.LoadByCookieCategoryIDViaPattern(ctx, conn, scope, categoryID, cursor); err != nil {
return fmt.Errorf("cannot list cookies: %w", err) return fmt.Errorf("cannot list cookies: %w", err)
} }
@@ -1381,7 +1437,7 @@ func (s *Service) CountCookiesForCategory(
var cookies coredata.Cookies var cookies coredata.Cookies
var err error var err error
count, err = cookies.CountByCookieCategoryID(ctx, conn, scope, categoryID) count, err = cookies.CountByCookieCategoryIDViaPattern(ctx, conn, scope, categoryID)
if err != nil { if err != nil {
return fmt.Errorf("cannot count cookies: %w", err) return fmt.Errorf("cannot count cookies: %w", err)
} }
@@ -1500,7 +1556,12 @@ func (s *Service) MoveCookieToCategory(
return fmt.Errorf("cannot load target cookie category: %w", err) return fmt.Errorf("cannot load target cookie category: %w", err)
} }
if cookie.CookieCategoryID == target.ID { var pattern coredata.CookiePattern
if err := pattern.LoadByID(ctx, tx, scope, cookie.CookiePatternID); err != nil {
return fmt.Errorf("cannot load cookie pattern: %w", err)
}
if pattern.CookieCategoryID == target.ID {
return ErrSameCategoryMove return ErrSameCategoryMove
} }
@@ -1508,11 +1569,11 @@ func (s *Service) MoveCookieToCategory(
return ErrCategoriesBannerMismatch return ErrCategoriesBannerMismatch
} }
cookie.CookieCategoryID = target.ID pattern.CookieCategoryID = target.ID
cookie.UpdatedAt = time.Now() pattern.UpdatedAt = time.Now()
if err := cookie.Update(ctx, tx, scope); err != nil { if err := pattern.Update(ctx, tx, scope); err != nil {
return fmt.Errorf("cannot update cookie: %w", err) return fmt.Errorf("cannot update cookie pattern: %w", err)
} }
var banner coredata.CookieBanner var banner coredata.CookieBanner
@@ -1611,9 +1672,9 @@ func (s *Service) DeleteCookieCategory(
return fmt.Errorf("cannot load uncategorised cookie category: %w", err) return fmt.Errorf("cannot load uncategorised cookie category: %w", err)
} }
var cookies coredata.Cookies var patterns coredata.CookiePatterns
if err := cookies.MoveToCategoryByCookieCategoryID(ctx, tx, scope, category.ID, uncategorised.ID); err != nil { if err := patterns.MoveToCategoryByCookieCategoryID(ctx, tx, scope, category.ID, uncategorised.ID); err != nil {
return fmt.Errorf("cannot move cookies to uncategorised: %w", err) return fmt.Errorf("cannot move cookie patterns to uncategorised: %w", err)
} }
if err := category.Delete(ctx, tx, scope); err != nil { if err := category.Delete(ctx, tx, scope); err != nil {
@@ -2204,26 +2265,53 @@ func (s *Service) ReportDetectedCookies(
now := time.Now() now := time.Now()
for _, dc := range req.Cookies { for _, dc := range req.Cookies {
cookie := &coredata.Cookie{ var matchedPattern coredata.CookiePattern
ID: gid.New(scope.GetTenantID(), coredata.CookieEntityType), err := matchedPattern.FindMatchingPattern(ctx, tx, scope, banner.ID, dc.Name)
OrganizationID: banner.OrganizationID, if err != nil && !errors.Is(err, coredata.ErrResourceNotFound) {
CookieBannerID: banner.ID, return fmt.Errorf("cannot find matching pattern: %w", err)
CookieCategoryID: uncategorised.ID,
Name: dc.Name,
Duration: dc.Duration,
Description: "",
Source: dc.Source,
CreatedAt: now,
UpdatedAt: now,
} }
ok, err := cookie.InsertIfNotExists(ctx, tx, scope) patternID := matchedPattern.ID
if err != nil { if errors.Is(err, coredata.ErrResourceNotFound) {
return fmt.Errorf("cannot insert detected cookie: %w", err) newPattern := &coredata.CookiePattern{
} ID: gid.New(scope.GetTenantID(), coredata.CookiePatternEntityType),
if ok { OrganizationID: banner.OrganizationID,
CookieBannerID: banner.ID,
CookieCategoryID: uncategorised.ID,
Pattern: dc.Name,
MatchType: coredata.CookiePatternMatchTypeExact,
DisplayName: dc.Name,
Duration: dc.Duration,
Description: "",
Source: dc.Source,
CreatedAt: now,
UpdatedAt: now,
}
if err := newPattern.Insert(ctx, tx, scope); err != nil {
if errors.Is(err, coredata.ErrResourceAlreadyExists) {
continue
}
return fmt.Errorf("cannot insert cookie pattern: %w", err)
}
patternID = newPattern.ID
inserted++ inserted++
} }
cookie := &coredata.Cookie{
ID: gid.New(scope.GetTenantID(), coredata.CookieEntityType),
OrganizationID: banner.OrganizationID,
CookieBannerID: banner.ID,
CookiePatternID: patternID,
Name: dc.Name,
Duration: dc.Duration,
Source: dc.Source,
CreatedAt: now,
UpdatedAt: now,
}
if _, err := cookie.InsertIfNotExists(ctx, tx, scope); err != nil {
return fmt.Errorf("cannot insert detected cookie: %w", err)
}
} }
if inserted > 0 { if inserted > 0 {

View File

@@ -30,16 +30,15 @@ import (
type ( type (
Cookie struct { Cookie struct {
ID gid.GID `db:"id"` ID gid.GID `db:"id"`
OrganizationID gid.GID `db:"organization_id"` OrganizationID gid.GID `db:"organization_id"`
CookieBannerID gid.GID `db:"cookie_banner_id"` CookieBannerID gid.GID `db:"cookie_banner_id"`
CookieCategoryID gid.GID `db:"cookie_category_id"` CookiePatternID gid.GID `db:"cookie_pattern_id"`
Name string `db:"name"` Name string `db:"name"`
Duration string `db:"duration"` Duration string `db:"duration"`
Description string `db:"description"` Source CookieSource `db:"source"`
Source CookieSource `db:"source"` CreatedAt time.Time `db:"created_at"`
CreatedAt time.Time `db:"created_at"` UpdatedAt time.Time `db:"updated_at"`
UpdatedAt time.Time `db:"updated_at"`
} }
Cookies []*Cookie Cookies []*Cookie
@@ -80,10 +79,9 @@ SELECT
id, id,
organization_id, organization_id,
cookie_banner_id, cookie_banner_id,
cookie_category_id, cookie_pattern_id,
name, name,
duration, duration,
description,
source, source,
created_at, created_at,
updated_at updated_at
@@ -118,7 +116,85 @@ LIMIT 1;
return nil return nil
} }
func (c *Cookies) LoadByCookieCategoryID( func (c *Cookies) LoadByCookiePatternID(
ctx context.Context,
conn pg.Querier,
scope Scoper,
cookiePatternID gid.GID,
cursor *page.Cursor[CookieOrderField],
) error {
q := `
SELECT
id,
organization_id,
cookie_banner_id,
cookie_pattern_id,
name,
duration,
source,
created_at,
updated_at
FROM
cookies
WHERE
%s
AND cookie_pattern_id = @cookie_pattern_id
AND %s
`
q = fmt.Sprintf(q, scope.SQLFragment(), cursor.SQLFragment())
args := pgx.StrictNamedArgs{"cookie_pattern_id": cookiePatternID}
maps.Copy(args, scope.SQLArguments())
maps.Copy(args, cursor.SQLArguments())
rows, err := conn.Query(ctx, q, args)
if err != nil {
return fmt.Errorf("cannot query cookies: %w", err)
}
cookies, err := pgx.CollectRows(rows, pgx.RowToAddrOfStructByName[Cookie])
if err != nil {
return fmt.Errorf("cannot collect cookies: %w", err)
}
*c = cookies
return nil
}
func (c *Cookies) CountByCookiePatternID(
ctx context.Context,
conn pg.Querier,
scope Scoper,
cookiePatternID gid.GID,
) (int, error) {
q := `
SELECT
COUNT(id)
FROM
cookies
WHERE
%s
AND cookie_pattern_id = @cookie_pattern_id
`
q = fmt.Sprintf(q, scope.SQLFragment())
args := pgx.StrictNamedArgs{"cookie_pattern_id": cookiePatternID}
maps.Copy(args, scope.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 (c *Cookies) LoadByCookieCategoryIDViaPattern(
ctx context.Context, ctx context.Context,
conn pg.Querier, conn pg.Querier,
scope Scoper, scope Scoper,
@@ -130,10 +206,9 @@ SELECT
id, id,
organization_id, organization_id,
cookie_banner_id, cookie_banner_id,
cookie_category_id, cookie_pattern_id,
name, name,
duration, duration,
description,
source, source,
created_at, created_at,
updated_at updated_at
@@ -141,7 +216,9 @@ FROM
cookies cookies
WHERE WHERE
%s %s
AND cookie_category_id = @cookie_category_id AND cookie_pattern_id IN (
SELECT id FROM cookie_patterns WHERE cookie_category_id = @cookie_category_id
)
AND %s AND %s
` `
@@ -166,7 +243,7 @@ WHERE
return nil return nil
} }
func (c *Cookies) CountByCookieCategoryID( func (c *Cookies) CountByCookieCategoryIDViaPattern(
ctx context.Context, ctx context.Context,
conn pg.Querier, conn pg.Querier,
scope Scoper, scope Scoper,
@@ -179,7 +256,9 @@ FROM
cookies cookies
WHERE WHERE
%s %s
AND cookie_category_id = @cookie_category_id AND cookie_pattern_id IN (
SELECT id FROM cookie_patterns WHERE cookie_category_id = @cookie_category_id
)
` `
q = fmt.Sprintf(q, scope.SQLFragment()) q = fmt.Sprintf(q, scope.SQLFragment())
@@ -208,10 +287,9 @@ SELECT
id, id,
organization_id, organization_id,
cookie_banner_id, cookie_banner_id,
cookie_category_id, cookie_pattern_id,
name, name,
duration, duration,
description,
source, source,
created_at, created_at,
updated_at updated_at
@@ -255,10 +333,9 @@ INSERT INTO cookies (
tenant_id, tenant_id,
organization_id, organization_id,
cookie_banner_id, cookie_banner_id,
cookie_category_id, cookie_pattern_id,
name, name,
duration, duration,
description,
source, source,
created_at, created_at,
updated_at updated_at
@@ -267,10 +344,9 @@ INSERT INTO cookies (
@tenant_id, @tenant_id,
@organization_id, @organization_id,
@cookie_banner_id, @cookie_banner_id,
@cookie_category_id, @cookie_pattern_id,
@name, @name,
@duration, @duration,
@description,
@source, @source,
@created_at, @created_at,
@updated_at @updated_at
@@ -278,17 +354,16 @@ INSERT INTO cookies (
` `
args := pgx.StrictNamedArgs{ args := pgx.StrictNamedArgs{
"id": c.ID, "id": c.ID,
"tenant_id": scope.GetTenantID(), "tenant_id": scope.GetTenantID(),
"organization_id": c.OrganizationID, "organization_id": c.OrganizationID,
"cookie_banner_id": c.CookieBannerID, "cookie_banner_id": c.CookieBannerID,
"cookie_category_id": c.CookieCategoryID, "cookie_pattern_id": c.CookiePatternID,
"name": c.Name, "name": c.Name,
"duration": c.Duration, "duration": c.Duration,
"description": c.Description, "source": c.Source,
"source": c.Source, "created_at": c.CreatedAt,
"created_at": c.CreatedAt, "updated_at": c.UpdatedAt,
"updated_at": c.UpdatedAt,
} }
_, err := tx.Exec(ctx, q, args) _, err := tx.Exec(ctx, q, args)
@@ -315,10 +390,9 @@ INSERT INTO cookies (
tenant_id, tenant_id,
organization_id, organization_id,
cookie_banner_id, cookie_banner_id,
cookie_category_id, cookie_pattern_id,
name, name,
duration, duration,
description,
source, source,
created_at, created_at,
updated_at updated_at
@@ -327,10 +401,9 @@ INSERT INTO cookies (
@tenant_id, @tenant_id,
@organization_id, @organization_id,
@cookie_banner_id, @cookie_banner_id,
@cookie_category_id, @cookie_pattern_id,
@name, @name,
@duration, @duration,
@description,
@source, @source,
@created_at, @created_at,
@updated_at @updated_at
@@ -341,18 +414,17 @@ ON CONFLICT (cookie_banner_id, name) DO UPDATE
` `
args := pgx.StrictNamedArgs{ args := pgx.StrictNamedArgs{
"id": c.ID, "id": c.ID,
"tenant_id": scope.GetTenantID(), "tenant_id": scope.GetTenantID(),
"organization_id": c.OrganizationID, "organization_id": c.OrganizationID,
"cookie_banner_id": c.CookieBannerID, "cookie_banner_id": c.CookieBannerID,
"cookie_category_id": c.CookieCategoryID, "cookie_pattern_id": c.CookiePatternID,
"name": c.Name, "name": c.Name,
"duration": c.Duration, "duration": c.Duration,
"description": c.Description, "source": c.Source,
"source": c.Source, "source_script": CookieSourceScript,
"source_script": CookieSourceScript, "created_at": c.CreatedAt,
"created_at": c.CreatedAt, "updated_at": c.UpdatedAt,
"updated_at": c.UpdatedAt,
} }
result, err := tx.Exec(ctx, q, args) result, err := tx.Exec(ctx, q, args)
@@ -371,10 +443,8 @@ func (c *Cookie) Update(
q := ` q := `
UPDATE cookies UPDATE cookies
SET SET
cookie_category_id = @cookie_category_id, cookie_pattern_id = @cookie_pattern_id,
name = @name,
duration = @duration, duration = @duration,
description = @description,
updated_at = @updated_at updated_at = @updated_at
WHERE WHERE
%s %s
@@ -384,22 +454,15 @@ WHERE
q = fmt.Sprintf(q, scope.SQLFragment()) q = fmt.Sprintf(q, scope.SQLFragment())
args := pgx.StrictNamedArgs{ args := pgx.StrictNamedArgs{
"id": c.ID, "id": c.ID,
"cookie_category_id": c.CookieCategoryID, "cookie_pattern_id": c.CookiePatternID,
"name": c.Name, "duration": c.Duration,
"duration": c.Duration, "updated_at": c.UpdatedAt,
"description": c.Description,
"updated_at": c.UpdatedAt,
} }
maps.Copy(args, scope.SQLArguments()) maps.Copy(args, scope.SQLArguments())
result, err := tx.Exec(ctx, q, args) result, err := tx.Exec(ctx, q, args)
if err != nil { if err != nil {
if pgErr, ok := errors.AsType[*pgconn.PgError](err); ok {
if pgErr.Code == "23505" && pgErr.ConstraintName == "idx_cookies_unique_name_per_banner" {
return ErrResourceAlreadyExists
}
}
return fmt.Errorf("cannot update cookie: %w", err) return fmt.Errorf("cannot update cookie: %w", err)
} }
@@ -435,35 +498,35 @@ WHERE
return nil return nil
} }
func (c *Cookies) MoveToCategoryByCookieCategoryID( func (c *Cookies) RelinkByCookiePatternID(
ctx context.Context, ctx context.Context,
tx pg.Tx, tx pg.Tx,
scope Scoper, scope Scoper,
sourceCategoryID gid.GID, sourcePatternID gid.GID,
targetCategoryID gid.GID, targetPatternID gid.GID,
) error { ) error {
q := ` q := `
UPDATE cookies UPDATE cookies
SET SET
cookie_category_id = @target_category_id, cookie_pattern_id = @target_pattern_id,
updated_at = @updated_at updated_at = @updated_at
WHERE WHERE
%s %s
AND cookie_category_id = @source_category_id AND cookie_pattern_id = @source_pattern_id
` `
q = fmt.Sprintf(q, scope.SQLFragment()) q = fmt.Sprintf(q, scope.SQLFragment())
args := pgx.StrictNamedArgs{ args := pgx.StrictNamedArgs{
"source_category_id": sourceCategoryID, "source_pattern_id": sourcePatternID,
"target_category_id": targetCategoryID, "target_pattern_id": targetPatternID,
"updated_at": time.Now(), "updated_at": time.Now(),
} }
maps.Copy(args, scope.SQLArguments()) maps.Copy(args, scope.SQLArguments())
_, err := tx.Exec(ctx, q, args) _, err := tx.Exec(ctx, q, args)
if err != nil { if err != nil {
return fmt.Errorf("cannot move cookies to category: %w", err) return fmt.Errorf("cannot relink cookies to pattern: %w", err)
} }
return nil return nil

View File

@@ -0,0 +1,488 @@
// 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 coredata
import (
"context"
"errors"
"fmt"
"maps"
"time"
"github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgconn"
"go.gearno.de/kit/pg"
"go.probo.inc/probo/pkg/gid"
"go.probo.inc/probo/pkg/page"
)
type (
CookiePattern struct {
ID gid.GID `db:"id"`
OrganizationID gid.GID `db:"organization_id"`
CookieBannerID gid.GID `db:"cookie_banner_id"`
CookieCategoryID gid.GID `db:"cookie_category_id"`
Pattern string `db:"pattern"`
MatchType CookiePatternMatchType `db:"match_type"`
DisplayName string `db:"display_name"`
Duration string `db:"duration"`
Description string `db:"description"`
Source CookieSource `db:"source"`
CreatedAt time.Time `db:"created_at"`
UpdatedAt time.Time `db:"updated_at"`
}
CookiePatterns []*CookiePattern
)
func (cp *CookiePattern) CursorKey(field CookiePatternOrderField) page.CursorKey {
switch field {
case CookiePatternOrderFieldCreatedAt:
return page.NewCursorKey(cp.ID, cp.CreatedAt)
}
panic(fmt.Sprintf("unsupported order by: %s", field))
}
func (cp *CookiePattern) AuthorizationAttributes(ctx context.Context, conn pg.Querier) (map[string]string, error) {
q := `SELECT organization_id FROM cookie_patterns WHERE id = $1 LIMIT 1;`
var organizationID gid.GID
if err := conn.QueryRow(ctx, q, cp.ID).Scan(&organizationID); err != nil {
if errors.Is(err, pgx.ErrNoRows) {
return nil, ErrResourceNotFound
}
return nil, fmt.Errorf("cannot query cookie pattern authorization attributes: %w", err)
}
return map[string]string{"organization_id": organizationID.String()}, nil
}
func (cp *CookiePattern) LoadByID(
ctx context.Context,
conn pg.Querier,
scope Scoper,
cookiePatternID gid.GID,
) error {
q := `
SELECT
id,
organization_id,
cookie_banner_id,
cookie_category_id,
pattern,
match_type,
display_name,
duration,
description,
source,
created_at,
updated_at
FROM
cookie_patterns
WHERE
%s
AND id = @cookie_pattern_id
LIMIT 1;
`
q = fmt.Sprintf(q, scope.SQLFragment())
args := pgx.StrictNamedArgs{"cookie_pattern_id": cookiePatternID}
maps.Copy(args, scope.SQLArguments())
rows, err := conn.Query(ctx, q, args)
if err != nil {
return fmt.Errorf("cannot query cookie patterns: %w", err)
}
pattern, err := pgx.CollectExactlyOneRow(rows, pgx.RowToStructByName[CookiePattern])
if err != nil {
if errors.Is(err, pgx.ErrNoRows) {
return ErrResourceNotFound
}
return fmt.Errorf("cannot collect cookie pattern: %w", err)
}
*cp = pattern
return nil
}
func (cp *CookiePattern) FindMatchingPattern(
ctx context.Context,
conn pg.Querier,
scope Scoper,
cookieBannerID gid.GID,
cookieName string,
) error {
q := `
SELECT
id,
organization_id,
cookie_banner_id,
cookie_category_id,
pattern,
match_type,
display_name,
duration,
description,
source,
created_at,
updated_at
FROM
cookie_patterns
WHERE
%s
AND cookie_banner_id = @cookie_banner_id
AND (
(match_type = @match_type_prefix AND starts_with(@cookie_name, pattern))
OR (match_type = @match_type_exact AND pattern = @cookie_name)
)
ORDER BY
CASE WHEN match_type = @match_type_prefix THEN 0 ELSE 1 END
LIMIT 1;
`
q = fmt.Sprintf(q, scope.SQLFragment())
args := pgx.StrictNamedArgs{
"cookie_banner_id": cookieBannerID,
"cookie_name": cookieName,
"match_type_prefix": CookiePatternMatchTypePrefix,
"match_type_exact": CookiePatternMatchTypeExact,
}
maps.Copy(args, scope.SQLArguments())
rows, err := conn.Query(ctx, q, args)
if err != nil {
return fmt.Errorf("cannot query cookie patterns: %w", err)
}
pattern, err := pgx.CollectExactlyOneRow(rows, pgx.RowToStructByName[CookiePattern])
if err != nil {
if errors.Is(err, pgx.ErrNoRows) {
return ErrResourceNotFound
}
return fmt.Errorf("cannot collect cookie pattern: %w", err)
}
*cp = pattern
return nil
}
func (cps *CookiePatterns) LoadByCookieCategoryID(
ctx context.Context,
conn pg.Querier,
scope Scoper,
cookieCategoryID gid.GID,
cursor *page.Cursor[CookiePatternOrderField],
) error {
q := `
SELECT
id,
organization_id,
cookie_banner_id,
cookie_category_id,
pattern,
match_type,
display_name,
duration,
description,
source,
created_at,
updated_at
FROM
cookie_patterns
WHERE
%s
AND cookie_category_id = @cookie_category_id
AND %s
`
q = fmt.Sprintf(q, scope.SQLFragment(), cursor.SQLFragment())
args := pgx.StrictNamedArgs{"cookie_category_id": cookieCategoryID}
maps.Copy(args, scope.SQLArguments())
maps.Copy(args, cursor.SQLArguments())
rows, err := conn.Query(ctx, q, args)
if err != nil {
return fmt.Errorf("cannot query cookie patterns: %w", err)
}
patterns, err := pgx.CollectRows(rows, pgx.RowToAddrOfStructByName[CookiePattern])
if err != nil {
return fmt.Errorf("cannot collect cookie patterns: %w", err)
}
*cps = patterns
return nil
}
func (cps *CookiePatterns) CountByCookieCategoryID(
ctx context.Context,
conn pg.Querier,
scope Scoper,
cookieCategoryID gid.GID,
) (int, error) {
q := `
SELECT
COUNT(id)
FROM
cookie_patterns
WHERE
%s
AND cookie_category_id = @cookie_category_id
`
q = fmt.Sprintf(q, scope.SQLFragment())
args := pgx.StrictNamedArgs{"cookie_category_id": cookieCategoryID}
maps.Copy(args, scope.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) LoadAllByCookieBannerID(
ctx context.Context,
conn pg.Querier,
scope Scoper,
cookieBannerID gid.GID,
) error {
q := `
SELECT
id,
organization_id,
cookie_banner_id,
cookie_category_id,
pattern,
match_type,
display_name,
duration,
description,
source,
created_at,
updated_at
FROM
cookie_patterns
WHERE
%s
AND cookie_banner_id = @cookie_banner_id
ORDER BY
created_at ASC, id ASC;
`
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 fmt.Errorf("cannot query cookie patterns: %w", err)
}
patterns, err := pgx.CollectRows(rows, pgx.RowToAddrOfStructByName[CookiePattern])
if err != nil {
return fmt.Errorf("cannot collect cookie patterns: %w", err)
}
*cps = patterns
return nil
}
func (cp *CookiePattern) Insert(
ctx context.Context,
tx pg.Tx,
scope Scoper,
) error {
q := `
INSERT INTO cookie_patterns (
id,
tenant_id,
organization_id,
cookie_banner_id,
cookie_category_id,
pattern,
match_type,
display_name,
duration,
description,
source,
created_at,
updated_at
) VALUES (
@id,
@tenant_id,
@organization_id,
@cookie_banner_id,
@cookie_category_id,
@pattern,
@match_type,
@display_name,
@duration,
@description,
@source,
@created_at,
@updated_at
)
`
args := pgx.StrictNamedArgs{
"id": cp.ID,
"tenant_id": scope.GetTenantID(),
"organization_id": cp.OrganizationID,
"cookie_banner_id": cp.CookieBannerID,
"cookie_category_id": cp.CookieCategoryID,
"pattern": cp.Pattern,
"match_type": cp.MatchType,
"display_name": cp.DisplayName,
"duration": cp.Duration,
"description": cp.Description,
"source": cp.Source,
"created_at": cp.CreatedAt,
"updated_at": cp.UpdatedAt,
}
_, err := tx.Exec(ctx, q, args)
if err != nil {
if pgErr, ok := errors.AsType[*pgconn.PgError](err); ok {
if pgErr.Code == "23505" && pgErr.ConstraintName == "idx_cookie_patterns_unique_pattern_per_banner" {
return ErrResourceAlreadyExists
}
}
return fmt.Errorf("cannot insert cookie pattern: %w", err)
}
return nil
}
func (cp *CookiePattern) Update(
ctx context.Context,
tx pg.Tx,
scope Scoper,
) error {
q := `
UPDATE cookie_patterns
SET
cookie_category_id = @cookie_category_id,
display_name = @display_name,
duration = @duration,
description = @description,
updated_at = @updated_at
WHERE
%s
AND id = @id
`
q = fmt.Sprintf(q, scope.SQLFragment())
args := pgx.StrictNamedArgs{
"id": cp.ID,
"cookie_category_id": cp.CookieCategoryID,
"display_name": cp.DisplayName,
"duration": cp.Duration,
"description": cp.Description,
"updated_at": cp.UpdatedAt,
}
maps.Copy(args, scope.SQLArguments())
result, err := tx.Exec(ctx, q, args)
if err != nil {
if pgErr, ok := errors.AsType[*pgconn.PgError](err); ok {
if pgErr.Code == "23505" && pgErr.ConstraintName == "idx_cookie_patterns_unique_pattern_per_banner" {
return ErrResourceAlreadyExists
}
}
return fmt.Errorf("cannot update cookie pattern: %w", err)
}
if result.RowsAffected() == 0 {
return ErrResourceNotFound
}
return nil
}
func (cp *CookiePattern) Delete(
ctx context.Context,
tx pg.Tx,
scope Scoper,
) error {
q := `
DELETE FROM cookie_patterns
WHERE
%s
AND id = @id
`
q = fmt.Sprintf(q, scope.SQLFragment())
args := pgx.StrictNamedArgs{"id": cp.ID}
maps.Copy(args, scope.SQLArguments())
_, err := tx.Exec(ctx, q, args)
if err != nil {
return fmt.Errorf("cannot delete cookie pattern: %w", err)
}
return nil
}
func (cps *CookiePatterns) MoveToCategoryByCookieCategoryID(
ctx context.Context,
tx pg.Tx,
scope Scoper,
sourceCategoryID gid.GID,
targetCategoryID gid.GID,
) error {
q := `
UPDATE cookie_patterns
SET
cookie_category_id = @target_category_id,
updated_at = @updated_at
WHERE
%s
AND cookie_category_id = @source_category_id
`
q = fmt.Sprintf(q, scope.SQLFragment())
args := pgx.StrictNamedArgs{
"source_category_id": sourceCategoryID,
"target_category_id": targetCategoryID,
"updated_at": time.Now(),
}
maps.Copy(args, scope.SQLArguments())
_, err := tx.Exec(ctx, q, args)
if err != nil {
return fmt.Errorf("cannot move cookie patterns to category: %w", err)
}
return nil
}

View File

@@ -0,0 +1,70 @@
// 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 coredata
import (
"database/sql/driver"
"fmt"
)
type CookiePatternMatchType string
const (
CookiePatternMatchTypeExact CookiePatternMatchType = "EXACT"
CookiePatternMatchTypePrefix CookiePatternMatchType = "PREFIX"
)
func CookiePatternMatchTypes() []CookiePatternMatchType {
return []CookiePatternMatchType{
CookiePatternMatchTypeExact,
CookiePatternMatchTypePrefix,
}
}
func (m CookiePatternMatchType) String() string {
return string(m)
}
func (m *CookiePatternMatchType) Scan(value any) error {
var v string
switch val := value.(type) {
case string:
v = val
case []byte:
v = string(val)
default:
return fmt.Errorf("unsupported type for CookiePatternMatchType: %T", value)
}
switch CookiePatternMatchType(v) {
case CookiePatternMatchTypeExact:
*m = CookiePatternMatchTypeExact
case CookiePatternMatchTypePrefix:
*m = CookiePatternMatchTypePrefix
default:
return fmt.Errorf("invalid CookiePatternMatchType value: %q", v)
}
return nil
}
func (m CookiePatternMatchType) Value() (driver.Value, error) {
switch m {
case CookiePatternMatchTypeExact,
CookiePatternMatchTypePrefix:
return string(m), nil
default:
return nil, fmt.Errorf("invalid CookiePatternMatchType: %s", m)
}
}

View File

@@ -0,0 +1,55 @@
// 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 coredata
import "fmt"
type CookiePatternOrderField string
const (
CookiePatternOrderFieldCreatedAt CookiePatternOrderField = "CREATED_AT"
)
func (p CookiePatternOrderField) Column() string {
switch p {
case CookiePatternOrderFieldCreatedAt:
return "created_at"
}
panic(fmt.Sprintf("unsupported order by: %s", p))
}
func (p CookiePatternOrderField) IsValid() bool {
switch p {
case CookiePatternOrderFieldCreatedAt:
return true
}
return false
}
func (p CookiePatternOrderField) String() string {
return string(p)
}
func (p *CookiePatternOrderField) UnmarshalText(text []byte) error {
*p = CookiePatternOrderField(text)
if !p.IsValid() {
return fmt.Errorf("%s is not a valid CookiePatternOrderField", string(text))
}
return nil
}
func (p CookiePatternOrderField) MarshalText() ([]byte, error) {
return []byte(p.String()), nil
}

View File

@@ -111,6 +111,7 @@ const (
CookieEntityType uint16 = 85 CookieEntityType uint16 = 85
CookieBannerTranslationEntityType uint16 = 86 CookieBannerTranslationEntityType uint16 = 86
AgentRunEntityType uint16 = 87 AgentRunEntityType uint16 = 87
CookiePatternEntityType uint16 = 88
) )
func NewEntityFromID(id gid.GID) (any, bool) { func NewEntityFromID(id gid.GID) (any, bool) {
@@ -281,6 +282,8 @@ func NewEntityFromID(id gid.GID) (any, bool) {
return &CookieBannerTranslation{ID: id}, true return &CookieBannerTranslation{ID: id}, true
case AgentRunEntityType: case AgentRunEntityType:
return &AgentRun{ID: id}, true return &AgentRun{ID: id}, true
case CookiePatternEntityType:
return &CookiePattern{ID: id}, true
default: default:
return nil, false return nil, false
} }

View File

@@ -0,0 +1,63 @@
-- 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.
CREATE TYPE cookie_pattern_match_type AS ENUM ('EXACT', 'PREFIX');
CREATE TABLE cookie_patterns (
id TEXT NOT NULL PRIMARY KEY,
tenant_id TEXT NOT NULL,
organization_id TEXT NOT NULL REFERENCES organizations(id) ON DELETE CASCADE,
cookie_banner_id TEXT NOT NULL REFERENCES cookie_banners(id) ON DELETE CASCADE,
cookie_category_id TEXT NOT NULL REFERENCES cookie_categories(id) ON DELETE CASCADE,
pattern TEXT NOT NULL,
match_type cookie_pattern_match_type NOT NULL,
display_name TEXT NOT NULL,
duration TEXT NOT NULL,
description TEXT NOT NULL,
source cookie_source NOT NULL,
created_at TIMESTAMP WITH TIME ZONE NOT NULL,
updated_at TIMESTAMP WITH TIME ZONE NOT NULL
);
CREATE UNIQUE INDEX idx_cookie_patterns_unique_pattern_per_banner
ON cookie_patterns (cookie_banner_id, pattern);
-- Backfill: create an EXACT pattern for each existing cookie
INSERT INTO cookie_patterns (
id, tenant_id, organization_id, cookie_banner_id,
cookie_category_id, pattern, match_type, display_name,
duration, description, source, created_at, updated_at
)
SELECT
generate_gid(decode_base64_unpadded(c.tenant_id), 88),
c.tenant_id, c.organization_id, c.cookie_banner_id,
c.cookie_category_id, c.name, 'EXACT', c.name,
c.duration, c.description, c.source, c.created_at, c.updated_at
FROM cookies c;
-- Link each cookie to its pattern
ALTER TABLE cookies ADD COLUMN cookie_pattern_id TEXT REFERENCES cookie_patterns(id) ON DELETE CASCADE;
UPDATE cookies c
SET cookie_pattern_id = cp.id
FROM cookie_patterns cp
WHERE cp.cookie_banner_id = c.cookie_banner_id
AND cp.pattern = c.name
AND cp.match_type = 'EXACT';
ALTER TABLE cookies ALTER COLUMN cookie_pattern_id SET NOT NULL;
-- Category and description now live on the pattern
ALTER TABLE cookies DROP COLUMN cookie_category_id;
ALTER TABLE cookies DROP COLUMN description;

View File

@@ -319,7 +319,7 @@ func (r *cookieCategoryResolver) Cookies(ctx context.Context, obj *types.CookieC
p := page.NewPage(cookies, cursor) p := page.NewPage(cookies, cursor)
return types.NewCookieConnection(p, r, obj.ID), nil return types.NewCookieConnection(p, r, obj.ID, obj.ID), nil
} }
// Permission is the resolver for the permission field. // Permission is the resolver for the permission field.
@@ -759,7 +759,7 @@ func (r *mutationResolver) MoveCookieToCategory(ctx context.Context, input types
} }
return &types.MoveCookieToCategoryPayload{ return &types.MoveCookieToCategoryPayload{
Cookie: types.NewCookie(result.Cookie), Cookie: types.NewCookie(result.Cookie, input.TargetCookieCategoryID),
CookieBanner: types.NewCookieBanner(result.Banner), CookieBanner: types.NewCookieBanner(result.Banner),
}, nil }, nil
} }
@@ -804,7 +804,7 @@ func (r *mutationResolver) CreateCookie(ctx context.Context, input types.CreateC
} }
return &types.CreateCookiePayload{ return &types.CreateCookiePayload{
CookieEdge: types.NewCookieEdge(cookie, coredata.CookieOrderFieldCreatedAt), CookieEdge: types.NewCookieEdge(cookie, coredata.CookieOrderFieldCreatedAt, input.CookieCategoryID),
CookieBanner: types.NewCookieBanner(banner), CookieBanner: types.NewCookieBanner(banner),
}, nil }, nil
} }
@@ -841,6 +841,13 @@ func (r *mutationResolver) UpdateCookie(ctx context.Context, input types.UpdateC
return nil, gqlutils.Internal(ctx) return nil, gqlutils.Internal(ctx)
} }
patternScope := coredata.NewScopeFromObjectID(cookie.CookiePatternID)
pattern, err := r.cookieBanner.GetCookiePattern(ctx, patternScope, cookie.CookiePatternID)
if err != nil {
r.logger.ErrorCtx(ctx, "cannot get cookie pattern", log.Error(err))
return nil, gqlutils.Internal(ctx)
}
bannerScope := coredata.NewScopeFromObjectID(cookie.CookieBannerID) bannerScope := coredata.NewScopeFromObjectID(cookie.CookieBannerID)
banner, err := r.cookieBanner.GetCookieBanner(ctx, bannerScope, cookie.CookieBannerID) banner, err := r.cookieBanner.GetCookieBanner(ctx, bannerScope, cookie.CookieBannerID)
if err != nil { if err != nil {
@@ -849,7 +856,7 @@ func (r *mutationResolver) UpdateCookie(ctx context.Context, input types.UpdateC
} }
return &types.UpdateCookiePayload{ return &types.UpdateCookiePayload{
Cookie: types.NewCookie(cookie), Cookie: types.NewCookie(cookie, pattern.CookieCategoryID),
CookieBanner: types.NewCookieBanner(banner), CookieBanner: types.NewCookieBanner(banner),
}, nil }, nil
} }

View File

@@ -37,11 +37,12 @@ func NewCookieConnection(
p *page.Page[*coredata.Cookie, coredata.CookieOrderField], p *page.Page[*coredata.Cookie, coredata.CookieOrderField],
parentType any, parentType any,
parentID gid.GID, parentID gid.GID,
cookieCategoryID gid.GID,
) *CookieConnection { ) *CookieConnection {
edges := make([]*CookieEdge, len(p.Data)) edges := make([]*CookieEdge, len(p.Data))
for i := range edges { for i := range edges {
edges[i] = NewCookieEdge(p.Data[i], p.Cursor.OrderBy.Field) edges[i] = NewCookieEdge(p.Data[i], p.Cursor.OrderBy.Field, cookieCategoryID)
} }
return &CookieConnection{ return &CookieConnection{
@@ -53,27 +54,26 @@ func NewCookieConnection(
} }
} }
func NewCookieEdge(c *coredata.Cookie, orderBy coredata.CookieOrderField) *CookieEdge { func NewCookieEdge(c *coredata.Cookie, orderBy coredata.CookieOrderField, cookieCategoryID gid.GID) *CookieEdge {
return &CookieEdge{ return &CookieEdge{
Cursor: c.CursorKey(orderBy), Cursor: c.CursorKey(orderBy),
Node: NewCookie(c), Node: NewCookie(c, cookieCategoryID),
} }
} }
func NewCookie(c *coredata.Cookie) *Cookie { func NewCookie(c *coredata.Cookie, cookieCategoryID gid.GID) *Cookie {
return &Cookie{ return &Cookie{
ID: c.ID, ID: c.ID,
CookieCategory: &CookieCategory{ CookieCategory: &CookieCategory{
ID: c.CookieCategoryID, ID: cookieCategoryID,
CookieBanner: &CookieBanner{ CookieBanner: &CookieBanner{
ID: c.CookieBannerID, ID: c.CookieBannerID,
}, },
}, },
Name: c.Name, Name: c.Name,
Duration: c.Duration, Duration: c.Duration,
Description: c.Description, Source: c.Source,
Source: c.Source, CreatedAt: c.CreatedAt,
CreatedAt: c.CreatedAt, UpdatedAt: c.UpdatedAt,
UpdatedAt: c.UpdatedAt,
} }
} }