UNPKG

@proofkit/fmdapi

Version:

FileMaker Data API client

283 lines (282 loc) • 10.5 kB
var __defProp = Object.defineProperty; var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value; var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value); import { FileMakerError } from "../client-types.js"; const TRAILING_SLASHES_REGEX = /\/+$/; const DEFAULT_AUTHORIZE_TIMEOUT_MS = 125e3; const DEFAULT_IDLE_TIMEOUT_SECONDS = 3600; const SESSION_HEADER_NAME = "X-ProofKit-Session"; const CLIENT_HEADER_NAME = "X-ProofKit-Client"; const envValue = (name) => { if (typeof process === "undefined") { return void 0; } return process.env[name]; }; const randomSessionId = () => { var _a; if ((_a = globalThis.crypto) == null ? void 0 : _a.randomUUID) { return globalThis.crypto.randomUUID(); } return Math.random().toString(36).slice(2); }; const statusReason = (status) => { if (status === "rejected") { return "authorization rejected"; } if (status === "timeout") { return "authorization timed out"; } if (status === "file_not_connected") { return "file not connected"; } return typeof status === "string" ? status : "authorization failed"; }; class FmMcpAdapter { constructor(options) { __publicField(this, "baseUrl"); __publicField(this, "connectedFileName"); __publicField(this, "scriptName"); __publicField(this, "sessionId"); __publicField(this, "clientName"); __publicField(this, "clientDescription"); __publicField(this, "idleTimeoutSeconds"); __publicField(this, "authorizationTimeoutMs"); __publicField(this, "disableInteractiveAuthorization"); __publicField(this, "pendingAuthorization"); __publicField(this, "sessionHeaders", () => { const headers = new Headers(); headers.set(SESSION_HEADER_NAME, this.sessionId); headers.set(CLIENT_HEADER_NAME, this.clientName); return headers; }); __publicField(this, "ensureAuthorized", () => { if (this.pendingAuthorization) { return this.pendingAuthorization; } this.pendingAuthorization = this.requestAuthorization().finally(() => { this.pendingAuthorization = void 0; }); return this.pendingAuthorization; }); __publicField(this, "requestAuthorization", async () => { if (this.disableInteractiveAuthorization) { throw new Error("interactive authorization disabled"); } const controller = new AbortController(); const timeout = setTimeout(() => controller.abort(), this.authorizationTimeoutMs); try { const res = await fetch(`${this.baseUrl}/authorizeSession`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ sessionId: this.sessionId, fileName: this.connectedFileName, clientName: this.clientName, clientDescription: this.clientDescription, idleTimeoutSeconds: this.idleTimeoutSeconds }), signal: controller.signal }); const payload = await res.json().catch(() => null); if (res.ok && (payload == null ? void 0 : payload.status) === "approved") { return; } const reason = typeof (payload == null ? void 0 : payload.error) === "string" ? payload.error : statusReason(payload == null ? void 0 : payload.status); throw new Error(reason); } catch (err) { if (err instanceof Error && err.name === "AbortError") { throw new Error("authorization timed out"); } throw err; } finally { clearTimeout(timeout); } }); __publicField(this, "isUnauthorizedSession", async (res) => { if (res.status !== 401) { return false; } const payload = await res.clone().json().catch(() => null); return (payload == null ? void 0 : payload.code) === "session_not_authorized"; }); __publicField(this, "request", async (params) => { var _a, _b; const { action = "read", layout, body } = params; const normalizedBody = { ...body }; if ("_offset" in normalizedBody) { normalizedBody.offset = normalizedBody._offset; normalizedBody._offset = void 0; } if ("_limit" in normalizedBody) { normalizedBody.limit = normalizedBody._limit; normalizedBody._limit = void 0; } if ("_sort" in normalizedBody) { normalizedBody.sort = normalizedBody._sort; normalizedBody._sort = void 0; } const scriptParam = JSON.stringify({ ...normalizedBody, layouts: layout, action, version: "vLatest" }); const raw = await this.postCallScript({ data: scriptParam, errorMessage: "FM MCP request failed", fetchOptions: params.fetchOptions, scriptName: this.scriptName, timeout: params.timeout }); let scriptResult; try { scriptResult = typeof raw.result === "string" ? JSON.parse(raw.result) : raw.result ?? raw; } catch (err) { throw new FileMakerError( "500", `FM MCP response parse failed: ${err instanceof Error ? err.message : String(err)}` ); } const respData = scriptResult; const errorCode = (_b = (_a = respData.messages) == null ? void 0 : _a[0]) == null ? void 0 : _b.code; if (errorCode !== void 0 && errorCode !== "0") { throw new FileMakerError( errorCode, `Filemaker Data API failed with (${errorCode}): ${JSON.stringify(respData, null, 2)}` ); } return respData.response; }); __publicField(this, "postCallScript", async (params) => { const { fetchOptions = {} } = params; const controller = new AbortController(); const timeout = params.timeout ? setTimeout(() => controller.abort(), params.timeout) : void 0; const headers = new Headers(this.sessionHeaders()); new Headers(fetchOptions.headers).forEach((value, key) => { headers.set(key, value); }); headers.set("Content-Type", "application/json"); const postCallScript = () => fetch(`${this.baseUrl}/callScript`, { ...fetchOptions, method: "POST", headers, body: JSON.stringify({ connectedFileName: this.connectedFileName, scriptName: params.scriptName, data: params.data }), signal: controller.signal }); let res; try { res = await postCallScript(); if (await this.isUnauthorizedSession(res)) { try { await this.ensureAuthorized(); } catch (err) { const reason = err instanceof Error ? err.message : "authorization failed"; throw new Error(`Not authorized to connect to FileMaker file "${this.connectedFileName}": ${reason}`); } res = await postCallScript(); } } finally { if (timeout) { clearTimeout(timeout); } } if (!res.ok) { throw new FileMakerError(String(res.status), `${params.errorMessage} (${res.status}): ${await res.text()}`); } return await res.json(); }); __publicField(this, "list", async (opts) => { return await this.request({ body: opts.data, layout: opts.layout, timeout: opts.timeout, fetchOptions: opts.fetch }); }); __publicField(this, "get", async (opts) => { return await this.request({ body: opts.data, layout: opts.layout, timeout: opts.timeout, fetchOptions: opts.fetch }); }); __publicField(this, "find", async (opts) => { return await this.request({ body: opts.data, layout: opts.layout, timeout: opts.timeout, fetchOptions: opts.fetch }); }); __publicField(this, "create", async (opts) => { return await this.request({ action: "create", body: opts.data, layout: opts.layout, timeout: opts.timeout, fetchOptions: opts.fetch }); }); __publicField(this, "update", async (opts) => { return await this.request({ action: "update", body: opts.data, layout: opts.layout, timeout: opts.timeout, fetchOptions: opts.fetch }); }); __publicField(this, "delete", async (opts) => { return await this.request({ action: "delete", body: opts.data, layout: opts.layout, timeout: opts.timeout, fetchOptions: opts.fetch }); }); __publicField(this, "layoutMetadata", async (opts) => { return await this.request({ action: "metaData", layout: opts.layout, body: {}, timeout: opts.timeout, fetchOptions: opts.fetch }); }); __publicField(this, "executeScript", async (opts) => { const raw = await this.postCallScript({ data: opts.scriptParam, errorMessage: "FM MCP executeScript failed", fetchOptions: opts.fetch, scriptName: opts.script, timeout: opts.timeout }); return { scriptResult: typeof raw.result === "string" ? raw.result : JSON.stringify(raw.result) }; }); __publicField(this, "containerUpload", () => { throw new Error("Container upload is not supported via FM MCP adapter"); }); this.baseUrl = options.baseUrl.replace(TRAILING_SLASHES_REGEX, ""); this.connectedFileName = options.connectedFileName; this.scriptName = options.scriptName ?? "PK_execute_data_api"; this.sessionId = options.sessionId ?? envValue("FM_MCP_SESSION_ID") ?? randomSessionId(); this.clientName = options.clientName ?? envValue("FM_MCP_CLIENT_NAME") ?? "ProofKit Typegen"; this.clientDescription = options.clientDescription ?? envValue("FM_MCP_CLIENT_DESCRIPTION") ?? "ProofKit Typegen is requesting FileMaker bridge access."; this.idleTimeoutSeconds = Math.min(options.idleTimeoutSeconds ?? DEFAULT_IDLE_TIMEOUT_SECONDS, 3600); this.authorizationTimeoutMs = options.authorizationTimeoutMs ?? DEFAULT_AUTHORIZE_TIMEOUT_MS; this.disableInteractiveAuthorization = options.disableInteractiveAuthorization ?? envValue("FM_MCP_DISABLE_INTERACTIVE_AUTHORIZATION") === "true"; } } export { FmMcpAdapter }; //# sourceMappingURL=fm-mcp.js.map