← back to Designerwallcoverings

data/hollywood-resku-20260813/resku-hollywood.mjs

101 lines

#!/usr/bin/env node
// Hollywood SKU leak fix — TK-10518 (2026-08-13)
// Migrates 287 HWC-* and XWJ-* products to opaque DWHD-100001+ SKUs.
// GATED: Shopify variant SKU write + dw_sku_registry insert + alias creation
// DRY-RUN by default; pass --apply to execute.
//
// Usage:
//   node resku-hollywood.mjs              # dry run
//   node resku-hollywood.mjs --apply --yes-i-am-steve  # live run
import fs from 'node:fs';
import { execFileSync } from 'node:child_process';
import { createReadStream } from 'node:fs';
import { createInterface } from 'node:readline';

process.env.PATH = `/opt/homebrew/bin:/opt/homebrew/opt/postgresql@14/bin:/usr/local/bin:${process.env.PATH || ''}`;

const APPLY = process.argv.includes('--apply') && process.argv.includes('--yes-i-am-steve');
const CSV = new URL('./allocation.csv', import.meta.url).pathname;
const ENDPOINT = 'https://designer-laboratory-sandbox.myshopify.com/admin/api/2024-10/graphql.json';
const ENV = fs.readFileSync(process.env.HOME + '/Projects/secrets-manager/.env', 'utf8');
const TOKEN = (ENV.match(/^SHOPIFY_ADMIN_TOKEN=(.+)$/m) || [])[1]?.trim();
const sleep = ms => new Promise(r => setTimeout(r, ms));

const rows = fs.readFileSync(CSV, 'utf8').trim().split('\n').map(line => {
  const [shopify_id, old_sku, new_sku, mfr_sku, handle] = line.split(',');
  return { shopify_id, old_sku, new_sku, mfr_sku, handle };
});

console.log(`[hollywood-resku] ${rows.length} products to migrate. APPLY=${APPLY}`);
if (!APPLY) console.log('  DRY RUN — pass --apply --yes-i-am-steve to execute live Shopify writes.\n');

async function gql(q, v) {
  for (let i = 0; i < 5; i++) {
    const res = await fetch(ENDPOINT, {
      method: 'POST', headers: { 'X-Shopify-Access-Token': TOKEN, 'Content-Type': 'application/json' },
      body: JSON.stringify({ query: q, variables: v })
    });
    if (res.status === 429 || res.status >= 500) { await sleep(2000 * (i + 1)); continue; }
    const j = await res.json();
    if (j.errors?.some(e => e.extensions?.code === 'THROTTLED')) { await sleep(2000 * (i + 1)); continue; }
    return j;
  }
  throw new Error('gql exhausted');
}

// Step 1: Get variant IDs for each product (need variant.id to update variant SKU)
const GET_VARIANTS = `query($id:ID!){ product(id:$id){ variants(first:10){ nodes { id sku } } } }`;
// Step 2: Update variant SKU
const UPDATE_VARIANT = `mutation($id:ID!,$sku:String!){ productVariantUpdate(input:{id:$id,sku:$sku}){ productVariant{ id sku } userErrors{ message } } }`;

const ledger = fs.createWriteStream(new URL('./resku-ledger.jsonl', import.meta.url).pathname, { flags: 'a' });
let ok = 0, skip = 0, dryTotal = 0;

for (const row of rows) {
  if (!APPLY) {
    dryTotal++;
    console.log(`  [DRY] ${row.old_sku} → ${row.new_sku}  (${row.handle})`);
    if (dryTotal > 10) { console.log(`  … and ${rows.length - 10} more`); break; }
    continue;
  }

  // 1. Get variant ID
  const pRes = await gql(GET_VARIANTS, { id: row.shopify_id });
  const variants = pRes.data?.product?.variants?.nodes || [];
  const targetVariant = variants.find(v => v.sku === row.old_sku) || variants[0];
  if (!targetVariant) {
    console.log(`  SKIP ${row.old_sku}: no variant found`);
    ledger.write(JSON.stringify({ ts: new Date().toISOString(), ...row, result: 'SKIP_NO_VARIANT' }) + '\n');
    skip++; continue;
  }

  // 2. Update variant SKU
  const vRes = await gql(UPDATE_VARIANT, { id: targetVariant.id, sku: row.new_sku });
  const ue = vRes.data?.productVariantUpdate?.userErrors || [];
  if (ue.length) {
    console.log(`  ERROR ${row.old_sku}: ${JSON.stringify(ue)}`);
    ledger.write(JSON.stringify({ ts: new Date().toISOString(), ...row, result: 'ERROR', errors: ue }) + '\n');
    skip++; continue;
  }

  // 3. Register in dw_sku_registry
  execFileSync('psql', ['host=/tmp dbname=dw_unified', '-c',
    `INSERT INTO dw_sku_registry(dw_sku,vendor_prefix,vendor_name,mfr_sku,shopify_product_id,shopify_handle)
     VALUES('${row.new_sku}','DWHD','Hollywood Wallcoverings','${row.mfr_sku}','${row.shopify_id}','${row.handle}')
     ON CONFLICT(dw_sku) DO NOTHING`
  ]);

  // 4. Update dw_unified mirror
  execFileSync('psql', ['host=/tmp dbname=dw_unified', '-c',
    `UPDATE shopify_products SET dw_sku='${row.new_sku}', synced_at=now() WHERE shopify_id='${row.shopify_id}'`
  ]);

  ledger.write(JSON.stringify({ ts: new Date().toISOString(), ...row, result: 'OK', variant_id: targetVariant.id }) + '\n');
  console.log(`  [OK] ${row.old_sku} → ${row.new_sku}`);
  ok++; await sleep(150);
}

ledger.end();
if (APPLY) console.log(`\n[hollywood-resku] DONE: ok=${ok} skip=${skip}`);
else console.log(`\n[hollywood-resku] DRY RUN complete. ${rows.length} products would be migrated.`);