← back to Dw Scrape Scheduler

lib/write-staging.js

54 lines

// Upsert a vendor's scraped products into the shared scrape_staging table.
// Change detection: rows whose last_seen predates this run's start are no longer
// in the feed => "gone" (candidate discontinued). We only REPORT gone; we never
// delete (a bad scrape shouldn't wipe a catalog — same fail-safe as elsewhere).
const { pool } = require('./db');

const CHUNK = 500;

async function writeStaging(vendorCode, rawProducts) {
  if (!rawProducts.length) return { inserted: 0, updated: 0, total: 0, gone: 0, dupes: 0 };
  // Dedupe by mfr_sku (Shopify feeds reuse/blank SKUs across products) — a single
  // ON CONFLICT batch can't touch the same key twice. Keep the last occurrence.
  const byKey = new Map();
  for (const p of rawProducts) if (p.mfr_sku) byKey.set(p.mfr_sku, p);
  const products = [...byKey.values()];
  const dupes = rawProducts.length - products.length;
  const client = await pool.connect();
  try {
    const runStart = new Date();
    let inserted = 0, updated = 0;
    for (let i = 0; i < products.length; i += CHUNK) {
      const batch = products.slice(i, i + CHUNK);
      const vals = [], params = [];
      batch.forEach((p, j) => {
        const b = j * 12;
        vals.push(`($${b+1},$${b+2},$${b+3},$${b+4},$${b+5},$${b+6},$${b+7},$${b+8},$${b+9},$${b+10},$${b+11},$${b+12},now(),now())`);
        params.push(vendorCode, p.mfr_sku, p.handle || null, p.title || null, p.product_type || null,
          p.vendor || null, p.price, p.image_url || null, p.product_url || null,
          p.tags || null, p.all_images || null, p.raw ? JSON.stringify(p.raw) : null);
      });
      // xmax=0 in RETURNING distinguishes INSERT (0) from UPDATE (nonzero).
      const res = await client.query(`
        INSERT INTO scrape_staging
          (vendor_code,mfr_sku,handle,title,product_type,vendor,price,image_url,product_url,tags,all_images,raw,last_seen,scraped_at)
        VALUES ${vals.join(',')}
        ON CONFLICT (vendor_code,mfr_sku) DO UPDATE SET
          handle=EXCLUDED.handle, title=EXCLUDED.title, product_type=EXCLUDED.product_type,
          vendor=EXCLUDED.vendor, price=EXCLUDED.price, image_url=EXCLUDED.image_url,
          product_url=EXCLUDED.product_url, tags=EXCLUDED.tags, all_images=EXCLUDED.all_images,
          raw=EXCLUDED.raw, last_seen=now(), scraped_at=now()
        RETURNING (xmax = 0) AS is_insert`, params);
      for (const r of res.rows) r.is_insert ? inserted++ : updated++;
    }
    const gone = await client.query(
      `SELECT count(*)::int c FROM scrape_staging WHERE vendor_code=$1 AND last_seen < $2`,
      [vendorCode, runStart]);
    return { inserted, updated, total: products.length, gone: gone.rows[0].c, dupes };
  } finally {
    client.release();
  }
}

module.exports = { writeStaging };