← back to Mfr Review Viewer Corruption
scripts/restore-record.mjs
64 lines
#!/usr/bin/env node
// restore-record.mjs — REVERSE a delete performed by execute-sweep.mjs.
//
// Re-creates a deleted WALLPAPER master from its saved data/restore/<rid>-<ISO>.json
// snapshot via the filemaker-mcp createRecord (allowDuplicate:true, since the sweep
// intentionally removed a duplicate master — restoring re-adds that exact copy).
//
// This is the CONCRETE, runnable undo the ledger's undo_cmd points at — so a deleted
// record is genuinely reversible, not just "we have a JSON somewhere".
//
// NOTE: FileMaker assigns a NEW recordId on re-create (the old internal id is gone).
// Field DATA is fully restored; only the opaque recordId differs. Container/calc/
// summary fields that FileMaker rejects on write are dropped with a warning.
//
// Usage:
// node scripts/restore-record.mjs data/restore/391954-2026-08-27T16-17-13-048Z.json # DRY-RUN
// node scripts/restore-record.mjs data/restore/391954-2026-08-27T16-17-13-048Z.json --apply # RE-CREATE
import { readFileSync, existsSync } from 'node:fs';
import path from 'node:path';
import os from 'node:os';
import { fileURLToPath } from 'node:url';
const __dir = path.dirname(fileURLToPath(import.meta.url));
const FM_PROJECT = path.join(os.homedir(), 'Projects', 'filemaker-mcp');
const FM_ENV = path.join(FM_PROJECT, '.env');
const FM_CLIENT = path.join(FM_PROJECT, 'src', 'fm-client.js');
const FM_DB = 'WALLPAPER';
const FM_LAYOUT = '*List Wallpapers - Full View';
function loadFmEnv() {
if (!existsSync(FM_ENV)) return;
for (const line of readFileSync(FM_ENV, 'utf8').split('\n')) {
const m = line.match(/^([A-Z_]+)=(.*)$/);
if (m && !process.env[m[1]]) process.env[m[1]] = m[2];
}
}
loadFmEnv();
const APPLY = process.argv.includes('--apply');
const snapArg = process.argv.slice(2).find((a) => !a.startsWith('--'));
if (!snapArg) { console.error('Usage: restore-record.mjs <snapshot.json> [--apply]'); process.exit(2); }
// Fields FileMaker will reject on a write (calc / summary / container / global metadata).
// We keep it conservative: strip nothing by name here, but let createRecord surface a
// field error — if it does, re-run with an explicit --skip-fields list. Most Full-View
// text/number fields write back fine.
async function main() {
const snapPath = path.isAbsolute(snapArg) ? snapArg : path.join(__dir, '..', snapArg);
if (!existsSync(snapPath)) { console.error(`snapshot not found: ${snapPath}`); process.exit(2); }
const snap = JSON.parse(readFileSync(snapPath, 'utf8'));
const fieldData = snap.fieldData || {};
const rid = snap.recordId;
console.log(`restore ${rid} (${Object.keys(fieldData).length} fields) from ${path.basename(snapPath)} — ${APPLY ? 'APPLY' : 'DRY-RUN'}`);
const { createRecord } = await import('file://' + FM_CLIENT);
const r = await createRecord(FM_DB, FM_LAYOUT, fieldData, { dryRun: !APPLY, allowDuplicate: true });
if (r.committed) console.log(` RECREATED as new recordId ${r.recordId} (old id ${rid} is retired by FileMaker).`);
else console.log(` ${r.note || JSON.stringify(r)}`);
console.log(APPLY ? '\n[RESTORED]' : '\n[DRY-RUN — add --apply to re-create the record]');
}
main().catch((e) => { console.error('FATAL:', e.message); process.exit(1); });