← back to Fix Live Board
probes/shopify-flock.mjs
69 lines
#!/usr/bin/env node
// Flock probe — the reference impl. Ports flock-fix-viewer's live Shopify derivation into the
// generalized row shape. Reads the live DW store (designer-laboratory-sandbox). READ-ONLY.
// Token resolution: env SHOPIFY_ADMIN_TOKEN -> secrets-manager/.env -> flock-fix-viewer/.token
import https from 'https';
import fs from 'fs';
import path from 'path';
import os from 'os';
const SHOP = 'designer-laboratory-sandbox.myshopify.com';
function token() {
if (process.env.SHOPIFY_ADMIN_TOKEN) return process.env.SHOPIFY_ADMIN_TOKEN.trim();
try {
const env = fs.readFileSync(path.join(os.homedir(), 'Projects/secrets-manager/.env'), 'utf8');
const m = env.match(/^SHOPIFY_ADMIN_TOKEN=(.+)$/m); if (m) return m[1].trim();
} catch (e) {}
return fs.readFileSync(path.join(os.homedir(), 'Projects/flock-fix-viewer/.token'), 'utf8').trim();
}
const TOKEN = token();
const QUERY = "vendor:'Phillipe Romano' status:active (tag:'Flock Velvet' OR handle:flock OR title:flock)";
const PRODUCT_Q = `query($c:String){
products(first:100, query:${JSON.stringify(QUERY)}, after:$c){
pageInfo{hasNextPage endCursor}
edges{node{ handle title status createdAt featuredImage{url} tags
variants(first:8){edges{node{sku title price}}}
mMin: metafield(namespace:"global", key:"v_prods_quantity_order_min"){value}
mWidth: metafield(namespace:"global", key:"width"){value}
}}
}
}`;
function gql(query, variables) {
return new Promise((resolve, reject) => {
const body = JSON.stringify({ query, variables: variables || {} });
const req = https.request({ host: SHOP, path: '/admin/api/2024-10/graphql.json', method: 'POST',
headers: { 'X-Shopify-Access-Token': TOKEN, 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(body) } },
res => { let d = ''; res.on('data', c => d += c); res.on('end', () => { try { resolve(JSON.parse(d)); } catch (e) { reject(e); } }); });
req.on('error', reject); req.write(body); req.end();
});
}
(async () => {
let cur = null, all = [];
do {
const r = await gql(PRODUCT_Q, { c: cur });
const p = r && r.data && r.data.products; if (!p) break;
all = all.concat(p.edges);
cur = p.pageInfo.hasNextPage ? p.pageInfo.endCursor : null;
} while (cur);
const rows = all.map(e => {
const n = e.node, tags = n.tags || [];
const hasQuotes = tags.some(t => String(t).replace(/["{}]/g, '').trim().toLowerCase() === 'quotes');
const vs = (n.variants.edges || []).map(v => v.node);
const roll = vs.filter(v => !/sample/i.test(v.title || ''));
const rollPrice = roll.reduce((m, v) => Math.max(m, parseFloat(v.price) || 0), 0);
const sku = (roll[0] && roll[0].sku) || (vs[0] && vs[0].sku) || '?';
const isDup = n.handle.startsWith('copy-of-') || /-\d+$/.test(n.handle);
const hasPrice = rollPrice > 10;
return {
id: sku, sku, handle: n.handle, title: n.title, status: n.status, price: rollPrice,
img: (n.featuredImage && n.featuredImage.url) || null, created: n.createdAt,
fields: { quotes: !hasQuotes, price: hasPrice, badge: !hasQuotes,
buyable: !hasQuotes && hasPrice && n.status === 'ACTIVE',
min: !!(n.mMin && n.mMin.value), width: !!(n.mWidth && n.mWidth.value),
colorway: !isDup },
fixed: !hasQuotes && hasPrice
};
});
process.stdout.write(JSON.stringify(rows));
})().catch(e => { process.stderr.write(String(e)); process.exit(1); });