← back to Hollywood Optc
apply-held36-canary.mjs
116 lines
#!/usr/bin/env node
// TK-10633 — Option-C EXTENSION: remap the 36 metafield-clean HELD products the original
// 62-run missed. Each of these 36 ACTIVE Hollywood products still carries a fabricated
// DWHD-* variant SKU on the LIVE store, yet already has its real, pre-existing HWC-####
// line code unanimously in global/dwc/custom.dw_sku (verified LIVE). This is the SAME
// Steve-approved identity scheme + write shape as apply.mjs (the 62-run): variant SKU
// DWHD-##### -> HWC-####, DWHD-#####-Sample -> HWC-####-Sample. manufacturer_sku (the
// bare Momentum number) is NEVER touched — no leak.
//
// GATED: this is a customer-facing LIVE Shopify write. Do NOT run without Steve's go.
// Drives off data/held36-canary.json (built read-only from LIVE Shopify).
// Reversibility FIRST: appends {variant_id, from_sku, to_sku, ts} before every write.
// Idempotent (skips a variant already == target). Rate-limit + THROTTLED aware.
// Rollback: node rollback.mjs data/apply-reversibility-held36-<ts>.jsonl
import { readFileSync, appendFileSync } from 'node:fs';
const SHOP = 'designer-laboratory-sandbox.myshopify.com';
const VER = '2024-10';
const env = readFileSync(process.env.HOME + '/Projects/secrets-manager/.env', 'utf8');
const TOKEN = (env.split('\n').find(l => l.startsWith('SHOPIFY_ADMIN_TOKEN=')) || '')
.replace('SHOPIFY_ADMIN_TOKEN=', '').replace(/["'\r]/g, '').trim();
if (!TOKEN) { console.error('no SHOPIFY_ADMIN_TOKEN'); process.exit(1); }
const dry = JSON.parse(readFileSync(new URL('./data/held36-canary.json', import.meta.url)));
const applySet = dry.apply_set || [];
if (applySet.length !== 1) { console.error(`expected 36 apply_set products, got ${applySet.length}`); process.exit(1); }
if (dry.problems && dry.problems.length) { console.error(`dry-run has ${dry.problems.length} problems — refusing`); process.exit(1); }
const TS = new Date().toISOString().replace(/[:.]/g, '-');
const REV_PATH = new URL(`./data/apply-reversibility-held36-${TS}.jsonl`, import.meta.url).pathname;
const OUT_PATH = new URL(`./data/apply-result-held36-${TS}.json`, import.meta.url).pathname;
const GQL = `https://${SHOP}/admin/api/${VER}/graphql.json`;
const sleep = ms => new Promise(r => setTimeout(r, ms));
async function graphql(query, variables, tries = 0) {
const res = await fetch(GQL, {
method: 'POST',
headers: { 'X-Shopify-Access-Token': TOKEN, 'Content-Type': 'application/json' },
body: JSON.stringify({ query, variables }),
});
if (res.status === 429) { await sleep(2000); return graphql(query, variables, tries); }
if ((res.status === 502 || res.status === 503) && tries < 4) { await sleep(1500 * (tries + 1)); return graphql(query, variables, tries + 1); }
const j = await res.json();
if (j.errors && j.errors.some(e => /THROTTLED|throttl/i.test(JSON.stringify(e))) && tries < 6) {
await sleep(2500 * (tries + 1)); return graphql(query, variables, tries + 1);
}
return j;
}
const MUT = `
mutation setSku($productId: ID!, $variants: [ProductVariantsBulkInput!]!) {
productVariantsBulkUpdate(productId: $productId, variants: $variants) {
productVariants { id inventoryItem { sku } }
userErrors { field message code }
}
}`;
const CUR = `query cur($id: ID!) { productVariant(id: $id) { id inventoryItem { sku } } }`;
async function currentSku(variantGid, tries = 0) {
const j = await graphql(CUR, { id: variantGid });
if (j.errors && tries < 4) { await sleep(1500 * (tries + 1)); return currentSku(variantGid, tries + 1); }
return j?.data?.productVariant?.inventoryItem?.sku ?? null;
}
async function setSku(productGid, variantGid, newSku) {
const j = await graphql(MUT, { productId: productGid, variants: [{ id: variantGid, inventoryItem: { sku: newSku } }] });
const ue = j?.data?.productVariantsBulkUpdate?.userErrors || [];
const pv = j?.data?.productVariantsBulkUpdate?.productVariants || [];
return { userErrors: ue, topErrors: j.errors || [], confirmedSku: pv[0]?.inventoryItem?.sku ?? null };
}
async function main() {
const results = { applied: 0, skipped_idempotent: 0, failed: 0, details: [] };
const totalVariants = applySet.reduce((a, p) => a + p.writes.length, 0);
let idx = 0;
for (const p of applySet) {
const productGid = `gid://shopify/Product/${p.product_id}`;
for (const w of p.writes) {
idx++;
const variantGid = `gid://shopify/ProductVariant/${w.variant_id}`;
appendFileSync(REV_PATH, JSON.stringify({ variant_id: w.variant_id, from_sku: w.from_sku, to_sku: w.to_sku, ts: new Date().toISOString() }) + '\n');
let live = null;
try { live = await currentSku(variantGid); } catch (e) { /* attempt write */ }
if (live && live.toUpperCase() === w.to_sku.toUpperCase()) {
results.skipped_idempotent++;
results.details.push({ variant_id: w.variant_id, status: 'skip_already_target', sku: live });
continue;
}
const r = await setSku(productGid, variantGid, w.to_sku);
if (r.topErrors.length || r.userErrors.length) {
results.failed++;
results.details.push({ variant_id: w.variant_id, handle: p.handle, from: w.from_sku, to: w.to_sku, status: 'ERROR', userErrors: r.userErrors, topErrors: r.topErrors });
console.error(`[${idx}/${totalVariants}] FAIL ${w.from_sku} -> ${w.to_sku}: ` + JSON.stringify(r.userErrors.length ? r.userErrors : r.topErrors));
} else if (r.confirmedSku && r.confirmedSku.toUpperCase() === w.to_sku.toUpperCase()) {
results.applied++;
results.details.push({ variant_id: w.variant_id, status: 'applied', from: w.from_sku, to: r.confirmedSku });
console.log(`[${idx}/${totalVariants}] OK ${w.from_sku} -> ${r.confirmedSku}`);
} else {
results.failed++;
results.details.push({ variant_id: w.variant_id, status: 'UNCONFIRMED', from: w.from_sku, to: w.to_sku, confirmedSku: r.confirmedSku });
console.error(`[${idx}/${totalVariants}] UNCONFIRMED ${w.from_sku} -> ${w.to_sku} (got ${r.confirmedSku})`);
}
await sleep(350);
}
}
console.log('\n=== SUMMARY ===');
console.log(`applied: ${results.applied}`);
console.log(`skipped_idempotent: ${results.skipped_idempotent}`);
console.log(`failed: ${results.failed}`);
console.log(`reversibility file: ${REV_PATH}`);
appendFileSync(OUT_PATH, JSON.stringify(results, null, 2));
console.log(`result detail: ${OUT_PATH}`);
if (results.failed) { console.error('\n!! FAILURES PRESENT — reversibility record intact, do NOT half-apply silently.'); process.exit(2); }
}
main().catch(e => { console.error('FATAL', e); process.exit(1); });