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:
@@ -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")
|
||||
)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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"
|
||||
)
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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!
|
||||
|
||||
81
pkg/server/api/console/v1/types/cookie_pattern.go
Normal file
81
pkg/server/api/console/v1/types/cookie_pattern.go
Normal 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,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user