← back to Letsbegin

add_ai_desc_thibaut.js

110 lines

const https = require('https');
const fs = require('fs');

const STORE = 'designer-laboratory-sandbox.myshopify.com';
const TOKEN = process.env.SHOPIFY_PRODUCT_TOKEN;
if (!TOKEN) {
  throw new Error('SHOPIFY_PRODUCT_TOKEN environment variable is required');
}

const aiData = JSON.parse(fs.readFileSync('/tmp/thibaut_ai_desc.json', 'utf8'));
const aiByMfr = {};
for (const d of aiData) {
  if (d.mfr_sku && d.ai_description) aiByMfr[d.mfr_sku.trim()] = d.ai_description.trim();
}
console.log(`Loaded ${Object.keys(aiByMfr).length} AI descriptions\n`);

function gql(body) {
  return new Promise((resolve, reject) => {
    const data = JSON.stringify(body);
    const req = https.request({
      hostname: STORE, path: '/admin/api/2024-10/graphql.json', method: 'POST',
      headers: { 'X-Shopify-Access-Token': TOKEN, 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(data) },
    }, res => { let c = ''; res.on('data', d => c += d); res.on('end', () => { try { resolve(JSON.parse(c)); } catch { resolve({ error: c.slice(0,300) }); } }); });
    req.on('error', reject);
    req.setTimeout(30000, () => { req.destroy(); reject(new Error('timeout')); });
    req.write(data); req.end();
  });
}
function sleep(ms) { return new Promise(r => setTimeout(r, ms)); }

function buildTwoParaBody(existingBody, aiDesc, title) {
  // Extract first meaningful paragraph text from existing body
  let firstPara = '';
  const pMatch = (existingBody || '').match(/<p[^>]*>(.*?)<\/p>/s);
  if (pMatch) {
    firstPara = pMatch[1].replace(/<[^>]+>/g, '').trim();
  } else {
    firstPara = (existingBody || '').replace(/<[^>]+>/g, '').trim();
  }

  // If first paragraph is empty/junk, generate a clean one
  if (!firstPara || firstPara.length < 20 || /^specifications$/i.test(firstPara)) {
    const cleanTitle = (title || '').split('|')[0].trim();
    firstPara = `${cleanTitle} is a premium wallcovering by Thibaut. This design brings refined texture and sophistication to both residential and commercial interiors.`;
  }

  // Clean AI desc
  let aiClean = aiDesc.charAt(0).toUpperCase() + aiDesc.slice(1);
  aiClean = aiClean.replace(/\bwallpaper\b/gi, 'wallcovering');

  // EXACTLY 2 paragraphs
  return `<p>${firstPara}</p>\n<p>${aiClean}</p>`;
}

async function main() {
  console.log('=== Thibaut Body Rewriter: Exactly 2 Paragraphs ===\n');
  let cursor = null, page = 1, updated = 0, noMatch = 0, total = 0, errors = 0;

  while (true) {
    const after = cursor ? `, after: "${cursor}"` : '';
    const r = await gql({
      query: `{ products(first: 25, query: "vendor:Thibaut AND status:active AND product_type:Wallcovering"${after}) {
        edges { cursor node { id title bodyHtml metafields(first: 2, keys: ["custom.manufacturer_sku"]) { edges { node { value } } } } }
        pageInfo { hasNextPage }
      } }`
    });

    const edges = r?.data?.products?.edges || [];
    if (edges.length === 0) break;

    const mutations = [];

    for (const edge of edges) {
      const p = edge.node;
      cursor = edge.cursor;
      total++;

      const mfrSku = p.metafields?.edges?.[0]?.node?.value || '';
      const aiDesc = aiByMfr[mfrSku];
      if (!aiDesc) { noMatch++; continue; }

      const newBody = buildTwoParaBody(p.bodyHtml, aiDesc, p.title);
      mutations.push({ id: p.id, body: newBody });
    }

    for (const m of mutations) {
      try {
        const result = await gql({
          query: 'mutation ($i: ProductInput!) { productUpdate(input: $i) { product { id } userErrors { message } } }',
          variables: { i: { id: m.id, bodyHtml: m.body } }
        });
        const errs = result?.data?.productUpdate?.userErrors || [];
        if (errs.length === 0) updated++;
        else { errors++; if (errors <= 3) console.log(`  ERR: ${errs[0].message}`); }
      } catch { errors++; }
      await sleep(500);
    }

    if (page % 10 === 0 || !r?.data?.products?.pageInfo?.hasNextPage) {
      console.log(`Page ${page}: total=${total} updated=${updated} noMatch=${noMatch} errors=${errors}`);
    }

    if (!r?.data?.products?.pageInfo?.hasNextPage) break;
    page++; await sleep(200);
  }

  console.log(`\n=== DONE === Updated: ${updated} | No AI match: ${noMatch} | Errors: ${errors}`);
}
main().catch(e => { console.error('Fatal:', e); process.exit(1); });