← back to 1960swallpaper
scripts/populate-catalog.js
81 lines
#!/usr/bin/env node
/**
* Stage-2 one-shot: populate dw_unified.microsite_products for 1960swallpaper.
* Pulls genuine 1960s-era wallcoverings from the DW master catalog
* (shopify_products) and inserts them so the site's niche filter has stock.
*/
const fs = require('fs');
const path = require('path');
const { Pool } = require(path.join(__dirname, '..', '..', '_shared', 'node_modules', 'pg'));
const { classifyAesthetic } = require(path.join(__dirname, '..', '..', '_shared', 'microsite-aesthetic.js'));
const siteCfg = JSON.parse(fs.readFileSync(path.join(__dirname, '..', 'site.config.json'), 'utf8'));
const SITE_SLUG = siteCfg.slug;
const RAILS = Array.isArray(siteCfg.rails) ? siteCfg.rails : [];
const CAP = 150;
const pool = new Pool({ host: process.env.PGHOST || '/tmp', database: 'dw_unified' });
(async () => {
// Strongest 1960s signal first (rank 0), mid-century modern last (rank 1),
// so a 150-cap leans toward the most on-theme stock.
const { rows } = await pool.query(`
SELECT DISTINCT ON (coalesce(sku, handle))
sku, handle, title, vendor, product_type, image_url, tags,
retail_price, price,
CASE WHEN title ~* '1960|\\mpop art\\M|\\mop art\\M|psychedelic|space.?age|\\matomic\\M'
OR tags ~* '1960|pop art|op art|psychedelic|space.?age|atomic'
THEN 0 ELSE 1 END AS rank
FROM shopify_products
WHERE status = 'ACTIVE' AND image_url IS NOT NULL AND image_url <> ''
AND product_type ILIKE '%Wallcovering%'
AND title !~* 'lamp|rug|pillow|throw|tripod|frame|mirror|vase|candle|sculpture|figurine'
AND (title ~* '1960|\\mpop art\\M|\\mop art\\M|psychedelic|space.?age|\\matomic\\M'
OR tags ~* '1960|pop art|op art|psychedelic|space.?age|atomic|mid.century modern')
AND lower(title || ' ' || coalesce(tags,'')) ~ 'mod|op art|op-art|pop|geometric|psychedelic|1960'
AND lower(title || ' ' || coalesce(tags,'')) !~ 'solid|blank|plain'
ORDER BY coalesce(sku, handle)
`);
// Re-sort by rank (strongest signal first), stable tiebreak on sku/handle,
// then trim to cap — deterministic so re-runs pick the same 150.
rows.sort((a, b) => a.rank - b.rank
|| String(a.sku || a.handle).localeCompare(String(b.sku || b.handle)));
const picked = rows.slice(0, CAP);
// Idempotent: drop any prior catalog-sourced rows for this site so a re-run
// with a changed query/cap doesn't leave stale extras behind.
await pool.query(
"DELETE FROM microsite_products WHERE site_slug=$1 AND source='catalog'",
[SITE_SLUG]);
let inserted = 0;
for (let i = 0; i < picked.length; i++) {
const p = picked[i];
const tags = typeof p.tags === 'string'
? p.tags.split(',').map(t => t.trim()).filter(Boolean)
: (Array.isArray(p.tags) ? p.tags : []);
const aesthetic = classifyAesthetic(tags, RAILS);
const skuKey = p.sku || p.handle;
await pool.query(`
INSERT INTO microsite_products
(site_slug, sku, source, title, vendor, product_type, handle,
image_url, tags, aesthetic, max_price, product_url, sort_order)
VALUES ($1,$2,'catalog',$3,$4,$5,$6,$7,$8,$9,$10,$11,$12)
ON CONFLICT (site_slug, sku) DO UPDATE SET
title=EXCLUDED.title, vendor=EXCLUDED.vendor, product_type=EXCLUDED.product_type,
handle=EXCLUDED.handle, image_url=EXCLUDED.image_url, tags=EXCLUDED.tags,
aesthetic=EXCLUDED.aesthetic, max_price=EXCLUDED.max_price,
product_url=EXCLUDED.product_url, visible=true, updated_at=now()
`, [SITE_SLUG, skuKey, p.title, p.vendor, p.product_type, p.handle,
p.image_url, tags, aesthetic, p.retail_price || p.price || null,
p.handle ? `https://designerwallcoverings.com/products/${p.handle}` : null, i]);
inserted++;
}
const { rows: cnt } = await pool.query(
'SELECT count(*) FROM microsite_products WHERE site_slug=$1', [SITE_SLUG]);
console.log(`candidates=${rows.length} picked=${picked.length} inserted=${inserted} total_now=${cnt[0].count}`);
await pool.end();
})().catch(e => { console.error(e); process.exit(1); });