Some checks failed
Sync dev → Gitea / Mirror dev to Gitea (push) Has been cancelled
- Added a comment line to main.ts for deployment verification purposes
195 lines
6.3 KiB
TypeScript
195 lines
6.3 KiB
TypeScript
/**
|
||
* 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);
|
||
});
|