← back to Majilite Onboard

scripts/reconcile.js

43 lines

#!/usr/bin/env node
/**
 * Reconcile the live store against products.json using productByHandle (strongly
 * consistent, unlike variant-sku search which lags). For each of the 160:
 *  - already recorded in published.json -> OK
 *  - handle resolves to OUR product (vendor Majilite, DWMJ sample sku) -> record it
 *  - handle resolves to a DIFFERENT product -> report collision (needs unique handle)
 *  - handle missing -> report as genuinely absent (to be (re)published)
 * Read-only: only records confirmed-ours into published.json; never creates/deletes.
 */
const fs = require('fs'); const path = require('path');
const ROOT = path.resolve(__dirname, '..');
const API = '2024-10'; const STORE = process.env.SHOPIFY_STORE || 'designer-laboratory-sandbox.myshopify.com';
const TOKEN = (fs.readFileSync(path.join(process.env.HOME,'Projects/secrets-manager/.env'),'utf8').match(/^SHOPIFY_ADMIN_TOKEN=(.+)$/m)||[])[1].replace(/["']/g,'').trim();
const GQL = `https://${STORE}/admin/api/${API}/graphql.json`;
const products = JSON.parse(fs.readFileSync(path.join(ROOT,'data/products.json'),'utf8'));
const PUB = path.join(ROOT,'data/published.json');
const pub = fs.existsSync(PUB) ? JSON.parse(fs.readFileSync(PUB,'utf8')) : {};
const slug = s => s.toLowerCase().replace(/[^a-z0-9]+/g,'-').replace(/^-|-$/g,'');
const sleep = ms => new Promise(r=>setTimeout(r,ms));
async function gql(q,v){ for(let a=0;a<5;a++){ const r=await fetch(GQL,{method:'POST',headers:{'X-Shopify-Access-Token':TOKEN,'Content-Type':'application/json'},body:JSON.stringify({query:q,variables:v})}); const j=await r.json(); if(j.errors){ if(JSON.stringify(j.errors).includes('THROTTLED')){await sleep(1500*(a+1));continue;} throw new Error(JSON.stringify(j.errors).slice(0,200)); } return j.data; } throw new Error('throttled'); }

(async()=>{
  let ok=0, recorded=0, collided=[], missing=[], noimg=[];
  for(const p of products){
    if(pub[p.dw_sku] && pub[p.dw_sku].productId){ ok++; continue; }
    const handle = `majilite-${slug(p.pattern+'-'+p.color)}`;
    const d = await gql(`query($h:String!){ productByHandle(handle:$h){ id handle status vendor variants(first:1){nodes{sku price}} featuredMedia{id} } }`, {h:handle});
    const prod = d.productByHandle;
    if(!prod){ missing.push(p.dw_sku); continue; }
    const v = prod.variants.nodes[0]||{};
    const ours = prod.vendor==='Majilite' && v.sku===`${p.dw_sku}-Sample`;
    if(ours){ pub[p.dw_sku]={productId:prod.id,handle:prod.handle,status:prod.status,sample_sku:v.sku,image:prod.featuredMedia?'ok':'none',reconciled:true}; recorded++; if(!prod.featuredMedia) noimg.push(p.dw_sku); }
    else collided.push({sku:p.dw_sku, handle, owner_vendor:prod.vendor, owner_variant:v.sku});
    await sleep(120);
  }
  fs.writeFileSync(PUB, JSON.stringify(pub,null,2));
  console.log(`already_ok=${ok} newly_recorded=${recorded} total_published=${Object.keys(pub).length}`);
  console.log(`genuinely_missing=${missing.length}${missing.length?': '+missing.join(','):''}`);
  console.log(`collisions_with_other_products=${collided.length}`); if(collided.length) console.log(JSON.stringify(collided,null,1));
  console.log(`recorded_without_image=${noimg.length}${noimg.length?': '+noimg.join(','):''}`);
})();