← back to Greenland Onboard
scripts/scout-full-catalog.mjs
75 lines
#!/usr/bin/env node
// Feed-first scout ($0): walk /product/<id> for the full Greenland catalog (Steve's
// "id=64 → id=300 and more, increase by 1" directive). Writes data/full-catalog.ndjson
// (one line per real collection) and prints a summary grouped by material class so we
// can plan the full onboard (which lines are cork vs silk vs grass vs sisal, colorway
// counts, what's already onboarded). Pure HTTP GET — no auth, no browser.
import { writeFileSync } from 'node:fs';
import { fileURLToPath } from 'node:url';
import { dirname, join } from 'node:path';
const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..');
const API = 'https://api.greenlandwallcoverings.com/api';
const LO = Number(process.argv[2] || 1);
const HI = Number(process.argv[3] || 340);
const CONC = 8;
const materialOf = (d) => {
const wc = (d.subCategories || []).find(s => s.category_type === 'wallClass');
return wc?.category_name || '(uncat)';
};
const wallTypeOf = (d) => {
const wt = (d.subCategories || []).find(s => s.category_type === 'wallType');
return wt?.category_name || '';
};
async function fetchId(id) {
try {
const r = await fetch(`${API}/product/${id}`, { signal: AbortSignal.timeout(20000) });
if (!r.ok) return null;
const j = await r.json();
if (j.code !== 0 || !j.data || !j.data.id) return null;
return j.data;
} catch { return null; }
}
const rows = [];
let id = LO;
async function worker() {
while (id <= HI) {
const myId = id++;
const d = await fetchId(myId);
if (d) {
rows.push({
id: d.id, name: d.name, code: d.code,
material: materialOf(d), wallType: wallTypeOf(d),
colorways: (d.skus || []).length,
skus: (d.skus || []).map(s => ({ model: s.model, color: s.colorDescription, thumb: s.thumbnail })),
coverImg: d.coverImg,
});
}
}
}
await Promise.all(Array.from({ length: CONC }, worker));
rows.sort((a, b) => Number(a.id) - Number(b.id));
const out = join(ROOT, 'data', 'full-catalog.ndjson');
writeFileSync(out, rows.map(r => JSON.stringify(r)).join('\n') + '\n');
// summary
const byMat = {};
let totalCw = 0;
for (const r of rows) {
const m = r.material;
byMat[m] = byMat[m] || { collections: 0, colorways: 0, codes: new Set() };
byMat[m].collections++; byMat[m].colorways += r.colorways; byMat[m].codes.add(r.code);
totalCw += r.colorways;
}
console.log(`\nScanned ids ${LO}..${HI} → ${rows.length} real collections, ${totalCw} colorway SKUs total\n`);
console.log('MATERIAL COLLECTIONS COLORWAYS');
console.log('----------------------- ----------- ---------');
for (const [m, v] of Object.entries(byMat).sort((a, b) => b[1].colorways - a[1].colorways)) {
console.log(`${m.padEnd(23)} ${String(v.collections).padStart(11)} ${String(v.colorways).padStart(9)}`);
}
console.log(`\nwrote ${out}`);