← back to Designerwallcoverings

scripts/artmura-onboard/update-live-titles.js

59 lines

#!/usr/bin/env node
/**
 * Update the 161 live Artmura draft titles → "<name> Wallcovering | Artmura".
 * PUT merges only the title field. Idempotent (skips titles already correct).
 *   node update-live-titles.js            # dry-run (first 3)
 *   node update-live-titles.js --apply
 */
const https = require('https');
const fs = require('fs');
const path = require('path');
const { Pool } = require('pg');

const STORE = process.env.SHOPIFY_STORE || 'designer-laboratory-sandbox.myshopify.com';
const TOKEN = process.env.SHOPIFY_ADMIN_TOKEN || process.env.SHOPIFY_PRODUCT_TOKEN;
const API = process.env.SHOPIFY_API_VERSION || '2024-10';
const DB = process.env.DATABASE_URL || 'postgresql:///dw_unified?host=/tmp';
const APPLY = process.argv.includes('--apply');
const PKG = path.join(__dirname, 'data', 'artmura.json');
if (!TOKEN) { console.error('FATAL: set SHOPIFY_ADMIN_TOKEN'); process.exit(1); }
const pool = new Pool({ connectionString: DB });
const sleep = ms => new Promise(r => setTimeout(r, ms));

function shopify(method, p, body) {
  return new Promise((resolve, reject) => {
    const data = body ? JSON.stringify(body) : null;
    const req = https.request({ hostname: STORE, path: `/admin/api/${API}${p}`, method,
      headers: { 'X-Shopify-Access-Token': TOKEN, 'Content-Type': 'application/json',
                 ...(data ? { 'Content-Length': Buffer.byteLength(data) } : {}) } },
      res => { let b = ''; res.on('data', c => b += c); res.on('end', () => { let j; try { j = JSON.parse(b); } catch { j = b; }
        resolve({ status: res.statusCode, body: j, callLimit: res.headers['x-shopify-shop-api-call-limit'] }); }); });
    req.on('error', reject); req.setTimeout(20000, () => req.destroy(new Error('timeout')));
    if (data) req.write(data); req.end();
  });
}
const newTitle = r => `${r.title} Wallcovering | Artmura`;

(async () => {
  const pkg = JSON.parse(fs.readFileSync(PKG, 'utf8')).products;
  const { rows } = await pool.query(
    `SELECT handle, shopify_product_id FROM newwall_catalog WHERE vendor_name='Artmura' AND shopify_product_id IS NOT NULL AND shopify_product_id<>''`);
  const pid = Object.fromEntries(rows.map(r => [r.handle, r.shopify_product_id]));
  const todo = pkg.filter(p => pid[p.handle]);
  console.log(`title update: ${todo.length} products · mode: ${APPLY ? 'APPLY' : 'DRY-RUN'}`);
  if (!APPLY) { todo.slice(0, 3).forEach(r => console.log(`  ${pid[r.handle]} → "${newTitle(r)}"`)); console.log('\nDRY-RUN. --apply to write.'); await pool.end(); return; }
  let ok = 0, fail = 0;
  for (const r of todo) {
    const id = pid[r.handle];
    try {
      const res = await shopify('PUT', `/products/${id}.json`, { product: { id: Number(id), title: newTitle(r) } });
      if (res.status === 200) { ok++; if (ok % 40 === 0) console.log(`  ...${ok}`); }
      else { fail++; console.error(`  FAIL ${id}: HTTP ${res.status} ${JSON.stringify(res.body).slice(0, 140)}`); }
      const [used] = (res.callLimit || '0/40').split('/').map(Number);
      await sleep(used > 30 ? 1500 : 540);
    } catch (e) { fail++; console.error(`  ERR ${id}: ${e.message}`); await sleep(540); }
  }
  console.log(`\nDONE. titles updated=${ok} failed=${fail}`);
  await pool.end();
})().catch(e => { console.error(e); process.exit(1); });