← back to Filemaker Mcp
grs-dedup-delete.mjs
35 lines
#!/usr/bin/env node
// REVERSIBLE FileMaker dedup: delete the EXTRA duplicate GRS masters listed in the dedup plan.
// Saves each record's FULL fieldData to a restore JSONL BEFORE deleting (re-createable).
// DEFAULT = dry-run. --apply fires. Only touches disposition='safe-delete' rows (never CONFLICT-HOLD).
import fs from 'fs';
import path from 'path';
const env = JSON.parse(fs.readFileSync('/tmp/fmenv.json', 'utf8'));
for (const [k, v] of Object.entries(env)) process.env[k] = v;
const fm = await import('./src/fm-client.js');
const DB = 'WALLPAPER', LAYOUT = '*List Wallpapers - Full View'; // delete-safe TO (Basic List of Fields hits [110] cascade)
const APPLY = process.argv.includes('--apply');
const LIMIT = parseInt((process.argv.find(a => a.startsWith('--limit=')) || '').split('=')[1] || '0', 10);
const Q = String.fromCharCode(34);
function P(t) { const R = []; let i = 0, f = '', r = [], q = false; while (i < t.length) { const c = t[i]; if (q) { if (c === Q) { if (t[i + 1] === Q) { f += Q; i++; } else q = false; } else f += c; } else { if (c === Q) q = true; else if (c === ',') { r.push(f); f = ''; } else if (c === '\n') { r.push(f); R.push(r); r = []; f = ''; } else if (c !== '\r') f += c; } i++; } if (f.length || r.length) { r.push(f); R.push(r); } return R; }
const raw = P(fs.readFileSync(process.env.HOME + '/Desktop/GRS_dedup_DELETE_plan_20260819.csv', 'utf8'));
const h = raw.shift(); const ix = Object.fromEntries(h.map((x, i) => [x, i]));
let targets = raw.filter(r => r.length && r[ix.disposition] === 'safe-delete');
if (LIMIT) targets = targets.slice(0, LIMIT);
const RESTORE = process.env.HOME + '/Desktop/GRS_dedup_RESTORE_' + new Date().toISOString().slice(0, 10).replace(/-/g, '') + '.jsonl';
console.log(`safe-delete targets: ${targets.length} (${APPLY ? 'LIVE DELETE' : 'DRY-RUN'})`);
if (!APPLY) { console.log(' [dry-run] no deletions. Re-run with --apply.'); process.exit(0); }
let done = 0, fail = 0;
for (const r of targets) {
const recId = r[ix.delete_record_id];
try {
const rec = await fm.getRecord(DB, LAYOUT, recId).catch(() => null);
const fd = rec?.records?.[0]?.fieldData || rec?.data?.fieldData || null;
fs.appendFileSync(RESTORE, JSON.stringify({ recId, combo: r[ix.combo], keeper: r[ix.keeper_record_id], fieldData: fd }) + '\n');
await fm.deleteRecord(DB, LAYOUT, recId);
done++; if (done % 25 === 0) console.log(` ...deleted ${done}/${targets.length}`);
await new Promise(r => setTimeout(r, 120));
} catch (e) { fail++; console.error(` FAIL rec ${recId}: ${e.message}`); await new Promise(r => setTimeout(r, 300)); }
}
console.log(`\nLIVE dedup: deleted ${done}, failed ${fail}. Restore -> ${RESTORE}`);