Add regenerateCookieBannerTrackerPolicy mutation
Expose a non-destructive re-trigger that re-arms tracker policy generation for a banner that already has a published version, so the tracker-policy worker regenerates the document after iterating on the generator. RegenerateTrackerPolicy returns a conflict when nothing has been published yet. Wire it across all API surfaces per the api-surface rule: the console GraphQL mutation and resolver, the MCP tool, the prb cookie-banner regenerate-policy command, and the n8n operation, with console e2e coverage for the success and no-published-version paths. Signed-off-by: Émile Ré <emile@probo.com>
This commit is contained in:
73
e2e/console/cookie_banner_tracker_policy_test.go
Normal file
73
e2e/console/cookie_banner_tracker_policy_test.go
Normal file
@@ -0,0 +1,73 @@
|
||||
// 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 console_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"go.probo.inc/probo/e2e/internal/factory"
|
||||
"go.probo.inc/probo/e2e/internal/testutil"
|
||||
)
|
||||
|
||||
const regeneratePolicyMutation = `
|
||||
mutation RegenerateCookieBannerTrackerPolicy($input: RegenerateCookieBannerTrackerPolicyInput!) {
|
||||
regenerateCookieBannerTrackerPolicy(input: $input) {
|
||||
cookieBanner { id }
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
func TestRegenerateCookieBannerTrackerPolicy(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
t.Run("succeeds for a published banner", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
owner := testutil.NewClient(t, testutil.RoleOwner)
|
||||
|
||||
bannerID := factory.CreateCookieBanner(owner)
|
||||
published := publishBanner(t, owner, bannerID)
|
||||
require.Equal(t, "PUBLISHED", published.State)
|
||||
|
||||
var result struct {
|
||||
RegenerateCookieBannerTrackerPolicy struct {
|
||||
CookieBanner struct {
|
||||
ID string `json:"id"`
|
||||
} `json:"cookieBanner"`
|
||||
} `json:"regenerateCookieBannerTrackerPolicy"`
|
||||
}
|
||||
|
||||
err := owner.Execute(regeneratePolicyMutation, map[string]any{
|
||||
"input": map[string]any{"cookieBannerId": bannerID},
|
||||
}, &result)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, bannerID, result.RegenerateCookieBannerTrackerPolicy.CookieBanner.ID)
|
||||
})
|
||||
|
||||
t.Run("conflicts when the banner has no published version", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
owner := testutil.NewClient(t, testutil.RoleOwner)
|
||||
|
||||
bannerID := factory.CreateCookieBanner(owner)
|
||||
|
||||
var result struct{}
|
||||
|
||||
err := owner.Execute(regeneratePolicyMutation, map[string]any{
|
||||
"input": map[string]any{"cookieBannerId": bannerID},
|
||||
}, &result)
|
||||
require.Error(t, err, "regenerating without a published version should fail")
|
||||
})
|
||||
}
|
||||
@@ -21,6 +21,7 @@ import * as deleteOp from './delete.operation';
|
||||
import * as activateOp from './activate.operation';
|
||||
import * as deactivateOp from './deactivate.operation';
|
||||
import * as publishOp from './publish.operation';
|
||||
import * as regeneratePolicyOp from './regeneratePolicy.operation';
|
||||
import * as translateOp from './translate.operation';
|
||||
|
||||
export const description: INodeProperties[] = [
|
||||
@@ -77,6 +78,12 @@ export const description: INodeProperties[] = [
|
||||
description: 'Publish a new cookie banner version',
|
||||
action: 'Publish a cookie banner version',
|
||||
},
|
||||
{
|
||||
name: 'Regenerate Policy',
|
||||
value: 'regeneratePolicy',
|
||||
description: 'Re-arm tracker policy generation for a published cookie banner',
|
||||
action: 'Regenerate a cookie banner tracker policy',
|
||||
},
|
||||
{
|
||||
name: 'Translate',
|
||||
value: 'translate',
|
||||
@@ -100,6 +107,7 @@ export const description: INodeProperties[] = [
|
||||
...activateOp.description,
|
||||
...deactivateOp.description,
|
||||
...publishOp.description,
|
||||
...regeneratePolicyOp.description,
|
||||
...translateOp.description,
|
||||
];
|
||||
|
||||
@@ -112,5 +120,6 @@ export {
|
||||
activateOp as activate,
|
||||
deactivateOp as deactivate,
|
||||
publishOp as publish,
|
||||
regeneratePolicyOp as regeneratePolicy,
|
||||
translateOp as translate,
|
||||
};
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
// Copyright (c) 2025-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.
|
||||
|
||||
import type { INodeProperties, IExecuteFunctions, INodeExecutionData } from 'n8n-workflow';
|
||||
import { proboApiRequest } from '../../GenericFunctions';
|
||||
|
||||
export const description: INodeProperties[] = [
|
||||
{
|
||||
displayName: 'Cookie Banner ID',
|
||||
name: 'cookieBannerId',
|
||||
type: 'string',
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['cookieBanner'],
|
||||
operation: ['regeneratePolicy'],
|
||||
},
|
||||
},
|
||||
default: '',
|
||||
description: 'The ID of the cookie banner to regenerate the tracker policy for',
|
||||
required: true,
|
||||
},
|
||||
];
|
||||
|
||||
export async function execute(
|
||||
this: IExecuteFunctions,
|
||||
itemIndex: number,
|
||||
): Promise<INodeExecutionData> {
|
||||
const cookieBannerId = this.getNodeParameter('cookieBannerId', itemIndex) as string;
|
||||
|
||||
const query = `
|
||||
mutation RegenerateCookieBannerTrackerPolicy($input: RegenerateCookieBannerTrackerPolicyInput!) {
|
||||
regenerateCookieBannerTrackerPolicy(input: $input) {
|
||||
cookieBanner {
|
||||
id
|
||||
name
|
||||
origin
|
||||
state
|
||||
createdAt
|
||||
updatedAt
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const responseData = await proboApiRequest.call(this, query, { input: { cookieBannerId } });
|
||||
|
||||
return {
|
||||
json: responseData,
|
||||
pairedItem: { item: itemIndex },
|
||||
};
|
||||
}
|
||||
@@ -24,6 +24,7 @@ import (
|
||||
"go.probo.inc/probo/pkg/cmd/cookie-banner/latestversion"
|
||||
"go.probo.inc/probo/pkg/cmd/cookie-banner/list"
|
||||
"go.probo.inc/probo/pkg/cmd/cookie-banner/publish"
|
||||
regeneratepolicy "go.probo.inc/probo/pkg/cmd/cookie-banner/regenerate-policy"
|
||||
"go.probo.inc/probo/pkg/cmd/cookie-banner/translate"
|
||||
"go.probo.inc/probo/pkg/cmd/cookie-banner/update"
|
||||
"go.probo.inc/probo/pkg/cmd/cookie-banner/view"
|
||||
@@ -43,6 +44,7 @@ func NewCmdCookieBanner(f *cmdutil.Factory) *cobra.Command {
|
||||
cmd.AddCommand(activate.NewCmdActivate(f))
|
||||
cmd.AddCommand(deactivate.NewCmdDeactivate(f))
|
||||
cmd.AddCommand(publish.NewCmdPublish(f))
|
||||
cmd.AddCommand(regeneratepolicy.NewCmdRegeneratePolicy(f))
|
||||
cmd.AddCommand(translate.NewCmdTranslate(f))
|
||||
cmd.AddCommand(latestversion.NewCmdLatestVersion(f))
|
||||
|
||||
|
||||
87
pkg/cmd/cookie-banner/regenerate-policy/regenerate_policy.go
Normal file
87
pkg/cmd/cookie-banner/regenerate-policy/regenerate_policy.go
Normal file
@@ -0,0 +1,87 @@
|
||||
// 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 regeneratepolicy
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"go.probo.inc/probo/pkg/cli/api"
|
||||
"go.probo.inc/probo/pkg/cmd/cmdutil"
|
||||
)
|
||||
|
||||
const regenerateMutation = `
|
||||
mutation($input: RegenerateCookieBannerTrackerPolicyInput!) {
|
||||
regenerateCookieBannerTrackerPolicy(input: $input) {
|
||||
cookieBanner {
|
||||
id
|
||||
name
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
func NewCmdRegeneratePolicy(f *cmdutil.Factory) *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "regenerate-policy <id>",
|
||||
Short: "Re-arm tracker policy generation for a published cookie banner",
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
cfg, err := f.Config()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
host, hc, err := cfg.DefaultHost()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
client := api.NewClient(
|
||||
host,
|
||||
hc.Token,
|
||||
"/api/console/v1/graphql",
|
||||
cfg.HTTPTimeoutDuration(),
|
||||
cmdutil.TokenRefreshOption(cfg, host, hc),
|
||||
)
|
||||
|
||||
input := map[string]any{"cookieBannerId": args[0]}
|
||||
|
||||
data, err := client.Do(regenerateMutation, map[string]any{"input": input})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var resp struct {
|
||||
RegenerateCookieBannerTrackerPolicy struct {
|
||||
CookieBanner struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
} `json:"cookieBanner"`
|
||||
} `json:"regenerateCookieBannerTrackerPolicy"`
|
||||
}
|
||||
if err := json.Unmarshal(data, &resp); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, _ = fmt.Fprintf(f.IOStreams.Out, "Re-armed tracker policy generation for cookie banner %s\n", args[0])
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
return cmd
|
||||
}
|
||||
@@ -979,6 +979,51 @@ func (s *Service) PublishCookieBannerVersion(
|
||||
return &version, nil
|
||||
}
|
||||
|
||||
// RegenerateTrackerPolicy re-arms tracker policy generation for a banner
|
||||
// that already has a published version, so the tracker-policy worker
|
||||
// regenerates the policy document (e.g. after iterating on the generator).
|
||||
// It returns ErrNoPublishedVersion when nothing has been published yet.
|
||||
func (s *Service) RegenerateTrackerPolicy(
|
||||
ctx context.Context,
|
||||
scope coredata.Scoper,
|
||||
bannerID gid.GID,
|
||||
) (*coredata.CookieBanner, error) {
|
||||
var banner coredata.CookieBanner
|
||||
|
||||
err := s.pg.WithTx(
|
||||
ctx,
|
||||
func(ctx context.Context, tx pg.Tx) error {
|
||||
if err := banner.LoadByID(ctx, tx, scope, bannerID); err != nil {
|
||||
if errors.Is(err, coredata.ErrResourceNotFound) {
|
||||
return ErrBannerNotFound
|
||||
}
|
||||
|
||||
return fmt.Errorf("cannot load cookie banner: %w", err)
|
||||
}
|
||||
|
||||
var version coredata.CookieBannerVersion
|
||||
if err := version.LoadLatestPublishedByCookieBannerID(ctx, tx, scope, bannerID); err != nil {
|
||||
if errors.Is(err, coredata.ErrResourceNotFound) {
|
||||
return ErrNoPublishedVersion
|
||||
}
|
||||
|
||||
return fmt.Errorf("cannot load latest published version: %w", err)
|
||||
}
|
||||
|
||||
if err := banner.SetPolicyGenerationRequested(ctx, tx); err != nil {
|
||||
return fmt.Errorf("cannot request tracker policy generation: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &banner, nil
|
||||
}
|
||||
|
||||
func (s *Service) ActivateCookieBanner(
|
||||
ctx context.Context,
|
||||
scope coredata.Scoper,
|
||||
|
||||
@@ -777,6 +777,33 @@ func (r *mutationResolver) PublishCookieBannerVersion(ctx context.Context, input
|
||||
}, nil
|
||||
}
|
||||
|
||||
// RegenerateCookieBannerTrackerPolicy is the resolver for the regenerateCookieBannerTrackerPolicy field.
|
||||
func (r *mutationResolver) RegenerateCookieBannerTrackerPolicy(ctx context.Context, input types.RegenerateCookieBannerTrackerPolicyInput) (*types.RegenerateCookieBannerTrackerPolicyPayload, error) {
|
||||
scope, err := r.authorize(ctx, input.CookieBannerID, probo.ActionCookieBannerUpdate)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
banner, err := r.cookieBanner.RegenerateTrackerPolicy(ctx, scope, input.CookieBannerID)
|
||||
if err != nil {
|
||||
if errors.Is(err, cookiebanner.ErrBannerNotFound) {
|
||||
return nil, gqlutils.NotFound(ctx, err)
|
||||
}
|
||||
|
||||
if errors.Is(err, cookiebanner.ErrNoPublishedVersion) {
|
||||
return nil, gqlutils.Conflict(ctx, err)
|
||||
}
|
||||
|
||||
r.logger.ErrorCtx(ctx, "cannot regenerate cookie banner tracker policy", log.Error(err))
|
||||
|
||||
return nil, gqlutils.Internal(ctx)
|
||||
}
|
||||
|
||||
return &types.RegenerateCookieBannerTrackerPolicyPayload{
|
||||
CookieBanner: types.NewCookieBanner(banner),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// CreateCookieCategory is the resolver for the createCookieCategory field.
|
||||
func (r *mutationResolver) CreateCookieCategory(ctx context.Context, input types.CreateCookieCategoryInput) (*types.CreateCookieCategoryPayload, error) {
|
||||
scope, err := r.authorize(ctx, input.CookieBannerID, probo.ActionCookieCategoryCreate)
|
||||
|
||||
@@ -607,6 +607,9 @@ extend type Mutation {
|
||||
publishCookieBannerVersion(
|
||||
input: PublishCookieBannerVersionInput!
|
||||
): PublishCookieBannerVersionPayload!
|
||||
regenerateCookieBannerTrackerPolicy(
|
||||
input: RegenerateCookieBannerTrackerPolicyInput!
|
||||
): RegenerateCookieBannerTrackerPolicyPayload!
|
||||
createCookieCategory(
|
||||
input: CreateCookieCategoryInput!
|
||||
): CreateCookieCategoryPayload!
|
||||
@@ -682,6 +685,10 @@ input PublishCookieBannerVersionInput {
|
||||
cookieBannerId: ID!
|
||||
}
|
||||
|
||||
input RegenerateCookieBannerTrackerPolicyInput {
|
||||
cookieBannerId: ID!
|
||||
}
|
||||
|
||||
input CreateCookieCategoryInput {
|
||||
cookieBannerId: ID!
|
||||
name: String!
|
||||
@@ -733,6 +740,10 @@ type PublishCookieBannerVersionPayload {
|
||||
cookieBanner: CookieBanner!
|
||||
}
|
||||
|
||||
type RegenerateCookieBannerTrackerPolicyPayload {
|
||||
cookieBanner: CookieBanner!
|
||||
}
|
||||
|
||||
type CreateCookieCategoryPayload {
|
||||
cookieCategoryEdge: CookieCategoryEdge!
|
||||
cookieBanner: CookieBanner!
|
||||
|
||||
@@ -5825,6 +5825,20 @@ func (r *Resolver) PublishCookieBannerVersionTool(ctx context.Context, req *mcp.
|
||||
return nil, types.PublishCookieBannerVersionOutput{CookieBannerVersion: types.NewCookieBannerVersion(version)}, nil
|
||||
}
|
||||
|
||||
func (r *Resolver) RegenerateCookieBannerTrackerPolicyTool(ctx context.Context, req *mcp.CallToolRequest, input *types.RegenerateCookieBannerTrackerPolicyInput) (*mcp.CallToolResult, types.RegenerateCookieBannerTrackerPolicyOutput, error) {
|
||||
scope, err := r.Authorize(ctx, input.CookieBannerID, probo.ActionCookieBannerUpdate)
|
||||
if err != nil {
|
||||
return nil, types.RegenerateCookieBannerTrackerPolicyOutput{}, err
|
||||
}
|
||||
|
||||
banner, err := r.cookieBanner.RegenerateTrackerPolicy(ctx, scope, input.CookieBannerID)
|
||||
if err != nil {
|
||||
return nil, types.RegenerateCookieBannerTrackerPolicyOutput{}, fmt.Errorf("cannot regenerate cookie banner tracker policy: %w", err)
|
||||
}
|
||||
|
||||
return nil, types.RegenerateCookieBannerTrackerPolicyOutput{CookieBanner: types.NewCookieBanner(banner)}, nil
|
||||
}
|
||||
|
||||
func (r *Resolver) ListCookieBannerVersionsTool(ctx context.Context, req *mcp.CallToolRequest, input *types.ListCookieBannerVersionsInput) (*mcp.CallToolResult, types.ListCookieBannerVersionsOutput, error) {
|
||||
scope, err := r.Authorize(ctx, input.CookieBannerID, probo.ActionCookieBannerVersionList)
|
||||
if err != nil {
|
||||
|
||||
@@ -10358,6 +10358,22 @@ components:
|
||||
cookie_banner_version:
|
||||
$ref: "#/components/schemas/CookieBannerVersion"
|
||||
|
||||
RegenerateCookieBannerTrackerPolicyInput:
|
||||
type: object
|
||||
required:
|
||||
- cookie_banner_id
|
||||
properties:
|
||||
cookie_banner_id:
|
||||
$ref: "#/components/schemas/GID"
|
||||
|
||||
RegenerateCookieBannerTrackerPolicyOutput:
|
||||
type: object
|
||||
required:
|
||||
- cookie_banner
|
||||
properties:
|
||||
cookie_banner:
|
||||
$ref: "#/components/schemas/CookieBanner"
|
||||
|
||||
ListCookieBannerVersionsInput:
|
||||
type: object
|
||||
required:
|
||||
@@ -13944,6 +13960,14 @@ tools:
|
||||
$ref: "#/components/schemas/PublishCookieBannerVersionInput"
|
||||
outputSchema:
|
||||
$ref: "#/components/schemas/PublishCookieBannerVersionOutput"
|
||||
- name: regenerateCookieBannerTrackerPolicy
|
||||
description: Re-arm tracker policy generation for a cookie banner that already has a published version. Returns immediately; the policy document is regenerated in the background.
|
||||
hints:
|
||||
readonly: false
|
||||
inputSchema:
|
||||
$ref: "#/components/schemas/RegenerateCookieBannerTrackerPolicyInput"
|
||||
outputSchema:
|
||||
$ref: "#/components/schemas/RegenerateCookieBannerTrackerPolicyOutput"
|
||||
- name: listCookieBannerVersions
|
||||
description: List all versions for a cookie banner
|
||||
hints:
|
||||
|
||||
Reference in New Issue
Block a user