From f6eeeb675f04d7be6b057ce71c7fb67769b99f38 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89mile=20R=C3=A9?= Date: Mon, 1 Jun 2026 17:54:58 +0200 Subject: [PATCH] Track cookie policy generation requests on publish MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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é --- pkg/cookiebanner/service.go | 5 + pkg/coredata/cookie_banner.go | 169 ++++++++++++++++--- pkg/coredata/migrations/20260602T161910Z.sql | 27 +++ 3 files changed, 174 insertions(+), 27 deletions(-) create mode 100644 pkg/coredata/migrations/20260602T161910Z.sql diff --git a/pkg/cookiebanner/service.go b/pkg/cookiebanner/service.go index 599b73a25..9ebfa2860 100644 --- a/pkg/cookiebanner/service.go +++ b/pkg/cookiebanner/service.go @@ -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 }, ) diff --git a/pkg/coredata/cookie_banner.go b/pkg/coredata/cookie_banner.go index be47357b8..171b5fc4d 100644 --- a/pkg/coredata/cookie_banner.go +++ b/pkg/coredata/cookie_banner.go @@ -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 +} diff --git a/pkg/coredata/migrations/20260602T161910Z.sql b/pkg/coredata/migrations/20260602T161910Z.sql new file mode 100644 index 000000000..226c9091a --- /dev/null +++ b/pkg/coredata/migrations/20260602T161910Z.sql @@ -0,0 +1,27 @@ +-- Copyright (c) 2026 Probo Inc . +-- +-- 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' +); \ No newline at end of file