@proofkit/fmdapi 5.0.0-beta.0
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/LICENSE.md +21 -0
- package/README.md +110 -0
- package/dist/esm/adapters/core.d.ts +55 -0
- package/dist/esm/adapters/fetch-base-types.d.ts +7 -0
- package/dist/esm/adapters/fetch-base.d.ts +60 -0
- package/dist/esm/adapters/fetch-base.js +256 -0
- package/dist/esm/adapters/fetch-base.js.map +1 -0
- package/dist/esm/adapters/fetch.d.ts +27 -0
- package/dist/esm/adapters/fetch.js +79 -0
- package/dist/esm/adapters/fetch.js.map +1 -0
- package/dist/esm/adapters/otto.d.ts +26 -0
- package/dist/esm/adapters/otto.js +29 -0
- package/dist/esm/adapters/otto.js.map +1 -0
- package/dist/esm/client-types.d.ts +226 -0
- package/dist/esm/client-types.js +41 -0
- package/dist/esm/client-types.js.map +1 -0
- package/dist/esm/client.d.ts +133 -0
- package/dist/esm/client.js +295 -0
- package/dist/esm/client.js.map +1 -0
- package/dist/esm/index.d.ts +8 -0
- package/dist/esm/index.js +16 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/tokenStore/file.d.ts +3 -0
- package/dist/esm/tokenStore/index.d.ts +3 -0
- package/dist/esm/tokenStore/memory.d.ts +3 -0
- package/dist/esm/tokenStore/memory.js +21 -0
- package/dist/esm/tokenStore/memory.js.map +1 -0
- package/dist/esm/tokenStore/types.d.ts +8 -0
- package/dist/esm/tokenStore/upstash.d.ts +6 -0
- package/dist/esm/utils.d.ts +8 -0
- package/dist/esm/utils.js +16 -0
- package/dist/esm/utils.js.map +1 -0
- package/package.json +99 -0
- package/src/adapters/core.ts +62 -0
- package/src/adapters/fetch-base-types.ts +5 -0
- package/src/adapters/fetch-base.ts +339 -0
- package/src/adapters/fetch.ts +95 -0
- package/src/adapters/otto.ts +59 -0
- package/src/client-types.ts +296 -0
- package/src/client.ts +534 -0
- package/src/index.ts +11 -0
- package/src/tokenStore/file.ts +33 -0
- package/src/tokenStore/index.ts +3 -0
- package/src/tokenStore/memory.ts +20 -0
- package/src/tokenStore/types.ts +7 -0
- package/src/tokenStore/upstash.ts +31 -0
- package/src/utils.ts +29 -0
- package/stubs/fmschema.config.stub.js +17 -0
- package/stubs/fmschema.config.stub.mjs +17 -0
|
@@ -0,0 +1,339 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
AllLayoutsMetadataResponse,
|
|
3
|
+
CreateResponse,
|
|
4
|
+
DeleteResponse,
|
|
5
|
+
GetResponse,
|
|
6
|
+
LayoutMetadataResponse,
|
|
7
|
+
PortalRanges,
|
|
8
|
+
RawFMResponse,
|
|
9
|
+
ScriptResponse,
|
|
10
|
+
ScriptsMetadataResponse,
|
|
11
|
+
UpdateResponse,
|
|
12
|
+
} from "../client-types.js";
|
|
13
|
+
import { FileMakerError } from "../client-types.js";
|
|
14
|
+
import type {
|
|
15
|
+
Adapter,
|
|
16
|
+
BaseRequest,
|
|
17
|
+
ContainerUploadOptions,
|
|
18
|
+
CreateOptions,
|
|
19
|
+
DeleteOptions,
|
|
20
|
+
FindOptions,
|
|
21
|
+
GetOptions,
|
|
22
|
+
LayoutMetadataOptions,
|
|
23
|
+
ListOptions,
|
|
24
|
+
UpdateOptions,
|
|
25
|
+
} from "./core.js";
|
|
26
|
+
import type {
|
|
27
|
+
BaseFetchAdapterOptions,
|
|
28
|
+
GetTokenArguments,
|
|
29
|
+
} from "./fetch-base-types.js";
|
|
30
|
+
|
|
31
|
+
export type ExecuteScriptOptions = BaseRequest & {
|
|
32
|
+
script: string;
|
|
33
|
+
scriptParam?: string;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export class BaseFetchAdapter implements Adapter {
|
|
37
|
+
protected server: string;
|
|
38
|
+
protected db: string;
|
|
39
|
+
private refreshToken: boolean;
|
|
40
|
+
baseUrl: URL;
|
|
41
|
+
|
|
42
|
+
constructor(options: BaseFetchAdapterOptions & { refreshToken?: boolean }) {
|
|
43
|
+
this.server = options.server;
|
|
44
|
+
this.db = options.db;
|
|
45
|
+
this.refreshToken = options.refreshToken ?? false;
|
|
46
|
+
this.baseUrl = new URL(
|
|
47
|
+
`${this.server}/fmi/data/vLatest/databases/${this.db}`,
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
if (this.db === "") throw new Error("Database name is required");
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
54
|
+
protected getToken = async (args?: GetTokenArguments): Promise<string> => {
|
|
55
|
+
// method must be implemented in subclass
|
|
56
|
+
throw new Error("getToken method not implemented by Fetch Adapter");
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
protected request = async (params: {
|
|
60
|
+
url: string;
|
|
61
|
+
body?: object | FormData;
|
|
62
|
+
query?: Record<string, string>;
|
|
63
|
+
method?: string;
|
|
64
|
+
retry?: boolean;
|
|
65
|
+
portalRanges?: PortalRanges;
|
|
66
|
+
timeout?: number;
|
|
67
|
+
fetchOptions?: RequestInit;
|
|
68
|
+
}): Promise<unknown> => {
|
|
69
|
+
const {
|
|
70
|
+
query,
|
|
71
|
+
body,
|
|
72
|
+
method = "GET",
|
|
73
|
+
retry = false,
|
|
74
|
+
fetchOptions = {},
|
|
75
|
+
} = params;
|
|
76
|
+
|
|
77
|
+
const url = new URL(`${this.baseUrl}${params.url}`);
|
|
78
|
+
|
|
79
|
+
if (query) {
|
|
80
|
+
const { _sort, ...rest } = query;
|
|
81
|
+
const searchParams = new URLSearchParams(rest);
|
|
82
|
+
if (query.portalRanges && typeof query.portalRanges === "object") {
|
|
83
|
+
for (const [portalName, value] of Object.entries(
|
|
84
|
+
query.portalRanges as PortalRanges,
|
|
85
|
+
)) {
|
|
86
|
+
if (value) {
|
|
87
|
+
if (value.offset && value.offset > 0) {
|
|
88
|
+
searchParams.set(
|
|
89
|
+
`_offset.${portalName}`,
|
|
90
|
+
value.offset.toString(),
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
if (value.limit) {
|
|
94
|
+
searchParams.set(`_limit.${portalName}`, value.limit.toString());
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
if (_sort) {
|
|
100
|
+
searchParams.set("_sort", JSON.stringify(_sort));
|
|
101
|
+
}
|
|
102
|
+
searchParams.delete("portalRanges");
|
|
103
|
+
url.search = searchParams.toString();
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (body && "portalRanges" in body) {
|
|
107
|
+
for (const [portalName, value] of Object.entries(
|
|
108
|
+
body.portalRanges as PortalRanges,
|
|
109
|
+
)) {
|
|
110
|
+
if (value) {
|
|
111
|
+
if (value.offset && value.offset > 0) {
|
|
112
|
+
url.searchParams.set(
|
|
113
|
+
`_offset.${portalName}`,
|
|
114
|
+
value.offset.toString(),
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
if (value.limit) {
|
|
118
|
+
url.searchParams.set(
|
|
119
|
+
`_limit.${portalName}`,
|
|
120
|
+
value.limit.toString(),
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
delete body.portalRanges;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const controller = new AbortController();
|
|
129
|
+
let timeout: NodeJS.Timeout | null = null;
|
|
130
|
+
if (params.timeout)
|
|
131
|
+
timeout = setTimeout(() => controller.abort(), params.timeout);
|
|
132
|
+
|
|
133
|
+
const token = await this.getToken({ refresh: retry });
|
|
134
|
+
|
|
135
|
+
const headers = new Headers(fetchOptions?.headers);
|
|
136
|
+
headers.set("Authorization", `Bearer ${token}`);
|
|
137
|
+
|
|
138
|
+
// Only set Content-Type for JSON bodies
|
|
139
|
+
if (!(body instanceof FormData)) {
|
|
140
|
+
headers.set("Content-Type", "application/json");
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const res = await fetch(url.toString(), {
|
|
144
|
+
...fetchOptions,
|
|
145
|
+
method,
|
|
146
|
+
body:
|
|
147
|
+
body instanceof FormData
|
|
148
|
+
? body
|
|
149
|
+
: body
|
|
150
|
+
? JSON.stringify(body)
|
|
151
|
+
: undefined,
|
|
152
|
+
headers,
|
|
153
|
+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
154
|
+
// @ts-ignore
|
|
155
|
+
signal: controller.signal,
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
if (timeout) clearTimeout(timeout);
|
|
159
|
+
|
|
160
|
+
let respData: RawFMResponse;
|
|
161
|
+
try {
|
|
162
|
+
respData = await res.json();
|
|
163
|
+
} catch {
|
|
164
|
+
respData = {};
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (!res.ok) {
|
|
168
|
+
if (
|
|
169
|
+
respData?.messages?.[0].code === "952" &&
|
|
170
|
+
!retry &&
|
|
171
|
+
this.refreshToken
|
|
172
|
+
) {
|
|
173
|
+
// token expired, get new token and retry once
|
|
174
|
+
return this.request({ ...params, retry: true });
|
|
175
|
+
} else {
|
|
176
|
+
throw new FileMakerError(
|
|
177
|
+
respData?.messages?.[0].code ?? "500",
|
|
178
|
+
`Filemaker Data API failed with (${res.status}): ${JSON.stringify(
|
|
179
|
+
respData,
|
|
180
|
+
null,
|
|
181
|
+
2,
|
|
182
|
+
)}`,
|
|
183
|
+
);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
return respData.response;
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
public list = async (opts: ListOptions): Promise<GetResponse> => {
|
|
191
|
+
const { data, layout } = opts;
|
|
192
|
+
|
|
193
|
+
const resp = await this.request({
|
|
194
|
+
url: `/layouts/${layout}/records`,
|
|
195
|
+
query: data as Record<string, string>,
|
|
196
|
+
fetchOptions: opts.fetch,
|
|
197
|
+
timeout: opts.timeout,
|
|
198
|
+
});
|
|
199
|
+
return resp as GetResponse;
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
public get = async (opts: GetOptions): Promise<GetResponse> => {
|
|
203
|
+
const { data, layout } = opts;
|
|
204
|
+
const resp = await this.request({
|
|
205
|
+
url: `/layouts/${layout}/records/${data.recordId}`,
|
|
206
|
+
fetchOptions: opts.fetch,
|
|
207
|
+
timeout: opts.timeout,
|
|
208
|
+
});
|
|
209
|
+
return resp as GetResponse;
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
public find = async (opts: FindOptions): Promise<GetResponse> => {
|
|
213
|
+
const { data, layout } = opts;
|
|
214
|
+
const resp = await this.request({
|
|
215
|
+
url: `/layouts/${layout}/_find`,
|
|
216
|
+
body: data,
|
|
217
|
+
method: "POST",
|
|
218
|
+
fetchOptions: opts.fetch,
|
|
219
|
+
timeout: opts.timeout,
|
|
220
|
+
});
|
|
221
|
+
return resp as GetResponse;
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
public create = async (opts: CreateOptions): Promise<CreateResponse> => {
|
|
225
|
+
const { data, layout } = opts;
|
|
226
|
+
const resp = await this.request({
|
|
227
|
+
url: `/layouts/${layout}/records`,
|
|
228
|
+
body: data,
|
|
229
|
+
method: "POST",
|
|
230
|
+
fetchOptions: opts.fetch,
|
|
231
|
+
timeout: opts.timeout,
|
|
232
|
+
});
|
|
233
|
+
return resp as CreateResponse;
|
|
234
|
+
};
|
|
235
|
+
|
|
236
|
+
public update = async (opts: UpdateOptions): Promise<UpdateResponse> => {
|
|
237
|
+
const {
|
|
238
|
+
data: { recordId, ...data },
|
|
239
|
+
layout,
|
|
240
|
+
} = opts;
|
|
241
|
+
const resp = await this.request({
|
|
242
|
+
url: `/layouts/${layout}/records/${recordId}`,
|
|
243
|
+
body: data,
|
|
244
|
+
method: "PATCH",
|
|
245
|
+
fetchOptions: opts.fetch,
|
|
246
|
+
timeout: opts.timeout,
|
|
247
|
+
});
|
|
248
|
+
return resp as UpdateResponse;
|
|
249
|
+
};
|
|
250
|
+
|
|
251
|
+
public delete = async (opts: DeleteOptions): Promise<DeleteResponse> => {
|
|
252
|
+
const { data, layout } = opts;
|
|
253
|
+
const resp = await this.request({
|
|
254
|
+
url: `/layouts/${layout}/records/${data.recordId}`,
|
|
255
|
+
method: "DELETE",
|
|
256
|
+
fetchOptions: opts.fetch,
|
|
257
|
+
timeout: opts.timeout,
|
|
258
|
+
});
|
|
259
|
+
return resp as DeleteResponse;
|
|
260
|
+
};
|
|
261
|
+
|
|
262
|
+
public layoutMetadata = async (
|
|
263
|
+
opts: LayoutMetadataOptions,
|
|
264
|
+
): Promise<LayoutMetadataResponse> => {
|
|
265
|
+
return (await this.request({
|
|
266
|
+
url: `/layouts/${opts.layout}`,
|
|
267
|
+
fetchOptions: opts.fetch,
|
|
268
|
+
timeout: opts.timeout,
|
|
269
|
+
})) as LayoutMetadataResponse;
|
|
270
|
+
};
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Execute a script within the database
|
|
274
|
+
*/
|
|
275
|
+
public executeScript = async (opts: ExecuteScriptOptions) => {
|
|
276
|
+
const { script, scriptParam, layout } = opts;
|
|
277
|
+
const resp = await this.request({
|
|
278
|
+
url: `/layouts/${layout}/script/${script}`,
|
|
279
|
+
query: scriptParam ? { "script.param": scriptParam } : undefined,
|
|
280
|
+
fetchOptions: opts.fetch,
|
|
281
|
+
timeout: opts.timeout,
|
|
282
|
+
});
|
|
283
|
+
return resp as ScriptResponse;
|
|
284
|
+
};
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* Returns a list of available layouts on the database.
|
|
288
|
+
*/
|
|
289
|
+
public layouts = async (opts?: Omit<BaseRequest, "layout">) => {
|
|
290
|
+
return (await this.request({
|
|
291
|
+
url: "/layouts",
|
|
292
|
+
fetchOptions: opts?.fetch,
|
|
293
|
+
timeout: opts?.timeout,
|
|
294
|
+
})) as AllLayoutsMetadataResponse;
|
|
295
|
+
};
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* Returns a list of available scripts on the database.
|
|
299
|
+
*/
|
|
300
|
+
public scripts = async (opts?: Omit<BaseRequest, "layout">) => {
|
|
301
|
+
return (await this.request({
|
|
302
|
+
url: "/scripts",
|
|
303
|
+
fetchOptions: opts?.fetch,
|
|
304
|
+
timeout: opts?.timeout,
|
|
305
|
+
})) as ScriptsMetadataResponse;
|
|
306
|
+
};
|
|
307
|
+
|
|
308
|
+
public containerUpload = async (opts: ContainerUploadOptions) => {
|
|
309
|
+
let url = `/layouts/${opts.layout}/records/${opts.data.recordId}/containers/${opts.data.containerFieldName}`;
|
|
310
|
+
if (opts.data.repetition) url += `/${opts.data.repetition}`;
|
|
311
|
+
const formData = new FormData();
|
|
312
|
+
formData.append("upload", opts.data.file);
|
|
313
|
+
|
|
314
|
+
await this.request({
|
|
315
|
+
url,
|
|
316
|
+
method: "POST",
|
|
317
|
+
body: formData,
|
|
318
|
+
timeout: opts.timeout,
|
|
319
|
+
fetchOptions: opts.fetch,
|
|
320
|
+
});
|
|
321
|
+
};
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* Set global fields for the current session
|
|
325
|
+
*/
|
|
326
|
+
public globals = async (
|
|
327
|
+
opts: Omit<BaseRequest, "layout"> & {
|
|
328
|
+
globalFields: Record<string, string | number>;
|
|
329
|
+
},
|
|
330
|
+
) => {
|
|
331
|
+
return (await this.request({
|
|
332
|
+
url: "/globals",
|
|
333
|
+
method: "PATCH",
|
|
334
|
+
body: { globalFields: opts.globalFields },
|
|
335
|
+
fetchOptions: opts?.fetch,
|
|
336
|
+
timeout: opts?.timeout,
|
|
337
|
+
})) as Record<string, never>;
|
|
338
|
+
};
|
|
339
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { FileMakerError } from "../client-types.js";
|
|
2
|
+
import memoryStore from "../tokenStore/memory.js";
|
|
3
|
+
import type { TokenStoreDefinitions } from "../tokenStore/types.js";
|
|
4
|
+
import type {
|
|
5
|
+
BaseFetchAdapterOptions,
|
|
6
|
+
GetTokenArguments,
|
|
7
|
+
} from "./fetch-base-types.js";
|
|
8
|
+
import { BaseFetchAdapter } from "./fetch-base.js";
|
|
9
|
+
|
|
10
|
+
export interface FetchAdapterOptions extends BaseFetchAdapterOptions {
|
|
11
|
+
auth: {
|
|
12
|
+
username: string;
|
|
13
|
+
password: string;
|
|
14
|
+
};
|
|
15
|
+
tokenStore?: TokenStoreDefinitions;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
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"];
|
|
23
|
+
|
|
24
|
+
constructor(args: FetchAdapterOptions) {
|
|
25
|
+
super({ ...args, refreshToken: true });
|
|
26
|
+
this.username = args.auth.username;
|
|
27
|
+
this.password = args.auth.password;
|
|
28
|
+
this.tokenStore = args.tokenStore ?? memoryStore();
|
|
29
|
+
this.getTokenKey =
|
|
30
|
+
args.tokenStore?.getKey ?? (() => `${args.server}/${args.db}`);
|
|
31
|
+
|
|
32
|
+
if (this.username === "") throw new Error("Username is required");
|
|
33
|
+
if (this.password === "") throw new Error("Password is required");
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Gets a FileMaker Data API token for authentication.
|
|
38
|
+
*
|
|
39
|
+
* This token is **NOT** guaranteed to be valid, since it expires 15 minutes after the last use. Pass `refresh=true` to forcibly get a fresh token
|
|
40
|
+
*
|
|
41
|
+
* @param args.refresh - If true, forces getting a new token instead of using cached token
|
|
42
|
+
* @internal This method is intended for internal use, you should not need to use it in most cases.
|
|
43
|
+
*/
|
|
44
|
+
public override getToken = async (
|
|
45
|
+
args?: GetTokenArguments,
|
|
46
|
+
): Promise<string> => {
|
|
47
|
+
const { refresh = false } = args ?? {};
|
|
48
|
+
let token: string | null = null;
|
|
49
|
+
if (!refresh) {
|
|
50
|
+
token = await this.tokenStore.getToken(this.getTokenKey());
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (!token) {
|
|
54
|
+
const res = await fetch(`${this.baseUrl}/sessions`, {
|
|
55
|
+
method: "POST",
|
|
56
|
+
headers: {
|
|
57
|
+
"Content-Type": "application/json",
|
|
58
|
+
Authorization: `Basic ${Buffer.from(
|
|
59
|
+
`${this.username}:${this.password}`,
|
|
60
|
+
).toString("base64")}`,
|
|
61
|
+
},
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
if (!res.ok) {
|
|
65
|
+
const data = await res.json();
|
|
66
|
+
throw new FileMakerError(
|
|
67
|
+
data.messages[0].code,
|
|
68
|
+
data.messages[0].message,
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
token = res.headers.get("X-FM-Data-Access-Token");
|
|
72
|
+
if (!token) throw new Error("Could not get token");
|
|
73
|
+
this.tokenStore.setToken(this.getTokenKey(), token);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return token;
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
public disconnect = async (): Promise<void> => {
|
|
80
|
+
const token = await this.tokenStore.getToken(this.getTokenKey());
|
|
81
|
+
if (token) {
|
|
82
|
+
await this.request({
|
|
83
|
+
url: `/sessions/${token}`,
|
|
84
|
+
method: "DELETE",
|
|
85
|
+
fetchOptions: {
|
|
86
|
+
headers: {
|
|
87
|
+
Authorization: `Bearer ${token}`,
|
|
88
|
+
"Content-Type": "application/json",
|
|
89
|
+
},
|
|
90
|
+
},
|
|
91
|
+
});
|
|
92
|
+
this.tokenStore.clearToken(this.getTokenKey());
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { BaseFetchAdapter } from "./fetch-base.js";
|
|
2
|
+
import type { BaseFetchAdapterOptions } from "./fetch-base-types.js";
|
|
3
|
+
|
|
4
|
+
export type Otto3APIKey = `KEY_${string}`;
|
|
5
|
+
export type OttoFMSAPIKey = `dk_${string}`;
|
|
6
|
+
export type OttoAPIKey = Otto3APIKey | OttoFMSAPIKey;
|
|
7
|
+
|
|
8
|
+
export function isOtto3APIKey(key: string): key is Otto3APIKey {
|
|
9
|
+
return key.startsWith("KEY_");
|
|
10
|
+
}
|
|
11
|
+
export function isOttoFMSAPIKey(key: string): key is OttoFMSAPIKey {
|
|
12
|
+
return key.startsWith("dk_");
|
|
13
|
+
}
|
|
14
|
+
export function isOttoAPIKey(key: string): key is OttoAPIKey {
|
|
15
|
+
return isOtto3APIKey(key) || isOttoFMSAPIKey(key);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function isOttoAuth(auth: unknown): auth is OttoAuth {
|
|
19
|
+
if (typeof auth !== "object" || auth === null) return false;
|
|
20
|
+
return "apiKey" in auth;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
type OttoAuth =
|
|
24
|
+
| {
|
|
25
|
+
apiKey: Otto3APIKey;
|
|
26
|
+
ottoPort?: number;
|
|
27
|
+
}
|
|
28
|
+
| { apiKey: OttoFMSAPIKey; ottoPort?: never };
|
|
29
|
+
|
|
30
|
+
export type OttoAdapterOptions = BaseFetchAdapterOptions & {
|
|
31
|
+
auth: OttoAuth;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export class OttoAdapter extends BaseFetchAdapter {
|
|
35
|
+
private apiKey: OttoAPIKey | Otto3APIKey;
|
|
36
|
+
private port: number | undefined;
|
|
37
|
+
|
|
38
|
+
constructor(options: OttoAdapterOptions) {
|
|
39
|
+
super({ ...options, refreshToken: false });
|
|
40
|
+
this.apiKey = options.auth.apiKey;
|
|
41
|
+
this.port = options.auth.ottoPort;
|
|
42
|
+
|
|
43
|
+
if (this.apiKey.startsWith("KEY_")) {
|
|
44
|
+
// otto v3 uses port 3030
|
|
45
|
+
this.baseUrl.port = (this.port ?? 3030).toString();
|
|
46
|
+
} else if (this.apiKey.startsWith("dk_")) {
|
|
47
|
+
// otto v4 uses default port, but with /otto prefix
|
|
48
|
+
this.baseUrl.pathname = `otto/${this.baseUrl.pathname.replace(/^\/+|\/+$/g, "")}`;
|
|
49
|
+
} else {
|
|
50
|
+
throw new Error(
|
|
51
|
+
"Invalid Otto API key format. Must start with 'KEY_' (Otto v3) or 'dk_' (OttoFMS)",
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
protected override getToken = async (): Promise<string> => {
|
|
57
|
+
return this.apiKey;
|
|
58
|
+
};
|
|
59
|
+
}
|