/** * EMEX Türkçe Backfill * * `emex_category_translations` tablosu doldurulduktan SONRA çalıştır. * `categories` ve `parts` tablolarındaki source='emex' kayıtlarının * `name` kolonunu çevirilmiş hali ile günceller. `name_original` korunur * (rollback edilebilir). * * İlk çalıştırmada UPDATE'leri hızlandıran partial index'leri oluşturur. * Sonunda EMEX kategori tree'lerinin Redis cache'ini flush eder. * * Kullanım: * pnpm tsx scripts/emex-backfill-tr-names.ts --dry-run * pnpm tsx scripts/emex-backfill-tr-names.ts --target=categories * pnpm tsx scripts/emex-backfill-tr-names.ts --target=parts * pnpm tsx scripts/emex-backfill-tr-names.ts # all */ import * as path from "node:path"; import * as dotenv from "dotenv"; import postgres from "postgres"; import Redis from "ioredis"; dotenv.config({ path: path.join(__dirname, "../apps/api/.env") }); // ---------- CLI ---------- const argv = process.argv.slice(2); const dryRun = argv.includes("--dry-run"); const targetArg = argv.find((a) => a.startsWith("--target=")); const target = targetArg ? targetArg.split("=")[1] : "all"; const chunkArg = argv.find((a) => a.startsWith("--chunk-size=")); const chunkSize = chunkArg ? parseInt(chunkArg.split("=")[1], 10) : 50_000; if (!["all", "categories", "parts"].includes(target)) { console.error(`Invalid --target: ${target}. Use one of: all, categories, parts`); process.exit(1); } async function main() { if (!process.env.DATABASE_URL) { throw new Error("DATABASE_URL env var is required"); } const sql = postgres(process.env.DATABASE_URL, { max: 1 }); // Step 1: ensure partial indexes exist (idempotent) if (!dryRun) { console.log("Ensuring partial indexes..."); await sql` CREATE INDEX IF NOT EXISTS categories_name_original_emex_idx ON categories(name_original) WHERE source = 'emex' `; await sql` CREATE INDEX IF NOT EXISTS parts_name_original_emex_idx ON parts(name_original) WHERE source = 'emex' `; } // Step 2: report scope console.log("\nScope:"); const [{ count: trCount }] = (await sql` SELECT COUNT(*)::int AS count FROM emex_category_translations `) as unknown as { count: number }[]; console.log(` emex_category_translations rows: ${trCount}`); if (trCount === 0) { console.warn("\nWARNING: emex_category_translations is empty."); console.warn("Run scripts/emex-translate-bootstrap.ts first."); } if (target === "all" || target === "categories") { const [{ count: catUpd }] = (await sql` SELECT COUNT(*)::int AS count FROM categories c JOIN emex_category_translations t ON c.name_original = t.original_name WHERE c.source IN ('emex', 'parts-catalogs') AND c.name_original IS NOT NULL AND c.name <> t.translated_name `) as unknown as { count: number }[]; console.log(` categories to update: ${catUpd}`); } if (target === "all" || target === "parts") { const [{ count: partUpd }] = (await sql` SELECT COUNT(*)::int AS count FROM parts p JOIN emex_category_translations t ON p.name_original = t.original_name WHERE p.source IN ('emex', 'parts-catalogs') AND p.name_original IS NOT NULL AND p.name <> t.translated_name `) as unknown as { count: number }[]; console.log(` parts to update: ${partUpd}`); } if (dryRun) { console.log("\nDRY RUN — no UPDATE / Redis flush executed."); await sql.end(); return; } // Step 3: chunked UPDATE if (target === "all" || target === "categories") { console.log("\nUpdating categories..."); let totalUpdated = 0; while (true) { const result = await sql` WITH targets AS ( SELECT c.id, t.translated_name FROM categories c JOIN emex_category_translations t ON c.name_original = t.original_name WHERE c.source IN ('emex', 'parts-catalogs') AND c.name_original IS NOT NULL AND c.name <> t.translated_name LIMIT ${chunkSize} ) UPDATE categories SET name = targets.translated_name FROM targets WHERE categories.id = targets.id RETURNING categories.id `; const n = result.count ?? result.length; totalUpdated += n; console.log(` +${n} (total: ${totalUpdated})`); if (n === 0) break; } console.log(`Categories updated: ${totalUpdated}`); } if (target === "all" || target === "parts") { console.log("\nUpdating parts..."); let totalUpdated = 0; while (true) { const result = await sql` WITH targets AS ( SELECT p.id, t.translated_name FROM parts p JOIN emex_category_translations t ON p.name_original = t.original_name WHERE p.source IN ('emex', 'parts-catalogs') AND p.name_original IS NOT NULL AND p.name <> t.translated_name LIMIT ${chunkSize} ) UPDATE parts SET name = targets.translated_name FROM targets WHERE parts.id = targets.id RETURNING parts.id `; const n = result.count ?? result.length; totalUpdated += n; console.log(` +${n} (total: ${totalUpdated})`); if (n === 0) break; } console.log(`Parts updated: ${totalUpdated}`); } // Step 4: Redis cache flush — both per-vehicle category trees and translation entries console.log("\nFlushing Redis caches..."); const redis = new Redis({ host: process.env.REDIS_HOST || "127.0.0.1", port: parseInt(process.env.REDIS_PORT || "6379", 10), password: process.env.REDIS_PASSWORD, lazyConnect: true, }); try { await redis.connect(); const patterns = ["cat:tree:*", "tr:*"]; for (const pattern of patterns) { let cursor = "0"; let totalDeleted = 0; do { const [next, keys] = await redis.scan(cursor, "MATCH", pattern, "COUNT", 500); cursor = next; if (keys.length) { await redis.del(...keys); totalDeleted += keys.length; } } while (cursor !== "0"); console.log(` ${pattern}: ${totalDeleted} keys deleted`); } } catch (err) { console.warn(`Redis flush failed: ${(err as Error).message}`); } finally { redis.disconnect(); } await sql.end(); console.log("\nDone."); } main().catch((err) => { console.error(err); process.exit(1); });