← back to Dw Contact Us Pages

scripts/unpublish-channels.mjs

84 lines

#!/usr/bin/env node
// unpublish-channels.mjs — remove the cohort from 3 sales channels ONLY. TK-11925.
//   node scripts/unpublish-channels.mjs           # dry-run
//   node scripts/unpublish-channels.mjs --apply
//   node scripts/unpublish-channels.mjs --rollback --apply
//
// Channels (Steve-specified, nothing else):
//   Google & YouTube  gid://shopify/Publication/29646651457
//   Shop              gid://shopify/Publication/44317507635
//   Buy Button        gid://shopify/Publication/22497296496
// Online Store stays PUBLISHED — the PDP must remain reachable; it is the contact-us page.
import { join } from 'node:path';
import { DATA_DIR, TICKET, COHORT_FLAG, parseArgs, banner, gql, loadTargets, appendJsonl, readJsonl, logReversible, TARGET_PUBLICATIONS, payloadErrors, assertFreshTargets } from './lib.mjs';

const a = parseArgs();
const LEDGER = join(DATA_DIR, 'ledger-channels.jsonl');
banner(a.rollback ? 'unpublish-channels --rollback' : 'unpublish-channels', a.apply);

const M_UNPUB = `mutation U($id: ID!, $input: [PublicationInput!]!) {
  publishableUnpublish(id: $id, input: $input) { publishable { availablePublicationsCount { count } } userErrors { field message } } }`;
const M_PUB = `mutation P($id: ID!, $input: [PublicationInput!]!) {
  publishablePublish(id: $id, input: $input) { publishable { availablePublicationsCount { count } } userErrors { field message } } }`;

const byId = new Map(TARGET_PUBLICATIONS.map((p) => [p.id, p.name]));
let work;
if (a.rollback) {
  const rows = readJsonl(LEDGER).filter((r) => r.applied);
  const m = new Map();
  for (const r of rows) if (!m.has(r.productId)) m.set(r.productId, r); // first record = true preimage
  work = [...m.values()]
    .map((r) => ({ id: r.productId, handle: r.handle, pubs: r.before.filter((b) => b.isPublished).map((b) => b.publicationId) }))
    .filter((w) => w.pubs.length);
} else {
  work = [];
  for (const p of loadTargets()) {
    const pubs = p.resourcePublicationsV2.filter((rp) => byId.has(rp.publication.id) && rp.isPublished);
    if (!pubs.length) continue;
    work.push({
      id: p.id, handle: p.handle, pubs: pubs.map((rp) => rp.publication.id),
      before: p.resourcePublicationsV2.filter((rp) => byId.has(rp.publication.id))
        .map((rp) => ({ publicationId: rp.publication.id, name: rp.publication.name, isPublished: rp.isPublished })),
    });
  }
}

const ops = work.reduce((n, w) => n + w.pubs.length, 0);
const perChannel = {};
for (const w of work) for (const id of w.pubs) perChannel[byId.get(id) || id] = (perChannel[byId.get(id) || id] || 0) + 1;
console.log(`plan (${a.rollback ? 'RE-PUBLISH' : 'UNPUBLISH'}): ${work.length} products, ${ops} publication operations`);
console.log('  per channel: ' + JSON.stringify(perChannel));
console.log('  Online Store is NOT in scope and stays published.');
if (!a.apply) { console.log('\nDRY-RUN: nothing was written.'); process.exit(0); }
assertFreshTargets(a);

// PREIMAGE FIRST (TK-11669) — see assign-template.mjs. Re-publishing a channel that was never
// unpublished is a no-op, so a preimage row for an unwritten product is harmless.
if (!a.rollback) {
  const runId = new Date().toISOString();
  for (const w of work) appendJsonl(LEDGER, { ts: runId, runId, productId: w.id, handle: w.handle, before: w.before, unpublished: w.pubs, applied: true, phase: 'preimage' });
  console.log(`  preimage: ${work.length} rows appended to ${LEDGER} BEFORE any write`);
}

let ok = 0, fail = 0;
for (const w of work) {
  const input = w.pubs.map((publicationId) => ({ publicationId }));
  let d; try { d = await gql(a.rollback ? M_PUB : M_UNPUB, { id: w.id, input }); } catch (e) { d = { _err: String(e.message || e) }; }
  const node = d._err ? null : (a.rollback ? d.publishablePublish : d.publishableUnpublish);
  if (d._err) console.error('  FAIL ' + w.handle + ': ' + d._err.slice(0, 200));
  const e = d._err ? [{ message: d._err }] : payloadErrors(node, a.rollback ? 'publishablePublish' : 'publishableUnpublish');
  if (e.length) { fail++; console.error(`  FAIL ${w.handle}: ${JSON.stringify(e).slice(0, 200)}`); }
  else {
    ok++;
    if (ok % 50 === 0) process.stderr.write(`\r  ${ok}/${work.length}   `);
  }
}
console.log(`\ndone: ${ok} products ok, ${fail} failed`);
if (ok && !a.rollback) logReversible({
  action: `${TICKET} unpublish ${ok} products from Google & YouTube / Shop / Buy Button (${ops} publication ops)`,
  blast: ops,
  undo: `cd ~/Projects/dw-contact-us-pages && node scripts/unpublish-channels.mjs${COHORT_FLAG} --rollback --apply`,
  verify: `cd ~/Projects/dw-contact-us-pages && node scripts/verify.mjs${COHORT_FLAG}`,
});
process.exit(fail ? 1 : 0);