← back to Dwjs Consolidation 2026 04 23

fix_bodies.js

95 lines

#!/usr/bin/env node
/**
 * Generate 3-sentence body HTML for products missing body text.
 * Synthesized from existing metadata — no AI needed.
 *
 * Format per CLAUDE.md rule:
 *   Sentence 1: what it is (pattern/color/vendor)
 *   Sentence 2: style/mood
 *   Sentence 3: recommended use (residential / commercial)
 *
 * NO specs, NO width, NO material, NO fire rating.
 */
const https = require('https');
const fs = require('fs');

const STORE='designer-laboratory-sandbox.myshopify.com';
const TOKEN=(process.env.SHOPIFY_ADMIN_TOKEN || '');
const LOG = '/Users/macstudio3/Projects/dwjs-consolidation-2026-04-23/fix_bodies.log';
const log = m => { const l=`[${new Date().toISOString().slice(0,19)}] ${m}\n`; process.stdout.write(l); fs.appendFileSync(LOG,l); };

function gql(body, retry=0) {
  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 { if (retry<3) setTimeout(()=>resolve(gql(body,retry+1)),2000); else resolve({error:c}); } });}
    );
    req.on('error', err => { if (retry<3) setTimeout(()=>resolve(gql(body,retry+1)), 2000); else reject(err); });
    req.setTimeout(30000, ()=>{ req.destroy(); if (retry<3) resolve(gql(body,retry+1)); else reject(new Error('t')); });
    req.write(data); req.end();
  });
}

// Load issue list
const csv = fs.readFileSync('/Users/macstudio3/Projects/dwjs-consolidation-2026-04-23/completeness_issues.csv','utf8').split('\n').filter(Boolean);
const rows = [];
const header = csv[0].split(',');
for (const l of csv.slice(1)) {
  // simple split (fields don't contain commas in this context)
  const vals = l.split(',');
  const r = {};
  header.forEach((h,i) => r[h] = vals[i] || '');
  rows.push(r);
}
const missingBody = rows.filter(r => r.flags.includes('NO_BODY')).map(r => ({ id: r.id, title: r.title.replace(/^"/,'').replace(/"$/,'') }));
log(`products missing body: ${missingBody.length}`);

function generateBody(title, meta) {
  const patternName = meta.pattern_name || title.split('|')[0].split('Wallcovering')[0].split('Wallpaper')[0].trim();
  const vendor = title.split('|')[1]?.trim() || 'Jeffrey Stevens';
  const color = meta.color || '';
  const style = (meta.style || '').toLowerCase();
  const pattern = (meta.pattern_mf || '').toLowerCase();

  const s1 = `${patternName}${color ? ` in ${color}` : ''} is a wallcovering from ${vendor}.`;
  const styleMood = style ? `The ${style} design` : 'The design';
  const patternDetail = pattern ? ` features ${pattern} patterning` : '';
  const s2 = `${styleMood}${patternDetail} brings a considered texture and tone to the wall.`;
  const s3 = 'Suitable for residential and commercial interiors — free samples and trade service available through Designer Wallcoverings.';
  return `<p>${s1} ${s2} ${s3}</p>`;
}

(async () => {
  let ok=0, fail=0;
  for (const m of missingBody) {
    // fetch its metadata
    const q = `{ product(id:"${m.id}") {
      title
      pattern_name: metafield(namespace:"custom",key:"pattern_name") { value }
      color: metafield(namespace:"custom",key:"color") { value }
      style: metafield(namespace:"specs",key:"style") { value }
      pattern_mf: metafield(namespace:"specs",key:"pattern") { value }
    } }`;
    const r = await gql({ query: q });
    const p = r?.data?.product;
    if (!p) { fail++; log(`fail fetch ${m.id}`); continue; }
    const meta = {
      pattern_name: p.pattern_name?.value || '',
      color: p.color?.value || '',
      style: p.style?.value || '',
      pattern_mf: p.pattern_mf?.value || '',
    };
    const body = generateBody(p.title, meta);
    // Write via productUpdate
    const upd = await gql({
      query: `mutation($input: ProductInput!) { productUpdate(input: $input) { product { id } userErrors { field message } } }`,
      variables: { input: { id: m.id, descriptionHtml: body } },
    });
    const errs = upd?.data?.productUpdate?.userErrors || [];
    if (errs.length) { fail++; log(`fail ${m.id}: ${errs[0].message}`); continue; }
    ok++;
  }
  log(`DONE body-gen: ok=${ok} fail=${fail}`);
})().catch(e => { log('FATAL ' + e.message); process.exit(1); });