← back to Japan Enrich
db/promote-import-queue.js
88 lines
#!/usr/bin/env node
/**
* Promote the viewer's import queue (staging/import-queue.jsonl) into dw_unified.
*
* GATED — this is the customer-data write half of the "⤓ Import" button. It is
* DRY-RUN by default (prints the planned upserts, touches nothing, zero deps).
* `--commit` performs the writes and requires `pg` + DATABASE_URL. Even on
* --commit it ONLY stages rows PostgreSQL-side (on_shopify=false / status draft),
* per DW's "PostgreSQL BEFORE Shopify" rule — it NEVER publishes to Shopify.
* Do not run --commit against canonical dw_unified without Steve's go.
*
* node db/promote-import-queue.js # dry-run (safe)
* node db/promote-import-queue.js --commit # gated: writes to dw_unified
*/
const fs = require('fs');
const path = require('path');
const QUEUE = path.join(__dirname, '..', 'staging', 'import-queue.jsonl');
const COMMIT = process.argv.includes('--commit');
// source → target per-vendor catalog table + vendor_code (matches DW *_catalog convention)
const TARGET = {
sangetsu: { table: 'sangetsu_catalog', vendor_code: 'sangetsu' },
lilycolor: { table: 'lilycolor_catalog', vendor_code: 'lilycolor' },
greenland: { table: 'greenland_catalog', vendor_code: 'greenland' },
};
function rows() {
if (!fs.existsSync(QUEUE)) return [];
return fs.readFileSync(QUEUE, 'utf8').trim().split('\n').filter(Boolean)
.map((l) => { try { return JSON.parse(l); } catch { return null; } }).filter(Boolean);
}
async function main() {
const q = rows();
if (!q.length) { console.log('import queue empty — nothing to promote.'); return; }
const byVendor = {};
for (const r of q) (byVendor[r.source] = byVendor[r.source] || []).push(r);
console.log(`Queue: ${q.length} SKUs across ${Object.keys(byVendor).length} vendor(s).`);
for (const [src, list] of Object.entries(byVendor)) {
const t = TARGET[src];
console.log(`\n${src} → ${t ? t.table : '(no mapping!)'} (${list.length} SKUs)`);
for (const r of list.slice(0, 5)) console.log(` ${r.sku} us_distributor=${r.distributor||'(none)'} ${r.title||''}`);
if (list.length > 5) console.log(` … +${list.length - 5} more`);
}
if (!COMMIT) {
console.log('\nDRY-RUN — no writes. Re-run with --commit (gated) to stage into dw_unified.');
console.log('Each row upserts (vendor_code, mfr_sku) with: pattern_name, collection, product_url,');
console.log('image_url, us_distributor=<distributor>, on_shopify=false. Shopify publish stays separate/gated.');
return;
}
// ---- gated write path ----
const DSN = process.env.DATABASE_URL;
if (!DSN) { console.error('COMMIT aborted: DATABASE_URL not set.'); process.exit(1); }
let Client;
try { ({ Client } = require('pg')); } catch { console.error('COMMIT needs `pg` — run: npm i pg'); process.exit(1); }
// SETTLEMENT GUARD: this script ONLY stages PostgreSQL-side with on_shopify=false.
// It NEVER publishes to Shopify. The Sangetsu mural line (~60% birds/tropical) must
// clear the `settlement` gate before ANY on_shopify flip — which is a separate,
// Steve-gated step this script deliberately does not perform.
if (byVendor.sangetsu && byVendor.sangetsu.length) {
console.log(`\n⚠️ SETTLEMENT: ${byVendor.sangetsu.length} Sangetsu SKUs will be STAGED (on_shopify=false) only.`);
console.log(' Do NOT flip on_shopify=true / publish to Shopify until the settlement gate passes per SKU.');
}
const db = new Client({ connectionString: DSN });
await db.connect();
let n = 0;
for (const [src, list] of Object.entries(byVendor)) {
const t = TARGET[src]; if (!t) { console.warn(`skip ${src}: no target table`); continue; }
const exists = await db.query('select to_regclass($1) as t', [`public.${t.table}`]);
if (!exists.rows[0].t) { console.warn(`skip ${src}: ${t.table} does not exist (create it first — gated schema step)`); continue; }
for (const r of list) {
await db.query(
`insert into ${t.table} (vendor_code, mfr_sku, pattern_name, collection, product_url, image_url, us_distributor, on_shopify)
values ($1,$2,$3,$4,$5,$6,$7,false)
on conflict (vendor_code, mfr_sku) do update set
pattern_name=excluded.pattern_name, collection=excluded.collection,
product_url=excluded.product_url, image_url=excluded.image_url,
us_distributor=excluded.us_distributor`,
[t.vendor_code, r.sku, r.title, r.collection, r.url, r.image, r.distributor]);
n++;
}
}
await db.end();
console.log(`\nCommitted ${n} rows to dw_unified (on_shopify=false). Shopify publish remains a separate gated step.`);
}
main().catch((e) => { console.error(e); process.exit(1); });