Add proboctl catalog upsert, link, and describe commands

Operators previously had no way to curate the global tracker catalog
beyond inspection and banner-scoped resets. Add three proboctl commands
backed by small coredata helpers:

- common-third-party upsert: create or update a vendor keyed by slug,
  with partial-merge so an unset flag never blanks an existing column.
- common-tracker-pattern link/unlink: repoint catalog rows at a common
  third party (re-arming enrichment and remapping the uncategorised org
  trackers so the mapping worker re-resolves the vendor) or detach them.
  Unlinking skips enrichment and remap since there is no new vendor.
- common-tracker-pattern set-description: write a description, mark the
  row enriched, and backfill linked org patterns lacking one.

Signed-off-by: Émile Ré <emile@probo.com>
This commit is contained in:
Émile Ré
2026-06-10 11:10:41 +02:00
parent ae61c20791
commit 31c4a0b39c
8 changed files with 683 additions and 0 deletions

View File

@@ -835,6 +835,39 @@ ORDER BY pattern ASC
return ids, nil
}
// RelinkCommonThirdPartyByIDs repoints the given common tracker patterns
// at a different common third party (or unlinks them when thirdPartyID is
// nil). It only touches the catalog rows; callers re-arm enrichment and
// remap the org-scoped tracker patterns separately. Returns the number of
// rows updated.
func (ps *CommonTrackerPatterns) RelinkCommonThirdPartyByIDs(
ctx context.Context,
tx pg.Tx,
ids []gid.GID,
thirdPartyID *gid.GID,
) (int64, error) {
q := `
UPDATE common_tracker_patterns
SET
common_third_party_id = @third_party_id,
updated_at = NOW()
WHERE
id = ANY(@ids)
`
args := pgx.StrictNamedArgs{
"ids": ids,
"third_party_id": thirdPartyID,
}
result, err := tx.Exec(ctx, q, args)
if err != nil {
return 0, fmt.Errorf("cannot relink common tracker pattern third party: %w", err)
}
return result.RowsAffected(), nil
}
// RequestEnrichmentByIDs arms enrichment on the given common tracker
// patterns by stamping enrichment_requested_at, which is the only column
// the enrichment worker claims on. Already-enriched rows are re-processed

View File

@@ -1383,6 +1383,49 @@ WHERE
return result.RowsAffected(), nil
}
// RequestMappingForUncategorisedByCommonTrackerPatternIDs re-arms mapping
// on the uncategorised org tracker patterns linked to the given common
// tracker patterns: it clears their resolved org third party and stamps
// mapping_requested_at so the mapping worker re-resolves the vendor from
// the catalog row's (now changed) common third party. Like the
// description backfill it is a global catalog operation, so it is
// intentionally not tenant-scoped. Excluded patterns and patterns in
// user-chosen categories are left untouched - only the uncategorised
// category is remapped, matching the reset-trackers philosophy. The
// cookie_categories subquery is used only for filtering. Returns the
// number of org patterns re-armed.
func (tps *TrackerPatterns) RequestMappingForUncategorisedByCommonTrackerPatternIDs(
ctx context.Context,
tx pg.Tx,
commonIDs []gid.GID,
) (int64, error) {
q := `
UPDATE tracker_patterns
SET
third_party_id = NULL,
mapping_requested_at = NOW(),
updated_at = NOW()
WHERE
common_tracker_pattern_id = ANY(@common_ids)
AND excluded = false
AND cookie_category_id IN (
SELECT id FROM cookie_categories WHERE kind = @uncategorised_kind
)
`
args := pgx.StrictNamedArgs{
"common_ids": commonIDs,
"uncategorised_kind": CookieCategoryKindUncategorised,
}
result, err := tx.Exec(ctx, q, args)
if err != nil {
return 0, fmt.Errorf("cannot request mapping for uncategorised tracker patterns: %w", err)
}
return result.RowsAffected(), nil
}
// ResetAndRequestMappingByCookieCategoryID detaches every pattern in the
// given category from its catalog row, org third party, and copied
// description, then re-arms mapping. Operators run this (via proboctl) on

