@proofkit/fmdapi 5.0.3-beta.1 → 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.
@@ -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/index.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  export { FetchAdapter } from "./adapters/fetch.js";
2
+ export { FmHttpAdapter, type FmHttpAdapterOptions } from "./adapters/fm-http.js";
2
3
  export { OttoAdapter, type OttoAPIKey } from "./adapters/otto.js";
3
4
  export { DataApi, DataApi as default } from "./client.js";
4
5
  export * as clientTypes from "./client-types.js";