@datocms/rest-client-utils 0.1.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 (80) hide show
  1. package/LICENSE.md +21 -0
  2. package/README.md +1 -0
  3. package/dist/cjs/ApiError.js +138 -0
  4. package/dist/cjs/ApiError.js.map +1 -0
  5. package/dist/cjs/CancelablePromise.js +53 -0
  6. package/dist/cjs/CancelablePromise.js.map +1 -0
  7. package/dist/cjs/deserialize.js +57 -0
  8. package/dist/cjs/deserialize.js.map +1 -0
  9. package/dist/cjs/index.js +26 -0
  10. package/dist/cjs/index.js.map +1 -0
  11. package/dist/cjs/internalTypes.js +3 -0
  12. package/dist/cjs/internalTypes.js.map +1 -0
  13. package/dist/cjs/pollJobResult.js +75 -0
  14. package/dist/cjs/pollJobResult.js.map +1 -0
  15. package/dist/cjs/rawPageIterator.js +152 -0
  16. package/dist/cjs/rawPageIterator.js.map +1 -0
  17. package/dist/cjs/request.js +283 -0
  18. package/dist/cjs/request.js.map +1 -0
  19. package/dist/cjs/serialize.js +142 -0
  20. package/dist/cjs/serialize.js.map +1 -0
  21. package/dist/cjs/toId.js +8 -0
  22. package/dist/cjs/toId.js.map +1 -0
  23. package/dist/cjs/wait.js +10 -0
  24. package/dist/cjs/wait.js.map +1 -0
  25. package/dist/esm/ApiError.d.ts +34 -0
  26. package/dist/esm/ApiError.js +135 -0
  27. package/dist/esm/ApiError.js.map +1 -0
  28. package/dist/esm/CancelablePromise.d.ts +7 -0
  29. package/dist/esm/CancelablePromise.js +49 -0
  30. package/dist/esm/CancelablePromise.js.map +1 -0
  31. package/dist/esm/deserialize.d.ts +10 -0
  32. package/dist/esm/deserialize.js +52 -0
  33. package/dist/esm/deserialize.js.map +1 -0
  34. package/dist/esm/index.d.ts +9 -0
  35. package/dist/esm/index.js +11 -0
  36. package/dist/esm/index.js.map +1 -0
  37. package/dist/esm/internalTypes.d.ts +8 -0
  38. package/dist/esm/internalTypes.js +2 -0
  39. package/dist/esm/internalTypes.js.map +1 -0
  40. package/dist/esm/pollJobResult.d.ts +2 -0
  41. package/dist/esm/pollJobResult.js +71 -0
  42. package/dist/esm/pollJobResult.js.map +1 -0
  43. package/dist/esm/rawPageIterator.d.ts +19 -0
  44. package/dist/esm/rawPageIterator.js +148 -0
  45. package/dist/esm/rawPageIterator.js.map +1 -0
  46. package/dist/esm/request.d.ts +28 -0
  47. package/dist/esm/request.js +276 -0
  48. package/dist/esm/request.js.map +1 -0
  49. package/dist/esm/serialize.d.ts +22 -0
  50. package/dist/esm/serialize.js +138 -0
  51. package/dist/esm/serialize.js.map +1 -0
  52. package/dist/esm/toId.d.ts +3 -0
  53. package/dist/esm/toId.js +4 -0
  54. package/dist/esm/toId.js.map +1 -0
  55. package/dist/esm/wait.d.ts +1 -0
  56. package/dist/esm/wait.js +6 -0
  57. package/dist/esm/wait.js.map +1 -0
  58. package/dist/types/ApiError.d.ts +34 -0
  59. package/dist/types/CancelablePromise.d.ts +7 -0
  60. package/dist/types/deserialize.d.ts +10 -0
  61. package/dist/types/index.d.ts +9 -0
  62. package/dist/types/internalTypes.d.ts +8 -0
  63. package/dist/types/pollJobResult.d.ts +2 -0
  64. package/dist/types/rawPageIterator.d.ts +19 -0
  65. package/dist/types/request.d.ts +28 -0
  66. package/dist/types/serialize.d.ts +22 -0
  67. package/dist/types/toId.d.ts +3 -0
  68. package/dist/types/wait.d.ts +1 -0
  69. package/package.json +44 -0
  70. package/src/ApiError.ts +167 -0
  71. package/src/CancelablePromise.ts +41 -0
  72. package/src/deserialize.ts +52 -0
  73. package/src/index.ts +11 -0
  74. package/src/internalTypes.ts +8 -0
  75. package/src/pollJobResult.ts +24 -0
  76. package/src/rawPageIterator.ts +66 -0
  77. package/src/request.ts +279 -0
  78. package/src/serialize.ts +115 -0
  79. package/src/toId.ts +3 -0
  80. package/src/wait.ts +5 -0
