← back to Tk10630 Sku Suffix Canary

apply-versa-alt.mjs

37 lines

// Fix image ALT text leaking "Versa" — the CORRECT way: productUpdateMedia on
// MediaImage ids (fileUpdate rejects ProductImage ids). Iterates the versa product
// IDs, fetches current media alt, rewrites 'Versa Designed Surfaces' -> 'Hollywood
// Wallcoverings'. DRY by default; --apply writes. Resumable via done-versa-alt.jsonl.
import { gql } from './shopify.mjs';
import { readFileSync, appendFileSync, existsSync } from 'node:fs';
const APPLY = process.argv.includes('--apply');
const DONE = 'done-versa-alt.jsonl';
const plan = JSON.parse(readFileSync('versa-fix-plan.json', 'utf8'));
const done = new Set();
if (existsSync(DONE)) for (const l of readFileSync(DONE, 'utf8').split('\n')) if (l.trim()) done.add(JSON.parse(l).id);
const work = plan.filter(p => !done.has(p.id));
console.log(`[versa-alt] mode=${APPLY ? 'LIVE' : 'DRY'} products=${plan.length} todo=${work.length}`);
const sleep = ms => new Promise(r => setTimeout(r, ms));
async function mut(q, v) { for (let a = 0; ; a++) { try { const { data } = await gql(q, v); return data; } catch (e) { if (/THROTTLED/i.test(e.message) && a < 8) { await sleep(1500 * (a + 1)); continue; } throw e; } } }
const MEDIA = `query($id:ID!){ product(id:$id){ media(first:20){ nodes{ ... on MediaImage { id alt } } } } }`;
const UPD = `mutation($pid:ID!,$media:[UpdateMediaInput!]!){ productUpdateMedia(productId:$pid, media:$media){ mediaUserErrors{ field message } } }`;
let idx = 0, ok = 0, err = 0, fixed = 0;
async function worker() {
  while (idx < work.length) {
    const p = work[idx++];
    try {
      const d = await mut(MEDIA, { id: p.id });
      const media = (d.product?.media?.nodes || []).filter(m => m && m.id);
      const changes = media.filter(m => /versa/i.test(m.alt || '')).map(m => ({ id: m.id, alt: m.alt.replace(/versa\s*designed\s*surfaces/ig, 'Hollywood Wallcoverings').replace(/\|\s*versa[^|]*/ig, '| Hollywood Wallcoverings') }));
      if (changes.length && APPLY) {
        const r = await mut(UPD, { pid: p.id, media: changes });
        if (r.productUpdateMedia.mediaUserErrors.length) { err++; console.log('✗', p.id, JSON.stringify(r.productUpdateMedia.mediaUserErrors)); }
        else { ok++; fixed += changes.length; appendFileSync(DONE, JSON.stringify({ id: p.id, n: changes.length }) + '\n'); }
      } else { ok++; if (APPLY) appendFileSync(DONE, JSON.stringify({ id: p.id, n: 0 }) + '\n'); }
    } catch (e) { err++; console.log('✗', p.id, e.message.slice(0, 90)); }
    if ((ok + err) % 100 === 0) process.stderr.write(`  ${ok + err}/${work.length} (alt fixed=${fixed})\n`);
  }
}
await Promise.all(Array.from({ length: 4 }, () => worker()));
console.log(`[versa-alt] DONE ok=${ok} err=${err} alt_images_fixed=${fixed} ${APPLY ? '(written)' : '(dry)'}`);