← back to Filemaker Mcp

scripts/create-grasscloth-masters.mjs

73 lines

// CANONICAL importer: create FileMaker WALLPAPER masters for WallQuest specialty
// grasscloths from dw_unified.specialty_grasscloths_stage — with EVERY field mapped,
// including "Name of Pattern" and "Color of Pattern" (the two the previous ad-hoc run
// dropped, leaving 77 records blank on 2026-08-10; see backfill-grs-name-color.mjs).
//
// ROOT-CAUSE FIXES baked in here so the next batch is correct at insert time:
//   1) Field mapping — stage.pattern -> "Name of Pattern", stage.color -> "Color of Pattern".
//   2) WRITE LAYOUT — uses "*List Wallpapers - Full View" (which EXPOSES Name/Color of
//      Pattern). The old path wrote through "Add wallcovering", a layout that lacks those
//      two fields, so they silently never persisted. Never create these through a layout
//      that doesn't expose every field you're writing.
//   3) Idempotent by Mfr Pattern (vid=WQ) — re-running never dupes.
//
// Series/JS Pattern derive from the canonical stage.dw_sku (e.g. GRS-801346 -> Series GRS,
// JS 801346). NOTE: the 2026-08-10 ad-hoc batch used a different GRS3000xx sequence; confirm
// the numbering scheme with Steve before a real --apply run if that sequence must continue.
//
//   node create-grasscloth-masters.mjs                 # DRY-RUN
//   node create-grasscloth-masters.mjs --apply [--limit=N] [--offset=N]
import { execFileSync } from 'node:child_process';
import * as fm from '../src/fm-client.js';

const APPLY = process.argv.includes('--apply');
const LIMIT = parseInt((process.argv.find(a => a.startsWith('--limit=')) || '').split('=')[1] || '0', 10);
const OFFSET = parseInt((process.argv.find(a => a.startsWith('--offset=')) || '').split('=')[1] || '0', 10);
const DB = 'WALLPAPER';
const LAYOUT = '*List Wallpapers - Full View';   // exposes Name/Color of Pattern — REQUIRED
const PSQL = process.env.PSQL_BIN || '/opt/homebrew/opt/postgresql@14/bin/psql';
const PG = 'postgresql:///dw_unified?host=/tmp&user=stevestudio2';
const sleep = ms => new Promise(r => setTimeout(r, ms));

// "36 in | 91.4 cm" -> '36" Wide (trim to 34")'  (grasscloth convention: trim 2" off)
const widthOf = w => { const m = String(w || '').match(/([\d.]+)\s*in/i) || String(w || '').match(/([\d.]+)/);
  if (!m) return ''; const inch = Math.round(parseFloat(m[1])); return `${inch}" Wide (trim to ${inch - 2}")`; };

const rows = JSON.parse(execFileSync(PSQL, [PG, '-tAc',
  `select coalesce(json_agg(json_build_object(
      'mfr',lower(mfr_sku),'name',name,'pattern',pattern,'color',color,
      'width',width,'price',our_price,'dw_sku',dw_sku) order by dw_sku)::text,'[]')
    from specialty_grasscloths_stage where coalesce(status,'active')='active'`],
  { encoding: 'utf8', maxBuffer: 128e6 }).trim());

async function exists(mfr) {
  const r = await fm.findRecords(DB, LAYOUT, { 'Mfr Pattern': '==' + mfr, vid: '==WQ' }, { limit: 1 })
    .catch(() => ({ records: [] }));
  return (r.records?.length || 0) > 0;
}

async function main() {
  const todo = rows.slice(OFFSET, LIMIT ? OFFSET + LIMIT : undefined);
  console.log(`specialty_grasscloths_stage(active): ${rows.length} · this run: ${todo.length} · ${APPLY ? 'APPLY' : 'DRY-RUN'}\n`);
  let ok = 0, skip = 0, fail = 0;
  for (const r of todo) {
    const [prefix, num] = String(r.dw_sku || '').split('-');
    if (!prefix || !num) { skip++; console.log(`  SKIP ${r.mfr} (no dw_sku)`); continue; }
    const fd = {
      Series: prefix, 'JS Pattern': num, 'Mfr Pattern': r.mfr,
      'Name of Pattern': r.pattern, 'Color of Pattern': r.color || '',
      Width: widthOf(r.width), 'Retail Price': r.price ?? '',
      'Internal Description': r.name, vid: 'WQ',
    };
    if (!APPLY) { if (ok < 8) console.log(`  ${r.dw_sku}  ${r.mfr}  "${fd['Name of Pattern']}" (${fd['Color of Pattern'] || '—'})  w=${fd.Width}  $${fd['Retail Price']}`); ok++; continue; }
    try {
      if (await exists(r.mfr)) { skip++; continue; }
      await fm.createRecord(DB, LAYOUT, fd, { dryRun: false });
      ok++; if (ok % 20 === 0) console.log(`  ...created ${ok}`);
      await sleep(150);
    } catch (e) { fail++; console.error(`  FAIL ${r.mfr}: ${e.fmCode || e.message}`); await sleep(400); }
  }
  console.log(`\n${APPLY ? 'created' : 'planned'}=${ok}  skipped=${skip}  fail=${fail}`);
}
main().catch(e => { console.error(e); process.exit(1); });