Add GraphQL API and service CRUD for cookie patterns

Add CookiePattern type, connection, and mutations to the GraphQL schema
with full resolver implementations. Add service methods for pattern
CRUD, category movement, listing, and counting. This enables the console
to manage cookie patterns instead of individual cookies.

Signed-off-by: Émile Ré <emile@getprobo.com>
This commit is contained in:
Émile Ré
2026-04-29 14:24:28 +04:00
parent f9fec45eb1
commit 9c402058ec
6 changed files with 847 additions and 0 deletions

View File

@@ -34,4 +34,7 @@ var (
ErrCategoriesBannerMismatch = errors.New("source and target categories belong to different banners")
ErrSameCategoryMove = errors.New("source and target cookie categories must be different")
ErrPostHogConsentKindInvalid = errors.New("PostHog consent can only be enabled on normal categories")
ErrCookiePatternNotFound = errors.New("cookie pattern not found")
ErrPatternAlreadyExists = errors.New("a pattern with this name already exists in this banner")
ErrSamePatternCategoryMove = errors.New("source and target cookie categories must be different")
)

View File

@@ -102,6 +102,32 @@ type (
TargetCookieCategoryID gid.GID
}
CreateCookiePatternRequest struct {
CookieCategoryID gid.GID
Pattern string
MatchType coredata.CookiePatternMatchType
DisplayName string
Duration string
Description string
}
UpdateCookiePatternRequest struct {
CookiePatternID gid.GID
DisplayName *string
Duration *string
Description *string
}
MoveCookiePatternToCategoryRequest struct {
CookiePatternID gid.GID
TargetCookieCategoryID gid.GID
}
MoveCookiePatternToCategoryResult struct {
CookiePattern *coredata.CookiePattern
Banner *coredata.CookieBanner
}
CreateCookieConsentRecordRequest struct {
CookieBannerID gid.GID
Version int
@@ -244,6 +270,48 @@ func (r *ReorderCookieCategoryRequest) Validate() error {
return v.Error()
}
func (r *CreateCookiePatternRequest) Validate() error {
v := validator.New()
v.Check(r.CookieCategoryID, "cookie_category_id", validator.Required(), validator.GID(coredata.CookieCategoryEntityType))
v.Check(r.Pattern, "pattern", validator.Required(), validator.SafeTextNoNewLine(255))
v.Check(string(r.MatchType), "match_type", validator.Required(), validator.OneOfSlice(
func() []string {
types := coredata.CookiePatternMatchTypes()
s := make([]string, len(types))
for i, t := range types {
s[i] = string(t)
}
return s
}(),
))
v.Check(r.DisplayName, "display_name", validator.Required(), validator.SafeTextNoNewLine(255))
v.Check(r.Duration, "duration", validator.Required(), validator.SafeTextNoNewLine(255))
v.Check(r.Description, "description", validator.SafeText(1000))
return v.Error()
}
func (r *UpdateCookiePatternRequest) Validate() error {
v := validator.New()
v.Check(r.CookiePatternID, "cookie_pattern_id", validator.Required(), validator.GID(coredata.CookiePatternEntityType))
v.Check(r.DisplayName, "display_name", validator.SafeTextNoNewLine(255))
v.Check(r.Duration, "duration", validator.SafeTextNoNewLine(255))
v.Check(r.Description, "description", validator.SafeText(1000))
return v.Error()
}
func (r *MoveCookiePatternToCategoryRequest) Validate() error {
v := validator.New()
v.Check(r.CookiePatternID, "cookie_pattern_id", validator.Required(), validator.GID(coredata.CookiePatternEntityType))
v.Check(r.TargetCookieCategoryID, "target_cookie_category_id", validator.Required(), validator.GID(coredata.CookieCategoryEntityType))
return v.Error()
}
func (r *MoveCookieToCategoryRequest) Validate() error {
v := validator.New()
@@ -1325,6 +1393,294 @@ func (s *Service) GetCookiePattern(
return &pattern, nil
}
func (s *Service) CreateCookiePattern(
ctx context.Context,
scope coredata.Scoper,
req CreateCookiePatternRequest,
) (*coredata.CookiePattern, error) {
if err := req.Validate(); err != nil {
return nil, fmt.Errorf("invalid request: %w", err)
}
var pattern *coredata.CookiePattern
err := s.pg.WithTx(
ctx,
func(ctx context.Context, tx pg.Tx) error {
var category coredata.CookieCategory
if err := category.LoadByID(ctx, tx, scope, req.CookieCategoryID); err != nil {
if errors.Is(err, coredata.ErrResourceNotFound) {
return ErrCategoryNotFound
}
return fmt.Errorf("cannot load cookie category: %w", err)
}
now := time.Now()
pattern = &coredata.CookiePattern{
ID: gid.New(scope.GetTenantID(), coredata.CookiePatternEntityType),
OrganizationID: category.OrganizationID,
CookieBannerID: category.CookieBannerID,
CookieCategoryID: category.ID,
Pattern: req.Pattern,
MatchType: req.MatchType,
DisplayName: req.DisplayName,
Duration: req.Duration,
Description: req.Description,
Source: coredata.CookieSourceScript,
CreatedAt: now,
UpdatedAt: now,
}
if err := pattern.Insert(ctx, tx, scope); err != nil {
if errors.Is(err, coredata.ErrResourceAlreadyExists) {
return ErrPatternAlreadyExists
}
return fmt.Errorf("cannot insert cookie pattern: %w", err)
}
if _, err := s.ensureDraftVersionForBanner(ctx, tx, scope, category.CookieBannerID); err != nil {
return fmt.Errorf("cannot ensure draft version: %w", err)
}
return nil
},
)
if err != nil {
return nil, err
}
return pattern, nil
}
func (s *Service) UpdateCookiePattern(
ctx context.Context,
scope coredata.Scoper,
req UpdateCookiePatternRequest,
) (*coredata.CookiePattern, error) {
if err := req.Validate(); err != nil {
return nil, fmt.Errorf("invalid request: %w", err)
}
var pattern coredata.CookiePattern
err := s.pg.WithTx(
ctx,
func(ctx context.Context, tx pg.Tx) error {
if err := pattern.LoadByID(ctx, tx, scope, req.CookiePatternID); err != nil {
if errors.Is(err, coredata.ErrResourceNotFound) {
return ErrCookiePatternNotFound
}
return fmt.Errorf("cannot load cookie pattern: %w", err)
}
if req.DisplayName != nil {
pattern.DisplayName = *req.DisplayName
}
if req.Duration != nil {
pattern.Duration = *req.Duration
}
if req.Description != nil {
pattern.Description = *req.Description
}
pattern.UpdatedAt = time.Now()
if err := pattern.Update(ctx, tx, scope); err != nil {
return fmt.Errorf("cannot update cookie pattern: %w", err)
}
if _, err := s.ensureDraftVersionForBanner(ctx, tx, scope, pattern.CookieBannerID); err != nil {
return fmt.Errorf("cannot ensure draft version: %w", err)
}
return nil
},
)
if err != nil {
return nil, err
}
return &pattern, nil
}
func (s *Service) DeleteCookiePattern(
ctx context.Context,
scope coredata.Scoper,
cookiePatternID gid.GID,
) error {
return s.pg.WithTx(
ctx,
func(ctx context.Context, tx pg.Tx) error {
var pattern coredata.CookiePattern
if err := pattern.LoadByID(ctx, tx, scope, cookiePatternID); err != nil {
if errors.Is(err, coredata.ErrResourceNotFound) {
return ErrCookiePatternNotFound
}
return fmt.Errorf("cannot load cookie pattern: %w", err)
}
if err := pattern.Delete(ctx, tx, scope); err != nil {
return fmt.Errorf("cannot delete cookie pattern: %w", err)
}
if _, err := s.ensureDraftVersionForBanner(ctx, tx, scope, pattern.CookieBannerID); err != nil {
return fmt.Errorf("cannot ensure draft version: %w", err)
}
return nil
},
)
}
func (s *Service) MoveCookiePatternToCategory(
ctx context.Context,
scope coredata.Scoper,
req MoveCookiePatternToCategoryRequest,
) (*MoveCookiePatternToCategoryResult, error) {
if err := req.Validate(); err != nil {
return nil, fmt.Errorf("invalid request: %w", err)
}
var result MoveCookiePatternToCategoryResult
err := s.pg.WithTx(
ctx,
func(ctx context.Context, tx pg.Tx) error {
var pattern coredata.CookiePattern
if err := pattern.LoadByID(ctx, tx, scope, req.CookiePatternID); err != nil {
if errors.Is(err, coredata.ErrResourceNotFound) {
return ErrCookiePatternNotFound
}
return fmt.Errorf("cannot load cookie pattern: %w", err)
}
var target coredata.CookieCategory
if err := target.LoadByID(ctx, tx, scope, req.TargetCookieCategoryID); err != nil {
if errors.Is(err, coredata.ErrResourceNotFound) {
return ErrCategoryNotFound
}
return fmt.Errorf("cannot load target cookie category: %w", err)
}
if pattern.CookieCategoryID == target.ID {
return ErrSamePatternCategoryMove
}
if pattern.CookieBannerID != target.CookieBannerID {
return ErrCategoriesBannerMismatch
}
pattern.CookieCategoryID = target.ID
pattern.UpdatedAt = time.Now()
if err := pattern.Update(ctx, tx, scope); err != nil {
return fmt.Errorf("cannot update cookie pattern: %w", err)
}
var banner coredata.CookieBanner
if err := banner.LoadByID(ctx, tx, scope, pattern.CookieBannerID); err != nil {
return fmt.Errorf("cannot load cookie banner: %w", err)
}
if _, err := s.ensureDraftVersionForBanner(ctx, tx, scope, pattern.CookieBannerID); err != nil {
return fmt.Errorf("cannot ensure draft version: %w", err)
}
result.CookiePattern = &pattern
result.Banner = &banner
return nil
},
)
if err != nil {
return nil, err
}
return &result, nil
}
func (s *Service) ListCookiePatternsForCategory(
ctx context.Context,
scope coredata.Scoper,
categoryID gid.GID,
cursor *page.Cursor[coredata.CookiePatternOrderField],
) (coredata.CookiePatterns, error) {
var patterns coredata.CookiePatterns
err := s.pg.WithConn(
ctx,
func(ctx context.Context, conn pg.Querier) error {
if err := patterns.LoadByCookieCategoryID(ctx, conn, scope, categoryID, cursor); err != nil {
return fmt.Errorf("cannot list cookie patterns: %w", err)
}
return nil
},
)
if err != nil {
return nil, err
}
return patterns, nil
}
func (s *Service) CountCookiePatternsForCategory(
ctx context.Context,
scope coredata.Scoper,
categoryID gid.GID,
) (int, error) {
var count int
err := s.pg.WithConn(
ctx,
func(ctx context.Context, conn pg.Querier) error {
var patterns coredata.CookiePatterns
var err error
count, err = patterns.CountByCookieCategoryID(ctx, conn, scope, categoryID)
if err != nil {
return fmt.Errorf("cannot count cookie patterns: %w", err)
}
return nil
},
)
if err != nil {
return 0, err
}
return count, nil
}
func (s *Service) CountCookiesForPattern(
ctx context.Context,
scope coredata.Scoper,
patternID gid.GID,
) (int, error) {
var count int
err := s.pg.WithConn(
ctx,
func(ctx context.Context, conn pg.Querier) error {
var cookies coredata.Cookies
var err error
count, err = cookies.CountByCookiePatternID(ctx, conn, scope, patternID)
if err != nil {
return fmt.Errorf("cannot count cookies for pattern: %w", err)
}
return nil
},
)
if err != nil {
return 0, err
}
return count, nil
}
func (s *Service) UpdateCookie(
ctx context.Context,
scope coredata.Scoper,

View File

@@ -410,6 +410,13 @@ const (
ActionCookieUpdate = "core:cookie:update"
ActionCookieDelete = "core:cookie:delete"
// CookiePattern actions
ActionCookiePatternGet = "core:cookie-pattern:get"
ActionCookiePatternList = "core:cookie-pattern:list"
ActionCookiePatternCreate = "core:cookie-pattern:create"
ActionCookiePatternUpdate = "core:cookie-pattern:update"
ActionCookiePatternDelete = "core:cookie-pattern:delete"
// CookieConsentRecord actions
ActionCookieConsentRecordList = "core:cookie-consent-record:list"
)

View File

@@ -322,6 +322,37 @@ func (r *cookieCategoryResolver) Cookies(ctx context.Context, obj *types.CookieC
return types.NewCookieConnection(p, r, obj.ID, obj.ID), nil
}
// CookiePatterns is the resolver for the cookiePatterns field.
func (r *cookieCategoryResolver) CookiePatterns(ctx context.Context, obj *types.CookieCategory, first *int, after *page.CursorKey, last *int, before *page.CursorKey, orderBy *types.CookiePatternOrderBy) (*types.CookiePatternConnection, error) {
if err := r.authorize(ctx, obj.ID, probo.ActionCookiePatternList); err != nil {
return nil, err
}
pageOrderBy := page.OrderBy[coredata.CookiePatternOrderField]{
Field: coredata.CookiePatternOrderFieldCreatedAt,
Direction: page.OrderDirectionAsc,
}
if orderBy != nil {
pageOrderBy = page.OrderBy[coredata.CookiePatternOrderField]{
Field: orderBy.Field,
Direction: orderBy.Direction,
}
}
cursor := types.NewCursor(first, after, last, before, pageOrderBy)
scope := coredata.NewScopeFromObjectID(obj.ID)
patterns, err := r.cookieBanner.ListCookiePatternsForCategory(ctx, scope, obj.ID, cursor)
if err != nil {
r.logger.ErrorCtx(ctx, "cannot list cookie patterns", log.Error(err))
return nil, gqlutils.Internal(ctx)
}
p := page.NewPage(patterns, cursor)
return types.NewCookiePatternConnection(p, r, obj.ID), nil
}
// Permission is the resolver for the permission field.
func (r *cookieCategoryResolver) Permission(ctx context.Context, obj *types.CookieCategory, action string) (bool, error) {
return r.Resolver.Permission(ctx, obj, action)
@@ -361,6 +392,65 @@ func (r *cookieConnectionResolver) TotalCount(ctx context.Context, obj *types.Co
return count, nil
}
// CookieCategory is the resolver for the cookieCategory field.
func (r *cookiePatternResolver) CookieCategory(ctx context.Context, obj *types.CookiePattern) (*types.CookieCategory, error) {
if err := r.authorize(ctx, obj.CookieCategory.ID, probo.ActionCookieCategoryGet); err != nil {
return nil, err
}
loaders := dataloader.FromContext(ctx)
category, err := loaders.CookieCategory.Load(ctx, obj.CookieCategory.ID)
if err != nil {
if errors.Is(err, coredata.ErrResourceNotFound) || errors.Is(err, dataloadgen.ErrNotFound) {
return nil, nil
}
r.logger.ErrorCtx(ctx, "cannot get cookie category", log.Error(err))
return nil, gqlutils.Internal(ctx)
}
return types.NewCookieCategory(category), nil
}
// CookieCount is the resolver for the cookieCount field.
func (r *cookiePatternResolver) CookieCount(ctx context.Context, obj *types.CookiePattern) (int, error) {
if err := r.authorize(ctx, obj.ID, probo.ActionCookiePatternGet); err != nil {
return 0, err
}
scope := coredata.NewScopeFromObjectID(obj.ID)
count, err := r.cookieBanner.CountCookiesForPattern(ctx, scope, obj.ID)
if err != nil {
r.logger.ErrorCtx(ctx, "cannot count cookies for pattern", log.Error(err))
return 0, gqlutils.Internal(ctx)
}
return count, nil
}
// Permission is the resolver for the permission field.
func (r *cookiePatternResolver) Permission(ctx context.Context, obj *types.CookiePattern, action string) (bool, error) {
return r.Resolver.Permission(ctx, obj, action)
}
// TotalCount is the resolver for the totalCount field.
func (r *cookiePatternConnectionResolver) TotalCount(ctx context.Context, obj *types.CookiePatternConnection) (int, error) {
if err := r.authorize(ctx, obj.ParentID, probo.ActionCookiePatternList); err != nil {
return 0, err
}
scope := coredata.NewScopeFromObjectID(obj.ParentID)
count, err := r.cookieBanner.CountCookiePatternsForCategory(ctx, scope, obj.ParentID)
if err != nil {
r.logger.ErrorCtx(ctx, "cannot count cookie patterns", log.Error(err))
return 0, gqlutils.Internal(ctx)
}
return count, nil
}
// CreateCookieBanner is the resolver for the createCookieBanner field.
func (r *mutationResolver) CreateCookieBanner(ctx context.Context, input types.CreateCookieBannerInput) (*types.CreateCookieBannerPayload, error) {
if err := r.authorize(ctx, input.OrganizationID, probo.ActionCookieBannerCreate); err != nil {
@@ -902,6 +992,179 @@ func (r *mutationResolver) DeleteCookie(ctx context.Context, input types.DeleteC
}, nil
}
// CreateCookiePattern is the resolver for the createCookiePattern field.
func (r *mutationResolver) CreateCookiePattern(ctx context.Context, input types.CreateCookiePatternInput) (*types.CreateCookiePatternPayload, error) {
if err := r.authorize(ctx, input.CookieCategoryID, probo.ActionCookiePatternCreate); err != nil {
return nil, err
}
scope := coredata.NewScopeFromObjectID(input.CookieCategoryID)
pattern, err := r.cookieBanner.CreateCookiePattern(
ctx,
scope,
cookiebanner.CreateCookiePatternRequest{
CookieCategoryID: input.CookieCategoryID,
Pattern: input.Pattern,
MatchType: input.MatchType,
DisplayName: input.DisplayName,
Duration: input.Duration,
Description: input.Description,
},
)
if err != nil {
if errors.Is(err, cookiebanner.ErrPatternAlreadyExists) {
return nil, gqlutils.Conflict(ctx, err)
}
if errors.Is(err, cookiebanner.ErrCategoryNotFound) {
return nil, gqlutils.NotFound(ctx, err)
}
if validationErrors, ok := errors.AsType[validator.ValidationErrors](err); ok {
return nil, gqlutils.InvalidValidationErrors(ctx, validationErrors)
}
r.logger.ErrorCtx(ctx, "cannot create cookie pattern", log.Error(err))
return nil, gqlutils.Internal(ctx)
}
bannerScope := coredata.NewScopeFromObjectID(pattern.CookieBannerID)
banner, err := r.cookieBanner.GetCookieBanner(ctx, bannerScope, pattern.CookieBannerID)
if err != nil {
r.logger.ErrorCtx(ctx, "cannot get cookie banner", log.Error(err))
return nil, gqlutils.Internal(ctx)
}
return &types.CreateCookiePatternPayload{
CookiePatternEdge: types.NewCookiePatternEdge(pattern, coredata.CookiePatternOrderFieldCreatedAt),
CookieBanner: types.NewCookieBanner(banner),
}, nil
}
// UpdateCookiePattern is the resolver for the updateCookiePattern field.
func (r *mutationResolver) UpdateCookiePattern(ctx context.Context, input types.UpdateCookiePatternInput) (*types.UpdateCookiePatternPayload, error) {
if err := r.authorize(ctx, input.CookiePatternID, probo.ActionCookiePatternUpdate); err != nil {
return nil, err
}
scope := coredata.NewScopeFromObjectID(input.CookiePatternID)
pattern, err := r.cookieBanner.UpdateCookiePattern(
ctx,
scope,
cookiebanner.UpdateCookiePatternRequest{
CookiePatternID: input.CookiePatternID,
DisplayName: input.DisplayName,
Duration: input.Duration,
Description: input.Description,
},
)
if err != nil {
if errors.Is(err, cookiebanner.ErrCookiePatternNotFound) {
return nil, gqlutils.NotFound(ctx, err)
}
if validationErrors, ok := errors.AsType[validator.ValidationErrors](err); ok {
return nil, gqlutils.InvalidValidationErrors(ctx, validationErrors)
}
r.logger.ErrorCtx(ctx, "cannot update cookie pattern", log.Error(err))
return nil, gqlutils.Internal(ctx)
}
bannerScope := coredata.NewScopeFromObjectID(pattern.CookieBannerID)
banner, err := r.cookieBanner.GetCookieBanner(ctx, bannerScope, pattern.CookieBannerID)
if err != nil {
r.logger.ErrorCtx(ctx, "cannot get cookie banner", log.Error(err))
return nil, gqlutils.Internal(ctx)
}
return &types.UpdateCookiePatternPayload{
CookiePattern: types.NewCookiePattern(pattern),
CookieBanner: types.NewCookieBanner(banner),
}, nil
}
// DeleteCookiePattern is the resolver for the deleteCookiePattern field.
func (r *mutationResolver) DeleteCookiePattern(ctx context.Context, input types.DeleteCookiePatternInput) (*types.DeleteCookiePatternPayload, error) {
if err := r.authorize(ctx, input.CookiePatternID, probo.ActionCookiePatternDelete); err != nil {
return nil, err
}
scope := coredata.NewScopeFromObjectID(input.CookiePatternID)
pattern, err := r.cookieBanner.GetCookiePattern(ctx, scope, input.CookiePatternID)
if err != nil {
if errors.Is(err, cookiebanner.ErrCookieNotFound) {
return nil, gqlutils.NotFound(ctx, err)
}
r.logger.ErrorCtx(ctx, "cannot get cookie pattern", log.Error(err))
return nil, gqlutils.Internal(ctx)
}
bannerID := pattern.CookieBannerID
err = r.cookieBanner.DeleteCookiePattern(ctx, scope, input.CookiePatternID)
if err != nil {
if errors.Is(err, cookiebanner.ErrCookiePatternNotFound) {
return nil, gqlutils.NotFound(ctx, err)
}
r.logger.ErrorCtx(ctx, "cannot delete cookie pattern", log.Error(err))
return nil, gqlutils.Internal(ctx)
}
bannerScope := coredata.NewScopeFromObjectID(bannerID)
banner, err := r.cookieBanner.GetCookieBanner(ctx, bannerScope, bannerID)
if err != nil {
r.logger.ErrorCtx(ctx, "cannot get cookie banner", log.Error(err))
return nil, gqlutils.Internal(ctx)
}
return &types.DeleteCookiePatternPayload{
DeletedCookiePatternID: input.CookiePatternID,
CookieBanner: types.NewCookieBanner(banner),
}, nil
}
// MoveCookiePatternToCategory is the resolver for the moveCookiePatternToCategory field.
func (r *mutationResolver) MoveCookiePatternToCategory(ctx context.Context, input types.MoveCookiePatternToCategoryInput) (*types.MoveCookiePatternToCategoryPayload, error) {
if err := r.authorize(ctx, input.CookiePatternID, probo.ActionCookiePatternUpdate); err != nil {
return nil, err
}
if err := r.authorize(ctx, input.TargetCookieCategoryID, probo.ActionCookieCategoryUpdate); err != nil {
return nil, err
}
scope := coredata.NewScopeFromObjectID(input.CookiePatternID)
result, err := r.cookieBanner.MoveCookiePatternToCategory(
ctx,
scope,
cookiebanner.MoveCookiePatternToCategoryRequest{
CookiePatternID: input.CookiePatternID,
TargetCookieCategoryID: input.TargetCookieCategoryID,
},
)
if err != nil {
switch {
case errors.Is(err, cookiebanner.ErrCategoryNotFound):
return nil, gqlutils.NotFound(ctx, err)
case errors.Is(err, cookiebanner.ErrCookiePatternNotFound):
return nil, gqlutils.NotFound(ctx, err)
case errors.Is(err, cookiebanner.ErrCategoriesBannerMismatch):
return nil, gqlutils.NotFoundf(ctx, "cookie pattern or target category not found")
default:
if validationErrors, ok := errors.AsType[validator.ValidationErrors](err); ok {
return nil, gqlutils.InvalidValidationErrors(ctx, validationErrors)
}
r.logger.ErrorCtx(ctx, "cannot move cookie pattern to category", log.Error(err))
return nil, gqlutils.Internal(ctx)
}
}
return &types.MoveCookiePatternToCategoryPayload{
CookiePattern: types.NewCookiePattern(result.CookiePattern),
CookieBanner: types.NewCookieBanner(result.Banner),
}, nil
}
// UpsertCookieBannerTranslation is the resolver for the upsertCookieBannerTranslation field.
func (r *mutationResolver) UpsertCookieBannerTranslation(ctx context.Context, input types.UpsertCookieBannerTranslationInput) (*types.UpsertCookieBannerTranslationPayload, error) {
if err := r.authorize(ctx, input.CookieBannerID, probo.ActionCookieBannerUpdate); err != nil {
@@ -971,6 +1234,14 @@ func (r *Resolver) CookieConnection() schema.CookieConnectionResolver {
return &cookieConnectionResolver{r}
}
// CookiePattern returns schema.CookiePatternResolver implementation.
func (r *Resolver) CookiePattern() schema.CookiePatternResolver { return &cookiePatternResolver{r} }
// CookiePatternConnection returns schema.CookiePatternConnectionResolver implementation.
func (r *Resolver) CookiePatternConnection() schema.CookiePatternConnectionResolver {
return &cookiePatternConnectionResolver{r}
}
type cookieResolver struct{ *Resolver }
type cookieBannerResolver struct{ *Resolver }
type cookieBannerConnectionResolver struct{ *Resolver }
@@ -978,3 +1249,5 @@ type cookieBannerVersionResolver struct{ *Resolver }
type cookieCategoryResolver struct{ *Resolver }
type cookieCategoryConnectionResolver struct{ *Resolver }
type cookieConnectionResolver struct{ *Resolver }
type cookiePatternResolver struct{ *Resolver }
type cookiePatternConnectionResolver struct{ *Resolver }

View File

@@ -156,12 +156,52 @@ type CookieCategory implements Node {
orderBy: CookieOrder
): CookieConnection @goField(forceResolver: true)
cookiePatterns(
first: Int
after: CursorKey
last: Int
before: CursorKey
orderBy: CookiePatternOrder
): CookiePatternConnection @goField(forceResolver: true)
createdAt: Datetime!
updatedAt: Datetime!
permission(action: String!): Boolean! @goField(forceResolver: true)
}
enum CookiePatternMatchType
@goModel(
model: "go.probo.inc/probo/pkg/coredata.CookiePatternMatchType"
) {
EXACT
@goEnum(
value: "go.probo.inc/probo/pkg/coredata.CookiePatternMatchTypeExact"
)
PREFIX
@goEnum(
value: "go.probo.inc/probo/pkg/coredata.CookiePatternMatchTypePrefix"
)
}
enum CookiePatternOrderField
@goModel(
model: "go.probo.inc/probo/pkg/coredata.CookiePatternOrderField"
) {
CREATED_AT
@goEnum(
value: "go.probo.inc/probo/pkg/coredata.CookiePatternOrderFieldCreatedAt"
)
}
input CookiePatternOrder
@goModel(
model: "go.probo.inc/probo/pkg/server/api/console/v1/types.CookiePatternOrderBy"
) {
direction: OrderDirection!
field: CookiePatternOrderField!
}
enum CookieOrderField
@goModel(
model: "go.probo.inc/probo/pkg/coredata.CookieOrderField"
@@ -207,6 +247,36 @@ type CookieEdge {
node: Cookie!
}
type CookiePattern implements Node {
id: ID!
cookieCategory: CookieCategory @goField(forceResolver: true)
pattern: String!
matchType: CookiePatternMatchType!
displayName: String!
duration: String!
description: String!
source: CookieSource!
cookieCount: Int! @goField(forceResolver: true)
createdAt: Datetime!
updatedAt: Datetime!
permission(action: String!): Boolean! @goField(forceResolver: true)
}
type CookiePatternConnection
@goModel(
model: "go.probo.inc/probo/pkg/server/api/console/v1/types.CookiePatternConnection"
) {
totalCount: Int! @goField(forceResolver: true)
edges: [CookiePatternEdge!]!
pageInfo: PageInfo!
}
type CookiePatternEdge {
cursor: CursorKey!
node: CookiePattern!
}
type CookieBannerVersion implements Node {
id: ID!
version: Int!
@@ -295,6 +365,18 @@ extend type Mutation {
createCookie(input: CreateCookieInput!): CreateCookiePayload!
updateCookie(input: UpdateCookieInput!): UpdateCookiePayload!
deleteCookie(input: DeleteCookieInput!): DeleteCookiePayload!
createCookiePattern(
input: CreateCookiePatternInput!
): CreateCookiePatternPayload!
updateCookiePattern(
input: UpdateCookiePatternInput!
): UpdateCookiePatternPayload!
deleteCookiePattern(
input: DeleteCookiePatternInput!
): DeleteCookiePatternPayload!
moveCookiePatternToCategory(
input: MoveCookiePatternToCategoryInput!
): MoveCookiePatternToCategoryPayload!
upsertCookieBannerTranslation(
input: UpsertCookieBannerTranslationInput!
): UpsertCookieBannerTranslationPayload!
@@ -449,6 +531,51 @@ type DeleteCookiePayload {
cookieBanner: CookieBanner!
}
input CreateCookiePatternInput {
cookieCategoryId: ID!
pattern: String!
matchType: CookiePatternMatchType!
displayName: String!
duration: String!
description: String!
}
input UpdateCookiePatternInput {
cookiePatternId: ID!
displayName: String
duration: String
description: String
}
input DeleteCookiePatternInput {
cookiePatternId: ID!
}
input MoveCookiePatternToCategoryInput {
cookiePatternId: ID!
targetCookieCategoryId: ID!
}
type CreateCookiePatternPayload {
cookiePatternEdge: CookiePatternEdge!
cookieBanner: CookieBanner!
}
type UpdateCookiePatternPayload {
cookiePattern: CookiePattern!
cookieBanner: CookieBanner!
}
type DeleteCookiePatternPayload {
deletedCookiePatternId: ID!
cookieBanner: CookieBanner!
}
type MoveCookiePatternToCategoryPayload {
cookiePattern: CookiePattern!
cookieBanner: CookieBanner!
}
input UpsertCookieBannerTranslationInput {
cookieBannerId: ID!
language: String!

View File

@@ -0,0 +1,81 @@
// 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.
package types
import (
"go.probo.inc/probo/pkg/coredata"
"go.probo.inc/probo/pkg/gid"
"go.probo.inc/probo/pkg/page"
)
type (
CookiePatternOrderBy OrderBy[coredata.CookiePatternOrderField]
CookiePatternConnection struct {
TotalCount int
Edges []*CookiePatternEdge
PageInfo PageInfo
Resolver any
ParentID gid.GID
}
)
func NewCookiePatternConnection(
p *page.Page[*coredata.CookiePattern, coredata.CookiePatternOrderField],
parentType any,
parentID gid.GID,
) *CookiePatternConnection {
edges := make([]*CookiePatternEdge, len(p.Data))
for i := range edges {
edges[i] = NewCookiePatternEdge(p.Data[i], p.Cursor.OrderBy.Field)
}
return &CookiePatternConnection{
Edges: edges,
PageInfo: *NewPageInfo(p),
Resolver: parentType,
ParentID: parentID,
}
}
func NewCookiePatternEdge(cp *coredata.CookiePattern, orderBy coredata.CookiePatternOrderField) *CookiePatternEdge {
return &CookiePatternEdge{
Cursor: cp.CursorKey(orderBy),
Node: NewCookiePattern(cp),
}
}
func NewCookiePattern(cp *coredata.CookiePattern) *CookiePattern {
return &CookiePattern{
ID: cp.ID,
CookieCategory: &CookieCategory{
ID: cp.CookieCategoryID,
CookieBanner: &CookieBanner{
ID: cp.CookieBannerID,
},
},
Pattern: cp.Pattern,
MatchType: cp.MatchType,
DisplayName: cp.DisplayName,
Duration: cp.Duration,
Description: cp.Description,
Source: cp.Source,
CreatedAt: cp.CreatedAt,
UpdatedAt: cp.UpdatedAt,
}
}