← 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); });