@proofkit/fmdapi 5.0.3-beta.0 → 5.1.0-beta.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/bin/intent.js +20 -0
  2. package/dist/esm/adapters/core.d.ts +4 -4
  3. package/dist/esm/adapters/fetch-base-types.d.ts +4 -4
  4. package/dist/esm/adapters/fetch-base.d.ts +2 -2
  5. package/dist/esm/adapters/fetch-base.js +36 -49
  6. package/dist/esm/adapters/fetch-base.js.map +1 -1
  7. package/dist/esm/adapters/fetch.d.ts +5 -5
  8. package/dist/esm/adapters/fetch.js +11 -10
  9. package/dist/esm/adapters/fetch.js.map +1 -1
  10. package/dist/esm/adapters/fm-http.d.ts +32 -0
  11. package/dist/esm/adapters/fm-http.js +170 -0
  12. package/dist/esm/adapters/fm-http.js.map +1 -0
  13. package/dist/esm/adapters/otto.d.ts +2 -2
  14. package/dist/esm/adapters/otto.js +3 -5
  15. package/dist/esm/adapters/otto.js.map +1 -1
  16. package/dist/esm/client-types.d.ts +41 -41
  17. package/dist/esm/client-types.js +1 -6
  18. package/dist/esm/client-types.js.map +1 -1
  19. package/dist/esm/client.d.ts +28 -44
  20. package/dist/esm/client.js +75 -80
  21. package/dist/esm/client.js.map +1 -1
  22. package/dist/esm/index.d.ts +5 -6
  23. package/dist/esm/index.js +7 -5
  24. package/dist/esm/index.js.map +1 -1
  25. package/dist/esm/tokenStore/index.d.ts +1 -1
  26. package/dist/esm/tokenStore/memory.js.map +1 -1
  27. package/dist/esm/tokenStore/types.d.ts +2 -2
  28. package/dist/esm/tokenStore/upstash.d.ts +1 -1
  29. package/dist/esm/utils.d.ts +7 -7
  30. package/dist/esm/utils.js +6 -4
  31. package/dist/esm/utils.js.map +1 -1
  32. package/package.json +37 -26
  33. package/skills/fmdapi-client/SKILL.md +490 -0
  34. package/src/adapters/core.ts +6 -9
  35. package/src/adapters/fetch-base-types.ts +5 -3
  36. package/src/adapters/fetch-base.ts +53 -78
  37. package/src/adapters/fetch.ts +19 -24
  38. package/src/adapters/fm-http.ts +224 -0
  39. package/src/adapters/otto.ts +8 -8
  40. package/src/client-types.ts +59 -83
  41. package/src/client.ts +131 -167
  42. package/src/index.ts +5 -9
  43. package/src/tokenStore/file.ts +2 -4
  44. package/src/tokenStore/index.ts +1 -1
  45. package/src/tokenStore/types.ts +2 -2
  46. package/src/tokenStore/upstash.ts +2 -5
  47. package/src/utils.ts +16 -23
@@ -24,32 +24,28 @@ import type {
24
24
  ListOptions,
25
25
  UpdateOptions,
26
26
  } from "./core.js";
27
- import type {
28
- BaseFetchAdapterOptions,
29
- GetTokenArguments,
30
- } from "./fetch-base-types.js";
27
+ import type { BaseFetchAdapterOptions, GetTokenArguments } from "./fetch-base-types.js";
31
28
 
