← back to Sister Parish Onboarding

scripts/backfill_shopify_ids.js

93 lines

#!/usr/bin/env node
/**
 * After Steve uploads the CSV, every variant in DW Shopify has SKU = `DWSP-<orig sku>`.
 * Walk all Sister Parish products in DW Shopify, match each variant's SKU back to
 * its `sp_product_id` in dw_unified.sisterparish_catalog, and write
 * shopify_product_id + shopify_handle into that table.
 *
 * Idempotent — safe to re-run.
 */
require('dotenv').config({ path: require('path').join(__dirname,'..','.env') });
const { execSync } = require('child_process');

const SHOP  = process.env.SHOPIFY_STORE;
const TOKEN = process.env.SHOPIFY_ADMIN_TOKEN;
if (!SHOP || !TOKEN) { console.error('Need SHOPIFY_STORE + SHOPIFY_ADMIN_TOKEN'); process.exit(1); }

const BASE = `https://${SHOP}/admin/api/2026-01`;

async function shopify(path) {
  const res = await fetch(`${BASE}${path}`, { headers: { 'X-Shopify-Access-Token': TOKEN } });
  const text = await res.text();
  if (!res.ok) throw new Error(`${res.status}: ${text.slice(0,300)}`);
  return { json: JSON.parse(text), link: res.headers.get('link') || '' };
}

function nextPageInfo(link) {
  // Shopify cursor pagination — extract rel="next" page_info
  const m = link.match(/<[^>]*[?&]page_info=([^&>]+)[^>]*>;\s*rel="next"/);
  return m ? m[1] : null;
}

(async () => {
  // Walk all Sister Parish products (paginated)
  const all = [];
  let path = '/products.json?vendor=Sister+Parish&limit=250&fields=id,handle,title,variants';
  for (;;) {
    const r = await shopify(path);
    all.push(...r.json.products);
    const next = nextPageInfo(r.link);
    if (!next) break;
    path = `/products.json?limit=250&fields=id,handle,title,variants&page_info=${next}`;
  }
  console.log(`Sister Parish products in DW Shopify: ${all.length}`);

  if (all.length === 0) {
    console.log('Nothing to backfill yet — re-run after CSV import finishes.');
    return;
  }

  // Map: source_product_id (parsed from DWSP-<orig sku> back to sp_product_id) — we need a join.
  // We'll use the PG catalog and look up sp_product_id by variant.sku (stripped of DWSP-).
  // First, write all (shopify_product_id, shopify_handle, variant_sku_list) to /tmp, then JOIN in SQL.
  const fs = require('fs');
  const lines = ['shopify_product_id\tshopify_handle\torig_sku'];
  for (const p of all) {
    for (const v of p.variants) {
      const orig = (v.sku || '').replace(/^DWSP-/, '');
      lines.push(`${p.id}\t${p.handle}\t${orig}`);
    }
  }
  fs.writeFileSync('/tmp/sp_shopify_map.tsv', lines.join('\n'));

  const sql = `
ALTER TABLE sisterparish_catalog ADD COLUMN IF NOT EXISTS shopify_product_id BIGINT;
ALTER TABLE sisterparish_catalog ADD COLUMN IF NOT EXISTS shopify_handle TEXT;
ALTER TABLE sisterparish_catalog ADD COLUMN IF NOT EXISTS shopify_synced_at TIMESTAMPTZ;

CREATE TEMP TABLE sp_map (shopify_product_id BIGINT, shopify_handle TEXT, orig_sku TEXT);
\\copy sp_map FROM '/tmp/sp_shopify_map.tsv' WITH (FORMAT csv, DELIMITER E'\\t', HEADER true);

-- Match: for each catalog row, find a variant in sp_map whose orig_sku matches any variant in JSONB.
UPDATE sisterparish_catalog c
SET shopify_product_id = m.shopify_product_id,
    shopify_handle     = m.shopify_handle,
    shopify_synced_at  = NOW()
FROM (
  SELECT DISTINCT ON (c2.sp_product_id) c2.sp_product_id, m.shopify_product_id, m.shopify_handle
  FROM sisterparish_catalog c2
  JOIN jsonb_array_elements(c2.variants) AS v ON true
  JOIN sp_map m ON m.orig_sku = v->>'sku'
) m
WHERE m.sp_product_id = c.sp_product_id;

SELECT
  COUNT(*) FILTER (WHERE shopify_product_id IS NOT NULL) AS matched,
  COUNT(*) FILTER (WHERE shopify_product_id IS NULL)     AS unmatched,
  COUNT(*) AS total
FROM sisterparish_catalog;
`;
  fs.writeFileSync('/tmp/sp_backfill.sql', sql);
  execSync('psql dw_unified -f /tmp/sp_backfill.sql', { stdio: 'inherit' });
})().catch(e => { console.error('FATAL', e); process.exit(1); });