#!/usr/bin/env node /** * seed-legacy-categories — F-114 * * Imports the legacy OpenCart `oc_category_description` rows into the * current catalog and brand tables. Reads the legacy list from the * pure helper at `src/modules/categories/legacy/legacy-catalog` so the * mapping stays in sync with the tests. * * Idempotent: skips entries whose name (case-insensitive) already exists in * the target table, and uniquifies slugs against existing ones. * * Usage: * npm run seed:legacy-categories # apply * DATABASE_URL=... node scripts/seed-legacy-categories.mjs --dry-run */ import pg from 'pg'; import { buildImportPlan, buildRenamePlan } from '../dist/modules/categories/legacy/legacy-catalog.js'; const DATABASE_URL = process.env.DATABASE_URL; if (!DATABASE_URL) { console.error('DATABASE_URL is required'); process.exit(2); } const args = new Set(process.argv.slice(2)); const dryRun = args.has('--dry-run'); const pool = new pg.Pool({ connectionString: DATABASE_URL, max: 1 }); try { const categories = await pool.query('SELECT id, name, slug FROM categories_categories'); const brands = await pool.query('SELECT id, name, slug FROM brands_brands'); const categoryNameIndex = new Set(categories.rows.map((r) => r.name.toLowerCase())); const brandNameIndex = new Set(brands.rows.map((r) => r.name.toLowerCase())); const slugIndex = new Set( [...categories.rows, ...brands.rows].map((r) => r.slug.toLowerCase()), ); const parentBySlug = new Map(categories.rows.map((r) => [r.slug, r.id])); const plan = buildImportPlan(categoryNameIndex, brandNameIndex, slugIndex); const renamePlan = buildRenamePlan(categoryNameIndex); // Validate that every parentSlug resolves to an existing category. const missingParents = new Set(); for (const cat of plan.categories) { if (cat.parentSlug && !parentBySlug.has(cat.parentSlug)) missingParents.add(cat.parentSlug); } if (missingParents.size > 0) { console.error(`Missing parent categories: ${[...missingParents].join(', ')}`); process.exit(1); } console.log(`Planned: ${plan.categories.length} categories, ${plan.brands.length} brands`); console.log(`Planned renames: ${renamePlan.length}`); if (dryRun) { console.log('DRY RUN — nothing will be written.'); for (const cat of plan.categories) { console.log(` + category ${cat.name} (slug=${cat.slug}, parent=${cat.parentSlug ?? 'ROOT'})`); } for (const brand of plan.brands) { console.log(` + brand ${brand.name} (slug=${brand.slug})`); } for (const rename of renamePlan) { console.log(` ~ rename ${rename.oldName} → ${rename.newName}`); } process.exit(0); } for (const cat of plan.categories) { const parentId = cat.parentSlug ? parentBySlug.get(cat.parentSlug) : null; await pool.query( `INSERT INTO categories_categories (parent_id, name, slug, is_parent) VALUES ($1, $2, $3, false) ON CONFLICT (slug) DO NOTHING`, [parentId, cat.name, cat.slug], ); console.log(` + category ${cat.name} (slug=${cat.slug}, parent=${cat.parentSlug ?? 'ROOT'})`); } for (const brand of plan.brands) { await pool.query( `INSERT INTO brands_brands (name, slug) VALUES ($1, $2) ON CONFLICT (slug) DO NOTHING`, [brand.name, brand.slug], ); console.log(` + brand ${brand.name} (slug=${brand.slug})`); } for (const rename of renamePlan) { const result = await pool.query( `UPDATE categories_categories SET name = $1, updated_at = now() WHERE name = $2`, [rename.newName, rename.oldName], ); if (result.rowCount > 0) { console.log(` ~ rename ${rename.oldName} → ${rename.newName}`); } } console.log(`\nDone. Inserted ${plan.categories.length} categories and ${plan.brands.length} brands. Renamed ${renamePlan.length} categories.`); } catch (error) { console.error('Error:', error.message); process.exit(1); } finally { await pool.end(); }