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