@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.
- package/bin/intent.js +20 -0
- package/dist/esm/adapters/core.d.ts +4 -4
- package/dist/esm/adapters/fetch-base-types.d.ts +4 -4
- package/dist/esm/adapters/fetch-base.d.ts +2 -2
- package/dist/esm/adapters/fetch-base.js +36 -49
- package/dist/esm/adapters/fetch-base.js.map +1 -1
- package/dist/esm/adapters/fetch.d.ts +5 -5
- package/dist/esm/adapters/fetch.js +11 -10
- package/dist/esm/adapters/fetch.js.map +1 -1
- package/dist/esm/adapters/fm-http.d.ts +32 -0
- package/dist/esm/adapters/fm-http.js +170 -0
- package/dist/esm/adapters/fm-http.js.map +1 -0
- package/dist/esm/adapters/otto.d.ts +2 -2
- package/dist/esm/adapters/otto.js +3 -5
- package/dist/esm/adapters/otto.js.map +1 -1
- package/dist/esm/client-types.d.ts +41 -41
- package/dist/esm/client-types.js +1 -6
- package/dist/esm/client-types.js.map +1 -1
- package/dist/esm/client.d.ts +28 -44
- package/dist/esm/client.js +75 -80
- package/dist/esm/client.js.map +1 -1
- package/dist/esm/index.d.ts +5 -6
- package/dist/esm/index.js +7 -5
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/tokenStore/index.d.ts +1 -1
- package/dist/esm/tokenStore/memory.js.map +1 -1
- package/dist/esm/tokenStore/types.d.ts +2 -2
- package/dist/esm/tokenStore/upstash.d.ts +1 -1
- package/dist/esm/utils.d.ts +7 -7
- package/dist/esm/utils.js +6 -4
- package/dist/esm/utils.js.map +1 -1
- package/package.json +37 -26
- package/skills/fmdapi-client/SKILL.md +490 -0
- package/src/adapters/core.ts +6 -9
- package/src/adapters/fetch-base-types.ts +5 -3
- package/src/adapters/fetch-base.ts +53 -78
- package/src/adapters/fetch.ts +19 -24
- package/src/adapters/fm-http.ts +224 -0
- package/src/adapters/otto.ts +8 -8
- package/src/client-types.ts +59 -83
- package/src/client.ts +131 -167
- package/src/index.ts +5 -9
- package/src/tokenStore/file.ts +2 -4
- package/src/tokenStore/index.ts +1 -1
- package/src/tokenStore/types.ts +2 -2
- package/src/tokenStore/upstash.ts +2 -5
- 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 === "")
|
|
41
|
+
if (this.db === "") {
|
|
42
|
+
throw new Error("Database name is required");
|
|
43
|
+
}
|
|
47
44
|
}
|
|
48
45
|
|
|
49
|
-
|
|
50
|
-
protected getToken = async (args?: GetTokenArguments): Promise<string> => {
|
|
46
|
+
protected getToken = (_args?: GetTokenArguments): Promise<string> => {
|
|
51
47
|
// method must be implemented in subclass
|
|
52
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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)
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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)
|
|
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
|
-
|
|
297
|
+
globals = async (
|
|
323
298
|
opts: Omit<BaseRequest, "layout"> & {
|
|
324
299
|
globalFields: Record<string, string | number>;
|
|
325
300
|
},
|
package/src/adapters/fetch.ts
CHANGED
|
@@ -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 === "")
|
|
33
|
-
|
|
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
|
-
|
|
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)
|
|
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
|
-
|
|
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
|
+
}
|
package/src/adapters/otto.ts
CHANGED
|
@@ -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)
|
|
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 =
|
|
57
|
-
return this.apiKey;
|
|
56
|
+
protected override getToken = (): Promise<string> => {
|
|
57
|
+
return Promise.resolve(this.apiKey);
|
|
58
58
|
};
|
|
59
59
|
}
|