@tryfinch/finch-api 3.1.1 → 4.0.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/CHANGELOG.md +63 -0
- package/README.md +39 -15
- package/_shims/ReadableStream.d.ts +38 -0
- package/_shims/ReadableStream.js +5 -0
- package/_shims/ReadableStream.mjs +7 -0
- package/_shims/ReadableStream.node.d.ts +6 -0
- package/_shims/ReadableStream.node.d.ts.map +1 -0
- package/_shims/ReadableStream.node.js +14 -0
- package/_shims/ReadableStream.node.js.map +1 -0
- package/_shims/ReadableStream.node.mjs +3 -0
- package/_shims/ReadableStream.node.mjs.map +1 -0
- package/_shims/fetch.d.ts +8 -1
- package/_shims/fetch.js +1 -1
- package/_shims/fetch.node.d.ts +11 -1
- package/core.d.ts +87 -38
- package/core.d.ts.map +1 -1
- package/core.js +245 -114
- package/core.js.map +1 -1
- package/core.mjs +231 -74
- package/core.mjs.map +1 -1
- package/error.d.ts +1 -0
- package/error.d.ts.map +1 -1
- package/error.js +12 -3
- package/error.js.map +1 -1
- package/error.mjs +12 -3
- package/error.mjs.map +1 -1
- package/index.d.mts +3 -5
- package/index.d.ts +3 -5
- package/index.d.ts.map +1 -1
- package/index.js +24 -27
- package/index.js.map +1 -1
- package/index.mjs +24 -27
- package/index.mjs.map +1 -1
- package/package.json +5 -7
- package/pagination.d.ts +17 -8
- package/pagination.d.ts.map +1 -1
- package/pagination.js +26 -26
- package/pagination.js.map +1 -1
- package/pagination.mjs +26 -26
- package/pagination.mjs.map +1 -1
- package/resources/account.d.ts +2 -2
- package/resources/account.d.ts.map +1 -1
- package/resources/ats/applications.d.ts +6 -3
- package/resources/ats/applications.d.ts.map +1 -1
- package/resources/ats/applications.js.map +1 -1
- package/resources/ats/applications.mjs.map +1 -1
- package/resources/ats/ats.d.ts.map +1 -1
- package/resources/ats/ats.js.map +1 -1
- package/resources/ats/candidates.d.ts +6 -3
- package/resources/ats/candidates.d.ts.map +1 -1
- package/resources/ats/candidates.js.map +1 -1
- package/resources/ats/candidates.mjs.map +1 -1
- package/resources/ats/jobs.d.ts +3 -3
- package/resources/ats/jobs.d.ts.map +1 -1
- package/resources/ats/offers.d.ts +3 -3
- package/resources/ats/offers.d.ts.map +1 -1
- package/resources/ats/stages.d.ts +1 -1
- package/resources/ats/stages.d.ts.map +1 -1
- package/resources/hris/benefits/benefits.d.ts +9 -10
- package/resources/hris/benefits/benefits.d.ts.map +1 -1
- package/resources/hris/benefits/benefits.js.map +1 -1
- package/resources/hris/benefits/benefits.mjs.map +1 -1
- package/resources/hris/benefits/individuals.d.ts +6 -6
- package/resources/hris/benefits/individuals.d.ts.map +1 -1
- package/resources/hris/company.d.ts +1 -1
- package/resources/hris/company.d.ts.map +1 -1
- package/resources/hris/directory.d.ts +2 -2
- package/resources/hris/directory.d.ts.map +1 -1
- package/resources/hris/hris.d.ts.map +1 -1
- package/resources/hris/hris.js.map +1 -1
- package/resources/hris/index.d.ts.map +1 -1
- package/resources/hris/index.js +7 -7
- package/resources/hris/index.js.map +1 -1
- package/resources/hris/individuals/employment-data.d.ts +1 -1
- package/resources/hris/individuals/employment-data.d.ts.map +1 -1
- package/resources/hris/individuals/employment-data.js.map +1 -1
- package/resources/hris/individuals/individuals.d.ts +4 -2
- package/resources/hris/individuals/individuals.d.ts.map +1 -1
- package/resources/hris/individuals/individuals.js.map +1 -1
- package/resources/hris/individuals/individuals.mjs.map +1 -1
- package/resources/hris/pay-statements.d.ts +1 -1
- package/resources/hris/pay-statements.d.ts.map +1 -1
- package/resources/hris/payments.d.ts +4 -1
- package/resources/hris/payments.d.ts.map +1 -1
- package/resources/hris/payments.js.map +1 -1
- package/resources/hris/payments.mjs.map +1 -1
- package/resources/index.d.ts +1 -0
- package/resources/index.d.ts.map +1 -1
- package/resources/index.js.map +1 -1
- package/resources/index.mjs.map +1 -1
- package/resources/providers.d.ts +1 -1
- package/resources/providers.d.ts.map +1 -1
- package/src/_shims/ReadableStream.d.ts +38 -0
- package/src/_shims/ReadableStream.js +5 -0
- package/src/_shims/ReadableStream.mjs +7 -0
- package/src/_shims/ReadableStream.node.ts +6 -0
- package/src/_shims/fetch.d.ts +8 -1
- package/src/_shims/fetch.deno.ts +23 -0
- package/src/_shims/fetch.js +1 -1
- package/src/_shims/fetch.node.d.ts +11 -1
- package/src/_shims/formdata.deno.ts +16 -0
- package/src/core.ts +302 -93
- package/src/error.ts +11 -1
- package/src/index.ts +28 -27
- package/src/pagination.ts +34 -29
- package/src/resources/account.ts +3 -3
- package/src/resources/ats/applications.ts +8 -5
- package/src/resources/ats/ats.ts +1 -1
- package/src/resources/ats/candidates.ts +8 -5
- package/src/resources/ats/jobs.ts +5 -5
- package/src/resources/ats/offers.ts +5 -5
- package/src/resources/ats/stages.ts +2 -2
- package/src/resources/hris/benefits/benefits.ts +12 -13
- package/src/resources/hris/benefits/individuals.ts +9 -9
- package/src/resources/hris/company.ts +2 -2
- package/src/resources/hris/directory.ts +4 -4
- package/src/resources/hris/hris.ts +1 -1
- package/src/resources/hris/index.ts +2 -2
- package/src/resources/hris/individuals/employment-data.ts +2 -2
- package/src/resources/hris/individuals/individuals.ts +6 -4
- package/src/resources/hris/pay-statements.ts +2 -2
- package/src/resources/hris/payments.ts +5 -2
- package/src/resources/index.ts +1 -0
- package/src/resources/providers.ts +2 -2
- package/src/uploads.ts +7 -6
- package/src/version.ts +1 -1
- package/uploads.d.ts +5 -4
- package/uploads.d.ts.map +1 -1
- package/uploads.js +2 -2
- package/uploads.js.map +1 -1
- package/uploads.mjs +2 -2
- package/uploads.mjs.map +1 -1
- package/version.d.ts +1 -1
- package/version.js +1 -1
- package/version.mjs +1 -1
package/src/core.ts
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import * as qs from 'qs';
|
|
2
1
|
import { VERSION } from './version';
|
|
3
2
|
import { APIError, APIConnectionError, APIConnectionTimeoutError, APIUserAbortError } from './error';
|
|
4
3
|
import type { Readable } from './_shims/node-readable';
|
|
@@ -10,6 +9,7 @@ import {
|
|
|
10
9
|
type RequestInit,
|
|
11
10
|
type Response,
|
|
12
11
|
} from './_shims/fetch.js';
|
|
12
|
+
export { type Response };
|
|
13
13
|
import { isMultipartBody } from './uploads';
|
|
14
14
|
export {
|
|
15
15
|
maybeMultipartFormRequestOptions,
|
|
@@ -22,6 +22,100 @@ const MAX_RETRIES = 2;
|
|
|
22
22
|
|
|
23
23
|
export type Fetch = (url: RequestInfo, init?: RequestInit) => Promise<Response>;
|
|
24
24
|
|
|
25
|
+
type PromiseOrValue<T> = T | Promise<T>;
|
|
26
|
+
|
|
27
|
+
type APIResponseProps = {
|
|
28
|
+
response: Response;
|
|
29
|
+
options: FinalRequestOptions;
|
|
30
|
+
controller: AbortController;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
async function defaultParseResponse<T>(props: APIResponseProps): Promise<T> {
|
|
34
|
+
const { response } = props;
|
|
35
|
+
const contentType = response.headers.get('content-type');
|
|
36
|
+
if (contentType?.includes('application/json')) {
|
|
37
|
+
const json = await response.json();
|
|
38
|
+
|
|
39
|
+
debug('response', response.status, response.url, response.headers, json);
|
|
40
|
+
|
|
41
|
+
return json as T;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// TODO handle blob, arraybuffer, other content types, etc.
|
|
45
|
+
const text = await response.text();
|
|
46
|
+
debug('response', response.status, response.url, response.headers, text);
|
|
47
|
+
return text as T;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* A subclass of `Promise` providing additional helper methods
|
|
52
|
+
* for interacting with the SDK.
|
|
53
|
+
*/
|
|
54
|
+
export class APIPromise<T> extends Promise<T> {
|
|
55
|
+
private parsedPromise: Promise<T> | undefined;
|
|
56
|
+
|
|
57
|
+
constructor(
|
|
58
|
+
private responsePromise: Promise<APIResponseProps>,
|
|
59
|
+
private parseResponse: (props: APIResponseProps) => PromiseOrValue<T> = defaultParseResponse,
|
|
60
|
+
) {
|
|
61
|
+
super((resolve) => {
|
|
62
|
+
// this is maybe a bit weird but this has to be a no-op to not implicitly
|
|
63
|
+
// parse the response body; instead .then, .catch, .finally are overridden
|
|
64
|
+
// to parse the response
|
|
65
|
+
resolve(null as any);
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
_thenUnwrap<U>(transform: (data: T) => U): APIPromise<U> {
|
|
70
|
+
return new APIPromise(this.responsePromise, async (props) => transform(await this.parseResponse(props)));
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Gets the raw `Response` instance instead of parsing the response
|
|
75
|
+
* data.
|
|
76
|
+
*
|
|
77
|
+
* If you want to parse the response body but still get the `Response`
|
|
78
|
+
* instance, you can use {@link withResponse()}.
|
|
79
|
+
*/
|
|
80
|
+
asResponse(): Promise<Response> {
|
|
81
|
+
return this.responsePromise.then((p) => p.response);
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Gets the parsed response data and the raw `Response` instance.
|
|
85
|
+
*
|
|
86
|
+
* If you just want to get the raw `Response` instance without parsing it,
|
|
87
|
+
* you can use {@link asResponse()}.
|
|
88
|
+
*/
|
|
89
|
+
async withResponse(): Promise<{ data: T; response: Response }> {
|
|
90
|
+
const [data, response] = await Promise.all([this.parse(), this.asResponse()]);
|
|
91
|
+
return { data, response };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
private parse(): Promise<T> {
|
|
95
|
+
if (!this.parsedPromise) {
|
|
96
|
+
this.parsedPromise = this.responsePromise.then(this.parseResponse);
|
|
97
|
+
}
|
|
98
|
+
return this.parsedPromise;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
override then<TResult1 = T, TResult2 = never>(
|
|
102
|
+
onfulfilled?: ((value: T) => TResult1 | PromiseLike<TResult1>) | undefined | null,
|
|
103
|
+
onrejected?: ((reason: any) => TResult2 | PromiseLike<TResult2>) | undefined | null,
|
|
104
|
+
): Promise<TResult1 | TResult2> {
|
|
105
|
+
return this.parse().then(onfulfilled, onrejected);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
override catch<TResult = never>(
|
|
109
|
+
onrejected?: ((reason: any) => TResult | PromiseLike<TResult>) | undefined | null,
|
|
110
|
+
): Promise<T | TResult> {
|
|
111
|
+
return this.parse().catch(onrejected);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
override finally(onfinally?: (() => void) | undefined | null): Promise<T> {
|
|
115
|
+
return this.parse().finally(onfinally);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
25
119
|
export abstract class APIClient {
|
|
26
120
|
baseURL: string;
|
|
27
121
|
maxRetries: number;
|
|
@@ -34,7 +128,7 @@ export abstract class APIClient {
|
|
|
34
128
|
constructor({
|
|
35
129
|
baseURL,
|
|
36
130
|
maxRetries,
|
|
37
|
-
timeout =
|
|
131
|
+
timeout = 60000, // 1 minute
|
|
38
132
|
httpAgent,
|
|
39
133
|
fetch: overridenFetch,
|
|
40
134
|
}: {
|
|
@@ -81,43 +175,43 @@ export abstract class APIClient {
|
|
|
81
175
|
*/
|
|
82
176
|
protected validateHeaders(headers: Headers, customHeaders: Headers) {}
|
|
83
177
|
|
|
84
|
-
/**
|
|
85
|
-
* Override this to add your own qs.stringify options, for example:
|
|
86
|
-
*
|
|
87
|
-
* {
|
|
88
|
-
* ...super.qsOptions(),
|
|
89
|
-
* strictNullHandling: true,
|
|
90
|
-
* }
|
|
91
|
-
*/
|
|
92
|
-
protected qsOptions(): qs.IStringifyOptions | undefined {
|
|
93
|
-
return {};
|
|
94
|
-
}
|
|
95
|
-
|
|
96
178
|
protected defaultIdempotencyKey(): string {
|
|
97
179
|
return `stainless-node-retry-${uuid4()}`;
|
|
98
180
|
}
|
|
99
181
|
|
|
100
|
-
get<Req extends {}, Rsp>(path: string, opts?: RequestOptions<Req
|
|
101
|
-
return this.
|
|
182
|
+
get<Req extends {}, Rsp>(path: string, opts?: PromiseOrValue<RequestOptions<Req>>): APIPromise<Rsp> {
|
|
183
|
+
return this.methodRequest('get', path, opts);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
post<Req extends {}, Rsp>(path: string, opts?: PromiseOrValue<RequestOptions<Req>>): APIPromise<Rsp> {
|
|
187
|
+
return this.methodRequest('post', path, opts);
|
|
102
188
|
}
|
|
103
|
-
|
|
104
|
-
|
|
189
|
+
|
|
190
|
+
patch<Req extends {}, Rsp>(path: string, opts?: PromiseOrValue<RequestOptions<Req>>): APIPromise<Rsp> {
|
|
191
|
+
return this.methodRequest('patch', path, opts);
|
|
105
192
|
}
|
|
106
|
-
|
|
107
|
-
|
|
193
|
+
|
|
194
|
+
put<Req extends {}, Rsp>(path: string, opts?: PromiseOrValue<RequestOptions<Req>>): APIPromise<Rsp> {
|
|
195
|
+
return this.methodRequest('put', path, opts);
|
|
108
196
|
}
|
|
109
|
-
|
|
110
|
-
|
|
197
|
+
|
|
198
|
+
delete<Req extends {}, Rsp>(path: string, opts?: PromiseOrValue<RequestOptions<Req>>): APIPromise<Rsp> {
|
|
199
|
+
return this.methodRequest('delete', path, opts);
|
|
111
200
|
}
|
|
112
|
-
|
|
113
|
-
|
|
201
|
+
|
|
202
|
+
private methodRequest<Req extends {}, Rsp>(
|
|
203
|
+
method: HTTPMethod,
|
|
204
|
+
path: string,
|
|
205
|
+
opts?: PromiseOrValue<RequestOptions<Req>>,
|
|
206
|
+
): APIPromise<Rsp> {
|
|
207
|
+
return this.request(Promise.resolve(opts).then((opts) => ({ method, path, ...opts })));
|
|
114
208
|
}
|
|
115
209
|
|
|
116
210
|
getAPIList<Item, PageClass extends AbstractPage<Item> = AbstractPage<Item>>(
|
|
117
211
|
path: string,
|
|
118
212
|
Page: new (...args: any[]) => PageClass,
|
|
119
213
|
opts?: RequestOptions<any>,
|
|
120
|
-
): PagePromise<PageClass> {
|
|
214
|
+
): PagePromise<PageClass, Item> {
|
|
121
215
|
return this.requestAPIList(Page, { method: 'get', path, ...opts });
|
|
122
216
|
}
|
|
123
217
|
|
|
@@ -127,9 +221,11 @@ export abstract class APIClient {
|
|
|
127
221
|
return Buffer.byteLength(body, 'utf8').toString();
|
|
128
222
|
}
|
|
129
223
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
224
|
+
if (typeof TextEncoder !== 'undefined') {
|
|
225
|
+
const encoder = new TextEncoder();
|
|
226
|
+
const encoded = encoder.encode(body);
|
|
227
|
+
return encoded.length.toString();
|
|
228
|
+
}
|
|
133
229
|
}
|
|
134
230
|
|
|
135
231
|
return null;
|
|
@@ -151,7 +247,10 @@ export abstract class APIClient {
|
|
|
151
247
|
const timeout = options.timeout ?? this.timeout;
|
|
152
248
|
const httpAgent = options.httpAgent ?? this.httpAgent ?? getDefaultAgent(url);
|
|
153
249
|
const minAgentTimeout = timeout + 1000;
|
|
154
|
-
if (
|
|
250
|
+
if (
|
|
251
|
+
typeof (httpAgent as any)?.options?.timeout === 'number' &&
|
|
252
|
+
minAgentTimeout > ((httpAgent as any).options.timeout ?? 0)
|
|
253
|
+
) {
|
|
155
254
|
// Allow any given request to bump our agent active socket timeout.
|
|
156
255
|
// This may seem strange, but leaking active sockets should be rare and not particularly problematic,
|
|
157
256
|
// and without mutating agent we would need to create more of them.
|
|
@@ -209,14 +308,31 @@ export abstract class APIClient {
|
|
|
209
308
|
return APIError.generate(status, error, message, headers);
|
|
210
309
|
}
|
|
211
310
|
|
|
212
|
-
|
|
213
|
-
options: FinalRequestOptions<Req
|
|
214
|
-
|
|
215
|
-
):
|
|
311
|
+
request<Req extends {}, Rsp>(
|
|
312
|
+
options: PromiseOrValue<FinalRequestOptions<Req>>,
|
|
313
|
+
remainingRetries: number | null = null,
|
|
314
|
+
): APIPromise<Rsp> {
|
|
315
|
+
return new APIPromise(this.makeRequest(options, remainingRetries));
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
private async makeRequest<T>(
|
|
319
|
+
optionsInput: PromiseOrValue<FinalRequestOptions>,
|
|
320
|
+
retriesRemaining: number | null,
|
|
321
|
+
): Promise<{ response: Response; options: FinalRequestOptions; controller: AbortController }> {
|
|
322
|
+
const options = await optionsInput;
|
|
323
|
+
if (retriesRemaining == null) {
|
|
324
|
+
retriesRemaining = options.maxRetries ?? this.maxRetries;
|
|
325
|
+
}
|
|
326
|
+
|
|
216
327
|
const { req, url, timeout } = this.buildRequest(options);
|
|
328
|
+
|
|
217
329
|
await this.prepareRequest(req, { url });
|
|
218
330
|
|
|
219
|
-
|
|
331
|
+
debug('request', url, options, req.headers);
|
|
332
|
+
|
|
333
|
+
if (options.signal?.aborted) {
|
|
334
|
+
throw new APIUserAbortError();
|
|
335
|
+
}
|
|
220
336
|
|
|
221
337
|
const controller = new AbortController();
|
|
222
338
|
const response = await this.fetchWithTimeout(url, req, timeout, controller).catch(castToError);
|
|
@@ -245,42 +361,21 @@ export abstract class APIClient {
|
|
|
245
361
|
const errJSON = safeJSON(errText);
|
|
246
362
|
const errMessage = errJSON ? undefined : errText;
|
|
247
363
|
|
|
248
|
-
|
|
364
|
+
debug('response', response.status, url, responseHeaders, errMessage);
|
|
249
365
|
|
|
250
366
|
const err = this.makeStatusError(response.status, errJSON, errMessage, responseHeaders);
|
|
251
367
|
throw err;
|
|
252
368
|
}
|
|
253
369
|
|
|
254
|
-
|
|
255
|
-
if (contentType?.includes('application/json')) {
|
|
256
|
-
const json = await response.json();
|
|
257
|
-
|
|
258
|
-
if (typeof json === 'object' && json != null) {
|
|
259
|
-
/** @deprecated – we expect to change this interface in the near future. */
|
|
260
|
-
Object.defineProperty(json, 'responseHeaders', {
|
|
261
|
-
enumerable: false,
|
|
262
|
-
writable: false,
|
|
263
|
-
value: responseHeaders,
|
|
264
|
-
});
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
this.debug('response', response.status, url, responseHeaders, json);
|
|
268
|
-
|
|
269
|
-
return json as APIResponse<Rsp>;
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
// TODO handle blob, arraybuffer, other content types, etc.
|
|
273
|
-
const text = response.text();
|
|
274
|
-
this.debug('response', response.status, url, responseHeaders, text);
|
|
275
|
-
return text as Promise<any>;
|
|
370
|
+
return { response, options, controller };
|
|
276
371
|
}
|
|
277
372
|
|
|
278
373
|
requestAPIList<Item = unknown, PageClass extends AbstractPage<Item> = AbstractPage<Item>>(
|
|
279
374
|
Page: new (...args: ConstructorParameters<typeof AbstractPage>) => PageClass,
|
|
280
375
|
options: FinalRequestOptions,
|
|
281
|
-
): PagePromise<PageClass> {
|
|
282
|
-
const
|
|
283
|
-
return new PagePromise(this,
|
|
376
|
+
): PagePromise<PageClass, Item> {
|
|
377
|
+
const request = this.makeRequest(options, null);
|
|
378
|
+
return new PagePromise<PageClass, Item>(this, request, Page);
|
|
284
379
|
}
|
|
285
380
|
|
|
286
381
|
buildURL<Req>(path: string, query: Req | undefined): string {
|
|
@@ -295,12 +390,29 @@ export abstract class APIClient {
|
|
|
295
390
|
}
|
|
296
391
|
|
|
297
392
|
if (query) {
|
|
298
|
-
url.search =
|
|
393
|
+
url.search = this.stringifyQuery(query);
|
|
299
394
|
}
|
|
300
395
|
|
|
301
396
|
return url.toString();
|
|
302
397
|
}
|
|
303
398
|
|
|
399
|
+
protected stringifyQuery(query: Record<string, unknown>): string {
|
|
400
|
+
return Object.entries(query)
|
|
401
|
+
.filter(([_, value]) => typeof value !== 'undefined')
|
|
402
|
+
.map(([key, value]) => {
|
|
403
|
+
if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') {
|
|
404
|
+
return `${encodeURIComponent(key)}=${encodeURIComponent(value)}`;
|
|
405
|
+
}
|
|
406
|
+
if (value === null) {
|
|
407
|
+
return `${encodeURIComponent(key)}=`;
|
|
408
|
+
}
|
|
409
|
+
throw new Error(
|
|
410
|
+
`Cannot stringify type ${typeof value}; Expected string, number, boolean, or null. If you need to pass nested query parameters, you can manually encode them, e.g. { query: { 'foo[key1]': value1, 'foo[key2]': value2 } }, and please open a GitHub issue requesting better support for your use case.`,
|
|
411
|
+
);
|
|
412
|
+
})
|
|
413
|
+
.join('&');
|
|
414
|
+
}
|
|
415
|
+
|
|
304
416
|
async fetchWithTimeout(
|
|
305
417
|
url: RequestInfo,
|
|
306
418
|
init: RequestInit | undefined,
|
|
@@ -391,12 +503,6 @@ export abstract class APIClient {
|
|
|
391
503
|
private getUserAgent(): string {
|
|
392
504
|
return `${this.constructor.name}/JS ${VERSION}`;
|
|
393
505
|
}
|
|
394
|
-
|
|
395
|
-
private debug(action: string, ...args: any[]) {
|
|
396
|
-
if (typeof process !== 'undefined' && process.env['DEBUG'] === 'true') {
|
|
397
|
-
console.log(`${this.constructor.name}:DEBUG:${action}`, ...args);
|
|
398
|
-
}
|
|
399
|
-
}
|
|
400
506
|
}
|
|
401
507
|
|
|
402
508
|
export class APIResource {
|
|
@@ -426,9 +532,14 @@ export abstract class AbstractPage<Item> implements AsyncIterable<Item> {
|
|
|
426
532
|
#client: APIClient;
|
|
427
533
|
protected options: FinalRequestOptions;
|
|
428
534
|
|
|
429
|
-
|
|
535
|
+
protected response: Response;
|
|
536
|
+
protected body: unknown;
|
|
537
|
+
|
|
538
|
+
constructor(client: APIClient, response: Response, body: unknown, options: FinalRequestOptions) {
|
|
430
539
|
this.#client = client;
|
|
431
540
|
this.options = options;
|
|
541
|
+
this.response = response;
|
|
542
|
+
this.body = body;
|
|
432
543
|
}
|
|
433
544
|
|
|
434
545
|
/**
|
|
@@ -485,35 +596,33 @@ export abstract class AbstractPage<Item> implements AsyncIterable<Item> {
|
|
|
485
596
|
}
|
|
486
597
|
}
|
|
487
598
|
|
|
599
|
+
/**
|
|
600
|
+
* This subclass of Promise will resolve to an instantiated Page once the request completes.
|
|
601
|
+
*
|
|
602
|
+
* It also implements AsyncIterable to allow auto-paginating iteration on an unawaited list call, eg:
|
|
603
|
+
*
|
|
604
|
+
* for await (const item of client.items.list()) {
|
|
605
|
+
* console.log(item)
|
|
606
|
+
* }
|
|
607
|
+
*/
|
|
488
608
|
export class PagePromise<
|
|
489
609
|
PageClass extends AbstractPage<Item>,
|
|
490
610
|
Item = ReturnType<PageClass['getPaginatedItems']>[number],
|
|
491
611
|
>
|
|
492
|
-
extends
|
|
612
|
+
extends APIPromise<PageClass>
|
|
493
613
|
implements AsyncIterable<Item>
|
|
494
614
|
{
|
|
495
|
-
/**
|
|
496
|
-
* This subclass of Promise will resolve to an instantiated Page once the request completes.
|
|
497
|
-
*/
|
|
498
615
|
constructor(
|
|
499
616
|
client: APIClient,
|
|
500
|
-
|
|
501
|
-
options: FinalRequestOptions,
|
|
617
|
+
request: Promise<APIResponseProps>,
|
|
502
618
|
Page: new (...args: ConstructorParameters<typeof AbstractPage>) => PageClass,
|
|
503
619
|
) {
|
|
504
|
-
super(
|
|
505
|
-
|
|
620
|
+
super(
|
|
621
|
+
request,
|
|
622
|
+
async (props) => new Page(client, props.response, await defaultParseResponse(props), props.options),
|
|
506
623
|
);
|
|
507
624
|
}
|
|
508
625
|
|
|
509
|
-
/**
|
|
510
|
-
* Enable subclassing Promise.
|
|
511
|
-
* Ref: https://stackoverflow.com/a/60328122
|
|
512
|
-
*/
|
|
513
|
-
static get [Symbol.species]() {
|
|
514
|
-
return Promise;
|
|
515
|
-
}
|
|
516
|
-
|
|
517
626
|
/**
|
|
518
627
|
* Allow auto-paginating iteration on an unawaited list call, eg:
|
|
519
628
|
*
|
|
@@ -600,11 +709,6 @@ export type FinalRequestOptions<Req extends {} = Record<string, unknown> | Reada
|
|
|
600
709
|
path: string;
|
|
601
710
|
};
|
|
602
711
|
|
|
603
|
-
export type APIResponse<T> = T & {
|
|
604
|
-
/** @deprecated - we plan to add a different way to access raw response information shortly. */
|
|
605
|
-
responseHeaders: Headers;
|
|
606
|
-
};
|
|
607
|
-
|
|
608
712
|
declare const Deno: any;
|
|
609
713
|
declare const EdgeRuntime: any;
|
|
610
714
|
type Arch = 'x32' | 'x64' | 'arm' | 'arm64' | `other:${string}` | 'unknown';
|
|
@@ -618,12 +722,13 @@ type PlatformName =
|
|
|
618
722
|
| 'Android'
|
|
619
723
|
| `Other:${string}`
|
|
620
724
|
| 'Unknown';
|
|
725
|
+
type Browser = 'ie' | 'edge' | 'chrome' | 'firefox' | 'safari';
|
|
621
726
|
type PlatformProperties = {
|
|
622
727
|
'X-Stainless-Lang': 'js';
|
|
623
728
|
'X-Stainless-Package-Version': string;
|
|
624
729
|
'X-Stainless-OS': PlatformName;
|
|
625
730
|
'X-Stainless-Arch': Arch;
|
|
626
|
-
'X-Stainless-Runtime': 'node' | 'deno' | 'edge' | 'unknown';
|
|
731
|
+
'X-Stainless-Runtime': 'node' | 'deno' | 'edge' | `browser:${Browser}` | 'unknown';
|
|
627
732
|
'X-Stainless-Runtime-Version': string;
|
|
628
733
|
};
|
|
629
734
|
const getPlatformProperties = (): PlatformProperties => {
|
|
@@ -658,7 +763,20 @@ const getPlatformProperties = (): PlatformProperties => {
|
|
|
658
763
|
'X-Stainless-Runtime-Version': process.version,
|
|
659
764
|
};
|
|
660
765
|
}
|
|
661
|
-
|
|
766
|
+
|
|
767
|
+
const browserInfo = getBrowserInfo();
|
|
768
|
+
if (browserInfo) {
|
|
769
|
+
return {
|
|
770
|
+
'X-Stainless-Lang': 'js',
|
|
771
|
+
'X-Stainless-Package-Version': VERSION,
|
|
772
|
+
'X-Stainless-OS': 'Unknown',
|
|
773
|
+
'X-Stainless-Arch': 'unknown',
|
|
774
|
+
'X-Stainless-Runtime': `browser:${browserInfo.browser}`,
|
|
775
|
+
'X-Stainless-Runtime-Version': browserInfo.version,
|
|
776
|
+
};
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
// TODO add support for Cloudflare workers, etc.
|
|
662
780
|
return {
|
|
663
781
|
'X-Stainless-Lang': 'js',
|
|
664
782
|
'X-Stainless-Package-Version': VERSION,
|
|
@@ -669,6 +787,44 @@ const getPlatformProperties = (): PlatformProperties => {
|
|
|
669
787
|
};
|
|
670
788
|
};
|
|
671
789
|
|
|
790
|
+
type BrowserInfo = {
|
|
791
|
+
browser: Browser;
|
|
792
|
+
version: string;
|
|
793
|
+
};
|
|
794
|
+
|
|
795
|
+
declare const navigator: { userAgent: string } | undefined;
|
|
796
|
+
|
|
797
|
+
// Note: modified from https://github.com/JS-DevTools/host-environment/blob/b1ab79ecde37db5d6e163c050e54fe7d287d7c92/src/isomorphic.browser.ts
|
|
798
|
+
function getBrowserInfo(): BrowserInfo | null {
|
|
799
|
+
if (!navigator || typeof navigator === 'undefined') {
|
|
800
|
+
return null;
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
// NOTE: The order matters here!
|
|
804
|
+
const browserPatterns = [
|
|
805
|
+
{ key: 'edge' as const, pattern: /Edge(?:\W+(\d+)\.(\d+)(?:\.(\d+))?)?/ },
|
|
806
|
+
{ key: 'ie' as const, pattern: /MSIE(?:\W+(\d+)\.(\d+)(?:\.(\d+))?)?/ },
|
|
807
|
+
{ key: 'ie' as const, pattern: /Trident(?:.*rv\:(\d+)\.(\d+)(?:\.(\d+))?)?/ },
|
|
808
|
+
{ key: 'chrome' as const, pattern: /Chrome(?:\W+(\d+)\.(\d+)(?:\.(\d+))?)?/ },
|
|
809
|
+
{ key: 'firefox' as const, pattern: /Firefox(?:\W+(\d+)\.(\d+)(?:\.(\d+))?)?/ },
|
|
810
|
+
{ key: 'safari' as const, pattern: /(?:Version\W+(\d+)\.(\d+)(?:\.(\d+))?)?(?:\W+Mobile\S*)?\W+Safari/ },
|
|
811
|
+
];
|
|
812
|
+
|
|
813
|
+
// Find the FIRST matching browser
|
|
814
|
+
for (const { key, pattern } of browserPatterns) {
|
|
815
|
+
const match = pattern.exec(navigator.userAgent);
|
|
816
|
+
if (match) {
|
|
817
|
+
const major = match[1] || 0;
|
|
818
|
+
const minor = match[2] || 0;
|
|
819
|
+
const patch = match[3] || 0;
|
|
820
|
+
|
|
821
|
+
return { browser: key, version: `${major}.${minor}.${patch}` };
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
return null;
|
|
826
|
+
}
|
|
827
|
+
|
|
672
828
|
const normalizeArch = (arch: string): Arch => {
|
|
673
829
|
// Node docs:
|
|
674
830
|
// - https://nodejs.org/api/process.html#processarch
|
|
@@ -727,8 +883,8 @@ const isAbsoluteURL = (url: string): boolean => {
|
|
|
727
883
|
|
|
728
884
|
const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
729
885
|
|
|
730
|
-
const validatePositiveInteger = (name: string, n:
|
|
731
|
-
if (!Number.isInteger(n)) {
|
|
886
|
+
const validatePositiveInteger = (name: string, n: unknown): number => {
|
|
887
|
+
if (typeof n !== 'number' || !Number.isInteger(n)) {
|
|
732
888
|
throw new Error(`${name} must be an integer`);
|
|
733
889
|
}
|
|
734
890
|
if (n < 0) {
|
|
@@ -747,6 +903,21 @@ export const ensurePresent = <T>(value: T | null | undefined): T => {
|
|
|
747
903
|
return value;
|
|
748
904
|
};
|
|
749
905
|
|
|
906
|
+
/**
|
|
907
|
+
* Read an environment variable.
|
|
908
|
+
*
|
|
909
|
+
* Will return undefined if the environment variable doesn't exist or cannot be accessed.
|
|
910
|
+
*/
|
|
911
|
+
export const readEnv = (env: string): string | undefined => {
|
|
912
|
+
if (typeof process !== 'undefined') {
|
|
913
|
+
return process.env?.[env] ?? undefined;
|
|
914
|
+
}
|
|
915
|
+
if (typeof Deno !== 'undefined') {
|
|
916
|
+
return Deno.env?.get?.(env);
|
|
917
|
+
}
|
|
918
|
+
return undefined;
|
|
919
|
+
};
|
|
920
|
+
|
|
750
921
|
export const coerceInteger = (value: unknown): number => {
|
|
751
922
|
if (typeof value === 'number') return Math.round(value);
|
|
752
923
|
if (typeof value === 'string') return parseInt(value, 10);
|
|
@@ -767,6 +938,27 @@ export const coerceBoolean = (value: unknown): boolean => {
|
|
|
767
938
|
return Boolean(value);
|
|
768
939
|
};
|
|
769
940
|
|
|
941
|
+
export const maybeCoerceInteger = (value: unknown): number | undefined => {
|
|
942
|
+
if (value === undefined) {
|
|
943
|
+
return undefined;
|
|
944
|
+
}
|
|
945
|
+
return coerceInteger(value);
|
|
946
|
+
};
|
|
947
|
+
|
|
948
|
+
export const maybeCoerceFloat = (value: unknown): number | undefined => {
|
|
949
|
+
if (value === undefined) {
|
|
950
|
+
return undefined;
|
|
951
|
+
}
|
|
952
|
+
return coerceFloat(value);
|
|
953
|
+
};
|
|
954
|
+
|
|
955
|
+
export const maybeCoerceBoolean = (value: unknown): boolean | undefined => {
|
|
956
|
+
if (value === undefined) {
|
|
957
|
+
return undefined;
|
|
958
|
+
}
|
|
959
|
+
return coerceBoolean(value);
|
|
960
|
+
};
|
|
961
|
+
|
|
770
962
|
// https://stackoverflow.com/a/34491287
|
|
771
963
|
export function isEmptyObj(obj: Object | null | undefined): boolean {
|
|
772
964
|
if (!obj) return true;
|
|
@@ -779,6 +971,12 @@ export function hasOwn(obj: Object, key: string): boolean {
|
|
|
779
971
|
return Object.prototype.hasOwnProperty.call(obj, key);
|
|
780
972
|
}
|
|
781
973
|
|
|
974
|
+
export function debug(action: string, ...args: any[]) {
|
|
975
|
+
if (typeof process !== 'undefined' && process.env['DEBUG'] === 'true') {
|
|
976
|
+
console.log(`Finch:DEBUG:${action}`, ...args);
|
|
977
|
+
}
|
|
978
|
+
}
|
|
979
|
+
|
|
782
980
|
/**
|
|
783
981
|
* https://stackoverflow.com/a/2117523
|
|
784
982
|
*/
|
|
@@ -790,6 +988,17 @@ const uuid4 = () => {
|
|
|
790
988
|
});
|
|
791
989
|
};
|
|
792
990
|
|
|
991
|
+
export const isRunningInBrowser = () => {
|
|
992
|
+
return (
|
|
993
|
+
// @ts-ignore
|
|
994
|
+
typeof window !== 'undefined' &&
|
|
995
|
+
// @ts-ignore
|
|
996
|
+
typeof window.document !== 'undefined' &&
|
|
997
|
+
// @ts-ignore
|
|
998
|
+
typeof navigator !== 'undefined'
|
|
999
|
+
);
|
|
1000
|
+
};
|
|
1001
|
+
|
|
793
1002
|
export interface HeadersProtocol {
|
|
794
1003
|
get: (header: string) => string | null | undefined;
|
|
795
1004
|
}
|
package/src/error.ts
CHANGED
|
@@ -13,12 +13,22 @@ export class APIError extends Error {
|
|
|
13
13
|
message: string | undefined,
|
|
14
14
|
headers: Headers | undefined,
|
|
15
15
|
) {
|
|
16
|
-
super(
|
|
16
|
+
super(APIError.makeMessage(error, message));
|
|
17
17
|
this.status = status;
|
|
18
18
|
this.headers = headers;
|
|
19
19
|
this.error = error;
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
+
private static makeMessage(error: any, message: string | undefined) {
|
|
23
|
+
return (
|
|
24
|
+
error?.message ?
|
|
25
|
+
typeof error.message === 'string' ? error.message
|
|
26
|
+
: JSON.stringify(error.message)
|
|
27
|
+
: error ? JSON.stringify(error)
|
|
28
|
+
: message || 'Unknown error occurred'
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
|
|
22
32
|
static generate(
|
|
23
33
|
status: number | undefined,
|
|
24
34
|
errorResponse: Object | undefined,
|