← back to Hollywood Import
momentum-feed/refresh.mjs
100 lines
#!/usr/bin/env node
// Momentum → Hollywood feed-first refresh (SAFE, non-clobbering).
// Pages the storefront Meilisearch index `redesign-colors`, keeps only
// Wallcovering + Acoustic colorways, and upserts into momentum_colorways:
// - INSERT new (pattern_name,color_name) rows (staging only; no dw_sku yet)
// - On existing rows, refresh ONLY volatile fields (price/width/image/new/closeout).
// NEVER overwrites dw_sku, ai_*, description, shopify_*, pl_* (curation is sacred).
// $0 — plain HTTPS. Run: NODE_PATH=.../AbramsOS/node_modules node refresh.mjs [--commit]
import { createRequire } from 'module';
const require = createRequire(import.meta.url);
const { Pool } = require('pg');
const HOST = 'https://ms-e886719d86e7-4256.sfo.meilisearch.io';
// Search key is hardcoded in momentumco.com app.js (meta tag is a decoy). Re-grab if it rotates:
// curl -s .../build/assets/app-*.js | grep -oE 'initializeMeiliSearch\("https[^"]+","[0-9a-f]{64}"'
const KEY = process.env.MOMENTUM_MS_KEY || '95fe8376edc78d49e787db49edda68426b21649f6271f49e2f1412118612fbd6';
const INDEX = 'redesign-colors';
const KEEP = new Set(['Wallcovering', 'Acoustic']);
const COMMIT = process.argv.includes('--commit');
const pool = new Pool({ connectionString: 'postgresql://dw_admin:DW2024!@127.0.0.1:5432/dw_unified' });
async function fetchPage(offset, limit = 200) {
const r = await fetch(`${HOST}/indexes/${INDEX}/search`, {
method: 'POST',
headers: { Authorization: `Bearer ${KEY}`, 'Content-Type': 'application/json' },
body: JSON.stringify({ q: '', limit, offset,
attributesToRetrieve: ['id','number','pattern_name','preferred_color_name','preferred_color_number',
'category_name','product_line','price','base_width','medium_image_url','new','series','collections'] }),
});
if (!r.ok) throw new Error(`feed ${r.status} at offset ${offset}`);
return (await r.json()).hits || [];
}
async function main() {
const rows = [];
let rawSeen = 0, lastOff = 0;
for (let off = 0; off < 20000; off += 200) {
const hits = await fetchPage(off);
if (!hits.length) break;
rawSeen += hits.length; lastOff = off + hits.length;
for (const h of hits) {
if (!KEEP.has(h.category_name)) continue;
if (!h.pattern_name || !h.preferred_color_name) continue;
rows.push({
pattern_name: h.pattern_name,
color_name: h.preferred_color_name,
color_number: h.preferred_color_number || null,
momentum_sku: h.number || null,
image_url: h.medium_image_url || null,
list_price: (h.price != null ? Number(h.price) : null),
width: (h.base_width != null ? String(h.base_width) : null),
category: h.category_name,
product_line: h.product_line?.code || null,
is_new: !!h.new,
collection_name: h.series || (Array.isArray(h.collections) ? h.collections[0]?.name : null) || null,
});
}
process.stdout.write(`\r fetched offset ${off} kept ${rows.length} `);
}
console.log(`\nFeed kept ${rows.length} Wallcovering+Acoustic colorways (scanned ${rawSeen} raw records).`);
if (lastOff >= 20000 || rawSeen >= 20000)
console.warn('⚠️ WARNING: hit Meilisearch maxTotalHits=20000 cap — index may be TRUNCATED; ' +
'WC/Acoustic records beyond the first 20k are unreachable via offset paging. ' +
'Shard the query (e.g. per product_line) if the true index exceeds 20k.');
const cats = rows.reduce((a, r) => (a[r.category] = (a[r.category] || 0) + 1, a), {});
console.log(' by category:', JSON.stringify(cats));
if (!COMMIT) { console.log('DRY-RUN (no DB writes). Re-run with --commit to apply.'); await pool.end(); return; }
const c = await pool.connect();
let inserted = 0, updated = 0;
try {
await c.query('BEGIN');
for (const r of rows) {
const res = await c.query(
`INSERT INTO momentum_colorways
(pattern_name,color_name,color_number,momentum_sku,image_url,list_price,width,category,product_line,is_new,collection_name,created_at,updated_at)
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,now(),now())
ON CONFLICT (pattern_name,color_name) DO UPDATE SET
momentum_sku = COALESCE(EXCLUDED.momentum_sku, momentum_colorways.momentum_sku),
image_url = COALESCE(EXCLUDED.image_url, momentum_colorways.image_url),
list_price = COALESCE(EXCLUDED.list_price, momentum_colorways.list_price),
width = COALESCE(EXCLUDED.width, momentum_colorways.width),
category = COALESCE(EXCLUDED.category, momentum_colorways.category),
product_line = COALESCE(EXCLUDED.product_line, momentum_colorways.product_line),
is_new = EXCLUDED.is_new,
collection_name= COALESCE(momentum_colorways.collection_name, EXCLUDED.collection_name),
updated_at = now()
RETURNING (xmax = 0) AS is_insert`,
[r.pattern_name, r.color_name, r.color_number, r.momentum_sku, r.image_url,
r.list_price, r.width, r.category, r.product_line, r.is_new, r.collection_name]);
res.rows[0].is_insert ? inserted++ : updated++;
}
await c.query('COMMIT');
console.log(`COMMITTED — inserted ${inserted} new, refreshed ${updated} existing (volatile fields only).`);
} catch (e) { await c.query('ROLLBACK'); console.error('ROLLBACK:', e.message); process.exitCode = 1; }
finally { c.release(); await pool.end(); }
}
main().catch(e => { console.error(e); process.exit(1); });