32
29
  export class BaseFetchAdapter implements Adapter {
33
30
  protected server: string;
34
31
  protected db: string;
35
- private refreshToken: boolean;
32
+ private readonly refreshToken: boolean;
36
33
  baseUrl: URL;
37
34
 
38
35
  constructor(options: BaseFetchAdapterOptions & { refreshToken?: boolean }) {
39
36
  this.server = options.server;
40
37
  this.db = options.db;
41
38
  this.refreshToken = options.refreshToken ?? false;
42
- this.baseUrl = new URL(
43
- `${this.server}/fmi/data/vLatest/databases/${this.db}`,
44
- );
39
+ this.baseUrl = new URL(`${this.server}/fmi/data/vLatest/databases/${this.db}`);
45
40
 
46
- if (this.db === "") throw new Error("Database name is required");
41
+ if (this.db === "") {
42
+ throw new Error("Database name is required");
43
+ }
47
44
  }
48
45
 
49
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
50
- protected getToken = async (args?: GetTokenArguments): Promise<string> => {
46
+ protected getToken = (_args?: GetTokenArguments): Promise<string> => {
51
47
  // method must be implemented in subclass
52
- throw new Error("getToken method not implemented by Fetch Adapter");
48
+ return Promise.reject(new Error("getToken method not implemented by Fetch Adapter"));
53
49
  };
54
50
 
55
51
  protected request = async (params: {
@@ -62,29 +58,20 @@ export class BaseFetchAdapter implements Adapter {
62
58
  timeout?: number;
63
59
  fetchOptions?: RequestInit;
64
60
  }): Promise<unknown> => {
65
- const {
66
- query,
67
- body,
68
- method = "GET",
69
- retry = false,
70
- fetchOptions = {},
71
- } = params;
61
+ const { query, body, method = "GET", retry = false, fetchOptions = {} } = params;
72
62
 
73
63
  const url = new URL(`${this.baseUrl}${params.url}`);
74
64
 
75
65
  if (query) {
76
66
  const { _sort, ...rest } = query;
77
- const searchParams = new URLSearchParams(rest);
67
+ // Filter out undefined/null values before creating URLSearchParams
68
+ const filteredRest = Object.fromEntries(Object.entries(rest).filter(([, v]) => v !== undefined && v !== null));
69
+ const searchParams = new URLSearchParams(filteredRest as Record<string, string>);
78
70
  if (query.portalRanges && typeof query.portalRanges === "object") {
79
- for (const [portalName, value] of Object.entries(
80
- query.portalRanges as PortalRanges,
81
- )) {
71
+ for (const [portalName, value] of Object.entries(query.portalRanges as PortalRanges)) {
82
72
  if (value) {
83
73
  if (value.offset && value.offset > 0) {
84
- searchParams.set(
85
- `_offset.${portalName}`,
86
- value.offset.toString(),
87
- );
74
+ searchParams.set(`_offset.${portalName}`, value.offset.toString());
88
75
  }
89
76
  if (value.limit) {
90
77
  searchParams.set(`_limit.${portalName}`, value.limit.toString());
@@ -100,31 +87,24 @@ export class BaseFetchAdapter implements Adapter {
100
87
  }
101
88
 
102
89
  if (body && "portalRanges" in body) {
103
- for (const [portalName, value] of Object.entries(
104
- body.portalRanges as PortalRanges,
105
- )) {
90
+ for (const [portalName, value] of Object.entries(body.portalRanges as PortalRanges)) {
106
91
  if (value) {
107
92
  if (value.offset && value.offset > 0) {
108
- url.searchParams.set(
109
- `_offset.${portalName}`,
110
- value.offset.toString(),
111
- );
93
+ url.searchParams.set(`_offset.${portalName}`, value.offset.toString());
112
94
  }
113
95
  if (value.limit) {
114
- url.searchParams.set(
115
- `_limit.${portalName}`,
116
- value.limit.toString(),
117
- );
96
+ url.searchParams.set(`_limit.${portalName}`, value.limit.toString());
118
97
  }
119
98
  }
120
99
  }
121
- delete body.portalRanges;
100
+ body.portalRanges = undefined;
122
101
  }
123
102
 
124
103
  const controller = new AbortController();
125
104
  let timeout: NodeJS.Timeout | null = null;
126
- if (params.timeout)
105
+ if (params.timeout) {
127
106
  timeout = setTimeout(() => controller.abort(), params.timeout);
107
+ }
128
108
 
129
109
  const token = await this.getToken({ refresh: retry });
130
110
 
@@ -136,22 +116,26 @@ export class BaseFetchAdapter implements Adapter {
136
116
  headers.set("Content-Type", "application/json");
137
117
  }
138
118
 
119
+ let requestBody: string | FormData | undefined;
120
+ if (body instanceof FormData) {
121
+ requestBody = body;
122
+ } else if (body) {
123
+ requestBody = JSON.stringify(body);
124
+ } else {
125
+ requestBody = undefined;
126
+ }
127
+
139
128
  const res = await fetch(url.toString(), {
140
129
  ...fetchOptions,
141
130
  method,
142
- body:
143
- body instanceof FormData
144
- ? body
145
- : body
146
- ? JSON.stringify(body)
147
- : undefined,
131
+ body: requestBody,
148
132
  headers,
149
- // eslint-disable-next-line @typescript-eslint/ban-ts-comment
150
- // @ts-ignore
151
133
  signal: controller.signal,
152
134
  });
153
135
 
154
- if (timeout) clearTimeout(timeout);
136
+ if (timeout) {
137
+ clearTimeout(timeout);
138
+ }
155
139
 
156
140
  let respData: RawFMResponse;
157
141
  try {
@@ -161,29 +145,20 @@ export class BaseFetchAdapter implements Adapter {
161
145
  }
162
146
 
163
147
  if (!res.ok) {
164
- if (
165
- respData?.messages?.[0].code === "952" &&
166
- !retry &&
167
- this.refreshToken
168
- ) {
148
+ if (respData?.messages?.[0].code === "952" && !retry && this.refreshToken) {
169
149
  // token expired, get new token and retry once
170
150
  return this.request({ ...params, retry: true });
171
- } else {
172
- throw new FileMakerError(
173
- respData?.messages?.[0].code ?? "500",
174
- `Filemaker Data API failed with (${res.status}): ${JSON.stringify(
175
- respData,
176
- null,
177
- 2,
178
- )}`,
179
- );
180
151
  }
152
+ throw new FileMakerError(
153
+ respData?.messages?.[0].code ?? "500",
154
+ `Filemaker Data API failed with (${res.status}): ${JSON.stringify(respData, null, 2)}`,
155
+ );
181
156
  }
182
157
 
183
158
  return respData.response;
184
159
  };
185
160
 
186
- public list = async (opts: ListOptions): Promise<GetResponse> => {
161
+ list = async (opts: ListOptions): Promise<GetResponse> => {
187
162
  const { data, layout } = opts;
188
163
 
189
164
  const resp = await this.request({
@@ -195,7 +170,7 @@ export class BaseFetchAdapter implements Adapter {
195
170
  return resp as GetResponse;
196
171
  };
197
172
 
198
- public get = async (opts: GetOptions): Promise<GetResponse> => {
173
+ get = async (opts: GetOptions): Promise<GetResponse> => {
199
174
  const { data, layout } = opts;
200
175
  const resp = await this.request({
201
176
  url: `/layouts/${layout}/records/${data.recordId}`,
@@ -205,7 +180,7 @@ export class BaseFetchAdapter implements Adapter {
205
180
  return resp as GetResponse;
206
181
  };
207
182
 
208
- public find = async (opts: FindOptions): Promise<GetResponse> => {
183
+ find = async (opts: FindOptions): Promise<GetResponse> => {
209
184
  const { data, layout } = opts;
210
185
  const resp = await this.request({
211
186
  url: `/layouts/${layout}/_find`,
@@ -217,7 +192,7 @@ export class BaseFetchAdapter implements Adapter {
217
192
  return resp as GetResponse;
218
193
  };
219
194
 
220
- public create = async (opts: CreateOptions): Promise<CreateResponse> => {
195
+ create = async (opts: CreateOptions): Promise<CreateResponse> => {
221
196
  const { data, layout } = opts;
222
197
  const resp = await this.request({
223
198
  url: `/layouts/${layout}/records`,
@@ -229,7 +204,7 @@ export class BaseFetchAdapter implements Adapter {
229
204
  return resp as CreateResponse;
230
205
  };
231
206
 
232
- public update = async (opts: UpdateOptions): Promise<UpdateResponse> => {
207
+ update = async (opts: UpdateOptions): Promise<UpdateResponse> => {
233
208
  const {
234
209
  data: { recordId, ...data },
235
210
  layout,
@@ -244,7 +219,7 @@ export class BaseFetchAdapter implements Adapter {
244
219
  return resp as UpdateResponse;
245
220
  };
246
221
 
247
- public delete = async (opts: DeleteOptions): Promise<DeleteResponse> => {
222
+ delete = async (opts: DeleteOptions): Promise<DeleteResponse> => {
248
223
  const { data, layout } = opts;
249
224
  const resp = await this.request({
250
225
  url: `/layouts/${layout}/records/${data.recordId}`,
@@ -255,9 +230,7 @@ export class BaseFetchAdapter implements Adapter {
255
230
  return resp as DeleteResponse;
256
231
  };
257
232
 
258
- public layoutMetadata = async (
259
- opts: LayoutMetadataOptions,
260
- ): Promise<LayoutMetadataResponse> => {
233
+ layoutMetadata = async (opts: LayoutMetadataOptions): Promise<LayoutMetadataResponse> => {
261
234
  return (await this.request({
262
235
  url: `/layouts/${opts.layout}`,
263
236
  fetchOptions: opts.fetch,
@@ -268,7 +241,7 @@ export class BaseFetchAdapter implements Adapter {
268
241
  /**
269
242
  * Execute a script within the database
270
243
  */
271
- public executeScript = async (opts: ExecuteScriptOptions) => {
244
+ executeScript = async (opts: ExecuteScriptOptions) => {
272
245
  const { script, scriptParam, layout } = opts;
273
246
  const resp = await this.request({
274
247
  url: `/layouts/${layout}/script/${script}`,
@@ -282,7 +255,7 @@ export class BaseFetchAdapter implements Adapter {
282
255
  /**
283
256
  * Returns a list of available layouts on the database.
284
257
  */
285
- public layouts = async (opts?: Omit<BaseRequest, "layout">) => {
258
+ layouts = async (opts?: Omit<BaseRequest, "layout">) => {
286
259
  return (await this.request({
287
260
  url: "/layouts",
288
261
  fetchOptions: opts?.fetch,
@@ -293,7 +266,7 @@ export class BaseFetchAdapter implements Adapter {
293
266
  /**
294
267
  * Returns a list of available scripts on the database.
295
268
  */
296
- public scripts = async (opts?: Omit<BaseRequest, "layout">) => {
269
+ scripts = async (opts?: Omit<BaseRequest, "layout">) => {
297
270
  return (await this.request({
298
271
  url: "/scripts",
299
272
  fetchOptions: opts?.fetch,
@@ -301,9 +274,11 @@ export class BaseFetchAdapter implements Adapter {
301
274
  })) as ScriptsMetadataResponse;
302
275
  };
303
276
 
304
- public containerUpload = async (opts: ContainerUploadOptions) => {
277
+ containerUpload = async (opts: ContainerUploadOptions) => {
305
278
  let url = `/layouts/${opts.layout}/records/${opts.data.recordId}/containers/${opts.data.containerFieldName}`;
306
- if (opts.data.repetition) url += `/${opts.data.repetition}`;
279
+ if (opts.data.repetition) {
280
+ url += `/${opts.data.repetition}`;
281
+ }
307
282
  const formData = new FormData();
308
283
  formData.append("upload", opts.data.file);
309
284
 
@@ -319,7 +294,7 @@ export class BaseFetchAdapter implements Adapter {
319
294
  /**
320
295
  * Set global fields for the current session
321
296
  */
322
- public globals = async (
297
+ globals = async (
323
298
  opts: Omit<BaseRequest, "layout"> & {
324
299
  globalFields: Record<string, string | number>;
325
300
  },
@@ -1,11 +1,8 @@
1
1
  import { FileMakerError } from "../client-types.js";
2
2
  import memoryStore from "../tokenStore/memory.js";
3
3
  import type { TokenStoreDefinitions } from "../tokenStore/types.js";
4
- import type {
5
- BaseFetchAdapterOptions,
6
- GetTokenArguments,
7
- } from "./fetch-base-types.js";
8
4
  import { BaseFetchAdapter } from "./fetch-base.js";
5
+ import type { BaseFetchAdapterOptions, GetTokenArguments } from "./fetch-base-types.js";
9
6
 
10
7
  export interface FetchAdapterOptions extends BaseFetchAdapterOptions {
11
8
  auth: {
@@ -16,21 +13,24 @@ export interface FetchAdapterOptions extends BaseFetchAdapterOptions {
16
13
  }
17
14
 
18
15
  export class FetchAdapter extends BaseFetchAdapter {
19
- private username: string;
20
- private password: string;
21
- private tokenStore: Omit<TokenStoreDefinitions, "getKey">;
22
- private getTokenKey: Required<TokenStoreDefinitions>["getKey"];
16
+ private readonly username: string;
17
+ private readonly password: string;
18
+ private readonly tokenStore: Omit<TokenStoreDefinitions, "getKey">;
19
+ private readonly getTokenKey: Required<TokenStoreDefinitions>["getKey"];
23
20
 
24
21
  constructor(args: FetchAdapterOptions) {
25
22
  super({ ...args, refreshToken: true });
26
23
  this.username = args.auth.username;
27
24
  this.password = args.auth.password;
28
25
  this.tokenStore = args.tokenStore ?? memoryStore();
29
- this.getTokenKey =
30
- args.tokenStore?.getKey ?? (() => `${args.server}/${args.db}`);
26
+ this.getTokenKey = args.tokenStore?.getKey ?? (() => `${args.server}/${args.db}`);
31
27
 
32
- if (this.username === "") throw new Error("Username is required");
33
- if (this.password === "") throw new Error("Password is required");
28
+ if (this.username === "") {
29
+ throw new Error("Username is required");
30
+ }
31
+ if (this.password === "") {
32
+ throw new Error("Password is required");
33
+ }
34
34
  }
35
35
 
36
36
  /**
@@ -41,9 +41,7 @@ export class FetchAdapter extends BaseFetchAdapter {
41
41
  * @param args.refresh - If true, forces getting a new token instead of using cached token
42
42
  * @internal This method is intended for internal use, you should not need to use it in most cases.
43
43
  */
44
- public override getToken = async (
45
- args?: GetTokenArguments,
46
- ): Promise<string> => {
44
+ override getToken = async (args?: GetTokenArguments): Promise<string> => {
47
45
  const { refresh = false } = args ?? {};
48
46
  let token: string | null = null;
49
47
  if (!refresh) {
@@ -55,28 +53,25 @@ export class FetchAdapter extends BaseFetchAdapter {
55
53
  method: "POST",
56
54
  headers: {
57
55
  "Content-Type": "application/json",
58
- Authorization: `Basic ${Buffer.from(
59
- `${this.username}:${this.password}`,
60
- ).toString("base64")}`,
56
+ Authorization: `Basic ${Buffer.from(`${this.username}:${this.password}`).toString("base64")}`,
61
57
  },
62
58
  });
63
59
 
64
60
  if (!res.ok) {
65
61
  const data = await res.json();
66
- throw new FileMakerError(
67
- data.messages[0].code,
68
- data.messages[0].message,
69
- );
62
+ throw new FileMakerError(data.messages[0].code, data.messages[0].message);
70
63
  }
71
64
  token = res.headers.get("X-FM-Data-Access-Token");
72
- if (!token) throw new Error("Could not get token");
65
+ if (!token) {
66
+ throw new Error("Could not get token");
67
+ }
73
68
  this.tokenStore.setToken(this.getTokenKey(), token);
74
69
  }
75
70
 
76
71
  return token;
77
72
  };
78
73
 
79
- public disconnect = async (): Promise<void> => {
74
+ disconnect = async (): Promise<void> => {
80
75
  const token = await this.tokenStore.getToken(this.getTokenKey());
81
76
  if (token) {
82
77
  await this.request({
@@ -0,0 +1,224 @@
1
+ import type {
2
+ CreateResponse,
3
+ DeleteResponse,
4
+ GetResponse,
5
+ LayoutMetadataResponse,
6
+ RawFMResponse,
7
+ ScriptResponse,
8
+ UpdateResponse,
9
+ } from "../client-types.js";
10
+ import { FileMakerError } from "../client-types.js";
11
+ import type {
12
+ Adapter,
13
+ CreateOptions,
14
+ DeleteOptions,
15
+ ExecuteScriptOptions,
16
+ FindOptions,
17
+ GetOptions,
18
+ LayoutMetadataOptions,
19
+ ListOptions,
20
+ UpdateOptions,
21
+ } from "./core.js";
22
+
23
+ const TRAILING_SLASHES_REGEX = /\/+$/;
24
+
25
+ export interface FmHttpAdapterOptions {
26
+ /** Base URL of the local FM HTTP server (e.g. "http://localhost:3000") */
27
+ baseUrl: string;
28
+ /** Name of the connected FileMaker file */
29
+ connectedFileName: string;
30
+ /** Name of the FM script that executes Data API calls. Defaults to "execute_data_api" */
31
+ scriptName?: string;
32
+ }
33
+
34
+ export class FmHttpAdapter implements Adapter {
35
+ protected baseUrl: string;
36
+ protected connectedFileName: string;
37
+ protected scriptName: string;
38
+
39
+ constructor(options: FmHttpAdapterOptions) {
40
+ this.baseUrl = options.baseUrl.replace(TRAILING_SLASHES_REGEX, "");
41
+ this.connectedFileName = options.connectedFileName;
42
+ this.scriptName = options.scriptName ?? "execute_data_api";
43
+ }
44
+
45
+ protected request = async (params: {
46
+ layout: string;
47
+ body: object;
48
+ action?: "read" | "metaData" | "create" | "update" | "delete";
49
+ timeout?: number;
50
+ fetchOptions?: RequestInit;
51
+ }): Promise<unknown> => {
52
+ const { action = "read", layout, body, fetchOptions = {} } = params;
53
+
54
+ // Normalize underscore-prefixed keys to match FM script expectations
55
+ const normalizedBody: Record<string, unknown> = { ...body } as Record<string, unknown>;
56
+ if ("_offset" in normalizedBody) {
57
+ normalizedBody.offset = normalizedBody._offset;
58
+ normalizedBody._offset = undefined;
59
+ }
60
+ if ("_limit" in normalizedBody) {
61
+ normalizedBody.limit = normalizedBody._limit;
62
+ normalizedBody._limit = undefined;
63
+ }
64
+ if ("_sort" in normalizedBody) {
65
+ normalizedBody.sort = normalizedBody._sort;
66
+ normalizedBody._sort = undefined;
67
+ }
68
+
69
+ const scriptParam = JSON.stringify({
70
+ ...normalizedBody,
71
+ layouts: layout,
72
+ action,
73
+ version: "vLatest",
74
+ });
75
+
76
+ const controller = new AbortController();
77
+ let timeout: NodeJS.Timeout | null = null;
78
+ if (params.timeout) {
79
+ timeout = setTimeout(() => controller.abort(), params.timeout);
80
+ }
81
+
82
+ const headers = new Headers(fetchOptions?.headers);
83
+ headers.set("Content-Type", "application/json");
84
+
85
+ let res: Response;
86
+ try {
87
+ res = await fetch(`${this.baseUrl}/callScript`, {
88
+ ...fetchOptions,
89
+ method: "POST",
90
+ headers,
91
+ body: JSON.stringify({
92
+ connectedFileName: this.connectedFileName,
93
+ scriptName: this.scriptName,
94
+ data: scriptParam,
95
+ }),
96
+ signal: controller.signal,
97
+ });
98
+ } finally {
99
+ if (timeout) {
100
+ clearTimeout(timeout);
101
+ }
102
+ }
103
+
104
+ if (!res.ok) {
105
+ throw new FileMakerError(String(res.status), `FM HTTP request failed (${res.status}): ${await res.text()}`);
106
+ }
107
+
108
+ const raw = await res.json();
109
+ // The /callScript response wraps the script result as a string or object
110
+ let scriptResult: unknown;
111
+ try {
112
+ scriptResult = typeof raw.result === "string" ? JSON.parse(raw.result) : (raw.result ?? raw);
113
+ } catch (err) {
114
+ throw new FileMakerError(
115
+ "500",
116
+ `FM HTTP response parse failed: ${err instanceof Error ? err.message : String(err)}`,
117
+ );
118
+ }
119
+
120
+ const respData = scriptResult as RawFMResponse;
121
+
122
+ const errorCode = respData.messages?.[0]?.code;
123
+ if (errorCode !== undefined && errorCode !== "0") {
124
+ throw new FileMakerError(
125
+ errorCode,
126
+ `Filemaker Data API failed with (${errorCode}): ${JSON.stringify(respData, null, 2)}`,
127
+ );
128
+ }
129
+
130
+ return respData.response;
131
+ };
132
+
133
+ list = async (opts: ListOptions): Promise<GetResponse> => {
134
+ return (await this.request({
135
+ body: opts.data,
136
+ layout: opts.layout,
137
+ timeout: opts.timeout,
138
+ fetchOptions: opts.fetch,
139
+ })) as GetResponse;
140
+ };
141
+
142
+ get = async (opts: GetOptions): Promise<GetResponse> => {
143
+ return (await this.request({
144
+ body: opts.data,
145
+ layout: opts.layout,
146
+ timeout: opts.timeout,
147
+ fetchOptions: opts.fetch,
148
+ })) as GetResponse;
149
+ };
150
+
151
+ find = async (opts: FindOptions): Promise<GetResponse> => {
152
+ return (await this.request({
153
+ body: opts.data,
154
+ layout: opts.layout,
155
+ timeout: opts.timeout,
156
+ fetchOptions: opts.fetch,
157
+ })) as GetResponse;
158
+ };
159
+
160
+ create = async (opts: CreateOptions): Promise<CreateResponse> => {
161
+ return (await this.request({
162
+ action: "create",
163
+ body: opts.data,
164
+ layout: opts.layout,
165
+ timeout: opts.timeout,
166
+ fetchOptions: opts.fetch,
167
+ })) as CreateResponse;
168
+ };
169
+
170
+ update = async (opts: UpdateOptions): Promise<UpdateResponse> => {
171
+ return (await this.request({
172
+ action: "update",
173
+ body: opts.data,
174
+ layout: opts.layout,
175
+ timeout: opts.timeout,
176
+ fetchOptions: opts.fetch,
177
+ })) as UpdateResponse;
178
+ };
179
+
180
+ delete = async (opts: DeleteOptions): Promise<DeleteResponse> => {
181
+ return (await this.request({
182
+ action: "delete",
183
+ body: opts.data,
184
+ layout: opts.layout,
185
+ timeout: opts.timeout,
186
+ fetchOptions: opts.fetch,
187
+ })) as DeleteResponse;
188
+ };
189
+
190
+ layoutMetadata = async (opts: LayoutMetadataOptions): Promise<LayoutMetadataResponse> => {
191
+ return (await this.request({
192
+ action: "metaData",
193
+ layout: opts.layout,
194
+ body: {},
195
+ timeout: opts.timeout,
196
+ fetchOptions: opts.fetch,
197
+ })) as LayoutMetadataResponse;
198
+ };
199
+
200
+ executeScript = async (opts: ExecuteScriptOptions): Promise<ScriptResponse> => {
201
+ const res = await fetch(`${this.baseUrl}/callScript`, {
202
+ method: "POST",
203
+ headers: { "Content-Type": "application/json" },
204
+ body: JSON.stringify({
205
+ connectedFileName: this.connectedFileName,
206
+ scriptName: opts.script,
207
+ data: opts.scriptParam,
208
+ }),
209
+ });
210
+
211
+ if (!res.ok) {
212
+ throw new FileMakerError(String(res.status), `FM HTTP executeScript failed (${res.status}): ${await res.text()}`);
213
+ }
214
+
215
+ const raw = await res.json();
216
+ return {
217
+ scriptResult: typeof raw.result === "string" ? raw.result : JSON.stringify(raw.result),
218
+ } as ScriptResponse;
219
+ };
220
+
221
+ containerUpload = (): Promise<never> => {
222
+ throw new Error("Container upload is not supported via FM HTTP adapter");
223
+ };
224
+ }
@@ -16,7 +16,9 @@ export function isOttoAPIKey(key: string): key is OttoAPIKey {
16
16
  }
17
17
 
18
18
  export function isOttoAuth(auth: unknown): auth is OttoAuth {
19
- if (typeof auth !== "object" || auth === null) return false;
19
+ if (typeof auth !== "object" || auth === null) {
20
+ return false;
21
+ }
20
22
  return "apiKey" in auth;
21
23
  }
22
24
 
@@ -32,8 +34,8 @@ export type OttoAdapterOptions = BaseFetchAdapterOptions & {
32
34
  };
33
35
 
34
36
  export class OttoAdapter extends BaseFetchAdapter {
35
- private apiKey: OttoAPIKey | Otto3APIKey;
36
- private port: number | undefined;
37
+ private readonly apiKey: OttoAPIKey | Otto3APIKey;
38
+ private readonly port: number | undefined;
37
39
 
38
40
  constructor(options: OttoAdapterOptions) {
39
41
  super({ ...options, refreshToken: false });
@@ -47,13 +49,11 @@ export class OttoAdapter extends BaseFetchAdapter {
47
49
  // otto v4 uses default port, but with /otto prefix
48
50
  this.baseUrl.pathname = `otto/${this.baseUrl.pathname.replace(/^\/+|\/+$/g, "")}`;
49
51
  } else {
50
- throw new Error(
51
- "Invalid Otto API key format. Must start with 'KEY_' (Otto v3) or 'dk_' (OttoFMS)",
52
- );
52
+ throw new Error("Invalid Otto API key format. Must start with 'KEY_' (Otto v3) or 'dk_' (OttoFMS)");
53
53
  }
54
54
  }
55
55
 
56
- protected override getToken = async (): Promise<string> => {
57
- return this.apiKey;
56
+ protected override getToken = (): Promise<string> => {
57
+ return Promise.resolve(this.apiKey);
58
58
  };
59
59
  }