Initial commit of Descrybe v2 without local scratch artifacts.
Drop one-shot tmp/axe scripts and agent i18n scratch so the Gitea tree is deployable.
This commit is contained in:
@@ -0,0 +1,165 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
// MigratorFixture is a minimal offline dump for dry-run without MySQL.
|
||||
// It is NOT a substitute for validating against production MySQL.
|
||||
type MigratorFixture struct {
|
||||
Companies []struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Language string `json:"language"`
|
||||
} `json:"companies"`
|
||||
Users []struct {
|
||||
ID string `json:"id"`
|
||||
Email string `json:"email"`
|
||||
Name string `json:"name"`
|
||||
Active bool `json:"active"`
|
||||
} `json:"users"`
|
||||
AdminUsers []struct {
|
||||
UserID string `json:"user_id"`
|
||||
Email string `json:"email"`
|
||||
} `json:"admin_users"`
|
||||
Profiles []struct {
|
||||
CompanyID string `json:"company_id"`
|
||||
UserID string `json:"user_id"`
|
||||
Role string `json:"role"`
|
||||
Status string `json:"status"`
|
||||
} `json:"profiles"`
|
||||
XMLFeeds []struct {
|
||||
ID int64 `json:"id"`
|
||||
CompanyID string `json:"company_id"`
|
||||
Name string `json:"name"`
|
||||
FieldMappings json.RawMessage `json:"field_mappings"`
|
||||
} `json:"xml_feeds"`
|
||||
Files []struct {
|
||||
ID int64 `json:"id"`
|
||||
CompanyID string `json:"company_id"`
|
||||
FileName string `json:"file_name"`
|
||||
} `json:"files"`
|
||||
RawProducts int `json:"raw_products_count"`
|
||||
}
|
||||
|
||||
func loadFixture(path string) (*MigratorFixture, error) {
|
||||
b, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var f MigratorFixture
|
||||
if err := json.Unmarshal(b, &f); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &f, nil
|
||||
}
|
||||
|
||||
// runFixtureDryRun remaps fixture rows and writes id-map + validation-style counts
|
||||
// without connecting to MySQL or Postgres.
|
||||
func runFixtureDryRun(fixturePath, mapsDir, idMapPath string) {
|
||||
fx, err := loadFixture(fixturePath)
|
||||
if err != nil {
|
||||
log.Fatalf("fixture: %v", err)
|
||||
}
|
||||
if err := os.MkdirAll(mapsDir, 0o755); err != nil {
|
||||
log.Fatalf("maps dir: %v", err)
|
||||
}
|
||||
outIDMap := idMapPath
|
||||
if outIDMap == "" {
|
||||
outIDMap = filepath.Join(mapsDir, "id-map.json")
|
||||
}
|
||||
|
||||
report := map[string]int{}
|
||||
companyMap := map[string]string{}
|
||||
userMap := map[string]string{}
|
||||
feedMap := map[string]string{}
|
||||
fileMap := map[string]string{}
|
||||
|
||||
for _, c := range fx.Companies {
|
||||
companyMap[c.ID] = uuid.New().String()
|
||||
report["companies"]++
|
||||
}
|
||||
for _, u := range fx.Users {
|
||||
userMap[u.ID] = uuid.New().String()
|
||||
report["users"]++
|
||||
}
|
||||
for _, a := range fx.AdminUsers {
|
||||
if _, ok := userMap[a.UserID]; ok {
|
||||
report["admin_users"]++
|
||||
} else {
|
||||
report["admin_users_unmatched"]++
|
||||
}
|
||||
}
|
||||
for _, p := range fx.Profiles {
|
||||
if _, okC := companyMap[p.CompanyID]; !okC {
|
||||
report["memberships_skipped"]++
|
||||
continue
|
||||
}
|
||||
if _, okU := userMap[p.UserID]; !okU {
|
||||
report["memberships_skipped"]++
|
||||
continue
|
||||
}
|
||||
report["memberships"]++
|
||||
}
|
||||
for _, f := range fx.XMLFeeds {
|
||||
if _, ok := companyMap[f.CompanyID]; !ok {
|
||||
report["input_feeds_skipped"]++
|
||||
continue
|
||||
}
|
||||
feedMap[fmt.Sprintf("%d", f.ID)] = uuid.New().String()
|
||||
report["input_feeds"]++
|
||||
if len(f.FieldMappings) > 0 && string(f.FieldMappings) != "null" && string(f.FieldMappings) != "{}" {
|
||||
report["feed_mappings"]++
|
||||
}
|
||||
}
|
||||
for _, f := range fx.Files {
|
||||
if _, ok := companyMap[f.CompanyID]; !ok {
|
||||
report["files_skipped"]++
|
||||
continue
|
||||
}
|
||||
fileMap[fmt.Sprintf("%d", f.ID)] = uuid.New().String()
|
||||
report["files"]++
|
||||
}
|
||||
report["raw_products"] = fx.RawProducts
|
||||
|
||||
idDoc := NewIDMapDocument(userMap, companyMap, true)
|
||||
idDoc.Source = "fixture"
|
||||
idDoc.AttachEntityMaps(nil, nil, feedMap, nil, fileMap, report)
|
||||
if err := WriteIDMap(outIDMap, idDoc); err != nil {
|
||||
log.Fatalf("id-map: %v", err)
|
||||
}
|
||||
writeEntityMapFiles(mapsDir, companyMap, userMap, nil, nil, feedMap, nil, fileMap)
|
||||
|
||||
counts := []CountPair{
|
||||
{Entity: "companies", MySQL: int64(len(fx.Companies)), Note: "fixture"},
|
||||
{Entity: "users", MySQL: int64(len(fx.Users)), Note: "fixture; password_hash never imported"},
|
||||
{Entity: "admin_users", MySQL: int64(len(fx.AdminUsers)), Note: "→ is_platform_admin"},
|
||||
{Entity: "profiles", MySQL: int64(len(fx.Profiles)), Note: "→ memberships"},
|
||||
{Entity: "xml_feeds", MySQL: int64(len(fx.XMLFeeds)), Note: "→ input_feeds + feed_mappings"},
|
||||
{Entity: "files", MySQL: int64(len(fx.Files)), Note: "metadata only"},
|
||||
{Entity: "raw_products", MySQL: int64(fx.RawProducts), Note: "count-only in fixture"},
|
||||
}
|
||||
v := ValidationReport{
|
||||
Mode: "fixture-dry-run",
|
||||
Counts: counts,
|
||||
Orphans: []OrphanFinding{{
|
||||
Check: "skipped_fixture",
|
||||
Pass: true,
|
||||
Sample: "orphan checks need live Postgres after a real load",
|
||||
}},
|
||||
OK: true,
|
||||
}
|
||||
v.OrphanSummary = summarizeOrphans(v.Orphans)
|
||||
printValidation(v)
|
||||
_ = writeJSON(filepath.Join(mapsDir, "validation-report.json"), v)
|
||||
|
||||
printMigrationReport(report, true)
|
||||
fmt.Printf("wrote maps under %s (unified: %s)\n", mapsDir, outIDMap)
|
||||
fmt.Println("BLOCKER: fixture mode is not a substitute for dry-run against production MySQL — set MIGRATE_MYSQL_DSN and re-run before cutover.")
|
||||
}
|
||||
Reference in New Issue
Block a user