← 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
A shopify/scripts/check-collection-publications.js
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 →