@proofkit/fmdapi
Version:
FileMaker Data API client
283 lines (282 loc) • 10.5 kB
JavaScript
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