← back to Dw Pairs Well

tools/pattern-id-write.js

35 lines

// GATED WRITE (Steve-authorized 2026-06-26): set custom.pattern_id on ~61.8k active products
// via ONE Shopify bulk operation (metafieldsSet). Uses full-access token. Reversible (metafield).
const https=require('https'), fs=require('fs'), cp=require('child_process');
const STORE='designer-laboratory-sandbox.myshopify.com', API='2024-10';
const TOK=cp.execSync(`grep -rhoE 'shpat_[a-f0-9]{32}' ~/Projects/secrets-manager/.env ~/Projects/Designer-Wallcoverings/DW-Agents ~/Projects/Designer-Wallcoverings/shopify 2>/dev/null | sort -u`).toString().split('\n').find(t=>t.startsWith('shpat_307')&&t.endsWith('a43b'));
function gql(q,v){return new Promise((res,rej)=>{const b=JSON.stringify({query:q,variables:v||{}});const r=https.request({host:STORE,path:`/admin/api/${API}/graphql.json`,method:'POST',headers:{'X-Shopify-Access-Token':TOK,'Content-Type':'application/json','Content-Length':Buffer.byteLength(b)}},x=>{let d='';x.on('data',c=>d+=c);x.on('end',()=>{try{res(JSON.parse(d))}catch(e){rej(d.slice(0,400))}})});r.on('error',rej);r.write(b);r.end();});}
(async()=>{
  // WS-1 re-backfill 2026-06-26: NEW title-first + color-invariant map (TH=30), NOT the old mfr-first map.
  const map=require('./pattern-id-titlefirst-by-dwsku.json');
  const lines=Object.values(map).filter(p=>p.sid&&p.pid)
    .map(p=>JSON.stringify({metafields:[{ownerId:p.sid,namespace:'custom',key:'pattern_id',type:'single_line_text_field',value:String(p.pid).slice(0,255)}]}));
  fs.writeFileSync('tools/pattern-id-write.jsonl', lines.join('\n')+'\n');
  console.log('JSONL lines:',lines.length);
  // 1) staged upload
  const su=await gql(`mutation{stagedUploadsCreate(input:{resource:BULK_MUTATION_VARIABLES,filename:"pid.jsonl",mimeType:"text/jsonl",httpMethod:POST}){stagedTargets{url resourceUrl parameters{name value}}userErrors{field message}}}`);
  const t=su.data.stagedUploadsCreate.stagedTargets[0];
  if(!t){console.error('staged upload failed',JSON.stringify(su));process.exit(1);}
  // 2) upload via curl (reliable multipart)
  const fArgs=t.parameters.map(p=>`-F ${JSON.stringify(p.name+'='+p.value)}`).join(' ');
  cp.execSync(`curl -s -o /dev/null -w "upload http=%{http_code}\\n" ${fArgs} -F "file=@tools/pattern-id-write.jsonl" ${JSON.stringify(t.url)}`,{stdio:'inherit'});
  const stagedPath=t.parameters.find(p=>p.name==='key').value;
  // 3) run bulk mutation
  const run=await gql(`mutation($path:String!){bulkOperationRunMutation(mutation:"mutation call($metafields:[MetafieldsSetInput!]!){metafieldsSet(metafields:$metafields){userErrors{field message}}}",stagedUploadPath:$path){bulkOperation{id status}userErrors{field message}}}`,{path:stagedPath});
  const ue=run.data?.bulkOperationRunMutation?.userErrors;
  if(ue&&ue.length){console.error('run userErrors',JSON.stringify(ue));process.exit(1);}
  console.log('bulk mutation started:',run.data.bulkOperationRunMutation.bulkOperation.id);
  // 4) poll
  for(let i=0;i<240;i++){await new Promise(r=>setTimeout(r,5000));
    const c=await gql(`{currentBulkOperation(type:MUTATION){status objectCount errorCode url}}`);
    const o=c.data.currentBulkOperation; process.stdout.write(`\r  ${o.status} objects=${o.objectCount||0}    `);
    if(o.status==='COMPLETED'){console.log('\n COMPLETED objects=',o.objectCount,'errorsUrl=',o.url||'(none)');break;}
    if(o.status==='FAILED'){console.error('\n FAILED',o.errorCode);process.exit(1);}
  }
})().catch(e=>{console.error('ERR',e);process.exit(1);});