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