← back to Rebel Walls Push
scripts/TK-10405-fullfix-undo.mjs
68 lines
#!/usr/bin/env node
// UNDO for TK-10405-fullfix.mjs. Recreates each deleted Default-Title orphan (NEW variant id)
// from the restore map's full_fix_reorder_delete.orphan_recreate payload, restores its inventory
// qty, and reorders variants back to their original positions. A recreated variant gets a NEW id
// (documented old->new in output). Run: node scripts/TK-10405-fullfix-undo.mjs
import fs from 'node:fs';
const DOM = 'designer-laboratory-sandbox.myshopify.com';
const VER = '2024-10';
const RESTORE = new URL('../data/TK-10405-restore-map.json', import.meta.url).pathname;
const env = fs.readFileSync(process.env.HOME + '/Projects/secrets-manager/.env', 'utf8');
const TOKEN = (env.match(/^SHOPIFY_FULL_ACCESS_TOKEN=(.+)$/m)?.[1] || '').trim().replace(/^["']|["']$/g, '');
if (!TOKEN) { console.error('no SHOPIFY_FULL_ACCESS_TOKEN'); process.exit(1); }
async function gql(query, variables, tries = 6) {
for (let i = 0; i < tries; i++) {
const r = await fetch(`https://${DOM}/admin/api/${VER}/graphql.json`, {
method: 'POST', headers: { 'X-Shopify-Access-Token': TOKEN, 'Content-Type': 'application/json' },
body: JSON.stringify({ query, variables }) });
if ([429, 500, 502, 503].includes(r.status) && i < tries - 1) { await new Promise(s => setTimeout(s, 1500 * (i + 1))); continue; }
return r.json();
}
}
const Q_PROD = `query($id:ID!){ product(id:$id){ variants(first:20){ nodes{ id title inventoryItem{ id } } } } }`;
const M_CREATE = `mutation($productId:ID!,$variants:[ProductVariantsBulkInput!]!){
productVariantsBulkCreate(productId:$productId,variants:$variants){ productVariants{ id title inventoryItem{ id } } userErrors{ field message } } }`;
const M_REORDER = `mutation($productId:ID!,$positions:[ProductVariantPositionInput!]!){
productVariantsBulkReorder(productId:$productId,positions:$positions){ product{ id } userErrors{ field message } } }`;
const M_INV = `mutation($input:InventorySetQuantitiesInput!){
inventorySetQuantities(input:$input){ inventoryAdjustmentGroup{ createdAt } userErrors{ field message } } }`;
const short = g => g.split('/').pop();
(async () => {
const rm = JSON.parse(fs.readFileSync(RESTORE, 'utf8'));
const ff = rm.full_fix_reorder_delete;
if (!ff) { console.error('no full_fix_reorder_delete block in restore map — nothing to undo'); process.exit(1); }
for (const pid of Object.keys(ff.products)) {
const rec = ff.products[pid];
const productId = rec.product_gid;
const o = rec.orphan_recreate;
if (!o) { console.log(`${rec.label}: no orphan payload captured — skip`); continue; }
console.log(`\n=== UNDO ${rec.label} (${pid}) ===`);
// 1. recreate orphan
const input = { price: o.price, taxable: o.taxable, inventoryPolicy: o.inventoryPolicy,
inventoryItem: { sku: o.sku, tracked: o.tracked },
optionValues: (o.selectedOptions || []).map(so => ({ name: so.value, optionName: so.name })) };
const rc = await gql(M_CREATE, { productId, variants: [input] });
const dc = rc?.data?.productVariantsBulkCreate;
if (dc?.userErrors?.length || rc?.errors) { console.error(' RECREATE FAILED:', JSON.stringify(dc?.userErrors || rc.errors)); continue; }
const newVid = dc.productVariants[0].id;
const newInv = dc.productVariants[0].inventoryItem?.id;
console.log(` recreated orphan: OLD ${short(o.id)} -> NEW ${short(newVid)}`);
// 2. restore inventory qty
if (o.tracked && newInv && typeof o.inventoryQuantity === 'number') {
await gql(M_INV, { input: { name: 'available', reason: 'correction', ignoreCompareQuantity: true,
quantities: [{ inventoryItemId: newInv, locationId: 'gid://shopify/Location/5795643504', quantity: o.inventoryQuantity }] } });
console.log(` restored inventory available=${o.inventoryQuantity}`);
}
// 3. restore original positions (map old orphan id -> new id)
const positions = rec.original_positions.map(p => ({ id: p.id === o.id ? newVid : p.id, position: p.position }));
const rr = await gql(M_REORDER, { productId, positions });
const dr = rr?.data?.productVariantsBulkReorder;
if (dr?.userErrors?.length || rr?.errors) console.error(' REORDER-BACK FAILED:', JSON.stringify(dr?.userErrors || rr.errors));
else console.log(' restored original positions');
}
console.log('\nUNDO complete (recreated orphans have NEW ids — see mapping above).');
})();