View File

@@ -39,6 +39,7 @@ func NewCmdCommonThirdParty(f *cmdutil.Factory) *cobra.Command {
cmd.AddCommand(newCmdList(f))
cmd.AddCommand(newCmdShow(f))
cmd.AddCommand(newCmdDomains(f))
cmd.AddCommand(newCmdUpsert(f))
return cmd
}

206
pkg/proboctl/commonthirdparty/upsert.go vendored Normal file
View File

@@ -0,0 +1,206 @@
// 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 commonthirdparty
import (
"context"
"errors"
"fmt"
"time"
"github.com/spf13/cobra"
"go.gearno.de/kit/pg"
"go.probo.inc/probo/pkg/coredata"
"go.probo.inc/probo/pkg/gid"
"go.probo.inc/probo/pkg/proboctl/cmdutil"
"go.probo.inc/probo/pkg/slug"
)
func newCmdUpsert(f *cmdutil.Factory) *cobra.Command {
var (
flagName string
flagSlug string
flagCategory string
flagWebsiteURL string
flagLegalName string
flagHeadquarterAddress string
flagPrivacyPolicyURL string
flagServiceLevelAgreementURL string
flagServiceSoftwareAgreementURL string
flagDataProcessingAgreementURL string
flagBusinessAssociateAgreementURL string
flagSubprocessorsListURL string
flagStatusPageURL string
flagTermsOfServiceURL string
flagSecurityPageURL string
flagTrustPageURL string
flagCertifications []string
flagDryRun bool
)
cmd := &cobra.Command{
Use: "upsert",
Short: "Create or update a common third party in the global catalog",
Long: "Insert a new common third party or update an existing one keyed by " +
"slug. Only --name and --category are required; every other field is " +
"updated only when its flag is passed, so an existing row's other " +
"columns are preserved.",
Args: cobra.NoArgs,
}
cmd.Flags().StringVar(&flagName, "name", "", "Display name (required)")
cmd.Flags().StringVar(&flagSlug, "slug", "", "Slug key (default: derived from --name)")
cmd.Flags().StringVar(&flagCategory, "category", "", "Third party category (required)")
cmd.Flags().StringVar(&flagWebsiteURL, "website-url", "", "Website URL")
cmd.Flags().StringVar(&flagLegalName, "legal-name", "", "Legal name")
cmd.Flags().StringVar(&flagHeadquarterAddress, "headquarter-address", "", "Headquarter address")
cmd.Flags().StringVar(&flagPrivacyPolicyURL, "privacy-policy-url", "", "Privacy policy URL")
cmd.Flags().StringVar(&flagServiceLevelAgreementURL, "service-level-agreement-url", "", "Service level agreement URL")
cmd.Flags().StringVar(&flagServiceSoftwareAgreementURL, "service-software-agreement-url", "", "Service software agreement URL")
cmd.Flags().StringVar(&flagDataProcessingAgreementURL, "data-processing-agreement-url", "", "Data processing agreement URL")
cmd.Flags().StringVar(&flagBusinessAssociateAgreementURL, "business-associate-agreement-url", "", "Business associate agreement URL")
cmd.Flags().StringVar(&flagSubprocessorsListURL, "subprocessors-list-url", "", "Subprocessors list URL")
cmd.Flags().StringVar(&flagStatusPageURL, "status-page-url", "", "Status page URL")
cmd.Flags().StringVar(&flagTermsOfServiceURL, "terms-of-service-url", "", "Terms of service URL")
cmd.Flags().StringVar(&flagSecurityPageURL, "security-page-url", "", "Security page URL")
cmd.Flags().StringVar(&flagTrustPageURL, "trust-page-url", "", "Trust page URL")
cmd.Flags().StringSliceVar(&flagCertifications, "certifications", nil, "Certifications (repeatable)")
cmd.Flags().BoolVar(&flagDryRun, "dry-run", false, "Print the resulting row without writing")
_ = cmd.MarkFlagRequired("name")
_ = cmd.MarkFlagRequired("category")
cmd.RunE = func(cmd *cobra.Command, args []string) error {
ctx := cmd.Context()
category := coredata.ThirdPartyCategory(flagCategory)
if !category.IsValid() {
return fmt.Errorf("invalid --category value %q", flagCategory)
}
partySlug := flagSlug
if partySlug == "" {
partySlug = slug.Make(flagName)
}
if partySlug == "" {
return fmt.Errorf("cannot derive a slug from --name %q; pass --slug explicitly", flagName)
}
pgClient, err := f.PgClient()
if err != nil {
return err
}
out := f.IOStreams.Out
var (
party coredata.CommonThirdParty
inserted bool
)
if err := pgClient.WithTx(
ctx,
func(ctx context.Context, tx pg.Tx) error {
now := time.Now()
existing := coredata.CommonThirdParty{}
err := existing.LoadBySlug(ctx, tx, partySlug)
switch {
case err == nil:
party = existing
case errors.Is(err, coredata.ErrResourceNotFound):
party = coredata.CommonThirdParty{
ID: gid.New(gid.NilTenant, coredata.CommonThirdPartyEntityType),
Slug: partySlug,
Certifications: []string{},
CreatedAt: now,
}
default:
return fmt.Errorf("cannot load common third party by slug: %w", err)
}
party.Name = flagName
party.Category = category
party.Slug = partySlug
party.UpdatedAt = now
applyFlag(cmd, "website-url", &party.WebsiteURL, flagWebsiteURL)
applyFlag(cmd, "legal-name", &party.LegalName, flagLegalName)
applyFlag(cmd, "headquarter-address", &party.HeadquarterAddress, flagHeadquarterAddress)
applyFlag(cmd, "privacy-policy-url", &party.PrivacyPolicyURL, flagPrivacyPolicyURL)
applyFlag(cmd, "service-level-agreement-url", &party.ServiceLevelAgreementURL, flagServiceLevelAgreementURL)
applyFlag(cmd, "service-software-agreement-url", &party.ServiceSoftwareAgreementURL, flagServiceSoftwareAgreementURL)
applyFlag(cmd, "data-processing-agreement-url", &party.DataProcessingAgreementURL, flagDataProcessingAgreementURL)
applyFlag(cmd, "business-associate-agreement-url", &party.BusinessAssociateAgreementURL, flagBusinessAssociateAgreementURL)
applyFlag(cmd, "subprocessors-list-url", &party.SubprocessorsListURL, flagSubprocessorsListURL)
applyFlag(cmd, "status-page-url", &party.StatusPageURL, flagStatusPageURL)
applyFlag(cmd, "terms-of-service-url", &party.TermsOfServiceURL, flagTermsOfServiceURL)
applyFlag(cmd, "security-page-url", &party.SecurityPageURL, flagSecurityPageURL)
applyFlag(cmd, "trust-page-url", &party.TrustPageURL, flagTrustPageURL)
if cmd.Flags().Changed("certifications") {
party.Certifications = flagCertifications
}
if flagDryRun {
return nil
}
inserted, err = party.Upsert(ctx, tx)
if err != nil {
return fmt.Errorf("cannot upsert common third party: %w", err)
}
return nil
},
); err != nil {
return err
}
if flagDryRun {
_, _ = fmt.Fprintf(out, "Would upsert common third party %q (slug %q, category %s).\n", party.Name, party.Slug, party.Category)
return nil
}
action := "Updated"
if inserted {
action = "Created"
}
_, _ = fmt.Fprintf(out, "%s common third party %s (%q, slug %q).\n", action, party.ID.String(), party.Name, party.Slug)
return nil
}
return cmd
}
// applyFlag overrides target with value when the named flag was passed.
// An empty value clears the column; an unset flag leaves it untouched, so
// an upsert can update one field without blanking the rest of the row.
func applyFlag(cmd *cobra.Command, name string, target **string, value string) {
if !cmd.Flags().Changed(name) {
return
}
if value == "" {
*target = nil
return
}
*target = &value
}

