← back to Stars of Design
scrapers/apply-vendor-blocklist.js
80 lines
#!/usr/bin/env node
'use strict';
// Tick 2 correction — Apply the vendor blocklist to existing sod_email_candidates
// rows that previously got auto-promoted to status='is_designer'/'is_firm' but
// are actually vendors. Steve's standing rule: StarsOfDesign surfaces DW CLIENTS
// (designers/firms who BUY from DW), NOT vendors (manufacturers we buy from).
//
// Run modes:
// node scrapers/apply-vendor-blocklist.js # apply
// node scrapers/apply-vendor-blocklist.js --dry # report only
require('dotenv').config();
const { Pool } = require('pg');
const fs = require('fs');
const path = require('path');
const DRY = process.argv.includes('--dry');
const pool = new Pool({
connectionString: process.env.DATABASE_URL || 'postgresql:///dw_unified?host=/tmp&user=stevestudio2',
max: 4,
});
const blocklist = JSON.parse(fs.readFileSync(path.join(__dirname, 'vendor-blocklist.json'), 'utf8'));
const VENDOR_DOMAINS = new Set(blocklist.vendor_domains.map(d => d.toLowerCase()));
const DEAD_DOMAINS = new Set(blocklist.dead_domains.map(d => d.toLowerCase()));
async function main() {
const r = await pool.query(`
SELECT id, email, domain, status, signal_score
FROM sod_email_candidates
WHERE status IN ('new','is_designer','is_firm')`);
let toVendor = 0, toDead = 0;
const previews = [];
for (const c of r.rows) {
const dom = (c.domain || c.email.split('@')[1] || '').toLowerCase();
if (DEAD_DOMAINS.has(dom)) {
previews.push(` DEAD ${c.email}`);
if (!DRY) await pool.query(
`UPDATE sod_email_candidates
SET status='is_noise',
signal_score=0,
notes=CONCAT_WS(' || ', NULLIF(notes,''), 'blocklist:dead-domain'::text),
updated_at=NOW()
WHERE id=$1`, [c.id]);
toDead++;
} else if (VENDOR_DOMAINS.has(dom)) {
previews.push(` VENDOR ${c.email}`);
if (!DRY) await pool.query(
`UPDATE sod_email_candidates
SET status='is_vendor',
notes=CONCAT_WS(' || ', NULLIF(notes,''), 'blocklist:vendor-domain'::text),
updated_at=NOW()
WHERE id=$1`, [c.id]);
toVendor++;
}
}
// Also kill any sod_designers/sod_firms rows whose primary_email is on the blocklist
let killedDesigners = 0, killedFirms = 0;
if (!DRY) {
const matchClause = [...VENDOR_DOMAINS, ...DEAD_DOMAINS].map(d => `'${d}'`).join(',');
const kd = await pool.query(`
DELETE FROM sod_designers
WHERE primary_email IS NOT NULL
AND split_part(primary_email, '@', 2) IN (${matchClause})
RETURNING id`);
killedDesigners = kd.rowCount;
const kf = await pool.query(`
DELETE FROM sod_firms
WHERE primary_email IS NOT NULL
AND split_part(primary_email, '@', 2) IN (${matchClause})
RETURNING id`);
killedFirms = kf.rowCount;
}
console.log(previews.slice(0, 40).join('\n') + (previews.length > 40 ? `\n …+${previews.length - 40} more` : ''));
console.log(`[blocklist] candidates: vendor=${toVendor} dead=${toDead} removed: designers=${killedDesigners} firms=${killedFirms} dry=${DRY}`);
await pool.end();
}
main().catch(e => { console.error('fatal:', e); process.exit(1); });