← back to Discontinued Agent

lib/filemaker.js

101 lines

// FileMaker Cloud Data API client for the discontinued-agent.
//
// We DO NOT reimplement Claris/Cognito SRP or hardcode any credentials.
// Instead we dynamically import the already-working fm-client from the
// filemaker-mcp project (FM_MCP_DIR) and load ITS .env for the host + creds.
// That project holds the FM Cloud credentials (FM_CLOUD_HOST / FM_CLARIS_* /
// FM_COGNITO_*) and the amazon-cognito-identity-js dependency.
//
// Cost: FileMaker Cloud Data API is self-hosted / already-paid = $0 per call.

import { readFileSync } from 'node:fs';
import { join } from 'node:path';
import { pathToFileURL } from 'node:url';

const FM_MCP_DIR = process.env.FM_MCP_DIR || '/Users/macstudio3/Projects/filemaker-mcp';
export const FM_DATABASE = process.env.FM_DATABASE || 'WALLPAPER';
export const FM_LAYOUT = process.env.FM_LAYOUT || 'REPORT ON SAMPLES ORDERED';

// Field names on the REPORT ON SAMPLES ORDERED layout (verified via field metadata).
export const FIELDS = {
  mfrPattern: 'Mfr Pattern',
  dateDiscontinued: 'Date Discontinued',
  dateSampleSent: 'Date WP Sample Sent',
  account: 'account',
  company: 'Clients::Company',
  requestPrinted: 'Date Sample Request printed for vendor',
};

// Load the filemaker-mcp .env into process.env WITHOUT clobbering anything we
// already set (our own env wins). Minimal parser: KEY=VALUE, ignores comments.
function loadFmEnv() {
  const envPath = join(FM_MCP_DIR, '.env');
  let raw;
  try {
    raw = readFileSync(envPath, 'utf8');
  } catch {
    throw new Error(`filemaker-mcp .env not found at ${envPath} — cannot resolve FM credentials.`);
  }
  for (const line of raw.split('\n')) {
    const m = line.match(/^([A-Za-z_][A-Za-z0-9_]*)=(.*)$/);
    if (!m) continue;
    const k = m[1];
    let v = m[2];
    // strip surrounding single/double quotes if present
    if ((v.startsWith('"') && v.endsWith('"')) || (v.startsWith("'") && v.endsWith("'"))) {
      v = v.slice(1, -1);
    }
    if (process.env[k] === undefined) process.env[k] = v;
  }
}

let _client;
async function client() {
  if (_client) return _client;
  loadFmEnv();
  const clientPath = pathToFileURL(join(FM_MCP_DIR, 'src', 'fm-client.js')).href;
  _client = await import(clientPath);
  return _client;
}

// Today's date in FileMaker MM/DD/YYYY format.
export function fmToday(d = new Date()) {
  const mm = String(d.getMonth() + 1).padStart(2, '0');
  const dd = String(d.getDate()).padStart(2, '0');
  const yyyy = d.getFullYear();
  return `${mm}/${dd}/${yyyy}`;
}

// Prove auth + a Data API session.
export async function ping() {
  const c = await client();
  return c.ping();
}

// Find all rows whose `Mfr Pattern` word-matches the given mfr#.
// FileMaker _find is a word/substring match on that field.
export async function findByMfrPattern(mfr, { limit = 200 } = {}) {
  const c = await client();
  const query = { [FIELDS.mfrPattern]: mfr };
  try {
    const { records } = await c.findRecords(FM_DATABASE, FM_LAYOUT, query, { limit });
    return records || [];
  } catch (e) {
    // FileMaker returns [401] "No records match the request" — treat as empty.
    if (String(e.fmCode) === '401') return [];
    throw e;
  }
}

// Update a single record's fields. dryRun:true returns the before->after diff
// WITHOUT committing; dryRun:false commits.
export async function updateRecord(recordId, fieldData, { dryRun = true } = {}) {
  const c = await client();
  return c.updateRecord(FM_DATABASE, FM_LAYOUT, recordId, fieldData, { dryRun });
}

// Convenience: pull fieldData + recordId from a Data API record object.
export function recRow(rec) {
  return { recordId: rec.recordId, fields: rec.fieldData || {} };
}