View File

@@ -39,6 +39,9 @@ func NewCmdCommonTrackerPattern(f *cmdutil.Factory) *cobra.Command {
cmd.AddCommand(newCmdShow(f))
cmd.AddCommand(newCmdReenrich(f))
cmd.AddCommand(newCmdStats(f))
cmd.AddCommand(newCmdLink(f))
cmd.AddCommand(newCmdUnlink(f))
cmd.AddCommand(newCmdSetDescription(f))
return cmd
}

View File

@@ -0,0 +1,163 @@
// 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 commontrackerpattern
import (
"context"
"fmt"
"github.com/spf13/cobra"
"go.gearno.de/kit/pg"
"go.probo.inc/probo/pkg/coredata"
"go.probo.inc/probo/pkg/proboctl/cmdutil"
)
func newCmdLink(f *cmdutil.Factory) *cobra.Command {
var (
flagIDs []string
flagLinkedBanner string
flagLinkedOrg string
flagCommonThirdParty string
flagTrackerType string
flagKeyword string
flagState string
flagWithoutDescription bool
flagTo string
flagDryRun bool
flagYes bool
)
cmd := &cobra.Command{
Use: "link",
Short: "Link common tracker patterns to a common third party",
Long: "Point selected common tracker patterns at a common third party " +
"(--to-common-third-party). The catalog rows are re-armed for enrichment " +
"so a description is re-derived for the vendor, and the uncategorised org " +
"tracker patterns linked to them are remapped (org third party cleared, " +
"mapping re-armed) so the mapping worker re-resolves the vendor. " +
"User-categorised and excluded org patterns are left untouched. Selection " +
"mirrors 'reenrich'. To detach patterns, use 'unlink'.",
Args: cobra.NoArgs,
}
cmd.Flags().StringSliceVar(&flagIDs, "id", nil, "Common tracker pattern GID(s) to link (repeatable)")
cmd.Flags().StringVar(&flagLinkedBanner, "linked-banner", "", "Select catalog rows linked to a cookie banner's patterns (GID)")
cmd.Flags().StringVar(&flagLinkedOrg, "linked-org", "", "Select catalog rows linked to an organization's patterns (GID)")
cmd.Flags().StringVar(&flagCommonThirdParty, "common-third-party", "", "Select patterns currently linked to a common third party (slug or GID)")
cmd.Flags().StringVar(&flagTrackerType, "tracker-type", "", "Filter selected patterns by tracker type")
cmd.Flags().StringVar(&flagKeyword, "keyword", "", "Filter selected patterns by a pattern/description substring")
cmd.Flags().StringVar(&flagState, "state", "", "Filter selected patterns by enrichment state (queued, enriched, unenriched)")
cmd.Flags().BoolVar(&flagWithoutDescription, "without-description", false, "Only patterns with a blank description")
cmd.Flags().StringVar(&flagTo, "to-common-third-party", "", "Target common third party to link to (slug or GID)")
cmd.Flags().BoolVar(&flagDryRun, "dry-run", false, "Print the selected patterns without linking")
cmd.Flags().BoolVar(&flagYes, "yes", false, "Skip confirmation")
_ = cmd.MarkFlagRequired("to-common-third-party")
cmd.RunE = func(cmd *cobra.Command, args []string) error {
ctx := cmd.Context()
pgClient, err := f.PgClient()
if err != nil {
return err
}
ids, err := resolveReenrichIDs(
ctx,
pgClient,
flagIDs,
flagLinkedBanner,
flagLinkedOrg,
flagCommonThirdParty,
flagTrackerType,
flagKeyword,
flagState,
flagWithoutDescription,
)
if err != nil {
return err
}
out := f.IOStreams.Out
if len(ids) == 0 {
_, _ = fmt.Fprintln(out, "No common tracker patterns matched the selection.")
return nil
}
if flagDryRun {
_, _ = fmt.Fprintf(out, "Would link %d common tracker pattern(s) to %s.\n", len(ids), flagTo)
printSample(out, ids)
return nil
}
if !flagYes {
return fmt.Errorf("about to link %d pattern(s) to %s; pass --yes to proceed or --dry-run to preview", len(ids), flagTo)
}
var (
linked int64
requeued int64
remapped int64
)
if err := pgClient.WithTx(
ctx,
func(ctx context.Context, tx pg.Tx) error {
thirdPartyID, err := resolveCommonThirdPartyID(ctx, tx, flagTo)
if err != nil {
return err
}
var ps coredata.CommonTrackerPatterns
linked, err = ps.RelinkCommonThirdPartyByIDs(ctx, tx, ids, &thirdPartyID)
if err != nil {
return err
}
requeued, err = ps.RequestEnrichmentByIDs(ctx, tx, ids)
if err != nil {
return err
}
var tps coredata.TrackerPatterns
remapped, err = tps.RequestMappingForUncategorisedByCommonTrackerPatternIDs(ctx, tx, ids)
if err != nil {
return err
}
return nil
},
); err != nil {
return fmt.Errorf("cannot link common tracker patterns: %w", err)
}
_, _ = fmt.Fprintf(
out,
"Linked %d pattern(s) to %s, re-queued %d for enrichment, remapped %d uncategorised org tracker pattern(s).\n",
linked,
flagTo,
requeued,
remapped,
)
return nil
}
return cmd
}

