package main import ( "encoding/json" "fmt" "os" "path/filepath" "time" "github.com/google/uuid" ) // IDMapDocument matches docs/schema-map.md (versioned unified ID map for cutover rehearsal). type IDMapDocument struct { Version int `json:"version"` GeneratedAt string `json:"generated_at"` Source string `json:"source"` Target string `json:"target"` Users map[string]string `json:"users"` Companies map[string]string `json:"companies"` Categories map[string]string `json:"categories,omitempty"` Attributes map[string]string `json:"attributes,omitempty"` Feeds map[string]string `json:"feeds,omitempty"` RawProducts map[string]string `json:"raw_products,omitempty"` Files map[string]string `json:"files,omitempty"` Meta IDMapMeta `json:"meta"` } type IDMapMeta struct { UserCount int `json:"user_count"` CompanyCount int `json:"company_count"` DryRun bool `json:"dry_run"` Report map[string]int `json:"report,omitempty"` } func copyMap(dst *map[string]string, src map[string]string) { if src == nil { return } if *dst == nil { *dst = map[string]string{} } for k, v := range src { (*dst)[k] = v } } // AttachEntityMaps merges optional catalog/feed entity remaps into the document. func (d *IDMapDocument) AttachEntityMaps( categories, attributes, feeds, rawProducts, files map[string]string, report map[string]int, ) { if d == nil { return } copyMap(&d.Categories, categories) copyMap(&d.Attributes, attributes) copyMap(&d.Feeds, feeds) copyMap(&d.RawProducts, rawProducts) copyMap(&d.Files, files) d.Meta.UserCount = len(d.Users) d.Meta.CompanyCount = len(d.Companies) if report != nil { d.Meta.Report = report } } func NewIDMapDocument(users, companies map[string]string, dryRun bool) IDMapDocument { if users == nil { users = map[string]string{} } if companies == nil { companies = map[string]string{} } return IDMapDocument{ Version: 1, GeneratedAt: time.Now().UTC().Format(time.RFC3339), Source: "mysql", Target: "postgres", Users: users, Companies: companies, Meta: IDMapMeta{ UserCount: len(users), CompanyCount: len(companies), DryRun: dryRun, }, } } func (d IDMapDocument) Validate() error { if d.Version != 1 { return fmt.Errorf("unsupported id map version %d", d.Version) } for legacy, id := range d.Users { if legacy == "" { return fmt.Errorf("empty user legacy id") } if _, err := uuid.Parse(id); err != nil { return fmt.Errorf("user %q maps to invalid uuid %q", legacy, id) } } for legacy, id := range d.Companies { if legacy == "" { return fmt.Errorf("empty company legacy id") } if _, err := uuid.Parse(id); err != nil { return fmt.Errorf("company %q maps to invalid uuid %q", legacy, id) } } return nil } func (d IDMapDocument) ResolveUser(legacy string) (uuid.UUID, bool) { raw, ok := d.Users[legacy] if !ok { return uuid.Nil, false } id, err := uuid.Parse(raw) if err != nil { return uuid.Nil, false } return id, true } func (d IDMapDocument) ResolveCompany(legacy string) (uuid.UUID, bool) { raw, ok := d.Companies[legacy] if !ok { return uuid.Nil, false } id, err := uuid.Parse(raw) if err != nil { return uuid.Nil, false } return id, true } func WriteIDMap(path string, d IDMapDocument) error { if err := d.Validate(); err != nil { return err } b, err := json.MarshalIndent(d, "", " ") if err != nil { return err } return os.WriteFile(path, b, 0o644) } func ReadIDMap(path string) (IDMapDocument, error) { b, err := os.ReadFile(path) if err != nil { return IDMapDocument{}, err } var d IDMapDocument if err := json.Unmarshal(b, &d); err != nil { return IDMapDocument{}, err } if err := d.Validate(); err != nil { return IDMapDocument{}, err } return d, nil } func WriteIDMapDir(dir string, users, companies map[string]string, dryRun bool) (string, error) { if err := os.MkdirAll(dir, 0o755); err != nil { return "", err } path := filepath.Join(dir, "id-map.json") doc := NewIDMapDocument(users, companies, dryRun) if err := WriteIDMap(path, doc); err != nil { return "", err } return path, nil }