@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.
Files changed (135) hide show
  1. package/CHANGELOG.md +63 -0
  2. package/README.md +39 -15
  3. package/_shims/ReadableStream.d.ts +38 -0
  4. package/_shims/ReadableStream.js +5 -0
  5. package/_shims/ReadableStream.mjs +7 -0
  6. package/_shims/ReadableStream.node.d.ts +6 -0
  7. package/_shims/ReadableStream.node.d.ts.map +1 -0
  8. package/_shims/ReadableStream.node.js +14 -0
  9. package/_shims/ReadableStream.node.js.map +1 -0
  10. package/_shims/ReadableStream.node.mjs +3 -0
  11. package/_shims/ReadableStream.node.mjs.map +1 -0
  12. package/_shims/fetch.d.ts +8 -1
  13. package/_shims/fetch.js +1 -1
  14. package/_shims/fetch.node.d.ts +11 -1
  15. package/core.d.ts +87 -38
  16. package/core.d.ts.map +1 -1
  17. package/core.js +245 -114
  18. package/core.js.map +1 -1
  19. package/core.mjs +231 -74
  20. package/core.mjs.map +1 -1
  21. package/error.d.ts +1 -0
  22. package/error.d.ts.map +1 -1
  23. package/error.js +12 -3
  24. package/error.js.map +1 -1
  25. package/error.mjs +12 -3
  26. package/error.mjs.map +1 -1
  27. package/index.d.mts +3 -5
  28. package/index.d.ts +3 -5
  29. package/index.d.ts.map +1 -1
  30. package/index.js +24 -27
  31. package/index.js.map +1 -1
  32. package/index.mjs +24 -27
  33. package/index.mjs.map +1 -1
  34. package/package.json +5 -7
  35. package/pagination.d.ts +17 -8
  36. package/pagination.d.ts.map +1 -1
  37. package/pagination.js +26 -26
  38. package/pagination.js.map +1 -1
  39. package/pagination.mjs +26 -26
  40. package/pagination.mjs.map +1 -1
  41. package/resources/account.d.ts +2 -2
  42. package/resources/account.d.ts.map +1 -1
  43. package/resources/ats/applications.d.ts +6 -3
  44. package/resources/ats/applications.d.ts.map +1 -1
  45. package/resources/ats/applications.js.map +1 -1
  46. package/resources/ats/applications.mjs.map +1 -1
  47. package/resources/ats/ats.d.ts.map +1 -1
  48. package/resources/ats/ats.js.map +1 -1
  49. package/resources/ats/candidates.d.ts +6 -3
  50. package/resources/ats/candidates.d.ts.map +1 -1
  51. package/resources/ats/candidates.js.map +1 -1
  52. package/resources/ats/candidates.mjs.map +1 -1
  53. package/resources/ats/jobs.d.ts +3 -3
  54. package/resources/ats/jobs.d.ts.map +1 -1
  55. package/resources/ats/offers.d.ts +3 -3
  56. package/resources/ats/offers.d.ts.map +1 -1
  57. package/resources/ats/stages.d.ts +1 -1
  58. package/resources/ats/stages.d.ts.map +1 -1
  59. package/resources/hris/benefits/benefits.d.ts +9 -10
  60. package/resources/hris/benefits/benefits.d.ts.map +1 -1
  61. package/resources/hris/benefits/benefits.js.map +1 -1
  62. package/resources/hris/benefits/benefits.mjs.map +1 -1
  63. package/resources/hris/benefits/individuals.d.ts +6 -6
  64. package/resources/hris/benefits/individuals.d.ts.map +1 -1
  65. package/resources/hris/company.d.ts +1 -1
  66. package/resources/hris/company.d.ts.map +1 -1
  67. package/resources/hris/directory.d.ts +2 -2
  68. package/resources/hris/directory.d.ts.map +1 -1
  69. package/resources/hris/hris.d.ts.map +1 -1
  70. package/resources/hris/hris.js.map +1 -1
  71. package/resources/hris/index.d.ts.map +1 -1
  72. package/resources/hris/index.js +7 -7
  73. package/resources/hris/index.js.map +1 -1
  74. package/resources/hris/individuals/employment-data.d.ts +1 -1
  75. package/resources/hris/individuals/employment-data.d.ts.map +1 -1
  76. package/resources/hris/individuals/employment-data.js.map +1 -1
  77. package/resources/hris/individuals/individuals.d.ts +4 -2
  78. package/resources/hris/individuals/individuals.d.ts.map +1 -1
  79. package/resources/hris/individuals/individuals.js.map +1 -1
  80. package/resources/hris/individuals/individuals.mjs.map +1 -1
  81. package/resources/hris/pay-statements.d.ts +1 -1
  82. package/resources/hris/pay-statements.d.ts.map +1 -1
  83. package/resources/hris/payments.d.ts +4 -1
  84. package/resources/hris/payments.d.ts.map +1 -1
  85. package/resources/hris/payments.js.map +1 -1
  86. package/resources/hris/payments.mjs.map +1 -1
  87. package/resources/index.d.ts +1 -0
  88. package/resources/index.d.ts.map +1 -1
  89. package/resources/index.js.map +1 -1
  90. package/resources/index.mjs.map +1 -1
  91. package/resources/providers.d.ts +1 -1
  92. package/resources/providers.d.ts.map +1 -1
  93. package/src/_shims/ReadableStream.d.ts +38 -0
  94. package/src/_shims/ReadableStream.js +5 -0
  95. package/src/_shims/ReadableStream.mjs +7 -0
  96. package/src/_shims/ReadableStream.node.ts +6 -0
  97. package/src/_shims/fetch.d.ts +8 -1
  98. package/src/_shims/fetch.deno.ts +23 -0
  99. package/src/_shims/fetch.js +1 -1
  100. package/src/_shims/fetch.node.d.ts +11 -1
  101. package/src/_shims/formdata.deno.ts +16 -0
  102. package/src/core.ts +302 -93
  103. package/src/error.ts +11 -1
  104. package/src/index.ts +28 -27
  105. package/src/pagination.ts +34 -29
  106. package/src/resources/account.ts +3 -3
  107. package/src/resources/ats/applications.ts +8 -5
  108. package/src/resources/ats/ats.ts +1 -1
  109. package/src/resources/ats/candidates.ts +8 -5
  110. package/src/resources/ats/jobs.ts +5 -5
  111. package/src/resources/ats/offers.ts +5 -5
  112. package/src/resources/ats/stages.ts +2 -2
  113. package/src/resources/hris/benefits/benefits.ts +12 -13
  114. package/src/resources/hris/benefits/individuals.ts +9 -9
  115. package/src/resources/hris/company.ts +2 -2
  116. package/src/resources/hris/directory.ts +4 -4
  117. package/src/resources/hris/hris.ts +1 -1
  118. package/src/resources/hris/index.ts +2 -2
  119. package/src/resources/hris/individuals/employment-data.ts +2 -2
  120. package/src/resources/hris/individuals/individuals.ts +6 -4
  121. package/src/resources/hris/pay-statements.ts +2 -2
  122. package/src/resources/hris/payments.ts +5 -2
  123. package/src/resources/index.ts +1 -0
  124. package/src/resources/providers.ts +2 -2
  125. package/src/uploads.ts +7 -6
  126. package/src/version.ts +1 -1
  127. package/uploads.d.ts +5 -4
  128. package/uploads.d.ts.map +1 -1
  129. package/uploads.js +2 -2
  130. package/uploads.js.map +1 -1
  131. package/uploads.mjs +2 -2
  132. package/uploads.mjs.map +1 -1
  133. package/version.d.ts +1 -1
  134. package/version.js +1 -1
  135. 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 = 60 * 1000, // 60s
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>): Promise<Rsp> {
101
- return this.request({ method: 'get', path, ...opts });
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
- post<Req extends {}, Rsp>(path: string, opts?: RequestOptions<Req>): Promise<Rsp> {
104
- return this.request({ method: 'post', path, ...opts });
189
+
190
+ patch<Req extends {}, Rsp>(path: string, opts?: PromiseOrValue<RequestOptions<Req>>): APIPromise<Rsp> {
191
+ return this.methodRequest('patch', path, opts);
105
192
  }
106
- patch<Req extends {}, Rsp>(path: string, opts?: RequestOptions<Req>): Promise<Rsp> {
107
- return this.request({ method: 'patch', path, ...opts });
193
+
194
+ put<Req extends {}, Rsp>(path: string, opts?: PromiseOrValue<RequestOptions<Req>>): APIPromise<Rsp> {
195
+ return this.methodRequest('put', path, opts);
108
196
  }
109
- put<Req extends {}, Rsp>(path: string, opts?: RequestOptions<Req>): Promise<Rsp> {
110
- return this.request({ method: 'put', path, ...opts });
197
+
198
+ delete<Req extends {}, Rsp>(path: string, opts?: PromiseOrValue<RequestOptions<Req>>): APIPromise<Rsp> {
199
+ return this.methodRequest('delete', path, opts);
111
200
  }
112
- delete<Req extends {}, Rsp>(path: string, opts?: RequestOptions<Req>): Promise<Rsp> {
113
- return this.request({ method: 'delete', path, ...opts });
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
- const encoder = new TextEncoder();
131
- const encoded = encoder.encode(body);
132
- return encoded.length.toString();
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 ((httpAgent as any)?.options && minAgentTimeout > ((httpAgent as any).options.timeout ?? 0)) {
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
- async request<Req extends {}, Rsp>(
213
- options: FinalRequestOptions<Req>,
214
- retriesRemaining = options.maxRetries ?? this.maxRetries,
215
- ): Promise<APIResponse<Rsp>> {
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
- this.debug('request', url, options, req.headers);
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
- this.debug('response', response.status, url, responseHeaders, errMessage);
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
- const contentType = response.headers.get('content-type');
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 requestPromise = this.request(options) as Promise<APIResponse<unknown>>;
283
- return new PagePromise(this, requestPromise, options, Page);
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 = qs.stringify(query, this.qsOptions());
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
- constructor(client: APIClient, response: APIResponse<unknown>, options: FinalRequestOptions) {
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 Promise<PageClass>
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
- requestPromise: Promise<APIResponse<unknown>>,
501
- options: FinalRequestOptions,
617
+ request: Promise<APIResponseProps>,
502
618
  Page: new (...args: ConstructorParameters<typeof AbstractPage>) => PageClass,
503
619
  ) {
504
- super((resolve, reject) =>
505
- requestPromise.then((response) => resolve(new Page(client, response, options))).catch(reject),
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
- // TODO add support for Cloudflare workers, browsers, etc.
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: number) => {
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(message || (error as any)?.message || 'Unknown error occurred.');
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,