View File

@@ -0,0 +1,109 @@
// 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 commontrackerpattern
import (
"context"
"errors"
"fmt"
"github.com/spf13/cobra"
"go.gearno.de/kit/pg"
"go.probo.inc/probo/pkg/coredata"
"go.probo.inc/probo/pkg/gid"
"go.probo.inc/probo/pkg/proboctl/cmdutil"
)
func newCmdSetDescription(f *cmdutil.Factory) *cobra.Command {
var (
flagDescription string
flagYes bool
)
cmd := &cobra.Command{
Use: "set-description <gid>",
Short: "Set a common tracker pattern's description and backfill org patterns",
Long: "Write a description on a common tracker pattern and mark it enriched so " +
"the enrichment worker leaves it alone, then backfill the description onto " +
"every linked org tracker pattern that does not already have one.",
Args: cobra.ExactArgs(1),
}
cmd.Flags().StringVar(&flagDescription, "description", "", "Description to set (required)")
cmd.Flags().BoolVar(&flagYes, "yes", false, "Skip confirmation")
_ = cmd.MarkFlagRequired("description")
cmd.RunE = func(cmd *cobra.Command, args []string) error {
ctx := cmd.Context()
if flagDescription == "" {
return fmt.Errorf("--description must not be empty")
}
id, err := gid.ParseGID(args[0])
if err != nil {
return fmt.Errorf("invalid GID %q: %w", args[0], err)
}
if !flagYes {
return fmt.Errorf("about to set the description on %s; pass --yes to proceed", id.String())
}
pgClient, err := f.PgClient()
if err != nil {
return err
}
out := f.IOStreams.Out
var backfilled int64
if err := pgClient.WithTx(
ctx,
func(ctx context.Context, tx pg.Tx) error {
var pattern coredata.CommonTrackerPattern
if err := pattern.LoadByID(ctx, tx, id); err != nil {
if errors.Is(err, coredata.ErrResourceNotFound) {
return fmt.Errorf("no common tracker pattern found for %q", args[0])
}
return fmt.Errorf("cannot load common tracker pattern: %w", err)
}
if err := pattern.SetEnriched(ctx, tx, flagDescription, nil); err != nil {
return fmt.Errorf("cannot set common tracker pattern description: %w", err)
}
var tps coredata.TrackerPatterns
backfilled, err = tps.BackfillDescriptionByCommonTrackerPatternID(ctx, tx, id, flagDescription)
if err != nil {
return err
}
return nil
},
); err != nil {
return err
}
_, _ = fmt.Fprintf(out, "Set description on %s, backfilled %d org tracker pattern(s).\n", id.String(), backfilled)
return nil
}
return cmd
}

