← back to Majilite Onboard

scripts/finish.js

56 lines

const fs=require('fs'),path=require('path');
const 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/2024-10/graphql.json`;
const sleep=ms=>new Promise(r=>setTimeout(r,ms));
const DRY=!process.argv.includes('--apply');
async function gql(q,v){for(let a=0;a<6;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 function allMajilite(){
  let all=[],c=null;
  do{const d=await gql(`query($c:String){ products(first:100, after:$c, query:"vendor:Majilite"){ pageInfo{hasNextPage endCursor} nodes{ id handle status variants(first:1){nodes{sku}} } } }`,{c});
     all.push(...d.products.nodes); c=d.products.pageInfo.hasNextPage?d.products.pageInfo.endCursor:null;}while(c);
  return all;
}

(async()=>{
  // 1) DELETE the 8 "-1" duplicate handles
  let all=await allMajilite();
  const bySku={};
  for(const p of all){const base=((p.variants.nodes[0]||{}).sku||'').replace(/-Sample$/,'');(bySku[base]=bySku[base]||[]).push(p);}
  const toDelete=[];
  for(const [sku,arr] of Object.entries(bySku)) if(arr.length>1){
    // keep the clean handle, delete the one(s) ending -\d
    const keep=arr.find(p=>!/-\d+$/.test(p.handle))||arr[0];
    for(const p of arr) if(p.id!==keep.id) toDelete.push(p);
  }
  console.log(`duplicates to delete: ${toDelete.length}`, toDelete.map(p=>p.handle).join(', '));
  if(!DRY) for(const p of toDelete){
    const d=await gql(`mutation($id:ID!){ productDelete(input:{id:$id}){ deletedProductId userErrors{message} } }`,{id:p.id});
    const ue=d.productDelete.userErrors; console.log(ue.length?('  ✗ '+p.handle+' '+JSON.stringify(ue)):('  ✓ deleted '+p.handle)); await sleep(250);
  }

  // 2) Get publication ids for Online Store + Google
  const pd=await gql(`{ publications(first:20){ nodes{ id name } } }`);
  const pubs=pd.publications.nodes;
  console.log('publications:', pubs.map(p=>p.name).join(' | '));
  const targets=pubs.filter(p=>/online store|google/i.test(p.name));
  console.log('publishing to:', targets.map(p=>p.name).join(', ')||'(none matched!)');

  // 3) Publish every remaining Majilite product to those channels
  all=await allMajilite();
  console.log(`publishing ${all.length} products to ${targets.length} channels…`);
  let ok=0,err=0;
  if(!DRY) for(const p of all){
    try{
      const d=await gql(`mutation($id:ID!,$pubs:[PublicationInput!]!){ publishablePublish(id:$id, input:$pubs){ userErrors{field message} } }`,
        {id:p.id, pubs:targets.map(t=>({publicationId:t.id}))});
      const ue=d.publishablePublish.userErrors; if(ue&&ue.length){err++; if(err<=5)console.log('  ✗',p.handle,JSON.stringify(ue).slice(0,80));}
      else ok++;
    }catch(e){err++; if(err<=5)console.log('  ✗',p.handle,e.message.slice(0,80));}
    if(ok%25===0&&ok)process.stdout.write(`  …${ok} published\n`);
    await sleep(200);
  }
  console.log(`\nDONE. deleted_dupes=${toDelete.length} published_ok=${ok} errors=${err} total_products=${all.length}`);
})();