← back to Tk10640 Redirect Prune
rollback-remediation.mjs
40 lines
// TK-10653 — ROLLBACK the broken-redirect remediation. Reads data/full-scan/done-remediation.jsonl
// (what ACTUALLY fired) + data/full-scan/restore-map.jsonl (the pre-computed undo) and reverses
// only rows that were applied:
// repointed -> urlRedirectUpdate(id, {path, target:restore_target}) (back to old target)
// pruned -> urlRedirectCreate({path, target:recreate_target}) (fresh GID, path is key)
// DRY-RUN default; --apply reverses. Uses SHOPIFY_FULL_ACCESS_TOKEN only.
import { readFileSync, existsSync } from 'node:fs';
const HOME=process.env.HOME;
const txt=readFileSync(`${HOME}/Projects/secrets-manager/.env`,'utf8');
const env={}; for(const l of txt.split('\n')){const m=l.match(/^([A-Z0-9_]+)=(.*)$/); if(m) env[m[1]]=m[2].replace(/^["']|["']$/g,'');}
const STORE=env.SHOPIFY_STORE_DOMAIN||'designer-laboratory-sandbox.myshopify.com', TOKEN=env.SHOPIFY_FULL_ACCESS_TOKEN, API='2024-10';
const APPLY=process.argv.includes('--apply');
const sleep=ms=>new Promise(r=>setTimeout(r,ms));
async function gql(q,v={}){for(let a=0;a<12;a++){try{
const res=await fetch(`https://${STORE}/admin/api/${API}/graphql.json`,{method:'POST',headers:{'Content-Type':'application/json','X-Shopify-Access-Token':TOKEN},body:JSON.stringify({query:q,variables:v})});
const tx=await res.text();let j;try{j=JSON.parse(tx);}catch{await sleep(1200*(a+1));continue;}
if(j.errors){if(/THROTTLED/i.test(JSON.stringify(j.errors))){await sleep(1200*(a+1));continue;}throw new Error(JSON.stringify(j.errors));}
return j.data;
}catch(e){if(a<11){await sleep(1200*(a+1));continue;}throw e;}}}
const D=`${HOME}/Projects/tk10640-redirect-prune/data/full-scan/done-remediation.jsonl`;
const R=`${HOME}/Projects/tk10640-redirect-prune/data/full-scan/restore-map.jsonl`;
if(!existsSync(D)){ console.log('no done-remediation log — nothing applied, nothing to roll back'); process.exit(0); }
const restoreByPath=new Map();
for(const l of readFileSync(R,'utf8').trim().split('\n')) if(l.trim()){ const x=JSON.parse(l); restoreByPath.set(x.path,x); }
const applied=readFileSync(D,'utf8').trim().split('\n').filter(Boolean).map(JSON.parse);
const UPD=`mutation($id:ID!,$r:UrlRedirectInput!){ urlRedirectUpdate(id:$id, urlRedirect:$r){ userErrors{ message } } }`;
const CRE=`mutation($r:UrlRedirectInput!){ urlRedirectCreate(urlRedirect:$r){ urlRedirect{ id } userErrors{ message } } }`;
let re_repoint=0, re_create=0, err=0;
for(const a of applied){
const rmap=restoreByPath.get(a.path);
if(a.repointed && rmap){
if(APPLY){ const d=await gql(UPD,{id:a.id, r:{path:a.path, target:rmap.restore_target}}); if(d.urlRedirectUpdate.userErrors.length) err++; else re_repoint++; }
else re_repoint++;
} else if(a.pruned && rmap){
if(APPLY){ const d=await gql(CRE,{r:{path:a.path, target:rmap.recreate_target}}); if(d.urlRedirectCreate.userErrors.length) err++; else re_create++; }
else re_create++;
}
}
console.log(JSON.stringify({mode:APPLY?'APPLY':'DRY-RUN', applied_in_log:applied.length, restore_repoint:re_repoint, recreate_pruned:re_create, errors:err},null,2));