← back to Interiordesignershowroom
scripts/ingest-cj-catalog.js
78 lines
// Scaled CJ catalog ingest for the Room Builder. Pulls a broad, image-required
// set across furniture / lighting / rugs / decor / appliances, normalizes, and
// UPSERTs into products. Idempotent (network+external_id key) so re-runs refresh
// prices AND swap in tracked links as advertisers get joined in CJ.
//
// Usage: node scripts/ingest-cj-catalog.js [--per=60] [--only=sofa,desk]
try { require('dotenv').config(); } catch (_) {}
const db = require('../lib/db');
const cj = require('../lib/adapters/cj');
const { normalizeProduct, UPSERT_SQL, upsertParams } = require('../lib/normalize');
const arg = (n, d) => { const h = process.argv.find(a => a.startsWith(`--${n}=`)); return h ? h.split('=')[1] : d; };
const sleep = ms => new Promise(r => setTimeout(r, ms));
// Category -> search keyword. Broad home-furnishing coverage + appliances.
const CATEGORIES = [
'sofa', 'sectional sofa', 'loveseat', 'accent chair', 'recliner', 'ottoman',
'coffee table', 'console table', 'side table', 'media console', 'tv stand',
'office chair', 'ergonomic office chair', 'standing desk', 'writing desk', 'bookcase',
'bed frame', 'upholstered headboard', 'nightstand', 'dresser', 'wardrobe',
'dining table', 'dining chair', 'bar stool', 'counter stool', 'sideboard buffet', 'bar cart',
'area rug', 'runner rug', 'floor lamp', 'table lamp', 'pendant light', 'chandelier', 'wall sconce',
'wall mirror', 'framed wall art', 'decorative vase', 'throw pillow', 'throw blanket', 'wall clock',
'planter', 'bookshelf', 'bench', 'vitamix blender', 'stand mixer', 'espresso machine', 'air purifier',
// Smart-cleaning appliances. Honiture switched OFF 2026-08-03 (Steve) — kept in
// the catalog but hidden via affiliate_settings (cj/Honiture, enabled=FALSE), so
// its brand keyword is dropped here to stop re-sweeping a source that won't show.
// To bring it back: re-add 'honiture' below AND turn it ON in /admin/affiliates.
'robot vacuum', 'cordless vacuum',
];
// Joined advertisers swept by partnerIds (whole datafeed, always tracked) —
// keyword search can't reach these: 'curtains' is saturated by unjoined giants
// (SHEIN/Temu/Wayfair) and the brand keyword only surfaces resellers.
// TWOPAGES joined 2026-08-02 (CJ welcome email); feed may lag the join by
// 24-48h, so a zero sweep is expected until CJ indexes it — re-runs pick it up.
const PARTNERS = [
{ id: '7835575', name: 'twopages' }, // custom curtains, 10% commission
];
async function main() {
await db.query('SELECT 1');
if (!cj.enabled(process.env)) { console.error('CJ not configured (need CJ_TOKEN/CJ_COMPANY_ID/CJ_WEBSITE_ID)'); process.exit(1); }
const per = parseInt(arg('per', '60'), 10);
const only = (arg('only', '') || '').split(',').filter(Boolean);
const cats = only.length ? CATEGORIES.filter(c => only.some(o => c.includes(o))) : CATEGORIES;
const partners = only.length ? PARTNERS.filter(p => only.some(o => p.name.includes(o))) : PARTNERS;
let total = 0, withImg = 0, tracked = 0;
const sweep = async (label, fetchOpts) => {
let raws = [];
try { raws = await cj.fetch(process.env, fetchOpts); }
catch (e) { console.log(` ${label}: ERROR ${e.message}`); return; }
let ok = 0;
for (const raw of raws) {
if (!raw.image_url) continue; // Room Builder needs images
withImg++;
if (/cj\.com|dpbolvw|anrdoezrs|kqzyfj|tkqlhce|jdoqocy/.test(raw.affiliate_url || '')) tracked++;
const norm = normalizeProduct(raw, 'cj');
if (!norm) continue;
try { await db.query(UPSERT_SQL, upsertParams(norm)); ok++; total++; } catch (_) {}
}
console.log(` ${label.padEnd(24)} -> ${ok} upserted (of ${raws.length})`);
await sleep(400); // pace the API
};
for (const cat of cats) await sweep(cat, { limit: per, keywords: cat });
// Partner-scoped sweeps pull the advertiser's whole feed (up to the cap) so a
// freshly-joined niche program lands even when keywords can't reach it.
for (const p of partners) await sweep(`partner:${p.name}`, { limit: Math.max(per, 500), partnerIds: [p.id] });
const { rows } = await db.query(`SELECT count(*) n, count(image_url) img FROM products WHERE network='cj'`);
console.log(`\nDONE. this run: ${total} upserted, ${tracked} with tracked links.`);
console.log(`catalog now: ${rows[0].n} CJ products (${rows[0].img} with images).`);
await db.pool.end();
}
main().catch(e => { console.error(e); process.exit(1); });