Add version detection and draft provisioning on publish
Signed-off-by: Émile Ré <emile@getprobo.com>
This commit is contained in:
@@ -23,5 +23,6 @@ var (
|
|||||||
ErrBannerAlreadyActive = errors.New("cookie banner is already active")
|
ErrBannerAlreadyActive = errors.New("cookie banner is already active")
|
||||||
ErrBannerAlreadyInactive = errors.New("cookie banner is already inactive")
|
ErrBannerAlreadyInactive = errors.New("cookie banner is already inactive")
|
||||||
ErrVersionNotPublished = errors.New("cookie banner version is not published")
|
ErrVersionNotPublished = errors.New("cookie banner version is not published")
|
||||||
|
ErrNoDraftVersion = errors.New("no draft cookie banner version to publish")
|
||||||
ErrCannotDeleteRequiredCategory = errors.New("cannot delete required cookie category")
|
ErrCannotDeleteRequiredCategory = errors.New("cannot delete required cookie category")
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -154,6 +154,81 @@ func (r *CreateCookieConsentRecordRequest) Validate() error {
|
|||||||
return v.Error()
|
return v.Error()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func buildSnapshot(
|
||||||
|
banner *coredata.CookieBanner,
|
||||||
|
categories coredata.CookieCategories,
|
||||||
|
) coredata.CookieBannerVersionSnapshot {
|
||||||
|
snapshotCategories := make([]coredata.CookieBannerVersionSnapshotCategory, len(categories))
|
||||||
|
for i, c := range categories {
|
||||||
|
snapshotCategories[i] = coredata.CookieBannerVersionSnapshotCategory{
|
||||||
|
Name: c.Name,
|
||||||
|
Description: c.Description,
|
||||||
|
Required: c.Required,
|
||||||
|
Cookies: c.Cookies,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return coredata.CookieBannerVersionSnapshot{
|
||||||
|
PrivacyPolicyURL: banner.PrivacyPolicyURL,
|
||||||
|
ConsentExpiryDays: banner.ConsentExpiryDays,
|
||||||
|
ConsentMode: string(banner.ConsentMode),
|
||||||
|
Categories: snapshotCategories,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) ensureDraftVersion(
|
||||||
|
ctx context.Context,
|
||||||
|
tx pg.Tx,
|
||||||
|
scope coredata.Scoper,
|
||||||
|
banner *coredata.CookieBanner,
|
||||||
|
categories coredata.CookieCategories,
|
||||||
|
) (*coredata.CookieBannerVersion, error) {
|
||||||
|
snapshot := buildSnapshot(banner, categories)
|
||||||
|
|
||||||
|
var latest coredata.CookieBannerVersion
|
||||||
|
err := latest.LoadLatestByCookieBannerID(ctx, tx, scope, banner.ID)
|
||||||
|
|
||||||
|
if err == nil && latest.State == coredata.CookieBannerVersionStateDraft {
|
||||||
|
if err := latest.SetSnapshot(snapshot); err != nil {
|
||||||
|
return nil, fmt.Errorf("cannot set snapshot: %w", err)
|
||||||
|
}
|
||||||
|
latest.UpdatedAt = time.Now()
|
||||||
|
if err := latest.Update(ctx, tx, scope); err != nil {
|
||||||
|
return nil, fmt.Errorf("cannot update draft version: %w", err)
|
||||||
|
}
|
||||||
|
return &latest, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil && !errors.Is(err, coredata.ErrResourceNotFound) {
|
||||||
|
return nil, fmt.Errorf("cannot load latest version: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
now := time.Now()
|
||||||
|
version := &coredata.CookieBannerVersion{
|
||||||
|
ID: gid.New(scope.GetTenantID(), coredata.CookieBannerVersionEntityType),
|
||||||
|
CookieBannerID: banner.ID,
|
||||||
|
State: coredata.CookieBannerVersionStateDraft,
|
||||||
|
CreatedAt: now,
|
||||||
|
UpdatedAt: now,
|
||||||
|
}
|
||||||
|
|
||||||
|
nextVersion, err := version.LoadNextVersion(ctx, tx, scope, banner.ID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("cannot determine next version: %w", err)
|
||||||
|
}
|
||||||
|
version.Version = nextVersion
|
||||||
|
|
||||||
|
if err := version.SetSnapshot(snapshot); err != nil {
|
||||||
|
return nil, fmt.Errorf("cannot set snapshot: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := version.Insert(ctx, tx, scope); err != nil {
|
||||||
|
return nil, fmt.Errorf("cannot insert draft version: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return version, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (s *Service) CreateCookieBanner(
|
func (s *Service) CreateCookieBanner(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
scope coredata.Scoper,
|
scope coredata.Scoper,
|
||||||
@@ -318,6 +393,8 @@ func (s *Service) UpdateCookieBanner(
|
|||||||
return fmt.Errorf("cannot load cookie banner: %w", err)
|
return fmt.Errorf("cannot load cookie banner: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
consentChanged := req.PrivacyPolicyURL != nil || req.ConsentExpiryDays != nil || req.ConsentMode != nil
|
||||||
|
|
||||||
if req.Name != nil {
|
if req.Name != nil {
|
||||||
banner.Name = *req.Name
|
banner.Name = *req.Name
|
||||||
}
|
}
|
||||||
@@ -340,6 +417,17 @@ func (s *Service) UpdateCookieBanner(
|
|||||||
return fmt.Errorf("cannot update cookie banner: %w", err)
|
return fmt.Errorf("cannot update cookie banner: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if consentChanged {
|
||||||
|
var categories coredata.CookieCategories
|
||||||
|
if err := categories.LoadAllByCookieBannerID(ctx, tx, banner.ID); err != nil {
|
||||||
|
return fmt.Errorf("cannot load cookie categories: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := s.ensureDraftVersion(ctx, tx, scope, &banner, categories); err != nil {
|
||||||
|
return fmt.Errorf("cannot ensure draft version: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
@@ -355,63 +443,27 @@ func (s *Service) PublishCookieBannerVersion(
|
|||||||
scope coredata.Scoper,
|
scope coredata.Scoper,
|
||||||
bannerID gid.GID,
|
bannerID gid.GID,
|
||||||
) (*coredata.CookieBannerVersion, error) {
|
) (*coredata.CookieBannerVersion, error) {
|
||||||
var version *coredata.CookieBannerVersion
|
var version coredata.CookieBannerVersion
|
||||||
|
|
||||||
err := s.pg.WithTx(
|
err := s.pg.WithTx(
|
||||||
ctx,
|
ctx,
|
||||||
func(ctx context.Context, tx pg.Tx) error {
|
func(ctx context.Context, tx pg.Tx) error {
|
||||||
var banner coredata.CookieBanner
|
if err := version.LoadLatestByCookieBannerID(ctx, tx, scope, bannerID); err != nil {
|
||||||
if err := banner.LoadByID(ctx, tx, scope, bannerID); err != nil {
|
|
||||||
if errors.Is(err, coredata.ErrResourceNotFound) {
|
if errors.Is(err, coredata.ErrResourceNotFound) {
|
||||||
return ErrBannerNotFound
|
return ErrNoDraftVersion
|
||||||
}
|
}
|
||||||
return fmt.Errorf("cannot load cookie banner: %w", err)
|
return fmt.Errorf("cannot load latest version: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
var categories coredata.CookieCategories
|
if version.State != coredata.CookieBannerVersionStateDraft {
|
||||||
if err := categories.LoadAllPublicByCookieBannerID(ctx, tx, bannerID); err != nil {
|
return ErrNoDraftVersion
|
||||||
return fmt.Errorf("cannot load cookie categories: %w", err)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
snapshotCategories := make([]coredata.CookieBannerVersionSnapshotCategory, len(categories))
|
version.State = coredata.CookieBannerVersionStatePublished
|
||||||
for i, c := range categories {
|
version.UpdatedAt = time.Now()
|
||||||
snapshotCategories[i] = coredata.CookieBannerVersionSnapshotCategory{
|
|
||||||
Name: c.Name,
|
|
||||||
Description: c.Description,
|
|
||||||
Required: c.Required,
|
|
||||||
Cookies: c.Cookies,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
snapshot := coredata.CookieBannerVersionSnapshot{
|
if err := version.Update(ctx, tx, scope); err != nil {
|
||||||
PrivacyPolicyURL: banner.PrivacyPolicyURL,
|
return fmt.Errorf("cannot publish version: %w", err)
|
||||||
ConsentExpiryDays: banner.ConsentExpiryDays,
|
|
||||||
ConsentMode: string(banner.ConsentMode),
|
|
||||||
Categories: snapshotCategories,
|
|
||||||
}
|
|
||||||
|
|
||||||
now := time.Now()
|
|
||||||
|
|
||||||
version = &coredata.CookieBannerVersion{
|
|
||||||
ID: gid.New(scope.GetTenantID(), coredata.CookieBannerVersionEntityType),
|
|
||||||
CookieBannerID: bannerID,
|
|
||||||
State: coredata.CookieBannerVersionStatePublished,
|
|
||||||
CreatedAt: now,
|
|
||||||
UpdatedAt: now,
|
|
||||||
}
|
|
||||||
|
|
||||||
nextVersion, err := version.LoadNextVersion(ctx, tx, scope, bannerID)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("cannot determine next version: %w", err)
|
|
||||||
}
|
|
||||||
version.Version = nextVersion
|
|
||||||
|
|
||||||
if err := version.SetSnapshot(snapshot); err != nil {
|
|
||||||
return fmt.Errorf("cannot set snapshot: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := version.Insert(ctx, tx, scope); err != nil {
|
|
||||||
return fmt.Errorf("cannot insert cookie banner version: %w", err)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
@@ -421,7 +473,7 @@ func (s *Service) PublishCookieBannerVersion(
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return version, nil
|
return &version, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) ActivateCookieBanner(
|
func (s *Service) ActivateCookieBanner(
|
||||||
@@ -562,6 +614,20 @@ func (s *Service) CreateCookieCategory(
|
|||||||
return fmt.Errorf("cannot insert cookie category: %w", err)
|
return fmt.Errorf("cannot insert cookie category: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var banner coredata.CookieBanner
|
||||||
|
if err := banner.LoadByID(ctx, tx, scope, req.CookieBannerID); err != nil {
|
||||||
|
return fmt.Errorf("cannot load cookie banner: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var categories coredata.CookieCategories
|
||||||
|
if err := categories.LoadAllByCookieBannerID(ctx, tx, req.CookieBannerID); err != nil {
|
||||||
|
return fmt.Errorf("cannot load cookie categories: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := s.ensureDraftVersion(ctx, tx, scope, &banner, categories); err != nil {
|
||||||
|
return fmt.Errorf("cannot ensure draft version: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
@@ -692,6 +758,20 @@ func (s *Service) UpdateCookieCategory(
|
|||||||
return fmt.Errorf("cannot update cookie category: %w", err)
|
return fmt.Errorf("cannot update cookie category: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var banner coredata.CookieBanner
|
||||||
|
if err := banner.LoadByID(ctx, tx, scope, category.CookieBannerID); err != nil {
|
||||||
|
return fmt.Errorf("cannot load cookie banner: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var categories coredata.CookieCategories
|
||||||
|
if err := categories.LoadAllByCookieBannerID(ctx, tx, category.CookieBannerID); err != nil {
|
||||||
|
return fmt.Errorf("cannot load cookie categories: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := s.ensureDraftVersion(ctx, tx, scope, &banner, categories); err != nil {
|
||||||
|
return fmt.Errorf("cannot ensure draft version: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
@@ -722,10 +802,26 @@ func (s *Service) DeleteCookieCategory(
|
|||||||
return ErrCannotDeleteRequiredCategory
|
return ErrCannotDeleteRequiredCategory
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bannerID := category.CookieBannerID
|
||||||
|
|
||||||
if err := category.Delete(ctx, tx, scope); err != nil {
|
if err := category.Delete(ctx, tx, scope); err != nil {
|
||||||
return fmt.Errorf("cannot delete cookie category: %w", err)
|
return fmt.Errorf("cannot delete cookie category: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var banner coredata.CookieBanner
|
||||||
|
if err := banner.LoadByID(ctx, tx, scope, bannerID); err != nil {
|
||||||
|
return fmt.Errorf("cannot load cookie banner: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var categories coredata.CookieCategories
|
||||||
|
if err := categories.LoadAllByCookieBannerID(ctx, tx, bannerID); err != nil {
|
||||||
|
return fmt.Errorf("cannot load cookie categories: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := s.ensureDraftVersion(ctx, tx, scope, &banner, categories); err != nil {
|
||||||
|
return fmt.Errorf("cannot ensure draft version: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -275,6 +275,53 @@ LIMIT 1;
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (v *CookieBannerVersion) LoadLatestByCookieBannerID(
|
||||||
|
ctx context.Context,
|
||||||
|
conn pg.Querier,
|
||||||
|
scope Scoper,
|
||||||
|
cookieBannerID gid.GID,
|
||||||
|
) error {
|
||||||
|
q := `
|
||||||
|
SELECT
|
||||||
|
id,
|
||||||
|
cookie_banner_id,
|
||||||
|
version,
|
||||||
|
state,
|
||||||
|
snapshot,
|
||||||
|
created_at,
|
||||||
|
updated_at
|
||||||
|
FROM
|
||||||
|
cookie_banner_versions
|
||||||
|
WHERE
|
||||||
|
%s
|
||||||
|
AND cookie_banner_id = @cookie_banner_id
|
||||||
|
ORDER BY version DESC
|
||||||
|
LIMIT 1;
|
||||||
|
`
|
||||||
|
|
||||||
|
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 banner versions: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
ver, err := pgx.CollectExactlyOneRow(rows, pgx.RowToStructByName[CookieBannerVersion])
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, pgx.ErrNoRows) {
|
||||||
|
return ErrResourceNotFound
|
||||||
|
}
|
||||||
|
return fmt.Errorf("cannot collect cookie banner version: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
*v = ver
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (v *CookieBannerVersion) LoadNextVersion(
|
func (v *CookieBannerVersion) LoadNextVersion(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
conn pg.Querier,
|
conn pg.Querier,
|
||||||
|
|||||||
@@ -222,7 +222,7 @@ WHERE
|
|||||||
return count, nil
|
return count, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *CookieCategories) LoadAllPublicByCookieBannerID(
|
func (c *CookieCategories) LoadAllByCookieBannerID(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
conn pg.Querier,
|
conn pg.Querier,
|
||||||
cookieBannerID gid.GID,
|
cookieBannerID gid.GID,
|
||||||
|
|||||||
Reference in New Issue
Block a user