View File

@@ -0,0 +1,125 @@
// 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 commontrackerpattern
import (
"context"
"fmt"
"github.com/spf13/cobra"
"go.gearno.de/kit/pg"
"go.probo.inc/probo/pkg/coredata"
"go.probo.inc/probo/pkg/proboctl/cmdutil"
)
func newCmdUnlink(f *cmdutil.Factory) *cobra.Command {
var (
flagIDs []string
flagLinkedBanner string
flagLinkedOrg string
flagCommonThirdParty string
flagTrackerType string
flagKeyword string
flagState string
flagWithoutDescription bool
flagDryRun bool
flagYes bool
)
cmd := &cobra.Command{
Use: "unlink",
Short: "Unlink common tracker patterns from any common third party",
Long: "Detach selected common tracker patterns from their common third party. " +
"Unlinking only clears the catalog link: there is no new vendor to " +
"re-enrich a description for or to remap org patterns onto, so neither " +
"enrichment nor org remapping is triggered. Selection mirrors 'reenrich'.",
Args: cobra.NoArgs,
}
cmd.Flags().StringSliceVar(&flagIDs, "id", nil, "Common tracker pattern GID(s) to unlink (repeatable)")
cmd.Flags().StringVar(&flagLinkedBanner, "linked-banner", "", "Select catalog rows linked to a cookie banner's patterns (GID)")
cmd.Flags().StringVar(&flagLinkedOrg, "linked-org", "", "Select catalog rows linked to an organization's patterns (GID)")
cmd.Flags().StringVar(&flagCommonThirdParty, "common-third-party", "", "Select patterns currently linked to a common third party (slug or GID)")
cmd.Flags().StringVar(&flagTrackerType, "tracker-type", "", "Filter selected patterns by tracker type")
cmd.Flags().StringVar(&flagKeyword, "keyword", "", "Filter selected patterns by a pattern/description substring")
cmd.Flags().StringVar(&flagState, "state", "", "Filter selected patterns by enrichment state (queued, enriched, unenriched)")
cmd.Flags().BoolVar(&flagWithoutDescription, "without-description", false, "Only patterns with a blank description")
cmd.Flags().BoolVar(&flagDryRun, "dry-run", false, "Print the selected patterns without unlinking")
cmd.Flags().BoolVar(&flagYes, "yes", false, "Skip confirmation")
cmd.RunE = func(cmd *cobra.Command, args []string) error {
ctx := cmd.Context()
pgClient, err := f.PgClient()
if err != nil {
return err
}
ids, err := resolveReenrichIDs(
ctx,
pgClient,
flagIDs,
flagLinkedBanner,
flagLinkedOrg,
flagCommonThirdParty,
flagTrackerType,
flagKeyword,
flagState,
flagWithoutDescription,
)
if err != nil {
return err
}
out := f.IOStreams.Out
if len(ids) == 0 {
_, _ = fmt.Fprintln(out, "No common tracker patterns matched the selection.")
return nil
}
if flagDryRun {
_, _ = fmt.Fprintf(out, "Would unlink %d common tracker pattern(s).\n", len(ids))
printSample(out, ids)
return nil
}
if !flagYes {
return fmt.Errorf("about to unlink %d pattern(s); pass --yes to proceed or --dry-run to preview", len(ids))
}
var unlinked int64
if err := pgClient.WithTx(
ctx,
func(ctx context.Context, tx pg.Tx) error {
var ps coredata.CommonTrackerPatterns
unlinked, err = ps.RelinkCommonThirdPartyByIDs(ctx, tx, ids, nil)
return err
},
); err != nil {
return fmt.Errorf("cannot unlink common tracker patterns: %w", err)
}
_, _ = fmt.Fprintf(out, "Unlinked %d pattern(s) from any common third party.\n", unlinked)
return nil
}
return cmd
}