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