Files
probo/pkg/proboctl/seed/common-third-parties/common_third_parties.go
Émile Ré 1a9cef7a9d Embed third-party data.json in seed command binary
Replace the --data flag with go:embed so the dataset is baked into the
proboctl binary and the seed command needs no external file argument.

Signed-off-by: Émile Ré <emile@probo.com>
2026-05-18 19:05:10 +04:00

190 lines
6.7 KiB
Go

// 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 commonthirdparties
import (
"bytes"
"context"
_ "embed"
"encoding/json"
"fmt"
"io"
"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"
)
//go:embed data/data.json
var dataJSON []byte
type thirdPartyData struct {
Name string `json:"name"`
Category *string `json:"category,omitempty"`
HeadquarterAddress *string `json:"headquarterAddress,omitempty"`
LegalName *string `json:"legalName,omitempty"`
WebsiteURL *string `json:"websiteUrl,omitempty"`
PrivacyPolicyURL *string `json:"privacyPolicyUrl,omitempty"`
ServiceLevelAgreementURL *string `json:"serviceLevelAgreementUrl,omitempty"`
ServiceSoftwareAgreementURL *string `json:"serviceSoftwareAgreementUrl,omitempty"`
DataProcessingAgreementURL *string `json:"dataProcessingAgreementUrl,omitempty"`
BusinessAssociateAgreementURL *string `json:"businessAssociateAgreementUrl,omitempty"`
SubprocessorsListURL *string `json:"subprocessorsListUrl,omitempty"`
Certifications []string `json:"certifications,omitempty"`
StatusPageURL *string `json:"statusPageUrl,omitempty"`
TermsOfServiceURL *string `json:"termsOfServiceUrl,omitempty"`
SecurityPageURL *string `json:"securityPageUrl,omitempty"`
TrustPageURL *string `json:"trustPageUrl,omitempty"`
Domains []string `json:"domains,omitempty"`
}
func NewCmdCommonThirdParties(f *cmdutil.Factory) *cobra.Command {
cmd := &cobra.Command{
Use: "common-third-parties",
Short: "Seed common third parties",
Long: "Seed the common_third_parties table from the embedded dataset. " +
"Re-running is safe: existing rows are upserted on conflict (slug) " +
"so ids and created_at are preserved.",
RunE: func(cmd *cobra.Command, args []string) error {
out := f.IOStreams.Out
errOut := f.IOStreams.ErrOut
ctx := cmd.Context()
thirdParties, err := loadThirdParties()
if err != nil {
return fmt.Errorf("cannot load third-party data: %w", err)
}
pgClient, err := f.PgClient()
if err != nil {
return fmt.Errorf("cannot create pg client: %w", err)
}
_, _ = fmt.Fprintf(out, "seeding %d common third parties\n", len(thirdParties))
var inserted, updated, domainsInserted, domainsUpdated int
if err := pgClient.WithTx(
ctx,
func(ctx context.Context, tx pg.Tx) error {
now := time.Now()
for _, tp := range thirdParties {
party := coredata.CommonThirdParty{
ID: gid.New(gid.NilTenant, coredata.CommonThirdPartyEntityType),
Name: tp.Name,
Slug: slug.Make(tp.Name),
Category: parseCategory(errOut, tp),
HeadquarterAddress: tp.HeadquarterAddress,
LegalName: tp.LegalName,
WebsiteURL: tp.WebsiteURL,
PrivacyPolicyURL: tp.PrivacyPolicyURL,
ServiceLevelAgreementURL: tp.ServiceLevelAgreementURL,
ServiceSoftwareAgreementURL: tp.ServiceSoftwareAgreementURL,
DataProcessingAgreementURL: tp.DataProcessingAgreementURL,
BusinessAssociateAgreementURL: tp.BusinessAssociateAgreementURL,
SubprocessorsListURL: tp.SubprocessorsListURL,
Certifications: tp.Certifications,
StatusPageURL: tp.StatusPageURL,
TermsOfServiceURL: tp.TermsOfServiceURL,
SecurityPageURL: tp.SecurityPageURL,
TrustPageURL: tp.TrustPageURL,
CreatedAt: now,
UpdatedAt: now,
}
wasInserted, err := party.Upsert(ctx, tx)
if err != nil {
return fmt.Errorf("cannot upsert common third party %q: %w", tp.Name, err)
}
if wasInserted {
inserted++
} else {
updated++
if err := party.LoadByName(ctx, tx, tp.Name); err != nil {
return fmt.Errorf("cannot reload common third party %q: %w", tp.Name, err)
}
}
for _, domain := range tp.Domains {
d := coredata.CommonThirdPartyDomain{
ID: gid.New(gid.NilTenant, coredata.CommonThirdPartyDomainEntityType),
CommonThirdPartyID: party.ID,
Domain: domain,
CreatedAt: now,
UpdatedAt: now,
}
domainInserted, err := d.Upsert(ctx, tx)
if err != nil {
return fmt.Errorf("cannot upsert domain %q for %q: %w", domain, tp.Name, err)
}
if domainInserted {
domainsInserted++
} else {
domainsUpdated++
}
}
}
return nil
},
); err != nil {
return err
}
_, _ = fmt.Fprintf(out, "seeded %d third parties (%d inserted, %d updated)\n", len(thirdParties), inserted, updated)
_, _ = fmt.Fprintf(out, "seeded %d domains (%d inserted, %d updated)\n", domainsInserted+domainsUpdated, domainsInserted, domainsUpdated)
return nil
},
}
return cmd
}
func loadThirdParties() ([]thirdPartyData, error) {
var thirdParties []thirdPartyData
dec := json.NewDecoder(bytes.NewReader(dataJSON))
dec.DisallowUnknownFields()
if err := dec.Decode(&thirdParties); err != nil {
return nil, fmt.Errorf("cannot decode embedded data.json: %w", err)
}
return thirdParties, nil
}
func parseCategory(errOut io.Writer, tp thirdPartyData) coredata.ThirdPartyCategory {
if tp.Category == nil || *tp.Category == "" {
return coredata.ThirdPartyCategoryOther
}
var c coredata.ThirdPartyCategory
if err := c.Scan(*tp.Category); err != nil {
_, _ = fmt.Fprintf(errOut, "warning: third party %q has unknown category %q, falling back to OTHER\n", tp.Name, *tp.Category)
return coredata.ThirdPartyCategoryOther
}
return c
}