← back to Koroseal Goodrich Audit

tk10347/apply_swaps.mjs

90 lines

#!/usr/bin/env node
// TK-10347 — apply 6 approved Koroseal cross-link image swaps on LIVE store.
// Steve APPROVED + UNGATED (2026-08-08). Idempotent-ish: skips upload if same source already present.
import fs from 'node:fs';
const ENV = fs.readFileSync(process.env.HOME + '/Projects/secrets-manager/.env','utf8');
const TOKEN = (ENV.match(/^SHOPIFY_ADMIN_TOKEN=(.+)$/m)||[])[1]?.trim().replace(/^["']|["']$/g,'');
const STORE = 'designer-laboratory-sandbox.myshopify.com', API='2024-10';
const URL = `https://${STORE}/admin/api/${API}/graphql.json`;
const AK='https://koroseal.asset.akeneo.cloud';
const KM='https://koroseal.com';
const APPLY = process.argv.includes('--apply');

const JOBS = [
  {sku:'TAK-EA02-04', pid:'gid://shopify/Product/7519950110771', mode:'upload',
   src:`${AK}/images/media/Kilika_ROOMSET_TAK_EA02_04.jpg`, alt:'Kilika Deep Taupe TAK-EA02-04',
   wrong:'gid://shopify/MediaImage/24773828247603'},
  {sku:'TAK-EA01-01', pid:'gid://shopify/Product/7519949357107', mode:'upload',
   src:`${AK}/images/media/TAKUMI_ROOMSET_TAK_EA01_01__WEAVES_.jpg`, alt:'Cyrus Weave Linen Natural TAK-EA01-01',
   wrong:'gid://shopify/MediaImage/24773827788851'},
  {sku:'TAK-CA05-01', pid:'gid://shopify/Product/7519948603443', mode:'upload',
   src:`${AK}/images/media/TAKUMI_ROOMSET_TAK_CA05_01__CORK_.jpg`, alt:'Aura Cork Silver TAK-CA05-01',
   wrong:'gid://shopify/MediaImage/24773827297331'},
  {sku:'SRD-CH1-02', pid:'gid://shopify/Product/7634018566195', mode:'upload',
   src:`${AK}/thumbnails/media/chess_1_gray_pink_srd_ch1_02__sarah_rowland_for_koroseal.jpg`, alt:'Chess 1 Gray Pink SRD-CH1-02',
   wrong:'gid://shopify/MediaImage/26514726125619'},
  {sku:'8221-08', pid:'gid://shopify/Product/7584922009651', mode:'upload',
   src:`${AK}/thumbnails/media/ramie_woven_8221_08_.jpg`, alt:'Ramie Woven 8221-08',
   wrong:'gid://shopify/MediaImage/26434620620851'},
  {sku:'SRD-AUR-02', pid:'gid://shopify/Product/7634018074675', mode:'reorder',
   keep:'gid://shopify/MediaImage/26514716131379', // existing AURORA_SMOKE_SRD-AUR-02
   wrong:'gid://shopify/MediaImage/26514747752499'},
];

async function gql(query, variables){
  const r = await fetch(URL, {method:'POST', headers:{'X-Shopify-Access-Token':TOKEN,'Content-Type':'application/json'},
    body: JSON.stringify({query, variables})});
  const j = await r.json();
  if(j.errors) throw new Error('GraphQL errors: '+JSON.stringify(j.errors));
  return j.data;
}
const sleep = ms => new Promise(r=>setTimeout(r,ms));

async function firstMediaId(pid){
  const d = await gql(`query($id:ID!){ product(id:$id){ media(first:1){ edges{ node{ id } } } } }`, {id:pid});
  return d.product.media.edges[0]?.node?.id;
}

async function run(){
  console.log(`APPLY=${APPLY}  token=${TOKEN? 'present':'MISSING'}`);
  for(const j of JOBS){
    console.log(`\n=== ${j.sku} (${j.mode}) ===`);
    let newId = j.keep;
    if(j.mode==='upload'){
      if(!APPLY){ console.log('  DRY: would createMedia', j.src); }
      else {
        const d = await gql(`mutation($pid:ID!,$m:[CreateMediaInput!]!){ productCreateMedia(productId:$pid, media:$m){ media{ ... on MediaImage{ id status } } mediaUserErrors{ field message } } }`,
          {pid:j.pid, m:[{originalSource:j.src, mediaContentType:'IMAGE', alt:j.alt}]});
        const ue=d.productCreateMedia.mediaUserErrors; if(ue.length) throw new Error(j.sku+' createMedia: '+JSON.stringify(ue));
        newId = d.productCreateMedia.media[0].id;
        console.log('  created', newId, 'status', d.productCreateMedia.media[0].status);
        // poll READY
        for(let i=0;i<30;i++){
          const p = await gql(`query($id:ID!){ node(id:$id){ ... on MediaImage{ status image{ url } } } }`, {id:newId});
          const st=p.node.status;
          if(st==='READY'){ console.log('  READY', (p.node.image?.url||'').slice(-70)); break; }
          if(st==='FAILED') throw new Error(j.sku+' media FAILED');
          await sleep(2000);
        }
      }
    }
    // reorder new/keep media to position 0 (featured)
    if(!APPLY){ console.log('  DRY: would reorder', newId||'<newId>', '->0 and delete', j.wrong); continue; }
    const rd = await gql(`mutation($id:ID!,$moves:[MoveInput!]!){ productReorderMedia(id:$id, moves:$moves){ mediaUserErrors{ message } job{ id done } } }`,
      {id:j.pid, moves:[{id:newId, newPosition:'0'}]});
    const rue=rd.productReorderMedia.mediaUserErrors; if(rue.length) throw new Error(j.sku+' reorder: '+JSON.stringify(rue));
    console.log('  reordered ->0');
    await sleep(2500);
    // delete the wrong sibling
    const dd = await gql(`mutation($pid:ID!,$ids:[ID!]!){ productDeleteMedia(productId:$pid, mediaIds:$ids){ deletedMediaIds mediaUserErrors{ message } } }`,
      {pid:j.pid, ids:[j.wrong]});
    const due=dd.productDeleteMedia.mediaUserErrors; if(due.length) throw new Error(j.sku+' delete: '+JSON.stringify(due));
    console.log('  deleted wrong', dd.productDeleteMedia.deletedMediaIds);
    // verify featured
    const fm = await firstMediaId(j.pid);
    console.log('  NOW featured media =', fm, fm===newId?'✓':'(check)');
  }
  console.log('\nDONE');
}
run().catch(e=>{ console.error('ERROR:', e.message); process.exit(1); });