Track cookie policy generation requests on publish

Add policy_document_id and policy_generation_requested_at columns to
cookie_banners and backfill banners that already have a published
version, so existing live banners get a policy on the worker's first
pass.

Flag the banner for policy generation inside the publish transaction so
generation is requested only when a cookie banner version is published,
not on draft edits. Fold policy_document_id into the existing Update so
the upcoming worker can persist the generated document id with the
scope it already holds.

Signed-off-by: Émile Ré <emile@probo.com>
This commit is contained in:
Émile Ré
2026-06-01 17:54:58 +02:00
parent 534079ddcc
commit f6eeeb675f
3 changed files with 174 additions and 27 deletions

View File

@@ -964,6 +964,11 @@ func (s *Service) PublishCookieBannerVersion(
return fmt.Errorf("cannot publish version: %w", err)
}
banner := coredata.CookieBanner{ID: bannerID}
if err := banner.SetPolicyGenerationRequested(ctx, tx); err != nil {
return fmt.Errorf("cannot request cookie policy generation: %w", err)
}
return nil
},
)

View File

@@ -31,19 +31,21 @@ import (
type (
CookieBanner struct {
ID gid.GID `db:"id"`
OrganizationID gid.GID `db:"organization_id"`
Name string `db:"name"`
Origin string `db:"origin"`
State CookieBannerState `db:"state"`
PrivacyPolicyURL *string `db:"privacy_policy_url"`
CookiePolicyURL string `db:"cookie_policy_url"`
ConsentExpiryDays int `db:"consent_expiry_days"`
ShowBranding bool `db:"show_branding"`
DefaultLanguage string `db:"default_language"`
PatternAnalysisRequestedAt *time.Time `db:"pattern_analysis_requested_at"`
CreatedAt time.Time `db:"created_at"`
UpdatedAt time.Time `db:"updated_at"`
ID gid.GID `db:"id"`
OrganizationID gid.GID `db:"organization_id"`
Name string `db:"name"`
Origin string `db:"origin"`
State CookieBannerState `db:"state"`
PrivacyPolicyURL *string `db:"privacy_policy_url"`
CookiePolicyURL string `db:"cookie_policy_url"`
ConsentExpiryDays int `db:"consent_expiry_days"`
ShowBranding bool `db:"show_branding"`
DefaultLanguage string `db:"default_language"`
PatternAnalysisRequestedAt *time.Time `db:"pattern_analysis_requested_at"`
PolicyDocumentID *gid.GID `db:"policy_document_id"`
PolicyGenerationRequestedAt *time.Time `db:"policy_generation_requested_at"`
CreatedAt time.Time `db:"created_at"`
UpdatedAt time.Time `db:"updated_at"`
}
CookieBanners []*CookieBanner
@@ -116,6 +118,8 @@ SELECT
show_branding,
default_language,
pattern_analysis_requested_at,
policy_document_id,
policy_generation_requested_at,
created_at,
updated_at
FROM
@@ -168,6 +172,8 @@ SELECT
show_branding,
default_language,
pattern_analysis_requested_at,
policy_document_id,
policy_generation_requested_at,
created_at,
updated_at
FROM
@@ -221,6 +227,8 @@ SELECT
show_branding,
default_language,
pattern_analysis_requested_at,
policy_document_id,
policy_generation_requested_at,
created_at,
updated_at
FROM
@@ -278,6 +286,8 @@ SELECT
show_branding,
default_language,
pattern_analysis_requested_at,
policy_document_id,
policy_generation_requested_at,
created_at,
updated_at
FROM
@@ -328,6 +338,8 @@ SELECT
show_branding,
default_language,
pattern_analysis_requested_at,
policy_document_id,
policy_generation_requested_at,
created_at,
updated_at
FROM
@@ -414,6 +426,8 @@ INSERT INTO cookie_banners (
show_branding,
default_language,
pattern_analysis_requested_at,
policy_document_id,
policy_generation_requested_at,
created_at,
updated_at
) VALUES (
@@ -429,26 +443,30 @@ INSERT INTO cookie_banners (
@show_branding,
@default_language,
@pattern_analysis_requested_at,
@policy_document_id,
@policy_generation_requested_at,
@created_at,
@updated_at
)
`
args := pgx.StrictNamedArgs{
"id": b.ID,
"tenant_id": scope.GetTenantID(),
"organization_id": b.OrganizationID,
"name": b.Name,
"origin": b.Origin,
"state": b.State,
"privacy_policy_url": b.PrivacyPolicyURL,
"cookie_policy_url": b.CookiePolicyURL,
"consent_expiry_days": b.ConsentExpiryDays,
"show_branding": b.ShowBranding,
"default_language": b.DefaultLanguage,
"pattern_analysis_requested_at": b.PatternAnalysisRequestedAt,
"created_at": b.CreatedAt,
"updated_at": b.UpdatedAt,
"id": b.ID,
"tenant_id": scope.GetTenantID(),
"organization_id": b.OrganizationID,
"name": b.Name,
"origin": b.Origin,
"state": b.State,
"privacy_policy_url": b.PrivacyPolicyURL,
"cookie_policy_url": b.CookiePolicyURL,
"consent_expiry_days": b.ConsentExpiryDays,
"show_branding": b.ShowBranding,
"default_language": b.DefaultLanguage,
"pattern_analysis_requested_at": b.PatternAnalysisRequestedAt,
"policy_document_id": b.PolicyDocumentID,
"policy_generation_requested_at": b.PolicyGenerationRequestedAt,
"created_at": b.CreatedAt,
"updated_at": b.UpdatedAt,
}
_, err := tx.Exec(ctx, q, args)
@@ -480,6 +498,7 @@ SET
consent_expiry_days = @consent_expiry_days,
show_branding = @show_branding,
default_language = @default_language,
policy_document_id = @policy_document_id,
updated_at = @updated_at
WHERE
%s
@@ -497,6 +516,7 @@ WHERE
"consent_expiry_days": b.ConsentExpiryDays,
"show_branding": b.ShowBranding,
"default_language": b.DefaultLanguage,
"policy_document_id": b.PolicyDocumentID,
"updated_at": b.UpdatedAt,
}
maps.Copy(args, scope.SQLArguments())
@@ -600,6 +620,8 @@ SELECT
show_branding,
default_language,
pattern_analysis_requested_at,
policy_document_id,
policy_generation_requested_at,
created_at,
updated_at
FROM
@@ -673,3 +695,96 @@ WHERE id = @id
return nil
}
func (b *CookieBanner) LoadNextForPolicyGenerationForUpdateSkipLocked(
ctx context.Context,
tx pg.Tx,
) error {
q := `
SELECT
id,
organization_id,
name,
origin,
state,
privacy_policy_url,
cookie_policy_url,
consent_expiry_days,
show_branding,
default_language,
pattern_analysis_requested_at,
policy_document_id,
policy_generation_requested_at,
created_at,
updated_at
FROM
cookie_banners
WHERE
policy_generation_requested_at IS NOT NULL
ORDER BY
policy_generation_requested_at ASC
FOR UPDATE SKIP LOCKED
LIMIT 1;
`
rows, err := tx.Query(ctx, q)
if err != nil {
return fmt.Errorf("cannot query cookie banners for policy generation: %w", err)
}
banner, err := pgx.CollectExactlyOneRow(rows, pgx.RowToStructByName[CookieBanner])
if err != nil {
if errors.Is(err, pgx.ErrNoRows) {
return ErrResourceNotFound
}
return fmt.Errorf("cannot collect cookie banner: %w", err)
}
*b = banner
return nil
}
func (b *CookieBanner) ClearPolicyGenerationRequestedAt(
ctx context.Context,
tx pg.Tx,
) error {
q := `
UPDATE cookie_banners
SET policy_generation_requested_at = NULL
WHERE id = @id
`
args := pgx.StrictNamedArgs{"id": b.ID}
_, err := tx.Exec(ctx, q, args)
if err != nil {
return fmt.Errorf("cannot clear policy generation requested at: %w", err)
}
b.PolicyGenerationRequestedAt = nil
return nil
}
func (b *CookieBanner) SetPolicyGenerationRequested(
ctx context.Context,
tx pg.Tx,
) error {
q := `
UPDATE cookie_banners
SET policy_generation_requested_at = NOW()
WHERE id = @id
AND policy_generation_requested_at IS NULL
`
args := pgx.StrictNamedArgs{"id": b.ID}
_, err := tx.Exec(ctx, q, args)
if err != nil {
return fmt.Errorf("cannot set policy generation requested: %w", err)
}
return nil
}

View File

@@ -0,0 +1,27 @@
-- 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.
ALTER TABLE cookie_banners
ADD COLUMN policy_document_id TEXT,
ADD COLUMN policy_generation_requested_at TIMESTAMP WITH TIME ZONE;
-- Backfill: any banner that already has a published version gets its cookie
-- policy generated on the worker's first pass.
UPDATE cookie_banners
SET policy_generation_requested_at = NOW()
WHERE id IN (
SELECT DISTINCT cookie_banner_id
FROM cookie_banner_versions
WHERE state = 'PUBLISHED'
);