@proofgeist/fmdapi
Version:
FileMaker Data API client
230 lines • 8.04 kB
JavaScript
import { FileMakerError } from "../index.js";
export class BaseFetchAdapter {
server;
db;
refreshToken;
baseUrl;
constructor(options) {
this.server = options.server;
this.db = options.db;
this.refreshToken = options.refreshToken ?? false;
this.baseUrl = new URL(`${this.server}/fmi/data/vLatest/databases/${this.db}`);
if (this.db === "")
throw new Error("Database name is required");
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
getToken = async (args) => {
// method must be implemented in subclass
throw new Error("getToken method not implemented by Fetch Adapter");
};
request = async (params) => {
const { query, body, method = "GET", retry = false, fetchOptions = {}, } = params;
const url = new URL(`${this.baseUrl}${params.url}`);
if (query) {
const { _sort, ...rest } = query;
const searchParams = new URLSearchParams(rest);
if (query.portalRanges && typeof query.portalRanges === "object") {
for (const [portalName, value] of Object.entries(query.portalRanges)) {
if (value) {
value.offset &&
value.offset > 0 &&
searchParams.set(`_offset.${portalName}`, value.offset.toString());
value.limit &&
searchParams.set(`_limit.${portalName}`, value.limit.toString());
}
}
}
if (_sort) {
searchParams.set("_sort", JSON.stringify(_sort));
}
searchParams.delete("portalRanges");
url.search = searchParams.toString();
}
if (body && "portalRanges" in body) {
for (const [portalName, value] of Object.entries(body.portalRanges)) {
if (value) {
value.offset &&
value.offset > 0 &&
url.searchParams.set(`_offset.${portalName}`, value.offset.toString());
value.limit &&
url.searchParams.set(`_limit.${portalName}`, value.limit.toString());
}
}
delete body.portalRanges;
}
const controller = new AbortController();
let timeout = null;
if (params.timeout)
timeout = setTimeout(() => controller.abort(), params.timeout);
const token = await this.getToken({ refresh: retry });
const headers = new Headers(fetchOptions?.headers);
headers.set("Authorization", `Bearer ${token}`);
// Only set Content-Type for JSON bodies
if (!(body instanceof FormData)) {
headers.set("Content-Type", "application/json");
}
const res = await fetch(url.toString(), {
...fetchOptions,
method,
body: body instanceof FormData
? body
: body
? JSON.stringify(body)
: undefined,
headers,
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
signal: controller.signal,
});
if (timeout)
clearTimeout(timeout);
let respData;
try {
respData = await res.json();
}
catch {
respData = {};
}
if (!res.ok) {
if (respData?.messages?.[0].code === "952" &&
!retry &&
this.refreshToken) {
// token expired, get new token and retry once
return this.request({ ...params, retry: true });
}
else {
throw new FileMakerError(respData?.messages?.[0].code ?? "500", `Filemaker Data API failed with (${res.status}): ${JSON.stringify(respData, null, 2)}`);
}
}
return respData.response;
};
list = async (opts) => {
const { data, layout } = opts;
const resp = await this.request({
url: `/layouts/${layout}/records`,
query: data,
fetchOptions: opts.fetch,
timeout: opts.timeout,
});
return resp;
};
get = async (opts) => {
const { data, layout } = opts;
const resp = await this.request({
url: `/layouts/${layout}/records/${data.recordId}`,
fetchOptions: opts.fetch,
timeout: opts.timeout,
});
return resp;
};
find = async (opts) => {
const { data, layout } = opts;
const resp = await this.request({
url: `/layouts/${layout}/_find`,
body: data,
method: "POST",
fetchOptions: opts.fetch,
timeout: opts.timeout,
});
return resp;
};
create = async (opts) => {
const { data, layout } = opts;
const resp = await this.request({
url: `/layouts/${layout}/records`,
body: data,
method: "POST",
fetchOptions: opts.fetch,
timeout: opts.timeout,
});
return resp;
};
update = async (opts) => {
const { data: { recordId, ...data }, layout, } = opts;
const resp = await this.request({
url: `/layouts/${layout}/records/${recordId}`,
body: data,
method: "PATCH",
fetchOptions: opts.fetch,
timeout: opts.timeout,
});
return resp;
};
delete = async (opts) => {
const { data, layout } = opts;
const resp = await this.request({
url: `/layouts/${layout}/records/${data.recordId}`,
method: "DELETE",
fetchOptions: opts.fetch,
timeout: opts.timeout,
});
return resp;
};
layoutMetadata = async (opts) => {
return (await this.request({
url: `/layouts/${opts.layout}`,
fetchOptions: opts.fetch,
timeout: opts.timeout,
}));
};
/**
* Execute a script within the database
*/
executeScript = async (opts) => {
const { script, scriptParam, layout } = opts;
const resp = await this.request({
url: `/layouts/${layout}/script/${script}`,
query: scriptParam ? { "script.param": scriptParam } : undefined,
fetchOptions: opts.fetch,
timeout: opts.timeout,
});
return resp;
};
/**
* Returns a list of available layouts on the database.
*/
layouts = async (opts) => {
return (await this.request({
url: "/layouts",
fetchOptions: opts?.fetch,
timeout: opts?.timeout,
}));
};
/**
* Returns a list of available scripts on the database.
*/
scripts = async (opts) => {
return (await this.request({
url: "/scripts",
fetchOptions: opts?.fetch,
timeout: opts?.timeout,
}));
};
containerUpload = async (opts) => {
let url = `/layouts/${opts.layout}/records/${opts.data.recordId}/containers/${opts.data.containerFieldName}`;
if (opts.data.repetition)
url += `/${opts.data.repetition}`;
const formData = new FormData();
formData.append("upload", opts.data.file);
await this.request({
url,
method: "POST",
body: formData,
timeout: opts.timeout,
fetchOptions: opts.fetch,
});
};
/**
* Set global fields for the current session
*/
globals = async (opts) => {
return (await this.request({
url: "/globals",
method: "PATCH",
body: { globalFields: opts.globalFields },
fetchOptions: opts?.fetch,
timeout: opts?.timeout,
}));
};
}
//# sourceMappingURL=fetch-base.js.map