← 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).');
})();