← back to Tk10640 Redirect Prune
pull-and-classify.mjs
80 lines
// TK-10640 — READ-ONLY pull of ALL Shopify url redirects + structural classification.
// Pulls every redirect once (paginated), dumps to data/redirects-dump.jsonl, then
// classifies prune candidates that need NO per-product call:
// self : path === target (useless self-redirect)
// empty_target : target empty or "/" (sends to homepage — usually junk)
// chain_source : target is itself the PATH of another redirect (redirect chain; Shopify
// won't hop twice, so the intermediate is dead weight)
// dup_target : >1 redirect whose path differs but all point at same target (informational)
// Dead-target (/products/<handle> gone) is done separately by prune-redirects.mjs.
// NO WRITES. Uses SHOPIFY_FULL_ACCESS_TOKEN (only token with content read scope).
import { readFileSync, writeFileSync, appendFileSync } from 'node:fs';
const txt = readFileSync(`${process.env.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, TOKEN=env.SHOPIFY_FULL_ACCESS_TOKEN;
const API='2024-10';
const sleep=ms=>new Promise(r=>setTimeout(r,ms));
async function gql(query,variables={}){
for(let a=0;;a++){
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,variables})});
const j=await res.json();
if(j.errors){ if(/THROTTLED/i.test(JSON.stringify(j.errors))&&a<8){await sleep(2000*(a+1));continue;} throw new Error(JSON.stringify(j.errors)); }
return j.data;
}
}
const DUMP=`${process.env.HOME}/Projects/tk10640-redirect-prune/data/redirects-dump.jsonl`;
writeFileSync(DUMP,'');
let cursor=null, n=0;
const all=[];
while(true){
const d=await gql(`query($c:String){ urlRedirects(first:250, after:$c){ pageInfo{ hasNextPage endCursor } nodes{ id path target } } }`,{c:cursor});
const buf=[];
for(const r of d.urlRedirects.nodes){ all.push(r); buf.push(JSON.stringify(r)); n++; }
appendFileSync(DUMP, buf.join('\n')+'\n');
if(n%5000===0) process.stderr.write(` pulled ${n}\n`);
if(!d.urlRedirects.pageInfo.hasNextPage) break;
cursor=d.urlRedirects.pageInfo.endCursor;
}
process.stderr.write(`pulled ${n} total\n`);
// classify
const pathSet=new Set(all.map(r=>r.path));
const norm=s=>(s||'').split('?')[0].split('#')[0];
const targetCount=new Map();
for(const r of all){ const t=norm(r.target); targetCount.set(t,(targetCount.get(t)||0)+1); }
const cat={self:[],empty_target:[],chain_source:[],};
const targetKind={products:0,collections:0,pages:0,blogs:0,other_internal:0,external:0,root_or_empty:0};
for(const r of all){
const t=norm(r.target);
if(!t||t==='/') { cat.empty_target.push(r); targetKind.root_or_empty++; continue; }
if(r.path===r.target){ cat.self.push(r); }
if(pathSet.has(t) && t!==r.path){ cat.chain_source.push(r); }
if(/^\/products\//.test(t)) targetKind.products++;
else if(/^\/collections\//.test(t)) targetKind.collections++;
else if(/^\/pages\//.test(t)) targetKind.pages++;
else if(/^\/blogs\//.test(t)) targetKind.blogs++;
else if(/^https?:\/\//.test(t)) targetKind.external++;
else if(/^\//.test(t)) targetKind.other_internal++;
}
const dupTargets=[...targetCount.entries()].filter(([,c])=>c>1);
const dupRedirectCount=dupTargets.reduce((s,[,c])=>s+c,0);
const summary={
total:n,
target_kind:targetKind,
prune_candidates_no_apicall:{
self_redirect:cat.self.length,
empty_or_root_target:cat.empty_target.length,
chain_source_intermediate:cat.chain_source.length,
},
duplicate_target_groups:dupTargets.length,
redirects_sharing_a_target:dupRedirectCount,
note:'dead-target(/products gone) counted separately by prune-redirects.mjs',
};
writeFileSync(`${process.env.HOME}/Projects/tk10640-redirect-prune/data/classify-summary.json`, JSON.stringify(summary,null,2));
// sample dumps for the memo
writeFileSync(`${process.env.HOME}/Projects/tk10640-redirect-prune/data/cand-self.jsonl`, cat.self.slice(0,200).map(r=>JSON.stringify(r)).join('\n'));
writeFileSync(`${process.env.HOME}/Projects/tk10640-redirect-prune/data/cand-empty.jsonl`, cat.empty_target.slice(0,200).map(r=>JSON.stringify(r)).join('\n'));
writeFileSync(`${process.env.HOME}/Projects/tk10640-redirect-prune/data/cand-chain.jsonl`, cat.chain_source.slice(0,200).map(r=>JSON.stringify(r)).join('\n'));
console.log(JSON.stringify(summary,null,2));