Add slug to cookie categories for stable consent identifiers

The category slug provides a stable, URL-safe key used as the
data-cookie-consent attribute value and consent data key, replacing
the fragile category name. This prevents breakage when categories
are renamed.

- Add slug column with unique-per-banner constraint and backfill migration
- Add Slug validator (lowercase alphanumeric + hyphens)
- Propagate slug through GraphQL schema, service layer, and snapshot
- Update console UI with slug field in create/edit forms
- Switch cookie-banner widget to use slug as consent data keys

Signed-off-by: Émile Ré <emile@getprobo.com>
This commit is contained in:
Émile Ré
2026-04-23 19:07:11 +04:00
parent bb39daebc5
commit 165b9ad9d3
19 changed files with 218 additions and 22 deletions

View File

@@ -15,6 +15,7 @@
package validator
import (
"fmt"
"net/url"
"regexp"
"slices"
@@ -25,6 +26,7 @@ import (
var (
domainRegex = regexp.MustCompile(`^(?:[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)*[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?$`)
slugRegex = regexp.MustCompile(`^[a-z0-9]+(-[a-z0-9]+)*$`)
)
// URL validates that a string is a valid URL with http or https scheme.
@@ -177,6 +179,36 @@ func Origin() ValidatorFunc {
}
}
// Slug validates that a string is a lowercase alphanumeric slug (with hyphens, no
// leading/trailing hyphens, no consecutive hyphens) and does not exceed maxLen.
func Slug(maxLen int) ValidatorFunc {
return func(value any) *ValidationError {
actualValue, isNil := dereferenceValue(value)
if isNil {
return nil
}
str, ok := actualValue.(string)
if !ok {
return newValidationError(ErrorCodeInvalidFormat, "value must be a string")
}
if str == "" {
return nil
}
if len(str) > maxLen {
return newValidationError(ErrorCodeTooLong, fmt.Sprintf("slug must be at most %d characters", maxLen))
}
if !slugRegex.MatchString(str) {
return newValidationError(ErrorCodeInvalidFormat, "slug must contain only lowercase letters, numbers, and hyphens")
}
return nil
}
}
// Domain validates that a string is a valid domain name.
func Domain() ValidatorFunc {
return func(value any) *ValidationError {