← 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);