← 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); });