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