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:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
206
pkg/proboctl/commonthirdparty/upsert.go
vendored
Normal 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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
163
pkg/proboctl/commontrackerpattern/link.go
Normal file
163
pkg/proboctl/commontrackerpattern/link.go
Normal 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
|
||||
}
|
||||
109
pkg/proboctl/commontrackerpattern/set_description.go
Normal file
109
pkg/proboctl/commontrackerpattern/set_description.go
Normal 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
|
||||
}
|
||||
125
pkg/proboctl/commontrackerpattern/unlink.go
Normal file
125
pkg/proboctl/commontrackerpattern/unlink.go
Normal 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
|
||||
}
|
||||
Reference in New Issue
Block a user