Add slug-based dedup and auto-create third parties from OCD
Add a slug column to common_third_parties for stable cross-source deduplication. The OCD tracker patterns importer now resolves third parties via a three-step cascade (slug lookup, domain fallback, auto-create) instead of silently dropping unmatched links. Signed-off-by: Émile Ré <emile@probo.com>
This commit is contained in:
@@ -14,7 +14,7 @@
|
|||||||
|
|
||||||
// Command common-third-parties-import seeds the common_third_parties table from
|
// Command common-third-parties-import seeds the common_third_parties table from
|
||||||
// packages/thirdParties/data.json. It is idempotent: re-running upserts on conflict
|
// packages/thirdParties/data.json. It is idempotent: re-running upserts on conflict
|
||||||
// (lower(name)) so existing rows keep their id and created_at.
|
// (slug) so existing rows keep their id and created_at.
|
||||||
//
|
//
|
||||||
// When -fetch-logos is set, the tool inspects each third party's website to
|
// When -fetch-logos is set, the tool inspects each third party's website to
|
||||||
// find the best available logo (SVG icon, apple-touch-icon, etc.) and stores
|
// find the best available logo (SVG icon, apple-touch-icon, etc.) and stores
|
||||||
@@ -44,6 +44,7 @@ import (
|
|||||||
"go.probo.inc/probo/pkg/coredata"
|
"go.probo.inc/probo/pkg/coredata"
|
||||||
"go.probo.inc/probo/pkg/filemanager"
|
"go.probo.inc/probo/pkg/filemanager"
|
||||||
"go.probo.inc/probo/pkg/gid"
|
"go.probo.inc/probo/pkg/gid"
|
||||||
|
"go.probo.inc/probo/pkg/slug"
|
||||||
"go.probo.inc/probo/pkg/version"
|
"go.probo.inc/probo/pkg/version"
|
||||||
"go.probo.inc/probo/pkg/webinspect"
|
"go.probo.inc/probo/pkg/webinspect"
|
||||||
)
|
)
|
||||||
@@ -177,6 +178,7 @@ func run() error {
|
|||||||
party := coredata.CommonThirdParty{
|
party := coredata.CommonThirdParty{
|
||||||
ID: gid.New(gid.NilTenant, coredata.CommonThirdPartyEntityType),
|
ID: gid.New(gid.NilTenant, coredata.CommonThirdPartyEntityType),
|
||||||
Name: tp.Name,
|
Name: tp.Name,
|
||||||
|
Slug: slug.Make(tp.Name),
|
||||||
Category: parseCategory(tp),
|
Category: parseCategory(tp),
|
||||||
HeadquarterAddress: tp.HeadquarterAddress,
|
HeadquarterAddress: tp.HeadquarterAddress,
|
||||||
LegalName: tp.LegalName,
|
LegalName: tp.LegalName,
|
||||||
|
|||||||
@@ -19,16 +19,17 @@
|
|||||||
// existing rows are updated on the unique constraint so ids and created_at
|
// existing rows are updated on the unique constraint so ids and created_at
|
||||||
// are preserved.
|
// are preserved.
|
||||||
//
|
//
|
||||||
// Entries are linked to the matching common_third_parties row (by
|
// Entries are linked to the matching common_third_parties row via a
|
||||||
// case-insensitive name lookup on the platform key). Entries whose platform
|
// three-step cascade: slug lookup, domain lookup, then auto-create.
|
||||||
// cannot be resolved are inserted with a NULL common_third_party_id.
|
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
"flag"
|
"flag"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"math"
|
||||||
"net"
|
"net"
|
||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
@@ -43,6 +44,7 @@ import (
|
|||||||
"go.gearno.de/kit/pg"
|
"go.gearno.de/kit/pg"
|
||||||
"go.probo.inc/probo/pkg/coredata"
|
"go.probo.inc/probo/pkg/coredata"
|
||||||
"go.probo.inc/probo/pkg/gid"
|
"go.probo.inc/probo/pkg/gid"
|
||||||
|
"go.probo.inc/probo/pkg/slug"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -68,6 +70,8 @@ type (
|
|||||||
TrackerType string
|
TrackerType string
|
||||||
MatchType string
|
MatchType string
|
||||||
ThirdPartyName *string
|
ThirdPartyName *string
|
||||||
|
Domain string
|
||||||
|
Category string
|
||||||
Description string
|
Description string
|
||||||
MaxAgeSeconds *int
|
MaxAgeSeconds *int
|
||||||
Confidence float32
|
Confidence float32
|
||||||
@@ -119,6 +123,7 @@ func run() error {
|
|||||||
fmt.Printf("importing %d common tracker patterns from Open Cookie Database\n", len(patterns))
|
fmt.Printf("importing %d common tracker patterns from Open Cookie Database\n", len(patterns))
|
||||||
|
|
||||||
var inserted, updated, skipped int
|
var inserted, updated, skipped int
|
||||||
|
var partiesCreated int
|
||||||
|
|
||||||
if err := pgClient.WithTx(
|
if err := pgClient.WithTx(
|
||||||
ctx,
|
ctx,
|
||||||
@@ -127,26 +132,9 @@ func run() error {
|
|||||||
thirdPartyCache := make(map[string]*gid.GID)
|
thirdPartyCache := make(map[string]*gid.GID)
|
||||||
|
|
||||||
for _, p := range patterns {
|
for _, p := range patterns {
|
||||||
var thirdPartyID *gid.GID
|
thirdPartyID, err := resolveThirdParty(ctx, tx, p, thirdPartyCache, now, &partiesCreated)
|
||||||
|
if err != nil {
|
||||||
if p.ThirdPartyName != nil {
|
return err
|
||||||
if cached, ok := thirdPartyCache[*p.ThirdPartyName]; ok {
|
|
||||||
thirdPartyID = cached
|
|
||||||
} else {
|
|
||||||
var party coredata.CommonThirdParty
|
|
||||||
if err := party.LoadByName(ctx, tx, *p.ThirdPartyName); err != nil {
|
|
||||||
fmt.Fprintf(
|
|
||||||
os.Stderr,
|
|
||||||
"warning: cannot find third party %q for pattern %q, skipping link\n",
|
|
||||||
*p.ThirdPartyName,
|
|
||||||
p.Pattern,
|
|
||||||
)
|
|
||||||
thirdPartyCache[*p.ThirdPartyName] = nil
|
|
||||||
} else {
|
|
||||||
thirdPartyCache[*p.ThirdPartyName] = &party.ID
|
|
||||||
thirdPartyID = &party.ID
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
trackerType, err := parseTrackerType(p.TrackerType)
|
trackerType, err := parseTrackerType(p.TrackerType)
|
||||||
@@ -195,11 +183,12 @@ func run() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fmt.Printf(
|
fmt.Printf(
|
||||||
"imported %d patterns (%d inserted, %d updated, %d skipped)\n",
|
"imported %d patterns (%d inserted, %d updated, %d skipped, %d third parties auto-created)\n",
|
||||||
len(patterns)-skipped,
|
len(patterns)-skipped,
|
||||||
inserted,
|
inserted,
|
||||||
updated,
|
updated,
|
||||||
skipped,
|
skipped,
|
||||||
|
partiesCreated,
|
||||||
)
|
)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
@@ -271,6 +260,8 @@ func loadPatternsFromOCD(dir string) ([]trackerPatternData, error) {
|
|||||||
TrackerType: "COOKIE",
|
TrackerType: "COOKIE",
|
||||||
MatchType: matchType,
|
MatchType: matchType,
|
||||||
ThirdPartyName: new(platform),
|
ThirdPartyName: new(platform),
|
||||||
|
Domain: e.Domain,
|
||||||
|
Category: e.Category,
|
||||||
Description: e.Description,
|
Description: e.Description,
|
||||||
MaxAgeSeconds: parseRetentionPeriod(e.RetentionPeriod),
|
MaxAgeSeconds: parseRetentionPeriod(e.RetentionPeriod),
|
||||||
Confidence: 1.0,
|
Confidence: 1.0,
|
||||||
@@ -282,6 +273,136 @@ func loadPatternsFromOCD(dir string) ([]trackerPatternData, error) {
|
|||||||
return patterns, nil
|
return patterns, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func resolveThirdParty(
|
||||||
|
ctx context.Context,
|
||||||
|
tx pg.Tx,
|
||||||
|
p trackerPatternData,
|
||||||
|
cache map[string]*gid.GID,
|
||||||
|
now time.Time,
|
||||||
|
created *int,
|
||||||
|
) (*gid.GID, error) {
|
||||||
|
if p.ThirdPartyName == nil || *p.ThirdPartyName == "" {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
platformSlug := slug.Make(*p.ThirdPartyName)
|
||||||
|
if platformSlug == "" {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
if cached, ok := cache[platformSlug]; ok {
|
||||||
|
return cached, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var party coredata.CommonThirdParty
|
||||||
|
if err := party.LoadBySlug(ctx, tx, platformSlug); err != nil {
|
||||||
|
if !errors.Is(err, coredata.ErrResourceNotFound) {
|
||||||
|
return nil, fmt.Errorf("cannot look up common third party by slug %q: %w", platformSlug, err)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
cache[platformSlug] = &party.ID
|
||||||
|
return &party.ID, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
domain := normalizeDomain(p.Domain)
|
||||||
|
if domain != "" {
|
||||||
|
var domainRow coredata.CommonThirdPartyDomain
|
||||||
|
if err := domainRow.LoadByDomain(ctx, tx, domain); err != nil {
|
||||||
|
if !errors.Is(err, coredata.ErrResourceNotFound) {
|
||||||
|
return nil, fmt.Errorf("cannot look up common third party by domain %q: %w", domain, err)
|
||||||
|
}
|
||||||
|
} else if err := party.LoadByID(ctx, tx, domainRow.CommonThirdPartyID); err == nil {
|
||||||
|
cache[platformSlug] = &party.ID
|
||||||
|
return &party.ID, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
party = coredata.CommonThirdParty{
|
||||||
|
ID: gid.New(gid.NilTenant, coredata.CommonThirdPartyEntityType),
|
||||||
|
Name: *p.ThirdPartyName,
|
||||||
|
Slug: platformSlug,
|
||||||
|
Category: mapOCDCategory(p.Category),
|
||||||
|
Certifications: []string{},
|
||||||
|
CreatedAt: now,
|
||||||
|
UpdatedAt: now,
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := party.Upsert(ctx, tx); err != nil {
|
||||||
|
return nil, fmt.Errorf("cannot auto-create common third party %q: %w", *p.ThirdPartyName, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := party.LoadBySlug(ctx, tx, platformSlug); err != nil {
|
||||||
|
return nil, fmt.Errorf("cannot reload auto-created common third party %q: %w", *p.ThirdPartyName, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if domain != "" {
|
||||||
|
d := coredata.CommonThirdPartyDomain{
|
||||||
|
ID: gid.New(gid.NilTenant, coredata.CommonThirdPartyDomainEntityType),
|
||||||
|
CommonThirdPartyID: party.ID,
|
||||||
|
Domain: domain,
|
||||||
|
CreatedAt: now,
|
||||||
|
UpdatedAt: now,
|
||||||
|
}
|
||||||
|
if _, err := d.Upsert(ctx, tx); err != nil {
|
||||||
|
return nil, fmt.Errorf("cannot upsert domain %q for %q: %w", domain, *p.ThirdPartyName, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
*created++
|
||||||
|
cache[platformSlug] = &party.ID
|
||||||
|
return &party.ID, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var domainValidRe = regexp.MustCompile(`^[a-zA-Z0-9][a-zA-Z0-9.-]*[a-zA-Z]$`)
|
||||||
|
|
||||||
|
func normalizeDomain(s string) string {
|
||||||
|
s = strings.TrimSpace(s)
|
||||||
|
|
||||||
|
// Strip zero-width spaces and other Unicode control characters.
|
||||||
|
s = strings.Map(func(r rune) rune {
|
||||||
|
if r == '\u200b' || r == '\ufeff' {
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
return r
|
||||||
|
}, s)
|
||||||
|
|
||||||
|
// Take only the first domain when "or" separates multiple values,
|
||||||
|
// e.g. ".bing.com (3rd party) or .microsoft.com (3rd party)".
|
||||||
|
if idx := strings.Index(s, " or "); idx != -1 {
|
||||||
|
s = s[:idx]
|
||||||
|
}
|
||||||
|
// Handle values starting with "or ", e.g. "or demdex.net (3rd party)".
|
||||||
|
s = strings.TrimPrefix(s, "or ")
|
||||||
|
|
||||||
|
if idx := strings.IndexByte(s, '('); idx != -1 {
|
||||||
|
s = strings.TrimSpace(s[:idx])
|
||||||
|
}
|
||||||
|
|
||||||
|
// Strip placeholders like "[account]".
|
||||||
|
if strings.ContainsAny(s, "[]") {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
s = strings.TrimPrefix(s, ".")
|
||||||
|
s = strings.TrimSuffix(s, ".")
|
||||||
|
s = strings.TrimSpace(s)
|
||||||
|
|
||||||
|
if s == "" || !domainValidRe.MatchString(s) {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
func mapOCDCategory(s string) coredata.ThirdPartyCategory {
|
||||||
|
switch strings.ToLower(strings.TrimSpace(s)) {
|
||||||
|
case "analytics":
|
||||||
|
return coredata.ThirdPartyCategoryAnalytics
|
||||||
|
case "marketing":
|
||||||
|
return coredata.ThirdPartyCategoryMarketing
|
||||||
|
default:
|
||||||
|
return coredata.ThirdPartyCategoryOther
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var retentionRe = regexp.MustCompile(`(?i)^(\d+)\s+(second|seconds|sec|secs|minute|minutes|mins|min|hour|hours|day|days|week|weeks|month|months|year|years)`)
|
var retentionRe = regexp.MustCompile(`(?i)^(\d+)\s+(second|seconds|sec|secs|minute|minutes|mins|min|hour|hours|day|days|week|weeks|month|months|year|years)`)
|
||||||
|
|
||||||
func parseRetentionPeriod(s string) *int {
|
func parseRetentionPeriod(s string) *int {
|
||||||
@@ -332,7 +453,7 @@ func parseRetentionPeriod(s string) *int {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
result := n * multiplier
|
result := min(n*multiplier, math.MaxInt32)
|
||||||
return &result
|
return &result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ type (
|
|||||||
CommonThirdParty struct {
|
CommonThirdParty struct {
|
||||||
ID gid.GID `db:"id"`
|
ID gid.GID `db:"id"`
|
||||||
Name string `db:"name"`
|
Name string `db:"name"`
|
||||||
|
Slug string `db:"slug"`
|
||||||
Category ThirdPartyCategory `db:"category"`
|
Category ThirdPartyCategory `db:"category"`
|
||||||
HeadquarterAddress *string `db:"headquarter_address"`
|
HeadquarterAddress *string `db:"headquarter_address"`
|
||||||
LegalName *string `db:"legal_name"`
|
LegalName *string `db:"legal_name"`
|
||||||
@@ -62,6 +63,7 @@ func (t *CommonThirdParty) LoadByID(
|
|||||||
SELECT
|
SELECT
|
||||||
id,
|
id,
|
||||||
name,
|
name,
|
||||||
|
slug,
|
||||||
category,
|
category,
|
||||||
headquarter_address,
|
headquarter_address,
|
||||||
legal_name,
|
legal_name,
|
||||||
@@ -117,6 +119,7 @@ func (t *CommonThirdParty) LoadByName(
|
|||||||
SELECT
|
SELECT
|
||||||
id,
|
id,
|
||||||
name,
|
name,
|
||||||
|
slug,
|
||||||
category,
|
category,
|
||||||
headquarter_address,
|
headquarter_address,
|
||||||
legal_name,
|
legal_name,
|
||||||
@@ -163,6 +166,62 @@ LIMIT 1;
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (t *CommonThirdParty) LoadBySlug(
|
||||||
|
ctx context.Context,
|
||||||
|
conn pg.Querier,
|
||||||
|
slug string,
|
||||||
|
) error {
|
||||||
|
q := `
|
||||||
|
SELECT
|
||||||
|
id,
|
||||||
|
name,
|
||||||
|
slug,
|
||||||
|
category,
|
||||||
|
headquarter_address,
|
||||||
|
legal_name,
|
||||||
|
website_url,
|
||||||
|
privacy_policy_url,
|
||||||
|
service_level_agreement_url,
|
||||||
|
service_software_agreement_url,
|
||||||
|
data_processing_agreement_url,
|
||||||
|
business_associate_agreement_url,
|
||||||
|
subprocessors_list_url,
|
||||||
|
certifications,
|
||||||
|
status_page_url,
|
||||||
|
terms_of_service_url,
|
||||||
|
security_page_url,
|
||||||
|
trust_page_url,
|
||||||
|
logo_file_id,
|
||||||
|
created_at,
|
||||||
|
updated_at
|
||||||
|
FROM
|
||||||
|
common_third_parties
|
||||||
|
WHERE
|
||||||
|
slug = @slug
|
||||||
|
LIMIT 1;
|
||||||
|
`
|
||||||
|
|
||||||
|
args := pgx.StrictNamedArgs{"slug": slug}
|
||||||
|
|
||||||
|
rows, err := conn.Query(ctx, q, args)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("cannot query common third party by slug: %w", err)
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
row, err := pgx.CollectExactlyOneRow(rows, pgx.RowToStructByName[CommonThirdParty])
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, pgx.ErrNoRows) {
|
||||||
|
return ErrResourceNotFound
|
||||||
|
}
|
||||||
|
return fmt.Errorf("cannot collect common third party by slug: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
*t = row
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (t CommonThirdParty) Insert(
|
func (t CommonThirdParty) Insert(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
conn pg.Tx,
|
conn pg.Tx,
|
||||||
@@ -171,6 +230,7 @@ func (t CommonThirdParty) Insert(
|
|||||||
INSERT INTO common_third_parties (
|
INSERT INTO common_third_parties (
|
||||||
id,
|
id,
|
||||||
name,
|
name,
|
||||||
|
slug,
|
||||||
category,
|
category,
|
||||||
headquarter_address,
|
headquarter_address,
|
||||||
legal_name,
|
legal_name,
|
||||||
@@ -192,6 +252,7 @@ INSERT INTO common_third_parties (
|
|||||||
) VALUES (
|
) VALUES (
|
||||||
@id,
|
@id,
|
||||||
@name,
|
@name,
|
||||||
|
@slug,
|
||||||
@category,
|
@category,
|
||||||
@headquarter_address,
|
@headquarter_address,
|
||||||
@legal_name,
|
@legal_name,
|
||||||
@@ -216,6 +277,7 @@ INSERT INTO common_third_parties (
|
|||||||
args := pgx.StrictNamedArgs{
|
args := pgx.StrictNamedArgs{
|
||||||
"id": t.ID,
|
"id": t.ID,
|
||||||
"name": t.Name,
|
"name": t.Name,
|
||||||
|
"slug": t.Slug,
|
||||||
"category": t.Category,
|
"category": t.Category,
|
||||||
"headquarter_address": t.HeadquarterAddress,
|
"headquarter_address": t.HeadquarterAddress,
|
||||||
"legal_name": t.LegalName,
|
"legal_name": t.LegalName,
|
||||||
@@ -244,7 +306,7 @@ INSERT INTO common_third_parties (
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Upsert inserts a row, or on lower(name) conflict updates every column except
|
// Upsert inserts a row, or on slug conflict updates every column except
|
||||||
// id and created_at. Returns true if a new row was inserted, false if an
|
// id and created_at. Returns true if a new row was inserted, false if an
|
||||||
// existing row was updated.
|
// existing row was updated.
|
||||||
func (t CommonThirdParty) Upsert(
|
func (t CommonThirdParty) Upsert(
|
||||||
@@ -255,6 +317,7 @@ func (t CommonThirdParty) Upsert(
|
|||||||
INSERT INTO common_third_parties (
|
INSERT INTO common_third_parties (
|
||||||
id,
|
id,
|
||||||
name,
|
name,
|
||||||
|
slug,
|
||||||
category,
|
category,
|
||||||
headquarter_address,
|
headquarter_address,
|
||||||
legal_name,
|
legal_name,
|
||||||
@@ -276,6 +339,7 @@ INSERT INTO common_third_parties (
|
|||||||
) VALUES (
|
) VALUES (
|
||||||
@id,
|
@id,
|
||||||
@name,
|
@name,
|
||||||
|
@slug,
|
||||||
@category,
|
@category,
|
||||||
@headquarter_address,
|
@headquarter_address,
|
||||||
@legal_name,
|
@legal_name,
|
||||||
@@ -295,7 +359,7 @@ INSERT INTO common_third_parties (
|
|||||||
@created_at,
|
@created_at,
|
||||||
@updated_at
|
@updated_at
|
||||||
)
|
)
|
||||||
ON CONFLICT (lower(name)) DO UPDATE
|
ON CONFLICT (slug) DO UPDATE
|
||||||
SET
|
SET
|
||||||
name = EXCLUDED.name,
|
name = EXCLUDED.name,
|
||||||
category = EXCLUDED.category,
|
category = EXCLUDED.category,
|
||||||
@@ -320,6 +384,7 @@ RETURNING (xmax = 0) AS inserted
|
|||||||
args := pgx.StrictNamedArgs{
|
args := pgx.StrictNamedArgs{
|
||||||
"id": t.ID,
|
"id": t.ID,
|
||||||
"name": t.Name,
|
"name": t.Name,
|
||||||
|
"slug": t.Slug,
|
||||||
"category": t.Category,
|
"category": t.Category,
|
||||||
"headquarter_address": t.HeadquarterAddress,
|
"headquarter_address": t.HeadquarterAddress,
|
||||||
"legal_name": t.LegalName,
|
"legal_name": t.LegalName,
|
||||||
@@ -384,6 +449,7 @@ func (t *CommonThirdParties) LoadAll(
|
|||||||
SELECT
|
SELECT
|
||||||
id,
|
id,
|
||||||
name,
|
name,
|
||||||
|
slug,
|
||||||
category,
|
category,
|
||||||
headquarter_address,
|
headquarter_address,
|
||||||
legal_name,
|
legal_name,
|
||||||
|
|||||||
32
pkg/coredata/migrations/20260514T123727Z.sql
Normal file
32
pkg/coredata/migrations/20260514T123727Z.sql
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
-- 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.
|
||||||
|
|
||||||
|
ALTER TABLE common_third_parties ADD COLUMN slug TEXT;
|
||||||
|
|
||||||
|
UPDATE common_third_parties
|
||||||
|
SET slug = lower(
|
||||||
|
trim(BOTH '-' FROM
|
||||||
|
regexp_replace(
|
||||||
|
regexp_replace(
|
||||||
|
regexp_replace(name, '[^a-zA-Z0-9 _-]', '', 'g'),
|
||||||
|
'[ _]+', '-', 'g'),
|
||||||
|
'-+', '-', 'g')
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
ALTER TABLE common_third_parties ALTER COLUMN slug SET NOT NULL;
|
||||||
|
|
||||||
|
DROP INDEX common_third_parties_name_key;
|
||||||
|
|
||||||
|
CREATE UNIQUE INDEX common_third_parties_slug_key ON common_third_parties (slug);
|
||||||
Reference in New Issue
Block a user