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:
@@ -964,6 +964,11 @@ func (s *Service) PublishCookieBannerVersion(
|
|||||||
return fmt.Errorf("cannot publish version: %w", err)
|
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
|
return nil
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -31,19 +31,21 @@ import (
|
|||||||
|
|
||||||
type (
|
type (
|
||||||
CookieBanner struct {
|
CookieBanner struct {
|
||||||
ID gid.GID `db:"id"`
|
ID gid.GID `db:"id"`
|
||||||
OrganizationID gid.GID `db:"organization_id"`
|
OrganizationID gid.GID `db:"organization_id"`
|
||||||
Name string `db:"name"`
|
Name string `db:"name"`
|
||||||
Origin string `db:"origin"`
|
Origin string `db:"origin"`
|
||||||
State CookieBannerState `db:"state"`
|
State CookieBannerState `db:"state"`
|
||||||
PrivacyPolicyURL *string `db:"privacy_policy_url"`
|
PrivacyPolicyURL *string `db:"privacy_policy_url"`
|
||||||
CookiePolicyURL string `db:"cookie_policy_url"`
|
CookiePolicyURL string `db:"cookie_policy_url"`
|
||||||
ConsentExpiryDays int `db:"consent_expiry_days"`
|
ConsentExpiryDays int `db:"consent_expiry_days"`
|
||||||
ShowBranding bool `db:"show_branding"`
|
ShowBranding bool `db:"show_branding"`
|
||||||
DefaultLanguage string `db:"default_language"`
|
DefaultLanguage string `db:"default_language"`
|
||||||
PatternAnalysisRequestedAt *time.Time `db:"pattern_analysis_requested_at"`
|
PatternAnalysisRequestedAt *time.Time `db:"pattern_analysis_requested_at"`
|
||||||
CreatedAt time.Time `db:"created_at"`
|
PolicyDocumentID *gid.GID `db:"policy_document_id"`
|
||||||
UpdatedAt time.Time `db:"updated_at"`
|
PolicyGenerationRequestedAt *time.Time `db:"policy_generation_requested_at"`
|
||||||
|
CreatedAt time.Time `db:"created_at"`
|
||||||
|
UpdatedAt time.Time `db:"updated_at"`
|
||||||
}
|
}
|
||||||
|
|
||||||
CookieBanners []*CookieBanner
|
CookieBanners []*CookieBanner
|
||||||
@@ -116,6 +118,8 @@ SELECT
|
|||||||
show_branding,
|
show_branding,
|
||||||
default_language,
|
default_language,
|
||||||
pattern_analysis_requested_at,
|
pattern_analysis_requested_at,
|
||||||
|
policy_document_id,
|
||||||
|
policy_generation_requested_at,
|
||||||
created_at,
|
created_at,
|
||||||
updated_at
|
updated_at
|
||||||
FROM
|
FROM
|
||||||
@@ -168,6 +172,8 @@ SELECT
|
|||||||
show_branding,
|
show_branding,
|
||||||
default_language,
|
default_language,
|
||||||
pattern_analysis_requested_at,
|
pattern_analysis_requested_at,
|
||||||
|
policy_document_id,
|
||||||
|
policy_generation_requested_at,
|
||||||
created_at,
|
created_at,
|
||||||
updated_at
|
updated_at
|
||||||
FROM
|
FROM
|
||||||
@@ -221,6 +227,8 @@ SELECT
|
|||||||
show_branding,
|
show_branding,
|
||||||
default_language,
|
default_language,
|
||||||
pattern_analysis_requested_at,
|
pattern_analysis_requested_at,
|
||||||
|
policy_document_id,
|
||||||
|
policy_generation_requested_at,
|
||||||
created_at,
|
created_at,
|
||||||
updated_at
|
updated_at
|
||||||
FROM
|
FROM
|
||||||
@@ -278,6 +286,8 @@ SELECT
|
|||||||
show_branding,
|
show_branding,
|
||||||
default_language,
|
default_language,
|
||||||
pattern_analysis_requested_at,
|
pattern_analysis_requested_at,
|
||||||
|
policy_document_id,
|
||||||
|
policy_generation_requested_at,
|
||||||
created_at,
|
created_at,
|
||||||
updated_at
|
updated_at
|
||||||
FROM
|
FROM
|
||||||
@@ -328,6 +338,8 @@ SELECT
|
|||||||
show_branding,
|
show_branding,
|
||||||
default_language,
|
default_language,
|
||||||
pattern_analysis_requested_at,
|
pattern_analysis_requested_at,
|
||||||
|
policy_document_id,
|
||||||
|
policy_generation_requested_at,
|
||||||
created_at,
|
created_at,
|
||||||
updated_at
|
updated_at
|
||||||
FROM
|
FROM
|
||||||
@@ -414,6 +426,8 @@ INSERT INTO cookie_banners (
|
|||||||
show_branding,
|
show_branding,
|
||||||
default_language,
|
default_language,
|
||||||
pattern_analysis_requested_at,
|
pattern_analysis_requested_at,
|
||||||
|
policy_document_id,
|
||||||
|
policy_generation_requested_at,
|
||||||
created_at,
|
created_at,
|
||||||
updated_at
|
updated_at
|
||||||
) VALUES (
|
) VALUES (
|
||||||
@@ -429,26 +443,30 @@ INSERT INTO cookie_banners (
|
|||||||
@show_branding,
|
@show_branding,
|
||||||
@default_language,
|
@default_language,
|
||||||
@pattern_analysis_requested_at,
|
@pattern_analysis_requested_at,
|
||||||
|
@policy_document_id,
|
||||||
|
@policy_generation_requested_at,
|
||||||
@created_at,
|
@created_at,
|
||||||
@updated_at
|
@updated_at
|
||||||
)
|
)
|
||||||
`
|
`
|
||||||
|
|
||||||
args := pgx.StrictNamedArgs{
|
args := pgx.StrictNamedArgs{
|
||||||
"id": b.ID,
|
"id": b.ID,
|
||||||
"tenant_id": scope.GetTenantID(),
|
"tenant_id": scope.GetTenantID(),
|
||||||
"organization_id": b.OrganizationID,
|
"organization_id": b.OrganizationID,
|
||||||
"name": b.Name,
|
"name": b.Name,
|
||||||
"origin": b.Origin,
|
"origin": b.Origin,
|
||||||
"state": b.State,
|
"state": b.State,
|
||||||
"privacy_policy_url": b.PrivacyPolicyURL,
|
"privacy_policy_url": b.PrivacyPolicyURL,
|
||||||
"cookie_policy_url": b.CookiePolicyURL,
|
"cookie_policy_url": b.CookiePolicyURL,
|
||||||
"consent_expiry_days": b.ConsentExpiryDays,
|
"consent_expiry_days": b.ConsentExpiryDays,
|
||||||
"show_branding": b.ShowBranding,
|
"show_branding": b.ShowBranding,
|
||||||
"default_language": b.DefaultLanguage,
|
"default_language": b.DefaultLanguage,
|
||||||
"pattern_analysis_requested_at": b.PatternAnalysisRequestedAt,
|
"pattern_analysis_requested_at": b.PatternAnalysisRequestedAt,
|
||||||
"created_at": b.CreatedAt,
|
"policy_document_id": b.PolicyDocumentID,
|
||||||
"updated_at": b.UpdatedAt,
|
"policy_generation_requested_at": b.PolicyGenerationRequestedAt,
|
||||||
|
"created_at": b.CreatedAt,
|
||||||
|
"updated_at": b.UpdatedAt,
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err := tx.Exec(ctx, q, args)
|
_, err := tx.Exec(ctx, q, args)
|
||||||
@@ -480,6 +498,7 @@ SET
|
|||||||
consent_expiry_days = @consent_expiry_days,
|
consent_expiry_days = @consent_expiry_days,
|
||||||
show_branding = @show_branding,
|
show_branding = @show_branding,
|
||||||
default_language = @default_language,
|
default_language = @default_language,
|
||||||
|
policy_document_id = @policy_document_id,
|
||||||
updated_at = @updated_at
|
updated_at = @updated_at
|
||||||
WHERE
|
WHERE
|
||||||
%s
|
%s
|
||||||
@@ -497,6 +516,7 @@ WHERE
|
|||||||
"consent_expiry_days": b.ConsentExpiryDays,
|
"consent_expiry_days": b.ConsentExpiryDays,
|
||||||
"show_branding": b.ShowBranding,
|
"show_branding": b.ShowBranding,
|
||||||
"default_language": b.DefaultLanguage,
|
"default_language": b.DefaultLanguage,
|
||||||
|
"policy_document_id": b.PolicyDocumentID,
|
||||||
"updated_at": b.UpdatedAt,
|
"updated_at": b.UpdatedAt,
|
||||||
}
|
}
|
||||||
maps.Copy(args, scope.SQLArguments())
|
maps.Copy(args, scope.SQLArguments())
|
||||||
@@ -600,6 +620,8 @@ SELECT
|
|||||||
show_branding,
|
show_branding,
|
||||||
default_language,
|
default_language,
|
||||||
pattern_analysis_requested_at,
|
pattern_analysis_requested_at,
|
||||||
|
policy_document_id,
|
||||||
|
policy_generation_requested_at,
|
||||||
created_at,
|
created_at,
|
||||||
updated_at
|
updated_at
|
||||||
FROM
|
FROM
|
||||||
@@ -673,3 +695,96 @@ WHERE id = @id
|
|||||||
|
|
||||||
return nil
|
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
|
||||||
|
}
|
||||||
|
|||||||
27
pkg/coredata/migrations/20260602T161910Z.sql
Normal file
27
pkg/coredata/migrations/20260602T161910Z.sql
Normal 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'
|
||||||
|
);
|
||||||
Reference in New Issue
Block a user