[object Object]

← back to Dw Validator Debug TK11314

Add check-collection-publications.js diagnostic (TK-10222/TK-10211)

85480a7e21569d66359ee6c4e56ece1584679335 · 2026-08-06 04:10:13 -0700 · Steve

Dry-run script: finds 136 collections (55,813 products) not published to Online Store.
Key unpublished vendor wallcovering collections: Designers Guild (878), Fromental (670),
Koroseal (2548), Paul Montgomery (1545), Et Cie (487), Zuber (162).
Run: node shopify/scripts/check-collection-publications.js --dry-run

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

Files touched

Diff

commit 85480a7e21569d66359ee6c4e56ece1584679335
Author: Steve <steve@designerwallcoverings.com>
Date:   Thu Aug 6 04:10:13 2026 -0700

    Add check-collection-publications.js diagnostic (TK-10222/TK-10211)
    
    Dry-run script: finds 136 collections (55,813 products) not published to Online Store.
    Key unpublished vendor wallcovering collections: Designers Guild (878), Fromental (670),
    Koroseal (2548), Paul Montgomery (1545), Et Cie (487), Zuber (162).
    Run: node shopify/scripts/check-collection-publications.js --dry-run
    
    Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---
 shopify/scripts/check-collection-publications.js | 239 +++++++++++++++++++++++
 1 file changed, 239 insertions(+)

