Drop one-shot tmp/axe scripts and agent i18n scratch so the Gitea tree is deployable.
182 lines
4.8 KiB
Go
182 lines
4.8 KiB
Go
package main
|
|
|
|
import (
|
|
"fmt"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// MigratorConfig holds portable CLI options for MySQL → Postgres ETL.
|
|
type MigratorConfig struct {
|
|
MySQLDSN string
|
|
PostgresURL string
|
|
DryRun bool
|
|
MapsDir string
|
|
IDMapPath string
|
|
ReportDir string
|
|
FixturePath string
|
|
Resume bool
|
|
CompanyFilter []string // legacy company ids; empty = all
|
|
Domains domainSet
|
|
SkipPostImport bool
|
|
EnsureDemo bool
|
|
DemoEmail string
|
|
DemoPassword string
|
|
DemoName string
|
|
LocalDemoCo string
|
|
}
|
|
|
|
type domainSet map[string]bool
|
|
|
|
func parseDomains(raw string) domainSet {
|
|
raw = strings.TrimSpace(strings.ToLower(raw))
|
|
if raw == "" || raw == "all" {
|
|
return domainSet{"all": true}
|
|
}
|
|
out := domainSet{}
|
|
for _, p := range strings.Split(raw, ",") {
|
|
p = strings.TrimSpace(p)
|
|
if p == "" {
|
|
continue
|
|
}
|
|
out[p] = true
|
|
}
|
|
if len(out) == 0 {
|
|
return domainSet{"all": true}
|
|
}
|
|
return out
|
|
}
|
|
|
|
func (d domainSet) has(name string) bool {
|
|
if d == nil || d["all"] {
|
|
return true
|
|
}
|
|
return d[name]
|
|
}
|
|
|
|
func (d domainSet) String() string {
|
|
if d == nil || d["all"] {
|
|
return "all"
|
|
}
|
|
parts := make([]string, 0, len(d))
|
|
for k := range d {
|
|
parts = append(parts, k)
|
|
}
|
|
return strings.Join(parts, ",")
|
|
}
|
|
|
|
func parseCompanyFilter(raw string) []string {
|
|
raw = strings.TrimSpace(raw)
|
|
if raw == "" {
|
|
return nil
|
|
}
|
|
var out []string
|
|
seen := map[string]bool{}
|
|
for _, p := range strings.Split(raw, ",") {
|
|
p = strings.TrimSpace(p)
|
|
if p == "" || seen[p] {
|
|
continue
|
|
}
|
|
seen[p] = true
|
|
out = append(out, p)
|
|
}
|
|
return out
|
|
}
|
|
|
|
func companyFilterSet(ids []string) map[string]bool {
|
|
if len(ids) == 0 {
|
|
return nil
|
|
}
|
|
m := make(map[string]bool, len(ids))
|
|
for _, id := range ids {
|
|
m[id] = true
|
|
}
|
|
return m
|
|
}
|
|
|
|
func filterCompanies(rows []companyRow, allow map[string]bool) []companyRow {
|
|
if allow == nil {
|
|
return rows
|
|
}
|
|
out := make([]companyRow, 0, len(rows))
|
|
for _, c := range rows {
|
|
if allow[c.LegacyID] {
|
|
out = append(out, c)
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
// mysqlCompanyFilter appends AND company_id IN (...) when a filter is set.
|
|
// Values are bound as ? placeholders; the column path is validated and quoted.
|
|
func mysqlCompanyFilter(column string, allow map[string]bool) (clause string, args []any) {
|
|
if len(allow) == 0 {
|
|
return "", nil
|
|
}
|
|
quotedCol, err := quoteMySQLIdentPath(column)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
ids := make([]string, 0, len(allow))
|
|
for id := range allow {
|
|
ids = append(ids, id)
|
|
}
|
|
placeholders := make([]string, len(ids))
|
|
args = make([]any, len(ids))
|
|
for i, id := range ids {
|
|
placeholders[i] = "?"
|
|
args[i] = id
|
|
}
|
|
return fmt.Sprintf(" AND %s IN (%s)", quotedCol, strings.Join(placeholders, ",")), args
|
|
}
|
|
|
|
// MigrationRunReport is the portable JSON artifact written after each run.
|
|
type MigrationRunReport struct {
|
|
GeneratedAt string `json:"generated_at"`
|
|
Mode string `json:"mode"`
|
|
Domains string `json:"domains"`
|
|
CompanyFilter []string `json:"company_filter,omitempty"`
|
|
Resume bool `json:"resume"`
|
|
Counts map[string]int `json:"counts"`
|
|
Validation any `json:"validation,omitempty"`
|
|
Demo *DemoReport `json:"demo,omitempty"`
|
|
Notes []string `json:"notes,omitempty"`
|
|
ElapsedMS int64 `json:"elapsed_ms"`
|
|
}
|
|
|
|
// DemoReport documents the ensure-demo outcome (no password plaintext).
|
|
type DemoReport struct {
|
|
Email string `json:"email"`
|
|
PasswordSet bool `json:"password_set"`
|
|
UserID string `json:"user_id,omitempty"`
|
|
PrimaryCompany string `json:"primary_company,omitempty"`
|
|
PrimaryName string `json:"primary_company_name,omitempty"`
|
|
Memberships int64 `json:"memberships_admin"`
|
|
PlatformAdmin bool `json:"platform_admin"`
|
|
Note string `json:"note,omitempty"`
|
|
}
|
|
|
|
func newRunReport(cfg MigratorConfig, dryRun bool) *MigrationRunReport {
|
|
mode := "live"
|
|
if dryRun {
|
|
mode = "dry-run"
|
|
}
|
|
return &MigrationRunReport{
|
|
GeneratedAt: time.Now().UTC().Format(time.RFC3339),
|
|
Mode: mode,
|
|
Domains: cfg.Domains.String(),
|
|
CompanyFilter: append([]string(nil), cfg.CompanyFilter...),
|
|
Resume: cfg.Resume,
|
|
Counts: map[string]int{},
|
|
Notes: []string{
|
|
"Clerk is excluded: users mapped by email only; no Clerk API.",
|
|
"Legacy password hashes are never imported.",
|
|
"API key secrets are not migrated; clients must mint new keys (seed-demo / ensure-demo for local).",
|
|
"File blobs are metadata-only; resync object storage separately.",
|
|
"Job history: domain jobs migrates processing_jobs (+ best-effort job_products) and tasks; tagged ai_provider_mode=migrated so retention keeps them.",
|
|
"company_settings: language + merge_products only (domain settings); other legacy settings fields are not imported.",
|
|
"woocommerce_configs: migrated from wc_* custom_fields when domain woo is enabled.",
|
|
},
|
|
}
|
|
}
|