// Command import-candidates sources cleared IT and cyber leads from People Data Labs (the Galapagos // preset) and upserts them into the tenant candidates table tagged external, writing a compliance row // per record (source, provider, query, legal basis, collected at). Clearance is left unknown: absence // is never a negative. Run with `make import-candidates` (small free tier batches). Flags let you set // the batch size, include contact fields, and set the legal basis. Reads PDL_API_KEY plus // MM_MIGRATOR_DATABASE_URL plus optional MM_DEMO_ORG_ID from env. package main import ( "context" "encoding/json" "errors" "flag" "fmt" "log" "os" "strings" "github.com/google/uuid" "github.com/jackc/pgx/v5/pgxpool" "github.com/missionworkforce/missionmatch/internal/platform/database" "github.com/missionworkforce/missionmatch/internal/platform/database/dbgen" "github.com/missionworkforce/missionmatch/internal/platform/extraction" "github.com/missionworkforce/missionmatch/internal/platform/sourcing" ) const defaultOrg = "11111111-1111-1111-1111-111111111111" func main() { size := flag.Int("size", 10, "number of leads to source (keep small for the free tier)") includeContact := flag.Bool("include-contact", false, "store email and phone (off by default)") legalBasis := flag.String("legal-basis", "Legitimate interest: recruitment sourcing of publicly available professional data", "the lawful basis recorded in the compliance log") flag.Parse() ctx := context.Background() key := strings.TrimSpace(os.Getenv("PDL_API_KEY")) dsn := firstNonEmpty(os.Getenv("MM_MIGRATOR_DATABASE_URL"), os.Getenv("MM_DATABASE_URL")) orgStr := firstNonEmpty(os.Getenv("MM_DEMO_ORG_ID"), defaultOrg) if key == "" { log.Fatal("import-candidates: PDL_API_KEY not set (server side env only)") } if dsn == "" { log.Fatal("import-candidates: set MM_MIGRATOR_DATABASE_URL (or MM_DATABASE_URL)") } orgID, err := uuid.Parse(orgStr) if err != nil { log.Fatalf("import-candidates: bad org id %q: %v", orgStr, err) } pool, err := pgxpool.New(ctx, dsn) if err != nil { log.Fatalf("import-candidates: db connect: %v", err) } defer pool.Close() if err := pool.Ping(ctx); err != nil { log.Fatalf("import-candidates: db ping: %v", err) } // Writes the candidate and its compliance row with an explicit organization_id and no // app.current_org_id, so the BYPASSRLS migrator role is required or RLS rejects every insert. if err := database.AssertMigratorRole(ctx, pool); err != nil { log.Fatalf("import-candidates: %v", err) } q := dbgen.New(pool) ext := extraction.NewDeterministic() // certs and agencies from the profile text; skills come structured prov := sourcing.NewPDL(key, os.Getenv("PDL_API_BASE"), *includeContact) queryObj := sourcing.GalapagosSearch() queryJSON, _ := json.Marshal(queryObj) log.Printf("import-candidates: provider=%q size=%d include_contact=%v org=%s", prov.Name(), *size, *includeContact, orgID) people, err := prov.Search(ctx, sourcing.SearchQuery{ESQuery: queryObj, Size: *size}) if err != nil { if errors.Is(err, sourcing.ErrThrottled) { log.Printf("import-candidates: provider rate limited (429), stopping run gracefully") return } log.Fatalf("import-candidates: search: %v", err) } log.Printf("import-candidates: provider returned %d leads", len(people)) upserted := 0 for _, sp := range people { if err := importOne(ctx, pool, q, ext, orgID, sp, prov.Name(), string(queryJSON), *legalBasis); err != nil { log.Printf("import-candidates: %s: %v", sp.ExternalID, err) continue } upserted++ } log.Printf("import-candidates: DONE, upserted %d external candidates with compliance rows", upserted) } // importOne resolves and upserts one sourced person (identity resolution + structured skills, certs, and // agencies) and writes its compliance row inside a single transaction, so a lead is never persisted // without the legal basis record that justifies holding it. Either both commit or neither does, which // keeps the sourcing provenance promise (a compliance row per record) true even if the second write // fails. Only a fully committed record counts toward the total. func importOne(ctx context.Context, pool *pgxpool.Pool, base *dbgen.Queries, ext extraction.Extractor, orgID uuid.UUID, sp sourcing.SourcedPerson, provider, queryJSON, legalBasis string) error { tx, err := pool.Begin(ctx) if err != nil { return fmt.Errorf("begin tx: %w", err) } defer func() { _ = tx.Rollback(ctx) }() // no op after a successful Commit q := base.WithTx(tx) candID, err := sourcing.IngestPerson(ctx, q, orgID, sp, ext) if err != nil { return err } if err := q.InsertSourcingLog(ctx, dbgen.InsertSourcingLogParams{ OrganizationID: orgID, CandidateID: &candID, ExternalID: sp.ExternalID, Source: "PDL", Provider: provider, Query: queryJSON, LegalBasis: legalBasis, }); err != nil { return fmt.Errorf("insert sourcing log: %w", err) } if err := tx.Commit(ctx); err != nil { return fmt.Errorf("commit tx: %w", err) } return nil } func firstNonEmpty(vals ...string) string { for _, v := range vals { if s := strings.TrimSpace(v); s != "" { return s } } return "" }