← back to Dw Pitch Followup
lib/fm.js
79 lines
// Thin wrapper around the existing filemaker-mcp Data API client.
// We REUSE that project's client + config + node_modules by importing it via
// absolute path (ESM resolves its bare deps against its own node_modules), and
// load its .env so FM_CLOUD_HOST / Cognito creds are present. READ-ONLY here:
// we force FM_READONLY=1 so this tool can never write to FileMaker.
import fs from 'node:fs';
const FM_MCP_DIR = '/Users/macstudio3/Projects/filemaker-mcp';
// Load filemaker-mcp/.env into process.env (no dotenv dep).
function loadFmEnv() {
const envPath = `${FM_MCP_DIR}/.env`;
if (!fs.existsSync(envPath)) throw new Error(`filemaker-mcp .env not found at ${envPath}`);
for (const line of fs.readFileSync(envPath, 'utf8').split('\n')) {
const m = line.match(/^\s*([A-Z0-9_]+)\s*=\s*(.*)\s*$/);
if (m && process.env[m[1]] === undefined) process.env[m[1]] = m[2].replace(/^["']|["']$/g, '');
}
process.env.FM_READONLY = '1'; // hard read-only for this tool
}
loadFmEnv();
const client = await import(`${FM_MCP_DIR}/src/fm-client.js`);
export const { findRecords, getRecord, listRecords, fieldMetadata, layouts } = client;
// Page through a find query, accumulating up to `cap` records.
// query = a single find-request object or an array (OR'd) per the Data API.
export async function findAll(db, layout, query, { pageSize = 100, cap = 5000, sort } = {}) {
const out = [];
let offset = 1;
while (out.length < cap) {
let res;
try {
res = await findRecords(db, layout, query, { limit: pageSize, offset, sort });
} catch (e) {
// FileMaker code 401 = "no records match" → clean empty result, not an error.
if (String(e.fmCode) === '401') break;
throw e;
}
const recs = res.records || [];
out.push(...recs);
if (recs.length < pageSize) break;
offset += pageSize;
}
return out.slice(0, cap);
}
// Fetch many accounts' parent project records in chunks via OR-queries.
export async function findProjectsByAccounts(accounts, { layout = 'Projects', chunk = 60 } = {}) {
const map = new Map();
for (let i = 0; i < accounts.length; i += chunk) {
const slice = accounts.slice(i, i + chunk);
const query = slice.map((a) => ({ 'Account Number': `==${a}` }));
let recs = [];
try {
recs = (await findRecords('Clients', layout, query, { limit: slice.length * 3 })).records || [];
} catch (e) {
if (String(e.fmCode) !== '401') throw e;
}
for (const r of recs) {
const acct = r.fieldData['Account Number'];
if (acct && !map.has(String(acct))) map.set(String(acct), r);
}
}
return map;
}
// US-format date helper for FileMaker find criteria (MM/DD/YYYY).
export function fmDate(d) {
const mm = String(d.getMonth() + 1).padStart(2, '0');
const dd = String(d.getDate()).padStart(2, '0');
return `${mm}/${dd}/${d.getFullYear()}`;
}
export function daysAgo(n, base = new Date('2026-06-30T12:00:00')) {
const d = new Date(base);
d.setDate(d.getDate() - n);
return d;
}