← back to Designerwallcoverings
Revert "google-channel unpublish: add tag-scoped path for shared-vendor showroom lines (TK-11307)"
fe298d64c4755ce4f895a8d556f066c3b1b71180 · 2026-09-09 07:28:14 -0700 · steve
This reverts commit 01c73ed80df87e56e908e160beb809efa69c62e7.
Files touched
M scripts/google-feed/apply-unpublish.mjsD scripts/google-feed/prep-showroom-unpublish-tag.mjs
Diff
commit fe298d64c4755ce4f895a8d556f066c3b1b71180
Author: steve <steve@designerwallcoverings.com>
Date: Wed Sep 9 07:28:14 2026 -0700
Revert "google-channel unpublish: add tag-scoped path for shared-vendor showroom lines (TK-11307)"
This reverts commit 01c73ed80df87e56e908e160beb809efa69c62e7.
---
scripts/google-feed/apply-unpublish.mjs | 31 +------
.../google-feed/prep-showroom-unpublish-tag.mjs | 98 ----------------------
2 files changed, 2 insertions(+), 127 deletions(-)
diff --git a/scripts/google-feed/apply-unpublish.mjs b/scripts/google-feed/apply-unpublish.mjs
index b754a6c..877f23c 100644
--- a/scripts/google-feed/apply-unpublish.mjs
+++ b/scripts/google-feed/apply-unpublish.mjs
@@ -24,7 +24,7 @@ import fs from 'node:fs';
import path from 'node:path';
import { createRequire } from 'node:module';
const require = createRequire(import.meta.url);
-const { isShowroomVendor, isShowroomProduct } = require(process.env.HOME + '/Projects/fix-live-board/config/showroom-vendor.cjs');
+const { isShowroomVendor } = require(process.env.HOME + '/Projects/fix-live-board/config/showroom-vendor.cjs');
const GOOGLE_PUBLICATION = 'gid://shopify/Publication/29646651457'; // Google & YouTube
const SHOP = 'designer-laboratory-sandbox.myshopify.com';
@@ -34,11 +34,6 @@ const APPLY = args.apply === true && args['i-am-steve'] === true;
const REASON = args.reason || null;
const LIST = typeof args.list === 'string' ? args.list : 'unpublish-list.csv';
const SHOWROOM_ONLY = args['showroom-only'] === true;
-// TK-11307: tag-based guard for a SHARED-vendor showroom line (MDC under 'Phillipe Romano').
-// The vendor guard (--showroom-only) can't be used because the vendor is not a showroom
-// vendor. This guard LIVE-fetches each row's vendor+tags and requires isShowroomProduct
-// (vendor-on-list OR carries the 'Showroom' tag) — never trusts the CSV alone.
-const SHOWROOM_PRODUCT_ONLY = args['showroom-product-only'] === true;
const LIMIT = args.limit ? parseInt(args.limit,10) : Infinity;
const TOKEN = (fs.readFileSync(process.env.HOME+'/Projects/secrets-manager/.env','utf8').match(/^SHOPIFY_ADMIN_TOKEN=(.+)$/m)||[])[1]?.trim();
@@ -72,28 +67,6 @@ if (SHOWROOM_ONLY) {
process.exit(1);
}
}
-// HARD SHOWROOM-PRODUCT GUARD (TK-11307) — live-verify EVERY row is a showroom product
-// (vendor-on-list OR carries the 'Showroom' tag) before allowing a channel unpublish.
-if (SHOWROOM_PRODUCT_ONLY) {
- const TQ = `query($ids:[ID!]!){ nodes(ids:$ids){ ... on Product { id vendor tags } } }`;
- const bad = [];
- for (let i = 0; i < allRows.length; i += 100) {
- const batch = allRows.slice(i, i + 100);
- const ids = batch.map(r => `gid://shopify/Product/${r.id}`);
- const d = await gql(TQ, { ids });
- const byId = new Map((d.nodes || []).filter(Boolean).map(n => [n.id.split('/').pop(), n]));
- for (const r of batch) {
- const n = byId.get(r.id);
- if (!n || !isShowroomProduct({ vendor: n.vendor, tags: n.tags })) bad.push(r.id + (n ? `(${n.vendor})` : '(missing)'));
- }
- }
- if (bad.length) {
- console.error(`\n⛔ ABORT: --showroom-product-only set but ${bad.length}/${allRows.length} rows are NOT showroom products (no 'Showroom' tag and vendor not on list).`);
- console.error(` e.g. ${bad.slice(0,3).join(', ')}`);
- console.error(` This guard prevents unpublishing a sellable product. Re-run prep-showroom-unpublish-tag.mjs to rebuild the list.`);
- process.exit(1);
- }
-}
const rows = allRows
.filter(x => !REASON || x.reasonClass === REASON)
.slice(0, LIMIT);
@@ -104,7 +77,7 @@ console.log(`target publication: Google & YouTube (${GOOGLE_PUBLICATION})`);
console.log(`candidates: ${rows.length}${REASON ? ` (reason=${REASON})` : (SHOWROOM_ONLY ? ' (showroom-scoped set)' : ' (entire excluded set)')}`);
if (!APPLY) {
console.log('\nDRY-RUN: nothing will be unpublished. To execute (Steve only):');
- console.log(` node apply-unpublish.mjs --apply --i-am-steve --list=${LIST}${SHOWROOM_ONLY?' --showroom-only':''}${SHOWROOM_PRODUCT_ONLY?' --showroom-product-only':''}${REASON?` --reason=${REASON}`:''}`);
+ console.log(` node apply-unpublish.mjs --apply --i-am-steve --list=${LIST}${SHOWROOM_ONLY?' --showroom-only':''}${REASON?` --reason=${REASON}`:''}`);
process.exit(0);
}
diff --git a/scripts/google-feed/prep-showroom-unpublish-tag.mjs b/scripts/google-feed/prep-showroom-unpublish-tag.mjs
deleted file mode 100644
index dce8b08..0000000
--- a/scripts/google-feed/prep-showroom-unpublish-tag.mjs
+++ /dev/null
@@ -1,98 +0,0 @@
-#!/usr/bin/env node
-/**
- * prep-showroom-unpublish-tag.mjs — build a TAG-SCOPED unpublish list for the
- * Google & YouTube sales channel, for a SHARED-vendor showroom line. (TK-11307)
- *
- * WHY A SEPARATE TAG PATH: prep-showroom-unpublish.mjs is VENDOR-scoped
- * (isShowroomVendor + `vendor:'X'` query). MDC lives under the SHARED private label
- * "Phillipe Romano", which ALSO carries sellable lines — so it can only be selected by
- * its product-level "Showroom" tag, never by vendor. This tool queries
- * `status:active AND tag:'Showroom'`, re-affirms each product actually carries the tag
- * (hasShowroomTag), keeps only those actually published to the Google channel, and
- * writes a CSV in the SAME format apply-unpublish.mjs consumes.
- *
- * READ-ONLY against Shopify (query only). Writes one local CSV. Nothing is unpublished
- * here — apply-unpublish.mjs (Steve-gated) does that with --showroom-product-only.
- *
- * OUTPUT: data/google-feed/unpublish-list-showroom-tag.csv
- * header: id,handle,vendor,reason_class,reasons,admin_link
- *
- * USAGE: node prep-showroom-unpublish-tag.mjs [--tag=Showroom]
- */
-import fs from 'node:fs';
-import path from 'node:path';
-import { createRequire } from 'node:module';
-const require = createRequire(import.meta.url);
-const { hasShowroomTag } = require(process.env.HOME + '/Projects/fix-live-board/config/showroom-vendor.cjs');
-
-const args = Object.fromEntries(process.argv.slice(2).map(a => { const [k,v]=a.replace(/^--/,'').split('='); return [k, v===undefined?true:v]; }));
-const TAG = typeof args.tag === 'string' ? args.tag : 'Showroom';
-
-const SHOP = 'designer-laboratory-sandbox.myshopify.com';
-const VER = '2024-10';
-const STORE = 'designer-laboratory-sandbox';
-const GOOGLE_PUBLICATION = 'gid://shopify/Publication/29646651457'; // Google & YouTube
-const REASON_CLASS = 'showroom_only_addressable_not_discoverable';
-const adminLink = id => `https://admin.shopify.com/store/${STORE}/products/${id}`;
-
-const TOKEN = (fs.readFileSync(process.env.HOME + '/Projects/secrets-manager/.env', 'utf8')
- .match(/^SHOPIFY_ADMIN_TOKEN=(.+)$/m) || [])[1]?.trim();
-if (!TOKEN) { console.error('no token'); process.exit(1); }
-const ENDPOINT = `https://${SHOP}/admin/api/${VER}/graphql.json`;
-const sleep = ms => new Promise(r => setTimeout(r, ms));
-async function gql(query, variables) {
- for (let a = 0; a < 8; a++) {
- let j;
- try {
- const r = await fetch(ENDPOINT, { method: 'POST',
- headers: { 'X-Shopify-Access-Token': TOKEN, 'Content-Type': 'application/json' },
- body: JSON.stringify({ query, variables }) });
- j = await r.json();
- } catch (e) { await sleep(1500 * (a + 1)); continue; }
- if (j.errors) { if (JSON.stringify(j.errors).includes('THROTTLED')) { await sleep(2000 * (a + 1)); continue; } throw new Error(JSON.stringify(j.errors)); }
- const t = j.extensions?.cost?.throttleStatus;
- if (t && t.currentlyAvailable < 400) await sleep(1200);
- return j.data;
- }
- throw new Error('retries');
-}
-const csvEsc = s => `"${String(s == null ? '' : s).replace(/"/g, '""')}"`;
-
-const Q = `query($cursor:String,$q:String!){
- products(first:100, after:$cursor, query:$q){
- pageInfo{ hasNextPage endCursor }
- nodes{ id handle vendor tags onGoogle:publishedOnPublication(publicationId:"${GOOGLE_PUBLICATION}") }
- }
-}`;
-
-(async () => {
- const DIR = path.join(process.cwd(), 'data', 'google-feed');
- fs.mkdirSync(DIR, { recursive: true });
- const q = `status:active tag:'${String(TAG).replace(/'/g, "\\'")}'`;
- const rows = []; let cursor = null, has = true, seen = 0, onCh = 0, offCh = 0;
- while (has) {
- const d = await gql(Q, { cursor, q });
- for (const p of d.products.nodes) {
- seen++;
- // Belt-and-suspenders: re-affirm the tag from the returned tags array, never trust the query alone.
- if (!hasShowroomTag(p.tags)) continue;
- if (!p.onGoogle) { offCh++; continue; } // only products actually on the paid channel
- onCh++;
- const id = p.id.split('/').pop();
- rows.push([id, csvEsc(p.handle), csvEsc(p.vendor), REASON_CLASS, csvEsc(REASON_CLASS), adminLink(id)].join(','));
- }
- has = d.products.pageInfo.hasNextPage;
- cursor = d.products.pageInfo.endCursor;
- }
- const header = 'id,handle,vendor,reason_class,reasons,admin_link';
- const out = path.join(DIR, 'unpublish-list-showroom-tag.csv');
- fs.writeFileSync(out, header + '\n' + rows.join('\n') + (rows.length ? '\n' : ''));
-
- console.log('TAG-SCOPED showroom unpublish list (Google & YouTube channel only)');
- console.log(` tag: '${TAG}'`);
- console.log(` active tag:'${TAG}' seen: ${seen} | on Google channel: ${onCh} | already off channel: ${offCh}`);
- console.log(' TOTAL to unpublish:', rows.length);
- console.log(' wrote:', out);
- console.log('\n Fire (Steve only):');
- console.log(' node apply-unpublish.mjs --apply --i-am-steve --list=unpublish-list-showroom-tag.csv --showroom-product-only');
-})();
← b8f7b46 Revert "google-feed: exclude showroom-tagged products via is
·
back to Designerwallcoverings
·
auto-data-snapshot: 2026-09-09T08:26:36 (3 data files) — scr ffd3c22 →