package/package.json ADDED
@@ -0,0 +1,44 @@
1
+ {
2
+ "name": "@datocms/rest-client-utils",
3
+ "version": "0.1.0",
4
+ "description": "Utilities for DatoCMS REST API clients",
5
+ "keywords": [
6
+ "datocms",
7
+ "client"
8
+ ],
9
+ "author": "Stefano Verna <s.verna@datocms.com>",
10
+ "homepage": "https://github.com/datocms/js-toolkit/tree/main/packages/rest-client-utils#readme",
11
+ "license": "MIT",
12
+ "main": "dist/cjs/index.js",
13
+ "module": "dist/esm/index.js",
14
+ "typings": "dist/types/index.d.ts",
15
+ "sideEffects": false,
16
+ "directories": {
17
+ "lib": "dist",
18
+ "test": "__tests__"
19
+ },
20
+ "files": [
21
+ "dist",
22
+ "src"
23
+ ],
24
+ "publishConfig": {
25
+ "access": "public"
26
+ },
27
+ "repository": {
28
+ "type": "git",
29
+ "url": "git+https://github.com/datocms/js-toolkit.git"
30
+ },
31
+ "scripts": {
32
+ "build": "tsc && tsc --project ./tsconfig.esnext.json",
33
+ "prebuild": "rimraf dist"
34
+ },
35
+ "bugs": {
36
+ "url": "https://github.com/datocms/js-toolkit/issues"
37
+ },
38
+ "dependencies": {
39
+ "async-scheduler": "^1.4.4",
40
+ "cross-fetch": "^3.1.5",
41
+ "qs": "^6.10.3"
42
+ },
43
+ "gitHead": "41036fc6a62b20638bf91f8c9f58ff8ebabb69bf"
44
+ }
@@ -0,0 +1,167 @@
1
+ export type ErrorEntity = {
2
+ id: string;
3
+ type: 'api_error';
4
+ attributes: {
5
+ code: string;
6
+ details: unknown;
7
+ };
8
+ };
9
+ type ErrorBody = { data: ErrorEntity[] };
10
+
11
+ function isErrorBody(body: unknown): body is ErrorBody {
12
+ if (typeof body !== 'object' || body === null || !('data' in body)) {
13
+ return false;
14
+ }
15
+
16
+ const bodyWithData = body as { data: unknown };
17
+
18
+ if (!Array.isArray(bodyWithData.data)) {
19
+ return false;
20
+ }
21
+
22
+ const bodyWithDataList = bodyWithData as { data: unknown[] };
23
+
24
+ if (bodyWithDataList.data.length === 0) {
25
+ return false;
26
+ }
27
+
28
+ const firstEl = bodyWithDataList.data[0];
29
+
30
+ if (
31
+ typeof firstEl !== 'object' ||
32
+ firstEl === null ||
33
+ !('id' in firstEl) ||
34
+ !('type' in firstEl) ||
35
+ !('attributes' in firstEl) ||
36
+ (firstEl as ErrorEntity).type !== 'api_error'
37
+ ) {
38
+ return false;
39
+ }
40
+
41
+ return true;
42
+ }
43
+
44
+ const humanMessageForCode = {
45
+ BATCH_DATA_VALIDATION_IN_PROGRESS: `The schema of this model changed, we're re-running validations over every record in background. Please retry with this operation in a few seconds!`,
46
+ INSUFFICIENT_PERMISSIONS: `Your role does not permit this action`,
47
+ MAINTENANCE_MODE: `The project is currently in maintenance mode!`,
48
+ DELETE_RESTRICTION: `Sorry, but you cannot delete this resource, as it's currently used/referenced elsewhere!`,
49
+ INVALID_CREDENTIALS: `Credentials are incorrect!`,
50
+ INVALID_EMAIL: `Email address is incorrect!`,
51
+ INVALID_FORMAT: `The format of the parameters passed is incorrect, take a look at the details of the error to know what's wrong!`,
52
+ ITEM_LOCKED: `The operation cannot be completed as some other user is currently editing this record!`,
53
+ LINKED_FROM_PUBLISHED_ITEMS: `Couldn't unpublish the record, as some published records are linked to it!`,
54
+ PLAN_UPGRADE_REQUIRED: `Cannot proceed, please upgrade plan!`,
55
+ PUBLISHED_CHILDREN: `Couldn't unpublish the record, some children records are still published!`,
56
+ REQUIRED_2FA_SETUP: `This project requires every user to turn on 2-factor authentication! Please go to your Dashboard and activate it! (https://dashboard.datocms.com/account/setup-2fa)`,
57
+ REQUIRED_BY_ASSOCIATION: `Cannot delete the record, as it's required by other records:`,
58
+ STALE_ITEM_VERSION: `Someone else made a change while you were editing this record, please refresh the page!`,
59
+ TITLE_ALREADY_PRESENT: `There can only be one Title field per model`,
60
+ UNPUBLISHED_LINK: `Couldn't publish the record, as it links some unpublished records!`,
61
+ UNPUBLISHED_PARENT: `Couldn't publish the record, as the parent record is not published!`,
62
+ UPLOAD_IS_CURRENTLY_IN_USE: `Couldn't delete this asset, as it's currently used by some records!`,
63
+ UPLOAD_NOT_PASSING_FIELD_VALIDATIONS: `Couldn't update this asset since some records are failing to pass the validations!`,
64
+ };
65
+
66
+ const humanMessageForPlanUpgradeLimit = {
67
+ build_triggers: `You've reached the maximum number of build triggers your plan allows`,
68
+ sandbox_environments: `You've reached the maximum number of environments your plan allows`,
69
+ item_types: `You've reached the maximum number of models your plan allows to create`,
70
+ items: `You've reached the maximum number of records your plan allows to create`,
71
+ locales: `You've reached the maximum number of locales your plan allows`,
72
+ mux_encoding_seconds: `You've reached the maximum video encoding limits of your plan`,
73
+ otp: `Two-factor authentication cannot be on the current plan`,
74
+ plugins: `You've reached the maximum number of plugins your plan allows`,
75
+ roles: `You've reached the maximum number of roles your plan allows to create`,
76
+ uploadable_bytes: `You've reached the file storage limits of your plan`,
77
+ users: `You've reached the maximum number of collaborators your plan allows to invite to the project`,
78
+ access_tokens: `You've reached the maximum number of API tokens your plan allows to create`,
79
+ };
80
+
81
+ export type ApiErrorRequest = {
82
+ url: string;
83
+ method: string;
84
+ headers: Record<string, string>;
85
+ body?: unknown;
86
+ };
87
+
88
+ export type ApiErrorResponse = {
89
+ status: number;
90
+ statusText: string;
91
+ headers: Record<string, string>;
92
+ body?: unknown;
93
+ };
94
+
95
+ export type ApiErrorInitObject = {
96
+ request: ApiErrorRequest;
97
+ response: ApiErrorResponse;
98
+ preCallStack?: string;
99
+ };
100
+
101
+ export class ApiError extends Error {
102
+ request: ApiErrorRequest;
103
+ response: ApiErrorResponse;
104
+ preCallStack?: string;
105
+
106
+ constructor(initObject: ApiErrorInitObject) {
107
+ super('API Error!');
108
+ Object.setPrototypeOf(this, new.target.prototype);
109
+
110
+ if ('captureStackTrace' in Error) {
111
+ Error.captureStackTrace(this, ApiError);
112
+ } else {
113
+ this.stack = new Error().stack;
114
+ }
115
+
116
+ this.request = initObject.request;
117
+ this.response = initObject.response;
118
+ this.preCallStack = initObject.preCallStack;
119
+
120
+ let message = `${initObject.request.method} ${initObject.request.url}: ${this.response.status} ${this.response.statusText}`;
121
+
122
+ if (this.errors.length > 0) {
123
+ message += `\n\n${JSON.stringify(this.errors, null, 2)}`;
124
+ }
125
+
126
+ this.message = message;
127
+
128
+ if (this.preCallStack) {
129
+ this.stack += `\nCaused By:\n${this.preCallStack}`;
130
+ }
131
+ }
132
+
133
+ get errors() {
134
+ if (!isErrorBody(this.response.body)) {
135
+ return [];
136
+ }
137
+
138
+ return this.response.body.data;
139
+ }
140
+
141
+ findErrorWithCode(codeOrCodes) {
142
+ const codes = Array.isArray(codeOrCodes) ? codeOrCodes : [codeOrCodes];
143
+ return this.errors.find((error) => codes.includes(error.attributes.code));
144
+ }
145
+
146
+ get humanMessage() {
147
+ const planUpgradeError = this.findErrorWithCode('PLAN_UPGRADE_REQUIRED');
148
+
149
+ if (planUpgradeError) {
150
+ const { limit } = planUpgradeError.attributes.details as Record<
151
+ string,
152
+ string
153
+ >;
154
+ return `${humanMessageForPlanUpgradeLimit[limit]}. Please head over to your account dashboard (https://dashboard.datocms.com/) to upgrade the plan or, if no publicly available plan suits your needs, contact our Sales team (https://www.datocms.com/contact) to get a custom quote!`;
155
+ }
156
+
157
+ const errors = Object.keys(humanMessageForCode)
158
+ .filter((code) => this.findErrorWithCode(code))
159
+ .map((code) => humanMessageForCode[code]);
160
+
161
+ if (errors.length === 0) {
162
+ return null;
163
+ }
164
+
165
+ return errors.join('\n');
166
+ }
167
+ }
@@ -0,0 +1,41 @@
1
+ export class CanceledPromiseError extends Error {
2
+ constructor() {
3
+ super('Promise canceled!');
4
+ Object.setPrototypeOf(this, new.target.prototype);
5
+ }
6
+ }
7
+
8
+ export interface CancelablePromise<T> extends Promise<T> {
9
+ cancel(): void;
10
+ }
11
+
12
+ export function makeCancelablePromise<T>(
13
+ promiseOrAsyncFn: Promise<T> | (() => Promise<T>),
14
+ onCancel: () => void,
15
+ ): CancelablePromise<T> {
16
+ let cancel: (() => void) | null = null;
17
+
18
+ const cancelable = <CancelablePromise<T>>new Promise((resolve, reject) => {
19
+ cancel = () => {
20
+ try {
21
+ onCancel();
22
+ reject(new CanceledPromiseError());
23
+ } catch (e) {
24
+ reject(e);
25
+ }
26
+ };
27
+
28
+ const promise =
29
+ typeof promiseOrAsyncFn === 'function'
30
+ ? promiseOrAsyncFn()
31
+ : promiseOrAsyncFn;
32
+
33
+ promise.then(resolve, reject);
34
+ });
35
+
36
+ if (cancel) {
37
+ cancelable.cancel = cancel;
38
+ }
39
+
40
+ return cancelable;
41
+ }
@@ -0,0 +1,52 @@
1
+ import { Rel } from './serialize';
2
+
3
+ type JsonApiEntity = {
4
+ id?: string;
5
+ type?: string;
6
+ attributes?: object;
7
+ relationships?: object;
8
+ meta?: object;
9
+ };
10
+
11
+ type ResponseWithData = {
12
+ data: JsonApiEntity | JsonApiEntity[];
13
+ };
14
+
15
+ function hasData(thing: unknown): thing is ResponseWithData {
16
+ return typeof thing === 'object' && !!thing && 'data' in thing;
17
+ }
18
+
19
+ export function deserializeJsonEntity<S>({
20
+ id,
21
+ type,
22
+ attributes,
23
+ relationships,
24
+ meta,
25
+ }: JsonApiEntity): S {
26
+ return {
27
+ ...(id ? { id } : {}),
28
+ ...(type ? { type } : {}),
29
+ ...(attributes || {}),
30
+ ...(relationships
31
+ ? Object.fromEntries(
32
+ Object.entries(relationships).map(([rel, value]) => [
33
+ rel,
34
+ value?.data,
35
+ ]),
36
+ )
37
+ : {}),
38
+ ...(meta ? { meta } : {}),
39
+ } as S;
40
+ }
41
+
42
+ export function deserializeResponseBody<T>(body: unknown): T {
43
+ if (!hasData(body)) {
44
+ throw new Error('Invalid body!');
45
+ }
46
+
47
+ if (Array.isArray(body.data)) {
48
+ return body.data.map(deserializeJsonEntity) as unknown as T;
49
+ }
50
+
51
+ return deserializeJsonEntity(body.data) as unknown as T;
52
+ }
package/src/index.ts ADDED
@@ -0,0 +1,11 @@
1
+ 'use strict';
2
+
3
+ export * from './ApiError';
4
+ export * from './deserialize';
5
+ export * from './pollJobResult';
6
+ export * from './rawPageIterator';
7
+ export * from './request';
8
+ export * from './serialize';
9
+ export * from './CancelablePromise';
10
+ export * from './toId';
11
+ export * from './wait';
@@ -0,0 +1,8 @@
1
+ export type JobResult = {
2
+ type: 'job_result';
3
+ id: string;
4
+ status: number;
5
+ payload: null | {
6
+ [k: string]: unknown;
7
+ };
8
+ };
@@ -0,0 +1,24 @@
1
+ import { ApiError } from './ApiError';
2
+ import { JobResult } from './internalTypes';
3
+ import { wait } from './wait';
4
+
5
+ export async function pollJobResult(
6
+ fetcher: () => Promise<JobResult>,
7
+ ): Promise<JobResult> {
8
+ let jobResult: JobResult | undefined;
9
+ let retryCount = 0;
10
+
11
+ do {
12
+ try {
13
+ retryCount += 1;
14
+ await wait(retryCount * 1000);
15
+ jobResult = await fetcher();
16
+ } catch (e) {
17
+ if (!(e instanceof ApiError) || e.response.status !== 404) {
18
+ throw e;
19
+ }
20
+ }
21
+ } while (!jobResult);
22
+
23
+ return jobResult;
24
+ }
@@ -0,0 +1,66 @@
1
+ import { Scheduler } from 'async-scheduler';
2
+
3
+ export type IteratorOptions = {
4
+ perPage?: number;
5
+ concurrency?: number;
6
+ };
7
+
8
+ type PaginationOptions = {
9
+ defaultLimit: number;
10
+ maxLimit: number;
11
+ };
12
+
13
+ type JsonApiPage<T> = {
14
+ data: T[];
15
+ meta: { total_count: number };
16
+ };
17
+
18
+ export async function* rawPageIterator<T>(
19
+ pagination: PaginationOptions,
20
+ callPerformer: (page: {
21
+ limit: number;
22
+ offset: number;
23
+ }) => Promise<JsonApiPage<T>>,
24
+ iteratorOptions?: IteratorOptions,
25
+ ) {
26
+ const perPage = iteratorOptions?.perPage || pagination.defaultLimit;
27
+
28
+ if (perPage > pagination.maxLimit) {
29
+ throw new Error(
30
+ `perPage option cannot exceed maximum value of ${pagination.maxLimit}`,
31
+ );
32
+ }
33
+
34
+ const concurrency = iteratorOptions?.concurrency || 1;
35
+
36
+ if (concurrency > 10) {
37
+ throw new Error(`concurrency option cannot exceed maximum value of 10`);
38
+ }
39
+
40
+ const firstResponse = await callPerformer({ limit: perPage, offset: 0 });
41
+
42
+ for (const item of firstResponse.data) {
43
+ yield item;
44
+ }
45
+
46
+ const totalCount = firstResponse.meta.total_count;
47
+
48
+ const limiter = new Scheduler(concurrency);
49
+ const promises: Promise<JsonApiPage<T>>[] = [];
50
+
51
+ for (let offset = perPage; offset < totalCount; offset += perPage) {
52
+ promises.push(
53
+ limiter.enqueue<JsonApiPage<T>, void>(() =>
54
+ callPerformer({ limit: perPage, offset }),
55
+ ),
56
+ );
57
+ }
58
+
59
+ while (promises.length > 0) {
60
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
61
+ const response = await promises.shift()!;
62
+ for (const item of response.data) {
63
+ yield item;
64
+ }
65
+ }
66
+ }
package/src/request.ts ADDED
@@ -0,0 +1,279 @@
1
+ import qs from 'qs';
2
+ import fetch from 'cross-fetch';
3
+ import { ApiError, ApiErrorInitObject } from './ApiError';
4
+ import { JobResult } from './internalTypes';
5
+
6
+ export enum LogLevel {
7
+ /** No logging */
8
+ NONE = 0,
9
+ /** Logs HTTP requests (method, URL) and responses (status) */
10
+ BASIC = 1,
11
+ /** Logs HTTP requests (method, URL, body) and responses (status, body) */
12
+ BODY = 2,
13
+ /** Logs HTTP requests (method, URL, headers, body) and responses (status, headers, body) */
14
+ BODY_AND_HEADERS = 3,
15
+ }
16
+
17
+ type RequestOptions = {
18
+ baseUrl: string;
19
+ fetchJobResult: (jobId: string) => Promise<JobResult>;
20
+ apiToken: string | null;
21
+ extraHeaders?: Record<string, string>;
22
+ logLevel?: LogLevel;
23
+ autoRetry?: boolean;
24
+ retryCount?: number;
25
+ method: 'GET' | 'PUT' | 'POST' | 'DELETE';
26
+ url: string;
27
+ queryParams?: Record<string, unknown>;
28
+ body?: unknown;
29
+ preCallStack?: string;
30
+ userAgent?: string;
31
+ };
32
+
33
+ function headersToObject(headers: Headers): Record<string, string> {
34
+ const result = {};
35
+
36
+ headers.forEach((value, key) => {
37
+ result[key] = value;
38
+ });
39
+
40
+ return result;
41
+ }
42
+
43
+ function buildApiErrorInitObject(
44
+ method: string,
45
+ url: string,
46
+ requestHeaders: Record<string, string>,
47
+ requestBody: unknown,
48
+ response: Response,
49
+ responseBody: unknown,
50
+ preCallStack?: string,
51
+ ): ApiErrorInitObject {
52
+ return {
53
+ request: {
54
+ url,
55
+ method,
56
+ headers: requestHeaders,
57
+ body: requestBody,
58
+ },
59
+ response: {
60
+ status: response.status,
61
+ statusText: response.statusText,
62
+ headers: headersToObject(response.headers),
63
+ body: responseBody,
64
+ },
65
+ preCallStack,
66
+ };
67
+ }
68
+
69
+ function buildApiErrorInitObjectFromJobResult(
70
+ method: string,
71
+ url: string,
72
+ requestHeaders: Record<string, string>,
73
+ requestBody: unknown,
74
+ responseStatus: number,
75
+ responseBody: unknown,
76
+ preCallStack?: string,
77
+ ): ApiErrorInitObject {
78
+ return {
79
+ request: {
80
+ url,
81
+ method,
82
+ headers: requestHeaders,
83
+ body: requestBody,
84
+ },
85
+ response: {
86
+ status: responseStatus,
87
+ statusText: 'N/A',
88
+ headers: {},
89
+ body: responseBody,
90
+ },
91
+ preCallStack,
92
+ };
93
+ }
94
+
95
+ function wait(time) {
96
+ return new Promise((resolve) => {
97
+ setTimeout(resolve, time);
98
+ });
99
+ }
100
+
101
+ function isErrorWithCode(error: unknown): error is { code: string } {
102
+ return typeof error === 'object' && !!error && 'code' in error;
103
+ }
104
+
105
+ let requestCount = 1;
106
+
107
+ export async function request<T>(options: RequestOptions): Promise<T> {
108
+ const requestId = requestCount;
109
+ requestCount += 1;
110
+
111
+ const preCallStack = options.preCallStack;
112
+ const userAgent = options.userAgent || `@datocms/rest-client-utils`;
113
+ const retryCount = options.retryCount || 1;
114
+ const logLevel = options.logLevel || LogLevel.NONE;
115
+ const autoRetry = 'autoRetry' in options ? options.autoRetry : true;
116
+
117
+ const headers = {
118
+ 'content-type': 'application/json',
119
+ accept: 'application/json',
120
+ authorization: `Bearer ${options.apiToken}`,
121
+ 'user-agent': userAgent,
122
+ ...(options.extraHeaders || {}),
123
+ };
124
+
125
+ const baseUrl = options.baseUrl.replace(/\/$/, '');
126
+ const body = options.body ? JSON.stringify(options.body, null, 2) : undefined;
127
+
128
+ const queryString =
129
+ options.queryParams && Object.keys(options.queryParams).length > 0
130
+ ? `?${qs.stringify(options.queryParams, { arrayFormat: 'brackets' })}`
131
+ : '';
132
+
133
+ const url = `${baseUrl}${options.url}${queryString}`;
134
+
135
+ if (logLevel >= LogLevel.BASIC) {
136
+ console.log(`[${requestId}] ${options.method} ${url}`);
137
+ if (logLevel >= LogLevel.BODY_AND_HEADERS) {
138
+ for (const [key, value] of Object.entries(headers || {})) {
139
+ console.log(`[${requestId}] ${key}: ${value}`);
140
+ }
141
+ }
142
+ if (logLevel >= LogLevel.BODY && body) {
143
+ console.log(`[${requestId}] ${body}`);
144
+ }
145
+ }
146
+
147
+ try {
148
+ const response = await fetch(url, {
149
+ method: options.method,
150
+ headers,
151
+ body,
152
+ });
153
+
154
+ if (response.status === 429) {
155
+ if (!autoRetry) {
156
+ throw new ApiError(
157
+ buildApiErrorInitObject(
158
+ options.method,
159
+ url,
160
+ headers,
161
+ options.body,
162
+ response,
163
+ undefined,
164
+ preCallStack,
165
+ ),
166
+ );
167
+ }
168
+
169
+ const waitTimeInSecs = response.headers.has('X-RateLimit-Reset')
170
+ ? // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
171
+ parseInt(response.headers.get('X-RateLimit-Reset')!, 10)
172
+ : retryCount;
173
+
174
+ if (logLevel >= LogLevel.BASIC) {
175
+ console.log(
176
+ `[${requestId}] Rate limit exceeded, waiting ${waitTimeInSecs} seconds...`,
177
+ );
178
+ }
179
+
180
+ await wait(waitTimeInSecs * 1000);
181
+
182
+ return request({ ...options, retryCount: retryCount + 1 });
183
+ }
184
+
185
+ if (logLevel >= LogLevel.BASIC) {
186
+ console.log(
187
+ `[${requestId}] Status: ${response.status} (${response.statusText})`,
188
+ );
189
+ if (logLevel >= LogLevel.BODY_AND_HEADERS) {
190
+ [
191
+ 'content-type',
192
+ 'x-api-version',
193
+ 'x-environment',
194
+ 'x-queue-time',
195
+ 'x-ratelimit-remaining',
196
+ ].forEach((key) => {
197
+ const value = response.headers.get(key);
198
+ if (value) {
199
+ console.log(`[${requestId}] ${key}: ${value}`);
200
+ }
201
+ });
202
+ }
203
+ }
204
+
205
+ let responseBody =
206
+ response.status === 204 ? undefined : await response.json();
207
+
208
+ if (logLevel >= LogLevel.BODY && responseBody) {
209
+ console.log(`[${requestId}] ${JSON.stringify(responseBody, null, 2)}`);
210
+ }
211
+
212
+ if (response.status === 202) {
213
+ const jobResult = await options.fetchJobResult(responseBody.data.id);
214
+
215
+ if (jobResult.status < 200 || jobResult.status >= 300) {
216
+ throw new ApiError(
217
+ buildApiErrorInitObjectFromJobResult(
218
+ options.method,
219
+ url,
220
+ headers,
221
+ options.body,
222
+ jobResult.status,
223
+ jobResult.payload,
224
+ preCallStack,
225
+ ),
226
+ );
227
+ }
228
+
229
+ responseBody = jobResult.payload;
230
+ }
231
+
232
+ if (response.status >= 200 && response.status < 300) {
233
+ return responseBody;
234
+ }
235
+
236
+ const error = new ApiError(
237
+ buildApiErrorInitObject(
238
+ options.method,
239
+ url,
240
+ headers,
241
+ options.body,
242
+ response,
243
+ responseBody,
244
+ preCallStack,
245
+ ),
246
+ );
247
+
248
+ if (
249
+ autoRetry &&
250
+ error.findErrorWithCode('BATCH_DATA_VALIDATION_IN_PROGRESS')
251
+ ) {
252
+ if (logLevel >= LogLevel.BASIC) {
253
+ console.log(
254
+ `[${requestId}] Data validation in progress, waiting ${retryCount} seconds...`,
255
+ );
256
+ }
257
+
258
+ await wait(retryCount * 1000);
259
+
260
+ return request({ ...options, retryCount: retryCount + 1 });
261
+ }
262
+
263
+ throw error;
264
+ } catch (error) {
265
+ if (isErrorWithCode(error) && error.code.includes('ETIMEDOUT')) {
266
+ if (logLevel >= LogLevel.BASIC) {
267
+ console.log(
268
+ `[${requestId}] Error ${error.code}, waiting ${retryCount} seconds...`,
269
+ );
270
+ }
271
+
272
+ await wait(retryCount * 1000);
273
+
274
+ return request({ ...options, retryCount: retryCount + 1 });
275
+ }
276
+
277
+ throw error;
278
+ }
279
+ }