← 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(','):''}`);
})();