diff --git a/shopify/scripts/check-collection-publications.js b/shopify/scripts/check-collection-publications.js
new file mode 100644
index 00000000..af79bf62
--- /dev/null
+++ b/shopify/scripts/check-collection-publications.js
@@ -0,0 +1,239 @@
+#!/usr/bin/env node
+/**
+ * TK-10222 / TK-10211 — Collection publication diagnostic + fix
+ *
+ * Fetches every smart + custom collection, checks whether each is published
+ * to the Online Store (publication id: 22208643184), and reports which are
+ * NOT visible to customers.
+ *
+ * Usage:
+ *   node check-collection-publications.js              # dry-run (default)
+ *   node check-collection-publications.js --dry-run    # explicit dry-run
+ *   node check-collection-publications.js --apply      # re-publish missing collections
+ *   node check-collection-publications.js --vendor "Designers Guild"  # filter by title
+ *
+ * Risk: LOW — only ADDS publications, never removes or modifies products.
+ * This script is idempotent: re-running after --apply is a no-op for already-
+ * published collections.
+ */
+
+const https = require('https');
+const fs = require('fs');
+const os = require('os');
+const path = require('path');
+
+const args = process.argv.slice(2);
+const APPLY = args.includes('--apply');
+const VENDOR_FILTER = (() => {
+  const i = args.indexOf('--vendor');
+  return i >= 0 ? args[i + 1]?.toLowerCase() : null;
+})();
+
+// Canonical Online Store publication id (from active-2026-and-all-channels.js PUBS list)
+const ONLINE_STORE_PUB_ID = '22208643184';
+const ONLINE_STORE_PUB_GID = `gid://shopify/Publication/${ONLINE_STORE_PUB_ID}`;
+
+// Load token from secrets-manager
+const envPath = path.join(os.homedir(), 'Projects', 'secrets-manager', '.env');
+const envContent = fs.readFileSync(envPath, 'utf8');
+const TOKEN = (envContent.match(/^SHOPIFY_ADMIN_TOKEN=(.*)$/m) || [])[1]?.replace(/['"]/g, '').trim();
+if (!TOKEN) { console.error('ERROR: SHOPIFY_ADMIN_TOKEN not found in secrets-manager/.env'); process.exit(1); }
+
+const STORE = 'designer-laboratory-sandbox.myshopify.com';
+const API = '2024-10';
+const sleep = ms => new Promise(r => setTimeout(r, ms));
+
+function gql(query, variables = {}) {
+  return new Promise((resolve, reject) => {
+    const body = JSON.stringify({ query, variables });
+    const req = https.request({
+      host: STORE,
+      path: `/admin/api/${API}/graphql.json`,
+      method: 'POST',
+      headers: {
+        'Content-Type': 'application/json',
+        'X-Shopify-Access-Token': TOKEN,
+        'Content-Length': Buffer.byteLength(body),
+      },
+    }, r => {
+      let d = '';
+      r.on('data', c => d += c);
+      r.on('end', () => { try { resolve(JSON.parse(d)); } catch (e) { reject(d); } });
+    });
+    req.on('error', reject);
+    req.write(body);
+    req.end();
+  });
+}
+
+async function gqlR(q, v = {}, tries = 5) {
+  for (let i = 0; i < tries; i++) {
+    let r;
+    try { r = await gql(q, v); } catch (e) { r = { errors: [{ message: String(e).slice(0, 120) }] }; }
+    if (r && !r.errors) return r;
+    console.warn(`  [retry ${i + 1}/${tries}]`, JSON.stringify(r.errors || []).slice(0, 200));
+    await sleep(1500 * (i + 1));
+  }
+  return gql(q, v);
+}
+
+// GraphQL queries for collections
+// publishedOnPublication is the canonical check used by active-2026-and-all-channels.js
+const Q_SMART = `
+  query($cursor: String) {
+    smartCollections(first: 50, after: $cursor) {
+      pageInfo { hasNextPage endCursor }
+      nodes {
+        id
+        title
+        handle
+        productsCount { count }
+        publishedOnPublication(publicationId: "${ONLINE_STORE_PUB_GID}")
+      }
+    }
+  }
+`;
+
+const Q_CUSTOM = `
+  query($cursor: String) {
+    collections(first: 50, after: $cursor) {
+      pageInfo { hasNextPage endCursor }
+      nodes {
+        id
+        title
+        handle
+        productsCount { count }
+        publishedOnPublication(publicationId: "${ONLINE_STORE_PUB_GID}")
+      }
+    }
+  }
+`;
+
+const M_PUBLISH = `
+  mutation($id: ID!, $input: [PublicationInput!]!) {
+    publishablePublish(id: $id, input: $input) {
+      userErrors { field message }
+    }
+  }
+`;
+
+async function fetchAllPages(queryTemplate, dataKey) {
+  const results = [];
+  let cursor = null;
+  while (true) {
+    const r = await gqlR(queryTemplate, { cursor });
+    if (r.errors) {
+      console.error('GraphQL error:', JSON.stringify(r.errors).slice(0, 300));
+      break;
+    }
+    const page = r.data[dataKey];
+    results.push(...page.nodes);
+    if (!page.pageInfo.hasNextPage) break;
+    cursor = page.pageInfo.endCursor;
+    await sleep(200); // gentle rate throttle
+  }
+  return results;
+}
+
+(async () => {
+  console.log(`\n=== check-collection-publications.js ===`);
+  console.log(`Mode       : ${APPLY ? 'APPLY (will re-publish missing collections)' : 'DRY-RUN (no writes)'}`);
+  console.log(`Store      : ${STORE}`);
+  console.log(`Online Store publication : gid://shopify/Publication/${ONLINE_STORE_PUB_ID}`);
+  if (VENDOR_FILTER) console.log(`Filter     : title contains "${VENDOR_FILTER}"`);
+  console.log('');
+
+  // Fetch smart collections (vendor/tag/rule-based)
+  console.log('Fetching smart collections...');
+  let smartColls = await fetchAllPages(Q_SMART, 'smartCollections');
+  console.log(`  Found ${smartColls.length} smart collections`);
+
+  // Fetch custom collections (manually curated)
+  console.log('Fetching custom collections...');
+  let customColls = await fetchAllPages(Q_CUSTOM, 'collections');
+  // De-duplicate: custom collections query returns ALL collections including smart ones
+  // Filter to only those not already in smartColls
+  const smartIds = new Set(smartColls.map(c => c.id));
+  const customOnly = customColls.filter(c => !smartIds.has(c.id));
+  console.log(`  Found ${customOnly.length} custom-only collections (after de-dup)`);
+
+  const allCollections = [...smartColls, ...customOnly];
+  console.log(`\nTotal collections: ${allCollections.length}`);
+
+  // Apply optional title filter
+  let filtered = allCollections;
+  if (VENDOR_FILTER) {
+    filtered = allCollections.filter(c => c.title.toLowerCase().includes(VENDOR_FILTER));
+    console.log(`Filtered to ${filtered.length} matching "${VENDOR_FILTER}"`);
+  }
+
+  // Partition into published vs unpublished
+  const published = filtered.filter(c => c.publishedOnPublication);
+  const unpublished = filtered.filter(c => !c.publishedOnPublication);
+
+  const totalUnpublishedProducts = unpublished.reduce((sum, c) => sum + (c.productsCount?.count || 0), 0);
+
+  console.log(`\n--- Results ---`);
+  console.log(`Published to Online Store  : ${published.length}`);
+  console.log(`NOT published (invisible)  : ${unpublished.length}  (${totalUnpublishedProducts} products hidden from customers)`);
+
+  if (unpublished.length === 0) {
+    console.log('\nAll collections are published to the Online Store. Nothing to fix.');
+    process.exit(0);
+  }
+
+  // Report the unpublished collections
+  console.log('\nUnpublished collections:');
+  console.log('  #  | Products | Title                                         | Handle');
+  console.log('-----|----------|-----------------------------------------------|----------------------------');
+  unpublished.forEach((c, i) => {
+    const num = String(i + 1).padStart(3);
+    const count = String(c.productsCount?.count || 0).padStart(8);
+    const title = c.title.padEnd(45).slice(0, 45);
+    console.log(`  ${num} | ${count} | ${title} | ${c.handle}`);
+  });
+
+  console.log(`\nTotal hidden products: ${totalUnpublishedProducts}`);
+
+  if (!APPLY) {
+    console.log('\nDRY-RUN complete. No changes made.');
+    console.log('To fix: node check-collection-publications.js --apply');
+    process.exit(0);
+  }
+
+  // === APPLY MODE: re-publish each unpublished collection ===
+  console.log('\n--- Applying fixes (publishing to Online Store) ---');
+  const OUTDIR = path.join(__dirname, 'data', 'collection-pub-fix');
+  fs.mkdirSync(OUTDIR, { recursive: true });
+  const LOGFILE = path.join(OUTDIR, `run-${new Date().toISOString().replace(/[:.]/g, '-')}.jsonl`);
+
+  let fixed = 0, errors = 0;
+  for (const c of unpublished) {
+    process.stdout.write(`  Publishing: ${c.title.slice(0, 60)} ... `);
+    const r = await gqlR(M_PUBLISH, {
+      id: c.id,
+      input: [{ publicationId: ONLINE_STORE_PUB_GID }],
+    });
+    const ue = r.data?.publishablePublish?.userErrors || [];
+    if (ue.length) {
+      console.log(`ERROR: ${ue.map(e => e.message).join('; ')}`);
+      errors++;
+      fs.appendFileSync(LOGFILE, JSON.stringify({ status: 'error', id: c.id, title: c.title, errors: ue }) + '\n');
+    } else {
+      console.log(`OK (${c.productsCount?.count || 0} products now visible)`);
+      fixed++;
+      fs.appendFileSync(LOGFILE, JSON.stringify({ status: 'fixed', id: c.id, title: c.title, products: c.productsCount?.count }) + '\n');
+    }
+    await sleep(200); // stay well within rate limits
+  }
+
+  console.log(`\n=== DONE ===`);
+  console.log(`Fixed  : ${fixed} collections`);
+  console.log(`Errors : ${errors}`);
+  console.log(`Log    : ${LOGFILE}`);
+
+  if (errors > 0) {
+    console.log('\nSome collections failed. Check the log and re-run --apply to retry (script is idempotent).');
+    process.exit(1);
+  }
+})();

← f7dd3f04 auto-save: 2026-08-06T03:18:00 (1 files) — DW-Programming/Im  ·  back to Dw Validator Debug TK11314  ·  auto-save: 2026-08-06T05:18:46 (1 files) — shopify/scripts/c 596d519c →